Мінімалізм віддаленого взаємодії на C + +11

  Деякий час тому мною був опублікований пост про створення власної системи плагінів на 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, а так само всі можна буде протестувати «з коробки» на реальному тестовому сервері.
 
 

Реалізація

 В основу реалізації лягли матеріали вже опубліковані мною раніше: пост про реалізацію 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 <iostream>
#include <string>

#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 <iostream>
#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_shared<Remote::Http::Remoting>("127.0.0.1", 5555);
    auto Factory = Remote::ClassFactory::Create // Создание фабрики классов.
          <
            Remote::Pkg::Xml::OutputPack, // Тип для сериализации исходящих пакетов.
            IFace // Список поддерживаемых интерфейсов. В данном примере один интерфейс.
          >(Remoting);
    // Создание объекта с интерфейсом IFace и идентификатором реализации на стороне сервера FaceClsId.
    auto Obj = Factory->Create<IFace>(FaceClsId);
    // Вызов методов объекта на сервере.
    Obj->Mtd2(10);
  }
  catch (std::exception const &e)
  {
    std::cerr << e.what() << std::endl;
  }
  return 0;
}

При створенні фабрики класів їй потрібно передати об'єкт, який реалізує транспорт і список інтерфейсів, які вона підтримує. За цим списком інтерфейсів фабрика для створеного на сервері об'єкта у себе визначає проксі-об'єкт, яким користувач буде користуватися для виклику методів інтерфейсу.
Можна помітити з прикладу, що фабрика не є класом-шаблоном, а тільки її методи створення самої фабрики і об'єктів є шаблонними. Навіщо? Якби фабрика була шаблоном, то по всіх одиницям трансляції потрібно було б «тягати» за собою всю інформацію про її типах, з якими вона працює. А так можна скористатися forward declaration і передавши покажчик на фабрику в інший файл проекту, в якому вже не треба знати і підключати всі файли з інформацією про тип, що виробляє сериализацию і десереалізацію пакетів і файлів з інтерфейсами, використання яких в тій частині проекту не потрібно.
 
Приклади наведені, дані пояснення для чого зроблено те чи інше рішення, можна переходити до технічних аспектів реалізації, а реалізація ще раз повторюся поділена на дві великі частини:
 
     
  • Створення Proxy / Stub 'ов
  •  
  • Інфраструктура та транспорт
  •  
 
Proxy / Stubs
 При виникненні необхідності в поділі об'єкта та його використовує коду між клієнтом і сервером, в цей момент з'являється необхідність в проксі-об'єктах (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 <std::uint32_t, typename>
  struct Method;
}

#define DECLARE_PROXY_METHOD(iface_, mtd_, id_) \
  namespace Methods \
  { \
    typedef decltype(&iface_::mtd_) mtd_##Type; \
    template <typename R, typename C, typename ... P> \
    struct Method<id_, R (C::*)(P ...)> \
      : public virtual iface_ \
    { \
      virtual R mtd_ (P ... p) \
      { \
        throw std::runtime_error("Not implemented."); \
      } \
    }; \
    template <typename R, typename C, typename ... P> \
    struct Method<id_, R (C::*)(P ...) const> \
      : public virtual iface_ \
    { \
      virtual R mtd_ (P ... p) const \
      { \
        throw std::runtime_error("Not implemented."); \
      } \
    }; \
    typedef Method<id_, mtd_##Type> mtd_##ProxyType; \
  }

Можна помітити, що макрос визначає дві спеціалізації: для константного і неконстантного методів, і тільки одна буде з них інстанціювати для конкретного методу. За допомогою цього макросу і всього що вже отримано раніше можна зібрати вже готовий проксі-клас:
 
 
DECLARE_PROXY_METHOD(ISessionManager, OpenSession, 1)
DECLARE_PROXY_METHOD(ISessionManager, CloseSession, 2)
DECLARE_PROXY_METHOD(ISessionManager, IsValidSession, 3)

