在 Flutter 的 1.10.x 后的分支,dart:ffi 被并入 flutter,现在 flutter 中也可以使用 ffi 了。 这东西是啥玩意呢,就是让 dart 可以直接调用 c/c++ 代码等东西的库,FFI(foreign function interface), 官方文档在这里。 但是在当前版本中,这东西在官方说明中依然处于技术预览版,就是可用,但后续不保证 API 不变更。
开发环境
首先我是 mac 系统,windows 系统不保证脚本的可用和工具的可用,linux 的话可能一些必要工具需要使用自己平台的包管理工具,并且涉及到 iOS 部分, 必须使用 mac。
所有需要的工具包:
- Xcode(或 XcodeBuild 命令行工具)
- brew
- clang
- CMake
- Android 工具链
- Android SDK
- NDK
- Android Studio(可选)
- Gradle
- Flutter 工具链
- SDK 1.10.x+
- VSCode(可选,这东西看你的情况,作为示例的话只要是文本编辑器即可,我本人使用这个作为主要的文本编辑器)
这里说的是包含后续所有用到的东西,并不仅仅是本文。 其中对于 Flutter 开发者可能需要单独安装的应该只有 NDK 和 CMake,这两个东西是包含在 Android SDK 下的,可以使用 Android Studio 下载,也可以单独下载。
ffi 的简单介绍
根据官方文档说明
可以理解为,将 c 的类型和 dart 的类型关联起来,然后 ffi 会在内部将两端关联起来,完成调用。
有如下几种类型
基本就是对应 c 中的类型,对应 Void 各种长度的有无符号的整型、单双精度浮点、指针、方法。
转化的过程
C 源码核心就这点,打印即可。
void hello_world() {
printf("Hello World\n");
}
导包,这个是第一步要做的。
import 'dart:ffi' as ffi;
// 定义一个 ffi 类型,包装成 c 识别的 typedef
typedef hello_world_func = ffi.Void Function();
// 将 ffi 类型定义为 dart 类型,返回值也统一成 typedef
typedef HelloWorld = void Function();
// 打开动态库, dylib 是 mac 上的动态库的后缀
final dylib = ffi.DynamicLibrary.open('hello_world.dylib');
// 这里是最难理解的一步, 后面会详细解说
final HelloWorld hello = dylib
.lookup<ffi.NativeFunction<hello_world_func>>('hello_world')
.asFunction();
// 调用
hello();
详细理解转化过程
这里以 lookup 方法为切入点,详细理解下这里做了什么,以便于后面我们可以自行完成这个过程,lookup 方法签名如下:
external Pointer<T> lookup<T extends NativeType>(String symbolName);
参数
很好理解,传入一个方法名,让我们能找到 c 方法。
泛型
这个是方法的类型签名的 dart:ffi 表现形式。c 方法的签名是这样的: void hello_world(),所以我们就需要一个对应的类型,也就是上面定义的 ffi 类型 ffi.Void Function()。
返回类型
这里的返回值是用于在实际调用时,转化 c 方法的返回值为 dart 的类型来使用的,所以就是对应的 dart 类型。
/// 定义是这样的
void Function()
/// 接收的 asFunction 方法
final void Function() hello = XXXX;
写起来的时候可能是这样的。
实例
extern "C" {
// __attribute__((visibility("default"))) __attribute__((used)) // 虽然说需要这行, 但是没这行也没报错
int32_t native_add(int32_t x, int32_t y) { return x + y; }
double double_add(double x, double y) { return x + y; }
}
import 'dart:ffi';
final DynamicLibrary dylib = Platform.isAndroid
? DynamicLibrary.open("libnative_add.so")
: DynamicLibrary.open("native_add.framework/native_add");
final int Function(int x, int y) nativeAdd = dylib
.lookup<NativeFunction<Int32 Function(Int32, Int32)>>("native_add")
.asFunction();
final double Function(double, double) doubleAdd = dylib
.lookup<NativeFunction<Double Function(Double, Double)>>("double_add")
.asFunction();
打包和运行
在 dart vm 中,可以有多种方案,只要能编译出 dylib 即可,官方的 hello world 示例中是直接使用 CMake,内部使用 GCC 打包编译。
设置 dylib 的目录到环境变量中, 以便于运行时可以找到动态库。
#!/bin/bash
export DYLD_LIBRARY_PATH=.:$DYLD_LIBRARY_PATH
在 Flutter 中使用
接着就要开始在 Flutter 中使用了,和在 dart vm 中使用不一样,不能使用环境变量,而是需要将库置入到项目中。
创建仓库
直接使用 flutter create -t plugin native_add 的方式即可。
cpp 文件
native_add.cpp
#include <stdint.h>
extern "C" {
// __attribute__((visibility("default"))) __attribute__((used))
int32_t native_add(int32_t x, int32_t y) { return x + y; }
double double_add(double x, double y) { return x + y; }
}
dart 文件
final DynamicLibrary dylib = Platform.isAndroid
? DynamicLibrary.open("libnative_add.so")
: DynamicLibrary.open("native_add.framework/native_add");
final int Function(int x, int y) nativeAdd = dylib
.lookup<NativeFunction<Int32 Function(Int32, Int32)>>("native_add")
.asFunction();
final double Function(double, double) doubleAdd = dylib
.lookup<NativeFunction<Double Function(Double, Double)>>("double_add")
.asFunction();
界面:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter = nativeAdd(_counter, 1);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
Text(
"native double value = ${doubleAdd(_counter.toDouble(), _counter.toDouble())}"),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
iOS
iOS 中,直接将 cpp 文件置入 ios/classes 文件夹内即可,然后因为 podspec 中包含默认配置的原因,这个文件会被自动引入项目。
s.source_files = 'Classes/**/*'
运行项目:
Android
Android 中其实有两种方法,一是用传统的 ndk 方式,就是 Android.mk 那种方案,我们略过这种方案,因为配置比较复杂,我们使用第二种方案,官方推荐的 CMake 方案。 因为 iOS 中,文件被置入源码中,我这里直接使用相对路径去引入这个文件。
CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
# for example
add_library( native_add
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
../ios/Classes/native_add.cpp )
- 指定源码对应的库是哪个库。
- 指定库的类型,这里是动态库,所以用 SHARED。
- 指定源码目录。
然后因为我们使用了 cmake,为了让安卓项目知道,我们需要修改 gradle 文件。
android{
// ...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
这里在 Android 节点下,添加属性即可,这里是指定 CMake 使用的文件。
简单总结
现在 ffi 处于初始阶段,还有诸多不足。比如,文档的缺失,现在如何传递字符串,数组都是问题,虽然有结构体的定义,也能看到部分说明,但没有简单的示例帮助开发者快速使用。只有基本数据类型,目前可能还不需要借用 c 来解决,未来则要看 ffi 会开放到什么程度。