From efabdcbe1f0897f0b18e3ea1d785d63c9eddd547 Mon Sep 17 00:00:00 2001 From: 190n Date: Wed, 26 Feb 2025 22:11:42 -0800 Subject: [PATCH] Start fixing bugs discovered by Node.js's Node-API tests (#14501) Co-authored-by: Kai Tamkun Co-authored-by: Jarred Sumner Co-authored-by: Ashcon Partovi Co-authored-by: Ciro Spaciari Co-authored-by: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Co-authored-by: 190n <190n@users.noreply.github.com> --- cmake/tools/SetupZig.cmake | 2 +- scripts/runner.node.mjs | 4 + src/bun.js/api/FFI.h | 15 +- src/bun.js/api/ffi.zig | 153 +- src/bun.js/bindings/BunProcess.cpp | 141 +- src/bun.js/bindings/ZigGlobalObject.cpp | 70 +- src/bun.js/bindings/ZigGlobalObject.h | 26 +- src/bun.js/bindings/bindings.cpp | 21 +- src/bun.js/bindings/bindings.zig | 13 +- src/bun.js/bindings/napi.cpp | 1644 ++++++++++------- src/bun.js/bindings/napi.h | 451 ++++- src/bun.js/bindings/napi_external.cpp | 7 +- src/bun.js/bindings/napi_external.h | 19 +- src/bun.js/bindings/napi_finalizer.cpp | 26 + src/bun.js/bindings/napi_finalizer.h | 31 + src/bun.js/bindings/napi_handle_scope.cpp | 14 +- src/bun.js/bindings/napi_handle_scope.h | 8 +- src/bun.js/bindings/napi_macros.h | 30 + src/bun.js/bindings/v8/V8External.cpp | 3 +- .../bindings/v8/shim/FunctionTemplate.cpp | 1 + .../bindings/webcore/JSEventTargetCustom.cpp | 2 +- .../webcore/MessagePortChannelProvider.cpp | 2 +- src/bun.js/bindings/webcore/Worker.cpp | 8 +- src/bun.js/event_loop.zig | 5 + src/bun.js/javascript.zig | 15 +- src/bun.js/javascript_core_c_api.zig | 3 + src/bun.js/web_worker.zig | 1 + src/crash_handler.zig | 14 +- src/napi/napi.zig | 804 ++++---- src/symbols.def | 7 +- src/symbols.dyn | 6 + src/symbols.txt | 6 + src/sync.zig | 6 +- test/bundler/native_plugin.cc | 12 +- test/js/node/dns/node-dns.test.js | 4 +- test/js/third_party/prisma/prisma.test.ts | 45 + test/napi/napi-app/.clangd | 2 + test/napi/napi-app/async_finalize_addon.c | 72 + test/napi/napi-app/async_tests.cpp | 194 ++ test/napi/napi-app/async_tests.h | 11 + test/napi/napi-app/binding.gyp | 36 +- test/napi/napi-app/bun.lock | 5 + test/napi/napi-app/class_test.cpp | 171 ++ test/napi/napi-app/class_test.h | 12 + test/napi/napi-app/conversion_tests.cpp | 172 ++ test/napi/napi-app/conversion_tests.h | 12 + test/napi/napi-app/ffi_addon_1.c | 59 + test/napi/napi-app/ffi_addon_2.c | 3 + test/napi/napi-app/js_test_helpers.cpp | 338 ++++ test/napi/napi-app/js_test_helpers.h | 13 + test/napi/napi-app/leak-fixture.js | 138 ++ test/napi/napi-app/leak_tests.cpp | 193 ++ test/napi/napi-app/leak_tests.h | 12 + test/napi/napi-app/main.cpp | 1218 +----------- test/napi/napi-app/main.js | 1 - test/napi/napi-app/module.js | 133 +- test/napi/napi-app/package.json | 6 +- test/napi/napi-app/standalone_tests.cpp | 537 ++++++ test/napi/napi-app/standalone_tests.h | 11 + test/napi/napi-app/wrap_tests.cpp | 29 +- test/napi/napi-value-ffi.test.ts | 89 + test/napi/napi.test.ts | 38 +- test/napi/node-napi-tests/.gitignore | 1 + test/napi/node-napi-tests/README.md | 5 + .../deps/v8/test/mjsunit/instanceof-2.js | 329 ++++ .../deps/v8/test/mjsunit/instanceof.js | 93 + test/napi/node-napi-tests/test/README.md | 55 + .../node-napi-tests/test/common/README.md | 1179 ++++++++++++ .../test/common/arraystream.js | 23 + .../test/common/assertSnapshot.js | 104 ++ .../node-napi-tests/test/common/benchmark.js | 54 + .../test/common/child_process.js | 156 ++ .../node-napi-tests/test/common/countdown.js | 28 + .../node-napi-tests/test/common/cpu-prof.js | 50 + .../node-napi-tests/test/common/crypto.js | 114 ++ .../node-napi-tests/test/common/debugger.js | 183 ++ test/napi/node-napi-tests/test/common/dns.js | 341 ++++ .../node-napi-tests/test/common/fixtures.js | 59 + .../node-napi-tests/test/common/fixtures.mjs | 5 + test/napi/node-napi-tests/test/common/gc.js | 192 ++ .../node-napi-tests/test/common/globals.js | 146 ++ test/napi/node-napi-tests/test/common/heap.js | 329 ++++ .../test/common/hijackstdio.js | 32 + .../napi/node-napi-tests/test/common/http2.js | 129 ++ .../napi/node-napi-tests/test/common/index.js | 1210 ++++++++++++ .../node-napi-tests/test/common/index.mjs | 110 ++ .../test/common/inspector-helper.js | 549 ++++++ .../node-napi-tests/test/common/internet.js | 58 + .../test/common/measure-memory.js | 57 + test/napi/node-napi-tests/test/common/net.js | 23 + .../node-napi-tests/test/common/package.json | 3 + .../test/common/process-exit-code-cases.js | 138 ++ test/napi/node-napi-tests/test/common/prof.js | 67 + .../node-napi-tests/test/common/report.js | 346 ++++ .../node-napi-tests/test/common/require-as.js | 27 + test/napi/node-napi-tests/test/common/sea.js | 144 ++ .../test/common/shared-lib-util.js | 50 + .../node-napi-tests/test/common/snapshot.js | 65 + test/napi/node-napi-tests/test/common/tick.js | 12 + test/napi/node-napi-tests/test/common/tls.js | 176 ++ .../node-napi-tests/test/common/tmpdir.js | 95 + test/napi/node-napi-tests/test/common/udp.js | 24 + test/napi/node-napi-tests/test/common/v8.js | 70 + test/napi/node-napi-tests/test/common/wasi.js | 43 + test/napi/node-napi-tests/test/common/wpt.js | 982 ++++++++++ .../node-napi-tests/test/common/wpt/worker.js | 70 + .../test/js-native-api/.gitignore | 7 + .../2_function_arguments.c | 39 + .../2_function_arguments/binding.gyp | 10 + .../2_function_arguments/test.js | 6 + .../js-native-api/3_callbacks/3_callbacks.c | 58 + .../js-native-api/3_callbacks/binding.gyp | 10 + .../test/js-native-api/3_callbacks/test.js | 22 + .../4_object_factory/4_object_factory.c | 24 + .../4_object_factory/binding.gyp | 10 + .../js-native-api/4_object_factory/test.js | 8 + .../5_function_factory/5_function_factory.c | 24 + .../5_function_factory/binding.gyp | 10 + .../js-native-api/5_function_factory/test.js | 7 + .../6_object_wrap/6_object_wrap.cc | 221 +++ .../js-native-api/6_object_wrap/binding.gyp | 10 + .../js-native-api/6_object_wrap/myobject.h | 26 + .../6_object_wrap/test-object-wrap-ref.js | 14 + .../test/js-native-api/6_object_wrap/test.js | 48 + .../7_factory_wrap/7_factory_wrap.cc | 32 + .../js-native-api/7_factory_wrap/binding.gyp | 11 + .../js-native-api/7_factory_wrap/myobject.cc | 101 + .../js-native-api/7_factory_wrap/myobject.h | 27 + .../test/js-native-api/7_factory_wrap/test.js | 27 + .../8_passing_wrapped/8_passing_wrapped.cc | 61 + .../8_passing_wrapped/binding.gyp | 11 + .../8_passing_wrapped/myobject.cc | 80 + .../8_passing_wrapped/myobject.h | 26 + .../js-native-api/8_passing_wrapped/test.js | 21 + .../test/js-native-api/common-inl.h | 71 + .../test/js-native-api/common.h | 136 ++ .../test/js-native-api/entry_point.h | 12 + .../test/js-native-api/test_array/binding.gyp | 10 + .../test/js-native-api/test_array/test.js | 60 + .../js-native-api/test_array/test_array.c | 188 ++ .../js-native-api/test_bigint/binding.gyp | 10 + .../test/js-native-api/test_bigint/test.js | 52 + .../js-native-api/test_bigint/test_bigint.c | 159 ++ .../test_cannot_run_js/binding.gyp | 18 + .../js-native-api/test_cannot_run_js/test.js | 24 + .../test_cannot_run_js/test_cannot_run_js.c | 76 + .../test_constructor/binding.gyp | 11 + .../js-native-api/test_constructor/test.js | 62 + .../js-native-api/test_constructor/test2.js | 8 + .../test_constructor/test_constructor.c | 200 ++ .../test_constructor/test_null.c | 111 ++ .../test_constructor/test_null.h | 8 + .../test_constructor/test_null.js | 18 + .../test_conversions/binding.gyp | 11 + .../js-native-api/test_conversions/test.js | 218 +++ .../test_conversions/test_conversions.c | 158 ++ .../test_conversions/test_null.c | 102 + .../test_conversions/test_null.h | 8 + .../js-native-api/test_dataview/binding.gyp | 10 + .../test/js-native-api/test_dataview/test.js | 24 + .../test_dataview/test_dataview.c | 102 + .../test/js-native-api/test_date/binding.gyp | 10 + .../test/js-native-api/test_date/test.js | 21 + .../test/js-native-api/test_date/test_date.c | 64 + .../test/js-native-api/test_error/binding.gyp | 10 + .../test/js-native-api/test_error/test.js | 148 ++ .../js-native-api/test_error/test_error.c | 197 ++ .../js-native-api/test_exception/binding.gyp | 10 + .../test/js-native-api/test_exception/test.js | 115 ++ .../test_exception/testFinalizerException.js | 32 + .../test_exception/test_exception.c | 116 ++ .../js-native-api/test_finalizer/binding.gyp | 11 + .../test/js-native-api/test_finalizer/test.js | 45 + .../test_finalizer/test_fatal_finalize.js | 31 + .../test_finalizer/test_finalizer.c | 148 ++ .../js-native-api/test_function/binding.gyp | 10 + .../test/js-native-api/test_function/test.js | 52 + .../test_function/test_function.c | 204 ++ .../js-native-api/test_general/binding.gyp | 10 + .../test/js-native-api/test_general/test.js | 97 + .../test_general/testEnvCleanup.js | 57 + .../test_general/testFinalizer.js | 37 + .../js-native-api/test_general/testGlobals.js | 8 + .../test_general/testInstanceOf.js | 95 + .../js-native-api/test_general/testNapiRun.js | 14 + .../test_general/testNapiStatus.js | 8 + .../js-native-api/test_general/test_general.c | 315 ++++ .../test_handle_scope/binding.gyp | 10 + .../js-native-api/test_handle_scope/test.js | 19 + .../test_handle_scope/test_handle_scope.c | 86 + .../test_instance_data/binding.gyp | 10 + .../js-native-api/test_instance_data/test.js | 41 + .../test_instance_data/test_instance_data.c | 96 + .../js-native-api/test_new_target/binding.gyp | 11 + .../js-native-api/test_new_target/test.js | 21 + .../test_new_target/test_new_target.c | 70 + .../js-native-api/test_number/binding.gyp | 11 + .../test/js-native-api/test_number/test.js | 134 ++ .../js-native-api/test_number/test_null.c | 77 + .../js-native-api/test_number/test_null.h | 8 + .../js-native-api/test_number/test_null.js | 18 + .../js-native-api/test_number/test_number.c | 110 ++ .../js-native-api/test_object/binding.gyp | 17 + .../test/js-native-api/test_object/test.js | 393 ++++ .../test_object/test_exceptions.c | 82 + .../test_object/test_exceptions.js | 18 + .../js-native-api/test_object/test_null.c | 400 ++++ .../js-native-api/test_object/test_null.h | 8 + .../js-native-api/test_object/test_null.js | 53 + .../js-native-api/test_object/test_object.c | 755 ++++++++ .../js-native-api/test_promise/binding.gyp | 10 + .../test/js-native-api/test_promise/test.js | 61 + .../js-native-api/test_promise/test_promise.c | 64 + .../js-native-api/test_properties/binding.gyp | 10 + .../js-native-api/test_properties/test.js | 68 + .../test_properties/test_properties.c | 113 ++ .../js-native-api/test_reference/binding.gyp | 16 + .../test/js-native-api/test_reference/test.js | 156 ++ .../test_reference/test_finalizer.c | 71 + .../test_reference/test_finalizer.js | 20 + .../test_reference/test_reference.c | 252 +++ .../test_reference_double_free/binding.gyp | 10 + .../test_reference_double_free/test.js | 11 + .../test_reference_double_free.c | 90 + .../test_reference_double_free/test_wrap.js | 10 + .../js-native-api/test_string/binding.gyp | 14 + .../test/js-native-api/test_string/test.js | 91 + .../js-native-api/test_string/test_null.c | 71 + .../js-native-api/test_string/test_null.h | 8 + .../js-native-api/test_string/test_null.js | 17 + .../js-native-api/test_string/test_string.c | 498 +++++ .../js-native-api/test_symbol/binding.gyp | 10 + .../test/js-native-api/test_symbol/test1.js | 19 + .../test/js-native-api/test_symbol/test2.js | 17 + .../test/js-native-api/test_symbol/test3.js | 19 + .../js-native-api/test_symbol/test_symbol.c | 38 + .../js-native-api/test_typedarray/binding.gyp | 10 + .../js-native-api/test_typedarray/test.js | 109 ++ .../test_typedarray/test_typedarray.c | 249 +++ .../test/js-native-api/testcfg.py | 6 + .../node-napi-tests/test/node-api/.buildstamp | 0 .../node-napi-tests/test/node-api/.gitignore | 1 + .../test/node-api/1_hello_world/binding.c | 17 + .../test/node-api/1_hello_world/binding.gyp | 8 + .../test/node-api/1_hello_world/test.js | 22 + .../test/node-api/node-api.status | 14 + .../test/node-api/test_async/binding.gyp | 8 + .../node-api/test_async/test-async-hooks.js | 60 + .../test/node-api/test_async/test-loop.js | 14 + .../test/node-api/test_async/test-uncaught.js | 18 + .../test/node-api/test_async/test.js | 30 + .../test/node-api/test_async/test_async.c | 216 +++ .../test_async_cleanup_hook/binding.c | 101 + .../test_async_cleanup_hook/binding.gyp | 9 + .../node-api/test_async_cleanup_hook/test.js | 8 + .../node-api/test_async_context/binding.c | 129 ++ .../node-api/test_async_context/binding.gyp | 9 + .../test-gcable-callback.js | 65 + .../test_async_context/test-gcable.js | 44 + .../test/node-api/test_async_context/test.js | 63 + .../test/node-api/test_buffer/binding.gyp | 15 + .../test_buffer/test-external-buffer.js | 14 + .../test/node-api/test_buffer/test.js | 34 + .../test/node-api/test_buffer/test_buffer.c | 189 ++ .../node-api/test_buffer/test_finalizer.c | 61 + .../node-api/test_buffer/test_finalizer.js | 25 + .../node-api/test_callback_scope/binding.c | 119 ++ .../node-api/test_callback_scope/binding.gyp | 9 + .../test_callback_scope/test-async-hooks.js | 39 + .../test_callback_scope/test-resolve-async.js | 6 + .../test/node-api/test_callback_scope/test.js | 17 + .../test/node-api/test_cleanup_hook/binding.c | 50 + .../node-api/test_cleanup_hook/binding.gyp | 9 + .../test/node-api/test_cleanup_hook/test.js | 13 + .../node-api/test_env_teardown_gc/binding.c | 37 + .../node-api/test_env_teardown_gc/binding.gyp | 8 + .../node-api/test_env_teardown_gc/test.js | 14 + .../test/node-api/test_exception/binding.gyp | 8 + .../test/node-api/test_exception/test.js | 20 + .../node-api/test_exception/test_exception.c | 27 + .../test/node-api/test_fatal/binding.gyp | 8 + .../test/node-api/test_fatal/test.js | 19 + .../test/node-api/test_fatal/test2.js | 19 + .../test/node-api/test_fatal/test_fatal.c | 48 + .../test/node-api/test_fatal/test_threads.js | 21 + .../test_fatal/test_threads_report.js | 36 + .../node-api/test_fatal_exception/binding.gyp | 8 + .../node-api/test_fatal_exception/test.js | 11 + .../test_fatal_exception.c | 26 + .../test/node-api/test_general/binding.gyp | 8 + .../test/node-api/test_general/test.js | 46 + .../test/node-api/test_general/test_general.c | 52 + .../test/node-api/test_init_order/binding.gyp | 8 + .../test/node-api/test_init_order/test.js | 10 + .../test_init_order/test_init_order.cc | 52 + .../test/node-api/test_instance_data/addon.c | 22 + .../node-api/test_instance_data/binding.gyp | 24 + .../test/node-api/test_instance_data/test.js | 50 + .../test_instance_data/test_instance_data.c | 206 +++ .../test_instance_data/test_ref_then_set.c | 10 + .../test_instance_data/test_set_then_ref.c | 10 + .../node-api/test_make_callback/binding.c | 60 + .../node-api/test_make_callback/binding.gyp | 9 + .../test_make_callback/test-async-hooks.js | 53 + .../test/node-api/test_make_callback/test.js | 86 + .../test_make_callback_recurse/binding.c | 41 + .../test_make_callback_recurse/binding.gyp | 9 + .../test_make_callback_recurse/test.js | 150 ++ .../test/node-api/test_null_init/binding.gyp | 8 + .../test/node-api/test_null_init/test.js | 7 + .../node-api/test_null_init/test_null_init.c | 53 + .../binding.gyp | 14 + .../test.js | 125 ++ .../test_reference_by_node_api_version.c | 169 ++ .../test_threadsafe_function/binding.c | 347 ++++ .../test_threadsafe_function/binding.gyp | 22 + .../node-api/test_threadsafe_function/test.js | 227 +++ .../test_legacy_uncaught_exception.js | 22 + .../test_uncaught_exception.c | 62 + .../test_uncaught_exception.js | 7 + .../test_uncaught_exception_v9.js | 8 + .../uncaught_exception.js | 31 + .../test/node-api/test_uv_loop/binding.gyp | 8 + .../test/node-api/test_uv_loop/test.js | 5 + .../node-api/test_uv_loop/test_uv_loop.cc | 89 + .../test_uv_threadpool_size/binding.gyp | 8 + .../test_uv_threadpool_size/node-options.js | 28 + .../node-api/test_uv_threadpool_size/test.js | 7 + .../test_uv_threadpool_size.c | 189 ++ .../test_worker_buffer_callback/binding.gyp | 8 + .../test-free-called.js | 17 + .../test_worker_buffer_callback/test.js | 18 + .../test_worker_buffer_callback.c | 46 + .../test_worker_terminate/binding.gyp | 8 + .../node-api/test_worker_terminate/test.js | 28 + .../test_worker_terminate.c | 39 + .../binding.gyp | 8 + .../test.js | 23 + .../test_worker_terminate_finalization.c | 51 + .../node-napi-tests/test/node-api/testcfg.py | 6 + test/napi/node-napi.test.ts | 138 ++ 341 files changed, 26257 insertions(+), 2540 deletions(-) create mode 100644 src/bun.js/bindings/napi_finalizer.cpp create mode 100644 src/bun.js/bindings/napi_finalizer.h create mode 100644 src/bun.js/bindings/napi_macros.h create mode 100644 test/napi/napi-app/.clangd create mode 100644 test/napi/napi-app/async_finalize_addon.c create mode 100644 test/napi/napi-app/async_tests.cpp create mode 100644 test/napi/napi-app/async_tests.h create mode 100644 test/napi/napi-app/class_test.cpp create mode 100644 test/napi/napi-app/class_test.h create mode 100644 test/napi/napi-app/conversion_tests.cpp create mode 100644 test/napi/napi-app/conversion_tests.h create mode 100644 test/napi/napi-app/ffi_addon_1.c create mode 100644 test/napi/napi-app/ffi_addon_2.c create mode 100644 test/napi/napi-app/js_test_helpers.cpp create mode 100644 test/napi/napi-app/js_test_helpers.h create mode 100644 test/napi/napi-app/leak-fixture.js create mode 100644 test/napi/napi-app/leak_tests.cpp create mode 100644 test/napi/napi-app/leak_tests.h create mode 100644 test/napi/napi-app/standalone_tests.cpp create mode 100644 test/napi/napi-app/standalone_tests.h create mode 100644 test/napi/napi-value-ffi.test.ts create mode 100644 test/napi/node-napi-tests/.gitignore create mode 100644 test/napi/node-napi-tests/README.md create mode 100644 test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof-2.js create mode 100644 test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof.js create mode 100644 test/napi/node-napi-tests/test/README.md create mode 100644 test/napi/node-napi-tests/test/common/README.md create mode 100644 test/napi/node-napi-tests/test/common/arraystream.js create mode 100644 test/napi/node-napi-tests/test/common/assertSnapshot.js create mode 100644 test/napi/node-napi-tests/test/common/benchmark.js create mode 100644 test/napi/node-napi-tests/test/common/child_process.js create mode 100644 test/napi/node-napi-tests/test/common/countdown.js create mode 100644 test/napi/node-napi-tests/test/common/cpu-prof.js create mode 100644 test/napi/node-napi-tests/test/common/crypto.js create mode 100644 test/napi/node-napi-tests/test/common/debugger.js create mode 100644 test/napi/node-napi-tests/test/common/dns.js create mode 100644 test/napi/node-napi-tests/test/common/fixtures.js create mode 100644 test/napi/node-napi-tests/test/common/fixtures.mjs create mode 100644 test/napi/node-napi-tests/test/common/gc.js create mode 100644 test/napi/node-napi-tests/test/common/globals.js create mode 100644 test/napi/node-napi-tests/test/common/heap.js create mode 100644 test/napi/node-napi-tests/test/common/hijackstdio.js create mode 100644 test/napi/node-napi-tests/test/common/http2.js create mode 100644 test/napi/node-napi-tests/test/common/index.js create mode 100644 test/napi/node-napi-tests/test/common/index.mjs create mode 100644 test/napi/node-napi-tests/test/common/inspector-helper.js create mode 100644 test/napi/node-napi-tests/test/common/internet.js create mode 100644 test/napi/node-napi-tests/test/common/measure-memory.js create mode 100644 test/napi/node-napi-tests/test/common/net.js create mode 100644 test/napi/node-napi-tests/test/common/package.json create mode 100644 test/napi/node-napi-tests/test/common/process-exit-code-cases.js create mode 100644 test/napi/node-napi-tests/test/common/prof.js create mode 100644 test/napi/node-napi-tests/test/common/report.js create mode 100644 test/napi/node-napi-tests/test/common/require-as.js create mode 100644 test/napi/node-napi-tests/test/common/sea.js create mode 100644 test/napi/node-napi-tests/test/common/shared-lib-util.js create mode 100644 test/napi/node-napi-tests/test/common/snapshot.js create mode 100644 test/napi/node-napi-tests/test/common/tick.js create mode 100644 test/napi/node-napi-tests/test/common/tls.js create mode 100644 test/napi/node-napi-tests/test/common/tmpdir.js create mode 100644 test/napi/node-napi-tests/test/common/udp.js create mode 100644 test/napi/node-napi-tests/test/common/v8.js create mode 100644 test/napi/node-napi-tests/test/common/wasi.js create mode 100644 test/napi/node-napi-tests/test/common/wpt.js create mode 100644 test/napi/node-napi-tests/test/common/wpt/worker.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/.gitignore create mode 100644 test/napi/node-napi-tests/test/js-native-api/2_function_arguments/2_function_arguments.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/2_function_arguments/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/2_function_arguments/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/3_callbacks/3_callbacks.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/3_callbacks/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/3_callbacks/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/4_object_factory/4_object_factory.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/4_object_factory/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/4_object_factory/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/5_function_factory/5_function_factory.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/5_function_factory/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/5_function_factory/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/6_object_wrap/6_object_wrap.cc create mode 100644 test/napi/node-napi-tests/test/js-native-api/6_object_wrap/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/6_object_wrap/myobject.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/6_object_wrap/test-object-wrap-ref.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/6_object_wrap/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/7_factory_wrap/7_factory_wrap.cc create mode 100644 test/napi/node-napi-tests/test/js-native-api/7_factory_wrap/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/7_factory_wrap/myobject.cc create mode 100644 test/napi/node-napi-tests/test/js-native-api/7_factory_wrap/myobject.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/7_factory_wrap/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/8_passing_wrapped/8_passing_wrapped.cc create mode 100644 test/napi/node-napi-tests/test/js-native-api/8_passing_wrapped/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/8_passing_wrapped/myobject.cc create mode 100644 test/napi/node-napi-tests/test/js-native-api/8_passing_wrapped/myobject.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/8_passing_wrapped/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/common-inl.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/common.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/entry_point.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_array/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_array/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_array/test_array.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_bigint/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_bigint/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_bigint/test_bigint.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_cannot_run_js/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_cannot_run_js/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_cannot_run_js/test_cannot_run_js.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_constructor/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_constructor/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_constructor/test2.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_constructor/test_constructor.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_constructor/test_null.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_constructor/test_null.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_constructor/test_null.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_conversions/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_conversions/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_conversions/test_conversions.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_conversions/test_null.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_conversions/test_null.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_dataview/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_dataview/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_dataview/test_dataview.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_date/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_date/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_date/test_date.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_error/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_error/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_error/test_error.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_exception/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_exception/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_exception/testFinalizerException.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_exception/test_exception.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_finalizer/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_finalizer/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_finalizer/test_fatal_finalize.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_finalizer/test_finalizer.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_function/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_function/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_function/test_function.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_general/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_general/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_general/testEnvCleanup.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_general/testFinalizer.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_general/testGlobals.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_general/testInstanceOf.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_general/testNapiRun.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_general/testNapiStatus.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_general/test_general.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_handle_scope/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_handle_scope/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_handle_scope/test_handle_scope.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_instance_data/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_instance_data/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_instance_data/test_instance_data.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_new_target/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_new_target/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_new_target/test_new_target.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_number/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_number/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_number/test_null.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_number/test_null.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_number/test_null.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_number/test_number.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_object/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_object/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_object/test_exceptions.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_object/test_exceptions.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_object/test_null.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_object/test_null.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_object/test_null.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_object/test_object.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_promise/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_promise/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_promise/test_promise.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_properties/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_properties/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_properties/test_properties.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_reference/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_reference/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_reference/test_finalizer.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_reference/test_finalizer.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_reference/test_reference.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_reference_double_free/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_reference_double_free/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_reference_double_free/test_reference_double_free.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_reference_double_free/test_wrap.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_string/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_string/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_string/test_null.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_string/test_null.h create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_string/test_null.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_string/test_string.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_symbol/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_symbol/test1.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_symbol/test2.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_symbol/test3.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_symbol/test_symbol.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_typedarray/binding.gyp create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_typedarray/test.js create mode 100644 test/napi/node-napi-tests/test/js-native-api/test_typedarray/test_typedarray.c create mode 100644 test/napi/node-napi-tests/test/js-native-api/testcfg.py create mode 100644 test/napi/node-napi-tests/test/node-api/.buildstamp create mode 100644 test/napi/node-napi-tests/test/node-api/.gitignore create mode 100644 test/napi/node-napi-tests/test/node-api/1_hello_world/binding.c create mode 100644 test/napi/node-napi-tests/test/node-api/1_hello_world/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/1_hello_world/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/node-api.status create mode 100644 test/napi/node-napi-tests/test/node-api/test_async/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_async/test-async-hooks.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_async/test-loop.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_async/test-uncaught.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_async/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_async/test_async.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_async_cleanup_hook/binding.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_async_cleanup_hook/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_async_cleanup_hook/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_async_context/binding.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_async_context/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_async_context/test-gcable-callback.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_async_context/test-gcable.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_async_context/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_buffer/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_buffer/test-external-buffer.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_buffer/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_buffer/test_buffer.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_buffer/test_finalizer.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_buffer/test_finalizer.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_callback_scope/binding.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_callback_scope/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_callback_scope/test-async-hooks.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_callback_scope/test-resolve-async.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_callback_scope/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_cleanup_hook/binding.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_cleanup_hook/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_cleanup_hook/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_env_teardown_gc/binding.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_env_teardown_gc/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_env_teardown_gc/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_exception/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_exception/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_exception/test_exception.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_fatal/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_fatal/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_fatal/test2.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_fatal/test_fatal.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_fatal/test_threads.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_fatal/test_threads_report.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_fatal_exception/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_fatal_exception/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_fatal_exception/test_fatal_exception.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_general/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_general/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_general/test_general.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_init_order/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_init_order/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_init_order/test_init_order.cc create mode 100644 test/napi/node-napi-tests/test/node-api/test_instance_data/addon.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_instance_data/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_instance_data/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_instance_data/test_instance_data.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_instance_data/test_ref_then_set.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_instance_data/test_set_then_ref.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_make_callback/binding.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_make_callback/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_make_callback/test-async-hooks.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_make_callback/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_make_callback_recurse/binding.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_make_callback_recurse/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_make_callback_recurse/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_null_init/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_null_init/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_null_init/test_null_init.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_reference_by_node_api_version/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_reference_by_node_api_version/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_reference_by_node_api_version/test_reference_by_node_api_version.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_threadsafe_function/binding.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_threadsafe_function/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_threadsafe_function/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_threadsafe_function/test_legacy_uncaught_exception.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_threadsafe_function/test_uncaught_exception.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_threadsafe_function/test_uncaught_exception.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_threadsafe_function/test_uncaught_exception_v9.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_threadsafe_function/uncaught_exception.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_uv_loop/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_uv_loop/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_uv_loop/test_uv_loop.cc create mode 100644 test/napi/node-napi-tests/test/node-api/test_uv_threadpool_size/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_uv_threadpool_size/node-options.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_uv_threadpool_size/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_uv_threadpool_size/test_uv_threadpool_size.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_buffer_callback/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_buffer_callback/test-free-called.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_buffer_callback/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_buffer_callback/test_worker_buffer_callback.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_terminate/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_terminate/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_terminate/test_worker_terminate.c create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_terminate_finalization/binding.gyp create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_terminate_finalization/test.js create mode 100644 test/napi/node-napi-tests/test/node-api/test_worker_terminate_finalization/test_worker_terminate_finalization.c create mode 100644 test/napi/node-napi-tests/test/node-api/testcfg.py create mode 100644 test/napi/node-napi.test.ts diff --git a/cmake/tools/SetupZig.cmake b/cmake/tools/SetupZig.cmake index 0534a36529..c64acc3425 100644 --- a/cmake/tools/SetupZig.cmake +++ b/cmake/tools/SetupZig.cmake @@ -20,7 +20,7 @@ else() unsupported(CMAKE_SYSTEM_NAME) endif() -set(ZIG_COMMIT "02c57c7ee3b8fde7528c74dd06490834d2d6fae9") +set(ZIG_COMMIT "bb9d6ab2c0bbbf20cc24dad03e88f3b3ffdb7de7") optionx(ZIG_TARGET STRING "The zig target to use" DEFAULT ${DEFAULT_ZIG_TARGET}) if(CMAKE_BUILD_TYPE STREQUAL "Release") diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index 4b4f480b25..511469bd6a 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -48,6 +48,7 @@ const testsPath = join(cwd, "test"); const spawnTimeout = 5_000; const testTimeout = 3 * 60_000; const integrationTimeout = 5 * 60_000; +const napiTimeout = 10 * 60_000; function getNodeParallelTestTimeout(testPath) { if (testPath.includes("test-dns")) { @@ -680,6 +681,9 @@ function getTestTimeout(testPath) { if (/integration|3rd_party|docker|bun-install-registry|v8/i.test(testPath)) { return integrationTimeout; } + if (/napi/i.test(testPath)) { + return napiTimeout; + } return testTimeout; } diff --git a/src/bun.js/api/FFI.h b/src/bun.js/api/FFI.h index 50a7d0bcd3..6ca644a1e2 100644 --- a/src/bun.js/api/FFI.h +++ b/src/bun.js/api/FFI.h @@ -15,6 +15,11 @@ #define ZIG_REPR_TYPE int64_t +#ifdef _WIN32 +#define BUN_FFI_IMPORT __declspec(dllimport) +#else +#define BUN_FFI_IMPORT +#endif // /* 7.18.1.1 Exact-width integer types */ typedef unsigned char uint8_t; @@ -60,9 +65,9 @@ typedef enum { napi_detachable_arraybuffer_expected, napi_would_deadlock // unused } napi_status; -void* NapiHandleScope__open(void* napi_env, bool detached); -void NapiHandleScope__close(void* napi_env, void* handleScope); -extern struct napi_env__ Bun__thisFFIModuleNapiEnv; +BUN_FFI_IMPORT void* NapiHandleScope__open(void* napi_env, bool detached); +BUN_FFI_IMPORT void NapiHandleScope__close(void* napi_env, void* handleScope); +BUN_FFI_IMPORT extern struct napi_env__ Bun__thisFFIModuleNapiEnv; #endif @@ -136,7 +141,7 @@ typedef void* JSContext; #ifdef IS_CALLBACK void* callback_ctx; -ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args); +BUN_FFI_IMPORT ZIG_REPR_TYPE FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args); // We wrap static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) __attribute__((__always_inline__)); static EncodedJSValue _FFI_Callback_call(void* ctx, size_t argCount, ZIG_REPR_TYPE* args) { @@ -348,7 +353,7 @@ static EncodedJSValue INT64_TO_JSVALUE(void* jsGlobalObject, int64_t val) { } #ifndef IS_CALLBACK -ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame); +BUN_FFI_IMPORT ZIG_REPR_TYPE JSFunctionCall(void* jsGlobalObject, void* callFrame); #endif diff --git a/src/bun.js/api/ffi.zig b/src/bun.js/api/ffi.zig index d99c5f12ce..caae47b9c6 100644 --- a/src/bun.js/api/ffi.zig +++ b/src/bun.js/api/ffi.zig @@ -25,6 +25,8 @@ const JSGlobalObject = bun.JSC.JSGlobalObject; const VM = bun.JSC.VM; const VirtualMachine = JSC.VirtualMachine; +const napi = @import("../../napi/napi.zig"); + const TCC = @import("../../deps/tcc.zig"); extern fn pthread_jit_write_protect_np(enable: c_int) void; @@ -423,7 +425,7 @@ pub const FFI = struct { for (this.symbols.map.values()) |*symbol| { if (symbol.needsNapiEnv()) { - state.addSymbol("Bun__thisFFIModuleNapiEnv", globalThis) catch return error.DeferredErrors; + state.addSymbol("Bun__thisFFIModuleNapiEnv", globalThis.makeNapiEnvForFFI()) catch return error.DeferredErrors; break; } } @@ -583,6 +585,7 @@ pub const FFI = struct { if (arguments.len == 0 or !arguments[0].isObject()) { return globalThis.throwInvalidArguments("Expected object", .{}); } + const allocator = bun.default_allocator; // Step 1. compile the user's code @@ -604,7 +607,7 @@ pub const FFI = struct { return error.JSError; } - if (try generateSymbols(globalThis, &compile_c.symbols.map, symbols_object)) |val| { + if (try generateSymbols(globalThis, allocator, &compile_c.symbols.map, symbols_object)) |val| { if (val != .zero and !globalThis.hasException()) return globalThis.throwValue(val); return error.JSError; @@ -626,7 +629,7 @@ pub const FFI = struct { if (flags_value.isArray()) { var iter = flags_value.arrayIterator(globalThis); - var flags = std.ArrayList(u8).init(bun.default_allocator); + var flags = std.ArrayList(u8).init(allocator); defer flags.deinit(); flags.appendSlice(CompileC.default_tcc_options) catch bun.outOfMemory(); @@ -634,7 +637,7 @@ pub const FFI = struct { if (!value.isString()) { return globalThis.throwInvalidArgumentTypeValue("flags", "array of strings", value); } - const slice = try value.toSlice(globalThis, bun.default_allocator); + const slice = try value.toSlice(globalThis, allocator); if (slice.len == 0) continue; defer slice.deinit(); flags.append(' ') catch bun.outOfMemory(); @@ -642,7 +645,7 @@ pub const FFI = struct { } flags.append(0) catch bun.outOfMemory(); compile_c.flags = flags.items[0 .. flags.items.len - 1 :0]; - flags = std.ArrayList(u8).init(bun.default_allocator); + flags = std.ArrayList(u8).init(allocator); } else { if (!flags_value.isString()) { return globalThis.throwInvalidArgumentTypeValue("flags", "string", flags_value); @@ -650,7 +653,7 @@ pub const FFI = struct { const str = try flags_value.getZigString(globalThis); if (!str.isEmpty()) { - compile_c.flags = str.toOwnedSliceZ(bun.default_allocator) catch bun.outOfMemory(); + compile_c.flags = str.toOwnedSliceZ(allocator) catch bun.outOfMemory(); } } } @@ -665,22 +668,22 @@ pub const FFI = struct { var iter = try Iter.init(globalThis, define_value); defer iter.deinit(); while (try iter.next()) |entry| { - const key = entry.toOwnedSliceZ(bun.default_allocator) catch bun.outOfMemory(); + const key = entry.toOwnedSliceZ(allocator) catch bun.outOfMemory(); var owned_value: [:0]const u8 = ""; if (iter.value != .zero and iter.value != .undefined) { if (iter.value.isString()) { const value = try iter.value.getZigString(globalThis); if (value.len > 0) { - owned_value = value.toOwnedSliceZ(bun.default_allocator) catch bun.outOfMemory(); + owned_value = value.toOwnedSliceZ(allocator) catch bun.outOfMemory(); } } } if (globalThis.hasException()) { - bun.default_allocator.free(key); + allocator.free(key); return error.JSError; } - compile_c.define.append(bun.default_allocator, .{ key, owned_value }) catch bun.outOfMemory(); + compile_c.define.append(allocator, .{ key, owned_value }) catch bun.outOfMemory(); } } } @@ -745,12 +748,13 @@ pub const FFI = struct { // we are unable to free memory safely in certain cases here. } + const napi_env = makeNapiEnvIfNeeded(compile_c.symbols.map.values(), globalThis); + var obj = JSC.JSValue.createEmptyObject(globalThis, compile_c.symbols.map.count()); for (compile_c.symbols.map.values()) |*function| { const function_name = function.base_name.?; - const allocator = bun.default_allocator; - function.compile(allocator, globalThis) catch |err| { + function.compile(napi_env) catch |err| { if (!globalThis.hasException()) { const ret = JSC.toInvalidArguments("{s} when translating symbol \"{s}\"", .{ @errorName(err), @@ -804,7 +808,7 @@ pub const FFI = struct { pub fn closeCallback(globalThis: *JSGlobalObject, ctx: JSValue) JSValue { var function = ctx.asPtr(Function); - function.deinit(globalThis, bun.default_allocator); + function.deinit(globalThis); return JSValue.jsUndefined(); } @@ -819,7 +823,7 @@ pub const FFI = struct { } const allocator = VirtualMachine.get().allocator; - var function: Function = .{}; + var function: Function = .{ .allocator = allocator }; var func = &function; if (generateSymbolForFunction(globalThis, allocator, interface, func) catch ZigString.init("Out of memory").toErrorInstance(globalThis)) |val| { @@ -830,17 +834,17 @@ pub const FFI = struct { func.base_name = ""; js_callback.ensureStillAlive(); - func.compileCallback(allocator, globalThis, js_callback, func.threadsafe) catch return ZigString.init("Out of memory").toErrorInstance(globalThis); + func.compileCallback(globalThis, js_callback, func.threadsafe) catch return ZigString.init("Out of memory").toErrorInstance(globalThis); switch (func.step) { .failed => |err| { const message = ZigString.init(err.msg).toErrorInstance(globalThis); - func.deinit(globalThis, allocator); + func.deinit(globalThis); return message; }, .pending => { - func.deinit(globalThis, allocator); + func.deinit(globalThis); return ZigString.init("Failed to compile, but not sure why. Please report this bug").toErrorInstance(globalThis); }, .compiled => { @@ -880,7 +884,7 @@ pub const FFI = struct { const allocator = VirtualMachine.get().allocator; for (this.functions.values()) |*val| { - val.deinit(globalThis, allocator); + val.deinit(globalThis); } this.functions.deinit(allocator); @@ -903,7 +907,7 @@ pub const FFI = struct { return JSC.toInvalidArguments("Expected an object", .{}, global); } - var function: Function = .{}; + var function: Function = .{ .allocator = allocator }; if (generateSymbolForFunction(global, allocator, object, &function) catch ZigString.init("Out of memory").toErrorInstance(global)) |val| { return val; } @@ -921,7 +925,7 @@ pub const FFI = struct { } pub fn print(global: *JSGlobalObject, object: JSC.JSValue, is_callback_val: ?JSC.JSValue) JSValue { - const allocator = VirtualMachine.get().allocator; + const allocator = bun.default_allocator; if (is_callback_val) |is_callback| { if (is_callback.toBoolean()) { return printCallback(global, object); @@ -933,7 +937,7 @@ pub const FFI = struct { } var symbols = bun.StringArrayHashMapUnmanaged(Function){}; - if (generateSymbols(global, &symbols, object) catch JSC.JSValue.zero) |val| { + if (generateSymbols(global, bun.default_allocator, &symbols, object) catch JSC.JSValue.zero) |val| { // an error while validating symbols for (symbols.keys()) |key| { allocator.free(@constCast(key)); @@ -1005,7 +1009,9 @@ pub const FFI = struct { pub fn open(global: *JSGlobalObject, name_str: ZigString, object: JSC.JSValue) JSC.JSValue { JSC.markBinding(@src()); const vm = VirtualMachine.get(); - const allocator = bun.default_allocator; + var scope = bun.AllocationScope.init(bun.default_allocator); + defer scope.deinit(); + const allocator = scope.allocator(); var name_slice = name_str.toSlice(allocator); defer name_slice.deinit(); @@ -1038,7 +1044,7 @@ pub const FFI = struct { } var symbols = bun.StringArrayHashMapUnmanaged(Function){}; - if (generateSymbols(global, &symbols, object) catch JSC.JSValue.zero) |val| { + if (generateSymbols(global, allocator, &symbols, object) catch JSC.JSValue.zero) |val| { // an error while validating symbols for (symbols.keys()) |key| { allocator.free(@constCast(key)); @@ -1074,6 +1080,9 @@ pub const FFI = struct { var obj = JSC.JSValue.createEmptyObject(global, size); obj.protect(); defer obj.unprotect(); + + const napi_env = makeNapiEnvIfNeeded(symbols.values(), global); + for (symbols.values()) |*function| { const function_name = function.base_name.?; @@ -1093,15 +1102,14 @@ pub const FFI = struct { function.symbol_from_dynamic_library = resolved_symbol; } - function.compile(allocator, global) catch |err| { + function.compile(napi_env) catch |err| { const ret = JSC.toInvalidArguments("{s} when compiling symbol \"{s}\" in \"{s}\"", .{ bun.asByteSlice(@errorName(err)), bun.asByteSlice(function_name), name, }, global); for (symbols.values()) |*value| { - allocator.free(@constCast(bun.asByteSlice(value.base_name.?))); - value.arg_types.clearAndFree(allocator); + value.deinit(global); } symbols.clearAndFree(allocator); dylib.close(); @@ -1109,21 +1117,18 @@ pub const FFI = struct { }; switch (function.step) { .failed => |err| { - for (symbols.values()) |*value| { - allocator.free(@constCast(bun.asByteSlice(value.base_name.?))); - value.arg_types.clearAndFree(allocator); - } + defer for (symbols.values()) |*other_function| { + other_function.deinit(global); + }; const res = ZigString.init(err.msg).toErrorInstance(global); - function.deinit(global, allocator); symbols.clearAndFree(allocator); dylib.close(); return res; }, .pending => { - for (symbols.values()) |*value| { - allocator.free(@constCast(bun.asByteSlice(value.base_name.?))); - value.arg_types.clearAndFree(allocator); + for (symbols.values()) |*other_function| { + other_function.deinit(global); } symbols.clearAndFree(allocator); dylib.close(); @@ -1171,7 +1176,7 @@ pub const FFI = struct { } var symbols = bun.StringArrayHashMapUnmanaged(Function){}; - if (generateSymbols(global, &symbols, object) catch JSC.JSValue.zero) |val| { + if (generateSymbols(global, allocator, &symbols, object) catch JSC.JSValue.zero) |val| { // an error while validating symbols for (symbols.keys()) |key| { allocator.free(@constCast(key)); @@ -1186,6 +1191,9 @@ pub const FFI = struct { var obj = JSValue.createEmptyObject(global, symbols.count()); obj.ensureStillAlive(); defer obj.ensureStillAlive(); + + const napi_env = makeNapiEnvIfNeeded(symbols.values(), global); + for (symbols.values()) |*function| { const function_name = function.base_name.?; @@ -1199,14 +1207,13 @@ pub const FFI = struct { return ret; } - function.compile(allocator, global) catch |err| { + function.compile(napi_env) catch |err| { const ret = JSC.toInvalidArguments("{s} when compiling symbol \"{s}\"", .{ bun.asByteSlice(@errorName(err)), bun.asByteSlice(function_name), }, global); for (symbols.values()) |*value| { - allocator.free(@constCast(bun.asByteSlice(value.base_name.?))); - value.arg_types.clearAndFree(allocator); + value.deinit(global); } symbols.clearAndFree(allocator); return ret; @@ -1219,7 +1226,7 @@ pub const FFI = struct { } const res = ZigString.init(err.msg).toErrorInstance(global); - function.deinit(global, allocator); + function.deinit(global); symbols.clearAndFree(allocator); return res; }, @@ -1358,6 +1365,7 @@ pub const FFI = struct { .arg_types = abi_types, .return_type = return_type, .threadsafe = threadsafe, + .allocator = allocator, }; if (try value.get(global, "ptr")) |ptr| { @@ -1376,9 +1384,8 @@ pub const FFI = struct { return null; } - pub fn generateSymbols(global: *JSGlobalObject, symbols: *bun.StringArrayHashMapUnmanaged(Function), object: JSC.JSValue) bun.JSError!?JSValue { + pub fn generateSymbols(global: *JSGlobalObject, allocator: Allocator, symbols: *bun.StringArrayHashMapUnmanaged(Function), object: JSC.JSValue) bun.JSError!?JSValue { JSC.markBinding(@src()); - const allocator = VirtualMachine.get().allocator; var symbols_iter = try JSC.JSPropertyIterator(.{ .skip_empty_name = true, @@ -1396,7 +1403,7 @@ pub const FFI = struct { return JSC.toTypeError(.ERR_INVALID_ARG_VALUE, "Expected an object for key \"{any}\"", .{prop}, global); } - var function: Function = .{}; + var function: Function = .{ .allocator = allocator }; if (try generateSymbolForFunction(global, allocator, value, &function)) |val| { return val; } @@ -1417,6 +1424,7 @@ pub const FFI = struct { arg_types: std.ArrayListUnmanaged(ABIType) = .{}, step: Step = Step{ .pending = {} }, threadsafe: bool = false, + allocator: Allocator, pub var lib_dirZ: [*:0]const u8 = ""; @@ -1431,16 +1439,16 @@ pub const FFI = struct { extern "c" fn FFICallbackFunctionWrapper_destroy(*anyopaque) void; - pub fn deinit(val: *Function, globalThis: *JSC.JSGlobalObject, allocator: std.mem.Allocator) void { + pub fn deinit(val: *Function, globalThis: *JSC.JSGlobalObject) void { JSC.markBinding(@src()); if (val.base_name) |base_name| { if (bun.asByteSlice(base_name).len > 0) { - allocator.free(@constCast(bun.asByteSlice(base_name))); + val.allocator.free(@constCast(bun.asByteSlice(base_name))); } } - val.arg_types.clearAndFree(allocator); + val.arg_types.clearAndFree(val.allocator); if (val.state) |state| { state.deinit(); @@ -1448,7 +1456,7 @@ pub const FFI = struct { } if (val.step == .compiled) { - // allocator.free(val.step.compiled.buf); + // val.allocator.free(val.step.compiled.buf); if (val.step.compiled.js_function != .zero) { _ = globalThis; // _ = JSC.untrackFunction(globalThis, val.step.compiled.js_function); @@ -1462,7 +1470,7 @@ pub const FFI = struct { } if (val.step == .failed and val.step.failed.allocated) { - allocator.free(val.step.failed.msg); + val.allocator.free(val.step.failed.msg); } } @@ -1508,17 +1516,13 @@ pub const FFI = struct { msg = msg[offset..]; } - this.step = .{ .failed = .{ .msg = VirtualMachine.get().allocator.dupe(u8, msg) catch unreachable, .allocated = true } }; + this.step = .{ .failed = .{ .msg = this.allocator.dupe(u8, msg) catch unreachable, .allocated = true } }; } const tcc_options = "-std=c11 -nostdlib -Wl,--export-all-symbols" ++ if (Environment.isDebug) " -g" else ""; - pub fn compile( - this: *Function, - allocator: std.mem.Allocator, - globalObject: *JSC.JSGlobalObject, - ) !void { - var source_code = std.ArrayList(u8).init(allocator); + pub fn compile(this: *Function, napiEnv: ?*napi.NapiEnv) !void { + var source_code = std.ArrayList(u8).init(this.allocator); var source_code_writer = source_code.writer(); try this.printSourceCode(&source_code_writer); @@ -1537,10 +1541,12 @@ pub const FFI = struct { } } - _ = state.addSymbol("Bun__thisFFIModuleNapiEnv", globalObject) catch { - this.fail("Failed to add NAPI env symbol"); - return; - }; + if (napiEnv) |env| { + _ = state.addSymbol("Bun__thisFFIModuleNapiEnv", env) catch { + this.fail("Failed to add NAPI env symbol"); + return; + }; + } CompilerRT.define(state); @@ -1560,9 +1566,9 @@ pub const FFI = struct { return; }; - const bytes: []u8 = try allocator.alloc(u8, relocation_size); + const bytes: []u8 = try this.allocator.alloc(u8, relocation_size); defer { - if (this.step == .failed) allocator.free(bytes); + if (this.step == .failed) this.allocator.free(bytes); } _ = dangerouslyRunWithoutJitProtections(TCC.Error!usize, TCC.State.relocate, .{ state, bytes.ptr }) catch { @@ -1586,13 +1592,12 @@ pub const FFI = struct { pub fn compileCallback( this: *Function, - allocator: std.mem.Allocator, js_context: *JSC.JSGlobalObject, js_function: JSValue, is_threadsafe: bool, ) !void { JSC.markBinding(@src()); - var source_code = std.ArrayList(u8).init(allocator); + var source_code = std.ArrayList(u8).init(this.allocator); var source_code_writer = source_code.writer(); const ffi_wrapper = Bun__createFFICallbackFunction(js_context, js_function); try this.printCallbackSourceCode(js_context, ffi_wrapper, &source_code_writer); @@ -1628,10 +1633,12 @@ pub const FFI = struct { } } - state.addSymbol("Bun__thisFFIModuleNapiEnv", js_context) catch { - this.fail("Failed to add NAPI env symbol"); - return; - }; + if (this.needsNapiEnv()) { + state.addSymbol("Bun__thisFFIModuleNapiEnv", js_context.makeNapiEnvForFFI()) catch { + this.fail("Failed to add NAPI env symbol"); + return; + }; + } CompilerRT.define(state); @@ -1666,10 +1673,10 @@ pub const FFI = struct { return; }; - const bytes: []u8 = try allocator.alloc(u8, relocation_size); + const bytes: []u8 = try this.allocator.alloc(u8, relocation_size); defer { if (this.step == .failed) { - allocator.free(bytes); + this.allocator.free(bytes); } } @@ -2428,3 +2435,13 @@ const CompilerRT = struct { }; pub const Bun__FFI__cc = FFI.Bun__FFI__cc; + +fn makeNapiEnvIfNeeded(functions: []const FFI.Function, globalThis: *JSGlobalObject) ?*napi.NapiEnv { + for (functions) |function| { + if (function.needsNapiEnv()) { + return globalThis.makeNapiEnvForFFI(); + } + } + + return null; +} diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index c41ccad1d7..33b008bb13 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -1,3 +1,5 @@ +#include "napi.h" + #include "BunProcess.h" #include #include @@ -37,6 +39,8 @@ #include #include #include "wtf-bindings.h" +#include "EventLoopTask.h" + #include #include "ProcessBindingTTYWrap.h" #include "wtf/text/ASCIILiteral.h" @@ -101,6 +105,7 @@ typedef int mode_t; #include // setuid, getuid #endif +#include extern "C" bool Bun__Node__ProcessNoDeprecation; extern "C" bool Bun__Node__ProcessThrowDeprecation; extern "C" int32_t bun_stdio_tty[3]; @@ -292,13 +297,76 @@ extern "C" bool Bun__resolveEmbeddedNodeFile(void*, BunString*); extern "C" HMODULE Bun__LoadLibraryBunString(BunString*); #endif +/// Returns a pointer that needs to be freed with `delete[]`. +static char* toFileURI(std::string_view path) +{ + auto needs_escape = [](char ch) { + return !(('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ('0' <= ch && ch <= '9') + || ch == '_' || ch == '-' || ch == '.' || ch == '!' || ch == '~' || ch == '*' || ch == '\'' || ch == '(' || ch == ')' || ch == '/' || ch == ':'); + }; + + auto to_hex = [](uint8_t nybble) -> char { + if (nybble < 0xa) { + return '0' + nybble; + } + + return 'a' + (nybble - 0xa); + }; + + size_t escape_count = 0; + for (char ch : path) { +#if OS(WINDOWS) + if (needs_escape(ch) && ch != '\\') { +#else + if (needs_escape(ch)) { +#endif + ++escape_count; + } + } + +#if OS(WINDOWS) +#define FILE_URI_START "file:///" +#else +#define FILE_URI_START "file://" +#endif + + const size_t string_size = sizeof(FILE_URI_START) + path.size() + 2 * escape_count; // null byte is included in the sizeof expression + char* characters = new char[string_size]; + strncpy(characters, FILE_URI_START, sizeof(FILE_URI_START)); + size_t i = sizeof(FILE_URI_START) - 1; + for (char ch : path) { +#if OS(WINDOWS) + if (ch == '\\') { + characters[i++] = '/'; + continue; + } +#endif + if (needs_escape(ch)) { + characters[i++] = '%'; + characters[i++] = to_hex(static_cast(ch) >> 4); + characters[i++] = to_hex(ch & 0xf); + } else { + characters[i++] = ch; + } + } + + characters[i] = '\0'; + ASSERT(i + 1 == string_size); + return characters; +} + +static char* toFileURI(std::span span) +{ + return toFileURI(std::string_view(span.data(), span.size())); +} + extern "C" size_t Bun__process_dlopen_count; JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalObject_, JSC::CallFrame* callFrame)) { Zig::GlobalObject* globalObject = reinterpret_cast(globalObject_); auto callCountAtStart = globalObject->napiModuleRegisterCallCount; - auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); auto& vm = JSC::getVM(globalObject); auto argCount = callFrame->argumentCount(); @@ -408,18 +476,17 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb return JSValue::encode(jsUndefined()); } - JSC::EncodedJSValue (*napi_register_module_v1)(JSC::JSGlobalObject* globalObject, - JSC::EncodedJSValue exports); #if OS(WINDOWS) #define dlsym GetProcAddress #endif // TODO(@190n) look for node_register_module_vXYZ according to BuildOptions.reported_nodejs_version // (bun/src/env.zig:36) and the table at https://github.com/nodejs/node/blob/main/doc/abi_version_registry.json - napi_register_module_v1 = reinterpret_cast( + auto napi_register_module_v1 = reinterpret_cast( dlsym(handle, "napi_register_module_v1")); + auto node_api_module_get_api_version_v1 = reinterpret_cast(dlsym(handle, "node_api_module_get_api_version_v1")); + #if OS(WINDOWS) #undef dlsym #endif @@ -430,21 +497,41 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb #else dlclose(handle); #endif + JSC::throwTypeError(globalObject, scope, "symbol 'napi_register_module_v1' not found in native module. Is this a Node API (napi) module?"_s); return {}; } + // TODO(@heimskr): get the API version without node_api_module_get_api_version_v1 a different way + int module_version = 8; + if (node_api_module_get_api_version_v1) { + module_version = node_api_module_get_api_version_v1(); + } + NapiHandleScope handleScope(globalObject); EncodedJSValue exportsValue = JSC::JSValue::encode(exports); - JSC::JSValue resultValue = JSValue::decode(napi_register_module_v1(globalObject, exportsValue)); + + char* filename_cstr = toFileURI(filename.utf8().span()); + + napi_module nmodule { + .nm_version = module_version, + .nm_flags = 0, + .nm_filename = filename_cstr, + .nm_register_func = nullptr, + .nm_modname = "[no modname]", + .nm_priv = nullptr, + .reserved = {}, + }; + + static_assert(sizeof(napi_value) == sizeof(EncodedJSValue), "EncodedJSValue must be reinterpretable as a pointer"); + + auto env = globalObject->makeNapiEnv(nmodule); + env->filename = filename_cstr; + + auto encoded = reinterpret_cast(napi_register_module_v1(env, reinterpret_cast(exportsValue))); RETURN_IF_EXCEPTION(scope, {}); - // If a module returns `nullptr` (cast to a napi_value) from its register function, we should - // use the `exports` value (which may have had properties added to it) as the return value of - // `require()`. - if (resultValue.isEmpty()) { - resultValue = exports; - } + JSC::JSValue resultValue = encoded == 0 ? exports : JSValue::decode(encoded); if (auto resultObject = resultValue.getObject()) { #if OS(DARWIN) || OS(LINUX) @@ -458,7 +545,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionDlopen, (JSC::JSGlobalObject * globalOb // TODO: think about the finalizer here // currently we do not dealloc napi modules so we don't have to worry about it right now auto* meta = new Bun::NapiModuleMeta(globalObject->m_pendingNapiModuleDlopenHandle); - Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, nullptr); + Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, env, nullptr); bool success = resultObject->putDirect(vm, WebCore::builtinNames(vm).napiDlopenHandlePrivateName(), napi_external, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); ASSERT(success); RETURN_IF_EXCEPTION(scope, {}); @@ -847,9 +934,9 @@ extern "C" void Bun__onSignalForJS(int signalNumber, Zig::GlobalObject* globalOb Process* process = jsCast(globalObject->processObject()); String signalName = signalNumberToNameMap->get(signalNumber); - Identifier signalNameIdentifier = Identifier::fromString(globalObject->vm(), signalName); + Identifier signalNameIdentifier = Identifier::fromString(JSC::getVM(globalObject), signalName); MarkedArgumentBuffer args; - args.append(jsString(globalObject->vm(), signalNameIdentifier.string())); + args.append(jsString(JSC::getVM(globalObject), signalNameIdentifier.string())); args.append(jsNumber(signalNumber)); process->wrapped().emitForBindings(signalNameIdentifier, args); @@ -901,12 +988,12 @@ extern "C" int Bun__handleUncaughtException(JSC::JSGlobalObject* lexicalGlobalOb args.append(jsString(vm, String("uncaughtException"_s))); } - auto uncaughtExceptionMonitor = Identifier::fromString(globalObject->vm(), "uncaughtExceptionMonitor"_s); + auto uncaughtExceptionMonitor = Identifier::fromString(JSC::getVM(globalObject), "uncaughtExceptionMonitor"_s); if (wrapped.listenerCount(uncaughtExceptionMonitor) > 0) { wrapped.emit(uncaughtExceptionMonitor, args); } - auto uncaughtExceptionIdent = Identifier::fromString(globalObject->vm(), "uncaughtException"_s); + auto uncaughtExceptionIdent = Identifier::fromString(JSC::getVM(globalObject), "uncaughtException"_s); // if there is an uncaughtExceptionCaptureCallback, call it and consider the exception handled auto capture = process->getUncaughtExceptionCaptureCallback(); @@ -937,7 +1024,7 @@ extern "C" int Bun__handleUnhandledRejection(JSC::JSGlobalObject* lexicalGlobalO MarkedArgumentBuffer args; args.append(reason); args.append(promise); - auto eventType = Identifier::fromString(globalObject->vm(), "unhandledRejection"_s); + auto eventType = Identifier::fromString(JSC::getVM(globalObject), "unhandledRejection"_s); auto& wrapped = process->wrapped(); if (wrapped.listenerCount(eventType) > 0) { wrapped.emit(eventType, args); @@ -2566,7 +2653,7 @@ JSValue createCryptoX509Object(JSGlobalObject* globalObject) JSC_DEFINE_HOST_FUNCTION(Process_functionBinding, (JSGlobalObject * jsGlobalObject, CallFrame* callFrame)) { - auto& vm = jsGlobalObject->vm(); + auto& vm = JSC::getVM(jsGlobalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); auto globalObject = jsCast(jsGlobalObject); auto process = jsCast(globalObject->processObject()); @@ -3003,7 +3090,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionReportUncaughtException, (JSC::JSGlobalObject JSC_DEFINE_HOST_FUNCTION(jsFunctionDrainMicrotaskQueue, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - globalObject->vm().drainMicrotasks(); + JSC::getVM(globalObject).drainMicrotasks(); return JSValue::encode(jsUndefined()); } @@ -3082,7 +3169,7 @@ static JSValue constructProcessNextTickFn(VM& vm, JSObject* processObject) { JSGlobalObject* lexicalGlobalObject = processObject->globalObject(); Zig::GlobalObject* globalObject = jsCast(lexicalGlobalObject); - return jsCast(processObject)->constructNextTickFn(globalObject->vm(), globalObject); + return jsCast(processObject)->constructNextTickFn(JSC::getVM(globalObject), globalObject); } JSC_DEFINE_CUSTOM_GETTER(processNoDeprecation, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName name)) @@ -3234,7 +3321,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionCwd, (JSC::JSGlobalObject * globalObjec JSC_DEFINE_HOST_FUNCTION(Process_functionReallyKill, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); if (callFrame->argumentCount() < 2) { throwVMError(globalObject, scope, "Not enough arguments"_s); @@ -3260,7 +3347,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionReallyKill, (JSC::JSGlobalObject * glob JSC_DEFINE_HOST_FUNCTION(Process_functionKill, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { - auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); auto pid_value = callFrame->argument(0); // this is mimicking `if (pid != (pid | 0)) {` @@ -3289,7 +3376,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionKill, (JSC::JSGlobalObject * globalObje } auto global = jsCast(globalObject); - auto& vm = global->vm(); + auto& vm = JSC::getVM(global); JSValue _killFn = global->processObject()->get(globalObject, Identifier::fromString(vm, "_kill"_s)); RETURN_IF_EXCEPTION(scope, {}); if (!_killFn.isCallable()) { @@ -3323,7 +3410,7 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionKill, (JSC::JSGlobalObject * globalObje extern "C" void Process__emitMessageEvent(Zig::GlobalObject* global, EncodedJSValue value) { auto* process = static_cast(global->processObject()); - auto& vm = global->vm(); + auto& vm = JSC::getVM(global); auto ident = vm.propertyNames->message; if (process->wrapped().hasEventListeners(ident)) { @@ -3336,7 +3423,7 @@ extern "C" void Process__emitMessageEvent(Zig::GlobalObject* global, EncodedJSVa extern "C" void Process__emitDisconnectEvent(Zig::GlobalObject* global) { auto* process = static_cast(global->processObject()); - auto& vm = global->vm(); + auto& vm = JSC::getVM(global); auto ident = Identifier::fromString(vm, "disconnect"_s); if (process->wrapped().hasEventListeners(ident)) { JSC::MarkedArgumentBuffer args; @@ -3347,7 +3434,7 @@ extern "C" void Process__emitDisconnectEvent(Zig::GlobalObject* global) extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValue value) { auto* process = static_cast(global->processObject()); - auto& vm = global->vm(); + auto& vm = JSC::getVM(global); if (process->wrapped().hasEventListeners(vm.propertyNames->error)) { JSC::MarkedArgumentBuffer args; args.append(JSValue::decode(value)); diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 0f2ec5ec80..4da4477966 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -1202,13 +1202,11 @@ GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure, const JSC::Gl , m_worldIsNormal(true) , m_builtinInternalFunctions(vm) , m_scriptExecutionContext(new WebCore::ScriptExecutionContext(&vm, this)) - , globalEventScope(*new Bun::WorkerGlobalScope(m_scriptExecutionContext)) + , globalEventScope(adoptRef(*new Bun::WorkerGlobalScope(m_scriptExecutionContext))) { // m_scriptExecutionContext = globalEventScope.m_context; mockModule = Bun::JSMockModule::create(this); - globalEventScope.m_context = m_scriptExecutionContext; - // FIXME: is there a better way to do this? this event handler should always be tied to the global object - globalEventScope.relaxAdoptionRequirement(); + globalEventScope->m_context = m_scriptExecutionContext; } GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure, WebCore::ScriptExecutionContextIdentifier contextId, const JSC::GlobalObjectMethodTable* methodTable) @@ -1219,22 +1217,15 @@ GlobalObject::GlobalObject(JSC::VM& vm, JSC::Structure* structure, WebCore::Scri , m_worldIsNormal(true) , m_builtinInternalFunctions(vm) , m_scriptExecutionContext(new WebCore::ScriptExecutionContext(&vm, this, contextId)) - , globalEventScope(*new Bun::WorkerGlobalScope(m_scriptExecutionContext)) + , globalEventScope(adoptRef(*new Bun::WorkerGlobalScope(m_scriptExecutionContext))) { // m_scriptExecutionContext = globalEventScope.m_context; mockModule = Bun::JSMockModule::create(this); - globalEventScope.m_context = m_scriptExecutionContext; - // FIXME: is there a better way to do this? this event handler should always be tied to the global object - globalEventScope.relaxAdoptionRequirement(); + globalEventScope->m_context = m_scriptExecutionContext; } GlobalObject::~GlobalObject() { - if (napiInstanceDataFinalizer) { - napi_finalize finalizer = reinterpret_cast(napiInstanceDataFinalizer); - finalizer(toNapi(this), napiInstanceData, napiInstanceDataFinalizerHint); - } - if (auto* ctx = scriptExecutionContext()) { ctx->removeFromContextsMap(); ctx->deref(); @@ -1926,7 +1917,7 @@ static inline JSC::EncodedJSValue jsFunctionAddEventListenerBody(JSC::JSGlobalOb EnsureStillAliveScope argument2 = callFrame->argument(2); auto options = argument2.value().isUndefined() ? false : convert, IDLBoolean>>(*lexicalGlobalObject, argument2.value()); RETURN_IF_EXCEPTION(throwScope, {}); - auto result = JSValue::encode(WebCore::toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.addEventListenerForBindings(WTFMove(type), WTFMove(listener), WTFMove(options)); })); + auto result = JSValue::encode(WebCore::toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl->addEventListenerForBindings(WTFMove(type), WTFMove(listener), WTFMove(options)); })); RETURN_IF_EXCEPTION(throwScope, {}); vm.writeBarrier(&static_cast(*castedThis), argument1.value()); return result; @@ -1955,7 +1946,7 @@ static inline JSC::EncodedJSValue jsFunctionRemoveEventListenerBody(JSC::JSGloba EnsureStillAliveScope argument2 = callFrame->argument(2); auto options = argument2.value().isUndefined() ? false : convert, IDLBoolean>>(*lexicalGlobalObject, argument2.value()); RETURN_IF_EXCEPTION(throwScope, {}); - auto result = JSValue::encode(WebCore::toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.removeEventListenerForBindings(WTFMove(type), WTFMove(listener), WTFMove(options)); })); + auto result = JSValue::encode(WebCore::toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl->removeEventListenerForBindings(WTFMove(type), WTFMove(listener), WTFMove(options)); })); RETURN_IF_EXCEPTION(throwScope, {}); vm.writeBarrier(&static_cast(*castedThis), argument1.value()); return result; @@ -1978,7 +1969,7 @@ static inline JSC::EncodedJSValue jsFunctionDispatchEventBody(JSC::JSGlobalObjec EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); auto event = convert>(*lexicalGlobalObject, argument0.value(), [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentTypeError(lexicalGlobalObject, scope, 0, "event"_s, "EventTarget"_s, "dispatchEvent"_s, "Event"_s); }); RETURN_IF_EXCEPTION(throwScope, {}); - RELEASE_AND_RETURN(throwScope, JSValue::encode(WebCore::toJS(*lexicalGlobalObject, throwScope, impl.dispatchEventForBindings(*event)))); + RELEASE_AND_RETURN(throwScope, JSValue::encode(WebCore::toJS(*lexicalGlobalObject, throwScope, impl->dispatchEventForBindings(*event)))); } JSC_DEFINE_HOST_FUNCTION(jsFunctionDispatchEvent, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) @@ -2460,7 +2451,6 @@ extern "C" JSC__JSValue ZigGlobalObject__readableStreamToJSON(Zig::GlobalObject* return JSC::JSValue::encode(call(globalObject, function, callData, JSC::jsUndefined(), arguments)); } -extern "C" JSC__JSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC__JSValue readableStreamValue); extern "C" JSC__JSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC__JSValue readableStreamValue) { auto& vm = JSC::getVM(globalObject); @@ -2481,6 +2471,11 @@ extern "C" JSC__JSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* return JSC::JSValue::encode(call(globalObject, function, callData, JSC::jsUndefined(), arguments)); } +extern "C" napi_env ZigGlobalObject__makeNapiEnvForFFI(Zig::GlobalObject* globalObject) +{ + return globalObject->makeNapiEnvForFFI(); +} + JSC_DECLARE_HOST_FUNCTION(functionReadableStreamToArrayBuffer); JSC_DEFINE_HOST_FUNCTION(functionReadableStreamToArrayBuffer, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { @@ -3079,12 +3074,6 @@ void GlobalObject::finishCreation(VM& vm) Bun::NapiExternal::createStructure(init.vm, init.owner, init.owner->objectPrototype())); }); - m_NAPIFunctionStructure.initLater( - [](const JSC::LazyProperty::Initializer& init) { - init.set( - Zig::createNAPIFunctionStructure(init.vm, init.owner)); - }); - m_NapiPrototypeStructure.initLater( [](const JSC::LazyProperty::Initializer& init) { init.set( @@ -3975,7 +3964,6 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_JSDirentClassStructure.visit(visitor); thisObject->m_NapiClassStructure.visit(visitor); thisObject->m_NapiExternalStructure.visit(visitor); - thisObject->m_NAPIFunctionStructure.visit(visitor); thisObject->m_NapiPrototypeStructure.visit(visitor); thisObject->m_NapiHandleScopeImplStructure.visit(visitor); thisObject->m_NapiTypeTagStructure.visit(visitor); @@ -3999,6 +3987,7 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) thisObject->m_utilInspectStylizeColorFunction.visit(visitor); thisObject->m_utilInspectStylizeNoColorFunction.visit(visitor); thisObject->m_vmModuleContextMap.visit(visitor); + thisObject->m_napiTypeTags.visit(visitor); thisObject->mockModule.activeSpySetStructure.visit(visitor); thisObject->mockModule.mockFunctionStructure.visit(visitor); thisObject->mockModule.mockImplementationStructure.visit(visitor); @@ -4107,7 +4096,7 @@ void GlobalObject::visitAdditionalChildren(Visitor& visitor) GlobalObject* thisObject = this; ASSERT_GC_OBJECT_INHERITS(thisObject, info()); - thisObject->globalEventScope.visitJSEventListeners(visitor); + thisObject->globalEventScope->visitJSEventListeners(visitor); ScriptExecutionContext* context = thisObject->scriptExecutionContext(); visitor.addOpaqueRoot(context); @@ -4532,6 +4521,37 @@ GlobalObject::PromiseFunctions GlobalObject::promiseHandlerID(Zig::FFIFunction h } } +napi_env GlobalObject::makeNapiEnv(const napi_module& mod) +{ + m_napiEnvs.append(std::make_unique(this, mod)); + return m_napiEnvs.last().get(); +} + +napi_env GlobalObject::makeNapiEnvForFFI() +{ + auto out = makeNapiEnv(napi_module { + .nm_version = 9, + .nm_flags = 0, + .nm_filename = "ffi://", + .nm_register_func = nullptr, + .nm_modname = "[ffi]", + .nm_priv = nullptr, + .reserved = {}, + }); + return out; +} + +bool GlobalObject::hasNapiFinalizers() const +{ + for (const auto& env : m_napiEnvs) { + if (env->hasFinalizers()) { + return true; + } + } + + return false; +} + #include "ZigGeneratedClasses+lazyStructureImpl.h" #include "ZigGlobalObject.lut.h" diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index a673897bed..843342a1c0 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -54,6 +54,7 @@ class GlobalInternals; #include "BunHttp2CommonStrings.h" #include "BunGlobalScope.h" #include +#include namespace WebCore { class WorkerGlobalScope; @@ -268,7 +269,6 @@ public: Structure* NapiExternalStructure() const { return m_NapiExternalStructure.getInitializedOnMainThread(this); } Structure* NapiPrototypeStructure() const { return m_NapiPrototypeStructure.getInitializedOnMainThread(this); } - Structure* NAPIFunctionStructure() const { return m_NAPIFunctionStructure.getInitializedOnMainThread(this); } Structure* NapiHandleScopeImplStructure() const { return m_NapiHandleScopeImplStructure.getInitializedOnMainThread(this); } Structure* NapiTypeTagStructure() const { return m_NapiTypeTagStructure.getInitializedOnMainThread(this); } @@ -308,7 +308,7 @@ public: WebCore::EventTarget& eventTarget(); WebCore::ScriptExecutionContext* m_scriptExecutionContext; - Bun::WorkerGlobalScope& globalEventScope; + Ref globalEventScope; void resetOnEachMicrotaskTick(); @@ -466,13 +466,6 @@ public: // To do that, we count the number of times we register a module. int napiModuleRegisterCallCount = 0; - // NAPI instance data - // This is not a correct implementation - // Addon modules can override each other's data - void* napiInstanceData = nullptr; - void* napiInstanceDataFinalizer = nullptr; - void* napiInstanceDataFinalizerHint = nullptr; - // Used by napi_type_tag_object to associate a 128-bit type ID with JS objects. // Should only use JSCell* keys and NapiTypeTag values. LazyProperty m_napiTypeTags; @@ -592,7 +585,6 @@ public: LazyProperty m_JSCryptoKey; LazyProperty m_NapiExternalStructure; LazyProperty m_NapiPrototypeStructure; - LazyProperty m_NAPIFunctionStructure; LazyProperty m_NapiHandleScopeImplStructure; LazyProperty m_NapiTypeTagStructure; @@ -616,16 +608,10 @@ public: bool hasOverridenModuleResolveFilenameFunction = false; - // Almost all NAPI functions should set error_code to the status they're returning right before - // they return it - napi_extended_error_info m_lastNapiErrorInfo = { - .error_message = "", - // Not currently used by Bun -- always nullptr - .engine_reserved = nullptr, - // Not currently used by Bun -- always zero - .engine_error_code = 0, - .error_code = napi_ok, - }; + WTF::Vector> m_napiEnvs; + napi_env makeNapiEnv(const napi_module&); + napi_env makeNapiEnvForFFI(); + bool hasNapiFinalizers() const; private: DOMGuardedObjectSet m_guardedObjects WTF_GUARDED_BY_LOCK(m_gcLock); diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 0d09815cd9..ef76e711f1 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -3776,7 +3776,7 @@ bool JSC__JSValue__isUndefinedOrNull(JSC__JSValue JSValue0) JSC__JSValue JSC__JSValue__jsBoolean(bool arg0) { return JSC::JSValue::encode(JSC::jsBoolean(arg0)); -}; +} JSC__JSValue JSC__JSValue__jsDoubleNumber(double arg0) { return JSC::JSValue::encode(JSC::jsNumber(arg0)); @@ -3785,32 +3785,35 @@ JSC__JSValue JSC__JSValue__jsDoubleNumber(double arg0) JSC__JSValue JSC__JSValue__jsEmptyString(JSC__JSGlobalObject* arg0) { return JSC::JSValue::encode(JSC::jsEmptyString(arg0->vm())); -}; -JSC__JSValue JSC__JSValue__jsNull() { return JSC::JSValue::encode(JSC::jsNull()); }; +} +JSC__JSValue JSC__JSValue__jsNull() +{ + return JSC::JSValue::encode(JSC::jsNull()); +} JSC__JSValue JSC__JSValue__jsNumberFromChar(unsigned char arg0) { return JSC::JSValue::encode(JSC::jsNumber(arg0)); -}; +} JSC__JSValue JSC__JSValue__jsNumberFromDouble(double arg0) { return JSC::JSValue::encode(JSC::jsNumber(arg0)); -}; +} JSC__JSValue JSC__JSValue__jsNumberFromInt32(int32_t arg0) { return JSC::JSValue::encode(JSC::jsNumber(arg0)); -}; +} JSC__JSValue JSC__JSValue__jsNumberFromInt64(int64_t arg0) { return JSC::JSValue::encode(JSC::jsNumber(arg0)); -}; +} JSC__JSValue JSC__JSValue__jsNumberFromU16(uint16_t arg0) { return JSC::JSValue::encode(JSC::jsNumber(arg0)); -}; +} JSC__JSValue JSC__JSValue__jsNumberFromUint64(uint64_t arg0) { return JSC::JSValue::encode(JSC::jsNumber(arg0)); -}; +} int64_t JSC__JSValue__toInt64(JSC__JSValue val) { diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index aaa3c40560..806d5697d5 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -21,6 +21,8 @@ const String = bun.String; const ErrorableString = JSC.ErrorableString; const JSError = bun.JSError; const OOM = bun.OOM; +const napi = @import("../../napi/napi.zig"); + pub extern const JSC__JSObject__maxInlineCapacity: c_uint; pub const JSObject = extern struct { pub const shim = Shimmer("JSC", "JSObject", @This()); @@ -513,8 +515,7 @@ pub const ZigString = extern struct { else try strings.allocateLatin1IntoUTF8WithList(list, 0, []const u8, this.slice()); - try list.append(0); - return list.items[0 .. list.items.len - 1 :0]; + return list.toOwnedSliceSentinel(0); } pub fn trunc(this: ZigString, len: usize) ZigString { @@ -3353,6 +3354,12 @@ pub const JSGlobalObject = opaque { return ZigGlobalObject__readableStreamToFormData(this, value, content_type); } + extern fn ZigGlobalObject__makeNapiEnvForFFI(*JSGlobalObject) *napi.NapiEnv; + + pub fn makeNapiEnvForFFI(this: *JSGlobalObject) *napi.NapiEnv { + return ZigGlobalObject__makeNapiEnvForFFI(this); + } + pub inline fn assertOnJSThread(this: *JSGlobalObject) void { if (bun.Environment.allow_assert) this.bunVM().assertOnJSThread(); } @@ -3860,6 +3867,8 @@ pub const JSValue = enum(i64) { .Float32Array => .kJSTypedArrayTypeFloat32Array, .Float64Array => .kJSTypedArrayTypeFloat64Array, .ArrayBuffer => .kJSTypedArrayTypeArrayBuffer, + .BigInt64Array => .kJSTypedArrayTypeBigInt64Array, + .BigUint64Array => .kJSTypedArrayTypeBigUint64Array, // .DataView => .kJSTypedArrayTypeDataView, else => .kJSTypedArrayTypeNone, }; diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 5b8a8924e8..ee95c89759 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -1,13 +1,18 @@ +#include "BunProcess.h" +#include "headers.h" #include "node_api.h" #include "root.h" +#include "JavaScriptCore/ConstructData.h" #include "JavaScriptCore/DateInstance.h" #include "JavaScriptCore/JSCast.h" #include "ZigGlobalObject.h" #include "JavaScriptCore/JSGlobalObject.h" #include "JavaScriptCore/SourceCode.h" -#include "js_native_api_types.h" +#include "js_native_api.h" #include "napi_handle_scope.h" +#include "napi_macros.h" +#include "napi_finalizer.h" #include "napi_type_tag.h" #include "helpers.h" @@ -59,6 +64,7 @@ #include #include "napi_external.h" +#include "wtf/Assertions.h" #include "wtf/Compiler.h" #include "wtf/NakedPtr.h" #include @@ -67,35 +73,23 @@ #include "wtf/text/ASCIIFastPath.h" #include "JavaScriptCore/WeakInlines.h" #include +#include -// #include using namespace JSC; using namespace Zig; -#define NAPI_VERBOSE 0 - -#if NAPI_VERBOSE -#include -#include - -void napi_log(long line, const char* function, const char* fmt, ...) -{ - printf("[napi.cpp:%ld] %s: ", line, function); - - va_list ap; - va_start(ap, fmt); - vprintf(fmt, ap); - va_end(ap); - - printf("\n"); -} - -#define NAPI_LOG_CURRENT_FUNCTION printf("[napi.cpp:%d] %s\n", __LINE__, __PRETTY_FUNCTION__) -#define NAPI_LOG(fmt, ...) napi_log(__LINE__, __PRETTY_FUNCTION__, fmt __VA_OPT__(, ) __VA_ARGS__) -#else -#define NAPI_LOG_CURRENT_FUNCTION -#define NAPI_LOG(fmt, ...) -#endif +// Every NAPI function should use this at the start. It does the following: +// - if NAPI_VERBOSE is 1, log that the function was called +// - if env is nullptr, return napi_invalid_arg +// - if there is a pending exception, return napi_pending_exception +// No do..while is used as this declares a variable that other macros need to use +#define NAPI_PREAMBLE(_env) \ + NAPI_LOG_CURRENT_FUNCTION; \ + NAPI_CHECK_ARG(_env, _env); \ + /* You should not use this throw scope directly -- if you need */ \ + /* to throw or clear exceptions, make your own scope */ \ + auto napi_preamble_throw_scope__ = DECLARE_THROW_SCOPE(toJS(_env)->vm()); \ + NAPI_RETURN_IF_EXCEPTION(_env) // Every NAPI function should use this at the start. It does the following: // - if NAPI_VERBOSE is 1, log that the function was called @@ -126,6 +120,12 @@ void napi_log(long line, const char* function, const char* fmt, ...) } \ } while (0) +// Assert that the environment is not performing garbage collection +#define NAPI_CHECK_ENV_NOT_IN_GC(_env) \ + do { \ + (_env)->checkGC(); \ + } while (0) + // Return the specified code if condition is false. Only use for input validation. #define NAPI_RETURN_EARLY_IF_FALSE(_env, condition, code) \ do { \ @@ -166,7 +166,7 @@ extern "C" napi_status napi_set_last_error(napi_env env, napi_status status) { if (env) { // napi_get_last_error_info will fill in the other fields if they are requested - toJS(env)->m_lastNapiErrorInfo.error_code = status; + env->m_lastNapiErrorInfo.error_code = status; } return status; } @@ -211,23 +211,19 @@ napi_get_last_error_info(napi_env env, const napi_extended_error_info** result) static_assert(std::size(error_messages) == last_status + 1, "error_messages array does not cover all status codes"); - auto globalObject = toJS(env); - - napi_status status = globalObject->m_lastNapiErrorInfo.error_code; + napi_status status = env->m_lastNapiErrorInfo.error_code; if (status >= 0 && status <= last_status) { - globalObject->m_lastNapiErrorInfo.error_message = error_messages[status]; + env->m_lastNapiErrorInfo.error_message = error_messages[status]; } else { - globalObject->m_lastNapiErrorInfo.error_message = nullptr; + env->m_lastNapiErrorInfo.error_message = nullptr; } - *result = &globalObject->m_lastNapiErrorInfo; + *result = &env->m_lastNapiErrorInfo; // return without napi_return_status as that would overwrite the error info return napi_ok; } -namespace Napi { - JSC::SourceCode generateSourceCode(WTF::String keyString, JSC::VM& vm, JSC::JSObject* object, JSC::JSGlobalObject* globalObject) { JSC::JSArray* exportKeys = ownPropertyKeys(globalObject, object, PropertyNameMode::StringsAndSymbols, DontEnumPropertiesMode::Include); @@ -254,78 +250,24 @@ JSC::SourceCode generateSourceCode(WTF::String keyString, JSC::VM& vm, JSC::JSOb return JSC::makeSource(sourceCodeBuilder.toString(), JSC::SourceOrigin(), JSC::SourceTaintedOrigin::Untainted, keyString, WTF::TextPosition(), JSC::SourceProviderSourceType::Module); } +void Napi::NapiRefWeakHandleOwner::finalize(JSC::Handle, void* context) +{ + auto* weakValue = reinterpret_cast(context); + weakValue->callFinalizer(); } -class NapiRefWeakHandleOwner final : public JSC::WeakHandleOwner { -public: - void finalize(JSC::Handle, void* context) final - { - auto* weakValue = reinterpret_cast(context); - - auto finalizer = weakValue->finalizer; - if (finalizer.finalize_cb) { - weakValue->finalizer.finalize_cb = nullptr; - finalizer.call(weakValue->globalObject.get(), weakValue->data); - } - } -}; - -static NapiRefWeakHandleOwner& weakValueHandleOwner() +void Napi::NapiRefSelfDeletingWeakHandleOwner::finalize(JSC::Handle, void* context) { - static NeverDestroyed jscWeakValueHandleOwner; - return jscWeakValueHandleOwner; + auto* weakValue = reinterpret_cast(context); + weakValue->callFinalizer(); + delete weakValue; } -void NapiFinalizer::call(JSC::JSGlobalObject* globalObject, void* data) +static uint32_t getPropertyAttributes(napi_property_descriptor prop) { - if (this->finalize_cb) { - NAPI_LOG_CURRENT_FUNCTION; - this->finalize_cb(toNapi(globalObject), data, this->finalize_hint); - } -} - -void NapiRef::ref() -{ - ++refCount; - if (refCount == 1 && !weakValueRef.isClear()) { - auto& vm = globalObject.get()->vm(); - strongRef.set(vm, weakValueRef.get()); - - // isSet() will return always true after being set once - // We cannot rely on isSet() to check if the value is set we need to use isClear() - // .setString/.setObject/.setPrimitive will assert fail if called more than once (even after clear()) - // We should not clear the weakValueRef here because we need to keep it if we call NapiRef::unref() - // so we can call the finalizer - } -} - -void NapiRef::unref() -{ - bool clear = refCount == 1; - refCount = refCount > 0 ? refCount - 1 : 0; - if (clear) { - // we still dont clean weakValueRef so we can ref it again using NapiRef::ref() if the GC didn't collect it - // and use it to call the finalizer when GC'd - strongRef.clear(); - } -} - -void NapiRef::clear() -{ - this->finalizer.call(this->globalObject.get(), this->data); - this->globalObject.clear(); - this->weakValueRef.clear(); - this->strongRef.clear(); -} - -// namespace Napi { -// class Reference -// } - -static uint32_t getPropertyAttributes(napi_property_attributes attributes_) -{ - const uint32_t attributes = static_cast(attributes_); uint32_t result = 0; + const uint32_t attributes = static_cast(prop.attributes); + if (!(attributes & static_cast(napi_key_configurable))) { result |= JSC::PropertyAttribute::DontDelete; } @@ -334,166 +276,49 @@ static uint32_t getPropertyAttributes(napi_property_attributes attributes_) result |= JSC::PropertyAttribute::DontEnum; } - // if (!(attributes & napi_key_writable)) { - // // result |= JSC::PropertyAttribute::ReadOnly; - // } + if (!(attributes & napi_key_writable || prop.setter != nullptr)) { + result |= JSC::PropertyAttribute::ReadOnly; + } return result; } -static uint32_t getPropertyAttributes(napi_property_descriptor prop) -{ - uint32_t result = getPropertyAttributes(prop.attributes); - - // if (!(prop.getter && !prop.setter)) { - // result |= JSC::PropertyAttribute::ReadOnly; - // } - - return result; -} - -NapiWeakValue::~NapiWeakValue() -{ - clear(); -} - -void NapiWeakValue::clear() -{ - switch (m_tag) { - case WeakTypeTag::Cell: { - m_value.cell.clear(); - break; - } - case WeakTypeTag::String: { - m_value.string.clear(); - break; - } - default: { - break; - } - } - - m_tag = WeakTypeTag::NotSet; -} - -bool NapiWeakValue::isClear() const -{ - return m_tag == WeakTypeTag::NotSet; -} - -void NapiWeakValue::setPrimitive(JSValue value) -{ - switch (m_tag) { - case WeakTypeTag::Cell: { - m_value.cell.clear(); - break; - } - case WeakTypeTag::String: { - m_value.string.clear(); - break; - } - default: { - break; - } - } - m_tag = WeakTypeTag::Primitive; - m_value.primitive = value; -} - -void NapiWeakValue::set(JSValue value, WeakHandleOwner& owner, void* context) -{ - if (value.isCell()) { - auto* cell = value.asCell(); - if (cell->isString()) { - setString(jsCast(cell), owner, context); - } else { - setCell(cell, owner, context); - } - } else { - setPrimitive(value); - } -} - -void NapiWeakValue::setCell(JSCell* cell, WeakHandleOwner& owner, void* context) -{ - switch (m_tag) { - case WeakTypeTag::Cell: { - m_value.cell.clear(); - break; - } - case WeakTypeTag::String: { - m_value.string.clear(); - break; - } - default: { - break; - } - } - - m_value.cell = JSC::Weak(cell, &owner, context); - m_tag = WeakTypeTag::Cell; -} - -void NapiWeakValue::setString(JSString* string, WeakHandleOwner& owner, void* context) -{ - switch (m_tag) { - case WeakTypeTag::Cell: { - m_value.cell.clear(); - break; - } - default: { - break; - } - } - - m_value.string = JSC::Weak(string, &owner, context); - m_tag = WeakTypeTag::String; -} - class NAPICallFrame { public: - NAPICallFrame(const JSC::ArgList args, void* dataPtr) - : m_args(args) + NAPICallFrame(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame, void* dataPtr, JSValue storedNewTarget) + : NAPICallFrame(globalObject, callFrame, dataPtr) + { + m_storedNewTarget = storedNewTarget; + m_isConstructorCall = !m_storedNewTarget.isEmpty(); + } + + NAPICallFrame(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame, void* dataPtr) + : m_callFrame(callFrame) , m_dataPtr(dataPtr) { - } - - JSC::JSValue thisValue() const - { - return m_args.at(0); - } - - static constexpr uintptr_t NAPICallFramePtrTag = static_cast(1) << 63; - - static bool isNAPICallFramePtr(uintptr_t ptr) - { - return ptr & NAPICallFramePtrTag; - } - - static uintptr_t tagNAPICallFramePtr(uintptr_t ptr) - { - return ptr | NAPICallFramePtrTag; - } - - static napi_callback_info toNapiCallbackInfo(NAPICallFrame& frame) - { - return reinterpret_cast(tagNAPICallFramePtr(reinterpret_cast(&frame))); - } - - static std::optional get(JSC::CallFrame* callFrame) - { - uintptr_t ptr = reinterpret_cast(callFrame); - if (!isNAPICallFramePtr(ptr)) { - return std::nullopt; + // Node-API function calls always run in "sloppy mode," even if the JS side is in strict + // mode. So if `this` is null or undefined, we use globalThis instead; otherwise, we convert + // `this` to an object. + // TODO change to global? or find another way to avoid JSGlobalProxy + JSC::JSObject* jscThis = globalObject->globalThis(); + if (!m_callFrame->thisValue().isUndefinedOrNull()) { + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); + jscThis = m_callFrame->thisValue().toObject(globalObject); + // https://tc39.es/ecma262/#sec-toobject + // toObject only throws for undefined and null, which we checked for + scope.assertNoException(); } - - ptr &= ~NAPICallFramePtrTag; - return { reinterpret_cast(ptr) }; + m_callFrame->setThisValue(jscThis); } - ALWAYS_INLINE const JSC::ArgList& args() const + JSValue thisValue() const { - return m_args; + return m_callFrame->thisValue(); + } + + napi_callback_info toNapi() + { + return reinterpret_cast(this); } ALWAYS_INLINE void* dataPtr() const @@ -501,142 +326,64 @@ public: return m_dataPtr; } - static void extract(NAPICallFrame& callframe, size_t* argc, // [in-out] Specifies the size of the provided argv array - // and receives the actual count of args. + void extract(size_t* argc, // [in-out] Specifies the size of the provided argv array + // and receives the actual count of args. napi_value* argv, // [out] Array of values napi_value* this_arg, // [out] Receives the JS 'this' arg for the call void** data, Zig::GlobalObject* globalObject) { if (this_arg != nullptr) { - *this_arg = toNapi(callframe.thisValue(), globalObject); + *this_arg = ::toNapi(m_callFrame->thisValue(), globalObject); } if (data != nullptr) { - *data = callframe.dataPtr(); + *data = dataPtr(); } size_t maxArgc = 0; if (argc != nullptr) { maxArgc = *argc; - *argc = callframe.args().size() - 1; + *argc = m_callFrame->argumentCount(); } if (argv != nullptr) { - size_t realArgCount = callframe.args().size() - 1; - - size_t overflow = maxArgc > realArgCount ? maxArgc - realArgCount : 0; - realArgCount = realArgCount < maxArgc ? realArgCount : maxArgc; - - if (realArgCount > 0) { - memcpy(argv, callframe.args().data() + 1, sizeof(napi_value) * realArgCount); - argv += realArgCount; - } - - if (overflow > 0) { - while (overflow--) { - *argv = toNapi(jsUndefined(), globalObject); - argv++; - } + for (size_t i = 0; i < maxArgc; i++) { + // OK if we overflow argumentCount(), because argument() returns JS undefined + // for OOB which is what we want + argv[i] = ::toNapi(m_callFrame->argument(i), globalObject); } } } - JSC::JSValue newTarget; + JSValue newTarget() + { + if (!m_isConstructorCall) { + return JSValue(); + } + + if (m_storedNewTarget.isUndefined()) { + // napi_get_new_target: + // "This API returns the new.target of the constructor call. If the current callback + // is not a constructor call, the result is NULL." + // they mean a null pointer, not JavaScript null + return JSValue(); + } else { + return m_storedNewTarget; + } + } private: - const JSC::ArgList m_args; + JSC::CallFrame* m_callFrame; void* m_dataPtr; + JSValue m_storedNewTarget; + bool m_isConstructorCall = false; }; -#define ADDRESS_OF_THIS_VALUE_IN_CALLFRAME(callframe) callframe->addressOfArgumentsStart() - 1 - -class NAPIFunction : public JSC::JSFunction { - -public: - using Base = JSC::JSFunction; - static constexpr unsigned StructureFlags = Base::StructureFlags; - - static JSC_HOST_CALL_ATTRIBUTES JSC::EncodedJSValue call(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callframe) - { - ASSERT(jsCast(callframe->jsCallee())); - auto* function = static_cast(callframe->jsCallee()); - auto* env = toNapi(globalObject); - ASSERT(function->m_method); - auto* callback = reinterpret_cast(function->m_method); - auto& vm = JSC::getVM(globalObject); - - MarkedArgumentBufferWithSize<12> args; - size_t argc = callframe->argumentCount() + 1; - args.fill(vm, argc, [&](auto* slot) { - memcpy(slot, ADDRESS_OF_THIS_VALUE_IN_CALLFRAME(callframe), sizeof(JSC::JSValue) * argc); - }); - NAPICallFrame frame(JSC::ArgList(args), function->m_dataPtr); - - auto scope = DECLARE_THROW_SCOPE(vm); - Bun::NapiHandleScope handleScope(jsCast(globalObject)); - - auto result = callback(env, NAPICallFrame::toNapiCallbackInfo(frame)); - - RELEASE_AND_RETURN(scope, JSC::JSValue::encode(toJS(result))); - } - - NAPIFunction(JSC::VM& vm, JSC::NativeExecutable* exec, JSGlobalObject* globalObject, Structure* structure, Zig::CFFIFunction method, void* dataPtr) - : Base(vm, exec, globalObject, structure) - , m_method(method) - , m_dataPtr(dataPtr) - { - } - - static NAPIFunction* create(JSC::VM& vm, Zig::GlobalObject* globalObject, unsigned length, const WTF::String& name, Zig::CFFIFunction method, void* dataPtr) - { - - auto* structure = globalObject->NAPIFunctionStructure(); - NativeExecutable* executable = vm.getHostFunction(&NAPIFunction::call, ImplementationVisibility::Public, &NAPIFunction::call, name); - NAPIFunction* functionObject = new (NotNull, JSC::allocateCell(vm)) NAPIFunction(vm, executable, globalObject, structure, method, dataPtr); - functionObject->finishCreation(vm, executable, length, name); - return functionObject; - } - - void* m_dataPtr = nullptr; - Zig::CFFIFunction m_method = nullptr; - - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForNAPIFunction.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForNAPIFunction = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForNAPIFunction.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForNAPIFunction = std::forward(space); }); - } - - DECLARE_EXPORT_INFO; - - static Structure* createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) - { - ASSERT(globalObject); - return Structure::create(vm, globalObject, prototype, TypeInfo(JSFunctionType, StructureFlags), info()); - } -}; - -const JSC::ClassInfo NAPIFunction::s_info = { "Function"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(NAPIFunction) }; - -Structure* Zig::createNAPIFunctionStructure(VM& vm, JSC::JSGlobalObject* globalObject) +static void defineNapiProperty(napi_env env, JSC::JSObject* to, napi_property_descriptor property, bool isInstance, JSC::ThrowScope& scope) { - ASSERT(globalObject); - auto* prototype = globalObject->functionPrototype(); - return NAPIFunction::createStructure(vm, globalObject, prototype); -} - -static void defineNapiProperty(Zig::GlobalObject* globalObject, JSC::JSObject* to, void* inheritedDataPtr, napi_property_descriptor property, bool isInstance, JSC::ThrowScope& scope) -{ - auto& vm = JSC::getVM(globalObject); + Zig::GlobalObject* globalObject = env->globalObject(); + JSC::VM& vm = JSC::getVM(globalObject); void* dataPtr = property.data; - if (!dataPtr) { - dataPtr = inheritedDataPtr; - } auto getPropertyName = [&]() -> JSC::Identifier { if (property.utf8name != nullptr) { @@ -661,45 +408,38 @@ static void defineNapiProperty(Zig::GlobalObject* globalObject, JSC::JSObject* t } if (property.method) { - JSC::JSValue value; - auto method = reinterpret_cast(property.method); - - auto* function = NAPIFunction::create(vm, globalObject, 1, propertyName.isSymbol() ? String() : propertyName.string(), method, dataPtr); - value = JSC::JSValue(function); + WTF::String name; + if (!propertyName.isSymbol()) { + name = propertyName.string(); + } + JSValue value = NapiClass::create(vm, env, name, property.method, dataPtr, 0, nullptr); to->putDirect(vm, propertyName, value, getPropertyAttributes(property)); return; } if (property.getter != nullptr || property.setter != nullptr) { - JSC::JSObject* getter = nullptr; JSC::JSObject* setter = nullptr; - auto getterProperty = reinterpret_cast(property.getter); - auto setterProperty = reinterpret_cast(property.setter); - if (getterProperty) { - getter = NAPIFunction::create(vm, globalObject, 0, makeString("get "_s, propertyName.isSymbol() ? String() : propertyName.string()), getterProperty, dataPtr); + if (property.getter) { + auto name = makeString("get "_s, propertyName.isSymbol() ? String() : propertyName.string()); + getter = NapiClass::create(vm, env, name, property.getter, dataPtr, 0, nullptr); } else { JSC::JSNativeStdFunction* getterFunction = JSC::JSNativeStdFunction::create( - globalObject->vm(), globalObject, 0, String(), [](JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) -> JSC::EncodedJSValue { - return JSC::JSValue::encode(JSC::jsUndefined()); + JSC::getVM(globalObject), globalObject, 0, String(), [](JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) -> JSC::EncodedJSValue { + return JSValue::encode(JSC::jsUndefined()); }); getter = getterFunction; } - if (setterProperty) { - setter = NAPIFunction::create(vm, globalObject, 1, makeString("set "_s, propertyName.isSymbol() ? String() : propertyName.string()), setterProperty, dataPtr); - } else { - JSC::JSNativeStdFunction* setterFunction = JSC::JSNativeStdFunction::create( - globalObject->vm(), globalObject, 1, String(), [](JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) -> JSC::EncodedJSValue { - return JSC::JSValue::encode(JSC::jsBoolean(true)); - }); - setter = setterFunction; + if (property.setter) { + auto name = makeString("set "_s, propertyName.isSymbol() ? String() : propertyName.string()); + setter = NapiClass::create(vm, env, name, property.setter, dataPtr, 0, nullptr); } auto getterSetter = JSC::GetterSetter::create(vm, globalObject, getter, setter); - to->putDirectAccessor(globalObject, propertyName, getterSetter, JSC::PropertyAttribute::Accessor | 0); + to->putDirectAccessor(globalObject, propertyName, getterSetter, PropertyAttribute::Accessor | getPropertyAttributes(property)); } else { JSC::JSValue value = toJS(property.value); @@ -707,7 +447,8 @@ static void defineNapiProperty(Zig::GlobalObject* globalObject, JSC::JSObject* t value = JSC::jsUndefined(); } - to->putDirect(vm, propertyName, value, getPropertyAttributes(property)); + PropertyDescriptor descriptor(value, getPropertyAttributes(property)); + to->methodTable()->defineOwnProperty(to, globalObject, propertyName, descriptor, false); } } @@ -720,7 +461,6 @@ extern "C" napi_status napi_set_property(napi_env env, napi_value target, NAPI_CHECK_ARG(env, value); JSValue targetValue = toJS(target); - NAPI_RETURN_EARLY_IF_FALSE(env, targetValue.isObject(), napi_object_expected); auto globalObject = toJS(env); auto* object = targetValue.toObject(globalObject); @@ -735,14 +475,49 @@ extern "C" napi_status napi_set_property(napi_env env, napi_value target, JSValue jsValue = toJS(value); - bool putResult = object->put(object, globalObject, identifier, jsValue, slot); - NAPI_RETURN_IF_EXCEPTION(env); - if (!putResult) return napi_set_last_error(env, napi_generic_failure); + // Ignoring the return value matches JS sloppy mode + (void)object->methodTable()->put(object, globalObject, identifier, jsValue, slot); + NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env); +} - // we should have returned if there is an exception +extern "C" napi_status napi_set_element(napi_env env, napi_value object_, + uint32_t index, napi_value value_) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ARG(env, object_); + NAPI_CHECK_ARG(env, value_); + + JSValue object = toJS(object_); + JSValue value = toJS(value_); + NAPI_RETURN_EARLY_IF_FALSE(env, !object.isEmpty() && !value.isEmpty(), napi_invalid_arg); + + JSObject* jsObject = object.getObject(); + NAPI_RETURN_EARLY_IF_FALSE(env, jsObject, napi_array_expected); + + jsObject->methodTable()->putByIndex(jsObject, toJS(env), index, value, false); + NAPI_RETURN_IF_EXCEPTION(env); NAPI_RETURN_SUCCESS(env); } +extern "C" napi_status napi_has_element(napi_env env, napi_value object_, + uint32_t index, bool* result) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ARG(env, object_); + NAPI_CHECK_ARG(env, result); + + JSValue object = toJS(object_); + NAPI_RETURN_EARLY_IF_FALSE(env, !object.isEmpty(), napi_invalid_arg); + + JSObject* jsObject = object.getObject(); + NAPI_RETURN_EARLY_IF_FALSE(env, jsObject, napi_array_expected); + + bool has_property = jsObject->hasProperty(toJS(env), index); + *result = has_property; + + NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env); +} + extern "C" napi_status napi_has_property(napi_env env, napi_value object, napi_value key, bool* result) { @@ -831,7 +606,9 @@ extern "C" napi_status napi_has_own_property(napi_env env, napi_value object, auto* target = toJS(object).toObject(globalObject); NAPI_RETURN_IF_EXCEPTION(env); - auto keyProp = toJS(key); + JSValue keyProp = toJS(key); + NAPI_RETURN_EARLY_IF_FALSE(env, keyProp.isString() || keyProp.isSymbol(), napi_name_expected); + *result = target->hasOwnProperty(globalObject, JSC::PropertyName(keyProp.toPropertyKey(globalObject))); NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env); } @@ -852,16 +629,16 @@ extern "C" napi_status napi_set_named_property(napi_env env, napi_value object, auto target = toJS(object).toObject(globalObject); NAPI_RETURN_IF_EXCEPTION(env); - JSC::JSValue jsValue = toJS(value); + JSValue jsValue = toJS(value); JSC::EnsureStillAliveScope ensureAlive(jsValue); JSC::EnsureStillAliveScope ensureAlive2(target); auto nameStr = WTF::String::fromUTF8({ utf8name, strlen(utf8name) }); auto identifier = JSC::Identifier::fromString(vm, WTFMove(nameStr)); - PutPropertySlot slot(target, true); + PutPropertySlot slot(target, false); - target->put(target, globalObject, identifier, jsValue, slot); + target->methodTable()->put(target, globalObject, identifier, jsValue, slot); NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env); } @@ -871,6 +648,7 @@ extern "C" napi_status napi_create_arraybuffer(napi_env env, { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); Zig::GlobalObject* globalObject = toJS(env); @@ -970,7 +748,7 @@ extern "C" napi_status napi_get_named_property(napi_env env, napi_value object, NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env); } -extern "C" napi_status +extern "C" JS_EXPORT napi_status node_api_create_external_string_latin1(napi_env env, char* str, size_t length, @@ -985,15 +763,17 @@ node_api_create_external_string_latin1(napi_env env, NAPI_CHECK_ARG(env, result); length = length == NAPI_AUTO_LENGTH ? strlen(str) : length; + // WTF::ExternalStringImpl does not allow creating empty strings, so we have this limitation for now. + NAPI_RETURN_EARLY_IF_FALSE(env, length > 0, napi_invalid_arg); Ref impl = WTF::ExternalStringImpl::create({ reinterpret_cast(str), static_cast(length) }, finalize_hint, [finalize_callback, env](void* hint, void* str, unsigned length) { if (finalize_callback) { - NAPI_LOG("finalizer"); + NAPI_LOG("latin1 string finalizer"); finalize_callback(env, str, hint); } }); Zig::GlobalObject* globalObject = toJS(env); - JSString* out = JSC::jsString(globalObject->vm(), WTF::String(impl.get())); + JSString* out = JSC::jsString(JSC::getVM(globalObject), WTF::String(impl.get())); ensureStillAliveHere(out); *result = toNapi(out, globalObject); ensureStillAliveHere(out); @@ -1005,7 +785,7 @@ node_api_create_external_string_latin1(napi_env env, NAPI_RETURN_SUCCESS(env); } -extern "C" napi_status +extern "C" JS_EXPORT napi_status node_api_create_external_string_utf16(napi_env env, char16_t* str, size_t length, @@ -1020,15 +800,17 @@ node_api_create_external_string_utf16(napi_env env, NAPI_CHECK_ARG(env, result); length = length == NAPI_AUTO_LENGTH ? std::char_traits::length(str) : length; + // WTF::ExternalStringImpl does not allow creating empty strings, so we have this limitation for now. + NAPI_RETURN_EARLY_IF_FALSE(env, length > 0, napi_invalid_arg); Ref impl = WTF::ExternalStringImpl::create({ reinterpret_cast(str), static_cast(length) }, finalize_hint, [finalize_callback, env](void* hint, void* str, unsigned length) { if (finalize_callback) { - NAPI_LOG("finalizer"); + NAPI_LOG("utf16 string finalizer"); finalize_callback(env, str, hint); } }); Zig::GlobalObject* globalObject = toJS(env); - JSString* out = JSC::jsString(globalObject->vm(), WTF::String(impl.get())); + JSString* out = JSC::jsString(JSC::getVM(globalObject), WTF::String(impl.get())); ensureStillAliveHere(out); *result = toNapi(out, globalObject); ensureStillAliveHere(out); @@ -1038,8 +820,9 @@ node_api_create_external_string_utf16(napi_env env, extern "C" size_t Bun__napi_module_register_count; extern "C" void napi_module_register(napi_module* mod) { - auto* globalObject = defaultGlobalObject(); - auto& vm = JSC::getVM(globalObject); + Zig::GlobalObject* globalObject = defaultGlobalObject(); + napi_env env = globalObject->makeNapiEnv(*mod); + JSC::VM& vm = JSC::getVM(globalObject); auto keyStr = WTF::String::fromUTF8(mod->nm_modname); globalObject->napiModuleRegisterCallCount++; Bun__napi_module_register_count++; @@ -1068,7 +851,15 @@ extern "C" void napi_module_register(napi_module* mod) JSC::Strong strongObject = { vm, object }; Bun::NapiHandleScope handleScope(globalObject); - JSValue resultValue = toJS(mod->nm_register_func(toNapi(globalObject), toNapi(object, globalObject))); + JSValue resultValue; + + if (mod->nm_register_func) { + resultValue = toJS(mod->nm_register_func(env, toNapi(object, globalObject))); + } else { + JSValue errorInstance = createError(globalObject, makeString("Module has no declared entry point."_s)); + globalObject->m_pendingNapiModuleAndExports[0].set(vm, globalObject, errorInstance); + return; + } RETURN_IF_EXCEPTION(scope, void()); @@ -1087,7 +878,7 @@ extern "C" void napi_module_register(napi_module* mod) auto* meta = new Bun::NapiModuleMeta(globalObject->m_pendingNapiModuleDlopenHandle); // TODO: think about the finalizer here - Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, nullptr); + Bun::NapiExternal* napi_external = Bun::NapiExternal::create(vm, globalObject->NapiExternalStructure(), meta, nullptr, env, nullptr); bool success = resultValue.getObject()->putDirect(vm, WebCore::builtinNames(vm).napiDlopenHandlePrivateName(), napi_external, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::ReadOnly); ASSERT(success); @@ -1104,6 +895,15 @@ extern "C" void napi_module_register(napi_module* mod) globalObject->m_pendingNapiModuleAndExports[1].set(vm, globalObject, object); } +static void wrap_cleanup(napi_env env, void* data, void* hint) +{ + auto* ref = reinterpret_cast(data); + ASSERT(ref->boundCleanup != nullptr); + ref->boundCleanup->deactivate(env); + ref->boundCleanup = nullptr; + ref->callFinalizer(); +} + static inline NapiRef* getWrapContentsIfExists(VM& vm, JSGlobalObject* globalObject, JSObject* object) { if (auto* napi_instance = jsDynamicCast(object)) { @@ -1150,23 +950,25 @@ extern "C" napi_status napi_wrap(napi_env env, NAPI_RETURN_EARLY_IF_FALSE(env, existing_wrap == nullptr, napi_invalid_arg); // create a new weak reference (refcount 0) - auto* ref = new NapiRef(globalObject, 0); - ref->weakValueRef.set(jsc_value, weakValueHandleOwner(), ref); - - ref->finalizer.finalize_cb = finalize_cb; - ref->finalizer.finalize_hint = finalize_hint; - ref->data = native_object; + auto* ref = new NapiRef(env, 0, Bun::NapiFinalizer { finalize_cb, finalize_hint }); + // In case the ref's finalizer is never called, we'll add a finalizer to execute on exit. + const auto& bound_cleanup = env->addFinalizer(wrap_cleanup, native_object, ref); + ref->boundCleanup = &bound_cleanup; + ref->nativeObject = 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); + auto* external = Bun::NapiExternal::create(JSC::getVM(globalObject), globalObject->NapiExternalStructure(), ref, nullptr, env, nullptr); jsc_object->putDirect(vm, propertyName, JSValue(external)); } if (result) { + ref->weakValueRef.set(jsc_value, Napi::NapiRefWeakHandleOwner::weakValueHandleOwner(), ref); *result = toNapi(ref); + } else { + ref->weakValueRef.set(jsc_value, Napi::NapiRefSelfDeletingWeakHandleOwner::weakValueHandleOwner(), ref); } NAPI_RETURN_SUCCESS(env); @@ -1184,7 +986,7 @@ extern "C" napi_status napi_remove_wrap(napi_env env, napi_value js_object, // may be null auto* napi_instance = jsDynamicCast(jsc_object); - auto* globalObject = toJS(env); + Zig::GlobalObject* globalObject = toJS(env); auto& vm = JSC::getVM(globalObject); NapiRef* ref = getWrapContentsIfExists(vm, globalObject, jsc_object); NAPI_RETURN_EARLY_IF_FALSE(env, ref, napi_invalid_arg); @@ -1197,9 +999,10 @@ extern "C" napi_status napi_remove_wrap(napi_env env, napi_value js_object, } if (result) { - *result = ref->data; + *result = ref->nativeObject; } - ref->finalizer.finalize_cb = nullptr; + + ref->finalizer.clear(); // 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. @@ -1218,14 +1021,12 @@ extern "C" napi_status napi_unwrap(napi_env env, napi_value js_object, JSObject* jsc_object = jsc_value.getObject(); NAPI_RETURN_EARLY_IF_FALSE(env, jsc_object, napi_object_expected); - auto* globalObject = toJS(env); + Zig::GlobalObject* globalObject = toJS(env); auto& vm = JSC::getVM(globalObject); NapiRef* ref = getWrapContentsIfExists(vm, globalObject, jsc_object); NAPI_RETURN_EARLY_IF_FALSE(env, ref, napi_invalid_arg); - if (result) { - *result = ref->data; - } + *result = ref->nativeObject; NAPI_RETURN_SUCCESS(env); } @@ -1239,18 +1040,17 @@ extern "C" napi_status napi_create_function(napi_env env, const char* utf8name, NAPI_CHECK_ARG(env, cb); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); auto name = WTF::String(); if (utf8name != nullptr) { name = WTF::String::fromUTF8({ utf8name, length == NAPI_AUTO_LENGTH ? strlen(utf8name) : length }); } - auto method = reinterpret_cast(cb); - auto* function = NAPIFunction::create(vm, globalObject, length, name, method, data); + auto function = NapiClass::create(vm, env, name, cb, data, 0, nullptr); ASSERT(function->isCallable()); - *result = toNapi(JSC::JSValue(function), globalObject); + *result = toNapi(JSValue(function), globalObject); NAPI_RETURN_SUCCESS(env); } @@ -1267,72 +1067,10 @@ extern "C" napi_status napi_get_cb_info( NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, cbinfo); - JSC::CallFrame* callFrame = reinterpret_cast(cbinfo); + auto* callFrame = reinterpret_cast(cbinfo); Zig::GlobalObject* globalObject = toJS(env); - if (NAPICallFrame* frame = NAPICallFrame::get(callFrame).value_or(nullptr)) { - NAPICallFrame::extract(*frame, argc, argv, this_arg, data, globalObject); - NAPI_RETURN_SUCCESS(env); - } - - auto inputArgsCount = argc == nullptr ? 0 : *argc; - - // napi expects arguments to be copied into the argv array. - if (inputArgsCount > 0) { - auto outputArgsCount = callFrame->argumentCount(); - auto argsToCopy = inputArgsCount < outputArgsCount ? inputArgsCount : outputArgsCount; - *argc = argsToCopy; - - memcpy(argv, callFrame->addressOfArgumentsStart(), argsToCopy * sizeof(JSC::JSValue)); - - for (size_t i = outputArgsCount; i < inputArgsCount; i++) { - argv[i] = toNapi(JSC::jsUndefined(), globalObject); - } - } - - JSC::JSValue thisValue = callFrame->thisValue(); - - if (this_arg != nullptr) { - *this_arg = toNapi(thisValue, globalObject); - } - - if (data != nullptr) { - JSC::JSValue callee = JSC::JSValue(callFrame->jsCallee()); - - if (Zig::JSFFIFunction* ffiFunction = JSC::jsDynamicCast(callee)) { - *data = ffiFunction->dataPtr; - } else if (auto* proto = JSC::jsDynamicCast(callee)) { - NapiRef* ref = proto->napiRef; - if (ref) { - *data = ref->data; - } - } else if (auto* proto = JSC::jsDynamicCast(callee)) { - void* local = proto->dataPtr; - if (!local) { - NapiRef* ref = nullptr; - if (ref) { - *data = ref->data; - } - } else { - *data = local; - } - } else if (auto* proto = JSC::jsDynamicCast(thisValue)) { - NapiRef* ref = proto->napiRef; - if (ref) { - *data = ref->data; - } - } else if (auto* proto = JSC::jsDynamicCast(thisValue)) { - void* local = proto->dataPtr; - if (local) { - *data = local; - } - } else if (auto* proto = JSC::jsDynamicCast(thisValue)) { - *data = proto->value(); - } else { - *data = nullptr; - } - } - + callFrame->extract(argc, argv, this_arg, data, globalObject); NAPI_RETURN_SUCCESS(env); } @@ -1340,34 +1078,27 @@ extern "C" napi_status napi_define_properties(napi_env env, napi_value object, size_t property_count, const napi_property_descriptor* properties) { - NAPI_PREAMBLE(env); + NAPI_PREAMBLE_NO_THROW_SCOPE(env); NAPI_CHECK_ARG(env, object); NAPI_RETURN_EARLY_IF_FALSE(env, properties || property_count == 0, napi_invalid_arg); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); - JSC::JSValue objectValue = toJS(object); + JSValue objectValue = toJS(object); JSC::JSObject* objectObject = objectValue.getObject(); NAPI_RETURN_EARLY_IF_FALSE(env, objectObject, napi_object_expected); auto throwScope = DECLARE_THROW_SCOPE(vm); - void* inheritedDataPtr = nullptr; - if (NapiPrototype* proto = jsDynamicCast(objectValue)) { - inheritedDataPtr = proto->napiRef ? proto->napiRef->data : nullptr; - } else if (NapiClass* proto = jsDynamicCast(objectValue)) { - inheritedDataPtr = proto->dataPtr; - } - for (size_t i = 0; i < property_count; i++) { - defineNapiProperty(globalObject, objectObject, inheritedDataPtr, properties[i], true, throwScope); + defineNapiProperty(env, objectObject, properties[i], true, throwScope); RETURN_IF_EXCEPTION(throwScope, napi_set_last_error(env, napi_pending_exception)); } throwScope.release(); - NAPI_RETURN_SUCCESS(env); + return napi_set_last_error(env, napi_ok); } static JSC::ErrorInstance* createErrorWithCode(JSC::JSGlobalObject* globalObject, const WTF::String& code, const WTF::String& message, JSC::ErrorType type) @@ -1379,7 +1110,7 @@ static JSC::ErrorInstance* createErrorWithCode(JSC::JSGlobalObject* globalObject auto& vm = JSC::getVM(globalObject); // we don't call JSC::createError() as it asserts the message is not an empty string "" - auto* error = JSC::ErrorInstance::create(globalObject->vm(), globalObject->errorStructure(type), message, JSValue(), nullptr, RuntimeType::TypeNothing, type); + auto* error = JSC::ErrorInstance::create(JSC::getVM(globalObject), globalObject->errorStructure(type), message, JSValue(), nullptr, RuntimeType::TypeNothing, type); if (!code.isNull()) { error->putDirect(vm, WebCore::builtinNames(vm).codePublicName(), JSC::jsString(vm, code), 0); } @@ -1449,19 +1180,21 @@ extern "C" napi_status napi_create_reference(napi_env env, napi_value value, napi_ref* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, value); JSC::JSValue val = toJS(value); - NAPI_RETURN_EARLY_IF_FALSE(env, val.isCell(), napi_object_expected); - Zig::GlobalObject* globalObject = toJS(env); + bool can_be_weak = true; - auto* ref = new NapiRef(globalObject, initial_refcount); - if (initial_refcount > 0) { - ref->strongRef.set(globalObject->vm(), val); + if (!(val.isObject() || val.isCallable() || val.isSymbol())) { + NAPI_RETURN_EARLY_IF_FALSE(env, env->napiModule().nm_version == NAPI_VERSION_EXPERIMENTAL, napi_invalid_arg); + can_be_weak = false; } - ref->weakValueRef.set(val, weakValueHandleOwner(), ref); + + auto* ref = new NapiRef(env, initial_refcount, Bun::NapiFinalizer {}); + ref->setValueInitial(val, can_be_weak); *result = toNapi(ref); NAPI_RETURN_SUCCESS(env); @@ -1472,7 +1205,7 @@ extern "C" void napi_set_ref(NapiRef* ref, JSC__JSValue val_) NAPI_LOG_CURRENT_FUNCTION; JSC::JSValue val = JSC::JSValue::decode(val_); if (val) { - ref->strongRef.set(ref->globalObject->vm(), val); + ref->strongRef.set(JSC::getVM(&*ref->globalObject), val); } else { ref->strongRef.clear(); } @@ -1485,27 +1218,50 @@ extern "C" napi_status napi_add_finalizer(napi_env env, napi_value js_object, napi_ref* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, js_object); NAPI_CHECK_ARG(env, finalize_cb); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); JSC::JSValue objectValue = toJS(js_object); JSC::JSObject* object = objectValue.getObject(); NAPI_RETURN_EARLY_IF_FALSE(env, object, napi_object_expected); - vm.heap.addFinalizer(object, [finalize_cb, env, native_object, finalize_hint](JSCell* cell) -> void { - NAPI_LOG("finalizer %p", finalize_hint); - finalize_cb(env, native_object, finalize_hint); - }); + if (result) { + // If they're expecting a Ref, use the ref. + auto* ref = new NapiRef(env, 0, Bun::NapiFinalizer { finalize_cb, finalize_hint }); + // TODO(@heimskr): consider detecting whether the value can't be weak, as we do in napi_create_reference. + ref->setValueInitial(object, true); + ref->nativeObject = native_object; + *result = toNapi(ref); + } else { + // Otherwise, it's cheaper to just call .addFinalizer. + vm.heap.addFinalizer(object, [env, finalize_cb, native_object, finalize_hint](JSCell* cell) -> void { + NAPI_LOG("finalizer %p", finalize_hint); + env->doFinalizer(finalize_cb, native_object, finalize_hint); + }); + } NAPI_RETURN_SUCCESS(env); } +extern "C" JS_EXPORT napi_status node_api_post_finalizer(napi_env env, + napi_finalize finalize_cb, + void* finalize_data, + void* finalize_hint) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ARG(env, finalize_cb); + napi_internal_enqueue_finalizer(env, finalize_cb, finalize_data, finalize_hint); + NAPI_RETURN_SUCCESS(env); +} + extern "C" napi_status napi_reference_unref(napi_env env, napi_ref ref, uint32_t* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, ref); NapiRef* napiRef = toJS(ref); @@ -1523,6 +1279,7 @@ extern "C" napi_status napi_get_reference_value(napi_env env, napi_ref ref, napi_value* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, ref); NAPI_CHECK_ARG(env, result); NapiRef* napiRef = toJS(ref); @@ -1531,16 +1288,11 @@ extern "C" napi_status napi_get_reference_value(napi_env env, napi_ref ref, NAPI_RETURN_SUCCESS(env); } -extern "C" JSC__JSValue napi_get_reference_value_internal(NapiRef* napiRef) -{ - NAPI_LOG_CURRENT_FUNCTION; - return JSC::JSValue::encode(napiRef->value()); -} - extern "C" napi_status napi_reference_ref(napi_env env, napi_ref ref, uint32_t* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, ref); NapiRef* napiRef = toJS(ref); napiRef->ref(); @@ -1553,24 +1305,19 @@ extern "C" napi_status napi_reference_ref(napi_env env, napi_ref ref, extern "C" napi_status napi_delete_reference(napi_env env, napi_ref ref) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, ref); NapiRef* napiRef = toJS(ref); delete napiRef; NAPI_RETURN_SUCCESS(env); } -extern "C" void napi_delete_reference_internal(napi_ref ref) -{ - NAPI_LOG_CURRENT_FUNCTION; - NapiRef* napiRef = toJS(ref); - delete napiRef; -} - extern "C" napi_status napi_is_detached_arraybuffer(napi_env env, napi_value arraybuffer, bool* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, arraybuffer); NAPI_CHECK_ARG(env, result); @@ -1586,14 +1333,15 @@ extern "C" napi_status napi_detach_arraybuffer(napi_env env, napi_value arraybuffer) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); JSC::JSArrayBuffer* jsArrayBuffer = JSC::jsDynamicCast(toJS(arraybuffer)); NAPI_RETURN_EARLY_IF_FALSE(env, jsArrayBuffer, napi_arraybuffer_expected); auto* arrayBuffer = jsArrayBuffer->impl(); - if (!arrayBuffer->isDetached()) { + if (!arrayBuffer->isDetached() && arrayBuffer->isDetachable()) { arrayBuffer->detach(vm); } NAPI_RETURN_SUCCESS(env); @@ -1618,10 +1366,11 @@ extern "C" napi_status napi_adjust_external_memory(napi_env env, extern "C" napi_status napi_is_exception_pending(napi_env env, bool* result) { NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); auto globalObject = toJS(env); - auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); *result = scope.exception() != nullptr; // skip macros as they assume we made a throw scope in the preamble return napi_set_last_error(env, napi_ok); @@ -1631,15 +1380,16 @@ extern "C" napi_status napi_get_and_clear_last_exception(napi_env env, napi_value* result) { NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); if (UNLIKELY(!result)) { return napi_set_last_error(env, napi_invalid_arg); } auto globalObject = toJS(env); - auto scope = DECLARE_CATCH_SCOPE(globalObject->vm()); + auto scope = DECLARE_CATCH_SCOPE(JSC::getVM(globalObject)); if (scope.exception()) { - *result = toNapi(JSC::JSValue(scope.exception()->value()), globalObject); + *result = toNapi(JSValue(scope.exception()->value()), globalObject); } else { *result = toNapi(JSC::jsUndefined(), globalObject); } @@ -1654,7 +1404,7 @@ extern "C" napi_status napi_fatal_exception(napi_env env, NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, err); auto globalObject = toJS(env); - JSC::JSValue value = toJS(err); + JSValue value = toJS(err); JSC::JSObject* obj = value.getObject(); NAPI_RETURN_EARLY_IF_FALSE(env, obj && obj->isErrorInstance(), napi_invalid_arg); @@ -1667,10 +1417,10 @@ extern "C" napi_status napi_throw(napi_env env, napi_value error) { NAPI_PREAMBLE_NO_THROW_SCOPE(env); auto globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); auto throwScope = DECLARE_THROW_SCOPE(vm); - JSC::JSValue value = toJS(error); + JSValue value = toJS(error); if (value) { JSC::throwException(globalObject, throwScope, value); } else { @@ -1685,11 +1435,19 @@ extern "C" napi_status node_api_symbol_for(napi_env env, size_t length, napi_value* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); - NAPI_CHECK_ARG(env, utf8description); auto* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); + + if (utf8description == nullptr) { + if (length == 0) { + utf8description = ""; + } else { + NAPI_CHECK_ARG(env, utf8description); + } + } auto description = WTF::String::fromUTF8({ utf8description, length == NAPI_AUTO_LENGTH ? strlen(utf8description) : length }); *result = toNapi(JSC::Symbol::create(vm, vm.symbolRegistry().symbolForKey(description)), globalObject); @@ -1703,6 +1461,7 @@ extern "C" napi_status node_api_create_syntax_error(napi_env env, napi_value* result) { NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); return createErrorWithNapiValues(env, code, msg, JSC::ErrorType::SyntaxError, result); } @@ -1726,6 +1485,7 @@ extern "C" napi_status napi_create_type_error(napi_env env, napi_value code, napi_value* result) { NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); return createErrorWithNapiValues(env, code, msg, JSC::ErrorType::TypeError, result); } @@ -1734,6 +1494,7 @@ extern "C" napi_status napi_create_error(napi_env env, napi_value code, napi_value* result) { NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); return createErrorWithNapiValues(env, code, msg, JSC::ErrorType::Error, result); } extern "C" napi_status napi_throw_range_error(napi_env env, const char* code, @@ -1751,7 +1512,7 @@ extern "C" napi_status napi_object_freeze(napi_env env, napi_value object_value) NAPI_RETURN_EARLY_IF_FALSE(env, value.isObject(), napi_object_expected); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); JSC::JSObject* object = JSC::jsCast(value); // TODO is this check necessary? @@ -1769,7 +1530,7 @@ extern "C" napi_status napi_object_seal(napi_env env, napi_value object_value) NAPI_RETURN_EARLY_IF_FALSE(env, value.isObject(), napi_object_expected); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); JSC::JSObject* object = JSC::jsCast(value); // TODO is this check necessary? @@ -1783,8 +1544,10 @@ extern "C" napi_status napi_object_seal(napi_env env, napi_value object_value) extern "C" napi_status napi_get_global(napi_env env, napi_value* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); Zig::GlobalObject* globalObject = toJS(env); + // TODO change to global? or find another way to avoid JSGlobalProxy *result = toNapi(globalObject->globalThis(), globalObject); NAPI_RETURN_SUCCESS(env); } @@ -1794,6 +1557,7 @@ extern "C" napi_status napi_create_range_error(napi_env env, napi_value code, napi_value* result) { NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); return createErrorWithNapiValues(env, code, msg, JSC::ErrorType::RangeError, result); } @@ -1802,19 +1566,14 @@ extern "C" napi_status napi_get_new_target(napi_env env, napi_value* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); // handle: // - if they call this function when it was originally a getter/setter call // - if they call this function without a result NAPI_CHECK_ARG(env, cbinfo); NAPI_CHECK_ARG(env, result); - CallFrame* callFrame = reinterpret_cast(cbinfo); - - if (NAPICallFrame* frame = NAPICallFrame::get(callFrame).value_or(nullptr)) { - *result = toNapi(frame->newTarget, toJS(env)); - NAPI_RETURN_SUCCESS(env); - } - + auto* callFrame = reinterpret_cast(cbinfo); JSC::JSValue newTarget = callFrame->newTarget(); *result = toNapi(newTarget, toJS(env)); NAPI_RETURN_SUCCESS(env); @@ -1825,17 +1584,111 @@ extern "C" napi_status napi_create_dataview(napi_env env, size_t length, size_t byte_offset, napi_value* result) { - NAPI_PREAMBLE(env); + NAPI_PREAMBLE_NO_THROW_SCOPE(env); + Zig::GlobalObject* globalObject = toJS(env); + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); + RETURN_IF_EXCEPTION(scope, napi_set_last_error(env, napi_pending_exception)); NAPI_CHECK_ARG(env, arraybuffer); NAPI_CHECK_ARG(env, result); - JSC::JSValue arraybufferValue = toJS(arraybuffer); + JSValue arraybufferValue = toJS(arraybuffer); auto arraybufferPtr = JSC::jsDynamicCast(arraybufferValue); NAPI_RETURN_EARLY_IF_FALSE(env, arraybufferPtr, napi_arraybuffer_expected); - Zig::GlobalObject* globalObject = toJS(env); + if (byte_offset + length > arraybufferPtr->impl()->byteLength()) { + JSC::throwRangeError(globalObject, scope, "byteOffset exceeds source ArrayBuffer byteLength"_s); + RETURN_IF_EXCEPTION(scope, napi_set_last_error(env, napi_pending_exception)); + } auto dataView = JSC::DataView::create(arraybufferPtr->impl(), byte_offset, length); *result = toNapi(dataView->wrap(globalObject, globalObject), globalObject); + RELEASE_AND_RETURN(scope, napi_set_last_error(env, napi_ok)); +} + +static JSC::TypedArrayType getTypedArrayTypeFromNAPI(napi_typedarray_type type) +{ + switch (type) { + case napi_int8_array: + return JSC::TypedArrayType::TypeInt8; + case napi_uint8_array: + return JSC::TypedArrayType::TypeUint8; + case napi_uint8_clamped_array: + return JSC::TypedArrayType::TypeUint8Clamped; + case napi_int16_array: + return JSC::TypedArrayType::TypeInt16; + case napi_uint16_array: + return JSC::TypedArrayType::TypeUint16; + case napi_int32_array: + return JSC::TypedArrayType::TypeInt32; + case napi_uint32_array: + return JSC::TypedArrayType::TypeUint32; + case napi_float32_array: + return JSC::TypedArrayType::TypeFloat32; + case napi_float64_array: + return JSC::TypedArrayType::TypeFloat64; + case napi_bigint64_array: + return JSC::TypedArrayType::TypeBigInt64; + case napi_biguint64_array: + return JSC::TypedArrayType::TypeBigUint64; + default: + ASSERT_NOT_REACHED_WITH_MESSAGE("Unexpected napi_typedarray_type"); + } +} + +static JSC::JSArrayBufferView* createArrayBufferView( + Zig::GlobalObject* globalObject, + napi_typedarray_type type, + RefPtr&& arrayBuffer, + size_t byteOffset, + size_t length) +{ + Structure* structure = globalObject->typedArrayStructure(getTypedArrayTypeFromNAPI(type), arrayBuffer->isResizableOrGrowableShared()); + switch (type) { + case napi_int8_array: + return JSC::JSInt8Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_uint8_array: + return JSC::JSUint8Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_uint8_clamped_array: + return JSC::JSUint8ClampedArray::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_int16_array: + return JSC::JSInt16Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_uint16_array: + return JSC::JSUint16Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_int32_array: + return JSC::JSInt32Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_uint32_array: + return JSC::JSUint32Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_float32_array: + return JSC::JSFloat32Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_float64_array: + return JSC::JSFloat64Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_bigint64_array: + return JSC::JSBigInt64Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + case napi_biguint64_array: + return JSC::JSBigUint64Array::create(globalObject, structure, WTFMove(arrayBuffer), byteOffset, length); + default: + ASSERT_NOT_REACHED_WITH_MESSAGE("Unexpected napi_typedarray_type"); + } +} + +extern "C" napi_status napi_create_typedarray( + napi_env env, + napi_typedarray_type type, + size_t length, + napi_value arraybuffer, + size_t byte_offset, + napi_value* result) +{ + NAPI_PREAMBLE(env); + Zig::GlobalObject* globalObject = toJS(env); + NAPI_RETURN_IF_EXCEPTION(env); + NAPI_CHECK_ARG(env, arraybuffer); + NAPI_CHECK_ARG(env, result); + JSValue arraybufferValue = toJS(arraybuffer); + auto arraybufferPtr = JSC::jsDynamicCast(arraybufferValue); + NAPI_RETURN_EARLY_IF_FALSE(env, arraybufferPtr, napi_arraybuffer_expected); + JSC::JSArrayBufferView* view = createArrayBufferView(globalObject, type, arraybufferPtr->impl(), byte_offset, length); + NAPI_RETURN_IF_EXCEPTION(env); + *result = toNapi(view, globalObject); NAPI_RETURN_SUCCESS(env); } @@ -1850,13 +1703,12 @@ void NapiClass::visitChildrenImpl(JSCell* cell, Visitor& visitor) DEFINE_VISIT_CHILDREN(NapiClass); -JSC_DEFINE_HOST_FUNCTION(NapiClass_ConstructorFunction, - (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +template +JSC_HOST_CALL_ATTRIBUTES JSC::EncodedJSValue NapiClass_ConstructorFunction(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) { - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); JSObject* constructorTarget = asObject(callFrame->jsCallee()); - JSObject* newTarget = asObject(callFrame->newTarget()); NapiClass* napi = jsDynamicCast(constructorTarget); while (!napi && constructorTarget) { constructorTarget = constructorTarget->getPrototypeDirect().getObject(); @@ -1865,58 +1717,68 @@ JSC_DEFINE_HOST_FUNCTION(NapiClass_ConstructorFunction, if (UNLIKELY(!napi)) { JSC::throwVMError(globalObject, scope, JSC::createTypeError(globalObject, "NapiClass constructor called on an object that is not a NapiClass"_s)); - return JSC::JSValue::encode(JSC::jsUndefined()); + return JSValue::encode(JSC::jsUndefined()); } - NapiPrototype* prototype = JSC::jsDynamicCast(napi->getIfPropertyExists(globalObject, vm.propertyNames->prototype)); - RETURN_IF_EXCEPTION(scope, {}); + JSValue newTarget; - if (!prototype) { - JSC::throwVMError(globalObject, scope, JSC::createTypeError(globalObject, "NapiClass constructor is missing the prototype"_s)); - return JSC::JSValue::encode(JSC::jsUndefined()); + if constexpr (ConstructCall) { + NapiPrototype* prototype = JSC::jsDynamicCast(napi->getIfPropertyExists(globalObject, vm.propertyNames->prototype)); + RETURN_IF_EXCEPTION(scope, {}); + + if (!prototype) { + JSC::throwVMError(globalObject, scope, JSC::createTypeError(globalObject, "NapiClass constructor is missing the prototype"_s)); + return JSValue::encode(JSC::jsUndefined()); + } + + newTarget = callFrame->newTarget(); + auto* subclass = prototype->subclass(globalObject, asObject(newTarget)); + RETURN_IF_EXCEPTION(scope, {}); + callFrame->setThisValue(subclass); } - auto* subclass = prototype->subclass(globalObject, newTarget); - RETURN_IF_EXCEPTION(scope, {}); - callFrame->setThisValue(subclass); - - MarkedArgumentBufferWithSize<12> args; - size_t argc = callFrame->argumentCount() + 1; - args.fill(vm, argc, [&](auto* slot) { - memcpy(slot, ADDRESS_OF_THIS_VALUE_IN_CALLFRAME(callFrame), sizeof(JSC::JSValue) * argc); - }); - NAPICallFrame frame(JSC::ArgList(args), nullptr); - frame.newTarget = newTarget; + NAPICallFrame frame(globalObject, callFrame, napi->dataPtr(), newTarget); Bun::NapiHandleScope handleScope(jsCast(globalObject)); - napi->constructor()(globalObject, reinterpret_cast(NAPICallFrame::toNapiCallbackInfo(frame))); + JSValue ret = toJS(napi->constructor()(napi->env(), frame.toNapi())); + napi_set_last_error(napi->env(), napi_ok); RETURN_IF_EXCEPTION(scope, {}); - RELEASE_AND_RETURN(scope, JSValue::encode(frame.thisValue())); + if (ret.isEmpty()) { + ret = jsUndefined(); + } + if constexpr (ConstructCall) { + RELEASE_AND_RETURN(scope, JSValue::encode(frame.thisValue())); + } else { + RELEASE_AND_RETURN(scope, JSValue::encode(ret)); + } } -NapiClass* NapiClass::create(VM& vm, Zig::GlobalObject* globalObject, const char* utf8name, - size_t length, +NapiClass* NapiClass::create(VM& vm, napi_env env, WTF::String name, napi_callback constructor, void* data, size_t property_count, const napi_property_descriptor* properties) { - WTF::String name = WTF::String::fromUTF8({ utf8name, length }).isolatedCopy(); - NativeExecutable* executable = vm.getHostFunction(NapiClass_ConstructorFunction, ImplementationVisibility::Public, NapiClass_ConstructorFunction, name); - Structure* structure = globalObject->NapiClassStructure(); - NapiClass* napiClass = new (NotNull, allocateCell(vm)) NapiClass(vm, executable, globalObject, structure); - napiClass->finishCreation(vm, executable, length, name, constructor, data, property_count, properties); + NativeExecutable* executable = vm.getHostFunction( + // for normal call + NapiClass_ConstructorFunction, + ImplementationVisibility::Public, + // for constructor call + NapiClass_ConstructorFunction, name); + Structure* structure = env->globalObject()->NapiClassStructure(); + NapiClass* napiClass = new (NotNull, allocateCell(vm)) NapiClass(vm, executable, env, structure, data); + napiClass->finishCreation(vm, executable, name, constructor, data, property_count, properties); return napiClass; } -void NapiClass::finishCreation(VM& vm, NativeExecutable* executable, unsigned length, const String& name, napi_callback constructor, +void NapiClass::finishCreation(VM& vm, NativeExecutable* executable, const String& name, napi_callback constructor, void* data, size_t property_count, const napi_property_descriptor* properties) { - Base::finishCreation(vm, executable, length, name); + Base::finishCreation(vm, executable, 0, name); ASSERT(inherits(info())); - this->m_constructor = reinterpret_cast(constructor); + this->m_constructor = constructor; auto globalObject = reinterpret_cast(this->globalObject()); this->putDirect(vm, vm.propertyNames->name, jsString(vm, name), JSC::PropertyAttribute::DontEnum | 0); @@ -1929,9 +1791,9 @@ void NapiClass::finishCreation(VM& vm, NativeExecutable* executable, unsigned le const napi_property_descriptor& property = properties[i]; if (property.attributes & napi_static) { - defineNapiProperty(globalObject, this, nullptr, property, true, throwScope); + defineNapiProperty(m_env, this, property, true, throwScope); } else { - defineNapiProperty(globalObject, prototype, nullptr, property, false, throwScope); + defineNapiProperty(m_env, prototype, property, false, throwScope); } if (throwScope.exception()) @@ -1953,6 +1815,7 @@ extern "C" napi_status napi_get_all_property_names( { NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, result); + NAPI_CHECK_ARG(env, objectNapi); auto objectValue = toJS(objectNapi); auto* object = objectValue.getObject(); NAPI_RETURN_EARLY_IF_FALSE(env, object, napi_object_expected); @@ -1964,13 +1827,63 @@ extern "C" napi_status napi_get_all_property_names( jsc_property_mode = PropertyNameMode::Strings; } else if (key_filter & napi_key_skip_strings) { jsc_property_mode = PropertyNameMode::Symbols; + } else { + // JSC requires key mode to be Include if property mode is StringsAndSymbols + jsc_key_mode = DontEnumPropertiesMode::Include; } auto globalObject = toJS(env); - JSC::JSArray* exportKeys = ownPropertyKeys(globalObject, object, jsc_property_mode, jsc_key_mode); - // TODO: filter - *result = toNapi(JSC::JSValue(exportKeys), globalObject); + JSArray* exportKeys = nullptr; + if (key_mode == napi_key_include_prototypes) { + exportKeys = allPropertyKeys(globalObject, object, jsc_property_mode, jsc_key_mode); + } else { + exportKeys = ownPropertyKeys(globalObject, object, jsc_property_mode, jsc_key_mode); + } + + NAPI_RETURN_IF_EXCEPTION(env); + + constexpr auto filter_by_any_descriptor = static_cast(napi_key_enumerable | napi_key_writable | napi_key_configurable); + // avoid expensive iteration if they don't care whether keys are enumerable, writable, or configurable + if (key_filter & filter_by_any_descriptor) { + JSArray* filteredKeys = JSArray::create(JSC::getVM(globalObject), globalObject->originalArrayStructureForIndexingType(ArrayWithContiguous), 0); + for (unsigned i = 0; i < exportKeys->getArrayLength(); i++) { + JSValue key = exportKeys->get(globalObject, i); + PropertyDescriptor desc; + + if (key_mode == napi_key_include_prototypes) { + // Climb up the prototype chain to find inherited properties + JSObject* current_object = object; + while (!current_object->getOwnPropertyDescriptor(globalObject, key.toPropertyKey(globalObject), desc)) { + JSObject* proto = current_object->getPrototype(JSC::getVM(globalObject), globalObject).getObject(); + if (!proto) { + break; + } + current_object = proto; + } + } else { + object->getOwnPropertyDescriptor(globalObject, key.toPropertyKey(globalObject), desc); + } + + bool include = true; + if (key_filter & napi_key_enumerable) { + include = include && desc.enumerable(); + } + if (key_filter & napi_key_writable) { + include = include && desc.writable(); + } + if (key_filter & napi_key_configurable) { + include = include && desc.configurable(); + } + + if (include) { + filteredKeys->push(globalObject, key); + } + } + exportKeys = filteredKeys; + } + + *result = toNapi(JSValue(exportKeys), globalObject); NAPI_RETURN_SUCCESS(env); } @@ -1986,19 +1899,21 @@ extern "C" napi_status napi_define_class(napi_env env, NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, utf8name); + NAPI_CHECK_ARG(env, constructor); NAPI_RETURN_EARLY_IF_FALSE(env, properties || property_count == 0, napi_invalid_arg); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); size_t len = length; if (len == NAPI_AUTO_LENGTH) { len = strlen(utf8name); } - NapiClass* napiClass = NapiClass::create(vm, globalObject, utf8name, len, constructor, data, property_count, properties); - JSC::JSValue value = JSC::JSValue(napiClass); + auto name = WTF::String::fromUTF8(std::span { utf8name, len }).isolatedCopy(); + NapiClass* napiClass = NapiClass::create(vm, env, name, constructor, data, property_count, properties); + JSValue value = JSValue(napiClass); JSC::EnsureStillAliveScope ensureStillAlive1(value); if (data != nullptr) { - napiClass->dataPtr = data; + napiClass->dataPtr() = data; } *result = toNapi(value, globalObject); @@ -2018,25 +1933,79 @@ extern "C" napi_status napi_coerce_to_string(napi_env env, napi_value value, JSC::EnsureStillAliveScope ensureStillAlive(jsValue); // .toString() can throw - JSC::JSValue resultValue = JSC::JSValue(jsValue.toString(globalObject)); + JSValue resultValue = JSValue(jsValue.toString(globalObject)); + NAPI_RETURN_IF_EXCEPTION(env); + JSC::EnsureStillAliveScope ensureStillAlive1(resultValue); *result = toNapi(resultValue, globalObject); NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env); } +extern "C" napi_status napi_coerce_to_bool(napi_env env, napi_value value, napi_value* result) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ARG(env, value); + NAPI_CHECK_ARG(env, result); + + Zig::GlobalObject* globalObject = toJS(env); + + JSValue jsValue = toJS(value); + // might throw + bool nativeBool = jsValue.toBoolean(globalObject); + NAPI_RETURN_IF_EXCEPTION(env); + + *result = toNapi(JSC::jsBoolean(nativeBool), globalObject); + NAPI_RETURN_SUCCESS(env); +} + +extern "C" napi_status napi_coerce_to_number(napi_env env, napi_value value, napi_value* result) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ARG(env, value); + NAPI_CHECK_ARG(env, result); + + Zig::GlobalObject* globalObject = toJS(env); + + JSValue jsValue = toJS(value); + // might throw + double nativeNumber = jsValue.toNumber(globalObject); + NAPI_RETURN_IF_EXCEPTION(env); + + *result = toNapi(JSC::jsNumber(nativeNumber), globalObject); + NAPI_RETURN_SUCCESS(env); +} + +extern "C" napi_status napi_coerce_to_object(napi_env env, napi_value value, napi_value* result) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ARG(env, value); + NAPI_CHECK_ARG(env, result); + + Zig::GlobalObject* globalObject = toJS(env); + + JSValue jsValue = toJS(value); + // might throw + JSObject* obj = jsValue.toObject(globalObject); + NAPI_RETURN_IF_EXCEPTION(env); + + *result = toNapi(obj, globalObject); + NAPI_RETURN_SUCCESS(env); +} + extern "C" napi_status napi_get_property_names(napi_env env, napi_value object, napi_value* result) { NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, object); NAPI_CHECK_ARG(env, result); - JSC::JSValue jsValue = toJS(object); - NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isObject(), napi_object_expected); + JSValue jsValue = toJS(object); + JSObject* jsObject = jsValue.getObject(); + NAPI_RETURN_EARLY_IF_FALSE(env, jsObject, napi_object_expected); Zig::GlobalObject* globalObject = toJS(env); JSC::EnsureStillAliveScope ensureStillAlive(jsValue); - JSC::JSValue value = JSC::ownPropertyKeys(globalObject, jsValue.getObject(), PropertyNameMode::Strings, DontEnumPropertiesMode::Include); + JSValue value = JSC::allPropertyKeys(globalObject, jsObject, PropertyNameMode::Strings, DontEnumPropertiesMode::Exclude); NAPI_RETURN_IF_EXCEPTION(env); JSC::EnsureStillAliveScope ensureStillAlive1(value); @@ -2055,11 +2024,9 @@ extern "C" napi_status napi_create_external_buffer(napi_env env, size_t length, Zig::GlobalObject* globalObject = toJS(env); - auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast(data), length }, createSharedTask([globalObject, finalize_hint, finalize_cb](void* p) { - if (finalize_cb != nullptr) { - NAPI_LOG("finalizer"); - finalize_cb(toNapi(globalObject), p, finalize_hint); - } + auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast(data), length }, createSharedTask([env, finalize_hint, finalize_cb](void* p) { + NAPI_LOG("external buffer finalizer"); + env->doFinalizer(finalize_cb, p, finalize_hint); })); auto* subclassStructure = globalObject->JSBufferSubclassStructure(); @@ -2076,16 +2043,14 @@ extern "C" napi_status napi_create_external_arraybuffer(napi_env env, void* exte NAPI_CHECK_ARG(env, result); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); - auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast(external_data), byte_length }, createSharedTask([globalObject, finalize_hint, finalize_cb](void* p) { - if (finalize_cb != nullptr) { - NAPI_LOG("finalizer"); - finalize_cb(toNapi(globalObject), p, finalize_hint); - } + auto arrayBuffer = ArrayBuffer::createFromBytes({ reinterpret_cast(external_data), byte_length }, createSharedTask([env, finalize_hint, finalize_cb](void* p) { + NAPI_LOG("external ArrayBuffer finalizer"); + env->doFinalizer(finalize_cb, p, finalize_hint); })); - auto* buffer = JSC::JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(ArrayBufferSharingMode::Shared), WTFMove(arrayBuffer)); + auto* buffer = JSC::JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(ArrayBufferSharingMode::Default), WTFMove(arrayBuffer)); *result = toNapi(buffer, globalObject); NAPI_RETURN_SUCCESS(env); @@ -2095,6 +2060,7 @@ extern "C" napi_status napi_create_double(napi_env env, double value, napi_value* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); *result = toNapi(jsDoubleNumber(value), toJS(env)); NAPI_RETURN_SUCCESS(env); @@ -2104,9 +2070,10 @@ extern "C" napi_status napi_get_value_double(napi_env env, napi_value value, double* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, value); - JSC::JSValue jsValue = toJS(value); + JSValue jsValue = toJS(value); NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isNumber(), napi_number_expected); *result = jsValue.asNumber(); @@ -2116,33 +2083,36 @@ extern "C" napi_status napi_get_value_double(napi_env env, napi_value value, extern "C" napi_status napi_get_value_int32(napi_env env, napi_value value, int32_t* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, value); - JSC::JSValue jsValue = toJS(value); + JSValue jsValue = toJS(value); NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isNumber(), napi_number_expected); - *result = jsValue.isInt32() ? jsValue.asInt32() : JSC::toInt32(jsValue.asNumber()); + *result = jsValue.isInt32() ? jsValue.asInt32() : JSC::toInt32(jsValue.asDouble()); NAPI_RETURN_SUCCESS(env); } extern "C" napi_status napi_get_value_uint32(napi_env env, napi_value value, uint32_t* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, value); - JSC::JSValue jsValue = toJS(value); + JSValue jsValue = toJS(value); NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isNumber(), napi_number_expected); - *result = jsValue.isUInt32() ? jsValue.asUInt32() : JSC::toUInt32(jsValue.asNumber()); + *result = jsValue.isUInt32() ? jsValue.asUInt32() : JSC::toUInt32(jsValue.asNumber()); NAPI_RETURN_SUCCESS(env); } extern "C" napi_status napi_get_value_int64(napi_env env, napi_value value, int64_t* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, value); - JSC::JSValue jsValue = toJS(value); + JSValue jsValue = toJS(value); NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isNumber(), napi_number_expected); double js_number = jsValue.asNumber(); @@ -2164,48 +2134,44 @@ extern "C" napi_status napi_get_value_int64(napi_env env, napi_value value, int6 NAPI_RETURN_SUCCESS(env); } -extern "C" napi_status napi_get_value_string_utf8(napi_env env, - napi_value napiValue, char* buf, - size_t bufsize, - size_t* writtenPtr) +template +napi_status napi_get_value_string_any_encoding(napi_env env, napi_value napiValue, BufferElement* buf, size_t bufsize, size_t* writtenPtr) { - NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, napiValue); JSValue jsValue = toJS(napiValue); NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isString(), napi_string_expected); Zig::GlobalObject* globalObject = toJS(env); String view = jsValue.asCell()->getString(globalObject); - NAPI_RETURN_IF_EXCEPTION(env); size_t length = view.length(); if (buf == nullptr) { // they just want to know the length NAPI_CHECK_ARG(env, writtenPtr); if (view.is8Bit()) { - *writtenPtr = Bun__encoding__byteLengthLatin1(view.span8().data(), length, static_cast(WebCore::BufferEncodingType::utf8)); + *writtenPtr = Bun__encoding__byteLengthLatin1(view.span8().data(), length, static_cast(EncodeTo)); } else { - *writtenPtr = Bun__encoding__byteLengthUTF16(view.span16().data(), length, static_cast(WebCore::BufferEncodingType::utf8)); + *writtenPtr = Bun__encoding__byteLengthUTF16(view.span16().data(), length, static_cast(EncodeTo)); } - NAPI_RETURN_SUCCESS(env); + return napi_set_last_error(env, napi_ok); } if (UNLIKELY(bufsize == 0)) { if (writtenPtr) *writtenPtr = 0; - NAPI_RETURN_SUCCESS(env); + return napi_set_last_error(env, napi_ok); } if (UNLIKELY(bufsize == NAPI_AUTO_LENGTH)) { if (writtenPtr) *writtenPtr = 0; buf[0] = '\0'; - NAPI_RETURN_SUCCESS(env); + return napi_set_last_error(env, napi_ok); } size_t written; if (view.is8Bit()) { - written = Bun__encoding__writeLatin1(view.span8().data(), view.length(), reinterpret_cast(buf), bufsize - 1, static_cast(WebCore::BufferEncodingType::utf8)); + written = Bun__encoding__writeLatin1(view.span8().data(), view.length(), reinterpret_cast(buf), bufsize - 1, static_cast(EncodeTo)); } else { - written = Bun__encoding__writeUTF16(view.span16().data(), view.length(), reinterpret_cast(buf), bufsize - 1, static_cast(WebCore::BufferEncodingType::utf8)); + written = Bun__encoding__writeUTF16(view.span16().data(), view.length(), reinterpret_cast(buf), bufsize - 1, static_cast(EncodeTo)); } if (writtenPtr != nullptr) { @@ -2216,6 +2182,46 @@ extern "C" napi_status napi_get_value_string_utf8(napi_env env, buf[written] = '\0'; } + return napi_set_last_error(env, napi_ok); +} + +extern "C" napi_status napi_get_value_string_utf8(napi_env env, + napi_value napiValue, char* buf, + size_t bufsize, + size_t* writtenPtr) +{ + NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); + // this function does set_last_error + return napi_get_value_string_any_encoding(env, napiValue, buf, bufsize, writtenPtr); +} + +extern "C" napi_status napi_get_value_string_latin1(napi_env env, napi_value napiValue, char* buf, size_t bufsize, size_t* writtenPtr) +{ + NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); + // this function does set_last_error + return napi_get_value_string_any_encoding(env, napiValue, buf, bufsize, writtenPtr); +} + +extern "C" napi_status napi_get_value_string_utf16(napi_env env, napi_value napiValue, char16_t* buf, size_t bufsize, size_t* writtenPtr) +{ + NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); + // this function does set_last_error + return napi_get_value_string_any_encoding(env, napiValue, buf, bufsize, writtenPtr); +} + +extern "C" napi_status napi_get_value_bool(napi_env env, napi_value value, bool* result) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); + NAPI_CHECK_ARG(env, value); + NAPI_CHECK_ARG(env, result); + JSValue jsValue = toJS(value); + NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isBoolean(), napi_boolean_expected); + + *result = jsValue.asBoolean(); NAPI_RETURN_SUCCESS(env); } @@ -2226,11 +2232,10 @@ extern "C" napi_status napi_get_element(napi_env env, napi_value objectValue, NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, objectValue); JSValue jsValue = toJS(objectValue); - NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isObject(), napi_object_expected); + JSObject* jsObject = jsValue.getObject(); + NAPI_RETURN_EARLY_IF_FALSE(env, jsObject, napi_object_expected); - JSObject* object = jsValue.getObject(); - - JSValue element = object->getIndex(toJS(env), index); + JSValue element = jsObject->getIndex(toJS(env), index); NAPI_RETURN_IF_EXCEPTION(env); *result = toNapi(element, toJS(env)); @@ -2243,11 +2248,11 @@ extern "C" napi_status napi_delete_element(napi_env env, napi_value objectValue, NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, objectValue); JSValue jsValue = toJS(objectValue); - NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isObject(), napi_object_expected); + JSObject* jsObject = jsValue.getObject(); + NAPI_RETURN_EARLY_IF_FALSE(env, jsObject, napi_object_expected); - JSObject* object = jsValue.getObject(); if (LIKELY(result)) { - *result = JSObject::deletePropertyByIndex(object, toJS(env), index); + *result = jsObject->methodTable()->deletePropertyByIndex(jsObject, toJS(env), index); } NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env); } @@ -2255,10 +2260,11 @@ extern "C" napi_status napi_delete_element(napi_env env, napi_value objectValue, extern "C" napi_status napi_create_object(napi_env env, napi_value* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); JSValue value = JSValue(NapiPrototype::create(vm, globalObject->NapiPrototypeStructure())); @@ -2277,10 +2283,10 @@ extern "C" napi_status napi_create_external(napi_env env, void* data, NAPI_CHECK_ARG(env, result); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); auto* structure = globalObject->NapiExternalStructure(); - JSValue value = Bun::NapiExternal::create(vm, structure, data, finalize_hint, reinterpret_cast(finalize_cb)); + JSValue value = Bun::NapiExternal::create(vm, structure, data, finalize_hint, env, finalize_cb); JSC::EnsureStillAliveScope ensureStillAlive(value); *result = toNapi(value, globalObject); NAPI_RETURN_SUCCESS(env); @@ -2290,9 +2296,10 @@ extern "C" napi_status napi_typeof(napi_env env, napi_value val, napi_valuetype* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); - JSC::JSValue value = toJS(val); + JSValue value = toJS(val); if (value.isEmpty()) { // This can happen *result = napi_undefined; @@ -2300,7 +2307,7 @@ extern "C" napi_status napi_typeof(napi_env env, napi_value val, } if (value.isCell()) { - JSC::JSCell* cell = value.asCell(); + JSCell* cell = value.asCell(); switch (cell->type()) { case JSC::JSFunctionType: @@ -2384,6 +2391,7 @@ static_assert(std::is_same_v, "All NAPI bigint functi extern "C" napi_status napi_get_value_bigint_int64(napi_env env, napi_value value, int64_t* result, bool* lossless) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, value); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, lossless); @@ -2416,6 +2424,7 @@ extern "C" napi_status napi_get_value_bigint_int64(napi_env env, napi_value valu extern "C" napi_status napi_get_value_bigint_uint64(napi_env env, napi_value value, uint64_t* result, bool* lossless) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, value); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, lossless); @@ -2441,19 +2450,15 @@ extern "C" napi_status napi_get_value_bigint_words(napi_env env, uint64_t* words) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, value); NAPI_CHECK_ARG(env, word_count); - JSC::JSValue jsValue = toJS(value); + JSValue jsValue = toJS(value); NAPI_RETURN_EARLY_IF_FALSE(env, jsValue.isHeapBigInt(), napi_bigint_expected); // If both sign_bit and words are nullptr, we're just querying the word count // However, if exactly one of them is nullptr, we have an invalid argument NAPI_RETURN_EARLY_IF_FALSE(env, (sign_bit == nullptr && words == nullptr) || (sign_bit && words), napi_invalid_arg); - static_assert(std::is_same_v); -#if USE(BIGINT32) -#error napi_get_value_bigint_words does not support BIGINT32 -#endif - JSC::JSBigInt* bigInt = jsValue.asHeapBigInt(); size_t available_words = *word_count; @@ -2478,6 +2483,7 @@ extern "C" napi_status napi_get_value_external(napi_env env, napi_value value, void** result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, value); auto* external = jsDynamicCast(toJS(value)); @@ -2494,8 +2500,7 @@ extern "C" napi_status napi_get_instance_data(napi_env env, NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, data); - Zig::GlobalObject* globalObject = toJS(env); - *data = globalObject->napiInstanceData; + *data = env->instanceData; NAPI_RETURN_SUCCESS(env); } @@ -2539,11 +2544,8 @@ extern "C" napi_status napi_set_instance_data(napi_env env, { NAPI_PREAMBLE(env); - Zig::GlobalObject* globalObject = toJS(env); - globalObject->napiInstanceData = data; - - globalObject->napiInstanceDataFinalizer = reinterpret_cast(finalize_cb); - globalObject->napiInstanceDataFinalizerHint = finalize_hint; + env->instanceData = data; + env->instanceDataFinalizer = Bun::NapiFinalizer { finalize_cb, finalize_hint }; NAPI_RETURN_SUCCESS(env); } @@ -2554,18 +2556,23 @@ extern "C" napi_status napi_create_bigint_words(napi_env env, const uint64_t* words, napi_value* result) { - NAPI_PREAMBLE(env); + NAPI_PREAMBLE_NO_THROW_SCOPE(env); NAPI_CHECK_ARG(env, result); NAPI_CHECK_ARG(env, words); - // JSBigInt::createWithLength's size argument is unsigned int + // JSBigInt::createWithLength's size argument is unsigned int. NAPI_RETURN_EARLY_IF_FALSE(env, word_count <= UINT_MAX, napi_invalid_arg); Zig::GlobalObject* globalObject = toJS(env); + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); - if (word_count == 0) { - auto* bigint = JSBigInt::createZero(globalObject); - *result = toNapi(bigint, globalObject); - NAPI_RETURN_SUCCESS(env); + // we check INT_MAX here because it won't reject any bigints that should be able to be created + // (as the true limit is much lower), and one Node.js test expects an exception instead of + // napi_invalid_arg in case the length is INT_MAX + if (word_count >= INT_MAX) { + // we use this error as the error from creating a massive bigint literal is simply + // "RangeError: Out of memory" + JSC::throwOutOfMemoryError(globalObject, scope); + RETURN_IF_EXCEPTION(scope, napi_set_last_error(env, napi_pending_exception)); } // JSBigInt requires there are no leading zeroes in the words array, but native modules may have @@ -2574,9 +2581,16 @@ extern "C" napi_status napi_create_bigint_words(napi_env env, word_count--; } + if (word_count == 0) { + auto* bigint = JSBigInt::createZero(globalObject); + RETURN_IF_EXCEPTION(scope, napi_set_last_error(env, napi_pending_exception)); + *result = toNapi(bigint, globalObject); + return napi_set_last_error(env, napi_ok); + } + // throws RangeError if size is larger than JSC's limit auto* bigint = JSBigInt::createWithLength(globalObject, word_count); - NAPI_RETURN_IF_EXCEPTION(env); + RETURN_IF_EXCEPTION(scope, napi_set_last_error(env, napi_pending_exception)); ASSERT(bigint); bigint->setSign(sign_bit != 0); @@ -2589,17 +2603,18 @@ extern "C" napi_status napi_create_bigint_words(napi_env env, } *result = toNapi(bigint, globalObject); - NAPI_RETURN_SUCCESS(env); + return napi_set_last_error(env, napi_ok); } extern "C" napi_status napi_create_symbol(napi_env env, napi_value description, napi_value* result) { NAPI_PREAMBLE(env); + NAPI_CHECK_ENV_NOT_IN_GC(env); NAPI_CHECK_ARG(env, result); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); JSC::JSValue descriptionValue = toJS(description); if (descriptionValue && !descriptionValue.isUndefinedOrNull()) { @@ -2628,18 +2643,18 @@ extern "C" napi_status napi_new_instance(napi_env env, napi_value constructor, NAPI_PREAMBLE(env); NAPI_CHECK_ARG(env, result); NAPI_RETURN_EARLY_IF_FALSE(env, argc == 0 || argv, napi_invalid_arg); - JSC::JSValue constructorValue = toJS(constructor); - NAPI_RETURN_EARLY_IF_FALSE(env, constructorValue.isObject(), napi_function_expected); + JSValue constructorValue = toJS(constructor); JSC::JSObject* constructorObject = constructorValue.getObject(); + NAPI_RETURN_EARLY_IF_FALSE(env, constructorObject, napi_function_expected); JSC::CallData constructData = getConstructData(constructorObject); NAPI_RETURN_EARLY_IF_FALSE(env, constructData.type != JSC::CallData::Type::None, napi_function_expected); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); JSC::MarkedArgumentBuffer args; args.fill(vm, argc, [&](JSValue* buffer) { - gcSafeMemcpy(buffer, reinterpret_cast(argv), sizeof(JSC::JSValue) * argc); + gcSafeMemcpy(buffer, reinterpret_cast(argv), sizeof(JSValue) * argc); }); auto value = construct(globalObject, constructorObject, constructData, args); @@ -2647,6 +2662,33 @@ extern "C" napi_status napi_new_instance(napi_env env, napi_value constructor, NAPI_RETURN_SUCCESS_UNLESS_EXCEPTION(env); } +extern "C" napi_status napi_instanceof(napi_env env, napi_value object, napi_value constructor, bool* result) +{ + NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ARG(env, result); + + Zig::GlobalObject* globalObject = toJS(env); + + JSValue objectValue = toJS(object); + JSValue constructorValue = toJS(constructor); + JSC::JSObject* constructorObject = constructorValue.getObject(); + + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); + + if (!constructorObject || !constructorValue.isConstructor()) { + throwVMError(globalObject, scope, JSC::createTypeError(globalObject, "Constructor must be a function"_s)); + return napi_set_last_error(env, napi_pending_exception); + } + + if (UNLIKELY(!constructorObject->structure()->typeInfo().implementsHasInstance())) { + *result = false; + } else { + *result = constructorObject->hasInstance(globalObject, objectValue); + } + + return napi_set_last_error(env, napi_ok); +} + extern "C" napi_status napi_call_function(napi_env env, napi_value recv_napi, napi_value func_napi, size_t argc, const napi_value* argv, @@ -2654,24 +2696,24 @@ extern "C" napi_status napi_call_function(napi_env env, napi_value recv_napi, { NAPI_PREAMBLE(env); NAPI_RETURN_EARLY_IF_FALSE(env, argc == 0 || argv, napi_invalid_arg); - JSC::JSValue funcValue = toJS(func_napi); + JSValue funcValue = toJS(func_napi); NAPI_RETURN_EARLY_IF_FALSE(env, funcValue.isObject(), napi_function_expected); JSC::CallData callData = getCallData(funcValue); NAPI_RETURN_EARLY_IF_FALSE(env, callData.type != JSC::CallData::Type::None, napi_function_expected); Zig::GlobalObject* globalObject = toJS(env); - auto& vm = JSC::getVM(globalObject); + JSC::VM& vm = JSC::getVM(globalObject); JSC::MarkedArgumentBuffer args; args.fill(vm, argc, [&](JSValue* buffer) { - gcSafeMemcpy(buffer, reinterpret_cast(argv), sizeof(JSC::JSValue) * argc); + gcSafeMemcpy(buffer, reinterpret_cast(argv), sizeof(JSValue) * argc); }); - JSC::JSValue thisValue = toJS(recv_napi); + JSValue thisValue = toJS(recv_napi); if (thisValue.isEmpty()) { thisValue = JSC::jsUndefined(); } - JSC::JSValue result = call(globalObject, funcValue, callData, thisValue, args); + JSValue result = call(globalObject, funcValue, callData, thisValue, args); if (result_ptr) { if (result.isEmpty()) { @@ -2721,3 +2763,285 @@ extern "C" napi_status napi_check_object_type_tag(napi_env env, napi_value value } NAPI_RETURN_SUCCESS(env); } + +extern "C" JS_EXPORT napi_status node_api_create_property_key_latin1(napi_env env, const char* str, size_t length, napi_value* result) +{ + // EXPERIMENTAL + // This is semantically correct but it may not have the performance benefit intended for node_api_create_property_key_latin1 + // TODO(@190n) use jsAtomString or something + NAPI_LOG_CURRENT_FUNCTION; + return napi_create_string_latin1(env, str, length, result); +} + +extern "C" JS_EXPORT napi_status node_api_create_property_key_utf16(napi_env env, const char16_t* str, size_t length, napi_value* result) +{ + // EXPERIMENTAL + // This is semantically correct but it may not have the performance benefit intended for node_api_create_property_key_utf16 + // TODO(@190n) use jsAtomString or something + NAPI_LOG_CURRENT_FUNCTION; + return napi_create_string_utf16(env, str, length, result); +} + +extern "C" JS_EXPORT napi_status node_api_create_property_key_utf8(napi_env env, const char* str, size_t length, napi_value* result) +{ + // EXPERIMENTAL + // This is semantically correct but it may not have the performance benefit intended for node_api_create_property_key_utf8 + // TODO(@190n) use jsAtomString or something + NAPI_LOG_CURRENT_FUNCTION; + return napi_create_string_utf8(env, str, length, result); +} + +extern "C" JS_EXPORT napi_status node_api_create_buffer_from_arraybuffer(napi_env env, + napi_value arraybuffer, + size_t byte_offset, + size_t byte_length, + napi_value* result) +{ + NAPI_LOG_CURRENT_FUNCTION; + NAPI_PREAMBLE_NO_THROW_SCOPE(env); + NAPI_CHECK_ARG(env, result); + + JSC::JSArrayBuffer* jsArrayBuffer = JSC::jsDynamicCast(toJS(arraybuffer)); + NAPI_RETURN_EARLY_IF_FALSE(env, jsArrayBuffer, napi_arraybuffer_expected); + + auto* globalObject = toJS(env); + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); + + if (byte_offset + byte_length > jsArrayBuffer->impl()->byteLength()) { + JSC::throwRangeError(globalObject, scope, "byteOffset exceeds source ArrayBuffer byteLength"_s); + RETURN_IF_EXCEPTION(scope, napi_set_last_error(env, napi_pending_exception)); + } + + auto* subclassStructure = globalObject->JSBufferSubclassStructure(); + JSC::JSUint8Array* uint8Array = JSC::JSUint8Array::create(globalObject, subclassStructure, byte_length); + void* destination = uint8Array->vector(); + const void* source = reinterpret_cast(jsArrayBuffer->impl()->data()) + byte_offset; + memmove(destination, source, byte_length); + + *result = toNapi(uint8Array, globalObject); + scope.release(); + return napi_set_last_error(env, napi_ok); +} + +extern "C" JS_EXPORT napi_status node_api_get_module_file_name(napi_env env, + const char** result) +{ + NAPI_PREAMBLE(env); + NAPI_CHECK_ARG(env, result); + *result = env->filename; + NAPI_RETURN_SUCCESS(env); +} + +extern "C" JS_EXPORT napi_status napi_add_env_cleanup_hook(napi_env env, + void (*function)(void*), + void* data) +{ + NAPI_PREAMBLE(env); + if (function) { + env->addCleanupHook(function, data); + } + NAPI_RETURN_SUCCESS(env); +} + +extern "C" JS_EXPORT napi_status napi_add_async_cleanup_hook(napi_env env, + napi_async_cleanup_hook function, + void* data, napi_async_cleanup_hook_handle* handle_out) +{ + NAPI_PREAMBLE(env); + if (function) { + napi_async_cleanup_hook_handle handle = env->addAsyncCleanupHook(function, data); + if (handle_out) { + *handle_out = handle; + } + } + NAPI_RETURN_SUCCESS(env); +} + +extern "C" JS_EXPORT napi_status napi_remove_env_cleanup_hook(napi_env env, + void (*function)(void*), + void* data) +{ + NAPI_PREAMBLE(env); + + if (LIKELY(function != nullptr) && !env->globalObject()->vm().hasTerminationRequest()) { + env->removeCleanupHook(function, data); + } + + NAPI_RETURN_SUCCESS(env); +} + +extern "C" JS_EXPORT napi_status napi_remove_async_cleanup_hook(napi_async_cleanup_hook_handle handle) +{ + ASSERT(handle != nullptr); + napi_env env = handle->env; + + NAPI_PREAMBLE(env); + + if (!env->globalObject()->vm().hasTerminationRequest()) { + env->removeAsyncCleanupHook(handle); + } + + NAPI_RETURN_SUCCESS(env); +} + +extern "C" void napi_internal_cleanup_env_cpp(napi_env env) +{ + env->cleanup(); +} + +extern "C" void napi_internal_remove_finalizer(napi_env env, napi_finalize callback, void* hint, void* data) +{ + env->removeFinalizer(callback, hint, data); +} + +extern "C" void napi_internal_check_gc(napi_env env) +{ + env->checkGC(); +} + +extern "C" uint32_t napi_internal_get_version(napi_env env) +{ + return env->napiModule().nm_version; +} + +extern "C" JSGlobalObject* NapiEnv__globalObject(napi_env env) +{ + return env->globalObject(); +} + +WTF_MAKE_TZONE_ALLOCATED_IMPL(NapiRef); + +void NapiRef::ref() +{ + NAPI_LOG("ref %p %u -> %u", this, refCount, refCount + 1); + ++refCount; + if (refCount == 1 && !weakValueRef.isClear()) { + auto& vm = globalObject.get()->vm(); + strongRef.set(vm, weakValueRef.get()); + + // isSet() will return always true after being set once + // We cannot rely on isSet() to check if the value is set we need to use isClear() + // .setString/.setObject/.setPrimitive will assert fail if called more than once (even after clear()) + // We should not clear the weakValueRef here because we need to keep it if we call NapiRef::unref() + // so we can call the finalizer + } +} + +void NapiRef::unref() +{ + NAPI_LOG("unref %p %u -> %u", this, refCount, refCount - 1); + bool clear = refCount == 1; + refCount = refCount > 0 ? refCount - 1 : 0; + if (clear && !m_isEternal) { + // we still dont clean weakValueRef so we can ref it again using NapiRef::ref() if the GC didn't collect it + // and use it to call the finalizer when GC'd + strongRef.clear(); + } +} + +void NapiRef::clear() +{ + NAPI_LOG("ref clear %p", this); + finalizer.call(env, nativeObject); + globalObject.clear(); + weakValueRef.clear(); + strongRef.clear(); +} + +NapiWeakValue::~NapiWeakValue() +{ + clear(); +} + +void NapiWeakValue::clear() +{ + switch (m_tag) { + case WeakTypeTag::Cell: { + m_value.cell.clear(); + break; + } + case WeakTypeTag::String: { + m_value.string.clear(); + break; + } + default: { + break; + } + } + + m_tag = WeakTypeTag::NotSet; +} + +bool NapiWeakValue::isClear() const +{ + return m_tag == WeakTypeTag::NotSet; +} + +void NapiWeakValue::setPrimitive(JSValue value) +{ + switch (m_tag) { + case WeakTypeTag::Cell: { + m_value.cell.clear(); + break; + } + case WeakTypeTag::String: { + m_value.string.clear(); + break; + } + default: { + break; + } + } + m_tag = WeakTypeTag::Primitive; + m_value.primitive = value; +} + +void NapiWeakValue::set(JSValue value, WeakHandleOwner& owner, void* context) +{ + if (value.isCell()) { + auto* cell = value.asCell(); + if (cell->isString()) { + setString(jsCast(cell), owner, context); + } else { + setCell(cell, owner, context); + } + } else { + setPrimitive(value); + } +} + +void NapiWeakValue::setCell(JSCell* cell, WeakHandleOwner& owner, void* context) +{ + switch (m_tag) { + case WeakTypeTag::Cell: { + m_value.cell.clear(); + break; + } + case WeakTypeTag::String: { + m_value.string.clear(); + break; + } + default: { + break; + } + } + + m_value.cell = JSC::Weak(cell, &owner, context); + m_tag = WeakTypeTag::Cell; +} + +void NapiWeakValue::setString(JSString* string, WeakHandleOwner& owner, void* context) +{ + switch (m_tag) { + case WeakTypeTag::Cell: { + m_value.cell.clear(); + break; + } + default: { + break; + } + } + + m_value.string = JSC::Weak(string, &owner, context); + m_tag = WeakTypeTag::String; +} diff --git a/src/bun.js/bindings/napi.h b/src/bun.js/bindings/napi.h index b27a3a21e3..3853337402 100644 --- a/src/bun.js/bindings/napi.h +++ b/src/bun.js/bindings/napi.h @@ -7,11 +7,301 @@ #include "headers-handwritten.h" #include "BunClientData.h" #include -#include "js_native_api.h" +#include "node_api.h" #include #include "JSFFIFunction.h" #include "ZigGlobalObject.h" #include "napi_handle_scope.h" +#include "napi_finalizer.h" +#include "wtf/Assertions.h" +#include "napi_macros.h" + +#include +#include + +extern "C" void napi_internal_register_cleanup_zig(napi_env env); +extern "C" void napi_internal_crash_in_gc(napi_env); +extern "C" void Bun__crashHandler(const char* message, size_t message_len); + +namespace Napi { +struct AsyncCleanupHook { + napi_async_cleanup_hook function = nullptr; + void* data = nullptr; + napi_async_cleanup_hook_handle handle = nullptr; +}; +} + +struct napi_async_cleanup_hook_handle__ { + napi_env env; + std::list::iterator iter; + + napi_async_cleanup_hook_handle__(napi_env env, decltype(iter) iter) + : env(env) + , iter(iter) + { + } +}; + +#define NAPI_ABORT(message) Bun__crashHandler(message "", sizeof(message "") - 1) + +#define NAPI_PERISH(...) \ + do { \ + WTFReportError(__FILE__, __LINE__, __PRETTY_FUNCTION__, __VA_ARGS__); \ + WTFReportBacktrace(); \ + NAPI_ABORT("Aborted"); \ + } while (0) + +#define NAPI_RELEASE_ASSERT(assertion, ...) \ + do { \ + if (UNLIKELY(!(assertion))) { \ + WTFReportAssertionFailureWithMessage(__FILE__, __LINE__, __PRETTY_FUNCTION__, #assertion, __VA_ARGS__); \ + WTFReportBacktrace(); \ + NAPI_ABORT("Aborted"); \ + } \ + } while (0) + +// Named this way so we can manipulate napi_env values directly (since napi_env is defined as a pointer to struct napi_env__) +struct napi_env__ { +public: + napi_env__(Zig::GlobalObject* globalObject, const napi_module& napiModule) + : m_globalObject(globalObject) + , m_napiModule(napiModule) + { + napi_internal_register_cleanup_zig(this); + } + + ~napi_env__() + { + delete[] filename; + } + + void cleanup() + { + while (!m_cleanupHooks.empty()) { + auto [function, data] = m_cleanupHooks.back(); + m_cleanupHooks.pop_back(); + ASSERT(function != nullptr); + function(data); + } + + while (!m_asyncCleanupHooks.empty()) { + auto [function, data, handle] = m_asyncCleanupHooks.back(); + ASSERT(function != nullptr); + function(handle, data); + delete handle; + m_asyncCleanupHooks.pop_back(); + } + + m_isFinishingFinalizers = true; + for (const BoundFinalizer& boundFinalizer : m_finalizers) { + Bun::NapiHandleScope handle_scope(m_globalObject); + boundFinalizer.call(this); + } + m_finalizers.clear(); + m_isFinishingFinalizers = false; + + instanceDataFinalizer.call(this, instanceData, true); + instanceDataFinalizer.clear(); + } + + void removeFinalizer(napi_finalize callback, void* hint, void* data) + { + m_finalizers.erase({ callback, hint, data }); + } + + struct BoundFinalizer; + + void removeFinalizer(const BoundFinalizer& finalizer) + { + m_finalizers.erase(finalizer); + } + + const auto& addFinalizer(napi_finalize callback, void* hint, void* data) + { + return *m_finalizers.emplace(callback, hint, data).first; + } + + bool hasFinalizers() const + { + return !m_finalizers.empty(); + } + + /// Will abort the process if a duplicate entry would be added. + void addCleanupHook(void (*function)(void*), void* data) + { + for (const auto& [existing_function, existing_data] : m_cleanupHooks) { + NAPI_RELEASE_ASSERT(function != existing_function || data != existing_data, "Attempted to add a duplicate NAPI environment cleanup hook"); + } + + m_cleanupHooks.emplace_back(function, data); + } + + void removeCleanupHook(void (*function)(void*), void* data) + { + for (auto iter = m_cleanupHooks.begin(), end = m_cleanupHooks.end(); iter != end; ++iter) { + if (iter->first == function && iter->second == data) { + m_cleanupHooks.erase(iter); + return; + } + } + + NAPI_PERISH("Attempted to remove a NAPI environment cleanup hook that had never been added"); + } + + napi_async_cleanup_hook_handle addAsyncCleanupHook(napi_async_cleanup_hook function, void* data) + { + for (const auto& [existing_function, existing_data, existing_handle] : m_asyncCleanupHooks) { + NAPI_RELEASE_ASSERT(function != existing_function || data != existing_data, "Attempted to add a duplicate async NAPI environment cleanup hook"); + } + + auto iter = m_asyncCleanupHooks.emplace(m_asyncCleanupHooks.end(), function, data); + iter->handle = new napi_async_cleanup_hook_handle__(this, iter); + return iter->handle; + } + + void removeAsyncCleanupHook(napi_async_cleanup_hook_handle handle) + { + for (const auto& [existing_function, existing_data, existing_handle] : m_asyncCleanupHooks) { + if (existing_handle == handle) { + m_asyncCleanupHooks.erase(handle->iter); + delete handle; + return; + } + } + + NAPI_PERISH("Attempted to remove an async NAPI environment cleanup hook that had never been added"); + } + + bool inGC() const + { + JSC::VM& vm = JSC::getVM(m_globalObject); + return vm.isCollectorBusyOnCurrentThread(); + } + + void checkGC() const + { + NAPI_RELEASE_ASSERT(!inGC(), + "Attempted to call a non-GC-safe function inside a NAPI finalizer from a NAPI module with version %d.\n" + "Finalizers must not create new objects during garbage collection. Use the `node_api_post_finalizer` function\n" + "inside the finalizer to defer the code to the next event loop tick.\n", + m_napiModule.nm_version); + } + + bool isVMTerminating() const + { + return JSC::getVM(m_globalObject).hasTerminationRequest(); + } + + void doFinalizer(napi_finalize finalize_cb, void* data, void* finalize_hint) + { + if (!finalize_cb) { + return; + } + + if (mustDeferFinalizers() && inGC()) { + napi_internal_enqueue_finalizer(this, finalize_cb, data, finalize_hint); + } else { + finalize_cb(this, data, finalize_hint); + } + } + + inline Zig::GlobalObject* globalObject() const { return m_globalObject; } + inline const napi_module& napiModule() const { return m_napiModule; } + + // Returns true if finalizers from this module need to be scheduled for the next tick after garbage collection, instead of running during garbage collection + inline bool mustDeferFinalizers() const + { + // Even when we'd normally have to defer the finalizer, if this is happening during the VM's last chance to finalize, + // we can't defer the finalizer and have to call it now. + return m_napiModule.nm_version != NAPI_VERSION_EXPERIMENTAL && !isVMTerminating(); + } + + inline bool isFinishingFinalizers() const { return m_isFinishingFinalizers; } + + // Almost all NAPI functions should set error_code to the status they're returning right before + // they return it + napi_extended_error_info m_lastNapiErrorInfo = { + .error_message = "", + // Not currently used by Bun -- always nullptr + .engine_reserved = nullptr, + // Not currently used by Bun -- always zero + .engine_error_code = 0, + .error_code = napi_ok, + }; + + void* instanceData = nullptr; + Bun::NapiFinalizer instanceDataFinalizer; + char* filename = nullptr; + + struct BoundFinalizer { + napi_finalize callback = nullptr; + void* hint = nullptr; + void* data = nullptr; + // Allows bound finalizers to effectively remove themselves during cleanup without breaking iteration. + // Safe to be mutable because it's not included in the hash. + mutable bool active = true; + + BoundFinalizer() = default; + + BoundFinalizer(const Bun::NapiFinalizer& finalizer, void* data) + : callback(finalizer.callback()) + , hint(finalizer.hint()) + , data(data) + { + } + + BoundFinalizer(napi_finalize callback, void* hint, void* data) + : callback(callback) + , hint(hint) + , data(data) + { + } + + void call(napi_env env) const + { + if (callback && active) { + callback(env, data, hint); + } + } + + void deactivate(napi_env env) const + { + if (env->isFinishingFinalizers()) { + active = false; + } else { + env->removeFinalizer(*this); + // At this point the BoundFinalizer has been destroyed, but because we're not doing anything else here it's safe. + // https://isocpp.org/wiki/faq/freestore-mgmt#delete-this + } + } + + bool operator==(const BoundFinalizer& other) const + { + return this == &other || (callback == other.callback && hint == other.hint && data == other.data); + } + + struct Hash { + std::size_t operator()(const napi_env__::BoundFinalizer& bound) const + { + constexpr std::hash hasher; + constexpr std::ptrdiff_t magic = 0x9e3779b9; + return (hasher(reinterpret_cast(bound.callback)) + magic) ^ (hasher(bound.hint) + magic) ^ (hasher(bound.data) + magic); + } + }; + }; + +private: + Zig::GlobalObject* m_globalObject = nullptr; + napi_module m_napiModule; + // TODO(@heimskr): Use WTF::HashSet + std::unordered_set m_finalizers; + bool m_isFinishingFinalizers = false; + std::list> m_cleanupHooks; + std::list m_asyncCleanupHooks; +}; + +extern "C" void napi_internal_cleanup_env_cpp(napi_env); +extern "C" void napi_internal_remove_finalizer(napi_env, napi_finalize callback, void* hint, void* data); namespace JSC { class JSGlobalObject; @@ -20,10 +310,33 @@ class JSSourceCode; namespace Napi { JSC::SourceCode generateSourceCode(WTF::String keyString, JSC::VM& vm, JSC::JSObject* object, JSC::JSGlobalObject* globalObject); + +class NapiRefWeakHandleOwner final : public JSC::WeakHandleOwner { +public: + // Equivalent to v8impl::Ownership::kUserland + void finalize(JSC::Handle, void* context) final; + + static NapiRefWeakHandleOwner& weakValueHandleOwner() + { + static NeverDestroyed jscWeakValueHandleOwner; + return jscWeakValueHandleOwner; + } +}; + +class NapiRefSelfDeletingWeakHandleOwner final : public JSC::WeakHandleOwner { +public: + // Equivalent to v8impl::Ownership::kRuntime + void finalize(JSC::Handle, void* context) final; + + static NapiRefSelfDeletingWeakHandleOwner& weakValueHandleOwner() + { + static NeverDestroyed jscWeakValueHandleOwner; + return jscWeakValueHandleOwner; + } +}; } namespace Zig { - using namespace JSC; static inline JSValue toJS(napi_value val) @@ -33,7 +346,7 @@ static inline JSValue toJS(napi_value val) static inline Zig::GlobalObject* toJS(napi_env val) { - return reinterpret_cast(val); + return val->globalObject(); } static inline napi_value toNapi(JSC::JSValue val, Zig::GlobalObject* globalObject) @@ -46,19 +359,6 @@ static inline napi_value toNapi(JSC::JSValue val, Zig::GlobalObject* globalObjec return reinterpret_cast(JSC::JSValue::encode(val)); } -static inline napi_env toNapi(JSC::JSGlobalObject* val) -{ - return reinterpret_cast(val); -} - -class NapiFinalizer { -public: - void* finalize_hint = nullptr; - napi_finalize finalize_cb; - - void call(JSC::JSGlobalObject* globalObject, void* data); -}; - // This is essentially JSC::JSWeakValue, except with a JSCell* instead of a // JSObject*. Sometimes, a napi embedder might want to store a JSC::Exception, a // JSC::HeapBigInt, JSC::Symbol, etc inside of a NapiRef. So we can't limit it @@ -147,37 +447,87 @@ public: void unref(); void clear(); - NapiRef(JSC::JSGlobalObject* global, uint32_t count) + NapiRef(napi_env env, uint32_t count, Bun::NapiFinalizer finalizer) + : env(env) + , globalObject(JSC::Weak(env->globalObject())) + , finalizer(WTFMove(finalizer)) + , refCount(count) { - globalObject = JSC::Weak(global); - strongRef = {}; - weakValueRef.clear(); - refCount = count; } JSC::JSValue value() const { - if (refCount == 0) { + if (refCount == 0 && !m_isEternal) { return weakValueRef.get(); } return strongRef.get(); } + void setValueInitial(JSC::JSValue value, bool can_be_weak) + { + if (refCount > 0) { + strongRef.set(globalObject->vm(), value); + } + + // In NAPI non-experimental, types other than object, function and symbol can't be used as values for references. + // In NAPI experimental, they can be, but we must not store weak references to them. + if (can_be_weak) { + weakValueRef.set(value, Napi::NapiRefWeakHandleOwner::weakValueHandleOwner(), this); + } + + if (value.isSymbol()) { + auto* symbol = jsDynamicCast(value); + ASSERT(symbol != nullptr); + if (symbol->uid().isRegistered()) { + // Global symbols must always be retrievable, + // even if garbage collection happens while the ref count is 0. + m_isEternal = true; + if (refCount == 0) { + strongRef.set(globalObject->vm(), symbol); + } + } + } + } + + void callFinalizer() + { + // Calling the finalizer may delete `this`, so we have to do state changes on `this` before + // calling the finalizer + Bun::NapiFinalizer saved_finalizer = this->finalizer; + this->finalizer.clear(); + saved_finalizer.call(env, nativeObject, !env->mustDeferFinalizers() || !env->inGC()); + } + ~NapiRef() { - strongRef.clear(); + NAPI_LOG("destruct napi ref %p", this); + if (boundCleanup) { + boundCleanup->deactivate(env); + boundCleanup = nullptr; + } + + if (!m_isEternal) { + strongRef.clear(); + } + // The weak ref can lead to calling the destructor // so we must first clear the weak ref before we call the finalizer weakValueRef.clear(); } + napi_env env = nullptr; JSC::Weak globalObject; NapiWeakValue weakValueRef; JSC::Strong strongRef; - NapiFinalizer finalizer; - void* data = nullptr; + Bun::NapiFinalizer finalizer; + const napi_env__::BoundFinalizer* boundCleanup = nullptr; + void* nativeObject = nullptr; uint32_t refCount = 0; + bool releaseOnWeaken = false; + +private: + bool m_isEternal = false; }; static inline napi_ref toNapi(NapiRef* val) @@ -210,8 +560,7 @@ public: DECLARE_EXPORT_INFO; - JS_EXPORT_PRIVATE static NapiClass* create(VM&, Zig::GlobalObject*, const char* utf8name, - size_t length, + JS_EXPORT_PRIVATE static NapiClass* create(VM&, napi_env, WTF::String name, napi_callback constructor, void* data, size_t property_count, @@ -223,25 +572,28 @@ public: return Structure::create(vm, globalObject, prototype, TypeInfo(JSFunctionType, StructureFlags), info()); } - CFFIFunction constructor() - { - return m_constructor; - } - - void* dataPtr = nullptr; - CFFIFunction m_constructor = nullptr; - NapiRef* napiRef = nullptr; + inline napi_callback constructor() const { return m_constructor; } + inline void*& dataPtr() { return m_dataPtr; } + inline void* const& dataPtr() const { return m_dataPtr; } + inline napi_env env() const { return m_env; } private: - NapiClass(VM& vm, NativeExecutable* executable, JSC::JSGlobalObject* global, Structure* structure) - : Base(vm, executable, global, structure) + NapiClass(VM& vm, NativeExecutable* executable, napi_env env, Structure* structure, void* data) + : Base(vm, executable, env->globalObject(), structure) + , m_dataPtr(data) + , m_env(env) { } - void finishCreation(VM&, NativeExecutable*, unsigned length, const String& name, napi_callback constructor, + + void finishCreation(VM&, NativeExecutable*, const String& name, napi_callback constructor, void* data, size_t property_count, const napi_property_descriptor* properties); + void* m_dataPtr = nullptr; + napi_callback m_constructor = nullptr; + napi_env m_env = nullptr; + DECLARE_VISIT_CHILDREN; }; @@ -275,23 +627,12 @@ public: NapiPrototype* subclass(JSC::JSGlobalObject* globalObject, JSC::JSObject* newTarget) { - auto& vm = this->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - auto* targetFunction = jsCast(newTarget); - FunctionRareData* rareData = targetFunction->ensureRareData(vm); - auto* prototype = newTarget->get(globalObject, vm.propertyNames->prototype).getObject(); - RETURN_IF_EXCEPTION(scope, nullptr); - - // This must be kept in-sync with InternalFunction::createSubclassStructure - Structure* structure = rareData->internalFunctionAllocationStructure(); - if (UNLIKELY(!(structure && structure->classInfoForCells() == this->structure()->classInfoForCells() && structure->globalObject() == globalObject))) { - structure = rareData->createInternalFunctionAllocationStructureFromBase(vm, globalObject, prototype, this->structure()); + VM& vm = globalObject->vm(); + Structure* structure = JSC::InternalFunction::createSubclassStructure(globalObject, newTarget, this->structure()); + if (!structure) { + return nullptr; } - - RETURN_IF_EXCEPTION(scope, nullptr); - NapiPrototype* footprint = new (NotNull, allocateCell(vm)) NapiPrototype(vm, structure); - footprint->finishCreation(vm); - RELEASE_AND_RETURN(scope, footprint); + return NapiPrototype::create(vm, structure); } NapiRef* napiRef = nullptr; @@ -308,6 +649,4 @@ static inline NapiRef* toJS(napi_ref val) return reinterpret_cast(val); } -Structure* createNAPIFunctionStructure(VM& vm, JSC::JSGlobalObject* globalObject); - } diff --git a/src/bun.js/bindings/napi_external.cpp b/src/bun.js/bindings/napi_external.cpp index f6925d9992..c303c85c4b 100644 --- a/src/bun.js/bindings/napi_external.cpp +++ b/src/bun.js/bindings/napi_external.cpp @@ -5,11 +5,8 @@ namespace Bun { NapiExternal::~NapiExternal() { - if (finalizer) { - // We cannot call globalObject() here because it is in a finalizer. - // https://github.com/oven-sh/bun/issues/13001#issuecomment-2290022312 - reinterpret_cast(finalizer)(toNapi(this->napi_env), m_value, m_finalizerHint); - } + ASSERT(m_env); + m_finalizer.call(m_env, m_value, !m_env->mustDeferFinalizers()); } void NapiExternal::destroy(JSC::JSCell* cell) diff --git a/src/bun.js/bindings/napi_external.h b/src/bun.js/bindings/napi_external.h index e755dc94d0..26b61f9d5f 100644 --- a/src/bun.js/bindings/napi_external.h +++ b/src/bun.js/bindings/napi_external.h @@ -2,10 +2,11 @@ #pragma once +#include "napi_finalizer.h" #include "root.h" -#include "BunBuiltinNames.h" #include "BunClientData.h" +#include "napi.h" namespace Bun { @@ -52,11 +53,11 @@ public: JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } - static NapiExternal* create(JSC::VM& vm, JSC::Structure* structure, void* value, void* finalizer_hint, void* finalizer) + static NapiExternal* create(JSC::VM& vm, JSC::Structure* structure, void* value, void* finalizer_hint, napi_env env, napi_finalize callback) { NapiExternal* accessor = new (NotNull, JSC::allocateCell(vm)) NapiExternal(vm, structure); - accessor->finishCreation(vm, value, finalizer_hint, finalizer); + accessor->finishCreation(vm, value, finalizer_hint, env, callback); #if BUN_DEBUG if (auto* callFrame = vm.topCallFrame) { @@ -80,13 +81,12 @@ public: return accessor; } - void finishCreation(JSC::VM& vm, void* value, void* finalizer_hint, void* finalizer) + void finishCreation(JSC::VM& vm, void* value, void* finalizer_hint, napi_env env, napi_finalize callback) { Base::finishCreation(vm); m_value = value; - m_finalizerHint = finalizer_hint; - napi_env = this->globalObject(); - this->finalizer = finalizer; + m_env = env; + m_finalizer = NapiFinalizer { callback, finalizer_hint }; } static void destroy(JSC::JSCell* cell); @@ -94,9 +94,8 @@ public: void* value() const { return m_value; } void* m_value; - void* m_finalizerHint; - void* finalizer; - JSGlobalObject* napi_env; + NapiFinalizer m_finalizer; + napi_env m_env; #if BUN_DEBUG String sourceOriginURL = String(); diff --git a/src/bun.js/bindings/napi_finalizer.cpp b/src/bun.js/bindings/napi_finalizer.cpp new file mode 100644 index 0000000000..cc2c25ea09 --- /dev/null +++ b/src/bun.js/bindings/napi_finalizer.cpp @@ -0,0 +1,26 @@ +#include "napi_finalizer.h" + +#include "napi.h" +#include "napi_macros.h" + +namespace Bun { + +void NapiFinalizer::call(napi_env env, void* data, bool immediate) +{ + if (m_callback) { + NAPI_LOG_CURRENT_FUNCTION; + if (immediate) { + m_callback(env, data, m_hint); + } else { + napi_internal_enqueue_finalizer(env, m_callback, data, m_hint); + } + } +} + +void NapiFinalizer::clear() +{ + m_callback = nullptr; + m_hint = nullptr; +} + +} // namespace Bun diff --git a/src/bun.js/bindings/napi_finalizer.h b/src/bun.js/bindings/napi_finalizer.h new file mode 100644 index 0000000000..65d4bbccfa --- /dev/null +++ b/src/bun.js/bindings/napi_finalizer.h @@ -0,0 +1,31 @@ +#pragma once + +#include "root.h" +#include "js_native_api.h" + +extern "C" void napi_internal_enqueue_finalizer(napi_env env, napi_finalize finalize_cb, void* data, void* hint); + +namespace Bun { + +class NapiFinalizer { +public: + NapiFinalizer(napi_finalize callback, void* hint) + : m_callback(callback) + , m_hint(hint) + { + } + + NapiFinalizer() = default; + + void call(napi_env env, void* data, bool immediate = false); + void clear(); + + inline napi_finalize callback() const { return m_callback; } + inline void* hint() const { return m_hint; } + +private: + napi_finalize m_callback = nullptr; + void* m_hint = nullptr; +}; + +} // namespace Bun diff --git a/src/bun.js/bindings/napi_handle_scope.cpp b/src/bun.js/bindings/napi_handle_scope.cpp index 6616125be0..75cc17d084 100644 --- a/src/bun.js/bindings/napi_handle_scope.cpp +++ b/src/bun.js/bindings/napi_handle_scope.cpp @@ -1,4 +1,5 @@ #include "napi_handle_scope.h" +#include "napi.h" #include "ZigGlobalObject.h" @@ -103,6 +104,7 @@ NapiHandleScopeImpl* NapiHandleScope::open(Zig::GlobalObject* globalObject, bool void NapiHandleScope::close(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current) { + NAPI_LOG_CURRENT_FUNCTION; // napi handle scopes may be null pointers if created inside a finalizer if (!current) { return; @@ -127,19 +129,19 @@ NapiHandleScope::~NapiHandleScope() NapiHandleScope::close(m_globalObject, m_impl); } -extern "C" NapiHandleScopeImpl* NapiHandleScope__open(Zig::GlobalObject* globalObject, bool escapable) +extern "C" NapiHandleScopeImpl* NapiHandleScope__open(napi_env env, bool escapable) { - return NapiHandleScope::open(globalObject, escapable); + return NapiHandleScope::open(env->globalObject(), escapable); } -extern "C" void NapiHandleScope__close(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current) +extern "C" void NapiHandleScope__close(napi_env env, NapiHandleScopeImpl* current) { - return NapiHandleScope::close(globalObject, current); + return NapiHandleScope::close(env->globalObject(), current); } -extern "C" void NapiHandleScope__append(Zig::GlobalObject* globalObject, JSC::EncodedJSValue value) +extern "C" void NapiHandleScope__append(napi_env env, JSC::EncodedJSValue value) { - globalObject->m_currentNapiHandleScopeImpl.get()->append(JSC::JSValue::decode(value)); + env->globalObject()->m_currentNapiHandleScopeImpl.get()->append(JSC::JSValue::decode(value)); } extern "C" bool NapiHandleScope__escape(NapiHandleScopeImpl* handleScope, JSC::EncodedJSValue value) diff --git a/src/bun.js/bindings/napi_handle_scope.h b/src/bun.js/bindings/napi_handle_scope.h index 851d5006a5..b678770038 100644 --- a/src/bun.js/bindings/napi_handle_scope.h +++ b/src/bun.js/bindings/napi_handle_scope.h @@ -3,6 +3,8 @@ #include "BunClientData.h" #include "root.h" +typedef struct napi_env__* napi_env; + namespace Bun { // An array of write barriers (so that newly-added objects are not lost by GC) to JSValues. Unlike @@ -80,14 +82,14 @@ private: }; // Create a new handle scope in the given environment -extern "C" NapiHandleScopeImpl* NapiHandleScope__open(Zig::GlobalObject* globalObject, bool escapable); +extern "C" NapiHandleScopeImpl* NapiHandleScope__open(napi_env env, bool escapable); // Pop the most recently created handle scope in the given environment and restore the old one. // Asserts that `current` is the active handle scope. -extern "C" void NapiHandleScope__close(Zig::GlobalObject* globalObject, NapiHandleScopeImpl* current); +extern "C" void NapiHandleScope__close(napi_env env, NapiHandleScopeImpl* current); // Store a value in the active handle scope in the given environment -extern "C" void NapiHandleScope__append(Zig::GlobalObject* globalObject, JSC::EncodedJSValue value); +extern "C" void NapiHandleScope__append(napi_env env, JSC::EncodedJSValue value); // Put a value from the current handle scope into its escape slot reserved in the outer handle // scope. Returns false if the current handle scope is not escapable or if escape has already been diff --git a/src/bun.js/bindings/napi_macros.h b/src/bun.js/bindings/napi_macros.h new file mode 100644 index 0000000000..dffecdab74 --- /dev/null +++ b/src/bun.js/bindings/napi_macros.h @@ -0,0 +1,30 @@ +#pragma once + +#define NAPI_VERBOSE 0 + +#if NAPI_VERBOSE +#include +#include + +#if defined __has_attribute +#if __has_attribute(__format__) +__attribute__((__format__(__printf__, 4, 5))) static inline void napi_log(const char* file, long line, const char* function, const char* fmt, ...) +#endif +#endif +{ + printf("[%s:%ld] %s: ", file, line, function); + + va_list ap; + va_start(ap, fmt); + vprintf(fmt, ap); + va_end(ap); + + printf("\n"); +} + +#define NAPI_LOG_CURRENT_FUNCTION printf("[%s:%d] %s\n", __FILE__, __LINE__, __PRETTY_FUNCTION__) +#define NAPI_LOG(fmt, ...) napi_log(__FILE__, __LINE__, __PRETTY_FUNCTION__, fmt __VA_OPT__(, ) __VA_ARGS__) +#else +#define NAPI_LOG_CURRENT_FUNCTION +#define NAPI_LOG(fmt, ...) +#endif diff --git a/src/bun.js/bindings/v8/V8External.cpp b/src/bun.js/bindings/v8/V8External.cpp index 7697289a5c..23581c2ecf 100644 --- a/src/bun.js/bindings/v8/V8External.cpp +++ b/src/bun.js/bindings/v8/V8External.cpp @@ -12,7 +12,8 @@ Local External::New(Isolate* isolate, void* value) auto globalObject = isolate->globalObject(); auto& vm = JSC::getVM(globalObject); auto structure = globalObject->NapiExternalStructure(); - Bun::NapiExternal* val = Bun::NapiExternal::create(vm, structure, value, nullptr, nullptr); + Bun::NapiExternal* val = Bun::NapiExternal::create(vm, structure, value, + nullptr /* hint */, nullptr /* env */, nullptr /* callback */); return isolate->currentHandleScope()->createLocal(vm, val); } diff --git a/src/bun.js/bindings/v8/shim/FunctionTemplate.cpp b/src/bun.js/bindings/v8/shim/FunctionTemplate.cpp index aeba5542fc..4e57e3327b 100644 --- a/src/bun.js/bindings/v8/shim/FunctionTemplate.cpp +++ b/src/bun.js/bindings/v8/shim/FunctionTemplate.cpp @@ -71,6 +71,7 @@ JSC::EncodedJSValue FunctionTemplate::functionCall(JSC::JSGlobalObject* globalOb // object. JSC::JSObject* jscThis = globalObject->globalThis(); if (!callFrame->thisValue().isUndefinedOrNull()) { + // TODO(@190n) throwscope, assert no exception jscThis = callFrame->thisValue().toObject(globalObject); } Local thisObject = hs.createLocal(vm, jscThis); diff --git a/src/bun.js/bindings/webcore/JSEventTargetCustom.cpp b/src/bun.js/bindings/webcore/JSEventTargetCustom.cpp index 1b00dc9874..368a57c74b 100644 --- a/src/bun.js/bindings/webcore/JSEventTargetCustom.cpp +++ b/src/bun.js/bindings/webcore/JSEventTargetCustom.cpp @@ -57,7 +57,7 @@ EventTarget* JSEventTarget::toWrapped(VM& vm, JSValue value) // if (value.inherits()) // return &jsCast(asObject(value))->wrapped(); if (value.inherits()) - return &jsCast(asObject(value))->globalEventScope; + return jsCast(asObject(value))->globalEventScope.ptr(); if (value.inherits()) return &jsCast(asObject(value))->wrapped(); return nullptr; diff --git a/src/bun.js/bindings/webcore/MessagePortChannelProvider.cpp b/src/bun.js/bindings/webcore/MessagePortChannelProvider.cpp index cde0d0dd4b..d5c8a8665b 100644 --- a/src/bun.js/bindings/webcore/MessagePortChannelProvider.cpp +++ b/src/bun.js/bindings/webcore/MessagePortChannelProvider.cpp @@ -64,7 +64,7 @@ MessagePortChannelProvider& MessagePortChannelProvider::fromContext(ScriptExecut // if (auto workletScope = dynamicDowncast(context)) // return workletScope->messagePortChannelProvider(); - return jsCast(context.jsGlobalObject())->globalEventScope.messagePortChannelProvider(); + return jsCast(context.jsGlobalObject())->globalEventScope->messagePortChannelProvider(); } } // namespace WebCore diff --git a/src/bun.js/bindings/webcore/Worker.cpp b/src/bun.js/bindings/webcore/Worker.cpp index 9a2c38b232..9e6bf96a5f 100644 --- a/src/bun.js/bindings/webcore/Worker.cpp +++ b/src/bun.js/bindings/webcore/Worker.cpp @@ -244,7 +244,7 @@ ExceptionOr Worker::postMessage(JSC::JSGlobalObject& state, JSC::JSValue m auto ports = MessagePort::entanglePorts(context, WTFMove(message.transferredPorts)); auto event = MessageEvent::create(*globalObject, message.message.releaseNonNull(), std::nullopt, WTFMove(ports)); - globalObject->globalEventScope.dispatchEvent(event.event); + globalObject->globalEventScope->dispatchEvent(event.event); }); return {}; } @@ -346,9 +346,9 @@ void Worker::dispatchOnline(Zig::GlobalObject* workerGlobalObject) return; } RELEASE_ASSERT(&thisContext->vm() == &workerGlobalObject->vm()); - RELEASE_ASSERT(thisContext == workerGlobalObject->globalEventScope.scriptExecutionContext()); + RELEASE_ASSERT(thisContext == workerGlobalObject->globalEventScope->scriptExecutionContext()); - if (workerGlobalObject->globalEventScope.hasActiveEventListeners(eventNames().messageEvent)) { + if (workerGlobalObject->globalEventScope->hasActiveEventListeners(eventNames().messageEvent)) { auto tasks = std::exchange(this->m_pendingTasks, {}); lock.unlockEarly(); for (auto& task : tasks) { @@ -454,7 +454,7 @@ extern "C" void WebWorker__dispatchError(Zig::GlobalObject* globalObject, Worker init.cancelable = false; init.bubbles = false; - globalObject->globalEventScope.dispatchEvent(ErrorEvent::create(eventNames().errorEvent, init, EventIsTrusted::Yes)); + globalObject->globalEventScope->dispatchEvent(ErrorEvent::create(eventNames().errorEvent, init, EventIsTrusted::Yes)); worker->dispatchError(message.toWTFString(BunString::ZeroCopy)); } diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index aadf239bc5..2d4796425c 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -19,6 +19,7 @@ const FetchTasklet = Fetch.FetchTasklet; const S3 = bun.S3; const S3HttpSimpleTask = S3.S3HttpSimpleTask; const S3HttpDownloadStreamingTask = S3.S3HttpDownloadStreamingTask; +const NapiFinalizerTask = bun.JSC.napi.NapiFinalizerTask; const Waker = bun.Async.Waker; @@ -496,6 +497,7 @@ pub const Task = TaggedPointerUnion(.{ Mkdir, Mkdtemp, napi_async_work, + NapiFinalizerTask, NativeBrotli, NativeZlib, Open, @@ -1350,6 +1352,9 @@ pub const EventLoop = struct { @field(Task.Tag, @typeName(PosixSignalTask)) => { PosixSignalTask.runFromJSThread(@intCast(task.asUintptr()), global); }, + @field(Task.Tag, @typeName(NapiFinalizerTask)) => { + task.get(NapiFinalizerTask).?.runOnJSThread(); + }, @field(Task.Tag, @typeName(StatFS)) => { var any: *StatFS = task.get(StatFS).?; any.runFromJSThread(); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 0cdd0e42f2..a24406ea04 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -1409,13 +1409,18 @@ pub const VirtualMachine = struct { pub fn onExit(this: *VirtualMachine) void { this.exit_handler.dispatchOnExit(); + this.is_shutting_down = true; const rare_data = this.rare_data orelse return; - var hooks = rare_data.cleanup_hooks; - defer if (!is_main_thread_vm) hooks.clearAndFree(bun.default_allocator); - rare_data.cleanup_hooks = .{}; - for (hooks.items) |hook| { - hook.execute(); + defer rare_data.cleanup_hooks.clearAndFree(bun.default_allocator); + // Make sure we run new cleanup hooks introduced by running cleanup hooks + while (rare_data.cleanup_hooks.items.len > 0) { + var hooks = rare_data.cleanup_hooks; + defer hooks.deinit(bun.default_allocator); + rare_data.cleanup_hooks = .{}; + for (hooks.items) |hook| { + hook.execute(); + } } } diff --git a/src/bun.js/javascript_core_c_api.zig b/src/bun.js/javascript_core_c_api.zig index 18e3f199cd..2d81c70987 100644 --- a/src/bun.js/javascript_core_c_api.zig +++ b/src/bun.js/javascript_core_c_api.zig @@ -46,6 +46,7 @@ pub const kJSTypeNumber = @intFromEnum(JSType.kJSTypeNumber); pub const kJSTypeString = @intFromEnum(JSType.kJSTypeString); pub const kJSTypeObject = @intFromEnum(JSType.kJSTypeObject); pub const kJSTypeSymbol = @intFromEnum(JSType.kJSTypeSymbol); +/// From JSValueRef.h:81 pub const JSTypedArrayType = enum(c_uint) { kJSTypedArrayTypeInt8Array, kJSTypedArrayTypeInt16Array, @@ -58,6 +59,8 @@ pub const JSTypedArrayType = enum(c_uint) { kJSTypedArrayTypeFloat64Array, kJSTypedArrayTypeArrayBuffer, kJSTypedArrayTypeNone, + kJSTypedArrayTypeBigInt64Array, + kJSTypedArrayTypeBigUint64Array, _, }; pub const kJSTypedArrayTypeInt8Array = @intFromEnum(JSTypedArrayType.kJSTypedArrayTypeInt8Array); diff --git a/src/bun.js/web_worker.zig b/src/bun.js/web_worker.zig index b50cb47cec..fde251a44b 100644 --- a/src/bun.js/web_worker.zig +++ b/src/bun.js/web_worker.zig @@ -478,6 +478,7 @@ pub const WebWorker = struct { /// Request a terminate (Called from main thread from worker.terminate(), or inside worker in process.exit()) /// The termination will actually happen after the next tick of the worker's loop. pub fn requestTerminate(this: *WebWorker) callconv(.C) void { + // TODO(@heimskr): make WebWorker termination more immediate. Currently, console.log after process.exit will go through if in a WebWorker. if (this.status.load(.acquire) == .terminated) { return; } diff --git a/src/crash_handler.zig b/src/crash_handler.zig index a364954f8e..c7554bf5df 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -347,7 +347,7 @@ pub fn crashHandler( , true), .{native_plugin_name}) catch std.posix.abort(); } else if (reason == .out_of_memory) { writer.writeAll( - \\Bun has ran out of memory. + \\Bun has run out of memory. \\ \\To send a redacted crash report to Bun's team, \\please file a GitHub issue using the link below: @@ -1462,7 +1462,9 @@ fn report(url: []const u8) void { fn crash() noreturn { switch (bun.Environment.os) { .windows => { - std.posix.abort(); + // This exit code is what Node.js uses when it calls + // abort. This is relied on by their Node-API tests. + bun.C.quick_exit(134); }, else => { // Install default handler so that the tkill below will terminate. @@ -1828,3 +1830,11 @@ pub fn removePreCrashHandler(ptr: *anyopaque) void { } else return; _ = before_crash_handlers.orderedRemove(index); } + +export fn Bun__crashHandler(message_ptr: [*]u8, message_len: usize) noreturn { + crashHandler(.{ .panic = message_ptr[0..message_len] }, null, @returnAddress()); +} + +comptime { + _ = &Bun__crashHandler; +} diff --git a/src/napi/napi.zig b/src/napi/napi.zig index c2980450c4..1eadf2a2d5 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -13,21 +13,17 @@ const log = bun.Output.scoped(.napi, false); const Async = bun.Async; -/// Actually a JSGlobalObject +/// This is `struct napi_env__` from napi.h pub const NapiEnv = opaque { - pub fn fromJS(global: *JSC.JSGlobalObject) *NapiEnv { - return @ptrCast(global); - } - pub fn toJS(self: *NapiEnv) *JSC.JSGlobalObject { - return @ptrCast(self); + return NapiEnv__globalObject(self); } extern fn napi_set_last_error(env: napi_env, status: NapiStatus) napi_status; /// Convert err to an extern napi_status, and store the error code in env so that it can be /// accessed by napi_get_last_error_info - pub fn setLastError(self: *NapiEnv, err: NapiStatus) napi_status { + pub fn setLastError(self: ?*NapiEnv, err: NapiStatus) napi_status { return napi_set_last_error(self, err); } @@ -50,9 +46,32 @@ pub const NapiEnv = opaque { } return self.setLastError(.generic_failure); } + + /// Assert that we're not currently performing garbage collection + pub fn checkGC(self: *NapiEnv) void { + napi_internal_check_gc(self); + } + + /// Return the Node-API version number declared by the module we are running code from + pub fn getVersion(self: *NapiEnv) u32 { + return napi_internal_get_version(self); + } + + extern fn NapiEnv__globalObject(*NapiEnv) *JSC.JSGlobalObject; + extern fn napi_internal_get_version(*NapiEnv) u32; }; -pub const napi_env = *NapiEnv; +fn envIsNull() napi_status { + // in this case we don't actually have an environment to set the last error on, so it doesn't + // make sense to call napi_set_last_error + @branchHint(.cold); + return @intFromEnum(NapiStatus.invalid_arg); +} + +/// This is nullable because native modules may pass null pointers for the NAPI environment, which +/// is an error that our NAPI functions need to handle (by returning napi_invalid_arg). To specify +/// a Zig API that uses a never-null napi_env, use `*NapiEnv`. +pub const napi_env = ?*NapiEnv; /// Contents are not used by any Zig code pub const Ref = opaque {}; @@ -60,35 +79,35 @@ pub const Ref = opaque {}; pub const napi_ref = *Ref; pub const NapiHandleScope = opaque { - pub extern fn NapiHandleScope__open(globalObject: *JSC.JSGlobalObject, escapable: bool) ?*NapiHandleScope; - pub extern fn NapiHandleScope__close(globalObject: *JSC.JSGlobalObject, current: ?*NapiHandleScope) void; - extern fn NapiHandleScope__append(globalObject: *JSC.JSGlobalObject, value: JSValue) void; - extern fn NapiHandleScope__escape(handleScope: *NapiHandleScope, value: JSValue) bool; + pub extern fn NapiHandleScope__open(env: *NapiEnv, escapable: bool) ?*NapiHandleScope; + pub extern fn NapiHandleScope__close(env: *NapiEnv, current: ?*NapiHandleScope) void; + extern fn NapiHandleScope__append(env: *NapiEnv, value: JSC.JSValueReprInt) void; + extern fn NapiHandleScope__escape(handleScope: *NapiHandleScope, value: JSC.JSValueReprInt) bool; /// Create a new handle scope in the given environment, or return null if creating one now is /// unsafe (i.e. inside a finalizer) - pub fn open(env: napi_env, escapable: bool) ?*NapiHandleScope { - return NapiHandleScope__open(env.toJS(), escapable); + pub fn open(env: *NapiEnv, escapable: bool) ?*NapiHandleScope { + return NapiHandleScope__open(env, escapable); } /// Closes the given handle scope, releasing all values inside it, if it is safe to do so. /// Asserts that self is the current handle scope in env. - pub fn close(self: ?*NapiHandleScope, env: napi_env) void { - NapiHandleScope__close(env.toJS(), self); + pub fn close(self: ?*NapiHandleScope, env: *NapiEnv) void { + NapiHandleScope__close(env, self); } /// Place a value in the handle scope. Must be done while returning any JS value into NAPI /// callbacks, as the value must remain alive as long as the handle scope is active, even if the /// native module doesn't keep it visible on the stack. - pub fn append(env: napi_env, value: JSC.JSValue) void { - NapiHandleScope__append(env.toJS(), value); + pub fn append(env: *NapiEnv, value: JSC.JSValue) void { + NapiHandleScope__append(env, @intFromEnum(value)); } /// Move a value from the current handle scope (which must be escapable) to the reserved escape /// slot in the parent handle scope, allowing that value to outlive the current handle scope. /// Returns an error if escape() has already been called on this handle scope. pub fn escape(self: *NapiHandleScope, value: JSC.JSValue) error{EscapeCalledTwice}!void { - if (!NapiHandleScope__escape(self, value)) { + if (!NapiHandleScope__escape(self, @intFromEnum(value))) { return error.EscapeCalledTwice; } } @@ -106,7 +125,7 @@ pub const napi_value = enum(i64) { pub fn set( self: *napi_value, - env: napi_env, + env: *NapiEnv, val: JSC.JSValue, ) void { NapiHandleScope.append(env, val); @@ -117,7 +136,7 @@ pub const napi_value = enum(i64) { return @enumFromInt(@intFromEnum(self.*)); } - pub fn create(env: napi_env, val: JSC.JSValue) napi_value { + pub fn create(env: *NapiEnv, val: JSC.JSValue) napi_value { NapiHandleScope.append(env, val); return @enumFromInt(@intFromEnum(val)); } @@ -247,16 +266,24 @@ const napi_type_tag = extern struct { upper: u64, }; pub extern fn napi_get_last_error_info(env: napi_env, result: [*c][*c]const napi_extended_error_info) napi_status; -pub export fn napi_get_undefined(env: napi_env, result_: ?*napi_value) napi_status { +pub export fn napi_get_undefined(env_: napi_env, result_: ?*napi_value) napi_status { log("napi_get_undefined", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; result.set(env, JSValue.jsUndefined()); return env.ok(); } -pub export fn napi_get_null(env: napi_env, result_: ?*napi_value) napi_status { +pub export fn napi_get_null(env_: napi_env, result_: ?*napi_value) napi_status { log("napi_get_null", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; @@ -264,24 +291,36 @@ pub export fn napi_get_null(env: napi_env, result_: ?*napi_value) napi_status { return env.ok(); } pub extern fn napi_get_global(env: napi_env, result: *napi_value) napi_status; -pub export fn napi_get_boolean(env: napi_env, value: bool, result_: ?*napi_value) napi_status { +pub export fn napi_get_boolean(env_: napi_env, value: bool, result_: ?*napi_value) napi_status { log("napi_get_boolean", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; result.set(env, JSValue.jsBoolean(value)); return env.ok(); } -pub export fn napi_create_array(env: napi_env, result_: ?*napi_value) napi_status { +pub export fn napi_create_array(env_: napi_env, result_: ?*napi_value) napi_status { log("napi_create_array", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; result.set(env, JSValue.createEmptyArray(env.toJS(), 0)); return env.ok(); } -pub export fn napi_create_array_with_length(env: napi_env, length: usize, result_: ?*napi_value) napi_status { +pub export fn napi_create_array_with_length(env_: napi_env, length: usize, result_: ?*napi_value) napi_status { log("napi_create_array_with_length", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; @@ -296,46 +335,66 @@ pub export fn napi_create_array_with_length(env: napi_env, length: usize, result return env.ok(); } pub extern fn napi_create_double(_: napi_env, value: f64, result: *napi_value) napi_status; -pub export fn napi_create_int32(env: napi_env, value: i32, result_: ?*napi_value) napi_status { +pub export fn napi_create_int32(env_: napi_env, value: i32, result_: ?*napi_value) napi_status { log("napi_create_int32", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; result.set(env, JSValue.jsNumber(value)); return env.ok(); } -pub export fn napi_create_uint32(env: napi_env, value: u32, result_: ?*napi_value) napi_status { +pub export fn napi_create_uint32(env_: napi_env, value: u32, result_: ?*napi_value) napi_status { log("napi_create_uint32", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; result.set(env, JSValue.jsNumber(value)); return env.ok(); } -pub export fn napi_create_int64(env: napi_env, value: i64, result_: ?*napi_value) napi_status { +pub export fn napi_create_int64(env_: napi_env, value: i64, result_: ?*napi_value) napi_status { log("napi_create_int64", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; result.set(env, JSValue.jsNumber(value)); return env.ok(); } -pub export fn napi_create_string_latin1(env: napi_env, str: ?[*]const u8, length: usize, result_: ?*napi_value) napi_status { +pub export fn napi_create_string_latin1(env_: napi_env, str: ?[*]const u8, length: usize, result_: ?*napi_value) napi_status { + const env = env_ orelse { + return envIsNull(); + }; const result: *napi_value = result_ orelse { return env.invalidArg(); }; const slice: []const u8 = brk: { - if (NAPI_AUTO_LENGTH == length) { - break :brk bun.sliceTo(@as([*:0]const u8, @ptrCast(str)), 0); - } else if (length > std.math.maxInt(u32)) { - return env.invalidArg(); + if (str) |ptr| { + if (NAPI_AUTO_LENGTH == length) { + break :brk bun.sliceTo(@as([*:0]const u8, @ptrCast(ptr)), 0); + } else if (length > std.math.maxInt(u32)) { + return env.invalidArg(); + } else { + break :brk ptr[0..length]; + } } - if (str) |ptr| - break :brk ptr[0..length]; - - return env.invalidArg(); + if (length == 0) { + break :brk &.{}; + } else { + return env.invalidArg(); + } }; log("napi_create_string_latin1: {s}", .{slice}); @@ -353,21 +412,30 @@ pub export fn napi_create_string_latin1(env: napi_env, str: ?[*]const u8, length result.set(env, string.toJS(env.toJS())); return env.ok(); } -pub export fn napi_create_string_utf8(env: napi_env, str: ?[*]const u8, length: usize, result_: ?*napi_value) napi_status { +pub export fn napi_create_string_utf8(env_: napi_env, str: ?[*]const u8, length: usize, result_: ?*napi_value) napi_status { + const env = env_ orelse { + return envIsNull(); + }; const result: *napi_value = result_ orelse { return env.invalidArg(); }; + const slice: []const u8 = brk: { - if (NAPI_AUTO_LENGTH == length) { - break :brk bun.sliceTo(@as([*:0]const u8, @ptrCast(str)), 0); - } else if (length > std.math.maxInt(u32)) { - return env.invalidArg(); + if (str) |ptr| { + if (NAPI_AUTO_LENGTH == length) { + break :brk bun.sliceTo(@as([*:0]const u8, @ptrCast(str)), 0); + } else if (length > std.math.maxInt(u32)) { + return env.invalidArg(); + } else { + break :brk ptr[0..length]; + } } - if (str) |ptr| - break :brk ptr[0..length]; - - return env.invalidArg(); + if (length == 0) { + break :brk &.{}; + } else { + return env.invalidArg(); + } }; log("napi_create_string_utf8: {s}", .{slice}); @@ -380,22 +448,30 @@ pub export fn napi_create_string_utf8(env: napi_env, str: ?[*]const u8, length: result.set(env, string); return env.ok(); } -pub export fn napi_create_string_utf16(env: napi_env, str: ?[*]const char16_t, length: usize, result_: ?*napi_value) napi_status { +pub export fn napi_create_string_utf16(env_: napi_env, str: ?[*]const char16_t, length: usize, result_: ?*napi_value) napi_status { + const env = env_ orelse { + return envIsNull(); + }; const result: *napi_value = result_ orelse { return env.invalidArg(); }; const slice: []const u16 = brk: { - if (NAPI_AUTO_LENGTH == length) { - break :brk bun.sliceTo(@as([*:0]const u16, @ptrCast(str)), 0); - } else if (length > std.math.maxInt(u32)) { - return env.invalidArg(); + if (str) |ptr| { + if (NAPI_AUTO_LENGTH == length) { + break :brk bun.sliceTo(@as([*:0]const u16, @ptrCast(str)), 0); + } else if (length > std.math.maxInt(u32)) { + return env.invalidArg(); + } else { + break :brk ptr[0..length]; + } } - if (str) |ptr| - break :brk ptr[0..length]; - - return env.invalidArg(); + if (length == 0) { + break :brk &.{}; + } else { + return env.invalidArg(); + } }; if (comptime bun.Environment.allow_assert) @@ -403,6 +479,7 @@ pub export fn napi_create_string_utf16(env: napi_env, str: ?[*]const char16_t, l if (slice.len == 0) { result.set(env, bun.String.empty.toJS(env.toJS())); + return env.ok(); } var string, const chars = bun.String.createUninitialized(.utf16, slice.len); @@ -411,6 +488,7 @@ pub export fn napi_create_string_utf16(env: napi_env, str: ?[*]const char16_t, l result.set(env, string.transferToJS(env.toJS())); return env.ok(); } + pub extern fn napi_create_symbol(env: napi_env, description: napi_value, result: *napi_value) napi_status; pub extern fn napi_create_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; pub extern fn napi_create_type_error(env: napi_env, code: napi_value, msg: napi_value, result: *napi_value) napi_status; @@ -420,69 +498,9 @@ pub extern fn napi_get_value_double(env: napi_env, value: napi_value, result: *f pub extern fn napi_get_value_int32(_: napi_env, value_: napi_value, result: ?*i32) napi_status; pub extern fn napi_get_value_uint32(_: napi_env, value_: napi_value, result_: ?*u32) napi_status; pub extern fn napi_get_value_int64(_: napi_env, value_: napi_value, result_: ?*i64) napi_status; -pub export fn napi_get_value_bool(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { - log("napi_get_value_bool", .{}); - const result = result_ orelse { - return env.invalidArg(); - }; - const value = value_.get(); +pub extern fn napi_get_value_bool(_: napi_env, value_: napi_value, result_: ?*bool) napi_status; - result.* = value.to(bool); - return env.ok(); -} -inline fn maybeAppendNull(ptr: anytype, doit: bool) void { - if (doit) { - ptr.* = 0; - } -} -pub export fn napi_get_value_string_latin1(env: napi_env, value_: napi_value, buf_ptr_: ?[*:0]c_char, bufsize: usize, result_ptr: ?*usize) napi_status { - log("napi_get_value_string_latin1", .{}); - const value = value_.get(); - defer value.ensureStillAlive(); - const buf_ptr = @as(?[*:0]u8, @ptrCast(buf_ptr_)); - - const str = value.toBunString(env.toJS()) catch @panic("unexpected exception"); - defer str.deref(); - - var buf = buf_ptr orelse { - if (result_ptr) |result| { - result.* = str.latin1ByteLength(); - } - - return env.ok(); - }; - - if (str.isEmpty()) { - if (result_ptr) |result| { - result.* = 0; - } - buf[0] = 0; - - return env.ok(); - } - - var buf_ = buf[0..bufsize]; - - if (bufsize == NAPI_AUTO_LENGTH) { - buf_ = bun.sliceTo(buf_ptr.?, 0); - if (buf_.len == 0) { - if (result_ptr) |result| { - result.* = 0; - } - return env.ok(); - } - } - const written = str.encodeInto(buf_, .latin1) catch unreachable; - const max_buf_len = buf_.len; - - if (result_ptr) |result| { - result.* = written; - } else if (written < max_buf_len) { - buf[written] = 0; - } - - return env.ok(); -} +pub extern fn napi_get_value_string_latin1(env: napi_env, value_: napi_value, buf_ptr_: ?[*:0]c_char, bufsize: usize, result_ptr: ?*usize) napi_status; /// Copies a JavaScript string into a UTF-8 string buffer. The result is the /// number of bytes (excluding the null terminator) copied into buf. @@ -493,88 +511,22 @@ pub export fn napi_get_value_string_latin1(env: napi_env, value_: napi_value, bu /// via the result parameter. /// The result argument is optional unless buf is NULL. pub extern fn napi_get_value_string_utf8(env: napi_env, value: napi_value, buf_ptr: [*c]u8, bufsize: usize, result_ptr: ?*usize) napi_status; -pub export fn napi_get_value_string_utf16(env: napi_env, value_: napi_value, buf_ptr: ?[*]char16_t, bufsize: usize, result_ptr: ?*usize) napi_status { - log("napi_get_value_string_utf16", .{}); - const value = value_.get(); - defer value.ensureStillAlive(); - const str = value.toBunString(env.toJS()) catch @panic("unexpected exception"); - defer str.deref(); - - var buf = buf_ptr orelse { - if (result_ptr) |result| { - result.* = str.utf16ByteLength(); - } - - return env.ok(); - }; - - if (str.isEmpty()) { - if (result_ptr) |result| { - result.* = 0; - } - buf[0] = 0; - - return env.ok(); - } - - var buf_ = buf[0..bufsize]; - - if (bufsize == NAPI_AUTO_LENGTH) { - buf_ = bun.sliceTo(@as([*:0]u16, @ptrCast(buf_ptr.?)), 0); - if (buf_.len == 0) { - if (result_ptr) |result| { - result.* = 0; - } - return env.ok(); - } - } - - const max_buf_len = buf_.len; - const written = (str.encodeInto(std.mem.sliceAsBytes(buf_), .utf16le) catch unreachable) >> 1; - - if (result_ptr) |result| { - result.* = written; - // We should only write to the buffer is no result pointer is provided. - // If we perform both operations, - } else if (written < max_buf_len) { - buf[written] = 0; - } - - return env.ok(); -} -pub export fn napi_coerce_to_bool(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status { - log("napi_coerce_to_bool", .{}); - const result = result_ orelse { - return env.invalidArg(); - }; - const value = value_.get(); - result.set(env, JSValue.jsBoolean(value.coerce(bool, env.toJS()))); - return env.ok(); -} -pub export fn napi_coerce_to_number(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status { - log("napi_coerce_to_number", .{}); - const result = result_ orelse { - return env.invalidArg(); - }; - const value = value_.get(); - result.set(env, JSC.JSValue.jsNumber(JSC.C.JSValueToNumber(env.toJS().ref(), value.asObjectRef(), TODO_EXCEPTION))); - return env.ok(); -} -pub export fn napi_coerce_to_object(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status { - log("napi_coerce_to_object", .{}); - const result = result_ orelse { - return env.invalidArg(); - }; - const value = value_.get(); - result.set(env, JSValue.c(JSC.C.JSValueToObject(env.toJS().ref(), value.asObjectRef(), TODO_EXCEPTION))); - return env.ok(); -} -pub export fn napi_get_prototype(env: napi_env, object_: napi_value, result_: ?*napi_value) napi_status { +pub extern fn napi_get_value_string_utf16(env: napi_env, value_: napi_value, buf_ptr: ?[*]char16_t, bufsize: usize, result_ptr: ?*usize) napi_status; +pub extern fn napi_coerce_to_bool(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status; +pub extern fn napi_coerce_to_number(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status; +pub extern fn napi_coerce_to_object(env: napi_env, value_: napi_value, result_: ?*napi_value) napi_status; +pub export fn napi_get_prototype(env_: napi_env, object_: napi_value, result_: ?*napi_value) napi_status { log("napi_get_prototype", .{}); + const env = env_ orelse { + return envIsNull(); + }; const result = result_ orelse { return env.invalidArg(); }; const object = object_.get(); + if (object == .zero) { + return env.invalidArg(); + } if (!object.isObject()) { return env.setLastError(.object_expected); } @@ -591,37 +543,17 @@ pub export fn napi_get_prototype(env: napi_env, object_: napi_value, result_: ?* // result.* = // } -pub export fn napi_set_element(env: napi_env, object_: napi_value, index: c_uint, value_: napi_value) napi_status { - log("napi_set_element", .{}); - const object = object_.get(); - const value = value_.get(); - if (!object.jsType().isIndexable()) { - return env.setLastError(.array_expected); - } - if (value == .zero) - return env.invalidArg(); - JSC.C.JSObjectSetPropertyAtIndex(env.toJS().ref(), object.asObjectRef(), index, value.asObjectRef(), TODO_EXCEPTION); - return env.ok(); -} -pub export fn napi_has_element(env: napi_env, object_: napi_value, index: c_uint, result_: ?*bool) napi_status { - log("napi_has_element", .{}); - const result = result_ orelse { - return env.invalidArg(); - }; - const object = object_.get(); - - if (!object.jsType().isIndexable()) { - return env.setLastError(.array_expected); - } - - result.* = object.getLength(env.toJS()) > index; - return env.ok(); -} +pub extern fn napi_set_element(env_: napi_env, object_: napi_value, index: c_uint, value_: napi_value) napi_status; +pub extern fn napi_has_element(env_: napi_env, object_: napi_value, index: c_uint, result_: ?*bool) napi_status; pub extern fn napi_get_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; pub extern fn napi_delete_element(env: napi_env, object: napi_value, index: u32, result: *napi_value) napi_status; pub extern fn napi_define_properties(env: napi_env, object: napi_value, property_count: usize, properties: [*c]const napi_property_descriptor) napi_status; -pub export fn napi_is_array(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_array(env_: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_array", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; @@ -629,8 +561,11 @@ pub export fn napi_is_array(env: napi_env, value_: napi_value, result_: ?*bool) result.* = value.jsType().isArray(); return env.ok(); } -pub export fn napi_get_array_length(env: napi_env, value_: napi_value, result_: [*c]u32) napi_status { +pub export fn napi_get_array_length(env_: napi_env, value_: napi_value, result_: [*c]u32) napi_status { log("napi_get_array_length", .{}); + const env = env_ orelse { + return envIsNull(); + }; const result = result_ orelse { return env.invalidArg(); }; @@ -643,8 +578,11 @@ pub export fn napi_get_array_length(env: napi_env, value_: napi_value, result_: result.* = @as(u32, @truncate(value.getLength(env.toJS()))); return env.ok(); } -pub export fn napi_strict_equals(env: napi_env, lhs_: napi_value, rhs_: napi_value, result_: ?*bool) napi_status { +pub export fn napi_strict_equals(env_: napi_env, lhs_: napi_value, rhs_: napi_value, result_: ?*bool) napi_status { log("napi_strict_equals", .{}); + const env = env_ orelse { + return envIsNull(); + }; const result = result_ orelse { return env.invalidArg(); }; @@ -655,16 +593,7 @@ pub export fn napi_strict_equals(env: napi_env, lhs_: napi_value, rhs_: napi_val } pub extern fn napi_call_function(env: napi_env, recv: napi_value, func: napi_value, argc: usize, argv: [*c]const napi_value, result: *napi_value) napi_status; pub extern fn napi_new_instance(env: napi_env, constructor: napi_value, argc: usize, argv: [*c]const napi_value, result_: ?*napi_value) napi_status; -pub export fn napi_instanceof(env: napi_env, object_: napi_value, constructor_: napi_value, result_: ?*bool) napi_status { - log("napi_instanceof", .{}); - const result = result_ orelse { - return env.invalidArg(); - }; - const object, const constructor = .{ object_.get(), constructor_.get() }; - // TODO: does this throw object_expected in node? - result.* = object.isObject() and object.isInstanceOf(env.toJS(), constructor); - return env.ok(); -} +pub extern fn napi_instanceof(env_: napi_env, object_: napi_value, constructor_: napi_value, result_: ?*bool) napi_status; pub extern fn napi_get_cb_info(env: napi_env, cbinfo: napi_callback_info, argc: [*c]usize, argv: *napi_value, this_arg: *napi_value, data: [*]*anyopaque) napi_status; pub extern fn napi_get_new_target(env: napi_env, cbinfo: napi_callback_info, result: *napi_value) napi_status; pub extern fn napi_define_class( @@ -688,10 +617,13 @@ pub extern fn napi_delete_reference(env: napi_env, ref: napi_ref) napi_status; pub extern fn napi_reference_ref(env: napi_env, ref: napi_ref, result: [*c]u32) napi_status; pub extern fn napi_reference_unref(env: napi_env, ref: napi_ref, result: [*c]u32) napi_status; pub extern fn napi_get_reference_value(env: napi_env, ref: napi_ref, result: *napi_value) napi_status; -pub extern fn napi_get_reference_value_internal(ref: napi_ref) JSC.JSValue; -pub export fn napi_open_handle_scope(env: napi_env, result_: ?*napi_handle_scope) napi_status { +pub export fn napi_open_handle_scope(env_: napi_env, result_: ?*napi_handle_scope) napi_status { log("napi_open_handle_scope", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; @@ -699,8 +631,12 @@ pub export fn napi_open_handle_scope(env: napi_env, result_: ?*napi_handle_scope return env.ok(); } -pub export fn napi_close_handle_scope(env: napi_env, handle_scope: napi_handle_scope) napi_status { +pub export fn napi_close_handle_scope(env_: napi_env, handle_scope: napi_handle_scope) napi_status { log("napi_close_handle_scope", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); if (handle_scope) |scope| { scope.close(env); } @@ -709,21 +645,30 @@ pub export fn napi_close_handle_scope(env: napi_env, handle_scope: napi_handle_s } // we don't support async contexts -pub export fn napi_async_init(env: napi_env, _: napi_value, _: napi_value, async_ctx: **anyopaque) napi_status { +pub export fn napi_async_init(env_: napi_env, _: napi_value, _: napi_value, async_ctx: **anyopaque) napi_status { log("napi_async_init", .{}); + const env = env_ orelse { + return envIsNull(); + }; async_ctx.* = env; return env.ok(); } // we don't support async contexts -pub export fn napi_async_destroy(env: napi_env, _: *anyopaque) napi_status { +pub export fn napi_async_destroy(env_: napi_env, _: *anyopaque) napi_status { log("napi_async_destroy", .{}); + const env = env_ orelse { + return envIsNull(); + }; return env.ok(); } // this is just a regular function call -pub export fn napi_make_callback(env: napi_env, _: *anyopaque, recv_: napi_value, func_: napi_value, arg_count: usize, args: ?[*]const napi_value, maybe_result: ?*napi_value) napi_status { +pub export fn napi_make_callback(env_: napi_env, _: *anyopaque, recv_: napi_value, func_: napi_value, arg_count: usize, args: ?[*]const napi_value, maybe_result: ?*napi_value) napi_status { log("napi_make_callback", .{}); + const env = env_ orelse { + return envIsNull(); + }; const recv, const func = .{ recv_.get(), func_.get() }; if (func.isEmptyOrUndefinedOrNull() or !func.isCallable(env.toJS().vm())) { return env.setLastError(.function_expected); @@ -771,23 +716,35 @@ fn notImplementedYet(comptime name: []const u8) void { ); } -pub export fn napi_open_escapable_handle_scope(env: napi_env, result_: ?*napi_escapable_handle_scope) napi_status { +pub export fn napi_open_escapable_handle_scope(env_: napi_env, result_: ?*napi_escapable_handle_scope) napi_status { log("napi_open_escapable_handle_scope", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; result.* = NapiHandleScope.open(env, true); return env.ok(); } -pub export fn napi_close_escapable_handle_scope(env: napi_env, scope: napi_escapable_handle_scope) napi_status { +pub export fn napi_close_escapable_handle_scope(env_: napi_env, scope: napi_escapable_handle_scope) napi_status { log("napi_close_escapable_handle_scope", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); if (scope) |s| { s.close(env); } return env.ok(); } -pub export fn napi_escape_handle(env: napi_env, scope_: napi_escapable_handle_scope, escapee: napi_value, result_: ?*napi_value) napi_status { +pub export fn napi_escape_handle(env_: napi_env, scope_: napi_escapable_handle_scope, escapee: napi_value, result_: ?*napi_value) napi_status { log("napi_escape_handle", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; @@ -798,32 +755,46 @@ pub export fn napi_escape_handle(env: napi_env, scope_: napi_escapable_handle_sc result.* = escapee; return env.ok(); } -pub extern fn napi_type_tag_object(_: napi_env, _: napi_value, _: [*c]const napi_type_tag) napi_status; -pub extern fn napi_check_object_type_tag(_: napi_env, _: napi_value, _: [*c]const napi_type_tag, _: *bool) napi_status; +pub extern fn napi_type_tag_object(env: napi_env, _: napi_value, _: [*c]const napi_type_tag) napi_status; +pub extern fn napi_check_object_type_tag(env: napi_env, _: napi_value, _: [*c]const napi_type_tag, _: *bool) napi_status; // do nothing for both of these -pub export fn napi_open_callback_scope(env: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status { +pub export fn napi_open_callback_scope(env_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status { log("napi_open_callback_scope", .{}); + const env = env_ orelse { + return envIsNull(); + }; return env.ok(); } -pub export fn napi_close_callback_scope(env: napi_env, _: *anyopaque) napi_status { +pub export fn napi_close_callback_scope(env_: napi_env, _: *anyopaque) napi_status { log("napi_close_callback_scope", .{}); + const env = env_ orelse { + return envIsNull(); + }; return env.ok(); } pub extern fn napi_throw(env: napi_env, @"error": napi_value) napi_status; pub extern fn napi_throw_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status; pub extern fn napi_throw_type_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status; pub extern fn napi_throw_range_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status; -pub export fn napi_is_error(env: napi_env, value_: napi_value, result: *bool) napi_status { +pub export fn napi_is_error(env_: napi_env, value_: napi_value, result: *bool) napi_status { log("napi_is_error", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const value = value_.get(); result.* = value.isAnyError(); return env.ok(); } pub extern fn napi_is_exception_pending(env: napi_env, result: *bool) napi_status; pub extern fn napi_get_and_clear_last_exception(env: napi_env, result: *napi_value) napi_status; -pub export fn napi_is_arraybuffer(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_arraybuffer(env_: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_arraybuffer", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; @@ -835,8 +806,12 @@ pub extern fn napi_create_arraybuffer(env: napi_env, byte_length: usize, data: [ pub extern fn napi_create_external_arraybuffer(env: napi_env, external_data: ?*anyopaque, byte_length: usize, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: *napi_value) napi_status; -pub export fn napi_get_arraybuffer_info(env: napi_env, arraybuffer_: napi_value, data: ?*[*]u8, byte_length: ?*usize) napi_status { +pub export fn napi_get_arraybuffer_info(env_: napi_env, arraybuffer_: napi_value, data: ?*[*]u8, byte_length: ?*usize) napi_status { log("napi_get_arraybuffer_info", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const arraybuffer = arraybuffer_.get(); const array_buffer = arraybuffer.asArrayBuffer(env.toJS()) orelse return env.setLastError(.arraybuffer_expected); const slice = array_buffer.slice(); @@ -849,26 +824,8 @@ pub export fn napi_get_arraybuffer_info(env: napi_env, arraybuffer_: napi_value, pub extern fn napi_is_typedarray(napi_env, napi_value, *bool) napi_status; -pub export fn napi_create_typedarray(env: napi_env, @"type": napi_typedarray_type, length: usize, arraybuffer_: napi_value, byte_offset: usize, result_: ?*napi_value) napi_status { - log("napi_create_typedarray", .{}); - const arraybuffer = arraybuffer_.get(); - const result = result_ orelse { - return env.invalidArg(); - }; - result.set(env, JSValue.c( - JSC.C.JSObjectMakeTypedArrayWithArrayBufferAndOffset( - env.toJS().ref(), - @"type".toC(), - arraybuffer.asObjectRef(), - byte_offset, - length, - TODO_EXCEPTION, - ), - )); - return env.ok(); -} pub export fn napi_get_typedarray_info( - env: napi_env, + env_: napi_env, typedarray_: napi_value, maybe_type: ?*napi_typedarray_type, maybe_length: ?*usize, @@ -877,6 +834,10 @@ pub export fn napi_get_typedarray_info( maybe_byte_offset: ?*usize, ) napi_status { log("napi_get_typedarray_info", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const typedarray = typedarray_.get(); if (typedarray.isEmptyOrUndefinedOrNull()) return env.invalidArg(); @@ -901,8 +862,11 @@ pub export fn napi_get_typedarray_info( return env.ok(); } pub extern fn napi_create_dataview(env: napi_env, length: usize, arraybuffer: napi_value, byte_offset: usize, result: *napi_value) napi_status; -pub export fn napi_is_dataview(env: napi_env, value_: napi_value, result_: ?*bool) napi_status { +pub export fn napi_is_dataview(env_: napi_env, value_: napi_value, result_: ?*bool) napi_status { log("napi_is_dataview", .{}); + const env = env_ orelse { + return envIsNull(); + }; const result = result_ orelse { return env.invalidArg(); }; @@ -911,7 +875,7 @@ pub export fn napi_is_dataview(env: napi_env, value_: napi_value, result_: ?*boo return env.ok(); } pub export fn napi_get_dataview_info( - env: napi_env, + env_: napi_env, dataview_: napi_value, maybe_bytelength: ?*usize, maybe_data: ?*[*]u8, @@ -919,6 +883,10 @@ pub export fn napi_get_dataview_info( maybe_byte_offset: ?*usize, ) napi_status { log("napi_get_dataview_info", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const dataview = dataview_.get(); const array_buffer = dataview.asArrayBuffer(env.toJS()) orelse return env.setLastError(.object_expected); if (maybe_bytelength) |bytelength| @@ -935,16 +903,23 @@ pub export fn napi_get_dataview_info( return env.ok(); } -pub export fn napi_get_version(env: napi_env, result_: ?*u32) napi_status { +pub export fn napi_get_version(env_: napi_env, result_: ?*u32) napi_status { log("napi_get_version", .{}); + const env = env_ orelse { + return envIsNull(); + }; const result = result_ orelse { return env.invalidArg(); }; - result.* = NAPI_VERSION; + // The result is supposed to be the highest NAPI version Bun supports, rather than the version reported by a NAPI module. + result.* = 9; return env.ok(); } -pub export fn napi_create_promise(env: napi_env, deferred_: ?*napi_deferred, promise_: ?*napi_value) napi_status { +pub export fn napi_create_promise(env_: napi_env, deferred_: ?*napi_deferred, promise_: ?*napi_value) napi_status { log("napi_create_promise", .{}); + const env = env_ orelse { + return envIsNull(); + }; const deferred = deferred_ orelse { return env.invalidArg(); }; @@ -956,8 +931,11 @@ pub export fn napi_create_promise(env: napi_env, deferred_: ?*napi_deferred, pro promise.set(env, deferred.*.get().asValue(env.toJS())); return env.ok(); } -pub export fn napi_resolve_deferred(env: napi_env, deferred: napi_deferred, resolution_: napi_value) napi_status { +pub export fn napi_resolve_deferred(env_: napi_env, deferred: napi_deferred, resolution_: napi_value) napi_status { log("napi_resolve_deferred", .{}); + const env = env_ orelse { + return envIsNull(); + }; const resolution = resolution_.get(); var prom = deferred.get(); prom.resolve(env.toJS(), resolution); @@ -965,8 +943,11 @@ pub export fn napi_resolve_deferred(env: napi_env, deferred: napi_deferred, reso bun.default_allocator.destroy(deferred); return env.ok(); } -pub export fn napi_reject_deferred(env: napi_env, deferred: napi_deferred, rejection_: napi_value) napi_status { +pub export fn napi_reject_deferred(env_: napi_env, deferred: napi_deferred, rejection_: napi_value) napi_status { log("napi_reject_deferred", .{}); + const env = env_ orelse { + return envIsNull(); + }; const rejection = rejection_.get(); var prom = deferred.get(); prom.reject(env.toJS(), rejection); @@ -974,8 +955,12 @@ pub export fn napi_reject_deferred(env: napi_env, deferred: napi_deferred, rejec bun.default_allocator.destroy(deferred); return env.ok(); } -pub export fn napi_is_promise(env: napi_env, value_: napi_value, is_promise_: ?*bool) napi_status { +pub export fn napi_is_promise(env_: napi_env, value_: napi_value, is_promise_: ?*bool) napi_status { log("napi_is_promise", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const value = value_.get(); const is_promise = is_promise_ orelse { return env.invalidArg(); @@ -990,8 +975,11 @@ pub export fn napi_is_promise(env: napi_env, value_: napi_value, is_promise_: ?* } pub extern fn napi_run_script(env: napi_env, script: napi_value, result: *napi_value) napi_status; pub extern fn napi_adjust_external_memory(env: napi_env, change_in_bytes: i64, adjusted_value: [*c]i64) napi_status; -pub export fn napi_create_date(env: napi_env, time: f64, result_: ?*napi_value) napi_status { +pub export fn napi_create_date(env_: napi_env, time: f64, result_: ?*napi_value) napi_status { log("napi_create_date", .{}); + const env = env_ orelse { + return envIsNull(); + }; const result = result_ orelse { return env.invalidArg(); }; @@ -999,8 +987,12 @@ pub export fn napi_create_date(env: napi_env, time: f64, result_: ?*napi_value) result.set(env, JSValue.c(JSC.C.JSObjectMakeDate(env.toJS().ref(), 1, &args, TODO_EXCEPTION))); return env.ok(); } -pub export fn napi_is_date(env: napi_env, value_: napi_value, is_date_: ?*bool) napi_status { +pub export fn napi_is_date(env_: napi_env, value_: napi_value, is_date_: ?*bool) napi_status { log("napi_is_date", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const is_date = is_date_ orelse { return env.invalidArg(); }; @@ -1010,16 +1002,24 @@ pub export fn napi_is_date(env: napi_env, value_: napi_value, is_date_: ?*bool) } pub extern fn napi_get_date_value(env: napi_env, value: napi_value, result: *f64) napi_status; pub extern fn napi_add_finalizer(env: napi_env, js_object: napi_value, native_object: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: napi_ref) napi_status; -pub export fn napi_create_bigint_int64(env: napi_env, value: i64, result_: ?*napi_value) napi_status { +pub export fn napi_create_bigint_int64(env_: napi_env, value: i64, result_: ?*napi_value) napi_status { log("napi_create_bigint_int64", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; result.set(env, JSC.JSValue.fromInt64NoTruncate(env.toJS(), value)); return env.ok(); } -pub export fn napi_create_bigint_uint64(env: napi_env, value: u64, result_: ?*napi_value) napi_status { +pub export fn napi_create_bigint_uint64(env_: napi_env, value: u64, result_: ?*napi_value) napi_status { log("napi_create_bigint_uint64", .{}); + const env = env_ orelse { + return envIsNull(); + }; + env.checkGC(); const result = result_ orelse { return env.invalidArg(); }; @@ -1027,8 +1027,8 @@ pub export fn napi_create_bigint_uint64(env: napi_env, value: u64, result_: ?*na return env.ok(); } pub extern fn napi_create_bigint_words(env: napi_env, sign_bit: c_int, word_count: usize, words: [*c]const u64, result: *napi_value) napi_status; -pub extern fn napi_get_value_bigint_int64(env: napi_env, value: napi_value, result: ?*i64, lossless: ?*bool) napi_status; -pub extern fn napi_get_value_bigint_uint64(env: napi_env, value: napi_value, result: ?*u64, lossless: ?*bool) napi_status; +pub extern fn napi_get_value_bigint_int64(_: napi_env, value_: napi_value, result_: ?*i64, _: *bool) napi_status; +pub extern fn napi_get_value_bigint_uint64(_: napi_env, value_: napi_value, result_: ?*u64, _: *bool) napi_status; pub extern fn napi_get_value_bigint_words(env: napi_env, value: napi_value, sign_bit: [*c]c_int, word_count: [*c]usize, words: [*c]u64) napi_status; pub extern fn napi_get_all_property_names(env: napi_env, object: napi_value, key_mode: napi_key_collection_mode, key_filter: napi_key_filter, key_conversion: napi_key_conversion, result: *napi_value) napi_status; @@ -1048,9 +1048,10 @@ pub const napi_async_work = struct { completion_task: ?*anyopaque = null, event_loop: *JSC.EventLoop, global: *JSC.JSGlobalObject, + env: *NapiEnv, execute: napi_async_execute_callback = null, complete: napi_async_complete_callback = null, - ctx: ?*anyopaque = null, + data: ?*anyopaque = null, status: std.atomic.Value(u32) = std.atomic.Value(u32).init(0), can_deinit: bool = false, wait_for_deinit: bool = false, @@ -1063,14 +1064,16 @@ pub const napi_async_work = struct { cancelled = 3, }; - pub fn create(global: *JSC.JSGlobalObject, execute: napi_async_execute_callback, complete: napi_async_complete_callback, ctx: ?*anyopaque) !*napi_async_work { + pub fn create(env: *NapiEnv, execute: napi_async_execute_callback, complete: napi_async_complete_callback, data: ?*anyopaque) !*napi_async_work { const work = try bun.default_allocator.create(napi_async_work); + const global = env.toJS(); work.* = .{ .global = global, + .env = env, .execute = execute, .event_loop = global.bunVM().eventLoop(), .complete = complete, - .ctx = ctx, + .data = data, }; return work; } @@ -1090,7 +1093,7 @@ pub const napi_async_work = struct { } return; } - this.execute.?(NapiEnv.fromJS(this.global), this.ctx); + this.execute.?(this.env, this.data); this.status.store(@intFromEnum(Status.completed), .seq_cst); this.event_loop.enqueueTaskConcurrent(this.concurrent_task.from(this, .manual_deinit)); @@ -1119,15 +1122,15 @@ pub const napi_async_work = struct { } fn runFromJSWithError(this: *napi_async_work) bun.JSError!void { - const handle_scope = NapiHandleScope.open(NapiEnv.fromJS(this.global), false); - defer if (handle_scope) |scope| scope.close(NapiEnv.fromJS(this.global)); + const handle_scope = NapiHandleScope.open(this.env, false); + defer if (handle_scope) |scope| scope.close(this.env); this.complete.?( - NapiEnv.fromJS(this.global), + this.env, @intFromEnum(if (this.status.load(.seq_cst) == @intFromEnum(Status.cancelled)) NapiStatus.cancelled else NapiStatus.ok), - this.ctx.?, + this.data, ); if (this.global.hasException()) { return error.JSError; @@ -1168,7 +1171,7 @@ pub const napi_node_version = extern struct { }; pub const struct_napi_async_cleanup_hook_handle__ = opaque {}; pub const napi_async_cleanup_hook_handle = ?*struct_napi_async_cleanup_hook_handle__; -pub const napi_async_cleanup_hook = *const fn (napi_async_cleanup_hook_handle, ?*anyopaque) callconv(.C) void; +pub const napi_async_cleanup_hook = ?*const fn (napi_async_cleanup_hook_handle, ?*anyopaque) callconv(.C) void; pub const napi_addon_register_func = *const fn (napi_env, napi_value) callconv(.C) napi_value; pub const struct_napi_module = extern struct { @@ -1200,13 +1203,16 @@ pub export fn napi_fatal_error(location_ptr: ?[*:0]const u8, location_len: usize const location = napiSpan(location_ptr, location_len); if (location.len > 0) { - bun.Output.panic("napi: {s}\n {s}", .{ message, location }); + bun.Output.panic("NAPI FATAL ERROR: {s} {s}", .{ location, message }); } bun.Output.panic("napi: {s}", .{message}); } -pub export fn napi_create_buffer(env: napi_env, length: usize, data: ?**anyopaque, result: *napi_value) napi_status { +pub export fn napi_create_buffer(env_: napi_env, length: usize, data: ?**anyopaque, result: *napi_value) napi_status { log("napi_create_buffer: {d}", .{length}); + const env = env_ orelse { + return envIsNull(); + }; var buffer = JSC.JSValue.createBufferFromLength(env.toJS(), length); if (length > 0) { if (data) |ptr| { @@ -1217,8 +1223,11 @@ pub export fn napi_create_buffer(env: napi_env, length: usize, data: ?**anyopaqu return env.ok(); } pub extern fn napi_create_external_buffer(env: napi_env, length: usize, data: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque, result: *napi_value) napi_status; -pub export fn napi_create_buffer_copy(env: napi_env, length: usize, data: [*]u8, result_data: ?*?*anyopaque, result_: ?*napi_value) napi_status { +pub export fn napi_create_buffer_copy(env_: napi_env, length: usize, data: [*]u8, result_data: ?*?*anyopaque, result_: ?*napi_value) napi_status { log("napi_create_buffer_copy: {d}", .{length}); + const env = env_ orelse { + return envIsNull(); + }; const result = result_ orelse { return env.invalidArg(); }; @@ -1237,12 +1246,14 @@ pub export fn napi_create_buffer_copy(env: napi_env, length: usize, data: [*]u8, return env.ok(); } extern fn napi_is_buffer(napi_env, napi_value, *bool) napi_status; -pub export fn napi_get_buffer_info(env: napi_env, value_: napi_value, data: ?*[*]u8, length: ?*usize) napi_status { +pub export fn napi_get_buffer_info(env_: napi_env, value_: napi_value, data: ?*[*]u8, length: ?*usize) napi_status { log("napi_get_buffer_info", .{}); + const env = env_ orelse { + return envIsNull(); + }; const value = value_.get(); const array_buf = value.asArrayBuffer(env.toJS()) orelse { - // TODO: is invalid_arg what to return here? - return env.setLastError(.arraybuffer_expected); + return env.setLastError(.invalid_arg); }; if (data) |dat| @@ -1261,7 +1272,7 @@ extern fn node_api_create_external_string_latin1(napi_env, [*:0]u8, usize, napi_ extern fn node_api_create_external_string_utf16(napi_env, [*:0]u16, usize, napi_finalize, ?*anyopaque, *JSValue, *bool) napi_status; pub export fn napi_create_async_work( - env: napi_env, + env_: napi_env, _: napi_value, _: [*:0]const u8, execute: napi_async_execute_callback, @@ -1270,16 +1281,22 @@ pub export fn napi_create_async_work( result_: ?**napi_async_work, ) napi_status { log("napi_create_async_work", .{}); + const env = env_ orelse { + return envIsNull(); + }; const result = result_ orelse { return env.invalidArg(); }; - result.* = napi_async_work.create(env.toJS(), execute, complete, data) catch { + result.* = napi_async_work.create(env, execute, complete, data) catch { return env.genericFailure(); }; return env.ok(); } -pub export fn napi_delete_async_work(env: napi_env, work_: ?*napi_async_work) napi_status { +pub export fn napi_delete_async_work(env_: napi_env, work_: ?*napi_async_work) napi_status { log("napi_delete_async_work", .{}); + const env = env_ orelse { + return envIsNull(); + }; const work = work_ orelse { return env.invalidArg(); }; @@ -1287,8 +1304,11 @@ pub export fn napi_delete_async_work(env: napi_env, work_: ?*napi_async_work) na work.deinit(); return env.ok(); } -pub export fn napi_queue_async_work(env: napi_env, work_: ?*napi_async_work) napi_status { +pub export fn napi_queue_async_work(env_: napi_env, work_: ?*napi_async_work) napi_status { log("napi_queue_async_work", .{}); + const env = env_ orelse { + return envIsNull(); + }; const work = work_ orelse { return env.invalidArg(); }; @@ -1296,8 +1316,11 @@ pub export fn napi_queue_async_work(env: napi_env, work_: ?*napi_async_work) nap work.schedule(); return env.ok(); } -pub export fn napi_cancel_async_work(env: napi_env, work_: ?*napi_async_work) napi_status { +pub export fn napi_cancel_async_work(env_: napi_env, work_: ?*napi_async_work) napi_status { log("napi_cancel_async_work", .{}); + const env = env_ orelse { + return envIsNull(); + }; const work = work_ orelse { return env.invalidArg(); }; @@ -1308,8 +1331,11 @@ pub export fn napi_cancel_async_work(env: napi_env, work_: ?*napi_async_work) na return env.genericFailure(); } -pub export fn napi_get_node_version(env: napi_env, version_: ?**const napi_node_version) napi_status { +pub export fn napi_get_node_version(env_: napi_env, version_: ?**const napi_node_version) napi_status { log("napi_get_node_version", .{}); + const env = env_ orelse { + return envIsNull(); + }; const version = version_ orelse { return env.invalidArg(); }; @@ -1317,13 +1343,17 @@ pub export fn napi_get_node_version(env: napi_env, version_: ?**const napi_node_ return env.ok(); } const napi_event_loop = if (bun.Environment.isWindows) *bun.windows.libuv.Loop else *JSC.EventLoop; -pub export fn napi_get_uv_event_loop(env: napi_env, loop_: ?*napi_event_loop) napi_status { +pub export fn napi_get_uv_event_loop(env_: napi_env, loop_: ?*napi_event_loop) napi_status { log("napi_get_uv_event_loop", .{}); + const env = env_ orelse { + return envIsNull(); + }; const loop = loop_ orelse { return env.invalidArg(); }; if (bun.Environment.isWindows) { // alignment error is incorrect. + // TODO(@190n) investigate @setRuntimeSafety(false); loop.* = JSC.VirtualMachine.get().uvLoop(); } else { @@ -1333,45 +1363,52 @@ pub export fn napi_get_uv_event_loop(env: napi_env, loop_: ?*napi_event_loop) na return env.ok(); } pub extern fn napi_fatal_exception(env: napi_env, err: napi_value) napi_status; +pub extern fn napi_add_async_cleanup_hook(env: napi_env, function: napi_async_cleanup_hook, data: ?*anyopaque, handle_out: ?*napi_async_cleanup_hook_handle) napi_status; +pub extern fn napi_add_env_cleanup_hook(env: napi_env, function: ?*const fn (?*anyopaque) void, data: ?*anyopaque) napi_status; +pub extern fn napi_create_typedarray(env: napi_env, napi_typedarray_type, length: usize, arraybuffer: napi_value, byte_offset: usize, result: ?*napi_value) napi_status; +pub extern fn napi_remove_async_cleanup_hook(handle: napi_async_cleanup_hook_handle) napi_status; +pub extern fn napi_remove_env_cleanup_hook(env: napi_env, function: ?*const fn (?*anyopaque) void, data: ?*anyopaque) napi_status; -// We use a linked list here because we assume removing these is relatively rare -// and array reallocations are relatively expensive. -pub export fn napi_add_env_cleanup_hook(env: napi_env, fun: ?*const fn (?*anyopaque) callconv(.C) void, arg: ?*anyopaque) napi_status { - log("napi_add_env_cleanup_hook", .{}); - if (fun == null) - return env.ok(); +extern fn napi_internal_cleanup_env_cpp(env: napi_env) callconv(.C) void; +extern fn napi_internal_check_gc(env: napi_env) callconv(.C) void; - env.toJS().bunVM().rareData().pushCleanupHook(env.toJS(), arg, fun.?); - return env.ok(); +pub export fn napi_internal_register_cleanup_zig(env_: napi_env) void { + const env = env_.?; + env.toJS().bunVM().rareData().pushCleanupHook(env.toJS(), env, struct { + fn callback(data: ?*anyopaque) callconv(.C) void { + napi_internal_cleanup_env_cpp(@ptrCast(data)); + } + }.callback); } -pub export fn napi_remove_env_cleanup_hook(env: napi_env, fun: ?*const fn (?*anyopaque) callconv(.C) void, arg: ?*anyopaque) napi_status { - log("napi_remove_env_cleanup_hook", .{}); - // Avoid looking up env.bunVM(). - if (bun.Global.isExiting()) { - return env.ok(); - } +extern fn napi_internal_remove_finalizer(env: napi_env, fun: napi_finalize, hint: ?*anyopaque, data: ?*anyopaque) callconv(.C) void; - const vm = JSC.VirtualMachine.get(); +pub const Finalizer = struct { + env: napi_env, + fun: napi_finalize, + data: ?*anyopaque = null, + hint: ?*anyopaque = null, - if (vm.rare_data == null or fun == null or vm.isShuttingDown()) - return env.ok(); - - var rare_data = vm.rare_data.?; - const cmp = JSC.RareData.CleanupHook.init(env.toJS(), arg, fun.?); - for (rare_data.cleanup_hooks.items, 0..) |*hook, i| { - if (hook.eql(cmp)) { - _ = rare_data.cleanup_hooks.orderedRemove(i); - break; + pub fn run(this: *Finalizer) void { + const env = this.env.?; + const handle_scope = NapiHandleScope.open(env, false); + defer if (handle_scope) |scope| scope.close(env); + if (this.fun) |fun| { + fun(env, this.data, this.hint); + } + napi_internal_remove_finalizer(env, this.fun, this.hint, this.data); + if (env.toJS().tryTakeException()) |exception| { + _ = env.toJS().bunVM().uncaughtException(env.toJS(), exception, false); } } - return env.ok(); -} - -pub const Finalizer = struct { - fun: napi_finalize, - data: ?*anyopaque = null, + /// For Node-API modules not built with NAPI_EXPERIMENTAL, finalizers should be deferred to the + /// immediate task queue instead of run immediately. This lets finalizers perform allocations, + /// which they couldn't if they ran immediately while the garbage collector is still running. + pub export fn napi_internal_enqueue_finalizer(env: napi_env, fun: napi_finalize, data: ?*anyopaque, hint: ?*anyopaque) callconv(.C) void { + const task = NapiFinalizerTask.init(.{ .env = env, .fun = fun, .data = data, .hint = hint }); + task.schedule(); + } }; // TODO: generate comptime version of this instead of runtime checking @@ -1411,9 +1448,9 @@ pub const ThreadSafeFunction = struct { event_loop: *JSC.EventLoop, tracker: JSC.AsyncTaskTracker, - env: napi_env, + env: *NapiEnv, - finalizer: Finalizer = Finalizer{ .fun = null, .data = null }, + finalizer: Finalizer = Finalizer{ .env = null, .fun = null, .data = null }, has_queued_finalizer: bool = false, queue: Queue = .{ .data = std.fifo.LinearFifo(?*anyopaque, .Dynamic).init(bun.default_allocator), @@ -1562,10 +1599,11 @@ pub const ThreadSafeFunction = struct { /// See: https://github.com/nodejs/node/pull/38506 /// In that case, we need to drain microtasks. fn call(this: *ThreadSafeFunction, task: ?*anyopaque, is_first: bool) void { - const globalObject = this.env.toJS(); + const env = this.env; if (!is_first) { this.event_loop.drainMicrotasks(); } + const globalObject = env.toJS(); this.tracker.willDispatch(globalObject); defer this.tracker.didDispatch(globalObject); @@ -1583,9 +1621,9 @@ pub const ThreadSafeFunction = struct { .c => |cb| { const js = cb.js.get() orelse .undefined; - const handle_scope = NapiHandleScope.open(this.env, false); - defer if (handle_scope) |scope| scope.close(this.env); - cb.napi_threadsafe_function_call_js(this.env, napi_value.create(this.env, js), this.ctx, task); + const handle_scope = NapiHandleScope.open(env, false); + defer if (handle_scope) |scope| scope.close(env); + cb.napi_threadsafe_function_call_js(env, napi_value.create(env, js), this.ctx, task); }, } } @@ -1636,9 +1674,7 @@ pub const ThreadSafeFunction = struct { this.unref(); if (this.finalizer.fun) |fun| { - const handle_scope = NapiHandleScope.open(this.env, false); - defer if (handle_scope) |scope| scope.close(this.env); - fun(this.env, this.finalizer.data, this.ctx); + Finalizer.napi_internal_enqueue_finalizer(this.env, fun, this.finalizer.data, this.ctx); } this.callback.deinit(); @@ -1692,10 +1728,10 @@ pub const ThreadSafeFunction = struct { }; pub export fn napi_create_threadsafe_function( - env: napi_env, + env_: napi_env, func_: napi_value, - _: napi_value, - _: napi_value, + _: napi_value, // async_resource + _: napi_value, // async_resource_name max_queue_size: usize, initial_thread_count: usize, thread_finalize_data: ?*anyopaque, @@ -1705,27 +1741,29 @@ pub export fn napi_create_threadsafe_function( result_: ?*napi_threadsafe_function, ) napi_status { log("napi_create_threadsafe_function", .{}); + const env = env_ orelse { + return envIsNull(); + }; const result = result_ orelse { return env.invalidArg(); }; const func = func_.get(); - const global = env.toJS(); - if (call_js_cb == null and (func.isEmptyOrUndefinedOrNull() or !func.isCallable(global.vm()))) { + if (call_js_cb == null and (func.isEmptyOrUndefinedOrNull() or !func.isCallable(env.toJS().vm()))) { return env.setLastError(.function_expected); } - const vm = global.bunVM(); + const vm = env.toJS().bunVM(); var function = ThreadSafeFunction.new(.{ .event_loop = vm.eventLoop(), .env = env, .callback = if (call_js_cb) |c| .{ .c = .{ .napi_threadsafe_function_call_js = c, - .js = if (func == .zero) .{} else JSC.Strong.create(func.withAsyncContextIfNeeded(global), vm.global), + .js = if (func == .zero) .{} else JSC.Strong.create(func.withAsyncContextIfNeeded(env.toJS()), vm.global), }, } else .{ - .js = if (func == .zero) .{} else JSC.Strong.create(func.withAsyncContextIfNeeded(global), vm.global), + .js = if (func == .zero) .{} else JSC.Strong.create(func.withAsyncContextIfNeeded(env.toJS()), vm.global), }, .ctx = context, .queue = ThreadSafeFunction.Queue.init(max_queue_size, bun.default_allocator), @@ -1734,7 +1772,7 @@ pub export fn napi_create_threadsafe_function( .tracker = JSC.AsyncTaskTracker.init(vm), }); - function.finalizer = .{ .data = thread_finalize_data, .fun = thread_finalize_cb }; + function.finalizer = .{ .env = env, .data = thread_finalize_data, .fun = thread_finalize_cb }; // nodejs by default keeps the event loop alive until the thread-safe function is unref'd function.ref(); function.tracker.didSchedule(vm.global); @@ -1759,30 +1797,25 @@ pub export fn napi_release_threadsafe_function(func: napi_threadsafe_function, m log("napi_release_threadsafe_function", .{}); return func.release(mode, false); } -pub export fn napi_unref_threadsafe_function(env: napi_env, func: napi_threadsafe_function) napi_status { +pub export fn napi_unref_threadsafe_function(env_: napi_env, func: napi_threadsafe_function) napi_status { log("napi_unref_threadsafe_function", .{}); + const env = env_ orelse { + return envIsNull(); + }; bun.assert(func.event_loop.global == env.toJS()); func.unref(); return env.ok(); } -pub export fn napi_ref_threadsafe_function(env: napi_env, func: napi_threadsafe_function) napi_status { +pub export fn napi_ref_threadsafe_function(env_: napi_env, func: napi_threadsafe_function) napi_status { log("napi_ref_threadsafe_function", .{}); + const env = env_ orelse { + return envIsNull(); + }; bun.assert(func.event_loop.global == env.toJS()); func.ref(); return env.ok(); } -pub export fn napi_add_async_cleanup_hook(env: napi_env, _: napi_async_cleanup_hook, _: ?*anyopaque, _: [*c]napi_async_cleanup_hook_handle) napi_status { - log("napi_add_async_cleanup_hook", .{}); - // TODO: - return env.ok(); -} -pub export fn napi_remove_async_cleanup_hook(_: napi_async_cleanup_hook_handle) napi_status { - log("napi_remove_async_cleanup_hook", .{}); - // TODO: - return @intFromEnum(NapiStatus.ok); -} - const NAPI_VERSION = @as(c_int, 8); const NAPI_AUTO_LENGTH = std.math.maxInt(usize); const NAPI_MODULE_VERSION = @as(c_int, 1); @@ -2010,7 +2043,6 @@ const napi_functions_to_export = .{ napi_get_null, napi_get_prototype, napi_get_reference_value, - napi_get_reference_value_internal, napi_get_threadsafe_function_context, napi_get_typedarray_info, napi_get_undefined, @@ -2090,3 +2122,41 @@ pub fn fixDeadCodeElimination() void { std.mem.doNotOptimizeAway(&@import("../bun.js/node/buffer.zig").BufferVectorized.fill); } + +pub const NapiFinalizerTask = struct { + finalizer: Finalizer, + + const AnyTask = JSC.AnyTask.New(@This(), runOnJSThread); + + pub fn init(finalizer: Finalizer) *NapiFinalizerTask { + const finalizer_task = bun.default_allocator.create(NapiFinalizerTask) catch bun.outOfMemory(); + finalizer_task.* = .{ + .finalizer = finalizer, + }; + return finalizer_task; + } + + pub fn schedule(this: *NapiFinalizerTask) void { + const vm = this.finalizer.env.?.toJS().bunVM(); + if (vm.isShuttingDown()) { + // Immediate tasks won't run, so we run this as a cleanup hook instead + vm.rareData().pushCleanupHook(vm.global, this, runAsCleanupHook); + } else { + this.finalizer.env.?.toJS().bunVM().event_loop.enqueueImmediateTask(JSC.Task.init(this)); + } + } + + pub fn deinit(this: *NapiFinalizerTask) void { + bun.default_allocator.destroy(this); + } + + pub fn runOnJSThread(this: *NapiFinalizerTask) void { + this.finalizer.run(); + this.deinit(); + } + + fn runAsCleanupHook(opaque_this: ?*anyopaque) callconv(.c) void { + const this: *NapiFinalizerTask = @alignCast(@ptrCast(opaque_this.?)); + this.runOnJSThread(); + } +}; diff --git a/src/symbols.def b/src/symbols.def index 38a1355b7d..4225732ee9 100644 --- a/src/symbols.def +++ b/src/symbols.def @@ -544,7 +544,6 @@ EXPORTS napi_reference_ref napi_reference_unref napi_get_reference_value - napi_get_reference_value_internal napi_throw napi_throw_error napi_throw_type_error @@ -567,6 +566,12 @@ EXPORTS napi_is_detached_arraybuffer napi_create_external_buffer napi_fatal_exception + node_api_create_buffer_from_arraybuffer + node_api_get_module_file_name + node_api_post_finalizer + node_api_create_property_key_latin1 + node_api_create_property_key_utf16 + node_api_create_property_key_utf8 ?TryGetCurrent@Isolate@v8@@SAPEAV12@XZ ?GetCurrent@Isolate@v8@@SAPEAV12@XZ ?GetCurrentContext@Isolate@v8@@QEAA?AV?$Local@VContext@v8@@@2@XZ diff --git a/src/symbols.dyn b/src/symbols.dyn index 6fc7fd75dd..88f97b6e92 100644 --- a/src/symbols.dyn +++ b/src/symbols.dyn @@ -149,6 +149,12 @@ _node_api_create_syntax_error; _node_api_symbol_for; _node_api_throw_syntax_error; + _node_api_create_buffer_from_arraybuffer; + _node_api_get_module_file_name; + _node_api_post_finalizer; + _node_api_create_property_key_latin1; + _node_api_create_property_key_utf16; + _node_api_create_property_key_utf8; __ZN2v87Isolate10GetCurrentEv; __ZN2v87Isolate13TryGetCurrentEv; __ZN2v87Isolate17GetCurrentContextEv; diff --git a/src/symbols.txt b/src/symbols.txt index cba22cc90e..93bb5e644c 100644 --- a/src/symbols.txt +++ b/src/symbols.txt @@ -148,6 +148,12 @@ _node_api_create_external_string_utf16 _node_api_create_syntax_error _node_api_symbol_for _node_api_throw_syntax_error +_node_api_create_buffer_from_arraybuffer +_node_api_get_module_file_name +_node_api_post_finalizer +_node_api_create_property_key_latin1 +_node_api_create_property_key_utf16 +_node_api_create_property_key_utf8 __ZN2v87Isolate10GetCurrentEv __ZN2v87Isolate13TryGetCurrentEv __ZN2v87Isolate17GetCurrentContextEv diff --git a/src/sync.zig b/src/sync.zig index e147116ec6..3740e849dc 100644 --- a/src/sync.zig +++ b/src/sync.zig @@ -535,7 +535,7 @@ pub const RwLock = if (@import("builtin").os.tag != .windows and @import("builti pub fn deinit(self: *RwLock) void { const safe_rc = switch (@import("builtin").os.tag) { .dragonfly, .netbsd => std.posix.EAGAIN, - else => 0, + else => std.c.E.SUCCESS, }; const rc = std.c.pthread_rwlock_destroy(&self.rwlock); @@ -884,7 +884,7 @@ else if (@import("builtin").link_libc) pub fn deinit(self: *Mutex) void { const safe_rc = switch (@import("builtin").os.tag) { .dragonfly, .netbsd => std.posix.EAGAIN, - else => 0, + else => std.c.E.SUCCESS, }; const rc = std.c.pthread_mutex_destroy(&self.mutex); @@ -1078,7 +1078,7 @@ else if (@import("builtin").link_libc) pub fn deinit(self: *Condvar) void { const safe_rc = switch (@import("builtin").os.tag) { .dragonfly, .netbsd => std.posix.EAGAIN, - else => 0, + else => std.c.E.SUCCESS, }; const rc = std.c.pthread_cond_destroy(&self.cond); diff --git a/test/bundler/native_plugin.cc b/test/bundler/native_plugin.cc index e3a38c9281..3eda64af5a 100644 --- a/test/bundler/native_plugin.cc +++ b/test/bundler/native_plugin.cc @@ -215,8 +215,8 @@ napi_value set_will_crash(napi_env env, napi_callback_info info) { napi_status status; External *external; - size_t argc = 1; - napi_value args[1]; + size_t argc = 2; + napi_value args[2]; status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); if (status != napi_ok) { napi_throw_error(env, nullptr, "Failed to parse arguments"); @@ -235,7 +235,7 @@ napi_value set_will_crash(napi_env env, napi_callback_info info) { } bool throws; - status = napi_get_value_bool(env, args[0], &throws); + status = napi_get_value_bool(env, args[1], &throws); if (status != napi_ok) { napi_throw_error(env, nullptr, "Failed to get boolean value"); return nullptr; @@ -250,8 +250,8 @@ napi_value set_throws_errors(napi_env env, napi_callback_info info) { napi_status status; External *external; - size_t argc = 1; - napi_value args[1]; + size_t argc = 2; + napi_value args[2]; status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); if (status != napi_ok) { napi_throw_error(env, nullptr, "Failed to parse arguments"); @@ -270,7 +270,7 @@ napi_value set_throws_errors(napi_env env, napi_callback_info info) { } bool throws; - status = napi_get_value_bool(env, args[0], &throws); + status = napi_get_value_bool(env, args[1], &throws); if (status != napi_ok) { napi_throw_error(env, nullptr, "Failed to get boolean value"); return nullptr; diff --git a/test/js/node/dns/node-dns.test.js b/test/js/node/dns/node-dns.test.js index aad547803a..06eefdef12 100644 --- a/test/js/node/dns/node-dns.test.js +++ b/test/js/node/dns/node-dns.test.js @@ -523,6 +523,8 @@ describe("uses `dns.promises` implementations for `util.promisify` factory", () }); it("util.promisify(dns.lookup) acts like dns.promises.lookup", async () => { - expect(await util.promisify(dns.lookup)("example.com")).toEqual(await dns.promises.lookup("example.com")); + // This test previously used example.com, but that domain has multiple A records, which can cause this test to fail. + // As of this writing, google.com has only one A record. If that changes, update this test with a domain that has only one A record. + expect(await util.promisify(dns.lookup)("google.com")).toEqual(await dns.promises.lookup("google.com")); }); }); diff --git a/test/js/third_party/prisma/prisma.test.ts b/test/js/third_party/prisma/prisma.test.ts index d470225b7a..76a66a5688 100644 --- a/test/js/third_party/prisma/prisma.test.ts +++ b/test/js/third_party/prisma/prisma.test.ts @@ -135,6 +135,51 @@ async function cleanTestId(prisma: PrismaClient, testId: number) { ); } + if (!isCI) { + test( + "does not leak", + async (prisma: PrismaClient, _: number) => { + // prisma leak was 8 bytes per query, so a million requests would manifest as an 8MB leak + const batchSize = 1000; + const warmupIters = 1_000_000 / batchSize; + const testIters = 4_000_000 / batchSize; + const gcPeriod = 10_000 / batchSize; + let totalIters = 0; + + async function runQuery() { + totalIters++; + // GC occasionally to make memory usage more deterministic + if (totalIters % gcPeriod == gcPeriod - 1) { + Bun.gc(true); + const line = `${totalIters},${(process.memoryUsage.rss() / 1024 / 1024) | 0}`; + // console.log(line); + // await appendFile("rss.csv", line + "\n"); + } + const queries = []; + for (let i = 0; i < batchSize; i++) { + queries.push(prisma.$queryRaw`SELECT 1`); + } + await Promise.all(queries); + } + + // warmup first + for (let i = 0; i < warmupIters; i++) { + await runQuery(); + } + // measure memory now + const before = process.memoryUsage.rss(); + // run a bunch more iterations to see if memory usage increases + for (let i = 0; i < testIters; i++) { + await runQuery(); + } + const after = process.memoryUsage.rss(); + const deltaMB = (after - before) / 1024 / 1024; + expect(deltaMB).toBeLessThan(10); + }, + 120_000, + ); + } + test( "CRUD basics", async (prisma: PrismaClient, testId: number) => { diff --git a/test/napi/napi-app/.clangd b/test/napi/napi-app/.clangd new file mode 100644 index 0000000000..248f378b6c --- /dev/null +++ b/test/napi/napi-app/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + CompilationDatabase: '.' diff --git a/test/napi/napi-app/async_finalize_addon.c b/test/napi/napi-app/async_finalize_addon.c new file mode 100644 index 0000000000..471f9a1554 --- /dev/null +++ b/test/napi/napi-app/async_finalize_addon.c @@ -0,0 +1,72 @@ +// This is a separate addon because the main one is built with +// NAPI_VERSION_EXPERIMENTAL, which makes finalizers run synchronously during GC +// and requires node_api_post_finalizer to run functions that could affect JS +// engine state. This module's purpose is to call napi_delete_reference directly +// during a finalizer -- not during a callback scheduled with +// node_api_post_finalizer -- so it cannot use NAPI_VERSION_EXPERIMENTAL. + +#include +#include +#include +#include +#include + +// "we have static_assert at home" - MSVC +char assertion[NAPI_VERSION == 8 ? 1 : -1]; + +#define NODE_API_CALL_CUSTOM_RETURN(env, call, retval) \ + 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 retval; \ + } \ + } while (0) + +#define NODE_API_CALL(env, call) NODE_API_CALL_CUSTOM_RETURN(env, call, NULL) +#define NODE_API_CALL_RETURN_VOID(env, call) \ + NODE_API_CALL_CUSTOM_RETURN(env, call, ) + +typedef struct { + napi_ref ref; +} RefHolder; + +static void finalizer(napi_env env, void *data, void *hint) { + printf("finalizer\n"); + (void)hint; + RefHolder *ref_holder = (RefHolder *)data; + NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, ref_holder->ref)); + free(ref_holder); +} + +static napi_value create_ref(napi_env env, napi_callback_info info) { + (void)info; + napi_value object; + NODE_API_CALL(env, napi_create_object(env, &object)); + RefHolder *ref_holder = calloc(1, sizeof *ref_holder); + NODE_API_CALL(env, napi_wrap(env, object, ref_holder, finalizer, NULL, + &ref_holder->ref)); + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; +} + +/* napi_value */ NAPI_MODULE_INIT(/* napi_env env, napi_value exports */) { + napi_value create_ref_function; + NODE_API_CALL(env, + napi_create_function(env, "create_ref", NAPI_AUTO_LENGTH, + create_ref, NULL, &create_ref_function)); + NODE_API_CALL(env, napi_set_named_property(env, exports, "create_ref", + create_ref_function)); + return exports; +} diff --git a/test/napi/napi-app/async_tests.cpp b/test/napi/napi-app/async_tests.cpp new file mode 100644 index 0000000000..89a645e23b --- /dev/null +++ b/test/napi/napi-app/async_tests.cpp @@ -0,0 +1,194 @@ +#include "async_tests.h" + +#include "utils.h" +#include +#include + +namespace napitests { + +struct AsyncWorkData { + int result; + napi_deferred deferred; + napi_async_work work; + bool do_throw; + + AsyncWorkData() + : result(0), deferred(nullptr), work(nullptr), do_throw(false) {} + + static void execute(napi_env env, void *data) { + AsyncWorkData *async_work_data = reinterpret_cast(data); + async_work_data->result = 42; + } + + static void complete(napi_env c_env, napi_status status, void *data) { + Napi::Env env(c_env); + AsyncWorkData *async_work_data = reinterpret_cast(data); + NODE_API_ASSERT_CUSTOM_RETURN(env, void(), status == napi_ok); + + if (async_work_data->do_throw) { + // still have to resolve/reject otherwise the process times out + // we should not see the resolution as our unhandled exception handler + // exits the process before that can happen + napi_value result = env.Undefined(); + NODE_API_CALL_CUSTOM_RETURN( + env, void(), + napi_resolve_deferred(env, async_work_data->deferred, result)); + + Napi::Error::New(env, "error from napi").ThrowAsJavaScriptException(); + } else { + char buf[64] = {0}; + snprintf(buf, sizeof(buf), "the number is %d", async_work_data->result); + napi_value result = Napi::String::New(env, buf); + NODE_API_CALL_CUSTOM_RETURN( + env, void(), + napi_resolve_deferred(env, async_work_data->deferred, result)); + } + + NODE_API_CALL_CUSTOM_RETURN( + env, void(), napi_delete_async_work(env, async_work_data->work)); + delete async_work_data; + } +}; + +// create_promise(void *unused_run_gc_callback, bool do_throw): makes a promise +// using napi_Async_work that either resolves or throws in the complete callback +static napi_value create_promise(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + auto *data = new AsyncWorkData(); + // info[0] is a callback to run the GC + data->do_throw = info[1].As(); + + napi_value promise; + NODE_API_CALL(env, napi_create_promise(env, &data->deferred, &promise)); + + napi_value resource_name = + Napi::String::New(env, "napitests__create_promise"); + NODE_API_CALL( + env, napi_create_async_work(env, /* async resource */ nullptr, + resource_name, AsyncWorkData::execute, + AsyncWorkData::complete, data, &data->work)); + NODE_API_CALL(env, napi_queue_async_work(env, data->work)); + return promise; +} + +class EchoWorker : public Napi::AsyncWorker { +public: + EchoWorker(Napi::Env env, Napi::Promise::Deferred deferred, + const std::string &&echo) + : Napi::AsyncWorker(env), m_echo(echo), m_deferred(deferred) {} + ~EchoWorker() override {} + + void Execute() override { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + void OnOK() override { m_deferred.Resolve(Napi::String::New(Env(), m_echo)); } + +private: + std::string m_echo; + Napi::Promise::Deferred m_deferred; +}; + +static Napi::Value +create_promise_with_napi_cpp(const Napi::CallbackInfo &info) { + auto deferred = Napi::Promise::Deferred::New(info.Env()); + auto *work = new EchoWorker(info.Env(), deferred, "hello world"); + work->Queue(); + return deferred.Promise(); +} + +struct ThreadsafeFunctionData { + napi_threadsafe_function tsfn; + napi_deferred deferred; + + static void thread_entry(ThreadsafeFunctionData *data) { + using namespace std::chrono_literals; + std::this_thread::sleep_for(10ms); + // nonblocking means it will return an error if the threadsafe function's + // queue is full, which it should never do because we only use it once and + // we init with a capacity of 1 + assert(napi_call_threadsafe_function(data->tsfn, nullptr, + napi_tsfn_nonblocking) == napi_ok); + } + + static void tsfn_finalize_callback(napi_env env, void *finalize_data, + void *finalize_hint) { + printf("tsfn_finalize_callback\n"); + ThreadsafeFunctionData *data = + reinterpret_cast(finalize_data); + delete data; + } + + static void tsfn_callback(napi_env c_env, napi_value js_callback, + void *context, void *data) { + // context == ThreadsafeFunctionData pointer + // data == nullptr + printf("tsfn_callback\n"); + ThreadsafeFunctionData *tsfn_data = + reinterpret_cast(context); + Napi::Env env(c_env); + + napi_value recv = env.Undefined(); + + // call our JS function with undefined for this and no arguments + napi_value js_result; + napi_status call_result = + napi_call_function(env, recv, js_callback, 0, nullptr, &js_result); + NODE_API_ASSERT_CUSTOM_RETURN(env, void(), + call_result == napi_ok || + call_result == napi_pending_exception); + + if (call_result == napi_ok) { + // only resolve if js_callback did not return an error + // resolve the promise with the return value of the JS function + NODE_API_CALL_CUSTOM_RETURN( + env, void(), + napi_resolve_deferred(env, tsfn_data->deferred, js_result)); + } + + // clean up the threadsafe function + NODE_API_CALL_CUSTOM_RETURN( + env, void(), + napi_release_threadsafe_function(tsfn_data->tsfn, napi_tsfn_abort)); + } +}; + +napi_value +create_promise_with_threadsafe_function(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + ThreadsafeFunctionData *tsfn_data = new ThreadsafeFunctionData; + + napi_value async_resource_name = Napi::String::New( + env, "napitests::create_promise_with_threadsafe_function"); + + // this is called directly, without the GC callback, so argument 0 is a JS + // callback used to resolve the promise + NODE_API_CALL(env, + napi_create_threadsafe_function( + env, info[0], nullptr, async_resource_name, + // max_queue_size, initial_thread_count + 1, 1, + // thread_finalize_data, thread_finalize_cb + tsfn_data, ThreadsafeFunctionData::tsfn_finalize_callback, + // context + tsfn_data, ThreadsafeFunctionData::tsfn_callback, + &tsfn_data->tsfn)); + // create a promise we can return to JS and put the deferred counterpart in + // tsfn_data + napi_value promise; + NODE_API_CALL(env, napi_create_promise(env, &tsfn_data->deferred, &promise)); + + // spawn and release std::thread + std::thread secondary_thread(ThreadsafeFunctionData::thread_entry, tsfn_data); + secondary_thread.detach(); + // return the promise to javascript + return promise; +} + +void register_async_tests(Napi::Env env, Napi::Object exports) { + REGISTER_FUNCTION(env, exports, create_promise); + REGISTER_FUNCTION(env, exports, create_promise_with_napi_cpp); + REGISTER_FUNCTION(env, exports, create_promise_with_threadsafe_function); +} + +} // namespace napitests diff --git a/test/napi/napi-app/async_tests.h b/test/napi/napi-app/async_tests.h new file mode 100644 index 0000000000..5bb0ee579e --- /dev/null +++ b/test/napi/napi-app/async_tests.h @@ -0,0 +1,11 @@ +#pragma once + +// Tests that use napi_async_work or napi_deferred + +#include "napi_with_version.h" + +namespace napitests { + +void register_async_tests(Napi::Env env, Napi::Object exports); + +} // namespace napitests diff --git a/test/napi/napi-app/binding.gyp b/test/napi/napi-app/binding.gyp index 3c4fa21129..6e10e64386 100644 --- a/test/napi/napi-app/binding.gyp +++ b/test/napi/napi-app/binding.gyp @@ -10,7 +10,8 @@ "AdditionalOptions": ["/std:c++20"], }, }, - "sources": ["main.cpp", "wrap_tests.cpp"], + # leak tests are unused as of #14501 + "sources": ["main.cpp", "async_tests.cpp", "class_test.cpp", "conversion_tests.cpp", "js_test_helpers.cpp", "standalone_tests.cpp", "wrap_tests.cpp"], "include_dirs": ["(data)); + + napi_value new_target; + NODE_API_CALL(env, napi_get_new_target(env, info, &new_target)); + napi_value new_target_string; + NODE_API_CALL(env, + napi_coerce_to_string(env, new_target, &new_target_string)); + char new_target_c_string[1024] = {0}; + NODE_API_CALL(env, napi_get_value_string_utf8( + env, new_target_string, new_target_c_string, + sizeof new_target_c_string, nullptr)); + + // node and bun output different whitespace when stringifying a function, + // which we don't want the test to fail for + // so we attempt to delete everything in between {} + auto *open_brace = reinterpret_cast( + memchr(new_target_c_string, '{', sizeof new_target_c_string)); + auto *close_brace = reinterpret_cast( + memchr(new_target_c_string, '}', sizeof new_target_c_string)); + if (open_brace && close_brace && open_brace < close_brace) { + open_brace[1] = '}'; + open_brace[2] = 0; + } + + printf("new.target = %s\n", new_target_c_string); + + printf("typeof this = %s\n", + napi_valuetype_to_string(get_typeof(env, this_value))); + + napi_value global; + NODE_API_CALL(env, napi_get_global(env, &global)); + bool equal; + NODE_API_CALL(env, napi_strict_equals(env, this_value, global, &equal)); + printf("this == global = %s\n", equal ? "true" : "false"); + + // define a property with a normal value + napi_value property_value = Napi::String::New(env, "meow"); + napi_set_named_property(env, this_value, "foo", property_value); + + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + return undefined; +} + +static napi_value getData_callback(napi_env env, napi_callback_info info) { + void *data; + + NODE_API_CALL(env, + napi_get_cb_info(env, info, nullptr, nullptr, nullptr, &data)); + const char *str_data = reinterpret_cast(data); + + napi_value ret; + NODE_API_CALL(env, + napi_create_string_utf8(env, str_data, NAPI_AUTO_LENGTH, &ret)); + return ret; +} + +static napi_value getStaticData_callback(napi_env env, + napi_callback_info info) { + void *data; + + NODE_API_CALL(env, + napi_get_cb_info(env, info, nullptr, nullptr, nullptr, &data)); + const char *str_data = reinterpret_cast(data); + + napi_value ret; + if (data) { + NODE_API_CALL( + env, napi_create_string_utf8(env, str_data, NAPI_AUTO_LENGTH, &ret)); + } else { + // we should hit this case as the data pointer should be null + NODE_API_CALL(env, napi_get_undefined(env, &ret)); + } + return ret; +} + +static napi_value static_getter_callback(napi_env env, + napi_callback_info info) { + void *data; + + NODE_API_CALL(env, + napi_get_cb_info(env, info, nullptr, nullptr, nullptr, &data)); + const char *str_data = reinterpret_cast(data); + + napi_value ret; + if (data) { + NODE_API_CALL( + env, napi_create_string_utf8(env, str_data, NAPI_AUTO_LENGTH, &ret)); + } else { + // we should hit this case as the data pointer should be null + NODE_API_CALL(env, napi_get_undefined(env, &ret)); + } + return ret; +} + +static napi_value get_class_with_constructor(const Napi::CallbackInfo &info) { + static char constructor_data[] = "constructor data"; + static char method_data[] = "method data"; + static char wrap_data[] = "wrap data"; + + napi_env env = info.Env(); + napi_value napi_class; + + const napi_property_descriptor property = { + .utf8name = "getData", + .name = nullptr, + .method = getData_callback, + .getter = nullptr, + .setter = nullptr, + .value = nullptr, + .attributes = napi_default_method, + .data = reinterpret_cast(method_data), + }; + + const napi_property_descriptor static_properties[] = { + { + .utf8name = "getStaticData", + .name = nullptr, + .method = getStaticData_callback, + .getter = nullptr, + .setter = nullptr, + .value = nullptr, + .attributes = napi_default_method, + // the class's data pointer should not be used instead -- it should + // stay nullptr + .data = nullptr, + }, + { + .utf8name = "getter", + .name = nullptr, + .method = nullptr, + .getter = static_getter_callback, + .setter = nullptr, + .value = nullptr, + .attributes = napi_default, + // the class's data pointer should not be used instead -- it should + // stay nullptr + .data = nullptr, + }, + }; + + NODE_API_CALL( + env, napi_define_class(env, "NapiClass", NAPI_AUTO_LENGTH, constructor, + reinterpret_cast(constructor_data), 1, + &property, &napi_class)); + NODE_API_CALL(env, + napi_define_properties(env, napi_class, 2, static_properties)); + NODE_API_CALL(env, + napi_wrap(env, napi_class, reinterpret_cast(wrap_data), + nullptr, nullptr, nullptr)); + return napi_class; +} + +void register_class_test(Napi::Env env, Napi::Object exports) { + REGISTER_FUNCTION(env, exports, get_class_with_constructor); +} + +} // namespace napitests diff --git a/test/napi/napi-app/class_test.h b/test/napi/napi-app/class_test.h new file mode 100644 index 0000000000..ee28be8c7c --- /dev/null +++ b/test/napi/napi-app/class_test.h @@ -0,0 +1,12 @@ +#pragma once + +// Functions exported to JS that make a class available with some interesting +// properties and methods + +#include + +namespace napitests { + +void register_class_test(Napi::Env env, Napi::Object exports); + +} // namespace napitests diff --git a/test/napi/napi-app/conversion_tests.cpp b/test/napi/napi-app/conversion_tests.cpp new file mode 100644 index 0000000000..755271a3b1 --- /dev/null +++ b/test/napi/napi-app/conversion_tests.cpp @@ -0,0 +1,172 @@ +#include "conversion_tests.h" + +#include "utils.h" + +#include +#include + +namespace napitests { + +// double_to_i32(any): number|undefined +static napi_value double_to_i32(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value input = info[0]; + + int32_t integer; + napi_value result; + napi_status status = napi_get_value_int32(env, input, &integer); + if (status == napi_ok) { + NODE_API_CALL(env, napi_create_int32(env, integer, &result)); + } else { + NODE_API_ASSERT(env, status == napi_number_expected); + NODE_API_CALL(env, napi_get_undefined(env, &result)); + } + return result; +} + +// double_to_u32(any): number|undefined +static napi_value double_to_u32(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value input = info[0]; + + uint32_t integer; + napi_value result; + napi_status status = napi_get_value_uint32(env, input, &integer); + if (status == napi_ok) { + NODE_API_CALL(env, napi_create_uint32(env, integer, &result)); + } else { + NODE_API_ASSERT(env, status == napi_number_expected); + NODE_API_CALL(env, napi_get_undefined(env, &result)); + } + return result; +} + +// double_to_i64(any): number|undefined +static napi_value double_to_i64(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value input = info[0]; + + int64_t integer; + napi_value result; + napi_status status = napi_get_value_int64(env, input, &integer); + if (status == napi_ok) { + NODE_API_CALL(env, napi_create_int64(env, integer, &result)); + } else { + NODE_API_ASSERT(env, status == napi_number_expected); + NODE_API_CALL(env, napi_get_undefined(env, &result)); + } + return result; +} + +// test from the C++ side +static napi_value +test_number_integer_conversions(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + using f64_limits = std::numeric_limits; + using i32_limits = std::numeric_limits; + using u32_limits = std::numeric_limits; + using i64_limits = std::numeric_limits; + + std::array, 14> i32_cases{{ + // special values + {f64_limits::infinity(), 0}, + {-f64_limits::infinity(), 0}, + {f64_limits::quiet_NaN(), 0}, + // normal + {0.0, 0}, + {1.0, 1}, + {-1.0, -1}, + // truncation + {1.25, 1}, + {-1.25, -1}, + // limits + {i32_limits::min(), i32_limits::min()}, + {i32_limits::max(), i32_limits::max()}, + // wrap around + {static_cast(i32_limits::min()) - 1.0, i32_limits::max()}, + {static_cast(i32_limits::max()) + 1.0, i32_limits::min()}, + {static_cast(i32_limits::min()) - 2.0, i32_limits::max() - 1}, + {static_cast(i32_limits::max()) + 2.0, i32_limits::min() + 1}, + }}; + + for (const auto &[in, expected_out] : i32_cases) { + napi_value js_in; + NODE_API_CALL(env, napi_create_double(env, in, &js_in)); + int32_t out_from_napi; + NODE_API_CALL(env, napi_get_value_int32(env, js_in, &out_from_napi)); + NODE_API_ASSERT(env, out_from_napi == expected_out); + } + + std::array, 12> u32_cases{{ + // special values + {f64_limits::infinity(), 0}, + {-f64_limits::infinity(), 0}, + {f64_limits::quiet_NaN(), 0}, + // normal + {0.0, 0}, + {1.0, 1}, + // truncation + {1.25, 1}, + {-1.25, u32_limits::max()}, + // limits + {u32_limits::max(), u32_limits::max()}, + // wrap around + {-1.0, u32_limits::max()}, + {static_cast(u32_limits::max()) + 1.0, 0}, + {-2.0, u32_limits::max() - 1}, + {static_cast(u32_limits::max()) + 2.0, 1}, + + }}; + + for (const auto &[in, expected_out] : u32_cases) { + napi_value js_in; + NODE_API_CALL(env, napi_create_double(env, in, &js_in)); + uint32_t out_from_napi; + NODE_API_CALL(env, napi_get_value_uint32(env, js_in, &out_from_napi)); + NODE_API_ASSERT(env, out_from_napi == expected_out); + } + + std::array, 12> i64_cases{ + {// special values + {f64_limits::infinity(), 0}, + {-f64_limits::infinity(), 0}, + {f64_limits::quiet_NaN(), 0}, + // normal + {0.0, 0}, + {1.0, 1}, + {-1.0, -1}, + // truncation + {1.25, 1}, + {-1.25, -1}, + // limits + // i64 max can't be precisely represented as double so it would round to + // 1 + i64 max, which would clamp and we don't want that yet. so we test + // the largest double smaller than i64 max instead (which is i64 max - + // 1024) + {i64_limits::min(), i64_limits::min()}, + {std::nextafter(static_cast(i64_limits::max()), 0.0), + static_cast( + std::nextafter(static_cast(i64_limits::max()), 0.0))}, + // clamp + {i64_limits::min() - 4096.0, i64_limits::min()}, + {i64_limits::max() + 4096.0, i64_limits::max()}}}; + + for (const auto &[in, expected_out] : i64_cases) { + napi_value js_in; + NODE_API_CALL(env, napi_create_double(env, in, &js_in)); + int64_t out_from_napi; + NODE_API_CALL(env, napi_get_value_int64(env, js_in, &out_from_napi)); + NODE_API_ASSERT(env, out_from_napi == expected_out); + } + + return ok(env); +} + +void register_conversion_tests(Napi::Env env, Napi::Object exports) { + REGISTER_FUNCTION(env, exports, double_to_i32); + REGISTER_FUNCTION(env, exports, double_to_u32); + REGISTER_FUNCTION(env, exports, double_to_i64); + REGISTER_FUNCTION(env, exports, test_number_integer_conversions); +} + +} // namespace napitests diff --git a/test/napi/napi-app/conversion_tests.h b/test/napi/napi-app/conversion_tests.h new file mode 100644 index 0000000000..eb9be87678 --- /dev/null +++ b/test/napi/napi-app/conversion_tests.h @@ -0,0 +1,12 @@ +#pragma once + +// Includes both some callbacks for module.js to use, and a long pure-C++ test +// of Node-API conversion functions + +#include "napi_with_version.h" + +namespace napitests { + +void register_conversion_tests(Napi::Env env, Napi::Object exports); + +} // namespace napitests diff --git a/test/napi/napi-app/ffi_addon_1.c b/test/napi/napi-app/ffi_addon_1.c new file mode 100644 index 0000000000..1e6de8fbcd --- /dev/null +++ b/test/napi/napi-app/ffi_addon_1.c @@ -0,0 +1,59 @@ +#include + +#define NODE_API_CALL_CUSTOM_RETURN(env, call, retval) \ + 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 retval; \ + } \ + } while (0) + +static int instance_data; + +#ifdef _WIN32 +#define EXPORT __declspec(dllexport) +#define CDECL __cdecl +#else +#define EXPORT +#define CDECL +#endif + +EXPORT void CDECL set_instance_data(napi_env env, int new_data) { + instance_data = new_data; + NODE_API_CALL_CUSTOM_RETURN( + env, napi_set_instance_data(env, (void *)&instance_data, NULL, NULL), ); +} + +EXPORT int CDECL get_instance_data(napi_env env) { + void *data; + NODE_API_CALL_CUSTOM_RETURN(env, napi_get_instance_data(env, &data), -1); + return *(int *)data; +} + +EXPORT const char *CDECL get_type(napi_env env, napi_value value) { + const char *names[] = { + [napi_undefined] = "undefined", [napi_null] = "null", + [napi_boolean] = "boolean", [napi_number] = "number", + [napi_string] = "string", [napi_symbol] = "symbol", + [napi_object] = "object", [napi_function] = "function", + [napi_external] = "external", [napi_bigint] = "bigint", + }; + size_t len = sizeof names / sizeof names[0]; + napi_valuetype type; + NODE_API_CALL_CUSTOM_RETURN(env, napi_typeof(env, value, &type), NULL); + if (type < 0 || type >= len) { + return "error"; + } + return names[type]; +} diff --git a/test/napi/napi-app/ffi_addon_2.c b/test/napi/napi-app/ffi_addon_2.c new file mode 100644 index 0000000000..85f8fde2af --- /dev/null +++ b/test/napi/napi-app/ffi_addon_2.c @@ -0,0 +1,3 @@ +// This can have the exact same functions as ffi_addon_1. We just want to build +// it to a different library. +#include "ffi_addon_1.c" diff --git a/test/napi/napi-app/js_test_helpers.cpp b/test/napi/napi-app/js_test_helpers.cpp new file mode 100644 index 0000000000..2542547373 --- /dev/null +++ b/test/napi/napi-app/js_test_helpers.cpp @@ -0,0 +1,338 @@ +#include "js_test_helpers.h" + +#include "utils.h" +#include +#include + +namespace napitests { + +static bool finalize_called = false; + +static void finalize_cb(napi_env env, void *finalize_data, + void *finalize_hint) { + node_api_post_finalizer( + env, + +[](napi_env env, void *data, void *hint) { + napi_handle_scope hs; + NODE_API_CALL_CUSTOM_RETURN(env, void(), + napi_open_handle_scope(env, &hs)); + NODE_API_CALL_CUSTOM_RETURN(env, void(), + napi_close_handle_scope(env, hs)); + finalize_called = true; + }, + finalize_data, finalize_hint); +} + +static napi_value create_ref_with_finalizer(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value object; + NODE_API_CALL(env, napi_create_object(env, &object)); + + napi_ref ref; + NODE_API_CALL(env, + napi_wrap(env, object, nullptr, finalize_cb, nullptr, &ref)); + + return ok(env); +} + +static napi_value was_finalize_called(const Napi::CallbackInfo &info) { + napi_value ret; + NODE_API_CALL(info.Env(), + napi_get_boolean(info.Env(), finalize_called, &ret)); + return ret; +} + +// calls a function (the sole argument) which must throw. catches and returns +// the thrown error +static napi_value call_and_get_exception(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value fn = info[0]; + napi_value undefined; + NODE_API_CALL(env, napi_get_undefined(env, &undefined)); + + NODE_API_ASSERT(env, napi_call_function(env, undefined, fn, 0, nullptr, + nullptr) == napi_pending_exception); + + bool is_pending; + NODE_API_CALL(env, napi_is_exception_pending(env, &is_pending)); + NODE_API_ASSERT(env, is_pending); + + napi_value exception; + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception)); + + napi_valuetype type = get_typeof(env, exception); + printf("typeof thrown exception = %s\n", napi_valuetype_to_string(type)); + + NODE_API_CALL(env, napi_is_exception_pending(env, &is_pending)); + NODE_API_ASSERT(env, !is_pending); + + return exception; +} + +// throw_error(code: string|undefined, msg: string|undefined, +// error_kind: 'error'|'type_error'|'range_error'|'syntax_error') +// if code and msg are JS undefined then change them to nullptr +static napi_value throw_error(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + Napi::Value js_code = info[0]; + Napi::Value js_msg = info[1]; + std::string error_kind = info[2].As().Utf8Value(); + + // these are optional + const char *code = nullptr; + std::string code_str; + const char *msg = nullptr; + std::string msg_str; + + if (js_code.IsString()) { + code_str = js_code.As().Utf8Value(); + code = code_str.c_str(); + } + if (js_msg.IsString()) { + msg_str = js_msg.As().Utf8Value(); + msg = msg_str.c_str(); + } + + using ThrowFunction = + napi_status (*)(napi_env, const char *code, const char *msg); + std::map functions{ + {"error", napi_throw_error}, + {"type_error", napi_throw_type_error}, + {"range_error", napi_throw_range_error}, + {"syntax_error", node_api_throw_syntax_error}}; + + auto throw_function = functions[error_kind]; + + if (msg == nullptr) { + NODE_API_ASSERT(env, throw_function(env, code, msg) == napi_invalid_arg); + return ok(env); + } else { + NODE_API_ASSERT(env, throw_function(env, code, msg) == napi_ok); + return nullptr; + } +} + +// create_and_throw_error(code: any, msg: any, +// error_kind: 'error'|'type_error'|'range_error'|'syntax_error') +// if code and msg are JS null then change them to nullptr +static napi_value create_and_throw_error(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value js_code = info[0]; + napi_value js_msg = info[1]; + std::string error_kind = info[2].As(); + + if (get_typeof(env, js_code) == napi_null) { + js_code = nullptr; + } + if (get_typeof(env, js_msg) == napi_null) { + js_msg = nullptr; + } + + using CreateErrorFunction = napi_status (*)( + napi_env, napi_value code, napi_value msg, napi_value *result); + std::map functions{ + {"error", napi_create_error}, + {"type_error", napi_create_type_error}, + {"range_error", napi_create_range_error}, + {"syntax_error", node_api_create_syntax_error}}; + + auto create_error_function = functions[error_kind]; + + napi_value err; + napi_status create_status = create_error_function(env, js_code, js_msg, &err); + // cases that should fail: + // - js_msg is nullptr + // - js_msg is not a string + // - js_code is not nullptr and not a string + // also we need to make sure not to call get_typeof with nullptr, since it + // asserts that napi_typeof succeeded + if (!js_msg || get_typeof(env, js_msg) != napi_string || + (js_code && get_typeof(env, js_code) != napi_string)) { + // bun and node may return different errors here depending on in what order + // the parameters are checked, but what's important is that there is an + // error + NODE_API_ASSERT(env, create_status == napi_string_expected || + create_status == napi_invalid_arg); + return ok(env); + } else { + NODE_API_ASSERT(env, create_status == napi_ok); + NODE_API_CALL(env, napi_throw(env, err)); + return nullptr; + } +} + +// perform_get(object, key) +static napi_value perform_get(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value obj = info[0]; + napi_value key = info[1]; + napi_status status; + napi_value value; + + // if key is a string, try napi_get_named_property + napi_valuetype type = get_typeof(env, key); + if (type == napi_string) { + char buf[1024]; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, key, buf, 1024, nullptr)); + status = napi_get_named_property(env, obj, buf, &value); + if (status == napi_ok) { + NODE_API_ASSERT(env, value != nullptr); + printf("value type = %d\n", get_typeof(env, value)); + } else { + NODE_API_ASSERT(env, status == napi_pending_exception); + return ok(env); + } + } + + status = napi_get_property(env, obj, key, &value); + if (status == napi_ok) { + NODE_API_ASSERT(env, value != nullptr); + printf("value type = %d\n", get_typeof(env, value)); + return value; + } else { + return ok(env); + } +} + +// perform_set(object, key, value) +static napi_value perform_set(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value obj = info[0]; + napi_value key = info[1]; + napi_value value = info[2]; + napi_status status; + + // if key is a string, try napi_set_named_property + napi_valuetype type = get_typeof(env, key); + if (type == napi_string) { + char buf[1024]; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, key, buf, 1024, nullptr)); + status = napi_set_named_property(env, obj, buf, value); + if (status != napi_ok) { + NODE_API_ASSERT(env, status == napi_pending_exception); + return ok(env); + } + } + + status = napi_set_property(env, obj, key, value); + if (status != napi_ok) { + NODE_API_ASSERT(env, status == napi_pending_exception); + } + return ok(env); +} + +static napi_value make_empty_array(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + napi_value js_size = info[0]; + uint32_t size; + NODE_API_CALL(env, napi_get_value_uint32(env, js_size, &size)); + napi_value array; + NODE_API_CALL(env, napi_create_array_with_length(env, size, &array)); + return array; +} + +// add_tag(object, lower, upper) +static napi_value add_tag(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value object = info[0]; + + uint32_t lower, upper; + NODE_API_CALL(env, napi_get_value_uint32(env, info[1], &lower)); + NODE_API_CALL(env, napi_get_value_uint32(env, info[2], &upper)); + napi_type_tag tag = {.lower = lower, .upper = upper}; + NODE_API_CALL(env, napi_type_tag_object(env, object, &tag)); + return env.Undefined(); +} + +// try_add_tag(object, lower, upper): bool +// true if success +static napi_value try_add_tag(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value object = info[0]; + + uint32_t lower, upper; + assert(napi_get_value_uint32(env, info[1], &lower) == napi_ok); + assert(napi_get_value_uint32(env, info[2], &upper) == napi_ok); + + napi_type_tag tag = {.lower = lower, .upper = upper}; + + napi_status status = napi_type_tag_object(env, object, &tag); + bool pending; + assert(napi_is_exception_pending(env, &pending) == napi_ok); + if (pending) { + napi_value ignore_exception; + assert(napi_get_and_clear_last_exception(env, &ignore_exception) == + napi_ok); + (void)ignore_exception; + } + + return Napi::Boolean::New(env, status == napi_ok); +} + +// check_tag(object, lower, upper): bool +static napi_value check_tag(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value object = info[0]; + + uint32_t lower, upper; + NODE_API_CALL(env, napi_get_value_uint32(env, info[1], &lower)); + NODE_API_CALL(env, napi_get_value_uint32(env, info[2], &upper)); + + napi_type_tag tag = {.lower = lower, .upper = upper}; + bool matches; + NODE_API_CALL(env, napi_check_object_type_tag(env, object, &tag, &matches)); + return Napi::Boolean::New(env, matches); +} + +static napi_value create_weird_bigints(const Napi::CallbackInfo &info) { + // create bigints by passing weird parameters to napi_create_bigint_words + napi_env env = info.Env(); + + std::array bigints; + std::array words{{123, 0, 0, 0}}; + + NODE_API_CALL(env, napi_create_bigint_int64(env, 0, &bigints[0])); + NODE_API_CALL(env, napi_create_bigint_uint64(env, 0, &bigints[1])); + // sign is not 0 or 1 (should be interpreted as negative) + NODE_API_CALL(env, + napi_create_bigint_words(env, 2, 1, words.data(), &bigints[2])); + // leading zeroes in word representation + NODE_API_CALL(env, + napi_create_bigint_words(env, 0, 4, words.data(), &bigints[3])); + // zero + NODE_API_CALL(env, + napi_create_bigint_words(env, 1, 0, words.data(), &bigints[4])); + // zero, another way + NODE_API_CALL( + env, napi_create_bigint_words(env, 1, 3, words.data() + 1, &bigints[5])); + + napi_value array; + NODE_API_CALL(env, + napi_create_array_with_length(env, bigints.size(), &array)); + for (size_t i = 0; i < bigints.size(); i++) { + NODE_API_CALL(env, napi_set_element(env, array, (uint32_t)i, bigints[i])); + } + return array; +} + +void register_js_test_helpers(Napi::Env env, Napi::Object exports) { + REGISTER_FUNCTION(env, exports, create_ref_with_finalizer); + REGISTER_FUNCTION(env, exports, was_finalize_called); + REGISTER_FUNCTION(env, exports, call_and_get_exception); + REGISTER_FUNCTION(env, exports, perform_get); + REGISTER_FUNCTION(env, exports, perform_set); + REGISTER_FUNCTION(env, exports, throw_error); + REGISTER_FUNCTION(env, exports, create_and_throw_error); + REGISTER_FUNCTION(env, exports, make_empty_array); + REGISTER_FUNCTION(env, exports, add_tag); + REGISTER_FUNCTION(env, exports, try_add_tag); + REGISTER_FUNCTION(env, exports, check_tag); + REGISTER_FUNCTION(env, exports, create_weird_bigints); +} + +} // namespace napitests diff --git a/test/napi/napi-app/js_test_helpers.h b/test/napi/napi-app/js_test_helpers.h new file mode 100644 index 0000000000..40bbdf9746 --- /dev/null +++ b/test/napi/napi-app/js_test_helpers.h @@ -0,0 +1,13 @@ +#pragma once + +// Functions that are used by tests implemented in module.js, rather than +// directly used by napi.test.ts, but are not complex enough or do not cleanly +// fit into a category to go in a separate C++ file + +#include "napi_with_version.h" + +namespace napitests { + +void register_js_test_helpers(Napi::Env env, Napi::Object exports); + +} // namespace napitests diff --git a/test/napi/napi-app/leak-fixture.js b/test/napi/napi-app/leak-fixture.js new file mode 100644 index 0000000000..9c7891daff --- /dev/null +++ b/test/napi/napi-app/leak-fixture.js @@ -0,0 +1,138 @@ +// UNUSED as of #14501 +const nativeTests = require("./build/Debug/napitests.node"); + +function usage() { + return process.memoryUsage.rss(); +} + +function gc() { + if (typeof Bun == "object") { + Bun.gc(true); + } else { + global.gc(); + } +} + +async function test(fn, warmupRuns, testRuns, maxDeltaMB) { + gc(); + // warmup + for (let i = 0; i < warmupRuns; i++) { + console.log(`warmup ${i}/${warmupRuns}`); + fn(); + await new Promise(resolve => setTimeout(resolve, 0)); + gc(); + } + const initial = usage() / 1024 / 1024; + + // test + for (let i = 0; i < testRuns; i++) { + console.log(`test ${i}/${testRuns}`); + fn(); + await new Promise(resolve => setTimeout(resolve, 0)); + gc(); + } + const after = usage() / 1024 / 1024; + + const deltaMB = after - initial; + console.log(`RSS ${initial} -> ${after} MiB`); + console.log(`Delta ${deltaMB} MB`); + if (deltaMB > maxDeltaMB) { + throw new Error("leaked!"); + } +} + +// Create a bunch of weak references and delete them +// Checks that napi_delete_reference cleans up memory associated with the napi_ref itself +function batchWeakRefs(n) { + if (typeof n != "number") throw new TypeError(); + for (let i = 0; i < n; i++) { + // create tons of weak references to objects that get destroyed + nativeTests.add_weak_refs({}); + } + // free all the weak refs + nativeTests.clear_weak_refs(); +} + +// Checks that strong references don't keep the value +function batchStrongRefs(n) { + if (typeof n != "number") throw new TypeError(); + for (let i = 0; i < n; i++) { + const array = new Uint8Array(10_000_000); + array.fill(i); + nativeTests.create_and_delete_strong_ref(array); + } +} + +function batchWrappedObjects(n) { + if (typeof n != "number") throw new TypeError(); + let wraps = []; + for (let i = 0; i < n; i++) { + const s = Math.random().toString(); + const wrapped = nativeTests.wrapped_object_factory( + s, + !process.isBun, // supports_node_api_post_finalize + ); + wraps.push(wrapped); + if (wrapped.get() != s) { + throw new Error("wrong value"); + } + } + gc(); + for (const w of wraps) { + w.get(); + } + // now GC them +} + +function batchExternals(n) { + if (typeof n != "number") throw new TypeError(); + let externals = []; + for (let i = 0; i < n; i++) { + const s = Math.random().toString(); + const external = nativeTests.external_factory(s); + externals.push(external); + if (nativeTests.external_get(external) != s) { + throw new Error("wrong value"); + } + } + gc(); + for (const e of externals) { + nativeTests.external_get(e); + } +} + +function batchThreadsafeFunctions(n, maxQueueSize) { + if (typeof n != "number") throw new TypeError(); + const callback = () => {}; + for (let i = 0; i < n; i++) { + nativeTests.create_and_delete_threadsafe_function(callback, maxQueueSize); + } + gc(); +} + +(async () => { + // TODO(@190n) get the rest of these tests working + // await test(() => batchWeakRefs(100), 10, 50, 8); + // await test(() => batchStrongRefs(100), 10, 50, 8); + // await test(() => batchWrappedObjects(1000), 20, 50, 20); + // await test(() => batchExternals(1000), 10, 400, 15); + + // a queue size of 10,000 would leak 80 kB (each queue item is a void*), so 400 iterations + // would be a 32MB leak + // call with a preallocated queue + const threadsafeFunctionJsCallback = () => {}; + await test( + () => nativeTests.create_and_delete_threadsafe_function(threadsafeFunctionJsCallback, 10_000, 10_000), + 100, + 400, + 10, + ); + + // call with a dynamic queue + await test( + () => nativeTests.create_and_delete_threadsafe_function(threadsafeFunctionJsCallback, 0, 10_000), + 100, + 400, + 10, + ); +})(); diff --git a/test/napi/napi-app/leak_tests.cpp b/test/napi/napi-app/leak_tests.cpp new file mode 100644 index 0000000000..84315a8c2e --- /dev/null +++ b/test/napi/napi-app/leak_tests.cpp @@ -0,0 +1,193 @@ +#include "leak_tests.h" + +#include "utils.h" +#include +#include + +namespace napitests { + +static std::vector> global_weak_refs; + +// add a weak reference to a global array +// this will cause extra memory usage for the ref, but it should not retain the +// JS object being referenced +Napi::Value add_weak_refs(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + for (int i = 0; i < 50; i++) { + global_weak_refs.emplace_back( + Napi::Reference::New(info[0], 0)); + } + return env.Undefined(); +} + +// delete all the weak refs created by add_weak_ref +Napi::Value clear_weak_refs(const Napi::CallbackInfo &info) { + global_weak_refs.clear(); + return info.Env().Undefined(); +} + +// create a strong reference to a JS value, and then delete it +Napi::Value create_and_delete_strong_ref(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + // strong reference + auto ref = Napi::Reference::New(info[0], 2); + // destructor will be called + return env.Undefined(); +} + +class WrappedObject { +public: + static napi_value factory(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value s = info[0]; + bool supports_node_api_post_finalize = info[1].As(); + + size_t len = 0; + NODE_API_CALL(env, napi_get_value_string_utf8(env, s, nullptr, 0, &len)); + char *string = new char[len + 1]; + string[len] = 0; + NODE_API_CALL(env, + napi_get_value_string_utf8(env, s, string, len + 1, nullptr)); + + napi_value js_object; + NODE_API_CALL(env, napi_create_object(env, &js_object)); + + WrappedObject *native_object = + new WrappedObject(string, supports_node_api_post_finalize); + NODE_API_CALL(env, napi_wrap(env, js_object, native_object, basic_finalize, + nullptr, &native_object->m_ref)); + napi_property_descriptor property = { + .utf8name = "get", + .name = nullptr, + .method = get, + .getter = nullptr, + .setter = nullptr, + .value = nullptr, + .attributes = napi_default_method, + .data = nullptr, + }; + NODE_API_CALL(env, napi_define_properties(env, js_object, 1, &property)); + return js_object; + } + + static napi_value get(napi_env env, napi_callback_info info) { + napi_value js_this; + NODE_API_CALL( + env, napi_get_cb_info(env, info, nullptr, nullptr, &js_this, nullptr)); + WrappedObject *native_object; + NODE_API_CALL(env, napi_unwrap(env, js_this, + reinterpret_cast(&native_object))); + return Napi::String::New(env, native_object->m_string); + } + +private: + static constexpr size_t big_alloc_size = 5'000'000; + + WrappedObject(char *string, bool supports_node_api_post_finalize) + : m_string(string), m_big_alloc(new char[big_alloc_size]), + m_supports_node_api_post_finalize(supports_node_api_post_finalize) { + memset(m_big_alloc, big_alloc_size, 'x'); + } + + ~WrappedObject() { + delete[] m_string; + delete[] m_big_alloc; + } + + static void delete_ref(napi_env env, void *data, void *hint) { + napi_delete_reference(env, reinterpret_cast(data)); + } + + static void basic_finalize(node_api_basic_env env, void *data, void *hint) { + auto *native_object = reinterpret_cast(data); + if (native_object->m_supports_node_api_post_finalize) { + node_api_post_finalizer(env, delete_ref, + reinterpret_cast(native_object->m_ref), + nullptr); + } else { + napi_delete_reference(env, native_object->m_ref); + } + delete native_object; + } + + char *m_string; + char *m_big_alloc; + napi_ref m_ref = nullptr; + bool m_supports_node_api_post_finalize; +}; + +class ExternalObject { +public: + static napi_value factory(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + std::string s = info[0].As(); + auto *native_object = new ExternalObject(std::move(s)); + napi_value js_external; + NODE_API_CALL(env, napi_create_external(env, native_object, basic_finalize, + nullptr, &js_external)); + return js_external; + } + + static napi_value get(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value v = info[0]; + ExternalObject *native_object; + NODE_API_CALL(env, napi_get_value_external( + env, v, reinterpret_cast(&native_object))); + return Napi::String::New(env, native_object->m_string); + } + +private: + ExternalObject(std::string &&string) : m_string(string) {} + static void basic_finalize(node_api_basic_env env, void *data, void *hint) { + auto *native_object = reinterpret_cast(data); + delete native_object; + } + + std::string m_string; +}; + +// creates a threadsafe function wrapping the passed JavaScript function, and +// then deletes it +// parameter 1: JavaScript function +// parameter 2: max queue size (0 means dynamic, like in +// napi_create_threadsafe_function) +// parameter 3: number of times to call the threadsafe function +napi_value +create_and_delete_threadsafe_function(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + napi_value js_func = info[0]; + size_t max_queue_size = info[1].As().Uint32Value(); + size_t num_calls = info[2].As().Uint32Value(); + NODE_API_ASSERT(env, num_calls <= max_queue_size || max_queue_size == 0); + napi_threadsafe_function tsfn; + napi_value async_resource_name; + NODE_API_CALL(env, + napi_create_string_utf8(env, "name", 4, &async_resource_name)); + NODE_API_CALL(env, + napi_create_threadsafe_function( + env, js_func, nullptr, async_resource_name, max_queue_size, + 1, nullptr, nullptr, nullptr, nullptr, &tsfn)); + for (size_t i = 0; i < num_calls; i++) { + // status should never be napi_queue_full, because we call this exactly as + // many times as there is capacity in the queue + NODE_API_CALL(env, napi_call_threadsafe_function(tsfn, nullptr, + napi_tsfn_nonblocking)); + } + NODE_API_CALL(env, napi_release_threadsafe_function(tsfn, napi_tsfn_abort)); + return env.Undefined(); +} + +void register_leak_tests(Napi::Env env, Napi::Object exports) { + REGISTER_FUNCTION(env, exports, add_weak_refs); + REGISTER_FUNCTION(env, exports, clear_weak_refs); + REGISTER_FUNCTION(env, exports, create_and_delete_strong_ref); + REGISTER_FUNCTION(env, exports, create_and_delete_threadsafe_function); + exports.Set("wrapped_object_factory", + Napi::Function::New(env, WrappedObject::factory)); + exports.Set("external_factory", + Napi::Function::New(env, ExternalObject::factory)); + exports.Set("external_get", Napi::Function::New(env, ExternalObject::get)); +} + +} // namespace napitests diff --git a/test/napi/napi-app/leak_tests.h b/test/napi/napi-app/leak_tests.h new file mode 100644 index 0000000000..16ebf12582 --- /dev/null +++ b/test/napi/napi-app/leak_tests.h @@ -0,0 +1,12 @@ +#pragma once + +// Helper functions used by JS to test that napi_ref, napi_wrap, and +// napi_external don't leak memory + +#include "napi_with_version.h" + +namespace napitests { + +void register_leak_tests(Napi::Env env, Napi::Object exports); + +} // namespace napitests diff --git a/test/napi/napi-app/main.cpp b/test/napi/napi-app/main.cpp index dd5aee63f9..bd020144f2 100644 --- a/test/napi/napi-app/main.cpp +++ b/test/napi/napi-app/main.cpp @@ -1,1151 +1,13 @@ #include "napi_with_version.h" -#include "utils.h" + +#include "async_tests.h" +#include "class_test.h" +#include "conversion_tests.h" +#include "js_test_helpers.h" +#include "standalone_tests.h" #include "wrap_tests.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -napi_value fail(napi_env env, const char *msg) { - napi_value result; - napi_create_string_utf8(env, msg, NAPI_AUTO_LENGTH, &result); - return result; -} - -napi_value fail_fmt(napi_env env, const char *fmt, ...) { - char buf[1024]; - va_list args; - va_start(args, fmt); - vsnprintf(buf, sizeof(buf), fmt, args); - va_end(args); - return fail(env, buf); -} - -napi_value test_issue_7685(const Napi::CallbackInfo &info) { - Napi::Env env(info.Env()); - Napi::HandleScope scope(env); -#define napi_assert(expr) \ - { \ - if (!expr) { \ - Napi::Error::New(env, #expr).ThrowAsJavaScriptException(); \ - } \ - } - // info[0] is a function to run the GC - napi_assert(info[1].IsNumber()); - napi_assert(info[2].IsNumber()); - napi_assert(info[3].IsNumber()); - napi_assert(info[4].IsNumber()); - napi_assert(info[5].IsNumber()); - napi_assert(info[6].IsNumber()); - napi_assert(info[7].IsNumber()); - napi_assert(info[8].IsNumber()); -#undef napi_assert - return ok(env); -} - -napi_threadsafe_function tsfn_11949; -napi_value tsfn_name_11949; - -static void test_issue_11949_callback(napi_env env, napi_value js_callback, - void *context, void *data) { - if (data != nullptr) { - printf("data: %p\n", data); - } else { - printf("data: nullptr\n"); - } - napi_unref_threadsafe_function(env, tsfn_11949); -} - -static napi_value test_issue_11949(const Napi::CallbackInfo &info) { - Napi::Env env(info.Env()); - Napi::HandleScope scope(env); - napi_status status; - status = napi_create_string_utf8(env, "TSFN", 4, &tsfn_name_11949); - assert(status == napi_ok); - status = napi_create_threadsafe_function( - env, NULL, NULL, tsfn_name_11949, 0, 1, NULL, NULL, NULL, - &test_issue_11949_callback, &tsfn_11949); - assert(status == napi_ok); - status = - napi_call_threadsafe_function(tsfn_11949, NULL, napi_tsfn_nonblocking); - assert(status == napi_ok); - napi_value result; - status = napi_get_undefined(env, &result); - assert(status == napi_ok); - return result; -} - -static void callback_1(napi_env env, napi_value js_callback, void *context, - void *data) {} - -napi_value test_napi_threadsafe_function_does_not_hang_after_finalize( - const Napi::CallbackInfo &info) { - - Napi::Env env = info.Env(); - napi_status status; - - napi_value resource_name; - status = napi_create_string_utf8(env, "simple", 6, &resource_name); - assert(status == napi_ok); - - napi_threadsafe_function cb; - status = napi_create_threadsafe_function(env, nullptr, nullptr, resource_name, - 0, 1, nullptr, nullptr, nullptr, - &callback_1, &cb); - assert(status == napi_ok); - - status = napi_release_threadsafe_function(cb, napi_tsfn_release); - assert(status == napi_ok); - - printf("success!"); - - return ok(env); -} - -napi_value -test_napi_get_value_string_utf8_with_buffer(const Napi::CallbackInfo &info) { - Napi::Env env = info.Env(); - - // info[0] is a function to run the GC - napi_value string_js = info[1]; - napi_value chars_to_copy_js = info[2]; - - // get how many chars we need to copy - uint32_t _len; - if (napi_get_value_uint32(env, chars_to_copy_js, &_len) != napi_ok) { - return fail(env, "call to napi_get_value_uint32 failed"); - } - size_t len = (size_t)_len; - - if (len == 424242) { - len = NAPI_AUTO_LENGTH; - } else if (len > 29) { - return fail(env, "len > 29"); - } - - size_t copied; - const size_t BUF_SIZE = 30; - char buf[BUF_SIZE]; - memset(buf, '*', BUF_SIZE); - buf[BUF_SIZE - 1] = '\0'; - - if (napi_get_value_string_utf8(env, string_js, buf, len, &copied) != - napi_ok) { - return fail(env, "call to napi_get_value_string_utf8 failed"); - } - - std::cout << "Chars to copy: " << len << std::endl; - std::cout << "Copied chars: " << copied << std::endl; - std::cout << "Buffer: "; - for (size_t i = 0; i < BUF_SIZE; i++) { - std::cout << (int)buf[i] << ", "; - } - std::cout << std::endl; - std::cout << "Value str: " << buf << std::endl; - return ok(env); -} - -napi_value test_napi_handle_scope_string(const Napi::CallbackInfo &info) { - // this is mostly a copy of test_handle_scope_gc from - // test/v8/v8-module/main.cpp -- see comments there for explanation - Napi::Env env = info.Env(); - - constexpr size_t num_small_strings = 10000; - - auto *small_strings = new napi_value[num_small_strings]; - - for (size_t i = 0; i < num_small_strings; i++) { - std::string cpp_str = std::to_string(i); - assert(napi_create_string_utf8(env, cpp_str.c_str(), cpp_str.size(), - &small_strings[i]) == napi_ok); - } - - run_gc(info); - - for (size_t j = 0; j < num_small_strings; j++) { - char buf[16]; - size_t result; - assert(napi_get_value_string_utf8(env, small_strings[j], buf, sizeof buf, - &result) == napi_ok); - printf("%s\n", buf); - assert(atoi(buf) == (int)j); - } - - delete[] small_strings; - return ok(env); -} - -napi_value test_napi_handle_scope_bigint(const Napi::CallbackInfo &info) { - // this is mostly a copy of test_handle_scope_gc from - // test/v8/v8-module/main.cpp -- see comments there for explanation - Napi::Env env = info.Env(); - - constexpr size_t num_small_ints = 10000; - constexpr size_t small_int_size = 100; - - auto *small_ints = new napi_value[num_small_ints]; - - for (size_t i = 0; i < num_small_ints; i++) { - std::array words; - words.fill(i + 1); - assert(napi_create_bigint_words(env, 0, small_int_size, words.data(), - &small_ints[i]) == napi_ok); - } - - run_gc(info); - - for (size_t j = 0; j < num_small_ints; j++) { - std::array words; - int sign; - size_t word_count = words.size(); - assert(napi_get_value_bigint_words(env, small_ints[j], &sign, &word_count, - words.data()) == napi_ok); - printf("%d, %zu\n", sign, word_count); - assert(sign == 0 && word_count == words.size()); - assert(std::all_of(words.begin(), words.end(), - [j](const uint64_t &w) { return w == j + 1; })); - } - - delete[] small_ints; - return ok(env); -} - -napi_value test_napi_delete_property(const Napi::CallbackInfo &info) { - Napi::Env env = info.Env(); - - // info[0] is a function to run the GC - napi_value object = info[1]; - napi_valuetype type = get_typeof(env, object); - assert(type == napi_object); - - napi_value key; - assert(napi_create_string_utf8(env, "foo", 3, &key) == napi_ok); - - napi_value non_configurable_key; - assert(napi_create_string_utf8(env, "bar", 3, &non_configurable_key) == - napi_ok); - - napi_value val; - assert(napi_create_int32(env, 42, &val) == napi_ok); - - bool delete_result; - assert(napi_delete_property(env, object, non_configurable_key, - &delete_result) == napi_ok); - assert(delete_result == false); - - assert(napi_delete_property(env, object, key, &delete_result) == napi_ok); - assert(delete_result == true); - - bool has_property; - assert(napi_has_property(env, object, key, &has_property) == napi_ok); - assert(has_property == false); - - return ok(env); -} - -void store_escaped_handle(napi_env env, napi_value *out, const char *str) { - // Allocate these values on the heap so they cannot be seen by stack scanning - // after this function returns. An earlier version tried putting them on the - // stack and using volatile stores to set them to nullptr, but that wasn't - // effective when the NAPI module was built in release mode as extra copies of - // the pointers would still be left in uninitialized stack memory. - napi_escapable_handle_scope *ehs = new napi_escapable_handle_scope; - napi_value *s = new napi_value; - napi_value *escaped = new napi_value; - assert(napi_open_escapable_handle_scope(env, ehs) == napi_ok); - assert(napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, s) == napi_ok); - assert(napi_escape_handle(env, *ehs, *s, escaped) == napi_ok); - // can't call a second time - assert(napi_escape_handle(env, *ehs, *s, escaped) == - napi_escape_called_twice); - assert(napi_close_escapable_handle_scope(env, *ehs) == napi_ok); - *out = *escaped; - - delete escaped; - delete s; - delete ehs; -} - -napi_value test_napi_escapable_handle_scope(const Napi::CallbackInfo &info) { - Napi::Env env = info.Env(); - - // allocate space for a napi_value on the heap - // use store_escaped_handle to put the value into it - // trigger GC - // the napi_value should still be valid even though it can't be found on the - // stack, because it escaped into the current handle scope - - constexpr const char *str = "this is a long string meow meow meow"; - - napi_value *hidden = new napi_value; - store_escaped_handle(env, hidden, str); - - run_gc(info); - - char buf[64]; - size_t len; - assert(napi_get_value_string_utf8(env, *hidden, buf, sizeof(buf), &len) == - napi_ok); - assert(len == strlen(str)); - assert(strcmp(buf, str) == 0); - - delete hidden; - return ok(env); -} - -napi_value test_napi_handle_scope_nesting(const Napi::CallbackInfo &info) { - Napi::Env env = info.Env(); - constexpr const char *str = "this is a long string meow meow meow"; - - // Create an outer handle scope, hidden on the heap (the one created in - // NAPIFunction::call is still on the stack - napi_handle_scope *outer_hs = new napi_handle_scope; - assert(napi_open_handle_scope(env, outer_hs) == napi_ok); - - // Make a handle in the outer scope, on the heap so stack scanning can't see - // it - napi_value *outer_scope_handle = new napi_value; - assert(napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, - outer_scope_handle) == napi_ok); - - // Make a new handle scope on the heap - napi_handle_scope *inner_hs = new napi_handle_scope; - assert(napi_open_handle_scope(env, inner_hs) == napi_ok); - - // Force GC - run_gc(info); - - // Try to read our first handle. Did the outer handle scope get - // collected now that it's not on the global object? - char buf[64]; - size_t len; - assert(napi_get_value_string_utf8(env, *outer_scope_handle, buf, sizeof(buf), - &len) == napi_ok); - assert(len == strlen(str)); - assert(strcmp(buf, str) == 0); - - // Clean up - assert(napi_close_handle_scope(env, *inner_hs) == napi_ok); - delete inner_hs; - assert(napi_close_handle_scope(env, *outer_hs) == napi_ok); - delete outer_hs; - delete outer_scope_handle; - return ok(env); -} - -napi_value constructor(napi_env env, napi_callback_info info) { - napi_value this_value; - assert(napi_get_cb_info(env, info, nullptr, nullptr, &this_value, nullptr) == - napi_ok); - napi_value property_value; - assert(napi_create_string_utf8(env, "meow", NAPI_AUTO_LENGTH, - &property_value) == napi_ok); - assert(napi_set_named_property(env, this_value, "foo", property_value) == - napi_ok); - napi_value undefined; - assert(napi_get_undefined(env, &undefined) == napi_ok); - return undefined; -} - -napi_value get_class_with_constructor(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - napi_value napi_class; - assert(napi_define_class(env, "NapiClass", NAPI_AUTO_LENGTH, constructor, - nullptr, 0, nullptr, &napi_class) == napi_ok); - return napi_class; -} - -struct AsyncWorkData { - int result; - napi_deferred deferred; - napi_async_work work; - bool do_throw; - - AsyncWorkData() - : result(0), deferred(nullptr), work(nullptr), do_throw(false) {} - - static void execute(napi_env env, void *data) { - AsyncWorkData *async_work_data = reinterpret_cast(data); - async_work_data->result = 42; - } - - static void complete(napi_env env, napi_status status, void *data) { - AsyncWorkData *async_work_data = reinterpret_cast(data); - assert(status == napi_ok); - - if (async_work_data->do_throw) { - // still have to resolve/reject otherwise the process times out - // we should not see the resolution as our unhandled exception handler - // exits the process before that can happen - napi_value result; - assert(napi_get_undefined(env, &result) == napi_ok); - assert(napi_resolve_deferred(env, async_work_data->deferred, result) == - napi_ok); - - napi_value err; - napi_value msg; - assert(napi_create_string_utf8(env, "error from napi", NAPI_AUTO_LENGTH, - &msg) == napi_ok); - assert(napi_create_error(env, nullptr, msg, &err) == napi_ok); - assert(napi_throw(env, err) == napi_ok); - } else { - napi_value result; - char buf[64] = {0}; - snprintf(buf, sizeof(buf), "the number is %d", async_work_data->result); - assert(napi_create_string_utf8(env, buf, NAPI_AUTO_LENGTH, &result) == - napi_ok); - assert(napi_resolve_deferred(env, async_work_data->deferred, result) == - napi_ok); - } - - assert(napi_delete_async_work(env, async_work_data->work) == napi_ok); - delete async_work_data; - } -}; - -// create_promise(void *unused_run_gc_callback, bool do_throw): makes a promise -// using napi_Async_work that either resolves or throws in the complete callback -napi_value create_promise(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - auto *data = new AsyncWorkData(); - // info[0] is a callback to run the GC - assert(napi_get_value_bool(env, info[1], &data->do_throw) == napi_ok); - - napi_value promise; - - assert(napi_create_promise(env, &data->deferred, &promise) == napi_ok); - - napi_value resource_name; - assert(napi_create_string_utf8(env, "napitests::create_promise", - NAPI_AUTO_LENGTH, &resource_name) == napi_ok); - assert(napi_create_async_work(env, nullptr, resource_name, - AsyncWorkData::execute, AsyncWorkData::complete, - data, &data->work) == napi_ok); - - assert(napi_queue_async_work(env, data->work) == napi_ok); - return promise; -} - -struct ThreadsafeFunctionData { - napi_threadsafe_function tsfn; - napi_deferred deferred; - - static void thread_entry(ThreadsafeFunctionData *data) { - using namespace std::chrono_literals; - std::this_thread::sleep_for(10ms); - // nonblocking means it will return an error if the threadsafe function's - // queue is full, which it should never do because we only use it once and - // we init with a capacity of 1 - assert(napi_call_threadsafe_function(data->tsfn, nullptr, - napi_tsfn_nonblocking) == napi_ok); - } - - static void tsfn_finalize_callback(napi_env env, void *finalize_data, - void *finalize_hint) { - printf("tsfn_finalize_callback\n"); - ThreadsafeFunctionData *data = - reinterpret_cast(finalize_data); - delete data; - } - - static void tsfn_callback(napi_env env, napi_value js_callback, void *context, - void *data) { - // context == ThreadsafeFunctionData pointer - // data == nullptr - printf("tsfn_callback\n"); - ThreadsafeFunctionData *tsfn_data = - reinterpret_cast(context); - - napi_value recv; - assert(napi_get_undefined(env, &recv) == napi_ok); - - // call our JS function with undefined for this and no arguments - napi_value js_result; - napi_status call_result = - napi_call_function(env, recv, js_callback, 0, nullptr, &js_result); - // assert(call_result == napi_ok || call_result == napi_pending_exception); - - if (call_result == napi_ok) { - // only resolve if js_callback did not return an error - // resolve the promise with the return value of the JS function - napi_status defer_result = - napi_resolve_deferred(env, tsfn_data->deferred, js_result); - printf("%d\n", defer_result); - assert(defer_result == napi_ok); - } - - // clean up the threadsafe function - assert(napi_release_threadsafe_function(tsfn_data->tsfn, napi_tsfn_abort) == - napi_ok); - } -}; - -napi_value -create_promise_with_threadsafe_function(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - ThreadsafeFunctionData *tsfn_data = new ThreadsafeFunctionData; - - napi_value async_resource_name; - assert(napi_create_string_utf8( - env, "napitests::create_promise_with_threadsafe_function", - NAPI_AUTO_LENGTH, &async_resource_name) == napi_ok); - - // this is called directly, without the GC callback, so argument 0 is a JS - // callback used to resolve the promise - assert(napi_create_threadsafe_function( - env, info[0], nullptr, async_resource_name, - // max_queue_size, initial_thread_count - 1, 1, - // thread_finalize_data, thread_finalize_cb - tsfn_data, ThreadsafeFunctionData::tsfn_finalize_callback, - // context - tsfn_data, ThreadsafeFunctionData::tsfn_callback, - &tsfn_data->tsfn) == napi_ok); - // create a promise we can return to JS and put the deferred counterpart in - // tsfn_data - napi_value promise; - assert(napi_create_promise(env, &tsfn_data->deferred, &promise) == napi_ok); - - // spawn and release std::thread - std::thread secondary_thread(ThreadsafeFunctionData::thread_entry, tsfn_data); - secondary_thread.detach(); - // return the promise to javascript - return promise; -} - -napi_value test_napi_ref(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - - napi_value object; - assert(napi_create_object(env, &object) == napi_ok); - - napi_ref ref; - assert(napi_create_reference(env, object, 0, &ref) == napi_ok); - - napi_value from_ref; - assert(napi_get_reference_value(env, ref, &from_ref) == napi_ok); - assert(from_ref != nullptr); - napi_valuetype typeof_result = get_typeof(env, from_ref); - assert(typeof_result == napi_object); - return ok(env); -} - -static bool finalize_called = false; - -void finalize_cb(napi_env env, void *finalize_data, void *finalize_hint) { - // only do this in bun - bool &create_handle_scope = *reinterpret_cast(finalize_hint); - if (create_handle_scope) { - napi_handle_scope hs; - assert(napi_open_handle_scope(env, &hs) == napi_ok); - assert(napi_close_handle_scope(env, hs) == napi_ok); - } - delete &create_handle_scope; - finalize_called = true; -} - -napi_value create_ref_with_finalizer(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - napi_value create_handle_scope_in_finalizer = info[0]; - - napi_value object; - assert(napi_create_object(env, &object) == napi_ok); - - bool *finalize_hint = new bool; - assert(napi_get_value_bool(env, create_handle_scope_in_finalizer, - finalize_hint) == napi_ok); - - napi_ref ref; - - assert(napi_wrap(env, object, nullptr, finalize_cb, - reinterpret_cast(finalize_hint), &ref) == napi_ok); - - return ok(env); -} - -napi_value was_finalize_called(const Napi::CallbackInfo &info) { - napi_value ret; - assert(napi_get_boolean(info.Env(), finalize_called, &ret) == napi_ok); - return ret; -} - -// 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) { - napi_env env = info.Env(); - napi_value fn = info[0]; - napi_value undefined; - assert(napi_get_undefined(env, &undefined) == napi_ok); - - (void)napi_call_function(env, undefined, fn, 0, nullptr, nullptr); - - bool is_pending; - assert(napi_is_exception_pending(env, &is_pending) == napi_ok); - assert(is_pending); - - napi_value exception; - assert(napi_get_and_clear_last_exception(env, &exception) == napi_ok); - - napi_valuetype type = get_typeof(env, exception); - printf("typeof thrown exception = %s\n", napi_valuetype_to_string(type)); - - assert(napi_is_exception_pending(env, &is_pending) == napi_ok); - assert(!is_pending); - - return exception; -} - -// throw_error(code: string|undefined, msg: string|undefined, -// error_kind: 'error'|'type_error'|'range_error'|'syntax_error') -// if code and msg are JS undefined then change them to nullptr -napi_value throw_error(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - - napi_value js_code = info[0]; - napi_value js_msg = info[1]; - napi_value js_error_kind = info[2]; - const char *code = nullptr; - const char *msg = nullptr; - char code_buf[256] = {0}, msg_buf[256] = {0}, error_kind_buf[256] = {0}; - - if (get_typeof(env, js_code) == napi_string) { - assert(napi_get_value_string_utf8(env, js_code, code_buf, sizeof code_buf, - nullptr) == napi_ok); - code = code_buf; - } - if (get_typeof(env, js_msg) == napi_string) { - assert(napi_get_value_string_utf8(env, js_msg, msg_buf, sizeof msg_buf, - nullptr) == napi_ok); - msg = msg_buf; - } - assert(napi_get_value_string_utf8(env, js_error_kind, error_kind_buf, - sizeof error_kind_buf, nullptr) == napi_ok); - - std::map - functions{{"error", napi_throw_error}, - {"type_error", napi_throw_type_error}, - {"range_error", napi_throw_range_error}, - {"syntax_error", node_api_throw_syntax_error}}; - - auto throw_function = functions[error_kind_buf]; - - if (msg == nullptr) { - assert(throw_function(env, code, msg) == napi_invalid_arg); - return ok(env); - } else { - assert(throw_function(env, code, msg) == napi_ok); - return nullptr; - } -} - -// create_and_throw_error(code: any, msg: any, -// error_kind: 'error'|'type_error'|'range_error'|'syntax_error') -// if code and msg are JS null then change them to nullptr -napi_value create_and_throw_error(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - - napi_value js_code = info[0]; - napi_value js_msg = info[1]; - napi_value js_error_kind = info[2]; - char error_kind_buf[256] = {0}; - - if (get_typeof(env, js_code) == napi_null) { - js_code = nullptr; - } - if (get_typeof(env, js_msg) == napi_null) { - js_msg = nullptr; - } - - assert(napi_get_value_string_utf8(env, js_error_kind, error_kind_buf, - sizeof error_kind_buf, nullptr) == napi_ok); - - std::map - functions{{"error", napi_create_error}, - {"type_error", napi_create_type_error}, - {"range_error", napi_create_range_error}, - {"syntax_error", node_api_create_syntax_error}}; - - auto create_error_function = functions[error_kind_buf]; - - napi_value err; - napi_status create_status = create_error_function(env, js_code, js_msg, &err); - // cases that should fail: - // - js_msg is nullptr - // - js_msg is not a string - // - js_code is not nullptr and not a string - // also we need to make sure not to call get_typeof with nullptr, since it - // asserts that napi_typeof succeeded - if (!js_msg || get_typeof(env, js_msg) != napi_string || - (js_code && get_typeof(env, js_code) != napi_string)) { - // bun and node may return different errors here depending on in what order - // the parameters are checked, but what's important is that there is an - // error - assert(create_status == napi_string_expected || - create_status == napi_invalid_arg); - return ok(env); - } else { - assert(create_status == napi_ok); - assert(napi_throw(env, err) == napi_ok); - return nullptr; - } -} - -napi_value eval_wrapper(const Napi::CallbackInfo &info) { - napi_value ret = nullptr; - // info[0] is the GC callback - (void)napi_run_script(info.Env(), info[1], &ret); - return ret; -} - -// perform_get(object, key) -napi_value perform_get(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - napi_value obj = info[0]; - napi_value key = info[1]; - napi_status status; - napi_value value; - - // if key is a string, try napi_get_named_property - napi_valuetype type = get_typeof(env, key); - if (type == napi_string) { - char buf[1024]; - assert(napi_get_value_string_utf8(env, key, buf, 1024, nullptr) == napi_ok); - status = napi_get_named_property(env, obj, buf, &value); - printf("get_named_property status is pending_exception or generic_failure " - "= %d\n", - status == napi_pending_exception || status == napi_generic_failure); - if (status == napi_ok) { - assert(value != nullptr); - printf("value type = %d\n", get_typeof(env, value)); - } else { - return ok(env); - } - } - - status = napi_get_property(env, obj, key, &value); - printf("get_property status is pending_exception or generic_failure = %d\n", - status == napi_pending_exception || status == napi_generic_failure); - if (status == napi_ok) { - assert(value != nullptr); - printf("value type = %d\n", get_typeof(env, value)); - return value; - } else { - return ok(env); - } -} - -// double_to_i32(any): number|undefined -napi_value double_to_i32(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - napi_value input = info[0]; - - int32_t integer; - napi_value result; - napi_status status = napi_get_value_int32(env, input, &integer); - if (status == napi_ok) { - assert(napi_create_int32(env, integer, &result) == napi_ok); - } else { - assert(status == napi_number_expected); - assert(napi_get_undefined(env, &result) == napi_ok); - } - return result; -} - -// double_to_u32(any): number|undefined -napi_value double_to_u32(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - napi_value input = info[0]; - - uint32_t integer; - napi_value result; - napi_status status = napi_get_value_uint32(env, input, &integer); - if (status == napi_ok) { - assert(napi_create_uint32(env, integer, &result) == napi_ok); - } else { - assert(status == napi_number_expected); - assert(napi_get_undefined(env, &result) == napi_ok); - } - return result; -} - -// double_to_i64(any): number|undefined -napi_value double_to_i64(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - napi_value input = info[0]; - - int64_t integer; - napi_value result; - napi_status status = napi_get_value_int64(env, input, &integer); - if (status == napi_ok) { - assert(napi_create_int64(env, integer, &result) == napi_ok); - } else { - assert(status == napi_number_expected); - assert(napi_get_undefined(env, &result) == napi_ok); - } - return result; -} - -// test from the C++ side -napi_value test_number_integer_conversions(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - using f64_limits = std::numeric_limits; - using i32_limits = std::numeric_limits; - using u32_limits = std::numeric_limits; - using i64_limits = std::numeric_limits; - - std::array, 14> i32_cases{{ - // special values - {f64_limits::infinity(), 0}, - {-f64_limits::infinity(), 0}, - {f64_limits::quiet_NaN(), 0}, - // normal - {0.0, 0}, - {1.0, 1}, - {-1.0, -1}, - // truncation - {1.25, 1}, - {-1.25, -1}, - // limits - {i32_limits::min(), i32_limits::min()}, - {i32_limits::max(), i32_limits::max()}, - // wrap around - {static_cast(i32_limits::min()) - 1.0, i32_limits::max()}, - {static_cast(i32_limits::max()) + 1.0, i32_limits::min()}, - {static_cast(i32_limits::min()) - 2.0, i32_limits::max() - 1}, - {static_cast(i32_limits::max()) + 2.0, i32_limits::min() + 1}, - }}; - - for (const auto &[in, expected_out] : i32_cases) { - napi_value js_in; - assert(napi_create_double(env, in, &js_in) == napi_ok); - int32_t out_from_napi; - assert(napi_get_value_int32(env, js_in, &out_from_napi) == napi_ok); - assert(out_from_napi == expected_out); - } - - std::array, 12> u32_cases{{ - // special values - {f64_limits::infinity(), 0}, - {-f64_limits::infinity(), 0}, - {f64_limits::quiet_NaN(), 0}, - // normal - {0.0, 0}, - {1.0, 1}, - // truncation - {1.25, 1}, - {-1.25, u32_limits::max()}, - // limits - {u32_limits::max(), u32_limits::max()}, - // wrap around - {-1.0, u32_limits::max()}, - {static_cast(u32_limits::max()) + 1.0, 0}, - {-2.0, u32_limits::max() - 1}, - {static_cast(u32_limits::max()) + 2.0, 1}, - - }}; - - for (const auto &[in, expected_out] : u32_cases) { - napi_value js_in; - assert(napi_create_double(env, in, &js_in) == napi_ok); - uint32_t out_from_napi; - assert(napi_get_value_uint32(env, js_in, &out_from_napi) == napi_ok); - assert(out_from_napi == expected_out); - } - - std::array, 12> i64_cases{ - {// special values - {f64_limits::infinity(), 0}, - {-f64_limits::infinity(), 0}, - {f64_limits::quiet_NaN(), 0}, - // normal - {0.0, 0}, - {1.0, 1}, - {-1.0, -1}, - // truncation - {1.25, 1}, - {-1.25, -1}, - // limits - // i64 max can't be precisely represented as double so it would round to - // 1 - // + i64 max, which would clamp and we don't want that yet. so we test - // the - // largest double smaller than i64 max instead (which is i64 max - 1024) - {i64_limits::min(), i64_limits::min()}, - {std::nextafter(static_cast(i64_limits::max()), 0.0), - static_cast( - std::nextafter(static_cast(i64_limits::max()), 0.0))}, - // clamp - {i64_limits::min() - 4096.0, i64_limits::min()}, - {i64_limits::max() + 4096.0, i64_limits::max()}}}; - - for (const auto &[in, expected_out] : i64_cases) { - napi_value js_in; - assert(napi_create_double(env, in, &js_in) == napi_ok); - int64_t out_from_napi; - assert(napi_get_value_int64(env, js_in, &out_from_napi) == napi_ok); - assert(out_from_napi == expected_out); - } - - return ok(env); -} - -napi_value make_empty_array(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - napi_value js_size = info[0]; - uint32_t size; - assert(napi_get_value_uint32(env, js_size, &size) == napi_ok); - napi_value array; - assert(napi_create_array_with_length(env, size, &array) == napi_ok); - return array; -} - -// add_tag(object, lower, upper) -static napi_value add_tag(const Napi::CallbackInfo &info) { - Napi::Env env = info.Env(); - napi_value object = info[0]; - - uint32_t lower, upper; - assert(napi_get_value_uint32(env, info[1], &lower) == napi_ok); - assert(napi_get_value_uint32(env, info[2], &upper) == napi_ok); - napi_type_tag tag = {.lower = lower, .upper = upper}; - - napi_status status = napi_type_tag_object(env, object, &tag); - if (status != napi_ok) { - char buf[1024]; - snprintf(buf, sizeof buf, "status = %d", status); - napi_throw_error(env, nullptr, buf); - } - return env.Undefined(); -} - -// check_tag(object, lower, upper): bool -static napi_value check_tag(const Napi::CallbackInfo &info) { - Napi::Env env = info.Env(); - napi_value object = info[0]; - - uint32_t lower, upper; - assert(napi_get_value_uint32(env, info[1], &lower) == napi_ok); - assert(napi_get_value_uint32(env, info[2], &upper) == napi_ok); - - napi_type_tag tag = {.lower = lower, .upper = upper}; - bool matches; - assert(napi_check_object_type_tag(env, object, &tag, &matches) == napi_ok); - return Napi::Boolean::New(env, matches); -} - -// try_add_tag(object, lower, upper): bool -// true if success -static napi_value try_add_tag(const Napi::CallbackInfo &info) { - Napi::Env env = info.Env(); - napi_value object = info[0]; - - uint32_t lower, upper; - assert(napi_get_value_uint32(env, info[1], &lower) == napi_ok); - assert(napi_get_value_uint32(env, info[2], &upper) == napi_ok); - - napi_type_tag tag = {.lower = lower, .upper = upper}; - - napi_status status = napi_type_tag_object(env, object, &tag); - bool pending; - assert(napi_is_exception_pending(env, &pending) == napi_ok); - if (pending) { - napi_value ignore_exception; - assert(napi_get_and_clear_last_exception(env, &ignore_exception) == - napi_ok); - (void)ignore_exception; - } - - return Napi::Boolean::New(env, status == napi_ok); -} - -static napi_value bigint_to_i64(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - // start at 1 is intentional, since argument 0 is the callback to run GC - // passed to every function - // perform test on all arguments - for (size_t i = 1; i < info.Length(); i++) { - napi_value bigint = info[i]; - - napi_valuetype type; - NODE_API_CALL(env, napi_typeof(env, bigint, &type)); - - int64_t result = 0; - bool lossless = false; - - if (type != napi_bigint) { - printf("napi_get_value_bigint_int64 return for non-bigint: %d\n", - napi_get_value_bigint_int64(env, bigint, &result, &lossless)); - } else { - NODE_API_CALL( - env, napi_get_value_bigint_int64(env, bigint, &result, &lossless)); - printf("napi_get_value_bigint_int64 result: %" PRId64 "\n", result); - printf("lossless: %s\n", lossless ? "true" : "false"); - } - } - - return ok(env); -} - -static napi_value bigint_to_u64(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - // start at 1 is intentional, since argument 0 is the callback to run GC - // passed to every function - // perform test on all arguments - for (size_t i = 1; i < info.Length(); i++) { - napi_value bigint = info[i]; - - napi_valuetype type; - NODE_API_CALL(env, napi_typeof(env, bigint, &type)); - - uint64_t result; - bool lossless; - - if (type != napi_bigint) { - printf("napi_get_value_bigint_uint64 return for non-bigint: %d\n", - napi_get_value_bigint_uint64(env, bigint, &result, &lossless)); - } else { - NODE_API_CALL( - env, napi_get_value_bigint_uint64(env, bigint, &result, &lossless)); - printf("napi_get_value_bigint_uint64 result: %" PRIu64 "\n", result); - printf("lossless: %s\n", lossless ? "true" : "false"); - } - } - - return ok(env); -} - -static napi_value bigint_to_64_null(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - - napi_value bigint; - NODE_API_CALL(env, napi_create_bigint_int64(env, 5, &bigint)); - - int64_t result_signed; - uint64_t result_unsigned; - bool lossless; - - printf("status (int64, null result) = %d\n", - napi_get_value_bigint_int64(env, bigint, nullptr, &lossless)); - printf("status (int64, null lossless) = %d\n", - napi_get_value_bigint_int64(env, bigint, &result_signed, nullptr)); - printf("status (uint64, null result) = %d\n", - napi_get_value_bigint_uint64(env, bigint, nullptr, &lossless)); - printf("status (uint64, null lossless) = %d\n", - napi_get_value_bigint_uint64(env, bigint, &result_unsigned, nullptr)); - - return ok(env); -} - -static napi_value create_weird_bigints(const Napi::CallbackInfo &info) { - // create bigints by passing weird parameters to napi_create_bigint_words - napi_env env = info.Env(); - - std::array bigints; - std::array words{{123, 0, 0, 0}}; - - NODE_API_CALL(env, napi_create_bigint_int64(env, 0, &bigints[0])); - NODE_API_CALL(env, napi_create_bigint_uint64(env, 0, &bigints[1])); - // sign is not 0 or 1 (should be interpreted as negative) - NODE_API_CALL(env, - napi_create_bigint_words(env, 2, 1, words.data(), &bigints[2])); - // leading zeroes in word representation - NODE_API_CALL(env, - napi_create_bigint_words(env, 0, 4, words.data(), &bigints[3])); - // zero - NODE_API_CALL(env, - napi_create_bigint_words(env, 1, 0, words.data(), &bigints[4])); - - napi_value array; - NODE_API_CALL(env, - napi_create_array_with_length(env, bigints.size(), &array)); - for (size_t i = 0; i < bigints.size(); i++) { - NODE_API_CALL(env, napi_set_element(env, array, (uint32_t)i, bigints[i])); - } - return array; -} - -// Call Node-API functions in ways that result in different error handling -// (erroneous call, valid call, or valid call while an exception is pending) and -// log information from napi_get_last_error_info -static napi_value test_extended_error_messages(const Napi::CallbackInfo &info) { - napi_env env = info.Env(); - const napi_extended_error_info *error; - - // this function is implemented in C++ - // error because the result pointer is null - printf("erroneous napi_create_double returned code %d\n", - napi_create_double(env, 1.0, nullptr)); - NODE_API_CALL(env, napi_get_last_error_info(env, &error)); - printf("erroneous napi_create_double info: code = %d, message = %s\n", - error->error_code, error->error_message); - - // this function should succeed and the success should overwrite the error - // from the last call - napi_value js_number; - printf("successful napi_create_double returned code %d\n", - napi_create_double(env, 5.0, &js_number)); - NODE_API_CALL(env, napi_get_last_error_info(env, &error)); - printf("successful napi_create_double info: code = %d, message = %s\n", - error->error_code, - error->error_message ? error->error_message : "(null)"); - - // this function is implemented in zig - // error because the value is not an array - unsigned int len; - printf("erroneous napi_get_array_length returned code %d\n", - napi_get_array_length(env, js_number, &len)); - NODE_API_CALL(env, napi_get_last_error_info(env, &error)); - printf("erroneous napi_get_array_length info: code = %d, message = %s\n", - error->error_code, error->error_message); - - // throw an exception - NODE_API_CALL(env, napi_throw_type_error(env, nullptr, "oops!")); - // nothing is wrong with this call by itself, but it should return - // napi_pending_exception without doing anything because an exception is - // pending - napi_value coerced_string; - printf("napi_coerce_to_string with pending exception returned code %d\n", - napi_coerce_to_string(env, js_number, &coerced_string)); - NODE_API_CALL(env, napi_get_last_error_info(env, &error)); - printf( - "napi_coerce_to_string with pending exception info: code = %d, message = " - "%s\n", - error->error_code, error->error_message); - - // clear the exception - napi_value exception; - NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception)); - - return ok(env); -} - -static napi_value test_is_buffer(const Napi::CallbackInfo &info) { - bool result; - napi_env env = info.Env(); - NODE_API_CALL(info.Env(), napi_is_buffer(env, info[1], &result)); - printf("napi_is_buffer -> %s\n", result ? "true" : "false"); - return ok(env); -} - -static napi_value test_is_typedarray(const Napi::CallbackInfo &info) { - bool result; - napi_env env = info.Env(); - NODE_API_CALL(info.Env(), napi_is_typedarray(env, info[1], &result)); - printf("napi_is_typedarray -> %s\n", result ? "true" : "false"); - return ok(env); -} +namespace napitests { Napi::Value RunCallback(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); @@ -1167,66 +29,16 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports1) { node::AddEnvironmentCleanupHook(isolate, [](void *) {}, isolate); node::RemoveEnvironmentCleanupHook(isolate, [](void *) {}, isolate); - exports.Set("test_issue_7685", Napi::Function::New(env, test_issue_7685)); - exports.Set("test_issue_11949", Napi::Function::New(env, test_issue_11949)); - exports.Set( - "test_napi_get_value_string_utf8_with_buffer", - Napi::Function::New(env, test_napi_get_value_string_utf8_with_buffer)); - exports.Set( - "test_napi_threadsafe_function_does_not_hang_after_finalize", - Napi::Function::New( - env, test_napi_threadsafe_function_does_not_hang_after_finalize)); - exports.Set("test_napi_handle_scope_string", - Napi::Function::New(env, test_napi_handle_scope_string)); - exports.Set("test_napi_handle_scope_bigint", - Napi::Function::New(env, test_napi_handle_scope_bigint)); - exports.Set("test_napi_delete_property", - Napi::Function::New(env, test_napi_delete_property)); - exports.Set("test_napi_escapable_handle_scope", - Napi::Function::New(env, test_napi_escapable_handle_scope)); - exports.Set("test_napi_handle_scope_nesting", - Napi::Function::New(env, test_napi_handle_scope_nesting)); - exports.Set("get_class_with_constructor", - Napi::Function::New(env, get_class_with_constructor)); - exports.Set("create_promise", Napi::Function::New(env, create_promise)); - exports.Set( - "create_promise_with_threadsafe_function", - Napi::Function::New(env, create_promise_with_threadsafe_function)); - exports.Set("test_napi_ref", Napi::Function::New(env, test_napi_ref)); - exports.Set("create_ref_with_finalizer", - Napi::Function::New(env, create_ref_with_finalizer)); - exports.Set("was_finalize_called", - Napi::Function::New(env, was_finalize_called)); - exports.Set("call_and_get_exception", - Napi::Function::New(env, call_and_get_exception)); - exports.Set("eval_wrapper", Napi::Function::New(env, eval_wrapper)); - exports.Set("perform_get", Napi::Function::New(env, perform_get)); - exports.Set("double_to_i32", Napi::Function::New(env, double_to_i32)); - exports.Set("double_to_u32", Napi::Function::New(env, double_to_u32)); - exports.Set("double_to_i64", Napi::Function::New(env, double_to_i64)); - exports.Set("test_number_integer_conversions", - Napi::Function::New(env, test_number_integer_conversions)); - exports.Set("make_empty_array", Napi::Function::New(env, make_empty_array)); - exports.Set("throw_error", Napi::Function::New(env, throw_error)); - exports.Set("create_and_throw_error", - Napi::Function::New(env, create_and_throw_error)); - exports.Set("add_tag", Napi::Function::New(env, add_tag)); - exports.Set("try_add_tag", Napi::Function::New(env, try_add_tag)); - exports.Set("check_tag", Napi::Function::New(env, check_tag)); - exports.Set("bigint_to_i64", Napi::Function::New(env, bigint_to_i64)); - exports.Set("bigint_to_u64", Napi::Function::New(env, bigint_to_u64)); - exports.Set("bigint_to_64_null", Napi::Function::New(env, bigint_to_64_null)); - exports.Set("create_weird_bigints", - Napi::Function::New(env, create_weird_bigints)); - exports.Set("test_extended_error_messages", - Napi::Function::New(env, test_extended_error_messages)); - exports.Set("test_is_buffer", Napi::Function::New(env, test_is_buffer)); - exports.Set("test_is_typedarray", - Napi::Function::New(env, test_is_typedarray)); - - napitests::register_wrap_tests(env, exports); + register_standalone_tests(env, exports); + register_async_tests(env, exports); + register_class_test(env, exports); + register_js_test_helpers(env, exports); + register_wrap_tests(env, exports); + register_conversion_tests(env, exports); return exports; } NODE_API_MODULE(napitests, InitAll) + +} // namespace napitests diff --git a/test/napi/napi-app/main.js b/test/napi/napi-app/main.js index d37ba09171..9c11801727 100644 --- a/test/napi/napi-app/main.js +++ b/test/napi/napi-app/main.js @@ -39,7 +39,6 @@ try { .catch(e => { console.error("rejected:", e); }); - result.then(x => console.log("resolved to", x)); } else if (process.argv[2] == "eval_wrapper") { // eval_wrapper just returns the result of the expression so it shouldn't be an error console.log(result); diff --git a/test/napi/napi-app/module.js b/test/napi/napi-app/module.js index f6a4cbc6c9..9a04c1b109 100644 --- a/test/napi/napi-app/module.js +++ b/test/napi/napi-app/module.js @@ -1,6 +1,7 @@ const assert = require("node:assert"); -const nativeTests = require("./build/Release/napitests.node"); -const secondAddon = require("./build/Release/second_addon.node"); +const nativeTests = require("./build/Debug/napitests.node"); +const secondAddon = require("./build/Debug/second_addon.node"); +const asyncFinalizeAddon = require("./build/Debug/async_finalize_addon.node"); async function gcUntil(fn) { const MAX = 100; @@ -33,16 +34,7 @@ nativeTests.test_napi_handle_scope_finalizer = async () => { nativeTests.create_ref_with_finalizer(Boolean(process.isBun)); // Wait until it actually has been collected by ticking the event loop and forcing GC - while (!nativeTests.was_finalize_called()) { - await new Promise(resolve => { - setTimeout(() => resolve(), 0); - }); - if (process.isBun) { - Bun.gc(true); - } else if (global.gc) { - global.gc(); - } - } + await gcUntil(() => nativeTests.was_finalize_called()); }; nativeTests.test_promise_with_threadsafe_function = async () => { @@ -87,7 +79,8 @@ nativeTests.test_get_property = () => { ), 5, "hello", - // TODO(@190n) test null and undefined here on the napi fix branch + null, + undefined, ]; const keys = [ "foo", @@ -111,7 +104,61 @@ nativeTests.test_get_property = () => { const ret = nativeTests.perform_get(object, key); console.log("native function returned", ret); } catch (e) { - console.log("threw", e.toString()); + console.log("threw", e.name); + } + } + } +}; + +nativeTests.test_set_property = () => { + const objects = [ + {}, + { foo: "bar" }, + { + set foo(value) { + throw new Error(`set foo to ${value}`); + }, + }, + { + // getter but no setter + get foo() {}, + }, + new Proxy( + {}, + { + set(_target, key, value) { + throw new Error(`proxy set ${key} to ${value}`); + }, + }, + ), + null, + undefined, + ]; + const keys = [ + "foo", + { + toString() { + throw new Error("toString"); + }, + }, + { + [Symbol.toPrimitive]() { + throw new Error("Symbol.toPrimitive"); + }, + }, + ]; + + for (const object of objects) { + for (const key of keys) { + console.log(objects.indexOf(object) + ", " + keys.indexOf(key)); + try { + const ret = nativeTests.perform_set(object, key, 42); + console.log("native function returned", ret); + if (object[key] != 42) { + throw new Error("setting property did not throw an error, but the property was not actually set"); + } + } catch (e) { + console.log("threw", e.name); } } } @@ -151,9 +198,7 @@ nativeTests.test_number_integer_conversions_from_js = () => { for (const [input, expectedOutput] of i32Cases) { const actualOutput = nativeTests.double_to_i32(input); console.log(`${input} as i32 => ${actualOutput}`); - if (actualOutput !== expectedOutput) { - console.error(`${input}: ${actualOutput} != ${expectedOutput}`); - } + assert(actualOutput === expectedOutput); } const u32Cases = [ @@ -182,9 +227,7 @@ nativeTests.test_number_integer_conversions_from_js = () => { for (const [input, expectedOutput] of u32Cases) { const actualOutput = nativeTests.double_to_u32(input); console.log(`${input} as u32 => ${actualOutput}`); - if (actualOutput !== expectedOutput) { - console.error(`${input}: ${actualOutput} != ${expectedOutput}`); - } + assert(actualOutput === expectedOutput); } const i64Cases = [ @@ -217,9 +260,7 @@ nativeTests.test_number_integer_conversions_from_js = () => { console.log( `${typeof input == "number" ? input.toFixed(2) : input} as i64 => ${typeof actualOutput == "number" ? actualOutput.toFixed(2) : actualOutput}`, ); - if (actualOutput !== expectedOutput) { - console.error(`${input}: ${actualOutput} != ${expectedOutput}`); - } + assert(actualOutput === expectedOutput); } }; @@ -291,6 +332,42 @@ nativeTests.test_type_tag = () => { console.log("o2 matches o2:", nativeTests.check_tag(o2, 3, 4)); }; +nativeTests.test_napi_class = () => { + const NapiClass = nativeTests.get_class_with_constructor(); + const instance = new NapiClass(); + console.log("static data =", NapiClass.getStaticData()); + console.log("static getter =", NapiClass.getter); + console.log("foo =", instance.foo); + console.log("data =", instance.getData()); +}; + +nativeTests.test_subclass_napi_class = () => { + const NapiClass = nativeTests.get_class_with_constructor(); + class Subclass extends NapiClass {} + const instance = new Subclass(); + console.log("subclass static data =", Subclass.getStaticData()); + console.log("subclass static getter =", Subclass.getter); + console.log("subclass foo =", instance.foo); + console.log("subclass data =", instance.getData()); +}; + +nativeTests.test_napi_class_non_constructor_call = () => { + const NapiClass = nativeTests.get_class_with_constructor(); + console.log("non-constructor call NapiClass() =", NapiClass()); + console.log("global foo set to ", typeof foo != "undefined" ? foo : undefined); +}; + +nativeTests.test_reflect_construct_napi_class = () => { + const NapiClass = nativeTests.get_class_with_constructor(); + let instance = Reflect.construct(NapiClass, [], Object); + console.log("reflect constructed foo =", instance.foo); + console.log("reflect constructed data =", instance.getData?.()); + class Foo {} + instance = Reflect.construct(NapiClass, [], Foo); + console.log("reflect constructed foo =", instance.foo); + console.log("reflect constructed data =", instance.getData?.()); +}; + nativeTests.test_napi_wrap = () => { const values = [ {}, @@ -485,6 +562,16 @@ nativeTests.test_remove_wrap_lifetime_with_strong_ref = async () => { await gcUntil(() => nativeTests.get_object_from_ref() === undefined); }; +nativeTests.test_ref_deleted_in_cleanup = () => { + let object = { foo: "bar" }; + assert(createWrapWithWeakRef(object) === object); + assert(nativeTests.get_wrap_data(object) === 42); +}; + +nativeTests.test_ref_deleted_in_async_finalize = () => { + asyncFinalizeAddon.create_ref(); +}; + nativeTests.test_create_bigint_words = () => { console.log(nativeTests.create_weird_bigints()); }; diff --git a/test/napi/napi-app/package.json b/test/napi/napi-app/package.json index 2c3157ea0c..ebc48bd9e2 100644 --- a/test/napi/napi-app/package.json +++ b/test/napi/napi-app/package.json @@ -3,11 +3,13 @@ "version": "1.0.0", "gypfile": true, "scripts": { - "build": "node-gyp rebuild", + "install": "node-gyp rebuild --debug", + "build": "node-gyp rebuild --debug", "clean": "node-gyp clean" }, "devDependencies": { "node-gyp": "^10.1.0", - "node-addon-api": "^8.0.0" + "node-addon-api": "^8.0.0", + "node-api-headers": "1.5.0" } } diff --git a/test/napi/napi-app/standalone_tests.cpp b/test/napi/napi-app/standalone_tests.cpp new file mode 100644 index 0000000000..9593bf4ada --- /dev/null +++ b/test/napi/napi-app/standalone_tests.cpp @@ -0,0 +1,537 @@ +#include "standalone_tests.h" + +#include +#include +#include +#include +#include + +#include "utils.h" + +namespace napitests { + +// https://github.com/oven-sh/bun/issues/7685 +static napi_value test_issue_7685(const Napi::CallbackInfo &info) { + Napi::Env env(info.Env()); + Napi::HandleScope scope(env); + // info[0] is a function to run the GC + NODE_API_ASSERT(env, info[1].IsNumber()); + NODE_API_ASSERT(env, info[2].IsNumber()); + NODE_API_ASSERT(env, info[3].IsNumber()); + NODE_API_ASSERT(env, info[4].IsNumber()); + NODE_API_ASSERT(env, info[5].IsNumber()); + NODE_API_ASSERT(env, info[6].IsNumber()); + NODE_API_ASSERT(env, info[7].IsNumber()); + NODE_API_ASSERT(env, info[8].IsNumber()); + return ok(env); +} + +static napi_threadsafe_function tsfn_11949 = nullptr; + +static void test_issue_11949_callback(napi_env env, napi_value js_callback, + void *opaque_context, void *opaque_data) { + int *context = reinterpret_cast(opaque_context); + int *data = reinterpret_cast(opaque_data); + printf("data = %d, context = %d\n", *data, *context); + delete context; + delete data; + napi_unref_threadsafe_function(env, tsfn_11949); + tsfn_11949 = nullptr; +} + +// https://github.com/oven-sh/bun/issues/11949 +static napi_value test_issue_11949(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + Napi::HandleScope scope(env); + napi_value name = Napi::String::New(env, "TSFN"); + + int *context = new int(42); + int *data = new int(1234); + + NODE_API_CALL(env, + napi_create_threadsafe_function( + env, /* JavaScript function */ nullptr, + /* async resource */ nullptr, name, + /* max queue size (unlimited) */ 0, + /* initial thread count */ 1, /* finalize data */ nullptr, + /* finalize callback */ nullptr, context, + &test_issue_11949_callback, &tsfn_11949)); + NODE_API_CALL(env, napi_call_threadsafe_function(tsfn_11949, data, + napi_tsfn_nonblocking)); + return env.Undefined(); +} + +static void noop_callback(napi_env env, napi_value js_callback, void *context, + void *data) {} + +static napi_value test_napi_threadsafe_function_does_not_hang_after_finalize( + const Napi::CallbackInfo &info) { + + Napi::Env env = info.Env(); + + napi_value resource_name = Napi::String::New(env, "simple"); + + napi_threadsafe_function cb; + NODE_API_CALL(env, + napi_create_threadsafe_function( + env, /* JavaScript function */ nullptr, + /* async resource */ nullptr, resource_name, + /* max queue size (unlimited) */ 0, + /* initial thread count */ 1, /* finalize data */ nullptr, + /* finalize callback */ nullptr, /* context */ nullptr, + &noop_callback, &cb)); + + NODE_API_CALL(env, napi_release_threadsafe_function(cb, napi_tsfn_release)); + printf("success!\n"); + return env.Undefined(); +} + +static napi_value +test_napi_get_value_string_utf8_with_buffer(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + + // info[0] is a function to run the GC + napi_value string_js = info[1]; + // get how many chars we need to copy + size_t len = info[2].As().Uint32Value(); + + if (len == 424242) { + len = NAPI_AUTO_LENGTH; + } else { + NODE_API_ASSERT(env, len <= 29); + } + + size_t copied; + const size_t BUF_SIZE = 30; + char buf[BUF_SIZE]; + memset(buf, '*', BUF_SIZE); + buf[BUF_SIZE - 1] = '\0'; + + NODE_API_CALL(env, + napi_get_value_string_utf8(env, string_js, buf, len, &copied)); + + std::cout << "Chars to copy: " << len << std::endl; + std::cout << "Copied chars: " << copied << std::endl; + std::cout << "Buffer: "; + for (size_t i = 0; i < BUF_SIZE; i++) { + std::cout << (int)buf[i] << ", "; + } + std::cout << std::endl; + std::cout << "Value str: " << buf << std::endl; + return ok(env); +} + +static napi_value +test_napi_handle_scope_string(const Napi::CallbackInfo &info) { + // this is mostly a copy of test_handle_scope_gc from + // test/v8/v8-module/main.cpp -- see comments there for explanation + Napi::Env env = info.Env(); + + constexpr size_t num_small_strings = 10000; + + auto *small_strings = new napi_value[num_small_strings]; + + for (size_t i = 0; i < num_small_strings; i++) { + std::string cpp_str = std::to_string(i); + NODE_API_CALL(env, + napi_create_string_utf8(env, cpp_str.c_str(), cpp_str.size(), + &small_strings[i])); + } + + run_gc(info); + + for (size_t j = 0; j < num_small_strings; j++) { + char buf[16]; + size_t result; + NODE_API_CALL(env, napi_get_value_string_utf8(env, small_strings[j], buf, + sizeof buf, &result)); + NODE_API_ASSERT(env, atoi(buf) == (int)j); + } + + delete[] small_strings; + return ok(env); +} + +static napi_value +test_napi_handle_scope_bigint(const Napi::CallbackInfo &info) { + // this is mostly a copy of test_handle_scope_gc from + // test/v8/v8-module/main.cpp -- see comments there for explanation + Napi::Env env = info.Env(); + + constexpr size_t num_small_ints = 10000; + constexpr size_t small_int_size = 100; + + auto *small_ints = new napi_value[num_small_ints]; + + for (size_t i = 0; i < num_small_ints; i++) { + std::array words; + words.fill(i + 1); + NODE_API_CALL(env, napi_create_bigint_words(env, 0, small_int_size, + words.data(), &small_ints[i])); + } + + run_gc(info); + + for (size_t j = 0; j < num_small_ints; j++) { + std::array words; + int sign; + size_t word_count = words.size(); + NODE_API_CALL(env, napi_get_value_bigint_words(env, small_ints[j], &sign, + &word_count, words.data())); + printf("%d, %zu\n", sign, word_count); + NODE_API_ASSERT(env, sign == 0 && word_count == words.size()); + NODE_API_ASSERT(env, + std::all_of(words.begin(), words.end(), + [j](const uint64_t &w) { return w == j + 1; })); + } + + delete[] small_ints; + return ok(env); +} + +static napi_value test_napi_delete_property(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + + // info[0] is a function to run the GC + napi_value object = info[1]; + napi_valuetype type = get_typeof(env, object); + NODE_API_ASSERT(env, type == napi_object); + + napi_value key = Napi::String::New(env, "foo"); + + napi_value non_configurable_key = Napi::String::New(env, "bar"); + + napi_value val; + NODE_API_CALL(env, napi_create_int32(env, 42, &val)); + + bool delete_result; + NODE_API_CALL(env, napi_delete_property(env, object, non_configurable_key, + &delete_result)); + NODE_API_ASSERT(env, delete_result == false); + + NODE_API_CALL(env, napi_delete_property(env, object, key, &delete_result)); + NODE_API_ASSERT(env, delete_result == true); + + bool has_property; + NODE_API_CALL(env, napi_has_property(env, object, key, &has_property)); + NODE_API_ASSERT(env, has_property == false); + + return ok(env); +} + +// Returns false if any napi function failed +static bool store_escaped_handle(napi_env env, napi_value *out, + const char *str) { + // Allocate these values on the heap so they cannot be seen by stack scanning + // after this function returns. An earlier version tried putting them on the + // stack and using volatile stores to set them to nullptr, but that wasn't + // effective when the NAPI module was built in release mode as extra copies of + // the pointers would still be left in uninitialized stack memory. + napi_escapable_handle_scope *ehs = new napi_escapable_handle_scope; + napi_value *s = new napi_value; + napi_value *escaped = new napi_value; + NODE_API_CALL_CUSTOM_RETURN(env, false, + napi_open_escapable_handle_scope(env, ehs)); + NODE_API_CALL_CUSTOM_RETURN( + env, false, napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, s)); + NODE_API_CALL_CUSTOM_RETURN(env, false, + napi_escape_handle(env, *ehs, *s, escaped)); + // can't call a second time + NODE_API_ASSERT_CUSTOM_RETURN(env, false, + napi_escape_handle(env, *ehs, *s, escaped) == + napi_escape_called_twice); + NODE_API_CALL_CUSTOM_RETURN(env, false, + napi_close_escapable_handle_scope(env, *ehs)); + *out = *escaped; + + delete escaped; + delete s; + delete ehs; + return true; +} + +static napi_value +test_napi_escapable_handle_scope(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + + // allocate space for a napi_value on the heap + // use store_escaped_handle to put the value into it + // trigger GC + // the napi_value should still be valid even though it can't be found on the + // stack, because it escaped into the current handle scope + + constexpr const char *str = "this is a long string meow meow meow"; + + napi_value *hidden = new napi_value; + NODE_API_ASSERT(env, store_escaped_handle(env, hidden, str)); + + run_gc(info); + + char buf[64]; + size_t len; + NODE_API_CALL( + env, napi_get_value_string_utf8(env, *hidden, buf, sizeof(buf), &len)); + NODE_API_ASSERT(env, len == strlen(str)); + NODE_API_ASSERT(env, strcmp(buf, str) == 0); + + delete hidden; + return ok(env); +} + +static napi_value +test_napi_handle_scope_nesting(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + constexpr const char *str = "this is a long string meow meow meow"; + + // Create an outer handle scope, hidden on the heap (the one created in + // NAPIFunction::call is still on the stack + napi_handle_scope *outer_hs = new napi_handle_scope; + NODE_API_CALL(env, napi_open_handle_scope(env, outer_hs)); + + // Make a handle in the outer scope, on the heap so stack scanning can't see + // it + napi_value *outer_scope_handle = new napi_value; + NODE_API_CALL(env, napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, + outer_scope_handle)); + + // Make a new handle scope on the heap so that the outer handle scope isn't + // active anymore + napi_handle_scope *inner_hs = new napi_handle_scope; + NODE_API_CALL(env, napi_open_handle_scope(env, inner_hs)); + + // Force GC + run_gc(info); + + // Try to read our first handle. Did the outer handle scope get + // collected now that it's not on the global object? The inner handle scope + // should be keeping it alive even though it's not on the stack. + char buf[64]; + size_t len; + NODE_API_CALL(env, napi_get_value_string_utf8(env, *outer_scope_handle, buf, + sizeof(buf), &len)); + NODE_API_ASSERT(env, len == strlen(str)); + NODE_API_ASSERT(env, strcmp(buf, str) == 0); + + // Clean up + NODE_API_CALL(env, napi_close_handle_scope(env, *inner_hs)); + delete inner_hs; + NODE_API_CALL(env, napi_close_handle_scope(env, *outer_hs)); + delete outer_hs; + delete outer_scope_handle; + return ok(env); +} + +// call this with a bunch (>10) of string arguments representing increasing +// decimal numbers. ensures that the runtime does not let these arguments be +// freed. +// +// test_napi_handle_scope_many_args(() => gc(), '1', '2', '3', ...) +static napi_value +test_napi_handle_scope_many_args(const Napi::CallbackInfo &info) { + Napi::Env env = info.Env(); + run_gc(info); + // now if bun is broken a bunch of our args are dead, because node-addon-api + // uses a heap array for >6 args + for (size_t i = 1; i < info.Length(); i++) { + Napi::String s = info[i].As(); + NODE_API_ASSERT(env, s.Utf8Value() == std::to_string(i)); + } + return env.Undefined(); +} + +static napi_value test_napi_ref(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value object; + NODE_API_CALL(env, napi_create_object(env, &object)); + + napi_ref ref; + NODE_API_CALL(env, napi_create_reference(env, object, 0, &ref)); + + napi_value from_ref; + NODE_API_CALL(env, napi_get_reference_value(env, ref, &from_ref)); + NODE_API_ASSERT(env, from_ref != nullptr); + napi_valuetype typeof_result = get_typeof(env, from_ref); + NODE_API_ASSERT(env, typeof_result == napi_object); + return ok(env); +} + +static napi_value test_napi_run_script(const Napi::CallbackInfo &info) { + napi_value ret = nullptr; + // info[0] is the GC callback + (void)napi_run_script(info.Env(), info[1], &ret); + return ret; +} + +// Call Node-API functions in ways that result in different error handling +// (erroneous call, valid call, or valid call while an exception is pending) and +// log information from napi_get_last_error_info +static napi_value test_extended_error_messages(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + const napi_extended_error_info *error; + + // this function is implemented in C++ + // error because the result pointer is null + printf("erroneous napi_create_double returned code %d\n", + napi_create_double(env, 1.0, nullptr)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf("erroneous napi_create_double info: code = %d, message = %s\n", + error->error_code, error->error_message); + + // this function should succeed and the success should overwrite the error + // from the last call + napi_value js_number; + printf("successful napi_create_double returned code %d\n", + napi_create_double(env, 5.0, &js_number)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf("successful napi_create_double info: code = %d, message = %s\n", + error->error_code, + error->error_message ? error->error_message : "(null)"); + + // this function is implemented in zig + // error because the value is not an array + unsigned int len; + printf("erroneous napi_get_array_length returned code %d\n", + napi_get_array_length(env, js_number, &len)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf("erroneous napi_get_array_length info: code = %d, message = %s\n", + error->error_code, error->error_message); + + // throw an exception + NODE_API_CALL(env, napi_throw_type_error(env, nullptr, "oops!")); + // nothing is wrong with this call by itself, but it should return + // napi_pending_exception without doing anything because an exception is + // pending + napi_value coerced_string; + printf("napi_coerce_to_string with pending exception returned code %d\n", + napi_coerce_to_string(env, js_number, &coerced_string)); + NODE_API_CALL(env, napi_get_last_error_info(env, &error)); + printf( + "napi_coerce_to_string with pending exception info: code = %d, message = " + "%s\n", + error->error_code, error->error_message); + + // clear the exception + napi_value exception; + NODE_API_CALL(env, napi_get_and_clear_last_exception(env, &exception)); + + return ok(env); +} + +static napi_value bigint_to_i64(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + // start at 1 is intentional, since argument 0 is the callback to run GC + // passed to every function + // perform test on all arguments + for (size_t i = 1; i < info.Length(); i++) { + napi_value bigint = info[i]; + + napi_valuetype type; + NODE_API_CALL(env, napi_typeof(env, bigint, &type)); + + int64_t result = 0; + bool lossless = false; + + if (type != napi_bigint) { + printf("napi_get_value_bigint_int64 return for non-bigint: %d\n", + napi_get_value_bigint_int64(env, bigint, &result, &lossless)); + } else { + NODE_API_CALL( + env, napi_get_value_bigint_int64(env, bigint, &result, &lossless)); + printf("napi_get_value_bigint_int64 result: %" PRId64 "\n", result); + printf("lossless: %s\n", lossless ? "true" : "false"); + } + } + + return ok(env); +} + +static napi_value bigint_to_u64(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + // start at 1 is intentional, since argument 0 is the callback to run GC + // passed to every function + // perform test on all arguments + for (size_t i = 1; i < info.Length(); i++) { + napi_value bigint = info[i]; + + napi_valuetype type; + NODE_API_CALL(env, napi_typeof(env, bigint, &type)); + + uint64_t result; + bool lossless; + + if (type != napi_bigint) { + printf("napi_get_value_bigint_uint64 return for non-bigint: %d\n", + napi_get_value_bigint_uint64(env, bigint, &result, &lossless)); + } else { + NODE_API_CALL( + env, napi_get_value_bigint_uint64(env, bigint, &result, &lossless)); + printf("napi_get_value_bigint_uint64 result: %" PRIu64 "\n", result); + printf("lossless: %s\n", lossless ? "true" : "false"); + } + } + + return ok(env); +} + +static napi_value bigint_to_64_null(const Napi::CallbackInfo &info) { + napi_env env = info.Env(); + + napi_value bigint; + NODE_API_CALL(env, napi_create_bigint_int64(env, 5, &bigint)); + + int64_t result_signed; + uint64_t result_unsigned; + bool lossless; + + printf("status (int64, null result) = %d\n", + napi_get_value_bigint_int64(env, bigint, nullptr, &lossless)); + printf("status (int64, null lossless) = %d\n", + napi_get_value_bigint_int64(env, bigint, &result_signed, nullptr)); + printf("status (uint64, null result) = %d\n", + napi_get_value_bigint_uint64(env, bigint, nullptr, &lossless)); + printf("status (uint64, null lossless) = %d\n", + napi_get_value_bigint_uint64(env, bigint, &result_unsigned, nullptr)); + + return ok(env); +} + +static napi_value test_is_buffer(const Napi::CallbackInfo &info) { + bool result; + napi_env env = info.Env(); + NODE_API_CALL(info.Env(), napi_is_buffer(env, info[1], &result)); + printf("napi_is_buffer -> %s\n", result ? "true" : "false"); + return ok(env); +} + +static napi_value test_is_typedarray(const Napi::CallbackInfo &info) { + bool result; + napi_env env = info.Env(); + NODE_API_CALL(info.Env(), napi_is_typedarray(env, info[1], &result)); + printf("napi_is_typedarray -> %s\n", result ? "true" : "false"); + return ok(env); +} + +void register_standalone_tests(Napi::Env env, Napi::Object exports) { + REGISTER_FUNCTION(env, exports, test_issue_7685); + REGISTER_FUNCTION(env, exports, test_issue_11949); + REGISTER_FUNCTION(env, exports, test_napi_get_value_string_utf8_with_buffer); + REGISTER_FUNCTION(env, exports, + test_napi_threadsafe_function_does_not_hang_after_finalize); + REGISTER_FUNCTION(env, exports, test_napi_handle_scope_string); + REGISTER_FUNCTION(env, exports, test_napi_handle_scope_bigint); + REGISTER_FUNCTION(env, exports, test_napi_delete_property); + REGISTER_FUNCTION(env, exports, test_napi_escapable_handle_scope); + REGISTER_FUNCTION(env, exports, test_napi_handle_scope_nesting); + REGISTER_FUNCTION(env, exports, test_napi_handle_scope_many_args); + REGISTER_FUNCTION(env, exports, test_napi_ref); + REGISTER_FUNCTION(env, exports, test_napi_run_script); + REGISTER_FUNCTION(env, exports, test_extended_error_messages); + REGISTER_FUNCTION(env, exports, bigint_to_i64); + REGISTER_FUNCTION(env, exports, bigint_to_u64); + REGISTER_FUNCTION(env, exports, bigint_to_64_null); + REGISTER_FUNCTION(env, exports, test_is_buffer); + REGISTER_FUNCTION(env, exports, test_is_typedarray); +} + +} // namespace napitests diff --git a/test/napi/napi-app/standalone_tests.h b/test/napi/napi-app/standalone_tests.h new file mode 100644 index 0000000000..499a90a906 --- /dev/null +++ b/test/napi/napi-app/standalone_tests.h @@ -0,0 +1,11 @@ +#pragma once + +// Functions that are run as the entire test by napi.test.ts + +#include "napi_with_version.h" + +namespace napitests { + +void register_standalone_tests(Napi::Env env, Napi::Object exports); + +} // namespace napitests diff --git a/test/napi/napi-app/wrap_tests.cpp b/test/napi/napi-app/wrap_tests.cpp index 5365a29e89..513dd6f234 100644 --- a/test/napi/napi-app/wrap_tests.cpp +++ b/test/napi/napi-app/wrap_tests.cpp @@ -8,15 +8,15 @@ 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 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; + wrap_finalize_called = true; +} static void finalize_for_create_wrap(napi_env env, void *opaque_data, void *opaque_hint) { @@ -25,11 +25,12 @@ static void finalize_for_create_wrap(napi_env env, void *opaque_data, 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; + if (ref_to_wrapped_object) { + // don't set wrap_finalize_called, wait for it to be set in delete_the_ref + node_api_post_finalizer(env, delete_the_ref, nullptr, nullptr); + } else { + wrap_finalize_called = true; + } } // create_wrap(js_object: object, ask_for_ref: boolean, strong: boolean): object diff --git a/test/napi/napi-value-ffi.test.ts b/test/napi/napi-value-ffi.test.ts new file mode 100644 index 0000000000..4734afb40d --- /dev/null +++ b/test/napi/napi-value-ffi.test.ts @@ -0,0 +1,89 @@ +import { dlopen, cc } from "bun:ffi"; +import { spawnSync } from "bun"; +import { bunExe, bunEnv, isWindows } from "harness"; +import { join } from "path"; +import { beforeAll, describe, it, expect } from "bun:test"; + +import source from "./napi-app/ffi_addon_1.c" with { type: "file" }; + +const symbols = { + set_instance_data: { + args: ["napi_env", "int"], + returns: "void", + }, + get_instance_data: { + args: ["napi_env"], + returns: "int", + }, + get_type: { + args: ["napi_env", "napi_value"], + returns: "cstring", + }, +}; + +let addon1, addon2, cc1, cc2; + +beforeAll(() => { + // build gyp + const install = spawnSync({ + cmd: [bunExe(), "install", "--verbose"], + cwd: join(__dirname, "napi-app"), + stderr: "inherit", + env: bunEnv, + stdout: "inherit", + stdin: "inherit", + }); + if (!install.success) { + throw new Error("build failed"); + } + addon1 = dlopen(join(__dirname, `napi-app/build/Debug/ffi_addon_1.node`), symbols).symbols; + addon2 = dlopen(join(__dirname, `napi-app/build/Debug/ffi_addon_2.node`), symbols).symbols; + try { + cc1 = cc({ + source, + symbols, + flags: `-I${join(__dirname, "napi-app/node_modules/node-api-headers/include")}`, + }).symbols; + cc2 = cc({ + source, + symbols, + flags: `-I${join(__dirname, "napi-app/node_modules/node-api-headers/include")}`, + }).symbols; + } catch (e) { + // ignore compilation failure on Windows + if (!isWindows) throw e; + } +}); + +describe("ffi napi integration", () => { + it("has a different napi_env for each ffi library", () => { + addon1.set_instance_data(undefined, 5); + addon2.set_instance_data(undefined, 6); + expect(addon1.get_instance_data()).toBe(5); + expect(addon2.get_instance_data()).toBe(6); + }); + + // broken + it.todo("passes values correctly", () => { + expect(addon1.get_type(undefined, 123).toString()).toBe("number"); + expect(addon1.get_type(undefined, "hello").toString()).toBe("string"); + expect(addon1.get_type(undefined, 190n).toString()).toBe("bigint"); + }); +}); + +describe("cc napi integration", () => { + // fails on windows as TCC can't link the napi_ functions + it.todoIf(isWindows)("has a different napi_env for each cc invocation", () => { + cc1.set_instance_data(undefined, 5); + cc2.set_instance_data(undefined, 6); + expect(cc1.get_instance_data()).toBe(5); + expect(cc2.get_instance_data()).toBe(6); + }); + + // broken + it.todo("passes values correctly", () => { + expect(cc1.get_type(undefined, 123).toString()).toBe("number"); + expect(cc1.get_type(undefined, "hello").toString()).toBe("string"); + expect(cc1.get_type(undefined, 190n).toString()).toBe("bigint"); + }); +}); diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts index d16b7fd9f9..773e12efe1 100644 --- a/test/napi/napi.test.ts +++ b/test/napi/napi.test.ts @@ -146,7 +146,7 @@ describe("napi", () => { describe("issue_11949", () => { it("napi_call_threadsafe_function should accept null", () => { const result = checkSameOutput("test_issue_11949", []); - expect(result).toStartWith("data: nullptr"); + expect(result).toStartWith("data = 1234, context = 42"); }); }); @@ -199,6 +199,9 @@ describe("napi", () => { it("exists while calling a napi_async_complete_callback", () => { checkSameOutput("create_promise", [false]); }); + it("keeps arguments moved off the stack alive", () => { + checkSameOutput("test_napi_handle_scope_many_args", ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]); + }); }); describe("escapable_handle_scope", () => { @@ -265,18 +268,18 @@ describe("napi", () => { describe("napi_run_script", () => { it("evaluates a basic expression", () => { - checkSameOutput("eval_wrapper", ["5 * (1 + 2)"]); + checkSameOutput("test_napi_run_script", ["5 * (1 + 2)"]); }); it("provides the right this value", () => { - checkSameOutput("eval_wrapper", ["this === global"]); + checkSameOutput("test_napi_run_script", ["this === global"]); }); it("propagates exceptions", () => { - checkSameOutput("eval_wrapper", ["(()=>{ throw new TypeError('oops'); })()"]); + checkSameOutput("test_napi_run_script", ["(()=>{ throw new TypeError('oops'); })()"]); }); it("cannot see locals from around its invocation", () => { // variable should_not_exist is declared on main.js:18, but it should not be in scope for the eval'd code // this doesn't use checkSameOutput because V8 and JSC use different error messages for a missing variable - let bunResult = runOn(bunExe(), "eval_wrapper", ["shouldNotExist"]); + let bunResult = runOn(bunExe(), "test_napi_run_script", ["shouldNotExist"]); // remove all debug logs bunResult = bunResult.replaceAll(/^\[\w+\].+$/gm, "").trim(); expect(bunResult).toBe( @@ -291,6 +294,12 @@ describe("napi", () => { }); }); + describe("napi_set_named_property", () => { + it("handles edge cases", () => { + checkSameOutput("test_set_property", []); + }); + }); + describe("napi_value <=> integer conversion", () => { it("works", () => { checkSameOutput("test_number_integer_conversions_from_js", []); @@ -323,6 +332,8 @@ describe("napi", () => { }); }); + // TODO(@190n) test allocating in a finalizer from a napi module with the right version + describe("napi_wrap", () => { it("accepts the right kinds of values", () => { checkSameOutput("test_napi_wrap", []); @@ -350,6 +361,19 @@ describe("napi", () => { checkSameOutput("test_wrap_lifetime_with_strong_ref", []); checkSameOutput("test_remove_wrap_lifetime_with_weak_ref", []); checkSameOutput("test_remove_wrap_lifetime_with_strong_ref", []); + // check that napi finalizers also run at VM exit, even if they didn't get run by GC + checkSameOutput("test_ref_deleted_in_cleanup", []); + // check that calling napi_delete_ref in the ref's finalizer is not use-after-free + checkSameOutput("test_ref_deleted_in_async_finalize", []); + }); + }); + + describe("napi_define_class", () => { + it("handles edge cases in the constructor", () => { + checkSameOutput("test_napi_class", []); + checkSameOutput("test_subclass_napi_class", []); + checkSameOutput("test_napi_class_non_constructor_call", []); + checkSameOutput("test_reflect_construct_napi_class", []); }); }); @@ -413,10 +437,10 @@ describe("napi", () => { ["null", null], ["undefined", undefined], ])("works when the module register function returns %s", (returnKind, expected) => { - expect(require(`./napi-app/build/Release/${returnKind}_addon.node`)).toEqual(expected); + expect(require(`./napi-app/build/Debug/${returnKind}_addon.node`)).toEqual(expected); }); it("works when the module register function throws", () => { - expect(() => require("./napi-app/build/Release/throw_addon.node")).toThrow(new Error("oops!")); + expect(() => require("./napi-app/build/Debug/throw_addon.node")).toThrow(new Error("oops!")); }); }); diff --git a/test/napi/node-napi-tests/.gitignore b/test/napi/node-napi-tests/.gitignore new file mode 100644 index 0000000000..bee8a64b79 --- /dev/null +++ b/test/napi/node-napi-tests/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/test/napi/node-napi-tests/README.md b/test/napi/node-napi-tests/README.md new file mode 100644 index 0000000000..29b71668f7 --- /dev/null +++ b/test/napi/node-napi-tests/README.md @@ -0,0 +1,5 @@ +These files are copied from https://github.com/190n/node/tree/napi-tests-bun, which in turn is a fork of Node.js with their js-native-api tests modified slightly to work in Bun. + +To change these files, edit the Node.js fork and then copy the changed version here. + +We should periodically (and definitely when we add new Node-API functions) sync that fork with Node.js upstream. diff --git a/test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof-2.js b/test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof-2.js new file mode 100644 index 0000000000..fd6874d6a5 --- /dev/null +++ b/test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof-2.js @@ -0,0 +1,329 @@ +// Copyright 2010 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +var except = "exception"; + +var correct_answer_index = 0; +var correct_answers = [ + false, false, true, true, false, false, true, true, + true, false, false, true, true, false, false, true, + false, true, true, false, false, true, true, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, false, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, false, true, true, false, + true, true, false, false, false, true, true, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, true, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, false, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, true, true, false, + true, false, false, true, true, true, false, false, + false, true, true, false, false, true, true, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, true, true, false, + true, false, false, true, false, true, true, false, + false, true, true, false, false, true, true, false, + true, true, false, false, false, true, true, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, false, +except, except, false, false, except, except, true, false, + false, false, except, except, false, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, true, true, false, false, + false, true, true, false, false, false, true, true, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, false, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, false, false, true, true, + true, true, false, false, false, false, true, true, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, true, true, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, true, true, false, false, + false, true, true, false, false, false, true, true, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, false, false, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, false, false, true, true, + true, true, false, false, false, false, true, true, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, true, true, +except, except, false, false, except, except, true, true, + false, false, except, except, false, false, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, false, false, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, false, false, true, true, + true, false, false, true, false, false, true, true, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, false, false, except, except, + true, false, except, except, false, false, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, + false, false, true, true, true, true, false, false, + true, false, false, true, true, true, false, false, + false, true, true, false, true, true, false, false, + true, true, false, false, true, true, false, false, +except, except, true, true, except, except, true, true, +except, except, false, true, except, except, true, true, +except, except, true, false, except, except, false, false, +except, except, false, false, except, except, false, false, + false, false, except, except, true, true, except, except, + true, false, except, except, true, true, except, except, + false, true, except, except, true, true, except, except, + true, true, except, except, true, true, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except, +except, except, except, except, except, except, except, except]; + +for (var i = 0; i < 256; i++) { + Test(i & 1, i & 2, i & 4, i & 8, i & 0x10, i & 0x20, i & 0x40, i & 0x80); +} + + +function InstanceTest(x, func) { + try { + var answer = (x instanceof func); + assertEquals(correct_answers[correct_answer_index], answer); + } catch (e) { + assertTrue(/prototype/.test(e)); + assertEquals(correct_answers[correct_answer_index], except); + } + correct_answer_index++; +} + + +function Test(a, b, c, d, e, f, g, h) { + var Foo = function() { } + var Bar = function() { } + + if (c) Foo.prototype = 12; + if (d) Bar.prototype = 13; + var x = a ? new Foo() : new Bar(); + var y = b ? new Foo() : new Bar(); + InstanceTest(x, Foo); + InstanceTest(y, Foo); + InstanceTest(x, Bar); + InstanceTest(y, Bar); + if (e) x.__proto__ = Bar.prototype; + if (f) y.__proto__ = Foo.prototype; + if (g) { + x.__proto__ = y; + } else { + if (h) y.__proto__ = x + } + InstanceTest(x, Foo); + InstanceTest(y, Foo); + InstanceTest(x, Bar); + InstanceTest(y, Bar); +} diff --git a/test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof.js b/test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof.js new file mode 100644 index 0000000000..050ef2d9d7 --- /dev/null +++ b/test/napi/node-napi-tests/deps/v8/test/mjsunit/instanceof.js @@ -0,0 +1,93 @@ +// Copyright 2008 the V8 project authors. All rights reserved. +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived +// from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +assertTrue({} instanceof Object); +assertTrue([] instanceof Object); + +assertFalse({} instanceof Array); +assertTrue([] instanceof Array); + +function TestChains() { + var A = {}; + var B = {}; + var C = {}; + B.__proto__ = A; + C.__proto__ = B; + + function F() { } + F.prototype = A; + assertTrue(C instanceof F); + assertTrue(B instanceof F); + assertFalse(A instanceof F); + + F.prototype = B; + assertTrue(C instanceof F); + assertFalse(B instanceof F); + assertFalse(A instanceof F); + + F.prototype = C; + assertFalse(C instanceof F); + assertFalse(B instanceof F); + assertFalse(A instanceof F); +} + +TestChains(); + + +function TestExceptions() { + function F() { } + var items = [ 1, new Number(42), + true, + 'string', new String('hest'), + {}, [], + F, new F(), + Object, String ]; + + var exceptions = 0; + var instanceofs = 0; + + for (var i = 0; i < items.length; i++) { + for (var j = 0; j < items.length; j++) { + try { + if (items[i] instanceof items[j]) instanceofs++; + } catch (e) { + assertTrue(e instanceof TypeError); + exceptions++; + } + } + } + assertEquals(10, instanceofs); + assertEquals(88, exceptions); + + // Make sure to throw an exception if the function prototype + // isn't a proper JavaScript object. + function G() { } + G.prototype = undefined; + assertThrows("({} instanceof G)"); +} + +TestExceptions(); diff --git a/test/napi/node-napi-tests/test/README.md b/test/napi/node-napi-tests/test/README.md new file mode 100644 index 0000000000..a79c97e941 --- /dev/null +++ b/test/napi/node-napi-tests/test/README.md @@ -0,0 +1,55 @@ +# Node.js Core Tests + +This directory contains code and data used to test the Node.js implementation. + +For a detailed guide on how to write tests in this +directory, see [the guide on writing tests](../doc/contributing/writing-tests.md). + +On how to run tests in this directory, see +[the contributing guide](../doc/contributing/pull-requests.md#step-6-test). + +For the tests to run on Windows, be sure to clone Node.js source code with the +`autocrlf` git config flag set to true. + +## Test Directories + +| Directory | Runs on CI | Purpose | +| ---------------- | ---------- | ------------------------------------------------------------------------------------------------------------- | +| `abort` | Yes | Tests that use `--abort-on-uncaught-exception` and other cases where we want to avoid generating a core file. | +| `addons` | Yes | Tests for [addon][] functionality along with some tests that require an addon. | +| `async-hooks` | Yes | Tests for [async\_hooks][async_hooks] functionality. | +| `benchmark` | Yes | Test minimal functionality of benchmarks. | +| `cctest` | Yes | C++ tests that are run as part of the build process. | +| `code-cache` | No | Tests for a Node.js binary compiled with V8 code cache. | +| `common` | _N/A_ | Common modules shared among many tests.[^1] | +| `doctool` | Yes | Tests for the documentation generator. | +| `es-module` | Yes | Test ESM module loading. | +| `fixtures` | _N/A_ | Test fixtures used in various tests throughout the test suite. | +| `internet` | No | Tests that make real outbound network connections.[^2] | +| `js-native-api` | Yes | Tests for Node.js-agnostic [Node-API][] functionality. | +| `known_issues` | Yes | Tests reproducing known issues within the system.[^3] | +| `message` | Yes | Tests for messages that are output for various conditions | +| `node-api` | Yes | Tests for Node.js-specific [Node-API][] functionality. | +| `parallel` | Yes | Various tests that are able to be run in parallel. | +| `pseudo-tty` | Yes | Tests that require stdin/stdout/stderr to be a TTY. | +| `pummel` | No | Various tests for various modules / system functionality operating under load. | +| `sequential` | Yes | Various tests that must not run in parallel. | +| `testpy` | _N/A_ | Test configuration utility used by various test suites. | +| `tick-processor` | No | Tests for the V8 tick processor integration.[^4] | +| `v8-updates` | No | Tests for V8 performance integration. | + +[^1]: [Documentation](../test/common/README.md) + +[^2]: Tests for networking related modules may also be present in other directories, but those tests do + not make outbound connections. + +[^3]: All tests inside of this directory are expected to fail. If a test doesn't fail on certain platforms, + those should be skipped via `known_issues.status`. + +[^4]: The tests are for the logic in `lib/internal/v8_prof_processor.js` and `lib/internal/v8_prof_polyfill.js`. + The tests confirm that the profile processor packages the correct set of scripts from V8 and introduces the + correct platform specific logic. + +[Node-API]: https://nodejs.org/api/n-api.html +[addon]: https://nodejs.org/api/addons.html +[async_hooks]: https://nodejs.org/api/async_hooks.html diff --git a/test/napi/node-napi-tests/test/common/README.md b/test/napi/node-napi-tests/test/common/README.md new file mode 100644 index 0000000000..5f5ff75fca --- /dev/null +++ b/test/napi/node-napi-tests/test/common/README.md @@ -0,0 +1,1179 @@ +# Node.js Core Test Common Modules + +This directory contains modules used to test the Node.js implementation. + +## Table of contents + +* [ArrayStream module](#arraystream-module) +* [Benchmark module](#benchmark-module) +* [Child process module](#child-process-module) +* [Common module API](#common-module-api) +* [Countdown module](#countdown-module) +* [CPU Profiler module](#cpu-profiler-module) +* [Debugger module](#debugger-module) +* [DNS module](#dns-module) +* [Environment variables](#environment-variables) +* [Fixtures module](#fixtures-module) +* [Heap dump checker module](#heap-dump-checker-module) +* [hijackstdio module](#hijackstdio-module) +* [HTTP2 module](#http2-module) +* [Internet module](#internet-module) +* [ongc module](#ongc-module) +* [process-exit-code-test-cases module](#process-exit-code-test-cases-module) +* [Report module](#report-module) +* [tick module](#tick-module) +* [tmpdir module](#tmpdir-module) +* [UDP pair helper](#udp-pair-helper) +* [WPT module](#wpt-module) + +## Benchmark module + +The `benchmark` module is used by tests to run benchmarks. + +### `runBenchmark(name, env)` + +* `name` [\][] Name of benchmark suite to be run. +* `env` [\][] Environment variables to be applied during the + run. + +## Child Process module + +The `child_process` module is used by tests that launch child processes. + +### `spawnSyncAndExit(command[, args][, spawnOptions], expectations)` + +Spawns a child process synchronously using [`child_process.spawnSync()`][] and +check if it runs in the way expected. If it does not, print the stdout and +stderr output from the child process and additional information about it to +the stderr of the current process before throwing and error. This helps +gathering more information about test failures coming from child processes. + +* `command`, `args`, `spawnOptions` See [`child_process.spawnSync()`][] +* `expectations` [\][] + * `status` [\][] Expected `child.status` + * `signal` [\][] | `null` Expected `child.signal` + * `stderr` [\][] | [\][] | + [\][] Optional. If it's a string, check that the output + to the stderr of the child process is exactly the same as the string. If + it's a regular expression, check that the stderr matches it. If it's a + function, invoke it with the stderr output as a string and check + that it returns true. The function can just throw errors (e.g. assertion + errors) to provide more information if the check fails. + * `stdout` [\][] | [\][] | + [\][] Optional. Similar to `stderr` but for the stdout. + * `trim` [\][] Optional. Whether this method should trim + out the whitespace characters when checking `stderr` and `stdout` outputs. + Defaults to `false`. +* return [\][] + * `child` [\][] The child process returned by + [`child_process.spawnSync()`][]. + * `stderr` [\][] The output from the child process to stderr. + * `stdout` [\][] The output from the child process to stdout. + +### `spawnSyncAndExitWithoutError(command[, args][, spawnOptions])` + +Similar to `expectSyncExit()` with the `status` expected to be 0 and +`signal` expected to be `null`. + +### `spawnSyncAndAssert(command[, args][, spawnOptions], expectations)` + +Similar to `spawnSyncAndExitWithoutError()`, but with an additional +`expectations` parameter. + +## Common module API + +The `common` module is used by tests for consistency across repeated +tasks. + +### `allowGlobals(...allowlist)` + +* `allowlist` [\][] Array of Globals +* return [\][] + +Takes `allowlist` and concats that with predefined `knownGlobals`. + +### `canCreateSymLink()` + +* return [\][] + +Checks whether the current running process can create symlinks. On Windows, this +returns `false` if the process running doesn't have privileges to create +symlinks +([SeCreateSymbolicLinkPrivilege](https://msdn.microsoft.com/en-us/library/windows/desktop/bb530716\(v=vs.85\).aspx)). +On non-Windows platforms, this always returns `true`. + +### `createZeroFilledFile(filename)` + +Creates a 10 MiB file of all null characters. + +### `enoughTestMem` + +* [\][] + +Indicates if there is more than 1gb of total memory. + +### ``escapePOSIXShell`shell command` `` + +Escapes values in a string template literal to pass them as env variable. On Windows, this function +does not escape anything (which is fine for most paths, as `"` is not a valid +char in a path on Windows), so for tests that must pass on Windows, you should +use it only to escape paths, inside double quotes. +This function is meant to be used for tagged template strings. + +```js +const { escapePOSIXShell } = require('../common'); +const fixtures = require('../common/fixtures'); +const { execSync } = require('node:child_process'); +const origin = fixtures.path('origin'); +const destination = fixtures.path('destination'); + +execSync(...escapePOSIXShell`cp "${origin}" "${destination}"`); + +// When you need to specify specific options, and/or additional env variables: +const [cmd, opts] = escapePOSIXShell`cp "${origin}" "${destination}"`; +console.log(typeof cmd === 'string'); // true +console.log(opts === undefined || typeof opts.env === 'object'); // true +execSync(cmd, { ...opts, stdio: 'ignore' }); +execSync(cmd, { stdio: 'ignore', env: { ...opts?.env, KEY: 'value' } }); +``` + +When possible, avoid using a shell; that way, there's no need to escape values. + +### `expectsError(validator[, exact])` + +* `validator` [\][] | [\][] | + [\][] | [\][] The validator behaves + identical to `assert.throws(fn, validator)`. +* `exact` [\][] default = 1 +* return [\][] A callback function that expects an error. + +A function suitable as callback to validate callback based errors. The error is +validated using `assert.throws(() => { throw error; }, validator)`. If the +returned function has not been called exactly `exact` number of times when the +test is complete, then the test will fail. + +### `expectWarning(name[, expected[, code]])` + +* `name` [\][] | [\][] +* `expected` [\][] | [\][] | [\][] +* `code` [\][] + +Tests whether `name`, `expected`, and `code` are part of a raised warning. + +The code is required in case the name is set to `'DeprecationWarning'`. + +Examples: + +```js +const { expectWarning } = require('../common'); + +expectWarning('Warning', 'Foobar is really bad'); + +expectWarning('DeprecationWarning', 'Foobar is deprecated', 'DEP0XXX'); + +expectWarning('DeprecationWarning', [ + 'Foobar is deprecated', 'DEP0XXX', +]); + +expectWarning('DeprecationWarning', [ + ['Foobar is deprecated', 'DEP0XXX'], + ['Baz is also deprecated', 'DEP0XX2'], +]); + +expectWarning('DeprecationWarning', { + DEP0XXX: 'Foobar is deprecated', + DEP0XX2: 'Baz is also deprecated', +}); + +expectWarning({ + DeprecationWarning: { + DEP0XXX: 'Foobar is deprecated', + DEP0XX1: 'Baz is also deprecated', + }, + Warning: [ + ['Multiple array entries are fine', 'SpecialWarningCode'], + ['No code is also fine'], + ], + SingleEntry: ['This will also work', 'WarningCode'], + SingleString: 'Single string entries without code will also work', +}); +``` + +### `getArrayBufferViews(buf)` + +* `buf` [\][] +* return [\][]\[] + +Returns an instance of all possible `ArrayBufferView`s of the provided Buffer. + +### `getBufferSources(buf)` + +* `buf` [\][] +* return [\][]\[] + +Returns an instance of all possible `BufferSource`s of the provided Buffer, +consisting of all `ArrayBufferView` and an `ArrayBuffer`. + +### `getTTYfd()` + +Attempts to get a valid TTY file descriptor. Returns `-1` if it fails. + +The TTY file descriptor is assumed to be capable of being writable. + +### `hasCrypto` + +* [\][] + +Indicates whether OpenSSL is available. + +### `hasFipsCrypto` + +* [\][] + +Indicates that Node.js has been linked with a FIPS compatible OpenSSL library, +and that FIPS as been enabled using `--enable-fips`. + +To only detect if the OpenSSL library is FIPS compatible, regardless if it has +been enabled or not, then `process.config.variables.openssl_is_fips` can be +used to determine that situation. + +### `hasIntl` + +* [\][] + +Indicates if [internationalization][] is supported. + +### `hasIPv6` + +* [\][] + +Indicates whether `IPv6` is supported on this platform. + +### `hasMultiLocalhost` + +* [\][] + +Indicates if there are multiple localhosts available. + +### `inFreeBSDJail` + +* [\][] + +Checks whether free BSD Jail is true or false. + +### `isAIX` + +* [\][] + +Platform check for Advanced Interactive eXecutive (AIX). + +### `isAlive(pid)` + +* `pid` [\][] +* return [\][] + +Attempts to 'kill' `pid` + +### `isDumbTerminal` + +* [\][] + +### `isFreeBSD` + +* [\][] + +Platform check for Free BSD. + +### `isIBMi` + +* [\][] + +Platform check for IBMi. + +### `isLinux` + +* [\][] + +Platform check for Linux. + +### `isLinuxPPCBE` + +* [\][] + +Platform check for Linux on PowerPC. + +### `isMacOS` + +* [\][] + +Platform check for macOS. + +### `isSunOS` + +* [\][] + +Platform check for SunOS. + +### `isWindows` + +* [\][] + +Platform check for Windows. + +### `localhostIPv4` + +* [\][] + +IP of `localhost`. + +### `localIPv6Hosts` + +* [\][] + +Array of IPV6 representations for `localhost`. + +### `mustCall([fn][, exact])` + +* `fn` [\][] default = () => {} +* `exact` [\][] default = 1 +* return [\][] + +Returns a function that calls `fn`. If the returned function has not been called +exactly `exact` number of times when the test is complete, then the test will +fail. + +If `fn` is not provided, an empty function will be used. + +### `mustCallAtLeast([fn][, minimum])` + +* `fn` [\][] default = () => {} +* `minimum` [\][] default = 1 +* return [\][] + +Returns a function that calls `fn`. If the returned function has not been called +at least `minimum` number of times when the test is complete, then the test will +fail. + +If `fn` is not provided, an empty function will be used. + +### `mustNotCall([msg])` + +* `msg` [\][] default = 'function should not have been called' +* return [\][] + +Returns a function that triggers an `AssertionError` if it is invoked. `msg` is +used as the error message for the `AssertionError`. + +### `mustNotMutateObjectDeep([target])` + +* `target` [\][] default = `undefined` +* return [\][] + +If `target` is an Object, returns a proxy object that triggers +an `AssertionError` on mutation attempt, including mutation of deeply nested +Objects. Otherwise, it returns `target` directly. + +Use of this function is encouraged for relevant regression tests. + +```mjs +import { open } from 'node:fs/promises'; +import { mustNotMutateObjectDeep } from '../common/index.mjs'; + +const _mutableOptions = { length: 4, position: 8 }; +const options = mustNotMutateObjectDeep(_mutableOptions); + +// In filehandle.read or filehandle.write, attempt to mutate options will throw +// In the test code, options can still be mutated via _mutableOptions +const fh = await open('/path/to/file', 'r+'); +const { buffer } = await fh.read(options); +_mutableOptions.position = 4; +await fh.write(buffer, options); + +// Inline usage +const stats = await fh.stat(mustNotMutateObjectDeep({ bigint: true })); +console.log(stats.size); +``` + +Caveats: built-in objects that make use of their internal slots (for example, +`Map`s and `Set`s) might not work with this function. It returns Functions +directly, not preventing their mutation. + +### `mustSucceed([fn])` + +* `fn` [\][] default = () => {} +* return [\][] + +Returns a function that accepts arguments `(err, ...args)`. If `err` is not +`undefined` or `null`, it triggers an `AssertionError`. Otherwise, it calls +`fn(...args)`. + +### `nodeProcessAborted(exitCode, signal)` + +* `exitCode` [\][] +* `signal` [\][] +* return [\][] + +Returns `true` if the exit code `exitCode` and/or signal name `signal` represent +the exit code and/or signal name of a node process that aborted, `false` +otherwise. + +### `opensslCli` + +* [\][] + +Indicates whether 'opensslCli' is supported. + +### `platformTimeout(ms)` + +* `ms` [\][] | [\][] +* return [\][] | [\][] + +Returns a timeout value based on detected conditions. For example, a debug build +may need extra time so the returned value will be larger than on a release +build. + +### `PIPE` + +* [\][] + +Path to the test socket. + +### `PORT` + +* [\][] + +A port number for tests to use if one is needed. + +### `printSkipMessage(msg)` + +* `msg` [\][] + +Logs '1..0 # Skipped: ' + `msg` + +### `pwdCommand` + +* [\][] First two argument for the `spawn`/`exec` functions. + +Platform normalized `pwd` command options. Usage example: + +```js +const common = require('../common'); +const { spawn } = require('node:child_process'); + +spawn(...common.pwdCommand, { stdio: ['pipe'] }); +``` + +### `requireNoPackageJSONAbove([dir])` + +* `dir` [\][] default = \_\_dirname + +Throws an `AssertionError` if a `package.json` file exists in any ancestor +directory above `dir`. Such files may interfere with proper test functionality. + +### `runWithInvalidFD(func)` + +* `func` [\][] + +Runs `func` with an invalid file descriptor that is an unsigned integer and +can be used to trigger `EBADF` as the first argument. If no such file +descriptor could be generated, a skip message will be printed and the `func` +will not be run. + +### `skip(msg)` + +* `msg` [\][] + +Logs '1..0 # Skipped: ' + `msg` and exits with exit code `0`. + +### `skipIfDumbTerminal()` + +Skip the rest of the tests if the current terminal is a dumb terminal + +### `skipIfEslintMissing()` + +Skip the rest of the tests in the current file when `ESLint` is not available +at `tools/eslint/node_modules/eslint` + +### `skipIfInspectorDisabled()` + +Skip the rest of the tests in the current file when the Inspector +was disabled at compile time. + +### `skipIf32Bits()` + +Skip the rest of the tests in the current file when the Node.js executable +was compiled with a pointer size smaller than 64 bits. + +### `skipIfWorker()` + +Skip the rest of the tests in the current file when not running on a main +thread. + +## ArrayStream module + +The `ArrayStream` module provides a simple `Stream` that pushes elements from +a given array. + + + +```js +const ArrayStream = require('../common/arraystream'); +const stream = new ArrayStream(); +stream.run(['a', 'b', 'c']); +``` + +It can be used within tests as a simple mock stream. + +## Countdown module + +The `Countdown` module provides a simple countdown mechanism for tests that +require a particular action to be taken after a given number of completed +tasks (for instance, shutting down an HTTP server after a specific number of +requests). The Countdown will fail the test if the remainder did not reach 0. + + + +```js +const Countdown = require('../common/countdown'); + +function doSomething() { + console.log('.'); +} + +const countdown = new Countdown(2, doSomething); +countdown.dec(); +countdown.dec(); +``` + +### `new Countdown(limit, callback)` + +* `limit` {number} +* `callback` {function} + +Creates a new `Countdown` instance. + +### `Countdown.prototype.dec()` + +Decrements the `Countdown` counter. + +### `Countdown.prototype.remaining` + +Specifies the remaining number of times `Countdown.prototype.dec()` must be +called before the callback is invoked. + +## CPU Profiler module + +The `cpu-prof` module provides utilities related to CPU profiling tests. + +### `env` + +* Default: { ...process.env, NODE\_DEBUG\_NATIVE: 'INSPECTOR\_PROFILER' } + +Environment variables used in profiled processes. + +### `getCpuProfiles(dir)` + +* `dir` {string} The directory containing the CPU profile files. +* return [\][] + +Returns an array of all `.cpuprofile` files found in `dir`. + +### `getFrames(file, suffix)` + +* `file` {string} Path to a `.cpuprofile` file. +* `suffix` {string} Suffix of the URL of call frames to retrieve. +* returns { frames: [\][], nodes: [\][] } + +Returns an object containing an array of the relevant call frames and an array +of all the profile nodes. + +### `kCpuProfInterval` + +Sampling interval in microseconds. + +### `verifyFrames(output, file, suffix)` + +* `output` {string} +* `file` {string} +* `suffix` {string} + +Throws an `AssertionError` if there are no call frames with the expected +`suffix` in the profiling data contained in `file`. + +## Debugger module + +Provides common functionality for tests for `node inspect`. + +### `startCLI(args[[, flags], spawnOpts])` + +* `args` [\][] +* `flags` [\][] default = \[] +* `showOpts` [\][] default = {} +* return [\][] + +Returns a null-prototype object with properties that are functions and getters +used to interact with the `node inspect` CLI. These functions are: + +* `flushOutput()` +* `waitFor()` +* `waitForPrompt()` +* `waitForInitialBreak()` +* `breakInfo` +* `ctrlC()` +* `output` +* `rawOutput` +* `parseSourceLines()` +* `writeLine()` +* `command()` +* `stepCommand()` +* `quit()` + +## `DNS` module + +The `DNS` module provides utilities related to the `dns` built-in module. + +### `errorLookupMock(code, syscall)` + +* `code` [\][] Defaults to `dns.mockedErrorCode`. +* `syscall` [\][] Defaults to `dns.mockedSysCall`. +* return [\][] + +A mock for the `lookup` option of `net.connect()` that would result in an error +with the `code` and the `syscall` specified. Returns a function that has the +same signature as `dns.lookup()`. + +### `mockedErrorCode` + +The default `code` of errors generated by `errorLookupMock`. + +### `mockedSysCall` + +The default `syscall` of errors generated by `errorLookupMock`. + +### `readDomainFromPacket(buffer, offset)` + +* `buffer` [\][] +* `offset` [\][] +* return [\][] + +Reads the domain string from a packet and returns an object containing the +number of bytes read and the domain. + +### `parseDNSPacket(buffer)` + +* `buffer` [\][] +* return [\][] + +Parses a DNS packet. Returns an object with the values of the various flags of +the packet depending on the type of packet. + +### `writeIPv6(ip)` + +* `ip` [\][] +* return [\][] + +Reads an IPv6 String and returns a Buffer containing the parts. + +### `writeDomainName(domain)` + +* `domain` [\][] +* return [\][] + +Reads a Domain String and returns a Buffer containing the domain. + +### `writeDNSPacket(parsed)` + +* `parsed` [\][] +* return [\][] + +Takes in a parsed Object and writes its fields to a DNS packet as a Buffer +object. + +## Environment variables + +The behavior of the Node.js test suite can be altered using the following +environment variables. + +### `NODE_COMMON_PORT` + +If set, `NODE_COMMON_PORT`'s value overrides the `common.PORT` default value of +12346\. + +### `NODE_REGENERATE_SNAPSHOTS` + +If set, test snapshots for a the current test are regenerated. +for example `NODE_REGENERATE_SNAPSHOTS=1 out/Release/node test/parallel/test-runner-output.mjs` +will update all the test runner output snapshots. + +### `NODE_SKIP_FLAG_CHECK` + +If set, command line arguments passed to individual tests are not validated. + +### `NODE_SKIP_CRYPTO` + +If set, crypto tests are skipped. + +### `NODE_TEST_KNOWN_GLOBALS` + +A comma-separated list of variables names that are appended to the global +variable allowlist. Alternatively, if `NODE_TEST_KNOWN_GLOBALS` is set to `'0'`, +global leak detection is disabled. + +## Fixtures module + +The `common/fixtures` module provides convenience methods for working with +files in the `test/fixtures` directory. + +### `fixtures.fixturesDir` + +* [\][] + +The absolute path to the `test/fixtures/` directory. + +### `fixtures.path(...args)` + +* `...args` [\][] + +Returns the result of `path.join(fixtures.fixturesDir, ...args)`. + +### `fixtures.fileURL(...args)` + +* `...args` [\][] + +Returns the result of `url.pathToFileURL(fixtures.path(...args))`. + +### `fixtures.readSync(args[, enc])` + +* `args` [\][] | [\][] + +Returns the result of +`fs.readFileSync(path.join(fixtures.fixturesDir, ...args), 'enc')`. + +### `fixtures.readKey(arg[, enc])` + +* `arg` [\][] + +Returns the result of +`fs.readFileSync(path.join(fixtures.fixturesDir, 'keys', arg), 'enc')`. + +## Heap dump checker module + +This provides utilities for checking the validity of heap dumps. +This requires the usage of `--expose-internals`. + +### `heap.recordState()` + +Create a heap dump and an embedder graph copy for inspection. +The returned object has a `validateSnapshotNodes` function similar to the +one listed below. (`heap.validateSnapshotNodes(...)` is a shortcut for +`heap.recordState().validateSnapshotNodes(...)`.) + +### `heap.validateSnapshotNodes(name, expected, options)` + +* `name` [\][] Look for this string as the name of heap dump + nodes. +* `expected` [\][] A list of objects, possibly with an `children` + property that points to expected other adjacent nodes. +* `options` [\][] + * `loose` [\][] Do not expect an exact listing of + occurrences of nodes with name `name` in `expected`. + +Create a heap dump and an embedder graph copy and validate occurrences. + + + +```js +validateSnapshotNodes('TLSWRAP', [ + { + children: [ + { name: 'enc_out' }, + { name: 'enc_in' }, + { name: 'TLSWrap' }, + ], + }, +]); +``` + +## hijackstdio module + +The `hijackstdio` module provides utility functions for temporarily redirecting +`stdout` and `stderr` output. + + + +```js +const { hijackStdout, restoreStdout } = require('../common/hijackstdio'); + +hijackStdout((data) => { + /* Do something with data */ + restoreStdout(); +}); + +console.log('this is sent to the hijacked listener'); +``` + +### `hijackStderr(listener)` + +* `listener` [\][]: a listener with a single parameter + called `data`. + +Eavesdrop to `process.stderr.write()` calls. Once `process.stderr.write()` is +called, `listener` will also be called and the `data` of `write` function will +be passed to `listener`. What's more, `process.stderr.writeTimes` is a count of +the number of calls. + +### `hijackStdout(listener)` + +* `listener` [\][]: a listener with a single parameter + called `data`. + +Eavesdrop to `process.stdout.write()` calls. Once `process.stdout.write()` is +called, `listener` will also be called and the `data` of `write` function will +be passed to `listener`. What's more, `process.stdout.writeTimes` is a count of +the number of calls. + +### restoreStderr() + +Restore the original `process.stderr.write()`. Used to restore `stderr` to its +original state after calling [`hijackstdio.hijackStdErr()`][]. + +### restoreStdout() + +Restore the original `process.stdout.write()`. Used to restore `stdout` to its +original state after calling [`hijackstdio.hijackStdOut()`][]. + +## HTTP/2 module + +The http2.js module provides a handful of utilities for creating mock HTTP/2 +frames for testing of HTTP/2 endpoints + + + +```js +const http2 = require('../common/http2'); +``` + +### Class: Frame + +The `http2.Frame` is a base class that creates a `Buffer` containing a +serialized HTTP/2 frame header. + + + +```js +// length is a 24-bit unsigned integer +// type is an 8-bit unsigned integer identifying the frame type +// flags is an 8-bit unsigned integer containing the flag bits +// id is the 32-bit stream identifier, if any. +const frame = new http2.Frame(length, type, flags, id); + +// Write the frame data to a socket +socket.write(frame.data); +``` + +The serialized `Buffer` may be retrieved using the `frame.data` property. + +### Class: HeadersFrame + +The `http2.HeadersFrame` is a subclass of `http2.Frame` that serializes a +`HEADERS` frame. + + + +```js +// id is the 32-bit stream identifier +// payload is a Buffer containing the HEADERS payload (see either +// http2.kFakeRequestHeaders or http2.kFakeResponseHeaders). +// padlen is an 8-bit integer giving the number of padding bytes to include +// final is a boolean indicating whether the End-of-stream flag should be set, +// defaults to false. +const frame = new http2.HeadersFrame(id, payload, padlen, final); + +socket.write(frame.data); +``` + +### Class: SettingsFrame + +The `http2.SettingsFrame` is a subclass of `http2.Frame` that serializes an +empty `SETTINGS` frame. + + + +```js +// ack is a boolean indicating whether or not to set the ACK flag. +const frame = new http2.SettingsFrame(ack); + +socket.write(frame.data); +``` + +### `http2.kFakeRequestHeaders` + +Set to a `Buffer` instance that contains a minimal set of serialized HTTP/2 +request headers to be used as the payload of a `http2.HeadersFrame`. + + + +```js +const frame = new http2.HeadersFrame(1, http2.kFakeRequestHeaders, 0, true); + +socket.write(frame.data); +``` + +### `http2.kFakeResponseHeaders` + +Set to a `Buffer` instance that contains a minimal set of serialized HTTP/2 +response headers to be used as the payload a `http2.HeadersFrame`. + + + +```js +const frame = new http2.HeadersFrame(1, http2.kFakeResponseHeaders, 0, true); + +socket.write(frame.data); +``` + +### `http2.kClientMagic` + +Set to a `Buffer` containing the preamble bytes an HTTP/2 client must send +upon initial establishment of a connection. + + + +```js +socket.write(http2.kClientMagic); +``` + +## Internet module + +The `common/internet` module provides utilities for working with +internet-related tests. + +### `internet.addresses` + +* [\][] + * `INET_HOST` [\][] A generic host that has registered common + DNS records, supports both IPv4 and IPv6, and provides basic HTTP/HTTPS + services + * `INET4_HOST` [\][] A host that provides IPv4 services + * `INET6_HOST` [\][] A host that provides IPv6 services + * `INET4_IP` [\][] An accessible IPv4 IP, defaults to the + Google Public DNS IPv4 address + * `INET6_IP` [\][] An accessible IPv6 IP, defaults to the + Google Public DNS IPv6 address + * `INVALID_HOST` [\][] An invalid host that cannot be resolved + * `MX_HOST` [\][] A host with MX records registered + * `SRV_HOST` [\][] A host with SRV records registered + * `PTR_HOST` [\][] A host with PTR records registered + * `NAPTR_HOST` [\][] A host with NAPTR records registered + * `SOA_HOST` [\][] A host with SOA records registered + * `CNAME_HOST` [\][] A host with CNAME records registered + * `NS_HOST` [\][] A host with NS records registered + * `TXT_HOST` [\][] A host with TXT records registered + * `DNS4_SERVER` [\][] An accessible IPv4 DNS server + * `DNS6_SERVER` [\][] An accessible IPv6 DNS server + +A set of addresses for internet-related tests. All properties are configurable +via `NODE_TEST_*` environment variables. For example, to configure +`internet.addresses.INET_HOST`, set the environment +variable `NODE_TEST_INET_HOST` to a specified host. + +## ongc module + +The `ongc` module allows a garbage collection listener to be installed. The +module exports a single `onGC()` function. + +```js +require('../common'); +const { onGC } = require('../common/gc'); + +onGC({}, { ongc() { console.log('collected'); } }); +``` + +### `onGC(target, listener)` + +* `target` [\][] +* `listener` [\][] + * `ongc` [\][] + +Installs a GC listener for the collection of `target`. + +This uses `async_hooks` for GC tracking. This means that it enables +`async_hooks` tracking, which may affect the test functionality. It also +means that between a `global.gc()` call and the listener being invoked +a full `setImmediate()` invocation passes. + +`listener` is an object to make it easier to use a closure; the target object +should not be in scope when `listener.ongc()` is created. + +## process-exit-code-test-cases module + +The `process-exit-code-test-cases` module provides a set of shared test cases +for testing the exit codes of the `process` object. The test cases are shared +between `test/parallel/test-process-exit-code.js` and +`test/parallel/test-worker-exit-code.js`. + +### `getTestCases(isWorker)` + +* `isWorker` [\][] +* return [\][] + +Returns an array of test cases for testing the exit codes of the `process`. Each +test case is an object with a `func` property that is a function that runs the +test case, a `result` property that is the expected exit code, and sometimes an +`error` property that is a regular expression that the error message should +match when the test case is run in a worker thread. + +The `isWorker` parameter is used to adjust the test cases for worker threads. +The default value is `false`. + +## Report module + +The `report` module provides helper functions for testing diagnostic reporting +functionality. + +### `findReports(pid, dir)` + +* `pid` [\][] Process ID to retrieve diagnostic report files + for. +* `dir` [\][] Directory to search for diagnostic report files. +* return [\][] + +Returns an array of diagnostic report file names found in `dir`. The files +should have been generated by a process whose PID matches `pid`. + +### `validate(filepath)` + +* `filepath` [\][] Diagnostic report filepath to validate. + +Validates the schema of a diagnostic report file whose path is specified in +`filepath`. If the report fails validation, an exception is thrown. + +### `validateContent(report)` + +* `report` [\][] | [\][] JSON contents of a + diagnostic report file, the parsed Object thereof, or the result of + `process.report.getReport()`. + +Validates the schema of a diagnostic report whose content is specified in +`report`. If the report fails validation, an exception is thrown. + +## SEA Module + +The `sea` module provides helper functions for testing Single Executable +Application functionality. + +### `skipIfSingleExecutableIsNotSupported()` + +Skip the rest of the tests if single executable applications are not supported +in the current configuration. + +### `generateSEA(targetExecutable, sourceExecutable, seaBlob, verifyWorkflow)` + +Copy `sourceExecutable` to `targetExecutable`, use postject to inject `seaBlob` +into `targetExecutable` and sign it if necessary. + +If `verifyWorkflow` is false (default) and any of the steps fails, +it skips the tests. Otherwise, an error is thrown. + +## tick module + +The `tick` module provides a helper function that can be used to call a callback +after a given number of event loop "ticks". + +### `tick(x, cb)` + +* `x` [\][] Number of event loop "ticks". +* `cb` [\][] A callback function. + +## tmpdir module + +The `tmpdir` module supports the use of a temporary directory for testing. + +### `path` + +* [\][] + +The realpath of the testing temporary directory. + +### `fileURL([...paths])` + +* `...paths` [\][] +* return [\][] + +Resolves a sequence of paths into absolute url in the temporary directory. + +When called without arguments, returns absolute url of the testing +temporary directory with explicit trailing `/`. + +### `refresh(useSpawn)` + +* `useSpawn` [\][] default = false + +Deletes and recreates the testing temporary directory. When `useSpawn` is true +this action is performed using `child_process.spawnSync`. + +The first time `refresh()` runs, it adds a listener to process `'exit'` that +cleans the temporary directory. Thus, every file under `tmpdir.path` needs to +be closed before the test completes. A good way to do this is to add a +listener to process `'beforeExit'`. If a file needs to be left open until +Node.js completes, use a child process and call `refresh()` only in the +parent. + +It is usually only necessary to call `refresh()` once in a test file. +Avoid calling it more than once in an asynchronous context as one call +might refresh the temporary directory of a different context, causing +the test to fail somewhat mysteriously. + +### `resolve([...paths])` + +* `...paths` [\][] +* return [\][] + +Resolves a sequence of paths into absolute path in the temporary directory. + +### `hasEnoughSpace(size)` + +* `size` [\][] Required size, in bytes. + +Returns `true` if the available blocks of the file system underlying `path` +are likely sufficient to hold a single file of `size` bytes. This is useful for +skipping tests that require hundreds of megabytes or even gigabytes of temporary +files, but it is inaccurate and susceptible to race conditions. + +## WPT module + +### `harness` + +A legacy port of [Web Platform Tests][] harness. + +See the source code for definitions. Please avoid using it in new +code - the current usage of this port in tests is being migrated to +the original WPT harness, see [the WPT tests README][]. + +### Class: WPTRunner + +A driver class for running WPT with the WPT harness in a worker thread. + +See [the WPT tests README][] for details. + +[]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array +[]: https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView +[]: https://nodejs.org/api/buffer.html#buffer_class_buffer +[]: https://developer.mozilla.org/en-US/docs/Web/API/BufferSource +[]: ../../doc/api/child_process.md#class-childprocess +[]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error +[]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function +[]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object +[]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp +[]: https://developer.mozilla.org/en-US/docs/Web/API/URL +[]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Data_types +[]: https://github.com/tc39/proposal-bigint +[]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type +[]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type +[]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type +[Web Platform Tests]: https://github.com/web-platform-tests/wpt +[`child_process.spawnSync()`]: ../../doc/api/child_process.md#child_processspawnsynccommand-args-options +[`hijackstdio.hijackStdErr()`]: #hijackstderrlistener +[`hijackstdio.hijackStdOut()`]: #hijackstdoutlistener +[internationalization]: ../../doc/api/intl.md +[the WPT tests README]: ../wpt/README.md diff --git a/test/napi/node-napi-tests/test/common/arraystream.js b/test/napi/node-napi-tests/test/common/arraystream.js new file mode 100644 index 0000000000..c9dae0512b --- /dev/null +++ b/test/napi/node-napi-tests/test/common/arraystream.js @@ -0,0 +1,23 @@ +'use strict'; + +const { Stream } = require('stream'); +function noop() {} + +// A stream to push an array into a REPL +function ArrayStream() { + this.run = function(data) { + data.forEach((line) => { + this.emit('data', `${line}\n`); + }); + }; +} + +Object.setPrototypeOf(ArrayStream.prototype, Stream.prototype); +Object.setPrototypeOf(ArrayStream, Stream); +ArrayStream.prototype.readable = true; +ArrayStream.prototype.writable = true; +ArrayStream.prototype.pause = noop; +ArrayStream.prototype.resume = noop; +ArrayStream.prototype.write = noop; + +module.exports = ArrayStream; diff --git a/test/napi/node-napi-tests/test/common/assertSnapshot.js b/test/napi/node-napi-tests/test/common/assertSnapshot.js new file mode 100644 index 0000000000..dbd4a60798 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/assertSnapshot.js @@ -0,0 +1,104 @@ +'use strict'; +const common = require('.'); +const path = require('node:path'); +const test = require('node:test'); +const fs = require('node:fs/promises'); +const assert = require('node:assert/strict'); + +const stackFramesRegexp = /(?<=\n)(\s+)((.+?)\s+\()?(?:\(?(.+?):(\d+)(?::(\d+))?)\)?(\s+\{)?(\[\d+m)?(\n|$)/g; +const windowNewlineRegexp = /\r/g; + +function replaceNodeVersion(str) { + return str.replaceAll(process.version, '*'); +} + +function replaceStackTrace(str, replacement = '$1*$7$8\n') { + return str.replace(stackFramesRegexp, replacement); +} + +function replaceWindowsLineEndings(str) { + return str.replace(windowNewlineRegexp, ''); +} + +function replaceWindowsPaths(str) { + return common.isWindows ? str.replaceAll(path.win32.sep, path.posix.sep) : str; +} + +function replaceFullPaths(str) { + return str.replaceAll('\\\'', "'").replaceAll(path.resolve(__dirname, '../..'), ''); +} + +function transform(...args) { + return (str) => args.reduce((acc, fn) => fn(acc), str); +} + +function getSnapshotPath(filename) { + const { name, dir } = path.parse(filename); + return path.resolve(dir, `${name}.snapshot`); +} + +async function assertSnapshot(actual, filename = process.argv[1]) { + const snapshot = getSnapshotPath(filename); + if (process.env.NODE_REGENERATE_SNAPSHOTS) { + await fs.writeFile(snapshot, actual); + } else { + let expected; + try { + expected = await fs.readFile(snapshot, 'utf8'); + } catch (e) { + if (e.code === 'ENOENT') { + console.log( + 'Snapshot file does not exist. You can create a new one by running the test with NODE_REGENERATE_SNAPSHOTS=1', + ); + } + throw e; + } + assert.strictEqual(actual, replaceWindowsLineEndings(expected)); + } +} + +/** + * Spawn a process and assert its output against a snapshot. + * if you want to automatically update the snapshot, run tests with NODE_REGENERATE_SNAPSHOTS=1 + * transform is a function that takes the output and returns a string that will be compared against the snapshot + * this is useful for normalizing output such as stack traces + * there are some predefined transforms in this file such as replaceStackTrace and replaceWindowsLineEndings + * both of which can be used as an example for writing your own + * compose multiple transforms by passing them as arguments to the transform function: + * assertSnapshot.transform(assertSnapshot.replaceStackTrace, assertSnapshot.replaceWindowsLineEndings) + * @param {string} filename + * @param {function(string): string} [transform] + * @param {object} [options] - control how the child process is spawned + * @param {boolean} [options.tty] - whether to spawn the process in a pseudo-tty + * @returns {Promise} + */ +async function spawnAndAssert(filename, transform = (x) => x, { tty = false, ...options } = {}) { + if (tty && common.isWindows) { + test({ skip: 'Skipping pseudo-tty tests, as pseudo terminals are not available on Windows.' }); + return; + } + let flags = common.parseTestFlags(filename); + if (options.flags) { + flags = [...options.flags, ...flags]; + } + + const executable = tty ? (process.env.PYTHON || 'python3') : process.execPath; + const args = + tty ? + [path.join(__dirname, '../..', 'tools/pseudo-tty.py'), process.execPath, ...flags, filename] : + [...flags, filename]; + const { stdout, stderr } = await common.spawnPromisified(executable, args, options); + await assertSnapshot(transform(`${stdout}${stderr}`), filename); +} + +module.exports = { + assertSnapshot, + getSnapshotPath, + replaceFullPaths, + replaceNodeVersion, + replaceStackTrace, + replaceWindowsLineEndings, + replaceWindowsPaths, + spawnAndAssert, + transform, +}; diff --git a/test/napi/node-napi-tests/test/common/benchmark.js b/test/napi/node-napi-tests/test/common/benchmark.js new file mode 100644 index 0000000000..7211ff8703 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/benchmark.js @@ -0,0 +1,54 @@ +'use strict'; + +const assert = require('assert'); +const fork = require('child_process').fork; +const path = require('path'); + +const runjs = path.join(__dirname, '..', '..', 'benchmark', 'run.js'); + +function runBenchmark(name, env) { + const argv = ['test']; + + argv.push(name); + + const mergedEnv = { ...process.env, ...env }; + + const child = fork(runjs, argv, { + env: mergedEnv, + stdio: ['inherit', 'pipe', 'inherit', 'ipc'], + }); + child.stdout.setEncoding('utf8'); + + let stdout = ''; + child.stdout.on('data', (line) => { + stdout += line; + }); + + child.on('exit', (code, signal) => { + assert.strictEqual(code, 0); + assert.strictEqual(signal, null); + + // This bit makes sure that each benchmark file is being sent settings such + // that the benchmark file runs just one set of options. This helps keep the + // benchmark tests from taking a long time to run. Therefore, stdout should be composed as follows: + // The first and last lines should be empty. + // Each test should be separated by a blank line. + // The first line of each test should contain the test's name. + // The second line of each test should contain the configuration for the test. + // If the test configuration is not a group, there should be exactly two lines. + // Otherwise, it is possible to have more than two lines. + + const splitTests = stdout.split(/\n\s*\n/); + + for (let testIdx = 1; testIdx < splitTests.length - 1; testIdx++) { + const lines = splitTests[testIdx].split('\n'); + assert.ok(/.+/.test(lines[0])); + + if (!lines[1].includes('group="')) { + assert.strictEqual(lines.length, 2, `benchmark file not running exactly one configuration in test: ${stdout}`); + } + } + }); +} + +module.exports = runBenchmark; diff --git a/test/napi/node-napi-tests/test/common/child_process.js b/test/napi/node-napi-tests/test/common/child_process.js new file mode 100644 index 0000000000..6c2bc6c961 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/child_process.js @@ -0,0 +1,156 @@ +'use strict'; + +const assert = require('assert'); +const { spawnSync, execFileSync } = require('child_process'); +const common = require('./'); +const util = require('util'); + +// Workaround for Windows Server 2008R2 +// When CMD is used to launch a process and CMD is killed too quickly, the +// process can stay behind running in suspended state, never completing. +function cleanupStaleProcess(filename) { + if (!common.isWindows) { + return; + } + process.once('beforeExit', () => { + const basename = filename.replace(/.*[/\\]/g, ''); + try { + execFileSync(`${process.env.SystemRoot}\\System32\\wbem\\WMIC.exe`, [ + 'process', + 'where', + `commandline like '%${basename}%child'`, + 'delete', + '/nointeractive', + ]); + } catch { + // Ignore failures, there might not be any stale process to clean up. + } + }); +} + +// This should keep the child process running long enough to expire +// the timeout. +const kExpiringChildRunTime = common.platformTimeout(20 * 1000); +const kExpiringParentTimer = 1; +assert(kExpiringChildRunTime > kExpiringParentTimer); + +function logAfterTime(time) { + setTimeout(() => { + // The following console statements are part of the test. + console.log('child stdout'); + console.error('child stderr'); + }, time); +} + +function checkOutput(str, check) { + if ((check instanceof RegExp && !check.test(str)) || + (typeof check === 'string' && check !== str)) { + return { passed: false, reason: `did not match ${util.inspect(check)}` }; + } + if (typeof check === 'function') { + try { + check(str); + } catch (error) { + return { + passed: false, + reason: `did not match expectation, checker throws:\n${util.inspect(error)}`, + }; + } + } + return { passed: true }; +} + +function expectSyncExit(caller, spawnArgs, { + status, + signal, + stderr: stderrCheck, + stdout: stdoutCheck, + trim = false, +}) { + const child = spawnSync(...spawnArgs); + const failures = []; + let stderrStr, stdoutStr; + if (status !== undefined && child.status !== status) { + failures.push(`- process terminated with status ${child.status}, expected ${status}`); + } + if (signal !== undefined && child.signal !== signal) { + failures.push(`- process terminated with signal ${child.signal}, expected ${signal}`); + } + + function logAndThrow() { + const tag = `[process ${child.pid}]:`; + console.error(`${tag} --- stderr ---`); + console.error(stderrStr === undefined ? child.stderr.toString() : stderrStr); + console.error(`${tag} --- stdout ---`); + console.error(stdoutStr === undefined ? child.stdout.toString() : stdoutStr); + console.error(`${tag} status = ${child.status}, signal = ${child.signal}`); + + const error = new Error(`${failures.join('\n')}`); + if (spawnArgs[2]) { + error.options = spawnArgs[2]; + } + let command = spawnArgs[0]; + if (Array.isArray(spawnArgs[1])) { + command += ' ' + spawnArgs[1].join(' '); + } + error.command = command; + Error.captureStackTrace(error, caller); + throw error; + } + + // If status and signal are not matching expectations, fail early. + if (failures.length !== 0) { + logAndThrow(); + } + + if (stderrCheck !== undefined) { + stderrStr = child.stderr.toString(); + const { passed, reason } = checkOutput(trim ? stderrStr.trim() : stderrStr, stderrCheck); + if (!passed) { + failures.push(`- stderr ${reason}`); + } + } + if (stdoutCheck !== undefined) { + stdoutStr = child.stdout.toString(); + const { passed, reason } = checkOutput(trim ? stdoutStr.trim() : stdoutStr, stdoutCheck); + if (!passed) { + failures.push(`- stdout ${reason}`); + } + } + if (failures.length !== 0) { + logAndThrow(); + } + return { child, stderr: stderrStr, stdout: stdoutStr }; +} + +function spawnSyncAndExit(...args) { + const spawnArgs = args.slice(0, args.length - 1); + const expectations = args[args.length - 1]; + return expectSyncExit(spawnSyncAndExit, spawnArgs, expectations); +} + +function spawnSyncAndExitWithoutError(...args) { + return expectSyncExit(spawnSyncAndExitWithoutError, [...args], { + status: 0, + signal: null, + }); +} + +function spawnSyncAndAssert(...args) { + const expectations = args.pop(); + return expectSyncExit(spawnSyncAndAssert, [...args], { + status: 0, + signal: null, + ...expectations, + }); +} + +module.exports = { + cleanupStaleProcess, + logAfterTime, + kExpiringChildRunTime, + kExpiringParentTimer, + spawnSyncAndAssert, + spawnSyncAndExit, + spawnSyncAndExitWithoutError, +}; diff --git a/test/napi/node-napi-tests/test/common/countdown.js b/test/napi/node-napi-tests/test/common/countdown.js new file mode 100644 index 0000000000..4aa86b4253 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/countdown.js @@ -0,0 +1,28 @@ +'use strict'; + +const assert = require('assert'); +const kLimit = Symbol('limit'); +const kCallback = Symbol('callback'); +const common = require('./'); + +class Countdown { + constructor(limit, cb) { + assert.strictEqual(typeof limit, 'number'); + assert.strictEqual(typeof cb, 'function'); + this[kLimit] = limit; + this[kCallback] = common.mustCall(cb); + } + + dec() { + assert(this[kLimit] > 0, 'Countdown expired'); + if (--this[kLimit] === 0) + this[kCallback](); + return this[kLimit]; + } + + get remaining() { + return this[kLimit]; + } +} + +module.exports = Countdown; diff --git a/test/napi/node-napi-tests/test/common/cpu-prof.js b/test/napi/node-napi-tests/test/common/cpu-prof.js new file mode 100644 index 0000000000..42f55b35fe --- /dev/null +++ b/test/napi/node-napi-tests/test/common/cpu-prof.js @@ -0,0 +1,50 @@ +'use strict'; + +require('./'); +const fs = require('fs'); +const path = require('path'); +const assert = require('assert'); + +function getCpuProfiles(dir) { + const list = fs.readdirSync(dir); + return list + .filter((file) => file.endsWith('.cpuprofile')) + .map((file) => path.join(dir, file)); +} + +function getFrames(file, suffix) { + const data = fs.readFileSync(file, 'utf8'); + const profile = JSON.parse(data); + const frames = profile.nodes.filter((i) => { + const frame = i.callFrame; + return frame.url.endsWith(suffix); + }); + return { frames, nodes: profile.nodes }; +} + +function verifyFrames(output, file, suffix) { + const { frames, nodes } = getFrames(file, suffix); + if (frames.length === 0) { + // Show native debug output and the profile for debugging. + console.log(output.stderr.toString()); + console.log(nodes); + } + assert.notDeepStrictEqual(frames, []); +} + +// We need to set --cpu-interval to a smaller value to make sure we can +// find our workload in the samples. 50us should be a small enough sampling +// interval for this. +const kCpuProfInterval = 50; +const env = { + ...process.env, + NODE_DEBUG_NATIVE: 'INSPECTOR_PROFILER', +}; + +module.exports = { + getCpuProfiles, + kCpuProfInterval, + env, + getFrames, + verifyFrames, +}; diff --git a/test/napi/node-napi-tests/test/common/crypto.js b/test/napi/node-napi-tests/test/common/crypto.js new file mode 100644 index 0000000000..10432d7e7a --- /dev/null +++ b/test/napi/node-napi-tests/test/common/crypto.js @@ -0,0 +1,114 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); +const { + createSign, + createVerify, + publicEncrypt, + privateDecrypt, + sign, + verify, +} = crypto; + +// The values below (modp2/modp2buf) are for a 1024 bits long prime from +// RFC 2412 E.2, see https://tools.ietf.org/html/rfc2412. */ +const modp2buf = Buffer.from([ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, + 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b, + 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, + 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, + 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd, 0xef, 0x95, + 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, + 0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, + 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, + 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x37, 0xed, 0x6b, 0x0b, 0xff, + 0x5c, 0xb6, 0xf4, 0x06, 0xb7, 0xed, 0xee, 0x38, 0x6b, 0xfb, + 0x5a, 0x89, 0x9f, 0xa5, 0xae, 0x9f, 0x24, 0x11, 0x7c, 0x4b, + 0x1f, 0xe6, 0x49, 0x28, 0x66, 0x51, 0xec, 0xe6, 0x53, 0x81, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, +]); + +// Asserts that the size of the given key (in chars or bytes) is within 10% of +// the expected size. +function assertApproximateSize(key, expectedSize) { + const u = typeof key === 'string' ? 'chars' : 'bytes'; + const min = Math.floor(0.9 * expectedSize); + const max = Math.ceil(1.1 * expectedSize); + assert(key.length >= min, + `Key (${key.length} ${u}) is shorter than expected (${min} ${u})`); + assert(key.length <= max, + `Key (${key.length} ${u}) is longer than expected (${max} ${u})`); +} + +// Tests that a key pair can be used for encryption / decryption. +function testEncryptDecrypt(publicKey, privateKey) { + const message = 'Hello Node.js world!'; + const plaintext = Buffer.from(message, 'utf8'); + for (const key of [publicKey, privateKey]) { + const ciphertext = publicEncrypt(key, plaintext); + const received = privateDecrypt(privateKey, ciphertext); + assert.strictEqual(received.toString('utf8'), message); + } +} + +// Tests that a key pair can be used for signing / verification. +function testSignVerify(publicKey, privateKey) { + const message = Buffer.from('Hello Node.js world!'); + + function oldSign(algo, data, key) { + return createSign(algo).update(data).sign(key); + } + + function oldVerify(algo, data, key, signature) { + return createVerify(algo).update(data).verify(key, signature); + } + + for (const signFn of [sign, oldSign]) { + const signature = signFn('SHA256', message, privateKey); + for (const verifyFn of [verify, oldVerify]) { + for (const key of [publicKey, privateKey]) { + const okay = verifyFn('SHA256', message, key, signature); + assert(okay); + } + } + } +} + +// Constructs a regular expression for a PEM-encoded key with the given label. +function getRegExpForPEM(label, cipher) { + const head = `\\-\\-\\-\\-\\-BEGIN ${label}\\-\\-\\-\\-\\-`; + const rfc1421Header = cipher == null ? '' : + `\nProc-Type: 4,ENCRYPTED\nDEK-Info: ${cipher},[^\n]+\n`; + const body = '([a-zA-Z0-9\\+/=]{64}\n)*[a-zA-Z0-9\\+/=]{1,64}'; + const end = `\\-\\-\\-\\-\\-END ${label}\\-\\-\\-\\-\\-`; + return new RegExp(`^${head}${rfc1421Header}\n${body}\n${end}\n$`); +} + +const pkcs1PubExp = getRegExpForPEM('RSA PUBLIC KEY'); +const pkcs1PrivExp = getRegExpForPEM('RSA PRIVATE KEY'); +const pkcs1EncExp = (cipher) => getRegExpForPEM('RSA PRIVATE KEY', cipher); +const spkiExp = getRegExpForPEM('PUBLIC KEY'); +const pkcs8Exp = getRegExpForPEM('PRIVATE KEY'); +const pkcs8EncExp = getRegExpForPEM('ENCRYPTED PRIVATE KEY'); +const sec1Exp = getRegExpForPEM('EC PRIVATE KEY'); +const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher); + +module.exports = { + modp2buf, + assertApproximateSize, + testEncryptDecrypt, + testSignVerify, + pkcs1PubExp, + pkcs1PrivExp, + pkcs1EncExp, // used once + spkiExp, + pkcs8Exp, // used once + pkcs8EncExp, // used once + sec1Exp, + sec1EncExp, +}; diff --git a/test/napi/node-napi-tests/test/common/debugger.js b/test/napi/node-napi-tests/test/common/debugger.js new file mode 100644 index 0000000000..d5d77fc7c6 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/debugger.js @@ -0,0 +1,183 @@ +'use strict'; +const common = require('../common'); +const spawn = require('child_process').spawn; + +const BREAK_MESSAGE = new RegExp('(?:' + [ + 'assert', 'break', 'break on start', 'debugCommand', + 'exception', 'other', 'promiseRejection', 'step', +].join('|') + ') in', 'i'); + +let TIMEOUT = common.platformTimeout(5000); +if (common.isWindows) { + // Some of the windows machines in the CI need more time to receive + // the outputs from the client. + // https://github.com/nodejs/build/issues/3014 + TIMEOUT = common.platformTimeout(15000); +} + +function isPreBreak(output) { + return /Break on start/.test(output) && /1 \(function \(exports/.test(output); +} + +function startCLI(args, flags = [], spawnOpts = {}) { + let stderrOutput = ''; + const child = + spawn(process.execPath, [...flags, 'inspect', ...args], spawnOpts); + + const outputBuffer = []; + function bufferOutput(chunk) { + if (this === child.stderr) { + stderrOutput += chunk; + } + outputBuffer.push(chunk); + } + + function getOutput() { + return outputBuffer.join('\n').replaceAll('\b', ''); + } + + child.stdout.setEncoding('utf8'); + child.stdout.on('data', bufferOutput); + child.stderr.setEncoding('utf8'); + child.stderr.on('data', bufferOutput); + + if (process.env.VERBOSE === '1') { + child.stdout.pipe(process.stdout); + child.stderr.pipe(process.stderr); + } + + return { + flushOutput() { + const output = this.output; + outputBuffer.length = 0; + return output; + }, + + waitFor(pattern) { + function checkPattern(str) { + if (Array.isArray(pattern)) { + return pattern.every((p) => p.test(str)); + } + return pattern.test(str); + } + + return new Promise((resolve, reject) => { + function checkOutput() { + if (checkPattern(getOutput())) { + tearDown(); + resolve(); + } + } + + function onChildClose(code, signal) { + tearDown(); + let message = 'Child exited'; + if (code) { + message += `, code ${code}`; + } + if (signal) { + message += `, signal ${signal}`; + } + message += ` while waiting for ${pattern}; found: ${this.output}`; + if (stderrOutput) { + message += `\n STDERR: ${stderrOutput}`; + } + reject(new Error(message)); + } + + const timer = setTimeout(() => { + tearDown(); + reject(new Error([ + `Timeout (${TIMEOUT}) while waiting for ${pattern}`, + `found: ${this.output}`, + ].join('; '))); + }, TIMEOUT); + + function tearDown() { + clearTimeout(timer); + child.stdout.removeListener('data', checkOutput); + child.removeListener('close', onChildClose); + } + + child.on('close', onChildClose); + child.stdout.on('data', checkOutput); + checkOutput(); + }); + }, + + waitForPrompt() { + return this.waitFor(/>\s+$/); + }, + + async waitForInitialBreak() { + await this.waitFor(/break (?:on start )?in/i); + + if (isPreBreak(this.output)) { + await this.command('next', false); + return this.waitFor(/break in/); + } + }, + + get breakInfo() { + const output = this.output; + const breakMatch = + output.match(/(step |break (?:on start )?)in ([^\n]+):(\d+)\n/i); + + if (breakMatch === null) { + throw new Error( + `Could not find breakpoint info in ${JSON.stringify(output)}`); + } + return { filename: breakMatch[2], line: +breakMatch[3] }; + }, + + ctrlC() { + return this.command('.interrupt'); + }, + + get output() { + return getOutput(); + }, + + get rawOutput() { + return outputBuffer.join('').toString(); + }, + + parseSourceLines() { + return getOutput().split('\n') + .map((line) => line.match(/(?:\*|>)?\s*(\d+)/)) + .filter((match) => match !== null) + .map((match) => +match[1]); + }, + + writeLine(input, flush = true) { + if (flush) { + this.flushOutput(); + } + if (process.env.VERBOSE === '1') { + process.stderr.write(`< ${input}\n`); + } + child.stdin.write(input); + child.stdin.write('\n'); + }, + + command(input, flush = true) { + this.writeLine(input, flush); + return this.waitForPrompt(); + }, + + stepCommand(input) { + this.writeLine(input, true); + return this + .waitFor(BREAK_MESSAGE) + .then(() => this.waitForPrompt()); + }, + + quit() { + return new Promise((resolve) => { + child.stdin.end(); + child.on('close', resolve); + }); + }, + }; +} +module.exports = startCLI; diff --git a/test/napi/node-napi-tests/test/common/dns.js b/test/napi/node-napi-tests/test/common/dns.js new file mode 100644 index 0000000000..738f2299dd --- /dev/null +++ b/test/napi/node-napi-tests/test/common/dns.js @@ -0,0 +1,341 @@ +'use strict'; + +const assert = require('assert'); +const os = require('os'); +const { isIP } = require('net'); + +const types = { + A: 1, + AAAA: 28, + NS: 2, + CNAME: 5, + SOA: 6, + PTR: 12, + MX: 15, + TXT: 16, + ANY: 255, + CAA: 257, +}; + +const classes = { + IN: 1, +}; + +// Naïve DNS parser/serializer. + +function readDomainFromPacket(buffer, offset) { + assert.ok(offset < buffer.length); + const length = buffer[offset]; + if (length === 0) { + return { nread: 1, domain: '' }; + } else if ((length & 0xC0) === 0) { + offset += 1; + const chunk = buffer.toString('ascii', offset, offset + length); + // Read the rest of the domain. + const { nread, domain } = readDomainFromPacket(buffer, offset + length); + return { + nread: 1 + length + nread, + domain: domain ? `${chunk}.${domain}` : chunk, + }; + } + // Pointer to another part of the packet. + assert.strictEqual(length & 0xC0, 0xC0); + // eslint-disable-next-line @stylistic/js/space-infix-ops, @stylistic/js/space-unary-ops + const pointeeOffset = buffer.readUInt16BE(offset) &~ 0xC000; + return { + nread: 2, + domain: readDomainFromPacket(buffer, pointeeOffset), + }; +} + +function parseDNSPacket(buffer) { + assert.ok(buffer.length > 12); + + const parsed = { + id: buffer.readUInt16BE(0), + flags: buffer.readUInt16BE(2), + }; + + const counts = [ + ['questions', buffer.readUInt16BE(4)], + ['answers', buffer.readUInt16BE(6)], + ['authorityAnswers', buffer.readUInt16BE(8)], + ['additionalRecords', buffer.readUInt16BE(10)], + ]; + + let offset = 12; + for (const [ sectionName, count ] of counts) { + parsed[sectionName] = []; + for (let i = 0; i < count; ++i) { + const { nread, domain } = readDomainFromPacket(buffer, offset); + offset += nread; + + const type = buffer.readUInt16BE(offset); + + const rr = { + domain, + cls: buffer.readUInt16BE(offset + 2), + }; + offset += 4; + + for (const name in types) { + if (types[name] === type) + rr.type = name; + } + + if (sectionName !== 'questions') { + rr.ttl = buffer.readInt32BE(offset); + const dataLength = buffer.readUInt16BE(offset); + offset += 6; + + switch (type) { + case types.A: + assert.strictEqual(dataLength, 4); + rr.address = `${buffer[offset + 0]}.${buffer[offset + 1]}.` + + `${buffer[offset + 2]}.${buffer[offset + 3]}`; + break; + case types.AAAA: + assert.strictEqual(dataLength, 16); + rr.address = buffer.toString('hex', offset, offset + 16) + .replace(/(.{4}(?!$))/g, '$1:'); + break; + case types.TXT: + { + let position = offset; + rr.entries = []; + while (position < offset + dataLength) { + const txtLength = buffer[offset]; + rr.entries.push(buffer.toString('utf8', + position + 1, + position + 1 + txtLength)); + position += 1 + txtLength; + } + assert.strictEqual(position, offset + dataLength); + break; + } + case types.MX: + { + rr.priority = buffer.readInt16BE(buffer, offset); + offset += 2; + const { nread, domain } = readDomainFromPacket(buffer, offset); + rr.exchange = domain; + assert.strictEqual(nread, dataLength); + break; + } + case types.NS: + case types.CNAME: + case types.PTR: + { + const { nread, domain } = readDomainFromPacket(buffer, offset); + rr.value = domain; + assert.strictEqual(nread, dataLength); + break; + } + case types.SOA: + { + const mname = readDomainFromPacket(buffer, offset); + const rname = readDomainFromPacket(buffer, offset + mname.nread); + rr.nsname = mname.domain; + rr.hostmaster = rname.domain; + const trailerOffset = offset + mname.nread + rname.nread; + rr.serial = buffer.readUInt32BE(trailerOffset); + rr.refresh = buffer.readUInt32BE(trailerOffset + 4); + rr.retry = buffer.readUInt32BE(trailerOffset + 8); + rr.expire = buffer.readUInt32BE(trailerOffset + 12); + rr.minttl = buffer.readUInt32BE(trailerOffset + 16); + + assert.strictEqual(trailerOffset + 20, dataLength); + break; + } + default: + throw new Error(`Unknown RR type ${rr.type}`); + } + offset += dataLength; + } + + parsed[sectionName].push(rr); + + assert.ok(offset <= buffer.length); + } + } + + assert.strictEqual(offset, buffer.length); + return parsed; +} + +function writeIPv6(ip) { + const parts = ip.replace(/^:|:$/g, '').split(':'); + const buf = Buffer.alloc(16); + + let offset = 0; + for (const part of parts) { + if (part === '') { + offset += 16 - 2 * (parts.length - 1); + } else { + buf.writeUInt16BE(parseInt(part, 16), offset); + offset += 2; + } + } + + return buf; +} + +function writeDomainName(domain) { + return Buffer.concat(domain.split('.').map((label) => { + assert(label.length < 64); + return Buffer.concat([ + Buffer.from([label.length]), + Buffer.from(label, 'ascii'), + ]); + }).concat([Buffer.alloc(1)])); +} + +function writeDNSPacket(parsed) { + const buffers = []; + const kStandardResponseFlags = 0x8180; + + buffers.push(new Uint16Array([ + parsed.id, + parsed.flags ?? kStandardResponseFlags, + parsed.questions?.length, + parsed.answers?.length, + parsed.authorityAnswers?.length, + parsed.additionalRecords?.length, + ])); + + for (const q of parsed.questions) { + assert(types[q.type]); + buffers.push(writeDomainName(q.domain)); + buffers.push(new Uint16Array([ + types[q.type], + q.cls === undefined ? classes.IN : q.cls, + ])); + } + + for (const rr of [].concat(parsed.answers, + parsed.authorityAnswers, + parsed.additionalRecords)) { + if (!rr) continue; + + assert(types[rr.type]); + buffers.push(writeDomainName(rr.domain)); + buffers.push(new Uint16Array([ + types[rr.type], + rr.cls === undefined ? classes.IN : rr.cls, + ])); + buffers.push(new Int32Array([rr.ttl])); + + const rdLengthBuf = new Uint16Array(1); + buffers.push(rdLengthBuf); + + switch (rr.type) { + case 'A': + rdLengthBuf[0] = 4; + buffers.push(new Uint8Array(rr.address.split('.'))); + break; + case 'AAAA': + rdLengthBuf[0] = 16; + buffers.push(writeIPv6(rr.address)); + break; + case 'TXT': { + const total = rr.entries.map((s) => s.length).reduce((a, b) => a + b); + // Total length of all strings + 1 byte each for their lengths. + rdLengthBuf[0] = rr.entries.length + total; + for (const txt of rr.entries) { + buffers.push(new Uint8Array([Buffer.byteLength(txt)])); + buffers.push(Buffer.from(txt)); + } + break; + } + case 'MX': + rdLengthBuf[0] = 2; + buffers.push(new Uint16Array([rr.priority])); + // fall through + case 'NS': + case 'CNAME': + case 'PTR': + { + const domain = writeDomainName(rr.exchange || rr.value); + rdLengthBuf[0] += domain.length; + buffers.push(domain); + break; + } + case 'SOA': + { + const mname = writeDomainName(rr.nsname); + const rname = writeDomainName(rr.hostmaster); + rdLengthBuf[0] = mname.length + rname.length + 20; + buffers.push(mname, rname); + buffers.push(new Uint32Array([ + rr.serial, rr.refresh, rr.retry, rr.expire, rr.minttl, + ])); + break; + } + case 'CAA': + { + rdLengthBuf[0] = 5 + rr.issue.length + 2; + buffers.push(Buffer.from([Number(rr.critical)])); + buffers.push(Buffer.from([Number(5)])); + buffers.push(Buffer.from('issue' + rr.issue)); + break; + } + default: + throw new Error(`Unknown RR type ${rr.type}`); + } + } + + return Buffer.concat(buffers.map((typedArray) => { + const buf = Buffer.from(typedArray.buffer, + typedArray.byteOffset, + typedArray.byteLength); + if (os.endianness() === 'LE') { + if (typedArray.BYTES_PER_ELEMENT === 2) buf.swap16(); + if (typedArray.BYTES_PER_ELEMENT === 4) buf.swap32(); + } + return buf; + })); +} + +const mockedErrorCode = 'ENOTFOUND'; +const mockedSysCall = 'getaddrinfo'; + +function errorLookupMock(code = mockedErrorCode, syscall = mockedSysCall) { + return function lookupWithError(hostname, dnsopts, cb) { + const err = new Error(`${syscall} ${code} ${hostname}`); + err.code = code; + err.errno = code; + err.syscall = syscall; + err.hostname = hostname; + cb(err); + }; +} + +function createMockedLookup(...addresses) { + addresses = addresses.map((address) => ({ address: address, family: isIP(address) })); + + // Create a DNS server which replies with a AAAA and a A record for the same host + return function lookup(hostname, options, cb) { + if (options.all === true) { + process.nextTick(() => { + cb(null, addresses); + }); + + return; + } + + process.nextTick(() => { + cb(null, addresses[0].address, addresses[0].family); + }); + }; +} + +module.exports = { + types, + classes, + writeDNSPacket, + parseDNSPacket, + errorLookupMock, + mockedErrorCode, + mockedSysCall, + createMockedLookup, +}; diff --git a/test/napi/node-napi-tests/test/common/fixtures.js b/test/napi/node-napi-tests/test/common/fixtures.js new file mode 100644 index 0000000000..75815b035b --- /dev/null +++ b/test/napi/node-napi-tests/test/common/fixtures.js @@ -0,0 +1,59 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const { pathToFileURL } = require('url'); + +const fixturesDir = path.join(__dirname, '..', 'fixtures'); + +function fixturesPath(...args) { + return path.join(fixturesDir, ...args); +} + +function fixturesFileURL(...args) { + return pathToFileURL(fixturesPath(...args)); +} + +function readFixtureSync(args, enc) { + if (Array.isArray(args)) + return fs.readFileSync(fixturesPath(...args), enc); + return fs.readFileSync(fixturesPath(args), enc); +} + +function readFixtureKey(name, enc) { + return fs.readFileSync(fixturesPath('keys', name), enc); +} + +function readFixtureKeys(enc, ...names) { + return names.map((name) => readFixtureKey(name, enc)); +} + +// This should be in sync with test/fixtures/utf8_test_text.txt. +// We copy them here as a string because this is supposed to be used +// in fs API tests. +const utf8TestText = '永和九年,嵗在癸丑,暮春之初,會於會稽山隂之蘭亭,脩稧事也。' + + '羣賢畢至,少長咸集。此地有崇山峻領,茂林脩竹;又有清流激湍,' + + '暎帶左右。引以為流觴曲水,列坐其次。雖無絲竹管弦之盛,一觴一詠,' + + '亦足以暢敘幽情。是日也,天朗氣清,恵風和暢;仰觀宇宙之大,' + + '俯察品類之盛;所以遊目騁懐,足以極視聽之娛,信可樂也。夫人之相與,' + + '俯仰一世,或取諸懐抱,悟言一室之內,或因寄所託,放浪形骸之外。' + + '雖趣舎萬殊,靜躁不同,當其欣扵所遇,暫得扵己,怏然自足,' + + '不知老之將至。及其所之既惓,情隨事遷,感慨係之矣。向之所欣,' + + '俛仰之閒以為陳跡,猶不能不以之興懐;況脩短隨化,終期扵盡。' + + '古人云:「死生亦大矣。」豈不痛哉!每攬昔人興感之由,若合一契,' + + '未嘗不臨文嗟悼,不能喻之扵懐。固知一死生為虛誕,齊彭殤為妄作。' + + '後之視今,亦由今之視昔,悲夫!故列敘時人,錄其所述,雖世殊事異,' + + '所以興懐,其致一也。後之攬者,亦將有感扵斯文。'; + +module.exports = { + fixturesDir, + path: fixturesPath, + fileURL: fixturesFileURL, + readSync: readFixtureSync, + readKey: readFixtureKey, + readKeys: readFixtureKeys, + utf8TestText, + get utf8TestTextPath() { + return fixturesPath('utf8_test_text.txt'); + }, +}; diff --git a/test/napi/node-napi-tests/test/common/fixtures.mjs b/test/napi/node-napi-tests/test/common/fixtures.mjs new file mode 100644 index 0000000000..81cd2a3199 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/fixtures.mjs @@ -0,0 +1,5 @@ +import fixtures from "./fixtures.js"; + +const { fixturesDir, path, fileURL, readSync, readKey } = fixtures; + +export { fileURL, fixturesDir, path, readKey, readSync }; diff --git a/test/napi/node-napi-tests/test/common/gc.js b/test/napi/node-napi-tests/test/common/gc.js new file mode 100644 index 0000000000..82cc4c79ed --- /dev/null +++ b/test/napi/node-napi-tests/test/common/gc.js @@ -0,0 +1,192 @@ +'use strict'; + +const wait = require('timers/promises').setTimeout; +const assert = require('assert'); +const common = require('../common'); +const gcTrackerMap = new WeakMap(); +const gcTrackerTag = 'NODE_TEST_COMMON_GC_TRACKER'; + +/** + * Installs a garbage collection listener for the specified object. + * Uses async_hooks for GC tracking, which may affect test functionality. + * A full setImmediate() invocation passes between a global.gc() call and the listener being invoked. + * @param {object} obj - The target object to track for garbage collection. + * @param {object} gcListener - The listener object containing the ongc callback. + * @param {Function} gcListener.ongc - The function to call when the target object is garbage collected. + */ +function onGC(obj, gcListener) { + const async_hooks = require('async_hooks'); + + const onGcAsyncHook = async_hooks.createHook({ + init: common.mustCallAtLeast(function(id, type) { + if (this.trackedId === undefined) { + assert.strictEqual(type, gcTrackerTag); + this.trackedId = id; + } + }), + destroy(id) { + assert.notStrictEqual(this.trackedId, -1); + if (id === this.trackedId) { + this.gcListener.ongc(); + onGcAsyncHook.disable(); + } + }, + }).enable(); + onGcAsyncHook.gcListener = gcListener; + + gcTrackerMap.set(obj, new async_hooks.AsyncResource(gcTrackerTag)); + obj = null; +} + +/** + * Repeatedly triggers garbage collection until a specified condition is met or a maximum number of attempts is reached. + * @param {string|Function} [name] - Optional name, used in the rejection message if the condition is not met. + * @param {Function} condition - A function that returns true when the desired condition is met. + * @returns {Promise} A promise that resolves when the condition is met, or rejects after 10 failed attempts. + */ +function gcUntil(name, condition) { + if (typeof name === 'function') { + condition = name; + name = undefined; + } + return new Promise((resolve, reject) => { + let count = 0; + function gcAndCheck() { + setImmediate(() => { + count++; + global.gc(); + if (condition()) { + resolve(); + } else if (count < 10) { + gcAndCheck(); + } else { + reject(name === undefined ? undefined : 'Test ' + name + ' failed'); + } + }); + } + gcAndCheck(); + }); +} + +// This function can be used to check if an object factor leaks or not, +// but it needs to be used with care: +// 1. The test should be set up with an ideally small +// --max-old-space-size or --max-heap-size, which combined with +// the maxCount parameter can reproduce a leak of the objects +// created by fn(). +// 2. This works under the assumption that if *none* of the objects +// created by fn() can be garbage-collected, the test would crash due +// to OOM. +// 3. If *any* of the objects created by fn() can be garbage-collected, +// it is considered leak-free. The FinalizationRegistry is used to +// terminate the test early once we detect any of the object is +// garbage-collected to make the test less prone to false positives. +// This may be especially important for memory management relying on +// emphemeron GC which can be inefficient to deal with extremely fast +// heap growth. +// Note that this can still produce false positives. When the test using +// this function still crashes due to OOM, inspect the heap to confirm +// if a leak is present (e.g. using heap snapshots). +// The generateSnapshotAt parameter can be used to specify a count +// interval to create the heap snapshot which may enforce a more thorough GC. +// This can be tried for code paths that require it for the GC to catch up +// with heap growth. However this type of forced GC can be in conflict with +// other logic in V8 such as bytecode aging, and it can slow down the test +// significantly, so it should be used scarcely and only as a last resort. +async function checkIfCollectable( + fn, maxCount = 4096, generateSnapshotAt = Infinity, logEvery = 128) { + let anyFinalized = false; + let count = 0; + + const f = new FinalizationRegistry(() => { + anyFinalized = true; + }); + + async function createObject() { + const obj = await fn(); + f.register(obj); + if (count++ < maxCount && !anyFinalized) { + setImmediate(createObject, 1); + } + // This can force a more thorough GC, but can slow the test down + // significantly in a big heap. Use it with care. + if (count % generateSnapshotAt === 0) { + // XXX(joyeecheung): This itself can consume a bit of JS heap memory, + // but the other alternative writeHeapSnapshot can run into disk space + // not enough problems in the CI & be slower depending on file system. + // Just do this for now as long as it works and only invent some + // internal voodoo when we absolutely have no other choice. + require('v8').getHeapSnapshot().pause().read(); + console.log(`Generated heap snapshot at ${count}`); + } + if (count % logEvery === 0) { + console.log(`Created ${count} objects`); + } + if (anyFinalized) { + console.log(`Found finalized object at ${count}, stop testing`); + } + } + + createObject(); +} + +// Repeat an operation and give GC some breathing room at every iteration. +async function runAndBreathe(fn, repeat, waitTime = 20) { + for (let i = 0; i < repeat; i++) { + await fn(); + await wait(waitTime); + } +} + +/** + * This requires --expose-internals. + * This function can be used to check if an object factory leaks or not by + * iterating over the heap and count objects with the specified class + * (which is checked by looking up the prototype chain). + * @param {(i: number) => number} fn The factory receiving iteration count + * and returning number of objects created. The return value should be + * precise otherwise false negatives can be produced. + * @param {Function} ctor The constructor of the objects being counted. + * @param {number} count Number of iterations that this check should be done + * @param {number} waitTime Optional breathing time for GC. + */ +async function checkIfCollectableByCounting(fn, ctor, count, waitTime = 20) { + const { queryObjects } = require('v8'); + const { name } = ctor; + const initialCount = queryObjects(ctor, { format: 'count' }); + console.log(`Initial count of ${name}: ${initialCount}`); + let totalCreated = 0; + for (let i = 0; i < count; ++i) { + const created = await fn(i); + totalCreated += created; + console.log(`#${i}: created ${created} ${name}, total ${totalCreated}`); + await wait(waitTime); // give GC some breathing room. + const currentCount = queryObjects(ctor, { format: 'count' }); + const collected = totalCreated - (currentCount - initialCount); + console.log(`#${i}: counted ${currentCount} ${name}, collected ${collected}`); + if (collected > 0) { + console.log(`Detected ${collected} collected ${name}, finish early`); + return; + } + } + + await wait(waitTime); // give GC some breathing room. + const currentCount = queryObjects(ctor, { format: 'count' }); + const collected = totalCreated - (currentCount - initialCount); + console.log(`Last count: counted ${currentCount} ${name}, collected ${collected}`); + // Some objects with the prototype can be collected. + if (collected > 0) { + console.log(`Detected ${collected} collected ${name}`); + return; + } + + throw new Error(`${name} cannot be collected`); +} + +module.exports = { + checkIfCollectable, + runAndBreathe, + checkIfCollectableByCounting, + onGC, + gcUntil, +}; diff --git a/test/napi/node-napi-tests/test/common/globals.js b/test/napi/node-napi-tests/test/common/globals.js new file mode 100644 index 0000000000..42caece2b8 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/globals.js @@ -0,0 +1,146 @@ +'use strict'; + +const intrinsics = new Set([ + 'Object', + 'Function', + 'Array', + 'Number', + 'parseFloat', + 'parseInt', + 'Infinity', + 'NaN', + 'undefined', + 'Boolean', + 'String', + 'Symbol', + 'Date', + 'Promise', + 'RegExp', + 'Error', + 'AggregateError', + 'EvalError', + 'RangeError', + 'ReferenceError', + 'SyntaxError', + 'TypeError', + 'URIError', + 'globalThis', + 'JSON', + 'Math', + 'Intl', + 'ArrayBuffer', + 'Uint8Array', + 'Int8Array', + 'Uint16Array', + 'Int16Array', + 'Uint32Array', + 'Int32Array', + 'Float32Array', + 'Float64Array', + 'Uint8ClampedArray', + 'BigUint64Array', + 'BigInt64Array', + 'DataView', + 'Map', + 'BigInt', + 'Set', + 'WeakMap', + 'WeakSet', + 'Proxy', + 'Reflect', + 'ShadowRealm', + 'FinalizationRegistry', + 'WeakRef', + 'decodeURI', + 'decodeURIComponent', + 'encodeURI', + 'encodeURIComponent', + 'escape', + 'unescape', + 'eval', + 'isFinite', + 'isNaN', + 'SharedArrayBuffer', + 'Atomics', + 'WebAssembly', + 'Iterator', +]); + +if (global.gc) { + intrinsics.add('gc'); +} + +// v8 exposes console in the global scope. +intrinsics.add('console'); + +const webIdlExposedWildcard = new Set([ + 'DOMException', + 'TextEncoder', + 'TextDecoder', + 'AbortController', + 'AbortSignal', + 'CustomEvent', + 'EventTarget', + 'Event', + 'URL', + 'URLSearchParams', + 'ReadableStream', + 'ReadableStreamDefaultReader', + 'ReadableStreamBYOBReader', + 'ReadableStreamBYOBRequest', + 'ReadableByteStreamController', + 'ReadableStreamDefaultController', + 'TransformStream', + 'TransformStreamDefaultController', + 'WritableStream', + 'WritableStreamDefaultWriter', + 'WritableStreamDefaultController', + 'ByteLengthQueuingStrategy', + 'CountQueuingStrategy', + 'TextEncoderStream', + 'TextDecoderStream', + 'CompressionStream', + 'DecompressionStream', +]); + +const webIdlExposedWindow = new Set([ + 'console', + 'BroadcastChannel', + 'queueMicrotask', + 'structuredClone', + 'MessageChannel', + 'MessagePort', + 'MessageEvent', + 'clearInterval', + 'clearTimeout', + 'setInterval', + 'setTimeout', + 'atob', + 'btoa', + 'Blob', + 'Performance', + 'performance', + 'fetch', + 'FormData', + 'Headers', + 'Request', + 'Response', + 'WebSocket', + 'EventSource', + 'CloseEvent', +]); + +const nodeGlobals = new Set([ + 'process', + 'global', + 'Buffer', + 'clearImmediate', + 'setImmediate', +]); + +module.exports = { + intrinsics, + webIdlExposedWildcard, + webIdlExposedWindow, + nodeGlobals, +}; diff --git a/test/napi/node-napi-tests/test/common/heap.js b/test/napi/node-napi-tests/test/common/heap.js new file mode 100644 index 0000000000..bec3b3208c --- /dev/null +++ b/test/napi/node-napi-tests/test/common/heap.js @@ -0,0 +1,329 @@ +'use strict'; +const assert = require('assert'); +const util = require('util'); + +let _buildEmbedderGraph; +function buildEmbedderGraph() { + if (_buildEmbedderGraph) { return _buildEmbedderGraph(); } + let internalBinding; + try { + internalBinding = require('internal/test/binding').internalBinding; + } catch (e) { + console.error('The test must be run with `--expose-internals`'); + throw e; + } + + ({ buildEmbedderGraph: _buildEmbedderGraph } = internalBinding('heap_utils')); + return _buildEmbedderGraph(); +} + +const { getHeapSnapshot } = require('v8'); + +function createJSHeapSnapshot(stream = getHeapSnapshot()) { + stream.pause(); + const dump = JSON.parse(stream.read()); + const meta = dump.snapshot.meta; + + const nodes = + readHeapInfo(dump.nodes, meta.node_fields, meta.node_types, dump.strings); + const edges = + readHeapInfo(dump.edges, meta.edge_fields, meta.edge_types, dump.strings); + + for (const node of nodes) { + node.incomingEdges = []; + node.outgoingEdges = []; + } + + let fromNodeIndex = 0; + let edgeIndex = 0; + for (const { type, name_or_index, to_node } of edges) { + while (edgeIndex === nodes[fromNodeIndex].edge_count) { + edgeIndex = 0; + fromNodeIndex++; + } + const toNode = nodes[to_node / meta.node_fields.length]; + const fromNode = nodes[fromNodeIndex]; + const edge = { + type, + to: toNode, + from: fromNode, + name: typeof name_or_index === 'string' ? name_or_index : null, + }; + toNode.incomingEdges.push(edge); + fromNode.outgoingEdges.push(edge); + edgeIndex++; + } + + for (const node of nodes) { + assert.strictEqual(node.edge_count, node.outgoingEdges.length, + `${node.edge_count} !== ${node.outgoingEdges.length}`); + } + return nodes; +} + +function readHeapInfo(raw, fields, types, strings) { + const items = []; + + for (let i = 0; i < raw.length; i += fields.length) { + const item = {}; + for (let j = 0; j < fields.length; j++) { + const name = fields[j]; + let type = types[j]; + if (Array.isArray(type)) { + item[name] = type[raw[i + j]]; + } else if (name === 'name_or_index') { // type === 'string_or_number' + if (item.type === 'element' || item.type === 'hidden') + type = 'number'; + else + type = 'string'; + } + + if (type === 'string') { + item[name] = strings[raw[i + j]]; + } else if (type === 'number' || type === 'node') { + item[name] = raw[i + j]; + } + } + items.push(item); + } + + return items; +} + +function inspectNode(snapshot) { + return util.inspect(snapshot, { depth: 4 }); +} + +function isEdge(edge, { node_name, edge_name }) { + if (edge_name !== undefined && edge.name !== edge_name) { + return false; + } + // From our internal embedded graph + if (edge.to.value) { + if (edge.to.value.constructor.name !== node_name) { + return false; + } + } else if (edge.to.name !== node_name) { + return false; + } + return true; +} + +class State { + constructor(stream) { + this.snapshot = createJSHeapSnapshot(stream); + this.embedderGraph = buildEmbedderGraph(); + } + + // Validate the v8 heap snapshot + validateSnapshot(rootName, expected, { loose = false } = {}) { + const rootNodes = this.snapshot.filter( + (node) => node.name === rootName && node.type !== 'string'); + if (loose) { + assert(rootNodes.length >= expected.length, + `Expect to find at least ${expected.length} '${rootName}', ` + + `found ${rootNodes.length}`); + } else { + assert.strictEqual( + rootNodes.length, expected.length, + `Expect to find ${expected.length} '${rootName}', ` + + `found ${rootNodes.length}`); + } + + for (const expectation of expected) { + if (expectation.children) { + for (const expectedEdge of expectation.children) { + const check = typeof expectedEdge === 'function' ? expectedEdge : + (edge) => (isEdge(edge, expectedEdge)); + const hasChild = rootNodes.some( + (node) => node.outgoingEdges.some(check), + ); + // Don't use assert with a custom message here. Otherwise the + // inspection in the message is done eagerly and wastes a lot of CPU + // time. + if (!hasChild) { + throw new Error( + 'expected to find child ' + + `${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`); + } + } + } + + if (expectation.detachedness !== undefined) { + const matchedNodes = rootNodes.filter( + (node) => node.detachedness === expectation.detachedness); + if (loose) { + assert(matchedNodes.length >= rootNodes.length, + `Expect to find at least ${rootNodes.length} with ` + + `detachedness ${expectation.detachedness}, ` + + `found ${matchedNodes.length}`); + } else { + assert.strictEqual( + matchedNodes.length, rootNodes.length, + `Expect to find ${rootNodes.length} with detachedness ` + + `${expectation.detachedness}, found ${matchedNodes.length}`); + } + } + } + } + + // Validate our internal embedded graph representation + validateGraph(rootName, expected, { loose = false } = {}) { + const rootNodes = this.embedderGraph.filter( + (node) => node.name === rootName, + ); + if (loose) { + assert(rootNodes.length >= expected.length, + `Expect to find at least ${expected.length} '${rootName}', ` + + `found ${rootNodes.length}`); + } else { + assert.strictEqual( + rootNodes.length, expected.length, + `Expect to find ${expected.length} '${rootName}', ` + + `found ${rootNodes.length}`); + } + for (const expectation of expected) { + if (expectation.children) { + for (const expectedEdge of expectation.children) { + const check = typeof expectedEdge === 'function' ? expectedEdge : + (edge) => (isEdge(edge, expectedEdge)); + // Don't use assert with a custom message here. Otherwise the + // inspection in the message is done eagerly and wastes a lot of CPU + // time. + const hasChild = rootNodes.some( + (node) => node.edges.some(check), + ); + if (!hasChild) { + throw new Error( + 'expected to find child ' + + `${util.inspect(expectedEdge)} in ${inspectNode(rootNodes)}`); + } + } + } + } + } + + validateSnapshotNodes(rootName, expected, { loose = false } = {}) { + this.validateSnapshot(rootName, expected, { loose }); + this.validateGraph(rootName, expected, { loose }); + } +} + +function recordState(stream = undefined) { + return new State(stream); +} + +function validateSnapshotNodes(...args) { + return recordState().validateSnapshotNodes(...args); +} + +/** + * A alternative heap snapshot validator that can be used to verify cppgc-managed nodes. + * Modified from + * https://chromium.googlesource.com/v8/v8/+/b00e995fb212737802810384ba2b868d0d92f7e5/test/unittests/heap/cppgc-js/unified-heap-snapshot-unittest.cc#134 + * @param {string} rootName Name of the root node. Typically a class name used to filter all native nodes with + * this name. For cppgc-managed objects, this is typically the name configured by + * SET_CPPGC_NAME() prefixed with an additional "Node /" prefix e.g. + * "Node / ContextifyScript" + * @param {[{ + * node_name?: string, + * edge_name?: string, + * node_type?: string, + * edge_type?: string, + * }]} retainingPath The retaining path specification to search from the root nodes. + * @returns {[object]} All the leaf nodes matching the retaining path specification. If none can be found, + * logs the nodes found in the last matching step of the path (if any), and throws an + * assertion error. + */ +function findByRetainingPath(rootName, retainingPath) { + const nodes = createJSHeapSnapshot(); + let haystack = nodes.filter((n) => n.name === rootName && n.type !== 'string'); + + for (let i = 0; i < retainingPath.length; ++i) { + const expected = retainingPath[i]; + const newHaystack = []; + + for (const parent of haystack) { + for (let j = 0; j < parent.outgoingEdges.length; j++) { + const edge = parent.outgoingEdges[j]; + // The strings are represented as { type: 'string', name: '' } in the snapshot. + // Ignore them or we'll poke into strings that are just referenced as names of real nodes, + // unless the caller is specifically looking for string nodes via `node_type`. + let match = (edge.to.type !== 'string'); + if (expected.node_type) { + match = (edge.to.type === expected.node_type); + } + if (expected.node_name && edge.to.name !== expected.node_name) { + match = false; + } + if (expected.edge_name && edge.name !== expected.edge_name) { + match = false; + } + if (expected.edge_type && edge.type !== expected.type) { + match = false; + } + if (match) { + newHaystack.push(edge.to); + } + } + } + + if (newHaystack.length === 0) { + const format = (val) => util.inspect(val, { breakLength: 128, depth: 3 }); + console.error('#'); + console.error('# Retaining path to search for:'); + for (let j = 0; j < retainingPath.length; ++j) { + console.error(`# - '${format(retainingPath[j])}'${i === j ? '\t<--- not found' : ''}`); + } + console.error('#\n'); + console.error('# Nodes found in the last step include:'); + for (let j = 0; j < haystack.length; ++j) { + console.error(`# - '${format(haystack[j])}`); + } + + assert.fail(`Could not find target edge ${format(expected)} in the heap snapshot.`); + } + + haystack = newHaystack; + } + + return haystack; +} + +function getHeapSnapshotOptionTests() { + const fixtures = require('../common/fixtures'); + const cases = [ + { + options: { exposeInternals: true }, + expected: [{ + children: [ + // We don't have anything special to test here yet + // because we don't use cppgc or embedder heap tracer. + { edge_name: 'nonNumeric', node_name: 'test' }, + ], + }], + }, + { + options: { exposeNumericValues: true }, + expected: [{ + children: [ + { edge_name: 'numeric', node_name: 'smi number' }, + ], + }], + }, + ]; + return { + fixtures: fixtures.path('klass-with-fields.js'), + check(snapshot, expected) { + snapshot.validateSnapshot('Klass', expected, { loose: true }); + }, + cases, + }; +} + +module.exports = { + recordState, + validateSnapshotNodes, + findByRetainingPath, + getHeapSnapshotOptionTests, +}; diff --git a/test/napi/node-napi-tests/test/common/hijackstdio.js b/test/napi/node-napi-tests/test/common/hijackstdio.js new file mode 100644 index 0000000000..749d6aab48 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/hijackstdio.js @@ -0,0 +1,32 @@ +'use strict'; + +// Hijack stdout and stderr +const stdWrite = {}; +function hijackStdWritable(name, listener) { + const stream = process[name]; + const _write = stdWrite[name] = stream.write; + + stream.writeTimes = 0; + stream.write = function(data, callback) { + try { + listener(data); + } catch (e) { + process.nextTick(() => { throw e; }); + } + + _write.call(stream, data, callback); + stream.writeTimes++; + }; +} + +function restoreWritable(name) { + process[name].write = stdWrite[name]; + delete process[name].writeTimes; +} + +module.exports = { + hijackStdout: hijackStdWritable.bind(null, 'stdout'), + hijackStderr: hijackStdWritable.bind(null, 'stderr'), + restoreStdout: restoreWritable.bind(null, 'stdout'), + restoreStderr: restoreWritable.bind(null, 'stderr'), +}; diff --git a/test/napi/node-napi-tests/test/common/http2.js b/test/napi/node-napi-tests/test/common/http2.js new file mode 100644 index 0000000000..1968b5bcfd --- /dev/null +++ b/test/napi/node-napi-tests/test/common/http2.js @@ -0,0 +1,129 @@ +'use strict'; + +// An HTTP/2 testing tool used to create mock frames for direct testing +// of HTTP/2 endpoints. + +const kFrameData = Symbol('frame-data'); +const FLAG_EOS = 0x1; +const FLAG_ACK = 0x1; +const FLAG_EOH = 0x4; +const FLAG_PADDED = 0x8; +const PADDING = Buffer.alloc(255); + +const kClientMagic = Buffer.from('505249202a20485454502f322' + + 'e300d0a0d0a534d0d0a0d0a', 'hex'); + +const kFakeRequestHeaders = Buffer.from('828684410f7777772e65' + + '78616d706c652e636f6d', 'hex'); + + +const kFakeResponseHeaders = Buffer.from('4803333032580770726976617465611d' + + '4d6f6e2c203231204f63742032303133' + + '2032303a31333a323120474d546e1768' + + '747470733a2f2f7777772e6578616d70' + + '6c652e636f6d', 'hex'); + +function isUint32(val) { + return val >>> 0 === val; +} + +function isUint24(val) { + return val >>> 0 === val && val <= 0xFFFFFF; +} + +function isUint8(val) { + return val >>> 0 === val && val <= 0xFF; +} + +function write32BE(array, pos, val) { + if (!isUint32(val)) + throw new RangeError('val is not a 32-bit number'); + array[pos++] = (val >> 24) & 0xff; + array[pos++] = (val >> 16) & 0xff; + array[pos++] = (val >> 8) & 0xff; + array[pos++] = val & 0xff; +} + +function write24BE(array, pos, val) { + if (!isUint24(val)) + throw new RangeError('val is not a 24-bit number'); + array[pos++] = (val >> 16) & 0xff; + array[pos++] = (val >> 8) & 0xff; + array[pos++] = val & 0xff; +} + +function write8(array, pos, val) { + if (!isUint8(val)) + throw new RangeError('val is not an 8-bit number'); + array[pos] = val; +} + +class Frame { + constructor(length, type, flags, id) { + this[kFrameData] = Buffer.alloc(9); + write24BE(this[kFrameData], 0, length); + write8(this[kFrameData], 3, type); + write8(this[kFrameData], 4, flags); + write32BE(this[kFrameData], 5, id); + } + + get data() { + return this[kFrameData]; + } +} + +class SettingsFrame extends Frame { + constructor(ack = false) { + let flags = 0; + if (ack) + flags |= FLAG_ACK; + super(0, 4, flags, 0); + } +} + +class HeadersFrame extends Frame { + constructor(id, payload, padlen = 0, final = false) { + let len = payload.length; + let flags = FLAG_EOH; + if (final) flags |= FLAG_EOS; + const buffers = [payload]; + if (padlen > 0) { + buffers.unshift(Buffer.from([padlen])); + buffers.push(PADDING.slice(0, padlen)); + len += padlen + 1; + flags |= FLAG_PADDED; + } + super(len, 1, flags, id); + buffers.unshift(this[kFrameData]); + this[kFrameData] = Buffer.concat(buffers); + } +} + +class PingFrame extends Frame { + constructor(ack = false) { + const buffers = [Buffer.alloc(8)]; + super(8, 6, ack ? 1 : 0, 0); + buffers.unshift(this[kFrameData]); + this[kFrameData] = Buffer.concat(buffers); + } +} + +class AltSvcFrame extends Frame { + constructor(size) { + const buffers = [Buffer.alloc(size)]; + super(size, 10, 0, 0); + buffers.unshift(this[kFrameData]); + this[kFrameData] = Buffer.concat(buffers); + } +} + +module.exports = { + Frame, + AltSvcFrame, + HeadersFrame, + SettingsFrame, + PingFrame, + kFakeRequestHeaders, + kFakeResponseHeaders, + kClientMagic, +}; diff --git a/test/napi/node-napi-tests/test/common/index.js b/test/napi/node-napi-tests/test/common/index.js new file mode 100644 index 0000000000..d3b58cd84b --- /dev/null +++ b/test/napi/node-napi-tests/test/common/index.js @@ -0,0 +1,1210 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +/* eslint-disable node-core/crypto-check */ +'use strict'; +const process = global.process; // Some tests tamper with the process global. + +const assert = require('assert'); +const { exec, execSync, spawn, spawnSync } = require('child_process'); +const fs = require('fs'); +const net = require('net'); +// Do not require 'os' until needed so that test-os-checked-function can +// monkey patch it. If 'os' is required here, that test will fail. +const path = require('path'); +const { inspect, getCallSites } = require('util'); +const { isMainThread } = require('worker_threads'); +const { isModuleNamespaceObject } = require('util/types'); + +const tmpdir = require('./tmpdir'); +const bits = ['arm64', 'loong64', 'mips', 'mipsel', 'ppc64', 'riscv64', 's390x', 'x64'] + .includes(process.arch) ? 64 : 32; +const hasIntl = !!process.config.variables.v8_enable_i18n_support; + +const { + atob, + btoa, +} = require('buffer'); + +// Some tests assume a umask of 0o022 so set that up front. Tests that need a +// different umask will set it themselves. +// +// Workers can read, but not set the umask, so check that this is the main +// thread. +if (isMainThread) + process.umask(0o022); + +const noop = () => {}; + +const hasCrypto = Boolean(process.versions.openssl) && + !process.env.NODE_SKIP_CRYPTO; + +// Synthesize OPENSSL_VERSION_NUMBER format with the layout 0xMNN00PPSL +const opensslVersionNumber = (major = 0, minor = 0, patch = 0) => { + assert(major >= 0 && major <= 0xf); + assert(minor >= 0 && minor <= 0xff); + assert(patch >= 0 && patch <= 0xff); + return (major << 28) | (minor << 20) | (patch << 4); +}; + +let OPENSSL_VERSION_NUMBER; +const hasOpenSSL = (major = 0, minor = 0, patch = 0) => { + if (!hasCrypto) return false; + if (OPENSSL_VERSION_NUMBER === undefined) { + const regexp = /(?\d+)\.(?\d+)\.(?

\d+)/; + const { m, n, p } = process.versions.openssl.match(regexp).groups; + OPENSSL_VERSION_NUMBER = opensslVersionNumber(m, n, p); + } + return OPENSSL_VERSION_NUMBER >= opensslVersionNumber(major, minor, patch); +}; + +const hasQuic = hasCrypto && !!process.config.variables.openssl_quic; + +function parseTestFlags(filename = process.argv[1]) { + // The copyright notice is relatively big and the flags could come afterwards. + const bytesToRead = 1500; + const buffer = Buffer.allocUnsafe(bytesToRead); + const fd = fs.openSync(filename, 'r'); + const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead); + fs.closeSync(fd); + const source = buffer.toString('utf8', 0, bytesRead); + + const flagStart = source.search(/\/\/ Flags:\s+--/) + 10; + + if (flagStart === 9) { + return []; + } + let flagEnd = source.indexOf('\n', flagStart); + // Normalize different EOL. + if (source[flagEnd - 1] === '\r') { + flagEnd--; + } + return source + .substring(flagStart, flagEnd) + .split(/\s+/) + .filter(Boolean); +} + +// Check for flags. Skip this for workers (both, the `cluster` module and +// `worker_threads`) and child processes. +// If the binary was built without-ssl then the crypto flags are +// invalid (bad option). The test itself should handle this case. +if (process.argv.length === 2 && + !process.env.NODE_SKIP_FLAG_CHECK && + isMainThread && + hasCrypto && + require('cluster').isPrimary && + fs.existsSync(process.argv[1])) { + const flags = parseTestFlags(); + for (const flag of flags) { + if (!process.execArgv.includes(flag) && + // If the binary is build without `intl` the inspect option is + // invalid. The test itself should handle this case. + (process.features.inspector || !flag.startsWith('--inspect'))) { + console.log( + 'NOTE: The test started as a child_process using these flags:', + inspect(flags), + 'Use NODE_SKIP_FLAG_CHECK to run the test with the original flags.', + ); + const args = [...flags, ...process.execArgv, ...process.argv.slice(1)]; + const options = { encoding: 'utf8', stdio: 'inherit' }; + const result = spawnSync(process.execPath, args, options); + if (result.signal) { + process.kill(0, result.signal); + } else { + process.exit(result.status); + } + } + } +} + +const isWindows = process.platform === 'win32'; +const isSunOS = process.platform === 'sunos'; +const isFreeBSD = process.platform === 'freebsd'; +const isOpenBSD = process.platform === 'openbsd'; +const isLinux = process.platform === 'linux'; +const isMacOS = process.platform === 'darwin'; +const isASan = process.config.variables.asan === 1; +const isRiscv64 = process.arch === 'riscv64'; +const isDebug = process.features.debug; +const isPi = (() => { + try { + // Normal Raspberry Pi detection is to find the `Raspberry Pi` string in + // the contents of `/sys/firmware/devicetree/base/model` but that doesn't + // work inside a container. Match the chipset model number instead. + const cpuinfo = fs.readFileSync('/proc/cpuinfo', { encoding: 'utf8' }); + const ok = /^Hardware\s*:\s*(.*)$/im.exec(cpuinfo)?.[1] === 'BCM2835'; + /^/.test(''); // Clear RegExp.$_, some tests expect it to be empty. + return ok; + } catch { + return false; + } +})(); + +const isDumbTerminal = process.env.TERM === 'dumb'; + +// When using high concurrency or in the CI we need much more time for each connection attempt +net.setDefaultAutoSelectFamilyAttemptTimeout(platformTimeout(net.getDefaultAutoSelectFamilyAttemptTimeout() * 10)); +const defaultAutoSelectFamilyAttemptTimeout = net.getDefaultAutoSelectFamilyAttemptTimeout(); + +// const buildType = process.config.target_defaults?.default_configuration ?? +// 'Debug'; +// Always use Node-API modules built in debug mode so that we get better errors +const buildType = 'Debug'; + +global.gc = () => Bun.gc(true); + +// If env var is set then enable async_hook hooks for all tests. +if (process.env.NODE_TEST_WITH_ASYNC_HOOKS) { + const destroydIdsList = {}; + const destroyListList = {}; + const initHandles = {}; + const { internalBinding } = require('internal/test/binding'); + const async_wrap = internalBinding('async_wrap'); + + process.on('exit', () => { + // Iterate through handles to make sure nothing crashes + for (const k in initHandles) + inspect(initHandles[k]); + }); + + const _queueDestroyAsyncId = async_wrap.queueDestroyAsyncId; + async_wrap.queueDestroyAsyncId = function queueDestroyAsyncId(id) { + if (destroyListList[id] !== undefined) { + process._rawDebug(destroyListList[id]); + process._rawDebug(); + throw new Error(`same id added to destroy list twice (${id})`); + } + destroyListList[id] = inspect(new Error()); + _queueDestroyAsyncId(id); + }; + + require('async_hooks').createHook({ + init(id, ty, tr, resource) { + if (initHandles[id]) { + process._rawDebug( + `Is same resource: ${resource === initHandles[id].resource}`); + process._rawDebug(`Previous stack:\n${initHandles[id].stack}\n`); + throw new Error(`init called twice for same id (${id})`); + } + initHandles[id] = { + resource, + stack: inspect(new Error()).slice(6), + }; + }, + before() { }, + after() { }, + destroy(id) { + if (destroydIdsList[id] !== undefined) { + process._rawDebug(destroydIdsList[id]); + process._rawDebug(); + throw new Error(`destroy called for same id (${id})`); + } + destroydIdsList[id] = inspect(new Error()); + }, + }).enable(); +} + +let opensslCli = null; +let inFreeBSDJail = null; +let localhostIPv4 = null; + +const localIPv6Hosts = + isLinux ? [ + // Debian/Ubuntu + 'ip6-localhost', + 'ip6-loopback', + + // SUSE + 'ipv6-localhost', + 'ipv6-loopback', + + // Typically universal + 'localhost', + ] : [ 'localhost' ]; + +const PIPE = (() => { + const localRelative = path.relative(process.cwd(), `${tmpdir.path}/`); + const pipePrefix = isWindows ? '\\\\.\\pipe\\' : localRelative; + const pipeName = `node-test.${process.pid}.sock`; + return path.join(pipePrefix, pipeName); +})(); + +// Check that when running a test with +// `$node --abort-on-uncaught-exception $file child` +// the process aborts. +function childShouldThrowAndAbort() { + const escapedArgs = escapePOSIXShell`"${process.argv[0]}" --abort-on-uncaught-exception "${process.argv[1]}" child`; + if (!isWindows) { + // Do not create core files, as it can take a lot of disk space on + // continuous testing and developers' machines + escapedArgs[0] = 'ulimit -c 0 && ' + escapedArgs[0]; + } + const child = exec(...escapedArgs); + child.on('exit', function onExit(exitCode, signal) { + const errMsg = 'Test should have aborted ' + + `but instead exited with exit code ${exitCode}` + + ` and signal ${signal}`; + assert(nodeProcessAborted(exitCode, signal), errMsg); + }); +} + +function createZeroFilledFile(filename) { + const fd = fs.openSync(filename, 'w'); + fs.ftruncateSync(fd, 10 * 1024 * 1024); + fs.closeSync(fd); +} + + +const pwdCommand = isWindows ? + ['cmd.exe', ['/d', '/c', 'cd']] : + ['pwd', []]; + + +function platformTimeout(ms) { + const multipliers = typeof ms === 'bigint' ? + { two: 2n, four: 4n, seven: 7n } : { two: 2, four: 4, seven: 7 }; + + if (isDebug) + ms = multipliers.two * ms; + + if (exports.isAIX || exports.isIBMi) + return multipliers.two * ms; // Default localhost speed is slower on AIX + + if (isPi) + return multipliers.two * ms; // Raspberry Pi devices + + if (isRiscv64) { + return multipliers.four * ms; + } + + return ms; +} + +let knownGlobals = [ + AbortController, + atob, + btoa, + clearImmediate, + clearInterval, + clearTimeout, + global, + setImmediate, + setInterval, + setTimeout, + queueMicrotask, + addEventListener, + alert, + confirm, + dispatchEvent, + postMessage, + prompt, + removeEventListener, + reportError, + Bun, + File, + process, + Blob, + Buffer, + BuildError, + BuildMessage, + HTMLRewriter, + Request, + ResolveError, + ResolveMessage, + Response, + TextDecoder, + AbortSignal, + BroadcastChannel, + CloseEvent, + DOMException, + ErrorEvent, + Event, + EventTarget, + FormData, + Headers, + MessageChannel, + MessageEvent, + MessagePort, + PerformanceEntry, + PerformanceObserver, + PerformanceObserverEntryList, + TextEncoder, + URL, + URLSearchParams, + WebSocket, + Worker, + onmessage, + onerror, +]; + +const globalKeys = [ + "gc", + "navigator", + "Navigator", + "Performance", + "performance", + "PerformanceMark", + "PerformanceMeasure", + "PerformanceResourceTiming", + "PerformanceServerTiming", + "PerformanceTiming", +]; + +for (const key of globalKeys) { + if (global[key]) { + knownGlobals.push(global[key]); + } +} + +// TODO(@ethan-arrowood): Similar to previous checks, this can be temporary +// until v16.x is EOL. Once all supported versions have structuredClone we +// can add this to the list above instead. +if (global.structuredClone) { + knownGlobals.push(global.structuredClone); +} + +if (global.EventSource) { + knownGlobals.push(EventSource); +} + +if (global.fetch) { + knownGlobals.push(fetch); +} +if (hasCrypto && global.crypto) { + knownGlobals.push(global.crypto); + knownGlobals.push(global.Crypto); + knownGlobals.push(global.CryptoKey); + knownGlobals.push(global.SubtleCrypto); +} +if (global.CustomEvent) { + knownGlobals.push(global.CustomEvent); +} +if (global.ReadableStream) { + knownGlobals.push( + global.ReadableStream, + global.ReadableStreamDefaultReader, + global.ReadableStreamBYOBReader, + global.ReadableStreamBYOBRequest, + global.ReadableByteStreamController, + global.ReadableStreamDefaultController, + global.TransformStream, + global.TransformStreamDefaultController, + global.WritableStream, + global.WritableStreamDefaultWriter, + global.WritableStreamDefaultController, + global.ByteLengthQueuingStrategy, + global.CountQueuingStrategy, + global.TextEncoderStream, + global.TextDecoderStream, + global.CompressionStream, + global.DecompressionStream, + ); +} + +if (global.Storage) { + knownGlobals.push( + global.localStorage, + global.sessionStorage, + global.Storage, + ); +} + +function allowGlobals(...allowlist) { + for (const key of allowlist) { + knownGlobals.push(global[key]); + } +} + +if (process.env.NODE_TEST_KNOWN_GLOBALS !== '0') { + if (process.env.NODE_TEST_KNOWN_GLOBALS) { + const knownFromEnv = process.env.NODE_TEST_KNOWN_GLOBALS.split(','); + allowGlobals(...knownFromEnv); + } + + function leakedGlobals() { + const leaked = []; + + for (const val in global) { + // globalThis.crypto is a getter that throws if Node.js was compiled + // without OpenSSL. + if (val !== 'crypto' && !knownGlobals.includes(global[val])) { + leaked.push(val); + } + } + + return leaked; + } + + process.on('exit', function() { + const leaked = leakedGlobals(); + if (leaked.length > 0) { + assert.fail(`Unexpected global(s) found: ${leaked.join(', ')}`); + } + }); +} + +const mustCallChecks = []; + +function runCallChecks(exitCode) { + if (exitCode !== 0) return; + + const failed = mustCallChecks.filter(function(context) { + if ('minimum' in context) { + context.messageSegment = `at least ${context.minimum}`; + return context.actual < context.minimum; + } + context.messageSegment = `exactly ${context.exact}`; + return context.actual !== context.exact; + }); + + failed.forEach(function(context) { + console.log('Mismatched %s function calls. Expected %s, actual %d.', + context.name, + context.messageSegment, + context.actual); + console.log(context.stack.split('\n').slice(2).join('\n')); + }); + + if (failed.length) process.exit(1); +} + +function mustCall(fn, exact) { + return _mustCallInner(fn, exact, 'exact'); +} + +function mustSucceed(fn, exact) { + return mustCall(function(err, ...args) { + assert.ifError(err); + if (typeof fn === 'function') + return fn.apply(this, args); + }, exact); +} + +function mustCallAtLeast(fn, minimum) { + return _mustCallInner(fn, minimum, 'minimum'); +} + +function _mustCallInner(fn, criteria = 1, field) { + if (process._exiting) + throw new Error('Cannot use common.mustCall*() in process exit handler'); + if (typeof fn === 'number') { + criteria = fn; + fn = noop; + } else if (fn === undefined) { + fn = noop; + } + + if (typeof criteria !== 'number') + throw new TypeError(`Invalid ${field} value: ${criteria}`); + + const context = { + [field]: criteria, + actual: 0, + stack: inspect(new Error()), + name: fn.name || '', + }; + + // Add the exit listener only once to avoid listener leak warnings + if (mustCallChecks.length === 0) process.on('exit', runCallChecks); + + mustCallChecks.push(context); + + const _return = function() { // eslint-disable-line func-style + context.actual++; + return fn.apply(this, arguments); + }; + // Function instances have own properties that may be relevant. + // Let's replicate those properties to the returned function. + // Refs: https://tc39.es/ecma262/#sec-function-instances + Object.defineProperties(_return, { + name: { + value: fn.name, + writable: false, + enumerable: false, + configurable: true, + }, + length: { + value: fn.length, + writable: false, + enumerable: false, + configurable: true, + }, + }); + return _return; +} + +function hasMultiLocalhost() { + const { internalBinding } = require('internal/test/binding'); + const { TCP, constants: TCPConstants } = internalBinding('tcp_wrap'); + const t = new TCP(TCPConstants.SOCKET); + const ret = t.bind('127.0.0.2', 0); + t.close(); + return ret === 0; +} + +function skipIfEslintMissing() { + if (!fs.existsSync( + path.join(__dirname, '..', '..', 'tools', 'eslint', 'node_modules', 'eslint'), + )) { + skip('missing ESLint'); + } +} + +function canCreateSymLink() { + // On Windows, creating symlinks requires admin privileges. + // We'll only try to run symlink test if we have enough privileges. + // On other platforms, creating symlinks shouldn't need admin privileges + if (isWindows) { + // whoami.exe needs to be the one from System32 + // If unix tools are in the path, they can shadow the one we want, + // so use the full path while executing whoami + const whoamiPath = path.join(process.env.SystemRoot, + 'System32', 'whoami.exe'); + + try { + const output = execSync(`${whoamiPath} /priv`, { timeout: 1000 }); + return output.includes('SeCreateSymbolicLinkPrivilege'); + } catch { + return false; + } + } + // On non-Windows platforms, this always returns `true` + return true; +} + +// Remove when Bun adds util.getCallSites +function getCallSite(top) { + const originalStackFormatter = Error.prepareStackTrace; + Error.prepareStackTrace = (err, stack) => + `${stack[0].getFileName()}:${stack[0].getLineNumber()}`; + const err = new Error(); + Error.captureStackTrace(err, top); + // With the V8 Error API, the stack is not formatted until it is accessed + err.stack; // eslint-disable-line no-unused-expressions + Error.prepareStackTrace = originalStackFormatter; + return err.stack; +} + +function mustNotCall(msg) { + const callSite = getCallSite(mustNotCall); + return function mustNotCall(...args) { + const argsInfo = args.length > 0 ? + `\ncalled with arguments: ${args.map((arg) => inspect(arg)).join(', ')}` : ''; + assert.fail( + `${msg || 'function should not have been called'} at ${callSite.scriptName}:${callSite.lineNumber}` + + argsInfo); + }; +} + +const _mustNotMutateObjectDeepProxies = new WeakMap(); + +function mustNotMutateObjectDeep(original) { + // Return primitives and functions directly. Primitives are immutable, and + // proxied functions are impossible to compare against originals, e.g. with + // `assert.deepEqual()`. + if (original === null || typeof original !== 'object') { + return original; + } + + const cachedProxy = _mustNotMutateObjectDeepProxies.get(original); + if (cachedProxy) { + return cachedProxy; + } + + const _mustNotMutateObjectDeepHandler = { + __proto__: null, + defineProperty(target, property, descriptor) { + assert.fail(`Expected no side effects, got ${inspect(property)} ` + + 'defined'); + }, + deleteProperty(target, property) { + assert.fail(`Expected no side effects, got ${inspect(property)} ` + + 'deleted'); + }, + get(target, prop, receiver) { + return mustNotMutateObjectDeep(Reflect.get(target, prop, receiver)); + }, + preventExtensions(target) { + assert.fail('Expected no side effects, got extensions prevented on ' + + inspect(target)); + }, + set(target, property, value, receiver) { + assert.fail(`Expected no side effects, got ${inspect(value)} ` + + `assigned to ${inspect(property)}`); + }, + setPrototypeOf(target, prototype) { + assert.fail(`Expected no side effects, got set prototype to ${prototype}`); + }, + }; + + const proxy = new Proxy(original, _mustNotMutateObjectDeepHandler); + _mustNotMutateObjectDeepProxies.set(original, proxy); + return proxy; +} + +function printSkipMessage(msg) { + console.log(`1..0 # Skipped: ${msg}`); +} + +function skip(msg) { + printSkipMessage(msg); + // In known_issues test, skipping should produce a non-zero exit code. + process.exit(require.main?.filename.startsWith(path.resolve(__dirname, '../known_issues/')) ? 1 : 0); +} + +// Returns true if the exit code "exitCode" and/or signal name "signal" +// represent the exit code and/or signal name of a node process that aborted, +// false otherwise. +function nodeProcessAborted(exitCode, signal) { + // Depending on the compiler used, node will exit with either + // exit code 132 (SIGILL), 133 (SIGTRAP) or 134 (SIGABRT). + let expectedExitCodes = [132, 133, 134]; + + // On platforms using KSH as the default shell (like SmartOS), + // when a process aborts, KSH exits with an exit code that is + // greater than 256, and thus the exit code emitted with the 'exit' + // event is null and the signal is set to either SIGILL, SIGTRAP, + // or SIGABRT (depending on the compiler). + const expectedSignals = ['SIGILL', 'SIGTRAP', 'SIGABRT']; + + // On Windows, 'aborts' are of 2 types, depending on the context: + // (i) Exception breakpoint, if --abort-on-uncaught-exception is on + // which corresponds to exit code 2147483651 (0x80000003) + // (ii) Otherwise, _exit(134) which is called in place of abort() due to + // raising SIGABRT exiting with ambiguous exit code '3' by default + if (isWindows) + expectedExitCodes = [0x80000003, 134]; + + // When using --abort-on-uncaught-exception, V8 will use + // base::OS::Abort to terminate the process. + // Depending on the compiler used, the shell or other aspects of + // the platform used to build the node binary, this will actually + // make V8 exit by aborting or by raising a signal. In any case, + // one of them (exit code or signal) needs to be set to one of + // the expected exit codes or signals. + if (signal !== null) { + return expectedSignals.includes(signal); + } + return expectedExitCodes.includes(exitCode); +} + +function isAlive(pid) { + try { + process.kill(pid, 'SIGCONT'); + return true; + } catch { + return false; + } +} + +function _expectWarning(name, expected, code) { + if (typeof expected === 'string') { + expected = [[expected, code]]; + } else if (!Array.isArray(expected)) { + expected = Object.entries(expected).map(([a, b]) => [b, a]); + } else if (expected.length !== 0 && !Array.isArray(expected[0])) { + expected = [[expected[0], expected[1]]]; + } + // Deprecation codes are mandatory, everything else is not. + if (name === 'DeprecationWarning') { + expected.forEach(([_, code]) => assert(code, `Missing deprecation code: ${expected}`)); + } + return mustCall((warning) => { + const expectedProperties = expected.shift(); + if (!expectedProperties) { + assert.fail(`Unexpected extra warning received: ${warning}`); + } + const [ message, code ] = expectedProperties; + assert.strictEqual(warning.name, name); + if (typeof message === 'string') { + assert.strictEqual(warning.message, message); + } else { + assert.match(warning.message, message); + } + assert.strictEqual(warning.code, code); + }, expected.length); +} + +let catchWarning; + +// Accepts a warning name and description or array of descriptions or a map of +// warning names to description(s) ensures a warning is generated for each +// name/description pair. +// The expected messages have to be unique per `expectWarning()` call. +function expectWarning(nameOrMap, expected, code) { + if (catchWarning === undefined) { + catchWarning = {}; + process.on('warning', (warning) => { + if (!catchWarning[warning.name]) { + throw new TypeError( + `"${warning.name}" was triggered without being expected.\n` + + inspect(warning), + ); + } + catchWarning[warning.name](warning); + }); + } + if (typeof nameOrMap === 'string') { + catchWarning[nameOrMap] = _expectWarning(nameOrMap, expected, code); + } else { + Object.keys(nameOrMap).forEach((name) => { + catchWarning[name] = _expectWarning(name, nameOrMap[name]); + }); + } +} + +// Useful for testing expected internal/error objects +function expectsError(validator, exact) { + return mustCall((...args) => { + if (args.length !== 1) { + // Do not use `assert.strictEqual()` to prevent `inspect` from + // always being called. + assert.fail(`Expected one argument, got ${inspect(args)}`); + } + const error = args.pop(); + // The error message should be non-enumerable + assert.strictEqual(Object.prototype.propertyIsEnumerable.call(error, 'message'), false); + + assert.throws(() => { throw error; }, validator); + return true; + }, exact); +} + +function skipIfInspectorDisabled() { + if (!process.features.inspector) { + skip('V8 inspector is disabled'); + } +} + +function skipIf32Bits() { + if (bits < 64) { + skip('The tested feature is not available in 32bit builds'); + } +} + +function skipIfWorker() { + if (!isMainThread) { + skip('This test only works on a main thread'); + } +} + +function getArrayBufferViews(buf) { + const { buffer, byteOffset, byteLength } = buf; + + const out = []; + + const arrayBufferViews = [ + Int8Array, + Uint8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + Float32Array, + Float64Array, + BigInt64Array, + BigUint64Array, + DataView, + ]; + + for (const type of arrayBufferViews) { + const { BYTES_PER_ELEMENT = 1 } = type; + if (byteLength % BYTES_PER_ELEMENT === 0) { + out.push(new type(buffer, byteOffset, byteLength / BYTES_PER_ELEMENT)); + } + } + return out; +} + +function getBufferSources(buf) { + return [...getArrayBufferViews(buf), new Uint8Array(buf).buffer]; +} + +function getTTYfd() { + // Do our best to grab a tty fd. + const tty = require('tty'); + // Don't attempt fd 0 as it is not writable on Windows. + // Ref: ef2861961c3d9e9ed6972e1e84d969683b25cf95 + const ttyFd = [1, 2, 4, 5].find(tty.isatty); + if (ttyFd === undefined) { + try { + return fs.openSync('/dev/tty'); + } catch { + // There aren't any tty fd's available to use. + return -1; + } + } + return ttyFd; +} + +function runWithInvalidFD(func) { + let fd = 1 << 30; + // Get first known bad file descriptor. 1 << 30 is usually unlikely to + // be an valid one. + try { + while (fs.fstatSync(fd--) && fd > 0); + } catch { + return func(fd); + } + + printSkipMessage('Could not generate an invalid fd'); +} + +// A helper function to simplify checking for ERR_INVALID_ARG_TYPE output. +function invalidArgTypeHelper(input) { + if (input == null) { + return ` Received ${input}`; + } + if (typeof input === 'function') { + return ` Received function ${input.name}`; + } + if (typeof input === 'object') { + if (input.constructor?.name) { + return ` Received an instance of ${input.constructor.name}`; + } + return ` Received ${inspect(input, { depth: -1 })}`; + } + + let inspected = inspect(input, { colors: false }); + if (inspected.length > 28) { inspected = `${inspected.slice(inspected, 0, 25)}...`; } + + return ` Received type ${typeof input} (${inspected})`; +} + +function skipIfDumbTerminal() { + if (isDumbTerminal) { + skip('skipping - dumb terminal'); + } +} + +function requireNoPackageJSONAbove(dir = __dirname) { + let possiblePackage = path.join(dir, '..', 'package.json'); + let lastPackage = null; + while (possiblePackage !== lastPackage) { + if (fs.existsSync(possiblePackage)) { + assert.fail( + 'This test shouldn\'t load properties from a package.json above ' + + `its file location. Found package.json at ${possiblePackage}.`); + } + lastPackage = possiblePackage; + possiblePackage = path.join(possiblePackage, '..', '..', 'package.json'); + } +} + +function spawnPromisified(...args) { + let stderr = ''; + let stdout = ''; + + const child = spawn(...args); + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (data) => { stderr += data; }); + child.stdout.setEncoding('utf8'); + child.stdout.on('data', (data) => { stdout += data; }); + + return new Promise((resolve, reject) => { + child.on('close', (code, signal) => { + resolve({ + code, + signal, + stderr, + stdout, + }); + }); + child.on('error', (code, signal) => { + reject({ + code, + signal, + stderr, + stdout, + }); + }); + }); +} + +/** + * Escape values in a string template literal. On Windows, this function + * does not escape anything (which is fine for paths, as `"` is not a valid char + * in a path on Windows), so you should use it only to escape paths – or other + * values on tests which are skipped on Windows. + * This function is meant to be used for tagged template strings. + * @returns {[string, object | undefined]} An array that can be passed as + * arguments to `exec` or `execSync`. + */ +function escapePOSIXShell(cmdParts, ...args) { + if (common.isWindows) { + // On Windows, paths cannot contain `"`, so we can return the string unchanged. + return [String.raw({ raw: cmdParts }, ...args)]; + } + // On POSIX shells, we can pass values via the env, as there's a standard way for referencing a variable. + const env = { ...process.env }; + let cmd = cmdParts[0]; + for (let i = 0; i < args.length; i++) { + const envVarName = `ESCAPED_${i}`; + env[envVarName] = args[i]; + cmd += '${' + envVarName + '}' + cmdParts[i + 1]; + } + + return [cmd, { env }]; +}; + +function getPrintedStackTrace(stderr) { + const lines = stderr.split('\n'); + + let state = 'initial'; + const result = { + message: [], + nativeStack: [], + jsStack: [], + }; + for (let i = 0; i < lines.length; ++i) { + const line = lines[i].trim(); + if (line.length === 0) { + continue; // Skip empty lines. + } + + switch (state) { + case 'initial': + result.message.push(line); + if (line.includes('Native stack trace')) { + state = 'native-stack'; + } else { + result.message.push(line); + } + break; + case 'native-stack': + if (line.includes('JavaScript stack trace')) { + state = 'js-stack'; + } else { + result.nativeStack.push(line); + } + break; + case 'js-stack': + result.jsStack.push(line); + break; + } + } + return result; +} + +/** + * Check the exports of require(esm). + * TODO(joyeecheung): use it in all the test-require-module-* tests to minimize changes + * if/when we change the layout of the result returned by require(esm). + * @param {object} mod result returned by require() + * @param {object} expectation shape of expected namespace. + */ +function expectRequiredModule(mod, expectation, checkESModule = true) { + const clone = { ...mod }; + if (Object.hasOwn(mod, 'default') && checkESModule) { + assert.strictEqual(mod.__esModule, true); + delete clone.__esModule; + } + assert(isModuleNamespaceObject(mod)); + assert.deepStrictEqual(clone, { ...expectation }); +} + +const common = { + allowGlobals, + buildType, + canCreateSymLink, + childShouldThrowAndAbort, + createZeroFilledFile, + defaultAutoSelectFamilyAttemptTimeout, + escapePOSIXShell, + expectsError, + expectRequiredModule, + expectWarning, + getArrayBufferViews, + getBufferSources, + getPrintedStackTrace, + getTTYfd, + hasIntl, + hasCrypto, + hasOpenSSL, + hasQuic, + hasMultiLocalhost, + invalidArgTypeHelper, + isAlive, + isASan, + isDebug, + isDumbTerminal, + isFreeBSD, + isLinux, + isMainThread, + isOpenBSD, + isMacOS, + isPi, + isSunOS, + isWindows, + localIPv6Hosts, + mustCall, + mustCallAtLeast, + mustNotCall, + mustNotMutateObjectDeep, + mustSucceed, + nodeProcessAborted, + PIPE, + parseTestFlags, + platformTimeout, + printSkipMessage, + pwdCommand, + requireNoPackageJSONAbove, + runWithInvalidFD, + skip, + skipIf32Bits, + skipIfDumbTerminal, + skipIfEslintMissing, + skipIfInspectorDisabled, + skipIfWorker, + spawnPromisified, + + get enoughTestMem() { + return require('os').totalmem() > 0x70000000; /* 1.75 Gb */ + }, + + get hasFipsCrypto() { + return hasCrypto && require('crypto').getFips(); + }, + + get hasIPv6() { + const iFaces = require('os').networkInterfaces(); + let re; + if (isWindows) { + re = /Loopback Pseudo-Interface/; + } else if (this.isIBMi) { + re = /\*LOOPBACK/; + } else { + re = /lo/; + } + return Object.keys(iFaces).some((name) => { + return re.test(name) && + iFaces[name].some(({ family }) => family === 'IPv6'); + }); + }, + + get hasOpenSSL3() { + return hasOpenSSL(3); + }, + + get hasOpenSSL31() { + return hasOpenSSL(3, 1); + }, + + get hasOpenSSL32() { + return hasOpenSSL(3, 2); + }, + + get inFreeBSDJail() { + if (inFreeBSDJail !== null) return inFreeBSDJail; + + if (exports.isFreeBSD && + execSync('sysctl -n security.jail.jailed').toString() === '1\n') { + inFreeBSDJail = true; + } else { + inFreeBSDJail = false; + } + return inFreeBSDJail; + }, + + // On IBMi, process.platform and os.platform() both return 'aix', + // when built with Python versions earlier than 3.9. + // It is not enough to differentiate between IBMi and real AIX system. + get isAIX() { + return require('os').type() === 'AIX'; + }, + + get isIBMi() { + return require('os').type() === 'OS400'; + }, + + get isLinuxPPCBE() { + return (process.platform === 'linux') && (process.arch === 'ppc64') && + (require('os').endianness() === 'BE'); + }, + + get localhostIPv4() { + if (localhostIPv4 !== null) return localhostIPv4; + + if (this.inFreeBSDJail) { + // Jailed network interfaces are a bit special - since we need to jump + // through loops, as well as this being an exception case, assume the + // user will provide this instead. + if (process.env.LOCALHOST) { + localhostIPv4 = process.env.LOCALHOST; + } else { + console.error('Looks like we\'re in a FreeBSD Jail. ' + + 'Please provide your default interface address ' + + 'as LOCALHOST or expect some tests to fail.'); + } + } + + if (localhostIPv4 === null) localhostIPv4 = '127.0.0.1'; + + return localhostIPv4; + }, + + // opensslCli defined lazily to reduce overhead of spawnSync + get opensslCli() { + if (opensslCli !== null) return opensslCli; + + if (process.config.variables.node_shared_openssl) { + // Use external command + opensslCli = 'openssl'; + } else { + // Use command built from sources included in Node.js repository + opensslCli = path.join(path.dirname(process.execPath), 'openssl-cli'); + } + + if (exports.isWindows) opensslCli += '.exe'; + + const opensslCmd = spawnSync(opensslCli, ['version']); + if (opensslCmd.status !== 0 || opensslCmd.error !== undefined) { + // OpenSSL command cannot be executed + opensslCli = false; + } + return opensslCli; + }, + + get PORT() { + if (+process.env.TEST_PARALLEL) { + throw new Error('common.PORT cannot be used in a parallelized test'); + } + return +process.env.NODE_COMMON_PORT || 12346; + }, + + /** + * Returns the EOL character used by this Git checkout. + */ + get checkoutEOL() { + return fs.readFileSync(__filename).includes('\r\n') ? '\r\n' : '\n'; + }, +}; + +const validProperties = new Set(Object.keys(common)); +module.exports = new Proxy(common, { + get(obj, prop) { + if (!validProperties.has(prop)) + throw new Error(`Using invalid common property: '${prop}'`); + return obj[prop]; + }, +}); diff --git a/test/napi/node-napi-tests/test/common/index.mjs b/test/napi/node-napi-tests/test/common/index.mjs new file mode 100644 index 0000000000..54d942e007 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/index.mjs @@ -0,0 +1,110 @@ +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const common = require("./index.js"); + +const { + allowGlobals, + buildType, + canCreateSymLink, + checkoutEOL, + childShouldThrowAndAbort, + createZeroFilledFile, + enoughTestMem, + escapePOSIXShell, + expectsError, + expectWarning, + getArrayBufferViews, + getBufferSources, + getTTYfd, + hasCrypto, + hasIntl, + hasIPv6, + hasMultiLocalhost, + isAIX, + isAlive, + isDumbTerminal, + isFreeBSD, + isIBMi, + isLinux, + isLinuxPPCBE, + isMainThread, + isOpenBSD, + isMacOS, + isSunOS, + isWindows, + localIPv6Hosts, + mustCall, + mustCallAtLeast, + mustNotCall, + mustNotMutateObjectDeep, + mustSucceed, + nodeProcessAborted, + opensslCli, + parseTestFlags, + PIPE, + platformTimeout, + printSkipMessage, + runWithInvalidFD, + skip, + skipIf32Bits, + skipIfDumbTerminal, + skipIfEslintMissing, + skipIfInspectorDisabled, + spawnPromisified, +} = common; + +const getPort = () => common.PORT; + +export { + allowGlobals, + buildType, + canCreateSymLink, + checkoutEOL, + childShouldThrowAndAbort, + createRequire, + createZeroFilledFile, + enoughTestMem, + escapePOSIXShell, + expectsError, + expectWarning, + getArrayBufferViews, + getBufferSources, + getPort, + getTTYfd, + hasCrypto, + hasIntl, + hasIPv6, + hasMultiLocalhost, + isAIX, + isAlive, + isDumbTerminal, + isFreeBSD, + isIBMi, + isLinux, + isLinuxPPCBE, + isMacOS, + isMainThread, + isOpenBSD, + isSunOS, + isWindows, + localIPv6Hosts, + mustCall, + mustCallAtLeast, + mustNotCall, + mustNotMutateObjectDeep, + mustSucceed, + nodeProcessAborted, + opensslCli, + parseTestFlags, + PIPE, + platformTimeout, + printSkipMessage, + runWithInvalidFD, + skip, + skipIf32Bits, + skipIfDumbTerminal, + skipIfEslintMissing, + skipIfInspectorDisabled, + spawnPromisified, +}; diff --git a/test/napi/node-napi-tests/test/common/inspector-helper.js b/test/napi/node-napi-tests/test/common/inspector-helper.js new file mode 100644 index 0000000000..864538f245 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/inspector-helper.js @@ -0,0 +1,549 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const http = require('http'); +const fixtures = require('../common/fixtures'); +const { spawn } = require('child_process'); +const { URL, pathToFileURL } = require('url'); +const { EventEmitter } = require('events'); + +const _MAINSCRIPT = fixtures.path('loop.js'); +const DEBUG = false; +const TIMEOUT = common.platformTimeout(15 * 1000); + +function spawnChildProcess(inspectorFlags, scriptContents, scriptFile) { + const args = [].concat(inspectorFlags); + if (scriptContents) { + args.push('-e', scriptContents); + } else { + args.push(scriptFile); + } + const child = spawn(process.execPath, args); + + const handler = tearDown.bind(null, child); + process.on('exit', handler); + process.on('uncaughtException', handler); + process.on('unhandledRejection', handler); + process.on('SIGINT', handler); + + return child; +} + +function makeBufferingDataCallback(dataCallback) { + let buffer = Buffer.alloc(0); + return (data) => { + const newData = Buffer.concat([buffer, data]); + const str = newData.toString('utf8'); + const lines = str.replace(/\r/g, '').split('\n'); + if (str.endsWith('\n')) + buffer = Buffer.alloc(0); + else + buffer = Buffer.from(lines.pop(), 'utf8'); + for (const line of lines) + dataCallback(line); + }; +} + +function tearDown(child, err) { + child.kill(); + if (err) { + console.error(err); + process.exit(1); + } +} + +function parseWSFrame(buffer) { + // Protocol described in https://tools.ietf.org/html/rfc6455#section-5 + let message = null; + if (buffer.length < 2) + return { length: 0, message }; + if (buffer[0] === 0x88 && buffer[1] === 0x00) { + return { length: 2, message, closed: true }; + } + assert.strictEqual(buffer[0], 0x81); + let dataLen = 0x7F & buffer[1]; + let bodyOffset = 2; + if (buffer.length < bodyOffset + dataLen) + return 0; + if (dataLen === 126) { + dataLen = buffer.readUInt16BE(2); + bodyOffset = 4; + } else if (dataLen === 127) { + assert(buffer[2] === 0 && buffer[3] === 0, 'Inspector message too big'); + dataLen = buffer.readUIntBE(4, 6); + bodyOffset = 10; + } + if (buffer.length < bodyOffset + dataLen) + return { length: 0, message }; + const jsonPayload = + buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8'); + try { + message = JSON.parse(jsonPayload); + } catch (e) { + console.error(`JSON.parse() failed for: ${jsonPayload}`); + throw e; + } + if (DEBUG) + console.log('[received]', JSON.stringify(message)); + return { length: bodyOffset + dataLen, message }; +} + +function formatWSFrame(message) { + const messageBuf = Buffer.from(JSON.stringify(message)); + + const wsHeaderBuf = Buffer.allocUnsafe(16); + wsHeaderBuf.writeUInt8(0x81, 0); + let byte2 = 0x80; + const bodyLen = messageBuf.length; + + let maskOffset = 2; + if (bodyLen < 126) { + byte2 = 0x80 + bodyLen; + } else if (bodyLen < 65536) { + byte2 = 0xFE; + wsHeaderBuf.writeUInt16BE(bodyLen, 2); + maskOffset = 4; + } else { + byte2 = 0xFF; + wsHeaderBuf.writeUInt32BE(bodyLen, 2); + wsHeaderBuf.writeUInt32BE(0, 6); + maskOffset = 10; + } + wsHeaderBuf.writeUInt8(byte2, 1); + wsHeaderBuf.writeUInt32BE(0x01020408, maskOffset); + + for (let i = 0; i < messageBuf.length; i++) + messageBuf[i] = messageBuf[i] ^ (1 << (i % 4)); + + return Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]); +} + +class InspectorSession { + constructor(socket, instance) { + this._instance = instance; + this._socket = socket; + this._nextId = 1; + this._commandResponsePromises = new Map(); + this._unprocessedNotifications = []; + this._notificationCallback = null; + this._scriptsIdsByUrl = new Map(); + this._pausedDetails = null; + + let buffer = Buffer.alloc(0); + socket.on('data', (data) => { + buffer = Buffer.concat([buffer, data]); + do { + const { length, message, closed } = parseWSFrame(buffer); + if (!length) + break; + + if (closed) { + socket.write(Buffer.from([0x88, 0x00])); // WS close frame + } + buffer = buffer.slice(length); + if (message) + this._onMessage(message); + } while (true); + }); + this._terminationPromise = new Promise((resolve) => { + socket.once('close', resolve); + }); + } + + + waitForServerDisconnect() { + return this._terminationPromise; + } + + async disconnect() { + this._socket.destroy(); + return this.waitForServerDisconnect(); + } + + _onMessage(message) { + if (message.id) { + const { resolve, reject } = this._commandResponsePromises.get(message.id); + this._commandResponsePromises.delete(message.id); + if (message.result) + resolve(message.result); + else + reject(message.error); + } else { + if (message.method === 'Debugger.scriptParsed') { + const { scriptId, url } = message.params; + this._scriptsIdsByUrl.set(scriptId, url); + const fileUrl = url.startsWith('file:') ? + url : pathToFileURL(url).toString(); + if (fileUrl === this.scriptURL().toString()) { + this.mainScriptId = scriptId; + } + } + if (message.method === 'Debugger.paused') + this._pausedDetails = message.params; + if (message.method === 'Debugger.resumed') + this._pausedDetails = null; + + if (this._notificationCallback) { + // In case callback needs to install another + const callback = this._notificationCallback; + this._notificationCallback = null; + callback(message); + } else { + this._unprocessedNotifications.push(message); + } + } + } + + unprocessedNotifications() { + return this._unprocessedNotifications; + } + + _sendMessage(message) { + const msg = JSON.parse(JSON.stringify(message)); // Clone! + msg.id = this._nextId++; + if (DEBUG) + console.log('[sent]', JSON.stringify(msg)); + + const responsePromise = new Promise((resolve, reject) => { + this._commandResponsePromises.set(msg.id, { resolve, reject }); + }); + + return new Promise( + (resolve) => this._socket.write(formatWSFrame(msg), resolve)) + .then(() => responsePromise); + } + + send(commands) { + if (Array.isArray(commands)) { + // Multiple commands means the response does not matter. There might even + // never be a response. + return Promise + .all(commands.map((command) => this._sendMessage(command))) + .then(() => {}); + } + return this._sendMessage(commands); + } + + waitForNotification(methodOrPredicate, description) { + const desc = description || methodOrPredicate; + const message = `Timed out waiting for matching notification (${desc})`; + return fires( + this._asyncWaitForNotification(methodOrPredicate), message, TIMEOUT); + } + + async _asyncWaitForNotification(methodOrPredicate) { + function matchMethod(notification) { + return notification.method === methodOrPredicate; + } + const predicate = + typeof methodOrPredicate === 'string' ? matchMethod : methodOrPredicate; + let notification = null; + do { + if (this._unprocessedNotifications.length) { + notification = this._unprocessedNotifications.shift(); + } else { + notification = await new Promise( + (resolve) => this._notificationCallback = resolve); + } + } while (!predicate(notification)); + return notification; + } + + _isBreakOnLineNotification(message, line, expectedScriptPath) { + if (message.method === 'Debugger.paused') { + const callFrame = message.params.callFrames[0]; + const location = callFrame.location; + const scriptPath = this._scriptsIdsByUrl.get(location.scriptId); + assert.strictEqual(decodeURIComponent(scriptPath), + decodeURIComponent(expectedScriptPath), + `${scriptPath} !== ${expectedScriptPath}`); + assert.strictEqual(location.lineNumber, line); + return true; + } + } + + waitForBreakOnLine(line, url) { + return this + .waitForNotification( + (notification) => + this._isBreakOnLineNotification(notification, line, url), + `break on ${url}:${line}`); + } + + waitForPauseOnStart() { + return this + .waitForNotification( + (notification) => + notification.method === 'Debugger.paused' && notification.params.reason === 'Break on start', + 'break on start'); + } + + pausedDetails() { + return this._pausedDetails; + } + + _matchesConsoleOutputNotification(notification, type, values) { + if (!Array.isArray(values)) + values = [ values ]; + if (notification.method === 'Runtime.consoleAPICalled') { + const params = notification.params; + if (params.type === type) { + let i = 0; + for (const value of params.args) { + if (value.value !== values[i++]) + return false; + } + return i === values.length; + } + } + } + + waitForConsoleOutput(type, values) { + const desc = `Console output matching ${JSON.stringify(values)}`; + return this.waitForNotification( + (notification) => this._matchesConsoleOutputNotification(notification, + type, values), + desc); + } + + async runToCompletion() { + console.log('[test]', 'Verify node waits for the frontend to disconnect'); + await this.send({ 'method': 'Debugger.resume' }); + await this.waitForNotification((notification) => { + if (notification.method === 'Debugger.paused') { + this.send({ 'method': 'Debugger.resume' }); + } + return notification.method === 'Runtime.executionContextDestroyed' && + notification.params.executionContextId === 1; + }); + await this.waitForDisconnect(); + } + + async waitForDisconnect() { + while ((await this._instance.nextStderrString()) !== + 'Waiting for the debugger to disconnect...'); + await this.disconnect(); + } + + scriptPath() { + return this._instance.scriptPath(); + } + + script() { + return this._instance.script(); + } + + scriptURL() { + return pathToFileURL(this.scriptPath()); + } +} + +class NodeInstance extends EventEmitter { + constructor(inspectorFlags = ['--inspect-brk=0', '--expose-internals'], + scriptContents = '', + scriptFile = _MAINSCRIPT, + logger = console) { + super(); + + this._logger = logger; + this._scriptPath = scriptFile; + this._script = scriptFile ? null : scriptContents; + this._portCallback = null; + this.resetPort(); + this._process = spawnChildProcess(inspectorFlags, scriptContents, + scriptFile); + this._running = true; + this._stderrLineCallback = null; + this._unprocessedStderrLines = []; + + this._process.stdout.on('data', makeBufferingDataCallback( + (line) => { + this.emit('stdout', line); + this._logger.log('[out]', line); + })); + + this._process.stderr.on('data', makeBufferingDataCallback( + (message) => this.onStderrLine(message))); + + this._shutdownPromise = new Promise((resolve) => { + this._process.once('exit', (exitCode, signal) => { + if (signal) { + this._logger.error(`[err] child process crashed, signal ${signal}`); + } + resolve({ exitCode, signal }); + this._running = false; + }); + }); + } + + get pid() { + return this._process.pid; + } + + resetPort() { + this.portPromise = new Promise((resolve) => this._portCallback = resolve); + } + + static async startViaSignal(scriptContents) { + const instance = new NodeInstance( + ['--expose-internals', '--inspect-port=0'], + `${scriptContents}\nprocess._rawDebug('started');`, undefined); + const msg = 'Timed out waiting for process to start'; + while (await fires(instance.nextStderrString(), msg, TIMEOUT) !== 'started'); + process._debugProcess(instance._process.pid); + return instance; + } + + onStderrLine(line) { + this.emit('stderr', line); + this._logger.log('[err]', line); + if (this._portCallback) { + const matches = line.match(/Debugger listening on ws:\/\/.+:(\d+)\/.+/); + if (matches) { + this._portCallback(matches[1]); + this._portCallback = null; + } + } + if (this._stderrLineCallback) { + this._stderrLineCallback(line); + this._stderrLineCallback = null; + } else { + this._unprocessedStderrLines.push(line); + } + } + + httpGet(host, path, hostHeaderValue) { + this._logger.log('[test]', `Testing ${path}`); + const headers = hostHeaderValue ? { 'Host': hostHeaderValue } : null; + return this.portPromise.then((port) => new Promise((resolve, reject) => { + const req = http.get({ host, port, family: 4, path, headers }, (res) => { + let response = ''; + res.setEncoding('utf8'); + res + .on('data', (data) => response += data.toString()) + .on('end', () => { + resolve(response); + }); + }); + req.on('error', reject); + })).then((response) => { + try { + return JSON.parse(response); + } catch (e) { + e.body = response; + throw e; + } + }); + } + + async sendUpgradeRequest() { + const response = await this.httpGet(null, '/json/list'); + const devtoolsUrl = response[0].webSocketDebuggerUrl; + const port = await this.portPromise; + return http.get({ + port, + family: 4, + path: new URL(devtoolsUrl).pathname, + headers: { + 'Connection': 'Upgrade', + 'Upgrade': 'websocket', + 'Sec-WebSocket-Version': 13, + 'Sec-WebSocket-Key': 'key==', + }, + }); + } + + async connectInspectorSession() { + this._logger.log('[test]', 'Connecting to a child Node process'); + const upgradeRequest = await this.sendUpgradeRequest(); + return new Promise((resolve) => { + upgradeRequest + .on('upgrade', + (message, socket) => resolve(new InspectorSession(socket, this))) + .on('response', common.mustNotCall('Upgrade was not received')); + }); + } + + async expectConnectionDeclined() { + this._logger.log('[test]', 'Checking upgrade is not possible'); + const upgradeRequest = await this.sendUpgradeRequest(); + return new Promise((resolve) => { + upgradeRequest + .on('upgrade', common.mustNotCall('Upgrade was received')) + .on('response', (response) => + response.on('data', () => {}) + .on('end', () => resolve(response.statusCode))); + }); + } + + expectShutdown() { + return this._shutdownPromise; + } + + nextStderrString() { + if (this._unprocessedStderrLines.length) + return Promise.resolve(this._unprocessedStderrLines.shift()); + return new Promise((resolve) => this._stderrLineCallback = resolve); + } + + write(message) { + this._process.stdin.write(message); + } + + kill() { + this._process.kill(); + return this.expectShutdown(); + } + + scriptPath() { + return this._scriptPath; + } + + script() { + if (this._script === null) + this._script = fs.readFileSync(this.scriptPath(), 'utf8'); + return this._script; + } +} + +function onResolvedOrRejected(promise, callback) { + return promise.then((result) => { + callback(); + return result; + }, (error) => { + callback(); + throw error; + }); +} + +function timeoutPromise(error, timeoutMs) { + let clearCallback = null; + let done = false; + const promise = onResolvedOrRejected(new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(error), timeoutMs); + clearCallback = () => { + if (done) + return; + clearTimeout(timeout); + resolve(); + }; + }), () => done = true); + promise.clear = clearCallback; + return promise; +} + +// Returns a new promise that will propagate `promise` resolution or rejection +// if that happens within the `timeoutMs` timespan, or rejects with `error` as +// a reason otherwise. +function fires(promise, error, timeoutMs) { + const timeout = timeoutPromise(error, timeoutMs); + return Promise.race([ + onResolvedOrRejected(promise, () => timeout.clear()), + timeout, + ]); +} + +module.exports = { + NodeInstance, +}; diff --git a/test/napi/node-napi-tests/test/common/internet.js b/test/napi/node-napi-tests/test/common/internet.js new file mode 100644 index 0000000000..51f18aeb44 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/internet.js @@ -0,0 +1,58 @@ +'use strict'; + +// Utilities for internet-related tests + +const addresses = { + // A generic host that has registered common DNS records, + // supports both IPv4 and IPv6, and provides basic HTTP/HTTPS services + INET_HOST: 'nodejs.org', + // A host that provides IPv4 services + INET4_HOST: 'nodejs.org', + // A host that provides IPv6 services + INET6_HOST: 'nodejs.org', + // An accessible IPv4 IP, + // defaults to the Google Public DNS IPv4 address + INET4_IP: '8.8.8.8', + // An accessible IPv6 IP, + // defaults to the Google Public DNS IPv6 address + INET6_IP: '2001:4860:4860::8888', + // An invalid host that cannot be resolved + // See https://tools.ietf.org/html/rfc2606#section-2 + INVALID_HOST: 'something.invalid', + // A host with MX records registered + MX_HOST: 'nodejs.org', + // On some systems, .invalid returns a server failure/try again rather than + // record not found. Use this to guarantee record not found. + NOT_FOUND: 'come.on.fhqwhgads.test', + // A host with SRV records registered + SRV_HOST: '_caldav._tcp.google.com', + // A host with PTR records registered + PTR_HOST: '8.8.8.8.in-addr.arpa', + // A host with NAPTR records registered + NAPTR_HOST: 'sip2sip.info', + // A host with SOA records registered + SOA_HOST: 'nodejs.org', + // A host with CAA record registered + CAA_HOST: 'google.com', + // A host with CNAME records registered + CNAME_HOST: 'blog.nodejs.org', + // A host with NS records registered + NS_HOST: 'nodejs.org', + // A host with TXT records registered + TXT_HOST: 'nodejs.org', + // An accessible IPv4 DNS server + DNS4_SERVER: '8.8.8.8', + // An accessible IPv4 DNS server + DNS6_SERVER: '2001:4860:4860::8888', +}; + +for (const key of Object.keys(addresses)) { + const envName = `NODE_TEST_${key}`; + if (process.env[envName]) { + addresses[key] = process.env[envName]; + } +} + +module.exports = { + addresses, +}; diff --git a/test/napi/node-napi-tests/test/common/measure-memory.js b/test/napi/node-napi-tests/test/common/measure-memory.js new file mode 100644 index 0000000000..ffde35f285 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/measure-memory.js @@ -0,0 +1,57 @@ +'use strict'; + +const assert = require('assert'); +const common = require('./'); + +// The formats could change when V8 is updated, then the tests should be +// updated accordingly. +function assertResultShape(result) { + assert.strictEqual(typeof result.jsMemoryEstimate, 'number'); + assert.strictEqual(typeof result.jsMemoryRange[0], 'number'); + assert.strictEqual(typeof result.jsMemoryRange[1], 'number'); +} + +function assertSummaryShape(result) { + assert.strictEqual(typeof result, 'object'); + assert.strictEqual(typeof result.total, 'object'); + assertResultShape(result.total); +} + +function assertDetailedShape(result, contexts = 0) { + assert.strictEqual(typeof result, 'object'); + assert.strictEqual(typeof result.total, 'object'); + assert.strictEqual(typeof result.current, 'object'); + assertResultShape(result.total); + assertResultShape(result.current); + if (contexts === 0) { + assert.deepStrictEqual(result.other, []); + } else { + assert.strictEqual(result.other.length, contexts); + for (const item of result.other) { + assertResultShape(item); + } + } +} + +function assertSingleDetailedShape(result) { + assert.strictEqual(typeof result, 'object'); + assert.strictEqual(typeof result.total, 'object'); + assert.strictEqual(typeof result.current, 'object'); + assert.deepStrictEqual(result.other, []); + assertResultShape(result.total); + assertResultShape(result.current); +} + +function expectExperimentalWarning() { + common.expectWarning( + 'ExperimentalWarning', + 'vm.measureMemory is an experimental feature and might change at any time', + ); +} + +module.exports = { + assertSummaryShape, + assertDetailedShape, + assertSingleDetailedShape, + expectExperimentalWarning, +}; diff --git a/test/napi/node-napi-tests/test/common/net.js b/test/napi/node-napi-tests/test/common/net.js new file mode 100644 index 0000000000..84eddd0966 --- /dev/null +++ b/test/napi/node-napi-tests/test/common/net.js @@ -0,0 +1,23 @@ +'use strict'; +const net = require('net'); + +const options = { port: 0, reusePort: true }; + +function checkSupportReusePort() { + return new Promise((resolve, reject) => { + const server = net.createServer().listen(options); + server.on('listening', () => { + server.close(resolve); + }); + server.on('error', (err) => { + console.log('The `reusePort` option is not supported:', err.message); + server.close(); + reject(err); + }); + }); +} + +module.exports = { + checkSupportReusePort, + options, +}; diff --git a/test/napi/node-napi-tests/test/common/package.json b/test/napi/node-napi-tests/test/common/package.json new file mode 100644 index 0000000000..5bbefffbab --- /dev/null +++ b/test/napi/node-napi-tests/test/common/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/napi/node-napi-tests/test/common/process-exit-code-cases.js b/test/napi/node-napi-tests/test/common/process-exit-code-cases.js new file mode 100644 index 0000000000..54cfe2655b --- /dev/null +++ b/test/napi/node-napi-tests/test/common/process-exit-code-cases.js @@ -0,0 +1,138 @@ +'use strict'; + +const assert = require('assert'); + +function getTestCases(isWorker = false) { + const cases = []; + function exitsOnExitCodeSet() { + process.exitCode = 42; + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 42); + assert.strictEqual(code, 42); + }); + } + cases.push({ func: exitsOnExitCodeSet, result: 42 }); + + function changesCodeViaExit() { + process.exitCode = 99; + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 42); + assert.strictEqual(code, 42); + }); + process.exit(42); + } + cases.push({ func: changesCodeViaExit, result: 42 }); + + function changesCodeZeroExit() { + process.exitCode = 99; + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 0); + assert.strictEqual(code, 0); + }); + process.exit(0); + } + cases.push({ func: changesCodeZeroExit, result: 0 }); + + function exitWithOneOnUncaught() { + process.exitCode = 99; + process.on('exit', (code) => { + // Cannot use assert because it will be uncaughtException -> 1 exit code + // that will render this test useless + if (code !== 1 || process.exitCode !== 1) { + console.log('wrong code! expected 1 for uncaughtException'); + process.exit(99); + } + }); + throw new Error('ok'); + } + cases.push({ + func: exitWithOneOnUncaught, + result: 1, + error: /^Error: ok$/, + }); + + function changeCodeInsideExit() { + process.exitCode = 95; + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 95); + assert.strictEqual(code, 95); + process.exitCode = 99; + }); + } + cases.push({ func: changeCodeInsideExit, result: 99 }); + + function zeroExitWithUncaughtHandler() { + const noop = () => { }; + process.on('exit', (code) => { + process.off('uncaughtException', noop); + assert.strictEqual(process.exitCode, undefined); + assert.strictEqual(code, 0); + }); + process.on('uncaughtException', noop); + throw new Error('ok'); + } + cases.push({ func: zeroExitWithUncaughtHandler, result: 0 }); + + function changeCodeInUncaughtHandler() { + const modifyExitCode = () => { process.exitCode = 97; }; + process.on('exit', (code) => { + process.off('uncaughtException', modifyExitCode); + assert.strictEqual(process.exitCode, 97); + assert.strictEqual(code, 97); + }); + process.on('uncaughtException', modifyExitCode); + throw new Error('ok'); + } + cases.push({ func: changeCodeInUncaughtHandler, result: 97 }); + + function changeCodeInExitWithUncaught() { + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 1); + assert.strictEqual(code, 1); + process.exitCode = 98; + }); + throw new Error('ok'); + } + cases.push({ + func: changeCodeInExitWithUncaught, + result: 98, + error: /^Error: ok$/, + }); + + function exitWithZeroInExitWithUncaught() { + process.on('exit', (code) => { + assert.strictEqual(process.exitCode, 1); + assert.strictEqual(code, 1); + process.exitCode = 0; + }); + throw new Error('ok'); + } + cases.push({ + func: exitWithZeroInExitWithUncaught, + result: 0, + error: /^Error: ok$/, + }); + + function exitWithThrowInUncaughtHandler() { + process.on('uncaughtException', () => { + throw new Error('ok'); + }); + throw new Error('bad'); + } + cases.push({ + func: exitWithThrowInUncaughtHandler, + result: isWorker ? 1 : 7, + error: /^Error: ok$/, + }); + + function exitWithUndefinedFatalException() { + process._fatalException = undefined; + throw new Error('ok'); + } + cases.push({ + func: exitWithUndefinedFatalException, + result: 6, + }); + return cases; +} +exports.getTestCases = getTestCases; diff --git a/test/napi/node-napi-tests/test/common/prof.js b/test/napi/node-napi-tests/test/common/prof.js new file mode 100644 index 0000000000..13047406dc --- /dev/null +++ b/test/napi/node-napi-tests/test/common/prof.js @@ -0,0 +1,67 @@ +'use strict'; + +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); + +function getHeapProfiles(dir) { + const list = fs.readdirSync(dir); + return list + .filter((file) => file.endsWith('.heapprofile')) + .map((file) => path.join(dir, file)); +} + +function findFirstFrameInNode(root, func) { + const first = root.children.find( + (child) => child.callFrame.functionName === func, + ); + if (first) { + return first; + } + for (const child of root.children) { + const first = findFirstFrameInNode(child, func); + if (first) { + return first; + } + } + return undefined; +} + +function findFirstFrame(file, func) { + const data = fs.readFileSync(file, 'utf8'); + const profile = JSON.parse(data); + const first = findFirstFrameInNode(profile.head, func); + return { frame: first, roots: profile.head.children }; +} + +function verifyFrames(output, file, func) { + const { frame, roots } = findFirstFrame(file, func); + if (!frame) { + // Show native debug output and the profile for debugging. + console.log(output.stderr.toString()); + console.log(roots); + } + assert.notStrictEqual(frame, undefined); +} + +// We need to set --heap-prof-interval to a small enough value to make +// sure we can find our workload in the samples, so we need to set +// TEST_ALLOCATION > kHeapProfInterval. +const kHeapProfInterval = 128; +const TEST_ALLOCATION = kHeapProfInterval * 2; + +const env = { + ...process.env, + TEST_ALLOCATION, + NODE_DEBUG_NATIVE: 'INSPECTOR_PROFILER', +}; + +// TODO(joyeecheung): share the fixutres with v8 coverage tests +module.exports = { + getHeapProfiles, + verifyFrames, + findFirstFrame, + kHeapProfInterval, + TEST_ALLOCATION, + env, +}; diff --git a/test/napi/node-napi-tests/test/common/report.js b/test/napi/node-napi-tests/test/common/report.js new file mode 100644 index 0000000000..3280116feb --- /dev/null +++ b/test/napi/node-napi-tests/test/common/report.js @@ -0,0 +1,346 @@ +'use strict'; +const assert = require('assert'); +const fs = require('fs'); +const net = require('net'); +const os = require('os'); +const path = require('path'); +const util = require('util'); +const cpus = os.cpus(); + +function findReports(pid, dir) { + // Default filenames are of the form + // report..