Сайт Алексея Муртазина (Star Cat) E-mail: starcat-rus@yandex.ru
Мои программы Новости сайта Мои идеи Мои стихи Форум Об авторе Мой ЖЖ
VB коды Статьи о VB6 API функции Самоучитель по VB.NET
Собрания сочинений Обмен ссылками Все работы с фото и видео
О моём деде Муртазине ГР Картинная галерея «Дыхание души»
Звёздный Кот

Самоучитель по VB.NET
Глава 15.

COM Interop и использование функций Win32 API

Жил да был на свете программист. За долгие годы он написал много программ, и все они хорошо работали. Однажды ему явился великий дух ОС и представил новый способ программирования. По словам духа, программист должен был написать еще больше программ, и работать они должны были еще лучше.

Но бедный программист посмотрел на новую систему и понял, что переработка готовых программ займет много лет (и вообще есть занятия поинтереснее). И еще он понял, что в новой системе кое-что сделать попросту невозможно, и это «кое-что» придется делать по-старому.

Но великий дух ОС дал программисту волшебную лампу. Стоило программисту потереть лампу, как появился синий джинн, распевавший песенки на музыку из диснеевских мультиков. Джинн произнес заклинание, и все программы сами собой адаптировались для новой ОС. А еще джинн подарил программисту волшебное кольцо, которое генерировало новые классы для всего, что не поддерживалось новой системой.

И после этого программист жил долго и счастливо.

Но такое случается только в сказках.

К сожалению, синие джинны водятся только в мультиках Диснея, поэтому Microsoft не сможет обеспечить автоматический перенос в .NET всех существующих приложений и компонентов на базе СОМ. А все волшебные кольца были переданы для съемок фильма «Властелин колец»1. Так что какой бы огромной ни была библиотека классов .NET, всегда найдутся задачи, для которых в ней не предусмотрено готовых решений. К счастью, разработчики Microsoft приложили немало усилий к тому, чтобы новые программы .NET могли работать с компонентами СОМ и вызывать базовые функции Win32 API. Первая задача решается при помощи средств COM Interop (сокращение от Interoperability), а для решения второй используется механизм Platform Invoke (P-Invoke). Большинство классов и атрибутов, упоминаемых в этой главе, находится в пространстве имен System. Runtime.InteropServices.

1К которому я не имею ни малейшего отношения, хотя и жду с нетерпением.

Это два разных механизма, однако на практике они довольно тесно связаны, поскольку в обоих случаях .NET Framework приходится работать с небезопасным кодом1.

1То есть кодом, не находящимся под непосредственным контролем .NET Framework.

 

COM Interop

COM Interop представляет собой часть .NET Framework, отвечающую за взаимодействие существующих компонентов/приложений СОМ с компонентами/приложениями .NET. Говоря о взаимодействии с СОМ, необходимо учитывать два ключевых обстоятельства.

Система COM Interop выполняет функции шлюза между компонентами .NET и компонентами СОМ. Благодаря ей компоненты СОМ с точки зрения .NET кажутся компонентами .NET, а компоненты .NET с точки зрения СОМ кажутся компонентами СОМ (рис. 15.1).

Рис. 15.1. Схема работы COM Interop 

Когда объект .NET хочет обратиться к объекту СОМ, он использует RCW (Runtime Callable Wrapper) — сборку .NET, благодаря которой объект СОМ выглядит как обычный объект .NET.

Когда объект СОМ желает обратиться к объекту .NET, среда создает объект CCW (COM Callable Wrapper), который выглядит и работает как обычный объект СОМ.

COM Interop работает в обоих направлениях. Очевидно, приоритетным направлением станет использование объектов СОМ объектами .NET — в настоящее время существует огромное количество объектов СОМ, а объектов .NET совсем мало. Конечно, новые объекты .NET должны работать с СОМ, но часто ли будут создаваться объекты .NET, которые должны использоваться существующими компонентами СОМ? Подозреваю, что это будет очень редким событием. Возможно, исключением окажутся транзакционные компоненты, которые должны работать за пределами процесса (для большинства таких компонентов базовым классом будет класс System. EnterpriseServices. ServicedComponent). Тем не менее возможность обращения к объектам .NET из объектов СОМ не стоит недооценивать, поскольку приложениям .NET нередко придется предоставлять объекты, предназначенные для использования в системах на базе СОМ.

Я не собираюсь излагать в этой главе все тонкости работы COM Interop. Это не нужно. Хотя в документации COM Interop чрезвычайно подробно расписаны все возможные ситуации, для большинства читателей интерес представляют только сценарии, удовлетворяющие двум критериям:

Остальные случаи будут игнорироваться, поскольку описанный сценарий действует в 95 % (а то и больше) ситуаций, с которыми вы столкнетесь. Начнем с направления «от .NET к СОМ».

1К сожалению, даже краткое изложение основ СОМ выходит за рамки этой книги. Впрочем, даже если вы незнакомы с СОМ, попробуйте прочитать эту главу, не обращая внимания на непонятные термины «Automation» и «IDispatcn».

 

Использование объектов СОМ в .NET

Использование объектов СОМ в Visual Basic .NET не вызывает ни малейших проблем. Достаточно выполнить следующие действия.

1. Убедитесь в том, что используемый объект СОМ зарегистрирован в системе.

2. Выберите команду Add> References в меню Project или в окне Solution Explorer.

3. Перейдите на вкладку СОМ и выберите нужный объект.

При добавлении ссылки на компонент СОМ в Visual Studio вам будет предложено построить «первичную сборку» (RCW) для компонента на основании библиотеки типов компонента. Это происходит лишь в том случае, если сборка RCW для данного компонента 'СОМ еще не была зарегистрирована в глобальном кэше сборок. Если ваш ответ будет положительным, Visual Studio создает сборку RCW и размещает ее в каталоге, в котором с ней будет работать ваш проект1.

1Дальнейшее описание относится только к локальным сборкам. Если вы хотите создать сборку RCW для размещения в глобальном кэше сборок, воспользуйтесь утилитой TLBImp, описанной в документации. Вопросы установки и размещения компонентов более подробно рассматриваются в главе 16.

Проект VB6COM содержит простую библиотеку ActiveX DLL для VB6, определяемую следующим образом:

' Пример обращения к компоненту СОМ из .NET

' Copyright ©2001 by Desaware Inc. All Rights Reserved

Option Explicit

Private Den GetCurrentThreadld Lib "kernel32" () As Long

Public Function TimesTwo(ByVal x As Long) As Long

TimesTwo = x * 2 End Function

Public Function TrimString(ByVal s As String) As String

TrimString = Trim(s) End Function

Public Function GetThisThreadldO As Long 

GetThisThreadld = GetCurrentThreadld()

 End Function

Чтобы компонент dwComFromNetdll стал доступным для VB .NET, его необходимо зарегистрировать. Для этого можно построить DLL заново или воспользоваться утилитой RegSvr32.

Пример обращения к сборке RCW после ее создания показан в приложении UsesCOMl (листинг 15.1).

Листинг 15.1. Проект UsesCOMl

' Пример обращения к компоненту СОМ из .NET

' Copyright ©2001 by Desaware Inc. All Rights Reserved

Iiriports System.Threading

Module Modulel

' В имя компонента, испопьзуемое в ,NET, включается

' номер версии библиотеки типов.

Dim ComObject As New dwComFromDotNet_8_0.Sample

Sub FromAl'terfrateThread(); 

Dim tid As Long

tid = ComObject.GetThisThreadld()

Console.WriteLine ("From other thread, TID = " & Str(tid))

 End Sub

Sub Main()

Dim newThread As New Thread(AddressOf FromAlternateThread)

 Dim tid As Long

  newThread.Start() 

tid = ComObject.GetThisThreadld()

Console.WriteLine ("From main thread, TID = " & Str(tid))

 Console.WriteLine (ComObject.TimesTwo(S))

 Console.WriteLine (ComObject.TrimString(" to trim"))

newThread.Join()

Console. Readline()

 End Sub

End Module

Из приведенного листинга видно, что объект СОМ создается тем же способом, как и объект .NET. При запуске программы будет получен следующий результат1:

From main thread, TID = 1692

10

to trim

From other thread, TID = 1692 

1Естественно, идентификаторы потоков будут другими. Строка «From other thread» может находиться в разных местах в зависимости от взаимодействия между потоками.

Обратите внимание на интересную подробность: .NET Framework обеспечивает потоковую безопасность для объекта СОМ. Хотя в программе UsesCOMl методы объекта вызываются из двух разных потоков, оба вызова автоматически передаются потоку объекта.

 

Обработка ошибок

Объекты СОМ сообщают об ошибках при помощи 32-разрядных кодов HRESULT. При получении кода HRESULT, соответствующего признаку ошибки, RCW автоматически инициирует исключение. Коды HRESULT отображаются на ближайшие типы исключений, а их исходные значения сохраняются в объектах Exception.

 

Освобождение объектов

Для каждого объекта СОМ, используемого в программе, сборка RCW создает промежуточный объект (proxy). Следовательно, программа .NET имеет дело с обычным объектом .NET, который передает обращения к методам и свойствам объекту СОМ. Промежуточный объект содержит ссылку на объект СОМ и освобождает его, когда тот становится ненужным. Но когда это происходит?

Как вы знаете, объекты .NET уничтожаются сборщиком мусора с недетерминированным завершением. Таким образом, при использовании объектов СОМ в .NET отсутствие ссылок на промежуточный объект еще не гарантирует его освобождения. Если объект СОМ удерживает некоторые ресурсы, возможно, вы предпочтете принудительно освободить эти ресурсы перед освобождением последней ссылки на объект. Задача решается методом System.Runtime. Interop-Services . Marshal. ReleaseComObject, уменьшающим счетчик ссылок объекта CbM. Класс Marshal также содержит метод AddRef для увеличения счетчика, поэтому при желании вы фактически можете самостоятельно реализовать механизм подсчета ссылок для объекта. Впрочем, будьте осторожны: неверное использование этих методов может привести к утечке памяти или возникновению исключений.

 

Контроль версии

В предыдущих главах говорилось о том, что в .NET проблема «кошмара DLL» решается привязкой к конкретным версиям зависимых сборок. К сожалению, на СОМ эта особенность не распространяется, поэтому при обращениях к объектам СОМ из .NET теоретически возможны все проблемы, хорошо знакомые программистам VB. Не забывайте обеспечивать контроль версии и совместимость даже в том маловероятном случае, если ваши объекты СОМ будут использоваться исключительно из сборок .NET.

Позднее связывание

На уровне COM Interop может выполняться позднее связывание объектов СОМ. Класс Туре содержит методы GetTypeFromProgID и GetTypeFromCLSID, позволяющие получить для объекта СОМ объект Туре, используемый при позднем связывании (см. главу 11).

 

Передача структур и других типов параметров

Многие программисты VB не привыкли к тому, что объекты ActiveX DLL могут предоставлять для доступа извне структуры (пользовательские типы VB6). Дело в том, что данная возможность появилась только в VB6 и несовместима со старыми версиями VB (и другими платформами); кроме того, она слишком ненадежна1. Оказывается, передача структур в неуправляемый код — процедура весьма сложная. Вообще передача параметров и типов данных СОМ в .NET обходится без особых сложностей (за исключением типа Variant, преобразуемого в тип .NET Object). Задача усложняется при использовании интерфейсов, несовместимых с Automation, но, как говорилось выше, в данной главе эта тема не рассматривается, поскольку программисты VB очень редко сталкиваются с ней.

