Proxy/Stubs своими руками

в 14:22, , рубрики: c++, Программирование, метки:

Введение

Не раз сталкивался в своей практике с компонентными моделями, которые как-то, хоть и отдаленно, а некоторые и весьма сильно напоминают реализацию MS COM'а. Многие компании имеют в своем арсенале подобные вещи, используемые в разработке продуктов компании. В некоторых компаниях это «допиленная» реализация MS COM'а для собственных нужд, в некоторых своя, написанная с нуля и, конечно же, кроссплатформенная, так как кроссплатформенность -это, пожалуй, один из самых весомых аргументов перед начальством на выделение ресурсов, когда кто-то решил написать что-то подобное, а писать подобные вещи всегда интереснее, чем повседневную логику и т.д. Так же и сам написал под нужды одного из проектов некоторую модель, смотрящую одним глазом на — MS COM. Нужды проекта привели к реализации модели, которая могла существовать только в рамках одного процесса и все компоненты могли жить в динамических библиотеках (dll/so) так же как и COM InProc сервера. Как некоторая переработка и выделение только самого необходимого для обеспечения компонентности было опубликовано в моей предыдущей статье Попытка написания своей компонентной модели на C++.
Реализовав пару подобных InProc моделей появилось желание написать нечто, что переступает границу процесса, т.е. модель с использованием IPC, к тому же в комментариях к предыдущей статье именно этого и хотели видеть другие участники.
Немного забегая вперед, скажу, что статья не является описанием некоторой законченной компонентной модели с поддержкой IPC/RPC, а всего лишь описывает некоторую идею реализации Proxy/Stub'ов. Самого же IPC в приведенной реализации не будет, тесты будут проведены в рамках одного процесса. Идея доведена до момента, когда вызов Proxy формирует некоторый буфер для отправки в другой процесс, а Stub на основе буфера вызывает соответствующий метод реализации. Если проложить некоторый транспорт для передачи этого самого буфера, то уже получится законченная модель.
Вся реализация построена на шаблонах и макросах. Да, макросах! Макросы — это зло, но в данном случае некоторые вещи без них не получатся.
Итак, господа эстеты, если Вам все же интересно и вы не испытываете большой неприязни к шаблонам и дурно пахнущим макросам, то

Чего хотелось добиться

