目次へ戻る

3. MRuby on Android

2012-05-30 (鈴)

Web 閲覧用ソース:

オプションとして jni/Application.mk を使ってもよい:

APP_ABI := all

このファイルを置いてビルドすると NDK がサポートするすべてのプラットフォーム (android-ndk-r8 では ARM, ARM v7a, MIPS, x86) 向けのバイナリが作られる。 開発時には余計な手間になるから,配布ソースには無効なファイル名 jni/Application.mk~ にして同梱した。

1 はじめに

第1章第2章では NDK と mruby を紹介した。 いよいよ NDK を使って mruby を Android で動かしてみよう。 ここでは GNU Prolog for Java on Android (右図) の Java による Prolog インタープリタの部分を C 言語による mruby インタープリタに置き換えたものを作ることを目標とする。

プロジェクトは Android 開発手順 3.応用編 「3.1 プロジェクトの新規作成」の手順で新規に作ることができる。

あるいは既存のプロジェクト・ディレクトリをコピーし,第1章 3.最初の NDK プログラム にあるように android update project でプロジェクトに仕立てることができる。

ここではそうやって GNU Prolog for Java on Android と同じようなソース・コードからなるプロジェクトを作ったとする。

2 jni ディレクトリと Android.mk

プロジェクト・ディレクトリの直下に jni ディレクトリを作り, 第2章で説明した方法などで取得した mruby のソースを展開する。 mruby という名前のシンボリック・リンクを作る。

01:~/mruby-oa$ mkdir jni
01:~/mruby-oa$ cd jni
01:~/mruby-oa/jni$ tar xf ~/mruby-mruby-2d73abd.tar.gz
01:~/mruby-oa/jni$ ln -s mruby-mruby-2d73abd mruby
01:~/mruby-oa/jni$  
配布ソースには 5月29日の朝に取得した mruby-mruby-2d73abd を同梱してあります。 Cygwin を使う場合は,NDK から Cygwin のシンボリック・リンクが認識されませんから,ディレクトリ名そのものを mruby にしてください。
01:~/mruby-oa/jni$ rm mruby
01:~/mruby-oa/jni$ mv mruby-mruby-2d73abd mruby
01:~/mruby-oa/jni$  

下記のような Android.mk を置く。

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := mruby-oa

LOCAL_CFLAGS := -Wall
LOCAL_C_INCLUDES := jni/mruby/include jni/mruby/src
#LOCAL_LDLIBS := -llog

