Рекомендации по использованию взаимодействия на уровне машинного кода
.NET предоставляет различные способы настройки собственного кода взаимодействия. В этой статье приводятся рекомендации для команд .NET-разработчиков, которые сталкиваются с проблемами взаимодействия на уровне машинного кода.
Общее руководство
Рекомендации в этом разделе относятся ко всем сценариям взаимодействия.
- ✔️ При необходимости используйте
[LibraryImport]
.NET 7+.- Существуют случаи, когда используется
[DllImport]
соответствующим образом. Анализатор кода с идентификатором SYSLIB1054 сообщает вам, когда это так.
- Существуют случаи, когда используется
- ✔️ РЕКОМЕНДУЕТСЯ использовать такое же именование и регистр для методов и параметров, как и для метода машинного кода, который вы хотите вызвать.
- ✔️ ДОПУСТИМО использовать такое же именование и регистр для констант.
- ✔️ РЕКОМЕНДУЕТСЯ использовать типы .NET, которые наиболее сопоставимы с нативным типом. Например, если собственный тип в C# —
unsigned int
, используйтеuint
. - ✔️ Предпочитайте выражение собственных типов более высокого уровня с помощью структур .NET, а не классов.
- ✔️ Рекомендуется использовать указатели функций, а не
Delegate
типы при передаче обратных вызовов в неуправляемые функции в C#. - ✔️
[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 возвращаются значения, которые вызывают исключения (и в результате возвращаемое значение в месте определения становится пустым). |
SetLastError | false |
Зависит от API | Присвойте этому параметру значение true, если для получения значения в API используется GetLastError и Marshal.GetLastWin32Error. Если API устанавливает условие, указывающее на ошибку, перед тем как выполнить другие вызовы, получите информацию об ошибке, чтобы избежать ее непреднамеренной перезаписи. |
CharSet | Определяемый компилятором (указанный в документации по charset) | Если в определении есть строки или символы, используйте CharSet.Unicode или CharSet.Ansi в явном виде |
Указывает реакцию на событие при маршалинге строк и то, как работает ExactSpelling , если задано значение false . Обратите внимание, что в Unix CharSet.Ansi имеет кодировку UTF8. Обычно в Windows используется Юникод, а в Unix — UTF8. См. дополнительные сведения в документации по кодировке. |
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]
.
Руководство по dllImport
✔️ Рассмотрите CharSet
возможность настройки свойства в [DllImport]
среде выполнения, чтобы среда выполнения знала ожидаемое кодирование строк.
✔️ Рекомендуется StringBuilder
избегать параметров. Во время маршалинга 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. Для строк
[Out]
в среде CLR по умолчанию используетсяCoTaskMemFree
(для свободных строк) илиSysStringFree
(для строк, обозначенных какUnmanagedType.BSTR
). Для большинства API с буфером выходной строки: переданный в число символов должен содержать значение NULL. Если возвращаемое значение меньше, чем количество переданных символов, значит, вызов завершился успешно. В таком случае значение — это количество символов без NULL в конце. В противном случае это требуемый размер буфера, включая нуль-символ.
- Передайте 5, получите 4: строка длиной 4 символов с конечным значением NULL.
- Передайте 5, получите 6: строка имеет длину 5 символов, требуется 6 символьных буфера, чтобы сохранить значение NULL. Windows Data Types for Strings (Типы данных Windows для работы со строками)
Логические параметры и поля
Логические значения можно легко спутать. По умолчанию тип .NET bool
маршалируется в тип Windows BOOL
, где имеет 4-байтовое значение. Но типы _Bool
и bool
в C и C++ имеют размер один байт. Это может усложнить поиск ошибок, так как половина возвращаемого значения может не учитываться, что, возможно, изменит результат. См. дополнительные сведения о маршалинге значений .NET bool
для типов C или C++ bool
в документации по настройке маршалинга логического поля.
Идентификаторы GUID
Идентификаторы GUID используются непосредственно в сигнатурах. Многие API Windows принимают такие псевдонимы типа GUID&
, как REFIID
. Если сигнатура метода содержит ссылочный параметр, поместите ref
ключевое слово или [MarshalAs(UnmanagedType.LPStruct)]
атрибут в объявление параметра GUID.
GUID | Идентификатор GUID для передачи по ссылке |
---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
❌ Не используйте [MarshalAs(UnmanagedType.LPStruct)]
ничего, кроме ref
параметров GUID.
Непреобразуемые типы данных
Непреобразуемые типы — это типы данных с одинаковым представлением на битовом уровне в управляемом и машинном коде. Для маршалинга в машинный код и из него эти типы не обязательно преобразовывать в другой формат, что повышает производительность. Поэтому им следует отдавать предпочтение. Некоторые типы не могут быть врезаемыми, но, как известно, содержат содержимое. Эти типы имеют аналогичные оптимизации, как типы, которые не содержатся в другом типе, но не считаются перерезаемыми в полях структур или в целях UnmanagedCallersOnlyAttribute
.
Типы blittable при включенной маршалинге среды выполнения
Непреобразуемые типы данных:
byte
,sbyte
,short
ushort
int
uint
long
ulong
single
double
- структуры с фиксированным макетом, которые имеют только типы значений, допускающих перерезку для полей экземпляра
- чтобы обеспечить фиксированную структуру, необходимо указать
[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
содержит содержимое в виде аргумента, если он не содержится в другом типе и передается значением (не илиout
) ref
в качестве аргумента и любым из следующих элементов:
- 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
Ниже приведен список типов данных, часто используемых в API Windows, и типов данных C#, используемых при вызове в виде кода Windows.
Указанные ниже типы имеют одинаковый размер в 32-разрядной и 64-разрядной версиях Windows независимо от их имен.
Width | 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
, который является C 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++ long
char
, short
int
и 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>()
для непреобразуемых структур, чтобы повысить производительность.
❌ Избегайте использования классов для выражения сложных собственных типов с помощью наследования.
❌ ИЗБЕГАЙТЕ использования полей 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, если структура содержит поле буфера фиксированного размера и вложена в преобразуемую структуру, поле буфера фиксированного размера не маршалируется правильно в машинный код.