diff --git a/.changeset/rich-donuts-mix.md b/.changeset/rich-donuts-mix.md new file mode 100644 index 00000000..964a352e --- /dev/null +++ b/.changeset/rich-donuts-mix.md @@ -0,0 +1,6 @@ +--- +"react-native-node-api-test-app": patch +"react-native-node-api": patch +--- + +Added implementation of async work runtime functions diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 1abfe758..5c8c9199 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -152,7 +152,18 @@ jobs: adb reverse tcp:8090 tcp:8090 # Uninstall the app if already in the snapshot (unlikely but could result in a signature mismatch failure) adb uninstall com.microsoft.reacttestapp || true + # Start logcat in background and save logs + adb logcat > emulator-logcat.txt 2>&1 & + LOGCAT_PID=$! # Build, install and run the app npm run test:android -- --mode Release # Wait a bit for the sub-process to terminate, before terminating the emulator sleep 5 + # Stop logcat + kill $LOGCAT_PID || true + - name: Upload device logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: emulator-logcat + path: apps/test-app/emulator-logcat.txt diff --git a/apps/test-app/App.tsx b/apps/test-app/App.tsx index 48a11861..3ca4ddc2 100644 --- a/apps/test-app/App.tsx +++ b/apps/test-app/App.tsx @@ -14,8 +14,11 @@ function loadTests() { for (const [suiteName, examples] of Object.entries(nodeAddonExamples)) { describe(suiteName, () => { for (const [exampleName, requireExample] of Object.entries(examples)) { - it(exampleName, () => { - requireExample(); + it(exampleName, async () => { + const test = requireExample(); + if (test instanceof Function) { + await test(); + } }); } }); diff --git a/package-lock.json b/package-lock.json index 9774e28a..1940bfc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6442,6 +6442,19 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -6470,6 +6483,21 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "1.8.4", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", @@ -6862,6 +6890,24 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -7580,6 +7626,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -8394,6 +8474,21 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -8762,6 +8857,18 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -9019,12 +9126,40 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -9082,6 +9217,24 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -9103,6 +9256,22 @@ "node": ">=8" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9121,6 +9290,24 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -9146,6 +9333,21 @@ "node": ">=4" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -10728,6 +10930,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -11075,6 +11322,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11766,6 +12022,23 @@ ], "license": "MIT" }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -11898,6 +12171,23 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -12773,6 +13063,19 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", "license": "MIT" }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12877,6 +13180,27 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -14191,6 +14515,9 @@ }, "packages/node-addon-examples": { "name": "@react-native-node-api/node-addon-examples", + "dependencies": { + "assert": "^2.1.0" + }, "devDependencies": { "cmake-rn": "*", "gyp-to-cmake": "*", diff --git a/packages/host/android/CMakeLists.txt b/packages/host/android/CMakeLists.txt index 9fd1d22c..d062762a 100644 --- a/packages/host/android/CMakeLists.txt +++ b/packages/host/android/CMakeLists.txt @@ -22,6 +22,8 @@ add_library(node-api-host SHARED ../cpp/WeakNodeApiInjector.cpp ../cpp/RuntimeNodeApi.cpp ../cpp/RuntimeNodeApi.hpp + ../cpp/RuntimeNodeApiAsync.cpp + ../cpp/RuntimeNodeApiAsync.hpp ) target_include_directories(node-api-host PRIVATE diff --git a/packages/host/cpp/CxxNodeApiHostModule.cpp b/packages/host/cpp/CxxNodeApiHostModule.cpp index 727241cc..950c7af6 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.cpp +++ b/packages/host/cpp/CxxNodeApiHostModule.cpp @@ -1,5 +1,6 @@ #include "CxxNodeApiHostModule.hpp" #include "Logger.hpp" +#include "RuntimeNodeApiAsync.hpp" using namespace facebook; @@ -10,6 +11,8 @@ CxxNodeApiHostModule::CxxNodeApiHostModule( : TurboModule(CxxNodeApiHostModule::kModuleName, jsInvoker) { methodMap_["requireNodeAddon"] = MethodMetadata{1, &CxxNodeApiHostModule::requireNodeAddon}; + + callInvoker_ = std::move(jsInvoker); } jsi::Value @@ -124,6 +127,7 @@ bool CxxNodeApiHostModule::initializeNodeModule(jsi::Runtime &rt, napi_set_named_property(env, global, addon.generatedName.data(), exports); assert(status == napi_ok); + callstack::nodeapihost::setCallInvoker(env, callInvoker_); return true; } diff --git a/packages/host/cpp/CxxNodeApiHostModule.hpp b/packages/host/cpp/CxxNodeApiHostModule.hpp index f77a89af..9445cdaf 100644 --- a/packages/host/cpp/CxxNodeApiHostModule.hpp +++ b/packages/host/cpp/CxxNodeApiHostModule.hpp @@ -28,6 +28,8 @@ class JSI_EXPORT CxxNodeApiHostModule : public facebook::react::TurboModule { std::string generatedName; }; std::unordered_map nodeAddons_; + std::shared_ptr callInvoker_; + using LoaderPolicy = PosixLoader; // FIXME: HACK: This is temporary workaround // for my lazyness (work on iOS and Android) diff --git a/packages/host/cpp/RuntimeNodeApi.cpp b/packages/host/cpp/RuntimeNodeApi.cpp index dc661833..1644240e 100644 --- a/packages/host/cpp/RuntimeNodeApi.cpp +++ b/packages/host/cpp/RuntimeNodeApi.cpp @@ -3,11 +3,13 @@ auto ArrayType = napi_uint8_array; -napi_status NAPI_CDECL callstack::nodeapihost::napi_create_buffer( +namespace callstack::nodeapihost { + +napi_status napi_create_buffer( napi_env env, size_t length, void** data, napi_value* result) { napi_value buffer; - const auto status = napi_create_arraybuffer(env, length, data, &buffer); - if (status != napi_ok) { + if (const auto status = napi_create_arraybuffer(env, length, data, &buffer); + status != napi_ok) { return status; } @@ -18,8 +20,7 @@ napi_status NAPI_CDECL callstack::nodeapihost::napi_create_buffer( return napi_create_typedarray(env, ArrayType, length, buffer, 0, result); } -napi_status NAPI_CDECL callstack::nodeapihost::napi_create_buffer_copy( - napi_env env, +napi_status napi_create_buffer_copy(napi_env env, size_t length, const void* data, void** result_data, @@ -38,8 +39,7 @@ napi_status NAPI_CDECL callstack::nodeapihost::napi_create_buffer_copy( return napi_ok; } -napi_status callstack::nodeapihost::napi_is_buffer( - napi_env env, napi_value value, bool* result) { +napi_status napi_is_buffer(napi_env env, napi_value value, bool* result) { if (!result) { return napi_invalid_arg; } @@ -74,7 +74,7 @@ napi_status callstack::nodeapihost::napi_is_buffer( return napi_ok; } -napi_status callstack::nodeapihost::napi_get_buffer_info( +napi_status napi_get_buffer_info( napi_env env, napi_value value, void** data, size_t* length) { if (!data || !length) { return napi_invalid_arg; @@ -101,12 +101,24 @@ napi_status callstack::nodeapihost::napi_get_buffer_info( return napi_ok; } -napi_status callstack::nodeapihost::napi_create_external_buffer(napi_env env, +napi_status napi_create_external_buffer(napi_env env, size_t length, void* data, node_api_basic_finalize basic_finalize_cb, void* finalize_hint, napi_value* result) { - return napi_create_external_arraybuffer( - env, data, length, basic_finalize_cb, finalize_hint, result); + napi_value buffer; + if (const auto status = napi_create_external_arraybuffer( + env, data, length, basic_finalize_cb, finalize_hint, &buffer); + status != napi_ok) { + return status; + } + + // Warning: The returned data structure does not fully align with the + // characteristics of a Buffer. + // @see + // https://github.com/callstackincubator/react-native-node-api/issues/171 + return napi_create_typedarray(env, ArrayType, length, buffer, 0, result); } + +} // namespace callstack::nodeapihost diff --git a/packages/host/cpp/RuntimeNodeApi.hpp b/packages/host/cpp/RuntimeNodeApi.hpp index e64b03fa..5b7d9b86 100644 --- a/packages/host/cpp/RuntimeNodeApi.hpp +++ b/packages/host/cpp/RuntimeNodeApi.hpp @@ -1,3 +1,5 @@ +#pragma once + #include "node_api.h" namespace callstack::nodeapihost { @@ -21,5 +23,4 @@ napi_status napi_create_external_buffer(napi_env env, node_api_basic_finalize basic_finalize_cb, void* finalize_hint, napi_value* result); - } // namespace callstack::nodeapihost diff --git a/packages/host/cpp/RuntimeNodeApiAsync.cpp b/packages/host/cpp/RuntimeNodeApiAsync.cpp new file mode 100644 index 00000000..dd4c87c3 --- /dev/null +++ b/packages/host/cpp/RuntimeNodeApiAsync.cpp @@ -0,0 +1,190 @@ +#include "RuntimeNodeApiAsync.hpp" +#include +#include "Logger.hpp" + +struct AsyncJob { + using IdType = uint64_t; + enum State { Created, Queued, Completed, Cancelled, Deleted }; + + IdType id{}; + State state{}; + napi_env env; + napi_value async_resource; + napi_value async_resource_name; + napi_async_execute_callback execute; + napi_async_complete_callback complete; + void* data{nullptr}; + + static AsyncJob* fromWork(napi_async_work work) { + return reinterpret_cast(work); + } + static napi_async_work toWork(AsyncJob* job) { + return reinterpret_cast(job); + } +}; + +class AsyncWorkRegistry { + public: + using IdType = AsyncJob::IdType; + + std::shared_ptr create(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data) { + const auto job = std::shared_ptr(new AsyncJob{ + .id = next_id(), + .state = AsyncJob::State::Created, + .env = env, + .async_resource = async_resource, + .async_resource_name = async_resource_name, + .execute = execute, + .complete = complete, + .data = data, + }); + + jobs_[job->id] = job; + return job; + } + + std::shared_ptr get(napi_async_work work) const { + const auto job = AsyncJob::fromWork(work); + if (!job) { + return {}; + } + if (const auto it = jobs_.find(job->id); it != jobs_.end()) { + return it->second; + } + return {}; + } + + bool release(IdType id) { + if (const auto it = jobs_.find(id); it != jobs_.end()) { + it->second->state = AsyncJob::State::Deleted; + jobs_.erase(it); + return true; + } + return false; + } + + private: + IdType next_id() { + if (current_id_ == std::numeric_limits::max()) [[unlikely]] { + current_id_ = 0; + } + return ++current_id_; + } + + IdType current_id_{0}; + std::unordered_map> jobs_; +}; + +static std::unordered_map> + callInvokers; +static AsyncWorkRegistry asyncWorkRegistry; + +namespace callstack::nodeapihost { + +void setCallInvoker(napi_env env, + const std::shared_ptr& invoker) { + callInvokers[env] = invoker; +} + +std::weak_ptr getCallInvoker(napi_env env) { + return callInvokers.contains(env) + ? callInvokers[env] + : std::weak_ptr{}; +} + +napi_status napi_create_async_work(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data, + napi_async_work* result) { + const auto job = asyncWorkRegistry.create( + env, async_resource, async_resource_name, execute, complete, data); + if (!job) { + log_debug("Error: Failed to create async work job"); + return napi_generic_failure; + } + + *result = AsyncJob::toWork(job.get()); + return napi_ok; +} + +napi_status napi_queue_async_work( + node_api_basic_env env, napi_async_work work) { + const auto job = asyncWorkRegistry.get(work); + if (!job) { + log_debug("Error: Received null job in napi_queue_async_work"); + return napi_invalid_arg; + } + + const auto invoker = getCallInvoker(env).lock(); + if (!invoker) { + log_debug("Error: No CallInvoker available for async work"); + return napi_invalid_arg; + } + + invoker->invokeAsync([env, weakJob = std::weak_ptr{job}]() { + const auto job = weakJob.lock(); + if (!job) { + log_debug("Error: Async job has been deleted before execution"); + return; + } + if (job->state == AsyncJob::State::Queued) { + job->execute(job->env, job->data); + } + + job->complete(env, + job->state == AsyncJob::State::Cancelled ? napi_cancelled : napi_ok, + job->data); + job->state = AsyncJob::State::Completed; + }); + + job->state = AsyncJob::State::Queued; + return napi_ok; +} + +napi_status napi_delete_async_work( + node_api_basic_env env, napi_async_work work) { + const auto job = asyncWorkRegistry.get(work); + if (!job) { + log_debug("Error: Received non-existent job in napi_delete_async_work"); + return napi_invalid_arg; + } + + if (!asyncWorkRegistry.release(job->id)) { + log_debug("Error: Failed to release async work job"); + return napi_generic_failure; + } + + return napi_ok; +} + +napi_status napi_cancel_async_work( + node_api_basic_env env, napi_async_work work) { + const auto job = asyncWorkRegistry.get(work); + if (!job) { + log_debug("Error: Received null job in napi_cancel_async_work"); + return napi_invalid_arg; + } + switch (job->state) { + case AsyncJob::State::Completed: + log_debug("Error: Cannot cancel async work that is already completed"); + return napi_generic_failure; + case AsyncJob::State::Deleted: + log_debug("Warning: Async work job is already deleted"); + return napi_generic_failure; + case AsyncJob::State::Cancelled: + log_debug("Warning: Async work job is already cancelled"); + return napi_ok; + } + + job->state = AsyncJob::State::Cancelled; + return napi_ok; +} +} // namespace callstack::nodeapihost diff --git a/packages/host/cpp/RuntimeNodeApiAsync.hpp b/packages/host/cpp/RuntimeNodeApiAsync.hpp new file mode 100644 index 00000000..f0108e6d --- /dev/null +++ b/packages/host/cpp/RuntimeNodeApiAsync.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include "node_api.h" + +namespace callstack::nodeapihost { +void setCallInvoker( + napi_env env, const std::shared_ptr& invoker); + +napi_status napi_create_async_work(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data, + napi_async_work* result); + +napi_status napi_queue_async_work(node_api_basic_env env, napi_async_work work); + +napi_status napi_delete_async_work( + node_api_basic_env env, napi_async_work work); + +napi_status napi_cancel_async_work( + node_api_basic_env env, napi_async_work work); +} // namespace callstack::nodeapihost diff --git a/packages/host/scripts/generate-weak-node-api-injector.ts b/packages/host/scripts/generate-weak-node-api-injector.ts index 71e66893..129d2fc0 100644 --- a/packages/host/scripts/generate-weak-node-api-injector.ts +++ b/packages/host/scripts/generate-weak-node-api-injector.ts @@ -13,6 +13,10 @@ const IMPLEMENTED_RUNTIME_FUNCTIONS = [ "napi_is_buffer", "napi_get_buffer_info", "napi_create_external_buffer", + "napi_create_async_work", + "napi_queue_async_work", + "napi_delete_async_work", + "napi_cancel_async_work", ]; /** @@ -25,6 +29,7 @@ export function generateSource(functions: FunctionDecl[]) { #include #include #include + #include #if defined(__APPLE__) #define WEAK_NODE_API_LIBRARY_NAME "@rpath/weak-node-api.framework/weak-node-api" diff --git a/packages/node-addon-examples/index.js b/packages/node-addon-examples/index.js index 8d5faecf..e5bf5eeb 100644 --- a/packages/node-addon-examples/index.js +++ b/packages/node-addon-examples/index.js @@ -17,5 +17,6 @@ module.exports = { }, "tests": { "buffers": () => require("./tests/buffers/addon.js"), + "async": () => require("./tests/async/addon.js"), }, }; diff --git a/packages/node-addon-examples/package.json b/packages/node-addon-examples/package.json index 8960d63d..3ebee6f5 100644 --- a/packages/node-addon-examples/package.json +++ b/packages/node-addon-examples/package.json @@ -22,5 +22,8 @@ "node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a", "gyp-to-cmake": "*", "read-pkg": "^9.0.1" + }, + "dependencies": { + "assert": "^2.1.0" } } diff --git a/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h b/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h new file mode 100644 index 00000000..9db55d7f --- /dev/null +++ b/packages/node-addon-examples/tests/RuntimeNodeApiTestsCommon.h @@ -0,0 +1,108 @@ +#pragma once + +#define NODE_API_RETVAL_NOTHING // Intentionally blank #define + +#define GET_AND_THROW_LAST_ERROR(env) \ + do { \ + const napi_extended_error_info* error_info; \ + napi_get_last_error_info((env), &error_info); \ + bool is_pending; \ + const char* err_message = error_info->error_message; \ + napi_is_exception_pending((env), &is_pending); \ + /* If an exception is already pending, don't rethrow it */ \ + if (!is_pending) { \ + const char* error_message = \ + err_message != NULL ? err_message : "empty error message"; \ + napi_throw_error((env), NULL, error_message); \ + } \ + } while (0) + +// The basic version of GET_AND_THROW_LAST_ERROR. We cannot access any +// exceptions and we cannot fail by way of JS exception, so we abort. +#define FATALLY_FAIL_WITH_LAST_ERROR(env) \ + do { \ + const napi_extended_error_info* error_info; \ + napi_get_last_error_info((env), &error_info); \ + const char* err_message = error_info->error_message; \ + const char* error_message = \ + err_message != NULL ? err_message : "empty error message"; \ + fprintf(stderr, "%s\n", error_message); \ + abort(); \ + } while (0) + +#define NODE_API_ASSERT_BASE(env, assertion, message, ret_val) \ + do { \ + if (!(assertion)) { \ + napi_throw_error( \ + (env), NULL, "assertion (" #assertion ") failed: " message); \ + return ret_val; \ + } \ + } while (0) + +#define NODE_API_BASIC_ASSERT_BASE(assertion, message, ret_val) \ + do { \ + if (!(assertion)) { \ + fprintf(stderr, "assertion (" #assertion ") failed: " message); \ + abort(); \ + return ret_val; \ + } \ + } while (0) + +// Returns NULL on failed assertion. +// This is meant to be used inside napi_callback methods. +#define NODE_API_ASSERT(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, NULL) + +// Returns empty on failed assertion. +// This is meant to be used inside functions with void return type. +#define NODE_API_ASSERT_RETURN_VOID(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, NODE_API_RETVAL_NOTHING) + +#define NODE_API_BASIC_ASSERT_RETURN_VOID(assertion, message) \ + NODE_API_BASIC_ASSERT_BASE(assertion, message, NODE_API_RETVAL_NOTHING) + +#define NODE_API_CALL_BASE(env, the_call, ret_val) \ + do { \ + if ((the_call) != napi_ok) { \ + GET_AND_THROW_LAST_ERROR((env)); \ + return ret_val; \ + } \ + } while (0) + +#define NODE_API_BASIC_CALL_BASE(env, the_call, ret_val) \ + do { \ + if ((the_call) != napi_ok) { \ + FATALLY_FAIL_WITH_LAST_ERROR((env)); \ + return ret_val; \ + } \ + } while (0) + +// Returns NULL if the_call doesn't return napi_ok. +#define NODE_API_CALL(env, the_call) NODE_API_CALL_BASE(env, the_call, NULL) + +// Returns empty if the_call doesn't return napi_ok. +#define NODE_API_CALL_RETURN_VOID(env, the_call) \ + NODE_API_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) + +#define NODE_API_BASIC_CALL_RETURN_VOID(env, the_call) \ + NODE_API_BASIC_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) + +#define NODE_API_CHECK_STATUS(the_call) \ + do { \ + napi_status status = (the_call); \ + if (status != napi_ok) { \ + return status; \ + } \ + } while (0) + +#define NODE_API_ASSERT_STATUS(env, assertion, message) \ + NODE_API_ASSERT_BASE(env, assertion, message, napi_generic_failure) + +#define DECLARE_NODE_API_PROPERTY(name, func) \ + {(name), NULL, (func), NULL, NULL, NULL, napi_default, NULL} + +#define DECLARE_NODE_API_GETTER(name, func) \ + {(name), NULL, NULL, (func), NULL, NULL, napi_default, NULL} + +#define DECLARE_NODE_API_PROPERTY_VALUE(name, value) \ + {(name), NULL, NULL, NULL, NULL, (value), napi_default, NULL} diff --git a/packages/node-addon-examples/tests/async/CMakeLists.txt b/packages/node-addon-examples/tests/async/CMakeLists.txt new file mode 100644 index 00000000..31d513c0 --- /dev/null +++ b/packages/node-addon-examples/tests/async/CMakeLists.txt @@ -0,0 +1,15 @@ +cmake_minimum_required(VERSION 3.15) +project(tests-async) + +add_compile_definitions(NAPI_VERSION=8) + +add_library(addon SHARED addon.c ${CMAKE_JS_SRC}) +set_target_properties(addon PROPERTIES PREFIX "" SUFFIX ".node") +target_include_directories(addon PRIVATE ${CMAKE_JS_INC}) +target_link_libraries(addon PRIVATE ${CMAKE_JS_LIB}) +target_compile_features(addon PRIVATE cxx_std_17) + +if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) + # Generate node.lib + execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) +endif() \ No newline at end of file diff --git a/packages/node-addon-examples/tests/async/addon.c b/packages/node-addon-examples/tests/async/addon.c new file mode 100644 index 00000000..9444aacf --- /dev/null +++ b/packages/node-addon-examples/tests/async/addon.c @@ -0,0 +1,259 @@ +#include +#include +#include +#include +#include +#include "../RuntimeNodeApiTestsCommon.h" + +#ifdef WIN32 +#include +#elif _POSIX_C_SOURCE >= 199309L +#include // for nanosleep +#else +#include // for usleep +#endif + +void sleep_ms(int milliseconds) { // cross-platform sleep function +#ifdef WIN32 + Sleep(milliseconds); +#elif _POSIX_C_SOURCE >= 199309L + struct timespec ts; + ts.tv_sec = milliseconds / 1000; + ts.tv_nsec = (milliseconds % 1000) * 1000000; + nanosleep(&ts, NULL); +#else + if (milliseconds >= 1000) sleep(milliseconds / 1000); + usleep((milliseconds % 1000) * 1000); +#endif +} +#define MAX_CANCEL_THREADS 6 + +typedef struct { + int32_t _input; + int32_t _output; + napi_ref _callback; + napi_async_work _request; +} carrier; + +static carrier the_carrier; +static carrier async_carrier[MAX_CANCEL_THREADS]; + +static void Execute(napi_env env, void* data) { + sleep_ms(10); + carrier* c = (carrier*)(data); + + assert(c == &the_carrier); + + c->_output = c->_input * 2; +} + +static void Complete(napi_env env, napi_status status, void* data) { + carrier* c = (carrier*)(data); + + if (c != &the_carrier) { + napi_throw_type_error(env, NULL, "Wrong data parameter to Complete."); + return; + } + + if (status != napi_ok && status != napi_cancelled) { + napi_throw_type_error(env, NULL, "Execute callback failed."); + return; + } + + napi_value argv[2]; + + NODE_API_CALL_RETURN_VOID(env, napi_get_null(env, &argv[0])); + NODE_API_CALL_RETURN_VOID(env, napi_create_int32(env, c->_output, &argv[1])); + napi_value callback; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, c->_callback, &callback)); + napi_value global; + NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &global)); + napi_value result; + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, global, callback, 2, argv, &result)); + + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, c->_callback)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request)); +} + +static napi_value Test(napi_env env, napi_callback_info info) { + size_t argc = 3; + napi_value argv[3]; + napi_value _this; + napi_value resource_name; + void* data; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, &_this, &data)); + NODE_API_ASSERT(env, argc >= 3, "Not enough arguments, expected 3."); + + napi_valuetype t; + NODE_API_CALL(env, napi_typeof(env, argv[0], &t)); + NODE_API_ASSERT( + env, t == napi_number, "Wrong first argument, integer expected."); + NODE_API_CALL(env, napi_typeof(env, argv[1], &t)); + NODE_API_ASSERT( + env, t == napi_object, "Wrong second argument, object expected."); + NODE_API_CALL(env, napi_typeof(env, argv[2], &t)); + NODE_API_ASSERT( + env, t == napi_function, "Wrong third argument, function expected."); + + the_carrier._output = 0; + + NODE_API_CALL(env, napi_get_value_int32(env, argv[0], &the_carrier._input)); + NODE_API_CALL( + env, napi_create_reference(env, argv[2], 1, &the_carrier._callback)); + + NODE_API_CALL(env, + napi_create_string_utf8( + env, "TestResource", NAPI_AUTO_LENGTH, &resource_name)); + NODE_API_CALL(env, + napi_create_async_work(env, + argv[1], + resource_name, + Execute, + Complete, + &the_carrier, + &the_carrier._request)); + NODE_API_CALL(env, napi_queue_async_work(env, the_carrier._request)); + + return NULL; +} + +static void BusyCancelComplete(napi_env env, napi_status status, void* data) { + carrier* c = (carrier*)(data); + NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request)); +} + +static void CancelComplete(napi_env env, napi_status status, void* data) { + carrier* c = (carrier*)(data); + + if (status == napi_cancelled) { + // ok we got the status we expected so make the callback to + // indicate the cancel succeeded. + napi_value callback; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, c->_callback, &callback)); + napi_value global; + NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &global)); + napi_value result; + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, global, callback, 0, NULL, &result)); + } + + NODE_API_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request)); + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, c->_callback)); +} + +static void CancelExecute(napi_env env, void* data) { + sleep_ms(100); +} + +static napi_value TestCancel(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + napi_value _this; + napi_value resource_name; + void* data; + + NODE_API_CALL(env, + napi_create_string_utf8( + env, "TestResource", NAPI_AUTO_LENGTH, &resource_name)); + + // make sure the work we are going to cancel will not be + // able to start by using all the threads in the pool + for (int i = 1; i < MAX_CANCEL_THREADS; i++) { + NODE_API_CALL(env, + napi_create_async_work(env, + NULL, + resource_name, + CancelExecute, + BusyCancelComplete, + &async_carrier[i], + &async_carrier[i]._request)); + NODE_API_CALL(env, napi_queue_async_work(env, async_carrier[i]._request)); + } + + // now queue the work we are going to cancel and then cancel it. + // cancel will fail if the work has already started, but + // we have prevented it from starting by consuming all of the + // workers above. + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, &_this, &data)); + NODE_API_CALL(env, + napi_create_async_work(env, + NULL, + resource_name, + CancelExecute, + CancelComplete, + &async_carrier[0], + &async_carrier[0]._request)); + NODE_API_CALL( + env, napi_create_reference(env, argv[0], 1, &async_carrier[0]._callback)); + NODE_API_CALL(env, napi_queue_async_work(env, async_carrier[0]._request)); + NODE_API_CALL(env, napi_cancel_async_work(env, async_carrier[0]._request)); + return NULL; +} + +struct { + napi_ref ref; + napi_async_work work; +} repeated_work_info = {NULL, NULL}; + +static void RepeatedWorkerThread(napi_env env, void* data) {} + +static void RepeatedWorkComplete(napi_env env, napi_status status, void* data) { + napi_value cb, js_status; + NODE_API_CALL_RETURN_VOID( + env, napi_get_reference_value(env, repeated_work_info.ref, &cb)); + NODE_API_CALL_RETURN_VOID( + env, napi_delete_async_work(env, repeated_work_info.work)); + NODE_API_CALL_RETURN_VOID( + env, napi_delete_reference(env, repeated_work_info.ref)); + repeated_work_info.work = NULL; + repeated_work_info.ref = NULL; + NODE_API_CALL_RETURN_VOID( + env, napi_create_uint32(env, (uint32_t)status, &js_status)); + NODE_API_CALL_RETURN_VOID( + env, napi_call_function(env, cb, cb, 1, &js_status, NULL)); +} + +static napi_value DoRepeatedWork(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value cb, name; + NODE_API_ASSERT(env, + repeated_work_info.ref == NULL, + "Reference left over from previous work"); + NODE_API_ASSERT(env, + repeated_work_info.work == NULL, + "Work pointer left over from previous work"); + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, &cb, NULL, NULL)); + NODE_API_CALL( + env, napi_create_reference(env, cb, 1, &repeated_work_info.ref)); + NODE_API_CALL(env, + napi_create_string_utf8(env, "Repeated Work", NAPI_AUTO_LENGTH, &name)); + NODE_API_CALL(env, + napi_create_async_work(env, + NULL, + name, + RepeatedWorkerThread, + RepeatedWorkComplete, + &repeated_work_info, + &repeated_work_info.work)); + NODE_API_CALL(env, napi_queue_async_work(env, repeated_work_info.work)); + return NULL; +} + +static napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor properties[] = { + DECLARE_NODE_API_PROPERTY("Test", Test), + DECLARE_NODE_API_PROPERTY("TestCancel", TestCancel), + DECLARE_NODE_API_PROPERTY("DoRepeatedWork", DoRepeatedWork), + }; + + NODE_API_CALL(env, + napi_define_properties( + env, exports, sizeof(properties) / sizeof(*properties), properties)); + + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/packages/node-addon-examples/tests/async/addon.js b/packages/node-addon-examples/tests/async/addon.js new file mode 100644 index 00000000..f30d87d2 --- /dev/null +++ b/packages/node-addon-examples/tests/async/addon.js @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +/* eslint-disable no-undef */ +const assert = require("assert"); +const test_async = require("bindings")("addon.node"); + +const test = () => + new Promise((resolve, reject) => { + test_async.Test(5, {}, (err, val) => { + if (err) { + reject(err); + return; + } + try { + assert.strictEqual(err, null); + assert.strictEqual(val, 10); + } catch (e) { + reject(e); + } + resolve(); + }); + }); + +const testCancel = () => + new Promise((resolve) => { + test_async.TestCancel(() => resolve()); + }); + +const doRepeatedWork = (count = 0) => + new Promise((resolve, reject) => { + const iterations = 100; + const workDone = (status) => { + try { + assert.strictEqual(status, 0); + } catch (e) { + reject(e); + } + if (++count < iterations) { + test_async.DoRepeatedWork(workDone); + } else { + resolve(); + } + }; + test_async.DoRepeatedWork(workDone); + }); + +module.exports = () => { + return Promise.all([test(), testCancel(), doRepeatedWork()]); +}; diff --git a/packages/node-addon-examples/tests/async/binding.gyp b/packages/node-addon-examples/tests/async/binding.gyp new file mode 100644 index 00000000..80f9fa87 --- /dev/null +++ b/packages/node-addon-examples/tests/async/binding.gyp @@ -0,0 +1,8 @@ +{ + "targets": [ + { + "target_name": "addon", + "sources": [ "addon.c" ] + } + ] +} diff --git a/packages/node-addon-examples/tests/async/package.json b/packages/node-addon-examples/tests/async/package.json new file mode 100644 index 00000000..c8016da0 --- /dev/null +++ b/packages/node-addon-examples/tests/async/package.json @@ -0,0 +1,14 @@ +{ + "name": "async-test", + "version": "0.0.0", + "description": "Tests of runtime async functions", + "main": "addon.js", + "private": true, + "dependencies": { + "bindings": "~1.5.0" + }, + "scripts": { + "test": "node addon.js" + }, + "gypfile": true +} diff --git a/packages/node-addon-examples/tests/buffers/CMakeLists.txt b/packages/node-addon-examples/tests/buffers/CMakeLists.txt index 7de25130..8e8cc950 100644 --- a/packages/node-addon-examples/tests/buffers/CMakeLists.txt +++ b/packages/node-addon-examples/tests/buffers/CMakeLists.txt @@ -12,4 +12,4 @@ target_compile_features(addon PRIVATE cxx_std_17) if(MSVC AND CMAKE_JS_NODELIB_DEF AND CMAKE_JS_NODELIB_TARGET) # Generate node.lib execute_process(COMMAND ${CMAKE_AR} /def:${CMAKE_JS_NODELIB_DEF} /out:${CMAKE_JS_NODELIB_TARGET} ${CMAKE_STATIC_LINKER_FLAGS}) -endif() +endif() \ No newline at end of file diff --git a/packages/node-addon-examples/tests/buffers/addon.c b/packages/node-addon-examples/tests/buffers/addon.c index 2f83f681..024fc0d3 100644 --- a/packages/node-addon-examples/tests/buffers/addon.c +++ b/packages/node-addon-examples/tests/buffers/addon.c @@ -2,113 +2,7 @@ #include #include #include - -#define NODE_API_RETVAL_NOTHING // Intentionally blank #define - -#define GET_AND_THROW_LAST_ERROR(env) \ - do { \ - const napi_extended_error_info* error_info; \ - napi_get_last_error_info((env), &error_info); \ - bool is_pending; \ - const char* err_message = error_info->error_message; \ - napi_is_exception_pending((env), &is_pending); \ - /* If an exception is already pending, don't rethrow it */ \ - if (!is_pending) { \ - const char* error_message = \ - err_message != NULL ? err_message : "empty error message"; \ - napi_throw_error((env), NULL, error_message); \ - } \ - } while (0) - -// The basic version of GET_AND_THROW_LAST_ERROR. We cannot access any -// exceptions and we cannot fail by way of JS exception, so we abort. -#define FATALLY_FAIL_WITH_LAST_ERROR(env) \ - do { \ - const napi_extended_error_info* error_info; \ - napi_get_last_error_info((env), &error_info); \ - const char* err_message = error_info->error_message; \ - const char* error_message = \ - err_message != NULL ? err_message : "empty error message"; \ - fprintf(stderr, "%s\n", error_message); \ - abort(); \ - } while (0) - -#define NODE_API_ASSERT_BASE(env, assertion, message, ret_val) \ - do { \ - if (!(assertion)) { \ - napi_throw_error( \ - (env), NULL, "assertion (" #assertion ") failed: " message); \ - return ret_val; \ - } \ - } while (0) - -#define NODE_API_BASIC_ASSERT_BASE(assertion, message, ret_val) \ - do { \ - if (!(assertion)) { \ - fprintf(stderr, "assertion (" #assertion ") failed: " message); \ - abort(); \ - return ret_val; \ - } \ - } while (0) - -// Returns NULL on failed assertion. -// This is meant to be used inside napi_callback methods. -#define NODE_API_ASSERT(env, assertion, message) \ - NODE_API_ASSERT_BASE(env, assertion, message, NULL) - -// Returns empty on failed assertion. -// This is meant to be used inside functions with void return type. -#define NODE_API_ASSERT_RETURN_VOID(env, assertion, message) \ - NODE_API_ASSERT_BASE(env, assertion, message, NODE_API_RETVAL_NOTHING) - -#define NODE_API_BASIC_ASSERT_RETURN_VOID(assertion, message) \ - NODE_API_BASIC_ASSERT_BASE(assertion, message, NODE_API_RETVAL_NOTHING) - -#define NODE_API_CALL_BASE(env, the_call, ret_val) \ - do { \ - if ((the_call) != napi_ok) { \ - GET_AND_THROW_LAST_ERROR((env)); \ - return ret_val; \ - } \ - } while (0) - -#define NODE_API_BASIC_CALL_BASE(env, the_call, ret_val) \ - do { \ - if ((the_call) != napi_ok) { \ - FATALLY_FAIL_WITH_LAST_ERROR((env)); \ - return ret_val; \ - } \ - } while (0) - -// Returns NULL if the_call doesn't return napi_ok. -#define NODE_API_CALL(env, the_call) NODE_API_CALL_BASE(env, the_call, NULL) - -// Returns empty if the_call doesn't return napi_ok. -#define NODE_API_CALL_RETURN_VOID(env, the_call) \ - NODE_API_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) - -#define NODE_API_BASIC_CALL_RETURN_VOID(env, the_call) \ - NODE_API_BASIC_CALL_BASE(env, the_call, NODE_API_RETVAL_NOTHING) - -#define NODE_API_CHECK_STATUS(the_call) \ - do { \ - napi_status status = (the_call); \ - if (status != napi_ok) { \ - return status; \ - } \ - } while (0) - -#define NODE_API_ASSERT_STATUS(env, assertion, message) \ - NODE_API_ASSERT_BASE(env, assertion, message, napi_generic_failure) - -#define DECLARE_NODE_API_PROPERTY(name, func) \ - {(name), NULL, (func), NULL, NULL, NULL, napi_default, NULL} - -#define DECLARE_NODE_API_GETTER(name, func) \ - {(name), NULL, NULL, (func), NULL, NULL, napi_default, NULL} - -#define DECLARE_NODE_API_PROPERTY_VALUE(name, value) \ - {(name), NULL, NULL, NULL, NULL, (value), napi_default, NULL} +#include "../RuntimeNodeApiTestsCommon.h" static inline void add_returned_status(napi_env env, const char* key, diff --git a/packages/node-addon-examples/tests/buffers/addon.js b/packages/node-addon-examples/tests/buffers/addon.js index 2e73501f..a5ccd75f 100644 --- a/packages/node-addon-examples/tests/buffers/addon.js +++ b/packages/node-addon-examples/tests/buffers/addon.js @@ -1,19 +1,26 @@ /* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable no-undef */ +const assert = require("assert"); const addon = require("bindings")("addon.node"); const toLocaleString = (text) => { return text - .toLocaleString() + .toString() .split(",") .map((code) => String.fromCharCode(parseInt(code, 10))) .join(""); }; -console.log(addon.newBuffer().toLocaleString(), addon.theText); -console.log(toLocaleString(addon.newExternalBuffer()), addon.theText); -console.log(addon.copyBuffer(), addon.theText); -let buffer = addon.staticBuffer(); -console.log(addon.bufferHasInstance(buffer), true); -console.log(addon.bufferInfo(buffer), true); -addon.invalidObjectAsBuffer({}); +module.exports = () => { + assert.strictEqual(toLocaleString(addon.newBuffer()), addon.theText); + assert.strictEqual(toLocaleString(addon.newExternalBuffer()), addon.theText); + assert.strictEqual(toLocaleString(addon.copyBuffer()), addon.theText); + let buffer = addon.staticBuffer(); + assert.strictEqual(addon.bufferHasInstance(buffer), true); + assert.strictEqual(addon.bufferInfo(buffer), true); + addon.invalidObjectAsBuffer({}); + + // TODO: Add gc tests + // @see + // https://github.com/callstackincubator/react-native-node-api/issues/182 +};