Функциональная совместимость

Глава из книги “C++/CLI: язык Visual C++ для среды .NET”

Автор: Гордон Хогенсон
Источник: C++/CLI: язык Visual C++ для среды .NET
Материал предоставил: Издательство ''Вильямс''
Опубликовано: 10.09.2007
Версия текста: 1.0
Многогранность взаимодействия
Взаимодействие с другими языками .NET
Использование родных библиотек с помощью вызова базовой платформы
Маршализация данных
Взаимодействие с СOM
Использование родных библиотек без P/Invoke
Перекомпиляция родной библиотеки в управляемый код
Внутренние указатели
Скрепляющие указатели
Родные объекты и управляемые объекты
Использование управляемых объектов в родном классе
Использольвание родного объекта в управляемом типе
Родные и управляемые точки входа
Как избежать двойного переключения системного вызова
Управляемые и родные исключения
Взаимодействие со структурированными исключениями (__try/__except)
Взаимодействие с кодами ошибок Win32
Взаимодействие с исключениями C++
Взаимодействие с СOM HRESULT
Резюме

Функциональная совместимость обычно называется еще взаимодействием с другими программами. Функциональная совместимость — это возможность обращения, т.е. использования или просто вызова кода программы, разработанной в некоторой другой среде программирования или на другом языке, например, вызов СOM или родного кода C++ из программы на управляемом языке. Взаимодействие — сложная, но красивая и чрезвычайно необходимая вещь. Многие люди думают, что язык C++/CLI для базовой платформы .NET прежде всего используется для того, чтобы расширить существующие возможности кода, написанного на родном C++. Конечно, нет разумной причины, по которой нельзя использовать C++/CLI в качестве основного языка .NET, однако предусмотренная в C++/CLI поддержка взаимодействия родного кода с базовой системой (платформой) .NET действительно внушительна. Во многих случаях вы просто включаете опцию компилятора /clr и повторно компилируете ваш родной код, генерируя тем самым управляемый код (или по крайней мере смешанный код, который главным образом скорее всего является MSIL, но с несколькими вставленными родными командами x86 или x64). Эта функция называлась IJW или “it just works” (“это просто работает”), когда она была первоначально реализована наряду с управляемыми расширениями для C++. И главным образом, это так и было. Это теперь она названа смешанным режимом. Огромный объем работы потребовался для того, чтобы сделать возможным создание такого типа взаимодействия. Кроме того, даже если вы пишете полностью новое приложение, которое использует родной API, такой как Win32, поддержка взаимодействия в C++ намного облегчает и ускоряет вызовы этих API в C++, и потому все это делается даже лучше, чем в программах на C#.

Многогранность взаимодействия

Есть несколько видов взаимодействия, о которых вы должны знать. Межъязыковое взаимодействие — это тот вид взаимодействия, с которого мы начнем знакомство, и позже вы увидите, что под ним подразумевается способность C++/CLI работать в тесном сотрудничестве с программами на C#, Visual Basic и других языках, которые компилируются в CLR. Из-за общей базовой платформы, общего IL и общих форматов сборки и метаданных вы в значительной степени можете использовать сборку C# или Visual Basic так, как другую сборку C++/CLI. Вы можете сослаться на нее с помощью #using, вы можете создать экземпляры типов, объявленных в таких сборках, вызывать их методы и т.д. Вы можете также пойти на шаг дальше и создать иерархии наследственности, которые стирают межъязыковые границы, такие как C#-класс, который реализует C++/CLI-интерфейс, или C++/CLI-класс, который является наследником класса, написанного в Visual Basic. Как только эти типы скомпилированы в MSIL, совсем немного указывает на исходный язык, на котором они были созданы.

В дополнение к межъязыковому взаимодействию, вы можете также взаимодействовать с родным кодом, написанном на C++. Способ взаимодействия зависит от того, имеете ли вы в наличии исходный текст или только двоичный файл, открыт ли родной API как функция или класс, открыт ли API через СOM и можете ли вы повторно скомпилировать код.

Давайте сначала рассматривать случай, когда вы не имеете доступа к исходному тексту, а просто имеете библиотечную функцию в родной DLL, которую вы хотите вызвать из управляемой среды. CLR имеет механизм для того, чтобы сделать это; этот механизм обычно называется вызовом базовой системы или вызовом базовой платформы (Platform Invoke, или P/Invoke), предполагая, что вы вызываете определенный для базовой системы (определенный для платформы) двоичный файл. В основном P/Invoke позволяет создать управляемую функцию, которая вызывает вашу родную функцию. Если родной код, который вы хотите вызвать, не открыт как родная, экспортируемая функция, вы не можете использовать P/Invoke; он работает хорошо для того, чтобы вызвать API Win32, он широко используется в языках CLI для этого. Есть некоторые сложности в использовании P/Invoke, так как необходимо объявить управляемые аналоги любых родных структур, которые передаются функции, и это иногда выглядит хитромудрым. Кроме того, есть значительные накладные расходы на переключение из управляемого кода к родному коду и обратно, как вы увидите позже.

В дополнение к P/Invoke, CLR поддерживает взаимодействие с СOM. Вы можете создать экземпляры объектов-модулей доступа к СOM-объектам в управляемом коде. Обычно это включает в себя создание сборки-обертки, которая содержит управляемые типы, которые экспонируют СOM-интерфейсы для вашего управляемого кода. Visual Studio содержит несколько инструментальных средств, которые упрощают этот процесс, к ним относится tlbimp.exe, которое создает сборку-обертку из typelib (файл TLB), который обычно есть в библиотеке COM. Вы можете также пойти другим путем, экспонируя управляемые объекты для СOM. Этот процесс включает в себя приписывание к типам СOM-атрибутов, указывая, например, GUID для данного типа, и использование tlbexp.exe для генерации библиотеки типов, которая может использоваться для инстанцирования управляемых объектов из СOM в качестве СOM-объектов.

Все упомянутые методы взаимодействия доступны во всех языках CLR, но в C++/CLI вы имеете возможность дополнительного типа взаимодействия, если у вас есть исходный текст на C++ и можно повторно скомпилировать его с опцией /clr. Большая часть кода на C++ компилируется с опцией компилятора /clr с минимальными изменениями (заменами), если они вообще потребуются. Если вы сделаете это, вы можете обновить вашу родную DLL как сборку. Типы все еще являются родными, но команды будут скомпилированы в IL. Этот код может использоваться из кода C++/CLI (по крайней мере, в смешанном режиме) так же, как обычно используется родной код на C++: включите заголовочный файл и отредактируйте связи с импортируемой библиотекой DLL. В чистом и безопасном режиме нельзя отредактировать связи с родными объектными файлами и получить чистый или безопасный файл. Если отредактировать объектные файлы различных режимов, полученная сборка будет “понижена” к самому меньшему общему знаменателю; например, если вы отредактируете объектные файлы чистого и смешанного режима, получится сборка смешанного режима.

Вы можете поместить как родные классы и типы, так и управляемые классы и типы в ту же самую сборку в чистом и смешанном режиме. Это полезно, если нужно экспонировать родные классы и типы другим языкам .NET, таким как C# или Visual Basic. Типичный сценарий: повторно скомпилировать исходный текст родной библиотеки классов с опцией /clr и в ту же самую сборку добавить управляемые классы, которые заключают в обертку родные классы, которые необходимо экспортировать в другие управляемые языки. Эти управляемые обертки в таком случае отмечаются как общедоступные, и программы на других языках будут их видеть. А ведь родные классы в DLL не были бы доступны для клиентов, которые используют сборку.

Чтобы поддерживать все это, предусмотрены различные языковые средства и средства CLR. Межъязыковое взаимодействие, P/Invoke и взаимодействие с СOM — средства CLR. Мы вкратце обсудим межъязыковое взаимодействие, P/Invoke и взаимодействие с СOM. Использование родных типов и управляемых типов в той же самой сборке, например для создания управляемой обертки родной библиотеки классов, как раз и является основным предметом рассмотрения в этой главе. Вы узнаете, как сослаться на родной тип в управляемом типе и как сослаться на управляемый тип в родном типе. Вы позже увидите типы указателей, которые помогают в работе со сценариями функциональной совместимости, такие как внутренние указатели и скрепляющие указатели. Вы также изучите преобразование типов между родными и эквивалентными им управляемыми типами. Этот тип преобразования обычно называется маршалингом, или маршализацией.

Взаимодействие — интригующая, сложная тема. Полное обсуждение всех тонких аспектов взаимодействие было бы невозможным во вводном тексте; таким образом, эта глава сосредоточится на некоторых основных элементарных сценариях, чтобы дать вам идею, что представляется вполне возможным. По теме взаимодействия с C++ можно написать целую книгу. Дополнительную информацию можно найти в книге Маркуса Хиджа Expert Visual C++/CLI (издательство Apress, готовится к выпуску), предназначенную для опытных пользователей Visual C++/CLI.

Взаимодействие с другими языками .NET

В C++/CLI можно непосредственно использовать типы, созданные в программе на другом языке .NET. Фактически вы делаете это все время, так как большая часть .NET Framework написана на C#. Взаимодействуя с C# или VB или любым множеством языков, написанным не фирмой Microsoft, вы должны знать, какие средства C++/CLI доступны на других языках, а какие таковыми не являются. Например, C# не поддерживает глобальные функции. Если вы определите глобальную функцию и сделаете ее общедоступной, вы не сможете вызвать ее из C#. Вы могли бы вызвать такую функцию через общедоступный статический метод общедоступного класса. Если вы хотите программировать на управляемом языке, который позволяет вам делать все, то, чтобы узнать все подробности, вам придется прочитать книгу Серджа Лидина (Serge Lidin, Expert .NET 2.0 IL Assembler; издательство Apress, 2006), предназначенную для опытных пользователей, посвященную ассемблеру IL платформы .NET 2.0. Справедливо сказать, что IL — язык ниже C++/CLI в CLR так же, как ассемблер — один из языков, лежащих ниже, чем C++ для многих базовых систем (платформ).

Для межъязыкового взаимодействия имеет смысл использовать чистый или безопасный режим, так как из VB или C# очень просто сослаться на сборки MSIL. Если бы пришлось компилировать в смешанном режиме, пришлось бы создать управляемую обертку, чтобы гарантировать, что к коду можно обратиться с других языков, как показано в листингах 12.1 и 12.2.

Листинг 12.1. Заключение глобальной функции в обертку
// global_function.cpp
// Скомпилировать с помощью командной строки 
// cl /clr:safe /LD global_function.cpp.

// используем пространство имен Система;

namespace G                   // пространство имен
{

    void FGlobal()            // пусто FGlobal ()
    {
        Console::WriteLine("Global C++/CLI Function.");
        // Пульт::WriteLine ("Глобальная функция C++/CLI.");
    }

