クラスをエクスポートするDLL

[戻る]
亀山 2009/11/07(土) 13:51:43
クラスをエクスポートするDLLについて質問させていただきます。

__declspec(dllexport)を使って通常のクラスをエクスポートすると、
メンバ変数も含まれるため、それらの追加や削除があるたびに
EXE側もビルドし直さなければいけないということは理解しています。

また、その問題の解決案として、
純粋仮想関数のみのクラスをエクスポートしておき、
DLL内部でメンバも持たせた派生クラスを作成するという方法を
ネット上でいくつか読みました。

そこで疑問点があるのですが、この方法の場合、
エクスポートクラスに純粋仮想関数の追加があったときも
EXE側のリビルドは必要無いものなのでしょうか?

Ban 2009/11/07(土) 14:09:43
> そこで疑問点があるのですが、この方法の場合、
> エクスポートクラスに純粋仮想関数の追加があったときも
> EXE側のリビルドは必要無いものなのでしょうか?

否。その場合はExe側にも影響が波及するのでリビルドが必要です。
実装的にvtblが変わるでしょうし、
意味的にも、そもそも純粋仮想関数が増えたってことはつまり、
「公開してるインタフェースが変更された」ってことですよね。

ロマ 2009/11/07(土) 21:02:20
> __declspec(dllexport)を使って通常のクラスをエクスポートすると、
> メンバ変数も含まれるため、それらの追加や削除があるたびに
> EXE側もビルドし直さなければいけないということは理解しています。

コンストラクタをprivateにして T* Create()friend/static関数をexportするか、
変数全体を構造体にしてポインタを持つようにすれば
再ビルド不要と思うんですが。
興味あるので、どなたかよろしく。

> 純粋仮想関数のみのクラスをエクスポートしておき、
実体が無い場合、exportできないと思います。

FlatWest 2009/11/07(土) 22:12:22
pImplイディオムを使って、インターフェース側(エクスポートする部分)と
実装側を分けてやればEXEのリビルドは不要になる。

・・・って、これじゃ話が変わってくるかな。^^;

wclrp ( 'o') 2009/11/08(日) 01:08:30
俺が行うとしたら
インターフェース(public純粋仮想関数のみのクラス)を定義している
ヘッダファイルを公開する。
このインターフェースの派生クラスのインスタンス生成し
これをインターフェースへのポインタとして返す関数のエクスポートだな。
deleteはDLL内で生成したメモリなので同DLL内でdeleteが呼ばれるようにする。

インターフェースに関数が増えた場合
最後に追加するだけなら既存部分の順番は
おそらく変わらないだろうけど気になるなあ。

struct hoge { int a,b,c; };みたいなのは
ちゃんとa,b,cの順になることらしいけど。

exeのリビルドが必要と考えるべきだな。

Ban 2009/11/08(日) 07:07:03
「純粋仮想関数のみのクラスをエクスポートしておき、…」
「エクスポートクラスに純粋仮想関数の追加があったとき」とあるので、
pImplとか関係なく、Exeに公開してるクラスにメンバ関数を追加すれば、
それはまぁリビルドが必要だろうと。

# defで序数固定してvtblの互換性を維持して…とかいう曲芸は
# できうるのかもしれませんが…I/Fを変更したら素直にリビルドするか、
# (COMとかのように)別クラスとして公開するべきかと。

Ban 2009/11/08(日) 07:36:32
wclrp ( 'o') さんに概ね同意。私も似たようなことをするかCOMにでもします。

> struct hoge { int a,b,c; };みたいなのは
> ちゃんとa,b,cの順になることらしいけど。

この例では全部public:(アクセス指定が変わっていない)なので
「非静的なデータメンバ変数(Nonstatic data members)のアドレス配置順」は
確かに保証されてますけど、言語仕様にはvtblなんて規定はないですし、
勿論メンバ関数の配置には保証がありません…。

Ban 2009/11/08(日) 07:53:09
> コンストラクタをprivateにして T* Create()friend/static関数をexportするか、
> 変数全体を構造体にしてポインタを持つようにすれば再ビルド不要と思うんですが。

