Skip to content

loopless/skip-firebase-encodable-write-repro

Repository files navigation

SkipFirebase: generic Encodable Firestore write methods crash on Android (nil JNI method-ID)

Minimal Skip Fuse app reproducing a crash in skip-firebase on Android.

Summary

The generic Encodable Firestore write methods crash the process on Android the moment they are called:

  • CollectionReference.addDocument(from:)
  • DocumentReference.setData(from:) / setData(from:mergeFields:)
  • WriteBatch.setData(from:forDocument:) / setData(from:forDocument:mergeFields:)

The abort is:

SkipBridge/BridgedTypes.swift:198: Fatal error: Unable to bridge Swift instance
ReproItem(id: ..., title: "batch", amount: 42.0, done: false) of type: ReproItem;
this is usually due to missing // SKIP @bridge or // SKIP @bridgeMembers on the Swift type

i.e. when one of these generic methods is called from native Swift, the raw generic value (T) is marshalled across the JNI bridge (SwiftJNI.jniContextSkipBridge.AnyBridging.toJavaObject) instead of being encoded to [String: Any] on the native side first. A plain Codable model is not a @bridge type, so toJavaObject hits _assertionFailure and the process dies with SIGTRAP.

The equivalent primitive [String: Any] write methods work fine on both platforms — that is the control case in this repro, and the workaround currently in use downstream.

The Decodable read counterpart, DocumentSnapshot.decoded(), had a related Android bridging problem (#81) that was fixed in #82 by marking it @inline(__always) (skipstone then emits a Kotlin inline fun <reified T> and the generic value never crosses the bridge). The generic Encodable write methods never received that treatment.

iOS is unaffected (native Firebase); the crash is Android-only.

Environment

  • skip-firebase: main (9a3f608 at time of filing) — i.e. the tree that already contains the #82 decoded() fix
  • Skip: 1.9.2
  • skip-fuse-ui: 1.0.0
  • App mode: Skip Fuse (native Swift compiled for Android), .swiftLanguageMode(.v5)
  • Firebase iOS 12.14.0 / Firebase Android 34.x (whatever the skip-firebase modules pull in)

No credentials required

This repro configures Firebase entirely in code with throwaway, well-formed FirebaseOptions (Sources/Repro/ReproApp.swift) — there is no GoogleService-Info.plist and no google-services.json, and the com.google.gms.google-services Gradle plugin is not applied. The crash happens in the bridge during local encoding, before any network round-trip, so dummy options are enough to reproduce it. The WriteBatch.setData(from:forDocument:) case is synchronous and is never committed, so it touches the network not at all.

Running

An Android emulator must be running. Then:

skip launch

(or open Project.xcworkspace in Xcode and run the "Repro App" target, which deploys to the running emulator). Watch logs with adb logcat.

Tap each row in the app:

Case iOS Android
WriteBatch.setData(from:forDocument:) OK CRASH
DocumentReference.setData(from:) OK CRASH
CollectionReference.addDocument(from:) OK CRASH
WriteBatch.setData([String: Any]:) (control) OK OK

Expected

All four cases complete without crashing (as they do on iOS).

Actual (Android)

The generic-Encodable cases abort the process. Verified on an Android emulator (API 36, arm64) against skip-firebase main (9a3f608) by tapping WriteBatch.setData(from:forDocument:). The primitive-dictionary control case keeps the app running. Full trace in CRASH_LOG.txt; top frames:

F SwiftRuntime: SkipBridge/BridgedTypes.swift:198: Fatal error: Unable to bridge Swift
    instance ReproItem(...) of type: ReproItem; this is usually due to missing
    // SKIP @bridge or // SKIP @bridgeMembers on the Swift type
F libc    : Fatal signal 5 (SIGTRAP), code 1 (TRAP_BRKPT) ... >>> com.example.firebasedecodedrepro
  #00 libswiftCore.so  _assertionFailure(...)
  #01 SkipBridge.AnyBridging.toJavaObject(_:options:)
  #02 SkipFirebaseFirestore.WriteBatch.setData(from:forDocument:) {closure}
  #04 SwiftJNI.jniContext(...)
  #05 SkipFirebaseFirestore.WriteBatch.setData(from:forDocument:)
  #06 Repro.ReproStore.runBatchEncodable()

The WriteBatch case is synchronous and never committed, so this is purely the encode/bridge call — no network, no real project.

Root cause / suggested fix

The generic write methods in SkipFirebaseFirestore.swift are plain generic functions that encode the value and forward to the [String: Any] overload:

public func setData<T: Encodable>(from value: T, forDocument document: DocumentReference) throws -> WriteBatch {
    let data = try FirestoreEncoder().encode(value)
    return setData(data, forDocument: document)
}

On Android, calling this from native Swift marshals the generic value itself across the JNI boundary (frames #02/#04/#05 above) before the body runs, and AnyBridging.toJavaObject cannot bridge an arbitrary Codable Swift instance — so it asserts. The intended FirestoreEncoder().encode(value)[String: Any] conversion never gets the chance to run on the native side.

decoded() had a comparable Android bridging problem and was fixed in #82 by marking it @inline(__always):

@inline(__always)
public func decoded<T: Decodable>() throws -> T { ... }

Applying the same @inline(__always) treatment to the generic Encodable write methods (addDocument(from:), setData(from:...), and the WriteBatch setData(from:...) variants) should inline them at the call site so the value is encoded to [String: Any] in native Swift and only the (bridgeable) dictionary crosses into the bridged [String: Any] overload — matching the #82 approach.

Project layout

  • Sources/Repro/ReproApp.swift — dummy-options Firebase configuration
  • Sources/Repro/ViewModel.swiftReproStore, runs the four cases
  • Sources/Repro/ContentView.swift — one tappable row per case with its result

About

Minimal Skip Fuse repro: skip-firebase generic Encodable Firestore writes crash on Android

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors