Pull to refresh

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

выглядит не слишком кроссплатформенно

О какой кроссплатформенности может идти речь, когда один из проектов написан на c++/cli?

Честно говоря, напоминает студенческое упражнение (даже на лабу не тянет). И ради этого писать статью? Ладно, если бы .NET движок поднять в соседнем процессе, который сугубо нативный, и в нем запустить какой-то код.
При написании статьи никто и не претендовал на диссертацию! Это просто отправная точка одного из подходов (о чем и сказано в начале), и зачастую именно простейших примеров и не хватает людям, которые только начали разбираться

Кто начал разбираться, мог бы погуглить. Подобная статья уже была на Хабре в 2011-м: https://habrahabr.ru/post/130690/ Тогда она была более своевременной, хоть и написана более сложным языком. Если есть цель сделать хорошую обучающую статью (это хорошая цель), то здесь объём всё же маловат для целой статьи. Если это отправная точка подхода, можно было и про подход развернуть. Опять же, сугубо моё мнение.

Поднимаем .net хост в с++ коде
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 заинжектить. Но не суть важно. Просто пример привёл, какой минимальной сложности вопросы должны подниматься на Хабре (сугубо моё мнение), чтобы не понижать планку ресурса.

В приведенном примере как раз есть и загрузка сборки управляемой и вызов метода. К этому еще стоит заранее выполнять проверку какие версии .net установлены, чтобы подгрузить правильную.

Пара советов из моего опыта поддержки большого смешанного проекта:


  • в области C++/CLI надо находиться как можно меньше, слой совместимости между нативным и управляемым кодом должен быть как можно тоньше
  • если объектная система или процесс взаимодействия не тривиален, то COM — оправданный выбор для организации взаимодействия. Хотя я бы воздержался от автоматической активации через реестр или манифесты, они приносят гораздо больше проблем, чем кажущейся пользы.
  • вопрос управления ресурсами должен быть обязательно продуман, чтобы большие куски памяти или того хуже соединения не зависали непонятно где.
вопрос управления ресурсами должен быть обязательно продуман, чтобы большие куски памяти или того хуже соединения не зависали непонятно где
Ох… легче сказать чем сделать. Тут, думаю стоит сказать о финализаторах, о которых в статье, почему-то, тактично умалчивается. А без них в, худо-бедно, большом проекте тяжело обойтись.
Случай из практики на С++: объект, который получает в качестве ссылки группу других объектов (наподобие паттерна «стратегия»). Группа параметризующих объектов, сама, обладает богатым интерфейсом. Необходимо: обернуть в Managed и дать возможность работать с объектами по такой же схеме.
В реальности, получается вызов из С++ кода обертки на .Net, которая передается в обертку на .Net и вызывает опять С++. Поддерживать такое это ад и боль, я вам скажу.

На финализаторы надеяться нельзя: они больше подушка безопасности.
У меня был случай, когда приходилось лезть с помощью reflection внутрь объекта, чтобы высвободить большой Bitmap, который не освобождался, хотя верхние объекты поддерживали IDispose. Сборщик мусора не видел проблем, так как его объекты занимали немного места, а вот нативной памяти уже не хватало.

В С++ привыкаешь к детерминированному освобождению ресурсов (не памяти). При соприкосновении с миром .Net начинаются проблемы, так как GC считает что умнее тебя и отказывается их освобождать.
На практике, это приводит к проблемам такого рода: заказчик жалуется, что ваш код обернутый в .Net, «почему-то», требует в 3 раза больше ресурсов чем С++.

А освобождение ресурсов — это, внезапно, и не задача GC. Для этого придуман интерфейс IDisposable и конструкция using.


Да, в C++ действительно быстро привыкаешь к автоматическим вызовам деструкторов и в C# писать эти using поначалу тяжело и многословно. Но это больше вопрос привычки. Вон, в go из языка вообще всю магию выкинули к чертям — и ничего, люди пишут и почему-то даже радуются.

А освобождение ресурсов — это, внезапно, и не задача GC. Для этого придуман интерфейс IDisposable и конструкция using

Вот и получается ручное управление (т.е. нужно не забыть вызвать Dispose) от которого в С++ давно уже все от казались в пользу RAII.

Чем using — не автоматический?

