This document explains how to build the DDS native shared library and call the
Double Dummy Solver from Java using the Foreign Function & Memory API
(Project Panama, java.lang.foreign). No hand-written JNI C/C++ glue is
required: the JVM loads the plain C ABI directly.
The bindings live under //jni and mirror how the other language
bindings work — see python_interface.md,
dotnet_interface.md, and wasm_build.md.
- Prerequisites
- Building the shared library
- The pure-C ABI shim
- Using the FFM bindings
- Worked example
- Loading and running
- Packaging & local install
- Threading and lifecycle
- Not yet supported
- JDK 22 or later.
java.lang.foreignis a stable API from JDK 22, so no--enable-previewis needed. The Bazel build is hermetic:rules_javasupplies the JDK (currently pinned toremotejdk_25in.bazelrc); you do not need a system JDK installed. - Bazel as pinned in
.bazelversion. Use bazelisk, which reads that file and fetches the matching Bazel automatically. Other versions may reject the committedMODULE.bazel.lock. See Bazel version and MODULE.bazel.lock. - JVM runtime. These FFM bindings presume a 64-bit JVM
No jextract install is required — the bindings are hand-written (see
Using the FFM bindings).
bazelisk build //jni:dds_sharedThis produces one self-contained native library rolling up the entire solver (all internal sub-libraries are linked in statically), named per OS:
| OS | Artifact | Location |
|---|---|---|
| Linux | libdds.so |
bazel-bin/jni/ |
| macOS | libdds.dylib |
bazel-bin/jni/ |
| Windows | dds.dll |
bazel-bin/jni/ |
Only the public C API is exported. Exports are constrained at link time by the
generated jni/version_script.lds (Linux) and jni/exported_symbols.lds
(macOS); on Windows the existing __declspec(dllexport) markers drive exports,
so the DLL's export set is a superset of the Linux/macOS one (see
Not yet supported).
The lists are generated from the headers by jni/gen_export_lists.py and
verified by //jni/tests:export_set_test, which asserts the library exports
exactly the public symbols and leaks no internal C++ symbols.
The modern context API in library/src/api/dds_api.hpp takes C++ references and
C++ types (const Deal&, SolverConfig, TTKind), which are not addressable
from a pure-C FFI. The shim library/src/api/dds_c_api.h
wraps it with a clean, C-valid, pointer-only, POD-only surface that Java, .NET,
and ctypes can all bind to:
- The solver handle is opaque:
typedef void* DDS_C_SOLVER_CTX. - Every struct is passed by pointer; no non-POD C++ type crosses the boundary.
SolverConfigis decomposed into scalar arguments andTTKindcrosses as anint, so nothing is passed by value either.
The shim covers the modern API's full surface: context lifecycle (default and
config-based creation, destroy), dds_c_solve_board, dds_c_calc_dd_table and
its _pbn twin, dds_c_calc_par, transposition-table configure/resize/clear,
both resets, and the logging passthroughs. The Java bindings here use a subset;
the .NET binding uses all of it (dotnet_interface.md).
The flat legacy C API from dll.h (SolveBoard, CalcDDtable, GetDDSInfo,
ErrorMessage, …) is exported unchanged and is also callable from FFM.
//jni:dds_ffm provides the Java bindings in package org.dds.ffm
(Dds.java). They are hand-written
rather than generated by jextract: jextract is distributed only as
non-hermetic early-access binaries, and the generated output is just plain
java.lang.foreign code, so the bindings are checked in directly. Dds
exposes:
- Public
MemoryLayoutconstants matching the C structs:DEAL,FUTURE_TRICKS,DD_TABLE_DEAL,DD_TABLE_RESULTS,PAR_RESULTS,DDS_INFO. - Typed downcall wrappers for the shim functions plus
getDdsInfo. These return anintDdsStatusRETURN_*code (RETURN_NO_FAULT == 1on success); compare against the named constants rather than magic numbers, and useDdsStatus.name(rc)for diagnostics. Dds.load(Path)— loads the library viaSymbolLookup.libraryLookupover an ownedArena;DdsisAutoCloseable.
Depend on it from your own java_library/java_binary:
java_binary(
name = "my_app",
srcs = ["MyApp.java"],
deps = ["//jni:dds_ffm"],
data = ["//jni:dds_shared"],
jvm_flags = ["--enable-native-access=ALL-UNNAMED"],
)Solve a single board (North holds all spades with spades trump — 13 tricks):
import static java.lang.foreign.ValueLayout.JAVA_INT;
import java.lang.foreign.Arena;
import java.lang.foreign.MemoryLayout.PathElement;
import java.lang.foreign.MemorySegment;
import java.nio.file.Path;
import org.dds.ffm.Dds;
try (Dds dds = Dds.load(Path.of(System.getProperty("dds.library.path")));
Arena arena = Arena.ofConfined()) {
long remain = Dds.DEAL.byteOffset(PathElement.groupElement("remainCards"));
MemorySegment deal = arena.allocate(Dds.DEAL);
deal.fill((byte) 0); // allocate() is not guaranteed to zero the memory
// deal.fill(...) zeroes the struct; Deal.trump uses strain index 0=S,1=H,2=D,3=C,4=NT (DDS_NOTRUMP), and first=0 is North.
deal.set(JAVA_INT, remain + 0L * 4, 0x7FFC); // North spades
deal.set(JAVA_INT, remain + 5L * 4, 0x7FFC); // East hearts
deal.set(JAVA_INT, remain + 10L * 4, 0x7FFC); // South diamonds
deal.set(JAVA_INT, remain + 15L * 4, 0x7FFC); // West clubs
MemorySegment ctx = dds.createSolverContext();
try {
MemorySegment fut = arena.allocate(Dds.FUTURE_TRICKS);
int rc = dds.solveBoard(ctx, deal, -1, 1, 1, fut); // target, solutions, mode
long score = Dds.FUTURE_TRICKS.byteOffset(PathElement.groupElement("score"));
System.out.println("tricks = " + fut.get(JAVA_INT, score)); // 13
} finally {
dds.destroySolverContext(ctx);
}
}remainCards is a [hand][suit] array with 4 suits per hand, so element
[hand][suit] is at index hand * 4 + suit. Holdings are 13-bit rank masks
(0x7FFC = all ranks 2..A). See
DdsSmokeTest.java for a complete,
tested program including GetDDSInfo.
There are two ways to load the native library:
Dds.loadEmbedded()(default) — extracts the library bundled in the jar for the current OS/arch (from the classpath resourcenative/<os>-<arch>/<lib>) to a temp file and loads it. Zero configuration; this is what the published jar (below) is for. Host-platform only for now.Dds.load(Path)— loads a library at an explicit filesystem path viaSymbolLookup.libraryLookup, with no reliance onSystem.loadLibrary/java.library.path. Useful when you built//jni:dds_sharedyourself; in Bazel, make it adatadependency and pass its runfiles path, e.g.-Ddds.library.path=$(rootpath //jni:dds_shared).
FFM downcalls are restricted native operations. Run with
--enable-native-access=ALL-UNNAMED (as a jvm_flag or on the java
command line) to grant access and silence the runtime warning.
The end-to-end smoke tests wire all of this together:
bazelisk test //jni:dds_ffm_smoke_test # explicit-path loading
bazelisk test //jni:dds_ffm_embedded_smoke_test # loadEmbedded() from the jar//jni:dds_ffm_dist is a java_export producing a Maven-coordinate jar,
org.dds:dds-ffm:3.1.0, that embeds the host-platform native library under
native/<os>-<arch>/ — so a consumer needs only the jar and
Dds.loadEmbedded(), no separate .so/.dylib/.dll to locate.
Install it into your local Maven repository (~/.m2):
bazelisk run //jni:dds_ffm_dist.publish \
--define maven_repo="file://$HOME/.m2/repository" \
--define gpg_sign=falseThis deposits the main jar plus -sources, -javadoc, and the generated POM
under org/dds/dds-ffm/3.1.0/. Another project on the same machine can then
depend on it:
implementation("org.dds:dds-ffm:3.1.0") // Gradle<dependency><groupId>org.dds</groupId><artifactId>dds-ffm</artifactId><version>3.1.0</version></dependency>The bundled native library is host-platform only — the jar built on macOS
contains the macOS .dylib and so on. A single multi-OS/arch ("fat") jar and
publishing to a public/remote repository are not yet available (see below).
Each DDS_C_SOLVER_CTX owns per-context solver state and its own transposition
table. A context is not thread-safe: use one solver context per thread.
Always release a context with destroySolverContext (the example above does so
in a finally block). Because the modern context API owns its state
per-handle, no global InitializeStaticMemory call is required before solving
through the shim.
- Remote/public publishing — only local
~/.m2installs are wired today. Publishing to Maven Central (OSSRH + GPG signing) or GitHub Packages is not yet set up. - Multi-OS/arch "fat" jar — the jar bundles only the host platform's native library. Producing one jar with every triplet requires a CI matrix building each platform and a final assembly step.
- A pinned Windows export set — the Linux and macOS libraries are linked
against a header-derived export list and verified by
//jni/tests:export_set_test, but the Windows DLL exports everything marked__declspec(dllexport), including the moderndds_*context API. Matching the other platforms needs a generated.deffile and adumpbin-based arm of the export test; the//jnitests are excluded on Windows meanwhile. - Hand-written JNI convenience API (idiomatic
native-method Java classes) — deferred; the FFM path above is the supported route.