Работа с памятью и указателями в VB.NET
Теоретически программист, пишущий на Visual Basic может обходиться
только средствами языка. Практически, рано или поздно ему придется обратиться к
API. А поскольку функции API
написаны, как правило, на C, то указатели встречаются
в них на каждом шагу. Большую часть проблем решает правильное объявление нужной
функции, но встречаются ситуации, когда правильное объявление функции только
половина дела. Вторая половина – получение и использование указателя. До
появления .NET проблема решалась при помощи
недокументированной функции VarPtr, ее близнецов
ObjPtr и StrPtr,
а также нескольких редко используемых родственников. Сказка кончилась… VarPtr отсутствует в runtime-библиотеке
VB.NET… Но на
наше проггерское счастье в этом языке появились новшества, которые значительно
снижают необходимость обращения к API-функциям.
Однако обращения к API все равно приходится
делать. И от работы с памятью и указателями нам никуда не уйти… Но не все так
плохо, как кажется, в VB.NET
появились вполне легальные средства работы с памятью и указателями. А если нам
их не хватит, то можно протоптать свои, партизанские строки кода. Начнем,
естественно, с легальных методов.
Начнем с того, что все встроенные в VB.NET (вернее в CLR – Common Language Runtime) типы являются
производными от корневого класса Object. Следует
различать так называемые структурные (value)
типы и ссылочные (reference).
К структурным типам относятся встроенные в CLR типы, такие как Byte
или Integer (чтоб не
путаться, к встроенным типам будем относить Byte,
Char, Short, Integer, Long, Single, Double, Date и Decimal. Эти типы я
также буду называть простыми типами). А также все пользовательские типы,
объявляемые при помощи ключевого слова Structure.
Все структурные типы являются производными не от Object,
а от класса ValueType, который переопределяет
виртуальные методы класса Object более
соответствующим для структурных типов образом. Структурные типы располагаются
не в куче, а в стеке или в сегменте данных приложения, что увеличивает скорость
работы с ними. Поэтому они наиболее удачно подходят для работы с простыми
объектами, такими как числа (не забывайте, что числовые типы в CLR являются на самом деле структурами) или, например,
координаты точек. В случае необходимости использования ссылочного типа в
качестве объекта происходит распределение памяти в куче для временного
ссылочного объекта и копирование туда структурного типа. Данный процесс
называется упаковка (boxing). Обратный процесс –
распаковка (unboxing). Благодаря упаковке и
распаковке любой тип может трактоваться как объект. Особенностью структурных
типов также является отсутствие возможности переопределения конструктора по
умолчанию. Встроенный конструктор по умолчанию инициализирует все ее поля
значениями Nothing (для полей простых типов это
будут нули). При присваивании одной структуры другой VB.NET копирует данные структур на уровне полей.
Ссылочные типы – это собственно классы, объявляемые при
помощи ключевого слова Class. Ссылочные объекты
всегда размещаются в куче. Присваивание одного объекта другому приводит к
получению новой ссылки на уже существующий объект, а не к копированию данных.
Классы, в отличии от структур, могут наследоваться от других классов, и другие
классы, в свою очередь, могут наследовать от них.
Ну а теперь к работе с памятью…
Код VB.NET
является управляемым (managed) кодом. Это
означает, что все создаваемые в коде объекты располагаются в памяти при помощи
специальных средств CLR. API-функции
в свою очередь работают с обычной памятью и их код является неуправляемым (unmanaged). Каким же образом из VB.NET достучаться до обычной неуправляемой памяти? Не так
уж это и сложно, на это существуют атрибуты и некоторые полезные классы.
Structure в VB.NET это не Type в VB. Массивы и
строки фиксированной длины в них теперь не поддерживаются, поля могут
располагаться в памяти в произвольном порядке. Вернее сказать никто и ничто не
дает нам гарантии, что поля структуры располагаются в памяти последовательно
друг за другом. Спрашивается, а как же передать все это в какую-нибудь функцию API, требующую структуру в качестве параметра?
Нет ничего невозможного. На этот случай имеется атрибут StructLayout. Этот атрибут как раз и позволяет нам
управлять расположением полей в структуре. Например:
<StructLayout(LayoutKind.Sequential,
Pack:=1)> Structure Point
Public X As Integer
Public Y As Integer
End Structure
В данном объявлении структура Point
объявляется таким образом, что ее поля следуют друг за другом и выравниваются
по краям байтов. Можно также указать автоматическое расположение полей
структуры в памяти (LayoutKind.Auto) или задать явное смещение каждого поля структуры
относительно ее начала (LayoutKind.Explicit). При этом каждое поле необходимо объявлять с
атрибутом FieldOffset. При помощи LayoutKind.Explicit можно,
например, создавать объединения (Union)
реализация которых ранее была невозможно в VB:
<StructLayout(LayoutKind.Explicit)>
Structure Point
<FieldOffset(0)>
Public X As Integer
<FieldOffset(0)>
Public XY As Long
<FieldOffset(4)>
Public Y As Integer
End Structure
Параметр Pack, при заданном LayoutKind.Sequential,
позволяет задать границы выравнивания полей в структуре. Рекомендуемые
значения: 0, 1, 2, 4, 8, 16, 32, 64 или 128. Если значение равно 0, то
выравнивание производится по умолчанию для текущей платформы. Интересно, что
если границы выравнивания не задать вовсе, то выравнивание будет производится
по границе 8 байтов!
Параметр Size позволяет
задать общий размер структуры. Его значение должно быть равным или большим
суммы всех членов структуры.
Ну и, наконец, параметр CharSet
указывает, как передаются строковые поля в структуре. При установке его в
значение CharSet.Unicode
строки будут интерпретироваться как Unicode-строки,
CharSet.Ansi –
указывает на преобразование строк в ANSI-строки.
А, установив значение в CharSet.Auto мы получим автоматическое преобразование строк в
нужный набор символов в зависимости от платформы (для Windows NT и ее потомков –
преобразование производится в Unicode-строки, а
для Windows 98 и ее потомков – в ANSI-строки). Работать с этим параметром просто. Если вы
предполагает работу c «A»
версией API-функции и, соответственно, с «A» версией структуры, то необходимо устанавливать
значение CharSet.ANSI,
в случае работы с «W» версией, устанавливается
атрибут CharSet.Unicode.
Можно конечно не устанавливать это поле, надеясь на правильный автоматический
выбор набора символов, но лучше перестраховаться и установить набор символов
вручную.
Интересно, что атрибут StructLayout
можно применять и к классам, а затем использовать их в функциях API. Это дает возможность использования конструктора по
умолчанию для инициализации полей, проверку на Nothing
и возможности наследования (ну это то вряд ли пригодится для API-функций).
А теперь поговорим о передаче строк и массивов в структурах.
Это делается при помощи атрибута MarshalAs. Это
очень мощное средство позволяющее отобразить нужным образом любой вариант поля
структуры, параметра или возвращаемого значения функции. Рассмотрим следующее
объявление структуры в C++:
typedef
struct _usertype {
BYTE b[4];
wchar_t c[16];
LPWSTR s;
} usertype;
Определить такую структуру в VB.NET не так то просто. Массивов и строк переменной длины
теперь нет, да и с обычными строками не все просто. Помогают естественно
атрибуты. Итак, правильное определение:
<StructLayout(LayoutKind.Sequential,
Pack:=1, CharSet:=CharSet.Unicode)> Structure UserType
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=4)>
Public b() As Byte
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=16)>
Public c As String
<MarshalAs(UnmanagedType.LPWStr)> Public s As String
End Structure
Рассмотрим особенности объявлений полей структуры.
Первое поле объявлено с атрибутом <MarshalAs(UnmanagedType.ByValArray, SizeConst:=4)>,
что обозначает передачу массива из 4-х элементов. Второе поле с атрибутом <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=16)> передается как строка из 16
символов. Тип строки определяется в атрибуте StructLayout параметром CharSet. В
данном случае этот параметр установлен в CharSet.Unicode и строка будет
интерпретироваться как Unicode-строка. Возможен
эквивалентный вариант объявления этого поля как <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
Public c()
As Char. Но в этом случае вместо строки
придется передавать массив символов. Третье поле благодаря атрибуту <MarshalAs(UnmanagedType.LPWStr)>
передается как Unicode-строка. В данном случае
этот атрибут является лишним, поскольку тип строки уже задан в атрибуте
структуры, и атрибут для поля можно спокойно опустить.
Как интерпретируются параметры функций или поля структур при
использовании других членов перечисления UnmanagedType
можно увидеть в следующей таблице (не включены члены, необходимые для работы с COM).
Имя члена
|
Описание
|
AnsiBStr
|
Передает параметр или поле как ANSI-строку
с префиксом, содержащим ее длину.
|
AsAny
|
Передает динамический параметр, определяемый типом объекта
во время выполнения программы.
|
Bool
|
Передается как 4-х байтное булево значение (true <>0, false =
0)
|
BStr
|
Передает параметр или поле как Unicode-строку
с префиксом, содержащим ее длину.
|
ByValArray
|
Интерпретирует поле как массив фиксированной длины. Длина
массива должна задаваться параметром SizeConst.
Данный член может быть применим только к полю структуры содержащему массив.
|
FunctionPtr
|
Передает параметр как указатель на функцию. Может
использоваться с полем имеющим тип Delegate
или производным от Delegate.
|
I1
|
1-байтовое целое число со знаком.
|
I2
|
2-байтовое целое число со знаком.
|
I4
|
4-байтовое целое число со знаком.
|
I8
|
8-байтовое целое число со знаком.
|
LPArray
|
Массив в стиле C. При
передаче из управляемого кода в неуправляемый длина массива определяется
длиной управляемого массива. При передаче из неуправляемого кода в
управляемый длина определяется параметром SizeConst.
Предполагается, что массив содержит тип отличный от строк.
|
LPStr
|
Передает ANSI-строку без
префиксов.
|
LPStruct
|
Передает как указатель на структуру в стиле C. Применяется для передачи указателя на структурный
или ссылочный тип.
|
LPTStr
|
Передает строку в зависимости от платформы. Для Windows NT
передается Unicode-строка, для Windows 98 – ANSI-строка.
|
LPWStr
|
Передает Unicode-строку без
префиксов.
|
R4
|
4-байтовое число с плавающей запятой.
|
R8
|
8-байтовое число с плавающей запятой.
|
Struct
|
Передает как структуру в стиле C.
Применяется для передачи структурного или ссылочного типа.
|
SysInt
|
Передает поле или аргумент как системное целое число. Для
32-разрядной Windows – 4-байтовое целое число
со знаком. Для 64-разрядной Windows –
8-байтовое целое число со знаком.
|
SysUInt
|
Системное целое число без знака.
|
TBStr
|
Передает строку с префиксом в зависимости от платформы.
Для Windows NT передается Unicode-строка,
для Windows 98 – ANSI-строка.
|
U1
|
1-байтовое целое число без знака.
|
U2
|
2-байтовое целое число без знака.
|
U4
|
4-байтовое целое число без знака.
|
U8
|
8-байтовое целое число без знака.
|
VBByRefStr
|
Позволяет Visual Basic изменять строку
из неуправляемого кода и отображать результаты в управляемый код.
|
Таким образом, зная, как интерпретируются параметры функций
и поля структур (классов) при использовании атрибута MarshalAs легко реализовать правильные объявления функции и
определения структур. Главное правильно понимать смысл типов С/С++ и
внимательно изучать определения функций и структур в заголовочных файлах.
В языке появился забавный тип IntPtr,
представляющий собой указатель или дескриптор (он же описатель). К сожалению,
этот тип не содержит методов получения указателя на переменную и методов
получения значений по указателю. Также отсутствует возможность использования
арифметики указателей. Поэтому данный тип мало, чем отличается от обыкновенного
Integer. Тем более что Integer
является полноценным типом, для которого определены все арифметические
действия, а для IntPtr ничего этого нет. То есть
арифметику указателей к нему применить невозможно. Тем не менее, методы VB.NET для работы с
указателями почти всегда используют тип IntPtr.
Будем придерживаться этого синтаксиса и мы. То есть везде, где используются
указатели, будет использоваться тип IntPtr. Тем
более что преобразование из IntPtr в Integer и наоборот выполняется весьма просто:
Dim i As
Integer = p.ToInt32 ‘ преобразование из IntPtr в Integer
Dim p As
IntPtr = New IntPtr(i) ‘ преобразование из Integer в IntPtr
Ничего загадочного в самом типе IntPtr
нет. Он реализован как структура, единственный закрытый член которой содержит
значение типа Integer (для 64-разрядных
операционных систем это значение имеет тип Long).
Вот этот член и является указателем.
Класс Marshal предоставляет
набор разделяемых методов применяемых для распределения неуправляемой памяти,
копирования блоков неуправляемой памяти и преобразования типов из управляемых в
управляемые. Содержащиеся в этом же классе метод, предназначенные для работы с COM мы рассматривать не будем.
Методами, предоставляемыми данным классом можно легко
реализовать подавляющее большинство задач, при решении которых необходима
работа с указателями и блоками неуправляемой памяти.
Наконец то появились две очень полезные функции SizeOf и OffsetOf.
SizeOf позволяет определить
размер класса, структуры и объекта. Функции Len
и LenB с давних пор существующие в VB не всегда давали корректный результат из-за нюансов
выравнивания, не работали с объектами класса (вернее давали размер ссылки – 4
байта) и уж тем более не могли определить размер типа.
Overloads
Public Shared Function SizeOf(ByVal t As Type) As Integer
Overloads Public Shared Function SizeOf(ByVal obj As Object) As
Integer
Первая версия позволяет определить размер типа, например:
Dim c As
Integer = Marshal.SizeOf(GetType(Integer))
или так
Dim c As
Integer = Marshal.SizeOf(GetType(UserType))
А вторая версия работает уже с объектами:
Dim i As
Integer
Dim c As Integer = Marshal.SizeOf(i)
или
Dim ut As
UserType
Dim c As Integer = Marshal.SizeOf(ut)
OffsetOf позволяет получить
смещение от начала структуры или класса заданного члена. Синтаксис прост и
понятен:
Public
Shared Function OffsetOf(ByVal t As Type, fieldName As String) As IntPtr
Единственная странность – результат типа IntPtr. Прям таки мазохизм какой то.
Стоит напомнить один нюанс – обе эти функции работают только
с простыми типами и структурами и классами, определенными с атрибутом StructLayout (есть еще пара нюансов, о них позже).
Для этого существуют три функции
Overloads
Public Shared Function AllocHGlobal(ByVal cb As Integer) As IntPtr
Overloads Public Shared Function AllocHGlobal(ByVal cb As
IntPtr) As IntPtr
Public
Shared Function ReAllocHGlobal(ByVal pv As IntPtr, cb As IntPtr) As IntPtr
Public
Shared Sub FreeHGlobal(ByVal hglobals As IntPtr)
Первый метод, AllocHGlobal
распределяет блок неуправляемой памяти заданного размера и возвращает указатель
на него. Внутренне этот метод реализован как вызов функции API GlobalAlloc. Поэтому вместо AllocHGlobal можно непосредственно вызывать функцию GlobalAlloc. Вторая версия этого
метода написана явно для мазохистов, предпочитающих задавать размер блока
памяти при помощи IntPtr вместо Integer.
Второй метод, ReAllocHGlobal
перераспределяет ранее распределенный блок памяти и возвращает указатель на
блок памяти нового размера. Как и в предыдущей функции здесь производится вызов
API функции GlobalReAlloc.
Размер блока памяти здесь задан только при помощи IntPtr,
так что от мазохизма вы не уйдете.
Ну и, наконец, FreeHGlobal
освобождает блок памяти, распределенный нами ранее. Внутри этого метода то же
самое – вызов API функции, на этот раз GlobalFree.
Итак, у программиста есть выбор – вызов непосредственно API функций или AllocHGlobal,
ReAllocHGlobal и FreeHGlobal.
При вызове API функций можно задавать
дополнительные параметры, управляющие распределением памяти, в частности можно
получить описатель блока памяти вместо указателя, но при этом вы сами должны
обрабатывать все возможные ошибки.
Алгоритм работы с блоком памяти следующий:
Dim cb As Integer
= 1024 ‘ задаем размер блока памяти
Dim p As IntPtr = Marshal.AllocHGlobal(cb) ‘
получаем указатель на блок памяти
‘
работаем с блоком памяти
Dim newcb As IntPtr = New IntPtr(2048) ‘ задаем новый размер блока
памяти
p = Marshal.ReAllocHGlobal(p, newcb) ‘ получаем указатель на новый
блок памяти
‘
работаем с блоком памяти нового размера
Marshal.FreeHGlobal(p) ‘
освобождаем память
p = IntPtr.Zero ‘
обнуляем указатель
Появились теперь и функции, которые могут копировать как
массив в блок неуправляемой памяти, так и блок неуправляемой памяти в массив.
Но только для семи типов: Byte, Char, Short,
Integer, Long, Single и Double. Рассмотрим эти функции на примере типа Integer.
Overloads
Shared Sub Copy(ByVal source() As Integer, ByVal startIndex As Integer, ByVal
dest As IntPtr, ByVal length As Integer)Overloads
Shared Sub Copy(ByVal source As IntPtr, ByVal startIndex As Integer, ByVal
dest() As Integer, ByVal length As Integer)
Итак, первая функция копирует length
элементов массива source, начиная с элемента startIndex в блок неуправляемой памяти на который
указывает указатель dest.
Вторая функция из блока неуправляемой памяти, на который
указывает указатель source, копирует
данные в length элементов массива dest, начиная с элемента startIndex.
Все предельно ясно и понятно. Если у вас имеется массив и
его данные необходимо неким образом обработать в памяти, то при помощи AllocHGlobal создаем блок памяти, необходимого размера,
копируем туда имеющийся массив, что-то делаем с его байтами и при необходимости
копируем их обратно. Естественно не забываем освободить память функцией FreeHGlobal. Аналогичным образом, полученный допустим из API функции, блок памяти можно скопировать в массив
подходящей длины.
Единственный недостаток – а что делать с другими типами? В
том числе с пользовательскими. А об этом речь пойдет позже!
Есть еще одна функция позволяющая получить указатель на
элемент массива (самый настоящий указатель!).
Public
Shared Function UnsafeAddrOfPinnedArrayElement(ByVal arr As Array, ByVal index As
Integer) As IntPtr
Функция возвращает указатель на заданный элемент массива.
Обязательное требование к массиву – это должен быть массив либо простых типов,
либо структур и классов, определенных с атрибутом StructLayout.
Второе требование – массив должен быть зафиксирован в памяти при помощи класса GCHandle (о нем позже). Если массив не зафиксировать, то
в один хороший момент CLR переместит массив в
другое место в самый неподходящий момент и наш указатель перестанет указывать
на массив, а мы про это даже и не узнаем. Правда, вероятность этого события
довольно таки мала. Но более вероятен тот факт, что CLR
распределит память для большого массива в нескольких блоках памяти и уж
практически наверняка массив переедет по другому адресу при переопределении его
размера в большую сторону.
Две очень полезные функции позволяют перемещать данные из
структуры (класса) в неуправляемую память и наоборот. При использовании
атрибута StructLayout данные методы позволяют
копировать данные для весьма сложных структур или классов.
Public Shared
Sub StructureToPtr(ByVal obj As Object,
ByVal ptr As IntPtr,
ByVal fDeleteOld As Boolean)
Данная функция помещает данные объекта obj
в блок неуправляемой памяти, на который указывает ptr.
Параметр fDeleteOld позволяет корректно удалить
данные, которые могут содержаться в блоке памяти. При установленном в True параметре, перед копированием в блок памяти
содержимого структуры блок памяти полностью очищается с учетом всех возможных
ссылок.
При использовании fDeleteOld происходит следующее. Представим, что имеется неуправляемый
блок памяти указываемый ptr. Расположение этого
блока зависит от соответствующей управляемой структуры (класса) structure. StructureToPtr
передает значения полей структуры в блок памяти. Представим, что указатель ptr содержит ссылку на строку, содержащую “abcd”. Представим, что соответствующее поле
структуры содержит “xyz”. Если не
указать True в параметре fDeleteOld,
то StructureToPtr распределит новый блок памяти
для “xyz” и присоединит ссылку на
него к ptr. Это приведет к тому, что старый блок
памяти содержащий “abcd” окажется
брошенным на произвол судьбы и не будет освобожден обратно в неуправляемую
кучу. Это приведет к появлению зависшего буфера, представляющего утечку памяти
в вашем коде. Если же параметр fDeleteOld будет
установлен в True, то блок памяти, содержащий “abcd” будет освобожден перед распределением нового
блока памяти для “xyz”.
Не следует забывать следующего: процедура StructureToPtr не создает
указатель на структуру или класс, она просто копирует содержимое класса в заранее
распределенный блок неуправляемой памяти!
Overloads
Public Shared Sub PtrToStructure(ByVal ptr As IntPtr, ByVal obj As Object)
Overloads Public Shared Function PtrToStructure(ByVal ptr As IntPtr,
ByVal objType As Type) As Object
Первый метод просто копирует данные из блока неуправляемой
памяти ptr в объект obj.
Объектом может быть только ссылочный тип! Структурный тип таким образом
получить невозможно.
Второй метод создает объект заданного типа objType, помещает в него данные из блока неуправляемой
памяти и возвращает созданный объект. Для успеха этой функции, структура или
класс должны иметь конструктор по умолчанию. Здесь можно создать как ссылочный,
так и структурный объект. Полученный объект будет упакованным и его необходимо
корректно преобразовать в нужный тип при помощи функции CType().
И, наконец, функция DestroyStructure
корректно освобождает все подструктуры, на которые ссылается заданный блок
памяти. Поэтому для структур (классов) содержащих ссылки на другие объекты
вызов DestroyStructure обязателен во избежание
утечек памяти.
Public Shared
Sub DestroyStructure(ByVal ptr As IntPtr, structType As Type)
Понятно, что ptr это
указатель на блок неуправляемой памяти, а structType
необходим для предоставления информации о расположении структуры в памяти.
Вызов DestroyStructure не
освобождает вас от последующего вызова FreeHGlobal
для освобождения блока памяти указываемого указателем ptr.
Рассмотрим механизм работы с памятью на примере следующей
структуры (структура откровенно надуманная):
<StructLayout(LayoutKind.Sequential)>
Structure StringWrapper
Public m_String As String
End Structure
Пишем следующий код:
Dim sw As
StringWrapper
sw.m_String = “Test String”
Dim cb As Integer = Marshal.SizeOf(sw) ‘ определяем размер объекта
Dim ptr As IntPtr = Marshal.AllocHGlobal(cb) ‘
распределяем память под объект
Marshal.StructureToPtr(sw, ptr, True) ‘ копируем объект в блок памяти
…
Dim sw2 As StringWrapper = CType(Marshal.PtrToStructure(ptr,
_
GetType(StringWrapper)), StringWrapper) ‘ создаем
копию исходного объекта
…
Marshal.DestroyStructure(ptr, GetType(StringWrapper)) '
очищаем память
Marshal.FreeHGlobal(ptr)
‘ освобождаем блок памяти
Остановимся на нескольких моментах этого кода:
Dim cb As Integer
= Marshal.SizeOf(sw) – размер структуры будет равен 4 байтам, поскольку
содержится в ней только указатель на строку, строка же находится в совершенно
другом месте.
Dim ptr As IntPtr
= Marshal.AllocHGlobal(cb) – распределяется блок памяти размером 4 байта под
структуру, про распределение памяти для строки речь не идет. Память для строки
уже распределена, и строка уже существует в памяти.
Marshal.DestroyStructure(ptr, GetType(StringWrapper))
– очищаем память, занятую структурой, в том числе и ссылки на строку. Если
после этой команды попробовать еще раз создать копию исходного объекта, то мы
получим структуру с нулевыми полями (Nothing для
строки).
Marshal.FreeHGlobal(ptr) – освобождаем распределенную ранее память. После этого
указатель ptr становится недействительным.
Формат строки в VB.NET изменился. Теперь строка является классом, содержащим
ссылку на хранящуюся где-то в куче последовательность символов. Устроим
небольшой экскурс в форматы строк в VB и в API.
В VB, до версии 4.0 строки хранились в
формате HLSTR (High Level STRing). Переменная типа String
представляла собой указатель на описатель строки, состоящий из 2-байтового
поля, содержащего длину строки и еще одного указателя на массив символов в ANSI-формате (один байт на один символ). Преобразование
этого нагромождения в строку, понятную Win 32 –
настоящий кошмар.
Начиная с VB 4.0 формат
строки в Visual Basic
изменился на BSTR.
BSTR – это формат официально определенный в
спецификации OLE 2.0 и являющийся частью
спецификации Microsoft ActiveX. Теперь переменная типа String
является реальным указателем на массив Unicode-символов.
Этому массиву предшествует 4-байтовое поле, содержащее длину массива в байтах,
а сам массив завершается 2-байтовым нуль-терминатором (в длине массива
2-байтовый нуль-терминатор не учитываеся). Поскольку длина символьного массива
задается специально, он может содержать один или несколько внедренных
нуль-терминаторов, что принципиально отличает этот формат от формата строки в Win 32.
Win 32 использует два формата
строки LPSTR и LPWSTR.
LPSTR строка определяется как указатель на массив ANSI-символов завершаемый нуль-терминатором. Длина строки
определяется положением нуль-терминатора, который определяет ее конец. Так как
других способов определения длины LPSTR строки
не существует, то LPSTR не может содержать
внедренные нуль-терминаторы.
LPWSTR строка аналогична LPSTR за исключением того, что является указателем на
массив Unicode-символов и завершается 2-байтовым
нуль-терминатором.
Рассмотрим теперь работу недокументированных функций VarPtr и StrPtr. Первая из
них, будучи примененной, к строковой переменной возвратит ее адрес (собственно
это она делает для любой переменной), содержащий 4-байтовый указатель на
содержимое строки. А StrPtr непосредственно
возвращает указатель на содержимое строки. Если обратится к адресу меньшему на
4 байта, то мы получим длину строки в байтах.
А теперь приступим к долгой и страшной истории под названием «передача
строки из VB в Win
32 и возвращение ее назад». Как известно строки в Windows
9x являются ANSI-строками,
а в Windows NT (и в ее потомках) Unicode-строками.
Внутренний формат VB для строк также Unicode. Тем не менее, в целях совместимости с Windows 95 VB всегда (даже в Windows NT) создает временную ABSTR строку (формат ABSTR
аналогичен BSTR, но содержит ANSI-символы), преобразовывает массив Unicode-символов BSTR-строки
в массив ANSI-символов и помещает
преобразованные символы в массив символов ABSTR-строки.
После этого полученная ABSTR-строка отправляется
в ANSI-вариант функции Win
32. Если все это происходит в системе с ANSI-представлением
строк (Windows 9x)
то приключения строки на пути туда заканчиваются. Если дело происходит в Windows NT,
то еще не вечер. ANSI-версия функции
преобразовывает полученную ANSI-строку в Unicode-строку и наконец-то передает ее в Unicode-версию функции, где эта строка благополучно
используется по назначению… Уфффф.
Обратная дорога также длинна и заковыриста. Unicode-версия
функции создает строку из Unicode-символов,
передает ее в ANSI-версию, которая преобразовывает
ее в строку из ANSI-символов и записывает по
переданному нами адресу содержимого временной ABSTR-строки.
VB копирует содержимое ABSTR-строки
обратно в исходную строку, преобразуя символы в Unicode.
Наконец-то результат получен!
Поскольку все эти преобразования из Unicode-формата
в ANSI-формат VB
производил за нашими спинами вне зависимости от того, какую версию функции (ANSI или Unicode), мы
вызываем и на какой платформе работаем, то всегда приходилось вызывать именно ANSI-версию, и только ее. Unicode-версию
можно было вызвать только передавая в функцию массив символов необходимой длины
вместо строки. Или преобразовывая строку функцией StrConv
перед отправкой в функцию и еще раз после получения результата. Что добавляло
еще два совершенно лишних преобразования.
Теперь все упрощается. При объявлении функции мы можем в
явном виде указать версию функции и указать способ преобразования строки при
помощи уже описанных выше атрибутов UnmanagedType.LPStr или UnmanagedType.LPWStr. И в нужную функцию будут отправлена строка
нужного формата, она же будет возвращена обратно. И все!!! Никаких
преобразований!!!
Имеется, однако, одна странность. Представим себе, что у нас
есть функция, которая принимает в качестве параметра структуру, одним из полей
которой является строка. Функция делает свою работу и записывает результат
своей деятельности в строковое поле в виде нескольких подстрок, разделенных
нуль-терминаторами. Скажете, не бывает таких функций? Бывают! Одна из них – GetOpenFileName с флагами OFN_ALLOWMULTISELECT и OFN_EXPLORER. И возвращаются, таким образом, каталог и выбранные файлы.
Так вот. Если строковое поле объявить в виде типа String
с атрибутом UnmanagedType.LPWStr
(или с UnmanagedType.LPStr),
то на выходе из функции мы получим поле со строкой, обрезанной до первого
нуль-терминатора! Вот такой вот сюрприз… Правильное объявление в данном случае
– массив символов нужной длины. В нем все будет возвращено полностью.
С передачей строк в API-функции
разобрались. Теперь поговорим об указателях. StrPtr
и VarPtr теперь нет, им просто не на что
указывать… Получить указатель на строку мы не сможем. Зато можем скопировать ее
содержимое в блок неуправляемой памяти. Имеются функции преобразования строки VB.NET в нужный формат.
Итак, первая из них:
Public
Shared Function StringToBSTR(ByVal s As String)As IntPtr
Эта функция распределяет блок неуправляемой памяти из кучи
необходимый для строки в формате BSTR и копирует
в нее содержимое предоставленной строки. Если в функцию будет передан Nothing, то будет возвращен нулевой указатель, иначе
возвращается указатель на BSTR-строку. Этот
указатель, после использования необходимо освободить вызовом функции FreeBSTR. Если мы не работаем с OLE,
то вряд ли нам понадобиться данная функция, а вот три следующих функции имеет
несомненную полезность:
Public
Shared Function StringToHGlobalAnsi(ByVal s As String) As IntPtr
Public Shared Function StringToHGlobalUni(ByVal s As String) As
IntPtr
Public Shared Function StringToHGlobalAuto(ByVal s As String) As
IntPtr
Первая функция распределяет блок памяти в неуправляемой куче
и копирует туда содержимое строки в ANSI-формате.
Вторая делает то же самое, но в формате Unicode.
А третья выбирает формат в зависимости от платформы. После завершения
использования, возвращенный из этой функции указатель необходимо освободить
функцией FreeHGlobal.
Естественно, что есть и обратные функции, то есть из
указателя получаем строку.
Overloads
Public Shared Function PtrToStringAnsi(ByVal ptr As IntPtr) As String
Overloads Public Shared Function PtrToStringAnsi(ByVal ptr As IntPtr,
ByVal len As Integer) As String
Overloads
Public Shared Function PtrToStringUni(ByVal ptr As IntPtr) As String
Overloads Public Shared Function PtrToStringUni(ByVal ptr As IntPtr,
ByVal len As Integer) As String
Overloads
Public Shared Function PtrToStringAuto(ByVal ptr As IntPtr) As String
Overloads Public Shared Function PtrToStringAuto(ByVal ptr As IntPtr,
ByVal len As Integer) As String
Первые две функции создают строку из указателя на ANSI строку, две следующих из указателя на Unicode строку, ну и, наконец, две последних из указателя
на строку, кодировка которой определяется операционной системой.
Первые версии функций копируют строку целиком, то есть до
ближайшего нуль-терминатора. А вторые копируют заданное количество символов.
Таким образом, мы имеем полный набор функций необходимых для
работы с памятью и строками.
Для пяти простых типов (Byte,
Short, Integer, Long и IntPtr)
предусмотрены функции чтения и записи в неуправляемую память. Рассмотрим
функции записи и чтения на примере типа Integer
(для других типов все реализовано аналогично).
Overloads
Public Shared Sub WriteInt32(ByVal ptr As IntPtr, ByVal val As Integer)
Overloads Public Shared Sub WriteInt32(ByVal ptr As IntPtr,
ByVal ofs As Integer, ByVal val As Integer)
Overloads Public Shared Sub WriteInt32(ByVal ptr As Object,
ByVal ofs As Integer, ByVal val As Integer)
Итак, функция номер раз пишет значение val
в память по адресу ptr, номер два делает то же
самое, но добавляет к адресу смещение ofs
(удобно для записи поля в классе или структуре), ну а третья делает то же
самое, но адрес в ней представлен типом Object
вместо IntPtr (на мой взгляд, стоило поставить
тип Integer).
Overloads
Public Shared Function ReadInt32(ByVal ptr As IntPtr) As Integer
Overloads Public Shared Function ReadInt32(ByVal ptr As IntPtr,
ByVal ofs As Integer) As Integer
Overloads Public Shared Function ReadInt32(ByVal ptr As Object,
ByVal ofs As Integer) As Integer
Первая функция читает из блока памяти с адресом ptr и возвращает прочитанное целое число, вторая перед
чтением добавляет смещение ofs, ну а третья в
качестве адреса принимает значение типа Object.
Данная структура предоставляет средства доступа к
управляемому объекту из неуправляемой памяти. Использование GCHandle удобно и эффективно при необходимости получения
доступа к ссылке на объект из неуправляемого кода. GCHandle
может быть использован для получения адреса объекта или для получения новой
ссылки на объект в управляемом коде.
Работа со структурой GCHandle
начинается с вызова разделяемого метода Alloc.
Overloads
Public Shared Function Alloc(ByVal Value As Object) As GCHandle
Overloads Public Shared Function Alloc(ByVal Value As Object,
ByVal type As GCHandleType) As GCHandle
Первый вариант данной функции распределяет обычный (Normal) описатель для заданного объекта. Данный тип
описателя непрозрачен и не позволяет получить адрес объекта через описатель.
Второй вариант функции распределяет для объекта заданный тип описателя.
Созданный описатель, после завершения работы с ним, необходимо освободить,
вызвав метод Free.
Самое время узнать, что за зверь такой – тип описателя.
Normal
|
Этот тип описателя представляет «непрозрачный» описатель,
который не позволяет вам получить адрес зафиксированного объекта через
описатель. Используется для отслеживания объекта и предотвращает его от сбора
коллектором мусора.
|
Pinned
|
Описатель этого типа аналогичен Normal,
но позволяет получить адрес зафиксированного объекта. Коллектор мусора не
может переместить этот объект и таким образом его эффективность снижается.
|
Weak
|
Этот описатель позволяет отслеживать объект, но позволяет
«собрать» себя коллектору мусора. Когда объект собран, содержимое его GCHandle обнуляется. Weak
ссылка обнуляется перед запуском завершителя, поэтому даже если завершитель
«воскресит» объект, Weak ссылка останется
нулевой.
|
WeakTrackResurrection
|
Этот описатель аналогичен Weak,
но описатель не обнуляется, если объект воскрешен при завершении.
|
Не будем останавливаться на двух последних типах описателей
– это всего лишь модификации типа Normal,
рассмотрим типы Normal и
Pinned.
Создать описатель простого типа можно для объекта абсолютно
любого типа. Это может быть строка, массив, целое число, структура или класс.
Единственное требование – класс должен быть определен с атрибутом StructLayout, то есть функции Alloc
необходимо знать, как располагаются поля объекта в памяти. Для структур все
проходит и без атрибута.
Что можно делать с таким описателем? В принципе что угодно,
но основное его предназначение передача в API
функцию, которой необходим параметр типа LPVOID
(или его родственник). Это могут быть, например функции, использующие callback-вызовы. Здесь вполне может сложиться такая ситуация,
когда в качестве параметра приходится передавать либо разные типы в зависимости
от ситуации, либо передавать ссылочный объект. Передать разные типы еще
возможно, используя некоторые партизанские способы (и легальные тоже), а вот
передать ссылочный объект… И не надейтесь, только через GCHandle!
Есть правда одно но. Объект, который используется в GCHandle нельзя изменять. Вот пример кода:
Dim n As Integer = 1000
Dim hGC As GCHandle =
GCHandle.Alloc(n)
Console.WriteLine("Перед
модификацией:")
Console.WriteLine("hGC.Target
= {0}", hGC.Target)
Console.WriteLine("n
= {0}", n)
n = 2000
Console.WriteLine("После
модификации:")
Console.WriteLine("hGC.Target
= {0}", hGC.Target)
Console.WriteLine("n
= {0}", n)
hGC.Free()
В окно консоли будет выведено:
Перед модификацией:
hGC.Target = 1000
n = 1000
После
модификации:
hGC.Target = 1000
n = 2000
Попробуем по другому:
Dim n As Integer = 1000
Dim hGC As GCHandle =
GCHandle.Alloc(n)
Console.WriteLine("Перед
модификацией:")
Console.WriteLine("hGC.Target
= {0}", hGC.Target)
Console.WriteLine("n
= {0}", n)
hGC.Target = 2000
Console.WriteLine("После
модификации:")
Console.WriteLine("hGC.Target
= {0}", hGC.Target)
Console.WriteLine("n
= {0}", n)
hGC.Free()
Результаты получаются с точностью до наоборот:
Перед модификацией:
hGC.Target = 1000
n = 1000
После
модификации:
hGC.Target = 2000
n = 1000
А связано это с тем, что GCHandle
работает с копией структурного объекта и всякая модификация исходного объекта
не сказывается на копии и наоборот.
Однако при работе со ссылочными объектами модификация полей
вполне возможна.
<StructLayout(LayoutKind.Sequential)>
Private Class CTestGCH
Public IntData As Integer
Public StrData As String
Public Sub New(ByVal n As Integer, ByVal s As
String)
IntData = n
StrData = s
End Sub
Public Overrides Function ToString() As
String
Return String.Format("IntData
= {0}, StrData = {1}", IntData, StrData)
End Function
End Class
...
Dim c As New CTestGCH(1000,
"First")
Dim hGC As GCHandle =
GCHandle.Alloc(c)
Console.WriteLine("Перед
модификацией:")
Console.WriteLine("hGC.Target
= {0}", hGC.Target)
Console.WriteLine("c
= {0}", c)
c.IntData = 2000
c.StrData =
"Second"
Console.WriteLine("После
модификации:")
Console.WriteLine("hGC.Target
= {0}", hGC.Target)
Console.WriteLine("c
= {0}", c)
hGC.Free()
В окне консоли получим:
Перед модификацией:
hGC.Target
= IntData = 1000, StrData = First
c = IntData = 1000, StrData = First
После модификации:
hGC.Target = IntData = 2000, StrData = Second
c = IntData = 2000, StrData = Second
После модификации строк:
c.IntData = 2000
c.StrData =
"Second"
на
CType(hGC.Target,
CTestGCH).IntData = 2000
CType(hGC.Target,
CTestGCH).StrData = "Second"
Результат остается неизменным. Связано это с тем, что GCHandle содержит не копию исходного объекта, а ссылку на
него, поэтому то модификация полей эффективна для всех ссылок на объект. При
работе с массивами CGHandle также успешно
отслеживает любые изменения его элементов, поскольку любой массив является
ссылочным типом, и изменение элементов распространяется на все ссылки на
массив. Но если вместо класса мы объявим аналогичную структуру, то модификация
полей будет сказываться только на той копии, к которой мы ее применим.
Пара слов о модификации строк. Если мы распределим GCHandle для строки, то последующее изменение строки
сказывается только на строке. Содержимое описателя не изменится. Это связано с
тем, что строки в VB.NET
являются неизменными. И при модификации строки на самом деле создается новая
строка.
Требования для создания фиксированного описателя те же, что
и для создания обычного описателя. С одним единственным дополнением – класс или
структура могут содержать в себе только простые типы. То есть ссылки, строки и
массивы содержаться внутри класса или структуры не должны. Это же относится и к
строкам!
После создания фиксированного описателя можно получить
указатель на область памяти, где содержится объект, распределенный при помощи
описателя. Для простых типов и структур это указатель на копию объекта.
Поэтому, если вы передадите при помощи фиксированного описателя в API-функцию такой указатель, и эта функция изменит
значение по этому указателю, то реально изменится значение, на который
указывает указатель (свойства Target), а
исходный объект останется неизменным. Поэтому, при работе с фиксированным
описателем (как впрочем, и с обычным) следует обновлять значение исходного
объекта, используя свойство Target.
При использовании фиксированного описателя при работе с
классами и массивами, указатель, содержащийся в описателе, совпадает с реальным
указателем на данные, содержащиеся в классе и массиве (на данные!!! Не на сам
массив или класс, а на данные!!!). Поэтому, изменяя значения полей объекта или
элементов массива при помощи такого указателя, мы меняем реальные исходные
данные.
Несколько иначе происходит работа со строками. При создании
фиксированного описателя для строки ее содержимое копируется, но, изменяя
строку, при помощи указателя мы изменяем и исходную строку. По-видимому, здесь
используется тот же механизм, что и при передаче строк в API-функции.
Налицо противоречие с тем фактом, что в NET
строки неизменны.
Резюме: фиксированный описатель – легальный способ получения
указателей на встроенные типы, структуры и классы. Причем для классов и строк –
этот способ является единственным!
Класс Buffer предоставляет
методы для копирования, получения и установки байтов в массивах. Также он
позволяет получить длину массива в байтах. Все это касается массивов
примитивных типов. Рассмотрим методы этого класса.
Public
Shared Sub BlockCopy(ByVal src As Array, ByVal srcOffset _
As Integer, ByVal dst
As Array, ByVal dstOffset As Integer, ByVal count As Integer
Метод копирует count байт из
массива src в массив dst
с учетом смещений srcOffset и dstOffset. Смещения задаются в БАЙТАХ. Тип
массивов здесь не имеет никакого значения, главное чтобы они содержали
примитивные типы. Собственно это неплохой, но медленный способ преобразования
из BYTE в WORD,
из WORD в DWORD и
так далее.
Public
Shared Function ByteLength(ByVal array As Array) As Integer
Возвращает количество байт, содержащихся в массиве array. Массив должен состоять из примитивных типов.
Public
Shared Function GetByte(ByVal array As Array, ByVal index As Integer) As Byte
Public Shared Sub SetByte(ByVal array As Array, ByVal index As
Integer, ByVal value As Byte)
Эти две функции соответственно возвращают и устанавливают
байт c индексом index в
массиве array. Массив, естественно, должен
состоять из примитивных типов.
Класс BitConverter позволяет
конвертировать примитивные типы в массив байтов и массив байтов в примитивные
типы. Примитивные типы преобразовываются в массив байтов при помощи метода GetBytes. Преобразование доступно для 10
типов: Char, Boolean, Short, Integer,
Long, Single, Double, UInt16, UInt32 и
UInt64. В качестве
примера приведем определение этого метода для типа Integer.
Overloads
Public Shared Function GetBytes(ByVal value As Integer) As Byte()
Значение value
преобразовывается в массив байтов соответствующей длины, в данном случае это
массив из четырех байтов.
Обратное преобразование производится методами ToChar, ToBoolean, ToInt16, ToInt32,
ToInt64, ToSingle,
ToDouble, ToUInt16,
ToUInt32 и ToUInt64.
Рассмотрим метод ToInt32.
Public
Shared Function ToInt32(ByVal value() As Byte, ByVal startIndex As Integer) As
Integer
Массив байтов value
преобразовывается в целое число, начиная с байта startIndex.
Разумеется, массив должен содержать необходимое для преобразования число байт
(с учетом startIndex).
Класс BitConverter также
включает методы преобразования числа с плавающей запятой двойной точности (Double) в 64-разрядной целое со знаком (Long) и наоборот. Это реализуется методами DoubleToInt64Bits и Int64BitsToDouble.
Public
Shared Function DoubleToInt64Bits(ByVal value As Double) As Long
Public Shared Function Int64BitsToDouble(ByVal value As Long) As
Double
Особенность этих методов в том, что байты не
преобразовываются и, например, значение Long
равное 255 преобразовывается в значение Double
равное 1.25986739689518E-321 (или в обратном направлении: 255.0 в
4643176031446892544).
Три перегруженных метода ToString
позволяют вывести массив байтов в шестнадцетиричном представлении. Массив
байтов выводится в виде строки, где каждый байт представлен в виде пары
шестнадцетиричных чисел, и каждая пара соединена с другой дефисом, например «7F-2C-4A».
Overloads
Public Shared Function ToString(ByVal value() As Byte) As String
Overloads Public Shared Function ToString(ByVal value() As Byte,
ByVal startIndex As Integer) As String
Overloads Public Shared Function ToString(ByVal value() As Byte,
ByVal startIndex As Integer, ByVal length As String) As String
Первая функция преобразовывает в строку весь массив value. Вторая функция преобразовывает в строку байтовый
массив value, начиная со startIndex элемента. И третья функция преобразовывает length элементов массива value,
начиная с элемента startIndex.
Ну и, наконец, единственное поле класса BitConverter IsLittleEndian указывает, в
каком порядке хранятся байты в памяти компьютера. Формат «Little-Endian» предполагает хранение байтов от младшим к
старшим, а в формате «Big-Endian» вначале хранятся старшие байты, а затем младшие.
Например, целое число –15732736 (в шестнадцатеричном формате это будет &HFF0FF000) в
формате «Little-Endian»
хранится в виде такой последовательности байт: &H00
&HF0 &H0F &HFF. В
формате «Big-Endian»
последовательность байтов будет обратная: &HFF
&H0F
&HF0 &H00.
Оператор AddressOf теперь в
отличие от предыдущих версий не возвращает указатель на функцию, а создает
делегат соответствующего типа. Именно его (делегат то есть) и надо передавать в
функции API. А реальный указатель на функцию
можно получить только через партизанские тропы.
Перед переходом на партизанские тропки-строчки замолвим словечко
за общий метод Copy. Этот метод копирует часть одного
массива в другой массив, при необходимости осуществляя приведение типов и
упаковку или распаковку.
Overloads
Public Shared Sub Copy(ByVal sourceArray As Array, ByVal destinationArray As
Array, ByVal length As Integer)
Первый вариант функции копирует length
элементов массива sourceArray в массив destinationArray. Массивы должны иметь одинаковое число
измерений и иметь совместимый тип. При копировании многомерных массивов
предполагается, что ряды расположены друг за другом (для двумерных массивов,
далее по аналогии).
Overloads
Public Shared Sub Copy(ByVal sourceArray As Array, ByVal sourceIndex As
Integer, ByVal destinationArray As Array, ByVal destinationIndex As Integer,
ByVal length As Integer)
Второй вариант функции копирует length элементов массива sourceArray, начиная с элемента sourceIndex в массив destinationArray, начиная с элемента destinationIndex. Ограничения те же. Этот вариант функции может копировать
элементы одного массива с одного места в другое. Данная возможность очень
удобна при вставке или удалении элементов из середины массива. Это можно
реализовать при помощи указателей API-функцией CopyMemory. Но получить указатель в VB.NET проблематично, а для ссылочных типов практически и
невозможно. Ну и последний аргумент в пользу метода Copy
– он работает быстрее, чем CopyMemory.
Все партизанские методы, описанные ниже, являются
результатом моих долгих личных экспериментов в VB.NET 2001. Все успешно работало и в VB.NET 2003.
Проверить работоспособность партизанских строк в VB.NET 2005 пока не удалось. Все нижеописанное вы
можете использовать на свой страх и риск без каких-либо гарантий.
Скорбь по безвременно покинувшим нас функциям VarPtr, StrPtr и ObjPtr должна быть недолгой. У хорошего проггера руки не
могут быть кривыми и написание трех строк в C не должно
вызывать каких либо сложностей:
__declspec(dllexport) LPVOID
GetPointer(LPVOID lp)
{
return lp;
}
Естественно этот код надо поместить в DLL,
написать процедуру входа, написать заголовочный файл, файл экспорта и так
далее… Вполне может быть, что помимо функции GetPointer
вы создадите еще несколько полезных для себя функций и создадите оченно
полезную DLL. Пусть, для определенности ее название
будет «NetLibSupport» (Библиотека поддержки для NET).
Остается объявить эту функцию с нужными параметрами,
например, так:
Public Declare Auto Function VarPtr Lib
"NetLibSupport" Alias "GetPointer" (ByRef v As Integer) As
IntPtr
Public Declare Auto Function VarPtr Lib
"NetLibSupport" Alias "GetPointer" (ByRef v As Double) As
IntPtr
Public Declare Auto Function VarPtr Lib
"NetLibSupport" Alias "GetPointer" (ByRef v As Single) As
IntPtr
Public Declare Auto Function VarPtr Lib
"NetLibSupport" Alias "GetPointer" (ByRef v As Char) As
IntPtr
Public Declare Auto Function VarPtr Lib
"NetLibSupport" Alias "GetPointer" (ByRef v As Byte) As
IntPtr
Или даже так:
Public
Declare Auto Function VarPtr Lib "NetLibSupport" Alias
"GetPointer" (ByRef v As UserType) As IntPtr
Вот вам и VarPtr для любого
нужного типа. Главное не забыть в объявлении ByRef
иначе на выходе вы получите то же что и на входе, то есть значение переменной,
а не ее адрес. Это проходит со всеми примитивными типами. Смысл работы этой
функции в том, что примитивные типы являются структурами, единственным членом
которых является поле этого самого типа. Передавая структуру по ссылке в функцию
GetPointer, мы как раз и получаем адрес первого члена
структуры, который функция и возвращает.
С типом String сложнее.
Можно конечно объявить функцию GetPointer с
аргументом String, но что вы получите? То есть
получить то указатель вы сможете, но вот на что он будет указывать? Получить
реальный указатель на данные строки можно таким вот образом:
Dim s
As String = “Test String”
Dim p As IntPtr = VarPtr(s.ToCharArray()(0))
То есть вычисляется указатель на первый символ строки.
Казалось бы, метод ToCharArray должен создавать
копию массива символов, но нет. Этот массив вполне соответствует исходной
строке. Этот указатель можно передать в функцию API
и функция сможет изменить данные по этому указателю. Но никто не даст Вам
гарантию, что в некий момент времени данные строки не переместятся в другое
место и API запишет свою строку совершенно не
туда, куда вам хотелось бы. Больше шансов получить не то, что хочется для
длинных строк. CLR может распределить память для
строки в нескольких отдельных блоках. Так что пользуйтесь законными методами: StringToHGlobalUni(Ansi/Auto) или GCHandle типа Pinned. Эти указатели законны и их использование не
приведет к непредсказуемым последствиям.
Можно объявить VarPtr и для
структуры и даже для класса, определенного с атрибутом StructLayout.
При объявлении функции VarPtr для класса
необходимо объявлять ее с параметром ByVal!!!!
Передавая объект ссылочного типа по значению, а не по ссылке мы получаем адрес
первого поля класса (не всегда верно, но что интересно всегда работает). Дополнительное
условие – класс должен содержать только простые типы.
Остается вопрос, что делать с этими указателями? Можно
передать в API функции. Только вот вряд ли
найдется функция, понимающая ваш класс. Разве что написанная собственноручно.
Или можно использовать такой указатель в callback-функциях
вместо GCHandle. Хотя, на мой взгляд, GCHandle значительно удобнее и легальнее.
Указатель на объект класса можно также успешно применить для
копирования объектов вместо копирующего конструктора.
Функция CopyMemory (на
самом деле RtlMoveMemory) никуда из kernel32 не делась. Надо только правильно ее
объявить:
Public
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory"
(ByVal Dest As IntPtr, ByVal Src As IntPtr, ByVal Length As Integer)
Public Declare
Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (ByRef
Dest As Type, ByVal Src As IntPtr, ByVal Length As Integer)
Public
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory"
(ByVal Dest As IntPtr, ByRef Src As Type, ByVal Length As Integer)
Public
Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory"
(ByRef Dest As Type, ByRef Src As Type, ByVal
Length As Integer)
Первый вариант позволяет копировать Length
байт из блока памяти Src в блок памяти Dest. То есть этот вариант универсальный. В остальных
трех вариантах производится копирование данных определенного типа. Вместо Type должен стоять
конкретный тип данных. Второй вариант позволяет копировать Length
байт из блока памяти Src в переменную Dest. Третий вариант копирует Length
байт из переменной Src в блок памяти Dest. Ну и, наконец, в четвертом блок памяти Length байт из переменной Src
копируются в переменную Dest.
В качестве Type
может использоваться любой примитивный тип, структура и класс, объявленный с
атрибутом StructLayout. Кроме того, для класса
параметры должны объявляться как ByVal(!!!!!)
и класс не должен содержать ссылочные типы.
Копировать строки через CopyMemory,
к сожалению (а может и к счастью) невозможно!!!!
При копировании массивов можно использовать эти же функции,
подставляя первый член массива в качестве параметра (собственно и не первый
тоже можно). Главное не ошибиться с числом копируемых байт. Всегда используйте Marshal.SizeOf
и Array.Length! Не полагайтесь на то, что, к примеру, Integer
содержит 4 байта. В прошлой версии в нем было всего лишь 2, и нет гарантии (а
хотелось бы), что через пару лет при переходе на 64-разрядные процессоры в Integer
не появиться еще 4 байта. Тем более, необходимо использовать SizeOf со
структурами.
Но лучше копировать массивы при помощи метода Copy класса Array. Он
работает надежнее и быстрее.
Как уже было сказано, оператор AddressOf
теперь создает делегат, вместо возможности получить адрес функции. Казалось бы,
какая разница как объявлять функцию API? Вместо IntPtr записываем делегат соответствующего типа и все
замечательно. К сожалению, вполне может сложиться такая ситуация, когда вам
необходим реальный указатель на функцию. Это возможно, например, при вызове
функции, которая обеспечивает Callback вызовы
при некоторых условиях. То есть у вас есть параметр, который при одних условиях
может быть указателем на функцию, а при других чем-то другим. Одним словом этот
параметр всегда целое число и перегрузка здесь, к сожалению, не поможет.
Все очень просто. Записываем делегат. Например:
Private
Delegate Function MyFunc(ByVal arg As Integer) As Integer
Затем объявляем уже упомянутую функцию GetPointer следующим образом:
Private
Declare Auto Function VarPtr Lib "NetLibSupport" Alias
"GetPointer" (ByVal v As MyFunc) As IntPtr
И получаем указатель на функцию:
Dim
pFunc As IntPtr = VarPtr(AddressOf F)
F – имя функции,
соответствующей сигнатуры.
И делайте с указателем на функцию все, что вам
заблагорассудится.
В VB.NET,
как и в предыдущих версиях языка почти нет необходимости в использовании
указателей и непосредственном доступе к неуправляемой памяти. Если вам все же
понадобятся указатели (я надеюсь, вы не станете писать структуру типа список
при помощи указателей? Здесь вполне можно обойтись типом Object),
то вы легко получите их, в 90% случаев используя легальные методы работы с
неуправляемой памятью. А оставшиеся 10% легко реализуются правильным
объявлением функций GetPointer и RtlMoveMemory. После прочтения этого скромного труда
количество вопросов об указателях должно значительно уменьшиться.
Так что вперед! На VB.NET можно написать все что угодно. Кроме разве что
драйверов и операционных систем. Но вряд ли это понадобиться проггеру на VB.NET.
И напоследок несколько рекомендаций.
1. Прежде
чем начать возню с указателями и неуправляемой памятью убедитесь в том, что это
действительно нужно. В большинстве случаев проблема решается правильным
объявлением функции с нужными атрибутами.
2. Если
у вас есть какие-либо неясности с тем, как передается тот или иной параметр, то
создайте тестовое решение из двух проектов, один из которых должен быть
написанной на C++ DLL-библиотекой
и содержать функцию, параметры которой точно совпадают с параметрами нужной вам
функции. Объявив во втором, стартовом проекте на VB
эту функцию и установив флажок Unmanaged Code Debugging в свойствах проекта, вы сможете
проконтролировать процесс передачи параметров.
3. Прежде
чем использовать партизанские строчки-тропки убедитесь, что вам действительно
не помогают легальные способы работы с памятью. Не забывайте, что классы
пространства имен System.Runtime.InteropServices
содержат огромное количество методов легальной работы с неуправляемой памятью и
указателями.
4. Много
интересного о внутренней структуре классов и структур CLR можно узнать при просмотре библиотеки mscorlib.dll в поставляемом со средой VS IL Disassembler.
Автор: Путевской Виктор