суббота, 19 сентября 2015 г.

[prog.sobjectizer] Новый пример convert_lib из грядущей версии 5.5.9

Одной из ключевых возможностей SObjectizer-5, которой не наблюдается у аналогов вроде C++ Actor Framework и Just::Thread Pro, является возможность запустить несколько независимых друг от друга экземпляров SObjectizer Environment внутри одного приложения. Грубо говоря, в CAF всего один ран-тайм на приложение, а в SO-5 таких может быть сколько угодно.

Важность такой фичи была хорошо прочувствована на собственной шкуре еще во времена SObjectizer-4, в котором ран-тайм был как раз один-единственный на приложение. Мы тогда сделали пару прикладных библиотек для заказчиков. Снаружи был обычный C++, а вот в потрохах, глубоко упрятанные от пользователя, работали агенты SO-4. Было удобно: мы легко пишем многопоточный код, клиенты ничего не знают про SObjectizer. Но эта благодать продолжалась не очень долго...

В один прекрасный момент нам потребовалось задействовать одну из таких библиотек внутри своего же SObjectizer-приложения. И тут выяснилось, что и приложение хочет управлять SO-4 ран-таймом, и библиотека. А потому они мешают друг другу. Пришлось выпускать новую версию библиотеки, в которой можно было указать опцию работать внутри уже имеющегося ран-тайма. Выглядело, да и являлось, все это настоящими костылями. Поэтому в самом начале работ над SO-5 была поставлена цель обеспечить возможность существования и независимой работы нескольких ран-таймов внутри одного приложения.

Так появилось понятие SO Environment, т.е. контейнера, который содержит ран-тайм и все кооперации с агентами, создаваемые пользователем.

Судя по отзывам на профильных форумах, понятие SO Environment как-то туго заходит. Уж не знаю почему. Может народ привык к приложениям в Erlang стиле, где есть всего один ран-тайм и абсолютно все живет внутри него. Может потому, что наличие SO Environment повышает многословность SObjectizer-овского кода, особенно если сравнивать с CAF/Just::Thread. Может потому, что в C++ модель акторов пока используется редко и в специфических областях.

Как бы то ни было, SO Environment уже есть, со своей задачей успешно справляется. Поэтому единственное, что остается, -- это пытаться на пальцах объяснять, где наличие нескольких экземпляров SO Environment может быть выгодно.

Для этих целей в состав разрабатывающейся сейчас версии 5.5.9 включен новый пример convert_lib. Этот пример показывает, как может выглядеть библиотека, которая выставляет наружу plain-old-C интерфейс, а внутри себя использует SO Environment. Пример игрушечный, демонстрационный. Но показанные в нем принципы работают и в реальных условиях.

В примере convert_lib показана библиотека для конвертации целых чисел из строкового представления в числовое. Все очень примитивно, но для демонстрации самое то. В реальной жизни может быть что-то посерьезнее, например, поддержка криптографии, общение с внешним оборудованием, тяжелые числодробилки и т.д. Целью примера было показать, как это все можно делать и конвертация чисел, имхо, подходит весьма неплохо.

Интерфейс "библиотеки" типичный для plain-old-C библиотек. Сначала посредством create_converter нужно создать экземпляр конвертера и получить его хендл. Когда конвертер больше не нужен, его экземпляр по хендлу уничтожается функцией destroy_converter. Для конвертации строки в число используется функция convert_value, куда так же передается хендл.

Интерфейс у "библиотеки" чисто сишный, хотя внутри используется махровый C++11. В принципе, если разделить код на h-файл и .cpp-файлы, то можно было бы создать библиотеку, которую можно было бы линковать как с C-шному коду, так и использовать из других языков, имеющий С-шный FFI (например, Ruby, Python, Erlang и т.д.). Но сейчас все собрано в одном единственном файле для того, чтобы код примера можно было затолкнуть в генерируемую Doxygen-ом документацию.

Полный код примера с достаточным количеством пояснительных комментариев приведен ниже. Пока же добавлю еще несколько слов.

В коде используется несколько новых фич версии 5.5.9, которых раньше не было и которые несколько упрощают использование SObjectizer. Во-первых, это возможность отсылать в качестве сообщений агентов объекты любых типов, наследование от so_5::rt::message_t уже не обязательно. Так, в примере строка для конвертации отсылается просто в виде std::string и агент-конвертер подписывается именно на получение std::string-а из почтового ящика.

Во-вторых, в 5.5.9 добавлены более простые функции для выполнения синхронных запросов к агентам. Так, в коде есть вызов so_5::request_value() вместо цепочки из get_one().waif_forever().make_sync_get(). Длинные цепочки вызовов появились во времена, когда нужно было поддерживать VC++ные компиляторы без variadic templates. Теперь таких ограничений нет и можно создавать более удобное API.

В-третьих, используется новый класс wrapped_env_t, который является специальной оберткой над экземпляром SO Environment и предназначен для предоставления возможности использования SO-5 в привычной для многих C++ников манере, вроде:

int main() {
  so_5::wrapped_env_t env; // Запуск пустого Environment.
  ... // Какой-то код.
  env.introduce_coop(...); // Наполнение Environment.
  ... // Еще какой-то код.
  env.stop_then_join(); // Останов и ожидание завершения Environment.
  ... // Еще какой-то код.
  return 0;
}

В примере каждый вызов create_converter как раз приводит к созданию нового объекта wrapped_env_t. А указатель на wrapped_env_t и является хендлом конвертера.

Ну а вот, собственно, и весь код примера с комментариями (в варианте C++14).

И у меня есть просьба к заинтересовавшимся читателям: если вам кажется, что что-то выглядит коряво и сложно, и вам хотелось бы видеть что-то другое, то не сочтите за труд и укажите, что именно. Это сильно поможет сделать SO-5 лучше и ближе к потенциальным пользователям.

/*
 * Очень простой пример, демонстрирующий использование SO Environment
 * внутри библиотеки, выставляющей наружу plain-old-C интерфейс.
 */
#include <iostream>
#include <string>
#include <stdexcept>
#include <sstream>

// Подключаем все необходимое из SObjectizer.
#include <so_5/all.hpp>

//
// API библиотеки.
// Функции create_converter и convert_value возвращают 0 в случае
// успешного завершения или отличный от 0 код ошибки в случае неудачи.
//
// Псевдо-тип для хендла конкретного экземпляра конвертера.
struct converter;
// Создание экземпляра конвертера.
// Если создание завершается успешно, аргумент handle_receiver
// получает хендл созданного экземпляра.
extern "C" int create_converter( converter ** handle_receiver );
// Конвертация значения.
// Если конвертация завершается успешно, аргумент receiver
// получает полученное из source_value значение.
extern "C" int convert_value(
  converter * handle, 
  const char * source_value,
  int * receiver );
// Уничтожение экземпляра конвертера по его хэндлу.
extern "C" void destroy_converter( converter * handle );

//
// Использование библиотеки.
//

// Демонстрационная функция, которая получает вектор значений для
// конвертации, пытается конвертировать каждое из них и возвращает
// вектор описаний результатов конвертации (успешно ли конвертировано
// значение или нет).
std::vector< std::string >
make_conversion(
  const std::vector< std::string > & values )
{
  // Создаем экземпляр конвертера и сохраняем его хендл.
  converter * handle = nullptr;
  const int rc1 = create_converter( &handle );
  if( rc1 )
    // При ошибке создания просто выбрасываем исключение и завершаем работу.
    throw std::runtime_error( "converter creation error: " +
        std::to_string( rc1 ) );

  // Для того, чтобы автоматически удалить экземпляр конвертера
  // используется std::unique_ptr. Разрушение converter_deleter при
  // выходе из make_conversion приведет к автоматическому вызову
  // библиотечной функции destroy_converter.
  std::unique_ptr< converter, void (*)(converter *) > converter_deleter(
      handle, destroy_converter );

  std::vector< std::string > result;

  // Выполняем конверсию всех входных значений и накапливаем результирующие
  // описания в переменной result.
  forconst auto & s : values )
  {
    // Выполняем конверсию посредством вызова библиотечной функции.
    int int_value = 0;
    const int rc2 = convert_value( handle, s.c_str(), &int_value );

    // Создаем описание в зависимости от результата.
    if( rc2 )
      result.push_back( "error=" + std::to_string( rc2 ) );
    else
      result.push_back( "success=" + std::to_string( int_value ) );
  }

  return result;
}

