返回列表FFI 与原生代码直调
系统讲解Dart FFI DynamicLibrary加载、NativeFunction类型映射、Pointer操作、Struct定义及内存管理最佳实践
FFI 与原生代码直调 (Dart FFI)
模块:八 - 平台交互与原生集成
预计阅读时间:40 分钟
前言
Platform Channel 解决了"Flutter 调用原生 API"的问题,但它有一个固有的成本:每次调用都需要在 Flutter 线程和平台主线程之间切换,序列化/反序列化数据,通常需要 0.5~5ms。
对于偶尔的调用(打开相机、获取设备信息),这个代价完全可以接受。但对于计算密集型场景——图像处理、音频编解码、加密算法、机器学习推理——每帧都调用 Platform Channel 会造成明显的性能瓶颈。
Dart FFI(Foreign Function Interface) 彻底解决了这个问题:它让 Dart 代码直接调用 C 语言函数,无需线程切换,无需序列化,函数调用的开销接近于零。
一、FFI 的核心概念
1.1 为什么 FFI 不需要线程切换
Platform Channel:
Dart(UI 线程)→ [序列化] → Platform 线程 → [反序列化] → 原生代码
^^^ ^^^
线程切换(~0.5ms) 线程切换
FFI 直调:
Dart(UI 线程)→ C 函数(同线程内调用)→ 返回
^^^
几乎零开销(< 1μs)
FFI 调用发生在调用方的线程内,不涉及任何线程切换。这使得 FFI 适合:
- 每帧都需要调用的高频函数
- 性能关键路径(如音频处理每 10ms 调用一次)
- 大型 C/C++ 库的直接集成
1.2 Dart 类型与 C 类型的映射
| C 类型 | Dart FFI 类型 | 描述 |
|---|
void | Void | 无返回值 |
|
Dart FFI与原生代码直调—C/C++互操作完全指南|新宇宙博客int (*)(int) | NativeFunction<Int32 Function(Int32)> | 函数指针 |
二、基础 FFI 使用
2.1 从零开始:调用 C 标准库
import 'dart:ffi';
import 'dart:io';
void main() {
// 加载动态库
final lib = Platform.isAndroid
? DynamicLibrary.open('libm.so') // Android
: DynamicLibrary.process(); // iOS(静态链接)
// 查找并绑定 C 函数
// C 签名:double sqrt(double x);
final sqrt = lib.lookupFunction<
Double Function(Double), // Native C 类型签名
double Function(double) // Dart 调用类型签名
>('sqrt');
print(sqrt(4.0)); // 输出:2.0
print(sqrt(9.0)); // 输出:3.0
}
2.2 调用自定义 C 库
#ifndef IMAGE_PROCESSOR_H
#define IMAGE_PROCESSOR_H
#include <stdint.h>
#include <stddef.h>
typedef struct {
uint8_t* data;
int width;
int height;
int channels;
} ImageResult;
ImageResult* process_image(
uint8_t* input_data,
int width,
int height,
int quality
);
void free_image_result(ImageResult* result);
uint64_t compute_hash(const uint8_t* data, size_t length);
#endif
#include "image_processor.h"
#include <stdlib.h>
#include <string.h>
ImageResult* process_image(uint8_t* input_data, int width, int height, int quality) {
ImageResult* result = (ImageResult*)malloc(sizeof(ImageResult));
size_t size = width * height * 3;
result->data = (uint8_t*)malloc(size);
for (size_t i = 0; i < size; i++) {
result->data[i] = (uint8_t)(input_data[i] * quality / 100);
}
result->width = width;
result->height = height;
result->channels = 3;
return result;
}
void free_image_result(ImageResult* result) {
if (result != NULL) {
free(result->data);
free(result);
}
}
uint64_t compute_hash(const uint8_t* data, size_t length) {
uint64_t hash = 14695981039346656037ULL;
for (size_t i = 0; i < length; i++) {
hash ^= data[i];
hash *= 1099511628211ULL;
}
return hash;
}
2.3 Dart 侧绑定
import 'dart:ffi';
import 'dart:io';
import 'dart:typed_data';
// ─── 定义 C 结构体的 Dart 对应 ───────────────────────────────────────────
// 对应 C 的 ImageResult 结构体
final class ImageResultStruct extends Struct {
external Pointer<Uint8> data;
@Int32()
external int width;
@Int32()
external int height;
@Int32()
external int channels;
}
// ─── 定义 C 函数的 Native 类型签名 ──────────────────────────────────────
typedef _ProcessImageNative = Pointer<ImageResultStruct> Function(
Pointer<Uint8> inputData,
Int32 width,
Int32 height,
Int32 quality,
);
typedef _ProcessImageDart = Pointer<ImageResultStruct> Function(
Pointer<Uint8> inputData,
int width,
int height,
int quality,
);
typedef _FreeImageResultNative = Void Function(Pointer<ImageResultStruct> result);
typedef _FreeImageResultDart = void Function(Pointer<ImageResultStruct> result);
typedef _ComputeHashNative = Uint64 Function(Pointer<Uint8> data, Size length);
typedef _ComputeHashDart = int Function(Pointer<Uint8> data, int length);
// ─── Dart 封装类 ─────────────────────────────────────────────────────────
class ImageProcessor {
late final DynamicLibrary _lib;
late final _ProcessImageDart _processImage;
late final _FreeImageResultDart _freeImageResult;
late final _ComputeHashDart _computeHash;
ImageProcessor() {
_lib = _loadLibrary();
_processImage = _lib.lookupFunction<_ProcessImageNative, _ProcessImageDart>(
'process_image',
);
_freeImageResult = _lib.lookupFunction<_FreeImageResultNative, _FreeImageResultDart>(
'free_image_result',
);
_computeHash = _lib.lookupFunction<_ComputeHashNative, _ComputeHashDart>(
'compute_hash',
);
}
static DynamicLibrary _loadLibrary() {
if (Platform.isAndroid) {
return DynamicLibrary.open('libimage_processor.so');
} else if (Platform.isIOS) {
return DynamicLibrary.process(); // iOS 静态链接,直接从进程查找
} else if (Platform.isMacOS) {
return DynamicLibrary.open('libimage_processor.dylib');
} else if (Platform.isLinux) {
return DynamicLibrary.open('libimage_processor.so');
} else if (Platform.isWindows) {
return DynamicLibrary.open('image_processor.dll');
}
throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}');
}
/// 处理图像并返回处理后的字节数据
/// 注意:调用者负责管理输入数据的生命周期
ProcessedImage processImage(
Uint8List imageBytes,
int width,
int height, {
int quality = 85,
}) {
// 1. 在 native heap 分配内存并复制数据
final inputPointer = malloc.allocate<Uint8>(imageBytes.length);
inputPointer.asTypedList(imageBytes.length).setAll(0, imageBytes);
Pointer<ImageResultStruct>? resultPointer;
try {
// 2. 调用 C 函数(直接在当前线程,无切换)
resultPointer = _processImage(inputPointer, width, height, quality);
if (resultPointer.address == 0) {
throw FFIException('process_image returned null pointer');
}
// 3. 读取结构体字段
final struct = resultPointer.ref;
final outputSize = struct.width * struct.height * struct.channels;
// 4. 将 C 内存中的数据拷贝到 Dart 管理的内存
final outputBytes = Uint8List.fromList(
struct.data.asTypedList(outputSize),
);
return ProcessedImage(
bytes: outputBytes,
width: struct.width,
height: struct.height,
channels: struct.channels,
);
} finally {
// 5. 释放 Dart 分配的内存
malloc.free(inputPointer);
// 6. 释放 C 库分配的内存(必须通过 C 库提供的函数释放!)
if (resultPointer != null && resultPointer.address != 0) {
_freeImageResult(resultPointer);
}
}
}
/// 计算数据的哈希值(高性能,适合大文件)
int computeHash(Uint8List data) {
final pointer = malloc.allocate<Uint8>(data.length);
try {
pointer.asTypedList(data.length).setAll(0, data);
return _computeHash(pointer, data.length);
} finally {
malloc.free(pointer);
}
}
}
class ProcessedImage {
const ProcessedImage({
required this.bytes,
required this.width,
required this.height,
required this.channels,
});
final Uint8List bytes;
final int width;
final int height;
final int channels;
}
class FFIException implements Exception {
const FFIException(this.message);
final String message;
@override
String toString() => 'FFIException: $message';
}
三、内存管理:Dart FFI 最重要的主题
3.1 两种内存区域
Dart GC 堆(Dart 管理) Native Heap(手动管理)
┌───────────────────────┐ ┌───────────────────────┐
│ Dart 对象、List、Map │ │ C malloc() 分配的内存 │
│ 由 GC 自动回收 │ │ 必须手动 free() │
└───────────────────────┘ └───────────────────────┘
↑ ↑
Uint8List.fromList(...) malloc.allocate<T>()
(拷贝到 GC 堆) (在 native heap 分配)
3.2 malloc 与 calloc
import 'package:ffi/ffi.dart'; // 需要 ffi 包
// 分配 native 内存
final ptr = malloc.allocate<Int32>(4); // 分配 4 个 Int32(16 字节)
// 或者
final ptr2 = calloc.allocate<Uint8>(1024); // 分配并清零
// 使用指针
ptr.value = 42; // 写入
print(ptr.value); // 读取
print(ptr[0]); // 数组访问
ptr[1] = 100; // 数组写入
// ⚠️ 必须手动释放!否则内存泄漏
malloc.free(ptr);
calloc.free(ptr2);
3.3 字符串处理(Pointer<Char>)
import 'package:ffi/ffi.dart';
// Dart String → C String
final nativeString = 'Hello, FFI!'.toNativeUtf8(); // malloc in native heap
try {
cFunction(nativeString); // 传递给 C 函数
} finally {
malloc.free(nativeString); // 使用完毕必须释放
}
// C String → Dart String
Pointer<Utf8> cStringPointer = getCStringFromCLib();
final dartString = cStringPointer.toDartString();
// 注意:如果 cStringPointer 的内存由 C 库管理,
// 调用 C 库的 free 函数而不是 malloc.free
3.4 Arena:批量内存管理(推荐)
Arena 是一种区域内存管理器,作用域结束时自动释放所有分配的内存:
import 'package:ffi/ffi.dart';
// ✅ 推荐:使用 Arena 管理多个分配
Future<void> processWithArena(Uint8List data) {
return using((Arena arena) {
// 所有在 arena 中分配的内存会在 using 结束时自动释放
final dataPtr = arena.allocate<Uint8>(data.length);
final configPtr = arena.allocate<ConfigStruct>(1);
final outputPtr = arena.allocate<Uint8>(4096);
dataPtr.asTypedList(data.length).setAll(0, data);
configPtr.ref.quality = 85;
configPtr.ref.flags = 0;
processNative(dataPtr, data.length, configPtr, outputPtr, 4096);
// 读取结果
final result = Uint8List.fromList(outputPtr.asTypedList(4096));
return result;
// ← 此处 arena 自动释放 dataPtr, configPtr, outputPtr
});
}
四、NativeCallable:C 调用 Dart(回调)
在某些场景下,C 库需要回调 Dart 函数(如进度回调、事件通知):
// Dart 回调的 Native 类型签名
typedef ProgressCallbackNative = Void Function(Int32 progress, Int32 total);
typedef ProgressCallbackDart = void Function(int progress, int total);
// 声明 C 函数,接受函数指针
typedef _ProcessWithCallbackNative = Int32 Function(
Pointer<Uint8> data,
Int32 length,
Pointer<NativeFunction<ProgressCallbackNative>> callback,
);
class FileProcessor {
late final _ProcessWithCallbackDart _processWithCallback;
void processWithProgress(
Uint8List data,
void Function(int progress, int total) onProgress,
) {
// 将 Dart 函数包装为 C 可调用的指针
final callback = NativeCallable<ProgressCallbackNative>.listener(
(progress, total) {
// 此回调在 Isolate 的事件循环中安全执行
onProgress(progress, total);
},
);
final dataPtr = malloc.allocate<Uint8>(data.length);
try {
dataPtr.asTypedList(data.length).setAll(0, data);
_processWithCallback(dataPtr, data.length, callback.nativeFunction);
} finally {
malloc.free(dataPtr);
callback.close(); // 必须关闭 NativeCallable!
}
}
}
.listener:回调排入 Isolate 的事件队列(线程安全,但稍有延迟)
.isolateLocal:在调用线程直接执行(更快,但只能在当前 Isolate 中使用)
五、ffigen:自动生成绑定
对于有大量函数的 C 库,手写绑定极其繁琐。ffigen 解析 C 头文件,自动生成 Dart 绑定:
dev_dependencies:
ffigen: ^9.0.0
ffigen:
output: 'lib/src/ffi/generated_bindings.dart'
name: 'ImageProcessorBindings'
description: 'Auto-generated bindings for image_processor'
headers:
entry-points:
- 'native/image_processor.h'
preamble: |
// Auto-generated by ffigen. Do not modify manually.
llvm-path:
- '/usr/lib/llvm-14/lib/libclang.so'
comments:
style: any
length: full
functions:
include:
- process_image
- free_image_result
- compute_hash
structs:
include:
- ImageResult
// GENERATED FILE - DO NOT MODIFY
// lib/src/ffi/generated_bindings.dart
class ImageProcessorBindings {
final DynamicLibrary _lib;
ImageProcessorBindings(this._lib);
late final _process_image = _lib.lookupFunction<
ffi.Pointer<ImageResult> Function(ffi.Pointer<ffi.Uint8>, ffi.Int32, ffi.Int32, ffi.Int32),
ffi.Pointer<ImageResult> Function(ffi.Pointer<ffi.Uint8>, int, int, int)
>('process_image');
ffi.Pointer<ImageResult> process_image(
ffi.Pointer<ffi.Uint8> input_data,
int width,
int height,
int quality,
) => _process_image(input_data, width, height, quality);
// ... 其他函数自动生成
}
六、与 Rust 互操作(通过 C ABI)
Rust 可以暴露 C 兼容的 ABI,让 Dart FFI 直接调用:
[lib]
crate-type = ["cdylib"]
[dependencies]
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn rust_process_bytes(
input: *const u8,
length: usize,
output: *mut u8,
output_length: usize,
) -> i32 {
unsafe {
let input_slice = std::slice::from_raw_parts(input, length);
let output_slice = std::slice::from_raw_parts_mut(output, output_length);
let copy_len = length.min(output_length);
for i in 0..copy_len {
output_slice[i] = input_slice[length - 1 - i];
}
copy_len as i32
}
}
#[no_mangle]
pub extern "C" fn rust_get_version() -> *mut std::os::raw::c_char {
let version = std::ffi::CString::new("1.0.0").unwrap();
version.into_raw()
}
#[no_mangle]
pub extern "C" fn rust_free_string(ptr: *mut std::os::raw::c_char) {
unsafe {
if !ptr.is_null() {
drop(std::ffi::CString::from_raw(ptr));
}
}
}
class RustBindings {
late final DynamicLibrary _lib;
RustBindings() {
_lib = Platform.isAndroid
? DynamicLibrary.open('libmy_rust_lib.so')
: DynamicLibrary.process();
}
// 绑定 rust_add
int add(int a, int b) {
final fn = _lib.lookupFunction<
Int32 Function(Int32, Int32),
int Function(int, int)
>('rust_add');
return fn(a, b);
}
// 绑定 rust_get_version(字符串)
String getVersion() {
final getVersionFn = _lib.lookupFunction<
Pointer<Char> Function(),
Pointer<Char> Function()
>('rust_get_version');
final freeStringFn = _lib.lookupFunction<
Void Function(Pointer<Char>),
void Function(Pointer<Char>)
>('rust_free_string');
final ptr = getVersionFn();
try {
return ptr.cast<Utf8>().toDartString();
} finally {
freeStringFn(ptr); // 用 Rust 提供的 free 函数释放
}
}
}
| 维度 | FFI | Platform Channel |
|---|
| 调用延迟 | < 1μs(同线程) | 0.5~5ms(线程切换) |
| 适用语言 | C/C++/Rust(C ABI) | 任何平台语言(Java/Kotlin/ObjC/Swift) |
| 调用 Android SDK | ❌ 无法直接调用 | ✅ 可以 |
| 调用 iOS SDK | ❌ 无法直接调用 | ✅ 可以 |
| 纯算法库 | ✅ 理想 | ⚠️ 可以但有开销 |
| 线程安全 | 需要手动管理 | Flutter 框架保障 |
| 内存管理 | 手动(malloc/free) | 自动 |
| 学习成本 | 高 | 中 |
- 需要调用 Android/iOS 系统 API → Platform Channel
- 需要集成 C/C++/Rust 算法库 → FFI
- 高频调用(每帧、每音频缓冲区)→ FFI
- 一次性/低频调用 → Platform Channel(更简单)
八、实战:集成 C 加密库
// 假设集成 libsodium 加密库
class SodiumWrapper {
late final DynamicLibrary _lib;
// 函数绑定
late final int Function() _sodiumInit;
late final int Function(
Pointer<Uint8> ciphertext,
Pointer<Uint64> ciphertextLen,
Pointer<Uint8> plaintext,
int plaintextLen,
Pointer<Uint8> nonce,
Pointer<Uint8> key,
) _cryptoSecretboxEasy;
late final int Function(
Pointer<Uint8> plaintext,
Pointer<Uint64> plaintextLen,
Pointer<Uint8> ciphertext,
int ciphertextLen,
Pointer<Uint8> nonce,
Pointer<Uint8> key,
) _cryptoSecretboxOpenEasy;
SodiumWrapper() {
_lib = _loadLibrary();
_initFunctions();
// 初始化 libsodium(必须首先调用)
final result = _sodiumInit();
if (result != 0) throw FFIException('sodium_init failed: $result');
}
void _initFunctions() {
_sodiumInit = _lib.lookupFunction<Int32 Function(), int Function()>(
'sodium_init',
);
// ... 绑定其他函数
}
Uint8List encrypt(Uint8List plaintext, Uint8List key, Uint8List nonce) {
// 输出大小 = 明文大小 + MAC 大小(16 字节)
const macBytes = 16;
final ciphertextLen = plaintext.length + macBytes;
return using((arena) {
final plaintextPtr = arena.allocate<Uint8>(plaintext.length);
final keyPtr = arena.allocate<Uint8>(key.length);
final noncePtr = arena.allocate<Uint8>(nonce.length);
final ciphertextPtr = arena.allocate<Uint8>(ciphertextLen);
plaintextPtr.asTypedList(plaintext.length).setAll(0, plaintext);
keyPtr.asTypedList(key.length).setAll(0, key);
noncePtr.asTypedList(nonce.length).setAll(0, nonce);
final result = _cryptoSecretboxEasy(
ciphertextPtr,
nullptr, // 可选:输出实际写入的长度
plaintextPtr,
plaintext.length,
noncePtr,
keyPtr,
);
if (result != 0) throw FFIException('Encryption failed');
return Uint8List.fromList(ciphertextPtr.asTypedList(ciphertextLen));
});
}
static DynamicLibrary _loadLibrary() {
if (Platform.isAndroid) return DynamicLibrary.open('libsodium.so');
if (Platform.isIOS) return DynamicLibrary.process();
throw UnsupportedError('Platform not supported');
}
}
九、常见陷阱与最佳实践
9.1 内存泄漏检测
// ❌ 内存泄漏:早期返回导致 free 未执行
Future<void> badExample(Uint8List data) async {
final ptr = malloc.allocate<Uint8>(data.length);
if (data.isEmpty) return; // ← 忘记 free ptr!
processData(ptr, data.length);
malloc.free(ptr);
}
// ✅ 使用 try/finally 确保释放
Future<void> goodExample(Uint8List data) async {
final ptr = malloc.allocate<Uint8>(data.length);
try {
if (data.isEmpty) return; // ← 即使早返回,finally 也会执行
processData(ptr, data.length);
} finally {
malloc.free(ptr); // 始终执行
}
}
// ✅ 更好:使用 using/Arena
Future<void> bestExample(Uint8List data) async {
return using((arena) {
final ptr = arena.allocate<Uint8>(data.length);
if (data.isEmpty) return; // ← Arena 在 using 结束时自动释放
processData(ptr, data.length);
});
}
9.2 野指针问题
// ❌ 悬空指针:使用已释放的内存
final ptr = malloc.allocate<Int32>(1);
ptr.value = 42;
malloc.free(ptr); // 释放
print(ptr.value); // ← 未定义行为!访问已释放内存
// ❌ C 函数返回的指针,不能用 malloc.free
final resultPtr = cLibGetResult(); // C 库内部 malloc 的内存
malloc.free(resultPtr); // ← 错误!必须用 C 库的 free 函数
cLibFreeResult(resultPtr); // ← 正确
9.3 与 Isolate 结合使用
// FFI 调用在 Isolate 中是安全的
// 但要注意:Pointer 不能跨 Isolate 传递(内存地址无意义)
// ✅ 正确:在同一 Isolate 中使用 FFI
Uint8List compressInBackground(Uint8List data) {
return Isolate.run(() {
// 在 Worker Isolate 中加载库并调用(每个 Isolate 独立加载)
final lib = DynamicLibrary.open('libcompressor.so');
// ... 使用 FFI 处理 data(Uint8List 会被拷贝到 Worker Isolate)
return result;
});
}
// ❌ 错误:跨 Isolate 传递 Pointer
final ptr = malloc.allocate<Uint8>(100);
Isolate.run(() {
ptr.value = 42; // ← 危险!ptr 在 Worker Isolate 中无效
});
十、过关自测
问题一:FFI 调用为什么不需要线程切换,而 Platform Channel 需要?
Platform Channel 要求处理器在平台主线程执行(因为它们通常需要访问 Android/iOS UI API),所以每次调用都需要从 Flutter UI 线程切换到平台主线程,再切回来。FFI 是直接的函数调用——C 函数在调用方的线程(即 Flutter UI 线程或 Worker Isolate)内直接执行,没有线程边界需要跨越,所以没有线程切换开销。
问题二:为什么 C 库分配的内存不能用 malloc.free 释放?
malloc.free 调用的是 Dart 的 ffi 包中的 malloc,它使用的是 C 标准库的 free。问题在于:① 不同的 C 运行时(CRT)有各自独立的堆,Windows 上尤为严重——DLL 使用不同 CRT 时,跨 CRT 释放内存会导致堆损坏;② C 库可能使用自定义内存分配器;③ C 库可能对分配的内存有额外的引用计数或元数据。因此,规则是:谁分配,谁释放——C 库分配的内存必须通过 C 库提供的 free 函数释放。
问题三:什么是 NativeCallable.listener 与 NativeCallable.isolateLocal 的区别?
NativeCallable.listener:回调通过 Dart Port 机制排入当前 Isolate 的事件队列中执行,保证线程安全,C 可以从任意线程调用这个函数指针。NativeCallable.isolateLocal:回调在调用该函数指针的线程上直接执行,没有队列中转。前者更安全(自动处理跨线程调用),适合 C 库在后台线程回调的场景;后者性能更高(无队列开销),但只能在当前 Isolate 的线程上被调用,否则会崩溃。
总结
| 技术 | 关键点 |
|---|
dart:ffi 基础 | Native 类型与 Dart 类型映射;DynamicLibrary.open/process |
| 内存管理 | malloc/calloc;using + Arena;谁分配谁释放 |
| 结构体 | extends Struct;@Int32() 注解;.ref 访问 |
| 回调 | NativeCallable.listener/isolateLocal;用完 close() |
| ffigen | 自动生成绑定;pubspec.yaml 配置头文件路径 |
| Rust 互操作 | extern "C" + #[no_mangle];cdylib crate 类型 |
| 选择原则 | 高频+算法密集 → FFI;系统API/低频 → Platform Channel |
Dart FFI 代表了 Flutter 平台能力的最高点——以原生代码的性能,享受 Dart 的开发体验。它的学习曲线陡峭,内存管理要求严格,但一旦掌握,你就拥有了调用整个 C/C++/Rust 生态的能力。