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

ОГЛАВЛЕНИЕ


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

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

Выбор способа преобразования зависит от типа аргументов и, что более важно, от контракта между вызывающей и вызываемой стороной. Впрочем, поскольку среда 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, однако при передаче по ссылке их использовать не стоит.