依存関係は専用のツールを使う

これまでの説明でWindows 10で基本的なC言語開発はできるようになった。開発環境としてVisual Studio Codeを、コンパイラインフラストラクチャとしてLLVM Clangを、デバッガにLLDBを、あとはそれに付帯するソフトウェアをインストールして利用できるようにした。

そして、ビルド作業はtasks.jsonファイルに次のようなデータを書いておくことで実施した。

tasks.json

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Clang",
            "type": "process",
            "command": "clang.exe",
            "args": [
                "-g",
                "${file}",
                "-o",
                "${fileDirname}/csv2tsv.exe"
            ],
            "problemMatcher": [],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

C言語の開発環境をセットアップするチュートリアルなどでは、1つのファイルで構成されたC言語のソースコードをビルドするという方法をサンプルとして取り上げることが多い。このため、ビルドの目的もtasks.jsonファイルで事足りるのだ。

しかし、実際に1ファイルで済むようなCのソースコードは、シンプルなコマンドのように比較的単機能でソースコードが短くて済むようなケースに限られる。実際にはいくつものCのソースコードファイルがあり、それらをビルドして1つのバイナリを生成するというビルドを行う必要がある。

tasks.jsonでそのようなビルドを実現できないことはないのだが、それ目的に開発された機能ではないので、それほど得意ではない。複数のCのソースコードファイルをビルドしてバイナリファイルを生成するといった処理を実現する方法はいくつかあるが、ここではmakeを使う方法を取り上げる。tasks.jsonはmakeを呼び出すものに書き換えて使うのだ。

makeを使う

依存関係を持った複数のファイルを処理する場合に使われる典型的なツールの一つがmakeだ。Cのソースコードファイルをビルドするために使われることはもちろん、それ以外のさまざまな用途で使われている。使えるようにしておくと何かと便利なツールだ。

makeはMakefileというファイルに処理のルールを記述する。Makefileの書き方はいくつかある。大きく分けるとBSD makeとGNU makeと辺りで、ここではGNU makeを使う方法を取り上げる。GNU makeはLinuxで使われることが多く、WindowsでもWinget経由でインストールできる。Macでも最初から使えるので、GNU makeに慣れておくとほかのプラットフォームでも扱いやすいのだ。

makeをインストール

Windows 10にGNU makeをインストールしよう。次のようにWinget経由でインストールする。Wingetが利用できるようになったことで、Windows 10にこうしたユーティリティをインストールするのは本当に簡単になった。

GNU makeのインストール

winget install GnuWin32.Make
  • GNU makeのインストールサンプル

    GNU makeのインストールサンプル

GNU makeをインストールしたらmakeコマンドが置かれているフォルダパスを環境変数PATHに追加する。「PATH」で検索して「システムのプロパティ」を起動し、システムのプロパティで「環境変数」ボタンをクリックする。ユーザーの環境変数「Path」を選択して、ここに「C:\Program Files (x86)\GnuWin32\bin」を追加する。

  • ユーザの環境変数Pathを編集

    ユーザーの環境変数Pathを編集

  • 「C:\Program Files (x86)\GnuWin32\bin」を追加

    「C:\Program Files (x86)\GnuWin32\bin」を追加

次のように、コマンドとしてmakeが実行できるようになっていればインストール完了だ。

  • makeコマンドが使用できることを確認

    makeコマンドが使用できることを確認

ビルドに使うソースコードを用意

makeの動作確認ができるように、複数のファイルで構成されたコマンドの開発を例に考える。ここでは「csv2tsv」というコマンドを開発するという想定で行ってみよう。このコマンドはCSVファイルをタブ区切りのTSVファイルにデータを変換するというコマンドだ。次のようなファイル構成で開発することにしよう。

  • csv2tsvコマンドのファイル構成

    csv2tsvコマンドのファイル構成

main()関数が含まれるのが「csv2tsv.c」で、実際のデータ変換処理は「util.c」で行うものとする。共通のヘッダファイルが「csv2tsv.h」だ。まず、makeの動作確認だけを行いたいので、次のように処理を行わないファイルとして用意する。

