[Dart翻译]在Dart 2.18中使用FFIGen

1,632 阅读9分钟

本文由 简悦SimpRead 转码,原文地址 medium.com

在Dart 2.18中使用FFIGen

在Dart 2.18中使用FFIGen

我们将简单介绍一下。

  1. 什么是FFIGen
  2. FFIGen的新内容
  3. Dart CLI应用程序和整合基于obj-c的库
  4. 测试FFIGen

www.youtube.com/watch?v=VIb…

在Dart中使用FFIGen

等等,什么是FFIGen?

在解释这个问题的答案之前,读者需要了解一下 "FFI"(外国函数接口)。

什么是FFI

FFI使用一种语言编写的程序可以调用用其他语言编写的库。术语FFI来自CommonLisp,然而,它适用于任何语言。一些语言,如Java,在其生态系统中使用FFI,并称其为JavaNativeInterface

如果我们把低级语言称为 "主机"语言,把高级语言称为 "客人"语言,下面是它们之间的通信方式。

  • 主机要与客体架起沟通的桥梁。我们编写主机语言的函数,专门供客人调用。我们为主机语言提供了一个API来与客体进行交流。
  • 这种差距是由某种不属于严格意义上的主机或客体语言的工具来弥补的。
  • 客人也被期望与东道主弥合差距。客体语言可以调用任何主机语言的功能,但它需要支持许多低级别的功能,以便与主机语言进行有效的交流。

根据维基百科,这些是FFI需要考虑的事情。

  • 如果一种语言支持垃圾收集(GC),而另一种语言不支持;必须注意非GC语言的代码不会导致另一种语言的GC失败。
  • 复杂的或非琐碎的对象或数据类型可能难以从一种环境映射到另一种环境。
  • 一种或两种语言都可能在虚拟机 (VM)上运行;此外,如果两者都是,这些可能是不同的VM。

image.png FFIGen in Dart 2.18

幸运的是,我们能够通过dart:ffi库在Dart中使用FFI。从Dart v2.12开始,Dart FFI可以在稳定频道中使用。Dart FFI允许你使用C库中的现有代码。通过使用FFI,我们可以利用可移植性和高度调整的C代码的整合来完成性能密集型任务。我们并不局限于 "C",事实上,我们可以用任何可以编译成C库的语言来编写代码,例如 "Go"、"Rust"。

使用Dart FFI的另一个用例是,有些时候Flutter应用程序需要对内存管理和垃圾收集有更大的控制,例如,一个使用张量流的应用程序。

Dart FFI可以被用来读取、写入、分配和删除本地内存。有一些包已经使用了这个功能。

file_picker, printing, win32, objectbox, realm, isar, tflite_flutter, 和dbus

使用Dart FFI的方法

有些时候,你想创建自己的新库,但最多的时候,这个库已经存在(由其他团队创建),你只是想使用它。在这两种情况下,我们有以下选择

  • 手动创建FFI绑定
  • 自动生成FFI绑定

如果你喜欢自动化,你可能会选择第二个选项,因此,我们有package:ffigen

ffigen背后的想法是。对于大型的API来说,编写Dart绑定是非常耗时的,因为它可以与C代码集成。因此,Dart团队想出了一个绑定生成器(ffigen),可以从C头文件中自动创建FFI包装器。

该软件包使用LLVMLibClang来解析C头文件。对于在macOS内安装LLVM

brew install llvm

dart:fi提供了多种类型来表示C语言中的类型。

  • 可实例化的本地类型
  • 纯标记的本地类型

可实例化的本地类型。它们或它们的子类型可以在Dart代码中被实例化。例如,Array Pointer Struct Union

纯粹的标记性本地类型。它们与平台无关,不能在Dart代码中实例化。例如,Bool Double Int64 Int32等。

还有一些ABI标记类型,扩展了AbiSpecificInteger,例如Size Short等。

到目前为止,我们已经涵盖了什么是FFI和什么是ffigen,让我们来探讨一下 Dart 2.18的ffigen 里面有什么新东西。

FFIGen的新内容

image.png Dart 2.18

Dart团队希望Dart能够支持与Dart运行平台上的所有主要语言的互操作性。

Dart 2.18开始,"Dart "代码现在可以调用Objective-C和Swift代码,因为这些代码是用来为macOS和iOS编写API。这种互操作机制支持所有类型的应用程序(例如,CLI应用程序到后台应用程序到Flutter代码)。

这个功能并不限于命令行应用程序。即使是在macOS或iOS的Dart Native platform上运行的Dart移动和服务器应用程序,也可以使用dart:fi

这释放了各种可能性,因为在2.18之前,只可能调用基于C/C++的库。

根据官方博客的说法。

这个新机制利用了Objective-C和Swift代码可以作为基于API绑定的C代码公开的事实。Dart API包装器生成工具ffigen可以从API头文件中创建这些捆绑物

这种对Objective-C和Swift的支持从 Dart 2.18开始被标记为实验性的。如果有人遇到任何问题,可以在GitHub上的feedback issue发表评论。

在这一节中,我们创建了一个基于Dart的命令行程序,演示如何使用ffigen的新功能调用一个基于Objective-C的库。

我们将选择macOS中的任何Objective-C库,并将其整合到Dart CLI应用程序中。

这样的一个库是NSURLCache

macOS有一个查询URL缓存信息的API,由NSURLCache类暴露。

NSURLCache通过将NSURLRequest对象映射到NSCachedURLResponse对象,实现对URL加载请求的缓存。它提供了一个复合的内存和磁盘缓存,并允许你操作内存和磁盘部分的大小。

我们将把NSURLCache集成到Dart中,并调用它的一些功能。

  • currentDiskUsage : 磁盘上缓存的当前大小,字节数。
  • diskCapacity : 磁盘缓存的容量,以字节为单位。
  • memoryCapacity : 内存缓存的容量,以字节为单位。

创建Dart CLI应用程序

我们首先使用下面的命令创建Dart CLI应用程序。同时,升级到最新的Dart版本2.18

注意:Dart有多种模板可供选择,见下文。默认情况下,它选择的是控制台应用程序。

image.png Dart模板

这给了我们一个基本的模板,包括所有必要的文件,例如,pubspeclinter。打开pubspec文件,检查与此模板捆绑的依赖关系。

dev_dependencies:
  lints: ^2.0.0
  test: ^1.16.0

编辑你的pubspec文件,添加[ffigen](https://pub.dev/packages/ffigen)dev dependency。接下来,指定这个依赖关系下的配置。配置可以通过2种方式提供--

  1. 在项目的pubspec.yaml文件中,在ffigen键下。
  2. 通过一个自定义的YAML文件,然后在运行时指定这个文件 - dart run ffigen --config config.yaml

我们将首先看到选项2。库的独立配置文件

创建一个名为url_cache_config.yaml的文件,并将以下内容放入其中。

name: URLCacheLibrary
language: objc
output: "url_cache_bindings.dart"
exclude-all-by-default: true
objc-interfaces:
  include:
    - "NSURLCache"
headers:
  entry-points:
    - "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSURLCache.h"

让我们看看上面的配置选项-

  • name 将被生成的类的名称,在我们运行ffigen后,这个类将被称为URLCacheLibrary
  • language 必须是cobjc之一。默认为c。由于我们选择的库是用Objective-C编写的,我们指定objc
  • output 生成的绑定文件的输出路径。这个文件将有所有的FFI绑定,它照顾到了Obj-C中的函数。
  • headers这包括头文件的路径,它包括在entry-points下指定的位置的所有东西。在我们的例子中,头文件存在于Foundation.framework中。
  • exclude-all-by-default 当一个声明过滤器(例如function:struct:)为空时,它默认为包括所有内容。如果这个标志被启用,默认行为是排除一切。

Objective-C配置选项

  • objc-interfaces 这是对接口声明的过滤。在我们的例子中,我们指定了NSURLCache接口。
objc-interfaces:
  include:
    # Includes a specific interface.
    - 'NSURLCache'
    # Includes all interfaces starting with "NS".
    - 'NS.*'
  exclude:
    # Override the above NS.* inclusion, to exclude NSURL.
    - 'NSURL'
  rename:
    # Removes '_' prefix from interface names.
    '_(.*)': '$1'

生成绑定

要生成绑定,请运行以下程序。

dart run ffigen --config url_cache_config.yaml
## url_cache_config is the file which we created above

这个命令创建了一个新的文件(url_cache_bindings.dart),在url_cache_config.yamloutput参数中指定,其中包含一堆生成的API绑定。使用这个绑定文件,我们可以编写我们的Dartmain方法。

集成到Dart中

我们在上面的步骤中使用ffigen生成了绑定文件。让我们看看如何把它整合到 "Dart "中 我们创建了一个新的dart文件,叫做url_cache.dart

在这个文件中,我们将加载并与生成的库进行交互。

void main() {
  const dylibPath = '/System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation';
final lib = URLCacheLibrary(DynamicLibrary.open(dylibPath));
final urlCache = NSURLCache.getSharedURLCache(lib);
  if (urlCache != null) {
    print('currentDiskUsage: ${urlCache.currentDiskUsage}');
    print('currentMemoryUsage: ${urlCache.currentMemoryUsage}');
    print('diskCapacity: ${urlCache.diskCapacity}');
    print('memoryCapacity: ${urlCache.memoryCapacity}');
  }
}

我们在第一步中提到了库的路径。因为,我们使用的库是一个内部库,dylib指向macOS的框架dylib我们可以认为这个库是动态链接的。

image.png 库链接

注意:我们也可以使用我们自己的库或静态库(在我们的应用程序内链接)。

动态链接。在这种类型中,外部库被放在最终的可执行文件中,然而,实际的链接发生在运行时。在动态链接中,只有一份共享库被保存在内存中,这减少了程序大小、内存和磁盘空间。由于库是共享的,与静态链接程序相比,**动态链接程序的速度较慢。

动态链接的库分布在应用程序中的一个单独的文件或文件夹中,并根据需要加载。一个动态链接的库可以通过DynamicLibrary.open加载到Dart。

静态链接。在这种类型中,模块在创建最终的可执行文件之前被复制到程序内部。由于这些程序包括库,它们的大小是大的。然而,由于库已经被编译,这些程序比动态链接程序快。

静态链接库被嵌入到应用程序的可执行图像中,并在应用程序启动时被加载。静态链接库的符号可以使用DynamicLibrary.executableDynamicLibrary.process加载。


接下来,我们通过使用构造函数来构造URLCacheLibrary,它需要dylib路径。为此,我们调用DynamicLibrary.open来加载库文件并提供对其符号的访问。

注意:这个过程只向DartVM加载一次库,而不考虑函数的调用。

一旦库被初始化,我们就可以调用库中的不同方法(这些方法已经生成)。

我们正在寻找一个NSURLCache类。这个类通过将NSURLRequest对象映射到NSCachedURLResponse对象,实现对URL加载请求的响应的缓存。为了获得这个类的实例,我们调用sharedURLCache

final urlCache = NSURLCache.getSharedURLCache(lib);

由于我们有URLCache的实例,我们可以访问不同的方法currentDiskUsage currentMemoryUsage diskCapacity memoryCapacity。让我们运行dart代码,使用

dart run bin/url_cache.dart

其结果是

image.png NSURLCache数据


使用pubspec里面的配置

在上一节中,我们看到了如何使用在一个单独的配置文件中指定的配置,让我们看看如何使用pubspec中的配置。

我们将选择另一个存在于macOS中的Objective-C库。

这样的一个库是NSTimeZone

这个API用于查询时区和一个地区的标准时间政策。这些时区可以有诸如 "美国/洛杉矶 "这样的标识符,也可以用诸如 "太平洋标准时间 "的缩写来标识。

这个库的头文件存在于NSTimeZone.h中,可以在苹果基础库中找到。让我们在pubspec中加入配置。

dev_dependencies:
ffigen:
  name: TimeZoneLibrary
  language: objc
  output: "timezone_bindings.dart"
  exclude-all-by-default: true
  objc-interfaces:
    include:
      - "NSTimeZone"
  headers:
    entry-points:
      - "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/NSTimeZone.h"

在上面的配置中,我们指定

  • name 这个类将被称为TimeZoneLibrary
  • language 我们选择的库是用Objective-C编写的,我们指定objc
  • headers 头文件的路径,它存在于Foundation.framework中。

为了生成绑定文件,我们运行以下程序

dart run ffigen

这个命令创建了一个新的文件(timezone_bindings.dart),正如在output参数中所指定的,包含了一堆生成的API绑定。使用这个绑定文件,我们可以编写我们的Dart main方法。

我们创建一个新的dart文件,叫做timezones.dart 在这个文件中,我们加载并与生成的库交互。

const dylibPath='/System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation';
final lib = TimeZoneLibrary(DynamicLibrary.open(dylibPath));
final timeZone = NSTimeZone.getLocalTimeZone(lib);
if (timeZone != null) {
  print('Timezone name: ${timeZone.name}');
  print('Offset: ${timeZone.secondsFromGMT / 60 / 60} hours');
}

我们通过使用构造函数来构造TimeZoneLibrary,它需要dylib路径。一旦库被初始化,我们就调用库中的不同方法。

我们将把NSTimeZone集成到Dart中,并调用它的一些功能。

  • name : 识别接收者的地缘政治区域ID。
  • secondsFromGMT : 接收者与格林威治时间之间的当前秒差。

为了得到这个类的实例,我们调用localTimeZone

final timeZone = NSTimeZone.getLocalTimeZone(lib)

由于我们有NSTimeZone的实例,我们可以访问不同的方法name secondsFromGMT。让我们运行dart代码,使用

dart run bin/timezones.dart

结果是这样的

image.png NSTimeZone数据


垃圾收集

Objective-C使用引用计数进行内存管理,但在Dart方面,内存管理是自动处理的。Dart包装对象保留了对Objective-C对象的引用,当Dart对象被垃圾回收时,生成的代码会使用NativeFinalizer自动释放该引用。

Objective-C互操作的局限性

目前多线程的问题是Dart对 "Objective-C "互操作实验性支持的限制。然而,这些限制并不是故意的,而是由于Dart隔离和操作系统线程之间的关系,以及苹果如何处理多线程。

  • 虽然ffigen支持将Dart函数转换为Objective-C块,但大多数苹果API并不保证回调会在哪个线程上运行。
  • Dart的隔离区与线程不一样。隔离器在线程上运行,但不保证在任何特定的线程上运行。虚拟机可以在没有警告的情况下改变一个隔离物的运行线程。
  • 苹果公司的API不是线程安全

由于虚拟机可以改变隔离区的运行线程,这意味着在一个隔离区中创建的回调可能会在另一个或没有隔离区的情况下被调用。不过,在这个问题上有一些调整,如在"cupertino:http "中实现的。


测试FFIGen

到目前为止,我们看到的是如何生成绑定,并从Dart CLI中消费它们。在本节中,我们将看到如何测试生成的绑定。

我们安装依赖项yamllogging并创建一个名为ffi_2_18_test的文件。

注意:测试应该遵循<name>_test.dart范式。

yaml依赖关系有助于解析YAML文件。而logging为我们提供了对日志有用的API(基于指定的配置)。

设置日志

我们配置日志级别并为日志信息添加一个处理程序。等级被设置为Level.SEVERE,接下来,我们在onRecord流中监听LogRecord事件。

void logWarnings([Level level = Level.WARNING]) {
  Logger.root.level = level;
  Logger.root.onRecord.listen((record) {
    print('${record.level.name.padRight(8)}: ${record.message}');
  });
}

这个函数logWarningssetUpAll中被调用,在setUpAll中注册的函数将在所有测试前运行一次。

NSURLCache的测试

test('url_cache', () {
  final pubspecFile = File('url_cache_config.yaml');
  final pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
  final config = Config.fromYaml(pubspecYaml);
  final output = parse(config).generate();
  expect(output, contains('class URLCacheLibrary{'));
  expect(output, contains('static NSURLCache?getSharedURLCache('));
 });
}

