Русский
Русский
English
Статистика
Реклама

C++14

Под капотом сортировок в STL

08.10.2020 16:23:54 | Автор: admin


Стандарт С++ почти никогда не указывает, как именно должен быть реализован тот или иной std алгоритм. Дается только описание того, что на входе, что на выходе и асимптотические ограничения по времени работы и памяти. В статье я постарался прикинуть, какие математические алгоритмы и структуры данных имели ввиду авторы стандарта, указывая ограничения для той или иной сортировки и для некоторых других алгоритмов. А так же как эти алгоритмы реализованы на практике.


При написании статьи я использовал стандарт C++17. В качестве реализаций рассматривал GCC 10.1.0 (май 2020) и LLVM/Clang 10.0.0 (март 2020). В каждой и них есть своя реализация STL, а значит и std алгоритмов.


1. Однопоточные реализации


1.1. Готовые сортировки


  • std::sort(). Еще в стандарте C++98/C++03 мы видим, что сложность алгоритма примерно n*log(n) сравнений. А также есть примечание, что если важна сложность в худшем случае, то следует использовать std::stable_sort() или std::partial_sort(). Похоже, что в качестве реализации std::sort() подразумевался quicksort (в худшем случае O(n2) сравнений). Однако, начиная с C++11 мы видим, что сложность std::sort() уже O(n*log(n)) сравнений безо всяких оговорок. GCC реализует предложенную в 1997 году introsort (O(n*log(n)) сравнений, как в среднем, так и в худшем случае). Introsort сначала сортирует как quicksort, но вскоре переключается на heapsort и в самом конце сортировки, когда остаются небольшие интервалы (в случае GCC менее 16 элементов), сортирует их при помощи insertion sort. А вот LLVM реализует весьма сложный алгоритм с множеством оптимизаций в зависимости от размеров сортируемых интервалов и того, являются ли сортируемые элементы тривиально копируемыми и тривиально конструируемыми.
  • std::partial_sort(). Поиск некоторого числа элементов с минимальным значением из множества элементов и их сортировка. Во всех версиях стандарта сложность примерно n*log(m) сравнений, где n количество элементов в контейнере, а m количество минимальных элементов, которое нужно найти. Задача для heapsort. Сложность в точности совпадает с этим алгоритмом. Так и реализовано в LLVM и GCC.
  • std::stable_sort(). Тут немного сложнее. Во-первых, в отличии от предыдущих сортировок в стандарте отмечено, что она стабильная. Т.е. не меняет местами эквивалентные элементы при сортировке. Во-вторых, сложность ее в худшем случае n*(log(n))2 сравнений и n*log(n) сравнений, если есть достаточно памяти. Т.е. имеется ввиду 2 разных алгоритма стабильной сортировки. В варианте, когда памяти много подходит стандартный merge sort. Как раз ему требуется дополнительная память для работы. Сделать merge sort без дополнительной памяти за O(n*log(n)) сравнений так же возможно. Но это сложный алгоритм и не смотря на асимптотику n*log(n) сравнений константа у него велика, и в обычных условиях он будет работать не очень быстро. Поэтому обычно используется вариант merge sort без дополнительной памяти, который имеет асимптотику n*(log(n))2 сравнений. И в GCC и в LLVM реализации в целом похожи. Реализованы оба алгоритма: один работает при наличии памяти, другой когда памяти не хватает. Обе реализации, когда дело доходит до небольших интервалов, используют insertion sort. Она стабильная и не требует дополнительной памяти. Но ее сложность O (n2) сравнений, что не играет роли на маленьких интервалах.
  • std::list::sort(), std::forward_list::sort(). Все перечисленные выше сортировки требуют итераторы произвольного доступа для задания сортируемого интервала. А что если требуется отсортировать контейнер, который не обеспечивает таких итераторов? Например, std::list или std::forward_list. У этих контейнеров есть специальный метод sort(). Согласно стандарту, он должен обеспечить стабильную сортировку за примерно n*log(n) сравнений, где n число элементов контейнера. В целом вполне подходит merge sort. Ее и реализуют GCC и LLVM и для std::list::sort(), и для std::forward_list::sort(). Но зачем вообще потребовались частные реализации сортировки для списков? Почему бы для std::stable_sort() просто не ослабить итераторы до однонаправленных или хотя бы двунаправленных, чтоб этот алгоритм можно было применять и к спискам? Дело в том, что в std::stable_sort() используются оптимизации, которые требуют итераторы произвольного доступа. Например, как я писал выше, когда дело доходит до сортировки не больших интервалов в std::stable_sort() разумно переключиться на insertion sort, а эта сортировка требует итераторы произвольного доступа.
  • std::make_heap(), std::sort_heap(). Алгоритмы по работе с кучей (max heap), включая сортировку. std::sort_heap() это единственный способ сортировки, алгоритм для которого указан явно. Сортировка должна быть реализована, как heapsort. Так и реализовано в LLVM и GCC.

Сводная таблица


Алгоритм Сложность согласно стандарту C++17 Реализация в GCC Реализация в LLVM/Clang
std::sort() O(n*log(n)) introsort -
std::partial_sort() O(n*log(m)) heapsort heapsort
std::stable_sort() O(n*log(n))/O(n*(log(n))2) merge sort merge sort
std::list::sort(), std::forward_list::sort() O(n*log(n)) merge sort merge sort
std::make_heap(), std::sort_heap() O(n*log(n)) heapsort heapsort

Примечание. В качестве реализации в GCC и LLVM указан алгоритм, который используется для больших сортируемых интервалов. Для особых случаев (не большие интервалы и т.п.) часто используются оптимизации.


1.2. Составляющие алгоритмов сортировки


  • std::merge(). Слияние двух сортированных интервалов. Этот алгоритм не меняет местами эквивалентные элементы, т.е. он стабильный. Количество сравнений не более, чем сумма длин сливаемых интервалов минус 1. На базе данного алгоритма очень просто реализовать merge sort. Однако напрямую этот алгоритм не используется в std::stable_sort() ни LLVM, ни в GCC. Для шага слияния в std::stable_sort() написаны отдельные реализации.
  • std::inplace_merge(). Этот алгоритм также реализует слияние двух сортированных интервалов и он также стабильный. У него есть интерфейсные отличия от std::merge(), но кроме них есть еще одно, очень важное. По сути std::inplace_merge() это два алгоритма. Один вызывается при наличии достаточного количества дополнительной памяти. Его сложность, как и в случае std::merge(), не более чем сумма длин объединяемых интервалов минус 1. А другой, если дополнительной памяти нет и нужно сделать слияние "in place". Сложность этого "in place" алгоритма n*log(n) сравнений, где n сумма элементов в сливаемых интервалах. Все это очень напоминает std::stable_sort(), и это не спроста. Как кажется, авторы стандарта предполагали использование std::inplace_merge() или подобных алгоритмов в std::stable_sort(). Эту идею отражают реализации. В LLVM для реализации std::stable_sort() используется std::inplace_merge(), в GCC для реализаций std::stable_sort() и std::inplace_merge() используются некоторые общие методы.
  • std::partition()/std::stable_partition(). Данные алгоритмы также можно использовать для написания сортировок. Например, для quicksort или introsort. Но ни GCC ни LLVM не использует их на прямую для реализации сортировок. Используются аналогичные им, но оптимизированные, для случая конкретной сортировки, варианты реализации.

2. Многопоточные реализации


В C++17 для многих алгоритмов появилась возможность задавать политику исполнения (ExecutionPolicy). Она обычно указывается первым параметром алгоритма. Алгоритмы сортировок не стали исключением. Политику исполнения можно задать для большинства алгоритмов рассмотренных выше. В том числе и указать, что алгоритм может выполняться в несколько потоков (std::execution::par, std::execution::par_unseq). Это значит, что именно может, а не обязан. А будет вычисляться в несколько потоков или нет зависит от целевой платформы, реализации и варианта сборки компилятора. Асимптотическая сложность также остается неизменной, однако константа может оказаться меньше за счет использования многих потоков.


Тут ключевое слово может. Дело в том, что однопоточные реализации, описанные выше, подходят для соответствующих алгоритмов, какая бы политика исполнения ни была задана. И однопоточные реализации часто используются на настоящий момент не зависимо от политики исполнения. Я рассмотрел следующие версии:


  • LLVM/Clang (Apple clang version 11.0.3 (clang-1103.0.32.62)) и MacOS 10.15.4. В этом случае заголовочный файл execution не нашелся. Т.е. политику многопоточности задать не получится;
  • LLVM/Clang 10.0.0 сборка из brew. Тот же результат, что и в случае Apple clang;
  • GCC 10.1.0 файл execution есть и политику задать можно. Но какая бы политика ни была задана, использоваться будет однопоточная версия. Для вызова многопоточной версии необходимо, чтобы был подключен файл tbb/tbb.h при компиляции на платформе Intel. А для этого должна быть установлена библиотека Intel Threading Building Blocks (TBB) и пути поиска заголовочных файлов были прописаны. Установлен ли TBB проверяется при помощи специальной команды в gcc: __has_include(<tbb/tbb.h>) в файле c++config.h. И если данный файл виден, то используется многопоточная версия написанная на базе Threading Building Blocks, а если нет, то последовательная. Про TBB немного подробнее ниже.
    Дополнительную информацию о поддержке компиляторам параллельных вычислений, как впрочем и другой функциональности, можно посмотреть здесь: https://en.cppreference.com/w/cpp/compiler_support

3. Intel Threading Building Blocks


