外部言語連携

このガイドは今回で最終回です。最後に、VCSSLから他言語のプログラムを実行する方法についてまとめます。

※ なお、Vnanoでは、処理系がアプリ内組み込み用である都合から、他言語プログラム(主に搭載されるホストJavaアプリ側の処理)とのやり取りの方法は、ここで扱うVCSSLの場合とかなり異なります。 詳細はVnanoの公式サイト内のガイド等をご参照ください。
- 目次 -

実行ファイルを呼ぶ
(コンパイラ型言語で作成したプログラム、OSコマンド、シェル経由でのシェルスクリプト実行など)

基本的に、Java言語以外で実装したプログラムをVCSSLから呼ぶには、実行ファイル(.exe や .out など)の形になっているものを呼ぶ事になります。

実行するサンプルプログラムをC言語で実装して用意

説明のために都合がいいプログラムがあった方がわかりやすいので、とりあえずC言語で即席で作ってみましょう。以下のようなプログラムを書きます:


# include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello C and VCSSL!\n");
    return 0;
}
example.c

これを適当なコンパイラでコンパイルし、以下の名前の実行ファイルを生成してください:

example.exe (Linux 等なら example.out)

コンパイル方法については、一般のC言語の解説ガイドやWebページの方がずっと詳しいので、ここでは割愛します。

さて、この実行ファイルを実行すると、標準出力に以下のようなメッセージが出力されます:

Hello C and VCSSL!

ここまで確認できたら、次に進みましょう!

実行してみる

さて、上で作った実行ファイルをVCSSLで実行するには、一番簡単な方法だと system 関数や exec 関数が使えます。 これはもうリンク先の仕様書の通りで、呼んだら一発で実行できて終わりです。 つまり呼ぶだけなら非常に簡単です:


import File;

// example.exe を実行
// ("argA/B/C"はプログラムに渡す引数)
string programPath = getFilePath("example.exe");
system(programPath, "argA", "argB", "argC");
Exec.vcssl

が、実際にスクリプト言語から、外部の実行ファイルを呼ぶ場面って、呼ぶ処理自体を書く事よりも、むしろちゃんと走ってるかの検証とか、デバッグの方にかなり時間を要しません? で、結局、標準エラー出力を拾ったりとか、入力のタイミングを微調整したりとか、終了を待機したりとか、そういう事がほぼ必須になるわけです。よっぽど投げっぱなしでOKなプログラム以外は。

という事で、ここでは最初から、そういう事ができる方法で実行してみましょう。コードは少しだけ長くなりますが、結局それが一番早いです。 これには Process ライブラリの機能を使用します:


import File;
import Process;

// example.exe の絶対パスを取得
//(相対パスやファイル名だけだと、環境依存で失敗する可能性がある)
string programPath = getFilePath("example.exe");

// example.exe のプロセスを生成
//("argA/B/C"はプログラムに渡す引数)
string processArgs[] = { programPath, "argA", "argB", "argC" };
int processID = newProcess(processArgs);

// 実行開始
startProcess(processID);

// この例では参照されないが、必要なら標準入力を入れる事も可能
// (改行が無いと受理されない場合があるので注意: 以下のEOLは改行)
setProcessInput(processID, "Hello!" + EOL);

// 終了を待機
waitForProcess(processID);

// 標準出力と標準エラーの内容を取得
//(実行時の全出力が控えられている)
string processOutput = getProcessOutput(processID);
string processError = getProcessError(processID);

// 上記をVCSSLコンソールに出力する
println("標準出力: " + processOutput);
println("標準エラー: " + processError);
SerialExecution.vcssl

実行すると:

標準出力: Hello C and VCSSL!

標準エラー:

と、このように、example.exe が吐いた標準出力と標準エラー(今回の場合は何もなし)を取得して表示できている事が分かりますね。

ここで外部プログラムを実行しているのは newProcess 関数です。この関数の仕様は:

で、簡単に言うと、プログラムを「OS上で実行可能な形(プロセス)」として準備してくれます。そしてその戻り値 = プロセスIDを使って、

という制御ができます。他にも、ここで行っているように入出力を行ったり、色々な設定などができます(後述)。

文字化けする場合は文字コードを指定

ところで、実行するプログラムがASCII文字以外、つまりマルチバイト文字(日本語とか)を出力する場合は、しばしば文字化けが問題になります。文字化けは、実行ファイルの出力の文字コードと、VCSSL実行環境がそれを解釈する文字コードが異なる場合に生じます。

例えば、日本語の内容を吐く実行ファイルだと、文字コードはCP932(≒いわゆる Shift_JIS、厳密には僅かに違うがほぼ同じ)を使っている exe がよくあるので:


// example.exe のプロセスを生成
string processArgs[] = { programPath, "argA", "argB", "argC" };
int processID = newProcess(processArgs);

// 入出力の文字コードを設定
setProcessInputEncoding(processID, "CP932");
setProcessOutputEncoding(processID, "CP932");
setProcessErrorEncoding(processID, "CP932");

// 実行開始
startProcess(processID);
...
Encoding.vcssl

のように文字コード指定してから実行すると、(文字コードが合っていれば)文字化けが直ります。なお、文字コード無指定時のデフォルトは UTF-8 が使われます。

標準出力/エラーをイベントハンドラで拾う

さて、先程の例では、実行ファイルの標準出力や標準エラーの内容を、実行完了後に一括で取得していました。

一方、以下のようにイベントハンドラでリアルタイムに受け取る事も可能です:


import File;
import Process;

// example.exe の絶対パスを取得
//(相対パスやファイル名だけだと、環境依存で失敗する可能性がある)
string programPath = getFilePath("example.exe");

// example.exe のプロセスを生成して実行
//("argA/B/C"はプログラムに渡す引数)
string processArgs[] = { programPath, "argA", "argB", "argC" };
int processID = newProcess(processArgs);
startProcess(processID);


// プロセスの標準出力内容が流れてくるイベントハンドラ
void onProcessOutput(int sourceProcessId, string text) {
    print(text);
}

// プロセスの標準エラー内容が流れてくるイベントハンドラ
void onProcessError(int sourceProcessId, string text) {
    print(text);
}
EventExecution.vcssl

こうすると、実行中の実行ファイルが標準出力や標準エラーに何か出力する度に、onProcessOutput や onProcessError 関数に書いた処理が実行されます。引数は以下の通りです:

ただし、「1バイトでも出力される度にイベントハンドラが呼ばれる」というわけではなく、ある程度の長さまで溜められてから呼ばれます。

どの程度の長さまで溜められて呼ばれるかというと、普通は行単位です。なので、大抵の場合、text は出力の行内容だと見なす事ができます。

※ ただし、改行なしでものすごく長い内容が出力された場合(バッファ容量を超過する場合)など、途中で強制的に区切られて、分割してイベントハンドラが呼ばれる事も一応あり得ます。なので、確実に行単位だと見なす事はできないです。

さて、「わざわざイベントハンドラで出力を受け取って何がうれしいのか?」というと、それは「対話的な入出力をしないといけない場合」の対応です。実行ファイルによっては、

みたいな処理を要するものがありますよね。そういう場合って、出力がされたタイミングで、その内容に基づいて入力処理を発動しないと処理が進まないので、イベントハンドラで捌く方が都合がいいわけです。

OSコマンドを実行したい場合は… シェルを実行ファイルとして呼ぶ

さて、ここまではC言語で自作したプログラムを呼ぶ想定でしたが、他にも、OS依存のコマンドとかを呼びたい場合などがよくあるかもしれません。

そのような場合は、基本的に

という方法が無難です。

一応、シェルの実行ファイルに、コマンド列をいわゆるワンライナーの形で、直接指定する事も可能ではあります(形はシェル依存)。ただ、そもそも「プログラムから外部のプログラムを呼ぶ」事自体が、デバッグが普通よりだいぶ難しい事なので、時間を溶かす覚悟が要ります。

