2021-m10 : C言語開発環境の構築(macOS) : BYOD PC のセッティング

1. はじめに

macOS の場合には、Homebrew を入れた時点で開発環境はインストール済です。 そのため、別途余計なものをインストールする手間はありません。

2. 開発環境の構築

電気電子では 4 年まで C 言語での開発を実施します。 学生の環境がバラバラなので、独自の IDE などは利用せず、make でコンパイルを実施します。 macOS ユーザも Windows ユーザも同一のリポジトリでコンパイルできるように Makefile に工夫をしています。 ここでは、用意した Makefile により、学生が make でテスト実施できるかどうかだけを確認します。

2.1 ファイルの準備

まず、iTerm2 を開き、テストをするフォルダを作成します。 今回は~/Documents/ctestという名前にしておきました。 作成したら、このフォルダに移動し、vscode を起動します。

mkdir -p ~/Documents/ctest
cd ~/Documents/ctest
code .

ここで、以下のファイルを作成します。 最初に main.c を作ると C/C++ プラグインのインストールを推奨されるので、インストールします。

  1. main.c
  2. sub.c
  3. sub.h
  4. testMain.c
  5. testCommon.c
  6. testCommon.h
  7. Makefile

さらに、C 言語のフォーマッタを設定します。 LaTeX の設定を書いた時と同様に左下の歯車から「設定...」を開き、右上の「設定(JSON)を開く」をクリックして、以下の行を追加します(詳細は省略します)。 最後に「,」があるので、設定の上の方に書くのが楽です(一番下に書いてしまうと、前の設定に「,」をつけて、貼り付けたものの「,」を取り去る作業が必要だからです)。 これを設定すると「C」モードの場合には、保存したときにフォーマッタが起動し、インデントなどを自動整形してくれます。 学生はちゃんとしたインデントがうまく書けないことがあるので、このあたりは機械に任せることにしました。

    "files.associations": {
        "*.h": "c"
    },
    "[c]": {
        "editor.wordBasedSuggestions": false,
        "editor.suggest.insertMode": "replace",
        "editor.formatOnType": true,
        "editor.formatOnSave": true,
    },
    "C_Cpp.clang_format_style": "{BasedOnStyle: Google, IndentWidth: 4}",

2.2 Makefile の準備

Makefile を開き、以下のテキストを記載します。 これは、電気電子工学コースで使っている標準的な Makefile です。 先頭の 5 つの変数を変更するだけですむように作っています。 変更すべき変数の説明だけをしておきます。

  • HEADER: 作成したい関数群のプロトタイプ宣言を記述するヘッダファイルの指定する
  • OBJECTS: 作成したい関数を書くファイルをコンパイルしたオブジェクトファイル名を記載する。4年では1関数1ファイルで分業するので、ここに大量のファイル名が並ぶ。
  • TEST_OBJECTS: 上記 OBJECTS の接頭子として「test」を追加したオブジェクトファイル名を記載する。基本は OBJECTS と同じ数だけ並ぶ。ただし、3年前期ではテストは全部一つのファイルに書くので、ここは空白となる。
  • TARGET: ライブラリ作成後に作成する最終実行ファイル名を記載する。macOS との互換性のため、Windows の場合でも .exe は付けないこと。
  • TEST_TARGET: 関数群のテスト時に実行するテストプログラムの名前。

この Makefile により、以下の三つのコマンドが実行できる。

  • make test: 関数群とそのテストを全てコンパイルし、テストプログラムを実行する。実行すると何個のテストが通過したかを教えてくれる。
  • make: 関数群のテストが全部通った後で、それを使ったメインプログラムをコンパイルし実行する
  • make clean: 何かトラブったときに、生成された実行ファイルやオブジェクトファイルなどを削除する。何度かプログラムを書いているとオブジェクトファイルにゴミが溜まってしまってうまく make できないことがたまにある。この場合にこのコマンドを実行するとよい。

Makefile は字下げの部分はタブで書かないとエラーになるので注意してください。

# makefile for xxx

#### 頻繁に変更が必要なもの
# 実装のためのヘッダー(プロトタイム宣言、構造体宣言、定数定義を含む)
HEADER=sub.h

# 作成したい関数が入ったファイル
OBJECTS= sub.o

# 関数のテストが記載されたファイル(3年前期では使わない)
TEST_OBJECTS=

