Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
.NET предоставляет различные способы настройки собственного кода взаимодействия. В этой статье содержатся рекомендации, которым команды .NET от Microsoft следуют для обеспечения нативной совместимости.
Общее руководство
Рекомендации в этом разделе относятся ко всем сценариям взаимодействия.
- ✔️ По возможности используйте
[LibraryImport]при работе с .NET 7+.- Существуют случаи, когда используется
[DllImport]соответствующим образом. Анализатор кода с идентификатором SYSLIB1054 сообщает вам, когда это так.
- Существуют случаи, когда используется
- ✔️ РЕКОМЕНДУЕТСЯ использовать такое же именование и регистр для методов и параметров, как и для нативного метода, который вы хотите вызвать.
- ✔️ РАССМОТРИТЕ использование такого же именования и написания для константных значений.
- ✔️ Определяйте сигнатуры P/Invoke и указателей функций, которые соответствуют параметрам C-функции.
- ✔️ Обязательно используйте типы .NET, которые максимально соответствуют исходным типам. Например, если собственный тип в C# —
uint, используйтеunsigned int. - ✔️ Предпочитайте выражение собственных типов более высокого уровня, используя .NET структуры, а не классы.
- ✔️ Рекомендуется использовать указатели на функции и UnmanagedCallersOnlyAttribute вместо
Delegateтипов при передаче обратных вызовов в неуправляемые функции на C#. Дополнительные сведения см. в разделе GetFunctionPointerForDelegate(Delegate). - ✔️ Обязательно используйте атрибуты
[In]и[Out]для параметров массива. - ✔️ Используйте
[In]и[Out]атрибуты только для других типов, если нужное поведение отличается от поведения по умолчанию. - ✔️ Подумайте о использовании System.Buffers.ArrayPool<T> для объединения нативных буферов массивов.
- ✔️ ДОПУСТИМО создавать оболочку для объявлений P/Invoke в классе с таким же именем и регистром, как в нативной библиотеке.
- Это позволяет
[LibraryImport][DllImport]использовать функцию языка C#nameof, чтобы передать имя собственной библиотеки и убедиться, что вы не пропустили имя собственной библиотеки.
- Это позволяет
- ✔️ Используйте
SafeHandleдескриптор для управления временем существования объектов, которые инкапсулируют неуправляемые ресурсы. Дополнительные сведения см. в разделе Очистка неуправляемых ресурсов. - ❌ Избегайте завершения для управления временем существования объектов, которые инкапсулируют неуправляемые ресурсы. Дополнительные сведения см. в разделе «Реализация метода Dispose».
Параметры атрибута LibraryImport
Анализатор кода с идентификатором SYSLIB1054 помогает вам LibraryImportAttribute. В большинстве случаев использование LibraryImportAttribute требует явного объявления, а не использования параметров по умолчанию. Эта конструкция является преднамеренной и помогает избежать непреднамеренного поведения в сценариях взаимодействия.
Параметры атрибута DllImport
| Настройка | По умолчанию. | Рекомендация | Сведения |
|---|---|---|---|
| PreserveSig | true |
Сохранить значение по умолчанию | Если явно задано значение false, неудачные возвращаемые значения HRESULT будут преобразованы в исключения (при этом возвращаемое значение в определении становится null). |
| SetLastError | false |
Зависит от API | Присвойте этому параметру значение true, если для получения значения в API используется GetLastError и Marshal.GetLastWin32Error. Если API устанавливает условие, указывающее на ошибку, перед тем как выполнить другие вызовы, получите информацию об ошибке, чтобы избежать ее непреднамеренной перезаписи. |
| CharSet | Определяемый компилятором (указанный в документации по charset) | Если в определении есть строки или символы, используйте CharSet.Unicode или CharSet.Ansi в явном виде |
Указывает специфику обработки строк и что делает ExactSpelling, когда false. Обратите внимание, что в Unix CharSet.Ansi имеет кодировку UTF8.
Большую часть времени Windows использует Юникод, а Unix — UTF-8. См. дополнительные сведения в документации по кодировке. |
| ExactSpelling | false |
true |
Если присвоить этому параметру значение true, можно немного повысить производительность — среда выполнения не будет искать другие имена функций с суффиксом "A" или "W" в зависимости от значения параметра CharSet ("A" для CharSet.Ansi и "W" для CharSet.Unicode). |
Параметры строки
Объект string закрепляется и используется непосредственно машинным кодом (а не копируется), когда он передается по значению (не ref или out) и соответствует любому из следующих условий:
- LibraryImportAttribute.StringMarshalling определяется как Utf16.
- Аргумент явно помечен как
[MarshalAs(UnmanagedType.LPWSTR)]. - DllImportAttribute.CharSet равно Unicode.
❌ Не используйте [Out] string параметры. Строковые параметры, передаваемые по значению с атрибутом [Out], могут дестабилизировать среду выполнения, если строка является интернированной. См. дополнительные сведения в документации по методу интернирования строк String.Intern.
✔️ Рассмотрите char[] или byte[] массивы из ArrayPool, когда ожидается, что нативный код заполнит буфер символов. Для этого требуется передать аргумент как [Out].
Руководство, cпецифичное для DllImport
✔️ Рассмотрите возможность настройки свойства CharSet в [DllImport] так, чтобы среда выполнения знала ожидаемое кодирование строк.
✔️ Следует избегать параметров StringBuilder. При маршаллингe StringBuilderвсегда создается копия нативного буфера. Таким образом, это может быть крайне неэффективным. Выполните типичный сценарий вызова API Windows, который принимает строку:
- Создайте нужную
StringBuilderемкость (выделяет управляемую емкость). {1} - Взывать:
- Выделяет собственный буфер {2}.
- Копирует содержимое, если
[In](по умолчанию дляStringBuilderпараметра) - Копирует собственный буфер в только что выделенный управляемый массив, если
[Out]{3}(также по умолчанию дляStringBuilder).
-
ToString()выделяет еще один управляемый массив {4}.
{4} Это выделение для получения строки из машинного кода. Лучшее, что можно сделать, чтобы ограничить потери — это повторно использовать StringBuilder в другом вызове, но это по-прежнему экономит только одно выделение. Гораздо лучше использовать и кэшировать буфер символов из ArrayPool. Затем можно перейти к выделению только для ToString() во время последующих вызовов.
Еще одна проблема, связанная с StringBuilder, заключается в том, что этот атрибут всегда создает резервную копию буфера возврата к первому значению NULL. Если возвращенная строка не завершена или завершается двумя символами NULL, атрибут P/Invoke задан неправильно (в лучшем случае).
Если вы все же используетеStringBuilder, возникает еще одна проблема — емкость не включает скрытого значения NULL, которое всегда учитывается при взаимодействии. Пользователи часто ошибаются, так как для многих API требуется, чтобы в размер буфера включалось значение NULL. Это может привести к чрезмерному или ненужному распределению. Кроме того, эта ошибка не позволяет среде выполнения оптимизировать маршалинг StringBuilder для минимизации количества копий.
Дополнительные сведения см. в статье Маршалинг по умолчанию для строк и разделе Customizing string parameters (Настройка строковых параметров).
Windows Specific Для строк
[Out]среда CLR будет использоватьCoTaskMemFreeпо умолчанию для свободных строк илиSysStringFreeдля строк, помеченных какUnmanagedType.BSTR. Для большинства API с буфером выходной строки: число переданных символов должно включать нулевой символ. Если возвращаемое значение меньше, чем количество переданных символов, значит, вызов завершился успешно. В таком случае значение — это количество символов без NULL в конце. В противном случае значение - требуемый размер буфера, включая нулевой символ.
- Передайте 5, получите 4: строка длиной 4 символа с завершающим нулевым символом.
- Передайте 5, получите 6: строка имеет длину 5 символов, требуется 6 символьных буфера, чтобы сохранить значение NULL. Windows Типы данных для строк
Логические параметры и поля
Булевые значения легко напутать. По умолчанию .NET bool маршалируется в объект Windows BOOL, где он является 4-байтовым значением. Но типы _Bool и bool в C и C++ имеют размер один байт. Это может привести к сложностям в отслеживании ошибок, поскольку половина возвращаемого значения будет отброшена, что лишь возможно изменит результат. Дополнительные сведения о маршализации значений .NET bool для типов C или C++ bool см. в документации по настройке маршализации булевых полей .
Идентификаторы GUID
Идентификаторы GUID можно использовать непосредственно в сигнатурах. Многие API Windows принимают псевдонимы типа GUID&, например REFIID. Если сигнатура метода содержит ссылочный параметр, в объявление параметра GUID поместите ключевое слово ref или атрибут [MarshalAs(UnmanagedType.LPStruct)].
| ГУИД | Идентификатор GUID для передачи по ссылке |
|---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
❌ Не используйте [MarshalAs(UnmanagedType.LPStruct)] ничего, кроме ref параметров GUID.
Плоские типы
Непреобразуемые типы — это типы данных с одинаковым представлением на битовом уровне в управляемом и машинном коде. Для маршалинга в нативный код и из него эти типы не нужно преобразовывать в другой формат, что увеличивает производительность. Поэтому им следует отдавать предпочтение. Некоторые типы не являются blittable, но, как известно, содержат переносимое содержимое. Эти типы имеют аналогичные оптимизации, как блиттбл-типы, когда они не содержатся в другом типе, но не считаются блиттбл в полях структур или для целей UnmanagedCallersOnlyAttribute.
Типы blittable при включенном маршалинге среды выполнения
Блитабельные типы данных:
-
byte,sbyte,shortushortintuintlongulongsingledoublenintnuint - неуправляемые указатели (например,
int*) - структуры с фиксированным макетом, которые имеют только типы значений, допускающих перерезку для полей экземпляра
- Для фиксированного макета требуется указать
[StructLayout(LayoutKind.Sequential)]или[StructLayout(LayoutKind.Explicit)] - Структуры являются по
LayoutKind.Sequentialумолчанию
- Для фиксированного макета требуется указать
Типы с содержимым, которое можно напрямую обменивать:
- невложенные одномерные массивы блитабельных примитивных типов (например,
int[]) - классы с фиксированным макетом, которые имеют только типы значений, поддерживающие преобразование в память, для полей экземпляра.
- Для фиксированного макета требуется указать
[StructLayout(LayoutKind.Sequential)]или[StructLayout(LayoutKind.Explicit)] - Классы
LayoutKind.Autoвключены по умолчанию
- Для фиксированного макета требуется указать
Преобразуемые типы данных:
bool
ПЕРИОДИЧЕСКИ преобразуемые типы данных:
char
Типы, содержимое которых иногда является blittable:
string
При передаче блиттабельных типов по ссылке с помощью in, ref или out, или при передаче типов с блиттабельным содержимым по значению, они просто закрепляются маршаллером, а не копируются в промежуточный буфер.
Тип char является блиттабельным в одномерном массиве или, если он является частью типа, который явно помечен [StructLayout] с CharSet = CharSet.Unicode.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
public char c;
}
string содержит блиттовое содержимое, если он не содержится в другом типе и передается значением (не ref или out) в качестве аргумента и при одном из следующих условий:
- StringMarshalling определяется как Utf16.
- Аргумент явно помечен как
[MarshalAs(UnmanagedType.LPWSTR)]. - CharSet — Юникод.
Вы можете узнать, является ли тип перерезаемым или содержит закрепления содержимое, пытаясь создать закрепленное GCHandleсодержимое. Если тип не является строкой или считается непреобразуемым, GCHandle.Alloc вызовет ArgumentException.
Типы blittable при отключении маршалинг среды выполнения
При отключении маршаллинга среды выполнения правила, для которых типы являются перерезаемыми, значительно проще. Все типы C# unmanaged и не имеют полей, помеченных как [StructLayout(LayoutKind.Auto)] blittable. Все типы, которые не являются типами C# unmanaged , не являются перерезаемыми. Концепция типов с объемным содержимым, например массивами или строками, не применяется при отключении маршаллинга среды выполнения. Любой тип, который не считается перерезанным в приведенном выше правиле, не поддерживается при отключении маршаллинга среды выполнения.
Эти правила отличаются от встроенной системы в первую очередь в ситуациях, когда bool и char используются. При отключении маршаллинга bool передается как 1-байтовое значение, не нормализовано, а char всегда передается в виде 2-байтового значения. Если маршалирование среды выполнения включено, bool может сопоставляться со значением 1, 2 или 4-байтов и всегда нормализовано, а char также сопоставляется со значением 1 или 2 байтов в зависимости от значения CharSet.
✔️ РЕКОМЕНДУЕТСЯ по возможности сделать свои структуры данных непреобразуемыми.
Дополнительные сведения см. в разделе:
Сохранение управляемых объектов активными
GC.KeepAlive() гарантирует, что объект остается в области видимости, пока не вызовется метод KeepAlive.
HandleRef позволяет маршалеру сохранять объект активным во время выполнения P/Invoke. Его можно использовать вместо IntPtr в сигнатурах метода.
SafeHandle фактически заменяет этот класс и должен использоваться вместо него.
GCHandle разрешает закреплять управляемый объект и получать нативный указатель на него. Базовый шаблон приведен ниже:
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();
Закрепление по умолчанию не предусмотрено для GCHandle. Еще один основной шаблон предназначен для передачи ссылки на управляемый объект через машинный код и обратно на управляемый код, обычно с обратным вызовом. Вот этот шаблон:
GCHandle handle = GCHandle.Alloc(obj);
SomeNativeEnumerator(callbackDelegate, GCHandle.ToIntPtr(handle));
// In the callback
GCHandle handle = GCHandle.FromIntPtr(param);
object managedObject = handle.Target;
// After the last callback
handle.Free();
Не забывайте, что GCHandle необходимо явно освобождать, чтобы избежать утечек памяти.
Распространенные типы данных Windows
Ниже приведен список типов данных, часто используемых в Windows API, и какие типы C# следует использовать при вызове кода Windows.
Следующие типы одинаковы для 32-разрядных и 64-разрядных Windows, несмотря на их имена.
| Ширина | Windows | C# | Альтернатива |
|---|---|---|---|
| 32 | BOOL |
int |
bool |
| 8 | BOOLEAN |
byte |
[MarshalAs(UnmanagedType.U1)] bool |
| 8 | BYTE |
byte |
|
| 8 | UCHAR |
byte |
|
| 8 | UINT8 |
byte |
|
| 8 | CCHAR |
byte |
|
| 8 | CHAR |
sbyte |
|
| 8 | CHAR |
sbyte |
|
| 8 | INT8 |
sbyte |
|
| 16 | CSHORT |
short |
|
| 16 | INT16 |
short |
|
| 16 | SHORT |
short |
|
| 16 | ATOM |
ushort |
|
| 16 | UINT16 |
ushort |
|
| 16 | USHORT |
ushort |
|
| 16 | WORD |
ushort |
|
| 32 | INT |
int |
|
| 32 | INT32 |
int |
|
| 32 | LONG |
int |
Ознакомьтесь с разделами CLong и CULong. |
| 32 | LONG32 |
int |
|
| 32 | CLONG |
uint |
Ознакомьтесь с разделами CLong и CULong. |
| 32 | DWORD |
uint |
Ознакомьтесь с разделами CLong и CULong. |
| 32 | DWORD32 |
uint |
|
| 32 | UINT |
uint |
|
| 32 | UINT32 |
uint |
|
| 32 | ULONG |
uint |
Ознакомьтесь с разделами CLong и CULong. |
| 32 | ULONG32 |
uint |
|
| 64 | INT64 |
long |
|
| 64 | LARGE_INTEGER |
long |
|
| 64 | LONG64 |
long |
|
| 64 | LONGLONG |
long |
|
| 64 | QWORD |
long |
|
| 64 | DWORD64 |
ulong |
|
| 64 | UINT64 |
ulong |
|
| 64 | ULONG64 |
ulong |
|
| 64 | ULONGLONG |
ulong |
|
| 64 | ULARGE_INTEGER |
ulong |
|
| 32 | HRESULT |
int |
|
| 32 | NTSTATUS |
int |
Следующие типы являются указателями и зависят от ширины платформы. Для них используйте IntPtr/UIntPtr.
Типы указателей со знаком (используйте IntPtr) |
Типы указателей без знака (используйте UIntPtr) |
|---|---|
HANDLE |
WPARAM |
HWND |
UINT_PTR |
HINSTANCE |
ULONG_PTR |
LPARAM |
SIZE_T |
LRESULT |
|
LONG_PTR |
|
INT_PTR |
Windows PVOID, который является void*, можно маршаллировать как IntPtr или UIntPtr, но рекомендуется по возможности предпочесть void*.
Ранее поддерживаемые встроенные типы
Редкие случаи, когда встроенная поддержка типа удаляется.
Поддержка встроенного маршала UnmanagedType.HString и UnmanagedType.IInspectable была удалена в версии .NET 5. Необходимо повторно компилировать двоичные файлы, которые используют этот тип маршалинга и предназначены для предыдущего фреймворка. Этот тип по-прежнему можно маршалировать, но его необходимо маршалировать вручную, как показано в следующем примере кода. Этот код будет работать вперед и также совместим с предыдущими платформами.
public sealed class HStringMarshaler : ICustomMarshaler
{
public static readonly HStringMarshaler Instance = new HStringMarshaler();
public static ICustomMarshaler GetInstance(string _) => Instance;
public void CleanUpManagedData(object ManagedObj) { }
public void CleanUpNativeData(IntPtr pNativeData)
{
if (pNativeData != IntPtr.Zero)
{
Marshal.ThrowExceptionForHR(WindowsDeleteString(pNativeData));
}
}
public int GetNativeDataSize() => -1;
public IntPtr MarshalManagedToNative(object ManagedObj)
{
if (ManagedObj is null)
return IntPtr.Zero;
var str = (string)ManagedObj;
Marshal.ThrowExceptionForHR(WindowsCreateString(str, str.Length, out var ptr));
return ptr;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
if (pNativeData == IntPtr.Zero)
return null;
var ptr = WindowsGetStringRawBuffer(pNativeData, out var length);
if (ptr == IntPtr.Zero)
return null;
if (length == 0)
return string.Empty;
return Marshal.PtrToStringUni(ptr, length);
}
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string sourceString, int length, out IntPtr hstring);
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int WindowsDeleteString(IntPtr hstring);
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern IntPtr WindowsGetStringRawBuffer(IntPtr hstring, out int length);
}
// Example usage:
[DllImport("api-ms-win-core-winrt-l1-1-0.dll", PreserveSig = true)]
internal static extern int RoGetActivationFactory(
/*[MarshalAs(UnmanagedType.HString)]*/[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(HStringMarshaler))] string activatableClassId,
[In] ref Guid iid,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object factory);
Рекомендации по кроссплатформенным типам данных
Существуют типы на языке C/C++, которые имеют широту в том, как они определены. При написании кроссплатформенного взаимодействия могут возникать случаи, когда платформы отличаются и могут вызывать проблемы, если они не рассматриваются.
C/C++ (язык программирования) long
C/C++ long и C# long не обязательно одинакового размера.
Тип long в C/C++ определяется как "не менее 32" битов. Это означает, что существует минимальное количество необходимых битов, но платформы могут использовать больше битов при желании. В следующей таблице показаны различия в предоставленных битах для типа данных C/C++ long между платформами.
| Платформа | 32-разрядное | 64-разрядное |
|---|---|---|
| Windows | 32 | 32 |
| macOS/*nix | 32 | 64 |
В отличие от этого, C# long всегда имеет 64-разрядную версию. По этой причине рекомендуется избежать использования C# long для взаимодействия с C/C++ long.
(Эта проблема с C/C++ не существует для C/C++ longchar, shortintи long long так как они имеют 8, 16, 32 и 64 бит соответственно на всех этих платформах.)
В .NET 6 и более поздних версиях используйте типы CLong и CULong для взаимодействия с типами данных C/C++ long и unsigned long. Следующий пример предназначен для CLong, но можно использовать CULong для абстрагирования unsigned long таким же образом.
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);
// Usage
nint result = Function(new CLong(10)).Value;
При выборе .NET 5 и более ранних версий следует объявить отдельные Windows и подписи, отличные от Windows, чтобы справиться с проблемой.
static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib", EntryPoint = "Function")]
extern static int FunctionWindows(int a);
[DllImport("NativeLib", EntryPoint = "Function")]
extern static nint FunctionUnix(nint a);
// Usage
nint result;
if (IsWindows)
{
result = FunctionWindows(10);
}
else
{
result = FunctionUnix(10);
}
Структуры
Управляемые структуры создаются в стеке и не удаляются, пока метод не вернет значение. Таким образом, по определению, эти структуры "закреплены" (не перемещаются службой сборки мусора). Вы также можете просто взять адрес в блоках неуправляемого кода, если родной код не будет использовать указатель после завершения текущего метода.
Блитблочные структуры более производительны, так как их можно использовать непосредственно на маршалинговом уровне. Попробуйте сделать структуры непреобразуемыми (например, не используйте атрибут bool). См. дополнительные сведения в разделе Непреобразуемые типы данных.
Если структура является непреобразуемой, используйте атрибут sizeof() вместо Marshal.SizeOf<MyStruct>(), чтобы повысить производительность. Как описано выше, чтобы проверить, является ли тип непреобразуемым, попытайтесь создать закрепленный атрибут GCHandle. Если тип не является строкой или считается непреобразуемым, атрибут GCHandle.Alloc вызовет ArgumentException.
Указатели на структуры в определениях необходимо передавать либо через ref, либо использовать unsafe и *.
✔️ НЕОБХОДИМО сопоставить управляемую структуру как можно точнее с формой и именами, которые используются в официальной документации платформы или заголовках.
✔️ РЕКОМЕНДУЕТСЯ использовать C# sizeof() вместо Marshal.SizeOf<MyStruct>() для непреобразуемых структур, чтобы повысить производительность.
❌ НЕ полагайтесь на внутреннее представление типов структур, предоставляемых библиотеками среды выполнения .NET, если оно явно не задокументировано.
❌ Избегайте использования классов для выражения сложных собственных типов с помощью наследования.
❌ ИЗБЕГАЙТЕ использования полей System.Delegate или System.MulticastDelegate для представления полей с указателями функций в структурах.
Поскольку System.Delegate и System.MulticastDelegate не имеют требуемых сигнатур, они не гарантируют соответствие переданного делегата сигнатуре, которую ожидает код на уровне системы. Кроме того, в .NET Framework и .NET Core маршалирование структуры, содержащей System.Delegate или System.MulticastDelegate, из её нативного представления в управляемый объект может дестабилизировать среду выполнения, если значение поля в нативном представлении не является указателем функции, оборачивающим управляемый делегат. В .NET 5 и более поздних версиях маршалирование поля System.Delegate или System.MulticastDelegate из собственного представления в управляемый объект не поддерживается. Используйте вместо System.Delegate или System.MulticastDelegate конкретный тип делегата.
Фиксированные буферы
Массив, как INT_PTR Reserved1[2], должен быть маршалирован в два поля IntPtr, Reserved1a и Reserved1b. Если собственный массив имеет простой тип, используйте ключевое слово fixed, чтобы записать его более точно. Например, SYSTEM_PROCESS_INFORMATION в исходном заголовке выглядит следующим образом:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION;
В C# можно написать такой код:
internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
internal uint NextEntryOffset;
internal uint NumberOfThreads;
private fixed byte Reserved1[48];
internal Interop.UNICODE_STRING ImageName;
...
}
Но с буферами фиксированного размера возникают некоторые ошибки. Буферы неподлежащих прямой проекции типов не маршалируются правильно, поэтому локальный массив следует развернуть в несколько отдельных полей. Кроме того, в .NET Framework и .NET Core до 3.0, если структура, содержащая фиксированное поле буфера, вложена в структуру, неизменяемую, фиксированное поле буфера не будет правильно маршалировано в машинный код.
Устранение неполадок P/Invoke
В следующей таблице перечислены распространенные симптомы с их вероятными причинами и рекомендуемыми исправлениями.
| Симптом | Вероятно, причина | Исправление |
|---|---|---|
| DllNotFoundException | Библиотека не найдена во время выполнения | Проверьте имя библиотеки, путь и платформу. Используйте TryLoad для тестирования загрузки. В Linux проверьте LD_LIBRARY_PATH или rpath. |
| EntryPointNotFoundException | Несоответствие имени экспорта | Проверьте собственные экспорты (dumpbin /exports в Windows, nm -D в Linux). Проверьте, не хватает ли имени C++ (отсутствует extern "C"). Задать EntryPoint явно. |
| AccessViolationException | Несоответствие подписей, использование после освобождения, или отсутствие привязки | Сравнение управляемых и нативных подписей. Проверьте размеры структуры с помощью Marshal.SizeOf<T>() по сравнению с sizeof. Проверьте время существования памяти. Использование сигнатуры для устранения проблемы маршаллинга |
| Незаметное повреждение данных | Неправильный размер или кодировка типов | Добавление ведения журнала граничных событий. Сравните Marshal.SizeOf<T>() с нативным sizeof. Тестирование с помощью известных пар входных и выходных данных. |
| Временные сбои | GC переместил незакреченный объект или собрал делегат | Фиксируют делегаты обратного вызова на всю продолжительность их жизни. Используйте GCHandle или fixed для указателей, удерживающихся между вызовами. |
| Кучи коррупции на свободе | Неправильный распределитель | Сопоставление распределителя: никогда не смешивайте malloc/free с CoTaskMemAlloc/CoTaskMemFree или Marshal.FreeHGlobal. Используйте собственную бесплатную функцию библиотеки. |
Запретить сбор делегатов с помощью GC.KeepAlive
GetFunctionPointerForDelegate При преобразовании делегата в указатель функции сборщик мусора не отслеживает связь между возвращаемым указателем и исходным делегатом. Если делегат имеет право на коллекцию до завершения работы машинного кода с помощью указателя, приложение завершит работу.
Используйте KeepAlive для предотвращения сбора:
var callback = new MyDelegate((level, msgPtr) =>
{
string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
Console.WriteLine($"[{level}] {msg}");
});
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
NativeUsesCallback(fnPtr);
GC.KeepAlive(callback); // Prevent collection — fnPtr does not root the delegate
Если машинный код хранит указатель функции за пределами вызова (например, в качестве постоянного обратного вызова), делегат должен быть коренен в течение всего времени существования, обычно сохраняя его в static поле.
Устранение конфликтов между документацией и собственными заголовками
При написании подписей P/Invoke можно столкнуться с несоответствиями между документацией по онлайн-API и фактическими заголовочными файлами. Файлы заголовков являются авторитетным источником для подписей функций, макетов структур, размеров типов и соглашений о вызовах. При сомнении проверьте подписи P/Invoke с заголовками, а не полагайтесь исключительно на документацию.