Pull to refresh

Используем Unmanaged С++ код в .NET программах

.NET *
image

Сегодня я (как, наверное, и многие другие программисты), все больше использую в своих разработках платформу .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.

image
(обратите внимание, для простоты отладки, и избежания необходимости дальнейшего копирования, выходная библиотека собирается в папку Windows).

После этого создаем C# проект — Windows Forms. Назовем его net2c. Собираем тестовую форму:

image

Из интерфейса думаю уже понятно, что примерно будет делать наша 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);



image
(все и правда работает)

На десерт


Теперь вторая, бонусная часть — пара простейших графических алгоритмов. Это более реальный пример. При обработке больших изображений, код алгоритма действительно целесообразно вынести в 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();


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

В результате:
image

Итоги


Смотрите что у нас получилось:
  • Немного кода приходится написать в самом начале, для обеспечения создания объектов
  • Далее, чтобы добавить новый класс, достаточно с генерировать один GUID и продублировать описание методов C++ класса, в C# интерфейсе
  • Чтобы добавить новый метод, достаточно указать его по одному разу в C++ и в C#
  • Об удалении объектов можно не заботится. Если в базовом классе CBaseUnknown есть счетчик ссылок, то сборщик мусора сделает все за нас
  • Абсолютно полноценная отладка, если оба проекта включить в одно решение
  • Чтобы библиотека работала, ее достаточно кинуть в одну папку с программой. Очень просто и удобно.

P.S. Полные исходные коды можно скачать тут: http://66bit.ru/files/paper/net2c/net2c.zip
P.P.S. Следующая статья будет об обратной задаче — прямом использовании C# библиотек в C++

UPD. Вышла вторая часть
Tags:
Hubs:
Total votes 21: ↑18 and ↓3 +15
Views 6.5K
Comments 7
Comments Comments 7