このページではJavaScriptを使用しています。

8.C言語インターフェース

8-1.概要

AZ-Prologは、ユーザが記述したC言語のプログラムをPrologとリンクする機能を提供しています。
それは言換えれば、AZ-Prologの組込述語をC言語で記述して追加することを可能にする機能です。

AZ-PrologコンパイラはPrologで記述されたプログラムをC言語ソースに変換し、そのC言語ソースを各OSのCコンパイラ・リンカを起動して実行モジュールを生成します。
したがって、AZ-PrologコンパイラがPrologを変換して出力したC言語ソースであろうと、ユーザが記述したC言語ソースであろうと、枠組が同じならば同じ扱いができます。

C言語プログラムのコンパイル・リンクも、AZ-Prologコンパイラの引数にファイル名を指定して行います。
<例>

 C:¥>azpc -p cprogram.c /i  /e  myprolog

コマンドラインには複数のCソース、Prologソースを記述できます。

C:¥>azpc -p cpro1.c  ppro.pl  cpro2.c  /i

この例では、C言語ソースファイルcpro1.c、cpro2.cとPrologソースファイルppro.plを一緒にコンパイル・リンクして、拡張インタプリタ(実行プログラム名は省略時のデフォルトprolog)を生成しています。

8-2.C言語ソースプログラムの記述方法

この節では、PrologとリンクするためのC言語ソースの書き方を説明します。
そのC言語ソースは基本的にAZ-Prologコンパイラから中間で出力されるC言語ソースと同じ枠組みを持たせる必要があります。

リンク可能なC言語ソースプログラムは次の枠組みを持ちます。


★ヘッダファイルのインクルード
★モジュールファイル初期化関数の定義
★各関数の定義


以下、順に説明します。

8-2-1.ヘッダーファイルのインクルード

第一の枠組として「azprolog.h」というヘッダファイルのインクルードがあります。
ユーザが定義するヘッダファイル以外に、次の1行を必ず追加してください。

#include <azprolog.h>

このヘッダファイルには、Prologとリンクするための情報が書かれています。

8-2-2.モジュールファイル初期化関数の定義

まず、次のC言語ソースプログラムの例を見てください。
ファイル名“cpro.c”

#include <azprolog.h>
pred P2_cprogram(Env) 組込述語 cprogram/2 となる関数の定義
Frame *Env;
{ 
  : 
  処理コード(述語の処理を関数内に記述) 
  : 
}

: 同様に他の述語を関数で定義

int initiate_cpro(Frame *Env)    /* 初期化関数 */
{
  put_bltn("cprogram",2,P2_cprogram); 
  : 定義された各述語について、put_bltn(..)を同様に記述。
  :
 その他の初期化コード
  :
  return 1;
}

この中でinitiate_cpro(Frame *Env) という関数が、モジュールファイル初期化関数と呼ばれるものです。これはリンクするモジュール(Cソースファイル)に含まれる組込述語を、システムに登録するための関数です。
モジュールファイル初期化関数の関数名は「initiate_ファイル名」としてください。つまり、個々のCソースファイル毎に初期化関数は必要になる訳です。この関数内には、そのファイルで定義されたCリンク組込述語の登録情報の記述及び、必要な初期化コードを入れておきます。

個々のCリンク組込述語登録の記述:
put_bltn(“組込述語名”,アリティ,関数名);

組込述語名と関数名は、必ず次の対応をとるようにします。 {}部分は実際の数字/名前に置き換えます。

関数名:
P{アリティ}_{組込述語名}

冒頭の例ではCリンク組込述語「 cprogram/2 」を追加するので

P2_cprogram()

となっています。


この枠組は、他のモジュール、特にコンパイルコードで、該当する関数を決定するためのものですから、必ず守るようにしてください。また、この関数が実行される前に該当の関数を定義しておきます。

azpcで実行プログラムを生成すると、リンク用のファイル「userfile.c」が作られます。
このファイルの内容は次の様なものです。
ファイル名 “userfile.c”

void user_file()
{
   initiate_{ファイル名1}(NULLFRAME);
   initiate_{ファイル名2}(NULLFRAME);
   :
}

関数「user_file()」の本体には、リンクした全てのユーザ定義ファイルの初期化関数の呼び出しが列挙されています。プログラムを実行すると、ワークエリアの初期化等の後でこの関数が呼ばれます。この時、各ファイルの初期化関数が順に呼び出されて、実行プログラムに述語の組込登録をする仕組になっています。