# 最終実行ファイル(名前を修正したら .gitignore も修正すること)
TARGET=main

# テスト実行ファイル(名前を修正したら .gitignore も修正すること)
TEST_TARGET=testMain

#### 以下は変更する必要がないもの

# 最終実行ファイルの実名
TARGET_EXE=$(TARGET)$(EXE)
# ターゲット実行ファイルの実名
TEST_TARGET_EXE=$(TEST_TARGET)$(EXE)
# 実装のメインファイル main 関数を含む
MAIN=$(TARGET).o
# テストのためのヘッダー(プロトタイプ宣言、3年前期では使わない)
#TEST_HEADER=$(TEST_TARGET).h
TEST_HEADER=
# テストのメインファイル main 関数を含む
TEST_MAIN=$(TEST_TARGET).o
# テストに必要なファイル
TEST_COMMON=testCommon.o
# 必要な CFLAGS
CFLAGS=-Wall -g
# 必要なライブラリ
LIBS=-lm

ifeq ($(OS),Windows_NT)
    CC=gcc
    RM=cmd.exe /C del
    EXE=.exe
    CHCP=chcp 65001
else
    RM=rm -f
    EXE=
    CHCP=
endif

### ここから下の先頭の空白はタブキー で入力すること
exec: $(TARGET_EXE) $(PNGS) $(SVGS)
test: $(TEST_TARGET_EXE)

$(TARGET_EXE): $(MAIN) $(OBJECTS) $(HEADER)
  $(CHCP)
  $(CC) -o $@ $(CFLAGS) $(MAIN) $(OBJECTS) $(LIBS)
  ./$(TARGET_EXE)
#↑この字下げ部分はタブ

$(TEST_TARGET_EXE): $(TEST_MAIN) $(OBJECTS) $(TEST_OBJECTS) $(TEST_COMMON) $(HEADER) $(TEST_HEADER)
  $(CHCP)
  $(CC) -o $@ $(CFLAGS) $(TEST_MAIN) $(OBJECTS) $(TEST_OBJECTS) $(TEST_COMMON) $(LIBS)
  ./${TEST_TARGET_EXE}
#↑この字下げ部分はタブ

clean:
  $(CHCP)
  $(RM) $(TARGET_EXE) $(TEST_TARGET_EXE) $(MAIN) $(TEST_MAIN) $(OBJECTS) $(TEST_OBJECTS) $(TEST_COMMON)
#↑この字下げ部分はタブ

2.3 テストフレームワークの準備

電気電子工学コースでは C 言語のテストを実行するために、独自のテストフレームワークを用意しています。 このフレームワークでは、「assertEqualsXXXX」というマクロを使って、テストを実行するようになっています。

まず、testCommon.c に以下のコードを記載します。 ここにはテストで使う関数が記載されています。

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

// 後期に複素数構造体を使うときにはここを活かす
// #define COMPLEX

#ifdef COMPLEX
#include "complex.h"
#endif
#include "testCommon.h"

static int existErrorCount = 0;
static int totalTestCount = 0;

void testStart(char *mes) {
    fprintf(stderr, "== Test %s ==\n", mes);
}

void testErrorCheck() {
    if (existErrorCount == 0) {
    fprintf(stderr, "All tests are Ok. [# of Tests = %d, # of pass = %d (%d%%)]\n", totalTestCount, totalTestCount, 100);
    } else {
    int p = totalTestCount - existErrorCount;
    fprintf(stderr, "###### Error exist!!!! [# of Tests = %d, # of pass = %d (%d%%)] ######\n", totalTestCount, p, p *100/totalTestCount);
    exit(1);
    }
}

void assertEqualsIntFunc(int a, int b, char *fname, int line) {
    totalTestCount++;
    if (a != b)
        messend4("Error in %s(%d): a != b (%d != %d)\n", fname, line, a, b);
}

void assertNotEqualsIntFunc(int a, int b, char *fname, int line) {
    totalTestCount++;
    if (a == b)
        messend4("Error in %s(%d): a == b (%d == %d)\n", fname, line, a, b);
}

void assertEqualsFloatFunc(float a, float b, char *fname, int line) {
    totalTestCount++;
    if (isnan(a) || isnan(b))
        messend4("Error in %s(%d): a(%f) or b(%f) is NaN\n", fname, line, a, b);
    if (fabs(a - b) > DELTA)
        messend4("Error in %s(%d): a != b (%f != %f)\n", fname, line, a, b);
}

void assertEqualsDoubleFunc(double a, double b, char *fname, int line) {
    totalTestCount++;
    if (isnan(a) || isnan(b))
        messend4("Error in %s(%d): a(%f) or b(%f) is NaN\n", fname, line, a, b);
    if (fabs(a - b) > DELTA)
        messend4("Error in %s(%d): a != b (%f != %f)\n", fname, line, a, b);
}

void assertEqualsIntArrayFunc(int *ap, int *bp, int n, char *fname, int line) {
    int i;
    for (i = 0; i < n; i++) {
        totalTestCount++;
        if (ap[i] != bp[i])
            messend6("Error in %s(%d): a[%d] != b[%d] (%d != %d)\n", fname, line, i, i, ap[i], bp[i]);
    }
}

void assertEqualsStringFunc(char *ap, char *bp, char *fname, int line) {
    totalTestCount++;
    if (strcmp(ap, bp) != 0)
        messend4("Error in %s(%d): ap != bp\nap = %s\nbp = %s\n", fname, line, ap, bp);
}

#ifdef COMPLEX
void assertEqualsComplexFunc(complex a, complex b, char *fname, int line) {
    totalTestCount++;
    if (isnan(a.real) || isnan(a.image) || isnan(b.real) || isnan(b.image))
        messend6("Error in %s(%d): a(%f%+fj) or b(%f%+fj) include NaN\n", fname, line, a.real, a.image, b.real, b.image);
        if (fabs(a.real - b.real) > DELTA || fabs(a.image - b.image) > DELTA)
                messend6("Error in %s(%d): a != b (%f%+fj != %f%+fj)\n", fname, line, a.real, a.image, b.real, b.image);
}
#endif

この関数を外部から利用できるように testCommon.h にマクロを定義します。 testCommon.h に以下のコードを記述してください。 上記の関数をマクロで呼び出すことで、元のテストプログラムのファイル名と行番号が取得できます。

#ifndef __TEST_COMMON_H
#define __TEST_COMMON_H

#include <assert.h>
#include <math.h>
#include <stdlib.h>

#define message(m) fprintf(stderr,(m))
#define message1(m,a) fprintf(stderr,(m),(a))
#define message2(m,a,b) fprintf(stderr,(m),(a),(b))
#define message3(m,a,b,c) fprintf(stderr,(m),(a),(b),(c))
#define message4(m,a,b,c,d) fprintf(stderr,(m),(a),(b),(c),(d))
#define message6(m,a,b,c,d,e,f) fprintf(stderr,(m),(a),(b),(c),(d),(e),(f))
#define message8(m,a,b,c,d,e,f,g,h) fprintf(stderr,(m),(a),(b),(c),(d),(e),(f),(g),(h))
#define messend1(m,a) do{message1((m),(a)); existErrorCount++;}while(0)
#define messend2(m,a,b) do{message2((m),(a),(b)); existErrorCount++;}while(0)
#define messend3(m,a,b,c) do{message3((m),(a),(b),(c)); existErrorCount++;}while(0)
#define messend4(m,a,b,c,d) do{message4((m),(a),(b),(c),(d)); existErrorCount++;}while(0)
#define messend6(m,a,b,c,d,e,f) do{message6((m),(a),(b),(c),(d),(e),(f)); existErrorCount++;}while(0)
#define messend8(m,a,b,c,d,e,f,g,h) do{message8((m),(a),(b),(c),(d),(e),(f),(g),(h)); existErrorCount++;}while(0)

#define DELTA 1e-6

void testStart(char *mes);
void testErrorCheck(void);
void assertEqualsIntFunc(int a, int b, char *fname, int line);
void assertNotEqualsIntFunc(int a, int b, char *fname, int line);
void assertEqualsFloatFunc(float a, float b, char *fname, int line);
void assertEqualsDoubleFunc(double a, double b, char *fname, int line);
void assertEqualsIntArrayFunc(int *ap, int *bp, int n, char *fname, int line);
void assertEqualsStringFunc(char *ap, char *bp, char *fname, int line);

#define assertEqualsInt(a, b) assertEqualsIntFunc(a, b, __FILE__, __LINE__)
#define assertNotEqualsInt(a, b) assertNotEqualsIntFunc(a, b, __FILE__, __LINE__)
#define assertEqualsFloat(a, b) assertEqualsFloatFunc(a, b, __FILE__, __LINE__)
#define assertEqualsDouble(a, b) assertEqualsDoubleFunc(a, b, __FILE__, __LINE__)
#define assertEqualsIntArray(a, b, n) assertEqualsIntArrayFunc((int *)a, (int *)b, n, __FILE__, __LINE__)
#define assertEqualsString(a, b) assertEqualsStringFunc(a, b, __FILE__, __LINE__)

#ifdef COMPLEX
#define assertEqualsComplex(a, b) assertEqualsComplexFunc(a, b, __FILE__, __LINE__)
#endif
#endif

2.4 テストの自動実行

C でもプログラムを書いたら自動的にコンパイルして欲しいです。 そこで、gnuplot の時と同様に chokidar を使います。 vscode 上のターミナルで以下のコマンドを実行します。 これで、何かファイルを更新した時に、自動的にmake testコマンドが実行されます。

chokidar "*.c" "*.h" Makefile -c "make test"

2.5 テストの作成

testMain.c に以下のコードを書いてみます。 できればコピーペーストせずにタイプしてください。 vscode の補完機能を体験して欲しいためです。 ほとんどタイプしなくても、これだけの文章が補完で記述できることがわかると思いまは。 ここでは sub 関数のテストを実施しています。 sub(1.0) が 1.0 を返し、sub(2.0) が 4.0 を返し、sub(3.0) が 9.0 を返すことをテストしています。 sub 関数が何をするものなのかは予想がつくでしょう。 これを保存した瞬間に chokidar が発火し、コンパイルが実行されます。

#include <stdio.h>
#include "sub.h"
#include "testCommon.h"

void testSub() {
    //
    testStart("sub");
    assertEqualsDouble(sub(1.0), 1.0);
    assertEqualsDouble(sub(2.0), 4.0);
    assertEqualsDouble(sub(3.0), 9.0);
}

int main() {
    //
    testSub();
    testErrorCheck();
}

保存すると、sub という関数が implicit declaration と言われています。 暗黙の定義をしてしまっていて、int だと勝手に解釈されているためです。 このままだと問題なので、sub.h に sub 関数のプロトタイプ宣言を書きます。 double を入力し、double を返すので以下のようになります。 保存すると先ほどの implicit declaration は消えます。

double sub(double x);

sub のプロトタイプは書いたが関数を書いていないので、まだコンパイルは成功していません。 そこで、sub.c に関数の雛形だけを記述します。 先ほどのプロトタイプ宣言の「;」を消して return 文だけ追加します。 ここで重要なのはまだ関数の中身を書きません。 まずはコンパイルできることだけを確認することが重要です。

double sub(double x) {
    //
    return 0.0;
}

ファイルを保存するとコンパイルは成功し、次のような画面が出る。

./testMain
== Test sub ==
Error in testMain.c(7): a != b (0.000000 != 1.000000)
Error in testMain.c(8): a != b (0.000000 != 4.000000)
Error in testMain.c(9): a != b (0.000000 != 9.000000)
###### Error exist!!!! [# of Tests = 3, # of pass = 0 (0%)] ######
make: *** [testMain] Error 1

このテストが通過するように sub.c を書き換えます(ここは各自で)。成功すると次のような画面になります。 もし正しく書いてもうまく動かない場合には、一度別のターミナルでmake cleanをしてください(困ったら make clean すると解決することがあります)。

./testMain
== Test sub ==
All tests are Ok. [# of Tests = 3, # of pass = 3 (100%)]

2.6 メインプログラムの記述

関数を作ることができたので、chokidar を止めて、今度はメイン側の chokidar を起動します。

chokidar "*.c" "*.h" Makefile -c "make"

最後に main.c に以下を記述します。 何が動くかは予想がつくと思います。 今後はこんな感じでテスト記述→関数記述→メイン記述の順でプログラムを作成していきます。

#include <stdio.h>
#include "sub.h"

int main() {
    //
    printf("3^2 + 4^2 = %f\n", sub(3.0) + sub(4.0));
    return 0;
}

開発環境が動くことまでは授業前までに確認しておいてください。


hkob.hatenablog.com