Шаблоны — мощный инструмент, добавляющий в язык новые возможности, а программистам в команде — новые проблемы. Доклад покажет, как тщательно продуманный шаблонный код может не усложнить, а упростить жизнь и дать надёжную абстракцию межпроцессных межъязыковых асинхронных вызовов функций. С помощью шаблонов можно:
адаптировать Promise/A+ из Javascript для C++
автоматически проверять и раскладывать динамический массив аргументов на статичные аргументы функции
сделать аналог std::bind для weak_ptr.
Эти вещи будут показаны на примере взаимных вызовов между C++ и Javascript в одном приложении с помощью CEF3.
2. Дилемма метапрограммирования
• Плюсы:
• Вносит в язык новые возможности
• Делает язык выразительнее
• Минусы:
• Имеет намного больший (чем ООП) порог вхождения
• Отнимает очень много (сколько угодно) времени
• Плохо подходит для решения повседневных задач на C++
• Хорошо подходит, чтобы заложить фундамент новых проектов
3. Суть нашей проблемы
CEF3 render processCEF3 browser process
V8 (Javascript)
Blink (HTML/CSS)
libcef.dll libcef.dll
JSON-подобные сообщения
(protobuf)
Прикладной протокол
(События, запуск фоновых
задач, управление жизненным
циклом UI)
???Прикладной
движок на C++
Прикладной UI
на HTML5/CSS3
+ Javascript
4. Диспетчеризация сообщений
string message_name = request;
if (message_name == kFileOpenMessageName) {
dialog_state_->mode_ = FILE_DIALOG_OPEN;
title = "My Open Dialog";
} else if (message_name == kFileOpenMultipleMessageName) {
dialog_state_->mode_ = FILE_DIALOG_OPEN_MULTIPLE;
title = "My Open Multiple Dialog";
} else if (message_name == kFileOpenFolderMessageName) {
dialog_state_->mode_ = FILE_DIALOG_OPEN_FOLDER;
title = "My Open Folder Dialog";
} /* ... */
• Наивный подход из разряда «Попробуй Смержить»
• Начиная с C++11 легко заменяется на map<string, function>
5. class CClientPaymentApi
{
public:
// Запуск операций: возвращает обещание результата
future<bool> StartPayment(int itemId);
future<bool> CompletePayment(int itemId);
future<bool> CancelPayment(int itemId);
// Сигналы-слоты: соединение "один ко многим"
Connection DoOnConnectionFailed(const Slot<void()> &handler);
};
Доменная модель API в приложении
• С такой моделью мы предпочли бы работать вместо switch/case
6. Запрос операции похож на вызов функции
• Операции могут завершиться успешно, с ошибкой либо быть
отменены
• Операция может выполниться немедленно или отложенно
Прикладной UI
на HTML5/CSS3
+ Javascript
Прикладной
движок на C++
OpenDocument("cbook.doc")
returns true
Прикладной
движок на C++
Прикладной UI
на HTML5/CSS3
+ Javascript
OpenDocument(42)
throws TypeError
Прикладной
движок на C++
Прикладной UI
на HTML5/CSS3
+ Javascript
OpenDocument("1GB.doc")
cancel that
7. Сложности работы с потоками
• Блокировать UI-потоки нельзя – это заденет пользователя
• Первый UI поток – browser-процессе (с прикладным C++-кодом)
• Второй UI поток – в render-процессе (с прикладным Javascript-кодом)
UI-поток в browser процессе
execute
task
handle
event
handle
event
handle
event
post
task
UI-поток в render процессе
IPC IPC
8. Тонкости маршалинга вызовов
• Можно ли проверить типы аргументов лучше, чем через assert?
• Повторять проверки в прикладном коде нелепо
• Информация о типах уже есть в сигнатуре функции-колбека
• Как сериализовать исключение?
• Тип или код ошибки могут подсказать стратегию обработки исключения
9. Мы решили писать шаблоны и велосипеды
https://github.com/sergey-shambir/cpp-promise-demo/
10. Преимущества Promise в Javascript
• Есть проверенная в деле спецификация: promisesaplus.com
• “An open standard for sound, interoperable JavaScript promises—by
implementers, for implementers.”
• Есть then/catch, т.е. можно повесить callback или продолжение
• Callback вызывается с чистым стеком на определённом потоке
(т.е. как новый task)
Pending
Fulfilled
Rejected
Задача запущена Выполнено
11. Подход «конвейер подзадач» с Promise
function loadGameMap() {
let contentPromise = utils.loadUrlAsStringAsync("/res/level1.tmx");
let xmlPromise = contentPromise.then((content) => {
return utils.parseXmlString(content)
});
let mapPromise = xmlPromise.then((xmlDocument) => {
return utils.buildGameMap(xmlDocument)
});
return mapPromise;
};
Запуск
FulfilledRejected
Чтение файла Разбор XML
Построение
карты уровня
12. Подход «у меня есть план B» с Promise
function loadUserPhotos(userId) {
let netPromise = netClient.loadPhotoCollectionAsync(userId);
let photosPromise = netPromise.catch(() => {
return localClient.loadCachedPhotoCollection(userId);
});
return photosPromise;
}
Запуск Запрос к сети
Запрос к
оффлайн-кешу
Fulfilled Rejected
13. Подход «подождать любого» с Promise
function loadUserPhotos(userId) {
let netPromise = netClient.loadPhotoCollection(userId);
let localPromise = localClient.loadCachedPhotoCollection(userId);
return Promise.race([netPromise, localPromise]);
}
Запуск
Запрос к сети
Запрос к
оффлайн-кешу
Fulfilled Rejected
14. Подход «подождать всех» с Promise
function loadUserPhotos(userId) {
let netPromise = netClient.loadPhotoCollection(userId);
let localPromise = localClient.loadCachedPhotoCollection(userId);
return Promise.all([netPromise, localPromise]);
}
Запуск
Запрос к сети
Запрос к
оффлайн-кешу
Fulfilled RejectedЖдём 2-х
15. Недостатки Promise в Javascript
• Легко нарушить контракт «Promise в конце операции переходит в
состояние Fulfilled или Rejected»
• Достаточно потерять колбеки в конструкторе Promise
• Легко нарушить контракт «Promise при успешном завершении
возвращает значимый результат»
• Просто сделайте обработчик catch такой же, как в примерах:
https://goo.gl/dEvi8V
var p1 = new Promise(function (resolve, reject) {
// .. давайте потеряем resolve/reject
});
16. Основной цикл и пул потоков
• В STL до сих пор нет каркаса событийного цикла
• Предполагаю, что комитет не пришёл к универсальной реализации
• В Boost.Asio и в каждой ОС есть свой основной цикл
• В UI-библиотеках циклы свои и в них надо встраиваться
• Цикл из Boost.Asio годен для серверов, а не для UI
UI-поток
execute
task
handle
event
handle
event
handle
event
post
task
17. Пул потоков на Boost.Asio в 35 строк
AsioThreadPool: https://goo.gl/NiYTUY
boost::asio::io_service
boost::asio::io_service::work
std::thread { io.run(); }
std::thread { io.run(); }
std::thread { io.run(); }
std::thread { io.run(); }
• Конструктор вызывает на каждом потоке io.run
• Деструктор вызывает io.stop() и затем join потоков
• Для добавления задачи вызываем io.post
18. future в C++ и Promise в Javascript
• В C++14 и C++17 future не расчитан на модель «исполнители и
задачи»
• В std::future нет then
• Если future получен от async, в деструкторе будет ожидание завершения
задачи
• В Concurrency TS future всё так же не расчитан на модель
«исполнители и задачи»
• К std::future добавляется then(callback), но нет стратегии вызова callback
• Нельзя выполнить callback в предсказуемом потоке и окружении
“Why is there no std::future::then in C++17?” stackoverflow.com/questions/41310197
19. Ответ на «Use the Boost, Luke!»
• Boost предоставляет then, он он имеет подводные камни
• Добиться работы «как в Javascript» можно, но сложно
• Даже над Boost лучше написать упрощённую и ограниченную
обёртку-велосипед
• И не забудьте взять с собой макросы:
#define BOOST_THREAD_PROVIDES_EXECUTORS
#define BOOST_THREAD_VERSION 4
#include <boost/thread.hpp>
20. Чемпионат по отстрелу ног с Boost, раунд 1
• На каком потоке по умолчанию будет вызван callback?
• Ответ: на новом потоке, т.к. Launch Policy – launch::none
// .. создаём boost::promise и получаем от него future
cerr << "called then on " << this_thread::get_id() << endl;
future.then([&](future<string> oldFuture) {
cerr << "then callback on " << this_thread::get_id() << endl;
dispatch.QuitMainLoop();
});
21. Чемпионат по отстрелу ног с Boost, раунд 2
• Будет ли вызван callback?
• Ответ: если задача ещё не завершилась, то не будет, т.к.
возвращённый от then объект future разрушается сразу после
выполнения инструкции
• Уточнение: если future получен от async, всё сложно.
// .. создаём boost::promise и получаем от него future
cerr << "called then on " << this_thread::get_id() << endl;
future.then(launch::deferred, [&](future<string> oldFuture) {
cerr << "then callback on " << this_thread::get_id() << endl;
dispatch.QuitMainLoop();
});
22. Чемпионат по отстрелу ног с Boost, раунд 3
• Будет ли вызван callback, если просто заменить Launch Policy?
• Ответ: если у future не указан executor, будет assert
• Assertion failed: this->future_->get_executor(), file
c:...boostthreadfuture.hpp, line 4761
// .. создаём boost::promise и получаем от него future
cerr << "called then on " << this_thread::get_id() << endl;
future.then(launch::executor, [&](future<string> oldFuture) {
cerr << "then callback on " << this_thread::get_id() << endl;
dispatch.QuitMainLoop();
});
23. Чемпионат по отстрелу ног с Boost, раунд 4
• Будет ли вызван callback, если установить executor, который
постит задачу в UI thread, и then вызывается из UI thread?
• Ответ: нет, wait() заблокирует обработку событий в UI thread
cerr << "called then on " << this_thread::get_id() << endl;
auto f2 = future.then(launch::executor, [&](future<string> oldFuture)
{
cerr << "then callback on " << this_thread::get_id() << endl;
dispatch.QuitMainLoop();
});
f2.wait();
24. Безопасный callback, связанный с объектом
void WelcomeController::OnLogin()
{
auto callback = std::bind(&WelcomeController::SaveLoginData, this, _1);
m_api.Login(m_view.GetEmail(), m_view.GetPassword(), callback);
}
• В момент вызова callback объект уже может быть уничтожен
• Из документации Boost.Signals: вызов слота может происходить после
disconnect, если disconnect был сделан в другом потоке
• Решения есть
• Weak this (аналог weak self в Objective-C)
• Monitor (альтернатива weak this)
25. Идиома “weak this”
BindWeakPtr: goo.gl/xlRM3E
• Нужно наследовать класс от enable_shared_from_this
• Нельзя вызывать shared_from_this в конструкторе и деструкторе
• Не работает с std::bind
std::weak_ptr<WelcomeController> weakThis = shared_from_this();
m_api.Login(m_view.GetEmail(), m_view.GetPassword(), [weakThis](const auto &data) {
if (auto strongThis = weakThis.lock())
{
strongThis->SaveLoginData(data);
}
});
26. BindWeakPtr – адаптер std::bind
BindWeakPtr: goo.gl/xlRM3E
• Функция BindWeakPtr перегружена для const и не-const методов
• Внутри создаёт WeakInvoker и вызывает bind с его копией
• Объект WeakInvoker хранит weak_ptr и реализует operator()
void WelcomeController::OnLogin()
{
auto callback = BindWeakPtr(
&WelcomeController::SaveLoginData, shared_from_this(), _1);
m_restClient->Login(m_view->GetEmail(), m_view->GetPassword(), callback);
}
27. Портируем JS Promise в C++
• Добавили метод Cancel и состояние Cancelled
• Для синхронизации использовали mutex
• Возможно, есть способ сделать lock-free, но в наших условиях нет
ежесекундного создания тысяч объектов Promise
Pending
Fulfilled
Rejected
Задача запущена Выполнено
Cancelled
Отменено
28. Портируем JS Promise в C++
template <class ValueType>
class IPromise
{
public:
using ThenFunction = function<void(ValueType)>;
using CatchFunction = function<void(exception_ptr const&)>;
virtual ~IPromise() = default;
virtual void Then(const ThenFunction &onFulfilled) = 0;
virtual void Catch(const CatchFunction &onRejected) = 0;
virtual void Cancel() = 0;
};
29. let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(42);
});
});
promise.then((value) => alert("Succeed 1st: " + value));
promise.then((value) => alert("Succeed 2nd: " + value));
promise.then((value) => alert("Succeed 3rd: " + value));
• В Javascript один объект Promise позволяет вызвать then несколько раз
• В C++ это невозможно для Movable-only значений
• Мы решили поддерживать Movable-only, нарушив стандарт Promise/A+
Выбор: совместимость или movable-значения
30. Состояние храним в variant
• Promise содержит либо ошибку, либо исключение, либо ничего
• Можно использовать variant для экономии памяти на хранение
• Чтобы хранить состояние целиком, добавим два теговых типа CancelState
и PendingState
struct CanceledTag {};
struct PendingState {};
using StorageType = boost::variant<
PendingState,
CanceledTag,
ValueType,
std::exception_ptr
>;
31. Switch по типам для variant
void Then(const ThenFunction &onFulfilled) override
{
lock_guard lock(m_mutex);
if (m_then)
throw std::logic_error("Cannot call Then twice");
switch (m_storage.which()) {
case detail::VariantIndex<StorageType, PendingState>:
m_then = onFulfilled;
break;
case detail::VariantIndex<StorageType, ValueType>:
m_then = onFulfilled;
InvokeThen();
break;
}
}
32. Получение which index для типа в variant
namespace detail
{
template <class VariantType, class VariantCase>
using WhichIndex = typename boost::mpl::find<
typename boost::mpl::copy<
typename VariantType::types,
boost::mpl::back_inserter<boost::mpl::vector<>>
>::type,
VariantCase
>::type::pos;
template <class VariantType, class VariantCase>
constexpr int VariantIndex = WhichIndex<VariantType, VariantCase>::value;
}
33. Постановка задачи
// args пришёл из Javascript и выглядит так:
auto args = { Value(42.2), Value("add") };
ApplyVariantArguments([](const double &value, const string &operation) {
// выполняем действие над аргументами
}, args);
•Есть рекурсивный вариантный тип, который по набору типов
похож на JSON
•Есть callable, имеющий точно указанную сигнатуру
•Нужно применить аргументы к функции
34. // Для функторов, имеющих operator()
template <typename T>
struct function_traits : public function_traits<decltype(&T::operator())> {
};
// Для указателей на функции
template <typename ReturnType, typename... Args>
struct function_traits<ReturnType(*)(Args...)> {
typedef std::function<ReturnType(Args...)> f_type;
};
// ... для методов (константных и неконстантных) ...
// функция для вывода типа из параметра
template <typename Callable>
typename function_traits<Callable>::f_type make_function(Callable callable) {
return (typename function_traits<Callable>::f_type)(callable);
}
Шаг 1: function_traits
•Задаёт синонимы типов параметров и результата
•Принимает лямбды, указатели на свободные функции и методы
•Не принимает ни std::bind, ни generic lambda
•Могут быть ошибки компиляции с перегруженными функциями
35. Шаг 2: формируем tuple и вызываем apply
// Remove `const&` and other dangerous qualifiers.
template <typename ...Args>
using arguments_tuple = std::tuple<typename std::decay_t<Args>...>;
template <typename R, typename ...Args>
R ApplyCefArgumentsImpl(const std::function<R(Args...)> &function,
const CefRefPtr<CefListValue> & args)
{
detail::CJavascriptArgumentsAdapter adapter(args);
detail::arguments_tuple<Args...> typedArgs;
adapter.CheckArgumentsCount(std::tuple_size<decltype(typedArgs)>::value);
detail::for_each_in_tuple(typedArgs, adapter);
return detail::apply_tuple<R>(typedArgs, function);
}
41. Тестирование с Boost.Test
•Вдохновлялись Javascript-библиотекой sinon.js
• Сделали proxy-объекты
•Тесты в отдельном потоке, чтобы не блокировать Event Loop
• Поток тестов ждал значение через std::future
•Проверили передачу всех типов данных и исключений
• Были проблемы с передачей Object и временем жизни
• Нельзя передавать тип Function
• Нельзя передать три значения типа double: NaN, +INF и -INF
42. Тестирование с Boost.Test
Ожидание std::future
Поток, запустивший unit_test_main
execute
task
handle
event
handle
event
handle
event
UI-поток в browser process
Вызов Javascript через Proxy
execute
task
Proxy получил значение
43. Идиома “monitor”
// Нюанс: не соблюдается rule of five,
// что влечёт неверное копирование monitor
struct Student {
std::shared_ptr<void> monitor;
std::string name;
Student() : monitor(this, ignore) {}
decltype(auto) GetNamePrinter() {
std::weak_ptr<void> monitor = this->monitor;
return [=]() {
if (!monitor.expired()) {
// working with this
}
};
}
};
44. Сторонние библиотеки для Promise/A+
• tored/qml-promise – однопоточные Promise для C++/QML
• rhashimoto/poolqueue – запускает Promise поверх пула потоков
или на базе таймера
• grantila/q – крупная библиотека со своими 🚲 Promise, thread pool,
timers и т.п.
• 0of/Promise2 – содержит заготовку Promise, интегрировать запуск
задач Promise в свой EventLoop/ThreadPool придётся
самостоятельно
45. “libdispatch” от Apple
Библиотека содержит примитивы для событийной
многозадачности: очереди задач, исполнители, пул потоков и
основной поток
• Версия libdispatch от Apple: https://github.com/apple/swift-corelibs-
libdispatch
• Версия с улучшением поддержки Linux (встраивание в event loop):
https://github.com/nickhutchinson/libdispatch
• Версия с поддержкой Win32:
https://github.com/DrPizza/libdispatch
46. Вредные советы документации
Иногда документация содержит вредные советы.
• Примеры для JS Promise в сети содержат неправильную
обработку исключений (нет перевыброса): https://goo.gl/dEvi8V
Иногда документация неоднозначна (пример из STL от Microsoft):
void pop()
{
// erase element at end
c.pop_front();
}
Notes de l'éditeur
О себе- я в iSpring в команде Desktop приложений для тренеров персонала в больших компаниях (ПО для быстрой разработки курсов)- так получилось, что попутно стал наставником для новичков и помогаю преподавать в ВУЗе- недавно команда стала использовать CEF3 для парочки проектов
Началась разработка серии гибридных приложений на CEF3
UI на JavaScript+HTML5
Вычислительное ядро на C++
Они находятся в разных процессах
Требуется интенсивное взаимодействие между С++ и JS:
Реакция на события
Запуск фоновых задач
Управление состоянием UI
CEF3 предполагает лишь асинхронную модель взаимодействия
Требуется маршалинг аргументов и результата вызова
В С++ типы и количество аргументов надо строго проверять
Также надо передавать исключения
Исключения могут нести код или тип ошибки для выбора стратегии обработки
CEF3 позволяет отправить сообщение в любую сторону, содержащее JSON-подобные данные
Если нужно извлекать аргументы вызова, код становится ещё запутаннее
Операции могут завершиться успешно, с ошибкой либо быть отменены
Операция может выполниться немедленно или отложенно
В С++ принято распределять задачи на фоновые потоки и на основной поток
В Javascript принято использовать один поток
Есть Web Workers, но в приложении из заменяет наш C++ бекенд
Нужна простая, понятная модель с приемлемым порогом вхождения
При обычном вызове действует контракт:
обычная функция может завершиться двумя путями: return и throw
можно его нарушить:
бесконечный цикл
longjmp или иное переключение контекста без возврата
останов процесса/потока
все способы нарушения контракта ненормальные
В render-процессе браузера есть поток, в котором исполняется Javascript код и происходит обработка DOM
Ниже показано, почему наши проблемы не решаются стандартными средствами
Либо решаются, но так, что решение позволит легко «отстрелить себе ногу»
Наиболее важное отличие Promise в Javascript от C++: вызов callback происходит как таск в предсказуемом и безопасном для исключений окружении
Приятная особенность: методы parseXmlString и loadTiledMap могут вернуть и Promise, и значение, и в любом случае произойдёт корректный переход в Fulfilled.
Если возвращать Promise, конвейер будет асинхронным, нагрузка может обрабатываться во вторичном потоке.
Приятная особенность: методы parseXmlString и loadTiledMap могут вернуть и Promise, и значение, и в любом случае произойдёт корректный переход в Fulfilled.
Если возвращать Promise, конвейер будет асинхронным, нагрузка может обрабатываться во вторичном потоке.
Одна из наибольших проблем для программистов – неправильное понимание работы языка. Полное незнание причиняет меньше проблем.
В основном Javascript-разработчики читают примеры, а не спецификацию, и совершают ошибки в обработке ошибок.
Такие ошибки остаются незаметными, т.к. сами по себе исключения редко возникают
В примере: «давайте потеряем resolve/reject» – это предложение навечно оставить Promise в состоянии Pending.
По ссылке https://goo.gl/dEvi8V показано, как легко забыть перебросить исключение в новый Promise
в Gamedev и в серверной разработке event loop всегда Data-Oriented
раздельные шаги (event process, update, render и т.п.)
много похожих данных на одном шаге
в Desktop и Mobile приложениях event loop всегда Object-Oriented
много разных событий, разных реакций, разных объектов
относительно мало массивов данных Вывод: в Desktop и Mobile простота event loop важнее низкого overhead на запуск одной задачи
Вывод:
в Desktop и Mobile хватает простого, но удобного объектно-ориентированного event loop, абстракции не страшны
на серверах и в играх нужен быстрый event loop, с поддержкой сотен тысяч задач в секунду
Мы сделал цикл на основе Win32 API
Мы также использовали Message Only Window для встраивания в существующий цикл Win32-событий, запущенный кем угодно (даже если наш код выполняется из DLL как плагин)
Не стоит использовать future, полученный от std::async: его деструктор будет блокировать поток до получения результат
В Visual Studio 2010, 2012, 2013 деструктор не блокирует поток, и это баг, который исправлен в VS2015: https://connect.microsoft.com/VisualStudio/feedback/details/810623
На момент, когда мы начинали работу, не было полного представления, как это должно выглядеть и способен ли Boost на такое
Позже оказалось, что самые свежие версии Boost способны
Boost Executors были реализованы в рамках Google Summer of Code 2014
В стандарт они войдут очень, очень нескоро
Объект WeakInvoker хранит weak_ptr и реализует operator()
Надо сделать две перегрузки BindWeakPtr: для const и не-const
Наиболее важное отличие Promise в Javascript от C++: вызов callback происходит как таск в предсказуемом и безопасном для исключений окружении
Наиболее важное отличие Promise в Javascript от C++: вызов callback происходит как таск в предсказуемом и безопасном для исключений окружении
Использовали template variables и boost::mpl для определения индекса в диапазоне типов во время компиляции
Есть рекурсивный вариантный тип, который по набору типов похож на JSON
Есть callable, имеющий точно указанную сигнатуру
Нужно без дополнительного указания типов применить аргументы к функции
decay_t очень полезен, если надо сформировать tuple со значениями
Перегружать функции для разных типов опасно тем, что появляются неоднозначные приведения типов
Можно добавить строгости и требовать от программиста использовать float вместо double, int вместо unsigned/short/long
Заворачивание в std::function позволяет вызвать callback отложенно, и даже добавить его в словарь, отображающий строковое имя сообщения на обработчик
Позволяет привязать обработчик к имени метода, экспортируемого в Javascript
Позволяет привязать обработчик к имени метода, экспортируемого в Javascript
Позволяет привязать обработчик к имени метода, экспортируемого в Javascript
Был создан отдельный поток для запуска Boost.Test
Был создан Proxy Binder, который отслеживал аргументы и факт вызова (на манер sinon.js)
Не требует enable_shared_from_this
Работает в конструкторе/деструкторе, но недоступен извне
Нельзя передавать monitor наружу
Проблемы примера:
Не соблюдает правило пяти, в итоге неправильно копируется/перемещается