Владимир Красавцев
Математик делает то, что можно, так, как нужно.
Программист делает то, что нужно, так, как можно.
Плакат в Галактика-ZOOM
Предположим, имеется старое хорошее приложение на C++ с исходными кодами. Вполне возможно, с пользовательским интерфейсом и являющееся COM-сервером (хотя все это и не обязательно). Естественно, это приложение реализовано на неуправляемом коде в виде исполняемого файла (ЕХЕ).
Очень хочется, не теряя его существующей функциональности и, по возможности, не перерабатывая старый код, обеспечить возможность использовать бизнес-логику этого приложения по технологии remoting.
Сформулированная задача имеет очень простое архитектурное решение: старое приложение на неуправляемом С++ превращается в сервер remoting, к нему добавляются объект и клиент remoting. И все.
Необходимо, правда, отметить, что старая бизнес-логика, которая должна быть доступна remoting-клиентам, реализована в исполняемом файле (EXE) на неуправляемом коде, а технология remoting полностью базируется платформе .NET, то есть использует управляемый код.
Таким образом, основную сложность представляет создание сервера remoting из приложения на unmanaged C++, чему и посвящена данная статья.
Любое remoting-приложение состоит из трех частей: объекта, клиента и сервера.
Объект remoting - это некоторая библиотека классов, унаследованных от MarshalByRefObject. Сама
библиотека реализуется на управляемом коде. Обычно в объекте remoting
располагается бизнес-логика, предоставляемая клиенту, однако в
рассматриваемом случае это не так. В рассматриваемом случае библиотека
классов будет использоваться только для организации вызовов из
remoting-клиента соответствующих методов бизнес-логики старого
приложения на C++, которое станет сервером remoting (хостом).
Вызов методов серверного приложения из объекта
remoting может осуществляться только с помощью технологии событий
(event), в которой используются callback функции (delegate).
Таким образом, в библиотеке классов (объекте remoting) для каждого
метода бизнес-логики старого приложения, который может вызываться по
технологии remoting, должны быть описаны соответствующие delegate и event.
Remoting-клиент в данной задаче является совершенно обычным (никаких особенностей в его архитектуре нет), он может быть реализован на любом языке .NET
Создание же сервера remoting из программы на неуправляемом (родном) C++, имеет ряд особенностей. Главной из них является то, что все программные блоки в составе любого сервера remoting, выполняющие активацию и регистрацию объекта remoting, должны быть написаны на управляемом коде. Поэтому, сервер remoting в рассматриваемом случае будет представлять собой приложение, состоящее из смеси управляемого и неуправляемого кода. То есть, в старое приложение на неуправляемом коде должны быть встроены программные блоки на управляемом C++, обеспечивающие функционирование remoting.
Технология создания любого remoting-приложения является довольно сложным многоходовым процессом, детальному описанию которого посвящено большое количество специализированной литературы. Встречается также литераутра, посвященная вопросам создания приложений со смешанным кодом. Однако, создание сервера remoting с использованием смешанного кода имеет ряд малодокументированных особенностей.
Поэтому вопросы практической реализации такого приложения представлены ниже в виде подробного описания технологии его создания. Предполагается, что для разработки используется MS Visual Studio (2003, 2005, 2008).
Описание процесса построения всех программных компонентов, необходимых для реализации рассматриваемой архитектуры, необходимо начать с анализа бизнес-логики старого приложения на родном С++. Ведь именно ее сохранение и обеспечение возможности ее использования по технологии remoting является главной целью рассматриваемой задачи.
Для простоты изложения выберем (или создадим) в качестве примера приложение на родном C++ с простейшей бизнес-логикой, которая, например, описывается так (h-файл) :
//Бизнес-логика, которая должна быть доступна по технологии remoting
class CMFC_2Dlg : public CDialog
{
public:
// Передача строки
void method_PutStr(const wchar_t* s);
// получение строки
wchar_t* method_GetStr();
};
В рамках сделанных предположений о структуре старой бизнес-логики будет вестись все дальнейшее изложение материала.
ПримечаниеВсе изложение материала ведется в предположении, что в рассматриаемой бизнес-логике старого приложения на родном С++ отсутствует работа с пользовательским графическим интерфейсом. Если это не так, то, возможно, потребуется некоторая переработка старой бизнес-логики для сохранения работоспособности графического интерфейса пользователя при доступе к нему по технологии remoting. Однако, обсуждение этого выходит за пределы тематики данной статьи.
Любой remoting-объект должен быть реализован на управляемом коде, поэтому создадим в VS новый проект типа «CLR Class library для С++» и назовем его, например, Rmt_obj.
В старой бизнес-логике рассматриваемого примера имеется два метода, и для каждого из них в remoting-объекте должны быть объявлены по одному
Кроме того, класс remoting-объекта должен быть наследником MarshalByRefObject, что указывается в его описании.
Таким образом, получается следующий код на C++/CLI для remoting-объекта рассматриваемого примера:
// Rmt_obj.h
//объект REMOTING
#pragma once
using namespace System;
namespace Rmt_obj {
public ref class CRmtngObj : MarshalByRefObject
{
public:
// Для метода method_PutStr.
delegate void dlg_method_PutStr(String^ str);
event dlg_method_PutStr^ ev_method_PutStr;
void mtd_method_PutStr(String^ str);
// Для метода method_GetStr.
delegate String^ dlg_method_GetStr();
event dlg_method_GetStr^ ev_method_GetStr;
String^ mtd_method_GetStr();
};
}
// Rmt_obj.cpp
//объект REMOTING
// This is the main DLL file.
#include "stdafx.h"
#using <mscorlib.dll>
#include "Rmt_obj.h"
namespace Rmt_obj {
//Для метода method_PutStr
void CRmtngObj::mtd_method_PutStr(String^ str)
{
ev_method_PutStr(str);
}
// Для метода method_GetStr
String^ CRmtngObj:: mtd_method_GetStr()
{
return ev_method_GetStr();
}
}
Приведенный код может быть скомпилирован в Rmt_Obj.dll - объект remoting.
VS поддерживает особое взаимодействие между родным C++ и C++/CLI в виде смешанного режима. Эта возможность и будет использована для превращения старого приложения на родном C++ в сервер remoting. Для того, чтобы в рассматриваемом случае не повредить код старого приложения на родном C++, удобно новый управляемый код C++/CLI, необходимый для функционирования remoting, включить в старый проект на родном C++ в виде отдельных файлов (h-файлы и cpp-файлы). И указать в свойствах этих cpp-файлов, что они должны быть откомпилированы в управляемом режиме. Чтобы включить этот режим компиляции, требуется навести курсор на имя нужного cpp-файла на C++/CLI в окне Solution Explorer в VS и, нажав правую кнопку мыши, выбрать Properties. В открывшемся окне полезно выполнить следующие типовые настройки:
|
Группа настроек |
Настройка |
Значение |
|
General |
Compile with CLR support |
/clr |
|
General |
Debug Information Format |
Program Database (/Zi) |
|
Code Generation |
Enable Minimal Rebuild |
No |
|
Code Generation |
Enable C++ Exception |
/EHa |
|
Code Generation |
Basic Runtime Checks |
Default |
|
Code Generation |
Struct Member Alignment |
Default (не повредит, особенно при странной ошибке error LNK2022) |
Можно перед выполнением настроек выбрать в Configuration режим “All Configuration”
Теоретически, для получения сервера remoting из старого приложения, реализованного на неуправляемом коде, необходимо к старому приложению добавить код на C++/CLI, обеспечивающий функционирование режима remoting, а именно создание, инициализацию и регистрацию remoting-объекта. Инициализация объекта remoting в рассматриваемом случае предполагает подключение старой бизнес-логики для ее использования по технологии remoting.
Однако, наибольший интерес представляет практическая реализация создания сервера remoting на основе приложения на родном C++. На приведенном рисунке представлена блочная архитектура создаваемого сервера remoting:
Таким образом, для создания remoting-сервера из старого приложения, к его коду на родном C++ (блоки выделены серыми тонами на рисунке) надо добавить три программных модуля на C++/CLI (h- и cpp-файлы, отмеченные голубым цветом на картинке), которые должны быть откомпилированы в управляемом режиме:
При этом, как видно, старая бизнес-логика остается нетронутой.
Рассмотрим более подробно новые модули на управляемом коде.
Управляемый класс-обертка для неуправляемых методов старой бизнес-логики необходим, чтобы объекты delegate из remoting-объекта могли вызывать неуправляемые методы бизнес-логики. Это связано с тем, что невозможно напрямую передать в delegate ссылку на неуправляемый метод.
Основная сложность написания такого управляемого класса-обертки связана с необходимостью корректного преобразования данных неуправляемых и управляемых типов.
Входным параметром при создании экземпляра
управляемого класса-обертки для неуправляемых методов является
указатель на существующий (неуправляемый) объект, которому
принадлежат методы бизнес-логики. Поэтому, в заголовочный файл
класса-обертки включаются #include описания старого класса бизнес-логики.
Таким образом, для его реализации в рамках рассматриваемого примера в состав старого проекта на родном C++ добавляются файлы MngCover.h и MngCover.cpp со следующим кодом на C++/CLI:
//MngCover.h
//Управляемый класс-обертка для неуправляемых методов
#if !defined(AFX_MNGCOVER__INCLUDED_)
#define AFX_MNGCOVER__INCLUDED_
#include "stdafx.h"
#include "MFC_2Dlg.h" //описание бизнес-логики
#include <string>
#using <mscorlib.dll>
using namespace System;
using namespace std;
public ref class CMngCover
{
CMFC_2Dlg* m_pDialog; //неуправляемый объект бизнес-логики
public:
//конструктор
CMngCover(CMFC_2Dlg* pDialog);
//обертка метода method_PutStr
void mng_method_PutStr(System::String^ str);
//обертка метода method_GetStr
String^ mng_method_GetStr();
};
#endif // defined(AFX_MNGCOVER__INCLUDED_)
// MngCover.cpp
// Управляемый класс-обертка для неуправляемых методов
#include "mngCover.h"
#include <vcclr.h>
#using <mscorlib.dll>
using namespace System;
using namespace std;
//конструктор
CMngCover::CMngCover(CMFC_2Dlg* pDialog):m_pDialog(pDialog){};
//обертка метода method_PutStr
void CMngCover::mng_method_PutStr(System::String^ str){
pin_ptr <const wchar_t> ptr = PtrToStringChars(str);
m_pDialog->method_PutStr(ptr);
};
//обертка метода method_GetStr
String^ CMngCover::mng_method_GetStr(){
String^ mm_s;
mm_s = gcnew String( m_pDialog->method_GetStr() );
return mm_s;
};
Класс CMngCover должен быть откомпилирован с поддержкой CLR, поэтому для него необходимо выполнить настройки, описанные в разделе «Модули на С++/CLI в проекте С++».
Превращение любого приложения в remoting-сервер всегда начинается с подключения remoting-объекта в качестве Reference. Это выполняется следующим образом: необходимо навести курсор на имя проекта будущего remoting-сервера в окне Solution Explorer в VS и, нажав правую кнопку мыши, выбрать Properties. В открывшемся окне нажать кнопку «Add New Reference» и в закладке «Browse» выбрать dll-файл remoting-объекта.
После этого можно приступить к созданию класса регистрации remoting-объекта. Для этого к старому проекту на родном C++ добавляются файлы Rmt_reg.h и Rmt_reg.cpp со следующим кодом на C++/CLI:
//Rmt_reg.h
//регистрация remoting-объекта
#include "stdafx.h"
#include "mngCover.h" // Управляемый класс-обертка для неуправляемых методов
#using <mscorlib.dll>
#using <System.Dll>
#using <System.Runtime.Remoting.Dll>
using namespace System;
using namespace System::Runtime;
using namespace System::Runtime::Remoting;
using namespace System::Runtime::Remoting::Channels;
using namespace System::Runtime::Remoting::Channels::Tcp;
using namespace Rmt_obj; //пространтво имен remoting-объекта
namespace Rmt_reg
{
public ref class CRmtReg
{
private:
TcpChannel^ m_chan;
public:
CRmtReg(CMngCover^ pMngCover); //конструктор
};
}
// Rmt_reg.cpp
//регистрация remoting-объекта
#include "Rmt_reg.h"
using namespace System;
using namespace System::Runtime;
using namespace System::Runtime::Remoting;
using namespace System::Runtime::Remoting::Channels;
using namespace System::Runtime::Remoting::Channels::Tcp;
namespace Rmt_reg
{
CRmtReg::CRmtReg(CMngCover^ pMngCover) //конструктор
{
m_chan = gcnew TcpChannel(8085); //создаем канал
ChannelServices::RegisterChannel(m_chan, false); //регистрируем
//описание переменной remoting-класса
CRmtngObj^ rmClass;
rmClass = gcnew CRmtngObj(); //создание remoting-класса
// регистрация remoting-класса
ObjRef^ refClass = RemotingServices::Marshal(rmClass, "RemoteTest");
// инициализация delegate для метода method_PutStr
rmClass->ev_method_PutStr +=
gcnew CRmtngObj:: dlg_method_PutStr(pMngCover,
&(CMngCover::mng_method_PutStr) );
// инициализация delegate для метода method_GetStr
rmClass-> ev_method_GetStr +=
gcnew CRmtngObj:: dlg_method_GetStr(pMngCover,
&(CMngCover::mng_method_GetStr) );
};
}
Класс регистрации remoting-объекта должен не только
его зарегистрировать, но и создав, выполнить его инициализацию.
Для этого используется уже созданный объект управляемого класса-обертки
неуправляемых методов, поэтому в заголовочный файл «Класса регистрации
remoting-объекта» добавлен #include на описание класса-обертки.
Класс регистрации remoting-объекта реализуется на управляемом коде (порядок включения режима компиляции с поддержкой CLR описан выше в разделе «Модули на С++/CLI в проекте С++»).
Основной задачей стартовой функции является запуск процесса регистрации remoting-объекта.
Стартовая функция реализуется на управляемом коде в
рамках старого проекта, но она должна вызываться из неуправляемого кода
- из точки регистрации remoting-объекта. Чтобы не нарушать старый
проект, код стартовой функции разместим в отдельном модуле (файлы
StarterRmt.h и StarterRmt.cpp). В h-файле StarterRmt.h будут
присутствовать #include только
неуправляемых модулей (описание класса методов бизнес-логики из старого
приложения на неуправляемом коде), а управляемые модули (h-файлы
управляемого класса-обертки и «Класса регистрации
remoting-объекта») будут подключены уже в cpp-файле. В итоге код
стартовой функции на С++/CLI в рассматриваемом примере будет иметь
такой вид:
// StarterRMT.h // стартовая функция #include "MFC_2Dlg.h" //описание бизнес-логики //Стартовая функция. Входной параметр - существующий объект бизнес-логики void StarterRMT(CMFC_2Dlg* pDialog);
//StarterRMT.cpp
// стартовая функция
#include "StarterRMT.h"
#include "mngCover.h" //Управляемый класс-обертка для неуправляемых методов
#include "Rmt_reg.h" //Класс регистрации remoting-объекта
//Стартовая функция. Входной параметр - существующий объект бизнес-логики
void StarterRMT (CMFC_2Dlg* pDialog)
{
//Управляемый класс-обертка для неуправляемых методов
CMngCover^ mm_MngCover; //описание
mm_MngCover = gcnew CMngCover(pDialog); //создание
//класс регистрации и иницилизации remoting-объекта
Rmt_reg::CRmtReg^ mm_RmtReg; //описание
mm_RmtReg = gcnew Rmt_reg::CRmtReg(mm_MngCover); //создание и регистрация
}
Как было уже сказано, модуль стартовой функции собирается в режиме управляемого кода поэтому для него необходимо выполнить настройки, приведенные в разделе «Модули на С++/CLI в проекте С++», кроме того, для него может потребоваться отключить использование прикомпилированных заголовков.
Регистрация remoting-объекта осуществляется при вызове стартовой функции из некоторого места старого кода - точки регистрации. Эта точка регистрации выбирается (добавляется) в неуправляемом коде старого приложения. При ее выборе необходимо учитывать, во-первых, что remoting-взаимодействие возможно только после регистрации remoting-объекта, и, во-вторых, что повторная регистрация remoting-объекта может привести к ошибке.
В модуль кода на родном C++, где она размещается, добавляется стандартный include для h-файла стартовой функции:
#include "StarterRMT.h"
При непосредственном вызове стартовой функции в нее в качестве параметра передается указатель на текущий объект бизнес-логики, например:
void CMFC_2Dlg::OnBnClickedButton1()
{
// инициализация remoting
StarterRMT (this);
}
Следует, наверное, отметить, что упоминание в h-файле стартовой функции модулей только на родном C++ дает возможность не менять параметров компиляции модуля с точкой регистрации remoting-объекта, то есть они остаются прежними для родного C++.
После внесения всех приведенных модернизаций в старый проект на неуправляемом коде его можно собрать, и в результате получится полноценный remoting-сервер (хост).
Никаких особенностей при создании клиента remoting для рассматриваемого примера нет. В качестве примера приведен код тривиального клиента на C# в виде консольного приложения
//ClientRemoting.cs
//Клиент remoting
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Channels.Tcp;
using Rmt_obj;
namespace ClientRemoting
{
class Program
{
static void Main(string[] args)
{
CRmtngObj m_class;
// регистрация TCP-канала
ChannelServices.RegisterChannel(new TcpChannel(),false);
m_class=(CRmtngObj)Activator.GetObject(
typeof(Rmt_obj.CRmtngObj), "tcp://localhost:8085/RemoteTest");
// Вызов старого метода method_GetStr
Console.WriteLine(m_class.mtd_method_GetStr());
Console.ReadLine();
// Вызов старого метода method_PutStr
m_class. mtd_method_PutStr("POIUYTR");
// Вызов старого метода method_GetStr
Console.WriteLine(m_class. mtd_method_GetStr());
Console.ReadLine();
}
}
}
В приведенном алгоритме создания сервера remoting из приложения на неуправляемом C++ не затронуты многие вопросы, обычно обсуждаемые при описании remoting-приложений - они, по-моему, выходят за рамки темы этой статьи и могут быть решены при конкретной реализации.
Надеюсь, описанная технология сможет продлить жизнь еще не одному старому хорошему приложению.