Ну хотя бы тем, что нужно знать что он нужен при использовании того, или иного объекта. Using выносит часть зоны ответственности класса наружу, а это всегда плохо. А как быть, если он не был нужен, а потом вдруг стал нужен, в процессе доработки класса? Что делать с иерархией родительских объектов, добавлять им всем Dispose? В C++ деструктор полностью занимается зачисткой ресурсов объекта и у пользователя класса нет никакой головной боли.

С каких пор освобождение ресурса оказалось ответственностью самого ресурса?

По-моему, я написал что объект ответственен за освобождение своих ресурсов. Разве не так?

Нет, вы написали что using выносит часть зоны ответственности класса наружу.

В С++ класс ответственен за освобождение ресурсов которые он использует. С чем вы не согласны?
В .Net получается что класс ответственен только за память, а за ресурсы ответственен внешний код через using. По-моему, нарушение инкапсуляции на лицо.

В С# класс точно так же обычно ответственен за освобождение тех ресурсов, которые он использует, не вижу отличий.


А для внешнего кода используемым управляемым ресурсов является уже этот класс, его-то using и освобождает. Не вижу в этом никакого нарушения инкапсуляции.

Я немного не об этом. Под освобождение ресурсов, я имел ввиду детерминированное освобождение. Ясно что в .Net ресурсы освободятся, но когда…
Using управляет именно детерминированным освобождением ресурсов, что так часто требуется при связке С++ c .Net, и именно тут это управление осуществляется в ручном режиме. В противном случае код на С++ быстро выходит за рамки установленных заказчиком характеристик потребления ресурсов.

А зачем вы используете его в ручном режиме? Не вижу проблем использовать RAII в С++ в связке с .Net.

Ох… Ну наконец-то вы согласились что, в общем случае, аспекты связанные с .Net Dispose в связке с С++ могут быть значимы и их следовало бы рассмотреть. Я к тому, что хороший пример не должен быть настолько тривиальным и оторванным от жизни.
В конце концов, сложить а+b я и в C++ могу и мне не нужен для этого .Net

Технически вызов Dispose ничем не отличается от вызова метода Add.

Ну если так упрощать, то не отличается. Но тогда и смысл статьи ускользает.

В .Net ресурсы могут и никогда не освободиться, если сборщик мусора не успел отработать до завершения программы.
Мне в своё время для анализа освобождения ресурсов очень помогло осознание того факта, что сборщик мусора, который ничего не собирает, а освобождает память по завершению процесса — тоже удовлетворяет предъявляемым к GC требованиям.
Поэтому на финализаторы надеяться нельзя, освобождение должно быть детерминированным.
А RAII и using это фактически один и тот же приём: связывание области видимости с временем жизни ресурса, только в C++ область задаётся неявно, а в C# — явно.

О финализаторах в статье умалчивается потому что взаимодействие идет в другую сторону: неуправляемый ресурс владеет управляемым. В такой ситуации никакие финализаторы не нужны.

Т.е. в .Net они нужны, а при вызове .Net из С++ становятся, вдруг, не нужны? Я вас правильно понял?

Они и в .Net не нужны пока класс не владеет никакими неуправляемыми ресурсами.


К какому из классов в этом примере вы собрались добавлять финализатор и что он будет делать?


Управляемый класс тут ровно 1 — Service, и ресурсами он не владеет. Предлагаете добавить пустой финализатор "чтоб был"?

Если ограничиться примером, то и обсуждать будет нечего. Ценность примера стремиться к нулю. А хотелось бы обсудить особенности взаимодействия C++ с .Net и сложности которые при этом возникают.

Вот как раз при одностороннем взаимодействии C++ с .Net никаких сложностей кроме рассмотренных тут не возникает.

Я очень раз за вас, но давайте не будет столь категоричны. Если объекту нужен Dispose, то вам придется его вызвать из С++.

И в чем же проблема вызвать метод Dispose из деструктора?

Проблем нет, есть проблема в примере, в котором не описан это случай.

Порекомендую книгу Adam Nathan — .NET and COM: The Complete Interoperability Guide.
Эта книга практически полностью описывает тему взаимодействия нативного и управляемого кода, если там чего-то нет, то скорее всего этого нельзя сделать.

Sign up to leave a comment.