Venturing into WASM on React Native and Golang
Tags: go, webassembly, reactnative, typescript
Published: 2022-11-10 10:08:31

---
title: Venturing into WASM on React Native and Golang
published: true
date: 2022-10-15 00:00:00 UTC
tags: golang,wasm,reactnative,typescript
canonical_url: https://david.coffee/react-native-wasm-golang/
---

![Gopher Programming](https://david.coffee/assets/goling.png)

_Appeared first on my [personal blog](https://david.coffee/react-native-wasm-golang/)_

I recently had the idea to create a little react native app for FastMails MaskedEmails, to quickly add new ones on my device, so I don’t have to open the FastMail app all the time.

I wanted to do this with as little code duplication as possible, so instead of rewriting a client from scratch, I decided to re-use my [maskedemail-cli](https://github.com/dvcrn/maskedemail-cli) somehow. I could have probably compiled it down to a shared library and pulled it into Swift, but I am no iOS developer and know React already, so why not do it in React Native with WASM?

I’ve never actually used wasm and was looking for an excuse to play around with it. Here’s what I learned.

You can find the app I worked on during this writeup at [https://maskedemailmanager.david.coffee](https://maskedemailmanager.david.coffee)

_(Spoiler: You can’t use native WASM within JSC on React Native yet)_

## Golang and WASM, how does it actually work?

Golang supports [compiling to wasm](https://github.com/golang/go/wiki/WebAssembly#webassembly) since version 1.11, with further improvements happening in 1.13. The basic gist of making it spit out wasm is

```
GOOS=js GOARCH=wasm go build -o main.wasm

```

Main interactions from Golang with JavaScript-land happen through the `js/syscall` [package](https://pkg.go.dev/syscall/js). For example, if we want to create a js function, we can do it like so:

```
cb = js.FuncOf(func(this js.Value, args []js.Value) any {
	fmt.Println("Hello World")
	return nil
})

```

The first argument is always `this`, and the second argument is always an array of actual arguments passed to the function.

We can also use `js.Global()` to get a ref to the global object and call into js:

```
js.Global().Call("alert", "Hello from wasm!")

```

… and of course, we can also make our functions available on the global object:

```
js.Global().Set("myCoolFunc", cb)

```

### Using Golang WASM from JavaScript

Golang comes with some [glue code](https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js) that you need to run wasm files built by Golang properly, that’s this copy statement you see in most guides:

```
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

```

Besides setting up the environment, this code defines the `globalThis.Go` [object](https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js#L95) that we’ll use to run our wasm code:

```
const go = new Go();
const res = await WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject);
go.run(res.instance);

```

(instantiateStreaming supports fetch; that’s why everything is so compact. we can also use `WebAssembly.instantiate(arrayBuffer, go.importObject)` directly without fetch).

`go.importObject` provides values to be imported into the new instance, which defines [all the supported syscalls](https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js#L209)

## Preparing maskedemail-cli for WASM - async code and Promises

First, we need to create glue code that exports the necessary functions from maskedemail-cli to JavaScript, in Golang.

```
func main() {
	done := make(chan struct{}, 0)
	js.Global().Set("maskedemailList", js.FuncOf(list))
	<-done
}

func list(this js.Value, args []js.Value) interface{} {
    // (...) get maskedemails here (...)
    return maskedEmailArray
}

```

Now here’s a problem: We can’t block the main goroutine with things like HTTP requests. It would be kinda weird if we do an ajax request in Golang and the browser freezes while our functions get executed…

Instead, async code has to go into separate goroutines, which means in JavaScript… Promises!!

Specifically, to make our code compatible with js promises from Golang, we have to use the Promise constructor through `js.Global()`, effectively creating a Promise from scratch:

```
func list(this js.Value, args []js.Value) interface{} {
    handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        resolve := args[0]
    	reject := args[1]
    	go func() {
    	    // (...) get maskedemails here (...)
    	    resolve.Invoke("Hi there")
    	}
    })
    promise := js.Global().Get("Promise")
    return promise.New(handler)
}

```

`js.Global().Get("Promise")` is getting the global Promise object, then calling `New` onto it with a custom handler.

Running `main()` from above would now populate `globalThis.list`, and it’s signature would look like this: `list(): Promise<string>`.

## Creating a NPM package that’s using WASM

Next, I wanted to publish an NPM package to pack away the complexity, but most guides on Golang + wasm were mainly focused on the browser, not NodeJS.

Luckily the key steps are effectively the same:

1. Use the `wasm_exec.js` glue script to populate globalThis.Go
2. Load the wasm file through whatever method
3. Run `WebAssembly.instantiate` or `WebAssembly.instantiateStreaming`, pass `go.importObject`
4. Run `go.run(instance)`

In NodeJS, we can solve 1. by doing a dirty `require("./wasm_exec.js")`. The code runs fine in the nodejs runtime so no issues there. Too bad it’s polluting `globalThis` and doesn’t give us a nice-to-use package back, but we can live with that.

Point 2. is a tad trickier - we could of course, just load it with `fs.readFileSync`:

```
const go = new globalThis.Go()
const buffer = fs.readFileSync('./main.wasm');
const inst = await WebAssembly.instantiate(buffer, go.importObject);
go.run(inst.instance);
// run func
globalThis.list();

```

… but given that this is JavaScript, usage of `fs` means that this is already unusable in a browser environment, which is not great.

What’s more, `wasm_exec.js` depends on a few browser-specific modules, mainly `crypto` and `TextEncoder`. So now we have some stuff that doesn’t work in the browser, and also some stuff that doesn’t work within node 😀 Let’s fix that with some polyfills:

```
if (!globalThis.crypto) {
  const crypto = require("crypto");
  globalThis.crypto = crypto;
}

if (!globalThis.TextEncoder) {
  const te = require("text-encoding-polyfill");
  globalThis.TextEncoder = te.TextEncoder;
  globalThis.TextDecoder = te.TextDecoder;
}

const go = new globalThis.Go()
const response = await fetch('./main.wasm');
const buffer = await response.arrayBuffer();
const module = await WebAssembly.compile(buffer);
const instance = await WebAssembly.instantiate(module);
go.run(inst.instance);
// run func 
globalThis.list()

```

Instead of `fs`, we can use `node-fetch` (or the new NodeJS native one) and `fetch` to pull the wasm file, then continue instantiating like usual. Now it’s no longer node/browser or fetches implementation dependent.

#### My final solution

Because I wanted to package this for ESM and CJS simultaneously, I went with [rollup](https://rollupjs.org/guide/en/) as bundler. [rollup-plugin-wasm](https://www.npmjs.com/package/@rollup/plugin-wasm) can handle loading + instantiating of the actual wasm file similar to how we did above, so we can slim the code down like this:

```
// polyfills here 
require("./wasm_exec.js");
import wasmMain from "./static/main.wasm";

const go = new globalThis.Go();
const wasmObj = await wasmMain(go.importObject);
go.run(wasmObj.instance);
globalThis.list()

```

### Packaging into a usable module

To make everything a tad nicer, let’s export some functions that other modules can use:

```
interface Mod {
  list: (token: string, accountId: string) => Promise<MaskedEmail[]>;
}

const instantiatedPromise = new Promise<Mod>(async (resolve, reject) => {
  const go = new (globalThis as any).Go();
  const wasmObj = await wasmMain(go.importObject);
  go.run(wasmObj.instance);

  resolve({
    list: (globalThis as any)["maskedemailList"],
  });
});

export const list = async (token: string, accountId: string) => {
  const mod = await instantiatedPromise;
  return mod.list(token, accountId);
};

```

We have some waiting to do before wasm is usable, so we package all of that into a new `instantiatedPromise`. The exported `list()` is then awaiting that promise before doing anything, then executing `mod.list`, our actual wasm function.

To make the API nicer to use I’ve added typescript type definitions for the module, that’s what the `interface Mod` is for. `instantiatedPromise` will resolve with `Mod`, so calling `mod.list` will have the same return signature as `Mod.list`

The package is available on [npm](https://www.npmjs.com/package/maskedemail), the code on [my github](https://github.com/dvcrn/maskedemail-js)

## Investigating React Native + WASM

By now, we have packaged everything so neatly that it doesn’t matter that we used Golang to create the wasm binary. It’s all just an npm package that _should_ be usable everywhere. (Ignore the big 7MB file size of the `.wasm` file for now 😛)

So all we have to do is `yarn add maskedemail` and that should be it… right? Right??

But life is not that simple, and adding that final lego piece was much harder than expected. Let’s go over what is making wasm + react native so hard?? (This was written by poking the simulator and debugging errors)

- `WebAssembly.instantiateStreaming` / `WebAssembly.compileStreaming` only works with `fetch` if the server is returning `application/wasm`, which react-native doesn’t. In fact, using `fetch` to pull in resources the same way we do in NodeJS or the Browser is in RN flimsy at best
  - Expo has [Asset.fromModule](https://docs.expo.dev/versions/latest/sdk/asset/#frommodulevirtualassetmodule) to make it possible to load arbitrary assets from disk
- `WebAssembly.instantiateStreaming` / `WebAssembly.compileStreaming` doesn’t even exist in the react native JavaScriptCore runtime (`WebAssembly` exists, but those functions don’t. Only `WebAssembly.instantiate` and `WebAssembly.compile` do, which work with ArrayBuffers
- Current asset loading functionality through `Asset.fromModule` or fetch implementations for react native can pull assets, but rn’s `FileReader` [does not implement readAsArrayBuffer](https://github.com/facebook/react-native/blob/main/Libraries/Blob/FileReader.js#L77-L79), so no `fetch().then((res) => res.arrayBuffer())`. No ArrayBuffer means no easy way to use `WebAssembly.instantiate`, and also no easy trickery to convert Blob into an ArrayBuffer
- RN’s JavaScriptCore does not have a `crypto` implementation
- Expos packager is trying to resolve all `require` and build and doesn’t support dynamic imports, so having `require("crypto")` in an if statement (like a env-dependent polyfill) will _still_ make the packager throw an error that the package doesn’t exist, effectively meaning that our generic solution ain’t gonna work

I probably forgot a few points but these were the most noticeable issues.

I banged my head against `FileReader.readAsArrayBuffer` to make it possible to read the `.wasm` file from disk, tried a couple polyfills and re-implementations, but eventually gave up. There are `fs.readFile` implementations that could work, but expo (the tool I am using to bootstrap my RN application) _does not support_ linking libraries, so we can’t easily add things that require editing of the application bundle.

To make things worse, the package resolving the issue I mentioned above also meant that all the glue code that `rollup-plugin-wasm` generated to switch based on environment for browser + nodejs doesn’t work either, because if there is a call to `require("fs")`, expo breaks. Same thing for attempts to polyfill `crypto` in the correct environment with `crypto-browserify.`

### Fixing RN issues one by one

Looking at `wasm_exec.js`, we can see that it doesn’t need the entire `crypto` package, just one single function:

```
if (!globalThis.crypto) {
	throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}

```

That’s much easier to polyfill with something like [polyfill-crypto.getrandomvalues](https://github.com/kumavis/polyfill-crypto.getrandomvalues) (though using `Math.random()` instead of proper entropy source…)

We can swap out our existing polyfill for this implementation:

```
import getRandomValues from "polyfill-crypto.getrandomvalues";
if (!globalThis.crypto) {
  globalThis.crypto = {
    getRandomValues,
  };
}

```

Now the import no longer breaks nodejs, browser, or react-native JavaScriptCore :)

Loading the `.wasm` file is a different beast, and the polyfills for `FileReader.readAsArrayBuffer` caused expo to crash consistently. So, what do we do if we can’t load something from disk in the format we need? We play dirty: Instead of async loading the `.wasm` file like a good citizen, we base64 that chunker and stuff it into our main JavaScript file!

`rollup-plugin-wasm` even has an option to inline wasm:

```
wasm({
  sync: ["main.wasm"],
  targetEnv: "auto-inline",
  maxFileSize: 0,
}),

```

The result will make any editor without proper optimizations for uber-large files crawl

![Delicious base64 blobs](https://david.coffee/assets/wasm-base64.png)

… but it does work inside the Simulator! We’re no longer dependent on asset loading logic anymore. It’s all just js now.

To fix remaining `FileLoader` issues, there is [this patch](https://github.com/acostalima/react-native-polyfill-globals/blob/master/patches/react-native%2B0.63.3.patch) available that polyfills the functionality. This patch alone doesn’t work to load the wasm file from disk and crashes the simulator, but it does work for smaller under-the-hood functionality that relies on `readAsArrayBuffer`.

With that out of the way, my app finally runs in the Simulator 🎉

I thought this would be the end of the story and continued chipping away on my app. After days of work, it was finally ready to run it on my iPhone to do a final test before release. Compiling everything… running…

![Screen Shot 2022-10-02 at 23.14.08](https://david.coffee/assets/Screen%20Shot%202022-10-02%20at%2023.14.08.png)

You gotta be kidding. `WebAssembly` does not exist at all in JavaScriptCore when running on an actual device, but it _does_ exist inside the Simulator. Apparently, Apple shut it down in recent iOS versions, but from reading through posts, this used to be available in the past.

## … the end? Did we fail? What next?

I’ve been considering what next steps to take to get this working, but without proper WebAssembly support in JSC on devices, performance will always be subpar. Here are some thoughts and directions I investigated:

#### Option 1: Fallback to WKWebView

This is the most promising but also a bit absurd. We create a WebView, inject our wasm binary into the webview and execute it there, then relay messages (with `window.postMessage`) back and forth with our main app.

In my tests, this actually worked pretty well, with the problem that making something generic that _just works_ is a bit hard. Actually, polyfilling `window.WebAssembly` would mean we need to somehow bridge `go.importObject` into our webview. On top of that, we need to proxy the changes the wasm binary did (like modifying globalThis) back to our native app using a transparent message relay, so libraries that rely on it can _just work_.

Without a proper polyfill, `window.WebAssembly` won’t be available in global scope, so our `maskedemail` NPM package won’t be able to use it.

All in all, a lot of headache. Probably better to split native and bridged code altogether and write the glue code from scratch. ([react-native-react-bridge](https://github.com/inokawa/react-native-react-bridge) tries to get around this by using a separate entrypoint that’s getting processed with babel)

#### Option 2: wasm2js

[binaryen](https://github.com/WebAssembly/binaryen) includes a tool called `wasm2js` that compiles wasm files back to JavaScript. I have tried this as well, but could not get it running with my library, probably due to complexity with some stuff like reflections and HTTP requests. Very promising but very hard to debug due to the way this tool spits out JavaScript. Either it works, or it doesn’t; for me, it didn’t. (Though the errors I got were actual Go errors related to HTTP, so it almost worked)

#### Option 3: gopherjs

Not a solution to wasm, but to make Go run within react-native. Instead of building a wasm binary, [gopherjs](https://github.com/gopherjs/gopherjs) can output functional JavaScript so we don’t have to deal with WebAssembly runtime at all.

Honestly, I think this might be the best option for code reusability until WebAssembly support is fully available in JSC.

## What I ended up doing: WASM -> GopherJS

I’ve tried all options listed above, and the GopherJS variant was the nicest to work with. Even better: The Go API for GopherJS is almost identical to WASM! For example:

This is the WASM version:

```
func list(this js.Value, args []js.Value) interface{} {
    handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        resolve := args[0]
    	reject := args[1]
    	go func() {
    	    // (...) get maskedemails here (...)
    	    resolve.Invoke("Hi there")
    	}
    })
    
    promiseConstructor := js.Global().Get("Promise")
    return promiseConstructor.New(handler)
}

```

This is the gopherjs version:

```
func list(this *js.Object, args []*js.Object) interface{} {
	handler := js.MakeFunc(func(this *js.Object, args []*js.Object) interface{} {
		resolve := args[0]
		reject := args[1]
		go func() {
        	// (...) get maskedemails here (...)
			resolve.Invoke(out)
		}()

		return nil
	})

	promiseConstructor := js.Global.Get("Promise")
	return promiseConstructor.New(handler)

```

In fact, it’s so similar that I was able to flat-out generate the `gopherjs` main.go file with this script:

```
.PHONY: generate-gopherjs
generate-gopherjs:
	rm -rf cmd/gopherjs
	mkdir -p cmd/gopherjs
	cp main.go cmd/gopherjs/
	sed -i '' 's/syscall\/js/github\.com\/gopherjs\/gopherjs\/js/g' cmd/gopherjs/main.go
	sed -i '' 's/FuncOf/MakeFunc/g' cmd/gopherjs/main.go
	sed -i '' 's/js\.Value/\*js\.Object/g' cmd/gopherjs/main.go
	sed -i '' 's/js\.Global()/js\.Global/g' cmd/gopherjs/main.go

```

With a bit of JavaScript, I was able to add an automatic fallback to the gopherjs version to `maskedemail-js`, so it’s now usable even when `window.WebAssembly` isn’t available (although ships with 2x 8MB files).

## No WASM in react-native as of now

A bit sad I wasn’t able to get my goal of using real WASM working within React Native, but learned a good bit with this adventure.

For now, I’ll use GopherJS when I want to re-use Go code, but I hope we’ll see actual WebAssembly support in JSC on devices soon-ish. With all the WASM hype, I doubt it’ll take long.

There is also stuff like [Wasmer](https://wasmer.io) that can embed WASM binaries into other languages like Swift, so instead of going the JSC route, it should be possible to embed it into native modules directly or non-RN apps - for when we really want to re-use the same binary at different places. But that’s also doable with Golang, without WASM.

The app is available at [https://maskedemailmanager.david.coffee](https://maskedemailmanager.david.coffee)