Skip to content

Commit 3f736b5

Browse files
Copilotedburns
andauthored
feat(java): add JNA binding interface and implementation (task 4.4)
- Add JNA 5.19.1 as optional dependency to java/sdk/pom.xml with jna.version property for deliberate upgrades - Create OutboundCallback.java: JNA Callback interface for native-to-Java outbound data delivery - Create NativeBinding.java: interface abstraction for the 5 copilot_runtime_* C ABI entry points - Create JnaNativeBinding.java: JNA implementation with static singleton (library-never-unloads pattern), duplicate path guard, and active-callback AtomicInteger tracking - Create JnaNativeBindingTest.java: 24 unit tests covering delegation, loading, duplicate guard, and callback behavior using the spike-3-4 test native library - Update module-info.java: requires static com.sun.jna, opens com.github.copilot.ffi to com.sun.jna Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent 3af7a20 commit 3f736b5

6 files changed

Lines changed: 950 additions & 0 deletions

File tree

java/sdk/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@
6262
<surefire.jvm.args />
6363
<!-- Override parent: the sdk module IS published to Maven Central. -->
6464
<maven.deploy.skip>false</maven.deploy.skip>
65+
<!-- JNA version — pin deliberately; a JNA upgrade must rerun the callback spike. -->
66+
<jna.version>5.19.1</jna.version>
6567
</properties>
6668

6769
<dependencies>
@@ -90,6 +92,18 @@
9092
<scope>provided</scope>
9193
</dependency>
9294

