分割コンパイルと静的ライブラリでチェーン実装のメモ(c++)

注意事項

  • 2週間くらい1人で適当に弄った結果,アテにならん
  • 静的リンクだけやってる
    • 動的リンクはやらない
    • 動的ロードもやらない
    • あくまで過酷環境の組込実装のためにやってる
      • 動的リンクも動的ロードも無理らしい…
      • 暇があれば後でやってみようと思ってる
  • スマートポインタは使わない
    • c++98を想定してる
    • スマートポインタはc++11以降の機能なので

概要

  • プロジェクトの構成
    • 認識のしやすさ
    • 管理のしやすさ
    • モジュールとして分割して開発
  • makefileの改造
    • シェルの文法なのかmakeの文法なのかこれもうわかんねぇな
    • makeの機能だけで色々やろうとしたが個人的には無理だった
      • 文法の縛りがキツくて関数の実用性をあまり感じなかった
    • もうシェルスクリプトを叩く方が早い?初期化もシェルで叩く?
      • モジュール追加時の自動構成もできると気持ちが良さそう
  • クラスxをメンバ変数に持つクラスyを定義するヘッダファイルでは,前方宣言でxを定義してincludeの多重ロードを防止する
    • つまりヘッダファイルではxを定義してincludeせずに,ソースファイルではxを定義するヘッダファイルをincludeする
    • たとえば「「「クラスx」を抱えるクラスy」を抱えるクラスz」を定義する時にヘッダファイルに依存関係を全て表示すると,yを定義するヘッダファイルに加えてyを経由してxを定義するヘッダファイルまでロードする,といった多重インクルードが発生する
      • 何レイヤも下にあるライブラリのヘッダファイルを遡上するのは,まともにやろうとしてもinclude pathの管理が壊れる
      • 名前空間の管理も高レベルになる,ヘッダファイルの挙動によっては思わぬ動作を生むかもしれない,俺は辛い耐えられない
      • 1つのライブラリを複数のソースで構成してincludeするならOK,ただしそれでもinclude pathには十分に注意する
  • そのような実装をするクラスyはクラスxをメンバ変数に持つが,コンパイラの都合によって実体ではなくポインタで持つ必要があり
    • たとえば「「「クラスx」を抱えるクラスy」を抱えるクラスz」の実装をコンパイルする際は,多重インクルードが阻止されているためにxのサイズが不定となり,つまりyのサイズも不定となり,そしてxのサイズも不定となるため,普通にエラーを吐かれる
      • もっと簡単に表現すると2つ下のレイヤで定義されたクラスのサイズがわからないから実体を持つとコンパイルできない
    • 実体を持たずにポインタを持つならば,ポインタのサイズは処理系によれどいずれも既知となるはずなので,この問題は起こらない
      • ある意味でシンボル指向に基づくオブジェクト指向みたいな感覚を覚えた,普段からリンクする時のことを考えた方が良い
    • ユニークポインタが未実装のc++98ではnewdeleteを自力で管理しないとメモリリークするので,気をつけるべし
    • さらに言えばヘッダファイルが隔離されているせいで2つ下レイヤのクラスにあるメソッドや変数は叩けない,気をつけるべし
  • 静的ライブラリの取り扱いについて
    • 実行ファイルの生成時にシンボルの多重定義を防止するには,ライブラリ毎に生成した静的ライブラリを最後にまとめて呼び出す(普通はこうするっぽい)か,依存する静的ライブラルをバラして得たオブジェクトとコンパイルで得たオブジェクトをまとめてマージして重複を消し飛ばす作業を,ライブラリ毎の静的ライブラリの生成からメインのオブジェクトが呼ぶ静的ライブラリの生成まで一貫してやり続けるか…
      • 前者はライブラリの依存関係がソースファイルのincludeの依存関係に一致しないので.リンカに渡すパスの管理が面倒
      • 後者はディスクを無駄に消費する,ライブラリ毎に設定されるべき責任境界が壊れる,実装の変更の反映も簡単にできない
    • ちなみに過酷環境の組込実装では浮動小数点演算がrun-time-supportのライブラリとして提供されている場合があるらしい

プロジェクトの構成

以下は一例

.
├── bin
│   └── main
├── libA
│   ├── inc
│   │   └── libA.hpp
│   ├── lib
│   │   └── liblibA.a
│   └── src
│       ├── clsA.cpp
│       └── clsA.hpp
├── libB
│   ├── inc
│   │   └── libB.hpp
│   ├── lib
│   │   └── liblibB.a
│   └── src
│       ├── clsB.cpp
│       └── clsB.hpp
├── makefile
└── src
    └── main.cpp

規則

  1. モジュール毎にディレクトリを設置
  2. その下はinclibsrcに分割
  3. srcにはソースファイルとヘッダファイルを設置
  4. libには静的ライブラリを設置
  5. incのはsrcにあるヘッダファイルを全てincludeした代表ヘッダファイルを設置
    • 他モジュールやメインモジュールから参照されるヘッダファイルはこの1つに統一する
    • モジュールのコンパイル時にソースファイルに応じてmakefileで自動的に生成する
    • ただしinclude pathsrcの方にも通す,代表ヘッダファイルが呼び出すので

makefileの改造

以下は一例

SHELL     =   /bin/zsh

MAIN_SRC    =   src/main.cpp
MAIN_OBJ    =   main.o
MAIN_BIN    =   bin/main

GCC_BIN     =   clang++
GCC_FLG     =   -std=c++98 -O2


make    :
    make libA
    make libB
    make main


libA    :   FORCE
    $(call setAllVar,libA)
    $(GCC_BIN) $(GCC_FLG) -c $(libA_SRC)
    @ar cr $(libA_OBJ) *.o
    @rm -f *.o
    $(shell echo '#pragma once' > $(libA_REP);)
    $(foreach hed,$(libA_HED),$(shell echo '#include "$(hed)"' >> $(libA_REP);))

libB    :   FORCE
    $(call setAllVar,libB)
    $(call setRefVar,libA)
    $(GCC_BIN) $(GCC_FLG) -c $(libA_INC) $(libB_SRC)
    @ar x $(libA_OBJ)
    @ar cr $(libB_OBJ) *.o
    @rm -f *.o __.*
    $(shell echo '#pragma once' > $(libB_REP);)
    $(foreach hed,$(libB_HED),$(shell echo '#include "$(hed)"' >> $(libB_REP);))

main    :   FORCE
    $(call setRefVar,libB)
    $(GCC_BIN) $(GCC_FLG) -c $(libB_INC) $(MAIN_SRC)
    @ar x $(libB_OBJ)
    $(GCC_BIN) $(GCC_FLG) -o $(MAIN_BIN) $(MAIN_OBJ) $(libB_LIB)
    @rm -f *.o __.*


FORCE   :


