This repository is StormByte Base: the C++26 foundation of the StormByte suite.
It is the module every other StormByte library links. Public headers live under StormByte/ and cover exceptions, Expected, little-endian serialization, CString / WCString, BinaryData, Size, ByteSize, UUID v4, bitmasks, clonable types, a reentrant ThreadLock, and the StormByte::Type concepts.
The suite is split on purpose. Buffer, Config, Crypto, Database, Logger, Multimedia, Network, String and System are other repositories. They depend on this one; this one does not implement them.
- Exceptions —
StormByte::Exception.what()isStormByte: …, orStormByte.Crypto.Crypter: …when a parent passes the segments underStormByte. The text is aCString. A final leaf adds no segment. - Error —
Domain,Category,CodeandFaultforstd::error_code.Faultis not thrown; its text is aCString. - Expected —
Expected<T, E>on top ofstd::expected. The error is aShared<E>on Base's heap. It converts tostd::shared_ptr<E>.Unexpected<E>("… {}", arg)stays as it is. - Serialization —
Serializable<T>toBinaryData, always little-endian, no BOM and no version tag. Optional / pair / container / trivial /Detail::Codec<T>. On-wire lengths areByteSize. - CString / WCString — owned NUL-terminated narrow and wide buffers, safe to use across a DLL boundary. Not
std::string/std::wstring.Length()isSize. Construct from C string,string_view/wstring_viewandstring/wstring(copy onto Base's heap). Content equality,<=>,swapandstd::hash. - BinaryData — owned contiguous
std::bytesequence, safe to use across a DLL boundary. Same kind of API asstd::vector<std::byte>. Lengths and indices useByteSize.HexDumpprints offset + hex + ASCII; column count isstd::size_t. - Size — abstract unit count (
uint64_tstorage), same width on every host and safe across a DLL. Implicit only tostd::size_t. Character counts, iteration counts, “how many items”. - ByteSize — octet length (
uint64_tstorage). Implicit only tostd::size_t. IEC / SI units (1 * KiB), human-readableCString(1.00 KiB). Area products are deleted. - UUID — RFC 4122 version 4 (
GenerateUUIDv4). - Bitmask — CRTP flags over
Type::UnsignedEnum. - Safe pointers —
Shared<T>,Unique<T>andWeak<T>complementstd::shared_ptr,std::unique_ptrandstd::weak_ptr. They do not replace them: use the standard pointers unless the object must be freed on Base's heap.Sharedconverts implicitly tostd::shared_ptr<T>(deleter stays Base).Uniqueconverts on move only tostd::unique_ptr<T, Heap::ObjectDeleter>. Norelease, and no constructor from a raw or standard pointer.Heapis not installed. - Clonable — polymorphic
Clone/Move. Not an owner: the result is aSharedor aUnique. - ThreadLock — owner-thread reentry;
Unlockfrom a non-owner is a no-op. - Type concepts —
StormByte::Type::*(String,Container,Optional,Pair,Numeral,Array, …).NumeralincludesSizeandByteSize. Noenable_if/void_tnext to them. - Platform / visibility —
WINDOWS/LINUX/MACOS,BIT32/BIT64,CLANG/GCC/MSVC(clang-cl isCLANG, notMSVC).
Public Base APIs do not take or return a raw std::size_t / std::uint64_t when the value is a count. Characters and units are Size. Octets are ByteSize.
| Module | Role | API |
|---|---|---|
| Base | This repository | /StormByte |
| Buffer | FIFO, SharedFIFO, Ring, Producer/Consumer and multi-stage pipelines | /StormByte-Buffer |
| Config | Human-readable text and versioned binary documents (groups, lists, raw bytes) | /StormByte-Config |
| Crypto | Hash, compress, encrypt, sign and key agreement — Crypto++ never leaves the private tree | /StormByte-Crypto |
| Database | One API over SQLite, PostgreSQL and MariaDB | /StormByte-Database |
| Logger | Stream logger with levels, headers, human-readable sizes and redaction (ThreadedLog) |
/StormByte-Logger |
| Multimedia | Decode, encode and containers without raw FFmpeg types; codecs enabled only if present | /StormByte-Multimedia |
| Network | Framed packets, Client/Server, IPv4/IPv6 TCP and Buffer pipelines (compress/encrypt) | /StormByte-Network |
| String | Suite text type and helpers (case, split, UTF-8, human-readable numbers and byte sizes) | /StormByte-String |
| System | Processes, pipes and environment variables across Linux, Windows and macOS | /StormByte-System |
- What this module does
- The rest of the suite
- Installation
- Usage
- Exceptions
- Expected
- Error
- CString / WCString
- BinaryData
- Size
- ByteSize
- Serialization
- UUID
- ThreadLock
- Clonable
- Type concepts
- Bitmask
- Contributing
- License
Needs a C++26 compiler and CMake 3.28 or newer.
git clone https://github.com/StormBytePP/StormByte.git
cd StormByte
cmake -S . -B build
cmake --build buildShared vs static follows CMake BUILD_SHARED_LIBS (declared in lib/, default ON). A plain configure builds the shared library. -DBUILD_SHARED_LIBS=OFF builds a static archive; on Windows the headers then do not use dllimport.
A shared build keeps this library as its own .so / .dll. Under the LGPL that is usually the simpler way to ship: the user can replace that file. A static archive is folded into your binary. The LGPL still applies to this code; you must give the recipient a way to relink your product with a different build of this library. If that does not fit how you distribute the final product, a commercial license is available from the copyright holder (see License).
Headers are #include <StormByte/….hxx>. Namespace root is StormByte.
Base owns the exception system other modules inherit. A throw of Exception reads StormByte: …. A parent passes Exception::Path (a string_view of its segments) and forwards the format and the arguments. It does not format. A bare string is not a path: that would be ambiguous with the format constructor. Exception is the only place that calls std::format, in the caller's translation unit, and copies a const char* into a CString. The view lives for that constructor call and is not stored. A runtime std::string is not a format string.
A final leaf inherits the parent constructors and adds no segment, so EncryptException("bad key {}", id) reads StormByte.Crypto.Crypter: bad key …. DeserializeError, OutOfBoundsError and Base64Error are leaves of the root: StormByte: ….
Each named type defines its destructor in that module's .cxx. That keeps one typeinfo, so catch matches across a DLL.
#include <StormByte/exception.hxx>
#include <iostream>
using namespace StormByte;
class CryptoError: public Exception {
public:
template <typename... Args>
explicit CryptoError(std::format_string<Args...> fmt, Args&&... args)
: Exception(Path{"Crypto"}, fmt, std::forward<Args>(args)...) {}
~CryptoError() override;
protected:
template <typename... Args>
explicit CryptoError(Path child, std::format_string<Args...> fmt, Args&&... args)
: Exception(Path{std::string("Crypto.") + std::string(child.text)}, fmt, std::forward<Args>(args)...) {}
};
class CrypterError: public CryptoError {
public:
template <typename... Args>
explicit CrypterError(std::format_string<Args...> fmt, Args&&... args)
: CryptoError(Path{"Crypter"}, fmt, std::forward<Args>(args)...) {}
~CrypterError() override;
};
class EncryptError: public CrypterError {
public:
using CrypterError::CrypterError;
~EncryptError() override;
};
void process_data(int value) {
if (value < 0)
throw Exception("Invalid value: {}", value);
}
int main() {
try {
process_data(-5);
} catch (const Exception& e) {
std::cerr << e.what() << std::endl; // StormByte: Invalid value: -5
}
}~CryptoError, ~CrypterError and ~EncryptError are defined in the module .cxx (= default is enough).
The error is a Shared<E> on Base's heap. Read it with result.error()->what(). It converts to std::shared_ptr<E> when a signature already asks for one. The call does not change: Unexpected<E>("Password '{}' not found", name) formats in the caller and constructs E from that string. Unexpected(result.error()) forwards the same Shared and does not allocate. A std::shared_ptr is not accepted.
#include <StormByte/expected.hxx>
#include <StormByte/exception.hxx>
#include <iostream>
using namespace StormByte;
Expected<int, Exception> divide(int a, int b) {
if (b == 0)
return Unexpected<Exception>("Division by zero");
return a / b;
}Fault wraps a std::error_code. Across a DLL use Fault::what() (CString), not error_code::message().
#include <StormByte/error.hxx>
#include <iostream>
#include <system_error>
using namespace StormByte;
int main() {
const std::error_code code = Error::Code::Unknown;
const Error::Fault fault{code};
if (fault)
std::cerr << fault.what() << std::endl;
}A module adds its own enum, specializes Error::Domain, and puts make_error_code next to the enum so ADL fills std::error_code. The category singleton lives in that module’s .cxx.
Owned buffers. operator bool is true when the pointer is not null: "" / L"" are valid empty text; a default-constructed object is null.
Construct from const char* / const wchar_t* (null stays null), from std::string_view / std::wstring_view, and from const std::string& / const std::wstring&. Those last two copy onto Base's heap. They are not a heap steal. An empty string / view yields "" / L"", not a null buffer.
Length() returns Size (character count, not octets). operator[] takes Size.
== / != / <=> compare text, not addresses. Two nulls are equal; null is not equal to "" / L"" and orders before any text. swap exchanges buffers. std::hash hashes the text (0 when null), so the types work in std::set and std::unordered_set.
explicit operator const char* / const wchar_t* has the same lifetime as std::string::c_str() / std::wstring::c_str(). Implicit std::string / std::wstring and operator<< are inline (caller CRT).
#include <StormByte/cstring.hxx>
#include <StormByte/size.hxx>
#include <StormByte/wcstring.hxx>
#include <iostream>
#include <set>
#include <string>
using namespace StormByte;
int main() {
CString text("hello");
if (text)
std::cout << text << " " << static_cast<std::size_t>(text.Length()) << std::endl;
CString from_std{std::string("hello")};
if (text == from_std && text == "hello")
std::cout << "same text" << std::endl;
text.Reset();
if (!text)
std::cout << "null" << std::endl;
CString empty("");
if (empty && empty.Length() == Size{0} && text < empty)
std::cout << "empty but valid" << std::endl;
std::set<CString> ordered{CString("b"), CString("a")};
WCString wide(L"wide");
std::wcout << wide << std::endl;
}BinaryData is the suite’s owned raw-byte container. Use it wherever a module would otherwise put std::vector<std::byte> in a public signature.
std::vector is not a safe ABI type between two copies of a C++ runtime. A vector allocated in the application and grown, returned or destroyed inside a StormByte shared library (or the other way around) uses two heaps. On Windows that is a hard crash when CRTs differ; on Unix it fails when libc++ and libstdc++ mix.
BinaryData owns its storage on StormByte Base’s heap. Construction, growth and destruction always run in this library. Other suite modules can carry payloads, encoded blobs, file images or wire fragments without exporting std::vector<std::byte>.
It is not text (CString) and not a structured document. Lengths and indices are StormByte::ByteSize. Member names stay lowercase to match the STL.
For <algorithm> and std::ranges it supports everything std::vector<std::byte> supports on a contiguous sequence of bytes: copy / transform / sort / reverse / rotate / unique / remove / replace / partition / heap / set operations / binary search / permutations, plus iterators, std::span and insert / erase / assign / append / operator+= / emplace. std::iota is the exception that is also true of std::vector<std::byte>: std::byte is an enum class and has no operator++.
at() throws OutOfBoundsError. operator[] is unchecked, like std::vector, and takes ByteSize.
Compare with another BinaryData or with std::span<const std::byte> (==, !=, <=>, both operand orders).
Hex dump. HexDump() and HexDump(std::size_t columns) return a CString. Each line is an 8-digit offset, a row of hex bytes, and the same bytes as ASCII (non-printable as .). columns is a row width, not a byte length — it is std::size_t, not ByteSize. 0 prints every byte on one line. The default is 16 columns.
std::vector and std::span. You can build a BinaryData from a span or from a caller-owned vector. You can view the bytes as a span (implicit). You can copy them out to a vector (explicit operator std::vector<std::byte>). The rvalue overloads look like a move: the source is emptied after the copy. They are not a heap steal. Base cannot donate its pointer to a foreign vector, and it cannot adopt a caller vector pointer. Peak usage is two copies during the transfer.
append(BinaryData&&) / operator+=(BinaryData&&) is different: both sides live on Base’s heap, so that move is real when *this is empty.
Serializable<BinaryData> uses the container path. The wire is the same as std::vector<std::byte>: uint64 little-endian count, then the payload.
#include <StormByte/binary_data.hxx>
#include <StormByte/byte_size.hxx>
#include <StormByte/serializable.hxx>
#include <algorithm>
#include <iostream>
#include <ranges>
#include <span>
#include <vector>
using namespace StormByte;
int main() {
BinaryData payload{std::byte{0xDE}, std::byte{0xAD}, std::byte{0xBE}, std::byte{0xEF}};
payload.push_back(std::byte{0x00});
payload += payload.span().first(2);
std::ranges::reverse(payload);
std::sort(payload.begin(), payload.end());
if (!payload.empty())
payload.front() = std::byte{0x01};
const ByteSize n = payload.size();
const std::size_t host = n;
std::cout << host << std::endl;
std::cout << payload.HexDump(8) << std::endl;
std::vector<std::byte> caller = static_cast<std::vector<std::byte>>(payload);
BinaryData back{std::move(caller)};
BinaryData extra{std::byte{0xFF}};
back += std::move(extra);
auto blob = Serializable<BinaryData>(back).Serialize();
auto loaded = Serializable<BinaryData>::Deserialize(blob);
if (loaded)
std::cout << (loaded.value() == back) << std::endl;
}#include <StormByte/binary_data.hxx>
#include <algorithm>
#include <array>
using namespace StormByte;
BinaryData from_range() {
const std::array<unsigned char, 4> raw{1, 2, 3, 4};
BinaryData data(raw);
data.insert(data.begin() + 1, std::byte{9});
data.erase(data.begin() + 2);
return data;
}Size is an abstract unit count, not an octet length. Storage is uint64_t, the same width on 32-bit and 64-bit hosts, and safe to return across a DLL.
Use it for “how many characters”, “how many items”, “how many steps”. Octet lengths belong to ByteSize.
Implicit conversion exists only to std::size_t (clamped to size_t::max). Every other integral destination is explicit and clamps to T::max. There is no Value() and no operator bool.
Size{100} is valid. A negative integer is undefined and asserts when assertions are on.
All arithmetic with another Size or with any Type::Integral yields Size. Mixed == / <=> with integers and with ByteSize compare the numeric counts. std::size_t n = size_a + 3 * size_b; works because the sum is a Size and that converts implicitly.
operator CString / operator WCString print the raw count.
#include <StormByte/cstring.hxx>
#include <StormByte/size.hxx>
#include <iostream>
using namespace StormByte;
int main() {
const Size chars{5};
const Size more = chars + 3;
const std::size_t host = more * 2;
if (chars == 5 && 5 == chars)
std::cout << static_cast<CString>(more) << " " << host << std::endl;
}ByteSize is an octet length. Storage is uint64_t, the same width on every host, and safe to return across a DLL.
Implicit conversion exists only to std::size_t (clamped). Every other integral destination is explicit. There is no Value() and no operator bool.
Area products (ByteSize * ByteSize) are deleted: two lengths do not make a length. Scaling by a Size or by an integer is allowed and yields ByteSize.
IEC factories live on the type (ByteSize::KiB(1)). Free constants live in StormByte so 1 * KiB and 2 * MiB work after using namespace StormByte. SI constants (KB…EB) are the same pattern.
operator CString / operator WCString print IEC text: 0 B, 1023 B, 1.00 KiB, 1.50 MiB. Only the B unit stays without decimals.
#include <StormByte/byte_size.hxx>
#include <StormByte/cstring.hxx>
#include <iostream>
using namespace StormByte;
int main() {
const ByteSize chunk = 4 * MiB + 512 * KiB;
const ByteSize twice = chunk * 2;
const ByteSize pieces = twice / 1024;
const ByteSize leftover = twice % 1024;
const std::size_t host = chunk;
std::cout << chunk << std::endl;
std::cout << static_cast<CString>(twice) << " " << pieces << " " << leftover << std::endl;
std::cout << host << std::endl;
if ((1 * KiB) == ByteSize{1024} && chunk > 1 * MiB)
std::cout << "units" << std::endl;
}Wire is little-endian. Serialize() returns BinaryData. Deserialize reads a prefix; leftover bytes stay with the caller. Custom types specialize StormByte::Detail::Codec<T> (Size returns ByteSize / Write / Read), not Serializable<T>.
BinaryData is a Type::Container of std::byte. No Codec specialization is required; the container path writes the same layout as std::vector<std::byte>.
#include <StormByte/serializable.hxx>
#include <iostream>
#include <string>
#include <vector>
using namespace StormByte;
int main() {
int number = 42;
auto blob = Serializable<int>(number).Serialize();
auto back = Serializable<int>::Deserialize(blob);
if (back)
std::cout << back.value() << std::endl;
std::string text = "Hello, World!";
auto sblob = Serializable<std::string>(text).Serialize();
auto sback = Serializable<std::string>::Deserialize(sblob);
std::vector<int> numbers{1, 2, 3};
auto vblob = Serializable<std::vector<int>>(numbers).Serialize();
auto vback = Serializable<std::vector<int>>::Deserialize(vblob.span());
}wstring / u16string / u32string travel as uint64 UTF-8 length + UTF-8 bytes. Host wchar_t width never appears on the wire.
#include <StormByte/uuid.hxx>
#include <iostream>
int main() {
std::cout << StormByte::GenerateUUIDv4() << std::endl;
}The owner may Lock() again. Another thread blocks. Unlock() from a non-owner does nothing.
Shared<T>, Unique<T> and Weak<T> (safe_pointers.hxx) complement the standard smart pointers. They do not replace them. Use std::shared_ptr, std::unique_ptr and std::weak_ptr when the object does not cross a DLL. Use these when the object must be freed on Base's heap. The heap implementation is private and is not installed.
Heap::MakeShared<T>(args…) / Heap::MakeUnique<T>(args…) construct T. MakePointer<Derived> constructs a derived object and owns it as the base. For Unique, ~Base must be virtual in that case. There is no constructor from a raw pointer or from a standard smart pointer, and Unique has no release.
The daily operations match the standard ones, so a port is a signature change. Shared also converts implicitly to std::shared_ptr<T> and keeps Base's deleter, so a parameter that is already std::shared_ptr<T> does not have to change. There is no conversion back. Unique converts on move only to std::unique_ptr<T, Heap::ObjectDeleter>. A std::unique_ptr<T> parameter has to change. Weak is built from a Shared, and lock returns a Shared.
Clonable is not an owner and it is not a smart pointer. Shared and Unique own the object. Clonable is the polymorphic interface: from a base you can Clone or Move and get the dynamic type back, without naming the derived class. MakePointer forwards to Shared::MakePointer or Unique::MakePointer, so the allocation is written once.
Clonable<T> stores a Shared<T>. Clonable<T, Unique<T>> stores a Unique<T>. std::shared_ptr and std::unique_ptr are not accepted as that parameter. ~T is virtual because Clone and Move are.
#include <StormByte/clonable.hxx>
#include <memory>
using namespace StormByte;
class Shape : public Clonable<Shape> {
public:
PointerType Clone() const override {
return MakePointer<Shape>(*this);
}
PointerType Move() override {
return MakePointer<Shape>(std::move(*this));
}
};
class Token : public Clonable<Token, Unique<Token>> {
public:
PointerType Clone() const override {
return MakePointer<Token>(*this);
}
PointerType Move() override {
return MakePointer<Token>(std::move(*this));
}
};
void use(const Shape& shape) {
Shape::PointerType copy = shape.Clone();
std::shared_ptr<Shape> as_std = copy;
(void)as_std;
}#include <StormByte/binary_data.hxx>
#include <StormByte/byte_size.hxx>
#include <StormByte/size.hxx>
#include <StormByte/type_traits.hxx>
#include <string>
#include <vector>
#include <optional>
using namespace StormByte;
static_assert(Type::String<std::string>);
static_assert(Type::Container<std::vector<int>>);
static_assert(Type::Container<BinaryData>);
static_assert(Type::Sized<BinaryData>);
static_assert(Type::Numeral<Size>);
static_assert(Type::Numeral<ByteSize>);
static_assert(Type::Optional<std::optional<int>>);Type::Detail::swap_endian always reverses bytes. Serializable decides when to call it (host not little-endian).
Needs an unsigned scoped enum. Operators return the derived CRTP type. Helpers are Add, Remove, Has, HasAny, HasNone, Value (not Any / None).
#include <StormByte/bitmask.hxx>
using namespace StormByte;
enum class MyFlags : uint8_t { FlagA = 0x01, FlagB = 0x02 };
class MyBitmask : public Bitmask<MyBitmask, MyFlags> {
public:
using Bitmask<MyBitmask, MyFlags>::Bitmask;
};Issues and pull requests belong on this repository. Fork and open a PR against master.
Read CONTRIBUTING.md before you send a patch (copyright assignment and review rules). Coding rules are in CODING_STYLE.md.
Since 2.0.0, original source in this repository is dual-licensed: GNU Lesser General Public License v3 or later, or a commercial license from the copyright holder (David C. Manuelda StormByte@gmail.com).
The grant applies only to original StormByte source in this repository. It does not cover other StormByte modules or third-party material shipped here (including everything under thirdparty/), which remains under its own license. Neither license grants patent rights.
See LICENSE for the dual-license notice and COPYING.LGPLv3 for the full GNU LGPL version 3 text. Also https://www.gnu.org/licenses/lgpl-3.0.html.
Static linking under the LGPL is described under Installation.
StormByte is developed in spare time. Sponsorship is optional and does not buy features, priority or support.