Guile によるスクリプティング

C と Scheme を強化する拡張言語

Scheme は Lisp を簡易化して派生させた言語で、1958年に初めてJohn McCarthy によって紹介されました。その Scheme 言語のインタープリターとして 1995年に Guile は登場しました。Guile はインタープリターである一方で、Scheme を組み込み可能な言語にします。このことから、Guile は組み込みスクリプトに最適なインタープリターとなっています。Guile は単なる拡張言語の 1 つではなく、GNU プロジェクトの公式拡張言語です。gEDA CAD ツールから (Scheme スクリプトを利用した動的な構成が可能な) Scwm (Scheme Constraints Window Manager) に至るまで、多数のオープンソース・アプリケーションで、スクリプティングに Guile を使用しています (「参考文献」セクションのリンクを参照)。これまでスクリプティングによるアプリケーション拡張は、GNU Emacs、GIMP、そして Apache Web Server など、大きな成功を収めてきましたが、Guile もそれに続いています。

Guile の背後にある鍵は、拡張性です。図 1 を見てください。Guile では、Scheme スクリプトを解釈したり、コンパイル済みの C プログラムに動的にバインドしたりできるだけでなく、コンパイル済み C 関数を Scheme スクリプトに統合することもできます。この貴重な機能が意味することは、ユーザーがアプリケーションを調整、あるいはカスタマイズして、それぞれに固有の付加価値を与えられるということです。

図 1. Guile を使用したスクリプティング・モデル

アプリケーションをカスタマイズする代表的な例の 1 つは、ビデオ・ゲーム業界です。ビデオ・ゲームは、スクリプティングによって大々的なカスタマイズを行っています。さらに、多くのゲーム・プログラムでは、コア設計にもスクリプティングを利用しており、ある特定の側面 (ノンプレイヤー・キャラクターの振る舞いなど) では実装をスクリプトで行っているほどです。

単純な例

まずは、Guile を C 言語プログラムに統合する単純な例を見てみましょう。この例では、Scheme スクリプトを呼び出す C プログラムを使用します。リスト 1 とリスト 2 に、この最初の例のソース・コードを記載します。

ゲームにおけるスクリプト 最近のゲームには、スクリプト言語が組み込まれているのが一般的です。これには、従来の Python や Ruby などといったインタープリターで解釈される言語から、UnrealScript といった特殊用途のスクリプト言語まで含まれます (「参考文献」セクションのリンクを参照)。ゲーム・システムでは、ノン・プレイヤー・キャラクターの振る舞いや、さらにはゲームに現れる物の動きを実装するためにもスクリプト言語を使用することができます。ゲーム開発は、スクリプトで行ったほうが便利です。その理由は、新たな振る舞いや動きを導入する際に長時間に及ぶコンパイル・サイクルが不要になるからです。PC で動作するお気に入りのゲームのサブディレクトリーを調べてみてください。大抵は、スクリプトが見つかるはずです。

リスト 1 は、Scheme スクリプトを呼び出す C アプリケーションです。最初に目に付くのは、libguile.h ヘッダー・ファイルがインクルードされていることです。これによって、必要な Guile シンボルが使用できるようになります。次に、 SCM という新しい型が定義されていることに注目してください。この型は、Guile 内に含まれるすべての Scheme オブジェクトを表す C の抽象的な型です。このリストでは、後で呼び出す Scheme 関数を示しています。

Guile を使ったスレッドで常に最初に実行しなければならないことは、 scm_init_guile の呼び出しです。この関数は Guile のグローバル状態を初期化するので、他の Scheme 関数を呼び出す前に呼び出さなければなりません。次に、呼び出す Scheme 関数が含まれるファイルをロードする必要があります。そのために使用するのは、 scm_c_primitive_load 関数です。この関数の名前で注意する点として、 _c_ は、この関数が Scheme 変数ではなく、C 変数に渡されることを示します。

このリストでは続いて scm_c_lookup を使用して、シンボル (モデル内の Scheme 関数) によってバインドされた変数を見つけて返します。その後、この変数は scm_variable_ref で逆参照されて Scheme 変数 func に格納されます。そして最後に、Scheme 関数を呼び出すために scm_call_0 を使用します。この Guile 関数は、事前に定義された Scheme 関数を引数なしで呼び出します。

