WebAssemblyでjavascriptからGoにJSONを渡して使う方法


WebAssemblyでjavascriptからGo側にJSONを渡してそれを利用する方法について書きました。Goのバージョンは1.12.7で確認しています。`syscall/js`を使用しているので今後変わる可能性があります。

javascriptからJSONを渡したら、Go側で一度文字列に変換してからUnmarshalします。

もしJSONに関数が含まれている場合は、javascriptの関数を文字列に変換する処理と、その文字列をjavascriptの関数に変換してGo側から呼び出せるようにする処理を追加します。

より良い方法があったら教えていただけると助かります。

# JSONを渡す
javascript側で生成したJSONをGo側に渡して参照する方法です。

以下の例では、javascript側で生成した`test_json`を、Goで定義している`CheckJson`という関数に渡します。

```javascript
function TestJson(){
	var test_json = {
		"Val" : 1,
		"Str" : "abcde",
		"Float" : 0.5
	}
	CheckJson(test_json)
}
```

go1.12.7 の時点の`syscall/js`の[ドキュメント](https://golang.org/pkg/syscall/js/) にはJSONを扱う方法は書いてないです。1.13の`syscall/js` の変更点を見た限りでも特に対応される様子はなさそうでした。

本記事では、Go側で受け取ったJSONを一度文字列に変換してからGoの構造体にUnmarshalします。javascriptから渡された値は、`js.Value`として扱われます。`syscall/js`には`js.Value`をstringに変換するための`String()`メソッドが用意されているためそれを利用しました。

実装は下のようになります。

```go
type CheckStruct struct {
	Val   int     `json:"val"`
	Str   string  `json:"str"`
	Float float64 `json:"float"`
}

func unmarshalJSON(src js.Value, dst interface{}) {
	str := js.Global().Get("JSON").Call("stringify", src).String()
	err := json.Unmarshal([]byte(str), &dst)
	if err != nil {
		fmt.Println(err)
	}
}

func CheckJSON(this js.Value, args []js.Value) interface{} {
	var checkStruct CheckStruct
	unmarshalJSON(args[0], &checkStruct)
	fmt.Println(checkStruct)
	return nil
}

func registerCallbacks() {
	js.Global().Set("CheckJson", js.FuncOf(CheckJSON))
}
```

`CheckStruct`はjavascriptから受け取った値をUnmarshalするための構造体です。`unmarshalJSON`は実際のUnmarshal処理になります。`CheckJson`はGo側から呼び出される関数です。`registerCallbacks`では、`CheckJson`をjavascript側から呼び出せるように登録します。


実際に処理を行っている`unmarshalJSON`について説明します。

引数の`src`はUnmarshalしたいJSON、`dst`は結果を格納するための構造体です。汎用的に使えるように`interface{}`にしています。

`str := js.Global().Get("JSON").Call("stringify", src).String()` では、以下の3つの処理を順番に行っています

- `js.Global().Get("JSON")`で`JSON`オブジェクトを取得
- `Call("stringify", src)` で渡されたjsonをjavascriptの空間で文字列に変換
- `Call("stringify", src)` した結果は`js.Value`なので、最後に、`String()`を呼び出してGoのstringに変換

あとは、Goの空間でjson.Unmarshalをしてあげると、javascript側から渡されたjsonを構造体にUnmarshalすることができます。これにより、Go側でJSONのフィールドを参照できるようになります。


# JSONのフィールドに関数を定義して渡す

javascript側で生成したJSONのフィールドに関数が含まれる場合に、その関数をGo側から呼び出す方法です。

以下の例では、javascript側で生成した`test_json`を、Goで定義している`Callback`という関数に渡します。JSONには、"callback"というフィールドが含まれます。これをGoの内部から呼び出します。

```javascript
function TestCallback(){
	var test_json = {
		"val": 101,
		"callback": (twice) => {
			console.log("callback is called")
			console.log(twice)
		}
	}
	Callback(test_json)
}
```

先ほどの方法をそのまま適用すると、JSONを文字列に変換したときに関数を含んでいるフィールドは消滅します。そのため、今回はJSONをjavascriptの空間で文字列に変換するときに、関数を文字列に変換する処理を加えます。そして、その文字列をjavascriptの関数に変換する処理をさらに追加します。

実装は下のようになります。

```go
type TestStruct struct {
	Val      int    `json:"val"`
	Callback string `json:"callback"`
}

func unmarshalJSONwithCallback(src js.Value, dst interface{}) {
	replacerString := `
		if (typeof v === 'function') {
			return v.toString();
		}
		return v;
	`
	replacer := js.Global().Get("Function").New(js.ValueOf("k"), js.ValueOf("v"), js.ValueOf(replacerString))
	str := js.Global().Get("JSON").Call("stringify", src, replacer).String()
	err := json.Unmarshal([]byte(str), &dst)
	if err != nil {
		fmt.Println(err)
	}
}

func getJSFuncFromString(function string) js.Value {
	this := js.Global().Get("this")
	return js.Global().Get("Function").Call("call", this, js.ValueOf("return"+function)).Invoke()
}

func Callback(this js.Value, args []js.Value) interface{} {
	var config TestStruct
	unmarshalJSONwithCallback(args[0], &config)
	callback := getJSFuncFromString(config.Callback)
	callback.Invoke(js.ValueOf(config.Val * 2))
	return nil
}
```

`TestStruct`はjavascriptから受け取った値をUnmarshalするための構造体です。Unmarshalをするために、関数は文字列で持っています。`unmarshalJSONwithCallback`は実際のUnmarshal処理になります。`getJSFuncFromString`は一度文字列に変換した関数をjavascriptの関数に戻すために使います。 `Callback`はGo側から呼び出される関数です。`registerCallbacks`で、`Callback`をjavascript側から呼び出せるように登録します。

まずは、`unmarshalJSONwithCallback`について説明します。

JSONのstringifyは第二引数にオブジェクトを文字列に変換するための関数が指定できるのでそれを活用します。

最初に`replacerString`を定義します。これは、関数を受け取ったときにそれを文字列にして返し、それ以外の時はそのまま値を返すjavasciptの関数を文字列にしたものです。

次に、その`replacerString`をjavascriptの関数に変換します。具体的には、javascript空間の`Function`オブジェクトを取得して新しい関数を生成します。

あとは、先ほどのUnmarshalの処理と同じですが、`stringify`を呼ぶときに生成した関数を引数に追加します。


`getJSFuncFromString` では、`return Function.call(this, 'return ' + v)();` 相当のjavascriptの処理を行っています。これによってjavascriptの関数を生成します。

後はここで生成した関数（実態はjs.Value)に対して、`Invoke()` を呼び出すことで、JSONのフィールドとして定義された関数をGo側から呼び出すことができます。


