Menyesuaikan penyusunan struktur

Terkadang aturan marshalling bawaan untuk struktur tidak selalu sesuai dengan yang Anda butuhkan. Runtime .NET menyediakan beberapa titik ekstensi bagi Anda untuk menyesuaikan tata letak struktur Anda dan cara field dimarshal. Penyesuaian tata letak struktur didukung untuk semua skenario, tetapi penyesuaian marshalling field hanya didukung untuk skenario yang mengaktifkan marshalling runtime. Jika runtime marshalling dinonaktifkan, maka marshalling setiap field harus dilakukan secara manual.

Note

Artikel ini tidak mencakup penyesuaian marshalling untuk interop yang dihasilkan oleh source generator. Jika Anda menggunakan interop yang dibuat secara otomatis dari sumber untuk P/Invokes atau COM, lihat menyesuaikan pengaturan marshalling.

Menyesuaikan tata letak struktur

.NET menyediakan System.Runtime.InteropServices.StructLayoutAttribute atribut dan System.Runtime.InteropServices.LayoutKind enumerasi untuk memungkinkan Anda menyesuaikan bagaimana bidang ditempatkan dalam memori. Panduan berikut akan membantu Anda menghindari masalah umum.

✔️ PERTIMBANGKAN untuk menggunakan LayoutKind.Sequential jika memungkinkan.

✔️ HANYA gunakan LayoutKind.Explicit dalam proses marshaling ketika struct asli Anda juga memiliki tata letak eksplisit, seperti union.

❌ HINDARI menggunakan kelas untuk mengungkapkan tipe asli yang kompleks melalui pewarisan.

❌ HINDARI penggunaan LayoutKind.Explicit saat melakukan marshaling pada struktur di platform non-Windows jika Anda perlu menargetkan runtime sebelum .NET Core 3.0. Runtime .NET Core sebelum versi 3.0 tidak mendukung meneruskan struktur eksplisit secara nilai ke fungsi native pada sistem non-Windows 64-bit berbasis Intel atau AMD. Namun, runtime mendukung pengoperan struktur eksplisit secara referensi di semua platform.

Buffer ukuran tetap dan array sebaris

Jika Anda perlu menentukan buffer ukuran tetap dalam struktur, jangan gunakan StructLayoutAttribute.Size properti untuk mengalokasikan ruang tambahan. Properti Size mengontrol tata letak tak terkelola dari tipe tersebut (tata letak yang digunakan saat memanggil Marshal.StructureToPtr) dan hanya memengaruhi tata letak terkelola untuk tipe blittable. Untuk tipe non-blittable, runtime hanya mengalokasikan ruang berdasarkan field yang sebenarnya, yang dapat menyebabkan kerusakan memori jika Anda mencoba memakai ruang tambahan tersebut.

❌ HINDARI menggunakan StructLayoutAttribute.Size untuk membuat buffer ukuran tetap dalam jenis terkelola.

✔️ DO gunakan System.Runtime.CompilerServices.InlineArrayAttribute untuk menentukan buffer ukuran tetap di .NET modern (C# 12 dan yang lebih baru).

Contoh berikut menunjukkan pendekatan yang salah yang dapat menyebabkan kerusakan memori:

// ❌ 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
        }
    }
}

Sebagai gantinya, gunakan InlineArrayAttribute untuk membuat buffer berukuran tepat:

// ✔️ 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
        }
    }
}

Untuk informasi selengkapnya tentang array sebaris, lihat referensi bahasa C#.

Menyesuaikan marshalling untuk kolom Boolean

Kode asli memiliki banyak representasi Boolean yang berbeda. Pada Windows saja, ada tiga cara untuk mewakili nilai Boolean. Runtime tidak tahu definisi asli struktur Anda, jadi yang terbaik yang dapat dilakukan adalah menebak cara marsekal nilai Boolean Anda. Runtime .NET menyediakan cara untuk menentukan bagaimana field Boolean Anda dimarshal. Contoh berikut menunjukkan cara memarshal .NET bool ke dalam berbagai tipe Boolean native.

Nilai Boolean secara default dimarshal sebagai nilai Win32 BOOL asli berukuran 4 byte, seperti yang ditunjukkan dalam contoh berikut:

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

Jika Anda ingin menjadi eksplisit, Anda dapat menggunakan UnmanagedType.Bool nilai untuk mendapatkan perilaku yang sama seperti di atas:

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

Dengan menggunakan nilai UnmanagedType.U1 atau UnmanagedType.I1 di bawah ini, Anda dapat memberi tahu runtime untuk memarshal bidang b sebagai tipe bool asli 1-byte.

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

Di Windows, Anda dapat menggunakan nilai UnmanagedType.VariantBool untuk memberi tahu runtime agar memarshal nilai Boolean Anda menjadi nilai VARIANT_BOOL 2-byte:

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

Note

VARIANT_BOOL berbeda dari sebagian besar tipe bool karena VARIANT_TRUE = -1 dan VARIANT_FALSE = 0. Selain itu, semua nilai yang tidak sama dengan VARIANT_TRUE dianggap false.

Menyesuaikan marshalling bidang array

.NET juga menyertakan beberapa cara untuk menyesuaikan array marshalling.

Secara bawaan, .NET memarshal array sebagai penunjuk ke daftar elemen yang berdampingan:

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

