java-foreign

v2026.09.24

Java Foreign Function & Memory API (JEP 442/454, finalized in JDK 22+) plus jextract tool. Replaces JNI for calling C libraries from JVM/Kotlin without writing native glue. Critical for desktop apps needing OS interop (keyring, system APIs) on Linux/macOS/Windows. Covers Linker, MethodHandle, MemorySegment, Arena, struct layout, jextract code generation, and integration with KMP desktop targets. USE WHEN: user mentions "Foreign Memory API", "FFM API", "jextract", "MemorySegment", "Linker", "java.lang.foreign", "JEP 442", "JEP 454", "JDK 22 FFI", "Java FFI no JNI", "panama", "OS keyring Java" DO NOT USE FOR: Rust ↔ Kotlin/Swift bindings - use `languages/uniffi` DO NOT USE FOR: Mobile Android JNI - use `mobile/android-native` DO NOT USE FOR: Generic Java patterns - use Java-specific skill

GitHub
安装命令
npx skhub add claude-dev-suite/java-foreign
Markdown
SKILL.md

Java Foreign Function & Memory API (Project Panama)

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: java-ffm or panama.

Why FFM API (Replacing JNI)

JNI has been Java's native interop since Java 1.1 — and is universally hated:

  • Brittle C glue code, manual memory management
  • Type marshaling overhead per call
  • Poor IDE support
  • One stack trace bug = JVM crash

Foreign Function & Memory API (FFM, finalized in JDK 22 via JEP 454) replaces JNI for most use cases:

  • Pure Java/Kotlin code calls C libraries directly
  • Type-safe via MethodHandle
  • Region-based memory management (Arena)
  • 10-100x faster than JNI for many ops
  • Generated bindings via jextract
  • Works seamlessly with virtual threads

For BHODL-style desktop wallet (Compose Desktop on JVM): the way to talk to OS keyring (libsecret on Linux, Keychain on macOS via Security framework, Credential Manager on Windows via wincred).

Setup

Requires JDK 22+ for stable, JDK 21 for preview:

// build.gradle.kts
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(22))
    }
}

application {
    applicationDefaultJvmArgs = listOf(
        "--enable-native-access=ALL-UNNAMED",            // grant FFI permission
    )
}

// For modular projects, declare in module-info.java:
// requires java.foreign;       — built-in module

For Kotlin DSL (Compose Desktop / KMP):

kotlin {
    jvm("desktop") {
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_22)
        }
    }
}

MemorySegment + Arena

Memory in FFM lives in an Arena — a scope that auto-frees memory at close:

import java.lang.foreign.*;

try (Arena arena = Arena.ofConfined()) {
    // Allocate 256 bytes, zeroed
    MemorySegment buffer = arena.allocate(256);

    // Write
    buffer.set(ValueLayout.JAVA_INT, 0, 42);             // store int at offset 0
    buffer.setUtf8String(8, "hello");                     // C string at offset 8

    // Read
    int value = buffer.get(ValueLayout.JAVA_INT, 0);
    String str = buffer.getUtf8String(8);

    System.out.println(value + " " + str);
}                                                         // memory freed here

Arena types:

  • Arena.ofConfined() — single thread, deterministic free at close()
  • Arena.ofShared() — multi-thread, deterministic free
  • Arena.global() — never freed (process lifetime)
  • Arena.ofAuto() — GC-managed (avoid for large allocations)

For Kotlin (with try-with-resources via use):

import java.lang.foreign.Arena
import java.lang.foreign.MemorySegment
import java.lang.foreign.ValueLayout

Arena.ofConfined().use { arena ->
    val buffer = arena.allocate(256)
    buffer.set(ValueLayout.JAVA_INT, 0, 42)
    val value = buffer.get(ValueLayout.JAVA_INT, 0)
    println(value)
}

Calling a C Function

Example: call strlen from libc.

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class HelloFFM {
    public static void main(String[] args) throws Throwable {
        Linker linker = Linker.nativeLinker();
        SymbolLookup stdlib = linker.defaultLookup();

        MethodHandle strlen = linker.downcallHandle(
            stdlib.find("strlen").orElseThrow(),
            FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
        );

        try (Arena arena = Arena.ofConfined()) {
            MemorySegment cString = arena.allocateUtf8String("Hello, world!");
            long len = (long) strlen.invoke(cString);
            System.out.println("Length: " + len);
        }
    }
}

Kotlin version:

import java.lang.foreign.*
import java.lang.invoke.MethodHandle

fun main() {
    val linker = Linker.nativeLinker()
    val stdlib = linker.defaultLookup()

    val strlen: MethodHandle = linker.downcallHandle(
        stdlib.find("strlen").orElseThrow(),
        FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
    )

    Arena.ofConfined().use { arena ->
        val cString = arena.allocateUtf8String("Hello, world!")
        val len = strlen.invoke(cString) as Long
        println("Length: $len")
    }
}