こんなのを定義して、
class __declspec(dllexport) Foo
{
private:
    Foo(){}
    Foo(const Foo&);
    Foo& operator =(const Foo&);
public:
    virtual ~Foo(){}
    static Foo* Create();
    static Destroy(const Foo*); // Dllで確保したメモリをExeで開放すると、
                // 実行時環境が異なってたら痛いので…。
};

実装側で、
class FooImpl : public Foo
{};
Foo* Foo::Create() { return new FooImpl(); }
みたいなの(もしくはFooImplをFooから派生せず、ポインタとしてFooに持たせるpImpl)を
イメージしてると思うのですが、この場合、
*Fooは変更せずに* FooImplだけ変更するなら確かにリビルドは不要になります。
ただ、質問者さんが聞いているような、
「エクスポートクラスに純粋仮想関数の追加があったとき」(Fooを変更)だと、
一般論でいえば、リビルドが必要だと思います。
# 相当注意を払えば実装上大丈夫とかはありえるかもしれませんが。


> 実体が無い場合、exportできないと思います。
インスタンス化できない純粋仮想なクラスでも__declspec(dllexport)は可能です。
(C++によるinterface実装の典型的なパターンかと思います)

ロマ 2009/11/08(日) 13:03:39
> __declspec(dllexport)を使って通常のクラスをエクスポートすると、
> メンバ変数も含まれるため、それらの追加や削除があるたびに
> EXE側もビルドし直さなければいけないということは理解しています。

うまくやれば再ビルドが不要になるのでは、と思い

>コンストラクタをprivateにして T* Create()friend/static関数をexportするか
この方法は、EXE側が実体をもてないようにする思いつきです。
class __declspec(dllexport) Foo
{
private:
    Foo(){}
    Foo(const Foo&);
    Foo& operator =(const Foo&);
    ~Foo(){}        // こっちに移動し、virtualをはずしました
public:
    static Foo* Create();
    void Destroy(); //非スタティックメンバーに変更しました
  bool Do();      
//    bool Do_ex(); //将来追加する
private:
  int x;
  int y;
//  int z;        //将来追加する
};
Do_ex()やzを追加した時にEXEの再ビルドは必要ですか。


> 変数全体を構造体にしてポインタを持つようにすれば再ビルド不要と思うんですが。
これはオブジェクトのサイズを変えないようにする思い付きです。
class __declspec(dllexport) Foo
{
public:
    Foo() : p(new Data){}
    Foo(const Foo&);
    Foo& operator =(const Foo&);
    ~Foo(){ delete p; } 
  bool Do();      
//    bool Do_ex(); //将来追加する
private:
  Data* p;
};
  
struct Data
{
  int x;
  int y;
//  int z;        //将来追加する
};
このやり方もまずいですか。

> インスタンス化できない純粋仮想なクラスでも__declspec(dllexport)は可能です。
ありがとうございます。調べてみます。

亀山 2009/11/09(月) 10:28:00
みなさん多くのご意見ありがとうございます。
ロマさんの提案されている方法は自分もぜひご意見いただければと思いますが、
以下の部分について確認させていただきますでしょうか。

関数のみをエクスポートするDLLのときには、
関数が増えたときも、defファイルに追記していくぶんには、
過去にビルドしたEXEは通常そのまま使えますよね。

> Exeに公開してるクラスにメンバ関数を追加すれば、
> それはまぁリビルドが必要だろうと

そうなると、wclrp ( 'o')さんの言われているような

> インターフェース(public純粋仮想関数のみのクラス)を定義している
> ヘッダファイルを公開する。

の方法(自分がネット上で読んだものもこのような方法でした)は、

「private部分に変更があっただけのときはリビルドの必要が無くなる」

というメリットがメインで、

「public純粋仮想関数に追加があったときは、
過去にビルドしたEXEがそれを使わないままでもリビルドは必要になる」

ということになるのでしょうか?

仲澤@失業者 2009/11/09(月) 11:52:56
要は、エクスポートされたvtblに変更があれば、それをインポートする側は
再コンパイルが必要だと考えます。