8-2-3.各関数の定義

組込述語を定義する関数の名前は前述の関数名のルールに従います。
関数は「Frame *」型の引数を一つだけとり、「pred」型の値を返します。
「Frame」「pred」 はいずれも「azprolog.h」で定義されたtypedef名です。
以下では次の二つの場合に分けて関数の記述方法を説明します。


(A)単純な決定性組込述語をC言語で直接記述する場合
(B)コンパイラが出力したCプログラムをテンプレートとして利用する場合


なお、以下で使用されるシステム定義の関数の詳細は、次節の関数リファレンスを参照してください。


(A)単純な決定性組込述語をC言語で直接記述する

実際にC言語インターフェースで記述する述語のほとんどはこのタイプに属するものとなります。
簡単なプログラム(内容自体はあまり意味が無い)を例に説明します。

次のような計算を実行する述語を作ります。

num_crunch(X,Y):-Y is 1/(1+exp(-X)).

同じことをAZ-PrologのC言語インターフェースを使って記述すると、次のようになります。
システム関数(GetDouble等)でエラーが発生する可能性がありますが、事前の引数の型チェックやエラー処理はここでは簡単のため省略しています。

pred P2_num_crunch(Env)          ・・・・・・・・・・・・・・・・①
Frame *Env;
{
   double    x,y;
   x=GetDouble(next_var_cell-2);       ・・・・・・・・・・②
   y=1.0/(1.0+exp(-x));
   if(UnifyDouble(Env,next_var_cell-1,y))  ・・・・・・③
       YIELD(DET_SUCC);             ・・・・・・・・・・・・・④
   else
       YIELD(FAIL);
}

①~④が Prolog とのインターフェース部分です。以下の表で説明します。

関数の型、名前、引数はこのようになります。
ENVにはProlog特有の実行制御のための情報が入っていますが、ここで説明しているようなタイプの述語(決定性述語)では直接には使用しません。(一部システム関数・マクロの引数として使用する場合があります)
渡された引数の値を取得します。
Prolog側での呼出しがpred(arg1,arg2,・・・・,argn). となっている時、argiの内容はnext_var_cell - n + i - 1 の位置に置かれています。
next_var_cell - n   → arg1
next_var_cell - n + 1 → arg2


next_var_cell - 1 → argn
next_var_cell (スタックポインタ)は「TERM *」型の外部変数です。
ただし、上の対応は関数呼出し直後のもので、システム関数・マクロを使用するとnext_var_cell が変化することがあるので、固定ではありません(関数リファレンス参照)。また引数領域から現在の next_var_cellの値の直前までは、その述語用の作業領域として使用してよいようになっています。
システム関数・マクロを介さず直接 next_var_cellの値を増加させることは禁止されています。 next_var_cellの値を変化させるシステム関数・マクロを多用する場合は初期の値をローカル変数にセーブしておくと引数へのアクセスに便利でしょう。
計算結果(y)を引数(Y)とユニファイします。
値をProlog側に返す場合はユニファイを行います。ユニファイは失敗する可能性があることに注意してください。ユニファイを行うシステム関数(上記の例ではUnifyDouble())。8-2-4.関数リファレンス d)引数へのユニファイ参照)は成功すると1、失敗すると0を返します。

成功または失敗でPrologに戻ります。

YIELDはreturn文に展開されるマクロです。YIELD(DET_SUCC)は決定性の成功(バックトラックしてもここには他の選択肢が無い)、YIELD(FAIL)は失敗を意味します。いずれの場合も使用した作業領域はProlog側で解放するようになっています。


注)
DET_SUCC、FAILは、それぞれ1、0を pred型にCASTしたものです。

 YIELD(UnifyDouble(Env, next_var_cell - 1, y));
とすることができます。

(B)テンプレート出力の利用

C言語による操作を行いたい部分にスタブ宣言をした述語を含めてProlog述語を作ります。


:-extern c:stub/3.
f00(X):- ・・・・・・, stub(X,A,B), ・・・.

このファイルをAZ-PrologコンパイラでコンパイルしてC言語ファイルを出力させます(/nccオプションを指定)。出力プログラムはおおよそ次のようになります。

