前回 https://kajindowsxp.com/go-tinygo-webassembly/
の続きです
WebAssemblyの仕様には文字列型が存在しないので、普通にexportしただけだとint型などの限られた型のみしか使用できない。
そこで「JavaScript」と「GoのWebAssembly」の間で共有されているメモリ領域を使ってstring文字列をやり取りする。
JavaScriptの構造
前回までの復習となるが、JavaScriptの全体の構造はこんな感じ
main.jsとして保存する
require("./wasm_exec_tiny");
const fs = require("fs");
async function callWasm() {
const go = new Go();
const obj = await WebAssembly.instantiate(
fs.readFileSync("./wasmbin"),
go.importObject
);
const wasm = obj.instance;
// ここにコードを書いていく
}
callWasm()
JSからGoにstringを引数で渡す場合
Go側実装
最初にGo側(文字列を受け取る側)を実装する。
ファイル名はmain.goにする
まず、JavaScriptから書き込めるように、空のbyte配列を作成する。
その先頭アドレスを返すgetBuffer()
という関数も作成し、exportしておく。
package main
func main() {}
var buf [1024]byte
//export getBuffer
func getBuffer() *byte {
return &buf[0]
}
また、今回はサンプルとして、引数の文字列の長さを返す関数を定義しておく。
//export stringFromJS
func stringFromJS(message string) int {
return len(message)
}
JS側実装
次にJavaScript側を実装する。
textとmodule(wasm)を受け取り、共有メモリに文字列を書き込む関数を作成する。
function writeBuffer(text, module) {
const addr = module.exports.getBuffer();
const buffer = module.exports.memory.buffer;
const mem = new Int8Array(buffer);
const view = mem.subarray(addr, addr + text.length);
for (let i = 0; i < text.length; i++) {
view[i] = text.charCodeAt(i);
}
return [addr, text.length];
}
この関数を使い、「先頭アドレス」と「バッファの長さ」を取得する
以下からのコードは「// ここにコードを書いていく」の部分に追加する
const text = "Hello World!";
const [addr, length] = writeBuffer(text, wasm);
Goの関数を呼び出す。
const result = wasm.exports.stringFromJS(addr, length);
console.log(result);
// Output: 12
ビルド&実行
wasm_exec_tinyが必要なので以下コマンドで持ってくる
$ cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js wasm_exec_tiny.js
Goで実装した通り、「Hello World!」の文字数12がコンソールに出力されることがわかる。
$ tinygo build -o wasmbin -target wasm main.go
$ node main.js
12
JavaScriptからwasm.exports.stringFromJS(addr, length)
のように呼び出すと、addr, lengthの部分がstringに変換され、特別な記述なしに、stringを受け取ることが可能であることがわかった。func stringFromJS(message string){ ...
GoからJSにstringを返り値で渡す場合
Go側実装
まずstringを受け取り、新しく作成した[]byte変数に書き込んだ後、そのポインタと長さを返す関数を作成する。
import "unsafe"
func stringToPtr(s string) (uint32, uint32) {
buf := []byte(s)
ptr := &buf[0]
unsafePtr := uintptr(unsafe.Pointer(ptr))
return uint32(unsafePtr), uint32(len(buf))
}
さらにこの関数を使って、
stringToJS
任意の文字列を「ポインタ」と「長さ」に変換した後「ポインタ」をJavaScriptに返す関数getBufSize
「長さ」をJavaScriptに返す関数
の2つを定義する
(多値返しに対応していれば2つ同時に返したかった…)
var bufSize uint32
//export stringToJS
func stringToJS() uint32 {
ptr, size := stringToPtr("Sample Str")
bufSize = size
return ptr
}
//export getBufSize
func getBufSize() uint32 {
return bufSize
}
JS側の実装
Goとの共有メモリを読み込む関数を作成する。
addrはGo側で書き込んだstringの先頭アドレス。
lengthはアドレスの長さ。
function readBuffer(addr, size, module) {
let memory = module.exports.memory;
let bytes = memory.buffer.slice(addr, addr + size);
let text = String.fromCharCode.apply(null, new Int8Array(bytes));
return text;
}
以下からのコードは「// ここにコードを書いていく」の部分に追加する
2回に分けて先頭アドレスaddr とバイトの長さlengthを取得し、readBufferで読み込んだ後出力する
const addr = wasm.exports.stringToJS();
const length = wasm.exports.getBufSize();
const result = readBuffer(addr, length, wasm);
console.log(result);
ビルド&実行
$ cp $(tinygo env TINYGOROOT)/targets/wasm_exec.js wasm_exec_tiny.js
$ tinygo build -o wasmbin -target wasm main.go
$ node main.js
Sample Str
全体のソースコード
GitHubにもあります
https://github.com/kajikentaro/wasm-go-tinygo-sample/tree/master/tinygo-string
main.go
package main
import (
"unsafe"
)
var buf [1024]byte
var bufSize uint32
//export stringToJS
func stringToJS() uint32 {
ptr, size := stringToPtr("Sample Str")
bufSize = size
return ptr
}
//export getBufSize
func getBufSize() uint32 {
return bufSize
}
func stringToPtr(s string) (uint32, uint32) {
buf := []byte(s)
ptr := &buf[0]
unsafePtr := uintptr(unsafe.Pointer(ptr))
return uint32(unsafePtr), uint32(len(buf))
}
//export stringFromJS
func stringFromJS(message string) int {
return len(message)
}
//export getBuffer
func getBuffer() *byte {
return &buf[0]
}
func main() {}
main.js
require("./wasm_exec_tiny");
const fs = require("fs");
async function callWasm() {
const go = new Go();
const obj = await WebAssembly.instantiate(
fs.readFileSync("./wasmbin"),
go.importObject
);
const wasm = obj.instance;
{
// GoにStringを渡す
const text = "Hello World!";
const [addr, length] = writeBuffer(text, wasm);
const result = wasm.exports.stringFromJS(addr, length);
console.log(result); // Example: 文字数が帰ってくるように実装
// Output: 12
}
{
// GoからStringをもらう
const addr = wasm.exports.stringToJS();
const length = wasm.exports.getBufSize();
const result = readBuffer(addr, length, wasm);
console.log(result);
// Output: Sample Str
}
}
// 共有メモリを読み込む
function readBuffer(addr, size, module) {
let memory = module.exports.memory;
let bytes = memory.buffer.slice(addr, addr + size);
let text = String.fromCharCode.apply(null, new Int8Array(bytes));
return text;
}
// 共有メモリに書き込む
function writeBuffer(text, module) {
// Get the address of the writable memory.
const addr = module.exports.getBuffer();
const buffer = module.exports.memory.buffer;
const mem = new Int8Array(buffer);
const view = mem.subarray(addr, addr + text.length);
for (let i = 0; i < text.length; i++) {
view[i] = text.charCodeAt(i);
}
// Return the address we started at.
return [addr, text.length];
}
callWasm();
参考
https://www.alcarney.me/blog/2020/passing-strings-between-tinygo-wasm/