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 の実装元となるインターフェースを宣言します。
[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 に保持されることになります。
[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 を使います。
// 自作 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 です。
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