Skip to content

Latest commit

 

History

History
2217 lines (1716 loc) · 86.6 KB

API.md

File metadata and controls

2217 lines (1716 loc) · 86.6 KB

ZXC API & ABI Reference

Library version: 0.15.0 SOVERSION: 6
License: BSD-3-Clause

This document is the authoritative reference for the public API surface and ABI guarantees of libzxc. It is intended for integrators, packagers, and language-binding authors.

For usage examples see EXAMPLES.md.
For the on-disk binary format see FORMAT.md.


Table of Contents


1. Headers and Include Graph

zxc.h                  <- freestanding umbrella (no <stdio.h>; kernel-safe)
├── zxc_buffer.h       <- Buffer API + Reusable Context API
│   └── zxc_export.h   <- visibility macros
├── zxc_constants.h    <- version macros, compression levels, block sizes, dict sizes
├── zxc_dict.h         <- Dictionary training, save/load, identification
│   └── zxc_export.h
├── zxc_error.h        <- error codes + zxc_error_name()
│   └── zxc_export.h
├── zxc_opts.h         <- parameter identifiers (zxc_cparam_t / zxc_dparam_t),
│   │                     their bounds, progress callback type
│   └── zxc_export.h
└── zxc_pstream.h      <- Push streaming API (caller-driven, single-thread)
    └── zxc_export.h

opt-in, freestanding-safe (no <stdio.h>):
    zxc_seekable.h     <- Seekable random-access API (storage-agnostic via
                          zxc_reader_t; usable from kernel-space)
        └── zxc_export.h

opt-in, userspace only (pull <stdio.h>; not freestanding-safe):
    zxc_stream.h       <- Multi-threaded FILE*-based streaming + the FILE*
                          open helper for the seekable API
                          (zxc_seekable_open_file)
        └── zxc_export.h
        └── zxc_seekable.h

<zxc.h> pulls only the minimal freestanding-compatible core (Buffer, Reusable Context, Push Streaming, one-shot Block APIs) and is therefore suitable for kernel-space, embedded, or any target where <stdio.h> is unavailable.

zxc_seekable.h is also freestanding-safe: it provides the random-access reader API on top of a caller-supplied zxc_reader_t callback. It is opt-in only because most consumers do not need random-access decompression; it does not pull <stdio.h>. Kernel / embedded consumers can include it directly.

Porting to a libc-less target: contrib/linux-kernel; Windows drivers: contrib/windows-kernel.

zxc_stream.h is the only header that requires <stdio.h>. It groups every FILE*-flavored entry point: the multi-threaded streaming driver (zxc_stream_compress / zxc_stream_decompress) and the seekable FILE* open helper (zxc_seekable_open_file, a thin wrapper that builds a pread/ReadFile-backed zxc_reader_t and delegates to zxc_seekable_open_reader). Kernel / freestanding builds simply do not include this header.


2. Symbol Visibility

libzxc uses an opt-in export strategy:

Build type How symbols are exposed
Shared library Default visibility is hidden. Only functions annotated with ZXC_EXPORT are exported. Internal FMV variants (_default, _neon32, _avx2, _avx512) are hidden.
Static library Define ZXC_STATIC_DEFINE (set automatically by CMake, Meson and pkg-config) to disable import/export annotations.

Macros

Macro Purpose
ZXC_EXPORT Marks a symbol as part of the public API (__declspec(dllexport) when building the Windows DLL, visibility("default") on GCC/Clang).
ZXC_NO_EXPORT Forces a symbol to be hidden (visibility("hidden")).
ZXC_DEPRECATED Emits a compiler warning when a deprecated symbol is used.
ZXC_STATIC_DEFINE Define when building or consuming as a static library.
ZXC_DLL_IMPORT Windows only, optional: define when consuming libzxc.dll to declare the API __declspec(dllimport).
zxc_lib_EXPORTS Set automatically by CMake when building the shared library. Do not define manually.

Windows: dllimport is opt-in

On Windows the header leaves the declarations unannotated unless you ask for dllimport via ZXC_DLL_IMPORT. The public API is functions only, so an unannotated declaration links against a DLL import library perfectly well — the linker just inserts a call thunk. The reverse is not true: dllimport against a static library is a hard link failure (unresolved external symbol __imp_zxc_*). Defaulting to "no annotation" means a consumer who forgets a define pays one indirect jump per call instead of failing to build.

You rarely need to set either macro by hand. Each integration path propagates the right one:

Path Static build Shared build
find_package(zxc) ZXC_STATIC_DEFINE ZXC_DLL_IMPORT
Meson subproject / dependency('libzxc') ZXC_STATIC_DEFINE ZXC_DLL_IMPORT
pkg-config --cflags libzxc ZXC_STATIC_DEFINE ZXC_DLL_IMPORT
Vendored sources / hand-rolled build (nothing needed) set zxc_lib_EXPORTS when building the DLL

3. ABI Versioning

libzxc follows the shared-library versioning convention:

libzxc.so.{SOVERSION}.{MAJOR}.{MINOR}.{PATCH}
Field Description Current
SOVERSION Bumped on ABI-breaking changes (struct layout, removed symbols, changed signatures). 6
VERSION Tracks the library release. 0.15.0

Compatibility rule: any binary compiled against SOVERSION N will load against any libzxc with the same SOVERSION, regardless of the VERSION triple.

Platform naming

Platform Files
Linux libzxc.so -> libzxc.so.6 -> libzxc.so.0.15.0
macOS libzxc.dylib -> libzxc.6.dylib -> libzxc.0.15.0.dylib
Windows libzxc.dll + zxc.lib (MSVC import, built from the shipped libzxc.def) or libzxc.dll.a (MinGW import)

4. Runtime Dependencies

libzxc has zero external dependencies.

Dependency Notes
C standard library <stdlib.h>, <string.h>, <stdint.h>, <stdio.h>
POSIX threads pthread on Unix/macOS; Windows threads on Win32

No dependency on OpenSSL, zlib, or any other compression library.


5. Constants and Enumerations

5.1 Version (compile-time)

Defined in zxc_constants.h:

#define ZXC_VERSION_MAJOR     0
#define ZXC_VERSION_MINOR     15
#define ZXC_VERSION_PATCH     0
#define ZXC_VERSION_CODENAME  "Hydrogen"
#define ZXC_LIB_VERSION_STR   "0.15.0"
#define ZXC_VERSION_NUMBER    (ZXC_VERSION_MAJOR * 10000 + ZXC_VERSION_MINOR * 100 + ZXC_VERSION_PATCH)

