docs: add C-ABI out-pointer pattern doc (DOC-FULL-031)

- ARM64 iOS ABI limitation: struct pass-by-value unstable
- Pattern: scalar-only params, paired out-pointers, JSON serialization
- Memory management: ownership transfer, rustbuffer_free_separate
- Template: write_result_to_out_ptrs + full FFI function
- Current 10 functions using out-pointer pattern

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-18 14:43:53 +08:00
parent c1bac717fd
commit f30809c5b7

View File

@ -0,0 +1,239 @@
# C-ABI Out-Pointer 模式文档iOS 兼容性)
## 1. 背景
ARM64 iOS ABI 对跨 FFI 边界的 struct 传参有严格限制。UniFFI 默认的 `RustBuffer` 结构体(包含 `{ capacity: i32, len: i32, data: *mut u8 }` 三个字段)在 ARM64 上不保证稳定传递。
**问题表现**:函数返回的 `RustBuffer` 在 iOS 真机上可能得到损坏的 capacity/len 值,导致数据丢失或 crash。
**解决方案**C-ABI out-pointer 模式 — 将 struct 成员拆分为独立的标量参数/返回。
---
## 2. 模式对比
### 2.1 Standard UniFFI有问题
```rust
// ❌ ARM64 不稳定RustBuffer struct 跨 FFI 传参
#[uniffi::export]
fn parse_markdown(content: String) -> Result<Vec<DocumentBlock>, DocumentError> {
// UniFFI 自动生成 RustBuffer 序列化
}
```
生成的 C-ABI
```c
// ❌ 返回 struct — ARM64 可能损坏
RustBuffer ffi_parse_markdown(RustBuffer content, RustCallStatus* status);
```
### 2.2 Out-Pointer 模式(安全)
```rust
// ✅ 所有参数/返回值均为标量int/pointer
#[no_mangle]
pub extern "C" fn ffi_parse_markdown_separate(
content_len: i32,
content_data: *const u8,
out_result_capacity: *mut u64,
out_result_len: *mut u64,
out_result_data: *mut *mut u8,
out_error_capacity: *mut u64,
out_error_len: *mut u64,
out_error_data: *mut *mut u8,
)
```
生成的 C-ABI — 所有参数为标量,兼容 ARM64。
---
## 3. 通用 Out-Pointer 模板
### 3.1 Result 序列化
```rust
/// Write Result<T, DocumentError> into out-pointers.
fn write_result_to_out_ptrs(
result: Result<Vec<u8>, DocumentError>,
out_result_cap: *mut u64, out_result_len: *mut u64, out_result_data: *mut *mut u8,
out_error_cap: *mut u64, out_error_len: *mut u64, out_error_data: *mut *mut u8,
) {
match result {
Ok(value) => {
let buf = uniffi::RustBuffer::from_vec(value);
unsafe {
*out_result_capacity = buf.capacity() as u64;
*out_result_len = buf.len() as u64;
*out_result_data = buf.data_pointer() as *mut u8;
*out_error_capacity = 0;
*out_error_len = 0;
*out_error_data = std::ptr::null_mut();
}
std::mem::forget(buf); // caller now owns the buffer
}
Err(e) => {
let err_json = serde_json::to_string(&e).unwrap_or_default().into_bytes();
let buf = uniffi::RustBuffer::from_vec(err_json);
unsafe {
*out_result_capacity = 0;
*out_result_len = 0;
*out_result_data = std::ptr::null_mut();
*out_error_capacity = buf.capacity() as u64;
*out_error_len = buf.len() as u64;
*out_error_data = buf.data_pointer() as *mut u8;
}
std::mem::forget(buf);
}
}
}
```
### 3.2 完整 FFI 函数
```rust
#[no_mangle]
pub extern "C" fn ffi_my_function_separate(
// Input: raw bytes (len + data ptr)
input_len: i32,
input_data: *const u8,
// Output: result (out-pointers for capacity/len/data)
out_result_cap: *mut u64,
out_result_len: *mut u64,
out_result_data: *mut *mut u8,
// Output: error (out-pointers)
out_error_cap: *mut u64,
out_error_len: *mut u64,
out_error_data: *mut *mut u8,
) {
// 1. Deserialize input
let input = unsafe { std::slice::from_raw_parts(input_data, input_len as usize) };
let args: MyArgs = match serde_json::from_slice(input) {
Ok(a) => a,
Err(_) => { /* error handling */ return; }
};
// 2. Execute logic
let result = my_core_function(args);
// 3. Serialize via out-pointers
write_result_to_out_ptrs(
result.map(|v| serde_json::to_vec(&v).unwrap_or_default()),
out_result_cap, out_result_len, out_result_data,
out_error_cap, out_error_len, out_error_data,
);
}
```
---
## 4. 内存管理
### 4.1 所有权
| 参数方向 | 所有权 | 释放责任 |
|----------|:--:|------|
| Input data | 借用caller 保留) | Caller |
| Output data | 转移给 caller | Caller 调用 `rustbuffer_free_separate` |
### 4.2 释放函数
```rust
#[no_mangle]
pub extern "C" fn ffi_rustbuffer_free_separate(
capacity: u64,
len: u64,
data: *mut u8,
) {
if data.is_null() { return; }
unsafe {
let _v = Vec::from_raw_parts(data, len as usize, capacity as usize);
// Drop here
}
}
```
**Swift 调用示例**
```swift
var resultCap: UInt64 = 0, resultLen: UInt64 = 0, resultData: UnsafeMutablePointer<UInt8>?
var errorCap: UInt64 = 0, errorLen: UInt64 = 0, errorData: UnsafeMutablePointer<UInt8>?
ffi_parse_markdown_separate(
Int32(jsonData.count), pointerToData,
&resultCap, &resultLen, &resultData,
&errorCap, &errorLen, &errorData
)
// 使用 result...
if let data = resultData {
let buf = Data(bytes: data, count: Int(resultLen))
// ... parse JSON
}
// 释放
ffi_rustbuffer_free_separate(resultCap, resultLen, resultData)
ffi_rustbuffer_free_separate(errorCap, errorLen, errorData)
```
---
## 5. 设计原则
### 5.1 标量仅传
- ✅ `i32`, `u64`, `*mut u8` — 单寄存器传参
- ❌ `RustBuffer { capacity, len, data }` — 多字段 struct
### 5.2 成对输出
每个输出 buffer 拆为 3 个 out-pointer
```
capacity: *mut u64 → buffer 容量
len: *mut u64 → 有效数据长度
data: *mut *mut u8 → 数据指针(双重指针:写入指针值)
```
### 5.3 序列化格式
输入/输出均使用 JSON`serde_json`)序列化,因为:
- C-ABI 仅传递字节数组
- JSON 在 Swift 侧有原生支持(`JSONDecoder`
- 避免复杂 struct 布局的 ABI 问题
---
## 6. 当前使用范围
项目中有以下函数使用 out-pointer 模式(`_separate` 后缀):
| 函数 | 输入 | 输出 | 状态 |
|------|:--:|:--:|:--:|
| `parse_markdown_separate` | JSON args | `Vec<DocumentBlock>` | ✅ 已实现 |
| `parse_text_separate` | JSON args | `Vec<DocumentBlock>` | ✅ 已实现 |
| `detect_material_type_separate` | 文件路径 | `MaterialType` | ✅ 已实现 |
| `read_image_meta_separate` | 文件路径 | `ImageMeta` | ✅ 已实现 |
| `read_text_stats_separate` | 文件路径 | `TextStats` | ✅ 已实现 |
| `search_markdown_blocks_separate` | JSON args | `Vec<SearchResult>` | ✅ 已实现 |
| `search_text_content_separate` | JSON args | `Vec<SearchResult>` | ✅ 已实现 |
| `search_pdf_pages_separate` | JSON args | `Vec<SearchResult>` | ✅ 已实现 |
| `push_reading_event_separate` | JSON event | () | ✅ 已实现 |
| `export_pending_events_separate` | () | `Vec<ReadingEvent>` | ✅ 已实现 |
---
## 7. 何时使用 Out-Pointer
| 场景 | 推荐方式 |
|------|----------|
| V2 Session / Event新接口 | Standard UniFFI已验证稳定 |
| 返回复杂类型Vec/struct且有 ARM64 兼容需求 | Out-pointer + JSON |
| 简单标量输入输出 | Standard UniFFI |
| 需要高性能避免 JSON 开销 | Standard UniFFI + 测试验证 |
---
## 8. 相关文档
- [UniFFI UDL 编写规范](./uniffi-udl-spec.md)
- [iOS FFI 调用指南](./ios-ffi-integration-guide.md)