
Сегодня я (как, наверное, и многие другие программисты), все больше использую в своих разработках платформу .NET и язык C#, но все еще остаются уголки где оправдано применение C++. Это создает необходимость их интеграции.
Зачем?
- В C++ код целесообразно выносить алгоритмы, критичные к производительности
- В C++ код целесообразно выносить части, связанные с защитой приложения
- Много старого кода написано на C++, и переписывать его весь — не лучшее решение
Перед хабракатом, скажу сразу, что статья расскажет и покажет один, не совсем классический способ такого взаимодействия, который может оказаться крайне полезным во многих реальных приложениях.
Как?
- P/Invoke
- COM
- C++/CLI
P/Invoke — очень просто, но не гибко. Нет поддержки ООП, не удобно выносить сразу много методов.
COM — удобно и довольно мощно, но COM библиотеки требуют регистрации в системном реестре, и, к тому же, для установки требуются права администратора (UPD: как справедливо отмечают в комментариях, не обязательно).
C++/CLI — очень плотная интеграция, довольно хорошее решение. Но если Вы не можете скомпилировать Ваш C++ код с ключем /CLI, то потребуется вводить дополнительную промежуточную C++/CLI библиотеку и прописывать в ней обертки для каждого вызова. Кроме того, CLI часто усложняет код и имеет некоторый ряд ограничений.
А как иначе?
В этой статье, я хочу рассказать о другом, интересном и весьма изящном решении, основанном на COM Interop + P/Invoke, и позволяющем легко вызвать C++ классы из C#, без необходимости их COM регистрации в системе (Ваши программы смогут запускаться портативно, например, с флешки). Для этого мы будем создавать самые обычные C++ классы с виртуальными методами и передавать их в C# через P/Invoke как COM интерфейсы.
Перейдем непосредственно к делу.
Создаем два проекта. Один — C++ (без использования CLR и MFC, включаем юникод). Пусть он назвывается например Lib.

(обратите внимание, для простоты отладки, и избежания необходимости дальнейшего копирования, выходная библиотека собирается в папку Windows).
После этого создаем C# проект — Windows Forms. Назовем его net2c. Собираем тестовую форму:

Из интерфейса думаю уже понятно, что примерно будет делать наша C++ библиотека.
Теперь возвращаемся в C++ проект и пишем одну единственную функцию, которая будет создавать наши объекты:
// Тип создаваемого объекта
enum EObjectType
{
Hello = 0,
GraphicAlgorithm
};
__declspec(dllexport) void* __stdcall Create(EObjectType AType)
{
if (AType == Hello) return new CHello();
if (AType == GraphicAlgorithm) return new CGraphicAlgorithm();
return NULL;
}
Чтобы избежать декорирования имен экспортируемых функций, нужно создать еще один .DEF файл и написать в нем:
EXPORTS
Create
Как альтернативный вариант, для создания необходимых объектов можно сделать отдельный единственный класс, который будет возвращаться этой функцией, но я пойду самым простым путем.
Теперь код класса CHello:
// Сгенерирован студией
// {08356CFE-A3DD-43c2-980C-1393E37118B2}
static const GUID IID_IHello =
{ 0x8356cfe, 0xa3dd, 0x43c2, { 0x98, 0xc, 0x13, 0x93, 0xe3, 0x71, 0x18, 0xb2 } };
class IHello : public CBaseUnknown
{
public:
STDHRESULTMETHOD SetName(LPWSTR AName) = 0;
STDHRESULTMETHOD Say(HWND AParent) = 0;
};
class CHello :
public IHello
{
public:
STDHRESULTMETHOD QueryInterface(const CLSID &AId, void ** ARet)
{
__super::QueryInterface(AId, ARet);
if (AId == IID_IHello) *ARet = (IHello*)this;
return (*ARet != NULL) ? S_OK : E_NOINTERFACE;
}
STDHRESULTMETHOD SetName(LPWSTR AName)
{
mName = AName;
return S_OK;
}
STDHRESULTMETHOD Say(HWND AParent)
{
wstring message = L"Hello, my friend " + mName;
MessageBox(AParent, message.c_str(), L"From C++", MB_ICONINFORMATION);
return S_OK;
}
private:
wstring mName;
};
Как видите, все очень просто. Класс поддерживает один единственный интерфейс IHello и реализует пару элементарных методов.
Теперь самое интересное, идем в C# проект и пишем:
// Копируем наш enum в C# с небольшой доработкой
public enum EObjectType : int
{
Hello = 0,
GraphicAlgorithm
}
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("08356CFE-A3DD-43c2-980C-1393E37118B2")]
public interface IHello
{
void SetName([MarshalAs(UnmanagedType.LPWStr)]string AName);
void Say(IntPtr AParent);
}
public class Lib
{
// Внешний метод из нашей библиотеки
[DllImport("Lib.dll")]
protected static extern IntPtr Create(EObjectType AType);
// Обертка для него
public static object CreateObject(EObjectType AType)
{
IntPtr ptr = Create(AType);
return Marshal.GetObjectForIUnknown(ptr);
}
}
И снова ничего сложного, кроме, возможно, Marshal.GetObjectForIUnknown. Эта функция принимает IntPtr, указывающий на COM объект, и возвращает System.Object для него. Отдельно обращу Ваше внимание на то, как объявлен интерфейс IHello. Тут мы говорим компилятору, что это COM интерфейс, и пишем тот же самый GUID, что и в C++ программе.
Все! Теперь С++ методы класса CHello можно вызывать из C# абсолютно без проблем, и даже не вспоминать что за этим стоит злой и страшный C++. Смотрите сами:
object hiObject = Lib.CreateObject(EObjectType.Hello);
IHello hello = hiObject as IHello;
hello.SetName(txtName.Text);
hello.Say(Handle);

