MINDON.NET

P/Invoke, GCHandle и неуправляемая память в .NET

«Вся наша икспедиция весь день бродила по лесу…»

Введение

Статья появилась частично как результат ответов в одной из USENET конференций и, частично, как попытка систематизировать свои знания в данной области. Первоочередная цель — разобраться с передачей управляемой памяти (т.е. классов или структур описанных на C#) в неуправляемый код (например внешнюю библиотеку написанную на C/C++.) При этом мы будем целенаправленно избегать решений требующих использования небезопасного (/unsafe) кода. В целом речь пойдет сначала про P/Invoke и основы маршаллинга, а потом про структуру GCHandle. Хочется верить, что этой информации будет достаточно для понимания «как это все работает» и она даст необходимые знания для разработки уже конкретных реализаций.

Плацдарм

Все примеры, приведенные в этой статье, тестировались на второй версии .NET фреймворка, Visual Studio 2005. Были созданы два проекта — консольное приложение на C# и DLL на C/C++. Каркас можно взять здесь. По ходу изложения мы будем немного модифицировать этот код, чтобы продемонстрировать тот или иной эффект.

P/Invoke и основы маршаллинга

Есть мнение, что начать лучше с начала. Поэтому, прежде чем приступить к разбору полетов с GCHandle, мы рассмотрим, что такое P/Invoke, и как, с его помощью, передать структуру или класс в неуправляемый (unmanaged) код.

.NET CLR (common language runtime) предоставляет нам готовый механизм для взаимодействия с неуправляемым кодом. Этих механизмов на самом деле два:

  • Platform invoke, сокращенно P/Invoke,
  • и взаимодействие с COM объектами.

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

Представление данных в CLR и на вызываемой стороне вполне может отличаться. При вызове необходимо сначала преобразовать данные в представление ожидаемое функцией, а затем преобразовать обратно результат. Такая операция называется маршаллингом (marshalling) данных. Среда .NET предоставляет средства автоматического маршаллинга при вызове через P/Invoke. Однако, корректная работа этого механизма требует от программиста указания соответствующих атрибутов при объявлении функций и структур данных. Для людей, впервые сталкивающихся с маршаллингом в .NET, это может показаться весьма нетривиальной задачей.

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

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

Для начала попробуем ответить на следующий вопрос. Как представлять неуправляемую структуру данных на стороне .NET? Должны ли мы использовать структуру или класс? И есть ли разница? Скажем сразу — разница есть и эта разница весьма существенна. Но, обо всем по порядку.

Во-первых, нам потребуется динамическая неуправляемая библиотека экспортирующая функции, которые мы и будем вызывать из .NET среды. Для начала функция будет одна (потом, если потребуется, добавим еще):

__declspec(dllexport) void Function1 (S1* ptrToStructure)
{
    return ;
}

Параметром этой функции является указатель на структуру S1 объявленную следующим образом:

struct S1 {
    int   I1    ;
    short W1    ;
    char  A1[9] ;
    short W2    ;
} ;

Итак, у нас есть структура данных S1, состоящая из 4 полей. Попробуем описать ее на стороне .NET, заполнить данными и передать нашей функции Function1. Уже на этом этапе мы сталкиваемся с определенными трудностями. Проблема возникает с полем A1, которое представляет собой массив байтов длиной 9. В .NET тип char описывает символ в кодировке Unicode (UTF-16). А типу C/C++ char соответствует тип byte. Наш первый вариант мог бы выглядеть следующим образом:

struct S1_0
{
    public int    I1 ;
    public short  W1 ;
    public byte[] A1 ;
    public short  W2 ;
}

К сожалению, в отличии от C/C++, мы не можем объявить массив фиксированного размера. Так как в C# объявление массива это объявление указателя на объект Array. Как вариант, можно воспользоваться ключевым словом fixed и написать:

struct S1_0_1
{
    public int    I1 ;
    public short  W1 ;
    public fixed byte A1[9] ;
    public short  W2 ;
}

Однако, ключевое слово fixed требует объявления кода как небезопасного (/unsafe). Нашей же задачей будет избегать небезопасного кода.

Вернемся к варианту S1_0. Одна из проблем, с которой мы сталкиваемся, — это неизвестный размер структуры, так как byte[] может содержать переменное количество элементов. Если бы мы могли, каким-то образом, сообщить среде исполнения о том, какой тип данных ожидается на удаленной стороне, то может получилось бы сделать автоматическую конвертацию. Такая возможность есть. Для этого, с помощью атрибутов, нужно указать необходимую информацию. Нас интересует атрибут MarshalAs, с помощью которого мы можем дать подсказку встроенному маршаллеру — что делать с типом. В частности мы можем указать, что данный тип это либо строка символов UnmanagedType.ByValTStr либо массив данных UnmanagedType.ByValArray. В обоих случаях нам надо специфицировать длину. А в случае строки символов еще и указать кодировку.

Итак, попробуем вариант массива.

[StructLayout(LayoutKind.Sequential)]
struct S1_1
{
    public int    I1 ;
    public short  W1 ;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=9)]
    public byte[] A1 ;
    public short  W2 ;
}

Забегая вперед отметим, что .NET не гарантирует, что порядок объявления полей будет соответствовать их порядку в оперативной памяти. Чтобы специфицировать порядок, следует воспользоваться атрибутом StructLayout, который применяется к классам и структурам. Этот атрибут задает три разных способа представления данных в памяти.

  • Sequential — последовательное расположение полей в порядке их объявления, т.е. то, что нас интересует в данном случае;
  • Explicit — явное указание позиции каждого поля как смещения от начала структуры данных; смещение указывается с помощью атрибута FieldOffset;
  • Auto — автоматическое размещение полей; среда сама принимает решение как лучше расположить поля в структуре данных; в частности будет предпринята попытка оптимизировать размещение.

В C# все структуры по умолчанию имеют Sequential размещение, в то время как классы объявлены как Auto. Поэтому, в случае использования классов как неуправляемых данных, надо явно указать атрибут StructLayout. Мы увидим в следующих примерах к чему может привести отсутствие этого атрибута.

Теперь у нас есть структура данных. Импортируем функцию Function1 из нашей библиотеки. В C# структура — это значимый тип (value type), и передается она по значению, а не по ссылке. Наша функция, напротив, ожидает указатель на структуру. Поэтому в объявлении надо явно указать этот факт с помощью ключевого слова ref:

[DllImport("Native.dll")]
public static extern void Function1 (ref S1_1 ptrToStructure) ;

Проинциализируем нашу структуру и вызовем функцию:

S1_1 s11 ;

s11.I1 = 1 ;
s11.W1 = 2 ;
s11.W2 = 3 ;
s11.A1 = new byte[9] ;
s11.A1[0] =  97 ; // a
s11.A1[1] =  98 ; // b
s11.A1[2] =  99 ; // c
s11.A1[3] = 100 ; // d

Function1 (ref s11) ;

Поставим точку остановa в теле функции и поглядим, что же мы получили.

Как и ожидалось, данные попали в нужном нам виде. Теперь проведем небольшой эксперимент. Заменим структуру на класс и уберем StructLayout. В этом случае нам больше не требуется ключевое слово ref, так как класс и так передается по ссылке.

class C1_1
{
    public int    I1 ;
    public short  W1 ;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=9)]
    public byte[] A1 ;
    public short  W2 ;
}

[DllImport("Native.dll")]
public static extern void Function1 (C1_1 ptrToStructure) ;

Наблюдаем набор бессмысленных данных. Если же вернуть атрибут на место, то получим ожидаемый результат.

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

Этот феномен легко объясним, если поглубже копнуть. Посмотрим, что же на самом деле маршаллер проделает с данными в случае подобного вызова. Оказывается вариантов может быть два. В зависимости от ситуации, данные могут быть либо зафиксированы по их реальному адресу (pinning) или же скопированы в новый динамически выделенный буфер (copying).

Какая из ситуаций будет реализована зависит от типа данных. Маршаллер различает, так называемые, blittable данные и non-blittable данные. Blittable данные имеют одинаковое представление внутри виртуальной машины (управляемой памяти) и в неуправляемой памяти. Сюда попадают все примитивные числовые типы (byte, short, int, long, их беззнаковые аналоги, а также single и double), одномерные массивы состоящие целиком из blittable типов (например массив целых чисел), а также структуры/классы состоящие из blittable типов (но не включающие массивов!) Строки, символы, а также тип bool не являются blittable.

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

В противном случае в памяти выделяется новый буфер и данные копируются в этот буфер. В процессе копирования маршаллер делает необходимые преобразования, чтобы данные оказались в правильном представлении. В случае передачи класса по значению, вызываемой функции передается указатель на копию. В случае передачи класса по ссылке, передается указатель на указатель на копию. Как же быть если вызываемая функция предполагает изменение переданных данных? В этом случае нужно сообщить маршаллеру, что такое поведение ожидается. Сделать это можно с помощью атрибутов In и Out. Если указан атрибут In, то копия инициализируется данными класса, но данные не будут скопированы обратно по окончанию вызова. Если указан атрибут Out, то создается пустая копия, которая копируется в исходный класс по окончанию вызова. Если указаны оба атрибута, то будут выполнены обе операции копирования. И наконец, если не указан ни один из атрибутов, то маршаллер сделает оптимизацию на свое усмотрение. В частности если передается структура, а не класс, то маршаллер будет руководствоваться наличием ключевых слов ref (соответствует указанию [In,Out]) и out (соответствует [Out]).

Это все конечно интересно, но давайте поглядим как это будет работать на практике. Итак, сначала разберемся с blittable структурами. Заведем еще одну структуру S2 и функцию Function2.

struct S2 {
    int       I1 ;
    short     W1 ; 
    long long L1 ; // 64-bit
    short     W2 ;
} ;

__declspec(dllexport) void Function2 (S2* ptrToStructure)
{
    return ;
}

И соответственно управляемые версии:

[StructLayout(LayoutKind.Sequential)]
struct S2
{
    public int   I1 ;
    public short W1 ;
    public long  L1 ;
    public short W2 ;
}

[StructLayout(LayoutKind.Sequential)]
class C2
{
    public int   I1 ;
    public short W1 ;
    public long  L1 ;
    public short W2 ;
}

[DllImport("Native.dll")]
public static extern void Function2 (ref S2 ptrToStructure) ;

[DllImport("Native.dll")]
public static extern void Function2 (C2 ptrToStructure) ;

Начнем с передачи структуры как [In,Out].

S2 s2 ;

s2.I1 = 1 ;
s2.W1 = 2 ;
s2.L1 = 4 ;
s2.W2 = 3 ;

Function2 (ref s2) ;

Вроде все правильно. А теперь тест на внимательность. На самом деле — не все как ожидалось. Если внимательно изучить дамп памяти, то можно заметить, что W1 занимает целых 4 байта, а не 2. Вот тут мы сталкиваемся с еще одной проблемой — проблемой выравнивания данных. В нашем случае память в управляемой куче оказалась выровнена на границу слова (т.е. 4 байта.) К счастью в неуправляемой памяти такая же самая история. Однако, давайте заменим нашу неуправляемую структуру на такую:

struct S2_1 {
    int    I1    ;
    short  W1    ; 
    char   L1[8] ; // 64-bit
    short  W2    ;
} ;

Размеры полей — те же самые. Посмотрим, что мы получим в этом случае.