例えば、カレントディレクトリのパスがどこに通ってるか(必要なら通し直さないと)とかのデバッグは、シェルスクリプトの形じゃないと辛いですよね。で、結局最初からこうすりゃよかったってなりがちです。

別のスクリプト言語のコードを実行したい場合は... インタープリタを呼ぶ?

同じように、実行ファイルとして別のスクリプト言語のインタープリタを呼べば、他のスクリプト言語のコードも実行できる可能性は原理的にはあります。

が、実際それでうまく走るかどうかは、呼ばれる側のインタープリタの実装依存なので、やってみないとわかりません。

スクリプト言語のインタープリタは、普通の実行ファイルよりもはるかに複雑で、何か少しでも想定と違うと走らなかったりするので、「動いたらラッキー」くらいの感覚の方が無難だと思います。

Java言語で書いた処理を呼ぶ

さてここからは後半です。今度はJava言語で書かれた処理を、VCSSLから呼び出す方法について説明しましょう。

なぜJava言語だけ別枠で説明するかというと、他の言語とは違う呼び方ができるからです。 というのも、VCSSLの実行環境はJava言語で開発されており、ライブラリ関数の処理の中身なども、多くはJava言語で実装されています。

なので、そういう "組み込み関数" みたいなものをJava言語で実装して、VCSSLから普通の関数のように呼び出すためのインターフェースが、もともとあるわけです。それを使ってみましょう。

GPCI2 インターフェース

インターフェースの種類は結構色々あるのですが、仕様を一般公開しているものの多くは Vnano 用のもので、現在のVCSSL実行環境で堂々と利用可能になっている段階のものは限られています。

※ これは、現在のVCSSL実行環境の設計世代がVnanoよりも古いためで、次の世代ではだいぶ対応を増やす予定です。

で、呼び出しのオーバーヘッドが大きい(=数値計算などでの超高頻度呼び出しは苦手な)ものの、簡単に使えて、だいぶ初期の頃からずっとサポートされているインターフェースとして:

があります。これを使ってみましょう。

インターフェースをコンパイル

インターフェースの宣言ファイルは上のURLから落としてもいいのですが、パッケージにクラスパスを通してコンパイルするのとかが面倒くさいので、以下のコードをコピペし、好きな場所に「GeneralProcessConnectionInterface2.java」として保存してくだざい。


public interface GeneralProcessConnectionInterface2 {

    // ここにスクリプト実行前の初期化処理を実装
    public void init();

    // ここにスクリプト実行後の破棄処理を実装
    public void dispose();

    // 処理する関数名に対してだけ true を返す
    public boolean isProcessable(String functionName);

    // VCSSLから呼ばれた際に実行する処理を実装する
    public String[] process(String functionName, String[] args);
}
GeneralProcessConnectionInterface2.java

どうせリフレクションで動的に読み込まれるので、メソッド名と引数の型さえ合っていればよく、上のコードを普通にコンパイルすればそのまま使えます:

javac GeneralProcessConnectionInterface2.java

このインターフェースにはメソッドが4つ宣言されていますが、それぞれの役割はコード内コメントの通りです。

インターフェースの定義通りに、行いたい処理を書く(= プラグインの実装)

さて、上のインターフェースを別のクラスファイルで implements して、行いたい処理を記述していきましょう。

ところで、VCSSL や Vnano では、実行環境に組み込み関数などを追加するためのJava言語製プログラムの事を「プラグイン」と呼びます。つまりここではプラグインを実装するわけです。なのでクラス名は「ExamplePlugin」にします:


public class ExamplePlugin implements GeneralProcessConnectionInterface2 {

    // ここにスクリプト実行前の初期化処理を実装
    @Override
    public void init() {
        System.out.println("プラグインを初期化");
    }

    // ここにスクリプト実行後の破棄処理を実装
    @Override
    public void dispose() {
        System.out.println("プラグインを破棄");
    }

