Redian新闻
>
手把手教你写 Dart ffi

手把手教你写 Dart ffi

科技

本文以step by step的方式说明了Dart ffi的使用,适合新手学习。


什么是ffi


ffi是 foreign function interface[1] 的缩写,是一种机制。通过这种机制,用一种编程语言编写的程序可以调用另一种编程语言编写的程序或服务。像我们熟悉的Java JNI便是ffi机制。


创建sample工程


本文示例是在macos创建并运行的,最终的产物是mac可执行程序。
flutter --version
Flutter 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

通过flutter create命令即可创建plugin[2]工程,以下命令中创建了名为plugin_ffi_sample的插件工程。
flutter create --template=plugin_ffi --platforms=macos plugin_ffi_sample

创建好工程后,执行以下命令即可编出macos的可执行文件并打开程序:
cd plugin_ffi_sample/exampleflutter run

接下来我们便基于plugin_ffi_sample工程进一步讲解ffi的使用。


plugin_ffi_sample 工程详解


在创建工程时指定了“--template=plugin_ffi”,所以plugin_ffi_sample是插件工程,子目录example是使用插件的示例工程。plugin_ffi_sample/example/pubspec.yaml文件中有对plugin_ffi_sample插件的依赖配置。
dependencies:  flutter:    sdk: flutter
plugin_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: ../

有关插件工程的文档可以进一步参考[3]。

plugin_ffi_sample/example/lib/main.dart

我们节选了sample工程main.dart的部分代码如下,可见导入了插件工程中的plugin_ffi_sample.dart,并调用了sum和sumAsync方法。
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;
@override void initState() { super.initState(); sumResult = plugin_ffi_sample.sum(1, 2); // sumAsyncResult = plugin_ffi_sample.sumAsync(3, 4);// }//...


plugin_ffi_sample/lib/plugin_ffi_sample.dart

节选部分代码如下:

可见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);

plugin_ffi_sample/lib/plugin_ffi_sample_bindings_generated.dart

注意plugin_ffi_sample_bindings_generated.dart文件开头的注释,此文件是通过ffigen[4]工具自动生成的,实现了Dart到C语言的绑定。节选代码片段如下:
// 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, ); }

头文件有变动时,重新执行以下命令就可以更新此文件:
flutter pub run ffigen --config ffigen.yaml

如果命令执行失败,请查看是否安装好llvm[5]:
brew install llvm

plugin_ffi_sample/ffigen.yaml是ffigen工具的配置文件,描述了根据哪些头文件生成ffi。
# 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'


plugin_ffi_sample/src/plugin_ffi_sample.h

FFI_PLUGIN_EXPORT表示要导出符号,可见sum和sum_long_running的定义。
#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);

至此,你可以运行起Sample工程并结合代码观察ffi的执行过程。也可以对头文件接口上的参数或者返回值稍加修改,然后重新生成binding文件。其中sumAsync方法的实现值得深入阅读学习。

C/C++如何调用Dart


在上述工程中已经完整的演示了如何在Dart中调用C或者C++的方法。那么如何在C/C++中反调回Dart呢?
在这个示例中,我们将演示从Dart调用C/C++的ping方法,然后在C/C++中回调pong方法。
src/plugin_ffi_sample.h
在文件末尾添加函数指针定义以及ping方法:
typedef void (*pong) (void);
FFI_PLUGIN_EXPORT void ping(pong callback);

这里是ping方法的实现,直接调用函数指针:
FFI_PLUGIN_EXPORT void ping(pong callback) {  printf("ping\n");  callback();}

执行ffigen命令重新生成绑定代码:
flutter pub run ffigen --config ffigen.yaml

文件末尾添加:
// C/C++要回调的必须是Top level的方法void pong() {  print("pong");}
void ping() { _bindings.ping(Pointer.fromFunction(pong));}

