Взаимодействие с DLL

Автор: Алексей Дубовцев
Опубликовано: 22.10.2001
Версия текста: 1.0

Введение
О чем же собственно речь?
Маршалинг вызова функций
Немного теории
Выбор набора символов
Определение собственных имен
Конфликт зависимости имени от "кодировки"
Форматы вызова функций
Анализ возвращаемого значения и обработка ошибок
Более сложные методы взаимодействия
Передача структур
Функции "обратного вызова"
Заключение

Source.zip - 30 KB

Введение

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

О чем же собственно речь?

Допустим, вы хотите самостоятельно вызвать некоторую функцию Windows API. Для этого вам надо будет знать, в какой библиотеке она размещена. Вы можете узнать это, найдя данную функцию в Platform SDK и посмотрев в разделе Requirements значение пункта Library. Когда имя библиотеки найдено, можно считать, что пол дела уже сделано. Далее сделаем следующее.

В статье я буду рассказывать об аттрибуте DllImport, но взгянув на код, написанный на MSIL, вы, к своему удивлению, не найдёте там даже упоминания об этом аттрибуте. Это нормально. Язык MSIL имеет встроенные возможности по взаимодействию с DLL при помощи ключевых слов.

Эти маленькие примерчики демонстрируют вызов всем известной функции MessageBox из библиотеки User32.lib. Как вы можете заметить, все очень просто. Объявляем атрибут DllImport с именем библиотеки, который указывает среде исполнения .NET, что функцию надо импортировать из динамической библиотеки, и смело используем эту функцию.

Когда будете писать собственную программу, не забудьте, что необходимо подключить пространство имен System.Runtime.InteropServices

Маршалинг вызова функций

Немного теории

Давайте посмотрим несколько глубже: ведь все данные, которые вы используете в среде .NET, являются управляемыми, то есть их расположением в памяти управляет среда исполнения .NET. А обычные библиотеки, в свою очередь, ничего не знают ни о среде исполнения, ни о типах данных, которые вы используете. Чтобы разрешить данную проблему, в игру вступает процесс, называемый маршалингом (маршалинг от английского marshaling, что в переводе означает выстраивать в порядке, переносить). Этот процесс позволяет перенести ваш вызов из среды исполнения .NET на уровень операционной системы, непосредственно к самим библиотекам. А происходит это так.


Маршалинг вызова функций

Давайте рассмотрим процесс вызова функции из DLL по порядку.

ПРИМЕЧАНИЕ
Если в ходе исполнения функции в DLL возникнет необработанное исключение, то среда исполнения преобразует его в исключение .NET, и вы сможете его обработать в своем коде стандартными средствами .NET, если это будет необходимо.

Выбор набора символов

Хотите, я вас удивлю? Для начала вспомним пример в начале статьи. Мы с вами импортировали функцию MessageBox из библиотеки User32.dll. Вспомнили? Что, ничего интересного не замечаете? А функции MessageBox в библиотеке User32.dll нет. Не верите? Тогда убедитесь в этом сами, выполнив команду:

DumpBin /exports User32.dll /out:User32.exports

Вот что получилось у меня:

        451  1C2 00013581 MessageBeep
        452  1C3 000275D5 MessageBoxA
        453  1C4 000275FD MessageBoxExA
        454  1C5 000222CC MessageBoxExW
        455  1C6 00048FF3 MessageBoxIndirectA
        456  1C7 0002DDD1 MessageBoxIndirectW
        457  1C8 0001FE1C MessageBoxW
        458  1C9 00015126 ModifyMenuA
     

Вы с легкостью сможете убедиться, что там присутствуют функции MessageBoxA и MessageBoxW. Первая используется для строк ANSI, вторая - для строк Unicode. Стандартный маршалер системных вызовов .NET знает об этом, и если он не найдет в библиотеке фунцкию с заданным именем, то он автоматически добавит к имени постфикс W или A, и будет искать функции с таким именем. Если вы хотите вызвать функцию для конкретного набора символов, то вам поможет параметр CharSet, который по умолчанию равен Charset.Ansi. Этот атрибут может принимать три значения: Unicode, Ansi и Auto. Здесь, наверное, надо оговориться только по поводу Auto. Этот параметр предписывает маршалеру самостоятельно выбрать нужную функцию. Вы можете задавать эти параметры следующим образом.

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

Для увеличения скорости маршалинга нужно правильно использовать параметры Unicode и ANSI. Если типы передаваемых строк совпадут, то маршалеру не понадобиться преобразовывать данные из Unocode в ANSI или обратно, тем самым вы увеличите скорость маршалинга.

ПРИМЕЧАНИЕ
Я бы вам советовал использовать везде, где только можно, набор символов Unicode, так как внутри вся система (здесь имеется ввиду Windows 2000) построена на Unicode, а ANSI-функции являются всего лишь заглушками для преобразования ANSI в Unicode.

Определение собственных имен

Разработчики стандартного маршалера явно думали о нас, и поэтому нам предоставлена такая нужная и полезная возможность как переименовывание функций. То есть вы можете импортировать функцию, присвоив ей другое имя. Эта возможность реализуется при помощи атрибута EntryPoint. Приведем пример:

Атрибут EntryPoint ведет себя немного хитрее, чем кажется, его поведение меняется в зависимости от формата строки, назначенной ему:

  • "ИмяФункции" - будет импортирована функция с таким символьным именем.
  • "#123" - будет импортирована функция с таким порядковым номером в библиотеке (для тех кто знает - будет произведем импорт по "ординалу").

Таким образом, вы можете импортировать функцию из динамической библиотеки и объявить ей любое имя. Данная возможность с первого взгляда может показаться не особо нужной. Но это на самом деле не так. Представьте себе, что вам нужно импортировать функцию в определенное пространство имен, а она конфликтует с уже существующим в нём именем. Как раз такие проблемы и призвана решить эта возможность.

Конфликт зависимости имени от "кодировки"

Большинство создателей динамически загружаемых библиотек не придерживается стандартов именования функций в соответствии с поддерживаемым набором символов (Unicode или ANSI), то есть они попросту не ставят постфиксов W и A. Если, к примеру, создатель библиотеки решил, что она будет работать с ANSI, то будьте уверены: ничто не заставит его сделать заглушки для Unicode. Тут-то и может возникнуть проблема, когда нужно вызвать функцию для соответствующего набора символов, которая не имеет нужного постфикса. Нам на помощь придет атрибут ExactSpelling, который запретит (разрешит) маршалеру изменять определенное нами имя функции, при помощи атрибута EntryPoint. А делается это вот так:

ПРИМЕЧАНИЕ
Параметр EntryPoint обязателен.

По умолчанию значение атрибута ExactSpelling равно False. При значении False маршалер подставляет в имени постфикс W или A, в зависимости от значения параметра CharSet, а при значении True ему не позволено изменять имя функции.

Обязательно обратите внимание на заданный для функции набор символов, он должен совпадать с реальным. То есть в данном случае вам самим предстоит выбирать между Unicode и ANSI. Если вы неправильно зададите кодировку, то ваш код попросту будет работать неправильно. Для того чтобы это понять, поробуйте задать EntryPoint="MessageBoxW", а CharSet=CharSet.Ansi. Или наоборот. Результат будет довольно интересный. У меня получилось вот что:


Форматы вызова функций

Существует несколько соглашений о вызове функции (call convention), я надеюсь что вы все представляете себе, что это такое. Если нет, то я поясню: грубо говоря, это набор правил, по которым передаются параметры и возвращаемое значение. Вы можете самостоятельно задать соглашение о вызове импортируемой функции при помощи атрибута CallingConvention. Этот атрибут иногда бывает просто жизненно необходим. Например, если вам потребовалось вызвать фукцию, подобную printf (она имеет переменное число аргументов и использует формат вызова cdecl). Как это делается, я продемонстрировал ниже.

Не забудьте задать атрибут CallingConvention.Cdecl, так как если вы этого не сделаете, может произойти ошибка при вызове функции или после. Этот формат вызова подразумевает, что стек очищается не самой функцией, а тем, кто ее вызывает.

Анализ возвращаемого значения и обработка ошибок

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

Если возвращаемое значение не равно S_OK, то считается, что функция не смогла успешно выполнить свою работу.

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

В дополнении ко всему сказанному, существует еще один атрибут: SetLastError. Он позволяет получить информацию об ошибке после вызова функции. Если его значение равно False, то вы не сможете получить значение кода ошибки при помощи функции GetLastError, потому что стандартный маршалер "любезно" заберет это значение себе. Для того чтобы обрабатывать ошибки самостоятельно, установите значение этого атрибута равным True.

Значение по умолчанию для этого параметра для разных языков приведены в таблице ниже.

Язык

Значение

Visual Basic.NET True
C# False
Managed Extensions for C++ False
Значение по умолчанию для SetLastError

Если вы пишете на MSIL, то вам нужно будет использовать ключевое слово lasterr. То есть написать следующее:

          .method public hidebysig static pinvokeimpl("user32.dll" 
              lasterr
             winapi) 
                          int32  MessageBox(int32 hWnd,
                                            string text,
                                            string caption,
                                            unsigned int32 type) cil managed preservesig
          

Более сложные методы взаимодействия

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

С первого взгляда может показаться, что передача структур достаточно тривиальна. Чего тут, описал структуру, передал, и дело в шляпе. Не тут-то было. Ведь по умолчанию все данные являются управляемыми, то есть их размещением в памяти будет управлять среда исполнения .NET. И нет никакой гарантии того, что поля структуры будут расположены в памяти последовательно друг за другом, как нам хочется. Скажу даже больше, среда .NET будет размещать поля структуры в памяти, руководствуясь в первую очередь правилами оптимизации. Вследствие чего могут возникнуть очень неприятные ошибки, связанные с распределением памяти. Для того чтобы управлять размещением структур в памяти, используется атрибут LayoutKind. Данный атрибут может принимать три значения, перечисленные ниже:

Поле

Описание

LayoutKind.Automatic

Позволяет среде исполнения .NET перераспределять
элементы структуры в памяти, руководствуясь
внутренними правилами.
ПРЕДУПРЕЖДЕНИЕ
Никогда не применяйте данный атрибут, для вызовов
в DLL.

LayoutKind.Explicit

Поля выравниваются в соответствии с атрибутом
FieldOffset, определенным для каждого поля.

LayoutKind.Sequential Поля структуру располагаются в памяти последовательно
в том порядке, в котором они были описаны.
ПРИМЕЧАНИЕ
Это наиболее приемлемый вариант для передачи в DLL функции.
Значения атрибута LayoutKind

Ну а теперь, как обычно смотрите примерчик:

Функции "обратного вызова"

Многие стандартные API функции в качестве аргумента принимают указатель на функцию. Для примера можно привести CreateThread, EnumPrinters, SetWindowsHookEx, EnumWindows и многие другие не менее полезные функции. Данный механизм, благодаря своей гибкости, позволяет настраивать поведение API функций в широких пределах. Разработчики среды исполнения .NET не забыли подумать и о нем, мы с легкостью сможем применять этот механизм в наших приложениях для .NET. Если вам все еще не понятно, о чем я тут толкую, взгляните на картинку.


Обратный вызов функций

Давайте рассмотрим, что нам придется для этого сделать.

Что бы вам было более понятно, я приведу пример.

У вас может возникнуть вопрос: каким же образом код из DLL вызывает код из среды .NET? Что, не видите никаких проблем? А дело вот в чем. Ведь код .NET и родной код системы кардинально различаются, и поэтому в принципе не могут вызывать друг друга. Для того чтобы решить эту проблему, стандартный маршалер поступает следующим образом: он создает маленькую native-функцию. Которая занимается только тем, что изменяет параметры и вызывает реальную .NET. Именно адрес этой функции передаётся в DLL. Таким образом, для вызова функций .NET из DLL используются переходники.

Заключение

Ну вот вроде и все, что я хотел сказать о взаимодействии с DLL из среды исполнения .NET. Собственно говоря, больше ничего и не осталось. Но вы не расслабляйтесь, это только начало: вызов функций из DLL является одним из самых простых взаимодействий. Вскоре я вам поведаю о взаимодействии с COM.

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