diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 0c8bdec56da47a..c06ac65170e75d 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 (native_object) { - ref->data = native_object; + 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)); } - *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 (!refPtr) { - return napi_object_expected; + if (!ref) { + return napi_invalid_arg; } - if (!(*refPtr)) { - // not sure if this should succeed or return an error - return napi_ok; + if (napi_instance) { + napi_instance->napiRef = nullptr; + } else { + const JSC::Identifier& propertyName = WebCore::builtinNames(vm).napiWrappedContentsPrivateName(); + jsc_object->deleteProperty(globalObject, propertyName); } - 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 0c04b13631d9fe..b3cb19b8164aba 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 592e2bd028cbec..fbf0da7978c74f 100755 Binary files a/test/bun.lockb and b/test/bun.lockb differ 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 00000000000000..9b1ee42e8482ee --- /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 00000000000000..3a98dd5ee0d538 Binary files /dev/null and b/test/js/third_party/@napi-rs/canvas/expected.png differ 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 00000000000000..69bc385e8ca994 Binary files /dev/null and b/test/js/third_party/@napi-rs/canvas/icon-small.png differ 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 00000000000000..aa29021f9d7a01 --- /dev/null +++ b/test/js/third_party/@napi-rs/canvas/napi-rs-canvas.test.ts @@ -0,0 +1,24 @@ +// Create an image, then print it as binary to stdout +import { createCanvas, loadImage } from "@napi-rs/canvas"; +import { Jimp } from "jimp"; + +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("icon-small.png"); + ctx.drawImage(image, 0, 0); + + const expected = await Jimp.read("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 aebdebb7efaace..39b61162a28755 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 6cb1280cf2f494..307e9ee9f64a70 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 00000000000000..f8521840873709 --- /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 00000000000000..85232861dd2819 --- /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 00000000000000..92e158e6b70c5a --- /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 00000000000000..5365a29e8961a1 --- /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 00000000000000..a70a44240ef383 --- /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 25144c80f7da01..18a53af2a7b60e 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 5fc461dc3239cd..56bcb98c722ef5 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": {