如何通过FFI在Dart与C之间规范共享TypedData?
问题
我有一个TypedData实例中的原始数据,希望通过FFI调用C库处理,处理结果同样是TypedData。具体场景如下:
- 我们有一个以双精度数组存储的动态热力图,每帧都会更新
- 需要基于顶点生成该热力图的Voronoi表示,理想情况是直接拿到可用于
Vertices.raw的Float32List(位置数据)和Int32List(颜色数据),且这些数组能复用,不用每帧重新实例化
我不确定怎么用Dart FFI正确实现这个需求。理想状态是在Dart中持有这些类型化数据:
// 初始化 final heatmap = Float64List(500 * 500); final outPositions = Float32List(maxVertices * 2); final outColors = Int32List(maxVertices);
之后调用C函数,通过引用传入输入和输出数据:
// 后续每帧调用 ffiCall(heatmap, outPositions, outColors); // 使用处理后的输出数据
目前知道.address属性,但它只适用于leaf native调用,我不确定是否要把FFI函数设为leaf调用。另外用Struct可以处理静态大小的数组,比如:
final class MyStruct extends Struct { @Array(100 * 100) external Array<Uint8> inlineArray; }
但很多场景下数组大小只有运行时才能确定。本质上,我希望在Dart中构造对应如下C结构体的对象,然后以引用形式传给C代码:
// 用于指定要在C中处理的Dart数据的结构体 typedef struct { int width; int height; double* data; } my_struct;
如果没法实现上述需求,我也希望尽可能减少Dart和C之间的大数据拷贝。之前找到过类似问题,但提问者能接受数毫秒的拷贝耗时,这只适合偶尔执行的操作,满足不了每帧运行的要求。
请问这个需求可行吗?这类场景的规范实现方式是什么?
解决方案
这个需求完全可行,是Dart FFI处理高性能跨语言数据交互的典型场景,以下是规范实现步骤:
1. 定义对应C结构体的Dart Struct
针对你需要的C结构体,在Dart中定义对应的Struct子类,通过指针字段承载运行时确定大小的数组:
import 'dart:ffi' as ffi; final class HeatmapInput extends ffi.Struct { @ffi.Int32() external int width; @ffi.Int32() external int height; // 对应C中的double*,指向Dart Float64List的原始内存 external ffi.Pointer<ffi.Double> data; } // 为输出数据定义结构体,也可直接传递指针+最大顶点数 final class VoronoiOutput extends ffi.Struct { @ffi.Int32() external int maxVertices; // 对应Float32List的原始内存,存储(x,y)坐标 external ffi.Pointer<ffi.Float> positions; // 对应Int32List的原始内存,存储颜色值 external ffi.Pointer<ffi.Int32> colors; }
2. 绑定C函数
假设你的C函数签名如下:
void generate_voronoi(const my_struct* input, const VoronoiOutput* output, int* out_vertex_count);
在Dart中绑定这个函数时直接使用指针类型:
typedef GenerateVoronoiC = ffi.Void Function( ffi.Pointer<HeatmapInput> input, ffi.Pointer<VoronoiOutput> output, ffi.Pointer<ffi.Int32> outVertexCount, ); typedef GenerateVoronoiDart = void Function( ffi.Pointer<HeatmapInput> input, ffi.Pointer<VoronoiOutput> output, ffi.Pointer<ffi.Int32> outVertexCount, ); // 加载动态库并获取函数指针 final dylib = ffi.DynamicLibrary.open('your_library.so'); // 替换为对应平台的库文件 final generateVoronoi = dylib.lookupFunction<GenerateVoronoiC, GenerateVoronoiDart>('generate_voronoi');
3. 初始化TypedData并传递给C
在Dart中初始化数组后,通过.dataAsPointer(等价于.address.cast<T>())获取内存指针,填充到Struct实例中再传递给C函数:
// 初始化阶段(仅执行一次) const heatmapWidth = 500; const heatmapHeight = 500; final heatmap = Float64List(heatmapWidth * heatmapHeight); const maxVertices = 10000; final outPositions = Float32List(maxVertices * 2); final outColors = Int32List(maxVertices); // 分配并填充输入结构体 final input = ffi.calloc<HeatmapInput>(); input.ref.width = heatmapWidth; input.ref.height = heatmapHeight; input.ref.data = heatmap.dataAsPointer; // 分配并填充输出结构体 final output = ffi.calloc<VoronoiOutput>(); output.ref.maxVertices = maxVertices; output.ref.positions = outPositions.dataAsPointer; output.ref.colors = outColors.dataAsPointer; // 用于接收实际生成的顶点数 final outVertexCount = ffi.calloc<ffi.Int32>(); // 每帧调用逻辑 void updateFrame() { // 先更新heatmap的动态数据... // 调用C函数处理,直接操作Dart数组的原始内存 generateVoronoi(input, output, outVertexCount); // 获取实际顶点数,用于后续渲染 final vertexCount = outVertexCount.value; // 使用处理后的输出数据,比如传给Vertices.raw // Vertices.raw(VertexMode.triangles, outPositions.sublist(0, vertexCount * 2), colors: outColors.sublist(0, vertexCount)); } // 程序结束时释放分配的Struct内存 void dispose() { ffi.calloc.free(input); ffi.calloc.free(output); ffi.calloc.free(outVertexCount); }
4. 关于leaf调用的说明
.dataAsPointer(或.address)可以安全用于非leaf调用,只要保证C函数执行期间,Dart的TypedData数组不会被GC回收。由于你是复用数组,只要持有数组的对象存活,就不会有问题。- 如果你的C函数是纯计算型(不调用Dart回调、不阻塞、不分配内存),可以在绑定函数时加
@ffi.Leaf()注解标记为leaf调用,能获得轻微性能提升,但不是必须的。
关键注意事项
- 必须传递数组的实际大小(比如
maxVertices)给C函数,避免C代码越界访问Dart数组内存。 - Dart的TypedData内存由GC管理,不要在C代码中长期持有这些指针,仅在函数调用期间使用。
- 你的场景完全不需要数据拷贝,C代码直接操作Dart侧数组的原始内存,性能可以达到最优。
内容的提问来源于stack exchange,提问作者filiph
相关产品推荐
相关产品推荐