    // 処理する関数名に対してだけ true を返す
    @Override
    public boolean isProcessable(String functionName) {
        if (functionName.equals("exampleFunction")) {
            return true;
        }
        return false;
    }

    // VCSSLから呼ばれた際に実行する処理を実装する
    @Override
    public String[] process(String functionName, String[] args) {
        if (functionName.equals("exampleFunction")) {
            return new String[]{ "exampleFunction が呼ばれた: arg0=" + args[0] + ", arg1=" + args[1] };
        }

        System.err.println("※ ここに処理が達しているという事は関数名をミスタイプしてる");
        return null;
    }
}
ExamplePlugin.java

上のプラグインは何をやっているかというと:

というものになっています。

これを先程の GeneralProcessConnectionInterface2 と同じ場所に置いてコンパイルしましょう:

javac ExamplePlugin.java

これで ExamplePlugin.class ができればプラグイン完成です。

VCSSLスクリプトから呼んでみる

それではVCSSLから呼んでみましょう。上記で作った ExamplePlugin.class と同じ場所にVCSSLスクリプトのファイルを作って、以下のようなコードを記述します:


// プラグイン ExamplePlugin.class を読み込む
connect ExamplePlugin;

// 上記プラグインが提供する関数「 exampleFunction 」を呼ぶ
string ret[] = exampleFunction("Hello", "World!");

// 結果を表示
println(ret);
ExamplePluginCall.vcssl

実行結果は:

exampleFunction が呼ばれた: arg0=Hello, arg1=World!

と、この通り、プラグイン側で合成したメッセージが、確かにVCSSL側に返されている事がわかりますね。成功です。

GPCI2 インターフェースの厄介なクセを知ってカバーする

さて、上で見たように、Java言語で書いた処理をGPCI2経由で呼ぶのはかなり簡単で、「これで十分」という場面も多いので、恐らく今後もずっとサポートされ続けます。

一方で、設計世代がVCSSL初期の頃なので、当時の処理系都合による厄介な仕様があったり、オーバーヘッドが大きかったり、自由過ぎたりという厄介な点もあります。必ずしも欠点というよりは、「香ばしいクセ」みたいな感じですね。以下では、その概要と対処法を抑えておきましょう。

任意の型・個数の引数で呼び出せる: VCSSLの関数でラップして対応

GPCI2は型の制約が非常に緩く、例えばさっきの関数 exampleFunction を、整数や論理値などの引数で呼び出す事もできてしまいます。個数も何個でもOKです:


string ret[] = exampleFunction(123, 4.56, true);
QuirksAnyType.vcssl

VCSSL側から渡されたこれらの引数は、全て String 型の値に自動で変換されてから、プラグイン側の配列引数 args に渡されます。 VCSSLは静的型付けの言語なのに、めちゃくちゃ緩い振る舞いですよね。有用な場面では自由度は高いですが。

で、この仕様は、望まない場合は厄介です。例えば、プラグイン内部で引数を整数と見なして処理してるのに、VCSSL側から "あいうえお" とかが渡される可能性がある、とかは対応が面倒です。 静的型付けの言語では、そういうのは関数の宣言で弾きたいものですよね。

じゃあ具体的にどうするかというと、プラグインの関数を直接呼ぶのではなく、それをラップしたVCSSLの関数を呼ぶようにします:


// VCSSLの関数でラップし、スクリプト内ではこれを介して呼ぶようにする
// (こうすれば、引数の型や個数を厳格に検査できる)
string wrapperFunction(int arg0, int arg1) {
    return exampleFunction(arg0, arg1);
}
QuirksAnyTypeWrap.vcssl

上記みたいなラッパー関数を定義したライブラリをVCSSLで書いて、それを他のVCSSLスクリプトから import して使うようにすれば、きちんと型検査付きで呼び出せます。

引数なしで呼び出しても、プラグイン側の args は要素数 1 になる: VCSSL側のラッパーで関数オーバーロードして呼び分ける

