В этой статье я опишу процесс разработки класса «таблицы вызовов» и применение получившегося класса для расширения функциональности программы с помощью модулей.
О задаче
Есть сервер, принимающий команды. На вход он получает индекс нужной команды и ее параметры, выполняет действия и возвращает результат. Индексы команд последовательны: 0,1,2,3 и т.д. При старте у сервера есть несколько базовых команд(в моем случае 20), остальные добавляются модулями во время работы. Для решения этой задачи хорошо подходит CallTable.
Написание класса CallTable
Класс CallTable должен быть:
- Безопасным(без неопределенного поведения)
- Удобным(без ручного приведения типов)
- Расширяемым(возможность в Runtime изменить размер таблицы)
В ядре Linux используется механизм calltable для системных вызовов. Мы должны получить нечто похожее, но лишённое ограничений, присущих ядру.
class CallTable
{
private:
CallTable( const CallTable& ) = delete; //Запрещаем копирование
void operator=( const CallTable& ) = delete; //Запрещаем копирование
public:
typedef message_result::results CmdResult;
typedef CmdResult (*CallCell)(std::string);
CallCell default_cell;
CallCell* table;
unsigned int size;
unsigned int autoincrement;
CallTable(unsigned int size,CallCell _default);
unsigned int add(CallCell c);
bool realloc(unsigned int newsize);
~CallTable();
};
Указатель table будет указывать на нашу таблицу
size хранит текущий размер таблицы
autoincrement хранит последний добавленный элемент
Конструктор должен будет выделить нужный объем памяти и инициализировать нашу таблицу вызовом-по-умолчанию
CallTable::CallTable(unsigned int size,CallCell _default)
{
table = new CallCell[size];
this->size = size;
for(unsigned int i=0;i<size;++i)
{
table[i] = _default;
}
default_cell = _default;
autoincrement = 0;
}
Функция add нужна для добавления элемента в таблицу. Так как мы заранее не знаем какие индексы попадутся какому вызову, мы должны вернуть индекс только что добавленного вызова из функции add:
unsigned int CallTable::add(CallCell c)
{
if(autoincrement == size) return -1;
table[autoincrement] = c;
autoincrement++;
return autoincrement - 1;
}
Функция realloc займется расширением таблицы. Для этого нужно выделить память нужного размера, скопировать элементы, оставшиеся неинициализированные области заполнить вызовом-по-умолчанию
bool CallTable::realloc(unsigned int newsize)
{
if(newsize < size) return false;
CallCell* newtable = new CallCell[newsize];
memcpy(newtable,table,size*sizeof(CallCell));
delete[] table;
for(unsigned int i=size;i<newsize;++i)
{
newtable[i] = default_cell;
}
table = newtable;
size = newsize;
return true;
}
Деструктор отчистит выделенную память
CallTable::~CallTable()
{
delete[] table;
}
Используем CallTable для добавления новых команд
Напишем программу, использующую CallTable. Для упрощения кода в статье команды будут приниматься через stdin вместо сокетов.
Функционал тестовой программы:
- На вход принимать строку вида «НомерКоманды Параметр»
- 3 тестовых команды: echo, loadmodule, stop
- Начальный размер таблицы: 4(1 команда останется неинициализированной)
Программу мы расширим с помощью модуля. Комманда loadmodule загрузит нужный модуль и выполнит функцию инициализации.
Функционал тестового модуля:
- Функция инициализации принимает на вход указатель на CallTable, производит добавление функций и завершается
- Конечный размер таблицы: 5
- 2 тестовых комманды: echomodule и testprogram
Тестовая программа
Инициализируем CallTable
std::cout << "Инициализация CallTable ";
table = new CallTable(4,&cmd_unknown);
std::cout << "OK" << std::endl;
Напишем функции для команд echo, stop и дефолтной:
message_result::results cmd_unknown(std::string)
{
return message_result::results::ERROR_CMDUNKNOWN;
}
message_result::results cmd_stop(std::string)
{
isContinue = false;
std:: cout << "[CMD STOP] Stopping" << std::endl;
return message_result::results::OK;
}
message_result::results cmd_echo(std::string e)
{
std:: cout << "[CMD ECHO] " << e << std::endl;
return message_result::results::OK;
}
Загружать модуль будем с помощью функций библиотеки libdl. Для Windows команда загрузки модуля будет отличаться.
message_result::results cmd_loadmodule(std::string file)
{
void* fd = dlopen(file.c_str(), RTLD_LAZY);
if(fd == NULL) {
return message_result::results::ERROR_FILENOTFOUND;
}
void (*test_module_main)(CallTable*);
test_module_main = (void (*)(CallTable*))dlsym(fd,"test_module_call_main");
if(test_module_main == NULL) {
dlclose(fd);
return message_result::results::ERROR_FILENOTFOUND;
}
test_module_main(table);
return message_result::results::OK;
};
Теперь эти комманды нужно добавить в таблицу:
std::cout << "Запись команд ";
table->add(&cmd_echo);
table->add(&cmd_loadmodule);
table->add(&cmd_stop);
std::cout << "OK" << std::endl;
В главном цикле нам нужно обработать команду, получить результат и вывести его в удобочитаемом виде
while(isContinue)
{
unsigned int cmdnumber = 0;
std::string param;
std::cin >> cmdnumber >> param;
if(cmdnumber >= table->size) {
std::cerr << "Команда не существует" << std::endl;
continue;
}
message_result::results r = table->table[cmdnumber](param);
using message_result::results;
if(r == results::OK) {}
else if(r == results::ERROR_FILENOTFOUND)
{
std::cout << "Файл не найден" << std::endl;
}
else if(r == results::ERROR_CMDUNKNOWN)
{
std::cout << "Вызвана default комманда" << std::endl;
}
}
Запускаем программу и смотрим на результат:
0 test
[CMD ECHO] test
2 stop
[CMD STOP] Stopping
Написание модуля
Напишем команды для модуля: echomodule и testprogram
message_result::results cmd_testprogram(std::string)
{
std:: cout << "I am module!" << std::endl;
return message_result::results::OK;
}
message_result::results cmd_echomodule(std::string e)
{
std:: cout << "[MODULE ECHO] " << e << std::endl;
return message_result::results::OK;
}
Функция инициализации должна вызвать realloc и добавить две своих функции в таблицу вызовов
extern "C"
{
void test_module_call_main(CallTable* table);
};
void test_module_call_main(CallTable* table)
{
std::cout << "Инициализация модуля" << std::endl;
table->realloc(5);
table->add(&cmd_testprogram);
table->add(&cmd_echomodule);
std::cout << "Инициализация модуля завершена" << std::endl;
}
Что происходит
- Программа запускается, инициализирует CallTable и добавляет базовые комманды в таблицу
- Запускается цикл обработки команд. Пользователь дает комманду с номером 4, но так как команды еще не существует, он получает ошибку.
- Пользователь дает команду на загрузку модуля
- В функцию инициализации модуля передается указатель на CallTable, модуль увеличивает размер таблицы до 5 и добавляет туда две своих команды
- Пользователь запрашивает команду с номером 4, теперь эта команда была добавлена загруженным модулем, и она выполняется.
- Пользователь останавливает программу командой с номером 2
Итог
С помощьюю CallTable можно создавать гибкие сервисы, функционал которых может расширяться с помощью модулей. Кроме добавления собственных команд, модули могут вызывать, переопределять, удалять уже существующие команды, вставлять свои действия до и/или после команды, запускать свои потоки и всячески расширять функционал приложения. Ссылка на github с кодом этой статьи.
Автор: Gravit