Commit b82876c
## 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
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
24 | 31 | | |
25 | 32 | | |
26 | 33 | | |
| |||
53 | 60 | | |
54 | 61 | | |
55 | 62 | | |
56 | | - | |
57 | | - | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
58 | 70 | | |
59 | 71 | | |
60 | 72 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
| 12 | + | |
| 13 | + | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| |||
0 commit comments