Dart FFI 异步回调(Async Callback):从 C 线程安全调用 Dart Isolate 入口

好的,下面是一篇关于 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 入口的异步回调的关键步骤:

  1. 定义 Dart 函数类型和对应的 C 函数指针类型。
  2. 创建 Dart ReceivePort 用于接收来自 C 线程的消息。
  3. 获取 ReceivePortSendPort 并将其传递给 C 代码。
  4. 在 C 代码中使用 SendPort 向 Dart Isolate 发送消息。
  5. 在 Dart 中监听 ReceivePort,并在收到消息时执行相应的回调函数。
  6. 确保 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_CObjecttypevalue 字段。
  • 编译链接: 编译 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. 运行示例

  1. 确保已安装 Dart SDK,并且已将 Dart SDK 的 bin 目录添加到 PATH 环境变量中。
  2. main.dartfibonacci.c 放在同一个目录下。
  3. 编译 C 代码:gcc -shared -fPIC -o libfibonacci.so fibonacci.c -lpthread (可能需要根据你的系统进行调整)。
  4. 运行 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 truefalse
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_stringas_byte_array。 如果 C 代码分配了这些指针指向的内存,则需要在 Dart 代码中释放这些内存。 可以使用 mallocfree 函数来分配和释放内存。
  • Dart 中的资源管理: Dart 代码应该使用 try...finally 块来确保资源在任何情况下都能被释放。 例如,如果 Dart 代码接收到一个包含指向 C 代码分配的内存的 Dart_CObject,则应该在 finally 块中释放这些内存。 使用 malloc 分配的内存,可以在 Dart 端使用callocfree来进行内存的释放。

7. 线程安全最佳实践

  • 避免共享可变状态: 尽量避免在 C 线程和 Dart Isolate 之间共享可变状态。 如果需要共享状态,请使用线程安全的机制 (例如,互斥锁) 来保护状态的访问。
  • 使用 Dart_PostCObject 进行通信: 使用 Dart_PostCObject 在 C 线程和 Dart Isolate 之间进行通信。 避免直接在 C 线程中访问 Dart 对象,因为这可能导致数据竞争和内存损坏。
  • 理解 Isolate 的内存模型: Dart Isolate 具有自己的内存空间。 C 代码无法直接访问 Dart Isolate 的内存。 所有的数据都需要通过 Dart_CObject 进行传递。
  • 使用原子操作: 如果需要在 C 代码中进行简单的原子操作,可以使用 C11 提供的原子操作函数 (例如,atomic_loadatomic_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,并构建出更强大的应用程序。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注