MZ Platformの機能開発記録

スクリプト実行

MZ Platformでは、 バージョン2.9から「スクリプト実行」コンポーネントを提供しています。

注:ただし、コンポーネント追加のメニューでは表示されません。 必要な場合はクラス指定で追加する必要があります。 (jp.go.aist.dmrc.platform.beans.interpreter.PFScriptInterpreter)

MZ Platform はソースコードを書かずにソフトウェアを作成して実行することができるツールですが、 このコンポーネントを用いると、ソフトウェアの処理記述にスクリプト言語のソースコードを含めることができます。

このスクリプト実行コンポーネントを開発するために、Java上でのスクリプト言語実行環境と、 それを扱うためのフレームワークBSFについて調べたので、それらの情報と作業記録を載せます。

目的

ソースコードを書かずにソフトウェアを作成できることが特徴のツールで、 ソースコードを書くためのコンポーネントを提供するということは、 矛盾してるように思われるでしょうから、経緯と目的を書いておきます。

MZ Platformでは、あらゆるソフトウェアをソースコードを書かずに作成できるとは言えないので、 ユーザがJavaのソースコードを書いて独自のコンポーネントを追加するための機能を提供しています。 このように独自のコンポーネントを作成するためにソースコードを書く作業は、 従来のJavaによるソフトウェア開発と同様になります。 このような開発を頻繁に実施すると手間がかかるので、 なるべくソースコードを書かずにビルダー上だけでソフトウェアが作成できるように、 MZはコンポーネントの拡充を続けています。

一方、すべてのソフトウェアの処理をコンポーネントの接続で記述していると、 ややこしい記述になったり、妙に長くて複雑な記述になることがあります。 もちろん、複雑な処理をしている場合や、機能が大規模になってきた場合は仕方がないのですが、 単純な処理のはずなのにそうなる場合があります。 これは、その処理が現在のMZでは記述が苦手な処理だと言えます。 今は記述が苦手な処理でも、 中にはコンポーネントの機能を追加したり修正を加えたりすることで記述が単純になるものもあるので、 こちらについても改善を続けています。

以上のような状況の中で、コンポーネントも一応揃っていて、どうにか記述を簡単にしようと改善を続けてきた中で、 明らかに効果が出ていない部分があります。その一つは、論理式の組み合わせによる条件分岐の記述です。

論理式の組み合わせによる条件分岐は、ソースコードを書くプログラミングではよくある記述で、 書き方を覚えればそれほど難しくないと思いますし、ソースコードで書くと簡潔に書けますが、 MZ上でコンポーネントの組み合わせによって表現しようとすると、かなり長く複雑になって面倒です。

例として、下図のサンプルアプリケーションで説明します。 これは数値Aと数値Bを入力して判定ボタンを押すと、 所定の論理式を用いて判定した結果をダイアログで表示するアプリケーションです。

ここでは所定の論理式として、1≦A≦5または1≦B≦5のときTRUE、それ以外をFALSEと判定する分岐処理を対象とします。 これをビルダー上で記述すると下図のようになります。

一方、Javaのソースコードで同様の分岐処理を記述すると下記のようになります。

if ((1<=A && A<=5)||(1<=B && B<=5)) {
  return true;
} else {
  return false;
}

MZの目的はユーザが「簡単に」ソフトウェアを作成できるようにすることですので、 「ソースコードを書かない」ことにこだわらず、ソースコードを書いて簡単に処理を記述できるのであれば、 その選択肢もあった方が良いだろうという判断で、スクリプト実行コンポーネントを開発したというのが経緯です。

このコンポーネントを開発することに決めてから、その目的は前述の論理式の組み合わせによる条件分岐の記述に限らず、 一般的なスクリプト言語のソースコードを広く扱え、対象とするスクリプト言語の利用者なら違和感なく使えて、 かつスクリプト言語を使ったことがないMZのユーザでも理解しやすいものを目指しています。

コンポーネントの概要

スクリプト実行コンポーネントの概要を説明します。

上の図は、スクリプト実行コンポーネントの構成と機能を表しています。 コンポーネントは内部にスクリプト実行環境のオブジェクトを持ち、 実行環境内の変数の設定と取得のメソッド、およびスクリプト文字列を指定して実行させるメソッドを提供します。 また、スクリプト実行環境から通知を受け取った際に、MZ側でイベントを発生させる機能も提供します。

このコンポーネントでは、スクリプト実行環境を明示的に初期化したとき以外は、 変数などの内部状態を保持し続け、スクリプトを実行するたびにその内部状態を更新していくという動作を想定しています。

下図は、スクリプト実行コンポーネントを用いて前述のサンプルの分岐処理を記述したものです。 スクリプトの実行環境を初期化して変数を設定した後、保存してあるスクリプトを実行すると、 各分岐に対応した番号でイベントが発生するので、分岐に対応した処理を簡潔に記述できていることがわかります。

Java上のスクリプト言語実行環境

スクリプト実行コンポーネントを開発するにあたっては、Java上のスクリプト言語実行環境がすでに存在していたことと、 Javaの文法でスクリプトを記述する外部ライブラリが存在したことが大きかったです。

Java上のスクリプト言語実行環境のフレームワークとして、 Bean Scripting Framework (BSF)が存在します。 現在のMZの標準の動作環境であるJava SE 6からBSFに対応するクラスが含まれているため、 これを前提としてコンポーネントを開発しました。

Java上で各種スクリプト言語の動作する環境そのものは、各言語を推進する団体が独自に提供しているようですが、 BSFに準拠した実行環境を提供しているものについては、Java上で統一的に扱うことが可能で、 Java開発者が利用しやすい状況になってきています。

ただし、BSFにはバージョンがあって、各言語が対応するバージョンが異なるので、 完全に統一できるわけではないことがわかりました。 Java SE 6に含まれるBSF対応のクラスはBSF3系のバージョンで、BSF3系に対応する言語であれば、 その言語実行環境をパスに追加するだけで利用を開始できます。 一方、BSF2系というバージョンも一般的に使用されているようですが、BSF3系とは互換性がなく、 BSF2系にしか対応していない言語の実行環境をJava SE 6で使うためには、 BSF2系のライブラリを追加する必要がありました。

このような状況とMZ上で使いたい言語の候補、およびそれらの動作を確認してみた結果、 現状はBSF2系をコンポーネントの機能の対象から外しています。 BSF3系で利用できてJava SE 6に実行環境が含まれている言語は、JavaScriptです。 これはMZのコンポーネントで使える状態にしてあります。

加えて、BSF3系に対応していないですが、BeanShellを個別に追加しました。 これは、Javaの文法でスクリプトを実行できる言語実行環境です。 Javaそのものはスクリプト言語ではないですが、Javaの文法を使ってスクリプトを書いて実行できるということで、 これについては特別扱いをしました。

Java:BeanShell

BeanShellは、Javaの文法でスクリプトが実行できる言語実行環境です。 Javaそのものはスクリプト言語ではないですが、Javaの文法を使ってスクリプトを書いて実行できるということで、 Javaのソースコードを書ける人にとってはかなり便利だと思います。 MZの配布に含めているので、ユーザはすぐにこの機能を使うことができます。

スクリプト実行コンポーネントの属性Languageをjavaとし、 StoredScriptにスクリプトを記述すれば、前述のサンプルアプリケーションのようにして使えます。 StoredScriptの欄に書くにはスクリプトが長い場合には、テキストエリア等で編集した文字列を指定することもできます。

上記のスクリプトでは、論理式による分岐は前述の説明の通りですが、各分岐で実行する命令として、 スクリプト実行コンポーネントが提供する独自の関数 mz_event(int) を呼び出しています。 mz_event(int) は、指定した番号でアクションイベントを発生させるための関数です。 同様の関数として、mz_event2(int,Object) は指定した番号で指定オブジェクトを内包した処理完了イベントを発生させます。

