Skip Fuse duplicates swift-java's JNI runtime across module .so files, leaving its global handle nil at runtime
Nobody has claimed this yet.
- Dominant language
- Swift
- Stars
- 3.2k
- Forks
- 106
- Avg merge
- 6d 13h
- Merged PRs (30d)
- 1
Description
Environment
- Skip CLI 1.9.5 (Homebrew), skip-fuse 1.0.2, skip-bridge (0.x)
- Swift 6.3.3, Swift Android SDK
swift-6.3.3-RELEASE_android, NDK r27d - Xcode 26.5, macOS 15 / arm64
- Native/fused mode app, multi-module package
- Third-party JNI dependency:
frameo-net/swift-okhttp0.1.1, which is built onswiftlang/swift-java0.4.2 /swift-java-jni-core0.5.1
Symptom
A Skip Fuse app that calls into any swift-java-generated wrapper crashes on the first bridged call:
F SwiftRuntime: SwiftJNI/SwiftJNI.swift:1390: Fatal error: Unexpectedly found nil
while implicitly unwrapping an Optional value
F libc : Fatal signal 5 (SIGTRAP), code 1 (TRAP_BRKPT)
Line 1390 is JNI.jni.withEnv { ... } inside String.fromJavaObject — swift-java's
global JNI handle is nil.
The app itself builds, links and launches correctly; the Gradle app build honours the
module's skip.yml dependency block, so the Java classes the wrapper needs are on the
classpath (an earlier ClassNotFoundException disappeared once that was declared).
It is not a missing JNI_OnLoad
swift-java initialises that handle in JNI_OnLoad, which the JVM invokes only for
libraries it loads itself. The obvious theory is that Skip's loader bypasses it, so we
forced the load from Kotlin in AndroidAppMain.onCreate():
for (lib in listOf("SwiftJava", "SwiftJNI", "FormsAPI")) {
try { System.loadLibrary(lib); logger.info("ok: $lib") }
catch (e: Throwable) { logger.info("failed: $lib -> $e") }
}
All three load successfully — so JNI_OnLoad does run — and the crash is
unchanged.
Analysis: the runtime is duplicated across module .sos
llvm-nm over the merged native libs shows SwiftJNI statically linked into seven
separate shared objects:
libSkipUI.so 4859 symbols
libSwiftJNI.so 1766
libSkipBridge.so 705
libSkipAndroidBridge.so 385
libFormsAPI.so 125
libFormsDemo.so 72
libSkipFuseUI.so 24
and three of them export their own JNI_OnLoad:
libFormsAPI.so: T JNI_OnLoad (alongside $s9SwiftJava0aB11_JNI_OnLoad...)
libSkipBridge.so: T JNI_OnLoad
libSwiftJava.so: T JNI_OnLoad
So each .so carries its own copy of SwiftJNI's global state. The JVM initialises
whichever copy belongs to the library it loaded; the copy that libFormsAPI.so's code
actually reads is a different instance and stays nil. No single initialisation — forced or
otherwise — can cover them all.
This looks like the runtime counterpart of #714: there, static-library duplication across
Skip's dynamic bridge products became a hard error under Swift 6.4's SwiftPM; here the same
duplication is silent at build time and fatal at runtime, because the duplicated library
happens to own process-global state.
Impact
Any Skip Fuse module that depends on a swift-java-based package is affected, since
swift-java is the standard way to wrap a Java library for Swift on Android. In our case
this blocks using swift-okhttp as an HTTP transport for a Swift OpenAPI client — the
generated client and its DTOs work correctly under Fuse (including polymorphic oneOf
types bridged to Kotlin), and only the transport fails.
Suggested direction
Link swift-jni / swift-java once and share it — e.g. force it dynamic (the same
SKIP_BRIDGE product gate skip-lib already uses) so a single copy of its globals exists
per process, rather than statically embedding it into every module .so.
More generally: any third-party Swift package with process-global state that is initialised
from JNI_OnLoad will break the same way under the current per-module linking, so a
documented rule about what Skip Fuse can and cannot statically duplicate would help.
Reproducing
Minimal shape:
skip init --native-appproject.- A module depending on
.product(name: "OpenAPIOkHttp", package: "swift-okhttp", condition: .when(platforms: [.android])). - Declare
implementation("com.squareup.okhttp3:okhttp:5.3.2")in that module'sskip.yml. - Call
OkHttpClient()from Swift on launch.
Happy to share a reproducer repo or run further diagnostics if useful.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the existing SKIP_BRIDGE/skip-lib handling and the module skip.yml dependency block. Reproduce the OpenAPIOkHttp setup, inspect the merged .so files with llvm-nm, and compare behavior after AndroidAppMain.onCreate() loads the libraries. Done means the reproducer’s OkHttpClient call works with one shared swift-java/swift-jni runtime rather than a nil JNI handle.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin, swift
- Domain
- build-system, mobile-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100