C++形式の共有ライブラリの書き方(gcc編)

  • 5
    いいね
  • 0
    コメント

この記事はADVENTARのC++ Advent Calender 2016の7日目の記事です。
明日はyumetodoさんのKCS_KAIでの高速化の経験をまとめた何かです。KanColleSimulatorの頭文字のようですがKAIってなんだろう?

1.はじめに

昨日、C++形式の動的リンク・ライブラリの書き方(msvc編)としてVisual C++用の「動的リンク・ライブラリ」のノウハウを書きました。
今日は、「共有ライブラリの書き方」としてます。共有ライブラリと動的リンク・ライブラリ何が違うのでしょうか? 実は良く分かりません(笑)。
大きな括りでは同じものと考えて良さそうです。例えば、CMakeは共有ライブラリも動的リンク・ライブラリもSharedと指定します。同じ用語を割り当ててますし。

さて、昨日と同じく私が今開発しているC++用のオート・シリアライザ(Theolizer)をMinGWとgccで共有ライブラリ化した時に見つけたノウハウのご紹介です。

まず、前提は昨日と同じく、共有ライブラリのメリットとして隠蔽を対象としています。そして、隠蔽は弱点となることがあるのでこれを避ける点も同じ考え方に基いてます。

ですので、昨日と同じく、自作ライブラリが静的リンクするライブラリを隠蔽し、しかし標準ライブラリは共有する前提です。

2.隠蔽について

MinGW(gcc)とmsvcでは共有ライブラリの隠蔽の考え方が正反対です。
msvcはデフォルト隠蔽で指定したものを公開します。
MinGWgccはデフォルト公開です。もしや隠蔽できないのでは!?と焦りましたが、ちゃんと方法は用意されてました。

2-1.シンボルの隠蔽方法

MinGW__declspec(dllexport)でシンボルをexportしますが、1つでもexportするとexportしていないシンボルが全て隠蔽されます。
この辺りの詳しいことが5.8. ld and WIN32 (cygwin/mingw)に記載されていました。

If, however, -export-all-symbols is not given explicitly on the command line, then the default auto-export behavior will be disabled if either of the following are true:
・A DEF file is used.
・Any symbol in any object file was marked with the __declspec(dllexport) attribute.

gccはコンパイルオプションで-fvisibility=hiddenを指定すると全て隠蔽でき、__attribute__((visibility ("default")))を指定したシンボルがexportされます。
この辺りの詳しいことはWhy is the new C++ visibility support so useful?に書かれています。

ただし、MinGWgccの両方とも「外部リンケージのない」シンボルをexportすることはできないようです。そのようなシンボルに__declspec(dllexport)__attribute__((visibility ("default")))を指定すると「外部リンケージが必要」エラーになります。


以下、dllの関数や変数をexe側で使えるようexportするために、下記のようなマクロが定義されているとし、かつ、gccは-fvisibility=hiddenを指定してビルドされているとして解説します。

dllのビルド時:  #define DLL_EXPORT  __declspec(dllexport) // MinGW
                #define DLL_EXPORT  __attribute__((visibility ("default"))) // gcc
exeのビルド時:  #define DLL_EXPORT

また、個人的にC++11に慣れているので、C++11基準でコードを書いています。

2-2.外部リンケージがないシンボルについて

外部リンケージがないとは、他のコンパイル単位(cppファイルとそのcppからインクルードされているヘッダ・ファイル)からアクセスできないことです。

ヘッダは複数のcppからインクルードするためちょっと混乱しやすいです。そこで少し説明させて下さい。
例えば下記のように定義した変数は定義した場所で生成されます。

const float pi=3.141592;

そのため、これをヘッダの中で定義し、そのヘッダを複数のcppからインクルードすると、各cpp毎にpiが定義されてしまいます。そして、このように「普通」に宣言した変数は「外部リンケージ」を持つため、そのコンパイル単位の「外」からもアクセスできます。
その結果「外」に同じ名前で複数の異なる変数が見えてしまい多重定義エラーになります。

これを防ぐために、定数の場合はstaticを付けて対処します。例えば、下記のように定義するわけです。

static const float pi=3.141592;

staticを付けることで「外部リンケージ」がなくなり、実体が複数あるにも関わらず「外」から見えないので多重定義エラーにならないのです。(他に無名namespaceでも外部リンケージをなくせますが、今回の記事には無関係なので割愛します。)

2-3.外部リンケージがあるstaticシンボルについて

staticの言語的な意味は静的です。
C言語ではローカル変数にstaticを付けることで静的な記憶領域に配置できます。(関数が終了してもその変数の値が保持されます。)
グローバル変数の場合は常に静的な記憶領域に記録されるため、グローバル変数にstaticを付けると外部リンケージを解除すると言う別の意味に流用されました。
関数内部で用いるローカル変数は元々外部リンケージを持たないため、問題ありませんでした。

ややこしいのはC++のクラス・メンバに指定するstaticです。こちらは外部リンケージを持ちます。