Loading External Libraries

val customLib = SymbolLookup.libraryLookup("libsecret", Arena.global())
val secretSchema = customLib.find("secret_schema_new").orElseThrow()

Path resolution:

  • Linux: searches LD_LIBRARY_PATH, /usr/lib, etc. (uses dlopen)
  • macOS: searches DYLD_LIBRARY_PATH, system paths
  • Windows: searches PATH, %SystemRoot%\System32, etc.

For bundled native libs:

val libPath = Path.of(System.getProperty("user.dir"), "libs", "libsecret.so")
val customLib = SymbolLookup.libraryLookup(libPath, Arena.global())

Structs

Define struct layouts using MemoryLayout:

import java.lang.foreign.MemoryLayout
import java.lang.foreign.ValueLayout

// struct Point { int x; int y; };
val pointLayout = MemoryLayout.structLayout(
    ValueLayout.JAVA_INT.withName("x"),
    ValueLayout.JAVA_INT.withName("y")
)

// Get var handles for fields
val xHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("x"))
val yHandle = pointLayout.varHandle(MemoryLayout.PathElement.groupElement("y"))

Arena.ofConfined().use { arena ->
    val point = arena.allocate(pointLayout)
    xHandle.set(point, 0L, 10)
    yHandle.set(point, 0L, 20)

    val x = xHandle.get(point, 0L) as Int
    val y = yHandle.get(point, 0L) as Int
    println("($x, $y)")
}

For complex layouts with nested structs and arrays, use paddingLayout to match C alignment.

Upcalls (C Callbacks → Java)

To pass a Java method as a C function pointer:

import java.lang.foreign.*
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles

class Comparator {
    companion object {
        @JvmStatic
        fun compare(a: MemorySegment, b: MemorySegment): Int {
            val aVal = a.get(ValueLayout.JAVA_INT, 0L)
            val bVal = b.get(ValueLayout.JAVA_INT, 0L)
            return aVal.compareTo(bVal)
        }
    }
}

val linker = Linker.nativeLinker()
val handle: MethodHandle = MethodHandles.lookup().findStatic(
    Comparator::class.java,
    "compare",
    java.lang.invoke.MethodType.methodType(Int::class.java, MemorySegment::class.java, MemorySegment::class.java)
)

val descriptor = FunctionDescriptor.of(
    ValueLayout.JAVA_INT,
    ValueLayout.ADDRESS, ValueLayout.ADDRESS
)

Arena.ofConfined().use { arena ->
    val callback = linker.upcallStub(handle, descriptor, arena)
    // pass `callback` to qsort or similar C function
}

jextract — Auto-Generate Bindings

For non-trivial C libraries, hand-writing FFM bindings is tedious. jextract parses C headers and generates Java/Kotlin source:

# Install jextract (separate download from java.net)
# https://jdk.java.net/jextract/

jextract --output src/main/java \
    --target-package com.bhodl.libsecret \
    --library secret-1 \
    /usr/include/libsecret-1/libsecret/secret.h

Generates Java classes wrapping every function and struct, ready to use:

import com.bhodl.libsecret.libsecret_h
import com.bhodl.libsecret.SecretSchema

val schema = SecretSchema.allocate(arena)
SecretSchema.name(schema, /* ... */)
val result = libsecret_h.secret_password_store_sync(/* args */)

OS Keyring Pattern (BHODL Desktop)

For Compose Desktop wallet, store the SQLCipher key in OS keyring:

Linux: libsecret

// jextract -t com.bhodl.libsecret -l secret-1 /usr/include/libsecret-1/libsecret/secret.h
import com.bhodl.libsecret.libsecret_h.*

class LinuxKeyring : Keyring {
    override fun store(account: String, password: ByteArray) {
        Arena.ofConfined().use { arena ->
            val schema = createSchema(arena)
            val accountStr = arena.allocateUtf8String(account)
            val passwordStr = arena.allocateUtf8String(String(password, Charsets.UTF_8))
            secret_password_store_sync(
                schema, /* collection */ secret_collection_default(), /* label */ accountStr,
                passwordStr, /* cancellable */ MemorySegment.NULL,
                /* error */ MemorySegment.NULL,
                accountStr, accountStr, MemorySegment.NULL
            )
        }
    }

    override fun retrieve(account: String): ByteArray? { /* ... */ }
}

macOS: Security framework via Keychain

// Use jextract on /System/Library/Frameworks/Security.framework/Headers
class MacKeyring : Keyring {
    override fun store(account: String, password: ByteArray) {
        // SecKeychainAddGenericPassword via FFM
    }
}

Windows: wincred (Credential Manager)

