+ * Loads the {@code runtime.node} native library by absolute path and delegates + * each {@link NativeBinding} method to the corresponding + * {@code copilot_runtime_*} C ABI export. + * + *
+ * The loaded JNA library handle is held in a {@code static} field and is never
+ * released. Native worker threads spawned by the runtime outlive any individual
+ * {@code FfiRuntimeHost} instance; unloading the library while those threads
+ * are active would cause a crash. This mirrors the Rust runtime's own
+ * {@code OnceLock
+ * Loading a library from a different absolute path in the same JVM
+ * process is rejected with {@link IllegalStateException}. Loading from the
+ * same path more than once is silently accepted.
+ *
+ *
+ * The {@link #activeCallbacks} counter is incremented when the native runtime
+ * enters the outbound callback and decremented when the callback returns.
+ * Callers (e.g. {@code FfiRuntimeHost}) must drain this counter to zero before
+ * calling {@link #connectionClose} or {@link #hostShutdown}.
+ *
+ *
+ * JNA callback upcalls are not supported under GraalVM Native Image. InProcess
+ * transport is not available in native-image executables; use subprocess
+ * transport instead.
+ */
+final class JnaNativeBinding implements NativeBinding {
+
+ private static final Logger LOG = Logger.getLogger(JnaNativeBinding.class.getName());
+
+ /**
+ * JNA inner interface mapping the five {@code copilot_runtime_*} C ABI exports.
+ */
+ interface CopilotRuntimeLibrary extends Library {
+ /** Corresponds to {@code copilot_runtime_host_start}. */
+ int copilot_runtime_host_start(byte[] argvJson, int argvJsonLen, byte[] envJson, int envJsonLen);
+
+ /**
+ * Corresponds to {@code copilot_runtime_host_shutdown}.
+ *
+ *
+ * Returns {@code byte} (not Java {@code boolean}) because the Rust ABI exports
+ * a one-byte {@code bool}. JNA maps Java {@code boolean} as a 32-bit C
+ * {@code int}, which would read three extra bytes.
+ */
+ byte copilot_runtime_host_shutdown(int serverId);
+
+ /** Corresponds to {@code copilot_runtime_connection_open}. */
+ int copilot_runtime_connection_open(int serverId, OutboundCallback callback, Pointer userData, byte[] extSource,
+ int extSourceLen, byte[] extName, int extNameLen, byte[] connToken, int connTokenLen);
+
+ /**
+ * Corresponds to {@code copilot_runtime_connection_write}.
+ *
+ * @see #copilot_runtime_host_shutdown for why this returns {@code byte}
+ */
+ byte copilot_runtime_connection_write(int connectionId, byte[] data, int dataLen);
+
+ /**
+ * Corresponds to {@code copilot_runtime_connection_close}.
+ *
+ * @see #copilot_runtime_host_shutdown for why this returns {@code byte}
+ */
+ byte copilot_runtime_connection_close(int connectionId);
+ }
+
+ // -------------------------------------------------------------------------
+ // Process-wide singleton — never unloaded
+ // -------------------------------------------------------------------------
+
+ private static final Object LOAD_LOCK = new Object();
+
+ /** Absolute path of the library that was first loaded into this JVM process. */
+ private static volatile Path loadedPath;
+
+ /** The loaded JNA library interface. Never released after first set. */
+ private static volatile CopilotRuntimeLibrary loadedLib;
+
+ // -------------------------------------------------------------------------
+ // Instance state
+ // -------------------------------------------------------------------------
+
+ /**
+ * The library interface used by this instance for all delegated calls.
+ *
+ *
+ * For the production path ({@link #JnaNativeBinding(Path)}), this is always the
+ * same object as {@link #loadedLib} (the static singleton). For the test path
+ * ({@link #JnaNativeBinding(CopilotRuntimeLibrary)}), this may be a stub or
+ * mock without modifying the static singleton.
+ */
+ private final CopilotRuntimeLibrary lib;
+
+ /**
+ * Count of callbacks currently executing on native threads. Must reach zero
+ * before {@link #connectionClose} or {@link #hostShutdown} is called.
+ */
+ final AtomicInteger activeCallbacks = new AtomicInteger(0);
+
+ /**
+ * Tracked callback wrappers keyed by connection handle. Prevents GC of the JNA
+ * callback function pointer while native code still holds it.
+ */
+ private final Map
+ * This constructor is package-private and intended solely for unit tests.
+ *
+ * @param library
+ * a {@link CopilotRuntimeLibrary} stub or mock for testing
+ */
+ JnaNativeBinding(CopilotRuntimeLibrary library) {
+ // Testing seam — skip the static singleton guard.
+ this.lib = library;
+ }
+
+ // -------------------------------------------------------------------------
+ // NativeBinding delegation
+ // -------------------------------------------------------------------------
+
+ @Override
+ public int hostStart(byte[] argvJson, int argvJsonLen, byte[] envJson, int envJsonLen) {
+ return lib.copilot_runtime_host_start(argvJson, argvJsonLen, envJson, envJsonLen);
+ }
+
+ @Override
+ public boolean hostShutdown(int serverId) {
+ return lib.copilot_runtime_host_shutdown(serverId) != 0;
+ }
+
+ @Override
+ public int connectionOpen(int serverId, OutboundCallback callback, Pointer userData, byte[] extSource,
+ int extSourceLen, byte[] extName, int extNameLen, byte[] connToken, int connTokenLen) {
+ // Wrap the caller's callback to maintain active-callback tracking.
+ OutboundCallback tracked = (ud, data, len) -> {
+ activeCallbacks.incrementAndGet();
+ try {
+ callback.invoke(ud, data, len);
+ } finally {
+ activeCallbacks.decrementAndGet();
+ }
+ };
+ int connectionId = lib.copilot_runtime_connection_open(serverId, tracked, userData, extSource, extSourceLen,
+ extName, extNameLen, connToken, connTokenLen);
+ if (connectionId != 0) {
+ // Hold a strong reference to prevent GC of the JNA function pointer.
+ trackedCallbacks.put(connectionId, tracked);
+ }
+ return connectionId;
+ }
+
+ @Override
+ public boolean connectionWrite(int connectionId, byte[] data, int dataLen) {
+ return lib.copilot_runtime_connection_write(connectionId, data, dataLen) != 0;
+ }
+
+ @Override
+ public boolean connectionClose(int connectionId) {
+ try {
+ return lib.copilot_runtime_connection_close(connectionId) != 0;
+ } finally {
+ trackedCallbacks.remove(connectionId);
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Testing support
+ // -------------------------------------------------------------------------
+
+ /**
+ * Resets the process-wide static state for unit tests.
+ *
+ *
+ * Must only be called from test code. Resets
+ * {@link #loadedPath} and {@link #loadedLib} so that a subsequent
+ * {@link #JnaNativeBinding(Path)} call can load a different library. In
+ * production, the library is never unloaded.
+ */
+ static void resetForTesting() {
+ synchronized (LOAD_LOCK) {
+ loadedPath = null;
+ loadedLib = null;
+ }
+ }
+}
diff --git a/java/sdk/src/main/java/com/github/copilot/ffi/NativeBinding.java b/java/sdk/src/main/java/com/github/copilot/ffi/NativeBinding.java
new file mode 100644
index 0000000000..3aa8ca9e4d
--- /dev/null
+++ b/java/sdk/src/main/java/com/github/copilot/ffi/NativeBinding.java
@@ -0,0 +1,131 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.ffi;
+
+import com.sun.jna.Pointer;
+
+/**
+ * Internal abstraction over the Copilot runtime C ABI.
+ *
+ *
+ * Defines the five {@code extern "C"} entry points exposed by the native
+ * {@code runtime.node} library. The JNA-backed implementation
+ * ({@link JnaNativeBinding}) delegates to these through JNA. A future FFM
+ * implementation may be substituted via the multi-release JAR mechanism without
+ * changing callers.
+ *
+ *
+ * All classes in {@code com.github.copilot.ffi} are internal; consumers must
+ * not reference them directly.
+ *
+ *
+ * All frames use LSP {@code Content-Length} header framing, identical to the
+ * stdio transport. No special encoding or decoding is needed at the FFI
+ * boundary.
+ */
+interface NativeBinding {
+
+ /**
+ * Starts the runtime host.
+ *
+ *
+ * Blocks for up to ~30 s while the worker boots and connects back. Must not be
+ * called on an async/reactive executor thread.
+ *
+ * @param argvJson
+ * UTF-8 JSON array of strings: the entrypoint and required flags
+ * @param argvJsonLen
+ * byte length of {@code argvJson}
+ * @param envJson
+ * UTF-8 JSON object of environment overrides, or {@code null} when
+ * empty
+ * @param envJsonLen
+ * byte length of {@code envJson}, or {@code 0} when {@code envJson}
+ * is null
+ * @return server handle ({@code 0} on failure)
+ */
+ int hostStart(byte[] argvJson, int argvJsonLen, byte[] envJson, int envJsonLen);
+
+ /**
+ * Shuts down the runtime host.
+ *
+ * @param serverId
+ * non-zero server handle returned by {@link #hostStart}
+ * @return {@code true} on success
+ */
+ boolean hostShutdown(int serverId);
+
+ /**
+ * Opens a bidirectional connection and registers the outbound data callback.
+ *
+ *
+ * The {@code extSource}, {@code extName}, and {@code connToken} parameters are
+ * reserved extension points. All current SDK implementations pass
+ * {@code null}/0 for all three.
+ *
+ * @param serverId
+ * non-zero server handle returned by {@link #hostStart}
+ * @param callback
+ * JNA callback invoked by the runtime on native threads when
+ * outbound data is available; must be held as a strong reference by
+ * the caller
+ * @param userData
+ * opaque cookie passed back to {@code callback} unchanged; pass
+ * {@link Pointer#NULL}
+ * @param extSource
+ * reserved; pass {@code null}
+ * @param extSourceLen
+ * byte length of {@code extSource}; pass {@code 0}
+ * @param extName
+ * reserved; pass {@code null}
+ * @param extNameLen
+ * byte length of {@code extName}; pass {@code 0}
+ * @param connToken
+ * reserved; pass {@code null}
+ * @param connTokenLen
+ * byte length of {@code connToken}; pass {@code 0}
+ * @return connection handle ({@code 0} on failure)
+ */
+ int connectionOpen(int serverId, OutboundCallback callback, Pointer userData, byte[] extSource, int extSourceLen,
+ byte[] extName, int extNameLen, byte[] connToken, int connTokenLen);
+
+ /**
+ * Writes a JSON-RPC frame to the runtime.
+ *
+ *
+ * The native side copies the buffer synchronously before returning; the byte
+ * array does not need to survive past this call.
+ *
+ * @param connectionId
+ * non-zero connection handle returned by {@link #connectionOpen}
+ * @param data
+ * frame bytes
+ * @param dataLen
+ * byte length of {@code data}
+ * @return {@code true} on success
+ */
+ boolean connectionWrite(int connectionId, byte[] data, int dataLen);
+
+ /**
+ * Closes a connection.
+ *
+ * @param connectionId
+ * non-zero connection handle returned by {@link #connectionOpen}
+ * @return {@code true} on success
+ */
+ boolean connectionClose(int connectionId);
+}
diff --git a/java/sdk/src/main/java/com/github/copilot/ffi/OutboundCallback.java b/java/sdk/src/main/java/com/github/copilot/ffi/OutboundCallback.java
new file mode 100644
index 0000000000..6f5c319224
--- /dev/null
+++ b/java/sdk/src/main/java/com/github/copilot/ffi/OutboundCallback.java
@@ -0,0 +1,46 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.ffi;
+
+import com.sun.jna.Callback;
+import com.sun.jna.Pointer;
+
+/**
+ * JNA callback interface for the runtime-to-Java outbound data path.
+ *
+ *
+ * The native runtime invokes this callback on a native thread when data is
+ * ready to be delivered to the Java side. JNA automatically attaches the native
+ * thread to the JVM before dispatching the callback.
+ *
+ *
+ * Buffer lifetime: The {@code data} pointer is only valid for
+ * the duration of the callback invocation. Implementations must copy the bytes
+ * out (e.g. {@code data.getByteArray(0, len)}) before returning.
+ *
+ *
+ * GC protection: Instances must be held as strong-reference
+ * fields for as long as native code may invoke the callback. If the instance is
+ * garbage-collected, the function pointer becomes dangling and the JVM will
+ * crash.
+ */
+@FunctionalInterface
+interface OutboundCallback extends Callback {
+
+ /**
+ * Invoked by the native runtime when outbound data is available.
+ *
+ * @param userData
+ * opaque cookie passed through unchanged from
+ * {@code copilot_runtime_connection_open}; always
+ * {@code Pointer.NULL} in this SDK
+ * @param data
+ * pointer to the outbound byte buffer; valid only for the duration
+ * of this invocation
+ * @param len
+ * byte length of the buffer pointed to by {@code data}
+ */
+ void invoke(Pointer userData, Pointer data, int len);
+}
diff --git a/java/sdk/src/main/java/module-info.java b/java/sdk/src/main/java/module-info.java
index 38bc1f93d5..8bc2dbd55c 100644
--- a/java/sdk/src/main/java/module-info.java
+++ b/java/sdk/src/main/java/module-info.java
@@ -12,6 +12,7 @@
requires com.fasterxml.jackson.datatype.jsr310;
requires static com.github.spotbugs.annotations;
requires static java.compiler;
+ requires static com.sun.jna;
requires java.net.http;
requires java.logging;
@@ -25,6 +26,7 @@
opens com.github.copilot.generated to com.fasterxml.jackson.databind;
opens com.github.copilot.generated.rpc to com.fasterxml.jackson.databind;
opens com.github.copilot.rpc to com.fasterxml.jackson.databind;
+ opens com.github.copilot.ffi to com.sun.jna;
provides javax.annotation.processing.Processor
with com.github.copilot.CopilotExperimentalProcessor, com.github.copilot.tool.CopilotToolProcessor;
diff --git a/java/sdk/src/test/java/com/github/copilot/ffi/JnaNativeBindingTest.java b/java/sdk/src/test/java/com/github/copilot/ffi/JnaNativeBindingTest.java
new file mode 100644
index 0000000000..9f0f939eee
--- /dev/null
+++ b/java/sdk/src/test/java/com/github/copilot/ffi/JnaNativeBindingTest.java
@@ -0,0 +1,521 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.ffi;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+import com.sun.jna.Pointer;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Unit tests for {@link JnaNativeBinding} using the spike-3-4 test native
+ * library ({@code libcallback_test}).
+ *
+ *
+ * The spike test library exports simplified versions of the runtime ABI
+ * functions: {@code host_start}, {@code host_shutdown},
+ * {@code connection_open}, {@code connection_write}, and
+ * {@code connection_close}. Callback tests use this library through a
+ * test-specific JNA interface, while loading and guard tests exercise
+ * {@link JnaNativeBinding} directly.
+ *
+ *
+ * Tests that require the native library are conditionally skipped when the
+ * library is not present (e.g. on an architecture without a pre-built binary).
+ */
+class JnaNativeBindingTest {
+
+ /**
+ * System property that points at the absolute path of the test native library.
+ *
+ *
+ * Default: the {@code libcallback_test.so} built from the spike-3-4 Rust crate,
+ * relative to {@code java/sdk/}.
+ */
+ private static final String TEST_LIB_PATH_PROP = "copilot.test.nativelib.path";
+
+ private static final String SPIKE_LIB_PATH = System.getProperty(TEST_LIB_PATH_PROP,
+ "../../1917-java-embed-rust-cli-runtime-remove-before-merge" + "/spike-3-4-jna-callback-and-threading"
+ + "/rust-dll/target/release/libcallback_test.so");
+
+ // -------------------------------------------------------------------------
+ // Test-specific JNA interface for the spike-3-4 test library
+ // -------------------------------------------------------------------------
+
+ /**
+ * JNA interface for the simplified test library. Maps Java names to the
+ * snake_case exports of {@code libcallback_test}.
+ *
+ *
+ * Note: This test library exports simplified names ({@code host_start},
+ * {@code connection_open}, etc.) rather than the production
+ * {@code copilot_runtime_*} names. Production symbol resolution is validated
+ * lazily by JNA when each method is first called through
+ * {@link JnaNativeBinding.CopilotRuntimeLibrary}.
+ */
+ interface CallbackTestLib extends Library {
+ /** Simulates {@code copilot_runtime_host_start}; always returns 42. */
+ int host_start();
+
+ /**
+ * Simulates {@code copilot_runtime_host_shutdown}; always returns nonzero.
+ * Returns {@code byte} to match the Rust ABI one-byte {@code bool}.
+ */
+ byte host_shutdown(int serverHandle);
+
+ /**
+ * Simulates {@code copilot_runtime_connection_open}. Spawns a native thread
+ * that invokes {@code callback} {@code burstCount} times. Returns 7.
+ */
+ int connection_open(int serverHandle, OutboundCallback callback, Pointer userData, int burstCount);
+
+ /**
+ * Simulates {@code copilot_runtime_connection_write}; always returns nonzero.
+ * Returns {@code byte} to match the Rust ABI one-byte {@code bool}.
+ */
+ byte connection_write(int connectionHandle, byte[] data, int len);
+
+ /**
+ * Simulates {@code copilot_runtime_connection_close}; always returns nonzero.
+ * Returns {@code byte} to match the Rust ABI one-byte {@code bool}.
+ */
+ byte connection_close(int connectionHandle);
+ }
+
+ // -------------------------------------------------------------------------
+ // Stub CopilotRuntimeLibrary for delegation tests
+ // -------------------------------------------------------------------------
+
+ /**
+ * Minimal stub for testing {@link JnaNativeBinding} delegation without disk
+ * I/O.
+ */
+ private static class StubRuntimeLibrary implements JnaNativeBinding.CopilotRuntimeLibrary {
+ int hostStartReturn = 1;
+ byte hostShutdownReturn = 1;
+ int connectionOpenReturn = 1;
+ byte connectionWriteReturn = 1;
+ byte connectionCloseReturn = 1;
+
+ byte[] lastArgvJson;
+ int lastArgvJsonLen;
+ int lastServerId;
+ int lastConnectionId;
+ OutboundCallback lastCallback;
+
+ @Override
+ public int copilot_runtime_host_start(byte[] argvJson, int argvJsonLen, byte[] envJson, int envJsonLen) {
+ lastArgvJson = argvJson;
+ lastArgvJsonLen = argvJsonLen;
+ return hostStartReturn;
+ }
+
+ @Override
+ public byte copilot_runtime_host_shutdown(int serverId) {
+ lastServerId = serverId;
+ return hostShutdownReturn;
+ }
+
+ @Override
+ public int copilot_runtime_connection_open(int serverId, OutboundCallback callback, Pointer userData,
+ byte[] extSource, int extSourceLen, byte[] extName, int extNameLen, byte[] connToken,
+ int connTokenLen) {
+ lastServerId = serverId;
+ lastCallback = callback;
+ return connectionOpenReturn;
+ }
+
+ @Override
+ public byte copilot_runtime_connection_write(int connectionId, byte[] data, int dataLen) {
+ lastConnectionId = connectionId;
+ return connectionWriteReturn;
+ }
+
+ @Override
+ public byte copilot_runtime_connection_close(int connectionId) {
+ lastConnectionId = connectionId;
+ return connectionCloseReturn;
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Helpers
+ // -------------------------------------------------------------------------
+
+ private static boolean testLibExists() {
+ return Files.isRegularFile(testLibAbsPath());
+ }
+
+ private static Path testLibAbsPath() {
+ return Path.of(SPIKE_LIB_PATH).toAbsolutePath().normalize();
+ }
+
+ private static CallbackTestLib loadTestLib() {
+ return Native.load(testLibAbsPath().toString(), CallbackTestLib.class);
+ }
+
+ @AfterEach
+ void resetStaticState() {
+ JnaNativeBinding.resetForTesting();
+ }
+
+ // =========================================================================
+ // Delegation via testing constructor (stub — no disk I/O)
+ // =========================================================================
+
+ @Test
+ void hostStartDelegatesToLibraryAndReturnsHandle() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.hostStartReturn = 77;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+
+ byte[] argv = "[\"copilot\"]".getBytes(StandardCharsets.UTF_8);
+ int result = binding.hostStart(argv, argv.length, null, 0);
+
+ assertEquals(77, result, "hostStart should return the stub's configured value");
+ assertEquals(argv, stub.lastArgvJson, "argv bytes should be passed through unchanged");
+ assertEquals(argv.length, stub.lastArgvJsonLen);
+ }
+
+ @Test
+ void hostStartReturnsZeroOnFailure() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.hostStartReturn = 0;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+
+ byte[] argv = "[\"copilot\"]".getBytes(StandardCharsets.UTF_8);
+ assertEquals(0, binding.hostStart(argv, argv.length, null, 0), "hostStart must return 0 to signal failure");
+ }
+
+ @Test
+ void hostShutdownDelegatesToLibrary() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.hostShutdownReturn = 1;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+
+ assertTrue(binding.hostShutdown(42));
+ assertEquals(42, stub.lastServerId);
+ }
+
+ @Test
+ void hostShutdownReturnsFalseOnFailure() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.hostShutdownReturn = 0;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+ assertFalse(binding.hostShutdown(1));
+ }
+
+ @Test
+ void connectionOpenDelegatesToLibrary() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.connectionOpenReturn = 55;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+
+ OutboundCallback noop = (ud, data, len) -> {
+ };
+ int connId = binding.connectionOpen(42, noop, Pointer.NULL, null, 0, null, 0, null, 0);
+
+ assertEquals(55, connId, "connectionOpen should return the stub's configured handle");
+ assertEquals(42, stub.lastServerId);
+ }
+
+ @Test
+ void connectionOpenReturnsZeroOnFailure() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.connectionOpenReturn = 0;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+
+ OutboundCallback noop = (ud, data, len) -> {
+ };
+ assertEquals(0, binding.connectionOpen(1, noop, Pointer.NULL, null, 0, null, 0, null, 0),
+ "connectionOpen must return 0 to signal failure");
+ }
+
+ @Test
+ void connectionWriteDelegatesToLibrary() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.connectionWriteReturn = 1;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+
+ byte[] data = "hello".getBytes(StandardCharsets.UTF_8);
+ assertTrue(binding.connectionWrite(7, data, data.length));
+ assertEquals(7, stub.lastConnectionId);
+ }
+
+ @Test
+ void connectionWriteReturnsFalseOnFailure() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.connectionWriteReturn = 0;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+
+ byte[] data = "x".getBytes(StandardCharsets.UTF_8);
+ assertFalse(binding.connectionWrite(1, data, data.length),
+ "connectionWrite must propagate false return from the library");
+ }
+
+ @Test
+ void connectionCloseDelegatesToLibrary() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.connectionCloseReturn = 1;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+ assertTrue(binding.connectionClose(7));
+ assertEquals(7, stub.lastConnectionId);
+ }
+
+ @Test
+ void connectionCloseReturnsFalseOnFailure() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.connectionCloseReturn = 0;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+ assertFalse(binding.connectionClose(1));
+ }
+
+ @Test
+ void activeCallbacksStartsAtZero() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+ assertEquals(0, binding.activeCallbacks.get(), "Active callback counter must start at zero");
+ }
+
+ // =========================================================================
+ // Library loading — success paths (requires native library on disk)
+ // =========================================================================
+
+ @Test
+ void loadByPathSucceedsWhenLibraryExists() {
+ assumeTrue(testLibExists(), "Native test library not found at " + SPIKE_LIB_PATH);
+ JnaNativeBinding binding = new JnaNativeBinding(testLibAbsPath());
+ assertNotNull(binding);
+ }
+
+ @Test
+ void loadByPathTwiceWithSamePathSucceeds() {
+ assumeTrue(testLibExists(), "Native test library not found at " + SPIKE_LIB_PATH);
+ new JnaNativeBinding(testLibAbsPath());
+ // Second construction with the same absolute path must not throw.
+ new JnaNativeBinding(testLibAbsPath());
+ }
+
+ @Test
+ void activeCallbacksStartsAtZeroAfterPathLoad() {
+ assumeTrue(testLibExists(), "Native test library not found at " + SPIKE_LIB_PATH);
+ JnaNativeBinding binding = new JnaNativeBinding(testLibAbsPath());
+ assertEquals(0, binding.activeCallbacks.get());
+ }
+
+ // =========================================================================
+ // Duplicate-load guard
+ // =========================================================================
+
+ @Test
+ void loadFromDifferentPathThrowsIllegalState(@TempDir Path tempDir) throws Exception {
+ assumeTrue(testLibExists(), "Native test library not found at " + SPIKE_LIB_PATH);
+ Path altPath = tempDir.resolve("libcallback_test_alt.so");
+ Files.copy(testLibAbsPath(), altPath);
+
+ new JnaNativeBinding(testLibAbsPath());
+
+ IllegalStateException ex = assertThrows(IllegalStateException.class, () -> new JnaNativeBinding(altPath));
+
+ String msg = ex.getMessage();
+ assertTrue(msg.contains("already loaded from"), "Diagnostic must mention 'already loaded from', got: " + msg);
+ assertTrue(msg.contains(testLibAbsPath().toString()), "Diagnostic must contain path A, got: " + msg);
+ assertTrue(msg.contains(altPath.toString()), "Diagnostic must contain path B, got: " + msg);
+ }
+
+ @Test
+ void duplicateLoadDiagnosticMentionsNotSupported(@TempDir Path tempDir) throws Exception {
+ assumeTrue(testLibExists(), "Native test library not found at " + SPIKE_LIB_PATH);
+ Path altPath = tempDir.resolve("libcallback_test_b.so");
+ Files.copy(testLibAbsPath(), altPath);
+
+ new JnaNativeBinding(testLibAbsPath());
+
+ IllegalStateException ex = assertThrows(IllegalStateException.class, () -> new JnaNativeBinding(altPath));
+ assertTrue(ex.getMessage().contains("not supported"),
+ "Diagnostic must mention 'not supported', got: " + ex.getMessage());
+ }
+
+ @Test
+ void resetForTestingAllowsReloadFromDifferentPath(@TempDir Path tempDir) throws Exception {
+ assumeTrue(testLibExists(), "Native test library not found at " + SPIKE_LIB_PATH);
+ Path altPath = tempDir.resolve("libcallback_test_reset.so");
+ Files.copy(testLibAbsPath(), altPath);
+
+ new JnaNativeBinding(testLibAbsPath());
+
+ JnaNativeBinding.resetForTesting();
+
+ // After reset, a different path must succeed.
+ new JnaNativeBinding(altPath);
+ }
+
+ // =========================================================================
+ // Callback invocation via test native library
+ // =========================================================================
+
+ @Test
+ void callbackIsInvokedFromNativeThread() throws Exception {
+ assumeTrue(testLibExists(), "Native test library not found at " + SPIKE_LIB_PATH);
+ CallbackTestLib lib = loadTestLib();
+ int serverHandle = lib.host_start();
+ assertEquals(42, serverHandle, "host_start should return 42");
+
+ int burstCount = 3;
+ CountDownLatch latch = new CountDownLatch(burstCount);
+ AtomicInteger callbackCount = new AtomicInteger(0);
+ AtomicInteger activeCallbacks = new AtomicInteger(0);
+
+ OutboundCallback callback = (userData, data, len) -> {
+ activeCallbacks.incrementAndGet();
+ try {
+ callbackCount.incrementAndGet();
+ // Copy before returning — pointer only valid during invocation.
+ byte[] bytes = data.getByteArray(0, len);
+ assertEquals(len, bytes.length, "Copied byte array length must equal len parameter");
+ } finally {
+ activeCallbacks.decrementAndGet();
+ latch.countDown();
+ }
+ };
+
+ int connHandle = lib.connection_open(serverHandle, callback, Pointer.NULL, burstCount);
+ assertEquals(7, connHandle, "connection_open should return 7");
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS), "All callbacks must complete within 10 seconds");
+ assertEquals(burstCount, callbackCount.get(), "Callback must be invoked exactly burstCount times");
+ assertEquals(0, activeCallbacks.get(),
+ "Active callback count must return to zero after all callbacks complete");
+ }
+
+ @Test
+ void activeCallbackCountIsIncrementedDuringCallback() {
+ StubRuntimeLibrary stub = new StubRuntimeLibrary();
+ stub.connectionOpenReturn = 99;
+ JnaNativeBinding binding = new JnaNativeBinding(stub);
+
+ AtomicInteger observedDuringCallback = new AtomicInteger(-1);
+
+ OutboundCallback userCallback = (userData, data, len) -> {
+ // Observe binding.activeCallbacks while inside the callback
+ observedDuringCallback.set(binding.activeCallbacks.get());
+ };
+
+ binding.connectionOpen(1, userCallback, Pointer.NULL, null, 0, null, 0, null, 0);
+
+ // The stub captured the tracked wrapper — invoke it to trigger tracking
+ assertNotNull(stub.lastCallback, "Stub must have captured the tracked callback");
+ stub.lastCallback.invoke(Pointer.NULL, Pointer.NULL, 0);
+
+ assertEquals(1, observedDuringCallback.get(), "binding.activeCallbacks must be 1 during callback execution");
+ assertEquals(0, binding.activeCallbacks.get(),
+ "binding.activeCallbacks must return to 0 after callback completes");
+ }
+
+ @Test
+ void callbackDataContainsJsonRpcContent() throws Exception {
+ assumeTrue(testLibExists(), "Native test library not found at " + SPIKE_LIB_PATH);
+ CallbackTestLib lib = loadTestLib();
+ int serverHandle = lib.host_start();
+
+ CountDownLatch latch = new CountDownLatch(1);
+ AtomicReferenceDuplicate-load guard
+ * Active-callback tracking
+ * GraalVM Native Image
+ * C ABI entry points
+ *
+ *
+ *
+ * Wire format
+ *