Cross-compile and package the Rust core for Android with UniFFI Kotlin bindings #92

Closed
opened 2026-08-17 19:10:20 +00:00 by hugo · 2 comments
Owner

Goal

Ship the existing Rust storage implementation as ABI-specific Android native libraries and expose a typed, generated Kotlin API.

Scope

  • Add an Android bridge crate as a thin mechanical UniFFI layer over crates/storage; reuse or extract the platform-neutral parts of crates/apple instead of maintaining two hand-copied interfaces.
  • Build cdylib outputs with the Android NDK for aarch64-linux-android and armv7-linux-androideabi; document how to narrow the release ABI set only through the support decision in #88.
  • Prefer cargo-ndk from Gradle tasks to place each .so in the correct generated jniLibs/<abi> directory. Do not commit build outputs.
  • Generate Kotlin bindings from the built library with the workspace-pinned UniFFI tool, enable UniFFI Android mode and immutable records, and package the required JNA Android artifact.
  • Make Gradle task inputs/outputs explicit so Rust compilation and binding generation are incremental, variant-aware, reproducible, and run before Kotlin compilation.
  • Preserve checksums and fail fast when Kotlin bindings and a native library do not match.
  • Keep bridge calls off the Android main thread, map cancellation and typed errors, and define callback/thread-attachment rules for any Rust-to-Kotlin platform callback.
  • Package stripped release libraries plus native debug symbols separately; verify AAB ABI splits and loading on available ARM emulator devices.
  • Verify every native dependency, including JNA, is compatible with 16 KB page sizes.

Acceptance criteria

  • Debug and release APK/AAB builds load the correct ABI library and call a fixture function through generated Kotlin without handwritten domain JNI.
  • All selected ABIs compile from a clean checkout with the pinned Rust, NDK, UniFFI, and JNA versions.
  • Generated APIs expose view-ready types and redact secrets in Debug/Display/error paths.
  • No Kotlin feature code parses Rust display strings to recover state.
  • Wrong/missing ABI, checksum mismatch, and native load failure produce deterministic non-secret diagnostics.
  • Rust project crates remain free of handwritten unsafe; generated/dependency FFI is isolated at the boundary.

Depends on #88 and the Android Gradle scaffold issue. Part of #87.

Android support update (2026-10-08)

User-directed scope: Android release and CI targets are arm64-v8a and armeabi-v7a. x86_64 Android is excluded for now. Validation for every Android milestone issue uses emulator devices only; physical-device testing and physical-device checklists are excluded. Record the limits of available ARM emulator/API images without claiming physical-device coverage.

## Goal Ship the existing Rust storage implementation as ABI-specific Android native libraries and expose a typed, generated Kotlin API. ## Scope - Add an Android bridge crate as a thin mechanical UniFFI layer over `crates/storage`; reuse or extract the platform-neutral parts of `crates/apple` instead of maintaining two hand-copied interfaces. - Build `cdylib` outputs with the Android NDK for `aarch64-linux-android` and `armv7-linux-androideabi`; document how to narrow the release ABI set only through the support decision in #88. - Prefer `cargo-ndk` from Gradle tasks to place each `.so` in the correct generated `jniLibs/<abi>` directory. Do not commit build outputs. - Generate Kotlin bindings from the built library with the workspace-pinned UniFFI tool, enable UniFFI Android mode and immutable records, and package the required JNA Android artifact. - Make Gradle task inputs/outputs explicit so Rust compilation and binding generation are incremental, variant-aware, reproducible, and run before Kotlin compilation. - Preserve checksums and fail fast when Kotlin bindings and a native library do not match. - Keep bridge calls off the Android main thread, map cancellation and typed errors, and define callback/thread-attachment rules for any Rust-to-Kotlin platform callback. - Package stripped release libraries plus native debug symbols separately; verify AAB ABI splits and loading on available ARM emulator devices. - Verify every native dependency, including JNA, is compatible with 16 KB page sizes. ## Acceptance criteria - Debug and release APK/AAB builds load the correct ABI library and call a fixture function through generated Kotlin without handwritten domain JNI. - All selected ABIs compile from a clean checkout with the pinned Rust, NDK, UniFFI, and JNA versions. - Generated APIs expose view-ready types and redact secrets in Debug/Display/error paths. - No Kotlin feature code parses Rust display strings to recover state. - Wrong/missing ABI, checksum mismatch, and native load failure produce deterministic non-secret diagnostics. - Rust project crates remain free of handwritten `unsafe`; generated/dependency FFI is isolated at the boundary. Depends on #88 and the Android Gradle scaffold issue. Part of #87. ## Android support update (2026-10-08) User-directed scope: Android release and CI targets are **arm64-v8a** and **armeabi-v7a**. x86_64 Android is excluded for now. Validation for every Android milestone issue uses emulator devices only; physical-device testing and physical-device checklists are excluded. Record the limits of available ARM emulator/API images without claiming physical-device coverage.
hugo added this to the 06 - Implement an Android version of the software milestone 2026-08-17 19:10:20 +00:00
hugo added the enhancement label 2026-08-17 19:10:20 +00:00
Author
Owner

Implemented #92 in fcb36a0.

The new Android cdylib reuses the existing mobile UniFFI exports without copied storage rules or handwritten JNI/unsafe Rust. Gradle verifies pinned Rust 1.97.1, cargo-ndk 4.1.2 and NDK 30.0.16248370, builds debug/release for arm64-v8a, armeabi-v7a and x86_64, compares their UniFFI metadata, generates immutable Android-mode Kotlin bindings with UniFFI 0.32.0, and packages JNA 5.19.1 as its Android AAR. A packaging step excludes the redundant Apple cdylib. Release native libraries are stripped; FULL Rust symbols and R8 mappings remain separate.

