在 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類型
typedef hello_world_func = ffi.Void Function();
// 將ffi類型定義為dart類型
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 示例中是直接使用 make, 內部使用 gcc 打包編譯
這里有一個腳本,是設置 dylib 的目錄到環境變量中, 以便於運行時可以找到動態庫
在 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 使用的文件
接着就可以運行項目了, 和 android 中一樣
簡單總結
現在 ffi 處於初始階段, 還有諸多不足.
比如, 文檔的缺失, 現在如何傳遞字符串,數組都是問題, 雖然有結構體的定義, 也能看到部分說明, 但沒有簡單的示例幫助開發者快速使用.
只有基本數據類型, 目前可能還不需要借用 c 來解決, 未來則要看 ffi 會開放到什么程度.
后記
項目地址: https://github.com/caijinglong/example_for_flutter_ffi