Дата публикации статьи: 18.02.2006 07:14

Работа с памятью и указателями в VB.NET

 

 

Теоретически программист, пишущий на Visual Basic может обходиться только средствами языка. Практически, рано или поздно ему придется обратиться к API. А поскольку функции API написаны, как правило, на C, то указатели встречаются в них на каждом шагу. Большую часть проблем решает правильное объявление нужной функции, но встречаются ситуации, когда правильное объявление функции только половина дела. Вторая половина – получение и использование указателя. До появления .NET проблема решалась при помощи недокументированной функции VarPtr, ее близнецов ObjPtr и StrPtr, а также нескольких редко используемых родственников. Сказка кончилась… VarPtr отсутствует в runtime-библиотеке VB.NET… Но на наше проггерское счастье в этом языке появились новшества, которые значительно снижают необходимость обращения к API-функциям. Однако обращения к API все равно приходится делать. И от работы с памятью и указателями нам никуда не уйти… Но не все так плохо, как кажется, в VB.NET появились вполне легальные средства работы с памятью и указателями. А если нам их не хватит, то можно протоптать свои, партизанские строки кода. Начнем, естественно, с легальных методов.

Структуры и классы

Начнем с того, что все встроенные в VB.NET (вернее в CLRCommon 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 98ANSI-строка.

LPWStr

Передает Unicode-строку без префиксов.

R4

4-байтовое число с плавающей запятой.

R8

8-байтовое число с плавающей запятой.

Struct

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

SysInt

Передает поле или аргумент как системное целое число. Для 32-разрядной Windows – 4-байтовое целое число со знаком. Для 64-разрядной Windows – 8-байтовое целое число со знаком.

SysUInt

Системное целое число без знака.

TBStr

Передает строку с префиксом в зависимости от платформы. Для Windows NT передается Unicode-строка, для Windows 98ANSI-строка.

U1

1-байтовое целое число без знака.

U2

2-байтовое целое число без знака.

U4

4-байтовое целое число без знака.

U8

8-байтовое целое число без знака.

VBByRefStr

Позволяет Visual Basic изменять строку из неуправляемого кода и отображать результаты в управляемый код.

Таким образом, зная, как интерпретируются параметры функций и поля структур (классов) при использовании атрибута MarshalAs легко реализовать правильные объявления функции и определения структур. Главное правильно понимать смысл типов С/С++ и внимательно изучать определения функций и структур в заголовочных файлах.

Тип IntPtr

В языке появился забавный тип 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

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

Работа со структурой 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.

Простой описатель (Normal)

Создать описатель простого типа можно для объекта абсолютно любого типа. Это может быть строка, массив, целое число, структура или класс. Единственное требование – класс должен быть определен с атрибутом 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

Класс 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

Класс 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. А реальный указатель на функцию можно получить только через партизанские тропы.

Класс Array

Перед переходом на партизанские тропки-строчки замолвим словечко за общий метод 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, что взамен?

Скорбь по безвременно покинувшим нас функциям 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.

 

Автор: Путевской Виктор