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