前回の記事 WebAssemblyでJavaVMを動かす で、WebAssemblyを使ったJavaインタプリタ(JavaVM)を作ってみましたが、のちのちのため、ちょっとした工夫や、拡張する際の参考となるように、備忘録を残しておきます。
ちなみに、WebAssemblyコンパイラは「Emscripten」を前提としています。
Javaインタプリタ(JavaVM)のC言語のソースファイル等々一式を、以下のGitHubに上げておきました。
https://github.com/poruruba/javaemu
JavascriptとC言語の連携
以下の連携を使っています。
- JavascriptからC言語関数の呼び出し
- JavascriptとC言語間でメモリの共有
※C言語からJavascript関数の呼び出しもできるようですが、今回は使っていません。
JavascriptからC言語関数の呼び出し
まずは、呼び出されるC言語側での関数定義です。
関数定義に、EMSCRIPTEN_KEEPALIVE を付けます。これだけです。
#include <emscripten/emscripten.h>
#ifdef __cplusplus
extern "C" {
#endif
int EMSCRIPTEN_KEEPALIVE setInoutBuffer(const unsigned char *buffer, long bufferSize );
int EMSCRIPTEN_KEEPALIVE setRomImage(const unsigned char *romImage, long romSize );
int EMSCRIPTEN_KEEPALIVE callStaticMain(char *className, char *param );
#ifdef __cplusplus
}
#endif
(ただ、C言語からJavascriptへの戻り値の表現力が乏しいので、のちに説明する共有するメモリで、パラメータをやり取りします。)
Javascript側からの呼び出しは以下の感じです。
emccでコンパイル時に生成される javaemu.js で定義されているユーティリティ「Module」が活躍します。
var ret = Module.ccall('setRomImage', "number", ["number", "number"], [g_pointer, bin.length]);
2番目の引数は、C言語関数の戻り値の型を示しています。
3番目の引数は、C言語関数の引数のリストの型を示しています。
4番目の引数は、実際にC言語関数の引数に渡す値です。
ccallという関数は、コンパイル時に指定した関数のうちの一つです。
s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'UTF8ArrayToString', 'stringToUTF8Array']"
JavascriptとC言語間でメモリの共有
以下の感じで使っています。
// 例えば、var bin = { array: [1, 2, 3, 4] };
g_pointer = Module._malloc(bin.array.length);
Module.HEAPU8.set(bin.array, g_pointer);
var ret = Module.ccall('setRomImage', "number", ["number", "number"], [g_pointer, bin.array.length]);
Module._malloc の呼び出しで、共有したいメモリを確保します。unsigned charの配列の場合はこれでよいですが、shortやlongの場合には、それに合わせて領域サイズを指定する必要があります。
Module.HEAPU8.set の呼び出しで、Javascript側がセットしたい配列の値をメモリにセットします。
unsigned charの配列としたため、メモリアクセス時には、HEAPU8というマクロを使っています。
以下のようにしても、セットできます。
Module.HEAPU8[g_pointer + ptr] = 123;
C言語側では、setRomImage の関数の中で、共有するメモリのアドレスを覚えておき、以降、参照したり設定したりします。
int EMSCRIPTEN_KEEPALIVE setRomImage(const unsigned char *romImage, long romSize )
{
FUNC_CALL();
classRom_ext = (unsigned char*)romImage;
classRomSize_ext = romSize;
FUNC_RETURN();
return FT_ERR_OK;
}
関数 setRomImage は、Jarファイル内のクラスファイルの共有用に、関数 setInoutBuffer は、関数 callStaticMain の呼び出し時の引数および C言語側からの戻り値の交換用に使っています。
fopenの疑似動作
C言語のfopenも利用可能ですが、ブラウザがSandbox動作のため、PC上のローカルファイルにアクセスすることはできません。
その代わり、コンパイル時のフォルダ構成とファイルをスナップショットとしてイメージファイル化(javaemu.data)し、実行時にfopenで読み出すことができます。
(参考情報)
https://emscripten.org/docs/porting/files/index.html
以下、その部分の抜粋です。
#define PRECLASS_DIR "pre_classes"
char *baseClassDir = PRECLASS_DIR;
static long file_read(const char *p_fname, unsigned char **pp_bin, long *p_size)
{
FILE *fp;
long fsize;
char path[255];
strcpy(path, baseClassDir);
strcat(path, "/");
strcat(path, p_fname);
strcat(path, ".class");
fp = fopen(path, "rb");
if (fp == NULL)
return FT_ERR_NOTFOUND;
// ・・・
pre_classes というのは、emccでコンパイル時に指定していたあれです。
emcc *.c -s WASM=1 -o javaemu.js -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'UTF8ArrayToString', 'stringToUTF8Array']" --preload-file pre_classes
Javaインタプリタの補足
WebAssemblyではなく、実装したJavaインタプリタの実装仕様です。
Java関数の呼び出し
関数 callStaticMain を利用します。
int EMSCRIPTEN_KEEPALIVE callStaticMain(char *className, char *param )
このなかで、JavaインタプリタであるJavaVMを初期化し、以下の部分で、Javaクラスの関数を呼び出しています。
ret = startStaticMain(className, param, &retType, &retVar);
引数に、呼び出すクラス名と渡したい引数を設定します。
これで呼び出されるJava側の関数名と型は固定にしています。
public static void main( String[] args ){
ですので、対象クラスのJavaソースで、上記の関数名を実装しておく必要があります。このJava関数に、処理したいロジックを実装します。
引数のやりとりは、
String[] input = System.getInput(-1);
で、Javascript側からの引数を取得し、
System.setOutput(new String[]{ "World", "こんばんは" });
で、Javascript側に戻り値を返します。
Javaインタプリタのネイティブ拡張
Java実装である程度のことはできますが、やはりC言語を使ってネイティブ処理を実装したいところです。
そのためには、Javaソースに以下のような関数を定義します。関数名は何でもよいです。要は、native static という修飾子を付けます。
public native static final String[] getInput(int max);
そうすると、まだネイティブ実装していない場合は、ブラウザの開発者コンソールに、以下のようなエラーが表示されてインタプリタが終了します。
** Native Method Missing:
javaemu.js:2178 // base/framework/System_getInput_(I)[Ljava/lang/String;
javaemu.js:2178 { 320190814UL, func },
ネイティブ関数の定義がないという意味です。
C言語でネイティブ関数を実装し、実装したC言語関数のポインタをwaba_native.c の以下の部分に設定します。
NativeMethod nativeMethods[] = {
// ・・・・・
// base/framework/System_printStackTrace_()V
{ 320187986UL, FCSystem_printStackTrace },
// base/framework/System_getInput_(I)[Ljava/lang/String; // ★追加
{ 320190814UL, FCSystem_getInput }, // ★追加
// base/framework/System_sleep_(I)I
{ 320192265UL, FCSystem_sleep },
// ・・・・・
};
NativeMethod nativeMethods[] に、C言語のネイティブ関数の実装が並んでいます。
ハッシュ値の昇順に並べていますので、適切な場所に差し込みます。(そうしないとインタプリタが見つけてくれません)
C言語のネイティブ関数の実装は、例えば以下のような感じです。
// base/framework/System_getInput_(I)[Ljava/lang/String;
long FCSystem_getInput(Var stack[]) {
Var v;
unsigned char num;
unsigned char i;
WObject strArray;
WObject *obj;
unsigned long ptr;
long max;
if (inoutBuff_ext == NULL)
return ERR_CondNotSatisfied;
max = stack[0].intValue;
num = inoutBuff_ext[0];
if (max >= 0 && num > max)
num = (unsigned char)max;
strArray = createArrayObject(TYPE_OBJECT, num);
obj = (WObject *)WOBJ_arrayStart(strArray);
if (pushObject(strArray) != FT_ERR_OK)
return ERR_OutOfObjectMem;
ptr = 1;
for (i = 0; i < num; i++) {
obj[i] = createString((const char*)&inoutBuff_ext[ptr]);
if (obj[i] == WOBJECT_NULL) {
popObject();
return ERR_OutOfObjectMem;
}
ptr += strlen((const char*)&inoutBuff_ext[ptr]) + 1;
}
popObject();
v.obj = strArray;
stack[0] = v;
return 0;
}
他のネイティブ関数を参考にしてみてください。
入力引数は、Var stack[]に積まれています。レスポンスは、stack[0]に設定します。(←なので、引数無しのネイティブ関数は作れないようになっています)
問題なければ、return 0; を返します。
Javaインタプリタ(JavaVM)の制限
JavaVMの機能は限定的です。
たとえば、float、double、longを使ったバイトコードはサポートしていません。
以下の関数を見ていただければわかりますが、上記のバイトコードはことごとくコメントアウトしています。
long executeMethod(WClass *wclass, WClassMethod *method, Var params[], unsigned short numParams, unsigned char *retType, Var* retValue) {
// ・・・
// NOTE: this is the full list of unsupported opcodes. Adding all
// these cases here does not cause the VM executable code to be any
// larger, it just makes sure that the compiler uses a jump table
// with no spaces in it to make sure performance is as good as we
// can get (tested under Codewarrior for PalmOS).
/*
case OP_lconst_0:
case OP_lconst_1:
case OP_dconst_0:
// ・・・
以上