ライブラリとバイナリ互換性
大規模なソフトウェアになってくると複数の機能を再利用したり、プラグインなどを作るためにライブラリを使うようになってきます。
それを静的リンクまたは動的リンクすればプログラムから機能を利用できます。
しかし、ただライブラリを作るだけではバージョンアップした場合などにバイナリ互換性の問題が発生してしまうことがあります。
ここではバイナリ互換性が何かということと、その解決策について紹介したいと思います。
バイナリ互換性の説明
バイナリ互換性とは大雑把にいうとライブラリとそれをリンクしたプログラムの間に互換性があるかどうかということです。
例えば、皆さんがプラグインを作りたくて次のようなライブラリクラスを作ったとします。
Plugin.hclass Plugin { public: Plugin(); ~Plugin(); private: int version; };
ソースファイルは省略します。これがライブラリのバージョン0です。
そして、これを別のプログラムにリンク(ここでは静的リンク)して利用します。
main.cpp#include <iostream> using namespace std; /**Pluginからプラグインを作る。*/ class MyPlugin : public Plugin { public: MyPlugin() : version(0){} ~MyPlugin(){} void printVersion() { cout << "MyPlugin version : " << version << endl; } private: int myVersion; }; int main() { MyPlugin plugin; plugin.printVersion(); return 0; }
Pluginを元にMyPluginという派生クラスを作っています。
もちろんこれは何の問題もなく動きます。
次にもっと機能を付けたくて次のようにライブラリを改造したとします。
Plugin.h#include <string> class Plugin { public: Plugin(); ~Plugin(); private: int version; std::string author; };
これがPluginのバージョン1になります。
バージョン1ではPluginにauthorという変数が追加されました。
ここでバージョン1のライブラリを作り、それを利用元のプログラムに再リンクしたらどうなるでしょうか?
なぜかエラーを吐いてクラッシュしてしまいます(main.cppも再コンパイルした場合はクラッシュしません)。
なぜこうなるかはMyPluginのメモリレイアウトを考えれば分かります。
下の図がPlugin中でのMyPluginのメモリレイアウトです([]内が変数のメモリ上のオフセット値)。
バージョンアップ前はmyVersionという変数のオフセット値は1でした。
それがバージョンアップしたせいでオフセット値が1つ増え、2になってしまいます。
しかし、ライブラリをリンクしている側ではオフセット値は1のままと考えているのでauthorにアクセスしてしまいます。
これがクラッシュしてしまう仕組みです。
では、どうすればバイナリ互換性を保ってライブラリを更新できるかというとPimplというイディオムを使います。
解決法(Pimpl)
Pimplはもともとはプライベートの実装を隠ぺいするために使われているイディオムです。
バイナリ互換性を保つためにPimplを使う理由は、変数を1つのクラスにまとめておき、後から内容を変更してもオフセット値が狂わない利点があるからです。
どういうことかを先ほどのPluginクラスを例に説明していきます。
まずはヘッダファイルです。
Plugin.hclass Impl; class Plugin { public: Plugin(); ~Plugin(); private: Impl * im; };
Implというクラスが前方宣言されていますが、このクラス内でプライベートなメンバを1まとめにして実装を隠ぺいすることができます。
そのImplクラスを
Impl.h#include <string> class Impl { public: int version; std::string author; };
このように変数を1つのクラス内にまとめて書くとあとからいくらでも変更可能です。
あとはPluginのソースでImplを実体化します。
Plugin.cpp#include "Plugin.h" #include "Impl.h" Plugin::Plugin() : im(new Impl) {} Plugin::~Plugin() { delete im; }
あとはImplを通してプライベートなメンバに好きにアクセスできます。
これをライブラリとしてコンパイルすればバージョンアップしたとしてもクラッシュすることはなくなります。
以上がPimplを使ったバイナリ互換性の保ち方です。
せっかく作ったライブラリがバージョンアップしただけでクラッシュすることがないように積極的にPimplを使うようにしたいですね。
では、また!