一、FFI 是什么

FFI:Foreign Function Interface,外部函数接口。 核心定义:一套语言调用另一套编译型原生语言(C/C++/Rust)编写的动态库的桥梁。 Dart 是托管语言,有自己的 Dart VM、独立堆内存、运行时管理;C/C++ 是无 VM 原生语言,直接操作操作系统内存、CPU。二者运行环境完全隔离,无法直接互相调用函数、传递内存数据,FFI 就是官方提供的跨语言互通标准接口

核心定位

  1. Dart VM 无法直接调用操作系统底层 API、硬件接口、大量成熟 C 库;
  2. 把底层逻辑写进 C 动态库(.so Linux、.dll Windows、.dylib Mac);
  3. 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 最大难点)

  1. Dart 托管内存

    • Uint8List、String、List、自定义对象,由 Dart VM GC 自动回收;
    • VM 管理内存偏移、对象标记,C 无法直接读写这块内存;
    • GC 会自动移动对象内存地址(内存压缩),C 裸指针会直接失效、崩溃。
  2. C 原生堆内存

    • 通过 malloc/calloc/free 分配,操作系统直接管理;
    • 无 GC,手动释放,忘记释放造成内存泄漏;
    • 固定内存地址,裸指针直接访问,Dart VM 无法自动管理。

FFI 的核心工作:在两块隔离内存之间拷贝数据,转换指针、类型

3. 指针 Pointer

FFI 中 ffi.Pointer 对应 C 的 * 裸指针,代表一块原生内存的起始地址:

  • Pointer<ffi.Int32> → C int*
  • Pointer<ffi.Char> → C char*(字符串)
  • Pointer<ffi.Uint8> → C uint8_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 侧绑定库与函数签名

  1. 使用 ffi.DynamicLibrary.open() 加载磁盘上的动态库;
  2. 定义两套函数签名:
    • 「C 函数签名」:给 FFI 做类型校验,对应原生类型;
    • 「Dart 包装签名」:Dart 可直接调用的普通函数;
  3. lookupFunction 将库内 C 函数映射为 Dart 可调用方法。

步骤 3:跨语言数据传递与内存管理

  1. Dart 托管数据 → 拷贝到 C 原生堆,生成 Pointer 传给 C 函数;
  2. C 函数读写原生内存、执行逻辑、返回结果;
  3. C 返回数据 → 拷贝回 Dart 托管对象;
  4. 手动释放所有 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 核心特性与限制

优势

  1. 复用海量成熟 C/C++ 开源库(图像处理、算法、硬件驱动、压缩库等),不用 Dart 从零重写;
  2. 绕过 Dart VM 性能限制,密集计算交给 C,提升运行速度;
  3. 直接调用操作系统原生 API,Dart 标准库未暴露的底层能力全部可用。

关键限制与坑

  1. 内存泄漏风险:C 侧分配内存不会被 Dart GC 回收,忘记 free 持续占用内存;
  2. 线程阻塞:FFI 同步调用 C 函数会阻塞当前 Dart Isolate 线程,耗时 C 逻辑必须放到独立 Isolate;
  3. 无类型安全:指针类型写错、长度传错直接触发段错误,崩溃无友好报错;
  4. 跨平台兼容成本高:Windows/Linux/Mac 动态库格式、编译命令、系统 API 均不同;
  5. 不支持 Dart 对象直接传给 C,只能传递基础数值、二进制数组、结构体。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