Javascript Is the New ABI
I’ve been twiddling around WASM related stacks lately, not exactly focusing on the WASM itself, but rather on developing a web app with a single programming language stack.
Both Tauri and Wails allow a desktop app to be powered by web platform a la electron. What’s different is Tauri is written for Rust, and Wails is written for Go, both framework also utilizing libwebkit or native webview from the OS itself, not defaulting to embedded webkit & v8 like electron does.
Since electron is written in Javascript, both the windowing (native) code and the frontend (web) code will be written in Javascript so it feels seamless, but for Tauri / Wails the windowing/native part will be written in Rust / Go, and if we go the vanilla ways, the content will be written in Javascript, this dual stack switching between Javascript and non Javscript is bothering me quite a bit, and I’m currently fiddling about to see how natural it is if all parts of the app is written in a single language, other than Javascript that is, either in Rust or Go.
I’ve been experimenting with both, though it’s much earlier with Tauri:
🔖 mnswpr
To develop with a single pl stack, we will need to resort to WASM for the frontend, for Tauri I use Yew for its similarity with React, the setup is simple and straightforward, since Tauri already has all the supporting boilerplate for running WASM, after the initial scaffolding we can just focus on developing the app.
This is not the case with Wails, as there is still no matured enough frontend (WASM) frameworks/libraries in Go compared to a handful alternatives in Rust. Ultimately, I decided to use Vugu which follows Vue concepts contrast to React.
The first issue I encounter when trying to run WASM with Wails was related to how the framework communicate to its (web) content. Both Tauri and Wails use custom protocol scheme to call into its content, this is possible by registering:
// example from wails' setup
webkit_web_context_register_uri_scheme(context, "wails", (WebKitURISchemeRequestCallback)processURLRequest, NULL, NULL);
with "wails"
as the custom URI scheme, and processURLRequest
as the request handler.
Apparently such custom scheme has different behavior compared to http(s), for example as shown in the following, there is no header sent back as part of the response.
This is a problem since we can’t load our WASM file without application/wasm
content header, I did some reading and apparently WebAssembly.instantiateStreaming
will always check for the Content-Type
header and will refuse to load our WASM file if it’s not application/wasm
. To be fair, this is not exactly Wails fault, but really makes you think why Tauri can still load the WASM despite they’re also using custom URI scheme (tauri://
).
After checking at how Tauri / Yew doing this, apparently they’re acknowledging that there’s no response header provided due to tauri://
custom scheme, instead they’re resorting to different API to load the WASM file: WebAssembly.instantiate
.
So I wrote this small wrapper for Wails/Vugu to allow graceful fallback when attempting instantiateStreaming
.
const go = new Go();
const mainWasmURL = 'main.wasm';
function runWasm(runner, module) {
return runner(module, go.importObject).then((result) => go.run(result.instance));
}
function fallbackWasmInstantiate() {
fetch(mainWasmURL)
.then((response) => response.arrayBuffer())
.then((buff) => runWasm(WebAssembly.instantiate, buff));
}
fetch(mainWasmURL).then((response) => {
runWasm(WebAssembly.instantiateStreaming, response).catch((err) => {
if (response.headers.get('Content-Type') != 'application/wasm') {
console.warn('cannot stream wasm, binary served without `application/wasm` Content-Type header');
fallbackWasmInstantiate();
} else throw(err);
});
});
I think it’s pretty obvious from the code above, that Go has additional runtime / support code that will need to be included when running WASM file, we have to include it, and it IS written in Javascript, so there you have it.
To compare both experience also to show how WASM in the browsers still cannot escape Javascript entirely beyond this, here is several snippets:
How Should We Interact with Promises or Async/Await in WASM #
In Go, that is undefined, Go only provide experimental syscall/js
std library which defines how Javascript values, types, and function invocation can be declared and used from Go, but that’s it. So we can use these primitives directly, or design another abstraction on top of it.
If we don’t want to use any abstraction, for example, this is how we typically call into function and interact with Javascript promises:
vchan := make(chan js.Value, 1)
errchan := make(chan js.Value, 1)
js.Global().
Call("Provide").
Call("then", js.FuncOf(func(_ js.Value, args []js.Value) {
vchan <- args[0]
})).
Call("catch", js.FuncOf(func(_ js.Value, args []js.Value) {
errchan <- args[0]
}))
select {
case err := <-errchan:
// we can use err here
return "", fmt.Errorf(err.String())
case val := <-vchan:
// we can use val here
return val.String(), nil
}
Just pretend that we have a global function called Provide
which returns a promise that will resolve to a string value.
As we noticed, it’s too verbose, and really tedious if we have a lot of use cases with promises.
Instead, I would like to have interaction with async call (promises) as the following in Go:
val, err := Promise[String]{js.Global().Call("Provide")}.Await()
This is possible if we abstracting the whole promise concept with some boiler plate like the following:
package bindings
import (
"syscall/js"
"github.com/fudanchii/infr"
)
type Promise[T infr.TryFromType[js.Value, T]] struct {
val js.Value
}
func (p Promise[T]) forward(t string, f func(arg js.Value)) Promise[T] {
var cb js.Func
cb = js.FuncOf(func(_ js.Value, args []js.Value) any {
f(args[0])
cb.Release()
return nil
})
p.val.Call(t, cb)
return p
}
func (p Promise[T]) Catch(f func(arg js.Value)) Promise[T] {
return p.forward("catch", f)
}
func (p Promise[T]) Then(f func(arg js.Value)) Promise[T] {
return p.forward("then", f)
}
func (p Promise[T]) Await() (T, error) {
vchan := make(chan js.Value, 1)
echan := make(chan js.Value, 1)
p.Then(func(arg js.Value) { vchan <- arg }).
Catch(func(err js.Value) { echan <- err })
var defT T
select {
case err := <-echan:
return defT, PromiseError{err}
case v := <-vchan:
return infr.TFI[js.Value, T]{Source: v}.TryInto()
}
}
type PromiseError struct {
val js.Value
}
func (pe PromiseError) Error() string {
if pe.val.Get("message").Type() == js.TypeString {
return pe.val.Get("message").String()
}
return pe.val.String()
}
It feels unfortunate when we realize Go hasn’t provided any absctraction for this interoperability, we can’t escape Javascript Promise’s then
& catch
.
In contrary for Rust, this is mostly a solved problem. For FFI, Rust has this crate (library) that can automatically generate necessary code to invoke the target function, called bindgen, and for WASM, we have wasm-bindgen
, furthermore specific for promises / async/await cases, we have wasm-bindgen-future
, this crate can automatically generate wrapper code required to invoke browser functionalities with Rust paradigm, for example following the Provide
example above, we can just declare the following:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(catch, js_namespace = ["window"])]
async fn provide() -> Result<JsValue, JsValue>
}
which then can be called like the following (in Rust async context):
let string = provide().await?.as_string();
No addition boilerplate needed.
Even Rust Can’t Escape Javascript Entirely #
At one point, the app I developed in Tauri/Yew require a long running time ticker, so at a glance I will need a mechanism to thread out and do infinite loop with a delay in between. But this kind of logic wont work in browser, again this is due to the execution environment coupled to Javascript runtime. Instead, we need to do it as how it supposed to be done in browsers: requestAnimationFrame
.
A wrapper for requestAnimationFrame
is already implemented under gloo_render
crate, but it’s not the way how I wanted it to be, so with Yew hooks support I wrote this rAF wrapper for Rust:
#[cfg(not(test))]
use gloo_render::{request_animation_frame, AnimationFrame};
use std::cell::RefCell;
use std::rc::Rc;
use yew::{functional::use_mut_ref, hook};
#[cfg(test)]
use tests::{request_animation_frame, AnimationFrame};
pub enum RAFNext {
Continue,
Abort,
}
impl From<bool> for RAFNext {
fn from(value: bool) -> Self {
if value {
return RAFNext::Continue;
}
RAFNext::Abort
}
}
fn raf_callback<P>(rafcell: Rc<RefCell<Option<AnimationFrame>>>, callback: P, frame: f64)
where
P: Fn(f64) -> RAFNext + 'static,
{
let rafcell_clone = rafcell.clone();
*rafcell.borrow_mut() = match callback(frame) {
RAFNext::Abort => None,
RAFNext::Continue => Some(request_animation_frame(move |f| {
raf_callback(rafcell_clone, callback, f)
})),
};
}
pub struct RequestAnimationFrame(Rc<RefCell<Option<AnimationFrame>>>);
#[hook]
pub fn use_request_animation_frame() -> RequestAnimationFrame {
RequestAnimationFrame(use_mut_ref(|| None))
}
impl RequestAnimationFrame {
pub fn each<Q>(&self, callback: Q)
where
Q: Fn(f64) -> RAFNext + 'static,
{
let raf_clone = self.0.clone();
*self.0.borrow_mut() = Some(request_animation_frame(move |f| {
raf_callback(raf_clone, callback, f)
}));
}
pub fn once<Q>(&self, callback: Q)
where
Q: FnOnce(f64) + 'static,
{
let raf_clone = self.0.clone();
*self.0.borrow_mut() = Some(request_animation_frame(move |f| {
callback(f);
*raf_clone.borrow_mut() = None;
}));
}
}
The highlight points here is we can use either RequestAnimationFrame.each
for a periodical loop run with rAF, or RequestAnimationFrame.once
for a one shot rAF call.
The complete code is listed here.
At the code above we’re more focusing on how it can be abstracted for a construct to run something periodically, and to be fair, it’s already far from Javascript paradigm, but the fact that we are abstracting around the behavior of requestAnimationFrame
that we have to call rAF recursively for periodical run is apparent here, we can’t escape from Javascript.
Application Binary Interface #
ABI is a set of API and additional rules for data type and data structures over binary executables, it’s a standard that is used by compilers to rule over how functions can be called, how operating system’s syscall
can be called, and how to pass data in-between application and its underlying OS, when compiling codes to binary executables.
We can create an analogy of browsers as operating systems, browsers functionalities as its syscalls, and Javascript can be either its API, ABI, and its ISA, depend on how we abstractize our application relative to Javascript. With WASM, we’re not compiling to Javascript (emscripten used to doing this with asmjs), we’re compiling directly to a binary format that we expect can be run directly on browsers, but there’s no direct access to browsers functionalities from WASM short of calling into Javascript.
We can visualize how WASM calls to browsers functionalities not only by using Javascript API, but also mapping to, and deriving from Javascript data types and structure.
Javascript is the ABI for browsers.