ExCodecs

CIHex.pmHex.pm DownloadsDocumentationLicenseElixirCoverage Status

An extensible BEAM-native codec framework for Elixir with specialized category APIs. Binary registry codecs use ExCodecs.encode/3 / decode/3; ExCodecs.Spatial handles point-cloud and Gaussian domain types.

Blosc2 produces C-Blosc2-compatible chunks only (not super-chunk / B2ND / .b2frame). Standalone :snappy is separate from Blosc2 cname:.

Design Philosophy

ExCodecs is not a compression library. It is a codec framework.

Compression and spatial formats are categories in one framework. Each category uses an API shaped for its data:

Discovery is category-specific: ExCodecs.available_codecs/0 lists registered binary codecs, while ExCodecs.Spatial.available_formats/0 lists spatial formats. This keeps one framework and error model without forcing unlike data shapes through an overloaded function.

The encode/decode naming is category-agnostic: for compression codecs, encoding is compressing and decoding is decompressing; for a future hash codec, encoding would produce a digest and decoding would verify it.

Installation

Add ex_codecs to your list of dependencies in mix.exs:

def deps do
[
{:ex_codecs, "~> 0.2.3"}
]
end

Then fetch dependencies and compile:

mix deps.get && mix compile

Precompiled NIF binaries are available for macOS (Intel and ARM64), Linux (x86_64 and ARM64, glibc and musl), and Windows (x86_64). They are downloaded automatically from the GitHub releases when you run mix deps.get. If a precompiled artifact is not available for your target, ExCodecs falls back to compiling the Rust NIF from source (requires Rust 1.94+). The native crate is pure Rust (no C toolchain / system compression libraries).

Quick Start

# Registry codecs (primary API — always codec atom + binary)
{:ok, compressed} = ExCodecs.encode(:zstd, "hello world")
{:ok, original} = ExCodecs.decode(:zstd, compressed)
original #=> "hello world"
{:ok, compressed} = ExCodecs.encode(:zstd, my_binary, level: 9)
{:ok, compressed} = ExCodecs.encode(:blosc2, my_binary, cname: :zstd, clevel: 5, shuffle: :byte)
# Category alias for compression
{:ok, compressed} = ExCodecs.Compression.compress(:lz4, data)
{:ok, original} = ExCodecs.Compression.decompress(:lz4, compressed)
# Shared discovery
ExCodecs.available_codecs() #=> [:blosc2, :bzip2, :gsplat, :lz4, :ply, :snappy, :spatial_binary, :zstd]
ExCodecs.available_codecs(:compression) #=> [:blosc2, :bzip2, :lz4, :snappy, :zstd]
ExCodecs.available_codecs(:spatial) #=> [:gsplat, :ply, :spatial_binary]
ExCodecs.supports?(:zstd) #=> true
ExCodecs.codec_info(:zstd) #=> {:ok, %ExCodecs.Codec{name: :zstd, category: :compression, ...}}
# Spatial category API (structs ↔ formats)
alias ExCodecs.Spatial.{Point, PointCloud}
cloud = PointCloud.new([Point.new(0.0, 0.0, 0.0, color: {255, 0, 0})])
{:ok, ply} = ExCodecs.Spatial.encode(cloud, format: :ply)
{:ok, cloud} = ExCodecs.Spatial.decode(ply, format: :ply)
ExCodecs.Spatial.stream_decode(ply, format: :ply) |> Enum.to_list()
ExCodecs.Spatial.available_formats() #=> [:ply, :spatial_binary, :gsplat]
ExCodecs.codec_info(:ply) #=> {:ok, %ExCodecs.Codec{category: :spatial, interface: :spatial, ...}}

API Overview

Binary registry API

{:ok, encoded} = ExCodecs.encode(:zstd, binary, level: 3)
{:ok, decoded} = ExCodecs.decode(:zstd, compressed)

The binary registry API is always codec atom + binary.

Category APIs

These are specialized entry points in the same framework, sharing conventions such as tagged results and %ExCodecs.Error{}.

encode/3 / decode/3

First argument is always a codec atom. Returns {:ok, binary} or {:error, %ExCodecs.Error{}}.

available_codecs/0