template <typename ... T>
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 <typename T>
    struct Crc32TableWrap
    {      
      static constexpr std::uint32_t const Table[256] =
      {
        0x00000000L, 0x77073096L, 0xee0e612cL, 0x990951baL, 0x076dc419L,
        0x706af48fL, 0xe963a535L, 0x9e6495a3L, 0x0edb8832L, 0x79dcb8a4L,
        0xe0d5e91eL, 0x97d2d988L, 0x09b64c2bL, 0x7eb17cbdL, 0xe7b82d07L,
        // И т.д. заполнение таблицы
      };
    };
    
    template <typename T>
    constexpr std::uint32_t const Crc32TableWrap<T>::Table[256];
    
    typedef Crc32TableWrap<void> Crc32Table;
    
    template<std::uint32_t const I>
    inline constexpr std::uint32_t Crc32Impl(char const *str)
    {
      return (Crc32Impl <I - 1>(str) >> 8) ^
        Crc32Table::Table[(Crc32Impl<I - 1>(str) ^ str[I - 1]) & 0x000000FF];
    }
    
    template<>
    inline constexpr std::uint32_t Crc32Impl<0>(char const *)
    {
      return 0xFFFFFFFF;
    }
    
  }
  
  template <std::uint32_t N>
  inline constexpr std::uint32_t Crc32(char const (&str)[N])
  {
    return (Private::Crc32Impl<sizeof(str)>(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 <std::uint32_t, typename>
  struct Method;
}

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

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

template <typename ... T>
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 <std::uint32_t>
struct TypeRegistry;

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

Вельми проста реалізація знову заснована на приватних спеціалізаціях. Додавши це в раніше розглянутий макрос визначення методу він наблизиться ще на крок до кінцевої мети.
 
 Оновлений макрос і приклад його використання
#define DECLARE_TYPE_REGISTRY(reg_name_) \
  namespace reg_name_ \
  { \
    template <std::uint32_t> \
    struct TypeRegistry; \
  }

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

DECLARE_TYPE_REGISTRY(Methods)

namespace Methods
{
  template <std::uint32_t, typename>
  struct Method;
}

#define DECLARE_PROXY_METHOD(iface_, mtd_, id_) \
  namespace Methods \
  { \
    enum {mtd_##Id = Crc32(#mtd_)}; \
    typedef decltype(&iface_::mtd_) mtd_##Type; \
    template <typename R, typename C, typename ... P> \
    struct Method<mtd_##Id, R (C::*)(P ...)> \
      : public virtual iface_ \
    { \
      virtual R mtd_ (P ... p) \
      { \
        throw std::runtime_error("Not implemented."); \
      } \
    }; \
    template <typename R, typename C, typename ... P> \
    struct Method<mtd_##Id, R (C::*)(P ...) const> \
      : public virtual iface_ \
    { \
      virtual R mtd_ (P ... p) const \
      { \
        throw std::runtime_error("Not implemented."); \
      } \
    }; \
    typedef Method<mtd_##Id, mtd_##Type> 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 <typename ... T>
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 <unsigned N>
  struct Hierarchy
    : public Hierarchy<N - 1>
  {
  };
  
  template <>
  struct Hierarchy<0>
  {
  };
}

#define INIT_STATIC_COUNTER(counter_name_, max_count_) \
  namespace counter_name_ \
  { \
    typedef ::Private::Hierarchy<max_count_> 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<CounterHierarchyType const *>(0))) }; \
    char (&GetCounterValue(::Private::Hierarchy<value_name_> 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 <iostream>
namespace Private
{
  
  template <unsigned N>
  struct Hierarchy
    : public Hierarchy<N - 1>
  {
  };
  
  template <>
  struct Hierarchy<0>
  {
  };
}
namespace MyCounter
{
  typedef ::Private::Hierarchy<100> CounterHierarchyType; char (&GetCounterValue(void const *))[1];
}

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

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

namespace MyCounter
{
  enum
  {
    Item3 = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
  };
  char (&GetCounterValue(::Private::Hierarchy<Item3> 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 <std::uint32_t, typename> \
        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 <typename R, typename C, typename ... P> \
    struct Method<mtd_##Id, R (C::*)(P ...)> \
      : public virtual IFaceType \
    { \
      virtual R mtd_ (P ... p) \
      { \
        throw std::runtime_error(#mtd_ " not implemented."); \
      } \
    }; \
    template <typename R, typename C, typename ... P> \
    struct Method<mtd_##Id, R (C::*)(P ...) const> \
      : public virtual IFaceType \
    { \
      virtual R mtd_ (P ... p) const \
      { \
        throw std::runtime_error(#mtd_ " not implemented."); \
      } \
    }; \
    typedef Method<mtd_##Id, mtd_##Type> mtd_##ProxyType; \
    REGIDTER_TYPE(ProxiesReg, MtdCounter::mtd_##Counter, mtd_##ProxyType) \
  }

#define END_PROXY_MAP() \
      GET_NEXT_STATIC_COUNTER(MtdCounter, LastCounter) \
      template <unsigned I> \
      class ProxyItem \
        : public Methods::ProxiesReg::TypeRegistry<I>::Type \
        , public ProxyItem<I - 1> \
      { \
      }; \
      template <> \
      class ProxyItem<0> \
      { \
      }; \
    } \
    typedef Impl::ProxyItem<Impl::MtdCounter::LastCounter - 1> Proxy; \
  }

Допоміжний код
namespace Private
{
  template <unsigned N>
  struct Hierarchy
    : public Hierarchy<N - 1>
  {
  };
  
  template <>
  struct Hierarchy<0>
  {
  };
}

#define INIT_STATIC_COUNTER(counter_name_, max_count_) \
  namespace counter_name_ \
  { \
    typedef ::Private::Hierarchy<max_count_> 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<CounterHierarchyType const *>(0))) }; \
    char (&GetCounterValue(::Private::Hierarchy<value_name_> const *))[value_name_ + 1]; \
  }

#define DECLARE_TYPE_REGISTRY(reg_name_) \
  namespace reg_name_ \
  { \
    template <std::uint32_t> \
    struct TypeRegistry; \
  }

#define REGIDTER_TYPE(reg_name_, id_, type_) \
  namespace reg_name_ \
  { \
    template <> \
    struct TypeRegistry<id_> \
    { \
      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 <unsigned N>
  struct Hierarchy
    : public Hierarchy<N - 1>
  {
  };
  
  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 <std::uint32_t>
        struct TypeRegistry;
      }
      template <std::uint32_t, typename>
      struct Method;
    }
    namespace MtdCounter
    {
      enum
      {
        OpenSessionCounter = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
      };
      char (&GetCounterValue(::Private::Hierarchy<OpenSessionCounter> const *))[OpenSessionCounter + 1];
    }
    namespace Methods
    {
      enum
      {
        OpenSessionId = Crc32("OpenSession")
      };
      typedef decltype(&IFaceType::OpenSession) OpenSessionType;
      template <typename R, typename C, typename ... P>
      struct Method<OpenSessionId, R (C::*)(P ...)>
        : public virtual IFaceType
      {
        virtual R OpenSession (P ... p)
        {
          throw std::runtime_error("OpenSession" " not implemented.");
        }
      };
      template <typename R, typename C, typename ... P>
      struct Method<OpenSessionId, R (C::*)(P ...) const>
        : public virtual IFaceType
      {
        virtual R OpenSession (P ... p) const
        {
          throw std::runtime_error("OpenSession" " not implemented.");
        }
      };
      typedef Method<OpenSessionId, OpenSessionType> OpenSessionProxyType;
      namespace ProxiesReg
      {
        template <>
        struct TypeRegistry<MtdCounter::OpenSessionCounter>
        {
          typedef OpenSessionProxyType Type;
        };
      }
    }
    namespace MtdCounter
    {
      enum
      {
        CloseSessionCounter = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
      };
      char (&GetCounterValue(::Private::Hierarchy<CloseSessionCounter> const *))[CloseSessionCounter + 1];
    }
    namespace Methods
    {
      enum
      {
        CloseSessionId = Crc32("CloseSession")
      };
      typedef decltype(&IFaceType::CloseSession) CloseSessionType;
      template <typename R, typename C, typename ... P>
      struct Method<CloseSessionId, R (C::*)(P ...)>
        : public virtual IFaceType
      {
        virtual R CloseSession (P ... p)
        {
          throw std::runtime_error("CloseSession" " not implemented.");
        }
      };
      template <typename R, typename C, typename ... P>
      struct Method<CloseSessionId, R (C::*)(P ...) const>
        : public virtual IFaceType
      {
        virtual R CloseSession (P ... p) const
        { 
          throw std::runtime_error("CloseSession" " not implemented.");
        }
      };
      typedef Method<CloseSessionId, CloseSessionType> CloseSessionProxyType;
      namespace ProxiesReg
      {
        template <>
        struct TypeRegistry<MtdCounter::CloseSessionCounter>
        {
          typedef CloseSessionProxyType Type;
        };
      }
    }
    namespace MtdCounter
    {
      enum
      {
        IsValidSessionCounter = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
      };
      char (&GetCounterValue(::Private::Hierarchy<IsValidSessionCounter> const *))[IsValidSessionCounter + 1];
    }
    namespace Methods
    {
      enum
      {
        IsValidSessionId = Crc32("IsValidSession")
      };
      typedef decltype(&IFaceType::IsValidSession) IsValidSessionType;
      template <typename R, typename C, typename ... P>
      struct Method<IsValidSessionId, R (C::*)(P ...)>
        : public virtual IFaceType
      {
        virtual R IsValidSession (P ... p)
        {
          throw std::runtime_error("IsValidSession" " not implemented.");
        }
      };
      template <typename R, typename C, typename ... P>
      struct Method<IsValidSessionId, R (C::*)(P ...) const>
        : public virtual IFaceType
      {
        virtual R IsValidSession (P ... p) const
        {
          throw std::runtime_error("IsValidSession" " not implemented.");
        }
      };
      typedef Method<IsValidSessionId, IsValidSessionType> IsValidSessionProxyType;
      namespace ProxiesReg
      {
        template <>
        struct TypeRegistry<MtdCounter::IsValidSessionCounter>
        {
          typedef IsValidSessionProxyType Type;
        };
      }
    }
    namespace MtdCounter
    {
      enum
      {
        LastCounter = sizeof(GetCounterValue(static_cast<CounterHierarchyType const *>(0)))
      };
      char (&GetCounterValue(::Private::Hierarchy<LastCounter> const *))[LastCounter + 1];
    }
    template <unsigned I>
    class ProxyItem
        : public Methods::ProxiesReg::TypeRegistry<I>::Type
        , public ProxyItem<I - 1>
    {
    };
    template <>
    class ProxyItem<0>
    {
    };
  }
  typedef Impl::ProxyItem<Impl::MtdCounter::LastCounter - 1> 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 <typename T>
class ProxyMethodBase
{
public:
  template <typename R, typename ... P>
  R Execute(std::uint32_t mtdId, P ... params)
  {
    return dynamic_cast<T &>(*this).Execute<R, P ...>(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 <std::uint32_t, typename> \
        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 <typename R, typename C, typename ... P> \
    struct Method<mtd_##Id, R (C::*)(P ...)> \
      : public virtual IFaceType \
      , public virtual ProxyMethodBase<ProxyImpl> \
    { \
      virtual R mtd_ (P ... p) \
      { \
        return Execute<R, P ...>(mtd_##Id, p ...); \
      } \
    }; \
    template <typename R, typename C, typename ... P> \
    struct Method<mtd_##Id, R (C::*)(P ...) const> \
      : public virtual IFaceType \
      , public virtual ProxyMethodBase<ProxyImpl> \
    { \
      typedef Method<mtd_##Id, R (C::*)(P ...) const> ThisType; \
      virtual R mtd_ (P ... p) const \
      { \
        return const_cast<ThisType *>(this)->Execute<R, P ...>(mtd_##Id, p ...); \
      } \
    }; \
    typedef Method<mtd_##Id, mtd_##Type> mtd_##ProxyType; \
    REGIDTER_TYPE(ProxiesReg, MtdCounter::mtd_##Counter, mtd_##ProxyType) \
  }

END_PROXY_MAP
#define END_PROXY_MAP() \
      GET_NEXT_STATIC_COUNTER(MtdCounter, LastCounter) \
      template <unsigned I> \
      class ProxyItem \
        : public Methods::ProxiesReg::TypeRegistry<I>::Type \
        , public ProxyItem<I - 1> \
      { \
      }; \
      template <> \
      class ProxyItem<0> \
      { \
      }; \
      class ProxyImpl \
        : public ProxyItem<MtdCounter::LastCounter - 1> \
      { \
      public: \
      private: \
        friend class ProxyMethodBase<ProxyImpl>; \
        template <typename R, typename ... P> \
        R Execute(std::uint32_t mtdId, P ... params) \
        { \
          throw std::runtime_error("Not implemented."); \
        } \
      }; \
    } \
    typedef Impl::ProxyImpl Proxy; \
  }

Користувальницький код ж ніяк не змінюється. Водночас у реалізації проксі-класу з'явилася одна локалізована точка, в якій можна проводити всі маніпуляції з параметрами і роботу з сервером. Якщо подивитися на метод проксі-класу, куди в результаті приходить виклик методу інтерфейсу

template <typename R, typename ... P>
R Execute(std::uint32_t mtdId, P ... params)
{
  // ...
}

то тут вже можна скористатися класичною схемою перебору всіх параметрів викликаного методу для їх сериализации, яка практично у всіх джерелах, присвячених C + +11 наводиться на прикладі реалізації типу безпечної функції printf.

template <typename S>
void Serialize(S &)
{
}

template <typename S, typename T, typename ... P>
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 <std::uint32_t, typename> \
        struct ProxyMethod; \
        DECLARE_TYPE_REGISTRY(StubsReg) \
        template <std::uint32_t, typename> \
        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 <typename R, typename C, typename ... P> \
    struct ProxyMethod<mtd_##Id, R (C::*)(P ...)> \
      : public virtual IFaceType \
      , public virtual ProxyMethodBase<ProxyImpl> \
    { \
      virtual R mtd_ (P ... p) \
      { \
        return Execute<R, P ...>(mtd_##Id, p ...); \
      } \
    }; \
    template <typename R, typename C, typename ... P> \
    struct ProxyMethod<mtd_##Id, R (C::*)(P ...) const> \
      : public virtual IFaceType \
      , public virtual ProxyMethodBase<ProxyImpl> \
    { \
      typedef ProxyMethod<mtd_##Id, R (C::*)(P ...) const> ThisType; \
      virtual R mtd_ (P ... p) const \
      { \
        return const_cast<ThisType *>(this)->Execute<R, P ...>(mtd_##Id, p ...); \
      } \
    }; \
    typedef ProxyMethod<mtd_##Id, mtd_##Type> mtd_##ProxyType; \
    REGIDTER_TYPE(ProxiesReg, MtdCounter::mtd_##Counter, mtd_##ProxyType) \
    template <typename R, typename C, typename ... P> \
    struct StubMethod<mtd_##Id, R (C::*)(P ...)> \
      : public virtual StubMethodBase<StubImpl> \
    { \
      template <typename TPkg> \
      R Call(TPkg &pack) \
      { \
        return Call(mtd_##Id, pack, &IFaceType::mtd_); \
      } \
    }; \
    template <typename R, typename C, typename ... P> \
    struct StubMethod<mtd_##Id, R (C::*)(P ...) const> \
      : public virtual StubMethodBase<StubImpl> \
    { \
      template <typename TPkg> \
      R Call(TPkg &pack) \
      { \
        return Call(mtd_##Id, pack, &IFaceType::mtd_); \
      } \
    }; \
    typedef StubMethod<mtd_##Id, mtd_##Type> mtd_##StubType; \
    REGIDTER_TYPE(StubsReg, MtdCounter::mtd_##Counter, mtd_##StubType) \
  }

PS_END_MAP
#define PS_END_MAP() \
      GET_NEXT_STATIC_COUNTER(MtdCounter, LastCounter) \
      template <unsigned I> \
      class ProxyItem \
        : public Methods::ProxiesReg::TypeRegistry<I>::Type \
        , public ProxyItem<I - 1> \
      { \
      }; \
      template <> \
      class ProxyItem<0> \
      { \
      }; \
      class ProxyImpl \
        : public ProxyItem<MtdCounter::LastCounter - 1> \
      { \
      public: \
      private: \
        friend class ProxyMethodBase<ProxyImpl>; \
        template <typename R, typename ... P> \
        R Execute(std::uint32_t mtdId, P ... params) \
        { \
          Serialize(std::cout, params ...); \
          throw std::runtime_error("Not implemented."); \
        } \
      }; \
      template <unsigned I> \
      class StubItem \
        : public Methods::StubsReg::TypeRegistry<I>::Type \
        , public StubItem<I - 1> \
      { \
      }; \
      template <> \
      class StubItem<0> \
      { \
      }; \
      class StubImpl \
        : public StubItem<MtdCounter::LastCounter - 1> \
      { \
      public: \
      private: \
        friend class StubMethodBase<StubImpl>; \
        template <typename C, typename R, typename ... P> \
        R Call(std::uint32_t mtdId, R (C::*mtd)(P ...)) \
        { \
          throw std::runtime_error("Not implenented."); \
        } \
      }; \
    } \
    typedef Impl::ProxyImpl Proxy; \
    typedef Impl::StubImpl Stub; \
  }

Користувальницький код
ruct 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;
};

PS_BEGIN_MAP(ISessionManager)
  PS_ADD_METHOD(OpenSession)
  PS_ADD_METHOD(CloseSession)
  PS_ADD_METHOD(IsValidSession)
PS_END_MAP()

int main()
{
  try
  {
    ISessionManagerPS::Proxy Proxy;
    ISessionManagerPS::Stub Stub;
  }
  catch (std::exception const &e)
  {
    std::cerr << e.what() << std::endl;
  }
  return 0;
}

Трохи помінялися імена макросів і деяких сутностей під ними і доданий код з формування класів-заглушок. Тепер це повноцінне опис Proxy / Stub, яке можна використовувати при реалізації власного RPC на основі інтерфейсів. У рамках же даного поста буде приведена деяка інфраструктура і робота з транспортним рівнем, що дасть запропонованим ідеям завершеність і можливість їх використання як готового продукту. А поки ще раз загострити увагу на деяких відмінностях реалізації класів-заглушок від проксі-класів. Якщо заглянути в макрос PS_ADD_METHOD, то можна побачити, що в нього додані приватні спеціалізації для реалізації класів, які реалізують заглушки для кожного з методів. Відмінність же таких реалізацій заглушок від проксі в тому, що тип, який реалізує один з методів проксі успадковується від інтерфейсу, а тип, який реалізує метод заглушки такого наслідування не має, так як воно йому не потрібно; при реалізації заглушки потрібно отримати адресу методу інтерфейсу, який буде надалі викликаний. Основні відмінності, взяті з макросу PS_ADD_METHOD наведені нижче.

ProxyMethod
template <typename R, typename C, typename ... P> \
struct ProxyMethod<mtd_##Id, R (C::*)(P ...)> \
  : public virtual IFaceType \
  , public virtual ProxyMethodBase<ProxyImpl> \
{ \
  virtual R mtd_ (P ... p) \
  { \
    return Execute<R, P ...>(mtd_##Id, p ...); \
  } \
}; \
template <typename R, typename C, typename ... P> \
struct ProxyMethod<mtd_##Id, R (C::*)(P ...) const> \
  : public virtual IFaceType \
  , public virtual ProxyMethodBase<ProxyImpl> \
{ \
  typedef ProxyMethod<mtd_##Id, R (C::*)(P ...) const> ThisType; \
  virtual R mtd_ (P ... p) const \
  { \
    return const_cast<ThisType *>(this)->Execute<R, P ...>(mtd_##Id, p ...); \
  } \
}; \

StubMethod
template <typename R, typename C, typename ... P> \
struct StubMethod<mtd_##Id, R (C::*)(P ...)> \
  : public virtual StubMethodBase<StubImpl> \
{ \
  template <typename TPkg> \
  R Call(TPkg &pack) \
  { \
    return Call(mtd_##Id, pack, &IFaceType::mtd_); \
  } \
}; \
template <typename R, typename C, typename ... P> \
struct StubMethod<mtd_##Id, R (C::*)(P ...) const> \
  : public virtual StubMethodBase<StubImpl> \
{ \
  template <typename TPkg> \
  R Call(TPkg &pack) \
  { \
    return Call(mtd_##Id, pack, &IFaceType::mtd_); \
  } \
}; \

Залишилося розглянути ще одну деталь відноситься до класів-заглушок — виклик методу класу-реалізації інтерфейсу з класу-заглушки. При цьому залишається вирішити завдання зворотну завданню формування пакета з параметрів: пакет параметрів якось «випрямити» до списку параметрів викликається методу інтерфейсу, який реалізовано у відповідному класі-реалізації. У прикладених вихідних кодах до посту у файлі ps.h наведено повний код того як з деякого пакета даних витягуються параметри і після побудови списку параметрів робиться виклик. Тут, щоб не захаращувати нюансами, наведу приклад аналогічної дії. Припустимо є деякий клас з методом, який приймає два цілочисельних параметра і виводить їх на екран.

class MyClass
{
public:
  void Func(int prm1, int prm2)
  {
    std::cout << "Prm1: " << prm1 << std::endl;
    std::cout << "Prm2: " << prm2 << std::endl;
  }
};

І є деякий масив даних, в даному випадку це вектор з пари цілочисельних значень. Потрібно написати код, який би з вектора даних формував список параметрів і викликав метод. Це можна зробити приблизно таким способом.

Приклад
class MyClass
{
public:
  void Func(int prm1, int prm2)
  {
    std::cout << __FUNCTION__ << std::endl;
    std::cout << "Prm1: " << prm1 << std::endl;
    std::cout << "Prm2: " << prm2 << std::endl;
  }
};

template <typename R, typename C, typename ... P>
constexpr unsigned GetParamCount(R (C::*)(P ...))
{
  return sizeof ... (P);
}
template <unsigned I>
struct Mtd
{
  template <typename TCtr, typename C, typename M, typename ... P>
  static void Call(TCtr const &ctr, C &obj, M mtd, P ... p)
  {
    auto Prm = ctr[I - 1];
    Mtd<I - 1>::Call(ctr, obj, mtd, Prm, p ... );
  };
};

template <>
struct Mtd<0>
{
  template <typename TCtr, typename C, typename M, typename ... P>
  static void Call(TCtr const &ctr, C &obj, M mtd, P ... p)
  {
    (obj.*mtd)(p ...);
  };
};

int main()
{
  std::vector<int> Data( {10, 20} );
  MyClass Obj;
  Mtd<GetParamCount(&MyClass::Func)>::Call(Data, Obj, &MyClass::Func);
  return 0;
}

Аналогічна реалізація лежить в запропонованій реалізації віддаленого взаємодії при реалізації класів-заглушок для виклику методу об'єкта на сервері з отриманого пакету даних від клієнта.

У висновку частини про Proxy / Stub 'ах пропоную поглянути на реальні макроси (можна по діагоналі, тому що вони трохи громіздкі), які використовуються при реалізації Proxy / Stub для віддаленого взаємодії клієнта з сервером.

PS_BEGIN_MAP
#define PS_BEGIN_MAP(iface_) \
  namespace iface_##_PS \
  { \
    DECLARE_RUNTIME_EXCEPTION(Proxy) \
    DECLARE_RUNTIME_EXCEPTION(Stub) \
    namespace PSImpl \
    { \
      template <typename> \
      class Proxy; \
      class Stub; \
      typedef iface_ IFaceType; \
      enum { IFaceId = ::Remote::Crc32(#iface_) }; \
      namespace Methods \
      { \
        INIT_STATIC_COUNTER(PSCounter, PS_MAX_METHOD_COUNT) \
        namespace MethodsProxy \
        { \
          typedef Proxy<void> ProxyClass; \
          DECLARE_TYPE_REGISTRY(ProxyTypeItemsReg) \
          template <typename TProxy, std::uint32_t Id, typename T> \
          class ProxyMethodImpl; \
        } \
        namespace MethodsStub \
        { \
          DECLARE_TYPE_REGISTRY(StubTypeItemsReg) \
          template <std::uint32_t Id, typename T> \
          class StubMethodImpl; \
        } \
      }

PS_ADD_METHOD
#define PS_ADD_METHOD(mtd_) \
  namespace Methods \
  { \
    GET_NEXT_STATIC_COUNTER(PSCounter, mtd_##Counter) \
    typedef decltype(&IFaceType::mtd_) mtd_##Type; \
    enum { mtd_##Id = Remote::Crc32(#mtd_) }; \
    namespace MethodsProxy \
    { \
      template <typename TProxy, typename C, typename R, typename ... P> \
      class ProxyMethodImpl<TProxy, mtd_##Id, R (C::*)(P ... )> \
        : public virtual IFaceType \
        , public virtual ::Remote::Private::PoxyMethodIFace<TProxy> \
      { \
      private: \
        virtual R mtd_ (P ... p) \
        { \
          return this->template __ProxyExecute<R, P ...>(mtd_##Id, p ...); \
        } \
      }; \
      template <typename TProxy, typename C, typename R, typename ... P> \
      class ProxyMethodImpl<TProxy, mtd_##Id, R (C::*)(P ... ) const> \
        : public virtual IFaceType \
        , public virtual ::Remote::Private::PoxyMethodIFace<TProxy> \
      { \
      private: \
        typedef ProxyMethodImpl<TProxy, mtd_##Id, R (C::*)(P ... ) const> ThisType; \
        virtual R mtd_ (P ... p) const \
        { \
          return const_cast<ThisType *>(this)->template __ProxyExecute<R, P ...>(mtd_##Id, p ...); \
        } \
      }; \
      typedef ProxyMethodImpl<ProxyClass, mtd_##Id, mtd_##Type> mtd_##ProxyImpl; \
      REGISTRY_ADD_TYPE(ProxyTypeItemsReg, PSCounter::mtd_##Counter, mtd_##ProxyImpl) \
    } \
    namespace MethodsStub \
    { \
      template <typename C, typename R, typename ... P> \
      class StubMethodImpl<mtd_##Id, R (C::*)(P ... )> \
        : public virtual ::Remote::Private::StubMethodIFace<Stub> \
      { \
      public: \
        enum { MethodId = mtd_##Id }; \
        template <typename TPkg> \
        R __Call(TPkg &pkg) \
        { \
          return this->__StubCall(pkg, &IFaceType::mtd_); \
        } \
      }; \
      template <typename C, typename R, typename ... P> \
      class StubMethodImpl<mtd_##Id, R (C::*)(P ... ) const> \
        : public virtual ::Remote::Private::StubMethodIFace<Stub> \
      { \
      public: \
        enum { MethodId = mtd_##Id }; \
        template <typename TPkg> \
        R __Call(TPkg &pkg) \
        { \
          return this->__StubCall(pkg, &IFaceType::mtd_); \
        } \
      }; \
      typedef StubMethodImpl<mtd_##Id, mtd_##Type> mtd_##StubImpl; \
      REGISTRY_ADD_TYPE(StubTypeItemsReg, PSCounter::mtd_##Counter, mtd_##StubImpl) \
    } \
  }

PS_END_MAP
#define PS_END_MAP() \
      namespace Methods \
      { \
        GET_NEXT_STATIC_COUNTER(PSCounter, LastPSCounter) \
        namespace MethodsProxy \
        { \
          template <typename T, typename TPack> \
          struct ChangePackPrm; \
          template <typename TPack, std::uint32_t Id, typename TMtd, typename TNewPack> \
          struct ChangePackPrm<ProxyMethodImpl<TPack, Id, TMtd>, TNewPack> \
          { \
            typedef ProxyMethodImpl<Proxy<TNewPack>, Id, TMtd> Type; \
          }; \
          template <typename TPack, std::size_t const N> \
          class ProxyMethodsImpl \
            : public ProxyMethodsImpl<TPack, N - 1> \
            , public ChangePackPrm<typename ProxyTypeItemsReg<N>::Type, TPack>::Type \
          { \
          }; \
          template <typename TPack> \
          class ProxyMethodsImpl<TPack, 0> \
            : public virtual IFaceType \
          { \
          }; \
        } \
      } \
      template <typename TPack> \
      class Proxy \
        : public Methods::MethodsProxy::ProxyMethodsImpl<TPack, Methods::PSCounter::LastPSCounter - 1> \
      { \
      public: \
        Proxy(std::uint32_t instId, ::Remote::IRemotingPtr remoting) \
          : InstId(instId) \
          , Remoting(remoting) \
        { \
          if (!Remoting) \
            throw ProxyException("IRemoting pointer must not be null."); \
        } \
      private: \
        template <typename> \
        friend class ::Remote::Private::PoxyMethodIFace; \
        std::uint32_t InstId; \
        ::Remote::IRemotingPtr Remoting; \
        template <typename R, typename ... P> \
        R __ProxyExecuteImpl(std::uint32_t mtdId, P ... p) \
        { \
          TPack Pack(IFaceId, mtdId, InstId, 0/*thread*/, p ...); \
          auto Buf(std::move(Remoting->Execute(Pack.GetData(), Pack.GetSize()))); \
          auto const &BufRef = *Buf; \
          auto NewPack(std::move(TPack::CreateFromData(&BufRef[0], BufRef.size()))); \
          if (NewPack->GetIFaceId() != IFaceId) \
            throw ProxyException("Bad interface Id."); \
          if (NewPack->GetInstId() != InstId) \
            throw ProxyException("Bad instance Id."); \
          if (NewPack->GetMethodId() != mtdId) \
            throw ProxyException("Bad method Id."); \
          auto Exception = NewPack->template GetException<::Remote::RemotingException>(); \
          if (Exception != std::exception_ptr()) \
            std::rethrow_exception(Exception); \
          ::Remote::Private::UpdateOutputParams<decltype(*NewPack), P ...>(*NewPack, p ...); \
          LastPack = std::move(::Remote::Private::IDisposablePtr(new ::Remote::Private::Disposable<decltype(NewPack)>(NewPack))); \
          return ::Remote::Private::ResultExtractor<R>::Extract(*NewPack); \
        } \
      private: \
        ::Remote::Private::IDisposablePtr LastPack; \
      }; \
      namespace Methods \
      { \
        namespace MethodsStub \
        { \
          template <std::size_t const N> \
          class StubMethodsImpl \
            : public StubMethodsImpl<N - 1> \
            , public StubTypeItemsReg<N>::Type \
          { \
          public: \
            template <typename TPkg> \
            void __StubMethodCall(TPkg &pkg, std::uint32_t mtdId) \
            { \
              typedef typename StubTypeItemsReg<N>::Type StubMtdType; \
              if (mtdId == StubMtdType::MethodId) \
              { \
                typedef decltype(StubMtdType::__Call(pkg)) R; \
                ::Remote::Private::CallAndPackRes<R>::Call(this, &StubMtdType::__Call, pkg); \
                return; \
              } \
              StubMethodsImpl<N - 1>::__StubMethodCall(pkg, mtdId); \
            } \
          }; \
          template <> \
          class StubMethodsImpl<0> \
          { \
          public: \
            template <typename TPkg> \
            void __StubMethodCall(TPkg &, std::uint32_t) \
            { \
              throw StubException("Method not found."); \
            } \
          }; \
        } \
      } \
      class Stub \
        : public Methods::MethodsStub::StubMethodsImpl<Methods::PSCounter::LastPSCounter - 1> \
      { \
      public: \
        typedef std::shared_ptr<IFaceType> IFaceTypePtr; \
        Stub(std::uint32_t instId, IFaceTypePtr obj) \
          : InstId(instId) \
          , Obj(obj) \
        { \
          if (!Obj) \
            throw StubException("IFace pointer must not be null."); \
        } \
        virtual ~Stub() \
        { \
        } \
        template <typename TPack> \
        ::Remote::DataBufPtr Call(TPack &pack) \
        { \
          try \
          { \
            if (pack.GetIFaceId() != IFaceId) \
              throw StubException("Bad interface Id."); \
            if (pack.GetInstId() != InstId) \
              throw StubException("Bad instance Id."); \
            this->__StubMethodCall(pack, pack.GetMethodId()); \
            pack.PackParams(); \
          } \
          catch (std::exception const &e) \
          { \
            pack.GetPack().SetException(e); \
          } \
          ::Remote::DataBufPtr Ret(new ::Remote::DataBuf); \
          auto &NewPack = pack.GetPack(); \
          auto Size = NewPack.GetSize(); \
          if (Size) \
          { \
            void const *Data = NewPack.GetData(); \
            std::copy( \
                reinterpret_cast<char const *>(Data), \
                reinterpret_cast<char const *>(Data) + Size, \
                std::back_inserter(*Ret) \
              ); \
          } \
          return std::move(Ret); \
        } \
      private: \
        template <typename> \
        friend class ::Remote::Private::StubMethodIFace; \
        std::uint32_t InstId; \
        IFaceTypePtr Obj; \
        template <typename TPkg, typename R, typename C, typename ... P> \
        R __StubCallImpl(TPkg &pkg, R (C::*mtd)(P ...)) \
        { \
          return ::Remote::Private::CallStubMethod::Call(Obj.get(), mtd, pkg); \
        } \
        template <typename TPkg, typename R, typename C, typename ... P> \
        R __StubCallImpl(TPkg &pkg, R (C::*mtd)(P ...) const) \
        { \
          return ::Remote::Private::CallStubMethod::Call(Obj.get(), mtd, pkg); \
        } \
      }; \
    } \
    template <typename T> \
    using Proxy = PSImpl::Proxy<T>; \
    using Stub = PSImpl::Stub; \
  }

Підсумки створення Proxy / Stub
Підведу підсумки послідовності кроків створення Proxy / Stub:

  • Отримати тип методу за допомогою decltype.
  • Для кожного методу обчислити його ідентифікатор за допомогою CRC32 від імені методу.
  • Створити з реалізації для кожного методу інтерфейсу, грунтуючись на спеціалізаціях деякого шаблону, що реалізує метод інтерфейсу.
  • Вести лічильник методів під час компіляції.
  • Додавати отримані реалізації проксі-класів та класів-заглушок до реєстрів проксі і заглушок.
  • Наприкінці побудови реалізацій методів отримати останнє значення лічильника методів.
  • Пройти по всьому реєстром (окремо для проксі реалізацій і реалізацій заглушок) за допомогою отриманих значень лічильника методів і побудувати ієрархію класів на основі типів, розташованих в реєстрі. Кожна така ієрархія буде закінченою реалізацією проксі-класу або класу-заглушки.
Інфраструктура та транспорт
Під інфраструктурою буде матися на увазі деякий набір сутностей, який на стороні сервера буде відповідати за створення реальних об'єктів-реалізацій та їх заглушок, а так само управляти їх часом життя, а на стороні клієнта набір сутностей для створення проксі-об'єктів, що повертаються клієнту при запиті того чи іншого об'єкта реалізації із заданим інтерфейсом і розташованим на сервері.

Під транспортом мається на увазі щось, що дозволяє обмінюватися пакетами даних між клієнтом і сервером. У даній реалізації це зроблено у вигляді надбудови над libevent (її http-функціоналом). Для цього взята майже без змін реалізація http-сервера з [5] і доповнена клієнтською частиною відправки http-запитів.

Основними інтерфейсами для клієнта і сервера, які входять в інфраструктуру, є інтерфейс фабрики класів

namespace Remote
{  
  struct IClassFactory
  {
    virtual ~IClassFactory() {}
    virtual std::uint32_t CreateObject(std::uint32_t classId) = 0;
  };  
}

і інтерфейс, призначений для видалення створених об'єктів на сервері, за допомогою якого клієнт оповіщає сервер, що він більш не потребує об'єкті і сервер його повинен видалити

namespace Remote
{
  struct IObjectDeleter
  {
    virtual ~IObjectDeleter() {}
    virtual void DeleteObject(std::uint32_t instId) = 0;
  }; 
}

Реалізації цих інтерфейсів передаються клієнту з зумовленими ідентифікаторами їх об'єктів. Клієнт, після створення об'єкта, що відповідає за транспортний рівень, звертається до сервера за отриманням цих двох інтерфейсів по визначеним ідентифікаторам і з використанням все тих же Proxy / Stub 'ов, розглянутих вище.

Proxy / Stub для IClassFactory і IObjectDeleter
namespace Remote
{
  PS_BEGIN_MAP(IClassFactory)
    PS_ADD_METHOD(CreateObject)
  PS_END_MAP()
  
  PS_BEGIN_MAP(IObjectDeleter)
    PS_ADD_METHOD(DeleteObject)
  PS_END_MAP()
}

PS_REGISTER_PSTYPES(Remote::IClassFactory)
PS_REGISTER_PSTYPES(Remote::IObjectDeleter)

Отримавши ж екземпляри цих об'єктів, клієнт створює через проксі-об'єкт фабрики класів потрібні йому об'єкти. На стороні сервера створюється реальний об'єкт, якому присвоюється певний унікальний ідентифікатор екземпляра, а так само створюється і пов'язаний з ним об'єкт-заглушка. Ідентифікатор повертається клієнтові. На стороні клієнта створюється проксі-об'єкт, якому передається отриманий ідентифікатор. Клієнт використовує розумні покажчики (в даному випадку — це std :: shared_ptr і в якості deleter'а йому переданий так само розумний покажчик на IObjectDeleter, під яким знаходиться його проксі-об'єкт) і коли проксі-об'єкт знищується, на сервер відправляється його ідентифікатор екземпляра через інтерфейс IObjectDeleter. Сервер на своєму боці, на підставі цього ідентифікатора, руйнує реальний об'єкт і заглушку з ним пов'язану. Так відбувається управління часом життя об'єктів між клієнтом і сервером. В іншому ж всі виклики проксі-об'єкта обробляються сервером на підставі його ідентифікатора примірника, отриманого при його створенні.

Сервер
На стороні сервера створюється http-сервер на основі [5] , в якості його обробника запитів, передається об'єкт, який розбирає вхідні пакети і віддає їх менеджеру об'єктів, в роботу якого входить створення і видалення об'єктів, а так само визначення об'єкта-виконавця прийшов запиту.

RequestHandler
template <typename TPkg, typename ... TImpls>
class RequestHandler final
  : private NonCopyable
{
public:
  RequestHandler()
    : Mgr(std::make_shared<ObjectManagerType>())
    , Creator(STUB_CLASS(IClassFactory)(SysClassId::ClassFactory, Mgr))
    , Deleter(STUB_CLASS(IObjectDeleter)(SysClassId::ObjectDeleter, Mgr))
  {
  }
  DataBufPtr Proccess(void const *data, std::size_t bytes)
  {
    if (!data || !bytes)
      throw RequestHandlerException("Request data must not be empty.");
    TPkg Pack(data, bytes);
    auto InstId = Pack.GetInstId();
    switch (InstId)
    {
    case SysClassId::ClassFactory :
      return std::move(Creator.Call(Pack));
    case SysClassId::ObjectDeleter :
      return std::move(Deleter.Call(Pack));
    default :
      break;
    }
    return std::move(Mgr->Call(InstId, Pack));
  }
  
private:
  typedef Private::ObjectManager<TPkg, TImpls ...> ObjectManagerType;
  std::shared_ptr<ObjectManagerType> Mgr;
  STUB_CLASS(IClassFactory) Creator;
  STUB_CLASS(IObjectDeleter) Deleter;
};

Де STUB_CLASS — це всього лише невеликий макрос формуючий ім'я класу-заглушки з імені інтерфейсу

STUB_CLASS
#define STUB_CLASS(iface_) iface_ ## _PS::Stub

Менеджер об'єктів — це реалізація IClassFactory і IObjectDeleter, яка є шаблоном, а параметрами його служать тип реалізує сериализацию / десеріалізацію і список типів, що описують інформацію про те які об'єкти з якими інтерфейсами і ідентифікаторами реалізацій сервер може створювати і обробляти.

При зверненні до обробника запитів, він їх передає менеджеру об'єктів, який має карту, що складається з ідентифікатора екземпляра і заглушки пов'язаної з реальним об'єктом. З цієї карті виклик вирушає потрібного об'єкту через його об'єкт-заглушку.

Якщо подивитися на приклади з початку посту, то на них видно, як створюється сервер і як йому передається вся інформація про підтримувані типи.

Клієнт
На стороні клієнта створюється http-клієнт, який реалізує інтерфейс IRemoting. Через реалізацію цього інтерфейсу клієнт відправляє запити серверу. Розумний покажчик на цей інтерфейс передається фабриці класів при її створенні. Так само фабрика класів при створенні приймає тип, який реалізує сериализацию / десеріалізацію і список підтримуваних інтерфейсів, об'єкти яких передбачається створювати на стороні сервера. У той же час сама фабрика класів не є шаблоном, але і не має відкритого конструктора, а тільки шаблонний статичний метод її створення. Це дозволяє добре инкапсулировать всю інформацію, передану при створенні фабрики і відмовитися від необхідності її наявності в місці використання фабрики.

Фабрика класів на стороні клієнта
class ClassFactory final
  : private NonCopyable
{
public:
  template <typename TPkg, typename ... TIFaces>
  static std::shared_ptr<ClassFactory> Create(IRemotingPtr remoting)
  {
    // Детали реализации
  }
  
  template <typename TIFace>
  std::shared_ptr<TIFace> Create(std::uint32_t classId)
  {
    // Детали реализации
  }
private:
  std::shared_ptr<Private::IProxyObjectManager> Creator;
  
  ClassFactory()
  {
  }
};
Нестатичних метод фабрики Create приймає як шаблонний параметр інтерфейс, на підставі якого проводиться пошук проксі-класу, і звичайно не шаблонний параметр ідентифікатор реалізації, під яким на сервері зареєстрована реалізація інтерфейсу. Фабрика класів на підставі цієї інформації та інформації, переданої при її створенні, робить запит на сервер на створення об'єкта, а отриманий ідентифікатор екземпляра віддає знайденому проксі-об'єкту. Користувачеві повертається std :: shared_ptr, під яким лежить проксі-об'єкт із зазначеним інтерфейсом і в якості deleter'а проксі-об'єкт з інтерфейсом IObjectDeleter, який посилає запит на сервер при руйнуванні проксі-об'єкта на стороні клієнта для знищення його на стороні сервера. 

<a name="ps_reg"></a><h5>Зв'язок інтерфейсів і Proxy / Stub </h5>
Подивившись на серверну і клієнтську частини може виникнути питання: як клієнт і сервер пов'язують інформацію про інтерфейси з інформацією про Proxy / Stub 'ах? 

При описі Proxy / Stub, можливо, Ви помітили макрос PS_REGISTER_PSTYPES, про який поки нічого не говорилося. Цей макрос реєструє створені Proxy / Stub 'и в деякому глобальному реєстрі типів в момент компіляції (схожому на реєстри, використовувані при реалізації методів інтерфейсів, з тією відмінністю, що в якості ідентифікатора використовується не число, а інтерфейс). А коли фабрика класів на стороні клієнта або менеджер об'єктів на стороні сервера шукає потрібну йому реалізацію проксі-класу або класу-заглушки, то робиться звернення до цього реєстру по типу інтерфейсу. Код пошуку проксі або заглушки так само створюється в момент компіляції. 

<div class="spoiler"><b class="spoiler_title">Реєстр Proxy / Stub &amp; # 39; ів </b><div class="spoiler_text"><code class="cpp">namespace Remote
{
  namespace Private
  {
    template <typename>
    struct ProxyStubRegistry;
  }
}

#define PROXY_CLASS(iface_, tpack_) iface_ ## _PS::Proxy<tpack_>

#define STUB_CLASS(iface_) iface_ ## _PS::Stub

#define PS_REGISTER_PSTYPES(iface_) \
  namespace Remote \
  { \
    namespace Private \
    { \
      template <> \
      struct ProxyStubRegistry<iface_> \
      { \
        enum { Id = Crc32(#iface_) }; \
        template <typename I> \
        using Proxy = PROXY_CLASS(iface_, I); \
        typedef STUB_CLASS(iface_) Stub; \
      }; \
    } \
  }

Здавалося б навіщо ще один макрос для реєстрації створених проксі і заглушок, і чому б цю реєстрацію не зробити в макросе PS_END_MAP? Можна і там, але тоді доведеться пожертвувати можливістю розкладати інтерфейси по просторам імен, а так само розкладати по однойменних просторів імен і їх Proxy / Stub. З коду макросів причину можна зрозуміти: не можна, перебуваючи в деякому просторі імен, зробити приватну спеціалізацію шаблона в іншому просторі імен, ніяк не пов'язаному з поточним. Тут довелося зробити вибір між відмовою від просторів імен для користувальницького коду або додати ще один макрос. Вибір був зроблений на користь додавання нового макросу. Дозволити відмовитися від просторів імен я собі не можу…

Підсумки роботи інфраструктури
Підводячи підсумки інфраструктури можна виділити наступні кроки з організації клієнт-серверної взаємодії:
  • Потрібна реалізація транспорту.
  • Реалізація на стороні сервера і клієнта сутностей для створення і управління об'єктами.
  • Серверна сторона повинна мати інформацію про підтримуваних нею об'єктах.
  • Клієнтська сторона повинна мати інформацію про підтримувані інтерфейси.
  • Потрібен реєстр типів, в якому зберігати всю інформацію про прив'язку Proxy / Stub до певного інтерфейсу, на підставі якої клієнт створює проксі-об'єкти, а сервер об'єкти-заглушки.

Приклади

Один із прикладів було наведено на самому початку посту. Ще один у якості прикладу буде розроблений. Можна придумати реалізацію з уже знайомим інтерфейсом ISessionManager і додавши новий IDateSoutce. Методи інтерфейсу ISessionManager вироблятимуть емуляцію роботи з сесіями користувача: видавати ідентифікатор сесії на основі переданого імені користувача і пароля, перевіряти отриманий ідентифікатор на валідність і закривати сесію. Методи інтерфейсу IDateSource отримуватимуть поточну дату. Один метод для отримання дати у вигляді рядка «yyyy.mm.dd hh: mm: ss», а другий повертає дату через вихідні параметри по частинах. Так само всі методи інтерфейсів можуть генерувати виключення, які передаватимуться на сторону клієнта і там викидатися у вигляді винятків типу Remote :: RemotingException.

Інтерфейси
// ifaces.h
namespace Test
{
  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;
  };
  struct IDateSource
  {
    virtual ~IDateSource() {}
    virtual void GetDate(std::uint16_t *year, std::uint16_t *month, std::uint16_t *day,
                         std::uint16_t *hour, std::uint16_t *min, std::uint16_t *sec) const = 0;
    virtual char const* GetDateAsString() const = 0;
  };
}

Опис Proxy / Stub & # 39; ів
// ifaces_ps.h
namespace Test
{
  PS_BEGIN_MAP(ISessionManager)
    PS_ADD_METHOD(OpenSession)
    PS_ADD_METHOD(CloseSession)
    PS_ADD_METHOD(IsValidSession)
  PS_END_MAP()
  
  PS_BEGIN_MAP(IDateSource)
    PS_ADD_METHOD(GetDate)
    PS_ADD_METHOD(GetDateAsString)
  PS_END_MAP()
}
PS_REGISTER_PSTYPES(Test::ISessionManager)
PS_REGISTER_PSTYPES(Test::IDateSource)

Десь потрібно ще визначити константи, за якими клієнт і сервер будуть розуміти з якою реалізацією інтерфейсу вони повинні працювати. Це можна зробити прямо в коді в місці виклику у вигляді «магічних цифр», а можна винести такі константи окремо, прагнучи зробити код більш незалежним від «магічних чисел».

Константи класів-реалізацій
// class_ids.h
namespace Test
{
  namespace ClassId
  {
    enum
    {
      SessionManager = Remote::Crc32("Test.SessionManager"),
      DateSource = Remote::Crc32("Test.DateSource")
    };
  }
}

Самі реалізації інтерфейсів ISessionManager і IDateSource наводити не буду, рутина; можна подивитися в доданих вихідних кодах прикладів.

Сервер
#include <iostream>
#include "session_manager.h"
#include "date_source.h"
#include "ifaces_ps.h"
#include "class_ids.h"
#include "xml/pack.h"
#include "http/server_creator.h"
int main()
{
  try
  {
    auto Srv = Remote::Http::CreateServer
      <
        Remote::Pkg::Xml::InputPack,
        Remote::ClassInfo<Test::ISessionManager, Test::SessionManager, Test::ClassId::SessionManager>,
        Remote::ClassInfo<Test::IDateSource, Test::DateSource, Test::ClassId::DateSource>
      >("127.0.0.1", 5555, 2);
    std::cin.get();
  }
  catch (std::exception const &e)
  {
    std::cerr << e.what() << std::endl;
  }
  return 0;
}

Клієнт
#include <iostream>
#include "ifaces_ps.h"
#include "class_ids.h"
#include "xml/pack.h"
#include "http/http_remoting.h"
#include "class_factory.h"
int main()
{
  try
  {
    auto Remoting = std::make_shared<Remote::Http::Remoting>("127.0.0.1", 5555);
    auto Factory = Remote::ClassFactory::Create
          <
            Remote::Pkg::Xml::OutputPack,
            Test::ISessionManager,
            Test::IDateSource
          >(Remoting);
    auto Mgr = Factory->Create<Test::ISessionManager>(Test::ClassId::SessionManager);
    auto SessionId = Mgr->OpenSession(UserName, Password);
    // TODO:
    Mgr->CloseSession(SessionId);
    auto Date = Factory->Create<Test::IDateSource>(Test::ClassId::DateSource);
    std::uint16_t Year = 0, Month = 0, Day = 0, Hour = 0, Min = 0, Sec = 0;
    Date->GetDate(&Year, &Month, &Day, &Hour, &Min, &Sec);
    auto const *DateAsString = Date->GetDateAsString();;
    // TODO:
  }
  catch (std::exception const &e)
  {
    std::cerr << e.what() << std::endl;
  }
  return 0;
}

Збираємо і пробуємо…

Запрацювало! (Знімок робочого столу з сервером і клієнтом)

Використання

В якості проміжного підсумку наведу невеликий опис кроків по використанню.

Весь код пропонованої бібліотеки поставляється у вигляді включення файл (за винятком прикладів). Для включення в проект не треба збирати всілякі бібліотеки і після з ними виробляти лінковку. Потрібно тільки додати шлях до включення файл бібліотеки, в яких надано все для створення Proxy / Stub 'ов і свій http-сервер на основі libevent. Для складання проекту з урахуванням пропонованої бібліотеки необхідно наявність libevent. Якщо ж користувач реалізує свій транспорт, то така умова знімається.

При створенні клієнт-серверного додатка на основі пропонованої бібліотеки потрібно виконати кілька кроків.

Спільні дії:

  • Визначити інтерфейси
  • Описати Proxy / Stub 'и
  • При бажанні створити файл з константами реалізацій
На стороні сервера:

  • Реалізувати певні інтерфейси
  • Створити сервер з прив'язкою до транспорту
  • При створенні сервера вказати всю інформацію про (де) серіалізатор і передати список з інформацією про підтримувані сервером об'єктах
На стороні клієнта:

  • Створити транспорт
  • Створити фабрику класів, передавши їй створений транспорт і список підтримуваних інтерфейсів
  • Створювати (запитувати з сервера) за допомогою фабрики об'єкти за певними раніше ідентифікаторами класів реалізацій
Усі винятки, викинуті сервером в їх методах, на клієнтську сторону приходять у вигляді Remote :: RemotingException, який породжений від std :: exception. Це виключення можна розуміти як «щось сталося в методі об'єкта на стороні сервера» і його причина описана в його повідомленні. Всі ж інші винятку говорять про системні помилки і помилки з'єднання.

Так само нагадаю, що сервер доступний поки існує його об'єкт. Об'єкт сервера не зупиняє його створює потік і тому після створення серверного об'єкта, потік, його створив, можливо потрібно призупинити іншими засобами. Для своєї роботи серверний об'єкт створює свої потоки обробки запитів.

При написанні класів-реалізацій інтерфейсів варто пам'ятати, що виклик методу може відбуватися з різних потоків якщо сервер створений не з єдиним потоком. Кількість потоків вказується при створенні сервера параметром threadCount. Тому вся синхронізація всередині класів-реалізацій лежить на боці їх розробника.

Клієнт же припускає використання в однопоточному режимі. Що в той же час не заважає створювати декілька клієнтів, розташованих у різних потоках. Умовою є звернення до методів створеного об'єкта через його інтерфейс в рамках потоку, в якому було створено його проксі об'єкт.

Обмеження

Межпроцессное взаємодія та взаємодія між різними робочими станціями в мережі завжди більш сковано ніж взаємодія частин системи в рамках одного процесу. Перехід через кордон процесу якщо говорити про програмування або перехід кордону держави завжди пов'язані з якоюсь скутістю ніж перебуваючи всередині тієї чи іншої системи. Так якщо я вирішу відкрити свій бізнес і знайду стартовий капітал для покупки двадцяти кілограм iPhone'ов однієї з останніх моделей в США, то звичайно в рамках транспорту (авіаперевізника) у мене не повинно виникнути проблем із ввезенням їх до Росії і навіть не доведеться оплачувати додатковий багаж. Все в дозволених рамках. А ось митна служба настільки зухвалі пориви сильно притупить беззастережним бажанням із законодавчим підкріпленням зібрати митні збори, оскільки не переконати її співробітників, що 20 кг iPhone я везу для особистого використання і коштують вони всі разом не більше дозволеної безмитної суми :) Так і в рамках описуваної бібліотеки є обмеження.

Якщо ж вбудований транспорт готовий передати будь-які дані з якимись несуттєвими обмеженнями в порівнянні з (де) серіалізатор, то (де) серіалізатор якраз і є тією самою митницею, яка безмитно дозволяє використовувати будь-яку кількість параметрів в методах інтерфейсів, обмежене тільки можливостями компілятора і стандартом C + +11. А на типи параметрів запроваджено митний збір. Можна передавати як параметрів і значень тільки інтегральні типи і типи з плаваючою крапкою. Параметри можуть бути передані як за значенням, так і за посиланням і вказівником і при необхідності з додаванням кваліфікатора const. Це «безкоштовно», тобто вбудовано в пропоновану реалізацію, а якщо є бажання передавати, наприклад, stl контейнери або інші типи, то доведеться внести «мито» у вигляді реалізації власного (де) серіалізатор або модифікації існуючого.

Так само значення, отримані за вказівником або засланні не повинні використовуватися за межами методу в / з який вони передані / отримані; їх значення має бути скопійовано. Моніторинг змін значень за посиланням або вказівником між процесами можна було реалізувати, але він був би невиправдано дорогий в обчислювальних ресурсах. У той же час це не виключає того, що передані параметри за посиланням або вказівником не можуть бути змінені в методі, в який вони передані. Їх можна змінювати так само як і в рамках процесу, змінені параметри в місці їх отримання будуть відправлені викликає стороні. Тут повна аналогія з роботою в рамках одного процесу.

Причини відмови від перевантаження методів інтерфейсів були наведені при описі реалізації Proxy / Stub.

Ісходникі, збірка, тестування

Бібліотека для реалізації клієнт-серверної взаємодії, побудована поверх HTTP-протоколу, прихованого під libevent доступна для скачування у вигляді zip-архіву з сервера, на якому можна її і протестувати. Поставляється вона у вигляді включення файл (крім прикладів).

При тестуванні я користувався компілятором gcc / g+ + 4.8.1. З великою часткою ймовірності все буде збиратися і на більш ранніх версіях, наприклад 4.7.2 і може бути на 4.7; не перевіряв. На ще більш ранніх версіях ймовірність збірки близька до нуля. Так само не перевіряв, але gcc 4.6.x ще слабо підтримують або зовсім не підтримують деякі речі, які використовуються в реалізації. Пробував збирати на clang 3.5. Виникли невеликі проблеми при розрахунку CRC32 в момент компіляції від довгих рядків, а довгою рядком цей компілятор порахував «Remote.IClassFactory» і маніпуляції з-ftemplate-depth і-fconstexpt-depth прапорами не дали результату. Звичайно, цю проблему можна вирішити викрутившись з довжинами ідентифікаторів або, наприклад, розрахунок їх по частинах рядка і xor'ом отриманих результатів, але поки від такого рішення відмовився в силу відсутності потреби в ньому і підтримки clang. При вирішенні ж проблеми з довжиною ідентифікатора, більше проблем з clang 3.5 не виявлено. Пробувати збирати за допомогою MS Visual Studio 2013 не став. Раніше пробував зібрати за допомогою «студії 2013» свій проект [1] , і був здивований, що constexpr вона не підтримує. Прибравши весь з ним пов'язаний код і підставивши заздалегідь обчислені ідентифікатори вона в одному з ключових місць видала повідомлення про внутрішню помилку компілятора, після чого всі експерименти з нею я вирішив відкласти як мінімум до виходу нової версії.

Як libevent, так і пропонована бібліотека є кроссплатформенную. При тестуванні збірка була здійснена на Ubuntu 12.10 32bit і Ubuntu 12.04 64bit. Під Windows за допомогою MinGW поки не пробував збирати. Можливо все збереться з першого разу або виникнуть невеликі тонкощі. Сподіваюся спробувати в недалекому майбутньому.

Разом з бібліотекою поставляються приклади. Їх можна випробувати на локальній машині або помінявши в клієнтах рядок з адресою «127.0.0.1» на адресу сервера «t-boss.ru», на якому розташований тестовий екземпляр серверної частини прикладів, протестувати приклади клієнтів так би мовити «в реальних умовах».

Висновок

Чи вдалося досягти мені заявленого в назві мінімалізму у використанні отриманого продукту судити Вам. У кожного цей ідеал може бути різним. Мені ж вдалося покрити своє бажання мінімалізувати зазначає інформацію при описі Proxy / Stub 'ов для інтерфейсів до того рівня, коли не треба нічого вказувати крім імені методу, а так же мені вдалося відмовитися від необхідності наявності інформації про транспорт, сериализации і тих же Proxy / Stub 'ах в місці використання в клієнтському коді об'єктів із заданим інтерфейсом і реалізацією логіки на сервері.

Велику допомогу в цьому мені надав стандарт C + +11. У порівнянні з C + +03 він дає більше можливостей: decltype, auto, шаблони з перемінним кількістю параметрів, лямбда функції, розумні покажчики, багатопоточність і багато іншого. Що дозволяє при реалізації кроссплатформенного коду відмовитися від безлічі власних обгорток над системними функціями. Відмовитися від розробки власних розумних покажчиків і більшою мірою мати можливість використання RAII [8] , так як раніше одного std :: auto_ptr було недостатньо.

decltype дав можливість отримувати типи змінних (у даному випадку покажчика на метод інтерфейсу). auto дає можливість абстрагуватися від вказівки якогось типу і працювати з отриманим значенням, грунтуючись на припущеннях про те, що він повинен містити ті чи інші методи і при цьому не вказувати явно цей тип і не протягувати інформацію про нього в місце використання.

З появою шаблонів з перемінним числом параметрів можна відмовитися від раніше використовуваних і більш громіздких списків типів в стилі Александреску. Позбутися від гір «копіпастного коду», який часто з'являвся при розробці бібліотек загального використання з наявністю шаблонів.

Бібліотека роботи з типами дає можливість скоротити код, позбувшись від написання власних речей типу std :: remove_pointer, std :: is_const і так далі, а деякі речі, нині реалізовані в цій бібліотеці раніше можна було зробити тільки компіляторозавісімимі.

Досягнувши ж за допомогою цих можливостей деякого для себе результату в мінімалізмі і комфортному, на мою думку, взаємодії клієнта з сервером, реалізація ж може здатися не настільки мінімальний або простий, а може і не здатися…

Завжди при розробці бібліотек загального використання намагався дотримуватися думки, що код, який використовує бібліотеку якомога менше повинен на ній зациклюватися і їй служити. Бібліотека повинна служити коду її використовують. А от реалізація самої бібліотеки може бути будь-якої складності, наскільки вистачить кваліфікації її авторів.

Переконання може здатися спірним. Але наведу улюблену мною аналогію. У багатьох проектах можна знайти використання boost. Так, це неоднозначно сприймається продукт IT-світом розробників C + +. Хтось її цурається, а хтось обома руками за її використання. У цілому ж можна сказати, що використання boost в більшості своїй сприймається позитивно, так як вона рятує від безлічі нюансів, приховуючи їх у своїй реалізації, часом вдаюся як до залежностей від компілятора, залежностям операційної системи і розширень, внесеним за допомогою препроцесора. Можна подивитися на одну з найстаріших функцій boost :: bind і якщо подивитися її реалізацію, то вона виявиться далеко не так проста як її використання. І це куди не межа складності реалізації в boost її простих речей. У цілому ж на іншій стороні малого, сильного і зручного плеча важеля використання boost видно набагато більш довге плече його реалізації, місцями дуже добре забарвлене boost.preprocessor. І мало хто задається деталями реалізації при оцінці критеріїв внесення в проект можливості використання цієї бібліотеки.

Так само при реалізації віддаленого взаємодії ще можна багато критеріїв оцінювати. Можливо це стане матеріалом одного з моїх наступних постів. Матеріал ж цього поста з'явився при розробці взаємодії між процесами плагінів [1] , про який, сподіваюся, скоро вдасться написати, після його завершення і тестування. З нього вийшов більш спрощений і самостійний продукт, який з деякими модифікаціями буде покладено в основу IPC для системи плагінів [1] .

Описана бібліотека пройшла повністю стадію 1 згідно [9] , багато речей пройшли третю стадію і трохи зачеплена другий (коротко про стадії: програма повинна виконувати закладену в неї логіку, має бути оптимальною за швидкістю, повинна бути оптимальна в дизайні зсередини).

При бажанні використовувати запропоновану бібліотеку або окремо якісь з її принципів і виникненні всіляких питань, пишіть про це в коментарях або в личку. Як правило, на все тематичні питання я намагаюся по можливості максимально розгорнуто відповідати.

Всім дякую за увагу!

Матеріали

  1. Система плагінів як вправу на C + + 11
  2. libevent
  3. gSOAP
  4. Proxy / Stubs своїми руками
  5. Свій http-сервер менш ніж в 40 рядків коду на libevent і C + +11
  6. RapidXml
  7. CRTP
  8. RAII
  9. Розробка через страждання



 
  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.