example/lib/main.dart
initState中插入ping方法调用
  @override  void initState() {    super.initState();    plugin_ffi_sample.ping(); //ping    sumResult = plugin_ffi_sample.sum(1, 2);    sumAsyncResult = plugin_ffi_sample.sumAsync(3, 4);  }

重新执行Flutter run之后输出如下:
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 _WIN32 Sleep(5000);#else usleep(5000 * 1000);#endif return 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}#endif


macos/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"

src/CMakeLists.txt

变更文件如下:
# 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 PROPERTIES PUBLIC_HEADER plugin_ffi_sample.h OUTPUT_NAME "plugin_ffi_sample")
target_compile_definitions(plugin_ffi_sample PUBLIC DART_SHARED_LIB)

make后会在当前目录生成动态库 libplugin_ffi_sample.dylib
cmake .make

macos/plugin_ffi_sample.podspec

末尾添加一行
  s.vendored_libraries = 'libplugin_ffi_sample.dylib'

然后将刚才生成的动态库添加软链到当前目录
ln -s ../src/libplugin_ffi_sample.dylib

lib/plugin_ffi_sample.dart

稍作变更如下:
 // C/C++要回调的必须是Top level的方法 void pong() {-  print("pong");+  print("pong ${sum(3, 4)}"); }

运行程序后发现会报错:
../../third_party/dart/runtime/vm/runtime_entry.cc: 3766: error: Cannot invoke native callback outside an isolate.

究其原因,可参看这里[6],只能在Flutter创建的isolate里执行Dart代码。

解决方案

通过Main isolate和Worker isolate通过message通信的逻辑得到启示,C++的线程既然不能直接调用Dart代码,那么能不能有一种C++给Dart isolate发送message的机制呢?
我们找到flutter sdk的安装路径,其中flutter/bin/cache/dart-sdk/include包含如下文件:
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

dart_api_dl.h

节选代码片段如下,可见有Dart_Post相关的api可用:
/** \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))

开始行动

  1. 将flutter sdk路径/flutter/bin/cache/dart-sdk/include拷贝到plugin_ffi_sample/src目录;
  2. 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 PROPERTIES PUBLIC_HEADER plugin_ffi_sample.h OUTPUT_NAME "plugin_ffi_sample")
target_compile_definitions(plugin_ffi_sample PUBLIC DART_SHARED_LIB)
  1. 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);
  1. 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); +}
  1. plugin_ffi_sample/macos/Classes/plugin_ffi_sample.c 这里不再需要了,因为我们通过CMake进行编译:
-#include "../../src/plugin_ffi_sample.h"+// #include "../../src/plugin_ffi_sample.h"
  1. 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);

  1. plugin_ffi_sample/example/lib/main.dart 务必记得先初始化动态链接的api:
  @override  void initState() {    super.initState();+    plugin_ffi_sample.initializeApiDL();    plugin_ffi_sample.ping(); //ping    sumResult = plugin_ffi_sample.sum(1, 2);    sumAsyncResult = plugin_ffi_sample.sumAsync(3, 4);

结语


至此,你已经掌握了基本的ffi用法,当然在实践中还会有更多需要注意的事项,这里再提两个知识点:
  1. 在Dart中调用 String的 toNativeUtf8方法,务必记得传入Allocator对象,ffi方法执行完成后要释放内存;

  2. NativeFinalizer 对象可以绑定Dart与C++对象,Dart对象被gc时可以回调指定的方法从而回收对象。

参考:

[1]https://en.wikipedia.org/wiki/Foreign_function_interface

[2]https://docs.flutter.dev/development/packages-and-plugins/developing-packages

[3]https://docs.flutter.dev/development/packages-and-plugins/developing-packages

[4]https://pub.dev/documentation/ffigen/latest/

[5]https://pub.dev/packages/ffigen

[6https://dart.dev/guides/language/concurrency]

[7]C interop using dart:ffi

https://dart.dev/guides/libraries/c-interop

[8]Binding to native Android code using dart:ff

https://docs.flutter.dev/development/platform-integration/android/c-interop

[9]Binding to native iOS code using dart:ffi

https://docs.flutter.dev/development/platform-integration/ios/c-interop

[10]Binding to native macOS code using dart:ffi

https://docs.flutter.dev/development/platform-integration/macos/c-interop

[11]ffigen | Dart Package

https://pub.dev/packages/ffigen

[12]ffigen - Dart API docs

https://pub.dev/documentation/ffigen/latest/




畅聊云栖


2022云栖大会一起见证科技创新,一起讨论云栖话题。


点击阅读原文查看详情。


微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
3000美元加州房主补助金你家符合条件吗?手把手教你申请加州房屋抗震加固补助款!美国房产最前线 孙斯陶 2022.11.09达拉斯超市牛排种类/等级/烹饪方法,手把手教你在家做牛排,比餐馆还好吃!3岁娃手把手教爷爷看直播!常看电子产品的孩子,应从小防这种眼病授人以渔!现代化应用程序动手实训营手把手教你玩转容器平台!实用!手把手教你轻松解决美国交通罚单,省钱省心亲测有效干货:手把手教你用PS处理WB电泳图!想吃腊味不用买!咸香鲜美有嚼劲,手把手教你做,比外面卖的好吃100倍~外刊精读写作19期丨新增作文模块,手把手教你写作文加州有120亿美元无人认领的支票,退税:看看你是否被欠钱,手把手教你怎么查!实战 | 手把手教你如何从GetShell到权限提升孤女命运被董建华他爸的一句话改变正在直播丨手把手教胃癌病友吃出营养攻略 | 手把手教你查询美国移民局官网直播预告丨手把手教胃癌病友吃出营养不用擀皮不用包!我妈手把手教我的配方,鲜香酥脆,比水煮好吃十几倍直播回放丨手把手教胃癌病友吃出营养毛泽东对三线建设的总构想方针入境英国生病怎么办?手把手教你注册GP免费看病!附HC2低收入证明申请教程谷歌翻译停服?别慌,手把手教你一招修复 Chrome 浏览器无法翻译网页问题个人养老金刷屏!在哪买?如何买?基金公司正式开售,手把手教你操作手把手教你查孩子学校的EQAO统考成绩:它能决定学区房价格!建国之初,苏联对华经济援助有多重要?16亿大单,专家手把手教微信转账后被删除怎么办?手把手教你维权!加州政府大撒钱!零首付买房,还白送10%购房款?手把手教你如何申请~秋招来了,你准备好了吗?简历CV/求职信CoverLetter等手把手教学!DC超市牛排种类/等级/烹饪方法,手把手教你在家做牛排,比餐馆还好吃!Co-IP 、ChIP 、RNA 抽提、RT- qPCR 、IHC 实验,漂亮小姐姐手把手教你做实验海湾百货与太平洋铁路这个占数学试卷一半分的思维能力,手把手教你怎么启蒙!【更新】堪培拉留学新生指南:手把手教你从行李准备,国内上飞机,抵澳,到家躺下完全攻略!还包括入关,填入境卡,买车票哦~哈佛前招生官手把手教你写2023大学申请文书人体的最佳运动时间手把手教你看懂芯片行业:从交大热能系毕业,到硅谷芯片大厂总监手把手教你填写新加坡电子入境卡教育随笔(110)点燃课堂教学的亮点
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。