Flutter FFI 异步调用 C/C++

Author Avatar
Amos
发表:2023-03-16 00:17:19
修改:2023-03-16 20:12:19

嗨~ 本咕咕来啦~

我们都知道在 Flutter 中需要原生调用,可以使用 Channel 构建通道进行通讯,也意味着需要针对各端单独编写几套接口,之前 《Flutter 集成 uni小程序(UniMPSDK)》 中就是这种实现方式。

那如果我们的业务涉及到高频的计算场景,又或是需要调用 C/C++ 层,Dart 中的 FFI 是一个非常不错的选择,FFI 可以直接调用本地的 C API,相比 Channel 减少了额外的资源消耗,性能更加优异,各端底层交互统一,在 C 如此完善的生态下,也能够弥补 Dart 中不太好完成的事情,我们还可以使用 Go、Rust 等编译为 C 进行交互使用。

FFI 全称 Foreign function interface 外部功能接口
Wiki:https://en.wikipedia.org/wiki/Foreign_function_interface

FFI 这么好,使用上又需要注意些什么?

  • 需要关注内存治理
    FFI 中提供了手动治理内存的方式,我们也必须要满足 “谁 alloc 谁 free” 的原则(在 Dart 中 alloc 就在 Dart 中 free,C 同理)。

  • 异步有些麻烦
    在 Flutter 中,FFI 与主线程同步,在处理等待响应或者高消耗的场景下,视图主线会被卡住直至处理完成,也无法使用 Future、Isolate 独立处理,如果想达到异步的效果,目前需要搭配官方提供的 Dart Native API 进行使用(会多一些样板代码)。

    Dart FFI 异步方案的讨论可以关注这个 issues
    https://github.com/dart-lang/sdk/issues/37022

Dart FFI 基础

本篇不讲 FFI 基础内容,网络上已经非常多了,下面这是官方 FFI 基础教程

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

Dart FFI 与 C 类型映射表

映射表对应关系详情:表1-1 Dart FFI 与 C 类型映射表

dart:ffi library 2.19.4:https://api.dart.dev/stable/2.19.4/dart-ffi/dart-ffi-library.html

实现

开发环境

Windows

[√] Flutter (Channel stable, 3.7.7, on Microsoft Windows [版本 10.0.22000.1574], locale zh-CN)
[√] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
[√] Visual Studio - develop for Windows (Visual Studio Community 2022 17.4.0)
[√] Android Studio (version 2021.3)
[√] VS Code (version 1.70.0)

macOS

[✓] Flutter (Channel stable, 3.7.7, on macOS 13.0 22A380 darwin-x64, locale zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 14.2)
[✓] Android Studio (version 2021.3)
[✓] VS Code (version 1.76.0)

示例思路

Dart FFI 使用 Isolate 通过 Dart Native API 开启新的线程异步调用,手动设置等待时间模拟响应,并监听线程消息进行操作。

示例源码:C/C++ 层Flutter 层

[GIF]

示例

C/C++ 层逻辑编写

我们需要使用官方提供的 Dart Native API 来帮助我们开启线程异步操作。

为了 Android 与 iOS 逻辑统一,我们可以将源代码添加到 iOS 项目文件夹中,因为 CocoaPods 不允许源码处于比 podspec 文件更高的目录层级,但是在 Android 中 Gradle 允许我们指向 iOS 项目文件夹。

首先将 Dart Native API 相关文件下载放入到我们自己指定的目录下。

Dart Native API: https://github.com/dart-lang/sdk/tree/main/runtime/include

我在 iOS 项目的根路径新建 Classes 文件夹(位置和名称看自己习惯)并将 FFI 相关源代码放入。

最终目录结构如下,源码位置:https://github.com/AmosHuKe/Mood-Example/tree/main/ios/Classes

├── ios
│   ├── Classes                  # Classes(目前存放 FFI 相关文件)
│   │   ├── include
│   │   │   └── dart_native_api  # Dart Native API 接口库
│   │   └── ffi.cpp              # FFI 逻辑

接下来我们编写具体的逻辑代码。

// ffi.cpp 文件

