FFI:基础、Foreign Function Interface 以及与 C 的集成

作者: IT Sectr 发布日期: 2026-06-05 阅读时间: 10 分钟

FFI(Foreign Function Interface)— 是 Dart 语言的一种机制,由 dart:ffi 包提供,允许直接调用原生 C 库中的函数,无需 Kotlin、Swift 或 Java 的中间层。开发者加载动态库(Android 上为 .so,iOS 上为 .dylib,Windows 上为 .dll),声明 C 函数的签名,并像普通 Dart 函数一样调用它们。根据 Dart API Reference (2025)FFI 将跨语言调用的开销降低到 0.1 微秒,比通过 Method Channel 快数十倍。

要点

  • FFI(Foreign Function Interface)— 从 Dart 直接调用 C 函数的机制
  • dart:ffi 提供用于加载库和声明签名的 API
  • 性能 通过 FFI 的调用比通过 Method Channel 高 50-100 倍
  • 类型系统 FFI 支持基本类型、结构和 C 指针
  • Flutter 使用 FFI 与原生库集成:OpenCV、SQLite、FFmpeg

什么是 FFI?

FFI(Foreign Function Interface)— 是一种机制,允许编程语言调用用其他语言编写的函数。在 Dart 和 Flutter 的上下文中,FFI 意味着能够直接从 Dart 代码调用 C/C++ 库中的函数,而无需在 Java(Android)或 Swift/Objective-C(iOS)中编写平台代码。

dart:ffi 包出现在 Dart 2.12(2021)中,从那时起它已成为将 Flutter 与原生代码集成的关键工具。在 dart:ffi 出现之前,从 Dart 调用 C 函数的唯一方法是 Method Channel — 一种通过 JSON 序列化在 Dart 和原生端之间传输消息的异步机制。FFI 的工作方式不同:Dart 代码直接访问 C 库的内存,通过 原生 ABI(应用程序二进制接口)调用函数,无需序列化和上下文切换。

FFI 在性能至关重要的场景中尤为需要:图像处理(OpenCV)、音频(FFmpeg)、加密(OpenSSL)、机器学习(TensorFlow Lite)和数据库(SQLite)。在所有这些情况下,Method Channel 会产生不可接受的延迟,而 FFI 提供了与原生 C/C++ 代码相当的性能。dart:ffi 库还支持内存操作:分配、释放和指针管理。

FFI 与 Method Channel 对比:根本区别

Method Channel 异步工作:Dart 向原生代码发送消息,原生代码处理它并发送回结果。每次调用都需要将参数序列化为 Map,通过队列传输并反序列化。每次调用需要 0.5-5 毫秒。FFI 同步且无需序列化 — C 函数调用只需 0.01-0.1 微秒。相差 50-500 倍,这对高频操作至关重要。

dart:ffi 如何工作?

使用 dart:ffi 包括三个阶段:加载库、声明签名和调用函数。每个阶段都使用 Dart 的严格类型系统,从而最大限度地减少运行时错误。

在第一阶段,通过 DynamicLibrary 类加载动态库。库可以按名称(libxyz.so、libxyz.dylib、xyz.dll)或按完整路径加载。Dart 会自动在系统的标准路径中搜索库。DynamicLibrary 提供 lookupFunction 方法,该方法根据符号名称将 Dart 函数与 C 函数关联起来。

在第二阶段,声明一个 Dart 函数,其类型注解与 C 签名对应。为此使用 dart:ffi 中的特殊类型:Int32、Float、Double、Pointer、NativeFunction、Handle 等。lookupFunction 注解接受两个泛型参数:Dart 函数的类型(在 Dart 中的样子)和原生 C 函数的类型(在 C 中声明的方式)。

在第三阶段,生成的 Dart 函数像普通函数一样被调用。参数直接传递,结果立即返回。如果 C 函数通过指针修改内存,Dart 可以通过 Pointer 类读取这些更改。内存管理在 C 端仍然是开发人员的责任 — dart:ffi 不管理 C 中由 malloc 分配的内存。

基本示例:从 Dart 调用 C 函数

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// C 函数声明:int add(int a, int b)
typedef AddNative = Int32 Function(Int32, Int32)
typedef AddDart = int Function(int, int)

void main() {
    final lib = DynamicLibrary.open('libcalculator.so')
    final AddDart add = lib
        .lookupFunction<AddNative, AddDart>('add')

    print(add(5, 3)) // 8
}