Чтоб стало возможным использовать многопоточные версии разных алгоритмов, на сегодня нужно использовать дополнительные библиотеку Threading Building Blocks, разрабатываемую Intel. Это не сложно:


  • Клонируем репозиторий Threading Building Blocks с https://github.com/oneapi-src/oneTBB
  • Из корня запускаем make и ждем несколько минут пока компилируется TBB или make all и ждем пару часов, чтоб прошли еще и тесты
  • Далее при компиляции указываем пути к includes (-I oneTBB/include) и к динамической библиотеке (у меня был такой путь -L tbb/oneTBB/build/macos_intel64_clang_cc11.0.3_os10.15.4_release -ltbb, т.к. я собирал TBB при помощи Apple clang version 11.0.3 на MacOS)

4. Эпилог


Как кажется, когда комитет C++ описывает те или иные особенности языка в стандарте, он предполагает те или иные сценарии использования и те или иные алгоритмы реализации. Часто все развивается, как предполагалось. Иногда идет своим путем. Иногда не используется вовсе. Какие бы ни были реализации тех или иных алгоритмов или структур данных сегодня, завтра это может измениться. Измениться впрочем может и стандарт. Но меняясь, стандарт почти всегда обеспечивает совместимость с ранее написанным кодом. При выпуске новых версий компиляторов, могут меняться и реализации. Но изменяясь они предполагают, что разработчики полагаются на стандарт. На мой взгляд, имеет смысл понимать, как обычно реализуются те или иные части стандартной библиотеки. Но полагаться при разработке нужно именно на ограничения заданные стандартом.


Ссылки на упомянутые алгоритмы и библиотеку:



Благодарности


Большое спасибо Ольге Serine за замечание по статье и картинку.

Подробнее..

Из песочницы Валидация данных в C с использованием библиотеки cpp-validator

27.10.2020 12:09:43 | Автор: admin


Казалось бы, валидация данных это одна из базовых задач в программировании, которая встретится и в начале изучения языка вместе с "Hello world!", и в том или ином виде будет присутствовать в множестве зрелых проектов. Тем не менее, Google до сих пор выдает ноль релевантных результатов при попытке найти универсальную библиотеку валидации данных с открытым исходным кодом на C++.


В лучшем случае, там найдутся или инструменты проверки самого кода C++, или библиотеки валидации определенных форматов, например, таких как JSON или XML. Похоже на то, что либо разработчики для каждого случая реализуют валидацию данных вручную, либо инструменты валидации создаются под конкретный проект и плохо приспособлены для использования в качестве универсальных библиотек.


Если в комментариях кто-то сможет привести примеры открытых библиотек валидации данных на C++ помимо отдельных GUI-форм, то буду очень признателен и добавлю соответствующий список в статью.


Содержание



Мотивация


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


  • условия выборки записей в базе данных;
  • условия проверки на сервере содержимого команд, поступающих от клиентов;
  • условия проверки данных, вводимых через пользовательский интерфейс на клиенте, перед их записью в объект и отправкой на сервер.

Т.е., если с прикладной точки зрения во всех трех случаях речь идет о разных представлениях одной и той же сущности, то почему бы не сделать так, чтобы правила проверки характеристик этой сущности описывались бы одинаковым образом? И уже потом эти правила обрабатывать разными способами применительно к каждому конкретному случаю например, транслировать в строку запроса к базе данных или в проверку набора содержимого ранее заполненного объекта или в проверку отдельных переменных перед записью в объект.


В итоге, библиотека валидации разрабатывалась с учетом основного требования, чтобы было четкое разделение между:


  • описанием правил валидации;
  • реализацией обработчиков правил валидации;
  • обработкой конкретных правил валидации конкретным обработчиком.

То есть, правила валидации должны предварительно описываться в отдельном месте, желательно с применением синтаксиса, похожего на декларативный. В другом месте должны быть реализованы обработчики правил валидации. Причем, разные обработчики могут транслировать те же самые правила в разные операции например, один обработчик использует правила для фактической проверки данных, а другой обработчик транслирует правила в запросы SQL. И, собственно, третий участок это непосредственно применение правил валидации конкретным обработчиком в момент вызова процедуры валидации.


Возможности библиотеки


cpp-validator является header-only библиотекой для современного C++ с поддержкой стандартов C++14/C++17. В коде cpp-validator активно используется метапрограммирование на шаблонах и библиотека Boost.Hana.


Основные возможности библиотеки cpp-validator перечислены ниже.


  • Валидация данных для различных конструкций языка:
    • простых переменных;
    • свойств объектов, включая:
      • переменные классов;
      • методы классов вида getter;
    • содержимого и свойств контейнеров;
    • иерархических типов данных, таких как вложенные объекты и контейнеры.
  • Пост-валидация объектов, когда проверяется содержимое уже заполненного объекта на соответствие сразу всем правилам.
  • Пре-валидация данных, когда перед записью в объект проверяются только те свойства, которые планируется изменить.
  • Комбинация правил с использованием логических связок AND, OR и NOT.
  • Массовая проверка элементов контейнеров с условиями ALL или ANY.
  • Частично подготовленные правила валидации с отложенной подстановкой аргументов (lazy operands).
  • Сравнение друг с другом разных свойств одного и того же объекта.
  • Автоматическая генерация описания ошибок валидации:
    • широкие возможности по настройке генерации текста ошибок;
    • перевод текста ошибок на различные языки с учетом грамматических атрибутов слов, например, числа, рода и т.д.
  • Расширяемость:
    • регистрация новых свойств объектов, доступных для валидации;
    • добавление новых операторов правил валидации;
    • добавление новых обработчиков правил валидации (адаптеров).
  • Операторы, уже встроенные в библиотеку:
    • сравнения;
    • лексикографические, с учетом и без учета регистра;
    • существования элементов;
    • проверки вхождения в интервал или набор;
    • регулярные выражения.
  • Широкая поддержка платформ и компиляторов, включая компиляторы Clang, GCC, MSVC и операционные системы Windows, Linux, macOS, iOS, Android.

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


Базовая валидация данных с использованием cpp-validator выполняется в три шага:


  1. сперва создается валидатор, содержащий правила валидации, описанные с использованием почти-декларативного языка;
  2. затем валидатор применяется к объекту валидации;
  3. в конце проверяется результат валидации, для работы с которым может использоваться либо специальный объект ошибки, либо исключение.

// определение валидатораauto container_validator=validator(   _[size](eq,1), // размер контейнера должен быть равен 1   _["field1"](exists,true), // поле "field1" должно существовать в контейнере   _["field1"](ne,"undefined") // поле "field1" должно быть не равно "undefined");// успешная валидацияstd::map<std::string,std::string> map1={{"field1","value1"}};validate(map1,container_validator);// неуспешная валидация, с объектом ошибкиerror_report err;std::map<std::string,std::string> map2={{"field2","value2"}};validate(map2,container_validator,err);if (err){    std::cerr<<err.message()<<std::endl;    /* напечатает:    field1 must exist    */}// неуспешная валидация, с исключениемtry{    std::map<std::string,std::string> map3={{"field1","undefined"}};    validate(map3,container_validator);}catch(const validation_error& ex){    std::cerr<<ex.what()<<std::endl;    /* напечатает:    field1 must be not equal to undefined    */}

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


Текущий статус библиотеки


Библиотека cpp-validator доступна на GitHub по адресу https://github.com/evgeniums/cpp-validator и готова к использованию на момент написания статьи номер стабильной версии 1.0.2. Библиотека распространяется под лицензией Boost 1.0.


Приветствуются замечания, пожелания и дополнения.


Примеры


Тривиальная валидация числа


// определение валидатораauto v=validator(gt,100); // больше чем 100// объект ошибкиerror err;// условия не выполненыvalidate(90,v,err);if (err){  // валидация неуспешна}// условия выполненыvalidate(200,v,err);if (!err){  // валидация успешна}

Валидация с исключением


// определение валидатораauto v=validator(gt,100); // больше чем 100try{    validate(200,v); // успешно    validate(90,v); // генерирует исключение}catch (const validation_error& err){    std::cerr << err.what() << std::endl;    /* напечатает:    must be greater than 100    */}

Явное применение валидатора к переменной


// определение валидатораauto v=validator(gt,100); // больше чем 100// применить валидатор к переменнымint value1=90;if (!v.apply(value1)){  // валидация неуспешна}int value2=200;if (v.apply(value2)){  // валидация успешна}

Составной валидатор


// валидатор: размер меньше 15 и значение бинарно больше или равно "sample string"auto v=validator(  length(lt,15),  value(gte,"sample string"));// явное применение валидатора к переменнымstd::string str1="sample";if (!v.apply(str1)){  // валидация неупешна потому что sample бинарно меньше, чем sample string}std::string str2="sample string+";if (v.apply(str2)){  // валидация успешна}std::string str3="too long sample string";if (!v.apply(str3)){  // валидация неуспешна, потому что длина строки больше 15 символов}

Проверить, что число входит в интервал, и напечатать описание ошибки


// валидатор: входит в интервал [95,100]auto v=validator(in,interval(95,100));// объект ошибкиerror_report err;// проверить значениеsize_t val=90;validate(val,v,err);if (err){    std::cerr << err.message() << std::endl;     /* напечатает:    must be in interval [95,100]    */}

Составной валидатор для проверки элемента контейнера


// составной валидаторauto v=validator(                _["field1"](gte,"xxxxxx")                 ^OR^                _["field1"](size(gte,100) ^OR^ value(gte,"zzzzzzzzzzzz"))            );// валидация контейнера и печать ошибкиerror_report err;std::map<std::string,std::string> test_map={{"field1","value1"}};validate(test_map,v,err);if (err){    std::cerr << err.message() << std::endl;    /* напечатает:    field1 must be greater than or equal to xxxxxx OR size of field1 must be greater than or equal to 100 OR field1 must be greater than or equal to zzzzzzzzzzzz    */}

