Skip to content

Commit b82876c

Browse files
feat(android): port NativeLib (Kotlin) JNI surface to Rust (#110) (#130)
## 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 #49/#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](https://claude.ai/code/session_01Gu1JFCZHuBtBhAWPr4sMQw)_ Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent d083122 commit b82876c

4 files changed

Lines changed: 592 additions & 62 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 57 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ repository = "https://github.com/hyperpolymath/neurophone"
2121
[workspace.dependencies]
2222
# Core neural network
2323
ndarray = { version = "0.17", features = ["rayon", "serde"] }
24+
# NOTE(#83): rand/rand_distr are held at 0.9/0.5 to stay compatible with
25+
# ndarray-rand 0.16 (which depends on rand 0.9). The earlier dependabot bumps
26+
# to rand 0.10 / rand_distr 0.6 (PRs #49, #67) are incompatible with
27+
# ndarray-rand 0.16 and break `esn`/`lsm` once the lockfile is refreshed; the
28+
# committed Cargo.lock still pinned rand 0.9.4, which masked the regression.
29+
# Revert keeps `cargo build --workspace` green. Re-bump only alongside an
30+
# ndarray-rand upgrade that supports rand 0.10.
2431
ndarray-rand = "0.16"
2532
# rand pinned to 0.9 / rand_distr 0.5 to match ndarray-rand 0.16 (which bundles
2633
# rand 0.9). A dependabot bump to rand 0.10 broke the build: rand 0.10 is a
@@ -53,8 +60,13 @@ chrono = { version = "0.4", features = ["serde"] }
5360
thiserror = "2.0"
5461
anyhow = "1.0"
5562

56-
# Android JNI
57-
jni = "0.22"
63+
# Android JNI.
64+
# Pinned to 0.21 (the stable, widely-used native-method API: `JNIEnv` as the
65+
# first argument with direct `get_string`/`new_string`/array helpers). jni 0.22
66+
# reworked the native-method entry points around `EnvUnowned::with_env` and is
67+
# still settling; the simpler 0.21 surface keeps the FFI boundary small and
68+
# auditable. Only `neurophone-android` consumes this dependency.
69+
jni = "0.21"
5870

5971
# Configuration
6072
config = "0.14"

crates/neurophone-android/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ crate-type = ["cdylib"]
99

1010
[dependencies]
1111
neurophone-core = { path = "../neurophone-core" }
12+
claude-client = { path = "../claude-client" }
13+
llm = { path = "../llm" }
1214
sensors = { path = "../sensors" }
1315

1416
jni = { workspace = true }

0 commit comments

Comments
 (0)