potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C# 9.0 P/Invokeで使う構造体の管理方法いくつか

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;
    }
}