Don't JNI-bridge generic @inline(__always) functions - #260
Merged
marcprux merged 2 commits intoJul 24, 2026
Conversation
A generic `@inline(__always)` function is emitted as a Kotlin
`inline fun <reified T>`, which has no JVM-callable method. The bridge
generator nonetheless emitted a `getMethodID(...)!` JNI lookup for it,
so the force-unwrap trapped at class load ("Unexpectedly found nil while
unwrapping an Optional value") for any native-Swift-on-Android caller.
This reproduces whenever the reified method lives on an `open` class that
has a subclass (the method is then lifted to a top-level `inline fun
<reified T>` extension and bridged via `getStaticMethodID`) — the root
cause behind skiptools/skip-firebase#81 and the still-open skiptools#91.
Return nil from `KotlinFunctionDeclaration.checkBridgable` for a generic
`@inline(__always)` function so it is not JNI-bridged. Native-Swift
callers use the `@inline(__always)` Swift body directly (inlined); Kotlin
callers use the `inline fun <reified T>`. An inline reified function was
never JNI-callable to begin with, so nothing is lost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `// SKIP DECLARE: ... inline fun <reified T> ...` override produces the same JVM-uncallable Kotlin as @inline(__always) but carries no structured attribute, so the attribute guard alone left the crash reachable via skip-firebase's FirestoreDecoder.decode(from:) (called from the inlined body of DocumentSnapshot.decoded()) — i.e. skiptools#91 was not fully closed. Also bail out of bridging when the declaration override is an inline reified function. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
A generic
@inline(__always)function on a bridged type is emitted as a Kotlininline fun <reified T>, which has no JVM-callable method. The bridge generator nonetheless emits agetMethodID(...)!/getStaticMethodID(...)!JNI lookup for it, so the force-unwrap traps at class load —Fatal error: Unexpectedly found nil while unwrapping an Optional value— for any native-Swift-on-Android caller (a#if !SKIP_BRIDGEapp type calling through the real JNI edge;swift test's JVM-native path never trips it because it inlines the body).It reproduces whenever the reified method lives on an
openclass that has a subclass: the method is then lifted to a top-levelinline fun <reified T>extension and bridged viagetStaticMethodID. This is the root cause behind skiptools/skip-firebase#81 (originally worked around by switching a manual// SKIP DECLARE: inline fun <reified T>to@inline(__always)) and the still-open skiptools/skip-firebase#91, whereDocumentSnapshot.decoded()(it has subclassQueryDocumentSnapshot) crashes again while the subclass-freeFirestoreDecoderkeeps working.Fix
In
KotlinFunctionDeclaration.checkBridgable(...) -> FunctionBridgable?, returnnilfor a generic@inline(__always)function so it is not JNI-bridged. Native-Swift callers use the@inline(__always)Swift body directly (inlined); Kotlin callers use theinline fun <reified T>. An inline reified function was never JNI-callable to begin with, so nothing is lost — the change only removes a lookup that could only ever trap.Also covers the manual
// SKIP DECLARE:path (second commit)A hand-written
// SKIP DECLARE: ... inline fun <reified T> ...produces the same JVM-uncallable Kotlin but carries no structured attribute — so the attribute guard alone does not fully close #91.DocumentSnapshot.decoded()(now un-bridged by the attribute guard) inlines a call toFirestoreDecoder.decode(from:), which uses exactly this override and is still emitting a trappinggetMethodID(name: "decode", ...)!in a real build artifact — the crash reproduces one frame deeper. The second commit therefore also bails out of bridging when the declaration override is itself an inline reified function. (Currently a textual match on the override — happy to switch to a more structured signal if you'd prefer.)Testing
BridgeInlineReifiedTestshas two cases, both asserting the generated bridge contains the Kotlininline fun <reified T>but no methodID lookup for the method:testReifiedInlineMethodIsNotBridged(the@inline(__always)path, Allow use of #warning and #error in Xcode #91 shape on anopenclass with a subclass) andtestSkipDeclareReifiedMethodIsNotBridged(the// SKIP DECLARE:path, skip-firebase'sFirestoreDecoder.decode(from:)shape).swift testsuite green (922 tests, 0 failures).Refs skiptools/skip-firebase#81, skiptools/skip-firebase#91