他にも、GPCI2 には引数周りで落とし穴になる仕様が存在します。それは:

というものです。これは合理的な理由は何もなく、単に古い実装の都合を、互換性のためにずーっと引きずっているだけです。

というのも、最初期のVCSSL実行環境の内部処理では、関数の扱いで「引数の情報が無い」という状態があると都合が悪く、「何らかのプレースホルダを詰めておいて、実際には使わない」という処理をした方が好都合だったためです。C言語の void 引数みたいな感じです。あの時に一工夫していれば…

これの一番の問題は、「引数が無い場合」と「引数が1個で空文字の場合」をGPCI2プラグイン側では区別不可能という点です。 なので、区別が必要な場合は、これも現状、VCSSLの関数でラップして、前者用と後者用の実装を呼び分けるしかないです。

GPCI2 の直系の後継インターフェースになる GPCI3 では、互換を保ちつつ、何らかのモード切り替えによって対処可能にしたいと思っているのですが、まだ仕様が未確定で、未サポートです。

配列引数は一個しか渡せない

GPCI2 の関数は、引数を配列でまとめて渡す事ができます:


string args[] = {"123", "4.56", "true"};
string ret[] = exampleFunction(args);
QuirksArrayArg.vcssl

が、この場合、渡せる引数は1個のみです。どうしても複数渡したい場合は、シリアライズみたいな事をやって1本の配列にまとめて、それぞれの要素数情報も埋め込んで渡すしかないです。後発のインターフェースだと普通にできるんですが、GPCI2ではそうするしかないです。

オーバーヘッドが大きい

ここまで見てきた通り、GPCI2 は型付けがフリーダムで緩く、内部で文字列配列への型変換をやっているので、その時点で呼び出し処理のオーバーヘッド(≒避けられない重さ)が大きいです。

さらに、呼び出し時に毎回関数名との一致判定処理が走ります:


// VCSSLから呼ばれた際に実行する処理を実装する
@Override
public String[] process(String functionName, String[] args) {
    if (functionName.equals("exampleFunction")) {
        ...
QuirksOverhead.java

これもだいぶ痛いオーバーヘッドです。

なので、数値計算とかで高頻度で呼び出す関数とかをGPCI2で実装するのは、明らかに好ましくないです。といっても、高速寄りの公開インターフェースは、現行世代のVCSSL実行環境ではまだ未対応(Vnanoでは使える)なので、これについては現状は回避策が無いです(すみません...)。需要次第ですが、なるべく早めに対応を進めたいと思っています。

最後に

さて、この即席ガイドの内容は、今回で完結です!

全5回で、VCSSLの全体像をあちこち味見しながら駆け足で解説してきたガイドでしたが、どうだったでしょうか。 とりあえず、ありがちな用途に対して、「どういう雰囲気の機能があって、どんな感じで進めればいいか」みたいな大まかな方向性だけでも伝わっていると、このガイドの目的は十分果たせたので嬉しいです。

VCSSLの公式サイトでは、テーマごとのより詳しいガイドも、無料で利用できます:

文法ガイド
VCSSLの各文法を坦々とまとめた、リファレンスのようなガイドです。
GUI開発ガイド
GUI機能を用いた開発のガイドです。
2DCG開発ガイド
2次元描画機能を用いた開発のガイドです。
3DCG開発ガイド
3次元描画機能を用いた開発のガイドです。
2Dグラフのプロット方法
2次元のグラフをプロットする方法をまとめたページです(リニアングラフ2D 公式ガイド内)
3Dグラフのプロット方法
3次元のグラフをプロットする方法をまとめたページです(リニアングラフ3D 公式ガイド内)
標準ライブラリ仕様書
全標準ライブラリ関数/変数の詳細仕様が掲載されています。
VCSSLコードアーカイブ
実際にVCSSLによる様々なプログラムのコードを、解説記事付きで多数配信しています。

より深く掘り下げて知りたい方は、ぜひ上記もご活用ください!