Вызов управляемого кода из неуправляемого

image С задачей вызова неуправляемого кода из управляемого мы сталкиваемся довольно часто, и эта задача имеет простое решение в виде одного атрибута [DllImport] и небольшого набора дополнительных правил, которые хорошо изложены в MSDN. Обратная же задача встречается гораздо реже. В данной статье мы и рассмотрим небольшой пример, как это можно сделать. Его не стоит рассматривать как исчерпывающий, скорее лишь, как направление хода мыслей и концепцию. Итак, начнем.


Наш пример будет состоять из трех проектов:
  1. MixedLibrary — C++/CLI
  2. SimpleLibrary — C#
  3. Win32App — C++

image

Начнем с самого простого — SimpleLibrary. Эта библиотека содержит один простой сервис, который складывает два числа и выводит результат в консоль:

public class Service
    {
        public void Add(Int32 a, Int32 b)
        {
            Console.WriteLine("Hello from Managed Code!");
            Console.WriteLine(String.Format("Result: {0}", a + b));
        }
    }


Теперь перейдем к библиотеке MixedLibrary. Эта библиотека содержит в себе класс-обертку над нашим SimpleService. Содержимое файла CppService.h:

// Директивы препроцессора нужны, чтобы компилятор сгенерировал записи
// об экспорте класса из библиотеки
#ifdef INSIDE_MANAGED_CODE
#    define DECLSPECIFIER __declspec(dllexport)
#    define EXPIMP_TEMPLATE
#else
#    define DECLSPECIFIER __declspec(dllimport)
#    define EXPIMP_TEMPLATE extern
#endif


namespace MixedLibrary
{

	class DECLSPECIFIER CppService
	{
	public:
		CppService();
		virtual ~CppService();

	public:
		void Add(int a, int b);

	private:
		void * m_impl;
	};
}

И содержимое файла CppService.cpp:

#include "CppService.h"

using namespace System;
using namespace System::Runtime::InteropServices;
using namespace SimpleLibrary;

namespace MixedLibrary
{
	CppService::CppService()
	{
		Service^ service = gcnew Service();
		m_impl = GCHandle::ToIntPtr(GCHandle::Alloc(service)).ToPointer();
	}

	CppService::~CppService()
	{
		GCHandle handle = GCHandle::FromIntPtr(IntPtr(m_impl));
		handle.Free();
	}

	void CppService::Add(int a, int b)
	{
		GCHandle handle = GCHandle::FromIntPtr(IntPtr(m_impl));
		Service^ service = safe_cast<Service^>(handle.Target);
		service->Add(a, b);
	}
}


Также для компилируемости необходимо добавить директиву препроцессора INSIDE_MANAGED_CODE:

image

И последний штрих — наше обычное неуправляемое приложение:

#include "stdafx.h"

#pragma comment(lib, "../Debug/MixedLibrary.lib")

#include <iostream>
#include "../MixedLibrary/CppService.h"


using namespace std;
using namespace MixedLibrary;


int main()
{
	CppService* service = new CppService();
	service->Add(5, 6);

	cout << "press any key..." << endl;
	getchar();
}

И, конечно же, результат:

image

Автор: nikitam
ICL Services
Цифровые технологии для бизнеса

Комментарии 44

    –4
    У меня от фразы «неуправляемое приложение» мурашки.
      +1

      Аналогично. Managed и unmanaged гораздо понятнее. А если русифицировать, тогда уж вместо


      Также для компилируемости необходимо добавить директиву препроцессора INSIDE_MANAGED_CODE:

      Так же для для преобразования в машинный нулёво-еденичковый код необходимо добавить указание преобработчика В_УПРАВЛЯЕМОМ_КОДЕ.

        +1
        Именно это я и хотел сказать.
      +1
      А можно поинтересоваться, а где удаляется вот этот объект?
      CppService* service = new CppService();
      
        +2
        А всего-то нужно было его там сконструировать на стеке.
          –2
          В данном примере явно нигде. Но это всего лишь пример, а не рабочее приложение. При освобождении ссылку на управляемый объект также освобождаем:
          CppService::~CppService()
          {
          GCHandle handle = GCHandle::FromIntPtr(IntPtr(m_impl));
          handle.Free();
          }
          0
          Вот этот кусок
          // Директивы препроцессора нужны, чтобы компилятор сгенерировал записи
          // об экспорте класса из библиотеки
          #ifdef INSIDE_MANAGED_CODE
          #    define DECLSPECIFIER __declspec(dllexport)
          #    define EXPIMP_TEMPLATE
          #else
          #    define DECLSPECIFIER __declspec(dllimport)
          #    define EXPIMP_TEMPLATE extern
          #endif
          

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

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

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

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

                0
                Поднимаем .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;
                	}
                }

                  0

                  Да я знаю, искал когда-то. .NET машину поднять несложно. Надо ещё и DLL заинжектить. Но не суть важно. Просто пример привёл, какой минимальной сложности вопросы должны подниматься на Хабре (сугубо моё мнение), чтобы не понижать планку ресурса.

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

                      Да, и это тоже, кстати. Спасибо!

                +2

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


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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

                        0

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

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

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


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


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

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

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

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

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

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

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

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое