为独立 Dart VM 提供原生扩展

作者:William Hesse
2012年5月

在独立 Dart VM(命令行应用程序)上运行的 Dart 程序可以通过本地扩展调用共享库中的 C 或 C++ 函数。 本文将讲解如何在 Windows,macOS,以及 Linux 上编写和构建这样的本地扩展。

你可以提供两种类型的本地扩展:异步扩展或同步扩展。_异步扩展_在一个单独的线程中执行一个本地函数,由 Dart VM 调度。_同步扩展_直接使用 Dart 虚拟机库的 C API ( Dart 内嵌 API ) 并在 Dart 的独占 线程中执行。通过向 Dart 端口( Dart port )发送消息来调用异步函数,在应答端口( reply port )接受响应。

本地扩展解析

一个 Dart 本地扩展包含两部分: Dart 库和本地库。 Dart 库的类及顶层函数的定义方式不变, 但在这些函数中,使用本地代码中实现的函数需要使用 native 关键字声明。本地库是使用 C 或 C++ 编写的共享库,库中包含了这些函数(使用 native 关键字声明的函数)的实现。

Dart 库使用 import 语句和 dart-ext: URI 的方案指定本机库。截至 1.20 , URI 必须是绝对路径,如 dart-ext:/path/to/extension ,或者只使用扩展名, 如 dart-ext:extension 。 VM 修改 URI 将平台特定的前缀和后缀添加到扩展名。例如,在 Linux 上 extension 会变成 libextension.so 。如果 URI 是绝对路径,那么文件不存 在的情况下会导入失败。如果 URI 只是扩展的名称,那么 VM 会首先查找与导入 Dart 库相邻的文 件。如果没有找到,那么 VM 会将扩展的文件名传递给平台的特殊调用,以加载动态库 (例如,Linux 上的 dlopen ),此时该库的加载将遵循它所在平台的搜索过程。

示例代码

文中介绍的扩展示例的代码位于 Dart 仓库的 samples/sample_extension 目录中。

扩展示例调用 C 标准库的 rand() 和 srand() 函数,将伪随机数返回到 Dart 程序。由于本地的异步扩展 和同步扩展共享了大部分本地代码,所以示例使用了单个本地源文件(以及生成单个共享库)实现了这两个扩展。 这两个扩展的 Dart 部分分别在两个库文件中实现。另外两个 Dart 文件提供了异步和同步扩展的使用和测试示例。

本文中扩展的共享库(本地代码)称为 sample_extension 。 它是一个 C++ 文件, sample_extension.cc, 其中包括 6 个被 Dart 调用的函数。

sample_extension_Init():
在扩展被加载时被调用。
ResolveName():
第一次调用给定名称的本地函数时,将本地函数的 Dart 名称解析为 C 函数指针。
SystemRand() and SystemSrand():
同步扩展的实现。这些本地方法由 Dart 直接调用,它们调用 C 标准库的 rand() 和 srand() 函数。
wrappedRandomArray() and randomArrayServicePort():
异步扩展的实现。 randomArrayServicePort() 函数创建一个本地端口,并将这个端口和 wrappedRandomArray() 函数关联在一起。当 Dart 向本地端口发送一个消息, Dart VM 就会调度 wrappedRandomArray() 函数在一个单独的线程中执行。

共享库中的一些代码用来设置和初始化,对于所有的扩展这些代码几乎是相同的。函数 sample_extension_Init() 和 ResolveName() 在所有的扩展中几乎相同,同样在所有的异步扩展中都 必须有一个类似于 randomArrayServicePort() 的函数。

示例中的同步扩展

因为异步扩展非常像是在同步扩展的基础上增加了一些函数,所以这里我们首先说明同步扩展。 首先,我们将展示扩展的 Dart 部分以及加载扩展时发生的函数调用序列。然后我们将解释如何使用 Dart 中嵌入的 API ,解释本地代码,以及描述扩展被调用时会发生了什么。

这是同步扩展的 Dart 部分,文件名 sample_synchronous_extension.dart

library sample_synchronous_extension;

import 'dart-ext:sample_extension';

// The simplest way to call native code: top-level functions.
int systemRand() native "SystemRand";
bool systemSrand(int seed) native "SystemSrand";

本地扩展的代码存在两个不同的执行时间。首先,它会在本地扩展加载的时候执行。后面,它会在 本地扩展实现被调用时执行。

下面是加载时的事件序列,当一个 Dart 应用导入 sample_synchronous_extension.dart 时开始执行:

  1. Dart 库 sample_synchronous_extension.dart 被加载,Dart VM 处理 import 'dart-ext:sample_extension' 代码。
  2. Dart VM 从 Dart 库的目录中加载共享库 ‘sample_extension’ 。
  3. 共享库中的 sample_extension_Init() 函数被调用。它将共享库函数 ResolveName() 注册为 sample_extension.dart 库中所有本地函数的名称解析器。通过解析器可以查找 Dart 中对应的同步 扩展的本地函数。

在本地代码中使用 Dart 内嵌 API

如扩展示例所示,本地共享库包含初始化函数,名称解析函数以及在扩展中由 Dart 部分声明并在本地实现的函数。 初始化函数注册本地名称解析函数,用作查找该库的本地函数名称。 当 Dart 库中以 native "function_name" 声明的函数被调用时, 本地库使用字符串 “function_name“,以及 “function_name” 函数的参数个数作为参数 调用名称解析函数。然后,名称解析函数会返回一个函数指针,这个函数指针指向对应 “function_name” 函数的本地函数实现。所有 Dart 的原生扩展中的初始化函数以及名称解析函数看上去几乎是一样的。

本地库中的函数使用 Dart 内嵌 API 与 VM 进行通信,因此本地代码要包含 dart_api.h 头文件, 它位于 SDK 中的 dart-sdk/include/dart_api.h 或者在仓库 runtime/include/dart_api.h。 Dart 内嵌 API 作为接口内嵌在包含 Dart VM 的浏览器或者运行命令行程序的独立 VM 中。API 由大约 100 个函数接口和许多数据类型和数据结构定义组成。它们在 dart_api.h 中声明,并备有注释。它们的使用示例在单元测试文件 runtime/vm/dart_api_impl_test.cc

由 Dart 调用的本地函数必须是 Dart_NativeFunction 类型,类型在 dart_api.h 中定义为:

typedef void (*Dart_NativeFunction)(Dart_NativeArguments arguments);

可以看到 Dart_NativeFunction 是一个函数指针,函数指针指向只接受一个 Dart_NativeArguments 参数对象,且无返回值的函数。接受的参数对象是一个 Dart 对象,通过 API 访问该参数对象,可以得到 参数个数,以及指定索引的参数 Dart_Handle 。本地函数向 Dart 应用返回一个 Dart 对象作为返回值。 该返回值被 Dart_SetReturnValue() 函数保存到参数对象里。

Dart handle

扩展的本地函数实现广泛的使用 Dart_Handles 。调用 Dart 内嵌 API 会返回一个 Dart_Handle 并且 常常将 Dart_Handle 作为函数参数。Dart_Handle 是一个间接不透明指针,指向一个在 Dart 堆上的对象, Dart_Handles 属于值拷贝(浅拷贝)。在垃圾收集阶段会移动堆上的 Dart 对象,但即使是在垃圾收集阶段 这些句柄仍保持有效,因此本地代码必须使用句柄来存储堆上对象的引用。由于这些句柄的存储和持有需要占用 资源,所以必须要在不使用它们的时候对它们进行释放。在释放句柄之前,VM 的垃圾收集器无法收集它指向的对 象,即使这些对象已经不存在其他的引用。

Dart 内嵌 API 会自动创建一个作用域来管理本地函数中句柄的生命周期。本地函数进入时会创建本地句柄的 作用域,并在该函数退出时将作用域删除。如果函数正常返回,或以 PropagateError 退出,则作用域删除。 Dart 内嵌 API 返回的大多数句柄和内存指针都在当前本地作用域内分配,并在函数返回后失效。如果扩展应用 想要长时间保持指向 Dart 对象的指针,可以使用 持久句柄(参见 Dart_NewPersistentHandle() 和 Dart_NewWeakPersistentHandle() ),这样可以使句柄在本地作用域结束后仍然有效。

调用 Dart 内嵌 API 可能会在 Dart_Handle 返回值中返回错误。这些错误或者是异常应该作为返回值传递 给函数的调用者。

本地扩展中的大多数函数—类型为 Dart_NativeFunction 的函数—没有返回值,必须以另一种 方式将错误传递给错误处理程序。函数中调用 Dart_PropagateError 来传递错误并控制程序流程到错误处理的 位置。该示例使用一个名为 HandleError() 的辅助函数使上述实现更加便捷。Dart_PropagateError() 函数 没有返回。

本地代码:sample_extension.cc

这里我们将展示扩展示例的本地代码,从初始化函数开始,然后是本地函数实现,最后是称解析函数。 两个异步扩展的本地函数会在后面内容展示。

#include <string.h>
#include "dart_api.h"
// 提前声明 ResolveName 函数。
Dart_NativeFunction ResolveName(Dart_Handle name, int argc, bool* auto_setup_scope);

// 以 _Init 扩展名结尾的初始化函数。
DART_EXPORT Dart_Handle sample_extension_Init(Dart_Handle parent_library) {
  if (Dart_IsError(parent_library)) return parent_library;

  Dart_Handle result_code =
      Dart_SetNativeResolver(parent_library, ResolveName, NULL);
  if (Dart_IsError(result_code)) return result_code;

  return Dart_Null();
}

Dart_Handle HandleError(Dart_Handle handle) {
 if (Dart_IsError(handle)) Dart_PropagateError(handle);
 return handle;
}

// 本地函数通过 Dart_NativeArguments 结构体获取函数参数,
// 并使用函数 Dart_SetReturnValue 返回执行结果。
void SystemRand(Dart_NativeArguments arguments) {
  Dart_Handle result = HandleError(Dart_NewInteger(rand()));
  Dart_SetReturnValue(arguments, result);
}

void SystemSrand(Dart_NativeArguments arguments) {
  bool success = false;
  Dart_Handle seed_object =
      HandleError(Dart_GetNativeArgument(arguments, 0));
  if (Dart_IsInteger(seed_object)) {
    bool fits;
    HandleError(Dart_IntegerFitsIntoInt64(seed_object, &fits));
    if (fits) {
      int64_t seed;
      HandleError(Dart_IntegerToInt64(seed_object, &seed));
      srand(static_cast<unsigned>(seed));
      success = true;
    }
  }
  Dart_SetReturnValue(arguments, HandleError(Dart_NewBoolean(success)));
}

Dart_NativeFunction ResolveName(Dart_Handle name, int argc, bool* auto_setup_scope) {
  // 如果执行失败,返回 NULL, Dart 会抛出异常。
  if (!Dart_IsString(name)) return NULL;
  Dart_NativeFunction result = NULL;
  const char* cname;
  HandleError(Dart_StringToCString(name, &cname));

  if (strcmp("SystemRand", cname) == 0) result = SystemRand;
  if (strcmp("SystemSrand", cname) == 0) result = SystemSrand;
  return result;
}

以下是第一次调用函数 systemRand() 时在运行时产生的事件序列 ( systemRand() 定义在 sample_synchronous_extension.dart 中)。

  1. 使用包含 “SystemRand” 的 Dart 字符串和整数 0 来调用共享库中的 ResolveName() 函数,这里整数表示调用中的参数数量。 “SystemRand” 是 systemRand()声明中 native 关键字后面的字符串。
  2. ResolveName() 返回共享库中本地函数 SystemRand() 的函数指针。
  3. Dart 中 systemRand() 调用的参数被打包到 Dart_NativeArguments 对象中,并使用 Dart_NativeArguments 对象作为 参数调用 SystemRand() 函数,且该对象是 SystemRand() 的唯一参数。
  4. SystemRand() 函数执行,将函数返回值存储到 Dart_NativeArguments 对象中,并返回。
  5. Dart VM 从 Dart_NativeArguments 对象中提取返回值,并将其作为对 systemRand() 在 Dart 调用的返回结果。

后续再调用 systemRand() 时,函数查找的结果已经被缓存,因此不会再调用 ResolveName() 。

本地异步扩展

如上所述,同步扩展使用 Dart 内嵌 API 来处理 Dart 的堆对象,并且在当前隔离的主 Dart 线程上执行。 那么与之相反,异步扩展基本上不使用 Dart 内嵌 API ,并且它在独立的线程上执行,这样就不会阻塞主 Dart 线程。

在某些方面,异步扩展的编写比同步扩展更容易。异步扩展使用 Dart 内嵌 API 中的本地端口函数在独立线程 上调度 C 函数执行。对于异步扩展 Dart 端的代码仅仅暴露为 Dart SendPort (端口)。发送到端口的消 息会自动转换为名为 Dart_CObject 的 C 结构体,该结构体包含 C 数据类型,如 int, double,和 char* 。 然后将结构体传递给 C 函数,C 函数在一个独立的线程中执行,此线程由 VM 管理的线程池分配。C 函数可以 通过 Dart_CObject 响应应答端口。 Dart_CObject 被转换回 Dart 对象树,并在 Dart 异步调用的应答 端口上作为应答返回。与同步扩展相比较,异步扩展将 Dart 对象自动转换为 Dart_CObject C 结构取代了 同步扩展中使用 Dart 内嵌 API 从对象获取字段并将 Dart 对象转换为 C 值类型的过程。

要创建异步本地扩展,需要做三件事情:

  1. 包装一个我们希望调用的 C 函数(包装器),在这个包装器中将 Dart_CObject 输入参数转换为期望 的输入参数,将函数的结果转换为 Dart_CObject ,并将其发送回 Dart 。
  2. 编写一个本地函数,创建一个本地端口并将其关联到包装器。这个本地函数是一个同步本地方法,在 本地扩展中看起来像是上述的同步扩展函数。这样,我们就将刚刚在步骤 1 中的包装器添加到了扩展中。
  3. 编写一个 Dart 类来获取本地端口并持有这个端口。在该类中,提供一个函数,将其参数作为消息转发 到本地端口,并在收到消息回复时调用一个回调处理。

包装 C 函数

下面是一个 C 函数的例子(由于使用了 reinterpret_cast,它实际上是一个 C++ 函数), 函数在给定种子和长度的情况下创建了一个随机字节数组。返回的数据存储在一个新分配数组中, 该数组会在后续处理中释放:

uint8_t* random_array(int seed, int length) {
  if (length <= 0 || length > 10000000) return NULL;

  uint8_t* values = reinterpret_cast<uint8_t*>(malloc(length));
  if (NULL == values) return NULL;

  srand(seed);
  for (int i = 0; i < length; ++i) {
    values[i] = rand() % 256;
  }
  return values;
}

在从 Dart 调用这个 C 函数之前,我们将它放到了一个包装器中,这个包装器用于解包 Dart_CObject 中包含的 随机种子和要生成的随机数长度,以及包装返回结果到 Dart_CObject 中。 Dart_CObject 可以包含一个整数 (任意大小值),一个浮点数,一个字符串或者一个 Dart_CObject 数组。Dart_CObject 在 dart_native_api.h 中 实现,是一个包含 union 的结构体。查看 dart_native_api.h 来查找用于访问的 union 成员字段和标记。发送 Dart_CObject 之后,可以释放 Dart_CObject 及其所有资源,因为它们已经被复制到了 Dart 堆上的 Dart 对象中。

void wrappedRandomArray(Dart_Port dest_port_id,
                        Dart_Port reply_port_id,
                        Dart_CObject* message) {
  if (message->type == Dart_CObject::kArray &&
      2 == message->value.as_array.length) {
    // 使用 .as_array 和 .as_int32 来访问 Dart_CObject 中的数据。
    Dart_CObject* param0 = message->value.as_array.values[0];
    Dart_CObject* param1 = message->value.as_array.values[1];
    if (param0->type == Dart_CObject::kInt32 &&
        param1->type == Dart_CObject::kInt32) {
      int length = param0->value.as_int32;
      int seed = param1->value.as_int32;

      uint8_t* values = randomArray(seed, length);

      if (values != NULL) {
        Dart_CObject result;
        result.type = Dart_CObject::kUint8Array;
        result.value.as_byte_array.values = values;
        result.value.as_byte_array.length = length;
        Dart_PostCObject(reply_port_id, &result);
        free(values);
        // 在函数退出时,结果是可以被释放的。
        // Dart_PostCObject 已经拷贝了这些数据。
        return;
      }
    }
  }
  Dart_CObject result;
  result.type = Dart_CObject::kNull;
  Dart_PostCObject(reply_port_id, &result);
}

Dart_PostCObject() 是 Dart 内嵌 API 中唯一一个可以被包装器或 C 函数调用的函数。由于这的 包装器或 C 函数不再当前隔离作用域,所以多数 API 在这里调用是非法的。在这里不能抛出任何错误或 异常,因此任何错误必须被编码到在应答消息中,以便由扩展的 Dart 部分进行解码和抛出。

设置本地端口

现在我们来设置从 Dart 代码发送消息到调用这个包装后的 C 函数的路径。我们创建一个调用此函数的 本地端口,并返回连接到这个端口的发送端口( send port )。 Dart 库从此函数获取端口,并对端口 发送调用。

void randomArrayServicePort(Dart_NativeArguments arguments) {
  Dart_SetReturnValue(arguments, Dart_Null());
  Dart_Port service_port =
      Dart_NewNativePort("RandomArrayService", wrappedRandomArray, true);
  if (service_port != kIllegalPort) {
    Dart_Handle send_port = Dart_NewSendPort(service_port);
    Dart_SetReturnValue(arguments, send_port);
  }
}

在 Dart 端调用本地端口

在 Dart 端,为了向端口发送消息后,端口的 Dart 异步函数回调能够被调用,我们需要一个类来存储 这个发送端口。 通常,在Dart 类第一次调用函数获取端口时,将端口缓存。下面异步扩展的 Dart 库部分:

library sample_asynchronous_extension;

import 'dart-ext:sample_extension';

// 一个缓冲本地端口的类,用于调用异步扩展。
class RandomArray {
  static SendPort _port;

  void randomArray(int seed, int length, void callback(List result)) {
    var args = new List(2);
    args[0] = seed;
    args[1] = length;
    _servicePort.call(args).then((result) {
      if (result != null) {
        callback(result);
      } else {
        throw new Exception("Random array creation failed");
      }
    });
  }

  SendPort get _servicePort {
    if (_port == null) {
      _port = _newServicePort();
    }
    return _port;
  }

  SendPort _newServicePort() native "RandomArray_ServicePort";
}

结论及更多资源

到这你已经了解了本地的同步扩展和异步扩展。我们希望你可以使用这些工具来访问现有的 C 和 C++ 库, 从而为独立的 Dart VM 添加新的有用的功能。因为异步扩展不会阻塞主 Dart 线程,而且实现更加简单, 所以我们更建议使用异步而不是使用同步来实现扩展。内置的 Dart I/O 库就是围绕着异步调用构建的, 从而实现了高效的,无阻塞的吞吐。扩展也应当拥有与 Dart I/O 同样的性能目标。

附录:扩展的编译和链接

