WebAssemblyでJavaVMを動かす

最近環境が整いつつあるWebAssemblyを触ってみます。
題材としてJavaを実行できる環境を作ってみたいと思います。そうです、Javaアプレットのような、ブラウザで動くJavaインタプリタ(JavaVM)です。

最初に、お断りしておきます。
Javaのバイトコードを解釈するJavaインタプリタではありますが、最低限のバイトコードしか実装していません。また、java/lang配下のほんの一部のクラスしか実装していないので、実用的には使い物にならないです。個人的に、Javaインタプリタの勉強のために作ってみたものです。
期待してこられた方にはすみません。

以下の通り進めていきます。

  • WebAssemblyコードを生成するコンパイラをインストールする。
  • プロジェクトフォルダを作成する
  • WebAssemblyに埋め込むJavaソースコードをコンパイルする。
  • JavaインタプリタのC言語ソースコードをコンパイルする。
  • Webに配置してWebAssemblyに埋め込んだJavaバイトコードの動作確認する。
  • WebAssemblyに埋め込まなかったJavaソースコードをコンパイルする。
  • WebAssemblyに埋め込まなかったJavaバイトコードの動作確認する。

JavaインタプリタのC言語ソースコードとして、「waba」を参考にさせていただきました。非常に勉強になりました。ありがとうございました。

wabasoft
 http://wabasoft.com/

WebAssemblyコンパイラには、「emscripten」を利用しました。
 https://emscripten.org/index.html

以下、続編です。
 WebAssemblyでJavaVMを動かす:補足

emscriptenのインストール

Windows10のWSL環境にインストールしてみました。
ですので、Python2.7を使うのですが、インストール済みでした。

> python --version
Python 2.7.15rc1
> git clone https://github.com/juj/emsdk.git
> cd emsdk
> ./emsdk install latest
> ./emsdk activate latest

これで、WebAssemblyコンパイラである「emcc」がインストールされました。
使うときには、パスを通す必要があるため、以下を実行します。

> source ./emsdk_env.sh --build=Release

プロジェクトフォルダの作成

適当なフォルダを作って作業します。

C言語のソースファイル一式を同フォルダに配置します。

以下のGitHubに上げておきました。

https://github.com/poruruba/javaemu
> git clone https://github.com/poruruba/javaemu.git
> cd javaemu

WebAssemblyに埋め込むJavaソースコードをコンパイルする

Javaソースコードをコンパイルします。
Javaソースコードは2種類に分けて、配置の方法を変えます。
一つ目は、WebAssemblyに埋め込むJavaソースで、もう一つは、WebAssemblyに埋め込まないJavaソースです。
後者は、通常のユーザ作成のJavaソースコードです。一方、java/langなどは通常のユーザは利用するだけで、変更することのないシステムのJavaソースが前者です。また、インタプリタとの連携やC言語でしか処理できないネイティブ関数があるJavaクラスも前者になります。

ソースコードは、java_srcのフォルダ配下に作っていくことを前提としています。
コンパイルしたクラスファイルを置くフォルダを作ります。pre_classesとします。

> mkdir pre_classes

コンパイルは以下の通りです。

