Öğretici: Kaynak tarafından oluşturulan P/Invoke'larda özel marshallers kullanma

Bu öğreticide, kaynak tarafından oluşturulan P/Invoke'larda bir marshaller uygulamayı ve özel sıralama için kullanmayı öğreneceksiniz.

Yerleşik bir tür için marshallers uygulayacak, belirli bir parametre ve kullanıcı tanımlı tür için sıralamayı özelleştireceksiniz ve kullanıcı tanımlı bir tür için varsayılan sıralamayı belirteceksiniz.

Bu öğreticide kullanılan tüm kaynak kodu dotnet/samples deposunda kullanılabilir.

Kaynak oluşturucuya LibraryImport genel bakış

Tür System.Runtime.InteropServices.LibraryImportAttribute , .NET 7'de tanıtılan bir kaynak oluşturucunun kullanıcı giriş noktasıdır. Bu kaynak oluşturucu, derleme zamanında çalışma zamanı yerine tüm marshalling kodunu oluşturmak için tasarlanmıştır. Giriş noktaları geçmişte kullanılarak DllImportbelirtilmiştir, ancak bu yaklaşım her zaman kabul edilebilir olmayabilecek maliyetlerle birlikte gelir; daha fazla bilgi için bkz . P/Invoke kaynak oluşturma. Kaynak LibraryImport oluşturucu tüm sıralama kodunu oluşturabilir ve içindeki çalışma zamanı oluşturma gereksinimini DllImportkaldırabilir.

Hem çalışma zamanı hem de kullanıcıların kendi türleri için özelleştirmesi için oluşturulan marshalling kodu için gereken ayrıntıları ifade etmek için birkaç tür gereklidir. Bu öğretici boyunca aşağıdaki türler kullanılır:

  • MarshalUsingAttribute – Kaynak oluşturucu tarafından kullanılan sitelerde aranan ve öznitelikli değişkenin marshalling için marshaller türünü belirlemek için kullanılan öznitelik.

  • CustomMarshallerAttribute – Bir tür için bir marshaller'ı ve sıralama işlemlerinin gerçekleştirileceği modu belirtmek için kullanılan öznitelik (örneğin, yönetilenten yönetilmeyene kadar olan başvuru).

  • NativeMarshallingAttribute – Öznitelik türü için hangi marshaller'ın kullanılacağını belirtmek için kullanılan öznitelik. Bu, bu türler için türler ve eşlik eden marshaller'lar sağlayan kitaplık yazarları için yararlıdır.

Ancak bu öznitelikler, özel bir marshaller yazarının kullanabileceği tek mekanizma değildir. Kaynak oluşturucu, marshallingin nasıl gerçekleşmesi gerektiğini bildiren diğer çeşitli göstergeler için marshaller'ın kendisini inceler.

Tasarımla ilgili tüm ayrıntılar dotnet/runtime deposunda bulunabilir.

Kaynak oluşturucu çözümleyicisi ve düzelticisi

Kaynak oluşturucunun kendisiyle birlikte bir çözümleyici ve düzeltici de sağlanır. Çözümleyici ve düzeltici .NET 7 RC1'den bu yana varsayılan olarak etkindir ve kullanılabilir. Çözümleyici, geliştiricilerin kaynak oluşturucuyu düzgün kullanmasına yardımcı olmak için tasarlanmıştır. Düzeltici, birçok DllImport desenden uygun LibraryImport imzaya otomatik dönüştürmeler sağlar.

Yerel kitaplığa giriş

Kaynak oluşturucunun LibraryImport kullanılması, yerel veya yönetilmeyen bir kitaplığın kullanılması anlamına gelir. Yerel kitaplık, .dll.NET aracılığıyla kullanıma sunulmayan bir işletim sistemi API'sini doğrudan çağıran paylaşılan bir kitaplık (, , .soveya dylib) olabilir. Kitaplık, bir .NET geliştiricisinin kullanmak istediği yönetilmeyen bir dilde yoğun olarak iyileştirilmiş bir kitaplık da olabilir. Bu öğreticide, C stili api yüzeyini kullanıma sunan kendi paylaşılan kitaplığınızı oluşturacaksınız. Aşağıdaki kod, kullanıcı tanımlı bir türü ve C# dilinden kullanabileceğiniz iki API'yi temsil eder. Bu iki API "in" modunu temsil eder, ancak örnekte keşfedilecek ek modlar vardır.

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

Yukarıdaki kod, ve olmak üzere iki ilgi char32_t*error_dataalanı türünü içerir. char32_t* , .NET'in geçmişe dönük olarak sıraladığı bir dize kodlaması olmayan UTF-32'de kodlanmış bir dizeyi temsil eder. error_data 32 bit tamsayı alanı, C++ Boole alanı ve UTF-32 kodlanmış dize alanı içeren kullanıcı tanımlı bir türdür. Bu türlerin her ikisi de kaynak oluşturucunun marshalling kodu oluşturması için bir yol sağlamanızı gerektirir.

Yerleşik tür için sıralamayı özelleştirme

char32_t* Bu tür, kullanıcı tanımlı tür için gerekli olduğundan önce türünü göz önünde bulundurun. char32_t* yerel tarafı temsil eder, ancak yönetilen kodda da gösteriminiz gerekir. .NET'te yalnızca bir "dize" türü vardır: string. Bu nedenle, yönetilen kodda türüne ve türünden yerel UTF-32 kodlanmış bir dizeyi string yapılandıracaksınız. UtF-8, UTF-16, ANSI ve hatta Windows BSTR türü olarak sıralayan türü için string zaten birkaç yerleşik marshaller vardır. Ancak UTF-32 olarak sıralama için bir tane yoktur. Tanımlamanız gereken budur.

Tür Utf32StringMarshaller , kaynak oluşturucuya ne yaptığını açıklayan bir CustomMarshaller öznitelikle işaretlenir. özniteliğinin string ilk tür bağımsız değişkeni türü, sıralamak için yönetilen tür, ikincisi ise marshaller'ın ne zaman kullanılacağını belirten mod ve üçüncü tür ise, sıralama için kullanılacak türdür Utf32StringMarshaller. Modu ve bu mod için kullanılacak marshaller türünü daha fazla belirtmek için birden çok kez uygulayabilirsiniz CustomMarshaller .

Geçerli örnekte, bazı girişler alan ve verileri sıralanmış biçimde döndüren "durum bilgisi olmayan" bir marshaller gösterilir. Free Yöntemi yönetilmeyen marshalling ile simetri için vardır ve çöp toplayıcı yönetilen marshaller için "ücretsiz" işlemdir. Uygulayıcı, çıkışın girişini sıralamak için istenen işlemleri gerçekleştirmekte serbesttir, ancak kaynak oluşturucu tarafından hiçbir durumun açıkça korunmayacağını unutmayın.

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

Bu belirli bir marshaller'ın dönüştürme string işlemini nasıl gerçekleştirdiğinize char32_t* ilişkin ayrıntılar örnekte bulunabilir. Tüm .NET API'lerinin kullanılabileceğini unutmayın (örneğin, Encoding.UTF32).

Durumun arzu edildiği bir durum düşünün. ekini CustomMarshaller gözlemleyin ve daha belirgin olan modunu MarshalMode.ManagedToUnmanagedInnot edin. Bu özelleştirilmiş marshaller "durum bilgisi olan" olarak uygulanır ve birlikte çalışma çağrısında durumu depolayabilir. Daha fazla uzmanlık ve eyalet izin iyileştirmeleri ve bir mod için özel marshalling. Örneğin, kaynak oluşturucuya, hazırlama sırasında açık ayırmayı önleyebilecek yığınla ayrılmış bir arabellek sağlaması bildirilebilir. Yığına ayrılmış arabelleğe yönelik desteği göstermek için, marshaller bir BufferSize özellik ve FromManaged bir tür alan bir Spanunmanaged yöntem uygular. BufferSize özelliği, geçirilecek yığın alanı miktarını (geçirilecek FromManageduzunluğuSpan) gösterir; marshaller, hazırlama çağrısı sırasında almak ister.

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

Artık UTF-32 dize marshaller'ınızı kullanarak iki yerel işlevden ilkini çağırabilirsiniz. Aşağıdaki bildirim, özniteliğini LibraryImport tıpkı gibi DllImportkullanır, ancak kaynak oluşturucuya yerel işlevi çağırırken hangi marshaller'ın kullanılacağını bildirmek için özniteliğine dayanır MarshalUsing . Durum bilgisi olmayan veya durum bilgisi olan marshaller'ın kullanılması gerekip gerekmediğini netleştirmeye gerek yoktur. Bu, uygulayıcı tarafından marshaller'ın CustomMarshaller özniteliklerini tanımlayan MarshalMode tarafından işlenir. Kaynak oluşturucu, öğesinin uygulandığı MarshalMode.Default bağlama MarshalUsing göre en uygun marshaller'ı seçer ve geri dönüş olur.

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

Kullanıcı tanımlı bir tür için sıralamayı özelleştirme

Kullanıcı tanımlı bir türün sıralanması için yalnızca marshalling mantığını değil, aynı zamanda C# dilindeki türün de sıralanması/sıralanması gerekir. Hazırlamaya çalıştığımız yerel türü hatırlayın.

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

Şimdi, C# dilinde ideal olarak nasıl görüneceğini tanımlayın. , int hem modern C++ hem de .NET'te aynı boyuttadır. A bool , .NET'teki Boole değerinin kurallı örneğidir. üzerinde Utf32StringMarshallerderlemek için .NET stringolarak sıralayabilirsinizchar32_t*. .NET stilinin hesaplandığında, sonuç C# dilinde aşağıdaki tanımdır:

struct ErrorData
{
    public int Code;
    public bool IsFatalError;
    public string? Message;
}