Jika Anda berinteraksi dengan API COM, Anda mungkin harus memarshalkan array sebagai objek SAFEARRAY*. Anda dapat menggunakan System.Runtime.InteropServices.MarshalAsAttribute dan UnmanagedType.SafeArray nilai untuk memberi tahu runtime untuk melakukan marshal array sebagai SAFEARRAY*:

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

Jika Anda perlu menyesuaikan jenis elemen di SAFEARRAY, Anda dapat menggunakan kolom MarshalAsAttribute.SafeArraySubType dan MarshalAsAttribute.SafeArrayUserDefinedSubType untuk menyesuaikan jenis elemen yang tepat dari SAFEARRAY.

Jika Anda perlu memarshal array secara in-place, Anda dapat menggunakan nilai UnmanagedType.ByValArray untuk memberi tahu marshaller agar memarshal array secara in-place. Saat Anda menggunakan metode marshalling ini, Anda juga harus memberikan nilai pada bidang MarshalAsAttribute.SizeConst yang menunjukkan jumlah elemen dalam array agar runtime dapat mengalokasikan ruang untuk struktur dengan benar.

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

Note

.NET tidak mendukung marshaling field array dengan panjang variabel sebagai Flexible Array Member C99.

Menyesuaikan marshalling untuk bidang string

.NET juga menyediakan berbagai penyesuaian untuk bidang string marshalling.

Secara bawaan, .NET memarshal string sebagai penunjuk ke string yang diakhiri dengan null. Pengodean bergantung pada nilai bidang StructLayoutAttribute.CharSet dalam System.Runtime.InteropServices.StructLayoutAttribute. Jika tidak ada atribut yang ditentukan, pengodean default ke pengodean 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.
};

Jika Anda perlu menggunakan pengodean yang berbeda untuk bidang yang berbeda, atau jika Anda ingin menjelaskan secara lebih eksplisit dalam definisi struct Anda, Anda dapat menggunakan nilai UnmanagedType.LPStr atau UnmanagedType.LPWStr pada atribut 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.
};

Jika Anda ingin melakukan marshal string Anda menggunakan pengodean UTF-8, Anda dapat menggunakan nilai UnmanagedType.LPUTF8Str dalam MarshalAsAttribute.

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

Note

Menggunakan UnmanagedType.LPUTF8Str memerlukan .NET Framework 4.7 (atau versi yang lebih baru) atau .NET Core 1.1 (atau versi yang lebih baru). Ini tidak tersedia di .NET Standard 2.0.

Jika Anda bekerja dengan API COM, Anda mungkin perlu memarshal string sebagai BSTR. Dengan menggunakan nilai UnmanagedType.BStr, Anda dapat menyusun sebuah string sebagai BSTR.

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

Saat menggunakan API berbasis WinRT, Anda mungkin perlu memarshal string sebagai HSTRING. Dengan menggunakan nilai UnmanagedType.HString, Anda dapat memarshal string sebagai HSTRING. HSTRING marshalling hanya didukung pada runtime dengan dukungan WinRT bawaan. Dukungan WinRT dihapus di .NET 5, sehingga HSTRING marshalling tidak didukung di .NET 5 atau yang lebih baru.

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

Jika API mengharuskan Anda meneruskan string secara langsung di dalam struktur, Anda dapat menggunakan nilai UnmanagedType.ByValTStr. Perlu diperhatikan bahwa pengodean untuk string yang dimarshalkan oleh ByValTStr ditentukan oleh atribut CharSet. Selain itu, ini mensyaratkan agar panjang string diteruskan melalui field 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.
};

Menyesuaikan pengaturan penyusunan kolom desimal

Jika Anda bekerja di Windows, Anda mungkin menemukan beberapa API yang menggunakan struktur native CYatau CURRENCY. Secara bawaan, tipe .NET decimal dimarshalkan ke struktur native DECIMAL. Namun, Anda dapat menggunakan MarshalAsAttribute dengan nilai UnmanagedType.Currency untuk menginstruksikan marshaller agar mengonversi nilai decimal menjadi nilai CY asli.

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

Serikat-serikat

Penyatuan adalah jenis data yang dapat berisi berbagai jenis data di atas memori yang sama. Ini adalah bentuk umum data dalam bahasa C. Serikat dapat diekspresikan dalam .NET menggunakan LayoutKind.Explicit. Disarankan untuk menggunakan struktur saat menentukan penyatuan dalam .NET. Menggunakan kelas dapat menyebabkan masalah tata letak dan menghasilkan perilaku yang tidak dapat diprediksi.

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

Marshal System.Object

Di Windows, Anda dapat memarshal field bertipe object ke kode native. Anda dapat mengonversi bidang ini menjadi salah satu dari tiga tipe:

Secara bawaan, field bertipe object akan dimarshalkan menjadi IUnknown* yang membungkus objek tersebut.

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

Jika Anda ingin marshal bidang objek ke IDispatch*, tambahkan MarshalAsAttribute dengan UnmanagedType.IDispatch nilai .

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

Jika Anda ingin memarshalkannya sebagai VARIANT, tambahkan MarshalAsAttribute dengan nilai UnmanagedType.Struct.

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

Tabel berikut menjelaskan bagaimana berbagai jenis runtime dari bidang obj dipetakan ke berbagai jenis yang disimpan dalam VARIANT:

Jenis .NET Jenis VARIAN
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