foo.h
struct Foo
{
    static int static_var;
};

と定義して、これをfoo.cppとbar.cppでインクルードします。皆さんご存知のようにFoo::static_varとしてアクセスする変数は全て同じものです。foo.cppで書き込んだ値をbar.cppで読み出せます。つまり「外部リンケージ」を持っています。

この記事を書くにあたって規格書(ドラフトn3337)にもあたってみました。9.4.2 Static data membersに確かに記載されてます。本の虫の江添亮氏がC++11の解説を書いてます。その9.8.2 staticデータメンバー(Static data members)に記載があり、規格書の日本語訳になってます。

名前空間スコープのクラスのstaticデータメンバーは、外部リンケージを持つ。ローカルクラスのstaticデータメンバーは、リンケージを持たない。

他のクラスの内部で定義されたクラス(ローカルクラス)のstaticメンバ変数と定数は外部リンケージを持たない。そして、最も外側で定義されたクラスのstaticメンバ変数と定数は外部リンケージを持つということです。

例えば、下記のように定義されている時、Foo::InnerClass::static_varとBar::InnerClass::static_varは別物です。そもそもFoo::InnerClassとBar::InnerClassは全く別のクラスですから当たり前ですね。

struct Foo
{
    struct InnerClass
    {
        static int static_var;
    };
};

struct Bar
{
    struct InnerClass
    {
        static int static_var;
    };
};

2-4.さて問題です

シングルトンを定義する時、staticを用いることは多いです。その際に大きく2種類の定義方法があります。シングルトン・オブジェクトの実体を、A)staticなメンバ変数でポインタを保持する方法と、B)staticなローカル変数で保持する方法です。下記に抽出したコードを記載しています。

このどちらかはクラス単位でexportすると不具合がでます。さて、それはどちらでしょうか?
ヒント(マウスを載せると表示されます)

メンバ変数版
// ヘッダ定義
class DLL_EXPORT SingletonA
{
    
    static SingletonA* mInstance;
public:
    static SingletonA& getInstance()
    {
        if (!mInstance) mInstance=new SingletonA();
        return *mInstance;
    }
    
};

// cppで定義
SingletonA* SingletonA::mInstance=nullptr;
ローカル変数版
// ヘッダ定義
class DLL_EXPORT SingletonB
{
    
public:
    static SingletonB& getInstance()
    {
        static SingletonB mInstance;
        return mInstance;
    }
    
};

私は、見やすいし楽なのでローカル変数版が好きです。

答え(マウスを載せると表示されます)

2-5.検証実験

まず、SingletonAの検証用コードの定義部です。(SingletonBもほぼ同じで、上記で示した部分のみの相違です。)

dll.h
// メンバ変数版
class DLL_EXPORT SingletonA
{
    int     mVal;
    SingletonA() : mVal(0) { }; // コピー/ムーブの禁止コードは省略
    ~SingletonA() { delete mInstance; }

    static SingletonA* mInstance;
public:
    static SingletonA& getInstance()
    {
        if (!mInstance) mInstance=new SingletonA();
        return *mInstance;
    }

    // ヘッダ内定義関数
    static void setVal(int iVal){ getInstance().mVal=iVal; }
    static int getVal()         { return getInstance().mVal; }

    // dll内定義関数
    static void setValDll(int iVal);
    static int  getValDll();
};

下記のmain.cppがexe側、sub.cppがdll側です。SingletonA部を抜粋してます。SingletonBのコードもmInstanceの定義が無いこと以外同じです。

main.cpp
    // SingletonAテスト
    SingletonA::setVal(10);
    SingletonA::setValDll(20);
    std::cout << "SingletonA::getVal()   =" << SingletonA::getVal() << "\n";
    std::cout << "SingletonA::getValDll()=" << SingletonA::getValDll() << "\n";
sub.cpp
// SingletonA
SingletonA* SingletonA::mInstance=nullptr;

void SingletonA::setValDll(int iVal)
{
    getInstance().mVal=iVal;
}

int  SingletonA::getValDll()
{
    return getInstance().mVal;
}

setVal()とgetVal()はヘッダで定義しているので、実体はexeとdllの両方に存在します。
setValDll()とgetValDll()はdll側で定義しているので、実体はdll側のみに存在します。
上記のように単純にsetVal()とgetVal()を呼び出しています。

もし、mInstanceがexeとdllに個別に存在していた場合、setVal()とgetVal()はそれを呼び出した側(exe or dll)のmInstanceを返却します。

さて、exe側(main.cpp)は下記処理を行っています。

  1. setVal()とgetVal()を直接呼び出し
  2. setValDll()とgetValDll()を呼び出して、dll経由でsetVal()とgetVal()を関節呼び出し

SingletonB側も全く同様に処理してます。

これらの結果を見ればmInstanceがdll側にのみ存在するのか、exeとdllの両方に個別に存在するのか判断できます。

