本投稿、[新卒が作る自作OS]は、我々が自作OSを作るにあたり、詰まったところや、備忘録的に残しておきたいところなどをまとめておこうという趣旨の投稿です。
今回はソースコードを分割する方法とmakeによる自動コンパイルについて取り上げます。使用する言語はC言語です。
本記事のソースコードの開発環境は以下の通りです。
OS | Windows 10 Pro 64bit |
コンパイラ | gcc 9.2.0 |
make | GNU Make version 3.79.1 |
ソースコードの分割
大規模なプログラムを書いていると、どうしても行数が増えてしまい複雑になります。そんなときは、ソースコードを機能ごとに複数に分割することで可読性を高めます。
ソースコードの分け方
C言語の文法上はどのように分けても問題はないのですが、一般的には以下のようなルールで分割すると可読性が高まります。プログラム本体のCファイルは基本的にはmain関数のみを記述します。それ以外の関数は類似機能ごとにヘッダファイルと関数定義用のCファイルに分割します。ヘッダファイルには関数の定義は書かず、プロトタイプ宣言を記述します。他に共通するinclude文、define定数、グローバル変数や型の定義を記述します。このように分けることでヘッダファイルが定義されている関数や変数の一覧表のような役割を果たし、より読みやすいソースコードになります。
ファイル | 記述内容 |
Cファイル(本体) |
|
ヘッダファイル |
|
Cファイル(関数定義) |
|
分割前のソースコード
ソースコードの分割のサンプルコードを作成しました。こちらが分割前のソースコードsample.cです。3つの関数print_hello、print_num、print_structではそれぞれグローバル変数、define定数、構造体を呼び出して画面表示しています。これらをmain関数から呼び出しています。
#include <stdio.h> #define NUM 10 struct MYSTRUCT { int i1, i2, i3; }; char *str; void print_hello(void) { str = "Hello!\n"; printf("%s", str); } void print_num(void) { printf("%d\n", NUM); } void print_struct(void) { struct MYSTRUCT ms; ms.i1 = 1; ms.i2 = 2; ms.i3 = 3; printf("%d,%d,%d\n", ms.i1, ms.i2, ms.i3); } int main(void) { print_hello(); print_num(); print_struct(); return 0; }
コンパイルをするにはコマンドラインに以下のように記述します。
# gcc sample.c
分割後のソースコード
sample.cを先程の表の分け方に従い分割します。上から順にmain.c、print.h、print.cです。
#include "print.h" int main(void) { print_hello(); print_num(); print_struct(); return 0; }
#pragma once //インクルード #include <stdio.h> //定数定義 #define NUM 10 //型定義 struct MYSTRUCT { int i1, i2, i3; }; //グローバル変数定義 char *str; //関数のプロトタイプ宣言 void print_hello(void); void print_num(void); void print_struct(void);
#include "print.h" void print_hello(void) { str = "Hello!\n"; printf("%s", str); } void print_num(void) { printf("%d\n", NUM); } void print_struct(void) { struct MYSTRUCT ms; ms.i1 = 1; ms.i2 = 2; ms.i3 = 3; printf("%d,%d,%d\n", ms.i1, ms.i2, ms.i3); }
コンパイルをするにはコマンドラインに以下のように記述します。
# gcc -o a.exe main.c print.c print.h
main.cはmain関数のみ、print.cは各関数の定義のみになりかなりスッキリしました。print.hを見ることでprint.cで定義した関数や使用している変数などがすべてわかります。この程度の分量のソースコードでも内容がわかりやすくなったと思います。大規模な開発になるほど可読性の差は顕著になります。
makeでコンパイルを自動化
makeとは
簡単にいうと高性能なバッチファイルです。ファイルの依存関係を調べ必要な部分のみ実行することができます。一部のソースファイルを修正したとき、通常のコンパイルであればすべてのソースコードをコンパイルし直さなければなりませんが、makeを使えば変更に関連するファイルのみをコンパイルすることができ、コンパイルの高速化を実現できます。
いくつか種類がありますが、ここではGNUmakeを使用しています。
GNU公式サイト:https://gnuwin32.sourceforge.net/packages/make.htm
makeの書き方
Makefileというファイル名で以下の内容を記述します。他にも様々な機能がありますがここでは省略します。
【ターゲット】:【依存ファイル】 【コマンド】
実行時に依存ファイルに変更があった場合、コマンドが実行されます。今回のソースコードをコンパイルする場合は以下のように記述します。main.c、print.c、print.hのいずれかが更新されると、それぞれ対応するmain.o、print.oが新たに生成されます。-cオプションはコンパイルのみ行いオブジェクトファイルを生成します。main.o、print.oのいずれかが更新されると実行ファイルであるa.exeのコンパイルが行われます。
a.exe: main.o print.o gcc -o a.exe main.o print.o main.o:main.c gcc -c main.c print.o:print.c print.h gcc -c print.c .PHONY: clean clean: del main.o print.o print.h.gch
コマンドラインに以下のように記述することで実行できます。
# make
このようにmakeを導入することで、ファイルを更新する度にすべてのファイルをコンパイルする必要がなくなり、コンパイルが高速化されます。コマンドを何度も書く必要もなくなるので手間も減らせます。
10~12行目は生成された中間ファイルを削除するためのコマンドです。以下のように記述することでコンパイルの過程で生成されたmain.o、print.o、print.h.gchを削除できます。
# make clean
まとめ
今回はソースコードを分割する方法とmakeによる自動コンパイルについてまとめました。OS開発に直接的に関わる部分ではありませんが、開発を効率的に行うために非常に重要な内容です。
最後までお読みいただきありがとうございます。