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

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

Часть 2

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

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


Совет 1. Следите за правильным оформлением объявления DLL-процедур

Само обращение к DLL-процедурам в программе выглядит точно также как к "обычным" процедурам Visual Basic, например так:

Call DllName ([список аргументов])

Однако для использования внешних DLL-функций (в том числе и Win API) их нужно обязательно объявить в программе с помощью оператора Declare, который имеет следующий вид:

[Public | Private] Declare Sub ИмяПроцедуры Lib "ИмяБиблиотеки" _
  [Alias "Псевдоним"] [([СписокАргументов])]

или

[Public | Private] Declare Function ИмяФункции Lib "ИмяБиблиотеки" _
[Alias "псевдоним"] ([СписокАргументов])] [As type]

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

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

Набор Win32 API реализован только в виде функций (в Win16 API было много подпрограмм Sub). В большинстве своем — это функции типа Long, которые чаще всего возвращают код завершения операции.

Оператор Declare появился в MS Basic еще во времена DOS, причем он использовался и для объявления внутренних процедур проекта. В Visual Basic этого не требуется, так как объявлением внутренних процедур автоматически является их описание Sub или Function. По сравнению с Basic/DOS в новом описании обязательно требуется указать имя файла-библиотеки, где находится искомая процедура. Библиотеки Wip API размещаются в системном каталоге Windows, поэтому достаточно указать только название файла. Если же вы обращаетесь к DLL, которая находится в произвольном месте, нужно записать полный путь к данному файлу.

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

Declare Function GetTempPath _
    Lib "kernel32" Alias "GetTempPathA" _
    (ByVal nBufferLength As Long, _
    ByVal lpBuffer As String) As Long

В этом случае все основные элементы описания разнесены на разные строчки и поэтому хорошо читаются.

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

Совет 2. Будьте особенно внимательны при работе с DLL-функциями.

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

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

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

Проблема усугубляется еще и тем, что разные языки программирования используют различные способы передачи параметров между процедурами. (Точнее, используют разные способы передачи по умолчанию, так как многие языки могут поддерживать несколько способов.) Win API реализованы на C/C++ и применяют соглашения о передачи параметров, принятой в этой системе, которое отличается от привычного для VB варианта.

В этой связи следует отметить, что появление встроенных в VB аналогов API-функций оправдано именно адаптацией последних к синтаксису VB и реализацией соответствующего механизма контроля обмена данными. Обратим также внимание, что на этапе опытной отладки приложения при создании исполняемого модуля лучше использовать вариант компиляции P-code вместо Native Code (машинный код). В первом случае программа будет работать под управлением интерпретатора — медленнее по сравнению с машинным кодом, но более надежно с точки зрения возможного ошибочного воздействия на операционную систему и обеспечивая более удобный режим выявления возможных ошибок.

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

Совет 3. 10 рекомендаций Дэна Эпплмана по надежному API-программированию в среде VB

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

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

1. Помните о ByVal

Наиболее частая ошибка, совершаемая при обращении к функциям API и DLL заключается в некорректном использовании ключевого слова ByVal: его или забывают ставить или, наоборот, ставят, когда в нем нет необходимости.

Здесь следует напомнить, что передача параметров в любой системе программирования, в том числе и VB, выполняется двумя основными путями: по ссылке (ByRef) или по значению (ByVal). В первом случае передается адрес переменной (этот вариант используется в VB по умолчанию), во втором — ее величина. Принципиальное отличие заключается в том, что с помощью ссылки обеспечивается возврат в вызывающую программу измененного значения передаваемого параметра.

Чтобы разобраться в этом проведите эксперимент с помощью таких программ:

Dim v As Integer 
v = 2 
Call MyProc(v) 
MsgBox "v = " & v

Sub MyProc (v As Integer) 
  v = v + 1 
End Sub

Запустив на выполнение этот пример, вы получите сообщение со значением переменной равным 3. Дело в том, что в данном случае в подпрограмму MyProc передается адрес переменной v, физически созданной в вызывающей программе. Теперь измените описание процедуры на

Sub MyProc (ByVal v As Integer)

В результате при выполнении теста вы получите "v = 2", потому что в процедуру передается лишь исходное значение переменной — результат выполненных с ним операций не возвращается в вызывающую программу. Режим передачи по значению можно поменять также с помощью оператора Call следующим образом:

Sub MyProc (v As Integer)
... 
Call MyProc((v)) ' (v) - скобки указывают режим передачи по значению

Однако при обращении к внутренним VB процедурам использование в операторе Call ключевого слова ByVal запрещено — вместо него применяются круглые скобки. Этому есть свое объяснение.

В классическом случае (С, Fortran, Pascal) режимы ByRef и ByVal отличаются тем, что помещается в стек обмена данными — адрес переменной или ее значение. В Basic исторически используется вариант программной эмуляции ByVal — в стеке всегда находится адрес, но только при передаче по значению для этого просто создается временная переменная.

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

DLL-функции реализованы по "классическим" принципам и поэтому требуют обязательного описания того, каким образом происходит обмен данными с каждой из аугементов. Именно этой цели служат объявления функций через описание Declare (точнее — списка передаваемых аргументов). Чаще всего передача параметров в функцию Windows API или DLL выполняется с помощью ключевого слова ByVal. Причем оно может быть задано как в операторе Declare, так и непосредственно при вызове функции.

Влияние оператора ByVal на передачу параметров показано на этих примерах:

----------------------------------------------------------------
Тип         С ByVal                   без ByVal
параметра
----------------------------------------------------------------
Integer    В стек помещается          В стек помещается
           16-разрядное целое         32-разрядный адрес
                                      16-разрядного целого

Long       В стек помещается          В стек помещается
           32-разрядное целое         32-разрядый адрес
                                      32-разрядного целого

String     Строка преобразуется       В стек помещается
           в формат используемый      VB-дескриптор строки.
           в С (данные и завершающий  Такие дескрипторы никогда
           нулевой байт).             не используется самим
           32-разрядный адрес новой   Windows API и распознаются
           строки помещается в стек.  только в DLL, реализованных
                                      специально для VB.
----------------------------------------------------------------

Последствия неправильной передачи параметров легко предугадать. В случае получения явно недопустимого адреса вы получите сообщение GPF (General Protection Fault - ошибка защиты памяти). Если же функция получит значение, совпадающее с допустимым адресом, то функция API залезет в чужую область (например, в ядро Windows) с вытекающими отсюда катастрофическими последствиями.

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

2. Проверяйте тип передаваемых параметров

Не менее важным является верное число и тип передаваемых параметров. Необходимо, чтобы объявленные в Declare аргументы соответствовали ожидаемым параметрам в функции API. Наиболее часто встречающийся случай ошибки в передаче параметров связан с различием между NULL и строкой нулевой длины — следует помнить, что это не одно и то же.

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

3. Проверяйте тип возвращаемого значения

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

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

4. С большой осторожностью используйте конструкцию "As Any"

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

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

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

5. Не забывайте инициализировать строки

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

Следует отметить, что в 32-разрядных Windows при использовании строк производится преобразование из Unicode (двухбайтовая кодировка) в ANSI (однобайтовая) и обратно, причем с учетом национальных установок системы. Поэтому для резервирования буферов порой удобнее использовать байтовые массивы вместо строковых переменных. (Об этом подробнее будет говориться далее.)

Чаще всего, функции Win API позволюет вам самим определить максимальный размер блока. В частности, иногда для этого нужно вызвать другую функцию API, которая "подскажет" размер блока. Например, GetWindowTextLength позволяет определить размер строки, необходимый для размещения заголовка окна, получаемого функцией GetWindowText. В этом случае, Windows гарантирует "не переходить" за границу.

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

6. Обязательно используйте Option Explicit

Тут не нужно приводить аргументы Дэна Эпплмана — мы уже подробно обсуждали это вопрос в наших "Советах".

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

7. Внимательно проверяйте значения параметров и возвращаемых величин

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

Windows 9x обладает усовершенствованной системой проверки параметров для большинства функций API. Поэтому наличие ошибки в данных обычно не вызывает фатальной ошибки. Однако определить, что же явилось причиной ошибки — не так-то просто.

Здесь можно посоветовать использовать несколько способов отладки данного типа ошибки:

Обязательно также нужно проверять результат выполнения API-функции.

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

8. Помните, что целые числа в VB и в Windows — это не одно и то же

В первую очередь, следует иметь в виду, что под термином "Integer" в VB понимается 16-разрядное число, в документации Win 32 — 32-разрядное. Во-вторых, целые числа (Integer и Long) в VB — это величины со знаком (т.е. один разряд используется как знак, остальные — мантисса числа), в Windows — только неотрицательные числа. Это обстоятельство нужно иметь ввиду, когда вы формируете передаваемый параметр с помощью арифметических операций (например, вычисляете адрес с помощью суммирования некоторой базы и смещения). Для этого стандартные арифметические функции VB не годятся. Как быть в этом случае мы будем говорить отдельно.

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

9. Внимательно следите за именами функций

В отличие от Win16 Имена все функции Win32 API являются чувствительными к точному использованию строчных и прописных букв (в Win16 такого не было). Если вы где-то используете строчную букву вместо прописной или наоборот, то нужная функция не будет найдена. Следите также за правильным использованием суффикса A или W в функциях, использующих строковые параметры. (Подробнее об этом — далее.)

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

10. Чаще сохраняйте результаты работы

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

Продолжение статьи, Часть 3

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