リスト 1. Scheme スクリプトを呼び出す C プログラム

#include <stdio.h> #include <libguile.h> int main( int argc, char **arg ) { SCM func; scm_init_guile(); scm_c_primitive_load( "script.scm" ); func = scm_variable_ref( scm_c_lookup( "simple-script" ) ); scm_call_0( func ); return 0; }

リスト 2 に記載するのは、C プログラム内から呼び出される Scheme 関数です。この関数は display プロシージャーを使ってストリングを画面に出力します。この関数に続いて呼び出されるのは newline プロシージャーです。このプロシージャーによってキャリッジ・リターンが出力されます。

リスト 2. C から呼び出される Scheme スクリプト (script.scm)

(define simple-script (lambda () (display "script called") (newline)))

ここで興味深い点は、このスクリプトが C プログラムに静的にバインドされていないことです。スクリプトは動的にバインドされます。Scheme スクリプトは変更可能で、事前コンパイル済みの C プログラムが実行されるときには、このスクリプトに実装された新しい振る舞いが実行されます。これこそが、組み込みスクリプトの威力です。つまり、コンパイル済みアプリケーションの処理速度を落とすことなく、動的スクリプティングが持つ拡張性という機能をもたらすことができます。

単純な例を理解したところで、今度はもう少し踏み込んで、C 言語内での Scheme スクリプトの他の要素について探ってみましょう。

Scheme の概要

Scheme に馴染みがない読者のために、この言語の特徴を示す具体的な例をいくつか見てみましょう。ここに紹介する例は、変数、条件構文、ループ、そして Scheme の重要ないくつかの機能を説明するものです。この記事では、Scheme についての完全な説明はしませんが、「参考文献」セクションに参考となる資料へのリンクを記載しています。

以降の例では、Guile インタープリターを使用することで、Scheme をリアルタイムで操作して Scheme コードを提供し、その結果を即座に確認します。

変数

Scheme は動的型付け言語です。そのため通常は、変数の型は実行時までわかりません。Scheme 変数は単なるコンテナーであり、その型は後から定義することができます。

変数は define プリミティブを使用して作成します。作成した後は、 set! プリミティブによって変更します。以下はその一例です。

guile> (define my-var 3) guile> (begin (display my-var) (newline)) guile> (set! my-var (* my-var my-var))

プロシージャー

当然のことながら、Scheme ではプロシージャーを作成することも可能です。プロシージャーを作成する場合も、やはり define プリミティブを使用します。プロシージャーは匿名にすることも (ラムダ・プロシージャー)、名前付きにすることもできます。名前付きプロシージャーの場合は、以下に示すように変数に格納されます。

(define (square val) (* val val))

Lisp を使い慣れている方にとって、上記の構文は従来の Lisp の構文とは異なりますが、いくらか読みやすくなっています。このように定義した後は、この新規プロシージャーを他のあらゆるプリミティブと同じように使用することができます。以下はその一例です。

guile> (square 5) 25

条件構文

Scheme には、条件分岐を行う方法がいくつかあります。そのうち最も基本的なのは、単純な if 条件です。if 条件で指定するのは、テスト条件、真の場合の式、そしてオプションで偽の場合の式です。以下の例を見ると、Scheme でのリスト処理の方法がわかるはずです。このリストは if で始まり、 (display "less") で終わっています。Scheme は Lisp から派生していることから、リストで構成されることを思い出してください。Scheme はコードとデータの両方をリストとして表すため、コードとデータとの境界があいまいになります (データとしてのコード、コードとしてのデータ)。

guile> (define my-var 3) guile> (if (> my-var 20) (display "more") (display "less")) less

ループ

Scheme では、再帰によってループを実装します。そのためループを実装するときには特定の観念が必要となりますが、繰り返し処理に再帰を使うのは自然なことです。以下の例に、0 から 9 を繰り返し処理してから「 done 」と出力する Scheme スクリプトを記載します。この例では、Scheme で末尾再帰と呼ばれるものを使用しています。ループの終わりで、引数を毎回 1 ずつインクリメントして同じ関数を再帰的に呼び出すことによって、ループの繰り返しを実装している点に注目してください。このような再帰は、従来の言語では呼び出しの履歴を保持するスタックを消耗させることになりますが、Scheme の場合は違います。最後の呼び出し (末尾) が、プロシージャー呼び出しやスタック保守のオーバーヘッドをもたらすことなく、関数を単純に呼び出します。

