В этой статье мы обсудим взаимодействие с динамически подключаемыми библиотеками. Если вы уже занимались программированием для .NET, вы наверняка заметили, что использовали общую библиотеку, предоставляемую средой исполнения, и наверняка не делали ни одного системного вызова. То есть вы не обращались к Windows API напрямую, а следовательно, ваш код платформенно независим. А это, в свою очередь, означает, что он может быть выполнен на любой платформе, где будет присутствовать среда исполнения .NET с общей библиотекой исполнения. Это, конечно, здорово, но что же делать, если вы строго ориентированы на платформу Windows, и вам необходимо использовать уже разработанный вами код? Вы, наверное, очень обрадуетесь, когда узнаете, что сделать это будет очень легко. Сейчас я поведаю вам все тонкости этого процесса.
Допустим, вы хотите самостоятельно вызвать некоторую функцию Windows API. Для этого вам надо будет знать, в какой библиотеке она размещена. Вы можете узнать это, найдя данную функцию в Platform SDK и посмотрев в разделе Requirements значение пункта Library. Когда имя библиотеки найдено, можно считать, что пол дела уже сделано. Далее сделаем следующее.
В статье я буду рассказывать об аттрибуте DllImport, но взгянув на код, написанный на MSIL, вы, к своему удивлению, не найдёте там даже упоминания об этом аттрибуте. Это нормально. Язык MSIL имеет встроенные возможности по взаимодействию с DLL при помощи ключевых слов. |
'Данный пример иллюстрирует вызов функции напрямую из DLL 'при его написании я использовал только встроенные средства 'языка Visual Basic.NET, не прибегая к использованию 'атрибутов Imports System Public Module Application 'Вот это и есть описание импортируемой функции 'которую мы намерены вызывать впоследствии Declare Auto Function MessageBox Lib "user32.dll" ... (hWnd As Integer,txt As String, caption As String, Typ As Integer) As Integer Sub Main() 'Вызов функции из DLL 'Обратите внимание на типы параметров они 'должны четко совпадать с теми что мы описали ранее MessageBox(0,"Hello World","",0) End Sub End Module |
То же самое, но с использование атрибутов.
Imports System 'Подключим сервисы взаимодействия с операционной системой Imports System.Runtime.InteropServices Public Module Application 'Применим атрибут для указания того, что функция 'будет вызываться из DLL User32.dll <DllImport ("user32.dll")> _ 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 |
using System; //Эта строка подключает сервисы взаимодействия с операционной системой using System.Runtime.InteropServices; class Application { //Опишем функцию, которую будем вызывать из DLL [DllImport ("user32.dll")] public static extern int MessageBox(int hWnd, string text, string caption, uint type); public static void Main() { //Эта функция будет вызвана из DLL User32.dll MessageBox(0,"Hello World","It's a good day to win",0); } } |
#using <mscorlib.dll> using namespace System; //В данном пространстве имен хранятся все сервисы, //отвечающие за взаимодействие с операционный системой. //В том числе атрибут DllImport, который и позволяет //нам вызывать DLL using namespace System::Runtime::InteropServices; //я описал данный тип самостоятельно, так как мне не хотелось //засорять код лишними includ'ами typedef void* HWND; //Это и есть описание фунции которую мы собираемся вызывать из //DLL. [DllImport("user32.dll")] int MessageBox(HWND hWnd, String* Message, String* Title, unsigned int uiType); void main() { //Ну а теперь вызовем эту функцию MessageBox(0,"Hello World","",0); } |
//Это имя нашей сборки .assembly App {} //Подключим DLL User32.dll как внешний модуль //так как мы собираемся использовать его функцию .module extern User32.dll .assembly extern mscorlib {} //Опишем функцию которую мы хотим вызвать из DLL //обратите внимание на модификатор pinvokeimpl, который //и собственно указывает на то что функция находиться в DLL .method public static pinvokeimpl("user32.dll" 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 //Первый параметр (HWND родительского окна) ldstr "Hello World" //Второй параметр (сообщение) ldstr "It is a good day" //Третий ... (заголовок) ldc.i4.0 //Четвёртый.. (флаги) //Ну а теперь вызываем нашу функцию call int32 MessageBox(int32, string, string, unsigned int32) //Теперь после выполнения функции надо убрать из стека //значение которое вернула функция. pop //Благополучно выходим ret } |
Эти маленькие примерчики демонстрируют вызов всем известной функции 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. Этот параметр предписывает маршалеру самостоятельно выбрать нужную функцию. Вы можете задавать эти параметры следующим образом.
Declare Function MessageBoxA Lib "dllname"_ (hWnd As Integer, txt As String, caption As String, Typ As Integer) As Integer Declare Unicode Function MessageBoxW Lib "dllname"_ (hWnd As Integer, txt As String, caption As String, Typ As Integer) As Integer Declare Auto Function MessageBox Lib "dllname" _ (hWnd As Integer, txt As String, caption As String, Typ As Integer) As Integer |
Тоже самое, но с использование атрибутов.
<DllImport ("dllname", CharSet := Charset.Ansi)> _ Public Function MessageBox (hWnd As Integer, _ txt As String, caption As String, Typ As Integer) As Integer End Function |
[DllImport("dllname", CharSet=CharSet.Ansi)] [DllImport("dllname", CharSet=CharSet.Unicode)] [DllImport("dllname", CharSet=CharSet.Auto)] |
[DllImport("dllname", CharSet=CharSet::Ansi)] [DllImport("dllname", CharSet=CharSet::Unicode)] [DllImport("dllname", CharSet=CharSet::Auto)] |
pinvokeimpl("dllname" ansi winapi) pinvokeimpl("dllname" unicode winapi) pinvokeimpl("dllname" autochar winapi) |
Вы должны понимать, что основное назначение этого атрибута не в том, чтобы подбирать функции по именам, а в том, чтобы настроить режим конвертирования строк, которые будут участвовать при передаче параметров, в функцию. Грубо говоря, этот параметр отвечает за то, к какому набору символов будет преобразована строка при передаче в функцию.
Для увеличения скорости маршалинга нужно правильно использовать параметры Unicode и ANSI. Если типы передаваемых строк совпадут, то маршалеру не понадобиться преобразовывать данные из Unocode в ANSI или обратно, тем самым вы увеличите скорость маршалинга.
ПРИМЕЧАНИЕ Я бы вам советовал использовать везде, где только можно, набор символов Unicode, так как внутри вся система (здесь имеется ввиду Windows 2000) построена на Unicode, а ANSI-функции являются всего лишь заглушками для преобразования ANSI в Unicode. |
Разработчики стандартного маршалера явно думали о нас, и поэтому нам предоставлена такая нужная и полезная возможность как переименовывание функций. То есть вы можете импортировать функцию, присвоив ей другое имя. Эта возможность реализуется при помощи атрибута EntryPoint. Приведем пример:
'Данный пример иллюстрирует вызов функции напрямую из DLL 'при его написании я использовал только встроенные средства 'языка Visual Basic.NET, не прибегая к использованию 'атрибутов Imports System Public Module Application 'Вот это и есть описание импортируемой функции 'которую мы намерены вызывать впоследствии 'При помощи ключевого слова Alias мы задаем реальное имя функции Declare Auto Function MsgBox Lib "user32.dll" Alias "MessageBox" _ (hWnd As Integer,txt As String, caption As String, Typ As Integer) As Integer Sub Main() 'Вызов функции из DLL 'Обратите внимание на типы параметров они 'должны четко совпадать с теми что мы описали ранее MsgBox(0,"Hello World","",0) End Sub End Module |
Тоже самое, но с использование атрибутов.
Imports System 'Подключим сервисы взаимодействия с операционной системой Imports System.Runtime.InteropServices Public Module Application 'Применим атрибут для указания того, что функция 'будет вызываться из DLL User32.dll 'Параметр EntryPoint определяет реальное имя функции <DllImport ("user32.dll", EntryPoint := "MessageBox")> _ Public Function MsgBox (hWnd As Integer, _ txt As String, caption As String, Typ As Integer) As Integer End Function Public Sub Main() 'Вызовем, определенную нами выше функцию MsgBox(0,"Hello World","It's a very good day",0) End Sub End Module |
using System; //Эта строка подключает сервисы взаимодействия с операционной системой using System.Runtime.InteropServices; class Application { //Опишем функцию, которую будем вызывать из DLL //Параметр EntryPoint задает реальное имя функции [DllImport ("user32.dll", CharSet = CharSet.Auto, EntryPoint="MessageBoxW")] public static extern int MsgBox(int hWnd, string text, string caption, uint type); public static void Main() { //Эта функция будет вызвана из DLL User32.dll MsgBox(0,"Hello World","It's a good day to win",0); } } |
#using <mscorlib.dll> using namespace System; //В данном пространстве имен хранятся все сервисы, //отвечающие за взаимодействие с операционный системой. //В том числе атрибут DllImport, который и позволяет //нам вызывать DLL using namespace System::Runtime::InteropServices; //я описал данный тип самостоятельно, так как мне не хотелось //засорять код лишними include'ами typedef void* HWND; //Это и есть описание функции которую мы собираемся вызывать из //DLL. //Параметр EntryPoint определяет реальное имя функции [DllImport("user32.dll",EntryPoint="MessageBox")] int MsgBox(HWND hWnd, String* Message, String* Title, unsigned int uiType); void main() { //Ну а теперь вызовем эту функцию MsgBox(0,"Hello World","",0); } |
//Это имя нашей сборки .assembly App {} //Подключим DLL User32.dll как внешний модуль //так как мы собираемся использовать его функцию .module extern User32.dll .assembly extern mscorlib {} //Опишем функцию которую мы хотим вызвать из DLL //обратите внимание на модификатор pinvokeimpl, который //и собственно указывает на то что функция находиться в DLL //Модификатор as задаёт реальное имя функции .method public static pinvokeimpl("user32.dll" as "MessageBox" winapi) int32 MsgBox(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 //Первый параметр (HWND родительского окна) ldstr "Hello World" //Второй параметр (сообщение) ldstr "It is a good day" //Третий ... (заголовок) ldc.i4.0 //Четвёртый.. (флаги) //Ну а теперь вызываем нашу функцию call int32 MsgBox(int32, string, string, unsigned int32) //Теперь после выполнения функции надо убрать из стека //значение которое вернула функция. pop //Благополучно выходим ret } |
Атрибут EntryPoint ведет себя немного хитрее, чем кажется, его поведение меняется в зависимости от формата строки, назначенной ему:
|
Таким образом, вы можете импортировать функцию из динамической библиотеки и объявить ей любое имя. Данная возможность с первого взгляда может показаться не особо нужной. Но это на самом деле не так. Представьте себе, что вам нужно импортировать функцию в определенное пространство имен, а она конфликтует с уже существующим в нём именем. Как раз такие проблемы и призвана решить эта возможность.
Большинство создателей динамически загружаемых библиотек не придерживается стандартов именования функций в соответствии с поддерживаемым набором символов (Unicode или ANSI), то есть они попросту не ставят постфиксов W и A. Если, к примеру, создатель библиотеки решил, что она будет работать с ANSI, то будьте уверены: ничто не заставит его сделать заглушки для Unicode. Тут-то и может возникнуть проблема, когда нужно вызвать функцию для соответствующего набора символов, которая не имеет нужного постфикса. Нам на помощь придет атрибут ExactSpelling, который запретит (разрешит) маршалеру изменять определенное нами имя функции, при помощи атрибута EntryPoint. А делается это вот так:
Imports System 'Подключим сервисы взаимодействия с операционной системой Imports System.Runtime.InteropServices Public Module Application 'Применим атрибут для указания того, что функция 'будет вызываться из DLL User32.dll <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 |
using System; //Эта строка подключает сервисы взаимодействия с операционной системой using System.Runtime.InteropServices; class Application { //Опишем функцию, которую будем вызывать из DLL [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() { //Эта функция будет вызвана из DLL User32.dll MessageBox(0,"Hello World","It's a good day to win",0); } } |
#using <mscorlib.dll> using namespace System; //В данном пространстве имен хранятся все сервисы, //отвечающие за взаимодействие с операционный системой. //В том числе атрибут DllImport, который и позволяет //нам вызывать DLL using namespace System::Runtime::InteropServices; //я описал данный тип самостоятельно, так как мне не хотелось //засорять код лишними include'ами typedef void* HWND; //Это и есть описание фунции которую мы собираемся вызывать из //DLL. [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); } |
//Это имя нашей сборки .assembly App {} //Подключим DLL User32.dll как внешний модуль, //так как мы собираемся использовать его функцию .module extern User32.dll .assembly extern mscorlib {} //Опишем функцию которую мы хотим вызвать из DLL //обратите внимание на модификатор pinvokeimpl, который //и собственно указывает на то что функция находиться в DLL .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 //Первый параметр (HWND родительского окна) 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). Как это делается, я продемонстрировал ниже.
'Данный пример иллюстрирует вызов функции напрямую из DLL 'при его написании я использовал только встроенные средства 'языка 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) 'Не пытайтесь сделать перенос строки при помощи символа \n 'так как VB ничего о нем не знает, и соответственно не преобразует его 'в символ с кодом 13. Я говорю это потому что сам сначала попался на этом. 'Если честно я был слегка удивлен когда вместо перевода строки на консоль 'было выведено следующее \n. Call Console.WriteLine() printf("Hello World: %i %s",2,"Hehe") End Sub End Module |
using System; //Эта строка подключает сервисы взаимодействия с операционной системой using System.Runtime.InteropServices; public class App { //C# не поддерживает функций с изменяемым числом параметров //поэтому нам придется самим определить функции для каждого конкретного //случая [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"); } } |
#using <mscorlib.dll> using namespace System; //В данном пространстве имен хранятся все сервисы, //отвечающие за взаимодействие с операционный системой. //В том числе атрибут DllImport, который и позволяет //нам вызывать DLL using namespace System::Runtime::InteropServices; //Опишем две функции для каждого конкретного случая //так как .NET не поддерживает функций с изменяемым //числом параметров [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"); } |
//Это имя нашей сборки .assembly App {} //Подключим DLL User32.dll как внешний модуль //так как мы собираемся использовать его функцию .module extern User32.dll .assembly extern mscorlib {} //Нам придётся описать две функции для каждого конкретного случая //так как IL не поддерживает функций с несколькими параметрами .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, то считается, что функция не смогла успешно выполнить свою работу. |
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 |
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); } |
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 |
Если вы пишете на MSIL, то вам нужно будет использовать ключевое слово lasterr. То есть написать следующее:
|
С первого взгляда может показаться, что передача структур достаточно тривиальна. Чего тут, описал структуру, передал, и дело в шляпе. Не тут-то было. Ведь по умолчанию все данные являются управляемыми, то есть их размещением в памяти будет управлять среда исполнения .NET. И нет никакой гарантии того, что поля структуры будут расположены в памяти последовательно друг за другом, как нам хочется. Скажу даже больше, среда .NET будет размещать поля структуры в памяти, руководствуясь в первую очередь правилами оптимизации. Вследствие чего могут возникнуть очень неприятные ошибки, связанные с распределением памяти. Для того чтобы управлять размещением структур в памяти, используется атрибут LayoutKind. Данный атрибут может принимать три значения, перечисленные ниже:
Поле |
Описание |
|
LayoutKind.Automatic |
Позволяет среде исполнения .NET перераспределять
|
|
LayoutKind.Explicit |
Поля выравниваются в соответствии с атрибутом |
|
|
в том порядке, в котором они были описаны.
|
Ну а теперь, как обычно смотрите примерчик:
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 |
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); } |
ПРИМЕЧАНИЕ Приводить пример здесь бессмысленно, так как это язык вкупе с управляемыми данными поддерживает также и не управляемые. Таким образом, данная возможность здесь просто излишня. |
Многие стандартные API функции в качестве аргумента принимают указатель на функцию. Для примера можно привести CreateThread, EnumPrinters, SetWindowsHookEx, EnumWindows и многие другие не менее полезные функции. Данный механизм, благодаря своей гибкости, позволяет настраивать поведение API функций в широких пределах. Разработчики среды исполнения .NET не забыли подумать и о нем, мы с легкостью сможем применять этот механизм в наших приложениях для .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 |
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; } } |
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 используются переходники.