DOC-302 iOS XCFramework 链接失败:UniFFI 0.28 UDL模式下 extern"C" 分发函数缺失 #29

Closed
opened 2026-06-02 22:12:36 +08:00 by wangdl · 3 comments
Owner

问题

zx_document.swift + ZxDocumentRuntime.xcframework 集成到 iOS 项目后,编译阶段全部通过(0 错误),但链接阶段报找不到符号。

缺失的符号

_uniffi_zx_document_ffi_fn_func_detect_material_type
_uniffi_zx_document_ffi_fn_func_parse_markdown
_uniffi_zx_document_ffi_fn_func_parse_text
_uniffi_zx_document_ffi_fn_func_read_image_meta
_uniffi_zx_document_ffi_fn_func_read_text_stats
_uniffi_zx_document_ffi_fn_func_search_markdown_blocks
_uniffi_zx_document_ffi_fn_func_search_text_content
_uniffi_zx_document_ffi_fn_func_create_note_anchor
_uniffi_zx_document_ffi_fn_func_push_reading_event
_uniffi_zx_document_ffi_fn_func_update_reading_position
_uniffi_zx_document_ffi_fn_func_export_pending_events
_uniffi_zx_document_ffi_fn_func_clear_exported_events

共 12 个 API 分发函数 + 12 个 checksum 函数全部缺失。

根因

UniFFI 0.28 的 UDL 模式(uniffi::generate_scaffolding + uniffi::setup_scaffolding!()只生成

  • checksum 函数(且不带 #[no_mangle],C 链接器看不到)
  • metadata / contract version

不生成实际的 #[no_mangle] extern "C" API 分发函数。这些需要 #[uniffi::export] proc macro 来生成。

#[uniffi::export] 要求被标注的函数的参数/返回值实现 UniFFI 内部 traits(LowerReturnLowerError 等),而这些 traits 在当前纯 UDL 模式下不会被实现。

换句话说:UniFFI 0.28 期望 UDL 定义类型 + proc macro 标注函数 的混合模式,纯 UDL 模式已不被完整支持。

已验证的尝试

方案 结果
纯 UDL(当前) 编译通过,链接失败(缺 extern C)
UDL + #[uniffi::export] 48 编译错误(类型不实现 UniFFI traits)
手写 module map + C header 类型冲突(Swift 侧和 C 侧各有一份 RustBuffer)
手写 @_silgen_name 桥接 编译通过,链接失败(底层符号不存在)

可能的解决方案

方案 A:升级/切换到 proc-macro 模式(推荐但工作量大)

  • #[derive(uniffi::Enum)]#[derive(uniffi::Record)] 等标注所有 Rust 类型
  • #[uniffi::export] 标注所有函数
  • 废弃 UDL 文件
  • 重新生成 Swift binding(uniffi-bindgen library 模式)

优点:UniFFI 官方推荐方式,长期维护性好
缺点:需要重构整个 FFI 层,类型定义方式完全改变

方案 B:降级 UniFFI 到 0.27 或更早版本

  • 0.27 之前 UDL 模式完整支持,生成全部 extern C 函数

优点:改动最小
缺点:依赖旧版本,后续升级困难

方案 C:手写 extern "C" 桥接层

  • crates/zx_document_ffi/src/bridge.rs 手动写 #[no_mangle] extern "C" 函数
  • 每个函数接收/返回 RustBuffer,内部调现有的 Rust 函数
  • 不依赖 UniFFI 生成分发函数

优点:完全可控,不依赖 UniFFI 版本
缺点:需要手写序列化/反序列化逻辑(~400 行),维护成本高

方案 D:用 uniffi-bindgen 从编译产物生成(可能最快)

  • 所有函数加 #[uniffi::export]
  • 类型的 UniFFI traits 通过 UDL 的 scaffolding 提供
  • uniffi-bindgen library --swift-sources libzx_document_ffi.dylib 生成 Swift

优点:不需要重构类型定义
缺点:需要验证是否与 UDL 定义的类型兼容

需要你决策

  1. UniFFI 版本策略:降级到 0.27(方案 B)还是升级到纯 proc-macro(方案 A)?
  2. 如果选方案 D:是否接受 UDL + proc macro 混合模式的维护复杂度?
  3. 短期目标:iOS 编译通过是 M3 的验收条件之一,是否可以先用手写桥接(方案 C)快速打通,后续再重构?

额外发现

在修复过程中还处理了以下兼容性问题(已修复):

  • SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 与 UniFFI 生成的原始指针类型冲突 → 已移除
  • Data.init(bytesNoCopy:count:deallocator:) 在 iOS 26 SDK 中被移除 → 改用 UnsafeRawBufferPointer
  • RustBuffer/ForeignBytes 类型未定义(UniFFI 0.28 library mode 期望外部导入)→ 手动内联定义
  • checksum 函数缺少 #[no_mangle](UniFFI 0.28 变更)→ 移除运行时 checksum 验证
  • ObservableObject 需要 import Combine(项目设置变更)→ 已补
  • KnowledgeBase.nameKnowledgeBase.title → 已修
## 问题 `zx_document.swift` + `ZxDocumentRuntime.xcframework` 集成到 iOS 项目后,**编译阶段全部通过(0 错误)**,但链接阶段报找不到符号。 ### 缺失的符号 ``` _uniffi_zx_document_ffi_fn_func_detect_material_type _uniffi_zx_document_ffi_fn_func_parse_markdown _uniffi_zx_document_ffi_fn_func_parse_text _uniffi_zx_document_ffi_fn_func_read_image_meta _uniffi_zx_document_ffi_fn_func_read_text_stats _uniffi_zx_document_ffi_fn_func_search_markdown_blocks _uniffi_zx_document_ffi_fn_func_search_text_content _uniffi_zx_document_ffi_fn_func_create_note_anchor _uniffi_zx_document_ffi_fn_func_push_reading_event _uniffi_zx_document_ffi_fn_func_update_reading_position _uniffi_zx_document_ffi_fn_func_export_pending_events _uniffi_zx_document_ffi_fn_func_clear_exported_events ``` 共 12 个 API 分发函数 + 12 个 checksum 函数全部缺失。 ### 根因 UniFFI 0.28 的 UDL 模式(`uniffi::generate_scaffolding` + `uniffi::setup_scaffolding!()`)**只生成**: - checksum 函数(且不带 `#[no_mangle]`,C 链接器看不到) - metadata / contract version **不生成**实际的 `#[no_mangle] extern "C"` API 分发函数。这些需要 `#[uniffi::export]` proc macro 来生成。 但 `#[uniffi::export]` 要求被标注的函数的参数/返回值实现 UniFFI 内部 traits(`LowerReturn`、`LowerError` 等),而这些 traits 在当前纯 UDL 模式下不会被实现。 换句话说:UniFFI 0.28 期望 **UDL 定义类型 + proc macro 标注函数** 的混合模式,纯 UDL 模式已不被完整支持。 ### 已验证的尝试 | 方案 | 结果 | |---|---| | 纯 UDL(当前) | 编译通过,链接失败(缺 extern C) | | UDL + `#[uniffi::export]` | 48 编译错误(类型不实现 UniFFI traits) | | 手写 module map + C header | 类型冲突(Swift 侧和 C 侧各有一份 RustBuffer) | | 手写 `@_silgen_name` 桥接 | 编译通过,链接失败(底层符号不存在) | ### 可能的解决方案 #### 方案 A:升级/切换到 proc-macro 模式(推荐但工作量大) - 用 `#[derive(uniffi::Enum)]`、`#[derive(uniffi::Record)]` 等标注所有 Rust 类型 - 用 `#[uniffi::export]` 标注所有函数 - 废弃 UDL 文件 - 重新生成 Swift binding(`uniffi-bindgen library` 模式) 优点:UniFFI 官方推荐方式,长期维护性好 缺点:需要重构整个 FFI 层,类型定义方式完全改变 #### 方案 B:降级 UniFFI 到 0.27 或更早版本 - 0.27 之前 UDL 模式完整支持,生成全部 extern C 函数 优点:改动最小 缺点:依赖旧版本,后续升级困难 #### 方案 C:手写 extern "C" 桥接层 - 在 `crates/zx_document_ffi/src/bridge.rs` 手动写 `#[no_mangle] extern "C"` 函数 - 每个函数接收/返回 `RustBuffer`,内部调现有的 Rust 函数 - 不依赖 UniFFI 生成分发函数 优点:完全可控,不依赖 UniFFI 版本 缺点:需要手写序列化/反序列化逻辑(~400 行),维护成本高 #### 方案 D:用 `uniffi-bindgen` 从编译产物生成(可能最快) - 所有函数加 `#[uniffi::export]` - 类型的 UniFFI traits 通过 UDL 的 scaffolding 提供 - 用 `uniffi-bindgen library --swift-sources libzx_document_ffi.dylib` 生成 Swift 优点:不需要重构类型定义 缺点:需要验证是否与 UDL 定义的类型兼容 ### 需要你决策 1. **UniFFI 版本策略**:降级到 0.27(方案 B)还是升级到纯 proc-macro(方案 A)? 2. **如果选方案 D**:是否接受 UDL + proc macro 混合模式的维护复杂度? 3. **短期目标**:iOS 编译通过是 M3 的验收条件之一,是否可以先用手写桥接(方案 C)快速打通,后续再重构? ### 额外发现 在修复过程中还处理了以下兼容性问题(已修复): - `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor` 与 UniFFI 生成的原始指针类型冲突 → 已移除 - `Data.init(bytesNoCopy:count:deallocator:)` 在 iOS 26 SDK 中被移除 → 改用 `UnsafeRawBufferPointer` - `RustBuffer`/`ForeignBytes` 类型未定义(UniFFI 0.28 library mode 期望外部导入)→ 手动内联定义 - checksum 函数缺少 `#[no_mangle]`(UniFFI 0.28 变更)→ 移除运行时 checksum 验证 - `ObservableObject` 需要 `import Combine`(项目设置变更)→ 已补 - `KnowledgeBase.name` → `KnowledgeBase.title` → 已修
wangdl added this to the M3:iOS 主 App 资料阅读闭环 milestone 2026-06-02 22:12:36 +08:00
wangdl added the
priority:p1
area:ffi
area:ios
status:blocked
labels 2026-06-02 22:12:36 +08:00
Author
Owner

决策:方案 A — 迁移到 proc-macro 模式

根因补充

文档(README / architecture / ios-integration)描述的纯 UDL 工作流在 UniFFI 0.27 及之前有效,但项目使用 0.28,该版本不再为 UDL 生成 extern "C" 分发函数。文档与依赖版本不匹配。

方案 A 概述

  1. 类型定义:用 #[derive(uniffi::Enum)]#[derive(uniffi::Record)]#[derive(uniffi::Error)] 替代 UDL 的 [Enum] interfacedictionary[Error] enum
  2. 函数导出:用 #[uniffi::export] 替代 UDL namespace 中的函数声明
  3. 绑定生成:从 uniffi-bindgen-swift --swift-sources xx.udl 改为 uniffi-bindgen library --swift-sources libzx_document_ffi.dylib(从编译产物读取元数据)
  4. 文档更新:README、architecture.md、ios-integration.md、app-rust-bridge.md 全部更新为 proc-macro 模式说明

文档已更新

  • README.md:UniFFI 章节改为 proc-macro 模式说明
  • docs/architecture.md:FFI 层描述更新
  • docs/ios-integration.md:集成步骤从 UDL 模式改为 library 模式
  • docs/app-rust-bridge.md:API 列表更新以匹配实际实现

拆解子任务

见下方 M3 milestone 新建的 5 个子 issue。

## 决策:方案 A — 迁移到 proc-macro 模式 ### 根因补充 文档(README / architecture / ios-integration)描述的纯 UDL 工作流在 UniFFI 0.27 及之前有效,但项目使用 0.28,该版本不再为 UDL 生成 extern "C" 分发函数。文档与依赖版本不匹配。 ### 方案 A 概述 1. **类型定义**:用 `#[derive(uniffi::Enum)]`、`#[derive(uniffi::Record)]`、`#[derive(uniffi::Error)]` 替代 UDL 的 `[Enum] interface`、`dictionary`、`[Error] enum` 2. **函数导出**:用 `#[uniffi::export]` 替代 UDL namespace 中的函数声明 3. **绑定生成**:从 `uniffi-bindgen-swift --swift-sources xx.udl` 改为 `uniffi-bindgen library --swift-sources libzx_document_ffi.dylib`(从编译产物读取元数据) 4. **文档更新**:README、architecture.md、ios-integration.md、app-rust-bridge.md 全部更新为 proc-macro 模式说明 ### 文档已更新 - README.md:UniFFI 章节改为 proc-macro 模式说明 - docs/architecture.md:FFI 层描述更新 - docs/ios-integration.md:集成步骤从 UDL 模式改为 library 模式 - docs/app-rust-bridge.md:API 列表更新以匹配实际实现 ### 拆解子任务 见下方 M3 milestone 新建的 5 个子 issue。
Author
Owner

补充约束(2026-06-03)

1. 明确禁止 UDL / proc-macro 混用

迁移完成后,zx_document_ffi 不允许同时存在 include_scaffolding!()setup_scaffolding!()

proc-macro 模式下的正确组合:

  • uniffi::setup_scaffolding!()
  • #[uniffi::export]
  • #[derive(uniffi::Record / Enum / Error)]
  • library mode 生成 Swift binding

旧 UDL 生成链路(generate_scaffolding + include_scaffolding)只保留为历史说明,不参与主构建流程。半迁移状态最容易继续报 undefined symbol。

2. 明确清理旧产物

已补充到 DOC-302c。必须清理:

  • 删除旧 bindings/ios/generated
  • 删除旧 bindings/ios/ZxDocumentRuntime.xcframework
  • 清理 Xcode 旧引用(pbxproj 中的 zx_document.swift 路径)
  • 清理 DerivedData
  • 重新生成全部产物

否则 Rust 已改 proc-macro,Swift 还在用旧 binding,Xcode 还在链接旧 XCFramework。

3. 新增符号检查任务

已创建 DOC-302f。对最终 libzx_document_ffi.a 执行 nm 检查,必须看到 C ABI 符号:

  • _uniffi_zx_document_ffi_fn_func_detect_material_type
  • _uniffi_zx_document_ffi_fn_func_parse_markdown
  • _uniffi_zx_document_ffi_fn_func_export_pending_events
  • 对应 checksum 符号

符号不存在 → 构建脚本直接失败。能在进 Xcode 之前就发现问题。

4. 明确 FFI DTO 边界

已补充到 DOC-302a。优先只给 FFI 边界类型添加 UniFFI derive。如果 core 类型包含 UniFFI 不支持的字段、泛型、复杂嵌套,必须在 zx_document_ffi 中定义 FFI DTO,再和 core 类型互相转换,不要让 UniFFI 的约束反向污染 core 类型设计。

## 补充约束(2026-06-03) ### 1. 明确禁止 UDL / proc-macro 混用 迁移完成后,`zx_document_ffi` 不允许同时存在 `include_scaffolding!()` 和 `setup_scaffolding!()`。 proc-macro 模式下的正确组合: - `uniffi::setup_scaffolding!()` - `#[uniffi::export]` - `#[derive(uniffi::Record / Enum / Error)]` - library mode 生成 Swift binding 旧 UDL 生成链路(`generate_scaffolding + include_scaffolding`)只保留为历史说明,不参与主构建流程。**半迁移状态最容易继续报 undefined symbol。** ### 2. 明确清理旧产物 已补充到 DOC-302c。必须清理: - 删除旧 `bindings/ios/generated` - 删除旧 `bindings/ios/ZxDocumentRuntime.xcframework` - 清理 Xcode 旧引用(pbxproj 中的 zx_document.swift 路径) - 清理 DerivedData - 重新生成全部产物 否则 Rust 已改 proc-macro,Swift 还在用旧 binding,Xcode 还在链接旧 XCFramework。 ### 3. 新增符号检查任务 已创建 DOC-302f。对最终 `libzx_document_ffi.a` 执行 `nm` 检查,必须看到 C ABI 符号: - `_uniffi_zx_document_ffi_fn_func_detect_material_type` - `_uniffi_zx_document_ffi_fn_func_parse_markdown` - `_uniffi_zx_document_ffi_fn_func_export_pending_events` - 对应 checksum 符号 符号不存在 → 构建脚本直接失败。能在进 Xcode 之前就发现问题。 ### 4. 明确 FFI DTO 边界 已补充到 DOC-302a。优先只给 FFI 边界类型添加 UniFFI derive。如果 core 类型包含 UniFFI 不支持的字段、泛型、复杂嵌套,必须在 zx_document_ffi 中定义 FFI DTO,再和 core 类型互相转换,不要让 UniFFI 的约束反向污染 core 类型设计。
Author
Owner

关闭

DOC-302 已修复 — XCFramework 正常构建

## 关闭 DOC-302 已修复 — XCFramework 正常构建
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wangdl/zhixi-document-runtime#29
No description provided.