extern pred P1_foo();
extern int stub();


pred P1_foo(Env)
register Frame *Env;
{
Frame Now;
Now.Link = Env;
LINK(Env, -, -);


if( !stub(&Now,Env->Local-1,Env->Global,Env->Global+1) ) YIELD(FAIL);


}

関数stubはint型のpublicな関数として宣言され、これを呼び出す1行(if文)が関数P1_fooの中に追加されていますが、関数の雛型は生成されません。Prologソースの中でのstub述語(テンプレートを出力するためのあくまで仮の述語)の呼び出し形式(上記の例では、変数Xの値を入力として受取り、変数AとBが出力として返す)が、生成されたstub関数呼び出し形式に反映されます。
ここで関数stubの内容を作成し(あるいは関数 stub の呼出しを直接置き換えて)再びコンパイル(今回はリンクまで。リンクバッチmkazを使用)します。この例では、スタブ関数の呼出しは第1引数に環境フレーム、第2引数以降にもとのProlog呼出しでの引数の並びに対応するTERM *型のデータが渡されています。
引数に「Frame* Env」を取るシステム関数(8-2-4.関数リファレンス参照)には、この第1引数の値を渡します。スタブ関数の戻り値は整数で1が返された時は成功、0の時は失敗として扱われます。

8-2-4.関数リファレンス
a)登録関数
以下の2関数は初期化関数(initiate_XXX)からのみ使用することができます。

void put_bltn(char *predicate_name, int arity, pred (*function)())
名前 predicate_name,アリティ arity の組込述語を登録し、処理関数を「function」とします。

BASEINT PutSystemAtom(Frame *Env, char *name)
名前 name をもつアトムをシステムアトム(GCによって回収されない)として登録し、アトム番号を返します。アトム番号とは全てのアトムに対して割り当てられ、個々のアトムを一意に特定する整数値です。
b)型チェック
int IsAtom(Term *t)
tがアトムの時1を、それ以外の時0を返します。

int IsInt(Term *t)
tが整数の時1を、それ以外の時0を返します。

int IsDouble(Term *t)
tが浮動小数点数の時1を、それ以外の時0を返します。

int IsCompTerm(Term *t)
tが複合項の時1を、それ以外の時0を返します。

int IsNil(Term *t)
tがアトム[ ](空リスト)の時は1を、それ以外の時は0を返します。

int IsCons(Term *t)
tが[A|B]の形の複合項の時1を、それ以外の時0を返します。

int IsUndef(Term *t)
tが未定義変数の時1を、それ以外の時0を返します。
c)引数の取得
以下の関数はいずれもエラーを発生することがあります。プログラムの実行を中断させないためには、予めb)の型チェック関数で引数のチェック等をした上で使用するようにしてください。

BASEINT GetAtom(Term *t)

tがアトムの時アトム番号を返します。それ以外の時エラー(Illegal argument)となります。

BASEINT GetInt(Term *t)
tが整数の時その値を返します。それ以外の時エラー(Illegal argument)となります。結果は int 型または long 型に代入できます。

double GetDouble(Term *t)
tが浮動小数点数の時その値を返します。
それ以外の時エラー(Illegal argument)となります。

int GetFunctor(Term *t, BASEINT *f)
複合項tのファンクタを取得する関数です。
tが複合項の時、引数fの指す領域にtのファンクタのアトム番号を入れます。tが複合項でない時はエラー(Illegal argument)となります。

void GetArg(Term *t, int n)
複合項tのn番目の引数を取得する関数です。
tが複合項の時、next_var_cell の値を+1して新たに領域を確保した上で、next_var_cell -1の位置にtのn番目の引数を入れます。tが複合項でない時、n がアリティより大きい時はエラー(Illegal argument)となります。

void GetCons(Term *t)
tが [A|B] の形の項の時、next_var_cell の値を+2して新たに領域を確保した上で、next_var_cell -2の位置にAを,next_var_cell -1 の位置に B を入れます。tが[A|B]の形の項以外の時はエラー(Illegal argument)となります。
d)引数へのユニファイ
以下の関数はいずれも、ユニファイに成功すれば1を、失敗すれば0を返します。第1引数には述語定義の唯一の引数Envをそのまま渡します。

int UnifyE(Frame *Env, TERM *t1, TERM *t2)
t1とt2をユニファイします。

