構造体のマーシャリングをカスタマイズする

場合によっては、構造体の既定のマーシャリング規則が、必要な規則ではありません。 .NET ランタイムには、構造体のレイアウトとフィールドのマーシャリング方法をカスタマイズするための拡張ポイントがいくつか用意されています。 構造レイアウトのカスタマイズはすべてのシナリオでサポートされますが、フィールド マーシャリングのカスタマイズは、ランタイム マーシャリングが有効になっているシナリオでのみサポートされます。 ランタイム マーシャリングが無効になっている場合は、フィールド マーシャリングを手動で行う必要があります。

この記事では、ソース生成相互運用機能のマーシャリングのカスタマイズについては説明しません。 P/Invokes または COM にソース生成相互運用機能を使用している場合はマーシャリングのカスタマイズに関するページを参照してください。

構造レイアウトをカスタマイズする

.NETでは、System.Runtime.InteropServices.StructLayoutAttribute属性とSystem.Runtime.InteropServices.LayoutKind列挙体を使用して、フィールドをメモリに配置する方法をカスタマイズできます。 次のガイダンスは、一般的な問題を回避するのに役立ちます。

✔️ 可能な限り、 LayoutKind.Sequential の使用を検討してください。

✔️ ネイティブ構造体に共用体などの明示的なレイアウトがある場合にのみ、マーシャリングで LayoutKind.Explicit を使用してください。

❌ クラスを用いて、継承によって複雑なネイティブ型を表現することは避けてください。

❌Core 3.0 の前にランタイムをターゲットにする必要がある場合は、Windows以外のプラットフォームで構造体をマーシャリングするときに .NETLayoutKind.Explicitを使用しないでください。 3.0 より前の .NET Core ランタイムでは、Intel または AMD 64 ビットの非Windows システム上のネイティブ関数に値による明示的な構造体の渡しはサポートされていません。 ただし、ランタイムでは、すべてのプラットフォームで明示的な構造体を参照渡しできます。

固定サイズのバッファーとインライン配列

構造体で固定サイズのバッファーを定義する必要がある場合は、 StructLayoutAttribute.Size プロパティを使用して余分な領域を割り当てないでください。 Size プロパティは、型のアンマネージド レイアウト(Marshal.StructureToPtr の呼び出し時に使用されるレイアウト)を制御し、ブリッタブル型のマネージド レイアウトにのみ影響します。 blittable 以外の型の場合、ランタイムは実際のフィールドに基づいてのみ領域を割り当てます。余分な領域を使用しようとすると、メモリが破損する可能性があります。

❌ マネージド型で固定サイズのバッファーを作成するには、 StructLayoutAttribute.Size を使用しないでください。