下記は、スクリプト実行コンポーネントのソースコードから、BeanShellに関する部分を抜き出して簡略化したものです。 例外処理やエラー処理はすべて省略してあります。 BeanShellはBSF2系に対応していましたが、前述のようにBSF2系には対応しないことにしたので、 ここでは独自のクラスを直接利用しています。 このようなシンプルな実装だけで、前述のように想定したコンポーネントの動作が実現できます。

package jp.go.aist.dmrc.platform.beans.interpreter;

import bsh.EvalError;
import bsh.Interpreter;

public class PFScriptInterpreter {

	private Interpreter enginebsh = null; // 言語実行環境のオブジェクト

	public void fireEvent(int eventNo); // MZのイベントが発生するメソッド
	public void fireEvent(int eventNo, Object data); // MZのイベントが発生するメソッド

	// 言語実行環境の初期化
	void initEngine() {
		enginebsh = new Interpreter(); // オブジェクト作成
		enginebsh.set("_mz_parent",this); // コンポーネント本体を変数として設定

		// スクリプト内からイベント発生を通知するための関数を設定
		String function = 
			"mz_event(id) {_mz_parent.fireEvent(id);} " +
			"mz_event2(id,data) {_mz_parent.fireEvent(id,data);}";
		enginebsh.eval(function);
	}

	// 変数の設定メソッド
	public void setVariable(String name, Object obj) {
		enginebsh.set(name,obj);
	}

	// 変数の取得メソッド
	public Object getVariable(String name) {
		return enginebsh.get(name);
	}

	// スクリプトの実行メソッド
	public Object evaluate(String script){
		return enginebsh.eval(script);
	}
}

JavaScript:Rhino

BSF3系で利用できてJava SE 6に含まれている実行環境としては、 JavaScriptのRhinoがあります。 これはMZのコンポーネントですぐに使える状態になっています。

スクリプト実行コンポーネントの属性Languageをjsとし、 StoredScriptにスクリプトを記述すれば、前述のサンプルアプリケーションのようにして使えます。 StoredScriptの欄に書くにはスクリプトが長い場合には、テキストエリア等で編集した文字列を指定することもできます。

上記のスクリプトでは、論理式による分岐は前述の説明の通りですが、各分岐で実行する命令として、 スクリプト実行コンポーネントが提供する独自の関数 mz_event(int) を呼び出しています。 mz_event(int) は、指定した番号でアクションイベントを発生させるための関数です。 同様の関数として、mz_event2(int,Object) は指定した番号で指定オブジェクトを内包した処理完了イベントを発生させます。

下記は、スクリプト実行コンポーネントのソースコードから、JavaScriptに関する部分を抜き出して簡略化したものです。 例外処理やエラー処理はすべて省略してあります。 JavaScriptはBSF3系に対応しているので、基本的にはBSF3系に対応する言語を導入する際は同様に実装できます。 ただし、スクリプト内からイベント発生を通知するための関数設定は、各言語で個別に定義する必要があるのと、 コンポーネント本体を変数として設定してスクリプト内でJavaのメソッドを起動できるかどうかは、各言語環境に依存します。

package jp.go.aist.dmrc.platform.beans.interpreter;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class PFScriptInterpreter {

	private ScriptEngineManager manager3x = new ScriptEngineManager(); // BSF3系で必要
	private ScriptEngine engine3x = null; // 言語実行環境のオブジェクト

	public void fireEvent(int eventNo); // MZのイベントが発生するメソッド
	public void fireEvent(int eventNo, Object data); // MZのイベントが発生するメソッド

	// 言語実行環境の初期化
	void initEngine() {
		engine3x = manager3x.getEngineByName("js"); // オブジェクト取得:言語名指定
		engine3x.put("_mz_parent",this); // コンポーネント本体を変数として設定

		// スクリプト内からイベント発生を通知するための関数を設定
		String function = 
			"function mz_event(id) {_mz_parent.fireEvent(id);} " +
			"function mz_event2(id,data) {_mz_parent.fireEvent(id,data);}";
		engine3x.eval(function);
	}

	// 変数の設定メソッド
	public void setVariable(String name, Object obj) {
		engine3x.put(name, obj);
	}

	// 変数の取得メソッド
	public Object getVariable(String name) {
		return engine3x.get(name);
	}

	// スクリプトの実行メソッド
	public Object evaluate(String script){
		return engine3x.eval(script);
	}
}