int UnifyAtomE(Frame *Env, TERM *t, BASEINT n)
tとアトム番号nをもつアトムをユニファイします。

int UnifyIntE(Frame *Env, TERM *t, BASEINT i)
tと整数iをユニファイします。BASEINT はlong型です。

int UnifyDouble(Frame *Env, TERM *t, double f)
tと浮動小数点数fをユニファイします。
エラー(Global stack Overflow)を起こす場合があります。

int UnifyCompTerm(Frame* Env, TERM* t, BASEINT func, int ari,TERM* arg1, ...)
tと複合項(func以降で与える)をユニファイします。複合項は、ファンクタがアトム番号funcのアトム、アリティがariで、引数は arg1
以降にari個並べ、何れもTerm * 型です。
エラー(Global stack Overflow)を起こす場合があります。

int UnifyCons(Frame *Env, TERM *t, TERM *A, TERM *B)
tと項 [A|B] をユニファイします。
エラー(Global stack Overflow)を起こす場合があります
e)その他
void MakeUndef(Frame *Env)
next_var_cellを+1して領域を確保し,next_var_cell –1 の内容を未代入変数とします。
複合項を合成する時の中間結果に使用できます。引数nの複合項の領域を確保するには、本関数をn+2回呼び出します。
エラー(Global stack Overflow)を起こす場合があります。

int Bn_xxx(Frame *Env, Term *t1, ・・・)
決定性組込述語xxx/nを呼び出します。t1以降にはn個のTerm *型の引数を与えます。成功したら1を、失敗したら0を返します。
但し、記号組込述語(述語名が記号)については、呼び出す関数名はxxxに記号を当てはめた形式ではなく、内部関数名です(この後に続く「付表:記号組込述語の内部関数名」を参照)。

void Atom2Asciz(BASEINT atom, char *buffer)

アトム番号atomの名前をbufferにコピーします。 bufferは必要な文字配列の領域を予め確保します。az_get_atom_length()で必要な長さを調べることもできます。

BASEINT Asciz2Atom(Frame *Env, char *name)
名前がnameであるアトム(無ければ新たに登録)のアトム番号を返します。
エラー(Heap Overflow)を起こす場合があります。
GCが起こると使われていないアトムは登録を抹消されるので、同じ名前でこの関数を呼んでも常に同じ値を返すとは限りません。この関数の戻り値の有効期間は制御が他の述語関数に移る(ような呼び出し、または成功/失敗でのリターン)か、次にこの関数がコールされるまでに限定されます。従って無効になる前に UnifyAtom等のアトム番号を引数にとる関数で使用する必要があります。
初期化関数中でPutAtomによって返されたアトム番号については、このような制限はありません。

int az_get_atom_length(BASEINT n);
引数nで指定したアトム番号のアトムの名前のバイト数を返します。

int az_charsetmode(void);
現在の文字エンコードを取得します。戻り値は以下のいずれかです。
0:sjis, 1:euc, 2:utf8
次の定数が定義されていますのでご利用ください。
#define AZ_ENC_SJIS (0)
#define AZ_ENC_EUCJP (1)
#define AZ_ENC_UTF8 (2)

int az_enc_len(int encode, const unsigned char* p);

文字列の先頭文字のバイト長を取得します。

また、次の定数も定義されていますのでご利用ください。
#define AZ_MAX_ATOM_LENGTH  アトム名の最大バイト長


付表:記号組込述語の内部関数名
述語のアリティは全て2。内部関数はFrame *Envと2つのTERM *型の引数を取ります。

記号述語 内部関数
= B_equal()
=:= B_eqnum()
=/= B_noteqnum()
< B_ltnum()
> B_gtnum()
=< B_lenum()
>= B_genum()
=.. B_univ()
== B_eqterm()
/== B_noteqterm()
¥== B_noteqterm()
@< B_ltterm()
@=< B_leterm()
@> B_gtterm()
@>= B_geterm()

8-3.他システムの一部としてAZ-Prologの組込み

8-2では、AZ-Prologの述語をC言語で記述する方法を説明しましたが、main関数をもつ C++などで書かれた他システムの一部としてAZ-Prologを利用することもできます。それにはインターフェース関数(8-3-1参照)を使用します。

