前回 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/

https://github.com/tinygo-org/tinygo/issues/3010