...ing logging 4.0

はてなブログに移行しました。D言語の話とかいろいろ。

D言語でMicrosoft Component Object Model (COM) を使う (その13)

D言語 Advent Calendar 2025 17日目の記事です。

その1でCOMインタフェースを使うために必要な宣言や定義の準備ができて、 その2でCOMの初期化と解放ができるようになりました。 その3では実際にファイルを開くダイアログを表示することができ、その4では選択したファイルのパスを取得できました。 その5ではQueryInterfaceを使って別のCOMインタフェースを取得して、ファイル作成日時を取得できました。 その6ではCOMインタフェースをアップキャストもダウンキャストもできないことを説明しました。

その7ではCOMオブジェクトとCOMインタフェースを自作するための準備をしました。 その8とその9では自作COMファクトリをCOMサーバーとして登録し、 自作COMオブジェクトと自作COMインタフェースを作成できました。 その10では既存のCOMインタフェースを拡張した新しいCOMインタフェースとCOMオブジェクトを作成しました。 その11では、COMファクトリクラスを拡張して、新しいCOMインタフェースを使えました。

その12では、AddRefメソッドとReleaseメソッドを自分で呼ばなくていいように ComPtrというスマートポインタを作ることにしました。

ComPtrのひな形

その12の記事で掲載したComPtrのひな形を再掲します。

struct ComPtr(BaseType : IUnknown)
{
// ...
private:
    BaseType _comObj;
}

使い方は、こんな感じを目指します。

ComPtr!IFilePicker2 pPicker;
hr = pFactory.CreateInstance(null, &IID_IFilePicker2, cast(void**)&pPicker);

ComPtrの基本原理は、ComPtrオブジェクトのコピーが行われるときに介入して、 AddRefメソッドを呼び出せばいいわけです。 また、オブジェクトが破棄されるときには ~this()デストラクタでReleaseメソッドを呼び出せばいいわけです。

これは構造体が得意とするところなので、クラスではなく構造体でComPtrを作ることになります。

コピーされるとき

コピーが行われるときに介入する方法は、次の3つがあります。

まずは、これらの挙動を調べたいので、上の3つを定義したTester構造体を作りました。 わざと引数や戻り値の型にはrefを付けていません。

struct Tester
{
    this(int id) // 初期化(インスタンスを区別するためのidを与えて生成するためのコンストラクタ)
    {
        writeln("this(int id) : ", id);
        this.id = id;
    }
    this(Tester comPtr) // コピーコンストラクタ?
    {
        writeln("this(Tester comPtr) : ", id);
    }
    this(this) // ビットコピーされた後に呼ばれるpostblit
    {
        writeln("postblit : ", id);
    }
    Tester opAssign(Tester comPtr) // 代入演算子オーバーロード
    {
        writeln("Tester opAssign(Tester comPtr) : ", id);
        return this;
    }
    ~this() // デストラクタ
    {
        writeln("~this : ", id);
    }
private:
    int id;
}

これを使って、次のとおり書いて実行してみます。

void f(Tester x) {}
Tester g(int id)
{
    return Tester(id);
}
void main()
{
    writeln("-- Sec.1 --");
    {
        Tester p = Tester(1); // 初期化
    }
    writeln("-- Sec.2 --");
    {
        Tester p = Tester(2); // 初期化
        f(p); // 初期化
    }
    writeln("-- Sec.3 --");
    {
        Tester p5 = Tester(31); // 初期化
        Tester p6 = Tester(32); // 初期化
        p5 = p6; // 代入
    }
    writeln("-- Sec.4 --");
    {
        f(Tester(4)); // 初期化
    }
    writeln("-- Sec.5 --");
    {
        Tester p = g(5); // 初期化
    }
    writeln("-- Sec.6 --");
    {
        Tester p;
        p = g(5); // 代入
    }
}

個別に見ていきます。

セクション1

writeln("-- S.1 --");
{
    Tester p = Tester(1); // 初期化
}
-- Sec.1 --
this(int id) : 1
~this : 1

コンストラクタの引数でidを1に指定してpを初期化すると、 Tester(int id)コンストラクタが呼ばれた後、 pが破棄されるときにデストラクタが呼ばれています。

ほかには何もトラップできていません。

コンストラクタが1回、デストラクタが1回でした。

セクション2

writeln("-- Sec.2 --");
{
    Tester p = Tester(2); // 初期化
    f(p); // 初期化
}
-- Sec.2 --
this(int id) : 2
postblit : 2
~this : 2
~this : 2

セクション1と同様にidを2に指定してpを初期化しています。

次に、f関数にpを与えて呼び出したときに、postblitが実行されています。 f関数の引数xが値型なので、xがpで初期化されるときに、 pからxへのビットコピー(blit)が行われ、その後にpostblitが走っています。

