Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
.NET memberi Anda berbagai cara untuk menyesuaikan kode interoperabilitas asli Anda. Artikel ini mencakup panduan yang diikuti tim .NET milik Microsoft untuk interoperabilitas bawaan.
Panduan Umum
Panduan di bagian ini berlaku untuk semua skenario interop.
- ✔️ Gunakanlah
[LibraryImport], jika memungkinkan, saat menargetkan .NET 7+.- Ada kasus saat menggunakan
[DllImport]sudah sesuai. Penganalisis kode dengan ID SYSLIB1054 memberi tahu Anda kapan hal itu terjadi.
- Ada kasus saat menggunakan
- ✔️ GUNAKAN penamaan dan kapitalisasi yang sama untuk metode dan parameter Anda seperti metode asli yang ingin Anda panggil.
- ✔️ PERTIMBANGKAN menggunakan penamaan dan kapitalisasi yang sama untuk nilai konstanta.
- Pastikan untuk menentukan P/Invoke dan pointer fungsi sehingga cocok dengan argumen fungsi C.
- ✔️ DO menggunakan jenis .NET yang memetakan paling dekat dengan jenis asli. Misalnya, dalam C#, gunakan
uintsaat jenis aslinya adalahunsigned int. - ✔️ DO lebih suka mengekspresikan jenis asli tingkat yang lebih tinggi menggunakan struktur .NET daripada kelas.
- ✔️ DO lebih suka menggunakan penunjuk fungsi dan UnmanagedCallersOnlyAttribute daripada tipe data
Delegatesaat mengirimkan callback ke fungsi yang tidak dikelola di C#. Untuk informasi selengkapnya, lihat GetFunctionPointerForDelegate(Delegate) . - ✔️ DO gunakan atribut
[In]dan[Out]pada parameter array. - ✔️ DO hanya menggunakan atribut
[In]dan[Out]pada jenis lain ketika perilaku yang Anda inginkan berbeda dari perilaku default. - ✔️ PERTIMBANGKAN menggunakan System.Buffers.ArrayPool<T> untuk mengelola buffer-array asli Anda.
- ✔️ PERTIMBANGKAN untuk membungkus deklarasi P/Invoke Anda di kelas dengan nama dan kapitalisasi yang sama dengan pustaka asli Anda.
- Ini memungkinkan
[LibraryImport]atau atribut[DllImport]Anda untuk menggunakan fitur bahasa C#nameofdalam meneruskan nama pustaka asli serta memastikan bahwa Anda tidak salah mengeja nama pustaka tersebut.
- Ini memungkinkan
- ✔️ DO menggunakan
SafeHandlehandel untuk mengelola masa pakai objek yang merangkum sumber daya yang tidak dikelola. Untuk informasi selengkapnya, lihat Membersihkan sumber daya yang tidak dikelola. - ❌ HINDARI finalizer untuk mengelola masa pakai objek yang merangkum sumber daya yang tidak dikelola. Untuk informasi selengkapnya, lihat Menerapkan cara Buang.
Pengaturan atribut LibraryImport
Penganalisis kode, dengan ID SYSLIB1054, membantu memandu Anda dengan LibraryImportAttribute. Dalam kebanyakan kasus, penggunaan LibraryImportAttribute memerlukan deklarasi eksplisit daripada mengandalkan pengaturan default. Desain ini disengaja dan membantu menghindari perilaku yang tidak diinginkan dalam skenario interop.
Pengaturan atribut DllImport
| Pengaturan | Bawaan | Rekomendasi | Detail |
|---|---|---|---|
| PreserveSig | true |
Pertahankan default | Ketika ini secara eksplisit diatur ke false, nilai pengembalian HRESULT yang gagal akan diubah menjadi pengecualian (dan nilai pengembalian dalam definisi fungsi atau metode menjadi null sebagai hasilnya). |
| SetLastError | false |
Bergantung pada API | Atur ini ke true jika API menggunakan GetLastError dan gunakan Marshal.GetLastWin32Error untuk mendapatkan nilainya. Jika API menetapkan kondisi yang menunjukkan kesalahan, dapatkan kesalahan tersebut sebelum melakukan panggilan lain untuk menghindari kesalahan tersebut ditimpa secara tidak sengaja. |
| CharSet | Ditentukan pengkompilasi (ditentukan dalam dokumentasi tataan karakter) | Secara eksplisit menggunakan CharSet.Unicode atau CharSet.Ansi ketika string atau karakter ada dalam definisi |
Ini menentukan perilaku pengaturan string dan apa yang dilakukan ExactSpelling ketika false. Perhatikan bahwa CharSet.Ansi sebenarnya UTF8 di Unix.
Sebagian besar waktu Windows menggunakan Unicode sementara Unix menggunakan UTF-8. Lihat informasi selengkapnya tentang dokumentasi tentang tataan karakter. |
| ExactSpelling | false |
true |
Atur ini ke true dan dapatkan sedikit manfaat perf karena runtime tidak akan mencari nama fungsi alternatif dengan akhiran "A" atau "W" tergantung pada nilai pengaturan CharSet ("A" untuk CharSet.Ansi dan "W" untuk CharSet.Unicode). |
Parameter tipe string
string disematkan dan digunakan langsung oleh kode asli (daripada disalin) saat diteruskan dengan nilai (bukan ref atau out) dan salah satu dari yang berikut:
- LibraryImportAttribute.StringMarshalling didefinisikan sebagai Utf16.
- Argumen secara eksplisit ditandai sebagai
[MarshalAs(UnmanagedType.LPWSTR)]. - DllImportAttribute.CharSet adalah Unicode.
❌ JANGAN gunakan [Out] string parameter. Parameter string yang diberikan melalui nilai dengan atribut [Out] dapat mendestabilisasi runtime jika string adalah string yang diinternalkan. Lihat informasi selengkapnya tentang penahanan string dalam dokumentasi untuk String.Intern.
✔️ PERTIMBANGKAN larik char[] atau byte[] dari ArrayPool ketika kode asli diharapkan mengisi buffer karakter. Ini memerlukan penerusan argumen sebagai [Out].
Panduan khusus DllImport
✔️ PERTIMBANGKAN mengatur properti CharSet di [DllImport] sehingga runtime mengetahui pengkodean string yang diharapkan.
✔️ Mempertimbangkan menghindari parameter StringBuilder. marshalling StringBuilderselalu menciptakan salinan buffer asli. Dengan demikian, itu bisa sangat tidak efisien. Ambil skenario umum untuk memanggil API Windows yang mengambil string:
- Buat
StringBuilderdengan kapasitas yang diinginkan (alokasi kapasitas terkelola) {1}. - Memohon:
- Alokasikan buffer asli {2}.
- Menyalin konten jika
[In](default untuk parameterStringBuilder). - Menyalin buffer bawaan ke dalam array terkelola yang baru dialokasikan jika
[Out]{3}(juga default untukStringBuilder).
-
ToString()mengalokasikan array terkelola lainnya {4}.
Itu adalah alokasi {4} untuk mengeluarkan string dari kode asli. Yang terbaik yang dapat Anda lakukan untuk membatasi hal ini adalah menggunakan StringBuilder kembali dalam panggilan lain, tetapi ini hanya menghemat satu alokasi. Jauh lebih baik menggunakan dan menyimpan buffer karakter dari ArrayPool. Anda kemudian dapat berfokus hanya pada alokasi untuk ToString() pada panggilan berikutnya.
Masalah lain dengan StringBuilder adalah selalu menyalin buffer yang ditampilkan kembali ke null pertama. Jika string yang diteruskan kembali tidak diakhiri atau diakhiri dengan dua karakter null, P/Invoke Anda sangat mungkin salah.
Jika Anda memang menggunakan StringBuilder, satu catatan penting terakhir adalah bahwa kapasitas tidak menyertakan null yang tersembunyi, yang selalu diperhitungkan dalam interop. Banyak orang keliru dalam hal ini karena sebagian besar API menginginkan ukuran buffer termasuk nilai nol. Ini dapat mengakibatkan alokasi yang terbuang/tidak perlu. Selain itu, gotcha ini mencegah runtime mengoptimalkan marshalling StringBuilder untuk meminimalkan salinan.
Untuk informasi selengkapnya tentang marshalling string, lihat Marshalling Default untuk String dan Menyesuaikan marshalling string.
Windows Spesifik Untuk string
[Out], CLR akan menggunakanCoTaskMemFreesecara default ke string bebas atauSysStringFreeuntuk string yang ditandai sebagaiUnmanagedType.BSTR. Untuk sebagian besar API dengan buffer string output: Jumlah karakter yang diteruskan harus menyertakan null. Jika nilai yang dikembalikan kurang dari jumlah karakter yang dilewatkan, panggilan telah berhasil dan nilainya adalah jumlah karakter tanpa null akhir. Jika tidak, hitungan adalah ukuran buffer yang diperlukan termasuk karakter null.
- Masukkan 5, keluarnya 4: String panjangnya 4 karakter dengan null di akhir.
- Teruskan 5, dapatkan 6: String panjangnya 5 karakter, memerlukan 6 karakter buffer untuk menahan null. Jenis Tipe Data Windows untuk String
Parameter dan bidang Boolean
Boolean mudah sekali disalahgunakan. Secara bawaan, .NET bool dikonversi ke Windows BOOL dengan nilai 4 byte. Namun, tipe _Bool dan bool dalam C dan C++ adalah satu byte. Ini dapat menyebabkan bug yang sulit dilacak karena setengah dari nilai pengembalian akan dibuang, yang berpotensi mengubah hasil. Untuk informasi selengkapnya tentang marshalling nilai .NET bool ke jenis C atau C++ bool, lihat dokumentasi tentang kustomisasi marshalling bidang boolean .
Pengidentifikasi Unik Global (GUID)
GUID dapat digunakan langsung dalam tanda tangan. Banyak API Windows mengambil alias jenis GUID& seperti REFIID. Saat tanda tangan metode berisi parameter referensi, tempatkan ref kata kunci atau [MarshalAs(UnmanagedType.LPStruct)] atribut pada deklarasi parameter GUID.
| GUID | GUID dengan referensi |
|---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
❌ JANGAN Gunakan [MarshalAs(UnmanagedType.LPStruct)] untuk apa pun selain ref parameter GUID.
Jenis blittable
Jenis blittable adalah jenis yang memiliki representasi tingkat bit yang sama dalam kode terkelola dan asli. Dengan demikian, mereka tidak perlu diubah ke format lain untuk dibentuk ke dan dari kode asli, dan karena ini meningkatkan performa, mereka sebaiknya lebih disukai. Beberapa jenis tidak dapat langsung dibaca dari memori tetapi diketahui mengandung konten yang dapat dibaca demikian. Jenis-jenis ini memiliki pengoptimalan yang sama dengan jenis blittable ketika tidak terkandung dalam jenis lain, tetapi tidak dianggap blittable ketika berada di dalam bidang struktur atau untuk tujuan UnmanagedCallersOnlyAttribute.
Jenis blittable ketika marshalling runtime diaktifkan
Jenis blittable:
-
byte,sbyte,short,ushort,int,uint,long,ulong,single,double,nint,nuint - pointer yang tidak dikelola (misalnya,
int*) - struktur dengan tata letak tetap yang hanya memiliki jenis nilai blittable untuk bidang instans
- tata letak tetap memerlukan
[StructLayout(LayoutKind.Sequential)]atau[StructLayout(LayoutKind.Explicit)] - structs
LayoutKind.Sequentialadalah secara bawaan
- tata letak tetap memerlukan
Jenis dengan konten yang bisa diblittable:
- array dimensi tunggal yang tidak bertingkat dari jenis primitif yang bisa di-blittable (misalnya,
int[]) - kelas dengan tata letak tetap yang hanya memiliki jenis nilai blittable untuk bidang instans
- tata letak tetap memerlukan
[StructLayout(LayoutKind.Sequential)]atau[StructLayout(LayoutKind.Explicit)] - kelas
LayoutKind.Autosecara bawaan
- tata letak tetap memerlukan
TIDAK bisa di-blittable:
bool
TERKADANG dapat dimampatkan (blittable):
char
Jenis dengan konten yang TERKADANG blittable:
string
Ketika jenis blittable diteruskan oleh referensi dengan in, ref, atau out, atau ketika jenis dengan konten blittable diteruskan oleh nilai, mereka hanya disematkan oleh marshaller alih-alih disalin ke buffer perantara.
Suatu char dapat menjadi blittable dalam array satu dimensi atau jika merupakan bagian dari tipe yang berisi elemen yang ditandai secara eksplisit dengan [StructLayout] dan CharSet = CharSet.Unicode.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
public char c;
}
string berisi konten blittable jika tidak terkandung dalam jenis lain dan sedang diteruskan secara nilai (bukan ref atau out) sebagai argumen dan salah satu hal berikut:
- StringMarshalling didefinisikan sebagai Utf16.
- Argumen secara eksplisit ditandai sebagai
[MarshalAs(UnmanagedType.LPWSTR)]. - CharSet adalah Unicode.
Anda dapat mengetahui apakah suatu tipe dapat menjadi blittable atau mengandung konten blittable dengan mencoba membuat GCHandle yang disematkan. Jika jenisnya bukan string atau dianggap blittable, GCHandle.Alloc akan menampilkan ArgumentException.
Jenis blittable saat marshalling runtime dinonaktifkan
Ketika marshalling runtime dinonaktifkan, aturan jenis mana yang menjadi blittable menjadi lebih sederhana secara signifikan. Semua jenis yang merupakan jenis C# unmanaged dan tidak memiliki elemen apa pun yang ditandai dengan [StructLayout(LayoutKind.Auto)] adalah blittable. Semua tipe yang bukan tipe C# unmanaged tidak termasuk blittable. Konsep tipe dengan konten yang dapat di-blittable, seperti array atau string, tidak berlaku saat marshalling runtime dinonaktifkan. Jenis apa pun yang tidak dianggap blittable oleh aturan yang disebutkan di atas tidak didukung saat marshalling runtime dinonaktifkan.
Aturan ini berbeda dari sistem bawaan terutama dalam situasi di mana bool dan char digunakan. Saat marshalling dinonaktifkan, bool diteruskan sebagai nilai 1-byte dan tidak dinormalisasi dan char selalu diteruskan sebagai nilai 2-byte. Saat marshalling runtime diaktifkan, bool dapat memetakan ke nilai 1, 2, atau 4 byte dan selalu dinormalisasi, dan char memetakan ke nilai 1 atau 2 byte tergantung pada CharSet.
✔️ BUAT struktur Anda agar blittable jika memungkinkan.
Untuk informasi selengkapnya, lihat:
Menjaga objek terkelola tetap hidup
GC.KeepAlive() akan memastikan objek tetap berada dalam cakupan hingga metode KeepAlive dipanggil.
HandleRef memungkinkan marshaller untuk menjaga objek tetap aktif selama P/Invoke. Hal ini dapat digunakan alih-alih IntPtr dalam signatur metode.
SafeHandle secara efektif menggantikan kelas ini dan harus digunakan sebagai gantinya.
GCHandle memungkinkan penyematan objek terkelola dan mendapatkan pointer asli dari objek tersebut. Pola dasarnya adalah:
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();
Penyematan bukanlah pengaturan default pada GCHandle. Pola utama lainnya adalah untuk meneruskan referensi ke objek terkelola melalui kode asli dan kembali ke kode terkelola, biasanya dengan panggilan balik. Berikut adalah polanya:
GCHandle handle = GCHandle.Alloc(obj);
SomeNativeEnumerator(callbackDelegate, GCHandle.ToIntPtr(handle));
// In the callback
GCHandle handle = GCHandle.FromIntPtr(param);
object managedObject = handle.Target;
// After the last callback
handle.Free();
Jangan lupa bahwa GCHandle perlu dibebaskan secara eksplisit untuk menghindari kebocoran memori.
Jenis data Windows umum
Berikut adalah daftar jenis data yang umum digunakan dalam API Windows dan jenis C# mana yang akan digunakan saat memanggil kode Windows.
Jenis berikut berukuran sama pada Windows 32-bit dan 64-bit, terlepas dari namanya.
| Lebar | Windows | C# | Alternatif |
|---|---|---|---|
| 32 | BOOL |
int |
bool |
| 8 | BOOLEAN |
byte |
[MarshalAs(UnmanagedType.U1)] bool |
| 8 | BYTE |
byte |
|
| 8 | UCHAR |
byte |
|
| 8 | UINT8 |
byte |
|
| 8 | CCHAR |
byte |
|
| 8 | CHAR |
sbyte |
|
| 8 | CHAR |
sbyte |
|
| 8 | INT8 |
sbyte |
|
| 16 | CSHORT |
short |
|
| 16 | INT16 |
short |
|
| 16 | SHORT |
short |
|
| 16 | ATOM |
ushort |
|
| 16 | UINT16 |
ushort |
|
| 16 | USHORT |
ushort |
|
| 16 | WORD |
ushort |
|
| 32 | INT |
int |
|
| 32 | INT32 |
int |
|
| 32 | LONG |
int |
Lihat CLong dan CULong. |
| 32 | LONG32 |
int |
|
| 32 | CLONG |
uint |
Lihat CLong dan CULong. |
| 32 | DWORD |
uint |
Lihat CLong dan CULong. |
| 32 | DWORD32 |
uint |
|
| 32 | UINT |
uint |
|
| 32 | UINT32 |
uint |
|
| 32 | ULONG |
uint |
Lihat CLong dan CULong. |
| 32 | ULONG32 |
uint |
|
| 64 | INT64 |
long |
|
| 64 | LARGE_INTEGER |
long |
|
| 64 | LONG64 |
long |
|
| 64 | LONGLONG |
long |
|
| 64 | QWORD |
long |
|
| 64 | DWORD64 |
ulong |
|
| 64 | UINT64 |
ulong |
|
| 64 | ULONG64 |
ulong |
|
| 64 | ULONGLONG |
ulong |
|
| 64 | ULARGE_INTEGER |
ulong |
|
| 32 | HRESULT |
int |
|
| 32 | NTSTATUS |
int |
Tipe berikut, yang merupakan pointer, menyesuaikan dengan lebar platform. Gunakan IntPtr/UIntPtr untuk ini.
Jenis Pointer yang Bertanda (gunakan IntPtr) |
Jenis Pointer yang Tidak Bertanda (gunakan UIntPtr) |
|---|---|
HANDLE |
WPARAM |
HWND |
UINT_PTR |
HINSTANCE |
ULONG_PTR |
LPARAM |
SIZE_T |
LRESULT |
|
LONG_PTR |
|
INT_PTR |
Windows PVOID, yang merupakan C void*, dapat diproses sebagai IntPtr atau UIntPtr, tetapi sebaiknya menggunakan void* jika memungkinkan.
Tipe bawaan yang sebelumnya didukung
Ada kejadian langka ketika dukungan bawaan untuk tipe dihapus.
Dukungan marshal bawaan untuk UnmanagedType.HString dan UnmanagedType.IInspectable telah dihapus dalam rilis .NET 5. Anda harus menyusun ulang kode biner yang menggunakan jenis marshalling ini dan yang menargetkan kerangka kerja sebelumnya. Masih mungkin untuk mengelola jenis ini, tetapi Anda harus mengelolanya secara manual, seperti yang ditunjukkan oleh contoh kode berikut. Kode ini ke depannya akan bekerja dan juga kompatibel dengan kerangka kerja sebelumnya.
public sealed class HStringMarshaler : ICustomMarshaler
{
public static readonly HStringMarshaler Instance = new HStringMarshaler();
public static ICustomMarshaler GetInstance(string _) => Instance;
public void CleanUpManagedData(object ManagedObj) { }
public void CleanUpNativeData(IntPtr pNativeData)
{
if (pNativeData != IntPtr.Zero)
{
Marshal.ThrowExceptionForHR(WindowsDeleteString(pNativeData));
}
}
public int GetNativeDataSize() => -1;
public IntPtr MarshalManagedToNative(object ManagedObj)
{
if (ManagedObj is null)
return IntPtr.Zero;
var str = (string)ManagedObj;
Marshal.ThrowExceptionForHR(WindowsCreateString(str, str.Length, out var ptr));
return ptr;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
if (pNativeData == IntPtr.Zero)
return null;
var ptr = WindowsGetStringRawBuffer(pNativeData, out var length);
if (ptr == IntPtr.Zero)
return null;
if (length == 0)
return string.Empty;
return Marshal.PtrToStringUni(ptr, length);
}
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string sourceString, int length, out IntPtr hstring);
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int WindowsDeleteString(IntPtr hstring);
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern IntPtr WindowsGetStringRawBuffer(IntPtr hstring, out int length);
}
// Example usage:
[DllImport("api-ms-win-core-winrt-l1-1-0.dll", PreserveSig = true)]
internal static extern int RoGetActivationFactory(
/*[MarshalAs(UnmanagedType.HString)]*/[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(HStringMarshaler))] string activatableClassId,
[In] ref Guid iid,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object factory);
Pertimbangan jenis data lintas platform
Ada jenis dalam bahasa C/C++ yang memiliki garis lintang dalam bagaimana mereka didefinisikan. Saat menulis interop lintas platform, dapat terjadi kasus di mana perbedaan antara platform dapat menyebabkan masalah jika tidak diperhitungkan.
C/C++ long
C/C++ long dan C# long belum tentu berukuran sama.
Jenis long dalam C/C++ didefinisikan untuk memiliki "setidaknya 32" bit. Ini berarti ada jumlah minimum bit yang diperlukan, tetapi platform dapat memilih untuk menggunakan lebih banyak bit jika diinginkan. Tabel berikut mengilustrasikan perbedaan bit yang disediakan untuk jenis data C/C++ long antar platform.
| Plattform | 32-bit | 64-bit |
|---|---|---|
| Windows | 32 | 32 |
| macOS/*nix | 32 | 64 |
Sebaliknya, C# long selalu 64 bit. Untuk alasan ini, yang terbaik adalah menghindari penggunaan C# long untuk menginteropsi C/C++ long.
(Masalah dengan C/C++ long ini tidak ada untuk C/C++ char, , short, intdan long long karena masing-masing 8, 16, 32, dan 64 bit pada semua platform ini.)
Dalam .NET 6 dan versi yang lebih baru, gunakan jenis CLong dan CULong untuk interop dengan jenis data C/C++ long dan unsigned long. Contoh berikut adalah untuk CLong, tetapi Anda dapat menggunakan CULong untuk mengabstraksi unsigned long dengan cara yang sama.
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);
// Usage
nint result = Function(new CLong(10)).Value;
Saat menargetkan .NET 5 dan versi yang lebih lama, Anda harus mendeklarasikan tanda tangan Windows dan non-Windows terpisah untuk menangani masalah.
static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib", EntryPoint = "Function")]
extern static int FunctionWindows(int a);
[DllImport("NativeLib", EntryPoint = "Function")]
extern static nint FunctionUnix(nint a);
// Usage
nint result;
if (IsWindows)
{
result = FunctionWindows(10);
}
else
{
result = FunctionUnix(10);
}
Struktur
Struct terkelola dibuat pada tumpukan dan tidak dihapus sebelum metode dikembalikan. Setelah itu, berdasarkan definisi, mereka "disematkan" (tidak akan dipindahkan oleh GC). Anda juga dapat langsung mengambil alamat dalam blok kode tidak aman jika kode asli tidak akan menggunakan pointer melewati batas akhir metode saat ini.
Struct blittable memiliki kinerja yang jauh lebih baik karena dapat langsung digunakan oleh lapisan marshalling. Cobalah untuk membuat struct yang bisa diblitt (misalnya, hindari bool). Untuk informasi selengkapnya, lihat bagian Jenis Blittable.
Jika struct adalah blittable, gunakan sizeof() alih-alih Marshal.SizeOf<MyStruct>() untuk performa yang lebih baik. Seperti disebutkan di atas, Anda dapat memvalidasi bahwa jenisnya blittable dengan mencoba membuat GCHandle yang disematkan. Jika jenisnya bukan string atau dianggap blittable, GCHandle.Alloc akan menampilkan ArgumentException.
Penunjuk ke struktur dalam definisi harus diteruskan oleh ref atau menggunakan unsafe dan *.
✔️ Pastikan struct yang dikelola sedekat mungkin dengan bentuk dan nama yang digunakan dalam dokumentasi atau header platform yang resmi.
✔️ GUNAKAN C# sizeof() alih-alih Marshal.SizeOf<MyStruct>() untuk struktur yang dapat langsung digunakan untuk meningkatkan performa.
❌ TIDAK bergantung pada representasi internal jenis struct yang diekspos oleh pustaka runtime .NET kecuali didokumentasikan secara eksplisit.
❌ HINDARI menggunakan kelas untuk mengungkapkan tipe asli yang kompleks melalui pewarisan.
❌ HINDARI menggunakan bidang System.Delegate atau System.MulticastDelegate untuk mewakili bidang pointer fungsi dalam struktur.
Karena System.Delegate dan System.MulticastDelegate tidak memiliki tanda tangan yang diperlukan, mereka tidak menjamin bahwa delegasi yang diteruskan akan cocok dengan tanda tangan yang diharapkan kode asli. Selain itu, dalam .NET Framework dan .NET Core, marshalling struct yang berisi System.Delegate atau System.MulticastDelegate dari representasi aslinya ke objek terkelola dapat mendestabilisasi runtime jika nilai bidang dalam representasi asli bukan penunjuk fungsi yang membungkus delegasi terkelola. Dalam .NET 5 dan versi yang lebih baru, pemrosesan field System.Delegate atau System.MulticastDelegate dari representasi asli menjadi objek terkelola tidak didukung. Gunakan jenis delegasi tertentu alih-alih System.Delegate atau System.MulticastDelegate.
Penyangga Tetap
Array seperti INT_PTR Reserved1[2] harus dinaungi ke dua bidang IntPtr, Reserved1a dan Reserved1b. Ketika array asli adalah jenis primitif, kita dapat menggunakan kata kunci fixed untuk menulisnya sedikit lebih bersih. Misalnya, SYSTEM_PROCESS_INFORMATION terlihat seperti ini di header asli:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION;
Dalam C#, kita dapat menulisnya seperti ini:
internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
internal uint NextEntryOffset;
internal uint NumberOfThreads;
private fixed byte Reserved1[48];
internal Interop.UNICODE_STRING ImageName;
...
}
Namun, ada beberapa masalah dengan buffer tetap. Buffer yang tetap dari jenis yang tidak bisa di-blit tidak akan dimarshall dengan benar, sehingga array yang ada perlu diperluas menjadi beberapa bidang individual. Selain itu, dalam .NET Framework dan .NET Core sebelum 3.0, jika struct yang berisi bidang buffer tetap terdapat dalam struct non-blittable, bidang buffer tetap tidak akan dijembatani dengan benar ke kode asli.
Memecahkan masalah kegagalan P/Invoke
Tabel berikut memetakan gejala umum terhadap kemungkinan penyebabnya dan perbaikan yang direkomendasikan.
| Gejala | Kemungkinan penyebabnya | Perbaiki |
|---|---|---|
| DllNotFoundException | Pustaka tidak ditemukan saat runtime | Periksa nama pustaka, jalur, dan platform. Gunakan TryLoad untuk menguji pemuatan. Di Linux, verifikasi LD_LIBRARY_PATH atau rpath. |
| EntryPointNotFoundException | Ketidakcocokan nama ekspor | Periksa ekspor asli (dumpbin /exports di Windows, nm -D di Linux). Periksa nama mangling C++ (hilang extern "C"). Atur EntryPoint secara eksplisit. |
| AccessViolationException | Tanda tangan tidak cocok, penggunaan setelah-bebas, atau penyematan yang hilang | Bandingkan tanda tangan terkelola dan asli. Periksa ukuran struktur dengan Marshal.SizeOf<T>() vs asli sizeof. Verifikasi masa pakai memori. Menggunakan tanda tangan blittable untuk memecahkan masalah marshalling |
| Kerusakan data senyap | Ukuran atau pengkodean jenis huruf salah | Tambahkan pencatatan batas. Bandingkan Marshal.SizeOf<T>() dengan sizeof asli. Uji dengan pasangan input/output yang diketahui. |
| Crash yang terjadi sesekali | GC memindahkan objek yang tidak disematkan atau mengumpulkan delegasi | Delegasi mempertahankan panggilan balik selama masa aktif penuh mereka. Gunakan GCHandle atau fixed untuk pointer yang disimpan selama panggilan berlangsung. |
| Tumpukan kerusakan secara gratis | Alokator yang tidak tepat | Cocokkan alokator: jangan pernah mencampur malloc/free denganCoTaskMemAlloc/CoTaskMemFree atau .Marshal.FreeHGlobal Gunakan fungsi gratis bawaan pustaka. |
Mencegah pengumpulan delegasi dengan GC.KeepAlive
Saat Anda menggunakan GetFunctionPointerForDelegate untuk mengubah deleget ke penunjuk fungsi, pengumpul sampah tidak melacak hubungan antara penunjuk yang dikembalikan dan deleget sumber. Jika delegasi memenuhi syarat untuk pengumpulan sebelum kode asli/natif selesai menggunakan pointer, aplikasi akan mengalami crash.
Gunakan KeepAlive untuk mencegah pengumpulan:
var callback = new MyDelegate((level, msgPtr) =>
{
string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
Console.WriteLine($"[{level}] {msg}");
});
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
NativeUsesCallback(fnPtr);
GC.KeepAlive(callback); // Prevent collection — fnPtr does not root the delegate
Jika kode asli menyimpan penunjuk fungsi di luar panggilan (misalnya, sebagai panggilan balik persisten), delegasi harus di-root untuk seluruh masa pakainya — biasanya dengan menyimpannya di static bidang.
Mengatasi konflik antara dokumentasi dan header asli
Saat menulis signature P/Invoke, dimungkinkan untuk menemukan perbedaan antara dokumentasi API online dan file header asli. File header adalah sumber otoritatif untuk tanda tangan fungsi, tata letak struktur, ukuran jenis, dan konvensi panggilan. Jika ragu, verifikasi tanda tangan P/Invoke Anda terhadap header daripada hanya mengandalkan dokumentasi.