Взаимодействие с DLL
Опубликовано: 22.10.2001
Исправлено: 07.11.2005
Версия текста: 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 в память (LoadLibrary).
-
Поиск необходимой функции в библиотеке (GetProcAddress).
-
Проталкивание параметров в стек в требуемом порядке, а также преобразование
типа проталкиваемого параметра (к примеру, из String в LPTSTR).
ПРИМЕЧАНИЕ
Этот процесс собственно и называется маршалингом.
|
-
Вызов функции из DLL.
-
Анализ возвращаемого значения функции и обработка ошибок в виде генерации исключений, если это требуется.
-
Приведение типов возвращаемых значений к типам .NET.
ПРИМЕЧАНИЕ
Если в ходе исполнения функции в 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.
А делается это вот так:
-
Visual Basic.NET
Imports System
Imports System.Runtime.InteropServices
Public Module Application
<DllImport ("user32.dll", EntryPoint := "MessageBoxW", ExactSpelling := True)> _
Public Function MessageBox (hWnd As Integer, _
txt As String, caption As String, Typ As Integer) As Integer
End Function
Public Sub Main()
MessageBox(0,"Hello World","It's a very good day",0)
End Sub
End Module
|
-
C#
using System;
using System.Runtime.InteropServices;
class Application
{
[DllImport ("user32.dll", EntryPoint="MessageBoxW", CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern int MessageBox(int hWnd, string text, string caption, uint type);
public static void Main()
{
MessageBox(0,"Hello World","It's a good day to win",0);
}
}
|
-
Managed Visual C++
#using <mscorlib.dll>
using namespace System;
using namespace System::Runtime::InteropServices;
typedef void* HWND;
[DllImport("user32.dll" ,EntryPoint="MessageBoxA", ExactSpelling = true)]
int MessageBox(HWND hWnd, String* Message, String* Title, unsigned int uiType);
void main()
{
MessageBox(0,"Hello World","",0);
}
|
-
Intermediate Language
.assembly App
{}
.module extern User32.dll
.assembly extern mscorlib
{}
.method public static pinvokeimpl("user32.dll"
nomangle
winapi)
int32 MessageBox(int32 hWnd,
string text,
string caption,
unsigned int32 type) cil managed preservesig
{
}
.method public static void Main() cil managed
{
.entrypoint
.maxstack 8
ldc.i4.0
ldstr "Hello World"
ldstr "It is a good day"
ldc.i4.0
call int32 MessageBox(int32,
string,
string,
unsigned int32)
pop
ret
}
|
ПРИМЕЧАНИЕ
Параметр EntryPoint обязателен.
|
По умолчанию значение атрибута ExactSpelling равно False. При значении False маршалер подставляет в имени постфикс W или
A, в зависимости от значения параметра CharSet, а при значении True ему не позволено изменять имя функции.
Обязательно обратите внимание на заданный для функции набор символов, он должен совпадать с реальным. То есть в данном случае вам самим
предстоит выбирать между Unicode и ANSI. Если вы неправильно зададите кодировку, то ваш код попросту будет работать неправильно.
Для того чтобы это понять, поробуйте задать EntryPoint="MessageBoxW", а CharSet=CharSet.Ansi. Или наоборот. Результат будет довольно интересный. У меня получилось вот что:
|
Форматы вызова функций
Существует несколько соглашений о вызове функции (call convention), я надеюсь что вы все представляете себе, что это такое. Если нет, то я поясню: грубо говоря, это набор правил, по которым
передаются параметры и возвращаемое значение. Вы можете самостоятельно задать соглашение о вызове импортируемой функции
при помощи атрибута CallingConvention. Этот атрибут иногда бывает просто жизненно необходим. Например, если вам потребовалось вызвать фукцию, подобную printf (она имеет переменное число аргументов и использует формат вызова cdecl). Как это делается, я продемонстрировал ниже.
-
Visual Basic.NET
Imports System
Imports System.Runtime.InteropServices
Public Module Application
//Опишем две функции для каждого конкретного случая
//так как .NET не поддерживает функций с изменяемым
//числом параметров
<DllImport("msvcrt.dll", CallingConvention := CallingConvention.Cdecl)> _
Overloads Function printf ( _
format As String, i As Integer, d As Double) As Integer
End Function
<DllImport("msvcrt.dll", CallingConvention := CallingConvention.Cdecl)> _
Overloads Function printf ( _
format As String, i As Integer, s As String) As Integer
End Function
Sub Main()
printf("Hello World: %i %f",2,3.3)
Call Console.WriteLine()
printf("Hello World: %i %s",2,"Hehe")
End Sub
End Module
|
-
C#
using System;
using System.Runtime.InteropServices;
public class App
{
[DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl)]
public static extern int printf(string format, int i, double d);
[DllImport("msvcrt.dll", CallingConvention=CallingConvention.Cdecl)]
public static extern int printf(string format, int i, string s);
public static void Main()
{
printf("\nHello World: %i %f", 99, 99.99);
printf("\nHelo World: %i %s", 99, "abcd");
}
}
|
-
Managed Visual C++
#using <mscorlib.dll>
using namespace System;
using namespace System::Runtime::InteropServices;
[DllImport("msvcrt.dll", CallingConvention=Cdecl)]
int printf(String* Format,int iNumber,double Number2);
[DllImport("msvcrt.dll", CallingConvention=Cdecl)]
int printf(String* Format,int iNumber,String* str);
void main()
{
printf("Hello World: %i %f\n", 2, 2.3);
printf("Hello World: %i %s\n", 2, "Hehe");
}
|
-
Intermediate Language
.assembly App
{}
.module extern User32.dll
.assembly extern mscorlib
{}
.method public static pinvokeimpl("msvcrt.dll" cdecl)
int32 printf(string format,
int32 i,
float64 d) cil managed preservesig
{
}
.method public static pinvokeimpl("msvcrt.dll" cdecl)
int32 printf(string format,
int32 i,
string s) cil managed preservesig
{
}
.method public static void Main() cil managed
{
.entrypoint
.maxstack 8
ldstr "Hello World: %i %f\n"
ldc.i4.s 99
ldc.r8 99.99
call int32 printf(string format,int32 i,float64 s)
pop
ldstr "Hello World: %i %s\n"
ldc.i4.s 99
ldstr "Hehe"
call int32 printf(string format,int32 i,string s)
pop
ret
}
|
Не забудьте задать атрибут CallingConvention.Cdecl, так как если вы этого не сделаете, может произойти ошибка при вызове функции или после. Этот формат вызова подразумевает, что стек очищается не самой функцией, а тем, кто ее вызывает.
|
Анализ возвращаемого значения и обработка ошибок
По умолчанию после вызова функции анализируется возвращаемое значение и если делается вывод о том,
что функция потерпела неудачу, то среда исполнения генерирует исключение. Чтобы этого избежать нужно использовать атрибут PreserveSig, который заставляет среду исполнения игнорировать возвращаемое значение.
Если возвращаемое значение не равно S_OK, то считается, что функция не смогла успешно выполнить свою работу.
|
-
Visual Basic.NET
Imports System.Runtime.InteropServices
Public Class Win32
<DllImport ("user32.dll", PreserveSig := False)> _
Public Shared Function MessageBox Lib "user32.dll"(hWnd As Integer, _
txt As String, caption As String, Typ As Integer) As Integer
End Function
End Class
|
-
C#
using System.Runtime.InteropServices;
public class Win32
{
[DllImport("user32.dll", PreserveSig=False)]
public static extern int MessageBox(int hWnd, string text, string caption,uint type);
}
|
-
Managed Visual C++
using namespace System::Runtime::InteropServices;
typedef void* HWND;
[DllImport("user32", PreserveSig=True)]
int MessageBox(HWND hWnd,
String* pText,
String* pCaption,
unsigned int uType);
|
Этот атрибут не так бесполезен, как кажется. Потому что значение функции может быть совершенно правильным, а среда исполнения, посчитав его "инвалидным", будет генерировать исключения при каждом вызове этой функции. Так что,
по-вашему, будет легче: отключить генерацию исключений или при каждом вызове их отлавливать и обрабатывать?
В дополнении ко всему сказанному, существует еще один атрибут: 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
Ну а теперь, как обычно смотрите примерчик:
-
Visual Basic.NET
Imports System.Runtime.InteropServices
<StructLayout(LayoutKind.Sequential)>
Public Structure Point
Public x As Integer
Public y As Integer
End Structure
Public Structure <StructLayout(LayoutKind.Explicit)> Rect
Public <FieldOffset(0)> left As Integer
Public <FieldOffset(4)> top As Integer
Public <FieldOffset(8)> right As Integer
Public <FieldOffset(12)> bottom As Integer
End Structure
Class Win32API
Declare Auto Function PtInRect Lib "user32.dll" _
(ByRef r As Rect, p As Point) As Boolean
End Class
|
-
C#
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
public int x;
public int y;
}
[StructLayout(LayoutKind.Explicit]
public struct Rect
{
[FieldOffset(0)] public int left;
[FieldOffset(4)] public int top;
[FieldOffset(8)] public int right;
[FieldOffset(12)] public int bottom;
}
class Win32API
{
[DllImport("User32.dll")]
public static extern bool PtInRect(ref Rect r, Point p);
}
|
-
Managed Visual C++
ПРИМЕЧАНИЕ
Приводить пример здесь бессмысленно, так как это язык вкупе с управляемыми данными
поддерживает также и не управляемые. Таким образом, данная возможность здесь просто излишня.
|
Функции "обратного вызова"
Многие стандартные API функции в качестве аргумента принимают указатель на функцию. Для примера можно привести CreateThread,
EnumPrinters, SetWindowsHookEx, EnumWindows и многие другие не менее полезные функции.
Данный механизм, благодаря своей гибкости, позволяет настраивать поведение API функций в широких пределах. Разработчики среды исполнения .NET не забыли подумать и о нем,
мы с легкостью сможем применять этот механизм в наших приложениях для .NET.
Если вам все еще не понятно, о чем я тут толкую, взгляните на картинку.
Обратный вызов функций
Давайте рассмотрим, что нам придется для этого сделать.
-
Для начала надо описать нашу функцию, которая будет вызываться из DLL.
Здесь главное правильно соблюсти типы аргументов и возвращаемого значения.
-
Необходимо описать делегат на нашу функцию.
-
Описать функцию, которую мы собираемся вызывать из DLL. В качестве типа одного
из аргументов нам понадобиться описанный ранее делегат. Этот аргумент как раз и принимает
указатель на нашу функцию.
-
Далее просто надо вызвать необходимую нам функцию из DLL и передать ей
указатель на нашу функцию способом, специфичным для каждого языка.
Что бы вам было более понятно, я приведу пример.
-
Visual Basic.NET
Imports System
Imports System.Runtime.InteropServices
Public Delegate Function CallBack( hwnd As Integer, lParam As Integer) As Boolean
Public Class Application
Declare Function EnumWindows Lib "user32" ( x As CallBack, y As Integer) As Integer
Public Shared Sub Main()
EnumWindows(AddressOf EnumReportApp.Report, 0)
End Sub
Public Shared Function Report(hwnd As Integer, lParam As Integer) As Boolean
Console.Write("Window handle is ")
Console.WriteLine(hwnd)
Return True
End Function
End Class
|
-
C#
using System;
using System.Runtime.InteropServices;
public delegate bool CallBack(int hwnd, int lParam);
public class Application
{
[DllImport("user32")]
public static extern int EnumWindows(CallBack x, int y);
public static void Main()
{
CallBack myCallBack = new CallBack(EnumReportApp.Report);
EnumWindows(myCallBack, 0);
}
public static bool Report(int hwnd, int lParam)
{
Console.Write("Window handle is ");
Console.WriteLine(hwnd);
return true;
}
}
|
-
Managed Visual C++
using namespace System::Runtime::InteropServices;
__delegate bool CallBack(int hwnd, int lParam);
__gc class EnumReport
{
public:
bool Report(int hwnd, int lParam)
{
Console::Write(L"Window handle is ");
Console::WriteLine(hwnd);
return
true;
}
};
[DllImport("user32")]
int EnumWindows(CallBack* x, int y);
void main() {
EnumReport* er = new EnumReport;
CallBack* myCallBack = new CallBack(er, &EnumReport::Report);
EnumWindows(myCallBack, 0);
}
|
У вас может возникнуть вопрос: каким же образом код из DLL вызывает код из среды .NET? Что, не
видите никаких проблем? А дело вот в чем. Ведь код .NET и родной код системы кардинально
различаются, и поэтому в принципе не могут вызывать друг друга. Для того чтобы решить эту проблему,
стандартный маршалер поступает следующим образом: он создает маленькую native-функцию.
Которая занимается только тем, что изменяет параметры и вызывает реальную .NET. Именно адрес этой функции передаётся в DLL. Таким образом, для вызова функций .NET из DLL используются переходники.
Заключение
Ну вот вроде и все, что я хотел сказать о взаимодействии с DLL из среды исполнения .NET.
Собственно говоря, больше ничего и не осталось. Но вы не расслабляйтесь, это только начало:
вызов функций из DLL является одним из самых простых взаимодействий.
Вскоре я вам поведаю о взаимодействии с COM.
Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы
то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских
прав.