Skip to content

CBOR (Concise Binary Object Representation)

Glaze provides comprehensive support for CBOR (RFC 8949), a binary data serialization format designed for small message size and extensibility. CBOR is an IETF standard that provides a self-describing binary format, making it ideal for interoperability with other languages and systems.

Basic Usage

Write CBOR

#include "glaze/cbor.hpp"

my_struct s{};
std::string buffer{};
auto ec = glz::write_cbor(s, buffer);
if (!ec) {
   // Success: ec.count contains bytes written
}

Read CBOR

my_struct s{};
auto ec = glz::read_cbor(s, buffer);
if (!ec) {
   // Success
}

[!NOTE] Reading CBOR is safe for invalid input and does not require null-terminated buffers.

Exceptions Interface

If you prefer exceptions over error codes:

#include "glaze/cbor/cbor_exceptions.hpp"

try {
   my_struct s{};
   auto buffer = glz::ex::write_cbor(s);
   glz::ex::read_cbor(s, buffer);
}
catch (const std::runtime_error& e) {
   // Handle error
}

Standards Compliance

Glaze CBOR implements the following standards:

Standard Description
RFC 8949 CBOR core specification
RFC 8746 Typed arrays and multi-dimensional arrays
IANA CBOR Tags Registered semantic tags

Typed Arrays (RFC 8746)

Glaze automatically uses RFC 8746 typed arrays for contiguous numeric containers, enabling bulk memory operations for maximum performance.

std::vector<double> values = {1.0, 2.0, 3.0, 4.0, 5.0};
std::string buffer{};
glz::write_cbor(values, buffer);  // Uses typed array with bulk memcpy

Supported Typed Array Tags

Type Little-Endian Tag Big-Endian Tag
uint8_t 64 64
uint16_t 69 65
uint32_t 70 66
uint64_t 71 67
int8_t 72 72
int16_t 77 73
int32_t 78 74
int64_t 79 75
float 85 81
double 86 82

Glaze automatically selects the correct tag based on the native endianness of the system. When reading, Glaze performs automatic byte-swapping if the data was written on a system with different endianness.

Multi-Dimensional Arrays (RFC 8746)

Glaze supports RFC 8746 multi-dimensional arrays using semantic tags:

Tag Description
40 Row-major multi-dimensional array
1040 Column-major multi-dimensional array

Eigen Matrix Support

Glaze provides native CBOR support for Eigen matrices and vectors:

#include "glaze/ext/eigen.hpp"

// Fixed-size matrix
Eigen::Matrix3d m;
m << 1, 2, 3,
     4, 5, 6,
     7, 8, 9;

std::string buffer{};
glz::write_cbor(m, buffer);

Eigen::Matrix3d result;
glz::read_cbor(result, buffer);
// Dynamic matrix
Eigen::MatrixXd m(3, 4);
// ... fill matrix ...

std::string buffer{};
glz::write_cbor(m, buffer);

Eigen::MatrixXd result;
glz::read_cbor(result, buffer);  // Automatically resizes

The CBOR format for matrices is:

tag(40 or 1040) [
   [rows, cols],      // dimensions array
   typed_array(data)  // data as RFC 8746 typed array
]

  • Tag 40 is used for row-major matrices
  • Tag 1040 is used for column-major matrices (Eigen default)

This format is self-describing and interoperable with other CBOR implementations.

Complex Numbers

Glaze supports complex numbers using IANA-registered CBOR tags:

Tag Description Format
43000 Single complex number [real, imag]
43001 Complex array Interleaved typed array [r0, i0, r1, i1, ...]

See: IANA CBOR Tags Registry

Single Complex Numbers

std::complex<double> c{1.0, 2.0};
std::string buffer{};
glz::write_cbor(c, buffer);

std::complex<double> result;
glz::read_cbor(result, buffer);
// result.real() == 1.0, result.imag() == 2.0

The CBOR format is:

tag(43000) [real, imag]

Complex Arrays

Complex arrays use tag 43001 with an interleaved typed array for bulk memory operations:

std::vector<std::complex<double>> values = {
   {1.0, 0.5},
   {2.0, 1.0},
   {3.0, 1.5}
};

std::string buffer{};
glz::write_cbor(values, buffer);  // Uses bulk memcpy

std::vector<std::complex<double>> result;
glz::read_cbor(result, buffer);

The CBOR format is:

tag(43001) tag(typed_array_tag) bstr([r0, i0, r1, i1, ...])

This format leverages the fact that std::complex<T> stores real and imaginary parts contiguously in memory, enabling efficient bulk serialization.

Complex Eigen Matrices

Complex Eigen matrices are fully supported:

