From 4eae3a90e8e25833a2f09af43cdadeecda64d469 Mon Sep 17 00:00:00 2001 From: 190n Date: Mon, 16 Dec 2024 15:54:39 -0800 Subject: [PATCH] fix(napi): Make napi_wrap work on regular objects (#15622) Co-authored-by: Jarred Sumner --- src/bun.js/bindings/napi.cpp | 136 +++++----- src/js/builtins/BunBuiltinNames.h | 1 + test/bun.lockb | Bin 400730 -> 424434 bytes .../js/third_party/@napi-rs/canvas/.gitignore | 175 +++++++++++++ .../third_party/@napi-rs/canvas/expected.png | Bin 0 -> 5283 bytes .../@napi-rs/canvas/icon-small.png | Bin 0 -> 4965 bytes .../@napi-rs/canvas/napi-rs-canvas.test.ts | 25 ++ test/napi/napi-app/binding.gyp | 15 +- test/napi/napi-app/main.cpp | 52 +--- test/napi/napi-app/module.js | 220 +++++++++++++++++ test/napi/napi-app/napi_with_version.h | 8 + test/napi/napi-app/second_addon.c | 53 ++++ test/napi/napi-app/utils.h | 89 +++++++ test/napi/napi-app/wrap_tests.cpp | 232 ++++++++++++++++++ test/napi/napi-app/wrap_tests.h | 11 + test/napi/napi.test.ts | 30 +++ test/package.json | 5 +- 17 files changed, 942 insertions(+), 110 deletions(-) create mode 100644 test/js/third_party/@napi-rs/canvas/.gitignore create mode 100644 test/js/third_party/@napi-rs/canvas/expected.png create mode 100644 test/js/third_party/@napi-rs/canvas/icon-small.png create mode 100644 test/js/third_party/@napi-rs/canvas/napi-rs-canvas.test.ts create mode 100644 test/napi/napi-app/napi_with_version.h create mode 100644 test/napi/napi-app/second_addon.c create mode 100644 test/napi/napi-app/utils.h create mode 100644 test/napi/napi-app/wrap_tests.cpp create mode 100644 test/napi/napi-app/wrap_tests.h diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 0c8bdec56d..c06ac65170 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -1025,6 +1025,21 @@ extern "C" void napi_module_register(napi_module* mod) globalObject->m_pendingNapiModuleAndExports[1].set(vm, globalObject, object); } +static inline NapiRef* getWrapContentsIfExists(VM& vm, JSGlobalObject* globalObject, JSObject* object) +{ + if (auto* napi_instance = jsDynamicCast(object)) { + return napi_instance->napiRef; + } else { + JSValue contents = object->getDirect(vm, WebCore::builtinNames(vm).napiWrappedContentsPrivateName()); + if (contents.isEmpty()) { + return nullptr; + } else { + // jsCast asserts: we should not have stored anything but a NapiExternal here + return static_cast(jsCast(contents)->value()); + } + } +} + extern "C" napi_status napi_wrap(napi_env env, napi_value js_object, void* native_object, @@ -1039,50 +1054,46 @@ extern "C" napi_status napi_wrap(napi_env env, { NAPI_PREMABLE - JSValue value = toJS(js_object); - if (!value || value.isUndefinedOrNull()) { - return napi_object_expected; - } - auto* globalObject = toJS(env); - - NapiRef** refPtr = nullptr; - if (auto* val = jsDynamicCast(value)) { - refPtr = &val->napiRef; - } else if (auto* val = jsDynamicCast(value)) { - refPtr = &val->napiRef; + auto& vm = globalObject->vm(); + JSValue jsc_value = toJS(js_object); + if (jsc_value.isEmpty()) { + return napi_invalid_arg; } - - if (!refPtr) { + JSObject* jsc_object = jsc_value.getObject(); + if (!jsc_object) { return napi_object_expected; } - if (*refPtr) { - // Calling napi_wrap() a second time on an object will return an error. - // To associate another native instance with the object, use - // napi_remove_wrap() first. + // NapiPrototype has an inline field to store a napi_ref, so we use that if we can + auto* napi_instance = jsDynamicCast(jsc_object); + + const JSC::Identifier& propertyName = WebCore::builtinNames(vm).napiWrappedContentsPrivateName(); + + if (getWrapContentsIfExists(vm, globalObject, jsc_object)) { + // already wrapped return napi_invalid_arg; } + // create a new weak reference (refcount 0) auto* ref = new NapiRef(globalObject, 0); + ref->weakValueRef.set(jsc_value, weakValueHandleOwner(), ref); - ref->weakValueRef.set(value, weakValueHandleOwner(), ref); + ref->finalizer.finalize_cb = finalize_cb; + ref->finalizer.finalize_hint = finalize_hint; + ref->data = native_object; - if (finalize_cb) { - ref->finalizer.finalize_cb = finalize_cb; - ref->finalizer.finalize_hint = finalize_hint; + if (napi_instance) { + napi_instance->napiRef = ref; + } else { + // wrap the ref in an external so that it can serve as a JSValue + auto* external = Bun::NapiExternal::create(globalObject->vm(), globalObject->NapiExternalStructure(), ref, nullptr, nullptr); + jsc_object->putDirect(vm, propertyName, JSValue(external)); } - if (native_object) { - ref->data = native_object; - } - - *refPtr = ref; - if (result) { *result = toNapi(ref); } - return napi_ok; } @@ -1091,35 +1102,41 @@ extern "C" napi_status napi_remove_wrap(napi_env env, napi_value js_object, { NAPI_PREMABLE - JSValue value = toJS(js_object); - if (!value || value.isUndefinedOrNull()) { + JSValue jsc_value = toJS(js_object); + if (jsc_value.isEmpty()) { + return napi_invalid_arg; + } + JSObject* jsc_object = jsc_value.getObject(); + if (!js_object) { return napi_object_expected; } + // may be null + auto* napi_instance = jsDynamicCast(jsc_object); - NapiRef** refPtr = nullptr; - if (auto* val = jsDynamicCast(value)) { - refPtr = &val->napiRef; - } else if (auto* val = jsDynamicCast(value)) { - refPtr = &val->napiRef; + auto* globalObject = toJS(env); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + NapiRef* ref = getWrapContentsIfExists(vm, globalObject, jsc_object); + + if (!ref) { + return napi_invalid_arg; } - if (!refPtr) { - return napi_object_expected; + if (napi_instance) { + napi_instance->napiRef = nullptr; + } else { + const JSC::Identifier& propertyName = WebCore::builtinNames(vm).napiWrappedContentsPrivateName(); + jsc_object->deleteProperty(globalObject, propertyName); } - if (!(*refPtr)) { - // not sure if this should succeed or return an error - return napi_ok; - } - - auto* ref = *refPtr; - *refPtr = nullptr; - if (result) { *result = ref->data; } - delete ref; + ref->finalizer.finalize_cb = nullptr; + // don't delete the ref: if weak, it'll delete itself when the JS object is deleted; + // if strong, native addon needs to clean it up. + // the external is garbage collected. return napi_ok; } @@ -1128,23 +1145,24 @@ extern "C" napi_status napi_unwrap(napi_env env, napi_value js_object, { NAPI_PREMABLE - JSValue value = toJS(js_object); - - if (!value.isObject()) { - return NAPI_OBJECT_EXPECTED; + JSValue jsc_value = toJS(js_object); + if (jsc_value.isEmpty()) { + return napi_invalid_arg; + } + JSObject* jsc_object = jsc_value.getObject(); + if (!jsc_object) { + return napi_object_expected; } - NapiRef* ref = nullptr; - if (auto* val = jsDynamicCast(value)) { - ref = val->napiRef; - } else if (auto* val = jsDynamicCast(value)) { - ref = val->napiRef; - } else { - ASSERT(false); + auto* globalObject = toJS(env); + auto& vm = globalObject->vm(); + NapiRef* ref = getWrapContentsIfExists(vm, globalObject, jsc_object); + if (!ref) { + return napi_invalid_arg; } - if (ref && result) { - *result = ref ? ref->data : nullptr; + if (result) { + *result = ref->data; } return napi_ok; diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 0c04b13631..b3cb19b816 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -255,6 +255,7 @@ using namespace JSC; macro(writing) \ macro(written) \ macro(napiDlopenHandle) \ + macro(napiWrappedContents) \ BUN_ADDITIONAL_BUILTIN_NAMES(macro) // --- END of BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME --- diff --git a/test/bun.lockb b/test/bun.lockb index 592e2bd028cbec1c45bc4a767a5aa41dd3122432..fbf0da7978c74fb43117143efa38953611026ec6 100755 GIT binary patch delta 52597 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 z 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 diff --git a/test/js/third_party/@napi-rs/canvas/.gitignore b/test/js/third_party/@napi-rs/canvas/.gitignore new file mode 100644 index 0000000000..9b1ee42e84 --- /dev/null +++ b/test/js/third_party/@napi-rs/canvas/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/test/js/third_party/@napi-rs/canvas/expected.png b/test/js/third_party/@napi-rs/canvas/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..3a98dd5ee0d538a7f6e72b722971ea6974b8b188 GIT binary patch literal 5283 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 literal 0 HcmV?d00001 diff --git a/test/js/third_party/@napi-rs/canvas/icon-small.png b/test/js/third_party/@napi-rs/canvas/icon-small.png new file mode 100644 index 0000000000000000000000000000000000000000..69bc385e8ca9947e54df56921d113487a205841d GIT binary patch literal 4965 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 literal 0 HcmV?d00001 diff --git a/test/js/third_party/@napi-rs/canvas/napi-rs-canvas.test.ts b/test/js/third_party/@napi-rs/canvas/napi-rs-canvas.test.ts new file mode 100644 index 0000000000..b03b58df0e --- /dev/null +++ b/test/js/third_party/@napi-rs/canvas/napi-rs-canvas.test.ts @@ -0,0 +1,25 @@ +// Create an image, then print it as binary to stdout +import { createCanvas, loadImage } from "@napi-rs/canvas"; +import { Jimp } from "jimp"; +import { join } from "path"; + +describe("@napi-rs/canvas", () => { + it("produces correct output", async () => { + const canvas = createCanvas(200, 200); + const ctx = canvas.getContext("2d"); + + ctx.lineWidth = 10; + ctx.strokeStyle = "red"; + ctx.fillStyle = "blue"; + + ctx.fillRect(0, 0, 200, 200); + ctx.strokeRect(50, 50, 100, 100); + + const image = await loadImage(join(__dirname, "icon-small.png")); + ctx.drawImage(image, 0, 0); + + const expected = await Jimp.read(join(__dirname, "expected.png")); + const actual = await Jimp.read(await canvas.encode("png")); + expect(Array.from(actual.bitmap.data)).toEqual(Array.from(expected.bitmap.data)); + }); +}); diff --git a/test/napi/napi-app/binding.gyp b/test/napi/napi-app/binding.gyp index aebdebb7ef..39b61162a2 100644 --- a/test/napi/napi-app/binding.gyp +++ b/test/napi/napi-app/binding.gyp @@ -10,7 +10,7 @@ "AdditionalOptions": ["/std:c++20"], }, }, - "sources": ["main.cpp"], + "sources": ["main.cpp", "wrap_tests.cpp"], "include_dirs": [" - -#include +#include "napi_with_version.h" +#include "utils.h" +#include "wrap_tests.h" #include #include @@ -30,23 +30,6 @@ napi_value fail_fmt(napi_env env, const char *fmt, ...) { return fail(env, buf); } -napi_value ok(napi_env env) { - napi_value result; - napi_get_undefined(env, &result); - return result; -} - -static void run_gc(const Napi::CallbackInfo &info) { - info[0].As().Call(0, nullptr); -} - -// calls napi_typeof and asserts it returns napi_ok -static napi_valuetype get_typeof(napi_env env, napi_value value) { - napi_valuetype result; - assert(napi_typeof(env, value, &result) == napi_ok); - return result; -} - napi_value test_issue_7685(const Napi::CallbackInfo &info) { Napi::Env env(info.Env()); Napi::HandleScope scope(env); @@ -595,33 +578,6 @@ napi_value was_finalize_called(const Napi::CallbackInfo &info) { return ret; } -static const char *napi_valuetype_to_string(napi_valuetype type) { - switch (type) { - case napi_undefined: - return "undefined"; - case napi_null: - return "null"; - case napi_boolean: - return "boolean"; - case napi_number: - return "number"; - case napi_string: - return "string"; - case napi_symbol: - return "symbol"; - case napi_object: - return "object"; - case napi_function: - return "function"; - case napi_external: - return "external"; - case napi_bigint: - return "bigint"; - default: - return "unknown"; - } -} - // calls a function (the sole argument) which must throw. catches and returns // the thrown error napi_value call_and_get_exception(const Napi::CallbackInfo &info) { @@ -1080,6 +1036,8 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports1) { exports.Set("try_add_tag", Napi::Function::New(env, try_add_tag)); exports.Set("check_tag", Napi::Function::New(env, check_tag)); + napitests::register_wrap_tests(env, exports); + return exports; } diff --git a/test/napi/napi-app/module.js b/test/napi/napi-app/module.js index 6cb1280cf2..307e9ee9f6 100644 --- a/test/napi/napi-app/module.js +++ b/test/napi/napi-app/module.js @@ -1,4 +1,30 @@ const nativeTests = require("./build/Release/napitests.node"); +const secondAddon = require("./build/Release/second_addon.node"); + +function assert(ok) { + if (!ok) { + throw new Error("assertion failed"); + } +} + +async function gcUntil(fn) { + const MAX = 100; + for (let i = 0; i < MAX; i++) { + await new Promise(resolve => { + setTimeout(resolve, 1); + }); + if (typeof Bun == "object") { + Bun.gc(true); + } else { + // if this fails, you need to pass --expose-gc to node + global.gc(); + } + if (fn()) { + return; + } + } + throw new Error(`Condition was not met after ${MAX} GC attempts`); +} nativeTests.test_napi_class_constructor_handle_scope = () => { const NapiClass = nativeTests.get_class_with_constructor(); @@ -270,4 +296,198 @@ nativeTests.test_type_tag = () => { console.log("o2 matches o2:", nativeTests.check_tag(o2, 3, 4)); }; +nativeTests.test_napi_wrap = () => { + const values = [ + {}, + {}, // should be able to be wrapped differently than the distinct empty object above + 5, + new Number(5), + "abc", + new String("abc"), + null, + Symbol("abc"), + Symbol.for("abc"), + new (nativeTests.get_class_with_constructor())(), + new Proxy( + {}, + Object.fromEntries( + [ + "apply", + "construct", + "defineProperty", + "deleteProperty", + "get", + "getOwnPropertyDescriptor", + "getPrototypeOf", + "has", + "isExtensible", + "ownKeys", + "preventExtensions", + "set", + "setPrototypeOf", + ].map(name => [ + name, + () => { + throw new Error("oops"); + }, + ]), + ), + ), + ]; + const wrapSuccess = Array(values.length).fill(false); + for (const [i, v] of values.entries()) { + wrapSuccess[i] = nativeTests.try_wrap(v, i + 1); + console.log(`${typeof v} did wrap: `, wrapSuccess[i]); + } + + for (const [i, v] of values.entries()) { + if (wrapSuccess[i]) { + if (nativeTests.try_unwrap(v) !== i + 1) { + throw new Error("could not unwrap same value"); + } + } else { + if (nativeTests.try_unwrap(v) !== undefined) { + throw new Error("value unwraps without being successfully wrapped"); + } + } + } +}; + +nativeTests.test_napi_wrap_proxy = () => { + const target = {}; + const proxy = new Proxy(target, {}); + assert(nativeTests.try_wrap(target, 5)); + assert(nativeTests.try_wrap(proxy, 6)); + console.log(nativeTests.try_unwrap(target), nativeTests.try_unwrap(proxy)); +}; + +nativeTests.test_napi_wrap_cross_addon = () => { + const wrapped = {}; + console.log("wrap succeeds:", nativeTests.try_wrap(wrapped, 42)); + console.log("unwrapped from other addon", secondAddon.try_unwrap(wrapped)); +}; + +nativeTests.test_napi_wrap_prototype = () => { + class Foo {} + console.log("wrap prototype succeeds:", nativeTests.try_wrap(Foo.prototype, 42)); + // wrapping should not look at prototype chain + console.log("unwrap instance:", nativeTests.try_unwrap(new Foo())); +}; + +nativeTests.test_napi_remove_wrap = () => { + const targets = [{}, new (nativeTests.get_class_with_constructor())()]; + for (const t of targets) { + const target = {}; + // fails + assert(nativeTests.try_remove_wrap(target) === undefined); + // wrap it + assert(nativeTests.try_wrap(target, 5)); + // remove yields the wrapped value + assert(nativeTests.try_remove_wrap(target) === 5); + // neither remove nor unwrap work anymore + assert(nativeTests.try_unwrap(target) === undefined); + assert(nativeTests.try_remove_wrap(target) === undefined); + // can re-wrap + assert(nativeTests.try_wrap(target, 6)); + assert(nativeTests.try_unwrap(target) === 6); + } +}; + +// parameters to create_wrap are: object, ask_for_ref, strong +const createWrapWithoutRef = o => nativeTests.create_wrap(o, false, false); +const createWrapWithWeakRef = o => nativeTests.create_wrap(o, true, false); +const createWrapWithStrongRef = o => nativeTests.create_wrap(o, true, true); + +nativeTests.test_wrap_lifetime_without_ref = async () => { + let object = { foo: "bar" }; + assert(createWrapWithoutRef(object) === object); + assert(nativeTests.get_wrap_data(object) === 42); + object = undefined; + await gcUntil(() => nativeTests.was_wrap_finalize_called()); +}; + +nativeTests.test_wrap_lifetime_with_weak_ref = async () => { + // this looks the same as test_wrap_lifetime_without_ref because it is -- these cases should behave the same + let object = { foo: "bar" }; + assert(createWrapWithWeakRef(object) === object); + assert(nativeTests.get_wrap_data(object) === 42); + object = undefined; + await gcUntil(() => nativeTests.was_wrap_finalize_called()); +}; + +nativeTests.test_wrap_lifetime_with_strong_ref = async () => { + let object = { foo: "bar" }; + assert(createWrapWithStrongRef(object) === object); + assert(nativeTests.get_wrap_data(object) === 42); + + object = undefined; + // still referenced by native module so this should fail + try { + await gcUntil(() => nativeTests.was_wrap_finalize_called()); + throw new Error("object was garbage collected while still referenced by native code"); + } catch (e) { + if (!e.toString().includes("Condition was not met")) { + throw e; + } + } + + // can still get the value using the ref + assert(nativeTests.get_wrap_data_from_ref() === 42); + + // now we free it + nativeTests.unref_wrapped_value(); + await gcUntil(() => nativeTests.was_wrap_finalize_called()); +}; + +nativeTests.test_remove_wrap_lifetime_with_weak_ref = async () => { + let object = { foo: "bar" }; + assert(createWrapWithWeakRef(object) === object); + + assert(nativeTests.get_wrap_data(object) === 42); + + nativeTests.remove_wrap(object); + assert(nativeTests.get_wrap_data(object) === undefined); + assert(nativeTests.get_wrap_data_from_ref() === undefined); + assert(nativeTests.get_object_from_ref() === object); + + object = undefined; + + // ref will stop working once the object is collected + await gcUntil(() => nativeTests.get_object_from_ref() === undefined); + + // finalizer shouldn't have been called + assert(nativeTests.was_wrap_finalize_called() === false); +}; + +nativeTests.test_remove_wrap_lifetime_with_strong_ref = async () => { + let object = { foo: "bar" }; + assert(createWrapWithStrongRef(object) === object); + + assert(nativeTests.get_wrap_data(object) === 42); + + nativeTests.remove_wrap(object); + assert(nativeTests.get_wrap_data(object) === undefined); + assert(nativeTests.get_wrap_data_from_ref() === undefined); + assert(nativeTests.get_object_from_ref() === object); + + object = undefined; + + // finalizer should not be called and object should not be freed + try { + await gcUntil(() => nativeTests.was_wrap_finalize_called() || nativeTests.get_object_from_ref() === undefined); + throw new Error("finalizer ran"); + } catch (e) { + if (!e.toString().includes("Condition was not met")) { + throw e; + } + } + + // native code can still get the object + assert(JSON.stringify(nativeTests.get_object_from_ref()) === `{"foo":"bar"}`); + + // now it gets deleted + nativeTests.unref_wrapped_value(); + await gcUntil(() => nativeTests.get_object_from_ref() === undefined); +}; + module.exports = nativeTests; diff --git a/test/napi/napi-app/napi_with_version.h b/test/napi/napi-app/napi_with_version.h new file mode 100644 index 0000000000..f852184087 --- /dev/null +++ b/test/napi/napi-app/napi_with_version.h @@ -0,0 +1,8 @@ +#pragma once +#define NAPI_EXPERIMENTAL +#include +#include + +// TODO(@190n): remove this when CI has Node 22.6 +typedef struct napi_env__ *napi_env; +typedef napi_env node_api_basic_env; diff --git a/test/napi/napi-app/second_addon.c b/test/napi/napi-app/second_addon.c new file mode 100644 index 0000000000..85232861dd --- /dev/null +++ b/test/napi/napi-app/second_addon.c @@ -0,0 +1,53 @@ +#include +#include +#include + +#define NODE_API_CALL(env, call) \ + do { \ + napi_status status = (call); \ + if (status != napi_ok) { \ + const napi_extended_error_info *error_info = NULL; \ + napi_get_last_error_info((env), &error_info); \ + const char *err_message = error_info->error_message; \ + bool is_pending; \ + napi_is_exception_pending((env), &is_pending); \ + /* If an exception is already pending, don't rethrow it */ \ + if (!is_pending) { \ + const char *message = \ + (err_message == NULL) ? "empty error message" : err_message; \ + napi_throw_error((env), NULL, message); \ + } \ + return NULL; \ + } \ + } while (0) + +static napi_value try_unwrap(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value argv[1]; + NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + if (argc != 1) { + napi_throw_error(env, NULL, "Wrong number of arguments to try_unwrap"); + return NULL; + } + + double *pointer; + if (napi_unwrap(env, argv[0], (void **)(&pointer)) != napi_ok) { + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; + } else { + napi_value number; + NODE_API_CALL(env, napi_create_double(env, *pointer, &number)); + return number; + } +} + +/* napi_value */ NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) { + napi_value try_unwrap_function; + NODE_API_CALL(env, + napi_create_function(env, "try_unwrap", NAPI_AUTO_LENGTH, + try_unwrap, NULL, &try_unwrap_function)); + NODE_API_CALL(env, napi_set_named_property(env, exports, "try_unwrap", + try_unwrap_function)); + return exports; +} diff --git a/test/napi/napi-app/utils.h b/test/napi/napi-app/utils.h new file mode 100644 index 0000000000..92e158e6b7 --- /dev/null +++ b/test/napi/napi-app/utils.h @@ -0,0 +1,89 @@ +#pragma once +#include "napi_with_version.h" +#include + +// e.g NODE_API_CALL(env, napi_create_int32(env, 5, &my_napi_integer)) +#define NODE_API_CALL(env, call) NODE_API_CALL_CUSTOM_RETURN(env, NULL, call) + +// Version of NODE_API_CALL for functions not returning napi_value +#define NODE_API_CALL_CUSTOM_RETURN(env, value_to_return_if_threw, call) \ + NODE_API_ASSERT_CUSTOM_RETURN(env, value_to_return_if_threw, \ + (call) == napi_ok) + +// Throw an error in the given napi_env and return if expr is false +#define NODE_API_ASSERT(env, expr) \ + NODE_API_ASSERT_CUSTOM_RETURN(env, NULL, expr) + +#ifdef _MSC_VER +#define CURRENT_FUNCTION_NAME __FUNCSIG__ +#else +#define CURRENT_FUNCTION_NAME __PRETTY_FUNCTION__ +#endif + +// Version of NODE_API_ASSERT for functions not returning napi_value +#define NODE_API_ASSERT_CUSTOM_RETURN(ENV, VALUE_TO_RETURN_IF_THREW, EXPR) \ + do { \ + if (!(EXPR)) { \ + bool is_pending; \ + napi_is_exception_pending((ENV), &is_pending); \ + /* If an exception is already pending, don't rethrow it */ \ + if (!is_pending) { \ + char buf[4096] = {0}; \ + snprintf(buf, sizeof(buf) - 1, "%s (%s:%d): Assertion failed: %s", \ + CURRENT_FUNCTION_NAME, __FILE__, __LINE__, #EXPR); \ + napi_throw_error((ENV), NULL, buf); \ + } \ + return (VALUE_TO_RETURN_IF_THREW); \ + } \ + } while (0) + +#define REGISTER_FUNCTION(ENV, EXPORTS, FUNCTION) \ + EXPORTS.Set(#FUNCTION, Napi::Function::New(ENV, FUNCTION)) + +static inline napi_value ok(napi_env env) { + napi_value result; + napi_get_undefined(env, &result); + return result; +} + +// For functions that take a garbage collection callback as the first argument +// (functions not called directly by module.js), use this to trigger GC +static inline void run_gc(const Napi::CallbackInfo &info) { + info[0].As().Call(0, nullptr); +} + +// calls napi_typeof and asserts it returns napi_ok +static inline napi_valuetype get_typeof(napi_env env, napi_value value) { + napi_valuetype result; + // return an invalid napi_valuetype if the call to napi_typeof fails + NODE_API_CALL_CUSTOM_RETURN(env, static_cast(INT_MAX), + napi_typeof(env, value, &result)); + return result; +} + +static inline const char *napi_valuetype_to_string(napi_valuetype type) { + switch (type) { + case napi_undefined: + return "undefined"; + case napi_null: + return "null"; + case napi_boolean: + return "boolean"; + case napi_number: + return "number"; + case napi_string: + return "string"; + case napi_symbol: + return "symbol"; + case napi_object: + return "object"; + case napi_function: + return "function"; + case napi_external: + return "external"; + case napi_bigint: + return "bigint"; + default: + return "unknown"; + } +} diff --git a/test/napi/napi-app/wrap_tests.cpp b/test/napi/napi-app/wrap_tests.cpp new file mode 100644 index 0000000000..5365a29e89 --- /dev/null +++ b/test/napi/napi-app/wrap_tests.cpp @@ -0,0 +1,232 @@ +#include "wrap_tests.h" + +#include "utils.h" +#include + +namespace napitests { + +static napi_ref ref_to_wrapped_object = nullptr; +static bool wrap_finalize_called = false; + +// static void delete_the_ref(napi_env env, void *_data, void *_hint) { +// printf("delete_the_ref\n"); +// // not using NODE_API_ASSERT as this runs in a finalizer where allocating +// an +// // error might cause a harder-to-debug crash +// assert(ref_to_wrapped_object); +// napi_delete_reference(env, ref_to_wrapped_object); +// ref_to_wrapped_object = nullptr; +// } + +static void finalize_for_create_wrap(napi_env env, void *opaque_data, + void *opaque_hint) { + int *data = reinterpret_cast(opaque_data); + int *hint = reinterpret_cast(opaque_hint); + printf("finalize_for_create_wrap, data = %d, hint = %d\n", *data, *hint); + delete data; + delete hint; + // TODO: this needs https://github.com/oven-sh/bun/pulls/14501 to work + // if (ref_to_wrapped_object) { + // node_api_post_finalizer(env, delete_the_ref, nullptr, nullptr); + // } + wrap_finalize_called = true; +} + +// create_wrap(js_object: object, ask_for_ref: boolean, strong: boolean): object +static napi_value create_wrap(const Napi::CallbackInfo &info) { + wrap_finalize_called = false; + napi_env env = info.Env(); + napi_value js_object = info[0]; + + napi_value js_ask_for_ref = info[1]; + bool ask_for_ref; + NODE_API_CALL(env, napi_get_value_bool(env, js_ask_for_ref, &ask_for_ref)); + napi_value js_strong = info[2]; + bool strong; + NODE_API_CALL(env, napi_get_value_bool(env, js_strong, &strong)); + + // wrap it + int *wrap_data = new int(42); + int *wrap_hint = new int(123); + + NODE_API_CALL(env, napi_wrap(env, js_object, wrap_data, + finalize_for_create_wrap, wrap_hint, + ask_for_ref ? &ref_to_wrapped_object : nullptr)); + if (ask_for_ref && strong) { + uint32_t new_refcount; + NODE_API_CALL( + env, napi_reference_ref(env, ref_to_wrapped_object, &new_refcount)); + NODE_API_ASSERT(env, new_refcount == 1); + } + + if (!ask_for_ref) { + ref_to_wrapped_object = nullptr; + } + + return js_object; +} + +// get_wrap_data(js_object: object): number +static napi_value get_wrap_data(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value js_object = info[0]; + + void *wrapped_data; + napi_status status = napi_unwrap(env, js_object, &wrapped_data); + if (status != napi_ok) { + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; + } + + napi_value js_number; + NODE_API_CALL(env, + napi_create_int32(env, *reinterpret_cast(wrapped_data), + &js_number)); + return js_number; +} + +// get_object_from_ref(): object +static napi_value get_object_from_ref(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value wrapped_object; + NODE_API_CALL(env, napi_get_reference_value(env, ref_to_wrapped_object, + &wrapped_object)); + + if (!wrapped_object) { + NODE_API_CALL(env, napi_get_undefined(env, &wrapped_object)); + } + return wrapped_object; +} + +// get_wrap_data_from_ref(): number|undefined +static napi_value get_wrap_data_from_ref(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value wrapped_object; + NODE_API_CALL(env, napi_get_reference_value(env, ref_to_wrapped_object, + &wrapped_object)); + + void *wrapped_data; + napi_status status = napi_unwrap(env, wrapped_object, &wrapped_data); + if (status == napi_ok) { + napi_value js_number; + NODE_API_CALL(env, + napi_create_int32(env, *reinterpret_cast(wrapped_data), + &js_number)); + return js_number; + } else if (status == napi_invalid_arg) { + // no longer wrapped + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; + } else { + NODE_API_ASSERT(env, false && "this should not be reached"); + return nullptr; + } +} + +// remove_wrap_data(js_object: object): undefined +static napi_value remove_wrap(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value js_object = info[0]; + + void *wrap_data; + NODE_API_CALL(env, napi_remove_wrap(env, js_object, &wrap_data)); + + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; +} + +// unref_wrapped_value(): undefined +static napi_value unref_wrapped_value(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + uint32_t new_refcount; + NODE_API_CALL( + env, napi_reference_unref(env, ref_to_wrapped_object, &new_refcount)); + // should never have been set higher than 1 + NODE_API_ASSERT(env, new_refcount == 0); + + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; +} + +static napi_value was_wrap_finalize_called(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + return Napi::Boolean::New(env, wrap_finalize_called); +} + +// try_wrap(value: any, num: number): bool +// wraps value in a C++ object corresponding to the number num +// true if success +static napi_value try_wrap(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value value = info[0]; + napi_value js_num = info[1]; + double c_num; + NODE_API_CALL(env, napi_get_value_double(env, js_num, &c_num)); + + napi_status status = napi_wrap( + env, value, reinterpret_cast(new double{c_num}), + [](napi_env env, void *data, void *hint) { + (void)env; + (void)hint; + delete reinterpret_cast(data); + }, + nullptr, nullptr); + + napi_value js_result; + assert(napi_get_boolean(env, status == napi_ok, &js_result) == napi_ok); + return js_result; +} + +// try_unwrap(any): number|undefined +static napi_value try_unwrap(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value value = info[0]; + + double *wrapped; + napi_status status = + napi_unwrap(env, value, reinterpret_cast(&wrapped)); + napi_value result; + if (status == napi_ok) { + NODE_API_CALL(env, napi_create_double(env, *wrapped, &result)); + } else { + NODE_API_CALL(env, napi_get_undefined(env, &result)); + } + return result; +} + +static napi_value try_remove_wrap(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value value = info[0]; + + double *wrapped; + napi_status status = + napi_remove_wrap(env, value, reinterpret_cast(&wrapped)); + napi_value result; + if (status == napi_ok) { + NODE_API_CALL(env, napi_create_double(env, *wrapped, &result)); + } else { + NODE_API_CALL(env, napi_get_undefined(env, &result)); + } + return result; +} + +void register_wrap_tests(Napi::Env env, Napi::Object exports) { + REGISTER_FUNCTION(env, exports, create_wrap); + REGISTER_FUNCTION(env, exports, get_wrap_data); + REGISTER_FUNCTION(env, exports, get_object_from_ref); + REGISTER_FUNCTION(env, exports, get_wrap_data_from_ref); + REGISTER_FUNCTION(env, exports, remove_wrap); + REGISTER_FUNCTION(env, exports, unref_wrapped_value); + REGISTER_FUNCTION(env, exports, was_wrap_finalize_called); + REGISTER_FUNCTION(env, exports, try_wrap); + REGISTER_FUNCTION(env, exports, try_unwrap); + REGISTER_FUNCTION(env, exports, try_remove_wrap); +} + +} // namespace napitests diff --git a/test/napi/napi-app/wrap_tests.h b/test/napi/napi-app/wrap_tests.h new file mode 100644 index 0000000000..a70a44240e --- /dev/null +++ b/test/napi/napi-app/wrap_tests.h @@ -0,0 +1,11 @@ +#pragma once + +// Helper functions used by JS to test napi_wrap + +#include "napi_with_version.h" + +namespace napitests { + +void register_wrap_tests(Napi::Env env, Napi::Object exports); + +} // namespace napitests diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index 25144c80f7..18a53af2a7 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -319,6 +319,36 @@ describe("napi", () => { checkSameOutput("test_type_tag", []); }); }); + + describe("napi_wrap", () => { + it("accepts the right kinds of values", () => { + checkSameOutput("test_napi_wrap", []); + }); + + it("is shared between addons", () => { + checkSameOutput("test_napi_wrap_cross_addon", []); + }); + + it("does not follow prototypes", () => { + checkSameOutput("test_napi_wrap_prototype", []); + }); + + it("does not consider proxies", () => { + checkSameOutput("test_napi_wrap_proxy", []); + }); + + it("can remove a wrap", () => { + checkSameOutput("test_napi_remove_wrap", []); + }); + + it("has the right lifetime", () => { + checkSameOutput("test_wrap_lifetime_without_ref", []); + checkSameOutput("test_wrap_lifetime_with_weak_ref", []); + checkSameOutput("test_wrap_lifetime_with_strong_ref", []); + checkSameOutput("test_remove_wrap_lifetime_with_weak_ref", []); + checkSameOutput("test_remove_wrap_lifetime_with_strong_ref", []); + }); + }); }); function checkSameOutput(test: string, args: any[] | string) { diff --git a/test/package.json b/test/package.json index 5fc461dc32..56bcb98c72 100644 --- a/test/package.json +++ b/test/package.json @@ -12,7 +12,7 @@ "@azure/service-bus": "7.9.4", "@grpc/grpc-js": "1.12.0", "@grpc/proto-loader": "0.7.10", - "@napi-rs/canvas": "0.1.47", + "@napi-rs/canvas": "0.1.65", "@prisma/client": "5.8.0", "@remix-run/react": "2.10.3", "@remix-run/serve": "2.10.3", @@ -69,7 +69,8 @@ "webpack": "5.88.0", "webpack-cli": "4.7.2", "xml2js": "0.6.2", - "yargs": "17.7.2" + "yargs": "17.7.2", + "jimp": "1.6.0" }, "private": true, "scripts": {