Semi-Arc の Android への移植

2011.12.12, 2012.1.10 (鈴)

1. はじめに

前回の Semi-Arc 10.2 を Android 2.3.3 (API 10) に移植した。

Android への移植をはじめてすぐ,意外にも,端末入出力を除く Semi-Arc インタープリタ本体が Android 上でそっくりそのまま動くことが分かった。 実行速度はともかく,直接のユーザ・インタフェース・ライブラリを除く Java SE の Android におけるサポートはとても充実していた。 今回の移植では Semi-Arc 10.2 のコンパイル済みバイナリ semi_arc.jar を無変更で利用した。

さしあたっての主な問題は (少なくともエミュレータ上で) スレッドのスタック・サイズが小さすぎることだった。 試みに Activity クラスの onCreate(Bundle) メソッド内でインタープリタを走らせてみると,Arc の初期化スクリプト arc.arc を実行しただけで StackOverflowError を起こした。 幸い,Java の Thread はコンストラクタ Thread (ThreadGroup group, Rnnable target, String name, long stackSize) の第4引数として陽にスタック・サイズを指定できる。 こうして作った Thread インスタンスの上でインタープリタを走らせることによって,この問題は解決した。 現在のコードでは InterpThread.java にある super コンストラクタ呼び出しで Thread のスタック・サイズを指定している。

この時点まではインタープリタの出力を System.out.println で表示し,adb logcat で確認していた。 実質三日間にわたる移植のほとんどは,端末入出力を模倣するユーザ・インタフェースの構築に費やされた。

Android API の使い方は主に scheme-droid [google.com] を参考にした。 その SchemeREPL.java r12 by D. da Silva on May 15, 2011 [google.com] と今回作成した SemiArcActivity.java を比較されたい。 両 Activity クラスは,どの Android クラスを利用し,どのメソッドをオーバーライドするかで基本的に同一である。 一方,scheme-droid と異なり,本アプリケーションはインタープリタを別スレッドで動作させており, 広いスタックサイズ,非同期的な出力など対話型インタープリタのための,より好ましい動作環境を実現している。

0.1 版 (2011年 12月) に対し 0.2 版 (2012年 1月) では Android Lint [android.com] と Eclipse によって警告された問題点を修正した。

以下,クラスの構成と実装の要点について説明する。 アプリケーションをソースから構築して動かすための具体的な手順については 付録: Android 開発手順 にまとめた。

もし Semi-Arc アプリケーションを今すぐ手持ちの Android マシンで試してみたいならば,このページを Android マシン内蔵の Web ブラウザで表示し,そこから SemiArcActivity-release.apk をダウンロードしてインストールすれば即,動作させることができます。 このとき,未知のソース (unknown sources) からのアプリケーションのインストールをその Android マシンに許可しておきます。 許可の方法については "付録: Android 開発手順" の SD カード経由のオフライン・インストール などを参考にしてください。

2. クラス構成

アプリケーションの大まかなクラス図を示す。

Semi-Arc インタープリタの本体である InterpInterpThread 上で動作する。 端末出力は java.io.PrintWriter 経由で OutputWriterandroid.os.Handler を利用して android.widget.TextView に行う。 EmphasizedOutputWriterOutputWriter の強調表示 (ここでは色つき表示) 版であり,主にプロンプト表示 ("arc> " など) を担う。

Activity クラス SemiArcActivity を動作させるメイン・スレッドは,android.widget.EditText からのテキスト入力を InteractiveInput#notifyOf(text) メソッドで通知する。 これは InteractiveInput#readLine(prompt) メソッドで入力待ちをしている InterpThread のスレッドに伝達される。 PromptInput は,InteractiveInput クラスを,インタープリタが要求する IInput インタフェースへと仕立てるアダプタである。

3. 実装の要点

OutputWriter: Handler による他スレッドからのウィジェット操作

一般に Android の各ウィジェットは Android が暗黙裏に用意するメイン・スレッドからしか操作できない。 別スレッドから TextView ウィジェットに対して文字列を直接 append しようとすると例外が発生する。 下記のように Handlerpost メソッドを使って処理を暗黙のメッセージ・キューに追加すればよい。

class OutputWriter extends Writer
{
    protected TextView textView;
    protected Handler handler;