Both app variants now call the Rust fixture API off the main thread through the core facade. Cancellation and superseded results preserve current state/tab selection. Library/ABI preflight, contract/checksum mismatch and load failures provide typed non-secret recovery states. Generated record, typed exception, panic and unexpected callback diagnostics are redacted. The checksum guard accounts for UniFFI's u16 return width while retaining every expected checksum. Callback attachment/lifetime/thread rules and repeatable build/test instructions are documented in android/README.md.

Validation: all four required Rust gates passed (417 tests). Strict Android dependency verification/locks, debug/release APK and release AAB builds, lint, ktlint, unit tests, configuration-cache reuse, and a fresh-source three-ABI build passed. All packaged native libraries (Rust, JNA and AndroidX) passed ELF/ABI/16 KB checks. Nine ABI split APKs produced by bundletool passed the same audit. Native fixture/error/redaction tests and all 25 fixture/tab UI combinations passed on API 37 arm64 emulators with 4 KB and actual 16 KB pages, with compatibility mode disabled on the 16 KB device. ADB UI control/screenshots independently confirmed native state rendering. Minified release APKs and AAB-delivered splits loaded the Rust fixture on both devices; a private test marker survived APK/split/debug updates. No app uninstall, device reset or credential reset was used.

Final review checked the implementation against the complete issue scope and found/fixed the generated Kotlin message-field collision, unsigned checksum return mapping, redundant dependency library, and unexpected panic/callback diagnostics.

Validation scope: the user clarified that all milestone testing is emulator-only and physical devices are excluded. Android QEMU2 explicitly rejects an x86_64 system image on this aarch64 Mac even with acceleration disabled. x86_64 runtime loading remains untested; its cross-compilation, metadata, ELF and split validation passed. The untested label records only that unavailable emulator check. Arm64 emulator runtime validation passed on 4 KB and 16 KB devices. The clarified testing scope is recorded in the Android architecture, parity matrix and build documentation.

Implemented #92 in [fcb36a0](https://git.rfc1437.de/hugo/IronStorage/commit/fcb36a0684ff1d20ef8f6bb0c3fdb8e1e2b9619c). The new Android cdylib reuses the existing mobile UniFFI exports without copied storage rules or handwritten JNI/unsafe Rust. Gradle verifies pinned Rust 1.97.1, cargo-ndk 4.1.2 and NDK 30.0.16248370, builds debug/release for arm64-v8a, armeabi-v7a and x86_64, compares their UniFFI metadata, generates immutable Android-mode Kotlin bindings with UniFFI 0.32.0, and packages JNA 5.19.1 as its Android AAR. A packaging step excludes the redundant Apple cdylib. Release native libraries are stripped; FULL Rust symbols and R8 mappings remain separate. Both app variants now call the Rust fixture API off the main thread through the core facade. Cancellation and superseded results preserve current state/tab selection. Library/ABI preflight, contract/checksum mismatch and load failures provide typed non-secret recovery states. Generated record, typed exception, panic and unexpected callback diagnostics are redacted. The checksum guard accounts for UniFFI's u16 return width while retaining every expected checksum. Callback attachment/lifetime/thread rules and repeatable build/test instructions are documented in android/README.md. Validation: all four required Rust gates passed (417 tests). Strict Android dependency verification/locks, debug/release APK and release AAB builds, lint, ktlint, unit tests, configuration-cache reuse, and a fresh-source three-ABI build passed. All packaged native libraries (Rust, JNA and AndroidX) passed ELF/ABI/16 KB checks. Nine ABI split APKs produced by bundletool passed the same audit. Native fixture/error/redaction tests and all 25 fixture/tab UI combinations passed on API 37 arm64 emulators with 4 KB and actual 16 KB pages, with compatibility mode disabled on the 16 KB device. ADB UI control/screenshots independently confirmed native state rendering. Minified release APKs and AAB-delivered splits loaded the Rust fixture on both devices; a private test marker survived APK/split/debug updates. No app uninstall, device reset or credential reset was used. Final review checked the implementation against the complete issue scope and found/fixed the generated Kotlin message-field collision, unsigned checksum return mapping, redundant dependency library, and unexpected panic/callback diagnostics. Validation scope: the user clarified that all milestone testing is emulator-only and physical devices are excluded. Android QEMU2 explicitly rejects an x86_64 system image on this aarch64 Mac even with acceleration disabled. x86_64 runtime loading remains untested; its cross-compilation, metadata, ELF and split validation passed. The untested label records only that unavailable emulator check. Arm64 emulator runtime validation passed on 4 KB and 16 KB devices. The clarified testing scope is recorded in the Android architecture, parity matrix and build documentation.
hugo added the untested label 2026-10-08 10:34:43 +00:00
hugo closed this issue 2026-10-08 10:34:43 +00:00
Author
Owner

The user updated Android milestone support on 2026-10-08 to arm64-v8a and armeabi-v7a, with emulator-only validation. The issue body is amended accordingly. Both ARM ABIs were built and audited; ARM64 4 KB and strict 16 KB emulator validation was completed for this issue. The historical x86_64 runtime limitation is now outside the required scope, so the untested label has been removed. Historical implementation evidence is retained.

The user updated Android milestone support on 2026-10-08 to arm64-v8a and armeabi-v7a, with emulator-only validation. The issue body is amended accordingly. Both ARM ABIs were built and audited; ARM64 4 KB and strict 16 KB emulator validation was completed for this issue. The historical x86_64 runtime limitation is now outside the required scope, so the untested label has been removed. Historical implementation evidence is retained.
hugo removed the untested label 2026-10-08 14:43:23 +00:00
Sign in to join this conversation.