(все и правда работает)
На десерт
Теперь вторая, бонусная часть — пара простейших графических алгоритмов. Это более реальный пример. При обработке больших изображений, код алгоритма действительно целесообразно вынести в C++ библиотеку.
C++ файл
STDMETHODIMP CGraphicAlgorithm::QueryInterface(const CLSID &AId, void ** ARet)
{
__super::QueryInterface(AId, ARet);
if (AId == IID_IGraphicAlgorithm) *ARet = (CBaseUnknown*)this;
return (*ARet != NULL) ? S_OK : E_NOINTERFACE;
}
STDMETHODIMP CGraphicAlgorithm::MakeGrayScale(void* APointer, int AWidth, int AHeight)
{
RGBQUAD *p = (RGBQUAD*)APointer;
for(int y = 0; y < AHeight; y++)
{
for(int x = 0; x < AWidth; x++)
{
// Это неправильный алго :)
short mid = ((short)p->rgbBlue + p->rgbGreen + p->rgbRed) / 3;
if (mid > 255) mid = 255;
BYTE v = (BYTE)mid;
memset(p, v, 3);
p++;
}
}
return S_OK;
}
STDMETHODIMP CGraphicAlgorithm::MakeAlpha(void* APointer, int AWidth, int AHeight)
{
RGBQUAD *p = (RGBQUAD*)APointer;
for(int y = 0; y < AHeight; y++)
{
for(int x = 0; x < AWidth; x++)
{
// Это еще более неправильный алго, зато красиво :)
memset(p, p->rgbReserved, 4);
p++;
}
}
return S_OK;
}
H файл
// {65ACBBC0-45D2-4622-A779-E67ED41D2F26}
static const GUID IID_IGraphicAlgorithm =
{ 0x65acbbc0, 0x45d2, 0x4622, { 0xa7, 0x79, 0xe6, 0x7e, 0xd4, 0x1d, 0x2f, 0x26 } };
class CGraphicAlgorithm : CBaseUnknown
{
public:
STDHRESULTMETHOD MakeGrayScale(void* APointer, int AWidth, int AHeight);
STDHRESULTMETHOD MakeAlpha(void* APointer, int AWidth, int AHeight);
STDHRESULTMETHOD QueryInterface(const CLSID &AId, void ** ARet);
};
Отдельно обращаю Ваше внимание на то, что здесь есть IID_IGraphicAlgorithm, но самого интерфейса IGraphicAlgorithm, как такого, нет. Как так? Это сделано специально, чтобы еще больше упростить нашу с вами работу и писать еще меньше кода. Единственное, что тут важно учитывать, все виртуальные методы, которые относятся к интерфейсу IGraphicAlgorithm должны идти в начале класса и строго по порядку. Кроме того, тогда класс не сможет предоставлять несколько разных интерфейсов (что нам и не надо, для этого удобнее и логичнее сделать еще один класс).
Возвращаемся в шарп и пишем такой код:
IGraphicAlgorithm utils = Lib.CreateObject(EObjectType.GraphicAlgorithm) as IGraphicAlgorithm;
BitmapData data = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
if (optGray.Checked)
utils.MakeGrayScale(data.Scan0, data.Width, data.Height);
else
utils.MakeAlpha(data.Scan0, data.Width, data.Height);
bmp.UnlockBits(data);
picDest.Image = bmp;
picSrc.Refresh();
picDest.Refresh();
Тут мы, сначала, получаем указатель на память, где хранятся данные битмапа, а потом отдаем этот указатель в С++ библиотеку, для последующей обработки. Сразу предупреждаю, что это еще не самый эффективный и удобный способ, но если тема Вам интересна, то о прямой работе с изображениями в памяти, я могу рассказать в отдельной статье.
В результате:

Итоги
Смотрите что у нас получилось:
- Немного кода приходится написать в самом начале, для обеспечения создания объектов
- Далее, чтобы добавить новый класс, достаточно с генерировать один GUID и продублировать описание методов C++ класса, в C# интерфейсе
- Чтобы добавить новый метод, достаточно указать его по одному разу в C++ и в C#
- Об удалении объектов можно не заботится. Если в базовом классе CBaseUnknown есть счетчик ссылок, то сборщик мусора сделает все за нас
- Абсолютно полноценная отладка, если оба проекта включить в одно решение
- Чтобы библиотека работала, ее достаточно кинуть в одну папку с программой. Очень просто и удобно.
P.S. Полные исходные коды можно скачать тут: http://66bit.ru/files/paper/net2c/net2c.zip
P.P.S. Следующая статья будет об обратной задаче — прямом использовании C# библиотек в C++
UPD. Вышла вторая часть