Хабрахабр

Реализация обработки команд на CallTable с модулями на современном C++

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

О задаче

Есть сервер, принимающий команды. На вход он получает индекс нужной команды и ее параметры, выполняет действия и возвращает результат. Индексы команд последовательны: 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 вместо сокетов.
Функционал тестовой программы:

  1. На вход принимать строку вида «НомерКоманды Параметр»
  2. 3 тестовых команды: echo, loadmodule, stop
  3. Начальный размер таблицы: 4(1 команда останется неинициализированной)

Программу мы расширим с помощью модуля. Комманда loadmodule загрузит нужный модуль и выполнит функцию инициализации.
Функционал тестового модуля:

  1. Функция инициализации принимает на вход указатель на CallTable, производит добавление функций и завершается
  2. Конечный размер таблицы: 5
  3. 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;
}

Что происходит

  1. Программа запускается, инициализирует CallTable и добавляет базовые комманды в таблицу
  2. Запускается цикл обработки команд. Пользователь дает комманду с номером 4, но так как команды еще не существует, он получает ошибку.
  3. Пользователь дает команду на загрузку модуля
  4. В функцию инициализации модуля передается указатель на CallTable, модуль увеличивает размер таблицы до 5 и добавляет туда две своих команды
  5. Пользователь запрашивает команду с номером 4, теперь эта команда была добавлена загруженным модулем, и она выполняется.
  6. Пользователь останавливает программу командой с номером 2

Итог

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»