wasm: implement promise handler using suspendresumecontrol
This is required to make sure we resume the wasm instance when a promise resolves. As a bonus QWasmSuspendResumeControl already implements the JS -> C++ callback mapping, and we can removed the fixed-4 ThunkPool which the current implementation is using. The implementation is straightforward, where the only snag is that cleanup must be done in the finally callback. Implement Promise::all by calling JS Promise.all(). This function returns a new Promise, which we can adopt. Make two changes to the test: - remove throwInThen(): We no longer support propagating JS exceptions from the then() handler to the catch function. (catching a rejected promise still works). As far as I can see this functionality is not used in qtbase. - In finallyWithThen(), change shared_ptr<bool> to plain pointer. This works around a (mysterious) issue where we were not getting the correct value when reading from the shared_ptr. Change-Id: I8fb11b66ecba74f80708bd39eeeac59bb62f3786 Reviewed-by: Lorn Potter <lorn.potter@qt.io>
This commit is contained in:
parent
03699cc0ff
commit
f5231e2dbb
@ -91,280 +91,6 @@ private:
|
||||
File file;
|
||||
};
|
||||
|
||||
enum class CallbackType {
|
||||
Then,
|
||||
Catch,
|
||||
Finally,
|
||||
};
|
||||
|
||||
void validateCallbacks(const PromiseCallbacks& callbacks) {
|
||||
Q_ASSERT(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc);
|
||||
}
|
||||
|
||||
using ThunkId = int;
|
||||
|
||||
#define THUNK_NAME(type, i) callbackThunk##type##i
|
||||
|
||||
// A resource pool for exported promise thunk functions. ThunkPool::poolSize sets of
|
||||
// 3 promise thunks (then, catch, finally) are exported and can be used by promises
|
||||
// in C++. To allocate a thunk, call allocateThunk. When a thunk is ready for use,
|
||||
// a callback with allocation RAII object ThunkAllocation will be returned. Deleting
|
||||
// the object frees the thunk and automatically makes any pending allocateThunk call
|
||||
// run its callback with a free thunk slot.
|
||||
class ThunkPool {
|
||||
public:
|
||||
static constexpr size_t poolSize = 4;
|
||||
|
||||
// An allocation for a thunk function set. Following the RAII pattern, destruction of
|
||||
// this objects frees a corresponding thunk pool entry.
|
||||
// To actually make the thunks react to a js promise's callbacks, call bindToPromise.
|
||||
class ThunkAllocation {
|
||||
public:
|
||||
ThunkAllocation(int thunkId, ThunkPool* pool) : m_thunkId(thunkId), m_pool(pool) {}
|
||||
~ThunkAllocation() {
|
||||
m_pool->free(m_thunkId);
|
||||
}
|
||||
|
||||
// The id of the underlaying thunk set
|
||||
int id() const { return m_thunkId; }
|
||||
|
||||
// Binds the corresponding thunk set to the js promise 'target'.
|
||||
void bindToPromise(emscripten::val target, const PromiseCallbacks& callbacks) {
|
||||
using namespace emscripten;
|
||||
|
||||
if (Q_LIKELY(callbacks.thenFunc)) {
|
||||
target = target.call<val>(
|
||||
"then",
|
||||
emscripten::val::module_property(thunkName(CallbackType::Then, id()).data()));
|
||||
}
|
||||
if (callbacks.catchFunc) {
|
||||
target = target.call<val>(
|
||||
"catch",
|
||||
emscripten::val::module_property(thunkName(CallbackType::Catch, id()).data()));
|
||||
}
|
||||
// Guarantee the invocation of at least one callback by always
|
||||
// registering 'finally'. This is required by WebPromiseManager
|
||||
// design
|
||||
target = target.call<val>(
|
||||
"finally", emscripten::val::module_property(
|
||||
thunkName(CallbackType::Finally, id()).data()));
|
||||
}
|
||||
|
||||
private:
|
||||
int m_thunkId;
|
||||
ThunkPool* m_pool;
|
||||
};
|
||||
|
||||
ThunkPool() {
|
||||
std::iota(m_free.begin(), m_free.end(), 0);
|
||||
}
|
||||
|
||||
void setThunkCallback(std::function<void(int, CallbackType, emscripten::val)> callback) {
|
||||
m_callback = std::move(callback);
|
||||
}
|
||||
|
||||
void allocateThunk(std::function<void(std::unique_ptr<ThunkAllocation>)> onAllocated) {
|
||||
if (m_free.empty()) {
|
||||
m_pendingAllocations.push_back(std::move(onAllocated));
|
||||
return;
|
||||
}
|
||||
|
||||
const int thunkId = m_free.back();
|
||||
m_free.pop_back();
|
||||
onAllocated(std::make_unique<ThunkAllocation>(thunkId, this));
|
||||
}
|
||||
|
||||
static QByteArray thunkName(CallbackType type, size_t i) {
|
||||
return QStringLiteral("promiseCallback%1%2").arg([type]() -> QString {
|
||||
switch (type) {
|
||||
case CallbackType::Then:
|
||||
return QStringLiteral("Then");
|
||||
case CallbackType::Catch:
|
||||
return QStringLiteral("Catch");
|
||||
case CallbackType::Finally:
|
||||
return QStringLiteral("Finally");
|
||||
}
|
||||
}()).arg(i).toLatin1();
|
||||
}
|
||||
|
||||
static ThunkPool* get();
|
||||
|
||||
#define THUNK(i) \
|
||||
static void THUNK_NAME(Then, i)(emscripten::val result) \
|
||||
{ \
|
||||
get()->onThunkCalled(i, CallbackType::Then, std::move(result)); \
|
||||
} \
|
||||
static void THUNK_NAME(Catch, i)(emscripten::val result) \
|
||||
{ \
|
||||
get()->onThunkCalled(i, CallbackType::Catch, std::move(result)); \
|
||||
} \
|
||||
static void THUNK_NAME(Finally, i)() \
|
||||
{ \
|
||||
get()->onThunkCalled(i, CallbackType::Finally, emscripten::val::undefined()); \
|
||||
}
|
||||
|
||||
THUNK(0);
|
||||
THUNK(1);
|
||||
THUNK(2);
|
||||
THUNK(3);
|
||||
|
||||
#undef THUNK
|
||||
|
||||
private:
|
||||
void onThunkCalled(int index, CallbackType type, emscripten::val result) {
|
||||
m_callback(index, type, std::move(result));
|
||||
}
|
||||
|
||||
void free(int thunkId) {
|
||||
if (m_pendingAllocations.empty()) {
|
||||
// Return the thunk to the free pool
|
||||
m_free.push_back(thunkId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Take the next enqueued allocation and reuse the thunk
|
||||
auto allocation = m_pendingAllocations.back();
|
||||
m_pendingAllocations.pop_back();
|
||||
allocation(std::make_unique<ThunkAllocation>(thunkId, this));
|
||||
}
|
||||
|
||||
std::function<void(int, CallbackType, emscripten::val)> m_callback;
|
||||
|
||||
std::vector<int> m_free = std::vector<int>(poolSize);
|
||||
std::vector<std::function<void(std::unique_ptr<ThunkAllocation>)>> m_pendingAllocations;
|
||||
};
|
||||
|
||||
Q_GLOBAL_STATIC(ThunkPool, g_thunkPool)
|
||||
|
||||
ThunkPool* ThunkPool::get()
|
||||
{
|
||||
return g_thunkPool;
|
||||
}
|
||||
|
||||
#define CALLBACK_BINDING(i) \
|
||||
emscripten::function(ThunkPool::thunkName(CallbackType::Then, i).data(), \
|
||||
&ThunkPool::THUNK_NAME(Then, i)); \
|
||||
emscripten::function(ThunkPool::thunkName(CallbackType::Catch, i).data(), \
|
||||
&ThunkPool::THUNK_NAME(Catch, i)); \
|
||||
emscripten::function(ThunkPool::thunkName(CallbackType::Finally, i).data(), \
|
||||
&ThunkPool::THUNK_NAME(Finally, i));
|
||||
|
||||
EMSCRIPTEN_BINDINGS(qtThunkPool) {
|
||||
CALLBACK_BINDING(0)
|
||||
CALLBACK_BINDING(1)
|
||||
CALLBACK_BINDING(2)
|
||||
CALLBACK_BINDING(3)
|
||||
}
|
||||
|
||||
#undef CALLBACK_BINDING
|
||||
#undef THUNK_NAME
|
||||
|
||||
class WebPromiseManager
|
||||
{
|
||||
public:
|
||||
WebPromiseManager();
|
||||
~WebPromiseManager();
|
||||
|
||||
WebPromiseManager(const WebPromiseManager& other) = delete;
|
||||
WebPromiseManager(WebPromiseManager&& other) = delete;
|
||||
WebPromiseManager& operator=(const WebPromiseManager& other) = delete;
|
||||
WebPromiseManager& operator=(WebPromiseManager&& other) = delete;
|
||||
|
||||
void adoptPromise(emscripten::val target, PromiseCallbacks callbacks);
|
||||
|
||||
static WebPromiseManager* get();
|
||||
|
||||
private:
|
||||
struct RegistryEntry {
|
||||
PromiseCallbacks callbacks;
|
||||
std::unique_ptr<ThunkPool::ThunkAllocation> allocation;
|
||||
};
|
||||
|
||||
static std::optional<CallbackType> parseCallbackType(emscripten::val callbackType);
|
||||
|
||||
void subscribeToJsPromiseCallbacks(int i, const PromiseCallbacks& callbacks, emscripten::val jsContextfulPromise);
|
||||
void promiseThunkCallback(int i, CallbackType type, emscripten::val result);
|
||||
|
||||
void registerPromise(std::unique_ptr<ThunkPool::ThunkAllocation> allocation, PromiseCallbacks promise);
|
||||
void unregisterPromise(ThunkId context);
|
||||
|
||||
std::array<RegistryEntry, ThunkPool::poolSize> m_promiseRegistry;
|
||||
};
|
||||
|
||||
Q_GLOBAL_STATIC(WebPromiseManager, webPromiseManager)
|
||||
|
||||
WebPromiseManager::WebPromiseManager()
|
||||
{
|
||||
ThunkPool::get()->setThunkCallback(std::bind(
|
||||
&WebPromiseManager::promiseThunkCallback, this,
|
||||
std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
|
||||
}
|
||||
|
||||
std::optional<CallbackType>
|
||||
WebPromiseManager::parseCallbackType(emscripten::val callbackType)
|
||||
{
|
||||
if (!callbackType.isString())
|
||||
return std::nullopt;
|
||||
|
||||
const std::string data = callbackType.as<std::string>();
|
||||
if (data == "then")
|
||||
return CallbackType::Then;
|
||||
if (data == "catch")
|
||||
return CallbackType::Catch;
|
||||
if (data == "finally")
|
||||
return CallbackType::Finally;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
WebPromiseManager::~WebPromiseManager() = default;
|
||||
|
||||
WebPromiseManager *WebPromiseManager::get()
|
||||
{
|
||||
return webPromiseManager();
|
||||
}
|
||||
|
||||
void WebPromiseManager::promiseThunkCallback(int context, CallbackType type, emscripten::val result)
|
||||
{
|
||||
auto* promiseState = &m_promiseRegistry[context];
|
||||
|
||||
auto* callbacks = &promiseState->callbacks;
|
||||
switch (type) {
|
||||
case CallbackType::Then:
|
||||
callbacks->thenFunc(result);
|
||||
break;
|
||||
case CallbackType::Catch:
|
||||
callbacks->catchFunc(result);
|
||||
break;
|
||||
case CallbackType::Finally:
|
||||
// Final callback may be empty, used solely for promise unregistration
|
||||
if (callbacks->finallyFunc) {
|
||||
callbacks->finallyFunc();
|
||||
}
|
||||
unregisterPromise(context);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void WebPromiseManager::registerPromise(
|
||||
std::unique_ptr<ThunkPool::ThunkAllocation> allocation,
|
||||
PromiseCallbacks callbacks)
|
||||
{
|
||||
const ThunkId id = allocation->id();
|
||||
m_promiseRegistry[id] =
|
||||
RegistryEntry {std::move(callbacks), std::move(allocation)};
|
||||
}
|
||||
|
||||
void WebPromiseManager::unregisterPromise(ThunkId context)
|
||||
{
|
||||
m_promiseRegistry[context] = {};
|
||||
}
|
||||
|
||||
void WebPromiseManager::adoptPromise(emscripten::val target, PromiseCallbacks callbacks) {
|
||||
ThunkPool::get()->allocateThunk([=](std::unique_ptr<ThunkPool::ThunkAllocation> allocation) {
|
||||
allocation->bindToPromise(std::move(target), callbacks);
|
||||
registerPromise(std::move(allocation), std::move(callbacks));
|
||||
});
|
||||
}
|
||||
#if defined(QT_STATIC)
|
||||
|
||||
EM_JS(bool, jsHaveAsyncify, (), { return typeof Asyncify !== "undefined"; });
|
||||
@ -726,58 +452,62 @@ EventCallback::EventCallback(emscripten::val element, const std::string &name,
|
||||
|
||||
}
|
||||
|
||||
namespace Promise {
|
||||
void adoptPromise(emscripten::val promiseObject, PromiseCallbacks callbacks) {
|
||||
validateCallbacks(callbacks);
|
||||
void Promise::adoptPromise(emscripten::val promise, PromiseCallbacks callbacks)
|
||||
{
|
||||
Q_ASSERT_X(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc,
|
||||
"Promise::adoptPromise", "must provide at least one callback function");
|
||||
|
||||
WebPromiseManager::get()->adoptPromise(
|
||||
std::move(promiseObject), std::move(callbacks));
|
||||
}
|
||||
QWasmSuspendResumeControl *suspendResume = QWasmSuspendResumeControl::get();
|
||||
Q_ASSERT(suspendResume);
|
||||
|
||||
void all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks) {
|
||||
struct State {
|
||||
std::map<int, emscripten::val> results;
|
||||
int remainingThenCallbacks;
|
||||
int remainingFinallyCallbacks;
|
||||
};
|
||||
// Registers a possibly-empty callback with suspendresumecontrol. Returns
|
||||
// the the handler index if there was a valid callback, or nullopt.
|
||||
auto registerCallback = [suspendResume](std::function<void(emscripten::val)> cb) -> std::optional<uint32_t>{
|
||||
if (!cb)
|
||||
return std::nullopt;
|
||||
return std::optional<uint32_t>{suspendResume->registerEventHandler(std::move(cb))};
|
||||
};
|
||||
|
||||
validateCallbacks(callbacks);
|
||||
// Register callbacks with suspendresumecontrol, so that it can
|
||||
// resume the wasm instance when the promise resolves. The finally
|
||||
// callback is sepecial, since we remove the event handlers there
|
||||
// as cleanup, including the event handler for the cleanup function
|
||||
// itself.
|
||||
std::optional<uint32_t> thenIndex = registerCallback(std::move(callbacks.thenFunc));
|
||||
std::optional<uint32_t> catchIndex = registerCallback(std::move(callbacks.catchFunc));
|
||||
std::shared_ptr<uint32_t> finallyIndex = std::make_shared<uint32_t>();;
|
||||
auto finallyFunc = callbacks.finallyFunc;
|
||||
|
||||
auto state = std::make_shared<State>();
|
||||
state->remainingThenCallbacks = state->remainingFinallyCallbacks = promises.size();
|
||||
// 'Finally' callback which performs clean-up and calls the user-provided finally.
|
||||
auto finally = [suspendResume, thenIndex, catchIndex, finallyIndex, finallyFunc](emscripten::val){
|
||||
|
||||
for (size_t i = 0; i < promises.size(); ++i) {
|
||||
PromiseCallbacks individualPromiseCallback;
|
||||
if (callbacks.thenFunc) {
|
||||
individualPromiseCallback.thenFunc = [i, state, callbacks](emscripten::val partialResult) mutable {
|
||||
state->results.emplace(i, std::move(partialResult));
|
||||
if (!--(state->remainingThenCallbacks)) {
|
||||
std::vector<emscripten::val> transformed;
|
||||
for (auto& data : state->results) {
|
||||
transformed.push_back(std::move(data.second));
|
||||
}
|
||||
callbacks.thenFunc(emscripten::val::array(std::move(transformed)));
|
||||
}
|
||||
};
|
||||
}
|
||||
if (callbacks.catchFunc) {
|
||||
individualPromiseCallback.catchFunc = [state, callbacks](emscripten::val error) mutable {
|
||||
callbacks.catchFunc(error);
|
||||
};
|
||||
}
|
||||
individualPromiseCallback.finallyFunc = [state, callbacks]() mutable {
|
||||
if (!--(state->remainingFinallyCallbacks)) {
|
||||
if (callbacks.finallyFunc)
|
||||
callbacks.finallyFunc();
|
||||
// Explicitly reset here for verbosity, this would have been done automatically with the
|
||||
// destruction of the adopted promise in WebPromiseManager.
|
||||
state.reset();
|
||||
}
|
||||
};
|
||||
// Clean up event handlers
|
||||
if (thenIndex)
|
||||
suspendResume->removeEventHandler(*thenIndex);
|
||||
if (catchIndex)
|
||||
suspendResume->removeEventHandler(*catchIndex);
|
||||
suspendResume->removeEventHandler(*finallyIndex);
|
||||
|
||||
adoptPromise(std::move(promises.at(i)), std::move(individualPromiseCallback));
|
||||
}
|
||||
}
|
||||
// Call user finally
|
||||
if (finallyFunc)
|
||||
finallyFunc();
|
||||
};
|
||||
|
||||
*finallyIndex = suspendResume->registerEventHandler(std::move(finally));
|
||||
|
||||
// Set handlers on the promise
|
||||
if (thenIndex)
|
||||
promise.call<emscripten::val>("then", suspendResume->jsEventHandlerAt(*thenIndex));
|
||||
if (catchIndex)
|
||||
promise.call<emscripten::val>("catch", suspendResume->jsEventHandlerAt(*catchIndex));
|
||||
promise.call<emscripten::val>("finally", suspendResume->jsEventHandlerAt(*finallyIndex));
|
||||
}
|
||||
|
||||
void Promise::all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks)
|
||||
{
|
||||
auto arr = emscripten::val::array(promises);
|
||||
auto all = val::global("Promise").call<emscripten::val>("all", arr);
|
||||
return adoptPromise(all, callbacks);
|
||||
}
|
||||
|
||||
// Asyncify and thread blocking: Normally, it's not possible to block the main
|
||||
|
@ -5,6 +5,7 @@
|
||||
#include <QtCore/QEvent>
|
||||
#include <QtCore/QMutex>
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtCore/private/qstdweb_p.h>
|
||||
|
||||
#include <qtwasmtestlib.h>
|
||||
@ -53,7 +54,6 @@ private slots:
|
||||
void multipleResolve();
|
||||
void simpleReject();
|
||||
void multipleReject();
|
||||
void throwInThen();
|
||||
void bareFinally();
|
||||
void finallyWithThen();
|
||||
void finallyWithThrow();
|
||||
@ -188,28 +188,6 @@ void WasmPromiseTest::multipleReject()
|
||||
}, promiseCount);
|
||||
}
|
||||
|
||||
void WasmPromiseTest::throwInThen()
|
||||
{
|
||||
init();
|
||||
|
||||
qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
|
||||
.thenFunc = [](val result) {
|
||||
Q_UNUSED(result);
|
||||
EM_ASM({
|
||||
throw "Expected error";
|
||||
});
|
||||
},
|
||||
.catchFunc = [](val error) {
|
||||
QWASMCOMPARE("Expected error", error.as<std::string>());
|
||||
QWASMSUCCESS();
|
||||
}
|
||||
}, std::string("throwInThen"));
|
||||
|
||||
EM_ASM({
|
||||
testSupport.resolve["throwInThen"]();
|
||||
});
|
||||
}
|
||||
|
||||
void WasmPromiseTest::bareFinally()
|
||||
{
|
||||
init();
|
||||
@ -229,7 +207,7 @@ void WasmPromiseTest::finallyWithThen()
|
||||
{
|
||||
init();
|
||||
|
||||
auto thenCalled = std::make_shared<bool>();
|
||||
bool *thenCalled = new bool(false);
|
||||
qstdweb::Promise::make(m_testSupport, "makeTestPromise", {
|
||||
.thenFunc = [thenCalled] (val result) {
|
||||
Q_UNUSED(result);
|
||||
@ -237,6 +215,7 @@ void WasmPromiseTest::finallyWithThen()
|
||||
},
|
||||
.finallyFunc = [thenCalled]() {
|
||||
QWASMVERIFY(*thenCalled);
|
||||
delete thenCalled;
|
||||
QWASMSUCCESS();
|
||||
}
|
||||
}, std::string("finallyWithThen"));
|
||||
|
Loading…
x
Reference in New Issue
Block a user