Dart语言FFI机制详解
一、FFI 是什么
FFI:Foreign Function Interface,外部函数接口。 核心定义:一套语言调用另一套编译型原生语言(C/C++/Rust)编写的动态库的桥梁。 Dart 是托管语言,有自己的 Dart VM、独立堆内存、运行时管理;C/C++ 是无 VM 原生语言,直接操作操作系统内存、CPU。二者运行环境完全隔离,无法直接互相调用函数、传递内存数据,FFI 就是官方提供的跨语言互通标准接口。
核心定位
- Dart VM 无法直接调用操作系统底层 API、硬件接口、大量成熟 C 库;
- 把底层逻辑写进 C 动态库(
.soLinux、.dllWindows、.dylibMac); - Dart 通过 FFI 加载动态库,调用库内 C 函数,双向传递数据。
二、核心基础概念
1. 动态库(Dynamic Library)
区别静态库:运行时才加载到进程内存,多个程序可共享同一份库文件,更新库无需重新编译 Dart 代码。
- Windows:
.dll - Linux:
.so - MacOS:
.dylib
C 代码编译命令示例(生成动态库)
bash
gcc -shared -fPIC native.c -o libnative.so
2. 两种完全隔离的内存模型(FFI 最大难点)
-
Dart 托管内存
- Uint8List、String、List、自定义对象,由 Dart VM GC 自动回收;
- VM 管理内存偏移、对象标记,C 无法直接读写这块内存;
- GC 会自动移动对象内存地址(内存压缩),C 裸指针会直接失效、崩溃。
-
C 原生堆内存
- 通过
malloc/calloc/free分配,操作系统直接管理; - 无 GC,手动释放,忘记释放造成内存泄漏;
- 固定内存地址,裸指针直接访问,Dart VM 无法自动管理。
- 通过
FFI 的核心工作:在两块隔离内存之间拷贝数据,转换指针、类型。
3. 指针 Pointer
FFI 中 ffi.Pointer 对应 C 的 * 裸指针,代表一块原生内存的起始地址:
Pointer<ffi.Int32>→ Cint*Pointer<ffi.Char>→ Cchar*(字符串)Pointer<ffi.Uint8>→ Cuint8_t*(字节数组)
三、FFI 完整工作流程(三步走)
步骤 1:编写、编译 C 原生动态库
写 C 函数,暴露外部可调用接口(无 static 修饰),编译为动态库。
#include <stdint.h>
// 普通计算函数
int32_t add(int32_t a, int32_t b) {
return a + b;
}
// 接收字符串,返回字符串长度
int32_t str_len(const char* str) {
int len = 0;
while(str[len] != '\0') len++;
return len;
}
// 填充字节数组
void fill_arr(uint8_t* buf, int32_t size, uint8_t val) {
for(int i = 0; i < size; i++) {
buf[i] = val;
}
}
步骤 2:Dart 侧绑定库与函数签名
- 使用
ffi.DynamicLibrary.open()加载磁盘上的动态库; - 定义两套函数签名:
- 「C 函数签名」:给 FFI 做类型校验,对应原生类型;
- 「Dart 包装签名」:Dart 可直接调用的普通函数;
lookupFunction将库内 C 函数映射为 Dart 可调用方法。
步骤 3:跨语言数据传递与内存管理
- Dart 托管数据 → 拷贝到 C 原生堆,生成 Pointer 传给 C 函数;
- C 函数读写原生内存、执行逻辑、返回结果;
- C 返回数据 → 拷贝回 Dart 托管对象;
- 手动释放所有 C 侧分配内存,防止泄漏。
完整 Dart 演示代码(纯计算,无任何网络)
dart
import 'dart:ffi' as ffi;
import 'dart:typed_data';
// 1. 加载动态库
final lib = ffi.DynamicLibrary.open("./libnative.so");
// 2. 绑定 add 函数签名
typedef AddFunc = ffi.Int32 Function(ffi.Int32 a, ffi.Int32 b);
typedef int Add(int a, int b);
final add = lib.lookupFunction<AddFunc, Add>("add");
// 绑定字符串长度函数
typedef StrLenFunc = ffi.Int32 Function(ffi.Pointer<ffi.Char> str);
typedef int StrLen(ffi.Pointer<ffi.Char> str);
final strLen = lib.lookupFunction<StrLenFunc, StrLen>("str_len");
// 绑定数组填充函数
typedef FillArrFunc = ffi.Void Function(
ffi.Pointer<ffi.Uint8> buf,
ffi.Int32 size,
ffi.Uint8 val,
);
typedef void FillArr(ffi.Pointer<ffi.Uint8> buf, int size, int val);
final fillArr = lib.lookupFunction<FillArrFunc, FillArr>("fill_arr");
void main() {
// 调用简单数字计算
print(add(10, 20));
// Dart字符串 → C char*
String dartStr = "hello ffi";
ffi.Pointer<ffi.Char> cStr = ffi.StringToUtf8(dartStr);
print(strLen(cStr));
ffi.calloc.free(cStr); // 释放C字符串内存
// Dart数组 ↔ C 字节缓冲区
int arrSize = 10;
ffi.Pointer<ffi.Uint8> cBuf = ffi.calloc<ffi.Uint8>(arrSize);
fillArr(cBuf, arrSize, 0xAA);
// C内存数据拷贝到Dart托管Uint8List
Uint8List dartBuf = cBuf.asTypedList(arrSize);
print(dartBuf);
ffi.calloc.free(cBuf); // 释放原生数组内存
}
四、FFI 类型映射对照表(Dart ↔ C)
表格
| Dart FFI 类型 | C 原生类型 | 说明 |
|---|---|---|
| ffi.Int8 / Int16 / Int32 / Int64 | int8_t / int16_t / int32_t / long long | 有符号整数 |
| ffi.Uint8 / Uint16 / Uint32 / Uint64 | uint8_t / uint16_t / uint32_t / uint64_t | 无符号整数 |
| ffi.Float / Double | float / double | 浮点数 |
| ffi.Pointer<T> | T* | 任意类型裸指针 |
| ffi.Char | char | 字符串基础单元 |
| ffi.Void | void | 无返回值 / 无参数 |
五、FFI 两种内存分配方式
1. ffi.calloc(推荐)
- 在 C 原生堆分配内存,自动清零;
- 生命周期独立于 Dart GC,必须手动 free;
- 用于传递数组、字符串、结构体给 C 函数。
2. Pointer.fromAddress 不推荐
直接操作内存地址,极易越界、崩溃,仅底层特殊场景使用。
3. Dart 托管内存禁止直接传给 C
直接把 Dart 的Uint8List.buffer.asPointer()传给 C 存在巨大风险: GC 运行时会移动托管对象内存,C 指针变成野指针,程序直接段错误 crash。 标准做法:calloc 分配独立原生内存,拷贝数据。
六、结构体传递 FFI
C 结构体可以在 Dart 中用 ffi.Struct 镜像定义,实现跨语言结构体传参。 C 侧:
c
运行
typedef struct {
int32_t x;
int32_t y;
} Point;
int32_t point_sum(Point p) {
return p.x + p.y;
}
Dart 侧镜像结构体:
dart
class Point extends ffi.Struct {
@ffi.Int32()
external int x;
@ffi.Int32()
external int y;
}
使用时分配结构体内存、赋值,传入 C 函数。
七、FFI 核心特性与限制
优势
- 复用海量成熟 C/C++ 开源库(图像处理、算法、硬件驱动、压缩库等),不用 Dart 从零重写;
- 绕过 Dart VM 性能限制,密集计算交给 C,提升运行速度;
- 直接调用操作系统原生 API,Dart 标准库未暴露的底层能力全部可用。
关键限制与坑
- 内存泄漏风险:C 侧分配内存不会被 Dart GC 回收,忘记 free 持续占用内存;
- 线程阻塞:FFI 同步调用 C 函数会阻塞当前 Dart Isolate 线程,耗时 C 逻辑必须放到独立 Isolate;
- 无类型安全:指针类型写错、长度传错直接触发段错误,崩溃无友好报错;
- 跨平台兼容成本高:Windows/Linux/Mac 动态库格式、编译命令、系统 API 均不同;
- 不支持 Dart 对象直接传给 C,只能传递基础数值、二进制数组、结构体。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)