This document provides complete, working examples for using the ZXC compression library in C.
Table of Contents
Buffer API (In-Memory)
Ideal for small assets or simple integrations. Thread-safe and ready for highly concurrent environments (Go routines, Node.js workers, Python threads).
#include "zxc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
const char* original = "Hello, ZXC! This is a sample text for compression.";
size_t original_size = strlen(original) + 1;
void* compressed = malloc(max_compressed_size);
void* decompressed = malloc(original_size);
if (!compressed || !decompressed) {
fprintf(stderr, "Memory allocation failed\n");
free(compressed);
free(decompressed);
return 1;
}
.checksum_enabled = 1,
};
original,
original_size,
compressed,
max_compressed_size,
&c_opts
);
if (compressed_size <= 0) {
fprintf(stderr, "Compression failed (error %lld)\n", (long long)compressed_size);
free(compressed);
free(decompressed);
return 1;
}
printf("Original size: %zu bytes\n", original_size);
printf("Compressed size: %lld bytes (%.1f%% ratio)\n",
(long long)compressed_size,
100.0 * (double)compressed_size / (double)original_size);
compressed,
(size_t)compressed_size,
decompressed,
original_size,
&d_opts
);
if (decompressed_size <= 0) {
fprintf(stderr, "Decompression failed (error %lld)\n", (long long)decompressed_size);
free(compressed);
free(decompressed);
return 1;
}
if ((size_t)decompressed_size == original_size &&
memcmp(original, decompressed, original_size) == 0) {
printf("Success! Data integrity verified.\n");
printf("Decompressed: %s\n", (char*)decompressed);
} else {
fprintf(stderr, "Data mismatch after decompression\n");
}
free(compressed);
free(decompressed);
return 0;
}
int64_t zxc_decompress(const void *src, const size_t src_size, void *dst, const size_t dst_capacity, const zxc_decompress_opts_t *opts)
Decompresses a complete ZXC archive.
int64_t zxc_compress(const void *src, const size_t src_size, void *dst, const size_t dst_capacity, const zxc_compress_opts_t *opts)
Compresses a buffer into a complete ZXC archive.
uint64_t zxc_compress_bound(const size_t input_size)
Maximum compressed size for an input of input_size bytes.
@ ZXC_LEVEL_DEFAULT
Recommended default: better ratio and decode speed than LZ4.
int level
Compression level 1-7 (0 = default, ZXC_LEVEL_DEFAULT).
Options for decompression.
int checksum_enabled
1 to verify the block checksums and archive digest, 0 to skip.
Compilation:
gcc -o buffer_example buffer_example.c -I include -L build -lzxc_lib
Stream API (Multi-Threaded)
For large files, use the streaming API to process data in parallel chunks. The FILE*-based entry points live in zxc_stream.h, which the freestanding <zxc.h> umbrella does not pull (so kernel/embedded builds stay clean of <stdio.h>). Userspace consumers include it explicitly.
#include "zxc.h"
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]) {
if (argc != 4) {
fprintf(stderr, "Usage: %s <input_file> <compressed_file> <output_file>\n", argv[0]);
return 1;
}
const char* input_path = argv[1];
const char* compressed_path = argv[2];
const char* output_path = argv[3];
printf("Compressing '%s' to '%s'...\n", input_path, compressed_path);
FILE* f_in = fopen(input_path, "rb");
if (!f_in) {
fprintf(stderr, "Error: cannot open '%s'\n", input_path);
return 1;
}
FILE* f_out = fopen(compressed_path, "wb");
if (!f_out) {
fprintf(stderr, "Error: cannot create '%s'\n", compressed_path);
fclose(f_in);
return 1;
}
.checksum_enabled = 1,
};
fclose(f_in);
fclose(f_out);
if (compressed_bytes < 0) {
fprintf(stderr, "Compression failed (error %lld)\n", (long long)compressed_bytes);
return 1;
}
printf("Compression complete: %lld bytes written\n", (long long)compressed_bytes);
printf("\nDecompressing '%s' to '%s'...\n", compressed_path, output_path);
FILE* f_compressed = fopen(compressed_path, "rb");
if (!f_compressed) {
fprintf(stderr, "Error: cannot open '%s'\n", compressed_path);
return 1;
}
FILE* f_decompressed = fopen(output_path, "wb");
if (!f_decompressed) {
fprintf(stderr, "Error: cannot create '%s'\n", output_path);
fclose(f_compressed);
return 1;
}
fclose(f_compressed);
fclose(f_decompressed);
if (decompressed_bytes < 0) {
fprintf(stderr, "Decompression failed (error %lld)\n", (long long)decompressed_bytes);
return 1;
}
printf("Decompression complete: %lld bytes written\n", (long long)decompressed_bytes);
printf("\nSuccess! Verify the output file matches the original.\n");
return 0;
}
int64_t zxc_stream_decompress(FILE *f_in, FILE *f_out, const zxc_decompress_opts_t *opts)
Decompresses one file stream into another.
int64_t zxc_stream_compress(FILE *f_in, FILE *f_out, const zxc_compress_opts_t *opts)
Compresses one file stream into another.
int n_threads
Worker thread count (0 = auto-detect CPU cores).
int n_threads
Worker thread count (0 = auto-detect CPU cores).
FILE*-flavored variants of the ZXC API.
Compilation:
gcc -o stream_example stream_example.c -I include -L build -lzxc_lib -lpthread -lm
Usage:
./stream_example large_file.bin compressed.zxc decompressed.bin
Features demonstrated:
- Multi-threaded parallel processing (auto-detects CPU cores)
- Checksum validation for data integrity
- Error handling for file operations
Reusable Context API
For tight loops - such as filesystem plug-ins (squashfs, dwarfs, erofs) - where allocating and freeing internal buffers on every call would add latency, ZXC provides opaque reusable contexts.
Internal buffers are only reallocated when the block size changes, so the common case (same block size, different data) is completely allocation-free.
Options are sticky: settings passed via opts to zxc_create_cctx() or zxc_compress_cctx() are remembered and reused on subsequent calls where opts is NULL.
#include "zxc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BLOCK_SIZE (64 * 1024)
#define NUM_BLOCKS 32
int main(void) {
.checksum_enabled = 0,
.block_size = BLOCK_SIZE,
};
if (!cctx || !dctx) {
fprintf(stderr, "Context creation failed\n");
return 1;
}
uint8_t* comp = malloc(comp_cap);
uint8_t* src = malloc(BLOCK_SIZE);
uint8_t* dec = malloc(BLOCK_SIZE);
for (int i = 0; i < NUM_BLOCKS; i++) {
for (size_t j = 0; j < BLOCK_SIZE; j++) src[j] = (uint8_t)(i ^ j);
if (csz <= 0) { fprintf(stderr, "Block %d: compress error %lld\n", i, (long long)csz); break; }
if (dsz != BLOCK_SIZE || memcmp(src, dec, BLOCK_SIZE) != 0) {
fprintf(stderr, "Block %d: roundtrip mismatch\n", i);
break;
}
}
printf("Level-5 compressed: %lld bytes\n", (long long)csz);
printf("Processed %d blocks of %d KB - no per-block allocation.\n",
NUM_BLOCKS, BLOCK_SIZE / 1024);
free(src); free(comp); free(dec);
return 0;
}
struct zxc_cctx_s zxc_cctx
Opaque reusable compression context (see zxc_create_cctx).
struct zxc_dctx_s zxc_dctx
Opaque reusable decompression context (see zxc_create_dctx).
zxc_cctx * zxc_create_cctx(const zxc_compress_opts_t *opts)
Creates a reusable compression context.
int64_t zxc_compress_cctx(zxc_cctx *cctx, const void *src, size_t src_size, void *dst, size_t dst_capacity, const zxc_compress_opts_t *opts)
Compresses data using a reusable context.
void zxc_free_cctx(zxc_cctx *cctx)
Frees a compression context and its resources.
zxc_dctx * zxc_create_dctx(void)
Creates a reusable decompression context.
int64_t zxc_decompress_dctx(zxc_dctx *dctx, const void *src, size_t src_size, void *dst, size_t dst_capacity, const zxc_decompress_opts_t *opts)
Decompresses data using a reusable context.
void zxc_free_dctx(zxc_dctx *dctx)
Frees a decompression context and its resources.
Compilation:
gcc -o ctx_example ctx_example.c -I include -L build -lzxc_lib
Seekable Reader (Custom Storage Backend)
The seekable API also accepts a user-supplied reader callback, letting you back random-access decompression with any storage that supports positional reads: mmap, an HTTP range-request client, an S3 object, a custom VFS, vfs_read() in kernel space, etc.
The reader exposes a single primitive:
typedef struct {
int64_t (*read_at)(void* ctx, void* dst, size_t len, uint64_t offset);
void* ctx;
uint64_t size;
Storage-agnostic reader interface for seekable archives.
read_at MUST be safe to call concurrently from multiple threads if you intend to use zxc_seekable_decompress_range_mt(). The single-threaded path makes no concurrent calls.
The example below wires the reader to an mmap()'d archive file, then decompresses an arbitrary byte range — no FILE* is involved.
#include "zxc.h"
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
typedef struct {
const uint8_t* base;
uint64_t size;
} mmap_reader_ctx_t;
static int64_t mmap_read_at(void* ctx, void* dst, size_t len, uint64_t offset) {
mmap_reader_ctx_t* m = (mmap_reader_ctx_t*)ctx;
if (offset > m->size || len > m->size - offset) return -1;
memcpy(dst, m->base + offset, len);
return (int64_t)len;
}
int main(int argc, char** argv) {
if (argc != 4) {
fprintf(stderr, "Usage: %s <archive.zxc> <offset> <length>\n", argv[0]);
return 1;
}
int fd = open(argv[1], O_RDONLY);
if (fd < 0) { perror("open"); return 1; }
struct stat st;
if (fstat(fd, &st) != 0) { perror("fstat"); close(fd); return 1; }
void* mapping = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapping == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
mmap_reader_ctx_t mctx = { .base = (const uint8_t*)mapping,
.size = (uint64_t)st.st_size };
.ctx = &mctx,
.size = (uint64_t)st.st_size };
if (!s) {
fprintf(stderr, "Not a valid seekable ZXC archive\n");
munmap(mapping, (size_t)st.st_size);
close(fd);
return 1;
}
fprintf(stderr, "Archive: %llu blocks, %llu bytes decompressed.\n",
const uint64_t offset = strtoull(argv[2], NULL, 0);
const size_t length = (size_t)strtoull(argv[3], NULL, 0);
uint8_t* out = malloc(length);
if (!out) {
zxc_seekable_free(s); munmap(mapping, st.st_size); close(fd);
return 1; }
if (n < 0) {
fprintf(stderr, "decompress_range failed: %lld\n", (long long)n);
return 1;
}
fwrite(out, 1, (size_t)n, stdout);
free(out);
munmap(mapping, (size_t)st.st_size);
close(fd);
return 0;
}
int64_t zxc_seekable_decompress_range_mt(zxc_seekable *s, void *dst, const size_t dst_capacity, const uint64_t offset, const size_t len, int n_threads)
Multi-threaded variant of zxc_seekable_decompress_range().
void zxc_seekable_free(zxc_seekable *s)
Frees a seekable handle and all associated resources.
uint64_t zxc_seekable_get_num_blocks(const zxc_seekable *s)
Returns the total number of blocks in the seekable archive.
uint64_t zxc_seekable_get_decompressed_size(const zxc_seekable *s)
Returns the total decompressed size of the seekable archive.
zxc_seekable * zxc_seekable_open_reader(const zxc_reader_t *r)
Opens a seekable archive through a user-supplied reader.
struct zxc_seekable_s zxc_seekable
Opaque handle for a seekable ZXC archive.
int64_t(* read_at)(void *ctx, void *dst, size_t len, uint64_t offset)
Reads exactly len bytes at offset into dst.
Seekable compression and random-access decompression API.
Compilation:
gcc -o seekable_reader seekable_reader.c -I include -L build -lzxc -lpthread
Other backends. The same pattern applies to anything addressable by offset: an HTTP Range: GET (return -1 on a non-206 response), an S3 GetObject with --range, a kernel vfs_read(file, ..., &pos), a custom VFS plug-in. As long as read_at returns exactly the requested length (or a negative zxc_error_t code), the seekable API treats it as a transparent backend.
Compressing Numeric Data (Pre-Filters)
ZXC is a pure LZ byte codec: it compresses by finding repeated byte sequences and entropy-coding the residue. Arrays of numbers (timestamps, IDs, counters, sensor readings, floats) carry a lot of redundancy — but it lives in their numeric structure (smooth progression, shared magnitude), which is invisible at the byte level, so a raw zxc_compress often barely compresses them.
The fix is a reversible pre-filter: a cheap, lossless transform applied before compression and inverted after decompression. It does not compress anything itself — it re-expresses the numeric structure as byte-level redundancy that ZXC can then exploit:
compress : data -> [filter] -> zxc_compress -> archive
decompress: archive -> zxc_decompress -> [inverse filter] -> data
ZXC keeps the filter out of the format on purpose: the codec stays pure, and you own the transform — including remembering which one you applied, in your own schema/metadata, so you can invert it. The two most useful filters are delta (correlated sequences) and byte-shuffle (wide values whose high bytes are similar, e.g. floats).
#include "zxc.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
static void delta_encode_i64(int64_t *a, size_t n) {
for (size_t i = n; i-- > 1;) a[i] -= a[i - 1];
}
static void delta_decode_i64(int64_t *a, size_t n) {
for (size_t i = 1; i < n; i++) a[i] += a[i - 1];
}
static void shuffle_encode(const uint8_t *src, uint8_t *dst, size_t n, size_t w) {
for (size_t b = 0; b < w; b++)
for (size_t i = 0; i < n; i++) dst[b * n + i] = src[i * w + b];
}
static void shuffle_decode(const uint8_t *src, uint8_t *dst, size_t n, size_t w) {
for (size_t b = 0; b < w; b++)
for (size_t i = 0; i < n; i++) dst[i * w + b] = src[b * n + i];
}
int main(void) {
const size_t n = 100000;
int64_t *ts = malloc(n * sizeof *ts);
int64_t t = 1700000000000000LL;
for (size_t i = 0; i < n; i++) { t += 1000000 + (int64_t)(i % 7); ts[i] = t; }
const size_t raw = n * sizeof *ts;
void *comp = malloc(cap);
delta_encode_i64(ts, n);
int64_t *out = malloc(raw);
delta_decode_i64(out, n);
free(ts); free(comp); free(out);
return 0;
}
Choosing a filter (you must record this choice yourself):
| Data | Filter | Inverse |
| Monotonic / correlated integers (timestamps, IDs, counters) | delta | prefix sum |
| Fixed-cadence series (drifting stride) | delta twice (delta-of-delta) | prefix sum twice |
| Float/double columns, wide integers | shuffle (byte transpose) | un-shuffle |
| Floats varying slightly between samples | XOR with previous | XOR again |
The archive does not record the filter. ZXC stores only the compressed bytes; it has no idea a filter was applied. Your application is responsible for remembering — in its own schema or metadata — which filter and elem_size it used, so it can run the matching inverse after zxc_decompress. Apply the filter only to genuinely numeric, type-known data; on arbitrary bytes it will not help (and may hurt).
</blockquote>
Using a Pre-Trained Dictionary
For corpora of small, similar payloads (JSON records, log lines, RPC messages, small files), a pre-trained dictionary substantially improves the ratio. The dictionary is logically prepended to the LZ window at the start of each block, so even the first bytes of a tiny payload can match against it.
How it ties together:
- A dictionary is up to 64 KB of content plus a shared literal Huffman table (128 bytes, derived from the real post-LZ literal distribution). A
.zxd file always carries both. zxc_dict_train() builds the whole thing from samples in one call; zxc_dict_load() unpacks it in one call.
- It is identified by a **
dict_id** that binds the **(content, table)** pair, stored in the .zxd file and in every archive compressed with it.
- A dictionary-compressed archive records
HAS_DICTIONARY + the dict_id in its header, but not the dictionary itself — you must provide the same dictionary (content and table) to decompress. A decoder reads the required id with zxc_get_dict_id().
- The biggest gains are on small blocks (4–128 KB): at level 6 the shared table lets literal sections skip their 128-byte per-block Huffman header, and the decoder builds the table once instead of once per block. On large blocks the data builds its own match history, so a dictionary helps little.
- Raw content-only dictionaries are still possible via the library API (pass
dict/dict_size with dict_huf = NULL) for ad-hoc priming where a trained table adds nothing (binary data, levels 1–5). Such archives never use the shared-table literal encoding. The .zxd file format always includes the table.
CLI quickstart
# Train from a corpus of files -> writes dictionary_<dict_id>.zxd in the
# current directory (use -o DIR/ or -o FILE to choose the destination).
zxc --train ./corpus/*
# Compress / decompress with the dictionary (required at both ends).
zxc -D dictionary_1a2b3c4d.zxd -z record.json
zxc -D dictionary_1a2b3c4d.zxd -d record.json.zxc -o record.json
C API
#include "zxc.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
const char *s0 = "{\"event\":\"login\",\"user_id\":1001,\"ok\":true}";
const char *s1 = "{\"event\":\"logout\",\"user_id\":1002,\"ok\":true}";
const void *samples[] = { s0, s1 };
const size_t sizes[] = { strlen(s0), strlen(s1) };
uint8_t *zxd = malloc(zbnd);
if (zxd_size < 0) { fprintf(stderr, "dict train failed\n"); return 1; }
const void *content, *table; size_t content_size; uint32_t dict_id;
zxc_dict_load(zxd, (
size_t)zxd_size, &content, &content_size, &table, &dict_id);
const char *msg = "{\"event\":\"login\",\"user_id\":2003,\"ok\":true}";
size_t n = strlen(msg) + 1;
uint8_t *comp = malloc(cap);
.dict_size = content_size, .dict_huf = table };
if (csize < 0) { fprintf(stderr, "compress failed\n"); return 1; }
uint8_t *out = malloc(n);
.dict_huf = table };
if (dsize != (int64_t)n || memcmp(msg, out, n) != 0) {
fprintf(stderr, "round-trip failed\n");
return 1;
}
free(zxd); free(comp); free(out);
return 0;
}
int64_t zxc_dict_train(const void *const *samples, const size_t *sample_sizes, size_t n_samples, void *zxd_buf, size_t zxd_capacity)
One-call dictionary creation: content + shared table, serialized to ready-to-write ....
size_t zxc_dict_save_bound(size_t content_size)
Returns the .zxd file size for a given content size.
int zxc_dict_load(const void *buf, size_t buf_size, const void **content_out, size_t *content_size_out, const void **huf_out, uint32_t *dict_id_out)
Loads and validates a .zxd dictionary file from a memory buffer.
#define ZXC_DICT_SIZE_MAX
Maximum dictionary content size in bytes (64 KB - 1).
const void * dict
Pre-trained dictionary content (NULL = none).
Pre-trained dictionary API for ZXC compression.
The content and table pointers returned by zxc_dict_load() aim into zxd (zero-copy), so keep zxd alive until you're done compressing or decompressing. The lower-level primitives (zxc_train_dict + zxc_train_dict_huf + zxc_dict_save, and zxc_dict_huf) remain available for advanced use — e.g. a raw content-only dictionary with no table (dict set, dict_huf = NULL) for ad-hoc window priming where a trained table adds nothing.
static int64_t build_zxd(const void* const* samples, const size_t* sizes, size_t n_samples,
void* zxd_buf, size_t zxd_cap) {
if (csize <= 0) { free(content); return csize; }
const int rc =
zxc_train_dict_huf(samples, sizes, n_samples, content, (
size_t)csize, table);
if (rc !=
ZXC_OK) { free(content);
return rc; }
const int64_t zsize =
zxc_dict_save(content, (
size_t)csize, table, zxd_buf, zxd_cap);
free(content);
return zsize;
}
int zxc_train_dict_huf(const void *const *samples, const size_t *sample_sizes, size_t n_samples, const void *dict, size_t dict_size, uint8_t *huf_lengths_out)
Trains the shared literal Huffman table for an already-trained dictionary.
int64_t zxc_train_dict(const void *const *samples, const size_t *sample_sizes, size_t n_samples, void *dict_buf, size_t dict_capacity)
Trains a dictionary from a corpus of samples.
int64_t zxc_dict_save(const void *content, size_t content_size, const void *huf_lengths, void *buf, size_t buf_capacity)
Serializes dictionary content and its shared Huffman table to .zxd.
#define ZXC_HUF_TABLE_SIZE
Size of a packed literal Huffman code-lengths table (256 symbols, 4 bits each): the shared table in a...
@ ZXC_ERROR_MEMORY
Memory allocation failure.
@ ZXC_OK
Success (no error).
Error contract
Decoding a dictionary archive enforces that the right dictionary is supplied:
| Situation | Result |
No dictionary supplied (opts == NULL or dict == NULL) | ZXC_ERROR_DICT_REQUIRED |
Wrong dictionary, or table missing/mismatched (dict_id mismatch) | ZXC_ERROR_DICT_MISMATCH |
Dictionary larger than ZXC_DICT_SIZE_MAX (64 KB) | ZXC_ERROR_DICT_TOO_LARGE |
Because the dict_id binds the **(content, table)** pair, decoding an archive that was compressed with a table requires passing that same table via dict_huf — omitting it (or supplying a different one) fails the id check with ZXC_ERROR_DICT_MISMATCH. So a decoder reads zxc_get_dict_id() first, fetches the matching .zxd, and passes its loaded content + zxc_dict_huf() table via zxc_decompress_opts_t. See docs/FORMAT.md §12 for the on-disk dictionary format and §3.1 for the header fields.
Meson Integration
Consuming zxc as a Meson subproject or through WrapDB — the .wrap file, the meson.build dependency and the build commands — is documented in INSTALL.md.
Language Bindings
For non-C languages, see the official bindings:
| Language | Package | Install Command | Documentation |
| Rust | crates.io | cargo add zxc-compress | README |
| Python | PyPI | pip install zxc-compress | README |
| Node.js | npm | npm install zxc-compress | README |
| Go | go get | go get github.com/hellobertrand/zxc/wrappers/go | README |
Community-maintained: