Dioxus 原生插件实战:用 manganis::ffi 构建跨 Android/iOS 的 Geolocation 定位插件
本文以 Dioxus 官方示例 geolocation-native-plugin 为主体,完整拆解一个最小化原生插件应用的实现:如何通过 #[manganis::ffi] 宏自动生成本地 FFI 绑定,在 Android(Kotlin/JNI)与 iOS(Swift/ObjC)两端实现位置权限检查、权限请求弹窗与一次性定位(one-shot position)能力,并在 Web/桌面平台上通过 fallback 桩优雅降级。读完本文,你将掌握 Dioxus 原生插件的完整工程结构、Dioxus.toml 关键配置、Rust 侧 API 设计以及 dx 打包管线如何处理原生 Gradle/Swift 产物。
示例定位与能力概览
该示例位于 geolocation-native-plugin,是一个实现了原生插件的最小化 Dioxus 应用。按其 README 描述,插件提供三组能力:
- 使用 Android/iOS 原生权限对话框检查并申请位置权限;
- 配置一次性定位请求(高精度开关
enable_high_accuracy+ 最大缓存年龄maximum_age); - 检查最近一次上报的坐标、精度、海拔、航向与速度。
README 特别指出:该示例与任何插件 crate 共享同一套元数据管线——原生 Gradle/Swift 产物通过链接器符号(linker symbols)嵌入,并由 dx 自动打包进最终应用。这一点在 Native Plugin System 设计文档 中有系统性阐述。
运行示例
README 给出的启动方式(在仓库根目录执行):
dx serve --project examples/01-app-demos/geolocation --platform mobile
需要注意:仓库中示例的实际目录名是 geolocation-native-plugin,执行时请将该路径替换为真实目录名(README 中的 geolocation 路径与仓库实际目录不一致)。
运行前提与限制(来自 README 的明确说明):
- Android/iOS 平台需要安装对应工具链(Android SDK/NDK、Xcode),原生模块才能在构建阶段编译;
- UI 在桌面/Web 上同样可以运行,但由于插件只支持移动端目标,所有定位调用会返回错误,这些错误会内联展示在界面上——这也是该示例故意保留的行为,用于演示错误路径。
Cargo 侧的平台切换由 feature 控制,见 Cargo.toml:
[features]
default = ["mobile"]
web = ["dioxus/web"]
desktop = ["dioxus/desktop"]
mobile = ["dioxus/mobile"]
工程结构与文件布局
整个示例由「Rust 应用 + 插件 Rust 层 + Android 源码 + iOS 源码」四部分组成:
examples/01-app-demos/geolocation-native-plugin/
├── Cargo.toml # 依赖 dioxus/manganis/serde/thiserror,平台 features
├── Dioxus.toml # bundle 元数据、iOS/Android 配置、权限声明
└── src/
├── main.rs # UI 入口:权限卡片、定位卡片、Live Activity 卡片
├── plugin/
│ ├── mod.rs # Geolocation 结构体 + #[manganis::ffi] FFI 声明
│ ├── models.rs # 跨 FFI 的 serde 数据模型(JSON 序列化)
│ └── error.rs # 统一错误枚举
├── android/src/main/kotlin/com/dioxus/geolocation/
│ ├── GeolocationPlugin.kt # Kotlin 插件入口,桥接 JSON 同步调用
│ └── Geolocation.kt # 基于 FusedLocationProviderClient 的定位实现
└── ios/plugin/Sources/
├── GeolocationPlugin.swift # Swift 插件,CLLocationManager + Live Activity
└── LocationActivityAttributes.swift
Dioxus.toml:插件应用的清单配置
Dioxus.toml 展示了插件应用需要声明的完整清单(文件头指向 CLI schema packages/cli/schema.json):
[bundle]
identifier = "com.dioxuslabs.geolocation"
publisher = "Dioxus Labs"
[ios]
deployment_target = "16.2"
background_modes = ["location"]
[ios.plist]
NSSupportsLiveActivities = true
[[ios.widget_extensions]]
source = "src/ios/widget"
display_name = "Location Widget"
bundle_id_suffix = "location-widget"
deployment_target = "16.2"
module_name = "GeolocationPlugin"
[android]
min_sdk = 24
target_sdk = 34
features = ["android.hardware.location.gps"]
[permissions]
location = { precision = "fine", description = "Access your precise location to provide location-based services" }
各配置项的作用:
[bundle]:应用标识与发布者,生成各平台 bundle id;[ios] deployment_target = "16.2"与background_modes = ["location"]:定位能力要求 iOS 16.2 起可用后台定位模式;[ios.plist] NSSupportsLiveActivities:启用锁屏 Live Activity 能力(对应 Rust 侧start_live_activity等方法);[[ios.widget_extensions]]:声明一个独立的 Widget Extension 源码目录src/ios/widget,用于承载 Live Activity 的 UI(WidgetKit 要求 Activity UI 必须放在 Extension 中);[android]:最低 SDK 24、目标 SDK 34,并声明android.hardware.location.gps硬件特性;[permissions] location:统一权限清单。plugin/mod.rs 的注释说明了映射规则:CLI 会把location = { precision = "fine" }自动映射为 Android 的ACCESS_FINE_LOCATION(写入 AndroidManifest.xml)与 iOS 的NSLocationWhenInUseUsageDescription(写入 Info.plist),description即系统弹窗中的用途说明文案。
Rust 侧插件 API:Geolocation
数据模型(JSON 序列化契约)
跨 FFI 边界的所有数据都通过 serde 序列化为 JSON 字符串传递,模型定义在 models.rs:
| 类型 | 说明 |
|---|---|
PermissionState |
Prompt(默认值)、PromptWithRationale(Android 12+)、Granted、Denied,序列化用 kebab-case |
PermissionStatus |
含 location 与 coarse_location 两个状态。Android 上分别对应 ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION;iOS 上两者同值 |
PositionOptions |
enable_high_accuracy: bool(Android 12+ 未授予细粒度权限时被忽略)、timeout: u32(毫秒,默认 10000;Android 的 getCurrentPosition 会忽略该项,iOS 也忽略)、maximum_age: u32(毫秒,默认 0,iOS 忽略) |
Coordinates / Position |
十进制度坐标、精度(米)、可选的海拔/海拔精度/速度/航向,timestamp 为 Unix 纪元毫秒 |
LiveActivityResult / LiveActivityUpdate |
仅 #[cfg(target_os = "ios")] 编译,含 activity_id 与坐标精度 |
FFI 声明:#[manganis::ffi] 宏
plugin/mod.rs 是插件的核心,两个 extern 块按目标平台条件编译:
// iOS/macOS:宏自动生成全部 ObjC FFI 代码,路径指向 SwiftPM 包
#[cfg(any(target_os = "ios", target_os = "macos"))]
#[manganis::ffi("src/ios/plugin")]
unsafe extern "Swift" {
pub type GeolocationPlugin;
pub fn getCurrentPositionJson(this: &GeolocationPlugin, optionsJson: String) -> String;
pub fn checkPermissionsJson(this: &GeolocationPlugin) -> String;
pub fn requestPermissionsJson(this: &GeolocationPlugin, permissionsJson: String) -> String;
pub fn startLiveActivityJson(this: &GeolocationPlugin) -> String;
// ...
}
// Android:宏自动生成 JNI 代码,路径指向 Gradle 工程
#[cfg(target_os = "android")]
#[manganis::ffi("src/android")]
unsafe extern "Kotlin" {
pub type GeolocationPlugin;
pub fn getCurrentPositionJson(this: &GeolocationPlugin, optionsJson: String) -> String;
// ...
}
宏的三条职责在设计文档 11-NATIVE-PLUGIN-FFI.md 中列明:① 输出一类新的资产(源码目录);② 生成对应的 FFI 函数(运行时通过 JNI/ObjC 查找调用,而非静态链接);③ 打包器随后提取元数据并编译源码。该方案相对传统 build.rs 的优势在于:不绑定 Cargo、对 IDE 零干扰、缓存由 bundler 统一管理、且原生编译发生在 rustc 之后。
Geolocation 结构体本身是一个薄封装:懒初始化原生 GeolocationPlugin 实例,然后把 Rust 侧选项序列化为 JSON、调用 FFI 函数、解析返回的 JSON(若 JSON 中带有 error 字段则转换为 Error::LocationUnavailable)。对外暴露的方法即 UI 所需的全部 API:check_permissions()、request_permissions(Option<Vec<PermissionType>>)、get_current_position(Option<PositionOptions>),以及仅 iOS 可用的 start_live_activity() / update_live_activity() / end_live_activity()。
非原生平台 fallback
mod.rs 末尾 定义了 Web/桌面等目标的 fallback 模块:GeolocationPlugin::new() 与所有 *Json 函数统一返回 Error::PlatformBridge("Geolocation is only supported on Android, iOS, and macOS")。这样 UI 层代码无需任何条件编译即可在桌面/Web 上运行,只是每次调用都会把错误消息展示在界面的错误横幅里。错误模型定义在 error.rs,包括 Json、PlatformBridge、LocationUnavailable 与 iOS 专属的 LiveActivity 四类,且实现 Serialize 便于直接回传前端。
UI 层:main.rs 的信号驱动集成
main.rs 展示了插件与 Dioxus 信号系统的组合方式。App 组件用一组 use_signal 维护状态:geolocation(插件实例)、permission_status、last_position、error、use_high_accuracy(默认开启)与 max_age_input(默认 "0")。
关键事件处理模式:
let on_fetch_position = move |_| {
let maximum_age = max_age_input.read().trim().parse::<u32>().unwrap_or(0);
let options = PositionOptions {
enable_high_accuracy: use_high_accuracy(),
timeout: 10_000,
maximum_age,
};
match geolocation.write().get_current_position(Some(options)) {
Ok(position) => { last_position.set(Some(position)); error.set(None); }
Err(err) => error.set(Some(err.to_string())),
}
};
注意 timeout 固定为 10 秒(与 PositionOptions 的默认值一致),maximum_age 由界面输入框解析而来,解析失败回退为 0(要求实时定位)。界面由三张卡片构成:
- Permissions:Check / Request 两个按钮,点击后用
PermissionBadge展示location与coarse_location的状态(Granted/Denied/Needs prompt); - Current position:高精度开关按钮 + “Max cached age (ms)” 数字输入 + “Get current position” 按钮,成功后以
CoordinateRow网格展示经纬度(6 位小数)、精度、海拔、海拔精度、速度、航向与时间戳; - Live Activity:仅
#[cfg(target_os = "ios")]编译的卡片,提供 Start / Update / End 三个按钮;非 iOS 平台该组件编译为空VNode::empty(),UI 代码因此保持跨平台可编译。
Android 端实现
Kotlin 侧由两个类组成。Geolocation.kt 负责真正的定位:
sendLocation:先检查 Google Play Services 可用性与系统定位开关,再根据enable_high_accuracy选择 FusedLocationProviderClient 的优先级——高精度用PRIORITY_HIGH_ACCURACY,否则在网络 provider 可用时用PRIORITY_BALANCED_POWER_ACCURACY,否则PRIORITY_LOW_POWER;getLastLocation(maximumAge):遍历所有 provider 的getLastKnownLocation,以纳秒级elapsedRealtimeNanos比较缓存年龄,满足maximumAge约束时直接返回缓存点,避免一次新的卫星/网络定位往返。
GeolocationPlugin.kt 是 Rust JNI 层调用的桥接入口:
checkPermissionsJson():用ContextCompat.checkSelfPermission分别检查细/粗粒度权限,映射为granted|denied|prompt三态 JSON;requestPermissionsJson():通过ActivityCompat.requestPermissions触发系统授权对话框(request code 1001),用CountDownLatch最多等待 5 秒后返回最新状态;getCurrentPositionJson(optionsJson):解析enableHighAccuracy/timeout/maximumAge(默认 10000ms/0ms),先尝试getLastLocation命中缓存,未命中则发起 FusedLocation 请求,并用Timer实现超时——等待timeout + 2000ms后若仍未取到结果,返回{"error":"Timeout waiting for location."};locationToPositionJson():把Location转为与 RustPosition模型 camelCase 对齐的 JSON(含 Android 8+ 的verticalAccuracyMeters)。
iOS 端实现
Swift 侧 GeolocationPlugin.swift 是一个 NSObject + CLLocationManagerDelegate:
getCurrentPositionJson(_:):若CLLocationManager.location缓存点满足maximumAge则立即返回;否则设置desiredAccuracy(高精度kCLLocationAccuracyBest/ 低精度kCLLocationAccuracyKilometer),状态未定时先requestWhenInUseAuthorization(),然后requestLocation()发起单次请求,通过信号量 + RunLoop 轮询在超时窗口内同步等待回调;超时返回{"error":"Timeout waiting for location"};checkPermissionsJson():把CLAuthorizationStatus映射为prompt(notDetermined)/denied(restricted、denied)/granted(authorizedAlways、authorizedWhenInUse),coarseLocation与location同值;若系统定位总开关关闭则直接返回错误 JSON;- Live Activity 三个方法基于 ActivityKit(
Activity<LocationPermissionAttributes>),要求 iOS 16.2+ 且areActivitiesEnabled,startLiveActivityJson返回 activity ID 与当前坐标,endLiveActivityJson以.immediate策略结束所有活动。
打包管线:linker 元数据如何进入 dx
README 提到该示例“shares the same metadata pipeline as any plugin crate”。结合 设计文档 可以理解为:#[manganis::ffi] 宏在编译期把源码目录信息以链接器符号形式嵌入二进制,dx 在 rustc 之后的 bundler 阶段提取这些 PluginMeta 条目,然后分别走 Swift(swiftc)或 Kotlin/Gradle 编译管线,把产物链接进最终应用包;对源文件做哈希缓存,未变更则跳过编译。从源码结构看,本示例目录中没有 build.rs,原生编译完全由这条 bundler 管线承担——这也是该设计文档选择“linker metadata”而非 build.rs 的核心动机(不拖慢 rust-analyzer、缓存可控、与构建系统解耦)。
实验清单(Things to try)
按 README 建议的操作顺序验证插件行为:
- 点击 Check permissions 查看 OS 当前授权状态(granted/denied/prompt);
- 点击 Request permissions 从应用内触发原生授权对话框;
- 切换 High accuracy 开关、设置 Max cached age 后再请求当前定位;
- 观察每次新读数到达时坐标网格的更新,或操作失败时的错误横幅(权限被拒、运行在不支持的平台等)。
在桌面/Web 上运行第 4 步时,错误横幅会显示 fallback 的 PlatformBridge 消息,这正好验证了跨平台降级路径的行为符合 README 的预期。
参考路径索引
- 示例说明:examples/01-app-demos/geolocation-native-plugin/README.md
- 清单配置:examples/01-app-demos/geolocation-native-plugin/Dioxus.toml
- Rust 插件层:mod.rs / models.rs / error.rs
- UI 入口:src/main.rs
- Android 原生端:GeolocationPlugin.kt / Geolocation.kt
- iOS 原生端:GeolocationPlugin.swift
- 插件系统设计文档:notes/architecture/11-NATIVE-PLUGIN-FFI.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00