Implementing Streaming Compression and Decompression with Kanzi: A Complete C++ Guide
Kanzi provides a fully-streaming API through CompressedOutputStream and CompressedInputStream classes that process data incrementally in configurable blocks, automatically distributing work across CPU cores via task-based parallelism.
The flanglet/kanzi-cpp repository delivers a high-performance compression engine designed for scenarios where data arrives incrementally or total size is unknown beforehand. Unlike batch-oriented compressors, Kanzi’s I/O layer in src/io/ enables real-time, block-by-block processing while maintaining random-access capabilities and optional integrity checking.
Core Streaming Architecture
Kanzi’s streaming implementation centers on two primary I/O classes that manage the full lifecycle of compression and decompression pipelines.
CompressedOutputStream (Compression Pipeline)
Located in src/io/CompressedOutputStream.hpp and src/io/CompressedOutputStream.cpp, this class manages block buffering, transform application, entropy encoding, and bit-stream output. Key methods include writeHeader() for emitting stream metadata, processBuffer() for accumulating input data, and submitBlock() for dispatching work units. The class uses internal _buffers to accumulate raw data until reaching the configured block size or receiving an explicit flush() call.
CompressedInputStream (Decompression Pipeline)
Defined in src/io/CompressedInputStream.hpp and src/io/CompressedInputStream.cpp, this component handles header parsing via readHeader(), on-demand block decoding through submitBlock(), and seekable random access via seek(bitPos). The stream maintains an _available byte counter to track ready-to-read data and resets the pipeline when seeking to block boundaries.
Task-Based Concurrency
When CONCURRENCY_ENABLED is defined (the default for modern compilers), Kanzi utilizes a ThreadPool from src/concurrent.hpp to parallelize block processing. The headers define template classes EncodingTask (in CompressedOutputStream.hpp) and DecodingTask (in CompressedInputStream.hpp) that encapsulate the transform-plus-entropy work for individual blocks. These tasks execute XXHash32/64 checksum computation (implemented in src/util/XXHash.hpp) alongside codec operations.
How Streaming Compression Works
The compression workflow follows a strict pipeline from context configuration to bit-stream finalization.
-
Context Configuration – Create a
Contextobject (defined insrc/Context.hpp) specifying the entropy codec (e.g., "ANS0"), transform pipeline (e.g., "BWT+LZ"), block size, and job count. This context is reusable across multiple streams. -
Stream Construction – Instantiate
CompressedOutputStreamwith an underlyingOutputStream(file, socket, or memory buffer). The constructor immediately callswriteHeader()to emit a bit-stream header recording these parameters. -
Block Buffering – Incoming data accumulates in internal
_buffers. When the buffer reaches the configured block size or uponflush(),processBuffer()copies the data to anEncodingTask. -
Parallel Processing –
submitBlock()hands tasks to the thread pool. EachEncodingTaskapplies transforms viaTransformFactory(fromsrc/transform/TransformFactory.hpp), encodes entropy viaEntropyEncoderFactory(fromsrc/entropy/EntropyEncoderFactory.hpp), and writes results into a sharedDefaultOutputBitStream(_obs), maintaining byte alignment on block boundaries. -
Finalization – Calling
close()flushes any pending blocks, finalizes the underlying stream, and optionally writes a footer containing the total uncompressed size.
#include <fstream>
#include "kanzi/CompressedOutputStream.hpp"
#include "kanzi/Context.hpp"
int main() {
std::ofstream fout("data.knz", std::ios::binary);
kanzi::Context ctx;
ctx.putString("entropy", "ANS0");
ctx.putString("transform", "BWT+LZ");
ctx.putInt("blockSize", 4*1024*1024); // 4 MiB blocks
ctx.putInt("jobs", 4); // 4 threads
kanzi::CompressedOutputStream cos(fout, ctx);
std::string data = "Streaming data chunk...";
cos.write(data.c_str(), data.size());
cos.close(); // Flushes automatically
}
How Streaming Decompression Works
Decompression reverses the pipeline using lazy evaluation and supports random access at block granularity.
-
Header Parsing – The
CompressedInputStreamconstructor callsreadHeader()to populate aContextwith the original compression parameters, unless operating in headerless mode. -
On-Demand Decoding – When
read()requests data, the stream checks_availablebytes. If insufficient,submitBlock()creates aDecodingTaskfor the next compressed block. -
Block Processing – The task reads per-block metadata (size, checksum, transform identifiers), decodes the entropy layer, applies inverse transforms via
TransformFactory, and verifies checksums. -
Data Delivery – Decoded bytes populate internal
_buffers. Theread()method copies from these buffers to the caller’s buffer, updating_available. -
Random Access – The
seek(bitPos)method resets the pipeline, cancels outstanding futures, and re-initializes the block queue, enabling seeks only to block boundaries for large-file archival.
#include <fstream>
#include "kanzi/CompressedInputStream.hpp"
int main() {
std::ifstream fin("data.knz", std::ios::binary);
kanzi::CompressedInputStream cis(fin);
std::vector<char> buffer(8192);
std::size_t total = 0;
while (true) {
std::size_t got = cis.read(buffer.data(), buffer.size());
total += got;
if (got < buffer.size()) break; // EOF
}
// buffer[0..total) holds original data
}
C API for Cross-Language Integration
The repository provides a thin C wrapper in src/api/Compressor.cpp and src/api/Decompressor.cpp exposing the streaming model through opaque handles. This API enables integration with Python ctypes, Java JNI, and other foreign function interfaces.
initCompressor()creates aCompressedOutputStreambound to aFILE*.compress()writes raw buffers incrementally, returning compressed byte counts.disposeCompressor()finalizes and releases resources.
The decompressor provides mirrored functions: initDecompressor(), decompress(), and disposeDecompressor().
#include "kanzi/kanzi.h"
#include <stdio.h>
int main() {
FILE *src = fopen("bigfile.bin", "rb");
FILE *dst = fopen("bigfile.knz", "wb");
struct cData cfg = {0};
strcpy(cfg.entropy, "ANS0");
strcpy(cfg.transform, "BWT+LZ");
cfg.blockSize = 4*1024*1024;
cfg.jobs = 4;
struct cContext *ctx = NULL;
if (initCompressor(&cfg, dst, &ctx) != 0) return -1;
unsigned char buf[65536];
size_t read, written;
while ((read = fread(buf, 1, sizeof(buf), src)) > 0) {
if (compress(ctx, buf, read, &written) != 0) break;
}
disposeCompressor(&ctx, &written);
fclose(src);
fclose(dst);
}
Summary
- Block-based streaming – Kanzi processes data through
CompressedOutputStreamandCompressedInputStreamwithout requiring total input size upfront. - Parallel execution –
EncodingTaskandDecodingTasktemplates distribute work across aThreadPoolwhenCONCURRENCY_ENABLEDis active. - Configurable pipelines – The
Contextclass and factory methods insrc/transform/TransformFactory.hppandsrc/entropy/EntropyEncoderFactory.hppallow runtime selection of transforms and entropy codecs. - Seekable archives – Block-aligned output enables
seek(bitPos)for random access decompression, suitable for video archives and large datasets. - C interoperability – The C API in
src/api/provides opaque handles for embedding in other languages while preserving the full streaming workflow.
Frequently Asked Questions
What block size should I use for streaming compression with Kanzi?
Select a block size between 1 MiB and 8 MiB via ctx.putInt("blockSize", 4*1024*1024). Larger blocks improve compression ratio but increase latency before data is written to the output stream, while smaller blocks enable finer-grained parallelism and faster flushing.
Can I seek to arbitrary byte positions in a Kanzi-compressed stream?
No, seeking is supported only on block boundaries. The seek(bitPos) method in CompressedInputStream resets the decoding pipeline to specific block starts, making Kanzi suitable for applications requiring random access to large files but not byte-level seeking within compressed data.
How does Kanzi verify data integrity during streaming?
Each block optionally includes a checksum computed via XXHash32 or XXHash64 (implemented in src/util/XXHash.hpp). When enabled in the Context, EncodingTask computes checksums during compression and DecodingTask verifies them during decompression, ensuring block-level integrity without buffering the entire stream.
Is the Kanzi streaming API thread-safe for concurrent stream processing?
Individual CompressedOutputStream and CompressedInputStream instances are not thread-safe for concurrent calls, but the internal ThreadPool safely handles parallel block processing across multiple tasks. For concurrent compression of multiple independent streams, create separate stream instances each with their own Context configuration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →