tinygo-org / tinygo

Go compiler for small places. Microcontrollers, WebAssembly (WASM/WASI), and command-line tools. Based on LLVM.
https://tinygo.org
Other
15.19k stars 891 forks source link

Tinygo - target=wasm results in uncaught promise error in browser console. #4415

Open owenwaller opened 4 weeks ago

owenwaller commented 4 weeks ago

Hi,

Can someone please clarify the level of support provided by tinygo (v0.32.0 - which is the latest AFAIK) with respect to a wasm target that is being executed in a browser, in comparison to Go v1.22.6.

I can currently seeing very different behaviour when I compile a trivial Go file to wasm using both compilers.

When I compile the Go code with tinygo I see the following error message in the browser console:

localhost/:1 Uncaught (in promise) 
TypeError: WebAssembly.instantiate(): Import #1 "wasi_snapshot_preview1": module is not an object or function
Promise.then (async)        
(anonymous) @   (index):29

However if I compile the same code with Go v 1.226 and server it with the same web server (nginx in a container in my case) the wasm code works as I expect.

Is this a bug in tinygo? I am using the dockerised version of tinygo if that's any help.

Steps to reproduce

main.go

package main

import (
    "fmt"
)

func main() {
    fmt.Printf("Wasm hello\n")
}

index.html

<!doctype html>
<html>
<head>

<title>Wasm Test</title>

<meta charset="utf-8"/>

<script src="https://cdn.jsdelivr.net/npm/text-encoding@0.7.0/lib/encoding.min.js"></script> 
<script src="/wasm_exec.js"></script>
</head>
<body>

<img style="position: absolute; top: 50%; left: 50%;" src="https://cdnjs.cloudflare.com/ajax/libs/galleriffic/2.0.1/css/loader.gif">

</div>
<script>
var wasmSupported = (typeof WebAssembly === "object");
if (wasmSupported) {
    if (!WebAssembly.instantiateStreaming) { 
        WebAssembly.instantiateStreaming = async (resp, importObject) => {
            const source = await (await resp).arrayBuffer();
            return await WebAssembly.instantiate(source, importObject);
        };
    }
    const go = new Go();
    WebAssembly.instantiateStreaming(fetch("/main.wasm"), go.importObject).then((result) => {
        go.run(result.instance);
    });
} else {
    document.getElementById("vugu_mount_point").innerHTML = 'This application requires WebAssembly support.  Please upgrade your browser.';
}
</script>
</body>
</html>

The go.mod is:

module example.com/wasm_http_example

go 1.22.6

The wasm_exec.js is taken from go env GOROOT/misc/wasm/wasm_exec.js`. This is my Go v1.22.6 root.

The tinygo build cmd is:

docker run --rm -v $(pwd):/home/tinygo tinygo/tinygo:0.32.0 tinygo build -o main.wasm -target=wasm main.go

executed in the the directory where the source code is, so the volume map works.

The standard Go build cmd is:

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

Again from the directory that contains the source code.

The resulting main.wasm is then served using a standard nginx container like this:

 docker run --name wasm-nginx --mount type=bind,source=`pwd`,target=/usr/share/nginx/html,readonly -p 8888:80 -d nginx

Again from the directory that contains the source code.

If I serve the main.wasm build with the standard Go v1.22.6 compiler I see Wasm hello in the browser console. If I serve the version built with tinygo I see the uncaught promise error.

This is 100% reproducible, as far as I can tell.

Does anyone have any idea of the cause and is there a fix?

Many thanks

Owen

~Updated to add: I see the same behaviour is I use Go v 1.23 in comparison to tinggo v0.32.0.~ So with go 1.23 nothing works. I had a old (read go 1.22.6) module cache locally and an outdated `go.mod that specified go v1.22.6.

If I update the go.mod to:

module example.com/wasm_http_example

go 1.23

I then see this when I attempt to build:

$docker  run --rm -v $(pwd):/home/tinygo tinygo/tinygo:latest tinygo build -o main.wasm -target=wasm main.go
error: requires go version 1.19 through 1.22, got go1.23
owen@carbonx1:/tmp/wasm-http-example$ docker  run --rm -v $(pwd):/go tinygo/tinygo-dev:latest tinygo build -o main.wasm -target=wasm main.go
go: warning: ignoring go.mod in $GOPATH /go
go: go.mod requires go >= 1.23 (running go 1.22.6; GOTOOLCHAIN=local)

$ go version
go version go1.23.0 linux/amd64

$ ls go/pkg/mod/cache/download/golang.org/toolchain/@v/v0.0.1-go1.23.0.linux-amd64.* # from the local `go` directory
go/pkg/mod/cache/download/golang.org/toolchain/@v/v0.0.1-go1.23.0.linux-amd64.lock
go/pkg/mod/cache/download/golang.org/toolchain/@v/v0.0.1-go1.23.0.linux-amd64.zip
go/pkg/mod/cache/download/golang.org/toolchain/@v/v0.0.1-go1.23.0.linux-amd64.ziphash

So that looks like Go v1.23 support isn't in place yet.

Updated to add: if I use the bleeding edge docket image tinygo/tinygo-dev@latest I still see the same uncaught exception error in the browser window. Again against Go v1.23. But in addition I now see this on the command like:

docker run --rm -v $(pwd):/go tinygo/tinygo-dev:latest tinygo build -o main.wasm -target=wasm main.go
go: warning: ignoring go.mod in $GOPATH /go

The warning is new, and as you can see from the docker command the volume mapping now requires mapping into /go on the container (it was /home/tinygo).

deadprogram commented 4 weeks ago

Make sure you copy wasm_exec.js to your runtime environment.

Via https://tinygo.org/docs/guides/webassembly/wasm/

Also note that the TinyGo wasm_exec.js file is different from the "big Go" file.

Hope that helps!

owenwaller commented 4 weeks ago

@deadprogram

Ah ha, that is indeed the problem. If I swap the wasm_exec.js for the one currently on the master branch:

https://github.com/tinygo-org/tinygo/blob/release/targets/wasm_exec.js

Then all is good using with tinygo v 0.32.0. Well spotted, thank you.

Is there any thing the tinygo team can do to spit this sort of mistake at runtime? Or at least make the error message meaningful?

owenwaller commented 3 weeks ago

Updated to add:

A similar situation exists with the standard Go compiler i.e. the version of wasm_exec.js must match the version of the compiler.

But, given a wasm_exec.js e.g. one that is committed to a repository, it is impossible to tell if that version of wasm_exec.js is compatible with the local Go toolchain.

I have raised this on the golang-nuts mailing list, along with a proposed solution - to embed a version string as a comment into wasm_exec.js See:

https://groups.google.com/g/golang-nuts/c/XJuEayu2KmE

Could the tinygo team please comment on this, and also consider a similar solution, or follow whatever solution to Go project uses?

Thanks

Owen