Проблемы передачи структур и других параметров будут подробно рассмотрены ниже, при обсуждении вызова функций Win32 API в .NET. В отличие от COM Interop, где процесс передачи в основном определялся библиотеками типов импорта/экспорта, при вызове функций Win32 API вам придется действовать самостоятельно. Разобравшись в принципах передачи параметров при вызове Win32 API, вы без особых трудностей разберетесь в любых ситуациях, возникающих при передаче параметров в СОМ.

1 Я несколько раз обжегся на использовании открытых структур в VB6. Возможно, эта проблема бы.-i.i исправлена в последующих обновлениях, но у меня не было желания возвращаться к работе с ними

 

Дополнительные замечания

Visual Studio прикладывает основательные усилия к тому, чтобы средства СОМ Interop хорошо работали в .NET. Предусмотрена даже такая возможность, как использование существующих элементов ActiveX в контейнерах .NET. Программисты C++, привыкшие к созданию более сложных объектов и непосредственной работе с IDL (Interface Description Language), найдут в спецификации Interop дополнительные сведения о различных типах параметров и других атрибутах, определяемых в файлах IDL. Тем не менее это один из тех случаев, когда поддержка в VB6 только подмножества СОМ, совместимого с Automation, играет положительную роль, поскольку у объектов СОМ, созданных в VB6, не будет проблем с .NET-совместимостью1.

1На момент издания книги еще неизвестно, до какой степени .NET будет поддерживать управление элементами ActiveX, созданными в VB6. Помните, что элементы ActiveX значительно сложнее компонентов ActiveX DLL и ActiveX EXE, поэтому при работе с ними в среде .NET с большей вероятностью возникают разного рода непредвиденные обстоятельства. Microsoft потратила огромные усилия на обеспечение взаимодействия, но если в этой области возникнут какие-либо проблемы, скорее всего, они будут связаны с элементами ActiveX.

 

Использование объектов .NET в СОМ

Создание объектов .NET, хорошо работающих в СОМ, — задача более сложная. Впрочем, речь идет не о реальных трудностях. Просто, работая в этой области, необходимо принимать во внимание некоторые дополнительные факторы.

Некоторые из этих факторов упоминаются в документации .NET как «необязательные». Вместо того чтобы перечислять все возможные варианты, я покажу, как правильно организовать внешний доступ к объектам из VB .NET. Данное решение применимо к объектам, которые совместимы с Automation и могут безопасно использоваться в VB6 и СОМ+. VB .NET также дает возможность создавать объекты, несовместимые с Automation, несовместимые с VB6 и не поддерживающие контроля версии средствами СОМ. Если вы захотите узнать, как создавать объекты этих типов, обращайтесь к документации Microsoft no VB.

Чтобы понять все требования и логические обоснования того подхода, который я собираюсь продемонстрировать, необходимо кое-что знать о том, как в VB6 организован внешний доступ к объектам СОМ.

Каждый объект VB6 поддерживает два интерфейса2. Первый — непосредственный интерфейс класса. Согласно правилам имя этого интерфейса начинается с символа подчеркивания, и он используется при раннем связывании. Второй интерфейс, IDispatch, обеспечивает позднее связывание средствами Automation. В листинге 15.2 приведена библиотека типов для файла dwComFromDotNet.dll (см. листинг 15.1).

ПРИМЕЧАНИЕ

Значения UUID и ID, используемые в вашем случае, будут отличаться от приведенных в листинге 

2На самом деле больше, но эти два интерфейса решают стандартные задачи СОМ (такие, как обработка событий) и не реализуют методы, определяемые программистом.

Листинг 15.2. Библиотека типов для файла dwComFromDotNetdll (VB6 ActiveX DLL)

// Файл .IDL (сгенерирован OLE/COM Object Viewer)

//

// typelib filename: dwConFromDotNet.dll

[

uuid(7CFFA0A5-388D-48DC-8C3F-A3F7206CFC86),

version(6.Q),

hetpstringC'MovingToVB .NET: Example of calling COM component from .NET") ]

library dwComFromDotNet

 {

// TLib: // TLib: OLE Automation : {00020430-0000-C000-00000000046}

importlib("stdole2.tlb");

// Опережающее объявление всех типов, определенных в библиотеке interface _Sample

[

odl,

uuid(F75159CA-A859-4B9A-10596313D0El) ,

version(1.0),

hidden,

dual,

nonextensible,

oleaUitomation ] interface _Sampleatch 

{

[id(0x60030000)J

HRESULT TimesTwo( [in] long x, [out, retval] long* );

[id(0x60030001)l

HRESULT TrimStringC [in] BSTR s, [out, retval] BSTR* );

[id(0x60030002)J

HRESULT GetThisThreadId([out, retval] long* );

 };

 [

uuid(47B953CE-7E05-4408-95AA-2A79D739F237),

version(l.S) ] coassl {

[default] interface _Sample

}; 

};

Что мы видим в этом листинге?

  •  В нем определяется интерфейс с именем _Sample определяющий методы и свойства класса. В качестве признака успеха или неудачи методы возвращают код HRESULT.
  •  Интерфейс помечен атрибутами dual и oleautomation. Это означает, что он совместим с Automation, а методы интерфейса могут вызываться как напрямую, так и через IDispatch. На это также указывает тот факт, что интерфейс объявлен производным от IDispatch.
  •  Интерфейсу присвоен UUID — универсально-уникальный идентификатор (universally unique identifier); также встречаются термины IID (interface identifier), GUID (globally unique identifier) и CLSID (er). Все эти термины относятся к 16-байтовой величине, которая заведомо однозначно идентифицирует объект, интерфейс или класс. Термин изменяется в зависимости от контекста использования, но он всегда означает одно и то же — уникальный 16-байтовый идентификатор.
  •  С каждым методом интерфейса связан диспетчерский идентификатор (dispid) — число, используемое для идентификации метода при обращении к нему через Automation. Этот идентификатор задается атрибутом id(...).
  •  Секция coдентифицирует сам объект. Указанный идентификатор UUID определяет идентификатор класса (CLSID), под которым регистрируется объект.

 В этой книге я уже несколько раз упоминал о «кошмаре DLL». Теперь давайте взглянем на него с точки зрения СОМ. Чтобы новая версия компонента dwComFromDotNet.dll сохранила совместимость с существующей версией, должны

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

1. Все UUID нового компонента должны совпадать с UUID существующего компонента1.

2. Все имена методов и параметров нового компонента должны совпадать с аналогичными именами существующего компонента (в интерфейс можно добавить новые методы, однако это не рекомендуется: вам придется работать с несколькими версиями библиотеки типов, что приведет к усложнению задачи).

3. Методы и свойства нового компонента должны сохранить общее функциональное поведение методов и свойств существующего компонента.

1Я не буду отвлекаться на контроль версии библиотек типов, для нашей темы это несущественно.

Создание компонента CalledViaCOM (первая попытка)

Чтобы обеспечить возможность обращения к компоненту .NET из СОМ, можно установить флажок Register for COM Interop в диалоговом окне параметров проекта. Однако в этом случае компонент будет поддерживать только позднее связывание. VB .NET не создает для класса двойственный интерфейс, допускающий как раннее, так и позднее связывание.

Правильный подход к созданию компонента, используемого приложениями СОМ, заключается в явном определении интерфейса класса (листинг 15.3).

Листинг 15.3. Явное определение интерфейса

Imports System.Runtime.InteropServices

 Public Interface _CallFromCOM

 Function TimesTwo(ByVal i As Integer) As Integer

End Interface

Public mCOM

Implements _CaUFromCOM

Public Function TimesTwo(ByVal i As Integer) As Integer Implements _

 _CallFromCOM.TimesTwo

Return 1*2 

End Function

<ComRegisterFunction()> Public Shared Sub _ 

OnRegistration(ByVal Т As Type)

MsgBox("I'm being registered!!! :" & T.FullName) 

End Sub

 End

Обращает на себя внимание функция OnRegistration. При помощи атрибута ComRegisterFunction она сообщает о том, что должна вызываться при регистрации компонента. Также существует парный атрибут ComUnregi sterFunction для пометки функций, вызываемых при удалении регистрационных данных компонента.

Компилятору VB .NET также необходимо сообщить о том, что компонент будет использоваться объектами СОМ. Для этого в файле Assemblylnfo.vb устанавливается атрибут ComVisible (листинг 15.4).

Листинг 15.4. Файл Assemblylnfo.vb для сборки, доступной для объектов СОМ

Imports System.Reflection

Imports System.Runt1 me.InteropServices

' Следующая группа атрибутов содержит общую информацию о сборке.

' Изменение значений этих атрибутов приводит к изменению

' информации, связанной со сборкой.

' Значения атрибутов сборки

<Assembly: AssemblyTitle("CalledViaCOM")>

' Выводится в диалоговом окне ссылок VB6!!

<Assembly: AssemblyDescription("MovingToVB.NET Example called via COM")>

<Assembly: AssemblyCompany("Desaware Inc.")>

<Assembly: AssemblyProduct("MovingToVB.NET")>

<Assembly: AssemblyCopyr1ght("Copyr1ght ®2001 by Desaware Inc.")>

<Assembly: AssemblyTrademark("")>

<Assembly: CLSCompliant(True)>

1Идентификатор библиотеки типа при обращении к проекту из СОМ 

<Assembly: Guid("6431f4c9-b2f9-40fb-9420-301b96e2fc8e")>

' Информация о версии сборки состоит из следующих 

' четырех величин:

' основная версия;

' дополнительная версия;

' ревизия;

' номер построения.

' Вы можете задать значения всех атрибутов

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

' Для этого используется знак '*', как показано ниже.

<Assembly: AsserablyVersion("l.0.0.*")>

<Assembly: ComVisible(True)>

<Assembly: (Type.None)>

Если атрибут ComVisiblе равен True, все открытые объекты и члены классов будут доступны при регистрации сборки для использования в СОМ. Тем не менее атрибут ComVisiblе позволяет управлять видимостью отдельных элементов сборки. Например, если установить атрибут ComVisible(False) для метода класса, этот метод нельзя будет вызвать из СОМ. Данная возможность будет продемонстрирована в примере CalledViaCOM2.

Если присвоить атрибуту Туре значение None, то при экспортировании библиотеки типов интерфейс IDispatch не будет сгенерирован как интерфейс по умолчанию, что позволит назначить для всех объектов сборки интерфейс, определенный вами. Не беспокойтесь, возможность позднего связывания при этом не теряется, поскольку определяемый вами интерфейс по умолчанию будет двойственным.

Создание и регистрация компонентов

Зарегистрировать компонент VB .NET для использования в СОМ очень просто. После создания библиотеки классов (единственная разновидность компонентов .NET, доступных из СОМ) установите флажок Register for COM Interop на вкладке Configuration Properties Build диалогового окна Project Properties. При этом VB .NET сообщит вам о том, что из СОМ доступны лишь проекты с сильными именами, и предложит создать сильное имя для сборки. Согласитесь с этим предложением (сильные имена рассматриваются в главе 16).

После построения сборка регистрируется для обращений со стороны объектов СОМ. В процессе регистрации выполняются следующие действия.

  •  Имя объекта (в нашем случае CalledViaCOM.CallFromCOM) заносится в реестр с ключом HKEY_LOCAL_MACHINE/Software/> Также в реестре сохраняется CLSID объекта.
  •  CLSID заносится в реестр с ключом HKEY_LOCAL_MACHINE/Sof tware/. Также создается под ключ InProcServer32co следующей дополнительной информацией.
  •  Параметр по умолчанию определяет библиотеку DLL, реализующую объект. Как ни странно, это библиотека .NET mscoree.dll! «Обертка» CCW создается исполнительной средой .NET, поэтому считается, что объект реализуется именно этой библиотекой.
  •  Параметр Assembly содержит сильное имя сборки с версией и открытым ключом, однозначно идентифицирующим сборку (см. главу 16).
  •  Параметр CodeBase определяет местонахождение файла DLL сборки. Таким образом, сборка не обязана находиться в глобальном кэше или в каталоге использующего компонента.
  • Параметр RuntimeVersion содержит версию исполнительной среды .NET, необходимую для работы сборки.
  •  Также существуют параметры для имени класса (каким оно представляется приложению СОМ) и потоковой модели.
  • Для сборки создается библиотека типов, которая регистрируется с ключом

HKEY_LOCAL_MACHINE/Software/b.

В .NET Framework входит утилита TlbExp, позволяющая вручную экспортировать библиотеку типов, и утилита RegAsm, регистрирующая сборку для использования в СОМ. Возможно, вы будете использовать RegAsm для регистрации сборок при их установке (например, утилита RegAsm позволяет создать .REG-файл, упрощающий регистрацию распространяемых вами сборок), но большинство разработчиков в процессе работы использует встроенные средства регистрации Visual Studio.

Проект VBGNetTest в каталоге проекта CalledViaCom демонстрирует вызов методов объекта .NET с поздним и ранним связыванием.

Private Sub cmdLate_Click()

 Dim c As Object

Set с = CreateObjectC'CalledViaCOM.CallFromCOM")

 MsgBox c.TimesTwo(S), vblnformation, "Result from .NET component"

End Sub

Private Sub cmdEarly_Click()

Dim с As New CallFromCOM

MsgBox c.TimesTwo(5) , vblnformation, "Result from .NET component" 

End Sub

Построение компонента

Проведите следующий эксперимент.

1. Постройте программу VBGNetTest и убедитесь в том, что она работает.

2. Закройте VB6.

3. Постройте сборку CalledViaCom заново.

4. Попробуйте запустить исполняемый файл VBGNetTest (не запуская среды VB6!).

Вы увидите, что позднее связывание по-прежнему работает, но вызовы с ранним связыванием завершаются неудачей. Почему это происходит?

Сравните библиотеку типов, полученную при первой регистрации сборки (листинг 15.5) с библиотекой типов, полученной при следующей регистрации (листинг 15.6).

ПРИМЕЧАНИЕ

 Значения UUID и ID, используемые в вашем случае, будут отличаться от приведенных в листингах 15.5 и 15.6.

Листинг 15.5. Исходная библиотека типов для сборки CalledViaCom

// Файл .IDL (сгенерирован OLE/COM Object Viewer)

//

// typelib filename: CalledViaCOM.tlb

[ uuid(6431A4C9-B2F9-40FB-9420-301B96E2FC8E),

version(l.0),

helpstringC'MovingToVB.NET Example called via COM") 

library CalledViaCOM

{

// TLib: // TLib: Common Language Runtime Library :

 {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}

  importlibC'mscorlib. tlb");

// Опережающее объявление всех типов, определенных в библиотеке

 interface _CallFromCOM;

[

odl,

uuid(340729AC-2E20-3909-A94A-15EF27ED5F04), 

version(l.0), 

dual, 

oleautomation,

custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},

 "CalldViaCOM._CallFromCOM") ]

interface _CallFromCOM : IDispatch {

 [id(0x60020000)]

 HRESULT TimesTwoC 

[in] long i,

[out, retval] long* pRetVal); 

}; 

[

uuid(541F4403-04F3-39B8-83AE-35AD26964015), 

version(l.O),

Custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},

 "CalldViaCOM._CallFromCOM")

]

