Native AOT で COM を使ってみた
はじめに
MS-IME の COM を Native AOT(以降「AOT」)で使い、漢字からひらがなに逆変換しました。
AOT の場合、通常の COM の使い方では動かないので、やり方を整理しておきます。
サンプルプログラムのソースコードは GitHub に上げてあります。
AOT の制約
C# で COM オブジェクトを作成する場合、手軽なのは Activator を使う方法かと思います。
Type? type = Type.GetTypeFromProgID("MSIME.Japan");
if (type != null)
{
	IFELanguage2? ime = Activator.CreateInstance(type) as IFELanguage2;
}
しかし AOT では Type を取得する時点で実行時エラーになります。
COM Interop is not supported on this platform.
ちなみに、プロジェクトプロパティーで <BuiltInComInteropSupport> を指定しても解決しません。
代わりに ComWrappers とソースジェネレーターを使えとのことなので、以下で対応していきます。
プロジェクトの設定
AOT 有効化
プロジェクトプロパティーの「ネイティブ AOT の公開」をオンにするなど、AOT を有効化します。
詳しくはこちらの記事をご覧ください。
また、アンセーフコードが必要になるため、「アンセーフコード」もオンにします。
CsWin32 導入
PInvoke のために、プロジェクトに CsWin32 パッケージをインストールします。
詳しくはこちらの記事をご覧ください。
注意点として、AOT で CsWin32 を使う場合は allowMarshaling を false にする必要があります。
{
	"$schema": "https://aka.ms/CsWin32.schema.json",
	"public": true,
	"allowMarshaling": false
}
ソースコード
インターフェース
COM インターフェースの IFELanguage2 は AOT しない場合と基本的には同じ書き方ですが、いくつか違いがあります。
- クラス属性:定番の 
[ComImport][InterfaceType]属性は使わず、代わりに[GeneratedComInterface]を使います。 - メソッド属性:
[PreserveSig]を入れます。AOT しない場合は省略しても動作していたので少々面倒くさいです。 - ソースジェネレーターのために partial にしておきます。
 
[GeneratedComInterface]
[Guid("21164102-C24A-11d1-851A-00C04FCC6B14")]
public partial interface IFELanguage2
{
	[PreserveSig]
	Int32 Open();
	[PreserveSig]
	Int32 Close();
	[PreserveSig]
	Int32 GetMorphResult(UInt32 request, UInt32 cmode, Int32 cwchInput, [MarshalAs(UnmanagedType.LPWStr)] String pwchInput, IntPtr cinfo, out IntPtr result);
	[PreserveSig]
	Int32 GetConversionModeCaps(ref UInt32 caps);
	[PreserveSig]
	Int32 GetPhonetic([MarshalAs(UnmanagedType.BStr)] String str, Int32 start, Int32 length, [MarshalAs(UnmanagedType.BStr)] out String result);
	[PreserveSig]
	Int32 GetConversion([MarshalAs(UnmanagedType.BStr)] String str, Int32 start, Int32 length, [MarshalAs(UnmanagedType.BStr)] out String result);
}
返値は Int32 ではなく HRESULT で宣言しておくこともでき、そのほうが利用時にキャストが不要になって便利です。
ただし、その場合はアセンブリ全体に [DisableRuntimeMarshalling] 属性を付ける必要があり、影響範囲が広いかなと思ったので、今回は Int32 にしています。CsWin32 で allowMarshaling を false にしているので今更という感じもしますが、属性を付与した場合の影響範囲については未検証です。
クライアント部分
COM オブジェクトの作成は、古の CoCreateInstance() でやりました。
HRESULT result = PInvoke.CoCreateInstance(clsId, null,
 	CLSCTX.CLSCTX_INPROC_SERVER | CLSCTX.CLSCTX_INPROC_HANDLER | CLSCTX.CLSCTX_LOCAL_SERVER,
	typeof(IFELanguage2).GUID, out void* ppv);
これを ComWrappers に通すことでインターフェースを得られます。
ComWrappers comWrappers = new StrategyBasedComWrappers();
IFELanguage2 ime = (IFELanguage2)comWrappers.GetOrCreateObjectForComInstance((nint)ppv, CreateObjectFlags.None);
その後は AOT しない場合と同様の使い方です。
サンプルプログラム
サンプルプログラム(GitHub に上げてあります)では、漢字の文章を入力して逆変換ボタンをクリックすると、ひらがなが表示されます。

COM を使っているのは MainPageViewModel です。
AOT アプリのビルド・実行方法についてはこちらの記事をご覧ください。
疑問
COM オブジェクトの解放(厳密には RCW の参照カウント減のようですが)はどうやればいいのでしょうか。公式ドキュメントに記載がありません。
ランタイムベースの COM 相互運用ができないので、
Marshal.ReleaseComObject(ime);
ができません。
ご存じの方がいらっしゃいましたら、ご教示いただけると幸いです。
より生々しく書く
COM インターフェースを CsWin32 に生成してもらうこともできますが、その場合ポインタ直接操作になるので、より生々しい感じになります。
IFELanguage* ime = null;
HRESULT result = PInvoke.CoCreateInstance(clsId, null, CLSCTX.CLSCTX_INPROC_SERVER | CLSCTX.CLSCTX_INPROC_HANDLER | CLSCTX.CLSCTX_LOCAL_SERVER, out ime);
...
ime->GetPhonetic(kanjiBStr, 1, -1, ref hiraganaBStr);
CsWin32 が生成する IFELanguage はインターフェースのような名前をしていますが実際は構造体です。
public unsafe partial struct IFELanguage
	:IVTable<IFELanguage,IFELanguage.Vtbl>,IComIID		{
	/// <inheritdoc cref="QueryInterface(global::System.Guid*, void**)"/>
	public unsafe winmdroot.Foundation.HRESULT QueryInterface(in global::System.Guid riid, out void* ppvObject)
	...
インターフェースではないので ComWrappers が使えず、ポインタで取得した ime をそのまま使う形になります。アロー演算子まで出てくると、C# ではなく C++ プログラミングをしている気分になります。
個人的には、これはこれでアリな気がしています。
生々しいので、COM オブジェクトの解放も従来通り行えます。
ime->Release();
解放がどうなっているのか不明な ComWrappers より安心です。
サンプルプログラムは cswin32-ifelanguage ブランチで、COM を使っているのは MainPageViewModel です。
確認環境
| 項目 | 環境 | 
|---|---|
| OS | Windows 11 Pro 23H2 / Windows 10 Home 22H2 | 
| Visual Studio | 2022 17.12.2 | 
| .NET | 9.0 | 
| Template Studio for WinUI | 5.5 | 
| WinUIEx | 2.5.0 | 
| Windows App SDK | 1.6.241114003 (1.6.3) | 
参考リンク
- サンプルプログラム
 - ComWrappers のソース生成
 - WinUI 3 で Native AOT が簡単になっていた
 - CsWin32 を別プロジェクトに分離する
 - AOT Support?
 - IFELanguage インターフェイス
 - IFE Language 2 Interface
 - Marshal.ReleaseComObject(Object) メソッド
 - Can I use CsWin32 to open the FileOpenDialog from a console application?
 
主な改訂履歴
- 2024/12/02 初版。
 - 2024/12/06 「クライアント部分」を更新。
 - 2025/04/01 「より生々しく書く」を新規作成。
 - 2025/08/07 「より生々しく書く」に加筆。
 
Discussion