Dart学习笔记(35):Dart VM本地扩展

运行在独立Dart VM(命令行应用)中的Dart程序能够利用本地扩展库,调用C或C++函数共享库。本文将讲解如何在Windows、Mac OS X、Linux中编写和编译本地扩展库。

在Dart中,本地扩展库分为两类:异步和同步。异步扩展是在一个独立的线程中运行本地函数,并通过Dart VM调度。同步扩展直接使用Dart VM库的C语言API,作为Dart isolate运行在相同的线程中。异步函数通过发送消息到Dart端口来调用,并在回复端口接收响应。

1、概括分析

Dart本地扩展包含两个文件:Dart库和本地库。通常,Dart库定义Class和顶层函数。但其中的一些函数是通过关键字native声明,然后在本地代码中实现。本地库是一个使用C或C++编写的共享库,包含函数的实现。

Dart库可以通过import声明和dart-ext:URI来指定本地库。与import一样,URI指定了共享库的路径,URI为绝对路径或相对路径。

2、简单的扩展示例

扩展功能的代码在Github仓库中,目录为 samples/sample_extension

上面的扩展示例调用了C标准库中的rand()和srand()函数,返回伪随机数到Dart程序。因为同步和异步本地扩展共享了大部分代码,所以此单一源文件(包括生成的共享库)实现了两种类型的扩展。但是两种类型的扩展都有单独的Dart库文件。额外的两个Dart文件是使用以及测试同步、异步扩展的示例。

扩展的共享库(本地代码)名称为sample_extension。sample_extension.cc是C++文件,包含供Dart调用的六个功能函数。

sample_extension_Init()
当扩展加载时被调用

ResolveName()
当本地函数通过给定的名称第一次被调用时被调用,将本地函数的Dart名称解析成一个C语言函数指针

SystemRand()和SystemSrand()
实现同步扩展。Dart直接调用该本地函数,并从C标准库中调用rand()srand()

wrappedRandomArray()和randomArrayServicePort()
实现异步扩展。randomArrayServicePort()中,wrappedRandomArray()创建一个本地端口并关联。当Dart发送消息到本地端口时,Dart VM会在一个单独的线程中调度 wrappedRandomArray()

在共享库中,一些代码用于设置以及初始化,并可用于其他所有的扩展。在所有的扩展中,函数sample_extension_Init()ResolveName() 应该一致。并且,randomArrayServicePort()的一个版本必须在所有异步扩展中。

3、同步类型本地扩展

由于异步扩展类似于具有一些附加功能的同步扩展,这里先说一下同步扩展。首先,说明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 VM运行代码 import ‘dart-ext:sample_extension’
  2. 虚拟机从包含Dart库的目录中加载共享库 sample_extension
  3. 共享库中的sample_extension_Init()函数被调用。它会注册共享库的ResolveName()函数,并作为sample_extension.dart库中所有本地函数的名称解析器。

注:共享库的文件名依赖于平台。Windows中,Dart VM加载sample_extension.dll。Linux中,加载libsample_extension.so。而在Mac中,它会加载libsample_extension.dylib。本文的末尾会说明如何编译和链接共享库。

4、在本地代码中使用Dart Embedding API

如之前示例所示,本地共享库包含一个初始化函数,一个命名解析函数,以及Dart部分扩展代码声明的函数的本地实现。其中,初始化函数注册本地命名解析函数,并负责查找库中本地函数的名称。当以native “function_name”为格式命名的函数在Dart中被调用时,本地命名解析函数以”function_name“为字符串参数被调用,并且在函数调用时附加上参数的个数。然后,命名解析函数返回该函数本地实现的函数指针。在Dart本地扩展中,初始化函数和命名解析函数看起来几乎一样。

在本地库中,函数使用Dart Embedding API与虚拟机进行通信,因此本地代码必须包含头文件”dart_api.h“。该头文件在SDK中的路径为dart-sdk/include/dart_api.h,在Git仓库中的路径为runtime/include/dart_api.h。Dart Embedding API是嵌入器的接口,用于Web浏览器嵌入的VM或独立的命令行VM。它包括100多个函数接口,许多的数据类型,以及数据结构体定义。在单元测试文件runtime/vm/dart_api_impl_test.cc中对这些功能进行了测试。

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

typedef void (*Dart_NativeFunction)(Dart_NativeArguments arguments);

可以看到,Dart_NativeFunction是一个函数指针,有一个Dart_NativeArguments对象作为参数,无返回值。参数对象是一个通过API访问的Dart对象,并且可以通过API函数返回参数的数量、以及返回指定索引处的参数的Dart_Handle。本地函数使用Dart_SetReturnValue() 函数将一个Dart对象存储到参数对象中,并将该Dart对象返回给Dart应用。

5、Dart Handle句柄

