понедельник, 22 февраля 2010 г.

Мой первый эксперимент с C++/CLI

Сегодня неважно себя чувствовал, и решил из-за этого вместо активной работы спокойно покопаться со связкой .NET и native C++. Первой мыслью было придумать лайфхак для возможности за день-два полностью перенести имеющийся код из текущего проекта по работе, который пишется на C++/Qt с C-шным фронтендом для олдскульных программистов. :)

Запала в голову сначала мысль сгенерировать в полуавтоматическом режиме какую-нибудь обёртку для кода на C++ (сразу вспомнился SWIG), но потом всё-таки решил, что это будет невозможно из-за того, что взаимодействие должно быть не в одну сторону (.NET использует C++), а в обе — .NET видит и использует код на C++, а C++ видит код из .NET, причём он должен видеть его именно как код на C++. Второе условие и кажется мне невыполнимым без ручного написания враппера. Потому остановился на том, что надо писать .NET-обёртку над C-шным фронтендом, благо, он в будущем планируется настолько полным, чтобы покрывать функциональность API из C++, ну и возможно придумать со стороны C++ некий тип, который выглядел бы как Qt-шный класс с массивом полей-функций, представляющих методы класса из .NET (для проекта по работе я написал некоторый type-safe аналог делегатам, базирующийся на динамике из Qt и template’ах, напишу о нём как-нибудь позже). Этот тип единственный из всего кода на C++ знал бы о существовании какого-то объекта в .NET, и делегировал ему все “сигналы” (“события” в терминах .NET) и вызовы методов из C++. Ну что ж, звучит красиво! Приступим!

Зная по опыту, что есть возможность писать на C++, используя одновременно платформу .NET и нативный код на C++, я решил сделать прототип, воплощающий мою идею. Основная идея сейчас это вызвать нативный код и вернуть результат в managed-код. Итак, в прототип входит:

  • Библиотека, написанная на чистом C++ с использованием Qt и скомпилированная в *.lib с помощью майкрософтовского компилятора (типа, наш “неприкасаемый” код C++, который ничего не знает об этих ваших .NET-ах).
  • Обёртка, склеивающая библиотеку с managed-кодом из .NET. Является проектом для C++/CLI, самая интересная часть. Видна со стороны .NET как обычная .NET-сборка, но может на полную мощность использовать нативный C++ и линковаться с нативными библиотеками.
  • “Клиент” на чистом .NET (я написал на C#), который референсит обёртку, и вызывает с помощью неё код из библиотеки.

Пишем код нашей C++-библиотеки, файл QtLibrary.h:

  1. class QTLIBRARY_EXPORT QtLibrary : public QObject
  2. {
  3.     Q_OBJECT
  4. public:
  5.     QtLibrary() { }
  6.     ~QtLibrary() { }
  7.  
  8.     int Sum(int arg1, int arg2);
  9. };

Файл QtLibrary.cpp:

  1. #include "QtLibrary.h"
  2.  
  3. int QtLibrary::Sum( int arg1, int arg2 )
  4. {
  5.     return arg1 + arg2;
  6. }

При этом не забываем пометить класс как экспортируемый, объявив в другом файле через макрос QTLIBRARY_EXPORT ключевое слово __declspec(dllexprort):

  1. #include <Qt/qglobal.h>
  2.  
  3. #ifdef QTLIBRARY_LIB
  4. # define QTLIBRARY_EXPORT __declspec(dllexport)
  5. #else
  6. # define QTLIBRARY_EXPORT __declspec(dllimport)
  7. #endif

Далее идём в свойства проекта и добавляем объявление QTLIBRARY_LIB (Properties - C/C++ – Preprocessor – Preprocessor Definitions). Этого не нужно делать, если библиотека импортирует класс, помеченный макросом QTLIBRARY_LIB, чтобы тот же самый хедер был обработан компилятором для получения __declspec(dllimport).

Также я поместил файл QtLibrary.h в отдельную папку /include, которая будет источником всех объявлений классов, которые являются “общими” для разных библиотек. Не забываем указать линкеру, куда складывать *.lib файлы: создаём папку /lib в корне папки проекта и указываем её (Properties – Linker – Advanced – Import Library). (Это классический способ “расшаривания” классов между библиотеками, и любой программист на C++ знает об этом, но я всё-таки написал подробно. :))

Приступаем к самому интересному — обёртке. Создаём проект на Visual C++, тип CLR/Class Library. Это означает, что проект будет виден как обычная .NET-сборка и может использовать как .NET, так и нативный C++. Файл Wrapper.h:

  1. using namespace System;
  2. class QtLibrary;
  3.  
  4. namespace CppCode
  5. {
  6.     public ref class CppWrapper
  7.     {
  8.     private:
  9.         QtLibrary* _cppClass;
  10.     public:
  11.         CppWrapper();
  12.         int SumFromCpp(int ar1, int ar2);
  13.     };
  14. }

Файл Wrapper.cpp:

  1. #include "stdafx.h"
  2. #include "Wrapper.h"
  3. #include <QtLibrary.h>
  4.  
  5. int CppCode::CppWrapper::SumFromCpp(int arg1, int arg2)
  6. {
  7.     return _cppClass->Sum(arg1, arg2);
  8. }
  9.  
  10. CppCode::CppWrapper::CppWrapper()
  11. {
  12.     _cppClass = new QtLibrary();
  13. }

Обратите внимание на #include <QtLibrary.h>: для того, чтобы этот файл указывать без лишних запутанных слешей, в свойствах проекта обёртки указываем созданную директорию /include как место, где надо искать хедеры (Properties – C/C++ – General – Additional Include Directories). В свойствах линкера указываем, откуда нам брать *.lib файлы для линковки с QtLibrary (Properties - Linker – General – Additional Library Directories). QTLIBRARY_LIB не объявляем нигде (ни в коде, ни в свойствах проекта), чтобы макрос QTLIBRARY_EXPORT работал в проекте обёртки как __declspec(dllimport).

Ну а теперь наконец-то клиент! Делаем обычную сборку под .NET (либо исполняемый файл, либо библиотеку). Я сделал консольное приложение, с ним меньше всего возни. Файл Program.cs:

  1. using System;
  2. using CppCode;
  3.  
  4. namespace NETApp
  5. {
  6.     class Program
  7.     {
  8.         static void Main(string[] args)
  9.         {
  10.             CppCode.CppWrapper wrapper = new CppCode.CppWrapper();
  11.             int result = wrapper.SumFromCpp(10, 20);
  12.             Console.WriteLine(result);
  13.         }
  14.     }
  15. }

В референсы добавляем сборку с обёрткой. Кстати, для простоты я указал для трёх сборок в качестве output-директории одну и ту же папку /out, лежащую в корне проекта.

Вот и всё! Результатом работы будет сумма 10 + 20 = 30. Как можно догадаться, таким же образом можно сделать .NET-обёртки для CUDA или других интересностей. Основной цели я добился, код вызвал, а это значит, что обёртку для C-шного front-endа можно сделать! Пойду думать, реализовать мою вторую идею насчёт класса-менеджера для связки Qt - .NET. :)

Комментариев нет:

Отправить комментарий