95+
<!--
96+
JNA — required only when using InProcess transport. Optional so that
97+
subprocess-only consumers do not receive JNA transitively.
98+
Pin the version; a JNA upgrade must rerun the callback spike.
99+
-->
100+
<dependency>
101+
<groupId>net.java.dev.jna</groupId>
102+
<artifactId>jna</artifactId>
103+
<version>${jna.version}</version>
104+
<optional>true</optional>
105+
</dependency>
106+
93107
<!-- Test dependencies -->
94108
<dependency>
95109
<groupId>org.junit.jupiter</groupId>
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.ffi;
6+
7+
import com.sun.jna.Library;
8+
import com.sun.jna.Native;
9+
import com.sun.jna.Pointer;
10+
11+
import java.nio.file.Path;
12+
import java.util.concurrent.atomic.AtomicInteger;
13+
import java.util.logging.Logger;
14+
15+
/**
16+
* JNA-backed implementation of {@link NativeBinding}.
17+
*
18+
* <p>
19+
* Loads the {@code runtime.node} native library by absolute path and delegates
20+
* each {@link NativeBinding} method to the corresponding
21+
* {@code copilot_runtime_*} C ABI export.
22+
*
23+
* <h2>Library-never-unloads pattern</h2>
24+
* <p>
25+
* The loaded JNA library handle is held in a {@code static} field and is never
26+
* released. Native worker threads spawned by the runtime outlive any individual
27+
* {@code FfiRuntimeHost} instance; unloading the library while those threads
28+
* are active would cause a crash. This mirrors the Rust runtime's own
29+
* {@code OnceLock<Mutex<HashMap<PathBuf, &'static Library>>>} pattern.
30+
*
31+
* <h2>Duplicate-load guard</h2>
32+
* <p>
33+
* Loading a library from a <em>different</em> absolute path in the same JVM
34+
* process is rejected with {@link IllegalStateException}. Loading from the
35+
* <em>same</em> path more than once is silently accepted.
36+
*
37+
* <h2>Active-callback tracking</h2>
38+
* <p>
39+
* The {@link #activeCallbacks} counter is incremented when the native runtime
40+
* enters the outbound callback and decremented when the callback returns.
41+
* Callers (e.g. {@code FfiRuntimeHost}) must drain this counter to zero before
42+
* calling {@link #connectionClose} or {@link #hostShutdown}.
43+
*
44+
* <h2>GraalVM Native Image</h2>
45+
* <p>
46+
* JNA callback upcalls are not supported under GraalVM Native Image. InProcess
47+
* transport is not available in native-image executables; use subprocess
48+
* transport instead.
49+
*/
50+
final class JnaNativeBinding implements NativeBinding {
51+
52+
private static final Logger LOG = Logger.getLogger(JnaNativeBinding.class.getName());
53+
54+
/**
55+
* JNA inner interface mapping the five {@code copilot_runtime_*} C ABI exports.
56+
*/
57+
interface CopilotRuntimeLibrary extends Library {
58+
/** Corresponds to {@code copilot_runtime_host_start}. */
59+
int copilot_runtime_host_start(byte[] argvJson, int argvJsonLen, byte[] envJson, int envJsonLen);
60+
61+
/** Corresponds to {@code copilot_runtime_host_shutdown}. */
62+
boolean copilot_runtime_host_shutdown(int serverId);
63+
64+
/** Corresponds to {@code copilot_runtime_connection_open}. */
65+
int copilot_runtime_connection_open(int serverId, OutboundCallback callback, Pointer userData, byte[] extSource,
66+
int extSourceLen, byte[] extName, int extNameLen, byte[] connToken, int connTokenLen);
67+
68+
/** Corresponds to {@code copilot_runtime_connection_write}. */
69+
boolean copilot_runtime_connection_write(int connectionId, byte[] data, int dataLen);
70+
71+
/** Corresponds to {@code copilot_runtime_connection_close}. */
72+
boolean copilot_runtime_connection_close(int connectionId);
73+
}
74+
75+
// -------------------------------------------------------------------------
76+
// Process-wide singleton — never unloaded
77+
// -------------------------------------------------------------------------
78+
79+
private static final Object LOAD_LOCK = new Object();
80+
81+
/** Absolute path of the library that was first loaded into this JVM process. */
82+
private static volatile Path loadedPath;
83+
84+
/** The loaded JNA library interface. Never released after first set. */
85+
private static volatile CopilotRuntimeLibrary loadedLib;
86+
87+
// -------------------------------------------------------------------------
88+
// Instance state
89+
// -------------------------------------------------------------------------
90+
91+
/**
92+
* The library interface used by this instance for all delegated calls.
93+
*
94+
* <p>
95+
* For the production path ({@link #JnaNativeBinding(Path)}), this is always the
96+
* same object as {@link #loadedLib} (the static singleton). For the test path
97+
* ({@link #JnaNativeBinding(CopilotRuntimeLibrary)}), this may be a stub or
98+
* mock without modifying the static singleton.
99+
*/
100+
private final CopilotRuntimeLibrary lib;
101+
102+
/**
103+
* Count of callbacks currently executing on native threads. Must reach zero
104+
* before {@link #connectionClose} or {@link #hostShutdown} is called.
105+
*/
106+
final AtomicInteger activeCallbacks = new AtomicInteger(0);
107+
108+
// -------------------------------------------------------------------------
109+
// Constructors
110+
// -------------------------------------------------------------------------
111+
112+
/**
113+
* Loads (or re-uses) the native library at the given absolute path.
114+
*
115+
* @param libraryPath
116+
* absolute path to the {@code runtime.node} native library
117+
* @throws IllegalStateException
118+
* if a <em>different</em> library path has already been loaded in
119+
* this JVM process
120+
*/
121+
JnaNativeBinding(Path libraryPath) {
122+
Path absPath = libraryPath.toAbsolutePath().normalize();
123+
synchronized (LOAD_LOCK) {
124+
if (loadedLib == null) {
125+
LOG.fine(() -> "Loading native library from: " + absPath);
126+
loadedLib = Native.load(absPath.toString(), CopilotRuntimeLibrary.class);
127+
loadedPath = absPath;
128+
LOG.fine(() -> "Native library loaded: " + absPath);
129+
} else if (!absPath.equals(loadedPath)) {
130+
throw new IllegalStateException("An in-process FFI runtime library is already loaded from '"
131+
+ loadedPath + "'; loading a different library from '" + absPath
132+
+ "' in the same process is not supported.");
133+
}
134+
}
135+
this.lib = loadedLib;
136+
}
137+
138+
/**
139+
* Testing constructor — accepts a pre-built {@link CopilotRuntimeLibrary}
140+
* directly, bypassing disk I/O and the static singleton guard.
141+
*
142+
* <p>
143+
* This constructor is package-private and intended solely for unit tests.
144+
*
145+
* @param library
146+
* a {@link CopilotRuntimeLibrary} stub or mock for testing
147+
*/
148+
JnaNativeBinding(CopilotRuntimeLibrary library) {
149+
// Testing seam — skip the static singleton guard.
150+
this.lib = library;
151+
}
152+
153+
// -------------------------------------------------------------------------
154+
// NativeBinding delegation
155+
// -------------------------------------------------------------------------
156+
157+
@Override
158+
public int hostStart(byte[] argvJson, int argvJsonLen, byte[] envJson, int envJsonLen) {
159+
return lib.copilot_runtime_host_start(argvJson, argvJsonLen, envJson, envJsonLen);
160+
}
161+
162+
@Override
163+
public boolean hostShutdown(int serverId) {
164+
return lib.copilot_runtime_host_shutdown(serverId);
165+
}
166+
167+
@Override
168+
public int connectionOpen(int serverId, OutboundCallback callback, Pointer userData, byte[] extSource,
169+
int extSourceLen, byte[] extName, int extNameLen, byte[] connToken, int connTokenLen) {
170+
// Wrap the caller's callback to maintain active-callback tracking.
171+
OutboundCallback tracked = (ud, data, len) -> {
172+
activeCallbacks.incrementAndGet();
173+
try {
174+
callback.invoke(ud, data, len);
175+
} finally {
176+
activeCallbacks.decrementAndGet();
177+
}
178+
};
179+
return lib.copilot_runtime_connection_open(serverId, tracked, userData, extSource, extSourceLen, extName,
180+
extNameLen, connToken, connTokenLen);
181+
}
182+
183+
@Override
184+
public boolean connectionWrite(int connectionId, byte[] data, int dataLen) {
185+
return lib.copilot_runtime_connection_write(connectionId, data, dataLen);
186+
}
187+
188+
@Override
189+
public boolean connectionClose(int connectionId) {
190+
return lib.copilot_runtime_connection_close(connectionId);
191+
}
192+
193+
// -------------------------------------------------------------------------
194+
// Testing support
195+
// -------------------------------------------------------------------------
196+
197+
/**
198+
* Resets the process-wide static state for unit tests.
199+
*
200+
* <p>
201+
* <strong>Must only be called from test code.</strong> Resets
202+
* {@link #loadedPath} and {@link #loadedLib} so that a subsequent
203+
* {@link #JnaNativeBinding(Path)} call can load a different library. In
204+
* production, the library is never unloaded.
205+
*/
206+
static void resetForTesting() {
207+
synchronized (LOAD_LOCK) {
208+
loadedPath = null;
209+
loadedLib = null;
210+
}
211+
}
212+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.ffi;
6+
7+
import com.sun.jna.Pointer;
8+
9+
/**
10+
* Internal abstraction over the Copilot runtime C ABI.
11+
*
12+
* <p>
13+
* Defines the five {@code extern "C"} entry points exposed by the native
14+
* {@code runtime.node} library. The JNA-backed implementation
15+
* ({@link JnaNativeBinding}) delegates to these through JNA. A future FFM
16+
* implementation may be substituted via the multi-release JAR mechanism without
17+
* changing callers.
18+
*
19+
* <p>
20+
* All classes in {@code com.github.copilot.ffi} are internal; consumers must
21+
* not reference them directly.
22+
*
23+
* <h2>C ABI entry points</h2>
24+
* <ul>
25+
* <li>{@code copilot_runtime_host_start} — start the runtime host</li>
26+
* <li>{@code copilot_runtime_host_shutdown} — shut down the runtime host</li>
27+
* <li>{@code copilot_runtime_connection_open} — open a bidirectional
28+
* connection</li>
29+
* <li>{@code copilot_runtime_connection_write} — write a JSON-RPC frame to the
30+
* runtime</li>
31+
* <li>{@code copilot_runtime_connection_close} — close a connection</li>
32+
* </ul>
33+
*
34+
* <h2>Wire format</h2>
35+
* <p>
36+
* All frames use LSP {@code Content-Length} header framing, identical to the
37+
* stdio transport. No special encoding or decoding is needed at the FFI
38+
* boundary.
39+
*/
40+
interface NativeBinding {
41+
42+
/**
43+
* Starts the runtime host.
44+
*
45+
* <p>
46+
* Blocks for up to ~30 s while the worker boots and connects back. Must not be
47+
* called on an async/reactive executor thread.
48+
*
49+
* @param argvJson
50+
* UTF-8 JSON array of strings: the entrypoint and required flags
51+
* @param argvJsonLen
52+
* byte length of {@code argvJson}
53+
* @param envJson
54+
* UTF-8 JSON object of environment overrides, or {@code null} when
55+
* empty
56+
* @param envJsonLen
57+
* byte length of {@code envJson}, or {@code 0} when {@code envJson}
58+
* is null
59+
* @return server handle ({@code 0} on failure)
60+
*/
61+
int hostStart(byte[] argvJson, int argvJsonLen, byte[] envJson, int envJsonLen);
62+
63+
/**
64+
* Shuts down the runtime host.
65+
*
66+
* @param serverId
67+
* non-zero server handle returned by {@link #hostStart}
68+
* @return {@code true} on success
69+
*/
70+
boolean hostShutdown(int serverId);
71+
72+
/**
73+
* Opens a bidirectional connection and registers the outbound data callback.
74+
*
75+
* <p>
76+
* The {@code extSource}, {@code extName}, and {@code connToken} parameters are
77+
* reserved extension points. All current SDK implementations pass
78+
* {@code null}/0 for all three.
79+
*
80+
* @param serverId
81+
* non-zero server handle returned by {@link #hostStart}
82+
* @param callback
83+
* JNA callback invoked by the runtime on native threads when
84+
* outbound data is available; must be held as a strong reference by
85+
* the caller
86+
* @param userData
87+
* opaque cookie passed back to {@code callback} unchanged; pass
88+
* {@link Pointer#NULL}
89+
* @param extSource
90+
* reserved; pass {@code null}
91+
* @param extSourceLen
92+
* byte length of {@code extSource}; pass {@code 0}
93+
* @param extName
94+
* reserved; pass {@code null}
95+
* @param extNameLen
96+
* byte length of {@code extName}; pass {@code 0}
97+
* @param connToken
98+
* reserved; pass {@code null}
99+
* @param connTokenLen
100+
* byte length of {@code connToken}; pass {@code 0}
101+
* @return connection handle ({@code 0} on failure)
102+
*/
103+
int connectionOpen(int serverId, OutboundCallback callback, Pointer userData, byte[] extSource, int extSourceLen,
104+
byte[] extName, int extNameLen, byte[] connToken, int connTokenLen);
105+
106+
/**
107+
* Writes a JSON-RPC frame to the runtime.
108+
*
109+
* <p>
110+
* The native side copies the buffer synchronously before returning; the byte
111+
* array does not need to survive past this call.
112+
*
113+
* @param connectionId
114+
* non-zero connection handle returned by {@link #connectionOpen}
115+
* @param data
116+
* frame bytes
117+
* @param dataLen
118+
* byte length of {@code data}
119+
* @return {@code true} on success
120+
*/
121+
boolean connectionWrite(int connectionId, byte[] data, int dataLen);
122+
123+
/**
124+
* Closes a connection.
125+
*
126+
* @param connectionId
127+
* non-zero connection handle returned by {@link #connectionOpen}
128+
* @return {@code true} on success
129+
*/
130+
boolean connectionClose(int connectionId);
131+
}

0 commit comments

Comments
 (0)