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
という関数に渡します。
function TestJson(){
var test_json = {
"Val" : 1,
"Str" : "abcde",
"Float" : 0.5
}
CheckJson(test_json)
}
go1.12.7 の時点のsyscall/js
のドキュメント にはJSONを扱う方法は書いてないです。1.13のsyscall/js
の変更点を見た限りでも特に対応される様子はなさそうでした。
本記事では、Go側で受け取ったJSONを一度文字列に変換してからGoの構造体にUnmarshalします。javascriptから渡された値は、js.Value
として扱われます。syscall/js
にはjs.Value
をstringに変換するためのString()
メソッドが用意されているためそれを利用しました。
実装は下のようになります。
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の内部から呼び出します。
function TestCallback(){
var test_json = {
"val": 101,
"callback": (twice) => {
console.log("callback is called")
console.log(twice)
}
}
Callback(test_json)
}
先ほどの方法をそのまま適用すると、JSONを文字列に変換したときに関数を含んでいるフィールドは消滅します。そのため、今回はJSONをjavascriptの空間で文字列に変換するときに、関数を文字列に変換する処理を加えます。そして、その文字列をjavascriptの関数に変換する処理をさらに追加します。
実装は下のようになります。
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と関数を別々に渡します。
function TestPassFunc(){
var test_json = {
"val": 101,
}
var callback = (twice) => {
console.log("callback is called")
console.log(twice)
}
PassFunc(test_json, callback)
}
そうすると、渡した関数を直接Invoke()
で呼び出すことができます。実際のオーバーヘッドは計測してませんが、特別な設計上の理由がない場合はJSONのフィールドに関数は入れないほうがよさそうです。
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
があるとします。
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
ソース
下記リポジトリにおいてあります。
参考
- JSON.stringify で関数(funciton) も文字列化、JSON.parseでdeserialize する。
https://takuya-1st.hatenablog.jp/entry/2016/06/05/160306 - Functionを保護してJSON.stringifyを行う
- 文字列にした関数を実行する