はじめに
現代ではC言語でライブラリ開発をする機会が少ないです。そのためか、コードの書き方のノウハウは数あれど、ソースファイルの構成や管理についてまとまった情報はあまり多くありません。
本記事はC言語でライブラリ開発する際の、ヘッダファイルのインクルード構成について提案します。
想定
マルチプラットフォーム向けライブラリをC言語で開発する場合を想定しています。
結論
- インクルード文に相対パス(遡る方向を使わない
-
<>
インクルードを使う -
(組織名)/(ライブラリ名)/(公開|非公開)/(機能).h
でインクルードできる構成にする
前提知識
#include
ディレクティブの記法は大きく分けて2種類存在します。
<>
でパスを囲む記法と引用符””
でパスを囲む記法です。パスをどのように解釈するかはどちらの記法も処理系定義です。
””
で記述し、指定のパスにファイルが見つからなかった場合、次は<>
で記述したものとして解釈されます。
#include
ディレクティブで指定したパスの解釈が処理系定義なのは、おそらくですが処理系が動作するホスト環境のファイルシステムに依存しているからだと推測します。
引用符includeに相対パス参照を期待しない
展開順序が処理系定義
引用符includeのパスの解釈は、処理系定義のため処理系によって異なります。
以下のファイル構成を考えます。
- decls.h
- hoge/defs.h
- hoge/hoge/impl.c
#include “../decls.h”
#include “../defs.h”
impl.cをコンパイルした時、decls.hのインクルードは成功するでしょうか?
これは#include
の展開順序に依存します。展開順序は処理系定義です。
#include "../decls.h"
がdefs.hを起点に参照されればインクルードに成功しますし、先にimpl.cの#include "../defs.h"
が展開されると、その中の#include "../decls.h"
がimpl.cを起点として参照され、impl.c直上にはdecls.hが存在しないためインクルードに失敗します。
引用符インクルードがインクルード元相対とは限らない
以下の通り同階層に配置したヘッダのインクルードでも問題になる場合があります。
- src/defs.h
- sec/impl.c
#include "defs.h"
ある処理系では、引用符インクルードで指定したパスをインクルード元のファイルを起点とした相対パスとは解釈しないかもしれません。処理系定義です。
この場合は、インクルードの検索先パスをコンパイラに指定する必要があります。
引用符インクルードがインクルード元を優先的に検索するため然るべきヘッダをインクルードできない
逆に以下のような場合を考えます。
- include/defs.h
- src/defs.h
- sec/impl.c
#include "defs.h"
処理系によっては、引用符インクルードに指定されたファイルを、インクルード文が記述されたファイルの置かれたディレクトリを起点として先に検索する処理系が存在します。この場合、-Iinclude
のようにコンパイラに検索パスを設定していたとしても、src/defs.h
が優先的にインクルードされてしまいます。
ローカル作業環境にリポジトリからの削除忘れによって古いバージョンのヘッダが残ってしまっている時に、不適切な内容のヘッダを読み込んでコンパイルが通ってしまう場合があります。
相対パスが正規化されずにホスト環境のパス長制限に引っかかる
相対パスによるインクルードは、パスが正規化されずに解釈される可能性もあります。つまり、../../../../../../../include/platform/windows/misc/defs.h
の中で../../../../../../../include/platform/windows/misc/decls.h
みたいなヘッダをインクルードした場合、../../../../../../../include/platform/windows/misc/../../../../../../../include/platform/windows/misc/decls.h
と愚直に展開して読み込もうとするかもしれません。(階層関係はやっつけなのであってません。雰囲気で理解してください。)
windows環境だとパス文字列長が約250文字程度までしか扱えず、指定の場所に確かにファイルが存在しているにも関わらずインクルードに失敗する場合があります。
この場合、移植時の作業がインクルード検索パスの調整だけでは済まず、ソースコードに手を入れたりローカル環境へのファイル配置を工夫(より浅いディレクトリへ配置)するといった対応が必要になります。場合によっては作業環境を著しく制限する事になるので、チーム開発時には足枷になります。
例えば不具合再現作業で、ビルドが通るはずのソースファイル一式をダウンロードしてきたら、ビルドが通らない、ファイルが存在するはずなのにコンパイラはファイルが見つからないというエラーを出す、みたいな問題に直面します。リポジトリ上のビルド構成が壊れているんじゃないかとか疑いますよね。
できれば検索順序に依存しない
コンパイラに指定したパスの検索順序は処理系定義ですが、インクルード検索パスの指定はコンパイラに外部から与える設定のため、処理系毎に完全に制御可能です。
とは言え、処理系毎にインクルード検索パスの検索順序の調整が必要となる構成だと、移植時にヒューマンエラーの原因になります。
- lib_a/include/defs.h
- lib_b/include/defs.h
- lib_b/src/impl.c
#include <defs.h> /* lib_aのdefs.hをインクルードしたい */
#include <defs.h> /* lib_bのdefs.hをインクルードしたい */
このような構成にしてしまうと、処理系毎にインクルード検索パスの設定順序の調整が必要になります。
なるべく移植時に困らない構成
結局のところ<>
インクルードも引用符""
インクルードも指定したパスの解釈は処理系定義のため、完全に可搬性のある記述は理論的には不可能です。
ただし、現実的には<>
インクルードは常にインクルード検索パスのみを対象とし、引用符""
インクルードはインクルード文が記述されたファイルが置かれたディレクトリを最初に検索するという仕様の処理系が多いようです。
相対パスによるインクルード記述は、前述の通り処理系毎にインクルード検索パスの追加設定が必要になります。また、インクルードするヘッダが曖昧になったり事故が発生する可能性があるので、原則は<>
を使い、相対パスを期待しない記述にした方が良いです。
前述の問題に引っかからない様なライブラリのヘッダ構成は以下の通りです。
- lib_a/include/lib_a/defs.h
- lib_b/include/lib_b/defs.h
- lib_b/src/impl.c
#include <lib_a/defs.h>
#include <lib_b/defs.h>
インクルード検索パスはlib_a/include/
とlib_b/include/
に設定します。これらヘッダが処理系やプラットフォーム共通であれば、全ての処理系でインクルード検索パスの指定を共通化できます。
公開API用ヘッダと非公開の内部実装用ヘッダを分ける
以下の二つのヘッダファイルが存在する場合を考えます。
- lib_a/include/lib_a/api.h # 外部公開API
- lib_a/include/lib_a/internal_defs.h # 非公開の内部実装用
インストール時にapi.hだけincludeディレクトリ以下に配置すれば良いですが、公開用ヘッダが一つだけとは限りませんし、その名前から明確に判断できるファイルばかりでもありません。
チーム開発なんかでヘッダファイルが増えてくると、どのファイルが公開でどのファイルが非公開か管理が大変になってきます。
公開用ヘッダの一覧を作っても良いですが、ホスト環境のファイルシステムは一種の階層型データベースなので、ファイルの命名規則と配置をそのまま公開用ヘッダの一覧がわりに利用できます。
つまり、外部公開用ヘッダと非公開内部向けヘッダで配置するディレクトリを分けます。
- lib_a/include/lib_a/external/api.h # 公開ヘッダ用ディレクトリ
- lib_a/include/lib_a/internal/defs.h # 非公開ヘッダ用ディレクトリ
実装impl.cからは以下の通りにインクルードします。
#include <lib_a/external/api.h>
#include <lib_a/internal/defs.h>
このライブラリのインストールではexternalのみをインストールするので、アプリケーションがインクルードできるのはlib_a/external/api.h
側のみです。
この様にライブラリ名/公開|非公開/機能ヘッダファイル.h
の様な構成に統一しておくと、インクルード文だけで何のライブラリのどの機能を使っているかがわかりやすくなりますし、インストール時の公開/非公開ヘッダの分類も簡単になります。
また、「ライブラリ名だけだと名前が競合するかも」とか「自社(自組織)開発とサードパーティ製の区別をつけたい」ということもあるかもしれません。その場合は組織名を先頭につける形のディレクトリ構成にすると良いです。
- lib_a/include/hoge_team/lib_a/external/api.h
- lib_a/include/hoge_team/lib_a/internal/defs.h
複数のサードパーティ製ライブラリを使う様なプロダクトの場合、この様に組織名がついているとどの組織の何を使っているのか検索しやすくなります。
#include <team_hoge/lib_a/external/api.h>
#include <team_hoge/lib_b/external/api.h>
#include <team_fuga/lib_a/external/api.h>
externalが冗長だと感じる場合は、非公開ヘッダをinternalディレクトリ以下に配置するにとどめる対応でも良いかもしれません。
ちなみに、Visual StudioやVS Codeのインテリセンスはインクルード文のパスにも働きます。ヘッダ構成を階層化しておくことでインクリメンタルサーチ的にインクルードするヘッダを記述できます。
おわりに
他にも「こんな感じでヘッダ構成考えてます」があれば意見ください。