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

в 9:48, , рубрики: c++, Preprocessor, rmi, RPC, Программирование, метки: , , ,

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

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

В данной публикации я хочу продемонстрировать возможность реализации весьма юзабильной RMI средствами С++ препроцессора.

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

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

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

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

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

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

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

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

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

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

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

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

Типов, как видно, более чем достаточно.
Немного подумав, почитав про возможности и ограничения каждого их них, а также учтя желаемую простоту синтаксиса и невозможность допустить ошибку — выбор был сделан в пользу 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
	))
)

Чтоб не было путаницы в том, кто ведущий, а кто ведомый — условимся так, что процедуры, описываемые на одной из сторон, являются реализациями, находящимися на противоположной стороне. Т.е., к примеру, client_invoker::registration(std::string, std::string) говорит нам о том, что реализация этой процедуры будет находиться на стороне сервера, в то время как интерфейс к этой процедуре будет находиться на стороне клиента, и наоборот.
(двойные круглые скобки мы используем потому, что препроцессор при формировании аргумента для нашего MACRO(), развернет нами описанное API. это можно побороть, но не знаю, нужно ли?..)

Итог

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

Код

namespace yarmi {

template<typename Impl, typename IO = Impl>
struct client_invoker {
	client_invoker(Impl &impl, IO &io)
		:impl(impl)
		,io(io)
	{}
	
	void registration(const std::string &arg0) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(0)
			& static_cast<std::uint8_t>(0)
			& arg0;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void activation(const std::string &arg0, const std::string &arg1, const std::string &arg2) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(1)
			& static_cast<std::uint8_t>(0)
			& arg0
			& arg1
			& arg2;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void login(const std::string &arg0, const std::string &arg1) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(2)
			& static_cast<std::uint8_t>(0)
			& arg0
			& arg1;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void logout() {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(3)
			& static_cast<std::uint8_t>(0);
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void users_online() {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(4)
			& static_cast<std::uint8_t>(0);
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void yarmi_error(const std::uint8_t &arg0, const std::uint8_t &arg1, const std::string &arg2) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(5)
			& static_cast<std::uint8_t>(0)
			& arg0
			& arg1
			& arg2;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void invoke(const char *ptr, std::size_t size) {
		std::uint8_t call_id, call_version;
		static const char* names[] = {
			 "registration"
			,"activation"
			,"login"
			,"logout"
			,"users_online"
			,"yarmi_error"
		};
		static const std::uint8_t versions[] = { 0, 0, 0, 0, 0, 0 };
		try {
			yas::binary_mem_iarchive ia(ptr, size, yas::no_header);
			ia & call_id
				& call_version;
			
			if ( call_id < 0 || call_id > 5 ) {
				char errstr[1024] = {0};
				std::snprintf(
					 errstr
					,sizeof(errstr)
					,"%s::%s(): bad call_id %d"
					,"client_invoker"
					,__FUNCTION__
					,static_cast<int>(call_id)
				);
				throw std::runtime_error(errstr);
			}
			if ( call_version > versions[call_id] ) {
				char errstr[1024] = {0};
				std::snprintf(
					 errstr
					,sizeof(errstr)
					,"%s::%s(): bad call_version %d for call_id %d(%s::%s())"
					,"client_invoker"
					,__FUNCTION__
					,static_cast<int>(call_version)
					,static_cast<int>(call_id)
					,"client_invoker"
					,names[call_id]
				);
				throw std::runtime_error(errstr);
			}
			
			switch ( call_id ) {
				case 0: {
					switch ( call_version ) {
						case 0: {
							std::string arg0;
							std::string arg1;
							ia & arg0
								& arg1;
							
							impl.on_registration(arg0, arg1);
						};
						break;
					}
				};
				break;
				case 1: {
					switch ( call_version ) {
						case 0: {
							std::string arg0;
							ia & arg0;
							
							impl.on_activation(arg0);
						};
						break;
					}
				};
				break;
				case 2: {
					switch ( call_version ) {
						case 0: {
							std::string arg0;
							ia & arg0;
							
							impl.on_login(arg0);
						};
						break;
					}
				};
				break;
				case 3: {
					switch ( call_version ) {
						case 0: {
							std::string arg0;
							ia & arg0;
							
							impl.on_logout(arg0);
						};
						break;
					}
				};
				break;
				case 4: {
					switch ( call_version ) {
						case 0: {
							std::vector<std::string> arg0;
							ia & arg0;
							
							impl.on_users_online(arg0);
						};
						break;
					}
				};
				break;
				case 5: {
					switch ( call_version ) {
						case 0: {
							std::uint8_t arg0;
							std::uint8_t arg1;
							std::string arg2;
							ia & arg0
								& arg1
								& arg2;
							
							impl.on_yarmi_error(arg0, arg1, arg2);
						};
						break;
					}
				};
				break;
			}
		} catch (const std::exception &ex) {
			char errstr[1024] = {0};
			std::snprintf(
				 errstr
				,sizeof(errstr)
				,"std::exception is thrown when %s::%s() is called: '%s'"
				,"client_invoker"
				,names[call_id]
				,ex.what()
			);
			yarmi_error(call_id, call_version, errstr);
		} catch (...) {
			char errstr[1024] = {0};
			std::snprintf(
				 errstr
				,sizeof(errstr)
				,"unknown exception is thrown when %s::%s() is called"
				,"client_invoker"
				,names[call_id]
			);
			yarmi_error(call_id, call_version, errstr);
		}
	}
private:
	Impl &impl;
	IO &io;
}; // struct client_invoker

template<typename Impl, typename IO = Impl>
struct server_invoker {
	server_invoker(Impl &impl, IO &io)
		:impl(impl)
		,io(io)
	{}
	
	void registration(const std::string &arg0, const std::string &arg1) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(0)
			& static_cast<std::uint8_t>(0)
			& arg0
			& arg1;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void activation(const std::string &arg0) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(1)
			& static_cast<std::uint8_t>(0)
			& arg0;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void login(const std::string &arg0) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(2)
			& static_cast<std::uint8_t>(0)
			& arg0;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void logout(const std::string &arg0) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(3)
			& static_cast<std::uint8_t>(0)
			& arg0;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void users_online(const std::vector<std::string> &arg0) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(4)
			& static_cast<std::uint8_t>(0)
			& arg0;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	void yarmi_error(const std::uint8_t &arg0, const std::uint8_t &arg1, const std::string &arg2) {
		yas::binary_mem_oarchive oa(yas::no_header);
		oa & static_cast<std::uint8_t>(5)
			& static_cast<std::uint8_t>(0)
			& arg0
			& arg1
			& arg2;
		yas::binary_mem_oarchive pa;
		pa & oa.get_intrusive_buffer();
		io.send(pa.get_shared_buffer());
	}
	
	void invoke(const char *ptr, std::size_t size) {
		std::uint8_t call_id, call_version;
		static const char* names[] = {
			 "registration"
			,"activation"
			,"login"
			,"logout"
			,"users_online"
			,"yarmi_error"
		};
		static const std::uint8_t versions[] = { 0, 0, 0, 0, 0, 0 };
		
		try {
			yas::binary_mem_iarchive ia(ptr, size, yas::no_header);
			ia & call_id
				& call_version;
			
			if ( call_id < 0 || call_id > 5 ) {
				char errstr[1024] = {0};
				std::snprintf(
					 errstr
					,sizeof(errstr)
					,"%s::%s(): bad call_id %d"
					,"server_invoker"
					,__FUNCTION__
					,static_cast<int>(call_id)
				);
				throw std::runtime_error(errstr);
			}
			if ( call_version > versions[call_id] ) {
				char errstr[1024] = {0};
				std::snprintf(
					 errstr
					,sizeof(errstr)
					,"%s::%s(): bad call_version %d for call_id %d(%s::%s())"
					,"server_invoker"
					,__FUNCTION__
					,static_cast<int>(call_version)
					,static_cast<int>(call_id)
					,"server_invoker"
					,names[call_id]
				);
				throw std::runtime_error(errstr);
			}
			
			switch ( call_id ) {
				case 0: {
					switch ( call_version ) {
						case 0: {
							std::string arg0;
							ia & arg0;
							
							impl.on_registration(arg0);
						};
						break;
					}
				};
				break;
				case 1: {
					switch ( call_version ) {
						case 0: {
							std::string arg0;
							std::string arg1;
							std::string arg2;
							ia & arg0
								& arg1
								& arg2;
							
							impl.on_activation(arg0, arg1, arg2);
						};
						break;
					}
				};
				break;
				case 2: {
					switch ( call_version ) {
						case 0: {
							std::string arg0;
							std::string arg1;
							ia & arg0
								& arg1;
							
							impl.on_login(arg0, arg1);
						};
						break;
					}
				};
				break;
				case 3: {
					switch ( call_version ) {
						case 0: {
							impl.on_logout();
						};
						break;
					}
				};
				break;
				case 4: {
					switch ( call_version ) {
						case 0: {
							impl.on_users_online();
						};
						break;
					}
				};
				break;
				case 5: {
					switch ( call_version ) {
						case 0: {
							std::uint8_t arg0;
							std::uint8_t arg1;
							std::string arg2;
							ia & arg0
								& arg1
								& arg2;
							
							impl.on_yarmi_error(arg0, arg1, arg2);
						};
						break;
					}
				};
				break;
			}
		} catch (const std::exception &ex) {
			char errstr[1024] = {0};
			std::snprintf(
				 errstr
				,sizeof(errstr)
				,"std::exception is thrown when %s::%s() is called: '%s'"
				,"server_invoker"
				,names[call_id]
				,ex.what()
			);
			yarmi_error(call_id, call_version, errstr);
		} catch (...) {
			char errstr[1024] = {0};
			std::snprintf(
				 errstr
				,sizeof(errstr)
				,"unknown exception is thrown when %s::%s() is called"
				,"server_invoker"
				,names[call_id]
			);
			yarmi_error(call_id, call_version, errstr);
		}
	}
private:
	Impl &impl;
	IO &io;
}; // struct server_invoker
} // ns yarmi

(в качестве сериализации используется другой мой проект — 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);

   void on_registration(const std::string &msg, const std::string &regkey);
   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)
Всё!

Из недоделок нужно отметить следующие:
1. ID процедуры yarmi_error() — «плавает». Это будет исправлено. (уже исправлено. за yarmi_error() закреплен нулевой ID.)
2. Даже когда у нас всего одна версия процедуры, генерируется swith() для версий процедур. Этого я пока не могу исправить. Проблема в том, что в ветке кодогенерации используется препроцессорный LAZY_IF(), в который нужно поместить еще один LAZY_IF(), который и будет определять, генерировать ли switch() для версий, или нет. Но! В ветке, образующейся путем использования LAZY_IF(), второй LAZY_IF() не будет развернут. Такова особенность препроцессора, он однопроходный.
К тому же, я право не уверен, что компилятор не сможет раскусить эту бессмысленную конструкцию, и вырезать непутевый swith()
3. В именах типов, описывающих процедуры, нельзя использовать запятые, ибо препроцессор не понимает контекста, в котором они используются. Будет исправлено.

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

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

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

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

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

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

PS
Этот код используется в нескольких наших коммерческих проектах, в геймдеве.

Автор: niXman

Источник

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


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