好的,下面是一篇关于 Dart FFI 异步回调:从 C 线程安全调用 Dart Isolate 入口的讲座式技术文章。
Dart FFI 异步回调:从 C 线程安全调用 Dart Isolate 入口
大家好!今天我们要深入探讨 Dart FFI (Foreign Function Interface) 中一个高级但非常重要的主题:如何通过 C 线程安全地调用 Dart Isolate 的入口函数,实现异步回调。 这在需要高性能计算、与现有 C/C++ 库集成,并同时保持 Dart 响应性的场景下至关重要。
1. 为什么需要异步回调?
在使用 FFI 时,我们经常需要在 C/C++ 代码中执行一些耗时的操作,然后将结果返回给 Dart 代码。如果直接在 Dart 主 Isolate 中调用 C/C++ 代码,可能会阻塞 UI 线程,导致应用卡顿。
为了避免这种情况,我们可以将耗时操作放在 C/C++ 的线程中执行,并在 C/C++ 线程执行完毕后,通过异步回调的方式通知 Dart Isolate。 这样,Dart UI 线程就可以保持响应,用户体验不会受到影响。
此外,Dart 的 Isolate 具有自己的内存空间,默认情况下,直接在 C 线程中修改 Dart 对象的行为是不安全的。我们需要确保对 Dart 对象的访问和修改都在 Dart Isolate 的上下文中进行。
2. 实现步骤
以下是实现从 C 线程安全调用 Dart Isolate 入口的异步回调的关键步骤:
- 定义 Dart 函数类型和对应的 C 函数指针类型。
- 创建 Dart
ReceivePort用于接收来自 C 线程的消息。 - 获取
ReceivePort的SendPort并将其传递给 C 代码。 - 在 C 代码中使用
SendPort向 Dart Isolate 发送消息。 - 在 Dart 中监听
ReceivePort,并在收到消息时执行相应的回调函数。 - 确保 C 代码在发送消息到
SendPort之前已经初始化 Dart VM。
3. 代码示例
为了更清晰地说明上述步骤,我们提供一个具体的代码示例。假设我们需要在 C 代码中计算一个很大的斐波那契数列,并将结果返回给 Dart 代码。
3.1 Dart 代码 (main.dart)
import 'dart:ffi';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
// 1. 定义 Dart 函数类型和对应的 C 函数指针类型
typedef Dart_PostCObject_Type = int Function(
int port_id, Pointer<Dart_CObject> message);
final Dart_PostCObject = DynamicLibrary.open(null)
.lookupFunction<Dart_PostCObject_Type, Dart_PostCObject_Type>(
'Dart_PostCObject',
);
// 定义 C 函数签名
typedef FibonacciCalculatorFunc = Void Function(
Int64 n, Int64 send_port_id);
typedef FibonacciCalculator = void Function(
int n, int send_port_id);
// 加载 C 动态库
final dylib = DynamicLibrary.open('libfibonacci.so'); // 替换为实际的库名称
// 获取 C 函数的指针
final fibonacciCalculator = dylib
.lookupFunction<FibonacciCalculatorFunc, FibonacciCalculator>(
'calculate_fibonacci',
);
void main() async {
// 2. 创建 ReceivePort
final receivePort = ReceivePort();
// 3. 获取 SendPort 并传递给 C 代码
final sendPort = receivePort.sendPort;
final sendPortId = sendPort.nativePort;
// 4. 调用 C 函数进行计算 (在 C 线程中)
print('Dart: Calling C function...');
fibonacciCalculator(40, sendPortId);
// 5. 监听 ReceivePort
print('Dart: Listening for result...');
receivePort.listen((message) {
print('Dart: Received result: $message');
receivePort.close(); // 关闭端口,防止内存泄漏
});
print('Dart: Done.');
}
// Dart_CObject 相关定义 (简化)
final class Dart_CObject extends Struct {
@Int32()
external int type;
external Pointer<Void> value; // 实际使用需要更精确的类型
}
// Dart_CObject_Type 枚举 (简化)
class Dart_CObject_Type_Enum {
static const int kDartNull = 0;
static const int kDartBool = 1;
static const int kDartInt32 = 2;
static const int kDartInt64 = 3;
static const int kDartDouble = 4;
static const int kDartString = 5;
static const int kDartByteArray = 6;
static const int kDartInt8Array = 7;
static const int kDartUint8Array = 8;
static const int kDartInt16Array = 9;
static const int kDartUint16Array = 10;
static const int kDartInt32Array = 11;
static const int kDartUint32Array = 12;
static const int kDartInt64Array = 13;
static const int kDartUint64Array = 14;
static const int kDartDoubleArray = 15;
static const int kDartArray = 16;
static const int kDartTypedData = 17;
static const int kDartExternalTypedData = 18;
static const int kDartSendPort = 19;
static const int kDartCapability = 20;
static const int kDartClosure = 21;
}
3.2 C 代码 (fibonacci.c)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdint.h>
#include <stdbool.h>
// 声明 Dart_PostCObject (需要在编译时链接 Dart VM)
typedef intptr_t Dart_Port_Id;
typedef struct Dart_CObject Dart_CObject;
typedef bool (*Dart_PostCObject_Type)(Dart_Port_Id port_id,
Dart_CObject *message);
extern Dart_PostCObject_Type Dart_PostCObject; // 声明 Dart_PostCObject
// Fibonacci 计算函数
long long fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 用于传递参数给线程的结构体
typedef struct {
int n;
Dart_Port_Id send_port_id;
} ThreadArgs;
// 线程函数
void* calculate_fibonacci_in_thread(void* arg) {
ThreadArgs* args = (ThreadArgs*)arg;
int n = args->n;
Dart_Port_Id send_port_id = args->send_port_id;
// 6. 计算 Fibonacci 数列
long long result = fibonacci(n);
// 构造 Dart_CObject
Dart_CObject result_object;
result_object.type = Dart_CObject_kInt64;
result_object.value.as_int64 = result;
// 发送结果到 Dart Isolate
bool success = Dart_PostCObject(send_port_id, &result_object);
if (!success) {
fprintf(stderr, "Error sending result to Dart Isolate!n");
}
free(args); // 释放 args 结构体
pthread_exit(NULL);
return NULL; // 避免警告
}
// C 函数,Dart 通过 FFI 调用它
void calculate_fibonacci(int n, int64_t send_port_id) {
pthread_t thread;
ThreadArgs* args = (ThreadArgs*)malloc(sizeof(ThreadArgs));
args->n = n;
args->send_port_id = (Dart_Port_Id)send_port_id; // 强制转换
int result = pthread_create(&thread, NULL, calculate_fibonacci_in_thread, args);
if (result != 0) {
fprintf(stderr, "Failed to create thread!n");
free(args); // 释放已分配的内存
return;
}
pthread_detach(thread); // 让线程在完成后自动释放资源
}
3.3 编译 C 代码
使用以下命令编译 C 代码:
gcc -shared -fPIC -o libfibonacci.so fibonacci.c -lpthread
3.4 重要说明
Dart_PostCObject: 这个函数是 Dart VM 提供的,用于将数据发送到指定的 Dart Isolate。 它必须在链接时与 Dart VM 链接,或者通过DynamicLibrary.open(null)在运行时查找。DynamicLibrary.open(null)表示打开当前进程的动态库,允许你访问 Dart VM 提供的符号。- 线程安全:
Dart_PostCObject是线程安全的,可以在任何线程中调用。 - 内存管理: C 代码负责分配和释放
Dart_CObject的内存。 在这个例子中,我们直接在栈上分配了Dart_CObject,所以不需要手动释放。 但是,如果Dart_CObject包含指向堆内存的指针 (例如,字符串),则需要在 Dart 代码中释放这些内存。 - 错误处理: C 代码应该检查
Dart_PostCObject的返回值,以确保消息已成功发送到 Dart Isolate。 - 数据类型:
Dart_CObject是一个联合体,可以包含各种 Dart 数据类型 (例如,整数、浮点数、字符串、数组)。 你需要根据实际情况设置Dart_CObject的type和value字段。 - 编译链接: 编译 C 代码时,需要链接 Dart VM 的库。 具体的链接方式取决于你的 Dart SDK 版本和操作系统。 通常,你需要将 Dart SDK 的
lib目录添加到链接器的搜索路径中,并链接libdart_api.so(或类似的库)。 - 类型匹配: 确保 C 代码和 Dart 代码中定义的类型匹配。 例如,C 中的
int64_t对应于 Dart 中的Int64。 - 异常处理: C 代码中发生的异常无法直接传递到 Dart 代码。 因此,需要在 C 代码中进行适当的错误处理,并将错误信息通过
Dart_PostCObject发送给 Dart 代码。 - 异步性:
Dart_PostCObject是异步的。 这意味着它会将消息放入 Dart Isolate 的消息队列中,并在稍后的某个时间点由 Dart Isolate 处理。 因此,C 代码不应该依赖于 Dart 代码立即处理消息。 - Isolate 状态: 确保在调用
Dart_PostCObject时,目标 Isolate 处于活动状态。 如果 Isolate 已经关闭,Dart_PostCObject将会失败。 - 复杂数据: 如果需要传递复杂的数据结构,可以使用
Dart_CObject的数组类型。 也可以考虑使用 Protocol Buffers 或 JSON 等序列化格式。 - Dart VM 初始化: 在某些情况下,可能需要在 C 代码中显式初始化 Dart VM。 这通常在使用 Dart Native Extensions 时才需要。
4. 运行示例
- 确保已安装 Dart SDK,并且已将 Dart SDK 的
bin目录添加到 PATH 环境变量中。 - 将
main.dart和fibonacci.c放在同一个目录下。 - 编译 C 代码:
gcc -shared -fPIC -o libfibonacci.so fibonacci.c -lpthread(可能需要根据你的系统进行调整)。 - 运行 Dart 代码:
dart run main.dart
你应该看到 Dart 代码调用 C 函数,C 函数在单独的线程中计算 Fibonacci 数列,并将结果发送回 Dart Isolate。
5. 深入理解 Dart_CObject
Dart_CObject 是一个关键的数据结构,用于在 C 代码和 Dart 代码之间传递数据。它本质上是一个 tagged union,可以包含各种 Dart 数据类型。
以下是一个更详细的 Dart_CObject 的结构体定义 (仅供参考,实际定义可能略有不同,请参考 Dart SDK 的头文件):
typedef struct Dart_CObject {
Dart_CObject_Type type;
union {
bool as_bool;
int32_t as_int32;
int64_t as_int64;
double as_double;
char* as_string;
uint8_t* as_byte_array;
struct Dart_CObject** as_array;
struct {
void* data;
intptr_t length;
} as_typed_data;
Dart_Port_Id as_send_port;
// ... 其他类型
} value;
} Dart_CObject;
下表总结了 Dart_CObject 的常用类型:
| 类型 | type 值 |
value 字段 |
描述 |
|---|---|---|---|
| Null | Dart_CObject_kNull |
(unused) | null 值 |
| Boolean | Dart_CObject_kBool |
as_bool |
true 或 false |
| Integer (32-bit) | Dart_CObject_kInt32 |
as_int32 |
32 位有符号整数 |
| Integer (64-bit) | Dart_CObject_kInt64 |
as_int64 |
64 位有符号整数 |
| Double | Dart_CObject_kDouble |
as_double |
双精度浮点数 |
| String | Dart_CObject_kString |
as_string |
UTF-8 编码的字符串 |
| Array | Dart_CObject_kArray |
as_array |
Dart_CObject 指针的数组 |
| TypedData (Uint8Array) | Dart_CObject_kTypedData |
as_typed_data |
类型化的字节数组 (例如,Uint8Array) |
| SendPort | Dart_CObject_kSendPort |
as_send_port |
用于发送消息的端口 |
6. 错误处理和资源管理
- C 代码中的错误处理: C 代码应该使用标准的错误处理机制 (例如,
errno) 来检测错误。 如果发生错误,C 代码应该将错误信息通过Dart_PostCObject发送给 Dart 代码,并在 Dart 代码中进行处理。 - 资源管理: C 代码负责分配和释放所有它分配的资源 (例如,内存、文件句柄)。 确保在不再需要资源时释放它们,以避免内存泄漏。 特别注意
Dart_CObject中包含的指针,例如as_string和as_byte_array。 如果 C 代码分配了这些指针指向的内存,则需要在 Dart 代码中释放这些内存。 可以使用malloc和free函数来分配和释放内存。 - Dart 中的资源管理: Dart 代码应该使用
try...finally块来确保资源在任何情况下都能被释放。 例如,如果 Dart 代码接收到一个包含指向 C 代码分配的内存的Dart_CObject,则应该在finally块中释放这些内存。 使用malloc分配的内存,可以在 Dart 端使用calloc和free来进行内存的释放。
7. 线程安全最佳实践
- 避免共享可变状态: 尽量避免在 C 线程和 Dart Isolate 之间共享可变状态。 如果需要共享状态,请使用线程安全的机制 (例如,互斥锁) 来保护状态的访问。
- 使用
Dart_PostCObject进行通信: 使用Dart_PostCObject在 C 线程和 Dart Isolate 之间进行通信。 避免直接在 C 线程中访问 Dart 对象,因为这可能导致数据竞争和内存损坏。 - 理解 Isolate 的内存模型: Dart Isolate 具有自己的内存空间。 C 代码无法直接访问 Dart Isolate 的内存。 所有的数据都需要通过
Dart_CObject进行传递。 - 使用原子操作: 如果需要在 C 代码中进行简单的原子操作,可以使用 C11 提供的原子操作函数 (例如,
atomic_load和atomic_store)。
8. 复杂场景:传递字符串和字节数组
如果需要在 C 代码和 Dart 代码之间传递字符串或字节数组,需要特别注意内存管理。
8.1 传递字符串 (C -> Dart)
C 代码:
void send_string_to_dart(Dart_Port_Id send_port_id, const char* str) {
Dart_CObject string_object;
string_object.type = Dart_CObject_kString;
string_object.value.as_string = strdup(str); // 重要:复制字符串
if (string_object.value.as_string == NULL) {
fprintf(stderr, "strdup failed!n");
return;
}
bool success = Dart_PostCObject(send_port_id, &string_object);
if (!success) {
fprintf(stderr, "Error sending string to Dart Isolate!n");
free(string_object.value.as_string); // 释放内存
}
}
Dart 代码:
receivePort.listen((message) {
final string = (message as String);
print('Dart: Received string: $string');
// C 代码使用了 strdup,Dart 需要释放内存
// 除非使用 `calloc`分配的字符串,否则不能使用`free`释放
// 否则可能会引发错误
// malloc.free(string.toNativeUtf8().cast<Void>());
receivePort.close();
});
关键点:
- C 代码需要使用
strdup复制字符串,因为Dart_PostCObject会在异步发送消息后立即返回。 如果不复制字符串,原始字符串可能会被 C 代码修改或释放,导致 Dart 代码访问无效内存。 - Dart 代码接收到字符串后,需要释放 C 代码分配的内存。
8.2 传递字节数组 (C -> Dart)
C 代码:
void send_byte_array_to_dart(Dart_Port_Id send_port_id, const uint8_t* data, int length) {
Dart_CObject byte_array_object;
byte_array_object.type = Dart_CObject_kTypedData;
byte_array_object.value.as_typed_data.data = malloc(length);
if (byte_array_object.value.as_typed_data.data == NULL) {
fprintf(stderr, "malloc failed!n");
return;
}
memcpy(byte_array_object.value.as_typed_data.data, data, length);
byte_array_object.value.as_typed_data.length = length;
bool success = Dart_PostCObject(send_port_id, &byte_array_object);
if (!success) {
fprintf(stderr, "Error sending byte array to Dart Isolate!n");
free(byte_array_object.value.as_typed_data.data); // 释放内存
}
}
Dart 代码:
import 'dart:ffi';
import 'dart:typed_data';
import 'package:ffi/ffi.dart';
receivePort.listen((message) {
final data = message as TypedData; // 或者 Uint8List
print('Dart: Received byte array: ${data.length} bytes');
// 释放 C 代码分配的内存
malloc.free(data.buffer.asInt8List().cast<Void>()); // 释放内存
receivePort.close();
});
关键点:
- C 代码需要使用
malloc分配内存来存储字节数组,并将数据复制到新分配的内存中。 - Dart 代码接收到字节数组后,需要释放 C 代码分配的内存。 可以使用
malloc.free释放由malloc分配的内存。
9. 性能优化建议
- 避免频繁的内存分配和释放: 频繁的内存分配和释放会影响性能。 尽量重用已分配的内存,避免不必要的内存分配。
- 使用
TypedData: 如果需要传递大量数据,建议使用TypedData(例如,Uint8List)。TypedData可以直接访问 native 内存,避免不必要的数据复制。 - 减少消息传递的次数: 每次调用
Dart_PostCObject都会有一定的开销。 尽量减少消息传递的次数,将多个数据打包成一个消息进行传递。 - 使用多线程: 如果需要执行大量的计算,可以使用多线程来提高性能。 但是,需要注意线程安全问题。
- 分析性能瓶颈: 使用 Dart VM 的性能分析工具 (例如,Dart DevTools) 来分析性能瓶颈,并进行针对性的优化。
10. 总结
通过本文,我们详细讲解了如何使用 Dart FFI 实现异步回调,从 C 线程安全地调用 Dart Isolate 的入口函数。 掌握这些技术可以让你充分利用 C/C++ 的高性能计算能力,并同时保持 Dart 应用的响应性。 记住,线程安全、内存管理和错误处理是关键。
希望这次讲座对你有所帮助! 通过实践和不断学习,你将能够更好地掌握 Dart FFI,并构建出更强大的应用程序。