✔️ System.Runtime.CompilerServices.InlineArrayAttributeを使用して、最新の.NET (C# 12 以降) で固定サイズバッファーを定義します。

次の例は、メモリの破損につながる可能性がある不適切なアプローチを示しています。

// ❌ DON'T: This is dangerous and can corrupt memory
[StructLayout(LayoutKind.Explicit, Size = 100)]
class Bad100ByteBuffer
{
    [FieldOffset(0)]
    private byte b;

    unsafe void UseBuffer()
    {
        fixed (byte* p = &b)
        {
            // Writing beyond the first byte corrupts memory
        }
    }
}

代わりに、 InlineArrayAttribute を使用して適切なサイズのバッファーを作成します。

// ✔️ DO: Use InlineArrayAttribute for fixed-size buffers
class Good100ByteBuffer
{
    [InlineArray(100)]
    private struct Buffer100
    {
        private byte _element0;
    }

    private Buffer100 buffer;

    unsafe void UseBuffer()
    {
        fixed (byte* p = &buffer[0])
        {
            // The buffer is guaranteed to be 100 bytes
        }
    }
}

インライン配列の詳細については、 C# 言語リファレンスを参照してください

ブール型フィールドのマーシャリングのカスタマイズ

ネイティブ コードには、さまざまなブール表現があります。 Windows単独では、ブール値を表す 3 つの方法があります。 ランタイムは構造体のネイティブ定義を認識しないため、可能な最善の方法は、ブール値をマーシャリングする方法を推測することです。 .NET ランタイムは、ブール型フィールドをマーシャリングする方法を示す方法を提供します。 次の例は、さまざまなネイティブブール型に.NET boolをマーシャリングする方法を示しています。

次の例に示すように、ブール値は既定でネイティブの 4 バイト Win32 BOOL 値としてマーシャリングされます。

public struct WinBool
{
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

明示的にする場合は、 UnmanagedType.Bool 値を使用して、上記と同じ動作を取得できます。

public struct WinBool
{
    [MarshalAs(UnmanagedType.Bool)]
    public bool b;
}
struct WinBool
{
    public BOOL b;
};

以下の UnmanagedType.U1 値または UnmanagedType.I1 値を使用して、 b フィールドを 1 バイトのネイティブ bool 型としてマーシャリングするようにランタイムに指示できます。

public struct CBool
{
    [MarshalAs(UnmanagedType.U1)]
    public bool b;
}
struct CBool
{
    public bool b;
};

Windowsでは、UnmanagedType.VariantBool値を使用して、ブール値を 2 バイトのVARIANT_BOOL値にマーシャリングするようにランタイムに指示できます。

public struct VariantBool
{
    [MarshalAs(UnmanagedType.VariantBool)]
    public bool b;
}
struct VariantBool
{
    public VARIANT_BOOL b;
};

VARIANT_BOOL は、その VARIANT_TRUE = -1VARIANT_FALSE = 0のほとんどのブール型とは異なります。 さらに、 VARIANT_TRUE と等しくないすべての値は false と見なされます。

配列フィールドのマーシャリングのカスタマイズ

.NETには、配列マーシャリングをカスタマイズするいくつかの方法も含まれています。

既定では、.NETは配列を要素の連続したリストへのポインターとしてマーシャリングします。

public struct DefaultArray
{
    public int[] values;
}
struct DefaultArray
{
    int32_t* values;
};

COM API とやり取りする場合は、配列を SAFEARRAY* オブジェクトとしてマーシャリングする必要がある場合があります。 System.Runtime.InteropServices.MarshalAsAttributeUnmanagedType.SafeArray値を使用して、配列をSAFEARRAY*としてマーシャリングするようにランタイムに指示できます。

public struct SafeArrayExample
{
    [MarshalAs(UnmanagedType.SafeArray)]
    public int[] values;
}
struct SafeArrayExample
{
    SAFEARRAY* values;
};

SAFEARRAYに含まれる要素の種類をカスタマイズする必要がある場合は、MarshalAsAttribute.SafeArraySubTypeフィールドとMarshalAsAttribute.SafeArrayUserDefinedSubType フィールドを使用して、SAFEARRAYの要素の正確な型をカスタマイズできます。

配列をインプレースでマーシャリングする必要がある場合は、 UnmanagedType.ByValArray 値を使用して、配列をインプレースマーシャリングするようにマーシャラーに指示できます。 このマーシャリングを使用する場合は、ランタイムが構造体の領域を正しく割り当てることができるように、配列内の要素数の MarshalAsAttribute.SizeConst フィールドに値を指定する必要もあります。

public struct InPlaceArray
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
    public int[] values;
}
struct InPlaceArray
{
    int values[4];
};

.NETでは、可変長配列フィールドを C99 フレキシブル配列メンバーとしてマーシャリングすることはできません。

文字列フィールドのマーシャリングのカスタマイズ

.NETには、文字列フィールドをマーシャリングするためのさまざまなカスタマイズも用意されています。

既定では、.NETは null で終わる文字列へのポインターとして文字列をマーシャリングします。 エンコードは、StructLayoutAttribute.CharSetSystem.Runtime.InteropServices.StructLayoutAttribute フィールドの値によって異なります。 属性が指定されていない場合、エンコードは既定で ANSI エンコードになります。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char* str;
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    public string str;
}
struct DefaultString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

異なるフィールドに異なるエンコードを使用する必要がある場合、または構造体定義でより明示的にする必要がある場合は、UnmanagedType.LPStr属性でUnmanagedType.LPWStr値またはSystem.Runtime.InteropServices.MarshalAsAttribute値を使用できます。

public struct AnsiString
{
    [MarshalAs(UnmanagedType.LPStr)]
    public string str;
}
struct AnsiString
{
    char* str;
};
public struct UnicodeString
{
    [MarshalAs(UnmanagedType.LPWStr)]
    public string str;
}
struct UnicodeString
{
    char16_t* str; // Could also be wchar_t* on Windows.
};

UTF-8 エンコードを使用して文字列をマーシャリングする場合は、UnmanagedType.LPUTF8StrMarshalAsAttribute値を使用できます。

public struct UTF8String
{
    [MarshalAs(UnmanagedType.LPUTF8Str)]
    public string str;
}
struct UTF8String
{
    char* str;
};

UnmanagedType.LPUTF8Strを使用するには、.NET Framework 4.7 (またはそれ以降のバージョン) または .NET Core 1.1 (またはそれ以降のバージョン) が必要です。 .NET Standard 2.0 では使用できません。

COM API を使用している場合は、文字列を BSTRとしてマーシャリングすることが必要になる場合があります。 UnmanagedType.BStr値を使用すると、文字列をBSTRとしてマーシャリングできます。

public struct BString
{
    [MarshalAs(UnmanagedType.BStr)]
    public string str;
}
struct BString
{
    BSTR str;
};

WinRT ベースの API を使用する場合は、文字列を HSTRINGとしてマーシャリングすることが必要になる場合があります。 UnmanagedType.HString値を使用すると、文字列をHSTRINGとしてマーシャリングできます。 HSTRING マーシャリングは、WinRT が組み込まれているランタイムでのみサポートされます。 WinRT のサポートは .NET 5 で削除されたため、HSTRINGマーシャリングは .NET 5 以降ではサポートされていません。

public struct HString
{
    [MarshalAs(UnmanagedType.HString)]
    public string str;
}
struct BString
{
    HSTRING str;
};

API で構造体に文字列をインプレースで渡す必要がある場合は、 UnmanagedType.ByValTStr 値を使用できます。 ByValTStrによってマーシャリングされた文字列のエンコードは、CharSet属性から決定されることに注意してください。 さらに、 MarshalAsAttribute.SizeConst フィールドで文字列の長さを渡す必要があります。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char str[4];
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
    public string str;
}
struct DefaultString
{
    char16_t str[4]; // Could also be wchar_t[4] on Windows.
};

10進数フィールドのマーシャリングのカスタマイズ

Windowsに取り組んでいる場合は、ネイティブ CYまたはCURRENCY構造を使用するいくつかの API が発生する可能性があります。 既定では、.NET decimal型はネイティブ DECIMAL構造体にマーシャリングされます。 ただし、MarshalAsAttribute値を持つUnmanagedType.Currencyを使用して、decimal値をネイティブのCY値に変換するようにマーシャラーに指示できます。

public struct Currency
{
    [MarshalAs(UnmanagedType.Currency)]
    public decimal dec;
}
struct Currency
{
    CY dec;
};

組合

共用体は、同じメモリ上に異なる種類のデータを含めることができるデータ型です。 これは、C 言語の一般的な形式のデータです。 共用体は、.NET では LayoutKind.Explicit を使って表すことができます。 .NETで共用体を定義するときは、構造体を使用することをお勧めします。 クラスを使用すると、レイアウトの問題が発生し、予期しない動作が発生する可能性があります。

struct device1_config
{
    void* a;
    void* b;
    void* c;
};
struct device2_config
{
    int32_t a;
    int32_t b;
};
struct config
{
    int32_t type;

    union
    {
        device1_config dev1;
        device2_config dev2;
    };
};
public unsafe struct Device1Config
{
    void* a;
    void* b;
    void* c;
}

public struct Device2Config
{
    int a;
    int b;
}

public struct Config
{
    public int Type;

    public _Union Anonymous;

    [StructLayout(LayoutKind.Explicit)]
    public struct _Union
    {
        [FieldOffset(0)]
        public Device1Config Dev1;

        [FieldOffset(0)]
        public Device2Config Dev2;
    }
}

マーシャル System.Object

Windowsでは、object型指定されたフィールドをネイティブ コードにマーシャリングできます。 これらのフィールドは、次の 3 種類のいずれかにマーシャリングできます。

既定では、 object型指定されたフィールドは、オブジェクトをラップする IUnknown* にマーシャリングされます。

public struct ObjectDefault
{
    public object obj;
}
struct ObjectDefault
{
    IUnknown* obj;
};

オブジェクト フィールドをIDispatch*にマーシャリングする場合は、MarshalAsAttribute値を持つUnmanagedType.IDispatchを追加します。

public struct ObjectDispatch
{
    [MarshalAs(UnmanagedType.IDispatch)]
    public object obj;
}
struct ObjectDispatch
{
    IDispatch* obj;
};

VARIANTとしてマーシャリングする場合は、MarshalAsAttribute値を持つUnmanagedType.Structを追加します。

public struct ObjectVariant
{
    [MarshalAs(UnmanagedType.Struct)]
    public object obj;
}
struct ObjectVariant
{
    VARIANT obj;
};

次の表は、 obj フィールドのさまざまなランタイム型が、 VARIANTに格納されているさまざまな型にどのようにマップされるかを示しています。

.NET 型 VARIANT 型
byte VT_UI1
sbyte VT_I1
short VT_I2
ushort VT_UI2
int VT_I4
uint VT_UI4
long VT_I8
ulong VT_UI8
float VT_R4
double VT_R8
char VT_UI2
string VT_BSTR
System.Runtime.InteropServices.BStrWrapper VT_BSTR
object VT_DISPATCH
System.Runtime.InteropServices.UnknownWrapper VT_UNKNOWN
System.Runtime.InteropServices.DispatchWrapper VT_DISPATCH
System.Reflection.Missing VT_ERROR
(object)null VT_EMPTY
bool VT_BOOL
System.DateTime VT_DATE
decimal VT_DECIMAL
System.Runtime.InteropServices.CurrencyWrapper VT_CURRENCY
System.DBNull VT_NULL