Смотря на многие реализации Proxy/Stub'ов меня всегда удручал один факт: слишком много действий для описания. Примеры того, с чем хотелось побороться:

  • Чтобы описать один метод надо указать идентификатор метода, описать его Proxy и Stub отдельно, причем для каждого указать количество параметров, явно указать какой параметр будет входным, а какой выходным, указать является ли метод константным и т.д..
  • Использовать для каждого типа метода при его описании разные макросы, как это делается, например, в макросах Google gmock/gtest (хоть это и не компонентная модель с Proxy/Stub'ами, но идея очень близка). gmock/gtest прекрасно решают свои задачи, но хотелось бы иметь немного другое.

Конечный результат, к которому и будет стремление

Основной единицей для данной модели будет интерфейс. Понятие интерфейса очень широко, в данной статье буду под интерфейсом иметь ввиду структуру с чисто виртуальными методами.
Пример интерфейса, на котором и будет все тестироваться:

struct IFace
{
	virtual ~IFace()
	{
	}
	virtual void Mtd1() = 0;
	virtual char const* Mtd2() const = 0;
	virtual bool Mtd3(int) = 0;
	virtual bool Mtd4(int) const = 0;
	virtual int Mtd5(char const *, bool, double, long, char) const = 0;
	virtual long Mtd6(int const *, bool const &, double &, long *, char) const = 0;
	virtual void Mtd7(wchar_t const *) = 0;
};

Интерфейс содержит методы с параметрами, передаваемыми по значению, по ссылке, по указателю, константные, не константные, методы без параметров, возвращаемые значения и методы, ничего не возвращающие — в общем, все, что надо для «счастья», чтоб было с чем покопаться при реализации.
Пример описания Proxy/Stub'ов на манер кнопки «Пыщь», которая делает по возможности максимум все, скрывая от пользователя кучу настроек:

TPS_BEGIN_MAP(IFace)
	TPS_METHOD(Mtd1)
	TPS_METHOD(Mtd2)
	TPS_METHOD(Mtd3)
	TPS_METHOD(Mtd4)
	TPS_METHOD(Mtd5)
	TPS_METHOD(Mtd6)
	TPS_METHOD(Mtd7)
TPS_END_MAP()

В сухом остатке получается, что для описания Proxy/Stub'а для интерфейса нужно всего 3 макроса: начало карты описания (TPS_BEGIN_MAP), конец карты (TPS_END_MAP) и макрос метода интерфейса, для которого все и будет генерироваться (TPS_METHOD).
Как пользователь я получил чего хотел — минимум, в котором надо указать только интерфейс и перечислить его методы без всякого указания их параметров и cv квалификаторов, но за все надо платить… За эту простоту описания пришлось поплатиться перегрузкой методов; ее нет. Да, перегрузка методов интерфейса не поддерживается, т.е не удастся получить два метода с одним именем и прочими разностями. В чем причина будет описано ниже. Можно и этот недуг победить добавлением еще одного макроса для описания перегруженных методов, но уже не столь короткого для пользователя, так как в нем надо будет указать все характеристики метода: параметры, возвращаемый тип, cv квалификатор. От этого и хотелось уйти. В рамках данной статьи такое решение рассматриваться не будет, ибо цель в ином.

Откуда тяга к минимизации?

Работая с очередной моделью, один из ключевых факторов на который обращаю внимание — это минимизация «ритуала», который надо исполнить, чтоб хоть что-то начало шевелиться.
Если посмотреть на MS COM (а есть и аналогичные «домашние» поделки в конторах), то при описании класса реализации интерфейса мы должны сделать как минимум 2 действия: сделать наследование от интерфейса и не забыть его (интерфейс) включить в карту экспортируемых классом интерфейсов, которая будет использована когда пользователь сделает QueryInterface.
Первую свою поделку на манер MS COM'а я тоже сделал с этими, как минимум, двумя необходимыми точками включения. Получил забавный эффект от использования самим же собой: пока создаешь очень часто компонент за компонентом, то весьма четко исполняешь установленный ритуал, как только ушел в написания бизнес логики и вернулся через весьма продолжительный период времени к добавлению новой реализации интерфейса, то наследовать интерфейс-то не забыл, а вот добавить его в карту экспортируемых интерфейсов забывал весьма часто, после чего некоторое время пытался понять почему сделал все как надо, а интерфейс из нового компонента не запрашивается. Такое повторялось неоднократно. В своей второй поделке этот недостаток я решил изничтожить. Ритуал был сведен к одному навязчивому действию, не выполнить которое нельзя было, чтоб не получить по ушам здесь и сейчас от компилятора, а не когда-то там в момент исполнения, когда уже много реализовано. Примеры и описание можно посмотреть в моей предыдущей статье (приведенной в начале).

Пример из предыдущей статьи

class ITestServiceImpl
  : public CoClassBase
      <
        TL::TypeListAdapter
          <
            ITestService,
            IMessageMgrImpl
          >
      >
{
public:
  BOSS_DECLARE_STR_CLSID(TEST_ITESTMSGMGRSERVICEIMPL)
  // ITestService
  virtual TResult BOSS_CALL_TYPE Start();
  virtual TResult BOSS_CALL_TYPE Stop();
  // …
};

Из примера можно видеть только некоторую «шапку» класса, в которой указывается от чего надо наследоваться (список реализуемых интерфейсов и уже готовых реализаций других интерфейсов), а далее при выполнении QueryInterface библиотечный код сам обойдет все дерево наследования из интерфейсов и унаследованных реализаций интерфейсов в поисках нужного интерфейса, заходя в каждую из реализаций.
Конкретно же в этой статье хотелось добиться минимума от описания Proxy/Stub'ов; получилось добиться того, что можно указать только интерфейс и перечислить его методы без перечисления параметров методов и возвращаемого типа, что, на мой взгляд, упрощает поддержку, сокращая тот самый ритуал, когда надо что-то поменять в интерфейсе (добавить удалить парметры, добавить / удалить const) и не забыть поменять описание его Proxy/Stub'а. Единственное, что надо править при изменениях, вносимых в интерфейс — это если добавили, удалили или переименовали метод.

Реализация

В данной части статьи будут описаны все детали реализации и проблемы, с которыми пришлось столкнуться. Будут макросы, шаблоны, частные специализации, списки типов…

Детали реализации

  • В приведенных макросах описания Proxy/Stub'ов можно заметить префикс TPS. Есть у меня привычка все прятать в пространства имен, весь код помещен в пространство имен Tps, а макросы имеют соответствующий префикс. Не думая долго как назвать пришел к названию. TPS — TinyProxyStubs.
  • Буфер, который будет передаваться от Proxy к Stub'у и обратно — это xml. На мой взгляд, xml имеет множество плюсов с одним большим минусом — избыточность, и как следствие, проблемы с производительностью. Производительность не цель данной статьи, поэтому xml, так как он легок в использовании. Соответственно сериализатор, используемый для формирования и разбора буфера, будет иметь xml реализацию.
  • В качестве библиотек для работы с xml будет использована библиотека RapidXml.
  • Список поддерживаемых типов сериализатором ограничен в рамках данной реализации идеи, но можно написать свою реализацию сериализатора, которая будет поддерживать все необходимые Вам типы. В данной же реализации поддерживаются следующие типы: bool, char, unsigned char, wchar_t, short, unsigned short, int, unsigned int, long, unsigned long, float, double. Так же поддерживаются комбинации [const] тип_из_приведенного_списка [&|*]. Для char/wchar_t const * сделано отдельное исключение в сериализаторе — они сериализуются именно как C-строки, а не указатель на константный символ.
  • Реализация поддерживает от 0 до 5 параметров метода. Если нужно больше, то можно «накопипастить» нужное количество соответствующего кода. По моему мнению, большое количество параметров метода — это проблема дизайна пользовательского кода.
  • Реализация была написана на C++ с небольшим применением stl.
  • Не смотря на то, что сейчас модно писать статьи с использованием C++11 и смачно описывать его новые фичи, в данной реализации используется C++03 (ну и совсем немного пришлось обратиться к C++11, правда опционально).
  • Библиотечный код, связанный с шаблонами, часто грешит почти идентичными участками кода, что будет и здесь. При желании его можно уменьшить, использовав boost preprocessor.

Приступим непосредственно к рассмотрению реализации и проблем, с которыми пришлось столкнуться.

Макрос TPS_BEGIN_MAP разворачивается в макрос

TPS_BEGIN_MAP_IMPL

#define TPS_BEGIN_MAP_IMPL(iface_) 
	namespace iface_##PS 
	{ 
		namespace PSImpl 
		{ 
			typedef iface_ IFacePSType; 
			template <unsigned id> 
			struct ProxyRegItem 
			{ 
				typedef Tps::Private::NullType Type; 
			}; 
			template <unsigned id> 
			struct StubRegItem 
			{ 
				typedef Tps::Private::NullType Type; 
			}; 
			typedef Tps::Private::TypeItem<50> TypeItemN; 
			char (& GetPSItemIndex(void *))[1];

Данный макрос определяет некоторую единицу, в которой и будут размещены Proxy/Stub'ы для указанного интерфейса. Так как классы не могут содержать вложенные шаблоны классов, а шаблонных классов в начинке будет немало, то используется пространство имен. При генерации Proxy в большей части и в меньшей Stub'ов будет создано некоторое количество разного рода вспомогательных сущностей, и чтобы не травмировать неокрепшую психику некоторых пользователей с тончайшей душевной организацией, которым intellisense их любимой IDE вывалит список непонятно чего, добавлено вложенное пространство имен PSImpl, в котором все действие и будет происходить. Так же данный макрос в PSImpl содержит typedef для переданного интерфейса; надо же какое-то одинаковое имя получить для дальнейшего использования. Остальные части макроса будут описаны ниже при рассмотрении TPS_METHOD.
Макрос TPS_METHOD разворачивается в макрос

TPS_METHOD_IMPL
#define TPS_METHOD_IMPL(mtd_) 
    typedef TPS_TYPEOF(&IFacePSType::mtd_) mtd_##MtdType; 
    typedef Tps::Private::Method<mtd_##MtdType> mtd_##MtdData; 
    TPS_CREATE_MTD_INDEX(mtd_) 
    TPS_MTD_PROXY_CLASS_IMPL(mtd_) 
    typedef mtd_##ProxyMtdWrap<Tps::Private::NullType, mtd_##MtdData, mtd_##MtdData::ParamsCount, !!mtd_##MtdData::IsConstMtd> mtd_##ProxyWrapType; 
    TPS_REG_PROXY_CLASS(mtd_##ProxyWrapType, mtd_##ID) 
    TPS_MTD_STUB_CLASS_IMPL(mtd_) 
    typedef mtd_##StubMtdWrap<mtd_##MtdData> mtd_##StubWrapType; 
    TPS_REG_STUB_CLASS(mtd_##StubWrapType, mtd_##ID)

Посмотрев на его код, в восторг не приходишь, что нормально при рассмотрении библиотечного кода с большим использованием макросов. Но не так страшен макрос, как его вид :) Посмотрим из чего он состоит:

  1. Получить тип метода интерфейса.
  2. Разобрать полученный тип метода и извлечь из него типы параметров, их количество, тип возвращаемого значение, cv квалификатор.
  3. Создать идентификатор метода.
  4. Реализовать Proxy-класс, содержащий реализацию метода интерфейса, который будет заниматься упаковкой / распаковкой параметров и отправлять запрос во вне.
  5. Зарегистрировать тип полученного Proxy-класса — сопоставить идентификатору метода.
  6. Выполнить аналогичные последним двум шагам действия для Stub'а.

Рассмотрим каждый шаг подробнее

Получить тип метода интерфейса

Имеем к примеру такой метод, а точнее указатель на метод:

&IFace::Mtd1

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

template <typename T>
void MyFunc(T)
{
    // TODO:
}

и передав в нее указатель на метод, можно получить его тип, но это функция, которая будет вызвана в момент выполнения программ, а хотелось бы получить тип метода на этапе компиляции. В С++11 есть прекрасная возможность для этого — decltype, а в C++03 придется подглянуть куда-нибудь, например в BOOST_TYPEOF, который для разных компиляторов реализован по разному. Так для MSVC++ 2010 уже можно использовать decltype, для GCC есть в нем расширение — typeof, а для MSVC++ версии более старой, чем 2010 придется делать что-то свое и это что-то было подсмотрено в BOOST_TYPEOF, которое основывается на использовании багофичи компилятора (адаптированный код для данной статьи расположен в файле tps_msvc_typeof.h). Определяем свой макрос TPS_TYPEOF, который и будет использован для получения типа метода интерфейса в такой конструкции:

typedef TPS_TYPEOF(&IFacePSType::mtd_) mtd_##MtdType;

Вот отсюда и ограничение, упомянутое в начале, что нельзя будет использовать перегрузку методов, так как наш макрос TPS_TYPEOF не умеет определять для какого из перегруженных методов он вызван.

Разобрать полученный тип метода и извлечь из него типы параметров, их количество, тип возвращаемого значение, cv квалификатор

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

Method

namespace Tps
{

	namespace Private
	{

		template <typename T>
		struct Method;

		template <typename R, typename C>
		struct Method<R (C::*)()>
		{
			enum { IsConstMtd = false };
			enum { ParamsCount = 0 };
			typedef C Class;
			typedef R Ret;
		};

		template <typename R, typename C, typename P1>
		struct Method<R (C::*)(P1)>
		{
			enum { IsConstMtd = false };
			enum { ParamsCount = 1 };
			typedef C Class;
			typedef R Ret;
			typedef P1 Param1;
		};

		template <typename R, typename C, typename P1, typename P2>
		struct Method<R (C::*)(P1, P2)>
		{
			enum { IsConstMtd = false };
			enum { ParamsCount = 2 };
			typedef C Class;
			typedef R Ret;
			typedef P1 Param1;
			typedef P2 Param2;
		};

		template <typename R, typename C>
		struct Method<R (C::*)() const>
		{
			enum { IsConstMtd = true };
			enum { ParamsCount = 0 };
			typedef C Class;
			typedef R Ret;
		};

		template <typename R, typename C, typename P1>
		struct Method<R (C::*)(P1) const>
		{
			enum { IsConstMtd = true };
			enum { ParamsCount = 1 };
			typedef C Class;
			typedef R Ret;
			typedef P1 Param1;
		};

		template <typename R, typename C, typename P1, typename P2>
		struct Method<R (C::*)(P1, P2) const>
		{
			enum { IsConstMtd = true };
			enum { ParamsCount = 2 };
			typedef C Class;
			typedef R Ret;
			typedef P1 Param1;
			typedef P2 Param2;
		};

	}

}

В примере кода приведен шаблон с максимальным количеством параметров метода равным двум, полный код приведен в файле tps_function.h. Используя данный шаблон разбираем тип метода интерфейса на запчасти

typedef Tps::Private::Method<mtd_##MtdType> mtd_##MtdData;
Создать идентификатор метода

Что использовать в качестве идентификатора метода? В начале я использовал __LINE__, решение вполне нормальное, но с двумя проблемами:

  • Большая зависимость от строки, в которой находится TPS_METHOD. Добавили комментарий в файл или любой код выше данного макроса, даже где-то далеко выше, номера строк поменялись, надо перекомпилировать и клиента и сервер, т.е. все части зависимые от данного Proxy/Stub'а.
  • При генерации списка реализаций методов (о чем будет подробнее рассказано ниже), возникают проблемы с «пустыми» типами если TPS_METHOD не идут строка за строкой без пустых строк и строк с комментариями. Эти пустые типы должен удалять библиотечный код из реализации.

После чего я отказался от использования __LINE__ как идентификатора метода, нужна была последовательность без разрывов, не зависящая от положения кода в файле. Нужна генерация последовательности чисел в момент компиляции.

Код, описывающий принцип получения последовательности констант в момент компиляции

#include <iostream>

template <unsigned N>
struct TypeItem
    : public TypeItem<N - 1>
{
};

template <>
struct TypeItem<0>
{
};

typedef TypeItem<50> TypeItemMax;

char (& GetIndex(void *))[1];

#define GET_NEXT_INDEX(item_) 
    enum { item_ = sizeof(GetIndex(static_cast<TypeItemMax *>(0))) }; 
    char (& GetIndex(TypeItem<item_ + 1> *))[item_ + 1];
    
GET_NEXT_INDEX(Item1)
GET_NEXT_INDEX(Item2)
GET_NEXT_INDEX(Item3)

int main()
{
    std::cout << Item1 << " " << Item2 << " " << Item3 << std::endl;
    return 0;
}

Если кратко описать словами принцип, то получается:

  • Сгенерировать некоторую иерархию классов.
  • Объявить функцию, принимающую void *, т.е. любой указатель и возвращающую ссылку на массив char размером 1.
  • На каждом шаге генерации константы получать sizeof от результата этой функции с переданным указателем на сгенерированную иерархию классов и тут же объявлять новую функцию для очередного типа в полученной иерархии классов с растущим размером возвращаемого массива

Все будет рассчитано в момент компиляции и функции могут иметь только объявления без определений.
Теперь можно вернуться к макросу TPS_BEGIN_MAP, думаю, теперь понятно для чего он содержит такие строки:

typedef Tps::Private::TypeItem<50> TypeItemN; 
char (& GetPSItemIndex(void *))[1];

это начало для генерации в момент компиляции идентификаторов методов интерфейса.

Реализовать Proxy-класс

Для каждого из методов интерфейса будем генерировать свой класс реализации этого метода и на основе этих кусочков соберем конечный класс Proxy-объекта.

Упрощенный код идеи

struct IFace
{
    virtual ~IFace()
    {
    }
    virtual void mtd1() = 0;
    virtual int mtd2(bool) const = 0;
};

struct mtd1_Impl
    : public virtual IFace
{
    virtual void mtd1()
    {
    }
};

struct mtd2_Impl
    : public virtual IFace
{
    virtual int mtd2(bool) const
    {
	return 0;
    }
};

class Proxy
    : public mtd1_Impl
    , public mtd2_Impl
{
};

Реальный код генерации Proxy классов-кирпичиков основан на частных специализациях шаблонов. Приведу код только для 0 и одного параметра метода интерфейса, полный код приведен в файле tps_mtd_class_impl.h. Макрос

TPS_MTD_PROXY_CLASS_IMPL

#define TPS_MTD_PROXY_CLASS_IMPL_MTD_BEGIN(mtd_) Ret mtd_(
#define TPS_MTD_PROXY_CLASS_IMPL_MTD_END() )

#define TPS_MTD_PROXY_CLASS_IMPL(mtd_) 
	template <typename ThisType, typename MtdType, unsigned ParamsCount, bool IsConstMtd> 
	struct mtd_##ProxyMtdWrap; 
	template <typename ThisType, typename MtdType> 
	struct mtd_##ProxyMtdWrap<ThisType, MtdType, 0, false> 
		: public virtual IFacePSType 
	{ 
	private: 
		typedef MtdType MtdTuple; 
		typedef typename MtdTuple::Ret Ret; 
        mutable Tps::Private::IDisposableHolder UnpackerHolder; 
	public: 
		TPS_MTD_PROXY_CLASS_IMPL_MTD_BEGIN(mtd_) TPS_MTD_PROXY_CLASS_IMPL_MTD_END() 
		{ 
			// Код, связанный с упаковкой, распаковкой данных
		} 
	}; 
	template <typename ThisType, typename MtdType> 
	struct mtd_##ProxyMtdWrap<ThisType, MtdType, 0, true> 
		: public virtual IFacePSType 
	{ 
	private: 
		typedef MtdType MtdTuple; 
		typedef typename MtdTuple::Ret Ret; 
        mutable Tps::Private::IDisposableHolder UnpackerHolder; 
	public: 
		TPS_MTD_PROXY_CLASS_IMPL_MTD_BEGIN(mtd_) TPS_MTD_PROXY_CLASS_IMPL_MTD_END() const 
		{ 
			// Код, связанный с упаковкой, распаковкой данных
		} 
	}; 
	template <typename ThisType, typename MtdType> 
	struct mtd_##ProxyMtdWrap<ThisType, MtdType, 1, false> 
		: public virtual IFacePSType 
	{ 
	private: 
		typedef MtdType MtdTuple; 
		typedef typename MtdTuple::Ret Ret; 
		typedef typename MtdTuple::Param1 Param1; 
        mutable Tps::Private::IDisposableHolder UnpackerHolder; 
	public: 
		TPS_MTD_PROXY_CLASS_IMPL_MTD_BEGIN(mtd_) Param1 prm1 TPS_MTD_PROXY_CLASS_IMPL_MTD_END() 
		{ 
			// Код, связанный с упаковкой, распаковкой данных
		} 
	}; 
	template <typename ThisType, typename MtdType> 
	struct mtd_##ProxyMtdWrap<ThisType, MtdType, 1, true> 
		: public virtual IFacePSType 
	{ 
	private: 
		typedef MtdType MtdTuple; 
		typedef typename MtdTuple::Ret Ret; 
		typedef typename MtdTuple::Param1 Param1; 
        mutable Tps::Private::IDisposableHolder UnpackerHolder; 
	public: 
		TPS_MTD_PROXY_CLASS_IMPL_MTD_BEGIN(mtd_) Param1 prm1 TPS_MTD_PROXY_CLASS_IMPL_MTD_END() const 
		{ 
			// Код, связанный с упаковкой, распаковкой данных
		} 
	}; 

При раскрытии этого макроса, появляется 2 + 2 * N специализаций шаблонного класса-реализации метода интерфейса, где N — максимальное количество поддерживаемых параметров реализацией, в данном случае N = 5. Вот для чего было введено пространство имен PSImpl, чтоб не захламлять ненужным выпадающий список intellisense'а его любимой IDE. Из всей этой кучи сгенерированных специализаций выбираем только одну, явно инстанцировав информацией, полученной при разборе типа метода интерфейса «на запчасти». Кирпич готов! Осталось этот кирпич зарегистрировать, т.е. полученному типу сопоставить идентификатор метода, сгенерированный в предыдущем шаге.

Регистрация типа полученного Proxy-класса (его кусочка)

И опять для реализации сопоставления тип — идентификатор нужен макрос. Ну пачкаться, так пачкаться :)

Код, описывающий принцип реестра типов

#include <iostream>

template <unsigned N>
struct RegItem;

#define REG_TYPE(type_, id_) 
    template <> 
    struct RegItem<id_> 
    { 
	typedef type_ Type; 
    };

#define GET_TYPE_BY_ID(id_) 
    typename RegItem<id_>::Type
    
class A
{
public:
    A()
    {
	std::cout << "A" << std::endl;
    }
};

class B
{
public:
    B()
    {
	std::cout << "B" << std::endl;
    }
};

REG_TYPE(A, 1)
REG_TYPE(B, 2)

int main()
{
    GET_TYPE_BY_ID(1) t1;
    GET_TYPE_BY_ID(2) t2;
    return 0;
}

Тут все до безобразия просто: с помощью макроса делаем частные специализации для шаблона. Макрос нужен чтоб тип «протащить». Получение типа по идентификатору — это всего лишь инстанцирование одной из полученных специализаций идентификатором. На этом принципе и построены макросы

TPS_REG_PROXY_CLASS

#define TPS_REG_PROXY_CLASS(class_, id_) 
	template <> 
	struct ProxyRegItem<id_> 
	{ 
		typedef class_ Type; 
	};

и

TPS_REG_STUB_CLASS

#define TPS_REG_STUB_CLASS(class_, id_) 
	template <> 
	struct StubRegItem<id_> 
	{ 
		typedef class_ Type; 
	};

и макрос TPS_BEGIN_MAP_IMPL стал окончательно понятен, для чего в нем эти строки

эти строки

struct ProxyRegItem 
{ 
	typedef Tps::Private::NullType Type; 
}; 
template <unsigned id> 
struct StubRegItem 
{ 
	typedef Tps::Private::NullType Type; 
}; 
Генерация и регистрация Stub'ов

Как регистрировать Stub только что было описано выше, а вот генерация Stub'а отличается от генерации Proxy, так как тут нет большого количества специализаций, так как не нужны, ну это они в макросе не нужны, они уйдут в иной библиотечный код уже не на макросах. Макрос для генерации Stub-метода

TPS_MTD_STUB_CLASS_IMPL

#define TPS_MTD_STUB_CLASS_IMPL(mtd_) 
	template <typename MtdType> 
	struct mtd_##StubMtdWrap 
	{ 
		typedef MtdType MethodType; 
		enum { StubMtdId = mtd_##ID }; 
		template <typename IFaceType, typename UnpackerType> 
		static typename MethodType::Ret Call(IFaceType *iface, UnpackerType *unpacker) 
		{ 
			return Tps::Private::CallStub<MtdType>(iface, &IFaceType::mtd_, unpacker); 
		} 
	};

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

Макрос TPS_END_MAP разворачивается в макрос

TPS_END_MAP_IMPL

#define TPS_END_MAP_IMPL() 
        TPS_CREATE_MTD_INDEX(EndMtdIndex) 
        typedef Tps::Private::CreateMtdTypeList<ProxyRegItem, 1, EndMtdIndexID>::List ProxyList; 
        typedef Tps::Private::CreateMtdTypeList<StubRegItem, 1, EndMtdIndexID>::List StubList; 
    } 
     
    template <typename RemoteMethod> 
    class Proxy 
        : public Tps::Private::InheritFromProxyList<Proxy<RemoteMethod>, PSImpl::ProxyList> 
    { 
    public: 
        // Некоторый малоинтересный код по формированию буфера, отправки его и разбору результата от Stub'а 
    }; 
    
    template <typename RemoteMethod> 
    class Stub 
        : private Tps::Private::NonCopyable 
    { 
    public: 
        // Некоторый малоинтересный код для работы с принятым буфером от Proxy
        typedef PSImpl::StubList Stubs; 
    }; 
}

В этом макросе формируются два списка типов, состоящих из «кирпичиков» — реализаций каждого метода для Proxy (в ProxyList) реализаций для Stub'ов (StubList), каждая из которых предназначена для вызова соответствующего метода реальной реализации интерфейса.
Класс Proxy наследует все элементы списка ProxyList, Класс Stub не нуждается в наследовании от списка Stub'ов, он просто содержит этот список (StubList) и перебирает его при поиске нужного Stub'а для вызова.

Упрощенный пример наследования класса от каждого элемента списка типов

#include <iostream>

template <typename H, typename T>
struct TypeList
{
  typedef H Head;
  typedef T Tail;
};
struct NullType
{
};

#define TYPE_LIST_1(t1) 
  TypeList<t1, NullType>

#define TYPE_LIST_2(t1, t2) 
  TypeList<t1, TYPE_LIST_1(t2)>

template <typename T>
  struct InheritedFromList
    : public T::Head
    , public InheritedFromList<typename T::Tail>
{
};
template <>
struct InheritedFromList<NullType>
{
};
class A
{
public:
  void m1() { std::cout << "A::m1" << std::endl; }
};
class B
{
public:
  void m2() { std::cout << "B::m2" << std::endl; }
};
class C
  : public InheritedFromList<TYPE_LIST_2(A, B)>
{
public:
  void m3() { std::cout << "C::m3" << std::endl; }
};

int main()
{
  C c;
  c.m1();
  c.m2();
  c.m3();
  return 0;
}

можно посмотреть в моей предыдущей статье. Но в данной реализации наследования класса Proxy от списка реализаций методов интерфейса есть одна тонкость — код класса Proxy содержит методы для работы по формированию буфера для отправки, его отправки и разбор результата; этот код должен быть доступен во всей иерархии наследования, во всех базовых класса, т.е. применить прием аналогичный

приему так сильно любимом в WTL к примеру

template <typename T>
class A
{
public:
	void m1()
	{
		static_cast<T *>(this)->m2();
	}
};

class B
	: public A<B>
{
public:
	void m2()
	{
	}
};

Если посмотреть на каждый элемент списка наследования для Proxy класса, то можно увидеть следующее

// кусок макроса
#define TPS_MTD_PROXY_CLASS_IMPL(mtd_) 
	template <typename ThisType, typename MtdType, unsigned ParamsCount, bool IsConstMtd> 

// И кусок макроса TPS_METHOD_IMPL
typedef mtd_##ProxyMtdWrap<Tps::Private::NullType, mtd_##MtdData, mtd_##MtdData::ParamsCount, !!mtd_##MtdData::IsConstMtd> mtd_##ProxyWrapType; 

Получается, что вместо Proxy класса-потомка при инстанцировании нужной специализации всем навязан Tps::Private::NullType. Ну чем-то же надо было заполнить «пустоту», к тому же Proxy — это шаблон. Выкрутиться с Forward declaration как-то с ходу не получилось, да и не ставил себе такую цель, так как Proxy специализируется гораздо позже. И информацию для специализации придется таскать очень много и «вовремя» специализировать нужным параметром. Меняем Tps::Private::NullType в каждом элементе списка ProxyList'а на инстанцированный Proxy, а сделать это можно примерно так:

template <typename T, typename NewThisType>
struct MtdWrapTypeThisChanger;
template
<
	template <typename, typename, unsigned, bool> class WrapType,
	typename ThisType, typename MtdType, unsigned ParamsCount, bool IsConstMtd,
	typename NewThisType
>
struct MtdWrapTypeThisChanger<WrapType<ThisType, MtdType, ParamsCount, IsConstMtd>, NewThisType>
{
	typedef WrapType<NewThisType, MtdType, ParamsCount, IsConstMtd> Type;
};
template <typename TThis, typename TBase>
struct InheritFromProxyList
	: public InheritFromProxyList<TThis, typename TBase::Tail>
	, public MtdWrapTypeThisChanger<typename TBase::Head, TThis>::Type
{
	virtual ~InheritFromProxyList()
	{
	};
};
template <typename TThis>
struct InheritFromProxyList<TThis, NullType>
{
	virtual ~InheritFromProxyList()
	{
	};
};

Заключение

Основные принципы раскрыты, можно еще рассматривать детали сериализации и десериализации, но это уже не интересно.
Тестовый пример приведен в файле main.cpp.
Весь проект лежит в svn. Код кроссплатформенный. Пробовал собирать на gcc 4.7, MSVC++2005, MSVC++2010.
Как и было отмечено в начале — статья не претендует на законченный framework и более того не содержит IPC механизмов, ее реализация доведена до момента формирования буфера и разбора буфера, но подложив сюда любой механизм IPC: сокеты, каналы и т.д. (в реализацию класса Transport из тестового кода в main.cpp) можно получить полноценное IPC/RPC основанное на интерфейсах с возможностью, на мой взгляд, весьма просто описывать Proxy/Stub'ы для методов.
Теперь у меня есть два колеса: одно от мопеда, второе от детской коляски (это я про предыдущую статью и текущую), осталось разжиться велосипедной рамой и попробовать из этого собрать какое-нибудь транспортное средство :) т.е. вполне может быть появится статья о полноценной компонентной модели, основанной на интерфейсах, в которой можно будет передавать интерфейсы между процессами, а не использовать только в рамках одного процесса.

Cпасибо за внимание!

Автор: NYM

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js