#include <thread>
#include <string>

#include "include/dart_native_api/dart_api.h"
#include "include/dart_native_api/dart_native_api.h"
#include "include/dart_native_api/dart_api_dl.h"

// FFI 库只能与 C 符号绑定,在 C++ 中需要添加标记,防止链接器在优化链接时会丢弃符号
#define DART_API extern "C" __attribute__((visibility("default"))) __attribute__((used))

// 声明执行函数
// test(线程端口, 模拟响应的等待时间)
DART_API void test(Dart_Port sendPort, int seconds);

// 初始化 Dart Native API
// Initialize `dart_api_dl.h`
DART_EXPORT intptr_t InitDartApiDL(void* data) {
  return Dart_InitializeApiDL(data);
}

// 注册发送端口开启线程,并调用实际操作
DART_EXPORT void RegisterSendPort(Dart_Port sendPort, int seconds) {
  std::thread thread1(test, sendPort, seconds);
  thread1.detach();
}

// 实际操作
DART_API void test(Dart_Port sendPort, int seconds) {
  // 等待设定的时间
  std::this_thread::sleep_for(std::chrono::seconds(seconds));
  std::string text("这是线程 " + std::to_string(sendPort) +"\n设定 " + std::to_string(seconds) + " 秒后的消息");

  // 创建 Dart 对象,发送给 Dart
  Dart_CObject dart_object2;
  // 设置 Dart 对象类型
  dart_object2.type = Dart_CObject_kString;
  // 设置 Dart 对象值
  dart_object2.value.as_string = (char*) text.c_str();
  // 发送给 Dart,并会触发端口的监听
  Dart_PostCObject_DL(sendPort, &dart_object2);
}

Android 构建

在 Android 端,我们需要在项目目录下创建 CMakeLists.txt 用来定义如何编译源文件,并通过 Gradle 定位 CMakeLists.txt 的位置,之后会和项目一同编译。

CMake 需要通过 NDK 的 clang 进行编译,我们在之后可以直接启动 Flutter 项目,如果 NDK 不存在,将会自动下载安装 NDK。
我们也可以通过 Android Studio 手动下载安装 NDK。

手动安装 NDK 方式:

  • Android Studio 打开 File/Settings
  • 搜索 Android SDK 打开 SDK Tools
  • 勾选 NDK,点击 Apply 直接下载安装
[GIF]

NDK 手动安装步骤

最终目录结构如下

├── android
│   ├── app
│   │   └── build.gradle  # Gradle 配置
│   └── CMakeLists.txt    # CMake 配置

CMake 配置

# CMakeLists.txt

# CMake 最小版本
cmake_minimum_required(VERSION 3.4.1)

# 项目名称
set(PROJECT_NAME "ffi")