Проверить элементы вложенных контейнеров


// составной валидатор элементов вложенных контейнеровauto v=validator(                _["field1"][1](in,range({10,20,30,40,50})),                _["field1"][2](lt,100),                _["field2"](exists,false),                _["field3"](empty(flag,true))            );// валидация вложенного контейнера и печать ошибкиerror_report err;std::map<std::string,std::map<size_t,size_t>> nested_map={            {"field1",{{1,5},{2,50}}},            {"field3",{}}        };validate(nested_map,v,err);if (err){    std::cerr << err.message() << std::endl;    /* напечатает:    element #1 of field1 must be in range [10, 20, 30, 40, 50]    */}

Провести валидацию кастомного свойства объекта


// структура с getter методомstruct Foo{    bool red_color() const    {        return true;    }};// зарегистрировать новое свойство red_colorDRACOSHA_VALIDATOR_PROPERTY_FLAG(red_color,"Must be red","Must be not red");// валидатор зарегистрированного свойства red_colorauto v=validator(    _[red_color](flag,false));// провести валидацию кастомного свойства и напечатать ошибкуerror_report err;Foo foo_instance;validate(foo_instance,v,err);if (err){    std::cerr << err.message() << std::endl;    /* напечатает:    "Must be not red"    */}

Пре-валидация данных перед записью


// структура с переменными и методом вида setterstruct Foo{    std::string bar_value;    uint32_t other_value;    size_t some_size;    void set_bar_value(std::string val)    {        bar_value=std::move(val);    }};using namespace DRACOSHA_VALIDATOR_NAMESPACE;// зарегистрировать кастомные свойстваDRACOSHA_VALIDATOR_PROPERTY(bar_value);DRACOSHA_VALIDATOR_PROPERTY(other_value);// специализация шаблона класса set_member_t для записи свойства bar_value структуры FooDRACOSHA_VALIDATOR_NAMESPACE_BEGINtemplate <>struct set_member_t<Foo,DRACOSHA_VALIDATOR_PROPERTY_TYPE(bar_value)>{    template <typename ObjectT, typename MemberT, typename ValueT>    void operator() (            ObjectT& obj,            MemberT&&,            ValueT&& val        ) const    {        obj.set_bar_value(std::forward<ValueT>(val));    }};DRACOSHA_VALIDATOR_NAMESPACE_END// валидатор с кастомными свойствамиauto v=validator(    _[bar_value](ilex_ne,"UNKNOWN"), // лексикографическое "не равно" без учета регистра    _[other_value](gte,1000) // больше или равно 1000);Foo foo_instance;error_report err;// запись валидного значение в свойство bar_value объекта foo_instanceset_validated(foo_instance,bar_value,"Hello world",v,err);if (!err){    // свойство bar_value объекта foo_instance успешно записано}// попытка записи невалидного значение в свойство bar_value объекта foo_instanceset_validated(foo_instance,bar_value,"unknown",v,err);if (err){    // запись не удалась    std::cerr << err.message() << std::endl;    /* напечатает:     bar_value must be not equal to UNKNOWN     */}

Один и тот же валидатор для пост-валидации и пре-валидации


#include <iostream>#include <dracosha/validator/validator.hpp>#include <dracosha/validator/validate.hpp>using namespace DRACOSHA_VALIDATOR_NAMESPACE;namespace validator_ns {// зарегистрировать getter свойства "x"DRACOSHA_VALIDATOR_PROPERTY(GetX);// валидатор GetXauto MyClassValidator=validator(   /*    "x" в кавычках - это имя поля, которое писать в отчете вместо GetX;   interval.open() - модификатор открытого интервала без учета граничных точек   */   _[GetX]("x")(in,interval(0,500,interval.open())) );}using namespace validator_ns;// определение тестового класса  class MyClass {  double x;public:  // Конструктор с пост-валидацией  MyClass(double _x) : x(_x) {      validate(*this,MyClassValidator);  }  // Getter  double GetX() const noexcept  {     return _x;  }  // Setter с пре-валидацией  void SetX(double _x) {    validate(_[validator_ns::GetX],_x,MyClassValidator);    x = _x;  }};int main(){// конструктор с валидным аргументомtry {    MyClass obj1{100.0}; // ok}catch (const validation_error& err){}// конструктор с невалидным аргументомtry {    MyClass obj2{1000.0}; // значение вне интервала}catch (const validation_error& err){    std::cerr << err.what() << std::endl;    /*     напечатает:     x must be in interval(0,500)    */}MyClass obj3{100.0};// запись с валидным аргументомtry {    obj3.SetX(200.0); // ok}catch (const validation_error& err){}// попытка записи с невалидным аргументомtry {    obj3.SetX(1000.0); // значение вне интервала}catch (const validation_error& err){    std::cerr << err.what() << std::endl;    /*     напечатает:     x must be in interval (0,500)    */}return 0;}

Перевод ошибок валидации на русский язык


// переводчик ключей контейнера на русский язык с учетом рода, падежа и числаphrase_translator tr;tr["password"]={                    {"пароль"},                    {"пароля",grammar_ru::roditelny_padezh}               };tr["hyperlink"]={                    {{"гиперссылка",grammar_ru::zhensky_rod}},                    {{"гиперссылки",grammar_ru::zhensky_rod},grammar_ru::roditelny_padezh}                };tr["words"]={                {{"слова",grammar_ru::mn_chislo}}            };/* финальный переводчик включает в себя встроенный переводчик на русскийvalidator_translator_ru() и переводчик tr для имен элементов*/auto tr1=extend_translator(validator_translator_ru(),tr);// контейнер для валидацииstd::map<std::string,std::string> m1={    {"password","123456"},    {"hyperlink","zzzzzzzzz"}};// адаптер с генерацией отчета об ошибке на русском языкеstd::string rep;auto ra1=make_reporting_adapter(m1,make_reporter(rep,make_formatter(tr1)));// различные валидаторы и печать ошибок на русском языкеauto v1=validator(    _["words"](exists,true) );if (!v1.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    слова должны существовать    */}rep.clear();auto v2=validator(    _["hyperlink"](eq,"https://www.boost.org") );if (!v2.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    гиперссылка должна быть равна https://www.boost.org    */}rep.clear();auto v3=validator(    _["password"](length(gt,7)) );if (!v3.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    длина пароля должна быть больше 7    */}rep.clear();auto v4=validator(    _["hyperlink"](length(lte,7)) );if (!v4.apply(ra1)){    std::cerr<<rep<<std::endl;    /*    напечатает:    длина гиперссылки должна быть меньше или равна 7    */}rep.clear();
Подробнее..

Новая функциональность в RESTinio и опять с помощью Cных шаблонов

12.11.2020 10:07:10 | Автор: admin

Увидело свет очередное обновление небольшой библиотеки для встраивания асинхронного HTTP-сервера в C++ приложения: RESTinio-0.6.12. Хороший повод рассказать о том, как в этой версии с помощью C++ных шаблонов был реализован принцип "не платишь за то, что не используешь".



Заодно в очередной раз можно напомнить о RESTinio, т.к. временами складывается ощущение, что многие C++ники думают, что для встраивания HTTP-сервера в современном C++ есть только Boost.Beast. Что несколько не так, а список существующих и заслуживающих внимания альтернатив приведен в конце статьи.


О чем речь пойдет сегодня?


Изначально библиотека RESTinio никак не ограничивала количество подключений к серверу. Поэтому RESTinio, приняв очередное новое входящее подключение, сразу же делала новый вызов accept() для принятия следующего. Так что если вдруг на какой-то RESTinio-сервер придет сразу 100500 подключений, то RESTinio не заморачиваясь постарается принять их все.


На такое поведение до сих пор никто не жаловался. Но в wish-list-е фича по ограничению принимаемых подключений маячила. Вот дошли руки и до нее.


В реализации были использованы C++ные шаблоны, посредством которых выбирается код, который должен или не должен использоваться. Об этом-то мы сегодня и поговорим.


Проблема


Нужно сделать так, чтобы RESTinio считала параллельные подключения и когда это количество достигает заданного пользователем предела, переставала делать вызовы accept. Так же RESTinio должна отслеживать моменты закрытия подключений и, если общее количество оставшихся подключений падает ниже заданного пользователем предела, вызовы accept должны возобновиться.


Проблема состояла в том, что в RESTinio есть две сущности: постоянно живущий объект Acceptor, который принимает новые подключения, и временно живущие объекты Connection, которые существуют пока соединение используется для взаимодействия с клиентом. При этом Acceptor порождает Connection, но далее Connection живет своей собственной жизнью и Acceptor ничего больше о Connection не знает. В том числе не знает когда Connection умирает.


Таким образом, нужно было установить какую-то связь между Acceptor-ом и Connection, чтобы Acceptor мог вести корректный подсчет живых на данный момент соединений.


Фактор "не платишь за то, что не используешь"


Итак, в Acceptor нужно добавить функциональность подсчета количества живых подключений, причем этот подсчет должен быть thread safe, если RESTinio работает в многопоточном режиме. А в Connection нужно добавить обратную ссылку на Acceptor, которая будет использоваться когда Connection разрушается.


Это дополнительные действия и дополнительные расходы. Пусть и небольшие, но если кто-то использует RESTinio в условиях, когда нет смысла ограничивать количество подключений, то зачем нести эти расходы? Правильно, незачем.


