ホーム < ゲームつくろー! < IKD備忘録

Raspberry Pi
メイクでコンパイルとリンクを自動処理させよう


 前章でg++で分割コンパイルする方法がわかったのですが、オブジェクトファイルを一つずつ作る方法ではファイル数の増加で簡単に作業破綻を来たしてしまいます。これは自動化しないといかんわけです。そういう時に必ずお目見えするのがメイクファイル。これを使ってみます。



@ メイク?

 分割コンパイルをする場合に、ほぼ必ず作るのが「メイクファイル(make file)」。makeファイルは、cppや.hの「依存関係」を記述するのが大きな目的です。依存関係について、前章のmain.cpp、electricfan.cppそしてelectricfan.hについておさらいしてみます。

 まず、ElectricFanクラスの宣言部であるelectricfan.hはこんな感じでした:

electricfan.h
class ElectricFan {

    bool bPower;

public:
    ElectricFan();

    const char* getStateString() const;

    void pushSwitch();
};

他のヘッダーファイルをインクルードしていないし、ライブラリの関数を使っているわけでもありません。つまり、このヘッダーファイルは「他者に依存していない」ファイルとなります。

 続いてelectricfan.cppはこうでした:

electricfan.cpp
#include "electricfan.h"

ElectricFan::ElectricFan() : bPower( false ) {
}

const char* getStateString() const {
    if ( bPower == true )
        return "Power On.";
    return "Power off";
}

void ElectricFan::pushSwitch() {
    bPower = !bPower;
}

electricfan.hをインクルードしています。これはこのヘッダーファイルに「依存している」状態です。他のファイルへの依存はありません。最後にmain.cppです:

main.cpp
#include <stdio.h>
#include "electricfan.h"

int main() {

    ElectricFan fan;

    fan.pushSwitch();   // Switch on

    printf( "Electric Fan State: %s", fan.getStateString() );

    return 0;
}

electricfan.hがインクルードされているので、このファイルに依存しています。またstdio.hにも依存していますね。関数内部ではElectricFanを作っています。これはelectricfan.cppの実装部が無いと実行できないのでelectricfan.cppにも依存しています。

 以上から各.h、.cppの依存関係はこんな感じになっているのがわかります:

もう一つ、最終的な実行ファイルであるa.outはmain.oやerectricfan.oオブジェクトファイルが更新された場合に作り直す必要がありますね。つまり、a.outファイルはこれらファイルに依存しているわけです。図示するとこうなります:

こう見るとたったこれだけのファイルでも色々と依存関係があるものです。ファイルが増えたら…ぞっとしますね。では、これを元にmakeファイルを作ってみましょう。



A makeファイルを作る→make実行してみる

 ファイルの依存関係を記述するmakeファイルはテキストファイルです。なのでvimで編集する事ができます。makeファイルの記述ルールはとっても簡単で、次のような書き方をします:

a.out : main.o electricfan.o
    g++ main.o electricalfan.o

1行目はコロンの左側に「作成するファイル」を記述します。コロンの右側には、そのファイルを作成するのに必要なファイル(依存ファイル)をスペース区切りで列挙していきます。こうすると述語的には「もしmain.oかelectricfan.cが更新されたら、下の行のコマンドを実行しなさい」という意味になります。下の行には実行すべきコマンドを記述します。上の例ではg++コンパイラでオブジェクトをリンクするように命令しています。下の行の大切な注意、必ず「タブでインデント」しなければなりません!こうしないとコマンドとして解釈してくれません。

 この一塊をあとはひたすら書いていくだけです。例えば、@の依存関係を愚直に書いていくとこうなります:

a.out : main.o electricfan.o
    g++ main.o electricalfan.o

main.o : main.cpp electricfan.cpp electricfan.h stdio.h
    g++ -c main.cpp -o main.o

electricfan.o : electricfan.cpp electricfan.h
    g++ -c electricfan.cpp -o electricfan.o

コロンの左側が常に「作られるファイル」になっているのがわかりますね。先の図でいう黄色やオレンジの枠にある物がコロンの左に来るわけです。一方.hや.cppなどは右側に並んでいますが、それらの間の依存関係は特に記述されていないのもわかります。

 こうしてできたmakeファイルは、一般には「makefile」という拡張子無しのファイルで.hや.cppがあるフォルダに保存します。このファイル名にしておくと、カレントディレクトリをmakefileがあるディレクトリにして、

make

とするだけで、上のルールを解釈し、コロンの右側のファイルが更新されていたら下の行のコマンドを自動的に実行してくれます。素晴らしい!メイクファイルの名前を独自にした場合は、

make -f hogehoge.mk

と-fオプションの後にメイクファイル名を記述すればメイクしてくれます。まぁmakefileにするのが無難です。

 で、上のメイクファイルで実際にメイクしてみると、次のようなエラーが出てしまいました:

「main.oが必要としている"stdio.h"を作るためのルールが無いのでメイクをやめました」というエラーのようです。なるほど、これは多分「stdio.h自体が見当たらないけど、どこにあるのさ?」とうい事なのかなと思います。良く考えたらstdio.hが変更される事はまず無いので、上のメイクファイルのmain.o行にあるstdio.hを消してしまいましょう。

a.out : main.o electricfan.o
    g++ main.o electricalfan.o

main.o : main.cpp electricfan.cpp electricfan.h
    g++ -c main.cpp -o main.o

electricfan.o : electricfan.cpp electricfan.h
    g++ -c electricfan.cpp -o electricfan.o

これで再度makeすると:

「a.outはup to date(最新)です」と出ました。前章で一度コンパイルとリンクをしてそれから変更をしていないので、a.outに依存するファイルを更新していません。makeはちゃんとそれを理解して「最新だ」と判断したわけです。

 では、試しにmain.cppを適当に更新して保存し、再度makeしてみます。すると、

お〜〜、更新したmain.cppからmain.oオブジェクトファイルを作り、main.oに依存していたa.outを続けて作ってくれています!すげー(^-^)/。

 このように、メイクファイルを一度作れば、後はコードを自由に変更して「make」とするだけで自動的にコンパイルとリンクが走るわけです。Visual Studioなどの統合環境でも、裏では実はメイクファイルのような物が自動的に作られて依存関係を解決しているんです。



B メイクファイルを再検討してみる

 さて、先程のメイクファイルはかなり実直に書いたものでした。確かに依存関係をきっちり記述したのですが、実は冗長な部分があります。

 例えばmain.o行を見てみましょう:

main.o : main.cpp electricfan.cpp electricfan.h
    g++ -c main.cpp -o main.o

main.oは、main.cppを書き換えた時はもちろんを再度作り直さなければなりません。しかし、electricfan.cppを書き換えた時はどうでしょうか?よ〜く考えてみて下さい。main.cppは「electricfan.hに書かれているクラス宣言を見てElectricFanオブジェクトを作り、そのメソッドを呼んでいる」のであって、その実体(.cpp)がどうなっているかは実は感知していないんです。なので、electricfan.cppを書き換えたとしても、呼び方が一緒なのは変わらないため、main.oオブジェクトを作り直す必要は無いんです。よって、上の記述からelectricfan.cppを消す事ができます。

 次のelectricfan.hはどうか?これを消すのはちょっとまずそうです。electricfan.hに書かれている宣言を見てmain.cpp内の呼び出しを行っているので、その変更を察知しないとリンク関係がおかしくなってしまいます。

 以上から、各.oオブジェクトファイルを作る時には、

そのオブジェクトファイルの元となる.cpp
その.cppに記述されている.hファイル群

をmakefileに書き込んでいけば良い事になります。

a.out : main.o electricfan.o
    g++ main.o electricalfan.o

main.o : main.cpp
    g++ -c main.cpp -o main.o

electricfan.o : electricfan.cpp
    g++ -c electricfan.cpp -o electricfan.o

main.o : electricfan.h
electricfan.o : electricfan.h

ちょっと注目は上の太文字。main.o行が再度出現して、依存ファイルがヘッダーのみになっています。そして実行すべきコマンドが省略されています。メイクファイルでこう書くと、main.oに対する依存関係を自動的に結合してくれます。つまり、

main.o : main.cpp
    g++ -c main.cpp -o main.o

main.o : electricfan.h
main.o : main.cpp electricfan.h
    g++ -c main.cpp -o main.o