8-3-2では、単純なprologのゴールをC++プログラムからAZ-Prologを利用して実行する例を示していますが、prologで書いた述語をコンパイラazpcで変換したCソースや、C言語で記述したユーザ定義述語もリンクさせることが可能です。この場合、初期化関数および初期化が必要ですので、azpc が生成するuserfile.c もコンパイルリンクします。但し、prologのモジュールには top_level/0 の定義はしないでください。 main関数に変換されるからです。

8-3-1.インターフェース関数

他言語からAZ-Prologを利用するインターフェース関数を解説します。何れの関数も、正常終了の時は1を、エラー終了の時は0を返します。

(1) AZ-Prologの初期化

AZ-Prologの利用のため、領域を確保して初期化します。領域指定する引数は-h/-l/-g/-aの各サイズです。直接指定しない場合のワークエリアサイズの決定ルールは、Prologインタプリタ等のPrologアプリケーションの場合と同じです(3-3-7参照)。

int az_init(int argc, char* argv[])

int argc: AZProlog 初期化パラメータの引数の総個数(プログラム名も含む)
char *argv[ ]:   AZProlog 初期化パラータの文字列を指すポインタの配列

(2) AZ-Prologの終了

AZ-Prologの使用を停止し、領域を開放します。
int az_end(void)


(3) Prologゴール実行 (結果取得が不要な場合)

Prologゴールを実行します。実行後のバックトラックはできません。

int az_exec(char *goal)

char *goal: Prologのゴールを文字列で与えます。

(4) Prologゴール実行,結果取得

Prologゴールを実行し、ゴールに含まれる変数の値を文字列で取得します。
結果取得後のバックトラックはできません。

int az_exec_unify(char *goal,int n, ... )

char *goal:   Prologのゴールを文字列で与えます。
int n : トップレベルのゴールに含まれる、値を取得する変数の(先頭から数えた)数を与えます。
... :   変数の値を文字列として格納するため、n個分の文字型配列を並べます。
同一の変数が複数ある場合は、その変数が最初に出現した位置に格納されます。

 

< 例 >
char retA[256],retB[256],retC[256];
az_exec_unify("my_goal(1,A,B,A,C)",3,retA,retB,retC);

8-3-2.C++からの利用例

C++でどのようにインターフェース関数を用いてAZ-Prologを利用するのか、記述方法の簡単な例と、コンパイル・リンク、そして実行までを見てみます。


C++プログラム例

C++プログラムからprologのゴール append([1,2,3],[4,5],L)をAZ-Prologを利用して実行し、結果を取得して表示する例です。

/*  callaz.cpp  */

#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>


extern "C" { extern int az_init(int argc,char **argv); }
extern "C" { extern int az_end(void); }
extern "C" { extern int az_exec_unify(char *args, int n,... ); }

int main(int argc, char **argv){
	int argcp    = 3;
	char *argvp[]= {(char *)"my_prolog",(char *)"-h",(char *)"100"};
	char goal[]  = "append([1,2,3],[4,5],L)";
	char retVar[256];

	if(az_init(argcp,argvp)){
		printf("AZ-Init OK¥n");
	} else {
		printf("AZ-Init error!¥n"); exit(1);
	}

	if(az_exec_unify((char *)goal,1,retVar)){
		printf("ret = %s¥n",retVar);
	} else {
		printf("exec Fail!¥n");
	}

	if(az_end()) {
		printf("AZ-End OK¥n");
	} else {
		printf("AZ-End error!¥n");
	}
}

コンパイル・リンク

[Windows版]

c:¥test>cl callaz.cpp /link  "<INST_DIR>¥lib¥azp.lib"

はAZ-Prologをインストールした場所です。Windows 64bit版をデフォルトでインストールした場合は、C:¥Program Files¥AZ-Prolog.9.24となります。
注意:Microsoft Visual Studio 2010では、リンク時以下のエラーが発生します。
「error LNK2019: 未解決の外部シンボル __report_rangecheckfailure が関数 _Read_GetTokenで参照されました。」
Visual Studio 2010には対応していませんので、Visual Studio 2012以降のバージョンでコンパイル・リンクして下さい。

[Linux版またはMac版]

$ g++ -Wall -o callaz callaz.cpp  -lazp -lm -ldl

実行と結果表示
$ ./callaz
AZ-Init OK
ret = [1,2,3,4,5] 
AZ-End OK