理解 Dart 如何通过外部函数接口(dart:ffi
)调用 C 函数是项强大技能。它能让你利用现有 C/C++ 库、提升计算密集型任务性能,并与原生系统 API 交互。
让我们通过一个简单而详细的示例逐步拆解。
整体原理:工作机制
该过程包含三个主要阶段:
- C 端 :编写 C 函数并编译为动态库 (Linux 的
.so
文件,macOS 的.dylib
,Windows 的.dll
)。这是独立的编译代码包,其他程序可在运行时加载使用。 - Dart 端 (
dart:ffi
) :使用dart:ffi
库: a. 将动态库加载到 Dart 应用 b. 通过函数名(符号)查找目标 C 函数 c. 告知 Dart 确切的函数签名(参数和返回类型)以确保安全调用 - 执行阶段 :像调用普通 Dart 函数一样调用 C 函数。
dart:ffi
在底层处理转换,向 C 函数传递数据并将结果返回给 Dart。
分步示例:实现 add
函数
让我们创建计算两数之和的 C 函数,并从 Dart 调用它。
步骤 1:编写 C 代码
创建 C 源文件 native_add.c
:
c
// native_add.c
// 导出函数使其对动态库加载器可见
// Windows 可能需要 __declspec(dllexport)
// Linux/macOS 默认导出所有函数,但显式声明更规范
#if defined(_WIN32)
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
// 这是要从 Dart 调用的 C 函数
// 接收两个 32 位整数并返回它们的和
EXPORT int add(int a, int b) {
return a + b;
}
说明:
#if defined...
:跨平台预处理指令。确保在 Windows 显式标记 DLL 导出函数,Linux/macOS 中EXPORT
为空标记。int add(int a, int b)
:标准 C 函数。需使用大小固定的 C 类型。现代平台中int
通常为 32 位整数,完美对应 Dart FFI 的Int32
。
步骤 2:编译 C 代码为动态库
此步骤与平台相关。需使用 GCC/Clang(Linux/macOS)或 Visual Studio/MinGW(Windows)。
在 native_add.c
所在目录执行对应命令:
-
Linux:
bashgcc -shared -o libnative_add.so -fPIC native_add.c
-
macOS:
bashgcc -shared -o libnative_add.dylib native_add.c
-
Windows(使用 GCC/MinGW):
bashgcc -shared -o native_add.dll native_add.c
编译器参数说明:
-shared
:指示编译器生成共享库而非可执行文件-o libnative_add.so/.dylib/.dll
:指定输出文件名(Linux/macOS 通常加lib
前缀)-fPIC
:生成位置无关代码(Position-Independent Code),共享库的核心要求
编译后目录会生成新文件(libnative_add.so
/libnative_add.dylib
/native_add.dll
),这就是 Dart 要加载的编译后 C 代码。
步骤 3:编写调用 C 函数的 Dart 代码
在相同目录创建 main.dart
:
dart
import 'dart:io'; // 获取平台信息
import 'dart:ffi'; // 外部函数接口库
// --- 步骤 1:定义函数签名类型 ---
// 定义 C 函数签名
// 必须与 C 代码的 `int add(int a, int b)` 匹配
// `Int32` 是 `dart:ffi` 中兼容 C 的 32 位整数
typedef CAddFunc = Int32 Function(Int32 a, Int32 b);
// 定义 Dart 函数签名
// 这是 Dart 代码中使用的友好接口
// `int` 是 Dart 原生整数类型
typedef DartAddFunc = int Function(int a, int b);
void main() {
// --- 步骤 2:加载动态库 ---
// 构建动态库路径(需与脚本同目录或提供完整路径)
var dylibPath = Platform.isWindows ? 'native_add.dll' : 'libnative_add.so';
if (Platform.isMacOS) {
// macOS 需要特定路径格式
// 通常置于可执行文件同级目录
dylibPath = 'libnative_add.dylib';
}
// 加载动态库
final dylib = DynamicLibrary.open(dylibPath);
// --- 步骤 3:查找并转换 C 函数 ---
// 在库中查找函数符号 'add'
// 使用 C 函数签名类型定义
final addPointer = dylib.lookup<NativeFunction<CAddFunc>>('add');
// 将 C 函数指针转换为可调用的 Dart 函数
// 使用 Dart 函数签名类型定义
final add = addPointer.asFunction<DartAddFunc>();
// --- 步骤 4:调用函数 ---
// 现在可以像调用 Dart 函数一样调用 C 函数!
final result = add(10, 25);
print('C 函数返回结果: $result');
// 再次调用示例
print('C 函数再次返回: ${add(99, 1)}');
}
运行 Dart 程序:
bash
dart run main.dart
输出结果:
C 函数返回结果: 35
C 函数再次返回: 100
Dart 代码深度解析
步骤 1:类型定义(函数蓝图)
dart
typedef CAddFunc = Int32 Function(Int32 a, Int32 b);
typedef DartAddFunc = int Function(int a, int b);
这是确保正确性的核心:
CAddFunc
:描述 C 函数的原生内存布局 。必须使用dart:ffi
类型(Int32
,Float
,Pointer
等),因其内存大小和表示与 C 完全一致。Dart 的int
可能是 64 位,会导致不匹配。DartAddFunc
:定义 Dart 环境 中的函数接口。可使用便捷的 Dart 类型(如int
/double
)。dart:ffi
会自动处理int
与Int32
的转换。分离这两个类型定义使代码更清晰。
步骤 2:加载动态库
dart
final dylib = DynamicLibrary.open(dylibPath);
程序要求操作系统查找并加载动态库文件(.dll
/.so
/.dylib
)。若文件未找到会抛出异常。
步骤 3:查找与转换
dart
final addPointer = dylib.lookup<NativeFunction<CAddFunc>>('add');
final add = addPointer.asFunction<DartAddFunc>();
两步关键操作:
- 查找符号 :在加载库中搜索名为
add
的函数(导出符号)。泛型<NativeFunction<CAddFunc>>
指示 Dart:"查找的对象是原生函数指针,其内存布局由CAddFunc
描述"。结果addPointer
是内存中函数的原始指针,不可直接调用。 - 函数转换 :
.asFunction<...>()
是桥梁。它将原始函数指针包装成 Dart 可调用对象。泛型<DartAddFunc>
指定最终 Dart 函数的形态。dart:ffi
生成高度优化的"桥接代码",在 Dart VM 和 C 函数间跳转,按签名转换参数和返回值。
步骤 4:函数调用
dart
final result = add(10, 25);
经过转换后,add
成为一等公民的 Dart 函数。可像普通 Dart 代码一样调用、传参,这正是 dart:ffi
的精妙之处。
核心类型映射表
C 类型 | dart:ffi 类型 (C 签名用) |
Dart 类型 (Dart 签名用) |
---|---|---|
int32_t , int |
Int32 |
int |
int64_t , long long |
Int64 |
int |
float |
Float |
double |
double |
Double |
double |
char* (UTF-8 字符串) |
Pointer<Utf8> |
String (需辅助方法) |
void* |
Pointer<Void> |
Pointer<Void> |
struct MyStruct { ... } |
class MyStruct extends Struct { ... } |
MyStruct |