    public ref class R        // общедоступный ссылочный класс R
    {
    public:                   // общедоступный:
        static void FMember() // статический пустой FMember()
        {
            Console::WriteLine("C++/CLI Static Member Function.");
            // Пульт::WriteLine ("C++/CLI Статическая функция-член.");
            FGlobal();
        }
    };
};
Листинг 12.2. Использование заключенной в обертку глобальной функции в C#
// consume_cpp.cs
// Скомпилировать с помощью командной строки
// csc /r:global_function.dll consume_cpp.cs.

using G; 

class C                       // класс C
{
    public static void Main()
    // общедоступная статическая пусто Главная программа()
    {
        // FGlobal(); //Ошибка: глобальные функции не доступны в C#.
        R.FMember(); // OK
    }
};

Вот выдача программы, приведенной в листинге 12.2:

C++/CLI Static Member Function.
Global C++/CLI Function.

В листинге 12.3 показан интерфейс C++/CLI, который реализован в классе, написанном на Visual Basic, — в листинге 12.4.

Листинг 12.3. Создание интерфейса в C++
// interface_example.cpp
// Скомпилировать с помощью командной строки
// cl /clr:pure /LD interface_example.cpp.

public interface class ITest
// общедоступный класс интерфейса ITest
{
    void F();                 // пусто F ();
    void G();                 // пусто
};
Листинг 12.4. Использование интерфейса в Visual Basic
' implement_example.vb
' Скомпилировать с помощью командной строки 
' vbc /r:interface_example.dll implement_example.vb.

Public Class VBClass ' Общедоступный Класс VBClass
    Implements Itest ' Реализация ITest

    Public Sub F() Implements ITest.F
        ' Общедоступный Sub F Реализует ITest. F
        Console.WriteLine("F in VB")
        ' Пульт. WriteLine("F в VB")
    End Sub ' Конец Sub

    Public Sub G() Implements ITest.G
        ' Общедоступный Sub G Реализует ITest.G
        Console.WriteLine("G in VB")
        ' Пульт.WriteLine("G в VB")
    End Sub ' Конец Sub

    Public Shared Sub Main()
        ' Общедоступная Совместно используемая Главная Sub
        Dim Test As ITest = New VBClass ' Новый VBClass
        With Test
            .F()
            .G()
        End With ' Конец
    End Sub 'Main
End Class ' Конец Класса VBClass

Вот выдача программы, приведенной в листинге 12.4:

F in VB
G in VB

Чтобы упростить проблемы с межъязыковым взаимодействием, была создана Common Language Specification (CLS, Спецификация общего языка), в которой определены общие конструкции языков .NET, пригодные для преодоления языковых границ. Если вы делаете все возможное, чтобы использовать только те средства, которые являются CLS-совместимыми в публично видимых частях общедоступных типов, можно быть увереным, что ваш код доступен для C# и Visual Basic и любого другого языка CLR, который распознает CLS-совместимые типы. Вы можете безопасно использовать несовместимые средства в методах общедоступного типа или в приватных типах, но общедоступные сигнатуры общедоступных типов должны быть CLS-совместимыми для типа, который будет рассматриваться как CLS-совместимый. Есть много средств C++/CLI, которые не являются CLS-совместимыми. В табл. 12.1 перечислены средства C++/CLI, которые не являются CLS-совместимыми, и предлагаются альтернативные им варианты.

СредствоВозможный CLS-совместимый вариант
Упакованные типы значенийИспользуйте System::Object (Система::Объект), System::ValueType (Система::ValueType) или System::Enum (Система::Перечисление)
Глобальные функцииИспользуйте статические методы вместо них
Родной кодСоздайте CLS-совместимые обертки
ШаблоныИспользуйте родовые объекты или создавайте родовые интерфейсы
Типы-указателиИспользуйте IntPtr
Исключения, которые не являются наследниками System::Exception (Система::Исключение)Используйте только исключения, которые являются наследниками System::Exception (Система::Исключение)
Интерфейсы со статическими членамиИспользуйте только нестатические методы, свойства и события
Свойства со средствами доступа (аксессорами), которые имеют различные модификаторы, например, одно виртуальное средство доступа и одно невиртуальноеИспользуйте только свойства с непротиворечивыми модификаторами для их средств доступа
Переопределение виртуальных методов, которые изменяют доступностьИспользуйте только типы, которые не делают этого
Перегрузка операторовПредоставьте методы с подобными функциональными возможностями, например, int Add(int a) (int Добавить(int a)) для оператора operator+(int)
Традиционные vararg, например, printf("%d%s", ...)Используйте новые синтаксические конструкции для массива параметров: например, f(String^ s, ... array<R^>^ params) (f(Строка^ s, ... массив <R^>^ параметры)).
Таблица 12.1. Основные средства C++/CLI, которые не являются CLS-совместимыми, и некоторые возможные их альтернативные варианты

Использование родных библиотек с помощью вызова базовой платформы

Вспомните, что в главе 3 “Разработка C++/CLI-программ на базовой платформе .NET Developer Platform с помощью Visual C++” было сказано, что есть несколько режимов компиляции, поддерживаемых в C++/CLI: смешанный режим (опция /clr), чистый режим (/clr:pure) и безопасный режим (/clr:safe). (Есть также опция /clr:oldSyntax, которая допускает синтаксические конструкции для управляемых расширений для C++ (Managed Extensions for C++), которые использовались в Visual Studio .NET 2002 и 2003.) В предыдущих главах большую часть кода можно скомпилировать в смешанном, чистом или безопасном режиме, кроме нескольких случаев, где явно указано иначе. Имея дело с взаимодействием, выбор режима компиляции более существен, потому что родной код потенциально опасен. Когда нужно вызвать функцию в родной DLL в безопасном режиме, используется P/Invoke. Даже при том, что родной код нельзя проверить (подтвердить) на предмет безопасности, это самый безопасный способ вызвать родной код из управляемого кода. P/Invoke используется широко в C#, но есть и другие альтернативные средства в C++/CLI, которые могут часто использоваться вместо P/Invoke в чистом и смешанном режимах. Эти другие методы будут описаны позже в данной главе. Если вы используете безопасный режим, то P/Invoke — единственная возможность для вызова родных функций.

Основная идея P/Invoke состоит в том, чтобы создать новое объявление функции и использовать атрибуты для того, чтобы связать его с существующей родной функцией, указывая DLL, которая экспортирует данную функцию. Это — самая простая часть. Сложность возникает с типами, которые будут использоваться как параметры функции. Эти типы должны быть созданы в коде C++/CLI и должны быть точно теми же самыми, что и родные типы, которые функция принимает в качестве параметров.

Предположим, нужно вызвать функцию MessageBox. Документация Windows SDK говорит нам, что MessageBox хранится в user32.dll, а ее заголовочный файл — в WinUser.h. Найдя это объявление, мы обнаруживаем то, что показано в листинге 12.5.