coCOM {

 interface _0bject;

  [default] interface _CallFromCOM; 

}: 

}:

Листинг 15.6. Библиотека типов сборки CalledViaCom после повторного построения

// Файл .IDL (сгенерирован OLE/COM Object Viewer)

//

// typelib filename: CalledViaCOM.tlb

[

uuid(6431A4C9-B2F9-40FB-9420-301B96E2FC8E),

 version(1.0),

 helpstringC'MovingToVB.NET Example called via COM")

]

library CalledViaCOM

{

// TLib: // TLib: Common Language Runtime Library :

{BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}

importlibC'mscorlib. tlb");

// TLib: OLE Automation: {00020430-0000-0000-C000-000000000046}

importlib("stdole2.tlb");

// Опережающее объявление всех типов, определенных в библиотеке

 interface _CallFromCOM;

[

odl,

uuid(340729AC-2E20-3909-A94A-15EF27ED5F04). version(l.0),

  dual, oleautomation,

custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},

 "CalldViaCOM._CaUFroinCOM") ]

interface _CallFromCOM : IDispatch 

{

  [id(0x60020000)] HRESULT TimesTwo( [in] long i ,

[out, retval] long* pRetVal);

 }: 

[

uuid(D54C0514-33B9-351F-BFll-923397721F88),

  version(1.0),

custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9},

 "CalldViaCOM._CallFromCOM")

]

coCOM { interface _0bject; [default] 

interface _CallFromCOM;

 };

 };

Как видно из листинга 15.6, итоговая библиотека типов очень похожа на исходную. Тем не менее идентификатор UUID объекта CalledViaCOM.CallFromCOM изменился.

Идентификатор UUID библиотеки типов остался прежним только потому, что Visual Studio .NET по умолчанию включает UUID библиотеки типов в атрибуты сборки, для чего используется строка вида: 

<Assembly: Guid("6431f4c9-b2f9-40fb-9420-301b96e2fc8e")>

Значения UUID библиотек типов в листингах 15.5 и 15.6 совпадают со значением, заданным атрибутом Assembly :GUID, хотя обычно каждой сборке присваивается уникальное значение GUID.

Интересно заметить, что UUID интерфейса изменяется лишь при фактической модификации интерфейса. Напрашивается предположение, что VB .NET старается по возможности сохранять идентификаторы интерфейсов, осуществляя внутреннюю проверку совместимости.

Для позднего связывания достаточно имен объекта и метода, поэтому вполне логично, что вызовы с поздним связыванием продолжают работать и после того, как сборка была построена заново. Однако все внутренние идентификаторы объектов, интерфейсов и т. д. могут при этом измениться, что приведет к сбоям при вызовах с ранним связыванием.

 

К счастью, эта проблема легко решается.

Создание компонента CalledViaCOM — вторая попытка

Фокус заключается в том, чтобы самостоятельно назначить идентификаторы UUID и Dispid разным составляющим сборки.

ВНИМАНИЕ 

 Приведенные ниже значения UUID выбраны только для пояснения материала. Не исполь-зуйте эти числа в своих приложениях.

В файле Assemblyinfo.vb проекта CallFromCom2 UUID библиотеки типов задается следующим образом: 

<Assembly : Guid("c392d911-7806-43e2-a61d-ad3cd3e2a8f3")>

В листинге 15.7 приведен обновленный файл P>

Листинг 15.7. Файл t> для сборки CallFromCom2

Imports System.Runtime.InteropServices

<Guid("fda97dca-bb0b-4987-961b-8383741cfa8f")> Interface _CallFromCOM2

 <DispId(l)> Function TimesTwo(ByVal i As Integer) As Integer 

<DispId(2)> Function BadWay(ByVal i As Integer) As Object

End Interface

<Guid("72d7e45a-3b76-4al2-bb7e-c096cd97709a")> Public COM2 Implements _CallFromCOM2

<DispId(l)> Public Function TiraesTwo(ByVal i As Integer) As Integer _ Implements _CallFromCOM2.TimesTwo

Return i * 2 End Function

<DispId(2)> Public Function BadWay(ByVal i As Integer) As Object Implements _CallFromCOM2.BadWay

Return i*2

 End Function

<ComVisible(False), Displd(3)> Public Function TimesThree(ByVal i As Integer) As Integer

Return i * 3

 End Function

<ComRegisterFunction()> Public Shared Sub OnRegistration(ByVal Т _ As Type)

MsgBox("I'm being registered!!! :" &T.FullName) 

End Sub 

End

Атрибут Guid, указанный перед классом, определяет UUID объекта. Значения UUID интерфейса и Dispid задаются внутри сборки.

Для получения величин, используемых в качестве значений атрибута Guid, можно воспользоваться утилитой uuidgen.exe из каталога утилит Visual Studio1.

А теперь попробуйте выполнить перечисленные действия.

1 По умолчанию используется каталог <диск>:\Ргадгат Files\Microsoft Visual Studio.NET\Common7\Tools.

1. Постройте программу VB6NetTest из каталога проекта CalledViaCOM2 и убедитесь в том, что она работает.

2. Закройте VB6.

3. Постройте сборку CalledViaCom2 заново.

4. Попробуйте запустить исполняемый файл VBGNetTest.

 На этот раз программа нормально работает.

Описанный подход фактически обеспечивает совместимость сборки на двоичном уровне.

Впрочем, в этом решении кроется один подвох.

ВНИМАНИЕ 

 Обеспечивая двоичную совместимость, проследите за тем, чтобы все было сделано правильно. В отличие от VB6 VB .NET не обеспечивает двоичной совместимости и не предупреждает о ее нарушении!

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

Дело даже не в дефектах или ограниченности VB .NET, а в самой природе COM. VB .NET предоставляет в ваше распоряжение средства, позволяющие обеспечить двоичную совместимость так, как программисты C++ делали в течение многих лет, однако программистам VB эта область незнакома. Как говорилось выше, все эти трудности возникают лишь при создании компонентов, предназначенных для использования в СОМ.

Впрочем, проблемы раннего связывания можно решить и другим, более простым способом, а именно — ограничиться поздним связыванием. Это делается так.

  •  Не включайте строку <Assembly: (Type.None)> в файл Assemblylnfb.vb.
  • Не создавайте отдельный интерфейс для вашего класса.
  •  Не устанавливайте атрибуты GUID и Dispid.

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

Учитывая, что большинство программистов VB не будет создавать компоненты .NET для использования в СОМ, такая стратегия вполне приемлема.

 

Дополнительные обстоятельства

