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.
Dalam tutorial ini, Anda akan mempelajari cara mengimplementasikan marshaller dan menggunakannya untuk marshalling kustom dalam P/Invokes yang dihasilkan sumber.
Anda akan menerapkan marshallers untuk jenis bawaan, menyesuaikan marshalling untuk parameter tertentu dan jenis yang ditentukan oleh pengguna, serta menentukan marshalling default untuk jenis yang ditentukan pengguna.
Semua kode sumber yang digunakan dalam tutorial ini tersedia di repositori dotnet/sampel.
Gambaran umum LibraryImport generator sumber
System.Runtime.InteropServices.LibraryImportAttribute adalah jenis yang menjadi titik masuk pengguna untuk generator kode sumber yang diperkenalkan di .NET 7. Pembangkit kode sumber ini dirancang untuk menghasilkan semua kode marshalling pada waktu kompilasi, bukan saat runtime. Titik masuk secara historis telah ditentukan menggunakan DllImport, tetapi pendekatan tersebut datang dengan biaya yang mungkin tidak selalu dapat diterima—untuk informasi selengkapnya, lihat generasi sumber P/Invoke. Generator LibraryImport sumber dapat menghasilkan semua kode marshalling dan menghapus persyaratan pembuatan runtime yang melekat pada DllImport.
Untuk mengekspresikan detail yang diperlukan untuk menghasilkan kode marshalling baik untuk runtime maupun bagi pengguna untuk menyesuaikan untuk jenis mereka sendiri, beberapa jenis diperlukan. Jenis berikut digunakan di seluruh tutorial ini:
MarshalUsingAttribute– Atribut yang dicari oleh generator sumber di lokasi penggunaan dan digunakan untuk menentukan jenis marshaller dalam marshalling variabel yang diberikan atribut.CustomMarshallerAttribute– Atribut yang digunakan untuk menunjukkan marshaller untuk jenis dan cara di mana operasi marshalling dilakukan (misalnya, by-ref dari terkelola ke tidak terkelola).NativeMarshallingAttribute– Atribut yang digunakan untuk menunjukkan marshaller mana yang akan dipakai pada tipe yang diatribusikan. Ini berguna untuk pengembang pustaka yang menyediakan tipe dan pengurai pendamping untuk tipe tersebut.
Namun, atribut ini bukan satu-satunya mekanisme yang tersedia untuk penulis marshaller kustom. Generator sumber daya memeriksa marshaller itu sendiri untuk berbagai indikasi tambahan yang menunjukkan cara marshalling seharusnya dilakukan.
Detail lengkap tentang desain dapat ditemukan di repositori dotnet/runtime .
Penganalisis dan perbaikan generator sumber
Bersamaan dengan generator sumber itu sendiri, sebuah penganalisis dan pemecah masalah juga disediakan. Penganalisis dan perbaikan diaktifkan dan tersedia secara default sejak .NET 7 RC1. Penganalisis dirancang untuk membantu memandu pengembang menggunakan generator sumber dengan benar. Alat perbaikan menyediakan konversi otomatis dari berbagai pola DllImport menjadi tanda tangan LibraryImport yang sesuai.
Memperkenalkan pustaka asli
Menggunakan generator kode sumber LibraryImport berarti menggunakan pustaka asli, atau tidak terkelola. Pustaka asli mungkin merupakan pustaka bersama (yaitu, .dll, , .soatau dylib) yang secara langsung memanggil API sistem operasi yang tidak diekspos melalui .NET. Pustaka mungkin juga merupakan pustaka dengan optimasi tinggi dalam bahasa yang tidak dikelola yang pengembang .NET ingin gunakan. Untuk tutorial ini, Anda akan membangun pustaka bersama yang Anda buat sendiri yang mengekspos antarmuka API bergaya C. Kode berikut mewakili jenis yang ditentukan pengguna dan dua API yang akan Anda gunakan dari C#. Kedua API ini mewakili mode "masuk", tetapi ada mode tambahan untuk dijelajahi dalam sampel.
struct error_data
{
int code;
bool is_fatal_error;
char32_t* message; /* UTF-32 encoded string */
};
extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintErrorData(error_data data);
Kode sebelumnya berisi dua jenis minat, char32_t* dan error_data.
char32_t* mewakili string yang dikodekan ke dalam UTF-32, yang bukan merupakan pengkodean string yang secara historis diolah oleh .NET.
error_data adalah jenis yang ditentukan pengguna yang berisi bidang bilangan bulat 32-bit, bidang Boolean C++, dan bidang string yang dikodekan UTF-32. Kedua jenis ini mengharuskan Anda untuk menyediakan cara bagi generator sumber untuk menghasilkan kode marshalling.
Menyesuaikan marshalling untuk jenis bawaan
Pertimbangkan terlebih dahulu jenis char32_t*, karena marshalling jenis ini diperlukan oleh jenis yang ditentukan oleh pengguna.
char32_t* mewakili sisi asli, tetapi Anda juga memerlukan representasi dalam kode terkelola. Di .NET, hanya ada satu jenis "string", string. Oleh karena itu, Anda akan melakukan marshalling string yang dikodekan UTF-32 asli ke dan dari tipe string dalam kode terkelola. Sudah ada beberapa pengalih bawaan untuk jenis string yang mengalihkan sebagai UTF-8, UTF-16, ANSI, dan bahkan sebagai jenis Windows BSTR. Namun, tidak ada untuk pemformatan sebagai UTF-32. Itulah yang perlu Anda tentukan.
Jenis Utf32StringMarshaller ditandai dengan atribut CustomMarshaller, yang menjelaskan fungsinya terhadap generator sumber. Argumen tipe pertama untuk atribut adalah jenis string, yaitu tipe yang akan dikelola, yang kedua adalah mode, yang menunjukkan kapan harus menggunakan marshaller, dan tipe ketiga adalah Utf32StringMarshaller, yakni tipe yang digunakan untuk pengelolaan data. Anda dapat menerapkan CustomMarshaller beberapa kali untuk menentukan lebih lanjut mode dan jenis marshaller mana yang akan digunakan untuk mode tersebut.
Contoh saat ini menampilkan marshaller "stateless" yang menerima beberapa masukan dan mengembalikan data dalam bentuk yang sudah dimarshall. Metode Free ini ada untuk memastikan simetri dengan marshalling yang tidak dikelola, dan pengumpul sampah adalah operasi "gratis" untuk marshaller yang terkelola. Pelaksana bebas untuk melakukan operasi apa pun yang diinginkan untuk mengalihkan input ke output, tetapi ingat bahwa tidak ada keadaan yang akan dipertahankan secara eksplisit oleh sumber generator.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
internal static unsafe class Utf32StringMarshaller
{
public static uint* ConvertToUnmanaged(string? managed)
=> throw new NotImplementedException();
public static string? ConvertToManaged(uint* unmanaged)
=> throw new NotImplementedException();
public static void Free(uint* unmanaged)
=> throw new NotImplementedException();
}
}
Spesifikasi tentang bagaimana marshaller khusus ini melakukan konversi dari string ke char32_t* dapat ditemukan dalam sampel. Perhatikan bahwa API .NET apa pun dapat digunakan (misalnya, Encoding.UTF32).
Pertimbangkan kasus di mana status diinginkan. Amati tambahan CustomMarshaller dan perhatikan mode yang lebih spesifik, MarshalMode.ManagedToUnmanagedIn. Marshaller khusus ini diimplementasikan sebagai "stateful" dan dapat menyimpan status di seluruh panggilan interop. Peningkatan spesialisasi dan optimasi izin negara serta penyesuaian marshalling untuk suatu mode. Misalnya, generator sumber dapat diinstruksikan untuk menyediakan buffer yang dialokasikan di tumpukan yang dapat menghindari alokasi eksplisit selama proses marshalling. Untuk menunjukkan dukungan untuk buffer yang dialokasikan di tumpukan, marshaller mengimplementasikan BufferSize properti dan FromManaged metode yang mengambil Span jenis unmanaged. Properti BufferSize menunjukkan jumlah ruang tumpukan memori—yaitu panjang Span yang akan diteruskan ke FromManaged—yang ingin diterima oleh marshaller selama pemanggilan marshal.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(string), MarshalMode.Default, typeof(Utf32StringMarshaller))]
[CustomMarshaller(typeof(string), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
internal static unsafe class Utf32StringMarshaller
{
//
// Stateless functions removed
//
public ref struct ManagedToUnmanagedIn
{
public static int BufferSize => 0x100;
private uint* _unmanagedValue;
private bool _allocated; // Used stack alloc or allocated other memory
public void FromManaged(string? managed, Span<byte> buffer)
=> throw new NotImplementedException();
public uint* ToUnmanaged()
=> throw new NotImplementedException();
public void Free()
=> throw new NotImplementedException();
}
}
}
Sekarang Anda dapat memanggil fungsi pertama dari dua fungsi bawaan menggunakan marshaller string UTF-32 Anda. Deklarasi berikut menggunakan LibraryImport atribut , seperti DllImport, tetapi bergantung pada MarshalUsing atribut untuk memberi tahu generator sumber mana yang akan digunakan marshaller saat memanggil fungsi asli. Tidak perlu mengklarifikasi apakah marshaller stateless atau stateful harus digunakan. Ini ditangani oleh pelaksana dengan mendefinisikan MarshalMode pada atribut-atribut dari marshaller CustomMarshaller. Generator sumber akan memilih marshaller yang paling tepat berdasarkan konteks di mana MarshalUsing diterapkan, dengan MarshalMode.Default sebagai opsi cadangan.
// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);
Sesuaikan marshalling untuk jenis yang didefinisikan oleh pengguna
Marshall jenis yang didefinisikan oleh pengguna memerlukan pendefinisian tidak hanya logika marshalling, tetapi juga tipe di C# untuk marshal ke/dari. Ingat tipe bawaan yang kita coba kelola.
struct error_data
{
int code;
bool is_fatal_error;
char32_t* message; /* UTF-32 encoded string */
};
Sekarang, tentukan seperti apa seharusnya dalam C#. Ukurannya int sama dalam C++ modern dan di .NET.
bool adalah contoh kanonis untuk nilai Boolean dalam .NET. Berdasarkan Utf32StringMarshaller, Anda dapat mengonversi char32_t* sebagai .NET string. Mempertimbangkan gaya .NET, hasilnya adalah definisi berikut dalam C#:
struct ErrorData
{
public int Code;
public bool IsFatalError;
public string? Message;
}
Mengikuti pola penamaan, beri nama marshaller ErrorDataMarshaller. Alih-alih menentukan marshaller untuk MarshalMode.Default, Anda hanya akan menentukan marshaller untuk beberapa mode. Dalam hal ini, jika marshaller digunakan untuk mode yang tidak disediakan, generator sumber akan gagal. Mulailah dengan menentukan marshaller untuk arah "masuk". Ini adalah marshaller tanpa status karena marshaller itu sendiri hanya terdiri dari static fungsi.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
internal static unsafe class ErrorDataMarshaller
{
// Unmanaged representation of ErrorData.
// Should mimic the unmanaged error_data type at a binary level.
internal struct ErrorDataUnmanaged
{
public int Code; // .NET doesn't support less than 32-bit, so int is 32-bit.
public byte IsFatal; // The C++ bool is defined as a single byte.
public uint* Message; // This could be as simple as a void*, but uint* is closer.
}
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
=> throw new NotImplementedException();
public static void Free(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
}
}
ErrorDataUnmanaged menyerupai bentuk dari tipe yang tidak dikelola. Konversi dari sebuah ErrorData ke sebuah ErrorDataUnmanaged sekarang menjadi sepele dengan Utf32StringMarshaller.
Pengaturan Marshaling int tidak perlu karena representasinya identik dalam kode yang tidak dikelola dan dikelola.
bool Representasi biner nilai tidak ditentukan dalam .NET, jadi gunakan nilainya saat ini untuk menentukan nilai nol dan bukan nol dalam jenis yang tidak dikelola. Kemudian, gunakan kembali marshaller UTF-32 Anda untuk mengonversi string bidang menjadi uint*.
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
return new ErrorDataUnmanaged
{
Code = managed.Code,
IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
};
}
Ingatlah bahwa Anda menentukan marshaller ini sebagai "in", jadi Anda harus membersihkan setiap alokasi yang dilakukan selama marshalling. Bidang int dan bool tidak mengalokasikan memori apa pun, tetapi bidang Message melakukannya. Gunakan Utf32StringMarshaller kembali untuk membersihkan string marshalled.
public static void Free(ErrorDataUnmanaged unmanaged)
=> Utf32StringMarshaller.Free(unmanaged.Message);
Mari kita pertimbangkan skenario "keluar" secara singkat. Pertimbangkan kasus ketika satu atau beberapa instans dari error_data dikembalikan.
extern "C" DLL_EXPORT error_data STDMETHODCALLTYPE GetFatalErrorIfNegative(int code)
extern "C" DLL_EXPORT error_data* STDMETHODCALLTYPE GetErrors(int* codes, int len)
[LibraryImport(LibName)]
internal static partial ErrorData GetFatalErrorIfNegative(int code);
[LibraryImport(LibName)]
[return: MarshalUsing(CountElementName = "len")]
internal static partial ErrorData[] GetErrors(int[] codes, int len);
P/Invoke yang menghasilkan sebuah jenis instans tunggal, bukan koleksi, diklasifikasikan sebagai MarshalMode.ManagedToUnmanagedOut. Biasanya, Anda menggunakan koleksi untuk mengembalikan beberapa elemen, dan dalam hal ini, Array digunakan. Marshaller untuk skenario koleksi, yang sesuai dengan MarshalMode.ElementOut mode, akan mengembalikan beberapa elemen dan dijelaskan nanti.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
internal static unsafe class ErrorDataMarshaller
{
//
// Other marshallers removed
//
public static class Out
{
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
=> throw new NotImplementedException();
public static void Free(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
}
}
}
Konversi dari ErrorDataUnmanaged ke ErrorData adalah kebalikan dari yang Anda lakukan untuk mode "in". Ingatlah bahwa Anda juga perlu membersihkan alokasi apa pun yang diharapkan oleh lingkungan yang tidak dikelola. Penting juga untuk dicatat bahwa fungsi di sini ditandai static dan karenanya "stateless", keadaan stateless adalah persyaratan untuk semua mode "Element". Anda juga akan melihat bahwa ada metode ConvertToUnmanaged seperti dalam mode "in". Semua mode "Element" memerlukan penanganan untuk mode "masuk" dan "keluar".
Untuk berhasil "keluar" marshaller yang tidak dikelola, Anda akan melakukan sesuatu yang istimewa. Nama jenis data yang Anda marshalling dipanggil error_data dan .NET biasanya mengekspresikan kesalahan sebagai pengecualian. Beberapa kesalahan lebih berdampak daripada yang lain dan kesalahan yang diidentifikasi sebagai "fatal" biasanya menunjukkan kesalahan bencana atau tidak dapat dipulihkan. Perhatikan bahwa error_data memiliki bidang untuk memeriksa apakah kesalahannya fatal. Anda akan mengonversi error_data ke dalam kode terkelola, dan jika terjadi kesalahan fatal, Anda akan melemparkan pengecualian daripada hanya mengonversinya menjadi ErrorData dan mengembalikannya.
namespace CustomMarshalling
{
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedOut, typeof(ThrowOnFatalErrorOut))]
internal static unsafe class ErrorDataMarshaller
{
//
// Other marshallers removed
//
public static class ThrowOnFatalErrorOut
{
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
public static void Free(ErrorDataUnmanaged unmanaged)
=> throw new NotImplementedException();
}
}
}
Parameter "out" dikonversi dari konteks yang tidak dikelola menjadi konteks terkelola, sehingga Anda menerapkan metode ConvertToManaged. Ketika callee yang tidak dikelola kembali dan menyediakan objek ErrorDataUnmanaged, Anda dapat memeriksanya menggunakan marshaller mode ElementOut Anda dan memeriksa apakah itu ditandai sebagai kesalahan fatal. Jika demikian, itu adalah pertanda bagi Anda untuk melempar alih-alih hanya mengembalikan ErrorData.
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
ErrorData data = Out.ConvertToManaged(unmanaged);
if (data.IsFatalError)
throw new ExternalException(data.Message, data.Code);
return data;
}
Mungkin Anda tidak hanya akan menggunakan pustaka asli, tetapi Anda juga ingin berbagi pekerjaan Anda dengan komunitas dan menyediakan perpustakaan interop. Anda dapat menyediakan ErrorData dengan marshaller tersirat setiap kali digunakan dalam P/Invoke dengan menambahkan [NativeMarshalling(typeof(ErrorDataMarshaller))] ke definisi ErrorData. Sekarang, siapa pun yang menggunakan definisi Anda dari jenis ini dalam LibraryImport panggilan akan mendapatkan manfaat dari marshaller Anda. Mereka selalu dapat mengambil alih marshaller Anda dengan menggunakan MarshalUsing di lokasi penggunaan.
[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }