Öğ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 DllImport
belirtilmiş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 DllImport
kaldı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 (, , .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 öğ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_data
alanı 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.ManagedToUnmanagedIn
not 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 Span
unmanaged
yöntem uygular. BufferSize
özelliği, geçirilecek yığın alanı miktarını (geçirilecek FromManaged
uzunluğ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 DllImport
kullanı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 Utf32StringMarshaller
derlemek için .NET string
olarak 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 ErrorDataMarshaller
adını verin. için MarshalMode.Default
bir 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 ' ErrorData
ErrorDataUnmanaged
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.ManagedToUnmanagedOut
kategorilere 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 { ... }