その他の言語の状況

BSF3系で利用できるスクリプト言語の実行環境の内、試してみて問題なく動作したものとして、 Pythonの実行環境Jythonがあります。 以前は対応していなかったようですが、少なくとも2.5.1以降はBSF3系に対応しているようです。 現状ではMZ本体に含めていませんが、リンク先からjython.jarを取得して、 MZかJavaのパスに追加すれば使えるようにしてあります。 その際、コンポーネント側の設定として、属性Languageの指定を"python"または"jython"とします。

Rhinoとの違いは、MZ側のイベント発生を通知するための関数の指定をPython方式で記述する必要があり、 下記のような処理を含めてあります。

	void initEngine() {
		engine3x = manager3x.getEngineByName("jython");
		engine3x.put("_mz_parent",this);

		String function = 
			"def mz_event(id): _mz_parent.fireEvent(id) \n" +
			"def mz_event2(id,data): _mz_parent.fireEvent(id,data)";
		engine3x.eval(function);
	}
これでわかるように、BSF3系に対応したスクリプト言語の実行環境を新たに追加する場合、 大部分を統一的に扱うことが可能とはいえ、MZ側のイベント発生を通知する関数の指定は個別の言語で記述する必要があります。 なので、追加する言語については、ユーザからのご要望を踏まえて対応していくしかないようです。

(2012/12/5追記) Rubyの実行環境JRubyにも対応しました。 BSF3系対応でしたがRhinoやJythonと同様のやり方では、コ ンポーネントとして必要な変数設定やイベント発生通知の機能が実現できなかったため保留にしてありましたが、 JRubyのドキュメントと使い方を紹介する記事を読んで実現方法がわかったので、MZ2.10からは対応しています。 JRuby自体は現状MZ本体に含めていませんが、リンク先からjruby.jarを取得して、 MZかJavaのパスに追加すれば使えるようにしてあります。 その際、コンポーネント側の設定として、属性Languageの指定を"ruby"または"jruby"とします。 MZ側のイベント発生を通知するための関数の指定をRuby方式で記述する必要があり、 下記のような処理を含めてあります。 ポイントは、Java側のオブジェクトを扱うにはグローバル変数とする必要があり、"$"を付けました。 また、evalの実行ごとにローカル変数がリセットされないようなプロパティ指定を追加しています。

	void initEngine() {
		System.setProperty("org.jruby.embed.localvariable.behavior", "persistent");
		engine3x = manager3x.getEngineByName("ruby");
		engine3x.put("$_mz_parent",this);

		String function = 
			"def mz_event(id) $_mz_parent.fireEvent(id) end\n" +
			"def mz_event2(id,data) $_mz_parent.fireEvent(id,data) end\n";
		engine3x.eval(function);
	}

残念ながら対象から外れた言語としては、Prologがあります。 Prologの実行環境JLogはBSF2系にしか対応していないようです。 BSF2系についても関係するライブラリを追加して動作を確認してみたのですが、 コンポーネントとして実現したかった機能がBSF2系のAPIではうまくいかなかったため、対応を見合わせています。 このあたりは確認した時点(2012年4月頃)での問題なので、時間が経って解決していたらまた検討してみます。

現状では利用できない言語についても、Java上で動作する実行環境が提供されていれば、 BeanShellのように個別に対応して機能に含めることは可能です。 しかし、その場合はその言語実行環境ライブラリの利用・再配布ライセンス形態が問題になってくるので、 なんでも対応するわけにはいかないという事情があります。 BeanShellはJavaの文法が扱えることが魅力的だった上にライセンスがLGPLだったので、 個別の対応を実施しました。

作成日 2012-11-12

最終更新日 2012-12-05