Last update 1999/08/07

Scheme処理系の制作 第4回

(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎


ちょっと反省

前回説明した継続の構造ですが、やっぱしヤメました。

なんでかと言うと、やっぱり毎ステップ継続オブジェクトを作るのは、ちょっとバカ正直すぎというか正直バカすぎみたいだからです。

そういうわけで、汎用スタックを上手に使うようにして毎ステップ継続を生成しないように内部構造を変更しました。結構大幅な変更になりましたが、副作用として組み込みマクロ・関数のコードは若干直感的になったのでヨシとします。

で、どこまですすんだの?

進捗状況ですが、実は2週間前にはほとんど完成してました。その後はDLLとしてのインターフェイスを整えたり上に書いたような構造の変更を行ったりするのに費やされていました。プログラム時間というものは、得てしてこういう一見どうでもよさそうなものに取られるものだと言う典型的な例ですね。基本システムはほとんど3日でできたというのに。

R5RSの言語仕様的には95%(当社比)完成しています。API仕様もほぼ納得いくものになりました。

動作スピード・メモリ効率的にはまだかなり不満ですが、アクションゲームに使うのでもなければ特に問題ない(今のままでも使おうと思えば使えるけど)と思われますし、何より自分が使うのには十分なので、この状態を第1安定バージョンとすることにしました。

改名

ついでに、この機会に処理系を正式に命名することにしました。名づけて、

Vism
Vism is scheme-based macro-processor

です。私のメインキャラがVケンなのはぜんぜん関係ありません。苦しいですか?(笑)

使ってみようVism

ということで、今回のメインテーマはVismDLLをスクリプトサーバとしたクライアントアプリケーションを作って見ようという話です。といっても今のところ公私共に割と予定が詰まっており趣味でゲームを作ってる余裕はありませんので、以下のようなツールを作ろうと考えています。

コンソールアプリケーション
ユーザから入力を受け取ってサーバに構文解析および評価をさせるアプリケーション。
オブジェクトブラウザ
ユーザから入力を受け取ってサーバに構文解析および評価をさせ、その値をビジュアルに表示するアプリケーション。

前者ではVismの基本概念を、後者ではVismを使ったデータハンドリングの実践を説明することになるでしょう。

コンソールアプリケーション

いきなりですが、以下がコンソールアプリケーションのソースです。

#include "stdafx.h"
#include "../vism/VismDLL.h"

bool    console_input;
bool    console_output;

// [6]
int input_callback_function(void)
{
    return std::cin.get();
}

void output_callback_function(int ch)
{
    std::cout << (char)ch;
}

void error_callback_function(int ch)
{
    std::cerr << (char) ch;
}

void main_loop(VCONTEXT context,std::istream& is,std::ostream& os,std::ostream& es)
{
    VISMOBJ last=NULL;
    for(;;){
        // プロンプト
        if(console_input){
            os << "> ";
            os.flush();
        }

        // さっきの戻り値のマーク解除
        if(last!=NULL){
            VIUnmark(context,last);
        }

        // ライン入力
        
        // [7]
        VISMOBJ r=VIReadFromInputPort(context,VIGetCurrentInputPort(context));
        if(r!=NULL){
            // 評価
            clock_t start=clock();

            // [8]
            VISMOBJ p=VIEval(context,r);
            clock_t finish=clock();
            double duration = (double)(finish - start) / CLOCKS_PER_SEC;
            if(p!=NULL){
                // [11]
                VIMark(context,p);
            }

            // [10]
            const char*     emes;
            VERROR          ecode;
            ecode=VIError(emes);

            if(ecode!=vismerr_ok){
                es << emes << '\n';
            } else {
                // [9]
                VIWriteToOutputPort(context,p,VIGetCurrentOutputPort(context));
                if(console_input){
                    os << " (" << duration << "sec.)";
                }
                os << '\n';
                os.flush();
            }
            // [12]
            os << VIGC(context) << '\n';

        } else {
            break;
        }
    }

}

int main(int argc, char* argv[])
{
    console_input   =_isatty(_fileno(stdin))  ? true : false;
    console_output  =_isatty(_fileno(stdout)) ? true : false;

    if(console_input){
        std::cout << "Vism-d [Vism console client]\n";
        std::cout << "Copyright (C) 1999 Naoyuki Hirayama (hirayama@fujiyama.jpn.org)\n";
    }

    // [1]
    VCONTEXT context    =VICreateContext();

    // [3]
    VISMOBJ  input_port =VIOpenCallbackInputPort (context,input_callback_function);
    VISMOBJ  output_port=VIOpenCallbackOutputPort(context,output_callback_function);
    VISMOBJ  error_port =VIOpenCallbackOutputPort(context,error_callback_function);
    
    // [4]
    VISetCurrentInputPort (context,input_port);
    VISetCurrentOutputPort(context,output_port);
    VISetCurrentErrorPort (context,error_port);

    main_loop(context,std::cin,std::cout,std::cerr);

    // [5]
    VICloseCallbackInputPort (context,input_port);
    VICloseCallbackOutputPort(context,output_port);
    VICloseCallbackOutputPort(context,error_port);

    // [2]
    VIDestroyContext(context);

    return 0;
}

いかがですか? これが全ソースです。短い上に、そのほとんどがユーザ入力を受け取るためのもので、実際にVismを扱っているのはごく一部に過ぎないことが分かると思います。エラー処理も省いてしまえば、大事なコードは数行しかありません。

順番に解説していくことにしましょう。まずはmain関数を見てください。

コンテキストの生成・削除

Vismによるソースの読み取り、および評価はすべて「コンテキスト」と呼ばれるオブジェクトのもとで行われます。コンテキストには、変数の束縛やスタックなど評価に必要なデータのほとんどが詰め込まれています。そのため、コンテキストを複数作っても、お互いにまったく干渉しません(がその代わり、schemeレベルでのデータのやりとりもまったくできないので注意が必要です。Cレベルで行う分には特に問題ありません)。

コンテキストを生成するには、以下のAPIを用います。ソース上では//[1]のコメントのついた部分です。

VCONTEXT VICreateContext(void);

ここで、VCONTEXTは特定のコンテキストを指すハンドルであり、関数名の先頭の「VI」はVismのAPIの共通プレフィクスとなっています。この関数を呼び出すことにより、新しいコンテキストオブジェクトが生成され、そのオブジェクトへのハンドルが戻りとして返されるのです。

VICreateContextで生成したコンテキストを削除するには、//[2]のように

void VIDestoryContext(VCONTEXT);

を用います。このAPIを呼び出すと引数としてあたえられたコンテキストが削除され、ハンドルは無効になります。

VismのほとんどすべてのAPIがVCONTEXTを必要としますので、これらのAPIは必然的にアプリケーションの開始直後/終了直前、およびそれに準ずるタイミングで呼び出されることになるでしょう。

schemeオブジェクトの扱い

VISMOBJハンドル

scheme処理系で扱われる「文字列」「整数」「ペア」などのオブジェクトは、クライアントアプリケーションからはVISMOBJハンドルで扱われます。このハンドルをプリミティブアクセサAPIに渡すことによって、整数・文字列・真偽値などの実体を得ることができるというわけです。

ストリーム

普通、言語処理系では、ファイルや標準入力からソースを得て、ファイルや標準出力に処理結果やコンパイル済みのコードを書き出す仕組みになっています。またインタプリタでは、スクリプト自体もデータの処理に標準入出力やファイル入出力を前提とするのが普通です。

schemeでもこの例にもれず「標準入力ポート」「標準出力ポート」という概念があり、それぞれOSの標準入出力に相当する機能を仕様的に要求しています。そこで、Vismはデフォルトでは標準入出力をこれらに割り当てていますが、ウィンドウアプリケーションでは必ずしも標準入出力に存在意義があるとは限らないし、クライアントアプリケーション自体がスクリプトインタプリタのフロントエンドになりたい、あるいはユーザから隠蔽したいことも少なくないハズです。

そこでVismでは、schemeソースやschemeプログラムが必要とするデータを、ファイルや標準入出力からだけではなく文字列やコールバック関数から得ることができるようにしました。

コールバックストリーム

main関数の//[3]〜//[5]は、この設定をしています。順に説明しましょう。

VISMOBJ VIOpenCallbackInputPort (VCONTEXT,icallbackstream_t);
VISMOBJ VIOpenCallbackOutputPort(VCONTEXT,ocallbackstream_t);

これらのAPIは、それぞれコールバック入力/出力ポートの作成を行います。コールバック入出力ポートとは、Vismからの入出力要求があったときに、ファイルに対する入出力を行う代わりにクライアントアプリケーションが指定した関数を呼び出すカスタムポートを指します。言うまでもなく、各関数の第2引数で、入出力時に呼ばれる関数へのポインタを指定します。

コールバック関数に関しては、//[6]からの関数3つを参考にしてください。意味は読んで普通に想像するとおりです。

VISM_API void VISetCurrentInputPort (VCONTEXT,VISMOBJ);
VISM_API void VISetCurrentOutputPort(VCONTEXT,VISMOBJ);
VISM_API void VISetCurrentErrorPort (VCONTEXT,VISMOBJ);

これらのAPIを用いて、Vismの標準入出力(及び標準エラー出力)を、新たに生成した入出力ポートなどで置きかえることができます。このコンソールクライアントでは、先ほど作ったコールバック入出力ポートで置き換えているわけです。そのため、例えばschemeのdisplay関数が(ポートを指定する)第2引数なしで呼ばれると、//[6]からの関数が呼ばれることになるわけです。

VISM_API void VICloseCallbackInputPort (VCONTEXT,VISMOBJ);
VISM_API void VICloseCallbackOutputPort(VCONTEXT,VISMOBJ);

当然これらは、//[3]で開いたコールバック入出力ポートを閉じるAPIです。

構文解析

さて、コンテキストおよび入出力ポートの準備が整ったので、これでコンソールからの入出力を行うことができます。もちろん、ユーザから受け取ったschemeプログラムを読み取らせるような必要がなければ(普通ゲームのスクリプトとして使うような場合はそうだと思いますが)ポートの用意も必要ありませんが、コンソールアプリケーションでは対話が必要なので用意しました。

読み取りには以下の//[7]のように以下のAPIを用います。

VISM_API VISMOBJ VIReadFromInputPort(VCONTEXT,VISMOBJ);
VISM_API VISMOBJ VIReadFromString   (VCONTEXT,const char*);
VISM_API VISMOBJ VIReadFromFile     (VCONTEXT,const char*);

上からそれぞれ、

  1. 入力ポートからの入力(第2引数は入力ポートのハンドル)
  2. C文字列からの入力(第2引数はC文字列へのポインタ)
  3. ファイルからの入力(第2引数はファイル名)

を行うAPIです。今回はコールバック入出力ポートを用意してあるので、1.を使っています。ゲームなどでは2.を使うケースが多くなるでしょう。

これらのAPIでは、テキストを構文解析して内部形式に変換し、その内部形式オブジェクトへのハンドルを返します。

評価

「構文解析」では、テキストを読み込んで内部形式に変換するだけです。これをschemeのプログラムとして扱うには、「評価」する必要があります。

//[8]の部分でそれを行っています。内部形式オブジェクトを評価するAPIは、

VISM_API VISMOBJ VIEval (VCONTEXT,VISMOBJ);

です。引数として与えたハンドルが指すオブジェクトを評価し、そのハンドルを返します。

今回のプログラムでは、//[8]のようになっています。

出力

schemeオブジェクトの出力には、// [9]のように、以下のAPI群を用います。

VISM_API void VIWriteToOutputPort   (VCONTEXT,VISMOBJ,VISMOBJ);
VISM_API int  VIWriteToString       (VCONTEXT,VISMOBJ,char*,int);
VISM_API void VIDisplayToOutputPort (VCONTEXT,VISMOBJ,VISMOBJ);
VISM_API int  VIDisplayToString     (VCONTEXT,VISMOBJ,char*,int);

VIWrite〜、VIDisplay〜はそれぞれschemeのwritedisplay関数に相当します。具体的に言うと、WriteReadで読み直せる形に出力するもの、displayは人間に読ませるために出力させるためのものです。

引数は想像のとおりです。戻り値があるものについては、出力した文字数を返します。

エラー処理

VismDLLのAPI群は、内部処理で実行時エラーがあってもそのまま戻る仕様になっています(戻り値があるものについては、特別な注意がない限りNULLもしくは0を返します)。では、どうやってエラーを検出するのかというと、// [10]のような仕組みになっています。

VISM_API VERROR VIError(const char*&);

引数は、エラーメッセージへのポインタを取得するためのものです。

エラー列挙子については、VismErr.hを参考にしてください。

エラーを無視してVIError以外のVismAPIを呼び出すとエラー状態がクリアされてしまいます。注意してください。

ガベージコレクション

lisp系の動的な言語ではガベージコレクションは避けては通れません。

といって、ゲームなどでは、勝手にガベージコレクタが起動してレスポンスが止まってしまったりされては困るし、裏スレッドでこっそり起動されるのも不安でしょう。

そこでVismでは、若干不便なのは認めた上でガベージコレクションを手動にしました。

ガベージコレクションには以下のAPIを使います。

VISM_API void VIMark (VCONTEXT,VISMOBJ);

VIMarkでは、オブジェクトに「削除禁止」のマークをつけます。Vismは、GCを起動したときに、Vismコンテキストが所持するオブジェクトについて

以外のオブジェクトをすべて削除してしまいます。このとき、各種のVismAPIによって生成されたオブジェクトは、このどれにも当てはまらないケースがあり、クライアント側のハンドル変数でポイントしているのにも関わらずガベージコレクションで削除されてしまうことがあります。VIMarkではこれを抑制します。

VISM_API void VIUnmark (VCONTEXT,VISMOBJ);

VIMarkでつけた「削除禁止」マークを解除します。マークされていないオブジェクトがコンテキストに含まれるシンボルから辿れなくなると、ガベージコレクションのときに削除されます。

VISM_API int VIGC (VCONTEXT);

ガベージコレクションを実行します。

Vismが抱えるオブジェクトが少なければ少ないほど短時間で済みます。オブジェクトが少なければほぼ瞬時に終わりますので、メモリのことも考えてこまめに実行するのが吉です。

戻り値は削除したオブジェクトの数です。

今後の予定

ふと気づいたんですが、WriteToFile及びDisplayToFileがありませんね。

さらに、まだコールバックscheme関数システムがありませんので、これを導入しましょう。

今現在VismAPI群は、NULLポインタを渡すと誤動作してしまうので注意してください。これは近いうちに修正する予定です。

lispでまったくヒープを使わないというわけには行きませんが、現在のシステムではそれを差し引いてもちょっと使いすぎだと感じています(特にスタック周りで)。将来的には、中小オブジェクトレベルならある程度メモリが自由に使える次世代コンシューマ機で問題なく動くようなところにまでは持っていきたいと考えています。

IObject系列のnewをオーバーロードすればいいだけのような気もしなくもないので、案外早く改善できるかもしれません。

現在のバージョンでは、構文解析までならほとんど作った内部構造の分しかメモリを使わない仕組みになっていますが、scheme関数呼び出しの式を評価すると結構メモリを消費するようになっていますので、使いわけに注意が必要です。

ダウンロード

以下のアーカイブには、コンソールアプリケーションの実行ファイル、ソース、DLL、DLLのソースが含まれています。適当にいじってあそんでみてください

今のところソースはVC++6.0用です。また、ビルドにはCygwin32が必要です。Msdevから実行する場合、実行パスの設定、unistd.h(空でよい)の用意、環境変数BISONLIB、/tmpディレクトリなどが必要なので注意してください。

うまくいかなかったら掲示板で質問してください。

vsch003.lzh


(C) 1999 Naoyuki Hirayama. All rights reserved.