Eigen::MatrixXcd m(2, 2);
m << std::complex<double>(1, 1), std::complex<double>(2, 2),
     std::complex<double>(3, 3), std::complex<double>(4, 4);

std::string buffer{};
glz::write_cbor(m, buffer);

Eigen::MatrixXcd result;
glz::read_cbor(result, buffer);

Bitsets

std::bitset is serialized as a CBOR byte string:

std::bitset<32> bits = 0b10101010'11110000'00001111'01010101;
std::string buffer{};
glz::write_cbor(bits, buffer);

std::bitset<32> result;
glz::read_cbor(result, buffer);

Floating-Point Preferred Serialization

Following RFC 8949 preferred serialization, Glaze automatically uses the smallest floating-point representation that exactly represents the value:

  • Half-precision (16-bit) when possible
  • Single-precision (32-bit) when half is insufficient
  • Double-precision (64-bit) as fallback

This reduces message size while preserving exact values.

Date and Time (std::chrono)

Glaze serializes std::chrono types to CBOR using the standard semantic tags defined by RFC 8949. See chrono documentation for the shared concepts; this section covers the CBOR-specific wire format.

Wire format

Type Encoding Source
std::chrono::duration<Rep, Period> Bare integer count in the duration's native units
std::chrono::system_clock::time_point Tag 0 + text string (RFC 3339 date/time) RFC 8949 §3.4.1
glz::epoch_seconds (Period ≥ 1s) Tag 1 + integer seconds since epoch RFC 8949 §3.4.2
glz::epoch_millis / epoch_micros / epoch_nanos Tag 1 + float64 seconds since epoch RFC 8949 §3.4.2
std::chrono::steady_clock::time_point Bare integer count (no tag)

RFC 8949 §3.4.2 restricts tag 1 content to integers (major types 0/1) or floating-point numbers (major type 7, additional info 25/26/27). Nested tags — including tag 4 decimal fractions — are explicitly forbidden as tag 1 content, so sub-second precision is carried via float64.

Example

#include "glaze/cbor.hpp"
#include <chrono>

struct Event {
    std::chrono::system_clock::time_point timestamp;  // tag 0 + RFC 3339 string
    glz::epoch_millis logged_at;                      // tag 1 + float64 seconds
    std::chrono::milliseconds duration;               // bare integer
};

Event e{};
e.timestamp  = std::chrono::system_clock::now();
e.logged_at  = std::chrono::system_clock::now();
e.duration   = std::chrono::milliseconds{2500};

std::string buffer;
glz::write_cbor(e, buffer);

Event parsed;
glz::read_cbor(parsed, buffer);

The canonical RFC 8949 §3.4.2 example 2013-03-21T20:04:00Z encoded as glz::epoch_seconds produces exactly 0xC1 0x1A 0x51 0x4B 0x67 0xB0.

Precision

Float64 has ~15–17 significant decimal digits:

  • epoch_millis — lossless round-trip for any realistic modern epoch.
  • epoch_micros — lossless through approximately year 2255.
  • epoch_nanos — for modern timestamps, round-trip error is on the order of ~100 ns (the mantissa is exceeded once nanoseconds-since-1970 surpasses 2^53). Applications needing lossless nanosecond wire precision can use a bare integer type or build an RFC 9581 tag 1001 wrapper.

system_clock::time_point (tag 0 string) carries precision matching the time point's Duration template parameter — seconds, milliseconds, microseconds, or nanoseconds fractional digits are written as appropriate.

Decoder behavior

system_clock::time_point accepts: - Tag 0 + text string (canonical, what Glaze writes) - Tag 1 + integer or float seconds (converted from epoch) - Bare text string without a tag (lenient — for producers that omit tag 0)

Bare numbers and other shapes are rejected, since a unitless number has no defined meaning for a calendar time point.

epoch_time<Duration> accepts: - Tag 1 + integer or float16/32/64 seconds (canonical) - Bare integer or float without a tag (lenient — same semantics)

Nested tags inside tag 1 are rejected per RFC 8949 §3.4.2.

Note: system_clock::time_point and epoch_time<Duration> do not cross-read — they pick different wire tags on write, so the type you read into must match the wire form you expect. Pick the type on both sides to match your protocol.

Safety

  • NaN / Infinity on tag-1 float payloads are rejected as meaningless timestamps.
  • Integer and float seconds are bounds-checked against the range system_clock::duration can represent, so an adversarial wire value cannot overflow std::chrono::duration_cast.
  • Out-of-range years on write — RFC 3339 requires a 4-digit year; times outside [0000, 9999] return an error instead of emitting corrupt digits.
  • Leap seconds (:60) are rejected on read, matching the JSON backend. std::chrono::system_clock uses Unix time and does not model leap seconds.