dllを分割するとき、ネイティブでunsafeなC/C++のポインタ型を扱うプロパティとかは、やっぱり実際のところvoid*的なノリ
時々ちょっと面白いCLR。まずこんな感じで、C++/CLRのクラスdllを作ります。(VC2010)
#pragma once public class TestHoge {}; public interface class ITestHoge { property TestHoge* Unsafe { TestHoge* get(); void set(TestHoge* value); } };
ではこのDLLをリンクして使う時(ITestHogeインターフェースを実装するクラスをこのdllの外側で作る時)、TestHogeクラスの定義はどうしたらええんでしょう。
ネイティブのC/C++クラスの型情報って、.NetなDLLとして公開できないんじゃないの?
そこはもうタイトルにほとんど正解書いちゃってますが、参照先のプロジェクトにも同じヘッダをインクルードするなりして、再定義しなきゃいけません。ただ良くも悪くも実際ただのvoid*なので、言っちゃえば元のクラスの実装を無視して好き放題に再定義もできます。
新しい実行ファイルプロジェクトを作り、さっき作ったDLLを参照に追加して、こんなコードを書きます。
// 上のプロジェクトとは同じTestHogeの名前で違う実装のTestHogeクラスを記述できる class TestHoge { public: int i; void Sub() { System::Console::WriteLine("hoge"); } }; ref class TestHogeImpl : ITestHoge { public: TestHogeImpl() : m_unsafe(NULL) {} // プロパティを実装します。少なくともこっちのプロジェクト内ではSub()の呼び出せるクラス型として扱う事が出来ます。 virtual property TestHoge* Unsafe { TestHoge* get() { return m_unsafe; } void set(TestHoge* value) { m_unsafe = value; } } private: TestHoge* m_unsafe; }; int main(int argc, char* argv[]) { TestHogeImpl^ obj = gcnew TestHogeImpl(); obj->Unsafe = new TestHoge(); obj->Unsafe->Sub(); System::Console::Read(); return 0; }
こういう使い方であれば、実装が違ってもTestHogeなら…要は名前空間を含めて型名さえ一致させりゃ良いわけです。
ちなみに元々のTestHogeは、ILSpyでdllの中身を眺めてみると、c#なstructとして型情報が格納されているので、例えpublicに記述しても、ネイティブポインタにセットしたり、ネイティブポインタとしてアクセスできる状態にはないように見えます。*1 *2(なお「いっそclass TestHoge;って具合に前方宣言だけでインターフェースDLLをコンパイルできねーかな!」と思ったらさすがにそれは許してもらえませんでした。)
冷静に考えてみるとネイティブC/C++では、良くも悪くもポインタはポインタ。そのプロジェクト内で知る限りの静的型情報を元にして、ただのアドレスに対するオフセット処理に置換されるだけって感じですね。
もちろん元のDLLプロジェクト側でも、インターフェース定義するだけでなく、なんらかの処理をしたりするんなら、やはりちゃんとどっちでも使えるようにヘッダやソースを書いて、両方のプロジェクトに参加させてあげるのがスジでしょうけど、それはそれ、どっちのDLLや実行ファイルにも同じバイナリがダブって入っちゃうのでちょっとうれしくないのですが、まぁ仕方のないところでしょうか。
*1:ちょっと違ってるっぽいのですがまた面白い部分だったので後日追加調査の記事を書きます。
*2:↑と思いきやうまく説明できない状況に陥り、まとまらなかったのでこのネタはここまでにしときます。基本的にはC#だけでも「名前だけダミー定義しちゃうC++ネイティブクラスモドキ」とか書けるようなんですが、簡単なサンプルではうまく行きますが実際今作ってるメタセコのプラグインに導入してみるとエラー吐いたりるんですね。なんなんだろう…プラグインだからデバッグ/例外情報を追いかけていくのがメタセコ経由になっちゃって難しいところがアッタリナカッタリ。別の要因のような気もするんですが、時間がないのでまた今度…(いつになるやら)