LoginSignup
1
0

More than 3 years have passed since last update.

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

Posted at

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側関数を呼び出すと、hogefuga参照できます。一方で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

ソース

下記リポジトリにおいてあります。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0