## そもそもJSONのフィールドで関数を渡すべきか

javascript側から関数を直接渡せば、Go側で関数を直接呼び出せます。

具体的には、下のようにJSONと関数を別々に渡します。

```javascript
function TestPassFunc(){
	var test_json = {
		"val": 101,
	}
	var callback = (twice) => {
			console.log("callback is called")
			console.log(twice)
	}
	PassFunc(test_json, callback)
}
```

そうすると、渡した関数を直接`Invoke()`で呼び出すことができます。実際のオーバーヘッドは計測してませんが、特別な設計上の理由がない場合はJSONのフィールドに関数は入れないほうがよさそうです。

```go
type TestStruct2 struct {
	Val int `json:"val"`
}

func PassFunc(this js.Value, args []js.Value) interface{} {
	var testStruct TestStruct2
	unmarshalJSONwithCallback(args[0], &testStruct)
	args[1].Invoke(js.ValueOf(testStruct.Val * 2))
	return nil
}
```

## Go側から関数を呼び出す際のjavascript側で宣言した変数のスコープについて

Go側から関数を呼び出した時のjavascriptの変数のスコープが、自分が思っているのと少し違いました。
下のように、globalで宣言した`hoge`と、関数内部で宣言した`fuga`があるとします。

```javascript
var hoge = 1
function TestCallback2(){
	var fuga = 0 // not acceptable
		var config = {
		"val": 101,
		"callback": (twice) => {
			console.log("callback is called")
			console.log("hoge = ",hoge)
			console.log("fuga = ", fuga)
		}
	}			
	config.callback(config.val*2)
	Callback(config)
}
```

javascript側関数を呼び出すと、`hoge`も`fuga`参照できます。一方でGo側から呼び出すと、`hoge`は参照できますが`fuga`は参照できませんでした。自分のjavascriptのスコープの知識が足りてないのでおかしなところに気がつかないだけかもしれないです。

```
callback is called
wasm_exec.html:44 hoge =  1
wasm_exec.html:45 fuga =  0
VM28:4 callback is called
VM28:5 hoge =  1
wasm_exec.js:47 panic: JavaScript error: fuga is not defined
wasm_exec.js:47 
wasm_exec.js:47 goroutine 2 [running]:
wasm_exec.js:47 syscall/js.Value.Invoke(0x7ff8000300000023, 0xc057ef8, 0x1, 0x1, 0x125a000d)
wasm_exec.js:47 	/snap/go/4098/src/syscall/js/js.go:317 +0x3a
wasm_exec.js:47 main.Callback(0x0, 0xc01e070, 0x1, 0x1, 0xc01e001, 0x15ba28b0f8cf3d00)
wasm_exec.js:47 	$GOPATH/src/github.com/neko_suki/wasm_callback/callback.go:59 +0x9
wasm_exec.js:47 syscall/js.handleEvent()
wasm_exec.js:47 	/snap/go/4098/src/syscall/js/func.go:90 +0x36
wasm_exec.js:84 exit code: 2
```

# ソース
下記リポジトリにおいてあります。

https://github.com/neko-suki/wasm_callback

# 参考
- JSON.stringify で関数(funciton) も文字列化、JSON.parseでdeserialize する。
  https://takuya-1st.hatenablog.jp/entry/2016/06/05/160306
- Functionを保護してJSON.stringifyを行う
  - https://qiita.com/suetake/items/52ec9d22e978ceb3111c#%E3%81%A8%E3%81%84%E3%81%86%E3%81%93%E3%81%A8%E3%81%A7
- 文字列にした関数を実行する
  - https://takuya-1st.hatenablog.jp/entry/2016/06/07/024300