Сегодня неважно себя чувствовал, и решил из-за этого вместо активной работы спокойно покопаться со связкой .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:
- class QTLIBRARY_EXPORT QtLibrary : public QObject
- {
- Q_OBJECT
- public:
- QtLibrary() { }
- ~QtLibrary() { }
-
- int Sum(int arg1, int arg2);
- };
Файл QtLibrary.cpp:
- #include "QtLibrary.h"
-
- int QtLibrary::Sum( int arg1, int arg2 )
- {
- return arg1 + arg2;
- }
При этом не забываем пометить класс как экспортируемый, объявив в другом файле через макрос QTLIBRARY_EXPORT ключевое слово __declspec(dllexprort):
- #include <Qt/qglobal.h>
-
- #ifdef QTLIBRARY_LIB
- # define QTLIBRARY_EXPORT __declspec(dllexport)
- #else
- # define QTLIBRARY_EXPORT __declspec(dllimport)
- #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:
- using namespace System;
- class QtLibrary;
-
- namespace CppCode
- {
- public ref class CppWrapper
- {
- private:
- QtLibrary* _cppClass;
- public:
- CppWrapper();
- int SumFromCpp(int ar1, int ar2);
- };
- }
Файл Wrapper.cpp:
- #include "stdafx.h"
- #include "Wrapper.h"
- #include <QtLibrary.h>
-
- int CppCode::CppWrapper::SumFromCpp(int arg1, int arg2)
- {
- return _cppClass->Sum(arg1, arg2);
- }
-
- CppCode::CppWrapper::CppWrapper()
- {
- _cppClass = new QtLibrary();
- }
Обратите внимание на #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:
- using System;
- using CppCode;
-
- namespace NETApp
- {
- class Program
- {
- static void Main(string[] args)
- {
- CppCode.CppWrapper wrapper = new CppCode.CppWrapper();
- int result = wrapper.SumFromCpp(10, 20);
- Console.WriteLine(result);
- }
- }
- }
В референсы добавляем сборку с обёрткой. Кстати, для простоты я указал для трёх сборок в качестве output-директории одну и ту же папку /out, лежащую в корне проекта.
Вот и всё! Результатом работы будет сумма 10 + 20 = 30. Как можно догадаться, таким же образом можно сделать .NET-обёртки для CUDA или других интересностей. Основной цели я добился, код вызвал, а это значит, что обёртку для C-шного front-endа можно сделать! Пойду думать, реализовать мою вторую идею насчёт класса-менеджера для связки Qt - .NET. :)