Программирование PC Маршалинг в C#. Простые типы Fri, December 13 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.


Маршалинг в C#. Простые типы Печать
Добавил(а) microsin   

Маршалингом (marshalling) называется процесс создания моста между управляемым кодом (managed code) и неуправляемым кодом (unmanaged code; прим. переводчика: далее для упрощения понимания эти термины и некоторые другие буду оставлять без перевода); это подсистема, которая передает сообщения из managed-окружения в unmanaged-окружение и обратно. Это один из основных сервисов, предоставляемых CLR (Common Language Runtime). Здесь приведен перевод статьи [1] (части 1 и 2), автор Mohammad Elsheimy.

Из-за того, что у многих типов в unmanaged-окружении нет таких же типов в managed-окружении, Вам нужно создать подпрограммы преобразования, которые будут конвертировать managed-типы в unmanaged-типы и наоборот; и как раз это и называется процессом маршалинга.

Примечание: многие непонятные термины разъясняются во врезках, также см. раздел "Словарик" в конце статьи.

Напомню, что мы называем код библиотеки .NET "managed" (управляемым, обслуживаемым), потому что он контролируется CLR. Другой код, который не контролируется CLR, называется unmanaged.

Почему применяется маршалинг? Как Вы уже знаете, нет реальной совместимости между managed и unmanaged окружениями. Другими словами, к примеру, в мире .NET нет таких типов, как HRESULT, DWORD и HANDLE, которые существуют в unmanaged коде. Таким образом, при использовании .NET Вам нужно найти замену этим типам, или создать свои собственные необходимые типы. Это и называется маршалинг.

Например, есть unmanaged DWORD; это беззнаковое (unsigned) 32-разрядное целое, так что в .NET мы можем сделать его маршалинг как System.UInt32. Таким образом, System.UInt32 является заменой для unmanaged DWORD. С другой стороны, unmanaged compound types (необслуживаемые составные типы, такие как структуры, объединения union и т. п.) не имеют аналогов или замен в managed-окружении. Так что надо создать свои собственные managed-типы (структуры/классы), которые будут нести те же функции и послужат заменой для используемых unmanaged-типов.

Marshalling process fig1 1

Где нужно применять маршалинг? Маршалинг удобен, когда Вы работаете с unmanaged кодом, как например обращаетесь к Windows API или используете компоненты COM. Это помогает правильно взаимодействовать (т. е. корректно работать) в этих условиях, путем обмена общими данными между этими двумя окружениями.

[Маршалинг простых типов]

В этом разделе рассматривается основные элементы процесса маршалинга. Это базовая часть для всего остального материала про маршалинг.

Данные на C# бывают 2 категорий - простые и составные. Простые типы (int, boolean и т. д.) это те типы, для которых не используются другие типы. В отличие от них, составные типы (структуры и классы) это те типы, которые требуют специальной поддержки, и они сделаны из других типов. При обсуждении простых типов мы также рассмотрим две их категории - типы blittable и non-blittable (составные типы всегда non-blittable).

Концепция типов данных blittable и non-blittable напрямую прикладывается к проблеме преобразования данных между обслуживаемой памятью (managed memory) и необслуживаемой памятью (unmanaged memory). Этот маршалинг выполняется специальным блоком interop marshaller, который при необходимости автоматически вызывается из CLR.

В терминологии разработки программ blittable-типы имеют уникальные для библиотеки .NET характеристики. Очень часто данные в памяти для окружения managed code и unmanaged code представлены и обрабатываются по-разному. Однако типы blittable определены так, что они имеют одинаковое представление в памяти для обоих окружений, так что могут напрямую использоваться совместно для окружения managed code и unmanaged code. Понимание различий между blittable и non-blittable типами может помочь в использовании COM Interop или P/Invoke - двух техник обеспечения совместимости (interoperability) в приложениях .NET.

[Откуда пошел термин blittable]

Операция по копированию памяти иногда называется "block transfer" (передача блока). Этот термин иногда заменяется аббревиатурой BLT (имеется даже инструкция BLT на платформе PDP-10), что произносится как 'blit'. Термин 'blittable' выражает, что это легальная копия объекта, полученная операцией block transfer.

Interoperability в контексте C# и библиотеки .NET - это обеспечение совместимости (совместного использования в одном приложении) managed .NET code и unmanaged code. С помощью Interoperability можно совместно использовать данные между этими двумя окружениями.

Для этого .NET предоставляет 2 способа: COM Interop и P/Invoke. Хотя эти методологии различаются, в обоих случаях должен применяться маршалинг (преобразования между представлениями данных, форматами вызова функций и формата для возврата значений из функций). COM Interop имеет дело с преобразованиями между managed code и объектами COM, в то время как P/Invoke обрабатывает взаимодействие между managed code и Win32 code.

Затем мы рассмотрим механизм передачи и обработки данных в .NET Framework.

[Простые и составные типы данных]

Итак, бывает два типа данных:

• Simple (primitive/basic, простой/базовый тип)
• Compound (complex, составной, сложный тип)

Primitive-типы данных это те, которые не могут быть определены из других типов. Они сами являются базовыми для других типов. Примеры managed primitive это числа наподобие System.Byte, System.Int32, System.UInt32, и System.Double, строки наподобие System.Char и System.String, и хендлы наподобие System.IntPtr.

Compound-типы данных это те, которые строятся из других типов данных. Например, класс или структура, в которой могут инкапсулироваться простые типы и другие compound-типы.

Примечание: в этой статье термины simple, primitive, basic будут использоваться в отношении к таким типам, как целые числа, строки, и т. д. (внимание, не все типы .NET для определения строк являются простыми). Термины compound и complex типов будут использоваться попеременно для классов и структур.

[Blittable и Non-Blittable типы данных]

Большинство типов данных имеют общее представления и для managed, и для unmanaged памяти, и не требуют специальной обработки. Эти типы называют blittable, потому что они не требуют специальной обработки при передачи между managed и unmanaged кодом. Другие типы, которые требуют специальных преобразований, называются non-blittable. Вы можете думать, что большинство простых типов являются blittable, и все составные типы являются non-blittable.

Следующая таблица показывает blittable типы данных, которые есть в .NET (их аналоги в unmanaged коде мы рассмотрим позже):

Описание managed-тип
8-разрядное целое со знаком System.SByte
8-разрядное целое без знака System.Byte
16-разрядное целое со знаком System.Int16
16-разрядное целое без знака System.UInt16
32-разрядное целое со знаком System.Int32
32-разрядное целое без знака System.UInt32
64-разрядное целое со знаком System.Int64
64-разрядное целое без знака System.UInt64
Указатель со знаком System.IntPtr
Указатель без знака System.UIntPtr

[Marshaling для Blittable типов данных]

Вы можете построить маршалинг простого unmanaged типа данных, если отследите его определение, и затем найдете его соответствующий тип-аналог (marshaling type) в managed-окружении.

Числовые типы данных. В таблице ниже перечислены некоторые unmanaged типы данных Windows, их ключевые слова на языке C/C++, и их соответствующие аналоги (marshaling types) в .NET. Как Вы уже наверное догадались, можно отследить каждый unmanaged тип, и найти его соответствующий managed тип. Имейте в виду, что это не полный список, имеются много типов с другими названиями и с аналогичным значением.

Описание тип Windows ключевое слово C/C++ managed-тип ключевое слово C#
8-разрядное целое со знаком CHAR char System.SByte sbyte
8-разрядное целое без знака BYTE unsigned char System.Byte
byte
16-разрядное целое со знаком SHORT short System.Int16 short
16-разрядное целое без знака WORD и USHORT unsigned short System.UInt16
ushort
32-разрядное целое со знаком INT, INT32, LONG и LONG32 int, long System.Int32 int
32-разрядное целое без знака DWORD, DWORD32, UINT и UINT32 unsigned int, unsigned long System.UInt32
uint
64-разрядное целое со знаком INT64, LONGLONG и LONG64 __int64, long long System.Int64 long
64-разрядное целое без знака DWORDLONG, DWORD64, ULONGLONG и UINT64 unsigned __int64, unsigned long long System.UInt64
ulong
Число с плавающей запятой FLOAT float System.Double double

Примечания. Имейте в виду, что long и int отличаются в разных платформах и в разных компиляторах. Для 32-битных версий Windows большинство компиляторов интерпретируют long и int одинаково - как 32-разрядное целое число.

Некоторые типы базируются на версии Windows. Например DWORD это 32 бита на 32-разрядной версии Windows и 64 бита на 64-битной версии Windows (в этой статье подразумевается 32-bit версия Windows).

Знайте, что нет отличий между типами данных Windows и типами данных языка C/C++. Типы данных Windows это просто алиасы (псевдонимы) действительных типов C.

Не смущайтесь, что многие типы ссылаются на один и тот же тип, что они по сути являются просто именами (алиасами, псевдонимами). Например, INT, INT32, LONG, и LONG32 (этот список можно продолжить) все являются 32-битными целыми числами.

Чтобы упростить понимание, в этой статье будут рассмотрены примеры для Windows API.

Хотя некоторые unmanaged-типы имеют имена, похожие на некоторые имена managed-типов, у них может быть разное значение. Например LONG очень похоже на System.Long, но LONG это 32-битное число, а System.Long 64-битное!

Если Вы хотите больше узнать про эти типы данных, прочитайте статью "Windows Data Types" из библиотеки MSDN.

Текстовые типы данных. В дополнение к числовым типам данных нужно знать, как делать маршалинг unmanaged текстовых типов данных (таких как одиночный символ или строка. Однако эти биты non-blittable, так что нуждаются в специальной обработке.

Ниже в таблице кратко перечислены необслуживаемые и обслуживаемые текстовые типы данных.

Описание unmanaged-тип managed-тип
8-разрядный символ ANSI CHAR System.Char
16-разрядный символ Unicode WCHAR System.Byte
Строка 8-разрядных символов ANSI LPSTR, LPCSTR, PCSTR и PSTR System.String
Строка 18-разрядных символов Unicode LPWSTR, LPCWSTR, PCWSTR и PWSTR System.String

[Проверка определения типа]

Как уже было сказано, для упрощения в этой статье мы будем использовать Windows API как базу для обсуждения. Нужно понимать, все типы данных Windows Data Types (INT, DWORD и т. д.) это просто имена (технически это просто определения с оператором typedef) для действительных типов C. То есть множество имен могут обращаться к одному и тому же типу наподобие INT и LONG.

Таким образом образом, мы можем сказать, что LONG определен как int языка C и DWORD определен как unsigned long языка C.

Типы INT и LONG просты для маршалинга. Однако есть примитивные типы, определения которых Вам понадобится отследить, чтобы понять как делать для них маршалинг.

Помните, что мы будем использовать документацию MSDN (а именно статью "Windows Data Types") при трекинге unmanaged типов данных (особенно типов данных Windows).

Далее рассмотрим некоторые типы, определенные как "другие типы". Вы можете думать, что эти типы являются алиасами базовых типов. Не всегда именно так, все же это зависит от платформы.

HRESULT: как Вы можете увидеть большое количество функций возвращают тип HRESULT, который представляет своим значением результат (статус) выполненной (или не выполненной, если ошибка) операции. Если HRESULT равен 0, то функция успешно завершила свою работу, иначе значение будет представлять код ошибки (error code) или информацию о состоянии (status information) операции. Определен как LONG, и LONG соответственно определен как 32-bit число со знаком (int). Таким образом, Вы можете сделать маршалинг HRESULT как System.Int32.

BOOL и BOOLEAN: и тот, и другой это двоичные типа, которые означают либо TRUE (не ноль) или FALSE (ноль). Большое отличие BOOL от BOOLEAN состоит в том, что BOOL определен как INT, таким образом занимая 4 байта, а BOOLEAN определен как BYTE, и занимает только 1 байт.

HFILE: хендл (handle, идентификатор) открытого файла, используемый функциями файлового ввода/вывода (Windows File IO functions), наподобие OpenFile(). Этот тип определен как INT, и INT соответственно определен как 32-bit число со знаком. Таким образом, можно сделать маршалинг HFILE в System.Int32. Хотя HFILE определен как INT, идентификаторы handle должны быть маршалированы как System.IntPtr, которые внутри библиотеки инкапсулируются в сырой handle. Если быть точным, то Вам лучше всего сделать маршалинг unmanaged handle как System.Runtime.InteropServices.SafeHandle или CriticalHandle, потому что это идеальный маршалируемый тип для любого handle. Таким образом, file handle лучше всего маршалировать как Microsoft.Win32.SafeHandles.SafeFileHandle, который унаследован от SafeHandleZeroOrMinusOneIsInvalid, и который в свою очередь унаследован от абстрактного класса System.Runtime.InteropServices.SafeHandle. Для дополнительной информации по поводу идентификаторов handle обратитесь к секции "Маршалинг для handle" этой статьи (см. далее).

Дополнительно есть типы, относящиеся к переменным, зависящим от операционной системы, например:

INT_PTR: указатель на знаковое целое. Определен как INT64 в 64-битных OS, иначе как INT.

LONG_PTR: указатель на signed long. Определен как INT64 в 64-битных OS, иначе как LONG.

UINT_PTR: указатель на беззнаковое целое. Определен как DWORD64 в 64-битных OS, иначе как DWORD.

ULONG_PTR: указатель на unsigned long. Определен как DWORD64 в 64-битных OS, иначе как DWORD.

Имейте в виду, что между переменной и указателем на переменную большая разница. Переменная ссылается непосредственно на значение переменной в памяти. Однако указатель содержит адрес некоей величины в памяти (это может быть адрес переменной):

Marshalling pointers fig2 1

На этом рисунке видно, что переменная i содержит значение 320, и Вы можете получить значение из переменной напрямую (direct access). Указатель ptr, с другой стороны, содержит адрес переменной i. Таким образом, указатель косвенно содержит значение переменной i (не прямой доступ, indirect access). Именно поэтому в случае указателя нельзя получить значение переменной непосредственно. Нужна операция разрешения ссылки, разадресация (dereference), чтобы получить значение переменной.

В дополнение для текстовых типов данных есть типы переменных, базирующиеся на Unicode (строки и буферы мы еще рассмотрим). Примеры:

TBYTE и TCHAR: если задано макроопределение UNICODE (оператором #define), то WCHAR, иначе CHAR.

LPCTSTR, LPTSTR, и PCTSTR: если задано макроопределение UNICODE, то LPCWSTR, иначе LPCSTR.

PTSTR: если задано макроопределение UNICODE, то PWSTR, иначе PSTR.

Обратите внимание, что некоторые типы имеют присоединенные специальные символы в своих именах - это общепринятые обозначения. Например, A и названии текстового типа означает ANSI, W означает Wide (т. е. Unicode, код символа занимает 2 байта). Дополнительно буква T в обозначении текстовых типов имеет разное информационное значение, в зависимости от OS. Другой пример - префикс P (в нижнем регистре) что означает указатель (pointer), и LP означает дальний указатель (long pointer). LPC означает дальний указатель на константу (long pointer constant).

[Вариативные типы]

Дополнительно Win32 API задает типы VOID, LPVOID и LPCVOID. VOID в переменных функции означает, что функция не принимает никаких значений. Если у функции тип VOID, то это значит функция не возвращает никаких значений (это процедура). Пример функции, которая не принимает аргументов:

DWORD GetVersion(VOID);

LPVOID и LPCVOID определены как указатель на любой тип (вариативный тип, variant). Это означает, что тип, на который указывает указатель, может быть любым (указатель это чистый адрес). Такой указатель может быть маршалирован как целое число, строка, handle, или даже составной тип - все что Вы захотите. Дополнительно Вы можете маршалировать LPVOID и LPCVOID как System.IntPtr, таким способом Вы можете установить адрес указателя на любой объект в памяти. Дополнительно Вы можете маршалировать эти указатели как указатели на объект. Например, маршалирование LPCVOID как System.Int32* (указатель на целый тип) в небезопасном коде (unsafe code). Более того, Вы можете использовать небезопасный код и маршалировать его как void*. Кроме того, Вы можете маршалировать его как System.Object, так что можете установить его на любой тип. Про небезопасный код подробнее см. [4].

Следует упомянуть, что при работе с VOID-ами рекомендуется декорировать Вашу переменную атрибутом MarshalAsAttribute, указывая UnmanagedType.AsAny, что говорит компилятору выполнять процесс маршалинга и установить тип аргумента во время выполнения (runtime). См. последний раздел "Управление процессом маршалинга" для дополнительной информации по этому атрибуту.

Если Вы работаете с традициональным Visual Basic, то думайте про LPVOID и LOCVOID как о типе Variant - это должно помочь.

Если нужно взаимодействие с традиционным кодом на Visual Basic, Вы можете использовать для типа Variant тот же способ маршалирования, какой используется для LPVOID и LPCVOID.

Давайте попробуем маршалинг простых типов. Создадим метод PInvoke для функции MessageBoxEx(). Пример демонстрирует, как точно контролировать процесс маршалинга, используя атрибут MarshalAsAttribute. Подробнее этот атрибут и дополнительную информацию мы также рассмотрим в последней части этой статьи "Управление процессом маршалинга". Идентификаторы handle раскрыты в секции "Маршалинг идентификаторов handle".

Следующий пример создает метод PInvoke для функции MessageBoxEx() и вызывает её для отображения дружественного сообщения для пользователя.

Определение функции MessageBoxEx(), Unmanaged Signature (C++):

int MessageBoxEx(HWND hWnd,
                 LPCTSTR lpText,
                 LPCTSTR lpCaption,
                 UINT uType,
                 WORD wLanguageId);

Та же самая функция MessageBoxEx(), managed-версия (C#):

// CharSet.Unicode задает использование UNICODE.
// Используйте либо либо этот способ управления
// всей функцией, или Вы можете управлять параметрами
// индивидуально, используя атрибут MarshalAsAttribute.
[DllImport("User32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.I4)]
static extern Int32 MessageBoxEx
               (IntPtr hWnd,
               // Маршалинг как символы Unicode
               [param: MarshalAs(UnmanagedType.LPTStr)]
               String lpText,
               // Маршалинг как символы Unicode
               [param: MarshalAs(UnmanagedType.LPTStr)]
               String lpCaption,
               // Маршалинг как 4-байтное (32-bit) беззнаковое целое
               [param: MarshalAs(UnmanagedType.U4)]
               UInt32 uType,
               // Маршалинг как 2-байтное (16-bit) беззнаковое целое
               [param: MarshalAs(UnmanagedType.U2)]
               UInt16 wLanguageId);

Дополнительную информацию по поводу маршалинга строк см. в разделе "Маршалинг строк и буферов".

[Главное правило маршалинга]

Имейте в виду следующее: фреймворк .NET позволяет Вам получить гранулярный уровень управления процессом маршалинга, и поэтому эта система очень сложная. Однако все можно упростить.

В большинстве случаев Вы можете игнорировать атрибуты, просто используя аналоги типов, и CLR постарается обработать эту ситуацию наилучшим образом. Скорее всего не требуется использовать managed signed int для unmanaged-окружений. Вы можете использовать managed signed int для unmanaged unsigned int, и наоборот. Вы также можете маршалировать SHORT как System.Char!

Ключевой принцип состоит в том, что пока маршалированный managed-тип занимает тот же объем памяти, что и не unmanaged-тип, Вы находитесь в безопасности. Хотя удержание этого правила в нужной позиции избавит Вас от нежелательных ошибок, такой вариант может быть довольно сложен, чтобы помнить о нем и поддерживать его выполнение.

Еще нужно иметь в виду, что информацию из этой статьи можно применить также и в любом unmanaged-окружении. Т. е. Вы можете применить её, работая с Windows API, библиотеками C/C++, Visual Basic, COM, OLE, ActiveX, и т. д. Однако ради простоты мы будем говорить только о Windows API к контексте образца для unmanaged-кода.

Примечание: в этом описании подразумевается, что используется 32-разрядная версия Windows. Таким образом DWORDs, HANDLE, и т. п. занимают 4 байта. На 64-битных версиях Windows они занимают 8 байт.

[Маршалинг двоичных типов (boolean)]

Как Вы наверное помните, имеется 2 двоичных типа, которые относятся к простым типам. В общем маршалинг простых типов очень простой процесс, и boolean не исключение. Однако, типы boolean не являются blittable-типами. Таким образом, они требуют некоторой обработки.

Первое, что нужно помнить, это то, что Windows определяет 2 типа для boolean-переменных (честно говоря, непонятно зачем):

BOOL: определен как INT, так что он занимает 4 байта.
BOOLEAN: определен как BYTE, занимая 1 байт.

Принцип обработки обоих этих типов одинаковый - если не 0, то значит это истина (TRUE), и если 0, то ложь (FALSE). Эти 2 типа существуют только в Windows SDK. Другие рабочие окружения могут определять другие типы с похожими именами.

Хотя верно, что и BOOL, и BOOLEAN лучше всего маршалируются как System.Boolean, BOOL может также маршалироваться как System.Int32, потому что это все-таки 32-bit int. С другой стороны, BOOLEAN можно маршалировать как System.Byte или System.U1, поскольку он определен как 8-битное целое. Вы запомнили это правило?

Имейте в виду, что как бы Вы не делали маршалинг типа boolean - либо как System.Boolean, либо как System.Int32, либо как System.Byte, рекомендуется применить к переменной атрибут MarshalAsAttribute, чтобы задать нижележащий unmanaged-тип. Например, чтобы указать нижележащий тип BOOL, укажите MarshalAsAttribute-конструкторе UnmanagedType.Bool (рекомендуется) или UnmanagedType.I4. С другой стороны BOOLEAN можно задать как UnmanagedType.U1. Если Вы опустите MarshalAsAttribute, то CLR будет подразумевать поведение по умолчанию для System.Boolean, которое займет 2 байта. Для дополнительной информации по атрибуту MarshalAsAttribute см. последний раздел "Управление процессом маршалинга".

Давайте разберем пример булевого маршалинга. К счастью, очень многие функции возвращают BOOL, показывая этим результат выполнения функции (успешное завершение при TRUE и неудача если FALSE).

Вот определение всеми любимой функции CloseHandle():

BOOL CloseHandle(HANDLE hObject);

Для неё получится следующая managed-версия:

[DllImport("Kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
// Дополнительно можно выполнить маршалинг так:
// [return: MarshalAs(UnmanagedType.I4)]
// Кроме того, можно заменить System.Boolean на System.Int32
static extern Boolean CloseHandle(IntPtr hObject)

Маршалинг HANDLE мы также скоро рассмотрим (в разделе "Маршалинг для handle"). Пока тут Вы можете видеть, что handle маршалируется к System.IntPtr.

[Маршалинг текстовых типов данных]

Давайте рассмотрим, как маршалировать строки и буферы. Мы будем попеременно использовать термины string и buffer, имея в виду последовательность символов.

В managed-окружении есть 2 типа для маршалинга unmanaged строковых буферов. Это System.String и System.Text.StringBuilder. Конечно, оба этих типа хранят последовательность символов. Однако StringBuilder более продвинутый, поскольку он очень эффективно работает для mutable string по сравнению с System.String.

В переводе с английского слова mutable и immutable соответственно означают "можно изменить" и "нельзя изменить". Т. е. в этом контексте смысл тот же: mutable string означает изменяемую строку, а immutable string означает, что строку поменять нельзя.

Значение этих слов одинаковое в C# / .NET и других языках программирования, хотя (что очевидно) имена типов могут отличаться, как и могут отличаться и другие подробности реализации.

Тип System.String является немутируемым, т. е. после его создания его состояние поменять нельзя. Класс StringBuilder является мутируемым.

Немутируемые строки применять предпочтительнее, поскольку это ускоряет выполнение кода (пропускаются проверки на переполнение буфера).

Каждый раз, когда Вы используете один из методов класса System.String, или когда передаете System.String в функцию, то создаете в памяти новый строковый объект, который конечно требует нового места в памяти для этого нового объекта. Дополнительно если функция изменяет строку, то Вы не получите результат обратно. Это потому, что System.String называется immutable. С другой стороны, StringBuilder не требует перераспределения места в памяти, за исключением случаев, когда Вы превысите объем его хранилища. Помимо маршалинга Вы должны использовать StringBuilder, чтобы соответствовать проблемам эффективности, если Вы часто используете одну и ту же строку множество раз.

Чтобы сохранить System.String неизменяемым (immutable), маршалер копирует содержимое строки в другой буфер перед вызовом функции, и затем передает этот буфер функции. Если Вы передаете строку по ссылке (by reference, параметр с атрибутом ref), то маршалер копирует содержимое буфера в оригинальную строку, когда произойдет возврат из функции.

С другой стороны, когда используется StringBuilder, он передает ссылку на внутренний буфер StringBuilder, если строка передается по значению (by value, когда у параметра нет атрибута ref). Передача StringBuilder по ссылке в действительности передает в функцию указатель на объект StringBuilder в памяти, а не указатель на сам буфер.

Примечание: подробнее про передачу параметра по значению (by value) или по ссылке (by reference) см. в секции "Механизм передачи параметра".

Еще одной фичей StringBuilder является возможность указать размер буфера. Как мы увидим, это во многих случаях очень полезно.

Если обобщить, то System.String желательнее использовать при работе с immutable строками, особенно для входных (In) аргументов. С другой стороны, System.Text.StringBuilder желательнее использовать для изменяемых строк, особенно для выходных (Out) аргументов.

Примечательно, что StringBuilder не может использоваться внутри составных типов (compound types). Так что в этих случаях используйте String.

Еще следует отметить, что Вы можете передать массив System.Char вместо System.String или System.Text.StringBuilder. Другими словами, Вы можете маршалировать unmanaged strings как managed-массивы System.Char (или System.Int16, помните?).

[Поддержка символьной кодировки строк]

Кодировка очень важна, потому что она определяет значение, которое может хранить символ, и размер, который он занимает в памяти. Например, если символ закодирован в кодировке ANSI, то он может быть только одним из 256 символов. Далее, если он закодирован в Unicode, то может принимать значение одного из 65536 символов, что очень хорошо подходит для большинства языков.

Примечание: если хотите больше узнать про Unicode, то можете просмотреть официальный сайт www.Unicode.org. Дополнительно книга "Programming Windows" Charles Petzold содержит отличное введение в Unicode и наборы символов (обязательно для чтения).

Для управления кодировкой символов при маршалинге unmanaged-типов можно выбрать один из способов или комбинировать их оба, если это нужно. Вы можете управлять кодировкой всей функции (например, на уровне функции), или можете спуститься ниже и управлять процессом кодировки на гранулярном уровне, путем управления каждым аргументом по отдельности (второй подход, требуется в определенных случаях, например с функцией MultiByteToWideChar()).

Для изменения кодировки всей функции, DllImportAttribute предоставляет свойство CharSet, которое показывает способ кодирования (примененный набор символов, character set) для строк и аргументов функции. Это свойство может получать одно из нескольких значений:

CharSet.Auto (умолчание CLR): строки кодируются на базе операционной системы; это Unicode на Windows NT и ANSI для других версий Windows.
CharSet.Ansi (умолчание C#): строки всегда кодируются как 8-bit ANSI.
CharSet.Unicode: строки кодируются всегда как 16-bit Unicode.
CharSet.None: устаревший вариант. Работает так же, как и CharSet.Ansi.

Примечание: имейте в виду, что если Вы не установили свойство CharSet, CLR автоматически установит свойство в CharSet.Auto. Однако некоторые языки переназначают поведение по умолчанию. Например умолчание C# это CharSet.Ansi.

Стоит упомянуть, что многие функции, которые принимают строки и буферы в действительности просто имена (технически это typedef-ы)! Это не реальные функции, а точки входа (алиасы) реальных функций. Например, функция ReadConsole() это только точка входа, которая перенаправляет вызов либо на ReadConsoleA() если задана кодировка ANSI, либо на ReadConsoleW() если задана кодировка Unicode (A обозначает ANSI, и W обозначает Wide, т. е. "широкий", что относится к Unicode). Таким образом, Вы можете фактически поменять эту точку вход, изменив имя метода PInvoke, чтобы он соответствовал правильной функции, или изменив DllImportAttribute.EntryPoint на имя требуемой функции. В обоих случаях установка DllImportAttribute.CharSet станет бесполезной.

Если Вы хотите управлять кодировкой на гранулярном уровне, то можете применить атрибут MarshalAsAttribute к аргументу, указывающий нижележащий unmanaged-тип.

Обычно Вам потребуется унифицировать кодировку символов всех Ваших native-функций и типов. То есть во всех функциях должен использоваться либо Unicode, либо ANSI. Очень редко бывают случаи, когда у некоторых функций должна использоваться другая кодировка символов.

Это функция, написанная на языке Windows, т. е. C/C++. Обычно имеется в виду интерфейс обращения к функциям Windows API.

Стоит упомянуть, что для строк фиксированной длины Вам нужно установить свойство SizeConst у MarshalAsAttribute на размер буфера.

Примечание: эти техники не ограничиваются только аргументами! Вы можете использовать их также с переменными и составными типами (compound types). Составные типы будут рассмотрены далее.

Давайте разберем примеры маршалинга строк. Посмотрите на unmanaged-функции ReadConsole() и FormatConsole(), и как их нужно вызывать в managed-окружении. Ниже приведена декларация обоих функций и других функций, которые нужны для примера:

HANDLE GetStdHandle(DWORD nStdHandle);
 
BOOL ReadConsole(HANDLE hConsoleInput,
                 [out] LPVOID lpBuffer,
                 DWORD nNumberOfCharsToRead,
                 [out] LPDWORD lpNumberOfCharsRead,
                 LPVOID lpReserved);
 
DWORD GetLastError(void);
 
DWORD FormatMessage(DWORD dwFlags,
                    LPCVOID lpSource,
                    DWORD dwMessageId,
                    DWORD dwLanguageId,
                    [out] LPTSTR lpBuffer,
                    DWORD nSize,
                    va_list* Arguments);

И вот managed-версия вместе с тестовым кодом:

// Для получения handle к конкретному устройству консоли
[DllImport("Kernel32.dll")]
static extern IntPtr GetStdHandle(
    [param: MarshalAs(UnmanagedType.U4)]
    int nStdHandle);
 
// Используется вместе с GetStdHandle() для получения входного
//  буфера консоли
const int STD_INPUT_HANDLE = -10;
// Указывает DLL вместе с кодировкой (character set)
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool ReadConsole(
    // Handle к входному устройству
    IntPtr hConsoleInput,
    // Буфер, в который пишется ввод
    [param: MarshalAs(UnmanagedType.LPTStr), Out()]
    // [param: MarshalAs(UnmanagedType.AsAny)]
    StringBuilder lpBuffer,
    // Количество символов для чтения
    [param: MarshalAs(UnmanagedType.U4)]
    uint nNumberOfCharsToRead,
    // Выводит количество прочитанных символов
    [param: MarshalAs(UnmanagedType.U4), Out()]
    out uint lpNumberOfCharsRead,
    // Зарезервировано = всегда устанавливается в NULL
    [param: MarshalAs(UnmanagedType.AsAny)]
    uint lpReserved);
 
// Для получения кода последней произошедшей ошибки
[DllImport("Kernel32.dll")]
[return: MarshalAs(UnmanagedType.U4)]
static extern uint GetLastError();
 
// Запрашивает сообщения об ошибках
[DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.U4)]
static extern uint FormatMessage(
    // Опции
    [param: MarshalAs(UnmanagedType.U4)]
    uint dwFlags,
    // Источник, откуда берется сообщение
    // [param: MarshalAs(UnmanagedType.AsAny)]
    [param: MarshalAs(UnmanagedType.U4)]
    uint lpSource,
    // Код сообщения = код ошибки (error code)
    [param: MarshalAs(UnmanagedType.U4)]
    uint dwMessageId,
    // Language ID (зарезервировано)
    [param: MarshalAs(UnmanagedType.U4)]
    uint dwLanguageId,
    // Выводит сообщение об ошибке
    [param: MarshalAs(UnmanagedType.LPTStr), Out()]
    out string lpBuffer,
    // Размер сообщения об ошибке
    [param: MarshalAs(UnmanagedType.U4)]
    uint nSize,
    // Дополнительные опции
    [param: MarshalAs(UnmanagedType.U4)]
    uint Arguments);
 
// Опции сообщения
const uint FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x0100;
const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x0200;
const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x1000;
const uint FORMAT_MESSAGE_FLAGS =
    FORMAT_MESSAGE_ALLOCATE_BUFFER |
    FORMAT_MESSAGE_IGNORE_INSERTS |
    FORMAT_MESSAGE_FROM_SYSTEM;
 
// Источник сообщения
public const int FORMAT_MESSAGE_FROM_HMODULE = 0x0800;
 
static void Main()
{
    // Handle к входному буферу
    IntPtr handle = GetStdHandle(STD_INPUT_HANDLE);
 
    const int maxCount = 256;
 
    uint noCharacters;
    StringBuilder builder = new StringBuilder(maxCount);
 
    if (ReadConsole(handle, builder, (uint)maxCount,
        out noCharacters, 0) == false) // false = non-zero = неудача
    {
        string errMsg;
        FormatMessage(FORMAT_MESSAGE_FLAGS,
            FORMAT_MESSAGE_FROM_HMODULE,
            GetLastError(),
            0,  // означает NULL
            out errMsg,
            0,  // максимальная длина
            0); // означает NULL
        Console.WriteLine("ERROR:\n{0}", errMsg);
    }
    else // true = zero = успех
    {
         // Вывод сообщения без новой строки
        Console.WriteLine("Пользователь ввел: = " +
            builder.ToString().Substring(0,
            builder.Length - Environment.NewLine.Length));
    }
    Console.WriteLine(new string('-', 25));
 
    builder = new StringBuilder(maxCount);
 
    // Недопустимый handle
    handle = GetStdHandle(12345);
    if (ReadConsole(handle, builder, (uint)maxCount,
        out noCharacters, 0) == false) // false = non-zero = неудача
    {
        string errMsg;
        FormatMessage(FORMAT_MESSAGE_FLAGS,
            FORMAT_MESSAGE_FROM_HMODULE,
            GetLastError(),
            0,  // означает NULL
            out errMsg,
            0,  // максимальная длина
            0); // означает NULL
        Console.WriteLine("ERROR: {0}", errMsg);
    }
    else // true = zero = успех
    {
        // Исключение символов новой строки
        Console.WriteLine("Пользователь ввел: = " +
            builder.ToString().Substring(0,
            builder.Length - Environment.NewLine.Length));
   }
}

В последнем коде демонстрируются следующие полезные техники:

• До настоящего момента идентификаторы handle должны быть маршалированы как System.IntPtr. В следующей секции более подробно будут рассмотрено маршалирование handle.

• Поскольку и LPVOID, и LPCVOID оба определены как указатель на неопределенный тип (pointer to a variant, т. е. указатель на любой тип), Вы можете установить его в любой тип, какой захотите. Это очень похоже на System.Object в методологии .NET, или на тип Variant для людей, которые привыкли программировать на Visual Basic. В нашем примере мы маршалировали LPVOID как System.UInt32, и установили его в 0. И опять, Вы свободны играть с маршалированием типов. LPVOID и LPCVOID оба 32-bit целые числа. Почему же не маршалировать их просто в любой из 32-разрядных managed-типов, и забыть об этом? Дополнительно Вы можете маршалировать их как System.IntPtr, и передать System.IntPtr.Zero, чтобы показать значение NULL. Кроме того, Вы можете маршалировать их как System.Object, и установить их в любое значение, даже в null, чтобы показать значение NULL. Variant обсуждался ранее в разделе "Marshaling для Blittable типов данных".

• va_list* это указатель на массив специальных аргументов. Вы можете маршалировать его как массив, или System.IntPtr. System.IntPtr желательнее, если Вы намерены передать его как значение NULL.

• Если функция требует передачи параметра по значению (by value) или по ссылке (by reference), то Вы можете добавить к параметру требуемые модификаторы наподобие ref и out, и декорировать параметр либо с InAttribute, либо с OutAttribute, либо и так и так. В секции "Передача аргумента по значению или по ссылке" обсуждаются параметры by-value и by-reference.

• Хотя DWORD определен как беззнаковое 32-разрядное число (unsigned int) и вроде должен бы маршалироваться как System.UInt32, мы находим, что GetStdHandle() может получить три значения: -10 для устройства ввода (input device), -11 для устройства вывода (output device), и -12 для устройства вывода ошибок (error device, обычно это output device). Хотя System.UInt32 не поддерживает отрицательные значения, Windows обработает для Вас эту ситуацию. Она преобразует значение со знаком (signed) в его эквивалентное значение без знака (unsigned). Таким образом, Вам не нужно беспокоиться по поводу передаваемого значения. Однако имейте в виду, что unsigned-значения слишком отличаются (с точки зрения многих разработчиков, не знакомых с дополнительным кодом). Например, unsigned-значение для -11 равно 0xFFFFFFF5! Не выглядит ли это для Вас странным? Если да, то ознакомьтесь с документацией по двоичному представлению отрицательных чисел.

[Маршалинг для handle. Generic Handle]

Что вообще такое handle? Это указатель на какой-то ресурс, загруженный в память, наподобие handle для консоли стандартного ввода, вывода, и устройства для вывода ошибок, handle для окна, handle для контекста устройства (device context, DC), и т. п.

Есть множество типов handle в unmanaged-коде, вот некоторые из них:

HANDLE: это наиболее широко используемый тип handle в unmanaged-окружении. Он представляет generic handle (стандартный хендл).

HWND: наиболее широко используется для приложений Windows. Это handle для окна или элемента управления (control, имеется в виду визуальные элементы интерфейса типа кнопок, галочек, списков и т. п.).

HDC, HGDIOBJ, HBITMAP, HICON, HBRUSH, HPEN и HFONT: если Вы работаете с GDI, то Вам знакомы эти типы handle. HDC это хендл к device context (DC), объект для рисования. HGDIOBJ это хендл для любого объекта GDI. HBITMAP это хендл для растровой картинки, HICON это хендл для иконки. HBRUSH это хендл для кисти, HPEN это хендл для пера, и HFONT это хендл для шрифта.

HFILE: хендл для файла, открытого любой из функций Windows File IO наподобие OpenFile().

HMENU: хендл на меню или на пункт меню.

Примечание: Вы наверное снова заметили, что в большинстве названий типов используются префиксы и суффиксы для идентификации назначения типа. Например, типы для handle имеют префикс H, в то время как некоторые указатели имеют суффикс _PTR или префикс P или LP. Хотя строки с буквой W имеют кодировку Unicode, кодировки строк с буквой T базируются на операционной системе.

Идентификаторы handle можно маршалировать как managed-тип System.IntPtr, который представляет указатель на объект в памяти. Следует упомянуть о том, что поскольку System.IntPtr представляет указатель на объект, не заботясь от том, какого типа этот объект, Вы можете использовать System.IntPtr маршалирования любого типа, не только handle. Однако это не рекомендуется, поскольку сложнее будет работать, и не очень гибко, хотя и предоставляет больше возможности по управлению объектом в памяти.

Дополнительно в .NET Framework начиная с версии 2.0 добавлены новые managed-типы для работы с unmanaged handle. Также добавлено новое пространство имен (namespace) Microsoft.Win32.SafeHandles, которое содержит новые типы. Другие типы имеются в System.Runtime.InteropServices. Эти типы называются managed handle.

Managed handle позволяют Вам передавать в unmanaged-код идентификатор handle на unmanaged-ресурс (наподобие DC), обернутый в управляемый класс (managed class).

Есть два вида managed handle - safe handle и critical handle.

[Safe Handle]

Safe handle (переводится как безопасный хендл) представлен абстрактным классом System.Runtime.InteropServices.SafeHandle. Идентификаторы safe handle предоставляют защиту от атак recycling security путем подсчета ссылок (что делает медленнее работу с safe handle). Дополнительно safe handle предоставляет критическую финализацию для ресурсов handle. Напомню, что термин финализация означает освобождение объекта и занимаемых им ресурсов в памяти, и критическая финализация (critical finalization) гарантирует, что финализация произойдет в любом случае. на рис. 2.2 показано определение SafeHandle и его наследников.

Marshalling SafeFileHandle and descendants fig2 2

Как показано на диаграмме рис. 2.2, SafeHandle является базовым классом, который представляет любой безопасный хендл (safe handle). Он наследуется из System.Runtime.ConstrainedExecution.CriticalFinalizerObject, что гарантирует процесс финализации. Вот общие члены класса SafeHandle:

IsClosed: возвращает значение, показывающее закрыт handle или нет.

IsInvalid: Abstract (абстрактное поле). Если его переназначить (overridden), возвратит значение, показывающее - допустим ли хендл или нет.

Close() и Dispose(): оба этих метода закрывают handle и освобождают его ресурсы. Внутренне они полагаются на абстрактный метод ReleaseHandle(), который освобождает handle. Таким образом, классы, унаследованные от SafeHandle, должны реализовать этот член класса. Избегайте ситуации, когда Dispose() наследуется от интерфейса System.IDispose, который реализован классом SafeHandle, и Close() не делает ничего кроме как вызывает метод Dispose(). Таким образом, Вы строго должны сделать освобождение dispose (close, закрытие) хендла, как только завершили работу с ним.

ReleaseHandle(): Protected Abstract (защищенный абстрактный метод). Используется для предоставления кода очистки хендла. Эта функция должна вернуть true, если ресурсы успешно освобождены, иначе false. В случае false метод генерирует исключение ReleaseHandleFailed Managed Debugging Assistant (MDA), которе не будет прерывать выполнение Вашего кода, однако предоставит Вам сигнал о проблеме. Имейте в виду, что ReleaseHandle() внутренне вызывается методом Dispose().

SetHandle(): Protected (защищенный метод). Устанавливает handle в указанный существующий handle.

SetHandleAsInvalid(): устанавливает handle как недопустимый (invalid), после чего его нельзя использовать.

DangerousGetHandle(): возвратит System.IntPtr, который представляет handle. Остерегайтесь ситуации, если Вы вызвали SetHandleAsInvalid() перед вызовом DangerousGetHandle(), это вернет оригинальный handle, который не является недопустимым.

DangerousRelease(): вручную освобождает handle небезопасным (unsafe) способом. Вместо этого рекомендуется использовать методы Close() или Dispose().

DangerousAddRef(): инкрементирует счетчик ссылок для handle. Не рекомендуется использовать ни DangerousRelease(), ни DangerousAddRef(), вместо этого используйте безопасные (safe) методы. Однако, когда работаете с COM, Вы будете использовать эти функции.

Примечание: не используйте небезопасные (unsafe) методы, за исключением случаев, когда их действительно нужно использовать, потому что они пропускают уровень защиты, предоставленный безопасными хендлами (safe handles).

Поскольку SafeHandle абстрактный, Вы должны либо реализовать его, либо использовать из классов с его реализацией. Только два класса из нового namespace Microsoft.Win32.SafeHandles реализуют SafeHandle, и оба также абстрактные:

SafeHandleMinusOneIsInvalid: представляет safe handle, значение -1 которого говорит, что хендл недопустим. Таким образом, свойство IsInvalid даст true только если handle == -1.

SafeHandleZeroOrMinusOneIsInvalid: представляет safe handle, у которого значение 0 или -1 говорит о том, что недопустим. Следовательно, IsInvalid вернет true только если handle равен 0 или -1.

Обратите внимание, что выбор между двумя реализациями происходит до типа нижележащего handle. Если считается, что хендл недопустим при установке -1, то используйте SafeHandleMinusOneIsInvalid. Если считается, что он недопустим при значении 0 или -1, используйте SafeHandleZeroOrMinusOneIsInvalid. Использование правильного класса для handle гарантирует, что методы наподобие IsInvalid() возвратят корректные результаты. Это также гарантирует, что CLR пометит handle как мусор только в том случае, если он недопустим (invalid).

Если нужно предоставить safe handle для Вашего объекта, то нужно будет наследовать из SafeHandleMinusOneIsInvalid, SafeHandleZeroOrMinusOneIsInvalid, или даже из SafeHandle. Знайте, что Вам всегда нужно переназначить (override) метод ReleaseHandle(), потому что ни SafeHandleMinusOneIsInvalid, ни SafeHandleZeroOrMinusOneIsInvalid этого не делают.

Как показано на диаграмме, из SafeHandleZeroOrMinusOneIsInvalid наследуются 2 класса:

SafeFileHandle: класс-обертка для IO device handle (например HFILE). Класс перезадает (overrides) внутри себя ReleaseHandle() и вызывает unmanaged-функцию CloseHandle() чтобы закрыть handle. Используйте этот класс, когда работает с хендлами HFILE в функцих файлового ввода/вывода (Windows File IO) наподобие OpenFile() и CreateFile(). Внутренне System.FileStream использует HFILE как SafeFileHandle, и предоставляет конструктор, который принимает SafeFileHandle.

SafeWaitHandle: если работаете с неуправляемыми объектами синхронизации потока (unmanaged thread synchronization objects) наподобие Mutex или Event, то это должен быть желаемый маршалируемый тип для хендлов объектов синхронизации.

Теперь давайте создадим файл, используя функцию CreateFile() с SafeFileHandle для процесса маршалинга. Неуправляемое определение для CreateFile() следующее:

HANDLE CreateFile(LPCTSTR lpFileName,
                  DWORD dwDesiredAccess,
                  DWORD dwShareMode,
                  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
                  DWORD dwCreationDisposition,
                  DWORD dwFlagsAndAttributes,
                  HANDLE hTemplateFile);

Вот дополнительный код .NET (пример защищенного создания файла):

[DllImport("Kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern SafeFileHandle CreateFile(
    string lpFileName,
    uint dwDesiredAccess,
    uint dwShareMode,
    // Поскольку мы переходим к установке аргумента
    // в NULL, то будем маршалировать его как IntPtr,
    // так мы можем использовать IntPtr.Zero для
    // представления значения NULL.
    IntPtr lpSecurityAttributes,
    uint dwCreationDisposition,
    uint dwFlagsAndAttributes,
    // handle для шаблона файла, мы установим
    // его в NULL, так что можно также использовать
    // маршалирование в System.IntPtr и передать
    // IntPtr.Zero для значения NULL.
    // Однако здесь применен другой способ
    SafeFileHandle hTemplateFile);
 
// Доступ к файлу на запись
const uint GENERIC_WRITE = 0x40000000;
// Разрешим совместный доступ к файлу
const uint FILE_SHARE_NONE = 0x0;
// Создадим файл и перезапишем его, если он уже существует
const uint CREATE_ALWAYS = 0x2;
// Обычный файл без установленных атрибутов
const uint FILE_ATTRIBUTE_NORMAL = 0x80;
 
static void Main()
{
   SafeFileHandle handle =
      CreateFile("C:\\MyFile.txt",
      GENERIC_WRITE,
      FILE_SHARE_NONE,
      IntPtr.Zero, // NULL
      CREATE_ALWAYS,
      FILE_ATTRIBUTE_NORMAL,
      new SafeFileHandle(IntPtr.Zero, true));
 
   // Поскольку SafeFileHandle наследует
   // SafeHandleZeroOrMinusOneIsInvalid,
   // то IsInvalid вернет true только если
   // handle равен 0 или -1
   if (handle.IsInvalid) // 0 или -1
   {
      Console.WriteLine("ERROR: {0}", Marshal.GetLastWin32Error());
      return;
      // Marshal.GetLastWin32Error() вернет только последнюю ошибку,
      // если DllImportAttribute.SetLastError установлен в true
   }
 
   FileStream stream = new FileStream(handle, FileAccess.Write);
   StreamWriter writer = new StreamWriter(stream);
   writer.WriteLine("Hello, World!");
   writer.Close();
 
   /*
    * Порядок, в котором методы вызываются из
    * StreamWriter для этого примера:
    *
    * StreamWriter.Close()
    * - StreamWriter.BaseStream.Close()
    * - - FileStream.SafeFileHandle.Close()
    * - - - SafeHandleZeroOrMinusOneIsInvalid
    *              .Close()
    * - - - - SafeHandle.Close()
    * - - - - - SafeHandle.ReleaseHandle()
    */
}

Примечание: хотя Вы можете использовать IntPtr вместо SafeFileHandle, конструктор FileStream, который принимает IntPtr, считается устаревшим (в версии .NET 2.0 и более новой), так что следует использовать конструктор, который принимает SafeFileHandle.

Следующий пример показывает, как создать свой собственный безопасный хендл (custom safe handle). Этот custom safe handle представляет handle, который недопустим только тогда, когд аон равен 0. Хотя Вы можете расширить функциональность либо SafeHandleMinusOneIsInvalid, либо SafeHandleZeroOrMinusOneIsInvalid, мы наследовали SafeHandle напрямую. Код очень прост:

public sealed class SafeHandleZeroIsInvalid : SafeHandle
{
   [DllImport("Kernel32.dll")]
   [return: MarshalAs(UnmanagedType.Bool)]
   private static extern bool CloseHandle(IntPtr hObject);
 
   // Если ownsHandle равен true, то handle будет
   // автоматически освобожден при процессе финализации,
   // иначе в Вашей зоне ответственности будет освободить
   // его в коде вне класса.
   // Автоматическое освобождение означает вызов метода
   // ReleaseHandle().
   public SafeHandleZeroIsInvalid
      (IntPtr preexistingHandle, bool ownsHandle)
      : base(IntPtr.Zero, ownsHandle)
   {
      this.SetHandle(preexistingHandle);
   }
 
   public override bool IsInvalid
   {
      get
      {
         // this.handle.ToInt32() == 0
         // this.handle == new IntPtr(0)
         return this.handle == IntPtr.Zero;
      }
   }
 
   protected override bool ReleaseHandle()
   {
      return CloseHandle(this.handle);
   }
}

До сих пор у меня нет ответа для того, почему handle должен быть недопустимым только если он установлен в 0! Возможно, это Вам понадобится для каких-то своих handle. Однако здесь это показано только для иллюстрации.

[Critical Handle]

Critical handle (переводится как критический хендл) то же самое, что и safe handle, за исключением того, что в нем не реализован подсчет ссылок, и следовательно нет защиты от атак recycling security.

Используйте critical handle вместо safe handle из соображений быстродействия, но Вам понадобится самостоятельно реализовать необходимую синхронизацию для подсчета ссылок.

Critical handle представлен абстрактным System.Runtime.InteropServices.CriticalHandle. На рис. 2.3 показано определение CriticalHandle и его наследников.

Marshalling CriticalHandle and descendants fig2 3

Как показано на диаграмме, CriticalHandle является базовым классом, который представляет critical handle. Он наследуется из System.Runtime.ConstrainedExecution.CriticalFinalizerObject, который гарантирует процесс финализации. Члены класса CriticalHandle те же самые, что и у класса SafeHandle, за исключением того, что нету методов с префиксом Dangerous, потому что critical handle сами по себе опасны, потому что не предоставляют необходимую защиту. Для получения дополнительной информации по поводу членов класса CriticalHandle обратитесь к описанию класса SafeHandle, приведенному ранее.

Поскольку CriticalHandle абстрактный, то Вы должны либо реализовать его, либо использовать один из классов с его реализацией. Есть только 2 класса из нового пространства имен Microsoft.Win32.SafeHandles, где реализован CriticalHandle, оба также абстрактные:

CriticalHandleMinusOneIsInvalid: представляет critical handle, значение которого -1 говорит о том, что хендл недопустим. Таким образом, IsInvalid возвращает true только если handle == -1.

CriticalHandleZeroOrMinusOneIsInvalid: представляет critical handle, у которого значение 0 или -1 показывает, что хендл недопустим. Таким образом, IsInvalid возвращает true только если handle равен 0 или -1.

Пример будет такой же, как и для SafeHandle, нужно просто изменить имя типа.

[Механизм передачи параметра]

Когда аргумент передается функции, то аргумент можно передать либо по значению (pass by value), либо по ссылке (pass by reference). Если функция предназначена для того, чтобы изменить передаваемый аргумент, то аргумент нужно передать по ссылке, иначе нужно использовать передачу аргумента по значению. Это называется механизмом передачи параметра (passing mechanism).

Аргументы, передаваемые по значению (например, аргументы input/In), передают при вызове функции копию переменной аргумента. Таким образом, эта копия переменной ведет себя как отдельная локальная переменная - она существует только в теле функции, и может быть внутри функции изменена, и при этом все изменения никак не повлияют на исходную переменную, которая была передана в функцию. С другой стороны аргументы, передаваемые по ссылке, передаются как оригинал, т. е. все изменения переданной переменной, если эти изменения сделает функция, сохранятся после завершения работы функции. Таким образом, вызывающий код увидит, что случилось с переменной внутри функции.

Аргументы, передаваемые по ссылке, могут быть либо In/Out (Input/Output, работают на ввод и вывод), либо только Out (Output, вывод). Аргументы In/Out используются для передачи входных данных в функцию и одновременного возврата выходных данных функции. Аргументы Out используются только для вывода данных из функции. Таким образом, переменные In/Out должны быть проинициализированы перед передачей в функцию, а аргументы Out не требуют предварительной инициализации.

Когда передается аргумент по значению, не требуется вносить изменения в метод PInvoke. Соответственно передача аргумента по ссылке требует двух дополнительных изменения. Первое состоит в добавлении модификатора ref к аргументу, если он используется как аргумент In/Out, или модификатора out, если он используется как аргумент Out. Второе изменение состоит в декорации Вашего аргумента обоими атрибутами InAttribute и OutAttribute, если он аргумент In/Out, или только атрибутом OutAttribute, если он работает как аргумент Out. Честно говоря, применение этих атрибутов не требуется, модификаторы адекватны во большинстве случаев. Однако накладывание атрибутов дает для CLR информацию о механизме передачи.

Как Вы видели в маршалинге строки, можно маршалировать её как System.String или как System.Text.StringBuilder. По умолчанию StringBuilder передается по ссылке (не нужно применять никаких изменений). С другой стороны, System.String передается по значению.

Следует упомянуть, что Windows API не поддерживает ссылочные аргументы. Вместо этого, если функция требует передачи аргумента по ссылке, то переменная декларируется как указатель на переменную, так что вызывающий код может увидеть произведенные изменения после возврата из функции. Другой код, такой как библиотеки COM, может потребовать либо указатель, либо аргумент по ссылке. В любом случае Вы можете безопасно применить требуемые изменения. Вы также можете, к примеру, маршалировать аргумент указателя как System.IntPtr или как небезопасный void*.

Многие предыдущие примеры показывали только функции, которые требовали передачу аргументов по значению. Некоторые функции требуют передачи одного или большего количества аргументов по ссылке. Хороший пример функции, которая требует аргумента In/Out, это GetVersionEx(), которая возвратит информацию о текущей версии системы. Она требует одного аргумента по ссылке (In/Out), который является структурой OSVERSIONINFOEX (пример использования этой функции будет рассмотрен далее, когда будут обсуждаться составные типы).

Много функций требуют аргумента Out специально для возврата информации о статусе. Хороший пример ReadConsole() и WriteConsole(), которые требуют аргументов по ссылке Out для возврата читаемых/записываемых символов. Вот unmanaged-сигнатура для функции WriteConsole():

BOOL WriteConsole(HANDLE hConsoleOutput,
                  VOID lpBuffer,
                  DWORD nNumberOfCharsToWrite,
                  LPDWORD lpNumberOfCharsWritten,
                  LPVOID lpReserved);

И вот managed-версия вместе с тестовым кодом:

[DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool WriteConsole(
   IntPtr hConsoleOutput,
   String lpBuffer,
   [param: MarshalAs(UnmanagedType.U4)]
   UInt32 nNumberOfCharsToWrite,
   [param: MarshalAs(UnmanagedType.U4)]
   out UInt32 lpNumberOfCharsWritten,
   [param: MarshalAs(UnmanagedType.AsAny)]
   object lpReserved);[DllImport("Kernel32.dll")]
 
static extern IntPtr GetStdHandle(
   [param: MarshalAs(UnmanagedType.U4)]
   Int32 nStdHandle);
 
const int STD_OUTPUT_HANDLE = -11;
 
static void Main()
{
   IntPtr handle = GetStdHandle(STD_OUTPUT_HANDLE);
 
   String textToWrite = "Hello, World!" + Environment.NewLine;
   uint noCharactersWritten;
 
   WriteConsole(handle,
      textToWrite,
      (uint)textToWrite.Length,
      out noCharactersWritten,
      null);
 
   Console.WriteLine("No. Characters written = {0}",
      noCharactersWritten);
}

Далее поговорим о техниках, которые нужно принимать во внимание при работе с unmanaged кодом: это инкапсуляция, создание оберток (wrapper), работа с null-аргументами, решение проблемы CLS.

[Дополнительные техники. Инкапсуляция]

Если функция требует аргумент, который может быть установлен в какое-то значение, или что-то еще, то Вы можете определить эти значения (константами или через typedef) в виде перечисления (enumeration, enum). Это дает упрощение в использования каждого набора по отдельности; такая техника называется инкапсуляцией (группирование). Следующий код покажет MessageBoxEx(), это самая подходящая функция для такого примера:

[DllImport("User32.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.I4)]
static extern UInt32 MessageBoxEx
    (IntPtr hWnd,
    [param: MarshalAs(UnmanagedType.LPTStr)]
    String lpText,
    [param: MarshalAs(UnmanagedType.LPTStr)]
    String lpCaption,
    [param: MarshalAs(UnmanagedType.U4)]
    UInt32 uType,
    [param: MarshalAs(UnmanagedType.U2)]
    UInt16 wLanguageId);
 
public enum MB_BUTTON : uint
{
    MB_OK = 0x0,
    MB_OKCANCEL = 0x1,
    MB_ABORTRETRYIGNORE = 0x2,
    MB_YESNOCANCEL = 0x3,
    MB_YESNO = 0x4,
    MB_RETRYCANCEL = 0x5,
    MB_HELP = 0x4000,
}
 
public enum MB_ICON : uint
{
    MB_ICONHAND = 0x10,
    MB_ICONQUESTION = 0x20,
    MB_ICONEXCLAMATION = 0x30,
    MB_ICONASTERISK = 0x40,
    MB_ICONERROR = MB_ICONHAND,
    MB_ICONSTOP = MB_ICONHAND,
    MB_ICONWARNING = MB_ICONEXCLAMATION,
    MB_ICONINFORMATION = MB_ICONASTERISK,
}
 
public enum MB_DEF_BUTTON : uint
{
    MB_DEFBUTTON1 = 0x0,
    MB_DEFBUTTON2 = 0x100,
    MB_DEFBUTTON3 = 0x200,
    MB_DEFBUTTON4 = 0x300,
}
 
public enum MB_MODAL : uint
{
    MB_APPLMODAL = 0x0,
    MB_SYSTEMMODAL = 0x1000,
    MB_TASKMODAL = 0x2000,
}
 
public enum MB_SPECIAL : uint
{
    MB_SETFOREGROUND = 0x10000,
    MB_DEFAULT_DESKTOP_ONLY = 0x20000,
    MB_SERVICE_NOTIFICATION_NT3X = 0x40000,
    MB_TOPMOST = 0x40000,
    MB_RIGHT = 0x80000,
    MB_RTLREADING = 0x100000,
    MB_SERVICE_NOTIFICATION = 0x200000,
}
 
public enum MB_RETURN : uint
{
    IDOK = 1,
    IDCANCEL = 2,
    IDABORT = 3,
    IDRETRY = 4,
    IDIGNORE = 5,
    IDYES = 6,
    IDNO = 7,
    IDCLOSE = 8,
    IDHELP = 9,
    IDTRYAGAIN = 10,
    IDCONTINUE = 11,
}
 
static void Main()
{
    UInt32 result = MessageBoxEx(IntPtr.Zero, // NULL
        "Do you want to save changes before closing?",
        "MyApplication",
        (UInt32)MB_BUTTON.MB_YESNOCANCEL |
        (UInt32)MB_ICON.MB_ICONQUESTION |
        (UInt32)MB_DEF_BUTTON.MB_DEFBUTTON3 |
        (UInt32)MB_SPECIAL.MB_TOPMOST,
        0);// Reserved
 
    if (result == 0) // error occurred
        Console.WriteLine("ERROR");
    else
    {
        MB_RETURN ret = (MB_RETURN)result;
        if (ret == MB_RETURN.IDYES)
            Console.WriteLine("User clicked Yes!");
        else if (ret == MB_RETURN.IDNO)
            Console.WriteLine("User clicked No!");
        else if (ret == MB_RETURN.IDCANCEL)
            Console.WriteLine("User clicked Cancel!");
    }
}

Вы можете также поменять имена констант на более дружественные. На рис. 2.4 показано окно сообщения (message box) которое появляется при запуске последнего кода.

Marshalling Message Box example fig2 4

Дополнительно Вы можете сделать маршалинг аргумента как enum, который конечно же будет типом аргумента. Это показано в следующем примере:

[DllImport("Kernel32.dll")]
static extern IntPtr GetStdHandle(
    [param: MarshalAs(UnmanagedType.U4)]
    CONSOLE_STD_HANDLE nStdHandle);
 
public enum CONSOLE_STD_HANDLE
{
    STD_INPUT_HANDLE = -10,
    STD_OUTPUT_HANDLE = -11,
    STD_ERROR_HANDLE = -12
}
 
static void Main()
{
    IntPtr handle;
    handle =
        GetStdHandle(CONSOLE_STD_HANDLE.STD_INPUT_HANDLE);
    if (handle == IntPtr.Zero)
        Console.WriteLine("Failed!");
    else
        Console.WriteLine("Succeeded!");
}

[Создание оберток]

Выставление методов PInvoke вне сборки не является хорошей практикой. Всегда рекомендуется группировать методы PInvoke во внутреннем классе, и такой класс должен называться как NativeMethods, SafeNativeMethods или UnsafeNativeMethods. Для получения дополнительной информации по этой теме см. "Code Analyzing Rules" (правила анализа кода) в документации MSDN, читайте статью "Move PInvokes to Native Methods Class" (перемещение PInvokes в класс native-методов).

Следующий код показывает wrapper-метод (метод обертки) для нашей функции MessageBoxEx():

public static MB_RETURN MessageBox
    (IntPtr handle, string text, string title,
    MB_BUTTON buttons, MB_ICON icon, MB_DEF_BUTTON defaultButton,
    MB_MODAL modality, MB_SPECIAL options)
{
    UInt32 result = MessageBoxEx(handle,
        "Do you want to save changes before closing?",
        "MyApplication",
        (UInt32)buttons |
        (UInt32)icon |
        (UInt32)defaultButton |
        (UInt32)modality |
        (UInt32)options,
        0);
 
    if (result == 0)
        // Не рекомендуется выбрасывать System.Exception,
        // вместо этого выбрасывайте порожденное исключение.
        throw new Exception("FAILED");
    return (MB_RETURN)result;
}

Также рекомендуется поменять тип перечисления на любой CLS-совместимый тип наподобие System.Int32.

[Работа с null-аргументами]

Некоторые аргументы функции называются обнуляемыми (nullable). Это означает, что они могут принимать значение NULL (на C# это null). Чтобы передать аргументу значение NULL, Вы можете маршалировать этот аргумент как System.IntPtr, таким способом Вы можете установить его в System.IntPtr.Zero, чтобы предоставить значение NULL. Другой трюк состоит в создании перезагрузки для функции, где в первой параметр маршалируется как тип аргумента, и в другой маршалируется как System.IntPtr. Таким образом, если Вы передадите System.IntPtr.Zero, то CLR направит вызов к функции с System.IntPtr. С другой стороны, если передать значение параметру, то вызов будет направлен к функции с правильным типом аргумента. Следующий код показывает эту технику:

[DllImport("Kernel32.dll", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool ScrollConsoleScreenBuffer(
    IntPtr hConsoleOutput,
    SMALL_RECT lpScrollRectangle,
    SMALL_RECT lpClipRectangle,
    COORD dwDestinationOrigin,
    CHAR_INFO lpFill);
 
[DllImport("Kernel32.dll", CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool ScrollConsoleScreenBuffer(
    IntPtr hConsoleOutput,
    SMALL_RECT lpScrollRectangle,
    IntPtr lpClipRectangle,
    COORD dwDestinationOrigin,
    CHAR_INFO lpFill);
...

[Решение проблемы CLS]

Следует знать, что некоторые типы не являются CLS-совместимыми, и Вы должны избегать их публикацию из сборки. Например, популярный System.UInt32 не является CLS-совместимым, и настоятельно не рекомендуется его показывать снаружи.

Быть не CLS-совместимым означает, что тип нарушает стандарты CLS (Common Language Specifications). Спецификации CLS помогают взаимодействовать языкам, которые поддерживаю .NET. Это помогает избежать некоторых действий наподобие декларирования специальных типов или последующих неудобных соглашений именования (uncommon naming conventions).

Почему нужно избегать таких действий? Это помогает использовать большое достоинство .NET Framework - взаимодействие языков .NET-библиотеки. К примеру, некоторые языки не поддерживают имена переменных, начинающихся на подчеркивание (_), а другие поддерживают. Таким образом, следование стандартам CLS позволит Вашей сборке проще быть вызванной из другого кода, построенного на другом языке.

Чтобы принудительно проверять спецификацию CLS, Вы можете декорировать сборку с атрибутом System.CLSCompliantAttribute -specifying true, и в результате компилятор будет выдавать предупреждения всякий раз, когда Вы попытаетесь выставить наружу тип, не совместимый с CLS.

Чтобы решить проблему CLS для функций, требующих в качестве аргумента UInt32, Вы можете создать обертку (wrapper), которая ведет себя как точка входа в частный, не совместимый с CLS метод (private non-CLS-compliant method). Метод обертки, к примеру, будет принимать System.Int32, и преобразовывать его внутри себя в System.UInt32.

Для структур Вы можете декларировать структуру как внутреннюю, и продолжить её использовать как обычно.

И снова, Вы можете заменить все не CLS-совместимые типы наподобие System.UInt32 на CLS-совместимые эквиваленты наподобие System.Int32, и получить выигрыш в простом распространении Ваших типов и сборки. Но в некоторых случаях это может быть непросто осуществить.

Очень полезно проконсультироваться с документацией по атрибуту System.CLSCompliantAttribute.

Далее давайте рассмотрим некоторые реальные примеры, которые решают проблемы, возникающие при разработке приложения. Эти проблемы можно решить, взаимодействуя с unmanaged-кодом.

[Примеры из реального мира]

Как поменять кнопки мыши. Следующий код программно переключает назначение кнопок мыши. В результате левая кнопка работает как правая, а правая как левая (т. е. правая делает выбор, а левая открывает контекстное меню).

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SwapMouseButton
    ([param: MarshalAs(UnmanagedType.Bool)] bool fSwap);
 
public void MakeRightButtonPrimary()
{
    SwapMouseButton(true);
}
 
public void MakeLeftButtonPrimary()
{
    SwapMouseButton(false);
}

Как запустить скринсейвер. Следующий пример программно запускает хранитель экрана.

[DllImport("User32.dll")]
public static extern int SendMessage
    (IntPtr hWnd,
    uint Msg,
    uint wParam,
    uint lParam);
 
public const uint WM_SYSCOMMAND = 0x112;
public const uint SC_SCREENSAVE = 0xF140;
 
public enum SpecialHandles
{
    HWND_DESKTOP = 0x0,
    HWND_BROADCAST = 0xFFFF
}
 
public static void TurnOnScreenSaver()
{
    SendMessage(
        new IntPtr((int)SpecialHandles.HWND_BROADCAST),
        WM_SYSCOMMAND,
        SC_SCREENSAVE,
        0);
}

Перетаскивание формы без плашки (Title Bar). В следующем коде показано, как перемещать окно формы за любое место. Это хороший пример техники обертки, которую мы уже обсуждали.

SafeNativeMethods.cs

internal static class SafeNativeMethods
{
    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.I4)]
    public static extern int SendMessage(
        IntPtr hWnd,
        [param: MarshalAs(UnmanagedType.U4)]
        uint Msg,
        [param: MarshalAs(UnmanagedType.U4)]
        uint wParam,
        [param: MarshalAs(UnmanagedType.I4)]
        int lParam);
 
    [DllImport("user32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool ReleaseCapture();
 
    public const uint WM_NCLBUTTONDOWN = 0xA1; // 161
    public const uint HTCAPTION = 2;
}

HelperMethods.cs

internal static class HelperMethods
{
    public static void MoveObject(IntPtr hWnd)
    {
        SafeNativeMethods.ReleaseCapture();
        SafeNativeMethods.SendMessage
            (hWnd, SafeNativeMethods.WM_NCLBUTTONDOWN,
            SafeNativeMethods.HTCAPTION, 0);
    }
}

MainForm.cs

// В форме напишите следующий код для обработчика
//  события MouseDown
private void MainForm_MouseDown(object sender, MouseEventArgs e)
{
    HelperMethods.MoveObject(this.Handle);
}

В качестве заключения стоит сказать, что не всегда требуется использовать MarshalAsAttribute. Иногда это необязательно, а иногда необходимо.

Например, если Вы маршалируете blittable-типы данных наподобие DWORD, то можете безопасно игнорировать MarshalAsAttribute. С другой стороны, если Вы маршалируете non-blittable типы данных наподобие boolean и строк, но Вам нужно использовать MarshalAsAttribute, чтобы гарантировать корректность процесса маршалирования. Однако всегда будет лучше дать для CLR и для других разработчиков нотацию по нижележащему типу данных путем наложения артибута MarshalAsAttribute также и на blittable типы данных.

В завершение следует заметить, что эта часть была ключевой основой к понимаю взаимодействия кода с unmanaged-окружениями. Здесь обсуждались самые важные части процесса маршалинга, маршалинг простых типов, которые всегда нужно иметь в виду при разработке.

В следующей части рассмотрим составные типы, как их маршалировать в managed-окружении.

[Словарик]

В этом разделе кратко перечислены некоторые термины, которые встречаются в статье.

blittable этот термин в контексте библиотеки .NET обозначает особые типы, которые для представления переменных в памяти используют одинаковый принцип выделения памяти в managed и unmanaged окружениях.

CLR Common Language Runtime, специальная защищенная среда выполнения библиотеки .NET, одинаковая для всех поддерживающих .NET языков.

GDI Graphics Device Interface, имеется в виду программирование графического интерфейса пользователя программ.

managed в данном контексте это так называемая управляемая среда выполнения кода.

marshalling стандартная процедура обмена данными между managed и unmanaged средами выполнения кода.

native традиционный код, имеется в виду код на C/C++ и интерфейс программирования Windows API.

.NET Framework среда разработки и выполнения, основанная на библиотеке .NET компании Microsoft.

non-blittable термин, противоположный по значению термину blittable. Т. е. это типы, которые в managed окружении размещаются в памяти не так, как в unmanaged окружении, и для их маршаллинга требуется специальная обработка.

unmanaged неуправляемый код, противоположный по значению термин для managed.

[Ссылки]

1. Marshaling with C# site:codeproject.com.
2. Blittable types site:wikipedia.org.
3Маршалинг в C#. Составные типы.
4. Visual C#: небезопасный код.

 

Добавить комментарий


Защитный код
Обновить

Top of Page