Closed
Description
embind already has coroutine implementation
I just now tried to write a toy version, that makes it possible to co_await
a JavaScript Promise in C++.
class CoPromise : public Napi::Promise
#include <coroutine>
#include <exception>
#include <napi.h>
class CoPromise : public Napi::Promise {
public:
CoPromise(napi_env env, napi_value value): Napi::Promise(env, value) {};
class promise_type {
private:
Napi::Env env_;
Napi::Promise::Deferred deferred_;
public:
promise_type(const Napi::CallbackInfo& info):
env_(info.Env()), deferred_(Napi::Promise::Deferred::New(info.Env())) {}
CoPromise get_return_object() const {
return deferred_.Promise().As<CoPromise>();
}
std::suspend_never initial_suspend () const noexcept { return {}; }
std::suspend_never final_suspend () const noexcept { return {}; }
void unhandled_exception() const {
std::exception_ptr exception = std::current_exception();
try {
std::rethrow_exception(exception);
} catch (const Napi::Error& e) {
deferred_.Reject(e.Value());
} catch (const std::exception &e) {
deferred_.Reject(Napi::Error::New(env_, e.what()).Value());
} catch (const std::string& e) {
deferred_.Reject(Napi::Error::New(env_, e).Value());
} catch (const char* e) {
deferred_.Reject(Napi::Error::New(env_, e).Value());
} catch (...) {
deferred_.Reject(Napi::Error::New(env_, "Unknown Error").Value());
}
}
void return_value(Value value) const {
Resolve(value);
}
void Resolve(Value value) const {
deferred_.Resolve(value);
}
void Reject(Value value) const {
deferred_.Reject(value);
}
};
class Awaiter {
private:
Napi::Promise promise_;
std::coroutine_handle<promise_type> handle_;
Napi::Value fulfilled_result_;
public:
Awaiter(Napi::Promise promise): promise_(promise), handle_(), fulfilled_result_() {}
constexpr bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<promise_type> handle) {
handle_ = handle;
promise_.Get("then").As<Napi::Function>().Call(promise_, {
Napi::Function::New(promise_.Env(), [this](const Napi::CallbackInfo& info) -> Value {
fulfilled_result_ = info[0];
handle_.resume();
return info.Env().Undefined();
}),
Napi::Function::New(promise_.Env(), [this](const Napi::CallbackInfo& info) -> Value {
handle_.promise().Reject(info[0]);
handle_.destroy();
return info.Env().Undefined();
})
});
}
Value await_resume() const {
return fulfilled_result_;
}
};
Awaiter operator co_await() const {
return Awaiter(*this);
}
};
binding.gyp
{
"target_defaults": {
"cflags_cc": [ "-std=c++20" ],
"xcode_settings": {
"CLANG_CXX_LANGUAGE_STANDARD":"c++20"
},
# https://github.com/nodejs/node-gyp/issues/1662#issuecomment-754332545
"msbuild_settings": {
"ClCompile": {
"LanguageStandard": "stdcpp20"
}
},
},
"targets": [
{
"target_name": "binding",
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except"
],
"sources": [
"src/binding.cpp"
]
}
]
}
binding.cpp
CoPromise NestedCoroutine(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::Value async_function = info[0];
if (!async_function.IsFunction()) {
throw Napi::Error::New(env, "not function");
}
Napi::Value result = co_await async_function.As<Napi::Function>()({}).As<CoPromise>();
co_return Napi::Number::New(env, result.As<Napi::Number>().DoubleValue() * 2);
}
CoPromise Coroutine(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::Value number = co_await NestedCoroutine(info);
co_return Napi::Number::New(env, number.As<Napi::Number>().DoubleValue() * 2);
}
CoPromise CoroutineThrow(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::Value number = co_await NestedCoroutine(info);
throw Napi::Error::New(env, "test error");
co_return Napi::Value();
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("coroutine", Napi::Function::New(env, Coroutine));
exports.Set("coroutineThrow", Napi::Function::New(env, CoroutineThrow));
return exports;
}
NODE_API_MODULE(addon, Init)
index.js
const binding = require('./build/Release/binding.node')
async function main () {
await binding.coroutine(function () {
return new Promise((resolve, _) => {
setTimeout(() => {
resolve(42)
}, 1000)
})
}).then(value => {
console.log(value)
}).catch(err => {
console.error('JS caught error', err)
})
await binding.coroutine(function () {
return new Promise((_, reject) => {
setTimeout(() => {
reject(42)
}, 1000)
})
}).then(value => {
console.log(value)
}).catch(err => {
console.error('JS caught error', err)
})
await binding.coroutineThrow(function () {
return new Promise((resolve, _) => {
setTimeout(() => {
resolve(42)
}, 1000)
})
}).then(value => {
console.log(value)
}).catch(err => {
console.error('JS caught error', err)
})
}
main()
node index.js
(1000ms after)
168
(1000ms after)
JS caught error 42
(1000ms after)
JS caught error [Error: test error]
output
Metadata
Metadata
Assignees
Labels
No labels