C#ではP/Invokeで使う構造体をいくつかの方法で実装できます。ここでは実用性は別としていくつかの方法を紹介します。ボックス化の回避などは割愛しています。
構造体として扱う
通常の構造体
C/C++の構造体を素直に構造体(struct
)として扱う方法です。C#構造体の性質(値渡し、スタックへの確保、Object
クラスメソッド呼び出し時のボクシング等)を理解していれば有力な選択肢ですが、in
引数やreadonly
変数で防衛的コピーが作成されること、防衛的コピー回避でreadonly struct
にすると内部で構造体を操作する関数(PSCoerceToCanonicalValue
関数(Microsoft Docs)等)で扱いにくいなどのデメリットがあります。
using System; using System.Runtime.InteropServices; class Program { static void Main() { var guid = GUID.CLSIDFromProgID("Shell.Application"); Console.WriteLine(guid.ToString()); // -> "{13709620-C279-11CE-A49E-444500000000}" } struct GUID { public uint Data1; public ushort Data2; public ushort Data3; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] Data4; public static GUID CLSIDFromProgID(string progId) { NativeMethods.CLSIDFromProgID(progId, out var clsid); return clsid; } public override string ToString() { return NativeMethods.StringFromCLSID(this); } static class NativeMethods { [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] public static extern void CLSIDFromProgID( [In] string lpszProgID, out GUID pclsid); [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(CoTaskMemToStringUniMarshaler))] public static extern string StringFromCLSID( in GUID rclsid); } } // https://potisan-programming-memo.hatenablog.jp/entry/2020/11/26/214836 internal sealed class CoTaskMemToStringUniMarshaler : ICustomMarshaler { ... } }
読み取り専用構造体
readonly struct
も指定できます。配列(Data4
)はbyte[]
としても定義できますが、その場合Data4
の各要素の値は変更可能になります。
using System; using System.Collections.ObjectModel; using System.Runtime.InteropServices; readonly struct GUID { public uint Data1 { get; init; } public ushort Data2 { get; init; } public ushort Data3 { get; init; } [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] readonly byte[] data4; public ReadOnlyCollection<byte> Data4 => Array.AsReadOnly(data4); public static GUID CLSIDFromProgID(string progId) { NativeMethods.CLSIDFromProgID(progId, out var clsid); return clsid; } public override string ToString() { return NativeMethods.StringFromCLSID(this); } static class NativeMethods { [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] public static extern void CLSIDFromProgID( [In] string lpszProgID, out GUID pclsid); [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(CoTaskMemToStringUniMarshaler))] public static extern string StringFromCLSID( in GUID rclsid); } }
絶対サイズ指定の構造体
GUID
構造体のサイズは128バイトなので、以下のように絶対サイズ指定もできます。フィールドを確認・操作する必要がなければこちらの方が手軽です。
using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential, Size = 128)] struct GUID { public static GUID CLSIDFromProgID(string progId) { NativeMethods.CLSIDFromProgID(progId, out var clsid); return clsid; } public override string ToString() { return NativeMethods.StringFromCLSID(this); } static class NativeMethods { [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] public static extern void CLSIDFromProgID( [In] string lpszProgID, out GUID pclsid); [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(CoTaskMemToStringUniMarshaler))] public static extern string StringFromCLSID( in GUID rclsid); } }
絶対サイズ指定の読み取り専用構造体
readonly struct
も指定できます。フィールドを確認・操作する必要がなければこちらの方が手軽です。
[StructLayout(LayoutKind.Sequential, Size =128)] readonly struct GUID { public static GUID CLSIDFromProgID(string progId) { NativeMethods.CLSIDFromProgID(progId, out var clsid); return clsid; } public override string ToString() { return NativeMethods.StringFromCLSID(this); } static class NativeMethods { [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] public static extern void CLSIDFromProgID( [In] string lpszProgID, out GUID pclsid); [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(CoTaskMemToStringUniMarshaler))] public static extern string StringFromCLSID( in GUID rclsid); } }
バイト配列(byte[]
)として扱う
C/C++の構造体をバイト配列として扱う方法です。配列はヒープ上に確保されるためメモリ管理が楽で、構造体の内容を意識せず扱うことができます。一方、後述のようなラッパーを使わなければ関連する関数がちらばること、特定の型として区別できないのでオーバーロードで使えないなどのデメリットがあります。
using System; using System.Runtime.InteropServices; class Program { static void Main() { var guid = NativeMethods.CLSIDFromProgID("Shell.Application"); Console.WriteLine(NativeMethods.StringFromCLSID(guid)); // -> "{13709620-C279-11CE-A49E-444500000000}" } const int SizeOfGUID = 128; static class NativeMethods { [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] public static extern void CLSIDFromProgID( [In] string lpszProgID, byte[] lpclsid); public static byte[] CLSIDFromProgID(string progId) { var buffer = new byte[SizeOfGUID]; NativeMethods.CLSIDFromProgID(progId, buffer); return buffer; } [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(CoTaskMemToStringUniMarshaler))] public static extern string StringFromCLSID( [In] byte[] rclsid); } // https://potisan-programming-memo.hatenablog.jp/entry/2020/11/26/214836 internal sealed class CoTaskMemToStringUniMarshaler : ICustomMarshaler { ... } }
ラッパークラスで扱う
class
を使う
構造体や配列をラッパークラスで扱う方法です。配列だと参照をひとつ増やすことになるので、構造体を直接持つ方がよいかもしれません。どちらにせよ、参照型としてカプセル化して扱うことができます。ただし読み取り専用は指定できないため、ReadOnly
あるいはImmutable
機能は別クラスで提供する必要があります。
using System; using System.Linq; using System.Runtime.InteropServices; class Program { static void Main() { var guid = GuidWrapper.CLSIDFromProgID("Shell.Application"); Console.WriteLine(guid.ToString()); // -> "{13709620-C279-11CE-A49E-444500000000}" } // Guid構造体が既に定義されているのでWrapperを付けた。 sealed class GuidWrapper { GUID guid; public GuidWrapper(in GUID guid) { this.guid = guid; } public static GuidWrapper CLSIDFromProgID(string progId) { NativeMethods.CLSIDFromProgID(progId, out var clsid); return new GuidWrapper(clsid); } public override string ToString() { return NativeMethods.StringFromCLSID(guid); } static class NativeMethods { [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] public static extern void CLSIDFromProgID( [In] string lpszProgID, out GUID pclsid); [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(CoTaskMemToStringUniMarshaler))] public static extern string StringFromCLSID( in GUID rclsid); } // 構造体を公開する場合はクラス外で定義あるいはpublic指定 struct GUID { public uint Data1; public ushort Data2; public ushort Data3; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] Data4; } } // https://potisan-programming-memo.hatenablog.jp/entry/2020/11/26/214836 internal sealed class CoTaskMemToStringUniMarshaler : ICustomMarshaler { ... }
record
を使う
値を扱う場合はrecord
を使えます。
// Guid構造体が既に定義されているのでWrapperを付けた。 sealed record GuidWrapper { GUID guid; public GuidWrapper() { } GuidWrapper(in GUID guid) { this.guid = guid; } public static GuidWrapper CLSIDFromProgID(string progId) { NativeMethods.CLSIDFromProgID(progId, out var clsid); return new GuidWrapper(clsid); } public override string ToString() { return NativeMethods.StringFromCLSID(guid); } static class NativeMethods { [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] public static extern void CLSIDFromProgID( [In] string lpszProgID, out GUID pclsid); [DllImport("ole32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)] [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(CoTaskMemToStringUniMarshaler))] public static extern string StringFromCLSID( in GUID rclsid); } // 構造体を公開する場合はクラス外で定義あるいはpublic指定 struct GUID { public uint Data1; public ushort Data2; public ushort Data3; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] public byte[] Data4; } }