ZXC_VERSION_NUMBER gates code at compile time (#if ZXC_VERSION_NUMBER >= 10000); zxc_version_number() returns the loaded library's value for the runtime check. ZXC_VERSION_CODENAME names the release; it is informational only, and zxc_version_codename() returns the loaded library's.

5.2 Block Size Constraints

#define ZXC_BLOCK_SIZE_MIN_LOG2  12              // exponent for minimum
#define ZXC_BLOCK_SIZE_MAX_LOG2  21              // exponent for maximum
#define ZXC_BLOCK_SIZE_MIN       (1U << 12)      // 4 KB
#define ZXC_BLOCK_SIZE_MAX       (1U << 21)      // 2 MB
#define ZXC_BLOCK_SIZE_DEFAULT   (512 * 1024)    // 512 KB

Block size must be a power of two within [ZXC_BLOCK_SIZE_MIN, ZXC_BLOCK_SIZE_MAX]. It is set on a context with zxc_cctx_set_param(cctx, ZXC_C_BLOCK_SIZE, n): 0 restores ZXC_BLOCK_SIZE_DEFAULT, any other value outside the rule is refused with ZXC_ERROR_BAD_BLOCK_SIZE. zxc_init_static_cctx() takes an explicit block size (no 0).

5.3 Compression Levels

typedef enum {
    ZXC_LEVEL_FASTEST  = 1,  // Best throughput, lowest ratio
    ZXC_LEVEL_FAST     = 2,  // Fast, good for real-time
    ZXC_LEVEL_DEFAULT  = 3,  // Recommended balance
    ZXC_LEVEL_BALANCED = 4,  // Higher ratio, good speed
    ZXC_LEVEL_COMPACT  = 5,  // High density
    ZXC_LEVEL_DENSITY  = 6,  // Higher density: adds PIVCO-Huffman-coded literals
    ZXC_LEVEL_ULTRA    = 7   // Maximum density: PIVCO-Huffman literals + tokens
} zxc_compression_level_t;

Levels 1..5 produce data decompressible at essentially the same speed; levels 6 and 7 add a per-block Huffman decode step — literals at level 6, literals and sequence tokens at level 7 — trading some decode throughput for denser output (level 7 is the densest and carries the largest decode cost). 0 means ZXC_LEVEL_DEFAULT everywhere a level is given; any other value outside 1–7 is refused with ZXC_ERROR_BAD_LEVEL, by the one-shot zxc_compress() as by zxc_cctx_set_param(cctx, ZXC_C_LEVEL, n). zxc_init_static_cctx() takes an explicit 1–7.

5.4 Error Codes

typedef enum {
    ZXC_OK                  =   0,
    ZXC_ERROR_MEMORY        =  -1,  // malloc failure
    ZXC_ERROR_DST_TOO_SMALL =  -2,  // output buffer too small
    ZXC_ERROR_SRC_TOO_SMALL =  -3,  // input truncated
    ZXC_ERROR_BAD_MAGIC     =  -4,  // invalid magic word
    ZXC_ERROR_BAD_VERSION   =  -5,  // unsupported format version
    ZXC_ERROR_BAD_HEADER    =  -6,  // corrupted header (checksum mismatch)
    ZXC_ERROR_BAD_CHECKSUM  =  -7,  // checksum verification failed
    ZXC_ERROR_CORRUPT_DATA  =  -8,  // corrupted compressed data
    ZXC_ERROR_BAD_OFFSET    =  -9,  // invalid match offset
    ZXC_ERROR_OVERFLOW      = -10,  // buffer overflow detected
    ZXC_ERROR_IO            = -11,  // file read/write/seek failure
    ZXC_ERROR_NULL_INPUT    = -12,  // required pointer is NULL
    ZXC_ERROR_BAD_BLOCK_TYPE = -13, // unknown block type
    ZXC_ERROR_BAD_BLOCK_SIZE = -14, // invalid block size
    ZXC_ERROR_DICT_REQUIRED  = -15, // file requires a dictionary but none provided
    ZXC_ERROR_DICT_MISMATCH  = -16, // provided dictionary ID does not match header
    ZXC_ERROR_DICT_TOO_LARGE = -17, // dictionary exceeds ZXC_DICT_SIZE_MAX
    ZXC_ERROR_BAD_LEVEL      = -18, // ZXC_C_LEVEL out of 1..7, or a static context
                                    // asked to raise into a tier its workspace lacks
    ZXC_ERROR_DICT_UNSUPPORTED = -19, // dictionary unsupported by this context's workspace
    ZXC_ERROR_BAD_PARAM      = -20  // unknown parameter id, or value out of bounds
                                    // (zxc_cctx_set_param / zxc_dctx_set_param)
} zxc_error_t;

All public functions that can fail return negative zxc_error_t values on error.


6. Type Definitions

6.1 Parameter Identifiers

There is no options struct: every setting lives on a context (zxc_cctx / zxc_dctx) and is set one at a time with zxc_cctx_set_param() / zxc_dctx_set_param(). The identifiers are enums defined in zxc_opts.h. They are stable: new parameters are appended, and an existing value never changes meaning, so a binding can hard-code them.

Compression parameters (zxc_cparam_t):

typedef enum {
    ZXC_C_LEVEL      = 1,   // compression level
    ZXC_C_BLOCK_SIZE = 2,   // block size in bytes
    ZXC_C_THREADS    = 3,   // worker threads, FILE* pipeline only
    ZXC_C_CHECKSUM   = 4,   // block checksums + archive digest
    ZXC_C_SEEKABLE   = 5    // append a seek table
} zxc_cparam_t;
Identifier Meaning Default Bounds
ZXC_C_LEVEL Compression level (zxc_compression_level_t). ZXC_LEVEL_DEFAULT (3) 1 to 7; out of range is ZXC_ERROR_BAD_LEVEL
ZXC_C_BLOCK_SIZE Block size in bytes. ZXC_BLOCK_SIZE_DEFAULT (512 KB) Power of two in [ZXC_BLOCK_SIZE_MIN, ZXC_BLOCK_SIZE_MAX] (4 KB to 2 MB); anything else is ZXC_ERROR_BAD_BLOCK_SIZE
ZXC_C_THREADS Workers of zxc_stream_compress(); the reader and writer come on top. 0: online processors, capped at ZXC_MAX_THREADS, minus one; at least one, at most 511. The buffer, block and push APIs are single-threaded and ignore it. 0 (auto-detect) 0 to ZXC_MAX_THREADS (512)
ZXC_C_CHECKSUM 1 appends the per-block checksums and the archive digest. 0 0 or 1
ZXC_C_SEEKABLE 1 appends a seek table for random-access decompression. 0 0 or 1

Decompression parameters (zxc_dparam_t):

typedef enum {
    ZXC_D_THREADS  = 1,     // worker threads, FILE* pipeline only
    ZXC_D_CHECKSUM = 2      // verify checksums when the archive carries them
} zxc_dparam_t;
Identifier Meaning Default Bounds
ZXC_D_THREADS Workers of zxc_stream_decompress(); the reader and writer come on top. 0: online processors, capped at ZXC_MAX_THREADS, minus one unless the workers read a regular file themselves; at least one, at most 511 or 512. 0 (auto-detect) 0 to ZXC_MAX_THREADS (512)
ZXC_D_CHECKSUM 1 verifies the block checksums and the archive digest when the archive carries them; 0 skips the verification. 0 0 or 1

For every parameter the value 0 restores the default, and zxc_cctx_reset() / zxc_dctx_reset() restore them all. Any other value must sit within the bounds reported by zxc_cparam_bounds() / zxc_dparam_bounds() (declared in zxc_opts.h, documented in §9): a setter refuses an out-of-bounds value rather than clamp it, and leaves the parameter as it was. Values the bounds do not cover (ZXC_C_THREADS, ZXC_C_CHECKSUM, ZXC_C_SEEKABLE, both decompression parameters) and unknown identifiers are ZXC_ERROR_BAD_PARAM.

The dictionary and the progress callback are pointers, so they have their own setters, zxc_cctx_set_dict() / zxc_dctx_set_dict() and zxc_cctx_set_progress() / zxc_dctx_set_progress(), also in §9.

6.2 Progress Callback

typedef void (*zxc_progress_callback_t)(
    uint64_t    bytes_processed,  // Total input bytes processed so far
    uint64_t    bytes_total,      // Total input bytes (0 if unknown, e.g. stdin)
    const void* user_data         // User-provided context
);

Installed on a context with zxc_cctx_set_progress() / zxc_dctx_set_progress() and honoured by the FILE* pipeline only (zxc_stream_compress() / zxc_stream_decompress()): the buffer, block and push APIs are synchronous and never call it. Called from the writer thread after each block, or batch of blocks, is processed. Must be fast and non-blocking.

6.3 Opaque Context Types

typedef struct zxc_cctx_s zxc_cctx;  // Opaque compression context
typedef struct zxc_dctx_s zxc_dctx;  // Opaque decompression context

Internal layout is hidden. Interact only through zxc_create_* / zxc_free_* / zxc_cctx_* / zxc_dctx_* and the *_cctx / *_dctx entry points. A context is both the configuration (its parameters, dictionary and callback) and the working buffers; the FILE* and push APIs take one as configuration alone (§10, §10b).


7. Buffer API

Declared in zxc_buffer.h. Single-threaded, blocking, in-memory operations.

Library Info Helpers

Runtime-queryable library metadata. Exposed so integrations and language bindings can discover the supported level range and library version without relying on compile-time constants alone.

zxc_min_level

ZXC_EXPORT int zxc_min_level(void);

Returns the minimum supported compression level (currently 1, equivalent to ZXC_LEVEL_FASTEST).

zxc_max_level

ZXC_EXPORT int zxc_max_level(void);

Returns the maximum supported compression level (currently 7, equivalent to ZXC_LEVEL_ULTRA).

zxc_default_level

ZXC_EXPORT int zxc_default_level(void);

Returns the default compression level (currently 3, equivalent to ZXC_LEVEL_DEFAULT).

zxc_version_string

ZXC_EXPORT const char* zxc_version_string(void);

Returns the library version as a null-terminated string (e.g. "0.15.0"). The returned pointer is a compile-time constant and must not be freed.

zxc_version_number

ZXC_EXPORT unsigned zxc_version_number(void);

Returns the version of the loaded library as one integer, MAJOR * 10000 + MINOR * 100 + PATCH (e.g. 1500 for 0.15.0). The header defines the same packing as ZXC_VERSION_NUMBER; comparing the two detects a runtime library older than the one the program was compiled against.

zxc_version_codename

ZXC_EXPORT const char* zxc_version_codename(void);

Returns the release code name of the loaded library (e.g. "Hydrogen"), the header's ZXC_VERSION_CODENAME. Informational only: version comparisons use zxc_version_number(). The returned pointer is a compile-time constant and must not be freed.

zxc_compress_bound

ZXC_EXPORT size_t zxc_compress_bound(size_t input_size);

Returns the worst-case compressed size for input_size bytes. Use to allocate the destination buffer before compression. Returns 0 when that size would not fit a size_t on the platform, i.e. no buffer could hold it.

zxc_compress

ZXC_EXPORT int64_t zxc_compress(
    const void* src,
    size_t      src_size,
    void*       dst,
    size_t      dst_capacity,
    int         level         // 1..7; 0 = ZXC_LEVEL_DEFAULT
);

The simple form: compresses src into dst at level, every other parameter at its default (512 KB blocks, no checksum, no seek table, no dictionary). Single-threaded. A level outside 1–7 (other than 0) is ZXC_ERROR_BAD_LEVEL. For any other setting configure a context and call zxc_compress_cctx() (§9).

Returns: compressed size (> 0) on success, or negative zxc_error_t. A zero src_size (with src NULL or not) writes the empty archive: 32 bytes.

zxc_decompress

ZXC_EXPORT int64_t zxc_decompress(
    const void* src,
    size_t      src_size,
    void*       dst,
    size_t      dst_capacity
);

The simple form: decompresses src into dst with no checksum verification and no dictionary. An archive that carries checksums decodes without them being checked; an archive that requires a dictionary is ZXC_ERROR_DICT_REQUIRED. For either, configure a zxc_dctx (ZXC_D_CHECKSUM, zxc_dctx_set_dict()) and call zxc_decompress_dctx() (§9). src and dst must not overlap (same contract as memcpy); for overlapping single-buffer decode, use zxc_decompress_inplace below.

Asking without a destination: a NULL dst, or a dst_capacity of 0, decodes nothing and reports whether the archive holds anything: 0 for a well-formed empty archive, ZXC_ERROR_DST_TOO_SMALL when it stores a payload, and the archive's own error otherwise.

A probe never waves through an archive the decode would refuse: a 0 here means a call with a destination would have returned 0 too, checksum and dictionary binding included. Not the reverse, since nothing is decoded: an archive that stores a payload reports ZXC_ERROR_DST_TOO_SMALL even where a decode would name the actual fault (a footer contradicting the blocks, say). Callers that need that fault must decode into a buffer.

Caller errors outrank all of it, before anything is read from src, a source too short to hold a frame included: a NULL dst with a non-zero dst_capacity is ZXC_ERROR_NULL_INPUT.

Returns: decompressed size, 0 for an empty archive, or negative zxc_error_t.

zxc_decompress_inplace_bound

ZXC_EXPORT size_t zxc_decompress_inplace_bound(
    const void* src,
    size_t      src_size
);

Reads src's header and footer (no decoding) and returns the single-buffer size zxc_decompress_inplace needs: decompressed_size plus the safety margin block_size + nblocks x (block header + per-block checksum, if any) + EOF block + seek table + file footer + ZXC_DECOMPRESS_TAIL_PAD — one block, the accumulated per-block framing overhead (incompressible blocks make the compressed stream run that much longer than the output), everything the encoder writes after the last data block, and the wild-copy tail. The trailing bytes matter because they sit to the right of the read cursor and so push the flush-right archive left, into the write cursor's path. The seek table (one u64 anchor per 64 blocks plus a u32 per block, about 4.1 bytes per block) counts only when the header's HAS_SEEK_TABLE announces it. Always size the buffer with this function rather than re-deriving the formula.

In practice the margin is one block, plus 8–12 bytes per block of framing, plus ~2 KB of tail; a static context (zxc_decompress_inplace_dctx) is three to four blocks. The block size (-B) decides both (archives without a seek table):

Block size (-B) Margin, 8 MB payload Static context
4 KB 22–30 KB (0.3–0.4 %) 16 KB
64 KB 67–68 KB (0.8 %) 208 KB
512 KB (default) 514 KB (6.3 %) 1.6 MB

The higher margin is with block checksums, which the CLI writes by default. A seek table (-S) adds its ~4.1 bytes per block: +8 KB at 4 KB blocks, under 1 KB above. Smaller blocks cost some ratio and decode speed; measure on your data.

Returns: required buffer size, or 0 if src is not a valid archive.

zxc_decompress_inplace

ZXC_EXPORT int64_t zxc_decompress_inplace(
    void*  buffer,
    size_t buffer_capacity,
    size_t comp_size
);

Decompresses in place inside a single caller-owned buffer, replacing the usual input + output pair with one allocation — decisive for memory-constrained targets (embedded, FOTA, firmware). The compressed archive must sit flush-right in buffer (its last comp_size bytes); decoding runs left-to-right into buffer[0..]. Because ZXC never expands a block, the write cursor provably never overtakes the flush-right read cursor once buffer_capacity >= zxc_decompress_inplace_bound(...); an archive whose output would reach unread input (padding, forged block sizes) is refused with ZXC_ERROR_CORRUPT_DATA first. An undersized buffer is rejected with ZXC_ERROR_DST_TOO_SMALL, never corruption.

size_t need = zxc_decompress_inplace_bound(archive, archive_size);
uint8_t* buf = malloc(need);
memcpy(buf + (need - archive_size), archive, archive_size);   // flush-right
int64_t n = zxc_decompress_inplace(buf, need, archive_size);
// buf[0 .. n) now holds the decompressed data

Like zxc_decompress(), this simple form verifies no checksum and takes no dictionary. The decode context is allocated for the duration of the call: about two blocks, up to roughly zxc_static_dctx_workspace_size once the archive holds entropy-coded blocks. For checksum verification, a dictionary archive, or a decode without any allocation, use zxc_decompress_inplace_dctx below with a configured (or static) context.

Returns: decompressed size (> 0), 0 for an empty frame, or negative zxc_error_t.

zxc_decompress_inplace_dctx

ZXC_EXPORT int64_t zxc_decompress_inplace_dctx(
    zxc_dctx* dctx,
    void*     buffer,
    size_t    buffer_capacity,
    size_t    comp_size
);

Same contract as zxc_decompress_inplace (flush-right archive, buffer of at least zxc_decompress_inplace_bound, same buffer checks and error codes), but the decode context, and with it the configuration (ZXC_D_CHECKSUM, dictionary), comes from dctx. Dictionary archives are supported on a heap context. With a static context (zxc_init_static_dctx) the call touches no allocator at all: peak memory is the caller's buffer plus the caller's workspace, both sized up front. This is the entry point for kernel code and heapless targets.

The rules of dctx apply: a static context rejects an archive whose block size differs from its own with ZXC_ERROR_BAD_BLOCK_SIZE, and any dictionary with ZXC_ERROR_DICT_UNSUPPORTED. A heap context (zxc_create_dctx) allocates on first use, then reuses its buffers across calls.

size_t ws_size = zxc_static_dctx_workspace_size(64 * 1024);
void* ws = aligned_alloc(64, ws_size);                  // or .bss, kmalloc, vmalloc
zxc_dctx* dctx = zxc_init_static_dctx(ws, ws_size, 64 * 1024);

memcpy(buf + (need - archive_size), archive, archive_size);   // flush-right
int64_t n = zxc_decompress_inplace_dctx(dctx, buf, need, archive_size);

Returns: decompressed size (> 0), 0 for an empty frame, or negative zxc_error_t.

zxc_get_decompressed_size

ZXC_EXPORT uint64_t zxc_get_decompressed_size(
    const void* src,
    size_t      src_size
);

Reads the original size from the file footer without decompressing.

Returns: original size, or 0 if the buffer is invalid.

zxc_get_frame_info

typedef struct {
    uint64_t decompressed_size; /* source bytes the frame decodes to */
    uint64_t compressed_size;   /* compressed bytes, footer included */
    uint64_t digest;            /* archive digest; 0 without checksums */
    size_t   block_size;        /* 4 KB .. 2 MB */
    uint32_t dict_id;           /* dictionary the frame needs; 0 for none */
    uint8_t  format_version;
    uint8_t  has_checksum;
    uint8_t  has_seek_table;
} zxc_frame_info_t;

ZXC_EXPORT int zxc_get_frame_info(const void* src, size_t src_size, zxc_frame_info_t* info,
                                  size_t info_size);
ZXC_EXPORT size_t zxc_frame_info_size(void);

Reads what a frame's header and footer declare, without decoding: the header is validated as a decoder would, the footer parsed back from the end, and the frame must span all of src with a reachable size. Blocks are not read, so a frame that passes may still fail to decode.

Pass sizeof(zxc_frame_info_t) as info_size: the library writes only the fields that fit, so a program built against an older header stays in bounds when the struct grows (fields are only ever added at the end; zero the struct first to read unknown ones as 0). zxc_frame_info_size() returns the size the library was built with, for bindings that mirror the struct by hand.

Returns: ZXC_OK with *info filled, or a negative zxc_error_t with *info untouched.

zxc_get_last_frame_info

ZXC_EXPORT int zxc_get_last_frame_info(const void* src, size_t src_size, zxc_frame_info_t* info,
                                       size_t info_size);

The same checks on the last frame of src, which may start anywhere: at src_size - info.compressed_size. Each footer stores its frame's compressed size, so concatenated frames are walked back from the end without decoding:

size_t n = src_size;
while (n > 0) {
    if (zxc_get_last_frame_info(src, n, &info, sizeof(info)) != ZXC_OK) break;
    n -= info.compressed_size;  /* the frame before ends here */
}

Returns: as zxc_get_frame_info().


8. Block API

Declared in zxc_buffer.h. Single-block compression and decompression without file framing (no file header, EOF block, or footer). Designed for filesystem integrations where the caller manages its own block indexing.

Output format: block_header (8 B) + compressed payload + optional checksum (4 B). The checksum covers the block's decompressed bytes, seeded with 0 (frames seed each block with its position), so it is verified after decoding.

zxc_compress_block_bound

ZXC_EXPORT size_t zxc_compress_block_bound(size_t input_size);

Returns the maximum compressed size for a single block.
Unlike zxc_compress_bound(), this does not include file header, EOF block, or footer overhead.

input_size must be in [0, ZXC_BLOCK_SIZE_MAX] (the Block API limit).

Returns: upper bound in bytes, or 0 if input_size > ZXC_BLOCK_SIZE_MAX or would overflow the computation.

zxc_decompress_block_bound

ZXC_EXPORT size_t zxc_decompress_block_bound(size_t uncompressed_size);

Returns the minimum dst_capacity required by zxc_decompress_block() for a block of uncompressed_size bytes. The fast decoder uses speculative wild-copy writes and needs a small tail pad beyond the declared uncompressed size: passing exactly uncompressed_size as dst_capacity forces the slow tail path and may trigger ZXC_ERROR_OVERFLOW on some inputs.

Use this helper to size destination buffers for the fast path. For callers that genuinely cannot oversize their output buffer, use zxc_decompress_block_safe() instead.

uncompressed_size must be in [0, ZXC_BLOCK_SIZE_MAX] (the Block API limit).

Returns: minimum dst_capacity in bytes, or 0 if uncompressed_size > ZXC_BLOCK_SIZE_MAX or would overflow the computation.

zxc_compress_block

ZXC_EXPORT int64_t zxc_compress_block(
    zxc_cctx*   cctx,
    const void* src,
    size_t      src_size,
    void*       dst,
    size_t      dst_capacity
);

Compresses a single block using a reusable context, under the context's configuration: ZXC_C_LEVEL, ZXC_C_BLOCK_SIZE, ZXC_C_CHECKSUM and the dictionary set with zxc_cctx_set_dict(); the shared literal table is rebuilt only when it changes. ZXC_C_SEEKABLE and ZXC_C_THREADS have no effect here. A static context cannot carry a dictionary (zxc_cctx_set_dict() refuses it with ZXC_ERROR_DICT_UNSUPPORTED).

src_size must be in [1, ZXC_BLOCK_SIZE_MAX] (2 MiB). For larger payloads, use the frame API (zxc_compress) or streaming API (zxc_cstream_*), which chunk transparently into format-conformant blocks.

Returns: compressed block size (> 0) on success, or negative zxc_error_t. Returns ZXC_ERROR_BAD_BLOCK_SIZE if src_size > ZXC_BLOCK_SIZE_MAX. An invalid block size, a level a static workspace cannot serve, or a dictionary on a static context are refused earlier, by the setter that would install them (§9).

zxc_decompress_block

ZXC_EXPORT int64_t zxc_decompress_block(
    zxc_dctx*   dctx,
    const void* src,
    size_t      src_size,
    void*       dst,
    size_t      dst_capacity
);

Decompresses a single block produced by zxc_compress_block(). dst_capacity is at least the uncompressed size. Adding ZXC_DECOMPRESS_TAIL_PAD to it, as zxc_decompress_block_bound(uncompressed_size) returns, lets a dictionary-free block on a heap context decode straight into dst; otherwise the decode falls back to an internal bounce buffer and a copy. A dictionary block always bounces, the decoder needing the dictionary and the payload contiguous, and so does a static context sized for a larger block. On error dst holds whatever the aborted decode wrote: its previous contents do not survive a failed call. dst_capacity must not exceed ZXC_BLOCK_SIZE_MAX + ZXC_DECOMPRESS_TAIL_PAD. For payloads produced by the frame or streaming APIs, use zxc_decompress instead. The context's ZXC_D_CHECKSUM and dictionary (zxc_dctx_set_dict()) apply; a block carries no dictionary id, so attach the same (content, table) pair as at compression. A static context cannot carry a dictionary (ZXC_ERROR_DICT_UNSUPPORTED from the setter).

Returns: decompressed size (> 0) on success, or negative zxc_error_t. Returns ZXC_ERROR_BAD_BLOCK_SIZE if dst_capacity exceeds the per-block limit. On a static context the carved block is the effective capacity: a larger block returns ZXC_ERROR_BAD_BLOCK_SIZE while it fits the workspace margin and fails like a too-small destination beyond it. dst contents are unspecified on error.

zxc_decompress_block_safe

ZXC_EXPORT int64_t zxc_decompress_block_safe(
    zxc_dctx*   dctx,
    const void* src,
    size_t      src_size,
    void*       dst,
    size_t      dst_capacity
);

Strict-sized variant of zxc_decompress_block(). Accepts dst_capacity == uncompressed_size exactly: no tail pad required. Intended for integrations whose output buffer cannot be oversized (page-aligned decoding into mapped pages, fixed-size slots in a columnar layout, etc.).

Output is bit-identical to zxc_decompress_block(). RAW blocks transparently forward to the fast path; only SPL/PAK blocks use the strict-tail decoder, which is slightly slower than the wild-copy fast path (see the performance table in EXAMPLES.md).

Strict-tail variant: dst_capacity is the exact uncompressed size with no tail-pad margin, so the upper limit is ZXC_BLOCK_SIZE_MAX (not MAX+TAIL_PAD as for zxc_decompress_block). Returns ZXC_ERROR_BAD_BLOCK_SIZE if dst_capacity > ZXC_BLOCK_SIZE_MAX. A static context applies the same bound and codes as zxc_decompress_block and accepts a larger dst_capacity alike.

Same configuration as zxc_decompress_block: the context's ZXC_D_CHECKSUM and dictionary; a dictionary routes through its bounce path.

Returns: decompressed size (> 0) on success, or negative zxc_error_t.

zxc_estimate_cctx_size

ZXC_EXPORT size_t zxc_estimate_cctx_size(size_t src_size, int level);

Returns an accurate estimate of the peak memory used when compressing a single block of src_size bytes at the given level via zxc_compress_block().

The estimate covers all per-chunk working buffers (chain table, literals, sequence/token/offset/extras buffers), the fixed hash tables and match-split histograms, and the cache-line alignment padding. At level >= 6 it also includes the optimal parser's scratch (~8.125 × src_size), allocated once and reused. It scales roughly linearly with src_size and is intended for integrators that need to build an accurate memory budget (filesystems, embedded devices, sandboxed workloads).

Returns: estimated peak cctx memory usage in bytes, or 0 if src_size == 0.


9. Reusable Context API

Declared in zxc_buffer.h. Eliminates per-call allocation overhead for hot-path integrations (filesystem plug-ins, batch processing), and is where every setting beyond the one-shot level is made.

The model

A context carries the configuration. zxc_create_cctx() / zxc_create_dctx() return a context with every parameter at its default; zxc_cctx_set_param() / zxc_dctx_set_param() change one parameter at a time (§6.1 lists them), zxc_cctx_set_dict() / zxc_dctx_set_dict() attach a dictionary and zxc_cctx_set_progress() / zxc_dctx_set_progress() a progress callback. A setting stays in force for every later call on that context until changed again or zxc_cctx_reset() / zxc_dctx_reset():

  • 0 restores the default of a parameter; reset restores them all and detaches the dictionary and the callback, keeping the working buffers.
  • A setter refuses, it never clamps. A value outside zxc_cparam_bounds() / zxc_dparam_bounds() leaves the parameter as it was and returns ZXC_ERROR_BAD_LEVEL (ZXC_C_LEVEL), ZXC_ERROR_BAD_BLOCK_SIZE (ZXC_C_BLOCK_SIZE, non-powers of two included) or ZXC_ERROR_BAD_PARAM (every other parameter, and any unknown identifier). The level argument of the one-shot zxc_compress() follows the same rule.
  • Static contexts pin block size and level (§9b). On a context from zxc_init_static_cctx(), ZXC_C_BLOCK_SIZE accepts only the pinned value, a raise into ZXC_LEVEL_DENSITY on a workspace carved below it is ZXC_ERROR_BAD_LEVEL, and zxc_cctx_set_dict() / zxc_dctx_set_dict() are ZXC_ERROR_DICT_UNSUPPORTED. These errors come from the setter, so a compress or decompress call on a configured context no longer meets them.
  • The FILE* and push APIs read a context as configuration only. zxc_stream_compress() / zxc_stream_decompress() and zxc_cstream_create() / zxc_dstream_create() take a const context, copy or read its parameters and never use its buffers; NULL means all defaults. The context may be freed, or used for a one-shot call, right after.

A heap context allocates its working buffers on the first call, sized from the parameters then in force, and re-carves them only when the block size changes or a level raise into ZXC_LEVEL_DENSITY needs the optimal-parser scratch.

zxc_create_cctx

ZXC_EXPORT zxc_cctx* zxc_create_cctx(void);

Creates a heap-allocated compression context with every parameter at its default: level 3, 512 KB blocks, threads auto, no checksum, no seek table, no dictionary, no callback. Only the handle is allocated; the working buffers come on the first compress call.

Returns: context pointer, or NULL on allocation failure.

zxc_free_cctx

ZXC_EXPORT void zxc_free_cctx(zxc_cctx* cctx);

Frees all resources. Safe to pass NULL; a no-op on a static context.

zxc_cctx_set_param

ZXC_EXPORT int zxc_cctx_set_param(zxc_cctx* cctx, zxc_cparam_t param, int64_t value);

Sets one parameter; 0 restores its default. Takes effect on the next call. An out-of-bounds value is refused and the parameter keeps its value: ZXC_ERROR_BAD_LEVEL for ZXC_C_LEVEL, ZXC_ERROR_BAD_BLOCK_SIZE for ZXC_C_BLOCK_SIZE, ZXC_ERROR_BAD_PARAM otherwise and for an unknown param. On a static context the block size is pinned and a level raise into ZXC_LEVEL_DENSITY is refused when the workspace was carved below it.

Returns: ZXC_OK, or a negative zxc_error_t (ZXC_ERROR_NULL_INPUT for a NULL cctx).

zxc_cctx_get_param

ZXC_EXPORT int zxc_cctx_get_param(const zxc_cctx* cctx, zxc_cparam_t param, int64_t* value);

Reads the value in force for param into *value.

Returns: ZXC_OK, ZXC_ERROR_BAD_PARAM for an unknown param, or ZXC_ERROR_NULL_INPUT.

zxc_cparam_bounds / zxc_dparam_bounds

#include <zxc_opts.h>   // pulled in by every API header

ZXC_EXPORT int zxc_cparam_bounds(zxc_cparam_t param, int64_t* min, int64_t* max);
ZXC_EXPORT int zxc_dparam_bounds(zxc_dparam_t param, int64_t* min, int64_t* max);

Report the accepted range of a parameter (min and max may each be NULL), so a caller or a binding can validate a value before setting it, and learn whether the loaded library knows param at all. For ZXC_C_BLOCK_SIZE the range is [ZXC_BLOCK_SIZE_MIN, ZXC_BLOCK_SIZE_MAX]; the power-of-two rule is checked by the setter.

Returns: ZXC_OK, or ZXC_ERROR_BAD_PARAM for an unknown param.

zxc_cctx_set_dict

ZXC_EXPORT int zxc_cctx_set_dict(
    zxc_cctx*   cctx,
    const void* dict,        // content, or NULL to detach
    size_t      dict_size,   // at most ZXC_DICT_SIZE_MAX
    const void* dict_huf     // 128-byte shared literal table, or NULL
);

Attaches a pre-trained dictionary; a NULL dict detaches it. The context keeps its own copy of the content and the table, so the caller's buffers are free once the call returns. dict_huf, when given, is the ZXC_HUF_TABLE_SIZE-byte table from zxc_dict_load() / zxc_dict_huf(): it codes literals at levels 6–7 and is bound into the archive's dict_id at every level (§11b). The shared table is rebuilt only when its bytes change between calls.

Returns: ZXC_OK; ZXC_ERROR_DICT_TOO_LARGE above ZXC_DICT_SIZE_MAX; ZXC_ERROR_DICT_UNSUPPORTED on a static context; ZXC_ERROR_MEMORY when the copy cannot be allocated. A refused call leaves the attached dictionary.

zxc_cctx_set_progress

ZXC_EXPORT int zxc_cctx_set_progress(zxc_cctx* cctx, zxc_progress_callback_t cb, void* user_data);

Installs the progress callback (§6.2); a NULL cb disables it. Honoured by zxc_stream_compress() only: the buffer, block and push APIs are synchronous and never call it.

Returns: ZXC_OK, or ZXC_ERROR_NULL_INPUT.

zxc_cctx_reset

ZXC_EXPORT int zxc_cctx_reset(zxc_cctx* cctx);

Restores every parameter to its default, detaches the dictionary and the callback. The working buffers are kept. On a static context the pinned block size and level are the defaults.

Returns: ZXC_OK, or ZXC_ERROR_NULL_INPUT.

zxc_compress_cctx

ZXC_EXPORT int64_t zxc_compress_cctx(
    zxc_cctx*   cctx,
    const void* src,
    size_t      src_size,
    void*       dst,
    size_t      dst_capacity
);

Compresses a complete archive like zxc_compress(), under the context's configuration and reusing its buffers: ZXC_C_LEVEL, ZXC_C_BLOCK_SIZE, ZXC_C_CHECKSUM, ZXC_C_SEEKABLE and the dictionary; ZXC_C_THREADS is ignored (single-threaded). The workspace is re-carved when the block size changes, or when a level raise into ZXC_LEVEL_DENSITY needs the optimal-parser scratch. ZXC_C_SEEKABLE appends the seek table without allocating, static contexts included.

Returns: compressed size (> 0) on success, or negative zxc_error_t. A zero src_size writes the empty archive without carving the workspace: 32 bytes, +8 with ZXC_C_CHECKSUM, +8 with ZXC_C_SEEKABLE.

zxc_create_dctx

ZXC_EXPORT zxc_dctx* zxc_create_dctx(void);

Creates a heap-allocated decompression context with every parameter at its default: threads auto, no checksum verification, no dictionary, no callback. Only the handle is allocated; the working buffers are sized from the archive header, so they come on the first decode.

Returns: context pointer, or NULL on allocation failure.

zxc_free_dctx

ZXC_EXPORT void zxc_free_dctx(zxc_dctx* dctx);

Frees all resources. Safe to pass NULL; a no-op on a static context.

zxc_dctx_set_param

ZXC_EXPORT int zxc_dctx_set_param(zxc_dctx* dctx, zxc_dparam_t param, int64_t value);

Sets one decompression parameter (ZXC_D_THREADS, ZXC_D_CHECKSUM); 0 restores its default. Takes effect on the next call. A value outside zxc_dparam_bounds() is ZXC_ERROR_BAD_PARAM, as is an unknown param; the parameter keeps its value.

Returns: ZXC_OK, or a negative zxc_error_t.

zxc_dctx_get_param

ZXC_EXPORT int zxc_dctx_get_param(const zxc_dctx* dctx, zxc_dparam_t param, int64_t* value);

Reads the value in force for param. Same contract as zxc_cctx_get_param().

zxc_dctx_set_dict

ZXC_EXPORT int zxc_dctx_set_dict(
    zxc_dctx*   dctx,
    const void* dict,        // content, or NULL to detach
    size_t      dict_size,   // at most ZXC_DICT_SIZE_MAX
    const void* dict_huf     // the table used at compression, or NULL
);

Attaches the dictionary the archives were compressed with; a NULL dict detaches it. Copies the bytes, as zxc_cctx_set_dict() does. dict_huf must be the table used at compression time when the archive carries one: the header's dict_id is checked against the (content, table) pair, ZXC_ERROR_DICT_MISMATCH otherwise.

Returns: ZXC_OK; ZXC_ERROR_DICT_TOO_LARGE above ZXC_DICT_SIZE_MAX; ZXC_ERROR_DICT_UNSUPPORTED on a static context.

zxc_dctx_set_progress

ZXC_EXPORT int zxc_dctx_set_progress(zxc_dctx* dctx, zxc_progress_callback_t cb, void* user_data);

Installs the progress callback; a NULL cb disables it. Honoured by zxc_stream_decompress() only.

Returns: ZXC_OK, or ZXC_ERROR_NULL_INPUT.

zxc_dctx_reset

ZXC_EXPORT int zxc_dctx_reset(zxc_dctx* dctx);

Restores every parameter to its default, detaches the dictionary and the callback. The working buffers are kept.

Returns: ZXC_OK, or ZXC_ERROR_NULL_INPUT.

zxc_decompress_dctx

ZXC_EXPORT int64_t zxc_decompress_dctx(
    zxc_dctx*   dctx,
    const void* src,
    size_t      src_size,
    void*       dst,
    size_t      dst_capacity
);

Decompresses a complete archive like zxc_decompress(), but under the context's configuration and reusing its buffers: ZXC_D_CHECKSUM decides whether checksums are verified, and the dictionary from zxc_dctx_set_dict() is used; the shared literal table is rebuilt only when it changes between calls. This is the entry point for checksum verification and dictionary archives, which the simple form does not offer. A static context cannot carry a dictionary (ZXC_ERROR_DICT_UNSUPPORTED from the setter).

The no-destination probe works here too, answered under this context's rules: a static context still rejects a foreign block size and a dictionary-bound archive.

Error codes changed after v0.14.0: they now match zxc_decompress() exactly. A truncated input reports ZXC_ERROR_SRC_TOO_SMALL and a malformed header reports what the header parse found (ZXC_ERROR_BAD_MAGIC, ZXC_ERROR_BAD_VERSION, ZXC_ERROR_BAD_BLOCK_SIZE), where both used to flatten to ZXC_ERROR_NULL_INPUT or ZXC_ERROR_BAD_HEADER.


9b. Static Context API

Declared in zxc_buffer.h. Mirrors the Reusable Context API but places the entire context, opaque handle plus every persistent sub-buffer, inside a single buffer allocated and owned by the caller. This pattern is mandatory for environments where the library cannot call into the host allocator on the hot path: Linux kernel filesystems (one workspace per mount, served via vmalloc / kmalloc up front), embedded targets without a heap (.bss or stack-allocated workspace), sandboxed runtimes with a fixed memory budget, etc.

Trade-off vs the dynamic API: the workspace is pinned to a single block_size (and, for compression, a level tier) at init time; the setters cannot enlarge the footprint afterwards, so a workload that needs to mix block sizes must size the workspace for the maximum block size up front.

Properties

  • Single allocation. All hash tables, chain table, sequence buffers, literal scratch, work buffer, and (at ZXC_LEVEL_DENSITY) the optimal- parser scratch live inside the caller-supplied buffer. No further ZXC_MALLOC / ZXC_ALIGNED_MALLOC is performed for the lifetime of the context.
  • zxc_free_*ctx is a no-op on a static handle. The caller owns the workspace.
  • block_size is locked. zxc_cctx_set_param(cctx, ZXC_C_BLOCK_SIZE, n) with any value other than the pinned one returns ZXC_ERROR_BAD_BLOCK_SIZE without re-initialising (0 is the pinned value here). ZXC_C_LEVEL may still change, within the tier the workspace was carved for: a raise into ZXC_LEVEL_DENSITY on a workspace sized below it is ZXC_ERROR_BAD_LEVEL. ZXC_C_CHECKSUM and ZXC_C_SEEKABLE are honoured without re-partitioning.
  • No dictionary. zxc_cctx_set_dict() / zxc_dctx_set_dict() return ZXC_ERROR_DICT_UNSUPPORTED: the workspace has no room for the prefix.
  • Workspace alignment. Must be at least cache-line (64-byte) aligned. Use posix_memalign(..., 64, ...), aligned_alloc(64, ...), the slab allocator (kmalloc returns ≥ ARCH_KMALLOC_MINALIGN), or a _Alignas(64) static array.
  • In place too. A static decompression context also drives zxc_decompress_inplace_dctx: a single-buffer decode with no allocation at all.

zxc_static_cctx_workspace_size

ZXC_EXPORT size_t zxc_static_cctx_workspace_size(
    const size_t block_size,
    const int    level
);

Returns the exact byte count required by a static compression workspace for the given block_size and level. Sum of the opaque zxc_cctx wrapper plus every persistent sub-buffer the library would partition.

Returns: workspace size in bytes, or 0 if either argument is invalid (non-power-of-two block_size, out-of-range level, ...).

Note: level 6 (ZXC_LEVEL_DENSITY) adds the optimal-parser scratch (~8.125 × block_size, at least ~213 KiB); levels 1–5 share the same workspace size.

zxc_init_static_cctx

ZXC_EXPORT zxc_cctx* zxc_init_static_cctx(
    void*        workspace,
    const size_t workspace_size,
    const size_t block_size,     // explicit: power of two, 4 KB..2 MB
    const int    level           // explicit: 1..7
);

Initialises a compression context inside a caller-supplied workspace. workspace_size must be at least zxc_static_cctx_workspace_size for the same block_size / level. Both are explicit and pinned: 0 is not a default here but an invalid argument. The other parameters start at their defaults and are set as on a heap context (ZXC_C_CHECKSUM, ZXC_C_SEEKABLE, a lower ZXC_C_LEVEL); zxc_cctx_set_dict() is refused with ZXC_ERROR_DICT_UNSUPPORTED.

The returned handle points inside workspace; the workspace must remain valid for the lifetime of the handle. zxc_free_cctx is a no-op.

Returns: handle pointing inside workspace, or NULL if the workspace is too small, block_size or level is invalid, or workspace is NULL.

zxc_static_dctx_workspace_size

ZXC_EXPORT size_t zxc_static_dctx_workspace_size(
    const size_t block_size
);

Returns the exact byte count required by a static decompression workspace for the given block_size. Unlike the compression variant, this size is independent of the source archive's level: lit_buffer is always provisioned worst-case because the decoder cannot predict the per-block literal encoding (RAW / RLE / HUFFMAN) until it sees each block header.

Returns: workspace size in bytes, or 0 if block_size is invalid.

zxc_init_static_dctx

ZXC_EXPORT zxc_dctx* zxc_init_static_dctx(
    void*         workspace,
    const size_t  workspace_size,
    const size_t  block_size
);

Initialises a decompression context inside a caller-supplied workspace. block_size is pinned at init time: feeding the returned handle an archive whose file header declares a different block_size returns ZXC_ERROR_BAD_BLOCK_SIZE; a block larger than it never decodes through the block API (see zxc_decompress_block for the codes). Dictionaries are not supported: ZXC_ERROR_DICT_UNSUPPORTED.

The returned handle points inside workspace; the workspace must remain valid for the lifetime of the handle. zxc_free_dctx is a no-op.

Returns: handle pointing inside workspace, or NULL if the workspace is too small, block_size is invalid, or workspace is NULL.

Typical usage

#include <zxc.h>

#define BLOCK_SZ   (64 * 1024)   /* must match the archive's block_size at decode */
#define LEVEL      ZXC_LEVEL_DEFAULT

/* --- Compression side --- */
size_t cws_sz = zxc_static_cctx_workspace_size(BLOCK_SZ, LEVEL);
void  *cws    = NULL;
posix_memalign(&cws, 64, cws_sz);                  /* or kmalloc / vmalloc / .bss */

zxc_cctx *cctx = zxc_init_static_cctx(cws, cws_sz, BLOCK_SZ, LEVEL);
zxc_cctx_set_param(cctx, ZXC_C_CHECKSUM, 1);       /* optional; no re-partitioning */

for (/* each block */) {
    zxc_compress_cctx(cctx, src, n, dst, cap);     /* no allocation */
}
zxc_free_cctx(cctx);  /* no-op for static */
free(cws);            /* caller owns the workspace */

/* --- Decompression side --- */
size_t dws_sz = zxc_static_dctx_workspace_size(BLOCK_SZ);
void  *dws    = NULL;
posix_memalign(&dws, 64, dws_sz);

zxc_dctx *dctx = zxc_init_static_dctx(dws, dws_sz, BLOCK_SZ);
zxc_dctx_set_param(dctx, ZXC_D_CHECKSUM, 1);       /* verify what the archive carries */
zxc_decompress_dctx(dctx, in, in_sz, out, out_cap);
zxc_free_dctx(dctx);  /* no-op */
free(dws);

Sizing notes

Values returned by zxc_static_cctx_workspace_size / zxc_static_dctx_workspace_size (arm64; the dctx column is the whole static workspace, work, literal, token and PivCo buffers included):

block_size level 1-5 cctx level 6-7 cctx dctx
4 KB ~307 KB ~519 KB ~16 KB
64 KB ~489 KB ~1.0 MB ~208 KB
512 KB ~1.8 MB ~5.9 MB ~1.6 MB
2 MB ~6.3 MB ~22.5 MB ~6.4 MB

(Exact figures depend on architecture and alignment padding; always query zxc_static_*_workspace_size rather than hard-coding.)

Discovering block_size at decode time

zxc_init_static_dctx pins block_size at workspace creation, so the caller must know it before calling init. Four patterns cover every use case:

  1. Pinned by contract. Producer and consumer agree on block_size at deployment time (kernel module, embedded firmware, intra-application pipeline). The constant lives in a shared header — no runtime discovery needed. This is the canonical static-context use case.

  2. Seekable archive — read the SEK table. The block size is recoverable from the table opened in any backing mode (buffer, FILE*, or zxc_reader_t):

    zxc_seekable* s = zxc_seekable_open_reader(&r);  /* parses SEK table only */
    const uint32_t bs = zxc_seekable_get_block_decomp_size(s, 0);
    /* For multi-block archives, block index 0 is always a full block.
     * For a single-block archive, bs equals the total decompressed size.
     * An empty archive has 0 blocks: bs is 0, nothing to decode. */
  3. Stream / buffer archive — peek the 16-byte file header. The block size is encoded at offset 0x05 (the Chunk Size Code, see FORMAT.md §3). Read the first ZXC_FILE_HEADER_SIZE bytes, validate the magic word, then decode:

    uint8_t hdr[ZXC_FILE_HEADER_SIZE];
    /* fread / read / pread the first 16 bytes of the archive */
    
    const uint32_t magic = (uint32_t)hdr[0]       | ((uint32_t)hdr[1] << 8)
                         | ((uint32_t)hdr[2] << 16) | ((uint32_t)hdr[3] << 24);
    if (magic != ZXC_MAGIC_WORD) { /* not a ZXC archive */ }
    
    const uint8_t code = hdr[5];
    size_t block_size;
    if (code >= ZXC_BLOCK_SIZE_MIN_LOG2 && code <= ZXC_BLOCK_SIZE_MAX_LOG2)
        block_size = (size_t)1U << code;       /* 4 KB..2 MB */
    else
        /* invalid archive */;

    After peeking, rewind the input (or keep the 16-byte prefix) and feed the whole archive to zxc_decompress_dctx / streaming decoder normally — the decoder re-parses the header.

  4. Worst-case sizing fallback. When the archive's block size cannot be pre-fetched (e.g., a one-shot streaming decoder that consumes the header in the same pass), size the dctx workspace for ZXC_BLOCK_SIZE_MAX (2 MB). The handle accepts any conforming archive at the cost of over-allocation (~4 MB dctx).

    size_t dws_sz = zxc_static_dctx_workspace_size(ZXC_BLOCK_SIZE_MAX);

    If the workspace pool must stay tight and worst-case sizing is too expensive, fall back to the dynamic Context API (zxc_create_dctx) — it sizes its buffers from each archive's header.


10. Streaming API

Declared in zxc_stream.h — the umbrella for every FILE*-flavored entry point (opt-in: not included by <zxc.h> because <stdio.h> would break freestanding/kernel builds). Hosts the multi-threaded streaming pipeline described below (reader -> workers -> writer) and the seekable FILE* open helper zxc_seekable_open_file (documented in §11).

zxc_stream_compress

ZXC_EXPORT int64_t zxc_stream_compress(
    FILE*           f_in,
    FILE*           f_out,
    const zxc_cctx* cfg          // configuration only; NULL = defaults
);

Compresses f_in -> f_out using a parallel pipeline. cfg is a configured compression context read for its parameters only: every one of them applies here (ZXC_C_LEVEL, ZXC_C_BLOCK_SIZE, ZXC_C_THREADS, ZXC_C_CHECKSUM, ZXC_C_SEEKABLE, the dictionary, the progress callback). Its buffers are not used, the pipeline carves its own per worker, so the context may be freed or reused for a one-shot call right after. NULL means all defaults.

Returns: total compressed bytes written, or negative zxc_error_t.

zxc_stream_decompress

ZXC_EXPORT int64_t zxc_stream_decompress(
    FILE*           f_in,
    FILE*           f_out,
    const zxc_dctx* cfg          // configuration only; NULL = defaults
);

Decompresses f_in -> f_out using a parallel pipeline. cfg is read for ZXC_D_THREADS, ZXC_D_CHECKSUM, the dictionary and the progress callback; its buffers are not used. NULL means all defaults (no checksum verification, no dictionary). Bytes after the footer are ZXC_ERROR_CORRUPT_DATA.

A regular file is read ahead in batches; a pipe or socket is decoded block by block, so a live stream's output is never held back.

Returns: total decompressed bytes written, or negative zxc_error_t.

zxc_stream_get_decompressed_size

ZXC_EXPORT int64_t zxc_stream_get_decompressed_size(FILE* f_in);

Reads the original size from the file footer, after validating the file header as a decoder would and capping the size by what the archive could hold. File position is restored.

Returns: original size, or negative zxc_error_t (the header's verdict, ZXC_ERROR_SRC_TOO_SMALL, ZXC_ERROR_CORRUPT_DATA for an implausible size, or an I/O error).

zxc_stream_get_frame_info

ZXC_EXPORT int zxc_stream_get_frame_info(FILE* f_in, zxc_frame_info_t* info, size_t info_size);

zxc_get_frame_info() on a seekable FILE*, read from offset 0. File position is restored.

Returns: ZXC_OK, or a negative zxc_error_t (ZXC_ERROR_IO included).

zxc_stream_get_last_frame_info

ZXC_EXPORT int zxc_stream_get_last_frame_info(FILE* f_in, uint64_t end, zxc_frame_info_t* info,
                                              size_t info_size);

zxc_get_last_frame_info() on the first end bytes of a seekable FILE*: pass the file size, then end - info.compressed_size, to walk its frames back to the first. File position is restored.

Returns: ZXC_OK, or a negative zxc_error_t: ZXC_ERROR_SRC_TOO_SMALL when end is past the end of the file, ZXC_ERROR_IO included.


10b. Push Streaming API

Declared in zxc_pstream.h. Single-threaded, caller-driven streaming, the inverse of the FILE*-based pipeline. Designed for callback-based integrations (async runtimes, network protocols) that cannot block on a FILE* and need to feed/drain in arbitrary chunks.

The on-disk output is bit-compatible with the Buffer / Stream APIs: files produced by the push API decode with zxc_decompress() / zxc_stream_decompress(), and vice-versa.

Buffer Descriptors

typedef struct {
    const void* src;
    size_t      size;
    size_t      pos;   // advanced by the library
} zxc_inbuf_t;

typedef struct {
    void*  dst;
    size_t size;
    size_t pos;        // advanced by the library
} zxc_outbuf_t;

The library reads [src+pos .. src+size) and writes [dst+pos .. dst+size), advancing pos by what it consumed/produced. Buffers are caller-owned. The whole [dst+pos .. dst+size) window is writable scratch: with enough remaining capacity the decoder decodes blocks straight into it with speculative (wild-copy) stores, so bytes beyond the final pos are unspecified — never keep live data inside the declared capacity.

Compression

zxc_cstream_create

ZXC_EXPORT zxc_cstream* zxc_cstream_create(const zxc_cctx* cfg);   // NULL = defaults

Creates a push compression stream. ZXC_C_LEVEL, ZXC_C_BLOCK_SIZE and ZXC_C_CHECKSUM are copied from cfg at creation and frozen for the lifetime of the stream; the context itself may be freed or reused afterwards. ZXC_C_THREADS, ZXC_C_SEEKABLE and the progress callback are ignored on this path. A dictionary on cfg fails creation: push streams take no dictionary yet, and dropping it silently would produce archives that decode wrong elsewhere.

Returns: stream, or NULL on allocation failure or a dictionary on cfg.

zxc_cstream_free

ZXC_EXPORT void zxc_cstream_free(zxc_cstream* cs);

Releases all resources. Safe with NULL.

zxc_cstream_compress

ZXC_EXPORT int64_t zxc_cstream_compress(
    zxc_cstream*  cs,
    zxc_outbuf_t* out,
    zxc_inbuf_t*  in
);

Pushes input into the stream and drains compressed output:

  • emits the file header on the first call;
  • copies input into the internal block accumulator;
  • compresses one block whenever the accumulator fills, writing it into out (up to out->size);
  • returns when in is fully consumed and no more compressed bytes are pending, or when out has no room left.

Fully reentrant: if out fills mid-block, the next call resumes draining where it left off. Safe to call with empty input (drain-only).

Returns:

  • 0 — in fully consumed and no pending output;
  • >0 — bytes still pending in the staging buffer (drain out and call again);
  • <0 — zxc_error_t (sticky: subsequent calls return the same code).

zxc_cstream_end

ZXC_EXPORT int64_t zxc_cstream_end(zxc_cstream* cs, zxc_outbuf_t* out);

Finalises the stream: compresses any partial last block, emits the EOF block (8 B) and the file footer (3 to 17 B, plus 8 with a digest). Must be called to produce a valid ZXC file.

Reentrant the same way _compress is: loop until it returns 0.

After _end returns 0, the stream is in DONE state and any further call returns ZXC_ERROR_NULL_INPUT.

Returns:

  • 0 — finalisation complete;
  • >0 — bytes still pending (drain out and call again);
  • <0 — zxc_error_t.

zxc_cstream_in_size / zxc_cstream_out_size

ZXC_EXPORT size_t zxc_cstream_in_size(const zxc_cstream* cs);
ZXC_EXPORT size_t zxc_cstream_out_size(const zxc_cstream* cs);

Suggested buffer sizes for best throughput. The caller may use any size; these are purely performance hints.

Decompression

zxc_dstream_create

ZXC_EXPORT zxc_dstream* zxc_dstream_create(const zxc_dctx* cfg);   // NULL = defaults

Creates a push decompression stream. Only ZXC_D_CHECKSUM is copied from cfg (it controls whether the block checksums and archive digest are verified when the file carries them). A dictionary on cfg fails creation, and an archive whose header requires a dictionary fails with ZXC_ERROR_DICT_REQUIRED at the first decompress call.

Returns: stream, or NULL on allocation failure or a dictionary on cfg.

zxc_dstream_free

ZXC_EXPORT void zxc_dstream_free(zxc_dstream* ds);

Releases all resources. Safe with NULL.

zxc_dstream_decompress

ZXC_EXPORT int64_t zxc_dstream_decompress(
    zxc_dstream*  ds,
    zxc_outbuf_t* out,
    zxc_inbuf_t*  in
);

Drives the parser state machine (file header → blocks → EOF → optional SEK → footer). Each call makes as much progress as in and out allow.

Each call stops at the end of a frame, in->pos just past it: a caller after one frame stops there. A later call with input decodes the next frame (concatenated archives) into the same output. Input that does not open with the magic word is ZXC_ERROR_CORRUPT_DATA, from its first byte; a frame that does is checked like the first, so a bad header gets its own code (ZXC_ERROR_BAD_VERSION, ZXC_ERROR_DICT_REQUIRED, ...), and input that stops inside the magic word is simply not finished.

Returns:

  • >0 — number of decompressed bytes written into out this call;
  • 0 — either DONE (use zxc_dstream_finished to confirm) or no progress possible without more input;
  • <0 — zxc_error_t (sticky).

zxc_dstream_finished

ZXC_EXPORT int zxc_dstream_finished(const zxc_dstream* ds);

Returns 1 iff the last call ended on a validated footer, until a call with more input starts the next frame. Callers that have finished feeding input should check this to detect truncated streams: zxc_dstream_decompress returning 0 with no output is ambiguous (DONE vs need-more-input) — _finished disambiguates.

zxc_dstream_in_size / zxc_dstream_out_size

ZXC_EXPORT size_t zxc_dstream_in_size(const zxc_dstream* ds);
ZXC_EXPORT size_t zxc_dstream_out_size(const zxc_dstream* ds);

Suggested buffer sizes. Returns 0 if ds is NULL. Before the file header has been parsed, zxc_dstream_in_size() may return a default recommended input size hint (for example ZXC_BLOCK_SIZE_DEFAULT), because the actual block size is not known until it is learned from the header.

Threading

Each zxc_cstream / zxc_dstream is single-threaded: one context, one thread. Multiple contexts may be used concurrently from different threads.

Compression Example

zxc_cctx* cfg = zxc_create_cctx();
zxc_cctx_set_param(cfg, ZXC_C_LEVEL, 3);
zxc_cctx_set_param(cfg, ZXC_C_CHECKSUM, 1);
zxc_cstream* cs = zxc_cstream_create(cfg);   /* parameters copied */
zxc_free_cctx(cfg);                          /* no longer needed */

uint8_t in_buf[64*1024], out_buf[64*1024];
zxc_outbuf_t out = { out_buf, sizeof out_buf, 0 };

ssize_t n;
while ((n = read_some(in_buf, sizeof in_buf)) > 0) {
    zxc_inbuf_t in = { in_buf, (size_t)n, 0 };
    while (in.pos < in.size) {
        int64_t r = zxc_cstream_compress(cs, &out, &in);
        if (r < 0) goto fatal;
        if (out.pos > 0) { write_to_sink(out_buf, out.pos); out.pos = 0; }
    }
}

int64_t pending;
do {
    pending = zxc_cstream_end(cs, &out);
    if (pending < 0) goto fatal;
    if (out.pos > 0) { write_to_sink(out_buf, out.pos); out.pos = 0; }
} while (pending > 0);

zxc_cstream_free(cs);

11. Seekable API

Declared in zxc_seekable.h (opt-in: not included by <zxc.h> because most consumers do not need random-access decompression). The header is freestanding-safe — it does not pull <stdio.h> — so kernel / embedded consumers can include it directly and plug their own storage backend behind the zxc_reader_t callback. The FILE* convenience open (zxc_seekable_open_file) lives in zxc_stream.h instead. Seekable archives are produced with ZXC_C_SEEKABLE set to 1 on the compression context.

Creating a Seekable Archive

Set ZXC_C_SEEKABLE to 1 on a zxc_cctx. The same context drives the Buffer API (zxc_compress_cctx) and the Streaming API (zxc_stream_compress):

zxc_cctx* cctx = zxc_create_cctx();
zxc_cctx_set_param(cctx, ZXC_C_LEVEL, 3);
zxc_cctx_set_param(cctx, ZXC_C_SEEKABLE, 1);
int64_t csize = zxc_compress_cctx(cctx, src, src_size, dst, dst_cap);
zxc_free_cctx(cctx);

The resulting archive contains a Seek Table block (SEK) between the EOF block and the file footer. Standard decompressors handle seekable archives transparently, the seek table is skipped during sequential decompression.

zxc_seekable_open

ZXC_EXPORT zxc_seekable* zxc_seekable_open(const void* src, const size_t src_size);

Opens a seekable archive from a memory buffer. The buffer must remain valid for the lifetime of the handle. Concatenated seekable archives open as one: blocks and offsets run across the frames. Every frame must carry a seek table; block sizes, checksums and dictionaries may differ.

Returns: handle (0 blocks if the archive is empty), or NULL if the buffer is not a valid seekable archive.

zxc_seekable_open_file

#include <zxc_stream.h>

ZXC_EXPORT zxc_seekable* zxc_seekable_open_file(FILE* f);

Opens a seekable archive from a FILE*. The file must be seekable (not stdin/pipe). The file position is saved and restored after parsing. Internally builds a zxc_reader_t over pread()/ReadFile() and delegates to zxc_seekable_open_reader.

Declared in zxc_stream.h (which gathers all FILE*-flavored entry points) rather than zxc_seekable.h — that header stays freestanding so it can be included from kernel-space / freestanding environments.

Returns: handle on success, or NULL on error.

zxc_reader_t

typedef struct {
    int64_t (*read_at)(void* ctx, void* dst, size_t len, uint64_t offset);
    void*    ctx;
    uint64_t size;   // total size of the compressed archive in bytes
} zxc_reader_t;

Storage-agnostic reader interface. Lets the caller plug any backend that supports positional reads — mmap, HTTP Range: requests, S3, a custom VFS, vfs_read() in Linux kernel space, etc. — behind the seekable API.

read_at returns the number of bytes read (== len on success), or a negative zxc_error_t on failure. Short reads are treated as errors.

Thread safety: read_at MUST be safe to call concurrently from multiple threads when the resulting handle is used with zxc_seekable_decompress_range_mt(), or when calls on one handle overlap: all of them read through it, zxc_seekable_get_block_comp_size() included.

Lifetime: ctx and the backing storage must remain valid until zxc_seekable_free().

zxc_seekable_open_reader

ZXC_EXPORT zxc_seekable* zxc_seekable_open_reader(const zxc_reader_t* r);

Opens a seekable archive through a user-supplied reader. The reader is invoked to fetch each frame's footer and file header at open time (2 reads per frame, whatever the block count, plus one for the last frame's EOF/SEK block headers; at most 2^20 frames, past which the open fails). For each frame a range touches, one read fetches the seek table groups it covers. When the range starts in table group 0, that read also carries the frame's 16 bytes of EOF/SEK block headers; when it starts after group 0, those headers take a separate 16-byte read in addition to the group read. Decompression reads each block once. zxc_seekable_get_block_comp_size() reads the block's group, and the frame's EOF/SEK headers, per call. No FILE* is involved — this is the entry point to use for kernel space, networked storage, or any non-POSIX backend.

Returns: handle (0 blocks if the archive is empty), or NULL if r/r->read_at is NULL, r->size is 0, the archive is not seekable, or any read_at call fails.

zxc_seekable_get_num_blocks

ZXC_EXPORT uint64_t zxc_seekable_get_num_blocks(const zxc_seekable* s);

Returns the total number of data blocks in the archive. The count is derived from the footer, never stored in a field, so only the archive size bounds it.

zxc_seekable_get_decompressed_size

ZXC_EXPORT uint64_t zxc_seekable_get_decompressed_size(const zxc_seekable* s);

Returns the total decompressed size of the archive.

zxc_seekable_get_block_comp_size

ZXC_EXPORT uint32_t zxc_seekable_get_block_comp_size(
    const zxc_seekable* s,
    uint64_t            block_idx
);

Returns the compressed size (on-disk, including header) of a specific block, read from its seek table group, or 0 if block_idx is out of range or the group is unreadable or invalid. Checked against bounds only: a forged size within them comes back as is.

zxc_seekable_get_block_decomp_size

ZXC_EXPORT uint32_t zxc_seekable_get_block_decomp_size(
    const zxc_seekable* s,
    uint64_t            block_idx
);

Returns the decompressed size of a specific block.

zxc_seekable_decompress_range

ZXC_EXPORT int64_t zxc_seekable_decompress_range(
    zxc_seekable* s,
    void*         dst,
    size_t        dst_capacity,
    uint64_t      offset,
    size_t        len
);

Decompresses len bytes starting at byte offset in the original uncompressed data. Only the blocks overlapping the requested range are read and decompressed.

The seek table is not authenticated: a forged one can return another block's bytes with no error. Only verified checksums (zxc_seekable_set_checksum) bind a block to its position.

Returns: len on success, or negative zxc_error_t.

zxc_seekable_decompress_range_mt

ZXC_EXPORT int64_t zxc_seekable_decompress_range_mt(
    zxc_seekable* s,
    void*         dst,
    size_t        dst_capacity,
    uint64_t      offset,
    size_t        len,
    int           n_threads     // 0 = auto-detect
);

Multi-threaded variant. Each worker thread uses pread() (POSIX) or ReadFile() (Windows) for lock-free concurrent I/O. Falls back to single-threaded mode when n_threads <= 1 or the range spans a single block.

Returns: len on success, or negative zxc_error_t.

zxc_seekable_free

ZXC_EXPORT void zxc_seekable_free(zxc_seekable* s);

Frees a seekable handle and all associated resources. Safe to call with NULL.

zxc_write_seek_table

ZXC_EXPORT int64_t zxc_write_seek_table(
    uint8_t*        dst,
    size_t          dst_capacity,
    const uint32_t* comp_sizes,
    uint64_t        num_blocks
);

Low-level: writes a seek table (block header + entries) to dst.

Returns: bytes written, or negative zxc_error_t.

zxc_seek_table_size

ZXC_EXPORT size_t zxc_seek_table_size(uint64_t num_blocks);

Returns the encoded byte size of a seek table for num_blocks blocks.


11b. Dictionary API

Declared in <zxc_dict.h>. Provides dictionary training, serialization (.zxd format), and identification.

The simple path: most callers only need two functions — zxc_dict_train() to build a .zxd from samples, and zxc_dict_load() to unpack one for (de)compression. The remaining functions are the lower-level primitives those two are built from, exposed for advanced use (raw content-only dictionaries, retraining only the table, supplying externally-sourced content).

When the shared table applies. Huffman-coded literals exist only at levels 6 (ZXC_LEVEL_DENSITY) and 7 (ZXC_LEVEL_ULTRA), so the table only ever codes literals there. It is not ignored below that: dict_id is computed over the (content, table) pair at every level, so passing it changes the id written into the archive. The two directions are not symmetric: at compression time omitting it silently costs ratio; at decompression time it is mandatory, and an archive built with a .zxd and opened with its content alone is rejected up front with ZXC_ERROR_DICT_MISMATCH — at any level. zxc_dict_load() returns both parts in one call, so the safe habit is to always pass both.

Training samples. samples[i] may be NULL only if its size is 0 (else ZXC_ERROR_NULL_INPUT); a wrapped size sum is ZXC_ERROR_OVERFLOW.

zxc_dict_train

ZXC_EXPORT int64_t zxc_dict_train(
    const void* const* samples,
    const size_t*       sample_sizes,
    size_t              n_samples,
    void*               zxd_buf,        // receives the .zxd bytes
    size_t              zxd_capacity    // zxc_dict_save_bound(ZXC_DICT_SIZE_MAX) is always safe
);

One-call dictionary creation. Trains the content, trains the shared literal Huffman table from it, and serializes both into ready-to-write .zxd bytes. Hides the (otherwise sequential) train-content → train-table → save pipeline. Returns the number of .zxd bytes written, or a negative zxc_error_t code.

zxc_dict_train_content

ZXC_EXPORT int64_t zxc_dict_train_content(
    const void* const* samples,
    const size_t*       sample_sizes,
    size_t              n_samples,
    void*               dict_buf,
    size_t              dict_capacity    // max ZXC_DICT_SIZE_MAX (64KB - 1)
);

Trains a dictionary from a corpus of representative samples. Returns the size of the trained dictionary, or a negative zxc_error_t code.

zxc_dict_train_huf

ZXC_EXPORT int zxc_dict_train_huf(
    const void* const* samples,
    const size_t*       sample_sizes,
    size_t              n_samples,
    const void*         dict,        // content from zxc_dict_train_content
    size_t              dict_size,
    uint8_t*            huf_lengths_out  // ZXC_HUF_TABLE_SIZE (128) bytes
);

Trains the shared literal Huffman table for an already-trained dictionary. It compresses the samples with dict and derives canonical code lengths from the real post-LZ literal distribution. The 128-byte table is required by zxc_dict_save() and is attached at (de)compression time as the dict_huf argument of zxc_cctx_set_dict() / zxc_dctx_set_dict(). It only codes literals at levels 6-7, but binds into dict_id at every level (see the note at the top of this section). Returns ZXC_OK or a negative error code.

zxc_dict_compute_id

ZXC_EXPORT uint32_t zxc_dict_compute_id(const void* dict, size_t dict_size, const void* huf_lengths);

Returns the deterministic 32-bit dictionary ID. With huf_lengths NULL it hashes the raw content only — the id of an in-memory content-only dictionary (buffer API). With a 128-byte table it binds the (content, table) pair: hash(table, seed = hash(content)) — the value recorded in .zxd files and in archive headers when a shared table is attached. Returns 0 for NULL/empty content.

zxc_dict_save

ZXC_EXPORT int64_t zxc_dict_save(
    const void* content, size_t content_size,
    const void* huf_lengths,    // 128-byte table from zxc_dict_train_huf (required)
    void* buf, size_t buf_capacity
);

Serializes dictionary content and its shared Huffman table to the .zxd file format. The table is mandatory (pass the 128 bytes from zxc_dict_train_huf()); the stored dict_id covers both. Use zxc_dict_save_bound(content_size) to size buf.

zxc_dict_load

ZXC_EXPORT int zxc_dict_load(
    const void* buf, size_t buf_size,
    const void** content_out, size_t* content_size_out,
    const void** huf_out,        // 128-byte shared table; may be NULL
    uint32_t* dict_id_out        // may be NULL
);

Validates and parses a .zxd file from memory in one call. On success, content_out and huf_out (when non-NULL) both point into the input buffer (zero-copy) — pass them straight to zxc_cctx_set_dict() / zxc_dctx_set_dict() as dict / dict_huf. Returns ZXC_OK or a negative error code.

zxc_dict_huf

ZXC_EXPORT const void* zxc_dict_huf(const void* buf, size_t buf_size);

Standalone accessor for the 128-byte shared table inside a .zxd buffer (zero-copy), or NULL if invalid. Equivalent to the huf_out of zxc_dict_load(); use it when you only need the table.

Returns a zero-copy pointer to the 128-byte shared Huffman table inside a .zxd buffer (valid as long as buf is), or NULL if buf is not a valid .zxd file. Pass this as the dict_huf argument of zxc_cctx_set_dict() / zxc_dctx_set_dict().

zxc_dict_save_bound

ZXC_EXPORT size_t zxc_dict_save_bound(size_t content_size);

Returns the .zxd file size for a given content size (ZXC_DICT_HEADER_SIZE + content_size + ZXC_HUF_TABLE_SIZE).

zxc_seekable_set_dict

ZXC_EXPORT int zxc_seekable_set_dict(
    zxc_seekable* s, const void* dict, size_t dict_size,
    const void* dict_huf         // 128-byte table, or NULL
);

Attaches a dictionary to a seekable handle for random-access decompression. Pass the shared table as dict_huf (the archive's dict_id binds the pair); pass NULL for a raw content-only dictionary. Both buffers are copied internally. Call it once per dictionary the frames use: one no frame uses is ZXC_ERROR_DICT_MISMATCH (an archive that needs no dictionary ignores it), and a range through a frame whose dictionary is missing returns ZXC_ERROR_DICT_REQUIRED.

zxc_seekable_set_checksum

ZXC_EXPORT int zxc_seekable_set_checksum(zxc_seekable* s, int enabled);

Turns per-block checksum verification on or off for this handle. Off by default, as in the one-shot API: random access reads whole blocks and verifying them costs a hash over each one, so the caller decides. With it on, a block whose checksum does not match returns ZXC_ERROR_BAD_CHECKSUM; with it off, a corrupted block can decode to wrong bytes with no error. Checksums are seeded with each block's position, so verification also catches a block moved by a forged seek table.

Does nothing on an archive compressed without checksums. May be called at any time and applies from the next call on, on both the single- and multi-threaded range functions. Returns ZXC_OK, or ZXC_ERROR_NULL_INPUT if s is NULL.


12. Error Handling

zxc_error_name

ZXC_EXPORT const char* zxc_error_name(int code);

Returns a constant, null-terminated string for any error code (e.g. "ZXC_OK", "ZXC_ERROR_MEMORY").
Returns "ZXC_UNKNOWN_ERROR" for unrecognized codes.

Pattern

All functions that can fail use the same convention:

int64_t result = zxc_compress(src, src_size, dst, dst_cap, ZXC_LEVEL_DEFAULT);
if (result < 0) {
    fprintf(stderr, "Error: %s\n", zxc_error_name((int)result));
}

Parameter errors

The setters validate up front, so a configuration mistake surfaces at the zxc_*ctx_set_* call, not at the compress or decompress call that follows:

Setter Code When
zxc_cctx_set_param ZXC_ERROR_BAD_LEVEL ZXC_C_LEVEL outside 1–7, or a raise into ZXC_LEVEL_DENSITY on a static workspace carved below it
zxc_cctx_set_param ZXC_ERROR_BAD_BLOCK_SIZE ZXC_C_BLOCK_SIZE out of range, not a power of two, or different from a static context's pinned size
zxc_cctx_set_param, zxc_dctx_set_param, zxc_cctx_get_param, zxc_dctx_get_param, zxc_cparam_bounds, zxc_dparam_bounds ZXC_ERROR_BAD_PARAM Unknown parameter identifier, or a value outside the bounds of ZXC_C_THREADS, ZXC_C_CHECKSUM, ZXC_C_SEEKABLE, ZXC_D_THREADS, ZXC_D_CHECKSUM
zxc_cctx_set_dict, zxc_dctx_set_dict ZXC_ERROR_DICT_TOO_LARGE dict_size above ZXC_DICT_SIZE_MAX
zxc_cctx_set_dict, zxc_dctx_set_dict ZXC_ERROR_DICT_UNSUPPORTED Dictionary on a static context

A refused value leaves the parameter unchanged, and the context stays usable. ZXC_ERROR_BAD_PARAM is also what a binding gets from zxc_cparam_bounds() for an identifier the loaded library predates.


13. Thread Safety

API Layer Safe to call concurrently? Notes
Buffer API Yes (stateless) Each call is self-contained. Multiple threads can compress/decompress simultaneously with independent buffers.
Block API Per-context Uses zxc_cctx / zxc_dctx: same rule as Context API. Create one context per thread.
Context API Per-context A single zxc_cctx / zxc_dctx must not be shared between threads. Create one context per thread.
Static Context API Per-handle Same per-context rule as the dynamic Context API. Allocate one workspace per thread; the static handle does not introduce any cross-thread synchronisation.
Streaming API Per-call Each zxc_stream_* call manages its own thread pool internally. Do not call from multiple threads on the same FILE*. The cfg context is only read, so one may serve several concurrent calls as long as no thread sets a parameter on it meanwhile.
Seekable API Per-handle A single zxc_seekable handle must not be shared between threads for single-threaded decompression. Use zxc_seekable_decompress_range_mt() for parallel access.
zxc_error_name Yes Returns a pointer to a static string.

14. Exported Symbols Summary

The shared library exports 86 symbols (verified with nm -gU):

# Symbol API Layer Header
1 zxc_min_level Info zxc_buffer.h
2 zxc_max_level Info zxc_buffer.h
3 zxc_default_level Info zxc_buffer.h
4 zxc_version_string Info zxc_buffer.h
5 zxc_version_number Info zxc_buffer.h
6 zxc_version_codename Info zxc_buffer.h
7 zxc_frame_info_size Info zxc_buffer.h
8 zxc_compress_bound Buffer zxc_buffer.h
9 zxc_compress Buffer zxc_buffer.h
10 zxc_decompress Buffer zxc_buffer.h
11 zxc_decompress_inplace_bound Buffer zxc_buffer.h
12 zxc_decompress_inplace Buffer zxc_buffer.h
13 zxc_get_decompressed_size Buffer zxc_buffer.h
14 zxc_get_dict_id Dictionary zxc_buffer.h
15 zxc_get_frame_info Buffer zxc_buffer.h
16 zxc_get_last_frame_info Buffer zxc_buffer.h
17 zxc_compress_block_bound Block zxc_buffer.h
18 zxc_decompress_block_bound Block zxc_buffer.h
19 zxc_compress_block Block zxc_buffer.h
20 zxc_decompress_block Block zxc_buffer.h
21 zxc_decompress_block_safe Block zxc_buffer.h
22 zxc_estimate_cctx_size Block zxc_buffer.h
23 zxc_cparam_bounds Parameters zxc_opts.h
24 zxc_dparam_bounds Parameters zxc_opts.h
25 zxc_create_cctx Context zxc_buffer.h
26 zxc_free_cctx Context zxc_buffer.h
27 zxc_cctx_set_param Context zxc_buffer.h
28 zxc_cctx_get_param Context zxc_buffer.h
29 zxc_cctx_set_dict Context zxc_buffer.h
30 zxc_cctx_set_progress Context zxc_buffer.h
31 zxc_cctx_reset Context zxc_buffer.h
32 zxc_compress_cctx Context zxc_buffer.h
33 zxc_create_dctx Context zxc_buffer.h
34 zxc_free_dctx Context zxc_buffer.h
35 zxc_dctx_set_param Context zxc_buffer.h
36 zxc_dctx_get_param Context zxc_buffer.h
37 zxc_dctx_set_dict Context zxc_buffer.h
38 zxc_dctx_set_progress Context zxc_buffer.h
39 zxc_dctx_reset Context zxc_buffer.h
40 zxc_decompress_dctx Context zxc_buffer.h
41 zxc_decompress_inplace_dctx Context zxc_buffer.h
42 zxc_static_cctx_workspace_size Static Context zxc_buffer.h
43 zxc_init_static_cctx Static Context zxc_buffer.h
44 zxc_static_dctx_workspace_size Static Context zxc_buffer.h
45 zxc_init_static_dctx Static Context zxc_buffer.h
46 zxc_stream_compress Streaming zxc_stream.h
47 zxc_stream_decompress Streaming zxc_stream.h
48 zxc_stream_get_decompressed_size Streaming zxc_stream.h
49 zxc_stream_get_frame_info Streaming zxc_stream.h
50 zxc_stream_get_last_frame_info Streaming zxc_stream.h
51 zxc_cstream_create Push Streaming zxc_pstream.h
52 zxc_cstream_free Push Streaming zxc_pstream.h
53 zxc_cstream_compress Push Streaming zxc_pstream.h
54 zxc_cstream_end Push Streaming zxc_pstream.h
55 zxc_cstream_in_size Push Streaming zxc_pstream.h
56 zxc_cstream_out_size Push Streaming zxc_pstream.h
57 zxc_dstream_create Push Streaming zxc_pstream.h
58 zxc_dstream_free Push Streaming zxc_pstream.h
59 zxc_dstream_decompress Push Streaming zxc_pstream.h
60 zxc_dstream_finished Push Streaming zxc_pstream.h
61 zxc_dstream_in_size Push Streaming zxc_pstream.h
62 zxc_dstream_out_size Push Streaming zxc_pstream.h
63 zxc_seekable_open Seekable zxc_seekable.h
64 zxc_seekable_open_reader Seekable zxc_seekable.h
65 zxc_seekable_open_file Seekable zxc_stream.h
66 zxc_seekable_get_num_blocks Seekable zxc_seekable.h
67 zxc_seekable_get_decompressed_size Seekable zxc_seekable.h
68 zxc_seekable_get_block_comp_size Seekable zxc_seekable.h
69 zxc_seekable_get_block_decomp_size Seekable zxc_seekable.h
70 zxc_seekable_decompress_range Seekable zxc_seekable.h
71 zxc_seekable_decompress_range_mt Seekable zxc_seekable.h
72 zxc_seekable_set_dict Seekable zxc_seekable.h
73 zxc_seekable_set_checksum Seekable zxc_seekable.h
74 zxc_seekable_free Seekable zxc_seekable.h
75 zxc_write_seek_table Seekable zxc_seekable.h
76 zxc_seek_table_size Seekable zxc_seekable.h
77 zxc_dict_train Dictionary zxc_dict.h
78 zxc_dict_train_content Dictionary zxc_dict.h
79 zxc_dict_train_huf Dictionary zxc_dict.h
80 zxc_dict_compute_id Dictionary zxc_dict.h
81 zxc_dict_save Dictionary zxc_dict.h
82 zxc_dict_load Dictionary zxc_dict.h
83 zxc_dict_huf Dictionary zxc_dict.h
84 zxc_dict_read_id Dictionary zxc_dict.h
85 zxc_dict_save_bound Dictionary zxc_dict.h
86 zxc_error_name Error zxc_error.h

No internal symbols leak into the public ABI. FMV dispatch variants (_default, _neon32, _avx2, _avx512) are compiled with -fvisibility=hidden and are not exported.