Go WebAssembly Internals - Part 2
Tags: go, webassembly, webdev, programming
Published: 2022-11-04 06:52:34

In [previous article](https://dev.to/x1unix/go-webassembly-internals-part-1-14aj) we covered how to build a simple Go program, interact with host environment by wrapping Go functions as JavaScript functions and how JS-to-Go call magic works under the hood.

This article will cover how Go runtime access global JavaScript objects and helper functions from `wasm_exec.js` glue-code library and how this mechanism can be exploited to link external JavaScript functions directly to our programs.

## WebAssembly Import Object

Although WebAssembly programs are isolated and have no direct access to a browser (or other host environment), each module can [import](https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format#importing_functions_from_javascript) symbols (usually functions) during instantiation that can be used to communicate with outside world.

All module dependencies to import have to be provided in a form of an object with a symbol name as key and function as a value.

On WASM program side, module should do an import with `import` statement.

```webassembly
(module
	(func $myFunction (;0;) (import "myFunction") (param i32))
)
```

Browser will link import object to a program during module instantiation process.

```js
const importObject = {
  "myFunction": () => console.log("hello world")
}

WebAssembly.instantiate(/* wasm module binary */, importObject);
```

## Go Runtime Dependencies

![WebAssembly Imports List In](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j1lvyv3bwy0aeo46sjgt.png)

*Go WebAssembly module imports*

Each Go program import a couple of runtime dependencies from import object prepared by `Go` helper class provided from [`wasm_exec.js`](https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.js) file.

Most of those functions are used for `syscall/js` package but also there are a few core Go runtime dependencies.

![Import Object in wasm_exec.js file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0nw0w5lp4ssuunzm16up.png)

*Import object in `wasm_exec.js`*

Each imported function accepts current program stack pointer and may manipulate with go program memory inside `this.mem` field of `Go` helper class and return result.

We also see to what exact Go function is will be linked and what params it will return by function key name and comments left above function declarations.

## Linking Imported Function

Lets have a look at declaration of [`syscall/js.stringVal`](https://github.com/golang/go/blob/master/src/syscall/js/js.go#L212) function which present in import object:

```go
//go:build js && wasm

package js

type ref uint64

func stringVal(x string) ref
```

The `stringVal` function doesn't have `//go:linkname` or other compiler directives, so how Go linker know that function implementation is defined in import object?

Function implementation is defined in a separate file `js_js.s` in the same folder. File is written in a [Go assembler language](https://go.dev/doc/asm) and contains body for each `syscall/js` dependency.

```webassembly
#include "textflag.h"

TEXT ·stringVal(SB), NOSPLIT, $0
  CallImport
  RET

```

The `CallImport` instruction is used to declare and call function imports.
Under the hood, Go compiler will generate an `import` statement with a path that corresponds to a function (including package name).

There is a [proposal](https://github.com/golang/go/issues/38248) to replace `CallImport` instruction with a more convenient `//go:wasmimport` compiler directive.

## Importing Custom Functions

All information above allows to define and link custom functions directly to a Go program. Execution of such calls are much cheaper from performance standpoint and Go programs and don't require global namespace pollution (function shouldn't be present in `window` object).

The main downside of this approach is that we have to manually deal with Go program stack, manually read and write data into program memory.

Lets write and import a simple multiplication function. Function will accept 2 integers and will return a result.

Full example source code is available in [this repo](https://github.com/x1unix/go-wasm-imports-example).

### Go Program

Our program will consist of 2 files: a main Go file and accompanying Go assembly file with WASM import.

Program will import and call `multiply` function which is written in JavaScript.

**main.go**

```go
package main

import "fmt"

func multiply(a, b int) int

package main() {
	fmt.Println("Multiply result:", multiply(3, 4))
}
```

**main_js.s**

```webassembly
#include "textflag.h"

TEXT ·multiply(SB), NOSPLIT, $0
  CallImport
  RET

```

The `textflag.h` file is available in `$GOROOT/src/runtime` directory.

#### Building Program

Set `GOOS` and `GOARCH` environment variables to build a program as WebAssembly module:

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


### Writing Wrapper

Go SDK provides a `wasm_exec.js` file with `Go` helper class that implements Go WebAssembly ABI for browsers and contains an import object that needs to be modified.

Copy of the file is available in `$GOROOT/misc/wasm/wasm_exec.js`.

Lets extend `Go` class with a small wrapper which will allow exporting custom functions without touching original `Go` class implementation.

**custom-go.mjs**

```js
// Copied from '$GOROOT/misc/wasm/wasm_exec.js'
import './wasm_exec.js';

export default class CustomGo extends global.Go {
  MAX_I32 = Math.pow(2, 32);

  // Copied from 'setInt64'
  setInt64(offset, value) {
    this.mem.setUint32(offset + 0, value, true);
    this.mem.setUint32(offset + 4, this.MAX_I32, true);
  }

  /**
   * Adds function to import object
   * @param name symbol name (package.functionName)
   * @param func function.
   */
  importFunction(name, func) {
    this.importObject.go[name] = func;
  }
}
```

### Function Implementation

Create a `main.mjs` file that will import and run our WebAssembly program.

```js
import Go from './custom-go.mjs';
import { promises as fs } from 'fs';

// Instantiate Go wrapper instance
const go = new Go();

// Add our function to import object
go.importFunction('main.multiply', sp => {
	sp >>>= 0;  
	const a1 = go.mem.getInt32(sp + 8, true);  // SP + sizeof(int64)  
	const a2 = go.mem.getInt32(sp + 16, true); // SP + sizeof(int64) * 2
	const result = a1 * a2;
	console.log('Got call from Go:', {a1, a2, result});
	go.setInt64(sp + 24, result);
});


// Run the program
const buff = await fs.readFile('./main.wasm');  
const {instance} = await WebAssembly.instantiate(buff, go.importObject);  
await go.run(instance);
```

#### Reading arguments

The `sp` argument is a stack pointer address which is passed to each imported function.
Values of first and second integers should be manually obtained from stack. 

`Go` class has `mem` property which is an instance of [`DataView`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) interface. As MDN documentation says:

> DataView provides a low-level interface for reading and writing different number values into program memory without having to care about platform [endianness]([endianness](https://developer.mozilla.org/en-US/docs/Glossary/Endianness)).

The `this.mem.getInt32` method accepts offset and boolean parameter to indicate that our value is stored in little endian format.

```js
const a1 = go.mem.getInt32(sp + 8, true);  // Stack Pointer + sizeof(int64)
const a2 = go.mem.getInt32(sp + 16, true); // Stack Pointer + sizeof(int64) * 2
```

#### Returning result

Result of the function should be put in memory after the last argument on stack.

```js
go.setInt64(sp + 24, result);
```

### Final Result

Lets run our WebAssembly module with `multiply` function implementation inside Node.js.

**Attention:** for Node.js 18 and below, [WebCrypto API polyfil](https://github.com/PeculiarVentures/webcrypto) is required.

```shell
$ node ./main.mjs

Got call from Go: { a1: 3, a2: 4, result: 12 }
Multiply result: 12
```

Full example source code with polyfill is available in [this repo](https://github.com/x1unix/go-wasm-imports-example).