FFI(Foreign Function Interface),即外部函数接口,是 Dart 语言提供的一种机制,允许 Dart 代码直接调用用其他语言(如 C、C++)编写的动态链接库(DLLs 或 shared libraries)中的函数,以及被其他语言调用。这使得 Dart 应用程序能够利用现有的高性能原生代码库,或者在需要与操作系统底层功能交互时提供更强大的能力。
1. FFI 的核心概念
dart:ffi 库:Dart 语言内置的 FFI 库,提供了与 C 语言兼容的类型和函数,用于加载动态库、查找函数指针、调用原生函数以及在 Dart 和 C 之间传递数据。.dll (Windows)、.so (Linux) 或 .dylib (macOS) 文件的形式存在。dart:ffi 提供了 int8_t、int16_t、int32_t、int64_t、float、double、Pointer 等类型,它们与 C 语言中的对应类型兼容。Pointer<T>:表示 C 语言中的指针。T 是指针指向的类型。例如,Pointer<Int32> 表示一个指向 32 位整数的指针。NativeFunction:用于声明 C 语言函数的 Dart 类型签名。DynamicLibrary:用于加载动态链接库。2. FFI 的基本使用步骤
DynamicLibrary.open() 加载 .dll、.so 或 .dylib 文件。lookupFunction() 方法查找原生函数的指针,并将其转换为 Dart 函数。typedef 定义 C 函数的 Dart 类型签名和 Dart 函数的类型签名。示例:调用 C 语言的加法函数
首先,我们需要一个 C 语言的动态链接库。创建一个 add.c 文件:
// add.c
#include <stdio.h>
// 导出函数,Windows 下需要 __declspec(dllexport)
#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
EXPORT int add(int a, int b) {
return a + b;
}
编译 add.c 为动态链接库:
gcc -shared -o libadd.so add.c (或 libadd.dylib for macOS)gcc -shared -o add.dll add.c然后,在 Dart 中调用:
import 'dart:ffi';
import 'dart:io' show Platform;
// 1. 定义 C 函数的类型签名
typedef AddFunctionC = Int32 Function(Int32 a, Int32 b);
// 2. 定义 Dart 函数的类型签名
typedef AddFunctionDart = int Function(int a, int b);
void main() {
// 3. 加载动态库
final DynamicLibrary nativeLib = DynamicLibrary.open(
Platform.isWindows ? 'add.dll' : (Platform.isMacOS ? 'libadd.dylib' : 'libadd.so'),
);
// 4. 查找函数指针并转换为 Dart 函数
final AddFunctionDart add = nativeLib
.lookup<NativeFunction<AddFunctionC>>('add')
.asFunction();
// 5. 调用原生函数
int result = add(10, 20);
print('10 + 20 = $result'); // 输出: 10 + 20 = 30
}
3. 传递复杂数据结构
FFI 不仅可以传递基本数据类型,还可以传递更复杂的数据结构,如结构体(Structs)和字符串。这通常涉及到 Pointer 和 Struct 的使用。
// 假设 C 语言中有一个结构体:
// struct Point { int x; int y; };
// 在 Dart 中定义对应的结构体
import 'dart:ffi';
import 'package:ffi/ffi.dart'; // 用于内存分配
class Point extends Struct {
@Int32() // 映射到 C 的 int
external int x;
@Int32()
external int y;
}
// 假设 C 语言中有一个函数:
// EXPORT void print_point(struct Point p) { printf("Point: (%d, %d)\n", p.x, p.y); }
// 在 Dart 中定义 C 函数的类型签名
typedef PrintPointFunctionC = Void Function(Point p);
// 在 Dart 中定义 Dart 函数的类型签名
typedef PrintPointFunctionDart = void Function(Point p);
void main() {
// ... 加载动态库 nativeLib ...
final PrintPointFunctionDart printPoint = nativeLib
.lookup<NativeFunction<PrintPointFunctionC>>('print_point')
.asFunction();
// 分配内存并创建 Point 结构体实例
final Pointer<Point> p = calloc<Point>();
p.ref.x = 100;
p.ref.y = 200;
printPoint(p.ref); // 调用原生函数,传递结构体
calloc.free(p); // 释放内存
}
FFI 在 Flutter 开发中是一个高级特性,通常用于以下场景:
案例:使用 FFI 调用原生库进行简单的字符串反转
我们将创建一个简单的 Flutter 应用,通过 FFI 调用一个 C 语言函数来反转字符串。这需要先编写 C 代码并编译成动态库,然后 Dart/Flutter 应用通过 FFI 调用它。
步骤 1: 编写 C 语言代码 (string_utils.c)
// string_utils.c
#include <string.h>
#include <stdlib.h>
#ifdef _WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
// 函数:反转字符串
// 注意:这里为了简单,直接修改传入的字符串。实际应用中可能需要返回新分配的字符串。
EXPORT void reverse_string(char* str) {
int length = strlen(str);
int i, j;
char temp;
for (i = 0, j = length - 1; i < j; i++, j--) {
temp = str[i];
str[i] = str[j];
str[j] = temp;
}
}
// 函数:获取字符串长度
EXPORT int get_string_length(char* str) {
return strlen(str);
}
步骤 2: 编译 C 代码为动态库
gcc -shared -o libstring_utils.so string_utils.cgcc -shared -o libstring_utils.dylib string_utils.cgcc -shared -o string_utils.dll string_utils.c将编译好的动态库文件放置在 Flutter 项目的合适位置,例如 lib/src/native_libs/ 目录下,或者系统路径下。
步骤 3: Flutter 应用中集成 FFI
import 'dart:ffi';
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:ffi/ffi.dart'; // 用于内存分配和字符串转换
// 1. 定义 C 函数的类型签名
typedef ReverseStringC = Void Function(Pointer<Utf8> str);
typedef GetStringLengthC = Int32 Function(Pointer<Utf8> str);
// 2. 定义 Dart 函数的类型签名
typedef ReverseStringDart = void Function(Pointer<Utf8> str);
typedef GetStringLengthDart = int Function(Pointer<Utf8> str);
// 获取动态库路径
String _getLibraryPath() {
if (Platform.isWindows) {
return 'string_utils.dll';
} else if (Platform.isMacOS) {
return 'libstring_utils.dylib';
} else if (Platform.isLinux) {
return 'libstring_utils.so';
} else {
throw UnsupportedError('Unknown platform');
}
}
class FFIExampleScreen extends StatefulWidget {
const FFIExampleScreen({super.key});
@override
State<FFIExampleScreen> createState() => _FFIExampleScreenState();
}
class _FFIExampleScreenState extends State<FFIExampleScreen> {
late DynamicLibrary _nativeLib;
late ReverseStringDart _reverseString;
late GetStringLengthDart _getStringLength;
final TextEditingController _inputController = TextEditingController();
String _reversedText = '';
int _textLength = 0;
@override
void initState() {
super.initState();
_loadNativeLibrary();
}
void _loadNativeLibrary() {
try {
_nativeLib = DynamicLibrary.open(_getLibraryPath());
_reverseString = _nativeLib
.lookup<NativeFunction<ReverseStringC>>('reverse_string')
.asFunction();
_getStringLength = _nativeLib
.lookup<NativeFunction<GetStringLengthC>>('get_string_length')
.asFunction();
} catch (e) {
print('Error loading native library: $e');
// 在实际应用中,这里应该给用户提示
setState(() {
_reversedText = 'Error: Could not load native library.';
});
}
}
void _processString() {
final String originalText = _inputController.text;
if (originalText.isEmpty) {
setState(() {
_reversedText = '';
_textLength = 0;
});
return;
}
// 将 Dart 字符串转换为 C 字符串 (UTF-8 编码)
final Pointer<Utf8> cString = originalText.toNativeUtf8();
try {
// 调用 C 函数反转字符串
_reverseString(cString);
// 调用 C 函数获取字符串长度
_textLength = _getStringLength(cString);
// 将 C 字符串转换回 Dart 字符串
setState(() {
_reversedText = cString.toDartString();
});
} catch (e) {
print('Error calling native function: $e');
setState(() {
_reversedText = 'Error processing string.';
});
} finally {
// 释放 C 字符串内存
calloc.free(cString);
}
}
@override
void dispose() {
_inputController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('FFI 字符串处理'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
controller: _inputController,
decoration: const InputDecoration(
labelText: '输入文本',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _processString,
child: const Text('处理字符串'),
),
const SizedBox(height: 24),
Text(
'反转后的文本: $_reversedText',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'文本长度 (通过 C 函数获取): $_textLength',
style: const TextStyle(fontSize: 16, color: Colors.grey),
),
],
),
),
);
}
}
void main() {
runApp(const MaterialApp(home: FFIExampleScreen()));
}
案例分析:
dart:ffi 和 package:ffi/ffi.dart:这两个库是使用 FFI 的核心。dart:ffi 提供了 FFI 的基本功能,而 package:ffi/ffi.dart 提供了方便的内存分配(calloc)和 Dart 字符串与 C 字符串(Utf8)之间的转换方法。typedef ReverseStringC = Void Function(Pointer<Utf8> str);:这里定义了 C 语言中 reverse_string 函数的 Dart 类型签名。Void 对应 C 的 void 返回类型,Pointer<Utf8> 对应 C 的 char*。typedef ReverseStringDart = void Function(Pointer<Utf8> str);:这是 Dart 中调用该 C 函数时使用的 Dart 函数类型签名。DynamicLibrary.open(_getLibraryPath()):根据当前平台加载对应的动态链接库。这是 FFI 的第一步。_nativeLib.lookup<NativeFunction<ReverseStringC>>('reverse_string').asFunction():通过 lookup 方法查找 C 函数的指针,然后使用 asFunction() 将其转换为可直接调用的 Dart 函数。originalText.toNativeUtf8():将 Dart 的 String 转换为 C 语言兼容的 UTF-8 编码的字符串指针。注意: 这会分配原生内存,因此在使用完毕后必须通过 calloc.free() 释放。cString.toDartString():将 C 语言的字符串指针转换回 Dart 的 String。try-catch 块来捕获可能发生的异常,以提高应用的健壮性。这个案例展示了如何在 Flutter 应用中使用 Dart FFI 调用原生 C 语言函数。虽然 FFI 提供了强大的能力,但它也增加了开发的复杂性,包括需要管理原生内存、处理不同平台下的库路径以及进行类型映射。因此,FFI 通常只在性能关键或需要与特定原生功能深度集成时才考虑使用。
注意: 运行此案例需要:
安装 C 编译器(如 GCC)。
将 string_utils.c 编译成对应平台的动态链接库。
在 pubspec.yaml 文件中添加 ffi 依赖:
dependencies:
flutter:
sdk: flutter
ffi: ^2.1.2 # 添加此行
运行 flutter pub get 获取依赖。
确保编译好的动态库文件位于 Flutter 应用可以访问的路径。在调试时,通常将其放在项目根目录或 build 目录下,或者将其添加到系统路径中。