手把手教你写 Dart ffi
本文以step by step的方式说明了Dart ffi的使用,适合新手学习。
什么是ffi
创建sample工程
本文示例是在macos创建并运行的,最终的产物是mac可执行程序。
通过flutter create命令即可创建plugin[2]工程,以下命令中创建了名为plugin_ffi_sample的插件工程。flutter --versionFlutter 3.3.0 • channel stable • https://github.com/flutter/flutter.gitFramework • revision ffccd96b62 (6 weeks ago) • 2022-08-29 17:28:57 -0700Engine • revision 5e9e0e0aa8Tools • Dart 2.18.0 • DevTools 2.15.0
创建好工程后,执行以下命令即可编出macos的可执行文件并打开程序:flutter create --template=plugin_ffi --platforms=macos plugin_ffi_sample
接下来我们便基于plugin_ffi_sample工程进一步讲解ffi的使用。cd plugin_ffi_sample/exampleflutter run
plugin_ffi_sample 工程详解
在创建工程时指定了“--template=plugin_ffi”,所以plugin_ffi_sample是插件工程,子目录example是使用插件的示例工程。plugin_ffi_sample/example/pubspec.yaml文件中有对plugin_ffi_sample插件的依赖配置。
有关插件工程的文档可以进一步参考[3]。dependencies:flutter:sdk: flutterplugin_ffi_sample:# When depending on this package from a real application you should use:# plugin_ffi_sample: ^x.y.z# See https://dart.dev/tools/pub/dependencies#version-constraints# The example app is bundled with the plugin so we use a path dependency on# the parent directory to use the current plugin's version.path: ../
import 'package:plugin_ffi_sample/plugin_ffi_sample.dart' as plugin_ffi_sample;//...class _MyAppState extends State<MyApp> {late int sumResult;late Future<int> sumAsyncResult;@overridevoid initState() {super.initState();sumResult = plugin_ffi_sample.sum(1, 2); //sumAsyncResult = plugin_ffi_sample.sumAsync(3, 4);//}//...
节选部分代码如下:
可见sum方法的实现调用了_bindings对象中的sum方法,DynamicLibrary相关的代码用来打开动态库。import 'plugin_ffi_sample_bindings_generated.dart';/// A very short-lived native function.////// For very short-lived functions, it is fine to call them on the main isolate./// They will block the Dart execution while running the native function, so/// only do this for native functions which are guaranteed to be short-lived.int sum(int a, int b) => _bindings.sum(a, b);//...const String _libName = 'plugin_ffi_sample';/// The dynamic library in which the symbols for [PluginFfiSampleBindings] can be found.final DynamicLibrary _dylib = () {if (Platform.isMacOS || Platform.isIOS) {return DynamicLibrary.open('$_libName.framework/$_libName');}if (Platform.isAndroid || Platform.isLinux) {return DynamicLibrary.open('lib$_libName.so');}if (Platform.isWindows) {return DynamicLibrary.open('$_libName.dll');}throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');}();/// The bindings to the native functions in [_dylib].final PluginFfiSampleBindings _bindings = PluginFfiSampleBindings(_dylib);
头文件有变动时,重新执行以下命令就可以更新此文件:// AUTO GENERATED FILE, DO NOT EDIT.//// Generated by `package:ffigen`.import 'dart:ffi' as ffi;/// Bindings for `src/plugin_ffi_sample.h`.////// Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.///class PluginFfiSampleBindings {/// Holds the symbol lookup function.final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)_lookup;/// The symbols are looked up in [dynamicLibrary].PluginFfiSampleBindings(ffi.DynamicLibrary dynamicLibrary): _lookup = dynamicLibrary.lookup;/// The symbols are looked up with [lookup].PluginFfiSampleBindings.fromLookup(ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName)lookup): _lookup = lookup;/// A very short-lived native function.////// For very short-lived functions, it is fine to call them on the main isolate./// They will block the Dart execution while running the native function, so/// only do this for native functions which are guaranteed to be short-lived.int sum(int a,int b,) {return _sum(a,b,);}
如果命令执行失败,请查看是否安装好llvm[5]:flutter pub run ffigen --config ffigen.yaml
plugin_ffi_sample/ffigen.yaml是ffigen工具的配置文件,描述了根据哪些头文件生成ffi。brew install llvm
# Run with `flutter pub run ffigen --config ffigen.yaml`.name: PluginFfiSampleBindingsdescription: |Bindings for `src/plugin_ffi_sample.h`.Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`.output: 'lib/plugin_ffi_sample_bindings_generated.dart'headers:entry-points:- 'src/plugin_ffi_sample.h'include-directives:- 'src/plugin_ffi_sample.h'
至此,你可以运行起Sample工程并结合代码观察ffi的执行过程。也可以对头文件接口上的参数或者返回值稍加修改,然后重新生成binding文件。其中sumAsync方法的实现值得深入阅读学习。#if _WIN32#define FFI_PLUGIN_EXPORT __declspec(dllexport)#else#define FFI_PLUGIN_EXPORT#endif// A very short-lived native function.//// For very short-lived functions, it is fine to call them on the main isolate.// They will block the Dart execution while running the native function, so// only do this for native functions which are guaranteed to be short-lived.FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b);// A longer lived native function, which occupies the thread calling it.//// Do not call these kind of native functions in the main isolate. They will// block Dart execution. This will cause dropped frames in Flutter applications.// Instead, call these native functions on a separate isolate.FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b);
C/C++如何调用Dart
在上述工程中已经完整的演示了如何在Dart中调用C或者C++的方法。那么如何在C/C++中反调回Dart呢? 在这个示例中,我们将演示从Dart调用C/C++的ping方法,然后在C/C++中回调pong方法。 src/plugin_ffi_sample.h 在文件末尾添加函数指针定义以及ping方法:
这里是ping方法的实现,直接调用函数指针:typedef void (*pong) (void);FFI_PLUGIN_EXPORT void ping(pong callback);
执行ffigen命令重新生成绑定代码:FFI_PLUGIN_EXPORT void ping(pong callback) {printf("ping\n");callback();}
文件末尾添加:flutter pub run ffigen --config ffigen.yaml
example/lib/main.dart initState中插入ping方法调用// C/C++要回调的必须是Top level的方法void pong() {print("pong");}void ping() {_bindings.ping(Pointer.fromFunction(pong));}
重新执行Flutter run之后输出如下:@overridevoid initState() {super.initState();plugin_ffi_sample.ping(); //pingsumResult = plugin_ffi_sample.sum(1, 2);sumAsyncResult = plugin_ffi_sample.sumAsync(3, 4);}
Building macOS application...pingflutter: pongSyncing files to device macOS... 108ms
C/C++异步线程调用Dart
上述示例中我们演示了如何同步的回调Dart,但是在实战中我们经常会在C/C++开启子线程。那么如何在子线程中回调Dart呢? 为了快速编写异步线程回调的case,我们把plugin_ffi_sample.c改名为plugin_ffi_sample.cc,以方便使用C++编写代码,然后使用std::thread创建子线程回调Dart。 这里需要注意的是 extern "C" 的使用,是告诉编译器按C的方式进行编译。因为C++是多态的,支持重载,同名函数在编译后会生成特殊的符号,而C语言编译后的符号在Dart中可以直接按原函数名查找。 src/plugin_ffi_sample.cc
#include <thread>#ifdef __cplusplusextern "C" {#include "plugin_ffi_sample.h"#endif// A very short-lived native function.//// For very short-lived functions, it is fine to call them on the main isolate.// They will block the Dart execution while running the native function, so// only do this for native functions which are guaranteed to be short-lived.FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b) { return a + b; }// A longer-lived native function, which occupies the thread calling it.//// Do not call these kind of native functions in the main isolate. They will// block Dart execution. This will cause dropped frames in Flutter applications.// Instead, call these native functions on a separate isolate.FFI_PLUGIN_EXPORT intptr_t sum_long_running(intptr_t a, intptr_t b) {// Simulate work.#if _WIN32Sleep(5000);#elseusleep(5000 * 1000);#endifreturn a + b;}void entry_point(pong call) {printf("entry_point\n");call();}FFI_PLUGIN_EXPORT void ping(pong callback) {printf("ping\n");pong p = callback;std::thread* t = new std::thread(entry_point, p);}#ifdef __cplusplus}#endifmacos/Classes/plugin_ffi_sample.c
// Relative import to be able to reuse the C sources.// See the comment in ../{projectName}}.podspec for more information.#include "../../src/plugin_ffi_sample.h"
make后会在当前目录生成动态库 libplugin_ffi_sample.dylib# The Flutter tooling requires that developers have CMake 3.10 or later# installed. You should not increase this version, as doing so will cause# the plugin to fail to compile for some customers of the plugin.cmake_minimum_required(VERSION 3.10)project(plugin_ffi_sample_library VERSION 0.0.1 LANGUAGES C CXX)set (CMAKE_CXX_STANDARD 11)add_library(plugin_ffi_sample SHARED"plugin_ffi_sample.cc")set_target_properties(plugin_ffi_sample PROPERTIESPUBLIC_HEADER plugin_ffi_sample.hOUTPUT_NAME "plugin_ffi_sample")target_compile_definitions(plugin_ffi_sample PUBLIC DART_SHARED_LIB)
cmake .make
然后将刚才生成的动态库添加软链到当前目录s.vendored_libraries = 'libplugin_ffi_sample.dylib'
ln -s ../src/libplugin_ffi_sample.dylib
运行程序后发现会报错:// C/C++要回调的必须是Top level的方法void pong() {- print("pong");+ print("pong ${sum(3, 4)}");}
究其原因,可参看这里[6],只能在Flutter创建的isolate里执行Dart代码。../../third_party/dart/runtime/vm/runtime_entry.cc: 3766: error: Cannot invoke native callback outside an isolate.
drwxr-xr-x 9 anql staff 288 7 13 06:23 .drwxr-xr-x 10 anql staff 320 8 15 20:00 ..-rw-r--r-- 1 anql staff 146111 7 13 06:33 dart_api.h-rw-r--r-- 1 anql staff 2240 7 13 06:33 dart_api_dl.c-rw-r--r-- 1 anql staff 7881 7 13 06:33 dart_api_dl.h-rw-r--r-- 1 anql staff 6775 7 13 06:33 dart_native_api.h-rw-r--r-- 1 anql staff 18150 7 13 06:33 dart_tools_api.h-rw-r--r-- 1 anql staff 620 7 13 06:33 dart_version.hdrwxr-xr-x 3 anql staff 96 7 13 06:23 internal
/** \mainpage Dynamically Linked Dart API** This exposes a subset of symbols from dart_api.h and dart_native_api.h* available in every Dart embedder through dynamic linking.** All symbols are postfixed with _DL to indicate that they are dynamically* linked and to prevent conflicts with the original symbol.** Link `dart_api_dl.c` file into your library and invoke* `Dart_InitializeApiDL` with `NativeApi.initializeApiDLData`.*/DART_EXPORT intptr_t Dart_InitializeApiDL(void* data);//.../* Dart_Port */ \F(Dart_Post, bool, (Dart_Port_DL port_id, Dart_Handle object)) \F(Dart_NewSendPort, Dart_Handle, (Dart_Port_DL port_id)) \F(Dart_SendPortGetId, Dart_Handle, \(Dart_Handle port, Dart_Port_DL * port_id))
- 将flutter sdk路径/flutter/bin/cache/dart-sdk/include拷贝到plugin_ffi_sample/src目录;
- src/CMakeLists.txt修改如下,这里主要是让“include/dart_api_dl.c”文件参与编译,以及指定include目录:
cmake_minimum_required(VERSION 3.10)project(plugin_ffi_sample_library VERSION 0.0.1 LANGUAGES C CXX)set (CMAKE_CXX_STANDARD 11)add_library(plugin_ffi_sample SHARED"include/dart_api_dl.c""plugin_ffi_sample.cc")include_directories(include)set_target_properties(plugin_ffi_sample PROPERTIESPUBLIC_HEADER plugin_ffi_sample.hOUTPUT_NAME "plugin_ffi_sample")target_compile_definitions(plugin_ffi_sample PUBLIC DART_SHARED_LIB)
- src/plugin_ffi_sample.h 变更如下,不再让函数指针作为ping方法的参数,而是让Main isolate的SendPort作为参数:
+#include <dart_api_dl.h>-FFI_PLUGIN_EXPORT void ping(pong callback);+FFI_PLUGIN_EXPORT void ping(Dart_Port_DL main_isolate_send_port);++FFI_PLUGIN_EXPORT intptr_t ffi_Dart_InitializeApiDL(void* data);
- src/plugin_ffi_sample.cc 这里的实现会在C++的子线程中发送消息给Dart,需要注意的是,因为涉及不同的技术栈,所以这里的消息类型也是非常有限的。不过你可以设定一些协议进行通信。
-void entry_point(pong call) {+void entry_point(Dart_Port_DL main_isolate_send_port) {printf("entry_point\n");- call();++ Dart_CObject dart_object;+ dart_object.type = Dart_CObject_kString;+ dart_object.value.as_string = "pong";++ const bool result = Dart_PostCObject_DL(main_isolate_send_port, &dart_object);+ if (!result) {+ printf("C : Posting message to port failed.\n");+ }}-FFI_PLUGIN_EXPORT void ping(pong callback) {+FFI_PLUGIN_EXPORT void ping(Dart_Port_DL main_isolate_send_port) {printf("ping\n");- pong p = callback;- std::thread t(entry_point, p);+ std::thread* t = new std::thread(entry_point, main_isolate_send_port);}+FFI_PLUGIN_EXPORT intptr_t ffi_Dart_InitializeApiDL(void* data) {+ return Dart_InitializeApiDL(data);+}
- plugin_ffi_sample/macos/Classes/plugin_ffi_sample.c 这里不再需要了,因为我们通过CMake进行编译:
-#include "../../src/plugin_ffi_sample.h"+// #include "../../src/plugin_ffi_sample.h"
- plugin_ffi_sample/lib/plugin_ffi_sample.dart 这里主要是通过ReceivePort添加监听:
// C/C++要回调的必须是Top level的方法-void pong() {- print("pong ${sum(3, 4)}");+void pong(dynamic msg) {+ print("pong $msg");}void ping() {- _bindings.ping(Pointer.fromFunction(pong));+ final receivePort = ReceivePort()+ ..listen((message) {+ pong(message);+ });+ _bindings.ping(receivePort.sendPort.nativePort);}++int initializeApiDL() =>+ _bindings.ffi_Dart_InitializeApiDL(NativeApi.initializeApiDLData);
- plugin_ffi_sample/example/lib/main.dart 务必记得先初始化动态链接的api:
@overridevoid initState() {super.initState();+ plugin_ffi_sample.initializeApiDL();plugin_ffi_sample.ping(); //pingsumResult = plugin_ffi_sample.sum(1, 2);sumAsyncResult = plugin_ffi_sample.sumAsync(3, 4);
结语
至此,你已经掌握了基本的ffi用法,当然在实践中还会有更多需要注意的事项,这里再提两个知识点:
- 在Dart中调用 String的 toNativeUtf8方法,务必记得传入Allocator对象,ffi方法执行完成后要释放内存;
- NativeFinalizer 对象可以绑定Dart与C++对象,Dart对象被gc时可以回调指定的方法从而回收对象。
往期推荐
2022云栖大会一起见证科技创新,一起讨论云栖话题。
点击阅读原文查看详情。