在此示例中,add 是一个 C 函数,接受两个 int 并返回 int。typedef AddNative 使用 dart:ffi 类型描述 C 签名,而 AddDart 描述此函数在 Dart 中的样子。lookupFunction 将它们关联起来并返回一个可以像普通函数一样调用的 Dart 函数

FFI 中的数据类型

dart:ffi 提供了一套与 C 类型对应的类型。每种类型都有固定的大小以及在 Dart 和 C 之间的转换规则。理解类型的对应关系对于 FFI 的正确运行至关重要 — 类型大小或符号的错误可能导致应用程序崩溃。

C 类型dart:ffi 类型Dart 类型大小(字节)
intInt32int4
longInt64int8
floatFloatdouble4
doubleDoubledouble8
char*Pointer<Int8>Pointer8(指针)
void*Pointer<Void>Pointer8(指针)
structPointer<T>(Struct)Pointer取决于字段

为了处理 C 字符串(char*),dart:ffi 使用 Pointer<Int8>。从 Dart String 到 C char* 及反向的转换通过 toNativeUtf8(来自 ffi 包)和 fromUtf8 方法完成。使用后通过 calloc.free 释放 C 字符串以防止内存泄漏非常重要。

结构体(Struct)

dart:ffi 支持将 C 结构体声明为继承 Struct 的 Dart 类。结构体的字段使用 @Int32()、@Float()、@Array() 等注解声明。字段的大小和偏移量根据平台的 ABI 自动计算。Pointer<Point> 可以从返回结构体指针的 C 函数获取,或在 Dart 中通过 calloc 分配。

dart
// C 结构体:typedef struct { int x; int y; } Point;
final class Point extends Struct {
    @Int32()
    external int x

    @Int32()
    external int y
}

// 调用返回 Point* 的 C 函数
typedef CreatePointNative = Pointer<Point> Function(Int32, Int32)
typedef CreatePointDart = Pointer<Point> Function(int, int)

final Pointer<Point> p = createPoint(10, 20)
print('x:${p.ref.x},y:${p.ref.y}')
calloc.free(p) // 释放内存

Point 类继承 Struct 并使用 @Int32() 注解声明字段 x 和 y。生成的 C 代码在内存中将具有完全相同的字段布局。Pointer.ref 通过 getter 和 setter 提供对结构体字段的访问。

FFI 实践示例

让我们看一个更复杂的示例 — 与 C 库集成以计算 SHA256 哈希。这是一个典型任务,FFI 在此比 Method Channel 提供显著的性能优势。

通过 FFI 与 OpenSSL 集成

OpenSSL 库提供 SHA256 函数,用于计算字符串的哈希值。通过 dart:ffi,我们可以直接调用它,无需编写 Java 或 Swift 包装器。这是一个示例,展示了 FFI 如何允许在 Flutter 中重用现有的 C 库。

dart
import 'dart:ffi'
import 'package:ffi/ffi.dart'

// 签名:unsigned char* SHA256(
//   const unsigned char *d, size_t n, unsigned char *md)
typedef Sha256Native = Pointer<Uint8> Function(
    Pointer<Uint8>, Size, Pointer<Uint8>)
typedef Sha256Dart = Pointer<Uint8> Function(
    Pointer<Uint8>, int, Pointer<Uint8>)

String sha256(String input) {
    final lib = DynamicLibrary.open('libcrypto.so')
    final Sha256Dart sha256Fn = lib
        .lookupFunction<Sha256Native, Sha256Dart>('SHA256')

    final inputPtr = input.toNativeUtf8()
    final outputPtr = calloc(Uint8)(32) // SHA256 = 32 字节

    sha256Fn(inputPtr, input.length, outputPtr)

    final digest = outputPtr.asTypedList(32)
    final hex = digest.map((b) => b.toRadixString(16)
        .padLeft(2, '0')).join()

    calloc.free(inputPtr)
    calloc.free(outputPtr)

    return hex
}

在此示例中,sha256 函数加载 libcrypto.so 库,找到 SHA256 符号并使用指向输入和输出数据的指针调用它。toNativeUtf8 将 Dart String 转换为 C 字符串(分配内存),asTypedList 允许读取结果的字节数组。内存在使用后释放 — 这是防止泄漏的强制步骤。

内存分配与释放

ffi 包提供 calloc 函数以分配与 C 兼容的内存。分配的内存必须通过 calloc.free 释放,否则将发生泄漏。要自动管理内存,可以使用 ffi 包中的 Arena 类,它在调用 arena.release() 时释放其中分配的所有内存。这在有大量临时分配时特别方便。