Поэтому новая функциональность должна включаться в RESTinio только если пользователь явно пожелал ее использовать. В остальных же случаях добавление ограничения на количество подключений не должно вести к возникновению накладных расходов (либо же эти расходы должны быть сведены к минимуму).


Решение


Ограничение на количество подключений включается/выключается через traits


Как и практически все остальное, включение connection_count_limiter-а в RESTinio осуществляется посредством свойств (traits) сервера. Для того, чтобы connection_count_limiter заработал нужно определить свой класс свойств, в котором должен быть статический constexpr член use_connection_count_limiter выставленный в true:


struct my_traits : public restinio::default_traits_t {   static constexpr bool use_connection_count_limiter = true;};

Если теперь задать максимальное количество параллельных подключений и запустить RESTinio-сервер с my_traits в качестве свойств сервера, то RESTinio начнет считать и ограничивать количество подключений:


restinio::run(   restinio::on_thread_pool<my_traits>(16)      .max_parallel_connections(1000u)      .request_handler(...));

Тут используется простой фокус: в предоставляемых RESTinio типах default_traits_t и default_single_thread_traits_t уже есть use_connection_count_limiter, который содержит значение false. Что означает, что connection_count_limiter работать не должен.


Если же пользователь наследуется от default_traits_t (или от default_single_thread_traits_t) и определяет use_connection_count_limiter в своем типе свойств, то пользовательское значение перекрывает старое значение от RESTinio. Но когда пользователь в своем типе свойств не определят свой собственный use_connection_count_limiter, то остается виден use_connection_count_limiter из базового типа.


Таким образом RESTinio ожидает, что в traits всегда есть use_connection_count_limiter. И в зависимости от значения use_connection_count_limiter уже определяются типы, которые реализуют подсчет количества соединений. Ну или ничего не делают, если use_connection_count_limiter равен false.


Что происходит, если пользователь задает use_connection_count_limiter=true?


Актуальный connection_count_limiter


Если пользователь задает use_connection_count_limiter со значением true, то в объекте Acceptor должен появится объект connection_count_limiter, который и будет заниматься подсчетом количества подключений и разрешением/запрещением вызова accept-ов.


Однако, тут нужно учесть, что RESTinio-сервер может работать в двух режимах:


  • однопоточном. Все, включая I/O и обработку принятых запросов, выполняется на одной единственной рабочей нити. В этом случае RESTinio не использует механизмов обеспечения thread safety. Соответственно, и connection_count_limiter-у незачем применять реальный mutex для защиты своих внутренностей;
  • многопоточном. И I/O, и обработка принятых запросов может выполняться на разных рабочих нитях. Например, RESTinio сразу запускается на пуле рабочих потоков, на котором выполняются I/O операции и работают обработчики запросов. Либо же RESTinio работает на одной рабочей нити, а реальная обработка запросов делегируется какой-то другой рабочей нити (пулу рабочих нитей). Либо же RESTinio работает на одном пуле рабочих нитей, а обработка запросов делегируется на другой пул рабочих нитей. В этом случае RESTinio задействует механизм strand-ов из Asio для обеспечения thread safety. А connection_count_limiter должен использовать mutex, чтобы не допустить порчи собственных данных когда его начнут дергать из разных нитей.

Поэтому реализация connection_count_limiter-а выполнена в виде шаблонного класса, который параметризуется типом mutex. А нужная реализация выбирается благодаря специализации шаблона:


template< typename Strand >class connection_count_limiter_t;template<>class connection_count_limiter_t< noop_strand_t >   :  public connection_count_limits::impl::actual_limiter_t< null_mutex_t >{   using base_t = connection_count_limits::impl::actual_limiter_t< null_mutex_t >;public:   using base_t::base_t;};template<>class connection_count_limiter_t< default_strand_t >   :  public connection_count_limits::impl::actual_limiter_t< std::mutex >{   using base_t = connection_count_limits::impl::actual_limiter_t< std::mutex >;public:   using base_t::base_t;};

Тип strand-а задается в traits, поэтому достаточно параметризовать connection_count_limiter_t типом traits::strand_t и автоматически получается либо версия для однопоточного, либо версия для многопоточного режимов.


Экземпляр connection_count_limiter-а теперь содержится в объекте Acceptor и Acceptor обращается к этому connection_count_limiter-у для того, чтобы узнать, можно ли делать очередной вызов accept. А connection_count_limiter либо разрешает вызвать accept, либо нет.


Объект connection_count_limiter получает уведомления от разрушаемых объектов Connection. Если connection_count_limiter видит, что вызовы accept были заблокированы, а сейчас появилась возможность возобновить прием новых подключений, то connection_count_limiter отсылает нотификацию Acceptor-у. И получив эту нотификацию Acceptor возобновляет вызовы accept.


А уведомления о разрушении объектов Connection к connection_count_limiter приходят благодаря объектам connection_lifetime_monitor, о которых речь пойдет дальше.


Актуальный connection_lifetime_monitor


В Acceptor-е есть connection_count_limiter который должен узнавать о моментах разрушения объектов Connection.


Очевидным решением было бы реализовать информирование connection_count_limiter-а прямо в деструкторе Connection. Но дело в том, что в RESTinio Connection может преобразовываться в WS_Connection в случае перевода соединения в режим WebSocket-а. Так что аналогичное информирование потребовалось бы делать и в деструкторе WS_Connection-а.


Дублировать же одну и ту же функциональность в двух разных местах не хотелось, поэтому задача информирования connection_count_limiter-а об исчезновении подключения была делегирована новому объекту connection_lifetime_monitor.


Это Noncopyable, но зато Movable объект, который создается внутри Connection. Соответственно, и разрушается он вместе с объектом Connection.


Если же Connection преобразуется в WS_Connection, то экземпляр connection_lifetime_monitor перемещается из Connection в WS_Connection. И затем разрушается уже вместе с владеющим WS_Connection.


Т.е. итоговая схема такая:


  • в Acceptor-е живет connection_count_limiter;
  • когда Acceptor принимает новое подключение, то вместе с новым Connection создается и новый экземпляр connection_lifetime_monitor;
  • когда Connection умирает, то разрушается и connection_lifetime_monitor;
  • умирающий connection_lifetime_monitor информирует connection_count_limiter о том, что количество соединений уменьшилось.

Если Connection преобразуется в WS_Connection, то ничего принципиально не меняется, просто актуальную информацию о живом соединении начинает держать у себя connection_lifetime_monitor из WS_Connection.


Подчеркнем, что connection_lifetime_monitor вынужден держать у себя внутри указатель на connection_count_limiter. Иначе он не сможет дернуть connection_count_limiter при своем разрушении.


Фиктивные connection_count_limiter и connection_lifetime_monitor


Выше было показано, что стоит за connection_count_limiter и connection_lifetime_monitor в случае, когда ограничение на количество подключений задано.


Если же пользователь задает use_connection_count_limiter равным false, то понятия connection_count_limiter и connection_lifetime_monitor остаются. Но теперь это фиктивные connection_count_limiter и connection_lifetime_monitor, которые, по сути, ничего не делают. Например, фиктивный connection_lifetime_monitor ничего внутри себя не хранит.


Тем не менее, внутри Acceptor-а все еще живет экземпляр connection_count_limiter, пусть даже и фиктивный. А внутри Connection (и WS_Connection) есть пустой connection_lifetime_monitor.


Можно было, конечно, попробовать упороться шаблонами по полной программе и постараться избавиться от присутствия пустого connection_lifetime_monitor в Connection. Но, имхо, наличие лишнего байта в Connection (WS_Connection) не стоит сложности кода, который позволяет от этого байта избавиться. Тем более, что в C++20 добавили атрибут no_unique_address, так что со временем эта проблема должна решиться гораздо более простым и наглядным способом. Впрочем, если для кого-то дополнительный байт в Connection это реальная проблема, то откройте Issue, будем ее решать :)


Выбор подходящих connection_count_limiter и connection_lifetime_monitor


После того, как появились актуальный и фиктивные реализации connection_count_limiter и connection_lifetime_monitor осталось научиться выбирать между ними в зависимости от содержимого traits. Делается это так:


template< typename Traits >struct connection_count_limit_types{   using limiter_t = typename std::conditional      <         Traits::use_connection_count_limiter,         connection_count_limits::connection_count_limiter_t<               typename Traits::strand_t >,         connection_count_limits::noop_connection_count_limiter_t      >::type;   using lifetime_monitor_t =         connection_count_limits::connection_lifetime_monitor_t< limiter_t >;};

Т.е. для того, чтобы получить актуальный тип connection_count_limiter-а достаточно написать что-то вроде:


typename connection_count_limit_types<traits>::limiter_t

Хранение ограничения на количество подключений в server_settings


Осталось рассмотреть еще один небольшой момент: параметры для RESTinio сервера хранятся в server_settings_t<Traits> и, по хорошему, надо бы сделать так, чтобы ограничение на количество подключений нельзя было задавать, если в traits use_connection_count_limiter выставлен в false.


Тут используется фокус, к которому мы уже прибегали раньше:


  • создается шаблонный тип, который должен использоваться в качестве примеси (mixin);
  • у этого шаблонного типа есть специализация для фиктивного connection_count_limiter-а;
  • этот шаблонный тип подмешивается в качестве базы в server_settings_t.

В самом же server_settings_t делается метод max_parallel_connections, который содержит внутри static_assert. Этот static_assert ведет к ошибке компиляции, если в Traits запрещено использовать ограничение на количество подключений. Такой подход, имхо, ведет к более понятным сообщениям об ошибках, нежели отсутствие метода max_parallel_connections когда use_connection_count_limiter равен false.


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