    OutputWriter (TextView textView, Handler handler) {
        this.textView = textView;
        this.handler = handler;
    }

    /** 文字列を TextView に追加する。
     */
    @Override public void write(char[] buf, int off, final int len) {
        final String str = new String (buf, off, len);
        handler.post(new Runnable () {
                @Override public void run() {
                    textView.append(str);
                }
            });
    }

    @Override public void flush() {}
    @Override public void close() {}
}

Handler インスタンスはメイン・スレッドで構築する必要がある。 Activity クラスの onCreate メソッドで構築し,別スレッドで動作することになる他のオブジェクトに,コンストラクタの引数として引き渡せばよい。

public class SemiArcActivity extends Activity
{
    ……
    @Override public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

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

        Handler handler = new Handler ();

        PrintWriter writer = new PrintWriter 
            (new OutputWriter (textView, handler));
        ……
     }
     ……
}

注意点が三つある。

post された処理は非同期的に実行される。 OutputWriterwrite メソッドから戻った時点では,おそらくまだ文字列は TextView に追記されていない。 しかし,本物の端末出力でもその点では大差なく,今回の目的には問題ない。

post メソッドには戻り値がある。 メッセージ・ループ自体が終了しようとしている等で,キューイングに失敗したときは false が返される。 ここではしていないが,クラスの用途によっては,戻り値をそのつど検査して例外の送出等,適切な処理をすることが必要だろう。

厳密にいえば,この OutputWriter や次に述べる EmphasizedOutputWriterHandler は必ずしも必要ない。 TextView のスーパークラスである View クラスには便宜として post(Runnable) メソッドが用意されている。 handler.post(new Runnable …) のかわりに単に textView.post(new Runnable …) とすることができる。 その内部処理は,若干の手間を経るものの結局,Handler を使った場合と同じである。

EmphasizedOutputWriter: TextView への色つき文字列の追記

TextView ウィジェットの任意の範囲の文字に色を着けるには android.text.SpannableString を使えばよい。 EmphasizedOutputWriterwrite メソッドは,明るい水色 (Color.rgb(100, 200, 255)) を前景色 (foreground color) として TextView ウィジェットに文字列を追記する。

class EmphasizedOutputWriter extends OutputWriter
{
    ……
    @Override public void write(char[] buf, int off, final int len) {
        final String str = new String (buf, off, len);
        handler.post(new Runnable () {
                @Override public void run() {
                    SpannableString sp = new SpannableString (str);
                    int color = Color.rgb(100, 200, 255);
                    ForegroundColorSpan span = new ForegroundColorSpan (color);
                    sp.setSpan(span, 0, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                    textView.append(sp);
                }
            });
    }
}

SemiArcActivity: EditText のイベント処理

EditText ウィジェットに任意の打鍵を処理させるには setOnKeyListener メソッドを使えばよい。 これを使って SemiArcActivity クラスは,editText で Enter キーが押下された時,入力されている文字列をインタープリタのスレッドに通知 (notify) させている。 onKey メソッドで KeyEvent.KEYCODE_ENTER だけでなく KeyEvent.ACTION_DOWN も判定しているのは,一度の打鍵で正確に一度だけ処理するためである。

public class SemiArcActivity extends Activity
{
    ……
    @Override public void onCreate(Bundle savedInstanceState)
    {
        ……
        PrintWriter writer = new PrintWriter 
            (new OutputWriter (textView, handler));
        PrintWriter metaWriter = new PrintWriter
            (new EmphasizedOutputWriter (textView, handler));
        input = new InteractiveInput (writer, metaWriter);
        Thread thread = new InterpThread (writer, metaWriter, input);

        editText.setOnKeyListener
            (new View.OnKeyListener () {
                    @Override public boolean onKey(View view, int code,
                                                   KeyEvent event) {
                        int action = event.getAction();
                        if (action == KeyEvent.ACTION_DOWN &&
                            code == KeyEvent.KEYCODE_ENTER) {
                            CharSequence text = editText.getText();
                            input.notifyOf(text);
                            editText.setText("");
                            return true;
                        }
                        return false;
                    }
                });

        thread.start();
    }
    ……
}

InteractiveInput: 古典的な notify と wait

前節に示したように,アプリケーションのメイン・スレッドは,EditText ウィジェットで Enter キーが押下された時,その時点でのテキストを引数として InteractiveInput インスタンスの notifyOf(text) を呼び出す。

notifyOf(text) メソッドは text を内部に保存してから,排他ロックに notify() する。

インタープリタのスレッドは,同じ InteractiveInput インスタンスの readLine(prompt) で入力行を得る。 readLine(prompt) は,もしも内部に保存している text がなければ,排他ロックで wait() し,別スレッド (この場合はメイン・スレッド) からの notify() を待つ。

こうして,メイン・スレッドからインタープリタのスレッドに text が渡される。

class InteractiveInput
{
    private PrintWriter echoBackWriter; // エコー・バック用
    private PrintWriter promptWriter;   // プロンプト表示用
    private String text = null;         // 現在の入力行
    private final Object lock = new Object (); // 排他ロック
    private volatile String lastPrompt = null; // 最新のプロンプト

