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);
ができません。
ご存じの方がいらっしゃいましたら、ご教示いただけると幸いです。
確認環境
項目 | 環境 |
---|---|
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?
- IFE Language 2 Interface
- Marshal.ReleaseComObject(Object) メソッド
主な改訂履歴
- 2024/12/02 初版。
- 2024/12/06 クライアント部分を更新。
Discussion