RESTinio продолжает развивается по мере наших сил и возможностей. Некоторые планы по дальнейшему развитию есть. Но как-то углубляться в них не хочется из-за суеверных соображений. Уж очень жизненным оказывается афоризм про озвучивание планов и Господа Бога. Такое ощущение, что он срабатывает в 99% случаев :)


Что можно точно сказать, так это то, что мы внимательно прислушиваемся к пожеланиям. Если вам чего-то не хватает в RESTinio, то расскажите нам об этом. Либо прямо здесь, в комментариях, либо на GitHub-е через Issues.


HTTP-клиент в RESTinio?


Время от время мы сталкиваемся с сожалениями потенциальных пользователей о том, что RESTinio реализует только сервер, но не имеет функциональности HTTP-клиента.


Тут все просто. Мы делали RESTinio под конкретные сценарии использования. И это были сценарии использования RESTinio для реализации HTTP-входа в C++ приложения. Клиент нам не был нужен.


Вероятно, реализация клиента в RESTinio может быть добавлена.


Вероятно.


С определенностью сложно сказать, т.к. эту тему мы никогда глубоко не прорабатывали. Если бы кто-то рискнул профинансировать эту работу, то можно было бы всерьез за реализацию клиента взяться. Но за собственный счет мы этот объем просто не поднимем. Поэтому HTTP-клиента в RESTinio нет.


Bonus track: Так Boost.Beast-ом ли единым?


Действительно очень часто на просторах Интернета на вопрос "А что есть в C++ для реализации HTTP-сервера" отвечают Boost.Beast. К моему удивлению часто все еще вспоминают CROW, который уже несколько лет как мертв.


Какие-то другие варианты встречаются довольно редко. Хотя их не так уж и мало. Кроме нашего RESTinio имеет смысл упомянуть, как минимум, следующие разработки (в алфавитном порядке):



Ну и не забудем про возможности фреймворка POCO.


Так что есть из чего выбирать. И, если вам не нужна экстремальная производительность и тотальный контроль за всеми аспектами, плюс вы хотите обойтись минимумом усилий, то есть смысл сперва рассмотреть альтернативы Boost.Beast. Потому что Beast, при всех своих достоинствах, слишком уж низкоуровневый.

Подробнее..

RESTinio-0.6.13 последний большой релиз RESTinio в 2020 и, вероятно, последний в ветке 0.6

29.12.2020 12:11:44 | Автор: admin


RESTinio это относительно небольшая C++14 библиотека для внедрения HTTP/WebSocket сервера в C++ приложения. Мы старались сделать RESTinio простой в использовании, с высокой степенью кастомизации, с приличной производительностью. И, вроде бы, пока что это получается.


Ранее здесь уже были статьи про RESTinio, но в них речь больше шла о том, что и как было сделано в потрохах библиотеки. Сегодня же хочется рассказать о том, что появилось в свежей версии RESTinio, и зачем это появилось. А так же сказать несколько слов о том, почему этот релиз, скорее всего, станет последним большим обновлением в рамках ветки 0.6. И о том, чего хотелось бы достичь при работе над веткой 0.7.


Кому интересно, милости прошу под кат.


Главная фича версии 0.6.13: цепочки из синхронных обработчиков


Главной целью, которую мы преследовали начиная в 2017-ом году проект RESTinio, было упрощение написания HTTP-точек входа в C++ приложения. И одним из способов такого упрощения было заимствование лучшего из того, что нас окружало. В частности, в RESTinio мы сделали аналог роутера запросов из ExpressJS. В итоге express_router стал чуть ли не наиболее востребованной из возможностей RESTinio.


Но в ExpressJS кроме роутера есть еще важная штука: middleware. И вот её-то мы изначально в RESTinio и не стали переносить.


Сперва эта функциональность нам была не нужна. Но по мере взросления RESTinio мы стали сталкиваться с ситуациями, в которых что-то похожее на middleware из ExpressJS было бы полезным. А раз так, то захотелось эти самые middleware поиметь и в RESTinio.


Что оказалось далеко не таким простым делом, как хотелось бы. Но давайте обо всем по порядку.


Цепочки из синхронных обработчиков


Итак, начиная с версии 0.6.13 обработчики запросов в RESTinio можно выстраивать в цепочки. И такие обработчики будут последовательно вызываться для обработки очередного запроса. Движение по цепочке от обработчика до обработчика происходит пока все они возвращают специальное значение not_handled. Если же какой-то из обработчиков возвращает accepted или rejected, то обработка запроса прекращается и оставшиеся в цепочке обработчики не вызываются.


Давайте представим себе, что прежде чем начать обрабатывать запрос, нам нужно сделать три действия:


  • залогировать сам запрос и какие-то его параметры;
  • проверить наличие и значения нужных нам HTTP-заголовков;
  • проверить аутентификационные параметры пользователя (если таковые представлены) и удостовериться, что у пользователя есть необходимые права.

Каждое из этих действий теперь может быть представлено в виде отдельного обработчика запросов:


auto incoming_req_logger(const restinio::request_handle_t & req){  ... // Логируем запрос.  // Разрешаем запустить следующий обработчик в цепочке.  return restinio::request_not_handled();  }auto mandatory_fields_checker(const restinio::request_handle_t & req){  ... // Выполняем нужные проверки.  if(!ok) {    // Отсылаем отрицательный ответ и прерываем цепочку.    return req->create_response(restinio::status_bad_request())      ...      .done(); // Здесь возвращается accepted.  }  // Разрешаем запустить следующий обработчик в цепочке.  return restinio::request_not_handled();  }auto permissions_checker(const restinio::request_handle_t & req){  ... // Проверяем пользователя и его права.  if(!ok) {    // Отсылаем отрицательный ответ и прерываем цепочку.    return req->create_response(restinio::status_unauthorized())      ...      .done(); // Здесь возвращается accepted.  }  // Разрешаем запустить следующий обработчик в цепочке.  return restinio::request_not_handled();}auto actual_processor(const restinio::request_handle_t & req){  ... // Основная обработка запроса.  return restinio::request_accepted();}

Для того, чтобы выстроить эти обработчики в цепочку мы должны сделать несколько шагов.


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


Итак, поскольку количество элементов в цепочке у нас строго фиксировано, то используем fixed_size_chain_t:


// Этот заголовочный файл нужно подключать явным образом.#include <restinio/sync_chain/fixed_size.hpp>...struct my_traits : public restinio::default_traits_t {  using request_handler_t = restinio::sync_chain::fixed_size_chain_t<4>;};

Во-вторых, саму цепочку обработчиков нужно сформировать и отдать серверу при старте:


restinio::run(restinio::on_this_thread<my_traits>()  .port(...)  .address(...)  .request_handler(    // Перечисляем обработчики в порядке их вызова.    incoming_req_logger,    mandatory_fields_checker,    permissions_checker,    actual_processor)  ...);

Вот, собственно, и все.


Почему цепочка из синхронных обработчиков?


RESTinio строился с прицелом именно на асинхронную обработку запросов. Но добавленные в версию 0.6.13 цепочки обработчиков отрабатывают только синхронно. Почему так?


Тут надо зайти издалека.


Начнем с того, что для RESTinio любой обработчик запроса выглядит как синхронный.


Давайте, для простоты, рассмотрим случай, когда RESTinio запускается на одной рабочей нити. На этой нити RESTinio делает все: слушает серверный сокет, принимает новые подключения, вычитывает данные из принятых подключений, разбирает прочитанные данные, вызывает обработчик для полностью принятых запросов, записывает исходящие данные, контролирует тайм-ауты...


Соответственно, когда RESTinio вызывает на этой рабочей нити обработчик запроса, то нить, фактически, блокируется до тех пор, пока обработчик не завершит свою работу. Поэтому-то для RESTinio все обработчики синхронные. Как только вызванный обработчик возвращает управление назад, RESTinio получает возможность продолжить свою работу. При этом RESTinio, по большому счету, все равно, что возвратил обработчик: rejected или accepted.


Разница между синхронностью и асинхронностью важна для программиста, который пишет обработчик запроса. Программист может либо полностью сформировать ответ прямо внутри обработчика (т.е. выполнить create_response()...done()) и тогда обработка будет синхронной. Либо же может делегировать обработку на какой-то другой рабочий контекст, где в конце-концов и будет вызван done().


Так вот, суть в том, что когда обработчик запроса возвращает accepted, то RESTinio не знает, был ли запрос уже обработан полностью. Или же обработка запроса была кому-то делегирована. Или же часть обработки была выполнена сразу, а оставшаяся часть отложена на какое-то время.


Для RESTinio факт возврата accepted из обработчика запросов означает, что пользователь взял на себя ответственность за дальнейшую судьбу запроса. И RESTinio не может строить предположений о том, в каком состоянии находится запрос.


Теперь вернемся к цепочкам.


Цепочка выглядит для RESTinio всего как один обработчик. Собственно, показанный выше fixed_size_chain_t это объект, метод которого и вызывается RESTinio. А уже внутри этого метода происходит последовательный вызов заданных пользователем обработчиков. Сам RESTinio про этот последовательный вызов ничего не знает, для RESTinio никакой последовательности нет вообще.


Предположим, что один из обработчиков вернул не not_handled, не rejected, а accepted. В каком состоянии находится запрос?


Неизвестно. Возможно, обработка запроса делегирована на отдельный рабочий поток и прямо сейчас на этом отдельном рабочем потоке с запросом что-то уже делают.


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


Из-за этого в версии 0.6.13 поддерживается только цепочка из синхронных обработчиков. Т.е. эта цепочка вызывается здесь и сейчас, от начала и до конца. Нельзя вызвать первый обработчик из цепочки, затем вернуть управление RESTinio, а потом, где-то и когда-то вызвать второй обработчик из цепочки. Нет, все обработчики из цепочки вызываются здесь и сейчас. И лишь после того, как все они отработают, управление будет возвращено RESTinio.


Значит ли это, что внутри цепочки нельзя делегировать обработку кому-то еще?


Нет. Обработчик может делегировать обработку запроса на какую-то другую нить. Но после этого обработчик должен возвратить accepted и цепочка будет прервана.


Можно ли сделать цепочку из асинхронных обработчиков?


Есть ощущение, что можно. В принципе. Но в рамках работ над версией 0.6.13 и при сохранении совместимости в рамках ветки 0.6 у меня не получилось придумать такого способа. Есть одна смутная и не до конца оформившаяся идея, только вот она требует изменения API RESTinio.


Обмен данными между обработчиками в цепочке


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


Давайте представим себе, что у нас есть следующая цепочка обработчиков:


  • authentification_handler, который проверяет наличие параметров аутентификации клиента и выполняет аутентификацию;
  • permissions_checker, который проверяет, есть ли у пользователя права на доступ к запрашиваемому ресурсу;
  • admin_access_logger, который логирует доступ пользователя к административным ресурсам;
  • actual_processor, который выполняет обработку запроса.

Первый обработчик должен породить некий объект user_permissions, в котором будет содержаться идентификатор пользователя и информация о его правах. Далее этот объект должен использоваться в permissions_checker-е (для проверки возможности доступа к ресурсу) и в admin_access_logger (для фиксации в журнале).


Соответственно, возникает вопрос, как созданный внутри authentification_handler объект сделать доступным в последующих обработчиках?


При поиске ответа на этот вопрос было рассмотрено несколько вариантов. В работу пошел вариант, который позволяет встроить пользовательские данные внутрь RESTinio-вского объекта request_t.


Выглядит это следующим образом.


Сперва пользователь определяет некую структуру/класс, экземпляры которой и должны встраиваться в объект-запрос:


struct user_permissions {...};...// Вот эта структура должна быть добавлена в каждый запрос.struct per_request_data {  user_permissions user_info_;  ... // Возможно, что-то еще.};

Далее нужно создать тип т.н. extra-data-factory, т.е. фабрики для этой самой дополнительной информации:


struct my_extra_data_factory {  // Внутри extra-data-factory должен быть тип с именем data_t.  using data_t = per_request_data;  // А также вот такой фабричный метод.  void make_within(restinio::extra_data_buffer_t<data_t> buf) {    new(buf.get()) data_t{};  }};

Затем нужно указать тип нашей фабрики в свойствах сервера:


struct my_traits : public restinio::default_traits_t {  using extra_data_factory_t = my_extra_data_factory;};

Ну и, самое, важное: теперь у обработчиков запросов поменяется формат. Вместо аргумента типа restinio::request_handle_t они будут получать restinio::generic_request_handle_t<per_request_data>:


restinio::request_handling_status_t authentification_handler(  const restinio::generic_request_handle_t<per_request_data> & req);restinio::request_handling_status_t permissions_checker(  const restinio::generic_request_handle_t<per_request_data> & req);restinio::request_handling_status_t admin_access_logger(  const restinio::generic_request_handle_t<per_request_data> & req);restinio::request_handling_status_t actual_processor(  const restinio::generic_request_handle_t<per_request_data> & req);

Собственно, это все.


Если наша фабрика не содержит внутри себя никаких данных и является DefaultConstructible типом, то ее экземпляр при запуске сервера даже создавать не нужно она будет создана автоматически. Но вот если фабрика представляет из себя stateful-объект, который требует инициализации, то пользователю придется создать ее самостоятельно. Например:


// Пусть у каждого запроса будет собственный поток для журналирования.struct per_request_data {   std::shared_ptr<log_stream> log_;   per_request_data(std::shared_ptr<log_stream> log)      : log_{std::move(log)}   {}};// За создание этих потоков будет отвечать фабрика.class my_extra_data_factory {   std::shared_ptr<logger> logger_;public:   using data_t = per_request_data;   my_extra_data_factory(std::shared_ptr<logger> logger)      : logger_{std::move(logger)}   {}   void make_within(restinio::extra_data_buffer_t<data_t> buf) {      new(buf.get()) data_t{         std::make_shared<log_stream>(logger_)      };   }};struct my_traits : public restinio::default_traits_t {   using extra_data_factory_t = my_user_data_factory;};auto logger = std::make_shared<logger>(...);// Фабрику нужно будет вручную создать перед запуском сервера.restinio::run(restinio::on_thread_pool<my_traits>(16)   .port(...)   .address(...)   // Вот мы создаем фабрику и передаем её RESTinio.   .extra_data_factory(std::make_shared<my_user_data_factory>(logger))   .request_handler(...));

Внутри обработчика запросов доступ к дополнительным данным можно получить посредством метода extra_data у объекта generic_request_t:


restinio::request_handling_status_t authentification_handler(  const restinio::generic_request_handle_t<per_request_data> & req){  ... // Производим аутентификацию.  if(!ok) {    // Шлем отрицательный ответ.    return req->create_response(...)...done();  }  else {    // Сохраняем информацию о пользователе внутри запроса.    req->extra_data().user_info_ = user_permissions{...};    return restinio::request_not_handled();  }}restinio::request_handling_status_t permissions_checker(  const restinio::generic_request_handle_t<per_request_data> & req){  // Запрашиваем информацию о пользователе с предыдущего шага.  const auto & user_info = req->extra_data().user_info_;  ... // Работа с информацией о пользователе.}

Дополнительная информация, generic_request_t<Extra_Data> и совместимость со старым кодом


По сути, начиная с версии 0.6.13, RESTinio работает уже с двумя новыми типами: шаблонным классом generic_request_t<Extra_Data> и шаблонным псевдонимом generic_request_handle_t<Extra_Data> (который есть std::shared_ptr<generic_request_t<Extra_Data>>).


А для того, чтобы такое кардинальное нововведение не поломало ранее написанный код, старые имена request_t и request_handle_t теперь являются всего лишь псевдонимами для generic_request_t<no_extra_data_factory_t::data_t> и generic_request_handle_t<no_extra_data_factory_t::data_t>, где no_extra_data_factory_t это новый тип для фабрики по умолчанию.


В restinio::traits_t, restinio::default_traits_t и restinio::default_single_thread_traits_t именно no_extra_data_factory_t используется в качестве extra_data_factory_t. Поэтому старый код, который использует имена request_t и request_handle_t, сохраняет свою работоспособность и требует только лишь перекомпиляции.


extra-data и express-/easy_parser_router


Выше уже говорилось, что express_router, сделанный по мотивам ExpressJS, является одной из наиболее востребованных возможностей RESTinio. При этом express_router вводит собственный формат для обработчиков запросов. Соответственно, появление extra-data для запроса сказалось и на express_router-е.


Если программист хочет использовать extra-data с запросами, которые обрабатываются посредством express_router-а, то ему нужно явно указать express_router-у тип фабрики extra-data. Например:


struct my_extra_data_factory { ... };struct my_traits : public restinio::default_traits_t {  using extra_data_factory_t = my_extra_data_factory;  using request_handler_t = restinio::router::express_router_t<    restinio::router::std_regex_engine_t,    extra_data_factory_t>;};

Вот после этого первым аргументом в обработчик запроса для express_router вместо request_handle_t будет generic_request_handle_t<my_traits::extra_data_factory_t::data_t>.


Тоже самое относится и к easy_parser_router:


struct my_traits : public restinio::default_traits_t {  using extra_data_factory_t = my_extra_data_factory;  using request_handler_t = restinio::router::easy_parser_router_t<    extra_data_factory_t>;};

Зачем делать RESTinio-0.7?


Теперь можно сказать несколько слов о том, почему же время жизни ветки 0.6 подходит к концу и зачем начинать ветку 0.7.


Причин несколько.


Во-первых, RESTinio с самого начала базировался на библиотеке http-parser. Но теперь, кажется, эта библиотека остается без поддержки. Соответственно, какую бы замену для http-parser мы не приготовили (стороннюю или же собственную), это скажется на списке зависимостей RESTinio. Что является достаточным поводом, чтобы сменить номер версии.


Во-вторых, в коде RESTinio уже обнаружилось несколько просчетов, которые было бы желательно исправить. Но исправления поломали бы совместимость, поэтому пока что эти исправления не вносились. Однако, рано или поздно с этим нужно было бы что-то делать. Так почему бы не сейчас?


В-третьих, есть ряд хотелок, воплощение которых в жизнь, как представляется, слишком затратно в рамках ветки 0.6 с оглядкой на совместимость. К таким хотелкам можно отнести следующие:


  • поддержка не только http/1.1, но и http/2, а затем и http/3;
  • дополнительный режим работы, в котором RESTinio не загружает весь запрос в память перед вызовом обработчика, а вызывает обработчик по мере загрузки отдельных частей запроса;
  • поддержка цепочки асинхронных обработчиков.

Как по мне, так вместе все это образует достаточно увесистый набор причин для того, чтобы оставить ветку 0.6 как есть и пойти вперед.


А есть ли у вас какие-то пожелания к RESTinio?


Поскольку мы стоим на пороге изменений в функциональности RESTinio, то сейчас отличный момент для того, чтобы высказать свои пожелания к RESTinio.


Могу уверить, что любые конструктивные соображения, замечания и предложения будут самым внимательным образом рассмотрены и приняты к сведению.


Сразу скажу, что клиента на базе RESTinio мы за свой счет не потянем. Об этом не просите :( Если только просьба не будет подкреплена материально ;)


В общем, приглашаю всех желающих высказать свои соображения о функциональности, которую хотелось бы видеть в RESTinio, в Issues или Discussions на GitHub. Или в Google-группу. Ну или можно прямо сюда, в комментарии.


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


2020-й год подходит к концу. Год был, мягко говоря, непростой. Тем не менее, RESTinio живет и развивается. Если не ошибаюсь, нам удалось выкатить порядка десятка релизов и опубликовать здесь несколько посвященных RESTinio статей.


Немалую роль в этом сыграли наши пользователи, которые рискнули выбрать RESTinio для своих задач. А кто-то и нашел время/возможность для обратной связи. Благодаря чему удалось избавить RESTinio от нескольких недостатков и снабдить несколькими фичами.


Так что хочу сказать большое спасибо всем, кто интересуется RESTinio. Ваше внимание очень важно для нас.


И, конечно же, огромное спасибо всем, кто использует RESTinio. Без вас у этого проекта бы не было развития.


Ну и с наступающим Новым Годом!

Подробнее..

Гетерогенный поиск в ассоциативных контейнерах на C

16.10.2020 00:05:23 | Автор: admin

Ассоциативные контейнеры в C++ работают с конкретным типом ключа. Для поиска в них по ключу подобного типа (std::string, std::string_view, const char*) мы можем нести существенные потери в производительности. В этой статье я расскажу как этого избежать с помощью относительно недавно добавленной возможности гетерогенного поиска.


Имея контейнер std::map<std::string, int> с мы должны быть проинформированны о возможной высокой цене при поиске (и некоторых других операциях с ключом в виде параметра) по нему в стиле c.find("hello world"). Дело в том, что по умолчанию все эти операции требуют ключ требуемого типа, в нашем случае это std::string. В результате чего при вызове find нам нужно неявно сконструировать ключ типа std::string из const char*, что будет стоить нам в лучшем случае одного лишнего memcpy (если в нашей реализации стандартной библиотеки есть "small string optimization" и ключ короткий), а также лишнего strlen (если компилятор не догадается или не будет иметь возможности вычислить длину строки во время компиляции). В худшем же случае придётся заплатить по полной: выделением и освобождением памяти из кучи для временного ключа на ровном, казалось бы, месте, а это уже может быть сопоставимо с самим временем поиска.


Мы можем избежать ненужной работы с помощью гетерогенного поиска. Функции для его корректной работы добавлены в упорядоченные контейнеры (set, multiset, map, multimap) во всех подобных местах с С++14 стандарта и в неупорядоченные (unordered_set, unordered_multiset, unordered_map, unordered_multimap) с C++20.


// до C++14 мы имели только такие функции поискаiterator find(const Key& key);const_iterator find(const Key& key) const;// начиная с C++14 мы имеем ещё и вот такиеtemplate < class K > iterator find(const K& x);template < class K > const_iterator find(const K& x) const;

Но, как и всегда, в C++ в этом месте есть подвох, имя ему дефолтный компаратор. Компаратор по умолчанию для нашего std::map<std::string, int> это std::less<std::string> функция сравнения которого объявлена как:


// где T это тип нашего ключа, т.е. std::stringbool operator()(const T& lhs, const T& rhs) const;

Он не может быть использован для нашего гетерогенного сравнения, так как имеет всё такие же проблемы (нужно конструировать конкретный тип ключа). На помощь приходит специализация std::less<void> которая лишена этих проблем.


template <>struct less<void> {    using is_transparent = void;    template < class T, class U >    bool operator()(T&& t, U&& u) const {        return std::forward<T>(t) < std::forward<U>(u);    }};

Примерно так выглядит эта специализация, я упустил моменты с constexpr и noexcept для простоты описания.

Пометка is_transparent говорит контейнерам, что этот компаратор умеет гетерогенное сравнение и по ней же становятся доступны новые функции гетерогенного поиска.


Теперь можно объявить контейнер, который будет использовать всё это добро:


std::map<std::string, int, std::less<>> c;

Естественно, можно написать и свой компаратор для своих типов, например, когда отсутствует глобальный operator< для них. Иногда мы просто не можем создать такой временный ключ прозрачно и гетерогенный поиск единственная возможность искать что-то в контейнерах по ключу, например, при хранении std::thread в std::set и поиску по std::thread::id.


struct thread_compare {    using is_transparent = void;    bool operator()(const std::thread& a, const std::thread& b) const {        return a.get_id() < b.get_id();    }    bool operator()(const std::thread& a, std::thread::id b) const {        return a.get_id() < b;    }    bool operator()(std::thread::id a, const std::thread& b) const {        return a < b.get_id();    }};// объявляем контейнер с нашим гетерогенным компараторомstd::set<std::thread, thread_compare> threads;// имеем возможность искать по idthreads.find(std::this_thread::get_id());

Ну и не стоит забывать, что это всё касается не только функции find. Так же это касается функций: count, equal_range, lower_bound, upper_bound и contains.


Счастливого гетерогенного поиска, уважаемый хаброчитатель!

Подробнее..
Категории: C++ , C++17 , C++20 , C++14

Попытка использовать современный C и паттерны проектирования для программирования микроконтроллеров

31.01.2021 18:12:32 | Автор: admin
Всем привет!

Проблема использования С++ в микроконтроллерах терзала меня довольно долгое время. Дело было в том, что я искренне не понимал, как этот объектно ориентированный язык может быть применим к встраиваем системам. Я имею ввиду, как выделять классы и на базе чего составлять объекты, то есть как именно применять этот язык правильно. Спустя некоторое время и прочтения n-ого количества литературы, я пришёл к кое каким результатам, о чем и хочу поведать в этой статье. Имеют ли какую либо ценность эти результаты или нет остается на суд читателя. Мне будет очень интересно почитать критику к моему подходу, чтобы наконец ответить себе на вопрос: Как же правильно использовать C++ при программировании микроконтроллеров?.

Предупреждаю, в статье будет много исходного кода.

В этой статье, я, на примере использования USART в МК stm32 для связи с esp8266 постараюсь изложить свой подход и его основные преимущества. Начнем с того, что главное преимущество использование C++ для меня это возможность сделать аппаратную развязку, т.е. сделать использование модулей верхнего уровня независимым от аппаратной платформы. Это будет вытекать в то, что система станет легко модифицирована при каких либо изменениях. Для этого я выделил три уровня абстракции системы:

  1. HW_USART аппаратный уровень, зависит от платформы
  2. MW_USART средний уровень, служит для развязки первого и третьего уровней
  3. APP_ESP8266 уровень приложения, ничего не знает о МК

HW_USART


Самый примитивный уровень. Я использовал камень stm32f411, USART 2, также выполнил поддержку DMA. Интерфейс реализован в виде всего трех функций: инициализировать, отправить, получить.

Функция инициализации выглядит следующим образом:

bool usart2_init(uint32_t baud_rate){  bool res = false;    /*-------------GPIOA Enable, PA2-TX/PA3-RX ------------*/  BIT_BAND_PER(RCC->AHB1ENR, RCC_AHB1ENR_GPIOAEN) = true;    /*----------GPIOA set-------------*/  GPIOA->MODER |= (GPIO_MODER_MODER2_1 | GPIO_MODER_MODER3_1);  GPIOA->OSPEEDR |= (GPIO_OSPEEDER_OSPEEDR2 | GPIO_OSPEEDER_OSPEEDR3);  constexpr uint32_t USART_AF_TX = (7 << 8);  constexpr uint32_t USART_AF_RX = (7 << 12);  GPIOA->AFR[0] |= (USART_AF_TX | USART_AF_RX);            /*!---------------USART2 Enable------------>!*/  BIT_BAND_PER(RCC->APB1ENR, RCC_APB1ENR_USART2EN) = true;    /*-------------USART CONFIG------------*/  USART2->CR3 |= (USART_CR3_DMAT | USART_CR3_DMAR);  USART2->CR1 |= (USART_CR1_TE | USART_CR1_RE | USART_CR1_UE);  USART2->BRR = (24000000UL + (baud_rate >> 1))/baud_rate;      //Current clocking for APB1    /*-------------DMA for USART Enable------------*/     BIT_BAND_PER(RCC->AHB1ENR, RCC_AHB1ENR_DMA1EN) = true;    /*-----------------Transmit DMA--------------------*/  DMA1_Stream6->PAR = reinterpret_cast<uint32_t>(&(USART2->DR));  DMA1_Stream6->M0AR = reinterpret_cast<uint32_t>(&(usart2_buf.tx));  DMA1_Stream6->CR = (DMA_SxCR_CHSEL_2| DMA_SxCR_MBURST_0 | DMA_SxCR_PL | DMA_SxCR_MINC | DMA_SxCR_DIR_0);       /*-----------------Receive DMA--------------------*/  DMA1_Stream5->PAR = reinterpret_cast<uint32_t>(&(USART2->DR));  DMA1_Stream5->M0AR = reinterpret_cast<uint32_t>(&(usart2_buf.rx));  DMA1_Stream5->CR = (DMA_SxCR_CHSEL_2 | DMA_SxCR_MBURST_0 | DMA_SxCR_PL | DMA_SxCR_MINC);    DMA1_Stream5->NDTR = MAX_UINT16_T;  BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = true;  return res;}

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

Тогда функция отправки выглядит следующим образом:

bool usart2_write(const uint8_t* buf, uint16_t len){   bool res = false;   static bool first_attempt = true;      /*!<-----Copy data to DMA USART TX buffer----->!*/   memcpy(usart2_buf.tx, buf, len);      if(!first_attempt)   {     /*!<-----Checking copmletion of previous transfer------->!*/     while(!(DMA1->HISR & DMA_HISR_TCIF6)) continue;     BIT_BAND_PER(DMA1->HIFCR, DMA_HIFCR_CTCIF6) = true;   }      first_attempt = false;      /*!<------Sending data to DMA------->!*/   BIT_BAND_PER(DMA1_Stream6->CR, DMA_SxCR_EN) = false;   DMA1_Stream6->NDTR = len;   BIT_BAND_PER(DMA1_Stream6->CR, DMA_SxCR_EN) = true;      return res;}

В функции есть костыль, в виде переменной first_attempt, которая помогает определить самая ли первая это отправка по DMA или нет. Зачем это нужно? Дело в том, что проверку о том, успешна ли предыдущая отправка в DMA или нет я сделал ДО отправки, а не ПОСЛЕ. Сделал я так, чтобы после отправки данных не тупо ждать её завершения, а выполнять полезный код в это время.

Тогда функция приема выглядит следующим образом:

uint16_t usart2_read(uint8_t* buf){   uint16_t len = 0;   constexpr uint16_t BYTES_MAX = MAX_UINT16_T; //MAX Bytes in DMA buffer      /*!<---------Waiting until line become IDLE----------->!*/   if(!(USART2->SR & USART_SR_IDLE)) return len;   /*!<--------Clean the IDLE status bit------->!*/   USART2->DR;      /*!<------Refresh the receive DMA buffer------->!*/   BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = false;   len = BYTES_MAX - (DMA1_Stream5->NDTR);   memcpy(buf, usart2_buf.rx, len);   DMA1_Stream5->NDTR = BYTES_MAX;   BIT_BAND_PER(DMA1->HIFCR, DMA_HIFCR_CTCIF5) = true;   BIT_BAND_PER(DMA1_Stream5->CR, DMA_SxCR_EN) = true;      return len;}

Особенностью этой функции является то, что мне заранее не известно сколько байт я должен получить. Для индикации полученных данных я проверяю флаг IDLE, затем, если состояние IDLE зафиксировано, чищу флаг и читаю данные из буфера. Если же состояние IDLE не зафиксировано, то функция просто возвращает нуль, то есть отсутствие данных.

На этом предлагаю закончить с низким уровнем и перейти непосредственно к C++ и паттернам.

MW_USART


Здесь я реализовал базовый абстрактный класс USART и применил паттерн прототип для создания наследников (конкретных классов USART1 и USART2). Я не буду описывать реализацию паттерна прототип, так как его можно найти по первой ссылке в гугле, а сразу приведу исходный код, и пояснения приведу ниже.

#pragma once#include <stdint.h>#include <vector>#include <map>/*!<========Enumeration of USART=======>!*/enum class USART_NUMBER : uint8_t{  _1,  _2};class USART; //declaration of basic USART classusing usart_registry = std::map<USART_NUMBER, USART*>; /*!<=========Registry of prototypes=========>!*/extern usart_registry _instance; //Global variable - IAR Crutch#pragma inline=forced static usart_registry& get_registry(void) { return _instance; }/*!<=======Should be rewritten as========>!*//*static usart_registry& get_registry(void) {   usart_registry _instance;  return _instance; }*//*!<=========Basic USART classes==========>!*/class USART{private:protected:     static void add_prototype(USART_NUMBER num, USART* prot)  {    usart_registry& r = get_registry();    r[num] = prot;  }    static void remove_prototype(USART_NUMBER num)  {    usart_registry& r = get_registry();    r.erase(r.find(num));  }public:  static USART* create_USART(USART_NUMBER num)  {    usart_registry& r = get_registry();    if(r.find(num) != r.end())    {      return r[num]->clone();    }    return nullptr;  }  virtual USART* clone(void) const = 0;  virtual ~USART(){}    virtual bool init(uint32_t baudrate) const = 0;  virtual bool send(const uint8_t* buf, uint16_t len) const = 0;  virtual uint16_t receive(uint8_t* buf) const = 0;};/*!<=======Specific class USART 1==========>!*/class USART_1 : public USART{private:  static USART_1 _prototype;    USART_1()   {      add_prototype( USART_NUMBER::_1, this);  }public:  virtual USART* clone(void) const override final  {   return new USART_1; }  virtual bool init(uint32_t baudrate) const override final; virtual bool send(const uint8_t* buf, uint16_t len) const override final; virtual uint16_t receive(uint8_t* buf) const override final;};/*!<=======Specific class USART 2==========>!*/class USART_2 : public USART{private:  static USART_2 _prototype;    USART_2()   {      add_prototype( USART_NUMBER::_2, this);  }public:  virtual USART* clone(void) const override final  {   return new USART_2; }  virtual bool init(uint32_t baudrate) const override final; virtual bool send(const uint8_t* buf, uint16_t len) const override final; virtual uint16_t receive(uint8_t* buf) const override final;};

Сначала файла идёт перечисление enum class USART_NUMBER со всеми доступными USART, для моего камня их всего два. Затем идёт опережающее объявление базового класса class USART. Далее идёт объявление контейнер а всех прототипов std::map<USART_NUMBER, USART*> и его реестра, который реализован в виде синглтона Мэйерса.

Тут я напоролся на особенность IAR ARM, а именно то, что он инициализирует статические переменные два раза, в начале программы и непосредственно при входе в main. Поэтому я несколько переписал синглтон, заменив статическую переменную _instance на глобальную. То, как это выглядит в идеале, описано в комментарии.

Далее объявлен базовый класс USART, где определены методы добавления прототипа, удаления прототипа, а также создания объекта(так как конструктор классов наследников объявлен как приватный, для ограничения доступа).

Также объявлен чисто виртуальный метод clone, и чисто виртуальные методы инициализации, отправки и получения.

После всего лишь, мы наследуем конкретные классы, где определяем чисто виртуальные методы, описанные выше.

Код определения методов привожу ниже:

#include "MW_USART.h"#include "HW_USART.h"usart_registry _instance; //Crutch for IAR/*!<========Initialization of global static USART value==========>!*/USART_1 USART_1::_prototype = USART_1();USART_2 USART_2::_prototype = USART_2();/*!<======================UART1 functions========================>!*/bool USART_1::init(uint32_t baudrate) const{ bool res = false; //res = usart_init(USART1, baudrate);  //Platform depending function return res;}bool USART_1::send(const uint8_t* buf, uint16_t len) const{  bool res = false;    return res;}uint16_t USART_1::receive(uint8_t* buf) const{  uint16_t len = 0;    return len;} /*!<======================UART2 functions========================>!*/bool USART_2::init(uint32_t baudrate) const{ bool res = false; res = usart2_init(baudrate);   //Platform depending function return res;}bool USART_2::send(const uint8_t* buf, const uint16_t len) const{  bool res = false;  res = usart2_write(buf, len); //Platform depending function  return res;}uint16_t USART_2::receive(uint8_t* buf) const{  uint16_t len = 0;  len = usart2_read(buf);       //Platform depending function  return len;}

Здесь реализованы методы НЕ пустышки только для USART2, так как его я и использую для общения с esp8266. Соответственно, наполнение может быть любое, также оно может быть реализовано с помощью указателей на функции, которые принимают свое значение исходя из текущего чипа.

Теперь же я предлагаю перейти к APP уровню и посмотреть, зачем же все это было нужно.

APP_ESP8266


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

class ESP8266{private:  ESP8266(){}  ESP8266(const ESP8266& root) = delete;  ESP8266& operator=(const ESP8266&) = delete;    /*!<---------USART settings for ESP8266------->!*/  static constexpr auto USART_BAUDRATE = ESP8266_USART_BAUDRATE;  static constexpr USART_NUMBER ESP8266_USART_NUMBER = USART_NUMBER::_2;  USART* usart;    static constexpr uint8_t LAST_COMMAND_SIZE = 32;  char last_command[LAST_COMMAND_SIZE] = {0};  bool send(uint8_t const *buf, const uint16_t len = 0);    static constexpr uint8_t ANSWER_BUF_SIZE = 32;  uint8_t answer_buf[ANSWER_BUF_SIZE] = {0};    bool receive(uint8_t* buf);  bool waiting_answer(bool (ESP8266::*scan_line)(uint8_t *));    bool scan_ok(uint8_t * buf);  bool if_str_start_with(const char* str, uint8_t *buf);public:    bool init(void);    static ESP8266& Instance()  {    static ESP8266 esp8266;    return esp8266;  }};

Здесь же есть constexpr переменная, в которой и хранится номер используемого USART. Теперь для изменения номера USART нам достаточно только лишь поменять её значение! Связывание же происходит в функции инициализации:

bool ESP8266::init(void){  bool res = false;    usart = USART::create_USART(ESP8266_USART_NUMBER);  usart->init(USART_BAUDRATE);    const uint8_t* init_commands[] =   {    "AT",    "ATE0",    "AT+CWMODE=2",    "AT+CIPMUX=0",    "AT+CWSAP=\"Tortoise_assistant\",\"00000000\",5,0",    "AT+CIPMUX=1",    "AT+CIPSERVER=1,8888"  };    for(const auto &command: init_commands)  {    this->send(command);    while(this->waiting_answer(&ESP8266::scan_ok)) continue;  }      return res;}

Строка usart = USART::create_USART(ESP8266_USART_NUMBER); связывает наш уровень приложения с конкретным USART модулем.

Вместо выводов, просто выражу надежду, что материал окажется кому-нибудь полезен. Спасибо за прочтение!
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru