前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

这篇文章不是“抄官方文档”的泛泛教程,
而是基于一个真实项目的完整排障与修复过程整理出来的实战记录。

项目目标很明确:
在鸿蒙真机上,
flutter_rust_bridge + cargokit 这条链路跑通,
并且把最难的 Rust -> Dart callback 链路做到稳定可回归。
在这里插入图片描述
如果你也遇到过下面这些报错,
这篇文章就是写给你的:

  • flutter_rust_bridge has not been initialized
  • Dart_InitializeApiDL: symbol not found
  • callback 一跑就崩
  • 清理后偶现“上一轮能跑,这一轮不行”

欢迎先收藏,
然后按目录一步步照着做。

本文适用于:希望把 Flutter + Rust 真正落地到 OpenHarmony/HarmonyOS 真机的开发者。

本文强调:不是“临时跳过测试”,而是把核心链路修到可复现、可回归、可解释。

目录

  1. 一、项目背景与适配目标
  2. 二、环境准备与版本矩阵
  3. 三、三方库关系图与调用链
  4. 四、问题复盘:为什么一直报未初始化
  5. 五、根因一:OHOS 下 Dart API DL 初始化缺失
  6. 六、根因二:C 文件被编成 Mach-O 不是 ELF
  7. 七、根因三:重复符号导致链接冲突
  8. 八、Demo 侧如何做“可测、可看、可回归”
  9. 九、真机验证:Callback 三轮压测 + 全量通过
  10. 十、为什么 cargo 会很慢?如何提速
  11. 十一、常见坑位与排查清单
  12. 十二、可复用的鸿蒙适配模板
  13. 总结

一、项目背景与适配目标

1.1 业务背景

我们有一个 Flutter 项目,
其中核心能力由 Rust 提供,
并通过 flutter_rust_bridge 对外暴露接口。

在 Android/iOS 上这条链路通常问题不大,
但迁移到鸿蒙后,
会出现一些“看起来像初始化问题,实际上是构建链路问题”的复合故障。

1.2 本次适配目标

本次不是只修“能跑一下”,
而是分三层目标推进:

  1. 功能层:Flutter 可以调用 Rust,Rust 也能回调 Dart。
  2. 稳定层:真机多轮运行稳定,去掉 callback 的 skip。
  3. 工程层:清理缓存后重编译仍可复现,不靠“玄学状态”。

1.3 结果先看

最终结果:

  • callback 定向测试:真机连续 3 轮通过。
  • integration_test/simple_test.dart:全量 8 项通过。
  • 关键初始化与动态库符号问题全部闭环。

二、环境准备与版本矩阵

2.1 核心环境

项目 版本/信息 说明
Flutter SDK 3.35.8-ohos-0.0.2 鸿蒙分支 Flutter
设备 FMR0223825079397 HarmonyOS 真机
系统 HarmonyOS 6.x 真实设备验证
FRB 本地 path 依赖 可直接改源码
Cargokit 本地 path 依赖 与 FRB 联动

2.2 目录布局(关键)

new/
├── flutter_rust_bridge/
│   ├── frb_dart/
│   └── frb_rust/
├── cargokit/
└── frb_ohos_demo/
    ├── lib/
    ├── rust/
    ├── integration_test/
    └── ohos/

这套布局的好处是:

  • 你能直接修改三方库源码并立刻在 demo 验证。
  • 避免“你以为改了,实际依赖还是远端版本”的错觉。

2.3 依赖方式(必须本地 path)

# frb_ohos_demo/pubspec.yaml
dependencies:
  flutter_rust_bridge:
    path: ../flutter_rust_bridge/frb_dart
  rust_lib_frb_ohos_demo:
    path: rust_builder
# frb_ohos_demo/rust/Cargo.toml
[dependencies]
flutter_rust_bridge = { path = "../../flutter_rust_bridge/frb_rust" }

如果你不用 path 依赖,后续很多修复根本不会生效在当前 demo 上。


三、三方库关系图与调用链

3.1 三方库各自做什么

组件 作用 你需要关注什么
frb_dart Dart 侧加载动态库、调用入口 RustLib.init() 初始化顺序
frb_rust Rust 侧编解码、回调、线程与 FFI dart_api_dl 初始化与符号
cargokit Rust 构建与产物拷贝到平台工程 构建目标三元组是否正确

3.2 调用链路(从按钮到回调)

Flutter UI

frb_dart generated API

DynamicLibrary.open(librust_lib_*.so)

frb_rust entrypoint

Rust business logic

Rust -> Dart callback

Dart callback closure

链路看起来短,
但鸿蒙上最容易出问题的恰恰是 C 动态链接 + Dart API DL 这一层。

四、问题复盘:为什么一直报未初始化

4.1 典型报错现象

在页面上你会看到类似:

Action: Call Rust 'greet("Tom")'
Result: 'Rust call failed: Bad state: flutter_rust_bridge has not been initialized...'

以及测试阶段:

Failed to load dynamic library 'librust_lib_frb_ohos_demo.so':
Error relocating ...: Dart_InitializeApiDL: symbol not found

4.2 “未初始化”不一定是你没 init()

很多人第一反应是:
“我是不是忘记 await RustLib.init(); 了?”

这次实战证明:
不一定

你确实可能已经调用了 init()
但由于动态库加载期符号解析失败,
Dart 侧看起来就像“没初始化”。

4.3 先定位,再修复

我们按照这个顺序排查:

  1. 看测试日志与 hilog,确认是加载阶段还是调用阶段崩。
  2. nm.so 符号是未定义还是已导出。
  3. 看 build script 输出,确认 C 文件到底被谁编译成什么格式。
  4. 再决定改 io.rsbuild.rs 还是 demo 侧构建脚本。

五、根因一:OHOS 下 Dart API DL 初始化缺失

5.1 问题本质

dart-opaque 回调链路会用到 Dart_*PersistentHandle* 相关符号。
如果 Dart_InitializeApiDL 没有正确执行,
这条链路一定炸。

5.2 修复点(frb_rust)

文件:
new/flutter_rust_bridge/frb_rust/src/ffi_binding/io.rs

#[no_mangle]
pub unsafe extern "C" fn frb_init_frb_dart_api_dl(data: *mut std::ffi::c_void) -> isize {
    #[cfg(feature = "dart-opaque")]
    return dart_sys::Dart_InitializeApiDL(data);
    #[cfg(not(feature = "dart-opaque"))]
    return 0;
}

5.3 这段逻辑的意义

  • 当启用 dart-opaque 时,必须走 Dart_InitializeApiDL
  • 这不是“可选优化”,是 callback 正常工作的前置条件。

经验:只要你要 Rust 回调 Dart,就把 dart_api_dl 初始化当成强约束。


六、根因二:C 文件被编成 Mach-O 不是 ELF

6.1 关键异常

我们在构建输出里看到了这一行:

running: "cc" ... "-c" "src/dart_api/dart_api_dl.c"

在跨到 OHOS 目标时,
如果这里还走宿主 cc
就很容易产出 Mach-O 对象,
而非 OHOS 需要的 ELF

这会带来两类后果:

  • 动态加载时符号无法解析;
  • 链接过程出现 “archive member is neither ET_REL nor LLVM bitcode”。

6.2 修复点(build.rs)

文件:
new/flutter_rust_bridge/frb_rust/build.rs

if std::env::var("CARGO_CFG_TARGET_ENV")
    .map(|x| x == "ohos")
    .unwrap_or(false)
{
    let mut build = cc::Build::new();
    if let Ok(linker) = std::env::var("RUSTC_LINKER") {
        build.compiler(linker);
    }

    build
        .file("src/dart_api/dart_api_dl.c")
        .include("src/dart_api")
        .warnings(false);

    if let Ok(target) = std::env::var("TARGET") {
        build.flag(&format!("--target={target}"));
    }

    build.compile("frb_dart_api_dl");
}

6.3 修复后如何验证

nm -u rust_builder/ohos/.cxx/.../librust_lib_frb_ohos_demo.so \
  | rg 'Dart_InitializeApiDL|Dart_NewPersistentHandle_DL'

如果没有未定义输出,
说明这条符号问题已被吃掉。


七、根因三:重复符号导致链接冲突

7.1 为什么会重复

一开始为了兜底,
io.rs 里手工导出了若干 Dart_*_DL 全局。

但后来 dart_api_dl.c 也正确编译并参与链接后,
两边都定义同名符号,
就会触发 duplicate symbol。

7.2 冲突日志(典型)

ld.lld: error: duplicate symbol: Dart_CurrentIsolate_DL
ld.lld: error: duplicate symbol: Dart_NewPersistentHandle_DL

7.3 修复原则

  • 保留一种权威定义来源即可。
  • 在本案例里:让 dart_api_dl.c 负责这组符号。
  • 删除 io.rs 中 OHOS 专用的手工全局定义。

7.4 另一个隐藏冲突:demo 重复注入

demo 的 rust/build.rs 一开始也手动编译了 dart_api_dl.c
这会与 FRB 自身注入形成双份静态库。

最终做法是:

// new/frb_ohos_demo/rust/build.rs
fn main() {}

统一让三方库自己管理 dart_api_dl,demo 不再重复注入。


八、Demo 侧如何做“可测、可看、可回归”

8.1 扩展一个最小 callback API

Rust 侧提供轻量 callback 接口,
避免被复杂业务噪音干扰。

pub fn rust_call_dart_ping(callback: impl Fn() -> DartFnFuture<()>) -> String {
    callback();
    "pong-from-rust".to_string()
}

8.2 Dart 侧集成测试样例

testWidgets('Can perform Rust->Dart callback', (tester) async {
  await RustLib.init();
  var called = false;
  final result = await api.rustCallDartPing(callback: () async {
    called = true;
  });
  expect(called, true);
  expect(result, contains('pong'));
});

8.3 页面侧做“可视化回执”

建议把每个能力都做一行状态展示:

  • init 是否成功
  • sync/async 接口是否通过
  • stream 是否收到事件
  • callback 是否触发
  • opaque 对象生命周期是否正常

这样出现回归时,
你在 UI 上一眼就能定位是哪一段链路挂了。

8.4 测试矩阵建议

测试类别 建议用例 目标
初始化 RustLib.init() 早发现加载失败
同步调用 greet_sync 验证基础 FFI
异步调用 greet 验证线程与回传
回调 Rust->Dart callback 验证最易崩链路
Stream rust stream to dart 验证持续事件通路

九、真机验证:Callback 三轮压测 + 全量通过

9.1 单项压测(callback)

flutter test integration_test/simple_test.dart \
  -d FMR0223825079397 \
  --plain-name "Can perform Rust->Dart callback"

建议做 3~10 轮,
每轮都包含:

  1. 构建 hap
  2. 安装到真机
  3. 启动能力并 attach
  4. 执行 callback 用例
  5. 卸载与回收

9.2 全量回归

flutter test integration_test/simple_test.dart -d FMR0223825079397

本次结果:

  • callback 连续 3 轮通过;
  • 全量 8 项全部 All tests passed

9.3 一组实用排查命令

# 设备是否在线
hdc list targets

# 只跑回调用例
flutter test integration_test/simple_test.dart \
  -d FMR0223825079397 \
  --plain-name "Can perform Rust->Dart callback" -v

# 查看符号
nm -g rust_builder/ohos/.cxx/.../librust_lib_frb_ohos_demo.so \
  | rg 'Dart_InitializeApiDL|frb_init_frb_dart_api_dl'

9.4 验证结果记录表

轮次 callback 全量测试 备注
Round 1 通过 - 冷编译
Round 2 通过 - 连续回归
Round 3 通过 - 连续回归
Full - 8/8 通过 真机执行

十、为什么 cargo 会很慢?如何提速

10.1 慢的根本原因

鸿蒙目标是交叉编译,
每次清理后都要重新做:

  • Rust 依赖编译
  • C 依赖编译
  • 链接与 strip
  • hap 打包与签名

所以你看到 cargo 慢是正常现象,
特别是第一次构建。

10.2 提速建议(非常实用)

  • 不要每次都 flutter clean + cargo clean
  • 稳定阶段优先增量构建。
  • 只在“确认构建链修复有效性”时做全量 clean。
  • 把 callback 用例拆成 --plain-name 单测先跑。

10.3 推荐流程

  1. 改一处源码。
  2. 跑 callback 定向测试。
  3. 再跑全量测试。
  4. 最后才做一次 clean 验证闭环。

这个流程比“每次全清理重来”快非常多。


十一、常见坑位与排查清单

11.1 设备明明在线,Flutter 却说找不到

现象:

  • hdc list targets 有设备;
  • flutter test -d <id> 报无设备。

排查思路:

  • 看 Flutter 调的是哪个 hdc 二进制。
  • 确认 DevEco toolchain 路径一致。
  • 断开重连设备后再试。

11.2 “Unable to locate an OpenHarmony SDK”

这个提示在某些日志里会出现,
不一定代表真失败。

关键是看后续是否能:

  • 成功 build hap;
  • 安装到设备;
  • 拉起 ability 并执行测试。

11.3 误区清单

  • 误区 1:只改 demo,不改三方库核心逻辑。
  • 误区 2:只看 Dart 报错,不看动态库符号。
  • 误区 3:遇到 callback 崩溃就直接 skip。
  • 误区 4:把“偶尔成功”当成“已经修复”。

11.4 发布前自检表

检查项 通过标准 是否建议自动化
动态库符号 无未定义 Dart_*_DL
callback 用例 连续 3 轮通过
全量集成测试 全通过
clean 后回归 至少 1 次通过
日志归档 有构建与测试记录

十二、可复用的鸿蒙适配模板

12.1 建议你固定下来的模板结构

# 统一本地依赖,避免“改了不生效”
flutter_rust_bridge:
  path: ../flutter_rust_bridge/frb_dart
# Rust 端同样固定 path
flutter_rust_bridge = { path = "../../flutter_rust_bridge/frb_rust" }
# 推荐回归脚本
hdc list targets
flutter test integration_test/simple_test.dart \
  -d FMR0223825079397 \
  --plain-name "Can perform Rust->Dart callback"
flutter test integration_test/simple_test.dart -d FMR0223825079397

12.2 可以继续演进的方向

后续你可以继续做:

  1. callback 压测升级到 10~30 轮;
  2. 引入自动化脚本输出成功率;
  3. 把回调链路扩展到带参数、异常、取消场景;
  4. 增加 CI 上的模拟检查(符号与构建链静态检查)。

12.3 一份“可取消 skip”的策略建议

  • 第一阶段:只恢复最小 callback 用例,不跳过。
  • 第二阶段:连续 3 轮稳定后再恢复复杂 callback。
  • 第三阶段:全量跑通后,把 skip 彻底移除并固化到回归脚本。

这比“一口气全开”更稳。


附:关键修复文件一览

文件 修改目的
new/flutter_rust_bridge/frb_rust/src/ffi_binding/io.rs 修正 OHOS 下 Dart API DL 初始化与符号策略
new/flutter_rust_bridge/frb_rust/build.rs 强制 C 文件使用 OHOS 交叉编译器
new/frb_ohos_demo/rust/build.rs 移除 demo 侧重复注入,避免冲突

参考链接(建议收藏)


总结

这次适配最大的收获,
不是“某个报错消失了”,
而是把一条跨语言、跨工具链、跨平台的链路彻底打通了:

  • 调用层:Flutter -> Rust 正常;
  • 回调层:Rust -> Dart callback 正常;
  • 构建层:OHOS 交叉编译目标正确;
  • 工程层:可清理、可重编译、可回归。

如果你正在做 Flutter + Rust 的鸿蒙落地,
建议你把本文的三件事记住:

  1. dart_api_dl 初始化必须可验证;
  2. C 编译目标必须是 OHOS 交叉链;
  3. 不要重复定义 Dart_*_DL 符号。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