Skip to content

feat(android): port NativeLib (Kotlin) JNI surface to Rust (#110)#130

Open
hyperpolymath wants to merge 1 commit into
mainfrom
chore/gossamer-nativelib-port
Open

feat(android): port NativeLib (Kotlin) JNI surface to Rust (#110)#130
hyperpolymath wants to merge 1 commit into
mainfrom
chore/gossamer-nativelib-port

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

Sub-PR #4 of the Android Kotlin→Rust/Gossamer migration (epic #83, RFC #97, sub-issue #110). Implements the native (neurophone_android) JNI surface, replacing the pub fn hello() stub with the full 11-method ai.neurophone.NativeLib contract. The most independent step of the migration — no Service/widget/UI code is touched.

Each Java_ai_neurophone_NativeLib_* export decodes its JVM arguments and delegates to the existing pure-Rust workspace crates:

  • neurophone-core (NeuroSymbolicSystem) — lifecycle, sensor processing, neural context, state, hybrid query router.
  • llm (MockBackend) — on-device LLM stand-in (llama.cpp swaps in later).
  • claude-client (HybridInference / ClaudeClient) — cloud path, with a graceful "no API key" branch.
  • sensors (SensorKind) — Android sensor-type id mapping.

JNI contract implemented (class ai.neurophone.NativeLib, lib neurophone_android)

init(String?)->bool, start()->bool, stop()->void, processSensor(int, float[], long, int)->bool, queryLocal(String)->String, queryClaude(String)->String, query(String, bool)->String, getNeuralContext()->String, getState()->String(JSON), reset()->void, isRunning()->bool.

Sensor-type id map per spec: accelerometer=1, magnetometer=2, gyroscope=4, light=5, proximity=8, else unknown (rejected).

Files changed

  • crates/neurophone-android/src/lib.rs — full JNI implementation (was a stub). A Mutex-guarded process-global NativeRuntime singleton; safe core_* functions hold all logic; thin unsafe extern "C" exports decode args and delegate. 7 unit tests.
  • crates/neurophone-android/Cargo.toml — add claude-client and llm path deps.
  • Cargo.toml (workspace) — pin jni = "0.21"; revert rand/rand_distr to 0.9/0.5 (see Risks).
  • Cargo.lock — refreshed (jni 0.21.1, rand 0.9.4).

The Kotlin/Java bindings (NativeLib.kt, MainActivity.kt) are intentionally left untouched — reconciled in the shim PRs / legacy-delete step. Marked TODO(#83) in code.

Unsafe-on-JNI-boundary justification

The crate is #[deny(unsafe_code)]. The JVM resolves native methods by C symbol name (Java_<class>_<method>), which requires #[unsafe(no_mangle)] + unsafe extern "C" — there is no safe-Rust spelling of an exported C-ABI symbol, and the jni handles are raw JVM-provided values. So:

  • The crate-level lint was relaxed from forbid to #[deny(unsafe_code)].
  • Each JNI export carries a local, documented #[allow(unsafe_code)] // JNI ABI: see module-level justification.
  • Every export body immediately hands off to a safe core_* function and performs no unsafe operations beyond the ABI declaration. The unsafe surface is purely the entry-point signatures, confined to the jni_boundary module.

What I verified

  • cargo build --workspace — green.
  • cargo test --workspace — green: 32 test binaries, all pass, incl. 7 new tests here (sensor id map, config fallback, init/start/stop/reset lifecycle, sensor arity/type validation, local/cloud/hybrid query paths, JSON state shape, and pre-init safety).
  • cargo clippy -p neurophone-android --all-targets — no warnings.
  • nm -D libneurophone_android.so — all 11 Java_ai_neurophone_NativeLib_* symbols exported.

TODOs / risks

  • rand/rand_distr revert (pre-existing breakage). Dependabot PRs chore(deps): bump rand from 0.9.3 to 0.10.1 #49/chore(deps): bump rand_distr from 0.5.1 to 0.6.0 #67 bumped rand 0.9→0.10 and rand_distr 0.5→0.6, but ndarray-rand 0.16 still requires rand 0.9. This breaks esn/lsm the moment the lockfile is refreshed; the committed Cargo.lock pinned rand 0.9.4, which masked the regression. I reverted to 0.9/0.5 (with an explanatory NOTE(#83) in Cargo.toml) so the workspace builds. Re-bump only alongside an ndarray-rand upgrade that supports rand 0.10. Happy to split this into its own commit/PR if preferred.
  • jni pinned to 0.21. The workspace previously declared 0.22, whose native-method API was reworked around EnvUnowned::with_env and is still settling. 0.21's JNIEnv-first-arg surface keeps the FFI boundary small and auditable. Only neurophone-android consumes jni, so the blast radius is nil. Revisit when 0.22's API stabilises.
  • MockBackend for local LLM is a deterministic stand-in; real llama.cpp wiring is out of scope (tracked separately).
  • queryClaude builds a short-lived current-thread tokio runtime per call and requires an API key from the environment; without one it returns a clear [claude-unavailable] string rather than failing the FFI call. Per-call runtime is fine for the current call pattern; revisit if it becomes hot.
  • Kotlin still references these symbols — binding declarations deliberately left as-is for the shim/legacy-delete PRs (TODO(#83)).

https://claude.ai/code/session_01Gu1JFCZHuBtBhAWPr4sMQw


Generated by Claude Code

Implement the 11-method `ai.neurophone.NativeLib` JNI contract in
crates/neurophone-android, replacing the `hello()` stub. Each
`Java_ai_neurophone_NativeLib_*` export decodes JVM arguments and
delegates to the safe pure-Rust workspace crates (neurophone-core,
llm, claude-client, sensors).

- Process-global, Mutex-guarded NativeRuntime singleton.
- Sensor id map per contract (accel=1, mag=2, gyro=4, light=5, prox=8).
- Local LLM via llm::MockBackend; cloud via claude_client (graceful
  "no API key" path); hybrid query via core orchestrator.
- Crate is #![deny(unsafe_code)] with justified #[allow(unsafe_code)]
  only on the JNI ABI declarations (documented).
- Pin jni to 0.21 (stable native-method API) for the android crate.
- Revert incompatible rand 0.10 / rand_distr 0.6 bumps back to 0.9/0.5
  to match ndarray-rand 0.16, unbreaking esn/lsm so the workspace
  builds (pre-existing regression masked by the pinned Cargo.lock).

Kotlin bindings (NativeLib.kt/MainActivity.kt) left untouched;
reconciled in the shim/legacy-delete PRs. TODO(#83).

cargo build --workspace + cargo test --workspace green (32 test
binaries, all pass; 7 new tests in neurophone-android).

https://claude.ai/code/session_01Gu1JFCZHuBtBhAWPr4sMQw
@hyperpolymath hyperpolymath marked this pull request as ready for review June 3, 2026 21:07
@hyperpolymath hyperpolymath enabled auto-merge (squash) June 3, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants