[Dart翻译]独立Dart虚拟机的本地扩展

1,310 阅读16分钟

原文地址:dart.dev/server/c-in…

原文作者:github.com/whesse

发布时间:2012年5月

注意:本页面讨论的扩展机制是用于自定义Dart嵌入器对虚拟机的深度集成。这个扩展机制不支持用于扩展目的,可能会在未来的版本中被移除。

如果你只需要调用用C或C++编写的现有代码,请参见使用FFI的C & C++互操作

本页的示例代码有一些已知的问题

在独立的Dart虚拟机上运行的Dart程序(命令行应用程序)可以通过本地扩展调用共享库中的C或C++函数。本文介绍了如何在Windows、macOS和Linux上编写和构建这种本地扩展。

你可以提供两种类型的本地扩展:异步或同步。异步扩展在一个单独的线程上运行一个原生函数,由Dart VM调度。同步扩展直接使用 Dart 虚拟机库的 C API(Dart Embedding API),并在与 Dart 隔离的同一线程上运行。异步函数的调用方式是向Dart端口发送消息,在response端口上接收响应。

原生扩展的解剖

一个Dart原生扩展包含两个文件:Dart库和原生库。Dart库像往常一样定义了类和顶层函数,但声明其中一些函数是用native关键字用本地代码实现的。本机库是一个共享库,用C或C++编写,包含这些函数的实现。

Dart库使用import语句和dart-ext:URI方案。从1.20开始,URI必须是绝对路径,比如dart-ext:/path/to/extension,或者只有扩展名,比如dart-ext:extension。虚拟机修改URI,将平台特定的前缀和后缀添加到扩展名中。例如,extension在Linux上变成libextension.so。如果URI是绝对路径,如果文件不存在,导入就会失败。如果URI只是扩展名,虚拟机首先会寻找与导入的Dart库相邻的文件。如果在那里没有找到,VM就会把文件名传给特定平台的动态库加载调用(例如Linux上的dlopen),后者可以自由地遵循自己的搜索过程。

示例代码

本文介绍的示例扩展的代码在Dart仓库的samples/sample_extension目录下。

样本扩展调用C标准库的rand()和srand()函数,向Dart程序返回伪随机数。因为异步和同步原生扩展的原生部分共享大部分代码,所以一个原生源文件(以及由此产生的共享库)实现了两个扩展。这两个扩展有单独的Dart库文件。两个额外的Dart文件提供了使用和测试异步和同步扩展的例子。

本文所展示的扩展的共享库(本地代码)被称为sample_extension。它的C++文件sample_extension.cc包含了六个从Dart中调用的函数。

sample_extension_Init():

当扩展被加载时调用。

ResolveName():

在第一次调用给定名称的本地函数时调用,将本地函数的Dart名称解析为C函数指针。

SystemRand()和SystemSrand():

实现同步扩展。这些都是直接从Dart中调用的原生函数,并且调用C标准库中的rand()和srand()。

wrappedRandomArray()和 randomArrayServicePort():

实现了异步扩展。randomArrayServicePort() 创建了一个本机端口,并将其与 wrappedRandomArray() 相关联。当Dart向本机端口发送消息时,Dart虚拟机会将wrappedRandomArray()调度到一个单独的线程上运行。

共享库中的一些代码是设置和初始化代码,对所有扩展来说都可以是一样的。在所有扩展中,函数sample_extension_Init()和ResolveName()应该几乎相同,所有异步扩展中都必须有一个版本的randomArrayServicePort()。

同步的示例扩展

因为异步扩展的工作原理就像同步扩展一样,增加了一些功能,所以我们先展示同步扩展。首先,我们将展示扩展的Dart部分和扩展加载时发生的函数调用序列。然后我们将解释如何使用Dart Embedding 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";

实现本地扩展的代码在两个不同的时间执行。首先,它在加载本地扩展时运行。之后,它在调用一个具有本地实现的函数时运行。

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

  1. Dart库sample_synchronous_extension.dart被加载,Dart虚拟机点击代码import 'dart-ext:sample_extension'.
  2. VM从包含Dart库的目录中加载共享库'sample_extension'。
  3. 共享库中的函数sample_extension_Init()被调用。它将共享库函数ResolveName()注册为sample_extension.dart库中所有本地函数的名称解析器。下面我们看同步原生函数时,就会知道名称解析器的作用。

注意:共享库的文件名取决于平台。在Windows上,VM加载sample_extension.dll,在Linux上加载libsample_extension.so,在Mac上加载libsample_extension.dylib。我们在文章最后的附录中展示了如何构建和链接这些共享库。

从本地代码中使用Dart嵌入API

正如扩展示例所示,本地共享库包含一个初始化函数、一个名称解析函数,以及扩展的Dart部分中声明为本地的函数的本地实现。初始化函数注册本机名称解析函数,负责在本机库中查找本机函数名称。当调用在Dart库中声明为native "function_name"的函数时,以字符串 "function_name "为参数,加上函数调用中的参数数,调用本地库的名称解析函数。然后,名称解析函数返回一个函数指针,指向该函数的本地实现。在所有的Dart原生扩展中,初始化函数和名称解析函数看起来都差不多。

原生库中的函数使用Dart Embedding API与虚拟机进行通信,所以原生代码中包含了头dart_api.h,它在SDK中的dart-sdk/include/dart_api.h或在runtime/include/dart_api.h的仓库中。Dart Embedding API是嵌入者用来将Dart虚拟机包含在Web浏览器中或单机虚拟机中的命令行的接口。它由大约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对象,并且不返回任何值。arguments对象是一个由API函数访问的Dart对象,它返回参数的数量,并在指定的索引处返回一个Dart_Handle到参数。原生函数通过使用Dart_SetReturnValue()函数将Dart对象存储在arguments对象中,作为返回值,返回给Dart应用。

Dart处理

该扩展的原生函数实现广泛使用Dart_Handles。在Dart Embedding API中的调用会返回一个Dart_Handle,并且经常使用Dart_Handles作为参数。Dart_Handle是一个不透明的间接指针,指向Dart堆上的一个对象,Dart_Handles是通过值复制的。即使在垃圾回收阶段移动堆上的Dart对象时,这些句柄仍然有效,所以本地代码必须使用句柄来存储对堆对象的引用。因为这些句柄需要资源来存储和维护,所以当它们不再使用时,你必须释放它们。在释放句柄之前,虚拟机的垃圾收集器无法收集它所指向的对象,即使没有其他引用也是如此。

Dart Embedding API会自动创建一个新的作用域来管理本地函数中的句柄的寿命。本地句柄作用域在本地函数进入时被创建,在函数退出时被删除。如果函数以PropagateError退出,以及正常返回,这个作用域就会被删除。Dart Embedding API返回的大部分句柄和内存指针都是在当前的本地作用域中分配的,在函数返回后会失效。如果扩展想要长时间保留一个Dart对象的指针,应该使用一个持久化的句柄(参见Dart_NewPersistentHandle()和Dart_NewWeakPersistentHandle()),它在一个局部作用域结束后仍然有效。

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

在本地扩展中的大多数函数--Dart_NativeFunction类型的函数--都没有返回值,必须以另一种方式将错误传递给适当的处理程序。它们调用Dart_PropagateError将错误和控制流传递到应该处理错误的地方。该示例使用了一个名为HandleError()的辅助函数,以方便处理。对Dart_PropagateError()的调用永远不会返回。

本机代码:sample_extension.cc

现在我们来展示一下示例扩展的原生代码,首先是初始化函数,然后是原生函数的实现,最后是名称解析函数。后面将展示实现异步扩展的两个本地函数。

#include <string.h>
#include "dart_api.h"
// Forward declaration of ResolveName function.
Dart_NativeFunction ResolveName(Dart_Handle name, int argc, bool* auto_setup_scope);

// The name of the initialization function is the extension name followed
// by _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;
}

// Native functions get their arguments in a Dart_NativeArguments structure
// and return their results with 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) {
  // If we fail, we return NULL, and Dart throws an exception.
  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()(定义在sample_synchronous_extension.dart中)第一次被调用时。

  1. 共享库中的函数ResolveName()被调用,其中包含 "SystemRand "和整数0的Dart字符串,代表调用中的参数数。字符串"SystemRand "是systemRand()声明中原生关键字后面的字符串文字。
  2. ResolveName()返回一个指向共享库中原生函数SystemRand()的指针。
  3. 在Dart中调用systemRand()的参数被打包成一个Dart_NativeArguments对象,SystemRand()是以这个对象作为唯一的参数被调用的。
  4. SystemRand()进行计算,将其返回值放入Dart_NativeArguments对象中,然后返回。
  5. Dart VM从Dart_NativeArguments对象中提取返回值,作为Dart调用systemRand()的结果返回。

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

异步的本地扩展

正如我们上面所看到的,同步扩展使用Dart Embedding API来处理Dart堆对象,并且它运行在当前隔离的主Dart线程上。另一方面,异步扩展不使用大部分的Dart Embedding API,它运行在一个单独的线程上,这样就不会阻塞主Dart线程。

在很多方面,异步扩展比同步扩展的编程更简单。它们使用Dart Embedding API中的原生ports函数来调度独立线程上的C函数。对于使用该扩展的Dart代码来说,它只是简单地以Dart SendPort的形式出现。发布到这个端口的消息会被自动翻译成一个名为Dart_CObject的C结构,其中包含int、double和char*等C数据类型。然后,这个 C 结构被传递给一个 C 函数,该函数在一个独立的线程中运行,该线程由 VM 管理的线程池中抽取。该C函数可以通过Dart_CObject响应一个回复端口。Dart_CObject会被翻译回Dart对象树,并作为回复出现在Dart async调用的回复端口上。这种将Dart对象自动转换为Dart_CObject C结构的做法,取代了使用Dart Embedding API从对象中获取字段和将Dart对象转换为C值类型的做法。

要创建一个异步的本地扩展,我们要做三件事。

  1. 用一个包装器来包装我们想要调用的C函数,这个包装器将Dart_CObject输入参数转换为所需的输入参数,将函数的结果转换为Dart_CObject,并将其发布回Dart。
  2. 编写一个本地函数,创建一个本地端口,并将其附加到封装的函数上。这个原生函数是一个同步的原生方法,它在一个原生扩展中,看起来就像上面的同步扩展一样。我们刚刚把步骤1中的封装函数也添加到了扩展中。
  3. 编写一个Dart类,用来获取本机端口并缓存它。在这个类中, 提供一个将其参数以消息的形式转发到本地端口的函数, 并在收到消息的回复时调用回调参数。

打包C函数

这里是一个C函数的例子(实际上是一个C++函数,因为使用了reinterpret_cast),它创建了一个随机字节数组,给定了一个种子和一个长度。它返回一个新分配的数组中的数据,这个数组将被包装器释放。

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中调用这个函数,我们把它放在一个包装器中,这个包装器将包含种子和长度的Dart_CObject解包,并将结果值打包成一个Dart_CObject。一个Dart_CObject可以容纳一个整数(各种大小),一个双数,一个字符串,或者一个Dart_CObjects数组。它在dart_native_api.h中被实现为一个包含联合的结构。在dart_native_api.h中可以看到用于访问联盟成员的字段和标签。当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) {
    // Use .as_array and .as_int32 to access the data in the 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);
        // It is OK that result is destroyed when function exits.
        // Dart_PostCObject has copied its data.
        return;
      }
    }
  }
  Dart_CObject result;
  result.type = Dart_CObject::kNull;
  Dart_PostCObject(reply_port_id, &result);
}