>javac -encoding UTF8 -d pre_classes -sourcepath java_src java_src/base/framework/*.java java_src/java/lang/*.java java_src/test/*.java

base/frameworkとjava/lang配下のJavaソースファイルをシステムのJavaソースとしています。java_src/test/*.java は、後でテスト用に使うものですので、テストが終わったら抜いてください。
これで、pre_classesフォルダ配下に、Javaクラスファイルができあがりました。

Windows上でOracle JDKを使いました。OpenJDKの場合、実行環境上のクラスファイルとバッティングしてしまって、排他する方法がわかりませんでした。。。

C言語ソースコードをコンパイルする

さあ、コンパイルしましょう

> emcc *.c -s WASM=1 -o javaemu.js -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'UTF8ArrayToString', 'stringToUTF8Array']" --preload-file pre_classes

(参考)コンパイル時オプション
 https://emscripten.org/docs/tools_reference/emcc.html#emccdoc

-s WASM=1 はWASMファイルを生成することを示すおまじないです。

-o javaemu.js は、WASMファイル(javaemu.wasm)とユーティリティのJavascript(javaemu.js)とWebAssemblyに埋め込むJavaクラスファイルをまとめたファイル(javaemu.data)を生成するためのものです。
javaemuという名前は適当です。好きな名前にしてください。

-s EXTRA_EXPORTED_RUNTIME_METHODS は、javaemu.jsが提供する関数のうち、Webに配置するHTMLページのJavascriptから呼び出すことを許可する関数名です。ccallは必須ですが、それ以外の2つは有用なので設定しました。

–preload-file は、WebAssemblyに埋め込むファイルが置かれたフォルダ名を指定します。指定したフォルダ名配下のファイルが、javaemu.dataというファイルにまとめられます。

出来上がったファイルのうちWebの配置するのは、「javaemu.wasm」と「javaemu.js」と「javaemu.data」です。
javaemu.wasmが、まさしくC言語をコンパイルしたWebAssemblyファイルです。

WebページのJavascriptから呼び出される関数は、main.cに定義しています。

main.c

int EMSCRIPTEN_KEEPALIVE setInoutBuffer(const unsigned char *buffer, long bufferSize );
int EMSCRIPTEN_KEEPALIVE setRomImage(unsigned char *romImage, long romSize );
int EMSCRIPTEN_KEEPALIVE callStaticMain(char *className, char *param );

Webに配置してWebAssemblyに埋め込んだJavaバイトコードの動作確認する。

GitHubから落としてきたファイルのうちのhtmlをWebサーバにアップします。
動作確認のためのページも用意しておきました。
さきほど生成した3つのファイルも、htmlフォルダに配置します。

> cp javaemu.wasm javaemu.js javaemu.data html/

または

> cd html
> ln -s ../javaemu.wasm ../javaemu.js ../javaemu.data .
> cd ..

(HTMLのルートフォルダにおいてください。そうしないと、javaemu.jsがうまくwasmやdataファイルを読んでくれないです)

ありがたいことに、emscriptenには簡易Webサーバ機能があるので、それを使います。ポート番号は8080にしました。

emrun –serve_root html –no_browser –port 8080 .

それでは、ブラウザからアクセスしてみます。
ブラウザとして、Windows上のChromeで動かしてみました。

 http://localhost:8080

image.png

わかりやすいように、F12を押して開発者コンソールを表示しています。

Class Nameのこところに、テスト用に埋め込んだJavaクラス名を入力します。「test/TestFunc」です。

もととなったソースコードは以下の感じです。

TestFunc.java

package test;

import base.framework.System;
import base.framework.Convert;
import base.framework.Util;

public class TestFunc{
  public static void main( String[] args ){
    try
    {
        System.println("Hello test/TestFunc");

        if( args.length >= 1 )
            System.println("args[0]=" + args[0]);

        String[] input = System.getInput(-1);
        for( int i = 0 ; i < input.length ; i++ )
            System.println("params[" + i + "]=" + input[i]);

        System.setOutput(new String[]{ "World", "こんばんは" });
    }catch( Exception ex )
    {
        System.print( ex.toString() );
        ex.printStackTrace();
    }
  }
}

Input ArgやInput Paramsにも適当な文字列をいれます。
さっそく「start wasm」ボタンを押下します。

以下の感じで、C言語で実装した関数を呼び出します。
WebAssemblyファイルをロードして実行するには、手間がかかるのですが、そこはjavaemu.jsがうまくやってくれています。

start.js

            this.output_return = Module.ccall('callStaticMain', // name of C function 
                                    "number", // return type
                                    ["string", "string"], // argument types
                                    [this.class_name, this.input_arg]); // arguments
image.png

すると、ブラウザのConsoleにごちゃごちゃデバッグ文字が表示された中に、以下のような文字列が表示されたかと思います。JavaインタプリタによるJavaクラスファイルの実行完了です。

Hello test/TestFunc
args[0]=ABCD
params[]=あいうえお
params[1]=かきくけこ

また、ページにも以下が表示されたかと思います。

Output Return 0
Output Params [ “World”, “こんばんは” ]

WebAssemblyに埋め込まなかったJavaソースコードをコンパイルする。

今度は、いろいろなJavaソースを作成して動かしてみましょう。

java_src 配下に作っていきます。
今回は、test2フォルダ以下に作っていきます。

コンパイルは以下の通りです。

> mkdir classes
> javac -encoding UTF8 -d classes -sourcepath java_src -classpath pre_classes java_src/test2/*.java

クラスファイル格納用に、フォルダclassesを作っています。
pre_classesはWebAssemblyファイルに埋め込まれている前提なので、すでにあるクラスファイルとみなしてコンパイルします。

次に、作ったクラスファイルをJarファイルにします。

> jar cvf classes.jar -C classes .

これで、classesフォルダ配下のファイルが、classes.jarファイルにまとめられました。test2のクラスファイル以外のファイルも作られてましたが、実害はないので無視してもいいですし、Jarファイル対象から除いてもよいです。

「ファイルを選択」ボタンを押下し、先ほど作成した「classes.jar」を指定します。
そうするとその下に、Jarファイルに含まれるクラスファイル一覧が表示されます。

Jarファイルの展開、すなわち、ZIPファイルの展開には、zlib.jsのunzip.min.jsを使わせていただきました。

zlib.js
 https://github.com/imaya/zlib.js

それではもう一度、ブラウザから実行します。

image.png

Class Nameに、今度はJarファイルに含まれるtest2/TestFuncを指定して、「start wasm」ボタンを押下して実行します。

コンソールに以下のように表示されましたでしょうか。test2/TestFuncとなって、該当ソースが実行されているのがわかります。

Hello test2/TestFunc
args[0]=ABCD
[]=あいうえお
[1]=かきくけこ