Skip to content

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

Open

Description

@luisquin-noma

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.

Activity

  1. ktoso commented on Jul 30, 2026

    @ktoso
    Collaborator

    @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?

  2. marcprux commented on Jul 31, 2026

    @marcprux
    Contributor

    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-jni JNI type parallel with swift-java/swift-java-jni-core's JavaVirtualMachine.

  3. luisquin-noma commented on Jul 31, 2026

    @luisquin-noma
    Author

    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 .dynamic SwiftJava 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions