fix(napi): Make napi_wrap work on regular objects (#15622)

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
190n
2024-12-16 15:54:39 -08:00
committed by GitHub
parent 9604733ee1
commit 4eae3a90e8
17 changed files with 942 additions and 110 deletions

View File

@@ -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<NapiPrototype*>(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<NapiRef*>(jsCast<Bun::NapiExternal*>(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<NapiPrototype*>(value)) {
refPtr = &val->napiRef;
} else if (auto* val = jsDynamicCast<NapiClass*>(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<NapiPrototype*>(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<NapiPrototype*>(jsc_object);
NapiRef** refPtr = nullptr;
if (auto* val = jsDynamicCast<NapiPrototype*>(value)) {
refPtr = &val->napiRef;
} else if (auto* val = jsDynamicCast<NapiClass*>(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<NapiPrototype*>(value)) {
ref = val->napiRef;
} else if (auto* val = jsDynamicCast<NapiClass*>(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;

View File

@@ -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 ---

Binary file not shown.

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -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));
});
});

View File

@@ -10,7 +10,7 @@
"AdditionalOptions": ["/std:c++20"],
},
},
"sources": ["main.cpp"],
"sources": ["main.cpp", "wrap_tests.cpp"],
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
"libraries": [],
"dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],
@@ -18,6 +18,17 @@
"NAPI_DISABLE_CPP_EXCEPTIONS",
"NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT=1",
],
}
},
{
"target_name": "second_addon",
"sources": ["second_addon.c"],
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
"libraries": [],
"dependencies": ["<!(node -p \"require('node-addon-api').gyp\")"],
"defines": [
"NAPI_DISABLE_CPP_EXCEPTIONS",
"NODE_API_EXPERIMENTAL_NOGC_ENV_OPT_OUT=1",
],
},
]
}

View File

@@ -1,6 +1,6 @@
#include <node.h>
#include <napi.h>
#include "napi_with_version.h"
#include "utils.h"
#include "wrap_tests.h"
#include <array>
#include <cassert>
@@ -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<Napi::Function>().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;
}

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
#pragma once
#define NAPI_EXPERIMENTAL
#include <napi.h>
#include <node.h>
// TODO(@190n): remove this when CI has Node 22.6
typedef struct napi_env__ *napi_env;
typedef napi_env node_api_basic_env;

View File

@@ -0,0 +1,53 @@
#include <js_native_api.h>
#include <node_api.h>
#include <stdio.h>
#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;
}

View File

@@ -0,0 +1,89 @@
#pragma once
#include "napi_with_version.h"
#include <climits>
// 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<Napi::Function>().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<napi_valuetype>(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";
}
}

View File

@@ -0,0 +1,232 @@
#include "wrap_tests.h"
#include "utils.h"
#include <cassert>
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<int *>(opaque_data);
int *hint = reinterpret_cast<int *>(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<int *>(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<int *>(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<void *>(new double{c_num}),
[](napi_env env, void *data, void *hint) {
(void)env;
(void)hint;
delete reinterpret_cast<double *>(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<void **>(&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<void **>(&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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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": {