扩展的本地实现中大量的使用了Dart_Handle。Dart_Handle是一个不透明的、间接的指向Dart堆区中的对象的指针。并且它是按值传递,而非地址或引用。即使在垃圾回收阶段Dart对象离开堆区,这些句柄仍然是有效的。因此,本地代码必须使用句柄来存储堆区对象的引用。由于句柄需要资源来存储和维护,所以当不使用时必须释放。在句柄释放之前,VM的垃圾回收器并不会处理指向的对象,即使没有其他的句柄指向该对象。

Dart Embedding API会自动地创建一个新的Scope(作用域)来管理本地函数中的句柄的生存期。在进入本地函数的时候,局部句柄作用域被创建。当函数正常返回或以PropagateError退出时,作用域会被删除。大多句柄和内存指针是通过Dart Embedding API返回得到的,并且被分配在当前局部作用域中,在函数返回后将失效。如果扩展希望长期保持到Dart对象的指针有效,应该使用持久指针(Dart_NewPersistentHandle()Dart_NewWeakPersistentHandle()),它在局部作用域结束后仍然有效。

在调用Dart Embedding API时,可能在Dart_Handle返回值的时候返回错误信息。这些错误可能是异常,应该将它们以返回值的方式传递给上层调用者。

本地扩展中的大部分函数(函数类型为Dart_NativeFunction)并没有返回值,错误值必须以其他方式,向上传递给适当的处理者。Dart_PropagateError()的作用就是传递错误信息和控制流程(应该在何处处理错误)。在示例代码中,使用了辅助函数HandleError()

6、本地代码: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()函数被调用,带有一个Dart字符串。该字符串包含SystemRand和整数0。其中,SystemRandsystemRand()native关键字声明的原义字符串,而整数表示调用时参数的个数
  2. ResolveName()返回一个指向共享库中的本地函数SystemRand()的指针
  3. Dart中调用的systemRand()的参数被打包成Dart_NativeArguments对象,SystemRand()以该对象作为参数被调用
  4. SystemRand()运行计算,将值放入Dart_NativeArguments对象中,然后返回
  5. Dart VM从Dart_NativeArguments对象中提取返回值,并将它作为Dart调用systemRand()的返回值返回

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

7、异步类型本地扩展

由之前的代码可以看到,同步扩展使用Dart Embedding API来控制Dart堆区对象。并且对于当前isolate,它运行在Dart主线程中。另一方面,异步扩展并未使用大部分的Dart Embedding API,并且在单独的线程上运行,这样就不会阻塞Dart主线程。

很多时候,编写异步扩展比同步扩展更简单。异步扩展在独立的线程上,使用Dart Embedding API中的本地端口函数来调度C语言函数。在Dart中使用扩展的时候,看起来就像是简单的SendPort。当消息发送到该端口时,会被自动转换成C语言结构体Dart_CObject,包含intdoublechar*等C语言数据类型。然后,该结构体传递给C函数,它运行在单独的线程中,而线程来自VM管理的线程池。C函数可以通过Dart_CObject对回复端口进行响应。而Dart_CObject可以被转换成Dart对象树,并作为回复数据出现在Dart异步调用回复端口的时候。

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

  1. 封装需要调用的C语言函数,将Dart_CObject输入参数转换成想要的参数,并将结果转换成Dart_CObject回发到Dart
  2. 编写本地函数,创建本地端口并附加到被封装的函数中。这里的本地函数是同步函数
  3. 编写用于获取本地端口和存储的Dart类

7.1 封装C函数

下面是通过给定的随机种子和长度,创建随机字节数组的C函数(由于使用了reinterpret_cast,实际上是C++函数)例子。它将返回一个新分配的数组,并由封装器释放资源。

