Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
fe32ef1
docs(java): add ADR-007 native runtime bundling strategy
edburns Jul 6, 2026
f68e644
Potential fix for pull request finding
edburns Jul 6, 2026
ca94698
Update adr-007 to address decision to not use Panama
edburns Jul 10, 2026
3dcb39e
Select Option 2 and Option 1
edburns Jul 10, 2026
6842f37
GUTDODP
edburns Jul 21, 2026
6bc9755
Add ignorance reduction plan for Java SDK native runtime embedding (#…
edburns Jul 22, 2026
b858c23
GUTDODP
edburns Jul 22, 2026
3ed3039
Get answers to pre-traversal questions in section 3.15.
edburns Jul 22, 2026
2e31d7a
GUTDODP
edburns Jul 22, 2026
34d5377
Phase 3, 3.1 WIP
edburns Jul 22, 2026
700cc69
Rethink answer for 3.1.
edburns Jul 22, 2026
6411654
Working through 3.1 and 3.2
edburns Jul 22, 2026
32262bb
Resolve 3.1 and 3.2: hybrid reactor structure + npm pack binary acqui…
edburns Jul 22, 2026
43d7a28
GUTDODP
edburns Jul 22, 2026
30f4eea
Expand step 4.6 into 4.6a/b/c for hybrid reactor restructure
edburns Jul 22, 2026
b3d4280
3.3 resolution: interface + direct instantiation + com.github.copilot…
edburns Jul 22, 2026
db99bac
Spike 3.4: JNA callback threading with Rust DLL
edburns Jul 22, 2026
1c92be5
3.4 resolution: QueueInputStream + MR-JAR virtual thread reader
edburns Jul 22, 2026
6cab27d
GUTDODP
edburns Jul 22, 2026
0f04d23
3.5.1 resolution: RuntimeConnection sealed class hierarchy
edburns Jul 22, 2026
afa71ad
3.5.2 resolution: FfiRuntimeHost parallel to CliServerManager
edburns Jul 22, 2026
2668851
3.5.3 resolution: JsonRpcClient.fromStreams() + QueueInputStream + Ff…
edburns Jul 22, 2026
7923c9c
GUTDODP
edburns Jul 22, 2026
80caeca
GUTDODP
edburns Jul 22, 2026
fc601e8
GUTDODP
edburns Jul 23, 2026
ecaea39
Spike 3.6 darwin-arm64
edburns Jul 23, 2026
0663e7b
Make spikes into maven projects
edburns Jul 23, 2026
380c241
3.6 Windows x64
edburns Jul 23, 2026
6f18b47
Document 3.6 platform detection resolution
edburns Jul 23, 2026
cfd7287
GUTDODP
edburns Jul 23, 2026
25ebcb3
Resolve native runtime extraction caching strategy
edburns Jul 24, 2026
02e202e
Document JNA GraalVM callback spike
edburns Jul 24, 2026
e290dbd
GUTDODP
edburns Jul 24, 2026
e2dba8a
Expand Question 3.9 to cover all five C ABI parameters
edburns Jul 24, 2026
126f094
Correct ABI table: argv_json example, host_start blocking, nullable p…
edburns Jul 24, 2026
0d01512
3.9: C ABI parameter semantics spike and resolution
edburns Jul 24, 2026
1a81e2b
3.10: Error handling and diagnostics resolution
edburns Jul 24, 2026
03175d9
GUTDODP
edburns Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
# DAY: 2026-07-10

## ghcp-sdk-1965-spike-how-hard-is-option-1-and-option-2-both


### 15:42 Prompt

Consider this response regarding the Panama vs. JNA/FFM question:

> The JNA calling for Java, is this not a case for FFI via Panama (assuming a Java 25 LTS base line).

I am leaning to **not** go with Panama. But how can I respond to this commenter?

### 15:53 Response

#### Full response

Great question — Panama FFM was seriously considered, and it's likely the eventual destination. For this iteration we're staying with JNA. Reasoning:

1. **Baseline.** The SDK supports Java 17, and FFM isn't available there (it finalized in 22 via JEP 454). A JNA path would be needed regardless, so FFM would be an *additional* binding implementation, not a replacement.

2. **Consumer friction.** FFM's restricted native access means every consumer would need `--enable-native-access=...` (or the manifest attribute) on their launcher as JEP 472 enforcement tightens. JNA works with zero configuration today. For an SDK, that flag becomes every downstream app's problem and a recurring support issue. (JNA sits on the same enforcement trajectory eventually, since it uses JNI internally — so this buys us time, not immunity.)

3. **The performance win doesn't materialize here.** FFM's advantage over JNA is per-call marshalling overhead. Our C ABI surface is ~12 stable `extern "C"` entry points carrying JSON-RPC strings; serialization cost dominates, and call frequency is bounded by agent-interaction rates, not tight loops. The latency delta would be unmeasurable end-to-end.

4. **Upcall complexity.** The transport is bidirectional, so the runtime calls back into Java from native threads. JNA's `Callback` semantics are well-worn; FFM upcall stubs require explicit `Arena` lifetime management, where a stub outlived by a Rust-held function pointer is a JVM crash.

5. **GraalVM native-image.** JNA's reachability metadata story is mature; FFM support (especially upcalls) is still maturing and varies by GraalVM release. Some plausible SDK consumers (Quarkus/Micronaut CLI tools) compile to native images.

That said, we're structuring the binding layer behind a small internal interface so an FFM implementation can be added later (e.g., as a multi-release JAR) without touching the transport logic. When the baseline moves past 17 and the JEP 472 endgame arrives, a ~12-function stable ABI makes that migration cheap.

#### Compact response

Good instinct — FFM is probably where this lands eventually, but not in this iteration, for three reasons:

- We support Java 17, where FFM doesn't exist, so JNA is required anyway; FFM would be a second parallel implementation.
- FFM pushes `--enable-native-access` onto every consumer's launcher as JEP 472 tightens; JNA is zero-config for downstream users today.
- The C ABI surface is ~12 fixed entry points carrying JSON-RPC strings, so JSON serialization dominates and FFM's call-overhead advantage is unmeasurable here. Bidirectional callbacks also add FFM `Arena`/upcall-lifetime complexity that JNA's `Callback` handles for free.

We're abstracting the binding layer behind an internal interface, so swapping in FFM later (once the baseline and JEP 472 timeline make it the clear win) is a contained change rather than a rewrite. Happy to expand any of this into the ADR if useful.

#### Following

Two drafts above — a full five-point rationale if you want the reasoning on the record (useful if this thread effectively becomes ADR documentation), and a tighter three-point version if the comment thread just needs closure.

Both open by validating the suggestion rather than rebutting it, because the commenter isn't wrong — FFM *is* the right long-term answer, just not the right answer for a 17-baseline SDK with a JSON-dominated transport today. Framing it as "eventual destination, deliberately deferred" also preempts the follow-up question ("so when?") by pointing at the concrete triggers: baseline moving past 17 and JEP 472 enforcement arriving.

One thing worth deciding before you post: whether to commit publicly to the binding-layer abstraction. Both drafts mention it, and it's a cheap promise that makes the "not now" more credible — but if it's not actually in ADR-007 yet, you may want to add it there first so the comment points at something real.

### 16:04 Prompt

Read `copilot-sdk/java/docs/adr/adr-007-native-bundling-strategy.md`.

I want to explore the ability to select Option 2, but use the `maven-assembly-plugin` to create an uber jar that contains the Java artifact and a select set of the native jars:

```
com.github:copilot-sdk-java-runtime:VERSION:linux-x64
com.github:copilot-sdk-java-runtime:VERSION:linux-arm64
com.github:copilot-sdk-java-runtime:VERSION:linuxmusl-x64
com.github:copilot-sdk-java-runtime:VERSION:linuxmusl-arm64
com.github:copilot-sdk-java-runtime:VERSION:darwin-x64
com.github:copilot-sdk-java-runtime:VERSION:darwin-arm64
com.github:copilot-sdk-java-runtime:VERSION:win32-x64
com.github:copilot-sdk-java-runtime:VERSION:win32-arm64
```

If we have such an uber jar, how can the java artifact select the correct native jar content for the JNA interaction?

### 16:09 Response

Updated todo list

Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-007-native-bundling-strategy.md#1-1), lines 1 to 200

Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-007-native-bundling-strategy.md#200-200), lines 200 to 229

Now I have the full picture. Let me look at what native loading infrastructure exists in the Java SDK currently.

Searched for files matching `**/java/src/**/*Native*`, no matches

Searched for files matching `**/java/src/**/*Platform*`, 2 matches

Searched for files matching `**/java/src/**/*Runtime*`, 1 match

Searched for regex `JNA|jna|native.*load|NativeLoader` (`**/java/src/**`), 2 results

No native loading code exists yet in the Java SDK — this is greenfield. Here's how the uber-jar approach works with Option 2's per-platform classifier JARs:

---

## How it works: classpath resource convention + platform detection

### 1. Each classifier JAR uses a well-known resource path

Each per-platform JAR (`copilot-sdk-java-runtime:VERSION:darwin-arm64`, etc.) places its binary under a deterministic path inside the JAR:

```
native/darwin-arm64/runtime.node
native/darwin-arm64/platform.properties
```

When `maven-assembly-plugin` creates the uber-jar, it unpacks all dependencies and merges them. The resulting uber-jar contains:

```
com/github/copilot/sdk/... (Java classes)
native/linux-x64/runtime.node
native/linux-arm64/runtime.node
native/linuxmusl-x64/runtime.node
native/linuxmusl-arm64/runtime.node
native/darwin-x64/runtime.node
native/darwin-arm64/runtime.node
native/win32-x64/runtime.node
native/win32-arm64/runtime.node
```

### 2. The coordination artifact selects at runtime via `getResourceAsStream`

```java
public class NativeRuntimeLoader {

public Path loadRuntime() {
String classifier = detectPlatformClassifier();
String resourcePath = "native/" + classifier + "/runtime.node";

try (InputStream in = getClass().getClassLoader()
.getResourceAsStream(resourcePath)) {
if (in == null) {
throw new UnsupportedOperationException(
"No native runtime for platform: " + classifier);
}
Path cached = getCachePath(classifier);
if (!Files.exists(cached)) {
Files.createDirectories(cached.getParent());
Files.copy(in, cached);
// Make executable on Unix
cached.toFile().setExecutable(true);
}
return cached;
}
}

private String detectPlatformClassifier() {
String os = normalizeOs(System.getProperty("os.name"));
String arch = normalizeArch(System.getProperty("os.arch"));
String libc = "linux".equals(os) ? detectLinuxLibc() : "";

// Produces: "linux-x64", "linuxmusl-arm64", "darwin-arm64", "win32-x64", etc.
return (libc.isEmpty() ? os : os + libc) + "-" + arch;
}

private String detectLinuxLibc() {
// Read ELF PT_INTERP from /proc/self/exe
// If interpreter contains "/ld-musl-" → "musl"
// Otherwise → "" (glibc is the default/unmarked case for "linux-")
// ...
}

private Path getCachePath(String classifier) {
String version = getClass().getPackage().getImplementationVersion();
return Path.of(System.getProperty("user.home"),
".copilot", "runtime-cache", version, classifier, "runtime.node");
}
}
```

### 3. JNA loads from the extracted path

Once extracted to a known filesystem path, JNA loads it directly:

```java
NativeLibrary lib = NativeLibrary.getInstance(extractedPath.toString());
// Or via a mapped interface:
CopilotRuntime runtime = Native.load(extractedPath.toString(), CopilotRuntime.class);
```

### Key insight: the same code works in both modes

The beauty is that `getResourceAsStream("native/darwin-arm64/runtime.node")` works identically whether:

- The native lives in a **separate classifier JAR** on the classpath (normal dev dependency), OR
- It's been **merged into an uber-jar** by `maven-assembly-plugin`

The classloader doesn't care which JAR file the resource came from — it searches the entire classpath. This means **zero code changes** between the two consumption models.

---

## Assembly plugin configuration (consumer-side)

A consumer building a portable uber-jar would configure:

```xml
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
```

With all classifier JARs declared as dependencies:

```xml
<dependencies>
<dependency>
<groupId>com.github</groupId>
<artifactId>copilot-sdk-java</artifactId>
<version>${copilot.version}</version>
</dependency>
<!-- Include platforms you need -->
<dependency>
<groupId>com.github</groupId>
<artifactId>copilot-sdk-java-runtime</artifactId>
<version>${copilot.version}</version>
<classifier>linux-x64</classifier>
</dependency>
<dependency>
<groupId>com.github</groupId>
<artifactId>copilot-sdk-java-runtime</artifactId>
<version>${copilot.version}</version>
<classifier>darwin-arm64</classifier>
</dependency>
<!-- ... etc for each target platform -->
</dependencies>
```

---

## Why this works cleanly

| Concern | How it's handled |
|---------|-----------------|
| No resource path collisions | Each platform has its own subdirectory (`native/<classifier>/`) |
| Extraction only happens once | Cached to `~/.copilot/runtime-cache/<version>/<classifier>/` |
| Works without uber-jar too | Same `getResourceAsStream` call — classloader finds it in the separate JAR |
| Subset selection | Consumer declares only the classifiers they need; missing platforms get a clear error at runtime |
| JNA loading | `NativeLibrary.getInstance(path)` loads from an absolute filesystem path after extraction — no JNA platform-detection magic needed |

The pattern is identical to how DJL's `LibUtils.loadLibrary()` works — detect platform, construct resource path, extract if needed, load via absolute path.
Loading
Loading