この左右は同じ意味になります。この左側の書き方、後で重要になってきます。

 少しだけ再検討したメイクファイルを見ると、main.cpp→main.c、electricfan.cpp→electricfan.o と書式がそっくりな所が出ています。今後.cppが増える度に同じような書式で追加していくだけと言えばだけなんですが、それはちょっとくたびれますね。また、ヘッダーの依存関係を記述するのはかなりヒューマンエラーが続出しそうです。

 そこでなるべく自動化すべくメイクファイルには様々な秘策が施されてきました。



C サフィックスルールで単純作業を減らす

 メイクファイルが上のような面倒なヒューマンエラーを起こしそうな作業になるのは誰でも気が付くわけで、それを回避する仕掛けがメイクファイルには色々あります。その一つが「サフィックスルール」です。

 サフィックス(Suffix)というのは「後ろにくっつく物」という意味です。メイクファイルの場合は「拡張子」がそれに相当します。拡張子間にメイク方法のルールを付けてあげましょうというのがサフィックスルールです。

 例えば、上のメイクの記述には、

****.o : ****.cpp
    g++ -c ****.cpp -o ****.o

という塊があります。これは「同じファイルベース名(****)の.cppから.oが下の命令によって作られる」という共通ルールになっています。これは以下のような「.SUFFUXES」という宣言で代用記述ができます:

.SUFFIXES: .cpp .o

.cpp.o:
    g++ -c $< -o $@