Листинг 12.5. Объявление MessageBox
int MessageBox( HWND hWnd,    // дескриптор окна владельца
               LPCTSTR lpText, // текст в окне сообщений
               LPCTSTR lpCaption, // заголовок окна сообщений
               UINT uType     // тип окна сообщений );

Этот вызов функции может быть открыт для использования в управляемом коде с помощью атрибута DllImport. Листинг 12.6 показывает, как функция Win32 MessageBox объявлена и используется в коде на C++/CLI.

Листинг 12.6. Вызов функции Win32 из кода C++/CLI
// pinvoke.cpp
using namespace System;
// используем пространство имен Система;
using namespace System::Runtime::InteropServices;
// используем пространство имен Система::Время выполнения::InteropServices;

// Заметьте, что используются 
// управляемые эквиваленты родных типов.
[DllImport("user32.dll", CharSet=CharSet::Auto)]
int MessageBox(IntPtr, String^ text, String^ caption,
// int MessageBox (IntPtr, Строка^ текст, Строка^ заголовок,
               unsigned int type);
            // int без знака тип);

int main()                    // int главная программа()
{
    MessageBox(IntPtr::Zero, "Hello, World!", "Win32 Message Box", 0);
    // MessageBox(IntPtr::Нуль, "Привет, мир!", "Окно сообщений Win32", 0);
}

Вы можете легко проверить, что этот код отлично работает в смешанном режиме (с опцией /clr), в чистом режиме (с опцией /clr:pure) и в безопасном режиме (с опцией /clr:safe).

Атрибут DllImport принимает имя DLL как параметр, так же как и параметр, который определяет, как должны быть обработаны строковые параметры. Как вы знаете, в родном коде строки могут быть ANSI или MBCS (тип char (символ)), или Unicode (тип wchar_t). Тип управляемой строки — всегда Unicode, но многие API принимают в качестве параметров ANSI-строки. Параметр CharSet позволяет указать системе преобразование управляемой строки в нужный родной тип строки. Кроме того, он фактически управляет, какая версия функции Win32 вызывается — Unicode или ANSI. Параметр CharSet имеет три возможных значения: CharSet::Ansi, CharSet::Auto и CharSet::Unicode. CharSet::Auto (Авто) позволяет системе выбирать правильный тип маршализации самостоятельно. Возможно, вы знаете, что фактически функция MessageBox не существует. Из WinUser.h видно, что MessageBox — макрокоманда, которая превращается в одно из действительных имен функции — MessageBoxA для ANSI-версии и MessageBoxW для версии Unicode. Если вы укзываете CharSet::Unicode, фактически вызываемой функцией будет MessageBoxW. Если указать CharSet::Ansi, то это будет MessageBoxA. Этот механизм независим из того, действительно ли определен UNICODE. Это — один из способов, которыми P/Invoke подстраивается для использования с API Win32, хотя он может использоваться и для любой родной DLL. Если вы используете P/Invoke с вашей собственной DLL и вы хотите отключить автоматическое отображение CharSet на ANSI-версию или Unicode-версию имени функции, вы можете установить булево свойство ExactSpelling равным true (истина), вот так:

[DllImport ("mydll.dll", CharSet = CharSet::Ansi, ExactSpelling = true)]

В листинге 12.6 есть еще один момент, который может заинтересовать вас: использование IntPtr для параметра HWND и использование IntPtr::Zero (Нуль) как параметра. IntPtr — полезная структура для программирования взаимодействия, так как она может использоваться в качестве типа указателя в родном коде, но не является указателем в управляемом коде. Однако, в отличие от родных указателей, она CLS-совместима, так как пригодна для использования в других языках. Размер IntPtr зависит от размера указателя для базовой платформы, таким образом, он может представлять 32-разрядный указатель или 64-разрядный указатель. Он может быть легко преобразован в 32-разрядное или 64-разрядное целое число или в указатель без контроля типов (void *). Тип IntPtr может хранить значения родных дескрипторов операционной системы (например, HWND) и указателей, полученных из других вызовов P/Invoke.

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

Листинг 12.7. Использование свойства EntryPoint для DllImport
// pinvoke_rename_entry_point.cpp

#using "System.Windows.Forms.dll"

using namespace System;
// используем пространство имен Система;
using namespace System::Runtime::InteropServices;
// используем пространство имен Система:: Время выполнения:: InteropServices;
using namespace System::Windows::Forms;
// используем пространство имен Система:: Windows::Формы;

[DllImport("user32.dll", CharSet=CharSet::Auto, EntryPoint="MessageBox")]
int NativeMessageBox(IntPtr, String^ text, String^ caption,
// int NativeMessageBox(IntPtr, Строка^ текст, Строка^ заголовок,
                     unsigned int type);
                  // int без знака тип);

int main()                    // int главная программа()
{
    NativeMessageBox(IntPtr::Zero, "Hello, World!", "Win32 Message Box", 0);
    // NativeMessageBox(IntPtr::Нуль, "Привет, мир!", 
    //                         "Окно сообщений Win32", 0);
    MessageBox::Show("Hello, Universe!", "Managed Message Box");
    // MessageBox::Отобразить("Привет, Вселенная!", 
    //                            "Управляемое Окно сообщений",);
}

Вообще, используя P/Invoke, вы должны убедиться, что знаете соглашение о вызовах целевой функции. Вызывая функции Win32, не стоит волноваться об используемом соглашении о вызовах, потому что все функции Win32 используют соглашение о вызовах __stdcall (WINAPI в заголовках Windows в конце концов приводит к этому заключению), и именно оно принято по умолчанию для DllImport. Однако, если вы используете вашу собственную родную DLL, скомпилированную с помощью Visual C++, в котором по умолчанию принято соглашение о вызовах __cdecl, возможно, придется установить свойство CallingConvention для атрибута DllImport. Например, необходимо установить CallingConvention в CallingConvention::Cdecl, если вы вызываете какую-нибудь функцию CRT с помощью P/Invoke. Например, функции Бесселя не доступны в .NET API Framework; таким образом, вы можете экспонировать их из CRT с помощью следующего объявления:

[DllImport("msvcr80.dll", CallingConvention=CallingConvention.Cdecl)]
extern double _jn(int n, double x); // функция Бесселя первого рода
// двойная точность _jn (int n, двойная точность x); 

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

Свойство CallingConvention может использоваться для вызова метода класса, который экспортируется из DLL. Давайте рассмотрим эту возможность в листингах 12.8 и 12.9.

Листинг 12.8. Компиляция родного класса в DLL
// nativeclasslib.cpp
// Скомпилировать с помощью командной строки 
// cl /LD nativeclasslib.cpp.

#include <stdio.h>

class __declspec(dllexport) NativeClass // класс 
{
private:                      //приватный:
    int m_member;
public:                       // общедоступный:
    NativeClass() : m_member(1) { }

    int F( int i )
    {
        // __ FUNCSIG __ - определенная в компиляторе макрокоманда,
        // которая определяет текущую сигнатуру функции.
        printf("%s\n", __FUNCSIG__);
        return m_member + i;  // возврат
    }

    static NativeClass* CreateObject() // статический
    {
        printf("%s\n", __FUNCSIG__);
        return new NativeClass(); // возвратить новый NativeClass();
    }

    static void DeleteObject(NativeClass* p)
    // статический пустой DeleteObject(NativeClass* p)
    {
        printf("%s\n", __FUNCSIG__);
        delete p;             // удаление p;
    }
};

// Если вы не хотите использовать запутывающие имена, 
// можете использовать следующий метод экспортирования:

extern "C" __declspec(dllexport) NativeClass* CreateObject()
{
    return NativeClass::CreateObject();
    // возвратить NativeClass:: CreateObject ();
}

extern "C" __declspec(dllexport) void DeleteObject(NativeClass* p)
{
    NativeClass::DeleteObject(p);
}

/* Чтобы скорректировать имена, выполняется следующая команда.
link /DUMP /EXPORTS nativeclasslib.dll
которая выводит:

ordinal hint RVA name

1 0 00001000 ??0NativeClass@@QAE@XZ
2 1 000010D0 ??4NativeClass@@QAEAAV0@ABV0@@Z
3 2 00001050 ?CreateObject@NativeClass@@SAPAV1@XZ
4 3 000010A0 ?DeleteObject@NativeClass@@SAXPAV1@@Z
5 4 00001020 ?F@NativeClass@@QAEHH@Z
6 5 000010F0 CreateObject
7 6 00001100 DeleteObject
*/
Листинг 12.9. Использование свойства CallingConvention
// pinvoke_thiscall.cpp
// Скомпилировать с помощью командной строки 
// cl /clr:safe pinvoke_thiscall.cpp.

using namespace System;
// используем пространство имен Система;
using namespace System::Text;
// используем пространство имен Система:: Текст;
using namespace System::Runtime::InteropServices;
// используем пространство имен Система:: Время выполнения:: InteropServices;

namespace NativeLib           // пространство имен NativeLib
{
    [ DllImport( "nativeclasslib.dll",
        EntryPoint="?F@NativeClass@@QAEHH@Z",
        CallingConvention=CallingConvention::ThisCall )]
    extern int F( IntPtr ths, int i );

    // static NativeClass* NativeClass::CreateObject();
    // статический
    [DllImport( "nativeclasslib.dll", EntryPoint=
        "?CreateObject@NativeClass@@SAPAV1@XZ" )]
    extern IntPtr CreateObject();

    // static void NativeClass::DeleteClass( NativeClass* p )
    // статический пустой
    [ DllImport( "nativeclasslib.dll", EntryPoint=
        "?DeleteObject@NativeClass@@SAXPAV1@@Z" )]
    extern void DeleteObject( IntPtr p );
}


int main()                    // int главная программа()
{
    IntPtr ptr = NativeLib::CreateObject();
    int result = NativeLib::F( ptr, 50 );
    Console::WriteLine( "Return value: {0} ", result );
    // Пульт::WriteLine("Возвращаемое значение: {0}", результат);
    NativeLib::DeleteObject( ptr );
}

Вывод программы в листинге 12.9 приведен ниже:

class NativeClass *__cdecl NativeClass::CreateObject(void)
int __thiscall NativeClass::F(int)
Return value: 51
void __cdecl NativeClass::DeleteObject(class NativeClass *)

Как видно, чтобы использовать P/Invoke с функциями класса, статическими или нестатическими, приходится прибегать к запутывающим именам, которые мы получаем, выполняя dumpbin.exe или link.exe /DUMP /EXPORTS так, как объяснялось в комментариях к коду. Статические функции не требуют специального соглашения о вызовах, так как они используют соглашение о вызовах __cdecl. Функция-член F требовала соглашение о вызовах __thiscall, потому что неявный параметр для любой функции-члена является указателем на объект.

Объявление функции P/Invoke создает управляемое имя для родной функции и небольшой фрагмент кода, который в свою очередь вызывает родную функцию. Этот фрагмент кода называется управляемой точкой входа родной функции, и он включает в себя то, что называется переключателем контекста между управляемым и родным кодом. Он также называется переключателем с управляемого кода на родной и наоборот. Контекстные переключатели прибавляют накладные расходы к вызову функции. Во время переключения контекстов происходит маршализация параметров между родными и управляемыми типами. Снова происходит ухудшение характеристик программы, когда контекст переключается обратно к управляемому коду. Можно сказать, что выполнение задерживается на какое-то время, когда пересекается граница между управляемым и родным кодом.

Маршализация данных

Большая часть того, что происходит при переключении контекстов, относится к маршализации параметров между родными типами и управляемыми типами. Маршализация примитивных типов является непосредственной и фактически не требует никакой работы во время выполнения. Маршализация символов, строк и структурных типов не будет столь простой. В табл. 12.2 показаны отображения, используемые по умолчанию. Так, если тип, используемый в родной вызываемой функции, находится в одном из первых двух столбцов, тип сигнатуры P/Invoke должен быть одним из типов в последних двух столбцах.

Тип WindowsРодной кодC++/CLICLR
HANDLE, DWORD_PTRvoid * (пусто *)void * (пусто *)IntPtr, UIntPtr
BYTE (БАЙТ)unsigned char (символ без знака)unsigned char (символ без знака)Byte (Байт)
SHORT (КОРОТКОЕ)short (короткое)short (короткое)Int16
WORDunsigned short (короткое без знака)unsigned short (короткое без знака)UInt16
INTintintInt32
UINTunsigned int (int без знака)unsigned int (int без знака)UInt32
LONG (ДЛИННОЕ)long (длинное)long (длинное)Int32
BOOLlong (длинное)boolBoolean (Булева переменная)
DWORDunsigned long (без знака длинное)unsigned long (без знака длинное)UInt32
ULONGunsigned long (без знака длинное)unsigned long (без знака длинное)UInt32
CHARchar (символ)char (символ)Char (Символ)
LPCSTRchar * (символ *)String ^ [in], StringBuilder ^ [in, out]String ^ [in], StringBuilder ^ [in, out]
LPCSTRconst char * (константа символ *)String ^ (Строка ^)String (Строка)
LPWSTRwchar_t *String ^ [in], StringBuilder ^ [in, out]String ^ [in], StringBuilder ^ [in, out]
LPCWSTRconst wchar_t * (константа wchar_t *)String ^ (Строка ^)String (Строка)
FLOAT (С ПЛАВАЮЩЕЙ ТОЧКОЙ)float (с плавающей точкой)float (с плавающей точкой)Single (Одинарная точность)
DOUBLE (ДВОЙНАЯ ТОЧНОСТЬ)double (двойная точность)double (двойная точность)Double (Двойная точность)
Таблица 12.2. Отображения, используемые по умолчанию при маршализации типов между родным и управляемым кодом

Чтобы изменить значение маршализации по умолчанию, следует применить атрибут MarshalAs. Маршализация для более сложных типов не столь проста. Класс Marshal в пространстве имен System::Runtime::InteropServices (Система::Время выполнения::InteropServices) имеет много полезных методов для функциональной совместимости. В этой книге рассматриваются только несколько из самых полезных. Будущие версии Visual C++ могут обрести библиотеку шаблонов маршализации, которая должна сделать маршализацию намного удобнее. Полное обсуждение выходит за пределы этой книги.

Если вы вместо этого используете другие методы взаимодействия, описанные позже, вы можете включить нужные заголовочные файлы, которые определяют все типы, используемые в списке параметров, и не только избежать их нового воссоздания в управляемом коде, но и во многих случаях избежать контекстного переключения к родному коду и наоборот. Однако, если действительно приходится использовать P/Invoke, следует воспользоваться Интернет-ресурсами для программирования P/Invoke, такими как www.pinvoke.net, который включает готовый код для многих вызовов Win32.

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

Взаимодействие с СOM можно осуществить двумя способами (тремя, если считать перекомпиляцию СOM-объекта с опцией /clr). Вы можете обратиться к СOM-объекту из управляемого кода или можете экспонировать ваш управляемый объект как СOM-объект.

Чтобы использовать СOM-объект из управляемого кода, следует создать обертывающую сборку, которая экспонирует СOM-объект через набор управляемых классов обертки и интерфейсов. Сборку обертки можно создать автоматически из библиотеки типов COM DLL или из исполнимого файла с помощью tlbimp.exe. Программа tlbimp.exe создает набор классов обертки, по умолчанию маршализируя управляемые и родные типы. Если нужно более тонкое маршализирование, вы можете также создать такие обертки вручную.

На сборку обертки можно сослаться с помощью #using и тогда можно вызвать СOM-объекты, если они должным образом зарегистрированы. Если использовать #import (обычный способ импортировать СOM-типы из DLL или библиотеки типов) с помощью управляемого кода, то код придется сгенерировать, причем он не будет компилироваться с опциями /clr:pure или /clr:safe.

Взаимодействие с СOM — средство CLR, а не специальное средство языка C++/CLI, потому оно не описывается здесь подробно. Есть превосходные книги, посвященные взаимодействию с СOM, такие как книга Эндрю Троэлсена (Andrew Troelsen) COM and .NET Interoperability (“Функциональная совместимость СOM и .NET”), изданная в издательстве Apress в 2002 году, и Адама Натана (Adam Nathan) .NET and COM: The Complete Interoperability Guide (“.NET и СOM: Полное Руководство по функциональной совместимости”), изданная в издательстве Sams в 2002 году.

Использование родных библиотек без P/Invoke

Родные библиотеки могут использоваться в коде C++/CLI без P/Invoke. Поскольку это родные объектные файлы, у них можно отредактировать связи. Если доступен исходный файл, его можно повторно скомпилировать как управляемый код, часто вообще не изменяя его. Если есть только двоичный файл и заголовочный файл, можно включить заголовок и отредактировать связи с родным объектным файлом, статической библиотекой или импортировать библиотеку для DLL. Компоновщик Visual Studio 2005 может также скомпоновать родные и управляемые файлы в единую сборку.

В безопасном режиме данные методики использовать нельзя; в безопасном режиме P/Invoke — единственный выход. Родные библиотеки можно использовать в чистом режиме и смешанном режиме.

Библиотека времени выполнения C (C Runtime Library, CRT) и Standard C++ Library (Стандартная библиотека C++) доступны как чистый MSIL. Имена DLL несколько отличаются: msvcm80.dll в противоположность msvcr80.dll. m указывает на управляемый код. Если скомпилировать код, который использует CRT, с опцией /clr либо с опцией /clr:pure, то получится соответствующая чистая скомпилированная MSIL CRT вместо родной CRT. Используя взаимодействие, следует заботиться о том, вызываете ли вы родную функцию или вызываете родной код, который был повторно скомпилирован как MSIL (например, функции в чистом режиме CRT), потому что намного проще избежать переключения контекста из управляемого кода (MSIL) к родному коду, если это возможно. Вообще из управляемого кода быстрее вызвать другой управляемый код, а из родного кода быстрее вызвать другой родной код. Я, кажется, уже упоминал, насколько медленно происходит переключаение контекста? Из-за медлительности переключения контекста обычно лучше повторно скомпилировать родной код как управляемый код, если вы хотите часто использовать переключение из управляемого кода, как мы делаем, поставляя управляемую CRT.

Рассмотрим некоторый простой код, который использует API Win32 так, как показано в листинге 12.10.

Листинг 12.10. Использование API Win32
// message_box.cpp

#include <windows.h>

int main()                    // int главная программа()
{
    MessageBox( 0, "Hello, World!", "Win32 Message Box", 0);
    // MessageBox(0, "Привет, мир!", "Окно сообщений Win32", 0);
}

Код функции MessageBox находится в user32.dll и экспортирует функцию оттуда. Чтобы сгенерировать родной исполнимый файл, мы редактируем связи с библиотекой импорта user32.lib.

cl message_box.cpp user32.lib

Однако мы могли также сделать следующее:

cl /clr message_box.cpp user32.lib
cl /clr:pure message_box.cpp user32.lib

Единственное изменение состоит в том, чтобы скомпилировать с опцией /clr или /clr:pure. Это большое различие, потому что оно означает, что объектный файл содержит управляемый код, а не родной код. Компоновщик в состоянии скомпоновать управляемый код с импортируемой родной библиотекой без каких-либо проблем. Но для программиста это означает, что можно вызвать функцию в родной DLL из управляемого кода, притом просто включением заголовочного файла и обычным вызовом функции. Это работает точно так же и в смешанном, и в чистом режимах. В Visual Studio IDE вы должны были бы сделать несколько изменений в свойствах проекта, чтобы повторно скомпилировать код, который использует Win32, с опцией CLR. Вы уже знаете (потому что мы обсуждали это в главе 3 “Разработка C++/CLI-программ на базовой платформе .NET Developer Platform с помощью Visual C++”) о свойстве Common Language Runtime (Общий язык времени выполнения). То, что могло быть неочевидно, — это то, что, чтобы обратиться к библиотеке, подобной user32.lib, возможно, пришлось бы изменить свойство Linker (Компоновщик) для Additional Dependencies (Дополнительные зависимости). В CLR-проекте это свойство устанавливается равным $(NOINHERIT). Вы должны будете удалить эту опцию, чтобы CLR-проекты могли отредактировать связи с динамически подключаемыми библиотеками Win32.

Недостаток этого метода состоит в том, что происходит переключение контекста от родного к управляемому коду и наоборот. Хотя можно просто вызвать метод MessageBox из управляемого кода, переключение контекстов имеет место в каждой точке перехода, и это — каждый раз, когда родная функция вызывается из управляемого кода. Если вы можете терпеть такой штраф рабочих характеристик, этот метод взаимодействия полезен. Этот метод также рекомендуется, если исходный текст ваших родных функций недоступен. Если вы действительно имеете исходный текст ваших родных DLL, повторно скомпилируйте его, поскольку управляемый код может быть лучше и поможет избежать дорогих переключений контекста между управляемым и родным кодом.

Кстати, если вы попробуете /clr:safe, я желаю вам удачно пробраться через тысячи строк сообщений об ошибках, выдаваемых компилятором, поскольку компилятор C++/CLI пробует интерпретировать заголовки Windows в безопасном режиме. Вот всего лишь небольшая выборка из вывода:

C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\include\imm.h(151) : error C4959: cannot define unmanaged struct 'tagIMECHARPOSITION' in /clr:safe because accessing its members yields unverifiable code
C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\include\imm.h(157) : error C4956: 'tagIMECHARPOSITION *' : this type is not verifiable
C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\include\imm.h(157) : error C4956: 'tagIMECHARPOSITION *' : this type is not verifiable
C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\include\imm.h(157) : error C4956: 'tagIMECHARPOSITION *' : this type is not verifiable
C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\include\imm.h(159) : error C4956: 'BOOL (__stdcall *)(HIMC,LPARAM)' : this type is not verifiable
C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\include\imm.h(315) : error C4956: 'int (__stdcall *)(LPCSTR,DWORD,LPCSTR,LPVOID)' : this type is not verifiable
C:\Program Files\Microsoft Visual Studio 8\VC\PlatformSDK\include\imm.h(316) : error C4956: 'int (__stdcall *)(LPCWSTR,DWORD,LPCWSTR,LPVOID)' : this type is not verifiable

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

Но мы все же еще не сделали никакого действительного взаимодействия, мы сделали только первый шаг. В любом сценарии взаимодействия нужно иметь управляемые типы, так что давайте дополним простой вызов MessageBox некоторым управляемым кодом. Листинг 12.11 показывает случай, где вы пишете управляемый класс, реализация которого вызывает некоторые функции Win32.

Листинг 12.11. Использование функции Win32 в управляемом классе
// interop_messagebox.cpp

#include <windows.h>
#include <vcclr.h>            // для PtrToStringChars

using namespace System;
// используем пространство имен Система;

public ref class MessageBoxClass
// общедоступный ссылочный класс MessageBoxClass
{
public:                       // общедоступный:

    property String^ Message;
    // Свойство Строка^ Сообщение;
    property String^ Caption;
    // Свойство Строка^ Заголовок;

    int DisplayBox()
    {
        // Использовать скрепляющие указатели, чтобы 
        // заблокировать данные, на время их 
        // использования в родном коде.
        pin_ptr<const wchar_t> message = PtrToStringChars(Message);
        // pin_ptr<константа wchar_t> сообщение = 
        //                             PtrToStringChars(Сообщение);
        pin_ptr<const wchar_t> caption = PtrToStringChars(Caption);
        // pin_ptr<константа wchar_t> заголовок = 
        //                             PtrToStringChars(Заголовок);
        return MessageBoxW( 0, message, caption, MB_OK);
        // возвратить MessageBoxW(0, сообщение, заголовок, MB_OK);
    }
};

int main()                    // int главная программа()
{
    MessageBoxClass m;
    m.Message = "Managed string used in native function";
    // m.Сообщение = "управляемая строка, используемая в родной функции";
    m.Caption = "Managed Code using Win32 Message Box";
    // m.Заголовок = "Управляемый код, использующий Окно сообщений Win32";
    m.DisplayBox();
}

В листинге 12.11 мы используем Уникодную (Unicode) форму функции MessageBox, MessageBoxW, так как мы начинаем со строки Уникода (Unicode). Мы начинаем видеть некоторые более сложные операции, поскольку мы маршализируем управляемый тип String (Строка) в родной параметр LPCWSTR; LPCWSTR — typedef для const wchar_t*. Функция PtrToStringChars — удобное средство в vcclr.h, которое дает указатель на основной массив символов. Данные имеют тип Char (Символ), который является тем же самым, что и wchar_t. Поскольку массив находится внутри объекта в управляемой куче, необходимо использовать скрепляющие указатели (pin_ptr), чтобы быть уверенным, что данные не перемещаются сборщиком мусора в течение выполняемых операций. Мы обсудим скрепляющие указатели более подробно позже в этой главе, но пока удовлетворимся тем, что скажем, что скрепляющие указатели работают следующим способом: независимо от того, на что они указывают, они отмечают объект как фиксированный в памяти, пока существует его скрепляющий указатель. Если скрепляющий указатель указывает на какой-нибудь внутренний элемент объекта, будет закреплен весь объект. Как только скрепляющий указатель выходит из области видимости, объект освобождается и может двигаться снова. Скрепляющий указатель имеет определенное преобразование в его основной тип указателя; таким образом, он не требует приведения, когда передается MessageBoxW. Соблюдайте осторожность и используйте скрепляющие указатели для любых указателей, которые вы передаете родному коду. Вы можете также применить скрепляющие указатели, когда нужно использовать арифметические операции над указателями на данные, которые находятся в управляемой куче. Пример вы видели в главе 5 “Базовые типы данных: строки, массивы и перечисления”, когда мы рассматривали выполнение итеративного обхода по элементам управляемого массива, используя арифметические операции над указателями.

Вызов MessageBoxW — переход к родному коду так же, как и в случае использования P/Invoke для вызова MessageBox. Если родные функции вызываются нечасто и основное действие находится в управляемом коде, эта форма взаимодействия имеет смысл. Если вы имеете только двоичный файл, у вас не остается никаких других вариантов. В следующем разделе вы увидите, как, имея исходный текст родной библиотеки и умея повторно скомпилировать его, можно избежать переключения контекстов между родным и управляемым кодом.

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

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

Теперь давайте рассмотрим другой вариант: повторная компиляция родного кода в управляемый код. Это может быть весьма привлекательной опцией, если вы имеете исходный текст родной библиотеки и планируете часто вызывать ваш родной код из управляемого кода. Именно эту возможность следует выбрать для часто используемой библиотеки, которая должна использоваться прежде всего управляемым кодом, возможно даже написанным на другом языке, таком как C# или VB .NET. Это весьма привлекательная возможность, потому что она позволяет избежать переключений контекста между управляемым и родным кодом. Компиляция родного кода в управляемый код не означает, что он будет выполняться медленнее. Помните, что весь управляемый код компилируется по требованию во время выполнения в родной код прежде, чем он будет выполнен CLR. Это и есть то, что делает компилятор JIT (Just In Time — как раз вовремя), и именно благодаря этому управляемый код и родной код могут иметь сопоставимые рабочие характеристики при выполнении. Конечно, всегда есть накладные расходы непосредственно на JIT-компиляцию и на обслуживание во время выполнения, такие как сборка мусора.

Рассмотрим родную библиотеку классов, показанную в листинге 12.12. В ней используются заголовки Windows и CRT.

Листинг 12.12. Создание родного класса окна сообщений
// native_message_box_class.h

#include <wchar.h>
#include <windows.h>

enum MessageBoxType
{
    OK, OKCANCEL, ABORTRETRYIGNORE,
    YESNOCANCEL, YESNO,
    RETRYCANCEL, CANCELTRYCONTINUE,
    ICONHAND = 0x10,
    ICONQUESTION = 0x20,
    ICONEXCLAMATION = 0x30,
    ICONASTERISK = 0x40,
    TYPEMASK = 0xF,
    ICONMASK = 0xF0
};

class MessageBoxClass         // класс MessageBoxClass
{

    wchar_t* m_message;
    wchar_t* m_caption;
    MessageBoxType m_type;
    static const size_t sz = 1024;
    // статическая константа size_t sz = 1024;

public:                       // общедоступный:

    MessageBoxClass(const wchar_t* message, const wchar_t* caption,
    // MessageBoxClass(константа wchar_t* сообщение, 
    //                                константа wchar_t* заголовок,
        MessageBoxType type)
        // MessageBoxType тип)
        : m_type(type)        // (Тип)
    {
        m_message = new wchar_t[sz];
        m_caption = new wchar_t[sz]; // новый 
        wcscpy_s(m_message, sz, message); // используем "безопасную" CRT
        // wcscpy_s(m_message, sz, сообщение); 
        wcscpy_s(m_caption, sz, caption);
        // wcscpy_s (m_caption, sz, заголовок);
    }

    void SetMessage(const wchar_t* message)
    // пусто SetMessage(константа wchar_t* сообщение)
    {
        if (message != NULL)  // если (сообщение != NULL)
        {
            wcscpy_s(m_message, sz, message); // сообщение
        }
    }
    const wchar_t* GetMessage() const { return m_message; }
    // константа wchar_t* GetMessage() константа {возвращает m_message;}

    void SetCaption(const wchar_t* caption)
    // пусто SetCaption(константа wchar_t* заголовок)
    {
        if (caption != NULL)  // если (заголовок != NULL)
        {
            wcscpy_s(m_caption, sz, caption); // заголовок
        }
    }
    const wchar_t* GetCaption() const { return m_caption; }
    // константа wchar_t* GetCaption() константа {возвращает m_caption;}

    MessageBoxType GetType() const { return m_type; }
    // MessageBoxType GetType() константа {возвратить m_type;}
    void SetType(MessageBoxType type){ m_type = type; }
    // пусто SetType (тип MessageBoxType) {m_type = тип;}



    int Display()             // int() Отобразить
    {
        return MessageBoxW(0, m_message, m_caption, m_type); // возврат
    }

    ~MessageBoxClass()
    {
        delete m_message;     // удаление
        delete m_caption;     // удаление
    }

};

В листинге 12.13 показан соответствующий исходный файл, содержащий главный метод main.

Листинг 12.13. Использование пользовательского класса окна сообщений
// native_message_box.cpp
#include "native_message_box_class.h"

int main()                    // int главная программа()
{
    MessageBoxClass* messageBox = new MessageBoxClass(
        L"Do you like this example?", L"Native message box",
        // "Нравится ли вам этот пример?", "Родное окно сообщений",
        static_cast<MessageBoxType>(YESNOCANCEL | ICONASTERISK));

    int result = messageBox->Display(); // Отобразить();

    wchar_t wstr[1024];
    swprintf_s( wstr, L"The dialog result was %d", result);
    // "результат диалога был %d", результат
    messageBox->SetMessage(wstr);
    messageBox->SetType(OK);
    messageBox->Display();    // messageBox->Отобразить();

}

Попробуйте повторно скомпилировать код в листинге 12.13 с опцией /clr. Он работает прекрасно. Вы можете также использовать опцию /clr:pure в данном случае. Как вы уже видели, и заголовки Windows, и CRT поддерживаются в чистом режиме. Независимо от того, компилируете вы с опцией /clr или нет, командная строка для редактирования связей остается той же самой, а исполнимые файлы подобны, хотя фактически они совсем различны.

Чтобы после повторной компиляции экспонировать родные библиотеки другим управляемым сборкам, вы должны написать уровень обертки. Уровень обертки компилируется в ту же самую сборку, что и повторно скомпилированный родной код. Листинг 12.14 показывает уровень обертки для примера окна сообщений. Здесь мы используем несколько методик оптимизации рабочих характеристик. Чтобы получить прямой указатель на символьные данные в классе String (Строка), мы используем PtrToStringChars. Это, конечно, скрепленный указатель, так как его нужно передать родному коду. Альтернативные методы, такие как использование ToCharArray и последующая работа с массивом для получения чего-нибудь подходящего для передачи в качестве константы const wchar_t *, содержали бы копирование символов строки.

Листинг 12.14. Заключение в обертку пользовательского класса окна сообщений
// message_box_wrapper.cpp

#include "native_message_box_class.h"
#include <vcclr.h>

using namespace System;
// используем пространство имен Система;

enum class MessageBoxTypeEnum // класс
{
    OK, OKCANCEL, ABORTRETRYIGNORE,
    YESNOCANCEL, YESNO,
    RETRYCANCEL, CANCELTRYCONTINUE,
    ICONHAND = 0x10,
    ICONQUESTION = 0x20,
    ICONEXCLAMATION = 0x30,
    ICONASTERISK = 0x40,
    TYPEMASK = 0xF,
    ICONMASK = 0xF0
};

wchar_t* MarshalString(String^ s, size_t sizeInCharacters) // Строка
{
    pin_ptr<const wchar_t> pinnedChars = PtrToStringChars(s);
    wchar_t* wcs = new wchar_t[sizeInCharacters]; // = новый
    wcscpy_s(wcs, sizeInCharacters, pinnedChars);
    return wcs;               // возвратить wcs;
}

public ref class MessageBoxWrapper
// общедоступный ссылочный класс MessageBoxWrapper
{

    MessageBoxClass* nativeMessageBox;
    literal unsigned int maxSize = 1024;
    // литерал int без знака maxSize = 1024;

public:                       // общедоступный:

    MessageBoxWrapper(String^ message, String^ caption, MessageBoxTypeEnum type)
    // MessageBoxWrapper(Строка^ сообщение, Строка^ заголовок, 
    //                                         MessageBoxTypeEnum тип)
    {
        pin_ptr<const wchar_t> pinnedMessage = PtrToStringChars(message);
        // (сообщение);
        pin_ptr<const wchar_t> pinnedCaption = PtrToStringChars(caption);
        // (заголовок);

        nativeMessageBox = new MessageBoxClass( // = новый 
            pinnedMessage, pinnedCaption,
            static_cast<MessageBoxType>(type)); // (тип));
    }

    property String^ Caption
    // Свойство Строка^ Заголовок
    {
        String^ get()         // Строка^ получить()
        {
            return gcnew String(nativeMessageBox->GetCaption());
            // возвратить gcnew Строка(nativeMessageBox->GetCaption());
        }
        void set(String^ s)   // пусто установить(Строка^ s)
        {
            nativeMessageBox->SetCaption( MarshalString(s, maxSize) );
        }
    }
    property String^ Message  // Свойство Строка^ Сообщение
    {
        String^ get()         // Строка^ получить ()
        {
            return gcnew String(nativeMessageBox->GetCaption());
            // возвратить gcnew Строка(nativeMessageBox->GetCaption());
        }
        void set(String^ s)   // пусто установить(Строка^ s) 
        {
            nativeMessageBox->SetMessage( MarshalString(s, maxSize) );
        }
    }
    property MessageBoxTypeEnum Type
    // свойство MessageBoxTypeEnum Тип
    {
        MessageBoxTypeEnum get() // получить()
        {
            return static_cast<MessageBoxTypeEnum>(nativeMessageBox->GetType());
            // возврат
        }
        void set(MessageBoxTypeEnum t)
        // пусто установить(MessageBoxTypeEnum t)
        {
            nativeMessageBox->SetType( static_cast<MessageBoxType>( t ));
        }
    }
    int Display()             // int Отобразить()
    {
        if (nativeMessageBox != NULL) 
        // если (nativeMessageBox != NULL)
            return nativeMessageBox->Display();
        // возвратить nativeMessageBox->Отобразить();
        else return -1;       // иначе возвратить -1;
    }

    ~MessageBoxWrapper()
    {
        this->!MessageBoxWrapper();
    }

    !MessageBoxWrapper()
    {
        delete nativeMessageBox; // удаление

    }

};

int main()                    // int главная программа()
{
    MessageBoxWrapper^ wrapper = gcnew MessageBoxWrapper( // обертка
        "Do you like this message box?",
        // "Вы любите это окно сообщений?",
        "Managed wrapper message box.",
        // "Управляемое окно сообщений обертки.",
        MessageBoxTypeEnum::YESNO);
    Console::WriteLine("Message is: {0}", wrapper->Message);
    // Пульт::WriteLine("Сообщение: {0}", обертка->Сообщение);
    int result = wrapper->Display(); // обертка->Отобразить();
    Console::WriteLine("Result was {0}", result);
    Пульт::WriteLine("Результат был {0}", результат); // }
}

Следующий шаг (листинг 12.15) состоит в том, чтобы использовать обертку из другой сборки, возможно даже написанную на другом языке .NET, например C#, фактически экспонируя родную библиотеку классов C#. Межъязыковая работа лучше всего организована в IDE, так как среда разработки делает много сложных вещей за программиста, например внедряет родные декларации в ваш код на C++ с помощью mt.exe, что требуется сделать на определенном этапе в Visual C++ 2005. (Справка относительно родных деклараций в Visual C++ имеется в документации по продукту.) Убедитесь, что скомпилировали код C++/CLI в DLL, а не в исполнимый файл, а затем прибавили ссылку в проект на языке C# на код в проекте на языке C++/CLI.

Листинг 12.15. Использование обертки в сборке C#
// Program.cs
using System;
// использование Системы;
using System.Collections.Generic;
// использование Системы.Коллекции.Родовой (общий);
using System.Text;
// использование Системы.Текст;

class Program                 // класс Программа
{
    static void Main(string[] args)
    // статическая пусто Главная(Строка[] параметры)
    {
        MessageBoxWrapper wrapper = // Обертка 
            new MessageBoxWrapper("I hope you love this message box!",
            // ("Я надеюсь, вам нравится это окно сообщений!",
            "C# using Native Message Box", MessageBoxTypeEnum.OKCANCEL);
        // "C# -- использование родного окна сообщений"
        wrapper.Display();    // обертка.Отобразить();
    }
}

Успешный уровень обертки, вероятно, содержит много преобразований между родными и управляемыми типами. Способ использования этих преобразований может потребовать поразительное количество кода и иметь большое негативное влияние на рабочие характеристики системы оберточных классов. При записи управляемого кода, который часто вызывает родной код в коротком цикле, следует сделать все возможное, чтобы минимизировать переключения контекста. Рассмотрим код в листинге 12.16, который демонстрирует влияние переключений контекста (с управляемого кода на родной и наоборот) на рабочие характеристики. Этот листинг также демонстрирует использование #pragma для включения и родного, и управляемого кода в тот же самый файл. В данном случае весь код после #pragma unmanaged (неуправляемый) и до #pragma managed (управляемый) интерпретируется как родной код. Никакие управляемые конструкции там не позволяются. Перемещая эти прагмы (псевдокомментарии) по коду, вы можете увидеть, как влияет наличие различных частей кода, родного и управляемого.

Листинг 12.16. Использование #pragma managed и #pragma unmanaged
// context_switch.cpp
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>

#pragma unmanaged             // неуправляемый
int native_function(wchar_t* str1, wchar_t* str2 )
{
    int i = 0;
    while (*str1++ = *str2++) i++; // пока 
    return i;                 // возврат
}

#pragma managed               // управляемый

wchar_t* random_string(wchar_t* wcs, int n)
{
    for (int i = 0; i < n - 1; i++)
    {
        wcs[i] = (wchar_t) floor(((double) rand() / (double) RAND_MAX * 26)) + L'A';
    }
    return wcs;               // возвратить wcs;
}
// Попробуйте закомментировать прагму (псевдокомментарий) выше random_string и раскомментировать эту прагму:
// #pragma managed // управляемый.

int main()                    // int главная программа()
{
    wchar_t wcs1[100];
    wchar_t* wcs2 = new wchar_t[100];
    memset(wcs1, 0, 100 * sizeof(wchar_t));
    clock_t t = clock();      // часы();
    const int num_iter = 100000;
    for (int i = 0; i < num_iter; i++)
    {
        random_string(wcs1, 100);
        native_function(wcs2, wcs1);
    }
    double time_elapsed = (clock()-t)/(double)CLOCKS_PER_SEC;
    // двойная точность time_elapsed = 
    //         (часы()-t)/(двойная точность) CLOCKS_PER_SEC;
    printf("total time elapsed: %2.2f seconds\n", time_elapsed);
    // printf ("полное время: %2.2f секунд\n", time_elapsed);
}

На моей системе, когда я выполняю код в листинге 12.16 с native_function в качестве родного кода и random_string в качестве управляемого кода, время выполнения примерно равно 1,3 секунды. С другой стороны, если и native_code, и random_string сделать родными, перемещая комментарий, как указано в комментарии, мы можем избежать переключения контекста в каждом цикле, и время выполнения уменьшится до 0,73 секунды, т.е. почти такое же, как (по крайней мере с точностью до двух десятичных разрядов) и у полностью родного кода, скомпилированного с опцией /O2, поскольку время выполнения полностью родного кода также равно 0,73 секунды.

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

Внутренние указатели

Иногда необходим настоящий указатель, чтобы выполнить немного быстрых арифметических операций над указателями на тип коллекции в алгоритме с критическими рабочими характеристиками. Это должен быть указатель на управляемый тип, который сам находится в управляемой куче. Эту функциональную возможность предоставляет внутренний указатель interior_ptr<тип>. Он называется внутренним указателем, потому что он указывает на адрес внутри управляемого объекта. Внутренний указатель поддерживает арифметические операции над указателями точно так же, как и обыкновенный указатель, но он обновляется сборщиком мусора, если объект перемещается в памяти, точно так же, как и основной адрес в дескрипторе. Внутренние указатели не могут использоваться, чтобы указать на что-нибудь, что не является частью управляемого объекта.

Чтобы присвоить значение внутреннему указателю, следует применить операцию вычисления адреса (&) на управляемом объекте (листинг 12.17).

Листинг 12.17. Использование внутреннего указателя
// interior_ptr.cpp
using namespace System;
// используем пространство имен Система;

ref struct S                  // ссылочный struct S
{
    array<int>^ array1;       // массив

    S()
    {
        array1 = gcnew array<int>(10) // массив
        { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90 };
    }

    void f()                  // пусто f ()
    {
        interior_ptr<int> p = &array1[0];
        for (int i = 0; i < 10; i++)
        {
            Console::WriteLine(*p++); // Пульт
        }
    }
};

Заметьте, что когда вы разыменовываете внутренний указатель, получается объект, точно так же как и при разыменовании обычного указателя. Если взять адрес внутреннего указателя, получится родной указатель, который является адресом, который определяет внутренний указатель. Однако так делать не следует, так как родной указатель не обязательно будет продолжать указывать на объект. Как только объект переместится сборщиком мусора, адрес объекта в памяти больше не будет соответствовать родному указателю.

Скрепляющие указатели

Как вы уже видели, можно запретить сборщику мусора перемещать объект в памяти, создавая то, что называется скрепляющим указателем, и установив его так, чтобы он указывал на член объекта. Любой объект, который содержит элемент, на который указывает скрепляющий указатель, не будет перемещен сборщиком мусора, пока данный скрепляющий указатель находится в области видимости и привязан к объекту. Говорят, что такой объект закреплен. Для этого используется синтаксическая конструкция pin_ptr<тип>.

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

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

Поскольку скрепляющие указатели закрепляют весь управляемый объект, даже если они указывают на какую-нибудь одну часть объекта, вы можете использовать это в своих интересах и написать некоторые эффективные алгоритмы. Например, вы можете закрепить управляемый массив, закрепляя один из его элементов. Затем вы можете использовать родные указатели, чтобы выполнить обработку массива, не беспокоясь, что управляемый массив мог бы переместиться в памяти. Но вы не должны злоупотреблять скрепляющими указателями, потому что закрепление объектов в управляемой куче приводит к снижению эффективности сборщика мусора. Поэтому будьте внимательны и следите за областью видимости скрепляющего указателя. Удостоверьтесь, что он выходит из области видимости как можно скорее после того, как он больше не нужен, или присвойте ему nullptr, что будет иметь тот же самый эффект.

Вы должны проявить особенную заботу и осторожность, присваивая значения скрепляющим указателям, чтобы гарантировать, что полученные указатели не будут сохраняться вне ограниченной области видимости, в которой объявлен скрепляющий указатель. Поэтому не возвращайте скрепляющий указатель как возвращаемое значение и не возвращайте указатель, которому был присвоен скрепляющий указатель в качестве возвращаемого значения. Если сделать так, то вы будете иметь указатель, который указывает на что-нибудь в управляемой куче, не обязательно на объект вообще (и это произойдет, как только сборщик мусора переместит исходный закрепленный объект). Этот тип ошибок программирования известен как GC-пустота. Листинг 12.18 показывает пример GC-пустоты.

Листинг 12.18. Демонстрация GC-пустоты
// gc_hole.cpp
using namespace System;
// используем пространство имен Система;

ref struct R                  // ссылочный struct R
{
    array<int>^ a;            // массив <int>^ a;

    R()
    {
        a = gcnew array<int> { 1, 2, 3, 4, 5 }; // массив
    }
};

void F(int* ptr)              // пусто F (int* ptr)
{
    if (ptr)                  // если (ptr)
        Console::WriteLine(*ptr); // Пульт -- возможный аварийный отказ
}

int* GcHole(R^ r)             // gc-пустота
{
    pin_ptr<int> pinp = &r->a[0];
    int *ptr;
    ptr = pinp; // указателю присваивается скрепляющий указатель
    // ...
    return ptr;               // возвратить ptr;
    // возвращается указатель, указывающий в кучу сборщика мусора (!)
}

int main() {                  // int главная программа() {
    R^ r = gcnew R;
    F(GcHole(r));
}

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

Иногда приходится смешивать управляемые объекты и родные типы. Вы теперь изучите, что нужно сделать, чтобы инкапсулировать управляемый объект в родном типе, а также как включить родные объекты в управляемые типы.

Но сначала небольшой фон и контекст. Когда требуется написать такой код? Если вы будете расширять родное приложение с помощью управляемых типов, то, вероятно, придется использовать родные типы в ваших управляемых типах. Если, кроме того, родные типы должны обращаться к управляемым типам, то вы должны будете использовать шаблон gcroot для обращения к ним, как вы позже увидите в следующем разделе.

Использование управляемых объектов в родном классе

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

Листинг 12.19. Неправильное использование дескриптора
// native_in_managed_bad.cpp

using namespace System;
// используем пространство имен Система;

ref class R {};               // ссылочный класс R{};

class N                       // класс N
{
    R^ r; // недопустимо, не разрешается

public:                       // общедоступный:
    N()
    {
        r = gcnew R();
    }

};

Есть способ должным образом вложить дескриптор в родной тип, и этот правильный способ состоит в том, чтобы использовать шаблон gcroot с дескриптором для ссылочного типа в качестве параметра. В главе 6 “Классы и структуры” вы видели, как это было сделано с шаблонами gcroot и auto_gcroot. Листинг 12.20 поясняет различие между шаблоном gcroot и шаблоном auto_gcroot.

Листинг 12.20. Сравнение gcroot с auto_gcroot
// auto_gcroot.cpp

#include <msclr/gcroot.h>
#include <msclr/auto_gcroot.h>
using namespace System;
// используем пространство имен Система;
using namespace msclr;
// используем пространство имен msclr;

ref class R                   // ссылочный класс R
{
public:                       // общедоступный:
    void f()                  // пусто f ()
    {
        Console::WriteLine("managed member function");
        // Пульт::WriteLine ("управляемая функция-член");
    }

    ~R()
    {
        Console::WriteLine("destructor");
        // Пульт::WriteLine("деструктор");
    }

};

class N                       // класс N
{
    gcroot<R^> r_gcroot;
    auto_gcroot<R^> r_auto_gcroot;

public:                       // общедоступный:
    N()
    {
        r_gcroot = gcnew R();
        r_gcroot->f();
        r_auto_gcroot = gcnew R();
        r_auto_gcroot->f();
    }

};

int main()                    // int главная программа()
{
    N n;
    // Когда n будет уничтожен, выполнится деструктор 
    // для объекта auto_gcroot,
    // но не для объекта gcroot.
}

Вот выдача программы, приведенной в листинге 12.20:

managed member function
managed member function
destructor

Как видно, деструктор вызывался только однажды, для объекта auto_gcroot. Теперь, если мы имеем функцию, которая принимает дескриптор управляемого объекта, мы можем передать ей вместо дескриптора gcroot или auto_gcroot. И gcroot, и auto_gcroot имеют неявные преобразования в основные дескрипторы. Они также оба работают с упакованными типами значений.

Использольвание родного объекта в управляемом типе

Еще в главе 6 “Классы и структуры” вы видели один из способов включить родной объект в управляемый тип. Несколько более пуританский способ включить такой объект состоит в том, чтобы использовать шаблонный класс, который удостоверится, что родной класс будет очищен должным образом автоматически, когда содержащий его класс выходит из области видимости. В листинге 12.21 определен шаблонный ссылочный тип native_root, который инкапсулирует родной указатель и может использоваться подобно auto_gcroot. Мы используем родной класс, чтобы открыть файл, и мы видим, что файл закрывается, когда вызывается delete (удаление) для включенного ссылочного типа или включающий объект выходит из области видимости.

Листинг 12.21. Инкапсуляция родного указателя
// native_in_managed.cpp

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

using namespace System;
// используем пространство имен Система;
using namespace System::Runtime::InteropServices;
// используем пространство имен Система::Время выполнения::InteropServices;

// шаблон для внедрения родного класса
// в ссылочный тип
template<typename T>          // шаблон 
ref class native_root         // ссылочный класс native_root
{
    T* t;

    !native_root()
    {
        if (t)                // если (t)
        {
            delete t;         // удаление t;
            t = NULL;
        }
    }

    ~native_root()
    {
        this->!native_root();
    }

public:                       // общедоступный:

    native_root() : t(new T) {}

    // Они должны быть статическими, чтобы их нельзя было использовать 
    // в пределах класса (например, когда мы используем 
    // this-> (этот->) в ~native_root).

    // предоставляет доступ к основному указателю
    static T* operator&(native_root% n) { return n.t; }
    // статический T* operator& (native_root % n) {возвратить n.t;}
    // позволяет использовать ->, чтобы обратиться к элементам
    static T* operator->(native_root% n) { return n.t; }
    // статический T* оператор->(native_root % n) {возвратить n.t;}
};

class native_exception {};    // класс native_exception {};

// типичный родной класс
class NativeClass             // класс NativeClass
{
    FILE* fp;
    static const int TIME_BUFFER_SIZE = 32;
    // статический константа int TIME_BUFFER_SIZE = 32;

public:                       // общедоступный:
    NativeClass()
    {
        printf("Opening the file.\n");
        // printf ("Открываю файл\n");
        // Открыть файл для записи в Уникоде (Unicode).
        int errcode = fopen_s(&fp, "myfile.txt", "a+, ccs=UNICODE");
        if (errcode != 0)     // если (errcode != 0)
        {
            throw new native_exception;
            // вызвать новый native_exception;
        }
    }

    void OutputText(const wchar_t* text)
    // пусто OutputText(константа wchar_t* текст)
    {
        if (fp)               // если (fp)
        {
            wprintf(text);    // wprintf (текст);
            fwprintf(fp, text); // fwprintf (fp, текст);
        }
        else                  // иначе
        {
            throw new native_exception;
            // вызвать новый native_exception;
        }
    }

    void TimeStamp()          // пусто TimeStamp()
    {
        tm newtime;
        __time32_t time;      // Время 
        wchar_t time_text[TIME_BUFFER_SIZE];
        _time32( &time );
        _localtime32_s( &newtime, &time );
        _wasctime_s(time_text, TIME_BUFFER_SIZE, &newtime);
        if (fp)               // если (fp)
        {
            wprintf(time_text);
            fwprintf(fp, time_text);
        }
        else                  // иначе
        {
            throw new native_exception;
            // вызвать новый native_exception;
        }
    }

    ~NativeClass()
    {
        printf("Closing the file.\n");
        // printf ("Закрываю файл\n");
        if (fp)               // если (fp)
        {
            fclose(fp);
        }
    }
};

// Ссылочный тип, включающий родной класс
ref class R                   // ссылочный класс R
{
    native_root<NativeClass> n;

public:                       // общедоступный:

    R() { }

    // Маршализировать строку String в строку Уникода (Unicode)
    // и передать указатель на метод родного класса
    void OutputToFile(String^ s)
    // пусто OutputToFile(Строка^ s), 
    {
        IntPtr ptr = Marshal::StringToHGlobalUni(s);
        n->OutputText(static_cast<wchar_t*>( ptr.ToPointer()));
        n->TimeStamp();
        Marshal::FreeHGlobal(ptr);
    }
};

int main()                    // int главная программа()
{
    R^ r1 = gcnew R();
    r1->OutputToFile("Output through native class!\n");
    // r1->OutputToFile ("Вывод через родной класс!\n");
    delete r1;                // удаление r1; -- файл закрыт

    R r2;
    r2.OutputToFile("More output\n");
    // Файл закрыт снова, когда r2 очищен.
}

Выдача программы, приведенной в листинге 12.21, выглядит примерно так, как показано ниже:

Opening the file.
Output through native class!
Tue Sep 05 23:39:57 2006
Closing the file.
Opening the file.
More output
Tue Sep 05 23:39:57 2006
Closing the file.

Обратите внимание, что статический элемент StringToHGlobalUni класса Marshal (Маршализация) используется для преобразования из String (Строка) в wchar_t*. Благодаря этому создается новый массив широких символов и возвращается указатель на него в форме IntPtr, который как раз и должен быть освобожден. С помощью Marshal::FreeHGlobal можно освободить память в управляемом коде вместо того, чтобы вызвать родной API GlobalFree для освобождения памяти. IntPtr имеет метод ToPointer, который возвращает пустой указатель, который мы затем приводим к нужному типу для вызова управляемой функции.

Родные и управляемые точки входа

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

Вы знаете, что функции в Visual C++ имеют соглашения о вызовах, определяющие, как параметры обрабатываются функцией — вы уже видели некоторые примеры. Родные соглашения о вызовах, такие как __cdecl, __stdcall и __thiscall, указывают определенные способы передачи параметров. Подобно этому и управляемые функции имеют соглашение о вызовах, __clrcall, которое описывает подробнocти вызова управляемых функций. Функция с соглашением о вызовах __clrcall имеет только управляемую точку входа. Родная точка входа для нее не генерируется.

Соглашение о вызовах для управляемых функций и, следовательно, множество точек входа, которые будут сгенерированы для функции, зависят от режима компиляции. В чистом режиме и в безопасном режиме для всех управляемых функций по умолчанию используется __clrcall. Если программа компилируется в безопасном режиме или в чистом режиме, то управляемая функция будет сгенерирована только с управляемой точкой входа, потому что в этих режимах отсутствует родной код, который требовал бы родной точки входа. Однако, когда вы компилируете в смешанном режиме (/clr), поскольку вероятен сценарий взаимодействия, генерируются и родные, и управляемые точки входа. Это происходит потому, что соглашение о вызовах должно быть родным соглашением о вызовах (__thiscall для методов и, вероятно, __cdecl для глобальных функций, но это может быть изменено опцией компилятора).

Если используется __declspec(dllexport) для управляемой функции, благодаря чему функция становится доступной для вызывающих программ вне DLL, то создается родная точка входа, которая может использоваться родными вызывающими программами, которые в этом случае могут использовать __declspec(dllimport) для вызова функции.

Как избежать двойного переключения системного вызова

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

Компилятор будет пробовать получить правильную точку входа; однако иногда он нуждается в некоторой помощи. Использовать соглашение о вызовах __clrcall — один из способов обойти проблему. Если не нужно вызвать вашу функцию из родного кода, можно использовать соглашение о вызовах __clrcall для управляемой функции. Если нет родной точки входа, то ее нельзя использовать ненадлежащим образом. Соглашение о вызовах __clrcall необходимо только при компиляции в смешанном режиме (с опцией /clr), потому что в чистом режиме и в безопасном режиме по умолчанию используется __clrcall; так или иначе, но родная точка входа не генерируется.

В другой ситуации, когда может произойти двойное переключение системного вызова (экспорт управляемой функции из DLL), следует избегать использовать __declspec(dllexport) и __declspec(dllimport) при вызовах управляемых функций из управляемого кода. Чтобы сослаться на управляемую сборку, вместо этого следует применить #using.

Управляемые и родные исключения

У вас может возникнуть вопрос: если управляемый код вызывает родной код, как ошибки и исключения передаются от родного кода управляемому коду? В этом разделе ответим на этот вопрос.

Взаимодействие со структурированными исключениями (__try/__except)

Структурированная обработка особых ситуаций, или Structured Exception Handling (SEH), используется на базовой платформе Windows в C и C++ для многих технических средств и программных аварийных ситуаций. Возможные коды ошибок перечислены в заголовках Windows. Если SEH-исключение передается в управляемый код, оно заключается в обертку как .NET-исключение некоторого типа. Многие структурированные исключения соответствуют определенным .NET-типам исключения. Например, EXCEPTION_INT_DIVIDE_BY_ZERO соответствует DivideByZeroException. Если отображение не определено, генерируется System::Runtime::InteropServices::SEHException.

В листинге 12.22 демонстрируются два способа обработки структурированных исключений. Исключение в родном коде — целочисленное деление на нуль. В первой ветви исключение передается управляемому коду и перехватывается как SEHException. Во второй ветви оно перехватывается как родное SEH-исключение в операторе перехвата __try/__catch.

Листинг 12.22. Обработка структурированных исключений
// try_except.cpp
#include <stdio.h>
#include <windows.h>          // для EXCEPTION_INT_DIVIDE_BY_ZERO
#include <excpt.h>

using namespace System;
// используем пространство имен Система;
using namespace System::Runtime::InteropServices;
// используем пространство имен Система::Время выполнения::InteropServices;

#pragma unmanaged             // #pragma неуправляемый
void generate_SEH_exception() // пусто generate_SEH_exception ()
{
    int i = 0;
    // Деление на нуль генерирует SEH-исключение.
    int x = 2 / i;
}

void generate_AV()            // пусто generate_AV ()
{
    int *pn = 0;
    int n = *pn; // генерирует нарушение прав доступа
}

int filter_div0(unsigned int code, struct _EXCEPTION_POINTERS *ep)
// int filter_div0(int без знака код, struct _EXCEPTION_POINTERS *ep)
{

    if (code == EXCEPTION_INT_DIVIDE_BY_ZERO)
    // если (код == EXCEPTION_INT_DIVIDE_BY_ZERO)
    {
        return EXCEPTION_EXECUTE_HANDLER; // возврат
    }
    else                      // иначе
    {
        return EXCEPTION_CONTINUE_SEARCH;
        // возвратить EXCEPTION_CONTINUE_SEARCH;
    };
}

// Это должно быть родной функцией, потому что 
// __try/__except (__пробовать/__кроме) не
// допускается в той же самой функции, в которой используется код, 
// перехвата try/catch (попытка/перехват).
void try_except(bool bThrowUnhandledAV)
// пусто try_except (bool bThrowUnhandledAV)
{
    __try                     // __пробовать
    {
        if (bThrowUnhandledAV) // если (bThrowUnhandledAV)
            generate_AV();
        else // иначе
            generate_SEH_exception();
    }
    __except( filter_div0(GetExceptionCode(), GetExceptionInformation()))
    {
        printf_s("Divide by zero exception caught via SEH __except block.");
        // ("исключение Деления на нуль, перехваченное через блок SEH.");
    }
}

#pragma managed               // #pragma управляемый

int main(array<String^>^ args)
// int главная программа(массив <Строка^>^ параметры)
{
    if (args->Length < 1)     // если (параметры->Длина <1)
    {
        Console::WriteLine("Usage: try_except [NET|SEH|AV]");
        // Пульт::WriteLine ("Использование: try_except [NET|SEH|AV]");
        return -1;            // возвратить -1;
    }
    if (args[0] == "NET") 
        // Демонстрируют перехват SEH как .NET-Исключения
                              // если (параметры[0] == "NET") 
    {
        try                   // попробовать
        {
            generate_SEH_exception();
        }
        catch(DivideByZeroException^ e) // перехват
        {
            Console::WriteLine(e->ToString()); // Пульт
        }
    }
    else if (args[0] == "SEH") 
        // Демонстрируется обработка родного SEH-исключения.
                             // иначе если (параметры[0] == "SEH")
    {
        // Вызвать родную функцию с блоком try/except (попытка/кроме)
        // и отфильтрировать исключения деления на нуль.
        try_except(false);    // try_except(ложь);
    }
    else if (args[0] == "AV") 
        // Демонстрируем фильтрацию родных обрабатываемых исключений
        // и позволяем пропускать остальные.
        // иначе если (параметры[0] == "AV") 
    {
        try                   // попробовать
        {
            // Исключения AV, однако, не фильтрируются и 
            // пропускаются к управляемому коду.
            try_except(true); // try_except (истина);
        }
        catch(AccessViolationException^ e) // перехват
        {
            Console::WriteLine(e->ToString()); // Пульт
        }
    }
}

Вот выдача программы, приведенной в листинге 12.22, если она была запущена с помощью командной строки try_except NET:

System.DivideByZeroException: Attempted to divide by zero.
at generate_SEH_exception()
at main(String[] args
the output with the command line try_except SEH is

А вот вывод той же программы, если она была запущена с помощью командной строки try_except SEH:

Divide by zero exception caught via SEH block.

И вот, наконец, вывод той же программы, если она была запущена с помощью командной строки try_except AV:

System.AccessViolationException: Attempted to read or write protected memory.
This is often an indication that other memory is corrupt.
at try_except(Boolean )
at main(String[] args)

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

Взаимодействие с кодами ошибок Win32

Вы не можете просто вызвать GetLastError через P/Invoke после вызова API Windows и ожидать получить код ошибки, соответствующий функции Win32, вызванной в последнем вызове P/Invoke, потому что нет гарантии, что между вызовом представляющей интерес функции и вызовом GetLastError сохраняется значение кода ошибки. Правильный способ получить код ошибки состоит в том, чтобы вызвать Marshal::GetLastWin32Error, как показано в листинге 12.23.

Листинг 12.23. Обработка кодов ошибок Win32
// getlasterror.cpp

#using "System.dll"

using namespace System;
// используем пространство имен Система;
using namespace System::ComponentModel; // для Win32Exception
// используем пространство имен Система::ComponentModel; 
using namespace System::Runtime::InteropServices;
// используем пространство имен Система::Время выполнения::InteropServices;

[DllImport("kernel32.dll", SetLastError=true)]
extern bool SetVolumeLabel(String^ lpRootPathName, String^ lpVolumeName);

bool TestGetLastWin32Error()
{
    if (SetVolumeLabel("BAD:\\", "VolumeName")) // если
    {
        System::Console::WriteLine("Success!");
        // Система::Пульт::WriteLine ("Успех!");
        return true;          // возвратить истину;
    }
    else                      // иначе
    {
        throw gcnew Win32Exception(Marshal::GetLastWin32Error());
    }
    return false;             // возвратить ложь;
}

int main()                    // int главная программа()
{
    try                       // попробовать
    {
        TestGetLastWin32Error();
    }
    catch(Win32Exception^ e)  // перехват
    {
        Console::WriteLine(e->ToString()); // Пульт
    }
}

Вот выдача программы, приведенной в листинге 12.23:

System.ComponentModel.Win32Exception: The filename, directory name, or volume
label syntax is incorrect
at TestGetLastWin32Error()
at main()

Взаимодействие с исключениями C++

Обработка особых ситуаций C++ может сосуществовать с обработкой особых ситуаций CLR. Вы можете использовать последовательные блоки перехвата, причем в некоторых фильтрах перехвата можно указать исключения C++, а CLR-исключения — в других фильтрах перехвата. Вспомните, что в главе 10 “Исключения, атрибуты и отражение” вы видели, что случается, если вбросить тип, который не является производным от System::Exception (Система::Исключение) в код на другом языке .NET. В этом случае тип неисключения будет заключен в обертку как RuntimeWrappedException. Заключение в обертку также происходит, когда родной тип вбрасывается из родного кода — такой тип заключается в обертку как SEHException в управляемом коде на C++/CLI, и, если он не перехватывается соответствующим блоком перехвата, он будет перехвачен фильтрами перехвата, которые соответствуют SEHException, ExternalException (базовый класс SEHException) или Exception (Исключение). Листинг 12.24 показывает поведение при вбрасывании значения и при вбрасывании через родной указатель.

Листинг 12.24. Сосуществование обработки особых ситуаций C++ и CLR
// native_exception.cpp
#include <wchar.h>

using namespace System;
// используем пространство имен Система;
using namespace System::Runtime::InteropServices;
// используем пространство имен Система::Время выполнения::InteropServices;

#pragma unmanaged             // #pragma неуправляемый

class NativeException         // класс NativeException
{
    wchar_t m_str[1024];

public:                       // общедоступный:

    NativeException(wchar_t* s)
    {
        wcscpy_s(m_str, s);
    }

    const wchar_t* GetMessage() { return m_str; }
    // константа wchar_t* GetMessage () {возвращает m_str;}
};

void throw_native_exception(bool byval)
// пусто throw_native_exception (bool byval)
{
    if (byval)                // если (byval)
        throw NativeException(L"Native Exception By Value");
    // вызвать NativeException ("Родное исключение по значению");
    else                      // иначе
        throw new NativeException(L"Native Exception on Native Heap");
    // вызвать новый NativeException ("Родное исключение в родной куче");
}

#pragma managed               // #pragma управляемый

int main()                    // int главная программа()
{
    bool byval = true;        // истина;

    try                       // попробовать
    {
        throw_native_exception(byval);
    }
    catch(NativeException& native_exception) // перехват
    {
        wprintf(L"Caught NativeException: %s\n", native_exception.GetMessage());
    }
    catch(SEHException^ e)    // перехват
    {
        Console::WriteLine("{0}\nErrorCode: 0x{1:x}", // Пульт
            e->ToString(), e->ErrorCode);
    }

    byval = false;            // ложь;

    try                       // попробовать
    {
        throw_native_exception(byval);
    }
    catch(NativeException* native_exception) // перехват
    {
        wprintf(L"Caught NativeException: %s\n", native_exception->GetMessage());
    }
    catch(SEHException^ e)    // перехват
    {
        Console::WriteLine("{0}\nErrorCode: 0x{1:x}", // Пульт
            e->ToString(), e->ErrorCode);
    }
}

Вывод программы в листинге 12.24 приведен ниже:

Caught NativeException: Native Exception By Value
Caught NativeException: Native Exception on Native Heap

Взаимодействие с СOM HRESULT

СOM HRESULT заключается в обертку как исключение. Оно может появиться как определенный тип исключения, такой как OutOfMemoryException для HRESULT E_OUTOFMEMORY, или, если нет определенного отображения, как COMException, которое имеет свойство ErrorCode, имеющее исходное значение HRESULT.

Резюме

В этой главе были освещены различные аспекты функциональной совместимости — взаимодействие с другими языками .NET, взаимодействие с родным кодом, включая P/Invoke, различные доступные режимы компиляции, экспонирование родного кода другим языкам .NET. Мы кратко коснулись взаимодействия с СOM. Мы также рассмотрели типы указателей, полезные для функциональной совместимости, такие как interior_ptr и pin_ptr, родные и управляемые точки входа, соглашение о вызовах __clrcall, двойное переключение системного вызова; как его избежать, как включить родной класс в управляемый класс и как включить управляемый класс в родной класс с помощью gcroot и auto_gcroot, и, наконец, как исключения и ошибки в родном коде пересекают границу между родным и управляемым кодом и обрабатываются в управляемом коде.


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