При обращении к объектам, NET в СОМ следует учитывать ряд дополнительных обстоятельств.

  •  При обращении к объектам из СОМ используются только конструкторы по умолчанию. Параметризованные конструкторы игнорируются.
  •  Общие члены классов в СОМ недоступны.
  •  Только первые две части номера версии сборки преобразуются в номер версии библиотеки типов.
  •  Идентификатор программы (ProgID) объекта .NET в СОМ состоит из названия пространства имен, объединенного с названием объекта. Значение может быть изменено при помощи атрибута Prog ID.
  •  Параметры и возвращаемые значения, определенные с типом As Object, преобразуются в тип COM Vari ant. Использовать их не рекомендуется.
  •  Объект, освобожденный объектом СОМ, уничтожается сборщиком мусора по стандартным правилам, как и все остальные объекты .NET.
  •  Проблемы передачи структур, которые не рассматривались при описании использования компонентов СОМ в .NET, действуют и в этом направлении. Данная тема рассматривается в разделе «Использование функций Win32 API».

Программистам, занимающимся решением нетривиальных или нестандартных задач в СОМ, следует изучить документацию .NET Framework. Разработчики транзакционных компонентов найдут в ней дополнительные сведения о том, как эти приемы следует применять к компонентам, используемым в СОМ+. Впрочем, для большинства программистов Visual Basic приведенной информации будет вполне достаточно.

Использование функций Win32 API

Возвращаясь к истории Visual Basic, мы видим, что в эпоху Visual Basic 1 при написании сколько-нибудь профессионального приложения практически неизбежно приходилось пользоваться Win32 API. По мере развития VB программисты продолжали часто обращаться к функциям Win32 API. Одни при помощи функций API получали доступ к средствам операционной системы, недоступным на уровне VB. Другие использовали API в целях оптимизации, например при выполнении сложных графических операций. Даже по мере того, как компания Microsoft расширяла функциональные возможности компонентов и оболочек (wrappers) COM, функции Win32 API занимали ключевое место в инструментарии любого серьезного программиста VB.

Эти времена прошли.

Поймите меня правильно: некоторые возможности не поддерживаются классами .NET Framework, поэтому вы должны уметь вызывать функции API. Тем не менее пользоваться ими следует как можно реже.

В этой главе изложена большая часть того, что необходимо знать о вызове функций Win32 API в VB .NET. Существует несколько причин, по которым рекомендуется избегать вызова функций Win32 API в VB .NET.

  •  Для вызова функций Win32 API программа должна иметь право выполнения неуправляемого кода (безопасность в .NET рассматривается в следующей главе). Пока достаточно сказать, что при вызове функции Win32 API сборке предоставляется максимально возможный уровень привилегий. Это затрудняет распространение сборки среди получателей, не пользующихся абсолютным доверием.
  •  Вызовы функций Win32 API в меньшей степени защищены от ошибок. Поскольку они выполняются в неуправляемом коде, вы можете столкнуться с ошибками защиты памяти, утечкой памяти и ресурсов и всех типичных ошибок, хорошо знакомых программистам, работающие в API на любых языках.
  •  Функциями Win32 API труднее пользоваться, чем эквивалентными средствами .NET Framework.

А если вам все-таки приходится использовать функции Win32 API, инкапсулируйте их в классах вместо непосредственного вызова в программе. Также рассмотрите возможность выделения всех классов, использующих неуправляемый код, в отдельные сборки. Если вы твердо убеждены в том, что злонамеренное использование ваших объектов невозможно, сообщите .NET об их безопасности при помощи метода Assert. Проблем с распространением это не решит (поскольку сам вызов Assert требует высокой степени доверия), но после установки сборка сможет безопасно использоваться менее надежным кодом. Я понимаю, что все это выглядит крайне запутанно, поскольку вопросы безопасности в .NET еще не рассматривались, но не огорчайтесь — ситуация прояснится в следующей главе.

 

Эволюция команды De

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

  •  как найти файл (DLL или ЕХЕ), содержащий программный код компонента, и создать экземпляр компонента в случае необходимости;
  •  как найти нужный код (имя метода или точку входа) в компоненте;
  •  как организовать передачу параметров в неуправляемый компонент.

При работе с объектами СОМ основная часть информации, необходимой для решения этих трех задач, сосредоточена в библиотеке типов. При вызове функций API или DLL все необходимые сведения должны определяться в команде De

Вы должны хорошо понимать, чем компонент DLL, предоставляющий свою функциональность через СОМ, отличается от обычных компонентов с традиционным экспортированием функций. Доступ к функциональности DLL может быть организован тремя способами.

1. Реализация в виде сборки .NET. Вы загружаете сборку и работаете с ее объектами и методами при помощи .NET CLR.

2. Реализация в виде объектов СОМ. Вы требуете у DLL создать объект заданного типа, после чего вызываете методы этого объекта.

3. Экспортирование функций (исходный способ предоставления доступа к функциональным возможностям DLL). Win32 API состоит из тысяч экспортируемых функций, реализованных базовыми DLL из состава Windows.

Некоторые программисты VB не видят принципиальных различий между этими подходами. Сборки Visual Basic .NET и С# не могут экспортировать функции в традиционном смысле этого слова, они могут лишь предоставлять доступ к объектам и их методам через .NET CLR. В Visual Basic 6 ActiveX DLL не могут экспортировать функции без помощи компонентов независимых фирм (наподобие Desaware SpyWorks): на уровне языка такая возможность не поддерживается.

Тем не менее Visual Basic .NET, C# с Visual Basic 6 могут работать с функциями, экспортируемыми DLL. Когда речь заходит о вызове функций Win32 API, мы по-прежнему говорим о вызове экспортируемых функций, реализованных в DLL операционной системы.

В соответствии с документацией .NET синтаксис команды De так:

[Public | Private | Protected | Friend | Protected Friend] De | Unicode | Auto] [Sub] имя Lib "имя_библиотеки" [Alias "псевдоним"] _ [([аргументы])]

Или:

[Public | Private | Protected | Friend | Protected Friend] De | _ Unicode | Auto] [Function] имя Lib "имя_библиотеки" [Alias "псевдоним"] _ [([аргументы])] [As тип]

Первая часть объявления ([Public | Private | Protected | Friend | Protected Friend]) просто описывает область видимости, в которой может использоваться функция.

Никогда не используйте объявления с ключевым словом Public: невозможно найти разумные причины для того, чтобы внешний код вызывал функции DLL через вашу сборку.

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

Секция [Ansi | Unicode | Auto] передает CLR информацию о том, как должны обрабатываться строки.

Windows NT/2000/CP основаны на технологии NT, которая (как и VB6 и VB .NET) использует во внутреннем представлении строковых данных кодировку Unicode. Однако Windows 95/98/ME основаны на устаревшей технологии1и используют внутреннюю кодировку ANSI. Чтобы в системах на базе Unicode могли использоваться как ANSI-, так и Unicode-программы, в этих системах экспортируются две версии большинства функций API, использующих строки. Например, функция API GetWindowText в действительности экспортируется дважды с именами GetWindowTextA (ANSI) и GetWindowTextW (Unicode).

В Visual Basic 6 всегда используются точки входа ANSI, чтобы создаваемые приложения были совместимы с операционными системами на базе ANSI и Unicode. Обычно в поле Alias (см. ниже) задается точка входа с суффиксом А. В результате в Unicode-системах происходит нечто странное: при вызове функции API VB6 сначала преобразует все строковые параметры в ANSI, затем вызывает ANSI-функцию DLL операционной системы, а затем преобразует строки в Unicode для нормальной работы операционной системы!

1Хотите — верьте, хотите — ист, по эти системы нее еще содержат 16-разрядный код.

Visual Basic .NET позволяет обойтись без многократных преобразований и выбрать нужную точку входа. Если вы хотите использовать точку входа ANSI, укажите ключевое слово Ansi и имя точки входа ANSI в секции Alias. Чтобы использовать точку входа Unicode, укажите ключевое слово Unicode и имя соответствующей точки входа в секции Allas. Если функция не использует строки, эти три параметра указывать не обязательно — достаточно указать точное имя точки входа в секции имени или псевдонима команды De

Но в большинстве случаев указывается параметр Auto. В этом случае CLR автоматически выполняет ряд действий.

  •  CLR проверяет, существует ли заданная точка входа.
  •  Если точка входа не найдена, CLR присоединяет к имени суффикс, соответствующий текущей операционной системе (W в Unicode-системах, А в ANSI-системах), и ищет полученное имя.
  •  После обнаружения точки входа все строки автоматически преобразуются в соответствии с ее типом.

Параметр Auto нормально работает для большинства функций API. В некоторых функциях (например, в функциях OLE) кодировка выбирается независимо от операционной системы, поэтому функция имеет всего одну точку входа. В этом случае кодировка выбирается в соответствии с документацией.

Псевдоним (Alias) задается в тех случаях, когда в приложении используется имя, отличное от имени точки входа, например, если имя экспортируемой функции API совпадает с ключевым словом языка Visual Basic.

Список аргументов рассматривается ниже.

По сравнению с Visual Basic 6 в команду Deыл внесен ряд усовершенствований.

  •  Visual Basic .NET требует объявлять тип возвращаемого значения, благодаря чему ликвидируется одна из самых распространенных ошибок VB6, когда программист забывает указать тип возвращаемого значения, что приводит к ошибке «Bad DLL Calling Convention», так как функция API возвращает 32-разрядное число, a VB6 рассчитывает получить Variant.
  •  В абсолютном большинстве функций API используется традиционная схема передачи параметров (конвенция PASCAL). По умолчанию она применяется и в функциях, объявленных командой DeТем не менее при помощи атрибута Calling-Convent ion в команде Deожно выбрать схему передачи параметров С (Cdecl). Обычно это приходится делать при работе DLL независимых фирм, когда разработчики случайно оставили схему передачи параметров С.
  •  Visual Basic .NET в текущей бета-версии распознает ошибки передачи параметров DLL менее эффективно, чем Visual Basic 6.
  •  Visual Basic .NET не поддерживает параметры As Any.

Три главных правила, о которых необходимо помнить при вызове функции API из VB .NET

Даже если вы совсем ничего не поймете в этом разделе, запомните три главных правила.

Даже в VB .NET вызовы функций API могут быть опасными

Ошибка при объявлении функции может привести к исключениям защиты памяти.

Помните о ключевом слове ByVal

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

Вместо Long следует использовать тип Integer

В VB .NET тип данных Long является 64-разрядным. В приложениях VB .NET использование типа Long вместо Integer часто приводит к снижению быстродействия, а в объявлениях API — вызывает ошибки и даже исключения защиты памяти.

 

Подсистема P-Invoke

Как ни странно, многие программисты VB6 громко жалуются на то, что разработчики Microsoft убрали из VB .NET «скрытые» операторы VarPtr, StrPtr и ObjPtr. Но они не понимают, что эти операторы стали ненужными. Их функциональные возможности не исчезли, а просто переместились в .NET Framework. В сущности, весь процесс вызова функций Win32 API вообще не является частью языка VB .NET — команда Deсего лишь инкапсулирует подсистему .NET Framework, которая называется P-Invoke (сокращение от Platform Invocation).

Управление работой подсистемы P-Invoke осуществляется при помощи методов пространства имен System.Runtime. InteropServices. Из-за этого нередко возникает путаница, поскольку не всегда понятно, какие объекты и методы этого пространства имен относятся к COM Interop, какие — к P-Invoke, а какие применимы в обеих областях. Ситуация усложняется тем, что атрибуты, управляющие передачей параметров при вызове функций Win32 API, также управляют передачей параметров при вызове методов СОМ!

Поэтому передача параметров не рассматривалась в части этой главы, посвященной СОМ, — они ничем не отличаются от параметров, передаваемых при вызове функций Win32 API. А поскольку COM Interop весьма разумно действует по умолчанию, приведенный ниже материал с большей вероятностью пригодится при вызове функций Win32 API, нежели в COM Interop.

Становится понятно, почему мы ничего не теряем с исчезновением VarPtr, StrPtr и Obj Ptr. Все «хакерские» приемы, основанные на использовании этих функций, значительно проще и надежнее реализуются средствами P-Invoke. Подумайте: все объекты .NET, использующие возможности операционной системы, от форм Windows до Winsock, должны использовать P-Invoke. Неудивительно, что P-Invoke не только справляется со всеми задачами, связанными с API, но и весьма стабильно работает.

Конечно, из этого вовсе не следует, что в P-Invoke легко разобраться.

 

Атрибуты передачи параметров

Существует ряд атрибутов, которые могут указываться перед параметрами (и возвращаемым значением) в объявлениях функций API. Для правильного вызова сложных функций API необходимо хорошо понимать смысл этих атрибутов.

  •  MarshalAs. Атрибут указывает, как CLR следует передавать данные (например, должна ли строка передаваться в виде указателя на строку, завершенную нуль-символом, или в строковом формате OLE BSTR).
  • UnmanagedType. Используется в сочетании с атрибутом MarshalAs для описания передачи параметров.
  •  Marshal. Объект содержит большое количество общих методов для выполнения исключительно разнообразных задач, связанных с управлением памятью. Например, вы можете выделить блок неуправляемый памяти и вручную передавать данные в память с использованием атрибутов передачи параметров.
  •  GCHandle. Атрибут позволяет зафиксировать местонахождение объекта в управляемой памяти и получить указатель, по которому можно обращаться из неуправляемой памяти. На первый взгляд такая возможность выглядит весьма заманчиво, но на практике почти не используется.

Осваиваем P-Invoke

В процессе работы над этой главой я столкнулся с двумя проблемами.

  1. Поскольку в Win32 API входит более 9000 функций, я не смогу ни предвидеть все нюансы каждой функции, ни изложить этот материал в ограниченном пространстве.
  2.  Мне неизвестна ваша квалификация. Если бы я написал эту главу для новичков в области Win32 API, только на описание базовых концепций потребовалось бы не менее сотни страниц.

Итак, я буду исходить из двух предположений. Во-первых, у вас уже имеется некоторый опыт вызова функций API в Visual Basic — достаточный, чтобы я мог сказать «как в VB6» и быть уверенным в том, что в дальнейшем вы разберетесь. Во-вторых, вы готовы читать документацию и заниматься самостоятельными исследованиями1.

Руководствуясь этими предположениями, я сначала покажу, как узнать точные значения параметров, переданные при вызове функции API. Я сам пользовался этим приемом, выясняя, как различные атрибуты влияют на передачу параметров.

1А если не готовы, я могу провести консультации в частном порядке, только это обойдется недешево.

Проект VBInterface

Откройте решение из каталога VBInterface (не обращайте внимания на файлы .срр). В результате открываются два проекта: VBInterface и VBInterfaceTest.

Программа VB .NET VBInterfaceTest демонстрирует вызов функций DLL с разными параметрами. Программа VBInterface представляет собой библиотеку DLL, экспортирующую функции. Библиотека написана на C++ и состоит из неуправляемого кода.

Оба проекта загружаются одновременно; проект VBInterfaceTest выбирается в качестве стартового. Для совместной отладки проектов остается лишь вызвать диалоговое окно свойств проекта VBInterfaceTest, выбрать команду Configuration Properties> Debugging на левой панели и убедиться в том, что флажок Unmanaged Code Debugging установлен.

Начнем с простого примера.

Функция ReceivesShortDLt определяется в файле VBInterface.срр следующим образом:

STDAPI_(short) ReceivesShort (short x)

{

_itoa(x, tbuf, 10); /* Разместить во временном буфере */ MessageBox(GetFocus(),(LP5TR)tbuf,(LPSTR)"ReceivesShort", MB_OK); return(x);

}

Функция выводит окно сообщения с полученной 16-разрядной величиной и возвращает это же значение. Объявление в файле VBInterface выглядит так:

Public Denction ReceivesShort Lib _ "..\..\VBInterface\Debug\VBInterface.dll" (ByVal s As Short) As Short

Пример вызова в функции Numbers программы VBInterfaceTest:

i = ReceivesShort(4) ; Console.WriteLine(i)

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

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

Итак, вы познакомились с первым приемом: если у вас возникли трудности с правильным объявлением и передачей параметров при вызове функций API, создайте в файле VBInterface.срр функцию, объявление которой точно соответствует объявлению вызываемой функции API на языке С, и попробуйте вызвать эту функцию из программы VB. Это позволит вам узнать точные значения параметров, полученные функцией API.

 

Секреты передачи параметров

На простом уровне передача большинства типов данных обходится без проблем. Если вы знакомы с вызовом функций API в VB6, единственное, о чем следует помнить (не считая особого подхода к структурам, о котором будет сказано ниже), — это замена типа Long типом Integer. В табл. 15.1 перечислены основные типы параметров Win32 API (согласно документации Win32 API) и эквивалентные определения в команде De

Таблица 15.1. Передача простых параметров функциям API

Тип параметра

Объявление

BYTE

ByVal As Byte

CHAR, Char

ByVal As Char

SHORT, USHORT, WORD

ByVal As Short

int, INT, long, DWORD, ULONG и т. д.

ByVal As Integer

LPBYTE

ByVal As Byte

LPCHAR

ByVal As Char

LPSTR

ByVal As String

LPSHORT, LPWORD

ByVal As Short

LPDWORD, LPLONG

ByVal As Integer

LPxxxPROC (указатель на функцию)

ByVal As Delegate (тип делегата) См. проект Delegates в главе 10

Внимательнее с типом Long!

Как я уже говорил, самой распространенной ошибкой программистов VB .NET при работе с Win32 API будет использование 64-разрядного типа Long для 32-разрядных параметров. Проект VBInterface показывает, что происходит при ошибочной передаче данных типа Long. Файл VBInterface.cpp:

STDAPI_(_int64) ReceivesLong(_int64 у) {

DumpValues((LPVOID)&y, 8);

return(y+0xl000200030004000); }

Модуль Modulel.vb проекта VBInterfaceTest:

Public Denction ReceivesLong Lib _ "..\..\VBInterface\Debug\VBInterface.dll" (ByVal a As Long) As Long

1 = ReceivesLong(4) ;

Console.WriteLine(Hex$(l))

Результат в окне сообщения: 00 40 00 30 00 20 00 10.

Возвращаемое значение: 2000400060008000.

Данные в окне сообщения выглядят несколько странно. Вывод шестнадцатеричного дампа памяти осуществляется функцией DumpValues. В PC память организована таким образом, что младшая часть числового значения расположена по младшему адресу памяти. Таким образом, байтовая последовательность 00 40 соответствует числу &Н4000.

Так или иначе, данный пример еще раз подчеркивает необходимость использования типа Integer вместо Long в объявлениях API. Конечно, при передаче типа .NET Long (64-разрядного) функции Win32 API, рассчитывающей на получение типа long C++ (32-разрядный), может произойти серьезная ошибка.

Передача строк

При вызове функций Win32 API, получающих строковые параметры, строки практически всегда объявляются в виде ByVal As String.

Вспомните, что говорилось в главе 9 о возможности модификации объектов, переданных по значению, и о неизменности строк. VB .NET идет на небольшое жульничество. Хотя в объявлении указано ключевое слово ByVal, VB .NET присваивает переменной типа String, переданной в качестве параметра, новое значение, совпадающее со значением строки после возврата из функции.

Иначе говоря, перед нами один из тех случаев, когда синтаксис VB .NET совместим с синтаксисом VB6. Мне это кажется довольно странным, поскольку в VB6 этот синтаксис выглядел нелогично. Странно, что этот дефект не был исправлен в процессе чистки языка. Впрочем, на передачу строковых параметров все равно можно повлиять при помощи атрибутов.

Начнем с рассмотрения функций DLL, не изменяющих строк.

Функции ReceivesANSIString и ReceivesUnicodeString предназначены для получения и вывода строк в кодировках ANSI и Unicode. Функция ReceivesAutoString выводит дамп полученного буфера и наглядно показывает, какой формат используется в конкретной операционной системе.

/* Используется для большинства функций API. VBcalls.

VB передает строку, завершенную нуль-символом.

*/ STDAPI_(VOID) ReceivesANSIString(LPSTR tptr)

{

MessageBox(GetFocus(), (LPSTR)tptr, (LPSTR)"ReceivesANSIString", MB_OK);

}

STDAPI_(VOID) ReceivesUnicodeString(LPWSTR tptr)

{

MessageBoxW(GetFocus(), tptr, L"ReceivesUnicodeString", MB_OK); }

STDAPI_(VOID) ReceivesAutoString(LPSTR tptr, int count) 

{

DumpValues(tptr, count); 

}

В листинге 15.8 приведены объявления и примеры вызовов этих функций в проекте VBInterfaceTest.

Листинг 15.8. Примеры использования строковых функций в проекте VBInterfaceTest

Public Deub ReceivesANSIString Lib _

"..\..\VBInterface\Debug\VBInterface.dU" (ByVal s As String)

Public De Sub ReceivesUnicodeString Lib _

"..\..\VBInterface\OebugWBInterface.dll" (ByVal s As String)

Public Deb ReceivesAutoString Lib _

"..\..\VBInterface\Debug\VBInterface.dll" (ByVal s As String,

ByVal chars As Integer)

Public DeeivesNoInfoString Lib _

"..\..\VBInterface\Debug\VBInterface.dll"

Alias "ReceivesAutoString" (ByVal s As String, ByVal chars As Integer)

Dim s As String = "Test string"

Dim s2 As String

Console.WriteLine ("Strings examples")

ReceivesANSIString (s)

ReceivesUnicodeString (s)

s2 = s

ReceivesAutoString(s, Len(s))

If Not s2 Is s Then

Console.WriteLlne _

("s and s2 are no longer the same after after ReceivesAutoString") 

End If ReceivesNoInfoString(s, Len(s))

Проанализируем полученный результат.

Функция ReceivesAnsiString правильно получает и выводит строку «Test String». To же самое можно сказать и о функции ReceivesUnicodeString. Это доказывает, что атрибуты Ansi и Unicode обеспечивают передачу строковых параметров в заданном формате.

Функция ReceivesAutoString выводит последовательность 54 00 65 00 73 00 74 00 20 00 73. Несомненно, перед нами строка Unicode (поскольку программа выполнялась в Windows 2000, а эта система использует кодировку Unicode). Последовательность усечена, поскольку количество байт вдвое больше количества символов в строке, но и приведенный фрагмент однозначно показывает, что перед нами строка Unicode.

Любопытная подробность: хотя строка передавалась с ключевым словом ByVal, на экране появляется сообщение о том, что строка изменилась. В данном случае поведение VB .NET не соответствует общепринятой трактовке атрибута ByVal.

При отсутствии заданной кодировки выводится последовательность 54 65 73 74 20 73 74 72 69 6е 67. Это наглядно доказывает, что по умолчанию в VB .NET используется кодировка ANSI (и это вполне логично для сохранения совместимости с VB6). Тем не менее атрибут кодировки всегда следует задавать явно.

В следующем примере (листинг 15.9) функция модифицирует переданные строки. Приведенный фрагмент взят из файла VBInterface.cpp.

Листинг 15.9. Примеры использования строковых функций в проекте VBInterfaceTest (продолжение)

/* Модификация строки (при условии, что программа не выходит

 за границы выделенного буфера */

5TDAPI_(VOID) ChangesStringA(LPSTR tptr) 

{

if Ctptr) *tptr = 'A' ; 

}

STDAPI_(VOID) ChangesStringW(LPWSTR tptr)

 {

if(*tptr) «tptr = 'W;

 }

STDAPI_(VOID) ChangesByRefStringW(LPWSTR *ptr) 

{

lstrcpyW(*ptr, L"New String"); 

}

STDAPI_(VOID) ChangesByRefStringACLPSTR »ptr) 

lstrcpyA(*ptr, "New String");

}

STDAPI_(VOID) ChangesBSTRString(BSTR *sptr)

 {

if(lsptr) return; 

// Условие никогда не выполняется (перестраховка)

if(*sptr) SysFreeString(*sptr);

*sptr = SysAllocString(L"Any Length Ok");

 }

/* Возвращение строки при вызове функции DLL. */

STDAPI_(BSTR) ReturnsVBStringO

{

return(SysAllocString(L"Here's a return string")); 

}

В проекте VBInterfaceTest используется следующий код:

Public De ChangesString Lib _ "..\..\VBInterface\Debug\VBInterface.dll" (ByVal s As String) 

Public De ChangesByRefString Lib _ "..\..\VBInterface\Debug\VBInterface.dll" (ByRef s As String) 

Public DeSub ChangesBSTRString Lib _ "..\..\VBInterface\Debug\VBInterface.dll" (<MarshalAs(UnmanagedType.BStr)> ByRef s As String) 

Public DeFunction ReturnsVBString Lib _ "..\..\VBInterface\Debug\VBInterface.dll" () As <MarshalAs(UnmanagedType.BStr)> 

String

s2 = s

ChangesString (s) If Not s2 Is s Then

Console.WriteLine ("s and s2 are no longer the same after ChangesString") End If

Console.WriteLine ("Changed String; " & s)

 ChangesByRefString (s)

Console.WriteLine ("Changed ByRef String: " & s)

 ChangesBSTRString (s)

Console.WriteLine ("Changed BSTR String: " & s) 

s = ReturnsVBString() 

Console.WriteLine ("Returned String: " & s)

Результат выглядит следующим образом:

s and s2 are no longer the same after ChangesString 

ChangedString: West string

 Changed ByRef String: New String 

Changed BSTR String: Any Length Ok 

Returned String: Here's a return string

Функция ChangesStrl ng изменяет один символ в строке, переданной по значению. Как и в VB6, необходимо действовать очень осторожно, чтобы не изменить данные за концом строки. Если длина строки, заданная при инициализации, недостаточна для хранения возвращаемых данных, вызов почти наверняка приведет к исключению защиты памяти.

У функции ChangesString есть одна любопытная особенность: она наглядно показывает, как работает атрибут Auto при объявлении функции. В объявлении ChangesString не указан псевдоним ChangesStringA или ChangesStringW, но результат доказывает, что в DLL была найдена точка входа ChangesStringW (предполагается, что программа работает в операционной системе, использующей кодировку Unicode).

При передаче строки с ключевым словом ByRef функция получает указатель на переменную, содержащую указатель на строковый буфер. Не пытайтесь присваивать значение этой переменной, поскольку это лишь приведет к сбою в программе. Правило о предварительном выделении буфера достаточного размера действует и в этом случае. Данный тип параметра (LPSTR) используется лишь небольшим подмножеством функций Win32 API. В таких случаях можно объявить параметр ByRef As Integer, а затем при помощи объекта Marshal получить строку но указателю, полученному при вызове функции API1. Кстати, в этом отношении VB .NET отличается от VB6, где для параметров ByRef As String передается указатель на BSTR (строковый тип OLE).

1Методы Marshal.PtrToStringAnsi, Marshal.PtrToStringAuto и Marshal.PtrToStringUni позволяют получить объект .NET типа S t M n g по указателю на блок неуправляемой памяти. Эти методы работают аналогично методу Marshal. PtrToStructure, встречающемуся ниже в этой главе.

Тип BSTR может передаваться при вызове функций API, хотя в общих функциях Win32 API он не используется (только в функциях подсистемы OLE). Эта возможность продемонстрирована в функциях ChangesBStrString и ReturnsVBString. Обратите внимание на атрибут MarshalAs(UnmanagedType.BSTR): он сообщает CLR о том, что переданные строки относятся к типу B5TR. Преимущество типа BSTR заключается в том, что он позволяет изменять длину строки или определить возвращаемую строку с произвольной длиной.

Хотя тип BSTR не применяется при вызовах функций Win32 API, он широко используется во внутренних операциях COM Interop.

Передача массивов

С передачей массивов нередко возникают трудности. В листинге 15.10 приведен фрагмент файла VBInterface.cpp. Функция ReceivesShortArray получает указатель на short (первое число в последовательности), а функция Recei vesShortRef Array получает указатель на переменную, содержащую адрес массива. Обе функции увеличивают значения первых двух элементов массива (показывая, что данные были скопированы в исходный массив). Функция ReturnsSafeArray показывает, как функция DLL может полностью переопределить переданный массив.

Листинг 15.10. Операции с массивами в программе C++

/* Целочисленный массив целых чисел - следите за тем,

чтобы не нарушить границы массива!

Обратите внимание на специальную схему передачи параметров

в примере VB - для строк она ие подходит.

 */

5TDAPI_(VOID) ReceivesShortArray(short FAR *iptr)

 { 

wsprintf((LPSTR)tbuf, (LPSTR)"lst 4 entries are %d %d %d %d",

*(iptr), *(iptr+l), *(iptr+2), *(iptr+3));

MessageBox(GetFocus() , (LPSTR)tbuf, (LPSTR)"ReceivesShortArray", MB_OK);

 (*iptr)++; // Увеличить элемент массива, чтобы проверить

 // передачу адресом и узнать, выполняется ли 

// операция с временной копией. 

(*(iptr+l))++; 

}

short newArrayBuffer[4] = { 5, 4, 3, 2};

STDAPI_(VOID) ReceivesShortRefArray(snort FAR **piptr)

 {

short *iptr;

iptr = *piptr;

wsprintf((LPSTR)tbuf, (LPSTR)"lst 4 entries are %d %d %d %d",

*(iptr), *(iptr+l), *(iptr+2), *(iptr+3));

MessageBox(GetFocus(), (LPSTR)tbuf, (LPSTR)"ReceivesShortArray", MB_OK);

 (*iptr)++; // Увеличить элемент массива, чтобы проверить

// передачу по ссылке и узнать, выполняется ли

 // операция с временной копией. 

(*(iptr+l))++; *piptr = newArrayBuffer; }

STDAPI_(VOID) ReturnsSafeArray(SAFEARRAY **psa) 

{

short *pdata;

pdata = (short *)

((*psa)->pvData);

wsprintf((LPSTR)tbuf, (LPSTR)"Array of %d dimensions, %ld bytes per elementXn First int entry is %d", (*psa)->cDims, (*psa)->cbElements, *pdata);

MessageBox(GetFocus(), (LPSTR)tbuf, (LPSTR)"ReturnsArray", MB_OK);

SAFEARRAYBOUND bounds[1];

long 1;

bounds[0].ILbound = 0;

bounds[0].cElements = 4;

*psa = SafeArrayCreate(VT_I2, 1, bounds);

short storeval;

ford = 0; 1<4; 1++)

 { 

storeval = (short)l * 5;

 SafeArrayPutElement(*psa, &1, Sstoreval); 

}

Начнем с ReceivesShortArray. Эта функция ожидает получить указатель на массив 16-разрядных чисел типа Short. При вызове используются два разных варианта объявления. В одном случае передается первый элемент массива, а во втором — весь массив по значению (листинг 15.11).

Листинг 15.11. Передача массивов в проекте VBInterfaceTest

Public DeivesShortArray1 Lib _

"..\..\VBInterface\Debug\VBInterface.dll" Alias_

"ReceivesShortArray" (ByRef i As Short) 

Public DeivesShortArray2 Lib _

"..\..\VBInterface\Debug\VBInterface.dll" Alias

"ReceivesShortArray" (ByVal i() As Short)

Dim i() As Short = {1, 2, 3, 4}

Dim x As Integer

ReceivesShortArrayl (i(0)) 

For x = 0 To 3 

Console.Write (Str(i(x)) & ", ")

Next x

console.WriteLine()

ReceivesShortArray2 (1)

 For x = 0 To 3

Console.Write (Strd(x)) & ", ")

 Next x console. 

WriteLine()

При первом вызове Recei vesShortArray в окне сообщения выводится последовательность «1, 2, 3, 4». Следовательно, хотя при вызове указывается только первый элемент массива, VB .NET фактически передает в неуправляемую память весь массив. Вероятно, эта возможность была добавлена в VB .NET для поддержки способа, часто используемого программистами VB6 при передаче массивов функциям API. При возвращении из функции выводится последовательность «2, 3,3,4», из чего можно сделать вывод, что измененные элементы массива успешно возвращены в массив .NET.

То же самое происходит и при передаче по значению, из чего можно сделать вывод об идентичности этих вызовов.

Функция ReceivesShortRefArray правильно получает массив (с элементами 3, 4, 3, 4), но по возвращении оказывается, что верхняя граница массива равна 0! Что произошло? Interop не может сделать обоснованных предположений относительно длины модифицированного массива, поэтому она копирует только первый элемент! Впрочем, как показано ниже, в программе можно определить массив фиксированной длины, чтобы система знала размер массива при возвращении из функции.

Public DeivesShortRefArray Lib "..\..\VBInterface\Debug\VBInterface.dll" (ByRef 1() As Short)

ReceivesShortRefArray (i)

Console.WriteLine ("Array bound is now: " & UBound(i))

console.WriteLine()

Функция ReturnsSafeArray демонстрирует передачу массива по ссылке в виде типа OLE SAFE ARRAY.

Public DernsSafeArray Lib ",.\..\VBInterface\Debug\VBInterface.dll" (<MarshalAs(UnmanagedType.SafeArray)> ByRef i() As Short)

ReturnsSafeArray (i) For x = 0 To UBound(i)

Console.Write (Str(i(x)) & ". ") Next console.WriteLine()

Тип SAPEARRAY, как и упоминавшийся выше тип BSTR, не применяется при вызове функций Win32 API (возможно, кроме функций OLE API), но широко используется во внутренней работе COM Interop.

 

Структуры

Передача параметров-структур и модификация их содержимого при вызове функций Win32 API — очень распространенное явление. Следовательно, очень важно не только правильно организовать передачу структур в неуправляемую память, но и возврат полученных данных.

Тем не менее задача усложняется рядом обстоятельств.

  •  Структуры .NET не похожи на те структуры, к которым вы привыкли. Например, поля в них могут храниться в памяти в произвольном порядке.
  •  В .NET не поддерживаются строки фиксированной длины — неотъемлемая часть многих структур Win32 API.
  •  В .NET не поддерживаются массивы фиксированной длины, также задействованные по многих структурах Win32 API.

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

Расположение полей структуры в памяти

Прежде всего, следует запомнить, что все функции API рассчитывают на определенный порядок следования полей структуры в памяти. Впрочем, это и так понятно. Тем не менее функции API не всегда подчиняются одним и тем же правилам выравнивания. В большинстве функций используется упаковка по границам байтов, то есть все поля следуют непосредственно друг за другом без заполнителей. В VB6 структуры передаются с естественным выравниванием, то есть каждое поле выравнивается по границе, кратной его размеру. Байтовые поля могут находиться где угодно, 16-разрядные поля выравниваются по границам четных байтов, а 32-разрядные поля выравниваются по границе 4 байт. Рассмотрим следующую структуру C++:

typedef struct usertypestruct {

BYTE a; // Соответствует типу VB Byte

short b;

long c;

BYTE d[4];

char e[16];

 } usertype;

При естественном выравнивании ANSI-версия этой структуры представляет собой следующую последовательность байтов (буквы обозначают байты соответствующих полей, а нули — заполнители, вставленные компилятором): 

a0bbccccddddeeeeeeeeeeeeeeee=28 байт

При выравнивании по границе байтов массив выглядит так:

 abbccccddddeeeeeeeeeeeeeeee=27 байт

В проекте VBInterface компилятор настроен на выравнивание по границе байтов, поэтому в данном случае VB6 не сможет правильно передать структуру функции DLL!1К счастью, в большинстве функции API поддерживается явное включение заполнителей. Даже несмотря на то, что такие функции используют упаковку по границе байтов, они правильно работают с VB6.

1 В таких случаях помогает компонент для упаковки/распаковки пользовательских типов, входящий в пакет Dcsaware SpyWorks.

В VB .NET тип упаковки полей определяется атрибутом StructLayout (листинг 15.12).

Листинг 15.12. Атрибут StructLayout

<Structl_ayout (LayoutKind. Sequential, Pack:=l)> Public Structure GoodStruct Public A As Byte

 Public В As Short 

Public С As Integer

<MarshalAs(UnmanagedType.ByValArray , SizeConst:=4)> Public D() As Byte <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=16)> Public E As String

Public Sub InitStruct()

A = 1

В = 2

С = 3

ReDim D(3)

D(0) = 4

D(l) = 5

D(2) = 6

0(3) = 7

E = "16 char string " ' С нуль-символом 

End Sub

 End Structure

Практически всегда используется последовательное расположение полей в структурах (LayoutKI nd . Sequent! al) и упаковка по границе байтов. Более того, атрибут StructLayout позволяет задать расположение полей в памяти с указанием точного смещения каждого поля от начала структуры. Проблемы с выравниванием уходят в прошлое!

Байтовые массивы и строки в .NET определяются в виде обычных массивов и объектов String. Массив должен инициализироваться с правильной длиной в методе Structure (для структур конструктор по умолчанию не переопределяется, поэтому не забудьте вызвать этот метод перед использованием структуры). Массив передается как при передаче по ссылке (как было показано из приведенного выше примера с массивами, именно этот способ передачи массивов функциям API является правильным). Указание размера массива обеспечивает возможность передачи массива фиксированного размера в обоих направлениях.

Для строки указан тип передачи ByValTStr — тип TSTR относится к строке, тип которой зависит от используемой операционной системы. Другими словами, в Unicode-системах строка представляется 32 байтами в кодировке Unicode. Большинство строк в структурах, используемых функциями API, фактически являются строками TSTR, тип которых зависит от использованной точки входа. Применение типа ByValTStr в структурах фактически эквивалентно использованию атрибута Auto в команде De

Определение функции ReceivesUserType для точки входа ANSI выглядит следующим образом:

/* Передача по ссылке */

"STDAPI_(VOID) ReceivesUserType(usertype FAR *u)

{

DumpValues(u, 30);

 wsprintf((LPSTR)tbuf,

(LPSTR)"usertype contains %d %d %d (%d %d %d %d) %s".

u->a, u->b, u->c, u->d[0]. u->d[l], u->d[2], u->d[3], u->e);

 MessageBox(GetFocus(), (LPSTR)tbuf, (LPSTR)"ReceivesUserTypel", MB_OK); if(u->c == 3) 

{

lstrcpy(u->e, "New Data"); v u->d[0] = 99;

 } 

}

При вызове функция выводит данные пользовательского типа в следующем виде:

 01 02 00 03 00 00 00 04 05 06 07 20 36 20 63 68 61 72 20 73 74 72 69 6е 67 20

Перед нами структура в формате с упаковкой полей по границе байтов.

Функция DLL копирует текст «New Data» в строковое поле. Вывод строки после возвращения из функции показывает, что модифицированные данные были успешно переданы в структуру. Также передается и измененное содержимое массива, о чем свидетельствует значение 99 в консольном окне.

 

Нетривиальные вызовы функций Win32 API

Если в начале этой главы вы хотя бы в общих чертах представляли, как функции API вызываются в VB6, то для большинства случаев изложенного материала вполне достаточно (если учесть, что из-за масштабов библиотеки классов .NET необходимость в вызове функций API возникает редко).

В завершение этой главы я хочу представить пару примеров нетривиальных вызовов функций Win32 API.

Получение информации о соединениях удаленного доступа при помощи функции API RasNumEntries

Функция API RasNumEntries заполняет буфер массивом структур RASENTRYNAME, содержащих информацию о соединениях удаленного доступа (создайте хотя бы пару соединений, прежде чем запускать эту программу).

ПРИМЕЧАНИЕ

Для понимания примеров RasEntries и RasGetEntry, описанных в этом разделе, требуется хорошее знание общих концепций программирования (указатели, расположение данных в памяти и т. д.) и нетривиальных приемов использования API в VB6. Я привожу эти примеры для опытных читателей, хотя разобраться в них полностью смогут далеко не все.

Структура RASENTRYNAME определяется следующим образом1:

typedef struct _RASENTRYNAME {

DWORD dwSize;

TCHAR szEntryName[RAS_MaxEntryName + 1]; 

} RASENTRYNAME;

Значение RAS_MaxEntryName равно 256.

В листинге 15.13 приведено определение структуры RASENTRYNAME из проекта RasEntries.

1 Для простоты из определения исключены новые поля, появившиеся в Windows 2000. Версия структуры определяется значением поля dwSi ze.

Листинг 15.13. Структура RASENTRYNAME

'Приложение RasEntries

' Copyright ©2091 by Oesaware Inc. All Rights Reserved

Imports System.Runtime.InteropServices Module Modulel

' 256 символов в szEntryName, буфер на 257 символов

<StructLayout(LayoutK1nd.Sequential. Pack:=4, CharSet:=Charset.Auto)> _ Structure RASENTRYNAME

 Public dwSize As Integer

<MarshalAs(UnmanagedType.ByValTStr, sizeConst:=257)>

 Public szEntryName As String Public

 Sub Init()

dwSize = Marshal.SizeOf(Me) 

End Sub

 End Structure

Если заглянуть в заголовочный файл rasapi.h, вы найдете в нем строку #include <pshpack4.h> — признак того, что поля этой структуры выравниваются по границе 4 байт вместо 1.

В листинге 15.13 также продемонстрировано использование автоматического выбора кодировки для структур. Атрибут Charset.Auto аналогичен атрибуту Auto команды Deно относится к строкам внутри структур.

Поле szEntryName передается в виде строки фиксированной длины, состоящей из 257 символов.

Поле dwSize должно содержать размер структуры в байтах. Хотя его значение можно вычислить вручную, объект Marshal содержит общий метод SizeOf, возвращающий размер структуры при передаче в неуправляемую память. Этот метод значительно надежнее функции VB6 LenB, и с его помощью можно проверять правильность задания атрибутов полей структуры. Инициализировать поле szEntryName не обязательно, поскольку атрибут MarshalAs определяет размер строки и обеспечивает передачу данных нужной длины.

Объявление функции API RasEnumEntries в документации MSDN выглядит так:

DWORD RasEnumEntries (

LPCTSTR reserved, // Зарезервировано, должно быть равно NULL

 LPTCSTR IpszPhonebook, // Указатель на полное имя файла

// телефонной книги 

LPRASENTRYNAME Iprasentryname,

// Буфер, заполняемый данными, 

// из телефонной книги.

LPDWORD Ipcb, // Размер буфера в байтах

 LPDWORD IpcEntries // Количество структур, записанных в буфер

 ):

Мы воспользуемся следующей командой De

Public Dection RasEnumEntries Lib _ "rasapi32.dll" (ByVal reserved As Integer, ByVal IpszPhoneBook As String, ByVal rasentries As IntPtr, ByRef Ipcb As Integer, ByRef IpcEntries As Integer) As Integer

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

Функция RasEnumEntries вызывается дважды: при первом вызове она возвращает количество элементов и размер массива, а при втором — непосредственные данные.

Программа вычисляет размер одной структуры RASENTRYNAME и выделяет в неуправляемой памяти блок для ее хранения. Выделение памяти осуществляется методом Marshal.AllocHGlobal.

Sub Main() 

Dim res As Integer 

Dim cb, cbentries As Integer 

Dim idx As Integer 

Dim iptr As IntPtr 

Dim SizePerStruct As Integer

SizePerStruct = Marshal.SizeOf(GetType(RASENTRYNAME))

 ' Получить размер структуры 

iptr = Marshal.AllocHGlobal(SizePerStruct)

Затем мы создаем одну структуру RASENTRYNAME, инициализируем ее и передаем в буфер методом Marshal .StructureToPtr. После вызова метода RasEnumEntries данные возвращаются в структурную переменную методом Marshal. PtrToStructure. Обратите внимание на освобождение буфера в неуправляемой памяти методом Marshal.FreeHGlobal.

Dim rasentries(G) As RASENTRYNAME

rasentries(0).Init()

cb = rasentries(0).dwSize

cbentries = 1

Marshal.StructureToPtr(rasentries(0), iptr, False)

' При первом вызове возвращается количество записей, res = RasEnumEntries(0, Nothing, iptr, cb, cbentries)

rasentries(0) = CType(Marshal.PtrToStructure(iptr, _

GetType(RASENTRYNAME)), RASENTRYNAME) Marshal.FreeHGlobal (iptr)

Если функция возвращает код 603, значит, в телефонной книге остались другие записи. В этом случае мы переобъявляем массив rasentries с новым размером, достаточным для храпения всех записей телефонной книги, а затем вручную копируем все записи в заново выделенный буфер нужного размера. Следующий фрагмент также демонстрирует инициализацию указателей на неуправляемую память в конструкторе IntPtr:

If res = 603 Then 