任意のDLLからエクスポートされた任意のクラスのvirtualなメンバ、
コンストラクタ、又はデストラクタが変更された場合、そのクラスの
vtblは変更されている可能性があります。

つまり、あるvirtualメンバ関数に変更があったDLLについて、それを参照している
実行ファイルの再コンパイルが必要ないと確信が持てるのは、
そのメンバ関数が単独でエクスポートされている場合に限られると
考えられます。また、そのメンバ関数は、インライン展開されない
と確実に言える場合のみ、との条件が付くと考えられます。
自分は、後から追加されたvirtualなメンバが、元のvtblに影響の出ない
部分に追加されるとの確信を持てません(vv;)。

>そこで疑問点があるのですが、この方法の場合、
>エクスポートクラスに純粋仮想関数の追加があったときも
>EXE側のリビルドは必要無いものなのでしょうか?

以上の私見から、必要だと考えます。

>「private部分に変更があっただけのときはリビルドの必要が無くなる」

もちろんprivateなメンバからpublicでvirtualなメンバが
呼び出される可能性は排除できないので、public protected private
に関係なく、一般的には参照側の再コンパイルが必要だと考えざるを得ません。

Ban 2009/11/11(水) 08:54:48
> 「public純粋仮想関数に追加があったときは、
> 過去にビルドしたEXEがそれを使わないままでもリビルドは必要になる」

一般論でいれば、そうなると思います。
どうしてもリビルドしたくないなら、*以前のクラスはそのままに*
変更した別のクラスも別途提供する、という方が一般論では、安全かと。

# COMの仕組みを調べてみると参考になるのではないでしょうか。

PATIO 2009/11/11(水) 10:01:17
他の皆さんが言われているように基本的にクラスでインターフェイスを
とるDLLで実行ファイル側のリビルドが不要なケースは極限られたケースで
基本的にはリビルドが必要と考えるべきだと思います。
実行ファイルのリビルド不要に重きを置きすぎると返って良くないように感じます。

亀山 2009/11/12(木) 10:51:44
[[解決]]
仲澤@失業者さん、Banさん、PATIOさん、
引き続きご意見ありがとうございます。

> 他の皆さんが言われているように基本的にクラスでインターフェイスを
> とるDLLで実行ファイル側のリビルドが不要なケースは極限られたケースで
> 基本的にはリビルドが必要と考えるべきだと思います。

ネットなどでよく見かける、
純粋仮想クラスをエクスポートするDLLを作る方法のメリットは、
「極限られたケース」のみということになるのですね。

となると、公開するメンバ関数を追加したりしない限りは、
private部分に変更があることもあまり無いでしょうし、
通常のクラスのままDLLにしてしまっても、
リビルドの機会は大して変わらないような気がします。

済マークは付けさせていただきますが、勘違いしている部分があれば、
引き続きアドバイスいただけると嬉しいです。

2009/11/12(木) 11:33:39
クラスをエクスポートという時点でEXEをVCで作る縛りになる。ネットでよく見かけるとしても、限られた話でのことになるね。
多分、あなたのいうエクスポートと俺のいうエクスポートは違う意味で使っているのでしょうけど。

インターフェースが変わらないならリビルド不要
そうじゃないとこの世にある大量のプラグインを頻繁にバージョンアップしないといけなくなる。

2009/11/12(木) 12:01:28
あなたにとってメリットがビルドしかないならそうでしょう。
LoadRibrary使う可能性あるならクラスのエクスポートが使えないから選択の余地なし

ITO 2009/11/12(木) 13:28:09
 皆さんは、なぜそこまで亀山さんが、ビルドをしないのにこだわっている理由が
分からないみたいです。
僕も分からないですね。
 ただ、両方ビルドをするのが大変だからという理由だけではないみたいですね。
その理由がわかるとより適切なアドバイスが出来ると思います。

PATIO 2009/11/12(木) 16:09:21
あさんも書かれていますけれど、

プラグインのような物であれば、実行ファイル側はリビルド不要になります。
ただし、実行ファイル側が知らないインターフェイスは実行ファイル側からは
見えませんし、使えません。既存のインターフェイスを変える事は許されません。
変えない事を条件にした設計になりますからね。
リリース済みの実行ファイルからは使えないけれど、
そのインターフェイスに合わせた実行ファイルからは見えるし使えると言う話なら
ありえますね。