// Основная функция, которая запускает демонстарацию.
// Внутри создается две исходных последовательности, для обработки
// которых выполняется два асинхронных вызова make_conversion.
// Т.е. каждая последовательность обрабатывается независимо и,
// возможно, обработка осуществляется параллельно.
void demo()
{
  // Две исходные последовательности.
  std::vector< std::string > seq1{ "1""2""three""4" };
  std::vector< std::string > seq2{ "11""12""thirteen""14" };

  // Запуск асинхронной обработки двух последовательностей.
  // Возвращаются объекты std::future из которых затем будет
  // извлекаться результирующее значение.
  auto f1 = std::async( make_conversion, std::cref(seq1) );
  auto f2 = std::async( make_conversion, std::cref(seq2) );

  // Вспомогательная вложенная функция для печати результатов.
  auto printer = []( auto * name, const auto & result ) {
      std::cout << name << ": " << std::flush;
      forconst auto & i : result )
        std::cout << i << ",";
      std::cout << std::endl;
    };

  // Получение и печать результатов.
  // Если результаты еще не готовы, то произойдет ожидание внутри get().
  printer( "First sequence", f1.get() );
  printer( "Second sequence", f2.get() );
}

// Главная фукнция, в которой запускается демонстрация и отлавливаются ошибки.
int main()
{
  try
  {
    demo();
  }
  catchconst std::exception & x )
  {
    std::cerr << "Exception: " << x.what() << std::endl;
    return 2;
  }

  return 0;
}

//
// Реализация библиотеки.
//

extern "C" int create_converter( converter ** handle_receiver )
{
  try
  {
    // Каждому экземпляру конвертера будет соответствовать свой экземпляр
    // SO Environment.
    // Этот экземпляр должен быть создан динамически.
    // Временно он хранится в unique_ptr для обеспечения exception safety.
    std::unique_ptr< so_5::wrapped_env_t > env{ new so_5::wrapped_env_t{} };

    // Для работы конвертера в SO Environment нужно создать одну
    // кооперацию с единственным агентом внутри.
    env->environment().introduce_coop( [&]( so_5::rt::coop_t & coop ) {
      // Для конвертации нужно отослать сообщение в почтовый ящик,
      // откуда его заберет агент-конвертер.
      // Это будет именованный почтовый ящик с жестко зафиксированным именем,
      // что позволит обращаться к нему из функции convert_value.
      auto mbox = coop.environment().create_local_mbox( "converter" );
      // Агент-конвертер.
      // Обрабатывает единственное сообщение, поэтому нет смысла
      // описывать тип агента в виде C++ класса. Достаточно простого
      // ad-hoc агента с единственным событием.
      // Агент обрабатывает сообщение типа std::string, отосланное на почтовый
      // ящик с именем "converter". В результате его обработки либо
      // возвращается целое число, извлеченное из исходной строки, либо же
      // порождается исключение.
      coop.define_agent().event( mbox, []( const std::string & v ) -> int {
          std::istringstream s{ v };
          int result;
          s >> result;
          if( s.fail() )
            throw std::invalid_argument( "unable to convert to int: '" + v + "'" );
          return result;
        } );
      } );

    // Экземпляр конвертера полностью сформирован, теперь можно возвратить
    // указатель на SO Environment в качестве хендла.
    *handle_receiver = reinterpret_cast< converter * >( env.release() );
  }
  catchconst std::exception & )
  {
    return -1;
  }

  return 0;
}

extern "C" int convert_value(
  converter * handle, 
  const char * source_value,
  int * receiver )
{
  try
  {
    // Мы знаем, что хендл -- это указатель на SO Environment.
    auto env = reinterpret_cast< so_5::wrapped_env_t * >( handle );

    // Для получения результата конвертации используем синхронный запрос.
    // Т.е. текущая нить будет приостановлена до тех пор, пока сообщение
    // типа std::string, отосланное в почтовый ящик с именем "converter",
    // не будет обработано (или проигнорировано).
    *receiver = so_5::request_value< int, std::string >(
      // Получаем ссылку на почтовый ящик.
      env->environment().create_local_mbox( "converter" ),
      // Ждем без ограничений по времени.
      so_5::infinite_wait,
      // Содержимое сообщения, которое будет отослано.
      source_value );
  }
  catchconst std::exception & )
  {
    return -2;
  }

  return 0;
}

extern "C" void destroy_converter( converter * handle )
{
  // Просто удаляем ставший ненужным SO Environment.
  // В процессе удаления SO Environment будет остановлен,
  // работающий внутри SO Environment агент будет дерегистрирован,
  // а все созданные SO Environment ресурсы будут освобождены.
  auto env = reinterpret_cast< so_5::wrapped_env_t * >( handle );
  delete env;
}

Комментариев нет: