👷🏽

Native AOT で COM の自作と使用

に公開

はじめに

COM コンポーネントを自作した際、わざわざシステムに登録して CoCreateInstance() するよりも、new したインスタンスを渡すほうが手軽です。

C# で Native AOT(以降「AOT」)ビルドの場合、COM の実装方法もインスタンス化の方法も非 AOT(JIT)とは異なるので、やり方を整理しておきます。

サンプルプログラムのソースコードは GitHub に上げてあります。

要点

COM の自作と使用に関するポイントは大きく 3 つあります。

  • インターフェースの宣言時に [GeneratedComInterface] 属性を付ける。
  • 自作 COM の実装時に [GeneratedComClass] 属性を付ける。
  • 自作 COM の使用時に ComWrappers を使う。

例として、IStream インターフェイスを実装する COM を自作します。

IStream は IWICBitmapEncoderで画像を保存する際などに使用します(もちろん通常は自作する必要はありません)。

前提として P/Invoke は CsWin32 で行っています。

インターフェースの宣言

自作 COM の実装元となるインターフェースを宣言します。

IStream.cs
[GeneratedComInterface]
[Guid("0000000C-0000-0000-C000-000000000046")]
public partial interface IStream
{
  [PreserveSig()]
  [return: MarshalAs(UnmanagedType.Error)]
  unsafe HRESULT Read(void* pv, UInt32 cb, [Optional] UInt32* pcbRead);

  [PreserveSig()]
  [return: MarshalAs(UnmanagedType.Error)]
  unsafe HRESULT Write(void* pv, UInt32 cb, [Optional] UInt32* pcbWritten);
(中略)
}

ポイントは、[GeneratedComInterface] 属性を付けることと、partial にすることです。これで後はソースジェネレーターが良きに計らってくれます。

ここは既存の COM を使う場合と同じやり方です。

ちなみに、CsWin32 を AOT で使う場合は allowMarshaling を false にしますが、一時的に allowMarshaling を true にすると上記のインターフェース宣言が生成されます(多少の手直しは必要ですが)。

allowMarshaling を false にした状態だとインターフェースという名の構造体が生成されますが、そちらは COM インスタンス使用時に用います。

自作 COM の実装

IStream を実装する CustomStream COM コンポーネントを作成します。

Write() で書き込まれた内容をメンバー変数 _contents に保持しておくだけのシンプルな COM コンポーネントです。

IWICBitmapEncoder で画像(例えば PNG)を保存する際にこの COM コンポーネントを使うと、画像ファイルのバイトデータが _contents に保持されることになります。

CustomStream.cs
[GeneratedComClass]
public partial class CustomStream : IStream
{
(中略)
  [PreserveSig()]
  [return: MarshalAs(UnmanagedType.Error)]
  public unsafe HRESULT Write(void* pv, UInt32 cb, [Optional] UInt32* pcbWritten)
  {
    _contents = new Byte[cb];
    Span<Byte> src = new(pv, (Int32)cb);
    src.CopyTo(_contents);
    if (pcbWritten != null)
    {
      *pcbWritten = cb;
    }
    return HRESULT.S_OK;
  }
(中略)
}

ポイントは、[GeneratedComClass] 属性を付けることと、partial にすることです。

自作 COM の使用

自作 COM をシステム登録せずに使うには、普通に new した後で ComWrappers を使います。

Program.cs
// 自作 COM のインスタンスを new で作成
Com.CustomStream customStream = new();

// COM として使えるように ComWrappers を通じて IUnknown を取得
StrategyBasedComWrappers cw = new();
IUnknown* unk = null;
unk = (IUnknown*)cw.GetOrCreateComInterfaceForObject(customStream, CreateComInterfaceFlags.None);

// IStream を取得
IStream* stream = null;
unk->QueryInterface(out stream).ThrowOnFailure();

ComWrappers.GetOrCreateComInterfaceForObject() で COM の基底インターフェースである IUnknown へのポインターを取得できます。

今回は IStream として使いたいので、QueryInterface() で IStream へのポインターを取得します。

これで自作 COM を IStream として使えるようになりました。後は IStream を欲している Win32 API (IWICBitmapEncoder) に渡せば OK です。

Program.cs
encoder->Initialize(stream, WICBitmapEncoderCacheOption.WICBitmapEncoderNoCache).ThrowOnFailure();

自作 COM 使用時の注意点

IUnknown からの変換

IUnknown を IStream にする際は、必ず QueryInterface() する必要があります。

ポインター同士だからといって単にキャストすると正しく動作しません(C# だからというわけではなく、COM の作法です)。

QueryInterface() は参照カウントを増やすので、IUnknown も IStream も両方とも使用後に Release() する必要があります。

2 種類の IStream

異なる 2 種類の IStream が登場することに注意が必要です。

1 つめは「インターフェースの宣言」で作成した IStream.cs の IStream です(サンプルプログラムでは名前空間は TestImplementComAotConsole.Com)。

2 つめは「自作 COM の使用」で使用した Program.cs の IStream です。こちらは CsWin32 が生成した構造体型インターフェースで、名前空間は Windows.Win32.System.Com です。

取り違えるとエラーになるので、間違ったまま使ってしまうということはないと思いますが、自動補完などで意図せずエラーになりやすいので注意が必要です。

IStream の使い回し

2 つめの IStream(Windows.Win32.System.Com)はポインターのまま使い回す必要があります。

IStream copy = *stream;

のようにポインターを解除しようとすると、ビルドは通りますが正しく実行されません。

ポインターにアロー演算子まで登場し、AOT で COM すると C# ではなく C++ な気分になってきます。

サンプルプログラム

サンプルプログラム(GitHub に上げてあります)では、自作 COM と既存 COM の 2 つに同じ画像データを出力して、内容が同じになるかチェックしています(内容が異なっていたら自作 COM のバグということです)。

COM に出力した画像データを再度読み込み、改めてファイルとして出力しているので、実際の PNG 画像として開くことができます(2x2 ピクセルなので拡大しないとよく見えませんが)。

確認環境

項目 環境
OS Windows 11 Pro 25H2 (x64)
Visual Studio 2026 18.7.0
.NET 10.0

参考リンク

Discussion