Visual2000 · Архив статей А.Колесова & О.Павловой

Работа с функциями Windows API и DLL

Часть 3

Андрей Колесов

© Андрей Колесов, 2000
Авторский вариант. Статья была опубликована c незначительной литературной правкой в журнале "КомпьютерПресс" N 09/2000, с.79-87.


Совет 4. Не нужно бояться применять Win API

После прочтения предыдущего совета может возникнуть мысль, что использование функций Win API — дело рискованное. В какой-то степени это так, но только в сравнении с безопасным программированием, предоставляемым самим VB. Но при умелом их применении и знании возможных подводных камней этот риск является минимальным. К тому же полностью отказаться от применения Win API зачастую просто невозможно — они все равно потребуются при сколь-нибудь серьезной разработке.

К тому же ранее мы говорили о "подводных" камнях для широкого класса DLL. В случае с Win API все обстоит гораздо проще, так здесь очень хорошо унифицирована форма обращения к этим функциям. При этом следует иметь в виду следующие основные моменты:

  1. Функции Win32 API являются именно функциями, т. е. процедурами типа Function (в Win16 API было много подпрограмм Sub). Все эти функции являются типа Long и соответственно их описания записывается в следующем виде:

    ' тип функции определяется в явном виде
    Declare Function name ... As Long
    

    или

    ' тип функции определяется с помощью суффикса
    Declare Function name&
    

  2. Обращение к API-функции выглядит так:
    Result& = ApiName& ([СписокАргументов]
    

    Чаще всего возвращаеме значение функции является кодом завершения операции. Причем ненулевое значение означает в данном случае нормальное завершение, нулевое — ошибку. Обычно (но не всегда) уточнить характер ошибки можно с помощью обращения к функции GetLastError. Описание этой функции имеет такой вид:

    Declare Function GetLastError& Lib "kernel32" ()
    

    ВНИМАНИЕ! При работе в среде VB для получения значения уточненного кода ошибки лучше использовать свойство LastDLLError объекта Err, так как иногда VB сбрасывает в ноль функцию GetLastError в промежутке между обращением к API и продолжением выполнения программы.

    Интерпретировать код, возвращаемый GelLastError можно с помощью констант, записанных в файле API32.TXT, с именам, начинающимися на префикса ERROR_.

    Наиболее типичные ошибки имеют следующие коды:

    Однако многие функции возвращают значение некоторого запрашиваемого параметра (например, OpenFile возвращает значение описателя файла). В таких случаях ошибка определяется каким-то другим специальным значением Return&, чаще всего 0 или -1.

  3. Win32 API используют строго фиксированные способы передачи самых простых типов данным.

Пример обращения к API-функции

Сказанное выше проиллюстрируем на примере использования двух полезных функций работы с файлами — lopen и lread, которые описываются следующим образом:

Declare Function lopen Lib "kernel32" Alias "_lopen" (_
   ByVal lpFileName As String, _
   ByVal wReadWrite As Long) As Long

Declare Function lread Lib "kernel32" Alias "_lread" (_
  ByVal hFile As Long, lpBuffer As Any, _
  ByVal wBytes As Long) As Long

В VB их аналогами — в данном случае точными — являются операторы Open и Get (для режима Binary). Обратим сразу внимание на использование ключевого слова Alias в объявлении функции — это как раз тот случай, когда без него не обойтись. Настоящие названия функции в библиотеке начинаются с символа подчеркивание (типичный стиль для языка C), что не разрешается в VB.

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

Const INVALID_HANDLE_VALUE = -1 ' неверное значение описателя

lpFileName$ = "D:\calc.bas"  ' имя файла
wReadWrite& = 2 ' режим "чтения-записи"
hFile& = lopen(lpFileName$, wReadWrite&)  ' определяем описатель файла
If hFile& = INVALID_HANDLE_VALUE Then ' ошибка открытия файла
  ' уточняем код ошибки
  CodeError& = Err.LastDllError
  'CodeError& = GetLastError  ' эта констукция не работает
End If

Здесь нужно обратить внимание на два момента:

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

Dim MyVar As Single

' чтение вещественного числа, 4 байта
wBytes = lread (hFile&, MyVar, Len(MyVar)
' wBytes — число фактически прочитанных данных, -1 —  ошибка
...
Type MyStruct
   x As Single
   i As Integer
End Type
Dim MyVar As MyStruct
' чтение  структуры данных, 6 байтов
wBytes = lread (hFile&, MyVar, Len(MyVar))

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

Однако, если вы хотите прочитать символьные данные в строку переменной длины, то вам нужно использовать иной вид обращения:

Dim MyVar As String
MyVar = Space$(10)  'резервируем переменную для 10-и символов
' чтение  символьной строки, 10 символов
wBytes = lread (hFile&, ByVal MyVar, Len(MyVar))

Здесь видно важное отличие от приведенного ранее примера — строковая переменная обязательно сопровождается ключевым словом ByVal.

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

Dim MyArray(1 To 10) As Byte
' чтение  10 элементов массива
wBytes = lread (hFile&, MyArray(1), Len(MyArray(1))* 10)

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

' чтение  элементов массива с 4 по 8
wBytes = lread (hFile&, MyArray(4), Len(MyArray(1))* 5)

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

В начало статьи

Совет 5. Используйте Alias для передачи параметров As Any

Здесь мы на основе предыдущего примера раскроем суть четвертого совета Дэна Эпплмана.

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

Declare Function lreadString Lib "kernel32" Alias "_lread" (_
  ByVal hFile As Long, ByVal lpBuffer As String, _
  ByVal wBytes As Long) As Long

При работе с этим описанием указывать ByVal при обращении уже не нужно:

wBytes = lreadString (hFile&, MyVarString, Len(MyVarString))  '

Казалось бы, синтаксис оператора Declare позволяет сделать подобное специальное описание для массива:

Declare Function lreadString Lib "kernel32" Alias "_lread" (_
  ByVal hFile As Long,  lpBuffer() As Byte, _
  ByVal wBytes As Long) As Long

Однако обращение

wBytes = lreadArray (hFile&, MyArray(), 10)  

неизбежно приводит к фатальной ошибке программы.

В начало статьи

Совет 6. Внимание при работе со строковыми переменными

Это продолжение вопроса об особенностях обработки строковых переменных в Visual Basic: VB использует двухбайтную кодировку Unicode, WinAPI — однобайтовую ANSI (причем с форматом, принятым в С — с нулевым байтом в конце). Соответственно, при использовании строковых переменных в качестве аргумента всегда автоматически производится преобразование из Unicode в ANSI при вызове API-функции (точнее DLL-функции) и обратное преобразование при возврате.

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

Как известно, тип String можно использовать для описания пользовательской структуры. В этой связи нужно иметь следующее:

И последнее замечание: применять массив строковых переменных (как фиксированной, так и переменной длины) при обращении к API-функции нельзя ни в коем случае. В противном случае появление "нелегальной операции" будет гарантировано.

В начало статьи

Совет 7. Как обращаться к DLL-функциям

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

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

Мнение автора таково — сколь-нибудь серьезное занятие программированием требует от разработчика владение, по крайней мере, двумя инструментами. Разумеется, в современных условиях четкого разделения труда, очень сложно быть отличным экспертом даже в двух системах, поэтому более логичным является схема "основной и вспомогательный языки". Идея здесь заключается в том, что даже поверхностное знание "вспомогательного" языка (написание довольно простых процедур) может очень сильно повысить эффективность применения "основного". Отметим, что знание VB, хотя бы в качестве вспомогательного, сегодня является практически обязательным требованием для профессионального программиста. Кстати, во времена DOS для любого программиста, в том числе Basic, было крайне желательным знание основ Ассемблера.

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

При изучении межпроцедурного интерфейса следует обратить внимание на такие возможные "подводные камни":

С учетом всего этого можно сформулировать следующие рекомендации:

  1. Используйте самые простые, проверенные способы передачи аргументов в DLL-функции. Стандарты, принятые для Win API вполне годятся в качестве образца.

  2. Ни в коем случае не передавайте массивы строковых переменных.

  3. Очень внимательно используйте передачу простых строковых переменных и многомерных массивов.

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

А что делать, если DLL-функция уже написана, например, на Фортране, но ее входной интерфейс не очень хорошо вписывается в приведенные выше стандарты VB? Здесь можно дать два совета. Первый — напишите тестовую DLL-функцию и с ее помощью постарайтесь методом проб и ошибок подобрать нужное обращение из VB-программы. Второй — напишите процедуру-переходник на том же Фортране, который бы обеспечивал простой интерфейс между VB и DLL-функцией с преобразованием простых структур данных в сложные (например, преобразовывал многомерный байтовый массив в строковый массив).

Итак: используйте DLL-функции. Но сохраняйте бдительность...

В начало статьи