Java Foreign Function & Memory (FFM) bindings for Zstandard.
No JNI, no hand-written C. Targets JDK 25+ (stable java.lang.foreign).
The differentiator is dictionary compression — trained from your own data via
ZDICT — plus a zero-copy MemorySegment API for callers whose bytes are
already off-heap (e.g. an mmap slice in, an arena buffer out).
Multi-module Maven build (io.github.dfa1.zstd:zstd):
zstd/— the library module, artifactIdzstd, pure-Java FFM bindings (packageio.github.dfa1.zstd). The only module with Java sources.native/<classifier>/— one module per platform; each packages alibzstd.{dylib,so,dll}built from thethird_party/zstdsubmodule. No Java. Classifiers:osx-aarch64,osx-x86_64,linux-x86_64,linux-aarch64,windows-x86_64,windows-aarch64.bom/— dependency BOM.zstd/— vendoredfacebook/zstdgit submodule (the C source of truth).
scripts/build-zstd.sh <output-resources-dir> <classifier> compiles the zstd
library sources directly with zig cc — no zstd Makefile, no CMake. Zig
bundles clang + libc for every target, so any host cross-compiles any of the six
classifiers hermetically. The Maven exec plugin runs it in generate-resources;
it is idempotent (skips if the library already exists).
Key flags: -DZSTD_DISABLE_ASM=1 (drops x86-only .S, identical codegen on every
target), -fvisibility=hidden on ELF/Mach-O, and lld --export-all-symbols on
Windows (do not use hidden visibility there — it suppresses the PE exports).
Built .dylib/.so/.dll are git-ignored; they are regenerated from the submodule.
- Checkstyle-clean (
./mvnw validateruns it); see the Code style section below. - Native pointers wrap in
NativeObject(AutoCloseable, idempotent close). - All native handles live in
Bindings;size_t/unsigned long longmap toJAVA_LONG. Public methods guard zstd's negative sentinels. - Native structs: model the layout as a named
StructLayoutand access fields throughstatic final VarHandles from it (LAYOUT.varHandle(groupElement("…"))), deriving size fromLAYOUT.byteSize(). No hardcoded offsets/sizes — seeZstdStreamBuffer. - API is segment-first for the zero-copy fast path, with thin
byte[]overloads for heap callers. Never allocate abyte[]for decode output on a hot path — see docs/zero-copy.md. - Run with
--enable-native-access=ALL-UNNAMED. - Wrap naked
int/long/Stringat public boundaries when the value has a validity constraint, unit, or semantic identity — sizes (ZstdByteSize), levels (ZstdCompressionLevel), versions (ZstdVersion), window logs (ZstdWindowLog), magic variants (ZstdMagicVariant), dictionary ids (ZstdDictionaryId). Validate once at construction so illegal states are unrepresentable rather than guarded by scattered runtime checks; for hostile/unrepresentable input returnOptional<VO>instead of throwing. This sweep is considered done — see CHANGELOG.md's Unreleased section. Exception: heterogeneous native passthroughs likeZstdCompressContext#parameter(ZstdCompressParameter, int)stay rawint, since the value's meaning depends on which enum constant is passed and native bounds-checking already validates it; the common cases have typed convenience methods (level(),windowLog(ZstdWindowLog),checksum(boolean)).
- Cover happy path, negative cases (invalid input / errors), and corners (empty, zero, max, boundaries). Unit tests must be fast — no file I/O, network, or sleep; mock or use in-memory data.
- Integration tests are ground truth (no formal spec): interop with the
zstd-jnireference binding (luben, the zstd C library via JNI) and the vendored golden corpus underthird_party/zstd/tests/(golden-compression,golden-decompression,golden-decompression-errors,golden-dictionaries). Write one for every encoding round-trip and file-format boundary. - JUnit 5 + Mockito (BDDMockito) + AssertJ. Class under test named
sut. Every test has// Given/// When/// Then. BDDMockito only:given(mock.m()).willReturn(v)/then(...)(static-import onlygiven/then, neverwillReturn/willThrow). For exception assertions, capture the action under// Whenas aThrowingCallable result = () -> sut.m(...);and assert it under// ThenwithassertThatThrownBy(result)— the callable is the When, the assertion is the Then. - Prefer
@ParameterizedTestover copy-paste (@ValueSource, else@ArgumentsSource/named cases). For large input spaces use seeded-random@MethodSourcegenerators — they find corners examples miss. Put generators inRandomArrays(integration) or a similar util; keep counts low (10–30) when the test does file I/O or JNI. @Nestedgroups related scenarios (@BeforeEachin a nested class applies only to it). Private helpers go after all@Testmethods.
- 4-space indent, zero SonarQube bugs/smells, no
sun.misc.Unsafeor internal JDK APIs. - Prefer explicit over clever; fail fast on unhandled cases.
- Always braces for
if/else/for/while, even one-liners (if (c) { return a; }). - Time quantities use
java.time.Duration, neverlong(nolong timeoutMs/delayNanos). Exception: low-level JDK interop takinglong ns(Thread.sleep,LockSupport.parkNanos,System.nanoTimemath) — convert at the call site viaduration.toNanos()/toMillis().
- Every public method: main prose description,
@paramper parameter,@return(unlessvoid). Every public record:@paramper component on the class doc.@see-only counts as no description. - All
///Markdown — no HTML (checkstyleRegexpSinglelineblocks<p>,<ul>,<li>,<strong>,<pre>,<table>, …). Use blank///for paragraphs,-lists,```java ```,**bold**. Cross-refs[ClassName#method(ParamType)]— verify the target exists (wrong refs are errors). - American English everywhere (javadoc, comments, identifiers):
recognize/optimize/finalize/serialize/behavior/color, never-ise/-isation/-our. Matches the JDK (Object.finalize,Serializable) and zstd's C docs. - Check:
./mvnw javadoc:javadoc -pl coremust produce zero output.