Luiz Henrique de Figueiredo、Roberto Ierusalimschy、Waldemar Celes 著
近年、アプリケーションの拡張とカスタマイズのために、多くの小さな言語が提案されています。一般的に、これらの拡張言語は次の属性を持つべきです。
拡張言語は、大規模なソフトウェアの作成用ではないため、静的型チェックや情報隠蔽などの大規模プログラミングをサポートするメカニズムは必須ではありません。
ここで紹介する拡張可能な埋め込み言語であるLuaは、これらの要件を満たしています。その構文と制御構造はシンプルで使い慣れています。Luaは小さく、実装全体はANSI Cで6000行未満です。ほとんどのプロシージャル言語に共通する機能に加えて、Luaには、強力な高レベルの拡張言語にする特別な機能があります。
Luaは、データ記述機能を備えたプロシージャルプログラミングをサポートするように設計された汎用埋め込みプログラミング言語です。パブリックドメインではありません(TeCGrafが著作権を保持)が、Luaはhttps://lua.dokyumento.jp/で学術目的と商業目的の両方で自由に利用できます。配布物には、数学関数(sin、cosなど)、I/Oとシステム関数、文字列操作関数の標準ライブラリも含まれています。このオプションのライブラリにより、システムに約1000行のコードが追加されます。デバッガーと、バイトコードを含むポータブルなバイナリファイルを作成する個別のコンパイラも含まれています。このコードは、gcc(AIX、IRIX、Linux、Solaris、SunOS、ULTRIX)、Turbo C(DOS)、Visual C++(Windows 3.1/95/NT)、Think C(MacOS)、CodeWarrior(MacOS)など、ほとんどのANSI Cコンパイラで変更せずにコンパイルされます。
アプリケーションとのリンク時に名前の衝突を避けるため、すべての外部識別子はluaという接頭辞が付いています。yaccによって生成されたコードでさえ、このルールに準拠するためにsedフィルタを通過するため、Luaを他の目的でyaccを使用するアプリケーションとリンクすることが可能です。
Luaは、ホストアプリケーションにリンクされる小さなC関数のライブラリとして提供されます。リスト1にある対話型のスタンドアロンインタープリターは、最もシンプルなLuaクライアントの例です。このプログラムでは、lua_dostring関数が、文字列に含まれるコードセクションに対してインタープリターを呼び出します。Luaコードの各チャンクには、ステートメントと関数定義を混ぜて含めることができます。
ヘッダーファイルlua.hは、約30個の関数を持つLuaのAPIを定義します。lua_dostringに加えて、ファイルに含まれるLuaコードを解釈するlua_dofile関数、Luaグローバル変数を操作するlua_getglobalとlua_setglobal関数、Lua関数を呼び出すlua_call関数、C関数をLuaからアクセスできるようにするlua_register関数などがあります。
Luaの構文はPascalとやや似ています。ぶら下がりelseを避けるため、ifやwhileなどの制御構造は、明示的なendで終わります。コメントはAdaの規則に従い、「--」で始まり、行末まで続きます。Luaは複数代入をサポートします。たとえば、x, y = y, xはxとyの値を入れ替えます。同様に、関数は複数の値を返すことができます。
Luaは動的型付け言語です。これは、値には型がありますが変数には型がないことを意味するため、型宣言や変数宣言はありません。内部的には、各値にはその型を識別するタグがあり、組み込み関数typeを使用して実行時にタグを照会できます。変数は型がなく、任意の型の値を保持できます。Luaのガベージコレクションは、使用されている値を追跡し、使用されていない値を破棄します。
Luaは、nil、string、number、user data、function、tableという型を提供します。nilは値nilの型であり、その主な特性は他のどの値とも異なることです。これは、たとえば変数の初期値として使用する場合に便利です。number型は浮動小数点の実数を表します。stringは通常の文字列です。user data型はCの汎用的なvoid *
ポインタに対応し、Luaではホストオブジェクトを表します。これらの型はすべて有用ですが、Luaの柔軟性は、LispとSchemeからの2つの重要な教訓である関数とテーブルによるものです。
Luaの関数値は、変数に格納したり、他の関数にパラメーターとして渡したり、テーブルに格納したりすることができます。
Luaで関数を宣言する場合(リスト2を参照)、関数本体はバイトコードに事前にコンパイルされ、関数値が作成されます。この値は、指定された名前のグローバル変数に割り当てられます。一方、C関数は、APIへの適切な呼び出しを通じてホストプログラムによって提供されます。Luaは、ホストによって登録されていないC関数を呼び出すことはできません。したがって、ホストは、オペレーティングシステムへの潜在的に危険なアクセスを含む、Luaプログラムが実行できることについて完全な制御権を持ちます。
テーブルは、LuaにおけるLispのリストのようなものです。強力なデータ構造化メカニズムです。Luaのテーブルは連想配列に似ています。連想配列は、数値だけでなく、任意の型の値でインデックス付けることができます。
連想配列を使用して実装する場合、多くのアルゴリズムは些細なものになります。これは、それらを検索するためのデータ構造とアルゴリズムが言語によって暗黙的に提供されるためです。Luaは連想配列をハッシュテーブルとして実装します。
連想配列を実装する他の言語とは異なり、Luaのテーブルは変数名にバインドされません。代わりに、従来の言語のポインタのように操作できる動的に作成されたオブジェクトです。つまり、テーブルは値ではなくオブジェクトです。変数はテーブル自体ではなく、テーブルへの参照を含みます。代入、パラメーターの受け渡し、関数の戻り値は常にテーブルへの参照を操作し、いかなる種類の複製も暗示しません。これは、テーブルを使用する前に明示的に作成する必要があることを意味しますが、テーブルが自由に他のテーブルを参照することも可能になります。そのため、Luaのテーブルは、循環構造を含む再帰的なデータ型や一般的なグラフ構造を表すために使用できます。
テーブルは、フィールド名をインデックスとして使用することでレコードをシミュレートします。Luaは、a["name"]に対する構文シュガーとしてa.nameを提供することで、これを容易にします。集合も、その要素をテーブルのインデックスとして格納することで簡単に実装できます。テーブル(したがって集合)は同種である必要はなく、関数やテーブルを含むすべての型の値を同時に格納できます。
Luaは、リスト、配列、レコードなどを初期化するのに便利な、テーブルを作成するための特別な種類の式であるコンストラクタを提供します。例1を参照してください。
構築しているデータ構造をより細かく制御する必要がある場合があります。少数の一般的なメタメカニズムのみを提供するという哲学に従って、Luaはユーザー定義コンストラクタを提供します。これらのコンストラクタはname{...}
と記述され、これはname({...})
のより直感的なバージョンです。つまり、このようなコンストラクタを使用すると、テーブルが作成され、初期化され、関数のパラメーターとして渡されます。この関数は、動的型チェック、欠落しているフィールドの初期化、補助データ構造の更新など、必要な初期化を実行できます。ホストプログラムでも同様です。
ユーザー定義コンストラクタを使用して、より高度な抽象化を提供できます。そのため、適切な定義のある環境では、window1=Window{x=200, y=300, color="blue"}
と記述し、「ウィンドウ」について考え、単純なテーブルについて考える必要はありません。さらに、コンストラクタは式であるため、リスト4のように、宣言的なスタイルでより複雑な構造を記述するためにネストできます。
関数は第一級の値であるため、テーブルフィールドは関数を参照できます。これはオブジェクト指向プログラミングへの一歩であり、メソッドの定義と呼び出しを簡素化した構文によって容易になります。
メソッド定義は例2(a)のように記述され、これは例2(b)と同等です。つまり、メソッドを定義することは、selfと呼ばれる隠れた最初の引数を持つ関数を定義し、その関数をテーブルフィールドに格納することと同等です。
メソッド呼び出しはreceiver:method(params)
のように記述され、receiver.method(receiver,params)
に変換されます。メソッドのレシーバーはメソッドの最初の引数として渡されるため、パラメーターselfに期待される意味が与えられます。
これらの構成では情報隠蔽は提供されないため、純粋主義者は(正しく)オブジェクト指向の重要な部分が欠けていると主張するかもしれません。さらに、Luaはクラスを提供しません。各オブジェクトは独自のメソッドディスパッチテーブルを持ちます。それにもかかわらず、これらの構成は非常に軽量であり、Selfなどの他のプロトタイプベースの言語で一般的であるように、継承を使用してクラスをシミュレートできます。
Luaは型のない言語であるため、多くの異常な実行時イベントが発生する可能性があります。数値以外のオペランドへの算術演算の適用、テーブル以外の値のインデックス付け、関数以外の値の呼び出しなどです。型付きのスタンドアロン言語では、これらの条件の一部はコンパイラによってフラグが立てられます。その他は、実行時にプログラムを中断させる結果になります。埋め込み言語がホストプログラムを中断させるのは失礼なので、埋め込み言語は通常、エラー処理のためのフックを提供します。
Luaでは、これらのフックは「フォールバック」と呼ばれ、テーブルに存在しないフィールドへのアクセスやガベージコレクションのシグナル送信など、厳密にはエラー条件ではない状況の処理にも使用されます。Luaはデフォルトのフォールバックハンドラを提供しますが、組み込み関数`setfallback`を2つの引数で呼び出すことで独自のハンドラを設定できます。引数は、フォールバック条件を識別する文字列(表1を参照)、および条件が発生するたびに呼び出される関数です。`setfallback`は古いフォールバック関数を返すため、必要に応じてフォールバックハンドラをチェーンできます。
フォールバックの最も興味深い用途の1つは、Luaでの継承の実装です。単純な継承により、オブジェクトは存在しないフィールドの値を「親」と呼ばれる別のオブジェクト内で検索できます。特に、このフィールドはメソッドにすることができます。このメカニズムは、SmalltalkやC++で採用されているより伝統的なクラス継承とは対照的に、一種のオブジェクト継承です。
Luaで単純な継承を実装する1つの方法は、親オブジェクトを`parent`などの別のフィールドに格納し、「index」フォールバック関数を設定することです(リスト5を参照)。このコードは関数`Inherit`を定義し、それをindexフォールバックとして設定します。Luaがオブジェクトに存在しないフィールドにアクセスしようとすると、フォールバックメカニズムによって関数`Inherit`が呼び出されます。この関数はまず、オブジェクトにテーブル値を含む`parent`フィールドがあるかどうかをチェックします。もしあれば、親オブジェクト内の目的のフィールドへのアクセスを試みます。フィールドが親に存在しない場合、フォールバックは自動的に再び呼び出されます。このプロセスは、フィールドの値が見つかるか、親チェーンが終了するまで「上向き」に繰り返されます。より高いパフォーマンスが必要な場合は、LuaのAPIを使用してCで同じ継承スキームを実装できます。
インタプリタ言語であるLuaは、いくつかの反射機能を提供します。1つの例は、既に述べた関数`type`です。他の強力な反射関数は、テーブルをトラバースする`next`と、すべてのグローバル変数をトラバースする`nextvar`です。関数`next`は、テーブルとテーブル内のインデックスの2つの引数を受け取り、実装依存の順序で「次の」インデックスを返します。(テーブルはハッシュテーブルとして実装されていることを思い出してください。)また、テーブル内のインデックスに関連付けられた値も返します。(Luaの関数は複数の値を返すことができることを思い出してください。)関数`nextvar`は同様の動作をしますが、テーブルのインデックスではなく、グローバル変数をトラバースします。
反射を使用する興味深い例は動的型付けです。前述のように、Luaには静的型付けがありません。しかし、プログラムで奇妙な動作を防ぐために、特定の値が正しい型を持っているかどうかを確認することが役立つ場合があります。`type`を使用して単純な型をチェックするのは簡単です。しかし、テーブルの場合は、すべてのフィールドが存在し、正しく入力されているかどうかを確認する必要があります。
Luaのデータ記述機能を使用すると、値を使用して型を記述できます。単一の型は名前で記述され、テーブル型は各フィールドを必要な型にマッピングするテーブルで記述されます(リスト6)。このような記述があれば、値が特定の型を持っているかどうかをチェックする単一の多型関数を記述できます(リスト7を参照)。
反射機能により、プログラムは独自の環境を操作することもできます。たとえば、プログラムは別のコードを実行するための「保護された環境」を作成できます。この状況は、ホストがインターネットから受信した信頼できないコード(たとえば、実行可能コンテンツを含むWebページ、Java全盛期の流行したもの)を実行する場合など、エージェントベースのアプリケーションで一般的です。いくつかの拡張言語は安全な実行のために特別なサポートを提供する必要がありますが、Luaは言語自体を使用してこれを行うのに十分な柔軟性があります。リスト8は、グローバル環境全体をテーブルに保存する方法を示しています。同様の関数は保存された環境を復元します。すべての関数は変数に割り当てられた第一級値であるため、グローバル環境から関数を削除するのは簡単です。リスト9は、保護された環境でコードを実行する関数を示しています。
Luaの自然な用途はGUIの説明です。オブジェクト(ウィジェット)の階層を記述し、ユーザーアクションをそれらにバインドする機能が必要です。Luaは、データ記述メカニズムとシンプルで強力で拡張可能なセマンティクスを組み合わせているため、このようなタスクに適しています。実際、私たちはLuaでいくつかのUIツールキットを開発しました。
Tkは汎用性の高いGUIツールキットですが、Tclは誰もが快適に感じるような言語ではありません。私たちはLuaをTclの代替手段と見なしているので、Tk/Luaバインディングを実装し、LuaからTkウィジェットにアクセスできるようにすることにしました。
可能な限り、ウィジェット名、属性、コマンドを含むTkの哲学を維持しました。既存のAPIを改善しようとするのは非常に魅力的ですが、長期的には、Tkユーザーにとってはより良い結果になります。新しい概念を学ぶ必要がないからです。(新しいマニュアルを書く必要がないため、私たちにとっても良いことです!)
すべてのTkウィジェットをLuaにマッピングしました。Luaテーブルコンストラクタを使用して、ウィジェットとその属性を記述します。たとえば、例3(a)はボタンを作成し、`b`に格納します。`b`は、ボタンを表すオブジェクトになります。この定義の後、通常のLua構文を使用してオブジェクトを操作できます。したがって、`b.label="Hello world from Lua!"`という代入は、ボタンのラベルを変更し、それが既に画面に表示されている場合はその画像を更新します。20文字に制限されたテキスト入力ウィジェットは`e=entry{width=20}`で作成できます。このウィジェットが表示されたウィンドウにマッピングされた後、`e.current`には、ユーザーがウィジェットに割り当てた現在の値が含まれます。(Tcl/Tkのようにグローバル変数を使用する代わりに、ウィジェット値を格納するために`current`フィールドを使用します。)
ウィジェットは自動的にウィンドウにマッピングされません。Tk/Tcl環境とは異なり、Tk/Luaには現在のウィンドウという概念はありません。他のウィジェットを保持するウィンドウ(メインウィンドウまたはトップレベルウィジェットにすることができます)を作成し、それを明示的に画面にマッピングする必要があります。例3(b)を参照してください。
このようにして、ユーザーは自由にダイアログを記述し、必要に応じてウィジェットを相互参照してマッピングできます。また、レイアウトを記述的な方法で指定する方が自然であるため、ウィジェットを明示的にパックする必要もなくなりました。したがって、ウィンドウ(メインとトップレベル)およびフレームウィジェットは、コンテンツを自動的にパックするコンテナとして使用されます。たとえば、2つのボトムボタン付きのメッセージを表示するには、例3(c)のように記述できます。すべての通常のTkウィジェットに加えて、Xlibへの簡素化されたAPIを使用するキャンバスとOpenGLを使用するキャンバスの2つを追加で実装しました。
これらのライブラリによって提供されるほとんどすべての関数はLuaにマッピングされました。そのため、カスタムウィジェットに対する直接操作を使用する洗練されたグラフィカルアプリケーションをLuaだけで作成できます。
すべてのTkウィジェットコマンドは、Tk/Luaではオブジェクトメソッドとして実装されています。その名前、パラメータ、機能は保持されています。`lb`がリストボックスウィジェットを表す場合、`lb:insert("New item")`はリストに新しいアイテムを挿入し、リストボックスのTk`insert`コマンドに従います。一方、最も使用頻度の高いTkウィジェットコマンドである`configure`は、その効果が単純な代入で得られるようになったため、もはや必要ありません。
`w`がウィンドウを表す場合、`w:iconify()`は通常の効果を持ちます。メインウィジェットとトップレベルウィジェットはウィンドウマネージャからメソッドを継承します。
Tk/Luaの実装は難しくありませんでした。Tcl/TkのCインターフェースを使用して、サービスプロバイダーを作成し、Luaからアクセスできるように登録しました。バインディングを実装するLuaコードは、前に述べたindexフォールバックを使用したオブジェクト指向のアプローチを使用します。各ウィジェットインスタンスはクラスオブジェクトから継承され、ウィジェットクラスが階層のトップにあります。このクラスは、すべてのウィジェットで使用される標準メソッドを提供します。リスト10は、この汎用クラスとそのウィジェットにフォーカスを設定するメソッドの定義を示しています。ボタンのクラス定義も示しています。
ご存じのとおり、各ウィジェットはテーブルコンストラクタで作成されます。コンストラクタはインスタンスクラスを設定し、ウィジェットを作成し、それをグローバル配列に格納します。ただし、小さなトリックも使用します。新しいテーブルを返す代わりに、コンストラクタはウィジェットの位置を数値IDとして返します(リスト11)。
したがって、`b.label`のようにLuaがウィジェットのインデックス付けを試行すると、数値はインデックス付けできないため、フォールバックが呼び出されます。このトリックにより、ウィジェットのセマンティクスを完全に制御できます。たとえば、`b`がボタン(実際には`b`はIDを格納)であり、`b.label = "New label,"`を設定した場合、フォールバックはウィジェットを更新するために適切なサービスコマンドを呼び出す責任があります。
リスト12はTk/Luaの「settable」フォールバック関数を示しています。このフォールバックは、非テーブル値のインデックス付けを試行するたびに呼び出されます。まず、最初の引数が有効なウィジェットIDに対応しているかどうかをチェックします。対応している場合は、グローバル配列にアクセスしてウィジェットテーブルを取得します。そうでない場合は、発生を以前に登録されたフォールバックにディスパッチします。
テーブル`tklua_IDtable`のウィジェットには、対応するTkウィジェット名を格納する`tkname`という内部フィールドがあります。この名前はTkコマンドを呼び出すために使用されます。対応するTkウィジェットが存在し、インデックス値が有効なTk属性であるかどうかを確認します。有効であれば、サービスプロバイダーにウィジェット属性の変更を要求します(登録されたC関数`tklua_configure`を呼び出します)。`h[f]=v`という代入により、ウィジェットテーブルを使用してTk属性以外の値を格納できることが保証されます。
「gettable」フォールバックの実装は同様です。これらの2つのフォールバックに加えて、Tk/Luaは継承の実装にもindexフォールバックを使用し(リスト5)、ウィジェットコマンドまたはウィンドウマネージャコマンドを呼び出すために「function」フォールバックを使用します。
拡張言語は、何らかの方法で常に解釈されます。単純な拡張言語は、ソースコードから直接解釈できます。一方、埋め込み言語は通常、複雑な構文とセマンティクスを持つ強力なプログラミング言語です。埋め込み言語のためのより効率的な実装テクニックは、現在標準になっています。言語のニーズに合った仮想マシンを設計し、拡張プログラムをこのマシンのバイトコードにコンパイルし、バイトコードを解釈することで仮想マシンをシミュレートします。Luaの実装にはこのハイブリッドアーキテクチャを選択しました。これは、字句解析と構文解析が一度だけ実行されるため、実行速度が向上するためです。また、拡張プログラムを事前にコンパイルされたバイトコード形式でのみ提供できるため、ロード速度が向上し、より安全な環境になります。
(a) t = {} -- empty table t[1] = i t[2] = i*2 t[3] = i*3 t[4] = i+j s = {} s.a = x -- same as s["a"] = x s.b = y (b) t = {i, i*2, i*3, i+j} s = {a=x, b=y}
(a) function object:method(params) ... end (b) function object.method(self, params) ... end
(a) b = button{label = "Hello world!" command = "exit(0)" } (b) w = toplevel{b} w:show() (c) b1 = button{label="Yes", command="yes=1"} b2 = button{label="No", command="yes=0"} w = toplevel{message{text="Overwrite file?"}, frame{b1, b2; side="left"}; side="top" }
文字列 | 条件 |
"arith" | 無効なオペランドでの算術演算。 |
"order" | 無効なオペランドでの順序比較。 |
"concat" | 無効なオペランドでの文字列連結。 |
"getglobal" | 定義されていないグローバル変数の値の読み取り。 |
"index" | テーブルに存在しないインデックスの値の取得。 |
"gettable" | 非テーブル値のインデックスの値の読み取り。 |
"settable" | 非テーブル値のインデックスの値の書き込み。 |
"function" | 非関数値の呼び出し。 |
"gc" | ガベージコレクション中に収集される各テーブルに対して呼び出されます。 |
"error" | 致命的なエラーが発生したときに呼び出されます。 |
#include <stdio.h> #include "lua.h" int main() { char line[BUFSIZ]; while (fgets(line,sizeof(line),stdin)!=0) lua_dostring(line); return 0; }
function map(list, func) local newlist = {} local i = 1 while list[i] do newlist[i] = func(list[i]) i = i+1 end return newlist end
list = {} i = 4 while i >= 1 do list = {head=i,tail=list} i = i-1 end
S = Separator{ drawStyle = DrawStyle{style = FILLED}, material = Material{ ambientColor = {0.377, 0.377, 0.377}, diffuseColor = {0.800, 0.771, 0.093}, emissiveColor = {0.102, 0.102, 0.102}, specularColor = {0.0, 0.0, 0.0} }, transform = Transform{ translation = {64.293, 20.206, 0.0}, rotation = {0.0, 0.0, 0.0, 0.0} }, shape = Sphere{radius = 10.0} }
function Inherit(t,f) if f == "parent" then -- avoid loops return nil end local p = t.parent if type(p) == "table" then return p[f] else return nil end end setfallback("index", Inherit)
TNumber="number" TPoint={x=TNumber, y=TNumber} TColor={red=TNumber, blue=TNumber, green=TNumber} TRectangle={topleft=TPoint, botright=TPoint} TWindow={title="string", bounds=TRectangle, color=TColor}
function checkType(d, t) if type(t) == "string" then -- t is the name of a type return (type(d) == t) else -- t is a table, so d must also be a table if type(d) ~= "table" then return nil else -- d is also a table; check its fields local i,v = next(t,nil) while i do if not checkType(d[i],v) then return nil end i,v = next(t,i) end end end return 1 end
function save() -- create table to hold environment local env = {} -- get first global var and its value local n, v = nextvar(nil) while n do -- save global variable in table env[n] = v -- get next global var and its value n, v = nextvar(n) end return env end
function runProtected(code) -- save current environment local oldenv = save() -- erase "dangerous" functions readfrom,writeto,execute = nil,nil,nil -- run untrusted code dostring(code) -- restore original environment restore(oldenv) end
widgetClass = {} function widgetClass:focus() if self.tkname then tklua_setFocus(self.tkname) end end buttonClass = { parent = widgetClass, tkwidget = "button" }
function button(self) self.parent = classButton tklua_ID = tklua_ID + 1 tklua_IDtable[tklua_ID] = self return tklua_ID end
function setFB(id, f, v) local h = tklua_IDtable[id] if h == nil then old_setFB(id,f,v) return end if h.tkname and h:isAttrib(f) then tklua_configure(h.tkname,f,v) end h[f] = v end old_setFB = setfallback("settable",setFB)