Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Сергей Шамбир, Адаптация Promise/A+ для взаимодействия между C++ и Javascript

5,582 views

Published on

Шаблоны — мощный инструмент, добавляющий в язык новые возможности, а программистам в команде — новые проблемы. Доклад покажет, как тщательно продуманный шаблонный код может не усложнить, а упростить жизнь и дать надёжную абстракцию межпроцессных межъязыковых асинхронных вызовов функций. С помощью шаблонов можно:

адаптировать Promise/A+ из Javascript для C++
автоматически проверять и раскладывать динамический массив аргументов на статичные аргументы функции
сделать аналог std::bind для weak_ptr.
Эти вещи будут показаны на примере взаимных вызовов между C++ и Javascript в одном приложении с помощью CEF3.

Published in: Software
  • Be the first to comment

  • Be the first to like this

Сергей Шамбир, Адаптация Promise/A+ для взаимодействия между C++ и Javascript

  1. 1. Адаптация Promise/A+ для взаимодействия C++ и JavaScript Сергей Шамбир Ведущий программист iSpring Solutions
  2. 2. Дилемма метапрограммирования • Плюсы: • Вносит в язык новые возможности • Делает язык выразительнее • Минусы: • Имеет намного больший (чем ООП) порог вхождения • Отнимает очень много (сколько угодно) времени • Плохо подходит для решения повседневных задач на C++ • Хорошо подходит, чтобы заложить фундамент новых проектов
  3. 3. Суть нашей проблемы CEF3 render processCEF3 browser process V8 (Javascript) Blink (HTML/CSS) libcef.dll libcef.dll JSON-подобные сообщения (protobuf) Прикладной протокол (События, запуск фоновых задач, управление жизненным циклом UI) ???Прикладной движок на C++ Прикладной UI на HTML5/CSS3 + Javascript
  4. 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. 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. 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. 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. 8. Тонкости маршалинга вызовов • Можно ли проверить типы аргументов лучше, чем через assert? • Повторять проверки в прикладном коде нелепо • Информация о типах уже есть в сигнатуре функции-колбека • Как сериализовать исключение? • Тип или код ошибки могут подсказать стратегию обработки исключения
  9. 9. Мы решили писать шаблоны и велосипеды https://github.com/sergey-shambir/cpp-promise-demo/
  10. 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. 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. 12. Подход «у меня есть план B» с Promise function loadUserPhotos(userId) { let netPromise = netClient.loadPhotoCollectionAsync(userId); let photosPromise = netPromise.catch(() => { return localClient.loadCachedPhotoCollection(userId); }); return photosPromise; } Запуск Запрос к сети Запрос к оффлайн-кешу Fulfilled Rejected
  13. 13. Подход «подождать любого» с Promise function loadUserPhotos(userId) { let netPromise = netClient.loadPhotoCollection(userId); let localPromise = localClient.loadCachedPhotoCollection(userId); return Promise.race([netPromise, localPromise]); } Запуск Запрос к сети Запрос к оффлайн-кешу Fulfilled Rejected
  14. 14. Подход «подождать всех» с Promise function loadUserPhotos(userId) { let netPromise = netClient.loadPhotoCollection(userId); let localPromise = localClient.loadCachedPhotoCollection(userId); return Promise.all([netPromise, localPromise]); } Запуск Запрос к сети Запрос к оффлайн-кешу Fulfilled RejectedЖдём 2-х
  15. 15. Недостатки Promise в Javascript • Легко нарушить контракт «Promise в конце операции переходит в состояние Fulfilled или Rejected» • Достаточно потерять колбеки в конструкторе Promise • Легко нарушить контракт «Promise при успешном завершении возвращает значимый результат» • Просто сделайте обработчик catch такой же, как в примерах: https://goo.gl/dEvi8V var p1 = new Promise(function (resolve, reject) { // .. давайте потеряем resolve/reject });
  16. 16. Основной цикл и пул потоков • В STL до сих пор нет каркаса событийного цикла • Предполагаю, что комитет не пришёл к универсальной реализации • В Boost.Asio и в каждой ОС есть свой основной цикл • В UI-библиотеках циклы свои и в них надо встраиваться • Цикл из Boost.Asio годен для серверов, а не для UI UI-поток execute task handle event handle event handle event post task
  17. 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. 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. 19. Ответ на «Use the Boost, Luke!» • Boost предоставляет then, он он имеет подводные камни • Добиться работы «как в Javascript» можно, но сложно • Даже над Boost лучше написать упрощённую и ограниченную обёртку-велосипед • И не забудьте взять с собой макросы: #define BOOST_THREAD_PROVIDES_EXECUTORS #define BOOST_THREAD_VERSION 4 #include <boost/thread.hpp>
  20. 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. 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. 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. 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. 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. 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. 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. 27. Портируем JS Promise в C++ • Добавили метод Cancel и состояние Cancelled • Для синхронизации использовали mutex • Возможно, есть способ сделать lock-free, но в наших условиях нет ежесекундного создания тысяч объектов Promise Pending Fulfilled Rejected Задача запущена Выполнено Cancelled Отменено
  28. 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. 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. 30. Состояние храним в variant • Promise содержит либо ошибку, либо исключение, либо ничего • Можно использовать variant для экономии памяти на хранение • Чтобы хранить состояние целиком, добавим два теговых типа CancelState и PendingState struct CanceledTag {}; struct PendingState {}; using StorageType = boost::variant< PendingState, CanceledTag, ValueType, std::exception_ptr >;
  31. 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. 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. 33. Постановка задачи // args пришёл из Javascript и выглядит так: auto args = { Value(42.2), Value("add") }; ApplyVariantArguments([](const double &value, const string &operation) { // выполняем действие над аргументами }, args); •Есть рекурсивный вариантный тип, который по набору типов похож на JSON •Есть callable, имеющий точно указанную сигнатуру •Нужно применить аргументы к функции
  34. 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. 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); }
  36. 36. Шаг 3: класс JavascriptArgumentsAdapter template <class T> using CanConvertType = is_any_of<T, bool, double, std::string, std::wstring, nlohmann::json, CefRefPtr<CefListValue>, CefRefPtr<CefDictionaryValue>, CefRefPtr<CefBinaryValue>>; template<class T> void operator()(T & destination, size_t index)const { static_assert(CanConvertType<std::decay_t<T>>{}, "argument conversion is not implemented for type T," " please use another type"); Convert(destination, index); } void Convert(bool &destination, size_t index)const; void Convert(int &destination, size_t index)const; void Convert(double &destination, size_t index)const; void Convert(std::string &destination, size_t index)const; void Convert(std::wstring &destination, size_t index)const; void Convert(nlohmann::json &destination, size_t index)const; void Convert(CefRefPtr<CefListValue> &destination, size_t index)const; void Convert(CefRefPtr<CefDictionaryValue> &destination, size_t index)const; void Convert(CefRefPtr<CefBinaryValue> &destination, size_t index)const; void CJavascriptArgumentsAdapter::Convert( std::string & destination, size_t index) const { assert(int(index) <= int(INT_MAX)); CheckArgument(index, VTYPE_STRING); destination = m_arguments->GetString(int(index)).ToString(); }
  37. 37. Шаг 4: заворачиваем в std::function using RemoteCallback = std::function< CefRefPtr<CefValue>(const CefRefPtr<CefListValue> &list)>; template <typename R, typename ...Args> CefCallback BindCefCallImpl(const std::function<R(Args...)> &function) { return [function](const CefRefPtr<CefListValue> &list) { return ConvertReturnValue(ApplyCefArgumentsImpl(function, list)); }; } template <typename Callable> CefCallback BindCefCall(Callable && callable) { return BindRemoteCallImpl(detail::make_function(callable)); }
  38. 38. Итог: интерфейс IJavascriptBinder class IJavascriptBinder { public: void BindOperation(const string &name, const NativeFn &fn) = 0; void BindAsyncOperation(const string &name, const PromiseFn &fn) = 0; template<class ...TArgs> ICefValuePromisePtr CallJavascript(const string &fn, TArgs&&... args) { const string code = detail::FormatJsArgs(forward<TArgs>(args)...); return CallJsImpl(functionName, jsCode); } protected: ICefValuePromisePtr CallJsImpl(const string &fn, const string &code) = 0; };
  39. 39. Итог: интерфейс на стороне Javascript cef.CefClient = goog.defineClass(null, { call: function(name, ...args) { var promise = new CancelablePromise(); var requestId = this._connector.sendRequest( name, args, promise.resolveFunc(), promise.rejectFunc()); return promise; }, addHandler: function(name, handler, object) { this._handlers[name] = handler.bind(object); }, removeHandler: function(name) { delete this._handlers[name]; }, }
  40. 40. Самое время спросить о чём-нибудь...
  41. 41. Тестирование с Boost.Test •Вдохновлялись Javascript-библиотекой sinon.js • Сделали proxy-объекты •Тесты в отдельном потоке, чтобы не блокировать Event Loop • Поток тестов ждал значение через std::future •Проверили передачу всех типов данных и исключений • Были проблемы с передачей Object и временем жизни • Нельзя передавать тип Function • Нельзя передать три значения типа double: NaN, +INF и -INF
  42. 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. 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. 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. 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. 46. Вредные советы документации Иногда документация содержит вредные советы. • Примеры для JS Promise в сети содержат неправильную обработку исключений (нет перевыброса): https://goo.gl/dEvi8V Иногда документация неоднозначна (пример из STL от Microsoft): void pop() { // erase element at end c.pop_front(); }

×