.NET Core で Marshal.GetActiveObject を再現してみたときのメモ
.NET Core には Marshal.GetActiveObject(String) メソッド (System.Runtime.InteropServices) | Microsoft Docs が存在しないため、試しに自分で実装してみたときのメモ。
なお、後から「そういえばリファレンスソースがあったよな…?」とおもって確認してみたら、Marshal.GetActiveObject
の部分もあったので、
下手に手実装せず、リファレンスソースをベースにした方がいいとは思います。
自分が作成したコード
Windows PowerShell 5.1、PowerShell Core 7.0 のどちらのAdd-Type
でも問題無く使用できることは確認。
using System; using System.ComponentModel; using System.Runtime.InteropServices; namespace Example.Example /* いい感じに変えること */ { public static class COMSupport { public static object GetActiveObject(string progID) { const int S_OK = 0x0000; Guid clsId = Guid.Empty; if (S_OK != NativeMethods.CLSIDFromString(progID, out clsId)) { throw new Win32Exception(); } object com; if (S_OK != NativeMethods.GetActiveObject(clsId, IntPtr.Zero, out com)) { throw new Win32Exception(); } return com; } private static class NativeMethods { /// <summary> /// Retrieves a pointer to a running object that has been registered with OLE. /// </summary> /// <param name="rclsid">The class identifier (CLSID) of the active object from the OLE registration database.</param> /// <param name="pvReserved">Reserved for future use. Must be null.</param> /// <param name="ppunk">The requested active object.</param> /// <returns>If this function succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code.</returns> /// <see cref="https://docs.microsoft.com/ja-jp/windows/win32/api/oleauto/nf-oleauto-getactiveobject"/> [DllImport( "oleaut32.dll", EntryPoint = "GetActiveObject", CallingConvention = CallingConvention.Winapi, ExactSpelling = true, PreserveSig = true, SetLastError = true )] public static extern int GetActiveObject( [MarshalAs(UnmanagedType.LPStruct), In] Guid rclsid, [In] IntPtr pvReserved /* = System.IntPtr.Zero */, [MarshalAs(UnmanagedType.Interface), Out] out object ppunk ); /// <summary> /// Converts a string generated by the StringFromCLSID function back into the original CLSID. /// </summary> /// <param name="lpsz">The string representation of the CLSID.</param> /// <param name="pclsid">A pointer to the CLSID.</param> /// <returns>This function can return the standard return value E_INVALIDARG, as well as the following values.</returns> [DllImport( "ole32.dll", EntryPoint = "CLSIDFromString", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = true, SetLastError = true )] public static extern int CLSIDFromString( [MarshalAs(UnmanagedType.LPWStr), In] string lpsz, [MarshalAs(UnmanagedType.Struct), Out] out Guid pclsid ); } } }
リファレンスソースとの違い
ProgID→CLSID変換方法
DLL関数の方の
GetActiveObjectではオブジェクトの取得に、慣れ親しんだProgID(Excel.Application
とか。人がオブジェクトを指定するためのもの)ではなく、CLSID(Windowsが対象を認識するためのID。GUID形式)を指定する。
そのProgIDからCLSIDの変換に使用しているDLL関数が異なっていた。
リファレンスソースは、CLSIDFromProgIDExを使用し、自分はCLSIDFromStringを使用している。
名前からするに、本来はCLSIDFromProgIDEx
を使うべきだが「レジストリを変更します」的な雰囲気の文言があったため、
VBAでExcelを使う - QiitaでProgIDからCLSIDへの変換に使われていたCLSIDFromString
を使用した。
.NET Framework内部でCLSIDFromProgIDEx
を使っている以上、それでいい気がするけれど、なんとなく気分の問題。
DLL関数のPreserveSig
の設定
今回使用しているDLL関数はHRESULT(成功や失敗の理由を示す整数値)の返り値を返す。
このような場合に、DllImport
のPreserveSig フィールドをfalse
にし、返り値をvoid
に変更するとDLL関数エラー時に自動でC#の例外に変換してくれる。
要するにPreserveSig = false
としてDLL関数の返り値をvoid
にすると、以下のように書いているところがただの呼び出しでOKになる。
if (S_OK != NativeMethods.CLSIDFromString(progID, out clsId)) { throw new Win32Exception(); }
ちゃんと書こうと思って、PreserveSig = true
としたけれど、結局エラーにする以上true
にしても良かった気がする(using
で指定するものも減る)。
はまったこと
Guid
を返り値で受け取る時のMarshalAs
の定義
Guidを入力で使うときは[MarshalAs(UnmanagedType.LPStruct)]
を付けるとよい、と聞いていたので
返り値の方にも間違えて付けてしまったところ、メモリアクセス違反のエラーでプロセスが落ちてしまった。
入力として渡す分には、ポインタを渡して参照してもらえればいいけれど、出力として貰う場合は[MarshalAs(UnmanagedType.Struct)]
とする必要があった。
参照への参照の表現方法
DLL関数のGetActiveObjectの定義は以下のようになっており、ppunk
がIUnknown
(COMオブジェクト)へのポインタのポインタ(参照への参照)となっている。
HRESULT GetActiveObject(
REFCLSID rclsid,
void *pvReserved,
IUnknown **ppunk
);
VBAであれば、ByRefでObject型を渡すように定義すればOKなので、C#でもそのままref object ppunk
のように定義したら、以下のようなエラーが発生してしまった。
Specified OLE variant is invalid. 指定された OLE 変数が無効です。
適当に試したところ、以下のどちらかの方法であればCOMオブジェクトへの参照への参照を表現出来るようだった。
ref IntPtr ppunk
として、Marshal.GetObjectForIUnknown
で変換する
まずは、COMオブジェクトへの参照として、ポインタIntPtr
でやりとりし、取得したポインタをMarshal.GetObjectForIUnknownでCOMオブジェクトにする、という方法。
[MarshalAs(UnmanagedType.Interface)]
を指定する
UnmanagedType.Interface
を指定することで、その引数がCOMの型と認識され自動でCOMオブジェクトとしてくれる。
今回は引数をobject
で定義しているためUnmanagedType.IUnknown
でも問題はない。
参考
GetActiveObject function (oleauto.h) - Win32 apps | Microsoft Docs
Marshal.GetActiveObject のリファレンスソース
GetActiveObject function (oleauto.h) - Win32 apps | Microsoft Docs
CLSIDFromProgIDEx function (combaseapi.h) - Win32 apps | Microsoft Docs
DllImportAttribute クラス (System.Runtime.InteropServices) | Microsoft Docs
MarshalAsAttribute クラス (System.Runtime.InteropServices) | Microsoft Docs
VBAでExcelを使う - Qiita
【Windows/C#】なるべく丁寧にDllImportを使う - Qiita