[Dart翻译]用Dart和Wasm做实验

1,917 阅读10分钟

将Dart编译为Wasm,并从Dart调用Wasm模块

原文地址:medium.com/dartlang/ex…

原文作者:medium.com/@mit.mit

发布时间:2021年7月28日 - 8分钟阅读

WebAssembly(通常缩写为Wasm)是 "一种基于堆栈的虚拟机的二进制指令格式"。虽然Wasm最初是为在网络上运行本地代码而设计的,但后来Wasm已经发展成为一种在多个平台上运行编译代码的通用技术。Dart已经是一个高度可移植和多平台的语言,所以我们对Wasm如何使我们扩展Dart的这些品质非常感兴趣。

为什么要用Wasm做实验?

Wasm已经获得了浏览器供应商的广泛支持,如Chrome, Edge, Firefox和WebKit。这使得Wasm在浏览器中运行二进制代码的前景非常有趣。然而,最初Wasm并不是为具有垃圾收集(GC)的编程语言而设计的,如Dart和Java/Kotlin,这使得有效地将基于GC的语言编译到Wasm中很困难。通过参与Wasm项目最近的GC提案,我们希望既能对提案提供技术反馈,又能更多地了解我们通过Wasm代码运行基于Dart的网络应用可能获得的收益。

Wasm的第二个特点是,二进制Wasm模块是独立于平台的。这有可能使与现有代码的互操作性更加实用:如果现有的代码可以被编译为Wasm,那么所有平台的Dart应用程序可以依赖于一个单一的,共享的二进制Wasm模块。

在这篇文章的其余部分,我们将讨论我们对Wasm和Dart两种形式的实验。

  1. Dart到Wasm的编译。扩展我们的AOT编译器,支持将Dart源代码编译为Wasm二进制代码(问题32894)。
  2. Dart to Wasm interop: 支持从Dart代码到编译的Wasm模块的调用(问题3735537882)。

image.png

说明Wasm与Dart的两种潜在用途

编译Dart到Wasm

如前所述,Wasm起源于一种在网络上运行本地代码的方式。传统上,网络是由JavaScript代码驱动的,这些代码在虚拟机(VM)中运行,在网络应用程序运行的同时,对JavaScript代码进行及时(JIT)编译为本地代码。在目前针对网络的Dart框架中,如Flutter web,Dart应用代码被编译为优化的JavaScript,用于部署,然后这些JavaScript在应用运行时被网络平台JIT编译为本地代码。

我们正在研究将Dart代码直接编译为Wasm原生代码,看看我们是否可以获得一个更直接的路径来在网络上运行原生代码。Wasm汇编格式是低层次的,比JavaScript更接近机器代码的抽象水平,这导致了启动时间的改善,一般来说,效率更可预测。

Dart对Wasm编译的支持是一个早期阶段的调查,编译器也不完整,但我们正在尝试学习。我们一直对Wasm作为Dart的编译目标感兴趣,但它的原始形式对于有垃圾收集的语言来说并不理想。Wasm缺乏内置的垃圾收集支持,所以像Dart这样的语言必须将垃圾收集的实现包含在编译的Wasm模块中。包括GC的实现将是非常复杂的,会增加编译后的Wasm代码的大小,影响启动时间,并且不能很好地与浏览器系统的其他部分进行对象级互操作。

幸运的是,WebAssembly社区正在进行的一项努力,即Wasm GC,正在探索扩大Wasm的可能性,对垃圾收集语言提供直接和有效的支持。鉴于我们对Wasm的长期兴趣,我们看到了一个机会,通过编写一个编译器将Dart翻译成Wasm GC,来参与社区并提供真实世界的经验。

现在预测这可能会给我们带来什么还为时过早,但我们最初的原型设计显示了非常积极的结果,最初的基准测试显示了更快的第一帧时间和更快的平均帧时间/吞吐量。如果你有兴趣了解更多关于这个项目的信息,请看一下wasm_prototype的源代码。

与Wasm代码的互操作性(package:wasm)

除了编译到Wasm,我们也有兴趣调查Wasm是否可以用来与现有的代码整合,以一种更跨平台的方式。有几种语言支持编译到遵循C语言调用惯例的模块,有了Dart FFI,你就可以与这些模块进行互操作。Dart FFI可以成为利用现有源代码和库的一个好方法,而不是在Dart中重新实现代码。

然而,由于C模块是特定于平台的,分发带有本地C模块的共享包是很复杂的:它需要一个通用的构建系统,或者分发多个二进制模块(每个所需平台一个)。如果一个单一的Wasm二进制汇编格式可以在所有的平台上使用,分发就会容易得多。那么,与其为每个目标平台将你的库编译成特定平台的二进制代码,你可以将它编译成一个Wasm二进制模块并在任何地方运行。这将有可能为在pub.dev上轻松发布包含本地代码的软件包打开大门。

我们正在尝试在一个新的软件包中支持Wasm互操作,即package:wasm。这个原型是建立在Wasmer运行时上的,它支持WASI的操作系统交互。请注意,我们目前的原型是不完整的,只支持桌面平台(Windows、Linux和macOS)。

例子。调用Brotli压缩库

让我们看看一个使用package:wasm来利用Brotli压缩库的例子,它被编译成一个Wasm模块。在这个例子中,我们将读取一个输入文件,对其进行压缩,报告其压缩率,然后对其进行解压,并验证我们得到的输入。请参阅GitHub repo获取完整的示例源代码。因为package:wasm是建立在dart:ffi之上的,如果你有FFI的经验,你可能会发现这些步骤很熟悉。

有几种将C语言代码编译到Wasm的方法,但在本例中我们使用wasienv。完整的细节可在README中找到。

对于这个例子,我们将尝试调用这些Brotli函数来压缩和解压数据。

int BrotliEncoderCompress(
  int quality, int lgwin, int mode, size_t input_size,
  const uint8_t* input_buffer, size_t* output_size,
  uint8_t* output_buffer);
int BrotliDecoderDecompress(
  size_t encoded_size, const uint8_t* encoded_buffer,
  size_t* output_size, uint8_t* output_buffer);

质量、lgwin和模式参数是编码器的调整参数。细节与本例无关,所以我们只使用这些参数的默认值。另一件要注意的事情是,output_size是一个输入输出参数。当我们调用这些函数时,output_size必须被初始化为我们分配的output_buffer的大小,之后它将被设置为实际使用的缓冲区的数量。

第一步是使用我们编译的Wasm二进制文件来构建一个WasmModule对象。二进制数据应该是一个Uint8List,我们可以通过使用file.readAsBytesSync()从文件中读取它来获得。

var brotliPath = Platform.script.resolve(‘libbrotli.wasm’);
var moduleData = File(brotliPath.path).readAsBytesSync();
var module = WasmModule(moduleData);

一个非常有用的调试工具是module.describe(),它可以确保我们的Wasm模块具有我们期望的API。这将返回一个字符串,列出模块的所有进口和出口。

print(module.describe());

对于我们的Brotli库,这就是输出。

import function: int32 wasi_unstable::fd_close(int32)
import function: int32 wasi_unstable::fd_write(int32, int32, int32, int32)
import function: int32 wasi_unstable::fd_fdstat_get(int32, int32)
import function: int32 wasi_unstable::fd_seek(int32, int64, int32, int32)
import function: void wasi_unstable::proc_exit(int32)
export memory: memory
export function: int32 BrotliDecoderSetParameter(int32, int32, int32)
export function: int32 BrotliDecoderCreateInstance(int32, int32, int32)
export function: void BrotliDecoderDestroyInstance(int32)
export function: int32 BrotliDecoderDecompress(int32, int32, int32, int32)
…
export function: int32 BrotliEncoderSetParameter(int32, int32, int32)
export function: int32 BrotliEncoderCreateInstance(int32, int32, int32)
export function: void BrotliEncoderDestroyInstance(int32)
export function: int32 BrotliEncoderMaxCompressedSize(int32)
export function: int32 BrotliEncoderCompress(int32, int32, int32, int32, int32, int32, int32)

我们可以看到,该模块导入了一些WASI函数,并导出了它的内存和一堆Brotli函数。我们感兴趣的两个函数是导出的,但它们的签名看起来有点不同。这是因为Wasm只支持32位和64位的ints和floats。指针已经变成int32索引,进入导出的内存。

下一步是对模块进行实例化。在实例化过程中,我们必须填充模块所期望的每一个导入。实例化使用了构建器模式(module.instantiate(). initialization... .build())。我们的库只导入WASI函数,所以我们可以直接调用enableWasi()。

var instance = module.instantiate().enableWasi().build();

如果我们有额外的非WASI函数导入,我们可以使用addFunction()将一个Dart函数导入wasm库中。 现在我们有了一个WasmInstance,我们可以查询它的任何一个导出的函数,或者检查它的内存。

var memory = instance.memory;
var compress = instance.lookupFunction(“BrotliEncoderCompress”);
var decompress = instance.lookupFunction(“BrotliDecoderDecompress”);

接下来我们要做的是对我们的输入文件使用压缩和解压函数。但是我们不能直接将数据传递给这些函数。C函数采取的是uint8_t的数据指针,但在Wasm代码中,这些指针成为实例内存的int32索引。Brotli还使用size_t指针报告压缩和解压缩数据的大小,这些指针也变成了int32。

因此,为了将我们的数据传递给函数,我们必须将其复制到实例的内存中,并将其索引传递给函数。我们需要5个区域的内存:输入数据、压缩数据、压缩大小、解压缩数据和解压缩大小。为了简单起见,我们只是要抓取一些未使用的内存区域,但你也可以在你的库中导出malloc()和free()。

为了确保我们把数据放在未使用的内存中,我们要增长实例内存,并使用新的区域来存放我们的数据。

var inputPtr = memory.lengthInBytes;
memory.grow((3 * inputData.length /
    WasmMemory.kPageSizeInBytes).ceil());
var memoryView = memory.view;
var outputPtr = inputPtr + inputData.length;
var outSizePtr = outputPtr + inputData.length;
var decodedPtr = outSizePtr + 4;
var decSizePtr = decodedPtr + inputData.length;

我们的内存区域看起来像这样。

[initial instance memory][input][output][output size][decoded][decoded size]

接下来,我们在内存中加载输入数据,并调用我们的压缩函数。

memoryView.setRange(
    inputPtr, inputPtr + inputData.length, inputData);
var status = compress(kDefaultQuality, kDefaultWindow, kDefaultMode,
    inputData.length, inputPtr, outSizePtr, outputPtr);

这个例子的其余部分也是这样工作的。这就是结果。

Loading lipsum.txt
Input size: 3210 bytes
Compressing…
Compression status: 1
Compressed size: 1198 bytes
Space saving: 62.68%
Decompressing…
Decompression status: 1
Decompressed size: 3210 bytes
Verifying decompression…
Decompression succeeded :)

尝试package:wasm

如果你对尝试Wasm互操作感兴趣,请查看package:wasm的README说明。

路线图

Wasm编译和Wasm互操作都是实验性的。如果这些实验被证明是有成效的,我们计划继续开发它们,并最终将其产品化为稳定的、支持的版本。然而,如果我们了解到有些东西不能按预期工作,或看到缺乏兴趣,我们将停止实验。

我们做这些实验是为了学习,有两个主要部分。首先,我们想了解在技术上支持Wasm的可行性,以及这种支持的特点可能是什么。它可以使Dart代码更快,更小,或更可预测吗?其次,我们有兴趣探索Wasm可能释放哪些新的技术能力,以及这些可能为Dart开发者带来哪些新的用例。我们可以使与本地代码的互操作更加便携吗?

您认为Wasm如何适用于您的需求?你认为你会用它来做什么?我们很想听听您的想法。请让我们知道在Dart misc讨论组


www.deepl.com