All notable changes to this project are documented here. Format loosely follows
Keep a Changelog; versions are released as v*
git tags, which trigger publication to Maven Central.
- Native builds now compile with
ZSTD_MULTITHREAD, makingZstdCompressParameter.NB_WORKERS(plusJOB_SIZEandOVERLAP_LOG) functional instead of a silent no-op. Workers engage above zstd's 512 KiB job-size minimum; multithreaded frames are format-valid but not byte-identical to single-threaded output. A context that compressed with workers holds its native worker threads untilclose()—reset()does not release them, so give such contexts a dedicated owner (never pool them). See ADR 0015, which supersedes ADR 0014. (#80)
- Native builds now decode legacy zstd frame formats v0.4-v0.7
(
ZSTD_LEGACY_SUPPORT=4, matching zstd-jni's default). v0.1-v0.3 stay unsupported — they predate zstd's 1.0 stabilization and are essentially never seen in practice. Verified against a real fixture of five concatenated legacy frames extracted from zstd's own test suite. (#73)
linux-x86_64/osx-x86_64/windows-x86_64native builds now include zstd's hand-written BMI2 Huffman-decode assembly (previously disabled). It is a no-op on non-x86_64 targets and only activates via zstd's own runtime CPU detection; benchmarked as throughput-neutral on this project's synthetic workload but carries no measured downside either. (#71)aarch64native builds now target an ARMv8-A + CRC baseline (-mcpu=generic+crc, zig's spelling of-march=armv8-a+crc), instead of the fully generic baseline. Measured +6.9% compress / +12-14% decompress throughput on Apple Silicon. (#71)
linux-x86_64/linux-aarch64native builds now link with full RELRO and immediate binding (-Wl,-z,relro,-z,now), closing off the classic GOT-overwrite exploit primitive. Verified withllvm-readelf. (#71)windows-x86_64/windows-aarch64native builds now export only zstd's public API (ZSTD_*/ZDICT_*) from the DLL, via-DZSTD_DLL_EXPORT=1(the PE analogue of the-fvisibility=hiddensurface already used on ELF/Mach-O), instead of dumping every internal symbol (FSE_*/HUF_*/COVER_*/...) into the export table via--export-all-symbols. Cut the windows-x86_64 export table from 576 to 185 symbols. (#79)
- Building the native library from source on Windows was silently broken:
Maven's exec plugin tried to execute
build-zstd.shdirectly, which only works via a shebang on macOS/Linux. Windows builds now invoke it throughbashexplicitly. A second latent bug this surfaced — unrecognized/Windows host OS detection crashed the build script underset -u— is fixed alongside it. (#75) - Native library compilation now runs every translation unit through a real
parallel work queue (
xargs -P) instead of a fixed-size batch-then-wait loop, and aborts immediately if anyzig ccinvocation fails. Previously a failed compile under&/waitwas invisible toset -e— it could silently produce no.oand only surface later as a cryptic link error, or worse, a link that "succeeded" against a stale.oleft over from a previous run. (#78)
Investigated and rejected as part of the same effort (see
#70 for full benchmark data):
LTO (real compress regression on x86_64, unsupported on macOS entirely — zig's
Mach-O linker has no LTO support, tracked in
#77) and an x86-64-v3
baseline (mixed result, hurts compress more than it helps decompress). Both
would have traded away this project's existing compress-side edge over
zstd-jni for a smaller decompress-side gain.
module-info.java:zstdnow ships as a named JPMS module (module io.github.dfa1.zstd), exporting the single public API package. Module-path consumers grant--enable-native-access=io.github.dfa1.zstdinstead ofALL-UNNAMED; classpath consumers are unaffected. See ADR 0011.
- 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.
ZstdCompressCtx.refPrefix(MemorySegment)/ZstdDecompressCtx.refPrefix(...)— reference native content as a single-use prefix (raw-content dictionary) for the next frame only: the building block for delta compression (compress a new version against a similar previous one). The prefix is referenced, not copied or digested, and writes no dictionary ID; the decompressor must reference the same prefix to decode. BindsZSTD_CCtx_refPrefix/ZSTD_DCtx_refPrefix. Segment-only by design — heap callers that need a copy should useloadDictionaryinstead.Zstd.dictId(byte[])/Zstd.dictId(MemorySegment)— read the dictionary id stamped in raw dictionary bytes without wrapping them in aZstdDictionary. BindsZSTD_getDictID_fromDict.ZstdDictionaryIdvalue type — arecordwrapping the 32-bit dictionary id with an unsignedvalue(),isPresent(), and theNONEsentinel for "no id".ZstdFrame.decompressedSize(byte[])/ZstdFrame.decompressedSize(MemorySegment)— the exact combined decompressed size of all concatenated frames, summed from each frame header (throws if any frame does not record its size). ComplementsdecompressedBound(upper bound). BindsZSTD_findDecompressedSize.ZstdFrame.headerSize(byte[])/ZstdFrame.headerSize(MemorySegment)— the size of a frame's header computed from just its leading bytes (as few as 5), without a full parse. BindsZSTD_frameHeaderSize.ZstdFrame.decompressionMargin(byte[])/ZstdFrame.decompressionMargin(MemorySegment)— the extra room needed to decompress a frame in place (output buffer overlaps the compressed input at its tail), sizeddecompressedSize + margin. BindsZSTD_decompressionMargin.ZstdDictionary.compressDict(int)/compressDict()/decompressDict()— factories for digested dictionaries, e.g.dict.compressDict(19)instead ofnew ZstdCompressDict(dict, 19). They signal that the result isAutoCloseableand are for sharing one digest across contexts viarefDictionary; a single context should prefer the context-ownedloadDictionary.
- Every dictionary-id accessor now returns
ZstdDictionaryIdinstead ofint:ZstdDictionary.id(),ZstdCompressDict.id(),ZstdDecompressDict.id(),ZstdFrame.dictId(...), andZstdFrameHeader.dictId(). The0sentinel is nowZstdDictionaryId.NONE, and the id reads as unsigned viavalue(). Zstd.decompress(byte[])now throwsZstdException(instead of letting a rawArithmeticExceptionescape) when a frame declares a content size larger than a Java array can hold. The size comes from the untrusted frame header; usedecompress(byte[], int)to bound output for untrusted input.
ZstdCompressCtx.reset(ZstdResetDirective)/ZstdDecompressCtx.reset(...)— recycle a context's native state between frames without freeing and recreating it.SESSION_ONLYkeeps the level, parameters, and dictionary;PARAMETERS/SESSION_AND_PARAMETERSrestore the defaults. BindsZSTD_CCtx_reset/ZSTD_DCtx_reset. (3dfd5b8)ZstdCompressCtx.loadDictionary(...)/ZstdDecompressCtx.loadDictionary(...)(aZstdDictionaryor a nativeMemorySegment) andrefDictionary(...)(a pre-digestedZstdCompressDict/ZstdDecompressDict, attached by reference, no copy). A sticky dictionary on the context lets compression combine a dictionary with the advanced parameters (checksum, window log, long-distance matching) — impossible through the per-callcompress(src, dict)overloads, which route the legacy dictionary path. A parameterreset(...)clears it. BindsZSTD_CCtx_loadDictionary/ZSTD_DCtx_loadDictionary(now on contexts, not just streams),ZSTD_CCtx_refCDict,ZSTD_DCtx_refDDict. (3dfd5b8)
NativeLibrary.classifier()now throws a clearUnsatisfiedLinkErrornaming the unsupported CPU arch instead of silently mapping it to x86_64 (which deferred failure to a crypticdlopenerror). Added an explicitamd64branch so Linux JVMs (which reportos.arch=amd64) still resolve x86_64. (ea1ac84)
- Native JARs are much smaller. The ELF shared library is now stripped at link
time (
-s), dropping debug info (libzstd.so4.0M -> ~650K), and the multi-MB.pdbdebug database and.libimport library that lld emits next to the Windows.dllare no longer bundled (neither is needed at runtime). Net: linux-x86_64 native jar 1.2M -> 285K, windows-x86_64 1.2M -> 372K. (ea1ac84)
Zstd.versionNumber()— the linked zstd version as a single integer (MAJOR * 10000 + MINOR * 100 + PATCH, e.g.10507for1.5.7), for programmatic version checks alongsideversion().
ZstdSkippableContentis now a true immutable value: it defensively copies its bytes on the way in and out, and compares by content (equals/hashCode/toStringover the payload, not array identity).- Public methods fail fast with a named
NullPointerExceptionon nullbyte[], dictionary, or sample arguments, instead of an opaque failure deep in native code. Streams are documented as not thread-safe; digested dictionaries (ZstdCompressDict/ZstdDecompressDict) as immutable and safe to share.
- A streaming wrapper that failed partway through construction (e.g. an invalid parameter or dictionary) leaked the native context. The context is now freed on every constructor error path.
- The bundled library is extracted into a directory created owner-only
(
rwx------) atomically at creation, not just by default. The third-partysetup-zigCI action is pinned to a full commit SHA.