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, Safe::String / Safe::WString, opaque Safe::Iterable, its Safe::Vector / Safe::Map aliases, Safe::Pair, Safe::Optional and Safe::Queue, BinaryData, Size, ByteSize, UUID v4, bitmasks, DLL-safe owners and clonable types (StormByte::Safe), a reentrant ThreadLock, and the StormByte::Type concepts.
The suite is split on purpose. Buffer, Config, Crypto, Database, Logger, Multimedia, Network 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 aSafe::String. A final leaf adds no segment. - Error —
Domain,Category,CodeandFaultforstd::error_code.Faultis not thrown; its text is aSafe::String. - Expected —
Expected<T, E>on top ofstd::expected. The error is aSafe::Shared<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. - Safe::String / Safe::WString — owned UTF-8 and wide text with private PIMPL
std::string/std::wstringstorage allocated and destroyed in Base's CRT. View inputs are copied with their full length, including embedded NUL code units.Bytes()returns a non-owningconst char*/const wchar_t*. - 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-readableSafe::String(1.00 KiB). Area products are deleted. - UUID — RFC 4122 version 4 (
GenerateUUIDv4). - Bitmask — CRTP flags over
Type::UnsignedEnum. - Safe pointers —
Safe::Shared<T>,Safe::Unique<T>andSafe::Weak<T>(StormByte/safe/pointers.hxx) 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, Safe::Heap::ObjectDeleter>. Norelease, and no constructor from a raw or standard pointer. TheHeapimplementation is not installed. - Clonable —
Safe::Clonable(StormByte/safe/clonable.hxx), polymorphicClone/Move. Not an owner: the result is aSharedor aUnique. Safe to derive from in another DLL. - 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 |
| 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
- Safe text and buffers
- Safe DLL boundaries
- BinaryData
- Size
- ByteSize
- Serialization
- UUID
- ThreadLock
- Clonable
- Type concepts
- Bitmask
- Telemetry
- 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. Formatting happens in the caller's translation unit; the result is copied into a Safe::String. Plain text constructors accept std::string_view or const Safe::String&; neither view nor caller storage is retained.
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: ….
AllocationError reports allocation failure with a static message and a non-allocating default constructor. ExpiredWeakPointerError reports promotion of an empty or expired Safe::Weak. OperationError wraps other foreign failures. Each named exception has its destructor defined in Base. Safe::Heap::Allocate, Safe pointer factories, text buffer allocation and named clock creation translate the identified standard failures to StormByte exceptions. Factories preserve an existing StormByte exception's dynamic type. Inside an active exception handler, modules can call Safe::Heap::RethrowException() to preserve a StormByte exception, translate std::bad_alloc to AllocationError, or translate another foreign exception to OperationError.
This translation is a boundary policy, not a guarantee about arbitrary STL operations or caller-owned conversions. Modules must translate foreign exceptions at their own throwing API boundaries and contain them in noexcept paths. Throwing from a noexcept API still terminates; changing the exception type does not change that contract.
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 Safe::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() (backed by Safe::String), 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.
Safe::String and Safe::WString are the owned text types used by Base APIs (<StormByte/safe/string.hxx> and <StormByte/safe/wstring.hxx>). Each owns an opaque PIMPL containing std::string / std::wstring; allocation, size-changing modifiers and destruction run inside Base's CRT. No caller STL allocation or allocator state is adopted or exposed across the DLL boundary.
Construct from const char* / const wchar_t* for NUL-terminated input (null stays null), or from std::string_view / std::wstring_view for length-bearing input. Views copy every code unit, including embedded NULs. Pass a view of an STL string rather than transferring its storage. An empty view creates valid empty text; a default-constructed object is null, and operator bool distinguishes those states.
Their size() / length() observers return Size and count the complete stored sequence, not the first C-string prefix. Implicit view conversion, ranges, comparisons, hashing and explicit STL-string exports use the full stored length. Bytes() returns a borrowed NUL-terminated const char* / const wchar_t*; a C API that ignores length will stop at the first embedded NUL. Borrowed pointers, views and iterators must not outlive their owner or an operation that invalidates its storage. Explicit conversion to std::string / std::wstring allocates in the caller's CRT.
capacity() / reserve(Size) count UTF-8 bytes or wide code units excluding the trailing terminator. Reserve never shrinks. Mutable contiguous ranges support in-place algorithms over existing code units; size-changing modifiers (assign, append, +=, insert, erase, replace, resize, push_back, pop_back, clear) operate on the private Base-owned STL storage and retain reserved capacity.
#include <StormByte/safe/string.hxx>
#include <StormByte/size.hxx>
#include <StormByte/safe/wstring.hxx>
#include <iostream>
#include <string>
#include <string_view>
using namespace StormByte;
using namespace StormByte::Safe;
int main() {
String text("hello");
if (text)
std::cout << text << " " << static_cast<std::size_t>(text.size()) << std::endl;
String full{std::string_view{"a\0b", 3}};
std::string caller_copy = static_cast<std::string>(full);
std::cout << caller_copy.size() << std::endl;
WString wide{std::wstring_view{L"wide"}};
std::wcout << wide << std::endl;
}Safe::Iterable<Container> is the common opaque owner for Safe sequence and ordered-map adapters. Its sequence specialization provides random-access proxy iterators for classic <algorithm> and std::ranges; its map specialization provides ordered bidirectional proxies with immutable keys, writable mapped values, and Safe::Pair entry values. Safe::Vector<T> and Safe::Map<K, V> are aliases of those specializations. Safe::Optional<T> is a zero-or-one-element range backed by the sequence adapter. It supports direct Safe-value and enum construction/assignment, std::nullopt, value_or, comparisons, in-place construction, swap, and the and_then / transform / or_else operations. Mutable operator* returns an Optional-specific callback proxy: conversion reads a value copy and assignment writes through the creator callback. Const operator* and value() return copies. operator-> calls const members through a caller-owned snapshot; the snapshot pointer is valid only for the full expression, and changes are not written back. Constructing an empty Optional may allocate its callback owner and therefore may throw. Safe::Vector exposes insertion/emplacement, removal, resize and capacity requests when supported by its underlying sequence. Safe::Map adds comparator-aware ordered bounds, insertion, range erase and arrow proxies; arrow entries own their snapshot and mapped writes still use callbacks. Map iterators retain key identity across insertions or erasure of other keys; erasing the iterated key invalidates that iterator. A moved-from map with a stateful comparator cannot be reused or exported because retaining that comparator state would require retaining foreign allocator state; those operations are rejected rather than changing its ordering. Iterators never expose owner-module nodes or references. Safe::Queue<T> preserves FIFO push / pop operations while exposing random-access iterators for <algorithm> and std::ranges. Mutable iterators and front / back use callback-backed proxies; they read caller-owned snapshots and write through to creator-owned deque storage without exposing node references. Structural mutations invalidate iterators and proxies. erase supports erase-remove workflows, and deserialization rejects counts above 1,048,576 to bound resource use.
Each adapter has an explicit constructor from its corresponding STL container and an explicit conversion back. Lvalue imports copy elements. Rvalue imports move elements and leave the source container valid and empty, but never adopt its allocation or allocator state. Export is STORMBYTE_FORCE_INLINE, so the returned std::vector, std::map, std::optional or std::queue is allocated and destroyed in the caller's CRT.
Safe::String::Split and Safe::WString::Split can fill a Safe::Vector; their Explode counterparts can fill a Safe::Queue. These overloads return Safe::Status and replace the destination only on success. Existing caller-local std::vector / std::queue overloads remain available. Use the Safe overload when tokens need to cross a DLL boundary.
These types require a compatible C++ ABI, packing and calling convention, and their creator module plus Base must remain loaded until all instances are destroyed. They isolate container allocations across CRTs; they do not make C++ templates independent of toolchain ABI.
#include <StormByte/safe/queue.hxx>
#include <StormByte/safe/string.hxx>
#include <vector>
using namespace StormByte;
std::vector<Safe::String> source{Safe::String("one"), Safe::String("two")};
Safe::Vector<Safe::String> safeValues(source);
std::vector<Safe::String> callerValues = static_cast<std::vector<Safe::String>>(safeValues);
Safe::Queue<Safe::String> tokens;
const auto status = Safe::String("a|b").Explode('|', tokens);StormByte::Type::IsSafe<T> means Base recognizes the type and backs its documented DLL-boundary contract. The guarantee is conditional on compatible C++ ABI, packing and calling convention, and keeping Base and every provider module loaded while values, owners or callbacks remain alive. It does not promise ABI independence from the compiler, standard library, or STL implementation.
StormByte::Type::MaybeSafe<T> means the type is admitted under explicit provider responsibility. Consumers must not specialize IsSafe or IsMaybeSafe directly. After a complete consumer type is declared, register it at global namespace scope with STORMBYTE_DECLARE_MAYBE_SAFE(fully::qualified::Type). Base checks the operations needed by the selected Safe wrapper and propagates the level through known Safe compositions: all-IsSafe components remain IsSafe; a composition containing a MaybeSafe component is MaybeSafe.
The registration is an assertion, not reflection or proof. C++ cannot inspect a class's private fields or determine whether a destructor or special member is defined out-of-line. The provider must ensure that owned resources are copied, moved, assigned and destroyed with the allocator and module that own them. For resource-bearing classes crossing a DLL, define the relevant constructors, assignments and destructor out-of-line in the provider module; keep its ABI compatible and its module loaded. Base can reject known incompatible standard-library values even if someone tries to register them: raw pointers/references, standard containers and strings/views, standard smart pointers, std::function, and standard tuple/optional/variant wrappers. Use the corresponding Safe type instead. The veto applies recursively through Safe::Shared, Safe::Unique, Safe::Weak and Safe collections. Base cannot discover a banned member hidden inside a user class; that remains part of the provider's registration responsibility.
Safe::Vector, Safe::Map, Safe::Optional, Safe::Queue and Safe::Pair accept admitted SafeValues. A MaybeSafe element must also meet the construction, copy, assignment and movement requirements of the particular wrapper. Safe::Unique<T> and Safe::Weak<T> are not collection values; ownership classification does not make a move-only owner copyable. Safe::Shared<T> and Safe::Unique<T> recurse into T for their classification and do not certify its fields or behavior. Safe::Shared still wraps std::shared_ptr, so compatible STL ABI is required. StormByte::Expected remains an alias of std::expected and is MaybeSafe only when its contained types are admitted; it also requires compatible STL ABI.
StormByte::Exception is IsSafe: its message is Safe::String, its virtual destructor is defined in Base, and producer/consumer tests verify cross-DLL catching. Derived exceptions are MaybeSafe; define each named derived destructor out-of-line in its owning module so its RTTI/vtable has a module anchor. Safe::Function lets typed callbacks propagate StormByte::Exception; non-Safe exceptions are caught and become Status::Failure.
Safe::Callback and Safe::Function<Signature> own callback contexts in the provider module and are copyable as well as movable. Copying invokes the provider's noexcept Clone function to create an independent context; a null result reports failure through StormByte::Exception. Release remains in the provider through a noexcept function. Typed Function arguments are passed by value or as const lvalue references borrowed for the duration of the call. Raw pointers, mutable references and types outside the IsSafe/MaybeSafe contracts are rejected.
Void callbacks return Safe::Status from Call. Value-returning signatures use an explicit output parameter; the callback writes to a temporary and publishes it only on Success:
using Progress = StormByte::Safe::Function<void(double)>;
using SelectSize = StormByte::Safe::Function<StormByte::Size(StormByte::Size)>;
Progress progress(context, &InvokeProgress, &CloneContext, &ReleaseContext);
const auto status = progress.Call(37.5);
SelectSize select(context, &InvokeSelect, &CloneContext, &ReleaseContext);
StormByte::Size selected{};
const auto selectStatus = select.Call(selected, StormByte::Size{80});Invoke returns Status. A thrown StormByte::Exception crosses unchanged; any other exception becomes Status::Failure. Provider release callbacks must not throw. The default C++ calling convention and a compatible C++ ABI are required. The provider must keep its module loaded while callbacks exist; synchronization, reentrancy and avoiding self-destruction during invocation remain provider responsibilities.
StormByte::Safe::Owner (declared in StormByte/safe/owner.hxx) is a copyable opaque state owner. The state is created by the provider; copying invokes its Clone callback there, and destroying/replacing invokes Destroy there. A null clone reports copy failure through StormByte::Exception; Clone and Destroy callbacks must be noexcept, and Destroy must release the state with its creating module's allocator. Move transfers ownership and empties the source. The provider and Base must remain loaded for every live owner.
Owner::Get() is only a borrowed state pointer for the typed provider facade to interpret. It is invalid after that owner is moved from, replaced or destroyed; never retain the pointer independently. Owner cannot inspect the object's members or prove that the callbacks are correct, so it is MaybeSafe, not an unconditional certificate. Store it in a Safe collection when an opaque owner value is useful; expose typed operations from the provider facade rather than treating an arbitrary void* as a checked object handle.
An Owner holding a pointer to a registry object owns only its callback state, not the pointed-to object. The provider must either retain a lifetime token that keeps the referent valid, or document the registry lifetime and the exact invalidation event. Copies must preserve that token or reference contract; Get() never extends a borrowed referent's lifetime.
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 (Safe::String) 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(Size columns) return a Safe::String. 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. 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 Safe::String / operator Safe::WString print the raw count.
#include <StormByte/safe/string.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<Safe::String>(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 Safe::String / operator Safe::WString 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/safe/string.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<Safe::String>(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>.
Built-in generic serialization supports std::optional and Safe::Optional with identical presence/value framing, pair-like values including Safe::Pair, iterable containers including Safe::Vector and Safe::Map, and FIFO queues including Safe::Queue (count followed by values in pop order). Safe collection iterators are snapshotted into their declared value_type; decoding uses each type's public insertion API. Safe::Shared, Safe::Unique, Safe::Weak, Safe::Callback and Safe::Clonable are not generically serializable: pointer identity, callback context and dynamic ownership have no portable value encoding.
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> live in StormByte::Safe (StormByte/safe/pointers.hxx) and 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.
Safe::Heap::MakeShared<T>(args…) / Safe::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, Safe::Heap::ObjectDeleter>. A std::unique_ptr<T> parameter has to change. Weak is built from a Shared, and lock returns a Shared.
Safe::Clonable (StormByte/safe/clonable.hxx) 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.
A class in another DLL may derive from Clonable. Storage always goes through Base's exported heap, and the Safe types carry STORMBYTE_PUBLIC_TYPE so their typeinfo and vtables have default visibility on Linux and macOS: every module agrees on typeid and dynamic_cast, even when the deriving module builds with -fvisibility=hidden. Export T from its own module (class MYLIB_PUBLIC Shape : public Safe::Clonable<Shape>). The module that defines the dynamic type must stay loaded while any of its objects exist.
#include <StormByte/safe/clonable.hxx>
#include <memory>
using namespace StormByte::Safe;
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;
};Telemetry is the derive-and-extend session object for operations across the StormByte suite. Named clocks aggregate independent samples; a thread-safe drawer finds the aggregate, and each sample owns its own start time. Concurrent and nested samples with the same name cannot replace or stop one another. Modules add their own domain counters and metrics.
#include <StormByte/telemetry.hxx>
using namespace StormByte;
class MyTelemetry final : public Telemetry {
public:
void TrackJob() {
auto sample = MeasureClock("job");
// ... perform work ...
(void)sample.Stop();
}
operator Safe::String() const override {
return Safe::String(std::string_view(std::string("job_count=") + std::to_string(Clock("job").Count())));
}
};Clock::Sample is move-only and records exactly once, on explicit Stop() or destruction. Its token may move to the thread that completes it; do not concurrently access one token from multiple threads. Independent tokens from the same named clock may overlap freely. Clock::GetValues() returns Count, cumulative Time and MeanDuration from one coherent snapshot. There is no shared-clock Start() / Stop() pair: use Clock::Measure() or Telemetry::MeasureClock(name) so every stop belongs to its own start.
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.