えーと、急に暗号化した感じです(^-^;。まず「.SUFFIXES: .c .o」というのは「.cと.oにサフィックスルールを適用しますよ」と宣言しています。具体的なルールが次にある2行です。「.cpp.o」というのは「.cppファイルから.oが作られます」という意味です。これ、「.o.cpp」と逆にしてはいけません(怒られます)。これは、

.cpp.o:

****.o: ****.cpp

というワイルドカードのようなものです。

 実行するコマンドの所も特徴的ですね。「$<」というのは「依存ファイルの最初の名前に置き換えますよ」というマクロ記号です。ここでの依存ファイルは「****.cpp」ですから、$<の所はcppファイル名で置き換わるわけです。続く「$@」は「ターゲットの名前に置き換えますよ」というマクロ記号です。ターゲットというのはコロンの左にあるファイル名「****.o」を指します。よって、g++〜の部分は、

g++ -c $< -o $@

g++ -c ****.cpp -o ****.o

と置き換えられる事になります。****の所はディレクトリの下にある該当するすべての.cppになりますので、このサフィックスルールを定めるだけで、.cppから.oを作るメイク部分を書かなくて良くなりました(^-^)。Bの最後に挙げたメイクファイルは次のように簡略化されます:

.SUFFIXES: .cpp .o

a.out : main.o electricfan.o
    g++ main.o electricalfan.o

.cpp.o:
    g++ -c $< -o $@


main.o : electricfan.h
electricfan.o : electricfan.h

オブジェクトファイルとヘッダーファイルの関係をどうするか…。ここにも工夫の余地があるようです。



D g++に依存ファイルを列挙してもらう

 色々調べてみると、どうやらg++には特定の.cppが依存しているファイルを列挙する機能があるようです。例えばmain.cppを次のようにg++に与えてみます:

g++ -MM main.cpp

すると、

こういう、もうまんま「メイクファイルの依存関係」な書式の出力がされます。ちゃんと自分自身(main.cpp)とインクルードしたヘッダーファイル(electricfan.h)が出ていますよね。.cppについてはサフィックスルールで依存関係を記述できたので、上の出力から.cppを取り除ければ最高です。出力を加工する…これは「パイプ」の出番です。

 パイプは上のようにコマンドの出力結果を再利用してさらに加工する機能です。例えば文字列の中から指定の文字列をピックアップするgrepをパイプに使ってみます:

先程の出力の「cpp」という所に色が付き強調されました。パイプは「|」で繋ぎます。左の結果を右が利用するという意味です。では文字列から特定の文字列を削除、もしくは置換する命令はというと…(調査中)、sedのようです。

 sedは引数に与えた構文で文字列内の特定個所を置換する機能です。例えば、上の.cppを消してしまうには、

g++ -MM main.cpp | sed -e 's/.cpp//g'

と書きます。-eオプションは「後の条件式を使います」というものです。続く「's/.cpp//g'」というのは「s/<置換前文字列>/<置換後文字列>/g」というルールになっています。最後の「g」を付けると当てはまるもの全部になりますが、gを外すと最初に見つかった物だけになります。上の例を読み解くと「見つかったすべての".cpp"を空文字に置換しなさい」と言っているわけです。実際に出力してみましょう:

出力にあったmian.cppの「.cpp」が消えました。では、「*.cpp」というワイルドカードな表現に一致する箇所を消すにはどうするか?あれこれ色々やってみて、こうすると出来るのを確認しました:

一致パターンを「 .*.cpp」としています。「スペースに続いて特定の文字列が並び(.*がそれに該当)、後ろに".cpp"が付いている文字列」という意味になっています。出力をみるとmain.cppが綺麗さっぱり消えてますね。

 ディレクトリの下にあるすべての.cppについてg++に依存ファイルを列挙してもらうには、

g++ -MM *.cpp

とワイルドカードが使えます。その出力から.cppファイルだけを削除するsed命令は上と同じなので、最終的に、

g++ -MM *.cpp | sed -e 's/ .*.cpp//g'

でメイクファイル内のオブジェクトファイルとヘッダーファイルの依存部分が出力されるはずです:

お〜出来てます(^-^)。これをメイクファイル内に埋め込めば完成です。そのために、この出力を一度テキストファイルとして保存します。これは先のコマンドの最後に「>> headerdepend」と付けます。こうするとheaderdependという拡張子無しのファイルに出力が保存されます:

g++ -MM *.cpp | sed -e 's/ .*.cpp//g' >> headerdepend

 さて、メイクする前に上のコマンドを実行し依存ファイルを保存し、メイクファイル内にそのファイルを埋め込むには、いくつか方法が考えられます。気楽なのはバッチかなぁと思います。Windowsだと.batでバッチ処理ができますが、Raspberry Piのバッチは…(調査中)、なるほど、次のようなテキストファイル(make.sh)を作成すると良いようです:

make.sh
#! /bin/sh

g++ -MM *.cpp | sed -e 's/ .*.cpp//g' >> headerdepend
make

作ったバッチ(シェルスクリプト)を実行するには、

./make.sh

とします。すると…、

おっと「バッチを実行する許可がありませんよ」と言われました。ちょっとパーミッション(許可)を見てみましょう:

lsに-lオプションを付けると各ファイルのパーミッションを見る事が出来ます。左列にある「-rw-r--r--」というのがそのフラグです。a.outは「-rwxr-xr-x」となっているのに対し、make.shは「-rw-r--r--」と3か所にある「x」がいずれもありません。このxが「実行権限」です。make.shの権限を変更するには「chmod(Change Mode)」コマンドを使います。

 chmodを次のように実行します:

chmod a+x make.sh

「a+x」が意味する所ですが、「a」は「3種類ある権限(ユーザ権限、グループ権限、その他の権限)全部」という意味です。「+」は「追加します」で「x」が追加する権限(実行権限)です。

 これで再度lsで権限を見てみると、

お〜a.outと同じ実行権限が付きました。これでシェルスクリプトを実行出来ます。

 実際make.shを実行すると、オブジェクトファイルとヘッダーファイルの依存関係を記述するheaderdependファイルが作られ、続いてmakeが実行されました。後は、make内にheaderdependの内容を埋め込めば完成です。

 メイクファイル内に特定のテキストを埋め込む方法はちゃんと用意されていて、メイクファイル内に「-include ファイル名」と記述します。サフィックスルールを適用していた先のメイクファイルを次のように書き換えます:

.SUFFIXES: .cpp .o

a.out : main.o electricfan.o
    g++ main.o electricalfan.o

.cpp.o:
    g++ -c $< -o $@

-include headerdepend

さ、では動かしてみますよ。

 electricfan.hを適当に更新して、make.shを動かしてmain.o等が再コンパイルされれば成功です:

やった〜〜(^-^)/。electricfan.hに関連するmain.cpp、electricfan.cppの再コンパイルが走りオブジェクトファイルができ、a.outが更新されました。

 これで出来たか〜っと思いましたが、最後の砦「a.outの依存関係」が残っていました。



D a.outの依存関係を自動列挙

 a.outの依存ファイルはオブジェクトファイル全部です。これをメイクファイルに逐一書いていくのは中々大変です。そこで、ディレクトリ下にある.cppファイルを列挙し、そこからオブジェクトファイルのリストを作り、メイクファイルにincludeしてみます。こんな感じの出力を得るのが目標です:

objfiles
OBJS = electricfan.o main.o

a.out : $(OBJS)
    g++ $(OBJS)

「OBJS」というのはメイクファイル内で使用するマクロです。このファイルをメイクファイル内にインクルードしてmakeすると、マクロ部分が置き換わるというわけです。オブジェクトファイルをスペース区切りで列挙する必要があるわけです。

 .cppの列挙ですが、あれこれ考えましたがlsを使ってみます:

この.cppを.oに置換します。ちょっと悩ましいのがスペース区切りで列挙する作業。上のlsコマンドの出力をファイルに出すと、

electricfan.cpp
main.cpp

こんな感じで「改行」が入ります。この改行をsedでスペースに置換したいんですが、これが、ん〜ちょっとうまくいきませんでした。そこで代わりに「tr」コマンドを使います。trも文字の置換や変更を行うコマンドです。上のコマンドの後にパイプで次のようにtrコマンドを発行します:

出力を見ると改行が無くなってスペースに置き変わりました。trは第1引数に置換前の文字、第2引数に置換したい文字を渡します。ここまでくればもう出来たようなもので、さらにパイプを繋げてsedで.cppを.oにするだけです。

 Dの最初に挙げた目標のテキストファイルを作るため、文字列を順にテキストファイルに出力するシェルスクリプトを作ります。せっかくなので先に作成したmake.shに追加しましょう。あれこれ試行錯誤してこんな感じで出来ました:

 真ん中のechoでの注意は\$というエスケープシーケンスですね。これが無いと変数と扱われてしまうのかうまくいきませんでした。makefile内ではobjfilesをインクルードします:

さ!これで./make.shとすればobjfilesとheaderdependファイルがそれぞれ出来て、makeが走るはずです。試しにelectricfan.hを適当に変更して保存した状態で./make.shを実行してみます:

動いとる〜(^-^)/。じゃ、じゃあmain.cppを変更したら:

きた!make.cppからmake.oが作られて、続いてオブジェクトファイルのリンクがちゃんと走った〜(^-^)/。これで、makefileに.cppや.hを登録する手間が無くなりました。もちろん穴もきっとあるとは思いますが、今はこれで自己満足。



D まとめ

 今回はメイクファイルで分割コンパイルする方法を探っていきました。愚直にメイクファイルを書いていくと、依存関係を記述しきれなくなるため自動化の道を考えざるを得ません。そのために、

1. サフィックスルールで.oと.cppの依存関係を記述
2. g++の-MMオプション+sepコマンドで.oと.hの依存関係をファイル出力(headerdependファイル)
3. ln *.cppで出力される.cppリストを加工してa.outと.oの依存関係のメイク文を出力(objfilesファイル)
4. make.shシェルスクリプトで2と3のファイル出力とmakeの連続実行

という道をたどって自動化を実現しました。作成したmake.sh及びmakefileを新しいプロジェクトのコードディレクトリに入れれば使い回しがききそうです。

 以下にそれらのファイルの内容をまとめておきます:

makefile
.SUFFIXES: .cpp .o

-include objfiles

.cpp.o:
    g++ -c $< -o $@

-include headerdepend

make.sh
#! /bin/sh

echo -n "OBJS = " > objfiles
ls *.cpp | tr '\n' ' ' | sed -e 's/cpp/o/g' >> objfiles
echo "\n\na.out : \$(OBJS)\n\tg++ \$(OBJS)\n" >> objfiles

g++ -MM *.cpp | sed -e 's/ .*.cpp//g' >> headerdepend

make

make.shはchmodコマンドで実行権限を付ける必要があります:

chmod a+x make.sh

 もう少し頑張れば、make.sh内でmakefile自体も作ってしまえると思います。そうすれば、make.shのみでコンパイルとリンクが動くようになるのでもっと楽になりますね。

 メイクの事、あんまり良く知りませんでしたが、調べてみると面白いもんでした。これで、Raspberry PiでC++プログラムをより気楽に楽しめそうです。