Представление в управляемой памяти не изменилось. Но изменилось выравнивание на неуправляемой стороне, теперь поле L1 имеет смещение 6, а не 8, байт. А поле W2 смещение 14 байт, и, в результате, число 3 потерялось. Мы можем явно выставить необходимое выравнивание как на управляемой, так и на неуправляемой стороне (см. #pragma pack(n).) В .NET это задается с помощью того же атрибута StructLayout. Надо лишь дополнительно указать Pack=выравнивание в байтах.

[StructLayout(LayoutKind.Sequential, Pack=2)]
struct S2
{
    public int   I1 ;
    public short W1 ;
    public long  L1 ;
    public short W2 ;
}

Попробуем снова.

Наблюдаем уже правильную картину. Обратите внимание на расположение полей в памяти.

С выравниванием мы разобрались. Вернемся к нашим blittable типам. Вернем исходный вариант S2 и попробуем изменять в отладчике значения полей в памяти находясь в неуправляемом коде. После чего проверим значения в управляемой структуре.

Как и ожидалось, значения заменились также и в исходной управляемой структуре.

Теперь перейдем к non-blittalbe типам. Здесь ситуация несколько интересней. Вернемся к С1_1 и S1_1. Сначала посмотрим на структуру.

[DllImport("Native.dll")]
public static extern void Function1 (ref S1_1 ptrToStructure) ;

Заменим значения памяти следующим образом

и посмотрим на управляемую память.

Что и требовалось доказать. Память была скопирована в обе стороны. Теперь заменим сигнатуру вызова на:

[DllImport("Native.dll")]
public static extern void Function1 (out S1_1 ptrToStructure) ;

Посмотрим, что было передано в неуправляемый код.

Память проинициализирована нулями. В целях оптимизации исходные данные не были скопированы, как и требуется для [Out] аргументов. Теперь проверим как работают классы. Для начала не будем указывать никаких атрибутов.

[DllImport("Native.dll")]
public static extern void Function1 (C1_1 ptrToStructure) ;

Смотрим в неуправляемый код.

Данные скопировались, т.е. маршаллер по умолчанию рассматривает ситуцию как [In]. Но, если мы изменим данные, то на управляемой стороне изменений не произойдет, так как атрибут [Out] указан не был.

Если же указать [Out], то ситуация будет аналогична ситуации со структурой.

Теперь, когда мы разобрались с деталями, вернемся к нашему исходному примеру. Вспомним, что на самом деле в неуправляемой структуре поле A1 это строка из 9 символов. Будем считать, что символы записаны в кодировке ANSI. Т.е. 1 байт на символ, стандартная ASCII таблица. Пока что нам приходилось руками указывать коды этих символов. В идеале хотелось бы просто передать управляемую строку и возложить задачу по перекодировке на маршаллер. Оказывается это тоже возможно. Чтобы этого добиться, надо указать UnmanagedType.ByValTStr, а также указать ожидаемую кодировку либо для всей структуры либо в объявлении функции. Объявление структуры будет выглядеть следующим образом. Обратите внимание на тип string.

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
struct S1_2
{
    public int    I1 ;
    public short  W1 ;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=9)]
    public string A1 ;
    public short  W2 ;
}

При таком объявлении мы можем написать следующий код:

S1_2 s12 ;

s12.I1 = 1 ;
s12.W1 = 2 ;
s12.W2 = 3 ;
s12.A1 = "abcd" ;

Function1 (ref s12) ;

И магическим образом получить правильный результат.

Заменим теперь "abcd" на "efgh" на неуправляемой стороне и проверим, что получилось.

Маршаллер проделал за нас всю работу. По моему неплохо.

Обещанный GCHandle или ссылки для посвященных

«И каждый в икспедиции конечно был бы рад
Узнать, что значит GCHandle и с чем его едят!»

Иногда возникает ситуация, когда необходимо не просто передать указатель на память в неуправляемую библиотеку, а гарантировать, что этот указатель будет корректным и после возвращения из функции. Как пример можно привести Waveform Audio Library. В случае работы с этой библиотекой из .NET, требуется передавать аудио подсистеме блоки данных, которые она асинхронно возвращает в процессе работы. В этом случае память должна быть зафиксирована все время, т.е. должен существовать актуальный указатель. Мы опять же не будем рассматривать варианты с небезопасным (/unsafe) кодом, а разберемся как можно решить эту задачу не выходя за рамки управляемого кода (хотя и привлекая тяжелую артиллерию .NET)

Решение строится на базе специальной структуры GCHandle. Эта структура позволяет создавать специальные «ссылки» на данные. Мы попытаемся детально разобрать два варианта таких ссылок — Normal и Pinned. В этой статье мы не будем рассматривать случай со слабыми ссылками (WeakReference).

В дальнейшем, чтобы не путаться в терминах, мы будем именовать объект GCHandle дескриптором, а ссылкой будем называть указатель на управляемый объект который этот дескриптор содержит.

Итак, чего же позволяет достичь GCHandle? Рассмотрим метод GCHandle.Alloc, который собственно и создает конкретный экземпляр типа GCHandle. В расширенной своей версии он принимает два аргумента — объект и тип ссылки/дескриптора. Таких типов четыре. Но нас интересуют только первые два:

  • Normal — непрозрачная ссылка на объект, т.е. такая ссылка которая не дает возможности узнать адрес объекта в памяти. Существование такой ссылки гарантирует, что объект не будет собран сборщиком мусора. Такого рода ссылки позволяют держать в памяти управляемый объект на который ссылается только некий неуправляемый объект за пределами виртуальной машины.
  • Pinned — такая ссылка имеет все свойства, что и Normal, но дополнительно дает возможность узнать адрес памяти по которому размещен объект. Фиксирование объекта по конкретному адресу достачно серъезная операция. Такой объект не может быть перемещен сборщиком мусора в случае оптимизации кучи. А это влечет за собой потерю производительности. (Более детальное описание потребовало бы разбора того как работает сборщик мусора в .NET, что впрочем тоже интересно.)

Теперь пройдемся по свойствам и методом структуры GCHandle. Свойства (их аж две штуки):

  • IsAllocated — индикатор, который показывает актуальна ли выделенная ссылка.
  • Target — возвращает ссылку на управляемый объект.

Методы (рассмотрим только нетривиальные):

  • AddrOfPinnedObject() возвращает адрес памяти (как IntPtr) по которому размещен управляемый объект. Этот метод можно вызывать только для Pinned ссылок, во всех остальных случаях он бросает исключение.
  • Alloc(...) собственно создает ссылку. Его мы уже рассмотрели.
  • Free() освобождает ссылку выделенную с помощью Alloc(). Важно всегда освобождать выделенные ссылки иначе будут утечки памяти. Неосвобожденные ссылки будут собраны только в момент выгрузки соответствующего AppDomain.
  • ToIntPtr() и op_Explicit(GCHandle) эти методы индентичны, оба возвращают внутреннее представление GCHandle дескриптора, которое можно передать в неуправляемый код.
  • FromIntPtr() и op_Explicit(IntPtr) восстанавливают экземпляр GCHandle из внутреннего представления, полученного с помощью ToIntPtr().

Глядя на всю эту путаницу со ссылками, дескрипторами и IntPtr, может возникнуть вопрос — в чем же разница между значениями возвращаемыми AddrOfPinnedObject() и ToIntPtr()?

Короткий вариант ответа: AddrOfPinnedObject() применим только к Pinned ссылкам и возвращает адрес памяти по которому расположен объект на который ссылается GCHandle. ToIntPtr() применим к любым ссылкам и возвращает представление самого GCHandle дескриптора, а не объект на который последний ссылается.

Чтобы лучше понять, что делают методы ToIntPtr()/FromIntPtr()/AddrOfPinnedObject() нам придется заглянуть в самое нутро виртуальной машины.

Вызов Alloc обращается к текущему AppDomain и просит последний выделить ему память под ссылку на объект из текущего пула ссылок. (Этот пул устроен как многоуровневый кеш.) Выделенная память инициализируется указателем на память по которому размещен объект. Т.е. если заглянуть внутрь экземпляра GCHandle, то, грубо говоря, он будет содержать текущий адрес ячейки памяти в которой содержится текущий адрес объекта. Если эта ссылка не закрепленная (pinned), то объект может быть перемещен сборщиком мусора и адрес объекта изменится. Индикатором закрепленности служит младший бит.

Вооружимся отладчиком и проверим. Вернемся к нашему классу C2 и напишем следующий код.

C2 c2 = new C2 () ;
c2.I1 = 1 ;
c2.W1 = 2 ;
c2.L1 = 3 ;
c2.W2 = 4 ;

GCHandle gch = GCHandle.Alloc (c2, GCHandleType.Normal) ;
IntPtr ptr   = GCHandle.ToIntPtr (gch) ;
gch.Free () ;

Как мы видим, GCHandle указывает на адрес памяти 0x003E11A8. Идем по этому адресу и обнаруживаем там адрес нашего объекта 0x01281D3C.

На самом деле сами данные начинаются по смещению в 4 байта. Впрочем дамп памяти по этому адресу говорит сам за себя.

После вызова gch.Free() память по адресу 0x003E11A8 обнулилась. Этот вызов возвращает выделенный слот обратно в пул.

Теперь заменим тип ссылки на GCHandleType.Pinned и посмотрим, что изменилось.

Теперь у дескриптора GCHandle взведен младший бит, в то время как указатель на объект располагается по четному адресу. Установленный младший бит указывает на pinned статус.

