Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/smoke/Smoke.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
// can pin the released version and the per-arch native jar from the matrix.

import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressCtx;
import io.github.dfa1.zstd.ZstdDecompressCtx;
import io.github.dfa1.zstd.ZstdCompressContext;
import io.github.dfa1.zstd.ZstdDecompressContext;
import io.github.dfa1.zstd.ZstdDictionary;

import java.util.ArrayList;
Expand Down Expand Up @@ -38,8 +38,8 @@ public static void main(String[] args) {
samples.add(("{\"id\":" + i + ",\"user\":\"user_" + (i % 100) + "\",\"event\":\"click\"}").getBytes());
}
ZstdDictionary dict = ZstdDictionary.train(samples, 8 * 1024);
try (ZstdCompressCtx cctx = new ZstdCompressCtx();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
try (ZstdCompressContext cctx = new ZstdCompressContext();
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
byte[] message = samples.get(7);
byte[] dictCompressed = cctx.compress(message, dict);
byte[] dictRestored = dctx.decompress(dictCompressed, message.length, dict);
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project are documented here. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/); versions are released as `v*`
git tags, which trigger publication to Maven Central.

## [Unreleased]

### Changed
- **Breaking:** renamed public types to spell out abbreviations, matching the
`Zstd<Compress|Decompress><Stream|Parameter>` family and zstd's own prose
("compression context", "dictionary"): `ZstdCompressCtx` → `ZstdCompressContext`,
`ZstdDecompressCtx` → `ZstdDecompressContext`, `ZstdCompressDict` →
`ZstdCompressDictionary`, `ZstdDecompressDict` → `ZstdDecompressDictionary`.

## [0.6] - 2026-06-27

### Added
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ List<byte[]> samples = ...; // representative records
ZstdDictionary dict = ZstdDictionary.train(samples, 8 * 1024);

byte[] message = ...;
try (ZstdCompressCtx cctx = new ZstdCompressCtx();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
try (ZstdCompressContext cctx = new ZstdCompressContext();
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
byte[] frame = cctx.compress(message, dict);
byte[] back = dctx.decompress(frame, message.length, dict);
}
Expand All @@ -58,8 +58,8 @@ import io.github.dfa1.zstd.*;
import java.lang.foreign.*;

try (Arena arena = Arena.ofConfined();
ZstdCompressCtx cctx = new ZstdCompressCtx();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
ZstdCompressContext cctx = new ZstdCompressContext();
ZstdDecompressContext dctx = new ZstdDecompressContext()) {

MemorySegment src = ...; // e.g. an mmap'd file slice
MemorySegment frame = cctx.compress(arena, src); // off-heap → off-heap
Expand Down
4 changes: 2 additions & 2 deletions adr/0010-native-context-pool.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

## Context

`ZstdCompressCtx`/`ZstdDecompressCtx` wrap native CCtx/DCtx — expensive to
`ZstdCompressContext`/`ZstdDecompressContext` wrap native CCtx/DCtx — expensive to
create (off-heap malloc + init) and **not thread-safe**. The library targets
JDK 25, where virtual threads are the default concurrency model: many, cheap,
short-lived. The question is how to reuse contexts across them.

`ThreadLocal<ZstdCompressCtx>` is the wrong answer under virtual threads: one
`ThreadLocal<ZstdCompressContext>` is the wrong answer under virtual threads: one
context per vthread means millions of native contexts (native-memory
explosion), and short vthread lifetimes mean the cached context is never
reused. Java's own guidance: **pool the scarce resource, not the thread.**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import io.airlift.compress.v3.zstd.ZstdJavaCompressor;
import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressCtx;
import io.github.dfa1.zstd.ZstdCompressContext;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -43,7 +43,7 @@ public class CompressBenchmark {

private byte[] src;

private ZstdCompressCtx ffmCtx;
private ZstdCompressContext ffmCtx;
private byte[] ffmDst;

private Arena arena;
Expand All @@ -58,7 +58,7 @@ public void setup() {
src = BenchData.generate(size);
int bound = (int) Zstd.compressBound(size);

ffmCtx = new ZstdCompressCtx().level(level);
ffmCtx = new ZstdCompressContext().level(level);
ffmDst = new byte[bound];

arena = Arena.ofConfined();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import io.airlift.compress.v3.zstd.ZstdJavaDecompressor;
import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdDecompressCtx;
import io.github.dfa1.zstd.ZstdDecompressContext;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -41,7 +41,7 @@ public class DecompressBenchmark {
private int originalSize;
private byte[] frame;

private ZstdDecompressCtx ffmCtx;
private ZstdDecompressContext ffmCtx;

private Arena arena;
private MemorySegment frameSeg;
Expand All @@ -55,7 +55,7 @@ public void setup() {
originalSize = size;
frame = Zstd.compress(BenchData.generate(size));

ffmCtx = new ZstdDecompressCtx();
ffmCtx = new ZstdDecompressContext();

arena = Arena.ofConfined();
frameSeg = arena.allocate(frame.length);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import static java.lang.foreign.ValueLayout.JAVA_BYTE;

import io.github.dfa1.zstd.Zstd;
import io.github.dfa1.zstd.ZstdCompressCtx;
import io.github.dfa1.zstd.ZstdDecompressCtx;
import io.github.dfa1.zstd.ZstdCompressContext;
import io.github.dfa1.zstd.ZstdDecompressContext;
import java.io.UncheckedIOException;
import java.io.IOException;
import java.lang.foreign.Arena;
Expand Down Expand Up @@ -61,8 +61,8 @@ public class GoldenCorpusBenchmark {

private int bound;

private ZstdCompressCtx cctx;
private ZstdDecompressCtx dctx;
private ZstdCompressContext cctx;
private ZstdDecompressContext dctx;
private byte[] compressDst;

private Arena arena;
Expand All @@ -88,8 +88,8 @@ public void setup() {
srcSize = src.length;
frame = Zstd.compress(src);

cctx = new ZstdCompressCtx().level(level);
dctx = new ZstdDecompressCtx();
cctx = new ZstdCompressContext().level(level);
dctx = new ZstdDecompressContext();
bound = (int) Zstd.compressBound(srcSize);
compressDst = new byte[bound];

Expand Down
44 changes: 22 additions & 22 deletions docs/how-to.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ Task-focused recipes. Each assumes you have the library on the classpath (see th
Reuse a context to amortise native allocation across many calls:

```java
try (ZstdCompressCtx cctx = new ZstdCompressCtx().level(19);
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
try (ZstdCompressContext cctx = new ZstdCompressContext().level(19);
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
byte[] packed = cctx.compress(message);
byte[] restored = dctx.decompress(packed, message.length);
}
Expand All @@ -26,7 +26,7 @@ or to abort a half-written frame and start clean — without freeing and recreat
it. Pick what to clear with `ZstdResetDirective`:

```java
try (ZstdCompressCtx cctx = new ZstdCompressCtx().level(19)) {
try (ZstdCompressContext cctx = new ZstdCompressContext().level(19)) {
byte[] a = cctx.compress(first);

// Cheap: drop any unflushed frame state, keep the level and parameters.
Expand All @@ -39,7 +39,7 @@ try (ZstdCompressCtx cctx = new ZstdCompressCtx().level(19)) {
}
```

`ZstdDecompressCtx.reset(...)` works the same way. Reuse alone amortises
`ZstdDecompressContext.reset(...)` works the same way. Reuse alone amortises
allocation; reset lets a long-lived or pooled context return to a known state
without churning native memory.

Expand All @@ -51,7 +51,7 @@ matching) set on the context. To combine the two, make the dictionary *sticky*
with `loadDictionary` — then the normal `compress` path honours both:

```java
try (ZstdCompressCtx cctx = new ZstdCompressCtx().level(19).checksum(true)) {
try (ZstdCompressContext cctx = new ZstdCompressContext().level(19).checksum(true)) {
cctx.loadDictionary(dict); // ZstdDictionary, or a native MemorySegment
byte[] frame = cctx.compress(record); // dictionary + checksum, together
}
Expand All @@ -62,9 +62,9 @@ by reference — no per-call digesting, no copy. It pairs with `reset` for a
pooled, recycled context:

```java
try (ZstdCompressDict cdict = dict.compressDict(19)) {
try (ZstdCompressDictionary cdict = dict.compressDict(19)) {
// one cctx per pooled worker, all sharing the one digested dictionary
try (ZstdCompressCtx cctx = new ZstdCompressCtx()) {
try (ZstdCompressContext cctx = new ZstdCompressContext()) {
cctx.refDictionary(cdict); // borrowed; cdict must outlive cctx
byte[] a = cctx.compress(first);
cctx.reset(ZstdResetDirective.SESSION_ONLY); // recycle, keep the dictionary
Expand All @@ -76,12 +76,12 @@ try (ZstdCompressDict cdict = dict.compressDict(19)) {
`refDictionary` only borrows: the digested `cdict` is *not* tied to the context's
lifetime, so it must be closed separately (hence its own try-with-resources). That
is the price of sharing one digest across many contexts. If you have just **one**
context, don't build a `ZstdCompressDict` at all — `loadDictionary` above digests
context, don't build a `ZstdCompressDictionary` at all — `loadDictionary` above digests
into the context and frees it for you, and a stray, never-closed
`ZstdCompressDict` is a native-memory leak.
`ZstdCompressDictionary` is a native-memory leak.

A loaded or referenced dictionary stays until replaced, cleared with `null`, or
dropped by a parameter `reset`. `ZstdDecompressCtx` mirrors all of this.
dropped by a parameter `reset`. `ZstdDecompressContext` mirrors all of this.

## Compress many small payloads with a dictionary

Expand All @@ -92,8 +92,8 @@ representative samples:
```java
ZstdDictionary dict = ZstdDictionary.train(sampleRecords, 16 * 1024);

try (ZstdCompressCtx cctx = new ZstdCompressCtx();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
try (ZstdCompressContext cctx = new ZstdCompressContext();
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
byte[] packed = cctx.compress(record, dict);
byte[] restored = dctx.decompress(packed, record.length, dict);
}
Expand All @@ -105,10 +105,10 @@ ZstdDictionary reloaded = ZstdDictionary.of(persisted);
On a hot path, digest the dictionary once to skip per-call setup:

```java
try (ZstdCompressDict cdict = dict.compressDict(19);
ZstdDecompressDict ddict = dict.decompressDict();
ZstdCompressCtx cctx = new ZstdCompressCtx();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
try (ZstdCompressDictionary cdict = dict.compressDict(19);
ZstdDecompressDictionary ddict = dict.decompressDict();
ZstdCompressContext cctx = new ZstdCompressContext();
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
byte[] packed = cctx.compress(record, cdict);
byte[] restored = dctx.decompress(packed, record.length, ddict);
}
Expand All @@ -122,7 +122,7 @@ hands zstd the segment address directly: no copy in, no copy out, no GC churn.

```java
try (Arena arena = Arena.ofConfined();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
MemorySegment frame = reader.mmapSlice(); // already native
long n = Zstd.decompressedSize(frame); // read header, no copy
MemorySegment out = arena.allocate(n); // becomes the backing buffer
Expand All @@ -138,10 +138,10 @@ The segment-API map:

| Operation | byte[] (convenience) | MemorySegment (boundary zero-copy) |
|------------------------|-------------------------------------------------|----------------------------------------------------|
| compress | `ZstdCompressCtx.compress(byte[])` | `ZstdCompressCtx.compress(dst, src)` |
| compress + dict | `ZstdCompressCtx.compress(byte[], ZstdCompressDict)` | `ZstdCompressCtx.compress(dst, src, ZstdCompressDict)` |
| decompress | `ZstdDecompressCtx.decompress(byte[], int)` | `ZstdDecompressCtx.decompress(dst, src)` |
| decompress + dict | `ZstdDecompressCtx.decompress(byte[], int, ZstdDecompressDict)` | `ZstdDecompressCtx.decompress(dst, src, ZstdDecompressDict)` |
| compress | `ZstdCompressContext.compress(byte[])` | `ZstdCompressContext.compress(dst, src)` |
| compress + dict | `ZstdCompressContext.compress(byte[], ZstdCompressDictionary)` | `ZstdCompressContext.compress(dst, src, ZstdCompressDictionary)` |
| decompress | `ZstdDecompressContext.decompress(byte[], int)` | `ZstdDecompressContext.decompress(dst, src)` |
| decompress + dict | `ZstdDecompressContext.decompress(byte[], int, ZstdDecompressDictionary)` | `ZstdDecompressContext.decompress(dst, src, ZstdDecompressDictionary)` |
| size output (no copy) | frame header via `Zstd.decompress(byte[])` | `Zstd.decompressedSize(MemorySegment)` |

Size `dst` with `Zstd.compressBound(srcSize)` for compression, or
Expand Down Expand Up @@ -177,7 +177,7 @@ direct buffer or a `byte[]` first.

```java
try (Arena arena = Arena.ofConfined();
ZstdCompressCtx cctx = new ZstdCompressCtx()) {
ZstdCompressContext cctx = new ZstdCompressContext()) {
ByteBuffer src = channel.map(READ_ONLY, 0, size, arena); // direct, off-heap
MemorySegment in = MemorySegment.ofBuffer(src); // covers [position, limit)
MemorySegment out = cctx.compress(arena, in); // arena-owned frame
Expand Down
4 changes: 2 additions & 2 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ native artifact per platform:
| Type | Role |
|---|---|
| `Zstd` | one-shot `compress` / `decompress`, level + version queries, `compressBound`, `decompressedSize` |
| `ZstdCompressCtx` / `ZstdDecompressCtx` | reusable contexts; `byte[]` and `MemorySegment` overloads, dictionary variants |
| `ZstdCompressContext` / `ZstdDecompressContext` | reusable contexts; `byte[]` and `MemorySegment` overloads, dictionary variants |
| `ZstdDictionary` | train (`ZDICT`), load, persist, query dict id |
| `ZstdCompressDict` / `ZstdDecompressDict` | pre-digested dictionaries for hot paths |
| `ZstdCompressDictionary` / `ZstdDecompressDictionary` | pre-digested dictionaries for hot paths |
| `ZstdFrame` | frame inspection: header, sizes, dict id, skippable frames |
| `ZstdException` / `ZstdErrorCode` | typed errors mapped from zstd's sentinels |

Expand Down
24 changes: 12 additions & 12 deletions docs/supported.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,26 +49,26 @@ rather than the deprecated `ZSTD_getDecompressedSize`.
| `ZSTD_versionNumber`, `ZSTD_versionString` | `Zstd.version` |
| `ZSTD_isError`, `ZSTD_getErrorName` | internal error mapping in `Zstd` |
| `ZSTD_getFrameContentSize` | `Zstd.decompress(byte[])`, `Zstd.decompressedSize` |
| `ZSTD_createCCtx`, `ZSTD_freeCCtx`, `ZSTD_compressCCtx` | `ZstdCompressCtx` |
| `ZSTD_createDCtx`, `ZSTD_freeDCtx`, `ZSTD_decompressDCtx` | `ZstdDecompressCtx` |
| `ZSTD_compress_usingDict` | `ZstdCompressCtx.compress(byte[], ZstdDictionary)` |
| `ZSTD_decompress_usingDict` | `ZstdDecompressCtx.decompress(byte[], int, ZstdDictionary)` |
| `ZSTD_createCDict`, `ZSTD_freeCDict`, `ZSTD_compress_usingCDict` | `ZstdCompressDict` |
| `ZSTD_createDDict`, `ZSTD_freeDDict`, `ZSTD_decompress_usingDDict` | `ZstdDecompressDict` |
| `ZSTD_createCCtx`, `ZSTD_freeCCtx`, `ZSTD_compressCCtx` | `ZstdCompressContext` |
| `ZSTD_createDCtx`, `ZSTD_freeDCtx`, `ZSTD_decompressDCtx` | `ZstdDecompressContext` |
| `ZSTD_compress_usingDict` | `ZstdCompressContext.compress(byte[], ZstdDictionary)` |
| `ZSTD_decompress_usingDict` | `ZstdDecompressContext.decompress(byte[], int, ZstdDictionary)` |
| `ZSTD_createCDict`, `ZSTD_freeCDict`, `ZSTD_compress_usingCDict` | `ZstdCompressDictionary` |
| `ZSTD_createDDict`, `ZSTD_freeDDict`, `ZSTD_decompress_usingDDict` | `ZstdDecompressDictionary` |
| `ZDICT_trainFromBuffer` | `ZstdDictionary.train` |
| `ZDICT_getDictID` | `ZstdDictionary.id` |
| `ZDICT_isError`, `ZDICT_getErrorName` | internal error mapping in `ZstdDictionary` |
| `ZSTD_compressStream2`, `ZSTD_CStreamInSize`, `ZSTD_CStreamOutSize`, `ZSTD_CCtx_setParameter` | `ZstdOutputStream` |
| `ZSTD_decompressStream`, `ZSTD_DStreamInSize`, `ZSTD_DStreamOutSize` | `ZstdInputStream` |
| `ZSTD_compress2`, `ZSTD_CCtx_setParameter` | `ZstdCompressCtx.parameter` / `checksum` / `longDistanceMatching` / `windowLog` (all of `ZstdCompressParameter`) |
| `ZSTD_DCtx_setParameter` | `ZstdDecompressCtx.parameter` / `windowLogMax` (`ZstdDecompressParameter`) |
| `ZSTD_compress2`, `ZSTD_CCtx_setParameter` | `ZstdCompressContext.parameter` / `checksum` / `longDistanceMatching` / `windowLog` (all of `ZstdCompressParameter`) |
| `ZSTD_DCtx_setParameter` | `ZstdDecompressContext.parameter` / `windowLogMax` (`ZstdDecompressParameter`) |
| `ZSTD_CCtx_setPledgedSrcSize` | `ZstdOutputStream.withPledgedSize` |
| `ZSTD_CCtx_reset`, `ZSTD_DCtx_reset` | `ZstdCompressCtx.reset` / `ZstdDecompressCtx.reset` (`ZstdResetDirective`) |
| `ZSTD_getDictID_fromCDict`, `ZSTD_getDictID_fromDDict` | `ZstdCompressDict.id()` / `ZstdDecompressDict.id()` |
| `ZSTD_CCtx_reset`, `ZSTD_DCtx_reset` | `ZstdCompressContext.reset` / `ZstdDecompressContext.reset` (`ZstdResetDirective`) |
| `ZSTD_getDictID_fromCDict`, `ZSTD_getDictID_fromDDict` | `ZstdCompressDictionary.id()` / `ZstdDecompressDictionary.id()` |
| `ZSTD_getErrorString` | `ZstdErrorCode.description()` |
| `ZSTD_cParam_getBounds`, `ZSTD_dParam_getBounds` | `ZstdCompressParameter.bounds()` / `ZstdDecompressParameter.bounds()` (`ZstdBounds`) |
| `ZSTD_CCtx_loadDictionary`, `ZSTD_DCtx_loadDictionary` | `ZstdCompressCtx.loadDictionary` / `ZstdDecompressCtx.loadDictionary`; `ZstdOutputStream` / `ZstdInputStream` dictionary constructors |
| `ZSTD_CCtx_refCDict`, `ZSTD_DCtx_refDDict` | `ZstdCompressCtx.refDictionary` / `ZstdDecompressCtx.refDictionary` |
| `ZSTD_CCtx_loadDictionary`, `ZSTD_DCtx_loadDictionary` | `ZstdCompressContext.loadDictionary` / `ZstdDecompressContext.loadDictionary`; `ZstdOutputStream` / `ZstdInputStream` dictionary constructors |
| `ZSTD_CCtx_refCDict`, `ZSTD_DCtx_refDDict` | `ZstdCompressContext.refDictionary` / `ZstdDecompressContext.refDictionary` |
| `ZSTD_isFrame`, `ZSTD_findFrameCompressedSize`, `ZSTD_decompressBound`, `ZSTD_getDictID_fromFrame`, `ZSTD_getFrameHeader`, `ZSTD_isSkippableFrame`, `ZSTD_writeSkippableFrame`, `ZSTD_readSkippableFrame` | `ZstdFrame` (+ `ZstdFrameHeader`, `ZstdFrameType`, `ZstdSkippableContent`) |
| `ZSTD_getErrorCode` | `ZstdException.code()` (+ `ZstdErrorCode`) |
| `ZSTD_getFrameProgression` | `ZstdCompressStream.progress()` (`ZstdFrameProgression`) |
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ the `MemorySegment` overloads hand zstd the segment address directly, so there i

```java
try (Arena arena = Arena.ofConfined();
ZstdDecompressCtx dctx = new ZstdDecompressCtx()) {
ZstdDecompressContext dctx = new ZstdDecompressContext()) {
MemorySegment frame = reader.mmapSlice(); // already native — never touches the heap
long n = Zstd.decompressedSize(frame); // read the header, no copy
MemorySegment out = arena.allocate(n); // this segment *is* the output buffer
Expand Down
Loading
Loading