January 3rd, 2010

glider

Указатели в сишарпе

Иногда даже в managed коде может возникнуть противоестественное желание прокастить объект к void* и шотатам поковырять. Вроде бы сишарп поддерживает и оператор "&", и указатели, и арифметику над указателями, и касты между указателями, и предсказуемое поведение массивов - кароче полный фарш, но не все так просто, в чем я на своем опыте и убедился.

Вначале немного теории:
   1) Любой код, манипулирующий указателями, классифицируется компилятором сишарпа как unsafe. Чтобы случайно не прострелить себе ногу, программист должен дополнительно продублировать свое желание работать с поинтерами, пометив соответствующий метод (или весь класс) ключевым словом unsafe. Чтобы уж наверняка не прострелить себе ногу, программист должен еще до кучи поставить галочку "Allow unsafe code" в Project Properties > Build > General. Абсолютно излишняя паранойя, на мой взгляд, но тут ничего не поделать.
   2) Надо помнить о том, что горбатый коллектор CLR является перемещающим (т.е. в целях оптимизации маллока он уплотняет кучу - детали про GC см. в энциклопедическом посте Notes on the CLR Garbage Collector). Поэтому перед какими-то действиями над сырой памятью объектов референс-типов (классы, массивы) последние надо зафиксировать, чтобы горбатый их случайно не переместил и не нарушил идиллию работы с void*.
   3) По вышеописанной причине в сишарпе нельзя взять адрес объекта референс-типа (например, не скомпилируется вот такой код: &"hello world"), массива (т.е. &arr тоже не скомпилируется - но зато можно написать &arr[0]), а также структуры, одно из полей которой это класс или массив. Более подробно см. в статье MSDN.
   4) Несмотря на ограничение предыдущего пункта мы все-таки можем копаться во внутренностях объектов и массивов, получив их адрес и прокастив его к void*. Сишарп нам этого не позволит, чтобы мы не дай Бог не прострелили себе ногу, но выход все равно есть. Об этом ниже.

Теперь практика. Использование указателей можно классифицировать на две категории: 1) исключительно внутри прилаги (для простоты предположим, что поинтеры не покидают границы аппдомена), 2) с целью интеропа с нативным кодом. Несмотря на кажущуюся надуманность, первая категория активностей не такая уж и бесполезная. Вот, например, как работает метод "byte[] BitConverter.GetBytes(int value)":
Copy Source | Copy HTML
public static unsafe byte[] GetBytes(int value)
{
    byte[] buffer = new byte[4];
    fixed (byte* numRef = buffer)
    {
        *((int*) numRef) = value;
    }
    return buffer;
}
В первом случае применения указателей (для внутренних нужд) все относительно несложно. Для структур можно тупо заюзать оператор "&" и дальше делать все, что угодно. Для классов и массивов нужно для начала запиннить область памяти при помощи класса new GCAlloc(obj, GCHandleType.Pinned), а потом заиметь адрес этой области памяти при помощи метода GCAlloc.AddrOfPinnedObject (внимание: GCAlloc.ToIntPtr предназначен совсем для другой цели!). AddrOfPinnedObject возвращает IntPtr, который кастим к void* и понеслась. В конце изысканий надо не забыть сделать GCAlloc.Free, а то будет мемори лик до самой выгрузки аппдомена. Все это можно делать руками, а можно подъюзать мой враппер.

Во втором случае (юзание указателей для интеропа) скорее всего удастся обойтись вообще без указателей, так как маршаллеры p/invoke и COM достаточно умны, чтобы справиться самостоятельно или с небольшой высокоуровневой помощью программера: например, они умеют сами выбрать корректные форматы маршаллинга (или прочитать значения атрибутов MarshalAs), запиннить необходимые объекты в памяти, правильно обработать ref и out параметры. Об этом можно писать бесконечно, поэтому всего лишь посоветую пару статеек: 1) Calling Win32 DLLs in C# with P/Invoke и 2) P/Invoke, GCHandle и неуправляемая память в .NET.

***

Пост родился из практической задачи. Мне нужно было интеропицца с драйвером nvcuda и передавать ему блоки памяти, которые надо или скопировать на GPU и получить из GPU. Ессно, соответствующий параметр в API имеет тип void*. CUDA.NET для этого объявляет пачку оверлоадов: для byte[], для int[], для float[], для int2[], для float4[] ну и так далее (а маршаллер p/invoke уже сам преобразует их в void*). Это меня не устроило, и я решил сделать один единственный метод, который принимает void*. Тогда автоматически возник вопрос - а как же получить указатель на массив в сишарпе и будет ли все работать с горбатым? Собственно из инвестигейта на эту тему и возник пост, который вы только что прочитали.