Bagikan melalui


Tutorial: Menggunakan marshaller kustom dalam P/Invokes yang dihasilkan sumber

Dalam tutorial ini, Anda akan mempelajari cara mengimplementasikan marshaller dan menggunakannya untuk marshalling kustom dalam P/Invokes yang dihasilkan sumber.

Anda akan menerapkan marshaller untuk jenis bawaan, menyesuaikan marshalling untuk parameter tertentu dan jenis yang ditentukan pengguna, dan menentukan marshalling default untuk jenis yang ditentukan pengguna.

Semua kode sumber yang digunakan dalam tutorial ini tersedia di repositori dotnet/sample.

Gambaran LibraryImport umum generator sumber

Jenisnya System.Runtime.InteropServices.LibraryImportAttribute adalah titik masuk pengguna untuk generator sumber yang diperkenalkan di .NET 7. Generator sumber ini dirancang untuk menghasilkan semua kode marshalling pada waktu kompilasi alih-alih pada waktu proses. Titik masuk secara historis telah ditentukan menggunakan DllImport, tetapi pendekatan tersebut dilengkapi dengan biaya yang mungkin tidak selalu dapat diterima—untuk informasi selengkapnya, lihat pembuatan sumber P/Panggil. Generator LibraryImport sumber dapat menghasilkan semua kode marshalling dan menghapus persyaratan pembuatan run-time intrinsik ke 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 yang digunakan situs dan digunakan untuk menentukan jenis marshaller untuk marshalling variabel yang dikaitkan.

  • CustomMarshallerAttribute – Atribut yang digunakan untuk menunjukkan marshaller untuk jenis dan mode di mana operasi marshalling harus dilakukan (misalnya, by-ref dari dikelola ke tidak dikelola).

  • NativeMarshallingAttribute – Atribut yang digunakan untuk menunjukkan marshaller mana yang akan digunakan untuk jenis yang diatribusikan. Ini berguna untuk penulis pustaka yang menyediakan jenis dan marshaller pendamping untuk jenis tersebut.

Namun, atribut ini bukan satu-satunya mekanisme yang tersedia untuk penulis marshaller kustom. Generator sumber memeriksa marshaller itu sendiri untuk berbagai indikasi lain yang menginformasikan bagaimana marshalling harus terjadi.

Detail lengkap tentang desain dapat ditemukan di repositori dotnet/runtime .

Penganalisis dan perbaikan generator sumber

Seiring dengan generator sumber itu sendiri, penganalisis dan perbaikan keduanya disediakan. Penganalisis dan perbaikan diaktifkan dan tersedia secara default sejak .NET 7 RC1. Penganalisis dirancang untuk membantu memandu pengembang menggunakan generator sumber dengan benar. Fixer menyediakan konversi otomatis dari banyak DllImport pola ke dalam tanda tangan yang sesuai LibraryImport .

Memperkenalkan pustaka asli

LibraryImport Menggunakan generator sumber berarti mengonsumsi 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 yang sangat dioptimalkan dalam bahasa yang tidak dikelola yang ingin dikonsumsi pengembang .NET. Untuk tutorial ini, Anda akan membangun pustaka bersama Anda sendiri yang mengekspos permukaan API gaya C. Kode berikut mewakili jenis yang ditentukan pengguna dan dua API yang akan Anda gunakan dari C#. Kedua API ini mewakili mode "dalam", 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 dalam UTF-32, yang bukan pengodean string yang secara historis marshal .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 jenisnya char32_t* terlebih dahulu, karena marshalling jenis ini diperlukan oleh jenis yang ditentukan 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 string jenis dalam kode terkelola. Sudah ada beberapa marshaller bawaan untuk string jenis yang marshal sebagai UTF-8, UTF-16, ANSI, dan bahkan sebagai jenis Windows BSTR . Namun, tidak ada untuk marsekal sebagai UTF-32. Itulah yang perlu Anda tentukan.

