2009 г.
Remoting с сервером на Unmanaged C++ или Вторая жизнь старых приложений
Владимир Красавцев
Математик делает то, что можно, так, как нужно.
Программист делает то, что нужно, так, как можно.
Плакат в Галактика-ZOOM
Содержание
- Введение
- Формулировка задачи.
- Общий подход к решению.
- Общая архитектура приложения.
- Вопросы реализации.
- Старая бизнес логика.
- Remoting - объект
- Модули на С++/CLI в проекте С++
- Сервер Remoting
- Клиент Remoting
- Заключение
Введение
Формулировка задачи.
Предположим, имеется старое хорошее приложение на 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 - объект
Любой remoting-объект должен быть реализован на
управляемом коде, поэтому создадим в VS новый проект типа «CLR Class
library для С++» и назовем его, например, Rmt_obj.
В старой бизнес-логике рассматриваемого примера
имеется два метода, и для каждого из них в remoting-объекте
должны быть объявлены по одному
- delegate,
- event,
- методу, использующему событие (доступен 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.
Модули на С++/CLI в проекте С++
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
Теоретически, для получения сервера remoting из
старого приложения, реализованного на неуправляемом коде,
необходимо к старому приложению добавить код на C++/CLI, обеспечивающий
функционирование режима remoting, а именно создание, инициализацию и
регистрацию remoting-объекта. Инициализация объекта remoting в
рассматриваемом случае предполагает подключение старой бизнес-логики
для ее использования по технологии remoting.
Однако, наибольший интерес представляет практическая
реализация создания сервера remoting на основе приложения на родном
C++. На приведенном рисунке представлена блочная архитектура
создаваемого сервера remoting:
Таким образом, для создания remoting-сервера из
старого приложения, к его коду на родном C++ (блоки выделены серыми
тонами на рисунке) надо добавить три программных модуля на C++/CLI (h-
и cpp-файлы, отмеченные голубым цветом на картинке), которые должны
быть откомпилированы в управляемом режиме:
- Класс-обертка CMngCover для
вызова неуправляемых методов старой бизнес-логики через их управляемые аналоги
(h-файл и cpp-файл).
- Управляемый класс CRmtReg для
создания, инициализации и регистрации remoting-объекта (h-файл и cpp-файл).
- Стартовая функция StarterRmt (h-файл и
cpp-файл) для включения режима remoting. Эта функция на управляемом
коде будет вызываться из старого кода (реально это единственное
изменение, которое вносится непосредственно в старый код на родном C++).
При этом, как видно, старая бизнес-логика остается нетронутой.
Рассмотрим более подробно новые модули на управляемом коде.
Управляемый класс-обертка для неуправляемых методов
Управляемый класс-обертка для неуправляемых методов старой бизнес-логики необходим, чтобы объекты 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-сервер всегда
начинается с подключения 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-объекта, и, во-вторых, что повторная регистрация
remoting-объекта может привести к ошибке.
В модуль кода на родном C++, где она размещается, добавляется стандартный include
для h-файла стартовой функции:
#include "StarterRMT.h"
При непосредственном вызове стартовой функции в нее в
качестве параметра передается указатель на текущий объект
бизнес-логики, например:
void CMFC_2Dlg::OnBnClickedButton1()
{
// инициализация remoting
StarterRMT (this);
}
Следует, наверное, отметить, что упоминание в h-файле
стартовой функции модулей только на родном C++ дает возможность
не менять параметров компиляции модуля с точкой регистрации
remoting-объекта, то есть они остаются прежними для родного C++.
После внесения всех приведенных модернизаций в
старый проект на неуправляемом коде его можно собрать, и в
результате получится полноценный remoting-сервер (хост).
Клиент 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-приложений - они, по-моему, выходят
за рамки темы этой статьи и могут быть решены при конкретной
реализации.
Надеюсь, описанная технология сможет продлить жизнь еще не одному старому хорошему приложению.