共享库的构建会比较棘手,而且构建共享库的工具决于平台。 Dart 本地扩展构建会更加棘手,因为本地扩展 是动态加载的,并且工具要链接 Dart 库包含的 Dart 内嵌 API 函数到动态加载的可以执行文件中。

与所有共享库一样,编译步骤必须生成与位置无关的代码。链接步骤中必须指定在加载库时允许在可执行文件中 存在未实现的函数。我们将在 Linux, Windows 和 Mac 平台上说明这些操作命令。如果你下载了 Dart 的 源码仓库,示例代码还包括一个独立于平台的构建系统(被称为 gyp )以及一个用于构建扩展示例的构建文件 sample_extension.gypi 。

在 Linux 上构建

在 Linux 上,在 samples/sample_extension 目录中编译代码,如下所示:

g++ -fPIC -m32 -I{path to SDK include directory} -DDART_SHARED_LIB -c sample_extension.cc

通过目标文件创建共享库:

gcc -shared -m32 -Wl,-soname,libsample_extension.so -o
libsample_extension.so sample_extension.o

移除 -m32 参数创建能够执行在 64 位独立 Dart VM 上的 64 位共享库。

在 Mac 上构建

  1. 使用 Xcode(测试环境 Xcode 3.2.6 ),创建一个与本地扩展名相同的新项目,选择 Framework & Library/BSD C Library,类型为 dynamic。
  2. 添加扩展的源文件到项目中。
  3. 在 Project/Edit 中进行以下更改,在对话框中选择 Build 选项卡和 All Configurations :
    1. 在 Linking 部分, Other Linker Flags 项中,增加 -undefined dynamic_lookup 。
    2. 在 Search Paths 部分, Header Search Paths 项中,增加 dart_api.h 文件路径,在文件位于已下载的 SDK 或 Dart 仓库中。
    3. 在 Preprocessing 部分, Preprocessor Macros 项中,增加 DART_SHARED_LIB=1 。
  4. Choose the correct architecture (i386 or x86_64), and build by choosing Build/Build.
  5. 选择当前架构( i386 或 x86_64 ),并选择 Build/Build 进行构建。

生成的 lib[extension_name].dylib 位于项目位置的 build/ 子目录中,因此需要将它复制到所需位置 (这里的可能是扩展的 Dart 库部分的位置)。

在 Windows 上构建

Windows DLL 的编译很复杂,因为我们需要链接库文件 dart.lib ,dart.lib 不包含代码本身,但 在 DLL 在动态加载的时候,通过它能够链接到的 Dart 可执行文件( dart.exe )来解析对 Dart 嵌入 API 的调用。构建 Dart 时会生成该库文件,且文件包含在 Dart SDK 中。

  1. 在 Visual Studio 2008 或 2010 中创建项目类型为 Win32/Win32 的新项目。项目命名与本地 扩展名相同。在 wizard 的下一个屏上,将应用程序类型修改为 DLL 并选择 “Empty project” , 然后选择完成。
  2. 将本地扩展的 C/C++ 文件添加到项目中的源文件目录中。确保源文件中包含 [extension name]_dllmain_win.cc 文件。
  3. 修改项目属性中的以下设置:
    1. 配置属性 / Linker / Input / Additional dependencies :增加 dart-sdk\bin\dart.lib ,文件位于已下载的 Dart SDK 中。
    2. 配置属性 / C/C++ / General / Additional Include Directories :增加包含 dart_api.h 目录的路径,该文件位于已下载的 Dart SDK 中的 dart-sdk/include 目录。
    3. 配置属性 / C/C++ / Preprocessor / Preprocessor Definitions :增加 DART_SHARED_LIB 。这里只是为了从 DLL 中导出 _init 函数,因为该函数在本地扩展中被声明为 DART_EXPORT 。
  4. 构建项目,并将 DLL 复制到正确的目录,对于相对于扩展的 Dart 库部分的目录。确保对于下载的 32 位 SDK 构建的是一个 32 位的 DLL ,对于下载的 64 位 SDK 构建的是一个 64 位的 DLL 。