Маршалинг данных между управляемым и неуправляемым кодом

ОГЛАВЛЕНИЕ

Посмотрим правде в глаза: нет в мире совершенства. Мало кто при разработке использует только управляемый код. А между тем, тяжким грузом лежат устаревшие неуправляемые приложения, с которыми приходится мириться. Есть ли способ интегрировать проекты, в которых задействован как управляемый, так и неуправляемый код? Какой вид принимает этот способ: вызов неуправляемого кода из управляемого приложения или вызов управляемого кода из неуправляемого приложения?

К счастью, взаимодействие в платформе Microsoft® .NET позволяет открыть канал между управляемым и неуправляемым кодом, и немаловажную роль в этом подключении играет маршалинг: он обеспечивает обмен данными между управляемым и неуправляемым кодом (см. рис. 1). Выполнение маршалинга данных между управляемым и неуправляемым кодом средой CLR определяется многими факторами, в частности атрибутами [MarshalAs], [StructLayout], [InAttribute], [OutAttribute], равно как и от некоторыми ключевыми словами языка, например out и ref в C#.


Рис. 1 Заполнение разрыва между машинным и управляемым кодом

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


Атрибуты [InAttribute] и [OutAttribute]

Для начала рассмотрим порядок использования атрибутов InAttribute и OutAttribute — это типы атрибутов, существующие в пространстве имен System.Runtime.InteropServices. (В коде на языке C# или Visual Basic® эти атрибуты можно писать в сокращенной форме — [In] и [Out] — однако в статье мы будем придерживаться полных форм, дабы избежать путаницы.)

Если указанные атрибуты относятся к параметрам метода или возвращаемым значениям, они определяют направление маршалинга, поэтому их называют атрибутами направления. При наличии атрибута [InAttribute] среда CLR выполняет маршалинг данных от вызывающей стороны к вызываемой в начале вызова, а при наличии атрибута [OutAttribute] — обратный маршалинг от вызываемой стороны к вызывающей при возврате. И вызывающая, и вызываемая сторона может быть представлена как управляемым, так и неуправляемым кодом. Например, при вызове P/Invoke управляемый код осуществляет вызов неуправляемого кода, а при обратном вызове P/Invoke неуправляемый код может осуществлять вызов управляемого кода через указатель функции.

При использовании атрибутов [InAttribute] и [OutAttribute] возможны четыре комбинации: только [InAttribute], только [OutAttribute], оба атрибута [InAttribute, OutAttribute] или ни одного. Если ни один из атрибутов не указан, среда CLR самостоятельно определяет их значения. Обычно по умолчанию используется атрибут [InAttribute], но бывают и исключения. Например, к классу StringBuilder применяются оба атрибута, если они не указаны явно. (Далее мы рассмотрим класс StringBuilder более подробно.) Ключевые слова out и ref в языке C# также могут менять порядок применения атрибутов (см. рис. 2). Обратите внимание на то, что если для параметра не указано ключевого слова, то по умолчанию он считается входным параметром.

Figure 2 Out and Ref and Their Associated Attributes

Ключевое слово C# Атрибут
(нет) [InAttribute]
out [OutAttribute]
ref [InAttribute], [OutAttribute]

Рассмотрим код, приведенный на рис. 3. В нем использованы три функции на C++ в машинном коде, одинаковым образом изменяющие элемент arg. Отметим, что при работе со строками strcpy используется только в иллюстративных целях — в рабочем коде должны быть безопасные версии этих функций, о которых рассказывалось в статье msdn.microsoft.com/msdnmag/issues/05/05/SafeCandC.

Figure 3 Trying Out Directional Attributes
MARSHALLIB_API void __stdcall Func_In_Attribute(char *arg) 
{
printf("Inside Func_In_Attribute: arg = %s\n", arg);
strcpy(arg, "New");
}
MARSHALLIB_API void __stdcall Func_Out_Attribute(char *arg)
{
printf("Inside Func_Out_Attribute: arg = %s\n", arg);
strcpy(arg, "New");
}
MARSHALLIB_API void __stdcall Func_InOut_Attribute(char *arg)
{
printf("Inside Func_InOut_Attribute: arg = %s\n", arg);
strcpy(arg, "New");
}

Единственное различие состоит в том, как эти функции вызываются, то есть в применении атрибутов направления в сигнатурах P/Invoke:

[DllImport(@"MarshalLib.dll")]
public static extern void Func_In_Attribute([In]char[] arg);
[DllImport(@"MarshalLib.dll")]
public static extern void Func_Out_Attribute([Out]char[] arg);
[DllImport(@"MarshalLib.dll")]
public static extern void Func_InOut_Attribute([In, Out]char[] arg);

Если вызвать перечисленные функции из управляемого кода методом P/Invoke и передать им Old в качестве массива символов, мы получим на выходе следующее (в примере приведен фрагмент выходных данных):

Before Func_In_Attribute: arg = Old
Inside Func_In_Attribute: arg = Old
After Func_In_Attribute: arg = Old

Before Func_Out_Attribute: arg = Old
Inside Func_Out_Attribute: arg =
After Func_Out_Attribute: arg = New

Before Func_InOut_Attribute: arg = Old
Inside Func_InOut_Attribute: arg = Old
After Func_InOut_Attribute: arg = New

Остановимся чуть подробнее на полученных результатах. Функции Func_In_Attribute передается исходное значение, но изменение, вносимое в ходе работы функции, обратно не передается. Функции Func_Out_Attribute исходное значение не передается, а измененное передается обратно. Функции Func_InOut_Attribute передается исходное значение, а значение, измененное в ходе ее работы, передается обратно. Однако даже незначительное изменение функций может полностью изменить результаты. Для начала изменим собственные функции так, чтобы использовался юникод:

MARSHALLIB_API void __stdcall Func_Out_Attribute_Unicode(wchar_t *arg)
{
wprintf(L"Inside Func_Out_Attribute_Unicode: arg = %s\n", arg);
printf("Inside Func_Out_Attribute_Unicode: strcpy(arg, \"New\")\n");
wcscpy(arg, L"New");
}

Мы объявляем функцию C#, применяем только атрибут [OutAttribute] и устанавливаем для CharSet значение CharSet.Unicode.

[DllImport(@"MarshalLib.dll", CharSet=CharSet.Unicode)]
public static extern void Func_Out_Attribute_Unicode([Out]char[] arg);

Результат выглядит так:

Before Func_Out_Attribute_Unicode: arg = Old
Inside Func_Out_Attribute_Unicode: arg = Old
After Func_Out_Attribute_Unicode: arg = New

Обратите внимание: исходное значение передается, несмотря на то, что атрибут [InAttribute] указан не был. Атрибут [DllImportAttribute] заставляет среду CLR выполнять маршалинг юникода. Поскольку для символов в CLR также используется юникод, CLR считает возможным оптимизировать процесс маршалинга путем фиксации массива символов и передачи непосредственно адресов символов. (Копирование и фиксация будут рассмотрены дальше.) На такое поведение среды полагаться не стоит. При маршалинге нужно всегда указывать атрибуты направления, если не используется стандартное поведение среды CLR. Наглядным примером стандартного поведения служит аргумент int: указывать [InAttribute] int arg не обязательно.

Существуют ситуации, в которых атрибут [OutAttribute] игнорируется. Например, выражение [OutAttribute]int не имеет смысла, поэтому среда CLR попросту пропускает атрибут [OutAttribute]. То же самое верно и для выражения [OutAttribute] string, поскольку объект string является неизменяемым.

В файлах определений интерфейса (IDL) существуют атрибуты [in] и [out] — их можно считать аналогами атрибутов [InAttribute] и [OutAttribute] в среде CLR.



Ключевые слова out и ref. Передача по ссылке

Выше отмечалось, что ключевые слова out и ref в языке C# можно напрямую сопоставить атрибутам [InAttribute] и [OutAttribute]. Ключевые слова out и ref также могут определять, в какой тип данных (или из какого типа данных) будет осуществляться маршалинг в среде CLR. Передача данных с ключевыми словами out или ref — это то же самое, что и передача по ссылке. Если вы обратите внимание на соответствующую подпись функции в промежуточном языке (IL) в ILDASM, вы обнаружите рядом с типом знак амперсанда (&) — это означает, что аргумент должен передаваться по ссылке. При передаче данных по ссылке среда CLR добавляет еще один уровень косвенности. На рис. 4 приведено несколько примеров.

Figure 4 Marshaling Results
Сигнатура C# Неуправляемая сигнатура Сигнатура MSIL Сигнатура MSIL, воспринимаемая средой CLR
Базовые типы     
int arg int arg int [in] int
out int arg int *arg [out] int & [out] int &
ref int arg int *arg int & [in, out] int &
Структуры      
MyStruct arg MyStruct arg MyStruct [in] MyStruct
out MyStruct arg MyStruct *arg [out] MyStruct & [out] MyStruct &
ref MyStruct arg MyStruct *arg MyStruct & [in, out] MyStruct &
Строки      
string arg char *arg string [in] string
out string arg char **arg [out] string & [out] string &
ref string arg char **arg string & [in, out] string &
Классы      
MyClass arg MyClass *arg MyClass [in] MyClass
out MyClass arg MyClass **arg [out] MyClass & [out] MyClass &
ref MyClass arg MyClass **arg MyClass & [in, out] Myclass &

Все сказанное о ключевых словах out и ref мы объединили в таблицу, приведенную на рис. 5.

Figure 5 Default Attributes
Сигнатура C# Сигнатура MSIL Атрибуты направления, используемые по умолчанию
<type> type [InAttribute]
out <type> [OutAttribute] type & [OutAttribute]
ref <type> type & [InAttribute, OutAttribute]

Следует отметить, что если при передаче по ссылке не указать атрибуты направления, среда CLR будет использовать атрибуты [InAttribute] и [OutAttribute] автоматически, и именно поэтому в сигнатуре на рис. 4 стоит только «string &». Если указать хотя бы один из атрибутов, среда CLR будет использовать указанные значения (как показано в примере ниже), и стандартного поведения мы не увидим.

public static extern void 
PassPointerToComplexStructure(
[In]ref ComplexStructure
pStructure);

Приведенная выше сигнатура преимущество перед стандартным поведением при наличии ключевого слова ref, поэтому используется только атрибут [InAttribute]. В данном примере при выполнении вызова P/Invoke указатель на ComplexStructure (это тип значений) передается из среды CLR в машинный код, но вызываемая сторона не может передать изменения обратно в тип ComplexStructure, к которому отсылает указатель pStructure. На рис. 6 приведены другие варианты сочетания атрибутов направления и ключевых слов.

Figure 6 More Attributes and Keywords
Сигнатура C# Неуправляемая сигнатура IDL Сигнатура MSIL
Out    
[InAttribute] out int arg Ошибка компиляции CS0036. Выходной параметр не может иметь атрибут In.
[OutAttribute] out int arg [out] int *arg [out] int &
[InAttribute, OutAttribute] out int arg Ошибка компиляции CS0036. Выходной параметр не может иметь атрибут In.
Ref    
[InAttribute] ref int arg [in] int *arg [in] int &
[OutAttribute] ref int arg Ошибка компиляции CS0662. Невозможно указать для параметра ref только атрибут Out. Используйте оба атрибута (In и Out) или ни одного из них.
[InAttribute, OutAttribute] ref int arg [in, out] int *arg [in] [out] int &
 


Возвращаемые значения

До сих пор мы говорили только об аргументах. А что происходит со значениями функций? Среда CLR автоматически воспринимает возвращаемые значения как обычные аргументы с атрибутом [OutAttribute]. Кроме того, среда CLR может трансформировать сигнатуру функции — за этот процесс отвечает атрибут PreserveSigAttribute. Если атрибут [PreserveSigAttribute], относящийся к сигнатуре P/Invoke, имеет значение «ложь», среда CLR сопоставляет возвращаемые значения HRESULT управляемым исключениям, а параметры [out, retval] — значениям, возвращаемым функцией. Рассмотрим в качестве примера следующую сигнатуру управляемой функции:

public static string extern GetString(int id);

После преобразования она превращается в неуправляемую сигнатуру:

HRESULT GetString([in]int id, [out, retval] char **pszString);

Если атрибут [PreserveSigAttribute] имеет значение «истина» (это значение используется по умолчанию при вызовах P/Invoke), преобразование не выполняется. Следует отметить, что для функций COM атрибут [PreserveSigAttribute] обычно по умолчанию имеет значение «ложь», однако есть несколько способов его изменить. Подробные сведения можно найти в документации MSDN® к программам TlbExp.exe и TlbImp.exe.



Маршалинг и класс StringBuilder

В компоненте маршалинга в среде CLR имеются встроенные сведения о существовании типа StringBuilder, потому он обрабатывается не так, как все остальные типы. По умолчанию класс StringBuilder передается с обоими атрибутами [InAttribute, OutAttribute]. Класс StringBuilder обрабатывается по-другому, потому что у него есть свойство Capacity, которое определяет необходимый размер буфера и может динамически изменяться. По этой причине во время выполнения маршалинга среда CLR может зафиксировать класс StringBuilder, передать напрямую адрес внутреннего буфера, используемый в классе StringBuilder, и позволить машинному коду изменять содержимое буфера.

Чтобы использовать все возможности класса StringBuilder в полной мере, нужно соблюдать следующие правила:

  1. Не передавайте класс StringBuilder по ссылке (с использованием ключевых слов out и ref). В противном случае среда CLR будет считать, что аргумент имеет сигнатуру wchar_t **, а не wchar_t *, и не сможет зафиксировать внутренний буфер класса StringBuilder. В результате значительно снизится производительность.
  2. Применяйте класс StringBuilder, если в неуправляемом коде используется Юникод. В противном случае среде CLR придется создавать копию строки и перекодировать ее из Юникода в ANSI и обратно, что также снизит производительность. Обычно при маршалинге класс StringBuilder представляется в виде массива LPARRAY, состоящего из символов Юникода, или в виде строки LPWSTR.
  3. Всегда заранее указывайте емкость StringBuilder и проверяйте, хватает ли ее для буфера. На стороне неуправляемого кода лучше всего принять в качестве аргумента размер буфера строки — так вы избежите переполнения буфера. В модели COM указать размер можно также при помощи size_is в IDL.


Копирование и фиксация

При выполнении маршалинга в среде CLR есть два пути развития событий: копирование и фиксация (см. статью msdn2.microsoft.com/23acw07k).

По умолчанию среда CLR создает копию, которая используется во время выполнения маршалинга. Например, если управляемый код передает строку ANSI C в неуправляемый код, среда CLR создает копию строки, преобразует ее в ANSI и передает временный указатель в неуправляемый код. Такое копирование может занимать довольно долгое время и негативно сказываться на производительности.

В некоторых случаях среда CLR может оптимизировать процесс маршалинга: она напрямую фиксирует управляемый объект в куче сборщика мусора, чтобы он (объект) не был перемещен во время выполнения вызова. Указатель на управляемый объект (или какой-то его фрагмент) передается непосредственно в неуправляемый код.

Фиксация проводится только при выполнении всех перечисленных ниже условий. Во-первых, вызов должен осуществляться из управляемого кода в машинный код, а не наоборот. Во-вторых, тип, участвующий в маршалинге, должен быть преобразуемым (или допускать преобразование при определенных условиях). В-третьих, передача не должна осуществляться по ссылке (то есть с использованием ключевых слов out и ref), и в-четвертых, вызывающая и вызываемая сторона должны находиться в одном контексте потока или в одном контейнере.

Второе правило следует оговорить особо. Преобразуемый тип — это тип, для которого есть представитель и в управляемом, и в неуправляемом коде. Преобразуемые типы никакого дополнительного преобразования во время маршалинга не требуют. Типичным примером непреобразуемого типа, допускающего преобразование в определенных условиях, является символьный тип. Изначально он не является преобразуемым, поскольку не может быть сопоставлен ни Юникоду, ни ANSI, однако, поскольку в среде CLR для символьного типа всегда используется Юникод, он становится преобразуемым, если указать [DllImportAttribute(CharSet= Unicode)] или [MarshalAsAttribute(UnmanagedType.LPWSTR)]. В следующем примере объект arg в функции PassUnicodeString может быть зафиксирован, а в функции PassAnsiString — не может.

[DllImport(@"MarshalLib.dll", CharSet = CharSet.Unicode)]
public static extern string PassUnicodeString(string arg);

[DllImport(@"MarshalLib.dll", CharSet = CharSet.Ansi)]
public static extern string PassAnsiString(string arg);


Владение памятью

Во время вызова функция может изменять аргументы двумя способами: по ссылке или на месте. Преобразование по ссылке — это преобразование объекта, на который ссылается указатель. Если он указывает на фрагмент выделенной памяти, она должна быть высвобождена до того, как будет удален указатель на нее. Преобразование на месте — это изменение фрагмента памяти, на которую указывает ссылка.

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

Figure 7 CLR Type Knowledge
Подпись IDL Тип преобразования
[In] Type Преобразование невозможно
[In] Type * Преобразование невозможно
[Out] Type * Преобразование на месте
[In, Out] Type * Преобразование на месте
[In] Type ** Преобразование невозможно
[Out] Type ** Преобразование по ссылке
[In, Out] Type ** Преобразование по ссылке или на месте

Как говорилось выше, два уровня косвенности при передаче по ссылке имеют только ссылочные типы (существуют, правда, исключения, например [MarshalAs(UnmanagedType.LPStruct)]ref Guid), поэтому изменяться может только указатель или ссылка на ссылочный тип (см. рис. 8).

Figure 8 Type Change Rules
Сигнатура C# Тип преобразования
int arg Преобразование невозможно
out int arg Преобразование на месте
ref int arg Преобразование на месте
string arg Преобразование невозможно
out string arg Преобразование по ссылке
ref string arg Преобразование по ссылке или на месте
[InAttribute, OutAttribute] StringBuilder arg Преобразование на месте
[OutAttribute] StringBuilder arg Преобразование на месте

При преобразовании на месте беспокоиться о владении памятью не приходится, поскольку вызывающая сторона выделяет память для вызываемой, и она же является владельцем памяти. Рассмотрим в качестве примера [OutAttribute] StringBuilder. Соответствующим собственным типом является тип char * (при условии что используется ANSI), поскольку передача выполняется не по ссылке. Выполняется маршалинг данных наружу, но не внутрь. Память выделяется вызывающей стороной (в данном случае средой CLR). Размер памяти определяется емкостью объекта StringBuilder. Вызываемая сторона не участвует в выделении памяти.

Чтобы изменить строку, вызываемая сторона вносит изменения непосредственно в память. Однако при преобразовании по ссылке важно знать, кто является владельцем памяти. В противном случае вы можете получить весьма неожиданные результаты. В вопросах владения памятью среда CLR следует правилам, принятым в модели COM:

  • Владельцем памяти, передаваемой с атрибутом [in], является вызывающая сторона. Вызывающая сторона выделяет и высвобождает память. Вызываемая сторона не должна пытаться ее высвободить или каким-то образом изменить.
  • Владельцем памяти, выделяемой вызываемой стороной и передаваемой с атрибутом [out] (или возвращаемой), является вызывающая сторона. Вызывающая сторона высвобождает память.
  • Вызываемая сторона может высвободить память, переданную от вызывающей стороны с обоими атрибутами [in, out], выделить новую память и заменить старое значение указателя, тем самым передав его обратно. Владельцем новой выделенной памяти является вызывающая сторона. Для проведения этих операций необходимо иметь два уровня косвенности (как в случае с char **, например).

С точки зрения взаимодействия систем, вызывающей стороной является среда CLR, а вызываемой — машинный код. Из перечисленных правил следует, что если фиксация не используется, то при передаче от среды CLR указателя на блок памяти с атрибутом [out] вы должны эту память высвободить. С другой стороны, если среда CLR получает указатель с атрибутом [out] от машинного кода, то среда CLR должна высвободить память. Это означает, что в первом случае освобождение производит машинный код, а во втором — управляемый код.

Когда речь заходит о выделении и освобождении памяти, главную сложность представляет выбор функции. Функций много: HeapAlloc/HeapFree, malloc/free, new/delete и т. д. Однако, поскольку среда CLR использует функции CoTaskMemAlloc/CoTaskMemFree (при отсутствии BSTR) или функции SysStringAlloc/SysStringAllocByteLen/SysStringFree (при наличии BSTR), применять приходится именно их. В противном случае в некоторых версиях Windows® вы рискуете столкнуться с утечкой или сбоями памяти. Были случаи, когда выделение памяти функцией malloc и передача ее в среду CLR вызывала сбой программы в Windows Vista®, хотя в Windows XP программ работала.

Кроме названных функций, нормально работает реализованный в системе интерфейс IMalloc, возвращенный от CoGetMalloc, поскольку он использует ту же кучу, что и функции CoTaskMemAlloc/CoTaskMemFree, SysStringAlloc/SysStringAllocByteLen/SysStringFree, но все же лучше остановить свой выбор именно на них: CoGetMalloc может измениться в будущем.

Рассмотрим пример. Функция GetAnsiStringFromNativeCode принимает аргумент типа char ** с атрибутами [in, out] и возвращает значение типа char * с атрибутами [out, retval]. Что касается аргумента char **, он может вызвать функцию CoTaskMemFree, чтобы освободить память, выделенную средой CLR, затем выделить новый блок памяти при помощи функции CoTaskMemAlloc и заменить старый указатель памяти новым. Затем среда CLR освободит память и создаст копию управляемой строки. Что же касается возвращаемого значения, то ему нужно только выделить новый блок памяти при помощи функции CoTaskMemAlloc и вернуть его вызывающей стороне. После этого владельцем нового выделенного блока становится среда CLR. Она создает на основе выделенного блока новую управляемую строку и вызывает функцию CoTaskMemFree, чтобы его освободить.

Рассмотрим первый вариант (см. рис. 9). Соответствующая функция в C# объявляется следующим образом:

Figure 9 Using Pointers
MARSHALLIB_API char *__stdcall GetAnsiStringFromNativeCode(char **arg)
{
char *szRet = (char *)::CoTaskMemAlloc(255);
strcpy(szRet, "Returned String From Native Code");

printf("Inside GetAnsiStringFromNativeCode: *arg = %s\n", *arg);
printf("Inside GetAnsiStringFromNativeCode: CoTaskMemFree(*arg);
*arg = CoTaskMemAlloc(100); strcpy(*arg, \"Changed\")\n");

::CoTaskMemFree(*arg);
*arg = (char *)::CoTaskMemAlloc(100);
strcpy(*arg, "Changed");

return szRet;
}
class Lib
{
[DllImport(@"MarshalLib.dll", CharSet= CharSet.Ansi)]
public static extern string GetAnsiStringFromNativeCode(
ref string inOutString);
}

Следующий код на C# вызывает функцию GetAnsiStringFromNativeCode:

string argStr = "Before";
Console.WriteLine("Before GetAnsiStringFromNativeCode : argStr = \"" +
argStr + "\"");
string retStr = Lib.GetAnsiStringFromNativeCode(ref argStr);
Console.WriteLine("AnsiStringFromNativeCode() returns \"" + retStr +
"\"" );
Console.WriteLine("After GetAnsiStringFromNativeCode : argStr = \"" +
argStr + "\"");

Результат выглядит так:

Before GetAnsiStringFromNativeCode : argStr = "Before"
Inside GetAnsiStringFromNativeCode: *arg = Before
Inside GetAnsiStringFromNativeCode: CoTaskMemFree(*arg); *arg = CoTaskMemAlloc(100); strcpy(*arg, "Changed")
AnsiStringFromNativeCode() returns "Returned String From Native Code"
After GetAnsiStringFromNativeCode : argStr = "Changed"

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

Еще одним важным фактором (помимо выделения) является размер передаваемой памяти. Обсуждая пример с классом StringBuilder, мы уже упомянули, что нужно правильно изменить свойство Capacity, чтобы среда CLR выделила блок памяти, способный вместить результаты. Кроме того, маршалинг строки с атрибутами [InAttribute, OutAttribute] (без ключевых слов out и ref или каких бы то ни было других атрибутов) — выбор неудачный, поскольку невозможно заранее знать, достаточно ли большой окажется строка. Чтобы указать размер буфера, можно задействовать поля SizeParamIndex и SizeConst в атрибуте MarshalAsAttribute, однако при передаче по ссылке их использовать не стоит.



Обратный вызов P/Invoke и время существования делегата

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


Рис. 10 Использование преобразователя

Обычно о времени существования делегата задумываться не приходится. При передаче делегата в неуправляемый код среда CLR гарантирует его существование на протяжении всего вызова.

Сложности возникают тогда, когда машинный код сохраняет копию указателя даже после завершения вызова и пытается впоследствии осуществить обратный вызов по этому указателю: в таком случае иногда приходится использовать GCHandle явным образом, чтобы сборщик мусора не подобрал делегат. Нужно иметь в виду, что при фиксации GCHandle может значительно снизиться производительность программы. К счастью, в нашем случае выделять фиксированный дескриптор GC не приходится, поскольку преобразователь адресов выделяется в неуправляемой куче и ссылается на делегат через ссылку, известную GC, поэтому преобразователь адресов перемещаться не может, и машинный код в любой момент способен вызвать делегат через неуправляемый указатель (если сам делегат еще существует).

Функция Marshal.GetFunctionPointerForDelegate преобразовывает делегат в указатель функции, но она никаким образом не гарантирует существование делегата в течение нужного срока. Обратите внимание на следующее объявление функции.

public delegate void PrintInteger(int n);

[DllImport(@"MarshalLib.dll", EntryPoint="CallDelegate")]
public static extern void CallDelegateDirectly(
IntPtr printIntegerProc);

Если вызвать Marshal.GetFunctionPointerForDelegate для этой функции и сохранить возвращенное значение IntPtr, то затем вы передадите его в функцию, которую вы собираетесь вызывать, следующим образом.

IntPtr printIntegerCallback = Marshal.GetFunctionPointerForDelegate(
new Lib.PrintInteger(MyPrintInteger));

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

CallDelegateDirectly(printIntegerCallback);

Существует возможность того, что делегат будет удален сборщиком мусора прежде, чем вы вызовете функцию CallDelegateDirectly. Тогда вы получите сообщение MDA о том, что обнаружено событие CallbackOnCollectedDelegate. Чтобы исправить эту ошибку, нужно либо сохранить в памяти ссылку на делегат, либо выделить дескриптор GC.

Если машинный код возвращает среде CLR неуправляемый указатель функции, то за сохранение кода функции отвечает машинный код. Здесь обычно проблем не возникает — если, конечно, код не находится в динамически загружаемой библиотеке DLL и не создается оперативно.



Помощник по взаимодействию P/Invoke

Необходимость знать и помнить все перечисленные атрибуты и правила может вас несколько огорчить. В конце концов, для большинства разработчиков управляемого кода достаточно уметь быстро определить сигнатуру P/Invoke для той или иной функции Win32® API и вставить ее в код. Тут на помощь приходит помощник по взаимодействию P/Invoke (его можно загрузить на веб-узле журнала MSDN Magazine). Это средство весьма эффективно при преобразовании C++ в управляемые сигнатуры P/Invoke и обратно. В нем есть база данных функций, типов и переменных Win32, так что банальные операции добавления подписей Win32 P/Invoke в код на C# или Visual Basic выполняются безо всяких проблем. К помощнику прилагаются две программы командной строки: SigImp и SigExp. Их можно использовать для пакетной обработки файлов. Кроме этого, существует средство с графическим интерфейсом, объединяющее функции обеих программ.

Графический интерфейс удобен при выполнении простейших преобразований. В нем три вкладки: SigExp, SigImp Search и SigImp Translate Snippet.

SigExp позволяет преобразовать управляемую сигнатуру в неуправляемую. Программа считывает все управляемые сборки и находит все объявления P/Invoke и все импортированные типы COM. На основании полученных данных программа создает соответствующие сигнатуры для машинного кода на C (см. рис. 11).


Рис. 11 Служебная программа P/Invoke Interop Assistant — SigExp

Вкладки SigImp Search и SigImp Translate Snippet позволяют преобразовать неуправляемые сигнатуры в управляемые. Они создают управляемые сигнатуры и определения либо на C#, либо на Visual Basic на основании машинных типов, функций, переменных и фрагментов кода сигнатур функций в машинном коде, введенных вручную.

Вкладка SigImp Search позволяет пользователю выбрать язык, на котором будет создан управляемый код, и указать машинный тип, процедуру или переменную, на основании которой будет выполняться преобразование. Средство выдает список поддерживаемых типов, методов и переменных, собранных из заголовков Windows SDK (см. рис. 12).


Рис. 12 Служебная программа P/Invoke Interop Assistant — SigImp Search

Вкладка SigImp Translate Snippet позволяет пользователю написать собственный фрагмент машинного кода. После этого программа создает и отображает в основном окне эквивалентный управляемый код, как показано на рис. 13.


Рис. 13 Служебная программа P/Invoke Interop Assistant — SigImp Translate Snippet

Подробные сведения о средстве с графическим интерфейсом и о программах командной строки помощника по взаимодействию P/Invoke вы найдете в документации, прилагаемой к средствам.



Переходите к практике

Как известно, маршалинг — тема сложная. Существует множество способов изменить процедуру маршалинга и приспособить его под конкретные нужды. Мы рекомендуем вам на практике проверить методы, приведенные в этой статье. Мы уверены, что они помогут вам не заплутать в дебрях маршалинга.

И Чжан (Yi Zhang) — инженер по разработке программного обеспечения в шанхайской группе Silverlight, входящей в состав подразделения Server and Tool Business, расположенного в Китае. В настоящее время он занимается вопросами взаимодействия со средой CLR.

Сяоин Го (Xiaoying Guo) — руководитель программ в шанхайской группе Silverlight, входящей в состав подразделения Server and Tools Business, расположенного в Китае. Сейчас Сяоин работает в области обеспечения взаимодействия управляемого и неуправляемого кода в среде CLR, а также занимается CoreCLR для «Макинтош». Кроме этого, на нее возложена часть обязанностей по налаживанию связей с клиентами в группе Silverlight.