ReDim rasentries(cbentries - 1)

 cb = 0 iptr = Marshal.AllocHGlobal(cbentries * SizePerStruct)

For idx = 0 To cbentries - 1 

rasentries(idx).Init() 

Marshal.StructureToPtr(rasentries(idx),

 New IntPtr(iptr.Tolnt32 + cb), False)

 cb = cb + rasentries(idx).dwSize

 Next

res = RasEnumEntries(0, Nothing, iptr, cb, cbentries) 

End If

If res = 0 Then cb = 0 For idx = 0 To cbentries - 1

rasentries(idx) = CType(Marshal.PtrToStructure

(New _ IntPtr(iptr.ToInt32 + cb), GetType(RASENTRYNAME)), RASENTRYNAME)

cb = cb + rasentries(idx).dwSize console.WriteLine (rasentries(idx).szEntryName) Next End If Marshal.FreeHGlobal (iptr)

console.ReadLine()

 End Sub

End Module

Итак, средства Visual Basic .NET позволяют выделять блоки в неуправляемой памяти и копировать данные в неуправляемую память и обратно. Более того, VB .NET значительно мощнее VB6, поскольку ваши возможности не ограничиваются простым копированием данных функцией API RtlMoveMemory, часто использовавшейся в подобных ситуациях в VB. Методы Marshal. StructureToPtr HMarshal.PtrToStructure копируют данные с учетом выравнивания и позволяют управлять передачей каждого поля структуры1.

1Вероятно, вас интересует, почему я не передаю сразу весь массив структур RASENTRYNAME? Просто мне не удалось заставить работать это решение.^Передача данных массива функции API проходила нормально, но с передачей в обратном направлении возникли проблемы. Система P-Invoke умеет передавать массивы простых типов, но на момент написания книги в документации ничего не говорилось о передаче массивов структур, Пока трудно сказать, что это такое — норма или ошибка в бета-версии.

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

Получение информации об одном соединении функцией API RasGetEntryProperties

Описание конкретной записи телефонной книги хранится в структуре RASENTRY (листинг 15.14), содержащей большое количество полей2.

2Как и прежде, мы не используем расширенную версию этой структуры с новыми полями, появившимися в Windows 2000.

Листинг 15.14. Структура RASENTRY (C++)

typedef struct tagRASENTRY {

DWORD dwSize;

DWORD dwfOptions;

//

// Местонахождение/телефон

//

DWORD dwCountrylD;

DWORD dwCountryCode;

TCHAR szAreaCode[ RAS_MaxAreaCode + 1 ];

TCHAR szLocalPhoneNumber[ RAS_MaxPhoneNumber + 1 ];

DWORD dwAlternateOffset;

// PPP/IP //

RASIPADOR ipaddr; RASIPADDR ipaddrDns;

 RASIPADDR ipaddrDnsAlt; 

RASIPADDR ipaddrWins;

 RASIPADDR ipaddrWinsAlt; //

// Сетевой протокол //

DWORD dwFrameSize; 

DWORD dwfNetProtocols; 

DWORD dwFramingProtocol; //

// Сценарии //

TCHAR szScript[ MAX_PATH ]; //

// Автодозвон //

TCHAR szAutodialDllt MAX_PATH ];

 TCHAR szAutodialFunc[ MAX_PATH ]; //

// Устройство //

TCHAR szDeviceType[ RAS_MaxDeviceType + 1 ];

 TCHAR szDeviceName[ RAS_MaxDeviceName + 1 ]; //

// X.25 //

TCHAR szX25PadType[ RAS_MaxPadType + 1 ];

 TCHAR szX25Address[ RAS_MaxX25Address + 1 ];

 TCHAR szX25Facilities[ RAS_MaxFacilities + 1 ] ;

 TCHAR szX25UserData[ RAS_MaxUserData + 1 ]; 

DWORD dwChannels; //

// Зарезервированные поля //

DWORD dwReservedl; DWORD dwReserved2;

 }

 RASENTRY;

Основные трудности преобразования этой структуры в VB .NET связаны с правильным указанием длин всех строк. Объявление, приведенное в листинге 15.15, имеет много общего с предыдущим примером (см. листинг 15.13).

Листинг 15.15. Структура RASENTRY (VB .NET) 

' Приложение RasGetEntry

' Copyright ©2001 by Desaware Inc.

 AU Rights Reserved Imports System.Runtime.InteropServices

Module Modulel

<StructLayout(LayoutKind.Sequential, Pack:=4, CharSet:=charset.Auto)> Structure RASENTRY

Public dwSize As Integer

Public dwfOptions As Integer

Public dwCountrylD As Integer Public dwCountryCode As Integer <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=!!)>

 Public

szAreayCode As String '11 символов 

<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=129)> Public _

szLocalPhoneNumber As String Public dwAlternateOffset As Integer 

Public ipaddr As Integer Public ipaddrDns As Integer 

Public ipaddrDnsAlt As Integer Public ipaddrWins As Integer

 Public ipaddrWinsAlt As Integer

Public dwFrameSize As Integer Public dwfNetProtocols As Integer 

Public dwFramingProtocol As Integer

<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=260)>

 Public szScript As String

<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=260)>

 Public

szAutodialDll As String

 <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=260)> 

Public _

szAutodialFunc As String

<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=17)>

 Public

szDeviceType As String <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=129)> 

Public

szDeviceName As String

<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=33)> 

Public

szX25PadType As String <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=201)> Public

szX25Address As String <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=201)> Public

szFacilities As String <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=201)> Public _

szllserData As String Public dwChannels As Integer

Public dwReservedl As Integer Public dwReserved2 As Integer

 End Structure

Функция RasGetEntryPropertieS определяется в документации MSDN следующим образом:

DWORD RasGetEntryProperties(

 LPCTSTR IpszPhonebook, // Указатель на полное имя файла

// телефонной книги

LPCTSTR IpszEntry, 

// Указатель на имя записи LPRASENTRY IpRasEntry, 

// Буфер, заполняемый данными записи LPDWORD IpdwEntrylnfoSize,

 // Размер буфера IpRasEntry

// в байтах LPBYTE IpbDevicelnfo, 

// Буфер, заполняемый параметрами

// конфигурации для конкретного устройства LPDWORD IpdwpevicelnfoSize 

// Размер буфера IpbDevicelnfo

// в байтах );

В программе для функции RasGetEntryProperties создаются два объявления. Одно объявление получает в качестве параметра указатель на одну структуру RASENTRY, а другое — указатель на блок неуправляемой памяти. Вскоре вы увидите, почему это необходимо. Объявления выглядят так:

Public Denction RasGetEntryProperties Lib _ "rasapi32.dU" (ByVal IpszPhoneBook As String, ByVal IpszEntry As String, ByRef IpRasEntry As RASENTRY, ByRef IpdwEntryInfoSize '_ As Integer, ByVal devinfo As Integer, ByVal devinfosize _

As Integer) As Integer

Public Deunction RasGetEntryProperties2 Lib _ "rasapi32.dll" Alias "RasGetEntryProperties" (ByVal _ IpszPhoneBook As String, ByVal IpszEntry As String, ByVal _ IpRasEntry As IntPtr, ByRef IpdwEntrylnfoSize As Integer, _ ByVal devinfo As Integer, ByVal devinfosize As Integer) As Integer

В документации Win32 сказано, что вместо неиспользуемых параметров devinfо можно передать Null. В нашем примере оба параметра объявлены с типом ByVal As Integer, и при вызове функции им присваиваются пули.

Присвойте константе PhoneBookEntryToGet имя записи своей телефонной книги по умолчанию (вряд ли в ней найдется запись с именем DesawareModem). Программа инициализирует поле dwSize правильной длиной, присваивает то же значение переменной bufsize и вызывает функцию RasGetEntryProperties. Const PhoneBookEntryToGet As String = "DesawareModem"

Sub Main()

Dim res As Integer

Dim re As RASENTRY

Dim bufsize As Integer

re.dwSize = Marshal.SizeOf(re)

bufsize = re.dwSize

res = RasGetEntryProperties(Nothing, PhoneBookEntryToGet, re, bufsize. 0, 0)

If res = 623 Then

Console.WriteLine ("Can't find specified dial-up entry")

 End If

Почему вызов может завершиться неудачей? Прежде всего — если в телефонной книге отсутствует запись с указанным именем. Столкнувшись с ошибкой 623, проверьте, не забыли ли вы присвоить константе PhoneBookeEntryToGet имя соединения удаленного доступа в вашей системе.

Но даже если имя соединения задано верно, ошибки все равно возможны, поскольку функция RasGetEntryProperties может возвращать дополнительную информацию об устройстве, выходящую за границы структуры. Подобные ситуации очень часто встречаются при вызове нетривиальных функций API. Иногда в этом дополнительном пространстве хранятся строковые данные, на которые ссылаются указатели в структуре (эти данные также легко извлекаются средствами P-Invoke). Универсальное решение — выделить блок памяти длины, ожидаемой функцией, скопировать структуру в блок, вызвать функцию API и затем скопировать данные обратно, как показано ниже:

If res = 603 Then

Dim iptr As IntPtr 

iptr = Marshal.AllocHGlobal(bufsize)

Marshal.StructureToPtr(re, iptr, False)

res = RasGetEntryProperties2(Nothing, PhoneBookEntryToGet, _

iptr, bufsize, 0, 0) 

re = CType(Marshal.PtrToStructure(iptr, GetType(RASENTRY)),

RASENTRY)

Marshal.FreeHGlobal (iptr)

 End If

Console.WriteLine (re.szLocalPhoneNumber) Console.ReadLine()

 End Sub

End Module

Итоги

Платформа .NET упрощает программирование, но переход на эту платформу может оказаться долгим — очень долгим. Готовые компоненты не переключатся на нее сами по себе. То же самое можно сказать и о других технологиях Microsoft — таких, как Transaction Server и Microsoft Message Queue (как бы они не назывались после очередного переименования). Следовательно, взаимодействие с СОМ и базовыми средствами операционной системы является важнейшей частью .NET Framework.

В этой главе было показано, как просто использовать компоненты СОМ в сборках .NET. Visual Studio выполняет за вас большую часть работы. С использованием компонентов .NET в программах СОМ дело обстоит чуть сложнее, но если ограничиться Automation-совместимыми интерфейсами (такими, как используются в VB6), вам удастся избежать практически всех затруднений, возникающих при работе с другими типами данных. Предоставляя доступ к сборкам .NET из СОМ, необходимо в первую очередь позаботиться о контроле версии, в противном случае может возникнуть знакомая ситуация «кошмара DLL».

Мы подошли к концу одной из моих любимых тем — использования функций API в Visual Basic .NET. В этой области VB .NET превосходит VB6 по гибкости и разнообразию возможностей. Многие функции API в VB .NET вызываются так же просто, как в VB6, однако при вызове сложных функций приходится использовать нетривиальные приемы (кстати говоря, в С# это делается ничуть не проще). Главное, о чем должен помнить программист VB6 при переходе на VB .NET, — что все параметры типа Long заменяются на Integer, а парал; фы типа Integer заменяются на Short.

Назад   Вперёд

 


Инфо
Сайт создан: 20 июня 2015 г.
Рейтинг@Mail.ru
Главная страница