使用Dart FFI访问Flutter中的本地库
Dart是一种功能丰富的语言,有很好的文档记录,易于学习;然而,当涉及到Flutter应用开发时,它可能缺乏一些功能。例如,可能需要一个应用程序链接到一个外部二进制库,或者用C、C+或Rust等低级语言编写一些代码可能是有益的。
幸运的是,Flutter应用程序能够通过dart:ffi library
.com使用外国函数接口(FFI)。FFI使用一种语言编写的程序可以调用用其他语言编写的库。例如,通过FFI,Flutter应用程序可以调用一个基于C语言的编译库,如cJSON.dylib
,或直接从Dart调用C语言的源代码,如lib/utils.c
。
在Dart中拥有FFI互操作机制的一个核心好处是,它使我们能够用任何编译为C库的语言编写代码。一些例子是Go和Rust。
FFI还使我们能够使用相同的代码在不同的平台上提供相同的功能。例如,假设我们想在所有媒体中利用一个特定的开源库,而不需要投入时间和精力在每个应用程序的开发语言(Swift、Kotlin等)中编写相同的逻辑。一个解决方案是用C或Rust实现代码,然后用FFI将其暴露在Flutter应用程序中。
Dart FFI开辟了新的开发机会,特别是对于需要在团队和项目之间共享原生代码或提升应用性能的项目。
在这篇文章中,我们将研究如何使用Dart FFI来访问Flutter中的本地库。
首先,让我们从基本知识和基础开始。
使用Dart FFI来访问动态库
让我们从用C语言编写一个基本的数学函数开始,我们将在一个简单的Dart应用程序中使用它。
/// native/add.c
int add(int a, int b)
{
return a + b;
}
一个本地库可以静态或动态地链接到一个应用程序中。一个静态链接的库被嵌入到应用程序的可执行图像中。它在应用程序启动时加载。相比之下,动态链接的库则分布在应用程序中的一个单独的文件或文件夹中。它是按需加载的。
我们可以通过运行以下代码将我们的C
文件转换为动态库dylib
。
gcc -dynamiclib add.c -o libadd.dylib
这将导致以下输出:add.dylib
。
我们将按照三个步骤在Dart中调用这个函数。
- 打开包含该函数的动态库
- 查阅函数(***N.B,***因为C和Dart中的类型不同,我们必须分别指定)
- 调用该函数
/// run.dart
import 'dart:developer' as dev;
import 'package:path/path.dart';
import 'dart:ffi';void main() {
final path = absolute('native/libadd.dylib');
dev.log('path to lib $path');
final dylib = DynamicLibrary.open(path);
final add = dylib.lookupFunction('add');
dev.log('calling native function');
final result = add(40, 2);
dev.log('result is $result'); // 42
}
这个例子说明,我们可以采用FFI在Dart应用程序中轻松使用任何动态库。
现在,是时候介绍一个可以通过代码生成帮助生成FFI绑定的工具了。
用FFIGEN在Dart中生成FFI绑定
可能有的时候,为Dart FFI编写绑定代码会过于耗时或繁琐。在这种情况下,Foreign Function Interface GENerator(ffigen
)会很有帮助。ffigen
是一个FFI的绑定生成器。它帮助解析C
头文件并自动生成dart
代码。
让我们使用这个包含基本数学函数的C
头文件的例子。
/// native/math.h
/** Adds 2 integers. */
int sum(int a, int b);
/** Subtracts 2 integers. */
int subtract(int *a, int b);
/** Multiplies 2 integers, returns pointer to an integer,. */
int *multiply(int a, int b);
/** Divides 2 integers, returns pointer to a float. */
float *divide(int a, int b);
/** Divides 2 floats, returns a pointer to double. */
double *dividePercision(float *a, float *b);
为了在Dart中生成FFI的绑定,我们将在pubspec.yml
文件中把ffigen
添加到dev_dependencies
。
/// pubspec.yaml
dev_dependencies:
ffigen: ^4.1.2
ffigen
要求将配置作为一个单独的config.yaml
文件添加,或者添加在pubspec.yaml
的ffigen
下,如图所示。
/// pubspec.yaml
....
ffigen:
name: 'MathUtilsFFI'
description: 'Written for the FFI article'
output: 'lib/ffi/generated_bindings.dart'
headers:
entry-points:
- 'native/headers/math.h'
应该生成的entry-points
和output
文件是强制性的字段;但是,我们也可以定义并包括一个name
和description
。
接下来,我们将运行以下代码。
dart run ffigen
这将导致以下输出。generated_bindings.dart
现在,我们可以在我们的Dart文件中使用MathUtilsFFI
类。
在一个演示中使用FFIGEN
现在我们已经介绍了ffigen
的基本知识,让我们来看看一个演示。
生成动态库
在这个演示中,我们将使用cJSON,它是一个超轻量级的JSON解析器,可用于Flutter
或Dart
应用程序。
整个cJSON库由一个C文件和一个头文件组成,所以我们可以简单地将cJSON.c
和cJSON.h
复制到我们项目的源代码中。然而,我们还需要使用CMake构建系统。CMake被推荐用于树外构建,即构建目录(包含编译文件)与源文件目录(包含源文件)分开。截至目前,CMake的版本为2.8.5或更高。
要在Unix平台上用CMake构建cJSON,我们首先建立一个build
目录,然后在该目录下运行CMake。
cd native/cJSON // where I have copied the source files
mkdir build
cd build
cmake ..
下面是输出结果。
-- The C compiler identification is AppleClang 13.0.0.13000029
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Performing Test FLAG_SUPPORTED_fvisibilityhidden
-- Performing Test FLAG_SUPPORTED_fvisibilityhidden - Success
-- Configuring done
-- Generating done
-- Build files have been written to: ./my_app_sample/native/cJSON/build
这将创建一个Makefile,以及其他几个文件。
我们用这个命令来编译。
make
构建进度条会前进,直到完成。
[ 88%] Built target readme_examples
[ 91%] Building C object tests/CMakeFiles/minify_tests.dir/minify_tests.c.o
[ 93%] Linking C executable minify_tests
[ 93%] Built target minify_tests
[ 95%] Building C object fuzzing/CMakeFiles/fuzz_main.dir/fuzz_main.c.o
[ 97%] Building C object fuzzing/CMakeFiles/fuzz_main.dir/cjson_read_fuzzer.c.o
[100%] Linking C executable fuzz_main
[100%] Built target fuzz_main
动态库是根据平台生成的。例如,Mac用户会看到libcjson.dylib
,而Windows用户可能看到cjson.dll
,Linux用户可能看到libcjson.so
。
生成Dart FFI绑定文件
接下来,我们需要生成Dart FFI绑定文件。为了演示如何使用分离配置,我们将创建一个新的配置文件,cJSON.config.yaml
,并配置cJSON库。
// cJSON.config.yaml
output: 'lib/ffi/cjson_generated_bindings.dart'
name: 'CJson'
description: 'Holds bindings to cJSON.'
headers:
entry-points:
- 'native/cJSON/cJSON.h'
include-directives:
- '**cJSON.h'
comments: false
typedef-map:
'size_t': 'IntPtr'
为了生成FFI绑定文件。我们必须运行dart run ffigen --config cJSON.config.yaml
。
> flutter pub run ffigen --config cJSON.config.yaml
Changing current working directory to: /**/my_app_sample
Running in Directory: '/**/my_app_sample'
Input Headers: [native/cJSON/cJSON.h]
Finished, Bindings generated in /**/my_app_sample/lib/ffi/cjson_generated_bindings.dart
为了使用这个库,我们创建一个JSON文件。
/// example.json
{
"name": "Majid Hajian",
"age": 30,
"nicknames": [
{
"name": "Mr. Majid",
"length": 9
},
{
"name": "Mr. Dart",
"length": 8
}
]
}
这个JSON文件的例子很简单,但想象一下同样的过程,重的JSON,需要执行解析。
加载该库
首先,我们必须确保我们正在正确地加载动态库。
/// cJSON.dart
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:path/path.dart' as p;
import './lib/ffi/cjson_generated_bindings.dart' as cj;
String _getPath() {
final cjsonExamplePath = Directory.current.absolute.path;
var path = p.join(cjsonExamplePath, 'native/cJSON/build/');
if (Platform.isMacOS) {
path = p.join(path, 'libcjson.dylib');
} else if (Platform.isWindows) {
path = p.join(path, 'Debug', 'cjson.dll');
} else {
path = p.join(path, 'libcjson.so');
}
return path;
}
接下来,我们打开动态库。
final cjson = cj.CJson(DynamicLibrary.open(_getPath()));
现在,我们可以使用生成的cJSON绑定了。
/// cJSON.dart
void main() {
final pathToJson = p.absolute('example.json');
final jsonString = File(pathToJson).readAsStringSync();
final cjsonParsedJson = cjson.cJSON_Parse(jsonString.toNativeUtf8().cast());
if (cjsonParsedJson == nullptr) {
print('Error parsing cjson.');
exit(1);
}
// The json is now stored in some C data structure which we need
// to iterate and convert to a dart object (map/list).
// Converting cjson object to a dart object.
final dynamic dartJson = convertCJsonToDartObj(cjsonParsedJson.cast());
// Delete the cjsonParsedJson object.
cjson.cJSON_Delete(cjsonParsedJson);
// Check if the converted json is correct
// by comparing the result with json converted by `dart:convert`.
if (dartJson.toString() == json.decode(jsonString).toString()) {
print('Parsed Json: $dartJson');
print('Json converted successfully');
} else {
print("Converted json doesn't match\n");
print('Actual:\n' + dartJson.toString() + '\n');
print('Expected:\n' + json.decode(jsonString).toString());
}
}
接下来,我们可以使用辅助函数来解析(或转换)cJSON为Dart对象。
/// main.dart
dynamic convertCJsonToDartObj(Pointer<cj.cJSON> parsedcjson) {
dynamic obj;
if (cjson.cJSON_IsObject(parsedcjson.cast()) == 1) {
obj = <String, dynamic>{};
Pointer<cj.cJSON>? ptr;
ptr = parsedcjson.ref.child;
while (ptr != nullptr) {
final dynamic o = convertCJsonToDartObj(ptr!);
_addToObj(obj, o, ptr.ref.string.cast());
ptr = ptr.ref.next;
}
} else if (cjson.cJSON_IsArray(parsedcjson.cast()) == 1) {
obj = <dynamic>[];
Pointer<cj.cJSON>? ptr;
ptr = parsedcjson.ref.child;
while (ptr != nullptr) {
final dynamic o = convertCJsonToDartObj(ptr!);
_addToObj(obj, o);
ptr = ptr.ref.next;
}
} else if (cjson.cJSON_IsString(parsedcjson.cast()) == 1) {
obj = parsedcjson.ref.valuestring.cast<Utf8>().toDartString();
} else if (cjson.cJSON_IsNumber(parsedcjson.cast()) == 1) {
obj = parsedcjson.ref.valueint == parsedcjson.ref.valuedouble
? parsedcjson.ref.valueint
: parsedcjson.ref.valuedouble;
}
return obj;
}
void _addToObj(dynamic obj, dynamic o, [Pointer<Utf8>? name]) {
if (obj is Map<String, dynamic>) {
obj[name!.toDartString()] = o;
} else if (obj is List<dynamic>) {
obj.add(o);
}
}
使用FFI将字符串从C语言传给Dart
[ffi]
包可以用来将字符串从C语言传递到Dart。我们把这个包添加到我们的依赖项中。
/// pubspec.yaml
dependencies:
ffi: ^1.1.2
测试调用
现在,让我们检查一下我们的演示是否成功
我们可以看到在这个例子中,name
,age
, 和nicknames
的C语言字符串被成功解析为Dart。
> dart cJSON.dart
Parsed Json: {name: Majid Hajian, age: 30, nicknames: [{name: Mr. Majid, length: 9}, {name: Mr. Dart, length: 8}]}
Json converted successfully
现在我们已经回顾了FFI的要点,让我们看看如何在Flutter中使用它们。
使用FFI将动态库添加到Flutter应用程序中
Dart FFI的大部分概念也适用于Flutter。为了简化本教程,我们将专注于Android和iOS,但这些方法也适用于其他应用程序。
为了使用FFI向Flutter应用程序添加动态库,我们将遵循以下步骤。
配置Android Studio C语言编译器
为了配置Android Studio C编译器,我们将遵循三个步骤。
-
转到
android/app
-
创建一个
CMakeLists.txt
file:cmake
-
打开
android/app/build.gradle
,添加以下代码段:android { ....externalNativeBuild { cmake { path "CMakeLists.txt" } }... }
这段代码告诉Android构建系统在构建应用程序时用CMakeLists.txt
来调用CMake
。它将在Android上把.c
源文件编译成一个共享对象库,后缀为.so
。
配置Xcode的C编译器
为了确保Xcode将用本地C代码构建我们的应用程序,我们将遵循以下10个步骤。
- 通过运行来打开Xcode工作区。
open< ios/Runner.xcworkspace
- 从顶部导航栏的Targets下拉菜单中,选择Runner
- 从标签行中,选择构建阶段
- 展开Compile Sources标签,并点击**+**键。
- 在弹出的窗口中,点击添加其他
- 导航到C文件的存储位置,例如:
FLUTTER_PROJCT_ROOT/DART/native/cJSON/cJSON.c
,并添加cJSON.c
和cJSON.h
两个文件。 - 展开 "编译源"标签,点击 "+"键
- 在弹出的窗口中,点击添加其他
- 导航到r
.c
文件的存储位置,例如。FLUTTER_PROJCT_ROOT/DART/native/cJSON/cJSON.c
- 选择复制项目(如果需要),然后点击完成
现在,我们准备将生成的Dart绑定代码添加到Flutter应用程序中,加载库,并调用函数。
生成FFI绑定代码
我们将使用ffigen
来生成绑定代码。首先,我们将添加ffigen
到Flutter应用程序。
/// pubspec.yaml for my Flutter project
...
dependencies:
ffigen: ^4.1.2
...
ffigen:
output: 'lib/ffi/cjson_generated_bindings.dart'
name: 'CJson'
description: 'Holds bindings to cJSON.'
headers:
entry-points:
- 'DART/native/cJSON/cJSON.h'
include-directives:
- '**cJSON.h'
comments: false
typedef-map:
'size_t': 'IntPtr'
接下来,我们将运行ffigen
。
flutter pub run ffigen
我们需要确保example.json
文件被添加到assets下。
/// pubspec.yaml
...
flutter:
uses-material-design: true
assets:
- example.json
...
加载动态库
就像静态链接库可以被嵌入到应用程序启动时加载一样,静态链接库中的符号可以使用DynamicLibrary.executable
或DynamicLibrary.process
来加载。
在Android上,动态链接库是以一组.so
(ELF)文件的形式发布的,每个架构都有一个。在iOS上,动态链接的库以.framework
文件夹的形式发布。
一个动态链接的库可以通过DynamicLibrary.open
命令加载到Dart中。
我们将使用下面的代码来加载该库。
/// lib/ffi_loader.dart
import 'dart:convert';
import 'dart:developer' as dev_tools;
import 'dart:ffi';
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:my_app_sample/ffi/cjson_generated_bindings.dart' as cj;
class MyNativeCJson {
MyNativeCJson({
required this.pathToJson,
}) {
final cJSONNative = Platform.isAndroid
? DynamicLibrary.open('libcjson.so')
: DynamicLibrary.process();
cjson = cj.CJson(cJSONNative);
}
late cj.CJson cjson;
final String pathToJson;
Future<void> load() async {
final jsonString = await rootBundle.loadString('assets/$pathToJson');
final cjsonParsedJson = cjson.cJSON_Parse(jsonString.toNativeUtf8().cast());
if (cjsonParsedJson == nullptr) {
dev_tools.log('Error parsing cjson.');
}
final dynamic dartJson = convertCJsonToDartObj(cjsonParsedJson.cast());
cjson.cJSON_Delete(cjsonParsedJson);
if (dartJson.toString() == json.decode(jsonString).toString()) {
dev_tools.log('Parsed Json: $dartJson');
dev_tools.log('Json converted successfully');
} else {
dev_tools.log("Converted json doesn't match\n");
dev_tools.log('Actual:\n$dartJson\n');
dev_tools.log('Expected:\n${json.decode(jsonString)}');
}
}
dynamic convertCJsonToDartObj(Pointer<cj.cJSON> parsedcjson) {
dynamic obj;
if (cjson.cJSON_IsObject(parsedcjson.cast()) == 1) {
obj = <String, dynamic>{};
Pointer<cj.cJSON>? ptr;
ptr = parsedcjson.ref.child;
while (ptr != nullptr) {
final dynamic o = convertCJsonToDartObj(ptr!);
_addToObj(obj, o, ptr.ref.string.cast());
ptr = ptr.ref.next;
}
} else if (cjson.cJSON_IsArray(parsedcjson.cast()) == 1) {
obj = <dynamic>[];
Pointer<cj.cJSON>? ptr;
ptr = parsedcjson.ref.child;
while (ptr != nullptr) {
final dynamic o = convertCJsonToDartObj(ptr!);
_addToObj(obj, o);
ptr = ptr.ref.next;
}
} else if (cjson.cJSON_IsString(parsedcjson.cast()) == 1) {
obj = parsedcjson.ref.valuestring.cast<Utf8>().toDartString();
} else if (cjson.cJSON_IsNumber(parsedcjson.cast()) == 1) {
obj = parsedcjson.ref.valueint == parsedcjson.ref.valuedouble
? parsedcjson.ref.valueint
: parsedcjson.ref.valuedouble;
}
return obj;
}
void _addToObj(dynamic obj, dynamic o, [Pointer<Utf8>? name]) {
if (obj is Map<String, dynamic>) {
obj[name!.toDartString()] = o;
} else if (obj is List<dynamic>) {
obj.add(o);
}
}
}
对于安卓系统,我们调用DynamicLibrary
,找到并打开libcjson.so
共享库。
final cJSONNative = Platform.isAndroid
? DynamicLibrary.open('libcJSON.so')
: DynamicLibrary.process();
cjson = cj.CJson(cJSONNative);
在iOS中不需要这个特别的步骤,因为所有链接的符号在iOS应用运行时都会映射。
测试Flutter中的调用
为了证明本地调用在Flutter中是有效的,我们在main.dart
文件中添加用法。
// main.dart
import 'package:flutter/material.dart';
import 'ffi_loader.dart';
void main() {
runApp(const MyApp());
final cJson = MyNativeCJson(pathToJson: 'example.json');
await cJson.load();
}
接下来,我们运行该应用程序。flutter run
Voilà!我们已经成功地从我们的Flutter应用中调用了本地库。
我们可以在控制台中查看本地调用的日志。
Launching lib/main_development.dart on iPhone 13 in debug mode...
lib/main_development.dart:1
Xcode build done. 16.5s
Connecting to VM Service at ws://127.0.0.1:53265/9P2HdUg5_Ak=/ws
[log] Parsed Json: {name: Majid Hajian, age: 30, nicknames: [{name: Mr. Majid, length: 9}, {name: Mr. Dart, length: 8}]}
[log] Json converted successfully
展望未来,我们可以在我们的Flutter应用中的不同部件和服务中使用这个库。
总结
Dart FFI为将本地库集成到Dart和Flutter应用程序提供了一个简单的解决方案。在这篇文章中,我们已经演示了如何使用Dart FFI在Dart中调用C函数,并将C库集成到Flutter应用程序中。
你可能想进一步尝试使用Dart FFI,使用用其他语言编写的代码。我对实验Go和Rust特别感兴趣,因为这些语言是内存管理的。Rust特别有趣,因为它是一种内存安全的语言,而且性能相当好。
本文中使用的所有例子都可以在GitHub上找到。
The postUsing Dart FFI to access native libraries in Flutterappeared first onLogRocket Blog.
转载自:https://juejin.cn/post/7067793861111709727