Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Porting DRuntime to WebAssembly

Porting Druntime to WebAssembly

WebAssembly

WebAssembly is born out of asm.js, which was a backend for llvm that compiles C/C++ to JavaScript. As a consequence WebAssembly has many of its features in line with C, and consequently with D as well.

The MVP version of WebAssembly is widely supported across browsers. Work continues to implement additional features, see the proposal tracking page.

Approach

The port will adhere to the limitations of WebAssembly. Features unavailable in WebAssembly will be unavailable in this port. This is in contrast with something like emscripten where missing functionality is often emulated. Most of the missing features in WebAssembly are in the proposal phase and are expected to be available in due time.

Another important goal is to keep the changes in druntime to a minimum. This port will not reimplement anything unless absolutely necessary.

This approach is to ensure the port can be completed in a reasonable timeframe, without going into myraid tangents. The goal is to quickly enable developers to compile D applications to WebAssembly. There are plenty of things that can be improved incrementally afterwards, preferably in such a way to also benefit other targets.

Again, the most important thing is to enable developers to use D to compile to WebAssembly. Things like binary size, while important, are second to that.

Compilers

DMD and GDC don't have a WebAssembly backend, only LDC can be used to compile to WebAssembly. None of the required changes are compiler specific and if any of the other compilers got a WebAssembly backend in the future, it would be able to use this port as well.

Libc dependency

WebAssembly itself has no syscalls, and the only intrinsics it provides are to query and grow its linear memory. This poses a problem because druntime is build on top of libc.

While it is certainly possible to reduce the dependency on libc and to reimplement some of its functions, that goes against the approach outlined above. For that reason this port will use WASI-libc to provide an implementation of libc that conforms to the WASI api.

WASI is the official WebAssembly System Interface and provides a POSIX like API. Read more about it on wasi.dev.

Adding WASI-libc requires minimal change, and it keeps the option open for incremental improvements by replacing current libc calls with implementations in pure D or with direct calls to WASI.

The added benefit is that it allows D code to run inside environments where WASI is already supported. E.g.:

This does mean that WebAssembly programs compiled with druntime will depend on some of WASI. In environments like the browser this means that these calls need to be implemented in JavaScript. Since WASI is standarized there are already JavaScript implementation available (see WasmerJS for instance). Another option would be to implement our own. This is a small task since there are only a few WASI calls druntime really depends on:

  • wasi_clock_res_get
  • wasi_clock_time_get
  • wasi_proc_exit
  • wasi_fd_close
  • wasi_fd_fdstat_get
  • wasi_fd_seek
  • wasi_fd_write

These correspond to quering the clock, exiting the process and writing to stdout/err (core druntime doesn't open files).

In short, using WASI-libc makes porting druntime a lot easier because it doens't require refactoring druntime to avoid libc nor does it require reimplementing functions like snprintf, malloc/free, etc.

Language Features

The next section describes what language features are going to be supported.

GC

Since the GC is the foundation of many language features it needs to be fully supported.

WebAssembly has a single data section where both immutable and mutable globals are stored, as well as a downward growing stack. It uses a 32-bit address space and it always starts from 0, both of which increases the chance of a false pointer. WebAssembly as such is not super favorable to a conservative GC, but it is assumed WebAssembly programs use relatively little memory, offsetting this problem. I am unaware of how much the current GC is precise, but that would offset the problem as well. Since this port uses the same GC, any improvements there would spill over to this port.

The only unknown part is how to dump the registers to the stack to ensure no pointers are held in the registers only.

With the GC the following language features are supported:

  • Dynamic arrays (as well as appending and concatenation)
  • Associative arrays (as well as insertion, removal or lookups)
  • NewExpression
  • Taking the address of (i.e. making a delegate to) a nested function that accesses variables in an outer scope
  • A function literal that accesses variables in an outer scope

Assembly

Assembly won't be supported. Inline LLVM IR most likely will be.

SIMD

WebAssembly support for SIMD is still in proposal phase and won't be supported in this port until after it is accepted. It is mostly waiting for it to be available in LLVM and having browsers support it.

Threads

Threads, thread-local storage, mutexes and atomics won't be supported. There is a Thread Proposal and until that is accepted they are out of scope of this initial port.

Exceptions

Exceptions can be thrown but not catched. A thrown exception will terminate the program. Exceptions are still in the proposal phase. When the proposal is accepted exceptions can be fully supported.

Emscripten does support exceptions, but it is disabled by default and comes with a substantial cost. Essentially what emscripten does is jump to JavaScript before and after every function invocation to setup and teardown try and catch handlers. Whenever a exception needs to be thrown it jumps to JavaScript again to throw the exception there. (Although there is a way to whitelist functions where this should happen, avoiding the extra cost for functions which are nothrow. However, this list needs to be maintained.)

Unit tests

Will be supported. But due to limited support for exceptions, the unittest runner will stop after the first assertion fails.

The only problem I forsee is that web apis are sometimes asynchronous and you cannot wait for them for completion in a unittest since that will block the main thread. Either D would need asynchronous unittests or some other mechanism needs to be developed for them. Possibly a library solution.

Reals

There are function signature mismatches whenever reals are used. WASI-libc's ABI uses 2 i64's for a real parameter and uses a i32 (pointer) in the first parameter whenever a function returns a real, while LDC simply emits a single f64 in both cases. I haven't investigated it properly but it likely can be be fixed in LDC.

Debugging

Browsers have used sourcemaps for mapping transpiled and minified JavaScript to its original source. I made a cli tool called wasm-sourcemaps that can convert DWARF debug info into sourcemaps suitable for browser's consumption. This allows the user to see the original D source code and set breakpoints. Ultimately however, sourcemaps cannot provide a full debugging experience. This is because sourcemaps don't contain type information and as a result only the WebAssembly values can be inspected.

Just a couple of days ago the Chrome DevTools team announced support for DWARF debugging. Since D already has full DWARF debugging information nothing else needs to be done.

Misc

Some miscellaneous items that are supported.

  • TypeInfo and ModuleInfo
  • Static module constructors and destructors
  • Probably others I have missed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.