Native AOT で COM を使ってみた

2024/12/02に公開

はじめに

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 を使う場合は allowMarshalingfalse にする必要があります

NativeMethods.json
{
	"$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 で allowMarshalingfalse にしているので今更という感じもしますが、その辺りの検証はしていません。

クライアント部分

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);

ができません。

ご存じの方がいらっしゃいましたら、ご教示いただけると幸いです。

確認環境

項目 環境
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)

参考リンク

主な改訂履歴

  • 2024/12/02 初版。
  • 2024/12/06 クライアント部分を更新。

Discussion