LOCAL_SRC_FILES := mruby-oa.c mruby/mrblib/mrblib.c
LOCAL_SRC_FILES += $(sort $(shell cd jni; echo mruby/src/*.c) \
                        mruby/src/y.tab.c)

MY_SRC_FILES := $(addprefix jni/, $(LOCAL_SRC_FILES))

$(MY_SRC_FILES): jni/mruby/include/mruby.h.orig

jni/mruby/include/mruby.h.orig:
        cd jni; patch -b mruby/include/mruby.h mruby.h.diff
        cd jni/mruby; make

clobber: clean
        cd jni/mruby/include;\
          if [ -f mruby.h.orig ]; then mv mruby.h.orig mruby.h; fi
        cd jni/mruby; make clean

include $(BUILD_SHARED_LIBRARY)

ここで LOCAL_SRC_FILES+= で追加している $(sort $(shell cd jni; echo mruby/src/*.c) mruby/src/y.tab.c) は,GNU make の shell 関数を使って jni/mruby/src の下にある *.c すべてを mruby/src/*.c という相対パスとして取得する。 はじめは y.tab.c がまだ bison から作られていないから,mruby/src/y.tab.c を陽に sort 関数の引数として追加する。 sort 関数は,与えられた名前の並びをソートするとき,重複する要素をひとつにまとめるから,y.tab.c の生成後にビルドした場合でも mruby/src/y.tab.c が並びに1個しか出現しないことが保証される。

MY_SRC_FILES := $(addprefix jni/, $(LOCAL_SRC_FILES)LOCAL_SRC_FILES の各要素に jni/ を接頭した並びを左辺の変数にセットする。 したがって,$(MY_SRC_FILES): jni/mruby/include/mruby.h.orig としてソース・ファイルのすべてを jni/mruby/include/mruby.h.orig に依存させることができる。 こうして,ビルド開始時に jni/mruby/include/mruby.h.orig を構築するためのルールを発動させる。

ルールではヘッダ・ファイル jni/mruby/include/mruby.h にパッチ jni/mruby.h.diff (次節参照) を適用する。 このとき副作用として名目上のターゲットである jni/mruby/include/mruby.h.orig が生成される。 ルールではパッチ適用後,ndk-build を実行しているホスト・マシン向けに mruby を make する。 第2章で説明したように bison により y.tab.c が生成され,mruby 中間言語コンパイラによりブートストラップで mrblib.c が生成される。

こうしてビルド開始時に y.tab.cmrblib.c を含む mruby の全ソースがそろうから, Java ネイティブ・メソッド実装用の jni/mruby-oa.c (次節参照) とあわせて,問題なくビルドが続行される。

この方法は,生成される mrblic.c に配列の形で含まれる中間言語がホスト・マシンと Android マシンのワード長などの CPU アーキテクチャの違いから独立であることに依存しています。 実際,64 ビット版 Mac OS X 10.7.4 でビルドしても ARM および x86 アーキテクチャの Android エミュレータで動作します。

clobber は,より徹底的に clean するルールに対して付ける Unix make の伝統的なルール名である。 ここでは,通常の clean に加え,mruby.h をパッチ適用前に戻し,ホスト・マシン向けの mruby の構築を取り消すように定義した。

3 Ruby から Java へのコールバックと mruby.h へのパッチ

インタープリタをアプリケーションに組み込むとき, 最も重要な課題の一つは,インタープリタからのアプリケーションの機能の呼び出しをいかに実現するかである。 これをかなえなければ,本当の意味で mruby を Android に載せたということはできない。

第1章で見たように JNI 関数を利用するには,Java ネイティブ・メソッドの呼び出しごとに与えられる JNIEnv へのポインタなどが必要である。 しかし,mruby の組込み Ruby メソッドの実装関数には,インタープリタ本体に該当する mrb_state へのポインタと,メソッドのレシーバ (つまり self) に該当する mrb_value 値が渡されるだけである。

Ruby の Array クラスを実現する jni/mruby/src/array.c から rindex メソッドの C 言語実装を例として示します。 実装関数 mrb_ary_rindex_m が二つの仮引数 mrb と self をとることに注意してください。
mrb_value
mrb_ary_rindex_m(mrb_state *mrb, mrb_value self)
{
  mrb_value obj;
  long i;

  mrb_get_args(mrb, "o", &obj);
  for (i = RARRAY_LEN(self) - 1; i >= 0; i--) {
    if (mrb_equal(mrb, RARRAY_PTR(self)[i], obj)) {
      return mrb_fixnum_value(i);
    }
  }
  return mrb_nil_value();
}
  mrb_define_method(mrb, a, "rindex", mrb_ary_rindex_m, ARGS_REQ(1)); /* 15.2.12.5.26 */

そこで,下記のパッチ jni/mruby.h.diffjni/mruby/include/mruby.h に適用して,mrb_state 構造体に JNI 用の文脈情報へのポインタを持たせることにした。 パッチの適用は前節で説明したルールにより,ndk-build 時に自動的に行われる。

--- mruby.h~	2012-05-23 10:31:02.000000000 +0900
+++ mruby.h	2012-05-23 15:15:08.000000000 +0900
@@ -216,6 +216,8 @@
   GC_STATE_SWEEP
 };
 
+struct jni_context;
+
 typedef struct mrb_state {
   void *jmp;
 
@@ -277,6 +279,8 @@
   struct RNode *local_svar;/* regexp */
 #endif
 
+  struct jni_context* jcx;
+
   struct RClass *eException_class;
   struct RClass *eStandardError_class;
   struct RClass *eRuntimeError_class;

mruby からは jni_context 構造体は不完全型としてポインタしか扱えない。 構造体の実装は mruby から完全に隠蔽されている。 逆に言えば,アプリケーション側の C 言語プログラムは構造体の内容を自由にデザインできる。

これは 5月29日朝現在の mruby を前提とすれば,おそらく,たった一つの冴えたやり方です。
JNI の文脈情報は1回の Java ネイティブ・メソッド呼出しのあいだだけ有効ですから, jni_context 構造体はスタック上にローカル変数としてとることができますし,そうすることが適切です。 もしも mruby インタープリタ本体の寿命を1回の Java ネイティブ・メソッド呼出しのあいだに限れば,インタープリタ本体と JNI 文脈情報の寿命が一致します。 このやり方には特定のワード長を仮定するようなプラットフォーム依存性はありませんし,複数の mruby インタープリタを同時に実行することもできます。 あえて欠点を挙げれば,勝手な改造は Android 用 mruby の拡張ライブラリがバイナリだけで流通する妨げになります。 しかし仮に将来バイナリの流通があるとしても,それは文脈情報をどこに置くかの問題が解決した後のことになります。 今はまだその問題を解決する段階です。

冴えていない方法の例は初期バージョン mruby-oa-0.1.tar.bz2 にありますが,非効率でポータビリティのない悪いお手本ですから見ないでください。

アプリケーション側の C 言語プログラム jni/mruby-oa.c から jni_context 構造体の定義を示す。 thiz メンバは Java ネイティブ・メソッド呼出しの this に該当する。 runtime_exc メンバは Java の java.lang.RuntimeException を参照する。

/* 1回の native メソッド呼び出しに対応する文脈情報。Ruby の中から
   Java メソッドを呼び出すときに使われる。
*/
struct jni_context {
    JNIEnv* env;
    jobject thiz;
    jclass runtime_exc;         /* RuntimeException */
    jmethodID putstr_id;        /* void putstr(String) */
    jmethodID getline_id;       /* String getline(String) */
};

さしあたり二つのコールバックを jni/mruby-oa.c に用意した。 j_putstr は Android アプリケーションが用意するコンソールに文字列を表示する。

/* Ruby 関数 j_putstr(string) の実装: 1 個の文字列引数を表示し,nil を返す。
*/
static mrb_value j_putstr(mrb_state* mrb, mrb_value obj)
{
    char* str = NULL;
    size_t len;
    mrb_get_args(mrb, "s", &str, &len);
    if (str != NULL && *str != '\0') {
        struct jni_context* cx = mrb->jcx;
        JNIEnv* env = cx->env;
        jstring jstr = (*env)->NewStringUTF(env, str);
        (*env)->CallVoidMethod(env, cx->thiz, cx->putstr_id, jstr);
    }
    return mrb_nil_value();
}

j_getline は Android アプリケーションが用意するコンソールにプロンプトを表示して1行入力をする。

/* Ruby 関数 j_getline(prompt_string) の実装:
   1 個の文字列引数をプロンプトとして表示し,入力された行を返す。
   引数が NULL か空のときは ArgumentError を送出する。
   EOF のときは EOFError を送出する。
*/
static mrb_value j_getline(mrb_state* mrb, mrb_value obj)
{
    char* str = NULL;
    size_t len;
    mrb_get_args(mrb, "s", &str, &len);
    if (str == NULL|| *str == '\0') {
        mrb_raise(mrb, E_ARGUMENT_ERROR, "no prompt given");
    }
    struct jni_context* cx = mrb->jcx;
    JNIEnv* env = cx->env;
    jstring jstr = (*env)->NewStringUTF(env, str);
    jstr = (*env)->CallObjectMethod(env, cx->thiz, cx->getline_id, jstr);
    if (jstr == NULL) {
        struct RClass* EOF_ERROR = mrb_class_obj_get(mrb, "EOFError");
        mrb_raise(mrb, EOF_ERROR, "end of file");
    }
    const char* result_s = (*env)->GetStringUTFChars(env, jstr, NULL);
    size_t result_len = (*env)->GetStringUTFLength(env, jstr);
    mrb_value result = mrb_str_new(mrb, result_s, result_len);
    (*env)->ReleaseStringUTFChars(env, jstr, result_s);
    return result;
}
このコードは,一見,どうにも不審に思えるかもしれません。 str や jstr が NULL のとき,mrb_raise を実行した後,そのまま次へと進んでいます。 このままでは NULL を間接参照しそうです……。
Ruby の例外を送出する mrb_raise 関数は jni/mruby/src/error.c で定義されています。 その処理を追っていくと longjmp をしていることが分かります。 つまり,実際には NULL を参照することなく mrb_raise のところで Ruby 例外の発生として無事(?)に大域脱出をします。 もし仮に GCC の拡張機能を使うとしたら,mrb_raise 関数は __attribure__((noreturn )) 付きで宣言されるべきものです。

この二つのコールバックを,次のように Ruby の Kernel モジュールのメソッド (つまり,事実上の Ruby 関数) として登録する。

/* 次のようにして mruby インタープリタを初期化する:
   Java メソッド void putstr(String) と String getline(String) の ID を得る。
   Ruby 関数 j_puststr と j_getline を定義する。
   Ruby クラス IOError と EOFError をもしなければ定義する。
*/
static void init_ruby(struct mrb_state* mrb)
{
    struct jni_context* cx = mrb->jcx;

    jclass clazz = (*cx->env)->GetObjectClass(cx->env, cx->thiz);
    cx->putstr_id = (*cx->env)->GetMethodID
        (cx->env, clazz, "putstr", "(Ljava/lang/String;)V");
    cx->getline_id = (*cx->env)->GetMethodID
        (cx->env, clazz, "getline", "(Ljava/lang/String;)Ljava/lang/String;");

    struct RClass* krn = mrb->kernel_module;
    mrb_define_method(mrb, krn, "j_putstr", j_putstr, ARGS_REQ(1));
    mrb_define_method(mrb, krn, "j_getline", j_getline, ARGS_REQ(1));

    int n = mrb_compile_string(mrb, 
                               "class IOError < StandardError; end; "
                               "class EOFError < IOError; end");
    run_ruby(mrb, n);
}
この init_ruby 関数では (少なくとも今の) mruby にない IOError と EOFError を (もしなければ) 定義するために,短い Ruby スクリプトを走らせています。 ここを C 言語で書かなかったのは,もしなければ定義する,という動作を実現するための手頃な方法が,見た限りでは用意されていなかったからです。 run_ruby 関数の内容は 第2章 で繰り返し出てきたおなじみのパターンです。
/* n 番目の内部表現としてコンパイルされたスクリプトを実行する。
 */
static mrb_value run_ruby(struct mrb_state* mrb, int n)
{
    struct RProc* proc = mrb_proc_new(mrb, mrb->irep[n]);
    return mrb_run(mrb, proc, mrb_top_self(mrb));
}

init_rubyGetMethodID によって ID を取得し,j_pusttrj_getline から呼び出している Java の二つのメソッドは,Interpreter.javavoid putstr(String s)String getline(String prompt) である。 cx->thiz はこのクラスのインスタンスである。

public class Interpreter implements Runnable
{
    protected final AbstractConsole con;
    protected final String scriptPath;

    static {
        System.loadLibrary("mruby-oa");
    }

    /**
     * @param con  インタープリタで使うコンソール入出力
     * @param scriptPath インタープリタが実行するスクリプトのパス名
     */
    public Interpreter (AbstractConsole con, String scriptPath) {
        this.con = con;
        this.scriptPath = scriptPath;
    }

    protected native void run(String fileName);

    protected void putstr(String s) {
        con.writer().print(s);
    }

    protected String getline(String prompt) {
        return con.readLine(prompt);
    }

    @Override public void run() {
        run(scriptPath);
    }
}
下図は j_pusttrj_getline を使ったスクリプトとその実行例です。


4 Java から Ruby へのネイティブ・メソッド呼出し

前節では C 言語で実装された Ruby 言語から Java を呼び出すコールバックを説明した。 ひるがえって Java から C 言語を呼び出すときは,Java ネイティブ・メソッドを使う。 ここでは Interpreter.javanative void run(String fileName) を用意した (前節参照)。 その引数は実行すべき Ruby スクリプトのファイル名とする。 与えられた Ruby スクリプトが終了するまでこのメソッドは戻ってこないものとする。

このネイティブ・メソッドに対する jni/mruby-oa.c の実装関数を下記に示す。

/* natie メソッド実装関数
 */
void
Java_jp_oki_1osk_esc_Interpreter_run(JNIEnv* env, jobject thiz, jstring jstr)
{
    jclass runtime_exc = (*env)->FindClass(env, "java/lang/RuntimeException");

    const char* str = (*env)->GetStringUTFChars(env, jstr, NULL);
    if (str == NULL) {
        (*env)->ThrowNew(env, runtime_exc, "null argument given");
        return;
    }
    struct jni_context context = { env, thiz, runtime_exc, NULL, NULL };
    struct mrb_state* mrb = mrb_open();
    mrb->jcx = &context;

    init_ruby(mrb);
    int n = compile_file(mrb, str);
    (*env)->ReleaseStringUTFChars(env, jstr, str);

    if (n >= 0) {
        run_ruby(mrb, n);
        if (mrb->exc) {
            mrb_value ex = mrb_funcall(mrb, mrb_obj_value(mrb->exc),
                                       "inspect", 0);
            if (mrb_type(ex) == MRB_TT_STRING) {
                struct RString* rs = mrb_str_ptr(ex);
                (*env)->ThrowNew(env, runtime_exc, rs->buf);
            } else {
                (*env)->ThrowNew(env, runtime_exc,
                                 "runtime exception occurred");
            }
        }
    }
    mrb_close(mrb);
}

引数として渡された env およびその他の JNI 文脈情報は,この実装関数の呼出しのあいだだけ有効である。 したがって,それらを格納する jni_context 構造体は,ローカル変数としてスタック上にとることが妥当である。 mruby インタープリタそれ自体のデータ構造は mrb_open によってヒープ上に割り付けられるから,関数の最後で mrb_close を呼び出して陽に解放する。

例外クラス java.lang.RuntimeException への参照を

  jclass runtime_exc = (*env)->FindClass(env, "java/lang/RuntimeException");

として取得しておき,異常が発生した場合は,これを

  (*env)->ThrowNew(env, runtime_exc, rs->buf);

のように使って Java を RuntimeException 発生の状態におく。 実際に例外が発生するのは Java に制御が戻ったときだから,この場合も実装関数最後の mrb_close は呼び出される。

実装関数に jstring 型の引数として渡されたスクリプト・ファイル名はそのままでは C 言語で扱えないから, 対応する UTF-8 文字列を GetStringUTFChars で得てから下記の compile_file 関数に渡す。

/* 与えられた Ruby スクリプト・ファイルをコンパイルし,
   結果の内部表現の mrb->irep の添字を返す。
   エラー時は Java 例外をセットして負数を返す。
*/
static int compile_file(struct mrb_state* mrb, const char* file_name)
{
    struct jni_context* cx =  mrb->jcx;
    FILE* rf = fopen(file_name, "r");
    if (rf == NULL) {
        (*cx->env)->ThrowNew(cx->env, cx->runtime_exc, strerror(errno));
        return -1;
    }
    int n = mrb_compile_file(mrb, rf);
    fclose(rf);

    if (n < 0)
        (*cx->env)->ThrowNew(cx->env, cx->runtime_exc, "failed to compile");
    return n;
}

5 Android アクティビティからのインタープリタの構築

第1節で述べたように Android アプリケーションの GUI コードには GNU Prolog for Java on Android から流用したものを使う。 前節までの仕込みのもとで,これに少しの変更を加えると,mruby が実際に Android 上で動き出す。

GNU Prolog for Java on Android の GUI コードは元々は SemiArc on Android からの流用です……というわけで,実は同じネタを Arc (Lisp), Prolog, Ruby と三度使い回しています。

mruby インタープリタを実行する Anroid のアクティビティ InterpreterActivityonCreate メソッドのうち,Interpreter クラス (前々節参照) のインスタンスを構築する部分を下記に示す。 前節,前々節で説明したように Interpreter クラスは,C 言語プログラムである mruby インタープリタに対する Java 側の仲介者として働く小さな Java クラスである。 下記では Runnable 変数 runnable の値としてその無名派生クラスのインスタンスが使われている。

        textView = (TextView) findViewById(R.id.tv1);
        editText = (EditText) findViewById(R.id.et1);

        final PrintWriter writer = new PrintWriter
            (new OutputWriter (textView));
        final PrintWriter promptWriter = new PrintWriter
            (new EmphasizedOutputWriter (textView));
        input = new InteractiveInput (writer, promptWriter);
        AbstractConsole con = new AbstractConsole () {
                { defaultPrompt = "_"; }
                @Override public String readLine(String prompt) {
                    return input.readLine(prompt);
                }
                @Override public PrintWriter writer() {
                    return writer;
                }
            };
        reader = con.reader();

        // Intent で指定されたファイルをスクリプトとして読み込む
        currentPath = getIntent().getStringExtra(Common.FILE_PATH);
        setTitle(currentPath);
        Runnable runnable = new Interpreter (con, currentPath) {
                @Override public void run() {
                    try {       // XXX GUI の立ち上がりを待ち合わせる
                        Thread.sleep(500);
                    } catch (InterruptedException ex) {
                        return;
                    }
                    try {
                        super.run();
                    } catch (RuntimeException ex) {
                        promptWriter.println(ex.getMessage());
                    }
                    promptWriter.println("Bye");
                    editText.post(new Runnable () {
                            @Override public void run() {
                                editText.setEnabled(false);
                            }
                        });
                }
            };
        interpThread = new Thread (runnable, "Interp");
        interpThread.setDaemon(true);

ここで runnablerun メソッドの冒頭にある Thread.sleep(500) に注意されたい。 これは「GUI の立ち上がりを待ち合わせる」ためのものである。 この待ち合わせを省くと,mruby からの最初のコンソール出力が表示されないことがある。 ネイティブ・コードである mruby の起動が高速すぎることがその原因である。 500 ミリ秒つまり 0.5 秒の待ち合わせ時間は決して確かな裏付けがあるものではなく,また,正攻法としてはおそらく textView に対する post メソッドの戻り値をテストしてリトライ等すべきであることが,コメントに XXX (正しくないがとりあえず動く,の意) のマークを記した理由である。

最初のコンソール出力が表示されないバグは CoreDuo 上の ARM 仮想マシンではなかなか発生しませんでしたが, Core i7 上の Intel x86 仮想マシンで試したところ,試行した限りでは常に発生しました。 長らくまれに起こる謎の現象だったものが,こうしてただちに一応仮にも対策できたわけです。
正攻法で直す場合,その対象はおそらく OutputWriter.javaEmphasizedOutputWriter.java になります。 このバグに対してだけ対策するならば簡単にできそうですが,広く一般性があるように,きちんと行おうとすると少々面倒になりそうです。 普通は描画要求が失敗するのはアプリケーションが終了途中にある場合であって,そのときは失敗を無視するのが妥当です。 またリトライを続けることは,状況によってはデッドロックの危険を孕みます。

6 おわりに

GNU Prolog for Java on Android の GUI に対する他の変更点は次のとおりである。

  1. FinderActivityEditorActivity で "Load" を "Go" と改名した。
  2. EditorActivity で "Save" と "Exit & Load" を1回の "Save & Go" の選択でできるようにした。
  3. EditorWatcher で閉じ括弧に対応する開き括弧の自動表示を一時的なカーソルの移動ではなく, 一時的な文字色の変化で示すようにした。

1. の変更は,2. で誤解を招く "Save & Load" を避けるために必要である。 どちらもコード上の変更箇所はごくわずかである。 3. の変更により,広範な入力環境で括弧の対応表示が効くことが期待される。

第3節で説明した二つのコールバック j_putstrj_getline だけを考えるならば,コンソール入出力のための AbstractConsole の抽象化は不要である。 将来,コールバックを拡充するときに再び役立つかもしれない遺産として残している。

この実験的なアプリケーションでは,mruby を Android にただ載せるだけでなく,コールバックによって Java と相互作用する実例を示した。 これを土台として Android への実用的な mruby の応用ができると考える。

GUI コードそのものは Arc, Prolog, Ruby と三度の経験を経てすっかり丸く(?) なりました。 Java または C 言語で書かれたお手許のインタープリタを Android にさらっと載せてみるための手頃なツールとして役に立つと思います ;-)   さしあたり現バージョンは LGPL v3 としています。

Copyright (c) 2012 OKI Software Co., Ltd.