# 批量添加 C 文件(之前编写的目录)
file(GLOB_RECURSE ffi ../ios/Classes/*)
# 构建链接库 add_library(项目名称 SHARED:构建动态链接库 源文件列表)
add_library(${PROJECT_NAME} SHARED ${ffi})

Gradle 配置

// build.gradle

android {
  ...
  externalNativeBuild {
    // CMake 构建配置
    cmake {
      // CMake 构建脚本相对路径
      path "../CMakeLists.txt"
    }
  }
  ...
}

Tips:启动 Flutter 项目,CMake 编译完成之后,可以发现 android/app 目录下多了 .cxx 文件夹(存放着已编译的文件)。

iOS 构建

在 Xcode 中直接将我们刚才包含了源码、Dart Native API 的 Classes 文件夹添加到项目内。

  • 在 Xcode 中,打开 Runner.xcworkspace
  • 右键 Runner,添加 Classes 文件夹到项目内

Flutter 调用

DynamicLibrary.open

主要用来加载我们编译完成后的文件,比如:刚才 CMake 配置编译的 ffi 项目,我们只需要以下方式就可以加载。

/// index.dart  

import 'dart:ffi';
import 'dart:io';

/// ffi 名称前加上 lib
final DynamicLibrary dl = Platform.isAndroid
        ? DynamicLibrary.open("libffi.so")
        : DynamicLibrary.process();

DynamicLibrary.process

在 iOS 及 MacOS 中已经自动加载好动态链接库(Windows 平台无法使用),也可以解析静态链接到应用的二进制文件符号。

DynamicLibrary.lookupFunction

F lookupFunction<T extends Function, F extends Function>(String symbolName, {bool isLeaf = false})

通过链接的库,查找到对应的符号名称(C 中的方法)并返回其内存地址,方便我们调用(可自定义调用、返回的参数类型,映射类型可以参考 Dart FFI 与 C 类型映射表
如下:

/// index.dart  

import 'dart:ffi';
import 'dart:io';

/// 类型定义
/// C 层 DartApi 初始化调用返回类型
typedef NativeDartInitializeApiDL = Int32 Function(Pointer<Void> data);
/// 对应 Dart 层 DartApi 初始化的调用返回类型
typedef DartInitializeApiDL = int Function(Pointer<Void> data);

/// 加载库
final DynamicLibrary dl = Platform.isAndroid
        ? DynamicLibrary.open("libffi.so")
        : DynamicLibrary.process();

/// 查找 DartApi 初始化函数
DartInitializeApiDL initDartApiDL =
    dl.lookupFunction<NativeDartInitializeApiDL, DartInitializeApiDL>(
        "InitDartApiDL");

/// 调用初始化函数
final int dartApiInited = initDartApiDL(NativeApi.initializeApiDLData);

开启线程,异步执行

通过 Dart Isolate 创建 ReceivePort,将端口传入 Dart Native API 并注册开启线程,在线程中执行业务逻辑代码,并监听端口的返回值进行操作。
如下:

/// index.dart  

import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';

/// 类型定义
/// C 层 DartApi 注册线程
typedef NativeRegisterSendPort = Void Function(Int64, Int);
/// 对应 Dart 层 DartApi 注册线程
typedef RegisterSendPort = void Function(int, int);

/// 加载库
final DynamicLibrary dl = Platform.isAndroid
        ? DynamicLibrary.open("libffi.so")
        : DynamicLibrary.process();

/// 接收端口1
final ReceivePort receivePort1 = ReceivePort();

/// 监听接收端口
receivePort1.listen((message) {
  /// 具体操作
  print("$message\ntype=${message.runtimeType}");
  /// 关闭端口
  receivePort1.close();
});

/// 查找 注册线程函数
RegisterSendPort registerSendPort =
    dl.lookupFunction<NativeRegisterSendPort, RegisterSendPort>(
        "RegisterSendPort");

/// 调用 开启线程并传入参数(端口值, 业务需要的值-比如这里是模拟 3 秒的响应)
registerSendPort(receivePort1.sendPort.nativePort, 3);

完整 Flutter 示例

在实际示例中,我们启用2个线程,并模拟不同响应时间,监听返回操作。

示例完整源码位置:链接

/// index.dart 

import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';

/// 类型定义
/// DartApi 初始化
typedef NativeDartInitializeApiDL = Int32 Function(Pointer<Void> data);
typedef DartInitializeApiDL = int Function(Pointer<Void> data);

/// DartApi 注册线程
typedef NativeRegisterSendPort = Void Function(Int64, Int);
typedef RegisterSendPort = void Function(int, int);

class FFIPage extends StatefulWidget {
  const FFIPage({super.key});

  @override
  State<FFIPage> createState() => _FFIPageState();
}

class _FFIPageState extends State<FFIPage> {
  ///
  late DynamicLibrary _dl;

  /// 接收端口1
  final ReceivePort _receivePort1 = ReceivePort();

  /// 接收端口2
  final ReceivePort _receivePort2 = ReceivePort();

  @override
  void initState() {
    super.initState();

    /// 初始化并调用 FFI 测试
    ffiInit();
    ffiTest1();
    ffiTest2();
  }

  @override
  void dispose() {
    /// 退出后关闭
    _receivePort1.close();
    _receivePort2.close();

    super.dispose();
  }

  /// 初始化 FFI
  void ffiInit() {
    /// 加载库 符号表
    _dl = Platform.isAndroid
        ? DynamicLibrary.open("libffi.so")
        : DynamicLibrary.process();

    /// 查找 DartApi 初始化函数
    DartInitializeApiDL initDartApiDL =
        _dl.lookupFunction<NativeDartInitializeApiDL, DartInitializeApiDL>(
            "InitDartApiDL");

    /// 调用初始化函数,并判断是否成功
    final int dartApiInited = initDartApiDL(NativeApi.initializeApiDLData);

    if (dartApiInited == 0) {
      debugPrint("初始化 Dart Native API 成功");
    } else {
      debugPrint("初始化 Dart Native API 失败");
    }
  }

  /// FFI 测试1
  void ffiTest1() {
    /// 监听接收端口
    _receivePort1.listen((message) {
      print("$message\ntype=${message.runtimeType}");
      /// 关闭端口
      _receivePort1.close();
    });

    /// 查找 注册线程函数
    RegisterSendPort registerSendPort =
        _dl.lookupFunction<NativeRegisterSendPort, RegisterSendPort>(
            "RegisterSendPort");

    /// 调用 开启线程并传入参数
    registerSendPort(_receivePort1.sendPort.nativePort, 3);
  }

  /// FFI 测试2
  void ffiTest2() {
    /// 监听接收端口
    _receivePort2.listen((message) {
      print("$message\ntype=${message.runtimeType}");
      /// 关闭端口
      _receivePort2.close();
    });

    /// 查找 注册线程函数
    RegisterSendPort registerSendPort =
        _dl.lookupFunction<NativeRegisterSendPort, RegisterSendPort>(
            "RegisterSendPort");

    /// 调用 开启线程并传入参数
    registerSendPort(_receivePort2.sendPort.nativePort, 1);
  }

  @override
  Widget build(BuildContext context) {
    return ......
  }
}

其他

在实际使用中可以搭配 ffigen 库使用,它会根据 C/C++ 头文件自动生成之前需要手动编写的绑定代码,大大提高开发效率。

相关资料

附录

表1-1 Dart FFI 与 C 类型映射表
[返回]
Dart 中的 NativeTypeC 中的类型说明
dynamicDart_CObjectDart 对象在 C 中的表现形式
Boolbool布尔类型
Charchar字符类型
Doubledouble64位双精度浮点类型
Floatfloat32位单精度浮点类型
HandleDart_HandleDart 句柄在 C 中的表示形式
Intint整数类型
Int8int8_t 或 char有符号8位整数
Int16int16_t 或 short有符号16位整数
Int32int32_t 或 int有符号32位整数
Int64int64_t 或 long long有符号64位整数
IntPtrintptr_t整数类型指针
Uint8uint8_t 或 unsigned char无符号8位整数
Uint16uint16_t 或 unsigned short无符号16位整数
Uint32int32_t 或 unsigned int无符号32位整数
Uint64uint64_t 或 unsigned long long无符号64位整数
UintPtruintptr_t无符号整数类型指针
Longlong int 或 long长整数类型
LongLonglong long长整数类型
NativeFunction函数函数类型
Opaqueopaque不透明类型,不暴露成员
Pointer*指针类型
Shortshort短整数类型
SignedCharsigned char短整数类型
Sizesize_t无符号整数类型
Structstruct结构体类型
Unionunion共用体类型
UnsignedCharunsigned char无符号字符类型
UnsignedIntunsigned int无符号整数类型
UnsignedLongunsigned long int 或 unsigned long无符号长整数类型
UnsignedLongLongunsigned long long无符号长整数类型
UnsignedShortunsigned short无符号短整数类型
Voidvoidvoid 类型
WCharwchar_t字符类型,主要用于国际化程序
nullptrNULL空指针

转载请遵循 协议许可
本文所有内容严禁任何形式的盗用
本文作者:Amos Amos
本文链接:https://amoshk.top/2023031201/

评论
✒️ 支持 Markdown 格式
🖼️ 头像与邮箱绑定 Gravatar 服务
📬 邮箱会回复提醒(也许会在垃圾箱内)