Минимализм удаленного взаимодействия на C++11

cfd9b32dd5350c57b7cb916b84822a56.jpg Некоторое время назад мной был опубликован пост о создании собственной системы плагинов на C++11 [1]. Где было рассказано о плагинах в рамках одного процесса. Есть желание дать возможность плагинам не зависеть от процесса и компьютера, на котором они исполняются. Сделать межпроцессное и удаленное взаимодействие. Для этого надо решить пару задач: создать наборы Proxy/Stub и предоставить транспорт для обмена данными между клиентом и сервером.Во многих библиотеках, предназначенных для организации удаленного взаимодействия предлагается некоторый скрипт или утилита для генерации Proxy/Stub из некоторого описания интерфейса. Так, например для libevent [2] при использовании ее части, связанной с RPC, есть скрипт event_rpcgen.py, который из С-подобного описания структур генерирует код для Proxy/Stub, а транспортом уже служит другая часть libevent. Так же gSOAP [3] предоставляет утилиту генерации кода из C++-подобного описания структур данных или из WSDL-описания и имеет свой встроенный транспорт, который можно использовать. gSOAP хороший и интересный продукт и в нем применение утилиты автогенерации кода оправдано, т. к. из C++-подобного описания можно сгенерировать WSDL-описание, которое уже может быть использовано при работе с Web-сервисами из других языков программирования.

Можно найти еще несколько примеров библиотек для построения клиент-серверного взаимодействия. Многие из них будут предлагать использовать те или иные механизмы генерации Proxy/Stub и свой встроенный транспорт.

Как можно взять за основу любой известный транспорт и отказаться от утилит генерации кода Proxy/Stub, возложив эту задачу на компилятор и воспользоваться преимуществами C++11 для создания объектного интерфейса удаленного взаимодействия с минимальными трудозатратами его использования? О принципах и реализации в виде законченного проекта, который может быть использован как библиотека при разработке своего клиент-серверного приложения изложены ниже.Пост получился не из разряда самых коротких. Его можно читать с любого интересующего Вас раздела. В зависимости от того, что является первостепенным интересом: технические детали реализации или возможность использования и примеры.

Разделы:

Введение Как уже было отмечено выше, появление поста было вызвано желанием «расселить» плагины одного процесса по разным и по возможности по разным компьютерам. Но пост не о доделках системы плагинов, а скорее о ее побочном полноценном и независимом от нее продукте, который в последствии и ляжет в ее основу реализации удаленного взаимодействия плагинов. А пока это всего лишь немного упрощенная реализация, позволяющая строить клиент-серверные приложения. В качестве интерфейса для такого взаимодействия выбран интерфейс в стиле C++ (структура с чисто виртуальными методами). Как из такого интерфейса получить набор Proxy/Stub’ов я уже ранее писал [4]. Это был пост с реализацией на C++03 и был своего рода идеально сферическим конем из абсолютно черной материи, т.е. не имел завершенной практической реализации, которую можно было бы взять и, что называется, «прямо из коробки» попробовать использовать. В этом же посте будет дана полная реализация с преимуществами, которые были получены благодаря стандарту C++11, а так же все можно будет протестировать «из коробки» на реальном тестовом сервере.Реализация af49a08a5b7d13ddf506f8799cb7396c.jpg В основу реализации легли материалы уже опубликованные мною ранее: пост о реализации Proxy/Stub’ов [4] и пост о создании своего http-сервера на базе libevent [5].Что использовать в качестве транспорта не важно, и при желании его можно легко заменить, реализовав пару простых интерфейсов. Мною был выбран http встроенный функционал libevent, так как он мне показался простым и не затратным в использовании и уже неплохо себя зарекомендовавшим.

Для формирования пакетов данных, которыми обмениваются клиент и сервер была выбрана простая библиотека rapidxml [6], которая меня привлекла тем, что она поставляется в виде набора только включаемых файлов, что для меня дает возможность более простого ее внедрения и распространения исходного кода на ее основе, а так же она показывает хорошие результаты производительности, что при работе с xml так же играет не последнюю роль. Так же как и транспорт предлагаемая реализация удаленного взаимодействия может легко переключиться на иной формат/протокол сериализации и десериализации данных при желании пользователя. Для этого нужно реализовать два класса с заданным интерфейсом.

Вся реализация построена с использованием шаблонов и немного макросов. Мда… Говорят, что макросы — это зло, а шаблоны — это сложно и непонятно. Получается сложное и непонятное зло. Так ли это? Нет. Макросы в небольшом их количестве полезны, т. к. иногда не все возможно выразить только средствами языка, а для достижения результата нужно прибегнуть к препроцессору. В свою же очередь шаблоны дают обобщение и сокращение кода. Получается сдвиг от сложно-непонятного зла в сторону полезного сокращения и обобщения кода.

Будучи не раз обруганным многоэтажным матом компилятора его длинных сообщений об ошибках в шаблонном коде да еще и под макросами, мне удалось достичь минимализма, примеры которого я приведу перед тем как погрузиться в детали реализации, чтобы было видно к какому результату будет стремление в описании реализации.

Пример интерфейса struct IFace { virtual ~IFace () {} virtual void Mtd1() = 0; virtual void Mtd2(int i) = 0; virtual void Mtd3(int i) const = 0; virtual int const* Mtd4() const = 0; virtual int const& Mtd5(int i, long *l1, long const *l2, double &d, long const &l3) const = 0; virtual char const* Mtd6() const = 0; virtual wchar_t const* Mtd7() = 0; virtual void Mtd8(char const *s1, wchar_t const *s2) = 0; }; Тестовый интерфейс, который содержит методы как с параметрами, так и без. Параметры передаются по значению, ссылке или указателю. Так же методы могут ничего не возвращать или какое-то значение, переданное по ссылке, указателю или по значению (простите за тавтологию). В общем, материал как раз для того, чтобы в полном объеме протестировать то минимальное описание Proxy/Stub, которое приведено ниже. И которое не требует никакого указания информации о параметрах и возвращаемых значениях, а только интерфейс и имена методов.Пример описания Proxy/Stub PS_BEGIN_MAP (IFace) PS_ADD_METHOD (Mtd1) PS_ADD_METHOD (Mtd2) PS_ADD_METHOD (Mtd3) PS_ADD_METHOD (Mtd4) PS_ADD_METHOD (Mtd5) PS_ADD_METHOD (Mtd6) PS_ADD_METHOD (Mtd7) PS_ADD_METHOD (Mtd8) PS_END_MAP () PS_REGISTER_PSTYPES (IFace) То к чему у меня было большое желание прийти — это отказаться от указания информации о параметрах и возвращаемых значениях при описании Proxy/Stub, задавая только имена методов. За это пришлось немного заплатить небольшим ограничением: отказом от использования перегрузки методов. И это ограничение можно обойти, добавив небольшой макрос, который уже не будет столь лаконичен. В рамках этого поста этого макроса не будет. При соответствующей мотивации это можно сделать очень быстро.Тяга к такому минимализму обусловлена тем, что приходилось иметь дело с разными библиотеками и framework’ами, которые заставляли прилагать много усилий для описания каждого метода и если не было автогенерации, то при внесении изменений в метод, приходилось после пинка компилятора вспоминать, что забыл поправить Proxy/Stub, отправляться в соответствующий файл и править. А когда и была автогенерация, то она иногда сильно изобиловала тонкостями описания ее входных данных, на основании которых она работала. Полностью от этого все же не удалось избавиться, так как средствами C++ можно всю информацию о методах класса легко получить, а вот стандартных средств перечислить методы нет. Поэтому единственное, что придется делать при изменении интерфейса — это добавлять и удалять методы в описании Proxy/Stub при их добавлении и удалении в интерфейсе, при этом поддерживать строгий порядок следования Proxy/Stub описания методов последовательности методов самого интерфейса нет необходимости. Этого не было в ранее опубликованном посте [4], а теперь благодаря средствам C++11 такую независимость удалось получить.

Пример сервера #include #include

#include «face.h» // Класс-реализация интерфейса IFace. #include «iface_ps.h» // Описание Proxy/Stub интерфейса. #include «xml/pack.h» // Реализация (де)сериализации #include «http/server_creator.h» // Функция создания сервера. #include «class_ids.h» // Идентификаторы классов-реализаций. int main () { try { // Создание сервера. auto Srv = Remote: Http: CreateServer < Remote::Pkg::Xml::InputPack, // Тип десериализатора входящих пакетов Remote::ClassInfo // Описание реализации интерфейса. // Описаний Remote::ClassInfo может быть несколько. // На каждую реализацию здесь передается Remote::ClassInfo. < IFace, // Реализуемый интерфейс Face, // Реализация интерфейса FaceClsId // Идентификатор реализации > >(»127.0.0.1» /*IP*/, 5555/*Port*/, 2/*ThreadCount*/); // Сетевой интерфейс, на котором работает сервер. std: cin.get (); // «Завешиваем» основной поток. Сервер работает пока существует его объект. } catch (std: exception const &e) { std: cerr << e.what() << std::endl; } return 0; } В примере показано, что при создании сервера нужно указать тип, который будет заниматься сборкой и разбором пакетов данных и указать список классов, реализации которых сервер будет поставлять его клиентам. Так как для одного и того же интерфейса может быть несколько разных реализаций, то они маркируются идентификатором. При создании объекта клиент передает серверу идентификатор реализации. Классы-реализации могут наследовать множество всего, а для того, чтобы при запросе клиента сервер мог создать нужный объект-заглушку, указывается и интерфейс, который реализует класс. По этому интерфейсу производится поиск нужного типа заглушки. Сервер может поставлять множество реализаций для множества интерфейсов и все они должны быть перечислены при создании сервера. Функцию CreateServer можно переписать для своего вида транспорта, а все необходимое для этого уже есть. Нужен только транспорт при желании его заменить. Вся работа по созданию объектов и объектов-заглушек уже реализована и не нуждается в замене.Пример клиента #include #include «iface_ps.h» // Описание Proxy/Stub интерфейса. #include «class_ids.h» // Идентификаторы классов-реализаций. #include «xml/pack.h» // Реализация (де)сериализации. #include «http/http_remoting.h» // Реализация транспорта клиента. #include «class_factory.h» // Фабрика классов. int main () { try { // Создание транспорта. В данном случае на основе libevent. // Реализовав интерфейс Remote: Http: Remoting можно предоставить собственный транспорт. auto Remoting = std: make_shared127.0.0.1», 5555); auto Factory = Remote: ClassFactory: Create // Создание фабрики классов. < Remote::Pkg::Xml::OutputPack, // Тип для сериализации исходящих пакетов. IFace // Список поддерживаемых интерфейсов. В данном примере один интерфейс. >(Remoting); // Создание объекта с интерфейсом IFace и идентификатором реализации на стороне сервера FaceClsId. auto Obj = Factory→Create(FaceClsId); // Вызов методов объекта на сервере. Obj→Mtd2(10); } catch (std: exception const &e) { std: cerr << e.what() << std::endl; } return 0; } При создании фабрики классов ей нужно передать объект, реализующий транспорт и список интерфейсов, которые она поддерживает. По этому списку интерфейсов фабрика для созданного на сервере объекта у себя определяет прокси-объект, которым пользователь будет пользоваться для вызова методов интерфейса.Можно заметить из примера, что фабрика не является классом-шаблоном, а только ее методы создания самой фабрики и объектов являются шаблонными. Зачем? Если бы фабрика была шаблоном, то по всем единицам трансляции нужно было бы «таскать» за собой всю информацию о ее типах, с которыми она работает. А так можно воспользоваться forward declaration и передав указатель на фабрику в иной файл проекта, в котором уже не надо знать и подключать все файлы с информацией о типе, производящем сериализацию и десереализацию пакетов и файлов с интерфейсами, использование которых в той части проекта не нужно.Примеры приведены, даны пояснения для чего сделано то или иное решение, можно переходить к техническим аспектам реализации, а реализация еще раз повторюсь поделена на две крупные части:

Создание Proxy/Stub’ов Инфраструктура и транспорт Proxy/Stubs 7f214854e2bc0a41e96a34d2fc3aa14e.jpg При возникновении необходимости в разделении объекта и его использующего кода между клиентом и сервером, в этот момент появляется необходимость в прокси-объектах (Proxy) на стороне клиента и объектах-заглушках (Stub) на стороне сервера. Первые при вызове метода на стороне клиента создают запрос и отправляют его на сервер, а вторые на стороне сервера разбирают этот запрос и вызывают соответствующий метод реального объекта.Как с помощью C++ можно получить всю информацию о методе на основе указателя на метод в момент компиляции. А так же как с помощью полученной информации и преимуществ C++11 создавать наборы Proxy/Stub так же в момент компиляции будет рассказано в этом разделе. Это можно сделать на основе шаблонов и немного прибегнув к средствам препроцессора.Предположим есть такой интерфейс:

struct ISessionManager { virtual ~ISessionManager () {} virtual std: uint32_t OpenSession (char const *userName, char const *password) = 0; virtual void CloseSession (std: uint32_t sessionId) = 0; virtual bool IsValidSession (std: uint32_t sessionId) const = 0; }; предназначенный для работы с сессиями пользователей. OpenSession — открывает сессию для заданного пользователя и в качестве результата возвращает идентификатор открытой сессии. CloseSession закрывает сессию по переданному идентификатору. IsValidSession — проверяет идентификатор сессии на валидность.При создании Proxy/Stub для каждого из методов надо получить его тип. Тип указателя на метод. В C++03 стандартными средствами этого сделать нельзя. Приходилось прибегать к некоторым компиляторозависимым решениям [4]. Так для gcc можно было воспользоваться его расширением typeof, а для MS Visual Studio сделать (более ранних версий, чем 2010) некоторый хак на шаблонах, который мог компилироваться только ее компилятором, т. к. подход выходит за рамки стандарта. Это все и многое другое можно подсмотреть, например, в boost. С появлением C++11 это стало возможно в рамках стандарта. Получать типы переменных и выражений можно с помощью decltype.

typedef decltype (&ISessionManager: OpenSession) OpenSessionMtdType; typedef decltype (&ISessionManager: CloseSession) CloseSessionMtdType; typedef decltype (&ISessionManager: IsValidSession) IsValidSessionMtdType; В этом и кроется ограничение, которое не дает при рассматриваемой простоте использования макросов создания Proxy/Stub использовать перегрузку методов, так как при передаче в decltype указателя на метод нет средств указать компилятору какой из перегруженных методов нужно использовать.Получив тип метода, можно с помощью еще одного средства C++11, шаблонов с переменным числом параметров сделать реализации для каждого из методов и на их основе построить или прокси-объект или объект-заглушку, получая при этом всю информацию о методе: о возвращаемом значении, передаваемых параметрах и его cv-квалификаторе (в данном случае интересен только const квалификатор для небольшого упрощения). А так как нужно в эту реализацию еще и имя метода подставить, то придется немного прибегнуть к препроцессору. На основании этого можно получить такой макрос для реализации метода:

namespace Methods { template struct Method; }

#define DECLARE_PROXY_METHOD (iface_, mtd_, id_) \ namespace Methods \ { \ typedef decltype (&iface_:: mtd_) mtd_##Type; \ template \ struct Method \ : public virtual iface_ \ { \ virtual R mtd_ (P … p) \ { \ throw std: runtime_error («Not implemented.»); \ } \ }; \ template \ struct Method \ : public virtual iface_ \ { \ virtual R mtd_ (P … p) const \ { \ throw std: runtime_error («Not implemented.»); \ } \ }; \ typedef Method mtd_##ProxyType; \ } Можно заметить, что макрос определяет две специализации: для константного и неконстантного методов, и только одна будет из них инстанцирована для конкретного метода. С помощью этого макроса и всего что уже получено ранее можно собрать уже готовый прокси-класс: DECLARE_PROXY_METHOD (ISessionManager, OpenSession, 1) DECLARE_PROXY_METHOD (ISessionManager, CloseSession, 2) DECLARE_PROXY_METHOD (ISessionManager, IsValidSession, 3)

template class Proxy : public T … { };

typedef Proxy < Methods::OpenSessionProxyType, Methods::CloseSessionProxyType, Methods::IsValidSessionProxyType > SessionManagerProxy; Сам макрос принимает три параметра: интерфейс, метод и идентификатор метода. Если с интерфейсом и методом все просто, то идентификатор откуда-то надо взять. В [4] предлагалось сделать некоторый счетчик в момент компиляции и его использовать как идентификатор метода. Это накладывает некоторые ограничения: последовательность методов важна. Если собран сервер с одной последовательностью методов, а клиент с другой, то идентификторы методов не будут совпадать. И при обмене пакетами клиент и сервер не смогут «договориться» о том какой метод использовать для присланного идентификатора. Отсюда необходимость пересборки обеих частей при изменении порядка методов или при их удалении и добавлении в описание Proxy/Stub. С помощью C++11 такое ограничение можно устранить, вычислив CRC32 имени метода в момент компиляции, а constexpr позволяет это легко сделать в момент компиляции. Счетчик так же пригодится для других целей. А пока пара слов о генерации CRC32 в момент компиляции. Вариант создания CRC32 в момент компиляции уже приводился ранее в [1]. Здесь еще раз приведу его.Создание CRC32 кода от строки в момент компиляции namespace Remote { namespace Private { template struct Crc32TableWrap { static constexpr std: uint32_t const Table[256] = { 0×00000000L, 0×77073096L, 0xee0e612cL, 0×990951baL, 0×076dc419L, 0×706af48fL, 0xe963a535L, 0×9e6495a3L, 0×0edb8832L, 0×79dcb8a4L, 0xe0d5e91eL, 0×97d2d988L, 0×09b64c2bL, 0×7eb17cbdL, 0xe7b82d07L, // И т.д. заполнение таблицы }; }; template constexpr std: uint32_t const Crc32TableWrap:: Table[256]; typedef Crc32TableWrap Crc32Table; template inline constexpr std: uint32_t Crc32Impl (char const *str) { return (Crc32Impl (str) >> 8) ^ Crc32Table: Table[(Crc32Impl(str) ^ str[I — 1]) & 0×000000FF]; } template<> inline constexpr std: uint32_t Crc32Impl<0>(char const *) { return 0xFFFFFFFF; } } template inline constexpr std: uint32_t Crc32(char const (&str)[N]) { return (Private: Crc32Impl(str) ^ 0xFFFFFFFF); } } Теперь макрос определения прокси-объекта и его использование становятся чуть более дружелюбными, так как необходимости раздачи идентификаторов методам возложена на компилятор.Пример обновленного макроса и его использование struct ISessionManager { virtual ~ISessionManager () {} virtual std: uint32_t OpenSession (char const *userName, char const *password) = 0; virtual void CloseSession (std: uint32_t sessionId) = 0; virtual bool IsValidSession (std: uint32_t sessionId) const = 0; };

typedef decltype (&ISessionManager: OpenSession) OpenSessionMtdType; typedef decltype (&ISessionManager: CloseSession) CloseSessionMtdType; typedef decltype (&ISessionManager: IsValidSession) IsValidSessionMtdType;

namespace Methods { template struct Method; }

#define DECLARE_PROXY_METHOD (iface_, mtd_) \ namespace Methods \ { \ enum {mtd_##Id = Crc32(#mtd_)}; \ typedef decltype (&iface_:: mtd_) mtd_##Type; \ template \ struct Method \ : public virtual iface_ \ { \ virtual R mtd_ (P … p) \ { \ throw std: runtime_error («Not implemented.»); \ } \ }; \ template \ struct Method \ : public virtual iface_ \ { \ virtual R mtd_ (P … p) const \ { \ throw std: runtime_error («Not implemented.»); \ } \ }; \ typedef Method mtd_##ProxyType; \ }

DECLARE_PROXY_METHOD (ISessionManager, OpenSession) DECLARE_PROXY_METHOD (ISessionManager, CloseSession) DECLARE_PROXY_METHOD (ISessionManager, IsValidSession)

template class Proxy : public T … { };

typedef Proxy < Methods::OpenSessionProxyType, Methods::CloseSessionProxyType, Methods::IsValidSessionProxyType > SessionManagerProxy; Несмотря на то, что в C++11 возможно наследование от шаблонного параметра, который является пакетом типов (шаблоны с переменным числом параметров), запись typedef Proxy < Methods::OpenSessionProxyType, Methods::CloseSessionProxyType, Methods::IsValidSessionProxyType > SessionManagerProxy; как-то еще очень слабо тянет на минимализм. Чтобы избавиться от конкретных имен типов в определении прокси-класса нужно создать некоторый реестр типов, пронумеровав каждый в нем содержащийся тип с помощью некоторого счетчика. На основе этого реестра и счетчика позднее построить иерархию наследования классов-реализаций методов интерфейса. Создать реестр можно с помощью небольшой связки шаблонов и макросов. template struct TypeRegistry;

#define REGIDTER_TYPE (id_, type_) \ template <> \ struct TypeRegistry \ { \ typedef type_ Type; \ }; Весьма простая реализация опять основанная на частных специализациях. Добавив это в ранее рассмотренный макрос определения метода он приблизится еще на шаг к конечной цели.Обновленный макрос и пример его использования #define DECLARE_TYPE_REGISTRY (reg_name_) \ namespace reg_name_ \ { \ template \ struct TypeRegistry; \ }

#define REGIDTER_TYPE (id_, type_) \ template <> \ struct TypeRegistry \ { \ typedef type_ Type; \ };

DECLARE_TYPE_REGISTRY (Methods)

namespace Methods { template struct Method; }

#define DECLARE_PROXY_METHOD (iface_, mtd_, id_) \ namespace Methods \ { \ enum {mtd_##Id = Crc32(#mtd_)}; \ typedef decltype (&iface_:: mtd_) mtd_##Type; \ template \ struct Method \ : public virtual iface_ \ { \ virtual R mtd_ (P … p) \ { \ throw std: runtime_error («Not implemented.»); \ } \ }; \ template \ struct Method \ : public virtual iface_ \ { \ virtual R mtd_ (P … p) const \ { \ throw std: runtime_error («Not implemented.»); \ } \ }; \ typedef Method mtd_##ProxyType; \ REGIDTER_TYPE (id_, mtd_##ProxyType) \ }

DECLARE_PROXY_METHOD (ISessionManager, OpenSession, 1) DECLARE_PROXY_METHOD (ISessionManager, CloseSession, 2) DECLARE_PROXY_METHOD (ISessionManager, IsValidSession, 3)

template class Proxy : public T … { };

typedef Proxy < typename Methods::TypeRegistry<1>:: Type, typename Methods: TypeRegistry<2>:: Type, typename Methods: TypeRegistry<3>:: Type > SessionManagerProxy; Посмотрев на конечный код построения прокси-класса typedef Proxy < typename Methods::TypeRegistry<1>:: Type, typename Methods: TypeRegistry<2>:: Type, typename Methods: TypeRegistry<3>:: Type > SessionManagerProxy; можно заметить, что в нем пропали какие-то специфичные имена при определении прокси-класса, но опять появился какой-то идентификатор. На данный момент это не идентификатор, это счетчик под которым находится определенная запись в реестре типов. Построив счетчик в момент компиляции можно отказаться от явного задания каких-то «цифр» и построить завершающий вариант упрощенных макросов для определения прокси-класса из информации о его методах.Как сделать счетчик в момент компиляции уже было написано в [4]. Бегло повторю. Для построения счетчика нужно:

Сгенерировать некоторую иерархию типов. Объявить функцию принимающую void * и возвращающую массив char размеров в один элемент. На каждом шаге для каждой новой константы счетчика с помощью sizeof получать размер массива объявленной (но не определенной) функции и объявлять новую функцию с одним из типов иерархии, которая при спуске по иерархии возвращает массив все большей и большей длины. Реализация функций не требуется, т. к. они никогда не вызываются, а используются в момент компиляции под sizeof для вычисления размера возвращаемого значения. Описания алгоритма звучит более непонятным, чем он реализуется… Реализация проста: namespace Private { template struct Hierarchy : public Hierarchy { }; template <> struct Hierarchy<0> { }; }

#define INIT_STATIC_COUNTER (counter_name_, max_count_) \ namespace counter_name_ \ { \ typedef: Private: Hierarchy CounterHierarchyType; \ char (&GetCounterValue (void const *))[1]; \ }

#define GET_NEXT_STATIC_COUNTER (counter_name_, value_name_) \ namespace counter_name_ \ { \ enum { value_name_ = sizeof (GetCounterValue (static_cast(0))) }; \ char (&GetCounterValue (:: Private: Hierarchy const *))[value_name_ + 1]; \ } Для того, чтобы опробовать работу такого счетчика можно написать тест: INIT_STATIC_COUNTER (MyCounter, 100)

GET_NEXT_STATIC_COUNTER (MyCounter, Item1) GET_NEXT_STATIC_COUNTER (MyCounter, Item2) GET_NEXT_STATIC_COUNTER (MyCounter, Item3)

int main () { std: cout << MyCounter::Item1 << std::endl; std::cout << MyCounter::Item2 << std::endl; std::cout << MyCounter::Item3 << std::endl; return 0; } В результате на экране будут распечатаны значения от одного до трех. Это можно отправить компилятору с ключом -E для того чтобы развернуть все макросы.Код после разворачивания макросов #include namespace Private { template struct Hierarchy : public Hierarchy { }; template <> struct Hierarchy<0> { }; } namespace MyCounter { typedef: Private: Hierarchy<100> CounterHierarchyType; char (&GetCounterValue (void const *))[1]; }

namespace MyCounter { enum { Item1 = sizeof (GetCounterValue (static_cast(0))) }; char (&GetCounterValue (:: Private: Hierarchy const *))[Item1 + 1]; }

namespace MyCounter { enum { Item2 = sizeof (GetCounterValue (static_cast(0))) }; char (&GetCounterValue (:: Private: Hierarchy const *))[Item2 + 1]; }

namespace MyCounter { enum { Item3 = sizeof (GetCounterValue (static_cast(0))) }; char (&GetCounterValue (:: Private: Hierarchy const *))[Item3 + 1]; }

int main () { std: cout << MyCounter::Item1 << std::endl; std::cout << MyCounter::Item2 << std::endl; std::cout << MyCounter::Item3 << std::endl; return 0; } Как и говорилось все просто. В начале немного макросы пугают, но тут они как раз на пользу идут. Без них трудно будет реализовать счетчик.Все составляющие для генерации конечного прокси-класса есть. Прокси-класс создается из его составляющих кубиков, каждый из которых содержит реализацию для одного из методов интерфейса. Можно написать конечный набор макросов для любого интерфейса и проверить на уже ранее разбираемом ISessionManager.

Макросы для описания прокси-класса #define BEGIN_PROXY_MAP (iface_) \ namespace iface_##PS \ { \ namespace Impl \ { \ typedef iface_ IFaceType; \ INIT_STATIC_COUNTER (MtdCounter, 100) \ namespace Methods \ { \ DECLARE_TYPE_REGISTRY (ProxiesReg) \ template \ struct Method; \ } #define ADD_PROXY_METHOD (mtd_) \ GET_NEXT_STATIC_COUNTER (MtdCounter, mtd_##Counter) \ namespace Methods \ { \ enum {mtd_##Id = Crc32(#mtd_)}; \ typedef decltype (&IFaceType: mtd_) mtd_##Type; \ template \ struct Method \ : public virtual IFaceType \ { \ virtual R mtd_ (P … p) \ { \ throw std: runtime_error (#mtd_ » not implemented.»); \ } \ }; \ template \ struct Method \ : public virtual IFaceType \ { \ virtual R mtd_ (P … p) const \ { \ throw std: runtime_error (#mtd_ » not implemented.»); \ } \ }; \ typedef Method mtd_##ProxyType; \ REGIDTER_TYPE (ProxiesReg, MtdCounter: mtd_##Counter, mtd_##ProxyType) \ }

#define END_PROXY_MAP () \ GET_NEXT_STATIC_COUNTER (MtdCounter, LastCounter) \ template \ class ProxyItem \ : public Methods: ProxiesReg: TypeRegistry:: Type \ , public ProxyItem \ { \ }; \ template <> \ class ProxyItem<0> \ { \ }; \ } \ typedef Impl: ProxyItem Proxy; \ } Вспомогательный код namespace Private { template struct Hierarchy : public Hierarchy { }; template <> struct Hierarchy<0> { }; }

#define INIT_STATIC_COUNTER (counter_name_, max_count_) \ namespace counter_name_ \ { \ typedef: Private: Hierarchy CounterHierarchyType; \ char (&GetCounterValue (void const *))[1]; \ }

#define GET_NEXT_STATIC_COUNTER (counter_name_, value_name_) \ namespace counter_name_ \ { \ enum { value_name_ = sizeof (GetCounterValue (static_cast(0))) }; \ char (&GetCounterValue (:: Private: Hierarchy const *))[value_name_ + 1]; \ }

#define DECLARE_TYPE_REGISTRY (reg_name_) \ namespace reg_name_ \ { \ template \ struct TypeRegistry; \ }

#define REGIDTER_TYPE (reg_name_, id_, type_) \ namespace reg_name_ \ { \ template <> \ struct TypeRegistry \ { \ typedef type_ Type; \ }; \ } Пользовательский код struct ISessionManager { virtual ~ISessionManager () {} virtual std: uint32_t OpenSession (char const *userName, char const *password) = 0; virtual void CloseSession (std: uint32_t sessionId) = 0; virtual bool IsValidSession (std: uint32_t sessionId) const = 0; };

BEGIN_PROXY_MAP (ISessionManager) ADD_PROXY_METHOD (OpenSession) ADD_PROXY_METHOD (CloseSession) ADD_PROXY_METHOD (IsValidSession) END_PROXY_MAP ()

int main () { try { ISessionManagerPS: Proxy Proxy; Proxy.OpenSession («user»,»111»); } catch (std: exception const &e) { std: cerr << e.what() << std::endl; } return 0; } Как можно видеть из приведенного примера получился простой интерфейс для описания прокси-класса, внешне мало отличающийся от того, что было приведено в примерах в начале поста. Этот код уже можно скомпилировать и попробовать вызвать методы прокси-объекта для интерфейса ISessionManager и в ответ получить исключение с сообщением «OpenSession not implemented.». На данный момент реализация каждого метода просто кидает исключение о том что метод пока ничего не делает. Немногим позднее эти реализации будут заполнены более осмысленным кодом. А пока можно попробовать пример, приведенный выше, отправить компилятору с ключом -E и посмотреть во что развернулись все макросы. Кода после раскрытия всех макросов получилось не так и мало для восприятия человеком как вспомогательного материала при чтении поста. Да и по большому счету он и не рассчитан на человека, препроцессор сделал свою грязную работу, заменив частично утилиту автогенерации, теперь очередь за компилятором продолжать работу над полученными типами, продолжая заменять утилиту автогенерации кода: инстанцировать, рассчитывать полученный счетчик и т. д. Можно по диагонали просмотреть код и легко понять его структуру, что получилось после разворачивания всех макросов.Код примера после разворачивания макросов namespace Private { template struct Hierarchy : public Hierarchy { }; template <> struct Hierarchy<0> { }; }

namespace ISessionManagerPS { namespace Impl { typedef ISessionManager IFaceType; namespace MtdCounter { typedef: Private: Hierarchy<100> CounterHierarchyType; char (&GetCounterValue (void const *))[1]; } namespace Methods { namespace ProxiesReg { template struct TypeRegistry; } template struct Method; } namespace MtdCounter { enum { OpenSessionCounter = sizeof (GetCounterValue (static_cast(0))) }; char (&GetCounterValue (:: Private: Hierarchy const *))[OpenSessionCounter + 1]; } namespace Methods { enum { OpenSessionId = Crc32(«OpenSession») }; typedef decltype (&IFaceType: OpenSession) OpenSessionType; template struct Method : public virtual IFaceType { virtual R OpenSession (P … p) { throw std: runtime_error («OpenSession» » not implemented.»); } }; template struct Method : public virtual IFaceType { virtual R OpenSession (P … p) const { throw std: runtime_error («OpenSession» » not implemented.»); } }; typedef Method OpenSessionProxyType; namespace ProxiesReg { template <> struct TypeRegistry { typedef OpenSessionProxyType Type; }; } } namespace MtdCounter { enum { CloseSessionCounter = sizeof (GetCounterValue (static_cast(0))) }; char (&GetCounterValue (:: Private: Hierarchy const *))[CloseSessionCounter + 1]; } namespace Methods { enum { CloseSessionId = Crc32(«CloseSession») }; typedef decltype (&IFaceType: CloseSession) CloseSessionType; template struct Method : public virtual IFaceType { virtual R CloseSession (P … p) { throw std: runtime_error («CloseSession» » not implemented.»); } }; template struct Method : public virtual IFaceType { virtual R CloseSession (P … p) const { throw std: runtime_error («CloseSession» » not implemented.»); } }; typedef Method CloseSessionProxyType; namespace ProxiesReg { template <> struct TypeRegistry { typedef CloseSessionProxyType Type; }; } } namespace MtdCounter { enum { IsValidSessionCounter = sizeof (GetCounterValue (static_cast(0))) }; char (&GetCounterValue (:: Private: Hierarchy const *))[IsValidSessionCounter + 1]; } namespace Methods { enum { IsValidSessionId = Crc32(«IsValidSession») }; typedef decltype (&IFaceType: IsValidSession) IsValidSessionType; template struct Method : public virtual IFaceType { virtual R IsValidSession (P … p) { throw std: runtime_error («IsValidSession» » not implemented.»); } }; template struct Method : public virtual IFaceType { virtual R IsValidSession (P … p) const { throw std: runtime_error («IsValidSession» » not implemented.»); } }; typedef Method IsValidSessionProxyType; namespace ProxiesReg { template <> struct TypeRegistry { typedef IsValidSessionProxyType Type; }; } } namespace MtdCounter { enum { LastCounter = sizeof (GetCounterValue (static_cast(0))) }; char (&GetCounterValue (:: Private: Hierarchy const *))[LastCounter + 1]; } template class ProxyItem : public Methods: ProxiesReg: TypeRegistry:: Type , public ProxyItem { }; template <> class ProxyItem<0> { }; } typedef Impl: ProxyItem Proxy; }

int main () { try { ISessionManagerPS: Proxy Proxy; Proxy.OpenSession («user»,»111»); } catch (std: exception const &e) { std: cerr << e.what() << std::endl; } return 0; } Реализовав все методы интерфейса в прокси-классе, эти реализации нужно заполнить кодом, который бы занимался упаковкой всех переданных параметров, отправкой полученного пакета данных на сервер и полученный ответ разбирал бы, находя в нем результат выполнения метода (если таковой имеется), а так же если в ходе выполнения метода было выброшено исключение, то получить информацию о нем из пакета с ответом, так как исключения так же передаются от сервера клиенту.Такой код можно разместить в каждой из реализаций методов. Однако это было бы не самым красивым и оптимальным решением. Желательно этот код расположить в одном и том же месте. Для решения этой задачи хорошо подойдет такой шаблон проектирования, как CRTP [7].

Все «кубики» (классы-реализации методов) можно наследовать от класса, который будет иметь метод для выполнения манипуляций с пакетом данных. А так как вся необходимая информация находится в самом прокси-классе (в самом низу иерархии наследования), то туда и должны перенаправляться все вызовы. Зачем? Логичнее всего было бы в этом прокси-классе разместить информацию об идентификаторе экземпляра объекта на стороне сервера, а так же указатель на интерфейс, через который осуществляется взаимодействие (транспорт), а не вставлять однотипный код с обработкой этой информации по всем классам-реализациям методов интерфейса.

В данном случае CRTP очень хорошо подходит и имеет преимущество над обычными интерфейсами с наборами чисто виртуальных методов. Сделать шаблонной виртуальную функцию нельзя, а сделать метод класса в CRTP шаблонным ничего не мешает. А так как в данном случае количество параметров для каждого вызова такого метода для работы с пакетами данных разное в силу того что разные методы интерфейса могут иметь разное количество параметров, то использование CRTP оказывается очень полезным.

Базовым классом для всех классов-реализаций методов будет:

template class ProxyMethodBase { public: template R Execute (std: uint32_t mtdId, P … params) { return dynamic_cast(*this).Execute(mtdId, params …); } protected: virtual ~ProxyMethodBase () { } }; Так как все реализации методов наследуют ProxyMethodBase виртуально, то в реализации ProxyMethodBase придется использовать dynamic_cast, а не более привычный static_cast для CRTP. Виртуальное наследование нужно для того, чтобы для всех методов определить одну базу и чтобы компилятор не испытывал затруднения при определении ветки наследования при приведении типа. Виртуальный деструктор в ProxyMethodBase нужен как минимум для того же, чтобы компилятор мог работать с dynamic_cast, в противном случае он скажет, что тип, к которому производится попытка приведения с помощью dynamic_cast не является полиморфным, а должен.С учетом добавленного базового класса для всех реализаций методов интерфейса немного меняются и макросы описания прокси-класса.

BEGIN_PROXY_MAP #define BEGIN_PROXY_MAP (iface_) \ namespace iface_##PS \ { \ namespace Impl \ { \ typedef iface_ IFaceType; \ INIT_STATIC_COUNTER (MtdCounter, 100) \ class ProxyImpl; \ namespace Methods \ { \ DECLARE_TYPE_REGISTRY (ProxiesReg) \ template \ struct Method; \ } ADD_PROXY_METHOD #define ADD_PROXY_METHOD (mtd_) \ GET_NEXT_STATIC_COUNTER (MtdCounter, mtd_##Counter) \ namespace Methods \ { \ enum {mtd_##Id = Crc32(#mtd_)}; \ typedef decltype (&IFaceType: mtd_) mtd_##Type; \ template \ struct Method \ : public virtual IFaceType \ , public virtual ProxyMethodBase \ { \ virtual R mtd_ (P … p) \ { \ return Execute(mtd_##Id, p …); \ } \ }; \ template \ struct Method \ : public virtual IFaceType \ , public virtual ProxyMethodBase \ { \ typedef Method ThisType; \ virtual R mtd_ (P … p) const \ { \ return const_cast(this)→Execute(mtd_##Id, p …); \ } \ }; \ typedef Method mtd_##ProxyType; \ REGIDTER_TYPE (ProxiesReg, MtdCounter: mtd_##Counter, mtd_##ProxyType) \ } END_PROXY_MAP #define END_PROXY_MAP () \ GET_NEXT_STATIC_COUNTER (MtdCounter, LastCounter) \ template \ class ProxyItem \ : public Methods: ProxiesReg: TypeRegistry:: Type \ , public ProxyItem \ { \ }; \ template <> \ class ProxyItem<0> \ { \ }; \ class ProxyImpl \ : public ProxyItem \ { \ public: \ private: \ friend class ProxyMethodBase; \ template \ R Execute (std: uint32_t mtdId, P … params) \ { \ throw std: runtime_error («Not implemented.»); \ } \ }; \ } \ typedef Impl: ProxyImpl Proxy; \ } Пользовательский код же никак не меняется. В то же время в реализации прокси-класса появилась одна локализованная точка, в которой можно производить все манипуляции с параметрами и работу с сервером. Если посмотреть на метод прокси-класса, куда в результате приходит вызов метода интерфейса template R Execute (std: uint32_t mtdId, P … params) { // … } то здесь уже можно воспользоваться классической схемой перебора всех параметров вызванного метода для их сериализации, которая практически во всех источниках, посвященных C++11 приводится на примере реализации типа безопасной функции printf. template void Serialize (S &) { }

template void Serialize (S &stream, T prm, P … params) { stream << prm; Serialize(stream, params ...); } И добавив вызов Serialize в метод Execute можно получить сериализацию параметров. Остается только реализовать свой класс для сериализации. Это рутинная и неинтересная задача. Ее решение на основе xml и rapidxml [6] приведено в исходных кодах прилагаемых к этому посту.Все, реализация прокси-класса полностью готова! Она немного отличается деталями от той, что приведана в исходном коде к этому посту. Отличие только в более сложной обработке параметров и взаимодействии с сервером — это все рутина, а идеологически она построена точно так же со всеми описанными подходами.

Выше говорилось о Proxy/Stub, а пока реализована работа только с прокси. Реализация классов-заглушек (Stub) почти аналогичны реализации прокси-классов. Отличие заключается в том, что для прокси-класса нужно при вызове метода интерфейса всю информацию упаковать и отправить, а полученный результат отдать вызывающей стороне, а для класса-заглушки нужно выполнить все строго наоборот: распаковать полученный пакет, на его основе собрать параметры в некоторый список аргументов метода интерфейса и вызвать его, а полученный результат отправить вызывающей стороне. Для этого нужно в существующие макросы добавить немного изменений, связанных с вызовом метода с параметрами, извлеченными из пришедшего пакета. Все остальное остается таким же. Так же нужен и реестр, и счетчик, и сбор конечного класса из его «кубиков». Реестра теперь становится два: один для прокси-классов, второй для класса-заглушек. Можно и в один все поместить, а потом разбирать какой элемент чем является. Это приведет к более сложному коду. Поэтому добавлен второй реестр для объектов-заглушек.

PS_BEGIN_MAP #define PS_BEGIN_MAP (iface_) \ namespace iface_##PS \ { \ namespace Impl \ { \ typedef iface_ IFaceType; \ INIT_STATIC_COUNTER (MtdCounter, 100) \ class ProxyImpl; \ class StubImpl; \ namespace Methods \ { \ DECLARE_TYPE_REGISTRY (ProxiesReg) \ template \ struct ProxyMethod; \ DECLARE_TYPE_REGISTRY (StubsReg) \ template \ struct StubMethod; \ } PS_ADD_METHOD #define PS_ADD_METHOD (mtd_) \ GET_NEXT_STATIC_COUNTER (MtdCounter, mtd_##Counter) \ namespace Methods \ { \ enum {mtd_##Id = Crc32(#mtd_)}; \ typedef decltype (&IFaceType: mtd_) mtd_##Type; \ template \ struct ProxyMethod \ : public virtual IFaceType \ , public virtual ProxyMethodBase \ { \ virtual R mtd_ (P … p) \ { \ return Execute(mtd_##Id, p …); \ } \ }; \ template \ struct ProxyMethod \ : public virtual IFaceType \ , public virtual ProxyMethodBase \ { \ typedef ProxyMethod ThisType; \ virtual R mtd_ (P … p) const \ { \ return const_cast(this)→Execute(mtd_##Id, p …); \ } \ }; \ typedef ProxyMethod mtd_##ProxyType; \ REGIDTER_TYPE (ProxiesReg, MtdCounter: mtd_##Counter, mtd_##ProxyType)

© Habrahabr.ru