Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8ce3374
feat(android): real JNI binding; route Android backend by abi
claude Jun 2, 2026
d380dd9
feat(android): native Service/Receiver/Widget component hosts
claude Jun 2, 2026
ccdbee8
feat(abi): Idris2 lifecycles + linearity for Android components
claude Jun 2, 2026
ddd6a4c
feat(android): generate JVM shims via tooling; drop hand-authored Java
claude Jun 2, 2026
4cef590
docs(android): component-hosting design, readiness checklist, state
claude Jun 2, 2026
a4991de
chore(android): track generated JVM shims (gitignore negation)
claude Jun 2, 2026
fb0f90b
fix(android): parse error on comptime block; harden JNI casts (CI)
claude Jun 2, 2026
9e51cdf
fix(android): relocate generated shims under android/**/src/** carve-out
claude Jun 2, 2026
e49a7df
fix(android): take address of test handlers (&fn) for fn-pointer params
claude Jun 2, 2026
d3758d5
ci(android): run component tests in a dedicated workflow (visible out…
claude Jun 2, 2026
7fe2114
fix(android): root the test aggregator in src/ (Zig module-path rule)
claude Jun 2, 2026
71fdf8c
fix(android): allocPrintZ -> allocPrintSentinel (Zig 0.15.2 std.fmt)
claude Jun 2, 2026
b551a6d
fix(cli): declare gossamer_watcher_start/stop externs (E2E Section 3)
claude Jun 2, 2026
1d5ac80
chore(hypatia): baseline jni.zig vtable casts as advisory (owner-ack)
claude Jun 2, 2026
c5a2fb7
feat(android): gossamer-android-services companion — subclass base cl…
claude Jun 2, 2026
3653bde
fix(android): move sample subclasses under src/androidTest (Java carv…
claude Jun 2, 2026
52f98e8
fix(cli): link gossamer_channel_register_defaults; honour %unsafe in …
claude Jun 2, 2026
7d88850
docs(jni): SAFETY notes on all vtable function-pointer casts
claude Jun 2, 2026
8c3ef85
chore(state): record CLI/jni-cast/§5 follow-through; binding split to…
claude Jun 2, 2026
dedd1ad
Merge remote-tracking branch 'origin/main' into claude/adoring-meitne…
claude Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/android.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ deps/
.cache/
build/
dist/

# Android per-ABI native build outputs (produced by `just android-build`)
android/gossamer-android-services/src/main/jniLibs/
34 changes: 34 additions & 0 deletions .hypatia-baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[
{
"severity": "high",
"severity_override": "advisory",
"rule_module": "code_safety",
"type": "zig_ptr_cast",
"file": "src/interface/ffi/src/jni.zig",
"note": "Inherent to the JNI binding: dispatching through the per-thread JNINativeInterface vtable requires reinterpreting opaque table slots as typed function pointers. This is CWE-704 by construction, not a defect — it is the identical pattern std.DynLib.lookup uses for dlsym results, and is documented at each call site with a // SAFETY: note. The only alternative is hand-transcribing the full ~232-entry NDK vtable as a typed struct, which is more error-prone than the cast. Owner-acknowledged; kept advisory (visible, non-blocking)."
},
{
"severity": "medium",
"severity_override": "advisory",
"rule_module": "code_safety",
"type": "zig_ptr_cast",
"file": "src/interface/ffi/src/jni.zig",
"note": "See the high-severity entry: inherent JNI vtable function-pointer cast; advisory."
},
{
"severity": "high",
"severity_override": "advisory",
"rule_module": "code_safety",
"type": "zig_align_cast",
"file": "src/interface/ffi/src/jni.zig",
"note": "Inherent to the JNI binding: @alignCast pairs with the vtable/env @ptrCast (same std.DynLib.lookup pattern). Not independently fixable; advisory (visible, non-blocking)."
},
{
"severity": "medium",
"severity_override": "advisory",
"rule_module": "code_safety",
"type": "zig_align_cast",
"file": "src/interface/ffi/src/jni.zig",
"note": "See the high-severity entry: inherent JNI vtable alignment cast; advisory."
}
]
77 changes: 74 additions & 3 deletions .machine_readable/6a2/STATE.a2ml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Android — `gossamer-android-services` companion: four shim base classes + real JNI (Phase 5, #71)**: Grows the Android target from "a WebView in an Activity" into a first-class mobile shell hosting the non-UI components, on the critical path for neurophone#97 (migrating a banned-Kotlin Android app onto gossamer). Owner-signed-off subclass model (issue #71).
- `src/interface/ffi/src/jni.zig` (new) — a REAL JNI binding in pure Zig: models the `JNINativeInterface`/`JNIInvokeInterface` tables (ordinal-indexed), uses the `...A`-variant calls (no C varargs across the boundary), caches the `JavaVM` for thread attach, uses global refs, and reads `float[]` sensor samples via `Get/ReleaseFloatArrayElements`. The previous bridge declared `extern fn jni_*` symbols defined nowhere, so it could never link.
- **Four abstract Java base classes** under `android/gossamer-android-services/src/main/java/io/gossamer/` that downstream apps EXTEND with ~5-20 lines (lifecycle methods are `final`): `services/GossamerForegroundService` (Service + SensorEventListener), `services/GossamerBootReceiver` (BroadcastReceiver), `services/GossamerAppWidgetProvider` (AppWidgetProvider), and the existing `GossamerActivity` extended with an `onIntentReceived` hook. gossamer owns every JNI call.
- `src/interface/ffi/src/services_android.zig` (new) — the native host: independent opaque `ServiceHandle` (separate from the webview handle) and the `Java_io_gossamer_services_*` JNI exports. Apps plug their Rust/Zig core in via a process-global callback registry (`gossamer_android_register_service_callbacks` / `_widget_callbacks` / `_boot_callback` / `_intent_callback`) — they never touch JNI. Config and widget state cross the boundary as JSON; sensors as primitive `int[]`.
- `src/interface/abi/AndroidComponents.idr` (wired into `gossamer-abi.ipkg`) — terminal-teardown and dispatch-only-while-live proofs per component; the long-lived Service is a `LinearHandle` over the same Allocated/Active/Consumed machine as the webview, so linearity crosses the JNI boundary. Zero `believe_me`.
- `webview_android.zig` rewritten onto `jni.zig` (fixes latent ref/registration bugs); `main.zig` selects the Android backend by `abi == .android` (it was falling through to GTK); `build.zig` links NDK `log`/`android` and skips WebKitGTK for the android abi.
- Host-runnable native tests (`zig build test-android`, plus a dedicated `Android Component Tests` workflow with visible output); `just android-build` per-ABI cross-compile.
- Design + "ready-to-depend-on" checklist: `docs/architecture/android-components.adoc`; module guide: `android/gossamer-android-services/README.adoc`.

- **Conf — real JSON loader for `gossamer.conf.json` (Phase 14a.4, #27)**: Replaces the hand-rolled string-scan parser in `cli/src/main.zig` (`parseConfig` + `extractStringField/IntField/BoolField`) with a `std.json`-backed loader exposed through libgossamer
- `src/interface/ffi/src/conf.zig` — six exports, all capability-gated on FileSystem (kind=0): `gossamer_conf_load(path, cap_token) -> opaque*`, `gossamer_conf_get_string(conf*, dotted_path) -> *char | null`, `gossamer_conf_get_int(conf*, path, default) -> i64`, `gossamer_conf_get_bool(conf*, path, default) -> i32`, `gossamer_conf_has(conf*, path) -> i32`, `gossamer_conf_free(conf*) -> void`.
- Dotted-path lookup so callers target nested keys explicitly: `productName` (top-level), `build.devUrl` (nested), `app.windows.0.width` (array element). Fixes the old scanner's "first match wins" bug where a key like `title` matched at the wrong nesting level.
Expand Down
36 changes: 33 additions & 3 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,31 @@ build-freebsd:
build-all-platforms: build-ffi build-macos-x64 build-macos-arm build-windows build-linux-arm build-linux-riscv
@echo "Built for: linux-x64, macos-x64, macos-arm64, windows-x64, linux-arm64, linux-riscv64"

# ─── Android (gossamer-android-services companion, issue #71) ───
# Run the host-runnable Android native logic tests (jni.zig + services_android.zig;
# no NDK needed). The on-device JNI paths are validated on an emulator (issue #67).
android-test:
cd src/interface/ffi && zig build test-android

# Cross-compile libgossamer.so for every Android ABI (requires ANDROID_NDK_HOME).
# Produces a jniLibs tree the consuming app bundles. The JNI WebView backend and
# the service/receiver/widget native host are selected automatically for
# *-android targets.
android-build:
#!/usr/bin/env bash
set -euo pipefail
: "${ANDROID_NDK_HOME:?set ANDROID_NDK_HOME to your NDK (r26+)}"
declare -A ABI=( [aarch64-linux-android]=arm64-v8a \
[x86_64-linux-android]=x86_64 \
[arm-linux-androideabi]=armeabi-v7a )
for tgt in "${!ABI[@]}"; do
echo "==> $tgt (${ABI[$tgt]})"
( cd src/interface/ffi && zig build -Dtarget="$tgt" -Doptimize=ReleaseSafe )
mkdir -p "android/gossamer-android-services/src/main/jniLibs/${ABI[$tgt]}"
cp "src/interface/ffi/zig-out/lib/libgossamer.so" "android/gossamer-android-services/src/main/jniLibs/${ABI[$tgt]}/" 2>/dev/null || true
done
@echo "Android .so built for: arm64-v8a, x86_64, armeabi-v7a (see android/gossamer-android-services/src/main/jniLibs/)"

# Show supported platform targets
platforms:
@echo "=== Gossamer Supported Platforms ==="
Expand All @@ -174,9 +199,14 @@ platforms:
@echo " openbsd-x64 WebKitGTK zig build -Dtarget=x86_64-openbsd"
@echo " netbsd-x64 WebKitGTK zig build -Dtarget=x86_64-netbsd"
@echo ""
@echo "Mobile (Phase 3 — v0.4.0+):"
@echo " ios-arm64 WKWebView/UIKit planned"
@echo " android-arm64 Android WebView planned"
@echo "Mobile (Phase 5):"
@echo " ios-arm64 WKWebView/UIKit zig build -Dtarget=aarch64-ios"
@echo " android-arm64 Android WebView zig build -Dtarget=aarch64-linux-android (just android-build)"
@echo " android-x86_64 Android WebView zig build -Dtarget=x86_64-linux-android (emulator)"
@echo " android-armv7 Android WebView zig build -Dtarget=arm-linux-androideabi"
@echo ""
@echo " Android also hosts native Service / BroadcastReceiver / AppWidgetProvider"
@echo " components — see docs/architecture/android-components.adoc."

# ═══════════════════════════════════════════════════════════════
# Run
Expand Down
46 changes: 34 additions & 12 deletions ROADMAP.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,40 @@ Mobile platforms using platform-native webviews.
* [ ] Push notification integration
* [ ] Safe area insets handling

=== Android (webview_android.zig) — Partially Implemented

* [x] JNI type definitions and entry points
* [x] GossamerActivity native init/destroy via JNI_OnLoad
* [x] WebviewState with JNI references
* [x] jni_NewObject for GossamerBridge constructor (fixed from CallObjectMethod, 2026-04-03)
* [x] nativeInit accepts and caches screen_width/screen_height params
* [ ] JNI FindClass / GetMethodID / CallVoidMethod for WebView operations
* [ ] loadData / loadUrl / evaluateJavascript via JNI
* [ ] addJavascriptInterface for IPC bridge
* [ ] Android NDK build integration
* [ ] Gradle project scaffolding for Java launcher
=== Android — WebView + native component hosts (2026-06-02)

WebView shell:

* [x] Real JNI binding (`jni.zig`): JNINativeInterface vtable, `...A`-variant
calls, cached JavaVM for thread attach, global refs. (Replaces the previous
`extern fn jni_*` symbols that were defined nowhere — the bridge now links.)
* [x] Platform dispatch routes Android by `abi == .android` (was falling through
to GTK); `build.zig` links NDK `log`/`android`, skips WebKitGTK.
* [x] loadDataWithBaseURL / loadUrl / evaluateJavascript / setTitle via JNI
* [x] GossamerActivity nativeInit(Activity, WebView, int, int) / nativeDestroy
* [x] GossamerBridge `@JavascriptInterface` IPC (single registration)

Native non-UI components — `gossamer-android-services` companion (issue #71,
owner-signed-off subclass model):

* [x] Four abstract Java base classes apps extend (`final` lifecycle) under
`android/gossamer-android-services/src/main/java/io/gossamer/`:
`services/GossamerForegroundService` (Service + SensorEventListener),
`services/GossamerBootReceiver`, `services/GossamerAppWidgetProvider`, and
`GossamerActivity` extended with an `onIntentReceived` hook
* [x] `services_android.zig`: independent `ServiceHandle` + the
`Java_io_gossamer_services_*` JNI exports; app native core plugs in via the
`gossamer_android_register_*` callback registry (apps never touch JNI)
* [x] Config / widget-state as JSON; sensors as primitive `int[]`
* [x] Idris2 lifecycle proofs (`Gossamer.ABI.AndroidComponents`): terminal
teardown, dispatch-only-while-live, Service as a LinearHandle
* [x] Host-runnable native tests (`zig build test-android` + dedicated workflow)

Remaining (tracked as issues):

* [ ] NDK CI runner: build `libgossamer.so` per ABI + emulator smoke test
* [ ] APK/AAB assembly + signing (aapt2/d8/r8/apksigner, gradle-free)
* [ ] Ephapax region-calculus modelling of long-lived Service handles

== Phase 4 — Production (v1.0.0)

Expand Down
Loading
Loading