在 iOS 中使用 dart:ffi 调用本地代码

Flutter 移动版可以使用 dart:ffi 库来调用本地的 C API。 FFI 代表 外部功能接口。类似功能的其他术语包括本地接口语言绑定

你必须首先确保本地代码已加载,并且其符号对 Dart 可见,然后才能在库或程序使用 FFI 库绑定本地代码。本页主要介绍如何在 Flutter 插件或应用程序中编译、打包和加载 iOS 原生代码。

本教程演示了如何在 Flutter 插件中捆绑 C/C++ 源代码,并在 iOS 上使用 Dart FFI 库绑定和使用。

在本示例中,你将创建一个实现 32 位的加法 C 函数,然后通过名为 “native_add” 的 Dart 插件暴露它。

动态链接 vs 静态链接

本地库可以动态或静态地链接到应用程序中。一个静态链接库会被嵌入到应用程序的可执行映像中,并在应用程序启动时加载。

静态链接中的符号可以使用 DynamicLibrary.executableDynamicLibrary.process 来加载.

相比之下,动态链接库则分布在应用程序中的单独的文件或文件夹中,并按需加载。在 iOS 上,它是作为 .framework 文件夹分发的。

动态链接库在 Dart 中可以通过 DynamicLibrary.open 加载。

Dart dev 频道中的 API 已经可用: Dart API 参考文档.

步骤 1:创建插件

如果你已经有一个插件,跳过这步。

如果要创建一个名为 “native_add” 的插件,你需要这么做:

$ flutter create --platforms=android,ios --template=plugin native_add
$ cd native_add

步骤 2:添加 C/C++ 源码

你需要让 Android 和 iOS 构建系统知道本地代码的存在,以便代码可以被编译并链接到最终的应用程序中。

你可以将源代码添加到 ios 文件夹,因为 CocoaPods 不允许源码处于比 podspec 文件更高的目录层级,但是 Gradle 允许你指向 ios 文件夹。

FFI 库只能与 C 符号绑定,因此在 C++ 中,这些符号添加 extern C 标记。还应该添加属性来表明符号是需要被 Dart 引用的,以防止链接器在优化链接时会丢弃符号。

作为示例,创建一个 C++ 文件,路径为:ios/Classes/native_add.cpp。(请注意,模板已经为你创建了此文件。)在项目的根目录下中执行以下命令:

cat > ios/Classes/native_add.cpp << EOF
#include <stdint.h>

extern "C" __attribute__((visibility("default"))) __attribute__((used))
int32_t native_add(int32_t x, int32_t y) {
    return x + y;
}
EOF

在 iOS 中,你需要告诉 Xcode 如何静态链接这个文件:

  1. 在 Xcode 中,打开 Runner.xcworkspace

  2. 添加 C/C++/Objective-C/Swift 源码文件到 Xcode 工程中。

步骤 3:在 FFI 库中读取代码

在示例中,你需要添加如下的代码到 lib/native_add.dart。但是,Dart 在何处进行代码绑定并不重要。

首先,你需要创建一个 DynamicLibrary 来处理本地代码。这一步在 iOS 和 Android 之间有所不同:

import 'dart:ffi'; // For FFI
import 'dart:io'; // For Platform.isX

final DynamicLibrary nativeAddLib = Platform.isAndroid
    ? DynamicLibrary.open('libnative_add.so')
    : DynamicLibrary.process();

请注意,在 Android 上,本地库的名称是定义在 CMakeLists.txt 中的(见上文),但在 iOS 上,它将使用插件的名称。

你可以通过使用库的句柄来解析 native_add 符号:

final int Function(int x, int y) nativeAdd = nativeAddLib
    .lookup<NativeFunction<Int32 Function(Int32, Int32)>>('native_add')
    .asFunction();

现在,你可以调用它了。在自动生成的 example 项目(example/lib/main.dart)中演示它。

// Inside of _MyAppState.build:
        body: Center(
          child: Text('1 + 2 == ${nativeAdd(1, 2)}'),
        ),

其他的用例

iOS 和 macOS

动态链接库在应用程序启动时由动态链接器自动加载。它们的组成符号可以用 DynamicLibrary.process。你还可以使用 DynamicLibrary.open 来限制符号解析的范围,但目前仍然不确定苹果的审查程序将如何处理两者的使用。

你可以使用 DynamicLibrary.executableDynamicLibrary.process 解析静态链接到应用程序二进制文件的符号。

平台库

要链接到平台库,请按照如下说明:

  1. 在 Xcode 中,打开 Runner.xcworkspace

  2. 选择目标设备。

  3. Linked Frameworks and Libraries 中点击 +

  4. 选择要链接的系统库。

第一方库

第一方本地库可以作为源文件或(已签名的).framework 文件被包含在内。它也可能包括静态链接的档案,但需要测试。

源码

要直接链接到源代码,请按照如下说明:

  1. 在 Xcode 中,打开 Runner.xcworkspace

  2. 添加 C/C++/Objective-C/Swift 源码到 Xcode 工程中。

  3. 将以下前缀添加到导出的符号声明中,以确保它们对 Dart 可见:

    C/C++/Objective-C

    extern "C" /* <= C++ only */ __attribute__((visibility("default"))) __attribute__((used))
    

    Swift

    @_cdecl("myFunctionName")
    

已编译的动态库

要链接到已编译过的动态库,请按照如下说明:

  1. 如果存在已进行签名的 Framework 文件,请打开 Runner.xcworkspace

  2. 添加 framework 文件到 Embedded Binaries 区域中。

  3. 同时将其添加到 Xcode 中目标的 Linked Frameworks & Libraries 部分。

开源的三方库

要创建一个包含 C/C++/Objective-C Dart 代码的 Flutter 插件,请按照如下说明:

  1. 在你的插件项目打开 ios/<myproject>.podspec.

  2. 添加本地代码到 source_files 字段。

本地代码会被静态链接到任何使用这个插件的应用二进制中。

闭源三方库

要创建包含 Dart 源代码,但 C/C++ 部分是以二进制形式分发的库的 Flutter 插件,请按照如下说明:

  1. 在你的插件目录打开 ios/<myproject>.podspec

  2. 添加 vendored_frameworks 字段。参考 CocoaPods 示例

精简 iOS 符号表

当创建一个 release 档案(IPA)时,符号会被 Xcode 删除。

  1. 在 Xcode 中, 点击 Target Runner > Build Settings > Strip Style.

  2. All Symbols 修改为 Non-Global Symbols