iris: add the android-app cdylib crate (I2, part 3)

iris/android-app is the concrete app RUST.md's I2 is judged against:
JNI_OnLoad, a Client implementing AndroidAppState, and new_view_peer
wrapping iris::android::new_peer's generic function in the plain
function pointer register_view_class needs. Its UI is tabs-ui::build,
unchanged from the winit example.

Deliberately excluded from the iris workspace (iris/Cargo.toml's new
`exclude`): android-view needs the NDK sysroot to link, so folding this
crate in would break `cargo build --workspace --all-targets` on the
host. It resolves as its own single-crate workspace instead, built
with `cd iris/android-app && cargo ndk -t x86_64 -P 26 build`.

Verified: cross-compiles and clippys clean for x86_64-linux-android
API 26; the host iris workspace (build/clippy/fmt/19 tests) is
unaffected. Not yet built: the Gradle shell (IrisView.java,
MainActivity, AndroidManifest) to actually install and run this on the
emulator -- next in RUST.md's I2.

Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
irisandClaude Sonnet committed 2026-09-05 04:46:10 -04:00
1 parent 9c935f8ce8
commit f79bd7ca71
5 files changed
+4280

No files matched your search

+1
View File
@@ -26,3 +26,4 @@ sessions/
# iris, the in-house UI library, is vendored at iris/ and built by cargo.
iris/target/
iris/android-app/target/
+7
View File
@@ -49,6 +49,13 @@ tabs-ui = { path = "tabs-ui" }
[workspace]
members = ["core", "macro", "tabs-ui"]
# android-app pulls in android-view, which needs the NDK sysroot to link
# -- excluded so `cargo build --workspace --all-targets` on the host stays
# buildable. Cross-compile it from its own directory (its own single-crate
# workspace, since it has no `[workspace]` table of its own and this
# exclusion stops it inheriting this one): `cd android-app && cargo ndk
# -t x86_64 -P 26 build`.
exclude = ["android-app"]
[workspace.package]
version = "0.1.0"
+4156
View File
File diff suppressed because it is too large. Load diff
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "iris-android-app"
version = "0.1.0"
edition = "2024"
# Deliberately outside the `iris` workspace (see that Cargo.toml's
# `[workspace] exclude`): this crate exists only to be cross-compiled with
# `cargo ndk` for the emulator/a phone, and pulls in android-view, which
# needs the NDK sysroot to link. Folding it into the main workspace would
# make `cargo build --workspace --all-targets` -- the host command RUST.md
# and AGENTS.md both require to stay clean -- try to link a cdylib against
# libraries that do not exist on this machine. See RUST.md's I2.
[lib]
name = "main"
crate-type = ["cdylib"]
[dependencies]
iris = { path = "../" }
tabs-ui = { path = "../tabs-ui" }
android-view = { git = "https://github.com/rust-mobile/android-view.git", rev = "bec6c62a96cef8239b0fd7fedeef9b184d02e3a1" }
android_logger = "0.15.0"
log = "0.4.28"
[profile.release]
panic = "abort"
[profile.dev]
panic = "abort"
+87
View File
@@ -0,0 +1,87 @@
//! The android-view demo app: iris's `tabs` widget tree (`tabs_ui::build`,
//! shared with the winit example) running through
//! `iris::android`'s `ViewPeer`. This is RUST.md's I2 pass condition made
//! concrete -- there is no UI here beyond what `tabs-ui` already draws.
//!
//! `JNI_OnLoad` and `new_view_peer` mirror android-view's own demo
//! (`~/src/android-view/demo/src/lib.rs`): the only android-view-specific
//! plumbing a real app needs is registering its `View` subclass and
//! wrapping `iris::android::new_peer`'s generic function in a concrete
//! `extern "system" fn`, since `register_view_class` wants a plain
//! function pointer.
use android_view::{
Context, View,
jni::{
JNIEnv, JavaVM,
sys::{JNI_VERSION_1_6, JavaVM as RawJavaVM, jint, jlong},
},
register_view_class,
};
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
use iris::prelude::*;
use log::LevelFilter;
use std::ffi::c_void;
/// The app's `View` subclass, matching the Java side's package --
/// `app/src/main/java/dev/iris/android/demo/IrisView.java`.
const VIEW_CLASS: &str = "dev/iris/android/demo/IrisView";
pub struct Client {
ui_state: AndroidUiState,
}
impl HasAndroidUiState for Client {
fn android_state(&self) -> &AndroidUiState {
&self.ui_state
}
fn android_state_mut(&mut self) -> &mut AndroidUiState {
&mut self.ui_state
}
}
impl AndroidAppState for Client {
fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self {
// `widgets.info` is the winit example's frame-debug readout, kept
// current from `DefaultAppState::window_event` -- android-view has
// no per-frame hook to drive the equivalent from here yet, so it
// is left at its built "" text rather than wired to nothing.
let _ = tabs_ui::build(rsc, &mut ui_state);
Self { ui_state }
}
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>, _render: &mut UiRenderState) -> bool {
// Nothing in the tabs example has a back stack of its own to pop --
// declining lets the activity finish, which is the same "no
// handler" behaviour the default impl gives. Present as an
// explicit override (rather than relying on the default) so a
// reader checking "does the back gesture reach this app" finds an
// answer here rather than nothing.
false
}
}
extern "system" fn new_view_peer<'local>(
env: JNIEnv<'local>,
view: View<'local>,
context: Context<'local>,
) -> jlong {
iris::android::new_peer::<Client>(env, view, context)
}
/// # Safety
/// Interacting with JNI at load time is always unsafe at some level --
/// mirrors android-view's own demo, which carries the same comment.
#[unsafe(no_mangle)]
pub unsafe extern "system" fn JNI_OnLoad(vm: *mut RawJavaVM, _: *mut c_void) -> jint {
android_logger::init_once(
android_logger::Config::default()
.with_max_level(LevelFilter::Debug)
.with_tag("iris-android-app"),
);
let vm = unsafe { JavaVM::from_raw(vm) }.unwrap();
let mut env = vm.get_env().unwrap();
register_view_class(&mut env, VIEW_CLASS, new_view_peer);
iris::android::register_native_methods(&mut env, VIEW_CLASS);
JNI_VERSION_1_6
}