Dart_PostCObject()是唯一一个应该从包装器或C函数中调用的Dart Embedding API函数。大部分的API在这里调用是非法的,因为没有当前的隔离体。不能抛出任何错误或异常,所以任何错误都必须在回复消息中进行编码,以便由扩展的Dart部分进行解码和抛出。

设置本地端口

现在我们通过发送消息的方式,建立起从Dart代码中调用这个封装的C函数的机制。我们创建一个本地端口来调用这个函数,并返回一个连接到该端口的发送端口。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方面,我们需要一个类来存储这个send port,当一个带有回调的Dart异步函数被调用时,向它发送消息。Dart类在第一次调用函数的时候就会得到这个端口,用通常的方式进行缓存。这里是异步扩展的Dart库。

library sample_asynchronous_extension;

import 'dart-ext:sample_extension';

// A class caches the native port used to call an asynchronous 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虚拟机添加有用的新功能。我们建议使用异步扩展而不是同步扩展,因为异步扩展不会阻塞Dart主线程,并且可以更简单地实现。内置的Dart I/O库是围绕异步调用建立的,以实现高、非阻塞的吞吐量。扩展应该有同样的性能目标。

附录。编译和链接扩展

构建一个共享库是很棘手的,而且构建共享库的工具是依赖于平台的。构建Dart原生扩展是特别棘手的,因为它们是动态加载的,而且它们链接到Dart嵌入在动态加载它们的可执行文件中的Dart库中的Dart Embedding 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位的库,与64位Dart独立虚拟机一起运行。

在Mac上构建

  1. 使用Xcode(用Xcode 3.2.6测试),创建一个新的项目,名称与本地扩展相同,类型为Framework & Library/BSD C Library,类型为dynamic。
  2. 将您的扩展的源文件添加到项目的源部分。
  3. 在 "项目/编辑项目设置 "中,选择 "构建 "选项卡,在对话框中选择 "所有配置",进行以下修改。
    • 在 Linking 部分,在 Other Linker Flags 行,添加 -undefined dynamic_lookup。
    • 在Search Paths部分,Header Search Paths一行,添加SDK下载或Dart仓库结账中dart_api.h的路径。
    • 在预处理部分,预处理程序宏行,添加DART_SHARED_LIB=1。
  4. 选择正确的架构(i386或x86_64),然后选择Build/Build进行构建。

由此产生的lib[extension_name].dylib将在你的项目位置的build/ 子目录中,所以将它复制到所需的位置(可能是扩展的Dart库部分的位置)。

在Windows上构建

Windows DLL的编译是复杂的,因为我们需要链接库文件dart.lib,它本身并不包含代码,但指定对Dart Embedding API的调用将通过链接到Dart可执行文件dart.exe来解决,当DLL被动态加载时。这个库文件是在构建dart时生成的,并且包含在Dart SDK中。

  1. 在Visual Studio 2008或2010中创建一个新的项目,类型为Win32/Win32项目。给该项目起一个与本机扩展名相同的名字。在向导的下一个屏幕上,改变应用程序类型为DLL,并选择 "空项目",然后选择完成。
  2. 将本机扩展的C/C++文件添加到项目的源文件文件夹中。确保包含[扩展名]_dllmain_win.cc文件。
  3. 在项目的属性中更改以下设置。
    • 配置属性/链接器/输入/附加依赖。添加dart-sdk\bin\dart.lib,来自下载的Dart SDK。
    • 配置属性/C/C++/通用/附加的包含目录。添加包含dart_api.h目录的路径,即下载的Dart SDK中的dart-sdk/include。
    • 配置属性/C/C++/预处理器/预处理器定义。添加DART_SHARED_LIB. 这只是为了从DLL中导出_init函数,因为它已经被声明为DART_EXPORT。
  4. 构建项目,并将DLL复制到正确的目录下,相对于扩展的Dart库部分。确保构建一个32位的DLL用于32位的SDK下载,以及一个64位的DLL用于64位的下载。

通过www.DeepL.com/Translator (免费版)翻译