GNU make には、たくさんの便利な関数をはじめ、多くの高度な機能が備わっています。しかし、完全なプログラミング言語を内蔵しているわけではないので、おのずと限界もあります。こうした限界は、shell 関数を使って別のプログラムを呼び出すことで乗り越えられる場合もありますが、その方法は効率の悪いことが少なくありません。
GNU make に組み込まれた機能だけでは要求を満たせない場合、make を拡張する方法が二つあります。一つは、その機能が提供されているシステムであれば、組み込みのスクリプト言語として GNU Guile を利用する方法です(GNU Guileとの連携を参照してください)。もう一つは、動的にロードできるオブジェクトをサポートしているシステムであれば、好きな言語で(そのようなオブジェクトにコンパイルできる言語であれば何でも)独自の拡張を書き、それをロードして拡張機能を提供する方法です(動的オブジェクトのロードを参照してください)。
GNU make は、組み込み拡張言語として GNU Guile をサポートするようにビルドできます。Guile は Scheme 言語を実装したものです。GNU Guile や Scheme 言語、そしてそれらの機能の解説は、このマニュアルの範囲を超えています。GNU Guile および Scheme のドキュメントを参照してください。
お使いの make が Guile をサポートしているかどうかは、.FEATURES 変数を調べれば分かります。Guile サポートが利用可能なら、この変数に guile という語が含まれています。
Guile との連携によって、新しい make 関数が一つ加わります。それが guile 関数です。guile 関数は引数を一つ取ります。その引数はまず make によって通常どおりに展開され、それから GNU Guile の評価器(エバリュエータ)に渡されます。評価器の結果は文字列に変換され、makefile 中で guile 関数を展開した結果として使われます。
さらに GNU make は、Guile スクリプトの中で使えるよう、いくつかの Guile プロシージャを公開しています。
make には「データ型」が一つしかありません。すなわち文字列です。一方 GNU Guile には、さまざまな種類の豊富なデータ型があります。make と GNU Guile のインタフェースで重要になるのが、Guile のデータ型を make の文字列へ変換することです。
この変換は、二つの場面で関わってきます。一つは、makefile が guile 関数を呼び出して Guile の式を評価するときです。その評価結果は、make がさらに評価できるように、make の文字列へ変換されなければなりません。もう一つは、Guile スクリプトが make の公開しているプロシージャのいずれかを呼び出すときです。そのプロシージャに渡された引数は、文字列へ変換されなければなりません。
Guile の型から make の文字列への変換は、以下のとおりです:
#f偽(False)は空文字列に変換されます。make の条件分岐では、空文字列は偽とみなされます。
#t真(True)は文字列「#t」に変換されます。make の条件分岐では、空でない文字列はすべて真とみなされます。
symbolnumberシンボルや数値は、そのシンボルや数値の文字列表現へ変換されます。
character印字可能な文字は、そのままの文字に変換されます。
string印字可能な文字だけからなる文字列は、そのままの文字列に変換されます。
listリストは、上記の規則に従って再帰的に変換されます。つまり、構造を持つリストは平坦化(フラット化)されることになります(言い換えれば、「'(a b (c d) e)」という結果は make の文字列「a b c d e」に変換されます)。
otherその他の Guile の型は、すべてエラーになります。将来のバージョンの make では、ほかの Guile の型も変換されるようになるかもしれません。
「#f」を空文字列に、「#t」を空でない文字列「#t」に変換するようにしてあるのは、Guile の真偽値の結果を、そのまま make の真偽条件として使えるようにするためです。例えば、次のように書けます:
$(if $(guile (access? "myfile" R_OK)),$(info myfile exists))
こうした変換規則があるため、Guile スクリプトがどんな結果を返すかを意識しておく必要があります。その結果は文字列に変換され、make によって解析されるからです。スクリプトに自然な結果がない場合(つまり、スクリプトがもっぱら副作用のためだけに存在する場合)は、makefile での構文エラーを避けるために、最後の式として「#f」を加えておくとよいでしょう。
makefile で使える guile 関数に加えて、make は Guile スクリプトの中で使えるプロシージャもいくつか公開しています。起動時に make は新しい Guile モジュール gnu make を作成し、そのモジュールから公開インタフェースとして以下のプロシージャをエクスポートします:
gmk-expandこのプロシージャは引数を一つ取り、それを文字列に変換します。その文字列は、通常の make の展開規則に従って make によって展開されます。展開の結果は Guile の文字列に変換され、このプロシージャの結果として返されます。
gmk-evalこのプロシージャは引数を一つ取り、それを文字列に変換します。その文字列は、あたかも makefile であるかのように make によって評価されます。これは eval 関数(eval関数を参照してください)で利用できる機能と同じものです。gmk-eval プロシージャの結果は、つねに空文字列です。
gmk-eval は、gmk-expand を eval 関数とともに使うのとはまったく同じではない、という点に注意してください。後者の場合、評価される文字列は二度展開されます。まず gmk-expand によって、続いて eval 関数によって、です。
ここでは、GNU Guile を使ってファイルへの書き込みを管理する、ごく簡単な例を示します。これらの Guile プロシージャは、単にファイルを開き、(1 行につき 1 つの文字列を)ファイルに書き込めるようにし、そしてファイルを閉じる、というものです。Guile のポートのような複雑な値は make の変数に保存できないので、ここではポートを Guile インタプリタ内のグローバル変数として保持することにします。
Guile の関数は、define/endef を使って Guile スクリプトを作り、それを guile 関数で取り込む(内部化する)ことで、簡単に作成できます:
define GUILEIO ;; GNU Make のための簡単な Guile 入出力ライブラリ (define MKPORT #f) (define (mkopen name mode) (set! MKPORT (open-file name mode)) #f) (define (mkwrite s) (display s MKPORT) (newline MKPORT) #f) (define (mkclose) (close-port MKPORT) #f) #f endef # Guile 入出力関数を内部化する $(guile $(GUILEIO))
Guile のサポートコードがかなりの量になる場合は、それを別のファイル(例えば guileio.scm)に保存しておき、makefile では guile 関数を使ってそれをロードする、という方法も検討するとよいでしょう:
$(guile (load "guileio.scm"))
この方法の利点は、guileio.scm を編集するとき、エディタがこのファイルを makefile の構文ではなく Scheme の構文として認識してくれることです。
さて、これらの Guile 関数を使ってファイルを作成できるようになりました。たとえば、コマンドラインに収まりきらないほど非常に大きなリストを扱う必要があるけれど、使おうとしているユーティリティがそのリストを入力としても受け付けてくれる、という状況を考えてみましょう:
prog: $(PREREQS)
@$(guile (mkopen "tmp.out" "w")) \
$(foreach X,$^,$(guile (mkwrite "$(X)"))) \
$(guile (mkclose))
$(LINK) < tmp.out
もちろん、これよりも本格的なファイル操作プロシージャの一式を作ることもできます。たとえば、それぞれの出力ファイルにシンボルを一つずつ割り当て、それをハッシュテーブルのキーとして使い(値にはポートを入れておきます)、そのシンボルを返して make の変数に保存する、というやり方をすれば、複数の出力ファイルを同時に扱うこともできるでしょう。
|
多くのオペレーティングシステムには、コンパイル済みのオブジェクトを動的にロードする仕組みが用意されています。お使いのシステムがこの仕組みを提供しているなら、GNU make はそれを利用して、実行時に動的オブジェクトをロードし、makefile から呼び出せる新しい機能を提供できます。
動的オブジェクトをロードするには、load ディレクティブを使います。オブジェクトがロードされると、「セットアップ」関数が呼び出され、そのオブジェクトが自分自身を初期化し、新しい機能を GNU make に登録できるようになります。たとえば、動的オブジェクトが新しい make 関数を含んでいるとすれば、「セットアップ」関数がそれらを GNU make の関数処理システムに登録する、という具合です。
オブジェクトを GNU make にロードするには、makefile に load ディレクティブを書きます。load ディレクティブの構文は、以下のとおりです:
load object-file …
あるいは:
load object-file(symbol-name) …
ファイル object-file は、GNU make によって動的にロードされます。object-file にディレクトリパスが含まれていない場合は、まずカレントディレクトリの中が探されます。そこで見つからなかった場合、あるいはディレクトリパスが含まれている場合は、システム固有のパスが探索されます。何らかの理由でロードに失敗すると、make はメッセージを表示して終了します。
ロードに成功すると、make は初期化関数を呼び出します。
symbol-name が指定されている場合は、それが初期化関数の名前として使われます。
symbol-name が指定されていない場合、初期化関数の名前は次のように作られます。まず object-file のベースとなるファイル名を取り、シンボル名として有効でない最初の文字までの部分を使います(英数字とアンダースコアがシンボル名として有効な文字です)。この接頭辞に、接尾辞 _gmk_setup を付け加えたものが、関数名になります。
一つの load ディレクティブで複数のオブジェクトファイルをロードすることもでき、同じディレクティブの中で load 引数の両方の書式を併用することもできます。
初期化関数には、その load 操作が呼び出された箇所のファイル名と行番号が渡されます。初期化関数は int 型の値を返さなければならず、失敗時には 0 を、成功時には 0 以外の値を返さなければなりません。返り値が -1 の場合、GNU Make はそのオブジェクトファイルを再ビルドしようとしません(ロードしたオブジェクトの再生成のされ方を参照してください)。
例えば:
load ../mk_funcs.so
これは動的オブジェクト ../mk_funcs.so をロードします。オブジェクトがロードされたあと、make は(その共有オブジェクトで定義されていると想定される)関数 mk_funcs_gmk_setup を呼び出します。
一方、次の場合:
load ../mk_funcs.so(init_mk_func)
これは動的オブジェクト ../mk_funcs.so をロードします。オブジェクトがロードされたあと、make は関数 init_mk_func を呼び出します。
あるオブジェクトファイルが load ディレクティブに何度現れたとしても、それがロードされるのは(そしてそのセットアップ関数が呼び出されるのは)一度だけです。
オブジェクトのロードに成功すると、そのファイル名が .LOADED 変数に追加されます。
動的オブジェクトのロードに失敗してもエラーとして報告されないようにしたい場合は、load の代わりに -load ディレクティブを使えます。オブジェクトのロードに失敗しても、GNU make は処理を中断せず、メッセージも生成しません。ロードに失敗したオブジェクトは .LOADED 変数に追加されないので、この変数を調べることで、ロードが成功したかどうかを判断できます。
ロードされたオブジェクトは、makefile と同じ再生成の手順を経ます(Makefileの再生成のされ方を参照してください)。ロードされたオブジェクトのいずれかが作り直されると、make は最初からやり直し、すべての makefile を読み直し、オブジェクトファイルを再びロードします。これをサポートするために、ロードされるオブジェクトの側で特別なことをする必要はありません。
ロードするオブジェクトを再ビルドするのに必要なルールを用意するのは、makefile の作者の責任です。
|
役に立つものであるためには、ロードオブジェクトは GNU make とやり取りできなければなりません。このやり取りには、ロードオブジェクトが makefile に提供するインタフェースと、make がロードオブジェクトに提供するインタフェース(make の動作を操作するためのもの)の両方が含まれます。
ロードオブジェクトと make のあいだのインタフェースは、C 言語のヘッダファイル gnumake.h によって定義されています。C 言語で書かれるロードオブジェクトはすべて、このヘッダファイルをインクルードすべきです。C 言語で書かれていないロードオブジェクトは、このヘッダファイルで定義されているインタフェースを実装する必要があります。
典型的には、ロードオブジェクトはセットアップ関数の中から gmk_add_function ルーチンを使って、一つ以上の新しい GNU make 関数を登録します。これらの make 関数の実装は、その仕事を行うために gmk_expand や gmk_eval といったルーチンを利用でき、必要に応じて、関数展開の結果として文字列を返すこともできます。
すべての動的拡張は、グローバルシンボル plugin_is_GPL_compatible を定義して、その拡張が GPL と互換性のあるライセンスのもとで提供されていることを表明しなければなりません。このシンボルが存在しないと、あなたの拡張をロードしようとした時点で make は致命的エラーを出して終了します。
このシンボルの宣言される型は int であるべきです。とはいえ、特定のセクションに配置する必要はありません。コードは、そのシンボルがグローバルスコープに存在することを表明しているだけだからです。次のように書けば十分です:
int plugin_is_GPL_compatible;
gmk_flocこの構造体は、ファイル名と位置(ロケーション)の組を表します。各種の項目を定義する際に提供され、これによって GNU make は、必要に応じてその定義がどこで行われたかを後からユーザーに知らせることができます。
現在のところ、makefile からロードオブジェクトの提供する操作を呼び出す方法は一つだけです。それは make の関数呼び出しインタフェースを通じてです。ロードオブジェクトは一つ以上の新しい関数を登録でき、それらは他のどんな関数とも同じように、makefile の中から呼び出せるようになります。
新しい make 関数を作るには gmk_add_function を使います。その引数は、以下のとおりです:
name関数名です。makefile はこの名前を使って関数を呼び出します。名前は 1 文字以上 255 文字以下でなければならず、含められる文字は英数字、ピリオド(「.」)、ダッシュ(「-」)、アンダースコア(「_」)だけです。また、ピリオドで始めることはできません。
func_ptrmakefile 中でこの関数を展開するときに make が呼び出す関数へのポインタです。この関数は、ロードオブジェクトが定義しなければなりません。
min_argsこの関数が受け付ける引数の最小個数です。0 以上 255 以下でなければなりません。GNU make はこれをチェックし、引数が少なすぎる状態で関数が呼び出された場合は、func_ptr を呼び出す前に失敗させます。
max_argsこの関数が受け付ける引数の最大個数です。0 以上 255 以下でなければなりません。GNU make はこれをチェックし、引数が多すぎる状態で関数が呼び出された場合は、func_ptr を呼び出す前に失敗させます。値が 0 の場合は、引数の個数に制限はありません。値が 0 より大きい場合は、min_args 以上でなければなりません。
flagsこの関数がどのように動作するかを指定するフラグです。望むフラグを OR で結合して指定します。GMK_FUNC_NOEXPAND フラグが指定された場合、関数が呼び出される前に引数は展開されません。指定されない場合は、まず引数が展開されます。
make に登録される関数は、gmk_func_ptr 型に合致していなければなりません。この関数は三つのパラメータを伴って呼び出されます。すなわち、name(関数の名前)、argc(関数の引数の個数)、argv(関数の引数へのポインタの配列)です。最後のポインタ(つまり argv[argc])はヌル(0)になります。
関数の返り値は、その関数を展開した結果です。関数が何も展開しない場合、返り値はヌルでも構いません。それ以外の場合は、gmk_alloc で作成された文字列へのポインタでなければなりません。関数がいったん戻ると、その文字列は make が所有することになり、適切なタイミングで make が解放します。ロードオブジェクトからその文字列にアクセスすることはできません。
GNU make は、ロードオブジェクトが使うための機能をいくつか公開しています。これらは典型的には、セットアップ関数や gmk_add_function で登録された関数の中から、make が扱うデータを取得したり変更したりするために実行されます。
gmk_expandこの関数は文字列を受け取り、make の展開規則を使ってそれを展開します。展開の結果は、ヌル終端された文字列バッファとして返されます。処理が済んだら、返されたバッファへのポインタを引数として gmk_free を呼び出すのは、呼び出し側の責任です。
gmk_evalこの関数はバッファを受け取り、それを makefile 構文の一区切りとして評価します。この関数は、新しい変数や新しいルールなどを定義するために使えます。これは make の eval 関数を使うのと同等です。
gmk_eval と、eval 関数を使って文字列に対して gmk_expand を呼び出すこととのあいだには、違いがある点に注意してください。後者の場合、文字列は二度展開されます。一度は gmk_expand によって、もう一度は eval 関数によって、です。gmk_eval を使えば、バッファは(make のパーサーが読み取るときに)せいぜい一度しか展開されません。
システムによっては、異なるメモリ管理方式を採用していることがあります。そのため、自分で直接確保したメモリを make の関数に渡したり、make の関数から返されたメモリを自分で直接解放しようとしたりしては、絶対にいけません。代わりに、gmk_alloc 関数と gmk_free 関数を使ってください。
とりわけ、gmk_add_function で登録した関数が make に返す文字列は、必ず gmk_alloc を使って確保しなければなりません。また、make の gmk_expand 関数から返された文字列は、(不要になったら)必ず gmk_free を使って解放しなければなりません。
gmk_alloc新しく確保されたバッファへのポインタを返します。この関数は、つねに有効なポインタを返します。メモリが足りない場合は、make が終了します。gmk_alloc は、確保したメモリを初期化しません。
gmk_freemake から返されたバッファを解放します。gmk_free 関数がいったん戻ると、その文字列はもう有効ではなくなります。NULL を gmk_free に渡した場合は、何の操作も行われません。
一時ファイルを作成してその名前を返す、新しい GNU make 関数を書きたいとしましょう。この関数には、接頭辞(プレフィックス)を引数として取らせたいとします。まず、ファイル mk_temp.c にその関数を書くことができます:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <gnumake.h>
int plugin_is_GPL_compatible;
char *
gen_tmpfile(const char *nm, int argc, char **argv)
{
int fd;
/* ファイル名の長さを計算し、その分の領域を確保する。 */
int len = strlen (argv[0]) + 6 + 1;
char *buf = gmk_alloc (len);
strcpy (buf, argv[0]);
strcat (buf, "XXXXXX");
fd = mkstemp(buf);
if (fd >= 0)
{
/* ファイルディスクリプタを漏らさないようにする。 */
close (fd);
return buf;
}
/* 失敗した場合。 */
fprintf (stderr, "mkstemp(%s) failed: %s\n", buf, strerror (errno));
gmk_free (buf);
return NULL;
}
int
mk_temp_gmk_setup (const gmk_floc *floc)
{
printf ("mk_temp plugin loaded from %s:%lu\n", floc->filenm, floc->lineno);
/* make 名 "mk-temp" でこの関数を登録する。 */
gmk_add_function ("mk-temp", gen_tmpfile, 1, 1, 1);
return 1;
}
次に、この共有オブジェクトをビルドし、ロードし、利用できる Makefile を書きます:
all:
@echo Temporary file: $(mk-temp tmpfile.)
load mk_temp.so
mk_temp.so: mk_temp.c
$(CC) -shared -fPIC -o $@ $<
MS-Windows では、共有オブジェクトの生成のされ方に特有の事情があるため、make のビルド時に生成されるインポートライブラリ(典型的には libgnumake-version.dll.a という名前で、version はロードオブジェクト API のバージョンです)をコンパイラがスキャンする必要があります。そのため、Windows で共有オブジェクトを生成するレシピは、次のようになります(ここでは API のバージョンを 1 と仮定しています):
mk_temp.dll: mk_temp.c
$(CC) -shared -o $@ $< -lgnumake-1
さて、make を実行すると、次のような出力が表示されるでしょう:
$ make mk_temp plugin loaded from Makefile:4 cc -shared -fPIC -o mk_temp.so mk_temp.c Temporary filename: tmpfile.A7JEwd