Closed toyobayashi closed 1 month ago
FWIW, node-addon-api is restricted to the same build restrictions as node, which is c++17.
@KevinEady you are right, but we have two choices:
optin
the feature in case C++ 20 is enabledWhaty do you think about?
I think in instances where functionality is added that is not specifically a wrapper for Node-API functionality, we defer to placing the functionality in a separate module/package owned by the original code writer (and therefore not maintained by us), eg. https://github.com/nodejs/node-addon-api/issues/1163
@KevinEady Is it a better choice to add promise_type
and operator co_await
to Napi::Value
instead of Napi::Promise
? It's similar to JavaScript that can await any type of JavaScript values and the coroutine suspends when await a Thenable
. If go this way, since the Napi::Value
is the base class of all values, I think place changes of Napi::Value
in node-addon-api repo is reasonable. Also adding #if __cplusplus >= 202002L
guard to allow optin.
{
"cflags_cc": [ "-std=c++20" ],
"xcode_settings": {
"CLANG_CXX_LANGUAGE_STANDARD":"c++20",
"OTHER_CPLUSPLUSFLAGS": [ "-std=c++20" ]
},
# https://github.com/nodejs/node-gyp/issues/1662#issuecomment-754332545
"msbuild_settings": {
"ClCompile": {
"LanguageStandard": "stdcpp20"
}
},
}
for example, I changed my toy implementation and placed it in node_modules/node-addon-api/napi.h
diff --git a/node_modules/node-addon-api/napi.h b/node_modules/node-addon-api/napi.h
Then the usage becomes more nature
#ifdef NAPI_CPP_EXCEPTIONS
#define NAPI_THROW_CO_RETURN(e, ...) throw e
#else
#define NAPI_THROW_CO_RETURN(e, ...) \
do { \
(e).ThrowAsJavaScriptException(); \
co_return __VA_ARGS__; \
} while (0)
#endif
Napi::Value NestedCoroutine(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::Value async_function = info[0];
if (!async_function.IsFunction()) {
NAPI_THROW_CO_RETURN(Napi::Error::New(env, "not function"), Napi::Value());
}
Napi::Value result = co_await async_function.As<Napi::Function>()({});
result = co_await result; // ok
co_return Napi::Number::New(env, result.As<Napi::Number>().DoubleValue() * 2);
}
Napi::Value 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);
}
Napi::Value CoroutineThrow(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
co_await NestedCoroutine(info);
NAPI_THROW_CO_RETURN(Napi::Error::New(env, "test error"), Napi::Value());
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;
}
It would be cool if node-addon-api can get this feature.
https://github.com/nodejs/node-addon-api/compare/main...toyobayashi:node-addon-api:coroutine
I added changes and test in my fork. This is a very simple implementation and have not tested complex use case.
Following up on @KevinEady's earlier comment about node-addon-api being a thin wrapper, this is documented in https://github.com/nodejs/node-addon-api/blob/main/CONTRIBUTING.md#source-changes.
We discussed in the node-api team meeting today and based on our documented approach we believe this functionality is best covered in a separated module outside of node-addon-api unless that is impossible.
Some team members are going to take a deeper look and we'll talk about it again next time.
This issue is stale because it has been open many days with no activity. It will be closed soon unless the stale label is removed or a comment is made.
We discussed again today, from the discussion in the meeting today, the feeling of the team continuies to be that it can be implemented on top of node-addon-api without needing to be integrated. Can you confirm that?
That along with the agreed approach of keeping node-api and node-addon-api lean as documented in https://github.com/nodejs/node-addon-api/blob/main/CONTRIBUTING.md#source-changes means that we believe it should be implemented in an external libray versus integrated into node-addon-api itself.
Our suggestion is that you create a separate repo/npm package to provide the functionality. If you do that please let us know and we will consider linking to it from the node-addon-api README.md for those who would be interested in co-routine support.
Let us know if that makes sense to you so we can close the issue?
Respect team's opinion, I create this issue just inspired by embind. Thanks for your suggestion.
embind already has coroutine implementation
https://github.com/emscripten-core/emscripten/blob/b5b7fedda835bdf8f172a700726109a4a3899909/system/include/emscripten/val.h#L703-L789
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
```cpp #include
binding.gyp
```python { "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": [ "
binding.cpp
```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
index.js
```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