【WebAssembly】wasm側で動的に作成したインスタンスをJSに渡す

f:id:shogonir:20170514025514p:plain

目次

  1. この記事の目的
  2. JSに渡したいクラスをC++で定義する
  3. wasmのソースコードを実装する
  4. JS側でポインタからインスタンスを読み込む
  5. HTMLからwasmを読み込んで実行する
  6. まとめ

 

1. この記事の目的

この記事では、wasm側で作成したインスタンスをJSに渡す方法を紹介します。

以下のような例で、サンプルコードも載せながら説明します。
C++でPointという名前のx, y-座標系上の点を表すクラスを定義します。
Pointがもつ情報はx, y座標と、これだけだと単純すぎるのでidをもたせます。
wasmに角度を渡すと、指定された角度に対応する単位円上の点を返します。
idは自動でユニークなのIDを振るのが面倒だったので固定で4423にします。

 
GitHubにソースを上げました。
github.com

 

2. JSに渡したいクラスをC++で定義する

Pointクラスを定義します。

まずはヘッダーからです。(point.h)

class Point
{
public:
    Point(float x, float y);
public:
    float x;
    float y;
    short id;
};

 
次に実装です。(point.cpp)

#include "point.h"

Point::Point(float x, float y): x(x), y(y) {
    this->id = 4423;
}

 

3. wasmのソースコードを実装する

下記の2つの関数を実装します。

  • Point* angleToPoint(float angle)
    • float型の角度を受け取って、指定された角度に対応する単位円上の点を返す
  • void freePoint(Point*)
    • 受け取ったポインタで確保しているPointインスタンスの領域を解放

 
実際のソースコードは下記のようになりました。

#include <cmath>

#include "point.cpp"

extern "C" {

    Point * angleToPoint(float theta) {
        float radian = theta * 3.1415926 / 180;
        float x = std::cos(radian);
        float y = std::sin(radian);
        Point * point = new Point(x, y);
        return point;
    }

    void freePoint(Point * point) {
        delete point;
    }
}

 

4. JS側でポインタからインスタンスを読み込む

wasmから受け取ったポインタをもとに、インスタンスの情報を読み込む処理は下記のようになります。

function pointerToPoint(pointer) {
  const POINT_OFFSET_X = pointer / 4;
  const POINT_OFFSET_Y = pointer / 4 + 1;
  const POINT_OFFSET_ID = pointer / 2 + 4;
  var point = {}
  point.x = module.HEAPF32[POINT_OFFSET_X];
  point.y = module.HEAPF32[POINT_OFFSET_Y];
  point.id = module.HEAP16[POINT_OFFSET_ID];
  return point;
}

 
この処理について解説します。
wasmのメモリへアクセスする方法については、こちらの3章を先に参照してください。

まずX座標ですが、これはHEAPF32の先頭からpointer / 4番目に格納されています。
これはfloat型が4byteであるためです。

次にY座標です、こちらはX座標の次にあるのでHEAPF32のpointer / 4 + 1番目です。

最後にidですが、これはHEAP16のpointer / 2 + 4番目になります。
idはshort型なのでHEAP16にアクセスする必要があり、pointer / 2から8byteはx, yが格納されていますので、2byteのshort型4つ先にidが格納されているはずです。

最後に読み込んだ情報をpointというObjectに格納して返しています。

 

5. HTMLからwasmを読み込んで実行する

wasmを読み込んでじっこうするHTMLは以下のようになります。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <title>share instance</title>
  </head>
  <body>
    <script src="share.js"></script>
    <script>

var module;

function pointerToPoint(pointer) {
  const POINT_OFFSET_X = pointer / 4;
  const POINT_OFFSET_Y = pointer / 4 + 1;
  const POINT_OFFSET_ID = pointer / 2 + 4;
  var point = {};
  point.x = module.HEAPF32[POINT_OFFSET_X];
  point.y = module.HEAPF32[POINT_OFFSET_Y];
  point.id = module.HEAP16[POINT_OFFSET_ID];
  return point;
}

fetch('share.wasm')
  .then(response => response.arrayBuffer())
  .then(buffer => new Uint8Array(buffer))
  .then(binary => {
    var moduleArgs = {
      wasmBinary: binary,
      onRuntimeInitialized: function () {
        var angleToPoint = module.cwrap('angleToPoint', 'number', ['number', 'number']);
        var freePoint = module.cwrap('freePoint', null, ['number']);
        var pointer = angleToPoint(30);
        var point = pointerToPoint(pointer);
        console.log(point);
        freePoint(pointer);
      }
    };
    module = Module(moduleArgs);
  });

    </script>
  </body>
</html>

 
onRuntimeInitializedのタイミングで、cwrapを用いて関数を取り出しています。
角度を30度に設定して、Pointのインスタンスのポインタをpointerに格納します。
それをpointerToPoint(number)に渡してpointにオブジェクトを格納します。
インスタンスの情報を読み込んで、pointerが不要になったらfreePoint()を実行するのを忘れないようにしてください。
これを忘れるとメモリリークになってしまいます。

 
もしメモリリークをJSで気にしたくない場合、次の方法が役に立つかもしれません。

 
http-serverを実行してブラウザで確認すると下記のようになりました。
(x, y, id) = (sqrt(3)/2, ½, 4423) のオブジェクトが表示されていることがわかります。
f:id:shogonir:20170521032022p:plain

 

6. まとめ

この記事ではwasm側で作成したインスタンスの情報をJSに渡す方法を紹介しました。
もし複雑なデータを一気にやり取りしたい場合には役に立つはずです。

しかし、複数の型のフィールドを持つクラスのインスタンスをやり取りするのは、JS側の実装コストが高く感じました。
C++のクラス定義を変更するたびに、JSのコードにも変更が必要になるのもいただけません。
できるだけ単純な構造でデータをやりとりしたり、複数の型を使うにしてもfloatとintなど同じバイト数のものを使うと良いかもしれません。