ExCodecs.available_codecs() # all available catalog entries
ExCodecs.available_codecs(:spatial) #=> [:gsplat, :ply, :spatial_binary]

Returns a sorted list of codec atoms that are both registered and have a loaded implementation. Use available_codecs/1 to filter the shared catalog by category.

supports?/1

ExCodecs.supports?(:zstd) #=> true
ExCodecs.supports?(:nonexistent) #=> false

Returns true only if the codec is registered and its native NIF is loaded.

codec_info/1

{:ok, info} = ExCodecs.codec_info(:zstd)
info.category #=> :compression
info.native? #=> true
info.streaming? #=> false # block API only today
info.configurable? #=> true
info.version #=> backend version string

Returns a structured %ExCodecs.Codec{} struct with metadata, or {:error, :unsupported_codec}.

Supported Codecs

CodecCategoryConfigurableStreamingOptions
:zstdcompressionYesNolevel 1-22 (pure-Rust structured-zstd); max_output_size (default 256 MiB)
:lz4compressionNoNosize-prepended lz4_flex; max_output_size
:snappycompressionNoNomax_output_size
:bzip2compressionYesNoblock_size 1-9; max_output_size
:blosc2compressionYesNoC-Blosc2 chunk only. cname / clevel / shuffle / typesize; max_output_size

Spatial formats

FormatTypesNotes
:plyPointCloud / GaussianCloudASCII or binary PLY; Gaussian PLY properties
:spatial_binaryPointCloudCompact little-endian EXCP container
:gsplatGaussianCloudCompact little-endian GSPL container

See Understanding Spatial Codecs and the frozen Spatial wire formats.

Known limitations

Architecture

ExCodecs is layered as follows:

  1. Category APIsExCodecs provides the binary registry API; ExCodecs.Compression adds compression terminology; ExCodecs.Spatial handles spatial domain types and formats.

  2. Codec Behaviour (ExCodecs.Codec) — encode/2 / decode/2 on binaries; optional __codec_info__/0 for registry metadata.

  3. Shared codec catalog (ExCodecs.CodecRegistry) — ETS map of codec atoms to modules, categories, interface shapes, and metadata, populated at startup.

  4. Native NIFs (ExCodecs.Native) — pure-Rust compression plus optional spatial DirtyCpu / mmap acceleration via rustler_precompiled (or local compile).

  5. Category discoveryavailable_codecs/0 lists the whole catalog; available_codecs/1 filters it, and ExCodecs.Spatial.available_formats/0 provides the spatial category's preferred built-in order.

To add a binary codec: implement ExCodecs.Codec, wire a NIF if needed, and register it with interface: :binary. Spatial implementations live under ExCodecs.Spatial and register with category: :spatial and interface: :spatial.

Error Handling

All public functions return {:ok, result} or {:error, %ExCodecs.Error{}}. Error reasons are atoms:

ReasonMeaning
:unsupported_codecThe codec name is not registered
:codec_unavailableRegistered but the native NIF failed to load
:invalid_dataData is not a binary or is otherwise invalid
:invalid_optionsOptions are out of range or malformed
:compression_failedThe underlying compression operation failed
:decompression_failedThe underlying decompression operation failed
:nif_not_loadedThe NIF library could not be loaded
:io_errorFile read/write failure
:truncated_inputIncomplete binary
:output_limit_exceededDecompress would exceed max_output_size
{:error, error} = ExCodecs.encode(:unknown, "data")
error.reason #=> :unsupported_codec
{:error, error} = ExCodecs.encode(:zstd, "data", level: 99)
error.reason #=> :invalid_options

Benchmarking

ExCodecs includes benchmarking utilities via benchee.

# Run all compression benchmarks
mix benchmarks

Benchmarks are defined in bench/ and run in the :bench environment. Results are saved to bench/results/ (git-ignored).

Development

# Fetch dependencies and compile (includes NIF compilation)
mix deps.get && mix compile
# Run the full test suite
mix test
# Run tests with coverage
mix coveralls
# Run static analysis
mix credo
mix dialyzer
# Format code
mix format
# Lint Rust NIF code
mix rust.lint
# Run Rust NIF tests
mix rust.test
# Generate documentation
mix docs

Requirements

License

Apache License, Version 2.0. See LICENSE for details.