限制与最佳实践

尽管 FFI 功能强大,但它有一些在 Design Flutter 应用架构时必须考虑的限制。主要限制涉及类型安全、内存管理和平台兼容性。

安全性

FFI 在运行时不会检查类型。如果 C 函数期望指针但传递了数字,应用程序将因段错误而崩溃。建议使用 FFIgen — 一种基于 C 头文件(.h 文件)生成类型安全 Dart 包装器的工具。FFIgen 分析 C 函数的声明并创建具有正确类型的 Dart 代码,从而消除编写代码阶段的错误。

平台兼容性

动态库的名称和路径在不同平台上有所不同:Android/Linux 上为 libxyz.so,iOS/macOS 上为 libxyz.dylib,Windows 上为 xyz.dll。对于跨平台库,通过 dart:io(Platform.isAndroid、Platform.isIOS)或诸如 package:ffi 的抽象使用条件编译。建议创建一个工厂方法,返回当前平台的正确库。

内存管理

FFI 不管理 C 端的内存。如果 C 函数通过 malloc 分配内存,必须通过 free 释放,否则将发生泄漏。Dart 中没有用于 C 内存的垃圾回收器。建议:始终在分配内存的同一方法中释放内存,或使用 Arena 进行分组释放。

性能与线程

FFI 调用在同一个线程中执行 Dart 代码。长时间的同步操作(超过 10 毫秒)会阻塞 UI 线程并导致帧丢失。对于长时间操作,应在隔离区(Isolate)中调用 C 函数,或确保 C 函数在后台线程启动工作并通过 Port 或回调通知 Dart。

dart
// 隔离区中的 FFI 用于长时间操作
import 'dart:isolate'

Future<String> computeHash(String input) async {
    final port = ReceivePort()
    await Isolate.spawn((SendPort sendPort) {
        final result = sha256(input) // FFI 调用
        sendPort.send(result)
    }, port.sendPort)

    return await port.first as String
}

将 FFI 调用转移到隔离区可确保 UI 线程不会被阻塞。但是,在隔离区之间传输大量数据需要复制内存。对于大型缓冲区(大于 10 MB),最好使用 ShareMemory 或内存映射文件。

常见问题

FFI 与 Method Channel 有何不同?

FFI 直接、同步且无需序列化地调用 C 函数 — 延迟 0.01-0.1 微秒。Method Channel 通过 JSON 序列化异步工作,延迟 0.5-5 毫秒。FFI 适用于高性能操作,Method Channel 适用于简单的平台 API 调用。

能否通过 FFI 调用 C++ 函数?

不能直接调用,dart:ffi 只支持 C 函数。要调用 C++,需要创建一个带有 extern "C" 的 C 包装器(作为 C 符号导出的入口点)。C++ 类需要额外的层来将方法调用转换为 C 函数。

如何处理 C 函数中的错误?

FFI 不支持异常 — 如果 C 函数返回错误代码,需要手动检查。建议将 FFI 调用包装在 Dart 的 try-catch 中,并检查 C 函数的返回代码。关键错误(段错误)无法捕获。

哪些库不能通过 FFI 使用?

FFI 无法与需要复杂 Java(JNI)或 Objective-C(Message Dispatch)初始化的库一起使用。例如,UIKit 和 Android Views 无法通过 FFI 访问。局限性与 FFI 在 C ABI 级别工作有关,而这些 API 需要特定的运行时环境。

是否需要为每个平台分别编译 C 库?

是的,C 库为每个目标平台分别编译。对于 Android,为不同的 ABI(armeabi-v7a、arm64-v8a、x86_64)构建 .so。对于 iOS — 通用 .dylib(arm64)。对于 Windows — .dll。Flutter 在构建时自动打包正确版本的库。

总结

  • FFI(Foreign Function Interface)— 通过 dart:ffi 从 Dart 直接调用 C 函数的机制
  • 性能 FFI 调用比通过 Method Channel 高 50-500 倍
  • 架构 包括加载库、声明签名和调用函数
  • 数据类型 dart:ffi 支持 Int32、Float、Double、Pointer、Struct 等 C 类型
  • 内存 C 端通过 calloc/free 或 Arena 手动管理
  • 限制 FFI:无运行时类型检查、无直接 C++ 支持、阻塞 UI 线程
  • 使用 FFI 在 Flutter 中与原生库进行高性能集成

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读