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

в 11:34, , рубрики: c++, calltable, Программирование, системное программирование

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

О задаче

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

Автор: Gravit

Источник

Поделиться

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