(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

RMI средствами С++ и boost.preprocessor

Источник: habrahabr

RMI - весьма банальная задача для ЯП, поддерживающих интроспекцию. Но, С++, к сожалению, к ним не относится.

Постановка задачи

1. Предоставить максимально простой синтаксис, чтоб невозможно было допустить ошибку.
2. Идентификация(связывание) процедур должна быть скрыта от пользователя для того, чтоб невозможно было допустить ошибку.
3. Синтаксис не должен накладывать ограничения на используемые С++ типы.
4. Должна присутствовать возможность версионности процедур, но, так, чтоб не ломалась совместимость с уже работающими клиентами.

Касательно четвертого пункта:
К примеру, у нас уже есть две процедуры add/div по одной версии каждой. Мы хотим для add добавить новую версию. Если просто добавить еще одну версию - у нас поплывут ID`ы процедур, о которых знают клиентские программы, собранные до внесения этого изменения.

Выбор инструмента

Т.к. конечный результат предполагается использовать совместно с С++ кодом, вариантов я вижу три:

  • Изобретаем синтаксис и пишем свой кодогенератор.
  • Используем С++ препроцессор.
  • Ищем нечто готовое и допиливаем под себя(если нужно).

Выскажусь о каждом из вариантов соответственно:

  • Зачем дополнительная стадия кодогенерации?
  • Препроцессор я люблю, и часто его использую.
  • Трата времени и сил. И, не понятно, будет ли в этом смысл.

Касательно первого, второго и третьего пунктов требований - препроцессорный вариант подходит.

Итак, выбор сделан - используем препроцессор. И да, разумеется boost.preprocessor.

Немного о препроцессоре

Типы данных С++ препроцессора:

  • arrays: (size, (elems, ....))
  • lists: (a, (b, (c, BOOST_PP_NIL)))
  • sequences: (a)(b)(c)
  • tuples: (a, b, c)

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

Несколько поясняющих примеров.
(a)(b)(c) - sequence. Тут, мы описали sequence, состоящий из трех элементов.
(a) - также sequence, но состоящий из одного элемента. (внимание!)
(a)(b, c)(d, e, f) - снова sequence, но состоящий из трех tuples. (обратите внимание на первый элемент - уловочка, однако, но это и правда tuple)
(a)(b, c)(d, (e, f)) - опять же sequence, и так же состоящий из трех tuples. Но! Последний tuple состоит их двух элементов: 1) любого элемента, 2) tuple.
И, напоследок, такой пример: (a)(b, c)(d, (e, (f)(g))) - тут уж разберитесь сами ;)
Как видно, все и вправду нереально просто.

Прототипируем синтаксис

После недолгого раздумья, вырисовывается такой синтаксис:
(proc_name0, // имя процедуры (signature_arg0, signature_arg1, signature_argN) // первая версия процедуры (signature_arg0) // вторая версия процедуры ) (proc_name1, // имя процедуры (signature_arg0, signature_arg1) // единственная версия этой процедуры ) (proc_name2, // имя процедуры () // единственная версия этой процедуры (без аргументов) )
Ну… весьма употрибительно, однако.

Некоторые детали реализации

Т.к. одно из требований - версионность процедур, да еще и такая, чтоб не ломалась совместимость с уже существующими клиентами - нам, для идентификации процедур, понадобятся два ID`а. Первый - ID процедуры, второй - ID версии.

Поясню на примере.
Допустим, это описание API нашего сервиса. Допустим, у нас уже есть клиентские программы, использующие этот API.
(proc_name0, // procID=0 (signature_arg0, signature_arg1) // sigID=0 ) (proc_name1, // procID=1 (signature_arg0, signature_arg1) // sigID=0 ) (proc_name2, // procID=2 () // sigID=0 )
Теперь, для proc_name0() нам нужно добавить еще одну версию с другой сигнатурой.
(proc_name0, // procID=0 (signature_arg0, signature_arg1) // sigID=0 (signature_arg0, signature_arg1, signature_arg2) // sigID=1 ) (proc_name1, // procID=1 (signature_arg0, signature_arg1) // sigID=0 ) (proc_name2, // procID=2 () // sigID=0 )
Таким образом, у нас появился новый ID версии процедуры, в то время как прежний остался без изменений.
Было: (0:0), стало: (0:0)(0:1)
Т.е. именно этого мы и пытались добиться. Прежние клиенты как использовали (0:0), так и далее будут использовать эти идентификаторы, не переживая о том, что появились новые версии этих процедур.
Также условимся в том, что все новые процедуры нужно добавлять в конец.

Далее, нам нужно позаботиться о том, чтоб ID`ы автоматически проставлялись на обоих сторонах сервиса. Запросто! - просто используем одну и ту же описанную последовательность дважды, для генерации клиентской и серверной сторон!

Самое время представить, как мы хотим видеть все это в конечном счете:
MACRO( client_invoker, // name of the client invoker implementation class ((registration, // procedure name ((std::string, std::string)) // message : registration key )) ((activation, ((std::string)) // message )) ((login, ((std::string)) // message )) ((logout, ((std::string)) // message )) ((users_online, ((std::vector<std::string>)) // without args )) , server_invoker, // name of the server invoker implementation class ((registration, ((std::string)) // username )) ((activation, ((std::string, std::string, std::string)) // registration key : username : password )) ((login, ((std::string, std::string)) // username : password )) ((logout, (()) // without args )) ((users_online, (()) // without args ((std::string)) // substring )) )
Чтоб не было путаницы в том, кто ведущий, а кто ведомый - условимся так, что процедуры, описываемые на одной из сторон, являются реализациями, находящимися на противоположной стороне. Т.е., к примеру,client_invoker::registration(std::string, std::string) говорит нам о том, что реализация этой процедуры будет находиться на стороне сервера, в то время как интерфейс к этой процедуре будет находиться на стороне клиента, и наоборот.
(двойные круглые скобки мы используем потому, что препроцессор при формировании аргумента для нашего MACRO(), развернет нами описанное API. это можно побороть, но не знаю, нужно ли?..)

Итог

Из приведенного выше макровызова, будет сгенерирован код, находящийся под спойлером.

Код

(в качестве сериализации используется другой мой проект - YAS)

Как бонус, была добавлена системная процедура yarmi_error() - используется для сообщения противоположной стороне о том, что при попытке произвести вызов произошла ошибка. Посмотрите внимательно, в client_invoker::invoke(), десериализация и вызов обернуты в try{}catch() блок, а в catch() блоках производится вызов yarmi_error(). Таким образом, если при десериализации или вызове процедуры возникнет исключение - оно будет успешно перехвачено catch()блоком, и информация об исключении будет отправлена вызывающей стороне. То же самое будет происходить и в противоположном направлении. Т.е. если сервер вызвал у клиента процедуру, в ходе вызова которой возникло исключение - клиент отправит серверу информацию об ошибке, также дополнительно сообщив ID и версию вызова, в котором возникло исключение. Но, использовать yarmi_error() вы можете и сами, ничто этого не запрещает. Пример: yarmi_error(call_id, version_id, "message");

Как вы могли заметить, к именам описанных нами процедур, на стороне их реализации добавляется префикс on_

Классы client_invoker и server_invoker принимают два параметра. Первый их низ - класс, в котором реализованы вызываемые процедуры, второй - класс, в котором реализован метод send(yas::shared_buffer buf).
Если у вас один и тот же класс выполняет обе роли, вы можете сделать так:
struct client_session: yarmi::client_base<client_session>, yarmi::client_invoker<client_session> { client_session(boost::asio::io_service &ios) :yarmi::client_base<client_session>(ios, *this) ,yarmi::client_invoker<client_session>(*this, *this) // <<<<<<<<<<<<<<<<<<<<<<<<< {} };
Конечный вариант выглядит так:
struct client_session: yarmi::client_base<client_session>, yarmi::client_invoker<client_session> { client_session(boost::asio::io_service &ios) :yarmi::client_base<client_session>(ios, *this) ,yarmi::client_invoker<client_session>(*this, *this) {} void on_registration(const std::string &msg, const std::string ®key) {} void on_activation(const std::string &msg) {} void on_login(const std::string &msg) {} void on_logout(const std::string &msg) {} void on_users_online(const std::vector<std::string> &users) {} };

Интерфейс к противоположной стороне будет унаследован из предкаyarmi::client_invoker. Т.е., к примеру, будучи в конструкторе нашегоclient_session, вы можете вызвать процедуру registration() следующим образом:
{ registration("niXman"); }
Ответ мы получим в нашу реализацию client_session::on_registration(std::string msg, std::string regkey)
Всё!

Из недоделок нужно отметить следующую:
В именах типов, описывающих процедуры, нельзя использовать запятые, ибо препроцессор не понимает контекста, в котором они используются. Будет исправлено.

В конечном счете, все это вылилось в проект под названием YARMI(Yet Another RMI).
Описанный кодогенератор закодирован в одном файле - yarmi.hpp. В сумме, на реализацию кодогенератора ушел один рабочий день.

Пример использования всего этого дела можно увидеть тут и тут. Первый тестовый проект все еще не завершен, к сожалению.

В дополнение к описанному, на странице проекта вы найдете коды асинхронного многопользовательского однопоточного сервера, и коды клинента.

Вместо заключения

Планы:
1. Генерация нескольких интерфейсов
2. Описать спецификацию (хоть она и проще некуда)
3. Возможность использовать собственный алокатор

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 06.11.2013 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Microsoft 365 Apps for business (corporate)
Microsoft Windows Professional 10, Электронный ключ
Microsoft Office 365 Профессиональный Плюс. Подписка на 1 рабочее место на 1 год
Microsoft Office 365 Персональный 32-bit/x64. 1 ПК/MAC + 1 Планшет + 1 Телефон. Все языки. Подписка на 1 год.
Microsoft Office 365 для Дома 32-bit/x64. 5 ПК/Mac + 5 Планшетов + 5 Телефонов. Подписка на 1 год.
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Безопасность компьютерных сетей и защита информации
Новости ITShop.ru - ПО, книги, документация, курсы обучения
Программирование на Microsoft Access
CASE-технологии
Программирование на Visual Basic/Visual Studio и ASP/ASP.NET
Работа в Windows и новости компании Microsoft
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100