Добавим вызов AddrOfPinnedObject(). Удостоверимся, что он возвращает правильный адрес объекта в памяти.

Так как AppDomain ведет учет всех выделенных дескрипторов, то, до тех пор пока не будет вызван метод Free(), ссылка на объект останется висеть (пока не будет выгружен сам AppDomain.) Соответственно, очень важно не потерять дескриптор, иначе произойдет утечка памяти. С другой стороны становится возможным передавать такой дескриптор за пределы управляемой памяти. Вызов ToIntPtr() фактически вернет адрес в пуле. Зная этот адрес, мы всегда можем восстановить экземпляр GCHandle (так как последний это просто обертка), это и делает метод FromIntPtr().

Таким образом, становится возможным следующий сценарий. Допустим у нас есть управляемый объект, указатель на который надо на время передать в неуправляемый код, а потом получить обратно. В управляемом коде на этот объект никто не ссылается. Это можно сделать следующим образом:

GCHandle gch = GCHandle (new Object ()) ;
IntPtr unmanagedPtr = GCHandle.ToIntPtr (gch) ;

Теперь мы можем передать unmanagedPtr неуправляемой функции. Заметим, что в данном случае мы создали GCHandle с типом Normal. То есть нам не требуется доступ к внутренностям объекта из неуправляемого кода, а только факт наличия ссылки на него. Когда мы получим этот дескриптор обратно, мы сможем восстановить наш объект (т.е. конечно не сам объект, он то никуда не исчезал, а управляемую ссылку на него):

IntPtr unmanagedPtr = CallToSomeUnmanagedFunction() ;
GCHandle gch = GCHandle.FromIntPtr (unmanagedPtr) ;
Object managedPtr = gch.Target ;
gch.Free () ;

Причем объект к этому моменту вполне может быть перемещен по другому адресу памяти.

Если же GCHandle был создан с типом Pinned, то дополнительно будет возможно узнать адрес памяти по которому размещен объект. То есть неуправляемый код может получить доступ к внутренностям объекта. Однако, как уже было сказано ранее, это накладывает ряд ограничений по производительности и должно применяться только в действительно необходимых ситуациях.

Стоит также отметить ряд подводных камней с которыми легко столкнутся при работе с GCHandle. Chris Lyon рассказывает о проблеме двойного освобождения дескриптора. Действительно, так как дескриптор GCHandle это структура, то такой экземпляр будет скопирован по значению и следующий код создаст два дескриптора — оба ссылающихся на одну и ту же запись в пуле ссылок:

GCHandle gch1 = GCHandle.Alloc (new Object ()) ;
GCHandle gch2 = gch1 ;

Если вызывать метод Free() у gch1, то эта запись будет освобождена. Однако экземпляр gch2 ничего об этом не узнает и будет продолжать считать, что держит указатель на актуальную ссылку. Последующий вызов Free() у gch2 скорее всего сразу приведет к исключительной ситуации внутри виртуальной машины. Такая же участь постигнет и попытка достать сам объект. Впрочем, не исключен и вариант, когда данный слот будет снова выделен, уже под другой объект, и gch2 будет ссылаться на новый объект, сам того не подозревая. Последствия могут оказаться весьма непредсказуемыми.

Еще один момент связан с использованием структур. Попробуйте ответить на вопрос — что будет делать данный код:

struct S
{
    public IntPtr AddressInMemory ;
}

S s = new S () ;
GCHandle gch = GCHandle.Alloc (s, GCHandle.Pinned) ;
s.AddressInMemory = gch.AddrOfPinnedObject () ;

Если вы подумали, что в AddressInMemory будет занесен адрес переменной s в памяти, то подумайте еще раз. Проблема в том, что s это структура и, соответственно, она будет передана в метод Alloc по значению, т.е. туда будет передана сбоксированная (boxed) копия, а не сама переменная s. В результате, gch будет указывать на эту временную копию, вместо исходной структуры s. Этот факт говорит о том, что использование GCHandle со структурами не имеет смысла. Если же заменить struct на class, то код будет работать как задумано. Стоит отметить, что я сам попался на подобную удочку. Такой момент очень легко упустить, а последствия очень сложно обнаружить.

На этой оптимистичной ноте мы и завершим наши изыскания.

© 2007, Олег Гродзевич