そして、f関数から呼び出し元に戻るときに、xが破棄されてデストラクタが呼ばれ、 セクション2ブロックを抜けるときにpが破棄されてデストラクタが呼ばれています。

コンストラクタとpostblitを合わせて2回、デストラクタが2回でした。

セクション3

writeln("-- Sec.3 --");
{
    Tester p5 = Tester(31); // 初期化
    Tester p6 = Tester(32); // 初期化
    p5 = p6; // 代入
}
-- Sec.3 --
this(int id) : 31
this(int id) : 32
postblit : 32
Tester opAssign(Tester comPtr) : 31
postblit : 31
~this : 32
~this : 31
~this : 32
~this : 31

少しずつ読み解きます。

this(int id) : 31
this(int id) : 32

最初に、idが31と32のオブジェクトが作られています。

その後、p6をp5に代入するときに色々起こっていますので、順に見ていきます。

postblit : 32

代入演算子オーバーロードの引数comPtrが値型なので、 idが32のオブジェクトがcomPtrへビットコピーされ、postblitが実行されています。

Tester opAssign(Tester comPtr) : 31

それから、idが31のオブジェクトの代入演算子オーバーロードの中身が実行されています。

postblit : 31

続いて、idが31のオブジェクトが代入演算子オーバーロードの戻り値が値型なので、 ここでもビットコピーされ、postblitされています。

~this : 32

次に、代入演算子オーバーロードから抜けるときに引数のcomPtrが破棄されるので、 idが32のオブジェクトのデストラクタが1回呼ばれています。

~this : 31

さらに、代入演算子オーバーロードの戻り値のオブジェクトが破棄されるので、 idが31のオブジェクトのデストラクタが1回呼ばれています。

~this : 32
~this : 31

最後に、最初のidが32と31のオブジェクトが生成と逆順に破棄されるので デストラクタが合計2回呼ばれています。

コンストラクタとpostblitと代入演算子オーバーロードを合わせて5回、デストラクタが4回でした。

うーん、これだと回数が一致しませんね。

セクション4

writeln("-- Sec.4 --");
{
    f(Tester(4)); // 初期化
}
-- Sec.4 --
this(int id) : 4
~this : 4

idが4のオブジェクトが作成されてから、すぐにデストラクタが呼び出されています。

関数呼び出しをしていますが、セクション1と同じ結果になっています。

Tester(4)で一時オブジェクトが作られるかと思いきや、関数の引数を初期化するだけでした。

コンストラクタが1回、デストラクタが1回でした。

セクション5

writeln("-- Sec.5 --");
{
    Tester p = g(5); // 初期化
}
-- Sec.5 --
this(int id) : 5
~this : 5

これもセクション1、4と同じ結果です。

g関数の中でidが5のオブジェクトが作られてから、 呼び出し元のpを初期化しているだけでした。

コンストラクタが1回、デストラクタが1回でした。

セクション6

writeln("-- Sec.6 --");
{
    Tester p;
    p = g(6); // 代入
}
-- Sec.6 --
this(int id) : 6
Tester opAssign(Tester comPtr) : 0
postblit : 0
~this : 6
~this : 0
~this : 0

シンプルなコードに対して、裏で起こることはかなり複雑です。

まず、Tester p;では何もトラップしていません。 pのidは0のままです。

this(int id) : 6

次に、g関数の中で、idが6のオブジェクトが構築され、戻り値になります。

それから、idが6の戻り値オブジェクトが、 代入演算子オーバーロードの引数comPtrにコピーされそうですが、 ここではビットコピーもpostblitもされていません。*1

Tester opAssign(Tester comPtr) : 0

idが0のオブジェクトpの代入演算子オーバーロードが呼ばれています。 pは構築済みのオブジェクトなので、初期化ではなく代入になっています。 ここで出力しているidは、comPtrのidではなく、左辺のpのidです。 pのidは未初期化でしたから0になっています。

postblit : 0

演算子オーバーロードの戻り値が値型なので、 戻り値の一時オブジェクトが構築され、 thisがビットコピーされた後にpostblitされています。

~this : 6

g関数の戻り値の一時オブジェクトが破棄され、デストラクタが呼ばれています。

~this : 0

演算子オーバーロードの戻り値の一時オブジェクトが破棄され、デストラクタが呼ばれています。

~this : 0

最後は、pが破棄されるときにデストラクタが呼び出されています。

コンストラクタと代入演算子オーバーロードとpostblitを合わせて3回、デストラクタが3回でした。

このままでは規則性がなさそう

構築と破棄の回数が揃っていないと、参照カウンタがうまく増減しません。

どうも値型を受け渡しているのが問題ぽいです。

次の記事で試行錯誤します。

*1:内部的にはムーブが起こっていそうです。