// jextract on wincred.h
class WindowsKeyring : Keyring {
    override fun store(account: String, password: ByteArray) {
        // CredWriteW via FFM
    }
}

Cross-platform interface

interface Keyring {
    fun store(account: String, password: ByteArray)
    fun retrieve(account: String): ByteArray?
    fun delete(account: String)
}

object KeyringFactory {
    fun create(): Keyring = when {
        System.getProperty("os.name").lowercase().contains("linux") -> LinuxKeyring()
        System.getProperty("os.name").lowercase().contains("mac") -> MacKeyring()
        System.getProperty("os.name").lowercase().contains("windows") -> WindowsKeyring()
        else -> throw UnsupportedOperationException("Unsupported OS")
    }
}

For Compose Desktop wallet: wrap once, use everywhere. The SQLCipher key never leaves OS-managed storage.

KMP Desktop Integration

Use FFM in desktopMain:

shared/
└── src/
    ├── commonMain/kotlin/Keyring.kt              # interface
    ├── desktopMain/kotlin/                        # JVM-only, can use FFM
    │   ├── DesktopKeyring.kt
    │   └── platform/
    │       ├── LinuxKeyring.kt
    │       ├── MacKeyring.kt
    │       └── WindowsKeyring.kt

Use expect class Keyring in commonMain, actual in desktopMain (and androidMain for Android Keystore, iosMain for Keychain).

Performance

OperationJNIFFM API
Empty native call~100 ns~10 ns
Pass int~120 ns~12 ns
Pass string (length 100)~500 ns~50 ns
Allocate + free 1KB~2 μs~200 ns

10x speedup typical. For hot paths (e.g., calling crypto primitives in loop), the difference is dramatic.

Comparison with Alternatives

ApproachPerformanceType safetyMaintenanceUse case
FFM API✅ Fast✅ via MethodHandle✅ jextract auto-genNew code, JDK 22+
JNISlow❌ Manual marshal❌ C glue codeLegacy / Android (no FFM)
JNASlowPartial✅ Reflection-basedQuick-and-dirty interop
JavaCPPFast✅GeneratorNumeric/AI libs
GraalVM Native ImageFastest (AOT)✅Build complexityNative binaries

For BHODL desktop: FFM API if targeting JDK 22+, fallback to JNA if must support older JVMs.

Anti-Patterns

Anti-patternWhy it's badCorrect approach
Arena.global() for short-lived dataMemory leaksArena.ofConfined() with try-with-resources
Forgetting --enable-native-accessRuntime warnings/errorsAdd to JVM args
Sharing MemorySegment across threads (confined)CrashUse Arena.ofShared() if cross-thread
Storing MemorySegment reference past Arena closeUse-after-free segfaultCopy data out before close
Hand-writing complex bindingsError-proneUse jextract
Missing --add-modules java.foreign (older JDKs)Compile failsOr upgrade to JDK 22+ where it's standard
Calling FFM from concurrent virtual threads without Arena.ofShared()RaceUse shared arena or per-thread confined
Treating MemorySegment.NULL as null pointer everywhereSome APIs need real pointerVerify with C signature
Allocating in hot loop without arena reuseGC pressureReuse arena across iterations or Arena.ofConfined once
Using FFM in mobile (Android)Not supportedAndroid uses JNI; FFM is JDK 22+ desktop

Troubleshooting

SymptomCauseFix
WARNING: A restricted method has been invoked--enable-native-access missingAdd JVM arg --enable-native-access=ALL-UNNAMED
UnsatisfiedLinkErrorLibrary not foundSet java.library.path or use SymbolLookup.libraryLookup with absolute path
Segfault on MethodHandle.invokeMismatched FunctionDescriptorVerify C signature matches descriptor
Memory leakArena not closedAlways use try-with-resources
IllegalStateException: Already closedUsed MemorySegment after Arena closeRestructure scope
Slow startupjextract-generated classes loadedLazy-load on first use
ClassCastException on invoke resultWrong return type in castMatch C return type to Java type via ValueLayout
jextract fails on headerMacros / preprocessingPre-process with gcc -E header.h > preprocessed.h
Different behavior x86_64 vs arm64Endianness or struct paddingUse MemoryLayout.paddingLayout explicitly

When NOT to Use This Skill

ScenarioUse Instead
Rust ↔ Kotlin bindingslanguages/uniffi
Android JNImobile/android-native (Android doesn't have FFM yet)
iOS native interoplanguages/swift interop
Generic JavaJava-specific skill
GraalVM native compilationGraalVM-specific
KMP commonMain (no JVM-only API)Stick to expect/actual + Kotlin/Native cinterop on iOS
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

skills/languages/java-foreign

默认分支

main

最新提交

9496306

Tree SHA

fe4e2f1