(let countup ((i 0)) (if (= i 10) (begin (display "done") (newline)) (begin (display i) (newline) (countup (+ i 1)))))

Scheme にはもう 1 つの興味深いループ方法として、 map プロシージャーを使用するという方法もあります。これは、以下の例に示すように、単にプロシージャーをリストに適用 (つまり、マップ) するという概念です。この方法は、読みやすさと単純さの両方の点において優れています。

guile> (define my-list '(1 2 3 4 5)) guile> (define (square val) (* val val)) guile> (map square my-list) (1 4 9 16 25)

Scheme スクリプトによる C プログラムの拡張

リスト 1 から明らかなように、Scheme では比較的簡単に C プログラムを拡張することができます。今度は別の例で、C を Scheme に結び付けるために使用できる、その他のアプリケーション・プログラミング・インターフェース (API) について検討します。ほとんどのアプリケーションでは、Scheme を呼び出すだけでなく、引数を Scheme 関数に渡し、戻り値を受け取って、変数を 2 つの環境で共有しなければなりません。Guile には、この機能を可能にする関数が豊富に用意されています。

Guile は 2 つの環境の境界を越えて Scheme の機能を C にまで拡張しようとしています。この点に関しては、動的な型、継続、ガーベッジ・コレクションなどの Scheme の概念が Guile API によって C に拡張されていることがわかるはずです。

Scheme の概念を C に拡張している一例には、C 環境で動的に新しい Scheme 変数を作成する機能が挙げられます。Scheme 変数を作成するために使用する C 関数は、 scm_c_define です。前述のとおり、 _c_ は、C の型を引数として提供していることを示しています。Scheme 変数 ( scm_c_lookup 関数によって提供された変数) が既にある場合には、代わりに scm_define を使用することができます。C で Scheme 変数を作成できるだけでなく、Scheme 変数を逆参照して 2 つの環境の間で値を変換することもできます。リスト 3 に、この一連の例を示しています。

リスト 3 とリスト 4 のそれぞれに、C と Scheme との相互作用を表す 2 つの例を示します。リスト 3 の最初の例では、C から Scheme 関数を呼び出し、引数を渡して戻り値を取得する方法を示しており、2 番目の例では引数を渡すための Scheme 変数を作成しています。リスト 4 に示す 2 つの Scheme 関数は同じ振る舞いを実装しますが、最初の関数では引数を使用し、2 番目の関数では静的変数を使用しています。

scm_call の制約事項 Guile には、 scm_call の 5 つのバリエーションが用意されています。Scheme 関数を呼び出す際の引数の個数が 0 個の場合 ( scm_call_0 ) から、最大 4 個の場合 ( scm_call_4 ) までがあります。これによって、Guile から渡される Scheme 変数の数が 4 個までに制限されます。また、引数の個数が可変の関数はサポートされません。引数が 4 個を超える場合、あるいは引数の個数が可変の場合には、Scheme リスト・オブジェクトを必要な引数の数で構成するという方法を使うことができます。

リスト 3 の最初の例では、単純に scm_call_1 関数を使用して、引数が 1 個の Scheme 関数を呼び出しています。この場合、関数には Scheme の値を渡す必要があることに注意してください。従って、C の整数値を Scheme の数値データ型に変換するために、 scm_int2num 関数を使用します。逆に、Scheme 変数 ret_val を C の整数値に変換するには scm_num2int を使用します。

リスト 3 の 2 番目の例では最初に、 scm_c_define を使用して Scheme 変数を新規に作成します。この関数を識別する C ストリング変数 ( sc_arg ) は、型変換関数 scm_int2num によって自動的に初期化されます。Scheme 変数が作成されたら、あとは単に Scheme 関数 square2 を呼び出して (この場合は引数を使用しません)、同じプロセスに従って戻り値を取得し、逆参照することができます。

リスト 3. C における Scheme 関数と Scheme 変数の扱い方

#include <stdio.h> #include <libguile.h> int main( int argc, char *argv[] ) { SCM func; SCM ret_val; int sqr_result; scm_init_guile(); /* Calling the square script with a passed argument */ scm_c_primitive_load( "script.scm" ); func = scm_variable_ref( scm_c_lookup( "square" ) ); ret_val = scm_call_1( func, scm_int2num(7) ); sqr_result = scm_num2int( ret_val, 0, NULL ); printf( "result of square is %d

", sqr_result ); /* Calling the square2 script using a Scheme variable */ scm_c_define( "sc_arg", scm_int2num(9) ); func = scm_variable_ref( scm_c_lookup( "square2" ) ); ret_val = scm_call_0( func ); sqr_result = scm_num2int( ret_val, 0, NULL ); printf( "result of square2 is %d

", sqr_result ); return 0; }

リスト 4 に、リスト 3 の C プログラムが使用する 2 つの Scheme プロシージャーを記載します。1 番目のプロシージャー、 square は、引数を 1 つ取って結果を返す従来の Scheme 関数です。2 番目の square2 プロシージャーは引数を取りませんが、代わりに Scheme 変数 ( sc_arg ) を扱います。最初のプロシージャーと同様、この変数も結果を返します。

リスト 4. リスト 3 から呼び出される Scheme スクリプト (script.scm)

(define square (lambda (x) (* x x))) (define square2 (lambda () (* sc_arg sc_arg)))

C 関数による Scheme スクリプトの拡張

最後に紹介する例では、Scheme スクリプトから C 関数を呼び出すプロセスを見ていきます。まず始めに取り掛かるのは、Scheme から呼び出し可能な関数です (リスト 5)。このリストで真っ先に気付くことは、これは C 関数でありながらも、Scheme オブジェクトを引数として受け取り、応答で Scheme オブジェクト ( SCM 型) を返すという点です。プログラムではまず、 SCM 引数を取得するために使用する C 変数を、 scm_num2int 関数 (Scheme 数値型を C int に変換する関数) を使って作成します。その上で、引数を二乗し、別の scm_from_int を呼び出して結果を返します。

リスト 5 のプログラムの残りの部分では、Scheme で起動するように環境をセットアップしています。Guile 環境を初期化した後、C 関数を Scheme にエクスポートするために scm_c_define_gsubr を呼び出します。この関数が取る引数は、Scheme での関数の名前、引数 (必須、オプション、その他) の数、そしてエクスポートする実際の C 関数です。あとは前に説明したとおりで、Scheme スクリプトをロードし、特定の Scheme 関数への参照を取得して、引数なしで関数を呼び出します。

リスト 5. 環境を Scheme 用にセットアップするための C プログラム

#include <stdio.h> #include <libguile.h> SCM c_square( SCM arg) { int c_arg = scm_num2int( arg, 0, NULL ); return scm_from_int( c_arg * c_arg ); } int main( int argc, char *argv[] ) { SCM func; scm_init_guile(); scm_c_define_gsubr( "c_square", 1, 0, 0, c_square ); scm_c_primitive_load( "script.scm" ); func = scm_variable_ref( scm_c_lookup("main-script") ); scm_call_0( func ); return 0; }

リスト 6 に、Scheme スクリプトを記載します。このスクリプトは、リスト 5 の C プログラムでエクスポートされた関数、 c_square の呼び出しに対する応答を表示します。

リスト 6. C 関数を呼び出す Scheme スクリプト (script.scm)

(define main-script (lambda () (begin (display (c_square 8)) (newline))))

平凡な例ですが、この例から、2 つの言語環境の間でコードと変数を簡単に共有できることがおわかりでしょう。

まとめ

静的なソフトウェアや製品を作成して配布する時代はもう終わりました。今やユーザーは、動的で、しかも簡単にカスタマイズできる製品を期待しています。この進化には新たな複雑さが伴いますが、最終的にはユーザーが、私たちが作成するアプリケーションに新しい価値を生み出す方法を教えることになるはずです。この記事を読んで、読者の皆さんが Guile の威力を少しでも理解していただけたことを願います。Scheme は現在使われているプログラミング言語のなかでも最古のものの 1 つかもしれませんが、未だに最強を誇るプログラミング言語の 1 つでもあります。Guile が、この Scheme をさらに協力かつ有益なものにすることに成功したからです。

ダウンロード可能なリソース

関連トピック