Adlandırma desenini izleyerek, marshaller ErrorDataMarshalleradını verin. için MarshalMode.Defaultbir marshaller belirtmek yerine, yalnızca bazı modlar için marshaller'ları tanımlarsınız. Bu durumda, marshaller sağlanmayan bir mod için kullanılırsa kaynak oluşturucu başarısız olur. "in" yönü için bir marshaller tanımlamayla başlayın. Bu , "durum bilgisi olmayan" bir marshaller'dır çünkü marshaller'ın kendisi yalnızca işlevlerden static oluşur.

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 yönetilmeyen türün şeklini taklit eder. bir'den ' ErrorDataErrorDataUnmanaged e dönüştürme artık ile Utf32StringMarshallerönemsizdir.

Gösterimi yönetilmeyen ve yönetilen kodda aynı olduğundan, öğesinin int yapılandırılması gereksizdir. Bir bool değerin ikili gösterimi .NET'te tanımlanmadığından, yönetilmeyen türde sıfır ve sıfır olmayan bir değer tanımlamak için geçerli değerini kullanın. Ardından, utf-32 marshaller'ınızı yeniden kullanarak alanı içine uint*dönüştürünstring.

public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
    return new ErrorDataUnmanaged
    {
        Code = managed.Code,
        IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
        Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
    };
}

Bu marshaller'ı "in" olarak tanımladığınızı hatırlayın; bu nedenle, sıralama sırasında gerçekleştirilen ayırmaları temizlemeniz gerekir. int ve bool alanları herhangi bir bellek ayırmadı, ancak Message alan ayırdı. Marshalled dizesini temizlemek için yeniden kullanın Utf32StringMarshaller .

public static void Free(ErrorDataUnmanaged unmanaged)
    => Utf32StringMarshaller.Free(unmanaged.Message);

Şimdi "out" senaryoyu kısaca ele alalım. Bir veya birden çok örneğinin error_data döndürüldüğü durumu göz önünde bulundurun.

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

Koleksiyon olmayan tek bir örnek türü döndüren P/Invoke, olarak MarshalMode.ManagedToUnmanagedOutkategorilere ayrılmıştır. Genellikle, birden çok öğe döndürmek için bir koleksiyon kullanırsınız ve bu durumda bir Array kullanılır. Moda karşılık gelen bir koleksiyon senaryosu için MarshalMode.ElementOut marshaller birden çok öğe döndürür ve daha sonra açıklanır.

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

'den ErrorDataUnmanaged 'a ErrorData dönüştürme, "in" modu için yaptığınız şeyin tersidir. Yönetilmeyen ortamın gerçekleştirmenizi beklediği ayırmaları da temizlemeniz gerektiğini unutmayın. Buradaki işlevlerin işaretlendiğine static ve bu nedenle "durum bilgisi olmayan" olduğuna dikkat etmek de önemlidir; durum bilgisi olmayan olmak tüm "Öğe" modları için bir gereksinimdir. "in" modunda olduğu gibi bir ConvertToUnmanaged yöntem olduğunu da fark edeceksiniz. Tüm "Öğe" modları hem "in" hem de "out" modları için işleme gerektirir.

Yönetilmeyen "out" marshaller için özel bir şey yapacaksın. Sıraladığınız veri türünün adı çağrılır error_data ve .NET genellikle hataları özel durum olarak ifade eder. Bazı hatalar diğerlerinden daha etkili olur ve "önemli" olarak tanımlanan hatalar genellikle yıkıcı veya kurtarılamaz bir hatayı gösterir. hatanın error_data önemli olup olmadığını denetlemek için bir alanı olduğuna dikkat edin. Yönetilen koda bir error_data sıralarsınız ve önemliyse, bunu bir koda dönüştürmek ve döndürmek yerine bir ErrorData özel durum oluşturursunuz.

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

"Out" parametresi yönetilmeyen bir bağlamdan yönetilen bağlama dönüştürülür, böylece yöntemini uygularsınız ConvertToManaged . Yönetilmeyen çağıran döndürdüğünde ve bir ErrorDataUnmanaged nesne sağladığında, mode marshaller'ınızı ElementOut kullanarak bu nesneyi inceleyebilir ve önemli bir hata olarak işaretlenip işaretlenmediğini denetleyebilirsiniz. Öyleyse, bu, yalnızca döndürmek yerine atmak için göstergenizdir ErrorData.

public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
    ErrorData data = Out.ConvertToManaged(unmanaged);
    if (data.IsFatalError)
        throw new ExternalException(data.Message, data.Code);

    return data;
}

Belki de yalnızca yerel kitaplığı kullanmakla kalmaz, aynı zamanda çalışmanızı toplulukla paylaşmak ve birlikte çalışma kitaplığı sağlamak istersiniz. Tanımına ekleyerek [NativeMarshalling(typeof(ErrorDataMarshaller))]ErrorData P/Invoke içinde her kullanıldığında zımni bir marshaller sağlayabilirsinizErrorData. Şimdi, bu tür tanımınızı bir LibraryImport aramada kullanan herkes, marshaller'larınızın avantajını elde edecektir. Her zaman, kullanım alanında kullanarak MarshalUsing marshaller'larınızı geçersiz kılabilir.

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

Ayrıca bkz.