Comments 44
Аналогично. Managed и unmanaged гораздо понятнее. А если русифицировать, тогда уж вместо
Также для компилируемости необходимо добавить директиву препроцессора INSIDE_MANAGED_CODE:
Так же для для преобразования в машинный нулёво-еденичковый код необходимо добавить указание преобработчика В_УПРАВЛЯЕМОМ_КОДЕ.
CppService* service = new CppService();
CppService::~CppService()
{
GCHandle handle = GCHandle::FromIntPtr(IntPtr(m_impl));
handle.Free();
}
// Директивы препроцессора нужны, чтобы компилятор сгенерировал записи
// об экспорте класса из библиотеки
#ifdef INSIDE_MANAGED_CODE
# define DECLSPECIFIER __declspec(dllexport)
# define EXPIMP_TEMPLATE
#else
# define DECLSPECIFIER __declspec(dllimport)
# define EXPIMP_TEMPLATE extern
#endif
выглядит не слишком кроссплатформенно
Кто начал разбираться, мог бы погуглить. Подобная статья уже была на Хабре в 2011-м: https://habrahabr.ru/post/130690/ Тогда она была более своевременной, хоть и написана более сложным языком. Если есть цель сделать хорошую обучающую статью (это хорошая цель), то здесь объём всё же маловат для целой статьи. Если это отправная точка подхода, можно было и про подход развернуть. Опять же, сугубо моё мнение.
class DotNetHostDispatcher
{
private:
ICLRMetaHost *pMetaHost;
ICLRRuntimeInfo *pRuntimeInfo;
ICLRRuntimeHost *pClrRuntimeHost;
PCWSTR pszStaticMethodName;
DWORD dwLengthRet;
public:
HRESULT hr;
DotNetHostDispatcher(PCWSTR pszVersion);
void StartMain(PCWSTR pszAssemblyPath, PCWSTR pszClassName, PCWSTR pszStringArg);
~DotNetHostDispatcher();
};
DotNetHostDispatcher::DotNetHostDispatcher(PCWSTR pszVersion)
{
pMetaHost = NULL;
pRuntimeInfo = NULL;
pClrRuntimeHost = NULL;
pszStaticMethodName = L"Main";
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
if (FAILED(hr))
throw L"CLRCreateInstance failed";
hr = pMetaHost->GetRuntime(pszVersion, IID_PPV_ARGS(&pRuntimeInfo));
if (FAILED(hr))
throw L"ICLRMetaHost::GetRuntime failed";
BOOL fLoadable;
hr = pRuntimeInfo->IsLoadable(&fLoadable);
if (FAILED(hr))
throw L"ICLRRuntimeInfo::IsLoadable failed";
if (!fLoadable)
throw L".NET runtime cannot be loaded";
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&pClrRuntimeHost));
if (FAILED(hr))
throw L"ICLRRuntimeInfo::GetInterface failed";
// Start the CLR.
hr = pClrRuntimeHost->Start();
if (FAILED(hr))
throw L"CLR failed to start";
}
void DotNetHostDispatcher::StartMain(PCWSTR pszAssemblyPath, PCWSTR pszClassName, PCWSTR pszStringArg)
{
hr = pClrRuntimeHost->
ExecuteInDefaultAppDomain(pszAssemblyPath, pszClassName,
pszStaticMethodName, pszStringArg, &dwLengthRet);
}
DotNetHostDispatcher::~DotNetHostDispatcher()
{
if (pMetaHost)
{
pMetaHost->Release();
pMetaHost = NULL;
}
if (pRuntimeInfo)
{
pRuntimeInfo->Release();
pRuntimeInfo = NULL;
}
if (pClrRuntimeHost)
{
pClrRuntimeHost->Release();
pClrRuntimeHost = NULL;
}
}
Да я знаю, искал когда-то. .NET машину поднять несложно. Надо ещё и DLL заинжектить. Но не суть важно. Просто пример привёл, какой минимальной сложности вопросы должны подниматься на Хабре (сугубо моё мнение), чтобы не понижать планку ресурса.
Пара советов из моего опыта поддержки большого смешанного проекта:
- в области C++/CLI надо находиться как можно меньше, слой совместимости между нативным и управляемым кодом должен быть как можно тоньше
- если объектная система или процесс взаимодействия не тривиален, то COM — оправданный выбор для организации взаимодействия. Хотя я бы воздержался от автоматической активации через реестр или манифесты, они приносят гораздо больше проблем, чем кажущейся пользы.
- вопрос управления ресурсами должен быть обязательно продуман, чтобы большие куски памяти или того хуже соединения не зависали непонятно где.
вопрос управления ресурсами должен быть обязательно продуман, чтобы большие куски памяти или того хуже соединения не зависали непонятно гдеОх… легче сказать чем сделать. Тут, думаю стоит сказать о финализаторах, о которых в статье, почему-то, тактично умалчивается. А без них в, худо-бедно, большом проекте тяжело обойтись.
Случай из практики на С++: объект, который получает в качестве ссылки группу других объектов (наподобие паттерна «стратегия»). Группа параметризующих объектов, сама, обладает богатым интерфейсом. Необходимо: обернуть в Managed и дать возможность работать с объектами по такой же схеме.
В реальности, получается вызов из С++ кода обертки на .Net, которая передается в обертку на .Net и вызывает опять С++. Поддерживать такое это ад и боль, я вам скажу.
На финализаторы надеяться нельзя: они больше подушка безопасности.
У меня был случай, когда приходилось лезть с помощью reflection внутрь объекта, чтобы высвободить большой Bitmap, который не освобождался, хотя верхние объекты поддерживали IDispose. Сборщик мусора не видел проблем, так как его объекты занимали немного места, а вот нативной памяти уже не хватало.
На практике, это приводит к проблемам такого рода: заказчик жалуется, что ваш код обернутый в .Net, «почему-то», требует в 3 раза больше ресурсов чем С++.
А освобождение ресурсов — это, внезапно, и не задача GC. Для этого придуман интерфейс IDisposable и конструкция using.
Да, в C++ действительно быстро привыкаешь к автоматическим вызовам деструкторов и в C# писать эти using поначалу тяжело и многословно. Но это больше вопрос привычки. Вон, в go из языка вообще всю магию выкинули к чертям — и ничего, люди пишут и почему-то даже радуются.
А освобождение ресурсов — это, внезапно, и не задача GC. Для этого придуман интерфейс IDisposable и конструкция using
Вот и получается ручное управление (т.е. нужно не забыть вызвать Dispose) от которого в С++ давно уже все от казались в пользу RAII.
Чем using — не автоматический?
С каких пор освобождение ресурса оказалось ответственностью самого ресурса?
Нет, вы написали что using выносит часть зоны ответственности класса наружу.
В .Net получается что класс ответственен только за память, а за ресурсы ответственен внешний код через using. По-моему, нарушение инкапсуляции на лицо.
В С# класс точно так же обычно ответственен за освобождение тех ресурсов, которые он использует, не вижу отличий.
А для внешнего кода используемым управляемым ресурсов является уже этот класс, его-то using и освобождает. Не вижу в этом никакого нарушения инкапсуляции.
Using управляет именно детерминированным освобождением ресурсов, что так часто требуется при связке С++ c .Net, и именно тут это управление осуществляется в ручном режиме. В противном случае код на С++ быстро выходит за рамки установленных заказчиком характеристик потребления ресурсов.
А зачем вы используете его в ручном режиме? Не вижу проблем использовать RAII в С++ в связке с .Net.
В конце концов, сложить а+b я и в C++ могу и мне не нужен для этого .Net
В .Net ресурсы могут и никогда не освободиться, если сборщик мусора не успел отработать до завершения программы.
Мне в своё время для анализа освобождения ресурсов очень помогло осознание того факта, что сборщик мусора, который ничего не собирает, а освобождает память по завершению процесса — тоже удовлетворяет предъявляемым к GC требованиям.
Поэтому на финализаторы надеяться нельзя, освобождение должно быть детерминированным.
А RAII и using это фактически один и тот же приём: связывание области видимости с временем жизни ресурса, только в C++ область задаётся неявно, а в C# — явно.
О финализаторах в статье умалчивается потому что взаимодействие идет в другую сторону: неуправляемый ресурс владеет управляемым. В такой ситуации никакие финализаторы не нужны.
Они и в .Net не нужны пока класс не владеет никакими неуправляемыми ресурсами.
К какому из классов в этом примере вы собрались добавлять финализатор и что он будет делать?
Управляемый класс тут ровно 1 — Service, и ресурсами он не владеет. Предлагаете добавить пустой финализатор "чтоб был"?
Порекомендую книгу Adam Nathan — .NET and COM: The Complete Interoperability Guide.
Эта книга практически полностью описывает тему взаимодействия нативного и управляемого кода, если там чего-то нет, то скорее всего этого нельзя сделать.
Вызов управляемого кода из неуправляемого