ExOpenzl

Elixir NIF bindings for OpenZL, Meta's format-aware compression framework.

OpenZL extends zstd with typed and columnar compression, enabling significantly better ratios on structured data like timestamps, enums, and variable-length strings. It also includes SDDL (Structured Data Description Language) for defining custom format-aware compression graphs.

Features

Prerequisites

Installation

Add ex_openzl to your dependencies in mix.exs:

def deps do
[
{:ex_openzl, "~> 0.4"}
]
end

The NIF is compiled automatically via elixir_make. The first build will compile the OpenZL C library from source (takes ~1 minute).

Precompiled binaries

Precompiled NIF binaries are available for the following targets:

If a precompiled binary is available for your platform, mix compile will download it automatically — no C++ toolchain required.

Usage

Basic compression

data = "hello world, this is a test of OpenZL compression"
{:ok, compressed} = ExOpenzl.compress(data)
{:ok, ^data} = ExOpenzl.decompress(compressed)

One-shot compression creates a fresh native context. For repeated operations, reuse the per-process contexts shown below to amortize native setup cost.

Decompression rejects frames that declare more than 256 MiB of output before allocating that output. Trusted larger frames can use an explicit byte limit:

{:ok, decompressed} = ExOpenzl.decompress(compressed, 1024 * 1024 * 1024)

Reusable contexts

{:ok, cctx} = ExOpenzl.create_compression_context()
:ok = ExOpenzl.set_compression_level(cctx, 9)
{:ok, compressed} = ExOpenzl.compress(cctx, data)
{:ok, dctx} = ExOpenzl.create_decompression_context()
{:ok, ^data} = ExOpenzl.decompress(dctx, compressed)

Typed columnar compression

Pack structured data into typed columns for better compression ratios:

{:ok, cctx} = ExOpenzl.create_compression_context()
# Timestamps: packed u64 little-endian (delta coding)
timestamps = <<1000::little-64, 1001::little-64, 1002::little-64>>
# Levels: u8 enum values (entropy coding)
levels = <<0, 1, 1>>
# Messages: concatenated strings + packed little-endian u32 lengths
messages = "helloworld!!"
msg_lengths = <<5::little-32, 5::little-32, 2::little-32>>
{:ok, compressed} = ExOpenzl.compress_multi_typed(cctx, [
{:numeric, timestamps, 8},
{:numeric, levels, 1},
{:string, messages, msg_lengths}
])
# Decompress
{:ok, dctx} = ExOpenzl.create_decompression_context()
{:ok, [ts_info, lv_info, msg_info]} = ExOpenzl.decompress_multi_typed(dctx, compressed)

Small multi-output frames

Versions before 0.4.10 may return {:error, "Destination capacity too small..."} from compress_multi_typed/2 for very small batches split across multiple typed outputs. If you see this error with valid inputs, upgrade to 0.4.10 or later.

Frame introspection

{:ok, info} = ExOpenzl.frame_info(compressed)
# => %{format_version: 1, num_outputs: 3, outputs: [...]}

SDDL format-aware compression

{:ok, compiled} = ExOpenzl.sddl_compile("u32 timestamp; u8 level;")
{:ok, compressor} = ExOpenzl.create_sddl_compressor(compiled)
{:ok, cctx} = ExOpenzl.create_compression_context()
:ok = ExOpenzl.set_compressor(cctx, compressor)
# Now compress/2 uses the format-aware graph
{:ok, compressed} = ExOpenzl.compress(cctx, data)

Thread safety

Compression and decompression contexts are not thread-safe. Each context should be used by a single Erlang/Elixir process at a time. If you need to compress or decompress from multiple concurrent processes, create a separate context per process.

Hardware acceleration

On x86-64, OpenZL uses BMI2 assembly for Huffman coding, SSE2/AVX2 SIMD for match finding, and BMI2 intrinsics for varint encoding. On other architectures (including Apple Silicon), it falls back to portable C.

License

MIT - see LICENSE.

OpenZL itself is licensed under the BSD License by Meta Platforms, Inc.