Введение в программирование на C# 2.0

Совмещение управляемого и неуправляемого кодов

Показывать лекцию целиком

Программный код, выполняющийся под управлением CLR, называется управляемым кодом.

Программный код, выполняющийся вне среды выполнения CLR, называется неуправляемым кодом.

Примеры неуправляемого программного кода:

  • функции Win32 API;
  • компоненты COM;
  • интерфейсы ActiveX.
  • .NET появилась не на пустом месте. Вновь разрабатываемый управляемый код вынужден взаимодействовать с существующим неуправляемым программным кодом. Поэтому на платформе .NET предусмотрены различные сценарии установления взаимодействия между управляемым и неуправляемым кодами. Microsoft .NET Framework обеспечивает взаимодействие с компонентами COM, службами COM+, внешними библиотеками типов и многими службами операционной системы.

    CLR скрывает имеющиеся в управляемой и неуправляемой моделях различия. Проблемы, связанные с типами данных, механизмами обработки ошибок и т. д. в управляемой и неуправляемой моделях, решаются CLR "незаметно" как для вызывающей стороны (клиента), так и для вызываемой стороны (сервера). Таким образом, организация взаимодействия между управляемым и неуправляемым кодом выглядит проще, чем могло быть...

    C++ .NET. Совмещение управляемого и неуправляемого кодов

    Реализованные в .NET языки программирования позволяют создавать управляемый код, который может взаимодействовать с неуправляемыми библиотеками Win32 и компонентами на основе модели компонентных объектов Microsoft (COM).

    Язык программирования C++ .NET является единственным, который позволяет создавать как управляемый, так и неуправляемый код. Это дает возможность не только использовать неуправляемый библиотечный код, но и смешивать управляемый и неуправляемый коды в одном приложении.

    В приводимых ниже примерах кроме языка программирования C# будет использоваться язык C++.

    Управляемый код. Осознать разницу

    Первый шаг тривиален. Создается C++ Win32 Console Project, то есть Неуправляемое Консольное Приложение на C++. Последняя версия Visual Studio .NET 2005 позволяет в широких пределах смешивать управляемый и неуправляемый коды.

    // Транслятору задали опцию /clr.
     // Эта опция преобразует все объявляемые в программе типы.
     // От имени объекта элементарного типа (например, int) можно
     // вызвать методы базового класса object.
     // Но только вызываются они лишь из фрагментов управляемого 
    кода.
     // Иначе – сообщение транслятора:
     // ... managed type or function cannot be used in an unmanaged function
    
     #include "stdafx.h"
    
     #include <iostream>
     using namespace std;
    
     #using <mscorlib.dll> // Ядро CLR
    using namespace System;
    
    class uClass; // Предварительное неполное объявление класса.
     class mClass; // Предварительное неполное объявление класса.
    
     #pragma managed
    class mClass
     {
     public:
     uClass *ucP;  
    mClass *mcP;  	
    int x;
    
    mClass()
       {
     // Только в неуправляемом коде!
     // cout << "Ha-Ha-Ha";
    
    Console::WriteLine("mClass");
     // Легко посмотрели значение непроинициализированной переменной.
     Console::WriteLine(x.ToString());
     }
     ~mClass()
       {
     	  Console::WriteLine("~mClass");
       }
    
    void mcF0()
     {
      Console::WriteLine("mcF0()");
     }
    
    mClass* mcGenerator();
    
     };
     #pragma unmanaged
    
    class uClass
     {
     public:
     uClass *ucP;  
    mClass *mcP;  	
    int x;
     	
    uClass()
       {
        // Только в управляемом коде!
        //Console::WriteLine("Ha-Ha-Ha");
        printf("uClass\n");
       }
     ~uClass()
       {
        printf("~uClass\n");
       }
    
    void ucF0()
     {
      cout << "ucF0()\n";
     }
    
    uClass* ucGenerator();
     };
     // Судя по всему, функция Управляемого
     // класса может быть НеУправляемой!
     mClass* mClass::mcGenerator()
     {
      //x.ToString();
      //Console::WriteLine("Ha-Ha-Ha");
      cout << "Ha-Ha-Ha from unmanaged function of managed class!" << endl;
      ucP = new uClass();
      ucP->ucF0();
      delete ucP;
      return new mClass();
     }
    
     #pragma managed
     // А сделать Управляемой функцию НеУправляемого класса невозможно. 
     // Прагма managed для функции - члена неуправляемого класса игнорируется. 
    uClass* uClass::ucGenerator()
     {
      cout << "Ha-Ha-Ha from function of unmanaged class!" << endl;
      //Console::WriteLine("Ha-Ha-Ha");
      //x.ToString();
      mcP = new mClass();
      mcP->mcF0();
      delete mcP;
      return new uClass();
     }
     #pragma unmanaged
    
    int _tmain(int argc, _TCHAR* argv[])
     {
     void *xPoint;
     int x = 125;	
    
     // Только не смешивать!
     //Console::WriteLine("Ha-Ha-Ha");
    
    mClass *mc = new mClass();
     mc->mcF0();
     xPoint = mc->mcGenerator();
     delete (mClass*)xPoint;
     delete mc;
    
    uClass *uc = new uClass();
     uc->ucF0();
     xPoint = uc->ucGenerator();
     delete (uClass*)xPoint;
     delete uc;
    
    return 0;
     }

    Управляемая библиотека

    Создание кода управляемой библиотеки тривиально. Для этого в Visual Studio предусмотрены специальные опции. Новый проект создается как библиотека классов (Class Library). Сборка автоматически получает расширение .dll.

    C# основывается на парадигме объектно-ориентированного программирования, поэтому библиотека классов представляет собой все то же объявление класса.

    Методы – члены класса составляют основу функциональности библиотеки. Наличие пары явно определяемых конструкторов – вынужденная мера. Это требование со стороны модуля на C++, использующего библиотеку. Там невозможно создать объект без явным образом объявленных конструкторов умолчания и копирования:

    using System;
    
    namespace CSLib00
     {
     public class Class1
     {
     // Явное объявление конструктора без параметров.
     public Class1()
     {
     }
             
     // Явное объявление конструктора копирования.
     // Конструктор с одним параметром – ссылкой на
     // объект - представитель собственного класса.
     // Именно такие конструкторы называются конструкторами
     // копирования.
     public Class1(Class1 cKey)
     {                
     }
    
     // Реализация функциональности. Метод - член класса Class1 Summ.
     public int Summ(int key1, int key2)
     {
     Console.WriteLine("this is Summ from CSLib00.dll");
     return (key1 + key2);        
     }
     }
     }

    Управляемая библиотека в управляемом коде

    Использование управляемой библиотеки в управляемом коде тривиально. Главная проблема, которая возникает при разработке приложения, использующего код управляемой библиотеки, – добавить ссылку (Add Reference) на библиотечную сборку. Для этого нужно указать месторасположение сборки в соответствующем диалоговом окне.

    После этого Visual Studio копирует сборку в директорию, в которой располагается разрабатываемый код. При согласовании пространств имен (оператор using или использование полного имени с точечной нотацией) библиотечного модуля и модуля клиента, библиотечные классы и методы готовы к использованию в коде клиента:

    using System;
    
    namespace CSClient
     {
     class Program
     {
     static void Main(string[] args)
     {
     // Здесь используется точечная нотация.
     // Явное указание принадлежности имени класса
     // пространству имен CSLib00.  
    CSLib00.Class1 c1 = new CSLib00.Class1();
     // Создали объект, затем вызываем метод!
     Console.WriteLine("the res = {0}", c1.Summ(1, 2));
     }
     }
     }

    Управляемая библиотека в неуправляемом коде

    Особенности разработки неуправляемого программного кода, использующего управляемую библиотеку, состоят в следующем:

  • неуправляемый код транслируется с опцией /clr;
  • добавляется ссылка на библиотечную сборку;
  • определяется управляемая функция, в теле которой с помощью оператора gcnew создается объект объявленного в управляемой библиотеке класса, от имени которого вызывается библиотечная функция.
  • #include "stdafx.h"
     #using <mscorlib.dll>
     using namespace System;
     using namespace CSLib00;
    
     #pragma managed
    void managedLibStarter()
     {
      Class1 cl1 = gcnew Class1();
      Console::WriteLine("{0}",cl1.Summ(1,2));
     }
     #pragma unmanaged
    
    int _tmain(int argc, _TCHAR* argv[])
     {
      printf("QWERTY\n");
      managedLibStarter();
      return 0;
     }

    Вызов неуправляемых функций из управляемого модуля

    Сначала определение.

    Платформный вызов — это служба, которая позволяет управляемому программному коду вызывать неуправляемые функции, реализованные в библиотеках динамической компоновки (DLL), например, функции библиотек Win32 API.

    Служба платформного вызова обеспечивает поиск и вызов экспортируемой функции и в случае необходимости обеспечивает маршалинг ее параметров (передачу параметров через границы процесса).

    Платформный вызов позволяет управлять значительной частью операционной системы посредством вызова функций Win32 API и других библиотек DLL. В дополнение к Win32 API, через платформный вызов доступны многие другие интерфейсы API и DLL.

    Вызов неуправляемых функций средствами платформного вызова предполагает следующие действия:

  • идентификация вызываемой функции в библиотеке DLL;
  • создание класса для сохранения вызываемой функции;
  • объявление прототипа (!) вызываемой функции в управляемом коде;
  • вызов функции.
  • При вызове неуправляемой функции платформный вызов выполняет следующую последовательность действий:

  • определяется местонахождение DLL-библиотеки, содержащей функцию;
  • DLL загружается в память;
  • определяется месторасположение вызываемой функции (адрес функции в памяти);
  • значения параметров функции заносятся в стек;
  • в случае необходимости осуществляется маршалинг данных;
  • осуществляется передача управления неуправляемой функции.
  • В лучших традициях .NET определение местонахождения и загрузка DLL, а также определение местоположения адреса функции в памяти, выполняется только при первом вызове функции.

    Идентификация вызываемой функции

    Идентификация функции DLL предполагает:

  • указание имени вызываемой функции;
  • указание имени содержащей ее библиотеки DLL (имени файла).
  • Например, идентификация функции MessageBox включает имя ( MessageBox ) и имя файла (User32.dll, User32, или user32). Программный интерфейс приложений Microsoft Windows (Win32 API) может содержать несколько версий функции. Например, версии функций, которые работают с символами и строками: для 1-байтовых символов – ANSI и для 2-байтовых символов – Unicode.

    Так MessageBoxA является точкой входа для функции MessageBox версии ANSI, а MessageBoxW — точкой входа для аналогичной функции версии Unicode.

    Имена функций, располагаемых в определенной библиотеке DLL, например, user32.dll, можно получить с помощью ряда инструментов в режиме командной строки. Например, для получения имен функций можно использовать вызовы

    dumpbin –exports user32.dll

    или

    link –dump –exports user32.dll.

    Создание класса для размещения библиотечной функции

    Эта деятельность называется упаковкой неуправляемой функции в обертку управляемого класса.

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

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

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

    Прототипы в управляемом коде

    Для обращения к неуправляемой функции DLL из управляемого программного кода требуется знать имя функции и имя библиотеки DLL, которая ее экспортирует.

    Располагая этой информацией, можно начинать создавать управляемое определение для неуправляемой функции, реализованной в DLL.

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

    using System.Runtime.InteropServices;
     [DllImport("user32.dll")]
     public static extern int MessageBox(int hWnd,
              String text, 
              String caption,
              uint type);

    Поведение управляемого программного кода при его выполнении задается значениями полей атрибутов. Служба платформного вызова работает в соответствии с набором стандартных значений различных полей атрибутов, которые хранятся как метаданные в сборке.

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

    Управление службой платформного вызова сводится к применению полей атрибутов. Знание правил применения полей атрибутов – основа управления службой платформного вызова!

    ПолеОписание
    BestFitMapping Отключает наилучшее соответствие
    CallingConvention Задает соглашение о вызовах, которое должно использоваться при передаче аргументов методов. По умолчанию используется WinAPI, что соответствует __ stdcall для 32-разрядных платформ на основе процессора Intel
    CharSet Управляет передачей имен и задает способ маршалинга строковых аргументов в функцию. Стандартное значение — CharSet.Ansi
    EntryPoint Задает точку входа DLL для вызова
    ExactSpelling Указывает, должна ли быть изменена точка входа в соответствии с символьным набором. Стандартное значение варьируется в зависимости от языка программирования
    PreserveSig Указывает, должна ли управляемая подпись метода быть преобразована в неуправляемую подпись, которая возвращает значение HRESULT и для возвращаемого значения имеет дополнительный аргумент [out, retval].

    По умолчанию используется значение true (подпись не должна преобразовываться)

    SetLastError Позволяет вызывающему объекту для определения факта ошибки при выполнении метода использовать API-функцию Marshal.GetLastWin32 Error. В Visual Basic по умолчанию используется значение true; в C# и C++ — значение false
    ThrowOnUnmappableChar Управляет возникновением исключения при появлении несопоставимого символа Unicode, который преобразуется в символ ANSI " ?"

    Указание точки входа

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

    Для переименования функции DLL возможны следующие причины:

  • чтобы избежать использования имен API-функций, чувствительных к регистру знаков;
  • чтобы привести имена в соответствие с существующими стандартами именования;
  • чтобы сделать возможным вызов функций, принимающих данные разных типов (объявляя несколько версий одной и той же функции DLL);
  • чтобы упростить применение API-интерфейсов, которые содержат функции версий для ANSI и Unicode.
  • Переименование функции в C#

    Для задания функции DLL по имени или порядковому номеру можно использовать поле DllImportAttribute.EntryPoint. Если имя функции в определении метода совпадает с именем точки входа в DLL, явно задавать функцию с помощью поля EntryPoint не требуется. В противном случае, чтобы указать имя или порядковый номер, следует использовать одну из следующих форм атрибута:

    [DllImport("dllname", EntryPoint="Functionname")]
     [DllImport("dllname", EntryPoint="#123")]

    При этом порядковому номеру должен предшествовать знак ' # '.

    Ниже приводится пример переименования функции. Имя функции MessageBoxA заменяется на MsgBox с помощью поля EntryPoint. Неуправляемая функция MessageBoxA, которая является точкой входа, в управляемом коде представляется под именем MsgBox:

    using System.Runtime.InteropServices;
    
    public class Win32
     {
         [DllImport("user32.dll", EntryPoint="MessageBoxA")]
         public static extern int MsgBox(int hWnd,
                    String text,
                    String caption,
                    uint type);
     }

    Указание набора знаков

    Поле DllImportAttribute.CharSet играет двойную роль:

  • управляет маршалингом строк;
  • определяет порядок нахождения платформным вызовом имен функций в DLL.
  • Некоторые API экспортируют две версии функций, которые принимают строковые аргументы: узкую (ANSI) и широкую (Unicode). Например, Win32 API для функции MessageBox содержит следующие имена точек входа:

  • MessageBoxA — обеспечивает форматирование с использованием 1-байтовых символов ANSI, на что указывает буква " A ", добавляемая в конец имени точки входа. При вызовах MessageBoxA маршалинг строк всегда выполняется в формате ANSI, который обычно применяется на платформах Windows 98 и Windows 95;
  • MessageBoxW — обеспечивает форматирование с использованием 2-байтовых символов Unicode, на что указывает буква " W ", добавляемая в конец имени точки входа. При вызовах MessageBoxW маршалинг строк всегда выполняется в формате Unicode, который обычно применяется на платформах Windows NT, Windows 2000 и Windows XP.
  • Маршалинг строк и совпадение имен

    Поле CharSet может принимать следующие значения:

  • CharSet.Ansi (стандартное значение).

    При этом:

  • Платформный вызов выполняет маршалинг строк из их управляемого формата Unicode в формат ANSI.
  • Если значение поля DllImportAttribute.ExactSpelling оказывается установленным в true, как это принято по умолчанию в Visual Basic .NET, платформный вызов ищет только указанное имя. Например, если указано MessageBox, платформный вызов ищет именно MessageBox. Если он не может найти точное посимвольное совпадение, возникает исключение при выполнении вызова функции.
  • Когда значение поля ExactSpelling установлено в false, как это принято по умолчанию в C# и управляемых расширениях для C++, платформный вызов сначала ищет точный псевдоним ( MessageBox ), а затем добавленное имя ( MessageBoxA ), если точный псевдоним не найден.

    Следует учитывать, что механизм совпадения имен в формате ANSI отличается от механизма совпадения имен в формате Unicode.

  • CharSet.Unicode.

    При этом платформный вызов обеспечивает маршалинг строк путем копирования строки из их управляемого формата в формат Unicode.

    Если же при этом значение поля ExactSpelling оказывается равным true, действует принцип совпадения имен: платформный вызов ищет только указанное имя. Например, если указано MessageBox, платформный вызов ищет именно MessageBox. Если он не может найти точное посимвольное совпадение, возникает сбой.

    А если значение поля ExactSpelling устанавливается в false, как это принято по умолчанию в C#, платформный вызов сначала ищет добавленное имя ( MessageBoxW ), а затем – точный псевдоним ( MessageBox ), если добавленное имя не найдено.

  • CharSet.Auto.

    При этом платформный вызов выбирает между форматами ANSI и Unicode во время выполнения в соответствии с платформой назначения.

  • Пример. Указание набора символов в C#

    Поле DllImportAttribute.CharSet определяет набор символов как базовый набор ANSI или Unicode. Набор символов определяет режим выполнения маршалинга строковых аргументов. Для указания набора символов применяется один из следующих вариантов атрибута:

    [DllImport("dllname", CharSet=CharSet.Ansi)]
     [DllImport("dllname", CharSet=CharSet.Unicode)]
     [DllImport("dllname", CharSet=CharSet.Auto)]

    В следующем примере показаны три управляемых определения функции MessageBox с атрибутами, задающими наборы символов. В первом определении, в котором значение поля CharSet не задано, по умолчанию принимается набор символов ANSI:

    [DllImport("user32.dll")]
         public static extern int MessageBoxA(int hWnd, String text, 
           String caption, uint type);
    
     [DllImport("user32.dll", CharSet=CharSet.Unicode)]
         public static extern int MessageBoxW(int hWnd, String text, 
           String caption, uint type);
    
     [DllImport("user32.dll", CharSet=CharSet.Auto)]
         public static extern int MessageBox(int hWnd, String text,
           String caption, uint type);

    Примеры платформного вызова. MessageBox, Beep, PlaySound

    Под ПЛАТФОРМОЙ НАЗНАЧЕНИЯ понимается платформа, которой предназначена закодированная в атрибутах информация. Естественно, это .NET.

    В других платформах службы платформного вызова нет. Никакая другая платформа просто не сумеет прочитать и понять значение полей атрибутов.

    Ниже демонстрируется определение и вызов функции MessageBox из библиотеки User32.dll. В качестве аргумента передается простая строка. Значение для атрибута поля DllImportAttribute.CharSet установлено в Auto. Это позволяет платформе назначения самостоятельно определять размер символов и выбирать маршалинг строк:

    using System.Runtime.InteropServices;
    
    public class Win32 {
          [DllImport("user32.dll", CharSet=CharSet.Auto)]
          public static extern int MessageBox(int hWnd, String text, 
                        String caption, uint type);
     }
    
    public class HelloWorld {
         public static void Main() {
          Win32.MessageBox(0, "Hello World", "Platform Invoke Sample", 0);
         }
     }

    Информация о функции Beep в хелпах по C# отсутствует. Такой функции в .NET НЕТ. Однако способы заставить C#-приложение "пропищать" все же существуют (передача функции Console.Writeline параметра с соответствующей escape-последовательностью не в счет).

    Итак, функция

    Beep

    обеспечивает генерацию simple tones (простых звуков) на спикере. Функция выполняется синхронно; она не возвращает управления до тех пор, пока не завершится звучание.

    BOOL Beep(
       DWORD dwFreq,
       DWORD dwDuration
     );

    Параметры

    dwFreq

    Частотная характеристика звука в герцах. Диапазон значений этого параметра ограничен следующими величинами 37–32,767 ( 0x25– 0x7FFF ).

    В Windows Me/98/95 функция Beep этот параметр игнорирует.

    dwDuration

    Продолжительность звучания в миллисекундах.

    В Windows Me/98/95 функция Beep этот параметр игнорирует.

    Возвращаемое значение

    Здесь речь идет о неуправляемой функции, судя по всему, реализованной в C++. Так вот, при успешном выполнении функция возвращает ненулевое значение. В C++ такие значения соответствуют значению ИСТИНА.

    В противном случае возвращается нулевое значение, то есть ЛОЖЬ:

    using System;
     using System.Threading;
     using System.Runtime.InteropServices;
    
    namespace BeepByWin32API
     {
    
     // Import a Beep() API function.
    
    class Class1
     {
     // Таким образом получаем возможность использования импортируемых
     // функций в приложении .NET.
    
     [DllImport("kernel32.dll")]
     private static extern bool Beep(int frec, int dur);
     // Импортируется функция PlaySound().
     [DllImport("winmm.dll")]
     public static extern bool PlaySound(string pszSound,
                                            int hmod,
     			                     int fdwSound);
     // Константы, необходимые для использования PlaySound...
     public const int SND_FILENAME = 0x00020000;
     // За конкретное значение параметра я не ручаюсь. Подобрал сам.
     // Играет - и ладно...
     public const int SND_ASYNC = 0x0010;//0x0001;
     		
    
     // The main entry Point for the application.
    
     static void Main(string[] args)
     {
     int i;
     // Извлекаем звук посредством escape-последовательности.
     Console.WriteLine("Пропищим..." + "\a\a\a\a\a\a\a\a\a\a");
     Console.WriteLine("И еще...");
     for (i = 0; i < 10; i++) {Console.Write("\a"); Thread.Sleep(1000);}
    
    Console.WriteLine("Нажми Any Key для продолжения опытов...");
     Console.ReadLine(); 
     // Извлекаем звук посредством обращения к функции API Beep().
     // Frequency of the sound, in hertz.
     // This parameter must be in the range 37 through 32,767 (0x25 through 0x7FFF). 
     // Duration of the sound, in milliseconds. 
     Beep(800,200);
     Beep(500,1000);
     Beep(100,500);
     Beep(1000,2000);
     Beep(0x25,2000);
     Beep(0x7FFF,2000);
    
    Console.WriteLine("Нажми Any Key для продолжения опытов...");
     Console.ReadLine(); 
    for (i = 0; i < 10; i++)
     {
     PlaySound("Trumpet1.wav", 0, SND_FILENAME|SND_ASYNC);
     }
    
     }
     }
     }
    Вернуться к учебному плану