実行結果です。(Windows 10のMinGW 5.4.0、ubuntu 16.04 LTCのgcc 5.4.0で確認してます。)

実行結果
1: SingletonA::getVal()   =20
1: SingletonA::getValDll()=20
1: SingletonB::getVal()   =10  ←これがまずい!! 他と同じ値になって欲しい。
1: SingletonB::getValDll()=20

う~ん、私の好みの方の負けですね。

結局、ローカル変数はstaticにしてもしなくても外部リンケージがありませんから、gccとMinGWはexportしてくれないということです。
もちろん静的リンクした時は、ローカル変数版mInstanceもちゃんと同じものになります。

つまり、悲しいかなgccとMinGWでは、共有ライブラリの場合と静的リンク・ライブラリの場合でstaticなローカル変数の振る舞いが異なります。要注意です。

CMakeなら共有リンクと静的リンクを手軽に切り替えることができます。
この実験で使ったソースとCMakeLists.txtとその使い方を最後に説明してますのでやってみて下さい。

2-6.対策

分かりさえすれば対策は簡単です。ローカル変数を定義するメンバ関数をヘッダで定義せず、dll側のcpp内で定義するだけです。
問題は、この対策を忘れないことです。

ローカル変数版
// dll.h
class DLL_EXPORT SingletonB
{
    
public:
    static SingletonB& getInstance();
    
};

// sub.cpp
SingletonB& SingletonB::getInstance()
{
    static SingletonB mInstance;
    return mInstance;
}

3.-fPICオプションについて

MinGWでは不要ですが、gccで共有ライブラリをビルドするには-fPICオプションを付けてコンパイルする必要が有ります。
32bitビルドの時は必須ではないけどいろいろメリットがあるようです。
64bitビルドの時は必須のようです。

詳しくは-fPICオプションについて調査で説明していますので、そちらを参照して下さい。

4.thread_local領域について

thread_localなグローバル変数を定義したいことがあります。(errnoのような変数はスレッド毎にユニークなグローバル変数にする必要があります。そのような時ですね。)

msvcはthread_local領域をexportできませんでした。
なので、gccとMinGWはどうなのかちょっと気になりましたので、調べたところ大丈夫でした。
特にコンパイル・エラーも出ず、dll側だけで定義されました。
(もちろん、外部リンケージがない形式で使った時は、他と同じく共有されない筈です。)

5.実験ソースのビルド方法(using CMake)

以上の実験で使ったソースとCMakeLists.txtをGistへ上げてます。

CMake 3.5.0以上をインストールしパスを通しておいて下さい。(ここで手短にCMakeのインストール方法を書いてます。)

上記Gistからダウンロードして解凍し、下記操作を行うことでビルドと実行できます。

MinGWの場合*
Windows 10上でMinGW 5.4.0で確認しています。また、MinGWにパスを通しておいて下さい。

mkdir build
cd build
cmake -G "MinGW Makefiles" ..
cmake --build .
main.exe

gccの場合
ubuntu 16.04 LTC上でgcc 5.4.0で確認しています。

mkdir build
cd build
cmake -G "MinGW Makefiles" ..
cmake --build .
./main

上記コマンドは極限までオプション指定を省略してます。CMakeは超便利ですが、それに応じて使い方も難しいので何か分からない時は当記事のコメントで尋ねて下さい。できるだけ回答します。

6.バージョンの相違問題について

dllのみ更新した時、問題を起こさないようにするにはどうすればよいのか?
本当に悩ましいです。でも、良い解は見つかっていません。解について私の見解を昨日の記事に書いてます。

7.まとめ

dll化する際、見落としがちで問題になりやすいのはメモリ問題でした。
msvcのまとめとほぼ同じですね。

  1. ヒープ等のリソースを管理する同じライブラリの実体がexeとdllに分かれると辛い
    貸出元へきちんと返却する必要があり、リソース管理がたいへん難しくなります。
    exeとdll間でやり取りするリソースを管理するライブラリ(標準ライブラリ含む)は、exeとdllで同じものを動的リンクするのが望ましいです。

  2. static変数やグローバル変数はデータ・セグメントに記録されますが、これらは要注意
    油断するとexeとdllの両方に領域が獲得されてしまいます。注意深くexportすることで避ける必要があります。
    できるだけクラス単位でexportすることが望ましいと思います。
    そして、staticなローカル変数はexportできません。そのような定義をする時、その関数はヘッダではなく、dll側のcppで定義しましょう。

8.さいごに

こちらはmsvcより随分楽な筈と思っていたのですが、結局、大きな記事になってしまいました。
staticと外部リンケージ周りが頭痛かったです。調べれば調べるほど、色々でてきましたので。
MinGWとgccもstaticなローカル変数をexportしてくれれば良いのになと思います。

さて、今回、これを纏めるにあたって追加調査し裏も取りましたが、まだ何か勘違いや見落としがあるかも知れません。
気がついた方は是非コメント下さい。