You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
JNI global state and JNI_OnLoad assume a single copy of the runtime per process — statically-linked-per-module embedders (Skip Fuse) end up with N copies and a nil JNI.jni #854
Context issue, cross-referencing skiptools/skip#716 (full evidence there: symbol counts showing SwiftJNI statically linked into seven .so files with three competing JNI_OnLoad exports, disproven System.loadLibrary workaround, crash signature).
When an embedder statically links the jni-core runtime into more than one .so in the same process — which is what Skip Fuse's per-module linking does today — JNI.jni exists once per object, JNI_OnLoad initialises only the copy in the library the JVM loaded, and the first use from any other copy fatals at SwiftJNI.swift:1390 (String.fromJavaObject).
Probably a build/packaging question more than a bug in swift-java itself, but worth deciding where the invariant lives: should swift-java (a) document "exactly one copy per process" as a hard requirement, (b) expose an explicit re-initialisation / state-sharing hook for multi-.so embedders, or (c) ship the runtime as a dynamically-linkable core so embedders can share it? Happy to test a direction against our reproduction.
I followed up at skiptools/skip#716 (comment). Since SwiftJava is .dynamic as of @madsodgaard's #493, I don't think that the theory of the same symbols being duplicated between libraries is correct. Rather, I think the confusion arises from that fact that Skip still has its own swift-jniJNI type parallel with swift-java/swift-java-jni-core's JavaVirtualMachine.
Closing the loop here after the full post-mortem on the Skip side (see the final comment on skiptools/skip#716): the premise of this issue as filed was wrong — @marcprux was right that the state-duplication theory didn't hold for the crash — while @ktoso's "these just should not be linked statically multiple times" turns out to be exactly the part that survives: the multiple static linkage is real and confirmed, it just wasn't the crash mechanism.
The crash was not N copies of jni-core state with an uninitialised JNI.jni — Skip's global exists exactly once in the process. The real mechanism was JNI_OnLoad shadowing between two coexisting JNI shims: ART invokes exactly one JNI_OnLoad per System.loadLibrary (resolved over the library and its dependency tree), the app graph carried three definitions (Skip's own shim's, plus swift-java's in libSwiftJava.so and a statically-embedded copy), and the one initialiser that mattered for the crash site always lost the resolution. One explicit System.loadLibrary of the right library fixes it, verified — with swift-okhttp then working end-to-end under Skip Fuse.
What remains genuinely relevant to swift-java from this thread, now confirmed with clean methodology (llvm-nm --defined-only, demangled): under the embedder's build, the .dynamicSwiftJava product was additionally statically embedded into a consumer .so — 6631 defined SwiftJava* symbols including its own T JNI_OnLoad, and duplicated state (static SwiftJava.JNI.shared, JavaVirtualMachine.sharedJVM) alongside the dynamic copy. This turned out to be benign once each copy's JNI_OnLoad actually runs — the process worked with both copies initialised — but it doubles runtime state and code, and it is what put the extra JNI_OnLoad into the resolution path in the first place.
So the (a)/(b)/(c) question in the description narrows considerably. If anything, the useful outcome here is probably just documentation: "one initialised copy per process is required, and embedders that end up with multiple copies must guarantee each copy's JNI_OnLoad runs" — plus whatever you think of the embedding itself, which is arguably the embedder's build concern rather than swift-java's. Happy for this to be closed or repurposed as that docs note.
Activity
ktoso commentedon Jul 30, 2026
@marcprux from the Skip side should likely chime in, these just should not be linked statically multiple times is the real answer I fear tbh?
marcprux commentedon Jul 31, 2026
I followed up at skiptools/skip#716 (comment). Since SwiftJava is
.dynamicas of @madsodgaard's #493, I don't think that the theory of the same symbols being duplicated between libraries is correct. Rather, I think the confusion arises from that fact that Skip still has its ownswift-jniJNItype parallel withswift-java/swift-java-jni-core'sJavaVirtualMachine.luisquin-noma commentedon Jul 31, 2026
Closing the loop here after the full post-mortem on the Skip side (see the final comment on skiptools/skip#716): the premise of this issue as filed was wrong — @marcprux was right that the state-duplication theory didn't hold for the crash — while @ktoso's "these just should not be linked statically multiple times" turns out to be exactly the part that survives: the multiple static linkage is real and confirmed, it just wasn't the crash mechanism.
The crash was not N copies of jni-core state with an uninitialised
JNI.jni— Skip's global exists exactly once in the process. The real mechanism wasJNI_OnLoadshadowing between two coexisting JNI shims: ART invokes exactly oneJNI_OnLoadperSystem.loadLibrary(resolved over the library and its dependency tree), the app graph carried three definitions (Skip's own shim's, plus swift-java's inlibSwiftJava.soand a statically-embedded copy), and the one initialiser that mattered for the crash site always lost the resolution. One explicitSystem.loadLibraryof the right library fixes it, verified — with swift-okhttp then working end-to-end under Skip Fuse.What remains genuinely relevant to swift-java from this thread, now confirmed with clean methodology (
llvm-nm --defined-only, demangled): under the embedder's build, the.dynamicSwiftJavaproduct was additionally statically embedded into a consumer.so— 6631 definedSwiftJava*symbols including its ownT JNI_OnLoad, and duplicated state (static SwiftJava.JNI.shared,JavaVirtualMachine.sharedJVM) alongside the dynamic copy. This turned out to be benign once each copy'sJNI_OnLoadactually runs — the process worked with both copies initialised — but it doubles runtime state and code, and it is what put the extraJNI_OnLoadinto the resolution path in the first place.So the (a)/(b)/(c) question in the description narrows considerably. If anything, the useful outcome here is probably just documentation: "one initialised copy per process is required, and embedders that end up with multiple copies must guarantee each copy's
JNI_OnLoadruns" — plus whatever you think of the embedding itself, which is arguably the embedder's build concern rather than swift-java's. Happy for this to be closed or repurposed as that docs note.