我们开始使用test方法编写一个测试。我们做的第一件事是使用一个file对象创建url_cache_config.yaml

接下来,我们使用loadYaml这个函数,从YAML字符串中加载一个文档。由于这个方法期望参数是一个字符串,我们使用readAsStringSync将文件内容同步转换为字符串。

返回值主要是正常的Dart对象。由于我们使用的是YAML文件,我们将结果指定为YamlMapYAML映射支持一些默认Dart地图实现没有的关键类型。

接下来,我们使用Configffigen中创建测试所需的配置,从上面的yaml地图。最后,我们使用parse来生成绑定的内容。

上述步骤的输出与字符串进行比较,例如

expect(output, contains('class URLCacheLibrary{'));
expect(output, contains('static NSURLCache? getSharedURLCache('));

这是因为一旦我们运行测试,使用

dart test test/ffi_2_18_test.dart

它在运行过程中会生成配置文件,这个文件会与上面的字符串进行比较。

image.png 使用FFIGen测试通过。


对NSTimeZone的测试

test('timezones', () {
  final pubspecFile = File('pubspec.yaml');
  final pubspecYaml = loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
  final config = Config.fromYaml(pubspecYaml['ffigen'] as YamlMap);
  final output = parse(config).generate();
expect(output, contains('class TimeZoneLibrary{'));
  expect(output, contains('class NSString extends _ObjCWrapper {'));
  expect(output, contains('static NSTimeZone? getLocalTimeZone('));
 );
}

我们使用pubspec.yaml文件创建一个file对象。接下来,我们使用loadYaml,从YAML字符串中加载文件。

接下来,我们使用Configffigen中创建测试所需的配置,从上述yaml地图中。由于pubspec文件里面定义了属性ffigen,我们直接引用它并指定输出类型为YamlMap

注意:对于NSTimeZone,我们在pubspec.yaml中指定了ffigen配置。

最后,我们使用parse`来生成绑定的内容。这一步的输出会与字符串进行比较,例如

expect(output, contains('class TimeZoneLibrary{'));
expect(output, contains('static NSTimeZone? getLocalTimeZone('));

这是因为一旦我们运行测试,使用

dart test test/ffi_2_18_test.dart

它在运行过程中会生成配置文件,并与测试中的字符串进行比较。

image.png 使用FFIGen测试通过。

Website: flatteredwithflutter.com/using-ffige…

其他文章。

medium.com/flutter-com…

levelup.gitconnected.com/google-pay-…

betterprogramming.pub/how-to-use-…

源代码