.NET 7で追加されたLibraryImportAttribute
や関連する機能の概要、生成されるコード、試行錯誤した点、サンプル等の備忘録記事です。
本記事の内容は、Microsoft Visual Studio Community 2022 (64-bit) - Current Version 17.4.3
と.NET SDK Version 7.0.101
で動作確認しました。
- 概要
- LibraryImportAttributeによる生成結果ソースの観察
- 独自マーシャラーを実装せずにできる用途の例
- 引数や戻り値がBlittable型のみの場合はそもそもマーシャリング不要
- bool型やSafeHandle型、Delegate型の使用時や、SetLastError指定時は、必要な事前処理と事後処理が生成される
- ref引数やout引数、in引数はfixedステートメントでポインターに変換される
- 配列型やSpan系統は標準マーシャラー経由で先頭要素アドレスを指すポインターへ変換される
- 文字列型のマーシャリングはUTF-16やUTF-8、BStrの場合は簡単にできて、Ansiの場合も標準でできる
- UnmanagedCallConvAttributeを使用している場合は、その内容がそのまま生成結果ソースへ反映される
- 独自マーシャラーの実装が必要な用途の例
- 独自マーシャラーを実装せずにできる用途の例
- その他色々
- 感想
概要
LibraryImportAttribute Classは、DllImportAttribute Classの代替となる属性です。どちらもPlatform Invoke (P/Invoke)用途で使用する属性で、人によっては全く縁がない分野かもしれません。しかし必要な人にとっては必須の分野だと思います。
LibraryImportAttribute
使用時は、ソースジェネーレーターのruntime/LibraryImportGenerator.cs at v7.0.0 · dotnet/runtimeによってビルド時にP/Invoke用ソースが生成されます。LibraryImportGenerator
によるエラーや警告、メッセージ等は、SYSLIB1050
等のエラー番号で表されます。SYSLIB
関係のドキュメントはSYSLIB diagnostics for Microsoft.Interop.LibraryImportGeneratorにあります。
P/Invoke source generationや、提案時のLibraryImportAttribute for p/invoke source generation · Issue #46822 · dotnet/runtimeによると、DllImportAttribute
とLibraryImportAttribute
の大きな違いは以下の点とのことです:
DllImportAttribute
では、マーシャリング用コードの動的生成が実行時に必要です。一方でLibraryImportAttribute
ではマーシャリング用コードをビルド時に生成できるため、実行時のコード生成が不要になります。また、マーシャリング箇所のインライン展開や、AOT(ahead of time)コンパイルも可能になります。DllImportAttribute
では、マーシャリング指定に誤りがあった場合はデバッグが大変です。一方でLiburaryImportAttribute
ではC#ソースが生成されているため分かりやすく、また必要な場合はステップ実行できます。DllImportAttribute
という名前には、Windows環境固有のDLL
という名称が含まれています。LibraryImportAttribute
と言う名称は、クロスプラットフォーム向きの名称になっています。(DllImortAttribute
でもLinuxやmacOSの動的ライブラリ中APIを呼び出すことはできます)
利用者にとっては以下の点が大きいと思います:
DllImportAttribute
では非対応でしたが、LibraryImportAttribute
では可能になったマーシャリングが存在します。詳細は後述します。- 一方で、
DllImportAttribute
では対応していたマーシャリングが、LibraryImportAttribute
では非対応になっていたり、独自マーシャラーの実装が必要になる場合があります。こちらも詳細は後述します。 - runtime/StructMarshalling.md at main · dotnet/runtimeに
Limited type information in ref assemblies
とある通り、ref assemblyでは型情報が欠落しているため、ref assemblyを使用する場合のコード生成はうまくいかないとのことです。後述するGuid
やSystem.Runtime.InteropServices.ComTypes.FILETIPE
の場合に該当するのだと思います。
なお、この後紹介するように、LibraryImportAttribute
の生成結果ソースはDllImportAttribute
を使用しています。そのためDllImportAttribute
が完全に無くなっても良いわけではありません。内部的に依存しています。
Win32APIを使うならCsWin32も便利
microsoft/CsWin32というソースジェネレーターがあります。C#やRustからWin32 APIをもっと簡単に呼び出せるように ~Microsoftが「win32metadata」プロジェクトを発表 - 窓の杜によると、2021/01/21頃、つまりは.NET 5が最新バージョンだった時代に公開されました(tagsから追える最初のリリースは2021/01/12です)。当該ライブラリはnuget経由で導入できます。
CsWin32
は、名前の通りWindows環境向けのソースジェネレーターで、P/Invoke用だけでなくCOM相互運用用途のコードも生成できます。使用したいAPI名や定数名等をNativeMethods.txt
に書くと、それらの相互運用用C#コードを生成してくれます。なお、Linux環境等でもソースコードの生成やビルドはできるとのことです。とはいえ実行は流石にできません。(出典: Visual Studio 16.9 Preview 3 (C# Next チョットある) | ++C++; // 未確認飛行 C ブログ)
CsWin32
の使用方法、使用感に言及している記事です:
- C#でWin32APIを呼び出す方法(CsWin32利用) - 備忘録
- WPF + .NET 5 で CsWin32metadata を利用するサンプル - sh1’s diary
- CsWin32 で Win32 API や COM を使ったアプリケーション開発を効率化する - しばやん雑記
私がCsWin32
リリース当初に少し触ってみたときの感想です:
- APIの引数や戻り値に使用する構造体や
SafeHandle
派生型の定義等もまとめて生成してくれるのが特に便利だと思いました。C++ヘッダーやMicrosoftドキュメントを見てAPIシグネチャを書き直す作業には慣れましたが、避けられるのはやはり良いものです。 CsWin32
の生成結果ソースにはポインター型が登場しやすい印象です。適宜、配列型やSpan<T>
を受け取り、fixedしつつCsWin32
の生成結果を呼び出すラッパーが欲しくなりそうだと思いました。CsWin32
を導入しているソリューションを開いている間、VisualStudioの動作が遅く感じました(数値測定はしておらず)。ただハイスペックマシンを使っている方なら気にならないかもしれません。
C#コードからWin32APIを呼び出したい場合には選択肢の1つになると思います。
ただし、本記事執筆時点の最新バージョンでもCsWin32
のソースジェネレーターはISourceGenerator
で実装されています(出典: CsWin32/SourceGenerator.cs at v0.2.162-beta · microsoft/CsWin32)。すなわちIIncrementalGenerator
実装では無いため、VisualStudioのパフォーマンスに悪影響を与えやすいようです。なお、LibraryImportAttribute
用のソースジェネレーターは、IIncrementalGenerator
で実装されています(出典: runtime/LibraryImportGenerator.cs at v7.0.0 · dotnet/runtime)。
IIncrementalGenerator
については.NET 6からのC# Incremental Source Generatorの開発入門やRoslyn とその活用法 - Speaker Deckで紹介されています。
CsWin32
の紹介はここまでで、ここからはLibraryImportAttribute
の紹介に入ります。
Quick ActionによるDllImportAttributeからLibraryImportAttributeへの変換
.NET 7のプロジェクトでDllImportAttribute
を使っていれば、エラー一覧に以下のようなメッセージが自動的に出てくると思います:
Message SYSLIB1054 Mark the method 'GetTickCount' with 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
メッセージをダブルクリックするとDllImportAttribute
のついたメソッドへカーソルが移動します。そこでQuick Action候補を表示すると、提案内容にLibraryImportAttribute
への自動変換が存在します。このとき、元の名前のまま変換する(=LibraryImportAttribute.EntryPoint
の指定なし)か、AまたはW接尾辞をつけた名前で変換する(=LibraryImportAttribute.EntryPoint
を指定)かを適切に選びます。LibraryImportAttribute
はEntryPoint指定があればその名前のみを、指定がなければメソッド名そのもののみをネイティブライブラリから探す仕様であるため、ネイティブ側の関数名をよく確認しましょう。
Quick Actionの実行例画像です:
「最初はDllImportAttribute
で書いて、Quick ActionでLibraryImportAttribtue
に変換してもらう」という実装の流れも十分ありだと思います。
DllImportAttributeとLibraryImportAttributeで異なる点の詳細
P/Invoke source generationで説明されている違いとして、以下の点があります。
- DllImportAttribute.CallingConventionに対応するメンバーは
LibraryImportAttribute
にはありません。代わりにUnmanagedCallConvAttribute Classを使用する必要があります。なお、Quick Actionで変換すると自動で変更してくれるので簡単です。 - DllImportAttribute.CharSetは、LibraryImportAttribute.StringMarshallingに代わっています。フィールド名だけでなく列挙型も異なるため、適宜変換が必要です。なお、Quick Actionで変換すると自動で変更してくれるので簡単です。
- DllImportAttribute.ExactSpellingに
false
が設定されている場合は、「EntryPointでの指定名またはメソッド名を探して存在すればそれを使い、存在しなければCharSetに応じてAまたはWを末尾に追加した名前でも探す」という動作を行います。LibraryImportAttribute
にはそのような動作はありません。「EntryPointでの指定名またはメソッド名を探して存在すればそれを使う」動作のみになります。理由は、DllImportAttribute.ExactSpelling
をtrue
に設定したコードを生成するためです。 - DllImportAttribute.PreserveSigに
false
が設定されている場合は、ネイティブAPI戻り値のint
型をHRESULT
として解釈し、失敗コードの場合は自動的に例外を送出してくれます。LibraryImportAttribute
にはそのような機能はありません。なお、DllImportAttribute.PreserveSig
がfalse
のメソッドをQuick Actionで変換すると、戻り値がvoid
からint
へ変換されました。呼び出しそのものは問題なくできますが、利用箇所で失敗判定を追加する必要があります。 - DllImportAttribute.BestFitMappingが
true
時の処理や、DllImportAttribute.ThrowOnUnmappableCharがtrue
時の処理は、LibraryImportAttribute
では提供されていません。当該APIのUtf-16版やUtf-8版APIが存在するなら、そちらを使用するよう切り替えるのが解決策になります。GetProcAddressのように本当にAnsi版APIだけが存在する場合は……DllImportAttribute
を使い続けるのも1つの手だと思います。なお、当該メンバーをtrue
に設定しているDllImportAttribute
を持つメソッドをQuick Actionで変更しようとすると、存在しないLibraryImportAttribute.BestFitMapping
やLibraryImportAttribute.ThrowOnUnmappableChar
を設定しようとするコードが生成されてしまいます。当然そのコードはコンパイルエラーになります。本現象は報告済みですので今後修正されると思いますが、「Quick Actionによってエラーになるコードになってしまう」可能性がある点に注意が必要です。
その他、触ってみてから気付いた違いも多くあります。
DllImportAttribute
の場合は、Span<T>
のマーシャリングは非対応です。LibraryImportAttribute
ではSpan<T>
等のマーシャリングに対応しています!DllImportAttribute
の場合は、ネイティブAPIからのcharバッファ読み書き用途でStringBuilder
型を使えます。ただしパフォーマンス上の理由で避けるよう推奨されています(出典: Default Marshalling for Strings - .NET Framework)。LibraryImportAttribute
の場合はStringBuilder
型のサポートはありません。char[]
やSpan<char>
を使いましょう。DllImportAttribute
の場合は、bool
型はデフォルトで、Win32APIで使用する4バイト値のBOOL
型へマーシャリングされます(出典: Customizing structure marshalling - .NET)。その挙動はMarshalAsAttribute Classで変更できます。LibraryImportAttribute
の場合は、bool
型へのMarshalAsAttribute
の指定が必須です。そのため分かりやすいコードになります。また、MarshalAsAttribute
の記述を忘れている場合は以下のようなエラーを出してくれます:
Error SYSLIB1051 Marshalling bool without explicit marshalling information is not supported. Specify either 'MarshalUsingAttribute' or 'MarshalAsAttribute'. The generated source will not handle marshalling of the return value of method 'ReadFile'.
DllImportAttribute
の場合は、unsafeコードを許可しないプロジェクトでも使用できます。一方でLibraryImportAttribute
の場合は生成結果ソースにunsafeコードを含むため、プロジェクト設定でunsafeコードを許可する必要があります。とはいえunsafeコードを許可していない場合は以下のエラーを出してくれるので、すぐに分かることでしょう:
Error SYSLIB1062 LibraryImportAttribute requires unsafe code. Project must be updated with '<AllowUnsafeBlocks>true</AllowUnsafeBlocks>'
DllImportAttribute
の場合は、C#のclass Foo
を、ネイティブAPIのFoo*
へマーシャリングできます(出典: Marshalling Classes, Structures, and Unions)。LibraryImportAttribute
の場合はclassのマーシャリングをするためには独自マーシャラーの実装が必要です。詳細は後述します。DllImportAttribute
の場合は、ネイティブAPIがGUID*
を引数に取る箇所で、[MarshalAs(UnmanagedType.LPStruct)] Guid
を指定できます(出典: c# - How do I marshal a structure as a pointer to a structure? - Stack Overflow)。LibraryImportAttribute
ではUnmanagedType.LPStruct
をサポートしていません(とはいえソースジェネレーターを使う時点でpartialメソッドが拡張されたC# 9以降を使っているはずなので、C# 7.2で追加されたin
パラメーターを使ってin Guid
と指定すればいいでしょう)。また、Guid
型のマーシャリングには独自マーシャラーの実装が必要であるようです。詳細は後述します。DllImportAttribute
の場合は、HandleRef Structをマーシャリングできます(出典: Type marshalling - .NET)。LibraryImportAttribute
の場合はHandleRef
のマーシャリングは出来ません。とはいえHandleRef
のドキュメントにある通りSafeHandle
型が後継ですし、そちらはLibraryImportAttribute
も対応しているため、SafeHandle
型を使えばいいでしょう(もしくはIntPtr
型か)。もしどうしてもHandleRef
型を使う必要がある場合は、HandleRefMarshaller.csを参考にして同様に独自マーシャラーを実装すると良さそうです。DllImportAttribute
の場合は__arglist
を扱えます。__arglist
の解説は可変引数をパラメータに持つC++の関数を、C#から利用する方法 | C# プログラミング解説等にあります。一方でLibraryImportAttribute
は__arglist
を全く扱えないようです。また、エラー内容を見ても__arglist
が原因とは全く分からない内容なので、ハマると困りそうです:
using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Test; internal static partial class NativeMethods { [LibraryImport("msvcrt.dll")] [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })] internal static partial int printf([MarshalAs(UnmanagedType.LPStr)] string s, __arglist); }
Error CS8795 Partial method 'NativeMethods.printf(string, __arglist)' must have an implementation part because it has accessibility modifiers.
LibraryImportAttributeによる生成結果ソースの観察
本節では、LibraryImportAttribute
による生成結果ソースを見ていきます。
なお、LibraryImportAttribute
関連箇所を編集していると、エラー一覧に以下の警告が出ていることがあります:
Warning CS8785 Generator 'LibraryImportGenerator' failed to generate source. It will not contribute to the output and compilation errors may occur as a result. Exception was of type 'ArgumentException' with message 'SyntaxTree is not part of the compilation
この警告が発生していると、partialメソッド定義箇所で「定義を移動」を選択しても生成結果にジャンプできなくなってしまい、不便です。その現象が発生した場合は、ソリューションを開き直すとまた通常通りの動作になります。
独自マーシャラーを実装せずにできる用途の例
LibraryImportAttribute
関係で標準でマーシャリングできる型、出来ない型が存在します。標準でマーシャリングできない型は、独自マーシャラーを実装してマーシャリングする必要があります。本項では、独自マーシャラーが不要な範囲でのLibraryImportAttribute
の使用例と生成結果ソースを解説します。
引数や戻り値がBlittable型のみの場合はそもそもマーシャリング不要
Blittable
型とは、マネージドとネイティブ側で同一メモリ表現であるため、マーシャリングが不要な型です(出典: Blittable and Non-Blittable Types - .NET Framework)。
int
等の整数型やenum
、IntPtr
やポインター型等はBlittable
型です。関数や引数がBlittable型のみである場合は、LibraryImportAttribute
による生成結果ソースは単にDllImportAttribute
を付与した定義になります。:
using System; using System.Runtime.InteropServices; namespace Test; internal enum WM : int { WM_CREATE = 1, // 他メンバーは省略 } internal static partial class NativeMethods { // ポインター型の例 [LibraryImport("Kernel32.dll")] internal static unsafe partial int DiscardVirtualMemory( void* VirtualAddress, nint Size); // enumや関数ポインター型の例 [LibraryImport("User32.dll", EntryPoint = "CallWindowProcW")] internal static unsafe partial IntPtr CallWindowProc( delegate* unmanaged[Stdcall]<IntPtr, WM, IntPtr, IntPtr, IntPtr> lpPrevWndFunc, IntPtr hWnd, WM Msg, IntPtr wParam, IntPtr lParam); }
// <auto-generated/> namespace Test { internal static unsafe partial class NativeMethods { [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "DiscardVirtualMemory", ExactSpelling = true)] internal static unsafe extern partial int DiscardVirtualMemory(void* VirtualAddress, nint Size); } } namespace Test { internal static unsafe partial class NativeMethods { [System.Runtime.InteropServices.DllImportAttribute("User32.dll", EntryPoint = "CallWindowProcW", ExactSpelling = true)] internal static unsafe extern partial nint CallWindowProc(delegate* unmanaged[Stdcall]<nint, global::Test.WM, nint, nint, nint> lpPrevWndFunc, nint hWnd, global::Test.WM Msg, nint wParam, nint lParam); } }
なお、関数ポインター型へ変換可能なC#コードはUnmanagedCallersOnlyAttribute Classを使うと実現できました:
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] private static IntPtr TestWndProc(IntPtr hWnd, WM Msg, IntPtr wParam, IntPtr lParam) { Console.WriteLine($"hWnd={hWnd}, Msg={Msg}, wParam={wParam}, lParam={lParam}"); return IntPtr.Zero; } // &演算子で関数ポインター型へ変換出来ます。 NativeMethods.CallWindowProc(&TestWndProc, 12, (WM)34, 56, 78);
bool型やSafeHandle型、Delegate型の使用時や、SetLastError指定時は、必要な事前処理と事後処理が生成される
bool
からの変換やbool
への変換、SafeHandle
からのネイティブハンドルの取得、Delegate
型からのポインター取得等、必要な処理が生成されます:
using System; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; namespace Test; internal static partial class NativeMethods { // SetLastError指定、SafeHandle入力と出力、bool引数と戻り値の例 [LibraryImport("Kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool DuplicateHandle( SafeProcessHandle hSourceProcessHandle, SafeFileHandle hSourceHandle, SafeProcessHandle hTargetProcessHandle, out SafeFileHandle lpTargetHandle, int dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwOptions); // SetLastError指定、SafeHandle入力、bool入力と出力と戻り値の例 [LibraryImport("Kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool GetProcessPriorityBoost( SafeProcessHandle hProcess, [MarshalAs(UnmanagedType.Bool)] out bool pDisablePriorityBoost); // SetLastError指定、Delegate入力、bool戻り値の例 [return: MarshalAs(UnmanagedType.Bool)] internal delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); [LibraryImport("User32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool EnumWindows( EnumWindowsProc lpEnumFunc, IntPtr lParam); }
// <auto-generated/> namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial bool DuplicateHandle(global::Microsoft.Win32.SafeHandles.SafeProcessHandle hSourceProcessHandle, global::Microsoft.Win32.SafeHandles.SafeFileHandle hSourceHandle, global::Microsoft.Win32.SafeHandles.SafeProcessHandle hTargetProcessHandle, out global::Microsoft.Win32.SafeHandles.SafeFileHandle lpTargetHandle, int dwDesiredAccess, bool bInheritHandle, int dwOptions) { int __lastError; bool __invokeSucceeded = default; System.Runtime.CompilerServices.Unsafe.SkipInit(out lpTargetHandle); System.IntPtr __hSourceProcessHandle_native = default; System.IntPtr __hSourceHandle_native = default; System.IntPtr __hTargetProcessHandle_native = default; System.IntPtr __lpTargetHandle_native = default; int __bInheritHandle_native = default; bool __retVal; int __retVal_native = default; // Setup - Perform required setup. bool hSourceProcessHandle__addRefd = false; bool hSourceHandle__addRefd = false; bool hTargetProcessHandle__addRefd = false; global::Microsoft.Win32.SafeHandles.SafeFileHandle lpTargetHandle__newHandle = new global::Microsoft.Win32.SafeHandles.SafeFileHandle(); try { // Marshal - Convert managed data to native data. hSourceProcessHandle.DangerousAddRef(ref hSourceProcessHandle__addRefd); __hSourceProcessHandle_native = hSourceProcessHandle.DangerousGetHandle(); hSourceHandle.DangerousAddRef(ref hSourceHandle__addRefd); __hSourceHandle_native = hSourceHandle.DangerousGetHandle(); hTargetProcessHandle.DangerousAddRef(ref hTargetProcessHandle__addRefd); __hTargetProcessHandle_native = hTargetProcessHandle.DangerousGetHandle(); __bInheritHandle_native = (int)(bInheritHandle ? 1 : 0); { System.Runtime.InteropServices.Marshal.SetLastSystemError(0); __retVal_native = __PInvoke(__hSourceProcessHandle_native, __hSourceHandle_native, __hTargetProcessHandle_native, &__lpTargetHandle_native, dwDesiredAccess, __bInheritHandle_native, dwOptions); __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError(); } __invokeSucceeded = true; // Unmarshal - Convert native data to managed data. __retVal = __retVal_native != 0; } finally { if (__invokeSucceeded) { // GuaranteedUnmarshal - Convert native data to managed data even in the case of an exception during the non-cleanup phases. System.Runtime.InteropServices.Marshal.InitHandle(lpTargetHandle__newHandle, __lpTargetHandle_native); lpTargetHandle = lpTargetHandle__newHandle; } // Cleanup - Perform required cleanup. if (hSourceProcessHandle__addRefd) hSourceProcessHandle.DangerousRelease(); if (hSourceHandle__addRefd) hSourceHandle.DangerousRelease(); if (hTargetProcessHandle__addRefd) hTargetProcessHandle.DangerousRelease(); } System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "DuplicateHandle", ExactSpelling = true)] static extern unsafe int __PInvoke(System.IntPtr hSourceProcessHandle, System.IntPtr hSourceHandle, System.IntPtr hTargetProcessHandle, System.IntPtr* lpTargetHandle, int dwDesiredAccess, int bInheritHandle, int dwOptions); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial bool GetProcessPriorityBoost(global::Microsoft.Win32.SafeHandles.SafeProcessHandle hProcess, out bool pDisablePriorityBoost) { int __lastError; System.Runtime.CompilerServices.Unsafe.SkipInit(out pDisablePriorityBoost); System.IntPtr __hProcess_native = default; int __pDisablePriorityBoost_native = default; bool __retVal; int __retVal_native = default; // Setup - Perform required setup. bool hProcess__addRefd = false; try { // Marshal - Convert managed data to native data. hProcess.DangerousAddRef(ref hProcess__addRefd); __hProcess_native = hProcess.DangerousGetHandle(); { System.Runtime.InteropServices.Marshal.SetLastSystemError(0); __retVal_native = __PInvoke(__hProcess_native, &__pDisablePriorityBoost_native); __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError(); } // Unmarshal - Convert native data to managed data. __retVal = __retVal_native != 0; pDisablePriorityBoost = __pDisablePriorityBoost_native != 0; } finally { // Cleanup - Perform required cleanup. if (hProcess__addRefd) hProcess.DangerousRelease(); } System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "GetProcessPriorityBoost", ExactSpelling = true)] static extern unsafe int __PInvoke(System.IntPtr hProcess, int* pDisablePriorityBoost); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial bool EnumWindows(global::Test.NativeMethods.EnumWindowsProc lpEnumFunc, nint lParam) { int __lastError; System.IntPtr __lpEnumFunc_native; bool __retVal; int __retVal_native; // Marshal - Convert managed data to native data. __lpEnumFunc_native = lpEnumFunc != null ? System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(lpEnumFunc) : default; { System.Runtime.InteropServices.Marshal.SetLastSystemError(0); __retVal_native = __PInvoke(__lpEnumFunc_native, lParam); __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError(); } // NotifyForSuccessfulInvoke - Keep alive any managed objects that need to stay alive across the call. global::System.GC.KeepAlive(lpEnumFunc); // Unmarshal - Convert native data to managed data. __retVal = __retVal_native != 0; System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("User32.dll", EntryPoint = "EnumWindows", ExactSpelling = true)] static extern unsafe int __PInvoke(System.IntPtr lpEnumFunc, nint lParam); } } }
ここで、DuplicateHandle
の生成結果ソースを見れば分かるように、out引数のSafeFileHandle
のデフォルトコンストラクタを呼び出しています。そのためSafeHandle
派生型のデフォルトコンストラクタを定義し忘れていた場合は、コンパイルエラーとなって気付けます。DllImportAttribute
によるSafeHandle
派生型のout引数の場合は、デフォルトコンストラクタが無ければMissingMethodException
が実行時に送出されていました。このようにLibraryImportAttribute
の場合はコンパイル時に気づけることが多くなる点も良い点です。
余談です。SafeHandle
関係で思い出す話ですが、かつてはConstrained Execution Regions(略称CER)という機能がありました。CERについてはUsing the Reliability Features of the .NET Frameworkが詳しいです。このCERを使うと「絶対に例外を発生させない区間」を実現できるため、「ネイティブハンドル取得からSafeHandle
格納までの間に例外が発生してしまってハンドルリークしてしまう」問題を回避できました。「ネイティブハンドル取得からSafeHandle
格納までの間に例外が発生」という状況は、例えばThread.Abortによって発生する可能性がありました。
しかしCERは.NET Core以降ではサポートされておらず、またThread.Abort
は.NET 5以降ではPlatformNotSupportedException
を送出するだけになっています。つまりは.NET 7の現状では、「ネイティブハンドル取得からSafeHandle格納までの間に例外が発生してしまってハンドルリークしてしまう問題」は考慮する必要がないようです(生成コードを見るに、OutOfMeoryException
やStackOverflowException
が発生する余地もなさそうです)。そのためDuplicateHandle
のコード生成で、P/Invokeで取得したネイティブハンドルをMarshal.InitHandle
で設定するまでの間に例外が発生することは無いのでしょう。
ref引数やout引数、in引数はfixedステートメントでポインターに変換される
参照系統の引数はいずれもfixedステートメント経由でポインター型へ変換され、ポインター型を使ってP/Invokeする挙動になります:
using System.Runtime.InteropServices; namespace Test; internal struct FILETIME { internal int dwLowDateTime; internal int dwHighDateTime; } internal struct SYSTEMTIME { internal short wYear; internal short wMonth; internal short wDayOfWeek; internal short wDay; internal short wHour; internal short wMinute; internal short wSecond; internal short wMilliseconds; } internal partial class NativeMethods { [LibraryImport("Kernel32.dll", EntryPoint = "FileTimeToSystemTime")] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool FileTimeToSystemTime_UsingInOut(in FILETIME lpFileTime, out SYSTEMTIME lpSystemTime); [LibraryImport("Kernel32.dll", EntryPoint = "FileTimeToSystemTime")] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool FileTimeToSystemTime_UsingRef(ref FILETIME lpFileTime, ref SYSTEMTIME lpSystemTime); }
// <auto-generated/> namespace Test { internal unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial bool FileTimeToSystemTime_UsingInOut(in global::Test.FILETIME lpFileTime, out global::Test.SYSTEMTIME lpSystemTime) { System.Runtime.CompilerServices.Unsafe.SkipInit(out lpSystemTime); bool __retVal; int __retVal_native; // Pin - Pin data in preparation for calling the P/Invoke. fixed (global::Test.FILETIME* __lpFileTime_native = &lpFileTime) fixed (global::Test.SYSTEMTIME* __lpSystemTime_native = &lpSystemTime) { __retVal_native = __PInvoke(__lpFileTime_native, __lpSystemTime_native); } // Unmarshal - Convert native data to managed data. __retVal = __retVal_native != 0; return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "FileTimeToSystemTime", ExactSpelling = true)] static extern unsafe int __PInvoke(global::Test.FILETIME* lpFileTime, global::Test.SYSTEMTIME* lpSystemTime); } } } namespace Test { internal unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial bool FileTimeToSystemTime_UsingRef(ref global::Test.FILETIME lpFileTime, ref global::Test.SYSTEMTIME lpSystemTime) { bool __retVal; int __retVal_native; // Pin - Pin data in preparation for calling the P/Invoke. fixed (global::Test.FILETIME* __lpFileTime_native = &lpFileTime) fixed (global::Test.SYSTEMTIME* __lpSystemTime_native = &lpSystemTime) { __retVal_native = __PInvoke(__lpFileTime_native, __lpSystemTime_native); } // Unmarshal - Convert native data to managed data. __retVal = __retVal_native != 0; return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "FileTimeToSystemTime", ExactSpelling = true)] static extern unsafe int __PInvoke(global::Test.FILETIME* lpFileTime, global::Test.SYSTEMTIME* lpSystemTime); } } }
余談です。.NET標準でもSystem.Runtime.InteropServices.ComTypes.FILETIMEが存在しますが、LibraryImportAttribute
でそちらを使おうとするとエラーになります:
using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; namespace Test; internal struct SYSTEMTIME { internal short wYear; internal short wMonth; internal short wDayOfWeek; internal short wDay; internal short wHour; internal short wMinute; internal short wSecond; internal short wMilliseconds; } internal static partial class NativeMethods { // 標準のFILETIMEを使おうとするとエラーになる [LibraryImport("Kernel32.dll", EntryPoint = "FileTimeToSystemTime")] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool FileTimeToSystemTime(in FILETIME lpFileTime, out SYSTEMTIME lpSystemTime); }
Error SYSLIB1051 Runtime marshalling must be disabled in this project by applying the 'System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute' to the assembly to enable marshalling this type. The generated source will not handle marshalling of parameter 'lpFileTime'.
記事最初の方に書いたように、ref assemblyでは型情報が欠落していることが原因かもしれません。.NET標準ソース内容をローカルコピペして使うとエラーにならず正しくコード生成されました。
配列型やSpan系統は標準マーシャラー経由で先頭要素アドレスを指すポインターへ変換される
.NET 7からSystem.Runtime.InteropServices.Marshalling Namespaceが追加されています。配列型やSpan<T>
型などは、当該名前空間以下のマーシャラー経由でポインターに変換するコードが生成されます:
using System; using System.Net.Sockets; using System.Runtime.InteropServices; namespace Test; internal static partial class NativeMethods { [LibraryImport("Psapi.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool EnumProcesses(int[] lpidProcess, int cb, out int lpcbNeeded); [LibraryImport("Psapi.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool EnumProcesses(Span<int> lpidProcess, int cb, out int lpcbNeeded); [LibraryImport("Ws2_32.dll")] internal static partial int send(SafeSocketHandle s, ReadOnlySpan<byte> buf, int len, int flags); }
// <auto-generated/> namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial bool EnumProcesses(int[] lpidProcess, int cb, out int lpcbNeeded) { int __lastError; System.Runtime.CompilerServices.Unsafe.SkipInit(out lpcbNeeded); bool __retVal; int __retVal_native; // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __lpidProcess_native = &global::System.Runtime.InteropServices.Marshalling.ArrayMarshaller<int, int>.ManagedToUnmanagedIn.GetPinnableReference(lpidProcess)) fixed (int* __lpcbNeeded_native = &lpcbNeeded) { System.Runtime.InteropServices.Marshal.SetLastSystemError(0); __retVal_native = __PInvoke((int*)__lpidProcess_native, cb, __lpcbNeeded_native); __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError(); } // Unmarshal - Convert native data to managed data. __retVal = __retVal_native != 0; System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Psapi.dll", EntryPoint = "EnumProcesses", ExactSpelling = true)] static extern unsafe int __PInvoke(int* lpidProcess, int cb, int* lpcbNeeded); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial bool EnumProcesses(global::System.Span<int> lpidProcess, int cb, out int lpcbNeeded) { int __lastError; System.Runtime.CompilerServices.Unsafe.SkipInit(out lpcbNeeded); bool __retVal; int __retVal_native; // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __lpidProcess_native = &global::System.Runtime.InteropServices.Marshalling.SpanMarshaller<int, int>.ManagedToUnmanagedIn.GetPinnableReference(lpidProcess)) fixed (int* __lpcbNeeded_native = &lpcbNeeded) { System.Runtime.InteropServices.Marshal.SetLastSystemError(0); __retVal_native = __PInvoke((int*)__lpidProcess_native, cb, __lpcbNeeded_native); __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError(); } // Unmarshal - Convert native data to managed data. __retVal = __retVal_native != 0; System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Psapi.dll", EntryPoint = "EnumProcesses", ExactSpelling = true)] static extern unsafe int __PInvoke(int* lpidProcess, int cb, int* lpcbNeeded); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int send(global::System.Net.Sockets.SafeSocketHandle s, global::System.ReadOnlySpan<byte> buf, int len, int flags) { System.IntPtr __s_native = default; int __retVal; // Setup - Perform required setup. bool s__addRefd = false; try { // Marshal - Convert managed data to native data. s.DangerousAddRef(ref s__addRefd); __s_native = s.DangerousGetHandle(); // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __buf_native = &global::System.Runtime.InteropServices.Marshalling.ReadOnlySpanMarshaller<byte, byte>.ManagedToUnmanagedIn.GetPinnableReference(buf)) { __retVal = __PInvoke(__s_native, (byte*)__buf_native, len, flags); } } finally { // Cleanup - Perform required cleanup. if (s__addRefd) s.DangerousRelease(); } return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Ws2_32.dll", EntryPoint = "send", ExactSpelling = true)] static extern unsafe int __PInvoke(System.IntPtr s, byte* buf, int len, int flags); } } }
SafeSocketHandleの存在を知ったので使ってみました。.NET Core 3.0から存在していたとのことです。Microsoft.Win32.SafeHandles
以下の型の派生型がSystem
以下にあるのが意外に感じました。
文字列型のマーシャリングはUTF-16やUTF-8、BStrの場合は簡単にできて、Ansiの場合も標準でできる
文字列型を引数等に含むAPIの場合は、LibraryImportAttribute.StringMarshallingCustomTypeを指定するか、引数にMarshalAsAttribute
を指定する必要があります。なお、もしも指定を忘れていてもエラーで教えてくれるので安心です:
Error SYSLIB1051 Marshalling string or char without explicit marshalling information is not supported. Specify 'LibraryImportAttribute.StringMarshalling', 'LibraryImportAttribute.StringMarshallingCustomType', 'MarshalUsingAttribute' or 'MarshalAsAttribute'. The generated source will not handle marshalling of parameter 'lpString'.
関連しているので、string
、char[]
、Span<char>
の使用例をまとめて紹介します。長くなりますがご容赦ください:
using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; namespace Test; internal static partial class NativeMethods { // UTF-16にすることをStringMarshallingで指定 [LibraryImport("Kernel32.dll", EntryPoint = "lstrlenW", StringMarshalling = StringMarshalling.Utf16)] internal static partial int lstrlenW1(string lpString); // UTF-16にすることをMarshalAsAtributeで指定 [LibraryImport("Kernel32.dll", EntryPoint = "lstrlenW")] internal static partial int lstrlenW2([MarshalAs(UnmanagedType.LPWStr)] string lpString); // AnsiにすることをStringMarshallingCustomTypeで指定。なおStringMarshallingメンバーのデフォルト値はCustomなので指定する必要はありません [LibraryImport("Kernel32.dll", EntryPoint = "lstrlenA", StringMarshallingCustomType = typeof(AnsiStringMarshaller))] internal static partial int lstrlenA1(string lpString); // AnsiにすることをMarshalAsAttributeで指定 [LibraryImport("Kernel32.dll", EntryPoint = "lstrlenA")] internal static partial int lstrlenA2([MarshalAs(UnmanagedType.LPStr)] string lpString); // char[]やSpan<char>の場合は、MarshalAsAttributeでのマーシャリング方法指定はできず、StringMarshallingでの指定が必要なようです // char[]をマーシャリングする例 [LibraryImport("Kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] internal static partial int GetEnvironmentVariableW(string? lpName, char[]? lpBuffer, int nSize); // Span<char>をマーシャリングする例 [LibraryImport("Kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] internal static partial int GetEnvironmentVariableW(string? lpName, Span<char> lpBuffer, int nSize); // [out] BStr*の例。なお前述したように、LibraryImportAttributeにはHRESULTの自動エラー処理をさせる機能はないので、呼び出し元で判断する必要があります [LibraryImport("OleAut32.dll")] internal static partial int VarBstrFromI4(int lIn, int lcid, int dwFlags, [MarshalAs(UnmanagedType.BStr)] out string pbstrOut); // UTF-8の例 [LibraryImport("Kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] internal static partial int MultiByteToWideChar( int CodePage, // CP_UTF8を前提としています int dwFlags, [MarshalAs(UnmanagedType.LPUTF8Str)] string lpMultiByteStr, // UTF-8引数としてマーシャリングさせます int cbMultiByte, Span<char> lpWideCharStr, // Span型は個別のMarshalAsAttributeによる指定が出来ないので、StringMarshalling側で指定しています int cchWideChar); }
// <auto-generated/> namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int lstrlenW1(string lpString) { int __retVal; // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __lpString_native = &global::System.Runtime.InteropServices.Marshalling.Utf16StringMarshaller.GetPinnableReference(lpString)) { __retVal = __PInvoke((ushort*)__lpString_native); } return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "lstrlenW", ExactSpelling = true)] static extern unsafe int __PInvoke(ushort* lpString); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int lstrlenW2(string lpString) { int __retVal; // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __lpString_native = &global::System.Runtime.InteropServices.Marshalling.Utf16StringMarshaller.GetPinnableReference(lpString)) { __retVal = __PInvoke((ushort*)__lpString_native); } return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "lstrlenW", ExactSpelling = true)] static extern unsafe int __PInvoke(ushort* lpString); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int lstrlenA1(string lpString) { byte* __lpString_native = default; int __retVal; // Setup - Perform required setup. global::System.Runtime.InteropServices.Marshalling.AnsiStringMarshaller.ManagedToUnmanagedIn __lpString_native__marshaller = new(); try { // Marshal - Convert managed data to native data. byte* __lpString_native__stackptr = stackalloc byte[global::System.Runtime.InteropServices.Marshalling.AnsiStringMarshaller.ManagedToUnmanagedIn.BufferSize]; __lpString_native__marshaller.FromManaged(lpString, new System.Span<byte>(__lpString_native__stackptr, global::System.Runtime.InteropServices.Marshalling.AnsiStringMarshaller.ManagedToUnmanagedIn.BufferSize)); { // PinnedMarshal - Convert managed data to native data that requires the managed data to be pinned. __lpString_native = __lpString_native__marshaller.ToUnmanaged(); __retVal = __PInvoke(__lpString_native); } } finally { // Cleanup - Perform required cleanup. __lpString_native__marshaller.Free(); } return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "lstrlenA", ExactSpelling = true)] static extern unsafe int __PInvoke(byte* lpString); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int lstrlenA2(string lpString) { byte* __lpString_native = default; int __retVal; // Setup - Perform required setup. global::System.Runtime.InteropServices.Marshalling.AnsiStringMarshaller.ManagedToUnmanagedIn __lpString_native__marshaller = new(); try { // Marshal - Convert managed data to native data. byte* __lpString_native__stackptr = stackalloc byte[global::System.Runtime.InteropServices.Marshalling.AnsiStringMarshaller.ManagedToUnmanagedIn.BufferSize]; __lpString_native__marshaller.FromManaged(lpString, new System.Span<byte>(__lpString_native__stackptr, global::System.Runtime.InteropServices.Marshalling.AnsiStringMarshaller.ManagedToUnmanagedIn.BufferSize)); { // PinnedMarshal - Convert managed data to native data that requires the managed data to be pinned. __lpString_native = __lpString_native__marshaller.ToUnmanaged(); __retVal = __PInvoke(__lpString_native); } } finally { // Cleanup - Perform required cleanup. __lpString_native__marshaller.Free(); } return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "lstrlenA", ExactSpelling = true)] static extern unsafe int __PInvoke(byte* lpString); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int GetEnvironmentVariableW(string lpName, char[] lpBuffer, int nSize) { int __lastError; int __retVal; // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __lpName_native = &global::System.Runtime.InteropServices.Marshalling.Utf16StringMarshaller.GetPinnableReference(lpName)) fixed (void* __lpBuffer_native = &global::System.Runtime.InteropServices.Marshalling.ArrayMarshaller<char, char>.ManagedToUnmanagedIn.GetPinnableReference(lpBuffer)) { System.Runtime.InteropServices.Marshal.SetLastSystemError(0); __retVal = __PInvoke((ushort*)__lpName_native, (char*)__lpBuffer_native, nSize); __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError(); } System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "GetEnvironmentVariableW", ExactSpelling = true)] static extern unsafe int __PInvoke(ushort* lpName, char* lpBuffer, int nSize); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int GetEnvironmentVariableW(string lpName, global::System.Span<char> lpBuffer, int nSize) { int __lastError; int __retVal; // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __lpName_native = &global::System.Runtime.InteropServices.Marshalling.Utf16StringMarshaller.GetPinnableReference(lpName)) fixed (void* __lpBuffer_native = &global::System.Runtime.InteropServices.Marshalling.SpanMarshaller<char, char>.ManagedToUnmanagedIn.GetPinnableReference(lpBuffer)) { System.Runtime.InteropServices.Marshal.SetLastSystemError(0); __retVal = __PInvoke((ushort*)__lpName_native, (char*)__lpBuffer_native, nSize); __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError(); } System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "GetEnvironmentVariableW", ExactSpelling = true)] static extern unsafe int __PInvoke(ushort* lpName, char* lpBuffer, int nSize); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int VarBstrFromI4(int lIn, int lcid, int dwFlags, out string pbstrOut) { System.Runtime.CompilerServices.Unsafe.SkipInit(out pbstrOut); ushort* __pbstrOut_native = default; int __retVal; try { { __retVal = __PInvoke(lIn, lcid, dwFlags, &__pbstrOut_native); } // Unmarshal - Convert native data to managed data. pbstrOut = global::System.Runtime.InteropServices.Marshalling.BStrStringMarshaller.ConvertToManaged(__pbstrOut_native); } finally { // Cleanup - Perform required cleanup. global::System.Runtime.InteropServices.Marshalling.BStrStringMarshaller.Free(__pbstrOut_native); } return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("OleAut32.dll", EntryPoint = "VarBstrFromI4", ExactSpelling = true)] static extern unsafe int __PInvoke(int lIn, int lcid, int dwFlags, ushort** pbstrOut); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int MultiByteToWideChar(int CodePage, int dwFlags, string lpMultiByteStr, int cbMultiByte, global::System.Span<char> lpWideCharStr, int cchWideChar) { int __lastError; byte* __lpMultiByteStr_native = default; int __retVal; // Setup - Perform required setup. global::System.Runtime.InteropServices.Marshalling.Utf8StringMarshaller.ManagedToUnmanagedIn __lpMultiByteStr_native__marshaller = new(); try { // Marshal - Convert managed data to native data. byte* __lpMultiByteStr_native__stackptr = stackalloc byte[global::System.Runtime.InteropServices.Marshalling.Utf8StringMarshaller.ManagedToUnmanagedIn.BufferSize]; __lpMultiByteStr_native__marshaller.FromManaged(lpMultiByteStr, new System.Span<byte>(__lpMultiByteStr_native__stackptr, global::System.Runtime.InteropServices.Marshalling.Utf8StringMarshaller.ManagedToUnmanagedIn.BufferSize)); // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __lpWideCharStr_native = &global::System.Runtime.InteropServices.Marshalling.SpanMarshaller<char, char>.ManagedToUnmanagedIn.GetPinnableReference(lpWideCharStr)) { // PinnedMarshal - Convert managed data to native data that requires the managed data to be pinned. __lpMultiByteStr_native = __lpMultiByteStr_native__marshaller.ToUnmanaged(); System.Runtime.InteropServices.Marshal.SetLastSystemError(0); __retVal = __PInvoke(CodePage, dwFlags, __lpMultiByteStr_native, cbMultiByte, (char*)__lpWideCharStr_native, cchWideChar); __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError(); } } finally { // Cleanup - Perform required cleanup. __lpMultiByteStr_native__marshaller.Free(); } System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "MultiByteToWideChar", ExactSpelling = true)] static extern unsafe int __PInvoke(int CodePage, int dwFlags, byte* lpMultiByteStr, int cbMultiByte, char* lpWideCharStr, int cchWideChar); } } }
指定エンコーディング方法に応じてUtf16StringMarshaller
、BStrStringMarshaller
、Utf8StringMarshaller.ManagedToUnmanagedIn
、AnsiStringMarshaller.ManagedToUnmanagedIn
等を使用していることが分かります。また、StringMarshalling.Utf16
を指定した場合とMarshalAs(UnmanagedType.LPWStr)
を指定した場合で生成結果ソースは同一になるようです。
生成結果を観察すると、Utf8StringMarshaller.ManagedToUnmanagedIn
やAnsiStringMarshaller.ManagedToUnmanagedIn
関係の処理でBufferSize
を使用したSpan<byte>
確保があります。マーシャリング結果がそのサイズを超過する場合にどうなるか気になりましたが、Utf8StringMarshaller.csやAnsiStringMarshaller.csの実装を見ると「マーシャリング結果がSpanサイズを超える可能性がある場合はヒープから確保」する分岐があるため問題なさそうです。
UnmanagedCallConvAttributeを使用している場合は、その内容がそのまま生成結果ソースへ反映される
UnmanagedCallConvAttribute
の使用例として、呼び出し規約がcdeclであるネイティブAPIの例を示します:
using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Test; internal static partial class NativeMethods { [LibraryImport("msvcrt.dll")] [UnmanagedCallConv(CallConvs = new Type[] { typeof(CallConvCdecl) })] internal static partial int puts([MarshalAs(UnmanagedType.LPStr)] string s); [LibraryImport("msvcrt.dll")] [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })] internal static unsafe partial int memcmp(byte* buffer1, byte* buffer2, uint count); }
// <auto-generated/> namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int puts(string s) { byte* __s_native = default; int __retVal; // Setup - Perform required setup. global::System.Runtime.InteropServices.Marshalling.AnsiStringMarshaller.ManagedToUnmanagedIn __s_native__marshaller = new(); try { // Marshal - Convert managed data to native data. byte* __s_native__stackptr = stackalloc byte[global::System.Runtime.InteropServices.Marshalling.AnsiStringMarshaller.ManagedToUnmanagedIn.BufferSize]; __s_native__marshaller.FromManaged(s, new System.Span<byte>(__s_native__stackptr, global::System.Runtime.InteropServices.Marshalling.AnsiStringMarshaller.ManagedToUnmanagedIn.BufferSize)); { // PinnedMarshal - Convert managed data to native data that requires the managed data to be pinned. __s_native = __s_native__marshaller.ToUnmanaged(); __retVal = __PInvoke(__s_native); } } finally { // Cleanup - Perform required cleanup. __s_native__marshaller.Free(); } return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("msvcrt.dll", EntryPoint = "puts", ExactSpelling = true)] [System.Runtime.InteropServices.UnmanagedCallConvAttribute(CallConvs = new System.Type[]{typeof(global::System.Runtime.CompilerServices.CallConvCdecl)})] static extern unsafe int __PInvoke(byte* s); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.Runtime.InteropServices.DllImportAttribute("msvcrt.dll", EntryPoint = "memcmp", ExactSpelling = true)] internal static unsafe extern partial int memcmp(byte* buffer1, byte* buffer2, uint count); } }
puts
の場合は、最終的にP/Invokeを行う__PInvoke
メソッドにUnmanagedCallConvAttribute
が適用されていることが分かります。
memcmp
の場合は、一見UnmanagedCallConvAttribute
が適用されていないように見えます。しかし実際は、partialメソッドそのものにDllImportAttribute
が適用されているため、C#言語仕様のAttributesの21.2.3 Positional and named parameters
にThe attributes of a type declared in multiple parts are determined by combining
とある通り、自身で記述したUnmanagedCallConvAttribute
が適用されます。そのため問題ありません。
独自マーシャラーの実装が必要な用途の例
本項目では、独自マーシャラーの実装方法と、独自マーシャラーを使用した場合の生成結果ソースについて解説します。
サンプル: 独自マーシャラーの実装方法
独自マーシャルの実装には以下の型が関わります:
- CustomMarshallerAttribute Class
- MarshalUsingAttribute Class
- NativeMarshallingAttribute Class
- MarshalMode Enum
これらの属性や列挙型は以下のように使用します:
using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; namespace Test; // NativeMarshallingAttributeが指定されている場合は、LibraryImportAttibute付きメソッドの引数でこの型が使われている場合にMarshalUsingAttributeを省略できます。 // その場合はNativeMarshallingAttributeで指定した独自マーシャラーが使用されます。 [NativeMarshalling(typeof(FooMarshaller))] internal struct FooManaged { internal int Bar; } // CustomMarshallerAttributeを指定して、独自マーシャラーを作成します。 // 状態が不要な場合はstatic classに、状態が必要な場合はstructにします。 // MarshalModeの値に応じて、必要なメソッドは異なります。 [CustomMarshaller(typeof(FooManaged), MarshalMode.Default, typeof(FooMarshaller))] internal static class FooMarshaller { internal struct FooUnmanaged { internal int Bar; } public static FooUnmanaged ConvertToUnmanaged(FooManaged managed) { // Blittableメンバーはコピーし、そうでないメンバーは適宜変換します return new FooUnmanaged() { Bar = managed.Bar }; } public static FooManaged ConvertToManaged(FooUnmanaged unmanaged) { // Blittableメンバーはコピーし、そうでないメンバーは適宜変換します return new FooManaged() { Bar = unmanaged.Bar }; } } internal static partial class NativeMethods { // FooManaged型にはNativeMarshallerAttributeが適用されているため、引数でのMarshalUsingAttribute指定は任意です。 [LibraryImport("Sample")] internal static partial void Test1(FooManaged managed); // MarshalUsingAttributeを明記することも出来ます。 // もしFooManaged型にNativeMarshallerAttributeが適用されていない場合は、MarshalUsingAttribute指定は必須です。 [LibraryImport("Sample")] internal static partial void Test2([MarshalUsing(typeof(FooMarshaller))] FooManaged managed); }
なお、CustomMarshallerAttribute
の第3引数の型にカーソルを合わせてQuick Actionを使えば、「エラーは出なくなる」メソッド定義を生成できます:
ただしほとんどの場合に、Unmanaged側はnint
ではなく独自のBlittable型に変更する必要があるでしょう。また、必要に応じてFree(Unmanaged)
メソッド等も追加する必要があるでしょう。
なお、Quick Actionで生成されるメソッドは、CustomMarshallerAttribute
の第2引数で指定するMarshalMode
の値で変化します。いろいろ試してみると学習が進みやすいと思います。
ちなみにですが、MSDNに独自マーシャラー実装関連の記事が存在します:
説明はあるのですが、残念ながら肝心のサンプルコードのメソッド内容の多くがthrow new NotImplementedException
になっています。実践的なサンプルコードになれば確実に参考になると思うので、改善されることを期待しています。また、サンプルコードを見るにList<T>
をマーシャリングできるようで、夢が広がりそうです。(とはいえSpan<T>
のマーシャリングで大体事足りるかも?)
Blittableな値型の独自マーシャリングはボイラープレートになってしまう
値型を独自マーシャリングする場合は、マーシャリング対象の型と、Bittable型の間を相互変換する形になります。正直サイズを合わせてひたすらコピーするだけです。Unsafe
やMemoryMarshal
型のメソッドで多少は楽ができますが、どうしても手間を感じてしまいます:
using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; namespace Test; [CustomMarshaller(typeof(Guid), MarshalMode.Default, typeof(GuidMarshaller))] internal static class GuidMarshaller { internal unsafe struct GuidUnmanaged { internal fixed byte data[16]; } static unsafe GuidMarshaller() { if (sizeof(Guid) != sizeof(GuidUnmanaged)) { throw new InvalidOperationException(); } } [SkipLocalsInit] public static GuidUnmanaged ConvertToUnmanaged(in Guid managed) { Unsafe.SkipInit<GuidUnmanaged>(out var unmanaged); var source = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref Unsafe.AsRef(managed), 1)); var destination = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref unmanaged, 1)); source.CopyTo(destination); return unmanaged; } [SkipLocalsInit] public static Guid ConvertToManaged(in GuidUnmanaged unmanaged) { Unsafe.SkipInit<Guid>(out var managed); var source = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref Unsafe.AsRef(unmanaged), 1)); var destination = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref Unsafe.AsRef(managed), 1)); source.CopyTo(destination); return managed; } // 値型をマーシャリングする場合は、残念ながらGetPinnableReferenceは呼び出されないようです public static ref readonly byte GetPinnableReference(in Guid guid) { return ref Unsafe.As<Guid, byte>(ref Unsafe.AsRef(in guid)); } } internal static partial class NativeMethods { [LibraryImport("Ole32.dll", EntryPoint = "IsEqualGUID")] [return: MarshalAs(UnmanagedType.Bool)] internal static unsafe partial bool IsEqualGUID_UsePointer( Guid* rguid1, Guid* rguid2); [LibraryImport("Ole32.dll", EntryPoint = "IsEqualGUID")] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool IsEqualGUID_UseInRef( [MarshalUsing(typeof(GuidMarshaller))] in Guid rguid1, [MarshalUsing(typeof(GuidMarshaller))] ref Guid rguid2); [LibraryImport("Ole32.dll", SetLastError = true)] internal static partial int IIDFromString( [MarshalAs(UnmanagedType.LPWStr)] string lpsz, [MarshalUsing(typeof(GuidMarshaller))] out Guid pclsid); }
// <auto-generated/> namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static unsafe partial bool IsEqualGUID_UsePointer(global::System.Guid* rguid1, global::System.Guid* rguid2) { bool __retVal; int __retVal_native; { __retVal_native = __PInvoke(rguid1, rguid2); } // Unmarshal - Convert native data to managed data. __retVal = __retVal_native != 0; return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Ole32.dll", EntryPoint = "IsEqualGUID", ExactSpelling = true)] static extern unsafe int __PInvoke(global::System.Guid* rguid1, global::System.Guid* rguid2); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial bool IsEqualGUID_UseInRef(in global::System.Guid rguid1, ref global::System.Guid rguid2) { global::Test.GuidMarshaller.GuidUnmanaged __rguid1_native; global::Test.GuidMarshaller.GuidUnmanaged __rguid2_native; bool __retVal; int __retVal_native; // Marshal - Convert managed data to native data. __rguid1_native = global::Test.GuidMarshaller.ConvertToUnmanaged(rguid1); __rguid2_native = global::Test.GuidMarshaller.ConvertToUnmanaged(rguid2); { __retVal_native = __PInvoke(&__rguid1_native, &__rguid2_native); } // Unmarshal - Convert native data to managed data. __retVal = __retVal_native != 0; rguid2 = global::Test.GuidMarshaller.ConvertToManaged(__rguid2_native); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Ole32.dll", EntryPoint = "IsEqualGUID", ExactSpelling = true)] static extern unsafe int __PInvoke(global::Test.GuidMarshaller.GuidUnmanaged* rguid1, global::Test.GuidMarshaller.GuidUnmanaged* rguid2); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial int IIDFromString(string lpsz, out global::System.Guid pclsid) { int __lastError; System.Runtime.CompilerServices.Unsafe.SkipInit(out pclsid); global::Test.GuidMarshaller.GuidUnmanaged __pclsid_native; int __retVal; // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __lpsz_native = &global::System.Runtime.InteropServices.Marshalling.Utf16StringMarshaller.GetPinnableReference(lpsz)) { System.Runtime.InteropServices.Marshal.SetLastSystemError(0); __retVal = __PInvoke((ushort*)__lpsz_native, &__pclsid_native); __lastError = System.Runtime.InteropServices.Marshal.GetLastSystemError(); } // Unmarshal - Convert native data to managed data. pclsid = global::Test.GuidMarshaller.ConvertToManaged(__pclsid_native); System.Runtime.InteropServices.Marshal.SetLastPInvokeError(__lastError); return __retVal; // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Ole32.dll", EntryPoint = "IIDFromString", ExactSpelling = true)] static extern unsafe int __PInvoke(ushort* lpsz, global::Test.GuidMarshaller.GuidUnmanaged* pclsid); } } }
ref引数やout引数、in引数それぞれについて、MarshalUsingAttribute
で指定した型のConvertToUnmanaged
とConvertToManaged
が適切に呼び出されていることが分かります。
ただし、Managed型とUnmanaged型との相互変換のために、どうしてもコピーが必要になります。効率を求める場合は、DllImportAttribute
適用時点で最初からポインター型でやり取りする必要がありそうです。
メンバーに文字列等のマーシャリングを含む場合は手動でそれぞれマーシャリングが必要
大量のボイラープレートが必要になります……:
using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; namespace Test; [NativeMarshalling(typeof(STARTUPINFOUnicodeMarshaller))] internal struct STARTUPINFO { internal int cb; internal string? lpReserved; internal string? lpDesktop; internal string? lpTitle; internal int dwX; internal int dwY; internal int dwXSize; internal int dwYSize; internal int dwXCountChars; internal int dwYCountChars; internal int dwFillAttribute; internal int dwFlags; internal short wShowWindow; internal short cbReserved2; internal IntPtr lpReserved2; internal IntPtr hStdInput; internal IntPtr hStdOutput; internal IntPtr hStdError; } [CustomMarshaller(typeof(STARTUPINFO), MarshalMode.Default, typeof(STARTUPINFOUnicodeMarshaller))] internal unsafe ref struct STARTUPINFOUnicodeMarshaller { internal ref struct STARTUPINFOUnmanaged { internal int cb; internal ushort* lpReserved; internal ushort* lpDesktop; internal ushort* lpTitle; internal int dwX; internal int dwY; internal int dwXSize; internal int dwYSize; internal int dwXCountChars; internal int dwYCountChars; internal int dwFillAttribute; internal int dwFlags; internal short wShowWindow; internal short cbReserved2; internal IntPtr lpReserved2; internal IntPtr hStdInput; internal IntPtr hStdOutput; internal IntPtr hStdError; } private bool _allocated; private STARTUPINFOUnmanaged _unmanaged; private STARTUPINFO _managed; public void FromManaged(in STARTUPINFO managed) { _allocated = true; _unmanaged = new STARTUPINFOUnmanaged() { cb = managed.cb, lpReserved = Utf16StringMarshaller.ConvertToUnmanaged(managed.lpReserved), lpDesktop = Utf16StringMarshaller.ConvertToUnmanaged(managed.lpDesktop), lpTitle = Utf16StringMarshaller.ConvertToUnmanaged(managed.lpTitle), dwX = managed.dwX, dwY = managed.dwY, dwXSize = managed.dwXSize, dwYSize = managed.dwYSize, dwXCountChars = managed.dwXCountChars, dwYCountChars = managed.dwYCountChars, dwFlags = managed.dwFlags, wShowWindow = managed.wShowWindow, cbReserved2 = managed.cbReserved2, lpReserved2 = managed.lpReserved2, hStdInput = managed.hStdInput, hStdOutput = managed.hStdOutput, hStdError = managed.hStdError, }; } public STARTUPINFOUnmanaged ToUnmanaged() => _unmanaged; public void FromUnmanaged(in STARTUPINFOUnmanaged unmanaged) { _managed = new STARTUPINFO() { cb = unmanaged.cb, lpReserved = Utf16StringMarshaller.ConvertToManaged(unmanaged.lpReserved), lpDesktop = Utf16StringMarshaller.ConvertToManaged(unmanaged.lpDesktop), lpTitle = Utf16StringMarshaller.ConvertToManaged(unmanaged.lpTitle), dwX = unmanaged.dwX, dwY = unmanaged.dwY, dwXSize = unmanaged.dwXSize, dwYSize = unmanaged.dwYSize, dwXCountChars = unmanaged.dwXCountChars, dwYCountChars = unmanaged.dwYCountChars, dwFlags = unmanaged.dwFlags, wShowWindow = unmanaged.wShowWindow, cbReserved2 = unmanaged.cbReserved2, lpReserved2 = unmanaged.lpReserved2, hStdInput = unmanaged.hStdInput, hStdOutput = unmanaged.hStdOutput, hStdError = unmanaged.hStdError, }; } public STARTUPINFO ToManaged() => _managed; public void Free() { // ConvertToUnmanagedを使っていた場合にだけ解放する必要があります。 if (_allocated) { Utf16StringMarshaller.Free(_unmanaged.lpReserved); Utf16StringMarshaller.Free(_unmanaged.lpDesktop); Utf16StringMarshaller.Free(_unmanaged.lpTitle); } } } internal static partial class NativeMethods { [LibraryImport("Kernel32.dll", EntryPoint = "GetStartupInfoW")] internal static partial void GetStartupInfo(out STARTUPINFO lpStartupInfo); }
namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial void GetStartupInfo(out global::Test.STARTUPINFO lpStartupInfo) { System.Runtime.CompilerServices.Unsafe.SkipInit(out lpStartupInfo); global::Test.STARTUPINFOUnicodeMarshaller.STARTUPINFOUnmanaged __lpStartupInfo_native = default; // Setup - Perform required setup. global::Test.STARTUPINFOUnicodeMarshaller __lpStartupInfo_native__marshaller = new(); try { { __PInvoke(&__lpStartupInfo_native); } // UnmarshalCapture - Capture the native data into marshaller instances in case conversion to managed data throws an exception. __lpStartupInfo_native__marshaller.FromUnmanaged(__lpStartupInfo_native); // Unmarshal - Convert native data to managed data. lpStartupInfo = __lpStartupInfo_native__marshaller.ToManaged(); } finally { // Cleanup - Perform required cleanup. __lpStartupInfo_native__marshaller.Free(); } // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "GetStartupInfoW", ExactSpelling = true)] static extern unsafe void __PInvoke(global::Test.STARTUPINFOUnicodeMarshaller.STARTUPINFOUnmanaged* lpStartupInfo); } } }
static classな独自マーシャラーではネイティブAPIから返ったBlittable構造体についてFree
を呼び出すべきかどうかが判断できないように思ったので、structな独自マーシャラーにして状態を持たせています。string
型をUtf16StringMarshaller
で変換するだけの独自マーシャラーなら十分自動生成できそうです。このような独自マーシャラーを生成してくれるソースジェネレーターが欲しくなりました。
参照型の独自マーシャラーではGetPinnableReferenceがあれば呼ばれる模様
using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; namespace Test; // この例ではclassであることに注目してください。 [StructLayout(LayoutKind.Sequential)] internal class SYSTEMTIME { internal short wYear; internal short wMonth; internal short wDayOfWeek; internal short wDay; internal short wHour; internal short wMinute; internal short wSecond; internal short wMilliseconds; } [CustomMarshaller(typeof(SYSTEMTIME), MarshalMode.Default, typeof(ClassSystemTimeMarshallerWithPinnableReference))] internal static class ClassSystemTimeMarshallerWithPinnableReference { // 今回の例ではConvertToUnmanaged/Managedは使われないので手抜きをしています。 public static nint ConvertToUnmanaged(SYSTEMTIME managed) => throw new NotSupportedException(); public static SYSTEMTIME ConvertToManaged(nint unmanaged) => throw new NotSupportedException(); public static ref short GetPinnableReference(SYSTEMTIME managed) => ref managed.wYear; } [CustomMarshaller(typeof(SYSTEMTIME), MarshalMode.Default, typeof(ClassSystemTimeMarshallerWithoutPinnableReference))] internal static class ClassSystemTimeMarshallerWithoutPinnableReference { internal unsafe struct SYSTEMTIMEUnmanaged { public fixed short data[8]; } unsafe static ClassSystemTimeMarshallerWithoutPinnableReference() { // sizeof(参照型)はsizeof(IntPtr)の値と同じになるので、アンマネージ領域サイズをM取れるarshal.SizeOfが必要です。 if (Marshal.SizeOf<SYSTEMTIME>() != sizeof(SYSTEMTIMEUnmanaged)) { throw new InvalidOperationException(); } } public static SYSTEMTIMEUnmanaged ConvertToUnmanaged(SYSTEMTIME managed) { return Unsafe.As<short, SYSTEMTIMEUnmanaged>(ref managed.wYear); } public static SYSTEMTIME ConvertToManaged(SYSTEMTIMEUnmanaged unmanaged) { var result = new SYSTEMTIME(); Unsafe.As<short, SYSTEMTIMEUnmanaged>(ref result.wYear) = unmanaged; return result; } } internal static partial class NativeMethods { // GetPinnableReferenceがあればポインターに変換してネイティブAPIへ渡してくれます。 [LibraryImport("Kernel32.dll", EntryPoint = "GetLocalTime")] internal static partial void GetLocalTime_WithPinnableReference( [MarshalUsing(typeof(ClassSystemTimeMarshallerWithPinnableReference))] SYSTEMTIME lpSystemTime); // GetPinnableReferenceがなく、かつrefやoutの指定等がない場合はConvertToManagedが呼ばれないため、取得結果が反映されません。 [LibraryImport("Kernel32.dll", EntryPoint = "GetLocalTime")] internal static partial void GetLocalTime_WithoutPinnableReference_Bad( [MarshalUsing(typeof(ClassSystemTimeMarshallerWithoutPinnableReference))] SYSTEMTIME lpSystemTime); // GetPinnableReferenceがなく、かつrefやoutの指定がある場合はConvertToManagedが呼ばれるため、正しく取得結果が反映されます。 [LibraryImport("Kernel32.dll", EntryPoint = "GetLocalTime")] internal static partial void GetLocalTime_WithoutPinnableReference_Good( [MarshalUsing(typeof(ClassSystemTimeMarshallerWithoutPinnableReference))] out SYSTEMTIME lpSystemTime); }
// <auto-generated/> namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial void GetLocalTime_WithPinnableReference(global::Test.SYSTEMTIME lpSystemTime) { // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __lpSystemTime_native = &global::Test.ClassSystemTimeMarshallerWithPinnableReference.GetPinnableReference(lpSystemTime)) { __PInvoke((nint)__lpSystemTime_native); } // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "GetLocalTime", ExactSpelling = true)] static extern unsafe void __PInvoke(nint lpSystemTime); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial void GetLocalTime_WithoutPinnableReference_Bad(global::Test.SYSTEMTIME lpSystemTime) { global::Test.ClassSystemTimeMarshallerWithoutPinnableReference.SYSTEMTIMEUnmanaged __lpSystemTime_native; // Marshal - Convert managed data to native data. __lpSystemTime_native = global::Test.ClassSystemTimeMarshallerWithoutPinnableReference.ConvertToUnmanaged(lpSystemTime); { __PInvoke(__lpSystemTime_native); } // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "GetLocalTime", ExactSpelling = true)] static extern unsafe void __PInvoke(global::Test.ClassSystemTimeMarshallerWithoutPinnableReference.SYSTEMTIMEUnmanaged lpSystemTime); } } } namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial void GetLocalTime_WithoutPinnableReference_Good(out global::Test.SYSTEMTIME lpSystemTime) { System.Runtime.CompilerServices.Unsafe.SkipInit(out lpSystemTime); global::Test.ClassSystemTimeMarshallerWithoutPinnableReference.SYSTEMTIMEUnmanaged __lpSystemTime_native; { __PInvoke(&__lpSystemTime_native); } // Unmarshal - Convert native data to managed data. lpSystemTime = global::Test.ClassSystemTimeMarshallerWithoutPinnableReference.ConvertToManaged(__lpSystemTime_native); // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Kernel32.dll", EntryPoint = "GetLocalTime", ExactSpelling = true)] static extern unsafe void __PInvoke(global::Test.ClassSystemTimeMarshallerWithoutPinnableReference.SYSTEMTIMEUnmanaged* lpSystemTime); } } }
ClassSystemTimeMarshallerWithPinnableReference
では、GetPinnableReference
経由でfixedされていることが分かります。一方でClassSystemTimeMarshallerWithoutPinnableReference
では、classの値渡しをしている場合では、Unmanagedへ変換されるだけで、取得結果をManagedへ戻していないことがわかります。classであったとしても、適切にin、out、refを指定する必要があることが分かります。
おまけ: 生成ソースから呼び出される独自マーシャラーの各種メソッド色々乗せ
runtime/UserTypeMarshallingV2.md at main · dotnet/runtimeに、独自マーシャラーが満たすべき要件が記述されています。各メソッドが例外を送出可能かどうかも記載があるので、一読をおすすめします。また、本記事では扱いませんがLinear (Array-like) Collection Marshaller Shapes
についての言及もあります(Span<T>
のデフォルトマーシャリングで事足りる可能性はあります)。
当該記事を見ながら、Value Marshaller Shapes
について色々試しました。ただ、with Caller Allocated Buffer
版でref引数を使った場合の生成ソースが明らかにおかしいもの(報告済み)だったりしています。GetPinnableReference
が呼び出されなかったり生成結果コードがエラーを起こしたりするものは、自分の書き方が変な可能性を捨てきれないので未報告です……:
using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; namespace Test; internal struct Managed { /* 簡単のためにメンバーなしです */ } internal struct Unmanaged { /* 簡単のためにメンバーなしです */ } [CustomMarshaller(typeof(Managed), MarshalMode.Default, typeof(StatelessStructWithoutBufferMarshaller))] internal static class StatelessStructWithoutBufferMarshaller { // 値渡し、in引数、ref引数に使用されます public static Unmanaged ConvertToUnmanaged(Managed managed) => new(); // out引数、ref引数に使用されます public static Managed ConvertToManaged(Unmanaged unmanaged) => new(); // out引数、ref引数に使用されます public static Managed ConvertToManagedFinally(Unmanaged unmanaged) => new(); // 値渡し、in引数、out引数、ref引数の全てに使用されます public static void Free(Unmanaged unmanaged) { } } // 「Stateless Managed->Unmanaged」 // 「Stateless Managed->Unmanaged with Caller-Allocated Buffer」 // 「Stateless Unmanaged->Managed」 // 「Stateless Unmanaged->Managed with Guaranteed Unmarshalling」 [CustomMarshaller(typeof(Managed), MarshalMode.Default, typeof(StatelessStructWithBufferMarshaller))] internal static class StatelessStructWithBufferMarshaller { // ソース生成処理としては、ConvertToUnmanaged(ValueManaged , Span<byte>)が存在する場合に、BufferSizeを取得しようとする処理が生成されるようです // 値渡し、in引数用のstackalloc byteのサイズとして使用されます public static int BufferSize => 16; // ref引数に使用されます(MSDNドキュメントには記載がないので仕様かどうか不明、struct版のref引数の動作が怪しかったのでそちらは報告している、ついでに備考として本現象も記述はしました) public static Unmanaged ConvertToUnmanaged(Managed managed) => new(); // 値渡し、in引数に使用されます public static Unmanaged ConvertToUnmanaged(Managed managed, Span<byte> callerAllocatedBuffer) => new(); // out引数、ref引数に使用されます public static Managed ConvertToManaged(Unmanaged unmanaged) => new(); // out引数、ref引数に使用されます public static Managed ConvertToManagedFinally(Unmanaged unmanaged) => new(); // 値渡し、in引数、out引数、ref引数の全てに使用されます public static void Free(Unmanaged unmanaged) { } } // 「Stateful Managed->Unmanaged」 // 「Stateful Unmanaged->Managed」 // 「Stateful Unmanaged->Managed with Guaranteed Unmarshalling」 [CustomMarshaller(typeof(Managed), MarshalMode.Default, typeof(StatefullStructWithoutBufferMarshaller))] internal struct StatefullStructWithoutBufferMarshaller { // 値渡し、in引数、ref引数に使用されます public void FromManaged(Managed managed) { } // UserTypeMarshallingV2では記載がありますが、使用される条件が分かりませんでした…… public ref byte GetPinnableReference() => ref Unsafe.NullRef<byte>(); // これがあると、値渡しのマーシャリング時にvoid*をStructUnmanagedへ変換しようとしてエラーを起こします…… // public static ref StructUnmanaged GetPinnableReference(StructManaged managed) => ref Unsafe.NullRef<StructUnmanaged>(); // 値渡し、in引数、ref引数に使用されます public Unmanaged ToUnmanaged() => new(); // 値渡し、in引数、ref引数に使用されます public void OnInvoked() { } // out引数、ref引数に使用されます public void FromUnmanaged(Unmanaged unmanaged) { } // out引数、ref引数に使用されます public Managed ToManaged() => new(); // out引数、ref引数に使用されます public Managed ToManagedFinally() => new(); // 値渡し、in引数、out引数、ref引数の全てに使用されます public void Free() { } } // 「Stateful Managed->Unmanaged with Caller Allocated Buffer」 // 「Stateful Unmanaged->Managed」 // 「Stateful Unmanaged->Managed with Guaranteed Unmarshalling」 [CustomMarshaller(typeof(Managed), MarshalMode.Default, typeof(StatefullStructWithBufferMarshaller))] internal struct StatefullStructWithBufferMarshaller { // 値渡し、in引数用のstackalloc byteのサイズとして使用されます public static int BufferSize => 16; // 値渡し、in引数に使用されます。ref引数で使用されないのはバグに見えたので報告済みです。 public void FromManaged(Managed managed, Span<byte> buffer) { } // UserTypeMarshallingV2では記載がありますが、使用される条件が分かりませんでした…… public ref byte GetPinnableReference() => ref Unsafe.NullRef<byte>(); // これがあると、値渡しのマーシャリング時にvoid*をStructUnmanagedへ変換しようとしてエラーを起こします…… // public static ref StructUnmanaged GetPinnableReference(StructManaged managed) => ref Unsafe.NullRef<StructUnmanaged>(); // 値渡し、in引数、ref引数に使用されます public Unmanaged ToUnmanaged() => new(); // 値渡し、in引数、ref引数に使用されます public void OnInvoked() { } // out引数、ref引数に使用されます public void FromUnmanaged(Unmanaged unmanaged) { } // out引数、ref引数に使用されます public Managed ToManaged() => new(); // out引数、ref引数に使用されます public Managed ToManagedFinally() => new(); // 値渡し、in引数、out引数、ref引数の全てに使用されます public void Free() { } } internal static partial class NativeMethods { // 1: static class、BufferSizeプロパティなし // 2: static class、BufferSizeプロパティあり // 3: struct, BufferSizeプロパティなし // 4: struct, BufferSizeプロパティなし [LibraryImport("Sample")] internal static partial void Sample( [MarshalUsing(typeof(StatelessStructWithoutBufferMarshaller))] Managed value1, [MarshalUsing(typeof(StatelessStructWithoutBufferMarshaller))] in Managed in1, [MarshalUsing(typeof(StatelessStructWithoutBufferMarshaller))] out Managed out1, [MarshalUsing(typeof(StatelessStructWithoutBufferMarshaller))] ref Managed ref1, [MarshalUsing(typeof(StatelessStructWithBufferMarshaller))] Managed value2, [MarshalUsing(typeof(StatelessStructWithBufferMarshaller))] in Managed in2, [MarshalUsing(typeof(StatelessStructWithBufferMarshaller))] out Managed out2, [MarshalUsing(typeof(StatelessStructWithBufferMarshaller))] ref Managed ref2, [MarshalUsing(typeof(StatefullStructWithoutBufferMarshaller))] Managed value3, [MarshalUsing(typeof(StatefullStructWithoutBufferMarshaller))] in Managed in3, [MarshalUsing(typeof(StatefullStructWithoutBufferMarshaller))] out Managed out3, [MarshalUsing(typeof(StatefullStructWithoutBufferMarshaller))] ref Managed ref3, [MarshalUsing(typeof(StatefullStructWithBufferMarshaller))] Managed value4, [MarshalUsing(typeof(StatefullStructWithBufferMarshaller))] in Managed in4, [MarshalUsing(typeof(StatefullStructWithBufferMarshaller))] out Managed out4, [MarshalUsing(typeof(StatefullStructWithBufferMarshaller))] ref Managed ref4); }
// <auto-generated/> namespace Test { internal static unsafe partial class NativeMethods { [System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "7.0.7.6804")] [System.Runtime.CompilerServices.SkipLocalsInitAttribute] internal static partial void Sample(global::Test.Managed value1, in global::Test.Managed in1, out global::Test.Managed out1, ref global::Test.Managed ref1, global::Test.Managed value2, in global::Test.Managed in2, out global::Test.Managed out2, ref global::Test.Managed ref2, global::Test.Managed value3, in global::Test.Managed in3, out global::Test.Managed out3, ref global::Test.Managed ref3, global::Test.Managed value4, in global::Test.Managed in4, out global::Test.Managed out4, ref global::Test.Managed ref4) { bool __invokeSucceeded = default; System.Runtime.CompilerServices.Unsafe.SkipInit(out out1); System.Runtime.CompilerServices.Unsafe.SkipInit(out out2); System.Runtime.CompilerServices.Unsafe.SkipInit(out out3); System.Runtime.CompilerServices.Unsafe.SkipInit(out out4); global::Test.Unmanaged __value1_native = default; global::Test.Unmanaged __in1_native = default; global::Test.Unmanaged __out1_native = default; global::Test.Unmanaged __ref1_native = default; global::Test.Unmanaged __value2_native = default; global::Test.Unmanaged __in2_native = default; global::Test.Unmanaged __out2_native = default; global::Test.Unmanaged __ref2_native = default; global::Test.Unmanaged __value3_native = default; global::Test.Unmanaged __in3_native = default; global::Test.Unmanaged __out3_native = default; global::Test.Unmanaged __ref3_native = default; global::Test.Unmanaged __value4_native = default; global::Test.Unmanaged __in4_native = default; global::Test.Unmanaged __out4_native = default; global::Test.Unmanaged __ref4_native = default; // Setup - Perform required setup. global::Test.StatefullStructWithoutBufferMarshaller __value3_native__marshaller = new(); global::Test.StatefullStructWithoutBufferMarshaller __in3_native__marshaller = new(); global::Test.StatefullStructWithoutBufferMarshaller __out3_native__marshaller = new(); global::Test.StatefullStructWithoutBufferMarshaller __ref3_native__marshaller = new(); global::Test.StatefullStructWithBufferMarshaller __value4_native__marshaller = new(); global::Test.StatefullStructWithBufferMarshaller __in4_native__marshaller = new(); global::Test.StatefullStructWithBufferMarshaller __out4_native__marshaller = new(); global::Test.StatefullStructWithBufferMarshaller __ref4_native__marshaller = new(); try { // Marshal - Convert managed data to native data. __value1_native = global::Test.StatelessStructWithoutBufferMarshaller.ConvertToUnmanaged(value1); __in1_native = global::Test.StatelessStructWithoutBufferMarshaller.ConvertToUnmanaged(in1); __ref1_native = global::Test.StatelessStructWithoutBufferMarshaller.ConvertToUnmanaged(ref1); System.Span<byte> __value2_native__buffer = stackalloc byte[global::Test.StatelessStructWithBufferMarshaller.BufferSize]; __value2_native = global::Test.StatelessStructWithBufferMarshaller.ConvertToUnmanaged(value2, __value2_native__buffer); System.Span<byte> __in2_native__buffer = stackalloc byte[global::Test.StatelessStructWithBufferMarshaller.BufferSize]; __in2_native = global::Test.StatelessStructWithBufferMarshaller.ConvertToUnmanaged(in2, __in2_native__buffer); __ref2_native = global::Test.StatelessStructWithBufferMarshaller.ConvertToUnmanaged(ref2); __value3_native__marshaller.FromManaged(value3); __in3_native__marshaller.FromManaged(in3); __ref3_native__marshaller.FromManaged(ref3); byte* __value4_native__stackptr = stackalloc byte[global::Test.StatefullStructWithBufferMarshaller.BufferSize]; __value4_native__marshaller.FromManaged(value4, new System.Span<byte>(__value4_native__stackptr, global::Test.StatefullStructWithBufferMarshaller.BufferSize)); byte* __in4_native__stackptr = stackalloc byte[global::Test.StatefullStructWithBufferMarshaller.BufferSize]; __in4_native__marshaller.FromManaged(in4, new System.Span<byte>(__in4_native__stackptr, global::Test.StatefullStructWithBufferMarshaller.BufferSize)); // Pin - Pin data in preparation for calling the P/Invoke. fixed (void* __value3_native__unused = __value3_native__marshaller) fixed (void* __in3_native__unused = __in3_native__marshaller) fixed (void* __value4_native__unused = __value4_native__marshaller) fixed (void* __in4_native__unused = __in4_native__marshaller) { // PinnedMarshal - Convert managed data to native data that requires the managed data to be pinned. __value3_native = __value3_native__marshaller.ToUnmanaged(); __in3_native = __in3_native__marshaller.ToUnmanaged(); __ref3_native = __ref3_native__marshaller.ToUnmanaged(); __value4_native = __value4_native__marshaller.ToUnmanaged(); __in4_native = __in4_native__marshaller.ToUnmanaged(); __ref4_native = __ref4_native__marshaller.ToUnmanaged(); __PInvoke(__value1_native, &__in1_native, &__out1_native, &__ref1_native, __value2_native, &__in2_native, &__out2_native, &__ref2_native, __value3_native, &__in3_native, &__out3_native, &__ref3_native, __value4_native, &__in4_native, &__out4_native, &__ref4_native); } __invokeSucceeded = true; // NotifyForSuccessfulInvoke - Keep alive any managed objects that need to stay alive across the call. __value3_native__marshaller.OnInvoked(); __in3_native__marshaller.OnInvoked(); __ref3_native__marshaller.OnInvoked(); __value4_native__marshaller.OnInvoked(); __in4_native__marshaller.OnInvoked(); __ref4_native__marshaller.OnInvoked(); // UnmarshalCapture - Capture the native data into marshaller instances in case conversion to managed data throws an exception. __ref4_native__marshaller.FromUnmanaged(__ref4_native); __out4_native__marshaller.FromUnmanaged(__out4_native); __ref3_native__marshaller.FromUnmanaged(__ref3_native); __out3_native__marshaller.FromUnmanaged(__out3_native); // Unmarshal - Convert native data to managed data. ref4 = __ref4_native__marshaller.ToManaged(); out4 = __out4_native__marshaller.ToManaged(); ref3 = __ref3_native__marshaller.ToManaged(); out3 = __out3_native__marshaller.ToManaged(); ref2 = global::Test.StatelessStructWithBufferMarshaller.ConvertToManaged(__ref2_native); out2 = global::Test.StatelessStructWithBufferMarshaller.ConvertToManaged(__out2_native); ref1 = global::Test.StatelessStructWithoutBufferMarshaller.ConvertToManaged(__ref1_native); out1 = global::Test.StatelessStructWithoutBufferMarshaller.ConvertToManaged(__out1_native); } finally { if (__invokeSucceeded) { // GuaranteedUnmarshal - Convert native data to managed data even in the case of an exception during the non-cleanup phases. ref4 = __ref4_native__marshaller.ToManagedFinally(); out4 = __out4_native__marshaller.ToManagedFinally(); ref3 = __ref3_native__marshaller.ToManagedFinally(); out3 = __out3_native__marshaller.ToManagedFinally(); ref2 = global::Test.StatelessStructWithBufferMarshaller.ConvertToManagedFinally(__ref2_native); out2 = global::Test.StatelessStructWithBufferMarshaller.ConvertToManagedFinally(__out2_native); ref1 = global::Test.StatelessStructWithoutBufferMarshaller.ConvertToManagedFinally(__ref1_native); out1 = global::Test.StatelessStructWithoutBufferMarshaller.ConvertToManagedFinally(__out1_native); } // Cleanup - Perform required cleanup. global::Test.StatelessStructWithoutBufferMarshaller.Free(__value1_native); global::Test.StatelessStructWithoutBufferMarshaller.Free(__in1_native); global::Test.StatelessStructWithoutBufferMarshaller.Free(__out1_native); global::Test.StatelessStructWithoutBufferMarshaller.Free(__ref1_native); global::Test.StatelessStructWithBufferMarshaller.Free(__value2_native); global::Test.StatelessStructWithBufferMarshaller.Free(__in2_native); global::Test.StatelessStructWithBufferMarshaller.Free(__out2_native); global::Test.StatelessStructWithBufferMarshaller.Free(__ref2_native); __value3_native__marshaller.Free(); __in3_native__marshaller.Free(); __out3_native__marshaller.Free(); __ref3_native__marshaller.Free(); __value4_native__marshaller.Free(); __in4_native__marshaller.Free(); __out4_native__marshaller.Free(); __ref4_native__marshaller.Free(); } // Local P/Invoke [System.Runtime.InteropServices.DllImportAttribute("Sample", EntryPoint = "Sample", ExactSpelling = true)] static extern unsafe void __PInvoke(global::Test.Unmanaged value1, global::Test.Unmanaged* in1, global::Test.Unmanaged* out1, global::Test.Unmanaged* ref1, global::Test.Unmanaged value2, global::Test.Unmanaged* in2, global::Test.Unmanaged* out2, global::Test.Unmanaged* ref2, global::Test.Unmanaged value3, global::Test.Unmanaged* in3, global::Test.Unmanaged* out3, global::Test.Unmanaged* ref3, global::Test.Unmanaged value4, global::Test.Unmanaged* in4, global::Test.Unmanaged* out4, global::Test.Unmanaged* ref4); } } }
コメントも生成されているので、各メソッドが行うべき動作は大体は推測できると思います。また、P/Invokeする際に、値渡し引数は値のまま、in/out/ref引数は&演算子で取得したアドレスを渡します(=値渡し、in/ref/out引数のセマンティクス通り)。なお、試しにマーシャリング対象の型をstructからclassに変えてみましたが、生成コードは同一になるようです。__PInvoke
の引数の型も(StructUnmanaged, StructUnmanaged*, StructUnmanaged*, StructUnmanaged*, 省略)
のまま変化しませんでした。DllImportAttribute
のclassのマーシャリングを考えると(StructUnmanaged*, StructUnmanaged**, StructUnmanaged**, StructUnmanaged**, 省略)
と1段階間接参照が増える印象だったので、注意が必要そうです。
その他色々
ここまでの記述で力尽きつつあるので簡単に済ませます。ご容赦ください。
複数のManaged型に対応できるジェネリックな独自マーシャラーは現状では実装不可能?
CustomMarshallerAttribute.GenericPlaceholder Structという、非常に夢が広がりそうな型があります。現状では標準の型ではArrayMarshaller.cs等で使われているようです。
C# 11で可能になったジェネリック属性等と組み合わせたりして面白いことができるのでは、と思ったのですが試した範囲ではエラーになるものばかりでした。また、MarshalUsing does not work with genericsというissueのコメントでBased on our offline conversation, using GenericPlaceholder as-is doesn’t work correctly at the moment.
との記述も見つかりました。修正を待つことになりそうです。
関連するAPI
関連するAPIです。なお、一部は.NET 6に導入されたものもあります。
- Marshal.GetLastPInvokeError Method :: 機能としてはMarshal.GetLastWin32Error Methodと同一ですが、よりクロスプラットフォームに向いた命名です。
- Marshal.GetLastPInvokeErrorMessage Method ::
Marshal.GetLastPInvokeError()
に対応するエラーメッセージを取得できます。Win32Exception
を経由してエラー文字列を取得する手法(出典: C# で Win32 のエラーコードをメッセージに変換する | shise.net)は不要になるかもしれません。
その他のその他
CustomMarshllerAttribute
等を使う代わりにDisableRuntimeMarshallingAttribute Classを使う手法があるようです(出典: Disabled runtime marshalling)。ただしDisableRuntimeMarshallingAttribute
の適用対象はアセンブリ単位のみであり、パラメーター単位やメソッド単位ではありません。影響範囲が大きすぎるため、CustomMarshallerAttribute
を使用するほうが良いと思います。MarshalUsingAttribute
のメンバーには、Size関係を表すメンバーがいくつかあります。それらを使用すると面白いことができるかもしれません。
感想
軽い気持ちで書き始めたら、想像以上のボリュームになってしまいました。奥深いと言える気もしますし、今までDllImportAttribute
が生成していたマーシャリングコードに大いに頼ってたとも言える気もします。
ところで本記事を書き始めた理由は「.NET 7公開から約1か月経つのに、LibraryImportAttribute
の解説はGithub/runtimeとMSDNだけに見える!他の資料も必要だろう!」というものです。どうか皆様も、新しいライブラリ等を触ってみたらぜひ感想等を発信してみてください。