csv2tsv.h

#include <stdio.h>

int csv2tsv(char *, char *);

csv2tsv.c

#include "csv2tsv.h"

int
main(int argc, char *argv[])
{
    char dummybuf1[4096];
    char dummybuf2[4096];

    csv2tsv(dummybuf1, dummybuf2);

    return 0;
}

util.c

#include "csv2tsv.h"

int
csv2tsv(char *buf_csv, char *buf_tsv)
{
    printf("開発中\n");

    return 0;
}

csv2tsv.cのmain()から、util.cのcsv2tsv()が呼ばれている。どちらのファイルもコンパイルしないと、動作するバイナリファイルは生成されないわけだ。

Makefileを用意

今回は詳しい説明は省くが、まずは動作するサンプルとして、次のようなMakefileを用意する。

Makefile

OBJS=       csv2tsv.o util.o
CMD=        csv2tsv.exe

CFLAGS+=    -g

CC=     clang

build: $(OBJS)
    $(CC) $(CFLAGS) -o $(CMD) $(OBJS)

csv2tsv.o: csv2tsv.c
    $(CC) $(CFLAGS) -c csv2tsv.c -o csv2tsv.o

util.o: util.c
    $(CC) $(CFLAGS) -c util.c -o util.o

clean:
    rm -f "$(CMD)"
    rm -f *.o
    rm -f *.ilk
    rm -f *.pdb

Makefileはファイルの依存関係、ビルド方法、そのほかのタスク(ターゲット)が記載されている。Makefileを読むと、開発者がどのような意図を持ってソースコードの分割や配置を行なったのかも見えてくる。

Makefileは一見すると複雑そうに見えるが、記述ルールはそれほど難しくない。汎用的に使えるようにすると変数が増えてくるのでどうしても読みにくくなるが、それもある程度は慣れの問題だ。Cはもちろん、さまざまな作業に使えるツールなので、使ったことがなければぜひなじんでもらえればと思う。

makeを使ってみよう

makeを実行するのにVisual Studio Codeは必要ない。Visual Studio Codeとの連携は後で取り上げるとして、まずはmakeだけで使えるようにする。先ほどのようなMakefileを作った場合、次のようにコマンドを実行することでバイナリファイル(実行ファイル)をビルドできる。

ビルド

make

または、次のように実行してもよい。

ビルド

make build

次のスクリーンショットのように生成された実行ファイルが実行できることを確認できる。

  • 生成されたコマンドの実行を確認

    生成されたコマンドの実行を確認

makeはMakefileを読んで処理を進め、実際には次のコマンドを実行している。

実際に実行されたコマンド

clang -g -c csv2tsv.c -o csv2tsv.o
clang -g -c util.c -o util.o
clang -g -o csv2tsv.exe csv2tsv.o util.o

実際、makeコマンドが実施するのはこうした処理だけだ。

makeのおいしさを知る

これではスクリプトにコマンドを列挙しておいて実行するのと変わらない。makeがおいしいのは、2回目以降、必要のないビルドは実行しないという点にある。例えば、util.cを次のようにちょっとだけ書き換えてみよう。

util.cをちょっとだけ書き換え

#include "csv2tsv.h"

int
csv2tsv(char *buf_csv, char *buf_tsv)
{
    printf("開発中...\n");

    return 0;
}

この状態でもう一度makeを実行すると次のようになる。

  • ビルド

    ビルド

今度実行されたのは次のコマンドだ。

実際に実行されたコマンド

clang -g -c util.c -o util.o
clang -g -o csv2tsv.exe csv2tsv.o util.o

今度はcsv2tsv.cというファイルのビルドは行われていない。このファイルは更新されていないので、ビルドする必要がないためだ。しかし、util.oは更新されたので、csv2tsv.exeは作り変える必要がある。makeはこうした関係を整理して必要な処理だけを行なってくれる。複数ファイルのビルドを行う上で便利なツールなのだ。

参考