Technical deep dive into using the Foreign Function & Memory (FFM) API for calling TensorFlow C library from Java.
// C code (JNI)
JNIEXPORT jstring JNICALL Java_TensorFlow_getVersion(JNIEnv *env, jclass cls) {
const char* version = TF_Version();
return (*env)->NewStringUTF(env, version);
}Problems with JNI:
- ❌ Requires writing C/C++ glue code
- ❌ Platform-specific compilation
- ❌ Manual memory management
- ❌ Error-prone (segfaults, memory leaks)
- ❌ Separate build toolchain (CMake, gcc, etc.)
- ❌ Distribution complexity (multiple .so/.dll files)
// Pure Java (FFM)
MethodHandle TF_Version = linker.downcallHandle(
symbol("TF_Version"),
FunctionDescriptor.of(ADDRESS)
);
MemorySegment cStr = (MemorySegment) TF_Version.invokeExact();
String version = cStr.getString(0);Benefits of FFM:
- ✅ Pure Java code - no C glue needed
- ✅ Cross-platform - same code for all OS
- ✅ Memory-safe - Arena-based lifecycle
- ✅ Type-safe - MethodHandle with exact types
- ✅ No compilation - just Java compilation
- ✅ Performance - zero overhead, as fast as JNI
macOS ARM64 (Apple Silicon):
- TensorFlow 2.18.0 CPU
- Download:
libtensorflow-cpu-darwin-arm64.tar.gz(~180 MB) - ✅ Fully supported
Linux x86_64:
- TensorFlow 2.18.0 CPU
- Download:
libtensorflow-cpu-linux-x86_64.tar.gz(~200 MB) - ✅ Fully supported
Windows x86_64:
- TensorFlow 2.18.0 CPU
- Download:
libtensorflow-cpu-windows-x86_64.zip(~190 MB) - ✅ Fully supported
macOS x86_64 (Intel Macs):
- ❌ Not supported
- Reason: TensorFlow dropped x86_64 macOS support after version 2.16.2
- Workaround: Provide your own build via
-PtensorflowHome=...
The build automatically downloads TensorFlow on first run:
tasks.register("downloadTensorFlow") {
onlyIf { !tensorflowHome.isPresent }
// Platform detection and download logic
}Why:
- ✅ Zero manual setup for users
- ✅ Consistent TensorFlow version (2.18.0)
- ✅ Works in CI/CD without configuration
- ✅ Can override with
-PtensorflowHomefor custom builds
val (url, isZip) = when {
os.contains("mac") && isArm64 -> darwinArm64Url to false
os.contains("linux") && isX86_64 -> linuxX86_64Url to false
os.contains("win") && isX86_64 -> windowsX86_64Url to true
else -> error("Unsupported platform")
}Design choices:
- Fail fast on unsupported platforms
- Clear error messages
- Separate extract logic for .zip vs .tar.gz
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}Why JDK 25:
- FFM is final in JDK 22 (no --enable-preview needed)
- JDK 25 has improved FFM performance
- Latest Vector API enhancements
- Demo consistency (most demos use JDK 25)
try (TensorFlowC tf = TensorFlowC.load()) {
// All FFM operations here
// Memory automatically freed when arena closes
}Benefits:
- Automatic cleanup - no memory leaks
- Scoped lifetime - clear ownership
- Exception-safe - cleanup even on error
MethodHandle TF_NewGraph = linker.downcallHandle(
symbol("TF_NewGraph"),
FunctionDescriptor.of(ADDRESS)
);
MemorySegment graph = (MemorySegment) TF_NewGraph.invokeExact();Key points:
downcallHandle- Java → native callsFunctionDescriptor- Specifies C function signatureinvokeExact()- Type-exact invocation (fastest)- Returns
MemorySegment- Pointer to native memory
FFM uses the platform's default calling convention:
- Linux/macOS: System V AMD64 ABI
- Windows: Microsoft x64 calling convention
- ARM64: ARM64 procedure call standard
No manual ABI specification needed - FFM handles it automatically.
| Approach | Call Overhead | Memory Safety | Cross-platform |
|---|---|---|---|
| FFM | ~0ns (inlined) | ✅ Safe | ✅ Yes |
| JNI | ~5-10ns | ❌ Unsafe | ❌ Platform-specific |
| JNA | ~50-100ns | ✅ Yes |
FFM performance:
- First call: ~100ns (method handle initialization)
- Subsequent calls: 0ns (JIT inlines them completely)
- Peak: Identical to direct C function call
- TensorFlow C library: ~180-200 MB (one-time download)
- Cached in
build/tensorflow/(not committed to git) - Reused across multiple runs
Reasoning:
- Latest stable - Released 2024
- Consistent across platforms - Works on all supported OS
- CPU-only - Simpler demo, no CUDA setup needed
- Good FFM test - Complex C API with callbacks, structs
Limitations:
- CPU-only (no GPU acceleration in this demo)
- For GPU, would need CUDA-enabled build (~1 GB download)
Cause: TensorFlow library not found
Fix:
# Let Gradle download it automatically
./gradlew :demos:tensorflow-ffm:setupTensorFlow
# Or provide your own
./gradlew :demos:tensorflow-ffm:run -PtensorflowHome=/path/to/libtensorflowCause: Missing --enable-native-access flag
Fix: Already configured in build.gradle.kts:
applicationDefaultJvmArgs = listOf("--enable-native-access=ALL-UNNAMED")Error:
macOS x86_64 is not supported by this demo.
TensorFlow dropped x86_64 macOS support after version 2.16.2.
Options:
- Use Apple Silicon Mac
- Use Linux/Windows
- Build TensorFlow 2.16.2 yourself and use
-PtensorflowHome
- No
--enable-previewneeded - Stable API (no breaking changes expected)
- Good performance (zero overhead after warmup)
- Excellent for native library bindings
- Must handle .tar.gz vs .zip extraction
- Architecture naming varies (x86_64 vs amd64, arm64 vs aarch64)
- Fail fast with clear error messages
- Users don't need to manually setup TensorFlow
- Consistent versions across all runs
- Works in CI/CD without manual intervention
- Can still override for custom builds
- FFM's Arena prevents memory leaks
- Automatic cleanup even on exceptions
- No need for manual
free()calls - Compile-time safety vs JNI's runtime crashes
| Demo | Technology | Native Access | Complexity |
|---|---|---|---|
| TensorFlow FFM | FFM API | TensorFlow C | Medium |
| JCuda | Panama/FFM-like | CUDA driver | Medium |
| java-llama.cpp | JNI | llama.cpp | Low (prebuilt) |
| Llama3.java | Pure Java | None | Low |
Potential improvements for this demo:
- GPU support - Use CUDA-enabled TensorFlow build
- More operations - Matrix multiplication, convolutions
- Model loading - Load and run actual TF SavedModels
- Callbacks - Demonstrate upcalls (native → Java)
- Findings.md - FFM technical deep dive
demos/jcuda/- JCuda (similar FFM approach for CUDA)demos/llama3-java/- Pure Java (no native dependencies)