分割コンパイル&makeで楽々


 分割コンパイルとmakeは大規模なプログラムを開発する際、なくてはならない存在になってきます。 特に分割コンパイルは、1箇所を修正してコンパイルし直すのに10分も待たされる、なんてことがなくなります。 コンパイラの消費メモリも少なくて済むので、その方法を覚えておくのに損はないでしょう。

分割コンパイル

 ソースが巨大になってくると、コンパイル時間が長くなったり、メモリを多く消費したり、 プログラムの見通しが悪くなったり(バグを探すのにたいへん)と、色々困ることが増えてきます。

 しかし、C言語ではソースを関数単位で分割し、別々にコンパイルすることが出来ます。 ソースが小さければ上の問題点は解消されます。ソースの管理が大変になる場合もありますが、 makeと組み合わせて使えば簡略化出来ます。
 数々のメリットがあるこの分割コンパイルの方法について説明しましょう。

 ソースを分割する際に注意する点としては、「関数単位で分割する」ことです。 当然のことですが、関数の途中から区切って二つのファイルに分けることは出来ません。
 細かくわけすぎるのも、無駄にコンパイル回数を増やすだけになるので、適度にしましょう。 似た系統の関数を一つのファイルにするなど、ある程度まとめるのもいいでしょう。

 さて、変数や関数には有効範囲というものがあります。 その変数、関数がどこまで参照出来るかを表すものです。

 変数の場合、プログラム全体から参照出来る、ソースの中だけで有効、関数の中だけで有効、 と大まかには三段階にわかれています。例を挙げてみます。


=============== main.c ====================
       short    a;
       short    b=256;
static short    c;

main() {
short    d;
    ....
}

init() {
short    d;
    ....
}
===========================================

=============== sub1.c ====================
       short    a;
extern short    b;
static short    c;

sub1() {
short    d;
    ....
}
===========================================

 どちらのソースにもa,b,c,dの4つの変数が定義されています。それぞれの変数について見てみましょう。 変数 a は両方のソースからアクセス出来る変数です。変数 b も同様にアクセス出来ます。 ただしmain.cで初期値が設定されているので、ほかのソースから参照するためにはextern宣言をしなければなりません。
 変数 c はどちらのソースにもありますが別物です。main.cの変数 c をいくら変更してもsub1.cの変数 c は変化しません。 static宣言をした変数はそのソースでのみ有効となります。

 変数 d は計三ヶ所で定義されていますが全て独立しています。 これら、ブロックの内部で定義された変数は局所変数と呼ばれ、定義したブロック内でのみ有効です。 また、ブロックを抜けると値は捨てられます。


 複数のソースから参照可能な変数が増えてくると、 各ソースの先頭に同じような変数宣言がずらずらとならぶことになります。 こうなると変数に関するちょっとした変更を行なうにも、全てのソースを修正しなければならず不便です。

 そこで#includeを使います。これはソースのその部分に指定のファイルを埋め込む、という命令です。 この命令は関数のプロトタイプ宣言や変数宣言などの、多くのファイルから参照される情報を埋め込むのに使われます。

 #includeの使い方には2種類あり、
#include <ファイル名> 環境変数includeで示されるディレクトリから探す
#include "ファイル名" カレントディレクトリから探す
と、インクルードファイルの位置によって使い分けます。

================ action.h =================

    int      mx,my,dx,dy,bl,br;
    short    c;
    char     fname[22];

===========================================
 このようなインクルードファイル(慣習的に拡張子は.H)をカレントに置き、 各ソースファイルの先頭で、
#include "action.h"
と記述しておけば、記述したすべてのソースから同じ変数にアクセス出来ます。

 初期値付きの変数定義では少し細工が必要です。初期値付きの定義はどこか一ヶ所でのみ行なわなければなりません。 複数のソースで同一の変数を(初期値が同じであっても)初期値付きで定義することは出来ないのです。

 では、どうすればいいのかということですが、いくつか方法があります。 そのうちのひとつを紹介しましょう。


================ action.h =================

int      mx,my,dx,dy,bl,br;

#ifdef  _MAIN_

//変数定義部
int     birth=19720714;
char    fname1[22] = "HolyUp.dat";
char    fname2[22] = "BirdShield.dat";

#else

//変数宣言部
extern int    birth;
extern char   fname1[22];
extern char   fname2[22];

#endif

===========================================
 #ifdef,#else,#endifというのが出てきましたがこれは#includeと同じプリプロセッサ命令です。 この場合「_MAIN_」というシンボルが定義されていたら#ifdef〜#else間を有効にし、 されていない時は#else〜#endif間を有効にします。
 これで、シンボルが定義されていたら初期値付き変数定義、シンボルが定義されていなければ 変数宣言をひとつのインクルードファイルで行なうことが出来ます。

 例を挙げてみます。


================ main.c ===================

#define    _MAIN_

#include  "action.h"

....
....

===========================================

================ sub1.c ===================

#include  "action.h"

....
....

===========================================
 main.cでは#define文でシンボル「_MAIN_」を定義しているので、初期値付きの変数定義がなされます。 sub1.cではシンボルが定義されていないので変数宣言が行なわれます。


 ソースを分割したらコンパイルです。分割したソースはリンカでひとつにまとめます。 よって、コンパイル作業はリンカを起動する直前で終わらせるようにします。 そのためのオプションは「-c」(小文字のみ)です。
gcc -c ファイル名(その他のオプションは場合に応じて付ける)
でコンパイル、アセンブルを行い、オブジェクトファイルを作成します。 オブジェクトファイルはソースファイルの拡張子を「.o」に置き換えた名前になります。

 すべてのファイルをコンパイルし、オブジェクトファイルが用意出来たら、 リンク作業を行ないます。
hlk -x -l main.o sub1.o sub2.o clib.l gnulib.l floatfnc.l
 コマンドラインから上のように打ち込むと、「main.x」という名前の実行ファイルが出来上がります。 この時エラーが出るならオブジェクトファイル、ライブラリの指定が足りないか、 変数の定義、宣言に誤りがあることになります。

 まれに、オーバーフローというエラーを起こすことがありますが、元がC言語の場合 コンパイラのバグか、ライブラリのバグでしょう。制作者に連絡しましょう。


 リンカにはいくつか、指定しておくと便利なコマンドオプションがあります。
-x          :シンボルテーブルを削除
-o filename :出力ファイルネームの指定
-l          :環境変数libを参照する
-i filename :インダイレクトファイルの指定
 「-x」のシンボルテーブルの削除ですが、デバッガによるデバッグ作業や、 ディスアセンブルを行なわないのであれば、削除した方がファイルサイズの節約になります。 ただし、消費メモリは変化しません。

 「-o」は出力ファイルネームを指定するオプションスイッチです。 これを指定しない時コマンドラインの先頭にあるオブジェクトファイルの拡張子を 「.x」に置き換えたものが出力されます。

 「-l」ライブラリを検索する時環境変数libを参照します。

 「-i」インダイレクトファイルの指定です。インダイレクトファイルとは、 リンクファイルが多すぎるなどの理由で、コマンドライン文字数が255文字を越えるような時に使われます。 コマンドラインで指定すべきファイル名、スイッチなどを、テキストファイルに記述し、読み込ませることが出来ます。
 インダイレクトファイル名を「action.ind」とすると例にあげたリンク作業は
hlk -i action.ind
と、これだけで済みます。インダイレクトファイルの内容は

=============== action.ind =================
-x
-l

main.o
sub1.o
sub2.o

clib.l
gnulib.l
floatfnc.l

============================================
このようになります。


 分割コンパイルについての説明は以上で終わりです。変数の定義にさえ注意すれば、 何も難しいことはありません。早速挑戦してみましょう。



make

 「make」とは、コマンドラインから
make

と打ち込むだけでコンパイル作業を全て行なってくれるありがたいツールです。 バッチファイルでコンパイルを行なう場合との相違点は、 ソースを修正した時必要なファイルのみを再コンパイルしてくれるという点です。

 もっとも、それなりの設定ファイルを用意する必要がありますが。設定ファイルは、 「Makefile」という名前をつけるのが一般的です。(拡張子はなし)

 要はMakefileの書き方を覚えればすべてうまくいくというわけです。 かなり複雑なことも出来るのですが、ここでは基本的なことしか説明しません。 私自身必要最低限の知識しかもっていないので。


 では早速、説明に入りましょう。

 Makefileの基本は
作成ファイル名 : 依存ファイル名
	実行コマンド
これです。「作成ファイル名」はその名の通りです。「依存ファイル名」は、 そこに記述されたファイルが更新されたとき「実行コマンド」を実行する、依存関係を表します。 ファイル名には複数のファイルを指定することが出来ます。「実行コマンド」は、 必ずタブの後ろに書かなければなりません。

 例を挙げてみましょう。


action.x : action.o
	hlk action.o
この場合action.xとaction.oのタイムスタンプを比較し、action.oの方が新しければ 「hlk action.o」をコマンドとして実行します。action.xの方が新しければコマンドは実行されません。


action.o : action.c action.h cell.h
	gcc -c action.c
action.c、action.h、cell.hのどれかが更新されていた場合コマンドを実行します。


all : action.x

action.x : action.o
	hlk action.o sfxvilib.l sfxvirun.l

action.o : action.c action.h cell.h
	gcc -c action.c
makeは「all」の右側にある「action.x」を完成させることを目的とします。 action.xはaction.oから作られ、action.oはaction.c、action.h、cell.hから作られます。 よってaction.c、action.h、cell.hのいずれかが更新されるとaction.xが新規作成されます。


all : action.x

action.x : main.o sub1.o sub2.o
	hlk -o action.x main.o sub1.o sub2.o sfxvilib.l sfxvirun.l

main.o : main.c action.h cell.h
	gcc -c main.c

sub1.o : sub1.c
	gcc -c sub1.c

sub2.o : sub2.c
	gcc -c sub2.c