    InteractiveInput (PrintWriter echoBackWriter, PrintWriter promptWriter) {
        this.echoBackWriter = echoBackWriter;
        this.promptWriter = promptWriter;
    }

    void notifyOf(CharSequence text) {
        synchronized (lock) {
            this.text = text.toString();
            lock.notify();
        }
    }

    String readLine(String prompt) {
        lastPrompt = prompt;
        writePrompt();
        String result;
        synchronized (lock) {
            while (text == null) { // cf. spurious wake-up
                try {
                    lock.wait();
                } catch (InterruptedException ex) {
                    text = null; // EOF に相当
                    break;
                }
            }
            result = text;
            text = null;
            lastPrompt = null;
        }
        if (result != null)
            echoBackWriter.println(result);
        return result;
    }

    void writePrompt() {
        String prompt = lastPrompt;
        if (prompt != null)
            promptWriter.write(prompt);
    }
}

readLine(prompt) はメソッドの入り口で,プロンプト表示用の PrintWriter を使ってプロンプトを表示し, メソッドの出口で,エコーバック用の PrintWriter を使ってテキストをエコーバックする。 したがって,EmphasizedOutputWriter インスタンスを使ってプロンプト表示用の PrintWriter インスタンスを構築すれば,プロンプトに色をつけることができる。

InteractiveInput インスタンスは一回分の text しか内部に保存しないから,プロンプトの表示を待たずに繰り返しテキストを入力して Enter キーを打鍵しても,最後に入力したテキストだけがインタープリタに伝達される。 もしも先行するテキストを捨てないようにしたければ,キューを使う必要がある。 今のところ,そのような高速打鍵ないし高速入力の需要はないと想定している。

4. おわりに

ここでは主に,Android 上で端末入出力を模倣するユーザ・インタフェースの実現について述べた。 現在 300 行に満たない小規模なコードである。 類似のアプリケーションの実装の手ごろな参考になれば幸いである。

未解決の問題

未解決の問題として,API Level 7 と Level 8 の Android 仮想デバイスで発見された奇妙な現象がある。

右図のように,- (マイナス) シンボルの大域変数値が減算関数ではなく数値の 0 になるという現象である。

シンボルの大域変数値は Interp クラスの symbols フィールドが保持している (下記参照)。 この NameMap インスタンスに原因があるのかどうかは不明である。 現時点では,Level 8 以前の Android の Java 実装に深刻なバグが潜在している可能性も否定できない。 根が深い問題ではないことを願う。

public class Interp implements IInterp
{
    private final Map<Symbol, Object> symbols
        = new NameMap<Object> ();  // シンボルから大域変数値への表
    ……

    /** 名前表,ただしキーワードは除外する
     */
    private static class NameMap<T> extends HashMap<Symbol, T>
    {
        public T put(Symbol k, T v) {
            if (k instanceof Symbol.Keyword)
                throw new EvalException ("keyword not expected", k);
            return super.put(k, v);
        }
    } // NameMap<T>
    ……
} // Interp

API Level 10 と Level 14 では異常は見られない。他のレベルについては未確認である。 さしあたり今は AndroidManifest.xml で下記のように Level 10 以上に限定している。 Level 10 未満での動作を確認するときは,この箇所を変更されたい。

    <uses-sdk android:minSdkVersion="10" />


総目次へ


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