- 前端
- UI组件
- 3D渲染
- 跨平台
- 游戏开发
【免费下载链接】makepad
Makepad is a creative software development platform for Rust that compiles to wasm/webGL, osx/metal, windows/dx11 linux/opengl
导读
makepad-android-state(crate 名为makepad-android-state)是 Makepad 项目中一个体量极小但职责关键的基础库:它持有 Makepad 在 Android 平台上运行所需的两个进程级上下文状态——由 JNI 层初始化的JavaVM实例,以及当前存活的 MakepadActivity实例。本文以该库的 README 为主线,结合仓库内 lib.rs 源码与 Android 平台层、语音系统等真实调用方,完整讲解其设计动机、公开 API、内部安全机制,以及如何在第三方 crate 中安全获取 JNI 环境与 Activity 句柄。读完本文,你将掌握在 Android 端通过 JNI 与 Java 层交互的正确姿势,并理解 Makepad 框架为何对这两处全局状态做如此严格的访问控制。
一、这个库为什么存在:避免"为取一个状态而依赖整个 Makepad"
Makepad 是一个可以编译到 wasm/webGL、macOS/Metal、Windows/DX11、Linux/OpenGL 等后端的大型 GUI 框架,其主 crate 体积庞大。而在 Android 平台上,任何需要与 Java 层(例如SpeechRecognizer、TextToSpeech、OpenXR 会话)交互的 Rust 代码,都绕不开两个全局句柄:
- 进程唯一的
JavaVM *(JVM 实例指针); - 当前正在运行的
Activity的jobject句柄。
如果把这些状态直接放在 Makepad 主 crate 里,那么任何只想"拿个 Activity 句柄"的外部 crate 都必须把整个 Makepad 拉进依赖树。makepad-android-state正是为此拆出的独立薄层——从 Cargo.toml 可以看到,它仅依赖makepad-jni-sys = "0.4.0"一个 crate,自身不含任何 UI、渲染或事件循环代码。README 的原话概括了其全部价值:"It exists solely to allow external crates to access those Android states without depending on the entirety of Makepad."(它存在的唯一目的,就是让外部 crate 无需依赖整个 Makepad 即可访问这些 Android 状态。)
二、两个核心状态及其生命周期差异
README 明确指出该库持有且仅持有两个状态,它们有着截然不同的生命周期语义,这也是访问控制策略设计的根源。
1.JavaVM实例:进程一生只初始化一次
JavaVM由 JNI 层在进程启动时初始化(Android 系统加载 JNI 库时触发JNI_OnLoad),在整个应用进程生命周期内只存在一个实例,且只被设置一次。
不可由外部代码设置:README 强调 "This cannot be set by foreign code outside this crate"。原因是它只会被设置一次,任何外部"重复写入"都意味着逻辑错误,因此该写路径被完全封死,只有 crate 内部(实际是 JNI 入口)可以写入。
从源码看,lib.rs 用
static mut VM: *mut jni_sys::JavaVM保存该指针;写入发生在两个标有#[no_mangle]的 JNI 入口函数中:JNI_OnLoad:标准 JNI 导出函数,Android 加载.so时自动调用,将传入的JavaVM*存入VM,并返回JNI_VERSION_1_6作为兼容版本声明(lib.rs);jni_on_load:另一个 C 导出入口,同样将vm写入全局VM(lib.rs)。
2.Activity实例:可被系统反复销毁重建
与JavaVM不同,Android 平台可能在一个应用进程的生命周期内多次拆除并重建 Activity 实例——典型的触发场景包括:设备旋转、进入分屏模式、窗口 resize、应用被移到后台后重建等。因此:
- Activity 句柄是可变的,外部代码在需要时必须"重新获取",绝不能缓存复用;
- 它可以由外部代码设置("Thiscanbe set by foreign code outside this crate"),因为框架需要在 Activity 每次重建后把新句柄写进来;
- 但出于安全考虑,只允许单个调用方拿到私有的 setter 函数——这正是下面要讲的
get_activity_setter_fn()机制。
三、对外公开 API:外部用户只需要两个函数
README 明确划定了"外部用户"的接触面:外部用户只应关心两个函数,其余函数均为 Makepad 内部专用,对外部用户没有用处。
get_java_vm():获取 JavaVM 指针
pub fn get_java_vm() -> *mut jni_sys::JavaVM- 返回 JNI 层初始化好的
JavaVM实例指针,通过它可以获取 JNI 环境(JNIEnv),进而调用 Java 方法、创建对象、访问类等。 - 若
JavaVM尚未初始化,返回空指针(null pointer)。 - 实现为
#[inline(always)],代价为零——本质上就是读一个static mut(lib.rs)。
get_activity():获取当前 Makepad Activity 句柄
pub fn get_activity() -> jni_sys::jobject- 返回当前 Makepad
Activity实例的jobject句柄(全局引用)。 - 若 Activity 尚未初始化,返回空指针。
- 源码文档特别提醒调用方(lib.rs):不要缓存或复用返回的 Activity 指针,而应在每次需要时重新调用本函数。因为 Activity 实例可能因系统动作(旋转、分屏、resize、移动等)在后台被销毁并重建。
需要说明的是,
get_java_vm()返回的*mut JavaVM与get_activity()返回的jobject都是原始指针,调用方需要自行保证其在 JNI 调用边界内的有效性。这两个函数只负责"读取"全局状态,不负责"保护"——保护责任由设置端的单次授权机制承担。
四、内部机制:为什么 Activity 的 setter 只能被领取一次
README 中最精妙的设计在于对 Activity 写路径的管控。源码用一个Mutex<Option<unsafe fn(jobject)>>包装了内部set_activity函数:
static SET_ACTIVITY_FN: Mutex<Option<unsafe fn(jni_sys::jobject)>> = { unsafe fn set_activity(activity: jni_sys::jobject) { ACTIVITY = activity; } std::sync::Mutex::new(Some(set_activity)) };对外的领取函数是get_activity_setter_fn()(标注#[doc(hidden)],即文档中"其他函数为 Makepad 内部专用"所指的成员):
pub fn get_activity_setter_fn() -> Option<unsafe fn(jni_sys::jobject)> { SET_ACTIVITY_FN.lock().unwrap().take() }其核心语义是"只会返回Some一次":
- 第一次调用时,
Option::take()从Mutex中取出并移除该函数,返回Some(set_activity); - 之后所有调用都返回
None。
这就从类型系统层面保证了只有唯一一个调用方(即 Makepad 内部框架的 Android 平台层)能获得设置 Activity 的能力,外部 crate 即使调用该函数也无法覆盖 Activity 状态。README 的原话:"for safety reasons, we only permit a single caller to obtain the private 'set_activity' function, which ensures that only the internal Makepad framework can set the activity instance."
在 android_jni.rs 中可以清晰看到这条"领取—注册—调用"链路:
pub fn jni_set_activity(activity_handle: jni_sys::jobject) { unsafe { // 第一次(也是唯一一次)领取 setter if let Some(func) = makepad_android_state::get_activity_setter_fn() { SET_ACTIVITY_FN = func; // 存入本模块的静态变量 } SET_ACTIVITY_FN(activity_handle); // 立即写入新 Activity } } pub fn jni_update_activity(activity_handle: jni_sys::jobject) { unsafe { SET_ACTIVITY_FN(activity_handle) }; // Activity 重建后持续更新 }jni_set_activity负责在首次拿到句柄时领取 setter 并写入;jni_update_activity则在 Activity 每次重建后反复调用同一 setter 更新全局句柄。配合fetch_activity_handle(android_jni.rs)通过NewGlobalRef将 Java 侧传入的 Activity 转为 JNI 全局引用,构成了完整的"Java 层 Activity 变化 → Rust 全局状态更新"回路。
五、仓库内的真实调用场景
理解设计后,看两个真实调用方可以更直观地体会这套 API 的用法与价值。
场景 A:Makepad Android 平台层——OpenXR 实例创建
在 android.rs 中,Makepad 的 Android 平台在尝试创建 OpenXR 会话前,直接调用:
let activity_handle = makepad_android_state::get_activity(); match self.os.openxr.create_instance(activity_handle) { ... }OpenXR 的XrInstanceCreateInfoAndroidKHR需要传入 AndroidActivity句柄作为applicationVM/applicationActivity参数。这里get_activity()成为 Makepad 自身从"保存状态的库"读取最新 Activity 的入口,且每次会话创建前都会重新读取,正好呼应"不要缓存 Activity 指针"的告诫。
场景 B:system_speech库——通过get_java_vm附加 JNI 线程
libs/system_speech/src/platform/android.rs 是仓库中"外部 crate 使用本库"的典型范例。它通过use makepad_android_state::{get_activity, get_java_vm}引入两个公开函数,并据此搭建完整的 JNI 胶水层:
- 线程附加:Rust 创建的工作线程对 JVM 而言是未知线程,必须先附加才能调用 JNI。
attach_env通过get_java_vm()拿到JavaVM指针,再调用AttachCurrentThread获取本线程的JNIEnv(android.rs)。这正是 README 所说"through which you can obtain the JNI environment"的落地实现。 - Activity 方法解析:
activity_method通过get_activity()拿到当前 Activity 句柄,用GetObjectClass获取其类,再以GetMethodID解析目标 Java 方法(如SpeechRecognizer、TextToSpeech相关方法)(android.rs)。因为 natively attached 线程只有系统类加载器,FindClass看不到应用类,而GetObjectClass(activity)永远可行——这是 Android JNI 场景下绕开类加载问题的标准手法。
system_speech只依赖makepad-android-state一个极小的 crate 便完整获得了 JNI 环境与 Activity 访问能力,恰恰验证了本库"解耦大框架、服务外部 crate"的设计初衷。
六、依赖策略与注意事项
刻意规避 path 依赖
Cargo.toml 中有一段值得注意的注释:
Note: we must not use local 'path' dependencies on
makepad-jni-sysin order to guarantee that only one instance of each crate exists in the app binary.
即:对makepad-jni-sys刻意不使用本地 path 依赖,而是使用 crates.io 版本依赖(0.4.0)。原因是JavaVM、Activity等 JNI 类型以原始指针形式跨 crate 传递,若makepad-jni-sys在依赖图中出现多个实例(path 依赖与版本依赖可能被去重逻辑拆成两份),类型与 ABI 就可能在编译期产生不一致,导致指针语义出错。保证"二进制中每种 crate 只有一份"是这套全局原始指针方案能够成立的前提。
使用建议:优先选用robius-android-env
README 在 Usage 一节给出了一条重要的实践建议:"you probably want to use therobius-android-envcrate instead of using this crate directly, or an even higher-level crate that depends onrobius-android-env."也就是说,普通外部项目应优先使用基于本库封装的更高级 crate(robius-android-env或依赖它的更高层 crate),把 JNI 环境的获取、线程附加、Activity 生命周期管理等样板逻辑交给成熟封装;只有当你的需求足够底层(例如自定义 JNI 胶水、极简依赖树)时,才直接使用本文介绍的两个函数。
使用时的三点提醒
- 判空不可省略:
get_java_vm()与get_activity()在对应状态未初始化时会返回空指针(源码注释 "If not initialized, returns a null pointer"),调用方必须先判空再解引用,例如system_speech中if vm.is_null() { return None; }(android.rs)。 - Activity 不缓存、随手取:Activity 会因旋转、分屏、resize 等系统动作销毁重建,务必在每次需要时重新调用
get_activity()。 - 写入路径不可外部调用:
get_activity_setter_fn()及JNI_OnLoad/jni_on_load均为#[doc(hidden)]/#[no_mangle]内部机制,外部代码只读不写。
七、总结
makepad-android-state用不到 100 行代码,解决了一个在 Android + Rust 混合开发中极易被搞错的问题——进程级 JNI 全局状态(JavaVM与Activity)如何被安全地持有与共享。其设计要点可归结为三条:拆分薄层隔离大框架(外部 crate 零成本接入)、读路径全开放(get_java_vm/get_activity随时取用)、写路径单次授权(Option::take()保证仅框架内部可设置 Activity)。理解这套机制,对任何需要在 Makepad Android 应用中接入原生 Java 能力(语音识别、TTS、OpenXR、相机、推送等)的开发者都具有直接的参考价值——它既是 Makepad 平台的 JNI 基础设施,也是 Rust 侧安全暴露 Android 上下文的可复用范例。
- 前端
- UI组件
- 3D渲染
- 跨平台
- 游戏开发
【免费下载链接】makepad
Makepad is a creative software development platform for Rust that compiles to wasm/webGL, osx/metal, windows/dx11 linux/opengl
相关推荐
终极Windows系统优化神器:WinUtil一键解决所有Windows管理难题
终极Windows系统优化神器:WinUtil一键解决所有Windows管理难题 还在为Windows系统卡顿、臃肿、难管理而烦恼吗?Chris Titus T
桌面应用运维Jetpack Compose 状态管理实战:深入理解 remember 与 State 的重组机制
Jetpack Compose 状态管理实战:深入理解 remember 与 State 的重组机制 本篇指南聚焦 Android 学习路线图中的 rememb
文档教程知识库Makepad 按钮控件(Button)完整指南:属性、样式与状态机制详解
Makepad 按钮控件(Button)完整指南:属性、样式与状态机制详解 本篇技术指南以 examples/uizoo/resources/button.md
前端UI组件3D渲染跨平台游戏开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考