Aracılığıyla paylaş


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

Bu öğreticide, kaynak tarafından oluşturulan P/Invoke'larda özel sıralama yapmak için bir marshaller uygulamayı ve 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şturucu hakkında genel bakış

Tür System.Runtime.InteropServices.LibraryImportAttribute , .NET 7'de tanıtılan bir kaynak oluşturucunun kullanıcı giriş noktasıdır. Kod üretecimiz, çalışma zamanından ziyade derleme zamanında tüm serileme 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 marshal kodunu oluşturabilir ve DllImport için içsel olan çalışma zamanı oluşturma gereksinimini ortadan kaldırabilir.

Çalışma zamanı için ve kullanıcıların kendi türlerini özelleştirmeleri için marshalling kodunu oluşturmak için gereken ayrıntıları ifade etmek amacıyla birkaç türe ihtiyaç vardır. Bu eğitici rehberde aşağıdaki türler kullanılır.

  • MarshalUsingAttribute – Kaynak oluşturucu tarafından kullanım yerlerinde aranan ve öznitelikli değişkenin marshalling amacıyla 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 – Ait olduğu türün hangi marshaller'ının kullanılacağını belirten öznitelik. Bu, türler ve ilgili marshaller'lar sağlayan kütüphane 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 analiz ve düzeltme aracı

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 bir kitaplık, doğrudan .NET aracılığıyla kullanıma sunulmayan bir işletim sistemi API'sini çağıran paylaşılan bir kitaplık (.dll, .so veya 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 eğitimde, C-stili API yüzeyini 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, char32_t* ve error_data olmak üzere iki tür ilgi alanı 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 marşal etme kodu oluşturması için bir yöntem sağlamanızı gerektirir.

Yerleşik bir tür için dizilemeyi özelleştirme

Öncelikle char32_t* türünü göz önünde bulundurun, çünkü bu türün yönetimi kullanıcı tanımlı tür için gereklidir. 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 string türüne ve türünden yerel UTF-32 kodlamalı bir dizeyi aktaracaksınız. UTF-8, UTF-16, ANSI ve hatta Windows string türü olarak dönüştüren birkaç yerleşik marshaller zaten BSTR türü için mevcuttur. 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ğin ilk tür bağımsız değişkeni string türü, serileştirmek için yönetilen türdür; ikincisi ise serileştiricinin ne zaman kullanılacağını belirten kiptir, ve üçüncü tür ise serileştirme için kullanılacak Utf32StringMarshaller türüdür. CustomMarshaller birden çok kez uygulayarak modu ve bu mod için kullanılacak marshaller türünü daha ayrıntılı olarak belirleyebilirsiniz.

Geçerli örnekte, bazı girdiler alıp verileri derlenmiş biçimde döndüren "durum bilgisi olmayan" bir marshaller gösterilmektedir. 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ı, girişi çıkışa düzenlemek için istediği işlemleri gerçekleştirmekte serbesttir, ancak kaynak üreteci 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 string'den char32_t*'e dönüştürmeyi nasıl gerçekleştirdiğine ilişkin ayrıntılar örnekte bulunabilir. Tüm .NET API'lerinin kullanılabileceğini unutmayın (örneğin, Encoding.UTF32).

Arzu edilen bir durumun söz konusu olduğu bir vaka düşünün. Ek CustomMarshaller gözlemleyin ve daha belirgin olan modu MarshalMode.ManagedToUnmanagedIn not edin. Bu özelleştirilmiş marshaller "durum tabanlı" olarak uygulanır ve entegrasyon çağrısında durumu depolayabilir. Daha fazla uzmanlık ve devlet izinleri optimizasyonları ve bir mod için özel veri düzenleme. Örneğin, kaynak jeneratörüne, hazırlama sırasında açık bir ayırmayı önleyebilecek yığından tahsis edilmiş bir arabellek sağlaması için talimat verilebilir. Yığına ayrılmış arabelleğe destek sağladığını göstermek için, marshaller bir BufferSize özelliği ve FromManaged türünde bir Span alan bir unmanaged yöntemi uygular. BufferSize özelliği, Span'ye geçirilecek FromManaged uzunluğundaki yığın alanı miktarını gösterir; marshaller, hazırlama çağrısı sırasında bunu 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, LibraryImport özniteliğini DllImport gibi kullanır, ancak yerel işlevi çağırırken hangi marshaller'ın kullanılacağını kaynak oluşturucuya belirtmek için MarshalUsing özniteliğine dayanır. Durumsuz veya durumlu marshaller kullanılmalı mı netleştirmeye gerek yoktur. Bu, marshaller'ın MarshalMode özniteliklerini uygulayıcının tanımladığı CustomMarshaller tarafından işlenir. Kaynak oluşturucu, MarshalUsing'ın uygulandığı bağlama göre en uygun marshaller'ı seçer ve MarshalMode.Default yedek seçenek 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 aktarılması için yalnızca marshalling mantığını değil, aynı zamanda C# dilindeki türü aktarma işlemini de tanımlamak 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. Utf32StringMarshaller üzerine inşa ederek, char32_t* öğesini bir .NET string olarak düzenleyebilirsiniz. .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. Bazı modlar için, MarshalMode.Default için bir marshaller belirtmek yerine, sadece marshaller'ları tanımlamanız gerekecek. Bu durumda, marshaller, sağlanmayan bir mod için kullanılırsa, kaynak oluşturucu başarısız olur. "Giriş" yönü için bir marshaller tanımlamayla başlayın. Bu "stateless" bir marshaller'dır çünkü marshaller'ın kendisi yalnızca static işlevlerden 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. ErrorDataErrorDataUnmanaged'e dönüştürmek artık Utf32StringMarshaller ile önemsizdir.

int nesnesinin yönetilmesi, gösterimi yönetilmeyen ve yönetilen kodda aynı olduğundan 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 string alanını uint* içine dönüştürün.

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ı. Utf32StringMarshaller işlevini kullanarak marşal edilen dizeyi bir kez daha temizleyin.

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

Kısaca "out" senaryosunu düşünelim. Bir veya birden çok error_data örneğinin 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 bir P/Invoke, MarshalMode.ManagedToUnmanagedOut olarak sınıflandırılır. Genellikle, birden çok öğe döndürmek için bir koleksiyon kullanırsınız ve bu durumda bir Array kullanılır. Mod MarshalMode.ElementOut'a karşılık gelen bir koleksiyon senaryosu için marshaller birden çok öğe döndürür ve bu 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önetilenden yönetilmeyene "out" marshaller için özel bir işlem yapacaksınız. Marshal ettiğiniz veri türünün adı error_data ve .NET hataları genellikle özel durumlar 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. error_data alanında hatanın ölümcül olup olmadığını kontrol etmek 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 bir bağlama dönüştürüldüğü için ConvertToManaged yöntemini uygularsınız. 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 ErrorData döndürmek yerine fırlatmanız gerektiğine dair bir göstergedir.

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. P/Invoke içinde her kullanıldığında ErrorData'e zımni bir marshaller sağlamak için [NativeMarshalling(typeof(ErrorDataMarshaller))]'i ErrorData tanımına ekleyebilirsiniz. Artık, bu tür tanımınızı bir LibraryImport aramada kullanan herkes, marshaller'larınızdan faydalanacak. Kullanım alanında "MarshalUsing" kullanarak her zaman marshaller'larınızı geçersiz kılabilirler.

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

Ayrıca bakınız