Application Development

WebAssembly: Using WASM / WASI for System Plugins

WebAssembly is a new way to run code on the web. With huge tech companies behind it, it’s poised to revolutionise the way we write web applications, but comes with its own quirks and limitations. Are WASM frameworks a viable competitor to JavaScript libraries like React?

What Is WebAssembly?

WebAssembly, or WASM, is the second universal programming language that all web browsers can understand and run. However, you’re not going to be writing scripts in WebAssembly yourself—it’s a low level assembly language, designed to be very close to compiled machine code, and very close to native performance.

The magic of WebAssembly is that it’s low level enough that it actually is an easy compilation target.

What this means in practice is that JavaScript is no longer the only language you can run on the web. Web browsers can run any language now, if that language has a WebAssembly compiler.

WASM currently runs in 94% of user browsers, with IE, UC browser, and Opera Mini support being the main things holding it back, as per usual. However, it’s backed by developers from Mozilla, Microsoft, Google, and Apple, and its support in modern browsers is fast-moving.

Like most web standards, it’s currently managed by the W3C standards organisation. ​

Using WASI to extend running systems as plugins

We are currently looking at different ways to provide extensions to applications. In the past, we’ve done this type of functionality with a scripting language, like Lua or JavaScript. ​

WASM (Web Assembly) is becoming a popular way to build web applications or parts of web applications. WASM lets you compile a large number of programming languages to a binary format that can be loaded and executed by web browsers. Here is partial list of some of the languages that currently compile to WASM. WASM performs quite well, and using WASM frees teams from having to write web applications in JavaScript. ​

On the server side, the popularity of WASM has led to the development of WASI – The Web Assembly System Interface. WASI is similar to WASM, but it is meant to be run on the server instead of in a browser. ​

WASI could, in the long run, replace docker containers using a run time like Wasmtime. However, we are currently interested in seeing if we can use WASI to extend running systems as plugins today. ​

Using WASI as a plugin system could have several benefits:

  • Provide system integration using whatever language a team is comfortable with
  • Help with migrating legacy applications to newer architectures (for example, [Cobol][cobweb] systems)
  • Text editor plugins based on the plugin language not the editor’s language
  • Running “memory unsafe” applications in a safe environment
  • Reuse of already existing code in new ways and composable ways ​

The Spike

The source code for this post can be found here.

For this spike we used golang as the host application, and the C and Rust language as the plugin language. The goal was to compile both C and Rust into WASM, and have each of them provide a single function called `sum`. The Go host application would then load the WASM files and then call `sum` function with some inputs. Seems pretty simple. ​

Rust to WASI

For Rust, it was simply a matter of telling the compiler to not mangle the function name: ​

#[no_mangle]
pub extern "C" fn sum(x:i32, y:i32) -> i32
{
return x + y;
}

And to compile to WASI ​

rustup target add wasm32-wasi
rustc $(ENTRY).rs --target wasm32-wasi

And that was all it took. ​

C to WASI

C was a bit more difficult. We used the Clang compiler, and in order to compile to WASI, C needs the headers and standard library to be within the WASI universe. ​

It’s a bit convoluted to describe here, but have a look at the C Makefile to see the setup. The tasks `download_sysroot` and `download_builtins` show where the downloads are coming from. ​

Once the system root and built-ins are setup, it’s just a matter of letting C know where they are and then compiling: ​

ENTRY=main
clang \
-v -nodefaultlibs \
-Wl,--no-entry \
-Wl,--export-dynamic \
-O2 -s \
-DNDEBUG \
--target=wasm32-wasi \
--sysroot=$(PWD)/wasi-sysroot -lc \
-o $(ENTRY).wasm \
$(PWD)/lib/wasi/libclang_rt.builtins-wasm32.a \
$(ENTRY).c

Import Functions

With the WASM plugins create, it’s now a matter of import them into the host system. This was the first major bump we came upon. ​

The web assembly module expects to be running in a particular environment – for example the browser. Because of this, it expects some function to be injected when it starts. For example, when your WASM called `print()` the module expects the host to inject some kind of parent function it can call. However, for this spike (and maybe in general), we don’t want to support these functions. Additionally, Rust and C both require different functions to be injected. For example a C WASI expects: ​

wat
...
(import "wasi_snapshot_preview1" "proc_exit" (func (;0;) (type 0)))
...

and Rust WASI expects: ​

wat
...
(import "wasi_snapshot_preview1" "fd_write" (func $_ZN4wasi13lib_generated22wasi_snapshot_preview18fd_write17h7ef83300274a0defE (type 8)))
(import "wasi_snapshot_preview1" "environ_get" (func $__imported_wasi_snapshot_preview1_environ_get (type 7)))
(import "wasi_snapshot_preview1" "environ_sizes_get" (func $__imported_wasi_snapshot_preview1_environ_sizes_get (type 7)))
(import "wasi_snapshot_preview1" "proc_exit" (func $__imported_wasi_snapshot_preview1_proc_exit (type 1)))
...

We spent a lot of time trying to discover how to turn those off, but there does not seem to be a way to do it currently. As you can see, they also have a `type` reference at the end of the import. That corresponds to a lookup table where the actual function definition is located. ​

To pass this limitation – to hack it essentially – we create a bunch of dummy functions when we load the WASM. ​

func makeImportMap(store *wasmtime.Store) map[string]*wasmtime.Func {
var imports = map[string]*wasmtime.Func{
"fd_write": wasmtime.WrapFunc(store, func(int32, int32, int32, int32) int32 { return 0 }),
"environ_get": wasmtime.WrapFunc(store, func(int32, int32) int32 { return 0 }),
"environ_sizes_get": wasmtime.WrapFunc(store, func(int32, int32) int32 { return 0 }),
"proc_exit": wasmtime.WrapFunc(store, func(int32) {}),
}
return imports
}

We inspect the WASM module before we instantiate it and feed it a dummy function for whatever system function it is looking for. In it’s current form, this is not a production ready solution, but depending on what the standard WASI required functions wind up being, and what you need a plugin system to do, this might not be a totally bonkers approach. ​

Results

Once everything is compiled, running the application loads the WASM files and runs them: ​

go run main.go
sum from plugins/main-c99.wasm module 3 + 4 = 7
sum from plugins/main-rust.wasm module 3 + 4 = 7

What does the future hold?

WebAssembly is only a few years old. It still has plenty of room to grow, and is still picking up speed. It’s not unreasonable that a few years from now, frameworks like Blazor and Yew will be just as common as React, Angular, and Vue.

This could be argued to be fragmenting the web ecosystem, but WASM is cross platform. WAPM, a WASM package manager, may become the go-to way to share libraries between frameworks of different languages.

In any case, web frameworks running on WebAssembly have huge potential and we’re confident that they’re the future of the web. ​

Notes and Links
​ [wasm]: https://webassembly.org/
[wasi]: https://wasi.dev/
[wasmtime]: https://github.com/bytecodealliance/wasmtime
[cobweb]: https://github.com/cloudflare/cobweb
[golang]: https://golang.org
[code]: https://github.com/LogicalCube/spike-wasi-chain

What is WebAssembly?

WebAssembly (often abbreviated as Wasm) is a binary instruction format that allows running code efficiently on the web. It is designed as a portable compilation target for programming languages, enabling high-performance applications to be developed for the web.

How does WebAssembly differ from JavaScript?

WebAssembly and JavaScript are complementary technologies. While JavaScript is a high-level programming language, WebAssembly is a low-level binary format. WebAssembly provides a more efficient execution environment and allows running code written in languages other than JavaScript, while JavaScript remains crucial for web interactivity and high-level application logic.

What are the benefits of using WebAssembly?

WebAssembly offers several benefits, including improved performance, portability, and security. It allows running code at near-native speed, supports multiple programming languages, and provides a sandboxed environment that ensures safety and prevents malicious activities.

Which programming languages can be compiled to WebAssembly?

WebAssembly is designed to be language-agnostic, which means it can be compiled from various programming languages. Some popular languages with WebAssembly support include C/C++, Rust, AssemblyScript, and Go. Additionally, there are tools and frameworks available to compile languages like Python, Java, and TypeScript to WebAssembly.

How can I use WebAssembly in my web applications?

To use WebAssembly in web applications, you need to compile your code into WebAssembly format using the appropriate compiler for your programming language. Once you have the WebAssembly module (a .wasm file), you can load it in your JavaScript code and interact with it using WebAssembly APIs.

Can WebAssembly interact with JavaScript code?

Yes, WebAssembly can interact with JavaScript code. It provides a two-way communication mechanism known as the WebAssembly JavaScript Interface (JS API). Through this interface, WebAssembly modules can import and export functions, access browser APIs, and exchange data with JavaScript.

Reach Out To Us

Unlock Your Business Potential with Cloud Computing Solutions.