手把手教你写 Dart ffi

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

什么是ffi

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

创建sample工程

本文示例是在macos创建并运行的,最终的产物是mac可执行程序。

flutter --version

   

Flutter 3.3.0 • channel stable • https://github.com/flutter/flutter.git

Framework • revision ffccd96b62 (6 weeks ago) • 2022-08-29 17:28:57 -0700

Engine • revision 5e9e0e0aa8

Tools • Dart 2.18.0 • DevTools 2.15.0

通过flutter create命令即可创建plugin工程,以下命令中创建了名为plugin_ffi_sample的插件工程。

flutter create --template=plugin_ffi --platforms=macos plugin_ffi_sample

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

cd plugin_ffi_sample/example

flutter 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: ../

有关插件工程的文档可以进一步参考https://docs.flutter.dev/development/packages-and-plugins/developing-packages

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工具自动生成的,实现了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

brew install llvm

plugin_ffi_sample/ffigen.yaml是ffigen工具的配置文件,描述了根据哪些头文件生成ffi。

# Run with `flutter pub run ffigen --config ffigen.yaml`.

name: PluginFfiSampleBindings

description: |

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

src/plugin_ffi_sample.c

这里是ping方法的实现,直接调用函数指针:

FFI_PLUGIN_EXPORT void ping(pong callback) {

 printf("ping\n");

 callback();

}

执行ffigen命令重新生成绑定代码:

flutter pub run ffigen --config ffigen.yaml

lib/plugin_ffi_sample.dart

文件末尾添加:

// 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...                                          

ping

flutter: pong

Syncing 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 __cplusplus

extern "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.

究其原因,可参看这里https://dart.dev/guides/language/concurrency,只能在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.h

drwxr-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时可以回调指定的方法从而回收对象。

参考

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

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

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

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

https://pub.dev/packages/ffigen

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

相关文章
|
4月前
|
NoSQL 网络协议 Java
【Azure Redis】Redis服务端的故障转移(Failover)导致客户端应用出现15分钟超时问题的模拟及解决
在使用 Azure Cache for Redis 服务时,因服务端维护可能触发故障转移。Linux 环境下使用 Lettuce SDK 会遇到超时 15 分钟的已知问题。本文介绍如何通过重启 Primary 节点主动复现故障转移,并提供多种解决方案,包括调整 TCP 设置、升级 Lettuce 版本、配置 TCP_USER_TIMEOUT 及使用其他 SDK(如 Jedis)来规避此问题。
183 1
|
Dart 编译器 API
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决
Dart ffi 使用问题之在C++线程中无法直接调用Dart函数的问题如何解决
|
8月前
|
人工智能 开发者
零门槛,即刻拥有DeepSeek-R1满血版
阿里云百炼DeepSeek解决方案提供高效、低成本的AI模型调用服务。用户无需购买昂贵显卡,按需付费,新用户享100万次免费试用。10分钟快速开通,支持自动扩容,高峰期不卡顿。DeepSeek模型擅长中文处理,多才多艺,生成速度快。避免官方服务拥堵,提供VIP通道和托管服务,降低自建成本,适合各类企业和开发者使用。
277 0
|
11月前
|
JavaScript 前端开发 开发工具
从零开始使用node.js制作一个脚手架
本文介绍了如何从零开始使用Node.js制作一个项目脚手架,涵盖了脚手架的基本概念、所需准备的第三方库、项目结构的初始化、命令注册、项目创建流程及用户交互设计等内容。通过实例演示了如何利用commander、inquirer等库实现命令行工具的开发,最终完成了一个能够根据用户选择自动创建Vue或React项目的脚手架。
200 1
从零开始使用node.js制作一个脚手架
|
Dart API 开发工具
Dart ffi 使用问题之Dart API要在C++中使用,该如何初始化
Dart ffi 使用问题之Dart API要在C++中使用,该如何初始化
|
关系型数据库 分布式数据库 数据库
PolarDB资源隔离技术:在多租户环境中的应用与优化
随着云计算普及,多租户架构助力云服务商提供高效服务。阿里云PolarDB采用独特分布式设计,在多租户环境下确保每个用户数据独立与资源隔离。通过逻辑与物理隔离技术,如Schema和分区,结合分布式存储节点,实现资源独占及安全。此技术不仅保障数据安全,还能动态分配资源,满足高性能需求。通过优化资源分配、增强事务处理及监控机制,进一步提升PolarDB在多租户环境中的表现。
409 4
|
数据采集 NoSQL 调度
flask celery python 每月定时任务
flask celery python 每月定时任务
|
机器学习/深度学习 算法 搜索推荐
优化IAA广告策略:通过A/B测试和实时反馈提高广告效果
【7月更文第30天】本文将介绍如何使用数据分析技术,特别是A/B测试和实时反馈机制,来改进移动应用内的广告策略。我们将展示一个实际案例,包括如何设置实验、收集数据、分析结果,并根据这些结果调整广告策略以实现更好的用户参与度和收入增长。
751 0
|
存储 UED 开发者
Flutter的状态管理:setState、Provider、Bloc的使用详解
【4月更文挑战第26天】Flutter状态管理详解:涵盖setState基础,Provider的跨组件共享及Bloc的复杂场景处理。了解这三种方法的优缺点,助力优化应用数据一致性与用户体验。当状态管理需求升级,从简单的setState到Provider的便利,再到Bloc的强大功能,开发者可根据项目规模和复杂度选择合适策略。
|
JSON 前端开发 Java
深入解析SpringBoot的请求响应机制
深入解析SpringBoot的请求响应机制