dave/wasmgo

Problems with Go 1.11rc1 [SOLVED]

Closed this issue · 20 comments

Golang 1.11rc1
macOS 10.13.6

Hi Dave, I've run go get -u github.com/dave/wasmgo on Go 1.11rc1 and as a result of Modules it had downloaded it to go/pkg/mod/github.com/dave/wasmgo@v0.0.0-20180815155418-cd9f1b82897a.

I switched into the helloworld folder where there is a main.go.

To save me having to type the full path, since I was there already, I tweaked the instructions to wasmgo deploy helloworld. Not surprisingly, since there is no source file called helloworld, this didn't work.

What do I have to do to get this to work?

Cheers,
Carl

dave commented

If you're in the correct directory, try just wasmgo deploy

dave commented

if there's no package argument, is uses . and it just passes this to the go build command, so wasmgo deploy will run go build .

Cool! It works, thanks a lot! And is it genuinely building a server-side wasm file and serving that?

dave commented

It's actually very simple - no magic going on at all... It does this:

  1. Runs go build on your machine to create a WASM binary.
  2. Uses a template to create the "loader js".
  3. Uses a template to create the index page.
  4. Works out the SHA1 hash of all three files, and asks the server if any of them are already present in the GCS bucket.
  5. If the server replies that the files are missing, it sends the contents of the files to the server.
  6. All the server does is verifies that the SHA1 hash is what the client says it is, and stores the file in the GCS bucket.

It's a very simple system... The complexity will come in future when we get the package splitting working... that'll be tricky.

It makes life very easy, at least for simple examples. I was experimenting with deploying to zeit.io using their now tool, but I am pleased to say that you have helped me deploy my first cloud-based wasm app. Well, it wasn't my app, that will follow!

Thanks again for this!

dave commented

No problem! Next I'm going to do a wasmgo serve mode that'll run a local development server and recompile each time you refresh the browser...

Just one question, if I may. Do you have, or do you know of any more complicated examples of Golang WASM code? I've tried googling but the ones I have found so far all fail to build, or If they do build, they don't run properly at jsgo.io

dave commented

It would be great if you can show me any examples of projects that should work but don't in jsgo.io... It's always worth remembering that jsgo only serves the wasm file - if the project depends on assets being served (CSS etc) they will need to be served from somewhere else. I've usually used rawgit.com for this...

I haven't actually done much WASM examples, but I did a lot of GopherJS things and they should be pretty trivial to convert... I'll have a go later today.

Okay, here is an example of some code that builds but which doesn't appear to run properly when deployed...

package main // import "lazyhackergo.com/wasmgo"

import (
	"math"
	"strconv"
	"syscall/js"
	"time"

	"lazyhackergo.com/browser"
)

var signal = make(chan int)

// Example of a callback function that can be registered.
func cb(args []js.Value) {
	println("callback")
}

// cbQuit is a function that gets attached to browser event.
func cbQuit(e js.Value) {
	println("got Quit event callback!")
	window := browser.GetWindow()
	window.Document.GetElementById("runButton").SetProperty("disabled", false)
	window.Document.GetElementById("quit").SetAttribute("disabled", true)
	signal <- 0
}

// keepalive waits for a specific value as a signal to quit.  Browsers still run
// javascript and wasm in a single thread so until the wasm module releases
// control, the entire browser window is blocked waiting for the module to
// finish.  It looks like that while waiting for the blocked channel, the
// browser window gets control back and can continue its event loop.
func keepalive() {
	for {
		m := <-signal
		if m == 0 {
			println("quit signal received")
			break
		}
	}
	// select {} also seems to work but the following doesn't:
	// select {
	//    case m <-signal:
	//       // do something
	//    default:
	//       // wait
	// }
}

func main() {
	q := js.NewEventCallback(js.StopImmediatePropagation, cbQuit)
	// defer q.Close()

	c := js.NewCallback(cb)
	// defer c.Close()
	browser.Invoke(c) //js.ValueOf(c).Invoke()

	window := browser.GetWindow()

	// Disable the Run button so the module doesn't get executed again while it
	// is running.  If it runs while a previous instance is still running then
	// the browswer will give an error.
	window.Document.GetElementById("runButton").SetAttribute("disabled", true)

	// Attach a browser event to the quit button so it calls our Go code when
	// it is clicked.  Also enable the Quit button now that the module is running.
	window.Document.GetElementById("quit").AddEventListener(browser.EventClick, q)
	window.Document.GetElementById("quit").SetProperty("disabled", false)
	//js.Global.Get("document").Call("getElementById", "quit").Call("addEventListener", "click", js.ValueOf(q))

	window.Alert("Triggered from Go WASM module")
	window.Console.Info("hello, browser console")

	canvas, err := window.Document.GetElementById("testcanvas").ToCanvas()
	if err != nil {
		window.Console.Warn(err.Error())
	}

	// Draw a cicule in the canvas.
	canvas.Clear()
	canvas.BeginPath()
	canvas.Arc(100, 75, 50, 0, 2*math.Pi, false)
	canvas.Stroke()

	// A Go routine that prints its counter to the canvas.
	canvas.Font("30px Arial")
	time.Sleep(5 * time.Second)
	go func() {
		for i := 0; i < 10; i++ {

			canvas.Clear()
			canvas.FillText(strconv.Itoa(i), 10, 50)
			time.Sleep(1 * time.Second) // sleep allows the browser to take control otherwise the whole UI gets frozen.
		}
		canvas.Clear()
		canvas.FillText("Stop counting!", 10, 50)

	}()
	window.Console.Info("Go routine running while this message is printed to the console.")

	keepalive()
}

Just to show that not everything is bad in example-land, this one works...

package main

import(
	"syscall/js"

	"github.com/PaulRosset/go-hacknews"
)

func topStories() []hacknews.Post {
	// This is an example from:
	// https://github.com/PaulRosset/go-hacknews
	// Copyright (c) 2017 Paul Rosset
	// See LICENSE in the link above.
	init := hacknews.Initializer{Story: "topstories", NbPosts: 10}
	codes, err := init.GetCodesStory()
	if err != nil {
		panic(err)
	}
	posts, err := init.GetPostStory(codes)
	if err != nil {
		panic(err)
	}
	return posts
}

var document = js.Global().Get("document")

func renderPost(post hacknews.Post, parent js.Value) {
	// Creates the following HTML elements
	// <li>
	//   <a href="{URL}">{Title}</a>
	// </li>
	li := document.Call("createElement", "li")
	a := document.Call("createElement", "a")
	text := document.Call("createTextNode", post.Title)
	a.Set("href", post.Url)
	a.Call("appendChild", text)
	li.Call("appendChild", a)
	parent.Call("appendChild", li)
}

func renderPosts(posts []hacknews.Post, parent js.Value) {
	ul := document.Call("createElement", "ul")
	parent.Call("appendChild", ul)
	for _, post := range(posts) {
		renderPost(post, ul)
	}
}

func main() {
	posts := topStories()
	body := document.Get("body")
	renderPosts(posts, body)
}
dave commented

The first example is looking for a DOM element called "runButton"... I believe in the example index page in the Go repo it has a run button instead of running the code when the page loads... This will need some tweaks...

This one doesn't build at all...

package main

import (
	"syscall/js"

	"github.com/shurcooL/github_flavored_markdown"
)

var document = js.Global().Get("document")

func getElementByID(id string) js.Value {
	return document.Call("getElementById", id)
}

func renderEditor(parent js.Value) js.Value {
	editorMarkup := `
		<div id="editor" style="display: flex; flex-flow: row wrap;">
			<textarea id="markdown" style="width: 50%; height: 400px"></textarea>
			<div id="preview" style="width: 50%;"></div>
			<button id="render">Render Markdown</button>
		</div>
	`
	parent.Call("insertAdjacentHTML", "beforeend", editorMarkup)
	return getElementByID("editor")
}

func main() {
	quit := make(chan struct{}, 0)

	// See example 2: Enable the stop button
	stopButton := getElementByID("stop")
	stopButton.Set("disabled", false)
	stopButton.Set("onclick", js.NewCallback(func([]js.Value) {
		println("stopping")
		stopButton.Set("disabled", true)
		quit <- struct{}{}
	}))

	// Simple markdown editor
	editor := renderEditor(document.Get("body"))
	markdown := getElementByID("markdown")
	preview := getElementByID("preview")
	renderButton := getElementByID("render")
	renderButton.Set("onclick", js.NewCallback(func([]js.Value) {
		// Process markdown input and show the result
		md := markdown.Get("value").String()
		html := github_flavored_markdown.Markdown([]byte(md))
		preview.Set("innerHTML", string(html))
	}))

	<-quit
	editor.Call("remove")
}

You may be interested to know that I've been building these with Go Modules activated (GO111MODULE=on) and after running go mod init <reponame> for each one, that aspect of it seems to work perfectly.

dave commented

OK so I just deployed a new version... Do a go get -u github.com/dave/wasmgo... now you might be able to get the first example working if you add an index.wasmgo.html with the following contents:

<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>

<head>
	<meta charset="utf-8">
	<title>Go wasm</title>
</head>

<body>
	<script src="{{ .Script }}"></script>
	<script>
		if (!WebAssembly.instantiateStreaming) { // polyfill
			WebAssembly.instantiateStreaming = async (resp, importObject) => {
				const source = await (await resp).arrayBuffer();
				return await WebAssembly.instantiate(source, importObject);
			};
		}
		const go = new Go();
		let mod, inst;
		WebAssembly.instantiateStreaming(fetch("{{ .Binary }}"), go.importObject).then((result) => {
			mod = result.module;
			inst = result.instance;
			document.getElementById("runButton").disabled = false;
		});
		async function run() {
			console.clear();
			await go.run(inst);
			inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
		}
	</script>

	<button onClick="run();" id="runButton" disabled>Run</button>
</body>

</html>
dave commented

The markdown example built fine and deployed for me but doesn't run...

 $ wasmgo -v -c=go1.11beta3 deploy
Compiling...
Querying server...
Files required: 3.
Bundling required files...
Sending payload: 1137KB.
Storing, 1 to go.
Storing, 2 to go.
Storing, 3 to go.
Storing, 2 to go.
Storing, 1 to go.
Storing, 0 to go.
Sending done.
https://jsgo.io/c4bc3cab491b3879df546d95a2e9394dce1ed079

... it doesn't look like a wasmgo failure though... I'd want to test this locally to check it runs ok.

Good work Dave! I can confirm that the example with the run button and the additional html file now works perfectly, if unspectacularly!

I don't know how familiar you are with Go Modules, but I've had some issues updating the package. Details are at golang/go#27028. Hopefully, Russ Cox will see the issue and answer it. We'll see!

dave commented

I’m afraid I haven’t looked into modules yet. Is there something I need to add as a package author? I assume not until I want to enforce dependencies...

No worries Dave! I'll see if Russ Cox answers my question and I'll keep you posted - it's an area that every Golang package author will need to address sooner or later.