Compile Dart to Wasm and call the Wasm module from Dart

Original address: medium.com/dartlang/ex…

Author: medium.com/@mit.mit

Published: July 28, 2021-8 minutes reading

WebAssembly (often abbreviated to Wasm) is “a stack-based virtual machine binary instruction format.” Although originally designed for running native code over a network, Wasm has since evolved into a general-purpose technique for running compiled code on multiple platforms. Dart is already a highly portable and multi-platform language, so we’re interested in how Wasm allows us to extend these qualities of Dart.

Why do experiments with Wasm?

Wasm is already widely supported by browser vendors such as Chrome, Edge, Firefox and WebKit. This makes the prospect of Wasm running binary code in a browser very interesting. However, initially Wasm was not designed for programming languages with garbage collection (GC), such as Dart and Java/Kotlin, making it difficult to effectively compile GC-based languages into Wasm. By participating in the recent GC proposal for the Wasm project, we hope to both provide technical feedback on the proposal and learn more about the benefits we might reap from running dart-based web applications with Wasm code.

A second feature of Wasm is that binary Wasm modules are platform-independent. This has the potential to make interoperability with existing code more practical: If existing code can be compiled to Wasm, Dart applications on all platforms can rely on a single, shared binary Wasm module.

In the remainder of this article, we will discuss our experiments with both Wasm and Dart.

  1. Compilation of Dart to Wasm. Extend our AOT compiler to support compilation of Dart source code into Wasm binaries (Question 32894).
  2. Dart to Wasm Interop: Supports calls from Dart code to compiled Wasm modules (issues 37355 and 37882).

Illustrate two potential uses of Wasm and Dart

Compile Dart into Wasm

As mentioned earlier, Wasm originated as a way to run native code over a network. Traditionally, networks are driven by JavaScript code that runs in a virtual machine (VM) and is compiled into native code in a just-in-time (JIT) manner while network applications run. In current Web-based Dart frameworks, such as Flutter Web, Dart application code is compiled into optimized JavaScript for deployment, which is then compiled into native code by the Web platform JIT while the application is running.

We’re looking at compiling the Dart code directly into Wasm native code to see if we can get a more direct path to running native code over the network. The Wasm assembly format is low level, closer to the abstraction level of machine code than JavaScript, which results in improved startup times and, in general, more predictable efficiency.

Dart’s support for Wasm compilation is an early stage investigation, and the compiler is incomplete, but we’re trying to learn. We’ve always been interested in Wasm as a compilation target for Dart, but its raw form is not ideal for languages with garbage collection. Wasm lacks built-in support for garbage collection, so languages like Dart must include implementations of garbage collection in the compiled Wasm module. An implementation that includes GC would be very complex, increase the size of the compiled Wasm code, affect startup time, and do not interoperate well at the object level with the rest of the browser system.

Fortunately, an ongoing effort by the WebAssembly community, the Wasm GC, is exploring the possibility of expanding Wasm to provide direct and effective support for garbage collection languages. Given our longstanding interest in Wasm, we saw an opportunity to engage the community and provide a real-world experience by writing a compiler to translate Dart into the Wasm GC.

It’s too early to predict where this might lead us, but our initial prototype design has shown very positive results, with initial benchmarks showing faster first frame times and faster average frame time/throughput. If you’re interested in learning more about this project, take a look at the wasm_Prototype source code.

Interoperability with Wasm code (Package: Wasm)

In addition to compiling into Wasm, we are also interested in investigating whether Wasm could be used to integrate with existing code in a more cross-platform manner. Several languages support compiling into modules that follow C’s calling conventions, and with Dart FFI, you can interoperate with those modules. Dart FFI can be a good way to leverage existing source code and libraries, rather than re-implementing code in Dart.

However, because C modules are platform specific, distributing a shared package with native C modules is complex: it requires a common build system, or distributing multiple binary modules (one for each required platform). Distribution would be much easier if a single Wasm binary assembly format were available on all platforms. So, instead of compiling your library into platplate-specific binaries for each target platform, you can compile it into a Wasm binary module and run it anywhere. This will potentially open the door for easy distribution of packages containing native code on pub.dev.

We are trying to support Wasm interoperability in a new package, Package: Wasm. This prototype is built on top of the Wasmer runtime, which supports WASI’s operating system interactions. Please note that our current prototype is incomplete and only supports desktop platforms (Windows, Linux and macOS).

Example. Call the Brotli compression library

Let’s look at an example of using package:wasm to take advantage of the Brotli compression library, which is compiled into a WASM module. In this example, we’ll read an input file, compress it, report its compression rate, then uncompress it, and verify the input we get. See GitHub repo for the complete sample source code. Because Package: WASM is built on top of DART: FFI, you may find these steps familiar if you have ffI experience.

There are several ways to compile C code into Wasm, but in this case we use Wasienv. Complete details can be found in the README.

For this example, we will try to call these Brotli functions to compress and decompress the data.

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);
Copy the code

The quality, LGWIN, and mode parameters are adjustment parameters for the encoder. The details are irrelevant for this example, so we will just use the default values for these parameters. Another thing to note is that output_size is an input/output parameter. When we call these functions, output_size must be initialized to the size of the output_buffer we allocate, which will then be set to the number of buffers actually used.

The first step is to build a WasmModule object using the Wasm binary we compiled. The binary data should be a Uint8List which we can get by reading it from a file using file.readasbytessync ().

varBrotliPath = Platform. The script. Resolve (' libbrotli. Wasm ');var moduleData = File(brotliPath.path).readAsBytesSync();
var module = WasmModule(moduleData);
Copy the code

A very useful debugging tool is module.describe(), which ensures that our Wasm module has the API we expect. This will return a string listing all the entries and exits of the module.

print(module.describe());
Copy the code

For our Brotli library, this is the output.

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)...Copy the code

As you can see, the module imports some WASI functions and exports its memory and a bunch of Brotli functions. The two functions we are interested in are derived, but their signatures look a little different. This is because Wasm supports only 32 – and 64-bit Ints and floats. The pointer has been changed to an INT32 index and entered the exported memory.

The next step is to instantiate the module. During instantiation, we must populate each import that the module expects. Instantiation uses the builder pattern (module.instantiate().Initialization… The build ()). Our library only imports WASI functions, so we can call enableWasi() directly.

var instance = module.instantiate().enableWasi().build();
Copy the code

If we have additional non-WASI functions to import, we can use addFunction() to import a Dart function into the WASM library. Now that we have a WasmInstance, we can query any of its exported functions or check its memory.

var memory = instance.memory;
varCompress = instance. LookupFunction (" BrotliEncoderCompress ");varDecompress = instance. LookupFunction (" BrotliDecoderDecompress ");Copy the code

The next thing we need to do is to use the compression and decompression functions on our input file. But we can’t pass data directly to these functions. The C function takes the Uint8_T data Pointers, but in the Wasm code these Pointers become the Int32 index of the instance memory. Brotli also reports the size of the compressed and decompressed data using size_t Pointers, which also become int32.

Therefore, in order to pass our data to the function, we must copy it into the memory of the instance and pass its index to the function. We need memory for five areas: input data, compressed data, compression size, decompressed data and decompressed size. For simplicity, we’re just going to grab some unused memory areas, but you can also export malloc() and free() in your library.

To ensure that we put data in unused memory, we will grow instance memory and use new areas to store our data.

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;
Copy the code

Our memory area looks like this.

[initial instance memory][input][output][output size][decoded][decoded size]
Copy the code

Next, we load the input data in memory and call our compression function.

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

The rest of the example works the same way. This is the result.

Loading lipsum. TXT Input size: 3210 bytes Compressing... Compression Status: 1 Compressed size: 1198 bytes Space Saving: 62.68% Decompressing... Decompression status: 1 Decompression status... Decompression succeeded :)Copy the code

Try to package: wasm

If you’re interested in trying out Wasm interoperability, check out the README instructions for Package: Wasm.

The roadmap

Wasm compilation and Wasm interoperability are experimental. If these experiments prove to be effective, we plan to continue developing them and eventually produce them into stable, supported versions. However, if we learn that something is not working as expected, or see a lack of interest, we will stop the experiment.

We do these experiments to learn, and there are two main parts. First, we wanted to understand the feasibility of technically supporting Wasm and what the features of such support might be. Can it make Dart code faster, smaller, or more predictable? Second, we are interested in exploring what new technical capabilities Wasm might unleash, and what new use cases these might lead to for Dart developers. Can we make interoperating with native code more portable?

How do you think Wasm fits your needs? What do you think you would do with it? We’d love to hear your thoughts. Please let us know in the Dart MISc discussion group.


www.deepl.com