Skip to content

Commit cc5b56c

Browse files
dfa1claude
andcommitted
feat: add ZSTD_CCtx_reset / ZSTD_DCtx_reset context recycling
Bind ZSTD_CCtx_reset and ZSTD_DCtx_reset behind a new ZstdResetDirective enum (SESSION_ONLY, PARAMETERS, SESSION_AND_PARAMETERS), exposed as ZstdCompressCtx.reset(...) / ZstdDecompressCtx.reset(...). Lets a pooled or long-lived context recycle its native state between frames without freeing and recreating it. A parameter reset clears the context back to defaults, so the compress context drops its cached level back to Zstd.defaultCompressionLevel() to stay in sync with the native state. Document the recipe in docs/how-to.md, flip both symbols to bound in docs/supported.md (advanced-parameter count 8 -> 10, total 55 -> 57), and add the 0.5 changelog entry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e9b3d5e commit cc5b56c

8 files changed

Lines changed: 199 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,30 @@ All notable changes to this project are documented here. Format loosely follows
44
[Keep a Changelog](https://keepachangelog.com/); versions are released as `v*`
55
git tags, which trigger publication to Maven Central.
66

7+
## [0.5]
8+
9+
### Added
10+
- `ZstdCompressCtx.reset(ZstdResetDirective)` / `ZstdDecompressCtx.reset(...)`
11+
recycle a context's native state between frames without freeing and recreating
12+
it. `SESSION_ONLY` keeps the level, parameters, and dictionary; `PARAMETERS` /
13+
`SESSION_AND_PARAMETERS` restore the defaults. Binds `ZSTD_CCtx_reset` /
14+
`ZSTD_DCtx_reset`.
15+
16+
### Changed
17+
- `NativeLibrary.classifier()` now throws a clear `UnsatisfiedLinkError` naming
18+
the unsupported CPU arch instead of silently mapping it to x86_64 (which
19+
deferred failure to a cryptic `dlopen` error). Added an explicit `amd64`
20+
branch so Linux JVMs (which report `os.arch=amd64`) still resolve x86_64.
21+
([ea1ac84](https://github.com/dfa1/zstd-java/commit/ea1ac84))
22+
23+
### Fixed
24+
- Native JARs are much smaller. The ELF shared library is now stripped at link
25+
time (`-s`), dropping debug info (`libzstd.so` 4.0M -> ~650K), and the
26+
multi-MB `.pdb` debug database and `.lib` import library that lld emits next
27+
to the Windows `.dll` are no longer bundled (neither is needed at runtime).
28+
Net: linux-x86_64 native jar 1.2M -> 285K, windows-x86_64 1.2M -> 372K.
29+
([ea1ac84](https://github.com/dfa1/zstd-java/commit/ea1ac84))
30+
731
## [0.4]
832

933
### Added

docs/how-to.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,31 @@ try (ZstdCompressCtx cctx = new ZstdCompressCtx().level(19);
1818
Pick the level explicitly with `Zstd.maxCompressionLevel()` /
1919
`minCompressionLevel()` when you need the extreme ends.
2020

21+
## Reset a context to recycle it
22+
23+
A context is already reusable across whole `compress` / `decompress` calls. Reset
24+
goes further: it recycles the *native state* of one context — for pooled contexts,
25+
or to abort a half-written frame and start clean — without freeing and recreating
26+
it. Pick what to clear with `ZstdResetDirective`:
27+
28+
```java
29+
try (ZstdCompressCtx cctx = new ZstdCompressCtx().level(19)) {
30+
byte[] a = cctx.compress(first);
31+
32+
// Cheap: drop any unflushed frame state, keep the level and parameters.
33+
cctx.reset(ZstdResetDirective.SESSION_ONLY);
34+
byte[] b = cctx.compress(second);
35+
36+
// Full wipe: parameters back to default, dictionary cleared, level reset to
37+
// Zstd.defaultCompressionLevel(). Only valid between frames, not mid-frame.
38+
cctx.reset(ZstdResetDirective.SESSION_AND_PARAMETERS);
39+
}
40+
```
41+
42+
`ZstdDecompressCtx.reset(...)` works the same way. Reuse alone amortises
43+
allocation; reset lets a long-lived or pooled context return to a known state
44+
without churning native memory.
45+
2146
## Compress many small payloads with a dictionary
2247

2348
For many small, similar payloads (log lines, JSON records, protobufs), a

docs/supported.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ rather than the deprecated `ZSTD_getDecompressedSize`.
3333
| Dictionary training (ZDICT) | 8 / 12 | trainFromBuffer, cover/fastCover optimizers, finalizeDictionary, getDictHeaderSize |
3434
| Streaming — compress | 3 / 22 | `ZstdOutputStream` (compressStream2 + buffer sizes) |
3535
| Streaming — decompress | 3 / 15 | `ZstdInputStream` (decompressStream + buffer sizes) |
36-
| Advanced parameters | 8 / 38 | all `ZSTD_cParameter` + `ZSTD_dParameter` via `ZstdCompressParameter`/`ZstdDecompressParameter`; `compress2`, `C/DCtx_setParameter`, `loadDictionary`, `c/dParam_getBounds`; MT inert on single-thread build |
36+
| Advanced parameters | 10 / 38 | all `ZSTD_cParameter` + `ZSTD_dParameter` via `ZstdCompressParameter`/`ZstdDecompressParameter`; `compress2`, `C/DCtx_setParameter`, `C/DCtx_reset`, `loadDictionary`, `c/dParam_getBounds`; MT inert on single-thread build |
3737
| Frame inspection | 10 / 13 | `ZstdFrame` + getFrameProgression; `_advanced` not bound |
3838
| Memory sizing | 8 / 14 | sizeof_C/DCtx, sizeof_C/DDict, estimate C/DCtx + C/DDict size |
3939
| Low-level block | 0 / 12 | expert block/continue API not bound |
@@ -63,6 +63,7 @@ rather than the deprecated `ZSTD_getDecompressedSize`.
6363
| `ZSTD_compress2`, `ZSTD_CCtx_setParameter` | `ZstdCompressCtx.parameter` / `checksum` / `longDistanceMatching` / `windowLog` (all of `ZstdCompressParameter`) |
6464
| `ZSTD_DCtx_setParameter` | `ZstdDecompressCtx.parameter` / `windowLogMax` (`ZstdDecompressParameter`) |
6565
| `ZSTD_CCtx_setPledgedSrcSize` | `ZstdOutputStream.withPledgedSize` |
66+
| `ZSTD_CCtx_reset`, `ZSTD_DCtx_reset` | `ZstdCompressCtx.reset` / `ZstdDecompressCtx.reset` (`ZstdResetDirective`) |
6667
| `ZSTD_getDictID_fromCDict`, `ZSTD_getDictID_fromDDict` | `ZstdCompressDict.id()` / `ZstdDecompressDict.id()` |
6768
| `ZSTD_getErrorString` | `ZstdErrorCode.description()` |
6869
| `ZSTD_cParam_getBounds`, `ZSTD_dParam_getBounds` | `ZstdCompressParameter.bounds()` / `ZstdDecompressParameter.bounds()` (`ZstdBounds`) |
@@ -90,7 +91,7 @@ zstd-jni's JNI sources (v1.5.7-11, `src/main/native/*.c`). The latter is
9091
symbol-exact, not functional equivalence: zstd-jni may expose an operation through
9192
a different symbol than this library — e.g. it routes one-shot compression through
9293
`ZSTD_compress2`, so `ZSTD_compress` reads `` for it even though `Zstd.compress`
93-
works. zstd-jni references 53 of these symbols; this library binds 55. They
94+
works. zstd-jni references 53 of these symbols; this library binds 57. They
9495
overlap on the modern context/streaming API and diverge mainly on zstd-jni's
9596
sequence-producer hooks vs this library's frame-inspection and typed-error surface.
9697

@@ -231,7 +232,7 @@ sequence-producer hooks vs this library's frame-inspection and typed-error surfa
231232
| `ZSTD_resetDStream` | — ᵈ ||
232233
| `ZSTD_sizeof_DStream` |||
233234

234-
### Advanced parameters (8/38)
235+
### Advanced parameters (10/38)
235236

236237
| Symbol | Bound | zstd-jni |
237238
|---|:---:|:---:|
@@ -249,7 +250,7 @@ sequence-producer hooks vs this library's frame-inspection and typed-error surfa
249250
| `ZSTD_CCtx_refPrefix` |||
250251
| `ZSTD_CCtx_refPrefix_advanced` |||
251252
| `ZSTD_CCtx_refThreadPool` |||
252-
| `ZSTD_CCtx_reset` | ||
253+
| `ZSTD_CCtx_reset` | ||
253254
| `ZSTD_CCtx_setCParams` |||
254255
| `ZSTD_CCtx_setFParams` |||
255256
| `ZSTD_CCtx_setParameter` |||
@@ -263,7 +264,7 @@ sequence-producer hooks vs this library's frame-inspection and typed-error surfa
263264
| `ZSTD_DCtx_refDDict` |||
264265
| `ZSTD_DCtx_refPrefix` |||
265266
| `ZSTD_DCtx_refPrefix_advanced` |||
266-
| `ZSTD_DCtx_reset` | ||
267+
| `ZSTD_DCtx_reset` | ||
267268
| `ZSTD_DCtx_setFormat` | — ᵈ ||
268269
| `ZSTD_DCtx_setMaxWindowSize` |||
269270
| `ZSTD_DCtx_setParameter` |||

zstd/src/main/java/io/github/dfa1/zstd/Bindings.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ final class Bindings {
138138
NativeLibrary.lookup("ZSTD_CCtx_setParameter",
139139
FunctionDescriptor.of(JAVA_LONG, ADDRESS, JAVA_INT, JAVA_INT));
140140

141+
// size_t ZSTD_CCtx_reset(ZSTD_CCtx*, ZSTD_ResetDirective)
142+
static final MethodHandle CCTX_RESET =
143+
NativeLibrary.lookup("ZSTD_CCtx_reset",
144+
FunctionDescriptor.of(JAVA_LONG, ADDRESS, JAVA_INT));
145+
141146
// size_t ZSTD_compress2(ZSTD_CCtx*, void* dst, size_t dstCap, const void* src, size_t srcSize)
142147
// Uses the advanced parameters set on the context (unlike ZSTD_compressCCtx).
143148
static final MethodHandle COMPRESS2 =
@@ -149,6 +154,11 @@ final class Bindings {
149154
NativeLibrary.lookup("ZSTD_DCtx_setParameter",
150155
FunctionDescriptor.of(JAVA_LONG, ADDRESS, JAVA_INT, JAVA_INT));
151156

157+
// size_t ZSTD_DCtx_reset(ZSTD_DCtx*, ZSTD_ResetDirective)
158+
static final MethodHandle DCTX_RESET =
159+
NativeLibrary.lookup("ZSTD_DCtx_reset",
160+
FunctionDescriptor.of(JAVA_LONG, ADDRESS, JAVA_INT));
161+
152162
// size_t ZSTD_CCtx_setPledgedSrcSize(ZSTD_CCtx*, unsigned long long pledgedSrcSize)
153163
static final MethodHandle CCTX_SET_PLEDGED_SRC_SIZE =
154164
NativeLibrary.lookup("ZSTD_CCtx_setPledgedSrcSize",

zstd/src/main/java/io/github/dfa1/zstd/ZstdCompressCtx.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,28 @@ public ZstdCompressCtx windowLog(int windowLog) {
9090
return parameter(ZstdCompressParameter.WINDOW_LOG, windowLog);
9191
}
9292

93+
/// Resets this context so it can be reused for the next frame without the
94+
/// cost of freeing and recreating its native state.
95+
///
96+
/// - [ZstdResetDirective#SESSION_ONLY] aborts the current frame and drops
97+
/// unflushed data, keeping the level, parameters, and any dictionary.
98+
/// - [ZstdResetDirective#PARAMETERS] and
99+
/// [ZstdResetDirective#SESSION_AND_PARAMETERS] also restore every
100+
/// parameter to its default and clear the dictionary; the level returns to
101+
/// [Zstd#defaultCompressionLevel()]. Only valid when no frame is in progress.
102+
///
103+
/// @param directive what to clear
104+
/// @return `this`, for chaining
105+
/// @throws ZstdException if a parameter reset is requested mid-frame
106+
public ZstdCompressCtx reset(ZstdResetDirective directive) {
107+
Objects.requireNonNull(directive, "directive");
108+
NativeCall.checkReturnValue(() -> (long) Bindings.CCTX_RESET.invokeExact(ptr(), directive.value()));
109+
if (directive != ZstdResetDirective.SESSION_ONLY) {
110+
this.level = Zstd.defaultCompressionLevel();
111+
}
112+
return this;
113+
}
114+
93115
/// Compresses `src` into a new zstd frame using this context and its
94116
/// advanced parameters.
95117
///

zstd/src/main/java/io/github/dfa1/zstd/ZstdDecompressCtx.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,25 @@ public ZstdDecompressCtx windowLogMax(int windowLogMax) {
4949
return parameter(ZstdDecompressParameter.WINDOW_LOG_MAX, windowLogMax);
5050
}
5151

52+
/// Resets this context so it can be reused for the next frame without the
53+
/// cost of freeing and recreating its native state.
54+
///
55+
/// - [ZstdResetDirective#SESSION_ONLY] aborts the current frame and drops
56+
/// buffered state, keeping all parameters and any dictionary.
57+
/// - [ZstdResetDirective#PARAMETERS] and
58+
/// [ZstdResetDirective#SESSION_AND_PARAMETERS] also restore every
59+
/// parameter to its default and clear the dictionary. Only valid when no
60+
/// frame is in progress.
61+
///
62+
/// @param directive what to clear
63+
/// @return `this`, for chaining
64+
/// @throws ZstdException if a parameter reset is requested mid-frame
65+
public ZstdDecompressCtx reset(ZstdResetDirective directive) {
66+
Objects.requireNonNull(directive, "directive");
67+
NativeCall.checkReturnValue(() -> (long) Bindings.DCTX_RESET.invokeExact(ptr(), directive.value()));
68+
return this;
69+
}
70+
5271
/// Decompresses a frame into a buffer of at most `maxSize` bytes.
5372
///
5473
/// @param compressed a complete zstd frame
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.github.dfa1.zstd;
2+
3+
/// Selects what a context reset clears, mirroring `ZSTD_ResetDirective`.
4+
///
5+
/// Use it with [ZstdCompressCtx#reset(ZstdResetDirective)] and
6+
/// [ZstdDecompressCtx#reset(ZstdResetDirective)] to recycle a context for the
7+
/// next frame without freeing and recreating its native state.
8+
public enum ZstdResetDirective {
9+
10+
/// Abort the current frame and discard any unflushed data, keeping all
11+
/// parameters and the loaded dictionary. Cheap; use it between frames.
12+
SESSION_ONLY(1),
13+
/// Restore every parameter to its default and clear the dictionary. Only
14+
/// valid when no frame is in progress.
15+
PARAMETERS(2),
16+
/// Do both: reset the session and the parameters in one call.
17+
SESSION_AND_PARAMETERS(3);
18+
19+
private final int value;
20+
21+
ZstdResetDirective(int value) {
22+
this.value = value;
23+
}
24+
25+
int value() {
26+
return value;
27+
}
28+
}

zstd/src/test/java/io/github/dfa1/zstd/ZstdParameterTest.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,71 @@ void rejectsOutOfRangeValue() {
143143
}
144144
}
145145

146+
@Nested
147+
class Reset {
148+
149+
@Test
150+
void sessionOnlyKeepsLevelAndParameters() {
151+
// Given a context used once, then reset for the session only
152+
byte[] reused;
153+
byte[] fresh;
154+
try (ZstdCompressCtx sut = new ZstdCompressCtx().level(19)) {
155+
sut.compress(PAYLOAD);
156+
sut.reset(ZstdResetDirective.SESSION_ONLY);
157+
reused = sut.compress(PAYLOAD);
158+
}
159+
try (ZstdCompressCtx ctx = new ZstdCompressCtx().level(19)) {
160+
fresh = ctx.compress(PAYLOAD);
161+
}
162+
163+
// Then the level survives the reset: the next frame matches a fresh level-19 frame
164+
assertThat(reused).isEqualTo(fresh);
165+
}
166+
167+
@Test
168+
void sessionAndParametersRestoresTheDefaultLevel() {
169+
// Given a level-19 context reset with parameters cleared
170+
byte[] afterReset;
171+
byte[] atDefault;
172+
try (ZstdCompressCtx sut = new ZstdCompressCtx().level(19)) {
173+
sut.compress(PAYLOAD);
174+
sut.reset(ZstdResetDirective.SESSION_AND_PARAMETERS);
175+
afterReset = sut.compress(PAYLOAD);
176+
}
177+
try (ZstdCompressCtx ctx = new ZstdCompressCtx()) {
178+
atDefault = ctx.compress(PAYLOAD);
179+
}
180+
181+
// Then the level falls back to the default, matching a fresh default-level frame
182+
assertThat(afterReset).isEqualTo(atDefault);
183+
}
184+
185+
@Test
186+
void decompressContextStillDecodesAfterReset() {
187+
// Given a decompression context reset between frames
188+
byte[] frame = Zstd.compress(PAYLOAD);
189+
try (ZstdDecompressCtx sut = new ZstdDecompressCtx()) {
190+
sut.decompress(frame, PAYLOAD.length);
191+
sut.reset(ZstdResetDirective.SESSION_AND_PARAMETERS);
192+
193+
// Then the next frame still decodes
194+
assertThat(sut.decompress(frame, PAYLOAD.length)).isEqualTo(PAYLOAD);
195+
}
196+
}
197+
198+
@Test
199+
void rejectsNullDirective() {
200+
// Given a compression context
201+
try (ZstdCompressCtx sut = new ZstdCompressCtx()) {
202+
// When reset with a null directive
203+
ThrowingCallable result = () -> sut.reset(null);
204+
205+
// Then it fails fast
206+
assertThatThrownBy(result).isInstanceOf(NullPointerException.class);
207+
}
208+
}
209+
}
210+
146211
@Nested
147212
class GenericSetter {
148213

0 commit comments

Comments
 (0)