なんにしてもライブラリ側が全く影響を受けないのは、
インターフェイスに変化がなく、DLL側の挙動の変化が実行フィル側に影響ない場合
だけの話なので、大幅な改修の場合は実行ファイル側のビルド無しにと言うのは
ちょっと難しいと思います。インターフェイスは設計済みで実装が後になっている
というケースならありえるかもしれませんけれど、実行ファイル側が全く新しい
インターフェイスを利用する為には、実行ファイル側の改修が必要になりますからね。

亀山 2009/11/15(日) 21:22:29
[[解決]]
あさん、ITOさん、PATIOさん、ご意見ありがとうございます。

プラグインの仕組みについては、それなりに理解しているつもりです。
この手のものは、クラスではなく
DEFファイルを使うような通常の関数群になっていて、
そのためDLL側にインターフェイス(関数)を増やしても
過去の関数に手を加えない限りはEXE側のリビルドは不要なのですよね。

こういう仕組みをクラスにも使えないのかなと思っていたときに、
純粋仮想関数を使ったクラスのエクスポートに関する内容を読んだため、
これならできるのかなと思って質問させていただきました。

クラスは基本的にインターフェイス(メンバ関数)を増やしたときにも
リビルドが必要になるということで理解しました。
ありがとうございます。

Ban 2009/11/16(月) 00:22:01
> 過去の関数に手を加えない限りはEXE側のリビルドは不要なのですよね。

クラスでも「過去のクラスに手を加えない限り」Exe側のリビルド不要となれば、
単純に、粒度を関数個別からクラスという単位に変更しているのですから、
関数レベルで過去の関数に手を加えない場合の影響範囲と比較して、
格段にひどくなったという印象は(私には)ありません。
関数レベルで引数を追加したときにリビルドが必要なのと大差ないというか…。


実際、
> クラスをエクスポートという時点でEXEをVCで作る縛りになる。
ですし、またVC間でも、inlnine展開されたSTLクラスの設定/バージョンが
合わないだけで互換性ないですし…。
ABI意識して汎用性にこだわるなら__declspecはナイと思います。


> こういう仕組みをクラスにも使えないのかなと思っていたときに、
> 純粋仮想関数を使ったクラスのエクスポートに関する内容を読んだため、
> これならできるのかなと思って質問させていただきました。

既述ですが、COMの考え方は「こういう仕組み」をクラスにまで拡張してると思います。
制限もありますが、ああいう形へと向かうのは一案でしょうか。
・「過去の関数に手を加えない」⇒
・過去のクラスに手を加えない⇒
・別インタフェースとして公開する
# 個人的には、クラスにしない方が大抵お手軽だと思いますが。

そもそも、クライアント側に(コードレベルで)埋め込まれたクラスの構成まで
影響を受けるような追加を、DLL側だけでやろうというのは無茶な話。
vtblのサイズが変わったクラスのバイナリから変わる前のバイナリを参照したり
することになると、関数レベルで引数の数や型を変えて互換性を維持するような
芸当と大差ない理解です。一般論では、そうならない範囲でしか切り替えられない。

亀山 2009/11/18(水) 21:13:52
[[解決]]
Banさん、ご意見ありがとうございます。

> 関数レベルで引数を追加したときにリビルドが必要なのと大差ないというか…。

たしかに昔、引数を追加したばかりに、
リビルドし忘れたEXEに影響が及んで正しく動かなかったことがあります。

今回はソリューションをしっかり構成し、
EXE側のリビルドし忘れが起きないようにした状態で、
クラスをそのままエクスポートするようにしようと思います。

# 恥ずかしながら、以前他部署から引き継いだ開発環境が、
# 同じフォルダに置かれるEXEやDLL一つ一つが別ソリューションになっていて、
# そのくせ一つのDLLをいろんなEXEがリンクして、
# このような問題が特にクラスDLLで多発したことがありました。

COMについても、勉強させていただきます。
ありがとうございます。

古いスレッドには追加発言できません。