Utf32StringMarshaller Jenis ditandai dengan CustomMarshaller atribut, yang menjelaskan apa yang dilakukannya pada generator sumber. Argumen jenis pertama ke atribut adalah string jenis, jenis terkelola ke marshal, yang kedua adalah mode, yang menunjukkan kapan harus menggunakan marshaller, dan jenis ketiga adalah Utf32StringMarshaller, jenis yang digunakan untuk marshalling. 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 mengambil beberapa input dan mengembalikan data dalam bentuk marshalled. Metode ini Free ada untuk simetri dengan marshalling yang tidak dikelola, dan pengumpul sampah adalah operasi "gratis" untuk marshaller terkelola. Pelaksana bebas untuk melakukan operasi apa pun yang diinginkan untuk melakukan marshal input ke output, tetapi ingat bahwa tidak ada status yang akan dipertahankan secara eksplisit oleh generator sumber.

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. Lebih banyak spesialisasi dan pengoptimalan izin negara dan marshalling yang disesuaikan untuk mode. Misalnya, generator sumber dapat diinstruksikan untuk menyediakan buffer yang dialokasikan tumpukan yang dapat menghindari alokasi eksplisit selama marshalling. Untuk menunjukkan dukungan untuk buffer yang dialokasikan tumpukan, marshaller mengimplementasikan BufferSize properti dan FromManaged metode yang mengambil Span jenis unmanaged . Properti BufferSize menunjukkan jumlah ruang tumpukan—panjang Span yang akan diteruskan ke FromManaged—marshaller ingin mendapatkan selama panggilan 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();
        }
    }
}

Anda sekarang dapat memanggil fungsi pertama dari dua fungsi asli 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 yang menentukan MarshalMode atribut marshaller CustomMarshaller . Generator sumber akan memilih marshaller yang paling tepat berdasarkan konteks di mana MarshalUsing diterapkan, dengan MarshalMode.Default menjadi fallback.

// extern "C" DLL_EXPORT void STDMETHODCALLTYPE PrintString(char32_t* chars);
[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);

Menyesuaikan marshalling untuk jenis yang ditentukan pengguna

Marshalling jenis yang ditentukan pengguna memerlukan pendefinisian tidak hanya logika marshalling, tetapi juga jenis di C# untuk marshal ke/dari. Ingat jenis asli kita mencoba untuk marshal.

struct error_data
{
    int code;
    bool is_fatal_error;
    char32_t* message;    /* UTF-32 encoded string */
};

Sekarang, tentukan seperti apa idealnya dalam C#. Ukurannya int sama dalam C++ modern dan di .NET. adalah bool contoh kanonis untuk nilai Boolean di .NET. Membangun di atas Utf32StringMarshaller, Anda dapat marshal char32_t* sebagai .NET string. Akuntansi untuk 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 "ke". Ini adalah marshaller "stateless" 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 menimpaik bentuk jenis yang tidak dikelola. Konversi dari ke ErrorData yang ErrorDataUnmanaged sekarang sepele dengan Utf32StringMarshaller.

Marsekal 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 Message bidangnya. 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 di mana satu atau beberapa instans 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 mengembalikan satu jenis instans, non-koleksi, dikategorikan sebagai MarshalMode.ManagedToUnmanagedOut. Biasanya, Anda menggunakan koleksi untuk mengembalikan beberapa elemen, dan dalam hal ini, digunakan Array . 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 inversi dari apa yang Anda lakukan untuk mode "dalam". 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 oleh karena itu "stateless", menjadi stateless adalah persyaratan untuk semua mode "Element". Anda juga akan melihat bahwa ada ConvertToUnmanaged metode seperti dalam mode "dalam". 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 memiliki error_data bidang untuk memeriksa apakah kesalahannya fatal. Anda akan melakukan marsekal ke dalam kode terkelola error_data , dan jika itu 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 "keluar" dikonversi dari konteks yang tidak dikelola menjadi konteks terkelola, sehingga Anda menerapkan metode .ConvertToManaged Ketika penerima panggilan yang tidak dikelola kembali dan menyediakan ErrorDataUnmanaged objek, Anda dapat memeriksanya menggunakan mode marshaller Anda ElementOut dan memeriksa apakah itu ditandai sebagai kesalahan fatal. Jika demikian, itu adalah indikasi Anda untuk melemparkan 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 ErrorData definisi. 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 situs penggunaan.

[NativeMarshalling(typeof(ErrorDataMarshaller))]
struct ErrorData { ... }

Lihat juga