define setAllVar
    $(eval $1_HED = $(notdir $(wildcard $1/src/*.hpp)))
    $(eval $1_SRC = $(wildcard $1/src/*.cpp))
    $(eval $1_OBJ = $1/lib/lib$1.a)
    $(eval $1_REP = $1/inc/$1.hpp)
    $(eval $1_INC = -I$1/inc -I$1/src)
    $(eval $1_LIB = -L$1/lib -l$1)
endef

define setRefVar
    $(eval $1_OBJ = $1/lib/lib$1.a)
    $(eval $1_REF = $1/inc/$1.hpp)
    $(eval $1_INC = -I$1/inc -I$1/src)
    $(eval $1_LIB = -L$1/lib -l$1)
endef

説明

  • shellzshを指定する
  • ただしシェルスクリプトの中でmakeの機能を叩くのは無理
    • 関数を書いても実用性を感じられなかったのはこのため
    • せいぜい変数を自動設定するくらいしかできなさそう…
  • 存在するソースに応じ代表ヘッダファイルを自動的に生成する
    • $(foreach)して$(shell echo)したらできた…
  • 空変数FORCEを使って常に強制的にmakeを実行させる
    • モジュール毎にコンパイルするだけなら十分だと思った
  • -Lは静的ライブラリの探索パスでリンク時に使う,-Iはヘッダファイルの探索パスでコンパイル時に使う
  • リンク時に-Lと一緒に使う-lはライブラリを指定するが(lib)libA.aみたいに先頭のlibが削れるらしい

静的ライブラリの取り扱い

バラしてマージするパターンを実装した ar x libNameでバラしてar cr libName *.oでマージしてrm *.o __.*で一時ファイルを消す

macOSに入るcommand line toolclang++__.SYMDEFなるものを出すが,たぶんgnuclang++でも同じことが起きる 見た感じシンボルのリスト(が短縮されたもの)が入ってるテキストファイルっぽかったが,用途がよくわからない,調べる必要があるかも

多重ロード防止実装

以下は一例

libA

#pragma once

namespace libA{

class clsA{
    private:
        int __Num;
    protected:
        int _Num;
    public:
        clsA();
        ~clsA();
        int Num;
        void aLog();
};

}
  • 3レベルそれぞれのアクセス権限に1つずつ変数を置く
#include <iostream>
#include "clsA.hpp"

using namespace std;
using namespace libA;

clsA::clsA(){
    cout << "construct A" << endl;
    __Num = 0;
    _Num = 1;
    Num = 2;
}

clsA::~clsA(){
    cout << "destruct A" << endl;
}

void clsA::aLog(){
    cout << "  log from A" << endl;
    cout << "    private num: " << __Num << endl;
    cout << "    protected num:  " << _Num << endl;
    cout << "    public num: " << Num << endl;
}
  • コンストラクタとデストラクタを追跡する
  • 同一オブジェクト内メソッドであれば,どのアクセスは権限レベルでも問題はない

libB

#pragma once
namespace libA{
    class clsA;
}

namespace libB{

class clsB{
    private:
        libA::clsA* __aObj;
    protected:
        libA::clsA* _aObj;
    public:
        libA::clsA* aObj;
        clsB();
        ~clsB();
        void bLog();
};

}
  • 1つ下のレイヤのクラスをポインタで持つ
    • 3レベルそれぞれのアクセス権限に1つずつ置く
#include <iostream>
#include "libA.hpp"
#include "clsB.hpp"

using namespace std;
using namespace libA;
using namespace libB;

clsB::clsB(){
    cout << "construct B" << endl;
    __aObj = new clsA();
    _aObj = new clsA();
    aObj = new clsA();
}

clsB::~clsB(){
    cout << "destruct B" << endl;
    delete __aObj;
    delete _aObj;
    delete aObj;
}

void clsB::bLog(){
    cout << "log from B" << endl;
    cout << "  private A:" << endl;
    __aObj->aLog();
    cout << "  protected A:" << endl;
    _aObj->aLog();
    cout << "  public A:" << endl;
    aObj->aLog();
}
  • newdeleteでメモリを管理する
    • c++11以降はユニークポインタを使うべき?
  • ポインタで持つので.ではなく->で変数やメソッドを呼び出す

main

#include <iostream>
#include "libB.hpp"

using namespace std;
using namespace libB;

int main(){
    clsB* bObj = new clsB();;
    bObj->bLog();
    delete bObj;
};
  • クラス設計するわけでもないので,オブジェクトは直置きできるはず
    • が,newdeleteを使った方がコーディング規約的に楽かも
  • mainからは2つ下のレイヤのclsAの変数とメソッドを叩けない
    • ある意味では意図せずにセキュアな実装になっててビックリした

実行

% make    
make libA
clang++ -std=c++98 -O2 -c libA/src/clsA.cpp
make libB
clang++ -std=c++98 -O2 -c -IlibA/inc -IlibA/src libB/src/clsB.cpp
make main
clang++ -std=c++98 -O2 -c -IlibB/inc -IlibB/src src/main.cpp
clang++ -std=c++98 -O2 -o bin/main main.o -LlibB/lib -llibB
% bin/main
construct B
construct A
construct A
construct A
log from B
  private A:
  log from A
    private num: 0
    protected num:  1
    public num: 2
  protected A:
  log from A
    private num: 0
    protected num:  1
    public num: 2
  public A:
  log from A
    private num: 0
    protected num:  1
    public num: 2
destruct B
destruct A
destruct A
destruct A

大域変数に自作クラスを使うと…

過酷環境の組込実装では初期化と割込制御をint main(){}interrupt void xxx(){}のように実装するので,例えば制御器は大域変数を介して動作する必要がある それぞれの関数や変数は別のソースに書いてexternしてまとめてコンパイルすれば使えるが,そういうやり方ではいつか地獄を見るのでよろしくないとされる

では大域変数として制御器の機能を持つ自作クラスをオブジェクト化するとどうなるのかと言えば,(処理系によって違うが)mainの実行前の処理にオブジェクトのコンストラクタが走るように登録されたり,実行後の処理にデストラクタが走るように登録されたりする,これもrun time supportのライブラリで提供される機能となる しかし実質的にmainが初期化ルーチンとなっているなら,クラスのポインタを大域変数に置きつつ後でnewで実体化しても良さそうなのだが,するとdeleteってどうやって登録すれば良いのだろうか…?