- C言語の規格
- コンパイル
- ビルド
- autotools
- unittest(おまけ)
Wikipedia等で調査した所、以下の4つの規格がC言語に存在しています。
(K&Rは規格という概念で良いのか分かりませんが・・・)
- K&R
所謂、カーニハン&リッチーの「プログラミング言語C」に書かれている規格 - C89/90
ANSIによって、最初に定められた標準規格。 - C99
C89/90に一部改定を行った規格 - C11
最新規格だが、gcc
やclang
は一部のみ対応
最初のうちは、そんなに意識しなくて良い!と思います。
そんなことより、Segmentation Fault
が何故起こるのかをきちんと理解した方がいいです。
規格が鍵になってくるのは、ソフトウェアの配布を考えた時だと思います。
例えば、以下のような書き方はC89ではエラーになります。
for(int i=0; i < 10; i++){
printf("%d¥n", i);
}
for文の中で、intの変数を宣言出来ないのです。
しかし、初学者がここまで意識すべきか?と言われると疑問です。
ソフトウェアをコンスタントに作るようになってから、おいおい理解していけば良いと思います。
-std=c89
等のコンパイルオプションで、規格を指定したコンパイルが行えます。
配布を考える時は、古い規格でもコンパイル出来るように、オプションを指定したコンパイルをやってみましょう。
コンパイルとは・・・
プログラミング言語で書かれたコンピュータプログラム(ソースコード)を解析し、コンピュータが直接実行可能な形式のプログラム(オブジェクトコード)に変換すること。
IT用語辞典 e-Words - コンパイルとは
繰返しになる人も多いと思いますが、復習を兼ねてとりあえずソースコードを書いてみましょう。
ファイル名comp01.c
#include <stdio.h>
int add (int a, int b){
return (a + b);
}
int main (){
printf("answer is %d\n", add(4, 5));
}
コンパイルコマンド
gcc -o comp01 comp01.c
作成されたcom01コマンドを実行してみましょう。
コンパイルという一言に対して、以下のような一連のプログラムが動作しています。
- プリプロセッサ
字句解析、マクロの展開、定数の数値への置換え等を行う。 - パーサー
構文解析、意味解析 - コードジェネレータ
オブジェクトコードを生成する => ほとんど機械語の状態です。しかし、実行可能ではありません。 - アセンブラ
オブジェクトコード内のアセンブラ言語(MOV, ADD, DIV等)を機械語に変換します。 - オプティマイザ
ソースコード内の、冗長な表現などを自動で最適なコードに変換してくれるものです。 C言語黎明期には、多数のC言語コンパイラが存在し、そのほとんどが有料でした。 金額の高さは、安定性とオプティマイザの性能にあったそうです。 今ではgccやclangを使うのが当たり前なので、隔世の感があります。 - リンカ
リンカは、オブジェクトファイル内の動的リンク、静的リンクを結合して、1ファイル内で完結する実行可能ファイルを作成します。 なんじゃらほい?という感じですが、全然難しくない概念です。後で、順を追って解説します。
演習1のgccコマンドは、上記の一連のコマンドが裏で実行されています。
gcc -o comp01 comp01.c
アセンブラを扱っていると、ニーモニックという単語が良く出てきます。
アセンブラでは、機械語だと人間に覚えられないから、機械語命令を簡単なアルファベットに置換えています。
置換えた命令(MOV, ADDとか)をニーモニックといいます。
日本においてニーモニックと言うと、アセンブラ言語のことを直接指すことが多いようですが、
海外では、それ以外にも人間が覚えにくい事柄を、適切な英単語で置き換えることをニーモニック(発音ではニモニック)と呼ぶようです。
英和辞書では、下記のような意味です。
記憶を助ける,記憶術の.
menemonic - Weblio
複数ファイルをコンパイルするのは、ある程度の規模のC言語プロジェクトであれば当然です。
しかし、どのようにgccコマンドを実行すれば良いのでしょうか?
実際にやってみましょう。
ファイル構成が複雑になってきますので、comp02
というディレクトリを作成して下さい。
その中に以下の3つのファイルを作成して下さい。
- main.c
#include <stdio.h>
#include "common.h"
int main (){
printf("answer is %d\n", add(4 , 5));
}
- func.c
int add (int a, int b){
return (a + b);
}
- common.h
int add (int a, int b);
演習2のプログラムは、演習1のプログラムとやっていることは同じです。
異なるのは、ファイルを分割し、add
関数を別のファイルに切り出しました。
非常に単純な例ですが、世の中のC言語プログラムは、大体同じ構造で作成されています。
ファイルの作成が終わったら、コンパイルを行います。
下記のとおりに、正確に順番にコマンドを実行して下さい。
※コマンドを実行した後は、ls -l
コマンドで実行結果で、どんなファイルが作られたかを確認して下さい。
(1) func.c
をコンパイル
gcc -c func.c
=> func.o
が出来る
(2) main.c
をコンパイル
gcc -c main.c
=> main.o
が出来る
(3) リンク
gcc -o add main.o func.o
=> add
コマンドが出来る
add
コマンドを実行すると、演習1と同じ結果になります。
(1) func.c
をコンパイル
gcc
のオプションに-c
を指定しています。
つまり、先ほど解説したコンパイルの流れの中の、コードジェネレータの部分まで実行しています。
作成されたfunc.o
はオブジェクトファイル(オブジェクトコードともいう)です。
ちなみに、-c
を付けずにgcc -o func func.c
とするとエラーになります。
main
関数が無いため、実行可能ファイルを作ることが出来ないからです。
=> 是非、試して見てください。
(2) main.c
をコンパイル
同様に、main.cをコンパイルしています。
しかし、add
関数はmain.c
で定義されていないのに、何故コンパイル可能なのでしょうか?
理由は、common.h
にプロトタイプ宣言(関数の定義)が書かれているためです。
そのため、リンクする前の段階までなら、gcc -c
コマンドでオブジェクトファイルを生成することが可能です。
(3) リンク
最後のコマンドで、二種類のリンクを行っています。
一つ目は、静的リンクです。
common.h
に記述された定義でしかなかったadd
関数に関数実体をリンクさせています。
main.o
内のadd
関数と、func.o
のadd
関数の実体がリンクします。
このように、コードとして実際に書かれている関数をリンクすることを静的リンクといいます。
=> 要するに自分が書いたコードです。
二つ目は、動的リンクです。
main.c
でprintf
コマンドを実行しています。これは共有ライブラリの関数を実行しています。
#include < >
で指定されたヘッダーファイルに記述されている関数は共有ライブラリの関数です。
これらの関数は、実行ファイル内に含めることが出来ません。
そのため、実行時に共有ライブラリを参照する形で実行します。
このように、関数実体ではなく、共有ライブラリを参照する形式でのリンクを動的リンクと呼びます。
ハンズオン第一回のおさらいになりますが、以下のコマンドを実行して見てください。
動的リンクの共有ライブラリが表示されます。
- Linux
ldd add
- Mac
otool -L add
オープンソースライセンスとして有名なGPLについて、下記のような議論がよく交わされます。
「動的リンク」は、ライセンス違反にあたるのか?否か?
このGPLの議論は、リンクという概念を理解した後じゃないと全く分かりません。
先ほど学んだ内容から考えれば、実行可能ファイル内に含まれず、実行時に共有ライブラリを参照する行為は、ライセンス違反にあたるのか?ということです。
静的リンクが可能な場合は、実行可能ファイル内に他者の著作物を含んでしまっているのでアウトなんだな!
っていう話も、リンクを理解してこそ、腹落ちしてきます。
動的リンクは、コンパイル時にどのようにコンパイラに参照されるのでしょうか?
そこを正確に理解することは、外部のミドルウェアのコンパイル等でも非常に役立ちますので、もう少しだけ深く見てみましょう。
(1) オブジェクトファイル生成時
オブジェクトファイル生成時は、ヘッダーファイル内にプロトタイプ宣言があれば、コンパイル出来ます。
共有ライブラリのヘッダーファイルは、以下の順番で探索されます。(公式ドキュメントより)
- /usr/local/include
- libdir/gcc/target/version/include
- /usr/target/include
- /usr/include
GCC - 2.3 Search Path
特定のライブラリーを使って、何かをコンパイルする場合はdevelパッケージが必要です。
へッダーファイルは、develパッケージにのみついてきます。
ちなみに上記以外の場所にヘッダーファイルがある場合は、下記のオプションでヘッダーファイルの探索pathを追加出来ます。
-I/usr/hogehoge/
(2) 動的リンク時
動的リンクを行う際は、ヘッダーファイルがあっても意味がありません。
共有ライブラリの本体が必要です。
Linuxにおいて、共有ライブラリーのロードは/lib/ld.so
が行います。
ldconfig
というコマンドで、共有ライブラリーのロードpathを表示出来ます。
Macにおいては、残念ながら分かりませんでした・・・。
動的リンク時にライブラリーが見つからない等のエラーがでた場合は、下記のオプションで共有ライブラリの探索pathを追加できます。
-L/usr/hogehoge/fugafuga
ミドルウェアをソースインストールする時に、上手くいかなくてStackoverflowを検索すると
解答の中に-I/fugafuga
を指定しろ!とかLD_LIBRARY_PATH
に追加しろ!とか書いてあります。
経験された方も多いと思います。
私も、意味も分からず色んなコマンドオプションをつけたり、環境変数を設定してみたりしました。
その当時は、何をやっているのか意味不明でしたが、分かってしまえば簡単です。
ようするに、共有ライブラリーのリンク問題を扱っていたというわけです。
まさに、先ほどやった演習2がビルドです。
つまり、コンパイルしてリンクして、実行可能ファイルを作成する一連の流れがビルドです。
演習1は、あまりにも単純な構成なのでコンパイル≒ビルドになっていたというわけです。
このビルドですが、結構面倒くさいのです。
演習2でさえ、3つのコマンドを打つ必要があります。しかも順番もあります。
C言語では、面倒くさいビルド作業を効率化するために、make
を使います。
とりあえずやってみましょう!
演習2で作ったディレクトリでMakefileを作りましょう。
=> 文字通り、Makefileという名前のファイルを作ります。
Makefile
all: clean func.o main.o
gcc -o add func.o main.o
func.o:
gcc -c func.c
main.o:
gcc -c main.c
clean:
rm -f *.o add
Makefileのインデントはタブです。
出来上がったら、make
コマンドを叩いて下さい。
add
という実行可能ファイルが生成されればOKです。
main.c
の足し算の数字を変更して、makeコマンドを再度実行して、ビルドし直されていることを確認して下さい。
makeの良い所は、非常に単純ということです。
make
というコマンドを実行すると、Makefile
の先頭のコマンドが実行されます。
コマンドはネストさせることが出来ます。
この例では、all
を実行すると、clean, func.o, main.o
の順番にコマンドを実行します。
それぞれのコマンドの中身は、それぞれのコマンドの項に書いてある通りです。
コマンドを指定して実行することも出来ます。make clean
と打てばclean
コマンドを実行できます。
とても単純ですが、プロジェクトが大きくなるとMakefile
も結構複雑になります。
make
のお陰でビルドは大分楽になるのですが、それでもまだまだ面倒臭いんです。
makefileさえも自動で生成してしまおう!
業界で標準的なmakefileにしちゃおう!
必要なライブラリーのチェックも自動でしちゃおう!
そんな期待に応えるのがautotools
です。
憧れの./configure
, make
, make install
です。
autotoolsに関しては、とにかくやってみるのが一番です。
本ハンズオンでも、とりあえず写経形式で、autotools
の関連ファイルを作って行きます。
autoconf
, automake
をインストールします。
brew install autoconf
brew install automake
Installing Autoconf, Automake & Libtool on Mac OSX
yum install autoconf
yum install automake
※sudoか、rootユーザで
apt-get install autoconf
apt-get install automake
※sudoか、rootユーザで
comp04
ディレクトリを作成して下さい。
演習2,3で使ったmain.c
, func.c
, common.h
をコピーして下さい。
スタート時点では、下記の構成です。
comp04
├── common.h
├── func.c
└── main.c
bin_PROGRAMS=add
add_SOURCES=func.c main.c
autoscan
コマンドで、雛形が出来ます。
autoscan
mv configure.scan configure.ac
configure.acの中身
せっかく雛形作りましたが、最小構成にするため、ほとんど削除します
AC_INIT([ADD PROGRAM], [1.0])
AM_INIT_AUTOMAKE
AC_PROG_CC
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
aclocal
autoconf
=> aclocal
コマンドで、aclocal.m4
というファイルが出来ます。autoconf
に必要なコマンド類です。
automake -a -c --foreign
-a
,-c
はautotools
が必要とするコマンド群をコピーして配置してくれます。
install-sh
, missing
, depcomp
, compile
とかがそれにあたります。
--foreign
は、GNUプロジェクトのしきたりに従う下記のファイル群を用意しないことを意味します。
- NEWS
- README
- AUTHORS
- ChangeLog
=> 今回はシンプルなautotools
を目指すのでこれらのファイルは無視します。
./configure
make
make install
※/usr/local/bin
にインストールされるので、後で削除しておきましょう・・・
- Makefile.am
=> コマンド名、対象ファイルを記述する - configure.ac
=> configureコマンドでチェックする中身を記述する => ヘッダーファイルや、ライブラリーのチェックとかも、ここでかけます。
AC_CHECK_HEADERS([stdlib.h string.h sys/param.h unistd.h])
AC_CHECK_LIB(readline, rl_digit_argument, [], [
echo "Error! readline-devel is required."
exit -1
])
それ以外のファイルは、autotools
のコマンドで自動生成します。
ls -l
してみましょう。憧れの代償として、いかに多くのファイルが必要とされるのかが分かりますw
最初は3ファイルでしたね・・・。
ちなみにですが、普通はC言語のプロジェクトではプロジェクトルートにソースコードを配置してないです。
src
というディレクトリを作って、その中に配置することが多いです。
autotools
までやれば、通常のC言語プロジェクトに対応できる下地が出来たことになります。
しかし、2015年ですから、ユニットテストについても、基本を抑えておきましょう。
恐れることはありません。今日すでに学んだ内容で充分理解できます。
つまり、リンクです。
Google TestというC言語のユニットテストツールがGoogleから提供されています。
Travis CIでの実装例もネット上に転がっていますので、初心者にやさしいテスト・ツールです。
演習2のファイル群をもう一度用意して下さい。
comp05
├── common.h
├── func.c
└── main.c
Google Testをチェックアウトして、テスト用ライブラリーをビルドしておきます。
comp05ディレクトリ内で実行して下さい。
svn co http://googletest.googlecode.com/svn/trunk gtest
mkdir build
cd build
cmake ..
make
comp05ディレクトリ直下に、test.cppというファイルを作ってください。
中身は、以下のとおりです。
#include <gtest/gtest.h>
extern "C"
{
#include "common.h"
}
TEST(SetConstructTest, ConstructFromArray)
{
ASSERT_EQ(12, add(4, 8));
ASSERT_EQ(9, add(4, 5));
}
int main(int argc, char **argv)
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Test用の実行ファイルを作成する。
gcc -c func.c
clang++ -std=c++11 -g -Wall -Wextra -c test.cpp -I./gtest/include
clang++ -std=c++11 -g -Wall -Wextra -o test func.o test.o -lgtest -L./gtest/build
Test実行
./test
Test自体はc++で書きますが、内容はユニットテストなので、なんとなく分かると思います。
func.c
のadd
関数をテストしています。
テスト用の実行ファイルを作成するために、下記のことをやっています。
func.c
のオブジェクトファイルを作成
=>add
関数の実体はfunc.o
です。test.cpp
のオブジェクトファイルを作成
=>add
関数のテストを行いますが、定義さえ分かればコンパイルは出来ますよね!なので、先頭でヘッダーファイルをinclude
してます。func.o
とtest.o
をリンクして、実行可能ファイルを作ります。
=> 演習2と基本は一緒です。
つまり、テストの実行可能ファイルは、テスト内容のオブジェクトファイルと、テスト対象の関数のオブジェクトファイルを静的リンクしたものです。
なお、それぞれのコマンドでは-I
, -L
でそれぞれGoogle Testのヘッダーファイルと、共有ライブラリのPATHを指定してます。