分割コンパイルをmakeで行なわせる場合はこのようになります。


all : action.x

action.x : main.o sub1.o sub2.o
	hlk -o action.x main.o sub1.o sub2.o sfxvilib.l sfxvirun.l

%.o : %.c
	gcc -c $<

main.o : main.c action.h cell.h
ひとつ前のサンプルとまったく同じ動作をします。 こちらの方が同じような部分がまとめられているのですっきりしています。

 「%.o : %.c」ですが、ファイル名が拡張子以外は同じであるという依存関係を意味しています。 「$<」はコマンド実行時に「:」の右側のファイル名に置き換えられます。

 「all」により最終目的はaction.xです。action.xはmain.o、sub1.o、sub2.oから作られます。 main.oはmain.c、action.h、cell.hに依存しているのでどれかが更新されるとコマンドが実行されます。 が、コマンドが記述されていません。 この場合「main.o : main.c」が「%.o : %.c」にマッチするので「gcc -c $<」が実行されます。 「$<」には依存ファイル群の先頭のものに置き換わるので「gcc -c main.c」がコマンドラインに渡されます。 sub1.o、sub2.oは依存ファイルすら書かれていませんが 「%.o : %.c」によりそれぞれsub1.c、sub2.cが依存ファイルとみなされます。


 ほかに覚えておくと便利なことは、継続行、マクロ設定、ファイル名置き換えです。

 分割コンパイルで依存ファイル数が多くなりすぎた時など、一行に書き切れなくなってきた場合、 継続行指定を行なえば複数行にまたがって指定することが出来ます。


action.x : main.o\
	   sub1.o\
	   sub2.o\
	   action.ind
	hlk -i action.ind
バックスラッシュ(もしくは円マーク)「\」の直後の改行は無効化されるので、 次の行も依存ファイルに含まれます。action.indの後ろにバックスラッシュがつかないことに注意。

 例の場合は


action.x : main.o sub1.o sub2.o action.ind
	hlk -i action.ind
これと同じことになります。


 マクロは活用範囲が広いのでいろいろなことに使えます。
マクロ名 = 定義文
で設定し、
$(マクロ名)
で、その位置に展開されます。


FLAGS = -O -c -I \develop\include

%.o : %.c
	gcc $(FLAGS) $<

このように書かれていた場合、$(FLAGS)の位置にFLAGSの内容が展開されるので
gcc $(FLAGS) $< → gcc -O -c -I \develop\include $<
のように置き換えが行なわれます。


 ファイル名の置き換えですが、これにはいくつか種類があります。
$@  作成ファイル名
$<  依存ファイル名
$^  依存ファイル名(連結)
本当はほかにもあるのですが、私は知りません。

 実行コマンドを記述する位置にこれを使うことで実行時に任意のファイル名に置き換えることが出来ます。

 それぞれの使い方を例を挙げて解説します。


scene1_1.pic : yakko1.pic back.pic
	apicg yakko1.pic
	apicg -pile back.pic
	apicg -s0,0,255,255 $@
コマンドの三行目に「$@」が使われています。これはscene1_1.picに置き換わります。

action.o : action.c
	gcc -c $<
コマンド実行時に「$<」はaction.cに置き換わります。

fire.x : fire.o bomber.o
	hlk $^
コマンドはhlk fire.o bomber.oに置き換わります。


 以上のことを踏まえてSFXVIキャラ制作用のMakefileの例を挙げます。 これが理解出来れば、Makefileの書き方は覚えられたようなものです。


=============== Makefile ===================
all : action.x

LIBS = \develop\lib\sfxvilib.l \develop\lib\sfxvirun.l floatfnclib.l

FLAGS = -O -c -I e:\develop\include

OBJS =	act_main.o\
	act_stand.o\
	act_crouch.o\
	act_sp_attack.o\
	act_sp_shoot.o\
	act_sp_move.o\
	act_throw.o\
	act_jump.o\
	act_command.o

action.x : $(OBJS)
	hlk -l -o $@ $^ $(LIBS)

%.o : %.c
	gcc.x $(FLAGS) $<

act_main.o : act_main.c action.h cell.h

============================================

 リンカの指定がやや冗長ですが、サンプルなのでこうしてあります。 実際にはインダイレクトファイルを使った方がすっきりするでしょう。


 Makefileの書き方がわかったら、 makeの使い方です。makeはカレントにあるMakefileを読み込んでそれに従って作業を行ないます。 よってMakefileはカレントディレクトリに用意します。

 Makefileに別の名前のファイルを使う場合は「-f」オプションに続けてファイル名を指定します。

 ファイルの新旧に関わらず作業を行なわせるには、「ーW」でファイル名を指定するか すべてを更新する「-a」をオプションとして与えます。

 オプションをつけずにファイル名だけを指定すると、そのファイルを作成することを目的として、 作業を行なわせることが出来ます。

 注意することは以上です。makeは基本的にMakefileさえ用意すれば、 あとはよきにはからってくれます。どんどん活用しましょう。


ひとつ前にもどるトップページへもどるSFXVI Worldへもどる