I'm trying to make a simple library for de/serialization in c++, but I know it can be tricky to implement, so I'd really like to have my code reviewed to see if there's anything that stands out and/or can be improved. Here is the full repo containing all the code. Any other necessary code can be found there, as most of it was written by others.
binaryio/reader.h:
#include <memory>
#include <span>
#include <stdexcept>
#include "binaryio/interface.h"
#include "binaryio/swap.h"
#ifndef BINARYIO_READER_H
#define BINARYIO_READER_H
namespace binaryio {
class BinaryReader : IBinaryIO {
public:
BinaryReader(void* const begin, void* const end)
: m_begin(reinterpret_cast<char*>(begin)),
m_end(reinterpret_cast<char*>(end)),
m_current(reinterpret_cast<char*>(begin)){};
BinaryReader(void* const begin, void* const end, const endian& endianness)
: m_begin{reinterpret_cast<char*>(begin)},
m_end{reinterpret_cast<char*>(end)},
m_current{reinterpret_cast<char*>(begin)}, m_endian{endianness} {};
BinaryReader(void* const begin, const std::ptrdiff_t& size)
: m_begin{reinterpret_cast<char*>(begin)}, m_end{m_begin + size},
m_current{reinterpret_cast<char*>(begin)} {};
BinaryReader(void* const begin, const std::ptrdiff_t& size,
const endian& endianness)
: m_begin{reinterpret_cast<char*>(begin)}, m_end{m_begin + size},
m_current{reinterpret_cast<char*>(begin)}, m_endian{endianness} {};
void seek(std::ptrdiff_t offset) override {
if (m_begin + offset > m_end)
throw std::out_of_range("out of bounds seek");
m_current = m_begin + offset;
}
size_t tell() const override {
size_t offset{static_cast<size_t>(m_current - m_begin)};
return offset;
}
template <typename T>
T read() {
if (m_current + sizeof(T) > m_end)
throw std::out_of_range("out of bounds read");
T val = *(T*)m_current;
swap_if_needed_in_place(val, m_endian);
m_current += sizeof(T);
return val;
}
std::string read_string(size_t max_len = 0) {
if (m_current + max_len > m_end || max_len == 0)
max_len = m_end - m_current;
return {m_current, strnlen(m_current, max_len)};
}
template <typename T>
std::span<T> read_many(int count) {
if (m_current + sizeof(T) * count > m_end)
throw std::out_of_range("out of bound read");
std::span<T> vals{{}, count};
for (int i{0}; i < count; ++i) {
vals[i] = *(T*)m_current;
swap_if_needed_in_place(vals[i], m_endian);
m_current += sizeof(T);
}
return vals;
}
endian endianness() { return m_endian; }
void set_endianness(endian new_endian) { m_endian = new_endian; }
void swap_endianness() {
if (m_endian == endian::big)
m_endian = endian::little;
else
m_endian = endian::big;
}
private:
char* m_begin;
char* m_end;
char* m_current;
endian m_endian{endian::native};
};
} // namespace binaryio
#endif
binaryio/writer.h:
#include <memory>
#include <span>
#include <vector>
#include "binaryio/align.h"
#include "binaryio/interface.h"
#include "binaryio/swap.h"
#ifndef BINARYIO_WRITER_H
#define BINARYIO_WRITER_H
namespace binaryio {
class BinaryWriter : IBinaryIO {
public:
// Based on
// https://github.com/zeldamods/oead/blob/master/src/include/oead/util/binary_reader.h
BinaryWriter() = default;
BinaryWriter(endian byte_order) : m_endian{byte_order} {};
std::vector<uint8_t> finalize() { return std::move(m_storage); }
void seek(std::ptrdiff_t offset) override { m_offset = offset; };
size_t tell() const override { return m_offset; }
void write_bytes(const uint8_t* data, size_t size) {
std::span<const uint8_t> bytes{data, size};
if (m_offset + bytes.size() > m_storage.size())
m_storage.resize(m_offset + bytes.size());
std::memcpy(&m_storage[m_offset], bytes.data(), bytes.size());
m_offset += bytes.size();
};
template <typename T,
typename std::enable_if_t<!std::is_pointer_v<T> &&
std::is_trivially_copyable_v<T>>* = nullptr>
void write(T value) {
swap_if_needed_in_place(value, m_endian);
write_bytes(reinterpret_cast<const uint8_t*>(&value), sizeof(value));
}
void write(std::string_view str) {
write_bytes(reinterpret_cast<const uint8_t*>(str.data()), str.size());
}
void write_null() { write<uint8_t>(0); }
void write_cstr(std::string_view str) {
write(str);
write_null();
}
void align_up(size_t n) { seek(AlignUp(tell(), n)); }
private:
std::vector<uint8_t> m_storage;
size_t m_offset{0};
endian m_endian{endian::native};
};
} // namespace binaryio
#endif
And here's a test I wrote using a wav file as a base:
#include <iostream>
#include <fstream>
#include <filesystem>
#include "binaryio/reader.h"
#include "binaryio/writer.h"
struct FileHeader {
uint32_t magic;
uint32_t fileSize;
BINARYIO_DEFINE_FIELDS(FileHeader, magic, fileSize);
};
struct WaveFile {
FileHeader riffHeader;
std::array<uint8_t, 4> magic;
std::array<uint8_t, 4> fmt;
uint32_t fmtSize;
uint16_t audioFormat;
uint16_t numChannels;
uint32_t sampleRate;
uint32_t byteRate;
uint16_t blockAlign;
uint16_t bitsPerSample;
std::array<uint8_t, 4> dataMagic;
uint32_t dataSize;
// Data starts
BINARYIO_DEFINE_FIELDS(WaveFile, riffHeader, magic, fmt, fmtSize, audioFormat, numChannels, sampleRate, byteRate, blockAlign, bitsPerSample, dataMagic, dataSize);
};
int main(int argc, char** argv)
try {
std::vector<uint8_t> bytes(std::filesystem::file_size(argv[1]));
{
std::ifstream ifs{argv[1]};
ifs.read(reinterpret_cast<char*>(bytes.data()), bytes.size());
}
// Read from the byte buffer
binaryio::BinaryReader reader{bytes.begin().base(), bytes.end().base()};
WaveFile wav {reader.read<WaveFile>()};
std::vector<uint8_t> data;
for (int i {sizeof(WaveFile)}; i<bytes.size(); ++i)
data.push_back(reader.read<uint8_t>());
std::cout << "Riff Magic: " << std::hex << wav.riffHeader.magic << std::endl;
std::cout << "Wave Magic: " << wav.magic.data() << std::endl;
// Write a new file
binaryio::BinaryWriter writer{binaryio::endian::little};
writer.write(wav);
for (uint8_t byte : data)
writer.write(byte);
bytes.clear();
bytes = writer.finalize();
{
std::ofstream ofs{"out_little.wav"};
ofs.write(reinterpret_cast<char*>(bytes.data()), bytes.size());
}
// Write a different file with its endianness swapped
writer = {binaryio::endian::big};
writer.write(wav);
for (uint8_t byte : data)
writer.write(byte);
bytes.clear();
bytes = writer.finalize();
{
std::ofstream ofs{"out_big.wav"};
ofs.write(reinterpret_cast<char*>(bytes.data()), wav.riffHeader.fileSize + sizeof(FileHeader));
}
// Read the new file, and compare the result with the original struct
reader = {bytes.begin().base(), bytes.end().base()};
if (reader.read<uint32_t>() == 0x52494646) {
reader.swap_endianness();
reader.seek(0);
}
WaveFile other_wav {reader.read<WaveFile>()};
std::cout << "Riff Magic: " << std::hex << other_wav.riffHeader.magic << std::endl;
std::cout << "Wave Magic: " << other_wav.magic.data() << std::endl;
if (wav.sampleRate == other_wav.sampleRate)
std::cout << "Data preserved, endianness swapped" << std::endl;
else {
throw std::runtime_error("Something went wrong and the data was changed");
}
return 0;
}
catch (std::runtime_error& err) {
std::cerr << err.what() << std::endl;
return 1;
}
```
swap_if_needed_in_place()is significant, yet that's not even declared here. I think we need more of the code to give it a decent review. And as J_H says, show the unit-tests to give us an idea of intended usage. \$\endgroup\$