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

1. はじめに

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

2. 開発環境の構築

電気電子では 4 年まで C 言語での開発を実施する。学生の環境がバラバラなので、独自の IDE などは利用せず、make でコンパイルを実施する。とりあえず、学生が make でテストが実施できるかどうかだけ、ここで確認する。

2.1 ファイルの準備

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

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」モードの場合には、保存したときにフォーマッタが起動し、インデントなどを自動整形してくれる。学生はちゃんとしたインデントがうまく書けないことがあるので、このあたりは機械に任せることにした。

    "[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 できないことがたまにある。この場合にこのコマンドを実行するとよい。

(5/29 追加) 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
else
    RM=rm -f
    EXE=
endif

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

$(TARGET_EXE): $(MAIN) $(OBJECTS) $(HEADER)
  $(CC) -o $@ $(CFLAGS) $(MAIN) $(OBJECTS) $(LIBS)
  ./$(TARGET_EXE)

$(TEST_TARGET_EXE): $(TEST_MAIN) $(OBJECTS) $(TEST_OBJECTS) $(TEST_COMMON) $(HEADER) $(TEST_HEADER)
  $(CC) -o $@ $(CFLAGS) $(TEST_MAIN) $(OBJECTS) $(TEST_OBJECTS) $(TEST_COMMON) $(LIBS)
  ./${TEST_TARGET_EXE}

clean:
  $(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 を使ってみる。iterm2 で以下のコマンドを実行する。

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

2.5 テストの作成

testMain.c に以下のコードを書いてみる。できればコピーペーストせずにタイプしてみて欲しい。Visual Studio Code の補完機能を体験して欲しいからである。ほとんどタイプしなくても、これだけの文章が補完で記述できることがわかると思う。ここでは 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をしてみるとよい。

./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