uint8_t* randomArray(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包含的seedlength,并将结果打包到Dart_CObject中。Dart_CObject可以保存的类型有intdoublestring,或Dart_CObject数组。这通过包含联合体的结构体被实现,详细的定义可以在dart_native_api.h头文件中查看。在Dart_CObject被发送后,它以及它的资源可以被释放,因为它们已经被复制为Dart堆区中的Dart对象。

void wrappedRandomArray(Dart_Port dest_port_id,
                        Dart_CObject* message) {
  Dart_Port reply_port_id = ILLEGAL_PORT;
  if (message->type == Dart_CObject_kArray &&
      3 == 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];
    Dart_CObject* param2 = message->value.as_array.values[2];
    if (param0->type == Dart_CObject_kInt32 &&
        param1->type == Dart_CObject_kInt32 &&
        param2->type == Dart_CObject_kSendPort) {
      int seed = param0->value.as_int32;
      int length = param1->value.as_int32;
      reply_port_id = param2->value.as_send_port.id;
      uint8_t* values = randomArray(seed, length);

      if (values != NULL) {
        Dart_CObject result;
        result.type = Dart_CObject_kTypedData;
        result.value.as_typed_data.type = Dart_TypedData_kUint8;
        result.value.as_typed_data.values = values;
        result.value.as_typed_data.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在这里调用是不合法的,因为它们并不在当前isolate中。对于错误或异常,必须编码到回复消息中,然后在扩展的Dart部分解码并抛出。

7.2 设置本地端口

现在,我们已经建立了一个从Dart部分,通过发送消息来调用该封装函数的方法。我们创建一个本地端口来调用该函数,并返回发送端口来连接本地端口。Dart库通过该函数得到端口,并将调用转发给端口。

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

7.3 在Dart中调用本地端口

在Dart端,我们需要一个类来存储发送端口,并在Dart异步函数的回调函数被调用时发送消息到该端口。Dart类在函数第一次被调用时获取端口,以普通方式保存。

library sample_asynchronous_extension;

import 'dart:async';
import 'dart:isolate';
import 'dart-ext:sample_extension';

// A class caches the native port used to call an asynchronous extension.
class RandomArray {
  static SendPort _port;

  Future<List<int> > randomArray(int seed, int length) {
    var completer = new Completer();
    var replyPort = new RawReceivePort();
    var args = new List(3);
    args[0] = seed;
    args[1] = length;
    args[2] = replyPort.sendPort;
    _servicePort.send(args);
    replyPort.handler = (result) {
      replyPort.close();
      if (result != null) {
        completer.complete(result);
      } else {
        completer.completeError(new Exception("Random array creation failed"));
      }
    };
    return completer.future;
  }

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

  SendPort _newServicePort() native "RandomArray_ServicePort";
}

就像之前说的,建议使用异步类型的扩展。因为异步扩展并不会阻塞Dart主线程,实现也更简单。Dart内置的I/O库就是以通过异步调用实现高吞吐量、非阻塞的目标来构建的,扩展应该具有相同的性能目标。

8、编译和链接扩展

构建共享库是件挺麻烦的事,因为构建的工具依赖于平台。而构建Dart本地扩展更麻烦,因为它们是动态加载的,它们链接的Dart Embedding API函数被嵌入可执行程序,并动态加载。

和所有的共享库一样,编译时必须生成与位置无关的代码。在库加载时,链接器必须指定可执行程序中将被解析的未解析函数。下面以Linux、Windows、Mac平台作说明。如果你下载了Dart源码,示例代码包含了一个跨平台的构建系统GYP,构建示例扩展的文件为sample_extension.gyp

8.1 Linux平台

在Linux中,可以在samples/sample_extension目录中编译代码如下:

g++ -fPIC -m32 -I{SDK中include目录的路径} -DDART_SHARED_LIB -c sample_extension.cc

从ojbect文件创建共享库:

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

当删除 -m32 参数时,默认构建64位共享库,以便运行在64位的独立Dart VM中。

8.2 Windows平台

基于Windows DLL编写麻烦的事实,我们需要链接库文件dart.lib。该文件自身并不包含代码,但DLL被动态加载时,调用的Dart Embedding API函数将通过链接Dart可执行程序dart.exe被解析。该库文件在构建Dart的时候被生成,并包含在Dart SDK中。

  1. 在VS2008或2010中创建一个新的Win32项目,项目名称和本地扩展名称相同。在下一步设置向导中,修改应用类型为DLL,附加选项选择空项目,完成。
  2. 将本地扩展的C/C++文件添加到项目的源文件文件夹中,确保包含文件[扩展名]_dllmain_win.cc
  3. 更改项目属性的下列设置:
    1. 配置依赖:链接器 / 输入 / 附加依赖项,添加 dart-sdk\bin\dart.lib
    2. 配置头文件目录:C/C++ / 常规 / 附加包含目录,添加dart_api.h的目录dart-sdk/include
    3. 配置预处理:C/C++ / 预处理器 / 预处理器定义,添加DART_SHARED_LIB,表示仅从DLL中导出 _init函数,这之前被声明为DART_EXPORT
  4. 构建项目。确保32位的DLL使用32位的Dart SDK,64位的DLL使用64位的Dart SDK。

8.3 Mac平台

  1. Using Xcode (tested with Xcode 3.2.6), create a new project with the same name as the native extension, of type Framework & Library/BSD C Library, type dynamic.
  2. Add the source files of your extension to the source section of the project.
  3. Make the following changes in Project/Edit Project Settings, choosing the Build tab and All Configurations in the dialog box:
    1. In section Linking, line Other Linker Flags, add -undefined dynamic_lookup.
    2. In section Search Paths, line Header Search Paths, add the path to dart_api.h in the SDK download or the Dart repository checkout.
    3. In section Preprocessing, line Preprocessor Macros, add DART_SHARED_LIB=1
  4. Choose the correct architecture (i386 or x86_64), and build by choosing Build/Build.

The resulting lib[extension_name].dylib will be in the build/ subdirectory of your project location, so copy it to the desired location (probably the location of the Dart library part of the extension).

sample_extension源码: sample_extension.zip

本文出自“Dart语言中文社区”,允许转载,转载时请务必以超链接形式标明文章原始出处
本文地址:
http://www.cndartlang.com/935.html

发表评论

登录后才能评论