Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Framework »

Обсудить на форуме Обсудить на форуме

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

 

Source.zip - 30 KB

Введение

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

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

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

ПРИМЕЧАНИЕ
В статье я буду рассказывать об аттрибуте DllImport, но взгянув на код, написанный на MISL, вы, к своему удивлению, не найдёте там даже упоминания об этом аттрибуте. Это нормально. Язык MISL имеет встроенные возможности по взаимодействию с DLL при помощи ключевых слов.
  • Viasual Basic.NET
    'Данный пример иллюстрирует вызов функции напрямую из 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
      "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
    
  • C#
    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);
      }
    }
    
  • Managed Visual C++
    #using 
    
    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);
    }
    
  • Intermediate Language
    //Это имя нашей сборки
    .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 в память (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. Этот параметр предписывает маршалеру самостоятельно выбрать нужную функцию. Вы можете задавать эти параметры следующим образом.

  • Visual Basic.NET
    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
    

    Тоже самое, но с использование атрибутов.

    "dllname", CharSet := Charset.Ansi)> _
    Public Function MessageBox (hWnd As Integer, _
                  txt As String, caption As String, Typ As Integer) As Integer
    End Function
    
  • C#
    [DllImport("dllname", CharSet=CharSet.Ansi)]
    [DllImport("dllname", CharSet=CharSet.Unicode)]
    [DllImport("dllname", CharSet=CharSet.Auto)]
    
  • Managed Visual C++
    [DllImport("dllname", CharSet=CharSet::Ansi)]
    [DllImport("dllname", CharSet=CharSet::Unicode)]
    [DllImport("dllname", CharSet=CharSet::Auto)]
    
  • Intermediate Language
    pinvokeimpl("dllname" ansi winapi)
    pinvokeimpl("dllname" unicode winapi)
    pinvokeimpl("dllname" autochar winapi)
    

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

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

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

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

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

  • Viasual Basic.NET
    'Данный пример иллюстрирует вызов функции напрямую из 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 определяет реальное имя функции
      "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
    
  • C#
    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);
      }
    }
    
  • Managed Visual C++
    #using 
    
    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);
    }
    
  • Intermediate Language
    //Это имя нашей сборки
    .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 ведет себя немного хитрее, чем кажется, его поведение меняется в зависимости от формата строки, назначенной ему:

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

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

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

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

  • Viasual Basic.NET
    Imports System
    'Подключим сервисы взаимодействия с операционной системой
    Imports System.Runtime.InteropServices
    
    Public Module Application
    
      'Применим атрибут для указания того, что функция
      'будет вызываться из DLL User32.dll
      "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
    {
      //Опишем функцию, которую будем вызывать из 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);
      }
    }
    
  • Managed Visual C++
    #using 
    
    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);
    }
    
    
  • Intermediate Language
    //Это имя нашей сборки
    .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). Как это делается, я продемонстрировал ниже.

  • Viasual Basic.NET
    'Данный пример иллюстрирует вызов функции напрямую из DLL
    'при его написании я использовал только встроенные средства
    'языка Visual Basic.NET, не прибегая к использованию
    'атрибутов
    
    Imports System
    Imports System.Runtime.InteropServices
    
    Public Module Application
    
      //Опишем две функции для каждого конкретного случая
      //так как .NET не поддерживает функций с изменяемым
      //числом параметров
    
      "msvcrt.dll", CallingConvention := CallingConvention.Cdecl)> _
      Overloads Function printf ( _
        format As String, i As Integer, d As Double) As Integer
      End Function
    
      "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
    
  • C#
    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");
      }
    }
    
  • Managed Visual C++
    #using 
    
    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");
    }
    
  • Intermediate Language
    //Это имя нашей сборки
    .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, то считается, что функция не смогла успешно выполнить свою работу.
  • Viasual Basic.NET
                    Imports System.Runtime.InteropServices
    
                    Public Class Win32
                      "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
ПРИМЕЧАНИЕ
 

Если вы пишете на MISL, то вам нужно будет использовать ключевое слово 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

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

  • Viasual Basic.NET
              Imports System.Runtime.InteropServices
    
              
              Public Structure Point
                Public x As Integer
                Public y As Integer
              End Structure
    
              Public Structure  Rect
                Public  left As Integer
                Public  top As Integer
                Public  right As Integer
                Public  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 и передать ей указатель на нашу функцию способом, специфичным для каждого языка.

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

  • Viasual 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.


Может пригодится:


Автор: Алексей Дубовцев
Прочитано: 11930
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру