Начнём с небольшой теории. Большинство стандартных типов в .NET являются объектами, жизненным циклом каждого из которых управляет CLR, и нам, программистам, не нужно заботиться о выделении и освобождении памяти для каждого из объектов. Всё это берет на себя CLR. Но бывают такие моменты когда возможностей .NET может не хватать (скорость алгоритмов [кривые руки не учитываются], взаимодействие с ОС на более низком уровне и т.п.) или некоторые решения уже есть но на языке отличном от C#. Данную проблему можно решить путём взаимодействия с библиотекой написанной на native языке (Platform Invoke) - где CLR будет выступать в виде средства взаимодействия (выполнять нужные преобразования над передаваемой информацией), или же использование небезопасного кода в приложении - для прямого (без контроля CLR) обращения к памяти, выделенной под объекты. Об этом и пойдет речь в данной статье.
Тип данных System.IntPtr используется для представления указателя (void *) на участок памяти. Размер данного типа будет различаться в зависимости от разрядности приложения: PE32 - 4 байта, PE64 - 8 байт. Начиная с .NET 4.0 у данной структуры появилась возможность использования арифметических операций "+" и "-".
Использовать функции предоставляемые данным классом нужно в тех случаях, когда есть необходимость в применении неуправляемого кода или PInvoke. В данной статье будут рассмотрены наиболее важные (по моему мнению) и часто используемые функции.
3.1. Функции выделения и освобождения неуправляемой памяти
3.1.1. Marshal.AllocHGlobal - выделение в куче участка памяти, инициализированного изначально мусором. Внутренняя реализация представляет из себя обёртку над WinAPI функцией LocalAlloc, с параметром uFlags равным LMEM_FIXED (выделяемая память фиксирована в куче). Если во время вызова LocalAlloc произошел сбой, то функция выдаст исключение OutOfMemoryException.
3.1.2. Marshal.ReAllocHGlobal - изменение размера выделенного функцией AllocHGlobal участка памяти, в большую или меньшую сторону. Внутренняя реализация является обёрткой над WinAPI LocalReAlloc. Если функция завершилась корректно, то будет возвращён указатель на участок выделенной памяти, он может совпадать с тем что был до вызова ReAllocHGlobal или нет, в этом случае участок памяти выделенный ранее, с помощью AllocHGlobal, будет скопирован (нужного размера) по новому адресу, а старый участок будет освобождён. В случае некорректного завершения LocalReAlloc, функция выдаст исключение OutOfMemoryException.
3.1.3. Marshal.FreeHGlobal - освобождает память выделенную вызовом AllocHGlobal. Внутренняя реализация является обёрткой над WinAPI LocalFree. В случае некорректного завершения LocalFree, FreeHGlobal выдаст исключение COMException с кодом ошибки.
usingSystem;usingSystem.Runtime.InteropServices;...staticvoid AllocationExample (){constint NEW_MEM_SIZE = 0x200;byte[] buff =newbyte[0x400];// Выделение 1024 байт
IntPtr pMem = Marshal.AllocHGlobal( 0x400 );for(int i =0; i < buff.Length;++i )
buff[i]=(byte)i;// Копируем массив в выделенный участок памяти
Marshal.Copy( buff, 0, pMem, buff.Length);// Уменьшаем размер выделенной области памяти в 2 раза
pMem = Marshal.ReAllocHGlobal( pMem, (IntPtr)NEW_MEM_SIZE );// Очищаем массивfor(int i =0; i < buff.Length;++i )
buff[i]=0;// Копируем значения из измененного участка памяти
Marshal.Copy( pMem, buff, 0, NEW_MEM_SIZE );/* Если скопировать участок памяти больший по объему * чем выделено, то есть шанс получить ошибку * Access Violation - это значит что доступ к участку * памяти заблокирован. Если же исключения не будет, то * скорее всего будет скопирован мусор. */// Обязательно освобождение выделенной памяти!
Marshal.FreeHGlobal( pMem );}
3.2. Копирование управляемой памяти в неуправляемую и наоборот
Marshal.Copy предоставляет множество перегрузок для копирования массивов всех стандартных типов из неуправляемой памяти в управляемую и наоборот.
Пример обработки изображения ускоренным методом (относительно GetPixel/SetPixel):
usingSystem;usingSystem.Runtime.InteropServices;usingSystem.Drawing;usingSystem.Drawing.Imaging;...staticvoid CopyExample (){using(var bmp =new Bitmap(100, 100)){// Фиксируем изображение в памятиvar bd = bmp.LockBits(new Rectangle(0, 0, 100, 100),
ImageLockMode.ReadWrite,
bmp.PixelFormat);// Буфер под размер изображенияvar buffer =newbyte[bd.Stride* bd.Height];// Копируем байтовое представление изображения// в выделенный буфер
Marshal.Copy( bd.Scan0, buffer, 0, buffer.Length);/* * Выполнение некоторых модификаций над буфером */// Копируем буфер обратно по адресу расположения// изображения в памяти
Marshal.Copy( buffer, 0, bd.Scan0, buffer.Length);// Разблокируем изображение
bmp.UnlockBits( bd );}}
3.3. Определение размера занимаемой объектом (или типом) неуправляемой памяти
Перед выделением памяти под объект нужно знать, сколько байт необходимо выделить. Для этого есть функция Marshal.SizeOf, которая может получить от CLR размер объекта или типа в неуправляемой памяти.
usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.InteropServices;...struct Example
{publicint i1;publiclong l1;}staticvoid SizeOfExample (){// Вывод размера структуры и типа в окно Output
Debug.WriteLine("Object: {0}\r\nType: {1}",
Marshal.SizeOf(new Example()),
Marshal.SizeOf(typeof( Example )));/* Object: 16 * Type: 16 */}
Функция Marshal.OffsetOf позволяет рассчитать смещение поля относительно начала размещения структуры в неуправляемой памяти. Это может понадобиться когда размер структуры меняется в зависимости от ОС и/или её разрядности. Функция позволяет автоматизировать расчет смещения, а не высчитывать его каждый раз самостоятельно.
usingSystem;usingSystem.Diagnostics;usingSystem.Runtime.InteropServices;...struct Example
{publicint i1;publiclong l1;}staticvoid OffsetOfExample (){// Вывод смещения поля l1 относительно// начала размещения структуры в памяти
Debug.WriteLine(
Marshal.OffsetOf(typeof( Example ), "l1"),
"Offset");// Offset: 8}
После того как память выделена с ней можно выполнять операции чтения/записи, для этого есть функции Marshal.ReadX и Marshal.WriteX (вместо X нужно подставить название стандартного типа заданного в CLR). Каждая функция имеет набор перегрузок, которые позволяют писать данные нужного типа в начало объекта или по определенному адресу (с указанием смещения, если это необходимо). Эти методы лучше всего использовать когда запись в память должна происходить небольшими фрагментами, если же есть необходимость писать или читать память большими кусками, то для этого лучше использовать функцию Marshal.Copy, т.к. запись большого участка памяти значительно быстрее чем побайтовая запись при помощи данных функций.
usingSystem;usingSystem.Runtime.InteropServices;...staticvoid ReadWriteExample (){using(var bmp =new Bitmap(100, 100)){var bd = bmp.LockBits(new Rectangle(0, 0, 100, 100),
ImageLockMode.ReadWrite,
bmp.PixelFormat);int length = bd.Stride* bd.Height;byte temp =0;for(int i =0; i < length;++i )/* Читаем 1 байт смещению i относительно * адреса Scan0, записываем его в temp * и сравниваем со значением 127 */if(( temp = Marshal.ReadByte( bd.Scan0, i ))>127)// Пишем по смещению i относительно// адреса Scan0 значение i
Marshal.WriteByte( bd.Scan0, i, (byte)i );else// Пишем по смещению i относительно// адреса Scan0 значение temp xor 7
Marshal.WriteByte( bd.Scan0, i, (byte)( temp ^7));
bmp.UnlockBits( bd );}}
3.6.1. Marshal.StructureToPtr и Marshal.PtrToStructure
Чтобы обращаться к объекту через неуправляемую память нужно получить адрес по которому объект располагается в памяти. Для этого служит функция Marshal.StructureToPtr. Принцип её работы состоит в том, что весь объект копируется (даже если это ссылочный тип) в заранее выделенный фрагмент памяти. В этой функции важное значение играет 3-й параметр типа Boolean, который отвечает за освобождение скопированных туда ранее данных. При первом вызове данной функции (сразу после выделения фрагмента памяти) этот параметр должен быть false, т.к. очищать нечего, но при последующих вызовах с использованием указателя на один и тот же участок памяти следует установить параметр в true, чтобы избежать "невидимых" утечек памяти.
Для того чтобы получить нужный нам объект из неуправляемой памяти существует функция Marshal.PtrToStructure, которая также выполняет полное копирование объекта из неуправляемой памяти в управляемую.
usingSystem;usingSystem.Runtime.InteropServices;...struct Example
{publicint i1;publiclong l1;}staticvoid StructureToPtrExample (){// Создаем объектvar ex =new Example();// Выделяем под него памятьvar ptr = Marshal.AllocHGlobal( Marshal.SizeOf(typeof( Example )));/* Копируем созданный объект в выделенный * участок памяти, и при этом не хотим * чтобы блок памяти перед этим очищался */
Marshal.StructureToPtr( ex, ptr, false);// Записываем в первые четыре байта выделенной// памяти, т.е. в поле i1, значение 10
Marshal.WriteInt32( ptr, 0xA );// Копируем структуру обратно в ex
ex =(Example)Marshal.PtrToStructure( ptr, typeof(Example));// Освобождаем память!
Marshal.FreeHGlobal( ptr );}
Чтобы избежать копирования ссылочных типов в неуправляемую память, нужно закрепить объект в управляемой памяти и не давать его очистить сборщику мусора. Для этого существует структура System.Runtime.InteropServices.GCHandle. Получить адрес объекта можно вызвав статический метод Alloc этой структуры, который принимает 2 параметра: 1-й это нужный объект адрес которого нужно получить, а второй это один из вариантов перечисления GCHandleType, который определяет какой тип дескриптора будет создан и как будет обращаться с объектом сборщик мусора, в рамках данной статьи нас интересует только GCHandleType.Pinned - объект будет закреплен в управляемой памяти и не будет удален сборщиком мусора, также появляется возможность получить адрес объекта вызвав AddrOfPinnedObject созданного экземпляра структуры GCHandle.
После того как работа с объектом, используя полученный адрес, завершена, следует обязательно вызвать метод Free созданного экземпляра структуры GCHandle, иначе объект будет навсегда закреплен в памяти и сборщик мусора к нему не притронется, что повлечет утечку памяти.
usingSystem;usingSystem.Runtime.InteropServices;...// О атрибуте StructLayout описано в разделе 4.3[StructLayout(LayoutKind.Sequential)]class Example
{publicint i1;publiclong l1;}staticvoid GCHandleExample (){// Создаём объектvar example =new Example();// Фиксируем его в памятиvar gch = GCHandle.Alloc( example, GCHandleType.Pinned);{// Пишем значение 4 в поле i1 напрямую, т.е.// значение в example будет изменено сразу// без копирования объектов
Marshal.WriteInt32( gch.AddrOfPinnedObject(), 0x4 );}
gch.Free();// Освобождаем объект от закрепления}
3.7. Копирование строк из управляемой памяти в неуправляемую и наоборот
Для копирования строки в неуправляемую память существуют функции Marshal.StringToHGlobalAnsi и Marshal.StringToHGlobalUni. Первая выполняет копирование строки в неуправляемую память в кодировке ANSI, а вторая в Unicode.
Для копирования строки из неуправляемой памяти служат функции Marshal.PtrToStringAnsi и Marshal.PtrToStringUni.
После того как работа с выделенным под строку фрагментом памяти завершена, необходимо её освободить вызвав Marshal.FreeHGlobal.
usingSystem;usingSystem.Runtime.InteropServices;...staticvoid AnsiStringCopyExample (){var example ="Some text here";// Копируем example в неуправляемую памятьvar pMem = Marshal.StringToHGlobalAnsi( example );// Пишем в память NULL взамен второго пробела
Marshal.WriteByte( pMem +9, 0);// Копируем строку обратно, и получаем// "Some text", т.к. будет скопирован участок// памяти до первого символа \0 (NULL)
example = Marshal.PtrToStringAnsi( pMem );// Освобождаем память!
Marshal.FreeHGlobal( pMem );}staticvoid UniStringCopyExample (){var example ="Some text here";// Копируем example в неуправляемую памятьvar pMem = Marshal.StringToHGlobalUni( example );// Пишем в память NULL взамен второго пробела// но теперь смещение мы берем в 2 раза больше// т.к. юникод это 2-х байтовая кодировка
Marshal.WriteByte( pMem +18, 0);// Обнуляем 19-й
Marshal.WriteByte( pMem +19, 0);// и 20-й байты// Копируем строку обратно, и получаем "Some text"
example = Marshal.PtrToStringUni( pMem );// Освобождаем память!
Marshal.FreeHGlobal( pMem );}
Множество алгоритмов и всевозможных решений в настоящее время существуют в виде DLL библиотек, что позволяет их использовать в приложениях путем вызова экспортируемых функций из них. В CLR для этого встроен специальный "сервис": Platform Invocation Service который позволяет легко вызывать находящиеся в DLL функции, а также выполнять необходимые преобразования параметров для передачи их из управляемого кода в неуправляемый. Для взаимодействия с этим сервисом существуют специальные атрибуты: DllImport, MarshalAs и StructLayout. Они позволяют настроить сервис необходимым образом, чтобы при вызове неуправляемой функции все параметры передавались нужным образом.
Атрибут DllImport служит для указания из какой библиотеки и какую функцию следует импортировать для использования в приложении. У данного атрибута есть несколько параметров, которые позволяют настроить поиск функции в библиотеке, указать способ передачи параметров и их преобразование.
Первым параметром при объявлении данного атрибута является имя DLL, в которой будет происходить поиск функции.
Прототип импортируемой функции должен быть объявлен с использованием ключевых слов static и extern.
Рассмотрим некоторые свойства данного атрибута:
EntryPoint - позволяет указать имя импортируемой функции или её порядковый номер. Если данный параметр установлен, то имя прототипа функции может быть любым.
CharSet - указывает преобразование строк в параметрах функции, а также позволяет CLR выбирать какую версию функции импортировать (Unicode или ANSI), подставляя к имени функции окончание A или W, если это не указано явно.
CharSet.None - это значение в настоящее время устарело и CLR вместо него использует CharSet.Ansi.
CharSet.Ansi - преобразовывает строки к однобайтовому (ANSI) представлению.
CharSet.Unicode - преобразовывает строки к двухбайтовому (Unicode) представлению.
CharSet.Auto - использует тип преобразования в зависимости от ОС. ANSI на Win 98 и ME, и Unicode на остальных. По умолчанию в C# используется CharSet.Ansi.
SetLastError - данный параметр очень важен при импорте WinAPI функций, т.к. он указывает CLR что после вызова функции необходимо вызвать WinAPI функцию GetLastError и сохранить в памяти результат её работы.
Этот момент важен, т.к. при вызове функции, CLR может внутри себя вызывать другие WinAPI функции, в результате чего вызов GetLastError напрямую может вернуть совсем не то значение которое ожидалось.
После того как WinAPI функция вернула управление нашей программе, то узнать результат выполнения функции GetLastError можно вызвав Marshal.GetLastWin32Error. По умолчанию значение параметра SetLastError равно false.
ExactSpelling - указывает является ли имя функции установленное в параметре EntryPoint или в прототипе, конечным именем. Если данный параметр установлен, то CLR не будт модифицировать название функции (подставлять A или W), а будет пытаться найти указанное имя. По умолчанию в C# значение данного параметра равно false.
CallingConvention - соглашение о порядке и способе передачи параметров в неуправляемую функцию. По умолчанию параметр имеет значение CallingConvention.StdCall.
Примеры построения некоторых прототипов WinAPI функций:
// Получение заголовка окна из приложения "Калькулятор"[DllImport("USER32.DLL", SetLastError =true)]staticextern IntPtr FindWindow (string lpClassName,
string lpWindowName
);[DllImport("USER32.DLL", SetLastError =true, EntryPoint ="GetWindowText"/*, ExactSpelling = true */)]staticexternint gwt (
IntPtr hWnd,
StringBuilder lpString,
int nMaxCount
);// Тоже самое что и пример выше//[DllImport( "USER32.DLL", SetLastError = true )]//static extern int GetWindowText (// IntPtr hWnd,// StringBuilder lpString,// int nMaxCount// );staticvoid Main (string[] args ){// Создаем буфер длиной 256 символовvar sb =new StringBuilder(256);// Получаем текст из приложения
gwt(// Ищем окно с классом CalcFrame
FindWindow("CalcFrame", null),
sb, // Буфер
sb.Capacity// Размер буфера);// Если вызов GetWindowText завершился некорректно// то выводим ошибкуif( Marshal.GetLastWin32Error()!=0)
Console.WriteLine("Error: "+ Marshal.GetLastWin32Error());else
Console.WriteLine( sb );
Console.ReadLine();}
Если немного изменить данный пример, раскомментировав параметр ExactSpelling и запустив приложение, выполнение программы прервётся на месте вызова функции gwt с ошибкой EntryPointNotFoundException: "Unable to find an entry point named 'GetWindowText' in DLL 'USER32.DLL'.".
[DllImport("TestC.dll", CallingConvention = CallingConvention.Cdecl)]staticexternintAdd(byte[] first,
byte[] second,
byte[] result,
int count
);staticvoid Main (string[] args ){constint COUNT =25;byte[] f =newbyte[COUNT],
s =newbyte[COUNT],
r =newbyte[COUNT];for(int i =0; i < COUNT;++i )
f[i]= s[i]=(byte)i;if(Add( f, s, r, COUNT )==0)for(int i =0; i < COUNT;++i )
Console.WriteLine( r[i]);}
Закомментировав параметр CallingConvention, при вызове функции будет выдано исключение PInvokeStackImbalance, что говорит о неверном способе передачи параметров в неуправляемую функцию.
Пример вызова функции с разным окончанием (A или W) в зависимости от установленного CharSet: (скачать проект - DllImportExample.rar)
Запустив скомпилированное приложение с закомментированным параметром CharSet и потом с раскомментированным, будет видно как параметр CharSet влияет на вызов функций.
Атрибут MarshalAs позволяет указать способ преобразования передаваемых параметров и возвращаемых значений при вызове неуправляемой функции. Также данный атрибут можно применять к полям класса или структуры.
Рассмотрим параметры атрибута:
UnmanagedType - позволяет указать тип неуправляемого объекта, к которому при необходимости управляемый объект будет преобразован. Данный параметр является перечислением и имеет множество значений которые понятны по названию. Рассмотрим наиболее важные:
Строковые преобразования - для преобразования строк есть 7 значений:
AnsiBStr - строка в кодировке ANSI, используется для COM взаимодействия;
BStr - тоже что и AnsiBStr, только строка в кодировке Unicode;
TBStr - кодировка строк зависит от ОС;
LPStr - используется для передачи строк в ANSI кодировке;
LPWStr - применяется для передачи строк в кодировке Unicode;
LPTStr - применяется для передачи строк в кодировке, которая используется ОС по умолчанию (тоже самое что CharSet.Auto);
ByValTStr - используется для передачи строк фиксированной длины (в символах). Если установленно данное значение, то в атрибуте MarshalAs обязательно должен быть использован параметр SizeConst отвечающий за длину строки. Кодировка строки зависит от параметра CharSet.
Примечание: Ввиду того что при машалинге CLR преобразует строки к C-стилю (null-terminated strings), т.е. строки с \0 (или \0\0 в Unicode) на конце, то действительная длина строки будет SizeConst - 1, т.к. последний символ будет заменён на \0.
Для преобразования целых чисел и чисел с плавающей точкой используются значения I1, I2, I4, I8, U1, U2, U4, U8 и R4, R6 соответсвенно.
Для передачи массивов в неуправляемый код используются значения UnmanagedType.LPArray для параметров функций и UnmanagedType.ByValArray для полей структур и классов. Эти параметры стоит использовать вместе с параметром SizeConst атрибута MarshalAs, чтобы устанавливать массивы фиксированной длины.
SizeConst - указывает количество элементов (не байт) в массиве или строке фиксированной длины. Используется совместно с параметрами UnmanagedType.ByValArray, UnmanagedType.ByValTStr. Значение данного параметра ограничено до 536870912 (512Мб).
ArraySubType - указывает тип элементов массива, который передаётся как UnmanagedType.ByValArray или UnmanagedType.LPArray.
Атрибут StructLayout позволяет указать способ расположения полей объекта в памяти. Данный атрибут применим как для структур, так и для классов.
Рассмотрим параметры данного атрибута:
LayoutKind - это первый, и обязательный параметр атрибута StructLayout. Указывает на то, как поля будут распологаться в памяти:
Sequential - поля структуры или класса будут располагаться в памяти в том порядке, в котором объявлены в коде, с учётом выравнивания (параметр Pack).
Explicit - поля будут располагаться в памяти в соответсвии с указанным смещением. Смещение указывается атрибутом FieldOffset для каждого объявленого поля в структуре или классе. Данный параметр позволяет создавать объединение из нескольких типов, таким образом, что они будут расположены по одному адресу в памяти (аналогично union в C/C++).
Auto - CLR сама выстраивает поля объекта в наиболее оптимальном по её мнению порядке.
CharSet - данный параметр отвечает за кодировку строк в строковых полях объекта. Pack - позволяет указать выравние полей в памяти, значение параметра должны быть числом, являющимся степенью двойки () и не превышать 128. Size - устанавливает размер занимаемой объектом памяти, может большим или равным физическому размеру объекта. Если значение меньше, то параметр будет проигнорирован.
Пример использования использования классов с атрибутом StructLayout, а также создание объединений (union):
[StructLayout( LayoutKind.Sequential, Pack =1)]struct BLOCK
{public IntPtr hMem;/* Если здесь заменить эти 3 параметра на массив с * атрибутом MarshalAs, то во время выполнения будет * выдано исключение TypeLoadException, это связано * с тем что при создании объединения CLR не может * создать требуемую структуру, возможно это баг CLR */#if !ERRORpublicuint dwReserved1;publicuint dwReserved2;publicuint dwReserved3;#else[MarshalAs( UnmanagedType.ByValArray, SizeConst =3)]publicuint[] dwReserved;#endif}[StructLayout( LayoutKind.Sequential, Pack =1)]struct REGION
{publicuint dwCommittedSize;publicuint dwUnCommittedSize;public IntPtr lpFirstBlock;public IntPtr lpLastBlock;}[StructLayout( LayoutKind.Explicit)]struct UNION
{// Создаём объединение из 2-х структур[FieldOffset(0)]public BLOCK Block;[FieldOffset(0)]public REGION Region;}[StructLayout( LayoutKind.Sequential, Pack =1)]class PROCESS_HEAP_ENTRY
{public IntPtr lpData;publicuint cbData;publicbyte cbOverhead;publicbyte iRegionIndex;publicushort wFlags;public UNION u;}[DllImport("Kernel32.dll", SetLastError =true)]staticexternint GetProcessHeaps (int NumberOfHeaps,
IntPtr[] ProcessHeaps
);[DllImport("Kernel32.dll", SetLastError =true)]staticexternbool HeapWalk (
IntPtr hHeap,
// т.к. PROCESS_HEAP_ENTRY ссылочный тип,// то нам не нужны модифифкаторы ref/out// У нас сразу получается указатель на// PROCESS_HEAP_ENTRY
PROCESS_HEAP_ENTRY lpEntry
);staticvoid Main (string[] args ){var phe =new PROCESS_HEAP_ENTRY();var pBuffArr =new IntPtr[// Получаем кол-во куч (heap) у процесса// подробнее см. описание на MSDN
GetProcessHeaps(0, null)];if( pBuffArr.Length==0)return;
GetProcessHeaps( pBuffArr.Length, pBuffArr );for(int i =0; i < pBuffArr.Length;++i ){if( HeapWalk( pBuffArr[i], phe )){// Остановимся здесь для просмотра значений
Debugger.Break();}}
Console.ReadLine();}
Данные атрибуты позволяют указать CLR когда и каким образом нужно выполнять преобразование над параметрами (marshaling). Применяются эти атрибуты исключительно для параметров методов и учитываются CLR только при COM взаимодействии или использовании неуправляемого кода. Используются преимущественно с ссылочными типами, т.к. большинство значимых типов CLR может без труда преобразовать самостоятельно, благодаря использованию ключевых слов ref/out при передаче значений по ссылке. С ссылочными типами дела обстоят немного иначе, CLR не может предсказать какие манипуляции с данными будут производиться - нужно ли копировать значения из управляемой памяти в неуправляемую перед вызовом неуправляемого метода (за это отвечает атрибут In) и нужно ли выполнять обратную операцию (копирование из неуправляемой памяти в управляемую) после того как неуправляемый метод вернул управление (за это отвечает атрибут Out).
Атрибут In - указывает CLR что преобразование над данными должно выполняться до вызова неуправляемого метода.
Атрибут Out - указывает CLR что при возвращении из неуправляемого метода нужно выполнить преобразование над данными, чтобы они приняли вид который был до передачи их в метод.
Данные атрибуты можно сочетать, тогда преобразование будет выполнятся до вызова метода и после.
Вот 2 примера использования данных атрибутов и их влияние на передаваемые/возвращаемые данные:
Что такое небезопасный код в C#.NET? Это код в котором есть возможность использования указателей в явном виде (например как в C - void *, char *, ..., а не обёртку IntPtr). Unsafe код выполняется без контроля CLR, поэтому в некоторых моментах он может оказаться быстрее чем код с использованием IntPtr и методов из класса Marshal. Так же использование unsafe кода может облегчить использование PInvoke.
К сожалению в C# на использование указателей наложены некоторые ограничения:
Использование указателей возможно только со стандартными значимыми типами (sbyte, byte, short, ushort, int, uint, bool, long, ulong, char, float, double, decimal и перечисления - enum), с любой структурой в которой поля только неуправляемых типов, а также использовать указатель на указатель (int **);
Нельзя использовать указатели на ссылочные типы (исключение тип string [ - при использовании fixed] и массивы, элементами которого являются типы из п. 1), а также структуры в которых содержаться ссылочные типы, т.к. они могут быть подвержены сборке мусора.
Т.е. объявить, например, указатель на string не получится.
Для использования unsafe кода в C#, достаточно разрешить его в свойствах проекта: "Properties -> Build -> Allow unsafe code" если используется Visual Studio. Если компилировать код вручную, вызывая csc.exe, то тогда нужно добавить параметр /unsafe.
/* Если ключевое слово unsafe указано всему классу, то * успользовать указатели можно везде, без указания * ключевого слова usnsafe к каждому полю/методу */unsafeclass Test0
{int* buffer;publicvoid CallSomeFunc (){void* pMem = Marshal.AllocHGlobal(1024).ToPointer();
Marshal.FreeHGlobal((IntPtr)pMem );}}/* Если ключевое слово unsafe не указано всему классу, то * успользовать указатели можно только там, где указано * ключевое слово usnsafe */class Test1
{// Отдельное указание для поляunsafeint*buffer;// Указание unsafe для всего методаunsafepublicvoid CallSomeFunc0 (){void*pMem = Marshal.AllocHGlobal(1024).ToPointer();*(decimal*)pMem = 10.5M;
Marshal.FreeHGlobal((IntPtr)pMem );}// Указание unsafe только для некоторого// региона в методеpublicvoid CallSomeFunc1 (){unsafe{// Работа с указателями уже должна вестись только в// границах { }void*pMem = Marshal.AllocHGlobal(1024).ToPointer();// Заносим значение 10 (4 байта) по адресу, на который// указывает pMem*(int*)pMem =10;
Marshal.FreeHGlobal((IntPtr)pMem );}//// Раскомментируйте и будет ошибка//int *pointer = (int*)0;}}
Данное ключевое слово не добавляет каких либо конструкций в IL код при компиляции, оно просто позволяет разделять код на учатки: safe и unsafe.
fixed используется для фиксирования управляемого объекта в памяти (чтобы GC его не удалил и не переместил) и получения адреса, по которому объект расположен в памяти. Также fixed служит для объявления буферов фиксированного размера (аналогично MarshalAs(UnmanagedType.ByValArray, SizeConst = ...)).
Фиксировать нужно только те объекты, которые могут быть перемещены или удалены сборщиком мусора (например поля классов, массивы, строки, параметры методов с модификаторами ref/out), адрес остальных объектов можно получить без fixed.
Синтаксис оператора (для получения адреса):
C#
1
2
3
fixed( type* name = expression ){// TODO}
где type может быть неуправляемым типом или void, expression переменная которая будет зафиксирована и адрес которой будет присвоен переменной name.
// Заполнение строки определнным символом, без создания дополнительных строкusingSystem;namespace FixedTest
{class Program
{staticvoid Main (string[] args ){stringvalue="Hello\0 world!";
Console.WriteLine(value);value.Fill('1');
Console.WriteLine(value);
Console.ReadKey();}}staticclass Extension
{/// <summary>/// Заполняет всю строку определнным символом key./// </summary>unsafepublicstaticvoid
Fill (thisstring val, char key ){// Фиксируем строку в памяти и получаем// указатель на начало строкиfixed(char* buff = val ){// Полученный указатель buff нельзя// менять, поэтому присвоем его значение// другой переменнойchar* temp = buff;// Указатель на конец строкиchar* end = temp + val.Length;while( temp != end ){*temp = key;
temp++;}}}}}
usingSystem;namespace FixedTest
{class Program
{unsafestaticvoid
Main (string[] args ){intvalue=10;var buffer =newint[]{10, 34, 35, 1, 7532};// fixed не надоint* pValue =&value;*pValue =25;
Console.WriteLine("Value: "+value);// Получаем адрес 0-го элемента массиваfixed(int* pointer = buffer ){// Меняем 0-й элемент массива*pointer =-10;// Меняем 1-й элемент массива*( pointer +1)=-34;// или// pointer[0] = -10;// pointer[1] = -34;}// Получаем адрес 2го элемента массиваfixed(int* pointer =&buffer[2]){int size = buffer.Length-2;// Меняем остальные элементы массиваfor(int i =0; i < size;++i )*( pointer + i )*=(-1);// или// pointer[i] *= (-1);}
Console.Write("buffer array: ");for(int i =0; i < buffer.Length;++i )
Console.Write( buffer[i]+" ");
Console.ReadKey();}}}
Буферы фиксированного размера созданные с использованием ключевого слова fixed являются аналогами буферов созданных при помощи MarshalAs, но с важными ограничениями:
fixed буферы могут быть созданы только в структурах;
Элементами буфера могут быть только элементы типов: bool, byte, short, int, long, char, sbyte, ushort, uint, ulong, float или double.
Синтаксис оператора fixed в данном случае будет таким:
C#
1
2
3
4
<unsafe>struct Test
{<public>fixed type name[size];}
где type - это один из заданных типов, name - имя поля, а size - размер буфера (константа). Ключевые слова обрамлённые в <> являются не обязательными (в зависимости от ситуации).
Пример использования буфера фиксированного размера и получения адреса управляемого объекта в памяти.
Получение информации обо всех MIB-2 интерфейсах в системе:
stackalloc используется для выделения памяти в стеке, а не в куче, как это происходит при вызове, например, Marshal.AllocHGlobal. Основные отличия между выделением памяти в стеке и в куче в том, что выделение памяти в стеке и доступ к ней осуществляется быстрее чем к куче, также есть шанс того, что данные в стеке попадут в кэш процессора, тогда доступ к ним (данным) будет максимально быстрым, в результате можно добиться некоторого ускорения алгоритма.
Не забываем что CLR довольно хорошо справляется с распределением памяти, в некоторых случаях использование stackalloc может не дать того прироста в скорости, который ожидался, т.к. CLR при возможности выделяет памяти немного больше чем требуется, в результате при создании какого либо объекта память не будет выделяться снова, а будет просто возвращен адрес в уже зарезервированном фрагменте.
Также к плюсам выделения памяти в стеке относится то, что память будет освобождена сразу после завершения метода.
Минус стековой памяти в том, что её объем относительно небольшой, всего 1 Мб (такое значение задается по умолчанию линкером), и при превышении установленного значения CLR бросит исключение StackOverflowException, после этого приложение будет экстренно завершено.
Каждый поток при создании получает своё стековое пространство. Размер этого пространства можно указывать при создании потока, начиная с .NET 4.0 на изменение размера стека наложено ограничение, подробнее о нём написано на MSDN. Изменить стандартный размер выделяемого пространства можно в PE заголовке файла (структура NT_OPTIONAL_HEADER поле SizeOfStackReserve).
Использовать stackalloc можно только во время инициализации объекта. Синтаксис использования stackalloc такой:
C#
1
type* name =stackalloc type[count];
где type - любой значимый тип (value type) данных на который можно представить ввиде указателя, count - размер выделяемой памяти, который расчитывается по формуле count * sizeof(type) (sizeof - размер объекта в неуправляемой памяти).
Пример:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsafestaticvoid
Main (string[] args ){uint* stack =stackallocuint[10];for(int i =-10; i <0;++i ){//*(stack + i) = (uint)i;//Console.WriteLine( *(stack + i) );
stack[i]=(uint)i;
Console.WriteLine( stack[i]);}
Console.ReadKey();}