Visual2000 · Архив статей А.Колесова & О.Павловой
Андрей Колесов
© Андрей Колесов, 2000После прочтения предыдущего совета может возникнуть мысль, что использование функций Win API — дело рискованное. В какой-то степени это так, но только в сравнении с безопасным программированием, предоставляемым самим VB. Но при умелом их применении и знании возможных подводных камней этот риск является минимальным. К тому же полностью отказаться от применения Win API зачастую просто невозможно — они все равно потребуются при сколь-нибудь серьезной разработке.
К тому же ранее мы говорили о "подводных" камнях для широкого класса DLL. В случае с Win API все обстоит гораздо проще, так здесь очень хорошо унифицирована форма обращения к этим функциям. При этом следует иметь в виду следующие основные моменты:
' тип функции определяется в явном виде Declare Function name ... As Long
или
' тип функции определяется с помощью суффикса Declare Function name&
Result& = ApiName& ([СписокАргументов]
Чаще всего возвращаеме значение функции является кодом завершения операции. Причем ненулевое значение означает в данном случае нормальное завершение, нулевое — ошибку. Обычно (но не всегда) уточнить характер ошибки можно с помощью обращения к функции GetLastError. Описание этой функции имеет такой вид:
Declare Function GetLastError& Lib "kernel32" ()
ВНИМАНИЕ! При работе в среде VB для получения значения уточненного кода ошибки лучше использовать свойство LastDLLError объекта Err, так как иногда VB сбрасывает в ноль функцию GetLastError в промежутке между обращением к API и продолжением выполнения программы.
Интерпретировать код, возвращаемый GelLastError можно с помощью констант, записанных в файле API32.TXT, с именам, начинающимися на префикса ERROR_.
Наиболее типичные ошибки имеют следующие коды:
Однако многие функции возвращают значение некоторого запрашиваемого параметра (например, OpenFile возвращает значение описателя файла). В таких случаях ошибка определяется каким-то другим специальным значением Return&, чаще всего 0 или -1.
ByVal ... As Long
С помощью переменных типа Long выполняется не менее 80% передачи аргументов. Обратите внимание, что аргумент ВСЕГДА сопровождается ключевым словом ByVal и это, кроме всего прочего означает, что выполняется односторонняя передача данных — от VB-программы к API-функции.
ByVal ... As String
Это тип передачи данных также встречается достаточно часто, причем с аргументом также ВСЕГДА применяется ByVal. При вызове API-функции в стек записывается адрес строки, поэтому в данном случае возможен двухсторонний обмен данными. При работе со строками нужно иметь в виду несколько опасностей.
Первая — резервирование памяти под строку производится в вызывающей программе, поэтому если APIфункция будет заполнять строки, то нужно перед ее вызовом создать строку необходимого размера. Например, функция GetWindowsDirectory возвращает путь к каталогу Windows, который по определению не должен занимать более 144 символов. Соответственно, обращение к этой функции должно выглядеть примерно так:
WinPath$ = Space$(144) ' резервируем строку в 144 символа Result& = GetWindowsDirectory& (WinTath$, 144) 'заполнение буфера ' Result& — фактическое число символов в имени каталога WinPath$ = Left$(WinPath, Result&)
Вторая проблема заключается в том, что при обращении к API-функции производится преобразование исходной строки в ее некоторое внутреннее представление, а при выходе из функции — наоборот. Если во времена Win16 эта операция лишь в добавлении нулевого байта в конце строки, то с появлением Win32 к этому добавилось трансформация двухбайтной кодировки Unicode в ANSI и наоборот. (Об этом подробно говорилось в статье "Особенности работы со строковыми переменными в VB", журнал "КомпьютерПресс" N 10/99 и 01/2000). Сейчас же только отметим, что с помощью конструкции ByVal ... As String можно обмениваться строками только с символьными данными.
... As Any
Это означает, что в стек будет помещен некоторый адрес буфера памяти, интерпретация содержимого которого будет выполнятся API-функцией, например, в зависимости от значения других аргументов. Однако As Any может использоваться только в операторе Declare — при конкретном обращении к функции в качестве аргумента должна быть определена конкретная переменная.
... As UserDefinedType
Такая конструкция также часто применяется, когда требуется обменяться данными (в общем случае в обе стороны) с помощью некоторой структуры. На самом деле это конструкция — некий вид конкретной реализации формы передачи As Any, просто в данном случае функция настроена на фиксированную структуру.
Форма структуры данных определяется конкретной API-функцией и на программисте лежит ответственность правильным образом описать и зарезервировать ее в вызывающей программе. Такая конструкция ВСЕГДА используется БЕЗ слова ByVal, т. е. в данном случае выполняется передача по ссылке — в стек записывается адрес переменной.
Сказанное выше проиллюстрируем на примере использования двух полезных функций работы с файлами — 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)
Таким же образом можно прочитать фрагмент многомерного массива, но при этому нужно знать алгоритм преобразования многомерной структуры в одномерную.
Здесь мы на основе предыдущего примера раскроем суть четвертого совета Дэна Эпплмана.
При работе с функцией 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)
неизбежно приводит к фатальной ошибке программы.
Это продолжение вопроса об особенностях обработки строковых переменных в Visual Basic: VB использует двухбайтную кодировку Unicode, WinAPI — однобайтовую ANSI (причем с форматом, принятым в С — с нулевым байтом в конце). Соответственно, при использовании строковых переменных в качестве аргумента всегда автоматически производится преобразование из Unicode в ANSI при вызове API-функции (точнее DLL-функции) и обратное преобразование при возврате.
Вывод из этого простой: с помощью переменных String можно обмениваться символьными данными, но нельзя использовать их для обмена произвольной двоичной информацией (как это было при работе с 16- разрядными версиями VB). В последнем случае лучше использовать одномерный байтовый массив.
Как известно, тип String можно использовать для описания пользовательской структуры. В этой связи нужно иметь следующее:
Type MyStruct x As Single s As String ' строка переменной длины End Type
В случает строки переменной длины в составе структуры передается дескриптор строки со всеми вытекающими последствиями в виде ошибки выполнения программы.
Type MyStruct x As Single s As String*8 ' строка фиксированной длины End Type
При этом производится соответствующее преобразование кодировок.
И последнее замечание: применять массив строковых переменных (как фиксированной, так и переменной длины) при обращении к API-функции нельзя ни в коем случае. В противном случае появление "нелегальной операции" будет гарантировано.
Вполне вероятно, что вы сами столкнетесь с ситуацией, когда вам потребуется написать собственную библиотеку DLL-функций. Потребность в этом неизбежно возникнет, если вы будете использовать технологию смешанного программирования — использования двух или более языков программирования для реализации одного приложения.
Отметим по этому поводу, что смешанное программирование — это вполне обычное явление для реализации достаточно сложного приложения. Действительно, каждый язык (точнее система программирования на базе языка) имеет свои сильные и слабые стороны, поэтому вполне логично использовать преимущества разных инструментов для решения разных задач. Например, VB — для создания пользовательского интерфейса, С — эффективного доступа к системным ресурсам, Fortran — реализации численных алгоритмов.
Мнение автора таково — сколь-нибудь серьезное занятие программированием требует от разработчика владение, по крайней мере, двумя инструментами. Разумеется, в современных условиях четкого разделения труда, очень сложно быть отличным экспертом даже в двух системах, поэтому более логичным является схема "основной и вспомогательный языки". Идея здесь заключается в том, что даже поверхностное знание "вспомогательного" языка (написание довольно простых процедур) может очень сильно повысить эффективность применения "основного". Отметим, что знание VB, хотя бы в качестве вспомогательного, сегодня является практически обязательным требованием для профессионального программиста. Кстати, во времена DOS для любого программиста, в том числе Basic, было крайне желательным знание основ Ассемблера.
Так или иначе, но даже в условиях групповой работы, когда каждый программист занимается своим конкретным делом, представление об особенностях процедурного интерфейса в разных языках нужно иметь всем участникам проекта. И знать, что многие системы программирования (в том числе и VB) кроме интерфейса, используемого по умолчанию, позволяют применять другие, расширенные методы обращения к процедурам, которые позволяют адаптировать интерфейс к другому языку.
При изучении межпроцедурного интерфейса следует обратить внимание на такие возможные "подводные камни":
С учетом всего этого можно сформулировать следующие рекомендации:
А что делать, если DLL-функция уже написана, например, на Фортране, но ее входной интерфейс не очень хорошо вписывается в приведенные выше стандарты VB? Здесь можно дать два совета. Первый — напишите тестовую DLL-функцию и с ее помощью постарайтесь методом проб и ошибок подобрать нужное обращение из VB-программы. Второй — напишите процедуру-переходник на том же Фортране, который бы обеспечивал простой интерфейс между VB и DLL-функцией с преобразованием простых структур данных в сложные (например, преобразовывал многомерный байтовый массив в строковый массив).
Итак: используйте DLL-функции. Но сохраняйте бдительность...