From 70336eff44f38a859340bb0efc0744c0d8a687e1 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 22 Oct 2026 23:55:15 +0000 Subject: [PATCH 01/22] bindgen: move every Rust function that takes a `CallFrame` to schemas and delete `#[host_fn]`, `$rust` and `$newRustFunction` --- docs/project/bindgen.mdx | 19 +- scripts/build/codegen.ts | 9 +- src/cares_sys/c_ares.rs | 2 - src/codegen/bindgen-emit.ts | 100 ++- src/codegen/bindgen-lib-internal.ts | 26 +- src/codegen/bindgen-lib.ts | 5 +- src/codegen/bundle-modules.ts | 5 +- src/codegen/generate-host-exports.ts | 78 +-- src/codegen/generate-js2native.ts | 252 +------- src/codegen/replacements.ts | 7 +- src/css_jsc/build.rs | 35 ++ src/css_jsc/color_js.bind.ts | 10 + src/css_jsc/color_js.rs | 12 +- src/css_jsc/css_internals.bind.ts | 28 + src/css_jsc/css_internals.rs | 219 ++++--- src/css_jsc/generated_bindgen.rs | 6 + src/css_jsc/lib.rs | 7 +- src/http/AsyncHTTP.rs | 2 +- src/http/H2Client.rs | 2 +- src/http/H3Client.rs | 5 +- src/http_jsc/build.rs | 35 ++ src/http_jsc/generated_bindgen.rs | 6 + src/http_jsc/headers_jsc.bind.ts | 9 + src/http_jsc/headers_jsc.rs | 73 +-- src/http_jsc/lib.rs | 1 + .../lockfile_json_stringify_for_debugging.rs | 2 +- src/install/npm.rs | 2 - src/install_jsc/build.rs | 35 ++ src/install_jsc/generated_bindgen.rs | 6 + src/install_jsc/hosted_git_info_jsc.bind.ts | 17 + src/install_jsc/hosted_git_info_jsc.rs | 55 +- src/install_jsc/ini_jsc.bind.ts | 17 + src/install_jsc/ini_jsc.rs | 46 +- src/install_jsc/install_binding.bind.ts | 12 + src/install_jsc/install_binding.rs | 43 +- src/install_jsc/lib.rs | 15 +- src/install_jsc/npm_jsc.bind.ts | 26 + src/install_jsc/npm_jsc.rs | 94 +-- src/js/builtins/Ipc.ts | 2 +- src/js/bun/ffi.ts | 2 +- src/js/internal-for-testing.ts | 304 +++------ src/js/internal/bake.ts | 10 +- src/js/internal/cluster/RoundRobinHandle.ts | 2 +- src/js/internal/cluster/SharedHandle.ts | 2 +- src/js/internal/cluster/primary.ts | 2 +- src/js/internal/fs/binding.ts | 2 +- src/js/internal/quic/binding.ts | 2 +- src/js/internal/serve_cli.ts | 2 +- src/js/internal/sql/mysql.ts | 2 +- src/js/internal/sql/postgres.ts | 2 +- src/js/node/_http_server.ts | 2 +- src/js/node/crypto.ts | 2 +- src/js/node/dns.ts | 2 +- src/js/node/net.ts | 4 +- src/js/node/tls.ts | 6 +- src/js/node/util.ts | 2 +- src/js/private.d.ts | 31 +- src/jsc/BytecodeOrderRecorder.bind.ts | 11 + src/jsc/BytecodeOrderRecorder.rs | 45 +- src/jsc/CallFrame.rs | 50 -- src/jsc/Counters.bind.ts | 5 + src/jsc/Counters.rs | 11 +- src/jsc/Debugger.rs | 17 +- src/jsc/JSGlobalObject.rs | 2 +- src/jsc/JSSecrets.rs | 3 +- src/jsc/MarkedArgumentBuffer.rs | 43 -- src/jsc/bindgen_test.bind.ts | 17 + src/jsc/bindgen_test.rs | 38 +- src/jsc/bindings/BunObject+exports.h | 23 - src/jsc/bindings/BunObject.cpp | 50 +- src/jsc/bindings/BunProcess.cpp | 4 +- .../bindings/InspectorTestReporterAgent.cpp | 8 +- src/jsc/bindings/InspectorTestReporterAgent.h | 2 +- src/jsc/bindings/JSDOMFile.cpp | 8 +- src/jsc/bindings/JSS3File.cpp | 9 +- src/jsc/bindings/JSSecrets.cpp | 8 +- src/jsc/bindings/NodeHTTP.cpp | 4 +- src/jsc/bindings/NodeTLS.cpp | 15 - src/jsc/bindings/NodeTLS.h | 3 - src/jsc/bindings/ZigGlobalObject.cpp | 7 +- src/jsc/bindings/ZigGlobalObject.lut.txt | 6 +- src/jsc/bindings/bindings.cpp | 4 +- src/jsc/bindings/webcore/JSDOMFormData.cpp | 5 +- src/jsc/bindings/webcore/JSDOMURL.cpp | 8 +- src/jsc/bun_string_jsc.bind.ts | 16 + src/jsc/bun_string_jsc.rs | 89 +-- src/jsc/event_loop.bind.ts | 5 + src/jsc/event_loop.rs | 10 +- src/jsc/host_fn.rs | 142 +---- src/jsc/host_object.rs | 35 -- src/jsc/lib.rs | 24 +- src/jsc/modules/NodeBufferModule.h | 5 +- src/jsc/modules/NodeModuleModule.cpp | 8 +- src/jsc/rare_data.bind.ts | 6 + src/jsc/rare_data.rs | 35 +- src/jsc/resolver_jsc.bind.ts | 9 + src/jsc/resolver_jsc.rs | 9 +- src/jsc/virtual_machine_exports.bind.ts | 10 + src/jsc/virtual_machine_exports.rs | 28 +- src/jsc/webcore_types.rs | 4 +- src/jsc_macros/lib.rs | 103 +-- src/patch_jsc/build.rs | 35 ++ src/patch_jsc/generated_bindgen.rs | 6 + src/patch_jsc/lib.rs | 3 +- src/patch_jsc/testing.bind.ts | 25 + src/patch_jsc/testing.rs | 115 +--- src/resolver/resolver.rs | 2 +- src/runtime/api.rs | 1 - src/runtime/api/BunObject.bind.ts | 31 + src/runtime/api/BunObject.rs | 221 ++----- src/runtime/api/JSBundler.bind.ts | 10 + src/runtime/api/JSBundler.rs | 39 +- src/runtime/api/JSSourceMap.rs | 10 +- src/runtime/api/MarkdownObject.bind.ts | 12 + src/runtime/api/MarkdownObject.rs | 115 ++-- .../api/MarkdownObjectInternals.bind.ts | 5 + src/runtime/api/bun/SecureContext.bind.ts | 4 + src/runtime/api/bun/SecureContext.rs | 7 +- .../api/bun/js_bun_spawn_bindings.bind.ts | 14 + src/runtime/api/bun/js_bun_spawn_bindings.rs | 34 +- src/runtime/api/bun/subprocess.bind.ts | 10 + src/runtime/api/bun/subprocess.rs | 54 +- src/runtime/api/crash_handler_jsc.bind.ts | 18 + src/runtime/api/crash_handler_jsc.rs | 101 +-- src/runtime/api/cron.bind.ts | 36 ++ src/runtime/api/cron.rs | 134 ++-- src/runtime/api/html_rewriter.rs | 95 ++- .../api/html_rewriter_internals.bind.ts | 13 + src/runtime/api/sourcemap.bind.ts | 4 + src/runtime/bake/config/plugins.bind.ts | 10 + src/runtime/bake/config/plugins.rs | 47 +- src/runtime/bake/router/js.bind.ts | 11 + src/runtime/bake/router/js.rs | 48 +- src/runtime/build.rs | 11 +- src/runtime/cli/pack_command.bind.ts | 10 + src/runtime/cli/pack_command.rs | 20 +- src/runtime/cli/run_command.rs | 8 +- src/runtime/cli/upgrade_command.bind.ts | 8 + src/runtime/cli/upgrade_command.rs | 54 +- src/runtime/crypto/PBKDF2.rs | 74 ++- src/runtime/dispatch.rs | 6 - src/runtime/dispatch_js2native.rs | 81 --- src/runtime/dns_jsc/cares_jsc.bind.ts | 5 + src/runtime/dns_jsc/cares_jsc.rs | 25 +- src/runtime/dns_jsc/dns.rs | 124 +--- src/runtime/dns_jsc/dns_internals.bind.ts | 27 + src/runtime/ffi/FFIObject.bind.ts | 9 + src/runtime/ffi/FFIObject.rs | 218 ++----- src/runtime/ffi/ffi.bind.ts | 18 + src/runtime/ffi/ffi_body.rs | 106 ++-- src/runtime/ffi/mod.rs | 9 - src/runtime/generated_host_exports.rs | 8 - src/runtime/generated_js2native.rs | 22 - src/runtime/hw_exports.bind.ts | 10 + src/runtime/hw_exports.rs | 45 +- src/runtime/ipc.bind.ts | 10 + src/runtime/ipc.rs | 11 +- src/runtime/ipc_host.bind.ts | 24 + src/runtime/ipc_host.rs | 66 +- src/runtime/jsc_hooks.rs | 2 +- src/runtime/lib.rs | 3 +- src/runtime/linear_fifo_testing.bind.ts | 5 + src/runtime/linear_fifo_testing.rs | 24 +- src/runtime/napi/napi_body.bind.ts | 5 + src/runtime/napi/napi_body.rs | 16 +- src/runtime/node.rs | 2 - src/runtime/node/Stat.bind.ts | 10 + src/runtime/node/Stat.rs | 16 +- src/runtime/node/memory_pressure.bind.ts | 12 + src/runtime/node/memory_pressure.rs | 54 +- src/runtime/node/node_cluster_binding.bind.ts | 13 + src/runtime/node/node_cluster_binding.rs | 68 +- src/runtime/node/node_crypto_binding.bind.ts | 69 ++ src/runtime/node/node_crypto_binding.rs | 595 ++++++++---------- src/runtime/node/node_fs_binding.bind.ts | 24 +- src/runtime/node/node_fs_binding.rs | 52 +- src/runtime/node/node_http_binding.bind.ts | 1 + src/runtime/node/node_http_binding.rs | 32 +- src/runtime/node/node_net_binding.bind.ts | 1 + src/runtime/node/node_net_binding.rs | 20 +- src/runtime/node/node_process.rs | 5 +- src/runtime/node/node_quic_binding.bind.ts | 13 + src/runtime/node/node_quic_binding.rs | 18 +- src/runtime/node/util/parse_args.bind.ts | 5 + src/runtime/node/util/parse_args.rs | 14 +- src/runtime/server/NodeHTTPResponse.bind.ts | 7 + src/runtime/server/NodeHTTPResponse.rs | 29 +- src/runtime/server/RequestContext.bind.ts | 42 ++ src/runtime/server/RequestContext.rs | 306 +++------ src/runtime/server/mod.rs | 13 +- src/runtime/server/server_body.bind.ts | 7 + src/runtime/server/server_body.rs | 41 +- src/runtime/shell/ParsedShellScript.bind.ts | 9 + src/runtime/shell/ParsedShellScript.rs | 27 +- src/runtime/shell/mod.rs | 2 - src/runtime/shell/shell_body.bind.ts | 19 + src/runtime/shell/shell_body.rs | 100 +-- src/runtime/socket/Listener.bind.ts | 4 + src/runtime/socket/Listener.rs | 12 +- src/runtime/socket/UpgradedDuplex.bind.ts | 16 + src/runtime/socket/UpgradedDuplex.rs | 99 ++- src/runtime/socket/mod.rs | 13 - src/runtime/socket/socket_body.bind.ts | 25 + src/runtime/socket/socket_body.rs | 125 ++-- src/runtime/socket/udp_socket.bind.ts | 7 + src/runtime/socket/udp_socket.rs | 4 +- src/runtime/test_runner/DoneCallback.rs | 22 +- .../test_runner/ScopeFunctions.bind.ts | 13 + src/runtime/test_runner/ScopeFunctions.rs | 34 +- src/runtime/test_runner/bun_test.bind.ts | 41 ++ src/runtime/test_runner/bun_test.rs | 117 ++-- src/runtime/test_runner/expect.bind.ts | 11 + src/runtime/test_runner/expect.rs | 58 +- src/runtime/test_runner/jest.bind.ts | 13 +- src/runtime/test_runner/jest.rs | 110 ++-- .../test_runner/timers/FakeTimers.bind.ts | 30 + src/runtime/test_runner/timers/FakeTimers.rs | 131 ++-- src/runtime/timer/Timer.bind.ts | 2 + src/runtime/timer/Timer.rs | 28 +- src/runtime/timer/mod.rs | 4 - src/runtime/webcore/Blob.bind.ts | 5 + src/runtime/webcore/Blob.rs | 138 ++-- src/runtime/webcore/ByteStream.bind.ts | 5 + src/runtime/webcore/ByteStream.rs | 40 +- src/runtime/webcore/Crypto.bind.ts | 10 + src/runtime/webcore/Crypto.rs | 26 +- src/runtime/webcore/FileSink.bind.ts | 9 + src/runtime/webcore/FileSink.rs | 87 +-- src/runtime/webcore/FormData.bind.ts | 11 + src/runtime/webcore/FormData.rs | 14 +- src/runtime/webcore/ObjectURLRegistry.bind.ts | 18 + src/runtime/webcore/ObjectURLRegistry.rs | 72 +-- src/runtime/webcore/S3File.rs | 36 +- src/runtime/webcore/fetch.bind.ts | 11 + src/runtime/webcore/fetch.rs | 83 +-- src/runtime/webcore/fetch/FetchSession.rs | 2 +- .../webcore/fetch/FetchTasklet.bind.ts | 7 + src/runtime/webcore/fetch/FetchTasklet.rs | 61 +- .../webcore/fetch/proxy_testing.bind.ts | 17 + src/runtime/webcore/fetch/proxy_testing.rs | 59 +- src/runtime/webcore/prompt.bind.ts | 10 + src/runtime/webcore/prompt.rs | 49 +- src/runtime/webcore/s3/client.bind.ts | 7 + src/runtime/webcore/s3/client.rs | 59 +- src/semver_jsc/SemverObject.bind.ts | 6 + src/semver_jsc/SemverObject.rs | 17 +- src/sourcemap_jsc/build.rs | 35 ++ src/sourcemap_jsc/generated_bindgen.rs | 6 + src/sourcemap_jsc/internal_jsc.bind.ts | 32 + src/sourcemap_jsc/internal_jsc.rs | 141 ++--- src/sourcemap_jsc/lib.rs | 3 +- src/sql_jsc/jsc.rs | 184 +----- src/sql_jsc/lib.rs | 2 +- src/sql_jsc/mysql.bind.ts | 23 + src/sql_jsc/mysql.rs | 23 - src/sql_jsc/mysql/JSMySQLConnection.rs | 27 +- src/sql_jsc/mysql/JSMySQLQuery.rs | 23 +- src/sql_jsc/mysql/MySQLContext.rs | 19 +- src/sql_jsc/postgres.bind.ts | 23 + src/sql_jsc/postgres.rs | 15 - src/sql_jsc/postgres/PostgresSQLConnection.rs | 24 +- src/sql_jsc/postgres/PostgresSQLContext.rs | 22 +- src/sql_jsc/postgres/PostgresSQLQuery.rs | 23 +- src/sql_jsc/shared/QueryCtorArgs.rs | 21 +- src/sys_jsc/build.rs | 35 ++ src/sys_jsc/error_jsc.bind.ts | 14 + src/sys_jsc/error_jsc.rs | 115 +--- src/sys_jsc/generated_bindgen.rs | 6 + src/sys_jsc/lib.rs | 5 +- test/integration/bun-types/bun-types.test.ts | 64 +- test/internal-for-testing.d.ts | 2 - test/internal/bindgen.test.ts | 29 + test/js/bun/patch/patch.test.ts | 2 - test/js/web/fetch/fetch-args.test.ts | 2 +- test/js/web/fetch/fetch-session.test.ts | 2 +- 275 files changed, 4102 insertions(+), 5208 deletions(-) create mode 100644 src/css_jsc/build.rs create mode 100644 src/css_jsc/color_js.bind.ts create mode 100644 src/css_jsc/css_internals.bind.ts create mode 100644 src/css_jsc/generated_bindgen.rs create mode 100644 src/http_jsc/build.rs create mode 100644 src/http_jsc/generated_bindgen.rs create mode 100644 src/http_jsc/headers_jsc.bind.ts create mode 100644 src/install_jsc/build.rs create mode 100644 src/install_jsc/generated_bindgen.rs create mode 100644 src/install_jsc/hosted_git_info_jsc.bind.ts create mode 100644 src/install_jsc/ini_jsc.bind.ts create mode 100644 src/install_jsc/install_binding.bind.ts create mode 100644 src/install_jsc/npm_jsc.bind.ts create mode 100644 src/jsc/BytecodeOrderRecorder.bind.ts create mode 100644 src/jsc/Counters.bind.ts create mode 100644 src/jsc/bun_string_jsc.bind.ts create mode 100644 src/jsc/event_loop.bind.ts delete mode 100644 src/jsc/host_object.rs create mode 100644 src/jsc/rare_data.bind.ts create mode 100644 src/jsc/resolver_jsc.bind.ts create mode 100644 src/jsc/virtual_machine_exports.bind.ts create mode 100644 src/patch_jsc/build.rs create mode 100644 src/patch_jsc/generated_bindgen.rs create mode 100644 src/patch_jsc/testing.bind.ts create mode 100644 src/runtime/api/MarkdownObject.bind.ts create mode 100644 src/runtime/api/MarkdownObjectInternals.bind.ts create mode 100644 src/runtime/api/bun/js_bun_spawn_bindings.bind.ts create mode 100644 src/runtime/api/crash_handler_jsc.bind.ts create mode 100644 src/runtime/api/html_rewriter_internals.bind.ts create mode 100644 src/runtime/bake/config/plugins.bind.ts create mode 100644 src/runtime/bake/router/js.bind.ts create mode 100644 src/runtime/cli/pack_command.bind.ts create mode 100644 src/runtime/cli/upgrade_command.bind.ts delete mode 100644 src/runtime/dispatch_js2native.rs create mode 100644 src/runtime/dns_jsc/cares_jsc.bind.ts create mode 100644 src/runtime/ffi/FFIObject.bind.ts delete mode 100644 src/runtime/generated_js2native.rs create mode 100644 src/runtime/hw_exports.bind.ts create mode 100644 src/runtime/ipc.bind.ts create mode 100644 src/runtime/ipc_host.bind.ts create mode 100644 src/runtime/linear_fifo_testing.bind.ts create mode 100644 src/runtime/napi/napi_body.bind.ts create mode 100644 src/runtime/node/Stat.bind.ts create mode 100644 src/runtime/node/memory_pressure.bind.ts create mode 100644 src/runtime/node/node_crypto_binding.bind.ts create mode 100644 src/runtime/node/node_quic_binding.bind.ts create mode 100644 src/runtime/node/util/parse_args.bind.ts create mode 100644 src/runtime/server/RequestContext.bind.ts create mode 100644 src/runtime/server/server_body.bind.ts create mode 100644 src/runtime/shell/shell_body.bind.ts create mode 100644 src/runtime/socket/UpgradedDuplex.bind.ts create mode 100644 src/runtime/test_runner/ScopeFunctions.bind.ts create mode 100644 src/runtime/test_runner/bun_test.bind.ts create mode 100644 src/runtime/test_runner/timers/FakeTimers.bind.ts create mode 100644 src/runtime/webcore/ByteStream.bind.ts create mode 100644 src/runtime/webcore/FileSink.bind.ts create mode 100644 src/runtime/webcore/FormData.bind.ts create mode 100644 src/runtime/webcore/ObjectURLRegistry.bind.ts create mode 100644 src/runtime/webcore/fetch.bind.ts create mode 100644 src/runtime/webcore/fetch/FetchTasklet.bind.ts create mode 100644 src/runtime/webcore/fetch/proxy_testing.bind.ts create mode 100644 src/runtime/webcore/prompt.bind.ts create mode 100644 src/runtime/webcore/s3/client.bind.ts create mode 100644 src/sourcemap_jsc/build.rs create mode 100644 src/sourcemap_jsc/generated_bindgen.rs create mode 100644 src/sourcemap_jsc/internal_jsc.bind.ts create mode 100644 src/sql_jsc/mysql.bind.ts create mode 100644 src/sql_jsc/postgres.bind.ts create mode 100644 src/sys_jsc/build.rs create mode 100644 src/sys_jsc/error_jsc.bind.ts create mode 100644 src/sys_jsc/generated_bindgen.rs diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 92d34a75c970..5bab30a2a848 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -18,10 +18,7 @@ The generator lives in `src/codegen/bindgen*.ts`, the Rust it generates against `src/jsc/bindgen.rs`, and the C++ in `src/jsc/bindings/Bindgen/` and `src/jsc/bindings/BunIDL*.h`. -The body of a binding is always Rust. These do overlapping work: - -- "JS2Native", allowing ad-hoc calls from `src/js` to native code (`$rust`, `$cpp`). -- `#[bun_jsc::host_fn]` functions, which take a `CallFrame`. +The body of a binding is always Rust. `$cpp` and `$newCppFunction` reach a function written in C++ from `src/js`. ## Functions @@ -79,7 +76,15 @@ not compile. To reach the function: - from `src/js`: `$bindgen("bindgen_test.bind.ts", "add")`, which is typed from the schema. An argument's type is what it takes, so one that coerces (`.loose`, `.wrap`, `.enforceRange()`) is `any`. A `.nullable` result, and a `.nullable` getter without a setter, is `T | null`; -- from a C++ static property table: `Bun::Bindgen::Generated::BindgenTest::jsAdd` in `GeneratedBindgenTest.h`. +- from a C++ static property table: `Bun::Bindgen::Generated::BindgenTest::jsAdd` in `GeneratedBindgenTest.h`; +- from Rust, or from C++ that compares host functions: `export: "Bun__CronJob__onPromiseResolve"` gives the host function C + linkage under that name. The generated header declares it, and in Rust it is a `JSHostFn`, + `crate::generated_bindgen::cron::Bun__CronJob__onPromiseResolve`, for `JSFunction::create` and `JSValue::then`. + +`passThis: true` adds `this_value: JsLocal<'a>` before the arguments, and `passCallee: true` adds `callee: JsLocal<'a>`, the +function that was called, after it. Neither goes with overloads. + +The caller's source location is `global.get_caller_src_loc()`: inside a host function `vm.topCallFrame` is its own frame. ### Namespaces @@ -99,7 +104,7 @@ export const semver = b.namespace({ `Bun::Bindgen::Generated::SemverObject::createSemver` makes the object. It has the signature of a `PropertyCallback` of a static property table, so the object is made when the property is -first read. +first read. `$bindgen("SemverObject.bind.ts", "semver")` makes one for `src/js`. A namespace that can itself be called, like `Bun.hash`, names the function that a call runs: `b.namespace({ wyhash: { .. }, crc32: { .. } }, { call: "wyhash" })`. The object is then a @@ -316,7 +321,7 @@ The second argument of `b.dictionary` takes: | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `allowArray` | an array is read like any other object, not refused | | `nonObjectAsEmpty` | a value that is not an object gives every member its default; with `"typeof"` so does a function, as in Node's `if (typeof options !== "object") options = null`; every member must be optional or have a default | -| `fromJS` | also generates `Name::from_js(global, value)`, for a `#[host_fn]` body; one that holds JS values is lent to a closure, `Name::from_js(global, value, \|converted\| ..)` | +| `fromJS` | also generates `Name::from_js(global, value)`, for a body that has a `JSValue`; one that holds JS values is lent to a closure, `Name::from_js(global, value, \|converted\| ..)` | | `nullPrototype` | as a result, the object has no prototype, and the inline room of `Object.create(null)` | ### Enumerations diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index 3cab02ccd975..5cde3f73a21c 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -814,13 +814,10 @@ function emitJsModules({ n, cfg, sources, o, dirStamp }: Ctx): void { resolve(cfg.codegenDir, "SyntheticModuleType.h"), resolve(cfg.codegenDir, "BuiltinModuleKeys.h"), resolve(cfg.codegenDir, "GeneratedJS2Native.h"), - // Rust sibling: include!()'d by src/runtime/generated_js2native.rs. Must be - // a declared output: bun_runtime's dep-info names it, and ninja can only - // rebuild that crate in the same build as this step if it knows the file. - resolve(cfg.codegenDir, "generated_js2native.rs"), // Specifier → module-ID tag table: include!()'d by the - // `resolved_source_tag` module in src/jsc/lib.rs. Declared for the same - // reason as generated_js2native.rs. + // `resolved_source_tag` module in src/jsc/lib.rs. Must be a declared + // output: bun_jsc's dep-info names it, and ninja can only rebuild that + // crate in the same build as this step if it knows the file. resolve(cfg.codegenDir, "generated_resolved_source_tag.rs"), // Canonical builtin key -> BuiltinModuleKeys.h index: include!()'d by `builtin_module_key_index` in src/jsc/lib.rs. resolve(cfg.codegenDir, "generated_builtin_module_key_index.rs"), diff --git a/src/cares_sys/c_ares.rs b/src/cares_sys/c_ares.rs index 09b41da74473..172d6c6703c7 100644 --- a/src/cares_sys/c_ares.rs +++ b/src/cares_sys/c_ares.rs @@ -1869,8 +1869,6 @@ pub const ARES_SOCKET_BAD: ares_socket_t = usize::MAX; // INVALID_SOCKET #[cfg(not(windows))] pub const ARES_SOCKET_BAD: ares_socket_t = -1; -// Bun__canonicalizeIP_ host fn: see bun_runtime::dns_jsc::cares_jsc - /// Creates a sockaddr structure from an address, port. /// /// # Parameters diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 862bd716f8c7..45a8e4841a0c 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -391,8 +391,9 @@ export function cppHeader(file: SchemaFile): string { ...file.namespaces.map(cppNamespaceSignature), ...file.types.filter(type => type instanceof ClassType && type.construct).map(cppConstructorSignature), ]; + const exported = file.functions.filter(fn => fn.exportName); const declarations = [ - ...file.functions.map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});`), + ...file.functions.filter(fn => !fn.exportName).map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});`), ...propertyCallbacks.map(signature => `// A PropertyCallback of a static property table.\n${signature};`), ]; return `${banner(file.origin)}#pragma once @@ -402,6 +403,8 @@ export function cppHeader(file: SchemaFile): string { #include "JSDOMConvertDictionary.h" #include "JSDOMConvertEnumeration.h" ${[...headers].map(header => `#include "${header}"\n`).join("")}${file.types.map(cppHeaderForType).join("")}${ + exported.length > 0 ? `\n${exported.map(fn => `BUN_DECLARE_HOST_FUNCTION(${fn.exportName});`).join("\n")}\n` : "" + }${ declarations.length > 0 ? ` namespace Bun::Bindgen::Generated::${file.origin.namespace} { @@ -412,7 +415,7 @@ ${declarations.join("\n")} }`; } -const hostFunctionName = (fn: FunctionDefinition) => `js${cap(fn.name.replace(/^_+/, ""))}`; +const hostFunctionName = (fn: FunctionDefinition) => fn.exportName ?? `js${cap(fn.name.replace(/^_+/, ""))}`; const cppNamespaceSignature = (namespace: NamespaceDefinition) => `JSC::JSValue create${cap(namespace.name)}(JSC::VM& vm, JSC::JSObject* owner)`; @@ -439,6 +442,10 @@ ${namespace.functions return object; } } +extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${origin.stem}_${namespace.name}(Zig::GlobalObject* globalObject) +{ + return JSC::JSValue::encode(Bun::Bindgen::Generated::${origin.namespace}::create${cap(namespace.name)}(JSC::getVM(globalObject), globalObject)); +} `; } @@ -1151,15 +1158,21 @@ const offsetOfPayload = (type: ClassType) => offsetOfInternalField(type.internal const maxCellSize = 8000; /** Defines the host function `name`. `takeThis` declares `thisObject`, which is then the thunks' first argument. */ +/** `passThis` and `passCallee`, which come before the arguments. */ +const cppLeadingOfFunction = (fn: FunctionDefinition) => [ + ...(fn.passThis ? ["JSC::JSValue::encode(callFrame->thisValue())"] : []), + ...(fn.passCallee ? ["JSC::JSValue::encode(callFrame->jsCallee())"] : []), +]; + function cppHostFunction( name: string, fn: FunctionDefinition, symbol: (signature: Signature) => string, takeThis = "", ): string { - const leading = takeThis ? ["thisObject"] : []; + const leading = takeThis ? ["thisObject"] : cppLeadingOfFunction(fn); if (fn.signatures.length === 1) { - return `JSC_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) + return `${fn.exportName ? "BUN" : "JSC"}_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { ${cppPrologue} ${takeThis}${cppCallBody(fn.signatures[0], symbol(fn.signatures[0]), leading)} @@ -1199,14 +1212,15 @@ ${takeThis}${indent(cppDispatch(fn.dispatch), 4)} function cppSourceForFunction(fn: FunctionDefinition): string { const name = hostFunctionName(fn); + const leading = cppLeadingOfFunction(fn).map(() => "JSC::EncodedJSValue"); + assert(fn.signatures.length === 1 || (leading.length === 0 && !fn.exportName), `${fn.name}: not with overloads`); + const hostFunction = cppHostFunction(name, fn, signature => thunkSymbol(fn, signature)); return ` -${fn.signatures.map(signature => cppExtern(thunkSymbol(fn, signature), signature)).join("\n")} -namespace Bun::Bindgen::Generated::${fn.origin!.namespace} { -${cppHostFunction(name, fn, signature => thunkSymbol(fn, signature))} -} +${fn.signatures.map(signature => cppExtern(thunkSymbol(fn, signature), signature, leading)).join("\n")} +${fn.exportName ? hostFunction : `namespace Bun::Bindgen::Generated::${fn.origin!.namespace} {\n${hostFunction}\n}`} extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${fn.origin!.stem}_${fn.name}(Zig::GlobalObject* globalObject) { - return JSC::JSValue::encode(JSC::JSFunction::create(JSC::getVM(globalObject), globalObject, ${fn.length}, ${asciiLiteral(fn.name)}, Bun::Bindgen::Generated::${fn.origin!.namespace}::${name}, JSC::ImplementationVisibility::Public)); + return JSC::JSValue::encode(JSC::JSFunction::create(JSC::getVM(globalObject), globalObject, ${fn.length}, ${asciiLiteral(fn.name)}, ${fn.exportName ?? `Bun::Bindgen::Generated::${fn.origin!.namespace}::${name}`}, JSC::ImplementationVisibility::Public)); } `; } @@ -1306,7 +1320,7 @@ function rustLayoutAssert(rustType: string, type: Type): string { } /** Names the generated thunks use themselves. */ -const reservedArgumentNames = new Set(["global", "this", "structure", "cx", "heap", "result"]); +const reservedArgumentNames = new Set(["global", "this", "this_value", "callee", "structure", "cx", "heap", "result"]); function rustArgumentName(arg: { name: string }): string { const name = snakeCase(arg.name); @@ -1832,27 +1846,38 @@ ${ function rustForFunction(fn: FunctionDefinition, file: SchemaFile): string { const scope = file.origin; - return fn.signatures - .map(signature => { - let path = signature.rustPath; - if (!path) { - assert(fn.signatures.length === 1, `${fn.name}: each overload has a rustPath of its own`); - assert( - file.moduleDefinition, - `${scope.stem}.bind.ts: export default b.module({ rustPath }) or give ${fn.name} a rustPath`, - ); - path = `${file.moduleDefinition.rustPath}::${rustIdent(snakeCase(fn.name))}`; - } - const call = `${path}(${[...capabilityArgument[signature.capability], ...rustArguments(signature)].join(", ")})`; - return rustThunk( - thunkSymbol(fn, signature), - rustExternParameters(signature, scope), - "JSValue", - `${rustTakeArguments(signature.args, scope)}let result = ${infallible(call, signature.throws, signature.returns)}; + const leading = [...(fn.passThis ? ["this_value"] : []), ...(fn.passCallee ? ["callee"] : [])]; + const hostFunction = fn.exportName + ? ` +bun_jsc::jsc_abi_extern! { + pub fn ${fn.exportName}(global: *mut JSGlobalObject, frame: *mut bun_jsc::CallFrame) -> JSValue; +} +` + : ""; + return ( + hostFunction + + fn.signatures + .map(signature => { + let path = signature.rustPath; + if (!path) { + assert(fn.signatures.length === 1, `${fn.name}: each overload has a rustPath of its own`); + assert( + file.moduleDefinition, + `${scope.stem}.bind.ts: export default b.module({ rustPath }) or give ${fn.name} a rustPath`, + ); + path = `${file.moduleDefinition.rustPath}::${rustIdent(snakeCase(fn.name))}`; + } + const call = `${path}(${[...capabilityArgument[signature.capability], ...leading, ...rustArguments(signature)].join(", ")})`; + return rustThunk( + thunkSymbol(fn, signature), + [...leading.map(name => `${name}: JSValue`), ...rustExternParameters(signature, scope)], + "JSValue", + `${leading.map(name => `let ${name} = bindgen::JsLocal::new(&cx.heap, ${name});\n`).join("")}${rustTakeArguments(signature.args, scope)}let result = ${infallible(call, signature.throws, signature.returns)}; bindgen::host_fn_return(cx, result, |heap, value| ${signature.returns.rustToJS("value", scope)})`, - ); - }) - .join(""); + ); + }) + .join("") + ); } /** Once each: two classes of a file may name the same trait. */ @@ -1978,6 +2003,21 @@ ${files .map(file => ` ${JSON.stringify(`${file.origin.stem}.bind.ts`)}: {\n${exported(file).join("\n")}\n };`) .join("\n")} } + /** The namespaces, which it returns too. */ + interface Namespaces { +${files + .filter(file => file.namespaces.length > 0) + .map(file => { + const functions = `Exports[${JSON.stringify(`${file.origin.stem}.bind.ts`)}]`; + return ` ${JSON.stringify(`${file.origin.stem}.bind.ts`)}: {\n${file.namespaces + .map( + namespace => + ` ${JSON.stringify(namespace.name)}: ${namespace.call ? `${functions}[${JSON.stringify(namespace.call.name)}] & ` : ""}Pick<${functions}, ${namespace.functions.map(fn => JSON.stringify(fn.name)).join(" | ")}>;`, + ) + .join("\n")}\n };`; + }) + .join("\n")} + } } `; } diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 60e605a02bd1..358713b9d33e 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1778,6 +1778,16 @@ export interface SignatureOptions { expected?: Record; } +/** What `b.fn` takes, and each function of a `b.namespace`. */ +export interface FunctionOptions extends SignatureOptions { + /** Also pass `this`, `this_value: JsLocal<'a>`. */ + passThis?: boolean; + /** Also pass the function that was called, `callee: JsLocal<'a>`. */ + passCallee?: boolean; + /** The host function has C linkage under this name. C++ declares it with `BUN_DECLARE_HOST_FUNCTION`, and in Rust it is a `JSHostFn` of the generated module. */ + export?: string; +} + export class Signature { readonly args: readonly { name: string; type: Type; expected?: readonly string[] | string }[]; readonly returns: Type; @@ -1865,10 +1875,20 @@ export type OverloadDispatch = Signature | { argument: number; byKind: Map 0, "b.overloads needs at least one signature"); + this.passThis = options.passThis ?? false; + this.passCallee = options.passCallee ?? false; + this.exportName = options.export; + assert( + this.exportName === undefined || /^[A-Za-z_]\w*$/.test(this.exportName), + `export: ${this.exportName} is not a C identifier`, + ); this.signatures = signatures.map(options => (options instanceof Signature ? options : new Signature(options))); assert(this.signatures.filter(signature => signature.length !== undefined).length <= 1, "length is given once"); const nodes = new Map(); @@ -2375,11 +2395,11 @@ export class NamespaceDefinition { readonly functions: readonly FunctionDefinition[]; readonly call: FunctionDefinition | undefined; name = ""; - constructor(functions: Record, options: NamespaceOptions = {}) { + constructor(functions: Record, options: NamespaceOptions = {}) { this.functions = Object.entries(functions).map(([name, options]) => { assert(/^[A-Za-z_$][\w$]*$/.test(name), `${name}: a function is named by an identifier`); assert(!(options instanceof FunctionDefinition), `${name}: write what b.fn takes, without b.fn`); - const fn = new FunctionDefinition([options]); + const fn = new FunctionDefinition([options], options); fn.name = name; return fn; }); diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 9fbaa2ef5f84..f7d21063ddfb 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -15,6 +15,7 @@ import { DoubleType, EnumerationType, FunctionDefinition, + type FunctionOptions, integers, type MemberOptions, ModuleDefinition, @@ -91,13 +92,13 @@ export { class_ as class }; /** The class being declared, in its own signatures: a `JsWrapper<'a, Self>`. */ export { selfType as self } from "./bindgen-lib-internal.ts"; -export const fn = (signature: SignatureOptions) => new FunctionDefinition([signature]); +export const fn = (options: FunctionOptions) => new FunctionDefinition([options], options); /** One JS function, several signatures, told apart by the kinds of their arguments. */ export const overloads = (...signatures: SignatureOptions[]) => new FunctionDefinition(signatures); /** `export const semver = b.namespace({ satisfies: { args, returns }, .. })`: an object that holds those functions, or with `{ call: "satisfies" }` a function that does what that one does. */ -export const namespace = (functions: Record, options?: NamespaceOptions) => +export const namespace = (functions: Record, options?: NamespaceOptions) => new NamespaceDefinition(functions, options); /** `export default b.module({ rustPath: "crate::node::os" })`: where the file's Rust functions are. */ diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index f6d1f8370c55..75346e5a5680 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -15,7 +15,7 @@ import path from "path"; import jsclasses from "./../jsc/bindings/js_classes"; import { sliceSourceCode } from "./builtin-parser"; import { createAssertClientJS, createLogClientJS } from "./client-js"; -import { getJS2NativeCPP, getJS2NativeRust } from "./generate-js2native"; +import { getJS2NativeCPP } from "./generate-js2native"; import { cap, checkAscii, engineStamp, sourceStamp, writeIfNotChanged, writeIfNotChangedBinary } from "./helpers"; import { createInternalModuleRegistry } from "./internal-module-registry-scanner"; import { define } from "./replacements"; @@ -749,9 +749,6 @@ writeIfNotChanged( writeIfNotChanged(path.join(CODEGEN_DIR, "GeneratedJS2Native.h"), getJS2NativeCPP()); -// Rust sibling: include!()'d by src/runtime/generated_js2native.rs -writeIfNotChanged(path.join(CODEGEN_DIR, "generated_js2native.rs"), getJS2NativeRust()); - const generatedDTSPath = path.join(TYPES_DIR, "generated.d.ts"); writeIfNotChanged( generatedDTSPath, diff --git a/src/codegen/generate-host-exports.ts b/src/codegen/generate-host-exports.ts index f9ea36436bbf..d1c6bde4158e 100644 --- a/src/codegen/generate-host-exports.ts +++ b/src/codegen/generate-host-exports.ts @@ -9,14 +9,12 @@ // // Marker grammar (one line, immediately preceding `pub fn`): // -// // HOST_EXPORT(Bun__drainMicrotasksFromJS) -// pub fn drain_microtasks_from_js(global: &JSGlobalObject, _cf: &CallFrame) -// -> JsResult { … } +// // HOST_EXPORT(BunObject__createBunStdin) +// pub fn create_bun_stdin(global: &JSGlobalObject) -> JSValue { … } // // Optional second arg selects the calling convention: -// - `jsc` (default for `(global, callframe) -> JSValue` shape) → JSC sysv64 -// on win-x64, `extern "C"` elsewhere; dispatched via `host_fn::host_fn_static`. -// - `c` → plain `extern "C"`; raw pointer thunk, no panic barrier. +// - `jsc` → JSC sysv64 on win-x64, `extern "C"` elsewhere. +// - `c` (default) → plain `extern "C"`; raw pointer thunk, no panic barrier. // - `rust` → `extern "Rust"`; for link-time `extern "Rust" {}` consumers // (cycle-breaking hooks). No pointer rewriting — signature is forwarded // verbatim. @@ -25,8 +23,6 @@ // // shape impl signature → thunk // ──────────── ───────────────────────────────────────────────────────────────── -// host (&JSGlobalObject, &CallFrame) -> JsResult|JSValue -// → host_fn::host_fn_static(g, cf, path::name) // lazy (&JSGlobalObject) -> JsResult|JSValue // → host_fn::host_fn_lazy(g, path::name) // generic (T0, T1, …) -> R where Tn ∈ {&X, &mut X, *T, scalar} @@ -121,7 +117,7 @@ interface Export { modPath: string; // `crate::…` (relative to bun_runtime) or `bun_jsc::…` params: Param[]; ret: string; - shape: "host" | "reaction" | "lazy" | "generic" | "rust"; + shape: "lazy" | "generic" | "rust"; isUnsafe: boolean; } @@ -279,26 +275,7 @@ for (const { dir, crate } of scanRoots) { const isJsRet = ret === "JSValue" || /^(?:bun_jsc::)?JsResult\s*<\s*JSValue\s*>$/.test(ret); let shape: Export["shape"]; if (abi === "rust") shape = "rust"; - else if ( - params.length === 3 && - /^(?:(?:::)?bun_ptr::)?ThisPtr\s*$/.test(e.ret); switch (e.shape) { - case "host": { - // JSC host fn: `(g, cf) -> JSValue`. Always JSC ABI — these are - // dispatched through `JSC::JSFunction` (`BUN_DECLARE_HOST_FUNCTION`). - // Keep raw `*mut` params so the symbol coerces to `host_fn::JSHostFn` - // when Rust code passes it as a callback (e.g. `JSValue::then2`). The - // thunk is `pub unsafe extern fn` (matches `JSHostFn`'s `unsafe` - // qualifier) and routes through the `_raw` helper that derefs under a - // documented safety contract — so no safe `pub fn` derefs a raw ptr. - const body = retIsJsResult - ? ` // SAFETY: JSC trampoline guarantees g/cf are non-null and valid.\n unsafe { host_fn::host_fn_static_raw(g, cf, ${impl}) }` - : ` // SAFETY: JSC trampoline guarantees g/cf are non-null and valid.\n unsafe { host_fn::host_fn_static_passthrough_raw(g, cf, ${impl}) }`; - return ` -// ${loc} -${emitNoMangle(e.abi, e.symbol, "g: *mut JSGlobalObject, cf: *mut CallFrame", "JSValue", body, /*unsafeFn*/ true)}`; - } - case "reaction": { - const wrap = retIsJsResult ? "host_fn::host_fn_static_raw" : "host_fn::host_fn_static_passthrough_raw"; - const body = ` // SAFETY: JSC trampoline guarantees g/cf are non-null and valid; the - // trailing argument is the \`ctx\` this reaction was registered with via - // \`JSValue::then\`, on which the registrant holds a ref until it runs. - unsafe { - ${wrap}(g, cf, |g, cf| { - let args = cf.arguments(); - let this = ::bun_ptr::ThisPtr::new(args[args.len() - 1].as_promise_ptr()); - ${impl}(this, g, cf) - }) - }`; - return ` -// ${loc} -${emitNoMangle(e.abi, e.symbol, "g: *mut JSGlobalObject, cf: *mut CallFrame", "JSValue", body, /*unsafeFn*/ true)}`; - } case "lazy": { // Lazy property creator: `(g) -> JSValue`. ABI is whatever the C++ decl // uses — `e.abi` (default `c` for direct `extern "C"` calls; `jsc` for @@ -531,8 +476,7 @@ const header = `// Auto-generated by src/codegen/generate-host-exports.ts — DO // // Centralised \`#[unsafe(no_mangle)] extern "C"\` thunks for hand-written host // exports. Each thunk derefs the raw C pointers and calls the safe-signature -// \`pub fn …_impl(&JSGlobalObject, &CallFrame) -> JsResult\` (or -// shape-appropriate variant) via \`bun_jsc::host_fn::*\`, so the source files +// \`pub fn\` via \`bun_jsc::host_fn::*\`, so the source files // contain zero \`#[no_mangle]\` and zero raw-pointer-deref boilerplate. // // Adding an export: put \`// HOST_EXPORT(SymbolName)\` on the line above the @@ -553,7 +497,6 @@ const importCandidates: Array<[string, string]> = [ ["core::ffi", "c_int"], ["core::ffi", "c_void"], ["bun_jsc", "host_fn"], - ["bun_jsc", "CallFrame"], ["bun_jsc", "JSGlobalObject"], ["bun_jsc", "JSValue"], ["bun_jsc", "JsResult"], @@ -587,8 +530,7 @@ writeIfNotChanged(path.join(outBase, "generated_host_exports.rs"), header + impo console.log( `generated_host_exports.rs: ${exportsFound.length} exports ` + - `(host=${exportsFound.filter(e => e.shape === "host" || e.shape === "reaction").length}, ` + - `lazy=${exportsFound.filter(e => e.shape === "lazy").length}, ` + + `(lazy=${exportsFound.filter(e => e.shape === "lazy").length}, ` + `generic=${exportsFound.filter(e => e.shape === "generic").length}, ` + `rust=${exportsFound.filter(e => e.shape === "rust").length}); ` + `${externTotal} extern-C blocks audited`, diff --git a/src/codegen/generate-js2native.ts b/src/codegen/generate-js2native.ts index 673eda86e66f..d291a33493b2 100644 --- a/src/codegen/generate-js2native.ts +++ b/src/codegen/generate-js2native.ts @@ -1,5 +1,5 @@ -// This file implements the global state for $rust and $cpp preprocessor macros -// as well as all the code it generates. +// This file implements the global state for the $cpp and $bindgen preprocessor macros +// as well as all the code they generate. // // For the actual parsing, see replacements.ts @@ -25,7 +25,7 @@ interface WrapperCall { filename: string; } -type NativeCallType = "rust" | "cpp" | "bind"; +type NativeCallType = "cpp" | "bind"; const nativeCalls: NativeCall[] = []; const wrapperCalls: WrapperCall[] = []; @@ -38,83 +38,16 @@ const sourceFiles = readdirRecursiveWithExclusionsAndExtensionsSync( [".cpp", ".bind.ts"], ); -// The $rust() macro's first argument is an identifier naming the module a -// native symbol belongs to. The file itself is never opened, but its path -// under src/ drives both the exported C symbol name (normalizeSymbolPathPrefix) -// and the Rust crate path (rustTarget). Adding a new $rust() call site -// requires adding its entry below. -const rustIdentifierPaths: Record = { - "bake/config/plugins.rs": "runtime/bake/config/plugins.rs", - "ipc.rs": "runtime/ipc_host.rs", - "Counters.rs": "jsc/Counters.rs", - "FrameworkRouter.rs": "runtime/bake/router/js.rs", - "JSBundler.rs": "runtime/api/JSBundler.rs", - "MarkdownObject.rs": "runtime/api/MarkdownObject.rs", - "SecureContext.rs": "runtime/api/bun/SecureContext.rs", - "SemverObject.rs": "semver_jsc/SemverObject.rs", - "Stat.rs": "runtime/node/Stat.rs", - "collections/linear_fifo.rs": "collections/linear_fifo.rs", - "crash_handler.rs": "crash_handler/crash_handler.rs", - "css_internals.rs": "css_jsc/css_internals.rs", - "BytecodeOrderRecorder.rs": "jsc/BytecodeOrderRecorder.rs", - "escapeRegExp.rs": "string/escapeRegExp.rs", - "event_loop.rs": "jsc/event_loop.rs", - "ffi.rs": "runtime/ffi/ffi.rs", - "hosted_git_info.rs": "install/hosted_git_info.rs", - "http/H2Client.rs": "http/H2Client.rs", - "http/H3Client.rs": "http/H3Client.rs", - "ini.rs": "ini/ini.rs", - "install_binding.rs": "install_jsc/install_binding.rs", - "memory_pressure.rs": "runtime/node/memory_pressure.rs", - "mysql.rs": "sql_jsc/mysql.rs", - "napi_body.rs": "runtime/napi/napi_body.rs", - "node_cluster_binding.rs": "runtime/node/node_cluster_binding.rs", - "node_crypto_binding.rs": "runtime/node/node_crypto_binding.rs", - "node_fs_binding.rs": "runtime/node/node_fs_binding.rs", - "node_http_binding.rs": "runtime/node/node_http_binding.rs", - "node_net_binding.rs": "runtime/node/node_net_binding.rs", - "node_quic_binding.rs": "runtime/node/node_quic_binding.rs", - "node_zlib_binding.rs": "runtime/node/node_zlib_binding.rs", - "npm.rs": "install/npm.rs", - "pack_command.rs": "runtime/cli/pack_command.rs", - "parse_args.rs": "runtime/node/util/parse_args.rs", - "patch.rs": "patch/patch.rs", - "postgres.rs": "sql_jsc/postgres.rs", - "runtime/dns_jsc/dns.rs": "runtime/dns_jsc/dns.rs", - "runtime/socket/socket.rs": "runtime/socket/socket.rs", - "runtime/timer/Timer.rs": "runtime/timer/Timer.rs", - "runtime/webcore/ByteStream.rs": "runtime/webcore/ByteStream.rs", - "runtime/webcore/FileSink.rs": "runtime/webcore/FileSink.rs", - "runtime/webcore/fetch.rs": "runtime/webcore/fetch.rs", - "shell.rs": "runtime/shell/shell.rs", - "sourcemap/InternalSourceMap.rs": "sourcemap/InternalSourceMap.rs", - "string/immutable/unicode.rs": "bun_core/string/immutable/unicode.rs", - "subprocess.rs": "runtime/api/bun/subprocess.rs", - "sys.rs": "sys/sys.rs", - "upgrade_command.rs": "runtime/cli/upgrade_command.rs", - "virtual_machine_exports.rs": "jsc/virtual_machine_exports.rs", -}; - function callBaseName(x: string) { return x.split(/[^A-Za-z0-9]/g).pop()!; } function resolveNativeFileId(call_type: NativeCallType, filename: string) { - const ext = call_type === "bind" ? ".bind.ts" : call_type === "rust" ? ".rs" : `.${call_type}`; + const ext = call_type === "bind" ? ".bind.ts" : `.${call_type}`; if (!filename.endsWith(ext)) { throw new Error(`Expected filename for $${call_type} to have ${ext} extension, got ${JSON.stringify(filename)}`); } - if (call_type === "rust") { - const relative = rustIdentifierPaths[filename]; - if (!relative) { - throw new Error( - `Unknown $rust() file identifier ${JSON.stringify(filename)}. Add it to rustIdentifierPaths in src/codegen/generate-js2native.ts.`, - ); - } - return path.join(srcDir, relative.replaceAll("/", sep)); - } - filename = filename.replaceAll("/", sep); const resolved = sourceFiles.find(file => file.endsWith(sep + filename)); @@ -168,62 +101,16 @@ export function registerNativeCall( return id; } -function symbol(call: Pick) { - return call.type === "rust" - ? `JS2Rust__${call.filename ? normalizeSymbolPathPrefix(call.filename) + "_" : ""}${call.symbol.replace(/[^A-Za-z]/g, "_")}` - : call.symbol; -} - -function normalizeSymbolPathPrefix(input: string) { - input = path.resolve(input); - - const bunDir = path.resolve(path.join(import.meta.dir, "..", "..")); - if (input.startsWith(bunDir)) { - input = input.slice(bunDir.length); - } - - return input.replaceAll(".rs", "_rs_").replace(/[^A-Za-z]/g, "_"); -} - export function getJS2NativeCPP() { const files = [ ...new Set(nativeCalls.filter(x => x.filename.endsWith(".cpp")).map(x => x.filename.replace(/.cpp$/, ".h"))), ]; - const externs: string[] = []; - - const nativeCallStrings = nativeCalls - .filter(x => x.type === "rust") - .flatMap( - call => ( - externs.push(`extern "C" SYSV_ABI JSC::EncodedJSValue ${symbol(call)}_workaround(Zig::GlobalObject*);` + "\n"), - [ - `static ALWAYS_INLINE JSC::JSValue ${symbol(call)}(Zig::GlobalObject* global) {`, - ` return JSValue::decode(${symbol(call)}_workaround(global));`, - `}` + "\n\n", - ] - ), - ); - const wrapperCallStrings = wrapperCalls.map(x => { if (x.wrap_kind === "new-function") { return [ - (x.type === "rust" && - externs.push( - `BUN_DECLARE_HOST_FUNCTION(${symbol({ - type: "rust", - symbol: x.symbol_target, - filename: x.filename, - })});`, - ), - "") || "", `static JSC::JSValue ${x.symbol_generated}(Zig::GlobalObject* globalObject) {`, - ` return js2nativeNewFunction(globalObject, ${x.call_length}, ${JSON.stringify(x.display_name)}_s, ${symbol({ - type: x.type, - symbol: x.symbol_target, - - filename: x.filename, - })});`, + ` return js2nativeNewFunction(globalObject, ${x.call_length}, ${JSON.stringify(x.display_name)}_s, ${x.symbol_target});`, `}`, ].join("\n"); } @@ -234,7 +121,6 @@ export function getJS2NativeCPP() { `#pragma once`, `#include "root.h"`, ...files.map(filename => `#include ${JSON.stringify(filename)}`), - ...externs, "\n" + "namespace JS2NativeGenerated {", "using namespace Bun;", "using namespace JSC;", @@ -243,7 +129,6 @@ export function getJS2NativeCPP() { `static NEVER_INLINE JSC::JSValue js2nativeNewFunction(Zig::GlobalObject* globalObject, unsigned length, ASCIILiteral name, JSC::NativeFunction function) {`, ` return JSC::JSFunction::create(globalObject->vm(), globalObject, length, name, function, JSC::ImplementationVisibility::Public);`, `}` + "\n", - ...nativeCallStrings, ...wrapperCallStrings, ...nativeCalls .filter(x => x.type === "bind") @@ -259,7 +144,7 @@ export function getJS2NativeCPP() { ` case ${x.id}: return ${ x.type === "bind" ? `JSC::JSValue::decode(js2native_bindgen_${basename(x.filename.replace(/\.bind\.ts$/, ""))}_${x.symbol}(global))` - : `${symbol(x)}(global)` + : `${x.symbol}(global)` };`, ), ` default:`, @@ -271,127 +156,6 @@ export function getJS2NativeCPP() { ].join("\n"); } -// Rust emitter. -// -// Emits, for every $rust() call site, a `#[unsafe(no_mangle)] extern "C"` -// thunk whose unmangled name and signature is byte-identical to the extern -// the C++ side declares in GeneratedJS2Native.h. The C++ output is invariant; -// only the implementer of the symbol changes. -// -// Two ABI shapes: -// • nativeCalls (type "rust") → `${sym}_workaround(global) -> JSValue` -// • wrapperCalls (type "rust") → `${sym}(global, callframe) -> JSValue` -// -// Each thunk calls the Rust function directly at -// `crate::::` — no trait, no runtime -// panic fallback. A missing function is a compile error. -export function getJS2NativeRust() { - // Symbols already hand-exported in src/ (via `export_host_fn!` or - // `#[unsafe(export_name = "JS2Rust__…")]`) — skip emitting a thunk for these - // so the linker doesn't see two definitions. - const handExported = new Set([ - "JS2Rust___src_runtime_dns_jsc_dns_rs__Resolver_newResolver", - "JS2Rust___src_runtime_dns_jsc_dns_rs__internal_seedCacheForTesting", - "JS2Rust___src_runtime_dns_jsc_dns_rs__internal_isLocalhostNameForTesting", - "JS2Rust___src_runtime_dns_jsc_dns_rs__internal_isAllLoopbackOfOneFamilyForTesting", - "JS2Rust___src_runtime_dns_jsc_dns_rs__internal_getaddrinfoErrorForTesting", - ]); - - const srcRoot = path.resolve(import.meta.dir, ".."); - const snake = (s: string) => - s - .replace(/([a-z0-9])([A-Z])/g, "$1_$2") - .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") - .replace(/[.\-]/g, "_") - .toLowerCase(); - - // `src/runtime/node/util/parse_args.rs` + `parseArgs` → `crate::node::util::parse_args::parse_args` - // `src/ini/ini.rs` + `IniTestingAPIs.parse` (outside bun_runtime) - // → `crate::dispatch::js2native::ini_ini_testing_apis_parse` (single - // landing pad; still a compile error if missing). - const rustTarget = (filename: string, sym: string) => { - const rel = path.relative(srcRoot, filename).replace(/\.rs$/, ""); - const segs = rel.split(path.sep); - const fn = sym - .split(".") - .map(s => snake(s)) - .join("::"); - if (segs[0] === "runtime") { - const mod = segs - .slice(1) - .map(s => snake(s)) - .join("::"); - return `crate::${mod}::${fn}`; - } - // Out-of-crate call site: route through a flat dispatch module that - // re-exports the real impl from its owning crate. - return `crate::dispatch::js2native::${snake(segs.join("_"))}_${fn.replace(/::/g, "_")}`; - }; - - const thunks: string[] = []; - const seen = new Set(); - - for (const call of nativeCalls.filter(x => x.type === "rust")) { - const sym = `${symbol(call)}_workaround`; - if (seen.has(sym)) continue; - seen.add(sym); - const target = rustTarget(call.filename, call.symbol); - thunks.push( - `// $rust(${path.basename(call.filename)}, ${call.symbol})`, - `bun_jsc::jsc_host_abi! {`, - ` #[allow(dead_code, unreachable_pub, unused)]`, - ` #[unsafe(no_mangle)]`, - ` pub unsafe fn ${sym}(global: &JSGlobalObject) -> JSValue {`, - ` host_fn::host_fn_lazy(global, |g| ${target}(g))`, - ` }`, - `}`, - ``, - ); - } - - for (const x of wrapperCalls.filter(x => x.type === "rust")) { - const sym = symbol({ type: "rust", symbol: x.symbol_target, filename: x.filename }); - if (seen.has(sym)) continue; - seen.add(sym); - if (handExported.has(sym)) { - thunks.push(`// ${sym}: hand-exported in src/ — thunk omitted to avoid duplicate symbol`, ``); - continue; - } - const target = rustTarget(x.filename, x.symbol_target); - thunks.push( - `// $rust(${path.basename(x.filename)}, ${x.symbol_target})`, - `bun_jsc::jsc_host_abi! {`, - ` #[allow(dead_code, unreachable_pub, unused)]`, - ` #[unsafe(no_mangle)]`, - ` pub unsafe fn ${sym}(global: &JSGlobalObject, callframe: &CallFrame) -> JSValue {`, - ` host_fn::host_fn_static(global, callframe, |g, cf| ${target}(g, cf))`, - ` }`, - `}`, - ``, - ); - } - - return [ - `// Auto-generated by src/codegen/generate-js2native.ts — DO NOT EDIT.`, - `//`, - `// \`#[unsafe(no_mangle)] extern "C"\` thunks satisfying the JS2Rust__* externs`, - `// declared by GeneratedJS2Native.h (the JS-module → native dispatch table).`, - `// Each thunk calls the hand-ported Rust function directly; a missing`, - `// function is a compile error in \`cargo check -p bun_runtime\`.`, - `//`, - `// Calling convention: \`jsc.conv\` is plain \`extern "C"\` on every target except`, - `// Windows-x64 (\`extern "sysv64"\`).`, - ``, - `#[allow(dead_code, unreachable_pub, unused)]`, - `#[allow(unused_imports)] // emitted for thunk shapes that vary per build`, - `use bun_jsc::{self, host_fn, CallFrame, JSGlobalObject, JSValue, JsError, JsResult};`, - ``, - ...thunks, - `// exported symbols: ${seen.size}`, - ``, - ].join("\n"); -} - export function getJS2NativeDTS() { return [ "declare type NativeBasenameCPP = " + @@ -401,10 +165,6 @@ export function getJS2NativeDTS() { .join("|"), // resolveNativeFileId matches any path suffix of a source file, e.g. "streams/BunStreamConsumers.cpp". "declare type NativeFilenameCPP = NativeBasenameCPP | `${string}/${NativeBasenameCPP}`", - "declare type NativeFilenameRust = " + - Object.keys(rustIdentifierPaths) - .map(x => JSON.stringify(x)) - .join("|"), "", ].join("\n"); } diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index a4770d342dfa..4ad9002d5287 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -165,8 +165,6 @@ export interface ReplacementRule { export const function_replacements = [ "$debug", "$assert", - "$rust", - "$newRustFunction", "$cpp", "$newCppFunction", "$isPromiseFulfilled", @@ -222,8 +220,7 @@ export function applyReplacements(src: string, length: number) { rest2, true, ]; - } else if (["rust", "cpp", "newRustFunction", "newCppFunction"].includes(name)) { - const kind = name.includes("ust") ? "rust" : "cpp"; + } else if (["cpp", "newCppFunction"].includes(name)) { const is_create_fn = name.startsWith("new"); const inner = sliceSourceCode(rest, true); @@ -253,7 +250,7 @@ export function applyReplacements(src: string, length: number) { } } - const id = registerNativeCall(kind, args[0], args[1], is_create_fn ? args[2] : null); + const id = registerNativeCall("cpp", args[0], args[1], is_create_fn ? args[2] : null); return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; } else if (name === "isPromiseFulfilled" || name === "isPromiseRejected" || name === "isPromisePending") { diff --git a/src/css_jsc/build.rs b/src/css_jsc/build.rs new file mode 100644 index 000000000000..944892265516 --- /dev/null +++ b/src/css_jsc/build.rs @@ -0,0 +1,35 @@ +// Build scripts run on the host before bun_* crates are compiled; std is the only option. +#![allow( + clippy::disallowed_methods, + clippy::disallowed_types, + clippy::disallowed_macros +)] +//! Export `BUN_CODEGEN_DIR` for `generated_bindgen.rs`, which `include!`s what `src/codegen/bindgen.ts` writes. + +use std::env; +use std::path::{Path, PathBuf}; + +fn main() { + let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let repo = manifest + .parent() + .and_then(Path::parent) + .expect("repo root from CARGO_MANIFEST_DIR") + .to_path_buf(); + + let codegen_dir = env::var("BUN_CODEGEN_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| repo.join("build/debug/codegen")); + + let generated = codegen_dir.join("generated_bindgen_bun_css_jsc.rs"); + if !generated.exists() { + panic!( + "generated_bindgen_bun_css_jsc.rs not found at {} — run `bun bd` first", + generated.display() + ); + } + + println!("cargo:rerun-if-changed={}", generated.display()); + println!("cargo:rustc-env=BUN_CODEGEN_DIR={}", codegen_dir.display()); + println!("cargo:rerun-if-env-changed=BUN_CODEGEN_DIR"); +} diff --git a/src/css_jsc/color_js.bind.ts b/src/css_jsc/color_js.bind.ts new file mode 100644 index 000000000000..2caba36ff439 --- /dev/null +++ b/src/css_jsc/color_js.bind.ts @@ -0,0 +1,10 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::color_js" }); + +export const color = b.fn({ + args: { input: b.any, format: b.any }, + returns: b.any, + capability: "js", + throws: true, +}); diff --git a/src/css_jsc/color_js.rs b/src/css_jsc/color_js.rs index 86ccdb49e781..17a76ef8f2bf 100644 --- a/src/css_jsc/color_js.rs +++ b/src/css_jsc/color_js.rs @@ -4,8 +4,9 @@ use bun_alloc::Arena; use bun_ast::Log; use bun_core::String as BunString; use bun_core::output::{ColorDepth, Source as OutputSource}; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::bun_string_jsc; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue}; +use bun_jsc::{JSGlobalObject, JSValue}; use crate::JsResult; @@ -210,7 +211,11 @@ fn zero_if_none(component: f32) -> f32 { if component.is_nan() { 0.0 } else { component } } -pub fn js_function_color(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn color<'a>( + cx: &mut JsScope<'a>, + input: JsLocal<'a>, + format: JsLocal<'a>, +) -> JsResult { use bun_ast::symbol::Map as SymbolMap; use bun_core::Utf8Bytes; use bun_css as css; @@ -218,7 +223,8 @@ pub fn js_function_color(global: &JSGlobalObject, frame: &CallFrame) -> JsResult use bun_css::values::color::{HSL, LAB, RGBA, SRGB}; use bun_jsc::StringJsc as _; - let args = frame.arguments_as_array::<2>(); + let global = cx.global(); + let args = [input.to_js(), format.to_js()]; if args[0].is_undefined() { return Err(global.throw_invalid_argument_type( "color", diff --git a/src/css_jsc/css_internals.bind.ts b/src/css_jsc/css_internals.bind.ts new file mode 100644 index 000000000000..de9145643b0e --- /dev/null +++ b/src/css_jsc/css_internals.bind.ts @@ -0,0 +1,28 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::css_internals" }); + +/** `options` is the browser targets, or the parser's options for a `WithOptions` function. */ +const stylesheetTest = () => + b.fn({ + args: { source: b.string, expected: b.string, options: b.any }, + returns: b.any, + capability: "js", + throws: true, + }); + +export const minifyTestWithOptions = stylesheetTest(); +export const minifyErrorTestWithOptions = stylesheetTest(); +export const testWithOptions = stylesheetTest(); +export const prefixTestWithOptions = stylesheetTest(); +export const minifyTest = stylesheetTest(); +export const prefixTest = stylesheetTest(); +export const test = stylesheetTest(); + +export const attrTest = b.fn({ + args: { source: b.string, expected: b.string, minify: b.any, targets: b.any }, + returns: b.any, + capability: "js", + throws: true, + length: 3, +}); diff --git a/src/css_jsc/css_internals.rs b/src/css_jsc/css_internals.rs index a7141d16c9f6..13c4cc82498e 100644 --- a/src/css_jsc/css_internals.rs +++ b/src/css_jsc/css_internals.rs @@ -1,8 +1,10 @@ use bun_alloc::Arena; // bumpalo::Bump re-export use bun_ast::Log; +use bun_core::String as BunString; use bun_css::targets::{Browsers, Targets}; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::bun_string_jsc; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue}; +use bun_jsc::{JSGlobalObject, JSValue}; use crate::JsResult; @@ -23,70 +25,123 @@ pub(crate) enum TestCategory { ParserOptions, } -// These test-only wrappers are consumed as plain safe fns through -// `dispatch_js2native.rs` re-exports, so they don't use the -// `#[bun_jsc::host_fn]` C-ABI shim. - -pub fn minify_error_test_with_options( - global: &JSGlobalObject, - frame: &CallFrame, +pub(crate) fn minify_error_test_with_options<'a>( + cx: &mut JsScope<'a>, + source: &BunString, + expected: &BunString, + options: JsLocal<'a>, ) -> JsResult { - testing_impl(global, frame, TestKind::Minify, TestCategory::ParserOptions) + testing_impl( + cx.global(), + source, + expected, + options.to_js(), + TestKind::Minify, + TestCategory::ParserOptions, + ) } -pub fn minify_test_with_options(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - testing_impl(global, frame, TestKind::Minify, TestCategory::ParserOptions) -} - -pub fn prefix_test_with_options(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - testing_impl(global, frame, TestKind::Prefix, TestCategory::ParserOptions) +pub(crate) fn minify_test_with_options<'a>( + cx: &mut JsScope<'a>, + source: &BunString, + expected: &BunString, + options: JsLocal<'a>, +) -> JsResult { + testing_impl( + cx.global(), + source, + expected, + options.to_js(), + TestKind::Minify, + TestCategory::ParserOptions, + ) } -pub fn test_with_options(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - testing_impl(global, frame, TestKind::Normal, TestCategory::ParserOptions) +pub(crate) fn prefix_test_with_options<'a>( + cx: &mut JsScope<'a>, + source: &BunString, + expected: &BunString, + options: JsLocal<'a>, +) -> JsResult { + testing_impl( + cx.global(), + source, + expected, + options.to_js(), + TestKind::Prefix, + TestCategory::ParserOptions, + ) } -pub fn minify_test(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - testing_impl(global, frame, TestKind::Minify, TestCategory::Normal) +pub(crate) fn test_with_options<'a>( + cx: &mut JsScope<'a>, + source: &BunString, + expected: &BunString, + options: JsLocal<'a>, +) -> JsResult { + testing_impl( + cx.global(), + source, + expected, + options.to_js(), + TestKind::Normal, + TestCategory::ParserOptions, + ) } -pub fn prefix_test(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - testing_impl(global, frame, TestKind::Prefix, TestCategory::Normal) +pub(crate) fn minify_test<'a>( + cx: &mut JsScope<'a>, + source: &BunString, + expected: &BunString, + options: JsLocal<'a>, +) -> JsResult { + testing_impl( + cx.global(), + source, + expected, + options.to_js(), + TestKind::Minify, + TestCategory::Normal, + ) } -pub fn _test(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - testing_impl(global, frame, TestKind::Normal, TestCategory::Normal) +pub(crate) fn prefix_test<'a>( + cx: &mut JsScope<'a>, + source: &BunString, + expected: &BunString, + options: JsLocal<'a>, +) -> JsResult { + testing_impl( + cx.global(), + source, + expected, + options.to_js(), + TestKind::Prefix, + TestCategory::Normal, + ) } -/// Shared arg-validation for the test-only CSS internals: pulls the next arg, -/// throws "{fn_name}: expected {expected_n} arguments, got {got_n}" if absent, -/// throws "{fn_name}: expected {arg_label} to be a string" if not a string, -/// otherwise returns the +1-ref `bun_core::String`. -/// Caller does `.to_utf8()` (borrows the bun_core::String, so can't be returned here). -fn eat_string_arg( - arguments: &mut bun_jsc::ArgumentsSlice<'_>, - global: &JSGlobalObject, - fn_name: &str, - expected_n: u32, - got_n: u32, - arg_label: &str, -) -> JsResult { - let Some(arg) = arguments.next_eat() else { - return Err(global.throw(format_args!( - "{fn_name}: expected {expected_n} arguments, got {got_n}" - ))); - }; - if !arg.is_string() { - return Err(global.throw(format_args!( - "{fn_name}: expected {arg_label} to be a string" - ))); - } - arg.to_bun_string(global) +pub(crate) fn test<'a>( + cx: &mut JsScope<'a>, + source: &BunString, + expected: &BunString, + options: JsLocal<'a>, +) -> JsResult { + testing_impl( + cx.global(), + source, + expected, + options.to_js(), + TestKind::Normal, + TestCategory::Normal, + ) } fn testing_impl( global: &JSGlobalObject, - frame: &CallFrame, + source: &BunString, + expected: &BunString, + options: JSValue, test_kind: TestKind, test_category: TestCategory, ) -> JsResult { @@ -106,30 +161,8 @@ fn testing_impl( // out of it below. let alloc: &'static Arena = unsafe { bun_ptr::detach_lifetime_ref(&arena) }; - // SAFETY: bunVM() never returns null for a Bun-owned global; reborrow the - // raw `*mut VirtualMachine` as a shared ref for the slice's lifetime. - let mut arguments = bun_jsc::ArgumentsSlice::init(global.bun_vm(), frame.arguments()); - let source_bunstr = eat_string_arg( - &mut arguments, - global, - "minifyTestWithOptions", - 2, - 0, - "source", - )?; - let source = source_bunstr.to_utf8(); - - let expected_bunstr = eat_string_arg( - &mut arguments, - global, - "minifyTestWithOptions", - 2, - 1, - "`expected` arg", - )?; - let _expected = expected_bunstr.to_utf8(); - - let browser_options_arg = arguments.next_eat(); + let source = source.to_utf8(); + let _expected = expected.to_utf8(); let mut log = Log::init(); @@ -140,17 +173,13 @@ fn testing_impl( match test_category { TestCategory::Normal => { - if let Some(optargs) = browser_options_arg { - if optargs.is_object() { - browsers = Some(targets_from_js(global, optargs)?); - } + if options.is_object() { + browsers = Some(targets_from_js(global, options)?); } } TestCategory::ParserOptions => { - if let Some(optargs) = browser_options_arg { - if optargs.is_object() { - parser_options_from_js(global, &arena, &mut opts, optargs)?; - } + if options.is_object() { + parser_options_from_js(global, &arena, &mut opts, options)?; } } } @@ -276,7 +305,13 @@ fn targets_from_js(global: &JSGlobalObject, jsobj: JSValue) -> JsResult JsResult { +pub(crate) fn attr_test<'a>( + cx: &mut JsScope<'a>, + source: &BunString, + expected: &BunString, + minify: JsLocal<'a>, + targets: JsLocal<'a>, +) -> JsResult { use bun_ast::ImportRecord; use bun_css::{ ImportRecordHandler, MinifyOptions, ParserOptions, PrinterOptions, StyleAttribute, @@ -293,25 +328,17 @@ pub fn attr_test(global: &JSGlobalObject, frame: &CallFrame) -> JsResult, is_url_owned: bool) { return; } - // Write-before-read: `Bun__fetchPreconnect` reaches here without going + // Write-before-read: `fetch.preconnect` reaches here without going // through any path that calls `HTTPThread::init`, so `schedule()` below // would deref the uninitialized `HTTP_THREAD` static (UB on niche-bearing // fields) if `fetch.preconnect()` is the process's first HTTP operation. diff --git a/src/http/H2Client.rs b/src/http/H2Client.rs index a946e78c8fd4..09e0ea121ab1 100644 --- a/src/http/H2Client.rs +++ b/src/http/H2Client.rs @@ -39,7 +39,7 @@ pub(crate) const WRITE_BUFFER_CONTROL_LIMIT: usize = 1024 * 1024; /// Live-object counters for the leak test in fetch-http2-leak.test.ts. /// Incremented at allocation, decremented in deinit. Read from the JS thread -/// via TestingAPIs.liveCounts so they must be atomic. +/// via `bun_http_jsc`'s `h2_live_counts` so they must be atomic. // Lower-case names kept so cross-crate readers (`bun_http_jsc`) and the gated // submodules share one identifier; SCREAMING_SNAKE alias preserved for the // existing internal reference. diff --git a/src/http/H3Client.rs b/src/http/H3Client.rs index 782443608a44..76fefd892a62 100644 --- a/src/http/H3Client.rs +++ b/src/http/H3Client.rs @@ -44,7 +44,7 @@ pub use stream::Stream; /// Live-object counters for the leak test in fetch-http3-client.test.ts. /// Incremented at allocation, decremented in deinit. Read from the JS thread -/// via TestingAPIs.quicLiveCounts so they must be atomic. +/// via `bun_http_jsc`'s `h3_live_counts` so they must be atomic. // Lower-case names kept so cross-crate readers (`bun_http_jsc`) and the gated // submodules share one identifier; SCREAMING_SNAKE aliases preserved for the // existing internal references. @@ -54,6 +54,3 @@ pub static live_sessions: AtomicU32 = AtomicU32::new(0); pub static live_streams: AtomicU32 = AtomicU32::new(0); pub use live_sessions as LIVE_SESSIONS; pub use live_streams as LIVE_STREAMS; - -// H3TestingAPIs lives in bun_http_jsc and is accessed via the -// extension-trait pattern there. diff --git a/src/http_jsc/build.rs b/src/http_jsc/build.rs new file mode 100644 index 000000000000..cf05d529c844 --- /dev/null +++ b/src/http_jsc/build.rs @@ -0,0 +1,35 @@ +// Build scripts run on the host before bun_* crates are compiled; std is the only option. +#![allow( + clippy::disallowed_methods, + clippy::disallowed_types, + clippy::disallowed_macros +)] +//! Export `BUN_CODEGEN_DIR` for `generated_bindgen.rs`, which `include!`s what `src/codegen/bindgen.ts` writes. + +use std::env; +use std::path::{Path, PathBuf}; + +fn main() { + let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let repo = manifest + .parent() + .and_then(Path::parent) + .expect("repo root from CARGO_MANIFEST_DIR") + .to_path_buf(); + + let codegen_dir = env::var("BUN_CODEGEN_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| repo.join("build/debug/codegen")); + + let generated = codegen_dir.join("generated_bindgen_bun_http_jsc.rs"); + if !generated.exists() { + panic!( + "generated_bindgen_bun_http_jsc.rs not found at {} — run `bun bd` first", + generated.display() + ); + } + + println!("cargo:rerun-if-changed={}", generated.display()); + println!("cargo:rustc-env=BUN_CODEGEN_DIR={}", codegen_dir.display()); + println!("cargo:rerun-if-env-changed=BUN_CODEGEN_DIR"); +} diff --git a/src/http_jsc/generated_bindgen.rs b/src/http_jsc/generated_bindgen.rs new file mode 100644 index 000000000000..fbb42b5a12a6 --- /dev/null +++ b/src/http_jsc/generated_bindgen.rs @@ -0,0 +1,6 @@ +//! What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. + +include!(concat!( + env!("BUN_CODEGEN_DIR"), + "/generated_bindgen_bun_http_jsc.rs" +)); diff --git a/src/http_jsc/headers_jsc.bind.ts b/src/http_jsc/headers_jsc.bind.ts new file mode 100644 index 000000000000..e34fca71f21a --- /dev/null +++ b/src/http_jsc/headers_jsc.bind.ts @@ -0,0 +1,9 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::headers_jsc" }); + +export const H2LiveCounts = b.dictionary({ sessions: b.i32, streams: b.i32 }); +export const h2LiveCounts = b.fn({ returns: H2LiveCounts }); + +export const H3LiveCounts = b.dictionary({ sessions: b.u32, streams: b.u32 }); +export const h3LiveCounts = b.fn({ returns: H3LiveCounts }); diff --git a/src/http_jsc/headers_jsc.rs b/src/http_jsc/headers_jsc.rs index 2568bb1e5efa..8cc1db80b152 100644 --- a/src/http_jsc/headers_jsc.rs +++ b/src/http_jsc/headers_jsc.rs @@ -5,9 +5,9 @@ use core::sync::atomic::Ordering; use bun_http::Headers; use bun_http::headers::{EntryList, api}; -use bun_jsc::{ - CallFrame, FetchHeaders, HTTPHeaderName, JSGlobalObject, JSValue, JsError, JsResult, -}; +use bun_jsc::{FetchHeaders, HTTPHeaderName, JSGlobalObject, JsError, JsResult}; + +use crate::generated_bindgen::headers_jsc::{H2LiveCounts, H3LiveCounts}; /// `Headers` indexes its buffer with `u32` `StringPointer`s, so the total size can't exceed `u32::MAX`. fn throw_headers_too_large(global: &JSGlobalObject) -> JsError { @@ -130,65 +130,18 @@ pub fn from_fetch_headers( Ok(headers) } -struct H2TestingAPIs; - -impl H2TestingAPIs { - // No attribute needed — generate-js2native.ts scans by signature shape. - fn live_counts(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - use bun_http::h2_client; - let obj = JSValue::create_empty_object(global, 2); - // h2 atomics - // are `AtomicI32` (signed) so no widening. - obj.put( - global, - b"sessions", - JSValue::js_number_from_int32(h2_client::live_sessions.load(Ordering::Relaxed)), - ); - obj.put( - global, - b"streams", - JSValue::js_number_from_int32(h2_client::live_streams.load(Ordering::Relaxed)), - ); - Ok(obj) +pub(crate) fn h2_live_counts() -> H2LiveCounts { + use bun_http::h2_client; + H2LiveCounts { + sessions: h2_client::live_sessions.load(Ordering::Relaxed), + streams: h2_client::live_streams.load(Ordering::Relaxed), } } -struct H3TestingAPIs; - -impl H3TestingAPIs { - /// Named distinctly from H2's `live_counts` because generate-js2native.ts - /// mangles `[^A-Za-z]` to `_`, so the H2 and H3 client paths produce - /// the same path prefix and the function name has to differ. - fn quic_live_counts(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - use bun_http::h3_client; - let obj = JSValue::create_empty_object(global, 2); - // h3 atomics are `AtomicU32`; widen to u64 for `js_number_from_uint64`. - obj.put( - global, - b"sessions", - JSValue::js_number_from_uint64(u64::from( - h3_client::live_sessions.load(Ordering::Relaxed), - )), - ); - obj.put( - global, - b"streams", - JSValue::js_number_from_uint64(u64::from( - h3_client::live_streams.load(Ordering::Relaxed), - )), - ); - Ok(obj) +pub(crate) fn h3_live_counts() -> H3LiveCounts { + use bun_http::h3_client; + H3LiveCounts { + sessions: h3_client::live_sessions.load(Ordering::Relaxed), + streams: h3_client::live_streams.load(Ordering::Relaxed), } } - -/// Free-fn aliases of [`H2TestingAPIs::live_counts`] / -/// [`H3TestingAPIs::quic_live_counts`] so `bun_runtime::dispatch::js2native` -/// can `pub use` them (associated fns aren't importable items). -#[inline] -pub fn h2_live_counts(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - H2TestingAPIs::live_counts(global, frame) -} -#[inline] -pub fn h3_quic_live_counts(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - H3TestingAPIs::quic_live_counts(global, frame) -} diff --git a/src/http_jsc/lib.rs b/src/http_jsc/lib.rs index 82238afb3266..1faa5d350ddc 100644 --- a/src/http_jsc/lib.rs +++ b/src/http_jsc/lib.rs @@ -5,6 +5,7 @@ pub mod error; pub use error::{Error, Result}; pub mod fetch_enums_jsc; +pub(crate) mod generated_bindgen; pub mod method_jsc; pub mod headers_jsc; diff --git a/src/install/lockfile/lockfile_json_stringify_for_debugging.rs b/src/install/lockfile/lockfile_json_stringify_for_debugging.rs index c26b90180319..64587c660a39 100644 --- a/src/install/lockfile/lockfile_json_stringify_for_debugging.rs +++ b/src/install/lockfile/lockfile_json_stringify_for_debugging.rs @@ -639,7 +639,7 @@ pub struct WriteStreamOptions { } /// JSON write stream over an in-memory `Vec`, sufficient for -/// `bun_install_js_bindings::jsParseLockfile`. +/// `bun_install_js_bindings::parse_lockfile`. pub struct WriteStream { pub(crate) out: Vec, opts: WriteStreamOptions, diff --git a/src/install/npm.rs b/src/install/npm.rs index 8451545dfb1d..42cc8be8811c 100644 --- a/src/install/npm.rs +++ b/src/install/npm.rs @@ -1140,8 +1140,6 @@ pub mod package_manifest { use super::*; use bun_io::Write as _; - // bindings — see bun_install_jsc::npm_jsc::ManifestBindings (deleted *_jsc alias) - pub struct Serializer; /// The `save_async` tasks that have not finished. diff --git a/src/install_jsc/build.rs b/src/install_jsc/build.rs new file mode 100644 index 000000000000..75a84816bcd5 --- /dev/null +++ b/src/install_jsc/build.rs @@ -0,0 +1,35 @@ +// Build scripts run on the host before bun_* crates are compiled; std is the only option. +#![allow( + clippy::disallowed_methods, + clippy::disallowed_types, + clippy::disallowed_macros +)] +//! Export `BUN_CODEGEN_DIR` for `generated_bindgen.rs`, which `include!`s what `src/codegen/bindgen.ts` writes. + +use std::env; +use std::path::{Path, PathBuf}; + +fn main() { + let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let repo = manifest + .parent() + .and_then(Path::parent) + .expect("repo root from CARGO_MANIFEST_DIR") + .to_path_buf(); + + let codegen_dir = env::var("BUN_CODEGEN_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| repo.join("build/debug/codegen")); + + let generated = codegen_dir.join("generated_bindgen_bun_install_jsc.rs"); + if !generated.exists() { + panic!( + "generated_bindgen_bun_install_jsc.rs not found at {} — run `bun bd` first", + generated.display() + ); + } + + println!("cargo:rerun-if-changed={}", generated.display()); + println!("cargo:rustc-env=BUN_CODEGEN_DIR={}", codegen_dir.display()); + println!("cargo:rerun-if-env-changed=BUN_CODEGEN_DIR"); +} diff --git a/src/install_jsc/generated_bindgen.rs b/src/install_jsc/generated_bindgen.rs new file mode 100644 index 000000000000..17545e50b92c --- /dev/null +++ b/src/install_jsc/generated_bindgen.rs @@ -0,0 +1,6 @@ +//! What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. + +include!(concat!( + env!("BUN_CODEGEN_DIR"), + "/generated_bindgen_bun_install_jsc.rs" +)); diff --git a/src/install_jsc/hosted_git_info_jsc.bind.ts b/src/install_jsc/hosted_git_info_jsc.bind.ts new file mode 100644 index 000000000000..e461377c6fa2 --- /dev/null +++ b/src/install_jsc/hosted_git_info_jsc.bind.ts @@ -0,0 +1,17 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::hosted_git_info_jsc" }); + +export const parseUrl = b.fn({ + args: { url: b.string }, + returns: b.string, + capability: "heap", + throws: true, +}); + +export const fromUrl = b.fn({ + args: { url: b.string }, + returns: b.any, + capability: "heap", + throws: true, +}); diff --git a/src/install_jsc/hosted_git_info_jsc.rs b/src/install_jsc/hosted_git_info_jsc.rs index da5ad9ce92d0..a72c3e370691 100644 --- a/src/install_jsc/hosted_git_info_jsc.rs +++ b/src/install_jsc/hosted_git_info_jsc.rs @@ -1,7 +1,9 @@ //! JSC bridges for `bun_install::hosted_git_info`. +use bun_core::String as BunString; +use bun_jsc::bindgen::{self, JsHeap}; use bun_jsc::bun_string_jsc; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, StringJsc}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult, StringJsc}; /// Extension trait providing `.to_js()` on `HostedGitInfo`. trait HostedGitInfoJsc { @@ -10,7 +12,6 @@ trait HostedGitInfoJsc { impl HostedGitInfoJsc for bun_install::hosted_git_info::HostedGitInfo { fn to_js(&self, go: &JSGlobalObject) -> JsResult { - use bun_core::String as BunString; let obj = JSValue::create_empty_object(go, 6); obj.put( go, @@ -56,73 +57,41 @@ impl HostedGitInfoJsc for bun_install::hosted_git_info::HostedGitInfo { } } -pub fn js_parse_url(go: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn parse_url<'a>(heap: &JsHeap<'a>, url: &BunString) -> bindgen::Result<'a, BunString> { use bun_install::hosted_git_info as hgi; - if callframe.arguments_count() != 1 { - return Err(go.throw(format_args!( - "hostedGitInfo.prototype.parseUrl takes exactly 1 argument" - ))); - } - - let arg0 = callframe.argument(0); - if !arg0.is_string() { - return Err(go.throw(format_args!(concat!( - "hostedGitInfo.prototype.parseUrl takes a string as its ", - "first argument", - )))); - } - // TODO(markovejnovic): This feels like there's too much going on all - // to give us a slice. Maybe there's a better way to code this up. - let npa_str = arg0.to_bun_string(go)?; - let as_utf8 = npa_str.to_utf8(); + let as_utf8 = url.to_utf8(); let parsed = match hgi::parse_url(as_utf8.slice()) { Ok(p) => p, Err(err) => { - return Err(go.throw(format_args!( + return Err(heap.error(format_args!( "Invalid Git URL: {}", bstr::BStr::new(<&'static str>::from(err)) ))); } }; - // `parsed.url` is `Box` (C++-owned WTF::URL); `href()` yields a - // `bun_core::String`. `defer parsed.url.deinit()` deleted — Box Drop frees. - parsed.url.href().to_js(go) + Ok(parsed.url.href()) } -pub fn js_from_url(go: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn from_url<'a>(heap: &JsHeap<'a>, url: &BunString) -> bindgen::Result<'a, JSValue> { use bun_install::hosted_git_info::HostedGitInfo; // TODO(markovejnovic): The original hosted-git-info actually takes another argument that // allows you to inject options. Seems untested so we didn't implement // it. - if callframe.arguments_count() != 1 { - return Err(go.throw(format_args!( - "hostedGitInfo.prototype.fromUrl takes exactly 1 argument" - ))); - } - - let arg0 = callframe.argument(0); - if !arg0.is_string() { - return Err(go.throw(format_args!( - "hostedGitInfo.prototype.fromUrl takes a string as its first argument" - ))); - } - // TODO(markovejnovic): This feels like there's too much going on all to give us a slice. - // Maybe there's a better way to code this up. - let npa_str = arg0.to_bun_string(go)?; - let as_utf8 = npa_str.to_utf8(); + let as_utf8 = url.to_utf8(); let parsed = match HostedGitInfo::from_url(as_utf8.slice()) { Ok(Some(p)) => p, Ok(None) => return Ok(JSValue::NULL), Err(err) => { - return Err(go.throw(format_args!( + return Err(heap.error(format_args!( "Invalid Git URL: {}", bstr::BStr::new(<&'static str>::from(err)) ))); } }; - parsed.to_js(go) + // SAFETY: the object is new and plain, and its properties are defined and not set. + Ok(parsed.to_js(unsafe { heap.global() })?) } diff --git a/src/install_jsc/ini_jsc.bind.ts b/src/install_jsc/ini_jsc.bind.ts new file mode 100644 index 000000000000..3306e7d19060 --- /dev/null +++ b/src/install_jsc/ini_jsc.bind.ts @@ -0,0 +1,17 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::ini_jsc::IniTestingAPIs" }); + +export const parse = b.fn({ + args: { source: b.string.loose.utf8 }, + returns: b.any, + capability: "heap", + throws: true, +}); + +export const loadNpmrc = b.fn({ + args: { contents: b.string.loose.utf8, env: b.any }, + returns: b.any, + capability: "js", + throws: true, +}); diff --git a/src/install_jsc/ini_jsc.rs b/src/install_jsc/ini_jsc.rs index 57bf1b00f360..9bcae7a37456 100644 --- a/src/install_jsc/ini_jsc.rs +++ b/src/install_jsc/ini_jsc.rs @@ -1,29 +1,16 @@ //! Test-only host fns for `bun.ini` (used by `internal-for-testing.ts`). //! Kept out of `ini/` so that directory has no JSC references. -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, LogJsc as _, StringJsc}; - -/// Free-fn aliases of the [`IniTestingAPIs`] associated fns so -/// `bun_runtime::dispatch::js2native` can `pub use` them (associated fns -/// aren't importable items). -#[inline] -pub fn ini_testing_parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - IniTestingAPIs::parse(global, frame) -} -#[inline] -pub fn ini_testing_load_npmrc_from_js( - global: &JSGlobalObject, - frame: &CallFrame, -) -> JsResult { - IniTestingAPIs::load_npmrc_from_js(global, frame) -} +use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult, LogJsc as _, StringJsc}; pub(crate) struct IniTestingAPIs; impl IniTestingAPIs { - pub(crate) fn load_npmrc_from_js( - global: &JSGlobalObject, - frame: &CallFrame, + pub(crate) fn load_npmrc<'a>( + cx: &mut JsScope<'a>, + contents: &[u8], + env: JsLocal<'a>, ) -> JsResult { use bun_ast::{Log, Source}; use bun_core::String as BunString; @@ -32,14 +19,12 @@ impl IniTestingAPIs { use bun_install::npm::Registry; use bun_options_types::schema::api::{BunInstall, NpmRegistry}; - let arg = frame.argument(0); - let npmrc_contents = arg.to_js_string_view(global)?; - let npmrc_utf8 = npmrc_contents.to_utf8(); - let source = Source::init_path_string(b"", npmrc_utf8.slice()); + let global = cx.global(); + let source = Source::init_path_string(b"", contents); let mut log = Log::init(); - let envjs = frame.argument(1); + let envjs = env.to_js(); // The loader is either VM-owned or built locally; keep local storage in // an `Option` so both arms yield the same `&Loader` type. let mut env_storage: Option = None; @@ -199,19 +184,16 @@ impl IniTestingAPIs { Ok(bun_jsc::JSObject::create(&pojo, global)?.to_js()) } - pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { + pub(crate) fn parse(heap: &JsHeap<'_>, source: &[u8]) -> JsResult { use bun_ast::ToJSError; use bun_ini::Parser; use bun_jsc::JsError; - let arguments = frame.arguments(); - - let jsstr = arguments[0]; - let bunstr = jsstr.to_js_string_view(global)?; - let utf8str = bunstr.to_utf8(); - + // SAFETY: neither parsing nor `expr_to_js` runs JavaScript: the keys are strings and the + // properties are defined, not set. What throws returns. + let global = unsafe { heap.global() }; let env = global.bun_vm().as_mut().transpiler.env(); - let source = bun_ast::Source::init_path_string(b"", utf8str.slice()); + let source = bun_ast::Source::init_path_string(b"", source); let arena = bun_alloc::Arena::new(); let mut parser = Parser::init(&source, env); parser.parse(&arena)?; diff --git a/src/install_jsc/install_binding.bind.ts b/src/install_jsc/install_binding.bind.ts new file mode 100644 index 000000000000..cb06428ed8ba --- /dev/null +++ b/src/install_jsc/install_binding.bind.ts @@ -0,0 +1,12 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::install_binding::bun_install_js_bindings" }); + +export const installTestHelpers = b.namespace({ + parseLockfile: { + args: { cwd: b.string.loose.utf8 }, + returns: b.any, + capability: "js", + throws: true, + }, +}); diff --git a/src/install_jsc/install_binding.rs b/src/install_jsc/install_binding.rs index 0ded4eb5ffc2..c7b693c560a3 100644 --- a/src/install_jsc/install_binding.rs +++ b/src/install_jsc/install_binding.rs @@ -1,35 +1,10 @@ -use bun_jsc::{JSGlobalObject, JSValue, StringJsc as _}; +use bun_jsc::bindgen::JsScope; +use bun_jsc::{JSValue, StringJsc as _}; -pub mod bun_install_js_bindings { +pub(crate) mod bun_install_js_bindings { use super::*; - pub fn generate(global: &JSGlobalObject) -> JSValue { - use bun_jsc::JSFunction; - let obj = JSValue::create_empty_object(global, 1); - obj.put( - global, - b"parseLockfile", - JSFunction::create( - global, - "parseLockfile", - // `#[bun_jsc::host_fn]` on the module-scope `js_parse_lockfile` - // emits this `JSHostFn`-ABI shim. - __jsc_host_js_parse_lockfile, - 1, - Default::default(), - ), - ); - obj - } - - // Lives at module scope (not in an `impl`) because the - // `#[bun_jsc::host_fn]` Free-kind shim body emits `#fn_name(__g, __f)` without - // a `Self::` qualifier, so the wrapped fn must resolve unqualified. - #[bun_jsc::host_fn] - fn js_parse_lockfile( - global: &JSGlobalObject, - frame: &bun_jsc::CallFrame, - ) -> bun_jsc::JsResult { + pub(crate) fn parse_lockfile(cx: &mut JsScope<'_>, cwd: &[u8]) -> bun_jsc::JsResult { use core::ptr::NonNull; use bstr::BStr; @@ -41,18 +16,16 @@ pub mod bun_install_js_bindings { use bun_paths::resolve_path; use bun_sys::FdExt as _; + let global = cx.global(); let mut log = bun_ast::Log::init(); - let args = frame.arguments(); - let cwd = args[0].to_utf8(global)?; - - let dir = match bun_sys::open_dir_absolute_not_for_deleting_or_renaming(cwd.slice()) { + let dir = match bun_sys::open_dir_absolute_not_for_deleting_or_renaming(cwd) { Ok(d) => d, Err(err) => { return Err(global.throw(format_args!( "failed to open: {}, '{}'", BStr::new(err.name()), - BStr::new(cwd.slice()), + BStr::new(cwd), ))); } }; @@ -60,7 +33,7 @@ pub mod bun_install_js_bindings { let dir = scopeguard::guard(dir, |d| d.close()); let lockfile_path = resolve_path::join_abs_string_z::( - cwd.slice(), + cwd, &[b"bun.lockb".as_slice()], ); diff --git a/src/install_jsc/lib.rs b/src/install_jsc/lib.rs index 2dcff5199bc4..060eb3c6feae 100644 --- a/src/install_jsc/lib.rs +++ b/src/install_jsc/lib.rs @@ -1,12 +1,9 @@ #![warn(unused_must_use)] //! JSC bridge surface for `bun_install`. Keeps `src/install/` free of -//! `JSValue`/`JSGlobalObject`/`CallFrame` references. -//! -//! Host-fn bodies for hosted_git_info / npm / install_binding compile -//! against the `bun_jsc` + `bun_install` surface and are wired into the -//! runtime via the `dispatch_js2native` re-exports. +//! `JSValue`/`JSGlobalObject` references. -pub mod hosted_git_info_jsc; -pub mod ini_jsc; -pub mod install_binding; -pub mod npm_jsc; +pub(crate) mod generated_bindgen; +pub(crate) mod hosted_git_info_jsc; +pub(crate) mod ini_jsc; +pub(crate) mod install_binding; +pub(crate) mod npm_jsc; diff --git a/src/install_jsc/npm_jsc.bind.ts b/src/install_jsc/npm_jsc.bind.ts new file mode 100644 index 000000000000..96ab3f34b280 --- /dev/null +++ b/src/install_jsc/npm_jsc.bind.ts @@ -0,0 +1,26 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::npm_jsc" }); + +export const operatingSystemIsMatch = b.fn({ + args: { names: b.any }, + returns: b.bool, + capability: "js", + throws: true, +}); + +export const architectureIsMatch = b.fn({ + args: { names: b.any }, + returns: b.bool, + capability: "js", + throws: true, +}); + +export const npmManifestTestHelpers = b.namespace({ + parseManifest: { + args: { manifestFilename: b.string, registry: b.string }, + returns: b.any, + capability: "heap", + throws: true, + }, +}); diff --git a/src/install_jsc/npm_jsc.rs b/src/install_jsc/npm_jsc.rs index 81ac3831bf2c..22e0f32e247d 100644 --- a/src/install_jsc/npm_jsc.rs +++ b/src/install_jsc/npm_jsc.rs @@ -1,92 +1,56 @@ //! JSC host fns for `bun_install::npm`, kept here so that `install/` has -//! no `JSValue`/`JSGlobalObject`/`CallFrame` references. +//! no `JSValue`/`JSGlobalObject` references. -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, StringJsc as _}; +use bun_core::String as BunString; +use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; +use bun_jsc::{JSValue, JsResult, StringJsc as _}; -pub fn operating_system_is_match(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn operating_system_is_match<'a>( + cx: &mut JsScope<'a>, + names: JsLocal<'a>, +) -> JsResult { use bun_install::npm; - let [arg] = frame.arguments_as_array::<1>(); + let global = cx.global(); let mut operating_system = npm::OperatingSystem::NONE.negatable(); - let mut iter = arg.array_iterator(global)?; + let mut iter = names.to_js().array_iterator(global)?; while let Some(item) = iter.next()? { let slice = item.to_utf8(global)?; operating_system.apply(slice.slice()); } - Ok(JSValue::js_boolean( - operating_system - .combine() - .is_match(npm::OperatingSystem::CURRENT), - )) + Ok(operating_system + .combine() + .is_match(npm::OperatingSystem::CURRENT)) } -pub fn architecture_is_match(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn architecture_is_match<'a>( + cx: &mut JsScope<'a>, + names: JsLocal<'a>, +) -> JsResult { use bun_install::npm; - let [arg] = frame.arguments_as_array::<1>(); + let global = cx.global(); let mut architecture = npm::Architecture::NONE.negatable(); - let mut iter = arg.array_iterator(global)?; + let mut iter = names.to_js().array_iterator(global)?; while let Some(item) = iter.next()? { let slice = item.to_utf8(global)?; architecture.apply(slice.slice()); } - Ok(JSValue::js_boolean( - architecture.combine().is_match(npm::Architecture::CURRENT), - )) + Ok(architecture.combine().is_match(npm::Architecture::CURRENT)) } -/// Free-fn alias of [`ManifestBindings::generate`] so `bun_runtime::dispatch::js2native` -/// can `pub use` it (associated fns aren't importable items). -#[inline] -pub fn package_manifest_bindings_generate(global: &JSGlobalObject) -> JSValue { - ManifestBindings::generate(global) -} - -/// Formerly `npm.PackageManifest.bindings` — testing-only (`internal-for-testing.ts`). -pub(crate) struct ManifestBindings; - -impl ManifestBindings { - pub(crate) fn generate(global: &JSGlobalObject) -> JSValue { - use bun_jsc::JSFunction; - let obj = JSValue::create_empty_object(global, 1); - obj.put( - global, - b"parseManifest", - JSFunction::create( - global, - "parseManifest", - // `#[bun_jsc::host_fn]` on the module-scope `js_parse_manifest` - // emits this `JSHostFn`-ABI shim. - __jsc_host_js_parse_manifest, - 2, - Default::default(), - ), - ); - obj - } -} - -// Lives at module scope (not `impl ManifestBindings`) because the -// `#[bun_jsc::host_fn]` Free-kind shim body emits `#fn_name(__g, __f)` without -// a `Self::` qualifier, so the wrapped fn must resolve unqualified. -#[bun_jsc::host_fn] -fn js_parse_manifest(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn parse_manifest( + heap: &JsHeap<'_>, + manifest_filename: &BunString, + registry: &BunString, +) -> JsResult { use bstr::BStr; - use bun_core::String as BunString; use bun_install::npm; use bun_jsc::JsError; use std::io::Write as _; - let args = frame.arguments(); - if args.len() < 2 || !args[0].is_string() || !args[1].is_string() { - return Err(global.throw(format_args!( - "expected manifest filename and registry string arguments" - ))); - } - - let manifest_filename_str = args[0].to_bun_string(global)?; - let manifest_filename = manifest_filename_str.to_utf8(); - - let registry_str = args[1].to_bun_string(global)?; - let registry = registry_str.to_utf8(); + // SAFETY: `JSON.parse` without a reviver runs no JavaScript, and what throws returns. + let global = unsafe { heap.global() }; + let manifest_filename = manifest_filename.to_utf8(); + let registry = registry.to_utf8(); let manifest_file = match bun_sys::openat_a( bun_sys::Fd::cwd(), diff --git a/src/js/builtins/Ipc.ts b/src/js/builtins/Ipc.ts index 351fc86e17d1..3f69ec22c185 100644 --- a/src/js/builtins/Ipc.ts +++ b/src/js/builtins/Ipc.ts @@ -54,7 +54,7 @@ export function serialize(message, handle: IpcHandle, options) { * @returns {void} */ export function parseHandle(target, serialized, fd) { - const emit = $newRustFunction("ipc.rs", "emitHandleIPCMessage", 3); + const emit = $bindgen("ipc_host.bind.ts", "emitHandleIPCMessage"); const net = require("node:net"); // const dgram = require("node:dgram"); switch (serialized.type) { diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index bfb98abf9889..87a63f8eaf88 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -171,7 +171,7 @@ const native = { }, }; -const ccFn = $newRustFunction("ffi.rs", "Bun__FFI__cc", 1); +const ccFn = $bindgen("ffi.bind.ts", "cc"); function normalizePath(path: string | URL | Bun.BunFile | undefined) { if (typeof path === "string" && path?.startsWith?.("file:")) { diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 9780c6e1702a..022a87d65d5a 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -13,7 +13,7 @@ export const highlightJavaScript = (code: string) => fmtBinding(code, "highlight export const highlightJavaScriptRedacted = (code: string) => fmtBinding(code, "highlight-javascript-redacted"); export const escapePowershell = (code: string) => fmtBinding(code, "escape-powershell"); -export const canonicalizeIP = $newCppFunction("NodeTLS.cpp", "Bun__canonicalizeIP", 1); +export const canonicalizeIP = $bindgen("cares_jsc.bind.ts", "canonicalizeIP"); // Process-wide additions to the public keys no certificate chain may hold (blocked_spkis.cpp). `hash` is the SHA-256 of // a DER SubjectPublicKeyInfo. @@ -65,53 +65,42 @@ export const setSQLiteColumnNameTransform = ( ) => statement.native.$setColumnNameTransform(transform); export const patchInternals = { - parse: $newRustFunction("patch.rs", "TestingAPIs.parse", 1), - apply: $newRustFunction("patch.rs", "TestingAPIs.apply", 2), - makeDiff: $newRustFunction("patch.rs", "TestingAPIs.makeDiff", 2), + parse: $bindgen("testing.bind.ts", "parse"), + apply: $bindgen("testing.bind.ts", "apply"), + makeDiff: $bindgen("testing.bind.ts", "makeDiff"), }; export const internalSourceMap = { - fromVLQ: $newRustFunction("sourcemap/InternalSourceMap.rs", "TestingAPIs.fromVLQ", 1) as (vlq: string) => Uint8Array, - toVLQ: $newRustFunction("sourcemap/InternalSourceMap.rs", "TestingAPIs.toVLQ", 1) as (blob: Uint8Array) => string, - find: $newRustFunction("sourcemap/InternalSourceMap.rs", "TestingAPIs.find", 3) as ( - blob: Uint8Array, - line: number, - col: number, - ) => { - generatedLine: number; - generatedColumn: number; - originalLine: number; - originalColumn: number; - sourceIndex: number; - } | null, + fromVLQ: $bindgen("internal_jsc.bind.ts", "fromVLQ") as (vlq: string) => Uint8Array, + toVLQ: $bindgen("internal_jsc.bind.ts", "toVLQ"), + find: $bindgen("internal_jsc.bind.ts", "find"), }; -const shellLex = $newRustFunction("shell.rs", "TestingAPIs.shellLex", 2); -const shellParse = $newRustFunction("shell.rs", "TestingAPIs.shellParse", 2); +const shellLex = $bindgen("shell_body.bind.ts", "shellLex"); +const shellParse = $bindgen("shell_body.bind.ts", "shellParse"); -export const sslCtxLiveCount = $newRustFunction("SecureContext.rs", "jsLiveCount", 0); +export const sslCtxLiveCount = $bindgen("SecureContext.bind.ts", "liveCount"); -export const napiThreadsafeFunctionLiveCount = $newRustFunction("napi_body.rs", "jsThreadsafeFunctionLiveCount", 0); +export const napiThreadsafeFunctionLiveCount = $bindgen("napi_body.bind.ts", "threadsafeFunctionLiveCount"); -export const bundlerWorkerLiveCount: () => number = $newRustFunction("JSBundler.rs", "jsWorkerLiveCount", 0); +export const bundlerWorkerLiveCount = $bindgen("JSBundler.bind.ts", "workerLiveCount"); -export const semverIntersects = $newRustFunction("SemverObject.rs", "intersectsForTesting", 2) as ( +export const semverIntersects = $bindgen("SemverObject.bind.ts", "intersects") as ( left: string, right: string, ) => boolean; /** What a bytecode order file calls the code of `text`: `M ` and ` ` lines, or null. */ -export const bytecodeOrderNames = $newRustFunction("BytecodeOrderRecorder.rs", "namesForTesting", 3) as ( +export const bytecodeOrderNames = $bindgen("BytecodeOrderRecorder.bind.ts", "namesForTesting") as ( text: string, kind: "module" | "script" | "builtin" | "internal", chunkPaths?: string, ) => string | null | undefined; -export const escapeRegExp = $newRustFunction("escapeRegExp.rs", "jsEscapeRegExp", 1); -export const escapeRegExpForPackageNameMatching = $newRustFunction( - "escapeRegExp.rs", - "jsEscapeRegExpForPackageNameMatching", - 1, +export const escapeRegExp = $bindgen("bun_string_jsc.bind.ts", "escapeRegExp"); +export const escapeRegExpForPackageNameMatching = $bindgen( + "bun_string_jsc.bind.ts", + "escapeRegExpForPackageNameMatching", ); export const shellInternals = { @@ -125,54 +114,35 @@ export const shellInternals = { * const isDisabled = builtinDisabled("cp") * ``` */ - builtinDisabled: $newRustFunction("shell.rs", "TestingAPIs.disabledOnThisPlatform", 1), + builtinDisabled: $bindgen("shell_body.bind.ts", "disabledOnThisPlatform"), }; export const subprocessInternals = { - injectStdioReadError: $newRustFunction("subprocess.rs", "TestingAPIs.injectStdioReadError", 2) as ( - subprocess: import("bun").Subprocess, - kind: "stdout" | "stderr", - ) => boolean, - closeStdinWriter: $newRustFunction("subprocess.rs", "TestingAPIs.closeStdinWriter", 1) as ( - subprocess: import("bun").Subprocess, - ) => boolean, + injectStdioReadError: $bindgen("subprocess.bind.ts", "injectStdioReadError"), + closeStdinWriter: $bindgen("subprocess.bind.ts", "closeStdinWriter"), }; export const iniInternals = { - parse: $newRustFunction("ini.rs", "IniTestingAPIs.parse", 1), - loadNpmrc: $newRustFunction("ini.rs", "IniTestingAPIs.loadNpmrcFromJS", 2), + parse: $bindgen("ini_jsc.bind.ts", "parse"), + loadNpmrc: $bindgen("ini_jsc.bind.ts", "loadNpmrc"), }; export const cssInternals = { - minifyTestWithOptions: $newRustFunction("css_internals.rs", "minifyTestWithOptions", 3), - minifyErrorTestWithOptions: $newRustFunction("css_internals.rs", "minifyErrorTestWithOptions", 3), - testWithOptions: $newRustFunction("css_internals.rs", "testWithOptions", 3), - prefixTestWithOptions: $newRustFunction("css_internals.rs", "prefixTestWithOptions", 3), - minifyTest: $newRustFunction("css_internals.rs", "minifyTest", 3), - prefixTest: $newRustFunction("css_internals.rs", "prefixTest", 3), - _test: $newRustFunction("css_internals.rs", "_test", 3), - attrTest: $newRustFunction("css_internals.rs", "attrTest", 3), + minifyTestWithOptions: $bindgen("css_internals.bind.ts", "minifyTestWithOptions"), + minifyErrorTestWithOptions: $bindgen("css_internals.bind.ts", "minifyErrorTestWithOptions"), + testWithOptions: $bindgen("css_internals.bind.ts", "testWithOptions"), + prefixTestWithOptions: $bindgen("css_internals.bind.ts", "prefixTestWithOptions"), + minifyTest: $bindgen("css_internals.bind.ts", "minifyTest"), + prefixTest: $bindgen("css_internals.bind.ts", "prefixTest"), + _test: $bindgen("css_internals.bind.ts", "test"), + attrTest: $bindgen("css_internals.bind.ts", "attrTest"), }; -export const crash_handler = $rust("crash_handler.rs", "js_bindings.generate") as { - getMachOImageZeroOffset: () => number; - segfault: () => void; - panic: () => void; - rootError: () => void; - outOfMemory: () => void; - operatorNew: (nothrow?: boolean) => boolean | undefined; - abort: () => void; - fastfail: () => void; - trap: () => void; - raiseIgnoringPanicHandler: () => void; -}; +export const crash_handler = $bindgen("crash_handler_jsc.bind.ts", "crashHandler"); -export const upgrade_test_helpers = $rust("upgrade_command.rs", "upgrade_js_bindings.generate") as { - openTempDirWithoutSharingDelete: () => void; - closeTempDirHandle: () => void; -}; +export const upgrade_test_helpers = $bindgen("upgrade_command.bind.ts", "upgradeTestHelpers"); -export const install_test_helpers = $rust("install_binding.rs", "bun_install_js_bindings.generate") as { +export const install_test_helpers = $bindgen("install_binding.bind.ts", "installTestHelpers") as { /** * Returns the lockfile at the given path as an object. */ @@ -188,60 +158,44 @@ export const nativeFrameForTesting: (callback: () => void) => void = $cpp( // Linux-only. Create an in-memory file descriptor with a preset size. // You should call fs.closeSync(fd) when you're done with it. -export const memfd_create: (size: number) => number = $newRustFunction( - "node_fs_binding.rs", - "createMemfdForTesting", - 1, -); +export const memfd_create = $bindgen("node_fs_binding.bind.ts", "createMemfdForTesting") as (size: number) => number; -export const createStatsForIno: (ino: bigint, big: boolean) => any = $newRustFunction( - "Stat.rs", - "createStatsForIno", - 2, -); +export const createStatsForIno: (ino: bigint, big: boolean) => any = $bindgen("Stat.bind.ts", "createStatsForIno"); -export const setSyntheticAllocationLimitForTesting: (limit: number) => number = $newRustFunction( - "virtual_machine_exports.rs", - "Bun__setSyntheticAllocationLimitForTesting", - 1, +export const setSyntheticAllocationLimitForTesting: (limit: number) => number = $bindgen( + "virtual_machine_exports.bind.ts", + "setSyntheticAllocationLimitForTesting", ); // Shrink the markdown parser's block-metadata cap (in bytes) so its // `TooManyBlocks` error is reachable without 4 GiB of input. The cap can only // be lowered, never raised past the real limit. Returns the previous value so // a test can restore it. -export const setMaxMarkdownBlockBytesForTesting: (limit: number) => number = $newRustFunction( - "MarkdownObject.rs", +export const setMaxMarkdownBlockBytesForTesting = $bindgen( + "MarkdownObjectInternals.bind.ts", "setMaxMarkdownBlockBytesForTesting", - 1, ); -export const npm_manifest_test_helpers = $rust("npm.rs", "PackageManifest.bindings.generate") as { +export const npm_manifest_test_helpers = $bindgen("npm_jsc.bind.ts", "npmManifestTestHelpers") as { /** * Returns the parsed manifest file. Currently only returns an array of available versions. */ parseManifest: (manifestFileName: string, registryUrl: string) => any; }; -export const readTarball: (tarball: string) => any = $newRustFunction("pack_command.rs", "bindings.jsReadTarball", 1); +export const readTarball = $bindgen("pack_command.bind.ts", "readTarball"); -export const isArchitectureMatch: (architecture: string[]) => boolean = $newRustFunction( - "npm.rs", - "Architecture.jsFunctionArchitectureIsMatch", - 1, +export const isArchitectureMatch: (architecture: string[]) => boolean = $bindgen( + "npm_jsc.bind.ts", + "architectureIsMatch", ); -export const isOperatingSystemMatch: (operatingSystem: string[]) => boolean = $newRustFunction( - "npm.rs", - "OperatingSystem.jsFunctionOperatingSystemIsMatch", - 1, +export const isOperatingSystemMatch: (operatingSystem: string[]) => boolean = $bindgen( + "npm_jsc.bind.ts", + "operatingSystemIsMatch", ); -export const createSocketPair: () => [number, number] = $newRustFunction( - "runtime/socket/socket.rs", - "jsCreateSocketPair", - 0, -); +export const createSocketPair = $bindgen("socket_body.bind.ts", "createSocketPair") as () => [number, number]; export const isModuleResolveFilenameSlowPathEnabled: () => boolean = $newCppFunction( "NodeModuleModule.cpp", @@ -249,11 +203,9 @@ export const isModuleResolveFilenameSlowPathEnabled: () => boolean = $newCppFunc 0, ); -export const frameworkRouterInternals = $rust("FrameworkRouter.rs", "JSFrameworkRouter.getBindings") as { - parseRoutePattern: (style: string, pattern: string) => null | { kind: string; pattern: string }; - FrameworkRouter: { - new (opts: any): any; - }; +export const frameworkRouterInternals = { + parseRoutePattern: $bindgen("js.bind.ts", "parseRoutePattern"), + FrameworkRouter: $bindgen("filesystem_router.bind.ts", "FrameworkFileSystemRouter"), }; // Only in a build with assertions: `isCompiled` in scripts/build/codegen.ts. @@ -325,6 +277,10 @@ export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { pinnedBytesAfterCallback: $bindgen("bindgen_test.bind.ts", "pinnedBytesAfterCallback"), stringAsIsOrBytes: $bindgen("bindgen_test.bind.ts", "stringAsIsOrBytes"), asBigInt: $bindgen("bindgen_test.bind.ts", "asBigInt"), + thisAndCallee: $bindgen("bindgen_test.bind.ts", "thisAndCallee"), + doubled: $bindgen("bindgen_test.bind.ts", "doubled"), + makeDoubled: $bindgen("bindgen_test.bind.ts", "makeDoubled"), + testNamespace: $bindgen("bindgen_test.bind.ts", "testNamespace"), integerOrBigInt: $bindgen("bindgen_test.bind.ts", "integerOrBigInt"), looseInteger: $bindgen("bindgen_test.bind.ts", "looseInteger"), looseIntegerOrBigInt: $bindgen("bindgen_test.bind.ts", "looseIntegerOrBigInt"), @@ -619,8 +575,8 @@ export const exposedInternals = { // Both are the REAL parsers the fs entry points use (FileSystemFlags::from_js // and args::Rm::from_js), not JS reimplementations -- vendored tests assert // the production behavior through these. - stringToFlags: $newRustFunction("node_fs_binding.rs", "string_to_flags_for_testing", 1), - validateRmOptionsSync: $newRustFunction("node_fs_binding.rs", "rm_options_for_testing", 2), + stringToFlags: $bindgen("node_fs_binding.bind.ts", "stringToFlagsForTesting"), + validateRmOptionsSync: $bindgen("node_fs_binding.bind.ts", "rmOptionsForTesting"), }, // internalBinding() is served by the registered "internal/test/binding" // module (src/js/internal/test/binding.ts), not from here. @@ -654,12 +610,10 @@ export const arrayBufferViewHasBuffer = $newCppFunction( ); export const timerInternals = { - timerClockMs: $newRustFunction("runtime/timer/Timer.rs", "internal_bindings.timerClockMs", 0), + timerClockMs: $bindgen("Timer.bind.ts", "timerClockMs"), monotonicNowMs: $bindgen("Timer.bind.ts", "monotonicNowMs"), // Sets the id of this thread's next timer. The counter can only move forward. - setNextTimerId: $newRustFunction("runtime/timer/Timer.rs", "internal_bindings.setNextTimerId", 1) as ( - id: number, - ) => void, + setNextTimerId: $bindgen("Timer.bind.ts", "setNextTimerId") as (id: number) => void, }; // Raw datagram descriptor helpers for tests that need an unbound fd (which @@ -676,18 +630,15 @@ export const decodeURIComponentSIMD = $newCppFunction( ); export const getDevServerDeinitCount = $bindgen("DevServer.bind.ts", "getDeinitCountForTesting"); -export const getCounters = $newRustFunction("Counters.rs", "createCountersObject", 0); -export const linearFifoOrderedRemoveProbe = $newRustFunction( - "collections/linear_fifo.rs", - "TestingAPIs.orderedRemoveProbe", - 1, -) as (scenario: number) => number[]; +export const getCounters = $bindgen("Counters.bind.ts", "createCountersObject"); +export const linearFifoOrderedRemoveProbe = $bindgen("linear_fifo_testing.bind.ts", "orderedRemoveProbe") as ( + scenario: number, +) => number[]; export const hasNonReifiedStatic = $newCppFunction("InternalForTesting.cpp", "jsFunction_hasReifiedStatic", 1); -const setNativeSocketOptions: (socket: unknown, option: 1 | 2, size: number) => void = $newRustFunction( - "runtime/socket/socket.rs", - "jsSetSocketOptions", - 3, +const setNativeSocketOptions: (socket: unknown, option: 1 | 2, size: number) => void = $bindgen( + "socket_body.bind.ts", + "setSocketOptions", ); /** @@ -759,17 +710,11 @@ export type SocketFaultRule = { export const socketFaultInjection = { /** True when the current binary was built with `--socket-fault-injection=on` (defaults to on for ASan builds). */ - available: $newRustFunction( - "runtime/socket/socket.rs", - "TestingAPIs.jsSocketFaultInjectionAvailable", - 0, - ) as () => boolean, + available: $bindgen("socket_body.bind.ts", "socketFaultInjectionAvailable"), /** Arm a process-wide fault rule for one usockets bsd_* syscall. */ - set: $newRustFunction("runtime/socket/socket.rs", "TestingAPIs.jsSetSocketFault", 1) as ( - rule: SocketFaultRule, - ) => boolean, + set: $bindgen("socket_body.bind.ts", "setSocketFault") as (rule: SocketFaultRule) => boolean, /** Disarm all fault rules. */ - clear: $newRustFunction("runtime/socket/socket.rs", "TestingAPIs.jsClearSocketFaults", 0) as () => void, + clear: $bindgen("socket_body.bind.ts", "clearSocketFaults") as () => void, }; export const namedPipeInternals = { @@ -778,7 +723,7 @@ export const namedPipeInternals = { * connecting or connected `Bun.connect({ unix: "\\\\.\\pipe\\..." })` socket and * one per accepted pipe client. Always 0 on other platforms. */ - liveCount: $newRustFunction("runtime/socket/socket.rs", "TestingAPIs.jsNamedPipeContextLiveCount", 0) as () => number, + liveCount: $bindgen("socket_body.bind.ts", "namedPipeContextLiveCount"), }; export const duplexTLSInternals = { @@ -787,11 +732,7 @@ export const duplexTLSInternals = { * (`tls.connect({ socket: duplex })`, `new tls.TLSSocket(duplex)`): one per * socket, until the task that frees it after the close has run. */ - liveCount: $newRustFunction( - "runtime/socket/socket.rs", - "TestingAPIs.jsDuplexUpgradeContextLiveCount", - 0, - ) as () => number, + liveCount: $bindgen("socket_body.bind.ts", "duplexUpgradeContextLiveCount"), }; type SerializationContext = "worker" | "window" | "postMessage" | "default"; export const structuredCloneAdvanced: ( @@ -843,21 +784,16 @@ export const spawnThreadsForTesting: (iterations: number, fd: number, parallelis // True when the installed watcher registered a real OS source (a PSI trigger // on Linux). The watcher installs silently without one when the kernel // refuses the trigger, so isMemoryPressureWatcherInstalled() cannot tell. -export const memoryPressureWatcherHasOsBackend: () => boolean = $newRustFunction( - "memory_pressure.rs", - "jsWatcherHasOsBackend", - 0, -); +export const memoryPressureWatcherHasOsBackend = $bindgen("memory_pressure.bind.ts", "watcherHasOsBackend"); // The exact bytes Bun writes to /proc/pressure/memory to arm its trigger. // null where there is no PSI backend (everything except Linux). -export const memoryPressurePsiTrigger: () => Buffer | null = $newRustFunction("memory_pressure.rs", "jsPsiTrigger", 0); +export const memoryPressurePsiTrigger = $bindgen("memory_pressure.bind.ts", "psiTrigger") as () => Buffer | null; // The PSI event filter: `armed` is the file at arm time, `polls` one read per POLLPRI. null off Linux. -export const memoryPressurePsiFilter: (armed: string, ...polls: string[]) => boolean[] | null = $newRustFunction( - "memory_pressure.rs", - "jsPsiFilter", - 1, +export const memoryPressurePsiFilter: (armed: string, ...polls: string[]) => boolean[] | null = $bindgen( + "memory_pressure.bind.ts", + "psiFilter", ); export const getEventLoopStats: () => { @@ -871,32 +807,18 @@ export const getEventLoopStats: () => { eventLoopAlive: boolean; /** usockets/libuv loop iterations so far (us_internal_loop_pre count). */ iteration: number; -} = $newRustFunction("event_loop.rs", "getActiveTasks", 0); +} = $bindgen("event_loop.bind.ts", "getActiveTasks"); export const hostedGitInfo = { - parseUrl: $newRustFunction("hosted_git_info.rs", "TestingAPIs.jsParseUrl", 1), - fromUrl: $newRustFunction("hosted_git_info.rs", "TestingAPIs.jsFromUrl", 1), + parseUrl: $bindgen("hosted_git_info_jsc.bind.ts", "parseUrl"), + fromUrl: $bindgen("hosted_git_info_jsc.bind.ts", "fromUrl"), }; -export const translateUVErrorToE: (code: number) => string | undefined = $newRustFunction( - "sys.rs", - "TestingAPIs.translateUVErrorToE", - 1, -); +export const translateUVErrorToE = $bindgen("error_jsc.bind.ts", "translateUVErrorToE"); -export const translateNtStatusToE: (status: number) => string | undefined = $newRustFunction( - "sys.rs", - "TestingAPIs.translateNtStatusToE", - 1, -); +export const translateNtStatusToE = $bindgen("error_jsc.bind.ts", "translateNtStatusToE"); -export const sigactionLayout: () => - | undefined - | { - installed: { handler: number; flags: number }; - readback: { handler: number; flags: number }; - sizeof: number; - } = $newRustFunction("sys.rs", "TestingAPIs.sigactionLayout", 0); +export const sigactionLayout = $bindgen("error_jsc.bind.ts", "sigactionLayout"); export const stringsInternals = { /** @@ -905,68 +827,46 @@ export const stringsInternals = { * path is otherwise only reachable from Windows `bun build --compile` * metadata, so this binding lets us exercise it on all platforms. */ - toUTF16AllocSentinel: $newRustFunction("string/immutable/unicode.rs", "TestingAPIs.toUTF16AllocSentinel", 1) as ( - bytes: Uint8Array, - ) => string, + toUTF16AllocSentinel: $bindgen("bun_string_jsc.bind.ts", "toUTF16AllocSentinel"), }; /** Seed the connect-path DNS cache for `hostname` via the real `process_results` interleave; returns family order. */ -export const dnsCacheSeed = $newRustFunction("runtime/dns_jsc/dns.rs", "internal.seedCacheForTesting", 2) as ( +export const dnsCacheSeed = $bindgen("dns_internals.bind.ts", "seedCacheForTesting") as ( hostname: string, addresses: string[], ) => number[]; /** Whether the connect-path resolver answers `hostname` with the loopback addresses itself instead of asking getaddrinfo. */ -export const dnsIsLocalhostName = $newRustFunction( - "runtime/dns_jsc/dns.rs", - "internal.isLocalhostNameForTesting", - 1, -) as (hostname: string) => boolean; +export const dnsIsLocalhostName = $bindgen("dns_internals.bind.ts", "isLocalhostNameForTesting"); /** Whether a getaddrinfo() answer made of `addresses` makes the connect-path resolver retry without AI_ADDRCONFIG. */ -export const dnsIsAllLoopbackOfOneFamily = $newRustFunction( - "runtime/dns_jsc/dns.rs", - "internal.isAllLoopbackOfOneFamilyForTesting", - 1, -) as (addresses: string[]) => boolean; +export const dnsIsAllLoopbackOfOneFamily = $bindgen("dns_internals.bind.ts", "isAllLoopbackOfOneFamilyForTesting") as ( + addresses: string[], +) => boolean; /** The error a getaddrinfo lookup of `hostname` reports when getaddrinfo(3) returns the `EAI_*` status named `code` (`"EAI_AGAIN"`, `"EAI_FAIL"`, `"EAI_NONAME"`). */ -export const dnsGetaddrinfoError = $newRustFunction( - "runtime/dns_jsc/dns.rs", - "internal.getaddrinfoErrorForTesting", - 2, -) as (code: string, hostname: string) => Error & { code: string; errno: number; syscall: string; hostname: string }; +export const dnsGetaddrinfoError = $bindgen("dns_internals.bind.ts", "getaddrinfoErrorForTesting") as ( + code: string, + hostname: string, +) => Error & { code: string; errno: number; syscall: string; hostname: string }; export const fetchH2Internals = { - liveCounts: $newRustFunction("http/H2Client.rs", "TestingAPIs.liveCounts", 0) as () => { - sessions: number; - streams: number; - }, + liveCounts: $bindgen("headers_jsc.bind.ts", "h2LiveCounts"), }; export const fetchH3Internals = { - liveCounts: $newRustFunction("http/H3Client.rs", "TestingAPIs.quicLiveCounts", 0) as () => { - sessions: number; - streams: number; - }, + liveCounts: $bindgen("headers_jsc.bind.ts", "h3LiveCounts"), }; -const proxyFor = $newRustFunction("runtime/webcore/fetch.rs", "TestingAPIs.proxyFor", 2) as ( - entries: string[], - url: string, -) => string | null; +const proxyFor = $bindgen("proxy_testing.bind.ts", "proxyFor"); export const proxyInternals = { /** Whether the `NO_PROXY` value `list` exempts `hostname` on `port`. */ - noProxyMatches: $newRustFunction("runtime/webcore/fetch.rs", "TestingAPIs.noProxyMatches", 3) as ( - list: string, - hostname: string, - port: number, - ) => boolean, + noProxyMatches: $bindgen("proxy_testing.bind.ts", "noProxyMatches"), /** The proxy an environment of exactly `env` selects for `url`, or null. */ proxyFor: (env: Record, url: string) => proxyFor(Object.entries(env).flat(), url), /** What the HTTP client's URL parser makes of `href`. */ - parseURL: $newRustFunction("runtime/webcore/fetch.rs", "TestingAPIs.parseURL", 1) as (href: string) => { + parseURL: $bindgen("proxy_testing.bind.ts", "parseURL") as (href: string) => { username: string; password: string; hostname: string; @@ -976,16 +876,14 @@ export const proxyInternals = { }; export const fileSinkInternals = { - liveCount: $newRustFunction("runtime/webcore/FileSink.rs", "TestingAPIs.fileSinkLiveCount", 0) as () => number, + liveCount: $bindgen("FileSink.bind.ts", "liveCount"), }; export const byteStreamInternals = { // Swap a ByteStream-backed stream's producer for one whose drain signal // re-enters on_cancel, making consumed-during-signal_drained re-entrancy // deterministic in tests. - cancelOnDrain: $newRustFunction("runtime/webcore/ByteStream.rs", "TestingAPIs.byteStreamCancelOnDrain", 1) as ( - stream: ReadableStream, - ) => void, + cancelOnDrain: $bindgen("ByteStream.bind.ts", "cancelOnDrain") as (stream: ReadableStream) => void, }; // How many internal modules (node:fs etc.) this process created from bytecode embedded by `bun build --compile diff --git a/src/js/internal/bake.ts b/src/js/internal/bake.ts index 9deb61755aac..9aeda859e711 100644 --- a/src/js/internal/bake.ts +++ b/src/js/internal/bake.ts @@ -12,11 +12,11 @@ const { serveOnFreePort, printBannerAndHandleShortcuts, } = require("internal/serve_cli"); -const setConfigPlugins = $newRustFunction("bake/config/plugins.rs", "setConfigPlugins", 2); -const isRestart = $newRustFunction("bake/config/plugins.rs", "isRestart", 0); -const restartProcess = $newRustFunction("bake/config/plugins.rs", "restartProcess", 0); -const setPages = $newRustFunction("bake/config/plugins.rs", "setPages", 1); -const checkFramework = $newRustFunction("bake/config/plugins.rs", "checkFramework", 1); +const setConfigPlugins = $bindgen("plugins.bind.ts", "setConfigPlugins"); +const isRestart = $bindgen("plugins.bind.ts", "isRestart"); +const restartProcess = $bindgen("plugins.bind.ts", "restartProcess"); +const setPages = $bindgen("plugins.bind.ts", "setPages"); +const checkFramework = $bindgen("plugins.bind.ts", "checkFramework"); // `app/server.ts` runs before the code that checks its export, and can change these on the prototypes. const ObjectKeys = Object.keys; diff --git a/src/js/internal/cluster/RoundRobinHandle.ts b/src/js/internal/cluster/RoundRobinHandle.ts index 4fa74dcc6426..facdb0f1e02a 100644 --- a/src/js/internal/cluster/RoundRobinHandle.ts +++ b/src/js/internal/cluster/RoundRobinHandle.ts @@ -3,7 +3,7 @@ const { kHandle } = require("internal/shared"); let net; -const sendHelper = $newRustFunction("node_cluster_binding.rs", "sendHelperPrimary", 4); +const sendHelper = $bindgen("node_cluster_binding.bind.ts", "sendHelperPrimary"); const uvTranslateSysError = $bindgen("node_util_binding.bind.ts", "uvTranslateSysError"); const { UV_EINVAL } = process.binding("uv"); diff --git a/src/js/internal/cluster/SharedHandle.ts b/src/js/internal/cluster/SharedHandle.ts index aa737ef5b2c9..880bce44899b 100644 --- a/src/js/internal/cluster/SharedHandle.ts +++ b/src/js/internal/cluster/SharedHandle.ts @@ -1,4 +1,4 @@ -const clusterRawBind = $newRustFunction("node_cluster_binding.rs", "clusterRawBind", 4); +const clusterRawBind = $bindgen("node_cluster_binding.bind.ts", "clusterRawBind"); const closeRawHandle = $bindgen("node_cluster_binding.bind.ts", "clusterCloseHandle"); const validateFd = $bindgen("node_cluster_binding.bind.ts", "clusterValidateFd"); diff --git a/src/js/internal/cluster/primary.ts b/src/js/internal/cluster/primary.ts index e0a2514ecca5..57dab3430c0f 100644 --- a/src/js/internal/cluster/primary.ts +++ b/src/js/internal/cluster/primary.ts @@ -5,7 +5,7 @@ const EventEmitter = require("node:events"); const Worker = require("internal/cluster/Worker"); const { kHandle } = require("internal/shared"); -const sendHelper = $newRustFunction("node_cluster_binding.rs", "sendHelperPrimary", 4); +const sendHelper = $bindgen("node_cluster_binding.bind.ts", "sendHelperPrimary"); const onInternalMessage = $bindgen("node_cluster_binding.bind.ts", "onInternalMessagePrimary"); const validateFd = $bindgen("node_cluster_binding.bind.ts", "clusterValidateFd"); const isDgramSocketFd = $bindgen("udp_socket.bind.ts", "dgramIsFdAdopted"); diff --git a/src/js/internal/fs/binding.ts b/src/js/internal/fs/binding.ts index 5d4f0efb8d8f..1f36d6a0c3d6 100644 --- a/src/js/internal/fs/binding.ts +++ b/src/js/internal/fs/binding.ts @@ -120,4 +120,4 @@ type FsCallbackArm = { ) => void; }; -export default $rust("node_fs_binding.rs", "createBinding") as FsBinding & FsCallbackArm; +export default $bindgen("node_fs_binding.bind.ts", "createBinding")() as FsBinding & FsCallbackArm; diff --git a/src/js/internal/quic/binding.ts b/src/js/internal/quic/binding.ts index d27914276a80..3b9033f8f1fe 100644 --- a/src/js/internal/quic/binding.ts +++ b/src/js/internal/quic/binding.ts @@ -1,2 +1,2 @@ // Bun's equivalent of Node's `internalBinding('quic')`. -export default $rust("node_quic_binding.rs", "createNodeQuicBinding"); +export default $bindgen("node_quic_binding.bind.ts", "createBinding")(); diff --git a/src/js/internal/serve_cli.ts b/src/js/internal/serve_cli.ts index e8b4424d8263..0a3c6cf8f634 100644 --- a/src/js/internal/serve_cli.ts +++ b/src/js/internal/serve_cli.ts @@ -6,7 +6,7 @@ const path = require("node:path"); const env = Bun.env; -const isRestarting = $newRustFunction("bake/config/plugins.rs", "isRestarting", 0); +const isRestarting = $bindgen("plugins.bind.ts", "isRestarting"); type ServeFlags = { /** `undefined` until `--host` or `--hostname` sets it. */ diff --git a/src/js/internal/sql/mysql.ts b/src/js/internal/sql/mysql.ts index ede6ef88378b..8073aaf1a995 100644 --- a/src/js/internal/sql/mysql.ts +++ b/src/js/internal/sql/mysql.ts @@ -15,7 +15,7 @@ const { createConnection: createMySQLConnection, createQuery: createMySQLQuery, init: initMySQL, -} = $rust("mysql.rs", "createBinding") as MySQLDotZig; +} = $bindgen("mysql.bind.ts", "binding") as MySQLDotZig; initMySQL(function onResolveMySQLQuery(query, result, count, is_last, last_insert_rowid, affected_rows, statement) { $assert(result instanceof SQLResultArray, "Invalid result array"); diff --git a/src/js/internal/sql/postgres.ts b/src/js/internal/sql/postgres.ts index 7227013a6730..a085899a3d26 100644 --- a/src/js/internal/sql/postgres.ts +++ b/src/js/internal/sql/postgres.ts @@ -24,7 +24,7 @@ const { createConnection: createPostgresConnection, createQuery: createPostgresQuery, init: initPostgres, -} = $rust("postgres.rs", "createBinding") as PostgresDotZig; +} = $bindgen("postgres.bind.ts", "binding") as PostgresDotZig; // 1 to 4 are the commands whose tag counts the rows written. const cmds = ["", "INSERT", "DELETE", "UPDATE", "MERGE", "SELECT", "MOVE", "FETCH", "COPY"]; diff --git a/src/js/node/_http_server.ts b/src/js/node/_http_server.ts index 958ba989a17d..c4513292b7c7 100644 --- a/src/js/node/_http_server.ts +++ b/src/js/node/_http_server.ts @@ -109,7 +109,7 @@ function traceServerRequestEnd() { traceEvents.emitEvent("e", kHttpTraceCat, "http.server.request"); } -const getBunServerAllClosedPromise = $newRustFunction("node_http_binding.rs", "getBunServerAllClosedPromise", 1); +const getBunServerAllClosedPromise = $bindgen("node_http_binding.bind.ts", "getBunServerAllClosedPromise"); const getBunServerOpenCount = $bindgen("node_http_binding.bind.ts", "getBunServerOpenCount"); const kServerResponse = Symbol("ServerResponse"); diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index 4f589e56a4ea..6b7e2718570f 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -67,7 +67,7 @@ const { scryptSync, argon2: _argon2, argon2Sync: _argon2Sync, -} = $rust("node_crypto_binding.rs", "createNodeCryptoBindingZig"); +} = $bindgen("node_crypto_binding.bind.ts", "crypto"); const normalizeEncoding = $bindgen("node_util_binding.bind.ts", "normalizeEncoding"); const timingSafeEqual = $bindgen("Crypto.bind.ts", "timingSafeEqual"); diff --git a/src/js/node/dns.ts b/src/js/node/dns.ts index 66d6dd84aae9..343595fb41c2 100644 --- a/src/js/node/dns.ts +++ b/src/js/node/dns.ts @@ -81,7 +81,7 @@ const getRuntimeDefaultResultOrderOption = $bindgen("dns_internals.bind.ts", "ge function newResolver(options) { if (!newResolver.native) { - newResolver.native = $newRustFunction("runtime/dns_jsc/dns.rs", "Resolver.newResolver", 1) as NewNativeResolver; + newResolver.native = $bindgen("dns_internals.bind.ts", "newResolver") as NewNativeResolver; } return newResolver.native(options); } diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 7439cbb7a255..cf24f68b50e5 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -269,10 +269,10 @@ function lazyBlockList() { return (BlockList ??= $bindgen("BlockList.bind.ts", "BlockList")); } const newDetachedSocket = $bindgen("node_net_binding.bind.ts", "newDetachedSocket"); -const doConnect = $newRustFunction("node_net_binding.rs", "doConnect", 2); +const doConnect = $bindgen("node_net_binding.bind.ts", "doConnect"); const addServerName = $bindgen("Listener.bind.ts", "addServerName"); -const upgradeDuplexToTLS = $newRustFunction("runtime/socket/socket.rs", "jsUpgradeDuplexToTLS", 2); +const upgradeDuplexToTLS = $bindgen("socket_body.bind.ts", "upgradeDuplexToTLS"); // tls.connect({ socket }) upgrade: hostname policy stays with this JS layer. const upgradeTLSDeferred = $bindgen("socket_body.bind.ts", "upgradeTLSDeferred"); const checkServerIdentityInHandshake = $bindgen("socket_body.bind.ts", "checkServerIdentityInHandshake"); diff --git a/src/js/node/tls.ts b/src/js/node/tls.ts index 84d009c9cc70..384184fa03d2 100644 --- a/src/js/node/tls.ts +++ b/src/js/node/tls.ts @@ -42,11 +42,11 @@ const getExtraCACertificates = $newCppFunction("NodeTLS.cpp", "getExtraCACertifi // Node warns at the first secure context; the once-per-process native load is what prints it. const maybeWarnAboutExtraCACerts = $newCppFunction("NodeTLS.cpp", "loadExtraCACertificates", 0); const getSystemCACertificates = $newCppFunction("NodeTLS.cpp", "getSystemCACertificates", 1); -const canonicalizeIP = $newCppFunction("NodeTLS.cpp", "Bun__canonicalizeIP", 1); +const canonicalizeIP = $bindgen("cares_jsc.bind.ts", "canonicalizeIP"); const parseCACertificates = $newCppFunction("NodeTLS.cpp", "parseCACertificates", 1); -const getTLSDefaultCiphers = $newCppFunction("NodeTLS.cpp", "getDefaultCiphers", 0); -const setTLSDefaultCiphers = $newCppFunction("NodeTLS.cpp", "setDefaultCiphers", 1); +const getTLSDefaultCiphers = $bindgen("rare_data.bind.ts", "getTLSDefaultCiphers"); +const setTLSDefaultCiphers = $bindgen("rare_data.bind.ts", "setTLSDefaultCiphers"); const selectServerName = $newCppFunction("NodeTLS.cpp", "selectServerName", 2); let _VALID_CIPHERS_SET: Set | undefined; function getValidCiphersSet() { diff --git a/src/js/node/util.ts b/src/js/node/util.ts index f5dea4be6f87..c47d1f277e51 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -36,7 +36,7 @@ function isFunction(value) { // https://github.com/nodejs/node/blob/main/lib/internal/util/comparisons.js const { isDeepStrictEqual } = require("internal/util/comparisons"); -const parseArgs = $newRustFunction("parse_args.rs", "parseArgs", 1); +const parseArgs = $bindgen("parse_args.bind.ts", "parseArgs"); let utl; function lazyInspectModule() { diff --git a/src/js/private.d.ts b/src/js/private.d.ts index 55f4fdd073bc..e7a18e4d78be 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -246,7 +246,7 @@ interface JSCommonJSModule { * * Binding files are located in `src/jsc/bindings` * - * @see {@link $rust} for native Rust bindings. + * @see {@link $bindgen} for native Rust bindings. * @see `src/codegen/replacements.ts` for the script that performs replacement of this funciton. * * @param filename name of the c++ file containing the function. Do not pass a path. @@ -256,36 +256,13 @@ interface JSCommonJSModule { * @returns whatever the binding function returns. */ declare function $cpp(filename: NativeFilenameCPP, symbol: string): T; -/** - * Call a native Rust binding function, getting whatever it returns. - * - * This is more like a macro; it is replaced with a WebKit intrisic during - * codegen. Passing a template parameter will break codegen. Prefer `$rust(...) - * as Foo` instead. - * - * @see {@link $cpp} for native c++ bindings. - * @see `src/codegen/replacements.ts` for the script that performs replacement of this funciton. - * - * @param filename identifier of the Rust module containing the function (see - * `rustIdentifierPaths` in `src/codegen/generate-js2native.ts`). - * @param symbol The name of the binding function. Use `dot.notation` to access - * member symbols. - * - * @returns whatever the binding function returns. - */ -declare function $rust(filename: NativeFilenameRust, symbol: string): T; declare function $newCppFunction any>( filename: NativeFilenameCPP, symbol: string, argCount: number, ): T; -declare function $newRustFunction any>( - filename: NativeFilenameRust, - symbol: string, - argCount: number, -): T; /** - * Retrieves a handle to a function or class defined in native code, defined in a + * Retrieves a handle to a function, namespace or class defined in native code, defined in a * `.bind.ts` file. For more information on how to define bindgen functions, see * [bindgen's documentation](https://bun.com/docs/project/bindgen). * `undefined` for a `*_test.bind.ts` file in a build without assertions. @@ -296,6 +273,10 @@ declare function $bindgen( + filename: File, + name: Name, +): $Bindgen.Namespaces[File][Name]; // NOTE: $debug, $assert, and $isPromiseFulfilled omitted declare module "node:stream" { diff --git a/src/jsc/BytecodeOrderRecorder.bind.ts b/src/jsc/BytecodeOrderRecorder.bind.ts new file mode 100644 index 000000000000..fc6ceb0e3eb2 --- /dev/null +++ b/src/jsc/BytecodeOrderRecorder.bind.ts @@ -0,0 +1,11 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::bytecode_order_recorder" }); + +export const namesForTesting = b.fn({ + args: { text: b.string.loose.utf8, kind: b.string.loose.utf8, chunkPaths: b.string.loose.utf8.optional }, + returns: b.any, + capability: "heap", + throws: true, + length: 3, +}); diff --git a/src/jsc/BytecodeOrderRecorder.rs b/src/jsc/BytecodeOrderRecorder.rs index faf82df17518..35961b8bad21 100644 --- a/src/jsc/BytecodeOrderRecorder.rs +++ b/src/jsc/BytecodeOrderRecorder.rs @@ -7,6 +7,7 @@ use bun_bundler::bytecode_order::{self, CodeNames, RecordedFunction, RecordedSource, Recording}; +use crate::bindgen::{self, JsHeap, JsLocal}; use crate::virtual_machine::VirtualMachine; use crate::vm::VM; @@ -396,36 +397,26 @@ fn write_replacing(path: &[u8], text: &[u8]) -> bun_sys::Maybe<()> { /// `bun:internal-for-testing`: what an order file calls the code of `text` (a `"module"` or `"script"` whose other chunks' /// paths start with the third argument, a `"builtin"`, or the specifier of an `"internal"` module, undefined if there is /// none), as `M ` and ` ` lines, or null for a text that has no names. -#[crate::host_fn] -pub fn names_for_testing( - global: &crate::JSGlobalObject, - frame: &crate::CallFrame, -) -> crate::JsResult { - let text = frame.argument(0).to_utf8(global)?; - let kind = frame.argument(1).to_utf8(global)?; - let chunk_paths = frame.argument(2); - let chunk_paths = if chunk_paths.is_undefined() { - None - } else { - Some(chunk_paths.to_utf8(global)?) - }; +pub(crate) fn names_for_testing<'a>( + heap: &JsHeap<'a>, + text: &[u8], + kind: &[u8], + chunk_paths: Option<&[u8]>, +) -> bindgen::Result<'a, JsLocal<'a>> { // One path to a line. - let mut chunk_paths: Vec<&[u8]> = bun_core::strings::split( - chunk_paths.as_ref().map_or(&b""[..], |paths| paths.slice()), - b"\n", - ) - .filter(|path| !path.is_empty()) - .collect(); + let mut chunk_paths: Vec<&[u8]> = bun_core::strings::split(chunk_paths.unwrap_or(b""), b"\n") + .filter(|path| !path.is_empty()) + .collect(); chunk_paths.sort_unstable(); let chunk = |is_esm| bytecode_order::Named::Chunk { - text: text.slice(), + text, is_esm, chunk_paths: &chunk_paths, }; - let names = match kind.slice() { + let names = match kind { b"module" => chunk(true).names(), b"script" => chunk(false).names(), - b"builtin" => bytecode_order::Named::InternalModule(text.slice()).names(), + b"builtin" => bytecode_order::Named::InternalModule(text).names(), // `text` is the specifier of one of this executable's internal modules, which are all ASCII. b"internal" => { let builtins = bun_exe_format::builtins::Builtins::parse( @@ -434,22 +425,22 @@ pub fn names_for_testing( .ok(); let Some(module) = builtins .as_ref() - .and_then(|builtins| builtins.module(builtins.find(text.slice())?)) + .and_then(|builtins| builtins.module(builtins.find(text)?)) else { - return Ok(crate::JSValue::UNDEFINED); + return Ok(JsLocal::UNDEFINED); }; bytecode_order::Named::InternalModule(module.source).names() } _ => { - return Err(global.throw(format_args!( + return Err(heap.error(format_args!( "expected \"module\", \"script\", \"builtin\" or \"internal\"" ))); } }; let Some(names) = names else { - return Ok(crate::JSValue::NULL); + return Ok(JsLocal::NULL); }; let mut lines = Vec::new(); bytecode_order::write_names(&mut lines, &names); - crate::bun_string_jsc::create_utf8_for_js(global, &lines) + heap.create_utf8_string(&lines).map(JsLocal::as_value) } diff --git a/src/jsc/CallFrame.rs b/src/jsc/CallFrame.rs index 19f618ea09cc..6608c58974a2 100644 --- a/src/jsc/CallFrame.rs +++ b/src/jsc/CallFrame.rs @@ -30,15 +30,6 @@ impl CallFrame { } } - /// Usage: `let [arg1, arg2] = call_frame.arguments_as_array::<2>();` - pub fn arguments_as_array(&self) -> [JSValue; COUNT] { - let slice = self.arguments(); - let mut value: [JSValue; COUNT] = [JSValue::UNDEFINED; COUNT]; - let n = slice.len().min(COUNT); - value[0..n].copy_from_slice(&slice[0..n]); - value - } - /// This function protects out-of-bounds access by returning undefined pub fn argument(&self, i: usize) -> JSValue { if (self.arguments_count() as usize) > i { @@ -59,12 +50,6 @@ impl CallFrame { unsafe { *self.as_unsafe_js_value_array().add(OFFSET_THIS_ARGUMENT) } } - /// `JSValue` for the current function being called. - pub fn callee(&self) -> JSValue { - // SAFETY: OFFSET_CALLEE is a valid slot in the JSC register file. - unsafe { *self.as_unsafe_js_value_array().add(OFFSET_CALLEE) } - } - /// From JavaScriptCore/interpreter/CallFrame.h /// /// | ...... | | @@ -126,22 +111,6 @@ impl CallFrame { payload as u32 } - /// Do not use this function. Migration path: - /// arguments_as_array::() - pub fn arguments_undef(&self) -> Arguments { - let slice = self.arguments(); - debug_assert!(MAX <= 9); - let count = slice.len().min(MAX); - if count == 0 { - Arguments { - ptr: [JSValue::UNDEFINED; MAX], - len: 0, - } - } else { - Arguments::::init_undef(count.min(MAX), slice) - } - } - pub fn get_caller_src_loc(&self, global_this: &JSGlobalObject) -> CallerSrcLoc { let mut str = bun_core::String::default(); let mut line: c_uint = 0; @@ -194,25 +163,6 @@ pub struct AsBits { tag: i32, } -pub struct Arguments { - pub ptr: [JSValue; MAX], - pub len: usize, -} - -impl Arguments { - #[inline] - pub(crate) fn init_undef(i: usize, src: &[JSValue]) -> Self { - let mut args: [JSValue; MAX] = [JSValue::UNDEFINED; MAX]; - args[0..i].copy_from_slice(&src[0..i]); - Self { ptr: args, len: i } - } - - #[inline] - pub fn mut_(&mut self) -> &mut [JSValue] { - &mut self.ptr[..] - } -} - pub struct CallerSrcLoc { pub str: bun_core::String, pub line: c_uint, diff --git a/src/jsc/Counters.bind.ts b/src/jsc/Counters.bind.ts new file mode 100644 index 000000000000..9c8d17b9080c --- /dev/null +++ b/src/jsc/Counters.bind.ts @@ -0,0 +1,5 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::counters" }); + +export const createCountersObject = b.fn({ returns: b.any, capability: "heap", throws: true }); diff --git a/src/jsc/Counters.rs b/src/jsc/Counters.rs index 2152650e5ca3..9beee9e5761e 100644 --- a/src/jsc/Counters.rs +++ b/src/jsc/Counters.rs @@ -1,4 +1,5 @@ -use crate::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use crate::bindgen::JsHeap; +use crate::{JSGlobalObject, JSValue, JsResult}; #[repr(C)] #[derive(Debug, Default, Clone, Copy)] @@ -32,11 +33,9 @@ impl Counters { } } -// Called through the `$rust(...)` js2native codegen, which emits the extern -// trampoline (`generate-js2native.ts` wraps this in `host_fn::host_fn_static`), -// so no `#[bun_jsc::host_fn]` attribute is needed here. -pub fn create_counters_object(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - // SAFETY: bun_vm() returns the per-thread VirtualMachine singleton; caller is on the JS thread. +pub(crate) fn create_counters_object(heap: &JsHeap<'_>) -> JsResult { + // SAFETY: the object is new and plain, and its properties are defined and not set. + let global = unsafe { heap.global() }; global.bun_vm().counters.to_js(global) } diff --git a/src/jsc/Debugger.rs b/src/jsc/Debugger.rs index d4eeb5720777..fa7ee6d0137e 100644 --- a/src/jsc/Debugger.rs +++ b/src/jsc/Debugger.rs @@ -16,7 +16,7 @@ use bun_io::KeepAlive; use bun_io::posix_event_loop::{AllocatorType, get_vm_ctx}; use crate::virtual_machine::{VirtualMachine, runtime_hooks}; -use crate::{self as jsc, CallFrame, JSGlobalObject, ZigException}; +use crate::{self as jsc, JSGlobalObject, ZigException}; bun_core::declare_scope!(debugger, visible); bun_core::declare_scope!(TestReporterAgent, visible); @@ -801,14 +801,13 @@ pub enum TestType { bun_opaque::opaque_ffi! { pub struct TestReporterHandle; } -// SAFETY (safe fn): `TestReporterHandle` and `CallFrame` are `opaque_ffi!` -// ZST handles (`!Freeze` via `UnsafeCell`); `BunString` is a `#[repr(C)]` +// SAFETY (safe fn): `TestReporterHandle` is an `opaque_ffi!` +// ZST handle (`!Freeze` via `UnsafeCell`); `BunString` is a `#[repr(C)]` // in/out-param the C++ side reads/consumes in-place. Remaining args are // by-value scalars. unsafe extern "C" { safe fn Bun__TestReporterAgentReportTestFound( agent: &mut TestReporterHandle, - call_frame: &CallFrame, test_id: c_int, name: &BunString, item_type: TestType, @@ -835,15 +834,12 @@ unsafe extern "C" { impl TestReporterHandle { pub(crate) fn report_test_found( &mut self, - call_frame: &CallFrame, test_id: i32, name: &BunString, item_type: TestType, parent_id: i32, ) { - Bun__TestReporterAgentReportTestFound( - self, call_frame, test_id, name, item_type, parent_id, - ); + Bun__TestReporterAgentReportTestFound(self, test_id, name, item_type, parent_id); } pub fn report_test_found_with_location( @@ -926,9 +922,10 @@ impl TestReporterAgent { /// Caller must ensure that it is enabled first. /// /// Since we may have to call .deinit on the name string. + /// + /// The test's location is that of the caller of the host function that is running. pub fn report_test_found( &self, - call_frame: &CallFrame, test_id: i32, name: &BunString, item_type: TestType, @@ -936,7 +933,7 @@ impl TestReporterAgent { ) { bun_core::scoped_log!(TestReporterAgent, "reportTestFound"); self.handle_mut() - .report_test_found(call_frame, test_id, name, item_type, parent_id); + .report_test_found(test_id, name, item_type, parent_id); } /// Caller must ensure that it is enabled first. diff --git a/src/jsc/JSGlobalObject.rs b/src/jsc/JSGlobalObject.rs index cd5a5e55675a..4a401cb9fa28 100644 --- a/src/jsc/JSGlobalObject.rs +++ b/src/jsc/JSGlobalObject.rs @@ -95,7 +95,7 @@ impl JSGlobalObject { pub fn throw_stack_overflow(&self) -> JsError { // Wrap the raw FFI throw in a validation scope (mirrors `VM::throw_error`). - // The Rust `#[bun_jsc::host_fn]` thunk inserts an `ExceptionValidationScope` + // A generated thunk inserts an `ExceptionValidationScope` // one frame above us; with that scope // present `~ThrowScope` inside `JSGlobalObject__throwStackOverflow` no // longer sees its previous scope as above `topEntryFrame`, so it diff --git a/src/jsc/JSSecrets.rs b/src/jsc/JSSecrets.rs index cfba538e4a41..0d4445bb5b50 100644 --- a/src/jsc/JSSecrets.rs +++ b/src/jsc/JSSecrets.rs @@ -64,11 +64,10 @@ impl crate::JobContext for SecretsJob { #[unsafe(no_mangle)] extern "C" fn Bun__Secrets__scheduleJob( global: &JSGlobalObject, - callframe: &crate::CallFrame, options: *mut SecretsJobOptions, promise: JSValue, ) { - let cx = global.js_thread(global.bun_vm().context_of_caller(callframe)); + let cx = global.js_thread_of_caller_no_frame(); crate::Job::::schedule( &cx, SecretsJob { diff --git a/src/jsc/MarkedArgumentBuffer.rs b/src/jsc/MarkedArgumentBuffer.rs index bb4df677566d..5144ee37c799 100644 --- a/src/jsc/MarkedArgumentBuffer.rs +++ b/src/jsc/MarkedArgumentBuffer.rs @@ -87,46 +87,3 @@ impl MarkedArgumentBuffer { ) } } - -/// Rust cannot parameterize a `fn` item by a const -/// fn-pointer, so this is a macro that expands to a `#[bun_jsc::host_fn]` wrapper. -#[macro_export] -macro_rules! marked_argument_buffer_wrap { - ($function:path) => {{ - #[$crate::host_fn] - pub(crate) fn wrapper( - global_this: &$crate::JSGlobalObject, - callframe: &$crate::CallFrame, - ) -> $crate::JsResult<$crate::JSValue> { - struct Context<'a> { - result: $crate::JsResult<$crate::JSValue>, - global_this: &'a $crate::JSGlobalObject, - callframe: &'a $crate::CallFrame, - } - extern "C" fn run( - this: *mut Context<'_>, - marked_argument_buffer: *mut $crate::MarkedArgumentBuffer, - ) { - // SAFETY: `this` is the `&mut ctx` passed to `MarkedArgumentBuffer::run` below; - // `marked_argument_buffer` is the live stack-allocated buffer C++ hands us. - let result = $function( - unsafe { (*this).global_this }, - unsafe { (*this).callframe }, - unsafe { &mut *marked_argument_buffer }, - ); - unsafe { (*this).result = result }; - } - - let mut ctx = Context { - global_this, - callframe, - // Placeholder; `run` - // unconditionally overwrites it before we read. - result: ::core::result::Result::Ok($crate::JSValue::ZERO), - }; - $crate::MarkedArgumentBuffer::run(&mut ctx, run); - ctx.result - } - wrapper - }}; -} diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 1d34a92849cc..b7d7d88ea71b 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -380,6 +380,23 @@ export const afterFirst = b.fn({ throws: true, }); +/** `[this, the function itself, value]`. */ +export const thisAndCallee = b.fn({ + passThis: true, + passCallee: true, + args: { value: b.any }, + returns: b.any, + capability: "heap", + throws: true, +}); +export const doubled = b.fn({ export: "Bun__BindgenTest__doubled", args: { value: b.i32 }, returns: b.i32 }); +/** A new function, made in Rust, that runs `doubled`. */ +export const makeDoubled = b.fn({ returns: b.any, capability: "js" }); +export const testNamespace = b.namespace({ + increment: { args: { value: b.i32 }, returns: b.i32 }, + decrement: { args: { value: b.i32 }, returns: b.i32 }, +}); + export const integerOrBigInt = b.fn({ args: { value: b.u32.allowBigInt }, returns: b.u32 }); export const looseInteger = b.fn({ args: { value: b.u16.loose }, returns: b.u16 }); export const looseIntegerOrBigInt = b.fn({ args: { value: b.u16.loose.allowBigInt }, returns: b.u16 }); diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 8f6284de710f..d4d603281115 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -9,9 +9,10 @@ use crate::bindgen::{ JsScope, JsStructure, JsWrapper, PinnedBytes, StringOrJsBytes, }; use crate::generated_bindgen::bindgen_test::{ - TestAnything, TestBare, TestChoice, TestColor, TestColorOrRest, TestFailure, TestFlag, - TestNames, TestOptions, TestOptionsEcho, TestOuter, TestTextOrBytes, TestValue, js_TestCounter, - js_TestError, js_TestNonFinal, js_TestNote, js_TestRefCounted, js_TestRefCountedNote, + Bun__BindgenTest__doubled, TestAnything, TestBare, TestChoice, TestColor, TestColorOrRest, + TestFailure, TestFlag, TestNames, TestOptions, TestOptionsEcho, TestOuter, TestTextOrBytes, + TestValue, js_TestCounter, js_TestError, js_TestNonFinal, js_TestNote, js_TestRefCounted, + js_TestRefCountedNote, }; use crate::{JSGlobalObject, JSString, JSValue, JsClass, JsResult}; use bun_ptr::{AsCtxPtr as _, RefPtr}; @@ -797,6 +798,37 @@ pub(crate) fn after_first<'a>( bindgen::to_js::array(heap, rest.iter().copied(), bindgen::to_js::any) } +pub(crate) fn this_and_callee<'a>( + heap: &JsHeap<'a>, + this_value: JsLocal<'a>, + callee: JsLocal<'a>, + value: JsLocal<'a>, +) -> bindgen::Result<'a, JsLocal<'a>> { + bindgen::to_js::array(heap, [this_value, callee, value], bindgen::to_js::any) +} + +pub(crate) fn doubled(value: i32) -> i32 { + value.wrapping_mul(2) +} + +pub(crate) fn make_doubled(cx: &mut JsScope<'_>) -> JSValue { + crate::JSFunction::create( + cx.global(), + "doubled", + Bun__BindgenTest__doubled, + 1, + Default::default(), + ) +} + +pub(crate) fn increment(value: i32) -> i32 { + value.wrapping_add(1) +} + +pub(crate) fn decrement(value: i32) -> i32 { + value.wrapping_sub(1) +} + pub(crate) struct TestError { code: i32, } diff --git a/src/jsc/bindings/BunObject+exports.h b/src/jsc/bindings/BunObject+exports.h index 0ff2c6293694..8098c65083bf 100644 --- a/src/jsc/bindings/BunObject+exports.h +++ b/src/jsc/bindings/BunObject+exports.h @@ -4,7 +4,6 @@ // --- Getters --- #define FOR_EACH_GETTER(macro) \ macro(FFI) \ - macro(markdown) \ macro(argv) \ macro(cron) \ macro(cwd) \ @@ -16,27 +15,6 @@ macro(s3) \ macro(valkey) \ -// --- Callbacks --- -#define FOR_EACH_CALLBACK(macro) \ - macro(build) \ - macro(color) \ - macro(connect) \ - macro(createParsedShellScript) \ - macro(file) \ - macro(jest) \ - macro(listen) \ - macro(resolve) \ - macro(resolveSync) \ - macro(serve) \ - macro(spawn) \ - macro(spawnSync) \ - macro(udpSocket) \ - macro(write) \ - -#define DECLARE_ZIG_BUN_OBJECT_CALLBACK(name) BUN_DECLARE_HOST_FUNCTION(BunObject_callback_##name); -FOR_EACH_CALLBACK(DECLARE_ZIG_BUN_OBJECT_CALLBACK); -#undef DECLARE_ZIG_BUN_OBJECT_CALLBACK - // declaration for the exported function in src/runtime/api/BunObject.rs #define DECLARE_ZIG_BUN_OBJECT_GETTER(name) extern "C" JSC::EncodedJSValue SYSV_ABI BunObject_lazyPropCb_##name(JSC::JSGlobalObject*, JSC::JSObject*); FOR_EACH_GETTER(DECLARE_ZIG_BUN_OBJECT_GETTER); @@ -51,4 +29,3 @@ FOR_EACH_GETTER(DEFINE_ZIG_BUN_OBJECT_GETTER_WRAPPER); #undef DEFINE_ZIG_BUN_OBJECT_GETTER_WRAPPER #undef FOR_EACH_GETTER -#undef FOR_EACH_CALLBACK diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index ae462b274e2b..c20aae961445 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -37,27 +37,37 @@ #include "BunObject+exports.h" #include "ErrorCode.h" #include "GeneratedArchive.h" +#include "GeneratedBlob.h" #include "GeneratedBunObject.h" #include "GeneratedBunObjectInternals.h" +#include "GeneratedColorJs.h" #include "GeneratedCrypto.h" #include "GeneratedCryptoHasher.h" #include "GeneratedCsrfJsc.h" #include "GeneratedDns.h" #include "GeneratedDnsInternals.h" +#include "GeneratedFetch.h" #include "GeneratedFetchSession.h" #include "GeneratedFilesystemRouter.h" #include "GeneratedGlob.h" #include "GeneratedHashObject.h" #include "GeneratedImage.h" #include "GeneratedInterpreter.h" +#include "GeneratedJSBundler.h" #include "GeneratedJSON5Object.h" #include "GeneratedJSONCObject.h" +#include "GeneratedJest.h" +#include "GeneratedJsBunSpawnBindings.h" +#include "GeneratedListener.h" +#include "GeneratedMarkdownObject.h" +#include "GeneratedParsedShellScript.h" #include "GeneratedPasswordObject.h" #include "GeneratedS3Client.h" #include "GeneratedSemverObject.h" #include "GeneratedTOMLObject.h" #include "GeneratedTerminal.h" #include "GeneratedTranspiler.h" +#include "GeneratedUdpSocket.h" #include "GeneratedUnsafeObject.h" #include "GeneratedValkey.h" #include "GeneratedXMLObject.h" @@ -77,10 +87,6 @@ extern "C" size_t Bun__Feature__heap_snapshot; -BUN_DECLARE_HOST_FUNCTION(Bun__fetch); -BUN_DECLARE_HOST_FUNCTION(Bun__fetchPreconnect); -BUN_DECLARE_HOST_FUNCTION(Bun__randomUUIDv7); - #include "sliceAnsi.h" #include "escapeHTML.h" @@ -340,10 +346,10 @@ static JSValue createBunSQLConstructor(VM& vm, Zig::GlobalObject* globalObject) JSValue constructBunFetchObject(VM& vm, JSObject* bunObject) { - JSFunction* fetchFn = JSFunction::create(vm, bunObject->globalObject(), 1, "fetch"_s, Bun__fetch, ImplementationVisibility::Public, NoIntrinsic); + JSFunction* fetchFn = JSFunction::create(vm, bunObject->globalObject(), 1, "fetch"_s, Bun::Bindgen::Generated::Fetch::jsFetch, ImplementationVisibility::Public, NoIntrinsic); auto* globalObject = uncheckedDowncast(bunObject->globalObject()); - fetchFn->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "preconnect"_s), 1, Bun__fetchPreconnect, ImplementationVisibility::Public, NoIntrinsic, + fetchFn->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "preconnect"_s), 1, Bun::Bindgen::Generated::Fetch::jsPreconnect, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); return fetchFn; @@ -351,7 +357,7 @@ JSValue constructBunFetchObject(VM& vm, JSObject* bunObject) static JSValue createBunShell(VM& vm, Zig::GlobalObject* globalObject) { - JSFunction* createParsedShellScript = JSFunction::create(vm, globalObject, 2, "createParsedShellScript"_s, BunObject_callback_createParsedShellScript, ImplementationVisibility::Private, NoIntrinsic); + JSFunction* createParsedShellScript = JSFunction::create(vm, globalObject, 2, "createParsedShellScript"_s, Bun::Bindgen::Generated::ParsedShellScript::jsCreateParsedShellScript, ImplementationVisibility::Private, NoIntrinsic); JSFunction* createShellInterpreterFunction = JSFunction::create(vm, globalObject, 1, "createShellInterpreter"_s, Bun::Bindgen::Generated::Interpreter::jsCreateShellInterpreter, ImplementationVisibility::Private, NoIntrinsic); JSC::JSFunction* createShellFn = JSC::JSFunction::create(vm, globalObject, shellCreateBunShellTemplateFunctionCodeGenerator(vm), globalObject); @@ -956,7 +962,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj JSONC Bun::Bindgen::Generated::JSONCObject::createJSONC DontDelete|PropertyCallback JSON5 Bun::Bindgen::Generated::JSON5Object::createJSON5 DontDelete|PropertyCallback JSONL constructJSONLObject ReadOnly|DontDelete|PropertyCallback - markdown BunObject_lazyPropCb_wrap_markdown DontDelete|PropertyCallback + markdown Bun::Bindgen::Generated::MarkdownObject::createMarkdown DontDelete|PropertyCallback TOML Bun::Bindgen::Generated::TOMLObject::createTOML DontDelete|PropertyCallback XML Bun::Bindgen::Generated::XMLObject::createXML DontDelete|PropertyCallback YAML Bun::Bindgen::Generated::YAMLObject::createYAML DontDelete|PropertyCallback @@ -967,12 +973,12 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj CSRF Bun::Bindgen::Generated::CsrfJsc::createCSRF DontDelete|PropertyCallback allocUnsafe Bun::Bindgen::Generated::BunObject::jsAllocUnsafe DontDelete|Function 1 argv BunObject_lazyPropCb_wrap_argv DontDelete|PropertyCallback - build BunObject_callback_build DontDelete|Function 1 + build Bun::Bindgen::Generated::JSBundler::jsBuild DontDelete|Function 1 concatArrayBuffers functionConcatTypedArrays DontDelete|Function 3 - connect BunObject_callback_connect DontDelete|Function 1 + connect Bun::Bindgen::Generated::Listener::jsConnect DontDelete|Function 1 cron BunObject_lazyPropCb_wrap_cron DontDelete|PropertyCallback cwd BunObject_lazyPropCb_wrap_cwd DontEnum|DontDelete|PropertyCallback - color BunObject_callback_color DontDelete|Function 2 + color Bun::Bindgen::Generated::ColorJs::jsColor DontDelete|Function 2 deepEquals functionBunDeepEquals DontDelete|Function 2 deepMatch functionBunDeepMatch DontDelete|Function 2 deflateSync Bun::Bindgen::Generated::BunObject::jsDeflateSync DontDelete|Function 1 @@ -981,7 +987,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj env constructEnvObject ReadOnly|DontDelete|PropertyCallback escapeHTML jsFunctionBunEscapeHTML DontDelete|Function 2 fetch constructBunFetchObject ReadOnly|DontDelete|PropertyCallback - file BunObject_callback_file DontDelete|Function 1 + file Bun::Bindgen::Generated::Blob::jsFile DontDelete|Function 1 fileURLToPath functionFileURLToPath DontDelete|Function 1 gc Bun::Bindgen::Generated::BunObject::jsGc DontDelete|Function 1 generateHeapSnapshot functionGenerateHeapSnapshot DontDelete|Function 2 @@ -993,9 +999,9 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj inspect BunObject_lazyPropCb_wrap_inspect DontDelete|PropertyCallback isMainThread constructIsMainThread ReadOnly|DontDelete|PropertyCallback isStandaloneExecutable BunObject_lazyPropCb_wrap_isStandaloneExecutable ReadOnly|DontDelete|PropertyCallback - jest BunObject_callback_jest DontEnum|DontDelete|Function 1 - listen BunObject_callback_listen DontDelete|Function 1 - udpSocket BunObject_callback_udpSocket DontDelete|Function 1 + jest Bun::Bindgen::Generated::Jest::jsJest DontEnum|DontDelete|Function 1 + listen Bun::Bindgen::Generated::Listener::jsListen DontDelete|Function 1 + udpSocket Bun::Bindgen::Generated::UdpSocket::jsUdpSocket DontDelete|Function 1 main bunObjectMain DontDelete|CustomAccessor mmap Bun::Bindgen::Generated::BunObject::jsMmap DontDelete|Function 1 nanoseconds functionBunNanoseconds DontDelete|Function 0 @@ -1006,7 +1012,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj pathToFileURL functionPathToFileURL DontDelete|Function 1 peek constructBunPeekObject DontDelete|PropertyCallback plugin constructPluginObject ReadOnly|DontDelete|PropertyCallback - randomUUIDv7 Bun__randomUUIDv7 DontDelete|Function 2 + randomUUIDv7 Bun::Bindgen::Generated::Crypto::jsRandomUUIDv7 DontDelete|Function 2 randomUUIDv5 Bun::Bindgen::Generated::Crypto::jsRandomUUIDv5 DontDelete|Function 3 readableStreamToArray WebCore::jsFunctionReadableStreamToArray DontDelete|Function 1 readableStreamToArrayBuffer WebCore::jsFunctionReadableStreamToArrayBuffer DontDelete|Function 1 @@ -1016,21 +1022,21 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj readableStreamToJSON WebCore::jsFunctionReadableStreamToJSON DontDelete|Function 1 readableStreamToText WebCore::jsFunctionReadableStreamToText DontDelete|Function 1 registerMacro Bun::Bindgen::Generated::BunObjectInternals::jsRegisterMacro DontEnum|DontDelete|Function 1 - resolve BunObject_callback_resolve DontDelete|Function 1 - resolveSync BunObject_callback_resolveSync DontDelete|Function 1 + resolve Bun::Bindgen::Generated::BunObject::jsResolve DontDelete|Function 1 + resolveSync Bun::Bindgen::Generated::BunObject::jsResolveSync DontDelete|Function 1 revision constructBunRevision ReadOnly|DontDelete|PropertyCallback semver Bun::Bindgen::Generated::SemverObject::createSemver ReadOnly|DontDelete|PropertyCallback sql defaultBunSQLObject DontDelete|PropertyCallback postgres defaultBunSQLObject DontDelete|PropertyCallback SQL constructBunSQLObject DontDelete|PropertyCallback - serve BunObject_callback_serve DontDelete|Function 1 + serve Bun::Bindgen::Generated::BunObject::jsServe DontDelete|Function 1 sha Bun::Bindgen::Generated::CryptoHasher::jsSha DontDelete|Function 1 shrink Bun::Bindgen::Generated::BunObject::jsShrink DontDelete|Function 0 sliceAnsi jsFunctionBunSliceAnsi DontDelete|Function 5 sleep functionBunSleep DontDelete|Function 1 sleepSync Bun::Bindgen::Generated::BunObject::jsSleepSync DontDelete|Function 1 - spawn BunObject_callback_spawn DontDelete|Function 1 - spawnSync BunObject_callback_spawnSync DontDelete|Function 1 + spawn Bun::Bindgen::Generated::JsBunSpawnBindings::jsSpawn DontDelete|Function 1 + spawnSync Bun::Bindgen::Generated::JsBunSpawnBindings::jsSpawnSync DontDelete|Function 1 stderr BunObject_lazyPropCb_wrap_stderr DontDelete|PropertyCallback stdin BunObject_lazyPropCb_wrap_stdin DontDelete|PropertyCallback stdout BunObject_lazyPropCb_wrap_stdout DontDelete|PropertyCallback @@ -1045,7 +1051,7 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj RedisClient Bun::Bindgen::Generated::Valkey::getRedisClientConstructor DontDelete|PropertyCallback redis BunObject_lazyPropCb_wrap_valkey DontDelete|PropertyCallback secrets constructSecretsObject DontDelete|PropertyCallback - write BunObject_callback_write DontDelete|Function 1 + write Bun::Bindgen::Generated::Blob::jsWrite DontDelete|Function 1 zstdCompressSync Bun::Bindgen::Generated::BunObject::jsZstdCompressSync DontDelete|Function 1 zstdDecompressSync Bun::Bindgen::Generated::BunObject::jsZstdDecompressSync DontDelete|Function 1 zstdCompress Bun::Bindgen::Generated::BunObject::jsZstdCompress DontDelete|Function 1 diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 1d0155876744..00e342dac979 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -66,6 +66,7 @@ #include "AsyncContextFrame.h" #include "ErrorCode.h" +#include "GeneratedIpcHost.h" #include "napi_handle_scope.h" #include "napi_external.h" @@ -191,7 +192,6 @@ extern "C" void Bun__ensureProcessIPCInitialized(JSGlobalObject*); extern "C" const char* Bun__githubURL; extern "C" const char* Bun__sqlite3_version(); extern "C" const char* Bun__typescript_version(); -BUN_DECLARE_HOST_FUNCTION(Bun__Process__send); extern "C" void Process__emitDisconnectEvent(Zig::GlobalObject* global); extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValue value); @@ -3144,7 +3144,7 @@ static JSValue constructProcessSend(VM& vm, JSObject* processObject) { auto* globalObject = processObject->globalObject(); if (Bun__GlobalObject__hasIPC(globalObject)) { - return JSC::JSFunction::create(vm, globalObject, 1, String("send"_s), Bun__Process__send, ImplementationVisibility::Public); + return JSC::JSFunction::create(vm, globalObject, 1, String("send"_s), Bun::Bindgen::Generated::IpcHost::jsSend, ImplementationVisibility::Public); } else { return jsUndefined(); } diff --git a/src/jsc/bindings/InspectorTestReporterAgent.cpp b/src/jsc/bindings/InspectorTestReporterAgent.cpp index dd90a8617888..e71e650115ca 100644 --- a/src/jsc/bindings/InspectorTestReporterAgent.cpp +++ b/src/jsc/bindings/InspectorTestReporterAgent.cpp @@ -31,7 +31,7 @@ enum class BunTestType : uint8_t { Describe, }; -void Bun__TestReporterAgentReportTestFound(Inspector::InspectorTestReporterAgent* agent, JSC::CallFrame* callFrame, int testId, const BunString* name, BunTestType item_type, int parentId) +void Bun__TestReporterAgentReportTestFound(Inspector::InspectorTestReporterAgent* agent, int testId, const BunString* name, BunTestType item_type, int parentId) { auto str = name->toWTFString(BunString::ZeroCopy); @@ -47,7 +47,7 @@ void Bun__TestReporterAgentReportTestFound(Inspector::InspectorTestReporterAgent ASSERT_NOT_REACHED(); } - agent->reportTestFound(callFrame, testId, str, type, parentId); + agent->reportTestFound(testId, str, type, parentId); } void Bun__TestReporterAgentReportTestFoundWithLocation(Inspector::InspectorTestReporterAgent* agent, int testId, const BunString* name, BunTestType item_type, int parentId, const BunString* sourceURL, int line) @@ -159,7 +159,7 @@ Protocol::ErrorStringOr InspectorTestReporterAgent::disable() return {}; } -void InspectorTestReporterAgent::reportTestFound(JSC::CallFrame* callFrame, int testId, const String& name, Protocol::TestReporter::TestType type, int parentId) +void InspectorTestReporterAgent::reportTestFound(int testId, const String& name, Protocol::TestReporter::TestType type, int parentId) { if (!m_enabled) return; @@ -171,7 +171,7 @@ void InspectorTestReporterAgent::reportTestFound(JSC::CallFrame* callFrame, int auto* globalObject = &m_globalObject; auto& vm = JSC::getVM(globalObject); - JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus { + JSC::StackVisitor::visit(vm.topCallFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus { if (Zig::isImplementationVisibilityPrivate(visitor)) return WTF::IterationStatus::Continue; diff --git a/src/jsc/bindings/InspectorTestReporterAgent.h b/src/jsc/bindings/InspectorTestReporterAgent.h index 5bd4e08a3cc3..042b3a0ee85f 100644 --- a/src/jsc/bindings/InspectorTestReporterAgent.h +++ b/src/jsc/bindings/InspectorTestReporterAgent.h @@ -36,7 +36,7 @@ class InspectorTestReporterAgent final : public InspectorAgentBase, public Inspe virtual Protocol::ErrorStringOr disable() final; // Public API for reporting test events - void reportTestFound(JSC::CallFrame*, int testId, const String& name, Protocol::TestReporter::TestType type = Protocol::TestReporter::TestType::Test, int parentId = -1); + void reportTestFound(int testId, const String& name, Protocol::TestReporter::TestType type = Protocol::TestReporter::TestType::Test, int parentId = -1); void reportTestFoundWithLocation(int testId, const String& name, Protocol::TestReporter::TestType type, int parentId, const String& sourceURL, int line); void reportTestStart(int testId); void reportTestEnd(int testId, Protocol::TestReporter::TestStatus status, double elapsed); diff --git a/src/jsc/bindings/JSDOMFile.cpp b/src/jsc/bindings/JSDOMFile.cpp index f9b95405c1fd..bda8507e5d64 100644 --- a/src/jsc/bindings/JSDOMFile.cpp +++ b/src/jsc/bindings/JSDOMFile.cpp @@ -1,4 +1,5 @@ #include "root.h" +#include "ErrorCode.h" #include "GeneratedBlob.h" #include "ZigGlobalObject.h" #include @@ -9,7 +10,7 @@ using Bun::Bindgen::Generated::JSBlob; using namespace JSC; -extern "C" SYSV_ABI void* JSDOMFile__construct(JSC::JSGlobalObject*, JSC::CallFrame* callframe); +extern "C" SYSV_ABI void* JSDOMFile__construct(JSC::JSGlobalObject*, EncodedJSValue bits, EncodedJSValue name, EncodedJSValue options); extern "C" SYSV_ABI bool JSDOMFile__hasInstance(EncodedJSValue, JSC::JSGlobalObject*, EncodedJSValue); // TODO: make this inehrit from JSBlob instead of InternalFunction @@ -72,7 +73,10 @@ class JSDOMFile : public JSC::InternalFunction { Structure* structure = Bun::Bindgen::structureForNewTarget(lexicalGlobalObject, callFrame, JSBlob::classIndex); RETURN_IF_EXCEPTION(scope, {}); - void* ptr = JSDOMFile__construct(lexicalGlobalObject, callFrame); + if (callFrame->argumentCount() < 2) [[unlikely]] + return Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, "new File(bits, name) expects at least 2 arguments"_s); + + void* ptr = JSDOMFile__construct(lexicalGlobalObject, JSValue::encode(callFrame->uncheckedArgument(0)), JSValue::encode(callFrame->uncheckedArgument(1)), JSValue::encode(callFrame->argument(2))); // Not RETURN_IF_EXCEPTION: it handles traps, and a termination thrown there would leak `ptr`. if (!ptr) [[unlikely]] { EXCEPTION_ASSERT(scope.exception()); diff --git a/src/jsc/bindings/JSS3File.cpp b/src/jsc/bindings/JSS3File.cpp index 1fda1821f978..957f8caee372 100644 --- a/src/jsc/bindings/JSS3File.cpp +++ b/src/jsc/bindings/JSS3File.cpp @@ -21,8 +21,8 @@ using namespace WebCore; // External C functions declarations extern "C" { -SYSV_ABI EncodedJSValue JSS3File__presign(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); -SYSV_ABI EncodedJSValue JSS3File__stat(void* ptr, JSC::JSGlobalObject*, JSC::CallFrame* callframe); +SYSV_ABI EncodedJSValue JSS3File__presign(void* ptr, JSC::JSGlobalObject*, EncodedJSValue options); +SYSV_ABI EncodedJSValue JSS3File__stat(void* ptr, JSC::JSGlobalObject*); SYSV_ABI EncodedJSValue JSS3File__bucket(void* ptr, JSC::JSGlobalObject*); } @@ -91,7 +91,8 @@ JSC_DEFINE_HOST_FUNCTION(functionS3File_presign, (JSGlobalObject * globalObject, return {}; } - return JSS3File__presign(thisObject->wrapped(), globalObject, callframe); + // `presign(undefined)` signs with the `acl` and `storageClass` of the store, `presign()` without. + return JSS3File__presign(thisObject->wrapped(), globalObject, JSValue::encode(callframe->argumentCount() ? callframe->uncheckedArgument(0) : JSValue())); } JSC_DEFINE_HOST_FUNCTION(functionS3File_stat, (JSGlobalObject * globalObject, CallFrame* callframe)) @@ -103,7 +104,7 @@ JSC_DEFINE_HOST_FUNCTION(functionS3File_stat, (JSGlobalObject * globalObject, Ca Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_THIS, "Expected a S3File instance"_s); return {}; } - return JSS3File__stat(thisObject->wrapped(), globalObject, callframe); + return JSS3File__stat(thisObject->wrapped(), globalObject); } const JSC::ClassInfo JSS3File::s_info = { "S3File"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSS3File) }; diff --git a/src/jsc/bindings/JSSecrets.cpp b/src/jsc/bindings/JSSecrets.cpp index fd3a00475ae6..d59f19ae83af 100644 --- a/src/jsc/bindings/JSSecrets.cpp +++ b/src/jsc/bindings/JSSecrets.cpp @@ -345,7 +345,7 @@ void Bun__SecretsJobOptions__deinit(SecretsJobOptions* opts) } // Native binding exports -void Bun__Secrets__scheduleJob(JSGlobalObject* global, CallFrame* callFrame, SecretsJobOptions* opts, EncodedJSValue promise); +void Bun__Secrets__scheduleJob(JSGlobalObject* global, SecretsJobOptions* opts, EncodedJSValue promise); } // extern "C" @@ -364,7 +364,7 @@ JSC_DEFINE_HOST_FUNCTION(secretsGet, (JSGlobalObject * globalObject, CallFrame* ASSERT(options); JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure()); - Bun__Secrets__scheduleJob(globalObject, callFrame, options, JSValue::encode(promise)); + Bun__Secrets__scheduleJob(globalObject, options, JSValue::encode(promise)); return JSValue::encode(promise); } @@ -379,7 +379,7 @@ JSC_DEFINE_HOST_FUNCTION(secretsSet, (JSGlobalObject * globalObject, CallFrame* ASSERT(options); JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure()); - Bun__Secrets__scheduleJob(globalObject, callFrame, options, JSValue::encode(promise)); + Bun__Secrets__scheduleJob(globalObject, options, JSValue::encode(promise)); return JSValue::encode(promise); } @@ -399,7 +399,7 @@ JSC_DEFINE_HOST_FUNCTION(secretsDelete, (JSGlobalObject * globalObject, CallFram ASSERT(options); JSPromise* promise = JSPromise::create(vm, globalObject->promiseStructure()); - Bun__Secrets__scheduleJob(globalObject, callFrame, options, JSValue::encode(promise)); + Bun__Secrets__scheduleJob(globalObject, options, JSValue::encode(promise)); return JSValue::encode(promise); } diff --git a/src/jsc/bindings/NodeHTTP.cpp b/src/jsc/bindings/NodeHTTP.cpp index 99159954a941..6853945be906 100644 --- a/src/jsc/bindings/NodeHTTP.cpp +++ b/src/jsc/bindings/NodeHTTP.cpp @@ -21,12 +21,12 @@ #include "JSSocketAddressDTO.h" #include "node/JSNodeHTTPServerSocket.h" #include "node/JSNodeHTTPServerSocketPrototype.h" +#include "GeneratedHwExports.h" namespace Bun { using namespace JSC; using namespace WebCore; -BUN_DECLARE_HOST_FUNCTION(Bun__drainMicrotasksFromJS); extern "C" EncodedJSValue Server__setAppFlags(JSC::JSGlobalObject*, EncodedJSValue, bool require_host_header, bool use_strict_method_validation, uint8_t lenient_http_flags, bool http_allow_half_open); extern "C" EncodedJSValue Server__setOnClientError(JSC::JSGlobalObject*, EncodedJSValue, EncodedJSValue); extern "C" EncodedJSValue Server__setOnConnection(JSC::JSGlobalObject*, EncodedJSValue, EncodedJSValue); @@ -976,7 +976,7 @@ JSValue createNodeHTTPInternalBinding(Zig::GlobalObject* globalObject) JSC::JSFunction::create(vm, globalObject, 2, "setServerMaxHeadersCount"_s, jsHTTPSetMaxHeadersCount, ImplementationVisibility::Public), 0); obj->putDirectNativeFunction( vm, globalObject, JSC::PropertyName(JSC::Identifier::fromString(vm, "drainMicrotasks"_s)), - 0, Bun__drainMicrotasksFromJS, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, 0); + 0, Bun::Bindgen::Generated::HwExports::jsDrainMicrotasks, ImplementationVisibility::Public, Intrinsic::NoIntrinsic, 0); return obj; } diff --git a/src/jsc/bindings/NodeTLS.cpp b/src/jsc/bindings/NodeTLS.cpp index e9098ab7f62a..65c68f69e3c3 100644 --- a/src/jsc/bindings/NodeTLS.cpp +++ b/src/jsc/bindings/NodeTLS.cpp @@ -307,21 +307,6 @@ JSC_DEFINE_HOST_FUNCTION(parseCACertificates, (JSC::JSGlobalObject * globalObjec RELEASE_AND_RETURN(scope, JSValue::encode(array)); } -// Rust side is #[host_fn(export = ...)] which emits extern "sysv64" on -// win-x64; BUN_DECLARE_HOST_FUNCTION carries SYSV_ABI so both sides agree. -BUN_DECLARE_HOST_FUNCTION(Bun__getTLSDefaultCiphers); -BUN_DECLARE_HOST_FUNCTION(Bun__setTLSDefaultCiphers); - -JSC_DEFINE_HOST_FUNCTION(getDefaultCiphers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - return Bun__getTLSDefaultCiphers(globalObject, callFrame); -} - -JSC_DEFINE_HOST_FUNCTION(setDefaultCiphers, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - return Bun__setTLSDefaultCiphers(globalObject, callFrame); -} - // (names: Map, servername: string) => T | undefined, matched by the tree a listener keeps its names in. JSC_DEFINE_HOST_FUNCTION(selectServerName, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { diff --git a/src/jsc/bindings/NodeTLS.h b/src/jsc/bindings/NodeTLS.h index 9fe8c22a72d0..ed3ef7980e97 100644 --- a/src/jsc/bindings/NodeTLS.h +++ b/src/jsc/bindings/NodeTLS.h @@ -3,14 +3,11 @@ namespace Bun { -BUN_DECLARE_HOST_FUNCTION(Bun__canonicalizeIP); JSC_DECLARE_HOST_FUNCTION(getBundledRootCertificates); JSC_DECLARE_HOST_FUNCTION(loadExtraCACertificates); JSC_DECLARE_HOST_FUNCTION(getExtraCACertificates); JSC_DECLARE_HOST_FUNCTION(getSystemCACertificates); JSC_DECLARE_HOST_FUNCTION(parseCACertificates); -JSC_DECLARE_HOST_FUNCTION(getDefaultCiphers); -JSC_DECLARE_HOST_FUNCTION(setDefaultCiphers); JSC_DECLARE_HOST_FUNCTION(selectServerName); JSC_DECLARE_HOST_FUNCTION(addBlockedSPKIForTesting); JSC_DECLARE_HOST_FUNCTION(clearBlockedSPKIsForTesting); diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 9d0093e9b556..8973df8ee6f8 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -72,6 +72,7 @@ #include "GeneratedBunObject.h" #include "GeneratedCrypto.h" #include "GeneratedHtmlRewriter.h" +#include "GeneratedPrompt.h" #include "GeneratedResolveMessage.h" #include "GeneratedResponse.h" #include "GeneratedTextDecoder.h" @@ -241,8 +242,6 @@ using namespace Bun; -BUN_DECLARE_HOST_FUNCTION(Bun__NodeUtil__jsParseArgs); - JSC_DECLARE_HOST_FUNCTION(jsFunctionMakeAbortError); using JSGlobalObject = JSC::JSGlobalObject; @@ -3038,10 +3037,6 @@ JSC::GCClient::IsoSubspace* GlobalObject::subspaceForImpl(JSC::VM& vm) [](auto& server) -> JSC::HeapCellType& { return server.m_heapCellTypeForJSWorkerGlobalScope; }); } -BUN_DECLARE_HOST_FUNCTION(WebCore__alert); -BUN_DECLARE_HOST_FUNCTION(WebCore__prompt); -BUN_DECLARE_HOST_FUNCTION(WebCore__confirm); - JSValue GlobalObject_getGlobalThis(VM& vm, JSObject* globalObject) { return uncheckedDowncast(globalObject)->globalThis(); diff --git a/src/jsc/bindings/ZigGlobalObject.lut.txt b/src/jsc/bindings/ZigGlobalObject.lut.txt index f5f74726d7d6..da9881d762ad 100644 --- a/src/jsc/bindings/ZigGlobalObject.lut.txt +++ b/src/jsc/bindings/ZigGlobalObject.lut.txt @@ -3,17 +3,17 @@ /* Source for ZigGlobalObject.lut.h @begin bunGlobalObjectTable addEventListener jsFunctionAddEventListener Function 2 - alert WebCore__alert Function 1 + alert Bun::Bindgen::Generated::Prompt::jsAlert Function 1 atob functionATOB Function 1 btoa functionBTOA Function 1 clearImmediate functionClearImmediate Function 1 clearInterval functionClearInterval Function 1 clearTimeout functionClearTimeout Function 1 - confirm WebCore__confirm Function 1 + confirm Bun::Bindgen::Generated::Prompt::jsConfirm Function 1 dispatchEvent jsFunctionDispatchEvent Function 1 fetch constructBunFetchObject PropertyCallback postMessage jsFunctionPostMessage Function 1 - prompt WebCore__prompt Function 1 + prompt Bun::Bindgen::Generated::Prompt::jsPrompt Function 1 queueMicrotask functionQueueMicrotask Function 1 removeEventListener jsFunctionRemoveEventListener Function 2 reportError functionReportError Function 1 diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index ac288933b8e4..b95e0ce78a29 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -6697,13 +6697,13 @@ CPP_DECL [[ZIG_EXPORT(check_slow)]] void Bun__JSValue__setPrototypeDirect(JSC::E return; } -CPP_DECL [[ZIG_EXPORT(nothrow)]] unsigned int Bun__CallFrame__getLineNumber(JSC::CallFrame* callFrame, JSC::JSGlobalObject* globalObject) +CPP_DECL [[ZIG_EXPORT(nothrow)]] unsigned int JSC__JSGlobalObject__getCallerLineNumber(JSC::JSGlobalObject* globalObject) { auto& vm = JSC::getVM(globalObject); JSC::LineColumn lineColumn; String sourceURL; - JSC::StackVisitor::visit(callFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus { + JSC::StackVisitor::visit(vm.topCallFrame, vm, [&](JSC::StackVisitor& visitor) -> WTF::IterationStatus { if (Zig::isImplementationVisibilityPrivate(visitor)) return WTF::IterationStatus::Continue; diff --git a/src/jsc/bindings/webcore/JSDOMFormData.cpp b/src/jsc/bindings/webcore/JSDOMFormData.cpp index f938e919c82d..18d7669a0183 100644 --- a/src/jsc/bindings/webcore/JSDOMFormData.cpp +++ b/src/jsc/bindings/webcore/JSDOMFormData.cpp @@ -54,6 +54,7 @@ #include #include #include "GeneratedBlob.h" +#include "GeneratedFormData.h" #include "GCDefferalContext.h" namespace WebCore { @@ -184,12 +185,10 @@ template<> JSValue JSDOMFormDataDOMConstructor::prototypeForStructure(JSC::VM& v return globalObject.functionPrototype(); } -BUN_DECLARE_HOST_FUNCTION(FormData__jsFunctionFromMultipartData); - template<> void JSDOMFormDataDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) { initializeBaseProperties(vm, 0, "FormData"_s, JSDOMFormData::prototype(vm, globalObject)); - putDirectNativeFunction(vm, &globalObject, vm.propertyNames->from, 1, FormData__jsFunctionFromMultipartData, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); + putDirectNativeFunction(vm, &globalObject, vm.propertyNames->from, 1, Bun::Bindgen::Generated::FormData::jsFrom, ImplementationVisibility::Public, NoIntrinsic, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | 0); } /* Hash table for prototype */ diff --git a/src/jsc/bindings/webcore/JSDOMURL.cpp b/src/jsc/bindings/webcore/JSDOMURL.cpp index 78e14db5ecdb..0c3ede088fda 100644 --- a/src/jsc/bindings/webcore/JSDOMURL.cpp +++ b/src/jsc/bindings/webcore/JSDOMURL.cpp @@ -24,6 +24,7 @@ #include "ExtendedDOMClientIsoSubspaces.h" #include "ExtendedDOMIsoSubspaces.h" +#include "GeneratedObjectURLRegistry.h" #include "IDLTypes.h" #include "JSDOMAttribute.h" #include "JSDOMBinding.h" @@ -68,9 +69,6 @@ static JSC_DECLARE_HOST_FUNCTION(jsDOMURLPrototypeFunction_toJSON); static JSC_DECLARE_HOST_FUNCTION(jsDOMURLPrototypeFunction_toString); static JSC_DECLARE_HOST_FUNCTION(jsDOMURLPrototypeFunction_inspectCustom); -BUN_DECLARE_HOST_FUNCTION(Bun__createObjectURL); -BUN_DECLARE_HOST_FUNCTION(Bun__revokeObjectURL); - // Attributes static JSC_DECLARE_CUSTOM_GETTER(jsDOMURLConstructor); @@ -136,8 +134,8 @@ using JSDOMURLDOMConstructor = JSDOMConstructor; static const HashTableValue JSDOMURLConstructorTableValues[] = { { "parse"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsDOMURLConstructorFunction_parse, 1 } }, { "canParse"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsDOMURLConstructorFunction_canParse, 1 } }, - { "createObjectURL"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Bun__createObjectURL, 1 } }, - { "revokeObjectURL"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Bun__revokeObjectURL, 1 } }, + { "createObjectURL"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Bun::Bindgen::Generated::ObjectURLRegistry::jsCreateObjectURL, 1 } }, + { "revokeObjectURL"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, Bun::Bindgen::Generated::ObjectURLRegistry::jsRevokeObjectURL, 1 } }, }; size_t JSDOMURL::estimatedSize(JSC::JSCell* cell, JSC::VM& vm) diff --git a/src/jsc/bun_string_jsc.bind.ts b/src/jsc/bun_string_jsc.bind.ts new file mode 100644 index 000000000000..b0c1d32bc958 --- /dev/null +++ b/src/jsc/bun_string_jsc.bind.ts @@ -0,0 +1,16 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::bun_string_jsc" }); + +export const escapeRegExp = b.fn({ args: { input: b.string }, returns: b.string.utf8, throws: true }); +export const escapeRegExpForPackageNameMatching = b.fn({ + args: { input: b.string }, + returns: b.string.utf8, + throws: true, +}); +export const toUTF16AllocSentinel = b.fn({ + args: { bytes: b.bytes({ from: ["buffer"] }) }, + returns: b.string, + capability: "heap", + throws: true, +}); diff --git a/src/jsc/bun_string_jsc.rs b/src/jsc/bun_string_jsc.rs index 56494cd46f60..4516352bb31c 100644 --- a/src/jsc/bun_string_jsc.rs +++ b/src/jsc/bun_string_jsc.rs @@ -1,11 +1,12 @@ //! JSC bridges for `bun_core::String` and `Utf8WithString`: the //! `StringJsc`/`Utf8WithStringJsc` extension traits and the free functions //! for bytes → JS string. Keeps `bun_core::string` free of -//! `JSValue`/`JSGlobalObject`/`CallFrame` types. +//! `JSValue`/`JSGlobalObject` types. use bun_core::{EncodedSlice, String, Tag, Utf8WithString, strings}; -use crate::{CallFrame, EncodedSliceJsc as _, JSGlobalObject, JSValue, JsError, JsResult}; +use crate::bindgen::{self, JsHeap}; +use crate::{EncodedSliceJsc as _, JSGlobalObject, JSValue, JsError, JsResult}; // ── extern decls ──────────────────────────────────────────────────────────── // `JSGlobalObject` is an opaque `UnsafeCell`-backed ZST handle and `&String`/ @@ -219,16 +220,9 @@ pub fn parse_date(this: &String, global_object: &JSGlobalObject) -> JsResult JsResult { - let input_value = call_frame.argument(0); - - if !input_value.is_string() { - return Err(global.throw(format_args!("expected string argument"))); - } - - let input = input_value.to_utf8(global)?; +// ── escapeRegExp ──────────────────────────────────────────────────────────── +pub(crate) fn escape_reg_exp(input: &String) -> JsResult> { + let input = input.to_utf8(); let mut buf: Vec = Vec::new(); @@ -237,21 +231,11 @@ pub fn js_escape_reg_exp(global: &JSGlobalObject, call_frame: &CallFrame) -> JsR return Err(JsError::OutOfMemory); } - create_utf8_for_js(global, &buf) + Ok(buf) } -#[bun_jsc::host_fn] -pub fn js_escape_reg_exp_for_package_name_matching( - global: &JSGlobalObject, - call_frame: &CallFrame, -) -> JsResult { - let input_value = call_frame.argument(0); - - if !input_value.is_string() { - return Err(global.throw(format_args!("expected string argument"))); - } - - let input = input_value.to_utf8(global)?; +pub(crate) fn escape_reg_exp_for_package_name_matching(input: &String) -> JsResult> { + let input = input.to_utf8(); let mut buf: Vec = Vec::new(); @@ -262,46 +246,27 @@ pub fn js_escape_reg_exp_for_package_name_matching( return Err(JsError::OutOfMemory); } - create_utf8_for_js(global, &buf) + Ok(buf) } -// ── unicode TestingAPIs ───────────────────────────────────────────────────── -pub mod unicode_testing_apis { - use super::*; - - /// Used in JS tests, see `internal-for-testing.ts`. - /// Exercises the `sentinel = true` path of `toUTF16AllocForReal`, which is - /// otherwise only reachable from Windows-only `bun build --compile` - /// metadata code. - #[bun_jsc::host_fn] - pub fn to_utf16_alloc_sentinel( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let arguments = callframe.arguments(); - if arguments.len() < 1 { - return Err( - global_this.throw(format_args!("toUTF16AllocSentinel: expected 1 argument")) - ); +/// Used in JS tests, see `internal-for-testing.ts`. +/// Exercises the `sentinel = true` path of `toUTF16AllocForReal`, which is +/// otherwise only reachable from Windows-only `bun build --compile` +/// metadata code. +pub(crate) fn to_utf16_alloc_sentinel<'a>( + heap: &JsHeap<'a>, + bytes: &[u8], +) -> bindgen::Result<'a, String> { + let result = match strings::to_utf16_alloc_for_real(bytes, false, true) { + Ok(r) => r, + Err(err) => { + return Err(heap.error(format_args!("{err:?} toUTF16AllocForReal failed"))); } - let Some(array_buffer) = arguments[0].as_array_buffer(global_this) else { - return Err( - global_this.throw(format_args!("toUTF16AllocSentinel: expected a Uint8Array")) - ); - }; - let bytes = array_buffer.byte_slice(); - - let result = match strings::to_utf16_alloc_for_real(bytes, false, true) { - Ok(r) => r, - Err(err) => { - return Err(global_this.throw(format_args!("{err:?} toUTF16AllocForReal failed"))); - } - }; + }; - // `to_utf16_alloc_for_real(.., sentinel=true)` includes the trailing - // NUL **in** `result.len()`, so slice it off before handing to JSC. - debug_assert_eq!(result.last().copied(), Some(0)); + // `to_utf16_alloc_for_real(.., sentinel=true)` includes the trailing + // NUL **in** `result.len()`, so slice it off before handing to JSC. + debug_assert_eq!(result.last().copied(), Some(0)); - String::clone_utf16(&result[..result.len() - 1]).into_js(global_this) - } + Ok(String::clone_utf16(&result[..result.len() - 1])) } diff --git a/src/jsc/event_loop.bind.ts b/src/jsc/event_loop.bind.ts new file mode 100644 index 000000000000..79240da8ab99 --- /dev/null +++ b/src/jsc/event_loop.bind.ts @@ -0,0 +1,5 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::event_loop" }); + +export const getActiveTasks = b.fn({ returns: b.any, capability: "heap" }); diff --git a/src/jsc/event_loop.rs b/src/jsc/event_loop.rs index 1763863bc57b..4c3d1512a873 100644 --- a/src/jsc/event_loop.rs +++ b/src/jsc/event_loop.rs @@ -17,7 +17,7 @@ use bun_uws as uws; use crate::js_promise::Status as PromiseStatus; use crate::virtual_machine::VirtualMachine; -use crate::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsResult}; +use crate::{self as jsc, JSGlobalObject, JSValue, JsResult}; // ────────────────────────────────────────────────────────────────────────── // Re-exports (thin re-exports of sibling/neighbor modules — do NOT inline @@ -1380,9 +1380,9 @@ impl EventLoop { } /// Testing API to expose event loop state -#[bun_jsc::host_fn] -pub fn get_active_tasks(global_object: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - // fields and call &-methods on it for the duration of this host fn. +pub(crate) fn get_active_tasks(heap: &crate::bindgen::JsHeap<'_>) -> JSValue { + // SAFETY: the object is new and plain, and its properties are defined and not set. + let global_object = unsafe { heap.global() }; let vm_ref = global_object.bun_vm(); let event_loop = vm_ref.event_loop_shared(); let result = JSValue::create_empty_object(global_object, 9); @@ -1442,7 +1442,7 @@ pub fn get_active_tasks(global_object: &JSGlobalObject, _frame: &CallFrame) -> J // SAFETY: usockets_loop() returns the live process-global loop. JSValue::js_number(unsafe { (*event_loop.usockets_loop()).iteration_number() } as f64), ); - Ok(result) + result } #[cfg(windows)] diff --git a/src/jsc/host_fn.rs b/src/jsc/host_fn.rs index 8b7684196517..f01543719570 100644 --- a/src/jsc/host_fn.rs +++ b/src/jsc/host_fn.rs @@ -1,11 +1,6 @@ //! Host function wrappers — the glue between Rust functions and JSC native callbacks. //! -//! The fn-wrapping machinery lives in the `#[bun_jsc::host_fn]` / -//! `#[bun_jsc::host_call]` proc-macro attributes. This file keeps: -//! - the runtime result-mapping helpers the macros call into, -//! - the FFI surface for `JSFunction` creation, -//! and stubs the reflection-driven generators (callers hand-write the -//! equivalent decode/dispatch glue until the proc-macros grow those modes). +//! The result-mapping helpers that generated thunks call into, and the FFI surface for `JSFunction` creation. use core::ffi::c_void; @@ -42,9 +37,6 @@ pub type JsHostFn = unsafe extern "sysv64" fn(*mut JSGlobalObject, *mut CallFram #[cfg(not(all(windows, target_arch = "x86_64")))] pub type JsHostFn = unsafe extern "C" fn(*mut JSGlobalObject, *mut CallFrame) -> JSValue; -/// Safe host-function shape; `#[bun_jsc::host_fn]` wraps it into a `JsHostFn`. -pub type JsHostFnZig = fn(&JSGlobalObject, &CallFrame) -> JsResult; - /// Expand to the JSC host-function ABI string for the current target. Rust /// forbids macros in `extern ""` position, but *does* accept them as the /// whole `extern` token sequence inside an item — so callers spell: @@ -68,66 +60,8 @@ macro_rules! jsc_host_abi { }; } -/// Re-exported for [`jsc_promise_handler!`] expansions so callers need no -/// direct `bun_opaque` dep. Not public API. -#[doc(hidden)] -pub use bun_opaque::opaque_deref as __opaque_deref; - -/// Define a `JsHostFn`-shaped promise-reaction shim that forwards to a safe -/// `fn(&JSGlobalObject, &CallFrame) -> JsResult` body. -/// -/// Expands to a [`jsc_host_abi!`] `extern fn` whose signature matches -/// [`JsHostFn`] (so the fn item coerces to the `JSValue::then`/`then2` -/// callback slot and to `Zig::GlobalObject::promiseHandlerID`'s address -/// comparison). The `*mut → &` derefs and `JsResult → JSValue` mapping are -/// centralised here so the caller spells zero `unsafe`. -/// -/// Two forms: -/// -/// ```ignore -/// // `#[unsafe(export_name = "Link__Symbol")]` on a locally-named shim: -/// bun_jsc::jsc_promise_handler!( -/// pub(crate) fn on_resolve_shim = "Link__Symbol" => on_resolve -/// ); -/// -/// // `#[unsafe(no_mangle)]` — the shim ident IS the link symbol: -/// bun_jsc::jsc_promise_handler!( -/// pub fn Link__Symbol => on_resolve -/// ); -/// ``` -#[macro_export] -macro_rules! jsc_promise_handler { - ($vis:vis fn $shim:ident = $link:literal => $body:path) => { - $crate::jsc_host_abi! { - #[unsafe(export_name = $link)] - $vis unsafe fn $shim( - g: *mut $crate::JSGlobalObject, - cf: *mut $crate::CallFrame, - ) -> $crate::JSValue { - let g = $crate::host_fn::__opaque_deref(g); - let cf = $crate::host_fn::__opaque_deref(cf); - $crate::host_fn::to_js_host_fn_result(g, $body(g, cf)) - } - } - }; - ($vis:vis fn $shim:ident => $body:path) => { - $crate::jsc_host_abi! { - #[unsafe(no_mangle)] - $vis unsafe fn $shim( - g: *mut $crate::JSGlobalObject, - cf: *mut $crate::CallFrame, - ) -> $crate::JSValue { - let g = $crate::host_fn::__opaque_deref(g); - let cf = $crate::host_fn::__opaque_deref(cf); - $crate::host_fn::to_js_host_fn_result(g, $body(g, cf)) - } - } - }; -} - -// Capitalized re-exports — enough call sites (and the crate-root re-export in -// lib.rs) use the acronym-caps `JSHostFn*` spelling that both must resolve. -pub use {JsHostFn as JSHostFn, JsHostFnZig as JSHostFnZig}; +// Enough call sites use the acronym-caps spelling that both must resolve. +pub use JsHostFn as JSHostFn; // ─────────────────────── host-fn wrapping (proc-macro) ─────────────────────── @@ -197,7 +131,7 @@ fn debug_exception_assertion(global_this: &JSGlobalObject, value: JSValue, func: // ──────────────────────── codegen-thunk dispatch helpers ──────────────────────── // -// The thunks of `#[bun_jsc::host_fn]`, `generate-host-exports.ts`, `generate-js2native.ts` and `generate-jssink.ts` call these. +// The thunks of `generate-host-exports.ts` and `generate-jssink.ts` call these. /// Normalize a host-fn body's return type to `JsResult`. pub trait IntoHostFnReturn { @@ -239,73 +173,6 @@ pub fn host_fn_static( host_fn_result(global, || f(global, callframe)) } -/// No exception scope. For `host`-shape exports whose -/// impl returns bare `JSValue` (e.g. `Bun__drainMicrotasksFromJS`) — wrapping -/// these in `to_js_host_call` would trip the return/exception biconditional -/// when the body legitimately leaves an exception pending. -#[inline] -pub(crate) fn host_fn_static_passthrough( - global: &JSGlobalObject, - callframe: &CallFrame, - f: impl FnOnce(&JSGlobalObject, &CallFrame) -> JSValue, -) -> JSValue { - f(global, callframe) -} - -/// Raw-pointer entry for `host`-shape exports whose `#[no_mangle]` thunk must -/// keep `(*mut JSGlobalObject, *mut CallFrame)` params so the symbol coerces -/// to [`JsHostFn`] when Rust passes it as a callback (e.g. `JSValue::then2`). -/// All other thunks take references directly. -/// -/// Both are `opaque_ffi!` ZST handles, so the `*mut → &` conversion is the -/// centralised [`bun_opaque::opaque_deref_nn`] proof (zero-byte deref). JSC's -/// host-call ABI never passes null for either argument — `globalObject` comes -/// from the running VM and `callFrame` is the on-stack frame pointer — so the -/// unchecked `_nn` variant is used to drop the two `testq; je ` pairs -/// from every `HOST_EXPORT` entry (`debug_assert!`ed in debug builds). -/// -/// # Safety -/// `global` and `callframe` must be non-null and valid for the duration of the -/// call (guaranteed by the JSC host-function ABI for every `JsHostFn` thunk). -#[track_caller] -#[inline] -pub unsafe fn host_fn_static_raw( - global: *mut JSGlobalObject, - callframe: *mut CallFrame, - f: impl FnOnce(&JSGlobalObject, &CallFrame) -> R, -) -> JSValue { - // SAFETY: JSC host-function ABI — `global`/`callframe` are always non-null. - let (global, callframe) = unsafe { - ( - JSGlobalObject::opaque_ref_nn(global), - CallFrame::opaque_ref_nn(callframe), - ) - }; - host_fn_static(global, callframe, f) -} - -/// Raw-pointer entry for `host`-shape exports, no exception scope. -/// See [`host_fn_static_raw`]. -/// -/// # Safety -/// `global` and `callframe` must be non-null and valid for the duration of the -/// call (guaranteed by the JSC host-function ABI for every `JsHostFn` thunk). -#[inline] -pub unsafe fn host_fn_static_passthrough_raw( - global: *mut JSGlobalObject, - callframe: *mut CallFrame, - f: impl FnOnce(&JSGlobalObject, &CallFrame) -> JSValue, -) -> JSValue { - // SAFETY: JSC host-function ABI — `global`/`callframe` are always non-null. - let (global, callframe) = unsafe { - ( - JSGlobalObject::opaque_ref_nn(global), - CallFrame::opaque_ref_nn(callframe), - ) - }; - host_fn_static_passthrough(global, callframe, f) -} - /// Lazy property creator / free getter: `fn(&JSGlobalObject) -> R`. Used by /// `generate-host-exports.ts` for the `BunObject__createX` / `Process__getX` /// shape (no `CallFrame`, no `this`). @@ -468,7 +335,6 @@ pub fn new_function_with_data( data: *mut c_void, ) -> JSValue { jsc::mark_binding(); - // The caller passes an already-wrapped `JsHostFn` (produced by `#[bun_jsc::host_fn]`). private::Bun__CreateFFIFunctionWithDataValue( global_object, symbol_name, diff --git a/src/jsc/host_object.rs b/src/jsc/host_object.rs deleted file mode 100644 index eaaab1c41e69..000000000000 --- a/src/jsc/host_object.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Host-function table builders — collapse the open-coded ladder -//! -//! ```ignore -//! let obj = JSValue::create_empty_object(global, N); -//! obj.put(global, b"foo", JSFunction::create(global, "foo", __jsc_host_foo, 1, Default::default())); -//! obj.put(global, b"bar", JSFunction::create(global, "bar", __jsc_host_bar, 2, Default::default())); -//! obj -//! ``` -//! -//! into a single declarative slice. -//! -//! Also fixes the capacity-hint drift bug class: `UnsafeObject.rs` passed -//! `len = 3` for 4 entries because the hand-counted N wasn't bumped when -//! `memoryFootprint` was added. - -use crate::{JSFunction, JSGlobalObject, JSHostFn, JSValue}; - -/// One row of a host-function table: `(js_visible_name, raw C-ABI shim, arity)`. -/// -/// The shim is the `__jsc_host_*` fn emitted by `#[bun_jsc::host_fn]` (already -/// a [`JSHostFn`]); `arity` is the JS `function.length` reported to userland. -pub type HostFnEntry = (&'static str, JSHostFn, u32); - -/// `{ name₀: fn₀, name₁: fn₁, … }` — fresh object pre-sized to `fns.len()` inline-capacity slots. -pub fn create_host_function_object(global: &JSGlobalObject, fns: &[HostFnEntry]) -> JSValue { - let object = JSValue::create_empty_object(global, fns.len()); - for &(name, host_fn, arity) in fns { - object.put( - global, - name.as_bytes(), - JSFunction::create(global, name, host_fn, arity, Default::default()), - ); - } - object -} diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index e5cc7e770e67..fc2c8efc3548 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -32,11 +32,10 @@ extern crate self as bun_jsc; use core::ffi::{c_char, c_void}; // ────────────────────────────────────────────────────────────────────────── -// Proc-macro re-exports. `#[bun_jsc::host_fn]` / -// `#[bun_jsc::host_call]` are implemented in the `bun_jsc_macros` crate +// Proc-macro re-exports. `#[bun_jsc::host_call]` is implemented in the `bun_jsc_macros` crate // (Rust forbids `proc-macro = true` crates from exporting non-macro items). // ────────────────────────────────────────────────────────────────────────── -pub use bun_jsc_macros::{JsAffine, host_call, host_fn}; +pub use bun_jsc_macros::{JsAffine, host_call}; // ────────────────────────────────────────────────────────────────────────── // Submodules. Each `#[path]` points at the actual PascalCase / snake_case @@ -434,8 +433,6 @@ pub mod bytecode_order_recorder; pub mod cached_bytecode; #[path = "DOMFormData.rs"] pub mod dom_form_data; -#[path = "host_object.rs"] -pub mod host_object; #[path = "JSArrayIterator.rs"] pub mod js_array_iterator; #[path = "JSGlobalObject.rs"] @@ -678,23 +675,8 @@ impl ThrowFmtArgs for core::fmt::Arguments<'_> { pub use bun_core::mark_binding; pub use self::host_fn::{ - JSHostFn, JSHostFnZig, from_js_host_call, from_js_host_call_generic, to_js_host_call, - to_js_host_fn_result, + JSHostFn, from_js_host_call, from_js_host_call_generic, to_js_host_call, to_js_host_fn_result, }; -pub use self::host_object::{HostFnEntry, create_host_function_object}; - -// Compile-time smoke test for the `host_fn` proc-macro (no runtime body — -// just asserts the expansion type-checks against the real -// `JSGlobalObject`/`CallFrame`/`JSValue`/`JsResult` shapes). -#[cfg(test)] -mod __macro_smoke { - use super::{CallFrame, JSGlobalObject, JSValue, JsResult}; - - #[crate::host_fn(export = "SmokeFree__call")] - fn smoke_free(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - Ok(JSValue::UNDEFINED) - } -} // JSC Classes Bindings — re-exported from their per-type modules (declared // above with `#[path = "…"] pub mod …;`). These were previously placeholder diff --git a/src/jsc/modules/NodeBufferModule.h b/src/jsc/modules/NodeBufferModule.h index 827ba91b775f..9e111b968096 100644 --- a/src/jsc/modules/NodeBufferModule.h +++ b/src/jsc/modules/NodeBufferModule.h @@ -5,6 +5,7 @@ #include "../bindings/JSBuffer.h" #include "ErrorCode.h" #include "GeneratedBlob.h" +#include "GeneratedObjectURLRegistry.h" #include "JavaScriptCore/PageCount.h" #include "NodeValidator.h" #include "_NativeModule.h" @@ -124,8 +125,6 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferConstructorFunction_isAscii, JSValue::encode(jsBoolean(simdutf::validate_ascii(ptr, byteLength)))); } -BUN_DECLARE_HOST_FUNCTION(jsFunctionResolveObjectURL); - BUN_DECLARE_HOST_FUNCTION(jsBufferTranscode); JSC_DEFINE_CUSTOM_GETTER(jsGetter_INSPECT_MAX_BYTES, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) @@ -199,7 +198,7 @@ DEFINE_NATIVE_MODULE(NodeBuffer) put(JSC::Identifier::fromString(vm, "transcode"_s), transcode); - auto* resolveObjectURL = JSC::JSFunction::create(vm, globalObject, 1, "resolveObjectURL"_s, jsFunctionResolveObjectURL, ImplementationVisibility::Public, NoIntrinsic); + auto* resolveObjectURL = JSC::JSFunction::create(vm, globalObject, 1, "resolveObjectURL"_s, Bun::Bindgen::Generated::ObjectURLRegistry::jsResolveObjectURL, ImplementationVisibility::Public, NoIntrinsic); put(JSC::Identifier::fromString(vm, "resolveObjectURL"_s), resolveObjectURL); diff --git a/src/jsc/modules/NodeModuleModule.cpp b/src/jsc/modules/NodeModuleModule.cpp index 20cafc782950..7bffa59c6af3 100644 --- a/src/jsc/modules/NodeModuleModule.cpp +++ b/src/jsc/modules/NodeModuleModule.cpp @@ -26,15 +26,13 @@ #include "BunString.h" #include "GeneratedNodeModuleModule.h" +#include "GeneratedResolverJsc.h" #include "GeneratedSourcemap.h" namespace Bun { using namespace JSC; -BUN_DECLARE_HOST_FUNCTION(Bun__JSSourceMap__find); - -BUN_DECLARE_HOST_FUNCTION(Resolver__nodeModulePathsForJS); JSC_DECLARE_HOST_FUNCTION(jsFunctionFindPath); JSC_DECLARE_HOST_FUNCTION(jsFunctionIsBuiltinModule); JSC_DECLARE_HOST_FUNCTION(jsFunctionNodeModuleCreateRequire); @@ -991,7 +989,7 @@ _extensions getModuleExtensionsObject PropertyCallback _findPath jsFunctionFindPath Function 3 _initPaths JSBuiltin Function|Builtin 0 _load jsFunctionLoad Function 1 -_nodeModulePaths Resolver__nodeModulePathsForJS Function 1 +_nodeModulePaths &Bun::Bindgen::Generated::ResolverJsc::jsNodeModulePaths Function 1 _pathCache getPathCacheObject PropertyCallback _preloadModules jsFunctionPreloadModules Function 0 _resolveFilename nodeModuleResolveFilename CustomAccessor @@ -1001,7 +999,7 @@ builtinModules getBuiltinModulesObject PropertyCallback constants getConstantsObject PropertyCallback createRequire jsFunctionNodeModuleCreateRequire Function 1 enableCompileCache jsFunctionEnableCompileCache Function 1 -findSourceMap Bun__JSSourceMap__find Function 1 +findSourceMap Bun::Bindgen::Generated::Sourcemap::jsFindSourceMap Function 1 flushCompileCache jsFunctionFlushCompileCache Function 0 getCompileCacheDir jsFunctionGetCompileCacheDir Function 0 globalPaths getGlobalPathsObject PropertyCallback diff --git a/src/jsc/rare_data.bind.ts b/src/jsc/rare_data.bind.ts new file mode 100644 index 000000000000..89fba4ff7224 --- /dev/null +++ b/src/jsc/rare_data.bind.ts @@ -0,0 +1,6 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::rare_data" }); + +export const setTLSDefaultCiphers = b.fn({ args: { ciphers: b.any }, capability: "js", throws: true }); +export const getTLSDefaultCiphers = b.fn({ returns: b.string.utf8, capability: "heap" }); diff --git a/src/jsc/rare_data.rs b/src/jsc/rare_data.rs index 9cb834e56697..9fedc34bdff3 100644 --- a/src/jsc/rare_data.rs +++ b/src/jsc/rare_data.rs @@ -3,9 +3,10 @@ use core::ptr::NonNull; use core::sync::atomic::Ordering; use std::sync::Arc; +use crate::bindgen::{JsHeap, JsLocal, JsScope}; use crate::strong::Optional as Strong; use crate::virtual_machine::VirtualMachine; -use crate::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use crate::{JSGlobalObject, JsResult}; use bun_boringssl::c as boring; use bun_collections::StringArrayHashMap; use bun_core::strings; @@ -1077,17 +1078,12 @@ extern "C" fn Bun__Process__getStdinFdType(vm: &VirtualMachine, fd: i32) -> Stdi // TLS default ciphers JS bindings // ────────────────────────────────────────────────────────────────────────── -#[crate::host_fn(export = "Bun__setTLSDefaultCiphers")] -fn set_tls_default_ciphers_from_js( - global_this: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let args = callframe.arguments(); - let ciphers = if !args.is_empty() { - args[0] - } else { - JSValue::UNDEFINED - }; +pub(crate) fn set_tls_default_ciphers<'a>( + cx: &mut JsScope<'a>, + ciphers: JsLocal<'a>, +) -> JsResult<()> { + let global_this = cx.global(); + let ciphers = ciphers.to_js(); if !ciphers.is_string() { return Err(global_this.throw_invalid_argument_type_value(b"ciphers", b"string", ciphers)); } @@ -1099,21 +1095,18 @@ fn set_tls_default_ciphers_from_js( .as_mut() .rare_data() .set_tls_default_ciphers(utf8.slice()); - Ok(JSValue::UNDEFINED) + Ok(()) } -#[crate::host_fn(export = "Bun__getTLSDefaultCiphers")] -fn get_tls_default_ciphers_from_js( - global_this: &JSGlobalObject, - _callframe: &CallFrame, -) -> JsResult { +pub(crate) fn get_tls_default_ciphers<'a>(heap: &JsHeap<'a>) -> &'a [u8] { + // SAFETY: only the VM is taken from it. + let global_this = unsafe { heap.global() }; // `bun_vm()` is the safe BACKREF accessor; see above. let rare = global_this.bun_vm().as_mut().rare_data(); - let bytes = match rare.tls_default_ciphers() { + match rare.tls_default_ciphers() { Some(c) => c, None => uws::get_default_ciphers().as_bytes(), - }; - crate::bun_string_jsc::create_utf8_for_js(global_this, bytes) + } } // ────────────────────────────────────────────────────────────────────────── diff --git a/src/jsc/resolver_jsc.bind.ts b/src/jsc/resolver_jsc.bind.ts new file mode 100644 index 000000000000..bd607e2359fa --- /dev/null +++ b/src/jsc/resolver_jsc.bind.ts @@ -0,0 +1,9 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export const _nodeModulePaths = b.fn({ + args: { path: b.any }, + returns: b.any, + capability: "js", + throws: true, + rustPath: "crate::resolver_jsc::node_module_paths", +}); diff --git a/src/jsc/resolver_jsc.rs b/src/jsc/resolver_jsc.rs index dc9cb1ff271c..9dd49d0a5e7b 100644 --- a/src/jsc/resolver_jsc.rs +++ b/src/jsc/resolver_jsc.rs @@ -4,15 +4,16 @@ use crate::HostReturn as _; use bstr::BStr; -use crate::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use crate::bindgen::{JsLocal, JsScope}; +use crate::{JSGlobalObject, JSValue, JsResult}; use bun_core::{String as BunString, strings}; use bun_paths::resolve_path; use bun_paths::{Platform, SEP, SEP_STR}; -#[crate::host_fn(export = "Resolver__nodeModulePathsForJS")] -fn node_module_paths_for_js(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn node_module_paths<'a>(cx: &mut JsScope<'a>, path: JsLocal<'a>) -> JsResult { crate::mark_binding!(); - let argument: JSValue = frame.argument(0); + let global = cx.global(); + let argument: JSValue = path.to_js(); if argument.is_empty() || !argument.is_string() { return Err(global.throw_invalid_argument_type("nodeModulePaths", "path", "string")); diff --git a/src/jsc/virtual_machine_exports.bind.ts b/src/jsc/virtual_machine_exports.bind.ts new file mode 100644 index 000000000000..17dc9187d36b --- /dev/null +++ b/src/jsc/virtual_machine_exports.bind.ts @@ -0,0 +1,10 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::virtual_machine_exports" }); + +export const setSyntheticAllocationLimitForTesting = b.fn({ + args: { limit: b.any }, + returns: b.usize, + capability: "js", + throws: true, +}); diff --git a/src/jsc/virtual_machine_exports.rs b/src/jsc/virtual_machine_exports.rs index 3eea15b5d2d2..cbbbed0b58b8 100644 --- a/src/jsc/virtual_machine_exports.rs +++ b/src/jsc/virtual_machine_exports.rs @@ -1,8 +1,8 @@ use core::ffi::c_void; +use crate::bindgen::{JsLocal, JsScope}; use crate::{ - CallFrame, JSGlobalObject, JSPromise, JSValue, JsResult, Strong, Task, - VirtualMachineRef as VirtualMachine, + JSGlobalObject, JSPromise, JSValue, JsResult, Strong, Task, VirtualMachineRef as VirtualMachine, }; use bun_core::String as BunString; use bun_sourcemap::SourceProviderMap; @@ -81,9 +81,6 @@ pub fn set_sampling_profiler_directory(this: &mut VirtualMachine, directory: &Bu this.sampling_profiler_directory = Some(absolute_directory(directory)); } -// `Bun__Process__send` lives in `bun_runtime::ipc_host` (its body — via -// `do_send` — names the `bun_runtime::Listener` type; LAYERING). - // HOST_EXPORT(Bun__reportUnhandledError, c) pub fn report_unhandled_error(global: &JSGlobalObject, value: JSValue) { crate::mark_binding!(); @@ -296,19 +293,12 @@ pub fn remove_source_provider_source_map( .remove_source_provider(opaque_source_provider, slice.slice()); } -#[crate::host_fn(export = "Bun__setSyntheticAllocationLimitForTesting")] -pub fn Bun__setSyntheticAllocationLimitForTesting( - global: &JSGlobalObject, - frame: &CallFrame, -) -> JsResult { - let [arg] = frame.arguments_as_array::<1>(); - if frame.arguments_count() < 1 { - return Err(global.throw_not_enough_arguments( - "setSyntheticAllocationLimitForTesting", - 1, - frame.arguments_count() as usize, - )); - } +pub(crate) fn set_synthetic_allocation_limit_for_testing<'a>( + cx: &mut JsScope<'a>, + limit: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let arg = limit.to_js(); if !arg.is_number() { return Err(global.throw_invalid_arguments(format_args!( @@ -322,5 +312,5 @@ pub fn Bun__setSyntheticAllocationLimitForTesting( .swap(limit, core::sync::atomic::Ordering::Relaxed); crate::virtual_machine::STRING_ALLOCATION_LIMIT .store(limit, core::sync::atomic::Ordering::Relaxed); - Ok(JSValue::js_number(prev as f64)) + Ok(prev) } diff --git a/src/jsc/webcore_types.rs b/src/jsc/webcore_types.rs index 316b67ed5719..561a529b9b70 100644 --- a/src/jsc/webcore_types.rs +++ b/src/jsc/webcore_types.rs @@ -10,9 +10,7 @@ //! Everything that touches the //! event loop / fs / network stays in `bun_runtime`. //! -//! `BuildArtifact` is **not** hoisted here: its `#[host_fn]`-decorated accessors -//! must live in an inherent `impl` (the macro emits a sibling shim referencing -//! `Self`), and those accessors call `BlobExt` methods. With no lower-tier +//! `BuildArtifact` is **not** hoisted here: its accessors call `BlobExt` methods. With no lower-tier //! consumer, the canonical `BuildArtifact` stays in `bun_runtime::api`. use core::cell::Cell; diff --git a/src/jsc_macros/lib.rs b/src/jsc_macros/lib.rs index 67e874f95cce..9f6620f78cd7 100644 --- a/src/jsc_macros/lib.rs +++ b/src/jsc_macros/lib.rs @@ -6,7 +6,7 @@ //! shims from a proc-macro instead. use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; +use proc_macro2::TokenStream as TokenStream2; use quote::{format_ident, quote}; use syn::{ FnArg, Ident, ItemFn, LitStr, Token, @@ -15,107 +15,6 @@ use syn::{ spanned::Spanned, }; -// ────────────────────────────────────────────────────────────────────────── -// #[bun_jsc::host_fn] / #[bun_jsc::host_fn(export = "Name")] -// ────────────────────────────────────────────────────────────────────────── - -#[derive(Default)] -struct HostFnArgs { - export: Option, -} - -impl Parse for HostFnArgs { - fn parse(input: ParseStream) -> syn::Result { - let mut out = HostFnArgs::default(); - while !input.is_empty() { - let ident: Ident = input.parse()?; - match ident.to_string().as_str() { - "export" => { - input.parse::()?; - out.export = Some(input.parse()?); - } - other => { - return Err(syn::Error::new( - ident.span(), - format!("unknown #[host_fn] argument `{other}`"), - )); - } - } - if input.peek(Token![,]) { - input.parse::()?; - } - } - Ok(out) - } -} - -/// Emit an extern shim with the JSC calling convention. -/// The body is duplicated under two `#[cfg]` arms because Rust does not accept -/// a macro/const in ABI-string position. -/// -/// `export_name = None` means no `#[export_name]` is emitted (Rust mangling -/// applies). -fn jsc_extern_fn( - export_name: Option<&str>, - sig_args: &TokenStream2, - ret: &TokenStream2, - body: &TokenStream2, -) -> TokenStream2 { - let export_attr = export_name.map(|n| { - let lit = LitStr::new(n, Span::call_site()); - quote! { #[unsafe(export_name = #lit)] } - }); - quote! { - #[cfg(all(windows, target_arch = "x86_64"))] - #export_attr - #[doc(hidden)] - pub unsafe extern "sysv64" fn #sig_args -> #ret { #body } - - #[cfg(not(all(windows, target_arch = "x86_64")))] - #export_attr - #[doc(hidden)] - pub unsafe extern "C" fn #sig_args -> #ret { #body } - } -} - -/// Applies to a `fn(global: &JSGlobalObject, frame: &CallFrame) -> JsResult`. -#[proc_macro_attribute] -pub fn host_fn(attr: TokenStream, item: TokenStream) -> TokenStream { - let args = parse_macro_input!(attr as HostFnArgs); - let func = parse_macro_input!(item as ItemFn); - expand_host_fn(&args, &func).into() -} - -fn expand_host_fn(args: &HostFnArgs, func: &ItemFn) -> TokenStream2 { - let fn_name = &func.sig.ident; - - // No default export name: the bare ident of a common name (`parse`, `crc32`) collides at link time. - let export: Option = args.export.as_ref().map(|l| l.value()); - let shim_ident = format_ident!("__jsc_host_{}", fn_name); - - let shim = jsc_extern_fn( - export.as_deref(), - "e! { - #shim_ident( - __global: *mut ::bun_jsc::JSGlobalObject, - __frame: *mut ::bun_jsc::CallFrame, - ) - }, - "e! { ::bun_jsc::JSValue }, - "e! { - // SAFETY: JSC guarantees both pointers are live for the call. - let __g = unsafe { &*__global }; - let __f = unsafe { &*__frame }; - ::bun_jsc::host_fn::host_fn_result(__g, || #fn_name(__g, __f)) - }, - ); - - quote! { - #func - #shim - } -} - // ────────────────────────────────────────────────────────────────────────── // #[bun_jsc::host_call] — bare ABI rewrite for non-JSHostFn signatures // (e.g. a static getter, `fn(*mut JSGlobalObject, JSValue, *const c_void) -> JSValue`). diff --git a/src/patch_jsc/build.rs b/src/patch_jsc/build.rs new file mode 100644 index 000000000000..c6f80de0409e --- /dev/null +++ b/src/patch_jsc/build.rs @@ -0,0 +1,35 @@ +// Build scripts run on the host before bun_* crates are compiled; std is the only option. +#![allow( + clippy::disallowed_methods, + clippy::disallowed_types, + clippy::disallowed_macros +)] +//! Export `BUN_CODEGEN_DIR` for `generated_bindgen.rs`, which `include!`s what `src/codegen/bindgen.ts` writes. + +use std::env; +use std::path::{Path, PathBuf}; + +fn main() { + let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let repo = manifest + .parent() + .and_then(Path::parent) + .expect("repo root from CARGO_MANIFEST_DIR") + .to_path_buf(); + + let codegen_dir = env::var("BUN_CODEGEN_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| repo.join("build/debug/codegen")); + + let generated = codegen_dir.join("generated_bindgen_bun_patch_jsc.rs"); + if !generated.exists() { + panic!( + "generated_bindgen_bun_patch_jsc.rs not found at {} — run `bun bd` first", + generated.display() + ); + } + + println!("cargo:rerun-if-changed={}", generated.display()); + println!("cargo:rustc-env=BUN_CODEGEN_DIR={}", codegen_dir.display()); + println!("cargo:rerun-if-env-changed=BUN_CODEGEN_DIR"); +} diff --git a/src/patch_jsc/generated_bindgen.rs b/src/patch_jsc/generated_bindgen.rs new file mode 100644 index 000000000000..ea3666ba4875 --- /dev/null +++ b/src/patch_jsc/generated_bindgen.rs @@ -0,0 +1,6 @@ +//! What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. + +include!(concat!( + env!("BUN_CODEGEN_DIR"), + "/generated_bindgen_bun_patch_jsc.rs" +)); diff --git a/src/patch_jsc/lib.rs b/src/patch_jsc/lib.rs index 81360404d3d9..4e2583099b87 100644 --- a/src/patch_jsc/lib.rs +++ b/src/patch_jsc/lib.rs @@ -1,3 +1,4 @@ #![warn(unused_must_use)] -pub mod testing; +pub(crate) mod generated_bindgen; +pub(crate) mod testing; diff --git a/src/patch_jsc/testing.bind.ts b/src/patch_jsc/testing.bind.ts new file mode 100644 index 000000000000..d32b8061025f --- /dev/null +++ b/src/patch_jsc/testing.bind.ts @@ -0,0 +1,25 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::testing::TestingAPIs" }); + +export const parse = b.fn({ + args: { patchfile: b.string.loose.utf8 }, + returns: b.string.utf8, + capability: "heap", + throws: true, +}); + +/** `dir` is converted before `patchfile`. */ +export const apply = b.fn({ + args: { patchfile: b.any, dir: b.any }, + returns: b.bool, + capability: "js", + throws: true, +}); + +export const makeDiff = b.fn({ + args: { oldFolder: b.string.loose.utf8, newFolder: b.string.loose.utf8 }, + returns: b.string.utf8, + capability: "js", + throws: true, +}); diff --git a/src/patch_jsc/testing.rs b/src/patch_jsc/testing.rs index d8ffdb160e56..8f080938b2f9 100644 --- a/src/patch_jsc/testing.rs +++ b/src/patch_jsc/testing.rs @@ -1,53 +1,42 @@ //! JS testing bindings for `bun.patch`. Keeps `src/patch/` free of JSC types. -use bun_jsc::bun_string_jsc; -use bun_jsc::{ArgumentsSlice, CallFrame, JSGlobalObject, JSValue, JsResult, SysErrorJsc}; +use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult, SysErrorJsc}; use bun_patch::{ApplyError, ParseErr, ParseErrAt, PatchFile, git_diff_internal, parse_patch_file}; use bun_sys::{Fd, FdExt}; pub(crate) struct TestingAPIs; impl TestingAPIs { - // `#[bun_jsc::host_fn]` Free-kind shim emits an unqualified - // `fn_name(g, f)` call, so it cannot wrap an associated fn. The C-ABI - // shim is emitted at module scope below (`__jsc_host_*`). - pub(crate) fn make_diff(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - // SAFETY: `bun_vm()` never returns null for a Bun-owned global; the VM - // outlives this call frame. - let mut arguments = ArgumentsSlice::init(global.bun_vm(), frame.arguments()); - - let Some(old_folder_jsval) = arguments.next_eat() else { - return Err(global.throw(format_args!("expected 2 strings"))); - }; - let old_folder_bunstr = old_folder_jsval.to_bun_string(global)?; - - let Some(new_folder_jsval) = arguments.next_eat() else { - return Err(global.throw(format_args!("expected 2 strings"))); - }; - let new_folder_bunstr = new_folder_jsval.to_bun_string(global)?; - - let old_folder = old_folder_bunstr.to_utf8(); - let new_folder = new_folder_bunstr.to_utf8(); - + pub(crate) fn make_diff( + cx: &mut JsScope<'_>, + old_folder: &[u8], + new_folder: &[u8], + ) -> JsResult> { + let global = cx.global(); // `git_diff_internal` routes through `bun_spawn::sync`, which on // Windows derefs `WindowsOptions.loop_` — supply the JS event loop. // `global.bun_vm().event_loop()` is the live per-thread `jsc::EventLoop`. let mut loop_ = bun_jsc::AnyEventLoop::js(global.bun_vm().event_loop().cast()); - let diff = match git_diff_internal(old_folder.slice(), new_folder.slice(), &mut loop_) { + let diff = match git_diff_internal(old_folder, new_folder, &mut loop_) { Ok(d) => d, Err(e) => return Err(global.throw_error(e, "failed to make diff")), }; - match diff { - Ok(s) => bun_string_jsc::create_utf8_for_js(global, s.as_slice()), - Err(e) => Err(global.throw(format_args!( + diff.map_err(|e| { + global.throw(format_args!( "failed to make diff: {}", bstr::BStr::new(e.as_slice()) - ))), - } + )) + }) } - pub(crate) fn apply(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let args = Self::parse_apply_args(global, frame)?; + pub(crate) fn apply<'a>( + cx: &mut JsScope<'a>, + patchfile: JsLocal<'a>, + dir: JsLocal<'a>, + ) -> JsResult { + let global = cx.global(); + let args = Self::parse_apply_args(global, patchfile.to_js(), dir.to_js())?; let patchfile: PatchFile<'_> = parse_patch_file(&args.patchfile_txt).expect("validated in parse_apply_args"); @@ -62,26 +51,16 @@ impl TestingAPIs { } } - Ok(JSValue::TRUE) + Ok(true) } /// Used in JS tests, see `internal-for-testing.ts` and patch tests. - pub(crate) fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - // SAFETY: `bun_vm()` never returns null for a Bun-owned global; the VM - // outlives this call frame. - let mut arguments = ArgumentsSlice::init(global.bun_vm(), frame.arguments()); - - let Some(patchfile_src_js) = arguments.next_eat() else { - return Err(global.throw(format_args!( - "TestingAPIs.parse: expected at least 1 argument, got 0" - ))); - }; - let patchfile_src_bunstr = patchfile_src_js.to_bun_string(global)?; - let patchfile_src = patchfile_src_bunstr.to_utf8(); - - let patchfile = match parse_patch_file(patchfile_src.slice()) { + pub(crate) fn parse(heap: &JsHeap<'_>, patchfile_src: &[u8]) -> JsResult> { + let patchfile = match parse_patch_file(patchfile_src) { Ok(p) => p, Err(ParseErrAt { err: e, .. }) => { + // SAFETY: it throws, and the body returns. + let global = unsafe { heap.global() }; if e == ParseErr::hunk_header_integrity_check_failed { return Err(global.throw_error(bun_patch::Error::from(e), "this indicates either that the supplied patch file was incorrect, or there is a bug in Bun. Please check your .patch file, or open a GitHub issue :)")); } else { @@ -97,24 +76,15 @@ impl TestingAPIs { use std::io::Write as _; write!(&mut str, "{}", bun_patch::json_fmt(&patchfile)).expect("unreachable"); } - let js = bun_string_jsc::create_utf8_for_js(global, &str)?; - drop(patchfile); - Ok(js) + Ok(str) } pub(crate) fn parse_apply_args( global: &JSGlobalObject, - frame: &CallFrame, + patchfile_js: JSValue, + dir_js: JSValue, ) -> JsResult { - // SAFETY: `bun_vm()` never returns null for a Bun-owned global; the VM - // outlives this call frame. - let mut arguments = ArgumentsSlice::init(global.bun_vm(), frame.arguments()); - - let Some(patchfile_js) = arguments.next_eat() else { - return Err(global.throw(format_args!("apply: expected at least 1 argument, got 0"))); - }; - - let dir_fd = if let Some(dir_js) = arguments.next_eat() { + let dir_fd = if !dir_js.is_undefined() { let bunstr = dir_js.to_bun_string(global)?; let path = bunstr.to_owned_slice_z(); @@ -166,7 +136,7 @@ impl TestingAPIs { } } -pub struct ApplyArgs { +pub(crate) struct ApplyArgs { patchfile_txt: Vec, dirfd: Fd, } @@ -180,28 +150,3 @@ impl Drop for ApplyArgs { } } } - -// ────────────────────────────────────────────────────────────────────────── -// C-ABI host-fn shims -// -// `#[bun_jsc::host_fn]` (Free kind) emits an unqualified `fn_name(g, f)` call -// in its generated shim body, so it can't wrap an associated fn directly. -// These module-scope thunks forward to `TestingAPIs::*` so the proc-macro can -// generate the JSC-calling-convention `__jsc_host_*` exports the codegen side -// links against. -// ────────────────────────────────────────────────────────────────────────── - -#[bun_jsc::host_fn] -pub fn patch_make_diff(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - TestingAPIs::make_diff(global, frame) -} - -#[bun_jsc::host_fn] -pub fn patch_apply(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - TestingAPIs::apply(global, frame) -} - -#[bun_jsc::host_fn] -pub fn patch_parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - TestingAPIs::parse(global, frame) -} diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs index 5e63425e68af..08cac08e4ce9 100644 --- a/src/resolver/resolver.rs +++ b/src/resolver/resolver.rs @@ -5348,7 +5348,7 @@ impl<'a> Resolver<'a> { r } - // nodeModulePathsForJS / Resolver__propForRequireMainPaths: see src/jsc/resolver_jsc.rs + // node_module_paths / Resolver__propForRequireMainPaths: see src/jsc/resolver_jsc.rs // (exported to C++ only) // NOTE: `dir_info` is a `DirInfoRef` (matching spec `*DirInfo`) so diff --git a/src/runtime/api.rs b/src/runtime/api.rs index 5f21cee85133..766e90139b9b 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -117,7 +117,6 @@ pub(crate) mod bun_x509; pub(crate) mod bun { pub(crate) use super::bun_process as process; - pub(crate) use super::bun_secure_context as secure_context; pub(crate) use super::bun_ssl_context_cache as ssl_context_cache; pub(crate) use super::bun_subprocess as subprocess; pub(crate) use process::Rusage; diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index 10a01493e500..bc4ac342ef99 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -93,6 +93,16 @@ export const indexOfLine = b.fn({ export const inflateSync = zlib("inflate_sync", ZlibDecompressionOptions); +/** `Bun.inspect()` is `""` and `Bun.inspect(undefined)` is `"undefined"`. */ +export const inspect = b.fn({ + export: "Bun__BunObject__inspect", + args: { arguments: b.any.variadic }, + returns: b.string.utf8, + capability: "js", + throws: true, + length: 2, +}); + export const MMapOptions = b.dictionary( { shared: b.bool.loose.default(true), @@ -132,6 +142,18 @@ export const openInEditor = b.fn({ length: 1, }); +const resolver = { + args: { specifier: b.any, from: b.any, esm: b.any }, + returns: b.any, + capability: "js", + throws: true, + length: 1, +} as const; +export const resolve = b.fn(resolver); +export const resolveSync = b.fn(resolver); + +export const serve = b.fn({ args: { options: b.any }, returns: b.any, capability: "js", throws: true }); + export const shellEscape = b.fn({ args: { input: b.string.loose }, returns: b.string, @@ -147,6 +169,15 @@ export const sleepSync = b.fn({ throws: true, }); +/** `Bun.inspect.table` */ +export const table = b.fn({ + export: "Bun__BunObject__table", + args: { tabularData: b.any, properties: b.any, options: b.any }, + returns: b.string.utf8, + capability: "js", + throws: true, +}); + export const WhichOptions = b.dictionary( { PATH: b.string.loose.utf8.optional, diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 3f32c89593d0..ef6d2c672867 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -67,8 +67,8 @@ use std::io::Write as _; use bun_core::Output; use bun_jsc::{ - self as jsc, ArrayBuffer, CallFrame, ConsoleObject, ErrorCode, JSFunction, JSGlobalObject, - JSObject, JSPromise, JSString, JSValue, JsResult, + self as jsc, ArrayBuffer, ConsoleObject, ErrorCode, JSFunction, JSGlobalObject, JSObject, + JSPromise, JSString, JSValue, JsResult, }; // `bun_jsc::VirtualMachine` is the *module* re-export; the struct lives one level deeper. use crate::cli::open::Editor; @@ -80,114 +80,27 @@ use bun_sys::{self as sys, Fd, FdExt as _}; use bun_zlib as zlib; use crate::node; -use crate::test_runner::jest::Jest; use crate::valkey_jsc::js_valkey::{SubscriptionCtx, js as js_RedisClient}; use bun_collections::index_sort; use bun_jsc::EncodedSliceJsc as _; -use bun_jsc::call_frame::ArgumentsSlice; use bun_jsc::{StringJsc as _, bun_string_jsc}; use crate::generated_bindgen::bun_object::{ - BracesOptions, EditorOptions, MMapOptions, WhichOptions, ZlibCompressionOptions, - ZlibDecompressionOptions, ZstdCompressionOptions, + BracesOptions, Bun__BunObject__inspect, Bun__BunObject__table, EditorOptions, MMapOptions, + WhichOptions, ZlibCompressionOptions, ZlibDecompressionOptions, ZstdCompressionOptions, }; use bun_jsc::bindgen::{self, BufferSource, JsHeap, JsLocal, JsScope, PinnedBytes}; -// ─── wrap_static_method adapters ─────────────────────────────────────────── -// `#[bun_jsc::host_fn(static)]` is not yet emitted, -// so hand-roll the arg-extraction shims for the six call sites below. -mod static_adapters { - use super::*; - - pub(super) fn listener_connect(g: &JSGlobalObject, cf: &CallFrame) -> JsResult { - let [opts] = cf.arguments_as_array::<1>(); - crate::socket::Listener::connect(&g.js_thread_of_caller(cf), opts) - } - - pub(super) fn listener_listen(g: &JSGlobalObject, cf: &CallFrame) -> JsResult { - let [opts] = cf.arguments_as_array::<1>(); - crate::socket::Listener::listen(&g.js_thread_of_caller(cf), opts) - } - - pub(super) fn udp_socket(g: &JSGlobalObject, cf: &CallFrame) -> JsResult { - let [opts] = cf.arguments_as_array::<1>(); - crate::socket::udp_socket::UDPSocket::udp_socket(&g.js_thread_of_caller(cf), opts) - } - - pub(super) fn subprocess_spawn(g: &JSGlobalObject, cf: &CallFrame) -> JsResult { - let [a0] = cf.arguments_as_array::<1>(); - let a1 = cf.arguments().get(1).copied(); - crate::api::js_bun_spawn_bindings::spawn(&g.js_thread_of_caller(cf), a0, a1) - } - - pub(super) fn subprocess_spawn_sync(g: &JSGlobalObject, cf: &CallFrame) -> JsResult { - let [a0] = cf.arguments_as_array::<1>(); - let a1 = cf.arguments().get(1).copied(); - crate::api::js_bun_spawn_bindings::spawn_sync(&g.js_thread_of_caller(cf), a0, a1) - } - - pub(super) fn js_bundler_build(g: &JSGlobalObject, cf: &CallFrame) -> JsResult { - crate::api::js_bundler::JSBundler::build_fn(g, cf) - } - /// `Bun.$` parsed-script constructor — wraps the marked-argument-buffer host fn. - pub(super) fn parsed_shell_script_create( - g: &JSGlobalObject, - cf: &CallFrame, - ) -> JsResult { - // `CREATE_PARSED_SHELL_SCRIPT` is the safe `JSHostFnZig` produced by - // `marked_argument_buffer_wrap!` (the C-ABI shim is exported separately - // by the macro); call it directly. - crate::shell::parsed_shell_script::CREATE_PARSED_SHELL_SCRIPT(g, cf) - } -} - -/// How to add a new function or property to the Bun global +/// How to add a new property to the Bun global (a function goes in `BunObject.bind.ts`) /// -/// - Add a callback or property to the below struct -/// - @export it in the appropriate place +/// - Add a property to `export_lazy_prop_callbacks!` below /// - Update "@begin bunObjectTable" in BunObject.cpp -/// - Getters use a generated wrapper function `BunObject_getter_wrap_` +/// - Getters use a generated wrapper function `BunObject_lazyPropCb_wrap_` /// - Update "BunObject+exports.h" /// - Run `bun run build` pub(crate) mod bun_object { use super::*; - // Each callback is exported under - // `BunObject_callback_` / `BunObject_lazyPropCb_`. The - // two `macro_rules!` below expand the static export tables. - // ABI check vs the C++ declarations (BunObject+exports.h:90): - // `extern "C" EncodedJSValue SYSV_ABI (JSGlobalObject*, JSObject*)` for the - // property variants — matched here by `jsc_host_abi!` (`extern "sysv64"` - // on Windows-x64, `extern "C"` elsewhere) returning `JSValue`, which is - // `#[repr(transparent)]` over `EncodedJSValue`. - - // Ident concat via `${concat()}` is unstable (`macro_metavar_expr_concat`), - // so the full `BunObject_callback_` / `BunObject_lazyPropCb_` - // export symbol is supplied verbatim by the caller (same pattern as - // `lazy_prop!` above). - macro_rules! export_callbacks { - ($( $(#[$attr:meta])* $sym:ident => $target:expr ),* $(,)?) => { - $( - // C++ declares - // these via `BUN_DECLARE_HOST_FUNCTION` → `JSC_HOST_CALL_ATTRIBUTES` - // = SysV on Windows-x64. Mismatching `extern "C"` here puts - // `globalObject` in RCX vs C++'s RDI → garbage deref. - bun_jsc::jsc_host_abi! { - $(#[$attr])* - #[unsafe(no_mangle)] - pub unsafe fn $sym( - g: *mut JSGlobalObject, - f: *mut CallFrame, - ) -> JSValue { - // SAFETY: JSC always passes valid pointers here. - let (g, f) = unsafe { (&*g, &*f) }; - bun_jsc::to_js_host_call(g, || $target(g, f)) - } - } - )* - }; - } - /// Adapter so `export_lazy_prop_callbacks!` accepts targets returning either /// a bare `JSValue` (most getters) or a `JsResult` (e.g. /// `get_embedded_files`, which can OOM allocating the result array). @@ -211,7 +124,7 @@ pub(crate) mod bun_object { ($( $sym:ident => $target:path ),* $(,)?) => { $( // C++ declares the extern as `SYSV_ABI` - // (`BunObject+exports.h:91`); on Windows-x64 that's RDI/RSI, + // (`BunObject+exports.h`); on Windows-x64 that's RDI/RSI, // not RCX/RDX, so `extern "C"` reads garbage for both args. bun_jsc::jsc_host_abi! { #[unsafe(no_mangle)] @@ -230,32 +143,9 @@ pub(crate) mod bun_object { }; } - // --- Callbacks --- - export_callbacks! { - BunObject_callback_build => super::static_adapters::js_bundler_build, - BunObject_callback_color => bun_css_jsc::js_function_color, - BunObject_callback_connect => super::static_adapters::listener_connect, - BunObject_callback_file => crate::webcore::blob::construct_bun_file, - BunObject_callback_jest => Jest::call, - BunObject_callback_listen => super::static_adapters::listener_listen, - BunObject_callback_resolve => super::resolve, - BunObject_callback_resolveSync => super::resolve_sync, - BunObject_callback_serve => super::serve, - BunObject_callback_spawn => super::static_adapters::subprocess_spawn, - BunObject_callback_spawnSync => super::static_adapters::subprocess_spawn_sync, - BunObject_callback_udpSocket => super::static_adapters::udp_socket, - BunObject_callback_write => crate::webcore::blob::write_file, - } - // Its body is already `JSHostFnZig`-shaped, so it goes through the same `to_js_host_call` thunk as the callbacks above. - export_callbacks! { - BunObject_callback_createParsedShellScript => super::static_adapters::parsed_shell_script_create, - } - // --- Callbacks --- - // --- Lazy property callbacks --- export_lazy_prop_callbacks! { BunObject_lazyPropCb_FFI => crate::ffi::ffi_object_draft::getter, - BunObject_lazyPropCb_markdown => super::get_markdown_object, BunObject_lazyPropCb_argv => super::get_argv, BunObject_lazyPropCb_cron => super::get_cron_object, BunObject_lazyPropCb_cwd => super::get_cwd, @@ -279,7 +169,7 @@ pub(crate) mod bun_object { // are spelled out verbatim in the `export_*!` macro invocations above. // type LazyPropertyCallback = extern "C" fn(*mut JSGlobalObject, *mut JSObject) -> JSValue - // (the `callconv(jsc.conv)` ABI is emitted by `#[bun_jsc::host_fn]` / the macro above; + // (the `callconv(jsc.conv)` ABI is emitted by the macro above; // see PORTING.md §FFI — cannot write `extern jsc_conv!()` in Rust.) // --- LazyProperty initializers --- @@ -445,18 +335,23 @@ pub(crate) fn which<'a>( .transpose() } -#[bun_jsc::host_fn] -fn inspect_table(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let mut args_buf = callframe.arguments_undef::<5>(); - let all_arguments = args_buf.mut_(); +/// `Bun.inspect.table` +pub(crate) fn table( + cx: &mut JsScope<'_>, + tabular_data: JsLocal<'_>, + properties: JsLocal<'_>, + options: JsLocal<'_>, +) -> JsResult> { + let global_this = cx.global(); + let mut all_arguments = [tabular_data.to_js(), properties.to_js(), options.to_js()]; if all_arguments[0].is_undefined_or_null() || !all_arguments[0].is_object() { - return Ok(JSValue::js_empty_string(global_this)); + return Ok(Vec::new()); } - // NOTE: protect/unprotect over a copied [JSValue; 5]; the borrow of + // NOTE: protect/unprotect over a copied [JSValue; 3]; the borrow of // `all_arguments` cannot escape into a guard closure, so copy out into an // array of RAII guards. - let _prot: [bun_jsc::ProtectedJSValue; 5] = + let _prot: [bun_jsc::ProtectedJSValue; 3] = core::array::from_fn(|i| all_arguments[i].protected()); let arguments = &mut all_arguments[..]; @@ -511,14 +406,14 @@ fn inspect_table(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResul // writer.flush(): Vec writer is unbuffered; nothing to flush. - bun_string_jsc::create_utf8_for_js(global_this, &array) + Ok(array) } -#[bun_jsc::host_fn] -fn inspect(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let arguments = callframe.arguments(); +pub(crate) fn inspect(cx: &mut JsScope<'_>, arguments: &[JsLocal<'_>]) -> JsResult> { + let global_this = cx.global(); + let arguments = JsLocal::to_js_slice(arguments); if arguments.is_empty() { - return Ok(JSValue::js_empty_string(global_this)); + return Ok(Vec::new()); } for arg in arguments { @@ -571,7 +466,7 @@ fn inspect(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult is unbuffered. - bun_string_jsc::create_utf8_for_js(global_this, &array) + Ok(array) } // HOST_EXPORT(Bun__inspect_singleline, c) @@ -607,7 +502,7 @@ fn get_inspect(global_object: &JSGlobalObject, _: &JSObject) -> JSValue { let fun = JSFunction::create( global_object, "inspect", - __jsc_host_inspect, + Bun__BunObject__inspect, 2, Default::default(), ); @@ -622,7 +517,7 @@ fn get_inspect(global_object: &JSGlobalObject, _: &JSObject) -> JSValue { JSFunction::create( global_object, "table", - __jsc_host_inspect_table, + Bun__BunObject__table, 3, Default::default(), ), @@ -903,31 +798,24 @@ pub(crate) fn shrink(cx: &mut JsScope<'_>) { cx.global().vm().shrink_footprint(); } -fn do_resolve(global_this: &JSGlobalObject, arguments: &[JSValue]) -> JsResult { - // SAFETY: bun_vm() returns the live per-thread singleton. - let vm = global_this.bun_vm(); - let mut args = ArgumentsSlice::init(vm, arguments); - let Some(specifier) = args.next_eat() else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a specifier and a from path"))); - }; - +fn do_resolve( + global_this: &JSGlobalObject, + specifier: JSValue, + from: JSValue, + esm: JSValue, +) -> JsResult { if specifier.is_undefined_or_null() { return Err(global_this.throw_invalid_arguments(format_args!("specifier must be a string"))); } - let Some(from) = args.next_eat() else { - return Err(global_this.throw_invalid_arguments(format_args!("Expected a from path"))); - }; - if from.is_undefined_or_null() { return Err(global_this.throw_invalid_arguments(format_args!("from must be a string"))); } let mut mode = ResolveMode::Esm; - if let Some(next) = args.next_eat() { - if next.is_boolean() { - mode = if next.to_boolean() { + if !esm.is_undefined() { + if esm.is_boolean() { + mode = if esm.to_boolean() { ResolveMode::Esm } else { ResolveMode::Require @@ -1004,14 +892,23 @@ fn resolve_with_args( Ok(Resolved::Found(result_value.into_js(ctx)?)) } -#[bun_jsc::host_fn] -fn resolve_sync(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - do_resolve(global_object, callframe.arguments()) +pub(crate) fn resolve_sync( + cx: &mut JsScope<'_>, + specifier: JsLocal<'_>, + from: JsLocal<'_>, + esm: JsLocal<'_>, +) -> JsResult { + do_resolve(cx.global(), specifier.to_js(), from.to_js(), esm.to_js()) } -#[bun_jsc::host_fn] -fn resolve(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let value = match do_resolve(global_object, callframe.arguments()) { +pub(crate) fn resolve( + cx: &mut JsScope<'_>, + specifier: JsLocal<'_>, + from: JsLocal<'_>, + esm: JsLocal<'_>, +) -> JsResult { + let global_object = cx.global(); + let value = match do_resolve(global_object, specifier.to_js(), from.to_js(), esm.to_js()) { Ok(v) => v, Err(e) => { return Ok( @@ -1187,13 +1084,13 @@ pub(crate) fn index_of_line(buffer: &[u8], offset: f64) -> f64 { } } -#[bun_jsc::host_fn] -fn serve(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn serve(cx: &mut JsScope<'_>, options: JsLocal<'_>) -> JsResult { + let global_object = cx.global(); // The server is the calling script's. - let context = global_object.bun_vm().context_of_caller(callframe); + let context = global_object.bun_vm().context_of_caller_no_frame(); let mut config = crate::server::ServerConfig::from_js( global_object, - callframe.argument(0), + options.to_js(), crate::server::server_config::FromJSOptions { allow_bake_config: bun_core::FeatureFlags::bake(), is_fetch_required: true, @@ -1480,10 +1377,6 @@ pub(crate) fn mmap(cx: &mut JsScope<'_>, path: &[u8], options: &MMapOptions) -> } } -fn get_markdown_object(global_this: &JSGlobalObject, _: &JSObject) -> JSValue { - crate::api::markdown_object::create(global_this) -} - fn get_s3_default_client(global_this: &JSGlobalObject, _: &JSObject) -> JsResult { // NOTE (layering): `RareData::s3_default_client` body lives in // `bun_jsc::rare_data::_accessor_body` and names `bun_runtime::s3` types. diff --git a/src/runtime/api/JSBundler.bind.ts b/src/runtime/api/JSBundler.bind.ts index ae1153b17723..5193a6d8ea07 100644 --- a/src/runtime/api/JSBundler.bind.ts +++ b/src/runtime/api/JSBundler.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::api::js_bundler" }); + const js = { capability: "js", throws: true } as const; const method = { returns: b.any, ...js } as const; const cached = { getter: b.any, cache: true, ...js } as const; @@ -23,3 +25,11 @@ export const BuildArtifact = b.class({ type: { getter: b.any, cache: true, capability: "js" }, }, }); + +export const build = b.fn({ + ...method, + args: { config: b.any }, + rustPath: "crate::api::js_bundler::js_bundler::build", +}); + +export const workerLiveCount = b.fn({ returns: b.usize }); diff --git a/src/runtime/api/JSBundler.rs b/src/runtime/api/JSBundler.rs index d680408323b7..7fc1148e4ac0 100644 --- a/src/runtime/api/JSBundler.rs +++ b/src/runtime/api/JSBundler.rs @@ -16,7 +16,7 @@ use bun_core::String as BunString; use bun_jsc::ConcurrentTask::ConcurrentTask; use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope, JsWrapper}; use bun_jsc::bun_string_jsc; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, StringJsc as _}; +use bun_jsc::{self as jsc, JSGlobalObject, JSValue, JsError, JsResult, StringJsc as _}; use bun_options_types::compile_target::CompileTarget; use bun_options_types::schema::api; // bun.schema.api use bun_standalone_graph::StandaloneModuleGraph; @@ -1467,18 +1467,18 @@ pub(crate) mod js_bundler { pub(crate) keep_names: bool, } - fn build( - global_this: &JSGlobalObject, - context: jsc::ContextId, - arguments: &[JSValue], - ) -> JsResult { - if arguments.is_empty() || !arguments[0].is_object() { + /// `Bun.build(config)` + pub(crate) fn build(cx: &mut JsScope<'_>, config: JsLocal<'_>) -> JsResult { + let global_this = cx.global(); + let config = config.to_js(); + if !config.is_object() { return Err(global_this.throw_invalid_arguments(format_args!( "Expected a config object to be passed to Bun.build" ))); } let vm = global_this.bun_vm(); + let context = vm.context_of_caller_no_frame().id(); // Detect and prevent calling Bun.build from within a macro during bundling. // This would cause a deadlock because: @@ -1495,7 +1495,7 @@ pub(crate) mod js_bundler { } let mut plugins: Option<*mut Plugin> = None; - let config = Config::from_js(global_this, arguments[0], &mut plugins)?; + let config = Config::from_js(global_this, config, &mut plugins)?; // `BundleV2.generateFromJavaScript` — the completion-task struct lives in // `crate::api::js_bundle_completion_task` (bun_runtime owns it because its @@ -1513,19 +1513,6 @@ pub(crate) mod js_bundler { Ok(promise) } - /// `Bun.build(config)` - #[bun_jsc::host_fn] - pub(crate) fn build_fn( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - build( - global_this, - global_this.bun_vm().context_of_caller(callframe).id(), - callframe.arguments(), - ) - } - // NOTE: `Resolve`/`Load`/`MiniImportRecord`/etc. are owned by // `bun_bundler::bundle_v2::api::JSBundler` so that `BundleV2` can operate // on them directly (`on_resolve_async`/`on_load_async`). `dispatch()` and @@ -2025,15 +2012,9 @@ pub(crate) mod js_bundler { pub(crate) use js_bundler as JSBundler; /// `bun:internal-for-testing`: bundler `Worker`s (one per pool thread a build ran on) not yet torn down. -#[bun_jsc::host_fn] -pub(crate) fn js_worker_live_count( - _global: &JSGlobalObject, - _callframe: &CallFrame, -) -> JsResult { +pub(crate) fn worker_live_count() -> usize { use core::sync::atomic::Ordering; - Ok(JSValue::js_number( - bun_bundler::thread_pool::WORKER_LIVE_COUNT.load(Ordering::SeqCst) as f64, - )) + bun_bundler::thread_pool::WORKER_LIVE_COUNT.load(Ordering::SeqCst) } pub(crate) use js_bundler::BunPluginTarget; diff --git a/src/runtime/api/JSSourceMap.rs b/src/runtime/api/JSSourceMap.rs index f645bd996d43..12dbb1a4e821 100644 --- a/src/runtime/api/JSSourceMap.rs +++ b/src/runtime/api/JSSourceMap.rs @@ -7,9 +7,7 @@ use bstr::BStr; use bun_core::{self as bstring, strings}; use bun_jsc::bindgen::{JsLocal, JsScope, JsStructure, JsWrapper}; -use bun_jsc::{ - CallFrame, JSGlobalObject, JSValue, JsClass as _, JsResult, StringJsc as _, bun_string_jsc, -}; +use bun_jsc::{JSGlobalObject, JSValue, JsClass as _, JsResult, StringJsc as _, bun_string_jsc}; use bun_sourcemap::{Mapping, Ordinal, ParsedSourceMap, mapping}; use crate::generated_bindgen::sourcemap::js_SourceMap as js; @@ -24,8 +22,7 @@ pub(crate) struct JSSourceMap { // Mutable global; AtomicBool for safe mutation. static ENABLE_SOURCE_MAPS: AtomicBool = AtomicBool::new(false); -#[bun_jsc::host_fn(export = "Bun__JSSourceMap__find")] -fn find_source_map(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn find_source_map(cx: &mut JsScope<'_>, path: JsLocal<'_>) -> JsResult { // Node.js doesn't enable source maps by default. // In Bun, we do use them for almost all files since we transpile almost all files // If we enable this by default, we don't have a `payload` object since we don't internally create one. @@ -35,7 +32,8 @@ fn find_source_map(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult JSValue { - bun_jsc::create_host_function_object( - global_this, - &[ - ("html", __jsc_host_render_to_html, 1), - ("ansi", __jsc_host_render_to_ansi, 2), - ("render", __jsc_host_render, 3), - ("react", __jsc_host_render_react, 3), - ], - ) -} - /// `bun:internal-for-testing`'s `setMaxMarkdownBlockBytesForTesting(limit)`: /// shrink the parser's block-metadata cap so its `TooManyBlocks` error is /// testable without 4 GiB of input. Returns the previous limit. -#[bun_jsc::host_fn] -pub(crate) fn set_max_markdown_block_bytes_for_testing( - global_this: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let [limit_value] = callframe.arguments_as_array::<1>(); - if !limit_value.is_number() { - return Err(global_this.throw_invalid_arguments(format_args!( - "setMaxMarkdownBlockBytesForTesting expects a number" - ))); - } - let limit = usize::try_from(limit_value.coerce_to_int64(global_this)?.max(0)) - .expect("non-negative i64 fits usize"); - let prev = bun_md::parser::set_max_block_bytes_for_testing(limit); - Ok(JSValue::js_number(prev as f64)) +pub(crate) fn set_max_markdown_block_bytes_for_testing(limit: f64) -> usize { + bun_md::parser::set_max_block_bytes_for_testing(limit as usize) } /// `Bun.markdown.ansi(text, theme?)` — render markdown to an ANSI-colored /// terminal string. `theme` is an optional object: `{ colors?, hyperlinks?, /// light?, columns? }`. By default colors are enabled, hyperlinks are /// disabled (the caller doesn't know if stdout is a TTY), and columns is 80. -#[bun_jsc::host_fn] -pub(crate) fn render_to_ansi( - global_this: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let [input_value, theme_value] = callframe.arguments_as_array::<2>(); +pub(crate) fn ansi( + cx: &mut JsScope<'_>, + input: JsLocal<'_>, + theme: JsLocal<'_>, +) -> JsResult> { + let global_this = cx.global(); + let [input_value, theme_value] = [input.to_js(), theme.to_js()]; if input_value.is_empty_or_undefined_or_null() { return Err(global_this @@ -168,23 +145,25 @@ pub(crate) fn render_to_ansi( } } - let result = match md::render_to_ansi(input, md::Options::TERMINAL, theme) { - Ok(Some(r)) => r, + match md::render_to_ansi(input, md::Options::TERMINAL, theme) { + Ok(Some(r)) => Ok(r), Ok(None) => { // The parser can only return null via JSError // from a renderer callback; the ANSI renderer has none, so this // path is unreachable but handle it safely. - return Err(global_this.throw_out_of_memory()); + Err(global_this.throw_out_of_memory()) } - Err(err) => return Err(parser_err_to_js(global_this, err, input.len())), - }; - - bun_string_jsc::create_utf8_for_js(global_this, &result) + Err(err) => Err(parser_err_to_js(global_this, err, input.len())), + } } -#[bun_jsc::host_fn] -fn render_to_html(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let [input_value, opts_value] = callframe.arguments_as_array::<2>(); +pub(crate) fn html( + cx: &mut JsScope<'_>, + input: JsLocal<'_>, + options: JsLocal<'_>, +) -> JsResult> { + let global_this = cx.global(); + let [input_value, opts_value] = [input.to_js(), options.to_js()]; if input_value.is_empty_or_undefined_or_null() { return Err(global_this @@ -204,12 +183,8 @@ fn render_to_html(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResu let options = parse_options(global_this, opts_value)?; - let result = match md::render_to_html_with_options(input, options) { - Ok(r) => r, - Err(err) => return Err(parser_err_to_js(global_this, err, input.len())), - }; - - bun_string_jsc::create_utf8_for_js(global_this, &result) + md::render_to_html_with_options(input, options) + .map_err(|err| parser_err_to_js(global_this, err, input.len())) } fn parse_options(global_this: &JSGlobalObject, opts_value: JSValue) -> JsResult { @@ -285,9 +260,15 @@ fn parse_options(global_this: &JSGlobalObject, opts_value: JSValue) -> JsResult< /// Each callback receives the accumulated children as a string plus an optional /// metadata object, and returns a string. The final result is the concatenation /// of all callback outputs. -#[bun_jsc::host_fn] -fn render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let [input_value, callbacks_value, opts_value] = callframe.arguments_as_array::<3>(); +pub(crate) fn render( + cx: &mut JsScope<'_>, + input: JsLocal<'_>, + callbacks: JsLocal<'_>, + options: JsLocal<'_>, +) -> JsResult { + let global_this = cx.global(); + let [input_value, callbacks_value, opts_value] = + [input.to_js(), callbacks.to_js(), options.to_js()]; if input_value.is_empty_or_undefined_or_null() { return Err(global_this @@ -335,9 +316,20 @@ fn render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult JsResult { - MarkedArgumentBuffer::new(|marked_args| render_react_impl(global_this, callframe, marked_args)) +pub(crate) fn react( + cx: &mut JsScope<'_>, + input: JsLocal<'_>, + components: JsLocal<'_>, + options: JsLocal<'_>, +) -> JsResult { + let global_this = cx.global(); + MarkedArgumentBuffer::new(|marked_args| { + render_react_impl( + global_this, + [input.to_js(), components.to_js(), options.to_js()], + marked_args, + ) + }) } unsafe extern "C" { @@ -350,10 +342,9 @@ unsafe extern "C" { fn render_react_impl( global_this: &JSGlobalObject, - callframe: &CallFrame, + args: [JSValue; 3], marked_args: &mut MarkedArgumentBuffer, ) -> JsResult { - let args = callframe.arguments_as_array::<3>(); let opts_value = args[2]; // options are the 3rd argument let mut react_version: u8 = 1; // default: react.transitional.element (React 19+) @@ -368,7 +359,7 @@ fn render_react_impl( } } - let children = render_ast(global_this, callframe, marked_args, Some(react_version))?; + let children = render_ast(global_this, args, marked_args, Some(react_version))?; let fragment = JSReactElement__createFragment(global_this, react_version, children); marked_args.append(fragment); Ok(fragment) @@ -376,12 +367,10 @@ fn render_react_impl( fn render_ast( global_this: &JSGlobalObject, - callframe: &CallFrame, + [input_value, components_value, opts_value]: [JSValue; 3], marked_args: &mut MarkedArgumentBuffer, react_version: Option, ) -> JsResult { - let [input_value, components_value, opts_value] = callframe.arguments_as_array::<3>(); - if input_value.is_empty_or_undefined_or_null() { return Err(global_this .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); diff --git a/src/runtime/api/MarkdownObjectInternals.bind.ts b/src/runtime/api/MarkdownObjectInternals.bind.ts new file mode 100644 index 000000000000..6ede44dcd3f4 --- /dev/null +++ b/src/runtime/api/MarkdownObjectInternals.bind.ts @@ -0,0 +1,5 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::api::markdown_object" }); + +export const setMaxMarkdownBlockBytesForTesting = b.fn({ args: { limit: b.f64 }, returns: b.usize }); diff --git a/src/runtime/api/bun/SecureContext.bind.ts b/src/runtime/api/bun/SecureContext.bind.ts index 2a5bb1306fee..0eb65e2fd116 100644 --- a/src/runtime/api/bun/SecureContext.bind.ts +++ b/src/runtime/api/bun/SecureContext.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::api::bun_secure_context" }); + const fromOptions = { args: { options: b.any }, capability: "js", throws: true } as const; /** What `node:tls` hands out as `secureContext.context`. */ @@ -21,3 +23,5 @@ export const SecureContext = b.class({ parsePkcs12: { args: { pfx: b.any, passphrase: b.any }, returns: b.any, capability: "js", throws: true }, }, }); + +export const liveCount = b.fn({ returns: b.f64 }); diff --git a/src/runtime/api/bun/SecureContext.rs b/src/runtime/api/bun/SecureContext.rs index cd97e5fabdbd..a3bff4c5d94f 100644 --- a/src/runtime/api/bun/SecureContext.rs +++ b/src/runtime/api/bun/SecureContext.rs @@ -21,7 +21,7 @@ use bun_core::EncodedSlice; use bun_jsc::EncodedSliceJsc as _; use bun_jsc::JsClass as _; use bun_jsc::bindgen::{JsHeap, JsLocal, JsPayload as _, JsScope, JsWrapper}; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult}; use bun_uws as uws; use core::cell::Cell; @@ -41,11 +41,10 @@ pub(crate) struct SecureContext { /// Exposed via `bun:internal-for-testing` so churn tests can assert /// `SSL_CTX_new` was called O(1) times, not O(connections). -#[bun_jsc::host_fn] -pub(crate) fn js_live_count(_global: &JSGlobalObject, _callframe: &CallFrame) -> JsResult { +pub(crate) fn live_count() -> f64 { // `us_ssl_ctx_live_count` is declared `safe fn` (reads a global atomic // counter, no preconditions). - Ok(JSValue::js_number(c::us_ssl_ctx_live_count() as f64)) + c::us_ssl_ctx_live_count() as f64 } impl SecureContext { diff --git a/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts b/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts new file mode 100644 index 000000000000..06e62d4c4116 --- /dev/null +++ b/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts @@ -0,0 +1,14 @@ +import * as b from "../../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::api::js_bun_spawn_bindings" }); + +/** `(cmd, options?)` or `({ cmd, ..options })`. */ +const spawner = { + args: { cmdOrOptions: b.any, options: b.any }, + returns: b.any, + capability: "js", + throws: true, + length: 1, +} as const; +export const spawn = b.fn(spawner); +export const spawnSync = b.fn(spawner); diff --git a/src/runtime/api/bun/js_bun_spawn_bindings.rs b/src/runtime/api/bun/js_bun_spawn_bindings.rs index 6c046a67b62a..87508dca2880 100644 --- a/src/runtime/api/bun/js_bun_spawn_bindings.rs +++ b/src/runtime/api/bun/js_bun_spawn_bindings.rs @@ -10,6 +10,7 @@ use bun_core::{Output, Timespec, TimespecMockMode, ZBox, fmt as bun_fmt}; use bun_core::{String as BunString, ZStr, strings}; use bun_event_loop::SpawnSyncEventLoop::TickState; use bun_io::max_buf::MaxBuf; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::{ self as jsc, EventLoopHandle, JSGlobalObject, JSObject, JSPropertyIterator, JSValue, JsError, JsResult, SystemError, @@ -264,21 +265,34 @@ fn get_argv( /// Bun.spawn() calls this. pub(crate) fn spawn( - cx: &bun_jsc::JsThread<'_>, - args: JSValue, - secondary_args_value: Option, + cx: &mut JsScope<'_>, + cmd_or_options: JsLocal<'_>, + options: JsLocal<'_>, ) -> JsResult { - spawn_maybe_sync(false, cx, args, secondary_args_value, &mut None) + spawn_maybe_sync( + false, + &cx.js_thread(), + cmd_or_options.to_js(), + options.to_js(), + &mut None, + ) } /// Bun.spawnSync() calls this. pub(crate) fn spawn_sync( - cx: &bun_jsc::JsThread<'_>, - args: JSValue, - secondary_args_value: Option, + cx: &mut JsScope<'_>, + cmd_or_options: JsLocal<'_>, + options: JsLocal<'_>, ) -> JsResult { + let cx = &cx.js_thread(); let mut bun_test_deadline: Option = None; - let result = spawn_maybe_sync(true, cx, args, secondary_args_value, &mut bun_test_deadline); + let result = spawn_maybe_sync( + true, + cx, + cmd_or_options.to_js(), + options.to_js(), + &mut bun_test_deadline, + ); // A bun:test deadline that passed while the isolated loop was blocking is reported only now: the loop is torn down and the child reaped, so the runner's callback re-enters nothing that is mid-flight. With an exception pending (spawn failure, termination) the file timer is left armed and reports it from the main loop instead. if let Some(deadline) = bun_test_deadline && result.is_ok() @@ -337,7 +351,7 @@ fn spawn_maybe_sync( is_sync: bool, cx: &bun_jsc::JsThread<'_>, args_: JSValue, - secondary_args_value: Option, + secondary_args_value: JSValue, bun_test_deadline: &mut Option, ) -> JsResult { if is_sync { @@ -419,7 +433,7 @@ fn spawn_maybe_sync( let args_type = args.js_type(); if args_type.is_array() { cmd_value = args; - args = secondary_args_value.unwrap_or_default(); + args = secondary_args_value; } else if !args.is_object() { return Err(cx .global() diff --git a/src/runtime/api/bun/subprocess.bind.ts b/src/runtime/api/bun/subprocess.bind.ts index 6406aa825109..492968b161bb 100644 --- a/src/runtime/api/bun/subprocess.bind.ts +++ b/src/runtime/api/bun/subprocess.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::api::bun::subprocess::testing_apis" }); + /** A stream, a file descriptor, `null` or `undefined`, made the first time it is read. */ const stdio = { getter: b.any, capability: "js" } as const; @@ -39,3 +41,11 @@ export const Subprocess = b.class({ "@@asyncDispose": { returns: b.any, capability: "js", throws: true, passThis: true }, }, }); + +export const injectStdioReadError = b.fn({ + args: { subprocess: Subprocess, kind: b.string.loose }, + returns: b.bool, + capability: "js", + throws: true, +}); +export const closeStdinWriter = b.fn({ args: { subprocess: Subprocess }, returns: b.bool, capability: "js" }); diff --git a/src/runtime/api/bun/subprocess.rs b/src/runtime/api/bun/subprocess.rs index e168a3149e9c..8d3228d15497 100644 --- a/src/runtime/api/bun/subprocess.rs +++ b/src/runtime/api/bun/subprocess.rs @@ -8,8 +8,7 @@ use bun_ptr::{RefCount, RefPtr}; use bun_jsc::bindgen::{JsLocal, JsScope, JsWrapper}; use bun_jsc::{ - self as jsc, CallFrame, JSGlobalObject, JSPromise, JSValue, JsCell, JsRef, JsResult, - VirtualMachine, + self as jsc, JSGlobalObject, JSPromise, JSValue, JsCell, JsRef, JsResult, VirtualMachine, }; use bun_jsc::{JsClass, SysErrorJsc}; #[cfg(not(windows))] @@ -1492,32 +1491,23 @@ pub(crate) mod testing_apis { /// /// Returns true if an error was injected, false if the given stdio is /// not (or no longer) a buffered pipe reader. - #[bun_jsc::host_fn] pub(crate) fn inject_stdio_read_error( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let [subprocess_value, kind_value] = callframe.arguments_as_array::<2>(); - let Some(subprocess_ptr) = Subprocess::from_js(subprocess_value) else { - return Err(global_this.throw(format_args!("first argument must be a Subprocess"))); - }; - // SAFETY: `from_js` returned a live `*mut Subprocess` owned by the JS wrapper. - // R-2: deref as shared (`&*const`) — fields are interior-mutable. - let subprocess = unsafe { &*subprocess_ptr }; - let kind_str = kind_value.to_bun_string(global_this)?; - - let out: &JsCell = if kind_str.eq_ascii(b"stdout") { + cx: &mut JsScope<'_>, + subprocess: &Subprocess<'static>, + kind: &bun_core::String, + ) -> JsResult { + let out: &JsCell = if kind.eq_ascii(b"stdout") { &subprocess.stdout - } else if kind_str.eq_ascii(b"stderr") { + } else if kind.eq_ascii(b"stderr") { &subprocess.stderr } else { - return Err( - global_this.throw(format_args!("second argument must be 'stdout' or 'stderr'")) - ); + return Err(cx + .global() + .throw(format_args!("second argument must be 'stdout' or 'stderr'"))); }; let Readable::Pipe(pipe) = out.get() else { - return Ok(JSValue::FALSE); + return Ok(false); }; // Mirror what the real error path does (onStreamRead on Windows, @@ -1531,34 +1521,24 @@ pub(crate) mod testing_apis { // SAFETY: live pipe reader; `on_error` is the raw entry so the // (maybe-freeing) error dispatch runs under no receiver protector. unsafe { bun_io::BufferedReader::on_error(reader, fake_err) }; - Ok(JSValue::TRUE) + Ok(true) } /// Close the stdin pipe's writer as a Windows worker's stop phase does: no JS wrapper, no ref. - #[bun_jsc::host_fn] pub(crate) fn close_stdin_writer( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { + _: &mut JsScope<'_>, + subprocess: &Subprocess<'static>, + ) -> bool { #[cfg(windows)] use bun_io::pipe_writer::BaseWindowsPipeWriter as _; - let [subprocess_value] = callframe.arguments_as_array::<1>(); - let Some(subprocess_ptr) = Subprocess::from_js(subprocess_value) else { - return Err(global_this.throw(format_args!("first argument must be a Subprocess"))); - }; - // SAFETY: `from_js` returned a live `*mut Subprocess` owned by the JS wrapper. - let subprocess = unsafe { &*subprocess_ptr }; let Writable::Pipe(pipe) = subprocess.stdin.get() else { - return Ok(JSValue::FALSE); + return false; }; let writer = pipe.writer.as_ptr(); // SAFETY: the Subprocess's ref keeps the sink and its writer live on entry; // `close()` may free both, and nothing touches them afterwards. unsafe { (*writer).close() }; - Ok(JSValue::TRUE) + true } } -// `generated_js2native.rs` snake-cases `TestingAPIs` as `testing_ap_is` -// (the converter splits the trailing `…APIs` cluster into `AP` + `Is`). -pub(crate) use testing_apis as testing_ap_is; diff --git a/src/runtime/api/crash_handler_jsc.bind.ts b/src/runtime/api/crash_handler_jsc.bind.ts new file mode 100644 index 000000000000..19b9af3fbaad --- /dev/null +++ b/src/runtime/api/crash_handler_jsc.bind.ts @@ -0,0 +1,18 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::api::crash_handler_jsc::js_bindings" }); + +export const crashHandler = b.namespace({ + getMachOImageZeroOffset: { returns: b.f64.optional, length: 1 }, + getFeatureData: { returns: b.any, capability: "js", throws: true, length: 1 }, + segfault: { length: 1 }, + segfaultInDll: { length: 1 }, + panic: { length: 1 }, + rootError: { length: 1 }, + outOfMemory: { length: 1 }, + operatorNew: { args: { nothrow: b.bool.loose.default(false) }, returns: b.bool.optional, length: 1 }, + abort: { length: 1 }, + fastfail: { length: 1 }, + trap: { length: 1 }, + raiseIgnoringPanicHandler: { length: 1 }, +}); diff --git a/src/runtime/api/crash_handler_jsc.rs b/src/runtime/api/crash_handler_jsc.rs index 8e6fb775723c..165631cd30b9 100644 --- a/src/runtime/api/crash_handler_jsc.rs +++ b/src/runtime/api/crash_handler_jsc.rs @@ -5,53 +5,16 @@ use bun_analytics as analytics; use bun_core::String as BunString; use bun_core::{Environment, Global}; use bun_crash_handler as crash_handler; -use bun_jsc::{CallFrame, JSFunction, JSGlobalObject, JSValue, JsResult, StringJsc}; +use bun_jsc::bindgen::JsScope; +use bun_jsc::{JSValue, JsResult, StringJsc}; pub(crate) mod js_bindings { use super::*; - pub(crate) fn generate(global: &JSGlobalObject) -> JSValue { - // `#[bun_jsc::host_fn]` emits an `extern "C"` shim named `__jsc_host_`; that - // shim is the `JSHostFn` value passed to `JSFunction::create`. - const ENTRIES: &[(&str, bun_jsc::JSHostFn)] = &[ - ( - "getMachOImageZeroOffset", - __jsc_host_js_get_mach_o_image_zero_offset, - ), - ("getFeatureData", __jsc_host_js_get_feature_data), - ("segfault", __jsc_host_js_segfault), - ("segfaultInDll", __jsc_host_js_segfault_in_dll), - ("panic", __jsc_host_js_panic), - ("rootError", __jsc_host_js_root_error), - ("outOfMemory", __jsc_host_js_out_of_memory), - ("operatorNew", __jsc_host_js_operator_new), - ("abort", __jsc_host_js_abort), - ("fastfail", __jsc_host_js_fastfail), - ("trap", __jsc_host_js_trap), - ( - "raiseIgnoringPanicHandler", - __jsc_host_js_raise_ignoring_panic_handler, - ), - ]; - let obj = JSValue::create_empty_object(global, ENTRIES.len()); - for &(name, func) in ENTRIES { - obj.put( - global, - name, - JSFunction::create(global, name, func, 1, Default::default()), - ); - } - obj - } - - #[bun_jsc::host_fn] - fn js_get_mach_o_image_zero_offset( - _global: &JSGlobalObject, - _frame: &CallFrame, - ) -> JsResult { + pub(crate) fn get_mach_o_image_zero_offset() -> Option { #[cfg(not(target_os = "macos"))] { - return Ok(JSValue::UNDEFINED); + None } #[cfg(target_os = "macos")] { @@ -61,17 +24,16 @@ pub(crate) mod js_bindings { } let header = _dyld_get_image_header(0); if header.is_null() { - return Ok(JSValue::UNDEFINED); + return None; } let base_address = header as usize; let vmaddr_slide = _dyld_get_image_vmaddr_slide(0) as usize; - Ok(JSValue::js_number((base_address - vmaddr_slide) as f64)) + Some((base_address - vmaddr_slide) as f64) } } - #[bun_jsc::host_fn] - fn js_segfault(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn segfault() { crash_handler::suppress_core_dumps_if_necessary(); // Under ASAN the SIGSEGV handler is intentionally not installed // (`reset_on_posix()` early-returns so ASAN's own DEADLYSIGNAL diagnostic @@ -93,14 +55,12 @@ pub(crate) mod js_bindings { core::ptr::write_unaligned(ptr, 0xDEADBEEF); core::hint::black_box(ptr); } - Ok(JSValue::UNDEFINED) } /// Triggers a segfault with the fault PC inside a system DLL rather than /// inside bun.exe. Exercises the Windows fault-context unwinder: the walk /// must recover the bun frames that called into the DLL. - #[bun_jsc::host_fn] - fn js_segfault_in_dll(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn segfault_in_dll() { crash_handler::suppress_core_dumps_if_necessary(); #[cfg(windows)] { @@ -119,20 +79,16 @@ pub(crate) mod js_bindings { // No equivalent on POSIX (the fault-context walk is fp-based and // doesn't care which image the fault is in); fall through to the // in-bun segfault so the test hook is defined everywhere. - return js_segfault(_global, _frame); + segfault(); } - #[allow(unreachable_code)] - Ok(JSValue::UNDEFINED) } - #[bun_jsc::host_fn] - fn js_panic(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn panic() { crash_handler::suppress_core_dumps_if_necessary(); crash_handler::panic_impl(b"invoked crashByPanic() handler", None); } - #[bun_jsc::host_fn] - fn js_abort(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn abort() { crash_handler::suppress_core_dumps_if_necessary(); // Under ASAN the POSIX signal handlers are not installed; invoke the // handler directly so the reporter test still observes the upload. @@ -147,8 +103,6 @@ pub(crate) mod js_bindings { unsafe { libc::abort(); } - #[allow(unreachable_code)] - Ok(JSValue::UNDEFINED) } /// Dies like foreign native code, with Bun's crash handler provably out @@ -157,8 +111,7 @@ pub(crate) mod js_bindings { /// and a raw SIGABRT on POSIX (handlers reset first, like the /// `raiseIgnoringPanicHandler` binding below). The `abort` binding /// above is the opposite: it routes into the crash handler on purpose. - #[bun_jsc::host_fn] - fn js_fastfail(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn fastfail() { crash_handler::suppress_core_dumps_if_necessary(); #[cfg(windows)] std::process::abort(); @@ -166,8 +119,7 @@ pub(crate) mod js_bindings { Global::raise_ignoring_panic_handler(bun_core::SignalCode::SIGABRT); } - #[bun_jsc::host_fn] - fn js_trap(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn trap() { crash_handler::suppress_core_dumps_if_necessary(); if Environment::ENABLE_ASAN || cfg!(windows) { crash_handler::crash_handler( @@ -192,50 +144,41 @@ pub(crate) mod js_bindings { crash_handler::CrashReason::Trap(0), crash_handler::TraceSeed::BeginAddr(crash_handler::debug::return_address()), ); - #[allow(unreachable_code)] - Ok(JSValue::UNDEFINED) } - #[bun_jsc::host_fn] - fn js_root_error(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn root_error() { crash_handler::handle_root_error("Unexpected"); } - #[bun_jsc::host_fn] - fn js_out_of_memory(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn out_of_memory() { crash_handler::suppress_core_dumps_if_necessary(); bun_core::out_of_memory(); } /// Calls `mi_new` or `mi_new_nothrow`, which back C++ `operator new` where mimalloc overrides it. /// Only `mi_new_nothrow` may return null. - #[bun_jsc::host_fn] - fn js_operator_new(_global: &JSGlobalObject, frame: &CallFrame) -> JsResult { + pub(crate) fn operator_new(nothrow: bool) -> Option { unsafe extern "C" { // safe: both accept any size, and the result is never dereferenced. safe fn mi_new(size: usize) -> *mut core::ffi::c_void; safe fn mi_new_nothrow(size: usize) -> *mut core::ffi::c_void; } - if frame.argument(0).to_boolean() { + if nothrow { let refused = mi_new_nothrow(usize::MAX / 2).is_null(); - return Ok(JSValue::js_boolean(refused)); + return Some(refused); } crash_handler::suppress_core_dumps_if_necessary(); let _ = mi_new(usize::MAX / 2); - Ok(JSValue::UNDEFINED) + None } - #[bun_jsc::host_fn] - fn js_raise_ignoring_panic_handler( - _global: &JSGlobalObject, - _frame: &CallFrame, - ) -> JsResult { + pub(crate) fn raise_ignoring_panic_handler() { crash_handler::suppress_core_dumps_if_necessary(); Global::raise_ignoring_panic_handler(bun_core::SignalCode::SIGSEGV); } - #[bun_jsc::host_fn] - fn js_get_feature_data(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn get_feature_data(cx: &mut JsScope<'_>) -> JsResult { + let global = cx.global(); let obj = JSValue::create_empty_object(global, 5); let list = analytics::PACKED_FEATURES_LIST; // Not `static_`: a feature can be named like a common string ("s3"). diff --git a/src/runtime/api/cron.bind.ts b/src/runtime/api/cron.bind.ts index 142d791997f6..2e7b7d4247cc 100644 --- a/src/runtime/api/cron.bind.ts +++ b/src/runtime/api/cron.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::api::cron" }); + const chainable = { returns: b.self, passThis: true } as const; export const CronJob = b.class({ @@ -15,3 +17,37 @@ export const CronJob = b.class({ "@@dispose": { ...chainable, rustName: "stop" }, }, }); + +const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; + +/** The reactions to the promise a job's callback returned. */ +export const onPromiseResolve = b.fn({ ...reaction, export: "Bun__CronJob__onPromiseResolve" }); +export const onPromiseReject = b.fn({ ...reaction, export: "Bun__CronJob__onPromiseReject" }); + +/** `Bun.cron(schedule, handler, options?)` or `Bun.cron(path, schedule, title)`. */ +export const cron = b.fn({ + export: "Bun__Cron__cron", + args: { pathOrSchedule: b.any, scheduleOrHandler: b.any, titleOrOptions: b.any }, + returns: b.any, + capability: "js", + throws: true, +}); + +export const remove = b.fn({ + export: "Bun__Cron__remove", + args: { title: b.string }, + expected: { title: "Bun.cron.remove() expects a string title" }, + returns: b.any, + capability: "js", + throws: true, +}); + +export const parse = b.fn({ + export: "Bun__Cron__parse", + args: { expression: b.string, from: b.any, options: b.any }, + expected: { expression: "Bun.cron.parse() expects a string cron expression as the first argument" }, + returns: b.f64.asDate.nullable, + capability: "js", + throws: true, + length: 1, +}); diff --git a/src/runtime/api/cron.rs b/src/runtime/api/cron.rs index c03f0decb443..749ba863c37a 100644 --- a/src/runtime/api/cron.rs +++ b/src/runtime/api/cron.rs @@ -22,11 +22,11 @@ use bun_core::EncodedSlice; use bun_core::env_var; use bun_io::BufferedReader as OutputReader; use bun_io::{KeepAlive, Loop as AsyncLoop}; -use bun_jsc::bindgen::JsWrapper; +use bun_jsc::bindgen::{JsLocal, JsScope, JsWrapper}; use bun_jsc::virtual_machine::{HotReload, VirtualMachine}; use bun_jsc::{ - self as jsc, CallFrame, EncodedSliceJsc as _, EventLoopHandle, GlobalRef, JSFunction, - JSGlobalObject, JSObject, JSValue, JsCell, JsRef, JsResult, + self as jsc, EncodedSliceJsc as _, EventLoopHandle, GlobalRef, JSFunction, JSGlobalObject, + JSObject, JSValue, JsCell, JsRef, JsResult, }; use bun_paths as path; use bun_ptr::{BackRef, RefPtr, ThisPtr}; @@ -39,7 +39,10 @@ use crate::api::bun::process::SpawnResultExt as _; use crate::api::bun::process::{ self as spawn, Process, ProcessHandle, Rusage, SpawnOptions, Status, }; -use crate::generated_bindgen::cron::js_CronJob as js; +use crate::generated_bindgen::cron::{ + Bun__Cron__cron, Bun__Cron__parse, Bun__Cron__remove, Bun__CronJob__onPromiseReject, + Bun__CronJob__onPromiseResolve, js_CronJob as js, +}; use crate::timer::{EventLoopTimer, EventLoopTimerState, EventLoopTimerTag}; use bun_core::ZStr; use bun_core::strings; @@ -745,17 +748,26 @@ fn resolve_cron_tz(global: &JSGlobalObject, opts: JSValue) -> JsResult { ))) } -// -- JS entry point -- (free fn: `#[host_fn]` Free shim calls bare `cron_register(..)`) - -#[bun_jsc::host_fn] -pub(crate) fn cron_register(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let args = frame.arguments_as_array::<3>(); +// -- JS entry point -- + +pub(crate) fn cron( + cx: &mut JsScope<'_>, + path_or_schedule: JsLocal<'_>, + schedule_or_handler: JsLocal<'_>, + title_or_options: JsLocal<'_>, +) -> JsResult { + let args = [ + path_or_schedule.to_js(), + schedule_or_handler.to_js(), + title_or_options.to_js(), + ]; // In-process callback cron: Bun.cron(schedule, handler, opts?) if args[1].is_callable() { - let tz = resolve_cron_tz(global, args[2])?; - return CronJob::register(&global.js_thread_of_caller(frame), args[0], args[1], tz); + let tz = resolve_cron_tz(cx.global(), args[2])?; + return CronJob::register(&cx.js_thread(), args[0], args[1], tz); } + let global = cx.global(); if args[0].is_string() && args[2].is_undefined() { return Err(global.throw_invalid_arguments(format_args!( "Bun.cron(schedule, handler) expects a function handler as the second argument" @@ -806,7 +818,7 @@ pub(crate) fn cron_register(global: &JSGlobalObject, frame: &CallFrame) -> JsRes let mut fmt_buf = [0u8; 512]; let normalized_schedule = parsed.format_numeric(&mut fmt_buf); - let abs_path = match resolve_path(global, frame, path_slice.slice()) { + let abs_path = match resolve_path(global, path_slice.slice()) { Ok(p) => p, Err(_) => { return Err(global.throw_invalid_arguments(format_args!("Failed to resolve path"))); @@ -1278,17 +1290,9 @@ impl CronRemoveJob { } } -// free fn: `#[host_fn]` Free shim calls bare `cron_remove(..)` -#[bun_jsc::host_fn] -pub(crate) fn cron_remove(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let args = frame.arguments_as_array::<1>(); - if !args[0].is_string() { - return Err(global - .throw_invalid_arguments(format_args!("Bun.cron.remove() expects a string title"))); - } - - let title_str = args[0].to_bun_string(global)?; - let title_slice = title_str.to_utf8(); +pub(crate) fn remove(cx: &mut JsScope<'_>, title: &bun_core::String) -> JsResult { + let global = cx.global(); + let title_slice = title.to_utf8(); if !validate_title(title_slice.slice()) { return Err(global.throw_invalid_arguments(format_args!( @@ -1710,8 +1714,8 @@ impl CronJob { result.then_with_value( &this.global, Self::create_tick_cell(this), - crate::generated_host_exports::Bun__CronJob__onPromiseResolve, - crate::generated_host_exports::Bun__CronJob__onPromiseReject, + Bun__CronJob__onPromiseResolve, + Bun__CronJob__onPromiseReject, ); // `then_with_value()` returns `()`, so re-check the VM status and recover on // termination: the reactions will not run. @@ -1855,15 +1859,10 @@ impl CronJob { } } -// C++ `promiseHandlerID` compares the handler passed to `JSValue::then` against -// these symbols by address, so they must stay function exports. -// HOST_EXPORT(Bun__CronJob__onPromiseResolve, jsc) -pub(crate) fn on_promise_resolve(_global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let args = frame.arguments(); +pub(crate) fn on_promise_resolve(_: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { // The cell hands back the job's ref. None: the claim was taken back (the VM is going). - let Some(job) = crate::api::native_promise_context::take::(args[args.len() - 1]) - else { - return Ok(JSValue::UNDEFINED); + let Some(job) = crate::api::native_promise_context::take::(context.to_js()) else { + return; }; // SAFETY: the claim the cell handed back is a ref on the live job. let this = unsafe { ThisPtr::new(job.as_ptr()) }; @@ -1873,22 +1872,18 @@ pub(crate) fn on_promise_resolve(_global: &JSGlobalObject, frame: &CallFrame) -> js::pending_promise_set_cached(js_this, &this.global, JSValue::UNDEFINED); } CronJob::schedule_next(this, vm); - Ok(JSValue::UNDEFINED) } -// HOST_EXPORT(Bun__CronJob__onPromiseReject, jsc) -pub(crate) fn on_promise_reject(_global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let args = frame.arguments(); +pub(crate) fn on_promise_reject(_: &mut JsScope<'_>, reason: JsLocal<'_>, context: JsLocal<'_>) { // As `on_promise_resolve`. - let Some(job) = crate::api::native_promise_context::take::(args[args.len() - 1]) - else { - return Ok(JSValue::UNDEFINED); + let Some(job) = crate::api::native_promise_context::take::(context.to_js()) else { + return; }; // SAFETY: the claim the cell handed back is a ref on the live job. let this = unsafe { ThisPtr::new(job.as_ptr()) }; let _guard = scopeguard::guard(this, CronJob::tick_settled); let vm = this.global.bun_vm().as_mut(); - let err = args[0]; + let err = reason.to_js(); let mut promise_value = JSValue::UNDEFINED; if let Some(js_this) = this.this_value.get().try_get() { promise_value = js::pending_promise_get_cached(js_this).unwrap_or(JSValue::UNDEFINED); @@ -1899,7 +1894,6 @@ pub(crate) fn on_promise_reject(_global: &JSGlobalObject, frame: &CallFrame) -> let global_ref = vm.global(); vm.unhandled_rejection(global_ref, err, promise_value); CronJob::schedule_next(this, vm); - Ok(JSValue::UNDEFINED) } // ============================================================================ @@ -1907,25 +1901,18 @@ pub(crate) fn on_promise_reject(_global: &JSGlobalObject, frame: &CallFrame) -> // ============================================================================ pub(crate) fn get_cron_object(global_this: &JSGlobalObject, _obj: &JSObject) -> JSValue { - // `#[bun_jsc::host_fn]` emits the C-ABI shim as `__jsc_host_`. - let cron_fn = JSFunction::create( - global_this, - "cron", - __jsc_host_cron_register, - 3, - Default::default(), - ); + let cron_fn = JSFunction::create(global_this, "cron", Bun__Cron__cron, 3, Default::default()); let remove_fn = JSFunction::create( global_this, "remove", - __jsc_host_cron_remove, + Bun__Cron__remove, 1, Default::default(), ); let parse_fn = JSFunction::create( global_this, "parse", - __jsc_host_cron_parse, + Bun__Cron__parse, 1, Default::default(), ); @@ -1934,18 +1921,15 @@ pub(crate) fn get_cron_object(global_this: &JSGlobalObject, _obj: &JSObject) -> cron_fn } -#[bun_jsc::host_fn] -pub(crate) fn cron_parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let args = frame.arguments_as_array::<3>(); - - if !args[0].is_string() { - return Err(global.throw_invalid_arguments(format_args!( - "Bun.cron.parse() expects a string cron expression as the first argument" - ))); - } - - let expr_str = args[0].to_bun_string(global)?; - let expr_slice = expr_str.to_utf8(); +pub(crate) fn parse( + cx: &mut JsScope<'_>, + expression: &bun_core::String, + from: JsLocal<'_>, + options: JsLocal<'_>, +) -> JsResult> { + let global = cx.global(); + let from = from.to_js(); + let expr_slice = expression.to_utf8(); let parsed = match CronExpression::parse(expr_slice.slice()) { Ok(p) => p, @@ -1957,10 +1941,9 @@ pub(crate) fn cron_parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult } }; - let from_ms: f64 = if !args[1].is_empty() && !args[1].is_undefined() && args[1] != JSValue::NULL - { - if args[1].is_number() || args[1].js_type() == jsc::JSType::JSDate { - args[1].to_number(global)? + let from_ms: f64 = if !from.is_empty() && !from.is_undefined() && from != JSValue::NULL { + if from.is_number() || from.js_type() == jsc::JSType::JSDate { + from.to_number(global)? } else { return Err(global.throw_invalid_arguments(format_args!( "Bun.cron.parse() expects the second argument to be a Date or number (ms since epoch)" @@ -1975,12 +1958,9 @@ pub(crate) fn cron_parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult return Err(global.throw_invalid_arguments(format_args!("Invalid date value"))); } - let tz = resolve_cron_tz(global, args[2])?; + let tz = resolve_cron_tz(global, options.to_js())?; - let Some(next_ms) = parsed.next(global, from_ms, tz)? else { - return Ok(JSValue::NULL); - }; - Ok(JSValue::from_date_number(global, next_ms)) + parsed.next(global, from_ms, tz) } // ============================================================================ @@ -2286,13 +2266,9 @@ fn find_crontab() -> Option { } } -fn resolve_path( - global: &JSGlobalObject, - frame: &CallFrame, - path_: &[u8], -) -> crate::Result { +fn resolve_path(global: &JSGlobalObject, path_: &[u8]) -> crate::Result { let vm = global.bun_vm().as_mut(); - let srcloc = frame.get_caller_src_loc(global); + let srcloc = global.get_caller_src_loc(); let caller_utf8 = srcloc.str.to_utf8(); let raw_dir = path::resolve_path::dirname::(caller_utf8.slice()); let source_dir: &[u8] = if raw_dir.is_empty() { b"." } else { raw_dir }; diff --git a/src/runtime/api/html_rewriter.rs b/src/runtime/api/html_rewriter.rs index 5937044b6603..db06097cf20b 100644 --- a/src/runtime/api/html_rewriter.rs +++ b/src/runtime/api/html_rewriter.rs @@ -6,9 +6,8 @@ use core::ptr::NonNull; use std::rc::Rc; use bun_jsc::{ - self as jsc, CallFrame, CommonAbortReason, CommonAbortReasonExt as _, GlobalRef, - JSGlobalObject, JSPromise, JSValue, JsCell, JsCellRefExt as _, JsRef, JsResult, SystemError, - bun_string_jsc, + self as jsc, CommonAbortReason, CommonAbortReasonExt as _, GlobalRef, JSGlobalObject, + JSPromise, JSValue, JsCell, JsCellRefExt as _, JsRef, JsResult, SystemError, bun_string_jsc, }; // Note: `bun_jsc::VirtualMachine` is a *module* re-export // (`pub use self::virtual_machine as VirtualMachine;`). The struct lives at @@ -25,6 +24,10 @@ use crate::api::native_promise_context; use crate::generated_bindgen::html_rewriter::{ AttributeIteratorResult, ContentOptions, js_HTMLRewriter, js_HTMLRewriterTransform, }; +use crate::generated_bindgen::html_rewriter_internals::{ + Bun__HTMLRewriter__onHandlerReject, Bun__HTMLRewriter__onHandlerResolve, + Bun__HTMLRewriter__onRejectInputStream, Bun__HTMLRewriter__onResolveInputStream, +}; use crate::generated_bindgen::response::js_Response; use crate::webcore::blob::SizeType as BlobSizeType; use crate::webcore::sink::JSSink; @@ -1177,8 +1180,8 @@ impl RewriterPipe { assignment_result.then_with_value( cx.global(), cell, - on_resolve_input_stream_shim, - on_reject_input_stream_shim, + Bun__HTMLRewriter__onResolveInputStream, + Bun__HTMLRewriter__onRejectInputStream, ); return; } @@ -1988,19 +1991,15 @@ impl crate::webcore::sink::JsSinkType for RewriterPipe { // ───────── .then() reactions for a content handler's promise ───────────── -bun_jsc::jsc_promise_handler!( - pub fn Bun__HTMLRewriter__onHandlerResolve => on_handler_resolve -); -bun_jsc::jsc_promise_handler!( - pub fn Bun__HTMLRewriter__onHandlerReject => on_handler_reject -); - -fn on_handler_resolve(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let args = frame.arguments(); +pub(crate) fn on_handler_resolve( + cx: &mut JsScope<'_>, + _: JsLocal<'_>, + context: JsLocal<'_>, +) -> JsResult<()> { // `take` nulls the context so its destructor is a no-op; `None` means the // suspension was already abandoned. - let Some(pipe) = native_promise_context::take::(args[args.len() - 1]) else { - return Ok(JSValue::UNDEFINED); + let Some(pipe) = native_promise_context::take::(context.to_js()) else { + return Ok(()); }; let pipe = BackRef::from(pipe); pipe.end_suspension(); @@ -2010,30 +2009,33 @@ fn on_handler_resolve(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult { - let args = frame.arguments(); - let reason = args[0]; - let Some(pipe) = native_promise_context::take::(args[args.len() - 1]) else { - return Ok(JSValue::UNDEFINED); +pub(crate) fn on_handler_reject( + cx: &mut JsScope<'_>, + reason: JsLocal<'_>, + context: JsLocal<'_>, +) -> JsResult<()> { + let global = cx.global(); + let Some(pipe) = native_promise_context::take::(context.to_js()) else { + return Ok(()); }; let pipe = BackRef::from(pipe); pipe.end_suspension(); // Fails the output stream: delivers the error to its reader (script may run). pipe.fail(webcore::body::ValueError::JSValue( - jsc::strong::Optional::create(reason, global), + jsc::strong::Optional::create(reason.to_js(), global), )); // Balances the `ref_()` in `begin_suspension`. RewriterPipe::deref_nn(pipe.into()); if global.has_exception() { return Err(jsc::JsError::Thrown); } - Ok(JSValue::UNDEFINED) + Ok(()) } // ──────────────────────── DocumentHandler ──────────────────────────────── @@ -3233,47 +3235,28 @@ impl WrapperLike for Element { // `RewriterPipe::wire_input`'s `.then_with_value()`; the cell (and thus the // pipe) stays alive as long as the pump promise's reaction is rooted. -fn on_resolve_input_stream( - _global_this: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let args = callframe.arguments(); - let Some(this) = js_HTMLRewriterTransform::from_js(args[args.len() - 1]) else { - return Ok(JSValue::UNDEFINED); +pub(crate) fn on_resolve_input_stream(_: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { + let Some(this) = js_HTMLRewriterTransform::from_js(context.to_js()) else { + return; }; let this = BackRef::from(this); let this = &*this; this.js_pump_reaction_pending.set(false); this.end_from_stream(None); - Ok(JSValue::UNDEFINED) } -fn on_reject_input_stream( - global_this: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let args = callframe.arguments(); - let Some(this) = js_HTMLRewriterTransform::from_js(args[args.len() - 1]) else { - return Ok(JSValue::UNDEFINED); +pub(crate) fn on_reject_input_stream( + cx: &mut JsScope<'_>, + reason: JsLocal<'_>, + context: JsLocal<'_>, +) { + let Some(this) = js_HTMLRewriterTransform::from_js(context.to_js()) else { + return; }; - let err = args[0]; let this = BackRef::from(this); let this = &*this; this.js_pump_reaction_pending.set(false); this.end_from_stream(Some(crate::webcore::streams::StreamError::JSValue( - jsc::strong::Optional::create(err, global_this), + jsc::strong::Optional::create(reason.to_js(), cx.global()), ))); - Ok(JSValue::UNDEFINED) -} - -// Exported as *function* symbols so `Zig::GlobalObject::promiseHandlerID`'s -// address comparison matches; a `static` fn-ptr export would export the data -// slot's address, not the code address (see `Bun__FileSink__onResolveStream`). -bun_jsc::jsc_promise_handler!( - pub(crate) fn on_resolve_input_stream_shim = "Bun__HTMLRewriter__onResolveInputStream" - => on_resolve_input_stream -); -bun_jsc::jsc_promise_handler!( - pub(crate) fn on_reject_input_stream_shim = "Bun__HTMLRewriter__onRejectInputStream" - => on_reject_input_stream -); +} diff --git a/src/runtime/api/html_rewriter_internals.bind.ts b/src/runtime/api/html_rewriter_internals.bind.ts new file mode 100644 index 000000000000..e09cca1df078 --- /dev/null +++ b/src/runtime/api/html_rewriter_internals.bind.ts @@ -0,0 +1,13 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::api::html_rewriter" }); + +const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; + +/** The reactions to the promise a content handler returned. */ +export const onHandlerResolve = b.fn({ ...reaction, throws: true, export: "Bun__HTMLRewriter__onHandlerResolve" }); +export const onHandlerReject = b.fn({ ...reaction, throws: true, export: "Bun__HTMLRewriter__onHandlerReject" }); + +/** The reactions to the promise of the JavaScript that reads the input stream. */ +export const onResolveInputStream = b.fn({ ...reaction, export: "Bun__HTMLRewriter__onResolveInputStream" }); +export const onRejectInputStream = b.fn({ ...reaction, export: "Bun__HTMLRewriter__onRejectInputStream" }); diff --git a/src/runtime/api/sourcemap.bind.ts b/src/runtime/api/sourcemap.bind.ts index cdcaef0e88b8..99a534410e51 100644 --- a/src/runtime/api/sourcemap.bind.ts +++ b/src/runtime/api/sourcemap.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::api::js_source_map" }); + const find = { returns: b.any, capability: "js", throws: true } as const; export const SourceMap = b.class({ @@ -20,3 +22,5 @@ export const SourceMap = b.class({ payload: { internalField: "payload" }, }, }); + +export const findSourceMap = b.fn({ args: { path: b.any }, returns: b.any, capability: "js", throws: true }); diff --git a/src/runtime/bake/config/plugins.bind.ts b/src/runtime/bake/config/plugins.bind.ts new file mode 100644 index 000000000000..51158a01bcb1 --- /dev/null +++ b/src/runtime/bake/config/plugins.bind.ts @@ -0,0 +1,10 @@ +import * as b from "../../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::bake::config::plugins" }); + +export const setConfigPlugins = b.fn({ args: { plugins: b.any, names: b.any }, capability: "js", throws: true }); +export const isRestart = b.fn({ returns: b.bool }); +export const restartProcess = b.fn({ capability: "js" }); +export const isRestarting = b.fn({ returns: b.bool }); +export const setPages = b.fn({ args: { framework: b.any }, capability: "js" }); +export const checkFramework = b.fn({ args: { framework: b.any }, capability: "js", throws: true }); diff --git a/src/runtime/bake/config/plugins.rs b/src/runtime/bake/config/plugins.rs index cbd6e6813095..51d6a2d28a0b 100644 --- a/src/runtime/bake/config/plugins.rs +++ b/src/runtime/bake/config/plugins.rs @@ -1,14 +1,19 @@ //! `bun bake`: the plugins of `bake.config.ts`. `internal/bake.ts` checks them, then hands them over //! here before it starts the server. -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use bun_jsc::JsResult; +use bun_jsc::bindgen::{JsLocal, JsScope}; /// `setConfigPlugins(plugins, names)`: `plugins` is an array of Bun plugin objects and `names` is /// an array of their names. -pub(crate) fn set_config_plugins(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let [plugins, names] = frame.arguments_as_array::<2>(); +pub(crate) fn set_config_plugins<'a>( + cx: &mut JsScope<'a>, + plugins: JsLocal<'a>, + names: JsLocal<'a>, +) -> JsResult<()> { + let global = cx.global(); let mut list: Vec> = Vec::new(); - let mut iter = names.array_iterator(global)?; + let mut iter = names.to_js().array_iterator(global)?; while let Some(name) = iter.next()? { list.push(Box::from(&*name.to_utf8(global)?)); } @@ -17,10 +22,10 @@ pub(crate) fn set_config_plugins(global: &JSGlobalObject, frame: &CallFrame) -> if list.is_empty() { vm.bake_config_plugins.deinit(); } else { - vm.bake_config_plugins.set(global, plugins); + vm.bake_config_plugins.set(global, plugins.to_js()); } vm.bake_config_plugin_names = list.into_boxed_slice(); - Ok(JSValue::UNDEFINED) + Ok(()) } /// Set in the environment by the first start of `bun bake --check`, so that the process that a @@ -46,18 +51,16 @@ pub(crate) fn mark_restart() { } /// `isRestart()`: whether this process is `bun bake --check` started again after a change. -pub(crate) fn is_restart(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - Ok(JSValue::from( - RESTARTED.load(core::sync::atomic::Ordering::Relaxed), - )) +pub(crate) fn is_restart() -> bool { + RESTARTED.load(core::sync::atomic::Ordering::Relaxed) } /// `restartProcess()`: starts `bun bake` again with the same arguments, after a change to /// `bake.config.ts`. The server loads plugins once, so only a new process gets the new ones. /// When the restart fails, `reload_process` prints why and this exits 1: the kill signal handlers /// have run and the signal dispositions are reset, so the server cannot go on. -pub(crate) fn restart_process(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - global.bun_vm().as_mut().restart_process(true); +pub(crate) fn restart_process(cx: &mut JsScope<'_>) { + cx.global().bun_vm().as_mut().restart_process(true); // Not `process.exit()`: during a restart it tries the restart again, and panics when it fails. bun_core::Output::flush(); bun_core::Global::exit(1); @@ -65,16 +68,15 @@ pub(crate) fn restart_process(global: &JSGlobalObject, _frame: &CallFrame) -> Js /// `isRestarting()`: whether a restart is running the kill signal listeners. The process is then /// about to start again, so a listener must not end it. -pub(crate) fn is_restarting(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - Ok(JSValue::from( - bun_jsc::posix_signal_handle::is_emitting_watch_kill_signal(), - )) +pub(crate) fn is_restarting() -> bool { + bun_jsc::posix_signal_handle::is_emitting_watch_kill_signal() } /// `setPages(framework?)`: the next `Bun.serve()` adds the routers of `framework` (the `framework` /// object of a plugin), or of the default React framework when it is undefined. -pub(crate) fn set_pages(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let [framework] = frame.arguments_as_array::<1>(); +pub(crate) fn set_pages<'a>(cx: &mut JsScope<'a>, framework: JsLocal<'a>) { + let global = cx.global(); + let framework = framework.to_js(); let vm = global.bun_vm().as_mut(); if framework.is_undefined_or_null() { vm.bake_framework.deinit(); @@ -82,16 +84,15 @@ pub(crate) fn set_pages(global: &JSGlobalObject, frame: &CallFrame) -> JsResult< vm.bake_framework.set(global, framework); } vm.bake_pages = true; - Ok(JSValue::UNDEFINED) } /// `checkFramework(framework)`: throws the error that `Bun.serve()` would throw for this `framework` /// object of a plugin, so that `bun bake` reports it as an error of `bake.config.ts`. -pub(crate) fn check_framework(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let [framework] = frame.arguments_as_array::<1>(); +pub(crate) fn check_framework<'a>(cx: &mut JsScope<'a>, framework: JsLocal<'a>) -> JsResult<()> { let mut bundler_options = crate::bake::SplitBundlerOptions::default(); - let result = crate::bake::Framework::from_js(framework, global, &mut bundler_options); + let result = + crate::bake::Framework::from_js(framework.to_js(), cx.global(), &mut bundler_options); bundler_options.destroy_plugin(); result?; - Ok(JSValue::UNDEFINED) + Ok(()) } diff --git a/src/runtime/bake/router/js.bind.ts b/src/runtime/bake/router/js.bind.ts new file mode 100644 index 000000000000..d8ff0f91c8c9 --- /dev/null +++ b/src/runtime/bake/router/js.bind.ts @@ -0,0 +1,11 @@ +import * as b from "../../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::bake::router::JSFrameworkRouter" }); + +export const parseRoutePattern = b.fn({ + args: { style: b.any, filepath: b.any }, + returns: b.any, + length: 1, + capability: "js", + throws: true, +}); diff --git a/src/runtime/bake/router/js.rs b/src/runtime/bake/router/js.rs index d462ef8ddd43..dabeaf13da08 100644 --- a/src/runtime/bake/router/js.rs +++ b/src/runtime/bake/router/js.rs @@ -6,14 +6,10 @@ use bun_alloc::{AllocError, Arena}; use bun_collections::BoundedArray; use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::bun_string_jsc; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, StringJsc, StrongOptional}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult, StringJsc, StrongOptional}; use bun_paths as paths; use super::*; -use crate::generated_bindgen::filesystem_router::js_FrameworkFileSystemRouter as js; - -/// `generated_js2native.rs` calls `js_framework_router::get_bindings`. -pub(crate) use self::JSFrameworkRouter as js_framework_router; /// This binding is currently only intended for testing FrameworkRouter, and not /// production usage. It uses a slower but easier to use pattern for object @@ -31,23 +27,6 @@ pub(crate) struct StoredParseError { } impl JSFrameworkRouter { - pub(crate) fn get_bindings(global: &JSGlobalObject) -> JsResult { - let obj = JSValue::create_empty_object(global, 2); - obj.put( - global, - b"parseRoutePattern", - bun_jsc::JSFunction::create( - global, - "parseRoutePattern", - __jsc_host_parse_route_pattern, - 1, - Default::default(), - ), - ); - obj.put(global, b"FrameworkRouter", js::get_constructor(global)); - Ok(obj) - } - pub(crate) fn constructor(cx: &mut JsScope<'_>, options: JsLocal<'_>) -> JsResult { let global = cx.global(); let options = options.to_js(); @@ -239,20 +218,16 @@ impl JSFrameworkRouter { Ok(obj) } - pub(crate) fn parse_route_pattern( - global: &JSGlobalObject, - frame: &CallFrame, + pub(crate) fn parse_route_pattern<'a>( + cx: &mut JsScope<'a>, + style: JsLocal<'a>, + filepath: JsLocal<'a>, ) -> JsResult { + let global = cx.global(); let arena = Arena::new(); - if frame.arguments_count() < 2 { - return Err(global - .throw_invalid_arguments(format_args!("parseRoutePattern takes two arguments"))); - } - - let [style_js, filepath_js] = frame.arguments_as_array::<2>(); - let filepath = filepath_js.to_utf8(global)?; - let style = Style::from_js(style_js, global)?; + let filepath = filepath.to_js().to_utf8(global)?; + let style = Style::from_js(style.to_js(), global)?; let mut log = TinyLog::empty(); let parsed = match style.parse( @@ -311,13 +286,6 @@ impl JSFrameworkRouter { } } -// Note: free-function host-fn shim — `#[bun_jsc::host_fn]` (Free kind) emits a -// shim that calls the bare ident, so this cannot live inside the `impl` block. -#[bun_jsc::host_fn] -fn parse_route_pattern(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - JSFrameworkRouter::parse_route_pattern(global, frame) -} - use bun_core::fmt::VecWriter as ByteFmtWriter; // `scan` needs `&mut jsfr.router` and the insertion handler simultaneously. diff --git a/src/runtime/build.rs b/src/runtime/build.rs index 0685ac6d8567..6d034cc7b03e 100644 --- a/src/runtime/build.rs +++ b/src/runtime/build.rs @@ -4,7 +4,7 @@ clippy::disallowed_types, clippy::disallowed_macros )] -//! Set BUN_CODEGEN_DIR for `include!(concat!(env!("BUN_CODEGEN_DIR"), "/generated_js2native.rs"))`. +//! Set BUN_CODEGEN_DIR for `include!(concat!(env!("BUN_CODEGEN_DIR"), "/generated_jssink.rs"))`. //! //! The codegen output lives at `/build//codegen/` and is //! produced by the scripts in `src/codegen/` (run as part of `bun bd`). @@ -28,14 +28,6 @@ fn main() { .map(PathBuf::from) .unwrap_or_else(|_| repo.join("build/debug/codegen")); - let gen_js2native_rs = codegen_dir.join("generated_js2native.rs"); - if !gen_js2native_rs.exists() { - panic!( - "generated_js2native.rs not found at {} — run `bun bd` (bundle-modules codegen) first", - gen_js2native_rs.display() - ); - } - let gen_jssink_rs = codegen_dir.join("generated_jssink.rs"); if !gen_jssink_rs.exists() { panic!( @@ -54,7 +46,6 @@ fn main() { } println!("cargo:rustc-env=BUN_CODEGEN_DIR={}", codegen_dir.display()); - println!("cargo:rerun-if-changed={}", gen_js2native_rs.display()); println!("cargo:rerun-if-changed={}", gen_jssink_rs.display()); println!("cargo:rerun-if-changed={}", gen_host_exports_rs.display()); println!("cargo:rerun-if-env-changed=BUN_CODEGEN_DIR"); diff --git a/src/runtime/cli/pack_command.bind.ts b/src/runtime/cli/pack_command.bind.ts new file mode 100644 index 000000000000..b801d90f06ce --- /dev/null +++ b/src/runtime/cli/pack_command.bind.ts @@ -0,0 +1,10 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::cli::pack_command::bindings" }); + +export const readTarball = b.fn({ + args: { tarballPath: b.string }, + returns: b.any, + capability: "js", + throws: true, +}); diff --git a/src/runtime/cli/pack_command.rs b/src/runtime/cli/pack_command.rs index 8a0deea22074..685b2f9ef9b2 100644 --- a/src/runtime/cli/pack_command.rs +++ b/src/runtime/cli/pack_command.rs @@ -3147,22 +3147,14 @@ fn is_special_file_or_variant(filename: &[u8], name: &'static [u8]) -> bool { pub(crate) mod bindings { use super::*; use bun_core::String as BunString; - use bun_jsc::{ - CallFrame, JSArray, JSGlobalObject, JSValue, JsResult, StringJsc as _, bun_string_jsc, - }; + use bun_jsc::bindgen::JsScope; + use bun_jsc::{JSArray, JSValue, JsResult, StringJsc as _, bun_string_jsc}; - #[bun_jsc::host_fn] - pub(crate) fn js_read_tarball( - global: &JSGlobalObject, - call_frame: &CallFrame, + pub(crate) fn read_tarball( + cx: &mut JsScope<'_>, + tarball_path_str: &BunString, ) -> JsResult { - let args = call_frame.arguments(); - if args.len() < 1 || !args[0].is_string() { - return Err(global.throw(format_args!("expected tarball path string argument"))); - } - - let tarball_path_str = args[0].to_bun_string(global)?; - + let global = cx.global(); let tarball_path = tarball_path_str.to_utf8(); let tarball_file = match bun_sys::open_file(tarball_path.slice(), Default::default()) { diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index b5080b7f7ddc..8f82021427a8 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -1664,15 +1664,11 @@ impl Run<'_> { if let Some(promise) = result.as_any_promise() { match promise.status() { PromiseStatus::Pending => { - // C-ABI shims are emitted by - // `generate-host-exports.ts` into - // `crate::generated_host_exports` under their - // link name (`Bun__on…EntryPointResult`). result.then2( vm.global(), JSValue::UNDEFINED, - crate::generated_host_exports::Bun__onResolveEntryPointResult, - crate::generated_host_exports::Bun__onRejectEntryPointResult, + crate::generated_bindgen::hw_exports::Bun__onResolveEntryPointResult, + crate::generated_bindgen::hw_exports::Bun__onRejectEntryPointResult, ); vm.tick(); vm.auto_tick_active(); diff --git a/src/runtime/cli/upgrade_command.bind.ts b/src/runtime/cli/upgrade_command.bind.ts new file mode 100644 index 000000000000..599cabdaf06e --- /dev/null +++ b/src/runtime/cli/upgrade_command.bind.ts @@ -0,0 +1,8 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::cli::upgrade_command::upgrade_js_bindings" }); + +export const upgradeTestHelpers = b.namespace({ + openTempDirWithoutSharingDelete: { length: 1 }, + closeTempDirHandle: { length: 1 }, +}); diff --git a/src/runtime/cli/upgrade_command.rs b/src/runtime/cli/upgrade_command.rs index 0ab50a055aef..666c5c3b542b 100644 --- a/src/runtime/cli/upgrade_command.rs +++ b/src/runtime/cli/upgrade_command.rs @@ -11,7 +11,6 @@ use bun_core::{ZStr, strings}; use bun_dotenv as DotEnv; use bun_http::{self as HTTP, headers}; use bun_install::integrity::{Integrity, Tag as IntegrityTag}; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsResult}; use bun_parsers::json as JSON; use bun_paths::{self, SEP_STR}; use bun_resolver::fs; @@ -1381,6 +1380,7 @@ impl UpgradeCommand { // ────────────────────────────────────────────────────────────────────────── pub(crate) mod upgrade_js_bindings { + #[cfg(windows)] use super::*; // Process-global, not threadlocal: if open/close are invoked from different @@ -1390,46 +1390,9 @@ pub(crate) mod upgrade_js_bindings { #[cfg(windows)] static TEMPDIR_FD: bun_core::RacyCell> = bun_core::RacyCell::new(None); - pub(crate) fn generate(global: &JSGlobalObject) -> JSValue { - let obj = JSValue::create_empty_object(global, 2); - obj.put( - global, - b"openTempDirWithoutSharingDelete", - jsc::JSFunction::create( - global, - "openTempDirWithoutSharingDelete", - // `#[bun_jsc::host_fn]` emits the C-ABI shim with a - // `__jsc_host_` prefix. - __jsc_host_js_open_temp_dir_without_sharing_delete, - 1, - Default::default(), - ), - ); - obj.put( - global, - b"closeTempDirHandle", - jsc::JSFunction::create( - global, - "closeTempDirHandle", - __jsc_host_js_close_temp_dir_handle, - 1, - Default::default(), - ), - ); - obj - } - /// For testing upgrades when the temp directory has an open handle without FILE_SHARE_DELETE. /// Windows only - #[bun_jsc::host_fn] - fn js_open_temp_dir_without_sharing_delete( - _global: &JSGlobalObject, - _frame: &CallFrame, - ) -> JsResult { - #[cfg(not(windows))] - { - return Ok(JSValue::UNDEFINED); - } + pub(crate) fn open_temp_dir_without_sharing_delete() { #[cfg(windows)] { use sys::windows as w; @@ -1439,7 +1402,7 @@ pub(crate) mod upgrade_js_bindings { let mut wtmp = bun_paths::w_path_buffer_pool::get(); let tmpdir_w = bun_core::convert_utf8_to_utf16_in_buffer(&mut wtmp[..], tmpdir_path); let path = match sys::normalize_path_windows(sys::Fd::INVALID, tmpdir_w, &mut buf[..]) { - sys::Result::Err(_) => return Ok(JSValue::UNDEFINED), + sys::Result::Err(_) => return, sys::Result::Ok(norm) => norm, }; @@ -1497,17 +1460,10 @@ pub(crate) mod upgrade_js_bindings { } _ => {} } - - Ok(JSValue::UNDEFINED) } } - #[bun_jsc::host_fn] - fn js_close_temp_dir_handle(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - #[cfg(not(windows))] - { - return Ok(JSValue::UNDEFINED); - } + pub(crate) fn close_temp_dir_handle() { #[cfg(windows)] { use bun_sys::FdExt as _; @@ -1517,8 +1473,6 @@ pub(crate) mod upgrade_js_bindings { if let Some(fd) = unsafe { core::mem::take(&mut *TEMPDIR_FD.get()) } { fd.close(); } - - Ok(JSValue::UNDEFINED) } } } diff --git a/src/runtime/crypto/PBKDF2.rs b/src/runtime/crypto/PBKDF2.rs index 3ef936c1d71e..3c861b6cb566 100644 --- a/src/runtime/crypto/PBKDF2.rs +++ b/src/runtime/crypto/PBKDF2.rs @@ -1,9 +1,7 @@ use core::ffi::c_uint; use bun_boringssl_sys as boringssl; -use bun_jsc::{ - ArrayBuffer, CallFrame, JSGlobalObject, JSValue, Job, JobContext, JsResult, JsThread, Strong, -}; +use bun_jsc::{ArrayBuffer, JSGlobalObject, JSValue, Job, JobContext, JsResult, JsThread, Strong}; use crate::node::{Flavor, StringOrBuffer, ThreadIsolated, ThreadIsolatedArg}; @@ -60,23 +58,27 @@ impl PBKDF2 { /// `JSValue::UNDEFINED` on `Sync` (as `Scrypt::from_js`). pub(crate) fn from_js( global_this: &JSGlobalObject, - call_frame: &CallFrame, + password: JSValue, + salt: JSValue, + iterations: JSValue, + keylen: JSValue, + mut digest: JSValue, + mut callback: JSValue, flavor: Flavor, ) -> JsResult<(PBKDF2, JSValue)> { - let [arg0, arg1, arg2, arg3, mut arg4, mut arg5] = call_frame.arguments_as_array::<6>(); // pbkdf2(password, salt, iterations, keylen, callback): digest omitted. - if flavor == Flavor::Async && arg4.is_function() { - arg5 = arg4; - arg4 = JSValue::UNDEFINED; + if flavor == Flavor::Async && digest.is_function() { + callback = digest; + digest = JSValue::UNDEFINED; } - if !arg3.is_number() { - return Err(global_this.throw_invalid_argument_type_value(b"keylen", b"number", arg3)); + if !keylen.is_number() { + return Err(global_this.throw_invalid_argument_type_value(b"keylen", b"number", keylen)); } - let keylen_num = arg3.as_number(); + let keylen_num = keylen.as_number(); - if !arg3.is_integer() { + if !keylen.is_integer() { return Err(global_this.throw_range_error( keylen_num, bun_jsc::RangeErrorOptions { @@ -102,17 +104,17 @@ impl PBKDF2 { // 0..=i32::MAX was checked above. let keylen = keylen_num as usize; - if !arg2.is_number() { + if !iterations.is_number() { return Err(global_this.throw_invalid_argument_type_value( b"iterations", b"number", - arg2, + iterations, )); } - let iterations_num = arg2.as_number(); + let iterations_num = iterations.as_number(); - if !arg2.is_integer() { + if !iterations.is_integer() { return Err(global_this.throw_range_error( iterations_num, bun_jsc::RangeErrorOptions { @@ -138,14 +140,14 @@ impl PBKDF2 { let iteration_count: i64 = iterations_num as i64; let algorithm = 'brk: { - if !arg4.is_string() { + if !digest.is_string() { return Err( - global_this.throw_invalid_argument_type_value(b"digest", b"string", arg4) + global_this.throw_invalid_argument_type_value(b"digest", b"string", digest) ); } 'invalid: { - let slice = arg4.to_utf8(global_this)?; + let slice = digest.to_utf8(global_this)?; match evp::lookup_ignore_case(slice.slice()) { Some(alg) => match alg { Algorithm::Shake128 | Algorithm::Shake256 => break 'invalid, @@ -156,7 +158,7 @@ impl PBKDF2 { } } - let slice = arg4.to_utf8(global_this)?; + let slice = digest.to_utf8(global_this)?; let name = slice.slice(); return Err(global_this .err( @@ -173,13 +175,13 @@ impl PBKDF2 { length: keylen, algorithm, }; - out.salt = match StringOrBuffer::from_js(global_this, arg1)? { + out.salt = match StringOrBuffer::from_js(global_this, salt)? { Some(v) => v, None => { return Err(global_this.throw_invalid_argument_type_value( b"salt", b"string or buffer", - arg1, + salt, )); } }; @@ -188,13 +190,13 @@ impl PBKDF2 { return Err(global_this.throw_invalid_arguments(format_args!("salt is too long"))); } - out.password = match StringOrBuffer::from_js(global_this, arg0)? { + out.password = match StringOrBuffer::from_js(global_this, password)? { Some(v) => v, None => { return Err(global_this.throw_invalid_argument_type_value( b"password", b"string or buffer", - arg0, + password, )); } }; @@ -205,14 +207,14 @@ impl PBKDF2 { let callback = match flavor { Flavor::Async => { - if !arg5.is_function() { + if !callback.is_function() { return Err(global_this.throw_invalid_argument_type_value( b"callback", b"function", - arg5, + callback, )); } - arg5 + callback } Flavor::Sync => JSValue::UNDEFINED, }; @@ -236,9 +238,23 @@ impl PBKDF2 { /// [`from_js`](Self::from_js) for the work-pool job, with its validated callback. pub(crate) fn from_js_async( global_this: &JSGlobalObject, - call_frame: &CallFrame, + password: JSValue, + salt: JSValue, + iterations: JSValue, + keylen: JSValue, + digest: JSValue, + callback: JSValue, ) -> JsResult<(ThreadIsolated, JSValue)> { - let (data, callback) = Self::from_js(global_this, call_frame, Flavor::Async)?; + let (data, callback) = Self::from_js( + global_this, + password, + salt, + iterations, + keylen, + digest, + callback, + Flavor::Async, + )?; // SAFETY: `Flavor::Async` copied the buffers and thread-isolated the strings. Ok((unsafe { ThreadIsolated::new(data) }, callback)) } diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index bcc58784fffe..c87b4e829fb9 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -19,12 +19,6 @@ //! 3. a `run_task` arm and a `release_task_unrun` arm here; //! 4. bump the `task_tag::COUNT` assertion below. -// Flat re-export landing pad for `generated_js2native.rs` thunks. Kept in a -// sibling file so this hot-path module stays focused on the task/timer/poll -// match loops. -#[path = "dispatch_js2native.rs"] -pub(crate) mod js2native; - use bun_event_loop::{Task, task_tag}; // `FilePoll::on_update` dispatch is POSIX-only (the symbol is declared diff --git a/src/runtime/dispatch_js2native.rs b/src/runtime/dispatch_js2native.rs deleted file mode 100644 index 61acce12055a..000000000000 --- a/src/runtime/dispatch_js2native.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! `crate::dispatch::js2native` — flat re-export landing pad for the -//! `JS2Rust__*` thunks emitted into `generated_js2native.rs`. -//! -//! `src/codegen/generate-js2native.ts::rustTarget()` routes every `$rust(...)` -//! call site whose source file lives **outside** `src/runtime/` through -//! `crate::dispatch::js2native::` instead of the file's own crate -//! path. `bun_runtime` is the highest-tier crate (already depends on every -//! `*_jsc` bridge crate plus `bun_jsc` itself), so the cross-crate fan-out -//! lands here without introducing a dep cycle. Each entry below is a `pub use` -//! of the real hand-ported function — there are no local bodies and no -//! fallback panics; a missing target stays a compile error in the owning -//! crate. -//! -//! Naming: the mangled identifier is `snake(.join("_")) ++ "_" -//! ++ snake(symbol).replace("::", "_")` (see `generate-js2native.ts`). - -pub(crate) use bun_sql_jsc::mysql::create_binding as sql_jsc_mysql_create_binding; -pub(crate) use bun_sql_jsc::postgres::create_binding as sql_jsc_postgres_create_binding; - -// The real body already lives in this crate. -pub(crate) use crate::api::crash_handler_jsc::js_bindings::generate as crash_handler_crash_handler_js_bindings_generate; - -pub(crate) use bun_install_jsc::hosted_git_info_jsc::js_from_url as install_hosted_git_info_testing_ap_is_js_from_url; -pub(crate) use bun_install_jsc::hosted_git_info_jsc::js_parse_url as install_hosted_git_info_testing_ap_is_js_parse_url; -pub(crate) use bun_install_jsc::install_binding::bun_install_js_bindings::generate as install_jsc_install_binding_bun_install_js_bindings_generate; -pub(crate) use bun_install_jsc::npm_jsc::architecture_is_match as install_npm_architecture_js_function_architecture_is_match; -pub(crate) use bun_install_jsc::npm_jsc::operating_system_is_match as install_npm_operating_system_js_function_operating_system_is_match; -pub(crate) use bun_install_jsc::npm_jsc::package_manifest_bindings_generate as install_npm_package_manifest_bindings_generate; - -// The `*_jsc` bodies live in `bun_install_jsc::ini_jsc` -// (ini's only JSC consumer is `bun install`'s npmrc loader). -pub(crate) use bun_install_jsc::ini_jsc::ini_testing_load_npmrc_from_js as ini_ini_ini_testing_ap_is_load_npmrc_from_js; -pub(crate) use bun_install_jsc::ini_jsc::ini_testing_parse as ini_ini_ini_testing_ap_is_parse; - -pub(crate) use bun_jsc::bytecode_order_recorder::names_for_testing as jsc_bytecode_order_recorder_names_for_testing; -pub(crate) use bun_jsc::counters::create_counters_object as jsc_counters_create_counters_object; -pub(crate) use bun_jsc::event_loop::get_active_tasks as jsc_event_loop_get_active_tasks; -pub(crate) use bun_jsc::virtual_machine_exports::Bun__setSyntheticAllocationLimitForTesting as jsc_virtual_machine_exports_bun__set_synthetic_allocation_limit_for_testing; - -pub(crate) use bun_jsc::bun_string_jsc::js_escape_reg_exp as string_escape_reg_exp_js_escape_reg_exp; -pub(crate) use bun_jsc::bun_string_jsc::js_escape_reg_exp_for_package_name_matching as string_escape_reg_exp_js_escape_reg_exp_for_package_name_matching; -pub(crate) use bun_jsc::bun_string_jsc::unicode_testing_apis::to_utf16_alloc_sentinel as bun_core_string_immutable_unicode_testing_ap_is_to_utf16_alloc_sentinel; - -pub(crate) use bun_semver_jsc::SemverObject::intersects_for_testing as semver_jsc_semver_object_intersects_for_testing; - -pub(crate) use bun_patch_jsc::testing::patch_apply as patch_patch_testing_ap_is_apply; -pub(crate) use bun_patch_jsc::testing::patch_make_diff as patch_patch_testing_ap_is_make_diff; -pub(crate) use bun_patch_jsc::testing::patch_parse as patch_patch_testing_ap_is_parse; - -pub(crate) use bun_sourcemap_jsc::internal_jsc::testing_find as sourcemap_internal_source_map_testing_ap_is_find; -pub(crate) use bun_sourcemap_jsc::internal_jsc::testing_from_vlq as sourcemap_internal_source_map_testing_ap_is_from_vlq; -pub(crate) use bun_sourcemap_jsc::internal_jsc::testing_to_vlq as sourcemap_internal_source_map_testing_ap_is_to_vlq; - -pub(crate) use bun_sys_jsc::error_jsc::TestingAPIs::sigaction_layout as sys_sys_testing_ap_is_sigaction_layout; -pub(crate) use bun_sys_jsc::error_jsc::TestingAPIs::translate_nt_status_to_e as sys_sys_testing_ap_is_translate_nt_status_to_e; -pub(crate) use bun_sys_jsc::error_jsc::TestingAPIs::translate_uv_error_to_e as sys_sys_testing_ap_is_translate_uv_error_to_e; - -pub(crate) use bun_http_jsc::headers_jsc::h2_live_counts as http_h2_client_testing_ap_is_live_counts; -pub(crate) use bun_http_jsc::headers_jsc::h3_quic_live_counts as http_h3_client_testing_ap_is_quic_live_counts; - -mod css { - pub(crate) use bun_css_jsc::css_internals::{ - _test, attr_test, minify_error_test_with_options, minify_test, minify_test_with_options, - prefix_test, prefix_test_with_options, test_with_options, - }; -} -pub(crate) use css::_test as css_jsc_css_internals__test; -pub(crate) use css::attr_test as css_jsc_css_internals_attr_test; -pub(crate) use css::minify_error_test_with_options as css_jsc_css_internals_minify_error_test_with_options; -pub(crate) use css::minify_test as css_jsc_css_internals_minify_test; -pub(crate) use css::minify_test_with_options as css_jsc_css_internals_minify_test_with_options; -pub(crate) use css::prefix_test as css_jsc_css_internals_prefix_test; -pub(crate) use css::prefix_test_with_options as css_jsc_css_internals_prefix_test_with_options; -pub(crate) use css::test_with_options as css_jsc_css_internals_test_with_options; - -// `LinearFifo` has no JSC consumer of its own; this `bun:internal-for-testing` -// probe lives in `bun_runtime` (which depends on both `bun_collections` and -// `bun_jsc`) rather than inventing a JSC edge into the collections crate. -pub(crate) use crate::linear_fifo_testing::ordered_remove_probe as collections_linear_fifo_testing_ap_is_ordered_remove_probe; - -// ported from: generated_js2native.rs diff --git a/src/runtime/dns_jsc/cares_jsc.bind.ts b/src/runtime/dns_jsc/cares_jsc.bind.ts new file mode 100644 index 000000000000..08e7532d7883 --- /dev/null +++ b/src/runtime/dns_jsc/cares_jsc.bind.ts @@ -0,0 +1,5 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::dns_jsc::cares_jsc" }); + +export const canonicalizeIP = b.fn({ args: { address: b.any }, returns: b.any, capability: "js", throws: true }); diff --git a/src/runtime/dns_jsc/cares_jsc.rs b/src/runtime/dns_jsc/cares_jsc.rs index b2c195118c16..02e1041114b1 100644 --- a/src/runtime/dns_jsc/cares_jsc.rs +++ b/src/runtime/dns_jsc/cares_jsc.rs @@ -1,5 +1,5 @@ //! JSC bridges for c-ares reply structs. Keeps `src/cares_sys/` free of -//! `JSValue`/`JSGlobalObject`/`CallFrame` types — the original methods on +//! `JSValue`/`JSGlobalObject` types — the original methods on //! each `struct_ares_*_reply` are aliased to the free fns here. use core::ffi::c_int; @@ -7,9 +7,8 @@ use core::ffi::c_int; use ::bstr::BStr; use bun_cares_sys::c_ares_draft as c_ares; use bun_core as bstr; -use bun_jsc::{ - CallFrame, JSGlobalObject, JSValue, JsResult, StringJsc, SystemError, bun_string_jsc, -}; +use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult, StringJsc, SystemError, bun_string_jsc}; use crate::dns_jsc::options_jsc::{address_to_js, result_to_js}; @@ -851,21 +850,11 @@ pub(crate) fn not_a_hostname_error(global_this: &JSGlobalObject, hostname: &[u8] .to_error_instance(global_this) } -// ── canonicalizeIP host fn ───────────────────────────────────────────────── -// `#[bun_jsc::host_fn(export = ...)]` emits the C-ABI shim under that link name. -#[bun_jsc::host_fn(export = "Bun__canonicalizeIP")] -fn bun_canonicalize_ip(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn canonicalize_ip<'a>(cx: &mut JsScope<'a>, address: JsLocal<'a>) -> JsResult { bun_jsc::mark_binding!(); + let global_this = cx.global(); - let arguments = callframe.arguments(); - - if arguments.is_empty() { - return Err(global_this.throw_invalid_arguments(format_args!( - "canonicalizeIP() expects a string but received no arguments." - ))); - } - - let addr_arg = arguments[0].to_utf8(global_this)?; + let addr_arg = address.to_js().to_utf8(global_this)?; let addr_str = addr_arg.slice(); let mut ip_addr = [0u8; bun_boringssl::INET6_ADDRSTRLEN + 1]; @@ -873,7 +862,7 @@ fn bun_canonicalize_ip(global_this: &JSGlobalObject, callframe: &CallFrame) -> J return Ok(JSValue::UNDEFINED); }; if addr_str == slice { - return Ok(arguments[0]); + return Ok(address.to_js()); } bun_string_jsc::create_utf8_for_js(global_this, slice) diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index 737f57db47e3..cd67686b1c00 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -25,7 +25,7 @@ use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::bun_string_jsc; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{ - self as jsc, CallFrame, JSGlobalObject, JSPromiseStrong, JSValue, JsCell, JsResult, SystemError, + self as jsc, JSGlobalObject, JSPromiseStrong, JSValue, JsCell, JsResult, SystemError, }; use bun_ptr::RefPtr; #[cfg(windows)] @@ -2844,45 +2844,33 @@ pub(crate) mod internal { } /// `bun:internal-for-testing`: [`is_localhost_name`] for one hostname. - pub(crate) fn is_localhost_name_for_testing( - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - let hostname = frame.argument(0); - if !hostname.is_string() { - return Err(global.throw_invalid_arguments(format_args!("expected (hostname: string)"))); - } - let hostname = hostname.to_utf8(global)?; - Ok(JSValue::js_boolean(is_localhost_name(hostname.slice()))) + pub(crate) fn is_localhost_name_for_testing(hostname: &bun::String) -> bool { + is_localhost_name(hostname.to_utf8().slice()) } /// `bun:internal-for-testing`: [`is_all_loopback_of_one_family`] over `addresses` laid out as a getaddrinfo() answer. - pub(crate) fn is_all_loopback_of_one_family_for_testing( - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - let mut addrs = addresses_for_testing(global, frame.argument(0))?; + pub(crate) fn is_all_loopback_of_one_family_for_testing<'a>( + cx: &mut JsScope<'a>, + addresses: JsLocal<'a>, + ) -> JsResult { + let mut addrs = addresses_for_testing(cx.global(), addresses.to_js())?; let chain = addrinfo_chain(&mut addrs); let head = chain.first().map_or(ptr::null(), ptr::from_ref); - Ok(JSValue::js_boolean(is_all_loopback_of_one_family(head))) + Ok(is_all_loopback_of_one_family(head)) } /// `bun:internal-for-testing`: seed the connect-path DNS cache for `hostname` /// by running `addresses` through the real [`process_results`] interleave and /// storing the result, so a real `fetch()` / `Bun.connect()` consumes it. - pub(crate) fn seed_cache_for_testing( - global: &JSGlobalObject, - frame: &CallFrame, + pub(crate) fn seed_cache_for_testing<'a>( + cx: &mut JsScope<'a>, + hostname: &bun::String, + addresses: JsLocal<'a>, ) -> JsResult { - let args = frame.arguments(); - if args.len() < 2 || !args[0].is_string() { - return Err(global.throw_invalid_arguments(format_args!( - "expected (hostname: string, addresses: string[])" - ))); - } - let hostname_slice = args[0].to_utf8(global)?; + let global = cx.global(); + let hostname_slice = hostname.to_utf8(); let hostname_z = bun::ZBox::from_bytes(hostname_slice.slice()); - let mut addrs = addresses_for_testing(global, args[1])?; + let mut addrs = addresses_for_testing(global, addresses.to_js())?; if addrs.is_empty() { return Err(global.throw_invalid_arguments(format_args!("addresses must not be empty"))); } @@ -2925,17 +2913,13 @@ pub(crate) mod internal { /// `bun:internal-for-testing`: the error a lookup of `hostname` reports /// when getaddrinfo(3) returns the `EAI_*` status named `code`. pub(crate) fn getaddrinfo_error_for_testing( - global: &JSGlobalObject, - frame: &CallFrame, + cx: &mut JsScope<'_>, + code: &bun::String, + hostname: &bun::String, ) -> JsResult { - let args = frame.arguments(); - if args.len() < 2 || !args[0].is_string() || !args[1].is_string() { - return Err(global.throw_invalid_arguments(format_args!( - "expected (code: string, hostname: string)" - ))); - } - let code = args[0].to_utf8(global)?; - let hostname = args[1].to_utf8(global)?; + let global = cx.global(); + let code = code.to_utf8(); + let hostname = hostname.to_utf8(); let Some(rc) = c_ares::Error::eai_raw_by_name(code.slice()) else { return Err(global.throw_invalid_arguments(format_args!( "unknown getaddrinfo status name: {}", @@ -5754,18 +5738,18 @@ impl Resolver { ) } - // FFI shim emitted by `export_host_fn!` below (JS2Native link name). - pub(crate) fn new_resolver( - global_this: &JSGlobalObject, - callframe: &CallFrame, + pub(crate) fn new_resolver<'a>( + cx: &mut JsScope<'a>, + options: JsLocal<'a>, ) -> JsResult { + let global_this = cx.global(); // SAFETY: bun_vm() returns a live VM pointer for the duration of the call. let resolver = Resolver::init( global_this.bun_vm(), - Some(global_this.bun_vm().context_of_caller(callframe).id()), + Some(global_this.bun_vm().context_of_caller_no_frame().id()), ); - let options = callframe.argument(0); + let options = options.to_js(); if options.is_object() { // SAFETY: `resolver` is the heap allocation from `init`; not yet // wrapped in JS so exclusively owned here. @@ -5895,55 +5879,3 @@ impl Resolver { bun::String::static_(<&'static str>::from(order)) } } - -// ───────── JS host-fn FFI exports ───────── -// The #[host_fn] attribute emits the JSC-ABI shim under the Rust function name; -// re-export each under its JS2Native link name. Mirrors the proc-macro's -// shim body (see `bun_jsc_macros::host_fn`). -macro_rules! export_host_fn { - ($scope:ident :: $f:ident, $name:literal) => { - const _: () = { - #[cfg(all(windows, target_arch = "x86_64"))] - #[unsafe(export_name = $name)] - pub(crate) unsafe extern "sysv64" fn __shim( - g: *mut ::bun_jsc::JSGlobalObject, - f: *mut ::bun_jsc::CallFrame, - ) -> ::bun_jsc::JSValue { - // SAFETY: JSC guarantees both pointers are live for the call. - let (g, f) = unsafe { (&*g, &*f) }; - ::bun_jsc::host_fn::host_fn_result(g, || $scope::$f(g, f)) - } - #[cfg(not(all(windows, target_arch = "x86_64")))] - #[unsafe(export_name = $name)] - pub(crate) unsafe extern "C" fn __shim( - g: *mut ::bun_jsc::JSGlobalObject, - f: *mut ::bun_jsc::CallFrame, - ) -> ::bun_jsc::JSValue { - // SAFETY: JSC guarantees both pointers are live for the call. - let (g, f) = unsafe { (&*g, &*f) }; - ::bun_jsc::host_fn::host_fn_result(g, || $scope::$f(g, f)) - } - }; - }; -} -// JS2Native ($newRustFunction) entry points — see GeneratedJS2Native.h -export_host_fn!( - Resolver::new_resolver, - "JS2Rust___src_runtime_dns_jsc_dns_rs__Resolver_newResolver" -); -export_host_fn!( - internal::seed_cache_for_testing, - "JS2Rust___src_runtime_dns_jsc_dns_rs__internal_seedCacheForTesting" -); -export_host_fn!( - internal::is_localhost_name_for_testing, - "JS2Rust___src_runtime_dns_jsc_dns_rs__internal_isLocalhostNameForTesting" -); -export_host_fn!( - internal::is_all_loopback_of_one_family_for_testing, - "JS2Rust___src_runtime_dns_jsc_dns_rs__internal_isAllLoopbackOfOneFamilyForTesting" -); -export_host_fn!( - internal::getaddrinfo_error_for_testing, - "JS2Rust___src_runtime_dns_jsc_dns_rs__internal_getaddrinfoErrorForTesting" -); diff --git a/src/runtime/dns_jsc/dns_internals.bind.ts b/src/runtime/dns_jsc/dns_internals.bind.ts index 452c9890c5c7..84e0b77d8db1 100644 --- a/src/runtime/dns_jsc/dns_internals.bind.ts +++ b/src/runtime/dns_jsc/dns_internals.bind.ts @@ -50,6 +50,8 @@ export const DNSResolver = b.class({ }, }); +export const newResolver = b.fn({ ...method, args: { options: b.any }, returns: b.any }); + // The undocumented `Bun.dns.*` functions: the same queries on the VM's global resolver. export const getGlobalServers = b.fn(getServers); export const globalResolve = b.fn(resolve); @@ -72,3 +74,28 @@ export const globalLookupService = b.fn({ expected: { address: ["string"], port: ["number"] }, returns: b.any, }); + +// `bun:internal-for-testing` +export const seedCacheForTesting = b.fn({ + ...method, + args: { hostname: b.string, addresses: b.any }, + returns: b.any, + rustPath: "crate::dns_jsc::internal::seed_cache_for_testing", +}); +export const isLocalhostNameForTesting = b.fn({ + args: { hostname: b.string }, + returns: b.bool, + rustPath: "crate::dns_jsc::internal::is_localhost_name_for_testing", +}); +export const isAllLoopbackOfOneFamilyForTesting = b.fn({ + ...method, + args: { addresses: b.any }, + returns: b.bool, + rustPath: "crate::dns_jsc::internal::is_all_loopback_of_one_family_for_testing", +}); +export const getaddrinfoErrorForTesting = b.fn({ + ...method, + args: { code: b.string, hostname: b.string }, + returns: b.any, + rustPath: "crate::dns_jsc::internal::getaddrinfo_error_for_testing", +}); diff --git a/src/runtime/ffi/FFIObject.bind.ts b/src/runtime/ffi/FFIObject.bind.ts new file mode 100644 index 000000000000..9029ec078761 --- /dev/null +++ b/src/runtime/ffi/FFIObject.bind.ts @@ -0,0 +1,9 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::ffi::ffi_object_draft" }); + +/** `(ptr, byteOffset?, byteLength?, deallocatorContext?, deallocator?)`, or with four arguments the last is the deallocator. */ +const view = { args: { ptr: b.any, rest: b.any.variadic }, returns: b.any, capability: "js", throws: true } as const; + +export const toBuffer = b.fn({ ...view, export: "Bun__FFIObject__toBuffer" }); +export const toArrayBuffer = b.fn({ ...view, export: "Bun__FFIObject__toArrayBuffer" }); diff --git a/src/runtime/ffi/FFIObject.rs b/src/runtime/ffi/FFIObject.rs index d2874ad74fdd..0dc746a1ca94 100644 --- a/src/runtime/ffi/FFIObject.rs +++ b/src/runtime/ffi/FFIObject.rs @@ -1,11 +1,11 @@ use core::ffi::c_void; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::bun_string_jsc; use bun_jsc::host_fn::DomCall; -use bun_jsc::{ - self as jsc, ArrayBuffer, CallFrame, JSFunction, JSGlobalObject, JSObject, JSString, JSValue, - JsResult, -}; +use bun_jsc::{self as jsc, ArrayBuffer, JSFunction, JSGlobalObject, JSObject, JSValue, JsResult}; + +use crate::generated_bindgen::{ffi, ffi_object}; /// Reinterpret a user-supplied raw address (from `bun:ffi` JS land) as a /// JSC typed-array bytes deallocator. Centralized so the `usize → fn ptr` @@ -112,8 +112,16 @@ const DOM_CALL: DomCall = DomCall { }; pub(crate) fn to_js(global_object: &JSGlobalObject) -> JSValue { - // Unrolled manually; keep in sync with `FIELDS` below. - let fields = FIELDS(); + let fields: [(&str, jsc::JSHostFn); 8] = [ + ("viewSource", ffi::Bun__FFI__viewSource), + ("dlopen", ffi::Bun__FFI__dlopen), + ("callback", ffi::Bun__FFI__callback), + ("linkSymbols", ffi::Bun__FFI__linkSymbols), + ("toBuffer", ffi_object::Bun__FFIObject__toBuffer), + ("toArrayBuffer", ffi_object::Bun__FFIObject__toArrayBuffer), + ("closeCallback", ffi::Bun__FFI__closeCallback), + ("cfunction", ffi::Bun__FFI__cfunction), + ]; let object = JSValue::create_empty_object(global_object, fields.len() + 3); for &(name, func) in &fields { @@ -611,15 +619,18 @@ fn get_cptr(value: JSValue) -> Option { None } -fn to_array_buffer( - global_this: &JSGlobalObject, - value: JSValue, - byte_offset: Option, - value_length: Option, - finalization_ctx_or_ptr: Option, - finalization_callback: Option, +pub(crate) fn to_array_buffer<'a>( + cx: &mut JsScope<'a>, + value: JsLocal<'a>, + rest: &'a [JsLocal<'a>], ) -> JsResult { - let (ptr, len) = get_ptr_slice(global_this, value, byte_offset, value_length)?; + let global_this = cx.global(); + let mut rest = JsLocal::to_js_slice(rest).iter().copied(); + let byte_offset = rest.next(); + let value_length = rest.next(); + let finalization_ctx_or_ptr = rest.next(); + let finalization_callback = rest.next(); + let (ptr, len) = get_ptr_slice(global_this, value.to_js(), byte_offset, value_length)?; let mut callback: jsc::JSTypedArrayBytesDeallocator = None; let mut ctx: Option<*mut c_void> = None; if let Some(callback_value) = finalization_callback { @@ -667,15 +678,18 @@ fn to_array_buffer( } } -fn to_buffer( - global_this: &JSGlobalObject, - value: JSValue, - byte_offset: Option, - value_length: Option, - finalization_ctx_or_ptr: Option, - finalization_callback: Option, +pub(crate) fn to_buffer<'a>( + cx: &mut JsScope<'a>, + value: JsLocal<'a>, + rest: &'a [JsLocal<'a>], ) -> JsResult { - let (ptr, len) = get_ptr_slice(global_this, value, byte_offset, value_length)?; + let global_this = cx.global(); + let mut rest = JsLocal::to_js_slice(rest).iter().copied(); + let byte_offset = rest.next(); + let value_length = rest.next(); + let finalization_ctx_or_ptr = rest.next(); + let finalization_callback = rest.next(); + let (ptr, len) = get_ptr_slice(global_this, value.to_js(), byte_offset, value_length)?; let mut callback: jsc::JSTypedArrayBytesDeallocator = None; let mut ctx: Option<*mut c_void> = None; if let Some(callback_value) = finalization_callback { @@ -724,166 +738,6 @@ pub(crate) fn getter(global_object: &JSGlobalObject, _: &JSObject) -> JSValue { to_js(global_object) } -// ── `fields` host-fn thunks ────────────────────────────────────────────────── -// The eight wrappers are unrolled manually here; each decodes its `CallFrame` -// arguments into the target's parameter types (only the `*JSGlobalObject` / -// `JSValue` / `Option` / string arms are exercised by this table). - -/// Minimal `ArgumentsSlice::nextEat` — pops the next non-consumed argument. -/// `wrapStaticMethod`'s arena/protect machinery is unused for the FFI fields -/// (no `StringOrBuffer` params, `auto_protect=false`), so a bare cursor over -/// `callframe.arguments()` is semantically identical. -#[inline] -fn next_eat<'a>(iter: &mut core::slice::Iter<'a, JSValue>) -> Option { - iter.next().copied() -} - -/// `wrapStaticMethod` decode arm for required `JSValue`. -#[inline] -fn eat_required( - global: &JSGlobalObject, - iter: &mut core::slice::Iter<'_, JSValue>, -) -> JsResult { - next_eat(iter).ok_or_else(|| global.throw_invalid_arguments(format_args!("Missing argument"))) -} - -/// Decode arm for string arguments. -#[inline] -fn eat_string<'a>( - global: &'a JSGlobalObject, - iter: &mut core::slice::Iter<'_, JSValue>, -) -> JsResult<&'a JSString> { - let string_value = next_eat(iter) - .ok_or_else(|| global.throw_invalid_arguments(format_args!("Missing argument")))?; - if string_value.is_undefined_or_null() { - return Err(global.throw_invalid_arguments(format_args!("Expected string"))); - } - string_value.to_js_string(global) -} - -/// Wrap a `JsHostFnZig` body into the raw `JSHostFn` ABI. Mints a fresh -/// `unsafe extern jsc.conv fn` per call site -/// so the address is usable in the static `FIELDS` table (Rust forbids -/// fn-pointer const generics, so this is a `macro_rules!` rather than a -/// generic fn). Uses `jsc_host_abi!` so the thunk gets `extern "sysv64"` on -/// Windows-x64 and `extern "C"` elsewhere — matching the `JSHostFn` typedef. -macro_rules! wrap_host_fn { - ($body:path) => {{ - bun_jsc::jsc_host_abi! { - unsafe fn thunk( - global: *mut JSGlobalObject, - callframe: *mut CallFrame, - ) -> JSValue { - // SAFETY: JSC guarantees both pointers are live for the host call. - let (global, callframe) = unsafe { (&*global, &*callframe) }; - jsc::to_js_host_fn_result(global, $body(global, callframe)) - } - } - thunk as jsc::JSHostFn - }}; -} - -mod fields { - use super::*; - // `print`/`callback`/`link_symbols`/`close_callback` live on - // `ffi_body::FFI` — not yet hoisted onto the canonical `crate::ffi::FFI`. - // They are static (no `&self`), so type identity is irrelevant; route to - // them directly until the two `FFI` structs merge. - use super::super::ffi_body::FFI as FfiImpl; - - // viewSource → FFI::print(global, JSValue, ?JSValue) -> JsResult - pub(super) fn view_source(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let mut iter = callframe.arguments().iter(); - let object = eat_required(global, &mut iter)?; - let is_callback = next_eat(&mut iter); - FfiImpl::print(global, object, is_callback) - } - - pub(super) fn dlopen(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let mut iter = callframe.arguments().iter(); - let name = eat_string(global, &mut iter)?; - let object = eat_required(global, &mut iter)?; - FfiImpl::open(global, &*name.view(global)?, object) - } - - // callback → FFI::callback(global, JSValue, JSValue) -> JsResult - pub(super) fn callback(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let mut iter = callframe.arguments().iter(); - let interface = eat_required(global, &mut iter)?; - let js_callback = eat_required(global, &mut iter)?; - FfiImpl::callback(global, interface, js_callback) - } - - // linkSymbols → FFI::link_symbols(global, JSValue) -> JSValue - pub(super) fn link_symbols( - global: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let mut iter = callframe.arguments().iter(); - let object = eat_required(global, &mut iter)?; - FfiImpl::link_symbols(global, object) - } - - // toBuffer → to_buffer(global, JSValue, ?JSValue×4) -> JsResult - pub(super) fn to_buffer(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let mut iter = callframe.arguments().iter(); - let value = eat_required(global, &mut iter)?; - let byte_offset = next_eat(&mut iter); - let length = next_eat(&mut iter); - let final_ctx = next_eat(&mut iter); - let final_cb = next_eat(&mut iter); - super::to_buffer(global, value, byte_offset, length, final_ctx, final_cb) - } - - // toArrayBuffer → to_array_buffer(global, JSValue, ?JSValue×4) -> JsResult - pub(super) fn to_array_buffer( - global: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let mut iter = callframe.arguments().iter(); - let value = eat_required(global, &mut iter)?; - let byte_offset = next_eat(&mut iter); - let length = next_eat(&mut iter); - let final_ctx = next_eat(&mut iter); - let final_cb = next_eat(&mut iter); - super::to_array_buffer(global, value, byte_offset, length, final_ctx, final_cb) - } - - pub(super) fn close_jsc_callback( - global: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let mut iter = callframe.arguments().iter(); - let callback = eat_required(global, &mut iter)?; - FfiImpl::close_jsc_callback(global, callback) - } - - pub(super) fn cfunction(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let mut iter = callframe.arguments().iter(); - let options = eat_required(global, &mut iter)?; - let name = next_eat(&mut iter); - FfiImpl::create_cfunction(global, options, name) - } -} - -// Represented here as a const slice of (name, JSHostFn) so `to_js` can iterate. -// Cannot be `const` — `wrap_host_fn!` expands to a block expression -// (item + cast), which const-eval rejects in array-literal position. The slice -// is tiny and only built once in `to_js`, so the runtime cost is nil. -#[allow(non_snake_case)] -fn FIELDS() -> [(&'static str, jsc::JSHostFn); 8] { - [ - ("viewSource", wrap_host_fn!(fields::view_source)), - ("dlopen", wrap_host_fn!(fields::dlopen)), - ("callback", wrap_host_fn!(fields::callback)), - ("linkSymbols", wrap_host_fn!(fields::link_symbols)), - ("toBuffer", wrap_host_fn!(fields::to_buffer)), - ("toArrayBuffer", wrap_host_fn!(fields::to_array_buffer)), - ("closeCallback", wrap_host_fn!(fields::close_jsc_callback)), - ("cfunction", wrap_host_fn!(fields::cfunction)), - ] -} - const MAX_ADDRESSABLE_MEMORY: usize = u56_max(); const fn u56_max() -> usize { diff --git a/src/runtime/ffi/ffi.bind.ts b/src/runtime/ffi/ffi.bind.ts index b42a29bb16ce..530b5729abe3 100644 --- a/src/runtime/ffi/ffi.bind.ts +++ b/src/runtime/ffi/ffi.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::ffi::FFI" }); + export const FFI = b.class({ rustPath: "crate::ffi::FFI", internalFields: ["symbolsValue", "functionsValue"], @@ -8,3 +10,19 @@ export const FFI = b.class({ symbols: { internalField: "symbolsValue" }, }, }); + +const method = { returns: b.any, length: 1, capability: "js", throws: true } as const; + +// The functions of `Bun.FFI`. One that is given bad options returns the error. +export const viewSource = b.fn({ + ...method, + export: "Bun__FFI__viewSource", + args: { symbols: b.any, isCallback: b.bool.loose }, +}); +export const dlopen = b.fn({ ...method, export: "Bun__FFI__dlopen", args: { name: b.any, symbols: b.any } }); +export const callback = b.fn({ ...method, export: "Bun__FFI__callback", args: { options: b.any, callback: b.any } }); +export const linkSymbols = b.fn({ ...method, export: "Bun__FFI__linkSymbols", args: { symbols: b.any } }); +export const closeCallback = b.fn({ export: "Bun__FFI__closeCallback", args: { callback: b.any } }); +export const cfunction = b.fn({ ...method, export: "Bun__FFI__cfunction", args: { options: b.any, name: b.any } }); + +export const cc = b.fn({ ...method, args: { options: b.any } }); diff --git a/src/runtime/ffi/ffi_body.rs b/src/runtime/ffi/ffi_body.rs index 83c0e47202fb..203e260e01df 100644 --- a/src/runtime/ffi/ffi_body.rs +++ b/src/runtime/ffi/ffi_body.rs @@ -13,11 +13,11 @@ use crate::napi; use bun_collections::StringArrayHashMap; use bun_core::{EncodedSlice, ZStr}; use bun_core::{ZBox, env_var, fmt as bun_fmt, zstr}; -use bun_jsc::bindgen::{JsPayload as _, JsScope, JsWrapper}; +use bun_jsc::bindgen::{JsLocal, JsPayload as _, JsScope, JsWrapper}; use bun_jsc::bun_string_jsc; use bun_jsc::{ - self as jsc, CallFrame, EncodedSliceJsc, ErrorCode, JSGlobalObject, JSObject, - JSPropertyIterator, JSValue, JsCell, JsClass, JsError, JsResult, SystemError, + self as jsc, EncodedSliceJsc, ErrorCode, JSGlobalObject, JSObject, JSPropertyIterator, JSValue, + JsCell, JsClass, JsError, JsResult, SystemError, }; #[cfg(target_os = "macos")] use bun_paths as path; @@ -967,12 +967,8 @@ impl StringArray { // ─── FFI host functions ───────────────────────────────────────────────────── impl FFI { - // No `#[bun_jsc::host_fn]` here — the `Free` shim it emits is a bare - // `bun_ffi_cc(__g, __f)` call, which doesn't resolve inside `impl FFI`. - pub(crate) fn bun_ffi_cc( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { + pub(crate) fn cc<'a>(cx: &mut JsScope<'a>, options: JsLocal<'a>) -> JsResult { + let global_this = cx.global(); if !global_this.bun_vm().allow_ffi_cc() { return Err(global_this .err( @@ -988,15 +984,13 @@ impl FFI { "bun:ffi cc() is not available in this build (TinyCC is disabled)" ))); } - let arguments = callframe.arguments(); - if arguments.is_empty() || !arguments[0].is_object() { + let object = options.to_js(); + if !object.is_object() { return Err(global_this.throw_invalid_arguments(format_args!("Expected object"))); } // Step 1. compile the user's code - let object = arguments[0]; - let mut compile_c = CompileC::default(); let symbols_object: JSValue = object @@ -1236,23 +1230,22 @@ impl FFI { Ok(js_object) } - pub(crate) fn close_jsc_callback( - _global_this: &JSGlobalObject, - callback: JSValue, - ) -> JsResult { + pub(crate) fn close_callback(callback: JsLocal<'_>) { unsafe extern "C" { fn Bun__JSCFFICallbackClose(callback: JSValue); } // SAFETY: thin FFI wrapper; the C++ side type-checks the cell (jsDynamicCast) before use. - unsafe { Bun__JSCFFICallbackClose(callback) }; - Ok(JSValue::UNDEFINED) + unsafe { Bun__JSCFFICallbackClose(callback.to_js()) }; } - pub(crate) fn callback( - global_this: &JSGlobalObject, - interface: JSValue, - js_callback: JSValue, + pub(crate) fn callback<'a>( + cx: &mut JsScope<'a>, + interface: JsLocal<'a>, + js_callback: JsLocal<'a>, ) -> JsResult { + let global_this = cx.global(); + let interface = interface.to_js(); + let js_callback = js_callback.to_js(); jsc::mark_binding(); if !interface.is_object() { return Ok(global_this.to_invalid_arguments(format_args!("Expected object"))); @@ -1364,15 +1357,15 @@ impl FFI { bun_string_jsc::create_utf8_for_js(global, text) } - pub(crate) fn print( - global: &JSGlobalObject, - object: JSValue, - is_callback_val: Option, + pub(crate) fn view_source<'a>( + cx: &mut JsScope<'a>, + object: JsLocal<'a>, + is_callback: bool, ) -> JsResult { - if let Some(is_callback) = is_callback_val { - if is_callback.to_boolean() { - return Self::print_callback(global, object); - } + let global = cx.global(); + let object = object.to_js(); + if is_callback { + return Self::print_callback(global, object); } if object.is_empty_or_undefined_or_null() { @@ -1417,11 +1410,18 @@ fn invalid_options_arg(global: &JSGlobalObject) -> JSValue { } impl FFI { - pub(crate) fn open( - global: &JSGlobalObject, - name_str: &bun_core::String, - object_value: JSValue, + pub(crate) fn dlopen<'a>( + cx: &mut JsScope<'a>, + name: JsLocal<'a>, + object_value: JsLocal<'a>, ) -> JsResult { + let global = cx.global(); + let name = name.to_js(); + if name.is_undefined_or_null() { + return Err(global.throw_invalid_arguments(format_args!("Expected string"))); + } + let name_str = name.to_js_string(global)?.view(global)?; + let object_value = object_value.to_js(); jsc::mark_binding(); let vm = jsc::VirtualMachineRef::get(); let name_slice = name_str.to_utf8(); @@ -1590,10 +1590,12 @@ impl FFI { Ok(js_object) } - pub(crate) fn link_symbols( - global: &JSGlobalObject, - object_value: JSValue, + pub(crate) fn link_symbols<'a>( + cx: &mut JsScope<'a>, + object_value: JsLocal<'a>, ) -> JsResult { + let global = cx.global(); + let object_value = object_value.to_js(); jsc::mark_binding(); if object_value.is_empty_or_undefined_or_null() { @@ -1669,11 +1671,14 @@ impl FFI { Ok(js_object) } - pub(crate) fn create_cfunction( - global: &JSGlobalObject, - options: JSValue, - name_value: Option, + pub(crate) fn cfunction<'a>( + cx: &mut JsScope<'a>, + options: JsLocal<'a>, + name_value: JsLocal<'a>, ) -> JsResult { + let global = cx.global(); + let options = options.to_js(); + let name_value = name_value.to_js(); jsc::mark_binding(); if options.is_empty_or_undefined_or_null() || !options.is_object() { @@ -1691,9 +1696,10 @@ impl FFI { ))); }; - let name = match name_value { - Some(value) if value.is_string() => value.to_bun_string(global)?, - _ => bun_core::String::static_("CFunction"), + let name = if name_value.is_string() { + name_value.to_bun_string(global)? + } else { + bun_core::String::static_("CFunction") }; if let Some(err) = function.reject_napi_types_error(global) { return Ok(err); @@ -2628,16 +2634,6 @@ static WORKAROUND: MyFunctionSStructWorkAround = MyFunctionSStructWorkAround { uint64_to_jsvalue: exposed_to_ffi::UINT64_TO_JSVALUE, }; -// ─── exports ──────────────────────────────────────────────────────────────── - -/// `Bun__FFI__cc` — module-level re-export of `FFI::bun_ffi_cc`, so the -/// `js2native` codegen can resolve it as `crate::ffi::ffi::bun__ffi__cc`. -#[allow(non_snake_case)] -#[inline] -pub(crate) fn bun__ffi__cc(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - FFI::bun_ffi_cc(global, callframe) -} - fn make_napi_env_if_needed<'a>( functions: impl IntoIterator, global_this: &JSGlobalObject, diff --git a/src/runtime/ffi/mod.rs b/src/runtime/ffi/mod.rs index d84b6f35981e..5028e397e92b 100644 --- a/src/runtime/ffi/mod.rs +++ b/src/runtime/ffi/mod.rs @@ -11,15 +11,6 @@ #[path = "ffi_body.rs"] mod ffi_body; -/// `js2native` codegen resolves `$rust(ffi.rs, Bun__FFI__cc)` to -/// `crate::ffi::ffi::bun__ffi__cc`; the module name maps the `.rs` basename. -/// `FFI::bun_ffi_cc` lives in `ffi_body` — re-export it under -/// the codegen-expected path so the dispatch table links without forcing the -/// generator to special-case `ffi/ffi.rs`. -pub(crate) mod ffi { - pub(crate) use super::ffi_body::bun__ffi__cc; -} - #[path = "FFIObject.rs"] pub(crate) mod ffi_object_draft; diff --git a/src/runtime/generated_host_exports.rs b/src/runtime/generated_host_exports.rs index 2b16456b985a..8071b09b5f0f 100644 --- a/src/runtime/generated_host_exports.rs +++ b/src/runtime/generated_host_exports.rs @@ -9,14 +9,6 @@ //! generated thunks call back into `crate::…` / `bun_jsc::…` paths via //! `host_fn::*`, so a missing impl is a hard compile error here, not a runtime //! `unimplemented!()`. -//! -//! Why a code generator instead of the `#[bun_jsc::host_fn(export = "…")]` -//! proc-macro: the proc-macro emits the `#[no_mangle]` shim *inline* next to -//! every impl, scattering ~425 unmangled symbols across 80+ files. Centralising -//! them here means (a) one place to audit the C-ABI surface, (b) the source -//! files contain zero raw-pointer-deref boilerplate, (c) win-x64 -//! `extern "sysv64"` cfg-splitting is done once instead of duplicated per -//! macro expansion. #![allow(non_snake_case, non_camel_case_types, non_upper_case_globals, // Thunks pass `*mut ` where C++ stores it as `void*` and never // derefs, so its field layout is not part of the ABI. improper_ctypes, improper_ctypes_definitions, clippy::missing_safety_doc, clippy::not_unsafe_ptr_arg_deref, diff --git a/src/runtime/generated_js2native.rs b/src/runtime/generated_js2native.rs deleted file mode 100644 index 785ce9719e41..000000000000 --- a/src/runtime/generated_js2native.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Hook for the codegen-emitted JS2Native Rust thunks. -//! -//! `src/codegen/generate-js2native.ts::getJS2NativeRust()` (driven by -//! `bundle-modules.ts`) writes `${BUN_CODEGEN_DIR}/generated_js2native.rs`; -//! this module `include!`s it so the `#[unsafe(no_mangle)] extern "C"` -//! `JS2Rust__*` symbols land in `bun_runtime` and satisfy the externs declared -//! by `GeneratedJS2Native.h` (the JS-module → native dispatch table). -//! -//! Each thunk calls the Rust function directly; a missing function is a -//! compile error in `cargo check -p bun_runtime`. -#![allow( - non_snake_case, - non_camel_case_types, - non_upper_case_globals, - clippy::missing_safety_doc, - clippy::not_unsafe_ptr_arg_deref, - // Closure form is intentional for `&mut T → &T` autoref/coercion. - clippy::redundant_closure, - clippy::unused_unit -)] - -include!(concat!(env!("BUN_CODEGEN_DIR"), "/generated_js2native.rs")); diff --git a/src/runtime/hw_exports.bind.ts b/src/runtime/hw_exports.bind.ts new file mode 100644 index 000000000000..6d4235d3d443 --- /dev/null +++ b/src/runtime/hw_exports.bind.ts @@ -0,0 +1,10 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::hw_exports" }); + +export const drainMicrotasks = b.fn({ args: {}, capability: "js" }); + +/** The reactions to the promise that `bun --print` evaluated to. Both exit the process. */ +const reaction = { args: { value: b.any }, capability: "js" } as const; +export const onResolveEntryPointResult = b.fn({ ...reaction, export: "Bun__onResolveEntryPointResult" }); +export const onRejectEntryPointResult = b.fn({ ...reaction, export: "Bun__onRejectEntryPointResult" }); diff --git a/src/runtime/hw_exports.rs b/src/runtime/hw_exports.rs index f8ac3dc2c031..c9d89df7f0aa 100644 --- a/src/runtime/hw_exports.rs +++ b/src/runtime/hw_exports.rs @@ -6,26 +6,20 @@ //! Symbols that already have a Rust `#[export_name]` / `#[no_mangle]` elsewhere //! are intentionally **not** re-declared here to avoid duplicate-symbol link //! errors: -//! - `Bun__getTLSDefaultCiphers` / `Bun__setTLSDefaultCiphers` //! - `Bun__Process__getStdinFdType` //! → `src/jsc/rare_data.rs` -//! - `Resolver__nodeModulePathsForJS` / `Resolver__nodeModulePathsJSValue` +//! - `Resolver__nodeModulePathsJSValue` //! → `src/jsc/resolver_jsc.rs` //! - `Zig__GlobalObject__getBodyStreamOrBytesForWasmStreaming` //! → `src/runtime/webcore/wasm_streaming.rs` //! - `Bun__Chrome__autoDetect` / `Bun__Chrome__ensure` //! → `src/runtime/webview/ChromeProcess.rs` -//! - `Bun__JSSourceMap__find` -//! → `src/runtime/api/JSSourceMap.rs` -//! - `Bun__Process__send` -//! → `bun_jsc::virtual_machine_exports` use core::ffi::c_void; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{ - CallFrame, JSGlobalObject, JSInternalPromise, JSValue, StringJsc as _, ZigStackFrame, -}; +use bun_jsc::{JSGlobalObject, JSInternalPromise, JSValue, StringJsc as _, ZigStackFrame}; // ─── VirtualMachine ────────────────────────────────────────────────────────── // @@ -40,15 +34,8 @@ pub(crate) fn is_main_thread_vm() -> bool { VirtualMachine::get().as_mut().is_main_thread } -/// `export fn Bun__drainMicrotasksFromJS(global, callframe) callconv(jsc.conv) JSValue` -/// -/// Returns plain `JSValue` (not `JsResult`) so the generated thunk is a bare -/// deref+call with no `ExceptionValidationScope`. -/// `drain_microtasks()` runs arbitrary microtasks; wrapping in a -/// scope would trip `assert_exception_presence_matches(false)` if one left an -/// exception pending while we return `UNDEFINED`. -// HOST_EXPORT(Bun__drainMicrotasksFromJS) -pub(crate) fn drain_microtasks_from_js(global: &JSGlobalObject, _cf: &CallFrame) -> JSValue { +pub(crate) fn drain_microtasks(cx: &mut JsScope<'_>) { + let global = cx.global(); // Hot path (~2×/request via cork callback chain): pass the incoming // `global` straight through instead of re-deriving it via // TLS→vm→event_loop→vm→global (4 dependent loads — perf root-cause #1). @@ -59,7 +46,6 @@ pub(crate) fn drain_microtasks_from_js(global: &JSGlobalObject, _cf: &CallFrame) let _ = vm .event_loop_mut() .drain_microtasks_with_global(global, jsc_vm); - JSValue::UNDEFINED } /// `export fn Bun__logUnhandledException(exception: JSValue) void { get().runErrorHandler(exception, null); }` @@ -292,12 +278,9 @@ mod sql_hooks { // ─── entry-point promise reactions (used by `--print`) ─────────────────────── -// HOST_EXPORT(Bun__onResolveEntryPointResult) -pub(crate) fn on_resolve_entry_point_result( - global: &JSGlobalObject, - callframe: &CallFrame, -) -> bun_jsc::JsResult { - let result = callframe.argument(0); +pub(crate) fn on_resolve_entry_point_result(cx: &mut JsScope<'_>, result: JsLocal<'_>) { + let global = cx.global(); + let result = result.to_js(); // SAFETY: `vals[..len]` is the single stack `result`; `ctype` is ignored by // `message_with_type_and_level` (it always resolves the per-VM console via // `vm_console(global)`), so null is fine. @@ -315,12 +298,9 @@ pub(crate) fn on_resolve_entry_point_result( bun_core::Global::exit(u32::from(global.bun_vm().as_mut().exit_handler.exit_code)); } -// HOST_EXPORT(Bun__onRejectEntryPointResult) -pub(crate) fn on_reject_entry_point_result( - global: &JSGlobalObject, - callframe: &CallFrame, -) -> bun_jsc::JsResult { - let result = callframe.argument(0); +pub(crate) fn on_reject_entry_point_result(cx: &mut JsScope<'_>, result: JsLocal<'_>) { + let global = cx.global(); + let result = result.to_js(); // SAFETY: `vals[..len]` is the single stack `result`; `ctype` is ignored by // `message_with_type_and_level` (it always resolves the per-VM console via // `vm_console(global)`), so null is fine. @@ -340,6 +320,3 @@ pub(crate) fn on_reject_entry_point_result( // `Bun__Chrome__autoDetect` / `Bun__Chrome__ensure` — exported from // `crate::webview::chrome_process` (mod webview is declared in lib.rs). -// -// `Bun__JSSourceMap__find` — exported from `crate::api::js_source_map` -// via `#[bun_jsc::host_fn(export = ...)]`. diff --git a/src/runtime/ipc.bind.ts b/src/runtime/ipc.bind.ts new file mode 100644 index 000000000000..b845463105c4 --- /dev/null +++ b/src/runtime/ipc.bind.ts @@ -0,0 +1,10 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::ipc" }); + +export const closeSentHandle = b.fn({ + export: "Bun__IPC__closeSentHandle", + args: { handle: b.any }, + capability: "js", + throws: true, +}); diff --git a/src/runtime/ipc.rs b/src/runtime/ipc.rs index 088e44bcd5b3..6a68458775fe 100644 --- a/src/runtime/ipc.rs +++ b/src/runtime/ipc.rs @@ -12,6 +12,7 @@ use bun_core::{String as BunString, strings}; use bun_io::KeepAlive; use bun_io::StreamBuffer; use bun_jsc as jsc; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::js_value::Protected; #[cfg(windows)] use bun_jsc::virtual_machine::VirtualMachine; @@ -828,9 +829,9 @@ impl SendHandle { } } -#[bun_jsc::host_fn] -fn close_sent_handle(global: &JSGlobalObject, callframe: &jsc::CallFrame) -> JsResult { - let [js] = callframe.arguments_as_array::<1>(); +pub(crate) fn close_sent_handle<'a>(cx: &mut JsScope<'a>, handle: JsLocal<'a>) -> JsResult<()> { + let global = cx.global(); + let js = handle.to_js(); if js.is_object() { if let Some(f) = js.get(global, "close")? { if f.is_callable() { @@ -838,14 +839,14 @@ fn close_sent_handle(global: &JSGlobalObject, callframe: &jsc::CallFrame) -> JsR } } } - Ok(JSValue::UNDEFINED) + Ok(()) } fn close_sent_handle_fn(global: &JSGlobalObject) -> JSValue { jsc::JSFunction::create( global, "", - __jsc_host_close_sent_handle, + crate::generated_bindgen::ipc::Bun__IPC__closeSentHandle, 1, Default::default(), ) diff --git a/src/runtime/ipc_host.bind.ts b/src/runtime/ipc_host.bind.ts new file mode 100644 index 000000000000..9b82ad148369 --- /dev/null +++ b/src/runtime/ipc_host.bind.ts @@ -0,0 +1,24 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::ipc_host" }); + +export const emitProcessErrorEvent = b.fn({ + export: "Bun__IPC__emitProcessErrorEvent", + args: { error: b.any }, + capability: "js", +}); + +export const emitHandleIPCMessage = b.fn({ + args: { target: b.any, message: b.any, handle: b.any }, + capability: "js", + throws: true, +}); + +/** `process.send()` */ +export const send = b.fn({ + args: { message: b.any, handle: b.any, options: b.any, callback: b.any }, + returns: b.bool, + length: 1, + capability: "js", + throws: true, +}); diff --git a/src/runtime/ipc_host.rs b/src/runtime/ipc_host.rs index 08f7b7545c5d..d2a2c6b0056d 100644 --- a/src/runtime/ipc_host.rs +++ b/src/runtime/ipc_host.rs @@ -6,9 +6,10 @@ use crate::ipc::{ self as IPC, DecodedIPCMessage, Handle, IsInternal, SendQueue, SerializeAndSendResult, }; +use bun_jsc::bindgen::{JsLocal, JsScope}; #[cfg(windows)] use bun_jsc::bun_string_jsc; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsClass, JsResult}; +use bun_jsc::{JSGlobalObject, JSValue, JsClass, JsResult}; use bun_ptr::RefPtr; use crate::api::bun::subprocess::Subprocess; @@ -49,14 +50,8 @@ pub(crate) fn attach_windows_socket_payload( Ok(Some(hex)) } -#[bun_jsc::host_fn] -fn emit_process_error_event( - global_this: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let [ex] = callframe.arguments_as_array::<1>(); - Process__emitErrorEvent(global_this, ex); - Ok(JSValue::UNDEFINED) +pub(crate) fn emit_process_error_event<'a>(cx: &mut JsScope<'a>, error: JsLocal<'a>) { + Process__emitErrorEvent(cx.global(), error.to_js()); } fn do_send_err( @@ -73,9 +68,7 @@ fn do_send_err( let target = bun_jsc::JSFunction::create( global_object, "", - // `#[bun_jsc::host_fn]` emits the C-ABI shim under this name; the - // safe `emit_process_error_event` is `JSHostFnZig`, not `JSHostFn`. - __jsc_host_emit_process_error_event, + crate::generated_bindgen::ipc_host::Bun__IPC__emitProcessErrorEvent, 1, Default::default(), ); @@ -301,12 +294,14 @@ pub(crate) fn do_send( Ok(status == SerializeAndSendResult::Success) } -#[bun_jsc::host_fn] -pub(crate) fn emit_handle_ipc_message( - global_this: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let [target, message, handle] = callframe.arguments_as_array::<3>(); +pub(crate) fn emit_handle_ipc_message<'a>( + cx: &mut JsScope<'a>, + target: JsLocal<'a>, + message: JsLocal<'a>, + handle: JsLocal<'a>, +) -> JsResult<()> { + let global_this = cx.global(); + let [target, message, handle] = [target.to_js(), message.to_js(), handle.to_js()]; if target.is_null() { // Cluster-internal replies that carried a descriptor (shared dgram // sockets) are marked with cmd: "NODE_CLUSTER"; hand them straight to @@ -322,7 +317,7 @@ pub(crate) fn emit_handle_ipc_message( message, handle, )?; - return Ok(JSValue::UNDEFINED); + return Ok(()); } } } @@ -331,34 +326,33 @@ pub(crate) fn emit_handle_ipc_message( let Some(ipc) = get_ipc_instance(vm) else { // Channel already gone: a handle that finished adopting after EOF is still delivered, as in node. Process__emitMessageEvent(global_this, message, handle); - return Ok(JSValue::UNDEFINED); + return Ok(()); }; // SAFETY: `get_ipc_instance` returns the live boxed IPCInstance. unsafe { (*ipc).handle_ipc_message(&DecodedIPCMessage::Data(message), handle) }?; } else { if !target.is_cell() { - return Ok(JSValue::UNDEFINED); + return Ok(()); } let Some(subprocess) = Subprocess::from_js_direct(target) else { - return Ok(JSValue::UNDEFINED); + return Ok(()); }; // SAFETY: `from_js_direct` returned a non-null `*mut Subprocess`; the JS // wrapper holds it alive for the call. unsafe { (*subprocess).handle_ipc_message(&DecodedIPCMessage::Data(message), handle) }?; } - Ok(JSValue::UNDEFINED) + Ok(()) } -// The #[bun_jsc::host_fn] attribute emits the jsc-callconv shim and the -// `Bun__Process__send` export. -// -// LAYERING: lives here (not in `bun_jsc::virtual_machine_exports`) because the -// body — via `do_send` — names `Listener` (`bun_runtime`). The export is a -// link-time `#[no_mangle]` symbol, so the defining crate does not matter to -// the C++ caller. -#[bun_jsc::host_fn(export = "Bun__Process__send")] -fn Bun__Process__send(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn send<'a>( + cx: &mut JsScope<'a>, + message: JsLocal<'a>, + handle: JsLocal<'a>, + options: JsLocal<'a>, + callback: JsLocal<'a>, +) -> JsResult { bun_jsc::mark_binding!(); + let global = cx.global(); let vm = global.bun_vm().as_mut(); // SAFETY: `get_ipc_instance` returns the live boxed `IPCInstance` (or // `None`); the instance is heap-allocated, not embedded in `vm`. @@ -378,11 +372,15 @@ fn Bun__Process__send(global: &JSGlobalObject, frame: &CallFrame) -> JsResult(), + [ + message.to_js(), + handle.to_js(), + options.to_js(), + callback.to_js(), + ], FromEnum::Process, peer_pid, ) - .map(JSValue::from) } // `JSGlobalObject` is an opaque `UnsafeCell`-backed ZST handle, so diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 50f85fd56aa3..0cbef912d979 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -4622,7 +4622,7 @@ pub(crate) extern "C" fn Bun__transpileVirtualModule( /// Materialise an embedded file (`.node`/`.so`/`.dylib`/`.dll` from /// `bun build --compile`) to an on-disk path `dlopen(2)` can open. Called /// from `resolve_embedded_node_file_hook` (`process.dlopen()`) and -/// `ffi_body::FFI::open` (`bun:ffi`). +/// `ffi_body::FFI::dlopen` (`bun:ffi`). /// /// The filename is a hash of the contents, so every `dlopen()` of the same /// embedded library — across calls, Worker VMs, and restarts — shares one diff --git a/src/runtime/lib.rs b/src/runtime/lib.rs index 9d1eb0984875..577b12ea74dd 100644 --- a/src/runtime/lib.rs +++ b/src/runtime/lib.rs @@ -66,9 +66,10 @@ pub(crate) mod napi; pub(crate) mod run_main; pub(crate) mod timer; +// In the crate graph for the functions generated C++ calls. +use {bun_css_jsc as _, bun_install_jsc as _, bun_patch_jsc as _, bun_semver_jsc as _}; pub(crate) mod generated_bindgen; // include!()s ${BUN_CODEGEN_DIR}/generated_bindgen_bun_runtime.rs pub(crate) mod generated_host_exports; // include!()s ${BUN_CODEGEN_DIR}/generated_host_exports.rs -pub(crate) mod generated_js2native; // include!()s ${BUN_CODEGEN_DIR}/generated_js2native.rs pub(crate) mod generated_jssink; // include!()s ${BUN_CODEGEN_DIR}/generated_jssink.rs pub(crate) mod dns_jsc; diff --git a/src/runtime/linear_fifo_testing.bind.ts b/src/runtime/linear_fifo_testing.bind.ts new file mode 100644 index 000000000000..04ba75e92193 --- /dev/null +++ b/src/runtime/linear_fifo_testing.bind.ts @@ -0,0 +1,5 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::linear_fifo_testing" }); + +export const orderedRemoveProbe = b.fn({ args: { scenario: b.any }, returns: b.array(b.i32) }); diff --git a/src/runtime/linear_fifo_testing.rs b/src/runtime/linear_fifo_testing.rs index 808b2ea296f3..2fa7cd522da5 100644 --- a/src/runtime/linear_fifo_testing.rs +++ b/src/runtime/linear_fifo_testing.rs @@ -11,12 +11,10 @@ //! //! Lives in `bun_runtime` (not `bun_collections`) because it needs the JSC //! types; `bun_runtime` already depends on both `bun_collections` and -//! `bun_jsc`. Registered via `$newRustFunction("collections/linear_fifo.rs", -//! "TestingAPIs.orderedRemoveProbe", 1)` — the path is only the codegen key; -//! the implementation is this Rust function (see `dispatch_js2native.rs`). +//! `bun_jsc`. use bun_collections::linear_fifo::{LinearFifo, StaticBuffer}; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use bun_jsc::bindgen::JsLocal; /// A 16-slot static buffer matches the real `weak_refs` FIFO in the dev /// server's source-map store and makes `POWERS_OF_TWO` true. @@ -32,11 +30,8 @@ type ProbeFifo = LinearFifo>; /// write 12, read 12, write 8 → head=12 count=8, remove offset 5. /// /// Any other scenario value returns an empty array. -pub(crate) fn ordered_remove_probe( - global: &JSGlobalObject, - frame: &CallFrame, -) -> JsResult { - let scenario = frame.argument(0).to_int32(); +pub(crate) fn ordered_remove_probe(scenario: JsLocal<'_>) -> impl Iterator { + let scenario = scenario.to_js().to_int32(); let mut fifo = ProbeFifo::init(); match scenario { @@ -67,14 +62,5 @@ pub(crate) fn ordered_remove_probe( _ => {} } - let len = fifo.readable_length(); - let array = JSValue::create_empty_array(global, len)?; - for i in 0..len { - array.put_index( - global, - i as u32, - JSValue::js_number_from_int32(fifo.peek_item(i)), - )?; - } - Ok(array) + (0..fifo.readable_length()).map(move |i| fifo.peek_item(i)) } diff --git a/src/runtime/napi/napi_body.bind.ts b/src/runtime/napi/napi_body.bind.ts new file mode 100644 index 000000000000..d8e0f7551fe7 --- /dev/null +++ b/src/runtime/napi/napi_body.bind.ts @@ -0,0 +1,5 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::napi::napi_body" }); + +export const threadsafeFunctionLiveCount = b.fn({ returns: b.usize }); diff --git a/src/runtime/napi/napi_body.rs b/src/runtime/napi/napi_body.rs index 73ba691b6165..6f4b5a48ad2b 100644 --- a/src/runtime/napi/napi_body.rs +++ b/src/runtime/napi/napi_body.rs @@ -14,8 +14,8 @@ use bun_jsc::bun_string_jsc; use bun_jsc::event_loop::{ConcurrentTaskItem as ConcurrentTask, EventLoop}; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{ - self as jsc, CallFrame, Debugger, GlobalRef, JSGlobalObject, JSPromiseStrong, JSValue, - JsResult, StrongOptional, Task, + self as jsc, Debugger, GlobalRef, JSGlobalObject, JSPromiseStrong, JSValue, JsResult, + StrongOptional, Task, }; use bun_threading::Condition as Condvar; use bun_threading::Mutex; @@ -366,7 +366,7 @@ impl Drop for NapiHandleScopeGuard<'_> { type napi_handle_scope = *mut NapiHandleScope; type napi_escapable_handle_scope = *mut NapiHandleScope; -pub(super) type napi_callback_info = *mut CallFrame; +pub(super) type napi_callback_info = *mut c_void; /// What `napi_create_promise` hands the addon. pub(crate) struct Deferred { promise: JSPromiseStrong, @@ -2680,14 +2680,8 @@ static THREADSAFE_FUNCTION_LIVE_COUNT: AtomicUsize = AtomicUsize::new(0); /// Exposed via `bun:internal-for-testing` so tests can assert a threadsafe /// function orphaned by a dead worker is freed rather than leaked. -#[bun_jsc::host_fn] -pub(crate) fn js_threadsafe_function_live_count( - _global: &JSGlobalObject, - _callframe: &CallFrame, -) -> JsResult { - Ok(JSValue::js_number( - THREADSAFE_FUNCTION_LIVE_COUNT.load(Ordering::SeqCst) as f64, - )) +pub(crate) fn threadsafe_function_live_count() -> usize { + THREADSAFE_FUNCTION_LIVE_COUNT.load(Ordering::SeqCst) } impl Drop for ThreadSafeFunction { diff --git a/src/runtime/node.rs b/src/runtime/node.rs index 3635c011321c..1db18e9f64c2 100644 --- a/src/runtime/node.rs +++ b/src/runtime/node.rs @@ -31,8 +31,6 @@ pub(crate) mod process; #[path = "node/node_crypto_binding.rs"] pub(crate) mod crypto; -// codegen (`generated_js2native.rs`) addresses this by its file-stem name. -pub(crate) use crypto as node_crypto_binding; #[cfg(target_os = "macos")] #[path = "node/fs_events.rs"] diff --git a/src/runtime/node/Stat.bind.ts b/src/runtime/node/Stat.bind.ts new file mode 100644 index 000000000000..a2b70784e1d1 --- /dev/null +++ b/src/runtime/node/Stat.bind.ts @@ -0,0 +1,10 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::node::stat" }); + +export const createStatsForIno = b.fn({ + args: { ino: b.any, big: b.bool.loose }, + returns: b.any, + capability: "js", + throws: true, +}); diff --git a/src/runtime/node/Stat.rs b/src/runtime/node/Stat.rs index e5b9e1d66750..fd102bc82b2c 100644 --- a/src/runtime/node/Stat.rs +++ b/src/runtime/node/Stat.rs @@ -1,7 +1,8 @@ //! Stats and BigIntStats classes from node:fs use bun_core::Timespec; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult}; // `bun.sys.PosixStat` — uv-shaped stat struct. Re-exported from `bun_sys` now // that the crate declares it; `PosixStat::init(&bun_sys::Stat)` handles the @@ -171,16 +172,15 @@ pub(crate) type StatsBig = StatType; /// Test-only: build a Stats/BigIntStats from a raw u64 ino via the real /// statToJS path, so regression tests can exercise high-inode values without /// a filesystem that hands them out. -#[bun_jsc::host_fn] -pub(crate) fn create_stats_for_ino( - global: &JSGlobalObject, - frame: &CallFrame, +pub(crate) fn create_stats_for_ino<'a>( + cx: &mut JsScope<'a>, + ino: JsLocal<'a>, + big: bool, ) -> JsResult { - let [ino_arg, big_arg] = frame.arguments_as_array::<2>(); // SAFETY: all-zero is a valid PosixStat (repr(C) POD with no NonNull/NonZero fields). let mut stat_: PosixStat = bun_core::ffi::zeroed(); - stat_.ino = ino_arg.to_uint64_no_truncate(); - Stats::init(&stat_, big_arg.to_boolean()).to_js_newly_created(global) + stat_.ino = ino.to_js().to_uint64_no_truncate(); + Stats::init(&stat_, big).to_js_newly_created(cx.global()) } /// Union between `Stats` and `BigIntStats` where the type can be decided at runtime diff --git a/src/runtime/node/memory_pressure.bind.ts b/src/runtime/node/memory_pressure.bind.ts new file mode 100644 index 000000000000..c9ca93d0596d --- /dev/null +++ b/src/runtime/node/memory_pressure.bind.ts @@ -0,0 +1,12 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::node::memory_pressure" }); + +export const psiTrigger = b.fn({ returns: b.bufferSource.nullable, capability: "heap", throws: true }); +export const psiFilter = b.fn({ + args: { armed: b.any, polls: b.any.variadic }, + returns: b.any, + capability: "js", + throws: true, +}); +export const watcherHasOsBackend = b.fn({ returns: b.bool, capability: "heap" }); diff --git a/src/runtime/node/memory_pressure.rs b/src/runtime/node/memory_pressure.rs index f5c89fdd6c19..36a9ec1f19d0 100644 --- a/src/runtime/node/memory_pressure.rs +++ b/src/runtime/node/memory_pressure.rs @@ -26,11 +26,10 @@ //! are wired. The watcher does not keep the event loop alive. use bun_event_loop::ConcurrentTask::{Task, task_tag}; -#[cfg(any(target_os = "linux", target_os = "android"))] -use bun_jsc::ArrayBuffer; +use bun_jsc::bindgen::{self, BufferSource, JsHeap, JsLocal, JsScope}; #[cfg(not(windows))] use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult}; #[cfg(not(windows))] use core::ptr::NonNull; @@ -523,33 +522,33 @@ pub(crate) extern "C" fn Bun__MemoryPressure__isInstalled(global: &JSGlobalObjec /// `memoryPressurePsiTrigger()`: the bytes `open_psi_fd` writes, as a /// `Buffer`. `null` where there is no PSI backend. -#[bun_jsc::host_fn] -pub(crate) fn js_psi_trigger(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { +pub(crate) fn psi_trigger<'a>( + heap: &JsHeap<'a>, +) -> bindgen::Result<'a, Option>> { #[cfg(any(target_os = "linux", target_os = "android"))] { - ArrayBuffer::create_buffer(global, posix::PSI_TRIGGER) + heap.create_buffer(posix::PSI_TRIGGER).map(Some) } #[cfg(not(any(target_os = "linux", target_os = "android")))] { - let _ = global; - Ok(JSValue::NULL) + let _ = heap; + Ok(None) } } /// `memoryPressurePsiFilter(armed, ...polls)`: one `bool` per poll, `null` off Linux. -#[bun_jsc::host_fn] -pub(crate) fn js_psi_filter(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn psi_filter<'a>( + cx: &mut JsScope<'a>, + armed: JsLocal<'a>, + polls: &'a [JsLocal<'a>], +) -> JsResult { #[cfg(any(target_os = "linux", target_os = "android"))] { - let args = frame.arguments(); - let Some((armed, polls)) = args.split_first() else { - return Err(global.throw_invalid_arguments(format_args!( - "memoryPressurePsiFilter() expects the file contents at arm time" - ))); - }; - let mut last_total = posix::parse_psi_some_total(&armed.to_utf8(global)?).unwrap_or(0); + let global = cx.global(); + let mut last_total = + posix::parse_psi_some_total(&armed.to_js().to_utf8(global)?).unwrap_or(0); JSValue::create_array_from_iter(global, polls.iter(), |contents| { - let contents = contents.to_utf8(global)?; + let contents = contents.to_js().to_utf8(global)?; Ok(JSValue::js_boolean(posix::psi_growth_reached_threshold( &mut last_total, &contents, @@ -558,7 +557,7 @@ pub(crate) fn js_psi_filter(global: &JSGlobalObject, frame: &CallFrame) -> JsRes } #[cfg(not(any(target_os = "linux", target_os = "android")))] { - let _ = (global, frame); + let _ = (cx, armed, polls); Ok(JSValue::NULL) } } @@ -566,16 +565,17 @@ pub(crate) fn js_psi_filter(global: &JSGlobalObject, frame: &CallFrame) -> JsRes /// `memoryPressureWatcherHasOsBackend()`: whether the watcher installed by /// the current listeners registered an OS source. On Windows `install` either /// starts the thread or leaves the slot empty, so this equals `isInstalled`. -#[bun_jsc::host_fn] -pub(crate) fn js_watcher_has_os_backend( - global: &JSGlobalObject, - _frame: &CallFrame, -) -> JsResult { +pub(crate) fn watcher_has_os_backend(heap: &JsHeap<'_>) -> bool { + // SAFETY: reads the watcher's slot in the VM. + let global = unsafe { heap.global() }; #[cfg(not(windows))] - let armed = posix::has_os_backend(global); + { + posix::has_os_backend(global) + } #[cfg(windows)] - let armed = Bun__MemoryPressure__isInstalled(global); - Ok(JSValue::js_boolean(armed)) + { + Bun__MemoryPressure__isInstalled(global) + } } #[cfg(not(windows))] diff --git a/src/runtime/node/node_cluster_binding.bind.ts b/src/runtime/node/node_cluster_binding.bind.ts index 198570fa9ce6..d812ae184de8 100644 --- a/src/runtime/node/node_cluster_binding.bind.ts +++ b/src/runtime/node/node_cluster_binding.bind.ts @@ -10,11 +10,24 @@ export const onInternalMessageChild = b.fn({ }); export const setRef = b.fn({ args: { enabled: b.bool } }); export const channelIgnoreOneDisconnectEventListener = b.fn({}); +export const clusterRawBind = b.fn({ + args: { addressType: b.any, address: b.any, port: b.any, flags: b.any }, + returns: b.any, + capability: "js", + throws: true, +}); export const clusterValidateFd = b.fn({ args: { fd: b.f64, wantDgram: b.bool }, returns: b.i32 }); export const clusterCloseHandle = b.fn({ args: { fd: b.f64 } }); /** `process[kHandle]` of a `cluster.Worker`, which takes any object as its process. */ export const ProcessHandle = b.union({ subprocess: Subprocess, ignored: b.any }); +/** `null` when nothing was sent, `false` when the channel asks the sender to back off. */ +export const sendHelperPrimary = b.fn({ + args: { processHandle: ProcessHandle, message: b.any, handle: b.any, callback: b.any }, + returns: b.bool.nullable, + capability: "js", + throws: true, +}); export const onInternalMessagePrimary = b.fn({ args: { handle: ProcessHandle, worker: b.any, callback: b.any }, capability: "js", diff --git a/src/runtime/node/node_cluster_binding.rs b/src/runtime/node/node_cluster_binding.rs index 0fff06cb24f8..d715f6d0ab4d 100644 --- a/src/runtime/node/node_cluster_binding.rs +++ b/src/runtime/node/node_cluster_binding.rs @@ -7,7 +7,7 @@ use crate::ipc::{IsInternal, SerializeAndSendResult}; use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, StrongOptional}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult, StrongOptional}; use crate::api::bun::subprocess::Subprocess; use crate::generated_bindgen::node_cluster_binding::ProcessHandle; @@ -66,25 +66,28 @@ pub(crate) fn handle_internal_message_child( child_singleton().dispatch(message, handle, global) } -#[bun_jsc::host_fn] -pub(crate) fn send_helper_primary(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn send_helper_primary<'a>( + cx: &mut JsScope<'a>, + process_handle: ProcessHandle<'a>, + message: JsLocal<'a>, + handle: JsLocal<'a>, + callback: JsLocal<'a>, +) -> JsResult> { bun_output::scoped_log!(IPC, "sendHelperPrimary"); - let arguments = frame.arguments_as_array::<4>(); - // `as_class_ref` is the safe shared-borrow downcast (centralised deref - // proof in `JSValue`); `Subprocess::ipc(&self)` projects the `JsCell`. // `cluster.Worker({ process })` accepts any object, so `process[kHandle]` // is `undefined` unless `cluster.fork()` created the process; Node's // `sendHelper` returns false for a worker with no IPC channel. - let Some(subprocess) = arguments[0].as_class_ref::>() else { - return Ok(JSValue::NULL); + let ProcessHandle::Subprocess(subprocess) = process_handle else { + return Ok(None); }; - let message = arguments[1]; - let handle = arguments[2]; - let callback = arguments[3]; + let global = cx.global(); + let message = message.to_js(); + let handle = handle.to_js(); + let callback = callback.to_js(); let Some(ipc_data) = subprocess.ipc() else { - return Ok(JSValue::NULL); + return Ok(None); }; if message.is_undefined() { @@ -105,7 +108,7 @@ pub(crate) fn send_helper_primary(global: &JSGlobalObject, frame: &CallFrame) -> let native_fd = { let raw_fd = fd_value.to_int32(); if raw_fd < 0 { - return Ok(JSValue::NULL); + return Ok(None); } bun_sys::Fd::from_uv(raw_fd) }; @@ -113,7 +116,7 @@ pub(crate) fn send_helper_primary(global: &JSGlobalObject, frame: &CallFrame) -> let native_fd = { let raw = fd_value.to_number(global)?; if !(raw.is_finite() && raw >= 0.0) { - return Ok(JSValue::NULL); + return Ok(None); } bun_sys::Fd::from_system(raw as u64 as usize as *mut core::ffi::c_void) }; @@ -125,7 +128,7 @@ pub(crate) fn send_helper_primary(global: &JSGlobalObject, frame: &CallFrame) -> global, message, native_fd, peer_pid, )? else { - return Ok(JSValue::NULL); + return Ok(None); }; let mut h = crate::ipc::Handle::init(native_fd, handle); h.win_export_hex = Some(hex); @@ -136,7 +139,7 @@ pub(crate) fn send_helper_primary(global: &JSGlobalObject, frame: &CallFrame) -> { native_handle = match crate::ipc::Handle::init_dup(native_fd, handle, false) { Ok(h) => Some(h), - Err(_) => return Ok(JSValue::NULL), + Err(_) => return Ok(None), }; } } @@ -178,9 +181,9 @@ pub(crate) fn send_helper_primary(global: &JSGlobalObject, frame: &CallFrame) -> native_handle, ); Ok(match success { - SerializeAndSendResult::Success => JSValue::TRUE, - SerializeAndSendResult::Backoff => JSValue::FALSE, - SerializeAndSendResult::Failure => JSValue::NULL, + SerializeAndSendResult::Success => Some(true), + SerializeAndSendResult::Backoff => Some(false), + SerializeAndSendResult::Failure => None, }) } @@ -305,16 +308,21 @@ pub(crate) fn should_ignore_one_disconnect_event_listener(global: &JSGlobalObjec vm.channel_ref_should_ignore_one_disconnect_event_listener } -#[bun_jsc::host_fn] -pub(crate) fn cluster_raw_bind(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn cluster_raw_bind<'a>( + cx: &mut JsScope<'a>, + address_type: JsLocal<'a>, + address: JsLocal<'a>, + port: JsLocal<'a>, + flags: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let address_type = address_type.to_js(); + let address = address.to_js(); + let port = port.to_js().to_int32(); + let flags = flags.to_js().to_int32(); + #[cfg(windows)] { - let arguments = frame.arguments_as_array::<4>(); - let address_type = arguments[0]; - let address = arguments[1]; - let port = arguments[2].to_int32(); - let flags = arguments[3].to_int32(); - if address_type.is_string() || (address_type.is_int32() && address_type.as_int32() == -1) { return Ok(JSValue::js_number_from_int32(-bun_sys::UV_E::NOTSUP)); } @@ -397,12 +405,6 @@ pub(crate) fn cluster_raw_bind(global: &JSGlobalObject, frame: &CallFrame) -> Js { use core::ffi::c_int; - let arguments = frame.arguments_as_array::<4>(); - let address_type = arguments[0]; - let address = arguments[1]; - let port = arguments[2].to_int32(); - let flags = arguments[3].to_int32(); - let mut is_udp = false; let atype: i32; if address_type.is_string() { diff --git a/src/runtime/node/node_crypto_binding.bind.ts b/src/runtime/node/node_crypto_binding.bind.ts new file mode 100644 index 000000000000..44c738acb907 --- /dev/null +++ b/src/runtime/node/node_crypto_binding.bind.ts @@ -0,0 +1,69 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::node::crypto" }); + +/** Each body validates its own arguments, in Node's order and with Node's errors. */ +const validatesItsArguments = { capability: "js", throws: true } as const; + +const argon2Parameters = { + message: b.any, + nonce: b.any, + parallelism: b.any, + tagLength: b.any, + memory: b.any, + passes: b.any, + secret: b.any, + associatedData: b.any, + type: b.any, +}; + +export const crypto = b.namespace({ + pbkdf2: { + args: { password: b.any, salt: b.any, iterations: b.any, keylen: b.any, digest: b.any, callback: b.any }, + ...validatesItsArguments, + }, + pbkdf2Sync: { + args: { password: b.any, salt: b.any, iterations: b.any, keylen: b.any, digest: b.any }, + returns: b.any, + ...validatesItsArguments, + }, + randomInt: { + args: { min: b.any, max: b.any, callback: b.any }, + returns: b.f64.optional, + length: 2, + ...validatesItsArguments, + }, + randomFill: { + args: { buf: b.any, offset: b.any, size: b.any, callback: b.any }, + ...validatesItsArguments, + }, + randomFillSync: { + args: { buf: b.any, offset: b.any, size: b.any }, + returns: b.any, + ...validatesItsArguments, + }, + randomUUID: { args: { options: b.any }, returns: b.string, ...validatesItsArguments }, + randomUUIDv7: { + args: { options: b.any }, + returns: b.string, + rustPath: "crate::node::crypto::random_uuid_v7", + ...validatesItsArguments, + }, + randomBytes: { args: { size: b.any, callback: b.any }, returns: b.any, ...validatesItsArguments }, + secureHeapUsed: {}, + getFips: { returns: b.i32 }, + setFips: { length: 1 }, + setEngine: { length: 2, capability: "heap", throws: true }, + getHashes: { returns: b.any, capability: "heap", throws: true }, + scrypt: { + args: { password: b.any, salt: b.any, keylen: b.any, options: b.any, callback: b.any }, + ...validatesItsArguments, + }, + scryptSync: { + args: { password: b.any, salt: b.any, keylen: b.any, options: b.any }, + returns: b.any, + ...validatesItsArguments, + }, + argon2: { args: { ...argon2Parameters, callback: b.any }, ...validatesItsArguments }, + argon2Sync: { args: argon2Parameters, returns: b.any, ...validatesItsArguments }, +}); diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index 859fcda09788..a0e83e2f50c1 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -3,14 +3,14 @@ use core::ffi::{c_char, c_void}; -// `rust-argon2` exports its lib as crate `argon2`; alias past the `argon2` host fn below. +// `rust-argon2` exports its lib as crate `argon2`; alias past the `argon2` fn below. use ::argon2 as rust_argon2; use bun_boringssl as boringssl; use bun_collections::CaseInsensitiveAsciiStringArrayHashMap; -use bun_jsc::bun_string_jsc; +use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope}; use bun_jsc::{ - self as jsc, ArrayBuffer, CallFrame, JSGlobalObject, JSValue, Job, JobContext, JsPtr, JsResult, - JsThread, Protected, Strong, + self as jsc, ArrayBuffer, JSGlobalObject, JSValue, Job, JobContext, JsPtr, JsResult, JsThread, + Protected, Strong, }; use crate::node::{Flavor, StringOrBuffer, ThreadIsolated, ThreadIsolatedArg}; @@ -299,16 +299,15 @@ pub(crate) mod random { } fn schedule( - global: &JSGlobalObject, - call_frame: &CallFrame, + cx: &JsThread<'_>, callback: JSValue, job: RandomFillJob, value: JSValue, memory: Option, ) { - let cx = global.js_thread_of_caller(call_frame); + let global = cx.global(); Job::::schedule( - &cx, + cx, job, RandomFillJs { callback: Strong::create(callback.with_async_context_if_needed(global), global), @@ -322,11 +321,17 @@ pub(crate) mod random { use super::*; use crate::node::util::validators; use bun_core::String as BunString; - use bun_jsc::{JSType, StringJsc as _, UUID, UUID7}; - - #[bun_jsc::host_fn] - fn random_int(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let [mut min_value, mut max_value, mut callback] = call_frame.arguments_as_array::<3>(); + use bun_jsc::{JSType, UUID, UUID7}; + + pub(crate) fn random_int<'a>( + cx: &mut JsScope<'a>, + min: JsLocal<'a>, + max: JsLocal<'a>, + callback: JsLocal<'a>, + ) -> JsResult> { + let global = cx.global(); + let [mut min_value, mut max_value, mut callback] = + [min.to_js(), max.to_js(), callback.to_js()]; let mut min_specified = true; if max_value.is_undefined() || max_value.is_callable() { @@ -444,35 +449,35 @@ pub(crate) mod random { JSValue::UNDEFINED, JSValue::js_number(res as f64), )?; - return Ok(JSValue::UNDEFINED); + return Ok(None); } - Ok(JSValue::js_number(res as f64)) + Ok(Some(res as f64)) } - #[bun_jsc::host_fn] - fn random_uuid(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let args = call_frame.arguments(); + pub(crate) fn random_uuid<'a>( + cx: &mut JsScope<'a>, + options: JsLocal<'a>, + ) -> JsResult { + let global = cx.global(); + let options = options.to_js(); let mut disable_entropy_cache = false; - if !args.is_empty() { - let options = args[0]; - if !options.is_undefined() { - validators::validate_object( + if !options.is_undefined() { + validators::validate_object( + global, + options, + format_args!("options"), + Default::default(), + )?; + if let Some(disable_entropy_cache_value) = + options.get(global, "disableEntropyCache")? + { + disable_entropy_cache = validators::validate_boolean( global, - options, - format_args!("options"), - Default::default(), + disable_entropy_cache_value, + format_args!("options.disableEntropyCache"), )?; - if let Some(disable_entropy_cache_value) = - options.get(global, "disableEntropyCache")? - { - disable_entropy_cache = validators::validate_boolean( - global, - disable_entropy_cache_value, - format_args!("options.disableEntropyCache"), - )?; - } } } @@ -489,32 +494,32 @@ pub(crate) mod random { .try_into() .expect("infallible: size matches"), ); - str.into_js(global) + Ok(str) } - #[bun_jsc::host_fn] - fn random_uuid_v7(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let args = call_frame.arguments(); + pub(crate) fn random_uuid_v7<'a>( + cx: &mut JsScope<'a>, + options: JsLocal<'a>, + ) -> JsResult { + let global = cx.global(); + let options = options.to_js(); let mut disable_entropy_cache = false; - if !args.is_empty() { - let options = args[0]; - if !options.is_undefined() { - validators::validate_object( + if !options.is_undefined() { + validators::validate_object( + global, + options, + format_args!("options"), + Default::default(), + )?; + if let Some(disable_entropy_cache_value) = + options.get(global, "disableEntropyCache")? + { + disable_entropy_cache = validators::validate_boolean( global, - options, - format_args!("options"), - Default::default(), + disable_entropy_cache_value, + format_args!("options.disableEntropyCache"), )?; - if let Some(disable_entropy_cache_value) = - options.get(global, "disableEntropyCache")? - { - disable_entropy_cache = validators::validate_boolean( - global, - disable_entropy_cache_value, - format_args!("options.disableEntropyCache"), - )?; - } } } @@ -536,7 +541,7 @@ pub(crate) mod random { .try_into() .expect("infallible: size matches"), ); - str.into_js(global) + Ok(str) } fn assert_offset( @@ -608,9 +613,13 @@ pub(crate) mod random { Ok(size as u32) } - #[bun_jsc::host_fn] - fn random_bytes(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let [size_value, callback] = call_frame.arguments_as_array::<2>(); + pub(crate) fn random_bytes<'a>( + cx: &mut JsScope<'a>, + size: JsLocal<'a>, + callback: JsLocal<'a>, + ) -> JsResult { + let global = cx.global(); + let [size_value, callback] = [size.to_js(), callback.to_js()]; let size = assert_size(global, size_value, 1, 0, MAX_POSSIBLE_LENGTH + 1)?; @@ -627,8 +636,7 @@ pub(crate) mod random { } schedule( - global, - call_frame, + &cx.js_thread(), callback, RandomFillJob::InPlace { // SAFETY: `bytes` is `result`'s backing store, kept alive by the job's @@ -645,9 +653,14 @@ pub(crate) mod random { Ok(JSValue::UNDEFINED) } - #[bun_jsc::host_fn] - fn random_fill_sync(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let [buf_value, offset_value, size_value] = call_frame.arguments_as_array::<3>(); + pub(crate) fn random_fill_sync<'a>( + cx: &mut JsScope<'a>, + buf: JsLocal<'a>, + offset: JsLocal<'a>, + size: JsLocal<'a>, + ) -> JsResult { + let global = cx.global(); + let [buf_value, offset_value, size_value] = [buf.to_js(), offset.to_js(), size.to_js()]; let Some(mut buf) = buf_value.as_array_buffer(global) else { return Err(global.throw_invalid_argument_type_value( @@ -689,10 +702,16 @@ pub(crate) mod random { Ok(buf_value) } - #[bun_jsc::host_fn] - fn random_fill(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { + pub(crate) fn random_fill<'a>( + cx: &mut JsScope<'a>, + buf: JsLocal<'a>, + offset: JsLocal<'a>, + size: JsLocal<'a>, + callback: JsLocal<'a>, + ) -> JsResult<()> { + let global = cx.global(); let [buf_value, offset_value, mut size_value, mut callback] = - call_frame.arguments_as_array::<4>(); + [buf.to_js(), offset.to_js(), size.to_js(), callback.to_js()]; let Some(buf) = buf_value.as_array_buffer(global) else { return Err(global.throw_invalid_argument_type_value( @@ -734,7 +753,7 @@ pub(crate) mod random { if size == 0 { let _ = callback.call(global, JSValue::UNDEFINED, &[JSValue::NULL, buf_value])?; - return Ok(JSValue::UNDEFINED); + return Ok(()); } // `vec![0u8; size]` aborts the process on OOM. The 3-arg overload @@ -748,8 +767,7 @@ pub(crate) mod random { let (memory, view_offset) = buf.find_wasm_memory().unzip(); schedule( - global, - call_frame, + &cx.js_thread(), callback, RandomFillJob::Scratch { scratch, @@ -760,7 +778,7 @@ pub(crate) mod random { memory, ); - Ok(JSValue::UNDEFINED) + Ok(()) } } // mod _hostfns @@ -807,7 +825,7 @@ pub(crate) struct Argon2 { mod _impl { use super::*; use crate::node::util::validators; - use bun_jsc::{ErrorCode, JSFunction, JSType}; + use bun_jsc::{ErrorCode, JSType}; use crate::crypto::pbkdf2::{self, PBKDF2}; @@ -816,15 +834,12 @@ mod _impl { /// returns `(Self, JSValue)`; the sync caller ignores the second element. fn from_js( global: &JSGlobalObject, - call_frame: &CallFrame, + password_value: JSValue, + salt_value: JSValue, + keylen_value: JSValue, + options_arg: JSValue, + callback_arg: JSValue, ) -> JsResult<(Self, JSValue)> { - let [ - password_value, - salt_value, - keylen_value, - options_arg, - callback_arg, - ] = call_frame.arguments_as_array::<5>(); let mut maybe_options_value: Option = Some(options_arg); let mut callback = callback_arg; @@ -999,9 +1014,14 @@ mod _impl { /// `from_js::` for the work-pool job, with its callback. fn from_js_async( global: &JSGlobalObject, - call_frame: &CallFrame, + password: JSValue, + salt: JSValue, + keylen: JSValue, + options: JSValue, + callback: JSValue, ) -> JsResult<(ThreadIsolated, JSValue)> { - let (ctx, callback) = Self::from_js::(global, call_frame)?; + let (ctx, callback) = + Self::from_js::(global, password, salt, keylen, options, callback)?; // SAFETY: `from_js::` copied the buffers and thread-isolated the strings. Ok((unsafe { ThreadIsolated::new(ctx) }, callback)) } @@ -1146,18 +1166,49 @@ mod _impl { } } - #[bun_jsc::host_fn] - fn pbkdf2(global_this: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let (data, callback) = PBKDF2::from_js_async(global_this, call_frame)?; - pbkdf2::create_job(&global_this.js_thread_of_caller(call_frame), data, callback); - Ok(JSValue::UNDEFINED) + pub(crate) fn pbkdf2<'a>( + cx: &mut JsScope<'a>, + password: JsLocal<'a>, + salt: JsLocal<'a>, + iterations: JsLocal<'a>, + keylen: JsLocal<'a>, + digest: JsLocal<'a>, + callback: JsLocal<'a>, + ) -> JsResult<()> { + let (data, callback) = PBKDF2::from_js_async( + cx.global(), + password.to_js(), + salt.to_js(), + iterations.to_js(), + keylen.to_js(), + digest.to_js(), + callback.to_js(), + )?; + pbkdf2::create_job(&cx.js_thread(), data, callback); + Ok(()) } - #[bun_jsc::host_fn] - fn pbkdf2_sync(global_this: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { + pub(crate) fn pbkdf2_sync<'a>( + cx: &mut JsScope<'a>, + password: JsLocal<'a>, + salt: JsLocal<'a>, + iterations: JsLocal<'a>, + keylen: JsLocal<'a>, + digest: JsLocal<'a>, + ) -> JsResult { + let global_this = cx.global(); // `PBKDF2`'s `StringOrBuffer` fields release on `Drop`, so the local // just goes out of scope. - let (mut data, _) = PBKDF2::from_js(global_this, call_frame, Flavor::Sync)?; + let (mut data, _) = PBKDF2::from_js( + global_this, + password.to_js(), + salt.to_js(), + iterations.to_js(), + keylen.to_js(), + digest.to_js(), + JSValue::UNDEFINED, + Flavor::Sync, + )?; // `create_buffer_from_length` → `JSBuffer__bufferFromLength`, which constructs // with `JSBufferSubclassStructure` (a Node.js `Buffer`, not a plain Uint8Array/ArrayBuffer). // `pbkdf2Sync()` MUST return a Buffer — `Buffer.isBuffer(result)` and Buffer-only methods @@ -1176,29 +1227,19 @@ mod _impl { Ok(out_arraybuffer) } - #[bun_jsc::host_fn] - fn secure_heap_used(_: &JSGlobalObject, _: &CallFrame) -> JsResult { - Ok(JSValue::UNDEFINED) - } + pub(crate) fn secure_heap_used() {} - #[bun_jsc::host_fn] - fn get_fips(_: &JSGlobalObject, _: &CallFrame) -> JsResult { - Ok(JSValue::js_number(0.0)) + pub(crate) fn get_fips() -> i32 { + 0 } - #[bun_jsc::host_fn] - fn set_fips(_: &JSGlobalObject, _: &CallFrame) -> JsResult { - Ok(JSValue::UNDEFINED) - } + pub(crate) fn set_fips() {} - #[bun_jsc::host_fn] - fn set_engine(global: &JSGlobalObject, _: &CallFrame) -> JsResult { - Err(global - .err( - ErrorCode::CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, - format_args!("Custom engines not supported by BoringSSL"), - ) - .throw()) + pub(crate) fn set_engine<'a>(heap: &JsHeap<'a>) -> bindgen::Result<'a, ()> { + Err(heap.error_with_code( + ErrorCode::CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, + format_args!("Custom engines not supported by BoringSSL"), + )) } extern "C" fn for_each_hash( @@ -1219,8 +1260,7 @@ mod _impl { bun_core::handle_oom(hashes.put(from_bytes, ())); } - #[bun_jsc::host_fn] - fn get_hashes(global: &JSGlobalObject, _: &CallFrame) -> JsResult { + pub(crate) fn get_hashes<'a>(heap: &JsHeap<'a>) -> bindgen::Result<'a, JsLocal<'a>> { let mut hashes: CaseInsensitiveAsciiStringArrayHashMap<()> = CaseInsensitiveAsciiStringArrayHashMap::new(); @@ -1231,24 +1271,31 @@ mod _impl { boringssl::c::EVP_MD_do_all_sorted(for_each_hash, (&raw mut hashes).cast::()); } - let array = JSValue::create_empty_array(global, hashes.count())?; - - for (i, hash) in hashes.keys().iter().enumerate() { - let str = bun_string_jsc::create_utf8_for_js(global, hash)?; - array.put_index(global, u32::try_from(i).expect("int cast"), str)?; - } - - Ok(array) + bindgen::to_js::array(heap, hashes.keys(), bindgen::to_js::utf8) } - #[bun_jsc::host_fn] - fn scrypt(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let (params, callback) = Scrypt::from_js_async(global, call_frame)?; + pub(crate) fn scrypt<'a>( + cx: &mut JsScope<'a>, + password: JsLocal<'a>, + salt: JsLocal<'a>, + keylen: JsLocal<'a>, + options: JsLocal<'a>, + callback: JsLocal<'a>, + ) -> JsResult<()> { + let cx = cx.js_thread(); + let global = cx.global(); + let (params, callback) = Scrypt::from_js_async( + global, + password.to_js(), + salt.to_js(), + keylen.to_js(), + options.to_js(), + callback.to_js(), + )?; if params.keylen as usize > jsc::virtual_machine::synthetic_allocation_limit() { return Err(global.throw_out_of_memory()); } let (buf, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, params.keylen)?; - let cx = global.js_thread_of_caller(call_frame); Job::::schedule( &cx, ScryptJob { @@ -1262,13 +1309,26 @@ mod _impl { buf: Strong::create(buf, global), }, ); - Ok(JSValue::UNDEFINED) + Ok(()) } - #[bun_jsc::host_fn] - fn scrypt_sync(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { + pub(crate) fn scrypt_sync<'a>( + cx: &mut JsScope<'a>, + password: JsLocal<'a>, + salt: JsLocal<'a>, + keylen: JsLocal<'a>, + options: JsLocal<'a>, + ) -> JsResult { + let global = cx.global(); // `password`/`salt` release on drop; nothing was protected on this path. - let (ctx, _) = Scrypt::from_js::(global, call_frame)?; + let (ctx, _) = Scrypt::from_js::( + global, + password.to_js(), + salt.to_js(), + keylen.to_js(), + options.to_js(), + JSValue::UNDEFINED, + )?; let (buf, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, ctx.keylen)?; if ctx.run_task_impl(bytes).is_some() { return Err(global @@ -1284,7 +1344,18 @@ mod _impl { impl Argon2 { /// Arguments arrive pre-validated from `checkArgon2()` in `crypto.ts`; /// the checks here only defend the internal binding itself. - fn from_js(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult<(Self, JSValue)> { + fn from_js( + global: &JSGlobalObject, + message_value: JSValue, + nonce_value: JSValue, + parallelism_value: JSValue, + tag_length_value: JSValue, + memory_value: JSValue, + passes_value: JSValue, + secret_value: JSValue, + associated_data_value: JSValue, + variant_value: JSValue, + ) -> JsResult { fn copy_buffer_arg( global: &JSGlobalObject, value: JSValue, @@ -1306,19 +1377,6 @@ mod _impl { Ok(copy) } - let [ - message_value, - nonce_value, - parallelism_value, - tag_length_value, - memory_value, - passes_value, - secret_value, - associated_data_value, - variant_value, - callback, - ] = call_frame.arguments_as_array::<10>(); - let parallelism = validators::validate_uint32( global, parallelism_value, @@ -1367,7 +1425,7 @@ mod _impl { let failed = (memory as usize).saturating_mul(1024) > limit || tag_length as usize > limit; - let ctx = Argon2 { + Ok(Argon2 { message: copy_buffer_arg(global, message_value, b"message")?, nonce: copy_buffer_arg(global, nonce_value, b"nonce")?, secret: copy_buffer_arg(global, secret_value, b"secret")?, @@ -1379,8 +1437,7 @@ mod _impl { variant, output: Vec::new(), failed, - }; - Ok((ctx, callback)) + }) } fn run(&mut self) { @@ -1471,11 +1528,35 @@ mod _impl { } } - #[bun_jsc::host_fn] - fn argon2(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let (ctx, callback) = Argon2::from_js(global, call_frame)?; + pub(crate) fn argon2<'a>( + cx: &mut JsScope<'a>, + message: JsLocal<'a>, + nonce: JsLocal<'a>, + parallelism: JsLocal<'a>, + tag_length: JsLocal<'a>, + memory: JsLocal<'a>, + passes: JsLocal<'a>, + secret: JsLocal<'a>, + associated_data: JsLocal<'a>, + variant: JsLocal<'a>, + callback: JsLocal<'a>, + ) -> JsResult<()> { + let cx = cx.js_thread(); + let global = cx.global(); + let callback = callback.to_js(); + let ctx = Argon2::from_js( + global, + message.to_js(), + nonce.to_js(), + parallelism.to_js(), + tag_length.to_js(), + memory.to_js(), + passes.to_js(), + secret.to_js(), + associated_data.to_js(), + variant.to_js(), + )?; let _ = validators::validate_function(global, "callback", callback)?; - let cx = global.js_thread_of_caller(call_frame); Job::::schedule( &cx, ctx, @@ -1483,12 +1564,34 @@ mod _impl { callback: Strong::create(callback.with_async_context_if_needed(global), global), }, ); - Ok(JSValue::UNDEFINED) + Ok(()) } - #[bun_jsc::host_fn] - fn argon2_sync(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { - let (mut ctx, _) = Argon2::from_js(global, call_frame)?; + pub(crate) fn argon2_sync<'a>( + cx: &mut JsScope<'a>, + message: JsLocal<'a>, + nonce: JsLocal<'a>, + parallelism: JsLocal<'a>, + tag_length: JsLocal<'a>, + memory: JsLocal<'a>, + passes: JsLocal<'a>, + secret: JsLocal<'a>, + associated_data: JsLocal<'a>, + variant: JsLocal<'a>, + ) -> JsResult { + let global = cx.global(); + let mut ctx = Argon2::from_js( + global, + message.to_js(), + nonce.to_js(), + parallelism.to_js(), + tag_length.to_js(), + memory.to_js(), + passes.to_js(), + secret.to_js(), + associated_data.to_js(), + variant.to_js(), + )?; ctx.run(); if ctx.failed { let err = global.create_error_instance(format_args!("Argon2 derivation failed")); @@ -1497,188 +1600,12 @@ mod _impl { // Ownership transfers to JSC (freed via MarkedArrayBuffer_deallocator). JSValue::create_buffer(global, ctx.output.leak()) } - - pub(crate) fn create_node_crypto_binding_zig(global: &JSGlobalObject) -> JSValue { - let crypto = JSValue::create_empty_object(global, 17); - - // `#[bun_jsc::host_fn]` emits a `__jsc_host_{name}` shim with the raw `JSHostFn` ABI; - // pass that (not the safe-Rust body) to `JSFunction::create`. - crypto.put( - global, - b"pbkdf2", - JSFunction::create(global, "pbkdf2", __jsc_host_pbkdf2, 6, Default::default()), - ); - crypto.put( - global, - b"pbkdf2Sync", - JSFunction::create( - global, - "pbkdf2Sync", - __jsc_host_pbkdf2_sync, - 5, - Default::default(), - ), - ); - crypto.put( - global, - b"randomInt", - JSFunction::create( - global, - "randomInt", - random::__jsc_host_random_int, - 2, - Default::default(), - ), - ); - crypto.put( - global, - b"randomFill", - JSFunction::create( - global, - "randomFill", - random::__jsc_host_random_fill, - 4, - Default::default(), - ), - ); - crypto.put( - global, - b"randomFillSync", - JSFunction::create( - global, - "randomFillSync", - random::__jsc_host_random_fill_sync, - 3, - Default::default(), - ), - ); - crypto.put( - global, - b"randomUUID", - JSFunction::create( - global, - "randomUUID", - random::__jsc_host_random_uuid, - 1, - Default::default(), - ), - ); - crypto.put( - global, - b"randomUUIDv7", - JSFunction::create( - global, - "randomUUIDv7", - random::__jsc_host_random_uuid_v7, - 1, - Default::default(), - ), - ); - crypto.put( - global, - b"randomBytes", - JSFunction::create( - global, - "randomBytes", - random::__jsc_host_random_bytes, - 2, - Default::default(), - ), - ); - - crypto.put( - global, - b"secureHeapUsed", - JSFunction::create( - global, - "secureHeapUsed", - __jsc_host_secure_heap_used, - 0, - Default::default(), - ), - ); - crypto.put( - global, - b"getFips", - JSFunction::create( - global, - "getFips", - __jsc_host_get_fips, - 0, - Default::default(), - ), - ); - crypto.put( - global, - b"setFips", - JSFunction::create( - global, - "setFips", - __jsc_host_set_fips, - 1, - Default::default(), - ), - ); - crypto.put( - global, - b"setEngine", - JSFunction::create( - global, - "setEngine", - __jsc_host_set_engine, - 2, - Default::default(), - ), - ); - - crypto.put( - global, - b"getHashes", - JSFunction::create( - global, - "getHashes", - __jsc_host_get_hashes, - 0, - Default::default(), - ), - ); - - crypto.put( - global, - b"scrypt", - JSFunction::create(global, "scrypt", __jsc_host_scrypt, 5, Default::default()), - ); - crypto.put( - global, - b"scryptSync", - JSFunction::create( - global, - "scryptSync", - __jsc_host_scrypt_sync, - 4, - Default::default(), - ), - ); - - crypto.put( - global, - b"argon2", - JSFunction::create(global, "argon2", __jsc_host_argon2, 10, Default::default()), - ); - crypto.put( - global, - b"argon2Sync", - JSFunction::create( - global, - "argon2Sync", - __jsc_host_argon2_sync, - 9, - Default::default(), - ), - ); - - crypto - } } // mod _impl -pub(crate) use _impl::create_node_crypto_binding_zig; +pub(crate) use _impl::{ + argon2, argon2_sync, get_fips, get_hashes, pbkdf2, pbkdf2_sync, scrypt, scrypt_sync, + secure_heap_used, set_engine, set_fips, +}; +pub(crate) use random::{ + random_bytes, random_fill, random_fill_sync, random_int, random_uuid, random_uuid_v7, +}; diff --git a/src/runtime/node/node_fs_binding.bind.ts b/src/runtime/node/node_fs_binding.bind.ts index e69a891caa68..9d2381e728d2 100644 --- a/src/runtime/node/node_fs_binding.bind.ts +++ b/src/runtime/node/node_fs_binding.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::node::node_fs_binding" }); + /** Arguments are parsed in `node_fs.rs`, which throws Node's errors. */ const operation = (length: number) => ({ args: { args: b.any.variadic }, returns: b.any, capability: "js", throws: true, length }) as const; @@ -51,7 +53,7 @@ const operations: Record = { const getter = { getter: b.any, capability: "js" } as const; -/** What `createBinding` makes: `src/js/internal/fs/binding.ts`. */ +/** `src/js/internal/fs/binding.ts`. */ export const NodeJSFS = b.class({ rustPath: "crate::node::node_fs_binding::Binding", proto: Object.fromEntries( @@ -70,3 +72,23 @@ export const NodeJSFS = b.class({ ].sort(([x], [y]) => x.localeCompare(y)), ), }); + +export const createBinding = b.fn({ returns: NodeJSFS, capability: "js" }); + +/** `(path, options)`, parsed as `fs.rm` parses them. */ +export const rmOptionsForTesting = b.fn({ + args: { args: b.any.variadic }, + returns: b.any, + capability: "js", + throws: true, + length: 2, +}); +export const stringToFlagsForTesting = b.fn({ args: { flags: b.any }, returns: b.i32, capability: "js", throws: true }); +/** Without `size` it makes nothing. */ +export const createMemfdForTesting = b.fn({ + args: { size: b.any.variadic }, + returns: b.i32.optional, + capability: "js", + throws: true, + length: 1, +}); diff --git a/src/runtime/node/node_fs_binding.rs b/src/runtime/node/node_fs_binding.rs index c04935c73220..db295de73ed7 100644 --- a/src/runtime/node/node_fs_binding.rs +++ b/src/runtime/node/node_fs_binding.rs @@ -1,11 +1,9 @@ use core::ptr::NonNull; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::{JsLocal, JsPayload as _, JsScope, JsWrapper}; use bun_jsc::call_frame::ArgumentsSlice; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{ - CallFrame, JSGlobalObject, JSPromise, JSValue, JsCell, JsClass as _, JsResult, SysErrorJsc as _, -}; +use bun_jsc::{JSGlobalObject, JSPromise, JSValue, JsCell, JsResult, SysErrorJsc as _}; use bun_sys_jsc::SystemErrorJsc as _; use crate::node::ThreadIsolated; @@ -372,27 +370,27 @@ impl Binding { // pub const statfsSync = callSync(.statfs); } -pub(crate) fn create_binding(global: &JSGlobalObject) -> JSValue { +pub(crate) fn create_binding<'a>(cx: &mut JsScope<'a>) -> JsWrapper<'a, Binding> { Binding { node_fs: JsCell::new(NodeFS { - vm: NonNull::new(global.bun_vm_ptr()), + vm: NonNull::new(cx.global().bun_vm_ptr()), ..Default::default() }), } - .to_js(global) + .into_js(&cx.heap) } /// Test-only (`bun:internal-for-testing`): run `(path, options)` through the /// exact argument parser `fs.rm` uses and return the parsed options, so node's /// `internal/fs/utils` `validateRmOptionsSync` tests exercise the production /// validation (including its rejection of own-but-`undefined` booleans). -#[bun_jsc::host_fn] pub(crate) fn rm_options_for_testing( - global: &JSGlobalObject, - frame: &CallFrame, + cx: &mut JsScope<'_>, + args: &[JsLocal<'_>], ) -> JsResult { + let global = cx.global(); let vm: &VirtualMachine = global.bun_vm(); - let mut slice = ArgumentsSlice::init(vm, frame.arguments()); + let mut slice = ArgumentsSlice::init(vm, JsLocal::to_js_slice(args)); let parsed = args::Rm::from_js(global, &mut slice)?; let obj = JSValue::create_empty_object(global, 4); obj.put( @@ -413,14 +411,12 @@ pub(crate) fn rm_options_for_testing( /// Test-only (`bun:internal-for-testing`): run a flags value through the same /// parser `fs.open` uses and return the numeric O_* mask, so node's /// `internal/fs/utils` `stringToFlags` tests can assert the production mapping. -#[bun_jsc::host_fn] pub(crate) fn string_to_flags_for_testing( - global: &JSGlobalObject, - frame: &CallFrame, -) -> JsResult { + cx: &mut JsScope<'_>, + flags: JsLocal<'_>, +) -> JsResult { use crate::node::types::FileSystemFlags; - let [val] = frame.arguments_as_array::<1>(); - let flags = FileSystemFlags::from_js(global, val)?.unwrap_or(FileSystemFlags::R); + let flags = FileSystemFlags::from_js(cx.global(), flags.to_js())?.unwrap_or(FileSystemFlags::R); // On Windows the internal bun.O bits are POSIX-shaped and translated to the // MSVCRT `_O_*` values at the open boundary; node's stringToFlags and // fs.constants both speak MSVCRT, so translate here too. @@ -428,19 +424,17 @@ pub(crate) fn string_to_flags_for_testing( let bits = bun_sys::windows::libuv::O::from_bun_o(flags.as_int()); #[cfg(not(windows))] let bits = flags.as_int(); - Ok(JSValue::js_number_from_int32(bits)) + Ok(bits) } -#[bun_jsc::host_fn] pub(crate) fn create_memfd_for_testing( - global: &JSGlobalObject, - frame: &CallFrame, -) -> JsResult { - let [size_arg] = frame.arguments_as_array::<1>(); - - if frame.arguments_count() < 1 { - return Ok(JSValue::UNDEFINED); - } + cx: &mut JsScope<'_>, + size: &[JsLocal<'_>], +) -> JsResult> { + let global = cx.global(); + let Some(size_arg) = size.first() else { + return Ok(None); + }; #[cfg(not(any(target_os = "linux", target_os = "android")))] { @@ -452,11 +446,11 @@ pub(crate) fn create_memfd_for_testing( #[cfg(any(target_os = "linux", target_os = "android"))] { - let size = size_arg.to_int64(); + let size = size_arg.to_js().to_int64(); match bun_sys::memfd_create(c"my_memfd", bun_sys::MemfdFlags::NonExecutable) { Ok(fd) => { let _ = bun_sys::ftruncate(fd, size); - Ok(JSValue::js_number_from_int32(fd.native() as i32)) + Ok(Some(fd.native() as i32)) } Err(err) => Err(global.throw_value(err.to_js(global))), } diff --git a/src/runtime/node/node_http_binding.bind.ts b/src/runtime/node/node_http_binding.bind.ts index e64026e979ff..c675367097a6 100644 --- a/src/runtime/node/node_http_binding.bind.ts +++ b/src/runtime/node/node_http_binding.bind.ts @@ -18,6 +18,7 @@ export const BunServer = b.union({ debugHttp: DebugHTTPServer, debugHttps: DebugHTTPSServer, }); +export const getBunServerAllClosedPromise = b.fn({ args: { server: BunServer }, returns: b.any, capability: "js" }); export const getBunServerOpenCount = b.fn({ args: { server: BunServer }, returns: b.f64 }); /** `kInternalSocketData` of the socket given to `handleUpgrade()` of `ws`, which can be any `Duplex`. */ diff --git a/src/runtime/node/node_http_binding.rs b/src/runtime/node/node_http_binding.rs index 2791d52383ad..137de0be470c 100644 --- a/src/runtime/node/node_http_binding.rs +++ b/src/runtime/node/node_http_binding.rs @@ -3,32 +3,24 @@ //! transport: `upgradeNodeHTTPResponseToWebSocket` / `sendWebSocketFrame`. use bun_jsc::bindgen::{JsLocal, JsScope}; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use bun_jsc::{JSValue, JsResult}; use bun_uws_sys::Opcode; use bun_uws_sys::response::PerMessageDeflateParams; use crate::generated_bindgen::node_http_binding::{BunServer, DeflateParams, UpgradeResponse}; -use crate::server::{AnyServer, ServerWebSocket}; +use crate::server::ServerWebSocket; -pub(crate) fn get_bun_server_all_closed_promise( - global: &JSGlobalObject, - frame: &CallFrame, -) -> JsResult { - let arguments = frame.arguments(); - if arguments.is_empty() { - return Err(global.throw_not_enough_arguments( - "getBunServerAllClosePromise", - 1, - arguments.len(), - )); +pub(crate) fn get_bun_server_all_closed_promise<'a>( + cx: &mut JsScope<'a>, + server: BunServer<'a>, +) -> JSValue { + let global = cx.global(); + match server { + BunServer::Http(server) => server.get_all_closed_promise(global), + BunServer::Https(server) => server.get_all_closed_promise(global), + BunServer::DebugHttp(server) => server.get_all_closed_promise(global), + BunServer::DebugHttps(server) => server.get_all_closed_promise(global), } - - let value = arguments[0]; - - let Some(server) = AnyServer::from_js(value) else { - return Err(global.throw_invalid_argument_type_value("server", "bun.Server", value)); - }; - Ok(server.get_all_closed_promise(global)) } /// What keeps the server open besides its listener: the connections (counted from accept), WebSockets and requests. diff --git a/src/runtime/node/node_net_binding.bind.ts b/src/runtime/node/node_net_binding.bind.ts index f5aa18a902ce..5de1585d22ff 100644 --- a/src/runtime/node/node_net_binding.bind.ts +++ b/src/runtime/node/node_net_binding.bind.ts @@ -17,3 +17,4 @@ export const setDefaultAutoSelectFamilyAttemptTimeout = b.fn({ }); export const newDetachedSocket = b.fn({ args: { isSSL: b.bool }, returns: b.any, capability: "js" }); +export const doConnect = b.fn({ args: { prev: b.any, opts: b.any }, returns: b.any, capability: "js", throws: true }); diff --git a/src/runtime/node/node_net_binding.rs b/src/runtime/node/node_net_binding.rs index 8e386ef3d749..4d3c397e9d19 100644 --- a/src/runtime/node/node_net_binding.rs +++ b/src/runtime/node/node_net_binding.rs @@ -5,8 +5,8 @@ use core::cell::Cell; use core::sync::atomic::{AtomicBool, Ordering}; use bun_io::KeepAlive; -use bun_jsc::bindgen::JsScope; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsCell, JsResult}; +use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::{self as jsc, JSGlobalObject, JSValue, JsCell, JsResult}; use bun_uws as uws; use crate::socket::{Listener, NativeCallbacks, NewSocket, SocketFlags, TCPSocket, TLSSocket}; @@ -86,15 +86,13 @@ pub(crate) fn new_detached_socket(cx: &mut JsScope<'_>, is_ssl: bool) -> JSValue } } -#[bun_jsc::host_fn] -pub(crate) fn do_connect(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let [prev, opts] = frame.arguments_as_array::<2>(); +pub(crate) fn do_connect<'a>( + cx: &mut JsScope<'a>, + prev: JsLocal<'a>, + opts: JsLocal<'a>, +) -> JsResult { + let prev = prev.to_js(); let maybe_tcp = prev.as_::(); let maybe_tls = prev.as_::(); - Listener::connect_inner( - &global.js_thread_of_caller(frame), - maybe_tcp, - maybe_tls, - opts, - ) + Listener::connect_inner(&cx.js_thread(), maybe_tcp, maybe_tls, opts.to_js()) } diff --git a/src/runtime/node/node_process.rs b/src/runtime/node/node_process.rs index 219c4afbf37d..c156fad53e3e 100644 --- a/src/runtime/node/node_process.rs +++ b/src/runtime/node/node_process.rs @@ -215,7 +215,7 @@ mod _impl { // The C++ caller // (headers.h) declares `EncodedJSValue Bun__Process__createExecArgv(JSGlobalObject*)`, - // not a `JSHostFunctionType`. Hand-roll the shim instead of `#[bun_jsc::host_fn]`. + // not a `JSHostFunctionType`. #[unsafe(no_mangle)] extern "C" fn Bun__Process__createExecArgv(global_object: &JSGlobalObject) -> JSValue { bun_jsc::to_js_host_fn_result(global_object, create_exec_argv(global_object)) @@ -453,8 +453,7 @@ mod _impl { // ───────────────────────────── cwd ───────────────────────────── // C++ (headers.h) declares - // `EncodedJSValue Bun__Process__getCwd(JSGlobalObject*)`. Hand-roll the - // shim instead of `#[bun_jsc::host_fn]` (caller is not a JSHostFunction). + // `EncodedJSValue Bun__Process__getCwd(JSGlobalObject*)`, not a `JSHostFunctionType`. #[unsafe(no_mangle)] extern "C" fn Bun__Process__getCwd(global_object: &JSGlobalObject) -> JSValue { bun_jsc::to_js_host_fn_result(global_object, get_cwd(global_object)) diff --git a/src/runtime/node/node_quic_binding.bind.ts b/src/runtime/node/node_quic_binding.bind.ts new file mode 100644 index 000000000000..c15f9177aeff --- /dev/null +++ b/src/runtime/node/node_quic_binding.bind.ts @@ -0,0 +1,13 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::node::node_quic_binding" }); + +/** `src/js/internal/quic/binding.ts`. */ +export const createBinding = b.fn({ returns: b.any, capability: "js", throws: true }); + +export const setCallbacks = b.fn({ + export: "Bun__NodeQuicBinding__setCallbacks", + args: { callbacks: b.any }, + capability: "js", + throws: true, +}); diff --git a/src/runtime/node/node_quic_binding.rs b/src/runtime/node/node_quic_binding.rs index a970e85937c5..4bfcc57b571b 100644 --- a/src/runtime/node/node_quic_binding.rs +++ b/src/runtime/node/node_quic_binding.rs @@ -1,7 +1,8 @@ //! The `node:quic` native binding object — Bun's equivalent of Node's //! `internalBinding('quic')` (reference: node/src/quic/*, v26.3.0). -use bun_jsc::{self as jsc, CallFrame, JSFunction, JSGlobalObject, JSValue, JsResult, StringJsc}; +use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::{self as jsc, JSFunction, JSGlobalObject, JSValue, JsResult, StringJsc}; use super::quic::{callbacks, endpoint, session, stream}; @@ -32,8 +33,8 @@ const QUIC_CALLBACK_NAMES: &[&str] = &[ "StreamTrailers", ]; -#[bun_jsc::host_fn] -fn set_callbacks(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn set_callbacks<'a>(cx: &mut JsScope<'a>, callbacks: JsLocal<'a>) -> JsResult<()> { + let global = cx.global(); // Only the first call takes effect; subsequent calls are ignored // (node/src/quic/bindingdata.cc `BindingData::SetCallbacks`). if global @@ -44,9 +45,9 @@ fn set_callbacks(global: &JSGlobalObject, frame: &CallFrame) -> JsResult()[0]; + let callbacks_obj = callbacks.to_js(); if !callbacks_obj.is_object() { return Err(global .err( @@ -71,7 +72,7 @@ fn set_callbacks(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult { +pub(crate) fn create_binding(cx: &mut JsScope<'_>) -> JsResult { + let global = cx.global(); let obj = JSValue::create_empty_object(global, 16); obj.put( @@ -115,7 +117,7 @@ pub(crate) fn create_node_quic_binding(global: &JSGlobalObject) -> JsResult ParseArgsState<'a> { } } -#[bun_jsc::host_fn(export = "Bun__NodeUtil__jsParseArgs")] -pub(crate) fn parse_args(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - MarkedArgumentBuffer::new(|default_roots| parse_args_impl(global, callframe, default_roots)) +pub(crate) fn parse_args<'a>(cx: &mut JsScope<'a>, config: JsLocal<'a>) -> JsResult { + let global = cx.global(); + MarkedArgumentBuffer::new(|default_roots| { + parse_args_impl(global, config.to_js(), default_roots) + }) } fn parse_args_impl( global: &JSGlobalObject, - callframe: &CallFrame, + config_value: JSValue, default_roots: &mut MarkedArgumentBuffer, ) -> JsResult { // jsc.markBinding(@src()) — debug-only, dropped - let config_value = callframe.arguments_as_array::<1>()[0]; // // Phase 0: parse the config object // diff --git a/src/runtime/server/NodeHTTPResponse.bind.ts b/src/runtime/server/NodeHTTPResponse.bind.ts index c4adf0d6c442..f210c0afa383 100644 --- a/src/runtime/server/NodeHTTPResponse.bind.ts +++ b/src/runtime/server/NodeHTTPResponse.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::server::node_http_response" }); + const eventHandler = { getter: b.any, setter: { capability: "js" }, passThis: true } as const; const head = { statusCode: b.i32.validateInteger(100, 999).default(200), @@ -70,3 +72,8 @@ export const NodeHTTPResponse = b.class({ }, }, }); + +/** The reactions to the promise that the `node:http` request handler returned. `context` is the `NodeHTTPResponse`. */ +const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +export const onResolve = b.fn({ ...reaction, export: "Bun__NodeHTTPRequest__onResolve" }); +export const onReject = b.fn({ ...reaction, export: "Bun__NodeHTTPRequest__onReject" }); diff --git a/src/runtime/server/NodeHTTPResponse.rs b/src/runtime/server/NodeHTTPResponse.rs index 75891a8f9765..d0ee29eeeb2f 100644 --- a/src/runtime/server/NodeHTTPResponse.rs +++ b/src/runtime/server/NodeHTTPResponse.rs @@ -17,7 +17,7 @@ use bun_uws_sys as uws_sys; use crate::generated_bindgen::node_http_response::js_NodeHTTPResponse as js; use crate::node::{Encoding, StringOrBuffer}; use crate::server::jsc::{ - self, CallFrame, ErrorCode, JSGlobalObject, JSValue, JsResult, StrongOptional, VirtualMachine, + self, ErrorCode, JSGlobalObject, JSValue, JsResult, StrongOptional, VirtualMachine, }; use crate::server::{AnyServer, AnyServerTag, HTTPStatusText, ServerWebSocket}; use crate::webcore::AutoFlusher; @@ -1399,13 +1399,12 @@ impl NodeHTTPResponse { } } -#[bun_jsc::host_fn(export = "Bun__NodeHTTPRequest__onResolve")] -fn node_http_request_on_resolve(global_object: &JSGlobalObject, callframe: &CallFrame) -> JSValue { +pub(crate) fn on_resolve(cx: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { scoped_log!(NodeHTTPResponse, "onResolve"); - let arguments = callframe.arguments_as_array::<2>(); - // arguments[1] is the JSNodeHTTPResponse cell from the resolve callback. + let global_object = cx.global(); + // `context` is the JSNodeHTTPResponse cell from the resolve callback. // R-2: deref shared — `maybe_stop_reading_body`/`on_request_complete` re-enter. - let this: &NodeHTTPResponse = arguments[1].as_class_ref::().unwrap(); + let this: &NodeHTTPResponse = context.to_js().as_class_ref::().unwrap(); // `promise` non-empty is the ownership token for the server-handler ref; // `mark_request_as_done` may have already released it on abort. let had_promise = this.promise.with_mut(|p| { @@ -1413,7 +1412,7 @@ fn node_http_request_on_resolve(global_object: &JSGlobalObject, callframe: &Call p.deinit(); had }); - this.maybe_stop_reading_body(arguments[1]); + this.maybe_stop_reading_body(context.to_js()); let flags = this.flags.get(); if !flags.contains(Flags::REQUEST_HAS_COMPLETED) && !this.is_socket_closed_or_closing() { @@ -1438,16 +1437,13 @@ fn node_http_request_on_resolve(global_object: &JSGlobalObject, callframe: &Call if had_promise { this.deref(); } - JSValue::UNDEFINED } -#[bun_jsc::host_fn(export = "Bun__NodeHTTPRequest__onReject")] -fn node_http_request_on_reject(global_object: &JSGlobalObject, callframe: &CallFrame) -> JSValue { - let arguments = callframe.arguments_as_array::<2>(); - let err = arguments[0]; - // arguments[1] is the JSNodeHTTPResponse cell from the reject callback. +pub(crate) fn on_reject(cx: &mut JsScope<'_>, err: JsLocal<'_>, context: JsLocal<'_>) { + let global_object = cx.global(); + // `context` is the JSNodeHTTPResponse cell from the reject callback. // R-2: deref shared — `maybe_stop_reading_body`/`on_request_complete` re-enter. - let this: &NodeHTTPResponse = arguments[1].as_class_ref::().unwrap(); + let this: &NodeHTTPResponse = context.to_js().as_class_ref::().unwrap(); // `promise` non-empty is the ownership token for the server-handler ref; // `mark_request_as_done` may have already released it on abort. let had_promise = this.promise.with_mut(|p| { @@ -1455,7 +1451,7 @@ fn node_http_request_on_reject(global_object: &JSGlobalObject, callframe: &CallF p.deinit(); had }); - this.maybe_stop_reading_body(arguments[1]); + this.maybe_stop_reading_body(context.to_js()); let flags = this.flags.get(); if !flags.contains(Flags::REQUEST_HAS_COMPLETED) @@ -1482,11 +1478,10 @@ fn node_http_request_on_reject(global_object: &JSGlobalObject, callframe: &CallF this.on_request_complete(); } - let _ = bun_vm_mut(global_object).uncaught_exception(global_object, err, true); + let _ = bun_vm_mut(global_object).uncaught_exception(global_object, err.to_js(), true); if had_promise { this.deref(); } - JSValue::UNDEFINED } impl NodeHTTPResponse { diff --git a/src/runtime/server/RequestContext.bind.ts b/src/runtime/server/RequestContext.bind.ts new file mode 100644 index 000000000000..9148b6f09abd --- /dev/null +++ b/src/runtime/server/RequestContext.bind.ts @@ -0,0 +1,42 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +/** The promise reactions of `RequestContext`, which C++ tells apart by their address. */ +function reactions(debug: boolean, mux: boolean, ssl: boolean) { + const context = `HTTPRequestContext${debug ? "Debug" : ""}${mux ? "Mux" : ""}${ssl ? "TLS" : ""}`; + const server = `crate::server::${debug ? "Debug" : ""}HTTP${ssl ? "S" : ""}Server`; + const reaction = (name: string, rustName: string) => + b.fn({ + export: `Bun__${context}__${name}`, + args: { value: b.any, context: b.any }, + capability: "js", + rustPath: `crate::server::request_context::RequestContext::<${server}, ${ssl}, ${debug}, ${mux}>::${rustName}`, + }); + return [ + reaction("onResolve", "on_resolve"), + reaction("onReject", "on_reject"), + reaction("onResolveStream", "on_resolve_stream"), + reaction("onRejectStream", "on_reject_stream"), + ]; +} + +export const [onResolve, onReject, onResolveStream, onRejectStream] = reactions(false, false, false); +export const [onResolveTLS, onRejectTLS, onResolveStreamTLS, onRejectStreamTLS] = reactions(false, false, true); +export const [onResolveDebug, onRejectDebug, onResolveStreamDebug, onRejectStreamDebug] = reactions(true, false, false); +export const [onResolveDebugTLS, onRejectDebugTLS, onResolveStreamDebugTLS, onRejectStreamDebugTLS] = reactions( + true, + false, + true, +); +export const [onResolveMux, onRejectMux, onResolveStreamMux, onRejectStreamMux] = reactions(false, true, false); +export const [onResolveMuxTLS, onRejectMuxTLS, onResolveStreamMuxTLS, onRejectStreamMuxTLS] = reactions( + false, + true, + true, +); +export const [onResolveDebugMux, onRejectDebugMux, onResolveStreamDebugMux, onRejectStreamDebugMux] = reactions( + true, + true, + false, +); +export const [onResolveDebugMuxTLS, onRejectDebugMuxTLS, onResolveStreamDebugMuxTLS, onRejectStreamDebugMuxTLS] = + reactions(true, true, true); diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index a43b51b6261c..e4f9baf281f6 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -7,6 +7,7 @@ use bun_sys::FdExt as _; use bun_core::String as BunString; use bun_http_types::Method::Method; use bun_jsc::JsCell; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_uws::{self as uws, WebSocketUpgradeContext}; use crate::server::jsc::{self, JSGlobalObject, JSValue, JsResult}; @@ -274,7 +275,6 @@ mod NativePromiseContext { pub(super) use npc::take_held; } use crate::node::types::PathLikeExt as _; -use crate::server::jsc::CallFrame; use crate::server::{AnyRequestContext, FileResponseStream, HTTPStatusText, file_response_stream}; use crate::webcore::blob::BlobExt as _; use crate::webcore::{Blob, ReadableStream, body as Body}; @@ -478,16 +478,7 @@ bun_core::declare_scope!(ReadableStream, visible); macro_rules! ctx_log { ($($t:tt)*) => { bun_core::scoped_log!(RequestContext, $($t)*) }; } macro_rules! stream_log { ($($t:tt)*) => { bun_core::scoped_log!(ReadableStream, $($t)*) }; } -/// Per-monomorphization C-ABI shim table for the four promise-reaction host -/// fns. The value passed to `then_with_value` must be identical to the -/// `Bun__HTTPRequestContext*__on*` symbol that C++'s -/// `GlobalObject::promiseHandlerID` compares against. -/// -/// The `#[no_mangle]` exports cannot live on a generic fn, so they are -/// emitted as concrete wrappers by `request_ctx_exports!` below. The trait -/// impls — also emitted by that macro — point at those *exported* wrappers -/// (not the inner generic shims), so `Self::ON_RESOLVE` and the C++ side agree -/// on the function-pointer identity and `promiseHandlerID` resolves. +/// One instantiation's `Bun__HTTPRequestContext*__on*`, which C++'s `promiseHandlerID` knows by address. /// /// NOTE (layering): expressed as a trait (not inherent consts) so /// downstream `where`-clauses that already name it keep type-checking. @@ -498,83 +489,11 @@ pub(crate) trait RequestContextHostFns { const ON_REJECT_STREAM: bun_jsc::JSHostFn; } -// Plain safe Rust helpers — only ever called Rust→Rust by the `#[no_mangle]` -// ABI wrappers in `request_ctx_exports!`, so they need no `extern` ABI and -// have no caller preconditions (bodies use safe `opaque_deref`). The wrappers -// carry `#[bun_jsc::host_call]` for the C++-visible symbol. -fn host_on_resolve( - g: *mut JSGlobalObject, - f: *mut CallFrame, -) -> JSValue -where - ThisServer: ServerLike + 'static, -{ - // S008: `JSGlobalObject`/`CallFrame` are `opaque_ffi!` ZST handles — - // safe `*mut → &` via `opaque_deref` (JSC guarantees non-null/live). - let (g, f) = (bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(f)); - bun_jsc::to_js_host_fn_result( - g, - RequestContext::::on_resolve(g, f), - ) -} -fn host_on_reject( - g: *mut JSGlobalObject, - f: *mut CallFrame, -) -> JSValue -where - ThisServer: ServerLike + 'static, -{ - // S008: `JSGlobalObject`/`CallFrame` are `opaque_ffi!` ZST handles — - // safe `*mut → &` via `opaque_deref` (JSC guarantees non-null/live). - let (g, f) = (bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(f)); - bun_jsc::to_js_host_fn_result( - g, - RequestContext::::on_reject(g, f), - ) -} -fn host_on_resolve_stream( - g: *mut JSGlobalObject, - f: *mut CallFrame, -) -> JSValue -where - ThisServer: ServerLike + 'static, -{ - // S008: `JSGlobalObject`/`CallFrame` are `opaque_ffi!` ZST handles — - // safe `*mut → &` via `opaque_deref` (JSC guarantees non-null/live). - let (g, f) = (bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(f)); - bun_jsc::to_js_host_fn_result( - g, - RequestContext::::on_resolve_stream(g, f), - ) -} -fn host_on_reject_stream( - g: *mut JSGlobalObject, - f: *mut CallFrame, -) -> JSValue -where - ThisServer: ServerLike + 'static, -{ - // S008: `JSGlobalObject`/`CallFrame` are `opaque_ffi!` ZST handles — - // safe `*mut → &` via `opaque_deref` (JSC guarantees non-null/live). - let (g, f) = (bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(f)); - bun_jsc::to_js_host_fn_result( - g, - RequestContext::::on_reject_stream(g, f), - ) -} - impl RequestContextHostFns for RequestContext where ThisServer: ServerLike + 'static, { - // These consts must resolve to the *exported* `#[no_mangle]` symbols - // (`Bun__HTTPRequestContext*__on*`), not the inner generic - // `host_on_*::<..>` shims: the function-pointer value is what C++'s - // `GlobalObject::promiseHandlerID` compares against (ZigGlobalObject.cpp), - // and the exported wrapper has a different address from the generic it - // forwards to. We route through a const-fn lookup keyed on the - // (SSL, DEBUG, MUX) tuple so the blanket impl can name concrete exports. const ON_RESOLVE: bun_jsc::JSHostFn = exported_host_fns(SSL, DBG, MUX).0; const ON_REJECT: bun_jsc::JSHostFn = exported_host_fns(SSL, DBG, MUX).1; const ON_RESOLVE_STREAM: bun_jsc::JSHostFn = exported_host_fns(SSL, DBG, MUX).2; @@ -732,24 +651,23 @@ where )); } - pub(crate) fn on_resolve(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + pub(crate) fn on_resolve(cx: &mut JsScope<'_>, result: JsLocal<'_>, cell: JsLocal<'_>) { ctx_log!("onResolve"); - let arguments = callframe.arguments_as_array::<2>(); - let Some(ctx) = NativePromiseContext::take::(arguments[1]) else { + let global = cx.global(); + let result = result.to_js(); + let Some(ctx) = NativePromiseContext::take::(cell.to_js()) else { // A termination path (abort, end, upgrade) reclaimed the cell's // claim; the context may already be gone. - Self::discard_response_body(global, arguments[0]); - return Ok(JSValue::UNDEFINED); + Self::discard_response_body(global, result); + return; }; let ctx = RequestContextRef::adopt(ctx.as_ptr()); ctx.ctx().promise_cell.set(JSValue::ZERO); - let result = arguments[0]; result.ensure_still_alive(); ctx.ctx().handle_resolve(global, result); - Ok(JSValue::UNDEFINED) } /// Cancel the body stream of a Response the server will not transmit, unless a consumer reads it. @@ -1136,33 +1054,31 @@ where report_unhandled_error(vm, global, unhandled); } - pub(crate) fn on_reject(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + pub(crate) fn on_reject(cx: &mut JsScope<'_>, reason: JsLocal<'_>, cell: JsLocal<'_>) { ctx_log!("onReject"); - let arguments = callframe.arguments_as_array::<2>(); // Pass the rejection reason through verbatim (including `null` and // `undefined`) so `error()` sees the same value the already-settled // path delivers. Only an empty JSValue is normalized. - let err = if arguments[0].is_empty() { + let err = if reason.to_js().is_empty() { JSValue::UNDEFINED } else { - arguments[0] + reason.to_js() }; - let Some(ctx) = NativePromiseContext::take::(arguments[1]) else { + let Some(ctx) = NativePromiseContext::take::(cell.to_js()) else { // A termination path (abort, end, upgrade) reclaimed the cell's // claim; the context may already be gone. Only an upgrade leaves // the server wrapper in the cell to report the error through. - let server_js = NativePromiseContext::take_held(arguments[1]); + let server_js = NativePromiseContext::take_held(cell.to_js()); if !server_js.is_empty() { - Self::report_error_after_upgrade(global, server_js, err); + Self::report_error_after_upgrade(cx.global(), server_js, err); } - return Ok(JSValue::UNDEFINED); + return; }; let ctx = RequestContextRef::adopt(ctx.as_ptr()); ctx.ctx().promise_cell.set(JSValue::ZERO); ctx.ctx().handle_reject(err); - Ok(JSValue::UNDEFINED) } fn handle_reject(&self, value: JSValue) { @@ -3021,36 +2937,25 @@ where self.end_stream(self.should_close_connection()); } - pub(crate) fn on_resolve_stream( - _global: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { + pub(crate) fn on_resolve_stream(_: &mut JsScope<'_>, _: JsLocal<'_>, cell: JsLocal<'_>) { stream_log!("onResolveStream"); - let args = callframe.arguments(); - let Some(req) = NativePromiseContext::take::(args[args.len() - 1]) else { - return Ok(JSValue::UNDEFINED); + let Some(req) = NativePromiseContext::take::(cell.to_js()) else { + return; }; let req = RequestContextRef::adopt(req.as_ptr()); req.ctx().promise_cell.set(JSValue::ZERO); req.ctx().handle_resolve_stream(); - Ok(JSValue::UNDEFINED) } - pub(crate) fn on_reject_stream( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { + pub(crate) fn on_reject_stream(cx: &mut JsScope<'_>, err: JsLocal<'_>, cell: JsLocal<'_>) { stream_log!("onRejectStream"); - let args = callframe.arguments(); - let Some(req) = NativePromiseContext::take::(args[args.len() - 1]) else { - return Ok(JSValue::UNDEFINED); + let Some(req) = NativePromiseContext::take::(cell.to_js()) else { + return; }; - let err = args[0]; let req = RequestContextRef::adopt(req.as_ptr()); req.ctx().promise_cell.set(JSValue::ZERO); - req.ctx().handle_reject_stream(global_this, err); - Ok(JSValue::UNDEFINED) + req.ctx().handle_reject_stream(cx.global(), err.to_js()); } pub(crate) fn handle_reject_stream(&self, global_this: &JSGlobalObject, err: JSValue) { @@ -4536,113 +4441,68 @@ const MUX_REQUEST_BODY_PREALLOCATE_LENGTH: usize = 64 * 1024; /// Pause socket reads at this many unconsumed request-body bytes (two 512 KB uWS recv buffers). const REQUEST_BODY_HIGH_WATER_MARK: usize = 1024 * 1024; -// ─── per-monomorphization C-ABI exports ────────────────────────────────────── -// The exported symbol name is "Bun__HTTPRequestContext" + (debug ? "Debug" : "") -// + (mux ? "Mux" : "") + (ssl ? "TLS" : "") + "__on*". -// Rust generics cannot own `#[no_mangle]` symbols, so each of the 8 concrete -// instantiations × 4 callbacks is spelled out via `request_ctx_exports!`. The -// generic body lives on the `impl RequestContext` block above -// (`on_resolve` / `on_reject` / `on_resolve_stream` / `on_reject_stream`); each -// shim is the result-mapping (`JsResult` → raw `JSValue`, -// `.zero` on error) over the monomorphic associated fn. -macro_rules! request_ctx_exports { - ($( - ($srv:ty, $ssl:literal, $dbg:literal, $mux:literal) => - $on_resolve:ident, $on_reject:ident, $on_resolve_stream:ident, $on_reject_stream:ident - );* $(;)?) => {$( - // Named C-ABI symbols for the C++ side. The bodies forward to the - // generic `host_on_*` shims monomorphized at this tuple — `#[no_mangle]` - // pins the link name. - #[unsafe(no_mangle)] - #[bun_jsc::host_call] - pub(crate) fn $on_resolve(g: *mut JSGlobalObject, f: *mut CallFrame) -> JSValue { - host_on_resolve::<$srv, $ssl, $dbg, $mux>(g, f) - } - #[unsafe(no_mangle)] - #[bun_jsc::host_call] - pub(crate) fn $on_reject(g: *mut JSGlobalObject, f: *mut CallFrame) -> JSValue { - host_on_reject::<$srv, $ssl, $dbg, $mux>(g, f) - } - #[unsafe(no_mangle)] - #[bun_jsc::host_call] - pub(crate) fn $on_resolve_stream(g: *mut JSGlobalObject, f: *mut CallFrame) -> JSValue { - host_on_resolve_stream::<$srv, $ssl, $dbg, $mux>(g, f) - } - #[unsafe(no_mangle)] - #[bun_jsc::host_call] - pub(crate) fn $on_reject_stream(g: *mut JSGlobalObject, f: *mut CallFrame) -> JSValue { - host_on_reject_stream::<$srv, $ssl, $dbg, $mux>(g, f) - } - )* - - /// Map the `(SSL, DEBUG, MUX)` const-generic tuple to the concrete - /// `#[no_mangle]` promise-reaction exports above. Used by the blanket - /// `RequestContextHostFns` impl so `Self::ON_*` resolves to the *same* - /// address C++'s `GlobalObject::promiseHandlerID` compares against. - const fn exported_host_fns( - ssl: bool, - debug: bool, - mux: bool, - ) -> ( - bun_jsc::JSHostFn, - bun_jsc::JSHostFn, - bun_jsc::JSHostFn, - bun_jsc::JSHostFn, - ) { - match (ssl, debug, mux) { - $( - ($ssl, $dbg, $mux) => ( - $on_resolve, - $on_reject, - $on_resolve_stream, - $on_reject_stream, - ), - )* - } +/// `RequestContext.bind.ts` has one set of reactions for each instantiation. +const fn exported_host_fns( + ssl: bool, + debug: bool, + mux: bool, +) -> ( + bun_jsc::JSHostFn, + bun_jsc::JSHostFn, + bun_jsc::JSHostFn, + bun_jsc::JSHostFn, +) { + use crate::generated_bindgen::request_context::*; + match (ssl, debug, mux) { + (false, false, false) => ( + Bun__HTTPRequestContext__onResolve, + Bun__HTTPRequestContext__onReject, + Bun__HTTPRequestContext__onResolveStream, + Bun__HTTPRequestContext__onRejectStream, + ), + (true, false, false) => ( + Bun__HTTPRequestContextTLS__onResolve, + Bun__HTTPRequestContextTLS__onReject, + Bun__HTTPRequestContextTLS__onResolveStream, + Bun__HTTPRequestContextTLS__onRejectStream, + ), + (false, true, false) => ( + Bun__HTTPRequestContextDebug__onResolve, + Bun__HTTPRequestContextDebug__onReject, + Bun__HTTPRequestContextDebug__onResolveStream, + Bun__HTTPRequestContextDebug__onRejectStream, + ), + (true, true, false) => ( + Bun__HTTPRequestContextDebugTLS__onResolve, + Bun__HTTPRequestContextDebugTLS__onReject, + Bun__HTTPRequestContextDebugTLS__onResolveStream, + Bun__HTTPRequestContextDebugTLS__onRejectStream, + ), + (false, false, true) => ( + Bun__HTTPRequestContextMux__onResolve, + Bun__HTTPRequestContextMux__onReject, + Bun__HTTPRequestContextMux__onResolveStream, + Bun__HTTPRequestContextMux__onRejectStream, + ), + (true, false, true) => ( + Bun__HTTPRequestContextMuxTLS__onResolve, + Bun__HTTPRequestContextMuxTLS__onReject, + Bun__HTTPRequestContextMuxTLS__onResolveStream, + Bun__HTTPRequestContextMuxTLS__onRejectStream, + ), + (false, true, true) => ( + Bun__HTTPRequestContextDebugMux__onResolve, + Bun__HTTPRequestContextDebugMux__onReject, + Bun__HTTPRequestContextDebugMux__onResolveStream, + Bun__HTTPRequestContextDebugMux__onRejectStream, + ), + (true, true, true) => ( + Bun__HTTPRequestContextDebugMuxTLS__onResolve, + Bun__HTTPRequestContextDebugMuxTLS__onReject, + Bun__HTTPRequestContextDebugMuxTLS__onResolveStream, + Bun__HTTPRequestContextDebugMuxTLS__onRejectStream, + ), } - }; -} -request_ctx_exports! { - (crate::server::HTTPServer, false, false, false) => - Bun__HTTPRequestContext__onResolve, - Bun__HTTPRequestContext__onReject, - Bun__HTTPRequestContext__onResolveStream, - Bun__HTTPRequestContext__onRejectStream; - (crate::server::HTTPSServer, true, false, false) => - Bun__HTTPRequestContextTLS__onResolve, - Bun__HTTPRequestContextTLS__onReject, - Bun__HTTPRequestContextTLS__onResolveStream, - Bun__HTTPRequestContextTLS__onRejectStream; - (crate::server::DebugHTTPServer, false, true, false) => - Bun__HTTPRequestContextDebug__onResolve, - Bun__HTTPRequestContextDebug__onReject, - Bun__HTTPRequestContextDebug__onResolveStream, - Bun__HTTPRequestContextDebug__onRejectStream; - (crate::server::DebugHTTPSServer, true, true, false) => - Bun__HTTPRequestContextDebugTLS__onResolve, - Bun__HTTPRequestContextDebugTLS__onReject, - Bun__HTTPRequestContextDebugTLS__onResolveStream, - Bun__HTTPRequestContextDebugTLS__onRejectStream; - (crate::server::HTTPServer, false, false, true) => - Bun__HTTPRequestContextMux__onResolve, - Bun__HTTPRequestContextMux__onReject, - Bun__HTTPRequestContextMux__onResolveStream, - Bun__HTTPRequestContextMux__onRejectStream; - (crate::server::HTTPSServer, true, false, true) => - Bun__HTTPRequestContextMuxTLS__onResolve, - Bun__HTTPRequestContextMuxTLS__onReject, - Bun__HTTPRequestContextMuxTLS__onResolveStream, - Bun__HTTPRequestContextMuxTLS__onRejectStream; - (crate::server::DebugHTTPServer, false, true, true) => - Bun__HTTPRequestContextDebugMux__onResolve, - Bun__HTTPRequestContextDebugMux__onReject, - Bun__HTTPRequestContextDebugMux__onResolveStream, - Bun__HTTPRequestContextDebugMux__onRejectStream; - (crate::server::DebugHTTPSServer, true, true, true) => - Bun__HTTPRequestContextDebugMuxTLS__onResolve, - Bun__HTTPRequestContextDebugMuxTLS__onReject, - Bun__HTTPRequestContextDebugMuxTLS__onResolveStream, - Bun__HTTPRequestContextDebugMuxTLS__onRejectStream; } struct StreamPair<'a, ThisServer, const SSL: bool, const DBG: bool, const MUX: bool> { diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index 496cc20b61cd..c17524653b66 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -114,7 +114,7 @@ macro_rules! for_each_mux_app { // route setup, …) split out to keep this module declaration file readable. #[path = "server_body.rs"] -mod server_body; +pub(crate) mod server_body; pub(crate) use server_body::{GetOrStartLoadResult, ServePluginsCallback}; // ─── write_status ──────────────────────────────────────────────────────────── @@ -1491,14 +1491,11 @@ impl NewServer { &mut strong_promise, jsc::StrongOptional::empty(), )); - // `#[host_fn(export = …)]` emits its - // C-ABI shim as `__jsc_host_`; the export name - // is link-only. result.then2( global, strong_self, - node_http_response::__jsc_host_node_http_request_on_resolve, - node_http_response::__jsc_host_node_http_request_on_reject, + crate::generated_bindgen::node_http_response::Bun__NodeHTTPRequest__onResolve, + crate::generated_bindgen::node_http_response::Bun__NodeHTTPRequest__onReject, ); is_async = true; } @@ -4149,10 +4146,6 @@ impl AnyServer { pub(crate) fn set_max_headers_count(&self, max_headers_count: u32) { any_server_dispatch!(self, |s| s.set_max_headers_count(max_headers_count)) } - - pub(crate) fn get_all_closed_promise(&self, global: &JSGlobalObject) -> JSValue { - any_server_dispatch!(self, |s| s.get_all_closed_promise(global)) - } } // ─── http_server_agent ─────────────────────────────────────────────────────── diff --git a/src/runtime/server/server_body.bind.ts b/src/runtime/server/server_body.bind.ts new file mode 100644 index 000000000000..0964242ec25d --- /dev/null +++ b/src/runtime/server/server_body.bind.ts @@ -0,0 +1,7 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::server::server_body" }); + +const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +export const onResolvePlugins = b.fn({ ...reaction, export: "BunServe__onResolvePlugins" }); +export const onRejectPlugins = b.fn({ ...reaction, export: "BunServe__onRejectPlugins" }); diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index acca37b4ffcc..fa95ec648fe5 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -8,6 +8,9 @@ use crate::api::{SocketAddress, js_bundler as JSBundler}; use crate::bake::router as FrameworkRouter; use crate::bake::{self as bake}; use crate::generated_bindgen::server::ServerProtocol; +use crate::generated_bindgen::server_body::{ + BunServe__onRejectPlugins, BunServe__onResolvePlugins, +}; use crate::node::types::PathLikeExt as _; use crate::webcore::BlobExt; use crate::webcore::body::Value as BodyValue; @@ -26,8 +29,8 @@ use bun_jsc::Debugger::DebuggerId; use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope, JsWrapper}; use bun_jsc::uuid::UUID; use bun_jsc::{ - self as jsc, ArrayBuffer, CallFrame, GlobalRef, JSGlobalObject, JSPromise, JSValue, JsError, - JsResult, Node, StringJsc as _, VirtualMachine, host_fn, + self as jsc, ArrayBuffer, GlobalRef, JSGlobalObject, JSPromise, JSValue, JsError, JsResult, + Node, StringJsc as _, VirtualMachine, host_fn, }; use bun_paths as paths; use bun_ptr::{AsCtxPtr, RefPtr}; @@ -1046,7 +1049,7 @@ impl ServePlugins { match promise.status() { // promise not fulfilled yet jsc::js_promise::Status::Pending => { - // The reaction's ref, adopted by `on_resolve_impl`/`on_reject_impl`. + // The reaction's ref, adopted by `on_resolve_plugins`/`on_reject_plugins`. self.ref_(); let promise_value = promise.as_value(); if let ServePluginsState::Pending { @@ -1059,8 +1062,8 @@ impl ServePlugins { promise_value.then( global, std::ptr::from_mut::(self), - __jsc_host_on_resolve_impl, - __jsc_host_on_reject_impl, + BunServe__onResolvePlugins, + BunServe__onRejectPlugins, ); return Ok(()); } @@ -1167,34 +1170,34 @@ impl Drop for ServePlugins { } } -#[bun_jsc::host_fn(export = "BunServe__onResolvePlugins")] -fn on_resolve_impl(_global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn on_resolve_plugins( + _: &mut JsScope<'_>, + plugins_result: JsLocal<'_>, + plugins_js: JsLocal<'_>, +) { ctx_log!("onResolve"); - let [plugins_result, plugins_js] = callframe.arguments_as_array::<2>(); - let plugins = plugins_js.as_promise_ptr::(); + let plugins = plugins_js.to_js().as_promise_ptr::(); // SAFETY: `plugins` was heap-allocated and ref()'d before .then(); this adopts that ref. let _guard = unsafe { RefPtr::from_raw(plugins) }; - plugins_result.ensure_still_alive(); + plugins_result.to_js().ensure_still_alive(); // SAFETY: pointer was passed via .then() above unsafe { &mut *plugins }.handle_on_resolve(); - - Ok(JSValue::UNDEFINED) } -#[bun_jsc::host_fn(export = "BunServe__onRejectPlugins")] -fn on_reject_impl(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn on_reject_plugins( + cx: &mut JsScope<'_>, + error_js: JsLocal<'_>, + plugin_js: JsLocal<'_>, +) { ctx_log!("onReject"); - let [error_js, plugin_js] = callframe.arguments_as_array::<2>(); - let plugins = plugin_js.as_promise_ptr::(); + let plugins = plugin_js.to_js().as_promise_ptr::(); // SAFETY: `plugins` was heap-allocated and ref()'d before .then(); this adopts that ref. let _guard = unsafe { RefPtr::from_raw(plugins) }; // SAFETY: pointer was passed via .then() above - unsafe { &mut *plugins }.handle_on_reject(global, error_js); - - Ok(JSValue::UNDEFINED) + unsafe { &mut *plugins }.handle_on_reject(cx.global(), error_js.to_js()); } /// The `options` argument of `server.upgrade(request, options)`. Each property is read once. diff --git a/src/runtime/shell/ParsedShellScript.bind.ts b/src/runtime/shell/ParsedShellScript.bind.ts index 1e6c92eaace2..048e2a7a38c6 100644 --- a/src/runtime/shell/ParsedShellScript.bind.ts +++ b/src/runtime/shell/ParsedShellScript.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::shell::parsed_shell_script" }); + export const ParsedShellScript = b.class({ rustPath: "crate::shell::ParsedShellScript", memoryCost: true, @@ -11,3 +13,10 @@ export const ParsedShellScript = b.class({ setQuiet: { args: { quiet: b.bool.loose } }, }, }); + +export const createParsedShellScript = b.fn({ + args: { strings: b.any, templateArgs: b.any }, + returns: b.any, + capability: "js", + throws: true, +}); diff --git a/src/runtime/shell/ParsedShellScript.rs b/src/runtime/shell/ParsedShellScript.rs index 6aed82a53cf6..672daeed8477 100644 --- a/src/runtime/shell/ParsedShellScript.rs +++ b/src/runtime/shell/ParsedShellScript.rs @@ -4,8 +4,8 @@ use core::sync::atomic::Ordering; use bun_core::String as BunString; use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::{ - CallFrame, JSGlobalObject, JSPropertyIterator, JSPropertyIteratorOptions, JSValue, JsCell, - JsClass as _, JsResult, MarkedArgumentBuffer, + JSGlobalObject, JSPropertyIterator, JSPropertyIteratorOptions, JSValue, JsCell, JsClass as _, + JsResult, MarkedArgumentBuffer, }; use super::env_map::EnvMap; @@ -150,26 +150,27 @@ fn env_bytes(global: &JSGlobalObject, name: &str, str: &BunString) -> JsResult( + cx: &mut JsScope<'a>, + strings: JsLocal<'a>, + template_args: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + MarkedArgumentBuffer::new(|buf| { + create_parsed_shell_script_impl(global, strings.to_js(), template_args.to_js(), buf) + }) +} fn create_parsed_shell_script_impl( global: &JSGlobalObject, - callframe: &CallFrame, + string_args: JSValue, + template_args_js: JSValue, marked_argument_buffer: &mut MarkedArgumentBuffer, ) -> JsResult { // Box drops automatically on every early `return`/`?` below, // so no scopeguard is needed. let mut shargs: Box = ShellArgs::init(); - let arguments = callframe.arguments(); - if arguments.len() < 2 { - return Err(global.throw_not_enough_arguments("Bun.$", 2, arguments.len())); - } - let string_args = arguments[0]; - let template_args_js = arguments[1]; let mut template_args = template_args_js.array_iterator(global)?; // PERF: a stack-fallback allocation may be worth it — profile if hot. diff --git a/src/runtime/shell/mod.rs b/src/runtime/shell/mod.rs index 3c50cdb83912..7bd0d68e7cbf 100644 --- a/src/runtime/shell/mod.rs +++ b/src/runtime/shell/mod.rs @@ -17,8 +17,6 @@ #[path = "shell_body.rs"] pub(crate) mod shell_body; -// Codegen (`generated_js2native.rs`) addresses this as `crate::shell::shell::*`. -pub(crate) use shell_body as shell; // ─── submodules ────────────────────────────────────────────────────────────── #[path = "EnvMap.rs"] diff --git a/src/runtime/shell/shell_body.bind.ts b/src/runtime/shell/shell_body.bind.ts new file mode 100644 index 000000000000..492b27d29057 --- /dev/null +++ b/src/runtime/shell/shell_body.bind.ts @@ -0,0 +1,19 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::shell::shell_body::testing_apis" }); + +const template = { + args: { strings: b.any, templateArgs: b.any }, + returns: b.any, + capability: "js", + throws: true, +} as const; + +export const disabledOnThisPlatform = b.fn({ + args: { builtin: b.any }, + returns: b.bool, + capability: "js", + throws: true, +}); +export const shellLex = b.fn(template); +export const shellParse = b.fn(template); diff --git a/src/runtime/shell/shell_body.rs b/src/runtime/shell/shell_body.rs index 2bc925d7466b..6d0aafd46ce7 100644 --- a/src/runtime/shell/shell_body.rs +++ b/src/runtime/shell/shell_body.rs @@ -15,8 +15,7 @@ use bun_jsc::EncodedSliceJsc as _; use bun_jsc::StringJsc as _; use bun_jsc::bun_string_jsc; use bun_jsc::{ - self as jsc, CallFrame, JSArrayIterator, JSGlobalObject, JSValue, JsResult, - MarkedArgumentBuffer, + self as jsc, JSArrayIterator, JSGlobalObject, JSValue, JsResult, MarkedArgumentBuffer, }; use bun_simdutf_sys::simdutf; use bun_sys::{self as sys, SystemError}; @@ -658,74 +657,52 @@ impl<'a> ShellSrcBuilder<'a> { /// Used in JS tests, see `internal-for-testing.ts` and shell tests. pub(crate) mod testing_apis { use super::*; + use bun_jsc::bindgen::{JsLocal, JsScope}; - #[bun_jsc::host_fn] - pub(crate) fn disabled_on_this_platform( - global: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { + pub(crate) fn disabled_on_this_platform<'a>( + cx: &mut JsScope<'a>, + builtin: JsLocal<'a>, + ) -> JsResult { #[cfg(windows)] { - let _ = (global, callframe); - return Ok(JSValue::FALSE); + let _ = (cx, builtin); + return Ok(false); } #[cfg(not(windows))] { - // SAFETY: bun_vm() is non-null for a Bun-owned global. - let vm = global.bun_vm(); - let mut arguments = jsc::ArgumentsSlice::init(vm, callframe.arguments()); - let string: JSValue = match arguments.next_eat() { - Some(s) => s, - None => { - return Err(global.throw(format_args!( - "shellInternals.disabledOnPosix: expected 1 arguments, got 0" - ))); - } - }; - - let bunstr = string.to_bun_string(global)?; + let bunstr = builtin.to_js().to_bun_string(cx.global())?; let utf8str = bunstr.to_utf8(); for disabled in crate::shell::builtin::Kind::DISABLED_ON_POSIX { // `strum::IntoStaticStr` would yield the PascalCase variant name // ("Cp"), so use `Kind::as_str` for the lowercase name. if utf8str.slice() == disabled.as_str().as_bytes() { - return Ok(JSValue::TRUE); + return Ok(true); } } - Ok(JSValue::FALSE) + Ok(false) } } - /// Codegen (`generated_js2native.rs`) wraps this with `host_fn_result`, so we - /// expose the bare `JsHostFnZig` signature here and do the buffer scope inline. - pub(crate) fn shell_lex(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - MarkedArgumentBuffer::new(|buf| shell_lex_impl(global, callframe, buf)) + pub(crate) fn shell_lex<'a>( + cx: &mut JsScope<'a>, + strings: JsLocal<'a>, + template_args: JsLocal<'a>, + ) -> JsResult { + let global = cx.global(); + MarkedArgumentBuffer::new(|buf| { + shell_lex_impl(global, strings.to_js(), template_args.to_js(), buf) + }) } fn shell_lex_impl( global: &JSGlobalObject, - callframe: &CallFrame, + string_args: JSValue, + template_args_js: JSValue, marked_argument_buffer: &mut MarkedArgumentBuffer, ) -> JsResult { - // SAFETY: bun_vm() is non-null for a Bun-owned global. - let vm = global.bun_vm(); - let mut arguments = jsc::ArgumentsSlice::init(vm, callframe.arguments()); - let string_args: JSValue = match arguments.next_eat() { - Some(s) => s, - None => { - return Err(global.throw(format_args!("shell_parse: expected 2 arguments, got 0"))); - } - }; - let arena = Bump::new(); - let template_args_js: JSValue = match arguments.next_eat() { - Some(s) => s, - None => { - return Err(global.throw(format_args!("shell: expected 2 arguments, got 0"))); - } - }; let mut template_args = template_args_js.array_iterator(global)?; let mut jsstrings: Vec = Vec::with_capacity(4); @@ -774,33 +751,25 @@ pub(crate) mod testing_apis { /// Testing API: parse the shell template-string arguments and return the /// AST as a JSON string (or throw on a parse error). Arguments are pinned /// in a `MarkedArgumentBuffer` for GC safety while parsing. - pub(crate) fn shell_parse(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - MarkedArgumentBuffer::new(|buf| shell_parse_impl(global, callframe, buf)) + pub(crate) fn shell_parse<'a>( + cx: &mut JsScope<'a>, + strings: JsLocal<'a>, + template_args: JsLocal<'a>, + ) -> JsResult { + let global = cx.global(); + MarkedArgumentBuffer::new(|buf| { + shell_parse_impl(global, strings.to_js(), template_args.to_js(), buf) + }) } fn shell_parse_impl( global: &JSGlobalObject, - callframe: &CallFrame, + string_args: JSValue, + template_args_js: JSValue, marked_argument_buffer: &mut MarkedArgumentBuffer, ) -> JsResult { - // SAFETY: bun_vm() is non-null for a Bun-owned global. - let vm = global.bun_vm(); - let mut arguments = jsc::ArgumentsSlice::init(vm, callframe.arguments()); - let string_args: JSValue = match arguments.next_eat() { - Some(s) => s, - None => { - return Err(global.throw(format_args!("shell_parse: expected 2 arguments, got 0"))); - } - }; - let arena = Bump::new(); - let template_args_js: JSValue = match arguments.next_eat() { - Some(s) => s, - None => { - return Err(global.throw(format_args!("shell: expected 2 arguments, got 0"))); - } - }; let mut template_args = template_args_js.array_iterator(global)?; let mut jsstrings: Vec = Vec::with_capacity(4); let mut script: Vec = Vec::new(); @@ -850,6 +819,3 @@ pub(crate) mod testing_apis { bun_string_jsc::create_utf8_for_js(global, str.as_bytes()) } } -// `generated_js2native.rs` snake-cases `TestingAPIs` as `testing_ap_is` -// (the codegen splits on capitalisation runs). -pub(crate) use testing_apis as testing_ap_is; diff --git a/src/runtime/socket/Listener.bind.ts b/src/runtime/socket/Listener.bind.ts index 9b7297aa259e..160e124060f4 100644 --- a/src/runtime/socket/Listener.bind.ts +++ b/src/runtime/socket/Listener.bind.ts @@ -23,6 +23,10 @@ export const Listener = b.class({ }, }); +const fromOptions = { args: { options: b.any }, returns: b.any, capability: "js", throws: true } as const; +export const connect = b.fn({ ...fromOptions, rustPath: "crate::socket::Listener::connect" }); +export const listen = b.fn({ ...fromOptions, rustPath: "crate::socket::Listener::listen" }); + export const addServerName = b.fn({ args: { listener: Listener, hostname: b.string.utf8, tls: b.any }, capability: "js", diff --git a/src/runtime/socket/Listener.rs b/src/runtime/socket/Listener.rs index f35c0aaff4b5..c7e6dbe2f531 100644 --- a/src/runtime/socket/Listener.rs +++ b/src/runtime/socket/Listener.rs @@ -209,10 +209,10 @@ impl Listener { Ok(()) } - // Note: no #[bun_jsc::host_fn] — BunObject.rs::static_adapters owns the - // C-ABI shim (it extracts `opts` from the CallFrame and calls this directly). - pub(crate) fn listen(cx: &bun_jsc::JsThread<'_>, opts: JSValue) -> JsResult { + pub(crate) fn listen(cx: &mut JsScope<'_>, options: JsLocal<'_>) -> JsResult { log!("listen"); + let cx = &cx.js_thread(); + let opts = options.to_js(); if opts.is_empty_or_undefined_or_null() || opts.is_boolean() || !opts.is_object() { return Err(cx .global() @@ -934,10 +934,8 @@ impl Listener { self.poll_ref.with_mut(|p| p.unref(bun_io::js_vm_ctx())); } - // Note: no #[bun_jsc::host_fn] — BunObject.rs::static_adapters owns the - // C-ABI shim (it extracts `opts` from the CallFrame and calls this directly). - pub(crate) fn connect(cx: &bun_jsc::JsThread<'_>, opts: JSValue) -> JsResult { - Self::connect_inner(cx, None, None, opts) + pub(crate) fn connect(cx: &mut JsScope<'_>, options: JsLocal<'_>) -> JsResult { + Self::connect_inner(&cx.js_thread(), None, None, options.to_js()) } /// `Bun.connect`, and node:net's dial (which hands in the socket it already made). diff --git a/src/runtime/socket/UpgradedDuplex.bind.ts b/src/runtime/socket/UpgradedDuplex.bind.ts new file mode 100644 index 000000000000..5022b7091db4 --- /dev/null +++ b/src/runtime/socket/UpgradedDuplex.bind.ts @@ -0,0 +1,16 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::socket::upgraded_duplex" }); + +// The listeners that `get_js_handlers` makes. Each carries its `UpgradedDuplex` as function data. +const listener = { passCallee: true, capability: "js" } as const; + +export const onReceivedData = b.fn({ + ...listener, + export: "Bun__UpgradedDuplex__onReceivedData", + args: { data: b.any }, + length: 0, +}); +export const onEnd = b.fn({ ...listener, export: "Bun__UpgradedDuplex__onEnd" }); +export const onWritable = b.fn({ ...listener, export: "Bun__UpgradedDuplex__onWritable" }); +export const onCloseJS = b.fn({ ...listener, export: "Bun__UpgradedDuplex__onCloseJS" }); diff --git a/src/runtime/socket/UpgradedDuplex.rs b/src/runtime/socket/UpgradedDuplex.rs index 18592fb91a93..08841bdf3cbb 100644 --- a/src/runtime/socket/UpgradedDuplex.rs +++ b/src/runtime/socket/UpgradedDuplex.rs @@ -15,13 +15,15 @@ use core::cell::Cell; use core::ffi::{CStr, c_uint, c_void}; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{CallFrame, GlobalRef, JSGlobalObject, JSValue, JsCell, JsResult, host_fn}; +use bun_jsc::{GlobalRef, JSGlobalObject, JSValue, JsCell, JsResult, host_fn}; use bun_uws::{us_bun_verify_error_t, uws_callback}; use super::DuplexUpgradeContext; use super::ssl_wrapper::SSLWrapper; use crate::generated_bindgen::socket_body::js_TLSSocket; +use crate::generated_bindgen::upgraded_duplex as listeners; use crate::timer::{ElTimespec, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag}; bun_output::declare_scope!(UpgradedDuplex, visible); @@ -49,7 +51,7 @@ pub(crate) struct UpgradedDuplex { pub current_timeout: Cell, /// Transport bytes that arrived before the TLS engine existed. /// - /// `js_upgrade_duplex_to_tls` defers `start_tls` to an event-loop task (so + /// `upgrade_duplex_to_tls` defers `start_tls` to an event-loop task (so /// `on_open` cannot re-enter JS before the caller holds the handle), but the /// JS caller attaches its `data` listener as soon as that function returns. /// When both ends of a `duplexPair()` are wrapped in-process, the peer's @@ -81,7 +83,7 @@ pub(crate) struct CertError { type WrapperType = SSLWrapper<*mut UpgradedDuplex>; /// Server-side peer-certificate policy for a duplex TLS upgrade, resolved in -/// `js_upgrade_duplex_to_tls` and applied via `SSLWrapper::set_server_verify`. +/// `upgrade_duplex_to_tls` and applied via `SSLWrapper::set_server_verify`. /// Ignored for client upgrades. #[derive(Clone, Copy)] pub(crate) struct ServerVerify { @@ -512,7 +514,7 @@ impl UpgradedDuplex { js_wrapper, js_TLSSocket::duplex_on_data_set_cached, global, - __jsc_host_on_received_data, + listeners::Bun__UpgradedDuplex__onReceivedData, this_ptr, ), )?; @@ -524,7 +526,7 @@ impl UpgradedDuplex { js_wrapper, js_TLSSocket::duplex_on_end_set_cached, global, - __jsc_host_on_end, + listeners::Bun__UpgradedDuplex__onEnd, this_ptr, ), )?; @@ -536,7 +538,7 @@ impl UpgradedDuplex { js_wrapper, js_TLSSocket::duplex_on_writable_set_cached, global, - __jsc_host_on_writable, + listeners::Bun__UpgradedDuplex__onWritable, this_ptr, ), )?; @@ -548,7 +550,7 @@ impl UpgradedDuplex { js_wrapper, js_TLSSocket::duplex_on_close_set_cached, global, - __jsc_host_on_close_js, + listeners::Bun__UpgradedDuplex__onCloseJS, this_ptr, ), )?; @@ -787,71 +789,61 @@ impl Drop for UpgradedDuplex { } } -// SAFETY (all four host fns): the function data is the `*mut UpgradedDuplex` +// SAFETY (all four listeners): the function data is the `*mut UpgradedDuplex` // installed by `get_js_handlers`; `teardown` clears it before the storage is // freed, so a non-null data pointer is live for the call. -#[bun_jsc::host_fn] -fn on_received_data(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn on_received_data<'a>(cx: &mut JsScope<'a>, callee: JsLocal<'a>, data: JsLocal<'a>) { bun_output::scoped_log!(UpgradedDuplex, "onReceivedData"); - let function = frame.callee(); - let [data_arg] = frame.arguments_as_array::<1>(); + let global = cx.global(); + let data_arg = data.to_js(); - if let Some(self_ptr) = host_fn::get_function_data(function) { - // SAFETY: see host-fn note above. + if let Some(self_ptr) = host_fn::get_function_data(callee.to_js()) { + // SAFETY: see the note above. let this = unsafe { &*self_ptr.cast::() }; let _keep_alive = this.keep_alive(); - if frame.arguments_count() >= 1 { - if !this.origin.get().is_empty() { - if data_arg.is_empty_or_undefined_or_null() { - return Ok(JSValue::UNDEFINED); - } - if let Some(array_buffer) = data_arg.as_array_buffer(global) { - // yay we can read the data - let payload = array_buffer.slice(); - this.on_internal_receive_data(payload); - } else { - // node.js errors in this case with the same error, lets keep it consistent - let error_value = global - .err( - bun_jsc::ErrorCode::STREAM_WRAP, - format_args!("Stream has StringDecoder set or is in objectMode"), - ) - .to_js(); - error_value.ensure_still_alive(); - (this.handlers.on_error)(this.handlers.ctx, error_value); - } + if !this.origin.get().is_empty() { + if data_arg.is_empty_or_undefined_or_null() { + return; + } + if let Some(array_buffer) = data_arg.as_array_buffer(global) { + // yay we can read the data + let payload = array_buffer.slice(); + this.on_internal_receive_data(payload); + } else { + // node.js errors in this case with the same error, lets keep it consistent + let error_value = global + .err( + bun_jsc::ErrorCode::STREAM_WRAP, + format_args!("Stream has StringDecoder set or is in objectMode"), + ) + .to_js(); + error_value.ensure_still_alive(); + (this.handlers.on_error)(this.handlers.ctx, error_value); } } } - Ok(JSValue::UNDEFINED) } -#[bun_jsc::host_fn] -fn on_end(_global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn on_end(_: &mut JsScope<'_>, callee: JsLocal<'_>) { bun_output::scoped_log!(UpgradedDuplex, "onEnd"); - let function = frame.callee(); - if let Some(self_ptr) = host_fn::get_function_data(function) { - // SAFETY: see host-fn note above. + if let Some(self_ptr) = host_fn::get_function_data(callee.to_js()) { + // SAFETY: see the note above. let this = unsafe { &*self_ptr.cast::() }; let _keep_alive = this.keep_alive(); // Like node's JSStreamSocket. Ahead of staged bytes too: no handshake can complete after it. (this.handlers.on_end)(this.handlers.ctx); } - Ok(JSValue::UNDEFINED) } -#[bun_jsc::host_fn] -fn on_writable(_global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn on_writable(_: &mut JsScope<'_>, callee: JsLocal<'_>) { bun_output::scoped_log!(UpgradedDuplex, "onWritable"); - let function = frame.callee(); - - if let Some(self_ptr) = host_fn::get_function_data(function) { - // SAFETY: see host-fn note above. + if let Some(self_ptr) = host_fn::get_function_data(callee.to_js()) { + // SAFETY: see the note above. let this = unsafe { &*self_ptr.cast::() }; let _keep_alive = this.keep_alive(); // flush pending data @@ -861,24 +853,17 @@ fn on_writable(_global: &JSGlobalObject, frame: &CallFrame) -> JsResult // call onWritable (will flush on demand) (this.handlers.on_writable)(this.handlers.ctx); } - - Ok(JSValue::UNDEFINED) } -#[bun_jsc::host_fn] -fn on_close_js(_global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn on_close_js(_: &mut JsScope<'_>, callee: JsLocal<'_>) { bun_output::scoped_log!(UpgradedDuplex, "onCloseJS"); - let function = frame.callee(); - - if let Some(self_ptr) = host_fn::get_function_data(function) { - // SAFETY: see host-fn note above. + if let Some(self_ptr) = host_fn::get_function_data(callee.to_js()) { + // SAFETY: see the note above. let this = unsafe { &*self_ptr.cast::() }; // flush pending data this.close(); } - - Ok(JSValue::UNDEFINED) } // ────────────────────────────────────────────────────────────────────────── diff --git a/src/runtime/socket/mod.rs b/src/runtime/socket/mod.rs index 392833229e83..aafa386a218e 100644 --- a/src/runtime/socket/mod.rs +++ b/src/runtime/socket/mod.rs @@ -94,19 +94,6 @@ pub(crate) use windows_named_pipe_context::WindowsNamedPipeContext; pub(crate) use udp_socket::UDPSocket; -/// Codegen path alias. -/// -/// `generated_js2native.rs` lowers `$rust(socket.rs, fnName)` to -/// `crate::socket::socket::fn_name(...)` (one path segment per directory plus -/// the file stem). The Rust port placed the bodies in `socket_body.rs` to keep -/// `mod.rs` as the wiring layer, so re-export the js2native entry points under -/// the name the generator expects rather than special-casing the generator. -pub(crate) mod socket { - pub(crate) use super::socket_body::{ - js_create_socket_pair, js_set_socket_options, js_upgrade_duplex_to_tls, testing_ap_is, - }; -} - // ─── RawSocketEvents glue ──────────────────────────────────────────────────── // `uws_handlers::RawSocketEvents` is the raw-pointer dispatch trait the // vtable layer requires of `api::NewSocket` (routed via `RawPtrHandler`). The diff --git a/src/runtime/socket/socket_body.bind.ts b/src/runtime/socket/socket_body.bind.ts index 97d4ea9c4c1e..4fc4fc5dde33 100644 --- a/src/runtime/socket/socket_body.bind.ts +++ b/src/runtime/socket/socket_body.bind.ts @@ -107,3 +107,28 @@ export const checkServerIdentityInHandshake = b.fn({ args: { socket: SocketHandl export const firstFlightBeforeFin = b.fn({ args: { socket: SocketHandle } }); export const isNamedPipeSocket = b.fn({ args: { socket: SocketHandle }, returns: b.bool }); export const getBufferedAmount = b.fn({ args: { socket: SocketHandle }, returns: b.usize }); +export const upgradeDuplexToTLS = b.fn({ ...method, args: { duplex: b.any, options: b.any } }); + +// `bun:internal-for-testing` +export const createSocketPair = b.fn({ ...js, returns: b.array(b.i32) }); +export const setSocketOptions = b.fn({ ...js, args: { socket: b.any, option: b.any, size: b.any } }); +const testing = (name: string) => `crate::socket::socket_body::testing_apis::${name}`; +export const socketFaultInjectionAvailable = b.fn({ + returns: b.bool, + rustPath: testing("socket_fault_injection_available"), +}); +export const namedPipeContextLiveCount = b.fn({ + returns: b.usize, + rustPath: testing("named_pipe_context_live_count"), +}); +export const duplexUpgradeContextLiveCount = b.fn({ + returns: b.usize, + rustPath: testing("duplex_upgrade_context_live_count"), +}); +export const clearSocketFaults = b.fn({ ...js, rustPath: testing("clear_socket_faults") }); +export const setSocketFault = b.fn({ + ...js, + args: { rule: b.any }, + returns: b.bool, + rustPath: testing("set_socket_fault"), +}); diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index feb11b8d54c6..384a940241de 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -19,7 +19,7 @@ use bun_boringssl_sys::SSL_CTX; use bun_collections::VecExt; use bun_core::{self, fmt as bun_fmt}; use bun_jsc::SysErrorJsc; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsRef, JsResult, SystemError}; +use bun_jsc::{self as jsc, JSGlobalObject, JSValue, JsRef, JsResult, SystemError}; // `bun_jsc::VirtualMachine` is the *module* (alias of `virtual_machine`); name the // struct directly so `VirtualMachine::get()` resolves as an associated fn. use super::upgraded_duplex::{Handlers as UpgradedDuplexHandlers, UpgradedDuplex}; @@ -4190,7 +4190,7 @@ impl DuplexUpgradeContext { // `has_handlers() == false` in `onOpen`. // // Refcount: `tls.socket` is `InternalSocket::UpgradedDuplex` - // here (assigned in `js_upgrade_duplex_to_tls` *before* + // here (assigned in `upgrade_duplex_to_tls` *before* // `start_tls()` enqueues anything and before any duplex // callback can dispatch), so `handle_connect_error`'s // `needs_deref = !is_detached()` is `true` and it consumes @@ -4337,7 +4337,7 @@ impl DuplexUpgradeContext { } /// process-lifetime per-thread singleton stored at construction - /// (`js_upgrade_duplex_to_tls`); `event_loop_mut()` is the safe accessor. + /// (`upgrade_duplex_to_tls`); `event_loop_mut()` is the safe accessor. /// Takes `ThisPtr` so the queued task carries the allocation's root /// pointer (the task's `run_event` may free it). #[inline] @@ -4358,7 +4358,7 @@ impl DuplexUpgradeContext { /// # Safety /// `this` must be the unique live pointer to the heap allocation produced - /// in `js_upgrade_duplex_to_tls`, with a ref count of zero. Frees the + /// in `upgrade_duplex_to_tls`, with a ref count of zero. Frees the /// allocation; callers must not /// hold a `&`/`&mut Self` across this call (taking `&mut self` here would /// be a Stacked Borrows protector violation when the backing `Box` is @@ -4378,7 +4378,7 @@ impl DuplexUpgradeContext { // `UpgradedDuplex` cleanup // runs via `Drop` when `heap::take(this)` frees the containing // struct below; an explicit call here would double-free. - // SAFETY: heap-allocated in `js_upgrade_duplex_to_tls`; this is the + // SAFETY: heap-allocated in `upgrade_duplex_to_tls`; this is the // matching free. No `&`/`&mut Self` survives past this point. drop(unsafe { bun_core::heap::take(this) }); } @@ -4429,19 +4429,18 @@ pub(crate) fn first_flight_before_fin(socket: SocketHandle<'_>) { } } -#[bun_jsc::host_fn] -pub(crate) fn js_upgrade_duplex_to_tls( - global: &JSGlobalObject, - callframe: &CallFrame, +pub(crate) fn upgrade_duplex_to_tls<'a>( + cx: &mut JsScope<'a>, + duplex: JsLocal<'a>, + options: JsLocal<'a>, ) -> JsResult { jsc::mark_binding!(); + let global = cx.global(); // The upgraded socket is the calling script's. - let context = global.bun_vm().context_of_caller(callframe); + let context = global.bun_vm().context_of_caller_no_frame(); - let [duplex, opts] = callframe.arguments_as_array::<2>(); - if callframe.arguments_count() < 2 { - return Err(global.throw(format_args!("Expected 2 arguments"))); - } + let duplex = duplex.to_js(); + let opts = options.to_js(); // TODO: do better type checking if duplex.is_empty_or_undefined_or_null() { return Err(global.throw(format_args!("Expected a Duplex instance"))); @@ -4728,12 +4727,9 @@ pub(crate) fn get_buffered_amount(socket: SocketHandle<'_>) -> usize { } } -#[bun_jsc::host_fn] -pub(crate) fn js_create_socket_pair( - global: &JSGlobalObject, - _frame: &CallFrame, -) -> JsResult { +pub(crate) fn create_socket_pair(cx: &mut JsScope<'_>) -> JsResult<[c_int; 2]> { jsc::mark_binding!(); + let global = cx.global(); #[cfg(windows)] { @@ -4754,10 +4750,7 @@ pub(crate) fn js_create_socket_pair( let _ = sys::update_nonblocking(sys::Fd::from_native(fds_[0]), true); let _ = sys::update_nonblocking(sys::Fd::from_native(fds_[1]), true); - let array = JSValue::create_empty_array(global, 2)?; - array.put_index(global, 0, JSValue::js_number(fds_[0] as f64))?; - array.put_index(global, 1, JSValue::js_number(fds_[1] as f64))?; - Ok(array) + Ok(fds_) } } @@ -4769,18 +4762,14 @@ unsafe extern "C" { /// `setSocketOptions(socket, 1 | 2, size)` of `bun:internal-for-testing`: SO_SNDBUF (1) or /// SO_RCVBUF (2). -#[bun_jsc::host_fn] -pub(crate) fn js_set_socket_options( - global: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let arguments = callframe.arguments(); - - if arguments.len() < 3 { - return Err(global.throw_not_enough_arguments("setSocketOptions", 3, arguments.len())); - } - - let target = arguments[0]; +pub(crate) fn set_socket_options<'a>( + cx: &mut JsScope<'a>, + socket: JsLocal<'a>, + option: JsLocal<'a>, + size: JsLocal<'a>, +) -> JsResult<()> { + let global = cx.global(); + let target = socket.to_js(); let socket = if let Some(socket) = target.as_class_ref::() { socket.socket.get().socket.get() } else if let Some(socket) = target.as_class_ref::() { @@ -4794,7 +4783,7 @@ pub(crate) fn js_set_socket_options( return Err(global.throw(format_args!("Expected a connected socket"))); }; - let is_recv = match arguments[1].to_int32() { + let is_recv = match option.to_js().to_int32() { 1 => false, 2 => true, _ => { @@ -4807,78 +4796,61 @@ pub(crate) fn js_set_socket_options( let mut value: c_int = 0; let res = uws::us_socket_t::opaque_mut(socket).buffer_size( is_recv, - arguments[2].to_int32(), + size.to_js().to_int32(), &mut value, ); if let Some(err) = get_us_error::(res, sys::Tag::setsockopt) { return Err(global.throw_value(err.to_js(global))); } - Ok(JSValue::UNDEFINED) + Ok(()) } pub(crate) mod testing_apis { use super::*; - #[bun_jsc::host_fn] - pub(crate) fn js_socket_fault_injection_available( - _global: &JSGlobalObject, - _frame: &CallFrame, - ) -> JsResult { - Ok(JSValue::from(cfg!(socket_fault_injection))) + pub(crate) fn socket_fault_injection_available() -> bool { + cfg!(socket_fault_injection) } /// Live `WindowsNamedPipeContext`s (one per connected or accepted socket /// over a Windows named pipe); always 0 elsewhere. - #[bun_jsc::host_fn] - pub(crate) fn js_named_pipe_context_live_count( - _global: &JSGlobalObject, - _frame: &CallFrame, - ) -> JsResult { + pub(crate) fn named_pipe_context_live_count() -> usize { #[cfg(windows)] - let count = crate::socket::windows_named_pipe_context::LIVE_COUNT - .load(core::sync::atomic::Ordering::Relaxed); + { + crate::socket::windows_named_pipe_context::LIVE_COUNT + .load(core::sync::atomic::Ordering::Relaxed) + } #[cfg(not(windows))] - let count = 0usize; - Ok(JSValue::js_number(count as f64)) + { + 0 + } } /// Live `DuplexUpgradeContext`s (one per TLS socket over a JS stream). - #[bun_jsc::host_fn] - pub(crate) fn js_duplex_upgrade_context_live_count( - _global: &JSGlobalObject, - _frame: &CallFrame, - ) -> JsResult { - let count = DUPLEX_UPGRADE_CONTEXT_LIVE_COUNT.load(core::sync::atomic::Ordering::Relaxed); - Ok(JSValue::js_number(count as f64)) + pub(crate) fn duplex_upgrade_context_live_count() -> usize { + DUPLEX_UPGRADE_CONTEXT_LIVE_COUNT.load(core::sync::atomic::Ordering::Relaxed) } - #[bun_jsc::host_fn] - pub(crate) fn js_clear_socket_faults( - global: &JSGlobalObject, - _frame: &CallFrame, - ) -> JsResult { + pub(crate) fn clear_socket_faults(cx: &mut JsScope<'_>) -> JsResult<()> { #[cfg(socket_fault_injection)] { - let _ = global; + let _ = cx; bun_uws_sys::fault_inject::us_fault_clear_all(); - Ok(JSValue::UNDEFINED) + Ok(()) } #[cfg(not(socket_fault_injection))] - Err(global.throw(format_args!( + Err(cx.global().throw(format_args!( "socket fault injection was not compiled into this build (build with --socket-fault-injection=on)" ))) } - #[bun_jsc::host_fn] - pub(crate) fn js_set_socket_fault( - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { + pub(crate) fn set_socket_fault<'a>(cx: &mut JsScope<'a>, rule: JsLocal<'a>) -> JsResult { jsc::mark_binding!(); + let global = cx.global(); #[cfg(not(socket_fault_injection))] { - let _ = frame; + let _ = rule; return Err(global.throw(format_args!( "socket fault injection was not compiled into this build (build with --socket-fault-injection=on)" ))); @@ -4887,7 +4859,7 @@ pub(crate) mod testing_apis { { use bun_uws_sys::fault_inject as fi; - let [opts] = frame.arguments_as_array::<1>(); + let opts = rule.to_js(); if !opts.is_object() { return Err(global.throw_invalid_argument_type_value("rule", "object", opts)); } @@ -5050,7 +5022,7 @@ pub(crate) mod testing_apis { // SAFETY: rule is a valid stack pointer for the duration of the call. unsafe { fi::us_fault_set(syscall, &rule) }; - Ok(JSValue::TRUE) + Ok(true) } } @@ -5098,4 +5070,3 @@ pub(crate) mod testing_apis { None } } -pub(crate) use testing_apis as testing_ap_is; diff --git a/src/runtime/socket/udp_socket.bind.ts b/src/runtime/socket/udp_socket.bind.ts index 5d1edfc3e654..8c00adc59585 100644 --- a/src/runtime/socket/udp_socket.bind.ts +++ b/src/runtime/socket/udp_socket.bind.ts @@ -54,6 +54,13 @@ export const UDPSocket = b.class({ }, }); +export const udpSocket = b.fn({ + ...js, + args: { options: b.any }, + returns: b.any, + rustPath: "crate::socket::udp_socket::UDPSocket::udp_socket", +}); + export const connect = b.fn({ args: { socket: UDPSocket, host: b.string.loose, port: b.i32 }, capability: "js", diff --git a/src/runtime/socket/udp_socket.rs b/src/runtime/socket/udp_socket.rs index 37fd2a3954b8..6cfc6508c651 100644 --- a/src/runtime/socket/udp_socket.rs +++ b/src/runtime/socket/udp_socket.rs @@ -565,8 +565,10 @@ impl UDPSocket { unsafe { &*user.cast::() } } - pub(crate) fn udp_socket(cx: &bun_jsc::JsThread<'_>, options: JSValue) -> JsResult { + pub(crate) fn udp_socket(cx: &mut JsScope<'_>, options: JsLocal<'_>) -> JsResult { bun_output::scoped_log!(UdpSocket, "udpSocket"); + let cx = &cx.js_thread(); + let options = options.to_js(); // What script of a disposed `Bun.ModuleGraph` opens is closed at once and reports nothing. // A socket that is closed from the queue can send before that: it is not bound at all. diff --git a/src/runtime/test_runner/DoneCallback.rs b/src/runtime/test_runner/DoneCallback.rs index ea0e6a5b8f79..cb6a1388b9e2 100644 --- a/src/runtime/test_runner/DoneCallback.rs +++ b/src/runtime/test_runner/DoneCallback.rs @@ -1,9 +1,10 @@ -use bun_jsc::{CallFrame, JSFunction, JSGlobalObject, JSValue, JsClass as _, JsResult}; +use bun_jsc::{JSFunction, JSGlobalObject, JSValue, JsClass as _, JsResult}; use bun_core::String as BunString; use bun_ptr::{JsCell, RefPtr}; use core::cell::Cell; -use crate::test_runner::bun_test::{group_begin, BunTest, RefData}; +use crate::generated_bindgen::bun_test::Bun__BunTest__bunTestDoneCallback; +use crate::test_runner::bun_test::{group_begin, RefData}; pub(crate) struct DoneCallback { /// Some = not called yet. None = done already called, no-op. @@ -29,25 +30,10 @@ impl DoneCallback { let call_fn = JSFunction::create( global, "done", - __jsc_host_bun_test_done_callback, + Bun__BunTest__bunTestDoneCallback, 1, Default::default(), ); call_fn.bind(global, value, &BunString::static_("done"), 1.0, &[]) } } - -// Raw C-ABI shim for [`BunTest::bun_test_done_callback`] so it can be passed -// as a `JSHostFn` pointer to `JSFunction::create` (the thunk routes the result through -// `to_js_host_fn_result` for `JsResult` → `JSValue` mapping + debug exception -// assertions). -bun_jsc::jsc_host_abi! { - unsafe fn __jsc_host_bun_test_done_callback( - g: *mut JSGlobalObject, - f: *mut CallFrame, - ) -> JSValue { - // SAFETY: JSC guarantees both pointers are live for the duration of the host call. - let (global, callframe) = unsafe { (&*g, &*f) }; - bun_jsc::to_js_host_fn_result(global, BunTest::bun_test_done_callback(global, callframe)) - } -} diff --git a/src/runtime/test_runner/ScopeFunctions.bind.ts b/src/runtime/test_runner/ScopeFunctions.bind.ts new file mode 100644 index 000000000000..5e9144114d7f --- /dev/null +++ b/src/runtime/test_runner/ScopeFunctions.bind.ts @@ -0,0 +1,13 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::test_runner::scope_functions" }); + +/** What `test`, `describe` and their modifiers are bound functions of: `this` is the `ScopeFunctions`. */ +export const callAsFunction = b.fn({ + export: "Bun__ScopeFunctions__callAsFunction", + passThis: true, + args: { description: b.any, callback: b.any, options: b.any }, + length: 1, + capability: "js", + throws: true, +}); diff --git a/src/runtime/test_runner/ScopeFunctions.rs b/src/runtime/test_runner/ScopeFunctions.rs index a8e36f0f9c57..da05abd4c3fc 100644 --- a/src/runtime/test_runner/ScopeFunctions.rs +++ b/src/runtime/test_runner/ScopeFunctions.rs @@ -2,10 +2,11 @@ use core::fmt; use crate::test_runner::expect::JSValueTestExt; use bun_jsc::bindgen::{JsLocal, JsScope}; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsClass, JsResult}; +use bun_jsc::{JSGlobalObject, JSValue, JsClass, JsResult}; use bun_core::String as BunString; use crate::generated_bindgen::jest::js_ScopeFunctions as js; +use crate::generated_bindgen::scope_functions::Bun__ScopeFunctions__callAsFunction; use crate::test_runner::bun_test::{self, BaseScopeCfg, BunTest, DescribeScope}; use crate::test_runner::bun_test::js_fns::Signature; use crate::test_runner::jest; @@ -119,14 +120,20 @@ impl ScopeFunctions { } } -#[bun_jsc::host_fn] -fn call_as_function(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn call_as_function( + cx: &mut JsScope<'_>, + this_value: JsLocal<'_>, + description: JsLocal<'_>, + callback: JsLocal<'_>, + options: JsLocal<'_>, +) -> JsResult<()> { let _g = group_log::begin(); - let Some(this) = frame.this().as_class_ref::() else { + let global = cx.global(); + let Some(this) = this_value.to_js().as_class_ref::() else { return Err(global.throw(format_args!("Expected callee to be ScopeFunctions"))); }; - let line_no = jest::capture_test_line_number(frame, global); + let line_no = jest::capture_test_line_number(global); let buntest_strong = bun_test::js_fns::clone_active_strong(global, Signature::ScopeFunctions(this))?; let bun_test_ptr = buntest_strong.get(); @@ -138,7 +145,7 @@ fn call_as_function(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult JsResult JsResult, description: Option<&[u8]>, options: &ParseArgumentsOptions, @@ -317,7 +321,6 @@ impl ScopeFunctions { -1 }; debugger.test_reporter_agent.report_test_found( - frame, id, &name, match self.mode { @@ -539,12 +542,10 @@ fn get_description( pub(crate) fn parse_arguments( global: &JSGlobalObject, - frame: &CallFrame, + [a1, a2, a3]: [JSValue; 3], signature: Signature, cfg: ParseArgumentsCfg, ) -> JsResult { - let [a1, a2, a3] = frame.arguments_as_array::<3>(); - #[derive(Copy, Clone)] enum Len { Three, Two, One, Zero } let len: Len = if !a3.is_undefined_or_null() { @@ -731,10 +732,7 @@ fn create_unbound(global: &JSGlobalObject, mode: Mode, each: JSValue, cfg: BaseS } fn bind(value: JSValue, global: &JSGlobalObject, name: &'static str) -> JsResult { - // `#[bun_jsc::host_fn]` on `call_as_function` emits the C-ABI thunk - // `__jsc_host_call_as_function`; `JSFunction::create` wants the raw - // `JSHostFn` shape, not the safe Rust signature. - let call_fn = bun_jsc::JSFunction::create(global, name, __jsc_host_call_as_function, 1, Default::default()); + let call_fn = bun_jsc::JSFunction::create(global, name, Bun__ScopeFunctions__callAsFunction, 1, Default::default()); let bound = JSValueTestExt::bind(call_fn, global, value, &BunString::static_(name), 1.0, &[])?; set_prototype_direct(bound, value.get_prototype(global)?, global)?; Ok(bound) diff --git a/src/runtime/test_runner/bun_test.bind.ts b/src/runtime/test_runner/bun_test.bind.ts new file mode 100644 index 000000000000..07ff5685deb4 --- /dev/null +++ b/src/runtime/test_runner/bun_test.bind.ts @@ -0,0 +1,41 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::test_runner::bun_test::js_fns::generic_hook" }); + +const js = { capability: "js", throws: true } as const; + +/** The arguments come in any of the orders that `parse_arguments` takes. */ +const hook = (name: string) => + b.fn({ + export: `Bun__BunTest__${name}`, + args: { description: b.any, callback: b.any, options: b.any }, + length: 1, + ...js, + }); +export const beforeAll = hook("beforeAll"); +export const beforeEach = hook("beforeEach"); +export const afterEach = hook("afterEach"); +export const afterAll = hook("afterAll"); +export const onTestFinished = hook("onTestFinished"); + +/** The reactions to the promise that a test or a hook returns. `context` is its `RefData`. */ +const reaction = { args: { value: b.any, context: b.any }, ...js } as const; +export const bunTestThen = b.fn({ + ...reaction, + export: "Bun__TestScope__Describe2__bunTestThen", + rustPath: "crate::test_runner::bun_test::BunTest::bun_test_then", +}); +export const bunTestCatch = b.fn({ + ...reaction, + export: "Bun__TestScope__Describe2__bunTestCatch", + rustPath: "crate::test_runner::bun_test::BunTest::bun_test_catch", +}); + +/** What the `done` of a test callback is a bound function of. `this` is the `DoneCallback`. */ +export const bunTestDoneCallback = b.fn({ + export: "Bun__BunTest__bunTestDoneCallback", + rustPath: "crate::test_runner::bun_test::BunTest::bun_test_done_callback", + passThis: true, + args: { error: b.any }, + ...js, +}); diff --git a/src/runtime/test_runner/bun_test.rs b/src/runtime/test_runner/bun_test.rs index b053910766b3..a80636f1950a 100644 --- a/src/runtime/test_runner/bun_test.rs +++ b/src/runtime/test_runner/bun_test.rs @@ -5,13 +5,17 @@ use std::rc::{Rc, Weak}; use bun_collections::LinearFifo; use bun_core::{Output, Timespec}; -use bun_jsc::{self as jsc, CallFrame, GlobalRef, JSGlobalObject, JSValue, JsResult, Strong}; +use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::{self as jsc, GlobalRef, JSGlobalObject, JSValue, JsResult, Strong}; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::js_promise::Status as PromiseStatus; use bun_ptr::RefPtr; use super::jest::{Jest, FileId, FileColumns as _}; use crate::timer::{EventLoopTimer, EventLoopTimerState, EventLoopTimerTag, ElTimespec}; use crate::cli::test_command::CommandLineReporter; +use crate::generated_bindgen::bun_test::{ + Bun__TestScope__Describe2__bunTestCatch, Bun__TestScope__Describe2__bunTestThen, +}; use super::execution::TimespecExt as _; bun_core::declare_scope!(bun_test_group, hidden); @@ -121,7 +125,7 @@ pub(crate) mod js_fns { /// Tags accepted by `generic_hook`. Superset of `DescribeScope::HookTag` /// (adds `OnTestFinished`). // was a const-generic param (`adt_const_params` is unstable); - // reshaped to runtime dispatch with per-tag thin host_fn wrappers below. + // reshaped to runtime dispatch with per-tag thin wrappers below. #[derive(Copy, Clone, PartialEq, Eq, strum::IntoStaticStr)] pub enum GenericHookTag { #[strum(serialize = "beforeAll")] @@ -158,24 +162,24 @@ pub(crate) mod js_fns { } // `adt_const_params` is unstable, so the body takes `tag` at runtime and - // 5 thin `#[host_fn]` wrappers below supply the per-tag entry points + // 5 thin wrappers below supply the per-tag entry points // (one fn per JS function so JSFunction::create gets a distinct address). pub(crate) fn generic_hook_impl( tag: GenericHookTag, global_this: &JSGlobalObject, - call_frame: &CallFrame, - ) -> JsResult { + arguments: [JSValue; 3], + ) -> JsResult<()> { let _g = group_begin!(); // Run the body in a closure so every error exit funnels through the // log below before the group guard closes. - let result = (|| -> JsResult { + let result = (|| -> JsResult<()> { let tag_name: &'static str = tag.into(); let sig_bytes: &'static [u8] = tag.sig(); let args = ScopeFunctions::parse_arguments( global_this, - call_frame, + arguments, Signature::Str(sig_bytes), ScopeFunctions::ParseArgumentsCfg { callback: ScopeFunctions::CallbackMode::Require, kind: ScopeFunctions::FunctionKind::Hook }, )?; @@ -210,7 +214,7 @@ pub(crate) mod js_fns { BaseScopeCfg::default(), AddedInPhase::Preload, )?; - return Ok(JSValue::UNDEFINED); + return Ok(()); }; match bun_test.phase { @@ -228,7 +232,7 @@ pub(crate) mod js_fns { BaseScopeCfg::default(), AddedInPhase::Collection, )?; - Ok(JSValue::UNDEFINED) + Ok(()) } Phase::Execution => { let active = bun_test.get_current_state_data(); @@ -310,7 +314,7 @@ pub(crate) mod js_fns { } bun_test.extra_execution_entries.push(new_item_ptr); - Ok(JSValue::UNDEFINED) + Ok(()) } Phase::Done => Err(global_this.throw(format_args!( "Cannot call {}() after the test run has completed", @@ -324,18 +328,23 @@ pub(crate) mod js_fns { result } - /// Per-tag `#[host_fn]` entry points (one fn per JS function so + /// Per-tag entry points (one fn per JS function so /// `JSFunction::create` gets a distinct address). pub(crate) mod generic_hook { use super::*; macro_rules! hook { ($name:ident, $tag:ident) => { - #[bun_jsc::host_fn] pub(crate) fn $name( - global_this: &JSGlobalObject, - call_frame: &CallFrame, - ) -> JsResult { - super::generic_hook_impl(GenericHookTag::$tag, global_this, call_frame) + cx: &mut JsScope<'_>, + description: JsLocal<'_>, + callback: JsLocal<'_>, + options: JsLocal<'_>, + ) -> JsResult<()> { + super::generic_hook_impl( + GenericHookTag::$tag, + cx.global(), + [description.to_js(), callback.to_js(), options.to_js()], + ) } }; } @@ -738,12 +747,12 @@ impl BunTest { fn bun_test_then_or_catch( global_this: &JSGlobalObject, - callframe: &CallFrame, + result: JSValue, + this_ptr: JSValue, is_catch: bool, ) -> JsResult<()> { let _g = group_begin!(); - let [result, this_ptr] = callframe.arguments_as_array::<2>(); if this_ptr.is_empty_or_undefined_or_null() { return Ok(()); } @@ -782,32 +791,35 @@ impl BunTest { Ok(()) } - // `#[bun_jsc::host_fn]` proc-macro emits a free-fn wrapper that - // calls the annotated item by bare name; that lookup fails for associated - // fns inside `impl` blocks. The extern-"C" trampoline is wired separately - // (see the gated `Bun__TestScope__Describe2__*` statics below), so the - // attribute is dropped here — these are plain JsResult fns. - fn bun_test_then(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - Self::bun_test_then_or_catch(global_this, callframe, false)?; - Ok(JSValue::UNDEFINED) + pub(crate) fn bun_test_then( + cx: &mut JsScope<'_>, + value: JsLocal<'_>, + context: JsLocal<'_>, + ) -> JsResult<()> { + Self::bun_test_then_or_catch(cx.global(), value.to_js(), context.to_js(), false) } - fn bun_test_catch(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - Self::bun_test_then_or_catch(global_this, callframe, true)?; - Ok(JSValue::UNDEFINED) + pub(crate) fn bun_test_catch( + cx: &mut JsScope<'_>, + value: JsLocal<'_>, + context: JsLocal<'_>, + ) -> JsResult<()> { + Self::bun_test_then_or_catch(cx.global(), value.to_js(), context.to_js(), true) } pub(crate) fn bun_test_done_callback( - global_this: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { + cx: &mut JsScope<'_>, + this_value: JsLocal<'_>, + error: JsLocal<'_>, + ) -> JsResult<()> { let _g = group_begin!(); - let Some(this) = callframe.this().as_class_ref::() else { + let global_this = cx.global(); + let Some(this) = this_value.to_js().as_class_ref::() else { return Err(global_this.throw(format_args!("Expected callee to be DoneCallback"))); }; - let [value] = callframe.arguments_as_array::<1>(); + let value = error.to_js(); let was_error = !value.is_empty_or_undefined_or_null(); if this.called.get() { @@ -822,7 +834,7 @@ impl BunTest { } this.called.set(true); let Some(ref_in) = this.r#ref.take() else { - return Ok(JSValue::UNDEFINED); + return Ok(()); }; // dupe the ref and enqueue a task to call the done callback. @@ -832,18 +844,18 @@ impl BunTest { let should_run = has_one_ref || was_error; if !should_run { - return Ok(JSValue::UNDEFINED); + return Ok(()); } let Some(strong): Option = ref_in.buntest_weak.upgrade() else { - return Ok(JSValue::UNDEFINED); + return Ok(()); }; // SAFETY: `&mut` derived via `UnsafeCell`; borrow ends before // `run_next_tick` re-derives. strong.get().add_result(ref_in.phase); Self::run_next_tick(&ref_in.buntest_weak, global_this, ref_in.phase); - Ok(JSValue::UNDEFINED) + Ok(()) } pub(crate) fn bun_test_timeout_callback( @@ -1387,35 +1399,6 @@ impl Drop for BunTest { } } -// `ZigGlobalObject::promiseHandlerID` (C++) compares the fn-ptr passed to -// `JSValue::then` against `&Bun__TestScope__Describe2__bunTestThen` by -// identity, so the Rust thunk MUST be the symbol itself — exporting a -// `static JSHostFn = thunk` puts the name in `.data` (nm `d`), and the address -// C++ sees never matches the local thunk we hand to `.then()`, tripping the -// `RELEASE_ASSERT_NOT_REACHED` at the bottom of `promiseHandlerID`. -bun_jsc::jsc_host_abi! { - #[unsafe(no_mangle)] - pub unsafe fn Bun__TestScope__Describe2__bunTestThen( - global: *mut JSGlobalObject, - frame: *mut CallFrame, - ) -> JSValue { - // SAFETY: JSC passes non-null live pointers for both. - let (global, frame) = unsafe { (&*global, &*frame) }; - jsc::host_fn::to_js_host_fn_result(global, BunTest::bun_test_then(global, frame)) - } -} -bun_jsc::jsc_host_abi! { - #[unsafe(no_mangle)] - pub unsafe fn Bun__TestScope__Describe2__bunTestCatch( - global: *mut JSGlobalObject, - frame: *mut CallFrame, - ) -> JSValue { - // SAFETY: JSC passes non-null live pointers for both. - let (global, frame) = unsafe { (&*global, &*frame) }; - jsc::host_fn::to_js_host_fn_result(global, BunTest::bun_test_catch(global, frame)) - } -} - // Clone/Copy: bitwise OK — `entry` is a non-owning erased borrow of an // `ExecutionEntry` owned by `BunTest::execution`. #[derive(Copy, Clone)] diff --git a/src/runtime/test_runner/expect.bind.ts b/src/runtime/test_runner/expect.bind.ts index b2673e3e68e2..ae5b59ee06b1 100644 --- a/src/runtime/test_runner/expect.bind.ts +++ b/src/runtime/test_runner/expect.bind.ts @@ -190,6 +190,17 @@ export const Expect = b.class({ }, }); +/** What `expect.extend()` wraps each matcher in. The callee holds the matcher. */ +export const applyCustomMatcher = b.fn({ + export: "Bun__Expect__applyCustomMatcher", + rustPath: "crate::test_runner::Expect::apply_custom_matcher", + passThis: true, + passCallee: true, + args: { args: b.any.variadic }, + returns: b.any, + ...js, +}); + /** What a matcher of `expect.extend()` makes when it is called on `expect` itself. */ export const ExpectCustomAsymmetricMatcher = b.class({ rustPath: "crate::test_runner::ExpectCustomAsymmetricMatcher", diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index c7efb52eaaf0..423603d29e4f 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -6,7 +6,7 @@ use bun_core::Output; use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsPayload as _, JsScope, JsWrapper}; use bun_jsc::bun_string_jsc; use bun_jsc::{ - CallFrame, JSGlobalObject, JSValue, JsError, JsResult, + JSGlobalObject, JSValue, JsError, JsResult, ConsoleObject, JSFunction, JSPropertyIterator, JSString, }; use bun_jsc::{JsClass as _, StringJsc as _}; @@ -22,8 +22,9 @@ use super::jest::Jest; use super::expect::{JSValueTestExt, FormatterTestExt, make_formatter}; use crate::expect_throw as throw; use crate::generated_bindgen::expect::{ - js_ExpectAny, js_ExpectArrayContaining, js_ExpectCloseTo, js_ExpectCustomAsymmetricMatcher, - js_ExpectObjectContaining, js_ExpectStringContaining, js_ExpectStringMatching, + Bun__Expect__applyCustomMatcher, js_ExpectAny, js_ExpectArrayContaining, js_ExpectCloseTo, + js_ExpectCustomAsymmetricMatcher, js_ExpectObjectContaining, js_ExpectStringContaining, + js_ExpectStringMatching, }; use bun_jsc::js_error_to_write_error; @@ -1303,19 +1304,6 @@ impl Expect { // Even though they point to the same native functions for all matchers, // multiple instances are created because each instance will hold the matcher_fn as a property - // `to_js_host_fn` returns an opaque closure, so emit an - // explicit C-ABI shim and pass its address. - bun_jsc::jsc_host_abi! { - unsafe fn __apply_custom_matcher_shim( - g: *mut bun_jsc::JSGlobalObject, - f: *mut bun_jsc::CallFrame, - ) -> JSValue { - // SAFETY: JSC guarantees both pointers are live for the call. - let (g, f) = unsafe { (&*g, &*f) }; - bun_jsc::to_js_host_fn_result(g, Expect::apply_custom_matcher(g, f)) - } - } - let host_fn_ptr: bun_jsc::JSHostFn = __apply_custom_matcher_shim; // SAFETY: FFI call with valid global, &bun_core::String, host-fn ptr, and JSValue. // C++ takes the function pointer **by value** (`NativeFunctionPtr`), not a // pointer-to-function-pointer — `JSHostFn` already is the @@ -1324,7 +1312,7 @@ impl Expect { Bun__JSWrappingFunction__create( global_this, &matcher_name, - host_fn_ptr, + Bun__Expect__applyCustomMatcher, matcher_fn, ) }; @@ -1470,13 +1458,19 @@ impl Expect { } /// Function that is run for either `expect.myMatcher()` call or `expect().myMatcher` call, - /// and we can known which case it is based on if the `callFrame.this()` value is an instance of Expect - pub(crate) fn apply_custom_matcher(global_this: &JSGlobalObject, call_frame: &CallFrame) -> JsResult { + /// and we can known which case it is based on if the `this` value is an instance of Expect + pub(crate) fn apply_custom_matcher( + cx: &mut JsScope<'_>, + this_value: JsLocal<'_>, + callee: JsLocal<'_>, + args: &[JsLocal<'_>], + ) -> JsResult { + let global_this = cx.global(); // SAFETY: bun_vm() returns the live VM pointer for this global. let _gc = global_this.bun_vm().as_mut().auto_gc_on_drop(); // retrieve the user-provided matcher function (matcher_fn) - let func: JSValue = call_frame.callee(); + let func: JSValue = callee.to_js(); let matcher_fn: JSValue = get_custom_matcher_fn(func, global_this).unwrap_or(JSValue::UNDEFINED); if !matcher_fn.js_type().is_function() { return Err(global_this.throw2( @@ -1487,10 +1481,16 @@ impl Expect { matcher_fn.ensure_still_alive(); // try to retrieve the Expect instance - let this_value: JSValue = call_frame.this(); + let this_value: JSValue = this_value.to_js(); + let args = JsLocal::to_js_slice(args); let Some(expect) = this_value.as_class_ref::() else { // if no Expect instance, assume it is a static call (`expect.myMatcher()`), so create an ExpectCustomAsymmetricMatcher instance - return ExpectCustomAsymmetricMatcher::create(global_this, call_frame, matcher_fn); + return ExpectCustomAsymmetricMatcher::create( + global_this, + this_value, + args, + matcher_fn, + ); }; // if we got an Expect instance, then it's a non-static call (`expect().myMatcher`), @@ -1524,7 +1524,6 @@ impl Expect { expect.increment_expect_call_counter(); // prepare the args array - let args = call_frame.arguments(); // MarkedArgumentBuffer::new is scoped (closure-borrow); collect into a Vec // since execute_custom_matcher takes &[JSValue]. let mut matcher_args: Vec = Vec::with_capacity(args.len() + 1); @@ -2218,9 +2217,14 @@ impl ExpectCustomAsymmetricMatcher { /// Implements the static call of the custom matcher (`expect.myCustomMatcher()`), /// which creates an asymmetric matcher instance (`ExpectCustomAsymmetricMatcher`). /// This will not run the matcher, but just capture the args etc. - pub(crate) fn create(global_this: &JSGlobalObject, call_frame: &CallFrame, matcher_fn: JSValue) -> JsResult { + pub(crate) fn create( + global_this: &JSGlobalObject, + this_value: JSValue, + args: &[JSValue], + matcher_fn: JSValue, + ) -> JsResult { // try to retrieve the ExpectStatic instance (to get the flags) - let flags = if let Some(expect_static) = call_frame.this().as_class_ref::() { + let flags = if let Some(expect_static) = this_value.as_class_ref::() { expect_static.flags } else { // if it's not an ExpectStatic instance, assume it was called from the Expect constructor, so use the default flags @@ -2235,7 +2239,6 @@ impl ExpectCustomAsymmetricMatcher { js_ExpectCustomAsymmetricMatcher::matcher_fn_set_cached(instance_jsvalue, global_this, matcher_fn); // capture the args as a JS array saved in the instance, so the matcher can be executed later on with them - let args = call_frame.arguments(); let array = JSValue::create_array_from_slice(global_this, args)?; js_ExpectCustomAsymmetricMatcher::captured_args_set_cached(instance_jsvalue, global_this, array); array.ensure_still_alive(); @@ -2844,8 +2847,7 @@ unsafe extern "C" { fn Bun__JSWrappingFunction__create( global_this: *const JSGlobalObject, symbol_name: &bun_core::String, - // C++: `Bun::NativeFunctionPtr` — a bare `EncodedJSValue (*)(JSGlobalObject*, CallFrame*)`. - // Rust's `JSHostFn` is already the pointer type, so no extra `*const`. + // C++: `Bun::NativeFunctionPtr`. Rust's `JSHostFn` is already the pointer type, so no extra `*const`. function_pointer: bun_jsc::JSHostFn, wrapped_fn: JSValue, ) -> JSValue; diff --git a/src/runtime/test_runner/jest.bind.ts b/src/runtime/test_runner/jest.bind.ts index 5e4c2963ab8b..71045966db83 100644 --- a/src/runtime/test_runner/jest.bind.ts +++ b/src/runtime/test_runner/jest.bind.ts @@ -5,10 +5,21 @@ export default b.module({ rustPath: "crate::test_runner::jest" }); export const fileGeneration = b.fn({ returns: b.u32 }); export const nodeTestMarkResult = b.fn({ args: { todo: b.bool, done: b.any } }); +const js = { capability: "js", throws: true } as const; + +/** `Bun.jest(path)` is what `import "bun:test"` gives the file at `path`. */ +export const jest = b.fn({ args: { path: b.any }, returns: b.any, ...js }); +/** Also `jest.setTimeout`. */ +export const setDefaultTimeout = b.fn({ + export: "Bun__Jest__setDefaultTimeout", + args: { milliseconds: b.any }, + capability: "heap", + throws: true, +}); + /** The bound `this` of the `done` a test callback takes. */ export const DoneCallback = b.class({ rustPath: "crate::test_runner::DoneCallback" }); -const js = { capability: "js", throws: true } as const; const modifier = { getter: b.any, cache: true, ...js } as const; const conditionalModifier = { args: { condition: b.bool.loose }, returns: b.any, ...js } as const; diff --git a/src/runtime/test_runner/jest.rs b/src/runtime/test_runner/jest.rs index 698f56ce3f67..c601c7c78de0 100644 --- a/src/runtime/test_runner/jest.rs +++ b/src/runtime/test_runner/jest.rs @@ -5,7 +5,7 @@ use crate::cli::command::TestOptions; use crate::cli::test_command::CommandLineReporter; use bun_collections::{ArrayHashMap, MultiArrayList}; use bun_core::Output; -use bun_jsc::bindgen::JsLocal; +use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; use bun_jsc::bun_string_jsc; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{ @@ -14,11 +14,15 @@ use bun_jsc::{ use crate::timer::ElTimespec; pub(crate) use super::bun_test; +use crate::generated_bindgen::bun_test::{ + Bun__BunTest__afterAll, Bun__BunTest__afterEach, Bun__BunTest__beforeAll, + Bun__BunTest__beforeEach, Bun__BunTest__onTestFinished, +}; use crate::generated_bindgen::expect::{js_Expect, js_ExpectTypeOf}; +use crate::generated_bindgen::jest::Bun__Jest__setDefaultTimeout; use super::scope_functions::{create_bound, Mode as ScopeKind}; use super::snapshot::Snapshots; use super::timers::fake_timers; -use bun_test::js_fns::generic_hook; use bun_test::{BaseScopeCfg, RefDataValue, ScopeMode}; #[derive(Default)] @@ -372,37 +376,35 @@ pub(crate) mod Jest { )?; module.put(global_object, b"xdescribe", xdescribe_scope_functions); - // `#[bun_jsc::host_fn]` emits a `__jsc_host_{name}` shim with the raw - // C-ABI `JSHostFn` signature; pass that to JSFunction::create. module.put( global_object, b"beforeEach", - jsc::JSFunction::create(global_object, "beforeEach", generic_hook::__jsc_host_before_each, 1, Default::default()), + jsc::JSFunction::create(global_object, "beforeEach", Bun__BunTest__beforeEach, 1, Default::default()), ); module.put( global_object, b"beforeAll", - jsc::JSFunction::create(global_object, "beforeAll", generic_hook::__jsc_host_before_all, 1, Default::default()), + jsc::JSFunction::create(global_object, "beforeAll", Bun__BunTest__beforeAll, 1, Default::default()), ); module.put( global_object, b"afterAll", - jsc::JSFunction::create(global_object, "afterAll", generic_hook::__jsc_host_after_all, 1, Default::default()), + jsc::JSFunction::create(global_object, "afterAll", Bun__BunTest__afterAll, 1, Default::default()), ); module.put( global_object, b"afterEach", - jsc::JSFunction::create(global_object, "afterEach", generic_hook::__jsc_host_after_each, 1, Default::default()), + jsc::JSFunction::create(global_object, "afterEach", Bun__BunTest__afterEach, 1, Default::default()), ); module.put( global_object, b"onTestFinished", - jsc::JSFunction::create(global_object, "onTestFinished", generic_hook::__jsc_host_on_test_finished, 1, Default::default()), + jsc::JSFunction::create(global_object, "onTestFinished", Bun__BunTest__onTestFinished, 1, Default::default()), ); module.put( global_object, b"setDefaultTimeout", - jsc::JSFunction::create(global_object, "setDefaultTimeout", __jsc_host_js_set_default_timeout, 1, Default::default()), + jsc::JSFunction::create(global_object, "setDefaultTimeout", Bun__Jest__setDefaultTimeout, 1, Default::default()), ); module.put(global_object, b"expect", js_Expect::get_constructor(global_object)); module.put(global_object, b"expectTypeOf", js_ExpectTypeOf::get_constructor(global_object)); @@ -437,7 +439,7 @@ pub(crate) mod Jest { jest.put(global_object, b"resetAllMocks", reset_all_mocks); jest.put(global_object, b"setSystemTime", set_system_time); jest.put(global_object, b"now", jsc::JSFunction::create(global_object, "now", JSMock__jsNow, 0, Default::default())); - jest.put(global_object, b"setTimeout", jsc::JSFunction::create(global_object, "setTimeout", __jsc_host_js_set_default_timeout, 1, Default::default())); + jest.put(global_object, b"setTimeout", jsc::JSFunction::create(global_object, "setTimeout", Bun__Jest__setDefaultTimeout, 1, Default::default())); module.put(global_object, b"jest", jest); module.put(global_object, b"spyOn", spy_on); @@ -468,52 +470,55 @@ pub(crate) mod Jest { pub(crate) fn JSMock__jsResetAllMocks(global: *mut JSGlobalObject, frame: *mut CallFrame) -> JSValue; pub(crate) fn JSMock__jsSpyOn(global: *mut JSGlobalObject, frame: *mut CallFrame) -> JSValue; } +} - #[bun_jsc::host_fn] - pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let vm = global_object.bun_vm(); +/// `Bun.jest(path)` +pub(crate) fn jest(cx: &mut JsScope<'_>, path: JsLocal<'_>) -> JsResult { + let global_object = cx.global(); + let vm = global_object.bun_vm(); - if vm.is_in_preload || runner().is_none() { - // in preload, no arguments needed - } else { - let arguments = callframe.arguments(); + if vm.is_in_preload || Jest::runner().is_none() { + // in preload, no arguments needed + } else { + let path = path.to_js(); - if arguments.len() < 1 || !arguments[0].is_string() { - return Err(global_object.throw(format_args!("Bun.jest() expects a string filename"))); - } - let str = arguments[0].to_utf8(global_object)?; - let slice = str.slice(); - - if !bun_paths::is_absolute(slice) { - return Err(global_object.throw(format_args!( - "Bun.jest() expects an absolute file path, got '{}'", - bstr::BStr::new(slice) - ))); - } + if !path.is_string() { + return Err(global_object.throw(format_args!("Bun.jest() expects a string filename"))); + } + let str = path.to_utf8(global_object)?; + let slice = str.slice(); + + if !bun_paths::is_absolute(slice) { + return Err(global_object.throw(format_args!( + "Bun.jest() expects an absolute file path, got '{}'", + bstr::BStr::new(slice) + ))); } - - jsc::from_js_host_call(global_object, || Bun__Jest__testModuleObject(global_object)) } - #[bun_jsc::host_fn] - fn js_set_default_timeout( - global_object: &JSGlobalObject, - callframe: &CallFrame, - ) -> JsResult { - let arguments = callframe.arguments(); - if arguments.len() < 1 || !arguments[0].is_number() { - return Err(global_object.throw(format_args!("setTimeout() expects a number (milliseconds)"))); - } + jsc::from_js_host_call(global_object, || { + Jest::Bun__Jest__testModuleObject(global_object) + }) +} - let timeout_ms: u32 = - u32::try_from(arguments[0].coerce::(global_object)?.max(0)).unwrap(); +/// `setDefaultTimeout(milliseconds)` and `jest.setTimeout(milliseconds)` +pub(crate) fn set_default_timeout(heap: &JsHeap<'_>, milliseconds: JsLocal<'_>) -> JsResult<()> { + // SAFETY: `ToInt32` of a number runs no JavaScript. + let global_object = unsafe { heap.global() }; + let milliseconds = milliseconds.to_js(); + if !milliseconds.is_number() { + return Err( + global_object.throw(format_args!("setTimeout() expects a number (milliseconds)")) + ); + } - if let Some(test_runner) = runner() { - test_runner.default_timeout_override = timeout_ms; - } + let timeout_ms: u32 = u32::try_from(milliseconds.coerce::(global_object)?.max(0)).unwrap(); - Ok(JSValue::UNDEFINED) + if let Some(test_runner) = Jest::runner() { + test_runner.default_timeout_override = timeout_ms; } + + Ok(()) } /// Reached only from `node:test`, through `$bindgen` rather than the @@ -819,17 +824,10 @@ pub(crate) fn format_label( Ok(list.into_boxed_slice()) } -pub(crate) fn capture_test_line_number(callframe: &CallFrame, global_this: &JSGlobalObject) -> u32 { +pub(crate) fn capture_test_line_number(global_this: &JSGlobalObject) -> u32 { if let Some(runner) = Jest::runner() { if runner.test_options.reporters.junit { - unsafe extern "C" { - fn Bun__CallFrame__getLineNumber( - callframe: *const CallFrame, - global: *const JSGlobalObject, - ) -> u32; - } - // SAFETY: callframe and global_this are valid live references. - return unsafe { Bun__CallFrame__getLineNumber(callframe, global_this) }; + return bun_jsc::cpp::JSC__JSGlobalObject__getCallerLineNumber(global_this); } } 0 diff --git a/src/runtime/test_runner/timers/FakeTimers.bind.ts b/src/runtime/test_runner/timers/FakeTimers.bind.ts new file mode 100644 index 000000000000..89fd8a88e878 --- /dev/null +++ b/src/runtime/test_runner/timers/FakeTimers.bind.ts @@ -0,0 +1,30 @@ +import * as b from "../../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::test_runner::timers::fake_timers" }); + +/** Returns `this`, which is `jest` or `vi`. */ +const chainable = { passThis: true, returns: b.any, capability: "js", throws: true } as const; + +export const useFakeTimers = b.fn({ + ...chainable, + export: "Bun__FakeTimers__useFakeTimers", + args: { options: b.any }, + length: 0, +}); +export const useRealTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__useRealTimers" }); +export const advanceTimersToNextTimer = b.fn({ ...chainable, export: "Bun__FakeTimers__advanceTimersToNextTimer" }); +export const advanceTimersByTime = b.fn({ + ...chainable, + export: "Bun__FakeTimers__advanceTimersByTime", + args: { milliseconds: b.any }, +}); +export const runOnlyPendingTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__runOnlyPendingTimers" }); +export const runAllTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__runAllTimers" }); +export const getTimerCount = b.fn({ + export: "Bun__FakeTimers__getTimerCount", + returns: b.usize, + capability: "heap", + throws: true, +}); +export const clearAllTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__clearAllTimers" }); +export const isFakeTimers = b.fn({ export: "Bun__FakeTimers__isFakeTimers", returns: b.bool }); diff --git a/src/runtime/test_runner/timers/FakeTimers.rs b/src/runtime/test_runner/timers/FakeTimers.rs index a030156e68dd..d907aa11a171 100644 --- a/src/runtime/test_runner/timers/FakeTimers.rs +++ b/src/runtime/test_runner/timers/FakeTimers.rs @@ -1,6 +1,13 @@ use bun_core::Timespec; -use bun_jsc::{CallFrame, JSFunction, JSGlobalObject, JSHostFn, JSValue, JsResult}; +use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; +use bun_jsc::{JSFunction, JSGlobalObject, JSHostFn, JSValue, JsResult}; use crate::api::cron::CronJob; +use crate::generated_bindgen::fake_timers::{ + Bun__FakeTimers__advanceTimersByTime, Bun__FakeTimers__advanceTimersToNextTimer, + Bun__FakeTimers__clearAllTimers, Bun__FakeTimers__getTimerCount, Bun__FakeTimers__isFakeTimers, + Bun__FakeTimers__runAllTimers, Bun__FakeTimers__runOnlyPendingTimers, + Bun__FakeTimers__useFakeTimers, Bun__FakeTimers__useRealTimers, +}; use crate::jsc::virtual_machine::VirtualMachine; use crate::timer::{ AbortSignalTimeout, ElTimespec, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag, @@ -331,15 +338,18 @@ fn set_fake_timer_marker(global: &JSGlobalObject, enabled: bool) -> JsResult<()> Ok(()) } -#[bun_jsc::host_fn] -fn use_fake_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn use_fake_timers( + cx: &mut JsScope<'_>, + this_value: JsLocal<'_>, + options: JsLocal<'_>, +) -> JsResult { + let global = cx.global(); // SAFETY: FFI call into C++ JSMock let mut js_now = JSMock__getCurrentUnixTimeMs(); // Check if options object was provided - let args = frame.arguments_as_array::<1>(); - if args.len() > 0 && !args[0].is_undefined() { - let options_value = args[0]; + let options_value = options.to_js(); + if !options_value.is_undefined() { if options_value.is_string() { // Jest 26 compat: useFakeTimers("modern" | "legacy") is a no-op. } else if !options_value.is_object() { @@ -373,11 +383,14 @@ fn use_fake_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult { +pub(crate) fn use_real_timers(cx: &mut JsScope<'_>, this_value: JsLocal<'_>) -> JsResult { + let global = cx.global(); // SAFETY: per-thread `timer::All`; the borrow ends before `release`. let cleared = unsafe { (*timer_all()).fake_timers.deactivate(global) }; cleared.release(global.bun_vm_ptr()); @@ -385,23 +398,36 @@ fn use_real_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult JsResult { +pub(crate) fn advance_timers_to_next_timer( + cx: &mut JsScope<'_>, + this_value: JsLocal<'_>, +) -> JsResult { + let global = cx.global(); error_unless_fake_timers(global)?; FakeTimers::execute_next(global)?; - Ok(bun_jsc::cpp::JSMock__toThisStrict(global, frame.this())) + Ok(bun_jsc::cpp::JSMock__toThisStrict( + global, + this_value.to_js(), + )) } -#[bun_jsc::host_fn] -fn advance_timers_by_time(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn advance_timers_by_time( + cx: &mut JsScope<'_>, + this_value: JsLocal<'_>, + milliseconds: JsLocal<'_>, +) -> JsResult { + let global = cx.global(); error_unless_fake_timers(global)?; - let arg = frame.arguments_as_array::<1>()[0]; + let arg = milliseconds.to_js(); if !arg.is_number() { return Err(global.throw_invalid_arguments(format_args!( "advanceTimersByTime() expects a number of milliseconds" @@ -436,69 +462,76 @@ fn advance_timers_by_time(global: &JSGlobalObject, frame: &CallFrame) -> JsResul } advanced?; - Ok(bun_jsc::cpp::JSMock__toThisStrict(global, frame.this())) + Ok(bun_jsc::cpp::JSMock__toThisStrict( + global, + this_value.to_js(), + )) } -#[bun_jsc::host_fn] -fn run_only_pending_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn run_only_pending_timers( + cx: &mut JsScope<'_>, + this_value: JsLocal<'_>, +) -> JsResult { + let global = cx.global(); error_unless_fake_timers(global)?; FakeTimers::execute_only_pending_timers(global)?; - Ok(bun_jsc::cpp::JSMock__toThisStrict(global, frame.this())) + Ok(bun_jsc::cpp::JSMock__toThisStrict( + global, + this_value.to_js(), + )) } -#[bun_jsc::host_fn] -fn run_all_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn run_all_timers(cx: &mut JsScope<'_>, this_value: JsLocal<'_>) -> JsResult { + let global = cx.global(); error_unless_fake_timers(global)?; FakeTimers::execute_all_timers(global)?; - Ok(bun_jsc::cpp::JSMock__toThisStrict(global, frame.this())) + Ok(bun_jsc::cpp::JSMock__toThisStrict( + global, + this_value.to_js(), + )) } -#[bun_jsc::host_fn] -fn get_timer_count(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { - error_unless_fake_timers(global)?; +pub(crate) fn get_timer_count(heap: &JsHeap<'_>) -> JsResult { + // SAFETY: throws, and the body returns. + error_unless_fake_timers(unsafe { heap.global() })?; // SAFETY: per-thread `timer::All`, live for the VM lifetime. - let count = unsafe { (*timer_all()).fake_timers.timers.count() }; - - Ok(JSValue::js_number(count as f64)) + Ok(unsafe { (*timer_all()).fake_timers.timers.count() }) } -#[bun_jsc::host_fn] -fn clear_all_timers(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn clear_all_timers(cx: &mut JsScope<'_>, this_value: JsLocal<'_>) -> JsResult { + let global = cx.global(); error_unless_fake_timers(global)?; // SAFETY: per-thread `timer::All`; the borrow ends before `release`. let cleared = unsafe { (*timer_all()).fake_timers.clear() }; cleared.release(global.bun_vm_ptr()); - Ok(bun_jsc::cpp::JSMock__toThisStrict(global, frame.this())) + Ok(bun_jsc::cpp::JSMock__toThisStrict( + global, + this_value.to_js(), + )) } -#[bun_jsc::host_fn] -fn is_fake_timers(_global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { +pub(crate) fn is_fake_timers() -> bool { // SAFETY: per-thread `timer::All`, live for the VM lifetime. - let is_active = unsafe { (*timer_all()).fake_timers.is_active() }; - - Ok(JSValue::from(is_active)) + unsafe { (*timer_all()).fake_timers.is_active() } } -// `#[bun_jsc::host_fn]` emits a `__jsc_host_{name}` shim with the raw -// `JSHostFn` ABI (`unsafe extern "C" fn(*mut JSGlobalObject, *mut CallFrame) -> JSValue`), -// which is what `JSFunction::create` expects. const FAKE_TIMERS_FNS: &[(&str, u32, JSHostFn)] = &[ - ("useFakeTimers", 0, __jsc_host_use_fake_timers), - ("useRealTimers", 0, __jsc_host_use_real_timers), - ("advanceTimersToNextTimer", 0, __jsc_host_advance_timers_to_next_timer), - ("advanceTimersByTime", 1, __jsc_host_advance_timers_by_time), - ("runOnlyPendingTimers", 0, __jsc_host_run_only_pending_timers), - ("runAllTimers", 0, __jsc_host_run_all_timers), - ("getTimerCount", 0, __jsc_host_get_timer_count), - ("clearAllTimers", 0, __jsc_host_clear_all_timers), - ("isFakeTimers", 0, __jsc_host_is_fake_timers), + ("useFakeTimers", 0, Bun__FakeTimers__useFakeTimers), + ("useRealTimers", 0, Bun__FakeTimers__useRealTimers), + ("advanceTimersToNextTimer", 0, Bun__FakeTimers__advanceTimersToNextTimer), + ("advanceTimersByTime", 1, Bun__FakeTimers__advanceTimersByTime), + ("runOnlyPendingTimers", 0, Bun__FakeTimers__runOnlyPendingTimers), + ("runAllTimers", 0, Bun__FakeTimers__runAllTimers), + ("getTimerCount", 0, Bun__FakeTimers__getTimerCount), + ("clearAllTimers", 0, Bun__FakeTimers__clearAllTimers), + ("isFakeTimers", 0, Bun__FakeTimers__isFakeTimers), ]; pub(crate) const TIMER_FNS_COUNT: usize = FAKE_TIMERS_FNS.len(); diff --git a/src/runtime/timer/Timer.bind.ts b/src/runtime/timer/Timer.bind.ts index e014a943001d..6b04863bf9d0 100644 --- a/src/runtime/timer/Timer.bind.ts +++ b/src/runtime/timer/Timer.bind.ts @@ -2,7 +2,9 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::timer::timer::internal_bindings" }); +export const timerClockMs = b.fn({ returns: b.f64 }); export const monotonicNowMs = b.fn({ returns: b.f64 }); +export const setNextTimerId = b.fn({ args: { id: b.any }, capability: "js", throws: true }); const chainable = { returns: b.self, passThis: true, invalidThis: "undefined" } as const; const ref = { ...chainable, rustName: "do_ref" } as const; diff --git a/src/runtime/timer/Timer.rs b/src/runtime/timer/Timer.rs index 376ef5d6bbf0..f295761688e1 100644 --- a/src/runtime/timer/Timer.rs +++ b/src/runtime/timer/Timer.rs @@ -9,8 +9,9 @@ use bun_core::String as BunString; use bun_core::{Timespec, TimespecMockMode}; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsClass as _, JsResult, StringJsc as _}; +use bun_jsc::{JSGlobalObject, JSValue, JsClass as _, JsResult, StringJsc as _}; use bun_uws::Loop as UwsLoop; use super::{ @@ -570,17 +571,8 @@ pub(crate) mod internal_bindings { /// thinking that the timing is wrong (this also happens when I run the modified test in /// Node.js). So the best course of action is for Bun to also expose a function that reveals the /// clock that is used to schedule timers. - #[bun_jsc::host_fn] - pub(crate) fn timer_clock_ms( - global_this: &JSGlobalObject, - call_frame: &CallFrame, - ) -> JsResult { - let _ = global_this; - let _ = call_frame; - let now = Timespec::now(TimespecMockMode::AllowMockedTime).ms(); - // bun_jsc::JSValue has no `js_number_from_int64`; route via - // `js_number(f64)` (i64 → f64 is lossless for the millisecond range). - Ok(JSValue::js_number(now as f64)) + pub(crate) fn timer_clock_ms() -> f64 { + Timespec::now(TimespecMockMode::AllowMockedTime).ms() as f64 } /// Unlike `Date.now()` and `performance.now()`, this ignores bun:test's fake timers. @@ -589,14 +581,10 @@ pub(crate) mod internal_bindings { } /// Implements `timerInternals.setNextTimerId(id)`. The id can only increase, so ids never repeat. - #[bun_jsc::host_fn] - pub(crate) fn set_next_timer_id( - global_this: &JSGlobalObject, - call_frame: &CallFrame, - ) -> JsResult { + pub(crate) fn set_next_timer_id<'a>(cx: &mut JsScope<'a>, id: JsLocal<'a>) -> JsResult<()> { let current = timer_all_mut().last_id; - let id = global_this.validate_integer_range::( - call_frame.argument(0), + let id = cx.global().validate_integer_range::( + id.to_js(), current, bun_jsc::IntegerRange { min: i128::from(current), @@ -605,6 +593,6 @@ pub(crate) mod internal_bindings { }, )?; timer_all_mut().last_id = id; - Ok(JSValue::UNDEFINED) + Ok(()) } } diff --git a/src/runtime/timer/mod.rs b/src/runtime/timer/mod.rs index fb3adee6c20d..e33104c03d99 100644 --- a/src/runtime/timer/mod.rs +++ b/src/runtime/timer/mod.rs @@ -23,10 +23,6 @@ pub(crate) use bun_event_loop::EventLoopTimer::Timespec as ElTimespec; use crate::jsc::JSValue; // ─── JS-facing surface (`impl All { set_timeout / clear_* / … }`) ──────────── -// Named `timer` so codegen (`generated_js2native.rs`) resolves -// `crate::timer::timer::internal_bindings::timer_clock_ms` per the -// `$rust(Timer.rs, …)` → `crate::::` path-mapping. - #[path = "Timer.rs"] pub(crate) mod timer; diff --git a/src/runtime/webcore/Blob.bind.ts b/src/runtime/webcore/Blob.bind.ts index fba5785f449b..5db0dca4d3ed 100644 --- a/src/runtime/webcore/Blob.bind.ts +++ b/src/runtime/webcore/Blob.bind.ts @@ -1,5 +1,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; +export default b.module({ rustPath: "crate::webcore::blob" }); + const js = { capability: "js", throws: true } as const; const method = { returns: b.any, ...js } as const; const unlink = { ...method, args: { options: b.any }, length: 0 } as const; @@ -35,3 +37,6 @@ export const Blob = b.class({ writer: { ...method, args: { options: b.any } }, }, }); + +export const file = b.fn({ ...method, args: { path: b.any, options: b.any }, length: 1 }); +export const write = b.fn({ ...method, args: { destination: b.any, data: b.any, options: b.any }, length: 1 }); diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs index d985e71cef97..e32d42f53e0e 100644 --- a/src/runtime/webcore/Blob.rs +++ b/src/runtime/webcore/Blob.rs @@ -12,7 +12,7 @@ use bun_jsc::JsCell; use bun_jsc::bindgen::{JsLocal, JsScope, JsWrapper}; use crate::webcore::jsc::{ - self as jsc, CallFrame, JSGlobalObject, JSPromise, JSValue, JsResult, VirtualMachine, + self as jsc, JSGlobalObject, JSPromise, JSValue, JsResult, VirtualMachine, }; #[cfg(any(target_os = "linux", target_os = "android"))] use bun_core as bun; @@ -3378,20 +3378,17 @@ impl BlobExt for Blob { // JSC-integration methods (host fns, to_js/from_js, S3/file I/O state machines) // ────────────────────────────────────────────────────────────────────────── +use self::write_file::{WriteFilePromise, WriteFileWaitFromLockedValueTask}; use crate::api::archive::Archive; use crate::image::Image; use crate::node; use crate::webcore::s3::client as s3_client; use crate::webcore::s3::simple_request::S3UploadResult; use crate::webcore::s3_file as S3File; -use bun_core::string_joiner::StringJoiner; -use bun_jsc::SysErrorJsc as _; -// The `write_file` module name coexists with `pub fn write_file` below (module -// vs value namespace); alias the module so the call sites read unambiguously. -use self::write_file as write_file_mod; -use self::write_file::{WriteFilePromise, WriteFileWaitFromLockedValueTask}; use bun_bundler::options_impl::LoaderExt as _; +use bun_core::string_joiner::StringJoiner; use bun_jsc::JsClass as _; +use bun_jsc::SysErrorJsc as _; /// Local mirror of `jsc.DOMFormData.FormDataEntry` (`union(enum) { string, file }`). /// `bun_jsc::dom_form_data::FormDataEntry` carries `&Blob` (immutable) but @@ -4302,7 +4299,7 @@ pub(crate) fn write_file_with_source_destination( .promise .set(cx.global(), promise_value) }; - match write_file_mod::WriteFileWindows::create( + match write_file::WriteFileWindows::create( cx.vm().event_loop(), cx.context(), destination_blob.borrowed_view(), @@ -4311,8 +4308,8 @@ pub(crate) fn write_file_with_source_destination( WriteFilePromise::run, options.mkdirp_if_not_exists.unwrap_or(true), ) { - Err(write_file_mod::WriteFileWindowsError::WriteFileWindowsDeinitialized) => {} - Err(write_file_mod::WriteFileWindowsError::Js(err)) => return Err(err), + Err(write_file::WriteFileWindowsError::WriteFileWindowsDeinitialized) => {} + Err(write_file::WriteFileWindowsError::Js(err)) => return Err(err), Ok(_) => {} } return Ok(promise_value); @@ -4320,7 +4317,7 @@ pub(crate) fn write_file_with_source_destination( #[cfg(not(windows))] { - let file_copier = write_file_mod::WriteFile::create( + let file_copier = write_file::WriteFile::create( destination_blob.borrowed_view(), source_blob.borrowed_view(), options.mkdirp_if_not_exists.unwrap_or(true), @@ -4332,7 +4329,7 @@ pub(crate) fn write_file_with_source_destination( write_file_promise.promise = jsc::JSPromiseStrong::init(cx.global()); let promise_value = write_file_promise.promise.value(); promise_value.ensure_still_alive(); - write_file_mod::WriteFile::schedule(file_copier, write_file_promise, cx); + write_file::WriteFile::schedule(file_copier, write_file_promise, cx); return Ok(promise_value); } } @@ -5007,22 +5004,23 @@ fn set_content_type_from_js( } /// `Bun.write(destination, input, options?)` -pub(crate) fn write_file(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let context = global_this.bun_vm().context_of_caller(callframe); - let arguments = callframe.arguments(); - // SAFETY: `bun_vm()` returns a live VM pointer for the calling JS context. - let mut args = jsc::ArgumentsSlice::init(global_this.bun_vm(), arguments); +pub(crate) fn write( + cx: &mut JsScope<'_>, + destination: JsLocal<'_>, + data: JsLocal<'_>, + options: JsLocal<'_>, +) -> JsResult { + let cx = cx.js_thread(); + let global_this = cx.global(); + let destination = destination.to_js(); + let mut args = + jsc::ArgumentsSlice::init(global_this.bun_vm(), core::slice::from_ref(&destination)); let mut path_or_blob = write_destination_from_js(global_this, &mut args)?; - let Some(data) = args.next_eat() else { - return Err(global_this.throw_invalid_arguments(format_args!( - "Bun.write(pathOrFdOrBlob, blob) expects a Blob-y thing to write" - ))); - }; let mut mkdirp_if_not_exists: Option = None; let mut mode: Option = None; - let options = args.next_eat(); + let options = (!options.is_undefined()).then(|| options.to_js()); if let Some(options_object) = options { if options_object.is_object() { if let Some(create_directory) = options_object.get_truthy(global_this, "createPath")? { @@ -5064,9 +5062,9 @@ pub(crate) fn write_file(global_this: &JSGlobalObject, callframe: &CallFrame) -> } } write_file_internal( - &global_this.js_thread(context), + &cx, &mut path_or_blob, - data, + data.to_js(), WriteFileOptions { mkdirp_if_not_exists, extra_options: options, @@ -5242,9 +5240,11 @@ bun_jsc::jsc_host_abi! { #[unsafe(no_mangle)] pub unsafe fn JSDOMFile__construct( global_this: &JSGlobalObject, - callframe: &CallFrame, + bits: JSValue, + name: JSValue, + options: JSValue, ) -> Option> { - match jsdom_file_construct(global_this, callframe) { + match jsdom_file_construct(global_this, bits, name, options) { Ok(b) => Some(NonNull::new(b).expect("jsdom_file_construct returns Blob::new (heap::alloc)")), Err(jsc::JsError::Thrown) => None, Err(jsc::JsError::Terminated) => { @@ -5262,55 +5262,47 @@ bun_jsc::jsc_host_abi! { pub(crate) fn jsdom_file_construct( global_this: &JSGlobalObject, - callframe: &CallFrame, + bits: JSValue, + name: JSValue, + options: JSValue, ) -> JsResult<*mut Blob> { jsc::mark_binding(); - let args = callframe.arguments(); - if args.len() < 2 { - return Err(global_this.throw_invalid_arguments(format_args!( - "new File(bits, name) expects at least 2 arguments" - ))); - } - let name = BunString::from_js(args[1], global_this)?; - let blob = Blob::get::(global_this, args[0])?; + let name = BunString::from_js(name, global_this)?; + let blob = Blob::get::(global_this, bits)?; // The store may be shared with the source blob (`dupe()`); never rename it. blob.name.set(name); let mut set_last_modified = false; - if args.len() > 2 { - let options = args[2]; - if options.is_object() { - // type, the ASCII-encoded string in lower case - // representing the media type of the Blob. - if let Some(content_type) = options.get(global_this, "type")? { - 'inner: { - if content_type.is_string() { - let content_type_str = content_type.to_utf8(global_this)?; - let slice = content_type_str.slice(); - if !is_valid_blob_type(slice) { - break 'inner; - } - blob.content_type_was_set.set(true); - blob.content_type.set( - match global_this.bun_vm().as_mut().mime_type(slice) { - Some(mime) => BlobContentType::from(mime), - None => BlobContentType::from_lowercased(slice), - }, - ); + if options.is_object() { + // type, the ASCII-encoded string in lower case + // representing the media type of the Blob. + if let Some(content_type) = options.get(global_this, "type")? { + 'inner: { + if content_type.is_string() { + let content_type_str = content_type.to_utf8(global_this)?; + let slice = content_type_str.slice(); + if !is_valid_blob_type(slice) { + break 'inner; } + blob.content_type_was_set.set(true); + blob.content_type + .set(match global_this.bun_vm().as_mut().mime_type(slice) { + Some(mime) => BlobContentType::from(mime), + None => BlobContentType::from_lowercased(slice), + }); } } + } - // WebIDL dictionary member: only `undefined` / not-present falls - // through to the Date.now() default. Any present value (including - // `null`) goes through ToNumber, with NaN normalized to 0. - if let Some(last_modified) = options.get(global_this, "lastModified")? { - set_last_modified = true; - let n = last_modified.to_number(global_this)?; - blob.last_modified.set(if n.is_nan() { 0.0 } else { n }); - } + // WebIDL dictionary member: only `undefined` / not-present falls + // through to the Date.now() default. Any present value (including + // `null`) goes through ToNumber, with NaN normalized to 0. + if let Some(last_modified) = options.get(global_this, "lastModified")? { + set_last_modified = true; + let n = last_modified.to_number(global_this)?; + blob.last_modified.set(if n.is_nan() { 0.0 } else { n }); } } @@ -5339,25 +5331,23 @@ pub(crate) fn jsdom_file_construct( // `calculate_estimated_byte_size` / `estimated_size`: canonical impls live // later in this file (near `dupe`/`to_js`). Duplicates removed here. -pub(crate) fn construct_bun_file( - global_object: &JSGlobalObject, - callframe: &CallFrame, +pub(crate) fn file( + cx: &mut JsScope<'_>, + path: JsLocal<'_>, + options: JsLocal<'_>, ) -> JsResult { + let global_object = cx.global(); // SAFETY: bun_vm() never returns null for a Bun-owned global. let vm = global_object.bun_vm(); - let arguments_slice = callframe.arguments(); - let mut args = jsc::ArgumentsSlice::init(vm, arguments_slice); + let path = path.to_js(); + let mut args = jsc::ArgumentsSlice::init(vm, core::slice::from_ref(&path)); let Some(mut path) = PathOrFileDescriptor::from_js(global_object, &mut args)? else { return Err(global_object.throw_invalid_arguments(format_args!( "Expected file path string or file descriptor" ))); }; - let options = if arguments_slice.len() >= 2 { - Some(arguments_slice[1]) - } else { - None - }; + let options = (!options.is_undefined()).then(|| options.to_js()); if let PathOrFileDescriptor::Path(ref p) = path { if p.slice().starts_with(b"s3://") { diff --git a/src/runtime/webcore/ByteStream.bind.ts b/src/runtime/webcore/ByteStream.bind.ts new file mode 100644 index 000000000000..c5eb5520008c --- /dev/null +++ b/src/runtime/webcore/ByteStream.bind.ts @@ -0,0 +1,5 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::webcore::byte_stream" }); + +export const cancelOnDrain = b.fn({ args: { stream: b.any }, capability: "js", throws: true }); diff --git a/src/runtime/webcore/ByteStream.rs b/src/runtime/webcore/ByteStream.rs index 75958f6f2cd1..2ace9f8388ba 100644 --- a/src/runtime/webcore/ByteStream.rs +++ b/src/runtime/webcore/ByteStream.rs @@ -1,8 +1,9 @@ use core::cell::Cell; use bun_collections::VecExt; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::strong::Optional as StrongOptional; -use bun_jsc::{self as jsc, JSGlobalObject, JSValue, JsCell}; +use bun_jsc::{self as jsc, JSValue, JsCell}; use bun_ptr::RefPtr; use bun_sys::Error as SysError; @@ -964,27 +965,18 @@ impl ByteStream { } } -pub(crate) mod testing_apis { - use super::*; - - /// `bun:internal-for-testing`: swap the stream's producer for - /// [`streams::SourceHandle::TestingCancelOnDrain`], whose drain signal - /// re-enters `on_cancel` and consumes the pending buffer action. - pub(crate) fn byte_stream_cancel_on_drain( - global: &JSGlobalObject, - frame: &bun_jsc::CallFrame, - ) -> bun_jsc::JsResult { - let stream = readable_stream::ReadableStream::from_js(frame.argument(0), global)?; - let Some(bytes) = stream.and_then(|s| s.ptr.bytes()) else { - return Err(global.throw(format_args!("expected a ByteStream-backed ReadableStream"))); - }; - bytes - .parent_const() - .producer - .set(streams::SourceHandle::TestingCancelOnDrain(bytes)); - Ok(JSValue::UNDEFINED) - } +/// `bun:internal-for-testing`: swap the stream's producer for +/// [`streams::SourceHandle::TestingCancelOnDrain`], whose drain signal +/// re-enters `on_cancel` and consumes the pending buffer action. +pub(crate) fn cancel_on_drain(cx: &mut JsScope<'_>, stream: JsLocal<'_>) -> bun_jsc::JsResult<()> { + let global = cx.global(); + let stream = readable_stream::ReadableStream::from_js(stream.to_js(), global)?; + let Some(bytes) = stream.and_then(|s| s.ptr.bytes()) else { + return Err(global.throw(format_args!("expected a ByteStream-backed ReadableStream"))); + }; + bytes + .parent_const() + .producer + .set(streams::SourceHandle::TestingCancelOnDrain(bytes)); + Ok(()) } -// `generated_js2native.rs` snake-cases `TestingAPIs` as `testing_ap_is` -// (acronym splitter treats `AP|Is` as two words); alias so both resolve. -pub(crate) use testing_apis as testing_ap_is; diff --git a/src/runtime/webcore/Crypto.bind.ts b/src/runtime/webcore/Crypto.bind.ts index d09e90af481a..4e65e44257af 100644 --- a/src/runtime/webcore/Crypto.bind.ts +++ b/src/runtime/webcore/Crypto.bind.ts @@ -21,6 +21,16 @@ export const randomUUIDv5 = b.fn({ rustPath: "crate::webcore::crypto::random_uuid_v5", }); +/** `randomUUIDv7(timestamp)` takes the timestamp. `randomUUIDv7(timestamp, undefined)` takes the clock's. */ +export const randomUUIDv7 = b.fn({ + args: { args: b.any.variadic }, + returns: b.any, + capability: "js", + throws: true, + length: 2, + rustPath: "crate::webcore::crypto::random_uuid_v7", +}); + const buffer = b.bytes({ from: ["buffer"] }); const anyBuffer = (name: string) => `The "${name}" argument must be an instance of ArrayBuffer, Buffer, TypedArray, or DataView.`; diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 294413cf4e79..58985bd83434 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -2,7 +2,7 @@ use bun_core::String as BunString; use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope, StringOrJsBytes}; use bun_jsc::uuid::{self, UUID, UUID5, UUID7}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{CallFrame, JSGlobalObject, JSType, JSValue, JsClass as _, JsResult, StringJsc}; +use bun_jsc::{JSGlobalObject, JSType, JSValue, JsClass as _, JsResult, StringJsc}; use crate::generated_bindgen::crypto::{EncodedUUID, UUIDEncoding}; use crate::node::Encoding; @@ -104,19 +104,17 @@ fn random_data(global: &JSGlobalObject, slice: &mut [u8]) { } } -// The #[bun_jsc::host_fn] attribute macro emits the `extern "C"` shim with the -// correct calling convention and `#[unsafe(no_mangle)]` under the exported name. -#[bun_jsc::host_fn(export = "Bun__randomUUIDv7")] -fn bun_random_uuid_v7(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let arguments = callframe.arguments_undef::<2>(); +pub(crate) fn random_uuid_v7(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult { + let global = cx.global(); + let arguments = JsLocal::to_js_slice(args); let mut encoding_value: JSValue = JSValue::UNDEFINED; let encoding: Encoding = 'brk: { - if arguments.len > 0 { - if !arguments.ptr[0].is_undefined() { - if arguments.ptr[0].is_string() { - encoding_value = arguments.ptr[0]; + if !arguments.is_empty() { + if !arguments[0].is_undefined() { + if arguments[0].is_string() { + encoding_value = arguments[0]; break 'brk match Encoding::from_js(encoding_value, global)? { Some(e) => e, None => { @@ -138,10 +136,10 @@ fn bun_random_uuid_v7(global: &JSGlobalObject, callframe: &CallFrame) -> JsResul }; let (timestamp, timestamp_source): (u64, uuid::TimestampSource) = 'brk: { - let timestamp_value: JSValue = if arguments.len > 1 { - arguments.ptr[1] - } else if arguments.len == 1 && encoding_value.is_undefined() { - arguments.ptr[0] + let timestamp_value: JSValue = if arguments.len() > 1 { + arguments[1] + } else if arguments.len() == 1 && encoding_value.is_undefined() { + arguments[0] } else { JSValue::UNDEFINED }; diff --git a/src/runtime/webcore/FileSink.bind.ts b/src/runtime/webcore/FileSink.bind.ts new file mode 100644 index 000000000000..91c91aeb75c2 --- /dev/null +++ b/src/runtime/webcore/FileSink.bind.ts @@ -0,0 +1,9 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::webcore::file_sink" }); + +export const liveCount = b.fn({ returns: b.i32 }); + +const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +export const onResolveStream = b.fn({ ...reaction, export: "Bun__FileSink__onResolveStream" }); +export const onRejectStream = b.fn({ ...reaction, export: "Bun__FileSink__onRejectStream", throws: true }); diff --git a/src/runtime/webcore/FileSink.rs b/src/runtime/webcore/FileSink.rs index ad9b44bc2cea..181ee8bf68d5 100644 --- a/src/runtime/webcore/FileSink.rs +++ b/src/runtime/webcore/FileSink.rs @@ -5,11 +5,15 @@ use core::sync::atomic::{AtomicI32, Ordering}; use bun_io::pipe_writer::BaseWindowsPipeWriter as _; use bun_io::{self, WriteResult, WriteStatus}; use bun_jsc::JsCell; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_ptr::RefPtr; use bun_sys::{self as sys, Fd, FdExt as _}; use crate::api::bun::process::Status as SpawnStatus; -use crate::webcore::jsc::{CallFrame, EventLoopHandle, JSGlobalObject, JSValue, JsError, JsResult}; +use crate::generated_bindgen::file_sink::{ + Bun__FileSink__onRejectStream, Bun__FileSink__onResolveStream, +}; +use crate::webcore::jsc::{EventLoopHandle, JSGlobalObject, JSValue, JsError, JsResult}; use crate::webcore::readable_stream::{self, ReadableStream}; use crate::webcore::{self, AutoFlusher, PathOrFileDescriptor, streams}; #[cfg(windows)] @@ -83,19 +87,9 @@ pub struct FileSink { /// `heapStats()` (which only counts JS wrapper objects). pub(crate) static LIVE_COUNT: AtomicI32 = AtomicI32::new(0); -pub(crate) mod testing_apis { - use super::*; - - pub(crate) fn file_sink_live_count( - _global: &JSGlobalObject, - _frame: &CallFrame, - ) -> JsResult { - Ok(JSValue::js_number(LIVE_COUNT.load(Ordering::Relaxed) as f64)) - } +pub(crate) fn live_count() -> i32 { + LIVE_COUNT.load(Ordering::Relaxed) } -// `generated_js2native.rs` snake-cases `TestingAPIs` as `testing_ap_is` -// (acronym splitter treats `AP|Is` as two words); alias so both resolve. -pub(crate) use testing_apis as testing_ap_is; /// `bun_sys` does not yet export /// an isPollable helper, so re-derive it locally from `S_IFMT`. Windows always @@ -1744,33 +1738,32 @@ impl FileSink { } } -fn on_resolve_stream(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn on_resolve_stream(cx: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { bun_core::scoped_log!(FileSink, "onResolveStream"); - let args = callframe.arguments(); - let this: *mut FileSink = args[args.len() - 1].as_promise_ptr::(); + let this: *mut FileSink = context.to_js().as_promise_ptr::(); // SAFETY: `this` is kept alive by the ref taken in `assign_to_stream`; this guard balances it. let _guard = unsafe { RefPtr::from_raw(this) }; // SAFETY: `as_promise_ptr` recovers the `*mut FileSink` stashed by `assign_to_stream`. unsafe { (*this).pump_promise_ref.set(false); - (*this).handle_resolve_stream(global_this); + (*this).handle_resolve_stream(cx.global()); } - Ok(JSValue::UNDEFINED) } -fn on_reject_stream(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn on_reject_stream( + cx: &mut JsScope<'_>, + err: JsLocal<'_>, + context: JsLocal<'_>, +) -> JsResult<()> { bun_core::scoped_log!(FileSink, "onRejectStream"); - let args = callframe.arguments(); - let this: *mut FileSink = args[args.len() - 1].as_promise_ptr::(); - let err = args[0]; + let this: *mut FileSink = context.to_js().as_promise_ptr::(); // SAFETY: `this` is kept alive by the ref taken in `assign_to_stream`; this guard balances it. let _guard = unsafe { RefPtr::from_raw(this) }; // SAFETY: `as_promise_ptr` recovers the `*mut FileSink` stashed by `assign_to_stream`. unsafe { (*this).pump_promise_ref.set(false); - (*this).handle_reject_stream(global_this, err)?; + (*this).handle_reject_stream(cx.global(), err.to_js()) } - Ok(JSValue::UNDEFINED) } impl FileSink { @@ -1936,8 +1929,8 @@ impl FileSink { promise_result.then( global_this, std::ptr::from_mut::(self), - on_resolve_stream_shim, - on_reject_stream_shim, + Bun__FileSink__onResolveStream, + Bun__FileSink__onRejectStream, ); } bun_jsc::js_promise::Status::Fulfilled => { @@ -1954,45 +1947,3 @@ impl FileSink { Ok(promise_result) } } - -// `#[bun_jsc::host_fn]` proc-macro is not yet ported, so emit the -// JSC host-function ABI shim by hand and export under the C symbol names the -// C++ side expects. -// -// IMPORTANT: these MUST be exported as *function* symbols (not as `static` -// function-pointer variables). C++ declares them via -// `BUN_DECLARE_HOST_FUNCTION(Bun__FileSink__onResolveStream)` and compares the -// resulting symbol address against the handler passed to `JSValue::then` in -// `Zig::GlobalObject::promiseHandlerID`. A `pub static …: JSHostFn = shim` -// exports the address of an 8-byte data slot, which never equals the shim's -// code address → RELEASE_ASSERT_NOT_REACHED at runtime. -bun_jsc::jsc_host_abi! { - #[unsafe(export_name = "Bun__FileSink__onResolveStream")] - unsafe fn on_resolve_stream_shim( - g: *mut JSGlobalObject, - cf: *mut CallFrame, - ) -> JSValue { - // S008: `JSGlobalObject`/`CallFrame` are `opaque_ffi!` ZST handles — - // safe `*mut → &` via `opaque_deref`. Kept as raw `JsHostFn` shape so - // the fn-item coerces to `.then()`'s `JsHostFn` pointer slot without a - // transmute. - match on_resolve_stream(bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(cf)) { - Ok(v) => v, - Err(_) => JSValue::ZERO, - } - } -} -bun_jsc::jsc_host_abi! { - #[unsafe(export_name = "Bun__FileSink__onRejectStream")] - unsafe fn on_reject_stream_shim( - g: *mut JSGlobalObject, - cf: *mut CallFrame, - ) -> JSValue { - // S008: `JSGlobalObject`/`CallFrame` are `opaque_ffi!` ZST handles — - // safe `*mut → &` via `opaque_deref`. - match on_reject_stream(bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(cf)) { - Ok(v) => v, - Err(_) => JSValue::ZERO, - } - } -} diff --git a/src/runtime/webcore/FormData.bind.ts b/src/runtime/webcore/FormData.bind.ts new file mode 100644 index 000000000000..3d27f26223fb --- /dev/null +++ b/src/runtime/webcore/FormData.bind.ts @@ -0,0 +1,11 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::webcore::form_data" }); + +export const from = b.fn({ + args: { input: b.any, boundary: b.any }, + returns: b.any, + capability: "js", + throws: true, + length: 1, +}); diff --git a/src/runtime/webcore/FormData.rs b/src/runtime/webcore/FormData.rs index c0d5a1eb8250..78fca391c337 100644 --- a/src/runtime/webcore/FormData.rs +++ b/src/runtime/webcore/FormData.rs @@ -2,7 +2,8 @@ use bun_core::{self, declare_scope, scoped_log}; use bun_core::{EncodedSlice, Utf8Bytes, strings}; -use bun_jsc::{AnyPromise, CallFrame, DOMFormData, JSGlobalObject, JSValue, JsError, JsResult}; +use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::{AnyPromise, DOMFormData, JSGlobalObject, JSValue, JsError, JsResult}; use bun_semver::{self, SlicedString}; use core::ffi::c_void; @@ -100,9 +101,14 @@ impl FormData { } } -#[bun_jsc::host_fn(export = "FormData__jsFunctionFromMultipartData")] -pub(crate) fn from_multipart_data(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let [input_value, boundary_value] = frame.arguments_as_array::<2>(); +/// `FormData.from(input, boundary)` +pub(crate) fn from( + cx: &mut JsScope<'_>, + input: JsLocal<'_>, + boundary: JsLocal<'_>, +) -> JsResult { + let global = cx.global(); + let (input_value, boundary_value) = (input.to_js(), boundary.to_js()); let boundary_slice: Utf8Bytes; let mut encoding = Encoding::URLEncoded; diff --git a/src/runtime/webcore/ObjectURLRegistry.bind.ts b/src/runtime/webcore/ObjectURLRegistry.bind.ts new file mode 100644 index 000000000000..cb9300a49c0b --- /dev/null +++ b/src/runtime/webcore/ObjectURLRegistry.bind.ts @@ -0,0 +1,18 @@ +import * as b from "../../codegen/bindgen-lib.ts"; +import { Blob } from "./Blob.bind.ts"; + +export default b.module({ rustPath: "crate::webcore::object_url_registry" }); + +export const createObjectURL = b.fn({ + args: { blob: Blob }, + expected: { blob: "createObjectURL expects a Blob object" }, + returns: b.string, +}); + +export const revokeObjectURL = b.fn({ + args: { url: b.string.allowStringObject }, + expected: { url: "revokeObjectURL expects a string" }, +}); + +/** `resolveObjectURL` of `node:buffer`. */ +export const resolveObjectURL = b.fn({ args: { url: b.string.loose }, returns: b.any, capability: "js" }); diff --git a/src/runtime/webcore/ObjectURLRegistry.rs b/src/runtime/webcore/ObjectURLRegistry.rs index d2d9f079fa5a..97aa9b2eeb78 100644 --- a/src/runtime/webcore/ObjectURLRegistry.rs +++ b/src/runtime/webcore/ObjectURLRegistry.rs @@ -2,8 +2,9 @@ use std::sync::OnceLock; use bun_collections::HashMap; use bun_core::strings; +use bun_jsc::bindgen::JsScope; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, StringJsc as _, UUID}; +use bun_jsc::{JSGlobalObject, JSValue, UUID}; use bun_threading::Guarded; use crate::webcore::Blob; @@ -111,88 +112,39 @@ fn uuid_from_pathname(pathname: &[u8]) -> Option { UUID::parse(pathname).ok() } -#[bun_jsc::host_fn(export = "Bun__createObjectURL")] -fn bun_create_object_url( - global_object: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let [blob_arg] = callframe.arguments_as_array::<1>(); - if callframe.arguments_count() < 1 { - return Err(global_object.throw_not_enough_arguments( - "createObjectURL", - 1, - callframe.arguments_count() as usize, - )); - } - let Some(blob) = blob_arg.as_class_ref::() else { - return Err(global_object - .throw_invalid_arguments(format_args!("createObjectURL expects a Blob object"))); - }; +pub(crate) fn create_object_url(blob: &Blob) -> bun_core::String { let registry = ObjectURLRegistry::singleton(); - // SAFETY: `bun_vm_ptr()` returns the live VM pointer for `global_object`. - let uuid = registry.register(unsafe { &mut *global_object.bun_vm_ptr() }, blob); - bun_core::String::create_format(format_args!("blob:{}", uuid)).into_js(global_object) + let uuid = registry.register(VirtualMachine::get().as_mut(), blob); + bun_core::String::create_format(format_args!("blob:{}", uuid)) } -#[bun_jsc::host_fn(export = "Bun__revokeObjectURL")] -fn bun_revoke_object_url( - global_object: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let [url_arg] = callframe.arguments_as_array::<1>(); - if callframe.arguments_count() < 1 { - return Err(global_object.throw_not_enough_arguments( - "revokeObjectURL", - 1, - callframe.arguments_count() as usize, - )); - } - if !url_arg.is_string() { - return Err( - global_object.throw_invalid_arguments(format_args!("revokeObjectURL expects a string")) - ); - } - // `is_string()` is `is_string_like()` and admits `StringObject`, so - // `to_bun_string` can still observe a user `toString` that throws. - let str = url_arg.to_bun_string(global_object)?; +pub(crate) fn revoke_object_url(str: &bun_core::String) { if !str.starts_with_ascii(b"blob:") { - return Ok(JSValue::UNDEFINED); + return; } let slice = str.to_utf8(); let sliced = slice.slice(); if sliced.len() < b"blob:".len() + UUID::STRING_LENGTH { - return Ok(JSValue::UNDEFINED); + return; } ObjectURLRegistry::singleton().revoke(&sliced[b"blob:".len()..]); - Ok(JSValue::UNDEFINED) } -#[bun_jsc::host_fn(export = "jsFunctionResolveObjectURL")] -fn js_function_resolve_object_url( - global_object: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let [url_arg] = callframe.arguments_as_array::<1>(); - +pub(crate) fn resolve_object_url(cx: &mut JsScope<'_>, str: &bun_core::String) -> JSValue { // Errors are ignored. // Not thrown. // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/blob.js#L441 - if callframe.arguments_count() < 1 { - return Ok(JSValue::UNDEFINED); - } - let str = url_arg.to_bun_string(global_object)?; - if !str.starts_with_ascii(b"blob:") || str.length() < SPECIFIER_LEN { - return Ok(JSValue::UNDEFINED); + return JSValue::UNDEFINED; } let slice = str.to_utf8(); let sliced = slice.slice(); let registry = ObjectURLRegistry::singleton(); - let blob = registry.resolve_and_dupe_to_js(&sliced[b"blob:".len()..], global_object); - Ok(blob.unwrap_or(JSValue::UNDEFINED)) + let blob = registry.resolve_and_dupe_to_js(&sliced[b"blob:".len()..], cx.global()); + blob.unwrap_or(JSValue::UNDEFINED) } const SPECIFIER_LEN: usize = b"blob:".len() + UUID::STRING_LENGTH; diff --git a/src/runtime/webcore/S3File.rs b/src/runtime/webcore/S3File.rs index e3bff0f36d0c..a7fb389c8bae 100644 --- a/src/runtime/webcore/S3File.rs +++ b/src/runtime/webcore/S3File.rs @@ -7,7 +7,7 @@ use crate::webcore::s3_client::S3CredentialsExt as _; use bun_core::strings; use bun_http::Method; use bun_jsc::bun_string_jsc; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsClass as _, JsError, JsResult}; +use bun_jsc::{JSGlobalObject, JSValue, JsClass as _, JsError, JsResult}; use bun_ptr::RefPtr; // Local front for `bun_core::pretty_fmt!` that accepts a runtime / const- @@ -556,14 +556,6 @@ fn get_bucket(this: &Blob, global: &JSGlobalObject) -> JsResult { Ok(JSValue::UNDEFINED) } -fn get_presign_url( - this: &Blob, - global: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - get_presign_url_from(this, global, callframe.arguments().first().copied()) -} - pub(crate) fn construct_internal_js( global: &JSGlobalObject, path: PathLike<'static>, @@ -580,7 +572,7 @@ pub(crate) fn to_js_unchecked(global: &JSGlobalObject, this: *mut Blob) -> JSVal } // Symbols exported with C linkage and JSC calling convention. -// JSS3File__presign -> raw shim wrapping get_presign_url (method-with-context) +// JSS3File__presign -> raw shim wrapping get_presign_url_from (method-with-context) // JSS3File__bucket -> get_bucket // JSS3File__stat -> raw shim wrapping S3BlobStatTask::stat (method-with-context) @@ -597,30 +589,28 @@ pub(crate) mod exports { bun_jsc::to_js_host_call(global, || super::get_bucket(this, global)) } - /// Method-with-context shim wrapping `get_presign_url`. + /// `options` is empty when it was not passed. #[unsafe(no_mangle)] #[bun_jsc::host_call] fn JSS3File__presign( this: *mut Blob, global: *mut JSGlobalObject, - callframe: *mut CallFrame, + options: JSValue, ) -> JSValue { - // SAFETY: JSC method shim passes the wrapper's live Blob, global and callframe. - let (this, global, callframe) = unsafe { (&*this, &*global, &*callframe) }; - bun_jsc::to_js_host_call(global, || super::get_presign_url(this, global, callframe)) + // SAFETY: JSC method shim passes the wrapper's live Blob and global. + let (this, global) = unsafe { (&*this, &*global) }; + bun_jsc::to_js_host_call(global, || { + super::get_presign_url_from(this, global, (!options.is_empty()).then_some(options)) + }) } #[unsafe(no_mangle)] #[bun_jsc::host_call] - fn JSS3File__stat( - this: *mut Blob, - global: *mut JSGlobalObject, - callframe: *mut CallFrame, - ) -> JSValue { - // SAFETY: JSC method shim passes the wrapper's live Blob, global and callframe. - let (this, global, callframe) = unsafe { (&*this, &*global, &*callframe) }; + fn JSS3File__stat(this: *mut Blob, global: *mut JSGlobalObject) -> JSValue { + // SAFETY: JSC method shim passes the wrapper's live Blob and global. + let (this, global) = unsafe { (&*this, &*global) }; bun_jsc::to_js_host_call(global, || { - S3BlobStatTask::stat(&global.js_thread_of_caller(callframe), this) + S3BlobStatTask::stat(&global.js_thread_of_caller_no_frame(), this) }) } } diff --git a/src/runtime/webcore/fetch.bind.ts b/src/runtime/webcore/fetch.bind.ts new file mode 100644 index 000000000000..c2b33e2d6caf --- /dev/null +++ b/src/runtime/webcore/fetch.bind.ts @@ -0,0 +1,11 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::webcore::fetch" }); + +const js = { capability: "js", throws: true } as const; +const request = { args: { input: b.any, init: b.any }, returns: b.any, ...js, length: 1 } as const; + +export const fetch = b.fn(request); +/** What `session.fetch` of a `Bun.FetchSession` is bound from. */ +export const sessionFetch = b.fn({ ...request, passThis: true, export: "Bun__Fetch__sessionFetch" }); +export const preconnect = b.fn({ args: { url: b.any }, ...js }); diff --git a/src/runtime/webcore/fetch.rs b/src/runtime/webcore/fetch.rs index f31aedd4a5eb..a1e2292e97e9 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -36,23 +36,22 @@ pub(crate) mod compress_body; pub(crate) mod fetch_session; pub(crate) use self::fetch_session::FetchSession; -/// `proxyInternals` of `bun:internal-for-testing`. `generated_js2native.rs` -/// snake-cases `TestingAPIs` as `testing_ap_is`. #[path = "fetch/proxy_testing.rs"] -pub(crate) mod testing_ap_is; +pub(crate) mod proxy_testing; // ────────────────────────────────────────────────────────────────────────── // fetch() implementation // ────────────────────────────────────────────────────────────────────────── use crate::webcore::jsc::{ - self as jsc, CallFrame, JSGlobalObject, JSPromise, JSValue, JsResult, VirtualMachine, + self as jsc, JSGlobalObject, JSPromise, JSValue, JsResult, VirtualMachine, }; use bun_core::{String as BunString, Tag as BunStringTag}; use bun_http::http_request_body::StreamFraming; use bun_http::{self as http, FetchRedirect, Headers, HeadersExt as _, MimeType}; use bun_http_jsc::method_jsc; use bun_http_types::Method::Method; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::{HTTPHeaderName, StringJsc as _, SysErrorJsc as _, URLJsc as _}; use bun_sys::FdExt as _; // `FromJsEnum for FetchRedirect` lives in bun_http_jsc; importing the impl crate @@ -206,25 +205,13 @@ fn data_url_response(url: BunString, global_this: &JSGlobalObject) -> JSValue { } // ────────────────────────────────────────────────────────────────────────── -// Bun__fetchPreconnect +// fetch.preconnect // ────────────────────────────────────────────────────────────────────────── -#[bun_jsc::host_fn(export = "Bun__fetchPreconnect")] -fn bun_fetch_preconnect( - global_object: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let arguments = callframe.arguments(); +pub(crate) fn preconnect(cx: &mut JsScope<'_>, url: JsLocal<'_>) -> JsResult<()> { + let global_object = cx.global(); - if arguments.len() < 1 { - return Err(global_object.throw_not_enough_arguments( - "fetch.preconnect", - 1, - arguments.len(), - )); - } - - let url_str = jsc::URL::href_from_js(arguments[0], global_object)?; + let url_str = jsc::URL::href_from_js(url.to_js(), global_object)?; if url_str.tag() == BunStringTag::Dead { return Err(global_object @@ -290,10 +277,10 @@ fn bun_fetch_preconnect( let vm = VirtualMachine::get(); if vm.env_loader().get_http_proxy_for(&url).is_some() || vm.tls_default_ciphers().is_some() { reclaim_href!(); - return Ok(JSValue::UNDEFINED); + return Ok(()); } http::async_http::preconnect(url, true); - Ok(JSValue::UNDEFINED) + Ok(()) } // ────────────────────────────────────────────────────────────────────────── @@ -317,21 +304,33 @@ impl StringOrURL { } // ────────────────────────────────────────────────────────────────────────── -// Bun__fetch entry point +// fetch entry point // ────────────────────────────────────────────────────────────────────────── /// Public entry point for `Bun.fetch` - validates body on GET/HEAD -#[bun_jsc::host_fn(export = "Bun__fetch")] -fn bun_fetch(ctx: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - reject_on_exception(ctx, fetch_impl::(ctx, callframe, None)) +pub(crate) fn fetch( + cx: &mut JsScope<'_>, + input: JsLocal<'_>, + init: JsLocal<'_>, +) -> JsResult { + let ctx = cx.global(); + reject_on_exception( + ctx, + fetch_impl::(ctx, input.to_js(), init.to_js(), None), + ) } /// What `session.fetch` is bound from: `fetch()` with `this` as its session. -#[bun_jsc::host_fn] -pub(crate) fn session_fetch(ctx: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn session_fetch( + cx: &mut JsScope<'_>, + this_value: JsLocal<'_>, + input: JsLocal<'_>, + init: JsLocal<'_>, +) -> JsResult { + let ctx = cx.global(); reject_on_exception( ctx, - fetch_impl::(ctx, callframe, Some(callframe.this())), + fetch_impl::(ctx, input.to_js(), init.to_js(), Some(this_value.to_js())), ) } @@ -400,11 +399,12 @@ fn reject_before_send( /// Shared implementation of fetch fn fetch_impl( ctx: &JSGlobalObject, - callframe: &CallFrame, + first_arg: JSValue, + options: JSValue, // `session.fetch()`: the session, which a `session` in the init does not replace. bound_session: Option, ) -> JsResult { - let context = ctx.bun_vm().context_of_caller(callframe); + let context = ctx.bun_vm().context_of_caller_no_frame(); // What script of a disposed `Bun.ModuleGraph` starts does not start, and reports nothing: no // connection goes out, and the promise stays pending. if context.is_stopped() { @@ -420,24 +420,10 @@ fn fetch_impl( let mut upgraded_connection = false; let mut forced_protocol: Option = None; - if callframe.arguments_count() == 0 { - let err = ctx.to_type_error( - jsc::ErrorCode::MISSING_ARGS, - format_args!("fetch() expects a string but received no arguments."), - ); - return Ok(JSPromise::rejected_promise(global_this, err).to_js()); - } - let mut headers: Option = None; - // hoist the one `&mut vm` accessor before `args` takes an - // immutable borrow of `vm` for the rest of the function. let vm_verbose_fetch = vm.get_verbose_fetch(); - let mut args = jsc::ArgumentsSlice::init(vm, callframe.arguments()); - - let first_arg = args.next_eat().unwrap(); - let mut disable_timeout = false; let mut idle_timeout_seconds: Option = None; let mut disable_keepalive = false; @@ -485,11 +471,8 @@ fn fetch_impl( // ssl_config are all owning types whose Drop runs on early return. let options_object: Option = 'brk: { - if let Some(options) = args.next_eat() { - let options: JSValue = options; - if options.is_object() || options.js_type() == jsc::JSType::DOMWrapper { - break 'brk Some(options); - } + if options.is_object() || options.js_type() == jsc::JSType::DOMWrapper { + break 'brk Some(options); } break 'brk None; }; diff --git a/src/runtime/webcore/fetch/FetchSession.rs b/src/runtime/webcore/fetch/FetchSession.rs index 7b0f60c550b9..147553786392 100644 --- a/src/runtime/webcore/fetch/FetchSession.rs +++ b/src/runtime/webcore/fetch/FetchSession.rs @@ -393,7 +393,7 @@ impl FetchSession { let target = jsc::JSFunction::create( global, "fetch", - super::__jsc_host_session_fetch, + crate::generated_bindgen::fetch::Bun__Fetch__sessionFetch, 1, Default::default(), ); diff --git a/src/runtime/webcore/fetch/FetchTasklet.bind.ts b/src/runtime/webcore/fetch/FetchTasklet.bind.ts new file mode 100644 index 000000000000..e21e35e427fd --- /dev/null +++ b/src/runtime/webcore/fetch/FetchTasklet.bind.ts @@ -0,0 +1,7 @@ +import * as b from "../../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::webcore::fetch::fetch_tasklet" }); + +const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +export const onResolveRequestStream = b.fn({ ...reaction, export: "Bun__FetchTasklet__onResolveRequestStream" }); +export const onRejectRequestStream = b.fn({ ...reaction, export: "Bun__FetchTasklet__onRejectRequestStream" }); diff --git a/src/runtime/webcore/fetch/FetchTasklet.rs b/src/runtime/webcore/fetch/FetchTasklet.rs index 2f64e8ccc8cc..4ffc86bad4c2 100644 --- a/src/runtime/webcore/fetch/FetchTasklet.rs +++ b/src/runtime/webcore/fetch/FetchTasklet.rs @@ -16,6 +16,7 @@ use bun_http::{ Signals, ThreadSafeStreamBuffer, }; use bun_io::KeepAlive; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::bun_string_jsc; use bun_jsc::debugger::AsyncTaskTracker; use bun_jsc::{self as jsc, GlobalRef, JSGlobalObject, JSValue, JsCell, JsResult, StrongOptional}; @@ -25,6 +26,9 @@ use bun_threading::Mutex; use bun_url::URL as ZigURL; use crate::api::bun_x509 as X509; +use crate::generated_bindgen::fetch_tasklet::{ + Bun__FetchTasklet__onRejectRequestStream, Bun__FetchTasklet__onResolveRequestStream, +}; use crate::webcore::blob::{Any as AnyBlob, Blob, SizeType as BlobSizeType, Store as BlobStore}; use crate::webcore::body::{self, Body, Value as BodyValue, ValueError as BodyValueError}; use crate::webcore::fetch::fetch_request_body_sink::{FetchRequestBodySink, RequestBodyChunk}; @@ -724,8 +728,8 @@ impl FetchTasklet { assignment_result.then( &global_this, self_ptr, - on_resolve_request_stream_shim, - on_reject_request_stream_shim, + Bun__FetchTasklet__onResolveRequestStream, + Bun__FetchTasklet__onRejectRequestStream, ); } bun_jsc::js_promise::Status::Fulfilled => { @@ -2708,12 +2712,8 @@ impl FetchTasklet { } } -fn on_resolve_request_stream( - _global_this: &JSGlobalObject, - callframe: &bun_jsc::CallFrame, -) -> JsResult { - let args = callframe.arguments(); - let this: *mut FetchTasklet = args[args.len() - 1].as_promise_ptr::(); +pub(crate) fn on_resolve_request_stream(_: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { + let this: *mut FetchTasklet = context.to_js().as_promise_ptr::(); // SAFETY: `as_promise_ptr` recovers the `*mut FetchTasklet` stashed by // `start_request_stream`; the `ref_()` there keeps it alive, balanced by // `write_end_request` below. Clear `sink.task` first so the sink's @@ -2724,16 +2724,14 @@ fn on_resolve_request_stream( } (*this).write_end_request(None); } - Ok(JSValue::UNDEFINED) } -fn on_reject_request_stream( - _global_this: &JSGlobalObject, - callframe: &bun_jsc::CallFrame, -) -> JsResult { - let args = callframe.arguments(); - let this: *mut FetchTasklet = args[args.len() - 1].as_promise_ptr::(); - let err = args[0]; +pub(crate) fn on_reject_request_stream( + _: &mut JsScope<'_>, + err: JsLocal<'_>, + context: JsLocal<'_>, +) { + let this: *mut FetchTasklet = context.to_js().as_promise_ptr::(); // SAFETY: `as_promise_ptr` recovers the `*mut FetchTasklet` stashed by // `start_request_stream`; the `ref_()` there keeps it alive, balanced by // `write_end_request` below. Clear `sink.task` first so the sink's @@ -2742,36 +2740,7 @@ fn on_reject_request_stream( if let Some(sink) = (*this).sink_mut() { sink.task = None; } - (*this).write_end_request(Some(err)); - } - Ok(JSValue::UNDEFINED) -} - -// Exported as function symbols so `Zig::GlobalObject::promiseHandlerID`'s -// address comparison matches; see `Bun__FileSink__onResolveStream` for why a -// `static` fn-ptr export would fail. -bun_jsc::jsc_host_abi! { - #[unsafe(export_name = "Bun__FetchTasklet__onResolveRequestStream")] - unsafe fn on_resolve_request_stream_shim( - g: *mut JSGlobalObject, - cf: *mut bun_jsc::CallFrame, - ) -> JSValue { - match on_resolve_request_stream(bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(cf)) { - Ok(v) => v, - Err(_) => JSValue::ZERO, - } - } -} -bun_jsc::jsc_host_abi! { - #[unsafe(export_name = "Bun__FetchTasklet__onRejectRequestStream")] - unsafe fn on_reject_request_stream_shim( - g: *mut JSGlobalObject, - cf: *mut bun_jsc::CallFrame, - ) -> JSValue { - match on_reject_request_stream(bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(cf)) { - Ok(v) => v, - Err(_) => JSValue::ZERO, - } + (*this).write_end_request(Some(err.to_js())); } } diff --git a/src/runtime/webcore/fetch/proxy_testing.bind.ts b/src/runtime/webcore/fetch/proxy_testing.bind.ts new file mode 100644 index 000000000000..64bb286b2c7a --- /dev/null +++ b/src/runtime/webcore/fetch/proxy_testing.bind.ts @@ -0,0 +1,17 @@ +import * as b from "../../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::webcore::fetch::proxy_testing" }); + +export const noProxyMatches = b.fn({ + args: { list: b.string.loose.utf8, hostname: b.string.loose.utf8, port: b.u16 }, + returns: b.bool, +}); + +export const proxyFor = b.fn({ + args: { entries: b.array(b.string.loose.utf8.owned), url: b.string.loose.utf8 }, + returns: b.string.local.nullable, + capability: "heap", + throws: true, +}); + +export const parseURL = b.fn({ args: { href: b.string.loose.utf8 }, returns: b.record(b.string.utf8) }); diff --git a/src/runtime/webcore/fetch/proxy_testing.rs b/src/runtime/webcore/fetch/proxy_testing.rs index 6206dc0dff2f..59f0b4ba9b08 100644 --- a/src/runtime/webcore/fetch/proxy_testing.rs +++ b/src/runtime/webcore/fetch/proxy_testing.rs @@ -1,57 +1,44 @@ //! `proxyInternals` of `bun:internal-for-testing`: the pure parts of proxy //! resolution, so their tables of cases need neither a server nor a subprocess. -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, bun_string_jsc}; +use bun_core::Utf8Bytes; +use bun_jsc::JSString; +use bun_jsc::bindgen::{self, JsHeap, JsLocal}; use bun_url::URL; /// `noProxyMatches(list, hostname, port)` -pub(crate) fn no_proxy_matches(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let list = frame.argument(0).to_utf8(global)?; - let hostname = frame.argument(1).to_utf8(global)?; - let port = frame.argument(2).to_int32(); - let Ok(port) = u16::try_from(port) else { - return Err(global.throw_invalid_arguments(format_args!("port must be a u16"))); - }; - Ok(JSValue::js_boolean(bun_dotenv::no_proxy::matches( - &list, &hostname, port, - ))) +pub(crate) fn no_proxy_matches(list: &[u8], hostname: &[u8], port: u16) -> bool { + bun_dotenv::no_proxy::matches(list, hostname, port) } /// `proxyFor([name, value, name, value, ...], url)`: the proxy an environment /// with exactly those variables selects for `url`, or null. -pub(crate) fn proxy_for(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { +pub(crate) fn proxy_for<'a>( + heap: &JsHeap<'a>, + entries: Vec>, + url: &[u8], +) -> bindgen::Result<'a, Option>> { let mut env = bun_dotenv::Loader::init(); - let mut entries = frame.argument(0).array_iterator(global)?; - while let Some(name) = entries.next()? { - let Some(value) = entries.next()? else { - return Err(global.throw_invalid_arguments(format_args!("a name without a value"))); + let mut entries = entries.iter(); + while let Some(name) = entries.next() { + let Some(value) = entries.next() else { + return Err(heap.invalid_arguments(format_args!("a name without a value"))); }; - bun_core::handle_oom(env.map.put(&name.to_utf8(global)?, &value.to_utf8(global)?)); - } - let href = frame.argument(1).to_utf8(global)?; - match env.get_http_proxy_for(&URL::parse(&href)) { - Some(proxy) => bun_string_jsc::create_utf8_for_js(global, proxy.href), - None => Ok(JSValue::NULL), + bun_core::handle_oom(env.map.put(name, value)); } + env.get_http_proxy_for(&URL::parse(url)) + .map(|proxy| heap.create_utf8_string(proxy.href)) + .transpose() } /// `parseURL(href)`: what the HTTP client's URL parser makes of `href`. -pub(crate) fn parse_url(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let href = frame.argument(0).to_utf8(global)?; - let url = URL::parse(&href); - let object = JSValue::create_empty_object(global, 5); - for (name, value) in [ - (&b"username"[..], url.username), +pub(crate) fn parse_url(href: &[u8]) -> [(&'static [u8], &[u8]); 5] { + let url = URL::parse(href); + [ + (b"username", url.username), (b"password", url.password), (b"hostname", url.hostname), (b"port", url.port), (b"pathname", url.pathname), - ] { - object.put( - global, - name, - bun_string_jsc::create_utf8_for_js(global, value)?, - ); - } - Ok(object) + ] } diff --git a/src/runtime/webcore/prompt.bind.ts b/src/runtime/webcore/prompt.bind.ts new file mode 100644 index 000000000000..5bbfc44aeace --- /dev/null +++ b/src/runtime/webcore/prompt.bind.ts @@ -0,0 +1,10 @@ +import * as b from "../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::webcore::prompt" }); + +/** `alert()` shows a message of its own. `alert(undefined)` shows "undefined". */ +const dialog = { args: { args: b.any.variadic }, capability: "js", throws: true, length: 1 } as const; + +export const alert = b.fn(dialog); +export const confirm = b.fn({ ...dialog, returns: b.bool }); +export const prompt = b.fn({ ...dialog, returns: b.any, rustPath: "crate::webcore::prompt::prompt::prompt" }); diff --git a/src/runtime/webcore/prompt.rs b/src/runtime/webcore/prompt.rs index 412cd2bf92ef..00c6992a6fe7 100644 --- a/src/runtime/webcore/prompt.rs +++ b/src/runtime/webcore/prompt.rs @@ -1,15 +1,16 @@ //! Implements prompt, alert, and confirm Web API -use crate::webcore::jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; +use crate::webcore::jsc::{JSValue, JsResult}; use bun_collections::VecExt as _; use bun_core::EncodedSlice; use bun_core::Output; use bun_jsc::EncodedSliceJsc as _; +use bun_jsc::bindgen::{JsLocal, JsScope}; /// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-alert -#[bun_jsc::host_fn(export = "WebCore__alert")] -fn alert(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let arguments = frame.arguments(); +pub(crate) fn alert(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult<()> { + let global = cx.global(); + let arguments = JsLocal::to_js_slice(args); let output = Output::writer(); let has_message = !arguments.is_empty(); @@ -27,7 +28,7 @@ fn alert(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { // 5. Show message to the user, treating U+000A LF as a line break. if output.write_all(message.slice()).is_err() { // 1. If we cannot show simple dialogs for this, then return. - return Ok(JSValue::UNDEFINED); + return Ok(()); } } } @@ -41,7 +42,7 @@ fn alert(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { .is_err() { // 1. If we cannot show simple dialogs for this, then return. - return Ok(JSValue::UNDEFINED); + return Ok(()); } // 6. Invoke WebDriver BiDi user prompt opened with this, "alert", and message. @@ -60,12 +61,12 @@ fn alert(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { // 8. Invoke WebDriver BiDi user prompt closed with this and true. // * Again, not necessary in a server context. - Ok(JSValue::UNDEFINED) + Ok(()) } -#[bun_jsc::host_fn(export = "WebCore__confirm")] -fn confirm(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let arguments = frame.arguments(); +pub(crate) fn confirm(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult { + let global = cx.global(); + let arguments = JsLocal::to_js_slice(args); let output = Output::writer(); let has_message = !arguments.is_empty(); @@ -79,7 +80,7 @@ fn confirm(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { if output.write_all(message.slice()).is_err() { // 1. If we cannot show simple dialogs for this, then return false. - return Ok(JSValue::FALSE); + return Ok(false); } } @@ -95,7 +96,7 @@ fn confirm(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { .is_err() { // 1. If we cannot show simple dialogs for this, then return false. - return Ok(JSValue::FALSE); + return Ok(false); } // 5. Invoke WebDriver BiDi user prompt opened with this, "confirm", and message. @@ -106,7 +107,7 @@ fn confirm(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { let mut reader = Output::stdin_reader(); let Ok(first_byte) = reader.take_byte() else { - return Ok(JSValue::FALSE); + return Ok(false); }; // 7. Invoke WebDriver BiDi user prompt closed with this, and true if @@ -114,34 +115,34 @@ fn confirm(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { // * Not relevant in a server context. match first_byte { - b'\n' => return Ok(JSValue::FALSE), + b'\n' => return Ok(false), b'\r' => { let Ok(next_byte) = reader.take_byte() else { // They may have said yes, but the stdin is invalid. - return Ok(JSValue::FALSE); + return Ok(false); }; if next_byte == b'\n' { - return Ok(JSValue::FALSE); + return Ok(false); } } b'y' | b'Y' => { let Ok(next_byte) = reader.take_byte() else { // They may have said yes, but the stdin is invalid. - return Ok(JSValue::FALSE); + return Ok(false); }; if next_byte == b'\n' { // 8. If the user responded positively, return true; // otherwise, the user responded negatively: return false. - return Ok(JSValue::TRUE); + return Ok(true); } else if next_byte == b'\r' { // Check Windows style let Ok(second_byte) = reader.take_byte() else { - return Ok(JSValue::FALSE); + return Ok(false); }; if second_byte == b'\n' { - return Ok(JSValue::TRUE); + return Ok(true); } } } @@ -156,7 +157,7 @@ fn confirm(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { // 8. If the user responded positively, return true; otherwise, the user // responded negatively: return false. - Ok(JSValue::FALSE) + Ok(false) } pub(crate) mod prompt { @@ -228,9 +229,9 @@ pub(crate) mod prompt { } /// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-prompt - #[bun_jsc::host_fn(export = "WebCore__prompt")] - fn call(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let arguments = frame.arguments(); + pub(crate) fn prompt(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult { + let global = cx.global(); + let arguments = JsLocal::to_js_slice(args); let output = Output::writer(); let has_message = !arguments.is_empty(); let has_default = arguments.len() >= 2; diff --git a/src/runtime/webcore/s3/client.bind.ts b/src/runtime/webcore/s3/client.bind.ts new file mode 100644 index 000000000000..a57ba7ff1e19 --- /dev/null +++ b/src/runtime/webcore/s3/client.bind.ts @@ -0,0 +1,7 @@ +import * as b from "../../../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::webcore::__s3_client" }); + +const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +export const onResolveUploadStream = b.fn({ ...reaction, export: "Bun__S3UploadStream__onResolveStream" }); +export const onRejectUploadStream = b.fn({ ...reaction, export: "Bun__S3UploadStream__onRejectStream" }); diff --git a/src/runtime/webcore/s3/client.rs b/src/runtime/webcore/s3/client.rs index 2c797c4e2e5b..9183caff52bf 100644 --- a/src/runtime/webcore/s3/client.rs +++ b/src/runtime/webcore/s3/client.rs @@ -7,7 +7,7 @@ use bun_collections::{ByteVecExt, VecExt}; use bun_core::MutableString; use bun_http::HeadersExt as _; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{GlobalRef, JSGlobalObject, JSValue, JsCell, JsResult}; +use bun_jsc::{GlobalRef, JSValue, JsCell, JsResult}; use bun_ptr::RefPtr; // Re-exports (thin aliases) @@ -44,6 +44,9 @@ pub(crate) use crate::webcore::s3::simple_request::S3UploadResult; use crate::webcore::s3::simple_request as s3_simple_request; +use crate::generated_bindgen::client::{ + Bun__S3UploadStream__onRejectStream, Bun__S3UploadStream__onResolveStream, +}; use crate::webcore::ByteStream; use crate::webcore::ReadableStream; use crate::webcore::readable_stream::Source as ReadableStreamPtr; @@ -53,7 +56,7 @@ use crate::webcore::streams::{NetworkSink, NetworkSinkJSSink}; use bun_collections::IntegerBitSet; use bun_io::KeepAlive; use bun_io::StreamBuffer; -use bun_jsc::CallFrame; +use bun_jsc::bindgen::{JsLocal, JsScope}; bun_core::declare_scope!(S3UploadStream, visible); @@ -736,59 +739,23 @@ impl S3UploadStreamWrapper { } } -fn s3_upload_stream_on_resolve( - _global_this: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let args = callframe.arguments(); +pub(crate) fn on_resolve_upload_stream(_: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { // The cell hands back the ref `upload_stream` gave it, which `handle_resolve_stream` balances. if let Some(this) = - crate::api::native_promise_context::take::(args[args.len() - 1]) + crate::api::native_promise_context::take::(context.to_js()) { // SAFETY: that ref keeps the wrapper alive. unsafe { (*this.as_ptr()).handle_resolve_stream() }; } - Ok(JSValue::UNDEFINED) } -fn s3_upload_stream_on_reject( - _global_this: &JSGlobalObject, - callframe: &CallFrame, -) -> JsResult { - let args = callframe.arguments(); - let err = args[0]; - // As `s3_upload_stream_on_resolve`; `handle_reject_stream` balances the ref. +pub(crate) fn on_reject_upload_stream(_: &mut JsScope<'_>, err: JsLocal<'_>, context: JsLocal<'_>) { + // As `on_resolve_upload_stream`; `handle_reject_stream` balances the ref. if let Some(this) = - crate::api::native_promise_context::take::(args[args.len() - 1]) + crate::api::native_promise_context::take::(context.to_js()) { // SAFETY: that ref keeps the wrapper alive. - unsafe { (*this.as_ptr()).handle_reject_stream(err) }; - } - Ok(JSValue::UNDEFINED) -} - -bun_jsc::jsc_host_abi! { - #[unsafe(export_name = "Bun__S3UploadStream__onResolveStream")] - unsafe fn s3_upload_stream_on_resolve_shim( - g: *mut JSGlobalObject, - cf: *mut CallFrame, - ) -> JSValue { - match s3_upload_stream_on_resolve(bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(cf)) { - Ok(v) => v, - Err(_) => JSValue::ZERO, - } - } -} -bun_jsc::jsc_host_abi! { - #[unsafe(export_name = "Bun__S3UploadStream__onRejectStream")] - unsafe fn s3_upload_stream_on_reject_shim( - g: *mut JSGlobalObject, - cf: *mut CallFrame, - ) -> JSValue { - match s3_upload_stream_on_reject(bun_opaque::opaque_deref(g), bun_opaque::opaque_deref(cf)) { - Ok(v) => v, - Err(_) => JSValue::ZERO, - } + unsafe { (*this.as_ptr()).handle_reject_stream(err.to_js()) }; } } @@ -1090,8 +1057,8 @@ pub(crate) fn upload_stream( ctx_ptr, JSValue::ZERO, ), - s3_upload_stream_on_resolve_shim, - s3_upload_stream_on_reject_shim, + Bun__S3UploadStream__onResolveStream, + Bun__S3UploadStream__onRejectStream, ); } bun_jsc::js_promise::Status::Fulfilled => { diff --git a/src/semver_jsc/SemverObject.bind.ts b/src/semver_jsc/SemverObject.bind.ts index ca2485e2de1c..20b87f542347 100644 --- a/src/semver_jsc/SemverObject.bind.ts +++ b/src/semver_jsc/SemverObject.bind.ts @@ -15,3 +15,9 @@ export const semver = b.namespace({ throws: true, }, }); + +export const intersects = b.fn({ + args: { left: b.string.loose.utf8, right: b.string.loose.utf8 }, + returns: b.bool, + throws: true, +}); diff --git a/src/semver_jsc/SemverObject.rs b/src/semver_jsc/SemverObject.rs index 5f1e47755535..a91ac1536d2b 100644 --- a/src/semver_jsc/SemverObject.rs +++ b/src/semver_jsc/SemverObject.rs @@ -4,7 +4,6 @@ use core::cmp::Ordering; use bun_core::strings; use bun_jsc::bindgen::{self, JsHeap}; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult}; use bun_semver::{SlicedString, Version, query}; pub(crate) fn order<'a>( @@ -56,16 +55,8 @@ pub(crate) fn satisfies<'a>(version: &'a [u8], range: &'a [u8]) -> bindgen::Resu } /// `bun:internal-for-testing`: `Group::intersects` on two range strings. -#[bun_jsc::host_fn] -pub fn intersects_for_testing(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let left = frame.argument(0).to_utf8(global)?; - let right = frame.argument(1).to_utf8(global)?; - let (Ok(left_group), Ok(right_group)) = ( - query::parse(&left, SlicedString::init(&left, &left)), - query::parse(&right, SlicedString::init(&right, &right)), - ) else { - return Err(global.throw_out_of_memory()); - }; - let intersects = left_group.intersects(&left, &right_group, &right); - Ok(JSValue::js_boolean(intersects)) +pub(crate) fn intersects<'a>(left: &'a [u8], right: &'a [u8]) -> bindgen::Result<'a, bool> { + let left_group = query::parse(left, SlicedString::init(left, left))?; + let right_group = query::parse(right, SlicedString::init(right, right))?; + Ok(left_group.intersects(left, &right_group, right)) } diff --git a/src/sourcemap_jsc/build.rs b/src/sourcemap_jsc/build.rs new file mode 100644 index 000000000000..874d6ca9f0b3 --- /dev/null +++ b/src/sourcemap_jsc/build.rs @@ -0,0 +1,35 @@ +// Build scripts run on the host before bun_* crates are compiled; std is the only option. +#![allow( + clippy::disallowed_methods, + clippy::disallowed_types, + clippy::disallowed_macros +)] +//! Export `BUN_CODEGEN_DIR` for `generated_bindgen.rs`, which `include!`s what `src/codegen/bindgen.ts` writes. + +use std::env; +use std::path::{Path, PathBuf}; + +fn main() { + let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let repo = manifest + .parent() + .and_then(Path::parent) + .expect("repo root from CARGO_MANIFEST_DIR") + .to_path_buf(); + + let codegen_dir = env::var("BUN_CODEGEN_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| repo.join("build/debug/codegen")); + + let generated = codegen_dir.join("generated_bindgen_bun_sourcemap_jsc.rs"); + if !generated.exists() { + panic!( + "generated_bindgen_bun_sourcemap_jsc.rs not found at {} — run `bun bd` first", + generated.display() + ); + } + + println!("cargo:rerun-if-changed={}", generated.display()); + println!("cargo:rustc-env=BUN_CODEGEN_DIR={}", codegen_dir.display()); + println!("cargo:rerun-if-env-changed=BUN_CODEGEN_DIR"); +} diff --git a/src/sourcemap_jsc/generated_bindgen.rs b/src/sourcemap_jsc/generated_bindgen.rs new file mode 100644 index 000000000000..660d33594642 --- /dev/null +++ b/src/sourcemap_jsc/generated_bindgen.rs @@ -0,0 +1,6 @@ +//! What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. + +include!(concat!( + env!("BUN_CODEGEN_DIR"), + "/generated_bindgen_bun_sourcemap_jsc.rs" +)); diff --git a/src/sourcemap_jsc/internal_jsc.bind.ts b/src/sourcemap_jsc/internal_jsc.bind.ts new file mode 100644 index 000000000000..92d9d908e764 --- /dev/null +++ b/src/sourcemap_jsc/internal_jsc.bind.ts @@ -0,0 +1,32 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::internal_jsc" }); + +export const fromVLQ = b.fn({ + args: { vlq: b.string.loose.utf8 }, + returns: b.bufferSource, + capability: "heap", + throws: true, +}); + +export const toVLQ = b.fn({ + args: { blob: b.bytes({ from: ["buffer"] }) }, + returns: b.string.utf8, + capability: "heap", + throws: true, +}); + +export const InternalSourceMapMapping = b.dictionary({ + generatedLine: b.i32, + generatedColumn: b.i32, + originalLine: b.i32, + originalColumn: b.i32, + sourceIndex: b.i32, +}); + +export const find = b.fn({ + args: { blob: b.bytes({ from: ["buffer"] }), line: b.any, col: b.any }, + returns: InternalSourceMapMapping.nullable, + capability: "heap", + throws: true, +}); diff --git a/src/sourcemap_jsc/internal_jsc.rs b/src/sourcemap_jsc/internal_jsc.rs index 696acddfb34e..70684756189f 100644 --- a/src/sourcemap_jsc/internal_jsc.rs +++ b/src/sourcemap_jsc/internal_jsc.rs @@ -1,104 +1,63 @@ //! JS testing bindings for `InternalSourceMap`. Keeps `src/sourcemap/` free of JSC types. use bun_core::MutableString; -use bun_jsc::{ArrayBuffer, CallFrame, JSGlobalObject, JSValue, JsResult, bun_string_jsc}; +use bun_jsc::bindgen::{self, BufferSource, JsHeap, JsLocal}; use bun_sourcemap::Ordinal; use bun_sourcemap::internal_source_map::{self, InternalSourceMap}; -struct TestingAPIs; +use crate::generated_bindgen::internal_jsc::InternalSourceMapMapping; -impl TestingAPIs { - fn from_vlq(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let vlq_str = frame.argument(0).to_bun_string(global)?; - let vlq = vlq_str.to_utf8(); +pub(crate) fn from_vlq<'a>( + heap: &JsHeap<'a>, + vlq: &[u8], +) -> bindgen::Result<'a, JsLocal<'a, BufferSource>> { + let Ok(blob) = internal_source_map::from_vlq(vlq) else { + return Err(heap.error(format_args!("InternalSourceMap.fromVLQ: invalid VLQ input"))); + }; + heap.create_uint8_array(&blob) +} - let Ok(blob) = internal_source_map::from_vlq(vlq.slice()) else { - return Err(global.throw(format_args!("InternalSourceMap.fromVLQ: invalid VLQ input"))); - }; - ArrayBuffer::create_uint8_array(global, &blob) +pub(crate) fn to_vlq<'a>(heap: &JsHeap<'a>, blob: &[u8]) -> bindgen::Result<'a, Vec> { + if !internal_source_map::is_valid_blob(blob) { + return Err(heap.error(format_args!("InternalSourceMap.toVLQ: invalid blob"))); } + let ism = InternalSourceMap { + data: blob.as_ptr(), + }; + let mut out = MutableString::init_empty(); + ism.append_vlq_to(&mut out); + Ok(out.list) +} - fn to_vlq(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let Some(ab) = frame.argument(0).as_array_buffer(global) else { - return Err(global.throw(format_args!("InternalSourceMap.toVLQ: expected Uint8Array"))); - }; - let bytes = ab.byte_slice(); - if !internal_source_map::is_valid_blob(bytes) { - return Err(global.throw(format_args!("InternalSourceMap.toVLQ: invalid blob"))); - } - let ism = InternalSourceMap { - data: bytes.as_ptr(), - }; - let mut out = MutableString::init_empty(); - ism.append_vlq_to(&mut out); - bun_string_jsc::create_utf8_for_js(global, out.list.as_slice()) +pub(crate) fn find<'a>( + heap: &JsHeap<'a>, + blob: &[u8], + line: JsLocal<'a>, + col: JsLocal<'a>, +) -> bindgen::Result<'a, Option> { + if !internal_source_map::is_valid_blob(blob) { + return Err(heap.error(format_args!("InternalSourceMap.find: invalid blob"))); } - - fn find(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - let Some(ab) = frame.argument(0).as_array_buffer(global) else { - return Err(global.throw(format_args!("InternalSourceMap.find: expected Uint8Array"))); - }; - let bytes = ab.byte_slice(); - if !internal_source_map::is_valid_blob(bytes) { - return Err(global.throw(format_args!("InternalSourceMap.find: invalid blob"))); - } - let line = frame.argument(1).to_int32(); - let col = frame.argument(2).to_int32(); - if line < 0 || col < 0 { - return Ok(JSValue::NULL); - } - let ism = InternalSourceMap { - data: bytes.as_ptr(), - }; - let Some(mapping) = ism.find( - Ordinal::from_zero_based(line), - Ordinal::from_zero_based(col), - ) else { - return Ok(JSValue::NULL); - }; - - let obj = JSValue::create_empty_object(global, 5); - obj.put( - global, - b"generatedLine", - JSValue::js_number(mapping.generated.lines.zero_based() as f64), - ); - obj.put( - global, - b"generatedColumn", - JSValue::js_number(mapping.generated.columns.zero_based() as f64), - ); - obj.put( - global, - b"originalLine", - JSValue::js_number(mapping.original.lines.zero_based() as f64), - ); - obj.put( - global, - b"originalColumn", - JSValue::js_number(mapping.original.columns.zero_based() as f64), - ); - obj.put( - global, - b"sourceIndex", - JSValue::js_number(mapping.source_index as f64), - ); - Ok(obj) + let line = line.to_js().to_int32(); + let col = col.to_js().to_int32(); + if line < 0 || col < 0 { + return Ok(None); } -} - -/// Free-fn aliases of the [`TestingAPIs`] associated fns so -/// `bun_runtime::dispatch::js2native` can `pub use` them (associated fns -/// aren't importable items). -#[inline] -pub fn testing_from_vlq(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - TestingAPIs::from_vlq(global, frame) -} -#[inline] -pub fn testing_to_vlq(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - TestingAPIs::to_vlq(global, frame) -} -#[inline] -pub fn testing_find(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { - TestingAPIs::find(global, frame) + let ism = InternalSourceMap { + data: blob.as_ptr(), + }; + let Some(mapping) = ism.find( + Ordinal::from_zero_based(line), + Ordinal::from_zero_based(col), + ) else { + return Ok(None); + }; + + Ok(Some(InternalSourceMapMapping { + generated_line: mapping.generated.lines.zero_based(), + generated_column: mapping.generated.columns.zero_based(), + original_line: mapping.original.lines.zero_based(), + original_column: mapping.original.columns.zero_based(), + source_index: mapping.source_index, + })) } diff --git a/src/sourcemap_jsc/lib.rs b/src/sourcemap_jsc/lib.rs index 2966b86a7015..18b4dffada83 100644 --- a/src/sourcemap_jsc/lib.rs +++ b/src/sourcemap_jsc/lib.rs @@ -2,4 +2,5 @@ #[path = "CodeCoverage.rs"] pub mod code_coverage; -pub mod internal_jsc; +pub(crate) mod generated_bindgen; +pub(crate) mod internal_jsc; diff --git a/src/sql_jsc/jsc.rs b/src/sql_jsc/jsc.rs index 44e92da4e3e7..ed30e6c0adb9 100644 --- a/src/sql_jsc/jsc.rs +++ b/src/sql_jsc/jsc.rs @@ -1,6 +1,6 @@ //! `bun_jsc` re-export façade for the SQL bindings. //! -//! All core handle types (`JSValue`, `JSGlobalObject`, `CallFrame`, `JsError`, +//! All core handle types (`JSValue`, `JSGlobalObject`, `JsError`, //! `JsResult`, `JSObject`, `JSType`, [`VirtualMachine`], //! [`EventLoop`], [`KeepAlive`], …) are **re-exported from `bun_jsc` / //! `bun_io`**. SQL-specific helpers that `bun_jsc` doesn't expose at @@ -17,13 +17,12 @@ use core::cell::Cell; use core::ffi::{c_char, c_void}; -use core::marker::PhantomData; use core::ptr::NonNull; pub use bun_jsc::{ - CallFrame, ErrorBuilder, ErrorCode, ExternColumnIdentifier, GlobalRef, JSArrayIterator, - JSGlobalObject, JSObject, JSType, JSValue, JsCell, JsError, JsRef, JsResult, - MarkedArgumentBuffer, StringJsc, Strong, StrongOptional, bun_string_jsc, + ErrorBuilder, ErrorCode, ExternColumnIdentifier, GlobalRef, JSArrayIterator, JSGlobalObject, + JSObject, JSType, JSValue, JsCell, JsError, JsRef, JsResult, MarkedArgumentBuffer, StringJsc, + Strong, StrongOptional, bun_string_jsc, }; pub use bun_jsc::IntegerRange; @@ -481,181 +480,6 @@ pub mod api { } } -// `bun_jsc::JSFunction::create` has another signature. SQL only needs the `JSHostFn` thunk. -#[repr(C)] -pub(crate) struct JSFunction { - _opaque: [u8; 0], - _m: PhantomData<(*mut u8, core::marker::PhantomPinned)>, -} - -/// `jsc.JSHostFn` — the JSC-ABI host-function pointer JSC dispatches to -/// (`extern "sysv64"` on win-x64, `extern "C"` elsewhere). Re-exported from -/// `bun_jsc` so the cfg-split lives in one place. -pub use bun_jsc::host_fn::JsHostFn as JSHostFn; - -pub(crate) trait IntoJSHostFn: Sized { - fn into_js_host_fn(self) -> JSHostFn; -} -#[doc(hidden)] -pub(crate) struct HostFnResult; -#[doc(hidden)] -pub(crate) struct HostFnPlain; - -// `jsc_host_abi!` can't express a generic `where` clause, so cfg-split the -// thunk body manually (sysv64 on win-x64, C elsewhere — matches `JSHostFn`). -// The where-clause is bracketed to avoid `tt`-muncher ambiguity against `{`. -// Thunk bodies scope their raw-ptr derefs locally, so the fn itself has no -// caller preconditions; a safe `extern fn` coerces to the `JSHostFn` type. -macro_rules! sql_jsc_host_thunk { - ($name:ident<$F:ident>($($args:tt)*) -> $ret:ty where [$($bound:tt)+] $body:block) => { - #[cfg(all(windows, target_arch = "x86_64"))] - extern "sysv64" fn $name<$F>($($args)*) -> $ret where $($bound)+ $body - #[cfg(not(all(windows, target_arch = "x86_64")))] - extern "C" fn $name<$F>($($args)*) -> $ret where $($bound)+ $body - }; -} - -impl IntoJSHostFn for F -where - F: Fn(&JSGlobalObject, &CallFrame) -> JsResult + Copy + 'static, -{ - fn into_js_host_fn(self) -> JSHostFn { - debug_assert_eq!( - core::mem::size_of::(), - 0, - "IntoJSHostFn: expected fn item (ZST)" - ); - let _ = self; - sql_jsc_host_thunk! { - thunk(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue - where [F: Fn(&JSGlobalObject, &CallFrame) -> JsResult + Copy + 'static] - { - let f: F = bun_core::ffi::conjure_zst::(); - // JSC passes live non-null `*JSGlobalObject` / `*CallFrame`; both - // strictly outlive the host-fn call, satisfying the `ParentRef` - // invariant. Safe `From` + `Deref` collapse the per-thunk - // raw `&*ptr` pair to one audited deref site in `bun_ptr`. - let global = bun_ptr::ParentRef::from(NonNull::new(g).expect("JSC host fn: global non-null")); - let frame = bun_ptr::ParentRef::from(NonNull::new(c).expect("JSC host fn: callframe non-null")); - match f(&global, &frame) { - Ok(v) => v, - Err(JsError::OutOfMemory) => { let _ = global.throw_out_of_memory(); JSValue::ZERO } - Err(_) => JSValue::ZERO, - } - } - } - thunk:: - } -} -impl IntoJSHostFn for F -where - F: Fn(&JSGlobalObject, &CallFrame) -> JSValue + Copy + 'static, -{ - fn into_js_host_fn(self) -> JSHostFn { - debug_assert_eq!( - core::mem::size_of::(), - 0, - "IntoJSHostFn: expected fn item (ZST)" - ); - let _ = self; - sql_jsc_host_thunk! { - thunk(g: *mut JSGlobalObject, c: *mut CallFrame) -> JSValue - where [F: Fn(&JSGlobalObject, &CallFrame) -> JSValue + Copy + 'static] - { - let f: F = bun_core::ffi::conjure_zst::(); - // JSC passes live non-null pointers; both outlive the host-fn - // call (the `ParentRef` invariant). Safe `Deref` recovers `&T`. - let global = bun_ptr::ParentRef::from(NonNull::new(g).expect("JSC host fn: global non-null")); - let frame = bun_ptr::ParentRef::from(NonNull::new(c).expect("JSC host fn: callframe non-null")); - f(&global, &frame) - } - } - thunk:: - } -} - -#[repr(u8)] -#[derive(Clone, Copy, Default)] -pub enum ImplementationVisibility { - #[default] - Public = 0, - Private = 1, - PrivateRecursive = 2, -} -#[repr(u8)] -#[derive(Clone, Copy, Default)] -pub enum Intrinsic { - #[default] - None = 0, -} -#[derive(Clone, Copy, Default)] -pub struct CreateJSFunctionOptions { - pub(crate) implementation_visibility: ImplementationVisibility, - pub(crate) intrinsic: Intrinsic, - pub(crate) constructor: Option, -} - -unsafe extern "C" { - // `&JSGlobalObject` is ABI-identical to a non-null `*const JSGlobalObject`; - // remaining args are by-value scalars/fn-ptrs. No caller-side memory - // preconditions remain → `safe fn`. - safe fn JSFunction__createFromZig( - global: &JSGlobalObject, - fn_name: &bun_core::String, - implementation: JSHostFn, - arg_count: u32, - implementation_visibility: ImplementationVisibility, - intrinsic: Intrinsic, - constructor: Option, - ) -> JSValue; -} - -/// `bun_jsc::create_host_function_object`-shaped helper for the SQL binding -/// objects. Macro (not fn) because each entry's `$f` is a *distinct* fn-item -/// ZST routed through [`IntoJSHostFn`] — a `&[(&str, JSHostFn, u32)]` slice -/// can't hold heterogeneous safe-Rust signatures. Expands to the same -/// `put`/`JSFunction::create` ladder the open-coded sites used; returns the -/// receiver for chaining. -#[macro_export] -macro_rules! put_host_functions { - ($obj:expr, $global:expr, [ $( ($name:literal, $f:expr, $arity:expr) ),* $(,)? ]) => {{ - let __obj: $crate::jsc::JSValue = $obj; - let __g = $global; - $( - __obj.put( - __g, - $name.as_bytes(), - $crate::jsc::JSFunction::create(__g, $name, $f, $arity, ::core::default::Default::default()), - ); - )* - __obj - }}; -} - -impl JSFunction { - /// Accepts a safe Rust `fn(&JSGlobalObject, &CallFrame) -> JSValue` / - /// `-> JsResult` via [`IntoJSHostFn`]. - pub(crate) fn create>( - global: &JSGlobalObject, - name: &str, - implementation: F, - arg_count: u32, - opts: CreateJSFunctionOptions, - ) -> JSValue { - let implementation: JSHostFn = implementation.into_js_host_fn(); - let fn_name = bun_core::String::from_bytes(name.as_bytes()); - JSFunction__createFromZig( - global, - &fn_name, - implementation, - arg_count, - opts.implementation_visibility, - opts.intrinsic, - opts.constructor, - ) - } -} - // Opaque handle to `bun_runtime::api::SSLContextCache` (owned by // `RuntimeState`). Reached via [`VirtualMachineSqlExt::ssl_ctx_cache`]; backed // by [`SqlRuntimeHooks::ssl_ctx_cache`] / `ssl_ctx_get_or_create`. diff --git a/src/sql_jsc/lib.rs b/src/sql_jsc/lib.rs index a43e63b7da32..7d32779c4ba5 100644 --- a/src/sql_jsc/lib.rs +++ b/src/sql_jsc/lib.rs @@ -15,7 +15,7 @@ pub use error::{Error, Result}; pub(crate) mod generated_bindgen; pub mod jsc; -pub use jsc::{CallFrame, JSGlobalObject, JSValue}; +pub use jsc::{JSGlobalObject, JSValue}; pub mod mysql; pub mod postgres; diff --git a/src/sql_jsc/mysql.bind.ts b/src/sql_jsc/mysql.bind.ts new file mode 100644 index 000000000000..acdbd3da9a5b --- /dev/null +++ b/src/sql_jsc/mysql.bind.ts @@ -0,0 +1,23 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +const js = { returns: b.any, capability: "js", throws: true } as const; + +export const binding = b.namespace({ + init: { + args: { onResolveQuery: b.any, onRejectQuery: b.any }, + capability: "heap", + length: 0, + rustPath: "crate::mysql::my_sql_context::init", + }, + createQuery: { + ...js, + args: { query: b.any, values: b.any, pendingValue: b.any, bigint: b.any, simple: b.any }, + rustPath: "crate::mysql::js_my_sql_query::JSMySQLQuery::create_query", + }, + createConnection: { + ...js, + args: { args: b.any.variadic }, + length: 2, + rustPath: "crate::mysql::js_my_sql_connection::JSMySQLConnection::create_connection", + }, +}); diff --git a/src/sql_jsc/mysql.rs b/src/sql_jsc/mysql.rs index fb61006e843f..025c7bfcf155 100644 --- a/src/sql_jsc/mysql.rs +++ b/src/sql_jsc/mysql.rs @@ -1,26 +1,3 @@ -use crate::jsc::{JSGlobalObject, JSValue}; - -pub fn create_binding(global_object: &JSGlobalObject) -> JSValue { - let binding = JSValue::create_empty_object_with_null_prototype(global_object); - crate::put_host_functions!( - binding, - global_object, - [ - ("init", my_sql_context::init, 0), - ( - "createQuery", - js_my_sql_query::JSMySQLQuery::create_instance, - 5 - ), - ( - "createConnection", - js_my_sql_connection::JSMySQLConnection::create_instance, - 2 - ), - ] - ) -} - // ────────────────────────────────────────────────────────────────────────── // Submodule tree (files use PascalCase basenames; wired via `#[path]`). // ────────────────────────────────────────────────────────────────────────── diff --git a/src/sql_jsc/mysql/JSMySQLConnection.rs b/src/sql_jsc/mysql/JSMySQLConnection.rs index 725ecc4048c3..60c6696d1bb6 100644 --- a/src/sql_jsc/mysql/JSMySQLConnection.rs +++ b/src/sql_jsc/mysql/JSMySQLConnection.rs @@ -1,13 +1,12 @@ use core::cell::Cell; use core::ffi::c_void; -use bun_jsc::bindgen::JsScope; +use bun_jsc::bindgen::{JsLocal, JsScope}; use crate::generated_bindgen::js_my_sql_connection::js_MySQLConnection as js; use crate::jsc::{ - AutoFlush, CallFrame, EventLoopSqlExt as _, EventLoopTimerTag, GlobalRef, JSGlobalObject, - JSValue, JsCell, JsRef, JsResult, KeepAlive, VirtualMachine, VirtualMachineSqlExt as _, - run_callback, + AutoFlush, EventLoopSqlExt as _, EventLoopTimerTag, GlobalRef, JSGlobalObject, JSValue, JsCell, + JsRef, JsResult, KeepAlive, VirtualMachine, VirtualMachineSqlExt as _, run_callback, }; use crate::shared::CachedStructure; use crate::shared::cached_structure::{column_name_to_js, transform_name}; @@ -251,14 +250,16 @@ impl JSMySQLConnection { self.poll_ref.with_mut(|p| p.unref(ctx)); } - pub(crate) fn create_instance( - global_object: &JSGlobalObject, - callframe: &CallFrame, + pub(crate) fn create_connection<'a>( + cx: &mut JsScope<'a>, + args: &'a [JsLocal<'a>], ) -> JsResult { + let global_object = cx.global(); let vm = global_object.bun_vm().as_mut(); // The connection is the calling script's. - let context = global_object.bun_vm().context_of_caller(callframe); - let arguments = callframe.arguments(); + let context = global_object.bun_vm().context_of_caller_no_frame(); + let arguments = JsLocal::to_js_slice(args); + let argument = |i: usize| arguments.get(i).copied().unwrap_or(JSValue::UNDEFINED); let Some(args) = ConnectionCtorArgs::parse(global_object, &mut *vm, arguments)? else { return Ok(JSValue::ZERO); }; @@ -285,9 +286,9 @@ impl JSMySQLConnection { let on_connect = arguments[9]; let on_close = arguments[10]; - let allow_public_key_retrieval = callframe.argument(15).to_boolean(); - let on_retire = callframe.argument(16); - let found_rows = callframe.argument(17).to_boolean(); + let allow_public_key_retrieval = argument(15).to_boolean(); + let on_retire = argument(16); + let found_rows = argument(17).to_boolean(); let ptr: *mut JSMySQLConnection = bun_core::heap::into_raw(Box::new(JSMySQLConnection { ref_count: Cell::new(1), @@ -374,7 +375,7 @@ impl JSMySQLConnection { this.js_value .with_mut(|r| r.set_strong(js_value, global_object)); js::onconnect_set_cached(js_value, global_object, on_connect); - let column_transform = callframe.argument(19); + let column_transform = argument(19); if column_transform.is_callable() { js::column_transform_set_cached(js_value, global_object, column_transform); } diff --git a/src/sql_jsc/mysql/JSMySQLQuery.rs b/src/sql_jsc/mysql/JSMySQLQuery.rs index 82cdfec7a972..f37ff6c8841a 100644 --- a/src/sql_jsc/mysql/JSMySQLQuery.rs +++ b/src/sql_jsc/mysql/JSMySQLQuery.rs @@ -3,7 +3,7 @@ use core::ptr::NonNull; use crate::generated_bindgen::js_my_sql_query::js_MySQLQuery as js; use crate::jsc::{ - CallFrame, JSGlobalObject, JSValue, JsRef, JsResult, MarkedArgumentBuffer, VirtualMachine, + JSGlobalObject, JSValue, JsRef, JsResult, MarkedArgumentBuffer, VirtualMachine, VirtualMachineSqlExt as _, bun_string_jsc, }; use crate::shared::cached_structure::Undescribed; @@ -52,18 +52,29 @@ impl JSMySQLQuery { self.this_value.with_mut(|v| v.finalize()); } - // Reached from JS via `put_host_functions!` in `mysql.rs`. - pub(crate) fn create_instance( - global_this: &JSGlobalObject, - callframe: &CallFrame, + pub(crate) fn create_query<'a>( + cx: &mut JsScope<'a>, + query: JsLocal<'a>, + values: JsLocal<'a>, + pending_value: JsLocal<'a>, + bigint: JsLocal<'a>, + simple: JsLocal<'a>, ) -> JsResult { + let global_this = cx.global(); let QueryCtorArgs { query, values, pending_value, bigint, simple, - } = QueryCtorArgs::parse(global_this, callframe.arguments())?; + } = QueryCtorArgs::parse( + global_this, + query.to_js(), + values.to_js(), + pending_value.to_js(), + bigint.to_js(), + simple.to_js(), + )?; let this_ptr = bun_core::heap::into_raw(Box::new(Self { this_value: JsCell::new(JsRef::empty()), diff --git a/src/sql_jsc/mysql/MySQLContext.rs b/src/sql_jsc/mysql/MySQLContext.rs index 4268e5573c6a..60400647c9c6 100644 --- a/src/sql_jsc/mysql/MySQLContext.rs +++ b/src/sql_jsc/mysql/MySQLContext.rs @@ -1,4 +1,5 @@ -use crate::jsc::{CallFrame, JSGlobalObject, JSValue, StrongOptional, VirtualMachineSqlExt as _}; +use crate::jsc::{StrongOptional, VirtualMachineSqlExt as _}; +use bun_jsc::bindgen::{JsHeap, JsLocal}; #[repr(C)] #[derive(Default)] @@ -7,14 +8,18 @@ pub struct MySQLContext { pub(crate) on_query_reject_fn: StrongOptional, } -// The binding object is built in Rust (`mysql.rs` registers this fn through -// `put_host_functions!`/`IntoJSHostFn`), so no C symbol is needed. -pub(crate) fn init(global: &JSGlobalObject, frame: &CallFrame) -> JSValue { +pub(crate) fn init<'a>( + heap: &JsHeap<'a>, + on_resolve_query: JsLocal<'a>, + on_reject_query: JsLocal<'a>, +) { + // SAFETY: setting a `Strong` runs no JavaScript. + let global = unsafe { heap.global() }; // `bun_vm()` → `&'static VirtualMachine` (per-thread singleton); `as_mut()` // is the canonical safe escape hatch for the shrinking set of `&mut self` // helpers like `sql_state()` — one audited unsafe lives in bun_jsc. let ctx = &mut global.bun_vm().as_mut().sql_state().mysql_context; - ctx.on_query_resolve_fn.set(global, frame.argument(0)); - ctx.on_query_reject_fn.set(global, frame.argument(1)); - JSValue::UNDEFINED + ctx.on_query_resolve_fn + .set(global, on_resolve_query.to_js()); + ctx.on_query_reject_fn.set(global, on_reject_query.to_js()); } diff --git a/src/sql_jsc/postgres.bind.ts b/src/sql_jsc/postgres.bind.ts new file mode 100644 index 000000000000..7fe832500b5d --- /dev/null +++ b/src/sql_jsc/postgres.bind.ts @@ -0,0 +1,23 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +const js = { returns: b.any, capability: "js", throws: true } as const; + +export const binding = b.namespace({ + init: { + args: { onResolveQuery: b.any, onRejectQuery: b.any, arrayLiteral: b.any }, + capability: "heap", + length: 0, + rustPath: "crate::postgres::PostgresSQLContext::init", + }, + createQuery: { + ...js, + args: { query: b.any, values: b.any, pendingValue: b.any, bigint: b.any, simple: b.any }, + rustPath: "crate::postgres::PostgresSQLQuery::create_query", + }, + createConnection: { + ...js, + args: { args: b.any.variadic }, + length: 2, + rustPath: "crate::postgres::postgres_sql_connection::create_connection", + }, +}); diff --git a/src/sql_jsc/postgres.rs b/src/sql_jsc/postgres.rs index 1501779bf233..5ec9168c8a5f 100644 --- a/src/sql_jsc/postgres.rs +++ b/src/sql_jsc/postgres.rs @@ -1,18 +1,3 @@ -use crate::jsc::{JSGlobalObject, JSValue}; - -pub fn create_binding(global_object: &JSGlobalObject) -> JSValue { - let binding = JSValue::create_empty_object_with_null_prototype(global_object); - crate::put_host_functions!( - binding, - global_object, - [ - ("init", PostgresSQLContext::init, 0), - ("createQuery", PostgresSQLQuery::call, 5), - ("createConnection", postgres_sql_connection::call, 2), - ] - ) -} - // ────────────────────────────────────────────────────────────────────────── // Submodule tree (files use PascalCase basenames; wired via `#[path]`). // ────────────────────────────────────────────────────────────────────────── diff --git a/src/sql_jsc/postgres/PostgresSQLConnection.rs b/src/sql_jsc/postgres/PostgresSQLConnection.rs index 59812298a3ce..61c94be9b65c 100644 --- a/src/sql_jsc/postgres/PostgresSQLConnection.rs +++ b/src/sql_jsc/postgres/PostgresSQLConnection.rs @@ -1,13 +1,13 @@ use bun_collections::VecExt; use bun_jsc::JsCell; -use bun_jsc::bindgen::JsScope; +use bun_jsc::bindgen::{JsLocal, JsScope}; use core::cell::Cell; use core::ffi::c_void; use core::sync::atomic::Ordering; use crate::jsc::api::server_config::SSLConfig; use crate::jsc::{ - self as jsc, AutoFlush, CallFrame, JSGlobalObject, JSValue, JsResult, Strong, VirtualMachine, + self as jsc, AutoFlush, JSGlobalObject, JSValue, JsResult, Strong, VirtualMachine, VirtualMachineSqlExt as _, bun_string_jsc, run_callback, }; use bun_boringssl as BoringSSL; @@ -842,12 +842,16 @@ impl PostgresSQLConnection { } } -#[bun_jsc::host_fn(export = "PostgresSQLConnection__createInstance")] -pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +pub(crate) fn create_connection<'a>( + cx: &mut JsScope<'a>, + args: &'a [JsLocal<'a>], +) -> JsResult { + let global_object = cx.global(); let vm = global_object.bun_vm().as_mut(); // The connection is the calling script's. - let context = global_object.bun_vm().context_of_caller(callframe); - let arguments = callframe.arguments(); + let context = global_object.bun_vm().context_of_caller_no_frame(); + let arguments = JsLocal::to_js_slice(args); + let argument = |i: usize| arguments.get(i).copied().unwrap_or(JSValue::UNDEFINED); let Some(args) = ConnectionCtorArgs::parse(global_object, &mut *vm, arguments)? else { return Ok(JSValue::ZERO); }; @@ -855,10 +859,10 @@ pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsR // Next to `rejectUnauthorized: false` it is not asked, as in Node. let check_server_identity = server_identity::callback_of(global_object, arguments[6])? .filter(|_| verifies_certificate(&tls_config, args.ssl_mode)); - let channel_binding = ChannelBinding::from_js(global_object, callframe.argument(18))?; + let channel_binding = ChannelBinding::from_js(global_object, argument(18))?; let options_str = arguments[7].to_bun_string(global_object)?; - let scram_password_str = callframe.argument(20).to_bun_string(global_object)?; + let scram_password_str = argument(20).to_bun_string(global_object)?; let (username, password, scram_password, database, options, path) = ( args.username_str.to_utf8(), args.password_str.to_utf8(), @@ -910,8 +914,8 @@ pub(crate) fn call(global_object: &JSGlobalObject, callframe: &CallFrame) -> JsR use_unnamed_prepared_statements: arguments[14].as_boolean(), on_connect: arguments[9], on_close: arguments[10], - on_retire: callframe.argument(16), - column_transform: callframe.argument(19), + on_retire: argument(16), + column_transform: argument(19), check_server_identity, cancel: None, }, diff --git a/src/sql_jsc/postgres/PostgresSQLContext.rs b/src/sql_jsc/postgres/PostgresSQLContext.rs index fb28f7561770..4272df102a7b 100644 --- a/src/sql_jsc/postgres/PostgresSQLContext.rs +++ b/src/sql_jsc/postgres/PostgresSQLContext.rs @@ -2,7 +2,8 @@ //! `us_socket_context_t` that used to live here is gone — connections link //! into `RareData.postgres_group`/`postgres_tls_group` instead. -use crate::jsc::{CallFrame, JSGlobalObject, JSValue, StrongOptional, VirtualMachineSqlExt as _}; +use crate::jsc::{StrongOptional, VirtualMachineSqlExt as _}; +use bun_jsc::bindgen::{JsHeap, JsLocal}; #[repr(C)] #[derive(Default)] @@ -13,17 +14,22 @@ pub struct PostgresSQLContext { } impl PostgresSQLContext { - // Registered directly as `init` via `put_host_functions!` in - // `postgres.rs`, so no exported symbol is needed. - pub(crate) fn init(global: &JSGlobalObject, frame: &CallFrame) -> JSValue { + pub(crate) fn init<'a>( + heap: &JsHeap<'a>, + on_resolve_query: JsLocal<'a>, + on_reject_query: JsLocal<'a>, + array_literal: JsLocal<'a>, + ) { + // SAFETY: setting a `Strong` runs no JavaScript. + let global = unsafe { heap.global() }; // `bun_vm()` → `&'static VirtualMachine` (per-thread singleton); // `as_mut()` is the canonical safe escape hatch for the shrinking set // of `&mut self` helpers like `sql_state()` — one audited unsafe lives // in bun_jsc. let ctx = &mut global.bun_vm().as_mut().sql_state().postgresql_context; - ctx.on_query_resolve_fn.set(global, frame.argument(0)); - ctx.on_query_reject_fn.set(global, frame.argument(1)); - ctx.array_literal_fn.set(global, frame.argument(2)); - JSValue::UNDEFINED + ctx.on_query_resolve_fn + .set(global, on_resolve_query.to_js()); + ctx.on_query_reject_fn.set(global, on_reject_query.to_js()); + ctx.array_literal_fn.set(global, array_literal.to_js()); } } diff --git a/src/sql_jsc/postgres/PostgresSQLQuery.rs b/src/sql_jsc/postgres/PostgresSQLQuery.rs index 5e1e86524571..ff0fef1892c9 100644 --- a/src/sql_jsc/postgres/PostgresSQLQuery.rs +++ b/src/sql_jsc/postgres/PostgresSQLQuery.rs @@ -1,6 +1,6 @@ use core::cell::Cell; -use crate::jsc::{CallFrame, JSGlobalObject, JSValue, JsRef, JsResult, VirtualMachineSqlExt as _}; +use crate::jsc::{JSGlobalObject, JSValue, JsRef, JsResult, VirtualMachineSqlExt as _}; use crate::shared::cached_structure::Undescribed; use crate::shared::query_ctor_args::QueryCtorArgs; use bun_core::String as BunString; @@ -374,16 +374,29 @@ impl PostgresSQLQuery { 0 } - // Registered directly as `createQuery` via - // `put_host_functions!` in `postgres.rs`, so no exported symbol is needed. - pub(crate) fn call(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + pub(crate) fn create_query<'a>( + cx: &mut JsScope<'a>, + query: JsLocal<'a>, + values: JsLocal<'a>, + pending_value: JsLocal<'a>, + bigint: JsLocal<'a>, + simple: JsLocal<'a>, + ) -> JsResult { + let global_this = cx.global(); let QueryCtorArgs { query, values, pending_value, bigint, simple, - } = QueryCtorArgs::parse(global_this, callframe.arguments())?; + } = QueryCtorArgs::parse( + global_this, + query.to_js(), + values.to_js(), + pending_value.to_js(), + bigint.to_js(), + simple.to_js(), + )?; let ptr = bun_core::heap::into_raw(Box::new(PostgresSQLQuery::default())); diff --git a/src/sql_jsc/shared/QueryCtorArgs.rs b/src/sql_jsc/shared/QueryCtorArgs.rs index ce63d709fb70..22a0a8c5a9aa 100644 --- a/src/sql_jsc/shared/QueryCtorArgs.rs +++ b/src/sql_jsc/shared/QueryCtorArgs.rs @@ -13,15 +13,14 @@ pub(crate) struct QueryCtorArgs { } impl QueryCtorArgs { - pub(crate) fn parse(global_this: &JSGlobalObject, arguments: &[JSValue]) -> JsResult { - let mut args = arguments.iter().copied(); - let Some(query) = args.next() else { - return Err(global_this.throw(format_args!("query must be a string"))); - }; - let Some(values) = args.next() else { - return Err(global_this.throw(format_args!("values must be an array"))); - }; - + pub(crate) fn parse( + global_this: &JSGlobalObject, + query: JSValue, + values: JSValue, + pending_value: JSValue, + js_bigint: JSValue, + js_simple: JSValue, + ) -> JsResult { if !query.is_string() { return Err(global_this.throw(format_args!("query must be a string"))); } @@ -30,10 +29,6 @@ impl QueryCtorArgs { return Err(global_this.throw(format_args!("values must be an array"))); } - let pending_value: JSValue = args.next().unwrap_or(JSValue::UNDEFINED); - let js_bigint: JSValue = args.next().unwrap_or(JSValue::FALSE); - let js_simple: JSValue = args.next().unwrap_or(JSValue::FALSE); - let bigint = js_bigint.is_boolean() && js_bigint.as_boolean(); let simple = js_simple.is_boolean() && js_simple.as_boolean(); if simple { diff --git a/src/sys_jsc/build.rs b/src/sys_jsc/build.rs new file mode 100644 index 000000000000..480cba0fe036 --- /dev/null +++ b/src/sys_jsc/build.rs @@ -0,0 +1,35 @@ +// Build scripts run on the host before bun_* crates are compiled; std is the only option. +#![allow( + clippy::disallowed_methods, + clippy::disallowed_types, + clippy::disallowed_macros +)] +//! Export `BUN_CODEGEN_DIR` for `generated_bindgen.rs`, which `include!`s what `src/codegen/bindgen.ts` writes. + +use std::env; +use std::path::{Path, PathBuf}; + +fn main() { + let manifest = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let repo = manifest + .parent() + .and_then(Path::parent) + .expect("repo root from CARGO_MANIFEST_DIR") + .to_path_buf(); + + let codegen_dir = env::var("BUN_CODEGEN_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| repo.join("build/debug/codegen")); + + let generated = codegen_dir.join("generated_bindgen_bun_sys_jsc.rs"); + if !generated.exists() { + panic!( + "generated_bindgen_bun_sys_jsc.rs not found at {} — run `bun bd` first", + generated.display() + ); + } + + println!("cargo:rerun-if-changed={}", generated.display()); + println!("cargo:rustc-env=BUN_CODEGEN_DIR={}", codegen_dir.display()); + println!("cargo:rerun-if-env-changed=BUN_CODEGEN_DIR"); +} diff --git a/src/sys_jsc/error_jsc.bind.ts b/src/sys_jsc/error_jsc.bind.ts new file mode 100644 index 000000000000..19fc8b3e1784 --- /dev/null +++ b/src/sys_jsc/error_jsc.bind.ts @@ -0,0 +1,14 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::error_jsc::TestingAPIs" }); + +export const translateNtStatusToE = b.fn({ args: { status: b.f64 }, returns: b.string.utf8.optional }); +export const translateUVErrorToE = b.fn({ args: { code: b.f64 }, returns: b.string.utf8.optional }); + +export const SigactionHandlerAndFlags = b.dictionary({ handler: b.usize, flags: b.i32 }); +export const SigactionLayout = b.dictionary({ + installed: SigactionHandlerAndFlags, + readback: SigactionHandlerAndFlags, + sizeof: { type: b.usize, rustName: "size" }, +}); +export const sigactionLayout = b.fn({ returns: SigactionLayout.optional }); diff --git a/src/sys_jsc/error_jsc.rs b/src/sys_jsc/error_jsc.rs index 323dc8ee0e89..3138b3906b45 100644 --- a/src/sys_jsc/error_jsc.rs +++ b/src/sys_jsc/error_jsc.rs @@ -2,9 +2,7 @@ use bun_sys::Error; -use crate::{CallFrame, JSGlobalObject, JSPromise, JSValue, JsResult, SystemErrorJsc}; -#[cfg(windows)] -use bun_jsc::bun_string_jsc; +use crate::{JSGlobalObject, JSPromise, JSValue, JsResult, SystemErrorJsc}; pub trait ErrorJsc { fn to_js(&self, global: &JSGlobalObject) -> JsResult; @@ -36,70 +34,34 @@ impl ErrorJsc for Error { } } -// `TestingAPIs` is a module (not `struct + impl`) because `#[bun_jsc::host_fn]`'s -// Free-kind shim emits `#fn_name(__g, __f)` without a `Self::` qualifier — the -// wrapped fn must resolve unqualified at module scope (same constraint as -// `install_jsc::install_binding::js_parse_lockfile`). -pub mod TestingAPIs { - use super::*; +pub(crate) mod TestingAPIs { + use crate::generated_bindgen::error_jsc::SigactionLayout; /// Exposes NTSTATUS -> `bun.sys.E` translation so tests can feed NTSTATUS /// values that filter drivers and cloud-sync placeholders return in the /// wild (STATUS_CANNOT_DELETE etc.) and verify they map to a sensible /// errno rather than `UNKNOWN`. Windows-only; returns `undefined` elsewhere. - #[bun_jsc::host_fn] - pub fn translate_nt_status_to_e( - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - let arguments = frame.arguments(); - if arguments.is_empty() || !arguments[0].is_number() { - return Err(global.throw(format_args!( - "translateNtStatusToE: expected 1 number argument" - ))); - } - #[cfg(not(windows))] - { - return Ok(JSValue::UNDEFINED); - } - #[cfg(windows)] - { - let raw: u32 = arguments[0].to_u32(); - let status = bun_sys::windows::NTSTATUS::from_raw(raw); - let result = bun_sys::windows::translate_nt_status_to_errno(status); - return bun_string_jsc::create_utf8_for_js( - global, - <&'static str>::from(result).as_bytes(), - ); - } + #[cfg(windows)] + pub(crate) fn translate_nt_status_to_e(status: f64) -> Option<&'static str> { + let status = bun_sys::windows::NTSTATUS::from_raw(status as u32); + let result = bun_sys::windows::translate_nt_status_to_errno(status); + Some(<&'static str>::from(result)) + } + #[cfg(not(windows))] + pub(crate) fn translate_nt_status_to_e(_: f64) -> Option<&'static str> { + None } /// Exposes libuv -> `bun.sys.E` translation so tests can feed out-of-range /// negative values and verify it does not panic. Windows-only. - #[bun_jsc::host_fn] - pub fn translate_uv_error_to_e( - global: &JSGlobalObject, - frame: &CallFrame, - ) -> JsResult { - let arguments = frame.arguments(); - if arguments.is_empty() || !arguments[0].is_number() { - return Err(global.throw(format_args!( - "translateUVErrorToE: expected 1 number argument" - ))); - } - #[cfg(not(windows))] - { - return Ok(JSValue::UNDEFINED); - } - #[cfg(windows)] - { - let code: core::ffi::c_int = arguments[0].to_int32(); - let result = bun_sys::windows::translate_uv_error_to_e(code); - return bun_string_jsc::create_utf8_for_js( - global, - <&'static str>::from(result).as_bytes(), - ); - } + #[cfg(windows)] + pub(crate) fn translate_uv_error_to_e(code: f64) -> Option<&'static str> { + let result = bun_sys::windows::translate_uv_error_to_e(code as core::ffi::c_int); + Some(<&'static str>::from(result)) + } + #[cfg(not(windows))] + pub(crate) fn translate_uv_error_to_e(_: f64) -> Option<&'static str> { + None } /// Exposes the `bun.sys.Sigaction` struct layout via a SIGUSR2 install / @@ -109,15 +71,14 @@ pub mod TestingAPIs { /// `libc` crate's `sigaction`/`sigset_t` directly, which already has the /// correct per-target layout (bionic included), so this is a sanity check /// rather than a fix-carrier. - #[bun_jsc::host_fn] - pub fn sigaction_layout(global: &JSGlobalObject, _frame: &CallFrame) -> JsResult { + pub(crate) fn sigaction_layout() -> Option { #[cfg(not(unix))] { - let _ = global; - return Ok(JSValue::UNDEFINED); + None } #[cfg(unix)] { + use crate::generated_bindgen::error_jsc::SigactionHandlerAndFlags; use bun_sys::posix::{Sigaction, sigaction, sigset_t}; extern "C" fn sentry(_: core::ffi::c_int) {} unsafe extern "C" { @@ -159,25 +120,17 @@ pub mod TestingAPIs { ) }; - let installed = JSValue::create_empty_object(global, 2); - installed.put( - global, - b"handler", - JSValue::js_number(sentry as *const () as usize as f64), - ); - installed.put(global, b"flags", JSValue::js_number(act_flags as f64)); - let rb = JSValue::create_empty_object(global, 2); - rb.put(global, b"handler", JSValue::js_number(rb_handler as f64)); - rb.put(global, b"flags", JSValue::js_number(rb_flags as f64)); - let out = JSValue::create_empty_object(global, 3); - out.put(global, b"installed", installed); - out.put(global, b"readback", rb); - out.put( - global, - b"sizeof", - JSValue::js_number(core::mem::size_of::() as f64), - ); - Ok(out) + Some(SigactionLayout { + installed: SigactionHandlerAndFlags { + handler: sentry as *const () as usize, + flags: act_flags, + }, + readback: SigactionHandlerAndFlags { + handler: rb_handler, + flags: rb_flags, + }, + size: core::mem::size_of::(), + }) } } } diff --git a/src/sys_jsc/generated_bindgen.rs b/src/sys_jsc/generated_bindgen.rs new file mode 100644 index 000000000000..03694fda3962 --- /dev/null +++ b/src/sys_jsc/generated_bindgen.rs @@ -0,0 +1,6 @@ +//! What `src/codegen/bindgen.ts` generates from this crate's `.bind.ts` files. + +include!(concat!( + env!("BUN_CODEGEN_DIR"), + "/generated_bindgen_bun_sys_jsc.rs" +)); diff --git a/src/sys_jsc/lib.rs b/src/sys_jsc/lib.rs index a5e4aa9c3627..ef7ddcb0e229 100644 --- a/src/sys_jsc/lib.rs +++ b/src/sys_jsc/lib.rs @@ -10,6 +10,7 @@ pub mod error_jsc; pub mod fd_jsc; +pub(crate) mod generated_bindgen; pub mod signal_code_jsc; pub use error_jsc::ErrorJsc; @@ -17,8 +18,8 @@ pub use fd_jsc::FdJsc; // Re-export the JSC types this crate's API surface needs. pub use bun_jsc::{ - CallFrame, FromJsEnum, JSGlobalObject, JSPromise, JSString, JSValue, JsError, JsResult, - RangeErrorOptions, SystemError, VM, + FromJsEnum, JSGlobalObject, JSPromise, JSString, JSValue, JsError, JsResult, RangeErrorOptions, + SystemError, VM, }; // ────────────────────────────────────────────────────────────────────────── diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index e21f1a28e660..530eb251f67d 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -310,15 +310,24 @@ function checkForEmptyInterfaces(program: ts.Program) { */ const PUBLISHED_AS: Record = { "Archive.bind.ts": "typeof Bun", - "Blob.bind.ts": "{ Blob: Class }", + "Blob.bind.ts": "typeof Bun & { Blob: Class }", "BlockList.bind.ts": null, - "BunObject.bind.ts": "typeof Bun & { braces: typeof Bun.$.braces; shellEscape: typeof Bun.$.escape }", + "BunObject.bind.ts": + "typeof Bun & { braces: typeof Bun.$.braces; shellEscape: typeof Bun.$.escape; table: typeof Bun.inspect.table }", "BunObjectInternals.bind.ts": null, + "ByteStream.bind.ts": null, + "BytecodeOrderRecorder.bind.ts": null, + "Counters.bind.ts": null, "Crypto.bind.ts": 'typeof Bun & { Crypto: Class>; timingSafeEqual: typeof import("node:crypto").timingSafeEqual }', "CryptoHasher.bind.ts": "typeof Bun", "DevServer.bind.ts": null, + "FFIObject.bind.ts": null, + "FakeTimers.bind.ts": 'typeof import("bun:test").jest', "FetchSession.bind.ts": "typeof Bun", + "FetchTasklet.bind.ts": null, + "FileSink.bind.ts": null, + "FormData.bind.ts": null, "Glob.bind.ts": "typeof Bun", "HTMLBundle.bind.ts": null, "HashObject.bind.ts": "typeof Bun.hash & { wyhash: typeof Bun.hash }", @@ -329,66 +338,115 @@ const PUBLISHED_AS: Record = { "JSON5Object.bind.ts": "typeof Bun.JSON5", "JSONCObject.bind.ts": "typeof Bun.JSONC", "Listener.bind.ts": null, + "MarkdownObject.bind.ts": "typeof Bun.markdown", + "MarkdownObjectInternals.bind.ts": null, "NodeHTTPResponse.bind.ts": null, "NodeModuleModule.bind.ts": null, + "ObjectURLRegistry.bind.ts": null, "ParsedShellScript.bind.ts": null, "PasswordObject.bind.ts": "typeof Bun.password", "PostgresSQLConnection.bind.ts": null, "PostgresSQLQuery.bind.ts": null, "ReadableStream.bind.ts": null, + "RequestContext.bind.ts": null, "ResourceUsage.bind.ts": null, "S3Client.bind.ts": "typeof Bun", "S3Stat.bind.ts": "{ S3Stat: Class }", "SSLConfig.bind.ts": null, + "ScopeFunctions.bind.ts": null, "SecureContext.bind.ts": null, - "SemverObject.bind.ts": "typeof Bun.semver", + "SemverObject.bind.ts": "typeof Bun.semver & { intersects(left: string, right: string): boolean }", "SocketAddress.bind.ts": null, "SocketConfig.bind.ts": null, + "Stat.bind.ts": null, "TOMLObject.bind.ts": "typeof Bun.TOML", "Terminal.bind.ts": "typeof Bun", "TextDecoder.bind.ts": "{ TextDecoder: typeof TextDecoder }", "Timer.bind.ts": null, "Transpiler.bind.ts": "typeof Bun", "UnsafeObject.bind.ts": "typeof Bun.unsafe", + "UpgradedDuplex.bind.ts": null, "XMLObject.bind.ts": "typeof Bun.XML", "YAMLObject.bind.ts": "typeof Bun.YAML", "bindgen_test.bind.ts": null, + "bun_string_jsc.bind.ts": null, + "bun_test.bind.ts": null, + "cares_jsc.bind.ts": null, + "client.bind.ts": null, + "color_js.bind.ts": "typeof Bun", + "crash_handler_jsc.bind.ts": null, "cron.bind.ts": null, "csrf_jsc.bind.ts": "typeof Bun.CSRF", + "css_internals.bind.ts": null, "dns.bind.ts": "typeof Bun.dns", "dns_internals.bind.ts": null, + "error_jsc.bind.ts": null, + "event_loop.bind.ts": null, "expect.bind.ts": null, + "fetch.bind.ts": + '{ fetch: typeof fetch; preconnect: typeof fetch.preconnect; sessionFetch: Bun.FetchSession["fetch"] }', "ffi.bind.ts": null, "filesystem_router.bind.ts": "typeof Bun & { FrameworkFileSystemRouter: Class<{ match(path: string): any; toJSON(): any }> }", "fmt_jsc.bind.ts": null, "h2_frame_parser.bind.ts": null, + "headers_jsc.bind.ts": null, + "hosted_git_info_jsc.bind.ts": null, "html_rewriter.bind.ts": "{ HTMLRewriter: typeof HTMLRewriter }", + "html_rewriter_internals.bind.ts": null, + "hw_exports.bind.ts": null, "imported_class_test.bind.ts": null, + "ini_jsc.bind.ts": null, + "install_binding.bind.ts": null, + "internal_jsc.bind.ts": null, "interpreter.bind.ts": null, + "ipc.bind.ts": null, + "ipc_host.bind.ts": null, "jest.bind.ts": null, + "js.bind.ts": null, + "js_bun_spawn_bindings.bind.ts": "typeof Bun", "jsc_hooks.bind.ts": null, + "linear_fifo_testing.bind.ts": null, + "memory_pressure.bind.ts": null, + "mysql.bind.ts": null, + "napi_body.bind.ts": null, "node_assert_binding.bind.ts": null, "node_cluster_binding.bind.ts": null, + "node_crypto_binding.bind.ts": null, "node_fs_binding.bind.ts": null, "node_fs_stat_watcher.bind.ts": null, "node_fs_watcher.bind.ts": null, "node_http_binding.bind.ts": null, "node_net_binding.bind.ts": null, "node_os.bind.ts": null, + "node_quic_binding.bind.ts": null, "node_tls_binding.bind.ts": null, "node_util_binding.bind.ts": null, "node_zlib_binding.bind.ts": null, + "npm_jsc.bind.ts": null, + "pack_command.bind.ts": null, + "parse_args.bind.ts": null, + "plugins.bind.ts": null, + "postgres.bind.ts": null, + "prompt.bind.ts": "{ alert: typeof alert; confirm: typeof confirm; prompt: typeof prompt }", + "proxy_testing.bind.ts": null, "quic.bind.ts": null, + "rare_data.bind.ts": null, "resolve_message.bind.ts": "{ ResolveMessage: Class; BuildMessage: Class }", + "resolver_jsc.bind.ts": null, "response.bind.ts": '{ Request: Class>; Response: typeof Response & { prototype: Response } }', "server.bind.ts": "{ ServerWebSocket: Class> }", + "server_body.bind.ts": null, + "shell_body.bind.ts": null, "socket_body.bind.ts": null, "sourcemap.bind.ts": null, "subprocess.bind.ts": null, + "testing.bind.ts": null, "udp_socket.bind.ts": null, + "upgrade_command.bind.ts": null, "valkey.bind.ts": "typeof Bun", + "virtual_machine_exports.bind.ts": null, }; /** diff --git a/test/internal-for-testing.d.ts b/test/internal-for-testing.d.ts index 49905583bb42..52c248c621cd 100644 --- a/test/internal-for-testing.d.ts +++ b/test/internal-for-testing.d.ts @@ -2,9 +2,7 @@ // src/js/builtins.d.ts declare them, next to changes to `Error`, `Function` and other global interfaces that only // apply to builtins. declare function $cpp(filename: string, symbol: string): T; -declare function $rust(filename: string, symbol: string): T; declare function $newCppFunction any>(filename: string, symbol: string, argCount: number): T; -declare function $newRustFunction any>(filename: string, symbol: string, argCount: number): T; declare function $bindgen any>(filename: string, name: string): T; declare function $ERR_INVALID_ARG_VALUE(name: string, value: any, reason?: string): TypeError; declare function $ERR_IPC_ONE_PIPE(): Error; diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index cb543b2d9834..3b005ce14d76 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -690,6 +690,35 @@ describe("b.any.variadic", () => { }); }); +describe("functions", () => { + it("passThis and passCallee come before the arguments", () => { + const receiver = {}; + const [thisValue, callee, value] = bindgen.thisAndCallee.call(receiver, 1); + expect(thisValue).toBe(receiver); + expect(callee).toBe(bindgen.thisAndCallee); + expect(value).toBe(1); + expect(bindgen.thisAndCallee.call(5, 1)[0]).toBe(5); + expect((0, bindgen.thisAndCallee)(1)[0]).toBeUndefined(); + expect(bindgen.thisAndCallee.length).toBe(1); + }); + + it("export names the host function for Rust", () => { + expect(bindgen.doubled(21)).toBe(42); + const made = bindgen.makeDoubled(); + expect(made).not.toBe(bindgen.doubled); + expect(made.name).toBe("doubled"); + expect(made(21)).toBe(42); + expect(() => made("21")).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); + expect(() => b.fn({ export: "not a name" })).toThrow("export: not a name is not a C identifier"); + }); + + it("$bindgen gives a namespace as an object", () => { + expect(Object.keys(bindgen.testNamespace)).toEqual(["increment", "decrement"]); + expect(bindgen.testNamespace.increment(1)).toBe(2); + expect(bindgen.testNamespace.decrement(1)).toBe(0); + }); +}); + describe("enumerations", () => { it("parses and returns the canonical spelling", () => { expect(bindgen.nextColor("red")).toBe("green"); diff --git a/test/js/bun/patch/patch.test.ts b/test/js/bun/patch/patch.test.ts index cc28c4da1b3d..a09c1d12a294 100644 --- a/test/js/bun/patch/patch.test.ts +++ b/test/js/bun/patch/patch.test.ts @@ -976,7 +976,6 @@ describe("apply", () => { "-e", `import { patchInternals } from "bun:internal-for-testing"; try { patchInternals.apply("@@"); console.log("no-error"); } catch (e) { console.log("invalid-patchfile: " + e.message); } - try { patchInternals.apply(); console.log("no-error"); } catch (e) { console.log("missing-argument: " + e.message); } try { patchInternals.apply("@@", "bun-patch-test-nonexistent-dir"); console.log("no-error"); } catch (e) { console.log("bad-dir: " + e.code); } try { patchInternals.apply({ toString() { throw new Error("coerce-fail"); } }, process.cwd()); console.log("no-error"); } catch (e) { console.log("bad-patchfile-arg: " + e.message); }`, ], @@ -989,7 +988,6 @@ describe("apply", () => { expect(stderr).toBe(""); expect(stdout.trim().split("\n")).toEqual([ "invalid-patchfile: bad_header_line failed to parse patchfile", - "missing-argument: apply: expected at least 1 argument, got 0", "bad-dir: ENOENT", "bad-patchfile-arg: coerce-fail", ]); diff --git a/test/js/web/fetch/fetch-args.test.ts b/test/js/web/fetch/fetch-args.test.ts index 619908e8fffd..522fc28739e2 100644 --- a/test/js/web/fetch/fetch-args.test.ts +++ b/test/js/web/fetch/fetch-args.test.ts @@ -164,7 +164,7 @@ describe.concurrent("fetch() early rejections are reported when unhandled", () = } const cases: [name: string, code: string, expectedStderr: string][] = [ - ["no arguments", `fetch()`, "fetch() expects a string but received no arguments"], + ["no arguments", `fetch()`, "fetch() URL must not be a blank string"], ["blank url", `fetch("")`, "fetch() URL must not be a blank string"], ["invalid url", `fetch("not a url")`, "fetch() URL is invalid"], ["unsupported protocol", `fetch("gopher://example.com/")`, "protocol must be http:, https: or s3:"], diff --git a/test/js/web/fetch/fetch-session.test.ts b/test/js/web/fetch/fetch-session.test.ts index 0a6596036720..06f873f9bf78 100644 --- a/test/js/web/fetch/fetch-session.test.ts +++ b/test/js/web/fetch/fetch-session.test.ts @@ -739,7 +739,7 @@ describe("session.fetch", () => { test("rejects like fetch() does", async () => { using session = new Bun.FetchSession(); expect(await (session.fetch as any)().catch((e: Error) => e.message)).toBe( - "fetch() expects a string but received no arguments.", + "fetch() URL must not be a blank string.", ); using dead = await deadPort(); expect(await session.fetch(`http://127.0.0.1:${dead.port}/`).catch(e => e.code)).toBe("ECONNREFUSED"); From b2343d6a7d14d0e6d95f5b1ce7ec643cb35bfbcd Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Thu, 22 Oct 2026 23:56:46 +0000 Subject: [PATCH 02/22] Keep `ERR_MISSING_ARGS` for `URL.revokeObjectURL()` and test `Bun.resolveSync` with an undefined third argument --- src/runtime/webcore/ObjectURLRegistry.bind.ts | 6 ++--- src/runtime/webcore/ObjectURLRegistry.rs | 24 +++++++++++++++---- test/js/bun/resolve/resolve.test.ts | 6 +++++ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/runtime/webcore/ObjectURLRegistry.bind.ts b/src/runtime/webcore/ObjectURLRegistry.bind.ts index cb9300a49c0b..466b14d243bc 100644 --- a/src/runtime/webcore/ObjectURLRegistry.bind.ts +++ b/src/runtime/webcore/ObjectURLRegistry.bind.ts @@ -9,10 +9,8 @@ export const createObjectURL = b.fn({ returns: b.string, }); -export const revokeObjectURL = b.fn({ - args: { url: b.string.allowStringObject }, - expected: { url: "revokeObjectURL expects a string" }, -}); +/** Without an argument it throws `ERR_MISSING_ARGS`, as in Node. */ +export const revokeObjectURL = b.fn({ args: { url: b.any.variadic }, capability: "js", throws: true, length: 1 }); /** `resolveObjectURL` of `node:buffer`. */ export const resolveObjectURL = b.fn({ args: { url: b.string.loose }, returns: b.any, capability: "js" }); diff --git a/src/runtime/webcore/ObjectURLRegistry.rs b/src/runtime/webcore/ObjectURLRegistry.rs index 97aa9b2eeb78..08c2afc537fe 100644 --- a/src/runtime/webcore/ObjectURLRegistry.rs +++ b/src/runtime/webcore/ObjectURLRegistry.rs @@ -2,9 +2,9 @@ use std::sync::OnceLock; use bun_collections::HashMap; use bun_core::strings; -use bun_jsc::bindgen::JsScope; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{JSGlobalObject, JSValue, UUID}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult, UUID}; use bun_threading::Guarded; use crate::webcore::Blob; @@ -118,17 +118,31 @@ pub(crate) fn create_object_url(blob: &Blob) -> bun_core::String { bun_core::String::create_format(format_args!("blob:{}", uuid)) } -pub(crate) fn revoke_object_url(str: &bun_core::String) { +pub(crate) fn revoke_object_url(cx: &mut JsScope<'_>, url: &[JsLocal<'_>]) -> JsResult<()> { + let global = cx.global(); + let Some(url) = url.first() else { + return Err(global.throw_not_enough_arguments("revokeObjectURL", 1, 0)); + }; + let url = url.to_js(); + if !url.is_string() { + return Err( + global.throw_invalid_arguments(format_args!("revokeObjectURL expects a string")) + ); + } + // `is_string()` is `is_string_like()` and admits `StringObject`, so + // `to_bun_string` can still observe a user `toString` that throws. + let str = url.to_bun_string(global)?; if !str.starts_with_ascii(b"blob:") { - return; + return Ok(()); } let slice = str.to_utf8(); let sliced = slice.slice(); if sliced.len() < b"blob:".len() + UUID::STRING_LENGTH { - return; + return Ok(()); } ObjectURLRegistry::singleton().revoke(&sliced[b"blob:".len()..]); + Ok(()) } pub(crate) fn resolve_object_url(cx: &mut JsScope<'_>, str: &bun_core::String) -> JSValue { diff --git a/test/js/bun/resolve/resolve.test.ts b/test/js/bun/resolve/resolve.test.ts index 2172327b82c0..bee8739d8625 100644 --- a/test/js/bun/resolve/resolve.test.ts +++ b/test/js/bun/resolve/resolve.test.ts @@ -306,6 +306,12 @@ it.skipIf(isWindows)("resolves an absolute specifier that is longer than a path ); }); +it("Bun.resolveSync takes undefined for its third argument", () => { + using dir = tempDir("resolve-undefined-esm", { "index.js": "" }); + // @ts-expect-error + expect(Bun.resolveSync("./index.js", String(dir), undefined)).toBe(join(String(dir), "index.js")); +}); + it("import override to node builtin", async () => { // @ts-expect-error expect(await import("#async_hooks")).toBeDefined(); From acd9aa81c75940bed33e19ea3eb032b1a41c109c Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 00:05:29 +0000 Subject: [PATCH 03/22] Name the schema `BunTest.bind.ts` so that a release build has it, and fix the type check and clippy --- .../test_runner/{bun_test.bind.ts => BunTest.bind.ts} | 0 src/runtime/webcore/fetch/proxy_testing.rs | 4 ++-- src/tsconfig.json | 5 ++--- test/integration/bun-types/bun-types.test.ts | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) rename src/runtime/test_runner/{bun_test.bind.ts => BunTest.bind.ts} (100%) diff --git a/src/runtime/test_runner/bun_test.bind.ts b/src/runtime/test_runner/BunTest.bind.ts similarity index 100% rename from src/runtime/test_runner/bun_test.bind.ts rename to src/runtime/test_runner/BunTest.bind.ts diff --git a/src/runtime/webcore/fetch/proxy_testing.rs b/src/runtime/webcore/fetch/proxy_testing.rs index 59f0b4ba9b08..12ae14cb90a1 100644 --- a/src/runtime/webcore/fetch/proxy_testing.rs +++ b/src/runtime/webcore/fetch/proxy_testing.rs @@ -19,12 +19,12 @@ pub(crate) fn proxy_for<'a>( url: &[u8], ) -> bindgen::Result<'a, Option>> { let mut env = bun_dotenv::Loader::init(); - let mut entries = entries.iter(); + let mut entries = entries.into_iter(); while let Some(name) = entries.next() { let Some(value) = entries.next() else { return Err(heap.invalid_arguments(format_args!("a name without a value"))); }; - bun_core::handle_oom(env.map.put(name, value)); + bun_core::handle_oom(env.map.put(&name, &value)); } env.get_http_proxy_for(&URL::parse(url)) .map(|proxy| heap.create_utf8_string(proxy.href)) diff --git a/src/tsconfig.json b/src/tsconfig.json index 0fa424a7e570..f89cccb03102 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -10,14 +10,13 @@ "include": ["**/*.ts", "**/*.tsx", "../packages/bun-types/index.d.ts"], // js and runtime/bake are projects of their own, with settings that only apply there. The `bun init` and // `bun create` templates are users' projects: each has its own tsconfig and dependencies that are not installed - // here (`cli` is a link to `runtime/cli`). The libuv stub generator needs tree-sitter, which is not installed. + // here. `cli` is a link to `runtime/cli`. The libuv stub generator needs tree-sitter, which is not installed. "exclude": [ "js", "runtime/bake", "runtime/cli/init", "runtime/cli/create", - "cli/init", - "cli/create", + "cli", "jsc/bindings/libuv" ] } diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 530eb251f67d..dbe0201bd2d4 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -315,6 +315,7 @@ const PUBLISHED_AS: Record = { "BunObject.bind.ts": "typeof Bun & { braces: typeof Bun.$.braces; shellEscape: typeof Bun.$.escape; table: typeof Bun.inspect.table }", "BunObjectInternals.bind.ts": null, + "BunTest.bind.ts": null, "ByteStream.bind.ts": null, "BytecodeOrderRecorder.bind.ts": null, "Counters.bind.ts": null, @@ -370,7 +371,6 @@ const PUBLISHED_AS: Record = { "YAMLObject.bind.ts": "typeof Bun.YAML", "bindgen_test.bind.ts": null, "bun_string_jsc.bind.ts": null, - "bun_test.bind.ts": null, "cares_jsc.bind.ts": null, "client.bind.ts": null, "color_js.bind.ts": "typeof Bun", From d343a1c8218a03ca384544521428da5b81b664b9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:06:38 +0000 Subject: [PATCH 04/22] [autofix.ci] apply automated fixes --- src/tsconfig.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/tsconfig.json b/src/tsconfig.json index f89cccb03102..d31cef902920 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -11,12 +11,5 @@ // js and runtime/bake are projects of their own, with settings that only apply there. The `bun init` and // `bun create` templates are users' projects: each has its own tsconfig and dependencies that are not installed // here. `cli` is a link to `runtime/cli`. The libuv stub generator needs tree-sitter, which is not installed. - "exclude": [ - "js", - "runtime/bake", - "runtime/cli/init", - "runtime/cli/create", - "cli", - "jsc/bindings/libuv" - ] + "exclude": ["js", "runtime/bake", "runtime/cli/init", "runtime/cli/create", "cli", "jsc/bindings/libuv"] } From dac641dc6655fbad08b400e8b8b0732a11e2ba35 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 00:14:21 +0000 Subject: [PATCH 05/22] bindgen: add `b.value`, make functions through one helper, and name schemas and Rust modules after what they hold --- docs/project/bindgen.mdx | 7 +- src/bun_core/lib.rs | 2 +- src/bun_core/string/escapeRegExp.rs | 3 +- src/codegen/bindgen-emit.ts | 27 +- src/codegen/bindgen-lib-internal.ts | 16 +- src/codegen/bindgen-lib.ts | 6 +- src/codegen/bindgen.ts | 13 + src/codegen/generate-host-exports.ts | 12 +- src/codegen/generate-js2native.ts | 4 - src/css_jsc/css_internals.rs | 26 +- src/ini/lib.rs | 2 - .../lockfile_json_stringify_for_debugging.rs | 2 +- src/install_jsc/ini_jsc.bind.ts | 2 +- src/install_jsc/ini_jsc.rs | 378 ++- src/install_jsc/install_binding.bind.ts | 2 +- src/install_jsc/install_binding.rs | 142 +- src/js/internal-for-testing.ts | 19 +- src/js/internal/bake.ts | 10 +- src/js/internal/fs/binding.ts | 2 +- src/js/internal/quic/binding.ts | 2 +- src/js/internal/serve_cli.ts | 2 +- src/js/node/_http_common.ts | 2 +- src/js/node/dgram.ts | 2 +- src/js/node/os.ts | 2 +- src/jsc/ConsoleObject.rs | 3 +- src/jsc/JSGlobalObject.rs | 5 +- src/jsc/MarkedArgumentBuffer.rs | 2 +- src/jsc/bindgen_test.bind.ts | 1 + src/jsc/bindgen_test.rs | 4 + src/jsc/bindings/Bindgen/ExternTraits.h | 4 + src/jsc/bindings/Bindgen/ToJS.cpp | 7 + src/jsc/host_fn.rs | 18 +- src/jsc/job.rs | 2 +- src/jsc/resolver_jsc.bind.ts | 10 +- ...{testing.bind.ts => patch_testing.bind.ts} | 2 +- src/patch_jsc/testing.rs | 222 +- src/runtime/api/BunObject.bind.ts | 2 +- src/runtime/api/BunObject.rs | 4 +- src/runtime/api/crash_handler_jsc.bind.ts | 2 +- src/runtime/api/crash_handler_jsc.rs | 354 ++- .../{plugins.bind.ts => bake_plugins.bind.ts} | 0 .../{js.bind.ts => FrameworkRouter.bind.ts} | 0 src/runtime/hw_exports.bind.ts | 2 +- src/runtime/node/node_crypto_binding.bind.ts | 26 +- src/runtime/node/node_crypto_binding.rs | 2381 ++++++++--------- src/runtime/node/node_fs_binding.bind.ts | 4 +- src/runtime/node/node_fs_binding.rs | 2 +- src/runtime/node/node_http_binding.bind.ts | 2 +- src/runtime/node/node_os.bind.ts | 2 +- src/runtime/node/node_quic_binding.bind.ts | 4 +- src/runtime/node/node_quic_binding.rs | 2 +- src/runtime/node/node_util_binding.bind.ts | 2 +- src/runtime/socket/UpgradedDuplex.rs | 2 +- src/runtime/test_runner/BunTest.bind.ts | 1 + src/runtime/test_runner/bun_test.rs | 8 +- src/runtime/webcore/prompt.bind.ts | 2 +- src/runtime/webcore/prompt.rs | 360 ++- ...{client.bind.ts => S3UploadStream.bind.ts} | 0 src/runtime/webcore/s3/client.rs | 2 +- src/semver_jsc/SemverObject.bind.ts | 6 - src/semver_jsc/SemverObjectInternals.bind.ts | 9 + ..._jsc.bind.ts => InternalSourceMap.bind.ts} | 0 src/sourcemap_jsc/internal_jsc.rs | 2 +- src/sys/Error.rs | 2 +- src/sys_jsc/error_jsc.bind.ts | 2 +- src/sys_jsc/error_jsc.rs | 2 +- src/sys_jsc/lib.rs | 1 - test/integration/bun-types/bun-types.test.ts | 13 +- test/internal/bindgen.test.ts | 4 + .../source-lints/host-export-callers.test.ts | 2 +- test/leaksan.supp | 2 +- 71 files changed, 2055 insertions(+), 2119 deletions(-) rename src/patch_jsc/{testing.bind.ts => patch_testing.bind.ts} (88%) rename src/runtime/bake/config/{plugins.bind.ts => bake_plugins.bind.ts} (100%) rename src/runtime/bake/router/{js.bind.ts => FrameworkRouter.bind.ts} (100%) rename src/runtime/webcore/s3/{client.bind.ts => S3UploadStream.bind.ts} (100%) create mode 100644 src/semver_jsc/SemverObjectInternals.bind.ts rename src/sourcemap_jsc/{internal_jsc.bind.ts => InternalSourceMap.bind.ts} (100%) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 5bab30a2a848..cbbb7726cb8f 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -81,8 +81,11 @@ To reach the function: linkage under that name. The generated header declares it, and in Rust it is a `JSHostFn`, `crate::generated_bindgen::cron::Bun__CronJob__onPromiseResolve`, for `JSFunction::create` and `JSValue::then`. -`passThis: true` adds `this_value: JsLocal<'a>` before the arguments, and `passCallee: true` adds `callee: JsLocal<'a>`, the -function that was called, after it. Neither goes with overloads. +`passThis: true` adds `this_value: JsLocal<'a>` after the capability, and `passCallee: true` adds `callee: JsLocal<'a>`, the +function that was called, after it. Neither they nor `export` go with overloads. + +`b.value({ returns: b.i32 })` is for what `src/js` reads once: `$bindgen` calls the Rust function and gives what it returns, +and no JS function is made. The caller's source location is `global.get_caller_src_loc()`: inside a host function `vm.topCallFrame` is its own frame. diff --git a/src/bun_core/lib.rs b/src/bun_core/lib.rs index e7ce6cf189c6..cb9296386a09 100644 --- a/src/bun_core/lib.rs +++ b/src/bun_core/lib.rs @@ -2760,7 +2760,7 @@ pub mod ffi { /// /// Replaces the `// SAFETY: H is a ZST → mem::zeroed()` comment repeated /// at every callback trampoline that smuggles a generic `H: Fn*` through C - /// (`uws_sys::thunk`, `sql_jsc::IntoJSHostFn`, `server_body::route_thunk`). + /// (`uws_sys::thunk`, `server_body::route_thunk`). #[inline(always)] pub fn conjure_zst() -> H { const { diff --git a/src/bun_core/string/escapeRegExp.rs b/src/bun_core/string/escapeRegExp.rs index 43d618dc7acd..8a2dc9c97c39 100644 --- a/src/bun_core/string/escapeRegExp.rs +++ b/src/bun_core/string/escapeRegExp.rs @@ -54,5 +54,4 @@ pub fn escape_reg_exp_for_package_name_matching( writer.write_all(remain) } -// The JS-facing wrappers (`jsEscapeRegExp` / `jsEscapeRegExpForPackageNameMatching`) -// live in the `*_jsc` crate as extension-trait methods. +// The JS-facing wrappers are in `src/jsc/bun_string_jsc.rs`. diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 45a8e4841a0c..5affce21ca37 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -393,7 +393,9 @@ export function cppHeader(file: SchemaFile): string { ]; const exported = file.functions.filter(fn => fn.exportName); const declarations = [ - ...file.functions.filter(fn => !fn.exportName).map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});`), + ...file.functions + .filter(fn => !fn.exportName && !fn.isValue) + .map(fn => `JSC_DECLARE_HOST_FUNCTION(${hostFunctionName(fn)});`), ...propertyCallbacks.map(signature => `// A PropertyCallback of a static property table.\n${signature};`), ]; return `${banner(file.origin)}#pragma once @@ -1157,13 +1159,13 @@ const offsetOfPayload = (type: ClassType) => offsetOfInternalField(type.internal /** A larger cell would not be in a `MarkedBlock`. C++ checks it against `MarkedSpace::largeCutoff`. */ const maxCellSize = 8000; -/** Defines the host function `name`. `takeThis` declares `thisObject`, which is then the thunks' first argument. */ /** `passThis` and `passCallee`, which come before the arguments. */ const cppLeadingOfFunction = (fn: FunctionDefinition) => [ ...(fn.passThis ? ["JSC::JSValue::encode(callFrame->thisValue())"] : []), ...(fn.passCallee ? ["JSC::JSValue::encode(callFrame->jsCallee())"] : []), ]; +/** Defines the host function `name`. `takeThis` declares `thisObject`, which is then the thunks' first argument. */ function cppHostFunction( name: string, fn: FunctionDefinition, @@ -1211,16 +1213,26 @@ ${takeThis}${indent(cppDispatch(fn.dispatch), 4)} } function cppSourceForFunction(fn: FunctionDefinition): string { + if (fn.isValue) { + const [signature] = fn.signatures; + return ` +${cppExtern(thunkSymbol(fn, signature), signature)} +extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${fn.origin!.stem}_${fn.name}(Zig::GlobalObject* globalObject) +{ +${cppPrologue} +${cppCallBody(signature, thunkSymbol(fn, signature))} +} +`; + } const name = hostFunctionName(fn); const leading = cppLeadingOfFunction(fn).map(() => "JSC::EncodedJSValue"); - assert(fn.signatures.length === 1 || (leading.length === 0 && !fn.exportName), `${fn.name}: not with overloads`); const hostFunction = cppHostFunction(name, fn, signature => thunkSymbol(fn, signature)); return ` ${fn.signatures.map(signature => cppExtern(thunkSymbol(fn, signature), signature, leading)).join("\n")} ${fn.exportName ? hostFunction : `namespace Bun::Bindgen::Generated::${fn.origin!.namespace} {\n${hostFunction}\n}`} extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${fn.origin!.stem}_${fn.name}(Zig::GlobalObject* globalObject) { - return JSC::JSValue::encode(JSC::JSFunction::create(JSC::getVM(globalObject), globalObject, ${fn.length}, ${asciiLiteral(fn.name)}, ${fn.exportName ?? `Bun::Bindgen::Generated::${fn.origin!.namespace}::${name}`}, JSC::ImplementationVisibility::Public)); + return Bun::Bindgen::createFunction(globalObject, ${fn.length}, ${asciiLiteral(fn.name)}, ${fn.exportName ?? `Bun::Bindgen::Generated::${fn.origin!.namespace}::${name}`}); } `; } @@ -1865,7 +1877,7 @@ bun_jsc::jsc_abi_extern! { file.moduleDefinition, `${scope.stem}.bind.ts: export default b.module({ rustPath }) or give ${fn.name} a rustPath`, ); - path = `${file.moduleDefinition.rustPath}::${rustIdent(snakeCase(fn.name))}`; + path = `${file.moduleDefinition.rustPath}::${rustIdent(snakeCase(fn.name.replace(/^_+/, "")))}`; } const call = `${path}(${[...capabilityArgument[signature.capability], ...leading, ...rustArguments(signature)].join(", ")})`; return rustThunk( @@ -1989,7 +2001,10 @@ ${[ }), ); const exported = (file: SchemaFile) => [ - ...file.functions.map(fn => ` ${JSON.stringify(fn.name)}: ${overloadsType(fn)};`), + ...file.functions.map( + fn => + ` ${JSON.stringify(fn.name)}: ${fn.isValue ? resultType(fn.signatures[0].returns) : overloadsType(fn)};`, + ), ...file.types .filter(type => type instanceof ClassType && type.construct) .map(type => ` ${JSON.stringify(type.name)}: ${type.name}Constructor;`), diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 358713b9d33e..d69d6d90f921 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1778,7 +1778,7 @@ export interface SignatureOptions { expected?: Record; } -/** What `b.fn` takes, and each function of a `b.namespace`. */ +/** What `b.fn` takes. */ export interface FunctionOptions extends SignatureOptions { /** Also pass `this`, `this_value: JsLocal<'a>`. */ passThis?: boolean; @@ -1788,6 +1788,9 @@ export interface FunctionOptions extends SignatureOptions { export?: string; } +/** What `b.value` takes. */ +export type ValueOptions = Pick & { returns: Type }; + export class Signature { readonly args: readonly { name: string; type: Type; expected?: readonly string[] | string }[]; readonly returns: Type; @@ -1880,7 +1883,12 @@ export class FunctionDefinition { readonly exportName: string | undefined; name = ""; origin: Origin | null = null; - constructor(signatures: readonly (SignatureOptions | Signature)[], options: FunctionOptions = {}) { + /** `isValue`: `b.value`. There is no JS function. */ + constructor( + signatures: readonly (SignatureOptions | Signature)[], + options: FunctionOptions = {}, + readonly isValue = false, + ) { assert(signatures.length > 0, "b.overloads needs at least one signature"); this.passThis = options.passThis ?? false; this.passCallee = options.passCallee ?? false; @@ -2395,11 +2403,11 @@ export class NamespaceDefinition { readonly functions: readonly FunctionDefinition[]; readonly call: FunctionDefinition | undefined; name = ""; - constructor(functions: Record, options: NamespaceOptions = {}) { + constructor(functions: Record, options: NamespaceOptions = {}) { this.functions = Object.entries(functions).map(([name, options]) => { assert(/^[A-Za-z_$][\w$]*$/.test(name), `${name}: a function is named by an identifier`); assert(!(options instanceof FunctionDefinition), `${name}: write what b.fn takes, without b.fn`); - const fn = new FunctionDefinition([options], options); + const fn = new FunctionDefinition([options]); fn.name = name; return fn; }); diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index f7d21063ddfb..4223f46f4748 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -16,6 +16,7 @@ import { EnumerationType, FunctionDefinition, type FunctionOptions, + type ValueOptions, integers, type MemberOptions, ModuleDefinition, @@ -94,11 +95,14 @@ export { selfType as self } from "./bindgen-lib-internal.ts"; export const fn = (options: FunctionOptions) => new FunctionDefinition([options], options); +/** What a Rust function returns. `$bindgen` calls it. */ +export const value = (options: ValueOptions) => new FunctionDefinition([options], {}, true); + /** One JS function, several signatures, told apart by the kinds of their arguments. */ export const overloads = (...signatures: SignatureOptions[]) => new FunctionDefinition(signatures); /** `export const semver = b.namespace({ satisfies: { args, returns }, .. })`: an object that holds those functions, or with `{ call: "satisfies" }` a function that does what that one does. */ -export const namespace = (functions: Record, options?: NamespaceOptions) => +export const namespace = (functions: Record, options?: NamespaceOptions) => new NamespaceDefinition(functions, options); /** `export default b.module({ rustPath: "crate::node::os" })`: where the file's Rust functions are. */ diff --git a/src/codegen/bindgen.ts b/src/codegen/bindgen.ts index b0b7bab7740f..d729a98908ca 100644 --- a/src/codegen/bindgen.ts +++ b/src/codegen/bindgen.ts @@ -63,6 +63,11 @@ async function load(source: string): Promise { throw new TypeError(`${stem}.bind.ts: export ${name} is not a type, a function, a namespace or b.module()`); } } + const names = new Set(); + for (const { name } of [...result.functions, ...result.namespaces]) { + assert(!names.has(name), `${stem}.bind.ts: two of its functions and namespaces are named ${name}`); + names.add(name); + } return result; } @@ -97,6 +102,14 @@ for (const file of files) { assert(!owners.has(name), `${owners.get(name)}.bind.ts and ${file.origin.stem}.bind.ts both export a type ${name}`); owners.set(name, file.origin.stem); } + for (const { exportName } of file.functions) { + if (!exportName) continue; + assert( + !owners.has(exportName), + `export: ${exportName} is in ${owners.get(exportName)}.bind.ts and ${file.origin.stem}.bind.ts`, + ); + owners.set(exportName, file.origin.stem); + } } const tables = analyze(files); diff --git a/src/codegen/generate-host-exports.ts b/src/codegen/generate-host-exports.ts index d1c6bde4158e..325df40f850f 100644 --- a/src/codegen/generate-host-exports.ts +++ b/src/codegen/generate-host-exports.ts @@ -360,19 +360,11 @@ const externTotal = Object.values(externBlocks).reduce((a, b) => a + b, 0); /// elsewhere) — same expansion as `#[bun_jsc::host_call]` / /// `bun_jsc::jsc_host_abi!`. Rust forbids macros in ABI position, so we emit /// both arms verbatim; the inactive one is compiled out. -function emitNoMangle( - abi: Export["abi"], - symbol: string, - sig: string, - ret: string, - body: string, - unsafeFn = false, -): string { - const qual = unsafeFn ? "unsafe " : ""; +function emitNoMangle(abi: Export["abi"], symbol: string, sig: string, ret: string, body: string): string { const item = (abiStr: string, cfg: string) => `${cfg}#[allow(dead_code, unreachable_pub, unused)] #[unsafe(no_mangle)] -pub ${qual}extern "${abiStr}" fn ${symbol}(${sig}) -> ${ret} { +pub extern "${abiStr}" fn ${symbol}(${sig}) -> ${ret} { ${body} }`; if (abi === "jsc") { diff --git a/src/codegen/generate-js2native.ts b/src/codegen/generate-js2native.ts index d291a33493b2..c870d1390bc8 100644 --- a/src/codegen/generate-js2native.ts +++ b/src/codegen/generate-js2native.ts @@ -16,13 +16,11 @@ interface NativeCall { } interface WrapperCall { - type: NativeCallType; wrap_kind: "new-function"; symbol_target: string; symbol_generated: string; display_name: string; call_length: number; - filename: string; } type NativeCallType = "cpp" | "bind"; @@ -89,13 +87,11 @@ export function registerNativeCall( }); if (create_fn_len != null) { wrapperCalls.push({ - type: call_type, wrap_kind: "new-function", symbol_target: symbol, symbol_generated: "js2native_wrap_" + symbol.replace(/[^A-Za-z]/g, "_"), display_name: callBaseName(symbol), call_length: create_fn_len, - filename: resolved_filename, }); } return id; diff --git a/src/css_jsc/css_internals.rs b/src/css_jsc/css_internals.rs index 13c4cc82498e..52fb0d021446 100644 --- a/src/css_jsc/css_internals.rs +++ b/src/css_jsc/css_internals.rs @@ -28,13 +28,12 @@ pub(crate) enum TestCategory { pub(crate) fn minify_error_test_with_options<'a>( cx: &mut JsScope<'a>, source: &BunString, - expected: &BunString, + _: &BunString, options: JsLocal<'a>, ) -> JsResult { testing_impl( cx.global(), source, - expected, options.to_js(), TestKind::Minify, TestCategory::ParserOptions, @@ -44,13 +43,12 @@ pub(crate) fn minify_error_test_with_options<'a>( pub(crate) fn minify_test_with_options<'a>( cx: &mut JsScope<'a>, source: &BunString, - expected: &BunString, + _: &BunString, options: JsLocal<'a>, ) -> JsResult { testing_impl( cx.global(), source, - expected, options.to_js(), TestKind::Minify, TestCategory::ParserOptions, @@ -60,13 +58,12 @@ pub(crate) fn minify_test_with_options<'a>( pub(crate) fn prefix_test_with_options<'a>( cx: &mut JsScope<'a>, source: &BunString, - expected: &BunString, + _: &BunString, options: JsLocal<'a>, ) -> JsResult { testing_impl( cx.global(), source, - expected, options.to_js(), TestKind::Prefix, TestCategory::ParserOptions, @@ -76,13 +73,12 @@ pub(crate) fn prefix_test_with_options<'a>( pub(crate) fn test_with_options<'a>( cx: &mut JsScope<'a>, source: &BunString, - expected: &BunString, + _: &BunString, options: JsLocal<'a>, ) -> JsResult { testing_impl( cx.global(), source, - expected, options.to_js(), TestKind::Normal, TestCategory::ParserOptions, @@ -92,13 +88,12 @@ pub(crate) fn test_with_options<'a>( pub(crate) fn minify_test<'a>( cx: &mut JsScope<'a>, source: &BunString, - expected: &BunString, + _: &BunString, options: JsLocal<'a>, ) -> JsResult { testing_impl( cx.global(), source, - expected, options.to_js(), TestKind::Minify, TestCategory::Normal, @@ -108,13 +103,12 @@ pub(crate) fn minify_test<'a>( pub(crate) fn prefix_test<'a>( cx: &mut JsScope<'a>, source: &BunString, - expected: &BunString, + _: &BunString, options: JsLocal<'a>, ) -> JsResult { testing_impl( cx.global(), source, - expected, options.to_js(), TestKind::Prefix, TestCategory::Normal, @@ -124,13 +118,12 @@ pub(crate) fn prefix_test<'a>( pub(crate) fn test<'a>( cx: &mut JsScope<'a>, source: &BunString, - expected: &BunString, + _: &BunString, options: JsLocal<'a>, ) -> JsResult { testing_impl( cx.global(), source, - expected, options.to_js(), TestKind::Normal, TestCategory::Normal, @@ -140,7 +133,6 @@ pub(crate) fn test<'a>( fn testing_impl( global: &JSGlobalObject, source: &BunString, - expected: &BunString, options: JSValue, test_kind: TestKind, test_category: TestCategory, @@ -162,7 +154,6 @@ fn testing_impl( let alloc: &'static Arena = unsafe { bun_ptr::detach_lifetime_ref(&arena) }; let source = source.to_utf8(); - let _expected = expected.to_utf8(); let mut log = Log::init(); @@ -308,7 +299,7 @@ fn targets_from_js(global: &JSGlobalObject, jsobj: JSValue) -> JsResult( cx: &mut JsScope<'a>, source: &BunString, - expected: &BunString, + _: &BunString, minify: JsLocal<'a>, targets: JsLocal<'a>, ) -> JsResult { @@ -330,7 +321,6 @@ pub(crate) fn attr_test<'a>( let global = cx.global(); let source = source.to_utf8(); - let _expected = expected.to_utf8(); let minify_arg = minify.to_js(); let minify = minify_arg.is_boolean() && minify_arg.to_boolean(); diff --git a/src/ini/lib.rs b/src/ini/lib.rs index e8a9132730b3..74b64b7bec4f 100644 --- a/src/ini/lib.rs +++ b/src/ini/lib.rs @@ -948,8 +948,6 @@ mod draft { } } - // `IniTestingAPIs` — *_jsc alias deleted (see PORTING.md "Idiom map"). - // ────────────────────────────────────────────────────────────────────────── // ToStringFormatter // ────────────────────────────────────────────────────────────────────────── diff --git a/src/install/lockfile/lockfile_json_stringify_for_debugging.rs b/src/install/lockfile/lockfile_json_stringify_for_debugging.rs index 64587c660a39..7b134b5700be 100644 --- a/src/install/lockfile/lockfile_json_stringify_for_debugging.rs +++ b/src/install/lockfile/lockfile_json_stringify_for_debugging.rs @@ -639,7 +639,7 @@ pub struct WriteStreamOptions { } /// JSON write stream over an in-memory `Vec`, sufficient for -/// `bun_install_js_bindings::parse_lockfile`. +/// `bun_install_jsc::install_binding::parse_lockfile`. pub struct WriteStream { pub(crate) out: Vec, opts: WriteStreamOptions, diff --git a/src/install_jsc/ini_jsc.bind.ts b/src/install_jsc/ini_jsc.bind.ts index 3306e7d19060..8816b99f13a2 100644 --- a/src/install_jsc/ini_jsc.bind.ts +++ b/src/install_jsc/ini_jsc.bind.ts @@ -1,6 +1,6 @@ import * as b from "../codegen/bindgen-lib.ts"; -export default b.module({ rustPath: "crate::ini_jsc::IniTestingAPIs" }); +export default b.module({ rustPath: "crate::ini_jsc" }); export const parse = b.fn({ args: { source: b.string.loose.utf8 }, diff --git a/src/install_jsc/ini_jsc.rs b/src/install_jsc/ini_jsc.rs index 9bcae7a37456..20a18d1e04c8 100644 --- a/src/install_jsc/ini_jsc.rs +++ b/src/install_jsc/ini_jsc.rs @@ -4,207 +4,203 @@ use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; use bun_jsc::{JSGlobalObject, JSValue, JsResult, LogJsc as _, StringJsc}; -pub(crate) struct IniTestingAPIs; - -impl IniTestingAPIs { - pub(crate) fn load_npmrc<'a>( - cx: &mut JsScope<'a>, - contents: &[u8], - env: JsLocal<'a>, - ) -> JsResult { - use bun_ast::{Log, Source}; - use bun_core::String as BunString; - use bun_dotenv as dotenv; - use bun_ini::load_npmrc; - use bun_install::npm::Registry; - use bun_options_types::schema::api::{BunInstall, NpmRegistry}; - - let global = cx.global(); - let source = Source::init_path_string(b"", contents); - - let mut log = Log::init(); - - let envjs = env.to_js(); - // The loader is either VM-owned or built locally; keep local storage in - // an `Option` so both arms yield the same `&Loader` type. - let mut env_storage: Option = None; - let env: &dotenv::Loader = if envjs.is_empty_or_undefined_or_null() { - global.bun_vm().as_mut().transpiler.env() - } else { - let mut envmap = dotenv::map::HashTable::new(); - let Some(envobj) = envjs.get_object() else { - return Err(global.throw_type_error(format_args!("env must be an object"))); - }; - let object_iter = bun_jsc::JSPropertyIterator::init( - global, - envobj, - bun_jsc::JSPropertyIteratorOptions::new( - /* skip_empty_name */ false, /* include_value */ true, - ), - )?; - - envmap.ensure_total_capacity(object_iter.len)?; - - while let Some((key, value)) = object_iter.next()? { - let keyslice = key.to_owned_slice(); - if value.is_undefined() { - continue; - } - - let slice = value.to_bun_string(global)?.to_owned_slice(); - - envmap.put( - &keyslice, - dotenv::map::Entry { - value: slice.into_boxed_slice(), - }, - )?; +pub(crate) fn load_npmrc<'a>( + cx: &mut JsScope<'a>, + contents: &[u8], + env: JsLocal<'a>, +) -> JsResult { + use bun_ast::{Log, Source}; + use bun_core::String as BunString; + use bun_dotenv as dotenv; + use bun_ini::load_npmrc; + use bun_install::npm::Registry; + use bun_options_types::schema::api::{BunInstall, NpmRegistry}; + + let global = cx.global(); + let source = Source::init_path_string(b"", contents); + + let mut log = Log::init(); + + let envjs = env.to_js(); + // The loader is either VM-owned or built locally; keep local storage in + // an `Option` so both arms yield the same `&Loader` type. + let mut env_storage: Option = None; + let env: &dotenv::Loader = if envjs.is_empty_or_undefined_or_null() { + global.bun_vm().as_mut().transpiler.env() + } else { + let mut envmap = dotenv::map::HashTable::new(); + let Some(envobj) = envjs.get_object() else { + return Err(global.throw_type_error(format_args!("env must be an object"))); + }; + let object_iter = bun_jsc::JSPropertyIterator::init( + global, + envobj, + bun_jsc::JSPropertyIteratorOptions::new( + /* skip_empty_name */ false, /* include_value */ true, + ), + )?; + + envmap.ensure_total_capacity(object_iter.len)?; + + while let Some((key, value)) = object_iter.next()? { + let keyslice = key.to_owned_slice(); + if value.is_undefined() { + continue; } - env_storage.insert(dotenv::Loader::init_with_map(dotenv::Map { map: envmap })) - }; + let slice = value.to_bun_string(global)?.to_owned_slice(); - let mut install = Box::new(BunInstall::default()); - if load_npmrc(&mut install, env, &mut log, &source).is_err() { - return log.to_js(global, format_args!("error")); + envmap.put( + &keyslice, + dotenv::map::Entry { + value: slice.into_boxed_slice(), + }, + )?; } - // The package manager applies `url_auth` in `Options::load`; the same walk reports what the default registry would get. - let default_url: Box<[u8]> = match install.default_registry.as_ref() { - Some(registry) => registry.url.clone(), - None => Registry::DEFAULT_URL.as_bytes().into(), - }; - let found = bun_ini::RegistryKey::from_url(&default_url) - .walk() - .find_map(|key| install.url_auth.iter().find(|entry| *entry.key == *key)) - .map(|entry| entry.credentials.clone()); - if let Some(found) = found { - let registry = install.default_registry.get_or_insert_with(|| NpmRegistry { - url: default_url, - ..Default::default() - }); - if !registry.has_credentials() || registry.declared_in_npmrc { - registry.token = found.token; - registry.auth = found.auth; - registry.username = found.username; - registry.password = found.password; - } + + env_storage.insert(dotenv::Loader::init_with_map(dotenv::Map { map: envmap })) + }; + + let mut install = Box::new(BunInstall::default()); + if load_npmrc(&mut install, env, &mut log, &source).is_err() { + return log.to_js(global, format_args!("error")); + } + // The package manager applies `url_auth` in `Options::load`; the same walk reports what the default registry would get. + let default_url: Box<[u8]> = match install.default_registry.as_ref() { + Some(registry) => registry.url.clone(), + None => Registry::DEFAULT_URL.as_bytes().into(), + }; + let found = bun_ini::RegistryKey::from_url(&default_url) + .walk() + .find_map(|key| install.url_auth.iter().find(|entry| *entry.key == *key)) + .map(|entry| entry.credentials.clone()); + if let Some(found) = found { + let registry = install.default_registry.get_or_insert_with(|| NpmRegistry { + url: default_url, + ..Default::default() + }); + if !registry.has_credentials() || registry.declared_in_npmrc { + registry.token = found.token; + registry.auth = found.auth; + registry.username = found.username; + registry.password = found.password; } + } - let ( - default_registry_url, - default_registry_token, - default_registry_username, - default_registry_password, - default_registry_auth, - ) = 'brk: { - let Some(default_registry) = install.default_registry.as_ref() else { - break 'brk ( - BunString::static_(Registry::DEFAULT_URL), - BunString::EMPTY, - BunString::EMPTY, - BunString::EMPTY, - BunString::EMPTY, - ); - }; - - ( - BunString::from_bytes(&default_registry.url), - BunString::from_bytes(&default_registry.token), - BunString::from_bytes(&default_registry.username), - BunString::from_bytes(&default_registry.password), - BunString::from_bytes(&default_registry.auth), - ) + let ( + default_registry_url, + default_registry_token, + default_registry_username, + default_registry_password, + default_registry_auth, + ) = 'brk: { + let Some(default_registry) = install.default_registry.as_ref() else { + break 'brk ( + BunString::static_(Registry::DEFAULT_URL), + BunString::EMPTY, + BunString::EMPTY, + BunString::EMPTY, + BunString::EMPTY, + ); }; - // Rust has no field reflection; mirror struct-literal object creation with - // a local `PojoFields` impl (the bun_jsc-convention until `#[derive(PojoFields)]` - // lands) so each `bun.String → JSValue` encoding interleaves with `put()` and - // stays on the stack for JSC's conservative scan. - struct Pojo { - default_registry_url: BunString, - default_registry_token: BunString, - default_registry_username: BunString, - default_registry_password: BunString, - default_registry_auth: BunString, - ignore_scripts: Option, - link_workspace_packages: Option, - save_exact: Option, - hoist: Option, - } - fn optional_bool(b: Option) -> JSValue { - b.map_or(JSValue::UNDEFINED, JSValue::js_boolean) - } - impl bun_jsc::js_object::PojoFields for Pojo { - const FIELD_COUNT: usize = 9; - fn put_fields( - &self, - global: &JSGlobalObject, - mut put: impl FnMut(&'static [u8], JSValue) -> JsResult<()>, - ) -> JsResult<()> { - put( - b"default_registry_url", - self.default_registry_url.to_js(global)?, - )?; - put( - b"default_registry_token", - self.default_registry_token.to_js(global)?, - )?; - put( - b"default_registry_username", - self.default_registry_username.to_js(global)?, - )?; - put( - b"default_registry_password", - self.default_registry_password.to_js(global)?, - )?; - put( - b"default_registry_auth", - self.default_registry_auth.to_js(global)?, - )?; - put(b"ignore_scripts", optional_bool(self.ignore_scripts))?; - put( - b"link_workspace_packages", - optional_bool(self.link_workspace_packages), - )?; - put(b"save_exact", optional_bool(self.save_exact))?; - put(b"hoist", optional_bool(self.hoist))?; - Ok(()) - } + + ( + BunString::from_bytes(&default_registry.url), + BunString::from_bytes(&default_registry.token), + BunString::from_bytes(&default_registry.username), + BunString::from_bytes(&default_registry.password), + BunString::from_bytes(&default_registry.auth), + ) + }; + // Rust has no field reflection; mirror struct-literal object creation with + // a local `PojoFields` impl (the bun_jsc-convention until `#[derive(PojoFields)]` + // lands) so each `bun.String → JSValue` encoding interleaves with `put()` and + // stays on the stack for JSC's conservative scan. + struct Pojo { + default_registry_url: BunString, + default_registry_token: BunString, + default_registry_username: BunString, + default_registry_password: BunString, + default_registry_auth: BunString, + ignore_scripts: Option, + link_workspace_packages: Option, + save_exact: Option, + hoist: Option, + } + fn optional_bool(b: Option) -> JSValue { + b.map_or(JSValue::UNDEFINED, JSValue::js_boolean) + } + impl bun_jsc::js_object::PojoFields for Pojo { + const FIELD_COUNT: usize = 9; + fn put_fields( + &self, + global: &JSGlobalObject, + mut put: impl FnMut(&'static [u8], JSValue) -> JsResult<()>, + ) -> JsResult<()> { + put( + b"default_registry_url", + self.default_registry_url.to_js(global)?, + )?; + put( + b"default_registry_token", + self.default_registry_token.to_js(global)?, + )?; + put( + b"default_registry_username", + self.default_registry_username.to_js(global)?, + )?; + put( + b"default_registry_password", + self.default_registry_password.to_js(global)?, + )?; + put( + b"default_registry_auth", + self.default_registry_auth.to_js(global)?, + )?; + put(b"ignore_scripts", optional_bool(self.ignore_scripts))?; + put( + b"link_workspace_packages", + optional_bool(self.link_workspace_packages), + )?; + put(b"save_exact", optional_bool(self.save_exact))?; + put(b"hoist", optional_bool(self.hoist))?; + Ok(()) } - let pojo = Pojo { - default_registry_url, - default_registry_token, - default_registry_username, - default_registry_password, - default_registry_auth, - ignore_scripts: install.ignore_scripts, - link_workspace_packages: install.link_workspace_packages, - save_exact: install.exact, - hoist: install.hoist, - }; - Ok(bun_jsc::JSObject::create(&pojo, global)?.to_js()) } + let pojo = Pojo { + default_registry_url, + default_registry_token, + default_registry_username, + default_registry_password, + default_registry_auth, + ignore_scripts: install.ignore_scripts, + link_workspace_packages: install.link_workspace_packages, + save_exact: install.exact, + hoist: install.hoist, + }; + Ok(bun_jsc::JSObject::create(&pojo, global)?.to_js()) +} - pub(crate) fn parse(heap: &JsHeap<'_>, source: &[u8]) -> JsResult { - use bun_ast::ToJSError; - use bun_ini::Parser; - use bun_jsc::JsError; - - // SAFETY: neither parsing nor `expr_to_js` runs JavaScript: the keys are strings and the - // properties are defined, not set. What throws returns. - let global = unsafe { heap.global() }; - let env = global.bun_vm().as_mut().transpiler.env(); - let source = bun_ast::Source::init_path_string(b"", source); - let arena = bun_alloc::Arena::new(); - let mut parser = Parser::init(&source, env); - parser.parse(&arena)?; - - match bun_js_parser_jsc::expr_to_js(&parser.out, global) { - Ok(v) => Ok(v), - Err(ToJSError::OutOfMemory) => Err(JsError::OutOfMemory), - Err(ToJSError::JSError) => Err(JsError::Thrown), - Err(e) => { - Err(global.throw_error(bun_jsc::CrateError::from(e), "failed to turn AST into JS")) - } +pub(crate) fn parse(heap: &JsHeap<'_>, source: &[u8]) -> JsResult { + use bun_ast::ToJSError; + use bun_ini::Parser; + use bun_jsc::JsError; + + // SAFETY: neither parsing nor `expr_to_js` runs JavaScript: the keys are strings and the + // properties are defined, not set. What throws returns. + let global = unsafe { heap.global() }; + let env = global.bun_vm().as_mut().transpiler.env(); + let source = bun_ast::Source::init_path_string(b"", source); + let arena = bun_alloc::Arena::new(); + let mut parser = Parser::init(&source, env); + parser.parse(&arena)?; + + match bun_js_parser_jsc::expr_to_js(&parser.out, global) { + Ok(v) => Ok(v), + Err(ToJSError::OutOfMemory) => Err(JsError::OutOfMemory), + Err(ToJSError::JSError) => Err(JsError::Thrown), + Err(e) => { + Err(global.throw_error(bun_jsc::CrateError::from(e), "failed to turn AST into JS")) } } } diff --git a/src/install_jsc/install_binding.bind.ts b/src/install_jsc/install_binding.bind.ts index cb06428ed8ba..50ba2ef80057 100644 --- a/src/install_jsc/install_binding.bind.ts +++ b/src/install_jsc/install_binding.bind.ts @@ -1,6 +1,6 @@ import * as b from "../codegen/bindgen-lib.ts"; -export default b.module({ rustPath: "crate::install_binding::bun_install_js_bindings" }); +export default b.module({ rustPath: "crate::install_binding" }); export const installTestHelpers = b.namespace({ parseLockfile: { diff --git a/src/install_jsc/install_binding.rs b/src/install_jsc/install_binding.rs index c7b693c560a3..8c9d713eb8d3 100644 --- a/src/install_jsc/install_binding.rs +++ b/src/install_jsc/install_binding.rs @@ -1,89 +1,85 @@ use bun_jsc::bindgen::JsScope; use bun_jsc::{JSValue, StringJsc as _}; -pub(crate) mod bun_install_js_bindings { - use super::*; +pub(crate) fn parse_lockfile(cx: &mut JsScope<'_>, cwd: &[u8]) -> bun_jsc::JsResult { + use core::ptr::NonNull; - pub(crate) fn parse_lockfile(cx: &mut JsScope<'_>, cwd: &[u8]) -> bun_jsc::JsResult { - use core::ptr::NonNull; + use bstr::BStr; + use bun_core::String as BunString; + use bun_install::lockfile::lockfile_json_stringify_for_debugging::{ + WriteStream, WriteStreamOptions, json_stringify, + }; + use bun_install::lockfile::{LoadResult, Lockfile}; + use bun_paths::resolve_path; + use bun_sys::FdExt as _; - use bstr::BStr; - use bun_core::String as BunString; - use bun_install::lockfile::lockfile_json_stringify_for_debugging::{ - WriteStream, WriteStreamOptions, json_stringify, - }; - use bun_install::lockfile::{LoadResult, Lockfile}; - use bun_paths::resolve_path; - use bun_sys::FdExt as _; + let global = cx.global(); + let mut log = bun_ast::Log::init(); - let global = cx.global(); - let mut log = bun_ast::Log::init(); - - let dir = match bun_sys::open_dir_absolute_not_for_deleting_or_renaming(cwd) { - Ok(d) => d, - Err(err) => { - return Err(global.throw(format_args!( - "failed to open: {}, '{}'", - BStr::new(err.name()), - BStr::new(cwd), - ))); - } - }; - // `defer dir.close()` — closed at fn return. - let dir = scopeguard::guard(dir, |d| d.close()); + let dir = match bun_sys::open_dir_absolute_not_for_deleting_or_renaming(cwd) { + Ok(d) => d, + Err(err) => { + return Err(global.throw(format_args!( + "failed to open: {}, '{}'", + BStr::new(err.name()), + BStr::new(cwd), + ))); + } + }; + // `defer dir.close()` — closed at fn return. + let dir = scopeguard::guard(dir, |d| d.close()); - let lockfile_path = resolve_path::join_abs_string_z::( - cwd, - &[b"bun.lockb".as_slice()], - ); + let lockfile_path = resolve_path::join_abs_string_z::( + cwd, + &[b"bun.lockb".as_slice()], + ); - let mut lockfile_ = Lockfile::default(); + let mut lockfile_ = Lockfile::default(); - // `bun_vm()` returns a raw `*mut VirtualMachine`; deref locally so the - // env-loader fixup and the package-manager borrow are scoped - // independently. - // SAFETY: `bun_vm()` returns the live VM that owns `global`; this host fn - // runs on the JS thread so no concurrent `&mut VirtualMachine` exists. - let vm = global.bun_vm().as_mut(); - if vm.transpiler.resolver.env_loader.is_none() { - vm.transpiler.resolver.env_loader = NonNull::new(vm.transpiler.env); - } + // `bun_vm()` returns a raw `*mut VirtualMachine`; deref locally so the + // env-loader fixup and the package-manager borrow are scoped + // independently. + // SAFETY: `bun_vm()` returns the live VM that owns `global`; this host fn + // runs on the JS thread so no concurrent `&mut VirtualMachine` exists. + let vm = global.bun_vm().as_mut(); + if vm.transpiler.resolver.env_loader.is_none() { + vm.transpiler.resolver.env_loader = NonNull::new(vm.transpiler.env); + } - // as long as we aren't migration from `package-lock.json`, leaving this undefined is okay - let manager = vm.package_manager(); + // as long as we aren't migration from `package-lock.json`, leaving this undefined is okay + let manager = vm.package_manager(); - let load_result: LoadResult<'_> = - lockfile_.load_from_dir::(*dir, Some(manager), &mut log); + let load_result: LoadResult<'_> = + lockfile_.load_from_dir::(*dir, Some(manager), &mut log); - match load_result { - LoadResult::Err(err) => { - return Err(global.throw(format_args!( - "failed to load lockfile: {}, '{}'", - err.value.name(), - BStr::new(lockfile_path.as_bytes()), - ))); - } - LoadResult::NotFound => { - return Err(global.throw(format_args!( - "lockfile not found: '{}'", - BStr::new(lockfile_path.as_bytes()), - ))); - } - LoadResult::Ok(_) => {} + match load_result { + LoadResult::Err(err) => { + return Err(global.throw(format_args!( + "failed to load lockfile: {}, '{}'", + err.value.name(), + BStr::new(lockfile_path.as_bytes()), + ))); } + LoadResult::NotFound => { + return Err(global.throw(format_args!( + "lockfile not found: '{}'", + BStr::new(lockfile_path.as_bytes()), + ))); + } + LoadResult::Ok(_) => {} + } - // Feed the lockfile through the in-crate - // `WriteStream` (lockfile_json_stringify_for_debugging.rs) into a - // `Vec`. - let mut w = WriteStream::new(WriteStreamOptions { - indent: 2, - emit_nonportable_numbers_as_strings: true, - }); - // `jsonStringify` only surfaces the underlying writer's error; the - // `Vec` writer is infallible, so `expect` rather than swallowing. - json_stringify(&lockfile_, &mut w).expect("Vec JSON writer is infallible"); - let stringified = w.into_bytes(); + // Feed the lockfile through the in-crate + // `WriteStream` (lockfile_json_stringify_for_debugging.rs) into a + // `Vec`. + let mut w = WriteStream::new(WriteStreamOptions { + indent: 2, + emit_nonportable_numbers_as_strings: true, + }); + // `jsonStringify` only surfaces the underlying writer's error; the + // `Vec` writer is infallible, so `expect` rather than swallowing. + json_stringify(&lockfile_, &mut w).expect("Vec JSON writer is infallible"); + let stringified = w.into_bytes(); - BunString::borrow_utf8(&stringified).to_js_by_parse_json(global) - } + BunString::borrow_utf8(&stringified).to_js_by_parse_json(global) } diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 022a87d65d5a..9463eaf1b184 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -65,15 +65,15 @@ export const setSQLiteColumnNameTransform = ( ) => statement.native.$setColumnNameTransform(transform); export const patchInternals = { - parse: $bindgen("testing.bind.ts", "parse"), - apply: $bindgen("testing.bind.ts", "apply"), - makeDiff: $bindgen("testing.bind.ts", "makeDiff"), + parse: $bindgen("patch_testing.bind.ts", "parse"), + apply: $bindgen("patch_testing.bind.ts", "apply"), + makeDiff: $bindgen("patch_testing.bind.ts", "makeDiff"), }; export const internalSourceMap = { - fromVLQ: $bindgen("internal_jsc.bind.ts", "fromVLQ") as (vlq: string) => Uint8Array, - toVLQ: $bindgen("internal_jsc.bind.ts", "toVLQ"), - find: $bindgen("internal_jsc.bind.ts", "find"), + fromVLQ: $bindgen("InternalSourceMap.bind.ts", "fromVLQ") as (vlq: string) => Uint8Array, + toVLQ: $bindgen("InternalSourceMap.bind.ts", "toVLQ"), + find: $bindgen("InternalSourceMap.bind.ts", "find"), }; const shellLex = $bindgen("shell_body.bind.ts", "shellLex"); @@ -85,7 +85,7 @@ export const napiThreadsafeFunctionLiveCount = $bindgen("napi_body.bind.ts", "th export const bundlerWorkerLiveCount = $bindgen("JSBundler.bind.ts", "workerLiveCount"); -export const semverIntersects = $bindgen("SemverObject.bind.ts", "intersects") as ( +export const semverIntersects = $bindgen("SemverObjectInternals.bind.ts", "intersects") as ( left: string, right: string, ) => boolean; @@ -204,7 +204,7 @@ export const isModuleResolveFilenameSlowPathEnabled: () => boolean = $newCppFunc ); export const frameworkRouterInternals = { - parseRoutePattern: $bindgen("js.bind.ts", "parseRoutePattern"), + parseRoutePattern: $bindgen("FrameworkRouter.bind.ts", "parseRoutePattern"), FrameworkRouter: $bindgen("filesystem_router.bind.ts", "FrameworkFileSystemRouter"), }; @@ -280,6 +280,7 @@ export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { thisAndCallee: $bindgen("bindgen_test.bind.ts", "thisAndCallee"), doubled: $bindgen("bindgen_test.bind.ts", "doubled"), makeDoubled: $bindgen("bindgen_test.bind.ts", "makeDoubled"), + primes: $bindgen("bindgen_test.bind.ts", "primes"), testNamespace: $bindgen("bindgen_test.bind.ts", "testNamespace"), integerOrBigInt: $bindgen("bindgen_test.bind.ts", "integerOrBigInt"), looseInteger: $bindgen("bindgen_test.bind.ts", "looseInteger"), @@ -714,7 +715,7 @@ export const socketFaultInjection = { /** Arm a process-wide fault rule for one usockets bsd_* syscall. */ set: $bindgen("socket_body.bind.ts", "setSocketFault") as (rule: SocketFaultRule) => boolean, /** Disarm all fault rules. */ - clear: $bindgen("socket_body.bind.ts", "clearSocketFaults") as () => void, + clear: $bindgen("socket_body.bind.ts", "clearSocketFaults"), }; export const namedPipeInternals = { diff --git a/src/js/internal/bake.ts b/src/js/internal/bake.ts index 9aeda859e711..c090804bf023 100644 --- a/src/js/internal/bake.ts +++ b/src/js/internal/bake.ts @@ -12,11 +12,11 @@ const { serveOnFreePort, printBannerAndHandleShortcuts, } = require("internal/serve_cli"); -const setConfigPlugins = $bindgen("plugins.bind.ts", "setConfigPlugins"); -const isRestart = $bindgen("plugins.bind.ts", "isRestart"); -const restartProcess = $bindgen("plugins.bind.ts", "restartProcess"); -const setPages = $bindgen("plugins.bind.ts", "setPages"); -const checkFramework = $bindgen("plugins.bind.ts", "checkFramework"); +const setConfigPlugins = $bindgen("bake_plugins.bind.ts", "setConfigPlugins"); +const isRestart = $bindgen("bake_plugins.bind.ts", "isRestart"); +const restartProcess = $bindgen("bake_plugins.bind.ts", "restartProcess"); +const setPages = $bindgen("bake_plugins.bind.ts", "setPages"); +const checkFramework = $bindgen("bake_plugins.bind.ts", "checkFramework"); // `app/server.ts` runs before the code that checks its export, and can change these on the prototypes. const ObjectKeys = Object.keys; diff --git a/src/js/internal/fs/binding.ts b/src/js/internal/fs/binding.ts index 1f36d6a0c3d6..3fd6e5c969ab 100644 --- a/src/js/internal/fs/binding.ts +++ b/src/js/internal/fs/binding.ts @@ -120,4 +120,4 @@ type FsCallbackArm = { ) => void; }; -export default $bindgen("node_fs_binding.bind.ts", "createBinding")() as FsBinding & FsCallbackArm; +export default $bindgen("node_fs_binding.bind.ts", "binding") as FsBinding & FsCallbackArm; diff --git a/src/js/internal/quic/binding.ts b/src/js/internal/quic/binding.ts index 3b9033f8f1fe..df4d175870d8 100644 --- a/src/js/internal/quic/binding.ts +++ b/src/js/internal/quic/binding.ts @@ -1,2 +1,2 @@ // Bun's equivalent of Node's `internalBinding('quic')`. -export default $bindgen("node_quic_binding.bind.ts", "createBinding")(); +export default $bindgen("node_quic_binding.bind.ts", "binding"); diff --git a/src/js/internal/serve_cli.ts b/src/js/internal/serve_cli.ts index 0a3c6cf8f634..c260e49dafed 100644 --- a/src/js/internal/serve_cli.ts +++ b/src/js/internal/serve_cli.ts @@ -6,7 +6,7 @@ const path = require("node:path"); const env = Bun.env; -const isRestarting = $bindgen("plugins.bind.ts", "isRestarting"); +const isRestarting = $bindgen("bake_plugins.bind.ts", "isRestarting"); type ServeFlags = { /** `undefined` until `--host` or `--hostname` sets it. */ diff --git a/src/js/node/_http_common.ts b/src/js/node/_http_common.ts index 02a92e7e57a3..61d8baec4dc2 100644 --- a/src/js/node/_http_common.ts +++ b/src/js/node/_http_common.ts @@ -122,7 +122,7 @@ const validateHeaderValue = (name, value) => { // Node's `getOptionValue('--insecure-http-parser')`. The flag is fixed during // CLI parsing, so reading it once here is equivalent. -const insecureHTTPParser = $bindgen("node_http_binding.bind.ts", "getInsecureHTTPParser")(); +const insecureHTTPParser = $bindgen("node_http_binding.bind.ts", "insecureHTTPParser"); const kIncomingMessage = Symbol("IncomingMessage"); const kSkipPendingData = Symbol("SkipPendingData"); diff --git a/src/js/node/dgram.ts b/src/js/node/dgram.ts index f32273ee4411..3db896253140 100644 --- a/src/js/node/dgram.ts +++ b/src/js/node/dgram.ts @@ -146,7 +146,7 @@ const UV_EINVAL = process.platform === "win32" ? -4071 : -22; // Unlike the codes above, ECANCELED's number differs across the POSIX // platforms (Linux 125, Darwin 89, FreeBSD 85) and is a libuv-synthetic // -4081 on Windows, so it has to come from native. -const UV_ECANCELED = $bindgen("node_util_binding.bind.ts", "ecanceledErrorCode")(); +const UV_ECANCELED = $bindgen("node_util_binding.bind.ts", "ecanceledErrorCode"); // Node's --test-udp-no-try-send makes udp_wrap skip uv_udp_try_send so every // request is counted until its callback runs. Only Node's own tests use it. diff --git a/src/js/node/os.ts b/src/js/node/os.ts index 39ca250ef850..d19d2ad588fa 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -170,7 +170,7 @@ function bound(binding) { } const out = bound({ - hostCpuCount: $bindgen("node_os.bind.ts", "hostCpuCount")(), + hostCpuCount: $bindgen("node_os.bind.ts", "hostCpuCount"), cpus: $bindgen("node_os.bind.ts", "cpus"), freemem: $bindgen("node_os.bind.ts", "freemem"), getPriority: $bindgen("node_os.bind.ts", "getPriority"), diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 2ef84d74eced..690e170952bf 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -5911,8 +5911,7 @@ pub(crate) extern "C" fn Bun__ConsoleObject__timeLog( /// /// Ident concat via `${concat()}` is unstable (`macro_metavar_expr_concat`) /// and `paste` is not a `bun_jsc` dep, so the full `Bun__ConsoleObject__` -/// export symbol is passed verbatim — same pattern as `export_callbacks!` in -/// `runtime/api/BunObject.rs`. +/// export symbol is passed verbatim. macro_rules! console_noop_hooks { (str: $($name:ident),+ $(,)?) => {$( #[unsafe(no_mangle)] diff --git a/src/jsc/JSGlobalObject.rs b/src/jsc/JSGlobalObject.rs index 4a401cb9fa28..7d9d14bfb05e 100644 --- a/src/jsc/JSGlobalObject.rs +++ b/src/jsc/JSGlobalObject.rs @@ -95,9 +95,8 @@ impl JSGlobalObject { pub fn throw_stack_overflow(&self) -> JsError { // Wrap the raw FFI throw in a validation scope (mirrors `VM::throw_error`). - // A generated thunk inserts an `ExceptionValidationScope` - // one frame above us; with that scope - // present `~ThrowScope` inside `JSGlobalObject__throwStackOverflow` no + // The generated C++ host function has a `ThrowScope` above us; with that + // scope present `~ThrowScope` inside `JSGlobalObject__throwStackOverflow` no // longer sees its previous scope as above `topEntryFrame`, so it // `simulateThrow()`s and leaves `m_needExceptionCheck` set. Observe the // exception here so the caller's scope dtor doesn't assert "unchecked diff --git a/src/jsc/MarkedArgumentBuffer.rs b/src/jsc/MarkedArgumentBuffer.rs index 5144ee37c799..78956bf3214d 100644 --- a/src/jsc/MarkedArgumentBuffer.rs +++ b/src/jsc/MarkedArgumentBuffer.rs @@ -70,7 +70,7 @@ impl MarkedArgumentBuffer { }) } - pub fn run(ctx: &mut T, func: extern "C" fn(ctx: *mut T, args: *mut MarkedArgumentBuffer)) { + fn run(ctx: &mut T, func: extern "C" fn(ctx: *mut T, args: *mut MarkedArgumentBuffer)) { // `MarkedArgumentBuffer__run` round-trips `ctx` opaquely back to `func`, // and `func`'s ABI is identical modulo the // pointee types (both params are thin pointers). diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index b7d7d88ea71b..1f8e1ea8796e 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -392,6 +392,7 @@ export const thisAndCallee = b.fn({ export const doubled = b.fn({ export: "Bun__BindgenTest__doubled", args: { value: b.i32 }, returns: b.i32 }); /** A new function, made in Rust, that runs `doubled`. */ export const makeDoubled = b.fn({ returns: b.any, capability: "js" }); +export const primes = b.value({ returns: b.array(b.u32) }); export const testNamespace = b.namespace({ increment: { args: { value: b.i32 }, returns: b.i32 }, decrement: { args: { value: b.i32 }, returns: b.i32 }, diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index d4d603281115..015af6268983 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -821,6 +821,10 @@ pub(crate) fn make_doubled(cx: &mut JsScope<'_>) -> JSValue { ) } +pub(crate) fn primes() -> [u32; 3] { + [2, 3, 5] +} + pub(crate) fn increment(value: i32) -> i32 { value.wrapping_add(1) } diff --git a/src/jsc/bindings/Bindgen/ExternTraits.h b/src/jsc/bindings/Bindgen/ExternTraits.h index 5a421d7dad1d..9bf15fa81e1b 100644 --- a/src/jsc/bindings/Bindgen/ExternTraits.h +++ b/src/jsc/bindings/Bindgen/ExternTraits.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include "Bindgen/JSBytesView.h" @@ -172,6 +173,9 @@ inline JSC::ArgList variadicArguments(JSC::CallFrame* callFrame, unsigned startI return { callFrame, static_cast(std::min(startIndex, callFrame->argumentCount())) }; } +// What `$bindgen` returns for a function. +JSC::EncodedJSValue createFunction(JSC::JSGlobalObject*, unsigned length, ASCIILiteral name, JSC::NativeFunction); + // A converted argument and the argument itself. template struct ExternWithValue { diff --git a/src/jsc/bindings/Bindgen/ToJS.cpp b/src/jsc/bindings/Bindgen/ToJS.cpp index c738b07c1b5d..94e6b623d9ee 100644 --- a/src/jsc/bindings/Bindgen/ToJS.cpp +++ b/src/jsc/bindings/Bindgen/ToJS.cpp @@ -1,13 +1,20 @@ #include "root.h" +#include "Bindgen/ExternTraits.h" #include "Bindgen/SchemaStrings.h" #include "helpers.h" #include #include +#include #include namespace Bun::Bindgen { using namespace JSC; +EncodedJSValue createFunction(JSGlobalObject* globalObject, unsigned length, ASCIILiteral name, NativeFunction function) +{ + return JSValue::encode(JSFunction::create(getVM(globalObject), globalObject, length, name, function, ImplementationVisibility::Public)); +} + // The members are named by Tables::memberNames, from `firstMemberNameIndex`. An empty value is a member that is absent. static EncodedJSValue putMembers(VM& vm, JSObject* object, unsigned firstMemberNameIndex, const JSValue* values, size_t count) { diff --git a/src/jsc/host_fn.rs b/src/jsc/host_fn.rs index f01543719570..9a146c03a05d 100644 --- a/src/jsc/host_fn.rs +++ b/src/jsc/host_fn.rs @@ -31,11 +31,11 @@ use crate::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult}; // `jsc.conv` is `"sysv64"` on Windows-x64 (JSC always uses System V there) and // `"C"` everywhere else. Rust forbids macros in ABI position, so cfg-split the // alias — the `#[bun_jsc::host_call]` proc-macro emits the matching ABI on -// each target so `JsHostFn`-typed slots accept its output without a cast. +// each target so `JSHostFn`-typed slots accept its output without a cast. #[cfg(all(windows, target_arch = "x86_64"))] -pub type JsHostFn = unsafe extern "sysv64" fn(*mut JSGlobalObject, *mut CallFrame) -> JSValue; +pub type JSHostFn = unsafe extern "sysv64" fn(*mut JSGlobalObject, *mut CallFrame) -> JSValue; #[cfg(not(all(windows, target_arch = "x86_64")))] -pub type JsHostFn = unsafe extern "C" fn(*mut JSGlobalObject, *mut CallFrame) -> JSValue; +pub type JSHostFn = unsafe extern "C" fn(*mut JSGlobalObject, *mut CallFrame) -> JSValue; /// Expand to the JSC host-function ABI string for the current target. Rust /// forbids macros in `extern ""` position, but *does* accept them as the @@ -60,10 +60,7 @@ macro_rules! jsc_host_abi { }; } -// Enough call sites use the acronym-caps spelling that both must resolve. -pub use JsHostFn as JSHostFn; - -// ─────────────────────── host-fn wrapping (proc-macro) ─────────────────────── +// ───────────────────────────── result mapping ───────────────────────────── /// Map a `JsResult` to the raw `JSValue` a host fn must return /// (`.zero` when an exception is pending). @@ -104,7 +101,6 @@ fn terminated_beneath_script(global_this: &JSGlobalObject) -> JSValue { } fn debug_exception_assertion(global_this: &JSGlobalObject, value: JSValue, func: &'static str) { - // `func` is the fn name string (the proc-macro supplies `stringify!(fn_name)`). if Environment::IS_DEBUG { if !value.is_empty() && global_this.has_exception() { let mut formatter = jsc::ConsoleObject::Formatter::new(global_this); @@ -299,9 +295,9 @@ mod private { global: &JSGlobalObject, symbol_name: Option<&EncodedSlice>, arg_count: u32, - // `JsHostFn` is already `unsafe extern "C" fn(...)`, i.e. the + // `JSHostFn` is already `unsafe extern "C" fn(...)`, i.e. the // fn-pointer type. - function: JsHostFn, + function: JSHostFn, data: *mut c_void, ) -> JSValue; @@ -331,7 +327,7 @@ pub fn new_function_with_data( global_object: &JSGlobalObject, symbol_name: Option<&EncodedSlice>, arg_count: u32, - function: JsHostFn, + function: JSHostFn, data: *mut c_void, ) -> JSValue { jsc::mark_binding(); diff --git a/src/jsc/job.rs b/src/jsc/job.rs index 362059249164..3e6fb5f9ddc5 100644 --- a/src/jsc/job.rs +++ b/src/jsc/job.rs @@ -72,7 +72,7 @@ impl JSGlobalObject { } } - /// What a host function starts with: this global, and the context of the script that called it. + /// This global, and the context of the script that called the host function. #[inline] pub fn js_thread_of_caller<'a>(&'a self, frame: &crate::CallFrame) -> JsThread<'a> { self.js_thread(self.bun_vm().context_of_caller(frame)) diff --git a/src/jsc/resolver_jsc.bind.ts b/src/jsc/resolver_jsc.bind.ts index bd607e2359fa..4bf23c0109b5 100644 --- a/src/jsc/resolver_jsc.bind.ts +++ b/src/jsc/resolver_jsc.bind.ts @@ -1,9 +1,5 @@ import * as b from "../codegen/bindgen-lib.ts"; -export const _nodeModulePaths = b.fn({ - args: { path: b.any }, - returns: b.any, - capability: "js", - throws: true, - rustPath: "crate::resolver_jsc::node_module_paths", -}); +export default b.module({ rustPath: "crate::resolver_jsc" }); + +export const _nodeModulePaths = b.fn({ args: { path: b.any }, returns: b.any, capability: "js", throws: true }); diff --git a/src/patch_jsc/testing.bind.ts b/src/patch_jsc/patch_testing.bind.ts similarity index 88% rename from src/patch_jsc/testing.bind.ts rename to src/patch_jsc/patch_testing.bind.ts index d32b8061025f..9e0ebfde57b8 100644 --- a/src/patch_jsc/testing.bind.ts +++ b/src/patch_jsc/patch_testing.bind.ts @@ -1,6 +1,6 @@ import * as b from "../codegen/bindgen-lib.ts"; -export default b.module({ rustPath: "crate::testing::TestingAPIs" }); +export default b.module({ rustPath: "crate::testing" }); export const parse = b.fn({ args: { patchfile: b.string.loose.utf8 }, diff --git a/src/patch_jsc/testing.rs b/src/patch_jsc/testing.rs index 8f080938b2f9..13c814220abd 100644 --- a/src/patch_jsc/testing.rs +++ b/src/patch_jsc/testing.rs @@ -5,135 +5,131 @@ use bun_jsc::{JSGlobalObject, JSValue, JsResult, SysErrorJsc}; use bun_patch::{ApplyError, ParseErr, ParseErrAt, PatchFile, git_diff_internal, parse_patch_file}; use bun_sys::{Fd, FdExt}; -pub(crate) struct TestingAPIs; - -impl TestingAPIs { - pub(crate) fn make_diff( - cx: &mut JsScope<'_>, - old_folder: &[u8], - new_folder: &[u8], - ) -> JsResult> { - let global = cx.global(); - // `git_diff_internal` routes through `bun_spawn::sync`, which on - // Windows derefs `WindowsOptions.loop_` — supply the JS event loop. - // `global.bun_vm().event_loop()` is the live per-thread `jsc::EventLoop`. - let mut loop_ = bun_jsc::AnyEventLoop::js(global.bun_vm().event_loop().cast()); - let diff = match git_diff_internal(old_folder, new_folder, &mut loop_) { - Ok(d) => d, - Err(e) => return Err(global.throw_error(e, "failed to make diff")), - }; - diff.map_err(|e| { - global.throw(format_args!( - "failed to make diff: {}", - bstr::BStr::new(e.as_slice()) - )) - }) - } +pub(crate) fn make_diff( + cx: &mut JsScope<'_>, + old_folder: &[u8], + new_folder: &[u8], +) -> JsResult> { + let global = cx.global(); + // `git_diff_internal` routes through `bun_spawn::sync`, which on + // Windows derefs `WindowsOptions.loop_` — supply the JS event loop. + // `global.bun_vm().event_loop()` is the live per-thread `jsc::EventLoop`. + let mut loop_ = bun_jsc::AnyEventLoop::js(global.bun_vm().event_loop().cast()); + let diff = match git_diff_internal(old_folder, new_folder, &mut loop_) { + Ok(d) => d, + Err(e) => return Err(global.throw_error(e, "failed to make diff")), + }; + diff.map_err(|e| { + global.throw(format_args!( + "failed to make diff: {}", + bstr::BStr::new(e.as_slice()) + )) + }) +} - pub(crate) fn apply<'a>( - cx: &mut JsScope<'a>, - patchfile: JsLocal<'a>, - dir: JsLocal<'a>, - ) -> JsResult { - let global = cx.global(); - let args = Self::parse_apply_args(global, patchfile.to_js(), dir.to_js())?; - - let patchfile: PatchFile<'_> = - parse_patch_file(&args.patchfile_txt).expect("validated in parse_apply_args"); - - match patchfile.apply(args.dirfd) { - Ok(()) => {} - Err(ApplyError::Sys(err)) => { - return Err(global.throw_value(SysErrorJsc::to_js(&err, global))); - } - Err(err @ ApplyError::HunkDoesNotApply { .. }) => { - return Err(global.throw(format_args!("{}", err))); - } +pub(crate) fn apply<'a>( + cx: &mut JsScope<'a>, + patchfile: JsLocal<'a>, + dir: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let args = parse_apply_args(global, patchfile.to_js(), dir.to_js())?; + + let patchfile: PatchFile<'_> = + parse_patch_file(&args.patchfile_txt).expect("validated in parse_apply_args"); + + match patchfile.apply(args.dirfd) { + Ok(()) => {} + Err(ApplyError::Sys(err)) => { + return Err(global.throw_value(SysErrorJsc::to_js(&err, global))); + } + Err(err @ ApplyError::HunkDoesNotApply { .. }) => { + return Err(global.throw(format_args!("{}", err))); } - - Ok(true) } - /// Used in JS tests, see `internal-for-testing.ts` and patch tests. - pub(crate) fn parse(heap: &JsHeap<'_>, patchfile_src: &[u8]) -> JsResult> { - let patchfile = match parse_patch_file(patchfile_src) { - Ok(p) => p, - Err(ParseErrAt { err: e, .. }) => { - // SAFETY: it throws, and the body returns. - let global = unsafe { heap.global() }; - if e == ParseErr::hunk_header_integrity_check_failed { - return Err(global.throw_error(bun_patch::Error::from(e), "this indicates either that the supplied patch file was incorrect, or there is a bug in Bun. Please check your .patch file, or open a GitHub issue :)")); - } else { - return Err( - global.throw_error(bun_patch::Error::from(e), "failed to parse patch file") - ); - } - } - }; + Ok(true) +} - let mut str: Vec = Vec::new(); - { - use std::io::Write as _; - write!(&mut str, "{}", bun_patch::json_fmt(&patchfile)).expect("unreachable"); +/// Used in JS tests, see `internal-for-testing.ts` and patch tests. +pub(crate) fn parse(heap: &JsHeap<'_>, patchfile_src: &[u8]) -> JsResult> { + let patchfile = match parse_patch_file(patchfile_src) { + Ok(p) => p, + Err(ParseErrAt { err: e, .. }) => { + // SAFETY: it throws, and the body returns. + let global = unsafe { heap.global() }; + if e == ParseErr::hunk_header_integrity_check_failed { + return Err(global.throw_error(bun_patch::Error::from(e), "this indicates either that the supplied patch file was incorrect, or there is a bug in Bun. Please check your .patch file, or open a GitHub issue :)")); + } else { + return Err( + global.throw_error(bun_patch::Error::from(e), "failed to parse patch file") + ); + } } - Ok(str) - } + }; - pub(crate) fn parse_apply_args( - global: &JSGlobalObject, - patchfile_js: JSValue, - dir_js: JSValue, - ) -> JsResult { - let dir_fd = if !dir_js.is_undefined() { - let bunstr = dir_js.to_bun_string(global)?; - let path = bunstr.to_owned_slice_z(); - - match bun_sys::open( - path.as_zstr(), - bun_sys::O::DIRECTORY | bun_sys::O::RDONLY, - 0, - ) { - Err(e) => { - let js_err = SysErrorJsc::to_js(&e.with_path(path.as_bytes()), global); - return Err(global.throw_value(js_err)); - } - Ok(fd) => fd, - } - } else { - Fd::cwd() - }; + let mut str: Vec = Vec::new(); + { + use std::io::Write as _; + write!(&mut str, "{}", bun_patch::json_fmt(&patchfile)).expect("unreachable"); + } + Ok(str) +} - let patchfile_bunstr = match patchfile_js.to_bun_string(global) { - Ok(bunstr) => bunstr, +pub(crate) fn parse_apply_args( + global: &JSGlobalObject, + patchfile_js: JSValue, + dir_js: JSValue, +) -> JsResult { + let dir_fd = if !dir_js.is_undefined() { + let bunstr = dir_js.to_bun_string(global)?; + let path = bunstr.to_owned_slice_z(); + + match bun_sys::open( + path.as_zstr(), + bun_sys::O::DIRECTORY | bun_sys::O::RDONLY, + 0, + ) { Err(e) => { - if Fd::cwd() != dir_fd { - dir_fd.close(); - } - return Err(e); + let js_err = SysErrorJsc::to_js(&e.with_path(path.as_bytes()), global); + return Err(global.throw_value(js_err)); } - }; - let patchfile_src = patchfile_bunstr.to_utf8(); - - // Validate the patch parses; on failure, clean up `dir_fd` and throw. - // The parsed `PatchFile<'_>` borrows `patchfile_src`, so it cannot be - // returned alongside its source without a self-referential struct - // (forbidden by PORTING.md). We discard it here; `apply()` reparses - // from the owned bytes below. - if let Err(ParseErrAt { err: e, .. }) = parse_patch_file(patchfile_src.slice()) { - // TODO: HAVE @zackradisic REVIEW THIS DIFF + Ok(fd) => fd, + } + } else { + Fd::cwd() + }; + + let patchfile_bunstr = match patchfile_js.to_bun_string(global) { + Ok(bunstr) => bunstr, + Err(e) => { if Fd::cwd() != dir_fd { dir_fd.close(); } - - drop(patchfile_src); - return Err(global.throw_error(bun_patch::Error::from(e), "failed to parse patchfile")); + return Err(e); + } + }; + let patchfile_src = patchfile_bunstr.to_utf8(); + + // Validate the patch parses; on failure, clean up `dir_fd` and throw. + // The parsed `PatchFile<'_>` borrows `patchfile_src`, so it cannot be + // returned alongside its source without a self-referential struct + // (forbidden by PORTING.md). We discard it here; `apply()` reparses + // from the owned bytes below. + if let Err(ParseErrAt { err: e, .. }) = parse_patch_file(patchfile_src.slice()) { + // TODO: HAVE @zackradisic REVIEW THIS DIFF + if Fd::cwd() != dir_fd { + dir_fd.close(); } - Ok(ApplyArgs { - dirfd: dir_fd, - patchfile_txt: patchfile_src.into_vec(), - }) + drop(patchfile_src); + return Err(global.throw_error(bun_patch::Error::from(e), "failed to parse patchfile")); } + + Ok(ApplyArgs { + dirfd: dir_fd, + patchfile_txt: patchfile_src.into_vec(), + }) } pub(crate) struct ApplyArgs { diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index bc4ac342ef99..0acda3a6aedb 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -96,7 +96,7 @@ export const inflateSync = zlib("inflate_sync", ZlibDecompressionOptions); /** `Bun.inspect()` is `""` and `Bun.inspect(undefined)` is `"undefined"`. */ export const inspect = b.fn({ export: "Bun__BunObject__inspect", - args: { arguments: b.any.variadic }, + args: { args: b.any.variadic }, returns: b.string.utf8, capability: "js", throws: true, diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index ef6d2c672867..b344e9f440b7 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -409,9 +409,9 @@ pub(crate) fn table( Ok(array) } -pub(crate) fn inspect(cx: &mut JsScope<'_>, arguments: &[JsLocal<'_>]) -> JsResult> { +pub(crate) fn inspect(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult> { let global_this = cx.global(); - let arguments = JsLocal::to_js_slice(arguments); + let arguments = JsLocal::to_js_slice(args); if arguments.is_empty() { return Ok(Vec::new()); } diff --git a/src/runtime/api/crash_handler_jsc.bind.ts b/src/runtime/api/crash_handler_jsc.bind.ts index 19b9af3fbaad..273b99000204 100644 --- a/src/runtime/api/crash_handler_jsc.bind.ts +++ b/src/runtime/api/crash_handler_jsc.bind.ts @@ -1,6 +1,6 @@ import * as b from "../../codegen/bindgen-lib.ts"; -export default b.module({ rustPath: "crate::api::crash_handler_jsc::js_bindings" }); +export default b.module({ rustPath: "crate::api::crash_handler_jsc" }); export const crashHandler = b.namespace({ getMachOImageZeroOffset: { returns: b.f64.optional, length: 1 }, diff --git a/src/runtime/api/crash_handler_jsc.rs b/src/runtime/api/crash_handler_jsc.rs index 165631cd30b9..73d00d74b12c 100644 --- a/src/runtime/api/crash_handler_jsc.rs +++ b/src/runtime/api/crash_handler_jsc.rs @@ -8,208 +8,204 @@ use bun_crash_handler as crash_handler; use bun_jsc::bindgen::JsScope; use bun_jsc::{JSValue, JsResult, StringJsc}; -pub(crate) mod js_bindings { - use super::*; - - pub(crate) fn get_mach_o_image_zero_offset() -> Option { - #[cfg(not(target_os = "macos"))] - { - None - } - #[cfg(target_os = "macos")] - { - unsafe extern "C" { - safe fn _dyld_get_image_header(image_index: u32) -> *const core::ffi::c_void; - safe fn _dyld_get_image_vmaddr_slide(image_index: u32) -> isize; - } - let header = _dyld_get_image_header(0); - if header.is_null() { - return None; - } - let base_address = header as usize; - let vmaddr_slide = _dyld_get_image_vmaddr_slide(0) as usize; - - Some((base_address - vmaddr_slide) as f64) - } +pub(crate) fn get_mach_o_image_zero_offset() -> Option { + #[cfg(not(target_os = "macos"))] + { + None } - - pub(crate) fn segfault() { - crash_handler::suppress_core_dumps_if_necessary(); - // Under ASAN the SIGSEGV handler is intentionally not installed - // (`reset_on_posix()` early-returns so ASAN's own DEADLYSIGNAL diagnostic - // stays in charge of real faults). A bare deref here would route to ASAN's - // handler — no trace string, no upload — and the `segfault should report` - // test times out waiting for a POST that never comes. Invoke the handler - // directly with the address it *would* have received from `siginfo_t`; the - // code path under test (`crash_handler(SegmentationFault, …)` → trace - // string → `report()`) is exactly what `handle_segfault_posix` calls. - if Environment::ENABLE_ASAN { - crash_handler::crash_handler( - crash_handler::CrashReason::SegmentationFault(0xDEADBEEF), - crash_handler::TraceSeed::BeginAddr(crash_handler::debug::return_address()), - ); + #[cfg(target_os = "macos")] + { + unsafe extern "C" { + safe fn _dyld_get_image_header(image_index: u32) -> *const core::ffi::c_void; + safe fn _dyld_get_image_vmaddr_slide(image_index: u32) -> isize; } - // SAFETY: intentionally dereferencing an invalid address to trigger SIGSEGV for testing. - unsafe { - let ptr = 0xDEADBEEFusize as *mut u64; - core::ptr::write_unaligned(ptr, 0xDEADBEEF); - core::hint::black_box(ptr); + let header = _dyld_get_image_header(0); + if header.is_null() { + return None; } - } + let base_address = header as usize; + let vmaddr_slide = _dyld_get_image_vmaddr_slide(0) as usize; - /// Triggers a segfault with the fault PC inside a system DLL rather than - /// inside bun.exe. Exercises the Windows fault-context unwinder: the walk - /// must recover the bun frames that called into the DLL. - pub(crate) fn segfault_in_dll() { - crash_handler::suppress_core_dumps_if_necessary(); - #[cfg(windows)] - { - // `RtlFillMemory` is exported by ntdll.dll, so the faulting - // instruction is outside bun.exe's image. - #[link(name = "ntdll")] - unsafe extern "system" { - fn RtlFillMemory(dest: *mut core::ffi::c_void, length: usize, fill: u8); - } - // SAFETY: intentionally writing to an invalid address to trigger an - // access violation inside ntdll.dll for testing. - unsafe { RtlFillMemory(0xDEADBEEFusize as *mut _, 8, 0) }; - } - #[cfg(not(windows))] - { - // No equivalent on POSIX (the fault-context walk is fp-based and - // doesn't care which image the fault is in); fall through to the - // in-bun segfault so the test hook is defined everywhere. - segfault(); - } + Some((base_address - vmaddr_slide) as f64) } +} - pub(crate) fn panic() { - crash_handler::suppress_core_dumps_if_necessary(); - crash_handler::panic_impl(b"invoked crashByPanic() handler", None); +pub(crate) fn segfault() { + crash_handler::suppress_core_dumps_if_necessary(); + // Under ASAN the SIGSEGV handler is intentionally not installed + // (`reset_on_posix()` early-returns so ASAN's own DEADLYSIGNAL diagnostic + // stays in charge of real faults). A bare deref here would route to ASAN's + // handler — no trace string, no upload — and the `segfault should report` + // test times out waiting for a POST that never comes. Invoke the handler + // directly with the address it *would* have received from `siginfo_t`; the + // code path under test (`crash_handler(SegmentationFault, …)` → trace + // string → `report()`) is exactly what `handle_segfault_posix` calls. + if Environment::ENABLE_ASAN { + crash_handler::crash_handler( + crash_handler::CrashReason::SegmentationFault(0xDEADBEEF), + crash_handler::TraceSeed::BeginAddr(crash_handler::debug::return_address()), + ); } + // SAFETY: intentionally dereferencing an invalid address to trigger SIGSEGV for testing. + unsafe { + let ptr = 0xDEADBEEFusize as *mut u64; + core::ptr::write_unaligned(ptr, 0xDEADBEEF); + core::hint::black_box(ptr); + } +} - pub(crate) fn abort() { - crash_handler::suppress_core_dumps_if_necessary(); - // Under ASAN the POSIX signal handlers are not installed; invoke the - // handler directly so the reporter test still observes the upload. - if Environment::ENABLE_ASAN || cfg!(windows) { - crash_handler::crash_handler( - crash_handler::CrashReason::Abort, - crash_handler::TraceSeed::BeginAddr(crash_handler::debug::return_address()), - ); - } - #[cfg(unix)] - // SAFETY: libc::abort has no preconditions; never returns. - unsafe { - libc::abort(); +/// Triggers a segfault with the fault PC inside a system DLL rather than +/// inside bun.exe. Exercises the Windows fault-context unwinder: the walk +/// must recover the bun frames that called into the DLL. +pub(crate) fn segfault_in_dll() { + crash_handler::suppress_core_dumps_if_necessary(); + #[cfg(windows)] + { + // `RtlFillMemory` is exported by ntdll.dll, so the faulting + // instruction is outside bun.exe's image. + #[link(name = "ntdll")] + unsafe extern "system" { + fn RtlFillMemory(dest: *mut core::ffi::c_void, length: usize, fill: u8); } + // SAFETY: intentionally writing to an invalid address to trigger an + // access violation inside ntdll.dll for testing. + unsafe { RtlFillMemory(0xDEADBEEFusize as *mut _, 8, 0) }; } - - /// Dies like foreign native code, with Bun's crash handler provably out - /// of the way on both platforms: `__fastfail` on Windows (uncatchable, - /// exit code 0xC0000409, same as UCRT abort(), Rust aborts, /GS checks) - /// and a raw SIGABRT on POSIX (handlers reset first, like the - /// `raiseIgnoringPanicHandler` binding below). The `abort` binding - /// above is the opposite: it routes into the crash handler on purpose. - pub(crate) fn fastfail() { - crash_handler::suppress_core_dumps_if_necessary(); - #[cfg(windows)] - std::process::abort(); - #[cfg(not(windows))] - Global::raise_ignoring_panic_handler(bun_core::SignalCode::SIGABRT); + #[cfg(not(windows))] + { + // No equivalent on POSIX (the fault-context walk is fp-based and + // doesn't care which image the fault is in); fall through to the + // in-bun segfault so the test hook is defined everywhere. + segfault(); } +} - pub(crate) fn trap() { - crash_handler::suppress_core_dumps_if_necessary(); - if Environment::ENABLE_ASAN || cfg!(windows) { - crash_handler::crash_handler( - crash_handler::CrashReason::Trap(0), - crash_handler::TraceSeed::BeginAddr(crash_handler::debug::return_address()), - ); - } - // int3 on x86_64 / brk on aarch64: both deliver SIGTRAP, matching the - // instruction WTF's CRASH()/RELEASE_ASSERT emits. - #[cfg(all(unix, target_arch = "x86_64"))] - // SAFETY: single trap instruction; no inputs/outputs. - unsafe { - core::arch::asm!("int3", options(nomem, nostack)); - } - #[cfg(all(unix, target_arch = "aarch64"))] - // SAFETY: single trap instruction; no inputs/outputs. - unsafe { - core::arch::asm!("brk #0", options(nomem, nostack)); - } - #[cfg(all(unix, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] +pub(crate) fn panic() { + crash_handler::suppress_core_dumps_if_necessary(); + crash_handler::panic_impl(b"invoked crashByPanic() handler", None); +} + +pub(crate) fn abort() { + crash_handler::suppress_core_dumps_if_necessary(); + // Under ASAN the POSIX signal handlers are not installed; invoke the + // handler directly so the reporter test still observes the upload. + if Environment::ENABLE_ASAN || cfg!(windows) { crash_handler::crash_handler( - crash_handler::CrashReason::Trap(0), + crash_handler::CrashReason::Abort, crash_handler::TraceSeed::BeginAddr(crash_handler::debug::return_address()), ); } - - pub(crate) fn root_error() { - crash_handler::handle_root_error("Unexpected"); + #[cfg(unix)] + // SAFETY: libc::abort has no preconditions; never returns. + unsafe { + libc::abort(); } +} - pub(crate) fn out_of_memory() { - crash_handler::suppress_core_dumps_if_necessary(); - bun_core::out_of_memory(); - } +/// Dies like foreign native code, with Bun's crash handler provably out +/// of the way on both platforms: `__fastfail` on Windows (uncatchable, +/// exit code 0xC0000409, same as UCRT abort(), Rust aborts, /GS checks) +/// and a raw SIGABRT on POSIX (handlers reset first, like the +/// `raiseIgnoringPanicHandler` binding below). The `abort` binding +/// above is the opposite: it routes into the crash handler on purpose. +pub(crate) fn fastfail() { + crash_handler::suppress_core_dumps_if_necessary(); + #[cfg(windows)] + std::process::abort(); + #[cfg(not(windows))] + Global::raise_ignoring_panic_handler(bun_core::SignalCode::SIGABRT); +} - /// Calls `mi_new` or `mi_new_nothrow`, which back C++ `operator new` where mimalloc overrides it. - /// Only `mi_new_nothrow` may return null. - pub(crate) fn operator_new(nothrow: bool) -> Option { - unsafe extern "C" { - // safe: both accept any size, and the result is never dereferenced. - safe fn mi_new(size: usize) -> *mut core::ffi::c_void; - safe fn mi_new_nothrow(size: usize) -> *mut core::ffi::c_void; - } - if nothrow { - let refused = mi_new_nothrow(usize::MAX / 2).is_null(); - return Some(refused); - } - crash_handler::suppress_core_dumps_if_necessary(); - let _ = mi_new(usize::MAX / 2); - None +pub(crate) fn trap() { + crash_handler::suppress_core_dumps_if_necessary(); + if Environment::ENABLE_ASAN || cfg!(windows) { + crash_handler::crash_handler( + crash_handler::CrashReason::Trap(0), + crash_handler::TraceSeed::BeginAddr(crash_handler::debug::return_address()), + ); } - - pub(crate) fn raise_ignoring_panic_handler() { - crash_handler::suppress_core_dumps_if_necessary(); - Global::raise_ignoring_panic_handler(bun_core::SignalCode::SIGSEGV); + // int3 on x86_64 / brk on aarch64: both deliver SIGTRAP, matching the + // instruction WTF's CRASH()/RELEASE_ASSERT emits. + #[cfg(all(unix, target_arch = "x86_64"))] + // SAFETY: single trap instruction; no inputs/outputs. + unsafe { + core::arch::asm!("int3", options(nomem, nostack)); + } + #[cfg(all(unix, target_arch = "aarch64"))] + // SAFETY: single trap instruction; no inputs/outputs. + unsafe { + core::arch::asm!("brk #0", options(nomem, nostack)); } + #[cfg(all(unix, not(any(target_arch = "x86_64", target_arch = "aarch64"))))] + crash_handler::crash_handler( + crash_handler::CrashReason::Trap(0), + crash_handler::TraceSeed::BeginAddr(crash_handler::debug::return_address()), + ); +} - pub(crate) fn get_feature_data(cx: &mut JsScope<'_>) -> JsResult { - let global = cx.global(); - let obj = JSValue::create_empty_object(global, 5); - let list = analytics::PACKED_FEATURES_LIST; - // Not `static_`: a feature can be named like a common string ("s3"). - let array = JSValue::create_array_from_iter(global, list.iter(), |feature| { - BunString::clone_latin1(feature.as_ref()).into_js(global) - })?; - obj.put(global, "features", array); - obj.put( - global, - "version", - BunString::static_(Global::package_json_version).to_js(global)?, - ); - obj.put( - global, - "is_canary", - JSValue::js_boolean(Environment::IS_CANARY), - ); +pub(crate) fn root_error() { + crash_handler::handle_root_error("Unexpected"); +} - // This is the source of truth for the git sha. - // Not the github ref or the git tag. - obj.put( - global, - "revision", - BunString::static_(Environment::GIT_SHA).to_js(global)?, - ); +pub(crate) fn out_of_memory() { + crash_handler::suppress_core_dumps_if_necessary(); + bun_core::out_of_memory(); +} - obj.put( - global, - "generated_at", - JSValue::js_number_from_int64(bun_core::time::milli_timestamp().max(0)), - ); - Ok(obj) +/// Calls `mi_new` or `mi_new_nothrow`, which back C++ `operator new` where mimalloc overrides it. +/// Only `mi_new_nothrow` may return null. +pub(crate) fn operator_new(nothrow: bool) -> Option { + unsafe extern "C" { + // safe: both accept any size, and the result is never dereferenced. + safe fn mi_new(size: usize) -> *mut core::ffi::c_void; + safe fn mi_new_nothrow(size: usize) -> *mut core::ffi::c_void; + } + if nothrow { + let refused = mi_new_nothrow(usize::MAX / 2).is_null(); + return Some(refused); } + crash_handler::suppress_core_dumps_if_necessary(); + let _ = mi_new(usize::MAX / 2); + None +} + +pub(crate) fn raise_ignoring_panic_handler() { + crash_handler::suppress_core_dumps_if_necessary(); + Global::raise_ignoring_panic_handler(bun_core::SignalCode::SIGSEGV); +} + +pub(crate) fn get_feature_data(cx: &mut JsScope<'_>) -> JsResult { + let global = cx.global(); + let obj = JSValue::create_empty_object(global, 5); + let list = analytics::PACKED_FEATURES_LIST; + // Not `static_`: a feature can be named like a common string ("s3"). + let array = JSValue::create_array_from_iter(global, list.iter(), |feature| { + BunString::clone_latin1(feature.as_ref()).into_js(global) + })?; + obj.put(global, "features", array); + obj.put( + global, + "version", + BunString::static_(Global::package_json_version).to_js(global)?, + ); + obj.put( + global, + "is_canary", + JSValue::js_boolean(Environment::IS_CANARY), + ); + + // This is the source of truth for the git sha. + // Not the github ref or the git tag. + obj.put( + global, + "revision", + BunString::static_(Environment::GIT_SHA).to_js(global)?, + ); + + obj.put( + global, + "generated_at", + JSValue::js_number_from_int64(bun_core::time::milli_timestamp().max(0)), + ); + Ok(obj) } diff --git a/src/runtime/bake/config/plugins.bind.ts b/src/runtime/bake/config/bake_plugins.bind.ts similarity index 100% rename from src/runtime/bake/config/plugins.bind.ts rename to src/runtime/bake/config/bake_plugins.bind.ts diff --git a/src/runtime/bake/router/js.bind.ts b/src/runtime/bake/router/FrameworkRouter.bind.ts similarity index 100% rename from src/runtime/bake/router/js.bind.ts rename to src/runtime/bake/router/FrameworkRouter.bind.ts diff --git a/src/runtime/hw_exports.bind.ts b/src/runtime/hw_exports.bind.ts index 6d4235d3d443..6d3b2197da4b 100644 --- a/src/runtime/hw_exports.bind.ts +++ b/src/runtime/hw_exports.bind.ts @@ -2,7 +2,7 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::hw_exports" }); -export const drainMicrotasks = b.fn({ args: {}, capability: "js" }); +export const drainMicrotasks = b.fn({ capability: "js" }); /** The reactions to the promise that `bun --print` evaluated to. Both exit the process. */ const reaction = { args: { value: b.any }, capability: "js" } as const; diff --git a/src/runtime/node/node_crypto_binding.bind.ts b/src/runtime/node/node_crypto_binding.bind.ts index 44c738acb907..ad86a0b7c0ae 100644 --- a/src/runtime/node/node_crypto_binding.bind.ts +++ b/src/runtime/node/node_crypto_binding.bind.ts @@ -3,7 +3,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::node::crypto" }); /** Each body validates its own arguments, in Node's order and with Node's errors. */ -const validatesItsArguments = { capability: "js", throws: true } as const; +const js = { capability: "js", throws: true } as const; const argon2Parameters = { message: b.any, @@ -20,36 +20,36 @@ const argon2Parameters = { export const crypto = b.namespace({ pbkdf2: { args: { password: b.any, salt: b.any, iterations: b.any, keylen: b.any, digest: b.any, callback: b.any }, - ...validatesItsArguments, + ...js, }, pbkdf2Sync: { args: { password: b.any, salt: b.any, iterations: b.any, keylen: b.any, digest: b.any }, returns: b.any, - ...validatesItsArguments, + ...js, }, randomInt: { args: { min: b.any, max: b.any, callback: b.any }, returns: b.f64.optional, length: 2, - ...validatesItsArguments, + ...js, }, randomFill: { args: { buf: b.any, offset: b.any, size: b.any, callback: b.any }, - ...validatesItsArguments, + ...js, }, randomFillSync: { args: { buf: b.any, offset: b.any, size: b.any }, returns: b.any, - ...validatesItsArguments, + ...js, }, - randomUUID: { args: { options: b.any }, returns: b.string, ...validatesItsArguments }, + randomUUID: { args: { options: b.any }, returns: b.string, ...js }, randomUUIDv7: { args: { options: b.any }, returns: b.string, rustPath: "crate::node::crypto::random_uuid_v7", - ...validatesItsArguments, + ...js, }, - randomBytes: { args: { size: b.any, callback: b.any }, returns: b.any, ...validatesItsArguments }, + randomBytes: { args: { size: b.any, callback: b.any }, returns: b.any, ...js }, secureHeapUsed: {}, getFips: { returns: b.i32 }, setFips: { length: 1 }, @@ -57,13 +57,13 @@ export const crypto = b.namespace({ getHashes: { returns: b.any, capability: "heap", throws: true }, scrypt: { args: { password: b.any, salt: b.any, keylen: b.any, options: b.any, callback: b.any }, - ...validatesItsArguments, + ...js, }, scryptSync: { args: { password: b.any, salt: b.any, keylen: b.any, options: b.any }, returns: b.any, - ...validatesItsArguments, + ...js, }, - argon2: { args: { ...argon2Parameters, callback: b.any }, ...validatesItsArguments }, - argon2Sync: { args: argon2Parameters, returns: b.any, ...validatesItsArguments }, + argon2: { args: { ...argon2Parameters, callback: b.any }, ...js }, + argon2Sync: { args: argon2Parameters, returns: b.any, ...js }, }); diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index a0e83e2f50c1..38bea48c3dc2 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -7,12 +7,15 @@ use core::ffi::{c_char, c_void}; use ::argon2 as rust_argon2; use bun_boringssl as boringssl; use bun_collections::CaseInsensitiveAsciiStringArrayHashMap; +use bun_core::String as BunString; use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope}; use bun_jsc::{ - self as jsc, ArrayBuffer, JSGlobalObject, JSValue, Job, JobContext, JsPtr, JsResult, JsThread, - Protected, Strong, + self as jsc, ArrayBuffer, ErrorCode, JSGlobalObject, JSType, JSValue, Job, JobContext, JsPtr, + JsResult, JsThread, Protected, Strong, UUID, UUID7, }; +use crate::crypto::pbkdf2::{self, PBKDF2}; +use crate::node::util::validators; use crate::node::{Flavor, StringOrBuffer, ThreadIsolated, ThreadIsolatedArg}; // `&JSGlobalObject` is ABI-identical to a non-null pointer; remaining params @@ -199,590 +202,544 @@ extern_crypto_job!(SignJob, "SignJob"); // CryptoJob // ─────────────────────────────────────────────────────────────────────────── -pub(crate) mod random { - use super::*; - - // No `Clone`: `value` is JSC-protected in `init`/unprotected in `deinit`, and - // `InPlace` borrows into that ArrayBuffer. Cloning would alias the protect/unprotect - // pair and the borrowed buffer. `CryptoJob::init` moves the ctx by value. - /// `crypto.randomFill` / `randomBytes` off the JS thread. - enum RandomFillJob { - /// `randomBytes`: the ArrayBuffer was allocated by us and nothing else - /// can observe it yet, so fill its bytes directly (under the job's ticket, - /// which keeps its VM alive). - InPlace { bytes: JsPtr, length: usize }, - /// `randomFill`: the caller's buffer stays untouched until completion; - /// `scratch` (empty, `size` bytes reserved) is filled off-thread and copied in at `offset` on the JS thread. - /// `offset` counts from the start of `memory` if the job has one. - Scratch { - scratch: Vec, - size: usize, - offset: usize, - }, - } +// No `Clone`: `value` is JSC-protected in `init`/unprotected in `deinit`, and +// `InPlace` borrows into that ArrayBuffer. Cloning would alias the protect/unprotect +// pair and the borrowed buffer. `CryptoJob::init` moves the ctx by value. +/// `crypto.randomFill` / `randomBytes` off the JS thread. +enum RandomFillJob { + /// `randomBytes`: the ArrayBuffer was allocated by us and nothing else + /// can observe it yet, so fill its bytes directly (under the job's ticket, + /// which keeps its VM alive). + InPlace { bytes: JsPtr, length: usize }, + /// `randomFill`: the caller's buffer stays untouched until completion; + /// `scratch` (empty, `size` bytes reserved) is filled off-thread and copied in at `offset` on the JS thread. + /// `offset` counts from the start of `memory` if the job has one. + Scratch { + scratch: Vec, + size: usize, + offset: usize, + }, +} - #[derive(bun_jsc::JsAffine)] - struct RandomFillJs { - callback: Strong, - value: Protected, - /// The `WebAssembly.Memory` that `value` is a view of. `grow()` detaches `value`. - memory: Option, - } +#[derive(bun_jsc::JsAffine)] +struct RandomFillJs { + callback: Strong, + value: Protected, + /// The `WebAssembly.Memory` that `value` is a view of. `grow()` detaches `value`. + memory: Option, +} - const MAX_POSSIBLE_LENGTH: usize = { - let a = ArrayBuffer::MAX_SIZE as usize; - let b = i32::MAX as usize; - if a < b { a } else { b } - }; - const MAX_RANGE: i64 = 0xffff_ffff_ffff; - - impl JobContext for RandomFillJob { - type OffThread = Self; - type Js = RandomFillJs; - - fn run( - this: &mut Self, - done: bun_jsc::Completion, - ) -> Option> { - match this { - RandomFillJob::Scratch { scratch, size, .. } => { - let size = *size; - // SAFETY: `rand_bytes` only writes, and fills every byte of the slice it is given. - unsafe { - bun_core::vec::fill_spare(scratch, 0, |spare| { - boringssl::rand_bytes(&mut spare[..size]); - (size, ()) - }) - } - } - RandomFillJob::InPlace { bytes, length } => { - // SAFETY: `bytes` points into the ArrayBuffer `value` keeps alive; - // the ticket keeps the VM (and so that buffer) alive; `length` is - // the buffer's own allocation size. - let slice = unsafe { - core::slice::from_raw_parts_mut( - core::ptr::from_mut(bytes.under_ticket(done.ticket())), - *length, - ) - }; - boringssl::rand_bytes(slice); +const MAX_POSSIBLE_LENGTH: usize = { + let a = ArrayBuffer::MAX_SIZE as usize; + let b = i32::MAX as usize; + if a < b { a } else { b } +}; +const MAX_RANGE: i64 = 0xffff_ffff_ffff; + +impl JobContext for RandomFillJob { + type OffThread = Self; + type Js = RandomFillJs; + + fn run(this: &mut Self, done: bun_jsc::Completion) -> Option> { + match this { + RandomFillJob::Scratch { scratch, size, .. } => { + let size = *size; + // SAFETY: `rand_bytes` only writes, and fills every byte of the slice it is given. + unsafe { + bun_core::vec::fill_spare(scratch, 0, |spare| { + boringssl::rand_bytes(&mut spare[..size]); + (size, ()) + }) } } - Some(done) - } - - fn then(this: Self, js: RandomFillJs, cx: &JsThread<'_>) -> JsResult<()> { - let global = cx.global(); - let mut args = [JSValue::NULL, js.value.value()]; - if let RandomFillJob::Scratch { - scratch, offset, .. - } = this - { - let target = js.memory.as_ref().unwrap_or(&js.value).value(); - if !ArrayBuffer::write_to_live(target, offset, &scratch) { - let error = global.err( - jsc::ErrorCode::CRYPTO_OPERATION_FAILED, - format_args!("The buffer was detached or resized before it was filled"), - ); - args = [error.to_js(), JSValue::UNDEFINED]; - } + RandomFillJob::InPlace { bytes, length } => { + // SAFETY: `bytes` points into the ArrayBuffer `value` keeps alive; + // the ticket keeps the VM (and so that buffer) alive; `length` is + // the buffer's own allocation size. + let slice = unsafe { + core::slice::from_raw_parts_mut( + core::ptr::from_mut(bytes.under_ticket(done.ticket())), + *length, + ) + }; + boringssl::rand_bytes(slice); } - global.bun_vm().event_loop_mut().run_callback( - bun_event_loop::ContextId::NONE, - js.callback.get(), - global, - JSValue::UNDEFINED, - &args, - ); - Ok(()) } + Some(done) } - fn schedule( - cx: &JsThread<'_>, - callback: JSValue, - job: RandomFillJob, - value: JSValue, - memory: Option, - ) { + fn then(this: Self, js: RandomFillJs, cx: &JsThread<'_>) -> JsResult<()> { let global = cx.global(); - Job::::schedule( - cx, - job, - RandomFillJs { - callback: Strong::create(callback.with_async_context_if_needed(global), global), - value: Protected::new(value), - memory: memory.map(Protected::new), - }, + let mut args = [JSValue::NULL, js.value.value()]; + if let RandomFillJob::Scratch { + scratch, offset, .. + } = this + { + let target = js.memory.as_ref().unwrap_or(&js.value).value(); + if !ArrayBuffer::write_to_live(target, offset, &scratch) { + let error = global.err( + jsc::ErrorCode::CRYPTO_OPERATION_FAILED, + format_args!("The buffer was detached or resized before it was filled"), + ); + args = [error.to_js(), JSValue::UNDEFINED]; + } + } + global.bun_vm().event_loop_mut().run_callback( + bun_event_loop::ContextId::NONE, + js.callback.get(), + global, + JSValue::UNDEFINED, + &args, ); + Ok(()) } +} - mod _hostfns { - use super::*; - use crate::node::util::validators; - use bun_core::String as BunString; - use bun_jsc::{JSType, UUID, UUID7}; - - pub(crate) fn random_int<'a>( - cx: &mut JsScope<'a>, - min: JsLocal<'a>, - max: JsLocal<'a>, - callback: JsLocal<'a>, - ) -> JsResult> { - let global = cx.global(); - let [mut min_value, mut max_value, mut callback] = - [min.to_js(), max.to_js(), callback.to_js()]; - - let mut min_specified = true; - if max_value.is_undefined() || max_value.is_callable() { - callback = max_value; - max_value = min_value; - min_value = JSValue::js_number(0.0); - min_specified = false; - } +fn schedule( + cx: &JsThread<'_>, + callback: JSValue, + job: RandomFillJob, + value: JSValue, + memory: Option, +) { + let global = cx.global(); + Job::::schedule( + cx, + job, + RandomFillJs { + callback: Strong::create(callback.with_async_context_if_needed(global), global), + value: Protected::new(value), + memory: memory.map(Protected::new), + }, + ); +} - if !callback.is_undefined() { - let _ = validators::validate_function(global, "callback", callback)?; - } +pub(crate) fn random_int<'a>( + cx: &mut JsScope<'a>, + min: JsLocal<'a>, + max: JsLocal<'a>, + callback: JsLocal<'a>, +) -> JsResult> { + let global = cx.global(); + let [mut min_value, mut max_value, mut callback] = [min.to_js(), max.to_js(), callback.to_js()]; + + let mut min_specified = true; + if max_value.is_undefined() || max_value.is_callable() { + callback = max_value; + max_value = min_value; + min_value = JSValue::js_number(0.0); + min_specified = false; + } - if !min_value.is_safe_integer() { - return Err(global.throw_invalid_argument_type_value2( - b"min", - b"a safe integer", - min_value, - )); - } - if !max_value.is_safe_integer() { - return Err(global.throw_invalid_argument_type_value2( - b"max", - b"a safe integer", - max_value, - )); - } + if !callback.is_undefined() { + let _ = validators::validate_function(global, "callback", callback)?; + } + + if !min_value.is_safe_integer() { + return Err(global.throw_invalid_argument_type_value2( + b"min", + b"a safe integer", + min_value, + )); + } + if !max_value.is_safe_integer() { + return Err(global.throw_invalid_argument_type_value2( + b"max", + b"a safe integer", + max_value, + )); + } + + let min: i64 = min_value.as_number().trunc() as i64; + let max: i64 = max_value.as_number().trunc() as i64; - let min: i64 = min_value.as_number().trunc() as i64; - let max: i64 = max_value.as_number().trunc() as i64; + if max <= min { + return Err(global + .err( + jsc::ErrorCode::OUT_OF_RANGE, + format_args!( + "The value of \"max\" is out of range. It must be greater than the value of \"min\" ({}). Received {}", + min, max + ), + ) + .throw()); + } - if max <= min { - return Err(global + if max - min > MAX_RANGE { + // Node's ERR_OUT_OF_RANGE adds "_" numerical separators to integer + // "Received" values whose magnitude exceeds 2^32 + // (lib/internal/errors.js, addNumericalSeparator). + let received = { + let digits = (max - min).to_string(); + let (sign, digits) = match digits.strip_prefix('-') { + Some(rest) => ("-", rest), + None => ("", digits.as_str()), + }; + let mut out = String::with_capacity(digits.len() + digits.len() / 3 + 1); + out.push_str(sign); + let lead = digits.len() % 3; + for (i, ch) in digits.chars().enumerate() { + if i != 0 && (i + 3 - lead) % 3 == 0 { + out.push('_'); + } + out.push(ch); + } + out + }; + if min_specified { + return Err(global .err( jsc::ErrorCode::OUT_OF_RANGE, format_args!( - "The value of \"max\" is out of range. It must be greater than the value of \"min\" ({}). Received {}", - min, max + "The value of \"max - min\" is out of range. It must be <= {}. Received {}", + MAX_RANGE, received ), ) .throw()); - } - - if max - min > MAX_RANGE { - // Node's ERR_OUT_OF_RANGE adds "_" numerical separators to integer - // "Received" values whose magnitude exceeds 2^32 - // (lib/internal/errors.js, addNumericalSeparator). - let received = { - let digits = (max - min).to_string(); - let (sign, digits) = match digits.strip_prefix('-') { - Some(rest) => ("-", rest), - None => ("", digits.as_str()), - }; - let mut out = String::with_capacity(digits.len() + digits.len() / 3 + 1); - out.push_str(sign); - let lead = digits.len() % 3; - for (i, ch) in digits.chars().enumerate() { - if i != 0 && (i + 3 - lead) % 3 == 0 { - out.push('_'); - } - out.push(ch); - } - out - }; - if min_specified { - return Err(global - .err( - jsc::ErrorCode::OUT_OF_RANGE, - format_args!( - "The value of \"max - min\" is out of range. It must be <= {}. Received {}", - MAX_RANGE, received - ), - ) - .throw()); - } - return Err(global - .err( - jsc::ErrorCode::OUT_OF_RANGE, - format_args!( - "The value of \"max\" is out of range. It must be <= {}. Received {}", - MAX_RANGE, received - ), - ) - .throw()); - } - - // Uniform random in [min, max) via Lemire's nearly-divisionless - // rejection sampling on the VM's entropy cache (`RAND_bytes` costs ~0.5 µs per call). - let res: i64 = { - let range = (max - min) as u64; - debug_assert!(range > 0); - let rare_data = global.bun_vm().as_mut().rare_data(); - let mut buf = [0u8; 8]; - let x = loop { - buf.copy_from_slice(rare_data.entropy_slice(8)); - let x = u64::from_ne_bytes(buf); - let m = (x as u128).wrapping_mul(range as u128); - let l = m as u64; - if l < range { - let t = range.wrapping_neg() % range; - if l >= t { - break (m >> 64) as u64; - } - // else: rejected, loop again - } else { - break (m >> 64) as u64; - } - }; - min.wrapping_add(x as i64) - }; - - if !callback.is_undefined() { - callback.call_next_tick_2( - global, - JSValue::UNDEFINED, - JSValue::js_number(res as f64), - )?; - return Ok(None); - } - - Ok(Some(res as f64)) } + return Err(global + .err( + jsc::ErrorCode::OUT_OF_RANGE, + format_args!( + "The value of \"max\" is out of range. It must be <= {}. Received {}", + MAX_RANGE, received + ), + ) + .throw()); + } - pub(crate) fn random_uuid<'a>( - cx: &mut JsScope<'a>, - options: JsLocal<'a>, - ) -> JsResult { - let global = cx.global(); - let options = options.to_js(); - - let mut disable_entropy_cache = false; - if !options.is_undefined() { - validators::validate_object( - global, - options, - format_args!("options"), - Default::default(), - )?; - if let Some(disable_entropy_cache_value) = - options.get(global, "disableEntropyCache")? - { - disable_entropy_cache = validators::validate_boolean( - global, - disable_entropy_cache_value, - format_args!("options.disableEntropyCache"), - )?; + // Uniform random in [min, max) via Lemire's nearly-divisionless + // rejection sampling on the VM's entropy cache (`RAND_bytes` costs ~0.5 µs per call). + let res: i64 = { + let range = (max - min) as u64; + debug_assert!(range > 0); + let rare_data = global.bun_vm().as_mut().rare_data(); + let mut buf = [0u8; 8]; + let x = loop { + buf.copy_from_slice(rare_data.entropy_slice(8)); + let x = u64::from_ne_bytes(buf); + let m = (x as u128).wrapping_mul(range as u128); + let l = m as u64; + if l < range { + let t = range.wrapping_neg() % range; + if l >= t { + break (m >> 64) as u64; } + // else: rejected, loop again + } else { + break (m >> 64) as u64; } + }; + min.wrapping_add(x as i64) + }; - let (str, bytes) = BunString::create_uninitialized_latin1(36); + if !callback.is_undefined() { + callback.call_next_tick_2(global, JSValue::UNDEFINED, JSValue::js_number(res as f64))?; + return Ok(None); + } - let uuid = if disable_entropy_cache { - UUID::init() - } else { - global.bun_vm().as_mut().rare_data().next_uuid() - }; + Ok(Some(res as f64)) +} - uuid.print( - (&mut bytes[..36]) - .try_into() - .expect("infallible: size matches"), - ); - Ok(str) - } +pub(crate) fn random_uuid<'a>(cx: &mut JsScope<'a>, options: JsLocal<'a>) -> JsResult { + let global = cx.global(); + let options = options.to_js(); - pub(crate) fn random_uuid_v7<'a>( - cx: &mut JsScope<'a>, - options: JsLocal<'a>, - ) -> JsResult { - let global = cx.global(); - let options = options.to_js(); - - let mut disable_entropy_cache = false; - if !options.is_undefined() { - validators::validate_object( - global, - options, - format_args!("options"), - Default::default(), - )?; - if let Some(disable_entropy_cache_value) = - options.get(global, "disableEntropyCache")? - { - disable_entropy_cache = validators::validate_boolean( - global, - disable_entropy_cache_value, - format_args!("options.disableEntropyCache"), - )?; - } - } + let mut disable_entropy_cache = false; + if !options.is_undefined() { + validators::validate_object(global, options, format_args!("options"), Default::default())?; + if let Some(disable_entropy_cache_value) = options.get(global, "disableEntropyCache")? { + disable_entropy_cache = validators::validate_boolean( + global, + disable_entropy_cache_value, + format_args!("options.disableEntropyCache"), + )?; + } + } - // jsDateNow() is exactly what JS Date.now() returns, so the embedded - // timestamp is never behind a Date.now() sample taken by the caller. - let now_ms = global.js_date_now().max(0.0) as u64; - let mut entropy = [0u8; 10]; - if disable_entropy_cache { - boringssl::rand_bytes(&mut entropy); - } else { - entropy - .copy_from_slice(&global.bun_vm().as_mut().rare_data().entropy_slice(10)[..10]); - } - let uuid = UUID7::init(now_ms, entropy, bun_jsc::uuid::TimestampSource::Clock); + let (str, bytes) = BunString::create_uninitialized_latin1(36); - let (str, bytes) = BunString::create_uninitialized_latin1(36); - uuid.print( - (&mut bytes[..36]) - .try_into() - .expect("infallible: size matches"), - ); - Ok(str) - } + let uuid = if disable_entropy_cache { + UUID::init() + } else { + global.bun_vm().as_mut().rare_data().next_uuid() + }; - fn assert_offset( - global: &JSGlobalObject, - offset_value: JSValue, - element_size: u8, - length: usize, - ) -> JsResult { - if !offset_value.is_number() { - return Err(global.throw_invalid_argument_type_value( - b"offset", - b"number", - offset_value, - )); - } - let offset = offset_value.as_number() * (element_size as f64); - - let max_length = length.min(MAX_POSSIBLE_LENGTH); - if offset.is_nan() || offset > (max_length as f64) || offset < 0.0 { - // Node spells this range with "&&" (lib/internal/crypto/random.js assertOffset). - let range = format!(">= 0 && <= {max_length}"); - return Err(global.throw_range_error( - offset, - jsc::RangeErrorOptions { - field_name: b"offset", - msg: range.as_bytes(), - ..Default::default() - }, - )); - } + uuid.print( + (&mut bytes[..36]) + .try_into() + .expect("infallible: size matches"), + ); + Ok(str) +} - Ok(offset as u32) +pub(crate) fn random_uuid_v7<'a>( + cx: &mut JsScope<'a>, + options: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let options = options.to_js(); + + let mut disable_entropy_cache = false; + if !options.is_undefined() { + validators::validate_object(global, options, format_args!("options"), Default::default())?; + if let Some(disable_entropy_cache_value) = options.get(global, "disableEntropyCache")? { + disable_entropy_cache = validators::validate_boolean( + global, + disable_entropy_cache_value, + format_args!("options.disableEntropyCache"), + )?; } + } - fn assert_size( - global: &JSGlobalObject, - size_value: JSValue, - element_size: u8, - offset: u32, - length: usize, - ) -> JsResult { - let mut size = validators::validate_number(global, size_value, "size", None, None)?; - size *= element_size as f64; - - if size.is_nan() || size > (MAX_POSSIBLE_LENGTH as f64) || size < 0.0 { - // Node spells this range with "&&" (lib/internal/crypto/random.js assertSize). - let range = format!(">= 0 && <= {MAX_POSSIBLE_LENGTH}"); - return Err(global.throw_range_error( - size, - jsc::RangeErrorOptions { - field_name: b"size", - msg: range.as_bytes(), - ..Default::default() - }, - )); - } + // jsDateNow() is exactly what JS Date.now() returns, so the embedded + // timestamp is never behind a Date.now() sample taken by the caller. + let now_ms = global.js_date_now().max(0.0) as u64; + let mut entropy = [0u8; 10]; + if disable_entropy_cache { + boringssl::rand_bytes(&mut entropy); + } else { + entropy.copy_from_slice(&global.bun_vm().as_mut().rare_data().entropy_slice(10)[..10]); + } + let uuid = UUID7::init(now_ms, entropy, bun_jsc::uuid::TimestampSource::Clock); - if size + (offset as f64) > (length as f64) { - return Err(global.throw_range_error( - size + (offset as f64), - jsc::RangeErrorOptions { - field_name: b"size + offset", - max: i64::try_from(length).expect("int cast"), - ..Default::default() - }, - )); - } + let (str, bytes) = BunString::create_uninitialized_latin1(36); + uuid.print( + (&mut bytes[..36]) + .try_into() + .expect("infallible: size matches"), + ); + Ok(str) +} - Ok(size as u32) - } +fn assert_offset( + global: &JSGlobalObject, + offset_value: JSValue, + element_size: u8, + length: usize, +) -> JsResult { + if !offset_value.is_number() { + return Err(global.throw_invalid_argument_type_value(b"offset", b"number", offset_value)); + } + let offset = offset_value.as_number() * (element_size as f64); + + let max_length = length.min(MAX_POSSIBLE_LENGTH); + if offset.is_nan() || offset > (max_length as f64) || offset < 0.0 { + // Node spells this range with "&&" (lib/internal/crypto/random.js assertOffset). + let range = format!(">= 0 && <= {max_length}"); + return Err(global.throw_range_error( + offset, + jsc::RangeErrorOptions { + field_name: b"offset", + msg: range.as_bytes(), + ..Default::default() + }, + )); + } - pub(crate) fn random_bytes<'a>( - cx: &mut JsScope<'a>, - size: JsLocal<'a>, - callback: JsLocal<'a>, - ) -> JsResult { - let global = cx.global(); - let [size_value, callback] = [size.to_js(), callback.to_js()]; + Ok(offset as u32) +} - let size = assert_size(global, size_value, 1, 0, MAX_POSSIBLE_LENGTH + 1)?; +fn assert_size( + global: &JSGlobalObject, + size_value: JSValue, + element_size: u8, + offset: u32, + length: usize, +) -> JsResult { + let mut size = validators::validate_number(global, size_value, "size", None, None)?; + size *= element_size as f64; + + if size.is_nan() || size > (MAX_POSSIBLE_LENGTH as f64) || size < 0.0 { + // Node spells this range with "&&" (lib/internal/crypto/random.js assertSize). + let range = format!(">= 0 && <= {MAX_POSSIBLE_LENGTH}"); + return Err(global.throw_range_error( + size, + jsc::RangeErrorOptions { + field_name: b"size", + msg: range.as_bytes(), + ..Default::default() + }, + )); + } - if !callback.is_undefined() { - let _ = validators::validate_function(global, "callback", callback)?; - } + if size + (offset as f64) > (length as f64) { + return Err(global.throw_range_error( + size + (offset as f64), + jsc::RangeErrorOptions { + field_name: b"size + offset", + max: i64::try_from(length).expect("int cast"), + ..Default::default() + }, + )); + } - let (result, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, size)?; + Ok(size as u32) +} - if callback.is_undefined() { - // sync - boringssl::rand_bytes(bytes); - return Ok(result); - } +pub(crate) fn random_bytes<'a>( + cx: &mut JsScope<'a>, + size: JsLocal<'a>, + callback: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let [size_value, callback] = [size.to_js(), callback.to_js()]; - schedule( - &cx.js_thread(), - callback, - RandomFillJob::InPlace { - // SAFETY: `bytes` is `result`'s backing store, kept alive by the job's - // Js side; a slice's data pointer is non-null even when empty. - bytes: unsafe { - JsPtr::new(core::ptr::NonNull::new_unchecked(bytes.as_mut_ptr())) - }, - length: size as usize, - }, - result, - None, - ); + let size = assert_size(global, size_value, 1, 0, MAX_POSSIBLE_LENGTH + 1)?; - Ok(JSValue::UNDEFINED) - } + if !callback.is_undefined() { + let _ = validators::validate_function(global, "callback", callback)?; + } - pub(crate) fn random_fill_sync<'a>( - cx: &mut JsScope<'a>, - buf: JsLocal<'a>, - offset: JsLocal<'a>, - size: JsLocal<'a>, - ) -> JsResult { - let global = cx.global(); - let [buf_value, offset_value, size_value] = [buf.to_js(), offset.to_js(), size.to_js()]; + let (result, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, size)?; - let Some(mut buf) = buf_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - b"buf", - b"ArrayBuffer or ArrayBufferView", - buf_value, - )); - }; + if callback.is_undefined() { + // sync + boringssl::rand_bytes(bytes); + return Ok(result); + } - let element_size = buf.bytes_per_element().unwrap_or(1); + schedule( + &cx.js_thread(), + callback, + RandomFillJob::InPlace { + // SAFETY: `bytes` is `result`'s backing store, kept alive by the job's + // Js side; a slice's data pointer is non-null even when empty. + bytes: unsafe { JsPtr::new(core::ptr::NonNull::new_unchecked(bytes.as_mut_ptr())) }, + length: size as usize, + }, + result, + None, + ); - let offset = assert_offset( - global, - if offset_value.is_undefined() { - JSValue::js_number(0.0) - } else { - offset_value - }, - element_size, - buf.byte_len, - )?; + Ok(JSValue::UNDEFINED) +} - // `size` is usize (`buf.byte_len - offset`, both usize). The - // `assert_size` branch is bounded by `MAX_POSSIBLE_LENGTH` (≤ i32::MAX) so widening - // its `u32` result is lossless; the default branch must NOT truncate to `u32` — - // a >4 GiB ArrayBuffer remainder would silently fill only `(n % 2^32)` bytes. - let size: usize = if size_value.is_undefined() { - buf.byte_len - offset as usize - } else { - assert_size(global, size_value, element_size, offset, buf.byte_len)? as usize - }; +pub(crate) fn random_fill_sync<'a>( + cx: &mut JsScope<'a>, + buf: JsLocal<'a>, + offset: JsLocal<'a>, + size: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let [buf_value, offset_value, size_value] = [buf.to_js(), offset.to_js(), size.to_js()]; + + let Some(mut buf) = buf_value.as_array_buffer(global) else { + return Err(global.throw_invalid_argument_type_value( + b"buf", + b"ArrayBuffer or ArrayBufferView", + buf_value, + )); + }; - if size == 0 { - return Ok(buf_value); - } + let element_size = buf.bytes_per_element().unwrap_or(1); + + let offset = assert_offset( + global, + if offset_value.is_undefined() { + JSValue::js_number(0.0) + } else { + offset_value + }, + element_size, + buf.byte_len, + )?; + + // `size` is usize (`buf.byte_len - offset`, both usize). The + // `assert_size` branch is bounded by `MAX_POSSIBLE_LENGTH` (≤ i32::MAX) so widening + // its `u32` result is lossless; the default branch must NOT truncate to `u32` — + // a >4 GiB ArrayBuffer remainder would silently fill only `(n % 2^32)` bytes. + let size: usize = if size_value.is_undefined() { + buf.byte_len - offset as usize + } else { + assert_size(global, size_value, element_size, offset, buf.byte_len)? as usize + }; - boringssl::rand_bytes(&mut buf.slice_mut()[offset as usize..][..size]); + if size == 0 { + return Ok(buf_value); + } - Ok(buf_value) - } + boringssl::rand_bytes(&mut buf.slice_mut()[offset as usize..][..size]); - pub(crate) fn random_fill<'a>( - cx: &mut JsScope<'a>, - buf: JsLocal<'a>, - offset: JsLocal<'a>, - size: JsLocal<'a>, - callback: JsLocal<'a>, - ) -> JsResult<()> { - let global = cx.global(); - let [buf_value, offset_value, mut size_value, mut callback] = - [buf.to_js(), offset.to_js(), size.to_js(), callback.to_js()]; - - let Some(buf) = buf_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - b"buf", - b"ArrayBuffer or ArrayBufferView", - buf_value, - )); - }; + Ok(buf_value) +} - let element_size = buf.bytes_per_element().unwrap_or(1); - - let offset: u32; - if offset_value.is_callable() { - callback = offset_value; - offset = - assert_offset(global, JSValue::js_number(0.0), element_size, buf.byte_len)?; - size_value = JSValue::js_number(buf.len as f64); - } else if size_value.is_callable() { - callback = size_value; - offset = assert_offset(global, offset_value, element_size, buf.byte_len)?; - // `offset` is a byte offset (already scaled by element_size) but `buf.len` - // is an element count, so `buf.len - offset` would mix units and can - // underflow. Defer to the `buf.byte_len - offset` default below instead. - size_value = JSValue::UNDEFINED; - } else { - let _ = validators::validate_function(global, "callback", callback)?; - offset = assert_offset(global, offset_value, element_size, buf.byte_len)?; - } +pub(crate) fn random_fill<'a>( + cx: &mut JsScope<'a>, + buf: JsLocal<'a>, + offset: JsLocal<'a>, + size: JsLocal<'a>, + callback: JsLocal<'a>, +) -> JsResult<()> { + let global = cx.global(); + let [buf_value, offset_value, mut size_value, mut callback] = + [buf.to_js(), offset.to_js(), size.to_js(), callback.to_js()]; + + let Some(buf) = buf_value.as_array_buffer(global) else { + return Err(global.throw_invalid_argument_type_value( + b"buf", + b"ArrayBuffer or ArrayBufferView", + buf_value, + )); + }; - // `size` is usize (`buf.byte_len - offset`, both usize). The - // `assert_size` branch is bounded by `MAX_POSSIBLE_LENGTH` (≤ i32::MAX) so widening - // its `u32` result is lossless; the default branch must NOT truncate to `u32` — - // a >4 GiB ArrayBuffer remainder would silently fill only `(n % 2^32)` bytes. - let size: usize = if size_value.is_undefined() { - buf.byte_len - offset as usize - } else { - assert_size(global, size_value, element_size, offset, buf.byte_len)? as usize - }; + let element_size = buf.bytes_per_element().unwrap_or(1); + + let offset: u32; + if offset_value.is_callable() { + callback = offset_value; + offset = assert_offset(global, JSValue::js_number(0.0), element_size, buf.byte_len)?; + size_value = JSValue::js_number(buf.len as f64); + } else if size_value.is_callable() { + callback = size_value; + offset = assert_offset(global, offset_value, element_size, buf.byte_len)?; + // `offset` is a byte offset (already scaled by element_size) but `buf.len` + // is an element count, so `buf.len - offset` would mix units and can + // underflow. Defer to the `buf.byte_len - offset` default below instead. + size_value = JSValue::UNDEFINED; + } else { + let _ = validators::validate_function(global, "callback", callback)?; + offset = assert_offset(global, offset_value, element_size, buf.byte_len)?; + } - if size == 0 { - let _ = callback.call(global, JSValue::UNDEFINED, &[JSValue::NULL, buf_value])?; - return Ok(()); - } + // `size` is usize (`buf.byte_len - offset`, both usize). The + // `assert_size` branch is bounded by `MAX_POSSIBLE_LENGTH` (≤ i32::MAX) so widening + // its `u32` result is lossless; the default branch must NOT truncate to `u32` — + // a >4 GiB ArrayBuffer remainder would silently fill only `(n % 2^32)` bytes. + let size: usize = if size_value.is_undefined() { + buf.byte_len - offset as usize + } else { + assert_size(global, size_value, element_size, offset, buf.byte_len)? as usize + }; - // `vec![0u8; size]` aborts the process on OOM. The 3-arg overload - // `randomFill(buf, offset, cb)` defaults `size` to the full - // remaining buffer length, which can exceed allocator limits for a - // multi-GiB ArrayBuffer — surface that as a JS error instead. - let mut scratch = Vec::new(); - if scratch.try_reserve_exact(size).is_err() { - return Err(global.throw_out_of_memory()); - } + if size == 0 { + let _ = callback.call(global, JSValue::UNDEFINED, &[JSValue::NULL, buf_value])?; + return Ok(()); + } - let (memory, view_offset) = buf.find_wasm_memory().unzip(); - schedule( - &cx.js_thread(), - callback, - RandomFillJob::Scratch { - scratch, - size, - offset: view_offset.unwrap_or(0) + offset as usize, - }, - buf_value, - memory, - ); + // `vec![0u8; size]` aborts the process on OOM. The 3-arg overload + // `randomFill(buf, offset, cb)` defaults `size` to the full + // remaining buffer length, which can exceed allocator limits for a + // multi-GiB ArrayBuffer — surface that as a JS error instead. + let mut scratch = Vec::new(); + if scratch.try_reserve_exact(size).is_err() { + return Err(global.throw_out_of_memory()); + } - Ok(()) - } - } // mod _hostfns + let (memory, view_offset) = buf.find_wasm_memory().unzip(); + schedule( + &cx.js_thread(), + callback, + RandomFillJob::Scratch { + scratch, + size, + offset: view_offset.unwrap_or(0) + offset as usize, + }, + buf_value, + memory, + ); - pub(crate) use _hostfns::*; + Ok(()) } // ─────────────────────────────────────────────────────────────────────────── @@ -822,790 +779,764 @@ pub(crate) struct Argon2 { failed: bool, } -mod _impl { - use super::*; - use crate::node::util::validators; - use bun_jsc::{ErrorCode, JSType}; - - use crate::crypto::pbkdf2::{self, PBKDF2}; - - impl Scrypt { - /// The return type cannot vary on the const-generic bool, so this always - /// returns `(Self, JSValue)`; the sync caller ignores the second element. - fn from_js( - global: &JSGlobalObject, - password_value: JSValue, - salt_value: JSValue, - keylen_value: JSValue, - options_arg: JSValue, - callback_arg: JSValue, - ) -> JsResult<(Self, JSValue)> { - let mut maybe_options_value: Option = Some(options_arg); - let mut callback = callback_arg; - - if IS_ASYNC { - if callback.is_undefined() { - callback = maybe_options_value.unwrap(); - maybe_options_value = None; - } +impl Scrypt { + /// The return type cannot vary on the const-generic bool, so this always + /// returns `(Self, JSValue)`; the sync caller ignores the second element. + fn from_js( + global: &JSGlobalObject, + password_value: JSValue, + salt_value: JSValue, + keylen_value: JSValue, + options_arg: JSValue, + callback_arg: JSValue, + ) -> JsResult<(Self, JSValue)> { + let mut maybe_options_value: Option = Some(options_arg); + let mut callback = callback_arg; + + if IS_ASYNC { + if callback.is_undefined() { + callback = maybe_options_value.unwrap(); + maybe_options_value = None; } + } - let Some(password) = StringOrBuffer::from_js(global, password_value)? else { - return Err(global.throw_invalid_argument_type_value( - b"password", - b"string, ArrayBuffer, Buffer, TypedArray, or DataView", - password_value, - )); - }; + let Some(password) = StringOrBuffer::from_js(global, password_value)? else { + return Err(global.throw_invalid_argument_type_value( + b"password", + b"string, ArrayBuffer, Buffer, TypedArray, or DataView", + password_value, + )); + }; - let Some(salt) = StringOrBuffer::from_js(global, salt_value)? else { - return Err(global.throw_invalid_argument_type_value( - b"salt", - b"string, ArrayBuffer, Buffer, TypedArray, or DataView", - salt_value, - )); - }; + let Some(salt) = StringOrBuffer::from_js(global, salt_value)? else { + return Err(global.throw_invalid_argument_type_value( + b"salt", + b"string, ArrayBuffer, Buffer, TypedArray, or DataView", + salt_value, + )); + }; - let keylen = validators::validate_int32( - global, - keylen_value, - format_args!("keylen"), - Some(0), - None, - )?; + let keylen = validators::validate_int32( + global, + keylen_value, + format_args!("keylen"), + Some(0), + None, + )?; - let mut n: Option = None; - let mut r: Option = None; - let mut p: Option = None; - let mut maxmem: Option = None; - - if let Some(options_value) = maybe_options_value { - if let Some(options) = options_value.get_object() { - // `get_object` returned non-null; the JSObject is rooted by - // `options_value` (kept alive on the stack for this scope). - // `JSObject` is an `opaque_ffi!` ZST handle; `opaque_ref` is the - // centralised non-null-ZST deref proof. - let options = bun_jsc::JSObject::opaque_ref(options); - if let Some(n_value) = options.get(global, "N")? { - n = Some(validators::validate_uint32( - global, - n_value, - format_args!("N"), - false, - )?); - } + let mut n: Option = None; + let mut r: Option = None; + let mut p: Option = None; + let mut maxmem: Option = None; + + if let Some(options_value) = maybe_options_value { + if let Some(options) = options_value.get_object() { + // `get_object` returned non-null; the JSObject is rooted by + // `options_value` (kept alive on the stack for this scope). + // `JSObject` is an `opaque_ffi!` ZST handle; `opaque_ref` is the + // centralised non-null-ZST deref proof. + let options = bun_jsc::JSObject::opaque_ref(options); + if let Some(n_value) = options.get(global, "N")? { + n = Some(validators::validate_uint32( + global, + n_value, + format_args!("N"), + false, + )?); + } - if let Some(cost_value) = options.get(global, "cost")? { - if n.is_some() { - return Err(global.throw_incompatible_option_pair(b"N", b"cost")); - } - n = Some(validators::validate_uint32( - global, - cost_value, - format_args!("cost"), - false, - )?); + if let Some(cost_value) = options.get(global, "cost")? { + if n.is_some() { + return Err(global.throw_incompatible_option_pair(b"N", b"cost")); } + n = Some(validators::validate_uint32( + global, + cost_value, + format_args!("cost"), + false, + )?); + } - if let Some(r_value) = options.get(global, "r")? { - r = Some(validators::validate_uint32( - global, - r_value, - format_args!("r"), - false, - )?); - } + if let Some(r_value) = options.get(global, "r")? { + r = Some(validators::validate_uint32( + global, + r_value, + format_args!("r"), + false, + )?); + } - if let Some(blocksize_value) = options.get(global, "blockSize")? { - if r.is_some() { - return Err(global.throw_incompatible_option_pair(b"r", b"blockSize")); - } - r = Some(validators::validate_uint32( - global, - blocksize_value, - format_args!("blockSize"), - false, - )?); + if let Some(blocksize_value) = options.get(global, "blockSize")? { + if r.is_some() { + return Err(global.throw_incompatible_option_pair(b"r", b"blockSize")); } + r = Some(validators::validate_uint32( + global, + blocksize_value, + format_args!("blockSize"), + false, + )?); + } - if let Some(p_value) = options.get(global, "p")? { - p = Some(validators::validate_uint32( - global, - p_value, - format_args!("p"), - false, - )?); - } + if let Some(p_value) = options.get(global, "p")? { + p = Some(validators::validate_uint32( + global, + p_value, + format_args!("p"), + false, + )?); + } - if let Some(parallelization_value) = options.get(global, "parallelization")? { - if p.is_some() { - return Err( - global.throw_incompatible_option_pair(b"p", b"parallelization") - ); - } - p = Some(validators::validate_uint32( - global, - parallelization_value, - format_args!("parallelization"), - false, - )?); + if let Some(parallelization_value) = options.get(global, "parallelization")? { + if p.is_some() { + return Err(global.throw_incompatible_option_pair(b"p", b"parallelization")); } + p = Some(validators::validate_uint32( + global, + parallelization_value, + format_args!("parallelization"), + false, + )?); + } - if let Some(maxmem_value) = options.get(global, "maxmem")? { - maxmem = Some(validators::validate_integer( - global, - maxmem_value, - "maxmem", - Some(0), - None, - )?); - } + if let Some(maxmem_value) = options.get(global, "maxmem")? { + maxmem = Some(validators::validate_integer( + global, + maxmem_value, + "maxmem", + Some(0), + None, + )?); } } + } - const N_DEFAULT: u32 = 16384; - const R_DEFAULT: u32 = 8; - const P_DEFAULT: u32 = 1; - const MAXMEM_DEFAULT: i64 = 33554432; - - if n.is_none() || n.unwrap() == 0 { - n = Some(N_DEFAULT); - } - if r.is_none() || r.unwrap() == 0 { - r = Some(R_DEFAULT); - } - if p.is_none() || p.unwrap() == 0 { - p = Some(P_DEFAULT); - } - if maxmem.is_none() || maxmem.unwrap() == 0 { - maxmem = Some(MAXMEM_DEFAULT); - } + const N_DEFAULT: u32 = 16384; + const R_DEFAULT: u32 = 8; + const P_DEFAULT: u32 = 1; + const MAXMEM_DEFAULT: i64 = 33554432; - let mut ctx = Scrypt { - password, - salt, - n: n.unwrap(), - r: r.unwrap(), - p: p.unwrap(), - maxmem: u64::try_from(maxmem.unwrap()).expect("int cast"), - keylen: u32::try_from(keylen).expect("int cast"), - }; - if IS_ASYNC { - let _ = validators::validate_function(global, "callback", callback)?; - } + if n.is_none() || n.unwrap() == 0 { + n = Some(N_DEFAULT); + } + if r.is_none() || r.unwrap() == 0 { + r = Some(R_DEFAULT); + } + if p.is_none() || p.unwrap() == 0 { + p = Some(P_DEFAULT); + } + if maxmem.is_none() || maxmem.unwrap() == 0 { + maxmem = Some(MAXMEM_DEFAULT); + } - ctx.check_scrypt_params(global)?; + let mut ctx = Scrypt { + password, + salt, + n: n.unwrap(), + r: r.unwrap(), + p: p.unwrap(), + maxmem: u64::try_from(maxmem.unwrap()).expect("int cast"), + keylen: u32::try_from(keylen).expect("int cast"), + }; + if IS_ASYNC { + let _ = validators::validate_function(global, "callback", callback)?; + } - // An option getter can detach or resize the buffers, so read them again. - for input in [&mut ctx.password, &mut ctx.salt] { - if IS_ASYNC { - input.make_thread_isolated_copy(global)?; - } else if let StringOrBuffer::Buffer(buffer) = input { - buffer.buffer = ArrayBuffer::from_typed_array(global, buffer.buffer.value); - } - } + ctx.check_scrypt_params(global)?; + // An option getter can detach or resize the buffers, so read them again. + for input in [&mut ctx.password, &mut ctx.salt] { if IS_ASYNC { - return Ok((ctx, callback)); + input.make_thread_isolated_copy(global)?; + } else if let StringOrBuffer::Buffer(buffer) = input { + buffer.buffer = ArrayBuffer::from_typed_array(global, buffer.buffer.value); } - Ok((ctx, JSValue::UNDEFINED)) } - /// `from_js::` for the work-pool job, with its callback. - fn from_js_async( - global: &JSGlobalObject, - password: JSValue, - salt: JSValue, - keylen: JSValue, - options: JSValue, - callback: JSValue, - ) -> JsResult<(ThreadIsolated, JSValue)> { - let (ctx, callback) = - Self::from_js::(global, password, salt, keylen, options, callback)?; - // SAFETY: `from_js::` copied the buffers and thread-isolated the strings. - Ok((unsafe { ThreadIsolated::new(ctx) }, callback)) + if IS_ASYNC { + return Ok((ctx, callback)); } + Ok((ctx, JSValue::UNDEFINED)) + } - fn check_scrypt_params(&self, global: &JSGlobalObject) -> JsResult<()> { - let n = self.n; - let r = self.r; - let p = self.p; - let maxmem = self.maxmem; - // SAFETY: all pointer args are null with len 0; numeric args are plain values. - if unsafe { - boringssl::c::EVP_PBE_validate_scrypt_params( - core::ptr::null(), - 0, - core::ptr::null(), - 0, - u64::from(n), - u64::from(r), - u64::from(p), - maxmem as usize, - core::ptr::null_mut(), - 0, - ) - } == 0 - { - return Err(global.throw_invalid_scrypt_params()); - } - Ok(()) + /// `from_js::` for the work-pool job, with its callback. + fn from_js_async( + global: &JSGlobalObject, + password: JSValue, + salt: JSValue, + keylen: JSValue, + options: JSValue, + callback: JSValue, + ) -> JsResult<(ThreadIsolated, JSValue)> { + let (ctx, callback) = + Self::from_js::(global, password, salt, keylen, options, callback)?; + // SAFETY: `from_js::` copied the buffers and thread-isolated the strings. + Ok((unsafe { ThreadIsolated::new(ctx) }, callback)) + } + + fn check_scrypt_params(&self, global: &JSGlobalObject) -> JsResult<()> { + let n = self.n; + let r = self.r; + let p = self.p; + let maxmem = self.maxmem; + // SAFETY: all pointer args are null with len 0; numeric args are plain values. + if unsafe { + boringssl::c::EVP_PBE_validate_scrypt_params( + core::ptr::null(), + 0, + core::ptr::null(), + 0, + u64::from(n), + u64::from(r), + u64::from(p), + maxmem as usize, + core::ptr::null_mut(), + 0, + ) + } == 0 + { + return Err(global.throw_invalid_scrypt_params()); } + Ok(()) + } - /// `Some(err)` on failure (`0` when there is no BoringSSL error code). - fn run_task_impl(&self, key: &mut [u8]) -> Option { - let password = self.password.slice(); - let salt = self.salt.slice(); + /// `Some(err)` on failure (`0` when there is no BoringSSL error code). + fn run_task_impl(&self, key: &mut [u8]) -> Option { + let password = self.password.slice(); + let salt = self.salt.slice(); - if key.is_empty() { - // result will be an empty buffer - return None; - } + if key.is_empty() { + // result will be an empty buffer + return None; + } - if password.len() > i32::MAX as usize || salt.len() > i32::MAX as usize { - return Some(0); - } + if password.len() > i32::MAX as usize || salt.len() > i32::MAX as usize { + return Some(0); + } - // SAFETY: password/salt/key are valid slices for the given lengths. - let res = unsafe { - boringssl::c::EVP_PBE_scrypt( - password.as_ptr(), - password.len(), - salt.as_ptr(), - salt.len(), - u64::from(self.n), - u64::from(self.r), - u64::from(self.p), - self.maxmem as usize, - key.as_mut_ptr(), - key.len(), - ) - }; + // SAFETY: password/salt/key are valid slices for the given lengths. + let res = unsafe { + boringssl::c::EVP_PBE_scrypt( + password.as_ptr(), + password.len(), + salt.as_ptr(), + salt.len(), + u64::from(self.n), + u64::from(self.r), + u64::from(self.p), + self.maxmem as usize, + key.as_mut_ptr(), + key.len(), + ) + }; - if res == 0 { - return Some(boringssl::c::ERR_peek_last_error()); - } - None + if res == 0 { + return Some(boringssl::c::ERR_peek_last_error()); } + None } +} - /// `crypto.scrypt` off the JS thread: derives straight into the result - /// ArrayBuffer's bytes under the job's ticket, which keeps their VM alive. - pub(crate) struct ScryptJob { - params: ThreadIsolated, - result: JsPtr<[u8]>, - err: Option, - } +/// `crypto.scrypt` off the JS thread: derives straight into the result +/// ArrayBuffer's bytes under the job's ticket, which keeps their VM alive. +pub(crate) struct ScryptJob { + params: ThreadIsolated, + result: JsPtr<[u8]>, + err: Option, +} - #[derive(bun_jsc::JsAffine)] - pub(crate) struct ScryptJs { - callback: Strong, - buf: Strong, - } +#[derive(bun_jsc::JsAffine)] +pub(crate) struct ScryptJs { + callback: Strong, + buf: Strong, +} - impl JobContext for ScryptJob { - type OffThread = Self; - type Js = ScryptJs; - - fn run( - this: &mut Self, - done: bun_jsc::Completion, - ) -> Option> { - // SAFETY: `result` is `buf`'s backing store (kept by the Js side); VM alive under the ticket. - let key = unsafe { this.result.under_ticket(done.ticket()) }; - this.err = this.params.run_task_impl(key); - Some(done) - } +impl JobContext for ScryptJob { + type OffThread = Self; + type Js = ScryptJs; - fn then(this: Self, js: ScryptJs, cx: &JsThread<'_>) -> JsResult<()> { - let global = cx.global(); - let event_loop = global.bun_vm().event_loop_mut(); - let callback = js.callback.get(); - - if let Some(err) = this.err { - let exception = if err != 0 { - let mut buf = [0u8; 256]; - // SAFETY: buf is a valid writable buffer of the given length. - unsafe { - boringssl::c::ERR_error_string_n(err, buf.as_mut_ptr().cast(), buf.len()) - }; - // SAFETY: ERR_error_string_n always NUL-terminates within `buf`. - let msg = unsafe { bun_core::ffi::cstr(buf.as_ptr().cast()) }; - global - .err( - ErrorCode::CRYPTO_OPERATION_FAILED, - format_args!("Scrypt failed: {}", bstr::BStr::new(msg.to_bytes())), - ) - .to_js() - } else { - global - .err( - ErrorCode::CRYPTO_OPERATION_FAILED, - format_args!("Scrypt failed"), - ) - .to_js() - }; - event_loop.run_callback( - bun_event_loop::ContextId::NONE, - callback, - global, - JSValue::UNDEFINED, - &[exception], - ); - return Ok(()); - } + fn run(this: &mut Self, done: bun_jsc::Completion) -> Option> { + // SAFETY: `result` is `buf`'s backing store (kept by the Js side); VM alive under the ticket. + let key = unsafe { this.result.under_ticket(done.ticket()) }; + this.err = this.params.run_task_impl(key); + Some(done) + } + fn then(this: Self, js: ScryptJs, cx: &JsThread<'_>) -> JsResult<()> { + let global = cx.global(); + let event_loop = global.bun_vm().event_loop_mut(); + let callback = js.callback.get(); + + if let Some(err) = this.err { + let exception = if err != 0 { + let mut buf = [0u8; 256]; + // SAFETY: buf is a valid writable buffer of the given length. + unsafe { + boringssl::c::ERR_error_string_n(err, buf.as_mut_ptr().cast(), buf.len()) + }; + // SAFETY: ERR_error_string_n always NUL-terminates within `buf`. + let msg = unsafe { bun_core::ffi::cstr(buf.as_ptr().cast()) }; + global + .err( + ErrorCode::CRYPTO_OPERATION_FAILED, + format_args!("Scrypt failed: {}", bstr::BStr::new(msg.to_bytes())), + ) + .to_js() + } else { + global + .err( + ErrorCode::CRYPTO_OPERATION_FAILED, + format_args!("Scrypt failed"), + ) + .to_js() + }; event_loop.run_callback( bun_event_loop::ContextId::NONE, callback, global, JSValue::UNDEFINED, - &[JSValue::UNDEFINED, js.buf.get()], + &[exception], ); - Ok(()) + return Ok(()); } - } - pub(crate) fn pbkdf2<'a>( - cx: &mut JsScope<'a>, - password: JsLocal<'a>, - salt: JsLocal<'a>, - iterations: JsLocal<'a>, - keylen: JsLocal<'a>, - digest: JsLocal<'a>, - callback: JsLocal<'a>, - ) -> JsResult<()> { - let (data, callback) = PBKDF2::from_js_async( - cx.global(), - password.to_js(), - salt.to_js(), - iterations.to_js(), - keylen.to_js(), - digest.to_js(), - callback.to_js(), - )?; - pbkdf2::create_job(&cx.js_thread(), data, callback); + event_loop.run_callback( + bun_event_loop::ContextId::NONE, + callback, + global, + JSValue::UNDEFINED, + &[JSValue::UNDEFINED, js.buf.get()], + ); Ok(()) } +} - pub(crate) fn pbkdf2_sync<'a>( - cx: &mut JsScope<'a>, - password: JsLocal<'a>, - salt: JsLocal<'a>, - iterations: JsLocal<'a>, - keylen: JsLocal<'a>, - digest: JsLocal<'a>, - ) -> JsResult { - let global_this = cx.global(); - // `PBKDF2`'s `StringOrBuffer` fields release on `Drop`, so the local - // just goes out of scope. - let (mut data, _) = PBKDF2::from_js( - global_this, - password.to_js(), - salt.to_js(), - iterations.to_js(), - keylen.to_js(), - digest.to_js(), - JSValue::UNDEFINED, - Flavor::Sync, - )?; - // `create_buffer_from_length` → `JSBuffer__bufferFromLength`, which constructs - // with `JSBufferSubclassStructure` (a Node.js `Buffer`, not a plain Uint8Array/ArrayBuffer). - // `pbkdf2Sync()` MUST return a Buffer — `Buffer.isBuffer(result)` and Buffer-only methods - // (`.toString('hex')`, `.readUInt32BE`, …) depend on it. - let out_arraybuffer = JSValue::create_buffer_from_length(global_this, data.length)?; - let Some(mut output) = out_arraybuffer.as_array_buffer(global_this) else { - return Err(global_this.throw_out_of_memory()); - }; +pub(crate) fn pbkdf2<'a>( + cx: &mut JsScope<'a>, + password: JsLocal<'a>, + salt: JsLocal<'a>, + iterations: JsLocal<'a>, + keylen: JsLocal<'a>, + digest: JsLocal<'a>, + callback: JsLocal<'a>, +) -> JsResult<()> { + let (data, callback) = PBKDF2::from_js_async( + cx.global(), + password.to_js(), + salt.to_js(), + iterations.to_js(), + keylen.to_js(), + digest.to_js(), + callback.to_js(), + )?; + pbkdf2::create_job(&cx.js_thread(), data, callback); + Ok(()) +} - if !data.run(output.slice_mut()) { - boringssl::c::ERR_clear_error(); - let err = global_this.create_error_instance(format_args!("PBKDF2 derivation failed")); - return Err(global_this.throw_value(err)); - } +pub(crate) fn pbkdf2_sync<'a>( + cx: &mut JsScope<'a>, + password: JsLocal<'a>, + salt: JsLocal<'a>, + iterations: JsLocal<'a>, + keylen: JsLocal<'a>, + digest: JsLocal<'a>, +) -> JsResult { + let global_this = cx.global(); + // `PBKDF2`'s `StringOrBuffer` fields release on `Drop`, so the local + // just goes out of scope. + let (mut data, _) = PBKDF2::from_js( + global_this, + password.to_js(), + salt.to_js(), + iterations.to_js(), + keylen.to_js(), + digest.to_js(), + JSValue::UNDEFINED, + Flavor::Sync, + )?; + // `create_buffer_from_length` → `JSBuffer__bufferFromLength`, which constructs + // with `JSBufferSubclassStructure` (a Node.js `Buffer`, not a plain Uint8Array/ArrayBuffer). + // `pbkdf2Sync()` MUST return a Buffer — `Buffer.isBuffer(result)` and Buffer-only methods + // (`.toString('hex')`, `.readUInt32BE`, …) depend on it. + let out_arraybuffer = JSValue::create_buffer_from_length(global_this, data.length)?; + let Some(mut output) = out_arraybuffer.as_array_buffer(global_this) else { + return Err(global_this.throw_out_of_memory()); + }; - Ok(out_arraybuffer) + if !data.run(output.slice_mut()) { + boringssl::c::ERR_clear_error(); + let err = global_this.create_error_instance(format_args!("PBKDF2 derivation failed")); + return Err(global_this.throw_value(err)); } - pub(crate) fn secure_heap_used() {} + Ok(out_arraybuffer) +} - pub(crate) fn get_fips() -> i32 { - 0 - } +pub(crate) fn secure_heap_used() {} - pub(crate) fn set_fips() {} +pub(crate) fn get_fips() -> i32 { + 0 +} - pub(crate) fn set_engine<'a>(heap: &JsHeap<'a>) -> bindgen::Result<'a, ()> { - Err(heap.error_with_code( - ErrorCode::CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, - format_args!("Custom engines not supported by BoringSSL"), - )) - } +pub(crate) fn set_fips() {} - extern "C" fn for_each_hash( - _: *const boringssl::c::EVP_MD, - maybe_from: *const c_char, - _: *const c_char, - ctx: *mut c_void, - ) { - if maybe_from.is_null() { - return; - } - // SAFETY: ctx was `&mut CaseInsensitiveAsciiStringArrayHashMap<()>` cast in `get_hashes`. - let hashes: &mut CaseInsensitiveAsciiStringArrayHashMap<()> = - unsafe { bun_ptr::callback_ctx::>(ctx) }; - // SAFETY: `maybe_from` is non-null (checked above) and points to a NUL-terminated C string - // from BoringSSL's static tables. - let from_bytes = unsafe { bun_core::ffi::cstr(maybe_from) }.to_bytes(); - bun_core::handle_oom(hashes.put(from_bytes, ())); - } +pub(crate) fn set_engine<'a>(heap: &JsHeap<'a>) -> bindgen::Result<'a, ()> { + Err(heap.error_with_code( + ErrorCode::CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED, + format_args!("Custom engines not supported by BoringSSL"), + )) +} - pub(crate) fn get_hashes<'a>(heap: &JsHeap<'a>) -> bindgen::Result<'a, JsLocal<'a>> { - let mut hashes: CaseInsensitiveAsciiStringArrayHashMap<()> = - CaseInsensitiveAsciiStringArrayHashMap::new(); +extern "C" fn for_each_hash( + _: *const boringssl::c::EVP_MD, + maybe_from: *const c_char, + _: *const c_char, + ctx: *mut c_void, +) { + if maybe_from.is_null() { + return; + } + // SAFETY: ctx was `&mut CaseInsensitiveAsciiStringArrayHashMap<()>` cast in `get_hashes`. + let hashes: &mut CaseInsensitiveAsciiStringArrayHashMap<()> = + unsafe { bun_ptr::callback_ctx::>(ctx) }; + // SAFETY: `maybe_from` is non-null (checked above) and points to a NUL-terminated C string + // from BoringSSL's static tables. + let from_bytes = unsafe { bun_core::ffi::cstr(maybe_from) }.to_bytes(); + bun_core::handle_oom(hashes.put(from_bytes, ())); +} - // Perf idea (dylan-conway): cache the names - // SAFETY: `for_each_hash` matches the expected callback signature; `&mut hashes` is valid - // for the duration of the call. - unsafe { - boringssl::c::EVP_MD_do_all_sorted(for_each_hash, (&raw mut hashes).cast::()); - } +pub(crate) fn get_hashes<'a>(heap: &JsHeap<'a>) -> bindgen::Result<'a, JsLocal<'a>> { + let mut hashes: CaseInsensitiveAsciiStringArrayHashMap<()> = + CaseInsensitiveAsciiStringArrayHashMap::new(); - bindgen::to_js::array(heap, hashes.keys(), bindgen::to_js::utf8) + // Perf idea (dylan-conway): cache the names + // SAFETY: `for_each_hash` matches the expected callback signature; `&mut hashes` is valid + // for the duration of the call. + unsafe { + boringssl::c::EVP_MD_do_all_sorted(for_each_hash, (&raw mut hashes).cast::()); } - pub(crate) fn scrypt<'a>( - cx: &mut JsScope<'a>, - password: JsLocal<'a>, - salt: JsLocal<'a>, - keylen: JsLocal<'a>, - options: JsLocal<'a>, - callback: JsLocal<'a>, - ) -> JsResult<()> { - let cx = cx.js_thread(); - let global = cx.global(); - let (params, callback) = Scrypt::from_js_async( - global, - password.to_js(), - salt.to_js(), - keylen.to_js(), - options.to_js(), - callback.to_js(), - )?; - if params.keylen as usize > jsc::virtual_machine::synthetic_allocation_limit() { - return Err(global.throw_out_of_memory()); - } - let (buf, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, params.keylen)?; - Job::::schedule( - &cx, - ScryptJob { - params, - // SAFETY: `bytes` is `buf`'s backing store, kept alive by the job's Js side. - result: unsafe { JsPtr::new(core::ptr::NonNull::from(bytes)) }, - err: None, - }, - ScryptJs { - callback: Strong::create(callback.with_async_context_if_needed(global), global), - buf: Strong::create(buf, global), - }, - ); - Ok(()) + bindgen::to_js::array(heap, hashes.keys(), bindgen::to_js::utf8) +} + +pub(crate) fn scrypt<'a>( + cx: &mut JsScope<'a>, + password: JsLocal<'a>, + salt: JsLocal<'a>, + keylen: JsLocal<'a>, + options: JsLocal<'a>, + callback: JsLocal<'a>, +) -> JsResult<()> { + let cx = cx.js_thread(); + let global = cx.global(); + let (params, callback) = Scrypt::from_js_async( + global, + password.to_js(), + salt.to_js(), + keylen.to_js(), + options.to_js(), + callback.to_js(), + )?; + if params.keylen as usize > jsc::virtual_machine::synthetic_allocation_limit() { + return Err(global.throw_out_of_memory()); } + let (buf, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, params.keylen)?; + Job::::schedule( + &cx, + ScryptJob { + params, + // SAFETY: `bytes` is `buf`'s backing store, kept alive by the job's Js side. + result: unsafe { JsPtr::new(core::ptr::NonNull::from(bytes)) }, + err: None, + }, + ScryptJs { + callback: Strong::create(callback.with_async_context_if_needed(global), global), + buf: Strong::create(buf, global), + }, + ); + Ok(()) +} - pub(crate) fn scrypt_sync<'a>( - cx: &mut JsScope<'a>, - password: JsLocal<'a>, - salt: JsLocal<'a>, - keylen: JsLocal<'a>, - options: JsLocal<'a>, - ) -> JsResult { - let global = cx.global(); - // `password`/`salt` release on drop; nothing was protected on this path. - let (ctx, _) = Scrypt::from_js::( - global, - password.to_js(), - salt.to_js(), - keylen.to_js(), - options.to_js(), - JSValue::UNDEFINED, - )?; - let (buf, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, ctx.keylen)?; - if ctx.run_task_impl(bytes).is_some() { - return Err(global - .err( - ErrorCode::CRYPTO_OPERATION_FAILED, - format_args!("Scrypt failed"), - ) - .throw()); - } - Ok(buf) +pub(crate) fn scrypt_sync<'a>( + cx: &mut JsScope<'a>, + password: JsLocal<'a>, + salt: JsLocal<'a>, + keylen: JsLocal<'a>, + options: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + // `password`/`salt` release on drop; nothing was protected on this path. + let (ctx, _) = Scrypt::from_js::( + global, + password.to_js(), + salt.to_js(), + keylen.to_js(), + options.to_js(), + JSValue::UNDEFINED, + )?; + let (buf, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, ctx.keylen)?; + if ctx.run_task_impl(bytes).is_some() { + return Err(global + .err( + ErrorCode::CRYPTO_OPERATION_FAILED, + format_args!("Scrypt failed"), + ) + .throw()); } + Ok(buf) +} - impl Argon2 { - /// Arguments arrive pre-validated from `checkArgon2()` in `crypto.ts`; - /// the checks here only defend the internal binding itself. - fn from_js( +impl Argon2 { + /// Arguments arrive pre-validated from `checkArgon2()` in `crypto.ts`; + /// the checks here only defend the internal binding itself. + fn from_js( + global: &JSGlobalObject, + message_value: JSValue, + nonce_value: JSValue, + parallelism_value: JSValue, + tag_length_value: JSValue, + memory_value: JSValue, + passes_value: JSValue, + secret_value: JSValue, + associated_data_value: JSValue, + variant_value: JSValue, + ) -> JsResult { + fn copy_buffer_arg( global: &JSGlobalObject, - message_value: JSValue, - nonce_value: JSValue, - parallelism_value: JSValue, - tag_length_value: JSValue, - memory_value: JSValue, - passes_value: JSValue, - secret_value: JSValue, - associated_data_value: JSValue, - variant_value: JSValue, - ) -> JsResult { - fn copy_buffer_arg( - global: &JSGlobalObject, - value: JSValue, - name: &'static [u8], - ) -> JsResult> { - let Some(buf) = value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - name, - b"ArrayBuffer, Buffer, TypedArray, or DataView", - value, - )); - }; - let bytes = buf.byte_slice(); - let mut copy = Vec::new(); - if copy.try_reserve_exact(bytes.len()).is_err() { - return Err(global.throw_out_of_memory()); - } - copy.extend_from_slice(bytes); - Ok(copy) - } - - let parallelism = validators::validate_uint32( - global, - parallelism_value, - format_args!("parameters.parallelism"), - true, - )?; - let tag_length = validators::validate_uint32( - global, - tag_length_value, - format_args!("parameters.tagLength"), - true, - )?; - let memory = validators::validate_uint32( - global, - memory_value, - format_args!("parameters.memory"), - true, - )?; - let passes = validators::validate_uint32( - global, - passes_value, - format_args!("parameters.passes"), - true, - )?; - let variant = match validators::validate_uint32( - global, - variant_value, - format_args!("type"), - false, - )? { - 0 => rust_argon2::Variant::Argon2d, - 1 => rust_argon2::Variant::Argon2i, - 2 => rust_argon2::Variant::Argon2id, - _ => { - return Err(global.throw_invalid_argument_type_value( - b"type", - b"a supported argon2 type", - variant_value, - )); - } + value: JSValue, + name: &'static [u8], + ) -> JsResult> { + let Some(buf) = value.as_array_buffer(global) else { + return Err(global.throw_invalid_argument_type_value( + name, + b"ArrayBuffer, Buffer, TypedArray, or DataView", + value, + )); }; - - // rust-argon2 ignores the synthetic allocation limit, so check it - // here. `hash_raw` reports real allocation failures. - let limit = jsc::virtual_machine::synthetic_allocation_limit(); - let failed = - (memory as usize).saturating_mul(1024) > limit || tag_length as usize > limit; - - Ok(Argon2 { - message: copy_buffer_arg(global, message_value, b"message")?, - nonce: copy_buffer_arg(global, nonce_value, b"nonce")?, - secret: copy_buffer_arg(global, secret_value, b"secret")?, - associated_data: copy_buffer_arg(global, associated_data_value, b"associatedData")?, - parallelism, - tag_length, - memory, - passes, - variant, - output: Vec::new(), - failed, - }) + let bytes = buf.byte_slice(); + let mut copy = Vec::new(); + if copy.try_reserve_exact(bytes.len()).is_err() { + return Err(global.throw_out_of_memory()); + } + copy.extend_from_slice(bytes); + Ok(copy) } - fn run(&mut self) { - if self.failed { - return; - } - let config = rust_argon2::Config { - ad: &self.associated_data, - hash_length: self.tag_length, - lanes: self.parallelism, - mem_cost: self.memory, - secret: &self.secret, - // Sequential like Bun.password (pwhash.rs): lanes determine - // the output, not the thread count, so results match node, - // which threads lanes via OpenSSL on its worker. - thread_mode: rust_argon2::ThreadMode::Sequential, - time_cost: self.passes, - variant: self.variant, - version: rust_argon2::Version::Version13, - }; - match rust_argon2::hash_raw(&self.message, &self.nonce, &config) { - Ok(hash) => self.output = hash, - // `checkArgon2()` already rejected every parameter rust-argon2 - // rejects, so this is an allocation failure. - Err(_) => self.failed = true, + let parallelism = validators::validate_uint32( + global, + parallelism_value, + format_args!("parameters.parallelism"), + true, + )?; + let tag_length = validators::validate_uint32( + global, + tag_length_value, + format_args!("parameters.tagLength"), + true, + )?; + let memory = validators::validate_uint32( + global, + memory_value, + format_args!("parameters.memory"), + true, + )?; + let passes = validators::validate_uint32( + global, + passes_value, + format_args!("parameters.passes"), + true, + )?; + let variant = match validators::validate_uint32( + global, + variant_value, + format_args!("type"), + false, + )? { + 0 => rust_argon2::Variant::Argon2d, + 1 => rust_argon2::Variant::Argon2i, + 2 => rust_argon2::Variant::Argon2id, + _ => { + return Err(global.throw_invalid_argument_type_value( + b"type", + b"a supported argon2 type", + variant_value, + )); } - } - } + }; - /// JS-thread state for the argon2 job: the user callback, invoked as - /// `(err)` or `(undefined, buffer)`. - #[derive(bun_jsc::JsAffine)] - pub(crate) struct Argon2Js { - callback: Strong, + // rust-argon2 ignores the synthetic allocation limit, so check it + // here. `hash_raw` reports real allocation failures. + let limit = jsc::virtual_machine::synthetic_allocation_limit(); + let failed = (memory as usize).saturating_mul(1024) > limit || tag_length as usize > limit; + + Ok(Argon2 { + message: copy_buffer_arg(global, message_value, b"message")?, + nonce: copy_buffer_arg(global, nonce_value, b"nonce")?, + secret: copy_buffer_arg(global, secret_value, b"secret")?, + associated_data: copy_buffer_arg(global, associated_data_value, b"associatedData")?, + parallelism, + tag_length, + memory, + passes, + variant, + output: Vec::new(), + failed, + }) } - /// `crypto.argon2` off the JS thread: `from_js` copied every input out of - /// JS, so the pool half owns plain memory and needs no `JsPtr`. - impl JobContext for Argon2 { - type OffThread = Self; - type Js = Argon2Js; - - fn run( - this: &mut Self, - done: bun_jsc::Completion, - ) -> Option> { - this.run(); - Some(done) + fn run(&mut self) { + if self.failed { + return; } - - fn then(mut this: Self, js: Argon2Js, cx: &JsThread<'_>) -> JsResult<()> { - let global = cx.global(); - let event_loop = global.bun_vm().event_loop_mut(); - let callback = js.callback.get(); - if this.failed { - let exception = - global.create_error_instance(format_args!("Argon2 derivation failed")); - event_loop.run_callback( - bun_event_loop::ContextId::NONE, - callback, - global, - JSValue::UNDEFINED, - &[exception], - ); - return Ok(()); - } - let output = core::mem::take(&mut this.output); - // Ownership transfers to JSC (freed via MarkedArrayBuffer_deallocator). - match JSValue::create_buffer(global, output.leak()) { - Ok(buf) => event_loop.run_callback( - bun_event_loop::ContextId::NONE, - callback, - global, - JSValue::UNDEFINED, - &[JSValue::UNDEFINED, buf], - ), - // The result could not be built (allocation failure): that is - // this derivation's error. - Err(err) => event_loop.run_callback( - bun_event_loop::ContextId::NONE, - callback, - global, - JSValue::UNDEFINED, - &[global.take_error(err)], - ), - } - Ok(()) + let config = rust_argon2::Config { + ad: &self.associated_data, + hash_length: self.tag_length, + lanes: self.parallelism, + mem_cost: self.memory, + secret: &self.secret, + // Sequential like Bun.password (pwhash.rs): lanes determine + // the output, not the thread count, so results match node, + // which threads lanes via OpenSSL on its worker. + thread_mode: rust_argon2::ThreadMode::Sequential, + time_cost: self.passes, + variant: self.variant, + version: rust_argon2::Version::Version13, + }; + match rust_argon2::hash_raw(&self.message, &self.nonce, &config) { + Ok(hash) => self.output = hash, + // `checkArgon2()` already rejected every parameter rust-argon2 + // rejects, so this is an allocation failure. + Err(_) => self.failed = true, } } +} - pub(crate) fn argon2<'a>( - cx: &mut JsScope<'a>, - message: JsLocal<'a>, - nonce: JsLocal<'a>, - parallelism: JsLocal<'a>, - tag_length: JsLocal<'a>, - memory: JsLocal<'a>, - passes: JsLocal<'a>, - secret: JsLocal<'a>, - associated_data: JsLocal<'a>, - variant: JsLocal<'a>, - callback: JsLocal<'a>, - ) -> JsResult<()> { - let cx = cx.js_thread(); - let global = cx.global(); - let callback = callback.to_js(); - let ctx = Argon2::from_js( - global, - message.to_js(), - nonce.to_js(), - parallelism.to_js(), - tag_length.to_js(), - memory.to_js(), - passes.to_js(), - secret.to_js(), - associated_data.to_js(), - variant.to_js(), - )?; - let _ = validators::validate_function(global, "callback", callback)?; - Job::::schedule( - &cx, - ctx, - Argon2Js { - callback: Strong::create(callback.with_async_context_if_needed(global), global), - }, - ); - Ok(()) +/// JS-thread state for the argon2 job: the user callback, invoked as +/// `(err)` or `(undefined, buffer)`. +#[derive(bun_jsc::JsAffine)] +pub(crate) struct Argon2Js { + callback: Strong, +} + +/// `crypto.argon2` off the JS thread: `from_js` copied every input out of +/// JS, so the pool half owns plain memory and needs no `JsPtr`. +impl JobContext for Argon2 { + type OffThread = Self; + type Js = Argon2Js; + + fn run(this: &mut Self, done: bun_jsc::Completion) -> Option> { + this.run(); + Some(done) } - pub(crate) fn argon2_sync<'a>( - cx: &mut JsScope<'a>, - message: JsLocal<'a>, - nonce: JsLocal<'a>, - parallelism: JsLocal<'a>, - tag_length: JsLocal<'a>, - memory: JsLocal<'a>, - passes: JsLocal<'a>, - secret: JsLocal<'a>, - associated_data: JsLocal<'a>, - variant: JsLocal<'a>, - ) -> JsResult { + fn then(mut this: Self, js: Argon2Js, cx: &JsThread<'_>) -> JsResult<()> { let global = cx.global(); - let mut ctx = Argon2::from_js( - global, - message.to_js(), - nonce.to_js(), - parallelism.to_js(), - tag_length.to_js(), - memory.to_js(), - passes.to_js(), - secret.to_js(), - associated_data.to_js(), - variant.to_js(), - )?; - ctx.run(); - if ctx.failed { - let err = global.create_error_instance(format_args!("Argon2 derivation failed")); - return Err(global.throw_value(err)); + let event_loop = global.bun_vm().event_loop_mut(); + let callback = js.callback.get(); + if this.failed { + let exception = global.create_error_instance(format_args!("Argon2 derivation failed")); + event_loop.run_callback( + bun_event_loop::ContextId::NONE, + callback, + global, + JSValue::UNDEFINED, + &[exception], + ); + return Ok(()); } + let output = core::mem::take(&mut this.output); // Ownership transfers to JSC (freed via MarkedArrayBuffer_deallocator). - JSValue::create_buffer(global, ctx.output.leak()) + match JSValue::create_buffer(global, output.leak()) { + Ok(buf) => event_loop.run_callback( + bun_event_loop::ContextId::NONE, + callback, + global, + JSValue::UNDEFINED, + &[JSValue::UNDEFINED, buf], + ), + // The result could not be built (allocation failure): that is + // this derivation's error. + Err(err) => event_loop.run_callback( + bun_event_loop::ContextId::NONE, + callback, + global, + JSValue::UNDEFINED, + &[global.take_error(err)], + ), + } + Ok(()) } -} // mod _impl +} -pub(crate) use _impl::{ - argon2, argon2_sync, get_fips, get_hashes, pbkdf2, pbkdf2_sync, scrypt, scrypt_sync, - secure_heap_used, set_engine, set_fips, -}; -pub(crate) use random::{ - random_bytes, random_fill, random_fill_sync, random_int, random_uuid, random_uuid_v7, -}; +pub(crate) fn argon2<'a>( + cx: &mut JsScope<'a>, + message: JsLocal<'a>, + nonce: JsLocal<'a>, + parallelism: JsLocal<'a>, + tag_length: JsLocal<'a>, + memory: JsLocal<'a>, + passes: JsLocal<'a>, + secret: JsLocal<'a>, + associated_data: JsLocal<'a>, + variant: JsLocal<'a>, + callback: JsLocal<'a>, +) -> JsResult<()> { + let cx = cx.js_thread(); + let global = cx.global(); + let callback = callback.to_js(); + let ctx = Argon2::from_js( + global, + message.to_js(), + nonce.to_js(), + parallelism.to_js(), + tag_length.to_js(), + memory.to_js(), + passes.to_js(), + secret.to_js(), + associated_data.to_js(), + variant.to_js(), + )?; + let _ = validators::validate_function(global, "callback", callback)?; + Job::::schedule( + &cx, + ctx, + Argon2Js { + callback: Strong::create(callback.with_async_context_if_needed(global), global), + }, + ); + Ok(()) +} + +pub(crate) fn argon2_sync<'a>( + cx: &mut JsScope<'a>, + message: JsLocal<'a>, + nonce: JsLocal<'a>, + parallelism: JsLocal<'a>, + tag_length: JsLocal<'a>, + memory: JsLocal<'a>, + passes: JsLocal<'a>, + secret: JsLocal<'a>, + associated_data: JsLocal<'a>, + variant: JsLocal<'a>, +) -> JsResult { + let global = cx.global(); + let mut ctx = Argon2::from_js( + global, + message.to_js(), + nonce.to_js(), + parallelism.to_js(), + tag_length.to_js(), + memory.to_js(), + passes.to_js(), + secret.to_js(), + associated_data.to_js(), + variant.to_js(), + )?; + ctx.run(); + if ctx.failed { + let err = global.create_error_instance(format_args!("Argon2 derivation failed")); + return Err(global.throw_value(err)); + } + // Ownership transfers to JSC (freed via MarkedArrayBuffer_deallocator). + JSValue::create_buffer(global, ctx.output.leak()) +} diff --git a/src/runtime/node/node_fs_binding.bind.ts b/src/runtime/node/node_fs_binding.bind.ts index 9d2381e728d2..60b9e63a6c03 100644 --- a/src/runtime/node/node_fs_binding.bind.ts +++ b/src/runtime/node/node_fs_binding.bind.ts @@ -53,7 +53,7 @@ const operations: Record = { const getter = { getter: b.any, capability: "js" } as const; -/** `src/js/internal/fs/binding.ts`. */ +/** The default export of `src/js/internal/fs/binding.ts`. */ export const NodeJSFS = b.class({ rustPath: "crate::node::node_fs_binding::Binding", proto: Object.fromEntries( @@ -73,7 +73,7 @@ export const NodeJSFS = b.class({ ), }); -export const createBinding = b.fn({ returns: NodeJSFS, capability: "js" }); +export const binding = b.value({ returns: NodeJSFS, capability: "js" }); /** `(path, options)`, parsed as `fs.rm` parses them. */ export const rmOptionsForTesting = b.fn({ diff --git a/src/runtime/node/node_fs_binding.rs b/src/runtime/node/node_fs_binding.rs index db295de73ed7..c2c747647770 100644 --- a/src/runtime/node/node_fs_binding.rs +++ b/src/runtime/node/node_fs_binding.rs @@ -370,7 +370,7 @@ impl Binding { // pub const statfsSync = callSync(.statfs); } -pub(crate) fn create_binding<'a>(cx: &mut JsScope<'a>) -> JsWrapper<'a, Binding> { +pub(crate) fn binding<'a>(cx: &mut JsScope<'a>) -> JsWrapper<'a, Binding> { Binding { node_fs: JsCell::new(NodeFS { vm: NonNull::new(cx.global().bun_vm_ptr()), diff --git a/src/runtime/node/node_http_binding.bind.ts b/src/runtime/node/node_http_binding.bind.ts index c675367097a6..52684e2f6224 100644 --- a/src/runtime/node/node_http_binding.bind.ts +++ b/src/runtime/node/node_http_binding.bind.ts @@ -10,7 +10,7 @@ export const setMaxHTTPHeaderSize = b.fn({ args: { maxHeaderSize: b.usize.validateInteger(1, Number.MAX_SAFE_INTEGER) }, rustPath: "bun_http::set_max_http_header_size", }); -export const getInsecureHTTPParser = b.fn({ returns: b.bool, rustPath: "bun_http::insecure_http_parser" }); +export const insecureHTTPParser = b.value({ returns: b.bool, rustPath: "bun_http::insecure_http_parser" }); export const BunServer = b.union({ http: HTTPServer, diff --git a/src/runtime/node/node_os.bind.ts b/src/runtime/node/node_os.bind.ts index d6fcbb343094..e8fb1d8c1058 100644 --- a/src/runtime/node/node_os.bind.ts +++ b/src/runtime/node/node_os.bind.ts @@ -4,7 +4,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::node::os" }); -export const hostCpuCount = b.fn({ returns: b.i32 }); +export const hostCpuCount = b.value({ returns: b.i32 }); export const cpus = b.fn({ returns: b.any, capability: "js", throws: true }); export const freemem = b.fn({ returns: b.u64 }); export const getPriority = b.fn({ diff --git a/src/runtime/node/node_quic_binding.bind.ts b/src/runtime/node/node_quic_binding.bind.ts index c15f9177aeff..82d2e6f03c9d 100644 --- a/src/runtime/node/node_quic_binding.bind.ts +++ b/src/runtime/node/node_quic_binding.bind.ts @@ -2,8 +2,8 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::node::node_quic_binding" }); -/** `src/js/internal/quic/binding.ts`. */ -export const createBinding = b.fn({ returns: b.any, capability: "js", throws: true }); +/** The default export of `src/js/internal/quic/binding.ts`. */ +export const binding = b.value({ returns: b.any, capability: "js", throws: true }); export const setCallbacks = b.fn({ export: "Bun__NodeQuicBinding__setCallbacks", diff --git a/src/runtime/node/node_quic_binding.rs b/src/runtime/node/node_quic_binding.rs index 4bfcc57b571b..72349accaa34 100644 --- a/src/runtime/node/node_quic_binding.rs +++ b/src/runtime/node/node_quic_binding.rs @@ -102,7 +102,7 @@ macro_rules! put_state_offsets_with_size { }; } -pub(crate) fn create_binding(cx: &mut JsScope<'_>) -> JsResult { +pub(crate) fn binding(cx: &mut JsScope<'_>) -> JsResult { let global = cx.global(); let obj = JSValue::create_empty_object(global, 16); diff --git a/src/runtime/node/node_util_binding.bind.ts b/src/runtime/node/node_util_binding.bind.ts index bbc1e726076c..a20df7c942a1 100644 --- a/src/runtime/node/node_util_binding.bind.ts +++ b/src/runtime/node/node_util_binding.bind.ts @@ -9,7 +9,7 @@ export const internalErrorEntries = b.fn({ returns: b.array(UVError) }); export const etimedoutErrorCode = b.fn({ returns: b.i32 }); export const enobufsErrorCode = b.fn({ returns: b.i32 }); -export const ecanceledErrorCode = b.fn({ returns: b.i32 }); +export const ecanceledErrorCode = b.value({ returns: b.i32 }); export const uvTranslateSysError = b.fn({ args: { errno: b.f64 }, returns: b.i32 }); export const extractedSplitNewLinesFastPathStringsOnly = b.fn({ diff --git a/src/runtime/socket/UpgradedDuplex.rs b/src/runtime/socket/UpgradedDuplex.rs index 08841bdf3cbb..238f98d616b2 100644 --- a/src/runtime/socket/UpgradedDuplex.rs +++ b/src/runtime/socket/UpgradedDuplex.rs @@ -126,7 +126,7 @@ fn lazy_js_handler( js_wrapper: JSValue, set_slot: fn(JSValue, &JSGlobalObject, JSValue), global: &JSGlobalObject, - func: host_fn::JsHostFn, + func: host_fn::JSHostFn, this_ptr: *mut c_void, ) -> JSValue { if !shadow.get().is_empty() { diff --git a/src/runtime/test_runner/BunTest.bind.ts b/src/runtime/test_runner/BunTest.bind.ts index 07ff5685deb4..5764d47c15ff 100644 --- a/src/runtime/test_runner/BunTest.bind.ts +++ b/src/runtime/test_runner/BunTest.bind.ts @@ -1,5 +1,6 @@ import * as b from "../../codegen/bindgen-lib.ts"; +// Not `bun_test.bind.ts`: a release build leaves out a `*_test.bind.ts`. export default b.module({ rustPath: "crate::test_runner::bun_test::js_fns::generic_hook" }); const js = { capability: "js", throws: true } as const; diff --git a/src/runtime/test_runner/bun_test.rs b/src/runtime/test_runner/bun_test.rs index a80636f1950a..bcf8e44297bd 100644 --- a/src/runtime/test_runner/bun_test.rs +++ b/src/runtime/test_runner/bun_test.rs @@ -124,8 +124,6 @@ pub(crate) mod js_fns { /// Tags accepted by `generic_hook`. Superset of `DescribeScope::HookTag` /// (adds `OnTestFinished`). - // was a const-generic param (`adt_const_params` is unstable); - // reshaped to runtime dispatch with per-tag thin wrappers below. #[derive(Copy, Clone, PartialEq, Eq, strum::IntoStaticStr)] pub enum GenericHookTag { #[strum(serialize = "beforeAll")] @@ -161,9 +159,6 @@ pub(crate) mod js_fns { } } - // `adt_const_params` is unstable, so the body takes `tag` at runtime and - // 5 thin wrappers below supply the per-tag entry points - // (one fn per JS function so JSFunction::create gets a distinct address). pub(crate) fn generic_hook_impl( tag: GenericHookTag, global_this: &JSGlobalObject, @@ -328,8 +323,7 @@ pub(crate) mod js_fns { result } - /// Per-tag entry points (one fn per JS function so - /// `JSFunction::create` gets a distinct address). + /// One for each hook of `BunTest.bind.ts`. pub(crate) mod generic_hook { use super::*; macro_rules! hook { diff --git a/src/runtime/webcore/prompt.bind.ts b/src/runtime/webcore/prompt.bind.ts index 5bbfc44aeace..ef5ad526b6b4 100644 --- a/src/runtime/webcore/prompt.bind.ts +++ b/src/runtime/webcore/prompt.bind.ts @@ -7,4 +7,4 @@ const dialog = { args: { args: b.any.variadic }, capability: "js", throws: true, export const alert = b.fn(dialog); export const confirm = b.fn({ ...dialog, returns: b.bool }); -export const prompt = b.fn({ ...dialog, returns: b.any, rustPath: "crate::webcore::prompt::prompt::prompt" }); +export const prompt = b.fn({ ...dialog, returns: b.any }); diff --git a/src/runtime/webcore/prompt.rs b/src/runtime/webcore/prompt.rs index 00c6992a6fe7..35372aa71f49 100644 --- a/src/runtime/webcore/prompt.rs +++ b/src/runtime/webcore/prompt.rs @@ -160,238 +160,232 @@ pub(crate) fn confirm(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult Result`; the only - /// concrete impl is the process-global `BufferedStdin`. - pub(crate) trait ReadByte { - type Error; - fn read_byte(&mut self) -> Result; - } +/// Small trait exposing `read_byte() -> Result`; the only +/// concrete impl is the process-global `BufferedStdin`. +pub(crate) trait ReadByte { + type Error; + fn read_byte(&mut self) -> Result; +} - impl ReadByte for bun_core::output::BufferedStdin { - type Error = bun_core::Error; - #[inline] - fn read_byte(&mut self) -> Result { - bun_core::output::BufferedStdin::read_byte(self) - } +impl ReadByte for bun_core::output::BufferedStdin { + type Error = bun_core::Error; + #[inline] + fn read_byte(&mut self) -> Result { + bun_core::output::BufferedStdin::read_byte(self) } +} - /// Reads bytes until `delimiter` (exclusive), erroring with `StreamTooLong` - /// once `max_size` bytes have been appended. - pub(crate) fn read_until_delimiter_array_list_append_assume_capacity( - reader: &mut R, - array_list: &mut Vec, - delimiter: u8, - max_size: usize, - ) -> Result<(), ReadError> { - loop { - if array_list.len() == max_size { - return Err(ReadError::StreamTooLong); - } - - let byte: u8 = reader.read_byte().map_err(|_| ReadError::Io)?; +/// Reads bytes until `delimiter` (exclusive), erroring with `StreamTooLong` +/// once `max_size` bytes have been appended. +pub(crate) fn read_until_delimiter_array_list_append_assume_capacity( + reader: &mut R, + array_list: &mut Vec, + delimiter: u8, + max_size: usize, +) -> Result<(), ReadError> { + loop { + if array_list.len() == max_size { + return Err(ReadError::StreamTooLong); + } - if byte == delimiter { - return Ok(()); - } + let byte: u8 = reader.read_byte().map_err(|_| ReadError::Io)?; - array_list.push(byte); + if byte == delimiter { + return Ok(()); } + + array_list.push(byte); } +} - /// Reads bytes until `delimiter` (exclusive), appending to `array_list` - /// with no size limit. - fn read_until_delimiter_array_list_infinity( - reader: &mut R, - array_list: &mut Vec, - delimiter: u8, - ) -> Result<(), ReadError> { - loop { - let byte: u8 = reader.read_byte().map_err(|_| ReadError::Io)?; - - if byte == delimiter { - return Ok(()); - } +/// Reads bytes until `delimiter` (exclusive), appending to `array_list` +/// with no size limit. +fn read_until_delimiter_array_list_infinity( + reader: &mut R, + array_list: &mut Vec, + delimiter: u8, +) -> Result<(), ReadError> { + loop { + let byte: u8 = reader.read_byte().map_err(|_| ReadError::Io)?; - array_list.push(byte); + if byte == delimiter { + return Ok(()); } + + array_list.push(byte); } +} - /// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-prompt - pub(crate) fn prompt(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult { - let global = cx.global(); - let arguments = JsLocal::to_js_slice(args); - let output = Output::writer(); - let has_message = !arguments.is_empty(); - let has_default = arguments.len() >= 2; - // 4. Set default to the result of optionally truncating default. - // * We don't really need to do this. - let default = if has_default { - arguments[1] - } else { - JSValue::NULL - }; +/// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-prompt +pub(crate) fn prompt(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult { + let global = cx.global(); + let arguments = JsLocal::to_js_slice(args); + let output = Output::writer(); + let has_message = !arguments.is_empty(); + let has_default = arguments.len() >= 2; + // 4. Set default to the result of optionally truncating default. + // * We don't really need to do this. + let default = if has_default { + arguments[1] + } else { + JSValue::NULL + }; - if has_message { - // 2. Set message to the result of normalizing newlines given message. - // * Not pertinent to a server runtime so we will just let the terminal handle this. + if has_message { + // 2. Set message to the result of normalizing newlines given message. + // * Not pertinent to a server runtime so we will just let the terminal handle this. - // 3. Set message to the result of optionally truncating message. - // * Not necessary so we won't do it. - let message = arguments[0].to_utf8(global)?; + // 3. Set message to the result of optionally truncating message. + // * Not necessary so we won't do it. + let message = arguments[0].to_utf8(global)?; - if output.write_all(message.slice()).is_err() { - // 1. If we cannot show simple dialogs for this, then return null. - return Ok(JSValue::NULL); - } + if output.write_all(message.slice()).is_err() { + // 1. If we cannot show simple dialogs for this, then return null. + return Ok(JSValue::NULL); } + } + + // 4. Set default to the result of optionally truncating default. + + // 5. Show message to the user, treating U+000A LF as a line break, + // and ask the user to either respond with a string value or + // abort. The response must be defaulted to the value given by + // default. + if output + .write_all(if has_message { + b" " as &[u8] + } else { + b"Prompt " + }) + .is_err() + { + // 1. If we cannot show simple dialogs for this, then return false. + return Ok(JSValue::FALSE); + } - // 4. Set default to the result of optionally truncating default. + if has_default { + let default_string = arguments[1].to_utf8(global)?; - // 5. Show message to the user, treating U+000A LF as a line break, - // and ask the user to either respond with a string value or - // abort. The response must be defaulted to the value given by - // default. if output - .write_all(if has_message { - b" " as &[u8] - } else { - b"Prompt " - }) + .print(format_args!( + "[{}] ", + bstr::BStr::new(default_string.slice()) + )) .is_err() { // 1. If we cannot show simple dialogs for this, then return false. return Ok(JSValue::FALSE); } + } - if has_default { - let default_string = arguments[1].to_utf8(global)?; - - if output - .print(format_args!( - "[{}] ", - bstr::BStr::new(default_string.slice()) - )) - .is_err() - { - // 1. If we cannot show simple dialogs for this, then return false. - return Ok(JSValue::FALSE); - } - } + // 6. Invoke WebDriver BiDi user prompt opened with this, "prompt" and message. + // * Not relevant in a server context. + Output::flush(); - // 6. Invoke WebDriver BiDi user prompt opened with this, "prompt" and message. - // * Not relevant in a server context. - Output::flush(); - - // unset `ENABLE_VIRTUAL_TERMINAL_INPUT` on windows. This prevents backspace from - // deleting the entire line - #[cfg(windows)] - let _restore = - bun_sys::windows::StdinModeGuard::set(bun_sys::windows::UpdateStdioModeFlagsOpts { - unset: bun_sys::windows::ENABLE_VIRTUAL_TERMINAL_INPUT, - ..Default::default() - }); - - // 7. Pause while waiting for the user's response. - // `bun.Output.buffered_stdin.reader()` — process-global 4 KiB buffered stdin. - // SAFETY: process-global static; prompt() runs single-threaded on the JS - // main thread, so the exclusive borrow is sound for this scope. - let reader: &mut bun_core::output::BufferedStdin = - unsafe { &mut *Output::buffered_stdin_reader() }; - let mut second_byte: Option = None; - let Ok(first_byte) = reader.read_byte() else { - // 8. Let result be null if the user aborts, or otherwise the string - // that the user responded with. + // unset `ENABLE_VIRTUAL_TERMINAL_INPUT` on windows. This prevents backspace from + // deleting the entire line + #[cfg(windows)] + let _restore = + bun_sys::windows::StdinModeGuard::set(bun_sys::windows::UpdateStdioModeFlagsOpts { + unset: bun_sys::windows::ENABLE_VIRTUAL_TERMINAL_INPUT, + ..Default::default() + }); + + // 7. Pause while waiting for the user's response. + // `bun.Output.buffered_stdin.reader()` — process-global 4 KiB buffered stdin. + // SAFETY: process-global static; prompt() runs single-threaded on the JS + // main thread, so the exclusive borrow is sound for this scope. + let reader: &mut bun_core::output::BufferedStdin = + unsafe { &mut *Output::buffered_stdin_reader() }; + let mut second_byte: Option = None; + let Ok(first_byte) = reader.read_byte() else { + // 8. Let result be null if the user aborts, or otherwise the string + // that the user responded with. + return Ok(JSValue::NULL); + }; + + if first_byte == b'\n' { + // 8. Let result be null if the user aborts, or otherwise the string + // that the user responded with. + return Ok(default); + } else if first_byte == b'\r' { + let Ok(second) = reader.read_byte() else { return Ok(JSValue::NULL); }; - - if first_byte == b'\n' { - // 8. Let result be null if the user aborts, or otherwise the string - // that the user responded with. + second_byte = Some(second); + if second == b'\n' { return Ok(default); - } else if first_byte == b'\r' { - let Ok(second) = reader.read_byte() else { - return Ok(JSValue::NULL); - }; - second_byte = Some(second); - if second == b'\n' { - return Ok(default); - } } + } - let mut input: Vec = Vec::with_capacity(2048); + let mut input: Vec = Vec::with_capacity(2048); - input.push(first_byte); - if let Some(second) = second_byte { - input.push(second); + input.push(first_byte); + if let Some(second) = second_byte { + input.push(second); + } + + // All of this code basically just first tries to load the input into a + // buffer of size 2048. If that is too small, then increase the buffer + // size to 4096. If that is too small, then just dynamically allocate + // the rest. + if let Err(e) = read_until_delimiter_array_list_append_assume_capacity( + &mut *reader, + &mut input, + b'\n', + 2048, + ) { + if !matches!(e, ReadError::StreamTooLong) { + // 8. Let result be null if the user aborts, or otherwise the string + // that the user responded with. + return Ok(JSValue::NULL); } - // All of this code basically just first tries to load the input into a - // buffer of size 2048. If that is too small, then increase the buffer - // size to 4096. If that is too small, then just dynamically allocate - // the rest. - if let Err(e) = read_until_delimiter_array_list_append_assume_capacity( + input.ensure_total_capacity(4096); + + if let Err(e2) = read_until_delimiter_array_list_append_assume_capacity( &mut *reader, &mut input, b'\n', - 2048, + 4096, ) { - if !matches!(e, ReadError::StreamTooLong) { + if !matches!(e2, ReadError::StreamTooLong) { // 8. Let result be null if the user aborts, or otherwise the string // that the user responded with. return Ok(JSValue::NULL); } - input.ensure_total_capacity(4096); - - if let Err(e2) = read_until_delimiter_array_list_append_assume_capacity( - &mut *reader, - &mut input, - b'\n', - 4096, - ) { - if !matches!(e2, ReadError::StreamTooLong) { - // 8. Let result be null if the user aborts, or otherwise the string - // that the user responded with. - return Ok(JSValue::NULL); - } - - if read_until_delimiter_array_list_infinity(&mut *reader, &mut input, b'\n') - .is_err() - { - // 8. Let result be null if the user aborts, or otherwise the string - // that the user responded with. - return Ok(JSValue::NULL); - } + if read_until_delimiter_array_list_infinity(&mut *reader, &mut input, b'\n').is_err() { + // 8. Let result be null if the user aborts, or otherwise the string + // that the user responded with. + return Ok(JSValue::NULL); } } + } - if !input.is_empty() && input[input.len() - 1] == b'\r' { - input.truncate(input.len() - 1); - } + if !input.is_empty() && input[input.len() - 1] == b'\r' { + input.truncate(input.len() - 1); + } - debug_assert!(!input.is_empty()); + debug_assert!(!input.is_empty()); - // 8. Let result be null if the user aborts, or otherwise the string - // that the user responded with. - let result = EncodedSlice::utf8(&input); + // 8. Let result be null if the user aborts, or otherwise the string + // that the user responded with. + let result = EncodedSlice::utf8(&input); - // 9. Invoke WebDriver BiDi user prompt closed with this, false if - // result is null or true otherwise, and result. - // * Too complex for server context. + // 9. Invoke WebDriver BiDi user prompt closed with this, false if + // result is null or true otherwise, and result. + // * Too complex for server context. - // 9. Return result. - Ok(result.to_js(global)) - } + // 9. Return result. + Ok(result.to_js(global)) } diff --git a/src/runtime/webcore/s3/client.bind.ts b/src/runtime/webcore/s3/S3UploadStream.bind.ts similarity index 100% rename from src/runtime/webcore/s3/client.bind.ts rename to src/runtime/webcore/s3/S3UploadStream.bind.ts diff --git a/src/runtime/webcore/s3/client.rs b/src/runtime/webcore/s3/client.rs index 9183caff52bf..a1d691fa7a19 100644 --- a/src/runtime/webcore/s3/client.rs +++ b/src/runtime/webcore/s3/client.rs @@ -44,7 +44,7 @@ pub(crate) use crate::webcore::s3::simple_request::S3UploadResult; use crate::webcore::s3::simple_request as s3_simple_request; -use crate::generated_bindgen::client::{ +use crate::generated_bindgen::s3_upload_stream::{ Bun__S3UploadStream__onRejectStream, Bun__S3UploadStream__onResolveStream, }; use crate::webcore::ByteStream; diff --git a/src/semver_jsc/SemverObject.bind.ts b/src/semver_jsc/SemverObject.bind.ts index 20b87f542347..ca2485e2de1c 100644 --- a/src/semver_jsc/SemverObject.bind.ts +++ b/src/semver_jsc/SemverObject.bind.ts @@ -15,9 +15,3 @@ export const semver = b.namespace({ throws: true, }, }); - -export const intersects = b.fn({ - args: { left: b.string.loose.utf8, right: b.string.loose.utf8 }, - returns: b.bool, - throws: true, -}); diff --git a/src/semver_jsc/SemverObjectInternals.bind.ts b/src/semver_jsc/SemverObjectInternals.bind.ts new file mode 100644 index 000000000000..6c4be812669a --- /dev/null +++ b/src/semver_jsc/SemverObjectInternals.bind.ts @@ -0,0 +1,9 @@ +import * as b from "../codegen/bindgen-lib.ts"; + +export default b.module({ rustPath: "crate::SemverObject" }); + +export const intersects = b.fn({ + args: { left: b.string.loose.utf8, right: b.string.loose.utf8 }, + returns: b.bool, + throws: true, +}); diff --git a/src/sourcemap_jsc/internal_jsc.bind.ts b/src/sourcemap_jsc/InternalSourceMap.bind.ts similarity index 100% rename from src/sourcemap_jsc/internal_jsc.bind.ts rename to src/sourcemap_jsc/InternalSourceMap.bind.ts diff --git a/src/sourcemap_jsc/internal_jsc.rs b/src/sourcemap_jsc/internal_jsc.rs index 70684756189f..6adbb2911fe0 100644 --- a/src/sourcemap_jsc/internal_jsc.rs +++ b/src/sourcemap_jsc/internal_jsc.rs @@ -5,7 +5,7 @@ use bun_jsc::bindgen::{self, BufferSource, JsHeap, JsLocal}; use bun_sourcemap::Ordinal; use bun_sourcemap::internal_source_map::{self, InternalSourceMap}; -use crate::generated_bindgen::internal_jsc::InternalSourceMapMapping; +use crate::generated_bindgen::internal_source_map::InternalSourceMapMapping; pub(crate) fn from_vlq<'a>( heap: &JsHeap<'a>, diff --git a/src/sys/Error.rs b/src/sys/Error.rs index d5b4f818859a..d2977ad3900f 100644 --- a/src/sys/Error.rs +++ b/src/sys/Error.rs @@ -438,7 +438,7 @@ impl fmt::Display for Error { } } -// `toJS` / `toJSWithAsyncStack` / `TestingAPIs` live as extension-trait +// `toJS` / `toJSWithAsyncStack` live as extension-trait // methods in the `bun_sys_jsc` crate per PORTING.md. // ────────────────────────────────────────────────────────────────────────── diff --git a/src/sys_jsc/error_jsc.bind.ts b/src/sys_jsc/error_jsc.bind.ts index 19fc8b3e1784..15a043e82ec6 100644 --- a/src/sys_jsc/error_jsc.bind.ts +++ b/src/sys_jsc/error_jsc.bind.ts @@ -1,6 +1,6 @@ import * as b from "../codegen/bindgen-lib.ts"; -export default b.module({ rustPath: "crate::error_jsc::TestingAPIs" }); +export default b.module({ rustPath: "crate::error_jsc::testing_apis" }); export const translateNtStatusToE = b.fn({ args: { status: b.f64 }, returns: b.string.utf8.optional }); export const translateUVErrorToE = b.fn({ args: { code: b.f64 }, returns: b.string.utf8.optional }); diff --git a/src/sys_jsc/error_jsc.rs b/src/sys_jsc/error_jsc.rs index 3138b3906b45..d6768f426d35 100644 --- a/src/sys_jsc/error_jsc.rs +++ b/src/sys_jsc/error_jsc.rs @@ -34,7 +34,7 @@ impl ErrorJsc for Error { } } -pub(crate) mod TestingAPIs { +pub(crate) mod testing_apis { use crate::generated_bindgen::error_jsc::SigactionLayout; /// Exposes NTSTATUS -> `bun.sys.E` translation so tests can feed NTSTATUS diff --git a/src/sys_jsc/lib.rs b/src/sys_jsc/lib.rs index ef7ddcb0e229..2799c80699b4 100644 --- a/src/sys_jsc/lib.rs +++ b/src/sys_jsc/lib.rs @@ -1,4 +1,3 @@ -#![allow(non_snake_case)] //! JSC bridge crate for `bun_sys`. Adds `to_js`/`from_js` extension surfaces //! onto `bun_sys::{Fd, Error, SignalCode}` without pulling JSC types into the //! syscall layer. diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index dbe0201bd2d4..72e57fa69b75 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -329,10 +329,12 @@ const PUBLISHED_AS: Record = { "FetchTasklet.bind.ts": null, "FileSink.bind.ts": null, "FormData.bind.ts": null, + "FrameworkRouter.bind.ts": null, "Glob.bind.ts": "typeof Bun", "HTMLBundle.bind.ts": null, "HashObject.bind.ts": "typeof Bun.hash & { wyhash: typeof Bun.hash }", "Image.bind.ts": "typeof Bun", + "InternalSourceMap.bind.ts": null, "JSBundler.bind.ts": null, "JSMySQLConnection.bind.ts": null, "JSMySQLQuery.bind.ts": null, @@ -353,10 +355,12 @@ const PUBLISHED_AS: Record = { "ResourceUsage.bind.ts": null, "S3Client.bind.ts": "typeof Bun", "S3Stat.bind.ts": "{ S3Stat: Class }", + "S3UploadStream.bind.ts": null, "SSLConfig.bind.ts": null, "ScopeFunctions.bind.ts": null, "SecureContext.bind.ts": null, - "SemverObject.bind.ts": "typeof Bun.semver & { intersects(left: string, right: string): boolean }", + "SemverObject.bind.ts": "typeof Bun.semver", + "SemverObjectInternals.bind.ts": null, "SocketAddress.bind.ts": null, "SocketConfig.bind.ts": null, "Stat.bind.ts": null, @@ -369,10 +373,10 @@ const PUBLISHED_AS: Record = { "UpgradedDuplex.bind.ts": null, "XMLObject.bind.ts": "typeof Bun.XML", "YAMLObject.bind.ts": "typeof Bun.YAML", + "bake_plugins.bind.ts": null, "bindgen_test.bind.ts": null, "bun_string_jsc.bind.ts": null, "cares_jsc.bind.ts": null, - "client.bind.ts": null, "color_js.bind.ts": "typeof Bun", "crash_handler_jsc.bind.ts": null, "cron.bind.ts": null, @@ -398,12 +402,10 @@ const PUBLISHED_AS: Record = { "imported_class_test.bind.ts": null, "ini_jsc.bind.ts": null, "install_binding.bind.ts": null, - "internal_jsc.bind.ts": null, "interpreter.bind.ts": null, "ipc.bind.ts": null, "ipc_host.bind.ts": null, "jest.bind.ts": null, - "js.bind.ts": null, "js_bun_spawn_bindings.bind.ts": "typeof Bun", "jsc_hooks.bind.ts": null, "linear_fifo_testing.bind.ts": null, @@ -426,7 +428,7 @@ const PUBLISHED_AS: Record = { "npm_jsc.bind.ts": null, "pack_command.bind.ts": null, "parse_args.bind.ts": null, - "plugins.bind.ts": null, + "patch_testing.bind.ts": null, "postgres.bind.ts": null, "prompt.bind.ts": "{ alert: typeof alert; confirm: typeof confirm; prompt: typeof prompt }", "proxy_testing.bind.ts": null, @@ -442,7 +444,6 @@ const PUBLISHED_AS: Record = { "socket_body.bind.ts": null, "sourcemap.bind.ts": null, "subprocess.bind.ts": null, - "testing.bind.ts": null, "udp_socket.bind.ts": null, "upgrade_command.bind.ts": null, "valkey.bind.ts": "typeof Bun", diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 3b005ce14d76..a466c93dad5e 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -712,6 +712,10 @@ describe("functions", () => { expect(() => b.fn({ export: "not a name" })).toThrow("export: not a name is not a C identifier"); }); + it("$bindgen gives what a b.value returns", () => { + expect(bindgen.primes).toEqual([2, 3, 5]); + }); + it("$bindgen gives a namespace as an object", () => { expect(Object.keys(bindgen.testNamespace)).toEqual(["increment", "decrement"]); expect(bindgen.testNamespace.increment(1)).toBe(2); diff --git a/test/internal/source-lints/host-export-callers.test.ts b/test/internal/source-lints/host-export-callers.test.ts index 5e5934c5119f..f4c27f24ac00 100644 --- a/test/internal/source-lints/host-export-callers.test.ts +++ b/test/internal/source-lints/host-export-callers.test.ts @@ -37,7 +37,7 @@ const root = path.resolve(import.meta.dir, "..", "..", ".."); // Same grammar as `markerRe` in src/codegen/generate-host-exports.ts: the // marker is the whole line. Prose such as "from the `// HOST_EXPORT(Sym, c)` // markers" in a doc comment does not match. Group 1 is the symbol, group 2 -// the abi (`jsc` when absent). +// the abi (`c` when absent). const MARKER = /^\s*\/\/\s*HOST_EXPORT\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:,\s*(jsc|c|rust))?\s*\)\s*$/; const GENERATED_PREFIXES = ["js2native_"]; diff --git a/test/leaksan.supp b/test/leaksan.supp index 06f00aa8c43d..1403d57c9a9f 100644 --- a/test/leaksan.supp +++ b/test/leaksan.supp @@ -95,7 +95,7 @@ leak:runtime.node.node_fs_watcher.FSWatcher.Arguments.fromJS leak:jsc.web_worker.create leak:runtime.node.zlib.NativeZlib.Context.init leak:JSC::intlCollatorAvailableLocales -leak:Bun__canonicalizeIP +leak:Bun__Bindgen__CaresJsc__canonicalizeIP leak:dlopen leak:Bun::evaluateCommonJSModuleOnce leak:fse_run_loop From 4c5877ff8b1f0a7a18245ba4b5c328e80847fbde Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:15:28 +0000 Subject: [PATCH 06/22] [autofix.ci] apply automated fixes --- src/codegen/bindgen-lib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 4223f46f4748..6969c2d43d92 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -16,7 +16,6 @@ import { EnumerationType, FunctionDefinition, type FunctionOptions, - type ValueOptions, integers, type MemberOptions, ModuleDefinition, @@ -29,6 +28,7 @@ import { type Type, UnionType, UnitType, + type ValueOptions, } from "./bindgen-lib-internal.ts"; export type { Capability, Type } from "./bindgen-lib-internal.ts"; From 432b725b23fa5171b40d2a069dbfa240ea44353f Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 00:19:00 +0000 Subject: [PATCH 07/22] Write `isValue` as a plain field so that Node runs the generator, and take a `String` object where `main` does --- src/codegen/bindgen-lib-internal.ts | 10 ++++------ src/css_jsc/css_internals.bind.ts | 4 ++-- src/install_jsc/hosted_git_info_jsc.bind.ts | 4 ++-- src/install_jsc/npm_jsc.bind.ts | 2 +- src/jsc/bun_string_jsc.bind.ts | 4 ++-- src/runtime/api/cron.bind.ts | 4 ++-- src/runtime/cli/pack_command.bind.ts | 2 +- src/runtime/dns_jsc/dns_internals.bind.ts | 6 +++--- test/js/bun/cron/cron-parse.test.ts | 6 ++++++ 9 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index d69d6d90f921..2395fdc34b6f 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1881,14 +1881,12 @@ export class FunctionDefinition { readonly passThis: boolean; readonly passCallee: boolean; readonly exportName: string | undefined; + /** `b.value`: there is no JS function. */ + readonly isValue: boolean; name = ""; origin: Origin | null = null; - /** `isValue`: `b.value`. There is no JS function. */ - constructor( - signatures: readonly (SignatureOptions | Signature)[], - options: FunctionOptions = {}, - readonly isValue = false, - ) { + constructor(signatures: readonly (SignatureOptions | Signature)[], options: FunctionOptions = {}, isValue = false) { + this.isValue = isValue; assert(signatures.length > 0, "b.overloads needs at least one signature"); this.passThis = options.passThis ?? false; this.passCallee = options.passCallee ?? false; diff --git a/src/css_jsc/css_internals.bind.ts b/src/css_jsc/css_internals.bind.ts index de9145643b0e..be6c10582f2e 100644 --- a/src/css_jsc/css_internals.bind.ts +++ b/src/css_jsc/css_internals.bind.ts @@ -5,7 +5,7 @@ export default b.module({ rustPath: "crate::css_internals" }); /** `options` is the browser targets, or the parser's options for a `WithOptions` function. */ const stylesheetTest = () => b.fn({ - args: { source: b.string, expected: b.string, options: b.any }, + args: { source: b.string.allowStringObject, expected: b.string.allowStringObject, options: b.any }, returns: b.any, capability: "js", throws: true, @@ -20,7 +20,7 @@ export const prefixTest = stylesheetTest(); export const test = stylesheetTest(); export const attrTest = b.fn({ - args: { source: b.string, expected: b.string, minify: b.any, targets: b.any }, + args: { source: b.string.allowStringObject, expected: b.string.allowStringObject, minify: b.any, targets: b.any }, returns: b.any, capability: "js", throws: true, diff --git a/src/install_jsc/hosted_git_info_jsc.bind.ts b/src/install_jsc/hosted_git_info_jsc.bind.ts index e461377c6fa2..46377b4cf3af 100644 --- a/src/install_jsc/hosted_git_info_jsc.bind.ts +++ b/src/install_jsc/hosted_git_info_jsc.bind.ts @@ -3,14 +3,14 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::hosted_git_info_jsc" }); export const parseUrl = b.fn({ - args: { url: b.string }, + args: { url: b.string.allowStringObject }, returns: b.string, capability: "heap", throws: true, }); export const fromUrl = b.fn({ - args: { url: b.string }, + args: { url: b.string.allowStringObject }, returns: b.any, capability: "heap", throws: true, diff --git a/src/install_jsc/npm_jsc.bind.ts b/src/install_jsc/npm_jsc.bind.ts index 96ab3f34b280..0af3d46ad690 100644 --- a/src/install_jsc/npm_jsc.bind.ts +++ b/src/install_jsc/npm_jsc.bind.ts @@ -18,7 +18,7 @@ export const architectureIsMatch = b.fn({ export const npmManifestTestHelpers = b.namespace({ parseManifest: { - args: { manifestFilename: b.string, registry: b.string }, + args: { manifestFilename: b.string.allowStringObject, registry: b.string.allowStringObject }, returns: b.any, capability: "heap", throws: true, diff --git a/src/jsc/bun_string_jsc.bind.ts b/src/jsc/bun_string_jsc.bind.ts index b0c1d32bc958..dbc08c507017 100644 --- a/src/jsc/bun_string_jsc.bind.ts +++ b/src/jsc/bun_string_jsc.bind.ts @@ -2,9 +2,9 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::bun_string_jsc" }); -export const escapeRegExp = b.fn({ args: { input: b.string }, returns: b.string.utf8, throws: true }); +export const escapeRegExp = b.fn({ args: { input: b.string.allowStringObject }, returns: b.string.utf8, throws: true }); export const escapeRegExpForPackageNameMatching = b.fn({ - args: { input: b.string }, + args: { input: b.string.allowStringObject }, returns: b.string.utf8, throws: true, }); diff --git a/src/runtime/api/cron.bind.ts b/src/runtime/api/cron.bind.ts index 2e7b7d4247cc..332a74a012e0 100644 --- a/src/runtime/api/cron.bind.ts +++ b/src/runtime/api/cron.bind.ts @@ -35,7 +35,7 @@ export const cron = b.fn({ export const remove = b.fn({ export: "Bun__Cron__remove", - args: { title: b.string }, + args: { title: b.string.allowStringObject }, expected: { title: "Bun.cron.remove() expects a string title" }, returns: b.any, capability: "js", @@ -44,7 +44,7 @@ export const remove = b.fn({ export const parse = b.fn({ export: "Bun__Cron__parse", - args: { expression: b.string, from: b.any, options: b.any }, + args: { expression: b.string.allowStringObject, from: b.any, options: b.any }, expected: { expression: "Bun.cron.parse() expects a string cron expression as the first argument" }, returns: b.f64.asDate.nullable, capability: "js", diff --git a/src/runtime/cli/pack_command.bind.ts b/src/runtime/cli/pack_command.bind.ts index b801d90f06ce..e8a356005f88 100644 --- a/src/runtime/cli/pack_command.bind.ts +++ b/src/runtime/cli/pack_command.bind.ts @@ -3,7 +3,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::cli::pack_command::bindings" }); export const readTarball = b.fn({ - args: { tarballPath: b.string }, + args: { tarballPath: b.string.allowStringObject }, returns: b.any, capability: "js", throws: true, diff --git a/src/runtime/dns_jsc/dns_internals.bind.ts b/src/runtime/dns_jsc/dns_internals.bind.ts index 84e0b77d8db1..3f195ad82154 100644 --- a/src/runtime/dns_jsc/dns_internals.bind.ts +++ b/src/runtime/dns_jsc/dns_internals.bind.ts @@ -78,12 +78,12 @@ export const globalLookupService = b.fn({ // `bun:internal-for-testing` export const seedCacheForTesting = b.fn({ ...method, - args: { hostname: b.string, addresses: b.any }, + args: { hostname: b.string.allowStringObject, addresses: b.any }, returns: b.any, rustPath: "crate::dns_jsc::internal::seed_cache_for_testing", }); export const isLocalhostNameForTesting = b.fn({ - args: { hostname: b.string }, + args: { hostname: b.string.allowStringObject }, returns: b.bool, rustPath: "crate::dns_jsc::internal::is_localhost_name_for_testing", }); @@ -95,7 +95,7 @@ export const isAllLoopbackOfOneFamilyForTesting = b.fn({ }); export const getaddrinfoErrorForTesting = b.fn({ ...method, - args: { code: b.string, hostname: b.string }, + args: { code: b.string.allowStringObject, hostname: b.string.allowStringObject }, returns: b.any, rustPath: "crate::dns_jsc::internal::getaddrinfo_error_for_testing", }); diff --git a/test/js/bun/cron/cron-parse.test.ts b/test/js/bun/cron/cron-parse.test.ts index f3dc7949f2c8..d4a2809fcd66 100644 --- a/test/js/bun/cron/cron-parse.test.ts +++ b/test/js/bun/cron/cron-parse.test.ts @@ -93,6 +93,12 @@ describe("Bun.cron.parse — invalid `from` argument", () => { expect(() => Bun.cron.parse("* * * * *", new Date(from))).toThrow("Invalid date value"); }); + test("takes a String object", () => { + const from = new Date("2026-06-15T00:00:30Z"); + // @ts-expect-error + expect(Bun.cron.parse(new String("* * * * *"), from)).toEqual(Bun.cron.parse("* * * * *", from)); + }); + test("accepts the Date range boundary", async () => { await using proc = Bun.spawn({ cmd: [ From 2355e410cfaee66741f90890177690827aaa89d0 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 00:34:51 +0000 Subject: [PATCH 08/22] Take a `BuildArtifact` in `URL.createObjectURL` as `main` does, free the list of test files before the exit, and update the `Bun.resolve` message in its test --- src/runtime/cli/test_command.rs | 2 ++ src/runtime/webcore/ObjectURLRegistry.bind.ts | 8 ++------ src/runtime/webcore/ObjectURLRegistry.rs | 15 ++++++++++++--- test/js/bun/resolve/resolve-test.js | 4 ++-- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/runtime/cli/test_command.rs b/src/runtime/cli/test_command.rs index 7d5dd0eed8cc..2171aead50c6 100644 --- a/src/runtime/cli/test_command.rs +++ b/src/runtime/cli/test_command.rs @@ -2758,6 +2758,8 @@ impl TestCommand { jest::Jest::RUNNER.write(None); } drop(reporter); + // `global_exit()` does not return, so nothing is dropped after it. + drop(all_test_files); { let vm_ptr: *mut VirtualMachine = vm; // SAFETY: `vm_ptr` reborrows the live `&mut VirtualMachine`; diff --git a/src/runtime/webcore/ObjectURLRegistry.bind.ts b/src/runtime/webcore/ObjectURLRegistry.bind.ts index 466b14d243bc..2400416cfba0 100644 --- a/src/runtime/webcore/ObjectURLRegistry.bind.ts +++ b/src/runtime/webcore/ObjectURLRegistry.bind.ts @@ -1,13 +1,9 @@ import * as b from "../../codegen/bindgen-lib.ts"; -import { Blob } from "./Blob.bind.ts"; export default b.module({ rustPath: "crate::webcore::object_url_registry" }); -export const createObjectURL = b.fn({ - args: { blob: Blob }, - expected: { blob: "createObjectURL expects a Blob object" }, - returns: b.string, -}); +/** `blob` is a `Blob` or a `BuildArtifact`. */ +export const createObjectURL = b.fn({ args: { blob: b.any }, returns: b.string, capability: "heap", throws: true }); /** Without an argument it throws `ERR_MISSING_ARGS`, as in Node. */ export const revokeObjectURL = b.fn({ args: { url: b.any.variadic }, capability: "js", throws: true, length: 1 }); diff --git a/src/runtime/webcore/ObjectURLRegistry.rs b/src/runtime/webcore/ObjectURLRegistry.rs index 08c2afc537fe..ec437350346f 100644 --- a/src/runtime/webcore/ObjectURLRegistry.rs +++ b/src/runtime/webcore/ObjectURLRegistry.rs @@ -2,7 +2,7 @@ use std::sync::OnceLock; use bun_collections::HashMap; use bun_core::strings; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope}; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{JSGlobalObject, JSValue, JsResult, UUID}; use bun_threading::Guarded; @@ -112,10 +112,19 @@ fn uuid_from_pathname(pathname: &[u8]) -> Option { UUID::parse(pathname).ok() } -pub(crate) fn create_object_url(blob: &Blob) -> bun_core::String { +pub(crate) fn create_object_url<'a>( + heap: &JsHeap<'a>, + blob: JsLocal<'a>, +) -> bindgen::Result<'a, bun_core::String> { + let Some(blob) = blob.to_js().as_class_ref::() else { + return Err(heap.invalid_arguments(format_args!("createObjectURL expects a Blob object"))); + }; let registry = ObjectURLRegistry::singleton(); let uuid = registry.register(VirtualMachine::get().as_mut(), blob); - bun_core::String::create_format(format_args!("blob:{}", uuid)) + Ok(bun_core::String::create_format(format_args!( + "blob:{}", + uuid + ))) } pub(crate) fn revoke_object_url(cx: &mut JsScope<'_>, url: &[JsLocal<'_>]) -> JsResult<()> { diff --git a/test/js/bun/resolve/resolve-test.js b/test/js/bun/resolve/resolve-test.js index c99bc53e1582..3ff36505976a 100644 --- a/test/js/bun/resolve/resolve-test.js +++ b/test/js/bun/resolve/resolve-test.js @@ -122,7 +122,7 @@ it("Bun.resolve throws TypeError when called without arguments", async () => { expect.unreachable("Should have thrown"); } catch (error) { expect(error).toBeInstanceOf(TypeError); - expect(error.message).toBe("Expected a specifier and a from path"); + expect(error.message).toBe("specifier must be a string"); expect(error.code).toBe("ERR_INVALID_ARG_TYPE"); } }); @@ -133,7 +133,7 @@ it("Bun.resolveSync throws TypeError when called without arguments", () => { expect.unreachable("Should have thrown"); } catch (error) { expect(error).toBeInstanceOf(TypeError); - expect(error.message).toBe("Expected a specifier and a from path"); + expect(error.message).toBe("specifier must be a string"); expect(error.code).toBe("ERR_INVALID_ARG_TYPE"); } }); From 417eda0fa64fd0ce76339389d76ec6a185818712 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 01:25:05 +0000 Subject: [PATCH 09/22] Type the arguments and results of the moved functions in their schemas, and add the types the generator lacked for them --- docs/project/bindgen.mdx | 58 +- src/codegen/bindgen-emit.ts | 118 ++- src/codegen/bindgen-lib-internal.ts | 332 ++++++- src/codegen/bindgen-lib.ts | 34 +- src/css_jsc/color_js.bind.ts | 56 +- src/css_jsc/color_js.rs | 364 +++---- src/css_jsc/css_internals.bind.ts | 50 +- src/css_jsc/css_internals.rs | 270 ++--- src/install_jsc/hosted_git_info_jsc.bind.ts | 15 +- src/install_jsc/hosted_git_info_jsc.rs | 82 +- src/install_jsc/npm_jsc.bind.ts | 14 +- src/install_jsc/npm_jsc.rs | 128 +-- src/js/internal-for-testing.ts | 19 + src/jsc/BytecodeOrderRecorder.bind.ts | 11 +- src/jsc/BytecodeOrderRecorder.rs | 30 +- src/jsc/ConsoleObject.rs | 38 +- src/jsc/Counters.bind.ts | 4 +- src/jsc/Counters.rs | 28 +- src/jsc/JSValue.rs | 11 +- src/jsc/bindgen.rs | 62 +- src/jsc/bindgen_test.bind.ts | 75 ++ src/jsc/bindgen_test.rs | 143 ++- src/jsc/bindings/Bindgen/IDLConvert.h | 71 ++ .../Bindgen/IDLConvertNativePromiseContext.h | 12 + src/jsc/bindings/Bindgen/IDLConvertNumbers.h | 22 + src/jsc/bindings/Bindgen/IDLTypes.h | 19 + src/jsc/event_loop.bind.ts | 14 +- src/jsc/event_loop.rs | 69 +- src/jsc/rare_data.bind.ts | 6 +- src/jsc/rare_data.rs | 20 +- src/jsc/resolver_jsc.bind.ts | 2 +- src/jsc/resolver_jsc.rs | 22 +- src/jsc/virtual_machine_exports.bind.ts | 7 +- src/jsc/virtual_machine_exports.rs | 22 +- src/md/root.rs | 74 +- src/patch_jsc/patch_testing.bind.ts | 4 +- src/patch_jsc/testing.rs | 107 +- src/runtime/api/BunObject.bind.ts | 52 +- src/runtime/api/BunObject.rs | 234 +++-- src/runtime/api/JSSourceMap.rs | 19 +- src/runtime/api/MarkdownObject.bind.ts | 127 ++- src/runtime/api/MarkdownObject.rs | 420 +++----- src/runtime/api/NativePromiseContext.rs | 4 +- src/runtime/api/crash_handler_jsc.bind.ts | 10 +- src/runtime/api/crash_handler_jsc.rs | 52 +- src/runtime/api/cron.bind.ts | 54 +- src/runtime/api/cron.rs | 158 +-- src/runtime/api/html_rewriter.rs | 33 +- .../api/html_rewriter_internals.bind.ts | 17 +- src/runtime/api/sourcemap.bind.ts | 8 +- src/runtime/bake/config/bake_plugins.bind.ts | 5 +- src/runtime/bake/config/plugins.rs | 15 +- .../bake/router/FrameworkRouter.bind.ts | 7 +- src/runtime/bake/router/js.rs | 36 +- src/runtime/bake/router/pattern.rs | 7 +- src/runtime/cli/pack_command.bind.ts | 34 +- src/runtime/cli/pack_command.rs | 126 +-- src/runtime/crypto/PBKDF2.rs | 274 ++--- src/runtime/dns_jsc/cares_jsc.bind.ts | 8 +- src/runtime/dns_jsc/cares_jsc.rs | 8 +- src/runtime/dns_jsc/dns.rs | 67 +- src/runtime/dns_jsc/dns_internals.bind.ts | 12 +- src/runtime/ffi/FFIObject.bind.ts | 15 +- src/runtime/ffi/FFIObject.rs | 50 +- src/runtime/ffi/ffi.bind.ts | 17 +- src/runtime/ffi/ffi_body.rs | 45 +- src/runtime/ipc.bind.ts | 3 +- src/runtime/ipc.rs | 9 +- src/runtime/ipc_host.bind.ts | 12 +- src/runtime/ipc_host.rs | 76 +- src/runtime/linear_fifo_testing.bind.ts | 2 +- src/runtime/linear_fifo_testing.rs | 5 +- src/runtime/node.rs | 4 +- src/runtime/node/Stat.bind.ts | 4 +- src/runtime/node/Stat.rs | 15 +- src/runtime/node/memory_pressure.bind.ts | 2 +- src/runtime/node/memory_pressure.rs | 5 +- src/runtime/node/node_cluster_binding.bind.ts | 14 +- src/runtime/node/node_cluster_binding.rs | 170 ++-- src/runtime/node/node_crypto_binding.bind.ts | 208 +++- src/runtime/node/node_crypto_binding.rs | 933 +++++++----------- src/runtime/node/node_fs_binding.bind.ts | 11 +- src/runtime/node/node_fs_binding.rs | 34 +- src/runtime/node/node_net_binding.bind.ts | 8 +- src/runtime/node/node_net_binding.rs | 13 +- src/runtime/node/node_quic_binding.bind.ts | 3 +- src/runtime/node/node_quic_binding.rs | 15 +- src/runtime/node/types.rs | 25 - src/runtime/node/util/validators.rs | 56 -- src/runtime/server/NodeHTTPResponse.bind.ts | 4 +- src/runtime/server/NodeHTTPResponse.rs | 22 +- src/runtime/server/RequestContext.bind.ts | 10 +- src/runtime/server/RequestContext.rs | 32 +- src/runtime/server/server_body.bind.ts | 5 +- src/runtime/server/server_body.rs | 18 +- src/runtime/shell/ParsedShellScript.bind.ts | 3 +- src/runtime/shell/shell_body.bind.ts | 10 +- src/runtime/shell/shell_body.rs | 44 +- src/runtime/socket/Listener.bind.ts | 8 +- src/runtime/socket/UpgradedDuplex.bind.ts | 4 +- src/runtime/socket/UpgradedDuplex.rs | 46 +- src/runtime/socket/socket_body.bind.ts | 62 +- src/runtime/socket/socket_body.rs | 276 ++---- src/runtime/socket/udp_socket.bind.ts | 1 + src/runtime/test_runner/BunTest.bind.ts | 17 +- .../test_runner/ScopeFunctions.bind.ts | 32 +- src/runtime/test_runner/ScopeFunctions.rs | 206 ++-- src/runtime/test_runner/bun_test.rs | 46 +- src/runtime/test_runner/jest.bind.ts | 9 +- src/runtime/test_runner/jest.rs | 33 +- .../test_runner/timers/FakeTimers.bind.ts | 11 +- src/runtime/test_runner/timers/FakeTimers.rs | 50 +- src/runtime/timer/Timer.bind.ts | 2 +- src/runtime/timer/Timer.rs | 34 +- src/runtime/webcore/Blob.bind.ts | 21 +- src/runtime/webcore/Blob.rs | 115 +-- src/runtime/webcore/ByteStream.bind.ts | 1 + src/runtime/webcore/Crypto.bind.ts | 17 +- src/runtime/webcore/Crypto.rs | 159 ++- src/runtime/webcore/FileSink.bind.ts | 5 +- src/runtime/webcore/FileSink.rs | 28 +- src/runtime/webcore/FormData.bind.ts | 24 +- src/runtime/webcore/FormData.rs | 103 +- src/runtime/webcore/ObjectURLRegistry.bind.ts | 14 +- src/runtime/webcore/ObjectURLRegistry.rs | 53 +- src/runtime/webcore/fetch.bind.ts | 5 +- src/runtime/webcore/fetch.rs | 51 +- .../webcore/fetch/FetchTasklet.bind.ts | 5 +- src/runtime/webcore/fetch/FetchTasklet.rs | 22 +- src/runtime/webcore/prompt.bind.ts | 15 +- src/runtime/webcore/prompt.rs | 87 +- src/runtime/webcore/s3/S3UploadStream.bind.ts | 5 +- src/runtime/webcore/s3/client.rs | 20 +- src/sourcemap_jsc/InternalSourceMap.bind.ts | 2 +- src/sourcemap_jsc/internal_jsc.rs | 8 +- src/sql_jsc/mysql.bind.ts | 43 +- src/sql_jsc/mysql/JSMySQLConnection.rs | 74 +- src/sql_jsc/mysql/JSMySQLQuery.rs | 36 +- src/sql_jsc/mysql/MySQLContext.rs | 6 +- src/sql_jsc/postgres.bind.ts | 44 +- src/sql_jsc/postgres/PostgresSQLConnection.rs | 113 ++- src/sql_jsc/postgres/PostgresSQLContext.rs | 8 +- src/sql_jsc/postgres/PostgresSQLQuery.rs | 32 +- src/sql_jsc/postgres/SASL.rs | 17 +- src/sql_jsc/shared/ConnectionCtorArgs.rs | 55 +- src/sql_jsc/shared/QueryCtorArgs.rs | 66 +- test/internal/bindgen.test.ts | 152 ++- test/js/bun/cron/cron-local-time.test.ts | 2 +- test/js/bun/md/md-render-callback.test.ts | 7 +- test/js/bun/test/describe.test.ts | 17 +- .../bun/util/socket-fault-injection.test.ts | 8 +- test/js/node/crypto/crypto-random.test.ts | 31 +- test/js/node/crypto/pbkdf2.test.ts | 34 +- test/js/node/crypto/scrypt.test.ts | 30 + 154 files changed, 4298 insertions(+), 4176 deletions(-) create mode 100644 src/jsc/bindings/Bindgen/IDLConvertNativePromiseContext.h diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index cbbb7726cb8f..caa10208773b 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -54,7 +54,7 @@ pub fn add<'a>(heap: &JsHeap<'a>, a: i32, b: i32) -> bindgen::Result<'a, i32> { `b.module` says where the file's functions are; an export named `requiredAndOptionalArg` is `required_and_optional_arg` there. A `rustPath` key on a function gives its full path instead. The arguments after the last that is not written -`.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull` or `.default()` are the ones +`.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull`, `.ifPassed` or `.default()` are the ones TypeScript lets a caller leave out. The function's `.length` is the number of the others, unless `length` sets it. @@ -82,7 +82,7 @@ To reach the function: `crate::generated_bindgen::cron::Bun__CronJob__onPromiseResolve`, for `JSFunction::create` and `JSValue::then`. `passThis: true` adds `this_value: JsLocal<'a>` after the capability, and `passCallee: true` adds `callee: JsLocal<'a>`, the -function that was called, after it. Neither they nor `export` go with overloads. +function that was called, after it. `b.value({ returns: b.i32 })` is for what `src/js` reads once: `$bindgen` calls the Rust function and gives what it returns, and no JS function is made. @@ -113,6 +113,8 @@ A namespace that can itself be called, like `Bun.hash`, names the function that `b.namespace({ wyhash: { .. }, crc32: { .. } }, { call: "wyhash" })`. The object is then a function named after the namespace. +An array in place of a function is its [overloads](#overloads). + ### What a function may do: `capability` What a function can do to the JS heap is a parameter it is given, so it is @@ -162,6 +164,12 @@ type that implements `bindgen::IntoError` (`bun_sys::Error`, `cx`. Such a function can return `JsResult`, and a plain `JSValue` under `returns: b.any`. +### Promises: `rejects` + +A function that returns a promise reports an error by rejecting one. With `rejects: true`, what the body +throws and what the conversion of an argument throws become the reason of a rejected promise +(`JSPromise::rejectedPromiseWithCaughtException`), so the body returns `Err` like any other. + ### Overloads `b.overloads(a, b, ..)` is one JS function with several signatures. Each names its @@ -174,6 +182,8 @@ export const read = b.overloads( ); ``` +`passThis`, `passCallee`, `export` and `rejects` are of the function, so every signature writes the same. + Use it when the type of one argument says what the others are. A union for each argument would accept combinations that mean nothing, and leave the body to sort them out. @@ -185,10 +195,10 @@ arguments from left to right: 2. An argument that was not passed is `undefined`. So `f(x)` and `f(x, undefined)` are the same call. It keeps the candidates where the argument can be left out, or if there are none, those whose type takes every kind. -3. A candidate with no argument at that position stays. +3. A candidate with no argument at that position takes only `undefined`. 4. If no candidate takes the argument, all of them stay, and the next argument decides. 5. When the arguments run out, the candidate written first wins. Its conversion reports - whatever is wrong. So of two signatures that start the same, the longer one goes first. + whatever is wrong. Choosing converts nothing and runs no JavaScript. A signature that no call can reach is an error when the schema is loaded. @@ -200,7 +210,7 @@ that is an integer in range, and nothing else. A failed strict or `.validateInteger` conversion, or a `.loose` one of an integer, throws `ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` naming the argument or the dictionary member; `.wrap`, `.enforceRange` and the other `.loose` conversions throw what WebIDL does. An argument that was not passed is `undefined` and is converted like -any other: nothing looks at how many there are, except a body that takes `b.any.variadic`. +any other: nothing looks at how many there are, except `.ifPassed` and a body that takes `b.any.variadic`. | Schema | Rust, as an argument | Takes | | ------------------------------------------------ | -------------------------------- | ------------------------------------------------------------- | @@ -208,6 +218,7 @@ any other: nothing looks at how many there are, except a body that takes `b.any. | `b.u8` `b.u16` `b.u32` `b.u64` `b.usize` `b.i32` | the same | a number that is an integer in range; see below | | `b.f64` | `f64` | a number; `.loose` is `ToNumber`; see below | | `b.string` | `&bun_core::String` | a string; `.loose` is `ToString` | +| `b.string.owned` | `bun_core::String` | as `b.string` | | `b.string.utf8` | `&'a [u8]` | the same, and a `String` object | | `b.string.utf8.owned` | `bun_core::Utf8Bytes<'static>` | as `b.string.utf8` | | `b.bufferSource` | `JsLocal<'a, BufferSource>` | a typed array, `DataView` or `ArrayBuffer` that is not shared | @@ -215,9 +226,14 @@ any other: nothing looks at how many there are, except a body that takes `b.any. | `b.bytes(options)` | `&'a [u8]`, and others | see [Bytes](#bytes) | | `b.blob` | `ExternalShared` | a `Blob` | | `b.callable` | `JsLocal<'a, Callable>` | a function, or a proxy of one | +| `b.object` | `JsLocal<'a, JSObject>` | an object | +| `b.jsArray` | `JsLocal<'a, JSArray>` | an array, as itself | | `b.any` | `JsLocal<'a>` | anything | | `b.any.variadic` | `&'a [JsLocal<'a>]` | this argument and every one after it; only the last argument | | `b.null` | `()` | `null` or `undefined`; only in a union | +| `b.undefined` | `()` | `undefined`; only in a union | +| `b.promisePtr("crate::T")` | `*mut T` | the context of a promise reaction; see below | +| `b.nativePromiseContext("crate::T")` | `Option>` | the same, when it is a `NativePromiseContext` cell | | `b.array(T)` | `Vec` | an array; `T` holds no JS value, which the GC would not see | | `b.enumeration(values)` | a generated `enum` | one of the strings | | `b.dictionary(members)` | `&` a generated `struct` | an object that is not an array | @@ -229,19 +245,28 @@ types: each is called what it is exported as, and has to be exported. It is used schemas of the crate it is generated into. A member or an alternative also names a C++ field, so one that is a C++ keyword is refused: a member takes a `rustName`. +Only the promise calls a reaction, with the context it was given, so nothing tests that argument. +`b.promisePtr` is the `*mut T` that `JSValue::then` was given. `b.nativePromiseContext` is what `take` of the cell +returns: `None` if the pointer was taken back before the promise settled. A generic `T` is written as its +instantiation. + These come last, in this order (`b.any` already takes `undefined` and `null`, so only `.emptyStringAsNull` applies to it): - `.optional`: `undefined` means absent; `Option`. - `.nullable`: `null` or `undefined` means absent; `Option`. - `.falsyAsNull`: every falsy value means absent; `Option`. - `.emptyStringAsNull`: `undefined`, `null` or `""` means absent, as for `JSValue::get_truthy`; `Option`. +- `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` both work and differ, like `alert`. It goes on every type, and only on an argument. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. +`.withValue` on an argument also gives the body the argument itself, `(T, JsLocal<'a>)`: for one that reads the +internal fields of an instance, or returns the argument when there is nothing to change. + As results, `b.string.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything that is `IntoIterator`. `b.bufferSource` as a result is what `heap.create_uninitialized_uint8_array(length)`, `heap.create_uint8_array(bytes)`, `heap.create_buffer(bytes)`, `heap.create_uint8_array_from_box(bytes)` or -`heap.create_buffer_from_box(bytes)` made. The last two take over the allocation, and the two +`heap.create_buffer_from_box(bytes)` made, and `b.jsArray` what `heap.create_array(items, to_js::u8)` made. The last two take over the allocation, and the two before them copy. These are results only: @@ -261,7 +286,7 @@ object is `heap.create_empty_object()`. A BigInt is refused unless the schema says `.allowBigInt`, which is for an argument whose type is `number | bigint`. `b.f64.loose.allowBigInt` is `ToNumeric`, and a BigInt becomes the -double nearest to it. On an integer: +double nearest to it. `b.f64.allowDate` also takes a `Date`, as its time value, which is NaN for an invalid one. On an integer: - `.allowBigInt`: a BigInt in the same range too. It is how a `b.u64` above 253 gets in. It goes with the strict conversion and with `.loose`; the others refuse a BigInt. - `.loose`: `ToNumber` first, or `ToNumeric` with `.allowBigInt`, then as strict as the default. @@ -283,8 +308,8 @@ its `toString` runs. `bun_core::Utf8Bytes<'static>`, which shares the JS string's buffer when it is 8-bit and ASCII and is a transcoded copy otherwise. -`b.string` shares the JS string's storage. As an argument it is a `&bun_core::String`, which a -body that keeps it clones. Inside `.optional`, a union or an array, and as the value of a setter, +`b.string` shares the JS string's storage. As an argument it is a `&bun_core::String`. A +body that keeps it takes `b.string.owned`, which comes after `.loose` or `.allowStringObject`. Inside `.optional`, a union or an array, and as the value of a setter, it is held by value. The string belongs to the JS thread: see "Cross-thread string hazards" in `src/CLAUDE.md` before sending one anywhere. `b.string.allowStringObject` also takes a `String` object, as `.utf8` does. @@ -304,7 +329,8 @@ export const BracesOptions = b.dictionary({ Each member is read, then converted, in declaration order, once. A member whose type does not accept `undefined` is required. A member can be written -`{ type, rustName, altNames }` to rename its field or to accept other spellings. +`{ type, rustName, altNames, expected }` to rename its field, to accept other spellings, or to report a wrong type as +a function's `expected` does. A dictionary whose members can all be absent implements `Default`. As an argument, with `.default()` or without, the struct stays in the frame of the thunk and the body borrows it: `options: &BracesOptions`. Inside `.optional`, a union or an array it is held by value. @@ -325,6 +351,7 @@ The second argument of `b.dictionary` takes: | `allowArray` | an array is read like any other object, not refused | | `nonObjectAsEmpty` | a value that is not an object gives every member its default; with `"typeof"` so does a function, as in Node's `if (typeof options !== "object") options = null`; every member must be optional or have a default | | `fromJS` | also generates `Name::from_js(global, value)`, for a body that has a `JSValue`; one that holds JS values is lent to a closure, `Name::from_js(global, value, \|converted\| ..)` | +| `reportedAs` | what an error calls the dictionary, which defaults to the name of the type: `"options"` makes a member `options.level`, and `""` makes it `level`, as Node's validators are given it | | `nullPrototype` | as a result, the object has no prototype, and the inline room of `Object.create(null)` | ### Enumerations @@ -333,10 +360,13 @@ The second argument of `b.dictionary` takes: export const Formatter = b.enumeration(["highlight-javascript", "highlight-javascript-redacted", "escape-powershell"]); ``` -generates `enum Formatter { HighlightJavascript, .. }`. An entry can be a list of +generates `enum Formatter { HighlightJavascript, .. }`; capitals in a row are a word, so `"ECONNRESET"` is `Econnreset`. An entry can be a list of spellings; the first is the one returned to JavaScript. It also takes a `String` object, except in a union. Returning an enumeration allocates nothing after the first time. +Values that make no identifier, or the same one, are written as a record, whose keys are the variants: +`b.enumeration({ Rgb: "rgb", RgbArray: "[rgb]" })`. + ### Unions and kinds Where there is a choice, between the alternatives of a union or between overloads, @@ -347,7 +377,8 @@ in play apart: | ---------------------------------- | -------------------------------------------------------------------------------- | | the bits of the value | `undefined`, `null`, boolean, number | | the type byte in the cell's header | string, BigInt, array, `ArrayBuffer`, typed array or `DataView`, `String` object | -| the cell's `ClassInfo` | an instance of a class | +| the cell's type info | what can be called | +| the cell's `ClassInfo` | an instance of a class, a `Date` | A strict type takes the kinds its row under [Types](#types) says. A type that coerces or reports its own error takes every kind: `.loose`, `.wrap`, `.enforceRange`, @@ -357,7 +388,8 @@ reports its own error takes every kind: `.loose`, `.wrap`, `.enforceRange`, alternative then converts it, and reports what is wrong with it: `1.5` given to `b.union({ integer: b.i32, text: b.string })` "must be an integer", not "must be of type integer or string". The buffer types refuse some values of their kinds (a shared buffer); those go on to the next alternative that takes -the kind and does not refuse the same. A value no alternative takes throws `ERR_INVALID_ARG_TYPE` listing them all. +the kind and does not refuse the same. So does a value that is not an object from a dictionary or `b.object`, unless the dictionary is the last +to take it, which then reports it. A value no alternative takes throws `ERR_INVALID_ARG_TYPE` listing them all. So the order only matters between alternatives that overlap, and one that takes every kind goes last. An alternative that can never be chosen is an error when the schema is diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 5affce21ca37..6e1442a82e6b 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -22,8 +22,10 @@ import { NamedType, type NamespaceDefinition, OptionalType, + isIfPassed, type Origin, type OverloadDispatch, + OwnedStringType, OwnedUtf8Type, rustIdent, type RustScope, @@ -162,6 +164,7 @@ const kindTests: Record, { ofValue?: (value: string) array: { ofCell: "JSC::isJSArray(cell)" }, arrayBuffer: { ofCell: "cell->inherits()" }, arrayBufferView: { ofCell: "cell->inherits()" }, + callable: { ofCell: "cell->isCallable()" }, }; /** @@ -232,7 +235,15 @@ function cppConvertToAlternative(type: UnionType, candidates: readonly number[]) .map(i => { const { name, type: alternative } = type.alternatives[i]; const member = `result.${name}`; - if (alternative.refuses) + const convert = (target: string) => alternative.cppConvertInto(target, "value", "ctx", "", ""); + if (alternative instanceof DictionaryType && i !== candidates.at(-1)) + return `if (value.isObject()) { + result.tag = ${i}; + scope.release(); +${indent(cppEmplace(member, convert), 4)} + return; +}`; + if (alternative.refuses && !(alternative instanceof DictionaryType)) return `if (auto converted = Bun::tryConvertIDL<${alternative.idlType}>(*globalObject, value, ctx)) { ${member} = WTF::move(*converted); result.tag = ${i}; @@ -240,7 +251,6 @@ function cppConvertToAlternative(type: UnionType, candidates: readonly number[]) } RETURN_IF_EXCEPTION(scope, void());`; if (alternative instanceof UnitType) return `result.tag = ${i};\nreturn;`; - const convert = (target: string) => alternative.cppConvertInto(target, "value", "ctx", "", ""); return `result.tag = ${i}; scope.release(); ${alternative.convertsInPlace ? cppEmplace(member, convert) : convert(member)} @@ -258,7 +268,9 @@ function cppHeaderForUnion(type: UnionType): string { .join(", "); const { outcomes, code } = cppChooseByKind("value", type.alternativesByKind); const mayMatchNone = (candidates: readonly number[]) => - candidates.length === 0 || !!alternatives[candidates.at(-1)!].type.refuses; + candidates.length === 0 || + (!!alternatives[candidates.at(-1)!].type.refuses && + !(alternatives[candidates.at(-1)!].type instanceof DictionaryType)); const cases = outcomes.flatMap((candidates, index) => candidates.length === 0 ? [] @@ -478,6 +490,31 @@ template<> std::optional<${type.cpp}> WebCore::parseEnumeration<${type.cpp}>(JSC const cppStaticContext = (variable: string, context: string) => `[[maybe_unused]] static constexpr auto ${variable} = Bun::Bindgen::${context};`; +/** Declares in `lines` what reports the `i`th argument or member, `value`, and returns the C++ expression for it. */ +function cppContext( + lines: string[], + i: number, + name: string, + expected: readonly string[] | string | undefined, + value: string, +): string { + if (typeof expected === "string") { + lines.push( + cppStaticContext(`ctx${i}`, `ArgumentMessageContext { ${asciiLiteral(name)}, ${asciiLiteral(expected)} }`), + ); + } else if (expected) { + lines.push( + `static constexpr WTF::ASCIILiteral expected${i}[] = { ${expected.map(asciiLiteral).join(", ")} };`, + cppStaticContext(`nodeArgument${i}`, `NodeArgument { ${asciiLiteral(name)}, expected${i} }`), + ); + // It holds the value, so it is made where a conversion takes it. + return `Bun::Bindgen::asLvalue(Bun::Bindgen::NodeArgumentContext { nodeArgument${i}, ${value} })`; + } else { + lines.push(cppStaticContext(`ctx${i}`, `LiteralConversionContext { ${asciiLiteral(name)} }`)); + } + return `ctx${i}`; +} + function cppSourceForDictionary(type: DictionaryType): string { if (!type.accepted) return ""; let nameIndex = 0; @@ -485,22 +522,23 @@ function cppSourceForDictionary(type: DictionaryType): string { const [first, ...others] = member.names.map( () => `members.get(${nameIndex++});\nRETURN_IF_EXCEPTION(scope, void());`, ); - const qualified = `${type.name}.${member.name}`; - const lines = [ - cppStaticContext(`ctx${i}`, `LiteralConversionContext { ${asciiLiteral(qualified)} }`), + const qualified = type.reportedAs ? `${type.reportedAs}.${member.name}` : member.name; + const lines: string[] = []; + const ctx = cppContext(lines, i, qualified, member.expected, `value${i}`); + lines.push( `JSC::JSValue value${i} = ${first}`, ...others.map(read => `if (value${i}.isUndefined()) {\n${indent(`value${i} = ${read}`, 4)}\n}`), - ]; + ); if (!member.type.takesUndefined) lines.push(`if (value${i}.isUndefined()) [[unlikely]] { - ctx${i}.throwRequired(*globalObject, scope); + ${ctx}.throwRequired(*globalObject, scope); return; }`); lines.push( member.type.cppConvertInto( `result.${member.fieldName}`, `value${i}`, - `ctx${i}`, + ctx, qualified, "RETURN_IF_EXCEPTION(scope, void());", ), @@ -514,7 +552,7 @@ template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalOb auto* globalObject = &lexicalGlobalObject; auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - ${cppStaticContext("ctx", `LiteralConversionContext { ${asciiLiteral(type.name)} }`)} + ${cppStaticContext("ctx", `LiteralConversionContext { ${asciiLiteral(type.reportedAs || type.name)} }`)} auto* object = value.getObject(); ${ type.nonObjectAsEmpty @@ -617,20 +655,12 @@ function cppCallBody( return; } const { holder, value } = cppHeldValue(type, `argument${i}`); - lines.push(`${holder} argument${i} = callFrame->argument(${i});`); - let ctx = `ctx${i}`; - if (typeof expected === "string") { - lines.push(cppStaticContext(ctx, `ArgumentMessageContext { ${asciiLiteral(name)}, ${asciiLiteral(expected)} }`)); - } else if (expected) { - lines.push( - `static constexpr WTF::ASCIILiteral expected${i}[] = { ${expected.map(asciiLiteral).join(", ")} };`, - cppStaticContext(`nodeArgument${i}`, `NodeArgument { ${asciiLiteral(name)}, expected${i} }`), - ); - // It holds the argument, so it is made where a conversion takes it. - ctx = `Bun::Bindgen::asLvalue(Bun::Bindgen::NodeArgumentContext { nodeArgument${i}, ${value} })`; - } else { - lines.push(cppStaticContext(ctx, `LiteralConversionContext { ${asciiLiteral(name)} }`)); - } + lines.push( + isIfPassed(type) + ? `${holder} argument${i} = callFrame->argumentCount() > ${i} ? callFrame->uncheckedArgument(${i}) : JSC::JSValue();` + : `${holder} argument${i} = callFrame->argument(${i});`, + ); + const ctx = cppContext(lines, i, name, expected, value); readLast.push(...type.cppReadLast(`converted${i}`, value)); lines.push(...cppDeclareConverted(type, `converted${i}`, value, ctx, name, "RETURN_IF_EXCEPTION(scope, {});")); }); @@ -1171,17 +1201,43 @@ function cppHostFunction( fn: FunctionDefinition, symbol: (signature: Signature) => string, takeThis = "", +): string { + const defineHostFunction = `${fn.exportName ? "BUN" : "JSC"}_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))`; + if (fn.signatures[0].rejects) { + const throwing = `${name}OrThrow`; + return `${cppHostFunctionBody(`static JSC::EncodedJSValue ${throwing}(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame)`, throwing, fn, symbol, takeThis)} +${defineHostFunction} +{ +${cppPrologue} + auto result = ${throwing}(globalObject, callFrame); + if (scope.exception()) [[unlikely]] + return JSC::JSValue::encode(JSC::JSPromise::rejectedPromiseWithCaughtException(globalObject, scope)); + return result; +}`; + } + return cppHostFunctionBody(defineHostFunction, name, fn, symbol, takeThis); +} + +/** `define`, then the braces. Its overloads are named after `name`. */ +function cppHostFunctionBody( + define: string, + name: string, + fn: FunctionDefinition, + symbol: (signature: Signature) => string, + takeThis: string, ): string { const leading = takeThis ? ["thisObject"] : cppLeadingOfFunction(fn); if (fn.signatures.length === 1) { - return `${fn.exportName ? "BUN" : "JSC"}_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) + return `${define} { ${cppPrologue} ${takeThis}${cppCallBody(fn.signatures[0], symbol(fn.signatures[0]), leading)} }`; } + // An overload reads `this` and the callee from the frame. + const passed = takeThis ? leading : []; const overloads = fn.signatures.map( - signature => `static JSC::EncodedJSValue ${name}${overloadSuffix(fn, signature)}(${["JSC::JSGlobalObject* globalObject", "JSC::CallFrame* callFrame", ...leading.map(argument => `JSC::JSCell* ${argument}`)].join(", ")}) + signature => `static JSC::EncodedJSValue ${name}${overloadSuffix(fn, signature)}(${["JSC::JSGlobalObject* globalObject", "JSC::CallFrame* callFrame", ...passed.map(argument => `JSC::JSCell* ${argument}`)].join(", ")}) { ${cppPrologue} ${cppCallBody(signature, symbol(signature), leading)} @@ -1189,7 +1245,7 @@ ${cppCallBody(signature, symbol(signature), leading)} ); const cppDispatch = (dispatch: OverloadDispatch): string => { if (dispatch instanceof Signature) - return `RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, dispatch)}(${["globalObject", "callFrame", ...leading].join(", ")}));`; + return `RELEASE_AND_RETURN(scope, ${name}${overloadSuffix(fn, dispatch)}(${["globalObject", "callFrame", ...passed].join(", ")}));`; const argument = `argument${dispatch.argument}`; const choice = `choice${dispatch.argument}`; const { outcomes, code, condition } = cppChooseByKind(argument, dispatch.byKind, choice); @@ -1205,7 +1261,7 @@ ${[...cases.slice(1), cases[0]].join("\n")} }`; }; return `${overloads.join("\n")} -JSC_DEFINE_HOST_FUNCTION(${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +${define} { ${cppPrologue} ${takeThis}${indent(cppDispatch(fn.dispatch), 4)} @@ -1251,6 +1307,7 @@ export function cppSource(file: SchemaFile): string { const accepted = file.types.filter(type => type instanceof DictionaryType && type.accepted) as DictionaryType[]; if (accepted.length > 0) headers.add("Bindgen/DictionaryMemberReader.h"); const systemHeaders = ["JavaScriptCore/JSFunction.h"]; + if (file.functions.some(fn => fn.signatures[0].rejects)) systemHeaders.push("JavaScriptCore/JSPromise.h"); if (file.types.some(type => type instanceof ClassType)) systemHeaders.unshift("JavaScriptCore/HeapAnalyzer.h"); if (accepted.some(type => !type.allowArray)) systemHeaders.unshift("JavaScriptCore/ArrayConstructor.h"); if (file.namespaces.length > 0) systemHeaders.push("JavaScriptCore/ObjectConstructor.h"); @@ -1343,7 +1400,8 @@ function rustArgumentName(arg: { name: string }): string { /** A dictionary or a string stays in the frame of the thunk, and the body borrows it. */ function rustBorrow(type: Type) { const inner = type instanceof DefaultType ? type.inner : type; - const isString = inner instanceof StringType && !(inner instanceof OwnedUtf8Type); + const isString = + inner instanceof StringType && !(inner instanceof OwnedUtf8Type) && !(inner instanceof OwnedStringType); return inner instanceof DictionaryType || isString ? "&" : ""; } const rustExternParameters = (signature: Signature, scope: RustScope) => @@ -1467,7 +1525,7 @@ ${type.alternatives ${type.alternatives .map(a => a.type instanceof UnitType - ? ` Self::${a.variant} => Ok(bindgen::JsLocal::NULL),` + ? ` Self::${a.variant} => Ok(bindgen::JsLocal::${a.type.unit === "null" ? "NULL" : "UNDEFINED"}),` : ` Self::${a.variant}(value) => ${a.type.rustToJS("value", scope)},`, ) .join("\n")} diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 2395fdc34b6f..88102308c782 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -97,6 +97,7 @@ export type ValueKind = | "array" | "arrayBuffer" | "arrayBufferView" + | "callable" | JSClass; /** The instances of a C++ `JSCell` class. */ @@ -106,6 +107,12 @@ export interface JSClass { type ValueKinds = readonly ValueKind[] | "any"; +const dateInstance: JSClass = { cpp: "JSC::DateInstance" }; + +const primitiveKinds: readonly (ValueKind | "other")[] = ["undefined", "null", "boolean", "number", "bigint", "string"]; +/** What a type that takes any object `refuses`. */ +const nonObjects = "what is not an object"; + const takes = (type: Type, kind: ValueKind | "other") => type.valueKinds === "any" || (kind !== "other" && type.valueKinds.includes(kind)); @@ -128,6 +135,10 @@ function alternativesByKind(alternatives: readonly Type[]): Map= 0; i--) if (alternatives[list[i]].refuses === nonObjects) list.splice(i, 1); + } if (!lists.has(`${list}`)) lists.set(`${list}`, list); return [kind, lists.get(`${list}`)!]; }), @@ -168,6 +179,14 @@ export abstract class Type { get emptyStringAsNull(): OptionalType { return new OptionalType(this, "emptyStringAsNull"); } + /** Absent only if the call has no such argument: `undefined` is converted like any other value. Only for an argument. */ + get ifPassed(): OptionalType { + return new OptionalType(this, "ifPassed"); + } + /** Also the argument itself: `(T, JsLocal<'a>)`. Only for an argument. */ + get withValue(): WithValueType { + return new WithValueType(this); + } /** Value to use when absent. Only for arguments and dictionary members. */ default(value: unknown): DefaultType { return new DefaultType(this, value); @@ -194,7 +213,7 @@ export abstract class Type { get refuses(): string | null { return null; } - /** Written `.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull` or `.default()`: as an argument with only such after it, it is one a caller leaves out. */ + /** Written `.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull`, `.ifPassed` or `.default()`: as an argument with only such after it, it is one a caller leaves out. */ get isOptional(): boolean { return false; } @@ -513,7 +532,18 @@ export const integers = { export class DoubleType extends PrimitiveType { constructor(idlType = "Bun::Bindgen::IDLStrictDouble", valueKinds: ValueKinds = ["number"]) { - super(idlType, valueKinds === "any" ? "any" : "number", "f64", 8, valueKinds); + super( + idlType, + valueKinds === "any" ? "any" : valueKinds.includes(dateInstance) ? "number | Date" : "number", + "f64", + 8, + valueKinds, + ); + } + /** A `Date` too, as its time value, which is NaN for an invalid one. */ + get allowDate(): DoubleType { + assert(this.idlType === "Bun::Bindgen::IDLStrictDouble", ".allowDate follows b.f64"); + return new DoubleType("Bun::Bindgen::IDLStrictDoubleOrDate", ["number", dateInstance]); } /** As a result: a `Date` with that time value. */ get asDate(): DateResultType { @@ -551,7 +581,7 @@ export class UnitType extends Type { constructor(unit: "null" | "undefined") { super(); this.unit = unit; - this.idlType = unit === "null" ? "Bun::Bindgen::IDLStrictNull" : ""; + this.idlType = unit === "null" ? "Bun::Bindgen::IDLStrictNull" : "Bun::Bindgen::IDLStrictUndefined"; } get tsType(): string { return this.unit === "null" ? "null | undefined" : "undefined"; @@ -617,6 +647,10 @@ export class StringType extends Type { get utf8(): Utf8Type { return new Utf8Type(this.valueKinds === "any"); } + /** As an argument: `bun_core::String` by value, for a body that keeps it, instead of `&bun_core::String`. */ + get owned(): OwnedStringType { + return new OwnedStringType(this.idlType, this.valueKinds); + } /** As a result: the JS string, which the body made. */ get local(): LocalStringResultType { assert(this.idlType === "Bun::Bindgen::IDLStrictString", ".local follows b.string"); @@ -663,8 +697,26 @@ export class LocalStringResultType extends ResultType { } } +export class OwnedStringType extends StringType { + get loose(): never { + throw new TypeError("write b.string.loose.owned"); + } + get allowStringObject(): never { + throw new TypeError("write b.string.allowStringObject.owned"); + } + get utf8(): never { + throw new TypeError("write b.string.utf8.owned"); + } + get owned(): never { + throw new TypeError("already .owned"); + } +} + /** UTF-8 to keep: the JS string's own buffer, shared, when it is 8-bit ASCII, otherwise a transcoded copy. */ export class OwnedUtf8Type extends StringType { + get owned(): never { + throw new TypeError("already .owned"); + } get loose(): never { throw new TypeError("write b.string.loose.utf8.owned"); } @@ -958,7 +1010,10 @@ export class BufferSourceType extends Type { /** A function, or a proxy of one, for the body to call or to store. */ export class CallableType extends Type { readonly idlType = "Bun::Bindgen::IDLRawCallable"; - readonly tsType = "(...args: any[]) => any"; + readonly tsType = "((...args: any[]) => any)"; + get valueKinds(): ValueKinds { + return ["callable"]; + } get holdsJSValue(): boolean { return true; } @@ -982,6 +1037,55 @@ export class CallableType extends Type { } } +/** A JS value of some kind, as itself. */ +export class LocalType extends Type { + readonly idlType: string; + readonly tsType: string; + readonly #marker: string; + readonly #valueKinds: ValueKinds; + readonly #refuses: string | null; + constructor(idlType: string, tsType: string, marker: string, valueKinds: ValueKinds, refuses: string | null) { + super(); + this.idlType = idlType; + this.tsType = tsType; + this.#marker = marker; + this.#valueKinds = valueKinds; + this.#refuses = refuses; + } + get valueKinds(): ValueKinds { + return this.#valueKinds; + } + get refuses(): string | null { + return this.#refuses; + } + get nullRepresentation() { + return "emptyValue" as const; + } + get holdsJSValue(): boolean { + return true; + } + get returnsValueUnchanged(): boolean { + return true; + } + layout(): Layout { + return { size: 8, align: 8 }; + } + rustExternType(): string { + return "JSValue"; + } + rustType(): string { + return `bindgen::JsLocal<'a, ${this.#marker}>`; + } + rustFromExtern(expr: string): string { + return `bindgen::JsLocal::from_extern(${expr}, heap)`; + } + rustToJS(value: string): string { + return `bindgen::to_js::any(heap, ${value})`; + } +} +export const objectType = new LocalType("Bun::Bindgen::IDLRawObject", "object", "bun_jsc::JSObject", "any", nonObjects); +export const jsArrayType = new LocalType("Bun::Bindgen::IDLRawArray", "any[]", "bun_jsc::JSArray", ["array"], null); + export class AnyType extends Type { readonly idlType = "Bun::Bindgen::IDLRawAny"; readonly tsType = "any"; @@ -1109,16 +1213,69 @@ export class RefType extends Type { } } +/** The `*mut T` that `JSValue::then` gave a promise reaction as its context. Only the promise calls a reaction, so nothing tests the value. */ +export class PromisePtrType extends Type { + readonly tsType = "never"; + readonly pointee: string; + constructor(pointee: string) { + super(); + this.pointee = pointee; + } + get idlType(): string { + return "Bun::Bindgen::IDLPromisePtr"; + } + get conversionThrows(): boolean { + return false; + } + layout(): Layout { + return { size: 8, align: 8 }; + } + /** Untyped, as for `RefType`. */ + rustExternType(): string { + return "*mut core::ffi::c_void"; + } + rustType(): string { + return `*mut ${this.pointee}`; + } + rustFromExtern(expr: string): string { + return `${expr}.cast::<${this.pointee}>()`; + } + rustToJS(): never { + throw new TypeError("the context of a promise reaction is only an argument"); + } +} + +/** As `PromisePtrType`, for the `NativePromiseContext` cell that `JSValue::then_with_value` gave the reaction: what `take` hands back. */ +export class NativePromiseContextType extends PromisePtrType { + get idlType(): string { + return "Bun::Bindgen::IDLNativePromiseContext"; + } + getHeaders(out: Set): void { + out.add("Bindgen/IDLConvertNativePromiseContext.h"); + } + rustType(): string { + return `Option>`; + } + rustFromExtern(expr: string): string { + return `core::ptr::NonNull::new(${super.rustFromExtern(expr)})`; + } +} + +const isNativePromiseContext = (type: Type) => type instanceof NativePromiseContextType; + // ───────────────────────────── combinators ───────────────────────────── export class OptionalType extends Type { readonly inner: Type; - readonly modifier: "optional" | "nullable" | "falsyAsNull" | "emptyStringAsNull"; + readonly modifier: "optional" | "nullable" | "falsyAsNull" | "emptyStringAsNull" | "ifPassed"; constructor(inner: Type, modifier: OptionalType["modifier"]) { super(); assert(!(inner instanceof OptionalType), "already optional"); assert(!(inner instanceof DefaultType), "put .default() last"); - if (inner instanceof AnyType) { + assert(!(inner instanceof WithValueType), "put .withValue last"); + if (modifier === "ifPassed") { + // Every type: what it does with `undefined` is its own. + } else if (inner instanceof AnyType) { assert(modifier === "emptyStringAsNull", "b.any already accepts undefined and null"); } else { assert(!inner.takesUndefined, `${inner.tsType} already takes undefined`); @@ -1131,12 +1288,12 @@ export class OptionalType extends Type { this.modifier = modifier; } get idlType(): string { - assert(!(this.inner instanceof RangeType), "a range cannot be absent; give it a .default()"); const template = { optional: "WebCore::IDLOptional", nullable: "WebCore::IDLNullable", falsyAsNull: "Bun::Bindgen::IDLFalsyAsNull", emptyStringAsNull: "Bun::Bindgen::IDLEmptyStringAsNull", + ifPassed: "Bun::Bindgen::IDLNotPassedAsNull", }[this.modifier]; return `${template}<${this.inner.idlType}>`; } @@ -1169,15 +1326,24 @@ export class OptionalType extends Type { cppReadLast(target: string, value: string): string[] { return this.inner.cppReadLast(target, value); } - /** What converts in place is null when default-constructed. A pointer to a class instance, which does not, is not. */ - cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { - if (!this.convertsInPlace) return super.cppConvertInto(target, value, ctx, name, check); - const present = { + #cppIsPresent(value: string): string { + return { optional: `!${value}.isUndefined()`, nullable: `!${value}.isUndefinedOrNull()`, falsyAsNull: `${value}.toBoolean(globalObject)`, emptyStringAsNull: `!Bun::Bindgen::isUndefinedOrNullOrEmptyString(${value})`, + ifPassed: `!${value}.isEmpty()`, }[this.modifier]; + } + /** The converter of a range takes the name, which the converter of `idlType` does not have. */ + cppConvert(value: string, ctx: string, name: string): string { + if (!(this.inner instanceof RangeType)) return super.cppConvert(value, ctx, name); + return `${this.#cppIsPresent(value)} ? std::optional { ${this.inner.cppConvert(value, ctx, name)} } : std::nullopt`; + } + /** What converts in place is null when default-constructed. A pointer to a class instance, which does not, is not. */ + cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { + if (!this.convertsInPlace) return super.cppConvertInto(target, value, ctx, name, check); + const present = this.#cppIsPresent(value); const convert = (slot: string) => this.inner.cppConvertInto(slot, value, ctx, name, check); const body = this.inner.nullRepresentation === "optional" ? cppEmplace(target, convert) : convert(target); return `if (${present}) {\n${indent(body, 4)}\n}`; @@ -1226,6 +1392,79 @@ export class OptionalType extends Type { } } +export class WithValueType extends Type { + readonly inner: Type; + constructor(inner: Type) { + super(); + assert(!inner.passesValue, "it already holds the argument"); + assert(!inner.returnsValueUnchanged, "it is the argument"); + assert(!isIfPassed(inner), ".ifPassed has no argument to give when it is absent"); + this.inner = inner; + } + get idlType(): string { + return this.inner.idlType; + } + get tsType(): string { + return this.inner.tsType; + } + get passesValue(): boolean { + return true; + } + get holdsJSValue(): boolean { + return true; + } + get takesUndefined(): boolean { + return this.inner.takesUndefined; + } + get isOptional(): boolean { + return this.inner.isOptional; + } + get valueKinds(): ValueKinds { + return this.inner.valueKinds; + } + get refuses(): string | null { + return this.inner.refuses; + } + get conversionThrows(): boolean { + return this.inner.conversionThrows; + } + get rustFromExternThrows(): boolean { + return this.inner.rustFromExternThrows; + } + get dependencies(): readonly Type[] { + return [this.inner]; + } + get convertsInPlace(): boolean { + return this.inner.convertsInPlace; + } + cppConvert(value: string, ctx: string, name: string): string { + return this.inner.cppConvert(value, ctx, name); + } + cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { + return this.inner.cppConvertInto(target, value, ctx, name, check); + } + cppReadLast(target: string, value: string): string[] { + return this.inner.cppReadLast(target, value); + } + layout(): Layout { + return structLayout([this.inner.layout(), { size: 8, align: 8 }]); + } + rustExternType(scope: RustScope): string { + return `bindgen::extern_types::ExternWithValue<${this.inner.rustExternType(scope)}>`; + } + rustType(scope: RustScope): string { + return `(${this.inner.rustType(scope)}, bindgen::JsLocal<'a>)`; + } + rustFromExtern(expr: string, scope: RustScope): string { + return `(${this.inner.rustFromExtern(`${expr}.converted`, scope)}, bindgen::JsLocal::from_extern(${expr}.value, heap))`; + } + rustToJS(): never { + throw new TypeError(".withValue is only for an argument"); + } +} + +export const isIfPassed = (type: Type) => type instanceof OptionalType && type.modifier === "ifPassed"; + /** Not a type of its own: marks an argument or member whose absence has a value. */ export class DefaultType extends Type { readonly inner: Type; @@ -1318,7 +1557,8 @@ export class ArrayType extends Type { super(); assert(!(element instanceof DefaultType), "an element of an array is never absent, so it has no default"); assert(!(element instanceof RangeType), "a range reports the name of its argument, which an element does not have"); - assert(!element.passesValue, "b.bytes is only for an argument"); + assert(!element.passesValue, "b.bytes and .withValue are only for an argument"); + assert(!isIfPassed(element), ".ifPassed is only for an argument"); assert( !element.borrowsCell, "an instance that Rust takes as &T cannot be in an array or a dictionary: only an argument's slot in the frame keeps it alive", @@ -1439,15 +1679,26 @@ export abstract class NamedType extends Type { } } +/** A record names the variants, for values that make no identifier or the same one. */ +export type EnumerationValues = + readonly (string | readonly string[])[] | Readonly>; + export class EnumerationType extends NamedType { /** `spellings[i]` all parse to variant `i`; the first is the canonical one. */ readonly spellings: readonly (readonly string[])[]; /** Index of the first canonical spelling in `Tables::enumerationValues`. Set by the generator. */ firstEnumerationValueIndex = -1; - constructor(values: readonly (string | readonly string[])[]) { + readonly variants: readonly string[]; + constructor(values: EnumerationValues) { super(); - assert(values.length > 0, "an enumeration needs at least one value"); - this.spellings = values.map(value => (typeof value === "string" ? [value] : value)); + const spellings: readonly (string | readonly string[])[] = Array.isArray(values) ? values : Object.values(values); + assert(spellings.length > 0, "an enumeration needs at least one value"); + this.spellings = spellings.map(value => (typeof value === "string" ? [value] : value)); + this.variants = Array.isArray(values) + ? this.spellings.map(([canonical]) => + pascalCase(canonical.replace(/[A-Z]{2,}/g, capitals => capitals.toLowerCase())), + ) + : Object.keys(values); const all = this.spellings.flat(); assert(new Set(all).size === all.length, `a value is listed twice: ${all.join(", ")}`); assert( @@ -1456,9 +1707,6 @@ export class EnumerationType extends NamedType { ); for (const variant of this.variants) assert(/^[A-Z]/.test(rustIdent(variant)), `${variant} is not an identifier`); } - get variants(): string[] { - return this.spellings.map(([canonical]) => pascalCase(canonical)); - } get valueKinds(): ValueKinds { return ["string"]; } @@ -1497,6 +1745,8 @@ export interface MemberOptions { rustName?: string; /** Other JS names, tried in order while the value is `undefined`. */ altNames?: readonly string[]; + /** As a function's `expected`, for this member. */ + expected?: readonly string[] | string; } export class Member { @@ -1504,6 +1754,7 @@ export class Member { /** Of the field in the generated C++ struct and, keywords aside, in the Rust one. */ readonly fieldName: string; readonly type: Type; + readonly expected?: readonly string[] | string; constructor(name: string, spec: Type | MemberOptions) { const options: MemberOptions = spec instanceof Type ? { type: spec } : spec; this.names = [name, ...(options.altNames ?? [])]; @@ -1513,6 +1764,8 @@ export class Member { assert(/^[a-z_][a-z0-9_]*$/.test(this.fieldName), `${this.fieldName}: rustName is written in snake_case`); assert(!cppKeywords.has(this.fieldName), `${this.fieldName} is a C++ keyword: name the field with rustName`); this.type = options.type; + assert(options.expected === undefined || options.expected.length > 0, `expected: ${name} expects nothing`); + this.expected = options.expected; } get name(): string { return this.names[0]; @@ -1534,6 +1787,8 @@ export interface DictionaryOptions { nonObjectAsEmpty?: boolean | "typeof"; /** As a result, the object has no prototype. */ nullPrototype?: boolean; + /** What an error calls the dictionary: `"options"` makes a member `options.level`, and `""` makes it `level`. Defaults to the name of the type. */ + reportedAs?: string; } export class DictionaryType extends NamedType { @@ -1554,7 +1809,8 @@ export class DictionaryType extends NamedType { "fields", ); for (const { name, type } of this.members) { - assert(!type.passesValue, `${name}: b.bytes is only for an argument`); + assert(!type.passesValue, `${name}: b.bytes and .withValue are only for an argument`); + assert(!isIfPassed(type), `${name}: .ifPassed is only for an argument`); assert(!(type instanceof VariadicType), `${name}: .variadic is only for the last argument`); } assert( @@ -1587,6 +1843,9 @@ export class DictionaryType extends NamedType { get nonObjectAsEmpty(): boolean | "typeof" { return this.options.nonObjectAsEmpty ?? false; } + get refuses(): string | null { + return this.nonObjectAsEmpty ? null : nonObjects; + } get nullPrototype(): boolean { return !!this.options.nullPrototype; } @@ -1602,6 +1861,9 @@ export class DictionaryType extends NamedType { cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { return cppThenCheck(`Bun::Bindgen::convertDictionaryInto(*globalObject, ${value}, ${target}, ${ctx});`, check); } + get reportedAs(): string { + return this.options.reportedAs ?? this.name; + } /** Every JS name read, in order. */ get names(): string[] { return this.members.flatMap(member => member.names); @@ -1675,7 +1937,7 @@ export class UnionType extends NamedType { assert(!(type instanceof UnionType), `${name}: list the alternatives of the other union here`); assert(!(type instanceof OptionalType), `${name}: add b.null as an alternative`); assert(!(type instanceof DefaultType), `${name}: put .default() on the union`); - assert(!type.passesValue, `${name}: b.bytes is only for an argument`); + assert(!type.passesValue, `${name}: b.bytes and .withValue are only for an argument`); assert(!(type instanceof VariadicType), `${name}: .variadic is only for the last argument`); assert(/^[a-z][A-Za-z0-9]*$/.test(name), `${name}: an alternative is named in camelCase`); assert(name !== "tag", "tag: the generated struct has a member of that name"); @@ -1770,6 +2032,8 @@ export interface SignatureOptions { capability?: Capability; /** The body returns `Result` for any `E: bindgen::IntoError`. */ throws?: boolean; + /** It returns a promise. What it throws, or the conversion of an argument throws, is the reason of a rejected one instead. */ + rejects?: boolean; /** Path of the Rust function. Defaults to the module's path plus the export's name in snake_case. */ rustPath?: string; /** The JS function's `length`. Defaults to the number of required arguments. */ @@ -1796,11 +2060,13 @@ export class Signature { readonly returns: Type; readonly capability: Capability; readonly throws: boolean; + readonly rejects: boolean; readonly rustPath: string | undefined; readonly length: number | undefined; /** `hasThisValue`: the body is also handed its own JS object. */ constructor(options: SignatureOptions, hasThisValue = false) { this.length = options.length; + this.rejects = options.rejects ?? false; const { args = {}, expected = {} } = options; this.args = Object.entries(args).map(([name, type]) => ({ name, @@ -1842,6 +2108,11 @@ export class Signature { pinned.length === 0 || !this.args.some(arg => arg.type.borrowsHeap), 'held: "pinned" moves the bytes of a small typed array, which another argument that is held: "borrowed" can be', ); + const taken = this.args.findIndex(({ type }) => [type, ...type.dependencies].some(isNativePromiseContext)); + assert( + taken < 0 || this.args.slice(taken + 1).every(arg => !arg.type.conversionThrows), + "b.nativePromiseContext takes the pointer out of the cell: if a later argument throws, nothing releases it", + ); assert(!(this.returns instanceof DefaultType), "a result has no default"); assert( !this.returns.holdsJSValue || this.capability !== "none" || hasThisValue, @@ -1891,12 +2162,24 @@ export class FunctionDefinition { this.passThis = options.passThis ?? false; this.passCallee = options.passCallee ?? false; this.exportName = options.export; + for (const key of ["passThis", "passCallee", "export"] as const) { + assert( + signatures.every( + signature => signature instanceof Signature || (signature as FunctionOptions)[key] === options[key], + ), + `its overloads have one ${key}`, + ); + } assert( this.exportName === undefined || /^[A-Za-z_]\w*$/.test(this.exportName), `export: ${this.exportName} is not a C identifier`, ); this.signatures = signatures.map(options => (options instanceof Signature ? options : new Signature(options))); assert(this.signatures.filter(signature => signature.length !== undefined).length <= 1, "length is given once"); + assert( + this.signatures.every(signature => signature.rejects === this.signatures[0].rejects), + "its overloads have one rejects", + ); const nodes = new Map(); this.dispatch = this.dispatchOn(this.signatures, 0, nodes); const chosen = new Set(nodes.values()); @@ -1921,7 +2204,7 @@ export class FunctionDefinition { const types = candidates.map(signature => signature.args[argument]?.type); const next = (kind: ValueKind | "other") => { const keep = (test: (type: Type) => boolean) => - candidates.filter((signature, i) => !types[i] || test(types[i])); + candidates.filter((signature, i) => (types[i] ? test(types[i]) : kind === "undefined")); const ofKind = keep(type => takes(type, kind)); const takers = kind === "undefined" ? keep(type => type.takesUndefined) : ofKind; const kept = [takers, ofKind, candidates].find(list => list.length > 0)!; @@ -2401,11 +2684,14 @@ export class NamespaceDefinition { readonly functions: readonly FunctionDefinition[]; readonly call: FunctionDefinition | undefined; name = ""; - constructor(functions: Record, options: NamespaceOptions = {}) { + constructor( + functions: Record, + options: NamespaceOptions = {}, + ) { this.functions = Object.entries(functions).map(([name, options]) => { assert(/^[A-Za-z_$][\w$]*$/.test(name), `${name}: a function is named by an identifier`); assert(!(options instanceof FunctionDefinition), `${name}: write what b.fn takes, without b.fn`); - const fn = new FunctionDefinition([options]); + const fn = new FunctionDefinition(Array.isArray(options) ? options : [options]); fn.name = name; return fn; }); diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index 6969c2d43d92..af2e6b108c77 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -14,6 +14,7 @@ import { DictionaryType, DoubleType, EnumerationType, + type EnumerationValues, FunctionDefinition, type FunctionOptions, integers, @@ -21,6 +22,8 @@ import { ModuleDefinition, NamespaceDefinition, type NamespaceOptions, + NativePromiseContextType, + PromisePtrType, RecordResultType, RefType, type SignatureOptions, @@ -32,7 +35,7 @@ import { } from "./bindgen-lib-internal.ts"; export type { Capability, Type } from "./bindgen-lib-internal.ts"; -export { null_ as null }; +export { null_ as null, undefined_ as undefined }; /** Strict: only a boolean. `.loose` for `ToBoolean`. */ export const bool = new BoolType(); @@ -52,6 +55,12 @@ export const any = new AnyType(); /** A function, or a proxy of one, as a `JsLocal<'a, Callable>`. */ export const callable = new CallableType(); +/** An object, as a `JsLocal<'a, JSObject>`. */ +export { objectType as object } from "./bindgen-lib-internal.ts"; + +/** An array, as a `JsLocal<'a, JSArray>`: `b.array(T)` converts its elements. */ +export { jsArrayType as jsArray } from "./bindgen-lib-internal.ts"; + /** A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsLocal<'a, BufferSource>`. `.arrayBuffer`. */ export const bufferSource = new BufferSourceType(); @@ -68,7 +77,16 @@ export const blob = new RefType( null, ); +/** The context of a promise reaction that `JSValue::then` was given: a `*mut` of the type at `rustPath`. */ +export const promisePtr = (rustPath: string) => new PromisePtrType(rustPath); + +/** The context of a promise reaction that is a `NativePromiseContext` cell: what `take` hands back, an `Option>` of the type at `rustPath`. */ +export const nativePromiseContext = (rustPath: string) => new NativePromiseContextType(rustPath); + +/** `null` or `undefined`, as an alternative of a union. */ const null_ = new UnitType("null"); +/** `undefined`, as an alternative of a union. */ +const undefined_ = new UnitType("undefined"); export const array = (element: Type) => new ArrayType(element); @@ -77,8 +95,8 @@ export const record = (value: Type) => new RecordResultType(value); // A named type is called what it is exported as: `export const Formatter = b.enumeration([..])`. -/** An array in place of a value lists several spellings of it; the first is the one given back to JS. */ -export const enumeration = (values: readonly (string | readonly string[])[]) => new EnumerationType(values); +/** An array in place of a value lists several spellings of it; the first is the one given back to JS. `{ RgbArray: "[rgb]" }` names the variants. */ +export const enumeration = (values: EnumerationValues) => new EnumerationType(values); export const dictionary = (members: Record, options?: DictionaryOptions) => new DictionaryType(members, options); @@ -99,11 +117,13 @@ export const fn = (options: FunctionOptions) => new FunctionDefinition([options] export const value = (options: ValueOptions) => new FunctionDefinition([options], {}, true); /** One JS function, several signatures, told apart by the kinds of their arguments. */ -export const overloads = (...signatures: SignatureOptions[]) => new FunctionDefinition(signatures); +export const overloads = (...signatures: FunctionOptions[]) => new FunctionDefinition(signatures, signatures[0]); -/** `export const semver = b.namespace({ satisfies: { args, returns }, .. })`: an object that holds those functions, or with `{ call: "satisfies" }` a function that does what that one does. */ -export const namespace = (functions: Record, options?: NamespaceOptions) => - new NamespaceDefinition(functions, options); +/** `export const semver = b.namespace({ satisfies: { args, returns }, .. })`: an object that holds those functions, or with `{ call: "satisfies" }` a function that does what that one does. An array is the overloads of one function. */ +export const namespace = ( + functions: Record, + options?: NamespaceOptions, +) => new NamespaceDefinition(functions, options); /** `export default b.module({ rustPath: "crate::node::os" })`: where the file's Rust functions are. */ export const module = (options: { rustPath: string }) => new ModuleDefinition(options); diff --git a/src/css_jsc/color_js.bind.ts b/src/css_jsc/color_js.bind.ts index 2caba36ff439..c2f0b24271bb 100644 --- a/src/css_jsc/color_js.bind.ts +++ b/src/css_jsc/color_js.bind.ts @@ -2,9 +2,61 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::color_js" }); +/** From 0 to 1. What is not a number is opaque. */ +export const ColorAlpha = b.union({ number: b.f64, ignored: b.any }); +export const ColorChannels = b.dictionary( + { r: b.f64, g: b.f64, b: b.f64, a: ColorAlpha }, + { allowArray: true, fromJS: true }, +); + +/** + * `list` and `buffer` hold 3 or 4 numbers, which are read one at a time. A `DataView` has no elements: it is read as + * `channels`. `text` is a string, or any other primitive converted to one. + */ +export const ColorInput = b.union({ + missing: b.undefined, + number: b.f64, + list: b.jsArray, + buffer: b.bufferSource.allowShared, + channels: ColorChannels, + text: b.string.loose.utf8, +}); + +export const OutputColorFormat = b.enumeration({ + Ansi: "ansi", + Ansi16: ["ansi-16", "ansi_16"], + Ansi16m: ["ansi-16m", "ansi_16m", "ansi-24bit", "ansi-truecolor"], + Ansi256: ["ansi-256", "ansi_256", "ansi256"], + Css: "css", + Hex: "hex", + HexUpper: "HEX", + Hsl: "hsl", + Lab: "lab", + Number: "number", + Rgb: "rgb", + Rgba: "rgba", + RgbArray: "[rgb]", + RgbaArray: ["[rgba]", "[r,g,b,a]"], + RgbObject: ["{rgb}", "{r,g,b}"], + RgbaObject: "{rgba}", +}); + +export const ColorOutputChannels = b.dictionary({ r: b.u8, g: b.u8, b: b.u8, a: b.f64.optional }); +export const ColorOutput = b.union({ + text: b.string, + number: b.u32, + /** Of 3 or 4 bytes. */ + list: b.jsArray, + channels: ColorOutputChannels, +}); + +const formats = OutputColorFormat.spellings.flat().map(format => `'${format}'`); + export const color = b.fn({ - args: { input: b.any, format: b.any }, - returns: b.any, + args: { input: ColorInput, format: OutputColorFormat.nullable.default("css") }, + expected: { format: `format must be one of ${formats.slice(0, -1).join(", ")} or ${formats.at(-1)}` }, + returns: ColorOutput.nullable, capability: "js", throws: true, + length: 2, }); diff --git a/src/css_jsc/color_js.rs b/src/css_jsc/color_js.rs index 17a76ef8f2bf..40a54f4aabb6 100644 --- a/src/css_jsc/color_js.rs +++ b/src/css_jsc/color_js.rs @@ -4,102 +4,59 @@ use bun_alloc::Arena; use bun_ast::Log; use bun_core::String as BunString; use bun_core::output::{ColorDepth, Source as OutputSource}; -use bun_jsc::bindgen::{JsLocal, JsScope}; -use bun_jsc::bun_string_jsc; -use bun_jsc::{JSGlobalObject, JSValue}; +use bun_css::values::color::RGBA; +use bun_jsc::bindgen::{self, JsHeap, JsScope, to_js}; +use bun_jsc::{JSGlobalObject, JSType, JSValue}; use crate::JsResult; +use crate::generated_bindgen::color_js::{ + ColorAlpha, ColorChannels, ColorInput, ColorOutput, ColorOutputChannels, OutputColorFormat, +}; -#[derive(Copy, Clone, PartialEq, Eq)] -pub(crate) enum OutputColorFormat { - Ansi, - Ansi16, - Ansi16m, - Ansi256, - Css, - Hex, - HexUpper, - Hsl, - Lab, - Number, - Rgb, - Rgba, - RgbArray, - RgbaArray, - RgbObject, - RgbaObject, -} - -impl bun_jsc::FromJsEnum for OutputColorFormat { - fn from_js_value( - v: JSValue, - global: &JSGlobalObject, - property_name: &'static str, - ) -> JsResult { - use bun_jsc::ComptimeStringMapExt as _; - match OUTPUT_COLOR_FORMAT_MAP.from_js(global, v)? { - Some(e) => Ok(e), - None => { - // List the accepted spellings straight from the lookup map so the - // error message can't drift from what the parser actually accepts. - let n = OUTPUT_COLOR_FORMAT_MAP.len(); - let mut one_of = std::string::String::from("'"); - for (i, key) in OUTPUT_COLOR_FORMAT_MAP.keys().enumerate() { - one_of.push_str(std::str::from_utf8(key).expect("map keys are ASCII")); - one_of.push('\''); - if i + 2 < n { - one_of.push_str(", '"); - } else if i + 2 == n { - one_of.push_str(" or '"); - } - } - Err(global.throw_invalid_arguments(format_args!( - "{property_name} must be one of {one_of}" - ))) - } - } - } -} - -bun_core::comptime_string_map! { - pub(crate) static OUTPUT_COLOR_FORMAT_MAP: OutputColorFormat = { - b"[r,g,b,a]" => OutputColorFormat::RgbaArray, - b"[rgb]" => OutputColorFormat::RgbArray, - b"[rgba]" => OutputColorFormat::RgbaArray, - b"{r,g,b}" => OutputColorFormat::RgbObject, - b"{rgb}" => OutputColorFormat::RgbObject, - b"{rgba}" => OutputColorFormat::RgbaObject, - b"ansi_256" => OutputColorFormat::Ansi256, - b"ansi-256" => OutputColorFormat::Ansi256, - b"ansi_16" => OutputColorFormat::Ansi16, - b"ansi-16" => OutputColorFormat::Ansi16, - b"ansi_16m" => OutputColorFormat::Ansi16m, - b"ansi-16m" => OutputColorFormat::Ansi16m, - b"ansi-24bit" => OutputColorFormat::Ansi16m, - b"ansi-truecolor" => OutputColorFormat::Ansi16m, - b"ansi" => OutputColorFormat::Ansi, - b"ansi256" => OutputColorFormat::Ansi256, - b"css" => OutputColorFormat::Css, - b"hex" => OutputColorFormat::Hex, - b"HEX" => OutputColorFormat::HexUpper, - b"hsl" => OutputColorFormat::Hsl, - b"lab" => OutputColorFormat::Lab, - b"number" => OutputColorFormat::Number, - b"rgb" => OutputColorFormat::Rgb, - b"rgba" => OutputColorFormat::Rgba, - }; +/// CSS spec says to clamp values to their valid range so we'll respect that here +fn clamp_to_u8(value: f64) -> u8 { + value as u8 } fn color_int_from_js( global: &JSGlobalObject, input: JSValue, property: &'static str, -) -> JsResult { +) -> JsResult { if input.is_empty() || input.is_undefined() || !input.is_number() { return Err(global.throw_invalid_argument_type("color", property, "integer")); } - // CSS spec says to clamp values to their valid range so we'll respect that here - Ok(input.coerce::(global)?.clamp(0, 255)) + Ok(clamp_to_u8(input.as_number())) +} + +/// `list` is an array, a typed array or an `ArrayBuffer`. +fn rgba_from_list(global: &JSGlobalObject, list: JSValue) -> JsResult { + let length = list.get_length(global)?; + if !matches!(length, 3 | 4) { + return Err(global.throw(format_args!("Expected array length 3 or 4"))); + } + Ok(RGBA { + red: color_int_from_js(global, list.get_index(global, 0)?, "[0]")?, + green: color_int_from_js(global, list.get_index(global, 1)?, "[1]")?, + blue: color_int_from_js(global, list.get_index(global, 2)?, "[2]")?, + alpha: if length == 4 { + color_int_from_js(global, list.get_index(global, 3)?, "[3]")? + } else { + 255 + }, + }) +} + +fn rgba_from_channels(channels: &ColorChannels<'_>) -> RGBA { + RGBA { + red: clamp_to_u8(channels.r), + green: clamp_to_u8(channels.g), + blue: clamp_to_u8(channels.b), + alpha: match channels.a { + ColorAlpha::Number(a) => clamp_to_u8(a * 255.0), + ColorAlpha::Ignored(_) => 255, + }, + } } // https://github.com/tmux/tmux/blob/dae2868d1227b95fd076fb4a5efa6256c7245943/colour.c#L44-L55 @@ -211,49 +168,38 @@ fn zero_if_none(component: f32) -> f32 { if component.is_nan() { 0.0 } else { component } } +fn channel_list<'a, const N: usize>( + heap: &JsHeap<'a>, + channels: [u8; N], +) -> bindgen::Result<'a, Option>> { + Ok(Some(ColorOutput::List( + heap.create_array(channels, to_js::u8)?, + ))) +} + pub(crate) fn color<'a>( cx: &mut JsScope<'a>, - input: JsLocal<'a>, - format: JsLocal<'a>, -) -> JsResult { + input: ColorInput<'a>, + format: OutputColorFormat, +) -> bindgen::Result<'a, Option>> { use bun_ast::symbol::Map as SymbolMap; - use bun_core::Utf8Bytes; use bun_css as css; use bun_css::CssColor; - use bun_css::values::color::{HSL, LAB, RGBA, SRGB}; - use bun_jsc::StringJsc as _; - - let global = cx.global(); - let args = [input.to_js(), format.to_js()]; - if args[0].is_undefined() { - return Err(global.throw_invalid_argument_type( - "color", - "input", - "string, number, or object", - )); - } + use bun_css::values::color::{HSL, LAB, SRGB}; let log = Log::init(); - let unresolved_format: OutputColorFormat = 'brk: { - if !args[1].is_empty_or_undefined_or_null() { - if !args[1].is_string() { - return Err(global.throw_invalid_argument_type("color", "format", "string")); - } - - break 'brk args[1].to_enum::(global, "format")?; + let parsed_color: css::CssColorParseResult = match input { + ColorInput::Missing => { + return Err(cx + .global() + .throw_invalid_argument_type("color", "input", "string, number, or object") + .into()); } - - break 'brk OutputColorFormat::Css; - }; - let input: Utf8Bytes; - - let parsed_color: css::CssColorParseResult = 'brk: { - if args[0].is_number() { - let number: i64 = args[0].to_int64(); + ColorInput::Number(number) => { // The color is the low 32 bits, LSB-first: blue, green, red, // alpha (one byte each). - let int: u32 = number as u32; + let int: u32 = (number as i64) as u32; let blue = (int & 0xff) as u8; let green = ((int >> 8) & 0xff) as u8; let red = ((int >> 16) & 0xff) as u8; @@ -265,88 +211,45 @@ pub(crate) fn color<'a>( 255 }; - break 'brk Ok(CssColor::Rgba(RGBA { + Ok(CssColor::Rgba(RGBA { alpha, red, green, blue, - })); - } else if args[0].js_type().is_array_like() { - match args[0].get_length(global)? { - 3 => { - let r = color_int_from_js(global, args[0].get_index(global, 0)?, "[0]")?; - let g = color_int_from_js(global, args[0].get_index(global, 1)?, "[1]")?; - let b = color_int_from_js(global, args[0].get_index(global, 2)?, "[2]")?; - break 'brk Ok(CssColor::Rgba(RGBA { - alpha: 255, - red: u8::try_from(r).expect("int cast"), - green: u8::try_from(g).expect("int cast"), - blue: u8::try_from(b).expect("int cast"), - })); - } - 4 => { - let r = color_int_from_js(global, args[0].get_index(global, 0)?, "[0]")?; - let g = color_int_from_js(global, args[0].get_index(global, 1)?, "[1]")?; - let b = color_int_from_js(global, args[0].get_index(global, 2)?, "[2]")?; - let a = color_int_from_js(global, args[0].get_index(global, 3)?, "[3]")?; - break 'brk Ok(CssColor::Rgba(RGBA { - alpha: u8::try_from(a).expect("int cast"), - red: u8::try_from(r).expect("int cast"), - green: u8::try_from(g).expect("int cast"), - blue: u8::try_from(b).expect("int cast"), - })); - } - _ => { - return Err(global.throw(format_args!("Expected array length 3 or 4"))); - } - } - } else if args[0].is_object() { - let r = color_int_from_js(global, args[0].get(global, b"r")?.unwrap_or_default(), "r")?; - let g = color_int_from_js(global, args[0].get(global, b"g")?.unwrap_or_default(), "g")?; - let b = color_int_from_js(global, args[0].get(global, b"b")?.unwrap_or_default(), "b")?; - - let a: Option = if let Some(a_value) = args[0].get_truthy(global, b"a")? { - 'brk2: { - if a_value.is_number() { - // CSS spec says to clamp values to their valid range so we'll respect that here - break 'brk2 Some( - u8::try_from(((a_value.as_number() * 255.0) as i64).clamp(0, 255)) - .unwrap(), - ); - } - break 'brk2 None; - } + })) + } + ColorInput::List(list) => Ok(CssColor::Rgba(rgba_from_list(cx.global(), list.to_js())?)), + ColorInput::Buffer(buffer) => { + let (global, buffer) = (cx.global(), buffer.to_js()); + Ok(CssColor::Rgba(if buffer.js_type() == JSType::DataView { + ColorChannels::from_js(global, buffer, |channels| { + Ok(rgba_from_channels(&channels)) + })? } else { - None - }; - - break 'brk Ok(CssColor::Rgba(RGBA { - alpha: a.unwrap_or(255), - red: u8::try_from(r).expect("int cast"), - green: u8::try_from(g).expect("int cast"), - blue: u8::try_from(b).expect("int cast"), - })); + rgba_from_list(global, buffer)? + })) + } + ColorInput::Channels(channels) => Ok(CssColor::Rgba(rgba_from_channels(&channels))), + ColorInput::Text(text) => { + // MimallocArena::new() calls mi_heap_new(), so defer creation to the + // paths that actually allocate. + let arena = Arena::new(); + let mut parser_input = css::ParserInput::new(text, &arena); + let mut parser = css::Parser::new( + &mut parser_input, + None, + css::css_parser::ParserOpts::default(), + None, + ); + CssColor::parse(&mut parser) } - - input = args[0].to_utf8(global)?; - - // MimallocArena::new() calls mi_heap_new(), so defer creation to the - // paths that actually allocate. - let arena = Arena::new(); - let mut parser_input = css::ParserInput::new(input.slice(), &arena); - let mut parser = css::Parser::new( - &mut parser_input, - None, - css::css_parser::ParserOpts::default(), - None, - ); - break 'brk CssColor::parse(&mut parser); }; + let heap = &cx.heap; match parsed_color { Err(err) => { if log.msgs.is_empty() { - return Ok(JSValue::NULL); + return Ok(None); } let kind_name = match err.basic().kind { @@ -356,19 +259,19 @@ pub(crate) fn color<'a>( css::BasicParseErrorKind::at_rule_body_invalid => "at_rule_body_invalid", css::BasicParseErrorKind::qualified_rule_invalid => "qualified_rule_invalid", }; - return Err(global.throw(format_args!("color() failed to parse {}", kind_name))); + return Err(heap.error(format_args!("color() failed to parse {}", kind_name))); } Ok(result) => { - let format: OutputColorFormat = if unresolved_format == OutputColorFormat::Ansi { + let format: OutputColorFormat = if format == OutputColorFormat::Ansi { match OutputSource::color_depth() { // No color terminal, therefore return an empty string - ColorDepth::None => return Ok(JSValue::js_empty_string(global)), + ColorDepth::None => return Ok(Some(ColorOutput::Text(BunString::EMPTY))), ColorDepth::C16 => OutputColorFormat::Ansi16, ColorDepth::C16m => OutputColorFormat::Ansi16m, ColorDepth::C256 => OutputColorFormat::Ansi256, } } else { - unresolved_format + format }; 'formatted: { @@ -404,73 +307,36 @@ pub(crate) fn color<'a>( let rgba = srgba.into_rgba(); match tag { OutputColorFormat::RgbaObject => { - let object = JSValue::create_empty_object(global, 4); - object.put(global, b"r", JSValue::js_number(rgba.red as f64)); - object.put(global, b"g", JSValue::js_number(rgba.green as f64)); - object.put(global, b"b", JSValue::js_number(rgba.blue as f64)); - object.put( - global, - b"a", - JSValue::js_number(rgba.alpha_f32() as f64), - ); - return Ok(object); + return Ok(Some(ColorOutput::Channels(ColorOutputChannels { + r: rgba.red, + g: rgba.green, + b: rgba.blue, + a: Some(rgba.alpha_f32() as f64), + }))); } OutputColorFormat::RgbObject => { - let object = JSValue::create_empty_object(global, 3); - object.put(global, b"r", JSValue::js_number(rgba.red as f64)); - object.put(global, b"g", JSValue::js_number(rgba.green as f64)); - object.put(global, b"b", JSValue::js_number(rgba.blue as f64)); - return Ok(object); + return Ok(Some(ColorOutput::Channels(ColorOutputChannels { + r: rgba.red, + g: rgba.green, + b: rgba.blue, + a: None, + }))); } OutputColorFormat::RgbArray => { - let object = JSValue::create_empty_array(global, 3)?; - object.put_index( - global, - 0, - JSValue::js_number(rgba.red as f64), - )?; - object.put_index( - global, - 1, - JSValue::js_number(rgba.green as f64), - )?; - object.put_index( - global, - 2, - JSValue::js_number(rgba.blue as f64), - )?; - return Ok(object); + return channel_list(heap, [rgba.red, rgba.green, rgba.blue]); } OutputColorFormat::RgbaArray => { - let object = JSValue::create_empty_array(global, 4)?; - object.put_index( - global, - 0, - JSValue::js_number(rgba.red as f64), - )?; - object.put_index( - global, - 1, - JSValue::js_number(rgba.green as f64), - )?; - object.put_index( - global, - 2, - JSValue::js_number(rgba.blue as f64), - )?; - object.put_index( - global, - 3, - JSValue::js_number(rgba.alpha as f64), - )?; - return Ok(object); + return channel_list( + heap, + [rgba.red, rgba.green, rgba.blue, rgba.alpha], + ); } OutputColorFormat::Number => { let mut int: u32 = 0; int |= (rgba.red as u32) << 16; int |= (rgba.green as u32) << 8; int |= rgba.blue as u32; - return Ok(JSValue::js_number(int as f64)); + return Ok(Some(ColorOutput::Number(int))); } OutputColorFormat::Hex => { break 'color BunString::create_format(format_args!( @@ -601,7 +467,7 @@ pub(crate) fn color<'a>( } }; - return str.into_js(global); + return Ok(Some(ColorOutput::Text(str))); } // Fallback to CSS string output @@ -620,11 +486,11 @@ pub(crate) fn color<'a>( ); if let Err(err) = result.to_css(&mut printer) { - return Err(global.throw(format_args!("color() internal error: {}", err.name()))); + return Err(heap.error(format_args!("color() internal error: {}", err.name()))); } drop(printer); - return bun_string_jsc::create_utf8_for_js(global, &dest); + return Ok(Some(ColorOutput::Text(BunString::clone_utf8(&dest)))); } } } diff --git a/src/css_jsc/css_internals.bind.ts b/src/css_jsc/css_internals.bind.ts index be6c10582f2e..8e61d6ac8227 100644 --- a/src/css_jsc/css_internals.bind.ts +++ b/src/css_jsc/css_internals.bind.ts @@ -2,26 +2,50 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::css_internals" }); -/** `options` is the browser targets, or the parser's options for a `WithOptions` function. */ -const stylesheetTest = () => +const version = b.u32.emptyStringAsNull; +export const BrowserTargets = b.dictionary({ + android: version, + chrome: version, + edge: version, + firefox: version, + ie: version, + ios_saf: version, + opera: version, + safari: version, + samsung: version, +}); + +export const CssParserFlag = b.enumeration(["DEEP_SELECTOR_COMBINATOR"]); +export const CssParserOptions = b.dictionary({ flags: b.array(CssParserFlag).emptyStringAsNull }); + +/** `failure` is what the parser logged, as an error that is not thrown, or `undefined` if an attribute cannot be printed. */ +export const CssTestOutput = b.union({ code: b.string.utf8, failure: b.any }); + +const stylesheetTest = (options: b.Type) => b.fn({ - args: { source: b.string.allowStringObject, expected: b.string.allowStringObject, options: b.any }, - returns: b.any, + args: { source: b.string.utf8, expected: b.string.utf8, options }, + returns: CssTestOutput, capability: "js", throws: true, + length: 3, }); -export const minifyTestWithOptions = stylesheetTest(); -export const minifyErrorTestWithOptions = stylesheetTest(); -export const testWithOptions = stylesheetTest(); -export const prefixTestWithOptions = stylesheetTest(); -export const minifyTest = stylesheetTest(); -export const prefixTest = stylesheetTest(); -export const test = stylesheetTest(); +export const minifyTestWithOptions = stylesheetTest(CssParserOptions.nullable); +export const minifyErrorTestWithOptions = stylesheetTest(CssParserOptions.nullable); +export const testWithOptions = stylesheetTest(CssParserOptions.nullable); +export const prefixTestWithOptions = stylesheetTest(CssParserOptions.nullable); +export const minifyTest = stylesheetTest(BrowserTargets.nullable); +export const prefixTest = stylesheetTest(BrowserTargets.nullable); +export const test = stylesheetTest(BrowserTargets.nullable); export const attrTest = b.fn({ - args: { source: b.string.allowStringObject, expected: b.string.allowStringObject, minify: b.any, targets: b.any }, - returns: b.any, + args: { + source: b.string.utf8, + expected: b.string.utf8, + minify: b.bool.default(false), + targets: BrowserTargets.nullable, + }, + returns: CssTestOutput, capability: "js", throws: true, length: 3, diff --git a/src/css_jsc/css_internals.rs b/src/css_jsc/css_internals.rs index 52fb0d021446..4d36a0717dde 100644 --- a/src/css_jsc/css_internals.rs +++ b/src/css_jsc/css_internals.rs @@ -1,12 +1,13 @@ use bun_alloc::Arena; // bumpalo::Bump re-export use bun_ast::Log; -use bun_core::String as BunString; +use bun_css::ParserFlags; use bun_css::targets::{Browsers, Targets}; use bun_jsc::bindgen::{JsLocal, JsScope}; -use bun_jsc::bun_string_jsc; -use bun_jsc::{JSGlobalObject, JSValue}; use crate::JsResult; +use crate::generated_bindgen::css_internals::{ + BrowserTargets, CssParserFlag, CssParserOptions, CssTestOutput, +}; // `adt_const_params` is unstable, so the test kind is passed as a runtime // value (the bodies branch on it anyway; no codegen difference for this fn). @@ -17,126 +18,94 @@ pub(crate) enum TestKind { Prefix, } -#[derive(PartialEq, Eq, Clone, Copy)] -pub(crate) enum TestCategory { - /// arg is browsers - Normal, - /// arg is parser options - ParserOptions, -} - pub(crate) fn minify_error_test_with_options<'a>( cx: &mut JsScope<'a>, - source: &BunString, - _: &BunString, - options: JsLocal<'a>, -) -> JsResult { - testing_impl( - cx.global(), - source, - options.to_js(), - TestKind::Minify, - TestCategory::ParserOptions, - ) + source: &[u8], + _: &[u8], + options: Option, +) -> JsResult> { + testing_impl(cx, source, None, parser_flags(options), TestKind::Minify) } pub(crate) fn minify_test_with_options<'a>( cx: &mut JsScope<'a>, - source: &BunString, - _: &BunString, - options: JsLocal<'a>, -) -> JsResult { - testing_impl( - cx.global(), - source, - options.to_js(), - TestKind::Minify, - TestCategory::ParserOptions, - ) + source: &[u8], + _: &[u8], + options: Option, +) -> JsResult> { + testing_impl(cx, source, None, parser_flags(options), TestKind::Minify) } pub(crate) fn prefix_test_with_options<'a>( cx: &mut JsScope<'a>, - source: &BunString, - _: &BunString, - options: JsLocal<'a>, -) -> JsResult { - testing_impl( - cx.global(), - source, - options.to_js(), - TestKind::Prefix, - TestCategory::ParserOptions, - ) + source: &[u8], + _: &[u8], + options: Option, +) -> JsResult> { + testing_impl(cx, source, None, parser_flags(options), TestKind::Prefix) } pub(crate) fn test_with_options<'a>( cx: &mut JsScope<'a>, - source: &BunString, - _: &BunString, - options: JsLocal<'a>, -) -> JsResult { - testing_impl( - cx.global(), - source, - options.to_js(), - TestKind::Normal, - TestCategory::ParserOptions, - ) + source: &[u8], + _: &[u8], + options: Option, +) -> JsResult> { + testing_impl(cx, source, None, parser_flags(options), TestKind::Normal) } pub(crate) fn minify_test<'a>( cx: &mut JsScope<'a>, - source: &BunString, - _: &BunString, - options: JsLocal<'a>, -) -> JsResult { + source: &[u8], + _: &[u8], + options: Option, +) -> JsResult> { testing_impl( - cx.global(), + cx, source, - options.to_js(), + options, + ParserFlags::default(), TestKind::Minify, - TestCategory::Normal, ) } pub(crate) fn prefix_test<'a>( cx: &mut JsScope<'a>, - source: &BunString, - _: &BunString, - options: JsLocal<'a>, -) -> JsResult { + source: &[u8], + _: &[u8], + options: Option, +) -> JsResult> { testing_impl( - cx.global(), + cx, source, - options.to_js(), + options, + ParserFlags::default(), TestKind::Prefix, - TestCategory::Normal, ) } pub(crate) fn test<'a>( cx: &mut JsScope<'a>, - source: &BunString, - _: &BunString, - options: JsLocal<'a>, -) -> JsResult { + source: &[u8], + _: &[u8], + options: Option, +) -> JsResult> { testing_impl( - cx.global(), + cx, source, - options.to_js(), + options, + ParserFlags::default(), TestKind::Normal, - TestCategory::Normal, ) } -fn testing_impl( - global: &JSGlobalObject, - source: &BunString, - options: JSValue, +fn testing_impl<'a>( + cx: &mut JsScope<'a>, + source: &[u8], + targets: Option, + flags: ParserFlags, test_kind: TestKind, - test_category: TestCategory, -) -> JsResult { +) -> JsResult> { use bun_ast::ImportRecord; use bun_css::{ DefaultAtRule, ImportRecordHandler, LocalsResultsMap, MinifyOptions, ParserOptions, @@ -153,35 +122,17 @@ fn testing_impl( // out of it below. let alloc: &'static Arena = unsafe { bun_ptr::detach_lifetime_ref(&arena) }; - let source = source.to_utf8(); - + let global = cx.global(); + let browsers = targets.map(Browsers::from); let mut log = Log::init(); - let mut browsers: Option = None; - let parser_options = { - let mut opts = ParserOptions::default(Some(&mut log)); - // if (test_kind == .prefix) break :parser_options opts; - - match test_category { - TestCategory::Normal => { - if options.is_object() { - browsers = Some(targets_from_js(global, options)?); - } - } - TestCategory::ParserOptions => { - if options.is_object() { - parser_options_from_js(global, &arena, &mut opts, options)?; - } - } - } - - opts - }; + let mut parser_options = ParserOptions::default(Some(&mut log)); + parser_options.flags |= flags; let mut import_records = Vec::::default(); match StyleSheet::::parse( alloc, - source.slice(), + source, parser_options, Some(&mut import_records), bun_ast::Index::INVALID, @@ -227,82 +178,54 @@ fn testing_impl( } }; - bun_string_jsc::create_utf8_for_js(global, &result.code) + Ok(CssTestOutput::Code(result.code)) } Err(err) => { if log.has_errors() { - return log.to_js(global, format_args!("parsing failed:")); + let errors = log.to_js(global, format_args!("parsing failed:"))?; + return Ok(CssTestOutput::Failure(JsLocal::new(&cx.heap, errors))); } Err(global.throw(format_args!("parsing failed: {}", err.kind))) } } } -fn parser_options_from_js( - global: &JSGlobalObject, - _arena: &Arena, - opts: &mut bun_css::ParserOptions, - jsobj: JSValue, -) -> JsResult<()> { - if let Some(val) = jsobj.get_truthy(global, b"flags")? { - if val.is_array() { - let mut iter = val.array_iterator(global)?; - while let Some(item) = iter.next()? { - let bunstr = item.to_bun_string(global)?; - let str = bunstr.to_utf8(); - if str.slice() == b"DEEP_SELECTOR_COMBINATOR" { - opts.flags |= bun_css::ParserFlags::DEEP_SELECTOR_COMBINATOR; - } else { - return Err(global.throw(format_args!( - "invalid flag: {}", - bstr::BStr::new(str.slice()) - ))); - } - } - } else { - return Err(global.throw(format_args!("flags must be an array"))); +fn parser_flags(options: Option) -> ParserFlags { + let mut flags = ParserFlags::default(); + for flag in options + .and_then(|options| options.flags) + .unwrap_or_default() + { + match flag { + CssParserFlag::DeepSelectorCombinator => flags |= ParserFlags::DEEP_SELECTOR_COMBINATOR, } } - - Ok(()) + flags } -fn targets_from_js(global: &JSGlobalObject, jsobj: JSValue) -> JsResult { - let mut targets = Browsers::default(); - - // Table-driven loop. Key order matters: it determines JS getter/exception - // ordering. - for (key, slot) in [ - ("android", &mut targets.android), - ("chrome", &mut targets.chrome), - ("edge", &mut targets.edge), - ("firefox", &mut targets.firefox), - ("ie", &mut targets.ie), - ("ios_saf", &mut targets.ios_saf), - ("opera", &mut targets.opera), - ("safari", &mut targets.safari), - ("samsung", &mut targets.samsung), - ] { - if let Some(val) = jsobj.get_truthy(global, key)? { - if val.is_int32() { - if let Some(value) = val.get_number() { - // `as` saturates on overflow/NaN - *slot = Some(value as u32); - } - } +impl From for Browsers { + fn from(targets: BrowserTargets) -> Self { + Browsers { + android: targets.android, + chrome: targets.chrome, + edge: targets.edge, + firefox: targets.firefox, + ie: targets.ie, + ios_saf: targets.ios_saf, + opera: targets.opera, + safari: targets.safari, + samsung: targets.samsung, } } - - Ok(targets) } pub(crate) fn attr_test<'a>( cx: &mut JsScope<'a>, - source: &BunString, - _: &BunString, - minify: JsLocal<'a>, - targets: JsLocal<'a>, -) -> JsResult { + source: &[u8], + _: &[u8], + minify: bool, + targets: Option, +) -> JsResult> { use bun_ast::ImportRecord; use bun_css::{ ImportRecordHandler, MinifyOptions, ParserOptions, PrinterOptions, StyleAttribute, @@ -320,16 +243,10 @@ pub(crate) fn attr_test<'a>( let alloc: &'static Arena = unsafe { bun_ptr::detach_lifetime_ref(&arena) }; let global = cx.global(); - let source = source.to_utf8(); - - let minify_arg = minify.to_js(); - let minify = minify_arg.is_boolean() && minify_arg.to_boolean(); - - let targets_arg = targets.to_js(); - let mut targets = Targets::default(); - if targets_arg.is_object() { - targets.browsers = Some(targets_from_js(global, targets_arg)?); - } + let targets = Targets { + browsers: targets.map(Browsers::from), + ..Default::default() + }; let mut log = Log::init(); @@ -338,7 +255,7 @@ pub(crate) fn attr_test<'a>( let mut import_records = Vec::::default(); match StyleAttribute::parse( alloc, - source.slice(), + source, &parser_options, &mut import_records, bun_ast::Index::INVALID, @@ -365,15 +282,16 @@ pub(crate) fn attr_test<'a>( Ok(r) => r, Err(_e) => { // The error is intentionally swallowed here. - return Ok(JSValue::UNDEFINED); + return Ok(CssTestOutput::Failure(JsLocal::UNDEFINED)); } }; - bun_string_jsc::create_utf8_for_js(global, &result.code) + Ok(CssTestOutput::Code(result.code)) } Err(err) => { if log.has_any() { - return log.to_js(global, format_args!("parsing failed:")); + let errors = log.to_js(global, format_args!("parsing failed:"))?; + return Ok(CssTestOutput::Failure(JsLocal::new(&cx.heap, errors))); } Err(global.throw(format_args!("parsing failed: {}", err.kind))) } diff --git a/src/install_jsc/hosted_git_info_jsc.bind.ts b/src/install_jsc/hosted_git_info_jsc.bind.ts index 46377b4cf3af..408d0a5f6db7 100644 --- a/src/install_jsc/hosted_git_info_jsc.bind.ts +++ b/src/install_jsc/hosted_git_info_jsc.bind.ts @@ -3,15 +3,24 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::hosted_git_info_jsc" }); export const parseUrl = b.fn({ - args: { url: b.string.allowStringObject }, + args: { url: b.string.utf8 }, returns: b.string, capability: "heap", throws: true, }); +export const HostedGitInfo = b.dictionary({ + type: { type: b.string, rustName: "host_provider" }, + domain: b.string, + project: b.string, + user: b.string.nullable, + committish: b.string.nullable, + default: { type: b.string, rustName: "default_representation" }, +}); + export const fromUrl = b.fn({ - args: { url: b.string.allowStringObject }, - returns: b.any, + args: { url: b.string.utf8 }, + returns: HostedGitInfo.nullable, capability: "heap", throws: true, }); diff --git a/src/install_jsc/hosted_git_info_jsc.rs b/src/install_jsc/hosted_git_info_jsc.rs index a72c3e370691..d4db7e7c4a5c 100644 --- a/src/install_jsc/hosted_git_info_jsc.rs +++ b/src/install_jsc/hosted_git_info_jsc.rs @@ -2,66 +2,13 @@ use bun_core::String as BunString; use bun_jsc::bindgen::{self, JsHeap}; -use bun_jsc::bun_string_jsc; -use bun_jsc::{JSGlobalObject, JSValue, JsResult, StringJsc}; -/// Extension trait providing `.to_js()` on `HostedGitInfo`. -trait HostedGitInfoJsc { - fn to_js(&self, go: &JSGlobalObject) -> JsResult; -} - -impl HostedGitInfoJsc for bun_install::hosted_git_info::HostedGitInfo { - fn to_js(&self, go: &JSGlobalObject) -> JsResult { - let obj = JSValue::create_empty_object(go, 6); - obj.put( - go, - b"type", - BunString::static_(self.host_provider.type_str()).to_js(go)?, - ); - obj.put( - go, - b"domain", - bun_string_jsc::create_utf8_for_js(go, self.host_provider.domain())?, - ); - obj.put( - go, - b"project", - bun_string_jsc::create_utf8_for_js(go, self.project())?, - ); - obj.put( - go, - b"user", - if let Some(user) = self.user() { - bun_string_jsc::create_utf8_for_js(go, user)? - } else { - JSValue::NULL - }, - ); - obj.put( - go, - b"committish", - if let Some(committish) = self.committish() { - bun_string_jsc::create_utf8_for_js(go, committish)? - } else { - JSValue::NULL - }, - ); - obj.put( - go, - b"default", - BunString::from_bytes(<&'static str>::from(self.default_representation).as_bytes()) - .to_js(go)?, - ); - - Ok(obj) - } -} +use crate::generated_bindgen::hosted_git_info_jsc::HostedGitInfo; -pub(crate) fn parse_url<'a>(heap: &JsHeap<'a>, url: &BunString) -> bindgen::Result<'a, BunString> { +pub(crate) fn parse_url<'a>(heap: &JsHeap<'a>, url: &[u8]) -> bindgen::Result<'a, BunString> { use bun_install::hosted_git_info as hgi; - let as_utf8 = url.to_utf8(); - let parsed = match hgi::parse_url(as_utf8.slice()) { + let parsed = match hgi::parse_url(url) { Ok(p) => p, Err(err) => { return Err(heap.error(format_args!( @@ -74,16 +21,17 @@ pub(crate) fn parse_url<'a>(heap: &JsHeap<'a>, url: &BunString) -> bindgen::Resu Ok(parsed.url.href()) } -pub(crate) fn from_url<'a>(heap: &JsHeap<'a>, url: &BunString) -> bindgen::Result<'a, JSValue> { - use bun_install::hosted_git_info::HostedGitInfo; +pub(crate) fn from_url<'a>( + heap: &JsHeap<'a>, + url: &[u8], +) -> bindgen::Result<'a, Option> { // TODO(markovejnovic): The original hosted-git-info actually takes another argument that // allows you to inject options. Seems untested so we didn't implement // it. - let as_utf8 = url.to_utf8(); - let parsed = match HostedGitInfo::from_url(as_utf8.slice()) { + let parsed = match bun_install::hosted_git_info::HostedGitInfo::from_url(url) { Ok(Some(p)) => p, - Ok(None) => return Ok(JSValue::NULL), + Ok(None) => return Ok(None), Err(err) => { return Err(heap.error(format_args!( "Invalid Git URL: {}", @@ -92,6 +40,14 @@ pub(crate) fn from_url<'a>(heap: &JsHeap<'a>, url: &BunString) -> bindgen::Resul } }; - // SAFETY: the object is new and plain, and its properties are defined and not set. - Ok(parsed.to_js(unsafe { heap.global() })?) + Ok(Some(HostedGitInfo { + host_provider: BunString::static_(parsed.host_provider.type_str()), + domain: BunString::clone_utf8(parsed.host_provider.domain()), + project: BunString::clone_utf8(parsed.project()), + user: parsed.user().map(BunString::clone_utf8), + committish: parsed.committish().map(BunString::clone_utf8), + default_representation: BunString::static_(<&'static str>::from( + parsed.default_representation, + )), + })) } diff --git a/src/install_jsc/npm_jsc.bind.ts b/src/install_jsc/npm_jsc.bind.ts index 0af3d46ad690..3c35680336bd 100644 --- a/src/install_jsc/npm_jsc.bind.ts +++ b/src/install_jsc/npm_jsc.bind.ts @@ -3,23 +3,21 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::npm_jsc" }); export const operatingSystemIsMatch = b.fn({ - args: { names: b.any }, + args: { names: b.array(b.string.loose.utf8.owned) }, returns: b.bool, - capability: "js", - throws: true, }); export const architectureIsMatch = b.fn({ - args: { names: b.any }, + args: { names: b.array(b.string.loose.utf8.owned) }, returns: b.bool, - capability: "js", - throws: true, }); +export const PackageManifest = b.dictionary({ name: b.string.utf8, versions: b.array(b.string.utf8) }); + export const npmManifestTestHelpers = b.namespace({ parseManifest: { - args: { manifestFilename: b.string.allowStringObject, registry: b.string.allowStringObject }, - returns: b.any, + args: { manifestFilename: b.string.utf8, registry: b.string.utf8 }, + returns: PackageManifest, capability: "heap", throws: true, }, diff --git a/src/install_jsc/npm_jsc.rs b/src/install_jsc/npm_jsc.rs index 22e0f32e247d..c0efc3aefac1 100644 --- a/src/install_jsc/npm_jsc.rs +++ b/src/install_jsc/npm_jsc.rs @@ -1,81 +1,59 @@ //! JSC host fns for `bun_install::npm`, kept here so that `install/` has //! no `JSValue`/`JSGlobalObject` references. -use bun_core::String as BunString; -use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; -use bun_jsc::{JSValue, JsResult, StringJsc as _}; +use bun_core::Utf8Bytes; +use bun_jsc::bindgen::{self, JsHeap}; -pub(crate) fn operating_system_is_match<'a>( - cx: &mut JsScope<'a>, - names: JsLocal<'a>, -) -> JsResult { +use crate::generated_bindgen::npm_jsc::PackageManifest; + +pub(crate) fn operating_system_is_match(names: Vec>) -> bool { use bun_install::npm; - let global = cx.global(); let mut operating_system = npm::OperatingSystem::NONE.negatable(); - let mut iter = names.to_js().array_iterator(global)?; - while let Some(item) = iter.next()? { - let slice = item.to_utf8(global)?; - operating_system.apply(slice.slice()); + for name in names { + operating_system.apply(name.slice()); } - Ok(operating_system + operating_system .combine() - .is_match(npm::OperatingSystem::CURRENT)) + .is_match(npm::OperatingSystem::CURRENT) } -pub(crate) fn architecture_is_match<'a>( - cx: &mut JsScope<'a>, - names: JsLocal<'a>, -) -> JsResult { +pub(crate) fn architecture_is_match(names: Vec>) -> bool { use bun_install::npm; - let global = cx.global(); let mut architecture = npm::Architecture::NONE.negatable(); - let mut iter = names.to_js().array_iterator(global)?; - while let Some(item) = iter.next()? { - let slice = item.to_utf8(global)?; - architecture.apply(slice.slice()); + for name in names { + architecture.apply(name.slice()); } - Ok(architecture.combine().is_match(npm::Architecture::CURRENT)) + architecture.combine().is_match(npm::Architecture::CURRENT) } -pub(crate) fn parse_manifest( - heap: &JsHeap<'_>, - manifest_filename: &BunString, - registry: &BunString, -) -> JsResult { +pub(crate) fn parse_manifest<'a>( + heap: &JsHeap<'a>, + manifest_filename: &[u8], + registry: &[u8], +) -> bindgen::Result<'a, PackageManifest> { use bstr::BStr; use bun_install::npm; - use bun_jsc::JsError; - use std::io::Write as _; - - // SAFETY: `JSON.parse` without a reviver runs no JavaScript, and what throws returns. - let global = unsafe { heap.global() }; - let manifest_filename = manifest_filename.to_utf8(); - let registry = registry.to_utf8(); - let manifest_file = match bun_sys::openat_a( - bun_sys::Fd::cwd(), - manifest_filename.slice(), - bun_sys::O::RDONLY, - 0, - ) { - Ok(fd) => bun_sys::File::from_fd(fd), - Err(err) => { - return Err(global.throw(format_args!( - "failed to open manifest file \"{}\": {}", - BStr::new(manifest_filename.slice()), - BStr::new(err.name()), - ))); - } - }; + let manifest_file = + match bun_sys::openat_a(bun_sys::Fd::cwd(), manifest_filename, bun_sys::O::RDONLY, 0) { + Ok(fd) => bun_sys::File::from_fd(fd), + Err(err) => { + return Err(heap.error(format_args!( + "failed to open manifest file \"{}\": {}", + BStr::new(manifest_filename), + BStr::new(err.name()), + ))); + } + }; let mut scope = npm::registry::Scope::default(); - scope.set_url(Box::from(registry.slice())); + scope.set_url(Box::from(registry)); let maybe_package_manifest = match npm::package_manifest::Serializer::load_by_file(&scope, &manifest_file) { Ok(m) => m, Err(err) => { - return Err(global.throw(format_args!( + return Err(heap.error(format_args!( "failed to load manifest file: {}", BStr::new(err.name()) ))); @@ -85,38 +63,22 @@ pub(crate) fn parse_manifest( let package_manifest: npm::PackageManifest = match maybe_package_manifest { Some(m) => m, None => { - return Err(global.throw(format_args!("manifest is invalid "))); + return Err(heap.error(format_args!("manifest is invalid "))); } }; - let mut buf: Vec = Vec::new(); - // TODO: we can add more information. for now just versions is fine - - write!( - &mut buf, - "{{\"name\":\"{}\",\"versions\":[", - BStr::new(package_manifest.name()), - ) - .map_err(|_| JsError::OutOfMemory)?; - - for (i, version) in package_manifest.versions.iter().enumerate() { - if i == package_manifest.versions.len() - 1 { - write!( - &mut buf, - "\"{}\"]}}", - version.fmt(&package_manifest.string_buf), - ) - .map_err(|_| JsError::OutOfMemory)?; - } else { - write!( - &mut buf, - "\"{}\",", - version.fmt(&package_manifest.string_buf), - ) - .map_err(|_| JsError::OutOfMemory)?; - } - } - - BunString::borrow_utf8(&buf).to_js_by_parse_json(global) + Ok(PackageManifest { + name: package_manifest.name().to_vec(), + versions: package_manifest + .versions + .iter() + .map(|version| { + version + .fmt(&package_manifest.string_buf) + .to_string() + .into_bytes() + }) + .collect(), + }) } diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 9463eaf1b184..c949f1ec38e3 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -280,8 +280,27 @@ export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { thisAndCallee: $bindgen("bindgen_test.bind.ts", "thisAndCallee"), doubled: $bindgen("bindgen_test.bind.ts", "doubled"), makeDoubled: $bindgen("bindgen_test.bind.ts", "makeDoubled"), + promisePtrContext: $bindgen("bindgen_test.bind.ts", "promisePtrContext"), + readPromisePtr: $bindgen("bindgen_test.bind.ts", "readPromisePtr"), + makeNativePromiseContext: $bindgen("bindgen_test.bind.ts", "makeNativePromiseContext"), + readNativePromiseContext: $bindgen("bindgen_test.bind.ts", "readNativePromiseContext"), primes: $bindgen("bindgen_test.bind.ts", "primes"), testNamespace: $bindgen("bindgen_test.bind.ts", "testNamespace"), + nodeOptionsSize: $bindgen("bindgen_test.bind.ts", "nodeOptionsSize"), + bareOptionsSize: $bindgen("bindgen_test.bind.ts", "bareOptionsSize"), + timeValue: $bindgen("bindgen_test.bind.ts", "timeValue"), + textIfPassed: $bindgen("bindgen_test.bind.ts", "textIfPassed"), + isPassed: $bindgen("bindgen_test.bind.ts", "isPassed"), + lengthWithValue: $bindgen("bindgen_test.bind.ts", "lengthWithValue"), + counterWithValue: $bindgen("bindgen_test.bind.ts", "counterWithValue"), + keptString: $bindgen("bindgen_test.bind.ts", "keptString"), + bracketEcho: $bindgen("bindgen_test.bind.ts", "bracketEcho"), + describeHandle: $bindgen("bindgen_test.bind.ts", "describeHandle"), + isObject: $bindgen("bindgen_test.bind.ts", "isObject"), + objectEcho: $bindgen("bindgen_test.bind.ts", "objectEcho"), + absenceEcho: $bindgen("bindgen_test.bind.ts", "absenceEcho"), + callbackOrDelay: $bindgen("bindgen_test.bind.ts", "callbackOrDelay"), + finiteLater: $bindgen("bindgen_test.bind.ts", "finiteLater"), integerOrBigInt: $bindgen("bindgen_test.bind.ts", "integerOrBigInt"), looseInteger: $bindgen("bindgen_test.bind.ts", "looseInteger"), looseIntegerOrBigInt: $bindgen("bindgen_test.bind.ts", "looseIntegerOrBigInt"), diff --git a/src/jsc/BytecodeOrderRecorder.bind.ts b/src/jsc/BytecodeOrderRecorder.bind.ts index fc6ceb0e3eb2..3ef0aa08f4d1 100644 --- a/src/jsc/BytecodeOrderRecorder.bind.ts +++ b/src/jsc/BytecodeOrderRecorder.bind.ts @@ -2,10 +2,13 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::bytecode_order_recorder" }); +export const BytecodeOrderTextKind = b.enumeration(["module", "script", "builtin", "internal"]); + +/** `unknown`: no internal module has that specifier. `unnamed`: the text does not parse. */ +export const BytecodeOrderNames = b.union({ unknown: b.undefined, unnamed: b.null, lines: b.string.utf8 }); + export const namesForTesting = b.fn({ - args: { text: b.string.loose.utf8, kind: b.string.loose.utf8, chunkPaths: b.string.loose.utf8.optional }, - returns: b.any, - capability: "heap", - throws: true, + args: { text: b.string.loose.utf8, kind: BytecodeOrderTextKind, chunkPaths: b.string.loose.utf8.optional }, + returns: BytecodeOrderNames, length: 3, }); diff --git a/src/jsc/BytecodeOrderRecorder.rs b/src/jsc/BytecodeOrderRecorder.rs index 35961b8bad21..16982d7f30ab 100644 --- a/src/jsc/BytecodeOrderRecorder.rs +++ b/src/jsc/BytecodeOrderRecorder.rs @@ -7,7 +7,9 @@ use bun_bundler::bytecode_order::{self, CodeNames, RecordedFunction, RecordedSource, Recording}; -use crate::bindgen::{self, JsHeap, JsLocal}; +use crate::generated_bindgen::bytecode_order_recorder::{ + BytecodeOrderNames, BytecodeOrderTextKind, +}; use crate::virtual_machine::VirtualMachine; use crate::vm::VM; @@ -397,12 +399,11 @@ fn write_replacing(path: &[u8], text: &[u8]) -> bun_sys::Maybe<()> { /// `bun:internal-for-testing`: what an order file calls the code of `text` (a `"module"` or `"script"` whose other chunks' /// paths start with the third argument, a `"builtin"`, or the specifier of an `"internal"` module, undefined if there is /// none), as `M ` and ` ` lines, or null for a text that has no names. -pub(crate) fn names_for_testing<'a>( - heap: &JsHeap<'a>, +pub(crate) fn names_for_testing( text: &[u8], - kind: &[u8], + kind: BytecodeOrderTextKind, chunk_paths: Option<&[u8]>, -) -> bindgen::Result<'a, JsLocal<'a>> { +) -> BytecodeOrderNames { // One path to a line. let mut chunk_paths: Vec<&[u8]> = bun_core::strings::split(chunk_paths.unwrap_or(b""), b"\n") .filter(|path| !path.is_empty()) @@ -414,11 +415,11 @@ pub(crate) fn names_for_testing<'a>( chunk_paths: &chunk_paths, }; let names = match kind { - b"module" => chunk(true).names(), - b"script" => chunk(false).names(), - b"builtin" => bytecode_order::Named::InternalModule(text).names(), + BytecodeOrderTextKind::Module => chunk(true).names(), + BytecodeOrderTextKind::Script => chunk(false).names(), + BytecodeOrderTextKind::Builtin => bytecode_order::Named::InternalModule(text).names(), // `text` is the specifier of one of this executable's internal modules, which are all ASCII. - b"internal" => { + BytecodeOrderTextKind::Internal => { let builtins = bun_exe_format::builtins::Builtins::parse( crate::cached_bytecode::__bun_jsc_host_builtins(), ) @@ -427,20 +428,15 @@ pub(crate) fn names_for_testing<'a>( .as_ref() .and_then(|builtins| builtins.module(builtins.find(text)?)) else { - return Ok(JsLocal::UNDEFINED); + return BytecodeOrderNames::Unknown; }; bytecode_order::Named::InternalModule(module.source).names() } - _ => { - return Err(heap.error(format_args!( - "expected \"module\", \"script\", \"builtin\" or \"internal\"" - ))); - } }; let Some(names) = names else { - return Ok(JsLocal::NULL); + return BytecodeOrderNames::Unnamed; }; let mut lines = Vec::new(); bytecode_order::write_names(&mut lines, &names); - heap.create_utf8_string(&lines).map(JsLocal::as_value) + BytecodeOrderNames::Lines(lines) } diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 690e170952bf..e0a876975832 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -1266,42 +1266,8 @@ impl ErrorDisplayLevel { } impl FormatOptions { - pub fn from_js(&mut self, global_this: &JSGlobalObject, arguments: &[JSValue]) -> JsResult<()> { - let arg1 = arguments[0]; - - if arg1.is_object() { - if let Some(opt) = arg1.get_truthy(global_this, "depth")? { - self.set_depth(global_this, opt)?; - } - if let Some(opt) = arg1.get_boolean_loose(global_this, "colors")? { - self.enable_colors = opt; - } - if let Some(opt) = arg1.get_boolean_loose(global_this, "sorted")? { - self.ordered_properties = opt; - } - if let Some(opt) = arg1.get_boolean_loose(global_this, "compact")? { - self.single_line = opt; - } - } else { - // formatOptions.show_hidden = arg1.toBoolean(); - if !arguments.is_empty() { - self.set_depth(global_this, arg1)?; - if arguments.len() > 1 && !arguments[1].is_empty_or_undefined_or_null() { - self.enable_colors = arguments[1].to_boolean(); - } - } - } - Ok(()) - } - - /// `depth`: a non-negative integer (clamped to `u16::MAX`) or `Infinity`; non-numbers are - /// ignored. Decided on the number's value, so an integer JSC boxed as a double (a - /// `Float64Array` element, `-0`) is accepted like its int32 twin. - fn set_depth(&mut self, global_this: &JSGlobalObject, depth: JSValue) -> JsResult<()> { - if !depth.is_number() { - return Ok(()); - } - let depth = depth.as_number(); + /// `depth`: a non-negative integer (clamped to `u16::MAX`) or `Infinity`. + pub fn set_depth(&mut self, global_this: &JSGlobalObject, depth: f64) -> JsResult<()> { if depth.is_infinite() { self.max_depth = u16::MAX; } else if depth.trunc() != depth { diff --git a/src/jsc/Counters.bind.ts b/src/jsc/Counters.bind.ts index 9c8d17b9080c..c7b29110b2bf 100644 --- a/src/jsc/Counters.bind.ts +++ b/src/jsc/Counters.bind.ts @@ -2,4 +2,6 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::counters" }); -export const createCountersObject = b.fn({ returns: b.any, capability: "heap", throws: true }); +export const CountersObject = b.dictionary({ spawnSync_blocking: b.i32, spawn_memfd: b.i32 }); + +export const createCountersObject = b.fn({ returns: CountersObject, capability: "heap" }); diff --git a/src/jsc/Counters.rs b/src/jsc/Counters.rs index 9beee9e5761e..e325bb352723 100644 --- a/src/jsc/Counters.rs +++ b/src/jsc/Counters.rs @@ -1,5 +1,5 @@ use crate::bindgen::JsHeap; -use crate::{JSGlobalObject, JSValue, JsResult}; +use crate::generated_bindgen::counters::CountersObject; #[repr(C)] #[derive(Debug, Default, Clone, Copy)] @@ -16,27 +16,15 @@ impl Counters { }; *slot = slot.saturating_add(1); } - - pub fn to_js(self, global: &JSGlobalObject) -> JsResult { - let obj = JSValue::create_empty_object(global, 2); - obj.put( - global, - b"spawnSync_blocking", - JSValue::js_number_from_int32(self.spawn_sync_blocking), - ); - obj.put( - global, - b"spawn_memfd", - JSValue::js_number_from_int32(self.spawn_memfd), - ); - Ok(obj) - } } -pub(crate) fn create_counters_object(heap: &JsHeap<'_>) -> JsResult { - // SAFETY: the object is new and plain, and its properties are defined and not set. - let global = unsafe { heap.global() }; - global.bun_vm().counters.to_js(global) +pub(crate) fn create_counters_object(heap: &JsHeap<'_>) -> CountersObject { + // SAFETY: only the VM is taken from it. + let counters = unsafe { heap.global() }.bun_vm().counters; + CountersObject { + spawn_sync_blocking: counters.spawn_sync_blocking, + spawn_memfd: counters.spawn_memfd, + } } #[derive(Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)] diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 5645001447c9..3a4e40f0a1a5 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -76,8 +76,8 @@ impl JSValue { } /// `JSValue.fromPtrAddress` — encode an arbitrary native pointer as a JS - /// number (round-trips via `as_promise_ptr`). Used to smuggle a `*mut T` - /// context through `Promise.then` reaction arguments. + /// number. Used to smuggle a `*mut T` context through `Promise.then` + /// reaction arguments, which a reaction takes as `b.promisePtr`. #[inline] pub fn from_ptr_address(addr: usize) -> JSValue { // Always double-encoded via `jsDoubleNumber` (never the int32 fast @@ -92,13 +92,6 @@ impl JSValue { self.as_number() as usize } - /// `JSValue.asPromisePtr` — decode a `*mut T` smuggled - /// through [`from_ptr_address`] as the trailing `.then` reaction argument. - #[inline] - pub fn as_promise_ptr(self) -> *mut T { - self.as_ptr_address() as *mut T - } - /// Attach `(resolve, reject)` reactions to this Promise, passing `ctx` as /// the trailing argument to each. Thin wrapper over `JSC__JSValue___then`. /// diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index be633e260cbe..f9cf7cf05498 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -8,8 +8,8 @@ use core::ptr::NonNull; use bun_core::EncodedSlice; use crate::{ - ArrayBuffer, CommonString, EncodedSliceJsc as _, ErrorCode, JSGlobalObject, JSObject, JSString, - JSValue, JsError, JsThread, PinnedArrayBuffer, ThreadIsolated, ThreadIsolatedArg, + ArrayBuffer, CommonString, EncodedSliceJsc as _, ErrorCode, JSArray, JSGlobalObject, JSObject, + JSString, JSValue, JsError, JsThread, PinnedArrayBuffer, ThreadIsolated, ThreadIsolatedArg, }; #[path = "bindgen/extern_types.rs"] @@ -67,6 +67,38 @@ impl<'a> JsHeap<'a> { } } + /// An array of what `convert`, a function of [`to_js`], makes of each item. + #[inline] + pub fn create_array( + &self, + items: I, + mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsLocal<'a>>, + ) -> Result<'a, JsLocal<'a, JSArray>> { + let global = self.global_object(); + // A short array is made with one call. A longer one takes a call per element. + let mut short = [JSValue::ZERO; 8]; + let mut items = items.into_iter(); + for count in 0..short.len() { + let Some(item) = items.next() else { + // SAFETY: `count` initialized values. + return JsLocal::from_js_result(crate::call_zero_is_throw(global, || unsafe { + extern_types::Bun__Bindgen__constructArrayFromValues( + self.global_ptr(), + short.as_ptr(), + count, + ) + })); + }; + short[count] = convert(self, item)?.0; + } + let array = JSValue::create_empty_array(global, short.len() + items.size_hint().0)?; + let rest = items.map(|item| convert(self, item).map(|value| value.0)); + for (index, value) in short.into_iter().map(Ok).chain(rest).enumerate() { + array.put_index(global, index as u32, value?)?; + } + Ok(JsLocal::from_raw(array)) + } + #[inline] pub fn create_utf8_string(&self, utf8: &[u8]) -> Result<'a, JsLocal<'a, JSString>> { JsLocal::from_js_result(crate::bun_string_jsc::create_utf8_for_js( @@ -938,31 +970,9 @@ pub mod to_js { pub fn array<'a, I: IntoIterator>( heap: &JsHeap<'a>, items: I, - mut convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsLocal<'a>>, + convert: impl FnMut(&JsHeap<'a>, I::Item) -> Result<'a, JsLocal<'a>>, ) -> Result<'a, JsLocal<'a>> { - let global = heap.global_object(); - // A short array is made with one call. A longer one takes a call per element. - let mut short = [JSValue::ZERO; 8]; - let mut items = items.into_iter(); - for count in 0..short.len() { - let Some(item) = items.next() else { - // SAFETY: `count` initialized values. - return JsLocal::from_js_result(crate::call_zero_is_throw(global, || unsafe { - extern_types::Bun__Bindgen__constructArrayFromValues( - heap.global_ptr(), - short.as_ptr(), - count, - ) - })); - }; - short[count] = convert(heap, item)?.0; - } - let array = JSValue::create_empty_array(global, short.len() + items.size_hint().0)?; - let rest = items.map(|item| convert(heap, item).map(|value| value.0)); - for (index, value) in short.into_iter().map(Ok).chain(rest).enumerate() { - array.put_index(global, index as u32, value?)?; - } - Ok(JsLocal::from_raw(array)) + heap.create_array(items, convert).map(JsLocal::as_value) } /// A key that comes again keeps its place and takes the new value. The inline room is the upper `size_hint`. diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 1f8e1ea8796e..eb963e71523b 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -392,10 +392,85 @@ export const thisAndCallee = b.fn({ export const doubled = b.fn({ export: "Bun__BindgenTest__doubled", args: { value: b.i32 }, returns: b.i32 }); /** A new function, made in Rust, that runs `doubled`. */ export const makeDoubled = b.fn({ returns: b.any, capability: "js" }); +/** What `JSValue::then` gives a reaction as its context, for a static that holds 42. */ +export const promisePtrContext = b.value({ returns: b.any, capability: "heap" }); +export const readPromisePtr = b.fn({ + args: { context: b.promisePtr("core::sync::atomic::AtomicI32") }, + returns: b.i32, +}); +/** A new `NativePromiseContext` cell with a pointer to the same static. */ +export const makeNativePromiseContext = b.fn({ returns: b.any, capability: "js" }); +export const readNativePromiseContext = b.fn({ + args: { context: b.nativePromiseContext("core::sync::atomic::AtomicI32") }, + returns: b.i32.nullable, +}); export const primes = b.value({ returns: b.array(b.u32) }); export const testNamespace = b.namespace({ increment: { args: { value: b.i32 }, returns: b.i32 }, decrement: { args: { value: b.i32 }, returns: b.i32 }, + negate: [ + { args: { value: b.f64 }, returns: b.f64, rustPath: "crate::bindgen_test::negate_number" }, + { args: { value: b.bool }, returns: b.bool, rustPath: "crate::bindgen_test::negate_flag" }, + ], +}); + +/** Reports its members as Node does. */ +export const TestNodeOptions = b.dictionary( + { size: b.u32.validateInteger(1, 10).optional, flag: { type: b.bool.default(false), expected: ["boolean"] } }, + { reportedAs: "options" }, +); +export const TestBareOptions = b.dictionary({ size: b.u32.validateInteger(1, 10).optional }, { reportedAs: "" }); +export const nodeOptionsSize = b.fn({ args: { options: TestNodeOptions }, returns: b.u32.optional }); +export const bareOptionsSize = b.fn({ args: { options: TestBareOptions }, returns: b.u32.optional }); + +export const timeValue = b.fn({ args: { time: b.f64.allowDate }, returns: b.f64 }); +/** "none" without the argument. `undefined` is converted, to "undefined". */ +export const textIfPassed = b.fn({ args: { text: b.string.loose.utf8.ifPassed }, returns: b.string.utf8 }); +export const isPassed = b.fn({ args: { first: b.any, second: b.any.ifPassed }, returns: b.bool }); +/** `[the text's length, the argument]`. */ +export const lengthWithValue = b.fn({ + args: { text: b.string.loose.utf8.withValue }, + returns: b.any, + capability: "heap", + throws: true, +}); +export const counterWithValue = b.fn({ args: { counter: TestCounter.withValue }, returns: b.any, capability: "heap" }); +export const keptString = b.fn({ args: { text: b.string.owned }, returns: b.string }); + +export const TestBracket = b.enumeration({ Plain: "rgb", Square: "[rgb]", Curly: ["{rgb}", "{RGB}"] }); +export const bracketEcho = b.fn({ args: { bracket: TestBracket }, returns: TestBracket }); + +export const TestPoint = b.dictionary({ x: b.f64 }); +/** `point` gets objects only, so `text` gets every other value that no alternative before it takes. */ +export const TestHandle = b.union({ + missing: b.undefined, + nothing: b.null, + callback: b.callable, + list: b.jsArray, + point: TestPoint, + text: b.string.loose.utf8, +}); +export const describeHandle = b.fn({ args: { value: TestHandle }, returns: b.string.utf8 }); +export const TestObjectOrRest = b.union({ object: b.object, rest: b.any }); +export const isObject = b.fn({ args: { value: TestObjectOrRest }, returns: b.bool }); +export const objectEcho = b.fn({ args: { value: b.object }, returns: b.object, capability: "heap" }); +export const TestAbsence = b.union({ missing: b.undefined, nothing: b.null, number: b.f64 }); +export const absenceEcho = b.fn({ args: { value: TestAbsence }, returns: TestAbsence }); + +const withThis = { passThis: true, export: "Bun__BindgenTest__callbackOrDelay", returns: b.string.utf8 }; +/** Told apart by whether the first argument can be called. */ +export const callbackOrDelay = b.overloads( + { ...withThis, args: { callback: b.callable }, rustPath: "crate::bindgen_test::callback_or_delay_callback" }, + { ...withThis, args: { delay: b.f64 }, rustPath: "crate::bindgen_test::callback_or_delay_delay" }, +); + +/** A promise for the number. One that is not finite, and an argument that is not a number, reject it. */ +export const finiteLater = b.fn({ + args: { value: b.f64 }, + returns: b.any, + capability: "js", + throws: true, + rejects: true, }); export const integerOrBigInt = b.fn({ args: { value: b.u32.allowBigInt }, returns: b.u32 }); diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 015af6268983..7accfaaa19fa 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -9,8 +9,9 @@ use crate::bindgen::{ JsScope, JsStructure, JsWrapper, PinnedBytes, StringOrJsBytes, }; use crate::generated_bindgen::bindgen_test::{ - Bun__BindgenTest__doubled, TestAnything, TestBare, TestChoice, TestColor, TestColorOrRest, - TestFailure, TestFlag, TestNames, TestOptions, TestOptionsEcho, TestOuter, TestTextOrBytes, + Bun__BindgenTest__doubled, TestAbsence, TestAnything, TestBare, TestBareOptions, TestBracket, + TestChoice, TestColor, TestColorOrRest, TestFailure, TestFlag, TestHandle, TestNames, + TestNodeOptions, TestObjectOrRest, TestOptions, TestOptionsEcho, TestOuter, TestTextOrBytes, TestValue, js_TestCounter, js_TestError, js_TestNonFinal, js_TestNote, js_TestRefCounted, js_TestRefCountedNote, }; @@ -821,6 +822,40 @@ pub(crate) fn make_doubled(cx: &mut JsScope<'_>) -> JSValue { ) } +static PROMISE_CONTEXT: AtomicI32 = AtomicI32::new(42); + +pub(crate) fn promise_ptr_context(_: &JsHeap<'_>) -> JSValue { + JSValue::from_ptr_address(PROMISE_CONTEXT.as_ptr() as usize) +} + +pub(crate) fn read_promise_ptr(context: *mut AtomicI32) -> i32 { + // SAFETY: `promise_ptr_context` made it of a static. + unsafe { &*context }.load(Ordering::Relaxed) +} + +pub(crate) fn make_native_promise_context(cx: &mut JsScope<'_>) -> JSValue { + unsafe extern "C" { + safe fn Bun__NativePromiseContext__create( + global: &JSGlobalObject, + ctx: *mut core::ffi::c_void, + tag: u8, + held: JSValue, + ) -> JSValue; + } + // Only the destructor of a cell that still has its pointer reads the tag, and the test takes the pointer. + Bun__NativePromiseContext__create( + cx.global(), + PROMISE_CONTEXT.as_ptr().cast(), + 0, + JSValue::ZERO, + ) +} + +pub(crate) fn read_native_promise_context(context: Option>) -> Option { + // SAFETY: `make_native_promise_context` made the cell of a static. + context.map(|context| unsafe { context.as_ref() }.load(Ordering::Relaxed)) +} + pub(crate) fn primes() -> [u32; 3] { [2, 3, 5] } @@ -833,6 +868,110 @@ pub(crate) fn decrement(value: i32) -> i32 { value.wrapping_sub(1) } +pub(crate) fn negate_number(value: f64) -> f64 { + -value +} + +pub(crate) fn negate_flag(value: bool) -> bool { + !value +} + +pub(crate) fn node_options_size(options: &TestNodeOptions) -> Option { + options.size.filter(|_| !options.flag) +} + +pub(crate) fn bare_options_size(options: &TestBareOptions) -> Option { + options.size +} + +pub(crate) fn time_value(time: f64) -> f64 { + time +} + +pub(crate) fn text_if_passed(text: Option<&[u8]>) -> &[u8] { + text.unwrap_or(b"none") +} + +pub(crate) fn is_passed(_: JsLocal<'_>, second: Option>) -> bool { + second.is_some() +} + +pub(crate) fn length_with_value<'a>( + heap: &JsHeap<'a>, + (text, value): (&[u8], JsLocal<'a>), +) -> bindgen::Result<'a, JsLocal<'a>> { + let length = bindgen::to_js::usize(heap, text.len())?; + bindgen::to_js::array(heap, [length, value], bindgen::to_js::any) +} + +pub(crate) fn counter_with_value<'a>( + _: &JsHeap<'a>, + (counter, value): (&TestCounter, JsLocal<'a>), +) -> JsLocal<'a> { + counter.count.set(counter.count.get() + 1); + value +} + +pub(crate) fn kept_string(text: bun_core::String) -> bun_core::String { + text +} + +pub(crate) fn bracket_echo(bracket: TestBracket) -> TestBracket { + bracket +} + +pub(crate) fn describe_handle(value: TestHandle<'_>) -> Vec { + match value { + TestHandle::Missing => b"missing".to_vec(), + TestHandle::Nothing => b"nothing".to_vec(), + TestHandle::Callback(_) => b"callback".to_vec(), + TestHandle::List(_) => b"list".to_vec(), + TestHandle::Point(point) => format!("point {}", point.x).into_bytes(), + TestHandle::Text(text) => [b"text ", text].concat(), + } +} + +pub(crate) fn is_object(value: TestObjectOrRest<'_>) -> bool { + matches!(value, TestObjectOrRest::Object(_)) +} + +pub(crate) fn object_echo<'a>( + _: &JsHeap<'a>, + value: JsLocal<'a, crate::JSObject>, +) -> JsLocal<'a, crate::JSObject> { + value +} + +pub(crate) fn absence_echo(value: TestAbsence) -> TestAbsence { + value +} + +pub(crate) fn callback_or_delay_callback( + this_value: JsLocal<'_>, + _: JsLocal<'_, Callable>, +) -> Vec { + format!("callback, this is undefined: {}", this_value.is_undefined()).into_bytes() +} + +pub(crate) fn callback_or_delay_delay(this_value: JsLocal<'_>, delay: f64) -> Vec { + format!( + "delay {delay}, this is undefined: {}", + this_value.is_undefined() + ) + .into_bytes() +} + +pub(crate) fn finite_later(cx: &mut JsScope<'_>, value: f64) -> JsResult { + let global = cx.global(); + if !value.is_finite() { + return Err(global.throw_invalid_arguments(format_args!("value must be finite"))); + } + Ok(crate::JSPromise::resolved_promise_value( + global, + JSValue::js_number(value), + )) +} + pub(crate) struct TestError { code: i32, } diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index 1dde8a6d2900..be0171c5ab33 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -22,6 +22,10 @@ template struct IDLHumanReadableName> : IDLHumanReadableName> {}; +template +struct IDLHumanReadableName> + : IDLHumanReadableName {}; + template struct IDLHumanReadableName> : BaseIDLHumanReadableName { static constexpr bool hasPreposition = true; @@ -54,6 +58,18 @@ template<> struct IDLHumanReadableName : BaseIDLHumanRe static constexpr auto humanReadableName = std::to_array("function"); }; +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("object"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("array"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("undefined"); +}; + template struct IDLHumanReadableName> : BaseIDLHumanReadableName { static constexpr auto humanReadableName = name; }; @@ -236,6 +252,38 @@ template<> struct WebCore::Converter : Bun::Defaul } }; +template<> struct WebCore::Converter : Bun::DefaultTryConverter { + template + static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) + { + if (!value.isObject()) + return std::nullopt; + return value; + } + + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + ctx.throwNotObject(globalObject, scope); + } +}; + +template<> struct WebCore::Converter : Bun::DefaultTryConverter { + template + static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) + { + if (!JSC::isJSArray(value)) + return std::nullopt; + return value; + } + + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + ctx.throwNotArray(globalObject, scope); + } +}; + template struct WebCore::Converter> : Bun::DefaultTryConverter> { template @@ -277,6 +325,14 @@ template<> struct WebCore::Converter : WebCore::Default } }; +// No type test: only the promise calls a reaction, with the context it was given. +template<> struct WebCore::Converter : WebCore::DefaultConverter { + static void* convert(JSC::JSGlobalObject&, JSC::JSValue value) + { + return reinterpret_cast(static_cast(value.asDouble())); + } +}; + template struct WebCore::Converter> : Bun::DefaultContextConverter> { @@ -314,6 +370,21 @@ struct WebCore::Converter> } }; +template +struct WebCore::Converter> + : Bun::DefaultContextConverter> { + + using ReturnType = WebCore::Converter>::ReturnType; + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + if (value.isEmpty()) + return IDL::nullValue(); + return Bun::convertIDL(globalObject, value, ctx); + } +}; + template<> struct WebCore::Converter : Bun::DefaultTryConverter { diff --git a/src/jsc/bindings/Bindgen/IDLConvertNativePromiseContext.h b/src/jsc/bindings/Bindgen/IDLConvertNativePromiseContext.h new file mode 100644 index 000000000000..03e09a565b96 --- /dev/null +++ b/src/jsc/bindings/Bindgen/IDLConvertNativePromiseContext.h @@ -0,0 +1,12 @@ +#pragma once +#include "Bindgen/IDLTypes.h" +#include "JSDOMConvertBase.h" +#include "NativePromiseContext.h" + +// As for IDLPromisePtr. +template<> struct WebCore::Converter : WebCore::DefaultConverter { + static void* convert(JSC::JSGlobalObject&, JSC::JSValue value) + { + return uncheckedDowncast(value)->take(); + } +}; diff --git a/src/jsc/bindings/Bindgen/IDLConvertNumbers.h b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h index f1c2cc6d726e..03495ff4ddae 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertNumbers.h +++ b/src/jsc/bindings/Bindgen/IDLConvertNumbers.h @@ -1,6 +1,7 @@ #pragma once #include "Bindgen/IDLTypes.h" #include "BunIDLConvertBase.h" +#include #include #include #include @@ -142,6 +143,27 @@ struct WebCore::Converter : Bun::DefaultTryConver } }; +template<> +struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) + { + if (value.isNumber()) + return value.asNumber(); + if (auto* date = dynamicDowncast(value)) + return date->internalNumber(); + return std::nullopt; + } + + template + static void throwConversionFailed(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) + { + ctx.throwTypeMustBe(globalObject, scope, "a number or Date"_s); + } +}; + template<> struct WebCore::Converter : Bun::DefaultContextConverter { diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index 7b03aa05cf3b..e99b94b0d0ed 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -31,6 +31,7 @@ struct IDLVariadicArguments : WebCore::IDLType {}; // For use in unions, to represent a nullable union. struct IDLStrictNull : WebCore::IDLType {}; +struct IDLStrictUndefined : WebCore::IDLType {}; // Treats all falsy values as null. template @@ -40,6 +41,10 @@ struct IDLFalsyAsNull : WebCore::IDLNullable {}; template struct IDLEmptyStringAsNull : WebCore::IDLNullable {}; +// Treats the empty value, which stands for an argument the call does not have, as null. +template +struct IDLNotPassedAsNull : WebCore::IDLNullable {}; + enum class AllowBigInt : bool { No, Yes, @@ -48,6 +53,8 @@ enum class AllowBigInt : bool { template struct IDLStrictInteger : WebCore::IDLInteger {}; struct IDLStrictDouble : WebCore::IDLUnrestrictedDouble {}; +// A Date is its time value. +struct IDLStrictDoubleOrDate : WebCore::IDLUnrestrictedDouble {}; // Converts to a numeric first. A BigInt is the double nearest to it. struct IDLLooseDoubleOrBigInt : WebCore::IDLUnrestrictedDouble {}; struct IDLStrictBoolean : WebCore::IDLBoolean {}; @@ -87,6 +94,12 @@ struct IDLArrayBufferRef : IDLRef {}; // Defined in IDLConvertBlob.h struct IDLBlobRef; +// The pointer that `JSValue::then` in Rust gave a promise reaction as its context: `JSValue::from_ptr_address`, a double. +struct IDLPromisePtr : WebCore::IDLType {}; + +// The NativePromiseContext cell that a promise reaction was given as its context, as what `take()` returns. +struct IDLNativePromiseContext : WebCore::IDLType {}; + // A dictionary that holds a raw `JSValue` or a `JSStringUTF8View`. Its struct is WTF_FORBID_HEAP_ALLOCATION. template struct IDLStackOnlyDictionary : WebCore::IDLType { @@ -141,6 +154,12 @@ struct IDLRawBufferSource : IDLRawAny {}; // A function, or a proxy of one, as the `JSValue` itself. See IDLRawAny. struct IDLRawCallable : IDLRawAny {}; +// An object as the `JSValue` itself. See IDLRawAny. +struct IDLRawObject : IDLRawAny {}; + +// An array as the `JSValue` itself. See IDLRawAny. +struct IDLRawArray : IDLRawAny {}; + // An instance of a generated class, as the cell itself. The caller's frame keeps it alive. template struct IDLRawInterface : WebCore::IDLType { diff --git a/src/jsc/event_loop.bind.ts b/src/jsc/event_loop.bind.ts index 79240da8ab99..c80afd6efa28 100644 --- a/src/jsc/event_loop.bind.ts +++ b/src/jsc/event_loop.bind.ts @@ -2,4 +2,16 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::event_loop" }); -export const getActiveTasks = b.fn({ returns: b.any, capability: "heap" }); +export const ActiveTasks = b.dictionary({ + activeTasks: b.usize, + tasks: b.usize, + immediateTasks: b.usize, + concurrentTasksEmpty: b.bool, + loopActive: b.bool, + eventLoopAlive: b.bool, + concurrentRef: b.i32, + numPolls: b.i32, + iteration: b.u64, +}); + +export const getActiveTasks = b.fn({ returns: ActiveTasks, capability: "heap" }); diff --git a/src/jsc/event_loop.rs b/src/jsc/event_loop.rs index 4c3d1512a873..ab63579bfdee 100644 --- a/src/jsc/event_loop.rs +++ b/src/jsc/event_loop.rs @@ -1380,49 +1380,12 @@ impl EventLoop { } /// Testing API to expose event loop state -pub(crate) fn get_active_tasks(heap: &crate::bindgen::JsHeap<'_>) -> JSValue { - // SAFETY: the object is new and plain, and its properties are defined and not set. - let global_object = unsafe { heap.global() }; - let vm_ref = global_object.bun_vm(); +pub(crate) fn get_active_tasks( + heap: &crate::bindgen::JsHeap<'_>, +) -> crate::generated_bindgen::event_loop::ActiveTasks { + // SAFETY: only the VM is taken from it. + let vm_ref = unsafe { heap.global() }.bun_vm(); let event_loop = vm_ref.event_loop_shared(); - let result = JSValue::create_empty_object(global_object, 9); - result.put( - global_object, - b"activeTasks", - JSValue::js_number(vm_ref.active_tasks as f64), - ); - result.put( - global_object, - b"tasks", - JSValue::js_number(event_loop.tasks.readable_length() as f64), - ); - result.put( - global_object, - b"immediateTasks", - JSValue::js_number( - (event_loop.immediate_tasks.len() + event_loop.next_immediate_tasks.len()) as f64, - ), - ); - result.put( - global_object, - b"concurrentTasksEmpty", - JSValue::from(event_loop.concurrent_tasks.is_empty()), - ); - result.put( - global_object, - b"loopActive", - JSValue::from(vm_ref.platform_loop_opt().is_some_and(|h| h.is_active())), - ); - result.put( - global_object, - b"eventLoopAlive", - JSValue::from(vm_ref.is_event_loop_alive()), - ); - result.put( - global_object, - b"concurrentRef", - JSValue::js_number(event_loop.concurrent_ref.load(Ordering::SeqCst) as f64), - ); #[cfg(windows)] // SAFETY: `Loop::get()` returns the live process-global `uv_loop_t`. let num_polls: i32 = @@ -1431,18 +1394,18 @@ pub(crate) fn get_active_tasks(heap: &crate::bindgen::JsHeap<'_>) -> JSValue { #[cfg(not(windows))] // SAFETY: uws::Loop::get() returns a live process-global loop. let num_polls: i32 = unsafe { (*uws::Loop::get()).num_polls }; - result.put( - global_object, - b"numPolls", - JSValue::js_number(num_polls as f64), - ); - result.put( - global_object, - b"iteration", + crate::generated_bindgen::event_loop::ActiveTasks { + active_tasks: vm_ref.active_tasks, + tasks: event_loop.tasks.readable_length(), + immediate_tasks: event_loop.immediate_tasks.len() + event_loop.next_immediate_tasks.len(), + concurrent_tasks_empty: event_loop.concurrent_tasks.is_empty(), + loop_active: vm_ref.platform_loop_opt().is_some_and(|h| h.is_active()), + event_loop_alive: vm_ref.is_event_loop_alive(), + concurrent_ref: event_loop.concurrent_ref.load(Ordering::SeqCst), + num_polls, // SAFETY: usockets_loop() returns the live process-global loop. - JSValue::js_number(unsafe { (*event_loop.usockets_loop()).iteration_number() } as f64), - ); - result + iteration: unsafe { (*event_loop.usockets_loop()).iteration_number() }, + } } #[cfg(windows)] diff --git a/src/jsc/rare_data.bind.ts b/src/jsc/rare_data.bind.ts index 89fba4ff7224..6f3addd0610c 100644 --- a/src/jsc/rare_data.bind.ts +++ b/src/jsc/rare_data.bind.ts @@ -2,5 +2,9 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::rare_data" }); -export const setTLSDefaultCiphers = b.fn({ args: { ciphers: b.any }, capability: "js", throws: true }); +export const setTLSDefaultCiphers = b.fn({ + args: { ciphers: b.string.utf8 }, + expected: { ciphers: ["string"] }, + capability: "heap", +}); export const getTLSDefaultCiphers = b.fn({ returns: b.string.utf8, capability: "heap" }); diff --git a/src/jsc/rare_data.rs b/src/jsc/rare_data.rs index 9fedc34bdff3..92f859b9848c 100644 --- a/src/jsc/rare_data.rs +++ b/src/jsc/rare_data.rs @@ -3,10 +3,10 @@ use core::ptr::NonNull; use core::sync::atomic::Ordering; use std::sync::Arc; -use crate::bindgen::{JsHeap, JsLocal, JsScope}; +use crate::JSGlobalObject; +use crate::bindgen::JsHeap; use crate::strong::Optional as Strong; use crate::virtual_machine::VirtualMachine; -use crate::{JSGlobalObject, JsResult}; use bun_boringssl::c as boring; use bun_collections::StringArrayHashMap; use bun_core::strings; @@ -1078,24 +1078,16 @@ extern "C" fn Bun__Process__getStdinFdType(vm: &VirtualMachine, fd: i32) -> Stdi // TLS default ciphers JS bindings // ────────────────────────────────────────────────────────────────────────── -pub(crate) fn set_tls_default_ciphers<'a>( - cx: &mut JsScope<'a>, - ciphers: JsLocal<'a>, -) -> JsResult<()> { - let global_this = cx.global(); - let ciphers = ciphers.to_js(); - if !ciphers.is_string() { - return Err(global_this.throw_invalid_argument_type_value(b"ciphers", b"string", ciphers)); - } - let utf8 = ciphers.to_utf8(global_this)?; +pub(crate) fn set_tls_default_ciphers(heap: &JsHeap<'_>, ciphers: &[u8]) { + // SAFETY: only the VM is taken from it. + let global_this = unsafe { heap.global() }; // `bun_vm()` is the safe BACKREF accessor for the per-thread VM; `as_mut()` // is the audited single-JS-thread `&mut` escape hatch. global_this .bun_vm() .as_mut() .rare_data() - .set_tls_default_ciphers(utf8.slice()); - Ok(()) + .set_tls_default_ciphers(ciphers); } pub(crate) fn get_tls_default_ciphers<'a>(heap: &JsHeap<'a>) -> &'a [u8] { diff --git a/src/jsc/resolver_jsc.bind.ts b/src/jsc/resolver_jsc.bind.ts index 4bf23c0109b5..88a2b0f59c0f 100644 --- a/src/jsc/resolver_jsc.bind.ts +++ b/src/jsc/resolver_jsc.bind.ts @@ -2,4 +2,4 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::resolver_jsc" }); -export const _nodeModulePaths = b.fn({ args: { path: b.any }, returns: b.any, capability: "js", throws: true }); +export const _nodeModulePaths = b.fn({ args: { path: b.string.allowStringObject }, returns: b.array(b.string) }); diff --git a/src/jsc/resolver_jsc.rs b/src/jsc/resolver_jsc.rs index 9dd49d0a5e7b..471066be766f 100644 --- a/src/jsc/resolver_jsc.rs +++ b/src/jsc/resolver_jsc.rs @@ -4,23 +4,14 @@ use crate::HostReturn as _; use bstr::BStr; -use crate::bindgen::{JsLocal, JsScope}; -use crate::{JSGlobalObject, JSValue, JsResult}; +use crate::{JSGlobalObject, JSValue}; use bun_core::{String as BunString, strings}; use bun_paths::resolve_path; use bun_paths::{Platform, SEP, SEP_STR}; -pub(crate) fn node_module_paths<'a>(cx: &mut JsScope<'a>, path: JsLocal<'a>) -> JsResult { +pub(crate) fn node_module_paths(path: &BunString) -> Vec { crate::mark_binding!(); - let global = cx.global(); - let argument: JSValue = path.to_js(); - - if argument.is_empty() || !argument.is_string() { - return Err(global.throw_invalid_argument_type("nodeModulePaths", "path", "string")); - } - - let in_str = argument.to_bun_string(global)?; - Ok(node_module_paths_js_value(&in_str, global, false)) + node_module_paths_list(path, false) } #[unsafe(no_mangle)] @@ -37,6 +28,11 @@ extern "C" fn node_module_paths_js_value( global: &JSGlobalObject, use_dirname: bool, ) -> JSValue { + crate::bun_string_jsc::to_js_array(global, &node_module_paths_list(in_str, use_dirname)) + .or_pending_exception() +} + +fn node_module_paths_list(in_str: &BunString, use_dirname: bool) -> Vec { let mut list: Vec = Vec::new(); let utf8 = in_str.to_utf8(); @@ -113,5 +109,5 @@ extern "C" fn node_module_paths_js_value( SEP_STR, ))); - crate::bun_string_jsc::to_js_array(global, &list).or_pending_exception() + list } diff --git a/src/jsc/virtual_machine_exports.bind.ts b/src/jsc/virtual_machine_exports.bind.ts index 17dc9187d36b..aa6d5cb8c6db 100644 --- a/src/jsc/virtual_machine_exports.bind.ts +++ b/src/jsc/virtual_machine_exports.bind.ts @@ -2,9 +2,4 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::virtual_machine_exports" }); -export const setSyntheticAllocationLimitForTesting = b.fn({ - args: { limit: b.any }, - returns: b.usize, - capability: "js", - throws: true, -}); +export const setSyntheticAllocationLimitForTesting = b.fn({ args: { limit: b.f64 }, returns: b.usize }); diff --git a/src/jsc/virtual_machine_exports.rs b/src/jsc/virtual_machine_exports.rs index cbbbed0b58b8..1b5b69a1b9a7 100644 --- a/src/jsc/virtual_machine_exports.rs +++ b/src/jsc/virtual_machine_exports.rs @@ -1,8 +1,7 @@ use core::ffi::c_void; -use crate::bindgen::{JsLocal, JsScope}; use crate::{ - JSGlobalObject, JSPromise, JSValue, JsResult, Strong, Task, VirtualMachineRef as VirtualMachine, + JSGlobalObject, JSPromise, JSValue, Strong, Task, VirtualMachineRef as VirtualMachine, }; use bun_core::String as BunString; use bun_sourcemap::SourceProviderMap; @@ -293,24 +292,11 @@ pub fn remove_source_provider_source_map( .remove_source_provider(opaque_source_provider, slice.slice()); } -pub(crate) fn set_synthetic_allocation_limit_for_testing<'a>( - cx: &mut JsScope<'a>, - limit: JsLocal<'a>, -) -> JsResult { - let global = cx.global(); - let arg = limit.to_js(); - - if !arg.is_number() { - return Err(global.throw_invalid_arguments(format_args!( - "setSyntheticAllocationLimitForTesting expects a number" - ))); - } - - let limit: usize = - usize::try_from(arg.coerce_to_int64(global)?.max(1024 * 1024)).expect("int cast"); +pub(crate) fn set_synthetic_allocation_limit_for_testing(limit: f64) -> usize { + let limit: usize = usize::try_from((limit as i64).max(1024 * 1024)).expect("int cast"); let prev = crate::virtual_machine::SYNTHETIC_ALLOCATION_LIMIT .swap(limit, core::sync::atomic::Ordering::Relaxed); crate::virtual_machine::STRING_ALLOCATION_LIMIT .store(limit, core::sync::atomic::Ordering::Relaxed); - Ok(prev) + prev } diff --git a/src/md/root.rs b/src/md/root.rs index 8cc9cec8ed18..ffc6a0a7fb16 100644 --- a/src/md/root.rs +++ b/src/md/root.rs @@ -25,18 +25,18 @@ pub struct Options { pub permissive_url_autolinks: bool, pub permissive_www_autolinks: bool, pub permissive_email_autolinks: bool, - pub(crate) hard_soft_breaks: bool, - pub(crate) wiki_links: bool, - pub(crate) underline: bool, - pub(crate) latex_math: bool, - pub(crate) collapse_whitespace: bool, - pub(crate) permissive_atx_headers: bool, - pub(crate) no_indented_code_blocks: bool, - pub(crate) no_html_blocks: bool, - pub(crate) no_html_spans: bool, + pub hard_soft_breaks: bool, + pub wiki_links: bool, + pub underline: bool, + pub latex_math: bool, + pub collapse_whitespace: bool, + pub permissive_atx_headers: bool, + pub no_indented_code_blocks: bool, + pub no_html_blocks: bool, + pub no_html_spans: bool, /// GFM tag filter: replaces `<` with `<` for disallowed HTML tags /// (title, textarea, style, xmp, iframe, noembed, noframes, script, plaintext). - pub(crate) tag_filter: bool, + pub tag_filter: bool, pub heading_ids: bool, pub autolink_headings: bool, /// GFM: `[^label]: ..` is the definition of a footnote. Only for consumers @@ -166,60 +166,6 @@ impl Options { autolink_headings: self.autolink_headings, } } - - /// `(snake_case, camelCase, setter)` for every bool field — used by - /// `Bun.markdown`'s option parser. - pub const BOOL_FIELD_SETTERS: &'static [( - &'static str, - &'static str, - fn(&mut Options, bool), - )] = &[ - ("tables", "tables", |o, v| o.tables = v), - ("strikethrough", "strikethrough", |o, v| o.strikethrough = v), - ("tasklists", "tasklists", |o, v| o.tasklists = v), - ("permissive_autolinks", "permissiveAutolinks", |o, v| { - o.permissive_autolinks = v - }), - ( - "permissive_url_autolinks", - "permissiveUrlAutolinks", - |o, v| o.permissive_url_autolinks = v, - ), - ( - "permissive_www_autolinks", - "permissiveWwwAutolinks", - |o, v| o.permissive_www_autolinks = v, - ), - ( - "permissive_email_autolinks", - "permissiveEmailAutolinks", - |o, v| o.permissive_email_autolinks = v, - ), - ("hard_soft_breaks", "hardSoftBreaks", |o, v| { - o.hard_soft_breaks = v - }), - ("wiki_links", "wikiLinks", |o, v| o.wiki_links = v), - ("underline", "underline", |o, v| o.underline = v), - ("latex_math", "latexMath", |o, v| o.latex_math = v), - ("collapse_whitespace", "collapseWhitespace", |o, v| { - o.collapse_whitespace = v - }), - ("permissive_atx_headers", "permissiveAtxHeaders", |o, v| { - o.permissive_atx_headers = v - }), - ("no_indented_code_blocks", "noIndentedCodeBlocks", |o, v| { - o.no_indented_code_blocks = v - }), - ("no_html_blocks", "noHtmlBlocks", |o, v| { - o.no_html_blocks = v - }), - ("no_html_spans", "noHtmlSpans", |o, v| o.no_html_spans = v), - ("tag_filter", "tagFilter", |o, v| o.tag_filter = v), - ("heading_ids", "headingIds", |o, v| o.heading_ids = v), - ("autolink_headings", "autolinkHeadings", |o, v| { - o.autolink_headings = v - }), - ]; } pub fn render_to_html(text: &[u8]) -> Result, parser::ParserError> { diff --git a/src/patch_jsc/patch_testing.bind.ts b/src/patch_jsc/patch_testing.bind.ts index 9e0ebfde57b8..c348eb27c8b8 100644 --- a/src/patch_jsc/patch_testing.bind.ts +++ b/src/patch_jsc/patch_testing.bind.ts @@ -9,12 +9,12 @@ export const parse = b.fn({ throws: true, }); -/** `dir` is converted before `patchfile`. */ export const apply = b.fn({ - args: { patchfile: b.any, dir: b.any }, + args: { patchfile: b.string.loose.utf8, dir: b.string.loose.utf8.optional }, returns: b.bool, capability: "js", throws: true, + length: 2, }); export const makeDiff = b.fn({ diff --git a/src/patch_jsc/testing.rs b/src/patch_jsc/testing.rs index 13c814220abd..fbbc2c2b1b10 100644 --- a/src/patch_jsc/testing.rs +++ b/src/patch_jsc/testing.rs @@ -1,7 +1,7 @@ //! JS testing bindings for `bun.patch`. Keeps `src/patch/` free of JSC types. -use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; -use bun_jsc::{JSGlobalObject, JSValue, JsResult, SysErrorJsc}; +use bun_jsc::bindgen::{JsHeap, JsScope}; +use bun_jsc::{JsResult, SysErrorJsc}; use bun_patch::{ApplyError, ParseErr, ParseErrAt, PatchFile, git_diff_internal, parse_patch_file}; use bun_sys::{Fd, FdExt}; @@ -27,18 +27,35 @@ pub(crate) fn make_diff( }) } -pub(crate) fn apply<'a>( - cx: &mut JsScope<'a>, - patchfile: JsLocal<'a>, - dir: JsLocal<'a>, -) -> JsResult { +pub(crate) fn apply(cx: &mut JsScope<'_>, patchfile: &[u8], dir: Option<&[u8]>) -> JsResult { let global = cx.global(); - let args = parse_apply_args(global, patchfile.to_js(), dir.to_js())?; + let dir = match dir { + Some(path) => { + let path = bun_jsc::String::borrow_utf8(path).to_owned_slice_z(); + + match bun_sys::open( + path.as_zstr(), + bun_sys::O::DIRECTORY | bun_sys::O::RDONLY, + 0, + ) { + Err(e) => { + let js_err = SysErrorJsc::to_js(&e.with_path(path.as_bytes()), global); + return Err(global.throw_value(js_err)); + } + Ok(fd) => ApplyDir(fd), + } + } + None => ApplyDir(Fd::cwd()), + }; - let patchfile: PatchFile<'_> = - parse_patch_file(&args.patchfile_txt).expect("validated in parse_apply_args"); + let patchfile: PatchFile<'_> = match parse_patch_file(patchfile) { + Ok(patchfile) => patchfile, + Err(ParseErrAt { err: e, .. }) => { + return Err(global.throw_error(bun_patch::Error::from(e), "failed to parse patchfile")); + } + }; - match patchfile.apply(args.dirfd) { + match patchfile.apply(dir.0) { Ok(()) => {} Err(ApplyError::Sys(err)) => { return Err(global.throw_value(SysErrorJsc::to_js(&err, global))); @@ -76,73 +93,13 @@ pub(crate) fn parse(heap: &JsHeap<'_>, patchfile_src: &[u8]) -> JsResult Ok(str) } -pub(crate) fn parse_apply_args( - global: &JSGlobalObject, - patchfile_js: JSValue, - dir_js: JSValue, -) -> JsResult { - let dir_fd = if !dir_js.is_undefined() { - let bunstr = dir_js.to_bun_string(global)?; - let path = bunstr.to_owned_slice_z(); - - match bun_sys::open( - path.as_zstr(), - bun_sys::O::DIRECTORY | bun_sys::O::RDONLY, - 0, - ) { - Err(e) => { - let js_err = SysErrorJsc::to_js(&e.with_path(path.as_bytes()), global); - return Err(global.throw_value(js_err)); - } - Ok(fd) => fd, - } - } else { - Fd::cwd() - }; - - let patchfile_bunstr = match patchfile_js.to_bun_string(global) { - Ok(bunstr) => bunstr, - Err(e) => { - if Fd::cwd() != dir_fd { - dir_fd.close(); - } - return Err(e); - } - }; - let patchfile_src = patchfile_bunstr.to_utf8(); - - // Validate the patch parses; on failure, clean up `dir_fd` and throw. - // The parsed `PatchFile<'_>` borrows `patchfile_src`, so it cannot be - // returned alongside its source without a self-referential struct - // (forbidden by PORTING.md). We discard it here; `apply()` reparses - // from the owned bytes below. - if let Err(ParseErrAt { err: e, .. }) = parse_patch_file(patchfile_src.slice()) { - // TODO: HAVE @zackradisic REVIEW THIS DIFF - if Fd::cwd() != dir_fd { - dir_fd.close(); - } - - drop(patchfile_src); - return Err(global.throw_error(bun_patch::Error::from(e), "failed to parse patchfile")); - } - - Ok(ApplyArgs { - dirfd: dir_fd, - patchfile_txt: patchfile_src.into_vec(), - }) -} - -pub(crate) struct ApplyArgs { - patchfile_txt: Vec, - dirfd: Fd, -} +struct ApplyDir(Fd); -impl Drop for ApplyArgs { +impl Drop for ApplyDir { fn drop(&mut self) { - // patchfile_txt freed by its own Drop impl. // TODO: HAVE @zackradisic REVIEW THIS DIFF - if Fd::cwd() != self.dirfd { - self.dirfd.close(); + if Fd::cwd() != self.0 { + self.0.close(); } } } diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index 0acda3a6aedb..8f7ec0bb7fd3 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -93,10 +93,27 @@ export const indexOfLine = b.fn({ export const inflateSync = zlib("inflate_sync", ZlibDecompressionOptions); -/** `Bun.inspect()` is `""` and `Bun.inspect(undefined)` is `"undefined"`. */ +/** What is not a number is ignored. */ +export const InspectDepth = b.union({ number: b.f64, ignored: b.any }); +export const InspectOptions = b.dictionary( + { + depth: InspectDepth, + colors: b.bool.loose.optional, + sorted: b.bool.loose.optional, + compact: b.bool.loose.optional, + }, + { allowArray: true }, +); +export const InspectOptionsOrDepth = b.union({ options: InspectOptions, depth: b.f64, ignored: b.any }); +export const InspectOptionsOrIgnored = b.union({ options: InspectOptions, ignored: b.any }); + +/** + * `(value, options)` or `(value, depth, colors)`. `Bun.inspect()` is `""` and `Bun.inspect(undefined)` is + * `"undefined"`. + */ export const inspect = b.fn({ export: "Bun__BunObject__inspect", - args: { args: b.any.variadic }, + args: { value: b.any.ifPassed, options: InspectOptionsOrDepth, colors: b.bool.loose.nullable }, returns: b.string.utf8, capability: "js", throws: true, @@ -142,15 +159,15 @@ export const openInEditor = b.fn({ length: 1, }); +/** Without a `specifier` or a `from`, `resolveSync` throws and `resolve` rejects. */ const resolver = { - args: { specifier: b.any, from: b.any, esm: b.any }, - returns: b.any, + args: { specifier: b.string.loose.nullable, from: b.string.loose.nullable, esm: b.bool.default(true) }, capability: "js", throws: true, length: 1, } as const; -export const resolve = b.fn(resolver); -export const resolveSync = b.fn(resolver); +export const resolve = b.fn({ ...resolver, returns: b.any, rejects: true }); +export const resolveSync = b.fn({ ...resolver, returns: b.string }); export const serve = b.fn({ args: { options: b.any }, returns: b.any, capability: "js", throws: true }); @@ -169,14 +186,23 @@ export const sleepSync = b.fn({ throws: true, }); +/** The table of what is not an object is `""`. */ +export const TabularData = b.union({ object: b.object, ignored: b.any }); +const printer = { export: "Bun__BunObject__table", returns: b.string.utf8, capability: "js", throws: true } as const; /** `Bun.inspect.table` */ -export const table = b.fn({ - export: "Bun__BunObject__table", - args: { tabularData: b.any, properties: b.any, options: b.any }, - returns: b.string.utf8, - capability: "js", - throws: true, -}); +export const table = b.overloads( + { + ...printer, + args: { tabularData: TabularData, properties: b.jsArray, options: InspectOptionsOrIgnored }, + length: 3, + rustPath: "crate::api::bun_object::table_with_properties", + }, + { + ...printer, + args: { tabularData: TabularData, options: InspectOptionsOrIgnored }, + rustPath: "crate::api::bun_object::table", + }, +); export const WhichOptions = b.dictionary( { diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index b344e9f440b7..6328b79d2544 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -67,8 +67,8 @@ use std::io::Write as _; use bun_core::Output; use bun_jsc::{ - self as jsc, ArrayBuffer, ConsoleObject, ErrorCode, JSFunction, JSGlobalObject, JSObject, - JSPromise, JSString, JSValue, JsResult, + self as jsc, ArrayBuffer, ConsoleObject, ErrorCode, JSArray, JSFunction, JSGlobalObject, + JSObject, JSPromise, JSString, JSValue, JsResult, }; // `bun_jsc::VirtualMachine` is the *module* re-export; the struct lives one level deeper. use crate::cli::open::Editor; @@ -86,7 +86,8 @@ use bun_jsc::EncodedSliceJsc as _; use bun_jsc::{StringJsc as _, bun_string_jsc}; use crate::generated_bindgen::bun_object::{ - BracesOptions, Bun__BunObject__inspect, Bun__BunObject__table, EditorOptions, MMapOptions, + BracesOptions, Bun__BunObject__inspect, Bun__BunObject__table, EditorOptions, InspectDepth, + InspectOptions, InspectOptionsOrDepth, InspectOptionsOrIgnored, MMapOptions, TabularData, WhichOptions, ZlibCompressionOptions, ZlibDecompressionOptions, ZstdCompressionOptions, }; use bun_jsc::bindgen::{self, BufferSource, JsHeap, JsLocal, JsScope, PinnedBytes}; @@ -335,32 +336,56 @@ pub(crate) fn which<'a>( .transpose() } -/// `Bun.inspect.table` -pub(crate) fn table( - cx: &mut JsScope<'_>, - tabular_data: JsLocal<'_>, - properties: JsLocal<'_>, - options: JsLocal<'_>, -) -> JsResult> { - let global_this = cx.global(); - let mut all_arguments = [tabular_data.to_js(), properties.to_js(), options.to_js()]; - if all_arguments[0].is_undefined_or_null() || !all_arguments[0].is_object() { - return Ok(Vec::new()); +fn apply_inspect_options( + format_options: &mut ConsoleObject::FormatOptions, + global_this: &JSGlobalObject, + options: &InspectOptions<'_>, +) -> JsResult<()> { + if let InspectDepth::Number(depth) = options.depth { + format_options.set_depth(global_this, depth)?; } + if let Some(colors) = options.colors { + format_options.enable_colors = colors; + } + if let Some(sorted) = options.sorted { + format_options.ordered_properties = sorted; + } + if let Some(compact) = options.compact { + format_options.single_line = compact; + } + Ok(()) +} - // NOTE: protect/unprotect over a copied [JSValue; 3]; the borrow of - // `all_arguments` cannot escape into a guard closure, so copy out into an - // array of RAII guards. - let _prot: [bun_jsc::ProtectedJSValue; 3] = - core::array::from_fn(|i| all_arguments[i].protected()); +/// `Bun.inspect.table(tabularData, options)` +pub(crate) fn table<'a>( + cx: &mut JsScope<'a>, + tabular_data: TabularData<'a>, + options: InspectOptionsOrIgnored<'a>, +) -> JsResult> { + print_table(cx, tabular_data, JSValue::UNDEFINED, options) +} - let arguments = &mut all_arguments[..]; - let value = arguments[0]; +/// `Bun.inspect.table(tabularData, properties, options)` +pub(crate) fn table_with_properties<'a>( + cx: &mut JsScope<'a>, + tabular_data: TabularData<'a>, + properties: JsLocal<'a, JSArray>, + options: InspectOptionsOrIgnored<'a>, +) -> JsResult> { + print_table(cx, tabular_data, properties.to_js(), options) +} - if !arguments[1].is_array() { - arguments[2] = arguments[1]; - arguments[1] = JSValue::UNDEFINED; - } +fn print_table( + cx: &mut JsScope<'_>, + tabular_data: TabularData<'_>, + properties: JSValue, + options: InspectOptionsOrIgnored<'_>, +) -> JsResult> { + let global_this = cx.global(); + let TabularData::Object(value) = tabular_data else { + return Ok(Vec::new()); + }; + let value = value.to_js(); let mut format_options = ConsoleObject::FormatOptions { enable_colors: false, @@ -372,18 +397,13 @@ pub(crate) fn table( single_line: true, ..Default::default() }; - if arguments[2].is_object() { - format_options.from_js(global_this, &arguments[2..])?; + if let InspectOptionsOrIgnored::Options(options) = options { + apply_inspect_options(&mut format_options, global_this, &options)?; } // very stable memory address let mut array: Vec = Vec::new(); - let properties: JSValue = if arguments[1].js_type().is_array() { - arguments[1] - } else { - JSValue::UNDEFINED - }; let mut table_printer = ConsoleObject::TablePrinter::init( global_this, ConsoleObject::MessageLevel::Log, @@ -409,32 +429,17 @@ pub(crate) fn table( Ok(array) } -pub(crate) fn inspect(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult> { +pub(crate) fn inspect<'a>( + cx: &mut JsScope<'a>, + value: Option>, + options: InspectOptionsOrDepth<'a>, + colors: Option, +) -> JsResult> { let global_this = cx.global(); - let arguments = JsLocal::to_js_slice(args); - if arguments.is_empty() { + let Some(value) = value else { return Ok(Vec::new()); - } - - for arg in arguments { - arg.protect(); - } - // Each arg is unprotected on scope exit. - // `arguments()` borrows the call-frame slot array; wrap the borrowed slice - // in the guard instead of heap-allocating a `Vec` per call. - // - // NOTE: this is *not* the fix for error-gc-test.test.js timing out under - // debug+ASAN — that test does 100k `Bun.inspect(new Error)` and the cost - // is spread across ASAN-instrumented memcpy/memset, mimalloc zero-checks - // and the source-file re-read in `remap_zig_exception`, none of which a - // 32-byte alloc elision can recover. The test is classified `[TIMEOUT]` - // for ASAN in test/expectations.txt instead. - let args_buf = scopeguard::guard(arguments, |buf| { - for arg in buf { - arg.unprotect(); - } - }); - let arguments = *args_buf; + }; + let value = value.to_js(); let mut format_options = ConsoleObject::FormatOptions { enable_colors: false, @@ -445,8 +450,15 @@ pub(crate) fn inspect(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult 1 { - format_options.from_js(global_this, &arguments[1..])?; + match options { + InspectOptionsOrDepth::Options(options) => { + apply_inspect_options(&mut format_options, global_this, &options)?; + } + InspectOptionsOrDepth::Depth(depth) => { + format_options.set_depth(global_this, depth)?; + format_options.enable_colors = colors.unwrap_or(false); + } + InspectOptionsOrDepth::Ignored(_) => format_options.enable_colors = colors.unwrap_or(false), } // very stable memory address @@ -456,7 +468,7 @@ pub(crate) fn inspect(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult) { cx.global().vm().shrink_footprint(); } -fn do_resolve( - global_this: &JSGlobalObject, - specifier: JSValue, - from: JSValue, - esm: JSValue, -) -> JsResult { - if specifier.is_undefined_or_null() { - return Err(global_this.throw_invalid_arguments(format_args!("specifier must be a string"))); - } - - if from.is_undefined_or_null() { - return Err(global_this.throw_invalid_arguments(format_args!("from must be a string"))); - } - - let mut mode = ResolveMode::Esm; - if !esm.is_undefined() { - if esm.is_boolean() { - mode = if esm.to_boolean() { - ResolveMode::Esm - } else { - ResolveMode::Require - }; - } else { - return Err(global_this.throw_invalid_arguments(format_args!("esm must be a boolean"))); - } - } - - let specifier_str = specifier.to_bun_string(global_this)?; - let from_str = from.to_bun_string(global_this)?; - do_resolve_with_args::(global_this, &specifier_str, &from_str, mode) -} - enum Resolved { - Found(JSValue), + Found(BunString), /// The resolver's `ResolveMessage` for a specifier it could not resolve; not thrown yet. NotFound(JSValue), } @@ -841,7 +821,7 @@ fn do_resolve_with_args( specifier: &BunString, from: &BunString, mode: ResolveMode, -) -> JsResult { +) -> JsResult { match resolve_with_args::(ctx, specifier, from, mode)? { Resolved::Found(value) => Ok(value), Resolved::NotFound(err) => Err(ctx.throw_value(err)), @@ -884,39 +864,45 @@ fn resolve_with_args( // Vec writes are infallible. let _ = write!(&mut arraylist, "{}{}", result_value, query_string); - return Ok(Resolved::Found(bun_string_jsc::create_utf8_for_js( - ctx, &arraylist, - )?)); + return Ok(Resolved::Found(BunString::clone_utf8(&arraylist))); } - Ok(Resolved::Found(result_value.into_js(ctx)?)) + Ok(Resolved::Found(result_value)) } pub(crate) fn resolve_sync( cx: &mut JsScope<'_>, - specifier: JsLocal<'_>, - from: JsLocal<'_>, - esm: JsLocal<'_>, -) -> JsResult { - do_resolve(cx.global(), specifier.to_js(), from.to_js(), esm.to_js()) + specifier: Option, + from: Option, + esm: bool, +) -> JsResult { + let global_this = cx.global(); + let Some(specifier) = specifier else { + return Err(global_this.throw_invalid_arguments(format_args!("specifier must be a string"))); + }; + let Some(from) = from else { + return Err(global_this.throw_invalid_arguments(format_args!("from must be a string"))); + }; + let mode = if esm { + ResolveMode::Esm + } else { + ResolveMode::Require + }; + do_resolve_with_args::(global_this, &specifier, &from, mode) } pub(crate) fn resolve( cx: &mut JsScope<'_>, - specifier: JsLocal<'_>, - from: JsLocal<'_>, - esm: JsLocal<'_>, + specifier: Option, + from: Option, + esm: bool, ) -> JsResult { + let resolved = resolve_sync(cx, specifier, from, esm)?; let global_object = cx.global(); - let value = match do_resolve(global_object, specifier.to_js(), from.to_js(), esm.to_js()) { - Ok(v) => v, - Err(e) => { - return Ok( - JSPromise::rejected_promise_with_caught_exception(global_object, e)?.to_js(), - ); - } - }; - Ok(JSPromise::resolved_promise_value(global_object, value)) + Ok(JSPromise::resolved_promise_value( + global_object, + resolved.into_js(global_object)?, + )) } // HOST_EXPORT(Bun__resolveSync, c) @@ -951,7 +937,8 @@ pub(crate) fn bun_resolve_sync( &specifier_str, &source_str, ResolveMode::from_ffi_bools(is_esm, is_user_require_resolve), - ) + )? + .into_js(global) }) } @@ -1014,7 +1001,8 @@ pub(crate) fn bun_resolve_sync_with_paths( &specifier_str, &source_str, ResolveMode::from_ffi_bools(is_esm, is_user_require_resolve), - ) + )? + .into_js(global) }) } @@ -1039,7 +1027,8 @@ pub(crate) fn bun_resolve_sync_with_strings( specifier, source, ResolveMode::from_ffi_bools(is_esm, false), - ) + )? + .into_js(global) }) } @@ -1058,16 +1047,15 @@ pub(crate) fn bun_resolve_sync_with_source_if_exists( return JSValue::ZERO; }; jsc::to_js_host_call(global, || { - resolve_with_args::( + match resolve_with_args::( global, &specifier_str, source, ResolveMode::from_ffi_bools(is_esm, false), - ) - .map(|r| match r { - Resolved::Found(value) => value, - Resolved::NotFound(_) => JSValue::UNDEFINED, - }) + )? { + Resolved::Found(value) => value.into_js(global), + Resolved::NotFound(_) => Ok(JSValue::UNDEFINED), + } }) } diff --git a/src/runtime/api/JSSourceMap.rs b/src/runtime/api/JSSourceMap.rs index 12dbb1a4e821..6384762de47e 100644 --- a/src/runtime/api/JSSourceMap.rs +++ b/src/runtime/api/JSSourceMap.rs @@ -6,8 +6,8 @@ use std::sync::Arc; use bstr::BStr; use bun_core::{self as bstring, strings}; -use bun_jsc::bindgen::{JsLocal, JsScope, JsStructure, JsWrapper}; -use bun_jsc::{JSGlobalObject, JSValue, JsClass as _, JsResult, StringJsc as _, bun_string_jsc}; +use bun_jsc::bindgen::{JsLocal, JsPayload as _, JsScope, JsStructure, JsWrapper}; +use bun_jsc::{JSGlobalObject, JSValue, JsResult, StringJsc as _, bun_string_jsc}; use bun_sourcemap::{Mapping, Ordinal, ParsedSourceMap, mapping}; use crate::generated_bindgen::sourcemap::js_SourceMap as js; @@ -22,20 +22,23 @@ pub(crate) struct JSSourceMap { // Mutable global; AtomicBool for safe mutation. static ENABLE_SOURCE_MAPS: AtomicBool = AtomicBool::new(false); -pub(crate) fn find_source_map(cx: &mut JsScope<'_>, path: JsLocal<'_>) -> JsResult { +pub(crate) fn find_source_map<'a>( + cx: &mut JsScope<'a>, + path: JsLocal<'a>, +) -> JsResult>> { // Node.js doesn't enable source maps by default. // In Bun, we do use them for almost all files since we transpile almost all files // If we enable this by default, we don't have a `payload` object since we don't internally create one. // This causes Next.js to emit errors like the below on start: // .next/server/chunks/ssr/[root-of-the-server]__012ba519._.js: Invalid source map. Only conformant source maps can be used to filter stack frames. Cause: TypeError: payload is not an Object. (evaluating '"sections" in payload') if !ENABLE_SOURCE_MAPS.load(Ordering::Relaxed) { - return Ok(JSValue::UNDEFINED); + return Ok(None); } let global = cx.global(); let source_url_value = path.to_js(); if !source_url_value.is_string() { - return Ok(JSValue::UNDEFINED); + return Ok(None); } let mut source_url_string = bun_core::String::from_js(source_url_value, global)?; @@ -47,7 +50,7 @@ pub(crate) fn find_source_map(cx: &mut JsScope<'_>, path: JsLocal<'_>) -> JsResu || source_url.starts_with(b"bun:") || source_url.starts_with(b"data:") { - return Ok(JSValue::UNDEFINED); + return Ok(None); } } @@ -74,7 +77,7 @@ pub(crate) fn find_source_map(cx: &mut JsScope<'_>, path: JsLocal<'_>) -> JsResu // SAFETY: `bun_vm()` returns the live per-thread VM for a Bun-owned global. let vm = global.bun_vm().as_mut(); let Some(source_map) = vm.source_mappings().get(source_url) else { - return Ok(JSValue::UNDEFINED); + return Ok(None); }; drop(source_url_slice); let fake_sources_array: Box<[bstring::String]> = Box::new([source_url_string]); @@ -86,7 +89,7 @@ pub(crate) fn find_source_map(cx: &mut JsScope<'_>, path: JsLocal<'_>) -> JsResu sources: fake_sources_array, names: Box::default(), }; - Ok(this.to_js(global)) + Ok(Some(this.into_js(&cx.heap))) } impl JSSourceMap { diff --git a/src/runtime/api/MarkdownObject.bind.ts b/src/runtime/api/MarkdownObject.bind.ts index 7122ab32255d..007b56db3a7c 100644 --- a/src/runtime/api/MarkdownObject.bind.ts +++ b/src/runtime/api/MarkdownObject.bind.ts @@ -2,11 +2,130 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::api::markdown_object" }); +/** The parser reads a byte more than once. */ +const input = b.bytes({ from: ["string", "buffer"], shared: "copied", held: "handle" }); +const expected = { input: "Expected a string or buffer to render" } as const; + +/** What is not an object is ignored. */ +const bag = { allowArray: true, nonObjectAsEmpty: true } as const; +const flag = b.bool.loose.optional; +/** What is not a number is ignored. */ +export const NumberOrIgnored = b.union({ number: b.f64, ignored: b.any }); + +export const AutolinkOptions = b.dictionary({ url: flag, www: flag, email: flag }, bag); +export const Autolinks = b.union({ all: b.bool, each: AutolinkOptions }); +export const HeadingOptions = b.dictionary({ ids: flag, autolink: flag }, bag); +export const Headings = b.union({ all: b.bool, each: HeadingOptions }); + +const options = { + autolinks: Autolinks.optional, + headings: Headings.optional, + tables: flag, + strikethrough: flag, + tasklists: flag, + hardSoftBreaks: { type: flag, altNames: ["hard_soft_breaks"] }, + wikiLinks: { type: flag, altNames: ["wiki_links"] }, + underline: flag, + latexMath: { type: flag, altNames: ["latex_math"] }, + collapseWhitespace: { type: flag, altNames: ["collapse_whitespace"] }, + permissiveAtxHeaders: { type: flag, altNames: ["permissive_atx_headers"] }, + noIndentedCodeBlocks: { type: flag, altNames: ["no_indented_code_blocks"] }, + noHtmlBlocks: { type: flag, altNames: ["no_html_blocks"] }, + noHtmlSpans: { type: flag, altNames: ["no_html_spans"] }, + tagFilter: { type: flag, altNames: ["tag_filter"] }, +} as const; +export const MarkdownOptions = b.dictionary(options, bag); +export const ReactOptions = b.dictionary({ reactVersion: NumberOrIgnored, ...options }, bag); + +export const AnsiTheme = b.dictionary( + { colors: flag, hyperlinks: flag, kittyGraphics: flag, light: flag, columns: NumberOrIgnored }, + bag, +); + +const members = (type: T, names: readonly Name[]) => + Object.fromEntries(names.map(name => [name, type])) as Record; + +export const CallbackOrIgnored = b.union({ callback: b.callable, ignored: b.any }); +export const RenderCallbacks = b.dictionary( + members(CallbackOrIgnored, [ + "heading", + "paragraph", + "blockquote", + "code", + "list", + "listItem", + "hr", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "html", + "strong", + "emphasis", + "link", + "image", + "codespan", + "strikethrough", + "text", + ]), + bag, +); + +/** The `type` of the elements of each tag. A boolean is ignored. */ +export const ComponentOverrides = b.dictionary( + members(b.any.emptyStringAsNull, [ + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "p", + "blockquote", + "ul", + "ol", + "li", + "pre", + "hr", + "html", + "table", + "thead", + "tbody", + "tr", + "th", + "td", + "em", + "strong", + "a", + "img", + "code", + "del", + "math", + "u", + "br", + ]), + bag, +); + const js = { capability: "js", throws: true } as const; export const markdown = b.namespace({ - html: { args: { input: b.any, options: b.any }, returns: b.string.utf8, length: 1, ...js }, - ansi: { args: { input: b.any, theme: b.any }, returns: b.string.utf8, ...js }, - render: { args: { input: b.any, callbacks: b.any, options: b.any }, returns: b.any, ...js }, - react: { args: { input: b.any, components: b.any, options: b.any }, returns: b.any, ...js }, + html: { args: { input, options: MarkdownOptions.default({}) }, returns: b.string.utf8, expected, ...js }, + ansi: { args: { input, theme: AnsiTheme.default({}) }, returns: b.string.utf8, expected, length: 2, ...js }, + /** The callbacks run while `input` is read. */ + render: { + args: { input: b.any, callbacks: RenderCallbacks.default({}), options: MarkdownOptions.default({}) }, + returns: b.string.utf8, + length: 3, + ...js, + }, + /** As `render`. */ + react: { + args: { input: b.any, components: ComponentOverrides.default({}), options: ReactOptions.default({}) }, + returns: b.any, + length: 3, + ...js, + }, }); diff --git a/src/runtime/api/MarkdownObject.rs b/src/runtime/api/MarkdownObject.rs index c7385b82a222..f643ece821e7 100644 --- a/src/runtime/api/MarkdownObject.rs +++ b/src/runtime/api/MarkdownObject.rs @@ -1,7 +1,11 @@ //! `Bun.markdown` — html/ansi/react/render host fns over `bun_md`. +use crate::generated_bindgen::markdown_object::{ + AnsiTheme, Autolinks, CallbackOrIgnored, ComponentOverrides, Headings, MarkdownOptions, + NumberOrIgnored, ReactOptions, RenderCallbacks, +}; use bun_core::StackCheck; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::{JsBytes, JsLocal, JsScope}; use bun_jsc::bun_string_jsc; use bun_jsc::{ JSGlobalObject, JSValue, JsResult, MarkedArgumentBuffer, PinnedArrayBuffer, RangeErrorOptions, @@ -65,6 +69,68 @@ fn parser_err_to_js( } } +/// `md::Options` of a `MarkdownOptions` or a `ReactOptions`, which have these members in common. +macro_rules! parser_options { + ($given:expr) => { + parser_options!( + $given; + tables, + strikethrough, + tasklists, + hard_soft_breaks, + wiki_links, + underline, + latex_math, + collapse_whitespace, + permissive_atx_headers, + no_indented_code_blocks, + no_html_blocks, + no_html_spans, + tag_filter + ) + }; + ($given:expr; $($flag:ident),*) => {{ + let given = $given; + let mut options = md::Options::default(); + match &given.autolinks { + Some(Autolinks::All(all)) => options.permissive_autolinks |= *all, + Some(Autolinks::Each(each)) => { + if let Some(url) = each.url { + options.permissive_url_autolinks = url; + } + if let Some(www) = each.www { + options.permissive_www_autolinks = www; + } + if let Some(email) = each.email { + options.permissive_email_autolinks = email; + } + } + None => {} + } + match &given.headings { + Some(Headings::All(all)) => { + options.heading_ids |= *all; + options.autolink_headings |= *all; + } + Some(Headings::Each(each)) => { + if let Some(ids) = each.ids { + options.heading_ids = ids; + } + if let Some(autolink) = each.autolink { + options.autolink_headings = autolink; + } + } + None => {} + } + $( + if let Some(value) = given.$flag { + options.$flag = value; + } + )* + options + }}; +} + /// Pins a buffer input for the render, which can re-enter JS; `None` for a string. fn pin(global: &JSGlobalObject, input: &StringOrBuffer) -> JsResult> { let StringOrBuffer::Buffer(buffer) = input else { @@ -84,175 +150,49 @@ pub(crate) fn set_max_markdown_block_bytes_for_testing(limit: f64) -> usize { } /// `Bun.markdown.ansi(text, theme?)` — render markdown to an ANSI-colored -/// terminal string. `theme` is an optional object: `{ colors?, hyperlinks?, -/// light?, columns? }`. By default colors are enabled, hyperlinks are +/// terminal string. By default colors are enabled, hyperlinks are /// disabled (the caller doesn't know if stdout is a TTY), and columns is 80. -pub(crate) fn ansi( - cx: &mut JsScope<'_>, - input: JsLocal<'_>, - theme: JsLocal<'_>, +pub(crate) fn ansi<'a>( + cx: &mut JsScope<'a>, + input: JsBytes<'a>, + theme: &AnsiTheme<'a>, ) -> JsResult> { - let global_this = cx.global(); - let [input_value, theme_value] = [input.to_js(), theme.to_js()]; - - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js_stable(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; - - let mut theme = md::AnsiTheme { - colors: true, - hyperlinks: false, - kitty_graphics: false, - light: md::detect_light_background(), - columns: 80, + let theme = md::AnsiTheme { + colors: theme.colors.unwrap_or(true), + hyperlinks: theme.hyperlinks.unwrap_or(false), + kitty_graphics: theme.kitty_graphics.unwrap_or(false), + light: theme.light.unwrap_or_else(md::detect_light_background), + columns: match theme.columns { + NumberOrIgnored::Number(columns) => columns as u16, + NumberOrIgnored::Ignored(_) => 80, + }, remote_image_paths: None, image_base_dir: None, }; - if theme_value.is_object() { - if let Some(v) = theme_value.get_boolean_loose(global_this, "colors")? { - theme.colors = v; - } - if let Some(v) = theme_value.get_boolean_loose(global_this, "hyperlinks")? { - theme.hyperlinks = v; - } - if let Some(v) = theme_value.get_boolean_loose(global_this, "kittyGraphics")? { - theme.kitty_graphics = v; - } - if let Some(v) = theme_value.get_boolean_loose(global_this, "light")? { - theme.light = v; - } - if let Some(cols) = theme_value.get(global_this, "columns")? { - if cols.is_number() { - let n = cols.to_int32(); - theme.columns = if n <= 0 { - 0 - } else { - u16::try_from(n.min(u16::MAX as i32)).expect("int cast") - }; - } - } - } + let input = input.get(&cx.heap); + let input_len = input.len(); match md::render_to_ansi(input, md::Options::TERMINAL, theme) { Ok(Some(r)) => Ok(r), Ok(None) => { // The parser can only return null via JSError // from a renderer callback; the ANSI renderer has none, so this // path is unreachable but handle it safely. - Err(global_this.throw_out_of_memory()) + Err(cx.global().throw_out_of_memory()) } - Err(err) => Err(parser_err_to_js(global_this, err, input.len())), + Err(err) => Err(parser_err_to_js(cx.global(), err, input_len)), } } -pub(crate) fn html( - cx: &mut JsScope<'_>, - input: JsLocal<'_>, - options: JsLocal<'_>, +pub(crate) fn html<'a>( + cx: &mut JsScope<'a>, + input: JsBytes<'a>, + options: &MarkdownOptions, ) -> JsResult> { - let global_this = cx.global(); - let [input_value, opts_value] = [input.to_js(), options.to_js()]; - - if input_value.is_empty_or_undefined_or_null() { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - } - - let Some(buffer) = StringOrBuffer::from_js_stable(global_this, input_value)? else { - return Err(global_this - .throw_invalid_arguments(format_args!("Expected a string or buffer to render"))); - }; - - let pinned = pin(global_this, &buffer)?; - let input: &[u8] = match &pinned { - Some(p) => p.slice(), - None => buffer.slice(), - }; - - let options = parse_options(global_this, opts_value)?; - - md::render_to_html_with_options(input, options) - .map_err(|err| parser_err_to_js(global_this, err, input.len())) -} - -fn parse_options(global_this: &JSGlobalObject, opts_value: JSValue) -> JsResult { - let mut options = md::Options::default(); - if opts_value.is_object() { - // Handle compound autolinks: true | { url, www, email } - if let Some(autolinks_val) = opts_value.get(global_this, "autolinks")? { - if autolinks_val.is_boolean() { - if autolinks_val.to_boolean() { - options.permissive_autolinks = true; - } - } else if autolinks_val.is_object() { - if let Some(v) = autolinks_val.get_boolean_loose(global_this, "url")? { - options.permissive_url_autolinks = v; - } - if let Some(v) = autolinks_val.get_boolean_loose(global_this, "www")? { - options.permissive_www_autolinks = v; - } - if let Some(v) = autolinks_val.get_boolean_loose(global_this, "email")? { - options.permissive_email_autolinks = v; - } - } - } - - // Handle compound headings: true | { ids, autolink } - if let Some(headings_val) = opts_value.get(global_this, "headings")? { - if headings_val.is_boolean() { - if headings_val.to_boolean() { - options.heading_ids = true; - options.autolink_headings = true; - } - } else if headings_val.is_object() { - if let Some(v) = headings_val.get_boolean_loose(global_this, "ids")? { - options.heading_ids = v; - } - if let Some(v) = headings_val.get_boolean_loose(global_this, "autolink")? { - options.autolink_headings = v; - } - } - } - - // Handle remaining boolean options (autolinks/headings are only settable via - // compound options above). `md::Options::BOOL_FIELD_SETTERS` is a hand-maintained - // table in bun_md that carries each bool field's snake_case and camelCase names - // plus a setter. - for (snake, camel, set) in md::Options::BOOL_FIELD_SETTERS { - // skip the compound-only fields - if matches!( - *snake, - "permissive_autolinks" - | "permissive_url_autolinks" - | "permissive_www_autolinks" - | "permissive_email_autolinks" - | "heading_ids" - | "autolink_headings" - ) { - continue; - } - if let Some(val) = opts_value.get_boolean_loose(global_this, camel)? { - set(&mut options, val); - } else if *camel != *snake { - if let Some(val) = opts_value.get_boolean_loose(global_this, snake)? { - set(&mut options, val); - } - } - } - } - Ok(options) + let input = input.get(&cx.heap); + let input_len = input.len(); + md::render_to_html_with_options(input, parser_options!(options)) + .map_err(|err| parser_err_to_js(cx.global(), err, input_len)) } /// `Bun.markdown.render(text, callbacks, options?)` — render markdown with custom callbacks. @@ -260,15 +200,14 @@ fn parse_options(global_this: &JSGlobalObject, opts_value: JSValue) -> JsResult< /// Each callback receives the accumulated children as a string plus an optional /// metadata object, and returns a string. The final result is the concatenation /// of all callback outputs. -pub(crate) fn render( - cx: &mut JsScope<'_>, - input: JsLocal<'_>, - callbacks: JsLocal<'_>, - options: JsLocal<'_>, -) -> JsResult { +pub(crate) fn render<'a>( + cx: &mut JsScope<'a>, + input: JsLocal<'a>, + callbacks: &RenderCallbacks<'a>, + options: &MarkdownOptions, +) -> JsResult> { let global_this = cx.global(); - let [input_value, callbacks_value, opts_value] = - [input.to_js(), callbacks.to_js(), options.to_js()]; + let input_value = input.to_js(); if input_value.is_empty_or_undefined_or_null() { return Err(global_this @@ -286,8 +225,7 @@ pub(crate) fn render( None => buffer.slice(), }; - // Parse parser options from 3rd argument - let options = parse_options(global_this, opts_value)?; + let options = parser_options!(options); // Create JS callback renderer let mut js_renderer = match JsCallbackRenderer::init(global_this, input, options.heading_ids) { @@ -295,40 +233,45 @@ pub(crate) fn render( Err(_) => return Err(global_this.throw_out_of_memory()), }; - // Extract callbacks from 2nd argument - js_renderer.extract_callbacks(if callbacks_value.is_object() { - callbacks_value - } else { - JSValue::UNDEFINED - })?; + js_renderer.extract_callbacks(callbacks); // Run parser with the JS callback renderer if let Err(err) = md::render_with_renderer(input, options, js_renderer.renderer()) { return Err(parser_err_to_js(global_this, err, input.len())); } - // Return accumulated result - let result = js_renderer.get_result(); - bun_string_jsc::create_utf8_for_js(global_this, result) + Ok(js_renderer.into_result()) } /// `Bun.markdown.react(text, components?, options?)` — returns a React Fragment element /// containing the parsed markdown as children. // The closure scopes a MarkedArgumentBuffer around the impl so every JSValue it // accumulates stays GC-visible for the duration of the call. -pub(crate) fn react( - cx: &mut JsScope<'_>, - input: JsLocal<'_>, - components: JsLocal<'_>, - options: JsLocal<'_>, +pub(crate) fn react<'a>( + cx: &mut JsScope<'a>, + input: JsLocal<'a>, + components: &ComponentOverrides<'a>, + options: &ReactOptions<'a>, ) -> JsResult { let global_this = cx.global(); + let react_version: u8 = match options.react_version { + // react.element (React 18 and older) + NumberOrIgnored::Number(version) if version as i32 <= 18 => 0, + // react.transitional.element (React 19+) + _ => 1, + }; MarkedArgumentBuffer::new(|marked_args| { - render_react_impl( + let children = render_ast( global_this, - [input.to_js(), components.to_js(), options.to_js()], + input.to_js(), + components, + parser_options!(options), marked_args, - ) + Some(react_version), + )?; + let fragment = JSReactElement__createFragment(global_this, react_version, children); + marked_args.append(fragment); + Ok(fragment) }) } @@ -340,34 +283,11 @@ unsafe extern "C" { ) -> JSValue; } -fn render_react_impl( - global_this: &JSGlobalObject, - args: [JSValue; 3], - marked_args: &mut MarkedArgumentBuffer, -) -> JsResult { - let opts_value = args[2]; // options are the 3rd argument - - let mut react_version: u8 = 1; // default: react.transitional.element (React 19+) - if opts_value.is_object() { - if let Some(rv) = opts_value.get(global_this, "reactVersion")? { - if rv.is_number() { - let num = rv.to_int32(); - if num <= 18 { - react_version = 0; // react.element (React 18 and older) - } - } - } - } - - let children = render_ast(global_this, args, marked_args, Some(react_version))?; - let fragment = JSReactElement__createFragment(global_this, react_version, children); - marked_args.append(fragment); - Ok(fragment) -} - fn render_ast( global_this: &JSGlobalObject, - [input_value, components_value, opts_value]: [JSValue; 3], + input_value: JSValue, + components: &ComponentOverrides<'_>, + options: md::Options, marked_args: &mut MarkedArgumentBuffer, react_version: Option, ) -> JsResult { @@ -387,9 +307,6 @@ fn render_ast( None => buffer.slice(), }; - // Parse parser options from 3rd argument - let options = parse_options(global_this, opts_value)?; - let mut renderer = match ParseRenderer::init( global_this, input, @@ -401,12 +318,7 @@ fn render_ast( Err(_) => return Err(global_this.throw_out_of_memory()), }; - // Extract component overrides from 2nd argument - renderer.extract_components(if components_value.is_object() { - components_value - } else { - JSValue::UNDEFINED - })?; + renderer.extract_components(components); if let Err(err) = md::render_with_renderer(input, options, renderer.renderer()) { return Err(parser_err_to_js(global_this, err, input.len())); @@ -569,16 +481,12 @@ impl<'a> ParseRenderer<'a> { // Note: deinit() dropped — Vec and HeadingIdTracker free via Drop. - /// Extract component overrides from options. Any non-boolean truthy value - /// (function, class, string, etc.) keyed by an HTML tag name is stored + /// Any non-boolean value (function, class, string, etc.) keyed by an HTML tag name is stored /// and used as the `type` field instead of the default string tag name. - fn extract_components(&mut self, opts: JSValue) -> JsResult<()> { - if opts.is_undefined_or_null() || !opts.is_object() { - return Ok(()); - } + fn extract_components(&mut self, components: &ComponentOverrides<'_>) { macro_rules! extract { ($($name:ident),* $(,)?) => {$( - if let Some(val) = opts.get_truthy(self.global_object, stringify!($name))? { + if let Some(val) = components.$name.map(JsLocal::to_js) { if !val.is_boolean() { self.components.$name = val; self.marked_args.append(val); @@ -590,7 +498,6 @@ impl<'a> ParseRenderer<'a> { h1, h2, h3, h4, h5, h6, p, blockquote, ul, ol, li, pre, hr, html, table, thead, tbody, tr, th, td, em, strong, a, img, code, del, math, u, br, ); - Ok(()) } fn get_block_component(&self, block_type: md::BlockType, data: u32) -> JSValue { @@ -1146,43 +1053,37 @@ impl<'a> JsCallbackRenderer<'a> { Ok(self_) } - fn extract_callbacks(&mut self, opts: JSValue) -> JsResult<()> { - if opts.is_undefined_or_null() || !opts.is_object() { - return Ok(()); - } + fn extract_callbacks(&mut self, callbacks: &RenderCallbacks<'_>) { macro_rules! extract { - ($($field:ident => $key:literal),* $(,)?) => {$( - if let Some(val) = opts.get_truthy(self.global_object, $key)? { - if val.is_callable() { - self.callbacks.$field = val; - } + ($($field:ident),* $(,)?) => {$( + if let CallbackOrIgnored::Callback(callback) = callbacks.$field { + self.callbacks.$field = callback.to_js(); } )*}; } extract!( - heading => "heading", - paragraph => "paragraph", - blockquote => "blockquote", - code => "code", - list => "list", - list_item => "listItem", - hr => "hr", - table => "table", - thead => "thead", - tbody => "tbody", - tr => "tr", - th => "th", - td => "td", - html => "html", - strong => "strong", - emphasis => "emphasis", - link => "link", - image => "image", - codespan => "codespan", - strikethrough => "strikethrough", - text => "text", + heading, + paragraph, + blockquote, + code, + list, + list_item, + hr, + table, + thead, + tbody, + tr, + th, + td, + html, + strong, + emphasis, + link, + image, + codespan, + strikethrough, + text, ); - Ok(()) } // Note: deinit() dropped — Vec (with Vec buffers) and HeadingIdTracker free via Drop. @@ -1260,11 +1161,12 @@ impl<'a> JsCallbackRenderer<'a> { Ok(()) } - fn get_result(&self) -> &[u8] { - if self.stack.is_empty() { - return b""; - } - self.stack[0].buffer.as_slice() + fn into_result(self) -> Vec { + self.stack + .into_iter() + .next() + .map(|root| root.buffer) + .unwrap_or_default() } // ======================================== diff --git a/src/runtime/api/NativePromiseContext.rs b/src/runtime/api/NativePromiseContext.rs index 4543770b3f3c..c001bf4374dd 100644 --- a/src/runtime/api/NativePromiseContext.rs +++ b/src/runtime/api/NativePromiseContext.rs @@ -13,8 +13,8 @@ //! let cell = native_promise_context::create(global, ctx); //! promise.then_with_value(global, cell, on_resolve, on_reject)?; //! -//! // In on_resolve/on_reject: -//! let Some(ctx) = native_promise_context::take::(arguments[1]) else { return; }; +//! // In on_resolve/on_reject, with `context: b.nativePromiseContext("crate::..::RequestContext")` in the schema: +//! let Some(ctx) = context else { return; }; //! // ... process ... //! ctx.deref_(); //! ``` diff --git a/src/runtime/api/crash_handler_jsc.bind.ts b/src/runtime/api/crash_handler_jsc.bind.ts index 273b99000204..067bcd79ef31 100644 --- a/src/runtime/api/crash_handler_jsc.bind.ts +++ b/src/runtime/api/crash_handler_jsc.bind.ts @@ -2,9 +2,17 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::api::crash_handler_jsc" }); +export const FeatureData = b.dictionary({ + features: b.array(b.string), + version: b.string, + is_canary: b.bool, + revision: b.string, + generated_at: b.u64, +}); + export const crashHandler = b.namespace({ getMachOImageZeroOffset: { returns: b.f64.optional, length: 1 }, - getFeatureData: { returns: b.any, capability: "js", throws: true, length: 1 }, + getFeatureData: { returns: FeatureData, length: 1 }, segfault: { length: 1 }, segfaultInDll: { length: 1 }, panic: { length: 1 }, diff --git a/src/runtime/api/crash_handler_jsc.rs b/src/runtime/api/crash_handler_jsc.rs index 73d00d74b12c..a279f56b1e9b 100644 --- a/src/runtime/api/crash_handler_jsc.rs +++ b/src/runtime/api/crash_handler_jsc.rs @@ -5,8 +5,8 @@ use bun_analytics as analytics; use bun_core::String as BunString; use bun_core::{Environment, Global}; use bun_crash_handler as crash_handler; -use bun_jsc::bindgen::JsScope; -use bun_jsc::{JSValue, JsResult, StringJsc}; + +use crate::generated_bindgen::crash_handler_jsc::FeatureData; pub(crate) fn get_mach_o_image_zero_offset() -> Option { #[cfg(not(target_os = "macos"))] @@ -174,38 +174,18 @@ pub(crate) fn raise_ignoring_panic_handler() { Global::raise_ignoring_panic_handler(bun_core::SignalCode::SIGSEGV); } -pub(crate) fn get_feature_data(cx: &mut JsScope<'_>) -> JsResult { - let global = cx.global(); - let obj = JSValue::create_empty_object(global, 5); - let list = analytics::PACKED_FEATURES_LIST; - // Not `static_`: a feature can be named like a common string ("s3"). - let array = JSValue::create_array_from_iter(global, list.iter(), |feature| { - BunString::clone_latin1(feature.as_ref()).into_js(global) - })?; - obj.put(global, "features", array); - obj.put( - global, - "version", - BunString::static_(Global::package_json_version).to_js(global)?, - ); - obj.put( - global, - "is_canary", - JSValue::js_boolean(Environment::IS_CANARY), - ); - - // This is the source of truth for the git sha. - // Not the github ref or the git tag. - obj.put( - global, - "revision", - BunString::static_(Environment::GIT_SHA).to_js(global)?, - ); - - obj.put( - global, - "generated_at", - JSValue::js_number_from_int64(bun_core::time::milli_timestamp().max(0)), - ); - Ok(obj) +pub(crate) fn get_feature_data() -> FeatureData { + FeatureData { + // Not `static_`: a feature can be named like a common string ("s3"). + features: analytics::PACKED_FEATURES_LIST + .iter() + .map(|feature| BunString::clone_latin1(feature.as_ref())) + .collect(), + version: BunString::static_(Global::package_json_version), + is_canary: Environment::IS_CANARY, + // This is the source of truth for the git sha. + // Not the github ref or the git tag. + revision: BunString::static_(Environment::GIT_SHA), + generated_at: bun_core::time::milli_timestamp().max(0) as u64, + } } diff --git a/src/runtime/api/cron.bind.ts b/src/runtime/api/cron.bind.ts index 332a74a012e0..70a21525903f 100644 --- a/src/runtime/api/cron.bind.ts +++ b/src/runtime/api/cron.bind.ts @@ -18,24 +18,50 @@ export const CronJob = b.class({ }, }); -const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +const reaction = { + args: { value: b.any, context: b.nativePromiseContext("crate::api::cron::CronJob") }, + capability: "js", +} as const; /** The reactions to the promise a job's callback returned. */ export const onPromiseResolve = b.fn({ ...reaction, export: "Bun__CronJob__onPromiseResolve" }); export const onPromiseReject = b.fn({ ...reaction, export: "Bun__CronJob__onPromiseReject" }); -/** `Bun.cron(schedule, handler, options?)` or `Bun.cron(path, schedule, title)`. */ -export const cron = b.fn({ - export: "Bun__Cron__cron", - args: { pathOrSchedule: b.any, scheduleOrHandler: b.any, titleOrOptions: b.any }, - returns: b.any, - capability: "js", - throws: true, -}); +export const CronOptions = b.dictionary({ tz: b.string.utf8.nullable }, { allowArray: true }); + +const signature = { export: "Bun__Cron__cron", capability: "js", throws: true } as const; + +export const cron = b.overloads( + /** An OS-level job. */ + { + ...signature, + args: { path: b.string.utf8, schedule: b.string.utf8, title: b.string.utf8 }, + expected: { + path: "Bun.cron() expects a string path as the first argument", + schedule: "Bun.cron() expects a string schedule as the second argument", + title: "Bun.cron() expects a string title as the third argument", + }, + returns: b.any, + length: 3, + rustPath: "crate::api::cron::register_os_job", + }, + /** An in-process job, whose `cron` is the `schedule` it was given. */ + { + ...signature, + args: { schedule: b.string.utf8.withValue, handler: b.callable, options: CronOptions.nullable }, + expected: { + schedule: "Bun.cron() expects a string cron expression", + handler: "Bun.cron(schedule, handler) expects a function handler as the second argument", + options: "Bun.cron: options must be an object", + }, + returns: CronJob, + rustPath: "crate::api::cron::CronJob::register", + }, +); export const remove = b.fn({ export: "Bun__Cron__remove", - args: { title: b.string.allowStringObject }, + args: { title: b.string.utf8 }, expected: { title: "Bun.cron.remove() expects a string title" }, returns: b.any, capability: "js", @@ -44,8 +70,12 @@ export const remove = b.fn({ export const parse = b.fn({ export: "Bun__Cron__parse", - args: { expression: b.string.allowStringObject, from: b.any, options: b.any }, - expected: { expression: "Bun.cron.parse() expects a string cron expression as the first argument" }, + args: { expression: b.string.utf8, from: b.f64.allowDate.nullable, options: CronOptions.nullable }, + expected: { + expression: "Bun.cron.parse() expects a string cron expression as the first argument", + from: "Bun.cron.parse() expects the second argument to be a Date or number (ms since epoch)", + options: "Bun.cron: options must be an object", + }, returns: b.f64.asDate.nullable, capability: "js", throws: true, diff --git a/src/runtime/api/cron.rs b/src/runtime/api/cron.rs index 749ba863c37a..f3181b383874 100644 --- a/src/runtime/api/cron.rs +++ b/src/runtime/api/cron.rs @@ -22,7 +22,7 @@ use bun_core::EncodedSlice; use bun_core::env_var; use bun_io::BufferedReader as OutputReader; use bun_io::{KeepAlive, Loop as AsyncLoop}; -use bun_jsc::bindgen::{JsLocal, JsScope, JsWrapper}; +use bun_jsc::bindgen::{Callable, JsLocal, JsScope, JsWrapper}; use bun_jsc::virtual_machine::{HotReload, VirtualMachine}; use bun_jsc::{ self as jsc, EncodedSliceJsc as _, EventLoopHandle, GlobalRef, JSFunction, JSGlobalObject, @@ -41,7 +41,7 @@ use crate::api::bun::process::{ }; use crate::generated_bindgen::cron::{ Bun__Cron__cron, Bun__Cron__parse, Bun__Cron__remove, Bun__CronJob__onPromiseReject, - Bun__CronJob__onPromiseResolve, js_CronJob as js, + Bun__CronJob__onPromiseResolve, CronOptions, js_CronJob as js, }; use crate::timer::{EventLoopTimer, EventLoopTimerState, EventLoopTimerTag}; use bun_core::ZStr; @@ -712,29 +712,10 @@ impl CronRegisterJob { } /// Resolve the `{ tz?: string }` option to a `CronTz`. -fn resolve_cron_tz(global: &JSGlobalObject, opts: JSValue) -> JsResult { - if opts.is_empty() || opts.is_undefined_or_null() { - return Ok(CronTz::Local); - } - if !opts.is_object() { - return Err( - global.throw_invalid_arguments(format_args!("Bun.cron: options must be an object")) - ); - } - let Some(tz_val) = opts.get(global, "tz")? else { +fn resolve_cron_tz(global: &JSGlobalObject, options: Option>) -> JsResult { + let Some(tz_bytes) = options.and_then(|options| options.tz) else { return Ok(CronTz::Local); }; - if tz_val.is_undefined_or_null() { - return Ok(CronTz::Local); - } - if !tz_val.is_string() { - return Err( - global.throw_invalid_arguments(format_args!("Bun.cron: options.tz must be a string")) - ); - } - let tz_str = tz_val.to_bun_string(global)?; - let tz_slice = tz_str.to_utf8(); - let tz_bytes = tz_slice.slice(); // IANA names are ASCII; rejecting here keeps the Latin-1 StringView cast in // Bun__resolveTimeZoneID sound for non-ASCII UTF-8 input. if tz_bytes.is_ascii() @@ -750,63 +731,23 @@ fn resolve_cron_tz(global: &JSGlobalObject, opts: JSValue) -> JsResult { // -- JS entry point -- -pub(crate) fn cron( +pub(crate) fn register_os_job( cx: &mut JsScope<'_>, - path_or_schedule: JsLocal<'_>, - schedule_or_handler: JsLocal<'_>, - title_or_options: JsLocal<'_>, + path: &[u8], + schedule: &[u8], + title: &[u8], ) -> JsResult { - let args = [ - path_or_schedule.to_js(), - schedule_or_handler.to_js(), - title_or_options.to_js(), - ]; - - // In-process callback cron: Bun.cron(schedule, handler, opts?) - if args[1].is_callable() { - let tz = resolve_cron_tz(cx.global(), args[2])?; - return CronJob::register(&cx.js_thread(), args[0], args[1], tz); - } let global = cx.global(); - if args[0].is_string() && args[2].is_undefined() { - return Err(global.throw_invalid_arguments(format_args!( - "Bun.cron(schedule, handler) expects a function handler as the second argument" - ))); - } - - if !args[0].is_string() { - return Err(global.throw_invalid_arguments(format_args!( - "Bun.cron() expects a string path as the first argument" - ))); - } - if !args[1].is_string() { - return Err(global.throw_invalid_arguments(format_args!( - "Bun.cron() expects a string schedule as the second argument" - ))); - } - if !args[2].is_string() { - return Err(global.throw_invalid_arguments(format_args!( - "Bun.cron() expects a string title as the third argument" - ))); - } - - let path_str = args[0].to_bun_string(global)?; - let schedule_str = args[1].to_bun_string(global)?; - let title_str = args[2].to_bun_string(global)?; - - let path_slice = path_str.to_utf8(); - let schedule_slice = schedule_str.to_utf8(); - let title_slice = title_str.to_utf8(); // Validate title: only [a-zA-Z0-9_-] - if !validate_title(title_slice.slice()) { + if !validate_title(title) { return Err(global.throw_invalid_arguments(format_args!( "Cron title must contain only alphanumeric characters, hyphens, and underscores" ))); } // Parse and normalize cron schedule to numeric form for crontab/launchd/schtasks - let parsed = match CronExpression::parse(schedule_slice.slice()) { + let parsed = match CronExpression::parse(schedule) { Ok(p) => p, Err(e) => { return Err(global.throw_invalid_arguments(format_args!( @@ -818,7 +759,7 @@ pub(crate) fn cron( let mut fmt_buf = [0u8; 512]; let normalized_schedule = parsed.format_numeric(&mut fmt_buf); - let abs_path = match resolve_path(global, path_slice.slice()) { + let abs_path = match resolve_path(global, path) { Ok(p) => p, Err(_) => { return Err(global.throw_invalid_arguments(format_args!("Failed to resolve path"))); @@ -866,7 +807,7 @@ pub(crate) fn cron( bun_exe, abs_path, schedule: ZString::from_bytes(normalized_schedule), - title: ZString::from_bytes(title_slice.slice()), + title: ZString::from_bytes(title), #[cfg(windows)] parsed_cron: parsed, state: Cell::new(RegisterState::ReadingCrontab), @@ -1290,11 +1231,10 @@ impl CronRemoveJob { } } -pub(crate) fn remove(cx: &mut JsScope<'_>, title: &bun_core::String) -> JsResult { +pub(crate) fn remove(cx: &mut JsScope<'_>, title: &[u8]) -> JsResult { let global = cx.global(); - let title_slice = title.to_utf8(); - if !validate_title(title_slice.slice()) { + if !validate_title(title) { return Err(global.throw_invalid_arguments(format_args!( "Cron title must contain only alphanumeric characters, hyphens, and underscores" ))); @@ -1306,7 +1246,7 @@ pub(crate) fn remove(cx: &mut JsScope<'_>, title: &bun_core::String) -> JsResult promise: JsCell::new(jsc::JSPromiseStrong::init(global)), global: GlobalRef::from(global), poll: JsCell::new(KeepAlive::default()), - title: ZString::from_bytes(title_slice.slice()), + title: ZString::from_bytes(title), state: Cell::new(RemoveState::ReadingCrontab), process: JsCell::new(None), stdout_reader: JsCell::new(OutputReader::init::()), @@ -1772,23 +1712,17 @@ impl CronJob { this_value } - pub(crate) fn register( - cx: &bun_jsc::JsThread<'_>, - schedule_arg: JSValue, - callback_arg: JSValue, - tz: CronTz, + pub(crate) fn register<'a>( + cx: &mut JsScope<'a>, + (schedule, schedule_value): (&[u8], JsLocal<'a>), + handler: JsLocal<'a, Callable>, + options: Option>, ) -> JsResult { + let tz = resolve_cron_tz(cx.global(), options)?; + let cx = &cx.js_thread(); let global = cx.global(); - if !schedule_arg.is_string() { - return Err(global.throw_invalid_arguments(format_args!( - "Bun.cron() expects a string cron expression" - ))); - } - - let schedule_str = schedule_arg.to_bun_string(global)?; - let schedule_slice = schedule_str.to_utf8(); - let parsed = match CronExpression::parse(schedule_slice.slice()) { + let parsed = match CronExpression::parse(schedule) { Ok(p) => p, Err(e) => { return Err(global.throw_invalid_arguments(format_args!( @@ -1821,7 +1755,7 @@ impl CronJob { let Some(next_time) = job.compute_next_timespec() else { return Err(global.throw_invalid_arguments(format_args!( "Cron expression '{}' has no future occurrences", - bstr::BStr::new(schedule_slice.slice()) + bstr::BStr::new(schedule) ))); }; @@ -1838,11 +1772,11 @@ impl CronJob { let js_value = Self::to_js_nonnull(job.as_non_null(), global); let job = job.into_this_ptr(); job.this_value.with_mut(|v| v.set_strong(js_value, global)); - js::cron_set_cached(js_value, global, schedule_arg); + js::cron_set_cached(js_value, global, schedule_value.to_js()); js::callback_set_cached( js_value, global, - callback_arg.with_async_context_if_needed(global), + handler.to_js().with_async_context_if_needed(global), ); job.poll_ref.with_mut(|p| p.ref_(bun_io::js_vm_ctx())); @@ -1859,9 +1793,13 @@ impl CronJob { } } -pub(crate) fn on_promise_resolve(_: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { +pub(crate) fn on_promise_resolve( + _: &mut JsScope<'_>, + _: JsLocal<'_>, + context: Option>, +) { // The cell hands back the job's ref. None: the claim was taken back (the VM is going). - let Some(job) = crate::api::native_promise_context::take::(context.to_js()) else { + let Some(job) = context else { return; }; // SAFETY: the claim the cell handed back is a ref on the live job. @@ -1874,9 +1812,13 @@ pub(crate) fn on_promise_resolve(_: &mut JsScope<'_>, _: JsLocal<'_>, context: J CronJob::schedule_next(this, vm); } -pub(crate) fn on_promise_reject(_: &mut JsScope<'_>, reason: JsLocal<'_>, context: JsLocal<'_>) { +pub(crate) fn on_promise_reject( + _: &mut JsScope<'_>, + reason: JsLocal<'_>, + context: Option>, +) { // As `on_promise_resolve`. - let Some(job) = crate::api::native_promise_context::take::(context.to_js()) else { + let Some(job) = context else { return; }; // SAFETY: the claim the cell handed back is a ref on the live job. @@ -1923,15 +1865,13 @@ pub(crate) fn get_cron_object(global_this: &JSGlobalObject, _obj: &JSObject) -> pub(crate) fn parse( cx: &mut JsScope<'_>, - expression: &bun_core::String, - from: JsLocal<'_>, - options: JsLocal<'_>, + expression: &[u8], + from: Option, + options: Option>, ) -> JsResult> { let global = cx.global(); - let from = from.to_js(); - let expr_slice = expression.to_utf8(); - let parsed = match CronExpression::parse(expr_slice.slice()) { + let parsed = match CronExpression::parse(expression) { Ok(p) => p, Err(e) => { return Err(global.throw_invalid_arguments(format_args!( @@ -1941,24 +1881,14 @@ pub(crate) fn parse( } }; - let from_ms: f64 = if !from.is_empty() && !from.is_undefined() && from != JSValue::NULL { - if from.is_number() || from.js_type() == jsc::JSType::JSDate { - from.to_number(global)? - } else { - return Err(global.throw_invalid_arguments(format_args!( - "Bun.cron.parse() expects the second argument to be a Date or number (ms since epoch)" - ))); - } - } else { - bun_core::time::milli_timestamp() as f64 - }; + let from_ms = from.unwrap_or_else(|| bun_core::time::milli_timestamp() as f64); // An out-of-range `from` is an invalid argument. next() would report it as "no match". if from_ms.is_nan() || from_ms.abs() > jsc::wtf::MAX_ECMASCRIPT_TIME { return Err(global.throw_invalid_arguments(format_args!("Invalid date value"))); } - let tz = resolve_cron_tz(global, options.to_js())?; + let tz = resolve_cron_tz(global, options)?; parsed.next(global, from_ms, tz) } diff --git a/src/runtime/api/html_rewriter.rs b/src/runtime/api/html_rewriter.rs index db06097cf20b..0f0e7271778c 100644 --- a/src/runtime/api/html_rewriter.rs +++ b/src/runtime/api/html_rewriter.rs @@ -1994,11 +1994,10 @@ impl crate::webcore::sink::JsSinkType for RewriterPipe { pub(crate) fn on_handler_resolve( cx: &mut JsScope<'_>, _: JsLocal<'_>, - context: JsLocal<'_>, + context: Option>, ) -> JsResult<()> { - // `take` nulls the context so its destructor is a no-op; `None` means the - // suspension was already abandoned. - let Some(pipe) = native_promise_context::take::(context.to_js()) else { + // `None` means the suspension was already abandoned. + let Some(pipe) = context else { return Ok(()); }; let pipe = BackRef::from(pipe); @@ -2018,10 +2017,10 @@ pub(crate) fn on_handler_resolve( pub(crate) fn on_handler_reject( cx: &mut JsScope<'_>, reason: JsLocal<'_>, - context: JsLocal<'_>, + context: Option>, ) -> JsResult<()> { let global = cx.global(); - let Some(pipe) = native_promise_context::take::(context.to_js()) else { + let Some(pipe) = context else { return Ok(()); }; let pipe = BackRef::from(pipe); @@ -3235,28 +3234,18 @@ impl WrapperLike for Element { // `RewriterPipe::wire_input`'s `.then_with_value()`; the cell (and thus the // pipe) stays alive as long as the pump promise's reaction is rooted. -pub(crate) fn on_resolve_input_stream(_: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { - let Some(this) = js_HTMLRewriterTransform::from_js(context.to_js()) else { - return; - }; - let this = BackRef::from(this); - let this = &*this; - this.js_pump_reaction_pending.set(false); - this.end_from_stream(None); +pub(crate) fn on_resolve_input_stream(_: &mut JsScope<'_>, _: JsLocal<'_>, context: &RewriterPipe) { + context.js_pump_reaction_pending.set(false); + context.end_from_stream(None); } pub(crate) fn on_reject_input_stream( cx: &mut JsScope<'_>, reason: JsLocal<'_>, - context: JsLocal<'_>, + context: &RewriterPipe, ) { - let Some(this) = js_HTMLRewriterTransform::from_js(context.to_js()) else { - return; - }; - let this = BackRef::from(this); - let this = &*this; - this.js_pump_reaction_pending.set(false); - this.end_from_stream(Some(crate::webcore::streams::StreamError::JSValue( + context.js_pump_reaction_pending.set(false); + context.end_from_stream(Some(crate::webcore::streams::StreamError::JSValue( jsc::strong::Optional::create(reason.to_js(), cx.global()), ))); } diff --git a/src/runtime/api/html_rewriter_internals.bind.ts b/src/runtime/api/html_rewriter_internals.bind.ts index e09cca1df078..aaf02a512079 100644 --- a/src/runtime/api/html_rewriter_internals.bind.ts +++ b/src/runtime/api/html_rewriter_internals.bind.ts @@ -1,13 +1,18 @@ import * as b from "../../codegen/bindgen-lib.ts"; +import { HTMLRewriterTransform } from "./html_rewriter.bind.ts"; export default b.module({ rustPath: "crate::api::html_rewriter" }); -const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; - /** The reactions to the promise a content handler returned. */ -export const onHandlerResolve = b.fn({ ...reaction, throws: true, export: "Bun__HTMLRewriter__onHandlerResolve" }); -export const onHandlerReject = b.fn({ ...reaction, throws: true, export: "Bun__HTMLRewriter__onHandlerReject" }); +const handlerReaction = { + args: { value: b.any, context: b.nativePromiseContext("crate::api::html_rewriter::RewriterPipe") }, + capability: "js", + throws: true, +} as const; +export const onHandlerResolve = b.fn({ ...handlerReaction, export: "Bun__HTMLRewriter__onHandlerResolve" }); +export const onHandlerReject = b.fn({ ...handlerReaction, export: "Bun__HTMLRewriter__onHandlerReject" }); /** The reactions to the promise of the JavaScript that reads the input stream. */ -export const onResolveInputStream = b.fn({ ...reaction, export: "Bun__HTMLRewriter__onResolveInputStream" }); -export const onRejectInputStream = b.fn({ ...reaction, export: "Bun__HTMLRewriter__onRejectInputStream" }); +const inputStreamReaction = { args: { value: b.any, context: HTMLRewriterTransform }, capability: "js" } as const; +export const onResolveInputStream = b.fn({ ...inputStreamReaction, export: "Bun__HTMLRewriter__onResolveInputStream" }); +export const onRejectInputStream = b.fn({ ...inputStreamReaction, export: "Bun__HTMLRewriter__onRejectInputStream" }); diff --git a/src/runtime/api/sourcemap.bind.ts b/src/runtime/api/sourcemap.bind.ts index 99a534410e51..8f4ec44ed123 100644 --- a/src/runtime/api/sourcemap.bind.ts +++ b/src/runtime/api/sourcemap.bind.ts @@ -23,4 +23,10 @@ export const SourceMap = b.class({ }, }); -export const findSourceMap = b.fn({ args: { path: b.any }, returns: b.any, capability: "js", throws: true }); +/** Returns before it looks at `path`: source maps are not enabled. */ +export const findSourceMap = b.fn({ + args: { path: b.any }, + returns: SourceMap.optional, + capability: "js", + throws: true, +}); diff --git a/src/runtime/bake/config/bake_plugins.bind.ts b/src/runtime/bake/config/bake_plugins.bind.ts index 51158a01bcb1..46fcd386b01e 100644 --- a/src/runtime/bake/config/bake_plugins.bind.ts +++ b/src/runtime/bake/config/bake_plugins.bind.ts @@ -2,7 +2,10 @@ import * as b from "../../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::bake::config::plugins" }); -export const setConfigPlugins = b.fn({ args: { plugins: b.any, names: b.any }, capability: "js", throws: true }); +export const setConfigPlugins = b.fn({ + args: { plugins: b.any, names: b.array(b.string.loose.utf8.owned) }, + capability: "js", +}); export const isRestart = b.fn({ returns: b.bool }); export const restartProcess = b.fn({ capability: "js" }); export const isRestarting = b.fn({ returns: b.bool }); diff --git a/src/runtime/bake/config/plugins.rs b/src/runtime/bake/config/plugins.rs index 51d6a2d28a0b..1e8d0a7bb3b8 100644 --- a/src/runtime/bake/config/plugins.rs +++ b/src/runtime/bake/config/plugins.rs @@ -1,6 +1,7 @@ //! `bun bake`: the plugins of `bake.config.ts`. `internal/bake.ts` checks them, then hands them over //! here before it starts the server. +use bun_core::Utf8Bytes; use bun_jsc::JsResult; use bun_jsc::bindgen::{JsLocal, JsScope}; @@ -9,23 +10,17 @@ use bun_jsc::bindgen::{JsLocal, JsScope}; pub(crate) fn set_config_plugins<'a>( cx: &mut JsScope<'a>, plugins: JsLocal<'a>, - names: JsLocal<'a>, -) -> JsResult<()> { + names: Vec>, +) { let global = cx.global(); - let mut list: Vec> = Vec::new(); - let mut iter = names.to_js().array_iterator(global)?; - while let Some(name) = iter.next()? { - list.push(Box::from(&*name.to_utf8(global)?)); - } let vm = global.bun_vm().as_mut(); // With none, the server sets up no plugins, unless bunfig.toml has some. - if list.is_empty() { + if names.is_empty() { vm.bake_config_plugins.deinit(); } else { vm.bake_config_plugins.set(global, plugins.to_js()); } - vm.bake_config_plugin_names = list.into_boxed_slice(); - Ok(()) + vm.bake_config_plugin_names = names.into_iter().map(|name| Box::from(&*name)).collect(); } /// Set in the environment by the first start of `bun bake --check`, so that the process that a diff --git a/src/runtime/bake/router/FrameworkRouter.bind.ts b/src/runtime/bake/router/FrameworkRouter.bind.ts index d8ff0f91c8c9..47da91485de2 100644 --- a/src/runtime/bake/router/FrameworkRouter.bind.ts +++ b/src/runtime/bake/router/FrameworkRouter.bind.ts @@ -2,9 +2,12 @@ import * as b from "../../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::bake::router::JSFrameworkRouter" }); +export const RoutePatternKind = b.enumeration(["page", "layout", "extra"]); +export const RoutePattern = b.dictionary({ kind: RoutePatternKind, pattern: b.string.utf8 }); + export const parseRoutePattern = b.fn({ - args: { style: b.any, filepath: b.any }, - returns: b.any, + args: { style: b.any, filepath: b.string.loose.utf8 }, + returns: RoutePattern.nullable, length: 1, capability: "js", throws: true, diff --git a/src/runtime/bake/router/js.rs b/src/runtime/bake/router/js.rs index dabeaf13da08..7fd47e5e91bc 100644 --- a/src/runtime/bake/router/js.rs +++ b/src/runtime/bake/router/js.rs @@ -10,6 +10,7 @@ use bun_jsc::{JSGlobalObject, JSValue, JsResult, StringJsc, StrongOptional}; use bun_paths as paths; use super::*; +use crate::generated_bindgen::framework_router::{RoutePattern, RoutePatternKind}; /// This binding is currently only intended for testing FrameworkRouter, and not /// production usage. It uses a slower but easier to use pattern for object @@ -221,22 +222,16 @@ impl JSFrameworkRouter { pub(crate) fn parse_route_pattern<'a>( cx: &mut JsScope<'a>, style: JsLocal<'a>, - filepath: JsLocal<'a>, - ) -> JsResult { + filepath: &[u8], + ) -> JsResult> { let global = cx.global(); let arena = Arena::new(); - let filepath = filepath.to_js().to_utf8(global)?; let style = Style::from_js(style.to_js(), global)?; let mut log = TinyLog::empty(); - let parsed = match style.parse( - filepath.slice(), - paths::extension(filepath.slice()), - &mut log, - true, - &arena, - ) { + let parsed = match style.parse(filepath, paths::extension(filepath), &mut log, true, &arena) + { Err(crate::Error::InvalidRoutePattern) => { return Err(global.throw(format_args!( "{} ({}:{})", @@ -246,25 +241,24 @@ impl JSFrameworkRouter { ))); } Err(e) => return Err(e.into()), - Ok(None) => return Ok(JSValue::NULL), + Ok(None) => return Ok(None), Ok(Some(p)) => p, }; - let mut rendered: Vec = Vec::with_capacity(filepath.slice().len()); + let mut rendered: Vec = Vec::with_capacity(filepath.len()); for part in parsed.parts { part.to_string_for_internal_use(&mut ByteFmtWriter::new(&mut rendered)) .expect("ByteFmtWriter is infallible"); } - let out = bun_core::String::clone_utf8(&rendered); - let obj = JSValue::create_empty_object(global, 2); - obj.put( - global, - b"kind", - bun_core::String::static_(<&'static str>::from(parsed.kind)).to_js(global)?, - ); - obj.put(global, b"pattern", out.into_js(global)?); - Ok(obj) + Ok(Some(RoutePattern { + kind: match parsed.kind { + ParsedPatternKind::Page => RoutePatternKind::Page, + ParsedPatternKind::Layout => RoutePatternKind::Layout, + ParsedPatternKind::Extra => RoutePatternKind::Extra, + }, + pattern: rendered, + })) } fn part_to_js(global: &JSGlobalObject, part: &Part<'_>) -> JsResult { diff --git a/src/runtime/bake/router/pattern.rs b/src/runtime/bake/router/pattern.rs index d92317d0609b..85e7ab6c96ec 100644 --- a/src/runtime/bake/router/pattern.rs +++ b/src/runtime/bake/router/pattern.rs @@ -375,18 +375,15 @@ pub(crate) struct ParsedPattern<'a> { pub kind: ParsedPatternKind, } -#[derive(Copy, Clone, Eq, PartialEq, strum::IntoStaticStr)] -pub enum ParsedPatternKind { +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) enum ParsedPatternKind { /// Can be navigated to. Pages can have children, which allows having /// nested routes exactly how Remix allows them. - #[strum(serialize = "page")] Page, /// Is not considered when resolving navigations, but is still a valid /// node in the route tree. - #[strum(serialize = "layout")] Layout, /// Another file related to a route - #[strum(serialize = "extra")] Extra, } diff --git a/src/runtime/cli/pack_command.bind.ts b/src/runtime/cli/pack_command.bind.ts index e8a356005f88..213bfc481d71 100644 --- a/src/runtime/cli/pack_command.bind.ts +++ b/src/runtime/cli/pack_command.bind.ts @@ -2,9 +2,37 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::cli::pack_command::bindings" }); +export const TarballEntryKind = b.enumeration([ + "block_device", + "character_device", + "directory", + "named_pipe", + "sym_link", + "file", + "unix_domain_socket", + "whiteout", + "door", + "event_port", + "unknown", +]); + +export const TarballEntry = b.dictionary({ + pathname: b.string, + kind: TarballEntryKind, + perm: b.f64, + contents: b.string.optional, +}); + +export const Tarball = b.dictionary({ + entries: b.array(TarballEntry), + size: b.usize, + shasum: b.string, + integrity: b.string.utf8, +}); + export const readTarball = b.fn({ - args: { tarballPath: b.string.allowStringObject }, - returns: b.any, - capability: "js", + args: { tarballPath: b.string.utf8 }, + returns: Tarball, + capability: "heap", throws: true, }); diff --git a/src/runtime/cli/pack_command.rs b/src/runtime/cli/pack_command.rs index 685b2f9ef9b2..c82d34628c2a 100644 --- a/src/runtime/cli/pack_command.rs +++ b/src/runtime/cli/pack_command.rs @@ -1439,24 +1439,6 @@ fn negation_reaches_below(pattern: &Pattern, dir_rel: &[u8]) -> bool { glob_segments.next().is_some() } -/// Tag names for `bun_core::FileKind`. -fn file_kind_tag(kind: bun_core::FileKind) -> &'static str { - use bun_core::FileKind as K; - match kind { - K::BlockDevice => "block_device", - K::CharacterDevice => "character_device", - K::Directory => "directory", - K::NamedPipe => "named_pipe", - K::SymLink => "sym_link", - K::File => "file", - K::UnixDomainSocket => "unix_domain_socket", - K::Whiteout => "whiteout", - K::Door => "door", - K::EventPort => "event_port", - K::Unknown => "unknown", - } -} - /// Everything `bun pm pack` would put in the tarball besides package.json: bins, then either the `files` list or /// the whole tree minus ignores. Shared with `bun pm diff`, whose local side is "what would be published". /// `root_path` is the path `root_dir` was opened from. @@ -3147,22 +3129,37 @@ fn is_special_file_or_variant(filename: &[u8], name: &'static [u8]) -> bool { pub(crate) mod bindings { use super::*; use bun_core::String as BunString; - use bun_jsc::bindgen::JsScope; - use bun_jsc::{JSArray, JSValue, JsResult, StringJsc as _, bun_string_jsc}; - - pub(crate) fn read_tarball( - cx: &mut JsScope<'_>, - tarball_path_str: &BunString, - ) -> JsResult { - let global = cx.global(); - let tarball_path = tarball_path_str.to_utf8(); - - let tarball_file = match bun_sys::open_file(tarball_path.slice(), Default::default()) { + use bun_jsc::bindgen::{self, JsHeap}; + + use crate::generated_bindgen::pack_command::{Tarball, TarballEntry, TarballEntryKind}; + + fn tarball_entry_kind(kind: bun_sys::FileKind) -> TarballEntryKind { + use bun_sys::FileKind as K; + match kind { + K::BlockDevice => TarballEntryKind::BlockDevice, + K::CharacterDevice => TarballEntryKind::CharacterDevice, + K::Directory => TarballEntryKind::Directory, + K::NamedPipe => TarballEntryKind::NamedPipe, + K::SymLink => TarballEntryKind::SymLink, + K::File => TarballEntryKind::File, + K::UnixDomainSocket => TarballEntryKind::UnixDomainSocket, + K::Whiteout => TarballEntryKind::Whiteout, + K::Door => TarballEntryKind::Door, + K::EventPort => TarballEntryKind::EventPort, + K::Unknown => TarballEntryKind::Unknown, + } + } + + pub(crate) fn read_tarball<'a>( + heap: &JsHeap<'a>, + tarball_path: &[u8], + ) -> bindgen::Result<'a, Tarball> { + let tarball_file = match bun_sys::open_file(tarball_path, Default::default()) { Ok(f) => f, Err(err) => { - return Err(global.throw(format_args!( + return Err(heap.error(format_args!( "failed to open tarball file \"{}\": {}", - bstr::BStr::new(tarball_path.slice()), + bstr::BStr::new(tarball_path), crate::Error::from(err).name(), ))); } @@ -3172,9 +3169,9 @@ pub(crate) mod bindings { Ok(b) => b, Err(err) => { let _ = tarball_file.close(); - return Err(global.throw(format_args!( + return Err(heap.error(format_args!( "failed to read tarball contents \"{}\": {}", - bstr::BStr::new(tarball_path.slice()), + bstr::BStr::new(tarball_path), crate::Error::from(err).name(), ))); } @@ -3194,22 +3191,15 @@ pub(crate) mod bindings { let mut sha512 = sha::SHA512::init(); sha512.update(&tarball); sha512.r#final(&mut sha512_digest); - let base64_buf = bun_base64::encode_alloc(&sha512_digest); - let integrity_value = bun_string_jsc::create_utf8_for_js(global, &base64_buf)?; + let integrity = bun_base64::encode_alloc(&sha512_digest); - struct EntryInfo { - pathname: BunString, - kind: bun_sys::FileKind, - perm: bun_sys::Mode, - contents: Option, - } - let mut entries_info: Vec = Vec::new(); + let mut entries: Vec = Vec::new(); let archive = Archive::opaque_ref(Archive::read_new()); - let check = |status: ArchiveResult, action: &str| -> JsResult<()> { + let check = |status: ArchiveResult, action: &str| -> bindgen::Result<'a, ()> { match status { - ArchiveResult::Failed | ArchiveResult::Fatal | ArchiveResult::Warn => Err(global - .throw(format_args!( + ArchiveResult::Failed | ArchiveResult::Fatal | ArchiveResult::Warn => Err(heap + .error(format_args!( "failed to {}: {}", action, bstr::BStr::new(archive.error_string()) @@ -3243,7 +3233,7 @@ pub(crate) mod bindings { continue; } ArchiveResult::Failed | ArchiveResult::Fatal => { - return Err(global.throw(format_args!( + return Err(heap.error(format_args!( "failed to read archive header: {}", bstr::BStr::new(archive.error_string()), ))); @@ -3265,10 +3255,10 @@ pub(crate) mod bindings { let kind = bun_sys::kind_from_mode(archive_entry_ref.filetype()); let perm = archive_entry_ref.perm(); - let mut entry_info = EntryInfo { + let mut entry = TarballEntry { pathname: pathname_string, - kind, - perm, + kind: tarball_entry_kind(kind), + perm: f64::from(perm), contents: None, }; @@ -3279,19 +3269,19 @@ pub(crate) mod bindings { let read = archive.read_data(&mut read_buf); if read < 0 { - let pathname_utf8 = entry_info.pathname.to_utf8(); - return Err(global.throw(format_args!( + let pathname_utf8 = entry.pathname.to_utf8(); + return Err(heap.error(format_args!( "failed to read archive entry \"{}\": {}", bstr::BStr::new(pathname_utf8.slice()), bstr::BStr::new(archive.error_string()), ))); } read_buf.truncate(usize::try_from(read).expect("int cast")); - entry_info.contents = Some(BunString::clone_utf8(&read_buf)); + entry.contents = Some(BunString::clone_utf8(&read_buf)); read_buf.clear(); } - entries_info.push(entry_info); + entries.push(entry); } } header_status = archive.read_next_header(&mut archive_entry); @@ -3300,29 +3290,11 @@ pub(crate) mod bindings { check(archive.read_close(), "close read archive")?; check(archive.read_free(), "close read archive")?; - let entries = JSArray::create_empty(global, entries_info.len())?; - - for (i, entry) in entries_info.into_iter().enumerate() { - let obj = JSValue::create_empty_object(global, 0); - obj.put(global, b"pathname", entry.pathname.into_js(global)?); - let kind = match entry.kind { - bun_sys::FileKind::Unknown => global.common_strings().unknown(), - kind => BunString::static_(file_kind_tag(kind)).to_js(global)?, - }; - obj.put(global, b"kind", kind); - obj.put(global, b"perm", JSValue::js_number(f64::from(entry.perm))); - if let Some(contents) = entry.contents { - obj.put(global, b"contents", contents.into_js(global)?); - } - entries.put_index(global, u32::try_from(i).expect("int cast"), obj)?; - } - - let result = JSValue::create_empty_object(global, 4); - result.put(global, b"entries", entries); - result.put(global, b"size", JSValue::js_number(tarball.len() as f64)); - result.put(global, b"shasum", shasum_str.into_js(global)?); - result.put(global, b"integrity", integrity_value); - - Ok(result) + Ok(Tarball { + entries, + size: tarball.len(), + shasum: shasum_str, + integrity, + }) } } diff --git a/src/runtime/crypto/PBKDF2.rs b/src/runtime/crypto/PBKDF2.rs index 3c861b6cb566..77d02a76dee5 100644 --- a/src/runtime/crypto/PBKDF2.rs +++ b/src/runtime/crypto/PBKDF2.rs @@ -1,26 +1,58 @@ use core::ffi::c_uint; use bun_boringssl_sys as boringssl; -use bun_jsc::{ArrayBuffer, JSGlobalObject, JSValue, Job, JobContext, JsResult, JsThread, Strong}; +use bun_jsc::{JSGlobalObject, JSValue, Job, JobContext, JsResult, JsThread, Strong}; -use crate::node::{Flavor, StringOrBuffer, ThreadIsolated, ThreadIsolatedArg}; +use crate::node::{StringOrBuffer, ThreadIsolated}; use crate::crypto::evp::{self, Algorithm}; pub(crate) struct PBKDF2 { - pub password: StringOrBuffer<'static>, - pub salt: StringOrBuffer<'static>, - pub iteration_count: u32, + iteration_count: u32, pub length: usize, algorithm: Algorithm, } -// SAFETY: `password` and `salt` are `StringOrBuffer`s (see its impl); the rest is plain data. -unsafe impl ThreadIsolatedArg for PBKDF2 {} + +pub(crate) fn algorithm_from_digest( + global_this: &JSGlobalObject, + digest: &[u8], +) -> JsResult { + match evp::lookup_ignore_case(digest) { + Some(Algorithm::Shake128 | Algorithm::Shake256) | None => {} + Some(algorithm) if algorithm.md().is_none() => {} + Some(algorithm) => return Ok(algorithm), + } + Err(global_this + .err( + bun_jsc::ErrorCode::CRYPTO_INVALID_DIGEST, + format_args!("Invalid digest: {}", bstr::BStr::new(digest)), + ) + .throw()) +} impl PBKDF2 { - pub(crate) fn run(&mut self, output: &mut [u8]) -> bool { - let password = self.password.slice(); - let salt = self.salt.slice(); + pub(crate) fn new( + global_this: &JSGlobalObject, + password: &[u8], + salt: &[u8], + iteration_count: u32, + length: usize, + algorithm: Algorithm, + ) -> JsResult { + if salt.len() > i32::MAX as usize { + return Err(global_this.throw_invalid_arguments(format_args!("salt is too long"))); + } + if password.len() > i32::MAX as usize { + return Err(global_this.throw_invalid_arguments(format_args!("password is too long"))); + } + Ok(PBKDF2 { + iteration_count, + length, + algorithm, + }) + } + + pub(crate) fn run(&self, password: &[u8], salt: &[u8], output: &mut [u8]) -> bool { let algorithm = self.algorithm; let iteration_count = self.iteration_count; let length = self.length; @@ -53,216 +85,13 @@ impl PBKDF2 { rc > 0 } - - /// The second element is the validated callback on the `Async` flavor and - /// `JSValue::UNDEFINED` on `Sync` (as `Scrypt::from_js`). - pub(crate) fn from_js( - global_this: &JSGlobalObject, - password: JSValue, - salt: JSValue, - iterations: JSValue, - keylen: JSValue, - mut digest: JSValue, - mut callback: JSValue, - flavor: Flavor, - ) -> JsResult<(PBKDF2, JSValue)> { - // pbkdf2(password, salt, iterations, keylen, callback): digest omitted. - if flavor == Flavor::Async && digest.is_function() { - callback = digest; - digest = JSValue::UNDEFINED; - } - - if !keylen.is_number() { - return Err(global_this.throw_invalid_argument_type_value(b"keylen", b"number", keylen)); - } - - let keylen_num = keylen.as_number(); - - if !keylen.is_integer() { - return Err(global_this.throw_range_error( - keylen_num, - bun_jsc::RangeErrorOptions { - field_name: b"keylen", - msg: b"an integer", - ..Default::default() - }, - )); - } - - if keylen_num < 0.0 || keylen_num > i32::MAX as f64 { - return Err(global_this.throw_range_error( - keylen_num, - bun_jsc::RangeErrorOptions { - field_name: b"keylen", - min: 0, - max: i32::MAX as i64, - ..Default::default() - }, - )); - } - - // 0..=i32::MAX was checked above. - let keylen = keylen_num as usize; - - if !iterations.is_number() { - return Err(global_this.throw_invalid_argument_type_value( - b"iterations", - b"number", - iterations, - )); - } - - let iterations_num = iterations.as_number(); - - if !iterations.is_integer() { - return Err(global_this.throw_range_error( - iterations_num, - bun_jsc::RangeErrorOptions { - field_name: b"iterations", - msg: b"an integer", - ..Default::default() - }, - )); - } - - if iterations_num < 1.0 || iterations_num > i32::MAX as f64 { - return Err(global_this.throw_range_error( - iterations_num, - bun_jsc::RangeErrorOptions { - field_name: b"iterations", - min: 1, - max: i32::MAX as i64, - ..Default::default() - }, - )); - } - - let iteration_count: i64 = iterations_num as i64; - - let algorithm = 'brk: { - if !digest.is_string() { - return Err( - global_this.throw_invalid_argument_type_value(b"digest", b"string", digest) - ); - } - - 'invalid: { - let slice = digest.to_utf8(global_this)?; - match evp::lookup_ignore_case(slice.slice()) { - Some(alg) => match alg { - Algorithm::Shake128 | Algorithm::Shake256 => break 'invalid, - other if other.md().is_none() => break 'invalid, - other => break 'brk other, - }, - None => break 'invalid, - } - } - - let slice = digest.to_utf8(global_this)?; - let name = slice.slice(); - return Err(global_this - .err( - bun_jsc::ErrorCode::CRYPTO_INVALID_DIGEST, - format_args!("Invalid digest: {}", bstr::BStr::new(name)), - ) - .throw()); - }; - - let mut out = PBKDF2 { - password: StringOrBuffer::default(), - salt: StringOrBuffer::default(), - iteration_count: u32::try_from(iteration_count).expect("int cast"), - length: keylen, - algorithm, - }; - out.salt = match StringOrBuffer::from_js(global_this, salt)? { - Some(v) => v, - None => { - return Err(global_this.throw_invalid_argument_type_value( - b"salt", - b"string or buffer", - salt, - )); - } - }; - - if out.salt.slice().len() > i32::MAX as usize { - return Err(global_this.throw_invalid_arguments(format_args!("salt is too long"))); - } - - out.password = match StringOrBuffer::from_js(global_this, password)? { - Some(v) => v, - None => { - return Err(global_this.throw_invalid_argument_type_value( - b"password", - b"string or buffer", - password, - )); - } - }; - - if out.password.slice().len() > i32::MAX as usize { - return Err(global_this.throw_invalid_arguments(format_args!("password is too long"))); - } - - let callback = match flavor { - Flavor::Async => { - if !callback.is_function() { - return Err(global_this.throw_invalid_argument_type_value( - b"callback", - b"function", - callback, - )); - } - callback - } - Flavor::Sync => JSValue::UNDEFINED, - }; - - // Re-read the salt because a String-object password's `toString` can resize or detach it. - match flavor { - Flavor::Sync => { - if let StringOrBuffer::Buffer(buffer) = &mut out.salt { - buffer.buffer = ArrayBuffer::from_typed_array(global_this, buffer.buffer.value); - } - } - Flavor::Async => { - out.salt.make_thread_isolated_copy(global_this)?; - out.password.make_thread_isolated_copy(global_this)?; - } - } - - Ok((out, callback)) - } - - /// [`from_js`](Self::from_js) for the work-pool job, with its validated callback. - pub(crate) fn from_js_async( - global_this: &JSGlobalObject, - password: JSValue, - salt: JSValue, - iterations: JSValue, - keylen: JSValue, - digest: JSValue, - callback: JSValue, - ) -> JsResult<(ThreadIsolated, JSValue)> { - let (data, callback) = Self::from_js( - global_this, - password, - salt, - iterations, - keylen, - digest, - callback, - Flavor::Async, - )?; - // SAFETY: `Flavor::Async` copied the buffers and thread-isolated the strings. - Ok((unsafe { ThreadIsolated::new(data) }, callback)) - } } /// `crypto.pbkdf2` off the JS thread. pub(crate) struct Pbkdf2Job { - pub pbkdf2: ThreadIsolated, + pub pbkdf2: PBKDF2, + pub password: ThreadIsolated>, + pub salt: ThreadIsolated>, pub output: Vec, pub err: bool, } @@ -288,7 +117,10 @@ impl JobContext for Pbkdf2Job { buf.resize(len, 0); this.output = buf; - if !this.pbkdf2.run(&mut this.output) { + if !this + .pbkdf2 + .run(this.password.slice(), this.salt.slice(), &mut this.output) + { this.err = true; boringssl::ERR_clear_error(); this.output = Vec::new(); @@ -337,16 +169,20 @@ impl JobContext for Pbkdf2Job { } } -/// Schedule the derivation on the work pool; `callback` was validated by `from_js_async`. +/// Schedule the derivation on the work pool. pub(crate) fn create_job( cx: &bun_jsc::JsThread<'_>, - data: ThreadIsolated, + data: PBKDF2, + password: ThreadIsolated>, + salt: ThreadIsolated>, callback: JSValue, ) { Job::::schedule( cx, Pbkdf2Job { pbkdf2: data, + password, + salt, output: Vec::new(), err: false, }, diff --git a/src/runtime/dns_jsc/cares_jsc.bind.ts b/src/runtime/dns_jsc/cares_jsc.bind.ts index 08e7532d7883..1b0019b42017 100644 --- a/src/runtime/dns_jsc/cares_jsc.bind.ts +++ b/src/runtime/dns_jsc/cares_jsc.bind.ts @@ -2,4 +2,10 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::dns_jsc::cares_jsc" }); -export const canonicalizeIP = b.fn({ args: { address: b.any }, returns: b.any, capability: "js", throws: true }); +/** An address that is already canonical is returned as it was given. */ +export const canonicalizeIP = b.fn({ + args: { address: b.string.loose.utf8.withValue }, + returns: b.any, + capability: "js", + throws: true, +}); diff --git a/src/runtime/dns_jsc/cares_jsc.rs b/src/runtime/dns_jsc/cares_jsc.rs index 02e1041114b1..e4be8c3d19f2 100644 --- a/src/runtime/dns_jsc/cares_jsc.rs +++ b/src/runtime/dns_jsc/cares_jsc.rs @@ -850,13 +850,13 @@ pub(crate) fn not_a_hostname_error(global_this: &JSGlobalObject, hostname: &[u8] .to_error_instance(global_this) } -pub(crate) fn canonicalize_ip<'a>(cx: &mut JsScope<'a>, address: JsLocal<'a>) -> JsResult { +pub(crate) fn canonicalize_ip<'a>( + cx: &mut JsScope<'a>, + (addr_str, address): (&[u8], JsLocal<'a>), +) -> JsResult { bun_jsc::mark_binding!(); let global_this = cx.global(); - let addr_arg = address.to_js().to_utf8(global_this)?; - let addr_str = addr_arg.slice(); - let mut ip_addr = [0u8; bun_boringssl::INET6_ADDRSTRLEN + 1]; let Some(slice) = bun_boringssl::canonicalize_ip(addr_str, &mut ip_addr) else { return Ok(JSValue::UNDEFINED); diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index cd67686b1c00..8cacd539656c 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -37,7 +37,7 @@ use bun_wyhash::hash as wyhash; use super::cares_jsc::error_to_deferred; use crate::generated_bindgen::dns::LookupOptions; -use crate::generated_bindgen::dns_internals::RecordTypeArgument; +use crate::generated_bindgen::dns_internals::{RecordTypeArgument, ResolverOptions}; use crate::socket::socket_address::inet::INET6_ADDRSTRLEN; use crate::timer::{ElTimespec, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag}; use bun_cares_sys::c_ares_draft as c_ares; @@ -2815,6 +2815,8 @@ pub(crate) mod internal { } } + const MAX_ADDRESSES_FOR_TESTING: usize = 64; + /// The `addresses: string[]` argument of the testing hooks below, as sockaddrs. fn addresses_for_testing( global: &JSGlobalObject, @@ -2826,7 +2828,7 @@ pub(crate) mod internal { ); } let len = addresses.get_length(global)? as usize; - if len > 64 { + if len > MAX_ADDRESSES_FOR_TESTING { return Err(global .throw_invalid_arguments(format_args!("addresses must have at most 64 entries"))); } @@ -2844,8 +2846,8 @@ pub(crate) mod internal { } /// `bun:internal-for-testing`: [`is_localhost_name`] for one hostname. - pub(crate) fn is_localhost_name_for_testing(hostname: &bun::String) -> bool { - is_localhost_name(hostname.to_utf8().slice()) + pub(crate) fn is_localhost_name_for_testing(hostname: &[u8]) -> bool { + is_localhost_name(hostname) } /// `bun:internal-for-testing`: [`is_all_loopback_of_one_family`] over `addresses` laid out as a getaddrinfo() answer. @@ -2864,12 +2866,11 @@ pub(crate) mod internal { /// storing the result, so a real `fetch()` / `Bun.connect()` consumes it. pub(crate) fn seed_cache_for_testing<'a>( cx: &mut JsScope<'a>, - hostname: &bun::String, + hostname: &[u8], addresses: JsLocal<'a>, - ) -> JsResult { + ) -> JsResult>> { let global = cx.global(); - let hostname_slice = hostname.to_utf8(); - let hostname_z = bun::ZBox::from_bytes(hostname_slice.slice()); + let hostname_z = bun::ZBox::from_bytes(hostname); let mut addrs = addresses_for_testing(global, addresses.to_js())?; if addrs.is_empty() { return Err(global.throw_invalid_arguments(format_args!("addresses must not be empty"))); @@ -2877,15 +2878,15 @@ pub(crate) mod internal { let mut chain = addrinfo_chain(&mut addrs); let results = process_results(chain.as_mut_ptr()); - let out = JSValue::create_empty_array(global, results.len())?; - for (i, entry) in (0u32..).zip(results.iter()) { - let fam: i32 = if entry.info.ai_family == netc::AF_INET6 { + let mut families = [0i32; MAX_ADDRESSES_FOR_TESTING]; + for (family, entry) in families.iter_mut().zip(results.iter()) { + *family = if entry.info.ai_family == netc::AF_INET6 { 6 } else { 4 }; - out.put_index(global, i, JSValue::js_number_from_int32(fam))?; } + let families = families.into_iter().take(results.len()); let key = RequestKey::init(Some(hostname_z.as_zstr()), 0); let req = Request::new(key.to_owned(), 0, GlobalCache::get_cache_timestamp()); @@ -2907,23 +2908,21 @@ pub(crate) mod internal { } DNS_CACHE_SIZE.store(guard.len, Ordering::Relaxed); drop(guard); - Ok(out) + Ok(families) } /// `bun:internal-for-testing`: the error a lookup of `hostname` reports /// when getaddrinfo(3) returns the `EAI_*` status named `code`. pub(crate) fn getaddrinfo_error_for_testing( cx: &mut JsScope<'_>, - code: &bun::String, - hostname: &bun::String, + code: &[u8], + hostname: &[u8], ) -> JsResult { let global = cx.global(); - let code = code.to_utf8(); - let hostname = hostname.to_utf8(); - let Some(rc) = c_ares::Error::eai_raw_by_name(code.slice()) else { + let Some(rc) = c_ares::Error::eai_raw_by_name(code) else { return Err(global.throw_invalid_arguments(format_args!( "unknown getaddrinfo status name: {}", - bstr::BStr::new(code.slice()) + bstr::BStr::new(code) ))); }; match c_ares::Error::init_eai(rc) { @@ -2931,7 +2930,7 @@ pub(crate) mod internal { err, global, b"getaddrinfo", - hostname.slice(), + hostname, ), None => Ok(JSValue::UNDEFINED), } @@ -5738,10 +5737,7 @@ impl Resolver { ) } - pub(crate) fn new_resolver<'a>( - cx: &mut JsScope<'a>, - options: JsLocal<'a>, - ) -> JsResult { + pub(crate) fn new_resolver(cx: &mut JsScope<'_>, options: &ResolverOptions) -> JSValue { let global_this = cx.global(); // SAFETY: bun_vm() returns a live VM pointer for the duration of the call. let resolver = Resolver::init( @@ -5749,24 +5745,17 @@ impl Resolver { Some(global_this.bun_vm().context_of_caller_no_frame().id()), ); - let options = options.to_js(); - if options.is_object() { - // SAFETY: `resolver` is the heap allocation from `init`; not yet - // wrapped in JS so exclusively owned here. - let opts_cell = unsafe { &(*resolver).options }; - let mut opts = opts_cell.get(); - if let Some(timeout) = options.get_truthy(global_this, "timeout")? { - opts.timeout = Some(timeout.coerce_to_i32(global_this)?); - } - if let Some(tries) = options.get_truthy(global_this, "tries")? { - opts.tries = Some(tries.coerce_to_i32(global_this)?); - } - opts_cell.set(opts); - } + // SAFETY: `resolver` is the heap allocation from `init`; not yet + // wrapped in JS so exclusively owned here. + let opts_cell = unsafe { &(*resolver).options }; + let mut opts = opts_cell.get(); + opts.timeout = Some(options.timeout); + opts.tries = Some(options.tries); + opts_cell.set(opts); // SAFETY: `resolver` was `heap::alloc`'d in `Resolver::init`; ownership // transfers to the JS wrapper, which holds one ref. - Ok(unsafe { Resolver::to_js_ptr(resolver, global_this) }) + unsafe { Resolver::to_js_ptr(resolver, global_this) } } pub(crate) fn cancel(&self, cx: &mut JsScope<'_>) -> JsResult<()> { diff --git a/src/runtime/dns_jsc/dns_internals.bind.ts b/src/runtime/dns_jsc/dns_internals.bind.ts index 3f195ad82154..8e49f0149a72 100644 --- a/src/runtime/dns_jsc/dns_internals.bind.ts +++ b/src/runtime/dns_jsc/dns_internals.bind.ts @@ -50,7 +50,9 @@ export const DNSResolver = b.class({ }, }); -export const newResolver = b.fn({ ...method, args: { options: b.any }, returns: b.any }); +/** What `validateResolverOptions()` of `node:dns` returns. */ +export const ResolverOptions = b.dictionary({ timeout: b.i32, tries: b.i32 }); +export const newResolver = b.fn({ capability: "js", args: { options: ResolverOptions }, returns: DNSResolver }); // The undocumented `Bun.dns.*` functions: the same queries on the VM's global resolver. export const getGlobalServers = b.fn(getServers); @@ -78,12 +80,12 @@ export const globalLookupService = b.fn({ // `bun:internal-for-testing` export const seedCacheForTesting = b.fn({ ...method, - args: { hostname: b.string.allowStringObject, addresses: b.any }, - returns: b.any, + args: { hostname: b.string.utf8, addresses: b.any }, + returns: b.array(b.i32), rustPath: "crate::dns_jsc::internal::seed_cache_for_testing", }); export const isLocalhostNameForTesting = b.fn({ - args: { hostname: b.string.allowStringObject }, + args: { hostname: b.string.utf8 }, returns: b.bool, rustPath: "crate::dns_jsc::internal::is_localhost_name_for_testing", }); @@ -95,7 +97,7 @@ export const isAllLoopbackOfOneFamilyForTesting = b.fn({ }); export const getaddrinfoErrorForTesting = b.fn({ ...method, - args: { code: b.string.allowStringObject, hostname: b.string.allowStringObject }, + args: { code: b.string.utf8, hostname: b.string.utf8 }, returns: b.any, rustPath: "crate::dns_jsc::internal::getaddrinfo_error_for_testing", }); diff --git a/src/runtime/ffi/FFIObject.bind.ts b/src/runtime/ffi/FFIObject.bind.ts index 9029ec078761..a8db6f6d8c85 100644 --- a/src/runtime/ffi/FFIObject.bind.ts +++ b/src/runtime/ffi/FFIObject.bind.ts @@ -2,8 +2,19 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::ffi::ffi_object_draft" }); -/** `(ptr, byteOffset?, byteLength?, deallocatorContext?, deallocator?)`, or with four arguments the last is the deallocator. */ -const view = { args: { ptr: b.any, rest: b.any.variadic }, returns: b.any, capability: "js", throws: true } as const; +/** With four arguments the last is the deallocator. */ +const view = { + args: { + ptr: b.any, + byteOffset: b.any.ifPassed, + byteLength: b.any.ifPassed, + deallocatorContext: b.any.ifPassed, + deallocator: b.any.ifPassed, + }, + returns: b.any, + capability: "js", + throws: true, +} as const; export const toBuffer = b.fn({ ...view, export: "Bun__FFIObject__toBuffer" }); export const toArrayBuffer = b.fn({ ...view, export: "Bun__FFIObject__toArrayBuffer" }); diff --git a/src/runtime/ffi/FFIObject.rs b/src/runtime/ffi/FFIObject.rs index 0dc746a1ca94..1e6a2b63e03f 100644 --- a/src/runtime/ffi/FFIObject.rs +++ b/src/runtime/ffi/FFIObject.rs @@ -621,24 +621,27 @@ fn get_cptr(value: JSValue) -> Option { pub(crate) fn to_array_buffer<'a>( cx: &mut JsScope<'a>, - value: JsLocal<'a>, - rest: &'a [JsLocal<'a>], + ptr: JsLocal<'a>, + byte_offset: Option>, + byte_length: Option>, + deallocator_context: Option>, + deallocator: Option>, ) -> JsResult { let global_this = cx.global(); - let mut rest = JsLocal::to_js_slice(rest).iter().copied(); - let byte_offset = rest.next(); - let value_length = rest.next(); - let finalization_ctx_or_ptr = rest.next(); - let finalization_callback = rest.next(); - let (ptr, len) = get_ptr_slice(global_this, value.to_js(), byte_offset, value_length)?; + let (ptr, len) = get_ptr_slice( + global_this, + ptr.to_js(), + byte_offset.map(JsLocal::to_js), + byte_length.map(JsLocal::to_js), + )?; let mut callback: jsc::JSTypedArrayBytesDeallocator = None; let mut ctx: Option<*mut c_void> = None; - if let Some(callback_value) = finalization_callback { + if let Some(callback_value) = deallocator.map(JsLocal::to_js) { if let Some(callback_ptr) = get_cptr(callback_value) { // SAFETY: user-supplied raw fn pointer address. callback = unsafe { deallocator_from_addr(callback_ptr) }; - if let Some(ctx_value) = finalization_ctx_or_ptr { + if let Some(ctx_value) = deallocator_context.map(JsLocal::to_js) { if let Some(ctx_ptr) = get_cptr(ctx_value) { ctx = Some(ctx_ptr as *mut c_void); } else if !ctx_value.is_undefined_or_null() { @@ -652,7 +655,7 @@ pub(crate) fn to_array_buffer<'a>( "Expected callback to be a C pointer (number or BigInt)" ))); } - } else if let Some(callback_value) = finalization_ctx_or_ptr { + } else if let Some(callback_value) = deallocator_context.map(JsLocal::to_js) { if let Some(callback_ptr) = get_cptr(callback_value) { // SAFETY: user-supplied raw fn pointer address. callback = unsafe { deallocator_from_addr(callback_ptr) }; @@ -680,24 +683,27 @@ pub(crate) fn to_array_buffer<'a>( pub(crate) fn to_buffer<'a>( cx: &mut JsScope<'a>, - value: JsLocal<'a>, - rest: &'a [JsLocal<'a>], + ptr: JsLocal<'a>, + byte_offset: Option>, + byte_length: Option>, + deallocator_context: Option>, + deallocator: Option>, ) -> JsResult { let global_this = cx.global(); - let mut rest = JsLocal::to_js_slice(rest).iter().copied(); - let byte_offset = rest.next(); - let value_length = rest.next(); - let finalization_ctx_or_ptr = rest.next(); - let finalization_callback = rest.next(); - let (ptr, len) = get_ptr_slice(global_this, value.to_js(), byte_offset, value_length)?; + let (ptr, len) = get_ptr_slice( + global_this, + ptr.to_js(), + byte_offset.map(JsLocal::to_js), + byte_length.map(JsLocal::to_js), + )?; let mut callback: jsc::JSTypedArrayBytesDeallocator = None; let mut ctx: Option<*mut c_void> = None; - if let Some(callback_value) = finalization_callback { + if let Some(callback_value) = deallocator.map(JsLocal::to_js) { if let Some(callback_ptr) = get_cptr(callback_value) { // SAFETY: user-supplied raw fn pointer address. callback = unsafe { deallocator_from_addr(callback_ptr) }; - if let Some(ctx_value) = finalization_ctx_or_ptr { + if let Some(ctx_value) = deallocator_context.map(JsLocal::to_js) { if let Some(ctx_ptr) = get_cptr(ctx_value) { ctx = Some(ctx_ptr as *mut c_void); } else if !ctx_value.is_empty_or_undefined_or_null() { @@ -711,7 +717,7 @@ pub(crate) fn to_buffer<'a>( "Expected callback to be a C pointer (number or BigInt)" ))); } - } else if let Some(callback_value) = finalization_ctx_or_ptr { + } else if let Some(callback_value) = deallocator_context.map(JsLocal::to_js) { if let Some(callback_ptr) = get_cptr(callback_value) { // SAFETY: user-supplied raw fn pointer address. callback = unsafe { deallocator_from_addr(callback_ptr) }; diff --git a/src/runtime/ffi/ffi.bind.ts b/src/runtime/ffi/ffi.bind.ts index 530b5729abe3..7587a880e92b 100644 --- a/src/runtime/ffi/ffi.bind.ts +++ b/src/runtime/ffi/ffi.bind.ts @@ -19,10 +19,21 @@ export const viewSource = b.fn({ export: "Bun__FFI__viewSource", args: { symbols: b.any, isCallback: b.bool.loose }, }); -export const dlopen = b.fn({ ...method, export: "Bun__FFI__dlopen", args: { name: b.any, symbols: b.any } }); +/** Without a `name` it throws. */ +export const dlopen = b.fn({ + ...method, + export: "Bun__FFI__dlopen", + args: { name: b.string.loose.utf8.nullable, symbols: b.any }, +}); export const callback = b.fn({ ...method, export: "Bun__FFI__callback", args: { options: b.any, callback: b.any } }); export const linkSymbols = b.fn({ ...method, export: "Bun__FFI__linkSymbols", args: { symbols: b.any } }); export const closeCallback = b.fn({ export: "Bun__FFI__closeCallback", args: { callback: b.any } }); -export const cfunction = b.fn({ ...method, export: "Bun__FFI__cfunction", args: { options: b.any, name: b.any } }); +/** What is not a string is ignored. */ +export const FunctionName = b.union({ name: b.string.allowStringObject, ignored: b.any }); +export const cfunction = b.fn({ + ...method, + export: "Bun__FFI__cfunction", + args: { options: b.any, name: FunctionName }, +}); -export const cc = b.fn({ ...method, args: { options: b.any } }); +export const cc = b.fn({ ...method, args: { options: b.any }, returns: FFI }); diff --git a/src/runtime/ffi/ffi_body.rs b/src/runtime/ffi/ffi_body.rs index 203e260e01df..886dde5cac6f 100644 --- a/src/runtime/ffi/ffi_body.rs +++ b/src/runtime/ffi/ffi_body.rs @@ -23,7 +23,7 @@ use bun_jsc::{ use bun_paths as path; use bun_resolver::fs as Fs; -use crate::generated_bindgen::ffi::js_FFI as js; +use crate::generated_bindgen::ffi::{FunctionName, js_FFI as js}; // ─── Local shims for upstream surfaces not yet wired (Phase D) ─────────────── @@ -1240,12 +1240,12 @@ impl FFI { pub(crate) fn callback<'a>( cx: &mut JsScope<'a>, - interface: JsLocal<'a>, - js_callback: JsLocal<'a>, + options: JsLocal<'a>, + callback: JsLocal<'a>, ) -> JsResult { let global_this = cx.global(); - let interface = interface.to_js(); - let js_callback = js_callback.to_js(); + let interface = options.to_js(); + let js_callback = callback.to_js(); jsc::mark_binding(); if !interface.is_object() { return Ok(global_this.to_invalid_arguments(format_args!("Expected object"))); @@ -1359,11 +1359,11 @@ impl FFI { pub(crate) fn view_source<'a>( cx: &mut JsScope<'a>, - object: JsLocal<'a>, + symbols: JsLocal<'a>, is_callback: bool, ) -> JsResult { let global = cx.global(); - let object = object.to_js(); + let object = symbols.to_js(); if is_callback { return Self::print_callback(global, object); } @@ -1412,19 +1412,16 @@ fn invalid_options_arg(global: &JSGlobalObject) -> JSValue { impl FFI { pub(crate) fn dlopen<'a>( cx: &mut JsScope<'a>, - name: JsLocal<'a>, - object_value: JsLocal<'a>, + name: Option<&[u8]>, + symbols: JsLocal<'a>, ) -> JsResult { let global = cx.global(); - let name = name.to_js(); - if name.is_undefined_or_null() { + let Some(name_slice) = name else { return Err(global.throw_invalid_arguments(format_args!("Expected string"))); - } - let name_str = name.to_js_string(global)?.view(global)?; - let object_value = object_value.to_js(); + }; + let object_value = symbols.to_js(); jsc::mark_binding(); let vm = jsc::VirtualMachineRef::get(); - let name_slice = name_str.to_utf8(); if object_value.is_empty_or_undefined_or_null() { return Ok(invalid_options_arg(global)); @@ -1454,7 +1451,7 @@ impl FFI { // `crate::jsc_hooks` — same crate, so a direct call. let _ = vm; if let Some(len) = crate::jsc_hooks::resolve_embedded_file_to_buf( - name_slice.slice(), + name_slice, ext, &mut filepath_buf[..], ) { @@ -1466,7 +1463,7 @@ impl FFI { break 'brk &filepath_buf[0..len]; } - break 'brk name_slice.slice(); + break 'brk name_slice; }; if name.is_empty() { @@ -1592,10 +1589,10 @@ impl FFI { pub(crate) fn link_symbols<'a>( cx: &mut JsScope<'a>, - object_value: JsLocal<'a>, + symbols: JsLocal<'a>, ) -> JsResult { let global = cx.global(); - let object_value = object_value.to_js(); + let object_value = symbols.to_js(); jsc::mark_binding(); if object_value.is_empty_or_undefined_or_null() { @@ -1674,11 +1671,10 @@ impl FFI { pub(crate) fn cfunction<'a>( cx: &mut JsScope<'a>, options: JsLocal<'a>, - name_value: JsLocal<'a>, + name: FunctionName<'a>, ) -> JsResult { let global = cx.global(); let options = options.to_js(); - let name_value = name_value.to_js(); jsc::mark_binding(); if options.is_empty_or_undefined_or_null() || !options.is_object() { @@ -1696,10 +1692,9 @@ impl FFI { ))); }; - let name = if name_value.is_string() { - name_value.to_bun_string(global)? - } else { - bun_core::String::static_("CFunction") + let name = match name { + FunctionName::Name(name) => name, + FunctionName::Ignored(_) => bun_core::String::static_("CFunction"), }; if let Some(err) = function.reject_napi_types_error(global) { return Ok(err); diff --git a/src/runtime/ipc.bind.ts b/src/runtime/ipc.bind.ts index b845463105c4..0bed14d3606b 100644 --- a/src/runtime/ipc.bind.ts +++ b/src/runtime/ipc.bind.ts @@ -2,9 +2,10 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::ipc" }); +export const SentHandle = b.union({ object: b.object, ignored: b.any }); export const closeSentHandle = b.fn({ export: "Bun__IPC__closeSentHandle", - args: { handle: b.any }, + args: { handle: SentHandle }, capability: "js", throws: true, }); diff --git a/src/runtime/ipc.rs b/src/runtime/ipc.rs index 6a68458775fe..b5d3c1f9064d 100644 --- a/src/runtime/ipc.rs +++ b/src/runtime/ipc.rs @@ -5,6 +5,7 @@ use core::mem::size_of; use bun_jsc::JsCell; use bun_ptr::RefPtr; +use crate::generated_bindgen::ipc::SentHandle; use crate::json_line_buffer::JSONLineBuffer; use bun_collections::{ByteVecExt, VecExt}; use bun_core::{Output, handle_oom}; @@ -12,7 +13,7 @@ use bun_core::{String as BunString, strings}; use bun_io::KeepAlive; use bun_io::StreamBuffer; use bun_jsc as jsc; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::JsScope; use bun_jsc::js_value::Protected; #[cfg(windows)] use bun_jsc::virtual_machine::VirtualMachine; @@ -829,10 +830,10 @@ impl SendHandle { } } -pub(crate) fn close_sent_handle<'a>(cx: &mut JsScope<'a>, handle: JsLocal<'a>) -> JsResult<()> { +pub(crate) fn close_sent_handle<'a>(cx: &mut JsScope<'a>, handle: SentHandle<'a>) -> JsResult<()> { let global = cx.global(); - let js = handle.to_js(); - if js.is_object() { + if let SentHandle::Object(handle) = handle { + let js = handle.to_js(); if let Some(f) = js.get(global, "close")? { if f.is_callable() { f.call(global, js, &[])?; diff --git a/src/runtime/ipc_host.bind.ts b/src/runtime/ipc_host.bind.ts index 9b82ad148369..7aa8c718e347 100644 --- a/src/runtime/ipc_host.bind.ts +++ b/src/runtime/ipc_host.bind.ts @@ -1,4 +1,5 @@ import * as b from "../codegen/bindgen-lib.ts"; +import { Subprocess } from "./api/bun/subprocess.bind.ts"; export default b.module({ rustPath: "crate::ipc_host" }); @@ -8,8 +9,17 @@ export const emitProcessErrorEvent = b.fn({ capability: "js", }); +/** Only `null` is this process. */ +export const MessageTarget = b.union({ + subprocess: Subprocess, + absent: b.undefined, + thisProcess: b.null, + ignored: b.any, +}); +/** What the other process sent: any value. */ +export const ReceivedMessage = b.union({ object: b.object, primitive: b.any }); export const emitHandleIPCMessage = b.fn({ - args: { target: b.any, message: b.any, handle: b.any }, + args: { target: MessageTarget, message: ReceivedMessage, handle: b.any }, capability: "js", throws: true, }); diff --git a/src/runtime/ipc_host.rs b/src/runtime/ipc_host.rs index d2a2c6b0056d..37d8fd06d7cb 100644 --- a/src/runtime/ipc_host.rs +++ b/src/runtime/ipc_host.rs @@ -12,7 +12,7 @@ use bun_jsc::bun_string_jsc; use bun_jsc::{JSGlobalObject, JSValue, JsClass, JsResult}; use bun_ptr::RefPtr; -use crate::api::bun::subprocess::Subprocess; +use crate::generated_bindgen::ipc_host::{MessageTarget, ReceivedMessage}; use crate::socket::Listener; bun_core::define_scoped_log!(log, IPC, visible); @@ -296,50 +296,50 @@ pub(crate) fn do_send( pub(crate) fn emit_handle_ipc_message<'a>( cx: &mut JsScope<'a>, - target: JsLocal<'a>, - message: JsLocal<'a>, + target: MessageTarget<'a>, + message: ReceivedMessage<'a>, handle: JsLocal<'a>, ) -> JsResult<()> { let global_this = cx.global(); - let [target, message, handle] = [target.to_js(), message.to_js(), handle.to_js()]; - if target.is_null() { - // Cluster-internal replies that carried a descriptor (shared dgram - // sockets) are marked with cmd: "NODE_CLUSTER"; hand them straight to - // the cluster's internal-message dispatcher instead of emitting a - // process 'message' event, mirroring Node's NODE_-prefix routing. - if message.is_object() { - if let Some(cmd) = message.get(global_this, "cmd")? { - if cmd.is_string() { - let cmd_str = cmd.to_bun_string(global_this)?; - if cmd_str.eq_ascii(b"NODE_CLUSTER") { - crate::node::node_cluster_binding::handle_internal_message_child( - global_this, - message, - handle, - )?; - return Ok(()); + let handle = handle.to_js(); + let (message, is_object) = match message { + ReceivedMessage::Object(object) => (object.to_js(), true), + ReceivedMessage::Primitive(primitive) => (primitive.to_js(), false), + }; + match target { + MessageTarget::Subprocess(subprocess) => { + subprocess.handle_ipc_message(&DecodedIPCMessage::Data(message), handle)?; + } + MessageTarget::Absent | MessageTarget::Ignored(_) => {} + MessageTarget::ThisProcess => { + // Cluster-internal replies that carried a descriptor (shared dgram + // sockets) are marked with cmd: "NODE_CLUSTER"; hand them straight to + // the cluster's internal-message dispatcher instead of emitting a + // process 'message' event, mirroring Node's NODE_-prefix routing. + if is_object { + if let Some(cmd) = message.get(global_this, "cmd")? { + if cmd.is_string() { + let cmd_str = cmd.to_bun_string(global_this)?; + if cmd_str.eq_ascii(b"NODE_CLUSTER") { + crate::node::node_cluster_binding::handle_internal_message_child( + global_this, + message, + handle, + )?; + return Ok(()); + } } } } + let vm = global_this.bun_vm().as_mut(); + let Some(ipc) = get_ipc_instance(vm) else { + // Channel already gone: a handle that finished adopting after EOF is still delivered, as in node. + Process__emitMessageEvent(global_this, message, handle); + return Ok(()); + }; + // SAFETY: `get_ipc_instance` returns the live boxed IPCInstance. + unsafe { (*ipc).handle_ipc_message(&DecodedIPCMessage::Data(message), handle) }?; } - let vm = global_this.bun_vm().as_mut(); - let Some(ipc) = get_ipc_instance(vm) else { - // Channel already gone: a handle that finished adopting after EOF is still delivered, as in node. - Process__emitMessageEvent(global_this, message, handle); - return Ok(()); - }; - // SAFETY: `get_ipc_instance` returns the live boxed IPCInstance. - unsafe { (*ipc).handle_ipc_message(&DecodedIPCMessage::Data(message), handle) }?; - } else { - if !target.is_cell() { - return Ok(()); - } - let Some(subprocess) = Subprocess::from_js_direct(target) else { - return Ok(()); - }; - // SAFETY: `from_js_direct` returned a non-null `*mut Subprocess`; the JS - // wrapper holds it alive for the call. - unsafe { (*subprocess).handle_ipc_message(&DecodedIPCMessage::Data(message), handle) }?; } Ok(()) } diff --git a/src/runtime/linear_fifo_testing.bind.ts b/src/runtime/linear_fifo_testing.bind.ts index 04ba75e92193..282ef78eefd5 100644 --- a/src/runtime/linear_fifo_testing.bind.ts +++ b/src/runtime/linear_fifo_testing.bind.ts @@ -2,4 +2,4 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::linear_fifo_testing" }); -export const orderedRemoveProbe = b.fn({ args: { scenario: b.any }, returns: b.array(b.i32) }); +export const orderedRemoveProbe = b.fn({ args: { scenario: b.i32 }, returns: b.array(b.i32) }); diff --git a/src/runtime/linear_fifo_testing.rs b/src/runtime/linear_fifo_testing.rs index 2fa7cd522da5..aca50ef51536 100644 --- a/src/runtime/linear_fifo_testing.rs +++ b/src/runtime/linear_fifo_testing.rs @@ -14,7 +14,6 @@ //! `bun_jsc`. use bun_collections::linear_fifo::{LinearFifo, StaticBuffer}; -use bun_jsc::bindgen::JsLocal; /// A 16-slot static buffer matches the real `weak_refs` FIFO in the dev /// server's source-map store and makes `POWERS_OF_TWO` true. @@ -30,9 +29,7 @@ type ProbeFifo = LinearFifo>; /// write 12, read 12, write 8 → head=12 count=8, remove offset 5. /// /// Any other scenario value returns an empty array. -pub(crate) fn ordered_remove_probe(scenario: JsLocal<'_>) -> impl Iterator { - let scenario = scenario.to_js().to_int32(); - +pub(crate) fn ordered_remove_probe(scenario: i32) -> impl Iterator { let mut fifo = ProbeFifo::init(); match scenario { 0 => { diff --git a/src/runtime/node.rs b/src/runtime/node.rs index 1db18e9f64c2..8ed8caa5bce8 100644 --- a/src/runtime/node.rs +++ b/src/runtime/node.rs @@ -16,8 +16,8 @@ pub(crate) mod assert { #[path = "node/types.rs"] pub(crate) mod types; pub(crate) use types::{ - BlobOrStringOrBuffer, Dirent, Encoding, FileBlobs, Flavor, PathLike, PathOrBlob, - PathOrFileDescriptor, StringObjects, StringOrBuffer, ThreadIsolated, ThreadIsolatedArg, + BlobOrStringOrBuffer, Dirent, Encoding, FileBlobs, PathLike, PathOrBlob, PathOrFileDescriptor, + StringObjects, StringOrBuffer, ThreadIsolated, }; #[path = "node/path.rs"] diff --git a/src/runtime/node/Stat.bind.ts b/src/runtime/node/Stat.bind.ts index a2b70784e1d1..6ffad90680af 100644 --- a/src/runtime/node/Stat.bind.ts +++ b/src/runtime/node/Stat.bind.ts @@ -2,8 +2,10 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::node::stat" }); +/** A number wraps as `JSValue::to_uint64_no_truncate` has it. */ +export const Ino = b.union({ number: b.f64, big: b.u64.allowBigInt }); export const createStatsForIno = b.fn({ - args: { ino: b.any, big: b.bool.loose }, + args: { ino: Ino, big: b.bool.loose }, returns: b.any, capability: "js", throws: true, diff --git a/src/runtime/node/Stat.rs b/src/runtime/node/Stat.rs index fd102bc82b2c..cf7ce021bce6 100644 --- a/src/runtime/node/Stat.rs +++ b/src/runtime/node/Stat.rs @@ -1,9 +1,11 @@ //! Stats and BigIntStats classes from node:fs use bun_core::Timespec; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::JsScope; use bun_jsc::{JSGlobalObject, JSValue, JsResult}; +use crate::generated_bindgen::stat::Ino; + // `bun.sys.PosixStat` — uv-shaped stat struct. Re-exported from `bun_sys` now // that the crate declares it; `PosixStat::init(&bun_sys::Stat)` handles the // libc-stat → uv_stat_t field copy on both POSIX and Windows there. @@ -172,14 +174,13 @@ pub(crate) type StatsBig = StatType; /// Test-only: build a Stats/BigIntStats from a raw u64 ino via the real /// statToJS path, so regression tests can exercise high-inode values without /// a filesystem that hands them out. -pub(crate) fn create_stats_for_ino<'a>( - cx: &mut JsScope<'a>, - ino: JsLocal<'a>, - big: bool, -) -> JsResult { +pub(crate) fn create_stats_for_ino(cx: &mut JsScope<'_>, ino: Ino, big: bool) -> JsResult { // SAFETY: all-zero is a valid PosixStat (repr(C) POD with no NonNull/NonZero fields). let mut stat_: PosixStat = bun_core::ffi::zeroed(); - stat_.ino = ino.to_js().to_uint64_no_truncate(); + stat_.ino = match ino { + Ino::Number(number) => JSValue::js_number(number).to_uint64_no_truncate(), + Ino::Big(ino) => ino, + }; Stats::init(&stat_, big).to_js_newly_created(cx.global()) } diff --git a/src/runtime/node/memory_pressure.bind.ts b/src/runtime/node/memory_pressure.bind.ts index c9ca93d0596d..ee1143aebeef 100644 --- a/src/runtime/node/memory_pressure.bind.ts +++ b/src/runtime/node/memory_pressure.bind.ts @@ -4,7 +4,7 @@ export default b.module({ rustPath: "crate::node::memory_pressure" }); export const psiTrigger = b.fn({ returns: b.bufferSource.nullable, capability: "heap", throws: true }); export const psiFilter = b.fn({ - args: { armed: b.any, polls: b.any.variadic }, + args: { armed: b.string.loose.utf8, polls: b.any.variadic }, returns: b.any, capability: "js", throws: true, diff --git a/src/runtime/node/memory_pressure.rs b/src/runtime/node/memory_pressure.rs index 36a9ec1f19d0..0840990c1366 100644 --- a/src/runtime/node/memory_pressure.rs +++ b/src/runtime/node/memory_pressure.rs @@ -539,14 +539,13 @@ pub(crate) fn psi_trigger<'a>( /// `memoryPressurePsiFilter(armed, ...polls)`: one `bool` per poll, `null` off Linux. pub(crate) fn psi_filter<'a>( cx: &mut JsScope<'a>, - armed: JsLocal<'a>, + armed: &[u8], polls: &'a [JsLocal<'a>], ) -> JsResult { #[cfg(any(target_os = "linux", target_os = "android"))] { let global = cx.global(); - let mut last_total = - posix::parse_psi_some_total(&armed.to_js().to_utf8(global)?).unwrap_or(0); + let mut last_total = posix::parse_psi_some_total(armed).unwrap_or(0); JSValue::create_array_from_iter(global, polls.iter(), |contents| { let contents = contents.to_js().to_utf8(global)?; Ok(JSValue::js_boolean(posix::psi_growth_reached_threshold( diff --git a/src/runtime/node/node_cluster_binding.bind.ts b/src/runtime/node/node_cluster_binding.bind.ts index d812ae184de8..610de3abe5a8 100644 --- a/src/runtime/node/node_cluster_binding.bind.ts +++ b/src/runtime/node/node_cluster_binding.bind.ts @@ -10,9 +10,16 @@ export const onInternalMessageChild = b.fn({ }); export const setRef = b.fn({ args: { enabled: b.bool } }); export const channelIgnoreOneDisconnectEventListener = b.fn({}); +/** `"udp4"` or `"udp6"`, or 4, 6 or -1 (a path). Node compares it with `===`, so anything else is IPv4. */ +export const BindAddressType = b.union({ udp: b.string.utf8, family: b.f64, ignored: b.any }); +/** Anything but a string is the wildcard address. */ +export const BindAddress = b.union({ text: b.string.utf8, ignored: b.any }); +export const BoundHandle = b.dictionary({ fd: b.u64, port: b.i32 }); +/** A negative errno when nothing was bound. */ +export const BindResult = b.union({ errno: b.i32, handle: BoundHandle }); export const clusterRawBind = b.fn({ - args: { addressType: b.any, address: b.any, port: b.any, flags: b.any }, - returns: b.any, + args: { addressType: BindAddressType, address: BindAddress, port: b.f64, flags: b.i32 }, + returns: BindResult, capability: "js", throws: true, }); @@ -23,8 +30,9 @@ export const clusterCloseHandle = b.fn({ args: { fd: b.f64 } }); export const ProcessHandle = b.union({ subprocess: Subprocess, ignored: b.any }); /** `null` when nothing was sent, `false` when the channel asks the sender to back off. */ export const sendHelperPrimary = b.fn({ - args: { processHandle: ProcessHandle, message: b.any, handle: b.any, callback: b.any }, + args: { processHandle: ProcessHandle, message: b.object, handle: b.object.nullable, callback: b.callable.optional }, returns: b.bool.nullable, + length: 4, capability: "js", throws: true, }); diff --git a/src/runtime/node/node_cluster_binding.rs b/src/runtime/node/node_cluster_binding.rs index d715f6d0ab4d..2703a06d4e9c 100644 --- a/src/runtime/node/node_cluster_binding.rs +++ b/src/runtime/node/node_cluster_binding.rs @@ -5,12 +5,14 @@ // - We should not be creating JSFunction's in process.nextTick. use crate::ipc::{IsInternal, SerializeAndSendResult}; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::{Callable, JsLocal, JsScope}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{JSGlobalObject, JSValue, JsResult, StrongOptional}; +use bun_jsc::{JSGlobalObject, JSObject, JSValue, JsResult, StrongOptional}; use crate::api::bun::subprocess::Subprocess; -use crate::generated_bindgen::node_cluster_binding::ProcessHandle; +use crate::generated_bindgen::node_cluster_binding::{ + BindAddress, BindAddressType, BindResult, BoundHandle, ProcessHandle, +}; // Struct lives in `crate::ipc` — // `SendQueue` stores one inline so it must live at that tier. Re-exported here so @@ -69,9 +71,9 @@ pub(crate) fn handle_internal_message_child( pub(crate) fn send_helper_primary<'a>( cx: &mut JsScope<'a>, process_handle: ProcessHandle<'a>, - message: JsLocal<'a>, - handle: JsLocal<'a>, - callback: JsLocal<'a>, + message: JsLocal<'a, JSObject>, + handle: Option>, + callback: Option>, ) -> JsResult> { bun_output::scoped_log!(IPC, "sendHelperPrimary"); @@ -83,21 +85,14 @@ pub(crate) fn send_helper_primary<'a>( }; let global = cx.global(); let message = message.to_js(); - let handle = handle.to_js(); - let callback = callback.to_js(); let Some(ipc_data) = subprocess.ipc() else { return Ok(None); }; - if message.is_undefined() { - return Err(global.throw_missing_arguments_value(&["message"])); - } - if !message.is_object() { - return Err(global.throw_invalid_argument_type_value("message", "object", message)); - } let mut native_handle: Option = None; - if !handle.is_null() && !handle.is_undefined() { + if let Some(handle) = handle { + let handle = handle.to_js(); let Some(fd_value) = handle.get(global, "fd")? else { return Err(global.throw(format_args!("cluster handle is missing 'fd'"))); }; @@ -145,15 +140,15 @@ pub(crate) fn send_helper_primary<'a>( } let this_seq = ipc_data.internal_msg_queue.with_mut(|q| { let seq = q.seq; - if callback.is_function() { + if let Some(callback) = callback { let _ = q .callbacks - .put(seq, StrongOptional::create(callback, global)); + .put(seq, StrongOptional::create(callback.to_js(), global)); } q.seq = seq.wrapping_add(1); seq }); - if callback.is_function() { + if callback.is_some() { if let Some(h) = &mut native_handle { h.cluster_seq = Some(this_seq); } @@ -310,34 +305,31 @@ pub(crate) fn should_ignore_one_disconnect_event_listener(global: &JSGlobalObjec pub(crate) fn cluster_raw_bind<'a>( cx: &mut JsScope<'a>, - address_type: JsLocal<'a>, - address: JsLocal<'a>, - port: JsLocal<'a>, - flags: JsLocal<'a>, -) -> JsResult { - let global = cx.global(); - let address_type = address_type.to_js(); - let address = address.to_js(); - let port = port.to_js().to_int32(); - let flags = flags.to_js().to_int32(); + address_type: BindAddressType<'a>, + address: BindAddress<'a>, + port: f64, + flags: i32, +) -> JsResult { + let port = port as i32; #[cfg(windows)] { - if address_type.is_string() || (address_type.is_int32() && address_type.as_int32() == -1) { - return Ok(JSValue::js_number_from_int32(-bun_sys::UV_E::NOTSUP)); + let _ = cx; + match address_type { + BindAddressType::Udp(_) => return Ok(BindResult::Errno(-bun_sys::UV_E::NOTSUP)), + BindAddressType::Family(family) if family == -1.0 => { + return Ok(BindResult::Errno(-bun_sys::UV_E::NOTSUP)); + } + BindAddressType::Family(_) | BindAddressType::Ignored(_) => {} } - let host_owned: Vec = if address.is_string() { - let mut v = address.to_js_string_view(global)?.to_owned_slice(); - v.push(0); - v - } else { - b"::\0".to_vec() - }; - let fallback_host: Option<&[u8]> = if address.is_string() { - None - } else { - Some(b"0.0.0.0\0") + let (host_owned, fallback_host): (Vec, Option<&[u8]>) = match address { + BindAddress::Text(host) => { + let mut v = host.to_vec(); + v.push(0); + (v, None) + } + BindAddress::Ignored(_) => (b"::\0".to_vec(), Some(b"0.0.0.0\0")), }; let options: core::ffi::c_int = if flags & 1 != 0 { @@ -381,48 +373,27 @@ pub(crate) fn cluster_raw_bind<'a>( if fd == bun_uws::LIBUS_SOCKET_DESCRIPTOR::MAX { // SAFETY: pure translation function. let uv_err = unsafe { bun_libuv_sys::uv_translate_sys_error(err) }; - return Ok(JSValue::js_number_from_int32(if uv_err != 0 { - uv_err - } else { - -4094 - })); + return Ok(BindResult::Errno(if uv_err != 0 { uv_err } else { -4094 })); } - let obj = JSValue::create_empty_object(global, 2); - obj.put( - global, - b"fd", - if (fd as u64) <= i32::MAX as u64 { - JSValue::js_number_from_int32(fd as i32) - } else { - JSValue::js_number_from_uint64(fd as u64) - }, - ); - obj.put(global, b"port", JSValue::js_number_from_int32(out_port)); - return Ok(obj); + return Ok(BindResult::Handle(BoundHandle { + fd: fd as u64, + port: out_port, + })); } #[cfg(not(windows))] { use core::ffi::c_int; - let mut is_udp = false; - let atype: i32; - if address_type.is_string() { - is_udp = true; - atype = if address_type.to_js_string_view(global)?.eq_ascii(b"udp6") { - 6 - } else { - 4 - }; - } else if address_type.is_int32() { - atype = address_type.as_int32(); - } else { - // Node compares addressType with ===, so anything that isn't 6 or -1 means IPv4. - atype = 4; - } + let (is_udp, atype): (bool, i32) = match address_type { + BindAddressType::Udp(name) => (true, if name == b"udp6" { 6 } else { 4 }), + BindAddressType::Family(6.0) => (false, 6), + BindAddressType::Family(-1.0) => (false, -1), + BindAddressType::Family(_) | BindAddressType::Ignored(_) => (false, 4), + }; - fn last_neg_errno() -> JSValue { - JSValue::js_number_from_int32(-bun_core::ffi::errno()) + fn last_neg_errno() -> BindResult { + BindResult::Errno(-bun_core::ffi::errno()) } fn close_fd(fd: c_int) { @@ -436,17 +407,21 @@ pub(crate) fn cluster_raw_bind<'a>( } if atype == -1 { - if !address.is_string() { - return Err(global.throw_invalid_argument_type_value("address", "string", address)); - } - let path_view = address.to_js_string_view(global)?; - let path_slice = path_view.to_utf8(); - let path_bytes = path_slice.slice(); + let path_bytes = match address { + BindAddress::Text(path) => path, + BindAddress::Ignored(value) => { + return Err(cx.global().throw_invalid_argument_type_value( + "address", + "string", + value.to_js(), + )); + } + }; // SAFETY: sockaddr_un is plain C data; all-zero is a valid value. let mut sun: libc::sockaddr_un = unsafe { bun_core::ffi::zeroed_unchecked() }; sun.sun_family = libc::AF_UNIX as libc::sa_family_t; if path_bytes.len() >= sun.sun_path.len() { - return Ok(JSValue::js_number_from_int32(-(libc::ENAMETOOLONG))); + return Ok(BindResult::Errno(-(libc::ENAMETOOLONG))); } for (i, b) in path_bytes.iter().enumerate() { sun.sun_path[i] = *b as _; @@ -469,10 +444,10 @@ pub(crate) fn cluster_raw_bind<'a>( close_fd(fd); return Ok(e); } - let obj = JSValue::create_empty_object(global, 2); - obj.put(global, b"fd", JSValue::js_number_from_int32(fd)); - obj.put(global, b"port", JSValue::js_number_from_int32(-1)); - return Ok(obj); + return Ok(BindResult::Handle(BoundHandle { + fd: fd as u64, + port: -1, + })); } } @@ -591,13 +566,10 @@ pub(crate) fn cluster_raw_bind<'a>( let ss_len: libc::socklen_t; let fd: c_int; let bound_family: c_int; - if address.is_string() { - let addr_view = address.to_js_string_view(global)?; - let addr_slice = addr_view.to_utf8(); - let addr_bytes = addr_slice.slice(); + if let BindAddress::Text(addr_bytes) = address { let mut addr_z: [u8; 256] = [0; 256]; if addr_bytes.len() >= addr_z.len() { - return Ok(JSValue::js_number_from_int32(-(libc::EINVAL))); + return Ok(BindResult::Errno(-(libc::EINVAL))); } addr_z[..addr_bytes.len()].copy_from_slice(addr_bytes); @@ -642,7 +614,7 @@ pub(crate) fn cluster_raw_bind<'a>( ) }; if rc != 0 || res.is_null() { - return Ok(JSValue::js_number_from_int32(-(libc::EINVAL))); + return Ok(BindResult::Errno(-(libc::EINVAL))); } // SAFETY: rc == 0 and res was null-checked; ai_addr/ai_addrlen unsafe { @@ -672,7 +644,7 @@ pub(crate) fn cluster_raw_bind<'a>( fd = bound; bound_family = family; } - Err(e) => return Ok(JSValue::js_number_from_int32(e)), + Err(e) => return Ok(BindResult::Errno(e)), } } else { let (ss6, len6) = wildcard_sockaddr(libc::AF_INET6, port); @@ -682,7 +654,7 @@ pub(crate) fn cluster_raw_bind<'a>( bound_family = libc::AF_INET6; } Err(e) if e == -(libc::EADDRINUSE) => { - return Ok(JSValue::js_number_from_int32(e)); + return Ok(BindResult::Errno(e)); } Err(_) => { let (ss4, len4) = wildcard_sockaddr(libc::AF_INET, port); @@ -691,7 +663,7 @@ pub(crate) fn cluster_raw_bind<'a>( fd = bound; bound_family = libc::AF_INET; } - Err(e) => return Ok(JSValue::js_number_from_int32(e)), + Err(e) => return Ok(BindResult::Errno(e)), } } } @@ -710,10 +682,10 @@ pub(crate) fn cluster_raw_bind<'a>( }; } - let obj = JSValue::create_empty_object(global, 2); - obj.put(global, b"fd", JSValue::js_number_from_int32(fd)); - obj.put(global, b"port", JSValue::js_number_from_int32(bound_port)); - Ok(obj) + Ok(BindResult::Handle(BoundHandle { + fd: fd as u64, + port: bound_port, + })) } } } diff --git a/src/runtime/node/node_crypto_binding.bind.ts b/src/runtime/node/node_crypto_binding.bind.ts index ad86a0b7c0ae..c58009c19734 100644 --- a/src/runtime/node/node_crypto_binding.bind.ts +++ b/src/runtime/node/node_crypto_binding.bind.ts @@ -2,68 +2,180 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::node::crypto" }); -/** Each body validates its own arguments, in Node's order and with Node's errors. */ +const INT32_MAX = 2 ** 31 - 1; +const UINT32_MAX = 2 ** 32 - 1; + const js = { capability: "js", throws: true } as const; -const argon2Parameters = { - message: b.any, - nonce: b.any, - parallelism: b.any, - tagLength: b.any, - memory: b.any, - passes: b.any, - secret: b.any, - associatedData: b.any, - type: b.any, +const stringOrBufferNames = ["string", "ArrayBuffer", "Buffer", "TypedArray", "DataView"]; +const passwordAndSaltNames = { password: stringOrBufferNames, salt: stringOrBufferNames }; +const stringOrBuffer = b.bytes({ from: ["string", "buffer"] }); +/** For a job: the body isolates a string, and copies a buffer once nothing more can run JavaScript. */ +const stringOrBufferHandle = b.bytes({ from: ["string", "buffer"], string: "asIs", held: "handle" }); +const passwordAndSaltHandles = { password: stringOrBufferHandle, salt: stringOrBufferHandle }; +const bufferNames = ["ArrayBuffer", "ArrayBufferView"]; +const keylen = b.u32.validateInteger(0, INT32_MAX); + +const pbkdf2Parameters = { + iterations: b.u32.validateInteger(1, INT32_MAX), + keylen, }; -export const crypto = b.namespace({ - pbkdf2: { - args: { password: b.any, salt: b.any, iterations: b.any, keylen: b.any, digest: b.any, callback: b.any }, - ...js, +export const RandomUUIDOptions = b.dictionary( + { disableEntropyCache: { type: b.bool.default(false), expected: ["boolean"] } }, + { reportedAs: "options" }, +); +const randomUUID = { + args: { options: RandomUUIDOptions.default({}) }, + expected: { options: ["object"] }, + returns: b.string, + length: 1, + capability: "heap", +} as const; + +const uint32 = b.u32.validateInteger().optional; +export const ScryptOptions = b.dictionary( + { + N: uint32, + cost: uint32, + r: uint32, + blockSize: uint32, + p: uint32, + parallelization: uint32, + maxmem: b.u64.validateInteger().optional, }, + { allowArray: true, nonObjectAsEmpty: true, reportedAs: "" }, +); + +const positiveUint32 = b.u32.validateInteger(1, UINT32_MAX); +const argon2Parameters = (bytes: b.Type) => ({ + message: bytes, + nonce: bytes, + parallelism: positiveUint32, + tagLength: positiveUint32, + memory: positiveUint32, + passes: positiveUint32, + secret: bytes, + associatedData: bytes, + /** `kArgon2Types` of `crypto.ts`. */ + variant: b.u8.validateInteger(0, 2), +}); + +export const crypto = b.namespace({ + pbkdf2: [ + { + // The body reports a digest that does not exist before a callback that is missing. + args: { ...passwordAndSaltHandles, ...pbkdf2Parameters, digest: b.string.utf8, callback: b.callable.optional }, + expected: { ...passwordAndSaltNames, digest: ["string"], callback: ["function"] }, + rustPath: "crate::node::crypto::pbkdf2", + length: 6, + ...js, + }, + { + args: { ...passwordAndSaltHandles, ...pbkdf2Parameters, callback: b.callable }, + expected: { ...passwordAndSaltNames, callback: ["function"] }, + rustPath: "crate::node::crypto::pbkdf2_without_digest", + ...js, + }, + ], pbkdf2Sync: { - args: { password: b.any, salt: b.any, iterations: b.any, keylen: b.any, digest: b.any }, - returns: b.any, - ...js, - }, - randomInt: { - args: { min: b.any, max: b.any, callback: b.any }, - returns: b.f64.optional, - length: 2, - ...js, - }, - randomFill: { - args: { buf: b.any, offset: b.any, size: b.any, callback: b.any }, - ...js, + args: { password: stringOrBuffer, salt: stringOrBuffer, ...pbkdf2Parameters, digest: b.string.utf8 }, + expected: { ...passwordAndSaltNames, digest: ["string"] }, + returns: b.bufferSource, + capability: "heap", + throws: true, }, + randomInt: [ + { + args: { min: b.f64, max: b.f64, callback: b.callable.optional }, + expected: { min: ["a safe integer"], max: ["a safe integer"], callback: ["function"] }, + returns: b.f64.optional, + rustPath: "crate::node::crypto::random_int_between", + length: 2, + ...js, + }, + { + args: { max: b.f64, callback: b.callable.optional }, + expected: { max: ["a safe integer"], callback: ["function"] }, + returns: b.f64.optional, + rustPath: "crate::node::crypto::random_int_below", + ...js, + }, + ], + randomFill: [ + { + args: { buf: b.bufferSource.allowShared, offset: b.f64, size: b.f64.optional, callback: b.callable }, + expected: { buf: bufferNames, offset: ["number"], size: ["number"], callback: ["function"] }, + rustPath: "crate::node::crypto::random_fill_range", + length: 4, + ...js, + }, + { + args: { buf: b.bufferSource.allowShared, offset: b.f64, callback: b.callable }, + expected: { buf: bufferNames, offset: ["number"], callback: ["function"] }, + rustPath: "crate::node::crypto::random_fill_from_offset", + ...js, + }, + { + args: { buf: b.bufferSource.allowShared, callback: b.callable }, + expected: { buf: bufferNames, callback: ["function"] }, + rustPath: "crate::node::crypto::random_fill_all", + ...js, + }, + ], randomFillSync: { - args: { buf: b.any, offset: b.any, size: b.any }, - returns: b.any, - ...js, + args: { buf: b.bufferSource.allowShared, offset: b.f64.default(0), size: b.f64.optional }, + expected: { buf: bufferNames, offset: ["number"], size: ["number"] }, + returns: b.bufferSource, + length: 3, + capability: "heap", + throws: true, }, - randomUUID: { args: { options: b.any }, returns: b.string, ...js }, - randomUUIDv7: { - args: { options: b.any }, - returns: b.string, - rustPath: "crate::node::crypto::random_uuid_v7", - ...js, + randomUUID, + randomUUIDv7: { ...randomUUID, rustPath: "crate::node::crypto::random_uuid_v7" }, + randomBytes: { + args: { size: b.f64, callback: b.callable.optional }, + expected: { size: ["number"], callback: ["function"] }, + returns: b.bufferSource.optional, + length: 2, + capability: "js", + throws: true, }, - randomBytes: { args: { size: b.any, callback: b.any }, returns: b.any, ...js }, secureHeapUsed: {}, getFips: { returns: b.i32 }, setFips: { length: 1 }, setEngine: { length: 2, capability: "heap", throws: true }, - getHashes: { returns: b.any, capability: "heap", throws: true }, - scrypt: { - args: { password: b.any, salt: b.any, keylen: b.any, options: b.any, callback: b.any }, - ...js, - }, + getHashes: { returns: b.array(b.string.utf8) }, + scrypt: [ + { + args: { ...passwordAndSaltHandles, keylen, options: ScryptOptions, callback: b.callable }, + expected: { ...passwordAndSaltNames, callback: ["function"] }, + rustPath: "crate::node::crypto::scrypt", + length: 5, + ...js, + }, + { + args: { ...passwordAndSaltHandles, keylen, callback: b.callable }, + expected: { ...passwordAndSaltNames, callback: ["function"] }, + rustPath: "crate::node::crypto::scrypt_with_defaults", + ...js, + }, + ], scryptSync: { - args: { password: b.any, salt: b.any, keylen: b.any, options: b.any }, - returns: b.any, - ...js, + args: { password: stringOrBuffer, salt: stringOrBuffer, keylen, options: ScryptOptions }, + expected: passwordAndSaltNames, + returns: b.bufferSource, + capability: "heap", + throws: true, + }, + argon2: { + args: { ...argon2Parameters(b.bytes({ from: ["buffer"], held: "copied" })), callback: b.callable }, + capability: "js", + }, + argon2Sync: { + args: argon2Parameters(b.bytes({ from: ["buffer"] })), + returns: b.bufferSource, + capability: "heap", + throws: true, }, - argon2: { args: { ...argon2Parameters, callback: b.any }, ...js }, - argon2Sync: { args: argon2Parameters, returns: b.any, ...js }, }); diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index 38bea48c3dc2..972b448c73c2 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -8,15 +8,15 @@ use ::argon2 as rust_argon2; use bun_boringssl as boringssl; use bun_collections::CaseInsensitiveAsciiStringArrayHashMap; use bun_core::String as BunString; -use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope}; +use bun_jsc::bindgen::{self, BufferSource, Callable, JsHeap, JsLocal, JsScope, StringOrJsBytes}; use bun_jsc::{ self as jsc, ArrayBuffer, ErrorCode, JSGlobalObject, JSType, JSValue, Job, JobContext, JsPtr, JsResult, JsThread, Protected, Strong, UUID, UUID7, }; use crate::crypto::pbkdf2::{self, PBKDF2}; -use crate::node::util::validators; -use crate::node::{Flavor, StringOrBuffer, ThreadIsolated, ThreadIsolatedArg}; +use crate::generated_bindgen::node_crypto_binding::{RandomUUIDOptions, ScryptOptions}; +use crate::node::{StringOrBuffer, ThreadIsolated}; // `&JSGlobalObject` is ABI-identical to a non-null pointer; remaining params // are by-value `JSValue`, so no caller-side preconditions remain. @@ -32,24 +32,10 @@ unsafe extern "C" { /// Local extension surface for `JSValue` methods not yet on `bun_jsc::JSValue`. /// (`with_async_context_if_needed` graduated to an inherent method upstream.) trait JSValueCryptoExt { - fn is_safe_integer(self) -> bool; fn call_next_tick_2(self, global: &JSGlobalObject, a: JSValue, b: JSValue) -> JsResult<()>; } impl JSValueCryptoExt for JSValue { - /// `Number.isSafeInteger` semantics. - #[inline] - fn is_safe_integer(self) -> bool { - if self.is_int32() { - return true; - } - if !self.is_double() { - return false; - } - let d = self.as_double(); - d.trunc() == d && d.abs() <= jsc::MAX_SAFE_INTEGER as f64 - } - #[inline] fn call_next_tick_2(self, global: &JSGlobalObject, a: JSValue, b: JSValue) -> JsResult<()> { jsc::from_js_host_call_generic(global, || Bun__Process__queueNextTick2(global, self, a, b)) @@ -314,44 +300,55 @@ fn schedule( ); } -pub(crate) fn random_int<'a>( +pub(crate) fn random_int_between<'a>( cx: &mut JsScope<'a>, - min: JsLocal<'a>, - max: JsLocal<'a>, - callback: JsLocal<'a>, + min: f64, + max: f64, + callback: Option>, ) -> JsResult> { - let global = cx.global(); - let [mut min_value, mut max_value, mut callback] = [min.to_js(), max.to_js(), callback.to_js()]; - - let mut min_specified = true; - if max_value.is_undefined() || max_value.is_callable() { - callback = max_value; - max_value = min_value; - min_value = JSValue::js_number(0.0); - min_specified = false; - } + random_int(cx, Some(min), max, callback) +} - if !callback.is_undefined() { - let _ = validators::validate_function(global, "callback", callback)?; - } +pub(crate) fn random_int_below<'a>( + cx: &mut JsScope<'a>, + max: f64, + callback: Option>, +) -> JsResult> { + random_int(cx, None, max, callback) +} + +/// `Number.isSafeInteger`. +fn is_safe_integer(number: f64) -> bool { + number.trunc() == number && number.abs() <= jsc::MAX_SAFE_INTEGER as f64 +} - if !min_value.is_safe_integer() { +fn random_int<'a>( + cx: &mut JsScope<'a>, + min: Option, + max: f64, + callback: Option>, +) -> JsResult> { + let global = cx.global(); + let min_specified = min.is_some(); + let min = min.unwrap_or(0.0); + + if !is_safe_integer(min) { return Err(global.throw_invalid_argument_type_value2( b"min", b"a safe integer", - min_value, + JSValue::js_number(min), )); } - if !max_value.is_safe_integer() { + if !is_safe_integer(max) { return Err(global.throw_invalid_argument_type_value2( b"max", b"a safe integer", - max_value, + JSValue::js_number(max), )); } - let min: i64 = min_value.as_number().trunc() as i64; - let max: i64 = max_value.as_number().trunc() as i64; + let min = min as i64; + let max = max as i64; if max <= min { return Err(global @@ -433,35 +430,26 @@ pub(crate) fn random_int<'a>( min.wrapping_add(x as i64) }; - if !callback.is_undefined() { - callback.call_next_tick_2(global, JSValue::UNDEFINED, JSValue::js_number(res as f64))?; + if let Some(callback) = callback { + callback.to_js().call_next_tick_2( + global, + JSValue::UNDEFINED, + JSValue::js_number(res as f64), + )?; return Ok(None); } Ok(Some(res as f64)) } -pub(crate) fn random_uuid<'a>(cx: &mut JsScope<'a>, options: JsLocal<'a>) -> JsResult { - let global = cx.global(); - let options = options.to_js(); - - let mut disable_entropy_cache = false; - if !options.is_undefined() { - validators::validate_object(global, options, format_args!("options"), Default::default())?; - if let Some(disable_entropy_cache_value) = options.get(global, "disableEntropyCache")? { - disable_entropy_cache = validators::validate_boolean( - global, - disable_entropy_cache_value, - format_args!("options.disableEntropyCache"), - )?; - } - } - +pub(crate) fn random_uuid(heap: &JsHeap<'_>, options: &RandomUUIDOptions) -> BunString { let (str, bytes) = BunString::create_uninitialized_latin1(36); - let uuid = if disable_entropy_cache { + let uuid = if options.disable_entropy_cache { UUID::init() } else { + // SAFETY: it only leads to the VM's entropy cache. + let global = unsafe { heap.global() }; global.bun_vm().as_mut().rare_data().next_uuid() }; @@ -470,33 +458,18 @@ pub(crate) fn random_uuid<'a>(cx: &mut JsScope<'a>, options: JsLocal<'a>) -> JsR .try_into() .expect("infallible: size matches"), ); - Ok(str) + str } -pub(crate) fn random_uuid_v7<'a>( - cx: &mut JsScope<'a>, - options: JsLocal<'a>, -) -> JsResult { - let global = cx.global(); - let options = options.to_js(); - - let mut disable_entropy_cache = false; - if !options.is_undefined() { - validators::validate_object(global, options, format_args!("options"), Default::default())?; - if let Some(disable_entropy_cache_value) = options.get(global, "disableEntropyCache")? { - disable_entropy_cache = validators::validate_boolean( - global, - disable_entropy_cache_value, - format_args!("options.disableEntropyCache"), - )?; - } - } +pub(crate) fn random_uuid_v7(heap: &JsHeap<'_>, options: &RandomUUIDOptions) -> BunString { + // SAFETY: it only reads the clock and leads to the VM's entropy cache. + let global = unsafe { heap.global() }; // jsDateNow() is exactly what JS Date.now() returns, so the embedded // timestamp is never behind a Date.now() sample taken by the caller. let now_ms = global.js_date_now().max(0.0) as u64; let mut entropy = [0u8; 10]; - if disable_entropy_cache { + if options.disable_entropy_cache { boringssl::rand_bytes(&mut entropy); } else { entropy.copy_from_slice(&global.bun_vm().as_mut().rare_data().entropy_slice(10)[..10]); @@ -509,19 +482,16 @@ pub(crate) fn random_uuid_v7<'a>( .try_into() .expect("infallible: size matches"), ); - Ok(str) + str } fn assert_offset( global: &JSGlobalObject, - offset_value: JSValue, + offset: f64, element_size: u8, length: usize, ) -> JsResult { - if !offset_value.is_number() { - return Err(global.throw_invalid_argument_type_value(b"offset", b"number", offset_value)); - } - let offset = offset_value.as_number() * (element_size as f64); + let offset = offset * (element_size as f64); let max_length = length.min(MAX_POSSIBLE_LENGTH); if offset.is_nan() || offset > (max_length as f64) || offset < 0.0 { @@ -542,13 +512,12 @@ fn assert_offset( fn assert_size( global: &JSGlobalObject, - size_value: JSValue, + size: f64, element_size: u8, offset: u32, length: usize, ) -> JsResult { - let mut size = validators::validate_number(global, size_value, "size", None, None)?; - size *= element_size as f64; + let size = size * (element_size as f64); if size.is_nan() || size > (MAX_POSSIBLE_LENGTH as f64) || size < 0.0 { // Node spells this range with "&&" (lib/internal/crypto/random.js assertSize). @@ -579,29 +548,24 @@ fn assert_size( pub(crate) fn random_bytes<'a>( cx: &mut JsScope<'a>, - size: JsLocal<'a>, - callback: JsLocal<'a>, -) -> JsResult { + size: f64, + callback: Option>, +) -> JsResult> { let global = cx.global(); - let [size_value, callback] = [size.to_js(), callback.to_js()]; - - let size = assert_size(global, size_value, 1, 0, MAX_POSSIBLE_LENGTH + 1)?; - if !callback.is_undefined() { - let _ = validators::validate_function(global, "callback", callback)?; - } + let size = assert_size(global, size, 1, 0, MAX_POSSIBLE_LENGTH + 1)?; let (result, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, size)?; - if callback.is_undefined() { + let Some(callback) = callback else { // sync boringssl::rand_bytes(bytes); - return Ok(result); - } + return Ok(Some(result)); + }; schedule( &cx.js_thread(), - callback, + callback.to_js(), RandomFillJob::InPlace { // SAFETY: `bytes` is `result`'s backing store, kept alive by the job's // Js side; a slice's data pointer is non-null even when empty. @@ -612,104 +576,97 @@ pub(crate) fn random_bytes<'a>( None, ); - Ok(JSValue::UNDEFINED) + Ok(None) } pub(crate) fn random_fill_sync<'a>( - cx: &mut JsScope<'a>, - buf: JsLocal<'a>, - offset: JsLocal<'a>, - size: JsLocal<'a>, -) -> JsResult { - let global = cx.global(); - let [buf_value, offset_value, size_value] = [buf.to_js(), offset.to_js(), size.to_js()]; + heap: &JsHeap<'a>, + buf: JsLocal<'a, BufferSource>, + offset: f64, + size: Option, +) -> JsResult> { + // SAFETY: it only says where the bytes of `buf` are, and throws. + let global = unsafe { heap.global() }; + let mut bytes = ArrayBuffer::from_typed_array(global, buf.to_js()); - let Some(mut buf) = buf_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - b"buf", - b"ArrayBuffer or ArrayBufferView", - buf_value, - )); - }; - - let element_size = buf.bytes_per_element().unwrap_or(1); + let element_size = bytes.bytes_per_element().unwrap_or(1); - let offset = assert_offset( - global, - if offset_value.is_undefined() { - JSValue::js_number(0.0) - } else { - offset_value - }, - element_size, - buf.byte_len, - )?; + let offset = assert_offset(global, offset, element_size, bytes.byte_len)?; - // `size` is usize (`buf.byte_len - offset`, both usize). The + // `size` is usize (`bytes.byte_len - offset`, both usize). The // `assert_size` branch is bounded by `MAX_POSSIBLE_LENGTH` (≤ i32::MAX) so widening // its `u32` result is lossless; the default branch must NOT truncate to `u32` — // a >4 GiB ArrayBuffer remainder would silently fill only `(n % 2^32)` bytes. - let size: usize = if size_value.is_undefined() { - buf.byte_len - offset as usize - } else { - assert_size(global, size_value, element_size, offset, buf.byte_len)? as usize + let size: usize = match size { + None => bytes.byte_len - offset as usize, + Some(size) => assert_size(global, size, element_size, offset, bytes.byte_len)? as usize, }; if size == 0 { - return Ok(buf_value); + return Ok(buf); } - boringssl::rand_bytes(&mut buf.slice_mut()[offset as usize..][..size]); + boringssl::rand_bytes(&mut bytes.slice_mut()[offset as usize..][..size]); + + Ok(buf) +} - Ok(buf_value) +pub(crate) fn random_fill_range<'a>( + cx: &mut JsScope<'a>, + buf: JsLocal<'a, BufferSource>, + offset: f64, + size: Option, + callback: JsLocal<'a, Callable>, +) -> JsResult<()> { + random_fill(cx, buf, Some(offset), size, callback) +} + +pub(crate) fn random_fill_from_offset<'a>( + cx: &mut JsScope<'a>, + buf: JsLocal<'a, BufferSource>, + offset: f64, + callback: JsLocal<'a, Callable>, +) -> JsResult<()> { + random_fill(cx, buf, Some(offset), None, callback) +} + +pub(crate) fn random_fill_all<'a>( + cx: &mut JsScope<'a>, + buf: JsLocal<'a, BufferSource>, + callback: JsLocal<'a, Callable>, +) -> JsResult<()> { + random_fill(cx, buf, None, None, callback) } -pub(crate) fn random_fill<'a>( +fn random_fill<'a>( cx: &mut JsScope<'a>, - buf: JsLocal<'a>, - offset: JsLocal<'a>, - size: JsLocal<'a>, - callback: JsLocal<'a>, + buf: JsLocal<'a, BufferSource>, + offset: Option, + size: Option, + callback: JsLocal<'a, Callable>, ) -> JsResult<()> { let global = cx.global(); - let [buf_value, offset_value, mut size_value, mut callback] = - [buf.to_js(), offset.to_js(), size.to_js(), callback.to_js()]; + let buf_value = buf.to_js(); + let callback = callback.to_js(); - let Some(buf) = buf_value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - b"buf", - b"ArrayBuffer or ArrayBufferView", - buf_value, - )); - }; + let buf = ArrayBuffer::from_typed_array(global, buf_value); let element_size = buf.bytes_per_element().unwrap_or(1); - let offset: u32; - if offset_value.is_callable() { - callback = offset_value; - offset = assert_offset(global, JSValue::js_number(0.0), element_size, buf.byte_len)?; - size_value = JSValue::js_number(buf.len as f64); - } else if size_value.is_callable() { - callback = size_value; - offset = assert_offset(global, offset_value, element_size, buf.byte_len)?; - // `offset` is a byte offset (already scaled by element_size) but `buf.len` - // is an element count, so `buf.len - offset` would mix units and can - // underflow. Defer to the `buf.byte_len - offset` default below instead. - size_value = JSValue::UNDEFINED; - } else { - let _ = validators::validate_function(global, "callback", callback)?; - offset = assert_offset(global, offset_value, element_size, buf.byte_len)?; - } + // Without an offset the size is every element, which `assert_size` holds to its limit. + let (offset, size) = match offset { + Some(offset) => (offset, size), + None => (0.0, Some(buf.len as f64)), + }; + let offset = assert_offset(global, offset, element_size, buf.byte_len)?; // `size` is usize (`buf.byte_len - offset`, both usize). The // `assert_size` branch is bounded by `MAX_POSSIBLE_LENGTH` (≤ i32::MAX) so widening // its `u32` result is lossless; the default branch must NOT truncate to `u32` — // a >4 GiB ArrayBuffer remainder would silently fill only `(n % 2^32)` bytes. - let size: usize = if size_value.is_undefined() { - buf.byte_len - offset as usize - } else { - assert_size(global, size_value, element_size, offset, buf.byte_len)? as usize + let size: usize = match size { + None => buf.byte_len - offset as usize, + Some(size) => assert_size(global, size, element_size, offset, buf.byte_len)? as usize, }; if size == 0 { @@ -746,30 +703,26 @@ pub(crate) fn random_fill<'a>( // Scrypt // ─────────────────────────────────────────────────────────────────────────── pub(crate) struct Scrypt { - password: StringOrBuffer<'static>, - salt: StringOrBuffer<'static>, n: u32, r: u32, p: u32, maxmem: u64, keylen: u32, } -// SAFETY: `password` and `salt` are `StringOrBuffer`s (see its impl); the rest is plain data. -unsafe impl ThreadIsolatedArg for Scrypt {} // ─────────────────────────────────────────────────────────────────────────── // Argon2 (crypto.argon2 / crypto.argon2Sync) // ─────────────────────────────────────────────────────────────────────────── /// One argon2 derivation, routed to the pure-Rust `rust-argon2` crate that -/// `Bun.password` already uses (BoringSSL has no argon2). Inputs are copied -/// out of JS at call time so the work-pool half never touches JS memory; -/// node's async jobs copy the same way. -pub(crate) struct Argon2 { - message: Vec, - nonce: Vec, - secret: Vec, - associated_data: Vec, +/// `Bun.password` already uses (BoringSSL has no argon2). `Bytes` is borrowed +/// from JS for `argon2Sync` and a copy for the work-pool job, which never +/// touches JS memory; node's async jobs copy the same way. +pub(crate) struct Argon2 { + message: Bytes, + nonce: Bytes, + secret: Bytes, + associated_data: Bytes, parallelism: u32, tag_length: u32, memory: u32, @@ -780,198 +733,36 @@ pub(crate) struct Argon2 { } impl Scrypt { - /// The return type cannot vary on the const-generic bool, so this always - /// returns `(Self, JSValue)`; the sync caller ignores the second element. - fn from_js( - global: &JSGlobalObject, - password_value: JSValue, - salt_value: JSValue, - keylen_value: JSValue, - options_arg: JSValue, - callback_arg: JSValue, - ) -> JsResult<(Self, JSValue)> { - let mut maybe_options_value: Option = Some(options_arg); - let mut callback = callback_arg; - - if IS_ASYNC { - if callback.is_undefined() { - callback = maybe_options_value.unwrap(); - maybe_options_value = None; - } + fn new(global: &JSGlobalObject, keylen: u32, options: &ScryptOptions) -> JsResult { + if options.n.is_some() && options.cost.is_some() { + return Err(global.throw_incompatible_option_pair(b"N", b"cost")); } - - let Some(password) = StringOrBuffer::from_js(global, password_value)? else { - return Err(global.throw_invalid_argument_type_value( - b"password", - b"string, ArrayBuffer, Buffer, TypedArray, or DataView", - password_value, - )); - }; - - let Some(salt) = StringOrBuffer::from_js(global, salt_value)? else { - return Err(global.throw_invalid_argument_type_value( - b"salt", - b"string, ArrayBuffer, Buffer, TypedArray, or DataView", - salt_value, - )); - }; - - let keylen = validators::validate_int32( - global, - keylen_value, - format_args!("keylen"), - Some(0), - None, - )?; - - let mut n: Option = None; - let mut r: Option = None; - let mut p: Option = None; - let mut maxmem: Option = None; - - if let Some(options_value) = maybe_options_value { - if let Some(options) = options_value.get_object() { - // `get_object` returned non-null; the JSObject is rooted by - // `options_value` (kept alive on the stack for this scope). - // `JSObject` is an `opaque_ffi!` ZST handle; `opaque_ref` is the - // centralised non-null-ZST deref proof. - let options = bun_jsc::JSObject::opaque_ref(options); - if let Some(n_value) = options.get(global, "N")? { - n = Some(validators::validate_uint32( - global, - n_value, - format_args!("N"), - false, - )?); - } - - if let Some(cost_value) = options.get(global, "cost")? { - if n.is_some() { - return Err(global.throw_incompatible_option_pair(b"N", b"cost")); - } - n = Some(validators::validate_uint32( - global, - cost_value, - format_args!("cost"), - false, - )?); - } - - if let Some(r_value) = options.get(global, "r")? { - r = Some(validators::validate_uint32( - global, - r_value, - format_args!("r"), - false, - )?); - } - - if let Some(blocksize_value) = options.get(global, "blockSize")? { - if r.is_some() { - return Err(global.throw_incompatible_option_pair(b"r", b"blockSize")); - } - r = Some(validators::validate_uint32( - global, - blocksize_value, - format_args!("blockSize"), - false, - )?); - } - - if let Some(p_value) = options.get(global, "p")? { - p = Some(validators::validate_uint32( - global, - p_value, - format_args!("p"), - false, - )?); - } - - if let Some(parallelization_value) = options.get(global, "parallelization")? { - if p.is_some() { - return Err(global.throw_incompatible_option_pair(b"p", b"parallelization")); - } - p = Some(validators::validate_uint32( - global, - parallelization_value, - format_args!("parallelization"), - false, - )?); - } - - if let Some(maxmem_value) = options.get(global, "maxmem")? { - maxmem = Some(validators::validate_integer( - global, - maxmem_value, - "maxmem", - Some(0), - None, - )?); - } - } + if options.r.is_some() && options.block_size.is_some() { + return Err(global.throw_incompatible_option_pair(b"r", b"blockSize")); + } + if options.p.is_some() && options.parallelization.is_some() { + return Err(global.throw_incompatible_option_pair(b"p", b"parallelization")); } const N_DEFAULT: u32 = 16384; const R_DEFAULT: u32 = 8; const P_DEFAULT: u32 = 1; - const MAXMEM_DEFAULT: i64 = 33554432; - - if n.is_none() || n.unwrap() == 0 { - n = Some(N_DEFAULT); - } - if r.is_none() || r.unwrap() == 0 { - r = Some(R_DEFAULT); - } - if p.is_none() || p.unwrap() == 0 { - p = Some(P_DEFAULT); - } - if maxmem.is_none() || maxmem.unwrap() == 0 { - maxmem = Some(MAXMEM_DEFAULT); - } - - let mut ctx = Scrypt { - password, - salt, - n: n.unwrap(), - r: r.unwrap(), - p: p.unwrap(), - maxmem: u64::try_from(maxmem.unwrap()).expect("int cast"), - keylen: u32::try_from(keylen).expect("int cast"), + const MAXMEM_DEFAULT: u64 = 33554432; + + let or_default = |value: Option, default| value.filter(|&n| n != 0).unwrap_or(default); + + let ctx = Scrypt { + n: or_default(options.n.or(options.cost), N_DEFAULT), + r: or_default(options.r.or(options.block_size), R_DEFAULT), + p: or_default(options.p.or(options.parallelization), P_DEFAULT), + maxmem: options + .maxmem + .filter(|&maxmem| maxmem != 0) + .unwrap_or(MAXMEM_DEFAULT), + keylen, }; - if IS_ASYNC { - let _ = validators::validate_function(global, "callback", callback)?; - } - ctx.check_scrypt_params(global)?; - - // An option getter can detach or resize the buffers, so read them again. - for input in [&mut ctx.password, &mut ctx.salt] { - if IS_ASYNC { - input.make_thread_isolated_copy(global)?; - } else if let StringOrBuffer::Buffer(buffer) = input { - buffer.buffer = ArrayBuffer::from_typed_array(global, buffer.buffer.value); - } - } - - if IS_ASYNC { - return Ok((ctx, callback)); - } - Ok((ctx, JSValue::UNDEFINED)) - } - - /// `from_js::` for the work-pool job, with its callback. - fn from_js_async( - global: &JSGlobalObject, - password: JSValue, - salt: JSValue, - keylen: JSValue, - options: JSValue, - callback: JSValue, - ) -> JsResult<(ThreadIsolated, JSValue)> { - let (ctx, callback) = - Self::from_js::(global, password, salt, keylen, options, callback)?; - // SAFETY: `from_js::` copied the buffers and thread-isolated the strings. - Ok((unsafe { ThreadIsolated::new(ctx) }, callback)) + Ok(ctx) } fn check_scrypt_params(&self, global: &JSGlobalObject) -> JsResult<()> { @@ -1001,10 +792,7 @@ impl Scrypt { } /// `Some(err)` on failure (`0` when there is no BoringSSL error code). - fn run_task_impl(&self, key: &mut [u8]) -> Option { - let password = self.password.slice(); - let salt = self.salt.slice(); - + fn run_task_impl(&self, password: &[u8], salt: &[u8], key: &mut [u8]) -> Option { if key.is_empty() { // result will be an empty buffer return None; @@ -1040,7 +828,9 @@ impl Scrypt { /// `crypto.scrypt` off the JS thread: derives straight into the result /// ArrayBuffer's bytes under the job's ticket, which keeps their VM alive. pub(crate) struct ScryptJob { - params: ThreadIsolated, + params: Scrypt, + password: ThreadIsolated>, + salt: ThreadIsolated>, result: JsPtr<[u8]>, err: Option, } @@ -1058,7 +848,9 @@ impl JobContext for ScryptJob { fn run(this: &mut Self, done: bun_jsc::Completion) -> Option> { // SAFETY: `result` is `buf`'s backing store (kept by the Js side); VM alive under the ticket. let key = unsafe { this.result.under_ticket(done.ticket()) }; - this.err = this.params.run_task_impl(key); + this.err = this + .params + .run_task_impl(this.password.slice(), this.salt.slice(), key); Some(done) } @@ -1111,48 +903,91 @@ impl JobContext for ScryptJob { } } +/// For a work-pool job: a string is thread-isolated, and the bytes of a buffer, as they are now, are copied. +fn isolate_for_job<'a>( + cx: &mut JsScope<'a>, + input: StringOrJsBytes<'a>, +) -> JsResult>> { + let bytes = match input { + StringOrJsBytes::String(string) => { + return Ok(StringOrBuffer::thread_isolated_string(cx.global(), string)); + } + StringOrJsBytes::Bytes(bytes) => bytes.get(&cx.heap), + }; + let mut copy = Vec::new(); + if copy.try_reserve_exact(bytes.len()).is_err() { + return Err(cx.global().throw_out_of_memory()); + } + copy.extend_from_slice(bytes); + cx.global().vm().report_extra_memory(copy.len()); + Ok(StringOrBuffer::owned_isolated(copy)) +} + pub(crate) fn pbkdf2<'a>( cx: &mut JsScope<'a>, - password: JsLocal<'a>, - salt: JsLocal<'a>, - iterations: JsLocal<'a>, - keylen: JsLocal<'a>, - digest: JsLocal<'a>, - callback: JsLocal<'a>, + password: StringOrJsBytes<'a>, + salt: StringOrJsBytes<'a>, + iterations: u32, + keylen: u32, + digest: &[u8], + callback: Option>, ) -> JsResult<()> { - let (data, callback) = PBKDF2::from_js_async( + let global = cx.global(); + let algorithm = pbkdf2::algorithm_from_digest(global, digest)?; + let Some(callback) = callback else { + return Err(global.throw_invalid_argument_type_value( + b"callback", + b"function", + JSValue::UNDEFINED, + )); + }; + + let password = isolate_for_job(cx, password)?; + let salt = isolate_for_job(cx, salt)?; + let data = PBKDF2::new( cx.global(), - password.to_js(), - salt.to_js(), - iterations.to_js(), - keylen.to_js(), - digest.to_js(), - callback.to_js(), + password.slice(), + salt.slice(), + iterations, + keylen as usize, + algorithm, )?; - pbkdf2::create_job(&cx.js_thread(), data, callback); + pbkdf2::create_job(&cx.js_thread(), data, password, salt, callback.to_js()); Ok(()) } -pub(crate) fn pbkdf2_sync<'a>( +/// `pbkdf2(password, salt, iterations, keylen, callback)`: as in Node, the digest is what is missing. +pub(crate) fn pbkdf2_without_digest<'a>( cx: &mut JsScope<'a>, - password: JsLocal<'a>, - salt: JsLocal<'a>, - iterations: JsLocal<'a>, - keylen: JsLocal<'a>, - digest: JsLocal<'a>, + _: StringOrJsBytes<'a>, + _: StringOrJsBytes<'a>, + _: u32, + _: u32, + _: JsLocal<'a, Callable>, +) -> JsResult<()> { + Err(cx + .global() + .throw_invalid_argument_type_value(b"digest", b"string", JSValue::UNDEFINED)) +} + +pub(crate) fn pbkdf2_sync( + heap: &JsHeap<'_>, + password: &[u8], + salt: &[u8], + iterations: u32, + keylen: u32, + digest: &[u8], ) -> JsResult { - let global_this = cx.global(); - // `PBKDF2`'s `StringOrBuffer` fields release on `Drop`, so the local - // just goes out of scope. - let (mut data, _) = PBKDF2::from_js( + // SAFETY: it only allocates the result and throws, and nothing borrowed is read after a throw. + let global_this = unsafe { heap.global() }; + let algorithm = pbkdf2::algorithm_from_digest(global_this, digest)?; + let data = PBKDF2::new( global_this, - password.to_js(), - salt.to_js(), - iterations.to_js(), - keylen.to_js(), - digest.to_js(), - JSValue::UNDEFINED, - Flavor::Sync, + password, + salt, + iterations, + keylen as usize, + algorithm, )?; // `create_buffer_from_length` → `JSBuffer__bufferFromLength`, which constructs // with `JSBufferSubclassStructure` (a Node.js `Buffer`, not a plain Uint8Array/ArrayBuffer). @@ -1163,7 +998,7 @@ pub(crate) fn pbkdf2_sync<'a>( return Err(global_this.throw_out_of_memory()); }; - if !data.run(output.slice_mut()) { + if !data.run(password, salt, output.slice_mut()) { boringssl::c::ERR_clear_error(); let err = global_this.create_error_instance(format_args!("PBKDF2 derivation failed")); return Err(global_this.throw_value(err)); @@ -1205,7 +1040,7 @@ extern "C" fn for_each_hash( bun_core::handle_oom(hashes.put(from_bytes, ())); } -pub(crate) fn get_hashes<'a>(heap: &JsHeap<'a>) -> bindgen::Result<'a, JsLocal<'a>> { +pub(crate) fn get_hashes() -> Vec> { let mut hashes: CaseInsensitiveAsciiStringArrayHashMap<()> = CaseInsensitiveAsciiStringArrayHashMap::new(); @@ -1216,27 +1051,23 @@ pub(crate) fn get_hashes<'a>(heap: &JsHeap<'a>) -> bindgen::Result<'a, JsLocal<' boringssl::c::EVP_MD_do_all_sorted(for_each_hash, (&raw mut hashes).cast::()); } - bindgen::to_js::array(heap, hashes.keys(), bindgen::to_js::utf8) + core::mem::take(&mut *hashes).into_entries().0 } pub(crate) fn scrypt<'a>( cx: &mut JsScope<'a>, - password: JsLocal<'a>, - salt: JsLocal<'a>, - keylen: JsLocal<'a>, - options: JsLocal<'a>, - callback: JsLocal<'a>, + password: StringOrJsBytes<'a>, + salt: StringOrJsBytes<'a>, + keylen: u32, + options: &ScryptOptions, + callback: JsLocal<'a, Callable>, ) -> JsResult<()> { + let params = Scrypt::new(cx.global(), keylen, options)?; + + let password = isolate_for_job(cx, password)?; + let salt = isolate_for_job(cx, salt)?; let cx = cx.js_thread(); let global = cx.global(); - let (params, callback) = Scrypt::from_js_async( - global, - password.to_js(), - salt.to_js(), - keylen.to_js(), - options.to_js(), - callback.to_js(), - )?; if params.keylen as usize > jsc::virtual_machine::synthetic_allocation_limit() { return Err(global.throw_out_of_memory()); } @@ -1245,37 +1076,52 @@ pub(crate) fn scrypt<'a>( &cx, ScryptJob { params, + password, + salt, // SAFETY: `bytes` is `buf`'s backing store, kept alive by the job's Js side. result: unsafe { JsPtr::new(core::ptr::NonNull::from(bytes)) }, err: None, }, ScryptJs { - callback: Strong::create(callback.with_async_context_if_needed(global), global), + callback: Strong::create( + callback.to_js().with_async_context_if_needed(global), + global, + ), buf: Strong::create(buf, global), }, ); Ok(()) } -pub(crate) fn scrypt_sync<'a>( +pub(crate) fn scrypt_with_defaults<'a>( cx: &mut JsScope<'a>, - password: JsLocal<'a>, - salt: JsLocal<'a>, - keylen: JsLocal<'a>, - options: JsLocal<'a>, + password: StringOrJsBytes<'a>, + salt: StringOrJsBytes<'a>, + keylen: u32, + callback: JsLocal<'a, Callable>, +) -> JsResult<()> { + scrypt( + cx, + password, + salt, + keylen, + &ScryptOptions::default(), + callback, + ) +} + +pub(crate) fn scrypt_sync( + heap: &JsHeap<'_>, + password: &[u8], + salt: &[u8], + keylen: u32, + options: &ScryptOptions, ) -> JsResult { - let global = cx.global(); - // `password`/`salt` release on drop; nothing was protected on this path. - let (ctx, _) = Scrypt::from_js::( - global, - password.to_js(), - salt.to_js(), - keylen.to_js(), - options.to_js(), - JSValue::UNDEFINED, - )?; + // SAFETY: it only allocates the result and throws, and nothing borrowed is read after a throw. + let global = unsafe { heap.global() }; + let ctx = Scrypt::new(global, keylen, options)?; let (buf, bytes) = ArrayBuffer::alloc::<{ JSType::ArrayBuffer }>(global, ctx.keylen)?; - if ctx.run_task_impl(bytes).is_some() { + if ctx.run_task_impl(password, salt, bytes).is_some() { return Err(global .err( ErrorCode::CRYPTO_OPERATION_FAILED, @@ -1286,82 +1132,23 @@ pub(crate) fn scrypt_sync<'a>( Ok(buf) } -impl Argon2 { - /// Arguments arrive pre-validated from `checkArgon2()` in `crypto.ts`; - /// the checks here only defend the internal binding itself. - fn from_js( - global: &JSGlobalObject, - message_value: JSValue, - nonce_value: JSValue, - parallelism_value: JSValue, - tag_length_value: JSValue, - memory_value: JSValue, - passes_value: JSValue, - secret_value: JSValue, - associated_data_value: JSValue, - variant_value: JSValue, - ) -> JsResult { - fn copy_buffer_arg( - global: &JSGlobalObject, - value: JSValue, - name: &'static [u8], - ) -> JsResult> { - let Some(buf) = value.as_array_buffer(global) else { - return Err(global.throw_invalid_argument_type_value( - name, - b"ArrayBuffer, Buffer, TypedArray, or DataView", - value, - )); - }; - let bytes = buf.byte_slice(); - let mut copy = Vec::new(); - if copy.try_reserve_exact(bytes.len()).is_err() { - return Err(global.throw_out_of_memory()); - } - copy.extend_from_slice(bytes); - Ok(copy) - } - - let parallelism = validators::validate_uint32( - global, - parallelism_value, - format_args!("parameters.parallelism"), - true, - )?; - let tag_length = validators::validate_uint32( - global, - tag_length_value, - format_args!("parameters.tagLength"), - true, - )?; - let memory = validators::validate_uint32( - global, - memory_value, - format_args!("parameters.memory"), - true, - )?; - let passes = validators::validate_uint32( - global, - passes_value, - format_args!("parameters.passes"), - true, - )?; - let variant = match validators::validate_uint32( - global, - variant_value, - format_args!("type"), - false, - )? { +impl> Argon2 { + /// `variant` is one of `kArgon2Types` of `crypto.ts`. + fn new( + message: Bytes, + nonce: Bytes, + parallelism: u32, + tag_length: u32, + memory: u32, + passes: u32, + secret: Bytes, + associated_data: Bytes, + variant: u8, + ) -> Self { + let variant = match variant { 0 => rust_argon2::Variant::Argon2d, 1 => rust_argon2::Variant::Argon2i, - 2 => rust_argon2::Variant::Argon2id, - _ => { - return Err(global.throw_invalid_argument_type_value( - b"type", - b"a supported argon2 type", - variant_value, - )); - } + _ => rust_argon2::Variant::Argon2id, }; // rust-argon2 ignores the synthetic allocation limit, so check it @@ -1369,11 +1156,11 @@ impl Argon2 { let limit = jsc::virtual_machine::synthetic_allocation_limit(); let failed = (memory as usize).saturating_mul(1024) > limit || tag_length as usize > limit; - Ok(Argon2 { - message: copy_buffer_arg(global, message_value, b"message")?, - nonce: copy_buffer_arg(global, nonce_value, b"nonce")?, - secret: copy_buffer_arg(global, secret_value, b"secret")?, - associated_data: copy_buffer_arg(global, associated_data_value, b"associatedData")?, + Argon2 { + message, + nonce, + secret, + associated_data, parallelism, tag_length, memory, @@ -1381,7 +1168,7 @@ impl Argon2 { variant, output: Vec::new(), failed, - }) + } } fn run(&mut self) { @@ -1389,11 +1176,11 @@ impl Argon2 { return; } let config = rust_argon2::Config { - ad: &self.associated_data, + ad: self.associated_data.as_ref(), hash_length: self.tag_length, lanes: self.parallelism, mem_cost: self.memory, - secret: &self.secret, + secret: self.secret.as_ref(), // Sequential like Bun.password (pwhash.rs): lanes determine // the output, not the thread count, so results match node, // which threads lanes via OpenSSL on its worker. @@ -1402,7 +1189,7 @@ impl Argon2 { variant: self.variant, version: rust_argon2::Version::Version13, }; - match rust_argon2::hash_raw(&self.message, &self.nonce, &config) { + match rust_argon2::hash_raw(self.message.as_ref(), self.nonce.as_ref(), &config) { Ok(hash) => self.output = hash, // `checkArgon2()` already rejected every parameter rust-argon2 // rejects, so this is an allocation failure. @@ -1418,9 +1205,9 @@ pub(crate) struct Argon2Js { callback: Strong, } -/// `crypto.argon2` off the JS thread: `from_js` copied every input out of -/// JS, so the pool half owns plain memory and needs no `JsPtr`. -impl JobContext for Argon2 { +/// `crypto.argon2` off the JS thread: every input is a copy, so the pool half +/// owns plain memory and needs no `JsPtr`. +impl JobContext for Argon2> { type OffThread = Self; type Js = Argon2Js; @@ -1470,73 +1257,65 @@ impl JobContext for Argon2 { pub(crate) fn argon2<'a>( cx: &mut JsScope<'a>, - message: JsLocal<'a>, - nonce: JsLocal<'a>, - parallelism: JsLocal<'a>, - tag_length: JsLocal<'a>, - memory: JsLocal<'a>, - passes: JsLocal<'a>, - secret: JsLocal<'a>, - associated_data: JsLocal<'a>, - variant: JsLocal<'a>, - callback: JsLocal<'a>, -) -> JsResult<()> { + message: Vec, + nonce: Vec, + parallelism: u32, + tag_length: u32, + memory: u32, + passes: u32, + secret: Vec, + associated_data: Vec, + variant: u8, + callback: JsLocal<'a, Callable>, +) { let cx = cx.js_thread(); let global = cx.global(); let callback = callback.to_js(); - let ctx = Argon2::from_js( - global, - message.to_js(), - nonce.to_js(), - parallelism.to_js(), - tag_length.to_js(), - memory.to_js(), - passes.to_js(), - secret.to_js(), - associated_data.to_js(), - variant.to_js(), - )?; - let _ = validators::validate_function(global, "callback", callback)?; - Job::::schedule( + Job::>>::schedule( &cx, - ctx, + Argon2::new( + message, + nonce, + parallelism, + tag_length, + memory, + passes, + secret, + associated_data, + variant, + ), Argon2Js { callback: Strong::create(callback.with_async_context_if_needed(global), global), }, ); - Ok(()) } pub(crate) fn argon2_sync<'a>( - cx: &mut JsScope<'a>, - message: JsLocal<'a>, - nonce: JsLocal<'a>, - parallelism: JsLocal<'a>, - tag_length: JsLocal<'a>, - memory: JsLocal<'a>, - passes: JsLocal<'a>, - secret: JsLocal<'a>, - associated_data: JsLocal<'a>, - variant: JsLocal<'a>, -) -> JsResult { - let global = cx.global(); - let mut ctx = Argon2::from_js( - global, - message.to_js(), - nonce.to_js(), - parallelism.to_js(), - tag_length.to_js(), - memory.to_js(), - passes.to_js(), - secret.to_js(), - associated_data.to_js(), - variant.to_js(), - )?; + heap: &JsHeap<'a>, + message: &[u8], + nonce: &[u8], + parallelism: u32, + tag_length: u32, + memory: u32, + passes: u32, + secret: &[u8], + associated_data: &[u8], + variant: u8, +) -> bindgen::Result<'a, JsLocal<'a, BufferSource>> { + let mut ctx = Argon2::new( + message, + nonce, + parallelism, + tag_length, + memory, + passes, + secret, + associated_data, + variant, + ); ctx.run(); if ctx.failed { - let err = global.create_error_instance(format_args!("Argon2 derivation failed")); - return Err(global.throw_value(err)); + return Err(heap.error(format_args!("Argon2 derivation failed"))); } - // Ownership transfers to JSC (freed via MarkedArrayBuffer_deallocator). - JSValue::create_buffer(global, ctx.output.leak()) + heap.create_buffer_from_box(ctx.output.into_boxed_slice()) } diff --git a/src/runtime/node/node_fs_binding.bind.ts b/src/runtime/node/node_fs_binding.bind.ts index 60b9e63a6c03..55a65472fa43 100644 --- a/src/runtime/node/node_fs_binding.bind.ts +++ b/src/runtime/node/node_fs_binding.bind.ts @@ -76,17 +76,24 @@ export const NodeJSFS = b.class({ export const binding = b.value({ returns: NodeJSFS, capability: "js" }); /** `(path, options)`, parsed as `fs.rm` parses them. */ +export const RmOptions = b.dictionary({ + retryDelay: b.u32, + maxRetries: b.u32, + recursive: b.bool, + force: b.bool, +}); export const rmOptionsForTesting = b.fn({ args: { args: b.any.variadic }, - returns: b.any, + returns: RmOptions, capability: "js", throws: true, length: 2, }); export const stringToFlagsForTesting = b.fn({ args: { flags: b.any }, returns: b.i32, capability: "js", throws: true }); +export const MemfdSize = b.union({ bytes: b.f64, empty: b.undefined }); /** Without `size` it makes nothing. */ export const createMemfdForTesting = b.fn({ - args: { size: b.any.variadic }, + args: { size: MemfdSize.ifPassed }, returns: b.i32.optional, capability: "js", throws: true, diff --git a/src/runtime/node/node_fs_binding.rs b/src/runtime/node/node_fs_binding.rs index c2c747647770..768912a3ad9a 100644 --- a/src/runtime/node/node_fs_binding.rs +++ b/src/runtime/node/node_fs_binding.rs @@ -6,6 +6,7 @@ use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{JSGlobalObject, JSPromise, JSValue, JsCell, JsResult, SysErrorJsc as _}; use bun_sys_jsc::SystemErrorJsc as _; +use crate::generated_bindgen::node_fs_binding::{MemfdSize, RmOptions}; use crate::node::ThreadIsolated; use crate::node::fs::{ self, AsyncCpTask, AsyncReaddirRecursiveTask, Flavor, FsArgument, FsReturn, NodeFS, @@ -387,25 +388,17 @@ pub(crate) fn binding<'a>(cx: &mut JsScope<'a>) -> JsWrapper<'a, Binding> { pub(crate) fn rm_options_for_testing( cx: &mut JsScope<'_>, args: &[JsLocal<'_>], -) -> JsResult { +) -> JsResult { let global = cx.global(); let vm: &VirtualMachine = global.bun_vm(); let mut slice = ArgumentsSlice::init(vm, JsLocal::to_js_slice(args)); let parsed = args::Rm::from_js(global, &mut slice)?; - let obj = JSValue::create_empty_object(global, 4); - obj.put( - global, - b"retryDelay", - JSValue::js_number(parsed.retry_delay as f64), - ); - obj.put( - global, - b"maxRetries", - JSValue::js_number(parsed.max_retries as f64), - ); - obj.put(global, b"recursive", JSValue::js_boolean(parsed.recursive)); - obj.put(global, b"force", JSValue::js_boolean(parsed.force)); - Ok(obj) + Ok(RmOptions { + retry_delay: parsed.retry_delay, + max_retries: parsed.max_retries, + recursive: parsed.recursive, + force: parsed.force, + }) } /// Test-only (`bun:internal-for-testing`): run a flags value through the same @@ -429,16 +422,16 @@ pub(crate) fn string_to_flags_for_testing( pub(crate) fn create_memfd_for_testing( cx: &mut JsScope<'_>, - size: &[JsLocal<'_>], + size: Option, ) -> JsResult> { let global = cx.global(); - let Some(size_arg) = size.first() else { + let Some(size) = size else { return Ok(None); }; #[cfg(not(any(target_os = "linux", target_os = "android")))] { - let _ = size_arg; + let _ = size; return Err(global.throw(format_args!( "memfd_create is not implemented on this platform" ))); @@ -446,7 +439,10 @@ pub(crate) fn create_memfd_for_testing( #[cfg(any(target_os = "linux", target_os = "android"))] { - let size = size_arg.to_js().to_int64(); + let size = match size { + MemfdSize::Bytes(bytes) => bytes as i64, + MemfdSize::Empty => 0, + }; match bun_sys::memfd_create(c"my_memfd", bun_sys::MemfdFlags::NonExecutable) { Ok(fd) => { let _ = bun_sys::ftruncate(fd, size); diff --git a/src/runtime/node/node_net_binding.bind.ts b/src/runtime/node/node_net_binding.bind.ts index 5de1585d22ff..bb937f92b855 100644 --- a/src/runtime/node/node_net_binding.bind.ts +++ b/src/runtime/node/node_net_binding.bind.ts @@ -1,4 +1,5 @@ import * as b from "../../codegen/bindgen-lib.ts"; +import { SocketHandle } from "../socket/socket_body.bind.ts"; export default b.module({ rustPath: "crate::node::node_net_binding" }); @@ -17,4 +18,9 @@ export const setDefaultAutoSelectFamilyAttemptTimeout = b.fn({ }); export const newDetachedSocket = b.fn({ args: { isSSL: b.bool }, returns: b.any, capability: "js" }); -export const doConnect = b.fn({ args: { prev: b.any, opts: b.any }, returns: b.any, capability: "js", throws: true }); +export const doConnect = b.fn({ + args: { prev: SocketHandle, opts: b.any }, + returns: b.any, + capability: "js", + throws: true, +}); diff --git a/src/runtime/node/node_net_binding.rs b/src/runtime/node/node_net_binding.rs index 4d3c397e9d19..820142f6d851 100644 --- a/src/runtime/node/node_net_binding.rs +++ b/src/runtime/node/node_net_binding.rs @@ -9,7 +9,8 @@ use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::{self as jsc, JSGlobalObject, JSValue, JsCell, JsResult}; use bun_uws as uws; -use crate::socket::{Listener, NativeCallbacks, NewSocket, SocketFlags, TCPSocket, TLSSocket}; +use crate::generated_bindgen::socket_body::SocketHandle; +use crate::socket::{Listener, NativeCallbacks, NewSocket, SocketFlags}; static AUTO_SELECT_FAMILY_DEFAULT: AtomicBool = AtomicBool::new(true); @@ -88,11 +89,13 @@ pub(crate) fn new_detached_socket(cx: &mut JsScope<'_>, is_ssl: bool) -> JSValue pub(crate) fn do_connect<'a>( cx: &mut JsScope<'a>, - prev: JsLocal<'a>, + prev: SocketHandle<'a>, opts: JsLocal<'a>, ) -> JsResult { - let prev = prev.to_js(); - let maybe_tcp = prev.as_::(); - let maybe_tls = prev.as_::(); + let (maybe_tcp, maybe_tls) = match prev { + SocketHandle::Tcp(socket) => (Some(socket.as_ctx_ptr()), None), + SocketHandle::Tls(socket) => (None, Some(socket.as_ctx_ptr())), + SocketHandle::Ignored(_) => (None, None), + }; Listener::connect_inner(&cx.js_thread(), maybe_tcp, maybe_tls, opts.to_js()) } diff --git a/src/runtime/node/node_quic_binding.bind.ts b/src/runtime/node/node_quic_binding.bind.ts index 82d2e6f03c9d..023c73768c83 100644 --- a/src/runtime/node/node_quic_binding.bind.ts +++ b/src/runtime/node/node_quic_binding.bind.ts @@ -7,7 +7,8 @@ export const binding = b.value({ returns: b.any, capability: "js", throws: true export const setCallbacks = b.fn({ export: "Bun__NodeQuicBinding__setCallbacks", - args: { callbacks: b.any }, + args: { callbacks: b.object }, + expected: { callbacks: ["object"] }, capability: "js", throws: true, }); diff --git a/src/runtime/node/node_quic_binding.rs b/src/runtime/node/node_quic_binding.rs index 72349accaa34..0a5d3a06a3c3 100644 --- a/src/runtime/node/node_quic_binding.rs +++ b/src/runtime/node/node_quic_binding.rs @@ -2,7 +2,7 @@ //! `internalBinding('quic')` (reference: node/src/quic/*, v26.3.0). use bun_jsc::bindgen::{JsLocal, JsScope}; -use bun_jsc::{self as jsc, JSFunction, JSGlobalObject, JSValue, JsResult, StringJsc}; +use bun_jsc::{self as jsc, JSFunction, JSGlobalObject, JSObject, JSValue, JsResult, StringJsc}; use super::quic::{callbacks, endpoint, session, stream}; @@ -33,7 +33,10 @@ const QUIC_CALLBACK_NAMES: &[&str] = &[ "StreamTrailers", ]; -pub(crate) fn set_callbacks<'a>(cx: &mut JsScope<'a>, callbacks: JsLocal<'a>) -> JsResult<()> { +pub(crate) fn set_callbacks<'a>( + cx: &mut JsScope<'a>, + callbacks: JsLocal<'a, JSObject>, +) -> JsResult<()> { let global = cx.global(); // Only the first call takes effect; subsequent calls are ignored // (node/src/quic/bindingdata.cc `BindingData::SetCallbacks`). @@ -48,14 +51,6 @@ pub(crate) fn set_callbacks<'a>(cx: &mut JsScope<'a>, callbacks: JsLocal<'a>) -> return Ok(()); } let callbacks_obj = callbacks.to_js(); - if !callbacks_obj.is_object() { - return Err(global - .err( - jsc::ErrorCode::INVALID_ARG_TYPE, - format_args!("The \"callbacks\" argument must be of type object"), - ) - .throw()); - } let mut name = String::with_capacity(32); for key in QUIC_CALLBACK_NAMES { name.clear(); diff --git a/src/runtime/node/types.rs b/src/runtime/node/types.rs index 73172e1feb77..5f49abe57865 100644 --- a/src/runtime/node/types.rs +++ b/src/runtime/node/types.rs @@ -289,31 +289,6 @@ impl StringOrBuffer<'static> { unsafe { ThreadIsolated::new(Self::owned(bytes)) } } - /// Copies a `Sync`-parsed buffer's current bytes, or isolates a string, for a work-pool job. - pub(crate) fn make_thread_isolated_copy(&mut self, global: &JSGlobalObject) -> JsResult<()> { - match self { - Self::Buffer(buffer) => { - buffer.buffer = jsc::ArrayBuffer::from_typed_array(global, buffer.buffer.value); - let mut bytes = Vec::new(); - if bytes.try_reserve_exact(buffer.slice().len()).is_err() { - return Err(global.throw_out_of_memory()); - } - bytes.extend_from_slice(buffer.slice()); - global.vm().report_extra_memory(bytes.len()); - *self = Self::owned(bytes); - } - Self::String(str) => { - str.make_thread_isolated(); - *self = Self::ThreadIsolatedString(core::mem::take(str)); - } - Self::ThreadIsolatedString(_) | Self::Utf8(_) => {} - Self::PinnedBuffer(_) => { - unreachable!("make_thread_isolated_copy on a value parsed with Flavor::Async") - } - } - Ok(()) - } - /// `value` is ArrayBuffer-like (the caller checked): borrowed for `Sync`, /// [`PinnedArrayBuffer::root_read_only`] for `Async`. pub(crate) fn buffer_from_js( diff --git a/src/runtime/node/util/validators.rs b/src/runtime/node/util/validators.rs index 8be48fdf8fab..4755ea9bd73e 100644 --- a/src/runtime/node/util/validators.rs +++ b/src/runtime/node/util/validators.rs @@ -226,62 +226,6 @@ pub(crate) fn validate_string( Ok(()) } -pub(crate) fn validate_number( - global_this: &JSGlobalObject, - value: JSValue, - name: &str, - maybe_min: Option, - maybe_max: Option, -) -> JsResult { - if !value.is_number() { - return Err(global_this.throw_invalid_argument_type_value(name, "number", value)); - } - - let num: f64 = value.as_number(); - let mut valid = true; - if let Some(min) = maybe_min { - if num < min { - valid = false; - } - } - if let Some(max) = maybe_max { - if num > max { - valid = false; - } - } - if (maybe_min.is_some() || maybe_max.is_some()) && num.is_nan() { - valid = false; - } - if !valid { - if let (Some(min), Some(max)) = (maybe_min, maybe_max) { - return Err(throw_range_error( - global_this, - format_args!( - "The value of \"{}\" is out of range. It must be >= {} && <= {}. Received {}", - name, min, max, num - ), - )); - } else if let Some(min) = maybe_min { - return Err(throw_range_error( - global_this, - format_args!( - "The value of \"{}\" is out of range. It must be >= {}. Received {}", - name, min, num - ), - )); - } else if let Some(max) = maybe_max { - return Err(throw_range_error( - global_this, - format_args!( - "The value of \"{}\" is out of range. It must be <= {}. Received {}", - name, max, num - ), - )); - } - } - Ok(num) -} - pub(crate) fn validate_boolean( global_this: &JSGlobalObject, value: JSValue, diff --git a/src/runtime/server/NodeHTTPResponse.bind.ts b/src/runtime/server/NodeHTTPResponse.bind.ts index f210c0afa383..e9a0de207a6c 100644 --- a/src/runtime/server/NodeHTTPResponse.bind.ts +++ b/src/runtime/server/NodeHTTPResponse.bind.ts @@ -73,7 +73,7 @@ export const NodeHTTPResponse = b.class({ }, }); -/** The reactions to the promise that the `node:http` request handler returned. `context` is the `NodeHTTPResponse`. */ -const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +/** The reactions to the promise that the `node:http` request handler returned. */ +const reaction = { args: { value: b.any, context: NodeHTTPResponse.withValue }, capability: "js" } as const; export const onResolve = b.fn({ ...reaction, export: "Bun__NodeHTTPRequest__onResolve" }); export const onReject = b.fn({ ...reaction, export: "Bun__NodeHTTPRequest__onReject" }); diff --git a/src/runtime/server/NodeHTTPResponse.rs b/src/runtime/server/NodeHTTPResponse.rs index d0ee29eeeb2f..0760908fc505 100644 --- a/src/runtime/server/NodeHTTPResponse.rs +++ b/src/runtime/server/NodeHTTPResponse.rs @@ -1399,12 +1399,13 @@ impl NodeHTTPResponse { } } -pub(crate) fn on_resolve(cx: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { +pub(crate) fn on_resolve( + cx: &mut JsScope<'_>, + _: JsLocal<'_>, + (this, wrapper): (&NodeHTTPResponse, JsLocal<'_>), +) { scoped_log!(NodeHTTPResponse, "onResolve"); let global_object = cx.global(); - // `context` is the JSNodeHTTPResponse cell from the resolve callback. - // R-2: deref shared — `maybe_stop_reading_body`/`on_request_complete` re-enter. - let this: &NodeHTTPResponse = context.to_js().as_class_ref::().unwrap(); // `promise` non-empty is the ownership token for the server-handler ref; // `mark_request_as_done` may have already released it on abort. let had_promise = this.promise.with_mut(|p| { @@ -1412,7 +1413,7 @@ pub(crate) fn on_resolve(cx: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal< p.deinit(); had }); - this.maybe_stop_reading_body(context.to_js()); + this.maybe_stop_reading_body(wrapper.to_js()); let flags = this.flags.get(); if !flags.contains(Flags::REQUEST_HAS_COMPLETED) && !this.is_socket_closed_or_closing() { @@ -1439,11 +1440,12 @@ pub(crate) fn on_resolve(cx: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal< } } -pub(crate) fn on_reject(cx: &mut JsScope<'_>, err: JsLocal<'_>, context: JsLocal<'_>) { +pub(crate) fn on_reject( + cx: &mut JsScope<'_>, + err: JsLocal<'_>, + (this, wrapper): (&NodeHTTPResponse, JsLocal<'_>), +) { let global_object = cx.global(); - // `context` is the JSNodeHTTPResponse cell from the reject callback. - // R-2: deref shared — `maybe_stop_reading_body`/`on_request_complete` re-enter. - let this: &NodeHTTPResponse = context.to_js().as_class_ref::().unwrap(); // `promise` non-empty is the ownership token for the server-handler ref; // `mark_request_as_done` may have already released it on abort. let had_promise = this.promise.with_mut(|p| { @@ -1451,7 +1453,7 @@ pub(crate) fn on_reject(cx: &mut JsScope<'_>, err: JsLocal<'_>, context: JsLocal p.deinit(); had }); - this.maybe_stop_reading_body(context.to_js()); + this.maybe_stop_reading_body(wrapper.to_js()); let flags = this.flags.get(); if !flags.contains(Flags::REQUEST_HAS_COMPLETED) diff --git a/src/runtime/server/RequestContext.bind.ts b/src/runtime/server/RequestContext.bind.ts index 9148b6f09abd..d81f7913710e 100644 --- a/src/runtime/server/RequestContext.bind.ts +++ b/src/runtime/server/RequestContext.bind.ts @@ -4,16 +4,18 @@ import * as b from "../../codegen/bindgen-lib.ts"; function reactions(debug: boolean, mux: boolean, ssl: boolean) { const context = `HTTPRequestContext${debug ? "Debug" : ""}${mux ? "Mux" : ""}${ssl ? "TLS" : ""}`; const server = `crate::server::${debug ? "Debug" : ""}HTTP${ssl ? "S" : ""}Server`; - const reaction = (name: string, rustName: string) => + const rustPath = `crate::server::request_context::RequestContext::<${server}, ${ssl}, ${debug}, ${mux}>`; + const reaction = (name: string, rustName: string, contextType: b.Type = b.nativePromiseContext(rustPath)) => b.fn({ export: `Bun__${context}__${name}`, - args: { value: b.any, context: b.any }, + args: { value: b.any, context: contextType }, capability: "js", - rustPath: `crate::server::request_context::RequestContext::<${server}, ${ssl}, ${debug}, ${mux}>::${rustName}`, + rustPath: `${rustPath}::${rustName}`, }); return [ reaction("onResolve", "on_resolve"), - reaction("onReject", "on_reject"), + // With no claim left to take, it takes what the cell holds. + reaction("onReject", "on_reject", b.nativePromiseContext(rustPath).withValue), reaction("onResolveStream", "on_resolve_stream"), reaction("onRejectStream", "on_reject_stream"), ]; diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index e4f9baf281f6..6813117c2cdf 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -651,12 +651,16 @@ where )); } - pub(crate) fn on_resolve(cx: &mut JsScope<'_>, result: JsLocal<'_>, cell: JsLocal<'_>) { + pub(crate) fn on_resolve( + cx: &mut JsScope<'_>, + result: JsLocal<'_>, + context: Option>, + ) { ctx_log!("onResolve"); let global = cx.global(); let result = result.to_js(); - let Some(ctx) = NativePromiseContext::take::(cell.to_js()) else { + let Some(ctx) = context else { // A termination path (abort, end, upgrade) reclaimed the cell's // claim; the context may already be gone. Self::discard_response_body(global, result); @@ -1054,7 +1058,11 @@ where report_unhandled_error(vm, global, unhandled); } - pub(crate) fn on_reject(cx: &mut JsScope<'_>, reason: JsLocal<'_>, cell: JsLocal<'_>) { + pub(crate) fn on_reject( + cx: &mut JsScope<'_>, + reason: JsLocal<'_>, + (context, cell): (Option>, JsLocal<'_>), + ) { ctx_log!("onReject"); // Pass the rejection reason through verbatim (including `null` and @@ -1065,7 +1073,7 @@ where } else { reason.to_js() }; - let Some(ctx) = NativePromiseContext::take::(cell.to_js()) else { + let Some(ctx) = context else { // A termination path (abort, end, upgrade) reclaimed the cell's // claim; the context may already be gone. Only an upgrade leaves // the server wrapper in the cell to report the error through. @@ -2937,9 +2945,13 @@ where self.end_stream(self.should_close_connection()); } - pub(crate) fn on_resolve_stream(_: &mut JsScope<'_>, _: JsLocal<'_>, cell: JsLocal<'_>) { + pub(crate) fn on_resolve_stream( + _: &mut JsScope<'_>, + _: JsLocal<'_>, + context: Option>, + ) { stream_log!("onResolveStream"); - let Some(req) = NativePromiseContext::take::(cell.to_js()) else { + let Some(req) = context else { return; }; let req = RequestContextRef::adopt(req.as_ptr()); @@ -2947,9 +2959,13 @@ where req.ctx().handle_resolve_stream(); } - pub(crate) fn on_reject_stream(cx: &mut JsScope<'_>, err: JsLocal<'_>, cell: JsLocal<'_>) { + pub(crate) fn on_reject_stream( + cx: &mut JsScope<'_>, + err: JsLocal<'_>, + context: Option>, + ) { stream_log!("onRejectStream"); - let Some(req) = NativePromiseContext::take::(cell.to_js()) else { + let Some(req) = context else { return; }; let req = RequestContextRef::adopt(req.as_ptr()); diff --git a/src/runtime/server/server_body.bind.ts b/src/runtime/server/server_body.bind.ts index 0964242ec25d..546d0c5ab3b9 100644 --- a/src/runtime/server/server_body.bind.ts +++ b/src/runtime/server/server_body.bind.ts @@ -2,6 +2,9 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::server::server_body" }); -const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +const reaction = { + args: { value: b.any, context: b.promisePtr("crate::server::server_body::ServePlugins") }, + capability: "js", +} as const; export const onResolvePlugins = b.fn({ ...reaction, export: "BunServe__onResolvePlugins" }); export const onRejectPlugins = b.fn({ ...reaction, export: "BunServe__onRejectPlugins" }); diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index fa95ec648fe5..f8d79e091099 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -1173,31 +1173,29 @@ impl Drop for ServePlugins { pub(crate) fn on_resolve_plugins( _: &mut JsScope<'_>, plugins_result: JsLocal<'_>, - plugins_js: JsLocal<'_>, + context: *mut ServePlugins, ) { ctx_log!("onResolve"); - let plugins = plugins_js.to_js().as_promise_ptr::(); - // SAFETY: `plugins` was heap-allocated and ref()'d before .then(); this adopts that ref. - let _guard = unsafe { RefPtr::from_raw(plugins) }; + // SAFETY: `context` was heap-allocated and ref()'d before .then(); this adopts that ref. + let _guard = unsafe { RefPtr::from_raw(context) }; plugins_result.to_js().ensure_still_alive(); // SAFETY: pointer was passed via .then() above - unsafe { &mut *plugins }.handle_on_resolve(); + unsafe { &mut *context }.handle_on_resolve(); } pub(crate) fn on_reject_plugins( cx: &mut JsScope<'_>, error_js: JsLocal<'_>, - plugin_js: JsLocal<'_>, + context: *mut ServePlugins, ) { ctx_log!("onReject"); - let plugins = plugin_js.to_js().as_promise_ptr::(); - // SAFETY: `plugins` was heap-allocated and ref()'d before .then(); this adopts that ref. - let _guard = unsafe { RefPtr::from_raw(plugins) }; + // SAFETY: `context` was heap-allocated and ref()'d before .then(); this adopts that ref. + let _guard = unsafe { RefPtr::from_raw(context) }; // SAFETY: pointer was passed via .then() above - unsafe { &mut *plugins }.handle_on_reject(cx.global(), error_js.to_js()); + unsafe { &mut *context }.handle_on_reject(cx.global(), error_js.to_js()); } /// The `options` argument of `server.upgrade(request, options)`. Each property is read once. diff --git a/src/runtime/shell/ParsedShellScript.bind.ts b/src/runtime/shell/ParsedShellScript.bind.ts index 048e2a7a38c6..e8ff858f38a4 100644 --- a/src/runtime/shell/ParsedShellScript.bind.ts +++ b/src/runtime/shell/ParsedShellScript.bind.ts @@ -14,9 +14,10 @@ export const ParsedShellScript = b.class({ }, }); +/** `strings` and `templateArgs` are array-likes that are read in turn, one element of each at a time. */ export const createParsedShellScript = b.fn({ args: { strings: b.any, templateArgs: b.any }, - returns: b.any, + returns: ParsedShellScript, capability: "js", throws: true, }); diff --git a/src/runtime/shell/shell_body.bind.ts b/src/runtime/shell/shell_body.bind.ts index 492b27d29057..1aa3bc3a1ace 100644 --- a/src/runtime/shell/shell_body.bind.ts +++ b/src/runtime/shell/shell_body.bind.ts @@ -2,18 +2,14 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::shell::shell_body::testing_apis" }); +/** `strings` and `templateArgs` are array-likes that are read in turn, one element of each at a time. */ const template = { args: { strings: b.any, templateArgs: b.any }, - returns: b.any, + returns: b.string.utf8, capability: "js", throws: true, } as const; -export const disabledOnThisPlatform = b.fn({ - args: { builtin: b.any }, - returns: b.bool, - capability: "js", - throws: true, -}); +export const disabledOnThisPlatform = b.fn({ args: { builtin: b.string.loose.utf8 }, returns: b.bool }); export const shellLex = b.fn(template); export const shellParse = b.fn(template); diff --git a/src/runtime/shell/shell_body.rs b/src/runtime/shell/shell_body.rs index 6d0aafd46ce7..8b5134f7e366 100644 --- a/src/runtime/shell/shell_body.rs +++ b/src/runtime/shell/shell_body.rs @@ -12,8 +12,6 @@ use bun_core::String as BunString; use bun_core::ZStr; use bun_core::strings; use bun_jsc::EncodedSliceJsc as _; -use bun_jsc::StringJsc as _; -use bun_jsc::bun_string_jsc; use bun_jsc::{ self as jsc, JSArrayIterator, JSGlobalObject, JSValue, JsResult, MarkedArgumentBuffer, }; @@ -659,28 +657,19 @@ pub(crate) mod testing_apis { use super::*; use bun_jsc::bindgen::{JsLocal, JsScope}; - pub(crate) fn disabled_on_this_platform<'a>( - cx: &mut JsScope<'a>, - builtin: JsLocal<'a>, - ) -> JsResult { + pub(crate) fn disabled_on_this_platform(builtin: &[u8]) -> bool { #[cfg(windows)] { - let _ = (cx, builtin); - return Ok(false); + let _ = builtin; + false } #[cfg(not(windows))] { - let bunstr = builtin.to_js().to_bun_string(cx.global())?; - let utf8str = bunstr.to_utf8(); - - for disabled in crate::shell::builtin::Kind::DISABLED_ON_POSIX { - // `strum::IntoStaticStr` would yield the PascalCase variant name - // ("Cp"), so use `Kind::as_str` for the lowercase name. - if utf8str.slice() == disabled.as_str().as_bytes() { - return Ok(true); - } - } - Ok(false) + // `strum::IntoStaticStr` would yield the PascalCase variant name + // ("Cp"), so use `Kind::as_str` for the lowercase name. + crate::shell::builtin::Kind::DISABLED_ON_POSIX + .iter() + .any(|disabled| builtin == disabled.as_str().as_bytes()) } } @@ -688,7 +677,7 @@ pub(crate) mod testing_apis { cx: &mut JsScope<'a>, strings: JsLocal<'a>, template_args: JsLocal<'a>, - ) -> JsResult { + ) -> JsResult { let global = cx.global(); MarkedArgumentBuffer::new(|buf| { shell_lex_impl(global, strings.to_js(), template_args.to_js(), buf) @@ -700,7 +689,7 @@ pub(crate) mod testing_apis { string_args: JSValue, template_args_js: JSValue, marked_argument_buffer: &mut MarkedArgumentBuffer, - ) -> JsResult { + ) -> JsResult { let arena = Bump::new(); let mut template_args = template_args_js.array_iterator(global)?; @@ -743,9 +732,7 @@ pub(crate) mod testing_apis { test_tokens.push(test_tok); } - let str = format!("{}", test::tokens_json_fmt(&test_tokens[..])); - let bun_str = BunString::from_bytes(str.as_bytes()); - bun_str.to_js(global) + Ok(format!("{}", test::tokens_json_fmt(&test_tokens[..]))) } /// Testing API: parse the shell template-string arguments and return the @@ -755,7 +742,7 @@ pub(crate) mod testing_apis { cx: &mut JsScope<'a>, strings: JsLocal<'a>, template_args: JsLocal<'a>, - ) -> JsResult { + ) -> JsResult { let global = cx.global(); MarkedArgumentBuffer::new(|buf| { shell_parse_impl(global, strings.to_js(), template_args.to_js(), buf) @@ -767,7 +754,7 @@ pub(crate) mod testing_apis { string_args: JSValue, template_args_js: JSValue, marked_argument_buffer: &mut MarkedArgumentBuffer, - ) -> JsResult { + ) -> JsResult { let arena = Bump::new(); let mut template_args = template_args_js.array_iterator(global)?; @@ -812,10 +799,9 @@ pub(crate) mod testing_apis { // `crate::shell::ast::Script` is a `'static`-erased alias of // `bun_shell_parser::ast::Script`, so it formats directly. - let str = format!( + Ok(format!( "{}", bun_shell_parser::json_fmt::script_json_fmt(&script_ast) - ); - bun_string_jsc::create_utf8_for_js(global, str.as_bytes()) + )) } } diff --git a/src/runtime/socket/Listener.bind.ts b/src/runtime/socket/Listener.bind.ts index 160e124060f4..42438d484b9d 100644 --- a/src/runtime/socket/Listener.bind.ts +++ b/src/runtime/socket/Listener.bind.ts @@ -23,9 +23,11 @@ export const Listener = b.class({ }, }); -const fromOptions = { args: { options: b.any }, returns: b.any, capability: "js", throws: true } as const; -export const connect = b.fn({ ...fromOptions, rustPath: "crate::socket::Listener::connect" }); -export const listen = b.fn({ ...fromOptions, rustPath: "crate::socket::Listener::listen" }); +/** `options` is what `SocketConfig` takes. */ +const fromOptions = { args: { options: b.any }, capability: "js", throws: true } as const; +/** Returns a promise of the socket. */ +export const connect = b.fn({ ...fromOptions, returns: b.any, rustPath: "crate::socket::Listener::connect" }); +export const listen = b.fn({ ...fromOptions, returns: Listener, rustPath: "crate::socket::Listener::listen" }); export const addServerName = b.fn({ args: { listener: Listener, hostname: b.string.utf8, tls: b.any }, diff --git a/src/runtime/socket/UpgradedDuplex.bind.ts b/src/runtime/socket/UpgradedDuplex.bind.ts index 5022b7091db4..6a80e011e984 100644 --- a/src/runtime/socket/UpgradedDuplex.bind.ts +++ b/src/runtime/socket/UpgradedDuplex.bind.ts @@ -5,10 +5,12 @@ export default b.module({ rustPath: "crate::socket::upgraded_duplex" }); // The listeners that `get_js_handlers` makes. Each carries its `UpgradedDuplex` as function data. const listener = { passCallee: true, capability: "js" } as const; +/** What is not a buffer is reported to the socket's `error` handler, not thrown. */ +export const DuplexChunk = b.union({ nothing: b.null, bytes: b.bufferSource.allowShared, other: b.any }); export const onReceivedData = b.fn({ ...listener, export: "Bun__UpgradedDuplex__onReceivedData", - args: { data: b.any }, + args: { data: DuplexChunk }, length: 0, }); export const onEnd = b.fn({ ...listener, export: "Bun__UpgradedDuplex__onEnd" }); diff --git a/src/runtime/socket/UpgradedDuplex.rs b/src/runtime/socket/UpgradedDuplex.rs index 238f98d616b2..582b00308e62 100644 --- a/src/runtime/socket/UpgradedDuplex.rs +++ b/src/runtime/socket/UpgradedDuplex.rs @@ -23,7 +23,7 @@ use bun_uws::{us_bun_verify_error_t, uws_callback}; use super::DuplexUpgradeContext; use super::ssl_wrapper::SSLWrapper; use crate::generated_bindgen::socket_body::js_TLSSocket; -use crate::generated_bindgen::upgraded_duplex as listeners; +use crate::generated_bindgen::upgraded_duplex::{self as listeners, DuplexChunk}; use crate::timer::{ElTimespec, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag}; bun_output::declare_scope!(UpgradedDuplex, visible); @@ -793,34 +793,36 @@ impl Drop for UpgradedDuplex { // installed by `get_js_handlers`; `teardown` clears it before the storage is // freed, so a non-null data pointer is live for the call. -pub(crate) fn on_received_data<'a>(cx: &mut JsScope<'a>, callee: JsLocal<'a>, data: JsLocal<'a>) { +pub(crate) fn on_received_data<'a>( + cx: &mut JsScope<'a>, + callee: JsLocal<'a>, + data: DuplexChunk<'a>, +) { bun_output::scoped_log!(UpgradedDuplex, "onReceivedData"); - let global = cx.global(); - let data_arg = data.to_js(); - if let Some(self_ptr) = host_fn::get_function_data(callee.to_js()) { // SAFETY: see the note above. let this = unsafe { &*self_ptr.cast::() }; let _keep_alive = this.keep_alive(); if !this.origin.get().is_empty() { - if data_arg.is_empty_or_undefined_or_null() { - return; - } - if let Some(array_buffer) = data_arg.as_array_buffer(global) { - // yay we can read the data - let payload = array_buffer.slice(); - this.on_internal_receive_data(payload); - } else { - // node.js errors in this case with the same error, lets keep it consistent - let error_value = global - .err( - bun_jsc::ErrorCode::STREAM_WRAP, - format_args!("Stream has StringDecoder set or is in objectMode"), - ) - .to_js(); - error_value.ensure_still_alive(); - (this.handlers.on_error)(this.handlers.ctx, error_value); + match data { + DuplexChunk::Nothing => {} + // The bytes are copied before a handler of the socket runs. + DuplexChunk::Bytes(buffer) => buffer.with_bytes(&cx.heap, |payload| { + this.on_internal_receive_data(payload); + }), + DuplexChunk::Other(_) => { + // node.js errors in this case with the same error, lets keep it consistent + let error_value = cx + .global() + .err( + bun_jsc::ErrorCode::STREAM_WRAP, + format_args!("Stream has StringDecoder set or is in objectMode"), + ) + .to_js(); + error_value.ensure_still_alive(); + (this.handlers.on_error)(this.handlers.ctx, error_value); + } } } } diff --git a/src/runtime/socket/socket_body.bind.ts b/src/runtime/socket/socket_body.bind.ts index 4fc4fc5dde33..4603a985b747 100644 --- a/src/runtime/socket/socket_body.bind.ts +++ b/src/runtime/socket/socket_body.bind.ts @@ -1,4 +1,6 @@ import * as b from "../../codegen/bindgen-lib.ts"; +import { ServerWebSocket } from "../server/server.bind.ts"; +import { SocketHandler } from "./SocketConfig.bind.ts"; export default b.module({ rustPath: "crate::socket::socket_body" }); @@ -107,11 +109,22 @@ export const checkServerIdentityInHandshake = b.fn({ args: { socket: SocketHandl export const firstFlightBeforeFin = b.fn({ args: { socket: SocketHandle } }); export const isNamedPipeSocket = b.fn({ args: { socket: SocketHandle }, returns: b.bool }); export const getBufferedAmount = b.fn({ args: { socket: SocketHandle }, returns: b.usize }); -export const upgradeDuplexToTLS = b.fn({ ...method, args: { duplex: b.any, options: b.any } }); +/** `tls` is `true` or what `SSLConfig` takes, and can hold the `secureContext`. */ +export const DuplexTLSOptions = b.dictionary({ + socket: SocketHandler, + isServer: b.bool.loose.default(false), + secureContext: b.any.emptyStringAsNull, + tls: b.any.emptyStringAsNull, + data: b.any, +}); +/** Returns the `TLSSocket` and the listeners for the duplex's `data`, `end`, `drain` and `close`. */ +export const upgradeDuplexToTLS = b.fn({ ...method, args: { duplex: b.any, options: DuplexTLSOptions } }); // `bun:internal-for-testing` export const createSocketPair = b.fn({ ...js, returns: b.array(b.i32) }); -export const setSocketOptions = b.fn({ ...js, args: { socket: b.any, option: b.any, size: b.any } }); +/** `other` is a `WebSocket` or the native handle of a node:http server socket. */ +export const AnySocket = b.union({ tcp: TCPSocket, tls: TLSSocket, serverWebSocket: ServerWebSocket, other: b.any }); +export const setSocketOptions = b.fn({ ...js, args: { socket: AnySocket, option: b.i32, size: b.i32 } }); const testing = (name: string) => `crate::socket::socket_body::testing_apis::${name}`; export const socketFaultInjectionAvailable = b.fn({ returns: b.bool, @@ -126,9 +139,52 @@ export const duplexUpgradeContextLiveCount = b.fn({ rustPath: testing("duplex_upgrade_context_live_count"), }); export const clearSocketFaults = b.fn({ ...js, rustPath: testing("clear_socket_faults") }); +/** `socket`, `close` and `shutdown` have no hook in bsd.c: a rule for one would never fire. */ +export const SocketFaultSyscall = b.enumeration([ + "recv", + "send", + "writev", + "sendmsg", + "recvmsg", + "connect", + "accept", + "ssl_loop_buffer", + "poll_start", + "session_buffer", + "ssl_write", +]); +export const SocketFaultAction = b.enumeration(["errno", "short", "zero", "excess", "none"]); +export const SocketFaultErrnoName = b.enumeration([ + "ECONNRESET", + "EPIPE", + "ETIMEDOUT", + "ECONNREFUSED", + "EAGAIN", + "EWOULDBLOCK", + "EINTR", + "ENOBUFS", + "ENOMEM", + "EBADF", + "EINVAL", + "ENETUNREACH", + "EHOSTUNREACH", + "EPROTOTYPE", +]); +export const SocketFaultErrno = b.union({ name: SocketFaultErrnoName, code: b.i32 }); +const count = b.i32.wrap.emptyStringAsNull; +export const SocketFaultRule = b.dictionary({ + syscall: SocketFaultSyscall, + action: SocketFaultAction, + /** `errno` is a macro in C++. */ + errno: { type: SocketFaultErrno.emptyStringAsNull, rustName: "error" }, + bytes: count, + fd: count, + after: count, + repeat: count, +}); export const setSocketFault = b.fn({ ...js, - args: { rule: b.any }, + args: { rule: SocketFaultRule }, returns: b.bool, rustPath: testing("set_socket_fault"), }); diff --git a/src/runtime/socket/socket_body.rs b/src/runtime/socket/socket_body.rs index 384a940241de..7a7c2a8526f8 100644 --- a/src/runtime/socket/socket_body.rs +++ b/src/runtime/socket/socket_body.rs @@ -4,7 +4,9 @@ use core::cell::Cell; use core::ffi::{c_int, c_uint, c_void}; use core::ptr::{self, NonNull}; -use crate::generated_bindgen::socket_body::{BunSocket, SocketHandle, js_TCPSocket, js_TLSSocket}; +use crate::generated_bindgen::socket_body::{ + AnySocket, BunSocket, DuplexTLSOptions, SocketHandle, js_TCPSocket, js_TLSSocket, +}; use bun_core::EncodedSlice; use bun_io::KeepAlive; use bun_jsc::EncodedSliceJsc as _; @@ -99,7 +101,6 @@ use std::rc::Rc; mod tls_socket_functions; use crate::api::bun::h2_frame_parser::H2FrameParser; use crate::api::bun_secure_context::SecureContext; -use crate::server::ServerWebSocket; use crate::socket::udp_socket::get_us_error; bun_output::declare_scope!(Socket, visible); @@ -4432,7 +4433,7 @@ pub(crate) fn first_flight_before_fin(socket: SocketHandle<'_>) { pub(crate) fn upgrade_duplex_to_tls<'a>( cx: &mut JsScope<'a>, duplex: JsLocal<'a>, - options: JsLocal<'a>, + options: &DuplexTLSOptions<'a>, ) -> JsResult { jsc::mark_binding!(); let global = cx.global(); @@ -4440,31 +4441,19 @@ pub(crate) fn upgrade_duplex_to_tls<'a>( let context = global.bun_vm().context_of_caller_no_frame(); let duplex = duplex.to_js(); - let opts = options.to_js(); // TODO: do better type checking if duplex.is_empty_or_undefined_or_null() { return Err(global.throw(format_args!("Expected a Duplex instance"))); } - if opts.is_empty_or_undefined_or_null() || opts.is_boolean() || !opts.is_object() { - return Err(global.throw(format_args!("Expected options object"))); - } - - let socket_obj = opts - .get(global, "socket")? - .ok_or_else(|| global.throw(format_args!("Expected \"socket\" option")))?; - - let mut is_server = false; - if let Some(is_server_val) = opts.get_truthy(global, "isServer")? { - is_server = is_server_val.to_boolean(); - } + let is_server = options.is_server; // `DuplexServer` mode makes `TLSSocket.isServer()` report the server role // for ALPN without claiming a listener parent — these handlers have none, // so `mark_inactive` must take the client path. The TLS direction itself is // controlled by DuplexUpgradeContext.mode. - let handlers = Handlers::from_js( + let handlers = Handlers::from_generated( global, - socket_obj, + &options.socket, if is_server { crate::socket::SocketMode::DuplexServer } else { @@ -4481,10 +4470,11 @@ pub(crate) fn upgrade_duplex_to_tls<'a>( // may also pass it top-level. Same lookup as `upgradeTLS` above. let mut owned_ctx: Option = None; let sc_js: JSValue = 'blk: { - if let Some(v) = opts.get_truthy(global, "secureContext")? { - break 'blk v; + if let Some(v) = options.secure_context { + break 'blk v.to_js(); } - if let Some(t) = opts.get_truthy(global, "tls")? { + if let Some(t) = options.tls { + let t = t.to_js(); if t.is_object() { if let Some(v) = t.get_truthy(global, "secureContext")? { break 'blk v; @@ -4508,7 +4498,8 @@ pub(crate) fn upgrade_duplex_to_tls<'a>( // wrapper, not the SSL_CTX) and as the build source when no SecureContext. let mut ssl_opts: Option = None; // Drop frees ssl_opts on error. - if let Some(tls) = opts.get_truthy(global, "tls")? { + if let Some(tls) = options.tls { + let tls = tls.to_js(); if !tls.is_boolean() { ssl_opts = SSLConfig::from_js(handlers.vm, global, tls, is_server)?; } else if tls.to_boolean() { @@ -4520,11 +4511,11 @@ pub(crate) fn upgrade_duplex_to_tls<'a>( } let socket_config: Option<&SSLConfig> = ssl_opts.as_ref(); - let mut default_data = JSValue::ZERO; - if let Some(v) = opts.fast_get(global, jsc::BuiltinName::Data)? { - default_data = v; - default_data.ensure_still_alive(); - } + let default_data = if options.data.is_undefined() { + JSValue::ZERO + } else { + options.data.to_js() + }; let reject_unauthorized = upgrade_reject_policy( handlers.vm, @@ -4762,28 +4753,26 @@ unsafe extern "C" { /// `setSocketOptions(socket, 1 | 2, size)` of `bun:internal-for-testing`: SO_SNDBUF (1) or /// SO_RCVBUF (2). -pub(crate) fn set_socket_options<'a>( - cx: &mut JsScope<'a>, - socket: JsLocal<'a>, - option: JsLocal<'a>, - size: JsLocal<'a>, +pub(crate) fn set_socket_options( + cx: &mut JsScope<'_>, + socket: AnySocket<'_>, + option: i32, + size: i32, ) -> JsResult<()> { let global = cx.global(); - let target = socket.to_js(); - let socket = if let Some(socket) = target.as_class_ref::() { - socket.socket.get().socket.get() - } else if let Some(socket) = target.as_class_ref::() { - socket.socket.get().socket.get() - } else if let Some(websocket) = target.as_class_ref::() { - websocket.us_socket() - } else { - NonNull::new(Bun__usSocketForTesting(target)).map(NonNull::as_ptr) + let socket = match socket { + AnySocket::Tcp(socket) => socket.socket.get().socket.get(), + AnySocket::Tls(socket) => socket.socket.get().socket.get(), + AnySocket::ServerWebSocket(websocket) => websocket.us_socket(), + AnySocket::Other(other) => { + NonNull::new(Bun__usSocketForTesting(other.to_js())).map(NonNull::as_ptr) + } }; let Some(socket) = socket else { return Err(global.throw(format_args!("Expected a connected socket"))); }; - let is_recv = match option.to_js().to_int32() { + let is_recv = match option { 1 => false, 2 => true, _ => { @@ -4794,11 +4783,7 @@ pub(crate) fn set_socket_options<'a>( }; let mut value: c_int = 0; - let res = uws::us_socket_t::opaque_mut(socket).buffer_size( - is_recv, - size.to_js().to_int32(), - &mut value, - ); + let res = uws::us_socket_t::opaque_mut(socket).buffer_size(is_recv, size, &mut value); if let Some(err) = get_us_error::(res, sys::Tag::setsockopt) { return Err(global.throw_value(err.to_js(global))); } @@ -4808,6 +4793,9 @@ pub(crate) fn set_socket_options<'a>( pub(crate) mod testing_apis { use super::*; + #[cfg(socket_fault_injection)] + use crate::generated_bindgen::socket_body::SocketFaultErrnoName; + use crate::generated_bindgen::socket_body::SocketFaultRule; pub(crate) fn socket_fault_injection_available() -> bool { cfg!(socket_fault_injection) @@ -4845,7 +4833,7 @@ pub(crate) mod testing_apis { ))) } - pub(crate) fn set_socket_fault<'a>(cx: &mut JsScope<'a>, rule: JsLocal<'a>) -> JsResult { + pub(crate) fn set_socket_fault(cx: &mut JsScope<'_>, rule: &SocketFaultRule) -> JsResult { jsc::mark_binding!(); let global = cx.global(); #[cfg(not(socket_fault_injection))] @@ -4857,77 +4845,32 @@ pub(crate) mod testing_apis { } #[cfg(socket_fault_injection)] { + use crate::generated_bindgen::socket_body::{ + SocketFaultAction as Action, SocketFaultErrno as Errno, + SocketFaultSyscall as Syscall, + }; use bun_uws_sys::fault_inject as fi; - let opts = rule.to_js(); - if !opts.is_object() { - return Err(global.throw_invalid_argument_type_value("rule", "object", opts)); - } - - let syscall_str = match opts.get_truthy(global, "syscall")? { - Some(v) => v.to_bun_string(global)?, - None => { - return Err(global.throw_invalid_argument_type_value( - "rule.syscall", - "string", - JSValue::UNDEFINED, - )); - } - }; - let syscall: c_int = if syscall_str.eq_ascii(b"recv") { - fi::RECV - } else if syscall_str.eq_ascii(b"send") { - fi::SEND - } else if syscall_str.eq_ascii(b"writev") { - fi::WRITEV - } else if syscall_str.eq_ascii(b"sendmsg") { - fi::SENDMSG - } else if syscall_str.eq_ascii(b"recvmsg") { - fi::RECVMSG - } else if syscall_str.eq_ascii(b"connect") { - fi::CONNECT - } else if syscall_str.eq_ascii(b"accept") { - fi::ACCEPT - } else if syscall_str.eq_ascii(b"ssl_loop_buffer") { - fi::SSL_LOOP_BUFFER - } else if syscall_str.eq_ascii(b"poll_start") { - fi::POLL_START - } else if syscall_str.eq_ascii(b"session_buffer") { - fi::SESSION_BUFFER - } else if syscall_str.eq_ascii(b"ssl_write") { - fi::SSL_WRITE - } else { - // socket/close/shutdown have enum slots but no bsd.c hooks; - // accepting them would arm rules that can never fire. - return Err(global.throw(format_args!( - "rule.syscall must be one of: recv, send, writev, sendmsg, recvmsg, connect, accept, ssl_loop_buffer, poll_start, session_buffer, ssl_write" - ))); + let syscall: c_int = match rule.syscall { + Syscall::Recv => fi::RECV, + Syscall::Send => fi::SEND, + Syscall::Writev => fi::WRITEV, + Syscall::Sendmsg => fi::SENDMSG, + Syscall::Recvmsg => fi::RECVMSG, + Syscall::Connect => fi::CONNECT, + Syscall::Accept => fi::ACCEPT, + Syscall::SslLoopBuffer => fi::SSL_LOOP_BUFFER, + Syscall::PollStart => fi::POLL_START, + Syscall::SessionBuffer => fi::SESSION_BUFFER, + Syscall::SslWrite => fi::SSL_WRITE, }; - let action_str = match opts.get_truthy(global, "action")? { - Some(v) => v.to_bun_string(global)?, - None => { - return Err(global.throw_invalid_argument_type_value( - "rule.action", - "string", - JSValue::UNDEFINED, - )); - } - }; - let action: c_int = if action_str.eq_ascii(b"errno") { - fi::ACTION_ERRNO - } else if action_str.eq_ascii(b"short") { - fi::ACTION_SHORT - } else if action_str.eq_ascii(b"zero") { - fi::ACTION_ZERO - } else if action_str.eq_ascii(b"excess") { - fi::ACTION_EXCESS - } else if action_str.eq_ascii(b"none") { - fi::ACTION_NONE - } else { - return Err(global.throw(format_args!( - "rule.action must be one of: errno, short, zero, excess, none" - ))); + let action: c_int = match rule.action { + Action::Errno => fi::ACTION_ERRNO, + Action::Short => fi::ACTION_SHORT, + Action::Zero => fi::ACTION_ZERO, + Action::Excess => fi::ACTION_EXCESS, + Action::None => fi::ACTION_NONE, }; // "short" clamps a byte count, which only these have; arming it on any other syscall would silently never fire. @@ -4961,32 +4904,18 @@ pub(crate) mod testing_apis { ))); } - let errno_value: c_int = match opts.get_truthy(global, "errno")? { + let errno_value: c_int = match rule.error { None if action == fi::ACTION_ERRNO => { return Err(global.throw(format_args!( "rule.errno is required when action is \"errno\"" ))); } None => 0, - Some(v) if v.is_number() => v.coerce_to_i32(global)?, - Some(v) => { - let name = v.to_bun_string(global)?; - parse_errno_name(&name).ok_or_else(|| { - global.throw(format_args!( - "rule.errno: unknown errno name (use a numeric value or one of: ECONNRESET, EPIPE, ETIMEDOUT, ECONNREFUSED, EAGAIN, EWOULDBLOCK, EINTR, ENOBUFS, ENOMEM, EBADF, EINVAL, ENETUNREACH, EHOSTUNREACH, EPROTOTYPE)" - )) - })? - } + Some(Errno::Code(code)) => code, + Some(Errno::Name(name)) => errno_by_name(name), }; - let get_i32 = |key: &str, default: i32| -> JsResult { - match opts.get_truthy(global, key)? { - Some(v) => v.coerce_to_i32(global), - None => Ok(default), - } - }; - - let clamp_bytes = get_i32("bytes", 0)?; + let clamp_bytes = rule.bytes.unwrap_or(0); // A 0-byte clamp makes recv()/send() length-0 syscalls, which read // back as EOF/backpressure — silently aliasing action "zero". if action == fi::ACTION_SHORT && clamp_bytes <= 0 { @@ -4998,7 +4927,7 @@ pub(crate) mod testing_apis { // ssl_loop_buffer/session_buffer are allocations, not socket // operations: their hooks pass fd = -1, so a rule pinned to a // descriptor would arm and then silently never fire. - let target_fd = get_i32("fd", -1)?; + let target_fd = rule.fd.unwrap_or(-1); if (syscall == fi::SSL_LOOP_BUFFER || syscall == fi::SESSION_BUFFER) && target_fd != -1 { return Err(global.throw(format_args!( @@ -5015,8 +4944,8 @@ pub(crate) mod testing_apis { action, errno_value, clamp_bytes, - after_n_calls: get_i32("after", 0)?, - repeat: get_i32("repeat", 1)?, + after_n_calls: rule.after.unwrap_or(0), + repeat: rule.repeat.unwrap_or(1), target_fd, }; @@ -5027,46 +4956,45 @@ pub(crate) mod testing_apis { } #[cfg(socket_fault_injection)] - fn parse_errno_name(name: &bun_core::String) -> Option { - macro_rules! map { - ($($s:literal => $v:expr,)*) => { - $(if name.eq_ascii($s) { return Some($v as c_int); })* - }; - } + fn errno_by_name(name: SocketFaultErrnoName) -> c_int { + use SocketFaultErrnoName as Name; #[cfg(unix)] - map! { - b"ECONNRESET" => libc::ECONNRESET, - b"EPIPE" => libc::EPIPE, - b"ETIMEDOUT" => libc::ETIMEDOUT, - b"ECONNREFUSED" => libc::ECONNREFUSED, - b"EAGAIN" => libc::EAGAIN, - b"EWOULDBLOCK" => libc::EWOULDBLOCK, - b"EINTR" => libc::EINTR, - b"ENOBUFS" => libc::ENOBUFS, - b"ENOMEM" => libc::ENOMEM, - b"EBADF" => libc::EBADF, - b"EINVAL" => libc::EINVAL, - b"ENETUNREACH" => libc::ENETUNREACH, - b"EHOSTUNREACH" => libc::EHOSTUNREACH, - b"EPROTOTYPE" => libc::EPROTOTYPE, + { + match name { + Name::Econnreset => libc::ECONNRESET, + Name::Epipe => libc::EPIPE, + Name::Etimedout => libc::ETIMEDOUT, + Name::Econnrefused => libc::ECONNREFUSED, + Name::Eagain => libc::EAGAIN, + Name::Ewouldblock => libc::EWOULDBLOCK, + Name::Eintr => libc::EINTR, + Name::Enobufs => libc::ENOBUFS, + Name::Enomem => libc::ENOMEM, + Name::Ebadf => libc::EBADF, + Name::Einval => libc::EINVAL, + Name::Enetunreach => libc::ENETUNREACH, + Name::Ehostunreach => libc::EHOSTUNREACH, + Name::Eprototype => libc::EPROTOTYPE, + } } #[cfg(windows)] - map! { - b"ECONNRESET" => 10054, - b"EPIPE" => 10054, - b"ETIMEDOUT" => 10060, - b"ECONNREFUSED" => 10061, - b"EAGAIN" => 10035, - b"EWOULDBLOCK" => 10035, - b"EINTR" => 10004, - b"ENOBUFS" => 10055, - b"ENOMEM" => 10055, - b"EBADF" => 10009, - b"EINVAL" => 10022, - b"ENETUNREACH" => 10051, - b"EHOSTUNREACH" => 10065, - b"EPROTOTYPE" => 10041, - } - None + { + match name { + Name::Econnreset => 10054, + Name::Epipe => 10054, + Name::Etimedout => 10060, + Name::Econnrefused => 10061, + Name::Eagain => 10035, + Name::Ewouldblock => 10035, + Name::Eintr => 10004, + Name::Enobufs => 10055, + Name::Enomem => 10055, + Name::Ebadf => 10009, + Name::Einval => 10022, + Name::Enetunreach => 10051, + Name::Ehostunreach => 10065, + Name::Eprototype => 10041, + } + } } } diff --git a/src/runtime/socket/udp_socket.bind.ts b/src/runtime/socket/udp_socket.bind.ts index 8c00adc59585..d01b3f594556 100644 --- a/src/runtime/socket/udp_socket.bind.ts +++ b/src/runtime/socket/udp_socket.bind.ts @@ -54,6 +54,7 @@ export const UDPSocket = b.class({ }, }); +/** `options` is what `UDPSocketConfig` takes. Returns a promise of the socket. */ export const udpSocket = b.fn({ ...js, args: { options: b.any }, diff --git a/src/runtime/test_runner/BunTest.bind.ts b/src/runtime/test_runner/BunTest.bind.ts index 5764d47c15ff..e6f571446f7b 100644 --- a/src/runtime/test_runner/BunTest.bind.ts +++ b/src/runtime/test_runner/BunTest.bind.ts @@ -1,15 +1,21 @@ import * as b from "../../codegen/bindgen-lib.ts"; +import { CallbackOrOptions, DescriptionOrCallback } from "./ScopeFunctions.bind.ts"; // Not `bun_test.bind.ts`: a release build leaves out a `*_test.bind.ts`. export default b.module({ rustPath: "crate::test_runner::bun_test::js_fns::generic_hook" }); const js = { capability: "js", throws: true } as const; -/** The arguments come in any of the orders that `parse_arguments` takes. */ +/** Takes what `callAsFunction` does. */ const hook = (name: string) => b.fn({ export: `Bun__BunTest__${name}`, - args: { description: b.any, callback: b.any, options: b.any }, + args: { first: DescriptionOrCallback, second: CallbackOrOptions, third: CallbackOrOptions }, + expected: { + first: "The first argument must be a named class, named function, number, or string", + second: "The second argument must be a function, number, or object", + third: "The third argument must be a function, number, or object", + }, length: 1, ...js, }); @@ -19,8 +25,11 @@ export const afterEach = hook("afterEach"); export const afterAll = hook("afterAll"); export const onTestFinished = hook("onTestFinished"); -/** The reactions to the promise that a test or a hook returns. `context` is its `RefData`. */ -const reaction = { args: { value: b.any, context: b.any }, ...js } as const; +/** The reactions to the promise that a test or a hook returns. */ +const reaction = { + args: { value: b.any, context: b.promisePtr("crate::test_runner::bun_test::RefData") }, + ...js, +} as const; export const bunTestThen = b.fn({ ...reaction, export: "Bun__TestScope__Describe2__bunTestThen", diff --git a/src/runtime/test_runner/ScopeFunctions.bind.ts b/src/runtime/test_runner/ScopeFunctions.bind.ts index 5e9144114d7f..3b382dd9cad8 100644 --- a/src/runtime/test_runner/ScopeFunctions.bind.ts +++ b/src/runtime/test_runner/ScopeFunctions.bind.ts @@ -2,11 +2,39 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::test_runner::scope_functions" }); -/** What `test`, `describe` and their modifiers are bound functions of: `this` is the `ScopeFunctions`. */ +/** What `test`, `describe` and the hooks take in place of a timeout. */ +export const BunTestOptions = b.dictionary( + { timeout: b.f64.optional, retry: b.f64.optional, repeats: b.f64.optional }, + { allowArray: true }, +); + +/** A function here is the callback if none follows it, and else what the test is named after. */ +export const DescriptionOrCallback = b.union({ + nothing: b.null, + callable: b.callable, + text: b.string.utf8.owned, + number: b.f64, +}); +export const CallbackOrOptions = b.union({ + nothing: b.null, + callback: b.callable, + timeout: b.f64, + options: BunTestOptions, +}); + +/** + * What `test`, `describe` and their modifiers are bound functions of: `this` is the `ScopeFunctions`. + * `(description?, callback, options?)`, `(description, options, callback)` or `(callback, options?)`. + */ export const callAsFunction = b.fn({ export: "Bun__ScopeFunctions__callAsFunction", passThis: true, - args: { description: b.any, callback: b.any, options: b.any }, + args: { first: DescriptionOrCallback, second: CallbackOrOptions, third: CallbackOrOptions }, + expected: { + first: "The first argument must be a named class, named function, number, or string", + second: "The second argument must be a function, number, or object", + third: "The third argument must be a function, number, or object", + }, length: 1, capability: "js", throws: true, diff --git a/src/runtime/test_runner/ScopeFunctions.rs b/src/runtime/test_runner/ScopeFunctions.rs index da05abd4c3fc..07e958a93112 100644 --- a/src/runtime/test_runner/ScopeFunctions.rs +++ b/src/runtime/test_runner/ScopeFunctions.rs @@ -1,12 +1,14 @@ use core::fmt; use crate::test_runner::expect::JSValueTestExt; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::{Callable, JsLocal, JsScope}; use bun_jsc::{JSGlobalObject, JSValue, JsClass, JsResult}; use bun_core::String as BunString; use crate::generated_bindgen::jest::js_ScopeFunctions as js; -use crate::generated_bindgen::scope_functions::Bun__ScopeFunctions__callAsFunction; +use crate::generated_bindgen::scope_functions::{ + Bun__ScopeFunctions__callAsFunction, CallbackOrOptions, DescriptionOrCallback, +}; use crate::test_runner::bun_test::{self, BaseScopeCfg, BunTest, DescribeScope}; use crate::test_runner::bun_test::js_fns::Signature; use crate::test_runner::jest; @@ -120,12 +122,12 @@ impl ScopeFunctions { } } -pub(crate) fn call_as_function( - cx: &mut JsScope<'_>, - this_value: JsLocal<'_>, - description: JsLocal<'_>, - callback: JsLocal<'_>, - options: JsLocal<'_>, +pub(crate) fn call_as_function<'a>( + cx: &mut JsScope<'a>, + this_value: JsLocal<'a>, + first: DescriptionOrCallback<'a>, + second: CallbackOrOptions<'a>, + third: CallbackOrOptions<'a>, ) -> JsResult<()> { let _g = group_log::begin(); @@ -145,7 +147,9 @@ pub(crate) fn call_as_function( let args = parse_arguments( global, - [description.to_js(), callback.to_js(), options.to_js()], + first, + second, + third, Signature::ScopeFunctions(this), ParseArgumentsCfg { callback: callback_mode, kind: FunctionKind::TestOrDescribe }, )?; @@ -502,36 +506,26 @@ pub(crate) struct ParseArgumentsCfg { pub(crate) kind: FunctionKind, } -fn get_description( +/// What a test is called that is named after a class or a function. +fn name_of_callable( global: &JSGlobalObject, - description: JSValue, + callable: JsLocal<'_, Callable>, signature: Signature, ) -> JsResult> { - if description.is_empty() { - return Ok(Vec::new()); - } - - if description.is_class(global) { - let description_class_name = description.get_class_name(global)?; + let callable = callable.to_js(); + if callable.is_class(global) { + let class_name = callable.get_class_name(global)?; - if !description_class_name.is_empty() { - return Ok(description_class_name.to_owned_slice()); + if !class_name.is_empty() { + return Ok(class_name.to_owned_slice()); } - let description_name = description.get_name(global)?; - return Ok(description_name.to_owned_slice()); - } - - if description.is_function() { - let func_name = description.get_name(global)?; - if func_name.length() > 0 { - return Ok(func_name.to_owned_slice()); - } + return Ok(callable.get_name(global)?.to_owned_slice()); } - if description.is_number() || description.is_string() { - let slice = description.to_utf8(global)?; - return Ok(slice.into_vec()); + let name = callable.get_name(global)?; + if name.length() > 0 { + return Ok(name.to_owned_slice()); } Err(global.throw(format_args!( @@ -540,79 +534,38 @@ fn get_description( ))) } -pub(crate) fn parse_arguments( +pub(crate) fn parse_arguments<'a>( global: &JSGlobalObject, - [a1, a2, a3]: [JSValue; 3], + first: DescriptionOrCallback<'a>, + second: CallbackOrOptions<'a>, + third: CallbackOrOptions<'a>, signature: Signature, cfg: ParseArgumentsCfg, ) -> JsResult { - #[derive(Copy, Clone)] - enum Len { Three, Two, One, Zero } - let len: Len = if !a3.is_undefined_or_null() { - Len::Three - } else if !a2.is_undefined_or_null() { - Len::Two - } else if !a1.is_undefined_or_null() { - Len::One - } else { - Len::Zero - }; - - #[derive(Copy, Clone)] - struct DescriptionCallbackOptions { - description: JSValue, - callback: JSValue, - options: JSValue, - } - impl Default for DescriptionCallbackOptions { - fn default() -> Self { - Self { - description: JSValue::UNDEFINED, - callback: JSValue::UNDEFINED, - options: JSValue::UNDEFINED, - } - } - } - - let items: DescriptionCallbackOptions = match len { - // description, callback(fn), options(!fn) - // description, options(!fn), callback(fn) - Len::Three => { - if a2.is_function() { - DescriptionCallbackOptions { description: a1, callback: a2, options: a3 } - } else { - DescriptionCallbackOptions { description: a1, callback: a3, options: a2 } - } - } - // callback(fn), options(!fn) - // description, callback(fn) - Len::Two => { - if a1.is_function() && !a2.is_function() { - DescriptionCallbackOptions { callback: a1, options: a2, ..Default::default() } - } else { - DescriptionCallbackOptions { description: a1, callback: a2, ..Default::default() } - } - } + let (description, callback, options) = match (first, second, third) { + // callback + // callback, options + ( + DescriptionOrCallback::Callable(callback), + options @ (CallbackOrOptions::Nothing | CallbackOrOptions::Timeout(_) | CallbackOrOptions::Options(_)), + CallbackOrOptions::Nothing, + ) => (DescriptionOrCallback::Nothing, CallbackOrOptions::Callback(callback), options), // description - // callback(fn) - Len::One => { - if a1.is_function() { - DescriptionCallbackOptions { callback: a1, ..Default::default() } - } else { - DescriptionCallbackOptions { description: a1, ..Default::default() } - } - } - Len::Zero => DescriptionCallbackOptions::default(), + // description, callback + // description, callback, options + (description, callback @ CallbackOrOptions::Callback(_), options) + | (description, callback, options @ CallbackOrOptions::Nothing) => (description, callback, options), + // description, options, callback + (description, options, callback) => (description, callback, options), }; - let (description, callback, options) = (items.description, items.callback, items.options); - let result_callback: Option = if cfg.callback != CallbackMode::Require && callback.is_undefined_or_null() { - None - } else if callback.is_function() { - Some(callback.with_async_context_if_needed(global)) - } else { - let ordinal = if cfg.kind == FunctionKind::Hook { "first" } else { "second" }; - return Err(global.throw(format_args!("{} expects a function as the {} argument", signature, ordinal))); + let result_callback: Option = match callback { + CallbackOrOptions::Callback(callback) => Some(callback.to_js().with_async_context_if_needed(global)), + CallbackOrOptions::Nothing if cfg.callback != CallbackMode::Require => None, + _ => { + let ordinal = if cfg.kind == FunctionKind::Hook { "first" } else { "second" }; + return Err(global.throw(format_args!("{} expects a function as the {} argument", signature, ordinal))); + } }; let mut result = ParseArgumentsResult { @@ -620,53 +573,36 @@ pub(crate) fn parse_arguments( callback: result_callback, options: ParseArgumentsOptions::default(), }; - // `result` cleanup handled by Drop on early return. let mut timeout_option: Option = None; - if options.is_number() { - timeout_option = Some(options.as_number()); - } else if options.is_function() { - return Err(global.throw(format_args!( - "{}() expects options to be a number or object, not a function", - signature - ))); - } else if options.is_object() { - if let Some(timeout) = options.get(global, "timeout")? { - if !timeout.is_number() { - return Err(global.throw(format_args!("{}() expects timeout to be a number", signature))); - } - timeout_option = Some(timeout.as_number()); + match options { + CallbackOrOptions::Nothing => {} + CallbackOrOptions::Timeout(timeout) => timeout_option = Some(timeout), + CallbackOrOptions::Callback(_) => { + return Err(global.throw(format_args!( + "{}() expects options to be a number or object, not a function", + signature + ))); } - if let Some(retries) = options.get(global, "retry")? { - if !retries.is_number() { - return Err(global.throw(format_args!("{}() expects retry to be a number", signature))); - } + CallbackOrOptions::Options(options) => { + timeout_option = options.timeout; // Lossy cast: Rust `as` saturates on overflow/NaN. - result.options.retry = Some(retries.as_number() as u32); - } - if let Some(repeats) = options.get(global, "repeats")? { - if !repeats.is_number() { - return Err(global.throw(format_args!("{}() expects repeats to be a number", signature))); - } - if result.options.retry.is_some() && result.options.retry.unwrap() != 0 { - return Err(global.throw(format_args!("{}(): Cannot set both retry and repeats", signature))); + result.options.retry = options.retry.map(|retry| retry as u32); + if let Some(repeats) = options.repeats { + if result.options.retry.is_some() && result.options.retry.unwrap() != 0 { + return Err(global.throw(format_args!("{}(): Cannot set both retry and repeats", signature))); + } + result.options.repeats = repeats as u32; } - result.options.repeats = repeats.as_number() as u32; } - } else if options.is_undefined_or_null() { - // no options - } else { - return Err(global.throw(format_args!( - "{}() expects a number, object, or undefined as the third argument", - signature - ))); } - result.description = if description.is_undefined_or_null() { - None - } else { - Some(get_description(global, description, signature)?) + result.description = match description { + DescriptionOrCallback::Nothing => None, + DescriptionOrCallback::Callable(callable) => Some(name_of_callable(global, callable, signature)?), + DescriptionOrCallback::Text(text) => Some(text.into_vec()), + DescriptionOrCallback::Number(number) => Some(bun_core::fmt::FormatDouble::dtoa(&mut [0; 124], number).to_vec()), }; if result.options.retry.is_none() { diff --git a/src/runtime/test_runner/bun_test.rs b/src/runtime/test_runner/bun_test.rs index bcf8e44297bd..12f56ac2c567 100644 --- a/src/runtime/test_runner/bun_test.rs +++ b/src/runtime/test_runner/bun_test.rs @@ -66,6 +66,7 @@ pub(crate) use super::done_callback::DoneCallback; pub(crate) mod js_fns { use super::*; + use crate::generated_bindgen::scope_functions::{CallbackOrOptions, DescriptionOrCallback}; #[derive(Clone, Copy)] pub(crate) enum Signature<'a> { @@ -159,10 +160,12 @@ pub(crate) mod js_fns { } } - pub(crate) fn generic_hook_impl( + pub(crate) fn generic_hook_impl<'a>( tag: GenericHookTag, global_this: &JSGlobalObject, - arguments: [JSValue; 3], + first: DescriptionOrCallback<'a>, + second: CallbackOrOptions<'a>, + third: CallbackOrOptions<'a>, ) -> JsResult<()> { let _g = group_begin!(); // Run the body in a closure so every error exit funnels through the @@ -174,7 +177,9 @@ pub(crate) mod js_fns { let args = ScopeFunctions::parse_arguments( global_this, - arguments, + first, + second, + third, Signature::Str(sig_bytes), ScopeFunctions::ParseArgumentsCfg { callback: ScopeFunctions::CallbackMode::Require, kind: ScopeFunctions::FunctionKind::Hook }, )?; @@ -328,17 +333,13 @@ pub(crate) mod js_fns { use super::*; macro_rules! hook { ($name:ident, $tag:ident) => { - pub(crate) fn $name( - cx: &mut JsScope<'_>, - description: JsLocal<'_>, - callback: JsLocal<'_>, - options: JsLocal<'_>, + pub(crate) fn $name<'a>( + cx: &mut JsScope<'a>, + first: DescriptionOrCallback<'a>, + second: CallbackOrOptions<'a>, + third: CallbackOrOptions<'a>, ) -> JsResult<()> { - super::generic_hook_impl( - GenericHookTag::$tag, - cx.global(), - [description.to_js(), callback.to_js(), options.to_js()], - ) + super::generic_hook_impl(GenericHookTag::$tag, cx.global(), first, second, third) } }; } @@ -742,18 +743,13 @@ impl BunTest { fn bun_test_then_or_catch( global_this: &JSGlobalObject, result: JSValue, - this_ptr: JSValue, + raw_ref: *mut RefData, is_catch: bool, ) -> JsResult<()> { let _g = group_begin!(); - if this_ptr.is_empty_or_undefined_or_null() { - return Ok(()); - } - - let raw_ref: *mut RefData = this_ptr.as_promise_ptr::(); - // SAFETY: `raw_ref` was produced by `RefPtr::into_raw` in `run_test_callback` - // and round-tripped via `as_promise_ptr`; we adopt the +1 it carried. + // SAFETY: `raw_ref` was produced by `RefPtr::into_raw` in `run_test_callback`; + // we adopt the +1 it carried. let refdata = unsafe { RefPtr::from_raw(raw_ref) }; // Remove the pending_then_refs entry first so it never holds a freed `RefData`. if let Some(runner) = Jest::runner() { @@ -788,17 +784,17 @@ impl BunTest { pub(crate) fn bun_test_then( cx: &mut JsScope<'_>, value: JsLocal<'_>, - context: JsLocal<'_>, + context: *mut RefData, ) -> JsResult<()> { - Self::bun_test_then_or_catch(cx.global(), value.to_js(), context.to_js(), false) + Self::bun_test_then_or_catch(cx.global(), value.to_js(), context, false) } pub(crate) fn bun_test_catch( cx: &mut JsScope<'_>, value: JsLocal<'_>, - context: JsLocal<'_>, + context: *mut RefData, ) -> JsResult<()> { - Self::bun_test_then_or_catch(cx.global(), value.to_js(), context.to_js(), true) + Self::bun_test_then_or_catch(cx.global(), value.to_js(), context, true) } pub(crate) fn bun_test_done_callback( diff --git a/src/runtime/test_runner/jest.bind.ts b/src/runtime/test_runner/jest.bind.ts index 71045966db83..49974802fa62 100644 --- a/src/runtime/test_runner/jest.bind.ts +++ b/src/runtime/test_runner/jest.bind.ts @@ -7,14 +7,15 @@ export const nodeTestMarkResult = b.fn({ args: { todo: b.bool, done: b.any } }); const js = { capability: "js", throws: true } as const; +/** Outside of `bun test`, and in a preload, `Bun.jest` takes anything. */ +export const TestFilePath = b.union({ path: b.string.utf8, other: b.any }); /** `Bun.jest(path)` is what `import "bun:test"` gives the file at `path`. */ -export const jest = b.fn({ args: { path: b.any }, returns: b.any, ...js }); +export const jest = b.fn({ args: { path: TestFilePath }, returns: b.any, ...js }); /** Also `jest.setTimeout`. */ export const setDefaultTimeout = b.fn({ export: "Bun__Jest__setDefaultTimeout", - args: { milliseconds: b.any }, - capability: "heap", - throws: true, + args: { milliseconds: b.f64 }, + expected: { milliseconds: "setTimeout() expects a number (milliseconds)" }, }); /** The bound `this` of the `done` a test callback takes. */ diff --git a/src/runtime/test_runner/jest.rs b/src/runtime/test_runner/jest.rs index c601c7c78de0..3c9660df1954 100644 --- a/src/runtime/test_runner/jest.rs +++ b/src/runtime/test_runner/jest.rs @@ -5,7 +5,7 @@ use crate::cli::command::TestOptions; use crate::cli::test_command::CommandLineReporter; use bun_collections::{ArrayHashMap, MultiArrayList}; use bun_core::Output; -use bun_jsc::bindgen::{JsHeap, JsLocal, JsScope}; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::bun_string_jsc; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{ @@ -19,7 +19,7 @@ use crate::generated_bindgen::bun_test::{ Bun__BunTest__beforeEach, Bun__BunTest__onTestFinished, }; use crate::generated_bindgen::expect::{js_Expect, js_ExpectTypeOf}; -use crate::generated_bindgen::jest::Bun__Jest__setDefaultTimeout; +use crate::generated_bindgen::jest::{Bun__Jest__setDefaultTimeout, TestFilePath}; use super::scope_functions::{create_bound, Mode as ScopeKind}; use super::snapshot::Snapshots; use super::timers::fake_timers; @@ -473,25 +473,21 @@ pub(crate) mod Jest { } /// `Bun.jest(path)` -pub(crate) fn jest(cx: &mut JsScope<'_>, path: JsLocal<'_>) -> JsResult { +pub(crate) fn jest(cx: &mut JsScope<'_>, path: TestFilePath<'_>) -> JsResult { let global_object = cx.global(); let vm = global_object.bun_vm(); if vm.is_in_preload || Jest::runner().is_none() { // in preload, no arguments needed } else { - let path = path.to_js(); - - if !path.is_string() { + let TestFilePath::Path(path) = path else { return Err(global_object.throw(format_args!("Bun.jest() expects a string filename"))); - } - let str = path.to_utf8(global_object)?; - let slice = str.slice(); + }; - if !bun_paths::is_absolute(slice) { + if !bun_paths::is_absolute(path) { return Err(global_object.throw(format_args!( "Bun.jest() expects an absolute file path, got '{}'", - bstr::BStr::new(slice) + bstr::BStr::new(path) ))); } } @@ -502,23 +498,12 @@ pub(crate) fn jest(cx: &mut JsScope<'_>, path: JsLocal<'_>) -> JsResult } /// `setDefaultTimeout(milliseconds)` and `jest.setTimeout(milliseconds)` -pub(crate) fn set_default_timeout(heap: &JsHeap<'_>, milliseconds: JsLocal<'_>) -> JsResult<()> { - // SAFETY: `ToInt32` of a number runs no JavaScript. - let global_object = unsafe { heap.global() }; - let milliseconds = milliseconds.to_js(); - if !milliseconds.is_number() { - return Err( - global_object.throw(format_args!("setTimeout() expects a number (milliseconds)")) - ); - } - - let timeout_ms: u32 = u32::try_from(milliseconds.coerce::(global_object)?.max(0)).unwrap(); +pub(crate) fn set_default_timeout(milliseconds: f64) { + let timeout_ms: u32 = u32::try_from((milliseconds as i32).max(0)).unwrap(); if let Some(test_runner) = Jest::runner() { test_runner.default_timeout_override = timeout_ms; } - - Ok(()) } /// Reached only from `node:test`, through `$bindgen` rather than the diff --git a/src/runtime/test_runner/timers/FakeTimers.bind.ts b/src/runtime/test_runner/timers/FakeTimers.bind.ts index 89fd8a88e878..e22e64e9f41c 100644 --- a/src/runtime/test_runner/timers/FakeTimers.bind.ts +++ b/src/runtime/test_runner/timers/FakeTimers.bind.ts @@ -5,18 +5,23 @@ export default b.module({ rustPath: "crate::test_runner::timers::fake_timers" }) /** Returns `this`, which is `jest` or `vi`. */ const chainable = { passThis: true, returns: b.any, capability: "js", throws: true } as const; +export const FakeTimersOptions = b.dictionary({ now: b.f64.allowDate.optional }, { allowArray: true }); +/** Jest 26 took `"modern"` or `"legacy"`. */ +export const FakeTimersArgument = b.union({ implementation: b.string.utf8, options: FakeTimersOptions }); + export const useFakeTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__useFakeTimers", - args: { options: b.any }, - length: 0, + args: { options: FakeTimersArgument.optional }, + expected: { options: "useFakeTimers() expects an options object" }, }); export const useRealTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__useRealTimers" }); export const advanceTimersToNextTimer = b.fn({ ...chainable, export: "Bun__FakeTimers__advanceTimersToNextTimer" }); export const advanceTimersByTime = b.fn({ ...chainable, export: "Bun__FakeTimers__advanceTimersByTime", - args: { milliseconds: b.any }, + args: { milliseconds: b.f64 }, + expected: { milliseconds: "advanceTimersByTime() expects a number of milliseconds" }, }); export const runOnlyPendingTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__runOnlyPendingTimers" }); export const runAllTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__runAllTimers" }); diff --git a/src/runtime/test_runner/timers/FakeTimers.rs b/src/runtime/test_runner/timers/FakeTimers.rs index d907aa11a171..3f29c6dcd87a 100644 --- a/src/runtime/test_runner/timers/FakeTimers.rs +++ b/src/runtime/test_runner/timers/FakeTimers.rs @@ -6,7 +6,7 @@ use crate::generated_bindgen::fake_timers::{ Bun__FakeTimers__advanceTimersByTime, Bun__FakeTimers__advanceTimersToNextTimer, Bun__FakeTimers__clearAllTimers, Bun__FakeTimers__getTimerCount, Bun__FakeTimers__isFakeTimers, Bun__FakeTimers__runAllTimers, Bun__FakeTimers__runOnlyPendingTimers, - Bun__FakeTimers__useFakeTimers, Bun__FakeTimers__useRealTimers, + Bun__FakeTimers__useFakeTimers, Bun__FakeTimers__useRealTimers, FakeTimersArgument, }; use crate::jsc::virtual_machine::VirtualMachine; use crate::timer::{ @@ -341,38 +341,23 @@ fn set_fake_timer_marker(global: &JSGlobalObject, enabled: bool) -> JsResult<()> pub(crate) fn use_fake_timers( cx: &mut JsScope<'_>, this_value: JsLocal<'_>, - options: JsLocal<'_>, + options: Option>, ) -> JsResult { let global = cx.global(); // SAFETY: FFI call into C++ JSMock let mut js_now = JSMock__getCurrentUnixTimeMs(); - // Check if options object was provided - let options_value = options.to_js(); - if !options_value.is_undefined() { - if options_value.is_string() { - // Jest 26 compat: useFakeTimers("modern" | "legacy") is a no-op. - } else if !options_value.is_object() { + // Jest 26 compat: useFakeTimers("modern" | "legacy") is a no-op. + if let Some(FakeTimersArgument::Options(options)) = options + && let Some(now) = options.now + { + // NaN is `JSGlobalObject::overridenDateNow`'s "no override" sentinel. + if !now.is_finite() { return Err(global.throw_invalid_arguments(format_args!( - "useFakeTimers() expects an options object" + "'now' must be a finite number or a valid Date" ))); - } else if let Some(now) = options_value.get(global, "now")? { - if now.is_number() { - js_now = now.as_number(); - } else if now.is_date() { - js_now = now.get_unix_timestamp(); - } else { - return Err(global.throw_invalid_arguments(format_args!( - "'now' must be a number or Date" - ))); - } - // NaN is `JSGlobalObject::overridenDateNow`'s "no override" sentinel. - if !js_now.is_finite() { - return Err(global.throw_invalid_arguments(format_args!( - "'now' must be a finite number or a valid Date" - ))); - } } + js_now = now; } // Set setTimeout.clock = true to signal that fake timers are enabled. @@ -422,35 +407,28 @@ pub(crate) fn advance_timers_to_next_timer( pub(crate) fn advance_timers_by_time( cx: &mut JsScope<'_>, this_value: JsLocal<'_>, - milliseconds: JsLocal<'_>, + milliseconds: f64, ) -> JsResult { let global = cx.global(); error_unless_fake_timers(global)?; - let arg = milliseconds.to_js(); - if !arg.is_number() { - return Err(global.throw_invalid_arguments(format_args!( - "advanceTimersByTime() expects a number of milliseconds" - ))); - } // SAFETY: per-thread `timer::All`, live for the VM lifetime. let Some(current) = (unsafe { (*timer_all()).fake_timers.now }) else { return Err(global.throw_invalid_arguments(format_args!( "Fake timers not initialized. Initialize with useFakeTimers() first." ))); }; - let arg_number = arg.as_number(); let max_advance = u32::MAX; - if arg_number.is_nan() || arg_number < 0.0 || arg_number > max_advance as f64 { + if milliseconds.is_nan() || milliseconds < 0.0 || milliseconds > max_advance as f64 { return Err(global.throw_invalid_arguments(format_args!( "advanceTimersByTime() ms is out of range. It must be >= 0 and <= {}. Received {:.0}", - max_advance, arg_number + max_advance, milliseconds ))); } // When advanceTimersByTime(0) is called, advance by 1ms to fire setTimeout(fn, 0) timers. // This is because setTimeout(fn, 0) is internally scheduled with a 1ms delay per HTML spec, // and Jest/testing-library expect advanceTimersByTime(0) to fire such "immediate" timers. - let effective_advance = if arg_number == 0.0 { 1.0 } else { arg_number }; + let effective_advance = if milliseconds == 0.0 { 1.0 } else { milliseconds }; let target = current.add_ms_float(effective_advance); let advanced = FakeTimers::execute_until(global, target); diff --git a/src/runtime/timer/Timer.bind.ts b/src/runtime/timer/Timer.bind.ts index 6b04863bf9d0..b0fd6eadd427 100644 --- a/src/runtime/timer/Timer.bind.ts +++ b/src/runtime/timer/Timer.bind.ts @@ -4,7 +4,7 @@ export default b.module({ rustPath: "crate::timer::timer::internal_bindings" }); export const timerClockMs = b.fn({ returns: b.f64 }); export const monotonicNowMs = b.fn({ returns: b.f64 }); -export const setNextTimerId = b.fn({ args: { id: b.any }, capability: "js", throws: true }); +export const setNextTimerId = b.fn({ args: { id: b.u64.optional }, length: 1, capability: "heap", throws: true }); const chainable = { returns: b.self, passThis: true, invalidThis: "undefined" } as const; const ref = { ...chainable, rustName: "do_ref" } as const; diff --git a/src/runtime/timer/Timer.rs b/src/runtime/timer/Timer.rs index f295761688e1..d649a78bcc53 100644 --- a/src/runtime/timer/Timer.rs +++ b/src/runtime/timer/Timer.rs @@ -9,7 +9,7 @@ use bun_core::String as BunString; use bun_core::{Timespec, TimespecMockMode}; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::{self, JsHeap}; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{JSGlobalObject, JSValue, JsClass as _, JsResult, StringJsc as _}; use bun_uws::Loop as UwsLoop; @@ -581,17 +581,29 @@ pub(crate) mod internal_bindings { } /// Implements `timerInternals.setNextTimerId(id)`. The id can only increase, so ids never repeat. - pub(crate) fn set_next_timer_id<'a>(cx: &mut JsScope<'a>, id: JsLocal<'a>) -> JsResult<()> { + pub(crate) fn set_next_timer_id<'a>( + heap: &JsHeap<'a>, + id: Option, + ) -> bindgen::Result<'a, ()> { + let Some(id) = id else { return Ok(()) }; let current = timer_all_mut().last_id; - let id = cx.global().validate_integer_range::( - id.to_js(), - current, - bun_jsc::IntegerRange { - min: i128::from(current), - max: i128::from(All::MAX_ID), - field_name: b"id", - }, - )?; + if id < current { + return Err(heap.error_with_code( + bun_jsc::ErrorCode::OUT_OF_RANGE, + format_args!( + "{}", + bun_core::fmt::out_of_range( + id as i64, + bun_core::fmt::OutOfRangeOptions { + field_name: b"id", + min: current as i64, + max: All::MAX_ID as i64, + ..Default::default() + }, + ) + ), + )); + } timer_all_mut().last_id = id; Ok(()) } diff --git a/src/runtime/webcore/Blob.bind.ts b/src/runtime/webcore/Blob.bind.ts index 5db0dca4d3ed..7e0bdbb77275 100644 --- a/src/runtime/webcore/Blob.bind.ts +++ b/src/runtime/webcore/Blob.bind.ts @@ -1,4 +1,5 @@ import * as b from "../../codegen/bindgen-lib.ts"; +import { BuildArtifact } from "../api/JSBundler.bind.ts"; export default b.module({ rustPath: "crate::webcore::blob" }); @@ -38,5 +39,23 @@ export const Blob = b.class({ }, }); -export const file = b.fn({ ...method, args: { path: b.any, options: b.any }, length: 1 }); +/** What `JSValue::as_class_ref::` finds a `Blob` in. */ +export const BlobOrBuildArtifact = b.union({ blob: Blob, buildArtifact: BuildArtifact }); + +/** What is not a string is not a type, and nor is `""`. A `String` object of it is the type `""`. */ +export const BlobType = b.union({ text: b.string, stringObject: b.string.allowStringObject, ignored: b.any }); +/** What `Bun.file` reads of its options, unless the path is `s3://`. */ +export const BunFileOptions = b.dictionary( + { type: BlobType, lastModified: b.f64.loose.optional }, + { allowArray: true, nonObjectAsEmpty: true, fromJS: true }, +); +/** `options` is also what an S3 file takes. */ +export const file = b.fn({ ...js, args: { path: b.any, options: b.any }, returns: Blob, length: 1 }); + +/** What `Bun.write` itself reads of its options. */ +export const BunWriteOptions = b.dictionary( + { createPath: b.bool.emptyStringAsNull, mode: b.f64.nullable }, + { allowArray: true, fromJS: true }, +); +/** `options` is also what a write to S3 takes. */ export const write = b.fn({ ...method, args: { destination: b.any, data: b.any, options: b.any }, length: 1 }); diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs index e32d42f53e0e..8eafdcde8b02 100644 --- a/src/runtime/webcore/Blob.rs +++ b/src/runtime/webcore/Blob.rs @@ -116,6 +116,9 @@ pub(crate) use bun_jsc::webcore_types::{Blob, BlobContentType, ClosingState, MAX const SERIALIZATION_VERSION: u8 = 4; pub(crate) use crate::generated_bindgen::blob::js_Blob as js; +use crate::generated_bindgen::blob::{ + BlobOrBuildArtifact, BlobType, BunFileOptions, BunWriteOptions, +}; // ────────────────────────────────────────────────────────────────────────── @@ -5018,55 +5021,35 @@ pub(crate) fn write( let mut path_or_blob = write_destination_from_js(global_this, &mut args)?; - let mut mkdirp_if_not_exists: Option = None; - let mut mode: Option = None; let options = (!options.is_undefined()).then(|| options.to_js()); - if let Some(options_object) = options { - if options_object.is_object() { - if let Some(create_directory) = options_object.get_truthy(global_this, "createPath")? { - if !create_directory.is_boolean() { - return Err(global_this.throw_invalid_argument_type( - "write", - "options.createPath", - "boolean", - )); - } - mkdirp_if_not_exists = Some(create_directory.to_boolean()); - } - if let Some(mode_value) = options_object.get(global_this, "mode")? { - if !mode_value.is_empty_or_undefined_or_null() { - if !mode_value.is_number() { - return Err(global_this.throw_invalid_argument_type( - "write", - "options.mode", - "number", - )); - } - let mode_int = mode_value.to_int64(); - if mode_int < 0 || mode_int > 0o777 { - return Err(global_this.throw_range_error( - mode_int, - jsc::RangeErrorOptions { - field_name: b"mode", - min: 0, - max: 0o777, - msg: b"", - }, - )); - } - mode = Some(mode_int as bun_sys::Mode); - } + let own_options = match options { + Some(options) if !options.is_null() => BunWriteOptions::from_js(global_this, options)?, + _ => BunWriteOptions::default(), + }; + let mode = match own_options.mode { + Some(mode) => { + let mode_int = mode as i64; + if mode_int < 0 || mode_int > 0o777 { + return Err(global_this.throw_range_error( + mode_int, + jsc::RangeErrorOptions { + field_name: b"mode", + min: 0, + max: 0o777, + msg: b"", + }, + )); } - } else if !options_object.is_empty_or_undefined_or_null() { - return Err(global_this.throw_invalid_argument_type("write", "options", "object")); + Some(mode_int as bun_sys::Mode) } - } + None => None, + }; write_file_internal( &cx, &mut path_or_blob, data.to_js(), WriteFileOptions { - mkdirp_if_not_exists, + mkdirp_if_not_exists: own_options.create_path, extra_options: options, mode, }, @@ -5331,6 +5314,15 @@ pub(crate) fn jsdom_file_construct( // `calculate_estimated_byte_size` / `estimated_size`: canonical impls live // later in this file (near `dupe`/`to_js`). Duplicates removed here. +impl<'a> BlobOrBuildArtifact<'a> { + pub(crate) fn blob(self) -> &'a Blob { + match self { + Self::Blob(blob) => blob, + Self::BuildArtifact(artifact) => &artifact.blob, + } + } +} + pub(crate) fn file( cx: &mut JsScope<'_>, path: JsLocal<'_>, @@ -5358,31 +5350,30 @@ pub(crate) fn file( let blob = Blob::find_or_create_file_from_path(&mut path, global_object, false); - if let Some(opts) = options { - if opts.is_object() { - if let Some(file_type) = opts.get_truthy(global_object, "type")? { - 'inner: { - if file_type.is_string() { - let str = file_type.to_utf8(global_object)?; - let slice = str.slice(); - if !is_valid_blob_type(slice) { - break 'inner; - } - blob.content_type_was_set.set(true); - blob.content_type.set( - match global_object.bun_vm().as_mut().mime_type(slice) { - Some(mime) => BlobContentType::from(mime), - None => BlobContentType::from_lowercased(slice), - }, - ); - } + if let Some(options) = options { + BunFileOptions::from_js(global_object, options, |options| { + let file_type = match options.r#type { + BlobType::Text(text) => Some(text).filter(|text| !text.is_empty()), + BlobType::StringObject(text) => Some(text), + BlobType::Ignored(_) => None, + }; + if let Some(file_type) = file_type { + let str = file_type.to_utf8(); + let slice = str.slice(); + if is_valid_blob_type(slice) { + blob.content_type_was_set.set(true); + blob.content_type + .set(match global_object.bun_vm().as_mut().mime_type(slice) { + Some(mime) => BlobContentType::from(mime), + None => BlobContentType::from_lowercased(slice), + }); } } - if let Some(last_modified) = opts.get(global_object, "lastModified")? { - let n = last_modified.to_number(global_object)?; + if let Some(n) = options.last_modified { blob.last_modified.set(if n.is_nan() { 0.0 } else { n }); } - } + Ok(()) + })?; } let ptr = Blob::new(blob); diff --git a/src/runtime/webcore/ByteStream.bind.ts b/src/runtime/webcore/ByteStream.bind.ts index c5eb5520008c..6b6cc12856af 100644 --- a/src/runtime/webcore/ByteStream.bind.ts +++ b/src/runtime/webcore/ByteStream.bind.ts @@ -2,4 +2,5 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::webcore::byte_stream" }); +/** `stream` is a `ReadableStream`, a class written in C++. */ export const cancelOnDrain = b.fn({ args: { stream: b.any }, capability: "js", throws: true }); diff --git a/src/runtime/webcore/Crypto.bind.ts b/src/runtime/webcore/Crypto.bind.ts index 4e65e44257af..29f70c9bcd75 100644 --- a/src/runtime/webcore/Crypto.bind.ts +++ b/src/runtime/webcore/Crypto.bind.ts @@ -21,11 +21,22 @@ export const randomUUIDv5 = b.fn({ rustPath: "crate::webcore::crypto::random_uuid_v5", }); +/** `clock` is the time now. */ +export const UUIDTimestamp = b.union({ clock: b.undefined, time: b.f64.allowDate }); + +/** Alone, what is not a string is the timestamp. Before a timestamp it is as in `UUIDEncoding`. */ +export const UUIDEncodingOrTimestamp = b.union({ + label: b.string.utf8, + clock: b.undefined, + time: b.f64.allowDate, + ignored: b.any, +}); + /** `randomUUIDv7(timestamp)` takes the timestamp. `randomUUIDv7(timestamp, undefined)` takes the clock's. */ export const randomUUIDv7 = b.fn({ - args: { args: b.any.variadic }, - returns: b.any, - capability: "js", + args: { encoding: UUIDEncodingOrTimestamp, timestamp: UUIDTimestamp.ifPassed }, + returns: EncodedUUID, + capability: "heap", throws: true, length: 2, rustPath: "crate::webcore::crypto::random_uuid_v7", diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 58985bd83434..5cf0da6c7e03 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -2,9 +2,11 @@ use bun_core::String as BunString; use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope, StringOrJsBytes}; use bun_jsc::uuid::{self, UUID, UUID5, UUID7}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{JSGlobalObject, JSType, JSValue, JsClass as _, JsResult, StringJsc}; +use bun_jsc::{JSGlobalObject, JSType, JSValue, JsClass as _}; -use crate::generated_bindgen::crypto::{EncodedUUID, UUIDEncoding}; +use crate::generated_bindgen::crypto::{ + EncodedUUID, UUIDEncoding, UUIDEncodingOrTimestamp, UUIDTimestamp, +}; use crate::node::Encoding; pub(crate) struct Crypto; @@ -104,89 +106,68 @@ fn random_data(global: &JSGlobalObject, slice: &mut [u8]) { } } -pub(crate) fn random_uuid_v7(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult { - let global = cx.global(); - let arguments = JsLocal::to_js_slice(args); - - let mut encoding_value: JSValue = JSValue::UNDEFINED; - - let encoding: Encoding = 'brk: { - if !arguments.is_empty() { - if !arguments[0].is_undefined() { - if arguments[0].is_string() { - encoding_value = arguments[0]; - break 'brk match Encoding::from_js(encoding_value, global)? { - Some(e) => e, - None => { - return Err(global - .err( - bun_jsc::ErrorCode::UNKNOWN_ENCODING, - format_args!( - "Encoding must be one of base64, base64url, hex, or buffer" - ), - ) - .throw()); - } - }; - } - } - } +fn encoding_from_label<'a>(heap: &JsHeap<'a>, label: &[u8]) -> bindgen::Result<'a, Encoding> { + Encoding::from(label).ok_or_else(|| { + heap.error_with_code( + bun_jsc::ErrorCode::UNKNOWN_ENCODING, + format_args!("Encoding must be one of base64, base64url, hex, or buffer"), + ) + }) +} - break 'brk Encoding::Hex; +pub(crate) fn random_uuid_v7<'a>( + heap: &JsHeap<'a>, + encoding: UUIDEncodingOrTimestamp<'a>, + timestamp: Option, +) -> bindgen::Result<'a, EncodedUUID<'a>> { + let (encoding, alone) = match encoding { + UUIDEncodingOrTimestamp::Label(label) => ( + encoding_from_label(heap, label)?, + Some(UUIDTimestamp::Clock), + ), + UUIDEncodingOrTimestamp::Clock => (Encoding::Hex, Some(UUIDTimestamp::Clock)), + UUIDEncodingOrTimestamp::Time(time) => (Encoding::Hex, Some(UUIDTimestamp::Time(time))), + UUIDEncodingOrTimestamp::Ignored(_) => (Encoding::Hex, None), + }; + let Some(timestamp) = timestamp.or(alone) else { + return Err(heap.invalid_arguments(format_args!("timestamp must be a number or Date"))); }; - let (timestamp, timestamp_source): (u64, uuid::TimestampSource) = 'brk: { - let timestamp_value: JSValue = if arguments.len() > 1 { - arguments[1] - } else if arguments.len() == 1 && encoding_value.is_undefined() { - arguments[0] - } else { - JSValue::UNDEFINED - }; - - if !timestamp_value.is_undefined() { + let (timestamp, timestamp_source): (u64, uuid::TimestampSource) = match timestamp { + UUIDTimestamp::Time(time) => { // UUIDv7's unix_ts_ms field is 48 bits (RFC 9562 §5.7). const MAX_TIMESTAMP: i64 = (1i64 << 48) - 1; - let range_opts = bun_jsc::RangeErrorOptions { - min: 0, - max: MAX_TIMESTAMP, - field_name: b"timestamp", - ..Default::default() - }; - if timestamp_value.is_date() { - let date = timestamp_value.get_unix_timestamp(); - if !date.is_finite() || date < 0.0 || date > MAX_TIMESTAMP as f64 { - return Err(global.throw_range_error(date, range_opts)); - } - break 'brk (date as u64, uuid::TimestampSource::Explicit); + if !(0.0..=MAX_TIMESTAMP as f64).contains(&time) { + return Err(heap.error_with_code( + bun_jsc::ErrorCode::OUT_OF_RANGE, + format_args!( + "{}", + bun_core::fmt::out_of_range( + time, + bun_jsc::RangeErrorOptions { + min: 0, + max: MAX_TIMESTAMP, + field_name: b"timestamp", + ..Default::default() + }, + ) + ), + )); } - if timestamp_value.is_number() && timestamp_value.as_number().is_nan() { - return Err(global.throw_range_error(f64::NAN, range_opts)); + if time.floor() != time { + return Err(heap.invalid_arguments(format_args!( + "The \"timestamp\" property must be of type integer. Received number" + ))); } - break 'brk ( - u64::try_from(global.validate_integer_range::( - timestamp_value, - 0, - bun_jsc::IntegerRange { - min: 0, - max: i128::from(MAX_TIMESTAMP), - field_name: b"timestamp", - ..Default::default() - }, - )?) - .unwrap(), - uuid::TimestampSource::Explicit, - ); + (time as u64, uuid::TimestampSource::Explicit) } - - break 'brk ( + UUIDTimestamp::Clock => ( u64::try_from(bun_core::time::milli_timestamp().max(0)).expect("int cast"), uuid::TimestampSource::Clock, - ); + ), }; - // SAFETY: `bun_vm()` never returns null for a Bun-owned global. - let entropy = global.bun_vm().as_mut().rare_data().entropy_slice(10); + let entropy = VirtualMachine::get().as_mut().rare_data().entropy_slice(10); let uuid = UUID7::init( timestamp, @@ -194,17 +175,22 @@ pub(crate) fn random_uuid_v7(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsRe timestamp_source, ); - if encoding == Encoding::Hex { - let (str, bytes) = BunString::create_uninitialized_latin1(36); - uuid.print( - (&mut bytes[0..36]) - .try_into() - .expect("infallible: size matches"), - ); - return str.into_js(global); + match encoding { + Encoding::Hex => { + let (str, bytes) = BunString::create_uninitialized_latin1(36); + uuid.print( + (&mut bytes[0..36]) + .try_into() + .expect("infallible: size matches"), + ); + Ok(EncodedUUID::Text(str)) + } + Encoding::Buffer => Ok(EncodedUUID::Bytes(heap.create_buffer(&uuid.bytes)?)), + encoding => Ok(EncodedUUID::Text(crate::webcore::encoding::to_bun_string( + &uuid.bytes, + encoding, + ))), } - - encoding.encode_with_max_size(global, 32, &uuid.bytes) } pub(crate) fn random_uuid_v5<'a>( @@ -214,12 +200,7 @@ pub(crate) fn random_uuid_v5<'a>( encoding: UUIDEncoding<'a>, ) -> bindgen::Result<'a, EncodedUUID<'a>> { let encoding = match encoding { - UUIDEncoding::Label(label) => Encoding::from(label).ok_or_else(|| { - heap.error_with_code( - bun_jsc::ErrorCode::UNKNOWN_ENCODING, - format_args!("Encoding must be one of base64, base64url, hex, or buffer"), - ) - })?, + UUIDEncoding::Label(label) => encoding_from_label(heap, label)?, UUIDEncoding::Ignored(_) => Encoding::Hex, }; let invalid_value = |message: &str| { diff --git a/src/runtime/webcore/FileSink.bind.ts b/src/runtime/webcore/FileSink.bind.ts index 91c91aeb75c2..14207add9ae7 100644 --- a/src/runtime/webcore/FileSink.bind.ts +++ b/src/runtime/webcore/FileSink.bind.ts @@ -4,6 +4,9 @@ export default b.module({ rustPath: "crate::webcore::file_sink" }); export const liveCount = b.fn({ returns: b.i32 }); -const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +const reaction = { + args: { value: b.any, context: b.promisePtr("crate::webcore::FileSink") }, + capability: "js", +} as const; export const onResolveStream = b.fn({ ...reaction, export: "Bun__FileSink__onResolveStream" }); export const onRejectStream = b.fn({ ...reaction, export: "Bun__FileSink__onRejectStream", throws: true }); diff --git a/src/runtime/webcore/FileSink.rs b/src/runtime/webcore/FileSink.rs index 181ee8bf68d5..b96262c8542f 100644 --- a/src/runtime/webcore/FileSink.rs +++ b/src/runtime/webcore/FileSink.rs @@ -78,7 +78,7 @@ pub struct FileSink { // `bun.ptr.RefCount(FileSink, "ref_count", deinit, .{})` — intrusive single-thread // refcount derived via #[derive(CellRefCounted)] above. `*FileSink` crosses FFI -// (JSSink wrapper, `@fieldParentPtr`, `asPromisePtr`), so this stays intrusive +// (JSSink wrapper, `@fieldParentPtr`, `b.promisePtr`), so this stays intrusive // rather than `Rc`. /// Count of live native FileSink instances. Incremented at allocation, @@ -1738,31 +1738,29 @@ impl FileSink { } } -pub(crate) fn on_resolve_stream(cx: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { +pub(crate) fn on_resolve_stream(cx: &mut JsScope<'_>, _: JsLocal<'_>, context: *mut FileSink) { bun_core::scoped_log!(FileSink, "onResolveStream"); - let this: *mut FileSink = context.to_js().as_promise_ptr::(); - // SAFETY: `this` is kept alive by the ref taken in `assign_to_stream`; this guard balances it. - let _guard = unsafe { RefPtr::from_raw(this) }; - // SAFETY: `as_promise_ptr` recovers the `*mut FileSink` stashed by `assign_to_stream`. + // SAFETY: `context` is kept alive by the ref taken in `assign_to_stream`; this guard balances it. + let _guard = unsafe { RefPtr::from_raw(context) }; + // SAFETY: `context` is the `*mut FileSink` stashed by `assign_to_stream`. unsafe { - (*this).pump_promise_ref.set(false); - (*this).handle_resolve_stream(cx.global()); + (*context).pump_promise_ref.set(false); + (*context).handle_resolve_stream(cx.global()); } } pub(crate) fn on_reject_stream( cx: &mut JsScope<'_>, err: JsLocal<'_>, - context: JsLocal<'_>, + context: *mut FileSink, ) -> JsResult<()> { bun_core::scoped_log!(FileSink, "onRejectStream"); - let this: *mut FileSink = context.to_js().as_promise_ptr::(); - // SAFETY: `this` is kept alive by the ref taken in `assign_to_stream`; this guard balances it. - let _guard = unsafe { RefPtr::from_raw(this) }; - // SAFETY: `as_promise_ptr` recovers the `*mut FileSink` stashed by `assign_to_stream`. + // SAFETY: `context` is kept alive by the ref taken in `assign_to_stream`; this guard balances it. + let _guard = unsafe { RefPtr::from_raw(context) }; + // SAFETY: `context` is the `*mut FileSink` stashed by `assign_to_stream`. unsafe { - (*this).pump_promise_ref.set(false); - (*this).handle_reject_stream(cx.global(), err.to_js()) + (*context).pump_promise_ref.set(false); + (*context).handle_reject_stream(cx.global(), err.to_js()) } } diff --git a/src/runtime/webcore/FormData.bind.ts b/src/runtime/webcore/FormData.bind.ts index 3d27f26223fb..164b13e23c19 100644 --- a/src/runtime/webcore/FormData.bind.ts +++ b/src/runtime/webcore/FormData.bind.ts @@ -1,11 +1,21 @@ import * as b from "../../codegen/bindgen-lib.ts"; +import { BlobOrBuildArtifact } from "./Blob.bind.ts"; export default b.module({ rustPath: "crate::webcore::form_data" }); -export const from = b.fn({ - args: { input: b.any, boundary: b.any }, - returns: b.any, - capability: "js", - throws: true, - length: 1, -}); +const stringOrBuffer = b.bytes({ from: ["string", "buffer"] }); +const parse = { returns: b.any, capability: "heap", throws: true } as const; + +/** Without a boundary, or with an empty one, the input is URL-encoded. A `Blob` that is a file is empty. */ +export const from = b.overloads( + { + ...parse, + args: { input: BlobOrBuildArtifact, boundary: stringOrBuffer.nullable }, + rustPath: "crate::webcore::form_data::from_blob", + }, + { + ...parse, + args: { input: stringOrBuffer, boundary: stringOrBuffer.nullable }, + rustPath: "crate::webcore::form_data::from_bytes", + }, +); diff --git a/src/runtime/webcore/FormData.rs b/src/runtime/webcore/FormData.rs index 78fca391c337..384e0d7e6d6a 100644 --- a/src/runtime/webcore/FormData.rs +++ b/src/runtime/webcore/FormData.rs @@ -1,12 +1,13 @@ //! HTML `FormData` parsing + JS bridge. use bun_core::{self, declare_scope, scoped_log}; -use bun_core::{EncodedSlice, Utf8Bytes, strings}; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_core::{EncodedSlice, strings}; +use bun_jsc::bindgen::JsHeap; use bun_jsc::{AnyPromise, DOMFormData, JSGlobalObject, JSValue, JsError, JsResult}; use bun_semver::{self, SlicedString}; use core::ffi::c_void; +use crate::generated_bindgen::blob::BlobOrBuildArtifact; use crate::webcore::Blob; use crate::webcore::BlobExt as _; @@ -102,54 +103,18 @@ impl FormData { } /// `FormData.from(input, boundary)` -pub(crate) fn from( - cx: &mut JsScope<'_>, - input: JsLocal<'_>, - boundary: JsLocal<'_>, +pub(crate) fn from_bytes( + heap: &JsHeap<'_>, + input: &[u8], + boundary: Option<&[u8]>, ) -> JsResult { - let global = cx.global(); - let (input_value, boundary_value) = (input.to_js(), boundary.to_js()); - let boundary_slice: Utf8Bytes; - - let mut encoding = Encoding::URLEncoded; - - if input_value.is_empty_or_undefined_or_null() { - return Err(global.throw_invalid_arguments(format_args!("input must not be empty"))); - } - - if !boundary_value.is_empty_or_undefined_or_null() { - if let Some(array_buffer) = boundary_value.as_array_buffer(global) { - if !array_buffer.byte_slice().is_empty() { - encoding = Encoding::Multipart(Box::from(array_buffer.byte_slice())); - } - } else if boundary_value.is_string() { - boundary_slice = boundary_value.to_utf8(global)?; - if !boundary_slice.slice().is_empty() { - encoding = Encoding::Multipart(Box::from(boundary_slice.slice())); - } - } else { - return Err(global.throw_invalid_arguments(format_args!( - "boundary must be a string or ArrayBufferView" - ))); - } - } - let input_slice: Utf8Bytes; - // Keep the `ArrayBuffer` view alive for the duration of `input`'s borrow. - let input_array_buffer; - let input: &[u8]; - - if let Some(array_buffer) = input_value.as_array_buffer(global) { - input_array_buffer = array_buffer; - input = input_array_buffer.byte_slice(); - } else if input_value.is_string() { - input_slice = input_value.to_utf8(global)?; - input = input_slice.slice(); - } else if let Some(blob) = input_value.as_class_ref::() { - input = blob.shared_view(); - } else { - return Err(global - .throw_invalid_arguments(format_args!("input must be a string or ArrayBufferView"))); - } + let encoding = match boundary { + Some(boundary) if !boundary.is_empty() => Encoding::Multipart(Box::from(boundary)), + _ => Encoding::URLEncoded, + }; + // SAFETY: parsing runs no JavaScript, and nor does making the `FormData` or appending to it. An append that throws + // ends the parse. + let global = unsafe { heap.global() }; match FormData::to_js(global, input, &encoding) { Ok(v) => Ok(v), @@ -158,6 +123,14 @@ pub(crate) fn from( } } +pub(crate) fn from_blob( + heap: &JsHeap<'_>, + input: BlobOrBuildArtifact<'_>, + boundary: Option<&[u8]>, +) -> JsResult { + from_bytes(heap, input.blob().shared_view(), boundary) +} + pub(crate) fn to_js_from_multipart_data( global: &JSGlobalObject, input: &[u8], @@ -173,15 +146,15 @@ pub(crate) fn to_js_from_multipart_data( struct Wrapper<'a> { global: &'a JSGlobalObject, form: &'a mut DOMFormData, - /// True after an append throws. The remaining entries are skipped. - threw: bool, } impl<'a> Wrapper<'a> { - fn on_entry(wrap: &mut Self, name: bun_semver::String, field: &Field<'_>, buf: &[u8]) { - if wrap.threw { - return; - } + fn on_entry( + wrap: &mut Self, + name: bun_semver::String, + field: &Field<'_>, + buf: &[u8], + ) -> JsResult<()> { let value_str: &[u8] = field.value; let key = EncodedSlice::utf8(name.slice(buf)); @@ -225,7 +198,7 @@ pub(crate) fn to_js_from_multipart_data( ); // `append_blob` dupes the content type; release this stack-local. blob.detach(); - wrap.threw = result.is_err(); + result } else { let value = EncodedSlice::utf8( // > Each part whose `Content-Disposition` header does not @@ -237,23 +210,15 @@ pub(crate) fn to_js_from_multipart_data( // > `charset` parameter. strings::without_utf8_bom(value_str), ); - wrap.threw = wrap.form.append(wrap.global, &key, &value).is_err(); + wrap.form.append(wrap.global, &key, &value) } } } { - let mut wrap = Wrapper { - global, - form, - threw: false, - }; + let mut wrap = Wrapper { global, form }; - let parsed = for_each_multipart_entry(input, boundary, &mut wrap, Wrapper::on_entry); - if wrap.threw { - return Err(crate::Error::JSError); - } - if let Err(e) = parsed { + if let Err(e) = for_each_multipart_entry(input, boundary, &mut wrap, Wrapper::on_entry) { scoped_log!(FormData, "failed to parse multipart data"); return Err(e); } @@ -266,7 +231,7 @@ pub(crate) fn for_each_multipart_entry( input: &[u8], boundary: &[u8], ctx: &mut C, - mut iterator: impl FnMut(&mut C, bun_semver::String, &Field<'_>, &[u8]), + mut iterator: impl FnMut(&mut C, bun_semver::String, &Field<'_>, &[u8]) -> JsResult<()>, ) -> crate::Result<()> { let mut slice = input; let subslicer = SlicedString::init(input, input); @@ -413,7 +378,7 @@ pub(crate) fn for_each_multipart_entry( field.filename = filename.unwrap_or_default(); field.is_file = is_file; - iterator(ctx, name, &field, input); + iterator(ctx, name, &field, input).map_err(|_| crate::Error::JSError)?; } Ok(()) diff --git a/src/runtime/webcore/ObjectURLRegistry.bind.ts b/src/runtime/webcore/ObjectURLRegistry.bind.ts index 2400416cfba0..855533533794 100644 --- a/src/runtime/webcore/ObjectURLRegistry.bind.ts +++ b/src/runtime/webcore/ObjectURLRegistry.bind.ts @@ -1,12 +1,18 @@ import * as b from "../../codegen/bindgen-lib.ts"; +import { Blob, BlobOrBuildArtifact } from "./Blob.bind.ts"; export default b.module({ rustPath: "crate::webcore::object_url_registry" }); -/** `blob` is a `Blob` or a `BuildArtifact`. */ -export const createObjectURL = b.fn({ args: { blob: b.any }, returns: b.string, capability: "heap", throws: true }); +export const createObjectURL = b.fn({ args: { blob: BlobOrBuildArtifact }, returns: b.string }); /** Without an argument it throws `ERR_MISSING_ARGS`, as in Node. */ -export const revokeObjectURL = b.fn({ args: { url: b.any.variadic }, capability: "js", throws: true, length: 1 }); +export const revokeObjectURL = b.fn({ + args: { url: b.string.allowStringObject.ifPassed }, + expected: { url: "revokeObjectURL expects a string" }, + capability: "heap", + throws: true, + length: 1, +}); /** `resolveObjectURL` of `node:buffer`. */ -export const resolveObjectURL = b.fn({ args: { url: b.string.loose }, returns: b.any, capability: "js" }); +export const resolveObjectURL = b.fn({ args: { url: b.string.loose }, returns: Blob.optional, capability: "js" }); diff --git a/src/runtime/webcore/ObjectURLRegistry.rs b/src/runtime/webcore/ObjectURLRegistry.rs index ec437350346f..8f92c2fe3a6f 100644 --- a/src/runtime/webcore/ObjectURLRegistry.rs +++ b/src/runtime/webcore/ObjectURLRegistry.rs @@ -2,11 +2,12 @@ use std::sync::OnceLock; use bun_collections::HashMap; use bun_core::strings; -use bun_jsc::bindgen::{self, JsHeap, JsLocal, JsScope}; +use bun_jsc::bindgen::{self, JsHeap, JsScope}; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{JSGlobalObject, JSValue, JsResult, UUID}; +use bun_jsc::{ErrorCode, JSGlobalObject, JSValue, UUID}; use bun_threading::Guarded; +use crate::generated_bindgen::blob::BlobOrBuildArtifact; use crate::webcore::Blob; use crate::webcore::BlobExt as _; @@ -112,35 +113,22 @@ fn uuid_from_pathname(pathname: &[u8]) -> Option { UUID::parse(pathname).ok() } -pub(crate) fn create_object_url<'a>( - heap: &JsHeap<'a>, - blob: JsLocal<'a>, -) -> bindgen::Result<'a, bun_core::String> { - let Some(blob) = blob.to_js().as_class_ref::() else { - return Err(heap.invalid_arguments(format_args!("createObjectURL expects a Blob object"))); - }; +pub(crate) fn create_object_url(blob: BlobOrBuildArtifact<'_>) -> bun_core::String { let registry = ObjectURLRegistry::singleton(); - let uuid = registry.register(VirtualMachine::get().as_mut(), blob); - Ok(bun_core::String::create_format(format_args!( - "blob:{}", - uuid - ))) + let uuid = registry.register(VirtualMachine::get().as_mut(), blob.blob()); + bun_core::String::create_format(format_args!("blob:{}", uuid)) } -pub(crate) fn revoke_object_url(cx: &mut JsScope<'_>, url: &[JsLocal<'_>]) -> JsResult<()> { - let global = cx.global(); - let Some(url) = url.first() else { - return Err(global.throw_not_enough_arguments("revokeObjectURL", 1, 0)); +pub(crate) fn revoke_object_url<'a>( + heap: &JsHeap<'a>, + url: Option, +) -> bindgen::Result<'a, ()> { + let Some(str) = url else { + return Err(heap.error_with_code( + ErrorCode::MISSING_ARGS, + format_args!("Not enough arguments to 'revokeObjectURL'. Expected 1, got 0."), + )); }; - let url = url.to_js(); - if !url.is_string() { - return Err( - global.throw_invalid_arguments(format_args!("revokeObjectURL expects a string")) - ); - } - // `is_string()` is `is_string_like()` and admits `StringObject`, so - // `to_bun_string` can still observe a user `toString` that throws. - let str = url.to_bun_string(global)?; if !str.starts_with_ascii(b"blob:") { return Ok(()); } @@ -154,20 +142,19 @@ pub(crate) fn revoke_object_url(cx: &mut JsScope<'_>, url: &[JsLocal<'_>]) -> Js Ok(()) } -pub(crate) fn resolve_object_url(cx: &mut JsScope<'_>, str: &bun_core::String) -> JSValue { +pub(crate) fn resolve_object_url(cx: &mut JsScope<'_>, url: &bun_core::String) -> Option { // Errors are ignored. // Not thrown. // https://github.com/nodejs/node/blob/2eff28fb7a93d3f672f80b582f664a7c701569fb/lib/internal/blob.js#L441 - if !str.starts_with_ascii(b"blob:") || str.length() < SPECIFIER_LEN { - return JSValue::UNDEFINED; + if !url.starts_with_ascii(b"blob:") || url.length() < SPECIFIER_LEN { + return None; } - let slice = str.to_utf8(); + let slice = url.to_utf8(); let sliced = slice.slice(); let registry = ObjectURLRegistry::singleton(); - let blob = registry.resolve_and_dupe_to_js(&sliced[b"blob:".len()..], cx.global()); - blob.unwrap_or(JSValue::UNDEFINED) + registry.resolve_and_dupe_to_js(&sliced[b"blob:".len()..], cx.global()) } const SPECIFIER_LEN: usize = b"blob:".len() + UUID::STRING_LENGTH; diff --git a/src/runtime/webcore/fetch.bind.ts b/src/runtime/webcore/fetch.bind.ts index c2b33e2d6caf..abdd85647a6b 100644 --- a/src/runtime/webcore/fetch.bind.ts +++ b/src/runtime/webcore/fetch.bind.ts @@ -3,9 +3,10 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::webcore::fetch" }); const js = { capability: "js", throws: true } as const; -const request = { args: { input: b.any, init: b.any }, returns: b.any, ...js, length: 1 } as const; +/** An `input` that is no `Request`, and whose `ToString` is no URL, is read as `init` is. */ +const request = { args: { input: b.any, init: b.any }, returns: b.any, ...js, rejects: true, length: 1 } as const; export const fetch = b.fn(request); /** What `session.fetch` of a `Bun.FetchSession` is bound from. */ export const sessionFetch = b.fn({ ...request, passThis: true, export: "Bun__Fetch__sessionFetch" }); -export const preconnect = b.fn({ args: { url: b.any }, ...js }); +export const preconnect = b.fn({ args: { url: b.string.loose }, ...js }); diff --git a/src/runtime/webcore/fetch.rs b/src/runtime/webcore/fetch.rs index a1e2292e97e9..1c70293e1e19 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -208,10 +208,10 @@ fn data_url_response(url: BunString, global_this: &JSGlobalObject) -> JSValue { // fetch.preconnect // ────────────────────────────────────────────────────────────────────────── -pub(crate) fn preconnect(cx: &mut JsScope<'_>, url: JsLocal<'_>) -> JsResult<()> { +pub(crate) fn preconnect(cx: &mut JsScope<'_>, url: &BunString) -> JsResult<()> { let global_object = cx.global(); - let url_str = jsc::URL::href_from_js(url.to_js(), global_object)?; + let url_str = bun_url::href_from_string(url); if url_str.tag() == BunStringTag::Dead { return Err(global_object @@ -313,11 +313,7 @@ pub(crate) fn fetch( input: JsLocal<'_>, init: JsLocal<'_>, ) -> JsResult { - let ctx = cx.global(); - reject_on_exception( - ctx, - fetch_impl::(ctx, input.to_js(), init.to_js(), None), - ) + fetch_impl::(cx.global(), input.to_js(), init.to_js(), None) } /// What `session.fetch` is bound from: `fetch()` with `this` as its session. @@ -327,45 +323,14 @@ pub(crate) fn session_fetch( input: JsLocal<'_>, init: JsLocal<'_>, ) -> JsResult { - let ctx = cx.global(); - reject_on_exception( - ctx, - fetch_impl::(ctx, input.to_js(), init.to_js(), Some(this_value.to_js())), + fetch_impl::( + cx.global(), + input.to_js(), + init.to_js(), + Some(this_value.to_js()), ) } -/// WHATWG fetch step 3: an exception thrown while processing `input`/`init` -/// rejects the returned promise; `fetch()` never throws synchronously. -fn reject_on_exception( - global_this: &JSGlobalObject, - result: JsResult, -) -> JsResult { - let err = match result { - Ok(v) => return Ok(v), - Err(jsc::JsError::OutOfMemory) => global_this.create_out_of_memory_error(), - // A terminated worker gets no rejected promise: its termination keeps unwinding. - Err(jsc::JsError::Thrown | jsc::JsError::Terminated) - if global_this.has_pending_termination_exception() => - { - return Err(jsc::JsError::Thrown); - } - Err(jsc::JsError::Terminated) => return Err(jsc::JsError::Terminated), - Err(jsc::JsError::Thrown) => match global_this.try_take_exception() { - Some(exc) => exc.to_error().unwrap_or(exc), - None => { - // `fetch_impl` only returns Err(Thrown) with an exception - // pending; reaching here means it was cleared, which is a bug. - debug_assert!( - false, - "fetch_impl signaled a thrown exception but none is pending" - ); - global_this.create_error_instance(format_args!("fetch() failed")) - } - }, - }; - Ok(JSPromise::rejected_promise(global_this, err).to_js()) -} - // ────────────────────────────────────────────────────────────────────────── // URLType // ────────────────────────────────────────────────────────────────────────── diff --git a/src/runtime/webcore/fetch/FetchTasklet.bind.ts b/src/runtime/webcore/fetch/FetchTasklet.bind.ts index e21e35e427fd..79528a55bf64 100644 --- a/src/runtime/webcore/fetch/FetchTasklet.bind.ts +++ b/src/runtime/webcore/fetch/FetchTasklet.bind.ts @@ -2,6 +2,9 @@ import * as b from "../../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::webcore::fetch::fetch_tasklet" }); -const reaction = { args: { value: b.any, context: b.any }, capability: "js" } as const; +const reaction = { + args: { value: b.any, context: b.promisePtr("crate::webcore::fetch::FetchTasklet") }, + capability: "js", +} as const; export const onResolveRequestStream = b.fn({ ...reaction, export: "Bun__FetchTasklet__onResolveRequestStream" }); export const onRejectRequestStream = b.fn({ ...reaction, export: "Bun__FetchTasklet__onRejectRequestStream" }); diff --git a/src/runtime/webcore/fetch/FetchTasklet.rs b/src/runtime/webcore/fetch/FetchTasklet.rs index 4ffc86bad4c2..7edd6a2280fd 100644 --- a/src/runtime/webcore/fetch/FetchTasklet.rs +++ b/src/runtime/webcore/fetch/FetchTasklet.rs @@ -2712,35 +2712,37 @@ impl FetchTasklet { } } -pub(crate) fn on_resolve_request_stream(_: &mut JsScope<'_>, _: JsLocal<'_>, context: JsLocal<'_>) { - let this: *mut FetchTasklet = context.to_js().as_promise_ptr::(); - // SAFETY: `as_promise_ptr` recovers the `*mut FetchTasklet` stashed by +pub(crate) fn on_resolve_request_stream( + _: &mut JsScope<'_>, + _: JsLocal<'_>, + context: *mut FetchTasklet, +) { + // SAFETY: `context` is the `*mut FetchTasklet` stashed by // `start_request_stream`; the `ref_()` there keeps it alive, balanced by // `write_end_request` below. Clear `sink.task` first so the sink's // `finalize()` fallback does not release a second time. unsafe { - if let Some(sink) = (*this).sink_mut() { + if let Some(sink) = (*context).sink_mut() { sink.task = None; } - (*this).write_end_request(None); + (*context).write_end_request(None); } } pub(crate) fn on_reject_request_stream( _: &mut JsScope<'_>, err: JsLocal<'_>, - context: JsLocal<'_>, + context: *mut FetchTasklet, ) { - let this: *mut FetchTasklet = context.to_js().as_promise_ptr::(); - // SAFETY: `as_promise_ptr` recovers the `*mut FetchTasklet` stashed by + // SAFETY: `context` is the `*mut FetchTasklet` stashed by // `start_request_stream`; the `ref_()` there keeps it alive, balanced by // `write_end_request` below. Clear `sink.task` first so the sink's // `finalize()` fallback does not release a second time. unsafe { - if let Some(sink) = (*this).sink_mut() { + if let Some(sink) = (*context).sink_mut() { sink.task = None; } - (*this).write_end_request(Some(err.to_js())); + (*context).write_end_request(Some(err.to_js())); } } diff --git a/src/runtime/webcore/prompt.bind.ts b/src/runtime/webcore/prompt.bind.ts index ef5ad526b6b4..9846fcb8a56a 100644 --- a/src/runtime/webcore/prompt.bind.ts +++ b/src/runtime/webcore/prompt.bind.ts @@ -3,8 +3,15 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::webcore::prompt" }); /** `alert()` shows a message of its own. `alert(undefined)` shows "undefined". */ -const dialog = { args: { args: b.any.variadic }, capability: "js", throws: true, length: 1 } as const; +const message = b.string.loose.utf8.ifPassed; -export const alert = b.fn(dialog); -export const confirm = b.fn({ ...dialog, returns: b.bool }); -export const prompt = b.fn({ ...dialog, returns: b.any }); +export const alert = b.fn({ args: { message }, length: 1 }); +export const confirm = b.fn({ args: { message }, returns: b.bool, length: 1 }); +/** An empty answer returns `defaultValue` as it is. */ +export const prompt = b.fn({ + args: { message, defaultValue: b.any.ifPassed }, + returns: b.any, + capability: "js", + throws: true, + length: 1, +}); diff --git a/src/runtime/webcore/prompt.rs b/src/runtime/webcore/prompt.rs index 35372aa71f49..8901aa958e22 100644 --- a/src/runtime/webcore/prompt.rs +++ b/src/runtime/webcore/prompt.rs @@ -8,17 +8,12 @@ use bun_jsc::EncodedSliceJsc as _; use bun_jsc::bindgen::{JsLocal, JsScope}; /// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-alert -pub(crate) fn alert(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult<()> { - let global = cx.global(); - let arguments = JsLocal::to_js_slice(args); +pub(crate) fn alert(message: Option<&[u8]>) { let output = Output::writer(); - let has_message = !arguments.is_empty(); // 2. If the method was invoked with no arguments, then let message be the empty string; otherwise, let message be the method's first argument. - if has_message { - let message = arguments[0].to_utf8(global)?; - - if !message.slice().is_empty() { + if let Some(message) = message { + if !message.is_empty() { // 3. Set message to the result of normalizing newlines given message. // * We skip step 3 because they are already done in most terminals by default. @@ -26,15 +21,15 @@ pub(crate) fn alert(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult<()> // * We just don't do this because it's not necessary. // 5. Show message to the user, treating U+000A LF as a line break. - if output.write_all(message.slice()).is_err() { + if output.write_all(message).is_err() { // 1. If we cannot show simple dialogs for this, then return. - return Ok(()); + return; } } } if output - .write_all(if has_message { + .write_all(if message.is_some() { b" [Enter] " as &[u8] } else { b"Alert [Enter] " @@ -42,7 +37,7 @@ pub(crate) fn alert(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult<()> .is_err() { // 1. If we cannot show simple dialogs for this, then return. - return Ok(()); + return; } // 6. Invoke WebDriver BiDi user prompt opened with this, "alert", and message. @@ -60,27 +55,20 @@ pub(crate) fn alert(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult<()> // 8. Invoke WebDriver BiDi user prompt closed with this and true. // * Again, not necessary in a server context. - - Ok(()) } -pub(crate) fn confirm(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult { - let global = cx.global(); - let arguments = JsLocal::to_js_slice(args); +pub(crate) fn confirm(message: Option<&[u8]>) -> bool { let output = Output::writer(); - let has_message = !arguments.is_empty(); - if has_message { + if let Some(message) = message { // 2. Set message to the result of normalizing newlines given message. // * Not pertinent to a server runtime so we will just let the terminal handle this. // 3. Set message to the result of optionally truncating message. // * Not necessary so we won't do it. - let message = arguments[0].to_utf8(global)?; - - if output.write_all(message.slice()).is_err() { + if output.write_all(message).is_err() { // 1. If we cannot show simple dialogs for this, then return false. - return Ok(false); + return false; } } @@ -88,7 +76,7 @@ pub(crate) fn confirm(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult, args: &[JsLocal<'_>]) -> JsResult, args: &[JsLocal<'_>]) -> JsResult, args: &[JsLocal<'_>]) -> JsResult return Ok(false), + b'\n' => return false, b'\r' => { let Ok(next_byte) = reader.take_byte() else { // They may have said yes, but the stdin is invalid. - return Ok(false); + return false; }; if next_byte == b'\n' { - return Ok(false); + return false; } } b'y' | b'Y' => { let Ok(next_byte) = reader.take_byte() else { // They may have said yes, but the stdin is invalid. - return Ok(false); + return false; }; if next_byte == b'\n' { // 8. If the user responded positively, return true; // otherwise, the user responded negatively: return false. - return Ok(true); + return true; } else if next_byte == b'\r' { // Check Windows style let Ok(second_byte) = reader.take_byte() else { - return Ok(false); + return false; }; if second_byte == b'\n' { - return Ok(true); + return true; } } } @@ -157,7 +145,7 @@ pub(crate) fn confirm(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult( } /// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-prompt -pub(crate) fn prompt(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult { +pub(crate) fn prompt<'a>( + cx: &mut JsScope<'a>, + message: Option<&[u8]>, + default_value: Option>, +) -> JsResult { let global = cx.global(); - let arguments = JsLocal::to_js_slice(args); let output = Output::writer(); - let has_message = !arguments.is_empty(); - let has_default = arguments.len() >= 2; // 4. Set default to the result of optionally truncating default. // * We don't really need to do this. - let default = if has_default { - arguments[1] - } else { - JSValue::NULL - }; + let default = default_value.map(JsLocal::to_js); - if has_message { + if let Some(message) = message { // 2. Set message to the result of normalizing newlines given message. // * Not pertinent to a server runtime so we will just let the terminal handle this. // 3. Set message to the result of optionally truncating message. // * Not necessary so we won't do it. - let message = arguments[0].to_utf8(global)?; - - if output.write_all(message.slice()).is_err() { + if output.write_all(message).is_err() { // 1. If we cannot show simple dialogs for this, then return null. return Ok(JSValue::NULL); } @@ -261,7 +244,7 @@ pub(crate) fn prompt(cx: &mut JsScope<'_>, args: &[JsLocal<'_>]) -> JsResult, args: &[JsLocal<'_>]) -> JsResult, args: &[JsLocal<'_>]) -> JsResult, _: JsLocal<'_>, context: JsLocal<'_>) { +pub(crate) fn on_resolve_upload_stream( + _: &mut JsScope<'_>, + _: JsLocal<'_>, + context: Option>, +) { // The cell hands back the ref `upload_stream` gave it, which `handle_resolve_stream` balances. - if let Some(this) = - crate::api::native_promise_context::take::(context.to_js()) - { + if let Some(this) = context { // SAFETY: that ref keeps the wrapper alive. unsafe { (*this.as_ptr()).handle_resolve_stream() }; } } -pub(crate) fn on_reject_upload_stream(_: &mut JsScope<'_>, err: JsLocal<'_>, context: JsLocal<'_>) { +pub(crate) fn on_reject_upload_stream( + _: &mut JsScope<'_>, + err: JsLocal<'_>, + context: Option>, +) { // As `on_resolve_upload_stream`; `handle_reject_stream` balances the ref. - if let Some(this) = - crate::api::native_promise_context::take::(context.to_js()) - { + if let Some(this) = context { // SAFETY: that ref keeps the wrapper alive. unsafe { (*this.as_ptr()).handle_reject_stream(err.to_js()) }; } diff --git a/src/sourcemap_jsc/InternalSourceMap.bind.ts b/src/sourcemap_jsc/InternalSourceMap.bind.ts index 92d9d908e764..e498d2efe7d7 100644 --- a/src/sourcemap_jsc/InternalSourceMap.bind.ts +++ b/src/sourcemap_jsc/InternalSourceMap.bind.ts @@ -25,7 +25,7 @@ export const InternalSourceMapMapping = b.dictionary({ }); export const find = b.fn({ - args: { blob: b.bytes({ from: ["buffer"] }), line: b.any, col: b.any }, + args: { blob: b.bytes({ from: ["buffer"] }), line: b.f64, col: b.f64 }, returns: InternalSourceMapMapping.nullable, capability: "heap", throws: true, diff --git a/src/sourcemap_jsc/internal_jsc.rs b/src/sourcemap_jsc/internal_jsc.rs index 6adbb2911fe0..2563e010cc0c 100644 --- a/src/sourcemap_jsc/internal_jsc.rs +++ b/src/sourcemap_jsc/internal_jsc.rs @@ -32,14 +32,14 @@ pub(crate) fn to_vlq<'a>(heap: &JsHeap<'a>, blob: &[u8]) -> bindgen::Result<'a, pub(crate) fn find<'a>( heap: &JsHeap<'a>, blob: &[u8], - line: JsLocal<'a>, - col: JsLocal<'a>, + line: f64, + col: f64, ) -> bindgen::Result<'a, Option> { if !internal_source_map::is_valid_blob(blob) { return Err(heap.error(format_args!("InternalSourceMap.find: invalid blob"))); } - let line = line.to_js().to_int32(); - let col = col.to_js().to_int32(); + let line = line as i32; + let col = col as i32; if line < 0 || col < 0 { return Ok(None); } diff --git a/src/sql_jsc/mysql.bind.ts b/src/sql_jsc/mysql.bind.ts index acdbd3da9a5b..bd0b81b12191 100644 --- a/src/sql_jsc/mysql.bind.ts +++ b/src/sql_jsc/mysql.bind.ts @@ -1,22 +1,57 @@ import * as b from "../codegen/bindgen-lib.ts"; +import { MySQLConnection } from "./mysql/JSMySQLConnection.bind.ts"; +import { MySQLQuery } from "./mysql/JSMySQLQuery.bind.ts"; -const js = { returns: b.any, capability: "js", throws: true } as const; +const js = { capability: "js", throws: true } as const; +const text = b.string.loose.utf8; +const milliseconds = b.f64.loose; export const binding = b.namespace({ init: { - args: { onResolveQuery: b.any, onRejectQuery: b.any }, + args: { onResolveQuery: b.callable, onRejectQuery: b.callable }, capability: "heap", length: 0, rustPath: "crate::mysql::my_sql_context::init", }, createQuery: { ...js, - args: { query: b.any, values: b.any, pendingValue: b.any, bigint: b.any, simple: b.any }, + args: { + query: b.string.allowStringObject.owned, + values: b.jsArray, + pendingValue: b.jsArray, + bigint: b.bool, + simple: b.bool, + }, + expected: { values: "values must be an array" }, + returns: MySQLQuery, rustPath: "crate::mysql::js_my_sql_query::JSMySQLQuery::create_query", }, + /** The arguments of `NativeCreateConnection` in `src/js/internal/sql/pool.ts`. The ones that are `b.any`, but `tls`, are for Postgres. */ createConnection: { ...js, - args: { args: b.any.variadic }, + args: { + hostname: text, + port: b.i32, + username: text, + password: text, + database: text, + sslMode: b.u8.validateInteger(0, 4), + tls: b.any, + query: b.any, + path: text, + onConnected: b.callable, + onDisconnected: b.callable, + idleTimeout: milliseconds, + connectionTimeout: milliseconds, + maxLifetime: milliseconds, + useUnnamedPreparedStatements: b.any, + allowPublicKeyRetrieval: b.bool.loose, + onRetire: b.callable.optional, + foundRows: b.bool.loose, + channelBinding: b.any, + columnTransform: b.callable.optional, + }, + returns: MySQLConnection, length: 2, rustPath: "crate::mysql::js_my_sql_connection::JSMySQLConnection::create_connection", }, diff --git a/src/sql_jsc/mysql/JSMySQLConnection.rs b/src/sql_jsc/mysql/JSMySQLConnection.rs index 60c6696d1bb6..55650fefae58 100644 --- a/src/sql_jsc/mysql/JSMySQLConnection.rs +++ b/src/sql_jsc/mysql/JSMySQLConnection.rs @@ -1,7 +1,7 @@ use core::cell::Cell; use core::ffi::c_void; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::{Callable, JsLocal, JsScope}; use crate::generated_bindgen::js_my_sql_connection::js_MySQLConnection as js; use crate::jsc::{ @@ -252,24 +252,51 @@ impl JSMySQLConnection { pub(crate) fn create_connection<'a>( cx: &mut JsScope<'a>, - args: &'a [JsLocal<'a>], + hostname: &[u8], + port: i32, + username: &[u8], + password: &[u8], + database: &[u8], + ssl_mode: u8, + tls: JsLocal<'a>, + _: JsLocal<'a>, + path: &[u8], + on_connected: JsLocal<'a, Callable>, + on_disconnected: JsLocal<'a, Callable>, + idle_timeout: f64, + connection_timeout: f64, + max_lifetime: f64, + _: JsLocal<'a>, + allow_public_key_retrieval: bool, + on_retire: Option>, + found_rows: bool, + _: JsLocal<'a>, + column_transform: Option>, ) -> JsResult { let global_object = cx.global(); let vm = global_object.bun_vm().as_mut(); // The connection is the calling script's. let context = global_object.bun_vm().context_of_caller_no_frame(); - let arguments = JsLocal::to_js_slice(args); - let argument = |i: usize| arguments.get(i).copied().unwrap_or(JSValue::UNDEFINED); - let Some(args) = ConnectionCtorArgs::parse(global_object, &mut *vm, arguments)? else { + let Some(args) = ConnectionCtorArgs::parse( + global_object, + &mut *vm, + hostname, + ssl_mode, + tls.to_js(), + idle_timeout, + connection_timeout, + max_lifetime, + )? + else { return Ok(JSValue::ZERO); }; let (secure, tls_config) = (args.secure, args.tls_config); - let check_server_identity = server_identity::callback_of(global_object, arguments[6])?; + let check_server_identity = server_identity::callback_of(global_object, tls.to_js())?; - let username = args.username_str.to_owned_slice().into_boxed_slice(); - let password = args.password_str.to_owned_slice().into_boxed_slice(); - let database = args.database_str.to_owned_slice().into_boxed_slice(); - let path = args.path_str.to_owned_slice().into_boxed_slice(); + let username: Box<[u8]> = Box::from(username); + let password: Box<[u8]> = Box::from(password); + let database: Box<[u8]> = Box::from(database); + let path: Box<[u8]> = Box::from(path); // Reject null bytes in connection parameters to prevent protocol injection // (null bytes act as field terminators in the MySQL wire protocol). @@ -284,12 +311,6 @@ impl JSMySQLConnection { } } - let on_connect = arguments[9]; - let on_close = arguments[10]; - let allow_public_key_retrieval = argument(15).to_boolean(); - let on_retire = argument(16); - let found_rows = argument(17).to_boolean(); - let ptr: *mut JSMySQLConnection = bun_core::heap::into_raw(Box::new(JSMySQLConnection { ref_count: Cell::new(1), js_value: JsCell::new(JsRef::empty()), @@ -321,8 +342,6 @@ impl JSMySQLConnection { let this = ParentRef::from(core::ptr::NonNull::new(ptr).expect("heap::into_raw non-null")); { - let hostname = args.hostname_str.to_utf8(); - // MySQL always opens plain TCP first; STARTTLS adopts into the TLS // group after the SSLRequest exchange. let group = vm.mysql_socket_group::(context); @@ -340,8 +359,8 @@ impl JSMySQLConnection { group, uws::DispatchKind::Mysql, None, - hostname.slice(), - args.port, + hostname, + port, ptr, false, ) @@ -374,14 +393,13 @@ impl JSMySQLConnection { js_value.ensure_still_alive(); this.js_value .with_mut(|r| r.set_strong(js_value, global_object)); - js::onconnect_set_cached(js_value, global_object, on_connect); - let column_transform = argument(19); - if column_transform.is_callable() { - js::column_transform_set_cached(js_value, global_object, column_transform); - } - js::onclose_set_cached(js_value, global_object, on_close); - if on_retire.is_callable() { - js::onretire_set_cached(js_value, global_object, on_retire); + js::onconnect_set_cached(js_value, global_object, on_connected.to_js()); + if let Some(column_transform) = column_transform { + js::column_transform_set_cached(js_value, global_object, column_transform.to_js()); + } + js::onclose_set_cached(js_value, global_object, on_disconnected.to_js()); + if let Some(on_retire) = on_retire { + js::onretire_set_cached(js_value, global_object, on_retire.to_js()); } // Next to `rejectUnauthorized: false` it is not asked, as in Node. if let Some(callback) = diff --git a/src/sql_jsc/mysql/JSMySQLQuery.rs b/src/sql_jsc/mysql/JSMySQLQuery.rs index f37ff6c8841a..bf527f6c5f3f 100644 --- a/src/sql_jsc/mysql/JSMySQLQuery.rs +++ b/src/sql_jsc/mysql/JSMySQLQuery.rs @@ -7,9 +7,9 @@ use crate::jsc::{ VirtualMachineSqlExt as _, bun_string_jsc, }; use crate::shared::cached_structure::Undescribed; -use crate::shared::query_ctor_args::QueryCtorArgs; -use bun_jsc::JsCell; +use crate::shared::query_ctor_args; use bun_jsc::bindgen::{JsLocal, JsScope, JsWrapper}; +use bun_jsc::{JSArray, JsCell}; use bun_ptr::{AsCtxPtr, BackRef, ParentRef, RefPtr}; use bun_sql::mysql::MySQLQueryResult; use bun_sql::mysql::protocol::any_mysql_error::{self as AnyMySQLError}; @@ -54,37 +54,21 @@ impl JSMySQLQuery { pub(crate) fn create_query<'a>( cx: &mut JsScope<'a>, - query: JsLocal<'a>, - values: JsLocal<'a>, - pending_value: JsLocal<'a>, - bigint: JsLocal<'a>, - simple: JsLocal<'a>, + query: bun_core::String, + values: JsLocal<'a, JSArray>, + pending_value: JsLocal<'a, JSArray>, + bigint: bool, + simple: bool, ) -> JsResult { let global_this = cx.global(); - let QueryCtorArgs { - query, - values, - pending_value, - bigint, - simple, - } = QueryCtorArgs::parse( - global_this, - query.to_js(), - values.to_js(), - pending_value.to_js(), - bigint.to_js(), - simple.to_js(), - )?; + let (values, pending_value) = (values.to_js(), pending_value.to_js()); + query_ctor_args::validate(global_this, &query, values, simple)?; let this_ptr = bun_core::heap::into_raw(Box::new(Self { this_value: JsCell::new(JsRef::empty()), ref_count: Cell::new(1), global_object: BackRef::new(global_this), - query: JsCell::new(MySQLQuery::init( - query.to_bun_string(global_this)?, - bigint, - simple, - )), + query: JsCell::new(MySQLQuery::init(query, bigint, simple)), undescribed: JsCell::new(Undescribed::default()), })); let this = ParentRef::from(NonNull::new(this_ptr).expect("heap::into_raw non-null")); diff --git a/src/sql_jsc/mysql/MySQLContext.rs b/src/sql_jsc/mysql/MySQLContext.rs index 60400647c9c6..382ebe636da6 100644 --- a/src/sql_jsc/mysql/MySQLContext.rs +++ b/src/sql_jsc/mysql/MySQLContext.rs @@ -1,5 +1,5 @@ use crate::jsc::{StrongOptional, VirtualMachineSqlExt as _}; -use bun_jsc::bindgen::{JsHeap, JsLocal}; +use bun_jsc::bindgen::{Callable, JsHeap, JsLocal}; #[repr(C)] #[derive(Default)] @@ -10,8 +10,8 @@ pub struct MySQLContext { pub(crate) fn init<'a>( heap: &JsHeap<'a>, - on_resolve_query: JsLocal<'a>, - on_reject_query: JsLocal<'a>, + on_resolve_query: JsLocal<'a, Callable>, + on_reject_query: JsLocal<'a, Callable>, ) { // SAFETY: setting a `Strong` runs no JavaScript. let global = unsafe { heap.global() }; diff --git a/src/sql_jsc/postgres.bind.ts b/src/sql_jsc/postgres.bind.ts index 7fe832500b5d..58d2438c739a 100644 --- a/src/sql_jsc/postgres.bind.ts +++ b/src/sql_jsc/postgres.bind.ts @@ -1,22 +1,58 @@ import * as b from "../codegen/bindgen-lib.ts"; +import { PostgresSQLConnection } from "./postgres/PostgresSQLConnection.bind.ts"; +import { PostgresSQLQuery } from "./postgres/PostgresSQLQuery.bind.ts"; -const js = { returns: b.any, capability: "js", throws: true } as const; +const js = { capability: "js", throws: true } as const; +const text = b.string.loose.utf8; +const milliseconds = b.f64.loose; export const binding = b.namespace({ init: { - args: { onResolveQuery: b.any, onRejectQuery: b.any, arrayLiteral: b.any }, + args: { onResolveQuery: b.callable, onRejectQuery: b.callable, arrayLiteral: b.callable }, capability: "heap", length: 0, rustPath: "crate::postgres::PostgresSQLContext::init", }, createQuery: { ...js, - args: { query: b.any, values: b.any, pendingValue: b.any, bigint: b.any, simple: b.any }, + args: { + query: b.string.allowStringObject.owned, + values: b.jsArray, + pendingValue: b.jsArray, + bigint: b.bool, + simple: b.bool, + }, + expected: { values: "values must be an array" }, + returns: PostgresSQLQuery, rustPath: "crate::postgres::PostgresSQLQuery::create_query", }, + /** The arguments of `NativeCreateConnection` in `src/js/internal/sql/pool.ts`. The ones that are `b.any`, but `tls`, are for MySQL. */ createConnection: { ...js, - args: { args: b.any.variadic }, + args: { + hostname: text, + port: b.i32, + username: text, + password: text, + database: text, + sslMode: b.u8.validateInteger(0, 4), + tls: b.any, + query: text, + path: text, + onConnected: b.callable, + onDisconnected: b.callable, + idleTimeout: milliseconds, + connectionTimeout: milliseconds, + maxLifetime: milliseconds, + useUnnamedPreparedStatements: b.bool, + allowPublicKeyRetrieval: b.any, + onRetire: b.callable.optional, + foundRows: b.any, + channelBinding: b.u8.validateInteger(0, 2), + columnTransform: b.callable.optional, + scramPassword: text, + }, + returns: PostgresSQLConnection, length: 2, rustPath: "crate::postgres::postgres_sql_connection::create_connection", }, diff --git a/src/sql_jsc/postgres/PostgresSQLConnection.rs b/src/sql_jsc/postgres/PostgresSQLConnection.rs index 61c94be9b65c..e265f73999e8 100644 --- a/src/sql_jsc/postgres/PostgresSQLConnection.rs +++ b/src/sql_jsc/postgres/PostgresSQLConnection.rs @@ -1,6 +1,6 @@ use bun_collections::VecExt; use bun_jsc::JsCell; -use bun_jsc::bindgen::{JsLocal, JsScope}; +use bun_jsc::bindgen::{Callable, JsLocal, JsScope}; use core::cell::Cell; use core::ffi::c_void; use core::sync::atomic::Ordering; @@ -844,42 +844,58 @@ impl PostgresSQLConnection { pub(crate) fn create_connection<'a>( cx: &mut JsScope<'a>, - args: &'a [JsLocal<'a>], + hostname: &[u8], + port: i32, + username: &[u8], + password: &[u8], + database: &[u8], + ssl_mode: u8, + tls: JsLocal<'a>, + query: &[u8], + path: &[u8], + on_connected: JsLocal<'a, Callable>, + on_disconnected: JsLocal<'a, Callable>, + idle_timeout: f64, + connection_timeout: f64, + max_lifetime: f64, + use_unnamed_prepared_statements: bool, + _: JsLocal<'a>, + on_retire: Option>, + _: JsLocal<'a>, + channel_binding: u8, + column_transform: Option>, + scram_password: &[u8], ) -> JsResult { let global_object = cx.global(); let vm = global_object.bun_vm().as_mut(); // The connection is the calling script's. let context = global_object.bun_vm().context_of_caller_no_frame(); - let arguments = JsLocal::to_js_slice(args); - let argument = |i: usize| arguments.get(i).copied().unwrap_or(JSValue::UNDEFINED); - let Some(args) = ConnectionCtorArgs::parse(global_object, &mut *vm, arguments)? else { + let Some(args) = ConnectionCtorArgs::parse( + global_object, + &mut *vm, + hostname, + ssl_mode, + tls.to_js(), + idle_timeout, + connection_timeout, + max_lifetime, + )? + else { return Ok(JSValue::ZERO); }; let (secure, tls_config) = (args.secure, args.tls_config); // Next to `rejectUnauthorized: false` it is not asked, as in Node. - let check_server_identity = server_identity::callback_of(global_object, arguments[6])? + let check_server_identity = server_identity::callback_of(global_object, tls.to_js())? .filter(|_| verifies_certificate(&tls_config, args.ssl_mode)); - let channel_binding = ChannelBinding::from_js(global_object, argument(18))?; - - let options_str = arguments[7].to_bun_string(global_object)?; - let scram_password_str = argument(20).to_bun_string(global_object)?; - let (username, password, scram_password, database, options, path) = ( - args.username_str.to_utf8(), - args.password_str.to_utf8(), - scram_password_str.to_utf8(), - args.database_str.to_utf8(), - options_str.to_utf8(), - args.path_str.to_utf8(), - ); // Reject null bytes in connection parameters to prevent Postgres startup // message parameter injection (null bytes act as field terminators in the // wire protocol's key\0value\0 format). for (entry, name) in [ - (username.slice(), &b"username"[..]), - (password.slice(), b"password"), - (database.slice(), b"database"), - (path.slice(), b"path"), + (username, &b"username"[..]), + (password, b"password"), + (database, b"database"), + (path, b"path"), ] { if !entry.is_empty() && strings::contains_char(entry, 0) { return Err(global_object.throw_invalid_arguments(format_args!( @@ -889,33 +905,32 @@ pub(crate) fn create_connection<'a>( } } - let hostname = args.hostname_str.to_utf8(); open( global_object, vm.postgres_socket_group::(context), ConnectParams { - hostname: hostname.slice(), - port: args.port, + hostname, + port, strings: ConnectionStrings::new( - username.slice(), - password.slice(), - scram_password.slice(), - database.slice(), - options.slice(), - path.slice(), + username, + password, + scram_password, + database, + query, + path, ), secure, tls_config: Rc::new(tls_config), ssl_mode: args.ssl_mode, - channel_binding, + channel_binding: ChannelBinding::ALL[usize::from(channel_binding)], idle_timeout_ms: args.idle_timeout_ms, connection_timeout_ms: args.connection_timeout_ms, max_lifetime_ms: args.max_lifetime_ms, - use_unnamed_prepared_statements: arguments[14].as_boolean(), - on_connect: arguments[9], - on_close: arguments[10], - on_retire: argument(16), - column_transform: argument(19), + use_unnamed_prepared_statements, + on_connect: on_connected.to_js(), + on_close: on_disconnected.to_js(), + on_retire: on_retire.map(JsLocal::to_js), + column_transform: column_transform.map(JsLocal::to_js), check_server_identity, cancel: None, }, @@ -923,18 +938,18 @@ pub(crate) fn create_connection<'a>( .map_err(|_| { let mut message: Vec = Vec::new(); use std::io::Write as _; - let _ = if path.slice().is_empty() { + let _ = if path.is_empty() { write!( &mut message, "Failed to connect to {}:{}", - bstr::BStr::new(hostname.slice()), - args.port + bstr::BStr::new(hostname), + port ) } else { write!( &mut message, "Failed to connect to {}", - bstr::BStr::new(path.slice()) + bstr::BStr::new(path) ) }; global_object.throw_value(postgres_error_to_js( @@ -1003,8 +1018,8 @@ struct ConnectParams<'a> { use_unnamed_prepared_statements: bool, on_connect: JSValue, on_close: JSValue, - on_retire: JSValue, - column_transform: JSValue, + on_retire: Option, + column_transform: Option, check_server_identity: Option, cancel: Option, } @@ -1147,12 +1162,12 @@ fn open( this.js_value .set(crate::jsc::JsRef::init_strong(js_value, global_object)); js::onconnect_set_cached(js_value, global_object, params.on_connect); - if params.column_transform.is_callable() { - js::column_transform_set_cached(js_value, global_object, params.column_transform); + if let Some(column_transform) = params.column_transform { + js::column_transform_set_cached(js_value, global_object, column_transform); } js::onclose_set_cached(js_value, global_object, params.on_close); - if params.on_retire.is_callable() { - js::onretire_set_cached(js_value, global_object, params.on_retire); + if let Some(on_retire) = params.on_retire { + js::onretire_set_cached(js_value, global_object, on_retire); } if let Some(callback) = params.check_server_identity { js::check_server_identity_set_cached(js_value, global_object, callback); @@ -1528,8 +1543,8 @@ impl PostgresSQLConnection { use_unnamed_prepared_statements: false, on_connect: JSValue::ZERO, on_close: JSValue::ZERO, - on_retire: JSValue::UNDEFINED, - column_transform: JSValue::UNDEFINED, + on_retire: None, + column_transform: None, check_server_identity: self.check_server_identity_callback(), cancel: Some(CancelTarget { session: self.ref_guard(), diff --git a/src/sql_jsc/postgres/PostgresSQLContext.rs b/src/sql_jsc/postgres/PostgresSQLContext.rs index 4272df102a7b..3c9a3ce5a0e9 100644 --- a/src/sql_jsc/postgres/PostgresSQLContext.rs +++ b/src/sql_jsc/postgres/PostgresSQLContext.rs @@ -3,7 +3,7 @@ //! into `RareData.postgres_group`/`postgres_tls_group` instead. use crate::jsc::{StrongOptional, VirtualMachineSqlExt as _}; -use bun_jsc::bindgen::{JsHeap, JsLocal}; +use bun_jsc::bindgen::{Callable, JsHeap, JsLocal}; #[repr(C)] #[derive(Default)] @@ -16,9 +16,9 @@ pub struct PostgresSQLContext { impl PostgresSQLContext { pub(crate) fn init<'a>( heap: &JsHeap<'a>, - on_resolve_query: JsLocal<'a>, - on_reject_query: JsLocal<'a>, - array_literal: JsLocal<'a>, + on_resolve_query: JsLocal<'a, Callable>, + on_reject_query: JsLocal<'a, Callable>, + array_literal: JsLocal<'a, Callable>, ) { // SAFETY: setting a `Strong` runs no JavaScript. let global = unsafe { heap.global() }; diff --git a/src/sql_jsc/postgres/PostgresSQLQuery.rs b/src/sql_jsc/postgres/PostgresSQLQuery.rs index ff0fef1892c9..27afc5babd69 100644 --- a/src/sql_jsc/postgres/PostgresSQLQuery.rs +++ b/src/sql_jsc/postgres/PostgresSQLQuery.rs @@ -2,10 +2,10 @@ use core::cell::Cell; use crate::jsc::{JSGlobalObject, JSValue, JsRef, JsResult, VirtualMachineSqlExt as _}; use crate::shared::cached_structure::Undescribed; -use crate::shared::query_ctor_args::QueryCtorArgs; +use crate::shared::query_ctor_args; use bun_core::String as BunString; -use bun_jsc::JsCell; use bun_jsc::bindgen::{JsLocal, JsScope, JsWrapper}; +use bun_jsc::{JSArray, JsCell}; use bun_ptr::{AsCtxPtr, RefPtr}; use super::PostgresSQLConnection; @@ -376,27 +376,15 @@ impl PostgresSQLQuery { pub(crate) fn create_query<'a>( cx: &mut JsScope<'a>, - query: JsLocal<'a>, - values: JsLocal<'a>, - pending_value: JsLocal<'a>, - bigint: JsLocal<'a>, - simple: JsLocal<'a>, + query: bun_core::String, + values: JsLocal<'a, JSArray>, + pending_value: JsLocal<'a, JSArray>, + bigint: bool, + simple: bool, ) -> JsResult { let global_this = cx.global(); - let QueryCtorArgs { - query, - values, - pending_value, - bigint, - simple, - } = QueryCtorArgs::parse( - global_this, - query.to_js(), - values.to_js(), - pending_value.to_js(), - bigint.to_js(), - simple.to_js(), - )?; + let (values, pending_value) = (values.to_js(), pending_value.to_js()); + query_ctor_args::validate(global_this, &query, values, simple)?; let ptr = bun_core::heap::into_raw(Box::new(PostgresSQLQuery::default())); @@ -405,7 +393,7 @@ impl PostgresSQLQuery { // SAFETY: ptr is exclusively owned here until returned to JS. unsafe { - (*ptr).query = query.to_bun_string(global_this)?; + (*ptr).query = query; (*ptr).this_value.set(JsRef::init_weak(this_value)); (*ptr).flags.set(Flags { bigint, diff --git a/src/sql_jsc/postgres/SASL.rs b/src/sql_jsc/postgres/SASL.rs index ce43499981b7..eafd976918f4 100644 --- a/src/sql_jsc/postgres/SASL.rs +++ b/src/sql_jsc/postgres/SASL.rs @@ -1,6 +1,5 @@ use bun_base64; -use crate::jsc::{JSGlobalObject, JSValue, JsResult}; use bun_boringssl_sys::SSL; use bun_sha_hmac::hmac::EVP_MAX_MD_SIZE; use bun_sql::postgres::protocol::authentication::SASLMechanisms; @@ -23,16 +22,12 @@ pub(crate) enum ChannelBinding { } impl ChannelBinding { - pub(crate) fn from_js(global: &JSGlobalObject, value: JSValue) -> JsResult { - match value.is_int32().then(|| value.as_int32()) { - Some(0) => Ok(ChannelBinding::Disable), - Some(1) => Ok(ChannelBinding::Prefer), - Some(2) => Ok(ChannelBinding::Require), - _ => Err(global.throw_invalid_arguments(format_args!( - "channelBinding must be 0 (disable), 1 (prefer) or 2 (require)" - ))), - } - } + /// In the order of the numbers `createConnection` takes. + pub(crate) const ALL: [ChannelBinding; 3] = [ + ChannelBinding::Disable, + ChannelBinding::Prefer, + ChannelBinding::Require, + ]; } /// The messages are libpq's. diff --git a/src/sql_jsc/shared/ConnectionCtorArgs.rs b/src/sql_jsc/shared/ConnectionCtorArgs.rs index 5bee5c270856..2d0089323914 100644 --- a/src/sql_jsc/shared/ConnectionCtorArgs.rs +++ b/src/sql_jsc/shared/ConnectionCtorArgs.rs @@ -19,12 +19,6 @@ const SSL_MODES: [SSLMode; 5] = [ ]; pub(crate) struct ConnectionCtorArgs { - pub hostname_str: bun_core::String, - pub port: i32, - pub username_str: bun_core::String, - pub password_str: bun_core::String, - pub database_str: bun_core::String, - pub path_str: bun_core::String, pub ssl_mode: SSLMode, pub tls_config: SSLConfig, /// Moves into the connection. @@ -35,45 +29,30 @@ pub(crate) struct ConnectionCtorArgs { pub max_lifetime_ms: u32, } -fn timeout_ms(global_object: &JSGlobalObject, value: JSValue) -> JsResult { +fn timeout_ms(value: f64) -> u32 { // `as` maps NaN to zero. - Ok(value - .to_number(global_object)? - .clamp(0.0, f64::from(i32::MAX)) as u32) + value.clamp(0.0, f64::from(i32::MAX)) as u32 } impl ConnectionCtorArgs { - /// Parses `arguments[0..=6]`, `[8]` and `[11..=13]`. `Ok(None)`: a JS exception is pending. + /// `ssl_mode` is an index of `SSL_MODES`. `Ok(None)`: a JS exception is pending. pub(crate) fn parse( global_object: &JSGlobalObject, vm: &mut VirtualMachine, - arguments: &[JSValue], + hostname: &[u8], + ssl_mode: u8, + tls_object: JSValue, + idle_timeout: f64, + connection_timeout: f64, + max_lifetime: f64, ) -> JsResult> { - let hostname_str = arguments[0].to_bun_string(global_object)?; - let port = arguments[1].coerce::(global_object)?; - let username_str = arguments[2].to_bun_string(global_object)?; - let password_str = arguments[3].to_bun_string(global_object)?; - let database_str = arguments[4].to_bun_string(global_object)?; - let path_str = arguments[8].to_bun_string(global_object)?; - let idle_timeout_ms = timeout_ms(global_object, arguments[11])?; - let connection_timeout_ms = timeout_ms(global_object, arguments[12])?; - let max_lifetime_ms = timeout_ms(global_object, arguments[13])?; // A C string from here on: the resolver, SNI and the certificate check would see the name up to the NUL. - if bun_core::strings::contains_char(hostname_str.to_utf8().slice(), 0) { + if bun_core::strings::contains_char(hostname, 0) { return Err(global_object .throw_invalid_arguments(format_args!("hostname must not contain null bytes"))); } - let Some(ssl_mode) = Some(arguments[5]) - .filter(|mode| mode.is_any_int()) - .and_then(|mode| usize::try_from(mode.to_int32()).ok()) - .and_then(|i| SSL_MODES.get(i).copied()) - else { - return Err(global_object.throw_invalid_arguments(format_args!( - "sslMode must be an integer from 0 (disable) to 4 (verify-full)" - ))); - }; + let ssl_mode = SSL_MODES[usize::from(ssl_mode)]; - let tls_object = arguments[6]; let mut tls_config = SSLConfig::default(); let mut secure: Option = None; if ssl_mode != SSLMode::Disable { @@ -114,18 +93,12 @@ impl ConnectionCtorArgs { } Ok(Some(Self { - hostname_str, - port, - username_str, - password_str, - database_str, - path_str, ssl_mode, tls_config, secure, - idle_timeout_ms, - connection_timeout_ms, - max_lifetime_ms, + idle_timeout_ms: timeout_ms(idle_timeout), + connection_timeout_ms: timeout_ms(connection_timeout), + max_lifetime_ms: timeout_ms(max_lifetime), })) } } diff --git a/src/sql_jsc/shared/QueryCtorArgs.rs b/src/sql_jsc/shared/QueryCtorArgs.rs index 22a0a8c5a9aa..bcb7dfc3bc83 100644 --- a/src/sql_jsc/shared/QueryCtorArgs.rs +++ b/src/sql_jsc/shared/QueryCtorArgs.rs @@ -1,55 +1,23 @@ -//! Shared `createQuery(query, values, pendingValue?, bigint?, simple?)` -//! constructor-argument parsing/validation used by both the Postgres and MySQL -//! query constructors. +//! What both the Postgres and MySQL `createQuery(query, values, pendingValue, bigint, simple)` +//! refuse. -use crate::jsc::{JSGlobalObject, JSType, JSValue, JsResult}; +use crate::jsc::{JSGlobalObject, JSValue, JsResult}; -pub(crate) struct QueryCtorArgs { - pub query: JSValue, - pub values: JSValue, - pub pending_value: JSValue, - pub bigint: bool, - pub simple: bool, -} - -impl QueryCtorArgs { - pub(crate) fn parse( - global_this: &JSGlobalObject, - query: JSValue, - values: JSValue, - pending_value: JSValue, - js_bigint: JSValue, - js_simple: JSValue, - ) -> JsResult { - if !query.is_string() { - return Err(global_this.throw(format_args!("query must be a string"))); - } - - if values.js_type() != JSType::Array { - return Err(global_this.throw(format_args!("values must be an array"))); +pub(crate) fn validate( + global_this: &JSGlobalObject, + query: &bun_core::String, + values: JSValue, + simple: bool, +) -> JsResult<()> { + if simple { + if values.get_length(global_this)? > 0 { + return Err(global_this + .throw_invalid_arguments(format_args!("simple query cannot have parameters"))); } - - let bigint = js_bigint.is_boolean() && js_bigint.as_boolean(); - let simple = js_simple.is_boolean() && js_simple.as_boolean(); - if simple { - if values.get_length(global_this)? > 0 { - return Err(global_this - .throw_invalid_arguments(format_args!("simple query cannot have parameters"))); - } - if query.get_length(global_this)? >= i32::MAX as u64 { - return Err(global_this.throw_invalid_arguments(format_args!("query is too long"))); - } - } - if !pending_value.js_type().is_array_like() { - return Err(global_this.throw_invalid_argument_type("query", "pendingValue", "Array")); + if query.length() >= i32::MAX as usize { + return Err(global_this.throw_invalid_arguments(format_args!("query is too long"))); } - - Ok(Self { - query, - values, - pending_value, - bigint, - simple, - }) } + + Ok(()) } diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index a466c93dad5e..e44331cc7670 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -116,9 +116,9 @@ itAlways("mistakes in a schema are errors when it loads", () => { expect(() => b.union({ bytes: stringOrBuffer })).toThrow("the bytes of a buffer are only for an argument"); for (const held of ["handle", "copied", "pinned"] as const) { const bytes = b.bytes({ from: ["string", "buffer"], held }); - expect(() => b.array(bytes)).toThrow("b.bytes is only for an argument"); - expect(() => b.dictionary({ bytes })).toThrow("bytes: b.bytes is only for an argument"); - expect(() => b.union({ bytes })).toThrow("bytes: b.bytes is only for an argument"); + expect(() => b.array(bytes)).toThrow("b.bytes and .withValue are only for an argument"); + expect(() => b.dictionary({ bytes })).toThrow("bytes: b.bytes and .withValue are only for an argument"); + expect(() => b.union({ bytes })).toThrow("bytes: b.bytes and .withValue are only for an argument"); } for (const held of ["handle", "copied", "pinned"] as const) { expect(() => b.bytes({ from: ["string", "buffer"], held }).optional).toThrow( @@ -712,18 +712,149 @@ describe("functions", () => { expect(() => b.fn({ export: "not a name" })).toThrow("export: not a name is not a C identifier"); }); + it("promisePtr and nativePromiseContext give the pointer in the context of a promise reaction", () => { + expect(bindgen.readPromisePtr(bindgen.promisePtrContext)).toBe(42); + const cell = bindgen.makeNativePromiseContext(); + expect(bindgen.readNativePromiseContext(cell)).toBe(42); + expect(bindgen.readNativePromiseContext(cell)).toBeNull(); + }); + it("$bindgen gives what a b.value returns", () => { expect(bindgen.primes).toEqual([2, 3, 5]); }); it("$bindgen gives a namespace as an object", () => { - expect(Object.keys(bindgen.testNamespace)).toEqual(["increment", "decrement"]); + expect(Object.keys(bindgen.testNamespace)).toEqual(["increment", "decrement", "negate"]); expect(bindgen.testNamespace.increment(1)).toBe(2); expect(bindgen.testNamespace.decrement(1)).toBe(0); }); + + it("a function of a namespace has overloads", () => { + expect(bindgen.testNamespace.negate(2)).toBe(-2); + expect(bindgen.testNamespace.negate(true)).toBe(false); + }); + + it("overloads have passThis and export, and tell a function from a number", () => { + const { callbackOrDelay } = bindgen; + expect(callbackOrDelay.call(undefined, () => {})).toBe("callback, this is undefined: true"); + expect(callbackOrDelay.call(undefined, new Proxy(() => {}, {}))).toBe("callback, this is undefined: true"); + expect(callbackOrDelay.call({}, class {})).toBe("callback, this is undefined: false"); + expect(callbackOrDelay.call({}, 5)).toBe("delay 5, this is undefined: false"); + expect(() => callbackOrDelay({})).toThrow(TypeError); + }); + + it(".ifPassed is absent only without the argument", () => { + expect(bindgen.textIfPassed()).toBe("none"); + expect(bindgen.textIfPassed(undefined)).toBe("undefined"); + expect(bindgen.textIfPassed(null)).toBe("null"); + expect(bindgen.textIfPassed("a")).toBe("a"); + expect(bindgen.textIfPassed.length).toBe(0); + expect(bindgen.isPassed()).toBe(false); + expect(bindgen.isPassed(1)).toBe(false); + expect(bindgen.isPassed(1, undefined)).toBe(true); + }); + + it(".withValue also gives the argument", () => { + const text = { toString: () => "four" }; + expect(bindgen.lengthWithValue(text)).toEqual([4, text]); + expect(bindgen.lengthWithValue(text)[1]).toBe(text); + const counter = new bindgen.TestCounter(1); + expect(bindgen.counterWithValue(counter)).toBe(counter); + expect(counter.count).toBe(2); + expect(() => bindgen.counterWithValue({})).toThrow(TypeError); + }); + + it("b.string.owned is the string", () => { + expect(bindgen.keptString("kept")).toBe("kept"); + expect(bindgen.keptString("")).toBe(""); + expect(() => bindgen.keptString(1)).toThrow(TypeError); + }); + + it(".allowDate takes a Date as its time value", () => { + expect(bindgen.timeValue(5)).toBe(5); + expect(bindgen.timeValue(new Date(7))).toBe(7); + expect(bindgen.timeValue(new (class extends Date {})(9))).toBe(9); + expect(bindgen.timeValue(new Date(NaN))).toBeNaN(); + expect(() => bindgen.timeValue("5")).toThrow("time must be a number or Date"); + expect(() => bindgen.timeValue({ valueOf: () => 5 })).toThrow(TypeError); + }); + + it("rejects: what the body or a conversion throws rejects the promise", async () => { + expect(await bindgen.finiteLater(1)).toBe(1); + const [thrown, refused] = [bindgen.finiteLater(Infinity), bindgen.finiteLater("1")]; + expect(thrown).toBeInstanceOf(Promise); + expect(refused).toBeInstanceOf(Promise); + expect(thrown).rejects.toThrow("value must be finite"); + expect(refused).rejects.toThrow("value must be a number"); + }); +}); + +describe("a dictionary of a node:* function", () => { + it(".validateInteger can be absent", () => { + expect(bindgen.nodeOptionsSize({})).toBe(undefined); + expect(bindgen.nodeOptionsSize({ size: undefined })).toBe(undefined); + expect(bindgen.nodeOptionsSize({ size: 3 })).toBe(3); + }); + + it("reportedAs names the members, and expected says what Node expects of one", () => { + expect(() => bindgen.nodeOptionsSize({ size: "3" })).toThrow( + `The "options.size" property must be of type number. Received type string ('3')`, + ); + expect(() => bindgen.nodeOptionsSize({ size: 11 })).toThrow( + `The value of "options.size" is out of range. It must be >= 1 && <= 10. Received 11`, + ); + expect(() => bindgen.nodeOptionsSize({ flag: 1 })).toThrow( + `The "options.flag" property must be of type boolean. Received type number (1)`, + ); + expect(() => bindgen.nodeOptionsSize(1)).toThrow("options must be an object"); + expect(() => bindgen.bareOptionsSize({ size: null })).toThrow( + `The "size" argument must be of type number. Received null`, + ); + }); +}); + +describe("values as themselves", () => { + it("a union tells a function, an array and an object apart, and what is not an object passes a dictionary", () => { + expect(bindgen.describeHandle(undefined)).toBe("missing"); + expect(bindgen.describeHandle()).toBe("missing"); + expect(bindgen.describeHandle(null)).toBe("nothing"); + expect(bindgen.describeHandle(() => {})).toBe("callback"); + expect(bindgen.describeHandle([1])).toBe("list"); + expect(bindgen.describeHandle(new (class extends Array {})())).toBe("list"); + expect(bindgen.describeHandle({ x: 2 })).toBe("point 2"); + expect(bindgen.describeHandle(true)).toBe("text true"); + expect(bindgen.describeHandle(3)).toBe("text 3"); + expect(bindgen.describeHandle(4n)).toBe("text 4"); + expect(bindgen.describeHandle("s")).toBe("text s"); + expect(() => bindgen.describeHandle({})).toThrow("x"); + expect(() => bindgen.describeHandle(Symbol())).toThrow(TypeError); + }); + + it("b.object takes every object", () => { + for (const value of [{}, [], () => {}, new Date(0), new String("s"), new Proxy({}, {})]) { + expect(bindgen.isObject(value)).toBe(true); + expect(bindgen.objectEcho(value)).toBe(value); + } + for (const value of [undefined, null, 1, "s", true, 1n, Symbol()]) { + expect(bindgen.isObject(value)).toBe(false); + expect(() => bindgen.objectEcho(value)).toThrow("value must be an object"); + } + }); + + it("b.undefined and b.null are two alternatives, taken and returned", () => { + expect(bindgen.absenceEcho(undefined)).toBe(undefined); + expect(bindgen.absenceEcho(null)).toBe(null); + expect(bindgen.absenceEcho(1)).toBe(1); + }); }); describe("enumerations", () => { + it("a record names the variants", () => { + for (const value of ["rgb", "[rgb]", "{rgb}"]) expect(bindgen.bracketEcho(value)).toBe(value); + expect(bindgen.bracketEcho("{RGB}")).toBe("{rgb}"); + expect(() => bindgen.bracketEcho("(rgb)")).toThrow(TypeError); + }); + it("parses and returns the canonical spelling", () => { expect(bindgen.nextColor("red")).toBe("green"); expect(bindgen.nextColor("green")).toBe("blue"); @@ -1202,8 +1333,12 @@ describe("overloads", () => { expect(bindgen.overloaded.length).toBe(1); }); - it("a signature ignores the arguments it does not have", () => { - expect(bindgen.overloaded(1, "x")).toBe("number 1"); + it("past its last argument a signature takes only undefined", () => { + expect(bindgen.overloaded(1, undefined)).toBe("number 1"); + expect(() => bindgen.overloaded(1, "x")).toThrow("b must be a number"); + }); + + it("the only signature left ignores the arguments it does not have", () => { expect(bindgen.overloaded([1], 5)).toBe("list [1.0]"); }); @@ -1225,10 +1360,7 @@ describe("overloads", () => { expect(() => b.overloads({ args: { a: b.i32, b: b.bool } }, { args: { a: b.f64, b: b.bool } })).toThrow( "overload 2 is never chosen", ); - // The shorter one ignores a second argument, so written first it takes every call. - expect(() => b.overloads({ args: { a: b.string } }, { args: { a: b.string, b: b.bool } })).toThrow( - "overload 2 is never chosen", - ); + b.overloads({ args: { a: b.string } }, { args: { a: b.string, b: b.bool } }); b.overloads({ args: { a: b.string, b: b.bool } }, { args: { a: b.string } }); b.overloads({ args: { a: b.string, b: b.f64 } }, { args: { a: b.string, b: b.bool } }); b.overloads({ args: { a: b.bool } }, { args: { a: b.any } }); diff --git a/test/js/bun/cron/cron-local-time.test.ts b/test/js/bun/cron/cron-local-time.test.ts index 8fc8ee87ee63..0d7e9d18972e 100644 --- a/test/js/bun/cron/cron-local-time.test.ts +++ b/test/js/bun/cron/cron-local-time.test.ts @@ -268,7 +268,7 @@ describe.concurrent("Bun.cron.parse — { tz } option", () => { test("non-string tz throws", () => { // @ts-expect-error - expect(() => Bun.cron.parse("* * * * *", Date.now(), { tz: 42 })).toThrow(/options\.tz must be a string/); + expect(() => Bun.cron.parse("* * * * *", Date.now(), { tz: 42 })).toThrow(/Options\.tz must be a string/); }); test("tz: undefined falls back to local", async () => { diff --git a/test/js/bun/md/md-render-callback.test.ts b/test/js/bun/md/md-render-callback.test.ts index 4f057a86beb3..961ff20a08d1 100644 --- a/test/js/bun/md/md-render-callback.test.ts +++ b/test/js/bun/md/md-render-callback.test.ts @@ -428,6 +428,7 @@ describe("Bun.markdown buffer input", () => { }); // A pin prevents a detach but not a shrink, and `resize()` unmaps the trimmed pages. + // The options are read before the bytes, so a getter that shrinks the input leaves nothing to render. describe("survives a resizable input that shrinks during the call", () => { const source = "# Hello\n\nworld **bold**\n"; @@ -447,7 +448,7 @@ describe("Bun.markdown buffer input", () => { return true; }, }; - expect(Markdown.html(view, options)).toBe(Markdown.html(source, { autolinks: true })); + expect(Markdown.html(view, options)).toBe(Markdown.html("", { autolinks: true })); expect(buffer.byteLength).toBe(0); }); @@ -459,7 +460,7 @@ describe("Bun.markdown buffer input", () => { return false; }, }; - expect(Markdown.ansi(view, theme)).toBe(Markdown.ansi(source, { colors: false })); + expect(Markdown.ansi(view, theme)).toBe(Markdown.ansi("", { colors: false })); expect(buffer.byteLength).toBe(0); }); @@ -487,7 +488,7 @@ describe("Bun.markdown buffer input", () => { }, }; expect(Markdown.react(view, undefined, options)).toEqual( - Markdown.react(source, undefined, { reactVersion: 18, autolinks: true }), + Markdown.react("", undefined, { reactVersion: 18, autolinks: true }), ); expect(buffer.byteLength).toBe(0); }); diff --git a/test/js/bun/test/describe.test.ts b/test/js/bun/test/describe.test.ts index 773f9b4ff67e..14ae382892a3 100644 --- a/test/js/bun/test/describe.test.ts +++ b/test/js/bun/test/describe.test.ts @@ -1,5 +1,5 @@ import { spawnSync } from "bun"; -import { describe, expect, jest, test } from "bun:test"; +import { afterEach, beforeAll, beforeEach, describe, expect, jest, test } from "bun:test"; import { bunEnv, bunExe, isLinux, tempDir } from "harness"; describe("blocks should handle a number, string, anonymous class, named class, or function for the first arg", () => { @@ -224,6 +224,21 @@ describe("passing arrow function as args", () => { }); }); +describe("a proxy of a function is a callback", () => { + const ran: string[] = []; + const proxy = (name: string) => new Proxy(() => void ran.push(name), {}); + describe("describe", new Proxy(() => test("test", proxy("test")), {})); + describe("hooks", () => { + beforeAll(proxy("beforeAll")); + beforeEach(proxy("beforeEach")); + afterEach(proxy("afterEach")); + test("test with a timeout", proxy("test with a timeout"), 5000); + }); + test("each ran", () => { + expect(ran).toEqual(["test", "beforeAll", "beforeEach", "test with a timeout", "afterEach"]); + }); +}); + test("deeply nested describe() doesn't overflow the native stack", async () => { // describe() callbacks run from a queue, so the JS stack limit doesn't stop the nesting. using test_dir = tempDir(".", { diff --git a/test/js/bun/util/socket-fault-injection.test.ts b/test/js/bun/util/socket-fault-injection.test.ts index 662725bcc631..e2e53b27e1b7 100644 --- a/test/js/bun/util/socket-fault-injection.test.ts +++ b/test/js/bun/util/socket-fault-injection.test.ts @@ -12,12 +12,12 @@ describe.skipIf(skip)("socketFaultInjection control surface", () => { test("set() validates syscall", () => { expect(() => fault.set({ syscall: "bogus" as any, action: "errno", errno: "ECONNRESET" })).toThrow( - /rule\.syscall must be one of/, + /SocketFaultRule\.syscall must be/, ); }); test("set() validates action", () => { - expect(() => fault.set({ syscall: "recv", action: "bogus" as any })).toThrow(/rule\.action must be one of/); + expect(() => fault.set({ syscall: "recv", action: "bogus" as any })).toThrow(/SocketFaultRule\.action must be/); }); // Only recv/send/writev have a byte count to clamp; arming "short" on any other @@ -66,7 +66,7 @@ describe.skipIf(skip)("socketFaultInjection control surface", () => { test("set() rejects unknown errno name", () => { expect(() => fault.set({ syscall: "recv", action: "errno", errno: "ENOSUCHERR" as any })).toThrow( - /unknown errno name/, + /SocketFaultRule\.errno must be/, ); }); @@ -118,7 +118,7 @@ describe.skipIf(skip)("socketFaultInjection control surface", () => { // and then never fire. test("set() rejects syscalls that have no fault hook", () => { for (const sc of ["socket", "close", "shutdown"]) { - expect(() => fault.set({ syscall: sc as any, action: "none" })).toThrow(/rule\.syscall must be one of/); + expect(() => fault.set({ syscall: sc as any, action: "none" })).toThrow(/SocketFaultRule\.syscall must be/); } }); diff --git a/test/js/node/crypto/crypto-random.test.ts b/test/js/node/crypto/crypto-random.test.ts index 25c190b8f26f..659b5bfbfa16 100644 --- a/test/js/node/crypto/crypto-random.test.ts +++ b/test/js/node/crypto/crypto-random.test.ts @@ -1,9 +1,38 @@ import { afterAll, beforeAll, describe, expect, it } from "bun:test"; -import { checkPrime, checkPrimeSync, randomBytes, randomFill, randomFillSync, randomInt } from "crypto"; +import { checkPrime, checkPrimeSync, randomBytes, randomFill, randomFillSync, randomInt, randomUUID } from "crypto"; import { statSync } from "fs"; import { bunEnv, bunExe, isLinux, isMacOS, isMusl, tempDir } from "harness"; import { join } from "path"; +describe("as Node", () => { + it("a proxy of a function is a callback", async () => { + const called = (call: (callback: (err: Error | null) => void) => void) => + new Promise((resolve, reject) => call(new Proxy(err => (err ? reject(err) : resolve()), {}))); + await called(callback => randomInt(5, callback)); + await called(callback => randomInt(1, 5, callback)); + await called(callback => randomFill(new Uint8Array(4), callback)); + await called(callback => randomFill(new Uint8Array(4), 0, callback)); + await called(callback => randomFill(new Uint8Array(4), 0, 1, callback)); + }); + + it("randomUUID refuses a proxy of an array", () => { + expect(() => randomUUID(new Proxy([], {}))).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); + }); + + it("randomUUID and randomFill report their arguments", () => { + // @ts-expect-error + expect(() => randomUUID(5)).toThrow('The "options" argument must be of type object. Received type number (5)'); + // @ts-expect-error + expect(() => randomUUID({ disableEntropyCache: 1 })).toThrow( + 'The "options.disableEntropyCache" property must be of type boolean. Received type number (1)', + ); + // @ts-expect-error + expect(() => randomFillSync(5)).toThrow( + 'The "buf" argument must be an instance of ArrayBuffer or ArrayBufferView. Received type number (5)', + ); + }); +}); + describe("randomInt args validation", () => { it("default min is 0 so max should be greater than 0", () => { expect(() => randomInt(-1)).toThrow(RangeError); diff --git a/test/js/node/crypto/pbkdf2.test.ts b/test/js/node/crypto/pbkdf2.test.ts index 2ad3860ec8be..58292ce4c4d6 100644 --- a/test/js/node/crypto/pbkdf2.test.ts +++ b/test/js/node/crypto/pbkdf2.test.ts @@ -129,7 +129,7 @@ describe("invalid inputs", () => { }); expect(() => { crypto.pbkdf2("password", "salt", 1, input, "sha256", outer); - }).toThrow(`The value of "keylen" is out of range. It must be >= 0 and <= 2147483647. Received ${input}`); + }).toThrow(`The value of "keylen" is out of range. It must be >= 0 && <= 2147483647. Received ${input}`); expect(outer).not.toHaveBeenCalled(); }); }); @@ -210,10 +210,15 @@ test("keylen=0 fails async via callback", async () => { expect(err.message).toBe("PBKDF2 derivation failed"); }); -[-1, 2147483648, 4294967296, 2 ** 52].forEach(input => { +[ + [-1, "-1"], + [2147483648, "2147483648"], + [4294967296, "4294967296"], + [2 ** 52, "4_503_599_627_370_496"], +].forEach(([input, received]) => { test(`${input} keylen`, () => { expect(() => crypto.pbkdf2("password", "salt", 1, input, "sha256")).toThrow( - `The value of "keylen" is out of range. It must be >= 0 and <= 2147483647. Received ${input}`, + `The value of "keylen" is out of range. It must be >= 0 && <= 2147483647. Received ${received}`, ); }); }); @@ -369,3 +374,26 @@ test("a throw inside the pbkdf2 callback is an uncaughtException, not an unhandl expect(stderr).toBe(""); expect(exitCode).toBe(0); }); + +test("pbkdf2 takes a proxy of a function as its callback", async () => { + const { promise, resolve, reject } = Promise.withResolvers(); + crypto.pbkdf2("p", "s", 1, 4, "sha1", new Proxy((err, key) => (err ? reject(err) : resolve(key)), {})); + expect(await promise).toStrictEqual(crypto.pbkdf2Sync("p", "s", 1, 4, "sha1")); +}); + +test("pbkdf2 reports its arguments as Node does", () => { + expect(() => crypto.pbkdf2("p", "s", 1, 1, "md55", 5)).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: 'The "callback" argument must be of type function. Received type number (5)', + }), + ); + for (const [name, args] of [ + ["password", [5, "s"]], + ["salt", ["p", 5]], + ] as const) { + expect(() => crypto.pbkdf2Sync(...args, 1, 1, "sha1")).toThrow( + `The "${name}" argument must be of type string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView. Received type number (5)`, + ); + } +}); diff --git a/test/js/node/crypto/scrypt.test.ts b/test/js/node/crypto/scrypt.test.ts index 468367f730d5..0e0ecf8b77e9 100644 --- a/test/js/node/crypto/scrypt.test.ts +++ b/test/js/node/crypto/scrypt.test.ts @@ -137,3 +137,33 @@ test("scryptSync reads its buffers only after every argument has been coerced", expect(passwordBytes.byteLength).toBe(0); expect(key).toStrictEqual(scryptSync("", "salt", 16, { N: 1024 })); }); + +test("scrypt takes a proxy of a function as its callback", async () => { + for (const options of [[], [{}]]) { + const { promise, resolve, reject } = Promise.withResolvers(); + scrypt("p", "s", 4, ...options, new Proxy((err, key) => (err ? reject(err) : resolve(key)), {})); + expect(await promise).toStrictEqual(scryptSync("p", "s", 4)); + } +}); + +test("scrypt reports its arguments and options as Node does", () => { + const received = "Received type string ('x')"; + for (const name of ["N", "cost", "r", "blockSize", "p", "parallelization", "maxmem"]) { + expect(() => scryptSync("p", "s", 1, { [name]: "x" })).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: `The "${name}" argument must be of type number. ${received}`, + }), + ); + } + // @ts-expect-error + expect(() => scryptSync("p", "s", "x")).toThrow(`The "keylen" argument must be of type number. ${received}`); + // @ts-expect-error + expect(() => scryptSync(5, "s", 1)).toThrow( + 'The "password" argument must be of type string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView. Received type number (5)', + ); + // @ts-expect-error + expect(() => scrypt("p", "s", 1, () => {}, 5)).toThrow( + 'The "callback" argument must be of type function. Received type number (5)', + ); +}); From 7de07613c16b0ef793efed58dd0a988b3693c0bb Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:26:12 +0000 Subject: [PATCH 10/22] [autofix.ci] apply automated fixes --- src/codegen/bindgen-emit.ts | 2 +- src/codegen/bindgen-lib.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 6e1442a82e6b..d3621629d4ce 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -15,6 +15,7 @@ import { Getter, indent, InternalFieldAccessor, + isIfPassed, type JSClass, memberIdentifier, type Method, @@ -22,7 +23,6 @@ import { NamedType, type NamespaceDefinition, OptionalType, - isIfPassed, type Origin, type OverloadDispatch, OwnedStringType, diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index af2e6b108c77..d92f56eb1325 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -60,6 +60,7 @@ export { objectType as object } from "./bindgen-lib-internal.ts"; /** An array, as a `JsLocal<'a, JSArray>`: `b.array(T)` converts its elements. */ export { jsArrayType as jsArray } from "./bindgen-lib-internal.ts"; +export { class_ as class }; /** A typed array, `DataView` or `ArrayBuffer` that is not shared, as a `JsLocal<'a, BufferSource>`. `.arrayBuffer`. */ export const bufferSource = new BufferSourceType(); @@ -106,7 +107,6 @@ export const union = (alternatives: Record) => new UnionType(alter /** A JS class whose state is the Rust struct at `options.rustPath`, stored inside the wrapper object. */ const class_ = (options: ClassOptions) => new ClassType(options); -export { class_ as class }; /** The class being declared, in its own signatures: a `JsWrapper<'a, Self>`. */ export { selfType as self } from "./bindgen-lib-internal.ts"; From 28d4d7564b4d8314cf00ca45a7ac9972abb2b4f8 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 02:02:35 +0000 Subject: [PATCH 11/22] Add `.otherAsNull` for what is ignored unless it is of the type, name public option bags in their errors, and close the schema checks the reviews found open --- docs/project/bindgen.mdx | 51 +++--- src/codegen/bindgen-emit.ts | 68 +++----- src/codegen/bindgen-lib-internal.ts | 163 ++++++++++++++---- src/css_jsc/color_js.bind.ts | 14 +- src/css_jsc/color_js.rs | 15 +- src/js/internal-for-testing.ts | 1 + src/jsc/JSGlobalObject.rs | 25 --- src/jsc/JSObject.rs | 8 - src/jsc/JSValue.rs | 9 - src/jsc/bindgen.rs | 11 ++ src/jsc/bindgen_test.bind.ts | 21 ++- src/jsc/bindgen_test.rs | 50 +++++- src/jsc/bindings/Bindgen/IDLConvert.h | 20 +++ .../Bindgen/IDLConvertNativePromiseContext.h | 2 +- src/jsc/bindings/Bindgen/IDLTypes.h | 4 + src/runtime/api/BunObject.bind.ts | 11 +- src/runtime/api/BunObject.rs | 26 +-- src/runtime/api/MarkdownObject.bind.ts | 23 ++- src/runtime/api/MarkdownObject.rs | 17 +- src/runtime/api/cron.bind.ts | 2 +- src/runtime/dns_jsc/cares_jsc.rs | 8 +- src/runtime/dns_jsc/dns.rs | 10 +- src/runtime/dns_jsc/dns_internals.bind.ts | 6 +- src/runtime/ffi/ffi.bind.ts | 4 +- src/runtime/ffi/ffi_body.rs | 9 +- src/runtime/ipc.bind.ts | 3 +- src/runtime/ipc.rs | 14 +- src/runtime/ipc_host.bind.ts | 2 +- src/runtime/ipc_host.rs | 2 +- src/runtime/node/Stat.bind.ts | 4 +- src/runtime/node/Stat.rs | 2 +- src/runtime/node/node_cluster_binding.bind.ts | 6 +- src/runtime/node/node_cluster_binding.rs | 10 +- src/runtime/node/node_crypto_binding.bind.ts | 17 +- src/runtime/node/node_crypto_binding.rs | 43 ++--- src/runtime/node/node_fs_binding.bind.ts | 5 +- src/runtime/node/node_fs_binding.rs | 10 +- src/runtime/node/node_http_binding.bind.ts | 5 +- src/runtime/node/node_http_binding.rs | 8 +- src/runtime/server/RequestContext.bind.ts | 2 +- src/runtime/socket/Listener.bind.ts | 8 +- src/runtime/socket/Listener.rs | 8 +- src/runtime/socket/socket_body.bind.ts | 23 +-- src/runtime/test_runner/BunTest.bind.ts | 6 +- .../test_runner/ScopeFunctions.bind.ts | 4 +- src/runtime/test_runner/ScopeFunctions.rs | 4 +- src/runtime/test_runner/jest.bind.ts | 6 +- src/runtime/test_runner/jest.rs | 6 +- .../test_runner/timers/FakeTimers.bind.ts | 11 +- src/runtime/test_runner/timers/FakeTimers.rs | 6 +- src/runtime/webcore/Blob.bind.ts | 6 +- src/runtime/webcore/Crypto.bind.ts | 9 +- src/runtime/webcore/Crypto.rs | 10 +- src/runtime/webcore/fetch.bind.ts | 2 +- src/sql_jsc/mysql.bind.ts | 2 +- src/sql_jsc/postgres.bind.ts | 2 +- test/internal/bindgen.test.ts | 93 ++++++++-- test/js/bun/cron/cron-local-time.test.ts | 2 +- test/js/bun/patch/patch.test.ts | 2 +- .../bun/util/socket-fault-injection.test.ts | 8 +- test/js/node/crypto/crypto-random.test.ts | 2 +- test/js/node/crypto/pbkdf2.test.ts | 8 +- test/js/sql/sql-options.test.ts | 6 +- 63 files changed, 562 insertions(+), 383 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index caa10208773b..941b88f6032a 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -113,7 +113,7 @@ A namespace that can itself be called, like `Bun.hash`, names the function that `b.namespace({ wyhash: { .. }, crc32: { .. } }, { call: "wyhash" })`. The object is then a function named after the namespace. -An array in place of a function is its [overloads](#overloads). +A function with [overloads](#overloads) is an array of its signatures. A function of a namespace has no `passThis`, `passCallee` or `export`. ### What a function may do: `capability` @@ -182,7 +182,7 @@ export const read = b.overloads( ); ``` -`passThis`, `passCallee`, `export` and `rejects` are of the function, so every signature writes the same. +`passThis`, `passCallee`, `export` and `rejects` belong to the JS function, so every signature gives them the same value. Use it when the type of one argument says what the others are. A union for each argument would accept combinations that mean nothing, and leave the body to sort them out. @@ -247,27 +247,29 @@ field, so one that is a C++ keyword is refused: a member takes a `rustName`. Only the promise calls a reaction, with the context it was given, so nothing tests that argument. `b.promisePtr` is the `*mut T` that `JSValue::then` was given. `b.nativePromiseContext` is what `take` of the cell -returns: `None` if the pointer was taken back before the promise settled. A generic `T` is written as its -instantiation. +returns: `None` if the pointer was taken back before the promise settled. No argument after it has a conversion that +throws: nothing would release the pointer. A generic `T` is written as its instantiation. -These come last, in this order (`b.any` already takes `undefined` and `null`, so only `.emptyStringAsNull` applies to it): +These come last, in this order (`b.any` already takes `undefined` and `null`, so only `.emptyStringAsNull` and `.ifPassed` apply to it): - `.optional`: `undefined` means absent; `Option`. - `.nullable`: `null` or `undefined` means absent; `Option`. - `.falsyAsNull`: every falsy value means absent; `Option`. - `.emptyStringAsNull`: `undefined`, `null` or `""` means absent, as for `JSValue::get_truthy`; `Option`. -- `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` both work and differ, like `alert`. It goes on every type, and only on an argument. +- `.otherAsNull`: every value the type does not take means absent; `Option`. It is for an argument or a member that is ignored unless it is of the type. An object is what a dictionary takes, which still reports its members. It is not for an integer type, which reports a number it does not take. +- `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` both work and differ, like `alert`. It also goes on a type that takes `undefined`, like `b.any`. It is only for an argument, and not for one of an overload. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. `.withValue` on an argument also gives the body the argument itself, `(T, JsLocal<'a>)`: for one that reads the -internal fields of an instance, or returns the argument when there is nothing to change. +internal fields of an instance, or returns the argument when there is nothing to change. It comes after all of these, +and not with `.ifPassed`. -As results, `b.string.utf8` is anything that is `AsRef<[u8]>` and `b.array(T)` anything that -is `IntoIterator`. `b.bufferSource` as a result is what +As results, `b.string.utf8` is anything that is `AsRef<[u8]>`, `b.array(T)` anything that +is `IntoIterator`, and `b.array(b.string)` anything that is `AsRef<[bun_core::String]>`: one call makes the array. `b.bufferSource` as a result is what `heap.create_uninitialized_uint8_array(length)`, `heap.create_uint8_array(bytes)`, `heap.create_buffer(bytes)`, `heap.create_uint8_array_from_box(bytes)` or -`heap.create_buffer_from_box(bytes)` made, and `b.jsArray` what `heap.create_array(items, to_js::u8)` made. The last two take over the allocation, and the two -before them copy. +`heap.create_buffer_from_box(bytes)` made. The last two take over the allocation, and the two +before them copy. `b.jsArray` as a result is what `heap.create_array(items, to_js::u8)` made. These are results only: @@ -315,8 +317,7 @@ JS thread: see "Cross-thread string hazards" in `src/CLAUDE.md` before sending one anywhere. `b.string.allowStringObject` also takes a `String` object, as `.utf8` does. A [class](#classes) is a type too: import it from its `.bind.ts` file. As an argument it is a `&'a` of the struct. -The argument's slot in the frame holds the instance, so it is only for an argument, alone or in -`.nullable` or a union. +The argument's slot in the frame holds the instance, so it is only for an argument, alone, in a union, or with one of the modifiers above. ### Dictionaries @@ -346,13 +347,13 @@ each one on it. An `.optional` member that is `None` is left out. The second argument of `b.dictionary` takes: -| Key | What it does | -| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `allowArray` | an array is read like any other object, not refused | -| `nonObjectAsEmpty` | a value that is not an object gives every member its default; with `"typeof"` so does a function, as in Node's `if (typeof options !== "object") options = null`; every member must be optional or have a default | -| `fromJS` | also generates `Name::from_js(global, value)`, for a body that has a `JSValue`; one that holds JS values is lent to a closure, `Name::from_js(global, value, \|converted\| ..)` | -| `reportedAs` | what an error calls the dictionary, which defaults to the name of the type: `"options"` makes a member `options.level`, and `""` makes it `level`, as Node's validators are given it | -| `nullPrototype` | as a result, the object has no prototype, and the inline room of `Object.create(null)` | +| Key | What it does | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `allowArray` | an array is read like any other object, not refused | +| `nonObjectAsEmpty` | a value that is not an object gives every member its default; with `"typeof"` so does a function, as in Node's `if (typeof options !== "object") options = null`; every member must be optional or have a default | +| `fromJS` | also generates `Name::from_js(global, value)`, for a body that has a `JSValue`; one that holds JS values is lent to a closure, `Name::from_js(global, value, \|converted\| ..)` | +| `reportedAs` | what an error calls the dictionary, which defaults to the name of the type: `"options"` makes a member `options.level`, and `""` makes it `level`, for a Node function that validates under the bare name (`validateUint32(N, "N")`) | +| `nullPrototype` | as a result, the object has no prototype, and the inline room of `Object.create(null)` | ### Enumerations @@ -377,22 +378,22 @@ in play apart: | ---------------------------------- | -------------------------------------------------------------------------------- | | the bits of the value | `undefined`, `null`, boolean, number | | the type byte in the cell's header | string, BigInt, array, `ArrayBuffer`, typed array or `DataView`, `String` object | -| the cell's type info | what can be called | +| `JSCell::isCallable` | a function, or a proxy of one | | the cell's `ClassInfo` | an instance of a class, a `Date` | A strict type takes the kinds its row under [Types](#types) says. A type that coerces or reports its own error takes every kind: `.loose`, `.wrap`, `.enforceRange`, -`.validateInteger`, `b.any`, `b.bytes`, a dictionary. +`.validateInteger`, `.otherAsNull`, `b.any`, `b.bytes`, `b.object`, a dictionary. **A value goes to the first alternative, in written order, that takes its kind.** That alternative then converts it, and reports what is wrong with it: `1.5` given to `b.union({ integer: b.i32, text: b.string })` "must be an integer", not "must be of type integer or string". The buffer types refuse some values of their kinds (a shared buffer); those go on to the next alternative that takes -the kind and does not refuse the same. So does a value that is not an object from a dictionary or `b.object`, unless the dictionary is the last -to take it, which then reports it. A value no alternative takes throws `ERR_INVALID_ARG_TYPE` listing them all. +the kind and does not refuse the same. A dictionary and `b.object` refuse what is not an object, and it goes on the same way. As the last +alternative to take the kind, they get the value anyway and report it. A value no alternative takes throws `ERR_INVALID_ARG_TYPE` listing them all. So the order only matters between alternatives that overlap, and one that takes every -kind goes last. An alternative that can never be chosen is an error when the schema is +value goes last. An alternative that can never be chosen is an error when the schema is loaded. Choosing tests whether the value is a cell once, loads the type byte once and repeats no test. Strings as UTF-8, dictionaries and unions are then converted in the place they end up in. diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index d3621629d4ce..5f4aa0a03bd6 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -16,7 +16,7 @@ import { indent, InternalFieldAccessor, isIfPassed, - type JSClass, + kindTests, memberIdentifier, type Method, type ModuleDefinition, @@ -25,13 +25,11 @@ import { OptionalType, type Origin, type OverloadDispatch, - OwnedStringType, - OwnedUtf8Type, + refusesNonObjects, rustIdent, type RustScope, Signature, snakeCase, - StringType, type Type, UnionType, UnitType, @@ -152,21 +150,6 @@ const externType = (type: Type): string => : `Bun::Bindgen::ExternOf<${type.idlType}>`; const toExtern = (variable: string) => `Bun::Bindgen::toExtern(${variable})`; -/** How C++ tells that a value is of a kind: `ofValue` if it is not a cell, `ofCell` of `cell`, by its type byte. */ -const kindTests: Record, { ofValue?: (value: string) => string; ofCell?: string }> = { - undefined: { ofValue: value => `${value}.isUndefined()` }, - null: { ofValue: value => `${value}.isNull()` }, - boolean: { ofValue: value => `${value}.isBoolean()` }, - number: { ofValue: value => `${value}.isNumber()` }, - bigint: { ofValue: value => `${value}.isBigInt32()`, ofCell: "cell->isHeapBigInt()" }, - string: { ofCell: "cell->isString()" }, - stringObject: { ofCell: "cell->inherits()" }, - array: { ofCell: "JSC::isJSArray(cell)" }, - arrayBuffer: { ofCell: "cell->inherits()" }, - arrayBufferView: { ofCell: "cell->inherits()" }, - callable: { ofCell: "cell->isCallable()" }, -}; - /** * C++ that declares `choice`, an index into the `outcomes` it returns: the distinct values of `byKind`, "other"'s first. * `condition`, if the choice is that simple, is true of `outcomes[1]` and false of `outcomes[0]`. @@ -236,25 +219,22 @@ function cppConvertToAlternative(type: UnionType, candidates: readonly number[]) const { name, type: alternative } = type.alternatives[i]; const member = `result.${name}`; const convert = (target: string) => alternative.cppConvertInto(target, "value", "ctx", "", ""); - if (alternative instanceof DictionaryType && i !== candidates.at(-1)) - return `if (value.isObject()) { - result.tag = ${i}; - scope.release(); -${indent(cppEmplace(member, convert), 4)} - return; -}`; - if (alternative.refuses && !(alternative instanceof DictionaryType)) + if (alternative instanceof UnitType) return `result.tag = ${i};\nreturn;`; + const take = `result.tag = ${i}; +scope.release(); +${alternative.convertsInPlace ? cppEmplace(member, convert) : convert(member)} +return;`; + // As the last, its own conversion reports what is not an object. + if (refusesNonObjects(alternative)) + return i === candidates.at(-1) ? take : `if (value.isObject()) {\n${indent(take, 4)}\n}`; + if (alternative.refuses) return `if (auto converted = Bun::tryConvertIDL<${alternative.idlType}>(*globalObject, value, ctx)) { ${member} = WTF::move(*converted); result.tag = ${i}; return; } RETURN_IF_EXCEPTION(scope, void());`; - if (alternative instanceof UnitType) return `result.tag = ${i};\nreturn;`; - return `result.tag = ${i}; -scope.release(); -${alternative.convertsInPlace ? cppEmplace(member, convert) : convert(member)} -return;`; + return take; }) .join("\n"); } @@ -267,10 +247,10 @@ function cppHeaderForUnion(type: UnionType): string { .map(a => a.type.idlType) .join(", "); const { outcomes, code } = cppChooseByKind("value", type.alternativesByKind); - const mayMatchNone = (candidates: readonly number[]) => - candidates.length === 0 || - (!!alternatives[candidates.at(-1)!].type.refuses && - !(alternatives[candidates.at(-1)!].type instanceof DictionaryType)); + const mayMatchNone = (candidates: readonly number[]) => { + const last = alternatives[candidates.at(-1) ?? -1]?.type; + return !last || (!!last.refuses && !refusesNonObjects(last)); + }; const cases = outcomes.flatMap((candidates, index) => candidates.length === 0 ? [] @@ -1218,7 +1198,7 @@ ${cppPrologue} return cppHostFunctionBody(defineHostFunction, name, fn, symbol, takeThis); } -/** `define`, then the braces. Its overloads are named after `name`. */ +/** The function that `define` opens. The function of each overload is `name` plus a suffix. */ function cppHostFunctionBody( define: string, name: string, @@ -1307,7 +1287,11 @@ export function cppSource(file: SchemaFile): string { const accepted = file.types.filter(type => type instanceof DictionaryType && type.accepted) as DictionaryType[]; if (accepted.length > 0) headers.add("Bindgen/DictionaryMemberReader.h"); const systemHeaders = ["JavaScriptCore/JSFunction.h"]; - if (file.functions.some(fn => fn.signatures[0].rejects)) systemHeaders.push("JavaScriptCore/JSPromise.h"); + const functions = [ + ...file.functions, + ...file.types.flatMap(type => (type instanceof ClassType ? type.nativeFunctions : [])), + ]; + if (functions.some(fn => fn.signatures[0].rejects)) systemHeaders.push("JavaScriptCore/JSPromise.h"); if (file.types.some(type => type instanceof ClassType)) systemHeaders.unshift("JavaScriptCore/HeapAnalyzer.h"); if (accepted.some(type => !type.allowArray)) systemHeaders.unshift("JavaScriptCore/ArrayConstructor.h"); if (file.namespaces.length > 0) systemHeaders.push("JavaScriptCore/ObjectConstructor.h"); @@ -1397,13 +1381,7 @@ function rustArgumentName(arg: { name: string }): string { return rustIdent(name); } -/** A dictionary or a string stays in the frame of the thunk, and the body borrows it. */ -function rustBorrow(type: Type) { - const inner = type instanceof DefaultType ? type.inner : type; - const isString = - inner instanceof StringType && !(inner instanceof OwnedUtf8Type) && !(inner instanceof OwnedStringType); - return inner instanceof DictionaryType || isString ? "&" : ""; -} +const rustBorrow = (type: Type) => ((type instanceof DefaultType ? type.inner : type).borrowedByBody ? "&" : ""); const rustExternParameters = (signature: Signature, scope: RustScope) => signature.args.map( arg => `${rustArgumentName(arg)}: ${isPassedByPointer(arg.type) ? "*mut " : ""}${arg.type.rustExternType(scope)}`, diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 88102308c782..91a251c2ceeb 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -109,9 +109,28 @@ type ValueKinds = readonly ValueKind[] | "any"; const dateInstance: JSClass = { cpp: "JSC::DateInstance" }; +/** How C++ tells that a value is of a kind: `ofValue` if it is not a cell, `ofCell` of `cell`, by its type byte. */ +export const kindTests: Record< + Exclude, + { ofValue?: (value: string) => string; ofCell?: string } +> = { + undefined: { ofValue: value => `${value}.isUndefined()` }, + null: { ofValue: value => `${value}.isNull()` }, + boolean: { ofValue: value => `${value}.isBoolean()` }, + number: { ofValue: value => `${value}.isNumber()` }, + bigint: { ofValue: value => `${value}.isBigInt32()`, ofCell: "cell->isHeapBigInt()" }, + string: { ofCell: "cell->isString()" }, + stringObject: { ofCell: "cell->inherits()" }, + array: { ofCell: "JSC::isJSArray(cell)" }, + arrayBuffer: { ofCell: "cell->inherits()" }, + arrayBufferView: { ofCell: "cell->inherits()" }, + callable: { ofCell: "cell->isCallable()" }, +}; + const primitiveKinds: readonly (ValueKind | "other")[] = ["undefined", "null", "boolean", "number", "bigint", "string"]; -/** What a type that takes any object `refuses`. */ +/** What a type that takes every object `refuses`. */ const nonObjects = "what is not an object"; +export const refusesNonObjects = (type: Type) => type.refuses === nonObjects; const takes = (type: Type, kind: ValueKind | "other") => type.valueKinds === "any" || (kind !== "other" && type.valueKinds.includes(kind)); @@ -134,10 +153,11 @@ function alternativesByKind(alternatives: readonly Type[]): Map alternatives[earlier].refuses === type.refuses)) continue; list.push(i); if (!type.refuses) break; + if (refusesNonObjects(type) && kind !== "other" && !primitiveKinds.includes(kind)) break; } - // Left as the last, it reports the value. + // A primitive skips an alternative that takes only objects, unless it is the last: that one reports it. if (primitiveKinds.includes(kind)) { - for (let i = list.length - 2; i >= 0; i--) if (alternatives[list[i]].refuses === nonObjects) list.splice(i, 1); + for (let i = list.length - 2; i >= 0; i--) if (refusesNonObjects(alternatives[list[i]])) list.splice(i, 1); } if (!lists.has(`${list}`)) lists.set(`${list}`, list); return [kind, lists.get(`${list}`)!]; @@ -183,6 +203,10 @@ export abstract class Type { get ifPassed(): OptionalType { return new OptionalType(this, "ifPassed"); } + /** Every value it does not take means absent. */ + get otherAsNull(): OptionalType { + return new OptionalType(this, "otherAsNull"); + } /** Also the argument itself: `(T, JsLocal<'a>)`. Only for an argument. */ get withValue(): WithValueType { return new WithValueType(this); @@ -213,7 +237,18 @@ export abstract class Type { get refuses(): string | null { return null; } - /** Written `.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull`, `.ifPassed` or `.default()`: as an argument with only such after it, it is one a caller leaves out. */ + /** A C++ condition that is true of the values it takes. Only for a type whose kinds tell, or that takes every object. */ + cppTakes(value: string): string { + if (refusesNonObjects(this)) return `${value}.isObject()`; + const kinds = this.valueKinds; + assert(kinds !== "any" && !this.refuses, `${this.tsType}: only converting a value tells whether it takes it`); + const ofValue = kinds.flatMap(kind => (typeof kind === "object" ? [] : (kindTests[kind].ofValue?.(value) ?? []))); + const ofCell = kinds + .flatMap(kind => (typeof kind === "object" ? `cell->inherits<${kind.cpp}>()` : (kindTests[kind].ofCell ?? []))) + .map(test => test.replace("cell", `${value}.asCell()`)); + return [...ofValue, ...(ofCell.length > 0 ? [`(${value}.isCell() && (${ofCell.join(" || ")}))`] : [])].join(" || "); + } + /** Written `.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull`, `.otherAsNull`, `.ifPassed` or `.default()`: as an argument with only such after it, it is one a caller leaves out. */ get isOptional(): boolean { return false; } @@ -221,6 +256,14 @@ export abstract class Type { get passesValue(): boolean { return false; } + /** Why it is not an element, a member, an alternative or a result, if it is only for an argument. */ + get onlyArgument(): string | null { + return this.passesValue ? "b.bytes and .withValue are only for an argument" : null; + } + /** As an argument it stays in the frame of the thunk, and the body borrows it: `&T`. */ + get borrowedByBody(): boolean { + return false; + } /** `rustFromExtern` is a `bindgen::Result`. Only for an argument. */ get rustFromExternThrows(): boolean { return false; @@ -310,6 +353,10 @@ export abstract class Type { rustToJS(value: string, scope: RustScope): string { throw new TypeError(`${this.tsType} cannot be returned to JavaScript`); } + /** The same for `value`, a `Vec` of it, where one call makes the whole array. */ + rustArrayToJS(value: string): string | null { + return null; + } } // ───────────────────────────── primitives ───────────────────────────── @@ -552,6 +599,7 @@ export class DoubleType extends PrimitiveType { } /** `ToNumber`: WebIDL's `unrestricted double`. */ get loose(): LooseDoubleType { + assert(this.idlType === "Bun::Bindgen::IDLStrictDouble", ".loose follows b.f64"); return new LooseDoubleType("WebCore::IDLUnrestrictedDouble", "any"); } } @@ -659,6 +707,9 @@ export class StringType extends Type { get nullRepresentation() { return "pointer" as const; } + get borrowedByBody(): boolean { + return true; + } toCpp(value: unknown): string { assert(typeof value === "string", `${util.inspect(value)} is not a string`); return `WTF::String { ${asciiLiteral(value)} }`; @@ -681,6 +732,9 @@ export class StringType extends Type { rustToJS(value: string): string { return `bindgen::to_js::string(heap, ${value})`; } + rustArrayToJS(value: string): string | null { + return `bindgen::to_js::string_array(heap, ${value})`; + } } export class LocalStringResultType extends ResultType { @@ -698,6 +752,15 @@ export class LocalStringResultType extends ResultType { } export class OwnedStringType extends StringType { + get onlyArgument(): string { + return ".owned is only for an argument: everywhere else the string is already by value"; + } + get borrowedByBody(): boolean { + return false; + } + get local(): never { + throw new TypeError(".local follows b.string"); + } get loose(): never { throw new TypeError("write b.string.loose.owned"); } @@ -714,6 +777,12 @@ export class OwnedStringType extends StringType { /** UTF-8 to keep: the JS string's own buffer, shared, when it is 8-bit ASCII, otherwise a transcoded copy. */ export class OwnedUtf8Type extends StringType { + get borrowedByBody(): boolean { + return false; + } + rustArrayToJS(): null { + return null; + } get owned(): never { throw new TypeError("already .owned"); } @@ -1224,6 +1293,9 @@ export class PromisePtrType extends Type { get idlType(): string { return "Bun::Bindgen::IDLPromisePtr"; } + get onlyArgument(): string { + return "the context of a promise reaction is only an argument"; + } get conversionThrows(): boolean { return false; } @@ -1240,9 +1312,6 @@ export class PromisePtrType extends Type { rustFromExtern(expr: string): string { return `${expr}.cast::<${this.pointee}>()`; } - rustToJS(): never { - throw new TypeError("the context of a promise reaction is only an argument"); - } } /** As `PromisePtrType`, for the `NativePromiseContext` cell that `JSValue::then_with_value` gave the reaction: what `take` hands back. */ @@ -1261,24 +1330,26 @@ export class NativePromiseContextType extends PromisePtrType { } } -const isNativePromiseContext = (type: Type) => type instanceof NativePromiseContextType; - // ───────────────────────────── combinators ───────────────────────────── export class OptionalType extends Type { readonly inner: Type; - readonly modifier: "optional" | "nullable" | "falsyAsNull" | "emptyStringAsNull" | "ifPassed"; + readonly modifier: "optional" | "nullable" | "falsyAsNull" | "emptyStringAsNull" | "otherAsNull" | "ifPassed"; constructor(inner: Type, modifier: OptionalType["modifier"]) { super(); assert(!(inner instanceof OptionalType), "already optional"); assert(!(inner instanceof DefaultType), "put .default() last"); assert(!(inner instanceof WithValueType), "put .withValue last"); - if (modifier === "ifPassed") { - // Every type: what it does with `undefined` is its own. - } else if (inner instanceof AnyType) { - assert(modifier === "emptyStringAsNull", "b.any already accepts undefined and null"); - } else { - assert(!inner.takesUndefined, `${inner.tsType} already takes undefined`); + assert(inner.passesValue || !inner.onlyArgument, inner.onlyArgument ?? ""); + // `.ifPassed` does not claim `undefined`, so it goes on a type that takes it. + if (modifier !== "ifPassed") { + if (inner instanceof AnyType) + assert(modifier === "emptyStringAsNull", "b.any already accepts undefined and null"); + else assert(!inner.takesUndefined, `${inner.tsType} already takes undefined`); + } + if (modifier === "otherAsNull") { + assert(!(inner instanceof IntegerType), "an integer type reports a number it does not take: b.f64.otherAsNull"); + assert(inner.valueKinds !== "any" || inner.refuses, `${inner.tsType} takes every value`); } assert( !(inner instanceof BytesType) || inner.canBe(modifier), @@ -1293,12 +1364,15 @@ export class OptionalType extends Type { nullable: "WebCore::IDLNullable", falsyAsNull: "Bun::Bindgen::IDLFalsyAsNull", emptyStringAsNull: "Bun::Bindgen::IDLEmptyStringAsNull", + otherAsNull: "Bun::Bindgen::IDLOtherAsNull", ifPassed: "Bun::Bindgen::IDLNotPassedAsNull", }[this.modifier]; return `${template}<${this.inner.idlType}>`; } get tsType(): string { - return `${this.inner.tsType} | ${this.modifier === "optional" ? "undefined" : "null | undefined"}`; + if (this.modifier === "otherAsNull") return "any"; + const absent = this.modifier === "optional" || this.modifier === "ifPassed" ? "undefined" : "null | undefined"; + return `${this.inner.tsType} | ${absent}`; } get takesUndefined(): boolean { return true; @@ -1306,9 +1380,11 @@ export class OptionalType extends Type { get isOptional(): boolean { return true; } + get onlyArgument(): string | null { + return this.modifier === "ifPassed" ? ".ifPassed is only for an argument" : super.onlyArgument; + } get valueKinds(): ValueKinds { - if (this.inner.valueKinds === "any" || this.modifier === "falsyAsNull" || this.modifier === "emptyStringAsNull") - return "any"; + if (this.inner.valueKinds === "any" || !["optional", "nullable", "ifPassed"].includes(this.modifier)) return "any"; return [...this.inner.valueKinds, "undefined", ...(this.modifier === "nullable" ? (["null"] as const) : [])]; } get dependencies(): readonly Type[] { @@ -1327,6 +1403,7 @@ export class OptionalType extends Type { return this.inner.cppReadLast(target, value); } #cppIsPresent(value: string): string { + if (this.modifier === "otherAsNull") return this.inner.cppTakes(value); return { optional: `!${value}.isUndefined()`, nullable: `!${value}.isUndefinedOrNull()`, @@ -1396,8 +1473,8 @@ export class WithValueType extends Type { readonly inner: Type; constructor(inner: Type) { super(); - assert(!inner.passesValue, "it already holds the argument"); - assert(!inner.returnsValueUnchanged, "it is the argument"); + assert(!inner.passesValue, ".withValue: b.bytes already passes the argument"); + assert(!inner.returnsValueUnchanged, ".withValue: the type is the argument itself"); assert(!isIfPassed(inner), ".ifPassed has no argument to give when it is absent"); this.inner = inner; } @@ -1458,9 +1535,6 @@ export class WithValueType extends Type { rustFromExtern(expr: string, scope: RustScope): string { return `(${this.inner.rustFromExtern(`${expr}.converted`, scope)}, bindgen::JsLocal::from_extern(${expr}.value, heap))`; } - rustToJS(): never { - throw new TypeError(".withValue is only for an argument"); - } } export const isIfPassed = (type: Type) => type instanceof OptionalType && type.modifier === "ifPassed"; @@ -1474,6 +1548,7 @@ export class DefaultType extends Type { constructor(inner: Type, value: unknown) { super(); assert(!(inner instanceof DefaultType), "already has a default"); + assert(!(inner instanceof WithValueType), "put .withValue last"); if (inner instanceof OptionalType) { assert(inner.modifier === "nullable", ".default() follows a type or .nullable"); this.inner = inner.inner; @@ -1557,8 +1632,7 @@ export class ArrayType extends Type { super(); assert(!(element instanceof DefaultType), "an element of an array is never absent, so it has no default"); assert(!(element instanceof RangeType), "a range reports the name of its argument, which an element does not have"); - assert(!element.passesValue, "b.bytes and .withValue are only for an argument"); - assert(!isIfPassed(element), ".ifPassed is only for an argument"); + assert(!element.onlyArgument, element.onlyArgument ?? ""); assert( !element.borrowsCell, "an instance that Rust takes as &T cannot be in an array or a dictionary: only an argument's slot in the frame keeps it alive", @@ -1616,7 +1690,10 @@ export class ArrayType extends Type { return `Vec<${this.element.rustResultFieldType(scope)}>`; } rustToJS(value: string, scope: RustScope): string { - return `bindgen::to_js::array(heap, ${value}, |heap, value| ${this.element.rustToJS("value", scope)})`; + return ( + this.element.rustArrayToJS(value) ?? + `bindgen::to_js::array(heap, ${value}, |heap, value| ${this.element.rustToJS("value", scope)})` + ); } } @@ -1809,8 +1886,7 @@ export class DictionaryType extends NamedType { "fields", ); for (const { name, type } of this.members) { - assert(!type.passesValue, `${name}: b.bytes and .withValue are only for an argument`); - assert(!isIfPassed(type), `${name}: .ifPassed is only for an argument`); + assert(!type.onlyArgument, `${name}: ${type.onlyArgument}`); assert(!(type instanceof VariadicType), `${name}: .variadic is only for the last argument`); } assert( @@ -1846,6 +1922,9 @@ export class DictionaryType extends NamedType { get refuses(): string | null { return this.nonObjectAsEmpty ? null : nonObjects; } + get borrowedByBody(): boolean { + return true; + } get nullPrototype(): boolean { return !!this.options.nullPrototype; } @@ -1937,7 +2016,7 @@ export class UnionType extends NamedType { assert(!(type instanceof UnionType), `${name}: list the alternatives of the other union here`); assert(!(type instanceof OptionalType), `${name}: add b.null as an alternative`); assert(!(type instanceof DefaultType), `${name}: put .default() on the union`); - assert(!type.passesValue, `${name}: b.bytes and .withValue are only for an argument`); + assert(!type.onlyArgument, `${name}: ${type.onlyArgument}`); assert(!(type instanceof VariadicType), `${name}: .variadic is only for the last argument`); assert(/^[a-z][A-Za-z0-9]*$/.test(name), `${name}: an alternative is named in camelCase`); assert(name !== "tag", "tag: the generated struct has a member of that name"); @@ -2032,7 +2111,7 @@ export interface SignatureOptions { capability?: Capability; /** The body returns `Result` for any `E: bindgen::IntoError`. */ throws?: boolean; - /** It returns a promise. What it throws, or the conversion of an argument throws, is the reason of a rejected one instead. */ + /** The function returns a promise: what the body or the conversion of an argument throws rejects one, and nothing is thrown. */ rejects?: boolean; /** Path of the Rust function. Defaults to the module's path plus the export's name in snake_case. */ rustPath?: string; @@ -2055,6 +2134,10 @@ export interface FunctionOptions extends SignatureOptions { /** What `b.value` takes. */ export type ValueOptions = Pick & { returns: Type }; +/** A range, absent or not: its converter takes the name and no context. */ +const reportsItsOwnName = (type: Type) => + (type instanceof OptionalType || type instanceof DefaultType ? type.inner : type) instanceof RangeType; + export class Signature { readonly args: readonly { name: string; type: Type; expected?: readonly string[] | string }[]; readonly returns: Type; @@ -2075,6 +2158,7 @@ export class Signature { })); for (const [name, types] of Object.entries(expected)) { assert(name in args, `expected: ${name} is not an argument`); + assert(!reportsItsOwnName(args[name]), `expected: ${name} is a range, which reports what Node does`); assert(types.length > 0, `expected: ${name} expects nothing`); } this.returns = options.returns ?? new UnitType("undefined"); @@ -2108,12 +2192,17 @@ export class Signature { pinned.length === 0 || !this.args.some(arg => arg.type.borrowsHeap), 'held: "pinned" moves the bytes of a small typed array, which another argument that is held: "borrowed" can be', ); - const taken = this.args.findIndex(({ type }) => [type, ...type.dependencies].some(isNativePromiseContext)); + // Of the modifiers, only `.withValue` goes on one. + const taken = this.args.findIndex( + ({ type }) => (type instanceof WithValueType ? type.inner : type) instanceof NativePromiseContextType, + ); assert( taken < 0 || this.args.slice(taken + 1).every(arg => !arg.type.conversionThrows), "b.nativePromiseContext takes the pointer out of the cell: if a later argument throws, nothing releases it", ); assert(!(this.returns instanceof DefaultType), "a result has no default"); + assert(!this.returns.onlyArgument, this.returns.onlyArgument ?? ""); + assert(!this.rejects || this.returns.holdsJSValue, "rejects: the result is a promise"); assert( !this.returns.holdsJSValue || this.capability !== "none" || hasThisValue, 'returning a JS value takes capability: "heap" or "js"', @@ -2180,6 +2269,11 @@ export class FunctionDefinition { this.signatures.every(signature => signature.rejects === this.signatures[0].rejects), "its overloads have one rejects", ); + assert( + this.signatures.length === 1 || + !this.signatures.some(signature => signature.args.some(arg => isIfPassed(arg.type))), + ".ifPassed is not for overloads: the kind of an argument chooses one, and an argument that is not passed has none", + ); const nodes = new Map(); this.dispatch = this.dispatchOn(this.signatures, 0, nodes); const chosen = new Set(nodes.values()); @@ -2691,7 +2785,10 @@ export class NamespaceDefinition { this.functions = Object.entries(functions).map(([name, options]) => { assert(/^[A-Za-z_$][\w$]*$/.test(name), `${name}: a function is named by an identifier`); assert(!(options instanceof FunctionDefinition), `${name}: write what b.fn takes, without b.fn`); - const fn = new FunctionDefinition(Array.isArray(options) ? options : [options]); + const signatures: readonly SignatureOptions[] = Array.isArray(options) ? options : [options]; + for (const key of ["passThis", "passCallee", "export"]) + assert(!signatures.some(signature => key in signature), `${name}: a function of a namespace has no ${key}`); + const fn = new FunctionDefinition(signatures); fn.name = name; return fn; }); diff --git a/src/css_jsc/color_js.bind.ts b/src/css_jsc/color_js.bind.ts index c2f0b24271bb..35086e0f8b58 100644 --- a/src/css_jsc/color_js.bind.ts +++ b/src/css_jsc/color_js.bind.ts @@ -2,11 +2,15 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::color_js" }); -/** From 0 to 1. What is not a number is opaque. */ -export const ColorAlpha = b.union({ number: b.f64, ignored: b.any }); export const ColorChannels = b.dictionary( - { r: b.f64, g: b.f64, b: b.f64, a: ColorAlpha }, - { allowArray: true, fromJS: true }, + { + r: b.f64, + g: b.f64, + b: b.f64, + /** From 0 to 1. What is not a number is opaque. */ + a: b.f64.otherAsNull, + }, + { allowArray: true, fromJS: true, reportedAs: "input" }, ); /** @@ -44,6 +48,8 @@ export const OutputColorFormat = b.enumeration({ export const ColorOutputChannels = b.dictionary({ r: b.u8, g: b.u8, b: b.u8, a: b.f64.optional }); export const ColorOutput = b.union({ text: b.string, + /** As the CSS printer writes it. */ + css: b.string.utf8, number: b.u32, /** Of 3 or 4 bytes. */ list: b.jsArray, diff --git a/src/css_jsc/color_js.rs b/src/css_jsc/color_js.rs index 40a54f4aabb6..30869dc75f68 100644 --- a/src/css_jsc/color_js.rs +++ b/src/css_jsc/color_js.rs @@ -10,7 +10,7 @@ use bun_jsc::{JSGlobalObject, JSType, JSValue}; use crate::JsResult; use crate::generated_bindgen::color_js::{ - ColorAlpha, ColorChannels, ColorInput, ColorOutput, ColorOutputChannels, OutputColorFormat, + ColorChannels, ColorInput, ColorOutput, ColorOutputChannels, OutputColorFormat, }; /// CSS spec says to clamp values to their valid range so we'll respect that here @@ -47,15 +47,12 @@ fn rgba_from_list(global: &JSGlobalObject, list: JSValue) -> JsResult { }) } -fn rgba_from_channels(channels: &ColorChannels<'_>) -> RGBA { +fn rgba_from_channels(channels: &ColorChannels) -> RGBA { RGBA { red: clamp_to_u8(channels.r), green: clamp_to_u8(channels.g), blue: clamp_to_u8(channels.b), - alpha: match channels.a { - ColorAlpha::Number(a) => clamp_to_u8(a * 255.0), - ColorAlpha::Ignored(_) => 255, - }, + alpha: channels.a.map_or(255, |a| clamp_to_u8(a * 255.0)), } } @@ -222,9 +219,7 @@ pub(crate) fn color<'a>( ColorInput::Buffer(buffer) => { let (global, buffer) = (cx.global(), buffer.to_js()); Ok(CssColor::Rgba(if buffer.js_type() == JSType::DataView { - ColorChannels::from_js(global, buffer, |channels| { - Ok(rgba_from_channels(&channels)) - })? + rgba_from_channels(&ColorChannels::from_js(global, buffer)?) } else { rgba_from_list(global, buffer)? })) @@ -490,7 +485,7 @@ pub(crate) fn color<'a>( } drop(printer); - return Ok(Some(ColorOutput::Text(BunString::clone_utf8(&dest)))); + return Ok(Some(ColorOutput::Css(dest))); } } } diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index c949f1ec38e3..7b13c7c19690 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -300,6 +300,7 @@ export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { objectEcho: $bindgen("bindgen_test.bind.ts", "objectEcho"), absenceEcho: $bindgen("bindgen_test.bind.ts", "absenceEcho"), callbackOrDelay: $bindgen("bindgen_test.bind.ts", "callbackOrDelay"), + presentOnes: $bindgen("bindgen_test.bind.ts", "presentOnes"), finiteLater: $bindgen("bindgen_test.bind.ts", "finiteLater"), integerOrBigInt: $bindgen("bindgen_test.bind.ts", "integerOrBigInt"), looseInteger: $bindgen("bindgen_test.bind.ts", "looseInteger"), diff --git a/src/jsc/JSGlobalObject.rs b/src/jsc/JSGlobalObject.rs index 7d9d14bfb05e..b49c030d251d 100644 --- a/src/jsc/JSGlobalObject.rs +++ b/src/jsc/JSGlobalObject.rs @@ -665,31 +665,6 @@ impl JSGlobalObject { .throw() } - pub(crate) fn create_not_enough_arguments( - &self, - name_: &'static str, - expected: usize, - got: usize, - ) -> JSValue { - self.to_type_error( - JscError::MISSING_ARGS, - format_args!( - "Not enough arguments to '{}'. Expected {}, got {}.", - name_, expected, got - ), - ) - } - - /// Not enough arguments passed to function named `name_` - pub fn throw_not_enough_arguments( - &self, - name_: &'static str, - expected: usize, - got: usize, - ) -> JsError { - self.throw_value(self.create_not_enough_arguments(name_, expected, got)) - } - pub(crate) fn reload(&self) -> JsResult<()> { self.vm().drain_microtasks(); self.vm().collect_async(false); diff --git a/src/jsc/JSObject.rs b/src/jsc/JSObject.rs index baf0108abf34..20411d7b3e99 100644 --- a/src/jsc/JSObject.rs +++ b/src/jsc/JSObject.rs @@ -111,14 +111,6 @@ impl JSObject { Ok(obj) } - pub fn get( - &self, - global: &JSGlobalObject, - prop: impl AsRef<[u8]>, - ) -> JsResult> { - self.to_js().get(global, prop.as_ref()) - } - /// # Safety /// `owner` must be a cell-tagged `JSValue` (its payload is a live /// `JSCell*`) that remains valid for the duration of the call. diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 3a4e40f0a1a5..3c121684e2ab 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -460,15 +460,6 @@ impl JSValue { Self::js_double_number(i as f64) } } - /// `JSValue.jsNumberFromInt64` — int32 fast-path, - /// otherwise lossy double. - pub fn js_number_from_int64(i: i64) -> JSValue { - if i <= i32::MAX as i64 && i >= i32::MIN as i64 { - Self::js_number_from_int32(i as i32) - } else { - Self::js_double_number(i as f64) - } - } pub fn js_number(n: f64) -> JSValue { JSC__JSValue__jsNumberFromDouble(n) } diff --git a/src/jsc/bindgen.rs b/src/jsc/bindgen.rs index f9cf7cf05498..f94953dfe1e5 100644 --- a/src/jsc/bindgen.rs +++ b/src/jsc/bindgen.rs @@ -966,6 +966,17 @@ pub mod to_js { JsLocal::from_js_result(crate::StringJsc::into_js(value, heap.global_object())) } + #[inline] + pub fn string_array<'a>( + heap: &JsHeap<'a>, + strings: impl AsRef<[bun_core::String]>, + ) -> Result<'a, JsLocal<'a>> { + JsLocal::from_js_result(crate::bun_string_jsc::to_js_array( + heap.global_object(), + strings.as_ref(), + )) + } + #[inline] pub fn array<'a, I: IntoIterator>( heap: &JsHeap<'a>, diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index eb963e71523b..de113eae7187 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -457,7 +457,26 @@ export const objectEcho = b.fn({ args: { value: b.object }, returns: b.object, c export const TestAbsence = b.union({ missing: b.undefined, nothing: b.null, number: b.f64 }); export const absenceEcho = b.fn({ args: { value: TestAbsence }, returns: TestAbsence }); -const withThis = { passThis: true, export: "Bun__BindgenTest__callbackOrDelay", returns: b.string.utf8 }; +export const TestLenientOptions = b.dictionary({ size: b.f64.otherAsNull, callback: b.callable.otherAsNull }); +/** Which of its arguments, and of the members of `options`, are of their types. */ +export const presentOnes = b.fn({ + args: { + number: b.f64.otherAsNull, + text: b.string.utf8.otherAsNull, + callback: b.callable.otherAsNull, + point: TestPoint.otherAsNull, + counter: TestCounter.otherAsNull, + options: TestLenientOptions.otherAsNull, + }, + returns: b.array(b.string), +}); + +const withThis = { + passThis: true, + passCallee: true, + export: "Bun__BindgenTest__callbackOrDelay", + returns: b.string.utf8, +}; /** Told apart by whether the first argument can be called. */ export const callbackOrDelay = b.overloads( { ...withThis, args: { callback: b.callable }, rustPath: "crate::bindgen_test::callback_or_delay_callback" }, diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 7accfaaa19fa..79fe3419b408 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -10,10 +10,10 @@ use crate::bindgen::{ }; use crate::generated_bindgen::bindgen_test::{ Bun__BindgenTest__doubled, TestAbsence, TestAnything, TestBare, TestBareOptions, TestBracket, - TestChoice, TestColor, TestColorOrRest, TestFailure, TestFlag, TestHandle, TestNames, - TestNodeOptions, TestObjectOrRest, TestOptions, TestOptionsEcho, TestOuter, TestTextOrBytes, - TestValue, js_TestCounter, js_TestError, js_TestNonFinal, js_TestNote, js_TestRefCounted, - js_TestRefCountedNote, + TestChoice, TestColor, TestColorOrRest, TestFailure, TestFlag, TestHandle, TestLenientOptions, + TestNames, TestNodeOptions, TestObjectOrRest, TestOptions, TestOptionsEcho, TestOuter, + TestPoint, TestTextOrBytes, TestValue, js_TestCounter, js_TestError, js_TestNonFinal, + js_TestNote, js_TestRefCounted, js_TestRefCountedNote, }; use crate::{JSGlobalObject, JSString, JSValue, JsClass, JsResult}; use bun_ptr::{AsCtxPtr as _, RefPtr}; @@ -946,14 +946,50 @@ pub(crate) fn absence_echo(value: TestAbsence) -> TestAbsence { value } +pub(crate) fn present_ones( + number: Option, + text: Option<&[u8]>, + callback: Option>, + point: Option, + counter: Option<&TestCounter>, + options: Option>, +) -> Vec { + let members = options.map(|options| (options.size, options.callback)); + let (size, member) = members.unwrap_or((None, None)); + [ + ("number", number.is_some()), + ("text", text.is_some()), + ("callback", callback.is_some()), + ("point", point.map(|point| point.x).is_some()), + ("counter", counter.is_some()), + ("options", members.is_some()), + ("options.size", size.is_some()), + ("options.callback", member.is_some()), + ] + .into_iter() + .filter(|(_, is_present)| *is_present) + .map(|(name, _)| bun_core::String::static_(name)) + .collect() +} + pub(crate) fn callback_or_delay_callback( this_value: JsLocal<'_>, - _: JsLocal<'_, Callable>, + callee: JsLocal<'_>, + callback: JsLocal<'_, Callable>, ) -> Vec { - format!("callback, this is undefined: {}", this_value.is_undefined()).into_bytes() + format!( + "callback, this is undefined: {}, given itself: {}", + this_value.is_undefined(), + callee.to_js() == callback.to_js() + ) + .into_bytes() } -pub(crate) fn callback_or_delay_delay(this_value: JsLocal<'_>, delay: f64) -> Vec { +pub(crate) fn callback_or_delay_delay( + this_value: JsLocal<'_>, + _: JsLocal<'_>, + delay: f64, +) -> Vec { format!( "delay {delay}, this is undefined: {}", this_value.is_undefined() diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index be0171c5ab33..2a58d5407f22 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -26,6 +26,11 @@ template struct IDLHumanReadableName> : IDLHumanReadableName {}; +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = true; + static constexpr auto humanReadableName = std::to_array("number or Date"); +}; + template struct IDLHumanReadableName> : BaseIDLHumanReadableName { static constexpr bool hasPreposition = true; @@ -370,6 +375,21 @@ struct WebCore::Converter> } }; +template +struct WebCore::Converter> + : Bun::DefaultContextConverter> { + + using ReturnType = WebCore::Converter>::ReturnType; + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + if (auto converted = Bun::tryConvertIDL(globalObject, value, ctx)) + return WTF::move(*converted); + return IDL::nullValue(); + } +}; + template struct WebCore::Converter> : Bun::DefaultContextConverter> { diff --git a/src/jsc/bindings/Bindgen/IDLConvertNativePromiseContext.h b/src/jsc/bindings/Bindgen/IDLConvertNativePromiseContext.h index 03e09a565b96..251a6161a006 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertNativePromiseContext.h +++ b/src/jsc/bindings/Bindgen/IDLConvertNativePromiseContext.h @@ -3,7 +3,7 @@ #include "JSDOMConvertBase.h" #include "NativePromiseContext.h" -// As for IDLPromisePtr. +// No type test: only the promise calls a reaction, with the context it was given. template<> struct WebCore::Converter : WebCore::DefaultConverter { static void* convert(JSC::JSGlobalObject&, JSC::JSValue value) { diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index e99b94b0d0ed..128a7e62f1c5 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -41,6 +41,10 @@ struct IDLFalsyAsNull : WebCore::IDLNullable {}; template struct IDLEmptyStringAsNull : WebCore::IDLNullable {}; +// Treats every value that IDL does not take as null. +template +struct IDLOtherAsNull : WebCore::IDLNullable {}; + // Treats the empty value, which stands for an argument the call does not have, as null. template struct IDLNotPassedAsNull : WebCore::IDLNullable {}; diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index 8f7ec0bb7fd3..2645650ec419 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -93,11 +93,9 @@ export const indexOfLine = b.fn({ export const inflateSync = zlib("inflate_sync", ZlibDecompressionOptions); -/** What is not a number is ignored. */ -export const InspectDepth = b.union({ number: b.f64, ignored: b.any }); export const InspectOptions = b.dictionary( { - depth: InspectDepth, + depth: b.f64.otherAsNull, colors: b.bool.loose.optional, sorted: b.bool.loose.optional, compact: b.bool.loose.optional, @@ -105,7 +103,6 @@ export const InspectOptions = b.dictionary( { allowArray: true }, ); export const InspectOptionsOrDepth = b.union({ options: InspectOptions, depth: b.f64, ignored: b.any }); -export const InspectOptionsOrIgnored = b.union({ options: InspectOptions, ignored: b.any }); /** * `(value, options)` or `(value, depth, colors)`. `Bun.inspect()` is `""` and `Bun.inspect(undefined)` is @@ -187,19 +184,19 @@ export const sleepSync = b.fn({ }); /** The table of what is not an object is `""`. */ -export const TabularData = b.union({ object: b.object, ignored: b.any }); +const tabularData = b.object.otherAsNull; const printer = { export: "Bun__BunObject__table", returns: b.string.utf8, capability: "js", throws: true } as const; /** `Bun.inspect.table` */ export const table = b.overloads( { ...printer, - args: { tabularData: TabularData, properties: b.jsArray, options: InspectOptionsOrIgnored }, + args: { tabularData, properties: b.jsArray, options: InspectOptions.otherAsNull }, length: 3, rustPath: "crate::api::bun_object::table_with_properties", }, { ...printer, - args: { tabularData: TabularData, options: InspectOptionsOrIgnored }, + args: { tabularData, options: InspectOptions.otherAsNull }, rustPath: "crate::api::bun_object::table", }, ); diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index 6328b79d2544..bcb9298d9ad0 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -86,9 +86,9 @@ use bun_jsc::EncodedSliceJsc as _; use bun_jsc::{StringJsc as _, bun_string_jsc}; use crate::generated_bindgen::bun_object::{ - BracesOptions, Bun__BunObject__inspect, Bun__BunObject__table, EditorOptions, InspectDepth, - InspectOptions, InspectOptionsOrDepth, InspectOptionsOrIgnored, MMapOptions, TabularData, - WhichOptions, ZlibCompressionOptions, ZlibDecompressionOptions, ZstdCompressionOptions, + BracesOptions, Bun__BunObject__inspect, Bun__BunObject__table, EditorOptions, InspectOptions, + InspectOptionsOrDepth, MMapOptions, WhichOptions, ZlibCompressionOptions, + ZlibDecompressionOptions, ZstdCompressionOptions, }; use bun_jsc::bindgen::{self, BufferSource, JsHeap, JsLocal, JsScope, PinnedBytes}; @@ -339,9 +339,9 @@ pub(crate) fn which<'a>( fn apply_inspect_options( format_options: &mut ConsoleObject::FormatOptions, global_this: &JSGlobalObject, - options: &InspectOptions<'_>, + options: &InspectOptions, ) -> JsResult<()> { - if let InspectDepth::Number(depth) = options.depth { + if let Some(depth) = options.depth { format_options.set_depth(global_this, depth)?; } if let Some(colors) = options.colors { @@ -359,8 +359,8 @@ fn apply_inspect_options( /// `Bun.inspect.table(tabularData, options)` pub(crate) fn table<'a>( cx: &mut JsScope<'a>, - tabular_data: TabularData<'a>, - options: InspectOptionsOrIgnored<'a>, + tabular_data: Option>, + options: Option, ) -> JsResult> { print_table(cx, tabular_data, JSValue::UNDEFINED, options) } @@ -368,21 +368,21 @@ pub(crate) fn table<'a>( /// `Bun.inspect.table(tabularData, properties, options)` pub(crate) fn table_with_properties<'a>( cx: &mut JsScope<'a>, - tabular_data: TabularData<'a>, + tabular_data: Option>, properties: JsLocal<'a, JSArray>, - options: InspectOptionsOrIgnored<'a>, + options: Option, ) -> JsResult> { print_table(cx, tabular_data, properties.to_js(), options) } fn print_table( cx: &mut JsScope<'_>, - tabular_data: TabularData<'_>, + tabular_data: Option>, properties: JSValue, - options: InspectOptionsOrIgnored<'_>, + options: Option, ) -> JsResult> { let global_this = cx.global(); - let TabularData::Object(value) = tabular_data else { + let Some(value) = tabular_data else { return Ok(Vec::new()); }; let value = value.to_js(); @@ -397,7 +397,7 @@ fn print_table( single_line: true, ..Default::default() }; - if let InspectOptionsOrIgnored::Options(options) = options { + if let Some(options) = options { apply_inspect_options(&mut format_options, global_this, &options)?; } diff --git a/src/runtime/api/MarkdownObject.bind.ts b/src/runtime/api/MarkdownObject.bind.ts index 007b56db3a7c..2ee320bbe01f 100644 --- a/src/runtime/api/MarkdownObject.bind.ts +++ b/src/runtime/api/MarkdownObject.bind.ts @@ -7,14 +7,12 @@ const input = b.bytes({ from: ["string", "buffer"], shared: "copied", held: "han const expected = { input: "Expected a string or buffer to render" } as const; /** What is not an object is ignored. */ -const bag = { allowArray: true, nonObjectAsEmpty: true } as const; +const ignoresNonObjects = { allowArray: true, nonObjectAsEmpty: true } as const; const flag = b.bool.loose.optional; -/** What is not a number is ignored. */ -export const NumberOrIgnored = b.union({ number: b.f64, ignored: b.any }); -export const AutolinkOptions = b.dictionary({ url: flag, www: flag, email: flag }, bag); +export const AutolinkOptions = b.dictionary({ url: flag, www: flag, email: flag }, ignoresNonObjects); export const Autolinks = b.union({ all: b.bool, each: AutolinkOptions }); -export const HeadingOptions = b.dictionary({ ids: flag, autolink: flag }, bag); +export const HeadingOptions = b.dictionary({ ids: flag, autolink: flag }, ignoresNonObjects); export const Headings = b.union({ all: b.bool, each: HeadingOptions }); const options = { @@ -34,20 +32,19 @@ const options = { noHtmlSpans: { type: flag, altNames: ["no_html_spans"] }, tagFilter: { type: flag, altNames: ["tag_filter"] }, } as const; -export const MarkdownOptions = b.dictionary(options, bag); -export const ReactOptions = b.dictionary({ reactVersion: NumberOrIgnored, ...options }, bag); +export const MarkdownOptions = b.dictionary(options, ignoresNonObjects); +export const ReactOptions = b.dictionary({ reactVersion: b.f64.otherAsNull, ...options }, ignoresNonObjects); export const AnsiTheme = b.dictionary( - { colors: flag, hyperlinks: flag, kittyGraphics: flag, light: flag, columns: NumberOrIgnored }, - bag, + { colors: flag, hyperlinks: flag, kittyGraphics: flag, light: flag, columns: b.f64.otherAsNull }, + ignoresNonObjects, ); const members = (type: T, names: readonly Name[]) => Object.fromEntries(names.map(name => [name, type])) as Record; -export const CallbackOrIgnored = b.union({ callback: b.callable, ignored: b.any }); export const RenderCallbacks = b.dictionary( - members(CallbackOrIgnored, [ + members(b.callable.otherAsNull, [ "heading", "paragraph", "blockquote", @@ -70,7 +67,7 @@ export const RenderCallbacks = b.dictionary( "strikethrough", "text", ]), - bag, + ignoresNonObjects, ); /** The `type` of the elements of each tag. A boolean is ignored. */ @@ -106,7 +103,7 @@ export const ComponentOverrides = b.dictionary( "u", "br", ]), - bag, + ignoresNonObjects, ); const js = { capability: "js", throws: true } as const; diff --git a/src/runtime/api/MarkdownObject.rs b/src/runtime/api/MarkdownObject.rs index f643ece821e7..5bcf322b7777 100644 --- a/src/runtime/api/MarkdownObject.rs +++ b/src/runtime/api/MarkdownObject.rs @@ -1,8 +1,8 @@ //! `Bun.markdown` — html/ansi/react/render host fns over `bun_md`. use crate::generated_bindgen::markdown_object::{ - AnsiTheme, Autolinks, CallbackOrIgnored, ComponentOverrides, Headings, MarkdownOptions, - NumberOrIgnored, ReactOptions, RenderCallbacks, + AnsiTheme, Autolinks, ComponentOverrides, Headings, MarkdownOptions, ReactOptions, + RenderCallbacks, }; use bun_core::StackCheck; use bun_jsc::bindgen::{JsBytes, JsLocal, JsScope}; @@ -155,17 +155,14 @@ pub(crate) fn set_max_markdown_block_bytes_for_testing(limit: f64) -> usize { pub(crate) fn ansi<'a>( cx: &mut JsScope<'a>, input: JsBytes<'a>, - theme: &AnsiTheme<'a>, + theme: &AnsiTheme, ) -> JsResult> { let theme = md::AnsiTheme { colors: theme.colors.unwrap_or(true), hyperlinks: theme.hyperlinks.unwrap_or(false), kitty_graphics: theme.kitty_graphics.unwrap_or(false), light: theme.light.unwrap_or_else(md::detect_light_background), - columns: match theme.columns { - NumberOrIgnored::Number(columns) => columns as u16, - NumberOrIgnored::Ignored(_) => 80, - }, + columns: theme.columns.map_or(80, |columns| columns as u16), remote_image_paths: None, image_base_dir: None, }; @@ -251,12 +248,12 @@ pub(crate) fn react<'a>( cx: &mut JsScope<'a>, input: JsLocal<'a>, components: &ComponentOverrides<'a>, - options: &ReactOptions<'a>, + options: &ReactOptions, ) -> JsResult { let global_this = cx.global(); let react_version: u8 = match options.react_version { // react.element (React 18 and older) - NumberOrIgnored::Number(version) if version as i32 <= 18 => 0, + Some(version) if version as i32 <= 18 => 0, // react.transitional.element (React 19+) _ => 1, }; @@ -1056,7 +1053,7 @@ impl<'a> JsCallbackRenderer<'a> { fn extract_callbacks(&mut self, callbacks: &RenderCallbacks<'_>) { macro_rules! extract { ($($field:ident),* $(,)?) => {$( - if let CallbackOrIgnored::Callback(callback) = callbacks.$field { + if let Some(callback) = callbacks.$field { self.callbacks.$field = callback.to_js(); } )*}; diff --git a/src/runtime/api/cron.bind.ts b/src/runtime/api/cron.bind.ts index 70a21525903f..108a12a88664 100644 --- a/src/runtime/api/cron.bind.ts +++ b/src/runtime/api/cron.bind.ts @@ -27,7 +27,7 @@ const reaction = { export const onPromiseResolve = b.fn({ ...reaction, export: "Bun__CronJob__onPromiseResolve" }); export const onPromiseReject = b.fn({ ...reaction, export: "Bun__CronJob__onPromiseReject" }); -export const CronOptions = b.dictionary({ tz: b.string.utf8.nullable }, { allowArray: true }); +export const CronOptions = b.dictionary({ tz: b.string.utf8.nullable }, { allowArray: true, reportedAs: "options" }); const signature = { export: "Bun__Cron__cron", capability: "js", throws: true } as const; diff --git a/src/runtime/dns_jsc/cares_jsc.rs b/src/runtime/dns_jsc/cares_jsc.rs index e4be8c3d19f2..421d36f825a7 100644 --- a/src/runtime/dns_jsc/cares_jsc.rs +++ b/src/runtime/dns_jsc/cares_jsc.rs @@ -852,17 +852,17 @@ pub(crate) fn not_a_hostname_error(global_this: &JSGlobalObject, hostname: &[u8] pub(crate) fn canonicalize_ip<'a>( cx: &mut JsScope<'a>, - (addr_str, address): (&[u8], JsLocal<'a>), + (address, address_value): (&[u8], JsLocal<'a>), ) -> JsResult { bun_jsc::mark_binding!(); let global_this = cx.global(); let mut ip_addr = [0u8; bun_boringssl::INET6_ADDRSTRLEN + 1]; - let Some(slice) = bun_boringssl::canonicalize_ip(addr_str, &mut ip_addr) else { + let Some(slice) = bun_boringssl::canonicalize_ip(address, &mut ip_addr) else { return Ok(JSValue::UNDEFINED); }; - if addr_str == slice { - return Ok(address.to_js()); + if address == slice { + return Ok(address_value.to_js()); } bun_string_jsc::create_utf8_for_js(global_this, slice) diff --git a/src/runtime/dns_jsc/dns.rs b/src/runtime/dns_jsc/dns.rs index 8cacd539656c..c11e2d95b946 100644 --- a/src/runtime/dns_jsc/dns.rs +++ b/src/runtime/dns_jsc/dns.rs @@ -37,7 +37,7 @@ use bun_wyhash::hash as wyhash; use super::cares_jsc::error_to_deferred; use crate::generated_bindgen::dns::LookupOptions; -use crate::generated_bindgen::dns_internals::{RecordTypeArgument, ResolverOptions}; +use crate::generated_bindgen::dns_internals::ResolverOptions; use crate::socket::socket_address::inet::INET6_ADDRSTRLEN; use crate::timer::{ElTimespec, EventLoopTimer, EventLoopTimerState, EventLoopTimerTag}; use bun_cares_sys::c_ares_draft as c_ares; @@ -4916,7 +4916,7 @@ impl Resolver { pub(crate) fn global_resolve( cx: &mut JsScope<'_>, name: &[u8], - record: RecordTypeArgument<'_>, + record: Option<&[u8]>, ) -> JsResult { global_resolver(cx.global()).resolve(cx, name, record) } @@ -4925,13 +4925,13 @@ impl Resolver { &self, cx: &mut JsScope<'_>, name: &[u8], - record: RecordTypeArgument<'_>, + record: Option<&[u8]>, ) -> JsResult { let global_this = cx.global(); let context = global_this.bun_vm().context_of_caller_no_frame().id(); let record_type: RecordType = match record { - RecordTypeArgument::Ignored(_) | RecordTypeArgument::Label(b"") => RecordType::DEFAULT, - RecordTypeArgument::Label(label) => match RECORD_TYPE_MAP.get(label) { + None | Some(b"") => RecordType::DEFAULT, + Some(label) => match RECORD_TYPE_MAP.get(label) { Some(r) => *r, None => { return Err(global_this.throw_invalid_argument_property_value( diff --git a/src/runtime/dns_jsc/dns_internals.bind.ts b/src/runtime/dns_jsc/dns_internals.bind.ts index 8e49f0149a72..c9eb6f676087 100644 --- a/src/runtime/dns_jsc/dns_internals.bind.ts +++ b/src/runtime/dns_jsc/dns_internals.bind.ts @@ -4,9 +4,6 @@ export default b.module({ rustPath: "crate::dns_jsc::Resolver" }); export const getRuntimeDefaultResultOrderOption = b.fn({ returns: b.string }); -/** A non-string means "A". */ -export const RecordTypeArgument = b.union({ label: b.string.utf8, ignored: b.any }); - const method = { capability: "js", throws: true } as const; const query = { @@ -18,7 +15,8 @@ const query = { const getServers = { ...method, returns: b.any } as const; const resolve = { ...method, - args: { name: b.string.utf8, record: RecordTypeArgument }, + /** A `record` that is not a string means "A". */ + args: { name: b.string.utf8, record: b.string.utf8.otherAsNull }, expected: { name: ["string"] }, returns: b.any, } as const; diff --git a/src/runtime/ffi/ffi.bind.ts b/src/runtime/ffi/ffi.bind.ts index 7587a880e92b..fab94f6a639c 100644 --- a/src/runtime/ffi/ffi.bind.ts +++ b/src/runtime/ffi/ffi.bind.ts @@ -28,12 +28,10 @@ export const dlopen = b.fn({ export const callback = b.fn({ ...method, export: "Bun__FFI__callback", args: { options: b.any, callback: b.any } }); export const linkSymbols = b.fn({ ...method, export: "Bun__FFI__linkSymbols", args: { symbols: b.any } }); export const closeCallback = b.fn({ export: "Bun__FFI__closeCallback", args: { callback: b.any } }); -/** What is not a string is ignored. */ -export const FunctionName = b.union({ name: b.string.allowStringObject, ignored: b.any }); export const cfunction = b.fn({ ...method, export: "Bun__FFI__cfunction", - args: { options: b.any, name: FunctionName }, + args: { options: b.any, name: b.string.allowStringObject.otherAsNull }, }); export const cc = b.fn({ ...method, args: { options: b.any }, returns: FFI }); diff --git a/src/runtime/ffi/ffi_body.rs b/src/runtime/ffi/ffi_body.rs index 886dde5cac6f..9e9c4a01217f 100644 --- a/src/runtime/ffi/ffi_body.rs +++ b/src/runtime/ffi/ffi_body.rs @@ -23,7 +23,7 @@ use bun_jsc::{ use bun_paths as path; use bun_resolver::fs as Fs; -use crate::generated_bindgen::ffi::{FunctionName, js_FFI as js}; +use crate::generated_bindgen::ffi::js_FFI as js; // ─── Local shims for upstream surfaces not yet wired (Phase D) ─────────────── @@ -1671,7 +1671,7 @@ impl FFI { pub(crate) fn cfunction<'a>( cx: &mut JsScope<'a>, options: JsLocal<'a>, - name: FunctionName<'a>, + name: Option, ) -> JsResult { let global = cx.global(); let options = options.to_js(); @@ -1692,10 +1692,7 @@ impl FFI { ))); }; - let name = match name { - FunctionName::Name(name) => name, - FunctionName::Ignored(_) => bun_core::String::static_("CFunction"), - }; + let name = name.unwrap_or_else(|| bun_core::String::static_("CFunction")); if let Some(err) = function.reject_napi_types_error(global) { return Ok(err); } diff --git a/src/runtime/ipc.bind.ts b/src/runtime/ipc.bind.ts index 0bed14d3606b..d4c22fe4ade0 100644 --- a/src/runtime/ipc.bind.ts +++ b/src/runtime/ipc.bind.ts @@ -2,10 +2,9 @@ import * as b from "../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::ipc" }); -export const SentHandle = b.union({ object: b.object, ignored: b.any }); export const closeSentHandle = b.fn({ export: "Bun__IPC__closeSentHandle", - args: { handle: SentHandle }, + args: { handle: b.object.otherAsNull }, capability: "js", throws: true, }); diff --git a/src/runtime/ipc.rs b/src/runtime/ipc.rs index b5d3c1f9064d..a87586280ffd 100644 --- a/src/runtime/ipc.rs +++ b/src/runtime/ipc.rs @@ -5,7 +5,6 @@ use core::mem::size_of; use bun_jsc::JsCell; use bun_ptr::RefPtr; -use crate::generated_bindgen::ipc::SentHandle; use crate::json_line_buffer::JSONLineBuffer; use bun_collections::{ByteVecExt, VecExt}; use bun_core::{Output, handle_oom}; @@ -13,11 +12,13 @@ use bun_core::{String as BunString, strings}; use bun_io::KeepAlive; use bun_io::StreamBuffer; use bun_jsc as jsc; -use bun_jsc::bindgen::JsScope; +use bun_jsc::bindgen::{JsLocal, JsScope}; use bun_jsc::js_value::Protected; #[cfg(windows)] use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{JSGlobalObject, JSValue, JsError, JsResult, SerializedFlags, StringJsc as _, Task}; +use bun_jsc::{ + JSGlobalObject, JSObject, JSValue, JsError, JsResult, SerializedFlags, StringJsc as _, Task, +}; use bun_sys::Fd; use bun_sys::FdExt; #[cfg(windows)] @@ -830,9 +831,12 @@ impl SendHandle { } } -pub(crate) fn close_sent_handle<'a>(cx: &mut JsScope<'a>, handle: SentHandle<'a>) -> JsResult<()> { +pub(crate) fn close_sent_handle<'a>( + cx: &mut JsScope<'a>, + handle: Option>, +) -> JsResult<()> { let global = cx.global(); - if let SentHandle::Object(handle) = handle { + if let Some(handle) = handle { let js = handle.to_js(); if let Some(f) = js.get(global, "close")? { if f.is_callable() { diff --git a/src/runtime/ipc_host.bind.ts b/src/runtime/ipc_host.bind.ts index 7aa8c718e347..99e5c1e0374e 100644 --- a/src/runtime/ipc_host.bind.ts +++ b/src/runtime/ipc_host.bind.ts @@ -12,7 +12,7 @@ export const emitProcessErrorEvent = b.fn({ /** Only `null` is this process. */ export const MessageTarget = b.union({ subprocess: Subprocess, - absent: b.undefined, + missing: b.undefined, thisProcess: b.null, ignored: b.any, }); diff --git a/src/runtime/ipc_host.rs b/src/runtime/ipc_host.rs index 37d8fd06d7cb..d04d35ff10dc 100644 --- a/src/runtime/ipc_host.rs +++ b/src/runtime/ipc_host.rs @@ -310,7 +310,7 @@ pub(crate) fn emit_handle_ipc_message<'a>( MessageTarget::Subprocess(subprocess) => { subprocess.handle_ipc_message(&DecodedIPCMessage::Data(message), handle)?; } - MessageTarget::Absent | MessageTarget::Ignored(_) => {} + MessageTarget::Missing | MessageTarget::Ignored(_) => {} MessageTarget::ThisProcess => { // Cluster-internal replies that carried a descriptor (shared dgram // sockets) are marked with cmd: "NODE_CLUSTER"; hand them straight to diff --git a/src/runtime/node/Stat.bind.ts b/src/runtime/node/Stat.bind.ts index 6ffad90680af..11b5ee302d94 100644 --- a/src/runtime/node/Stat.bind.ts +++ b/src/runtime/node/Stat.bind.ts @@ -2,8 +2,8 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::node::stat" }); -/** A number wraps as `JSValue::to_uint64_no_truncate` has it. */ -export const Ino = b.union({ number: b.f64, big: b.u64.allowBigInt }); +/** A number is converted as `JSValue::to_uint64_no_truncate` does. */ +export const Ino = b.union({ number: b.f64, bigint: b.u64.allowBigInt }); export const createStatsForIno = b.fn({ args: { ino: Ino, big: b.bool.loose }, returns: b.any, diff --git a/src/runtime/node/Stat.rs b/src/runtime/node/Stat.rs index cf7ce021bce6..6b8cc3b8b375 100644 --- a/src/runtime/node/Stat.rs +++ b/src/runtime/node/Stat.rs @@ -179,7 +179,7 @@ pub(crate) fn create_stats_for_ino(cx: &mut JsScope<'_>, ino: Ino, big: bool) -> let mut stat_: PosixStat = bun_core::ffi::zeroed(); stat_.ino = match ino { Ino::Number(number) => JSValue::js_number(number).to_uint64_no_truncate(), - Ino::Big(ino) => ino, + Ino::Bigint(ino) => ino, }; Stats::init(&stat_, big).to_js_newly_created(cx.global()) } diff --git a/src/runtime/node/node_cluster_binding.bind.ts b/src/runtime/node/node_cluster_binding.bind.ts index 610de3abe5a8..ee7a621631a7 100644 --- a/src/runtime/node/node_cluster_binding.bind.ts +++ b/src/runtime/node/node_cluster_binding.bind.ts @@ -27,16 +27,16 @@ export const clusterValidateFd = b.fn({ args: { fd: b.f64, wantDgram: b.bool }, export const clusterCloseHandle = b.fn({ args: { fd: b.f64 } }); /** `process[kHandle]` of a `cluster.Worker`, which takes any object as its process. */ -export const ProcessHandle = b.union({ subprocess: Subprocess, ignored: b.any }); +const processHandle = Subprocess.otherAsNull; /** `null` when nothing was sent, `false` when the channel asks the sender to back off. */ export const sendHelperPrimary = b.fn({ - args: { processHandle: ProcessHandle, message: b.object, handle: b.object.nullable, callback: b.callable.optional }, + args: { processHandle, message: b.object, handle: b.object.nullable, callback: b.callable.optional }, returns: b.bool.nullable, length: 4, capability: "js", throws: true, }); export const onInternalMessagePrimary = b.fn({ - args: { handle: ProcessHandle, worker: b.any, callback: b.any }, + args: { handle: processHandle, worker: b.any, callback: b.any }, capability: "js", }); diff --git a/src/runtime/node/node_cluster_binding.rs b/src/runtime/node/node_cluster_binding.rs index 2703a06d4e9c..24510d772335 100644 --- a/src/runtime/node/node_cluster_binding.rs +++ b/src/runtime/node/node_cluster_binding.rs @@ -11,7 +11,7 @@ use bun_jsc::{JSGlobalObject, JSObject, JSValue, JsResult, StrongOptional}; use crate::api::bun::subprocess::Subprocess; use crate::generated_bindgen::node_cluster_binding::{ - BindAddress, BindAddressType, BindResult, BoundHandle, ProcessHandle, + BindAddress, BindAddressType, BindResult, BoundHandle, }; // Struct lives in `crate::ipc` — @@ -70,7 +70,7 @@ pub(crate) fn handle_internal_message_child( pub(crate) fn send_helper_primary<'a>( cx: &mut JsScope<'a>, - process_handle: ProcessHandle<'a>, + process_handle: Option<&Subprocess>, message: JsLocal<'a, JSObject>, handle: Option>, callback: Option>, @@ -80,7 +80,7 @@ pub(crate) fn send_helper_primary<'a>( // `cluster.Worker({ process })` accepts any object, so `process[kHandle]` // is `undefined` unless `cluster.fork()` created the process; Node's // `sendHelper` returns false for a worker with no IPC channel. - let ProcessHandle::Subprocess(subprocess) = process_handle else { + let Some(subprocess) = process_handle else { return Ok(None); }; let global = cx.global(); @@ -184,13 +184,13 @@ pub(crate) fn send_helper_primary<'a>( pub(crate) fn on_internal_message_primary<'a>( cx: &mut JsScope<'a>, - handle: ProcessHandle<'a>, + handle: Option<&Subprocess>, worker: JsLocal<'a>, callback: JsLocal<'a>, ) { // Same guard as `send_helper_primary`: nothing to subscribe to when the // worker's process has no native child handle. - let ProcessHandle::Subprocess(subprocess) = handle else { + let Some(subprocess) = handle else { return; }; let Some(ipc_data) = subprocess.ipc() else { diff --git a/src/runtime/node/node_crypto_binding.bind.ts b/src/runtime/node/node_crypto_binding.bind.ts index c58009c19734..e9ee377e91bf 100644 --- a/src/runtime/node/node_crypto_binding.bind.ts +++ b/src/runtime/node/node_crypto_binding.bind.ts @@ -33,15 +33,15 @@ const randomUUID = { capability: "heap", } as const; -const uint32 = b.u32.validateInteger().optional; +const optionalUint32 = b.u32.validateInteger().optional; export const ScryptOptions = b.dictionary( { - N: uint32, - cost: uint32, - r: uint32, - blockSize: uint32, - p: uint32, - parallelization: uint32, + N: optionalUint32, + cost: optionalUint32, + r: optionalUint32, + blockSize: optionalUint32, + p: optionalUint32, + parallelization: optionalUint32, maxmem: b.u64.validateInteger().optional, }, { allowArray: true, nonObjectAsEmpty: true, reportedAs: "" }, @@ -64,8 +64,7 @@ const argon2Parameters = (bytes: b.Type) => ({ export const crypto = b.namespace({ pbkdf2: [ { - // The body reports a digest that does not exist before a callback that is missing. - args: { ...passwordAndSaltHandles, ...pbkdf2Parameters, digest: b.string.utf8, callback: b.callable.optional }, + args: { ...passwordAndSaltHandles, ...pbkdf2Parameters, digest: b.string.utf8, callback: b.callable }, expected: { ...passwordAndSaltNames, digest: ["string"], callback: ["function"] }, rustPath: "crate::node::crypto::pbkdf2", length: 6, diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index 972b448c73c2..3ea5ea0b28f3 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -930,17 +930,9 @@ pub(crate) fn pbkdf2<'a>( iterations: u32, keylen: u32, digest: &[u8], - callback: Option>, + callback: JsLocal<'a, Callable>, ) -> JsResult<()> { - let global = cx.global(); - let algorithm = pbkdf2::algorithm_from_digest(global, digest)?; - let Some(callback) = callback else { - return Err(global.throw_invalid_argument_type_value( - b"callback", - b"function", - JSValue::UNDEFINED, - )); - }; + let algorithm = pbkdf2::algorithm_from_digest(cx.global(), digest)?; let password = isolate_for_job(cx, password)?; let salt = isolate_for_job(cx, salt)?; @@ -1132,8 +1124,22 @@ pub(crate) fn scrypt_sync( Ok(buf) } +/// rust-argon2 ignores the synthetic allocation limit. `hash_raw` reports real allocation failures. +fn argon2_exceeds_allocation_limit(memory: u32, tag_length: u32) -> bool { + let limit = jsc::virtual_machine::synthetic_allocation_limit(); + (memory as usize).saturating_mul(1024) > limit || tag_length as usize > limit +} + +/// `index` is one of `kArgon2Types` of `crypto.ts`. +fn argon2_variant(index: u8) -> rust_argon2::Variant { + match index { + 0 => rust_argon2::Variant::Argon2d, + 1 => rust_argon2::Variant::Argon2i, + _ => rust_argon2::Variant::Argon2id, + } +} + impl> Argon2 { - /// `variant` is one of `kArgon2Types` of `crypto.ts`. fn new( message: Bytes, nonce: Bytes, @@ -1145,17 +1151,6 @@ impl> Argon2 { associated_data: Bytes, variant: u8, ) -> Self { - let variant = match variant { - 0 => rust_argon2::Variant::Argon2d, - 1 => rust_argon2::Variant::Argon2i, - _ => rust_argon2::Variant::Argon2id, - }; - - // rust-argon2 ignores the synthetic allocation limit, so check it - // here. `hash_raw` reports real allocation failures. - let limit = jsc::virtual_machine::synthetic_allocation_limit(); - let failed = (memory as usize).saturating_mul(1024) > limit || tag_length as usize > limit; - Argon2 { message, nonce, @@ -1165,9 +1160,9 @@ impl> Argon2 { tag_length, memory, passes, - variant, + variant: argon2_variant(variant), output: Vec::new(), - failed, + failed: argon2_exceeds_allocation_limit(memory, tag_length), } } diff --git a/src/runtime/node/node_fs_binding.bind.ts b/src/runtime/node/node_fs_binding.bind.ts index 55a65472fa43..65302351edd5 100644 --- a/src/runtime/node/node_fs_binding.bind.ts +++ b/src/runtime/node/node_fs_binding.bind.ts @@ -75,13 +75,13 @@ export const NodeJSFS = b.class({ export const binding = b.value({ returns: NodeJSFS, capability: "js" }); -/** `(path, options)`, parsed as `fs.rm` parses them. */ export const RmOptions = b.dictionary({ retryDelay: b.u32, maxRetries: b.u32, recursive: b.bool, force: b.bool, }); +/** `(path, options)`, parsed as `fs.rm` parses them. */ export const rmOptionsForTesting = b.fn({ args: { args: b.any.variadic }, returns: RmOptions, @@ -90,10 +90,9 @@ export const rmOptionsForTesting = b.fn({ length: 2, }); export const stringToFlagsForTesting = b.fn({ args: { flags: b.any }, returns: b.i32, capability: "js", throws: true }); -export const MemfdSize = b.union({ bytes: b.f64, empty: b.undefined }); /** Without `size` it makes nothing. */ export const createMemfdForTesting = b.fn({ - args: { size: MemfdSize.ifPassed }, + args: { size: b.f64.optional }, returns: b.i32.optional, capability: "js", throws: true, diff --git a/src/runtime/node/node_fs_binding.rs b/src/runtime/node/node_fs_binding.rs index 768912a3ad9a..2551732b4485 100644 --- a/src/runtime/node/node_fs_binding.rs +++ b/src/runtime/node/node_fs_binding.rs @@ -6,7 +6,7 @@ use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{JSGlobalObject, JSPromise, JSValue, JsCell, JsResult, SysErrorJsc as _}; use bun_sys_jsc::SystemErrorJsc as _; -use crate::generated_bindgen::node_fs_binding::{MemfdSize, RmOptions}; +use crate::generated_bindgen::node_fs_binding::RmOptions; use crate::node::ThreadIsolated; use crate::node::fs::{ self, AsyncCpTask, AsyncReaddirRecursiveTask, Flavor, FsArgument, FsReturn, NodeFS, @@ -422,7 +422,7 @@ pub(crate) fn string_to_flags_for_testing( pub(crate) fn create_memfd_for_testing( cx: &mut JsScope<'_>, - size: Option, + size: Option, ) -> JsResult> { let global = cx.global(); let Some(size) = size else { @@ -439,13 +439,9 @@ pub(crate) fn create_memfd_for_testing( #[cfg(any(target_os = "linux", target_os = "android"))] { - let size = match size { - MemfdSize::Bytes(bytes) => bytes as i64, - MemfdSize::Empty => 0, - }; match bun_sys::memfd_create(c"my_memfd", bun_sys::MemfdFlags::NonExecutable) { Ok(fd) => { - let _ = bun_sys::ftruncate(fd, size); + let _ = bun_sys::ftruncate(fd, size as i64); Ok(Some(fd.native() as i32)) } Err(err) => Err(global.throw_value(err.to_js(global))), diff --git a/src/runtime/node/node_http_binding.bind.ts b/src/runtime/node/node_http_binding.bind.ts index 52684e2f6224..e09a477aa8ca 100644 --- a/src/runtime/node/node_http_binding.bind.ts +++ b/src/runtime/node/node_http_binding.bind.ts @@ -21,8 +21,6 @@ export const BunServer = b.union({ export const getBunServerAllClosedPromise = b.fn({ args: { server: BunServer }, returns: b.any, capability: "js" }); export const getBunServerOpenCount = b.fn({ args: { server: BunServer }, returns: b.f64 }); -/** `kInternalSocketData` of the socket given to `handleUpgrade()` of `ws`, which can be any `Duplex`. */ -export const UpgradeResponse = b.union({ response: NodeHTTPResponse, ignored: b.any }); export const DeflateParams = b.dictionary({ serverMaxWindowBits: b.f64.loose.nullable.default(15), clientMaxWindowBits: b.f64.loose.nullable.default(15), @@ -31,7 +29,8 @@ export const DeflateParams = b.dictionary({ }); export const upgradeNodeHTTPResponseToWebSocket = b.fn({ args: { - response: UpgradeResponse, + /** `kInternalSocketData` of the socket given to `handleUpgrade()` of `ws`, which can be any `Duplex`. */ + response: NodeHTTPResponse.otherAsNull, websocket: b.any, handlers: b.any, handshake: b.string.utf8, diff --git a/src/runtime/node/node_http_binding.rs b/src/runtime/node/node_http_binding.rs index 137de0be470c..d92dcbc50081 100644 --- a/src/runtime/node/node_http_binding.rs +++ b/src/runtime/node/node_http_binding.rs @@ -7,8 +7,8 @@ use bun_jsc::{JSValue, JsResult}; use bun_uws_sys::Opcode; use bun_uws_sys::response::PerMessageDeflateParams; -use crate::generated_bindgen::node_http_binding::{BunServer, DeflateParams, UpgradeResponse}; -use crate::server::ServerWebSocket; +use crate::generated_bindgen::node_http_binding::{BunServer, DeflateParams}; +use crate::server::{NodeHTTPResponse, ServerWebSocket}; pub(crate) fn get_bun_server_all_closed_promise<'a>( cx: &mut JsScope<'a>, @@ -47,7 +47,7 @@ pub(crate) fn get_bun_server_open_count(server: BunServer<'_>) -> f64 { /// longer be upgraded. pub(crate) fn upgrade_node_http_response_to_web_socket<'a>( cx: &mut JsScope<'a>, - response: UpgradeResponse<'a>, + response: Option<&NodeHTTPResponse>, websocket: JsLocal<'a>, handlers: JsLocal<'a>, handshake: &[u8], @@ -58,7 +58,7 @@ pub(crate) fn upgrade_node_http_response_to_web_socket<'a>( const AUTO_PONG: i32 = 1 << 0; const SKIP_UTF8_VALIDATION: i32 = 1 << 1; - let UpgradeResponse::Response(response) = response else { + let Some(response) = response else { return Ok(JSValue::UNDEFINED); }; let global = cx.global(); diff --git a/src/runtime/server/RequestContext.bind.ts b/src/runtime/server/RequestContext.bind.ts index d81f7913710e..6f544d4e6f82 100644 --- a/src/runtime/server/RequestContext.bind.ts +++ b/src/runtime/server/RequestContext.bind.ts @@ -14,7 +14,7 @@ function reactions(debug: boolean, mux: boolean, ssl: boolean) { }); return [ reaction("onResolve", "on_resolve"), - // With no claim left to take, it takes what the cell holds. + // After an upgrade the cell holds the server, which reports the rejection. reaction("onReject", "on_reject", b.nativePromiseContext(rustPath).withValue), reaction("onResolveStream", "on_resolve_stream"), reaction("onRejectStream", "on_reject_stream"), diff --git a/src/runtime/socket/Listener.bind.ts b/src/runtime/socket/Listener.bind.ts index 42438d484b9d..e31fde108750 100644 --- a/src/runtime/socket/Listener.bind.ts +++ b/src/runtime/socket/Listener.bind.ts @@ -36,7 +36,7 @@ export const addServerName = b.fn({ rustPath: "crate::socket::Listener::add_server_name", }); -/** The `_handle` of a `tls.Server`. A cluster worker's is no `Listener`: JS wraps its connections. */ -export const ServerHandle = b.union({ listener: Listener, ignored: b.any }); -export const ListenerContext = b.union({ secureContext: SecureContext, ignored: b.any }); -export const setSecureContext = b.fn({ args: { handle: ServerHandle, context: ListenerContext } }); +/** `handle` is the `_handle` of a `tls.Server`. A cluster worker's is no `Listener`: JS wraps its connections. */ +export const setSecureContext = b.fn({ + args: { handle: Listener.otherAsNull, context: SecureContext.otherAsNull }, +}); diff --git a/src/runtime/socket/Listener.rs b/src/runtime/socket/Listener.rs index c7e6dbe2f531..bc92468bed92 100644 --- a/src/runtime/socket/Listener.rs +++ b/src/runtime/socket/Listener.rs @@ -19,7 +19,7 @@ use bun_uws as uws; use bun_uws_sys as uws_sys; use crate::api::bun_secure_context::SecureContext; -use crate::generated_bindgen::listener::{ListenerContext, ServerHandle, js_Listener as js}; +use crate::generated_bindgen::listener::js_Listener as js; use crate::socket::{ Handlers, NewSocket, SocketConfig, SocketFlags, SocketMode, TCPSocket, TLSSocket, }; @@ -1594,10 +1594,8 @@ fn connect_finish( } /// `tls.Server#setSecureContext()` while listening. Accepted sockets keep their context. -pub(crate) fn set_secure_context(handle: ServerHandle<'_>, context: ListenerContext<'_>) { - let (ServerHandle::Listener(this), ListenerContext::SecureContext(secure_context)) = - (handle, context) - else { +pub(crate) fn set_secure_context(handle: Option<&Listener>, context: Option<&SecureContext>) { + let (Some(this), Some(secure_context)) = (handle, context) else { return; }; if !this.ssl { diff --git a/src/runtime/socket/socket_body.bind.ts b/src/runtime/socket/socket_body.bind.ts index 4603a985b747..e972dd282584 100644 --- a/src/runtime/socket/socket_body.bind.ts +++ b/src/runtime/socket/socket_body.bind.ts @@ -172,16 +172,19 @@ export const SocketFaultErrnoName = b.enumeration([ ]); export const SocketFaultErrno = b.union({ name: SocketFaultErrnoName, code: b.i32 }); const count = b.i32.wrap.emptyStringAsNull; -export const SocketFaultRule = b.dictionary({ - syscall: SocketFaultSyscall, - action: SocketFaultAction, - /** `errno` is a macro in C++. */ - errno: { type: SocketFaultErrno.emptyStringAsNull, rustName: "error" }, - bytes: count, - fd: count, - after: count, - repeat: count, -}); +export const SocketFaultRule = b.dictionary( + { + syscall: SocketFaultSyscall, + action: SocketFaultAction, + /** `errno` is a macro in C++. */ + errno: { type: SocketFaultErrno.emptyStringAsNull, rustName: "error" }, + bytes: count, + fd: count, + after: count, + repeat: count, + }, + { reportedAs: "rule" }, +); export const setSocketFault = b.fn({ ...js, args: { rule: SocketFaultRule }, diff --git a/src/runtime/test_runner/BunTest.bind.ts b/src/runtime/test_runner/BunTest.bind.ts index e6f571446f7b..800c081aa7ce 100644 --- a/src/runtime/test_runner/BunTest.bind.ts +++ b/src/runtime/test_runner/BunTest.bind.ts @@ -12,9 +12,9 @@ const hook = (name: string) => export: `Bun__BunTest__${name}`, args: { first: DescriptionOrCallback, second: CallbackOrOptions, third: CallbackOrOptions }, expected: { - first: "The first argument must be a named class, named function, number, or string", - second: "The second argument must be a function, number, or object", - third: "The third argument must be a function, number, or object", + first: `${name}() expects a function as the first argument`, + second: `${name}() expects a number or object as the second argument`, + third: `${name}() expects a number or object as the third argument`, }, length: 1, ...js, diff --git a/src/runtime/test_runner/ScopeFunctions.bind.ts b/src/runtime/test_runner/ScopeFunctions.bind.ts index 3b382dd9cad8..87e08782fdd4 100644 --- a/src/runtime/test_runner/ScopeFunctions.bind.ts +++ b/src/runtime/test_runner/ScopeFunctions.bind.ts @@ -5,13 +5,13 @@ export default b.module({ rustPath: "crate::test_runner::scope_functions" }); /** What `test`, `describe` and the hooks take in place of a timeout. */ export const BunTestOptions = b.dictionary( { timeout: b.f64.optional, retry: b.f64.optional, repeats: b.f64.optional }, - { allowArray: true }, + { allowArray: true, reportedAs: "options" }, ); /** A function here is the callback if none follows it, and else what the test is named after. */ export const DescriptionOrCallback = b.union({ nothing: b.null, - callable: b.callable, + function: b.callable, text: b.string.utf8.owned, number: b.f64, }); diff --git a/src/runtime/test_runner/ScopeFunctions.rs b/src/runtime/test_runner/ScopeFunctions.rs index 07e958a93112..c0fbed68a06b 100644 --- a/src/runtime/test_runner/ScopeFunctions.rs +++ b/src/runtime/test_runner/ScopeFunctions.rs @@ -546,7 +546,7 @@ pub(crate) fn parse_arguments<'a>( // callback // callback, options ( - DescriptionOrCallback::Callable(callback), + DescriptionOrCallback::Function(callback), options @ (CallbackOrOptions::Nothing | CallbackOrOptions::Timeout(_) | CallbackOrOptions::Options(_)), CallbackOrOptions::Nothing, ) => (DescriptionOrCallback::Nothing, CallbackOrOptions::Callback(callback), options), @@ -600,7 +600,7 @@ pub(crate) fn parse_arguments<'a>( result.description = match description { DescriptionOrCallback::Nothing => None, - DescriptionOrCallback::Callable(callable) => Some(name_of_callable(global, callable, signature)?), + DescriptionOrCallback::Function(callable) => Some(name_of_callable(global, callable, signature)?), DescriptionOrCallback::Text(text) => Some(text.into_vec()), DescriptionOrCallback::Number(number) => Some(bun_core::fmt::FormatDouble::dtoa(&mut [0; 124], number).to_vec()), }; diff --git a/src/runtime/test_runner/jest.bind.ts b/src/runtime/test_runner/jest.bind.ts index 49974802fa62..f312c7c290a7 100644 --- a/src/runtime/test_runner/jest.bind.ts +++ b/src/runtime/test_runner/jest.bind.ts @@ -7,10 +7,8 @@ export const nodeTestMarkResult = b.fn({ args: { todo: b.bool, done: b.any } }); const js = { capability: "js", throws: true } as const; -/** Outside of `bun test`, and in a preload, `Bun.jest` takes anything. */ -export const TestFilePath = b.union({ path: b.string.utf8, other: b.any }); -/** `Bun.jest(path)` is what `import "bun:test"` gives the file at `path`. */ -export const jest = b.fn({ args: { path: TestFilePath }, returns: b.any, ...js }); +/** `Bun.jest(path)` is what `import "bun:test"` gives the file at `path`. Outside of `bun test`, and in a preload, it takes anything. */ +export const jest = b.fn({ args: { path: b.string.utf8.otherAsNull }, returns: b.any, ...js }); /** Also `jest.setTimeout`. */ export const setDefaultTimeout = b.fn({ export: "Bun__Jest__setDefaultTimeout", diff --git a/src/runtime/test_runner/jest.rs b/src/runtime/test_runner/jest.rs index 3c9660df1954..5e04559c37b8 100644 --- a/src/runtime/test_runner/jest.rs +++ b/src/runtime/test_runner/jest.rs @@ -19,7 +19,7 @@ use crate::generated_bindgen::bun_test::{ Bun__BunTest__beforeEach, Bun__BunTest__onTestFinished, }; use crate::generated_bindgen::expect::{js_Expect, js_ExpectTypeOf}; -use crate::generated_bindgen::jest::{Bun__Jest__setDefaultTimeout, TestFilePath}; +use crate::generated_bindgen::jest::Bun__Jest__setDefaultTimeout; use super::scope_functions::{create_bound, Mode as ScopeKind}; use super::snapshot::Snapshots; use super::timers::fake_timers; @@ -473,14 +473,14 @@ pub(crate) mod Jest { } /// `Bun.jest(path)` -pub(crate) fn jest(cx: &mut JsScope<'_>, path: TestFilePath<'_>) -> JsResult { +pub(crate) fn jest(cx: &mut JsScope<'_>, path: Option<&[u8]>) -> JsResult { let global_object = cx.global(); let vm = global_object.bun_vm(); if vm.is_in_preload || Jest::runner().is_none() { // in preload, no arguments needed } else { - let TestFilePath::Path(path) = path else { + let Some(path) = path else { return Err(global_object.throw(format_args!("Bun.jest() expects a string filename"))); }; diff --git a/src/runtime/test_runner/timers/FakeTimers.bind.ts b/src/runtime/test_runner/timers/FakeTimers.bind.ts index e22e64e9f41c..5d93ec4b1a14 100644 --- a/src/runtime/test_runner/timers/FakeTimers.bind.ts +++ b/src/runtime/test_runner/timers/FakeTimers.bind.ts @@ -5,14 +5,17 @@ export default b.module({ rustPath: "crate::test_runner::timers::fake_timers" }) /** Returns `this`, which is `jest` or `vi`. */ const chainable = { passThis: true, returns: b.any, capability: "js", throws: true } as const; -export const FakeTimersOptions = b.dictionary({ now: b.f64.allowDate.optional }, { allowArray: true }); -/** Jest 26 took `"modern"` or `"legacy"`. */ -export const FakeTimersArgument = b.union({ implementation: b.string.utf8, options: FakeTimersOptions }); +export const FakeTimersOptions = b.dictionary( + { now: b.f64.allowDate.optional }, + { allowArray: true, reportedAs: "options" }, +); +/** Jest 26 took `"modern"` or `"legacy"`. It is ignored. */ +export const FakeTimersImplementationOrOptions = b.union({ implementation: b.string.utf8, options: FakeTimersOptions }); export const useFakeTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__useFakeTimers", - args: { options: FakeTimersArgument.optional }, + args: { options: FakeTimersImplementationOrOptions.optional }, expected: { options: "useFakeTimers() expects an options object" }, }); export const useRealTimers = b.fn({ ...chainable, export: "Bun__FakeTimers__useRealTimers" }); diff --git a/src/runtime/test_runner/timers/FakeTimers.rs b/src/runtime/test_runner/timers/FakeTimers.rs index 3f29c6dcd87a..6e617c88bddf 100644 --- a/src/runtime/test_runner/timers/FakeTimers.rs +++ b/src/runtime/test_runner/timers/FakeTimers.rs @@ -6,7 +6,7 @@ use crate::generated_bindgen::fake_timers::{ Bun__FakeTimers__advanceTimersByTime, Bun__FakeTimers__advanceTimersToNextTimer, Bun__FakeTimers__clearAllTimers, Bun__FakeTimers__getTimerCount, Bun__FakeTimers__isFakeTimers, Bun__FakeTimers__runAllTimers, Bun__FakeTimers__runOnlyPendingTimers, - Bun__FakeTimers__useFakeTimers, Bun__FakeTimers__useRealTimers, FakeTimersArgument, + Bun__FakeTimers__useFakeTimers, Bun__FakeTimers__useRealTimers, FakeTimersImplementationOrOptions, }; use crate::jsc::virtual_machine::VirtualMachine; use crate::timer::{ @@ -341,14 +341,14 @@ fn set_fake_timer_marker(global: &JSGlobalObject, enabled: bool) -> JsResult<()> pub(crate) fn use_fake_timers( cx: &mut JsScope<'_>, this_value: JsLocal<'_>, - options: Option>, + options: Option>, ) -> JsResult { let global = cx.global(); // SAFETY: FFI call into C++ JSMock let mut js_now = JSMock__getCurrentUnixTimeMs(); // Jest 26 compat: useFakeTimers("modern" | "legacy") is a no-op. - if let Some(FakeTimersArgument::Options(options)) = options + if let Some(FakeTimersImplementationOrOptions::Options(options)) = options && let Some(now) = options.now { // NaN is `JSGlobalObject::overridenDateNow`'s "no override" sentinel. diff --git a/src/runtime/webcore/Blob.bind.ts b/src/runtime/webcore/Blob.bind.ts index 7e0bdbb77275..5e16b8ab6db0 100644 --- a/src/runtime/webcore/Blob.bind.ts +++ b/src/runtime/webcore/Blob.bind.ts @@ -39,10 +39,10 @@ export const Blob = b.class({ }, }); -/** What `JSValue::as_class_ref::` finds a `Blob` in. */ +/** A `Blob`, or an output of `Bun.build()`, which holds one. */ export const BlobOrBuildArtifact = b.union({ blob: Blob, buildArtifact: BuildArtifact }); -/** What is not a string is not a type, and nor is `""`. A `String` object of it is the type `""`. */ +/** Anything but a string is ignored, and so is `""`. `new String("")` sets the type to `""`. */ export const BlobType = b.union({ text: b.string, stringObject: b.string.allowStringObject, ignored: b.any }); /** What `Bun.file` reads of its options, unless the path is `s3://`. */ export const BunFileOptions = b.dictionary( @@ -55,7 +55,7 @@ export const file = b.fn({ ...js, args: { path: b.any, options: b.any }, returns /** What `Bun.write` itself reads of its options. */ export const BunWriteOptions = b.dictionary( { createPath: b.bool.emptyStringAsNull, mode: b.f64.nullable }, - { allowArray: true, fromJS: true }, + { allowArray: true, fromJS: true, reportedAs: "options" }, ); /** `options` is also what a write to S3 takes. */ export const write = b.fn({ ...method, args: { destination: b.any, data: b.any, options: b.any }, length: 1 }); diff --git a/src/runtime/webcore/Crypto.bind.ts b/src/runtime/webcore/Crypto.bind.ts index 29f70c9bcd75..e71f99860e88 100644 --- a/src/runtime/webcore/Crypto.bind.ts +++ b/src/runtime/webcore/Crypto.bind.ts @@ -2,9 +2,6 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::webcore::crypto" }); -/** What is not a string is not converted: it is "hex". */ -export const UUIDEncoding = b.union({ label: b.string.utf8, ignored: b.any }); - /** `bytes` is a `Buffer`, for the encoding "buffer". */ export const EncodedUUID = b.union({ text: b.string, bytes: b.bufferSource }); @@ -13,7 +10,8 @@ export const randomUUIDv5 = b.fn({ name: b.bytes({ from: ["string", "buffer"] }), /** A string is a UUID or the name of one. A buffer is its 16 bytes. */ namespace: b.bytes({ from: ["string", "buffer"], string: "asIs", held: "handle" }), - encoding: UUIDEncoding, + /** What is not a string means "hex". */ + encoding: b.string.utf8.otherAsNull, }, returns: EncodedUUID, capability: "heap", @@ -24,7 +22,7 @@ export const randomUUIDv5 = b.fn({ /** `clock` is the time now. */ export const UUIDTimestamp = b.union({ clock: b.undefined, time: b.f64.allowDate }); -/** Alone, what is not a string is the timestamp. Before a timestamp it is as in `UUIDEncoding`. */ +/** Without a second argument, a first one that is not a string is the timestamp. With one, it means "hex". */ export const UUIDEncodingOrTimestamp = b.union({ label: b.string.utf8, clock: b.undefined, @@ -35,6 +33,7 @@ export const UUIDEncodingOrTimestamp = b.union({ /** `randomUUIDv7(timestamp)` takes the timestamp. `randomUUIDv7(timestamp, undefined)` takes the clock's. */ export const randomUUIDv7 = b.fn({ args: { encoding: UUIDEncodingOrTimestamp, timestamp: UUIDTimestamp.ifPassed }, + expected: { timestamp: "timestamp must be a number or Date" }, returns: EncodedUUID, capability: "heap", throws: true, diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 5cf0da6c7e03..b6cb48f5c0d4 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -4,9 +4,7 @@ use bun_jsc::uuid::{self, UUID, UUID5, UUID7}; use bun_jsc::virtual_machine::VirtualMachine; use bun_jsc::{JSGlobalObject, JSType, JSValue, JsClass as _}; -use crate::generated_bindgen::crypto::{ - EncodedUUID, UUIDEncoding, UUIDEncodingOrTimestamp, UUIDTimestamp, -}; +use crate::generated_bindgen::crypto::{EncodedUUID, UUIDEncodingOrTimestamp, UUIDTimestamp}; use crate::node::Encoding; pub(crate) struct Crypto; @@ -197,11 +195,11 @@ pub(crate) fn random_uuid_v5<'a>( heap: &JsHeap<'a>, name: &[u8], namespace: StringOrJsBytes<'a>, - encoding: UUIDEncoding<'a>, + encoding: Option<&[u8]>, ) -> bindgen::Result<'a, EncodedUUID<'a>> { let encoding = match encoding { - UUIDEncoding::Label(label) => encoding_from_label(heap, label)?, - UUIDEncoding::Ignored(_) => Encoding::Hex, + Some(label) => encoding_from_label(heap, label)?, + None => Encoding::Hex, }; let invalid_value = |message: &str| { heap.error_with_code( diff --git a/src/runtime/webcore/fetch.bind.ts b/src/runtime/webcore/fetch.bind.ts index abdd85647a6b..1b9ce0352974 100644 --- a/src/runtime/webcore/fetch.bind.ts +++ b/src/runtime/webcore/fetch.bind.ts @@ -3,7 +3,7 @@ import * as b from "../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::webcore::fetch" }); const js = { capability: "js", throws: true } as const; -/** An `input` that is no `Request`, and whose `ToString` is no URL, is read as `init` is. */ +/** An `input` that is neither a `Request` nor, after `ToString`, a URL is read as `init` is. */ const request = { args: { input: b.any, init: b.any }, returns: b.any, ...js, rejects: true, length: 1 } as const; export const fetch = b.fn(request); diff --git a/src/sql_jsc/mysql.bind.ts b/src/sql_jsc/mysql.bind.ts index bd0b81b12191..d08e0a0a0332 100644 --- a/src/sql_jsc/mysql.bind.ts +++ b/src/sql_jsc/mysql.bind.ts @@ -26,7 +26,7 @@ export const binding = b.namespace({ returns: MySQLQuery, rustPath: "crate::mysql::js_my_sql_query::JSMySQLQuery::create_query", }, - /** The arguments of `NativeCreateConnection` in `src/js/internal/sql/pool.ts`. The ones that are `b.any`, but `tls`, are for Postgres. */ + /** The arguments of `NativeCreateConnection` in `src/js/internal/sql/pool.ts`. The ones that are `b.any`, except `tls`, are for Postgres. */ createConnection: { ...js, args: { diff --git a/src/sql_jsc/postgres.bind.ts b/src/sql_jsc/postgres.bind.ts index 58d2438c739a..1e03963aaa15 100644 --- a/src/sql_jsc/postgres.bind.ts +++ b/src/sql_jsc/postgres.bind.ts @@ -26,7 +26,7 @@ export const binding = b.namespace({ returns: PostgresSQLQuery, rustPath: "crate::postgres::PostgresSQLQuery::create_query", }, - /** The arguments of `NativeCreateConnection` in `src/js/internal/sql/pool.ts`. The ones that are `b.any`, but `tls`, are for MySQL. */ + /** The arguments of `NativeCreateConnection` in `src/js/internal/sql/pool.ts`. The ones that are `b.any`, except `tls`, are for MySQL. */ createConnection: { ...js, args: { diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index e44331cc7670..9641655fdcda 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -109,11 +109,59 @@ itAlways("mistakes in a schema are errors when it loads", () => { "f: write what b.fn takes, without b.fn", ); expect(() => b.namespace({ "not-a-name": {} })).toThrow("not-a-name: a function is named by an identifier"); + for (const key of ["passThis", "passCallee", "export"]) { + expect(() => b.namespace({ f: { [key]: true } })).toThrow(`f: a function of a namespace has no ${key}`); + } expect(() => b.array(b.null)).toThrow("an array of nothing but null"); const stringOrBuffer = b.bytes({ from: ["string", "buffer"] }); expect(() => b.array(stringOrBuffer)).toThrow("the bytes of a buffer are only for an argument"); expect(() => b.dictionary({ bytes: stringOrBuffer })).toThrow("the bytes of a buffer are only for an argument"); expect(() => b.union({ bytes: stringOrBuffer })).toThrow("the bytes of a buffer are only for an argument"); + for (const [type, reason] of [ + [b.i32.withValue, "b.bytes and .withValue are only for an argument"], + [b.i32.ifPassed, ".ifPassed is only for an argument"], + [b.string.owned, ".owned is only for an argument"], + [b.promisePtr("T"), "the context of a promise reaction is only an argument"], + [b.nativePromiseContext("T"), "the context of a promise reaction is only an argument"], + ] as const) { + expect(() => b.array(type)).toThrow(reason); + expect(() => b.dictionary({ member: type })).toThrow(`member: ${reason}`); + expect(() => b.fn({ returns: type, capability: "heap" })).toThrow(reason); + } + for (const type of [b.i32.withValue, b.string.owned, b.promisePtr("T")]) { + expect(() => b.union({ alternative: type, nothing: b.null })).toThrow("alternative: "); + } + expect(() => b.string.owned.optional).toThrow(".owned is only for an argument"); + expect(() => b.promisePtr("T").nullable).toThrow("the context of a promise reaction is only an argument"); + expect(() => b.string.owned.owned).toThrow("already .owned"); + expect(() => b.string.owned.local).toThrow(".local follows b.string"); + expect(() => b.i32.withValue.optional).toThrow("put .withValue last"); + expect(() => b.i32.withValue.default(0)).toThrow("put .withValue last"); + expect(() => b.i32.ifPassed.withValue).toThrow(".ifPassed has no argument to give when it is absent"); + expect(() => b.any.withValue).toThrow(".withValue: the type is the argument itself"); + expect(() => b.bytes({ from: ["buffer"], held: "handle" }).withValue).toThrow( + ".withValue: b.bytes already passes the argument", + ); + expect(() => b.f64.loose.allowDate).toThrow(".allowDate follows b.f64"); + expect(() => b.f64.allowDate.loose).toThrow(".loose follows b.f64"); + expect(() => b.any.otherAsNull).toThrow("b.any already accepts undefined and null"); + expect(() => b.string.loose.otherAsNull).toThrow("any takes every value"); + expect(() => b.i32.otherAsNull).toThrow("an integer type reports a number it does not take"); + expect(() => b.fn({ args: { context: b.nativePromiseContext("T"), value: b.i32 }, export: "Bun__reaction" })).toThrow( + "b.nativePromiseContext takes the pointer out of the cell", + ); + expect(() => b.overloads({ args: { a: b.i32.ifPassed } }, { args: { a: b.string } })).toThrow( + ".ifPassed is not for overloads", + ); + expect(() => b.fn({ returns: b.i32, rejects: true })).toThrow("rejects: the result is a promise"); + expect(() => b.fn({ args: { size: b.u8.validateInteger(0, 4) }, expected: { size: ["number"] } })).toThrow( + "expected: size is a range", + ); + const point = b.dictionary({ x: b.f64 }); + for (const later of [b.jsArray, b.callable, b.bufferSource, b.array(b.f64)]) { + expect(() => b.union({ point, later, text: b.string })).toThrow("later is never chosen"); + expect(() => b.union({ object: b.object, later, text: b.string })).toThrow("later is never chosen"); + } for (const held of ["handle", "copied", "pinned"] as const) { const bytes = b.bytes({ from: ["string", "buffer"], held }); expect(() => b.array(bytes)).toThrow("b.bytes and .withValue are only for an argument"); @@ -734,11 +782,14 @@ describe("functions", () => { expect(bindgen.testNamespace.negate(true)).toBe(false); }); - it("overloads have passThis and export, and tell a function from a number", () => { + it("overloads have passThis, passCallee and export, and tell a function from a number", () => { const { callbackOrDelay } = bindgen; - expect(callbackOrDelay.call(undefined, () => {})).toBe("callback, this is undefined: true"); - expect(callbackOrDelay.call(undefined, new Proxy(() => {}, {}))).toBe("callback, this is undefined: true"); - expect(callbackOrDelay.call({}, class {})).toBe("callback, this is undefined: false"); + expect(callbackOrDelay.call(undefined, () => {})).toBe("callback, this is undefined: true, given itself: false"); + expect(callbackOrDelay.call(undefined, new Proxy(() => {}, {}))).toBe( + "callback, this is undefined: true, given itself: false", + ); + expect(callbackOrDelay.call({}, class {})).toBe("callback, this is undefined: false, given itself: false"); + expect(callbackOrDelay.call({}, callbackOrDelay)).toBe("callback, this is undefined: false, given itself: true"); expect(callbackOrDelay.call({}, 5)).toBe("delay 5, this is undefined: false"); expect(() => callbackOrDelay({})).toThrow(TypeError); }); @@ -754,6 +805,28 @@ describe("functions", () => { expect(bindgen.isPassed(1, undefined)).toBe(true); }); + it(".otherAsNull is absent for every value the type does not take", () => { + const { presentOnes } = bindgen; + expect(presentOnes()).toEqual([]); + expect(presentOnes(1.5, "", () => {}, { x: 1 }, new bindgen.TestCounter(1), {})).toEqual([ + "number", + "text", + "callback", + "point", + "counter", + "options", + ]); + expect(presentOnes("1", 1, {}, 1, {}, 1)).toEqual([]); + expect(presentOnes(null, Symbol(), null, "x", null, null)).toEqual([]); + expect(presentOnes(NaN, new String("s"), new Proxy(() => {}, {}))).toEqual(["number", "text", "callback"]); + const options = (value: unknown) => presentOnes(0, 0, 0, 0, 0, value).slice(1); + expect(options({ size: 1, callback() {} })).toEqual(["options", "options.size", "options.callback"]); + expect(options({ size: "1", callback: {} })).toEqual(["options"]); + // An object is a dictionary, whose own members still report what is wrong with them. + expect(() => presentOnes(0, 0, 0, {})).toThrow("TestPoint.x is required"); + expect(presentOnes.length).toBe(0); + }); + it(".withValue also gives the argument", () => { const text = { toString: () => "four" }; expect(bindgen.lengthWithValue(text)).toEqual([4, text]); @@ -764,7 +837,7 @@ describe("functions", () => { expect(() => bindgen.counterWithValue({})).toThrow(TypeError); }); - it("b.string.owned is the string", () => { + it("b.string.owned takes what b.string takes", () => { expect(bindgen.keptString("kept")).toBe("kept"); expect(bindgen.keptString("")).toBe(""); expect(() => bindgen.keptString(1)).toThrow(TypeError); @@ -784,8 +857,8 @@ describe("functions", () => { const [thrown, refused] = [bindgen.finiteLater(Infinity), bindgen.finiteLater("1")]; expect(thrown).toBeInstanceOf(Promise); expect(refused).toBeInstanceOf(Promise); - expect(thrown).rejects.toThrow("value must be finite"); - expect(refused).rejects.toThrow("value must be a number"); + await expect(thrown).rejects.toThrow("value must be finite"); + await expect(refused).rejects.toThrow("value must be a number"); }); }); @@ -813,8 +886,8 @@ describe("a dictionary of a node:* function", () => { }); }); -describe("values as themselves", () => { - it("a union tells a function, an array and an object apart, and what is not an object passes a dictionary", () => { +describe("b.callable, b.jsArray, b.object and b.undefined", () => { + it("a union tells a function, an array and an object apart, and what is not an object skips a dictionary that is not last", () => { expect(bindgen.describeHandle(undefined)).toBe("missing"); expect(bindgen.describeHandle()).toBe("missing"); expect(bindgen.describeHandle(null)).toBe("nothing"); @@ -826,7 +899,7 @@ describe("values as themselves", () => { expect(bindgen.describeHandle(3)).toBe("text 3"); expect(bindgen.describeHandle(4n)).toBe("text 4"); expect(bindgen.describeHandle("s")).toBe("text s"); - expect(() => bindgen.describeHandle({})).toThrow("x"); + expect(() => bindgen.describeHandle({})).toThrow("TestPoint.x is required"); expect(() => bindgen.describeHandle(Symbol())).toThrow(TypeError); }); diff --git a/test/js/bun/cron/cron-local-time.test.ts b/test/js/bun/cron/cron-local-time.test.ts index 0d7e9d18972e..8fc8ee87ee63 100644 --- a/test/js/bun/cron/cron-local-time.test.ts +++ b/test/js/bun/cron/cron-local-time.test.ts @@ -268,7 +268,7 @@ describe.concurrent("Bun.cron.parse — { tz } option", () => { test("non-string tz throws", () => { // @ts-expect-error - expect(() => Bun.cron.parse("* * * * *", Date.now(), { tz: 42 })).toThrow(/Options\.tz must be a string/); + expect(() => Bun.cron.parse("* * * * *", Date.now(), { tz: 42 })).toThrow(/options\.tz must be a string/); }); test("tz: undefined falls back to local", async () => { diff --git a/test/js/bun/patch/patch.test.ts b/test/js/bun/patch/patch.test.ts index a09c1d12a294..d2c7d7f0d701 100644 --- a/test/js/bun/patch/patch.test.ts +++ b/test/js/bun/patch/patch.test.ts @@ -965,7 +965,7 @@ describe("apply", () => { // TODO: simple, multiline, multiple hunks }); - // Each of these hits an error path in `parse_apply_args`. The native code must + // Each of these hits an error path in `apply`. The native code must // surface the failure as a catchable JS exception; it used to return a normal // value while the exception was still pending, which aborts debug/ASAN builds // with "Unexpected exception observed" (releaseAssertNoException). diff --git a/test/js/bun/util/socket-fault-injection.test.ts b/test/js/bun/util/socket-fault-injection.test.ts index e2e53b27e1b7..9be29cbd971e 100644 --- a/test/js/bun/util/socket-fault-injection.test.ts +++ b/test/js/bun/util/socket-fault-injection.test.ts @@ -12,12 +12,12 @@ describe.skipIf(skip)("socketFaultInjection control surface", () => { test("set() validates syscall", () => { expect(() => fault.set({ syscall: "bogus" as any, action: "errno", errno: "ECONNRESET" })).toThrow( - /SocketFaultRule\.syscall must be/, + /rule\.syscall must be/, ); }); test("set() validates action", () => { - expect(() => fault.set({ syscall: "recv", action: "bogus" as any })).toThrow(/SocketFaultRule\.action must be/); + expect(() => fault.set({ syscall: "recv", action: "bogus" as any })).toThrow(/rule\.action must be/); }); // Only recv/send/writev have a byte count to clamp; arming "short" on any other @@ -66,7 +66,7 @@ describe.skipIf(skip)("socketFaultInjection control surface", () => { test("set() rejects unknown errno name", () => { expect(() => fault.set({ syscall: "recv", action: "errno", errno: "ENOSUCHERR" as any })).toThrow( - /SocketFaultRule\.errno must be/, + /rule\.errno must be/, ); }); @@ -118,7 +118,7 @@ describe.skipIf(skip)("socketFaultInjection control surface", () => { // and then never fire. test("set() rejects syscalls that have no fault hook", () => { for (const sc of ["socket", "close", "shutdown"]) { - expect(() => fault.set({ syscall: sc as any, action: "none" })).toThrow(/SocketFaultRule\.syscall must be/); + expect(() => fault.set({ syscall: sc as any, action: "none" })).toThrow(/rule\.syscall must be/); } }); diff --git a/test/js/node/crypto/crypto-random.test.ts b/test/js/node/crypto/crypto-random.test.ts index 659b5bfbfa16..7af0df35871a 100644 --- a/test/js/node/crypto/crypto-random.test.ts +++ b/test/js/node/crypto/crypto-random.test.ts @@ -4,7 +4,7 @@ import { statSync } from "fs"; import { bunEnv, bunExe, isLinux, isMacOS, isMusl, tempDir } from "harness"; import { join } from "path"; -describe("as Node", () => { +describe("arguments are validated as in Node", () => { it("a proxy of a function is a callback", async () => { const called = (call: (callback: (err: Error | null) => void) => void) => new Promise((resolve, reject) => call(new Proxy(err => (err ? reject(err) : resolve()), {}))); diff --git a/test/js/node/crypto/pbkdf2.test.ts b/test/js/node/crypto/pbkdf2.test.ts index 58292ce4c4d6..1255785f8023 100644 --- a/test/js/node/crypto/pbkdf2.test.ts +++ b/test/js/node/crypto/pbkdf2.test.ts @@ -139,7 +139,7 @@ describe("invalid inputs", () => { (err as any).code = "ERR_CRYPTO_INVALID_DIGEST"; let thrown: NodeJS.ErrnoException; try { - crypto.pbkdf2("password", "salt", 1, 1, "md55"); + crypto.pbkdf2("password", "salt", 1, 1, "md55", () => {}); expect.unreachable(); } catch (e) { thrown = e as Error; @@ -382,6 +382,12 @@ test("pbkdf2 takes a proxy of a function as its callback", async () => { }); test("pbkdf2 reports its arguments as Node does", () => { + expect(() => crypto.pbkdf2("p", "s", 1, 1, "md55")).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: 'The "callback" argument must be of type function. Received undefined', + }), + ); expect(() => crypto.pbkdf2("p", "s", 1, 1, "md55", 5)).toThrow( expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE", diff --git a/test/js/sql/sql-options.test.ts b/test/js/sql/sql-options.test.ts index c83eccca18b0..1c37858cabcb 100644 --- a/test/js/sql/sql-options.test.ts +++ b/test/js/sql/sql-options.test.ts @@ -818,9 +818,9 @@ console.log(JSON.stringify(results));`, // A mode that cannot be mapped must not mean `disable`. "sslMode object": "ERR_INVALID_ARG_TYPE", "sslMode string": "ERR_INVALID_ARG_TYPE", - "sslMode negative": "ERR_INVALID_ARG_TYPE", - "sslMode out of range": "ERR_INVALID_ARG_TYPE", - "sslMode fraction": "ERR_INVALID_ARG_TYPE", + "sslMode negative": "ERR_OUT_OF_RANGE", + "sslMode out of range": "ERR_OUT_OF_RANGE", + "sslMode fraction": "ERR_OUT_OF_RANGE", }); expect(exitCode).toBe(0); }); From 9262d0ddc56238001cb8cb0d2f3648e37d048f18 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 02:26:13 +0000 Subject: [PATCH 12/22] Make `.otherAsNull` go by the kind of a value, parse an enumeration by length first, and skip the markdown callbacks and components that are not passed --- docs/project/bindgen.mdx | 11 ++- src/codegen/bindgen-emit.ts | 27 ++++-- src/codegen/bindgen-lib-internal.ts | 92 +++++++++++++------ src/js/internal-for-testing.ts | 2 +- src/jsc/bindgen_test.bind.ts | 10 +- src/jsc/bindgen_test.rs | 16 ++-- src/runtime/api/MarkdownObject.bind.ts | 8 +- src/runtime/api/MarkdownObject.rs | 14 ++- src/runtime/node/node_crypto_binding.bind.ts | 3 +- src/runtime/node/node_crypto_binding.rs | 15 ++- .../test_runner/timers/FakeTimers.bind.ts | 2 +- test/internal/bindgen.test.ts | 56 ++++++----- test/js/node/crypto/pbkdf2.test.ts | 12 +++ 13 files changed, 174 insertions(+), 94 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 941b88f6032a..b0cb47c53cdd 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -54,7 +54,7 @@ pub fn add<'a>(heap: &JsHeap<'a>, a: i32, b: i32) -> bindgen::Result<'a, i32> { `b.module` says where the file's functions are; an export named `requiredAndOptionalArg` is `required_and_optional_arg` there. A `rustPath` key on a function gives its full path instead. The arguments after the last that is not written -`.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull`, `.ifPassed` or `.default()` are the ones +`.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull`, `.otherAsNull`, `.ifPassed` or `.default()` are the ones TypeScript lets a caller leave out. The function's `.length` is the number of the others, unless `length` sets it. @@ -65,7 +65,7 @@ be of type string or an instance of Buffer, TypedArray, or DataView. Received nu must be a string, ArrayBuffer or ArrayBufferView". What the type takes is the same. A string in the place of the list is the whole message: Node's C++ writes its own out that way, and so do the Bun APIs that have a message of their own. A dictionary named in `expected` reports what is not an object as the -argument, and not under the dictionary's name. +argument, and not under the dictionary's name. A range has its own errors, so it is not named in `expected`. Nothing else is written by hand, except once per crate: `src//generated_bindgen.rs` includes the generated file, the crate root declares `mod generated_bindgen`, and a @@ -256,7 +256,7 @@ These come last, in this order (`b.any` already takes `undefined` and `null`, so - `.nullable`: `null` or `undefined` means absent; `Option`. - `.falsyAsNull`: every falsy value means absent; `Option`. - `.emptyStringAsNull`: `undefined`, `null` or `""` means absent, as for `JSValue::get_truthy`; `Option`. -- `.otherAsNull`: every value the type does not take means absent; `Option`. It is for an argument or a member that is ignored unless it is of the type. An object is what a dictionary takes, which still reports its members. It is not for an integer type, which reports a number it does not take. +- `.otherAsNull`: every value of a [kind](#unions-and-kinds) the type does not take means absent; `Option`. It is for an argument or a member that is ignored unless it is of the type. A dictionary still reports its members, and an array its elements. It is not for a type that throws for some values of a kind it takes: an integer (`b.f64.otherAsNull` takes every number), an enumeration, a dictionary without `allowArray`. A union writes `ignored: b.any` as its last alternative. - `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` both work and differ, like `alert`. It also goes on a type that takes `undefined`, like `b.any`. It is only for an argument, and not for one of an overload. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. @@ -317,7 +317,7 @@ JS thread: see "Cross-thread string hazards" in `src/CLAUDE.md` before sending one anywhere. `b.string.allowStringObject` also takes a `String` object, as `.utf8` does. A [class](#classes) is a type too: import it from its `.bind.ts` file. As an argument it is a `&'a` of the struct. -The argument's slot in the frame holds the instance, so it is only for an argument, alone, in a union, or with one of the modifiers above. +The argument's slot in the frame holds the instance, so it is only for an argument, alone, in a union, or with one of the modifiers above but `.default()`. ### Dictionaries @@ -383,7 +383,8 @@ in play apart: A strict type takes the kinds its row under [Types](#types) says. A type that coerces or reports its own error takes every kind: `.loose`, `.wrap`, `.enforceRange`, -`.validateInteger`, `.otherAsNull`, `b.any`, `b.bytes`, `b.object`, a dictionary. +`.validateInteger`, `b.any`, `b.bytes`, `b.object`, a dictionary. So does an argument written `.falsyAsNull`, +`.emptyStringAsNull` or `.otherAsNull`. **A value goes to the first alternative, in written order, that takes its kind.** That alternative then converts it, and reports what is wrong with it: `1.5` given to diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 5f4aa0a03bd6..535af8abeada 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -444,18 +444,26 @@ extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${origin.stem}_${names } function cppSourceForEnum(type: EnumerationType): string { - const pairs = type.spellings - .flatMap((spellings, index) => spellings.map(spelling => [spelling, type.variants[index]] as const)) - .sort(([a], [b]) => (a < b ? -1 : 1)); - const pair = `std::pair`; + // The length rules out most spellings, so few are compared. + const byLength = new Map(); + type.spellings.forEach((spellings, index) => { + for (const spelling of spellings) { + const compared = byLength.get(spelling.length) ?? []; + byLength.set(spelling.length, compared); + compared.push( + ` if (string == ${asciiLiteral(spelling)})\n return ${type.cpp}::${type.variants[index]};`, + ); + } + }); return ` template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type.cpp}>(const WTF::String& string) { - static constexpr WTF::SortedArrayMap mapping { std::to_array<${pair}>({ -${pairs.map(([spelling, variant]) => ` { ${asciiLiteral(spelling)}, ${type.cpp}::${variant} },`).join("\n")} - }) }; - if (auto* value = mapping.tryGet(string)) [[likely]] - return *value; + switch (string.length()) { +${[...byLength] + .sort(([a], [b]) => a - b) + .map(([length, compared]) => ` case ${length}:\n${compared.join("\n")}\n break;`) + .join("\n")} + } return std::nullopt; } @@ -1295,7 +1303,6 @@ export function cppSource(file: SchemaFile): string { if (file.types.some(type => type instanceof ClassType)) systemHeaders.unshift("JavaScriptCore/HeapAnalyzer.h"); if (accepted.some(type => !type.allowArray)) systemHeaders.unshift("JavaScriptCore/ArrayConstructor.h"); if (file.namespaces.length > 0) systemHeaders.push("JavaScriptCore/ObjectConstructor.h"); - if (file.types.some(type => type instanceof EnumerationType)) systemHeaders.push("wtf/SortedArrayMap.h"); return `${banner(file.origin)}#include "root.h" #include "Generated${file.origin.namespace}.h" #include "ZigGlobalObject.h" diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 91a251c2ceeb..2d926aa79303 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -199,14 +199,14 @@ export abstract class Type { get emptyStringAsNull(): OptionalType { return new OptionalType(this, "emptyStringAsNull"); } + /** Every value of a kind it does not take means absent. */ + get otherAsNull(): OptionalType { + return new OptionalType(this, "otherAsNull"); + } /** Absent only if the call has no such argument: `undefined` is converted like any other value. Only for an argument. */ get ifPassed(): OptionalType { return new OptionalType(this, "ifPassed"); } - /** Every value it does not take means absent. */ - get otherAsNull(): OptionalType { - return new OptionalType(this, "otherAsNull"); - } /** Also the argument itself: `(T, JsLocal<'a>)`. Only for an argument. */ get withValue(): WithValueType { return new WithValueType(this); @@ -237,11 +237,15 @@ export abstract class Type { get refuses(): string | null { return null; } - /** A C++ condition that is true of the values it takes. Only for a type whose kinds tell, or that takes every object. */ + /** It throws for some values of a kind it takes, like `1.5` or an unknown string, so the kind does not tell whether it takes one. */ + get narrowsItsKinds(): boolean { + return false; + } + /** A C++ condition that is true of the values `.otherAsNull` converts. */ cppTakes(value: string): string { if (refusesNonObjects(this)) return `${value}.isObject()`; const kinds = this.valueKinds; - assert(kinds !== "any" && !this.refuses, `${this.tsType}: only converting a value tells whether it takes it`); + assert(kinds !== "any"); const ofValue = kinds.flatMap(kind => (typeof kind === "object" ? [] : (kindTests[kind].ofValue?.(value) ?? []))); const ofCell = kinds .flatMap(kind => (typeof kind === "object" ? `cell->inherits<${kind.cpp}>()` : (kindTests[kind].ofCell ?? []))) @@ -256,8 +260,8 @@ export abstract class Type { get passesValue(): boolean { return false; } - /** Why it is not an element, a member, an alternative or a result, if it is only for an argument. */ - get onlyArgument(): string | null { + /** What a schema that makes it an element, a member, an alternative or a result is told, if it is only for an argument. */ + get refusalOutsideArgument(): string | null { return this.passesValue ? "b.bytes and .withValue are only for an argument" : null; } /** As an argument it stays in the frame of the thunk, and the body borrows it: `&T`. */ @@ -432,6 +436,9 @@ interface IntegerInfo { } export class IntegerType extends PrimitiveType { + get narrowsItsKinds(): boolean { + return true; + } readonly info: IntegerInfo; readonly #conversion: "Strict" | "Loose" | `${string}::${string}`; readonly #allowBigInt: boolean; @@ -752,7 +759,7 @@ export class LocalStringResultType extends ResultType { } export class OwnedStringType extends StringType { - get onlyArgument(): string { + get refusalOutsideArgument(): string { return ".owned is only for an argument: everywhere else the string is already by value"; } get borrowedByBody(): boolean { @@ -1293,8 +1300,8 @@ export class PromisePtrType extends Type { get idlType(): string { return "Bun::Bindgen::IDLPromisePtr"; } - get onlyArgument(): string { - return "the context of a promise reaction is only an argument"; + get refusalOutsideArgument(): string { + return "the context of a promise reaction is an argument, alone or with .withValue"; } get conversionThrows(): boolean { return false; @@ -1340,17 +1347,19 @@ export class OptionalType extends Type { assert(!(inner instanceof OptionalType), "already optional"); assert(!(inner instanceof DefaultType), "put .default() last"); assert(!(inner instanceof WithValueType), "put .withValue last"); - assert(inner.passesValue || !inner.onlyArgument, inner.onlyArgument ?? ""); + if (!inner.passesValue) refuseOutsideArgument(inner); + if (modifier === "otherAsNull") { + const byKind = ".otherAsNull goes by the kind of a value"; + assert(!(inner instanceof UnionType), "a union takes the rest as its last alternative: ignored: b.any"); + assert(inner.valueKinds !== "any" || refusesNonObjects(inner), `${byKind}: this type takes every kind`); + assert(!inner.narrowsItsKinds, `${byKind}: this type throws for some values of a kind it takes`); + } // `.ifPassed` does not claim `undefined`, so it goes on a type that takes it. if (modifier !== "ifPassed") { if (inner instanceof AnyType) assert(modifier === "emptyStringAsNull", "b.any already accepts undefined and null"); else assert(!inner.takesUndefined, `${inner.tsType} already takes undefined`); } - if (modifier === "otherAsNull") { - assert(!(inner instanceof IntegerType), "an integer type reports a number it does not take: b.f64.otherAsNull"); - assert(inner.valueKinds !== "any" || inner.refuses, `${inner.tsType} takes every value`); - } assert( !(inner instanceof BytesType) || inner.canBe(modifier), 'b.bytes can be absent if it is held: "borrowed", and .nullable if it is string: "asIs"', @@ -1380,8 +1389,12 @@ export class OptionalType extends Type { get isOptional(): boolean { return true; } - get onlyArgument(): string | null { - return this.modifier === "ifPassed" ? ".ifPassed is only for an argument" : super.onlyArgument; + get refusalOutsideArgument(): string | null { + return this.modifier === "ifPassed" ? ".ifPassed is only for an argument" : super.refusalOutsideArgument; + } + /** `.otherAsNull` of a string resolves a rope, and of a dictionary reads its members. */ + get conversionThrows(): boolean { + return this.modifier === "otherAsNull" || this.inner.conversionThrows; } get valueKinds(): ValueKinds { if (this.inner.valueKinds === "any" || !["optional", "nullable", "ifPassed"].includes(this.modifier)) return "any"; @@ -1473,6 +1486,7 @@ export class WithValueType extends Type { readonly inner: Type; constructor(inner: Type) { super(); + assert(!(inner instanceof WithValueType), "already .withValue"); assert(!inner.passesValue, ".withValue: b.bytes already passes the argument"); assert(!inner.returnsValueUnchanged, ".withValue: the type is the argument itself"); assert(!isIfPassed(inner), ".ifPassed has no argument to give when it is absent"); @@ -1573,6 +1587,9 @@ export class DefaultType extends Type { get isOptional(): boolean { return true; } + get refusalOutsideArgument(): string | null { + return this.inner.refusalOutsideArgument; + } get conversionThrows(): boolean { return this.inner.conversionThrows; } @@ -1632,7 +1649,7 @@ export class ArrayType extends Type { super(); assert(!(element instanceof DefaultType), "an element of an array is never absent, so it has no default"); assert(!(element instanceof RangeType), "a range reports the name of its argument, which an element does not have"); - assert(!element.onlyArgument, element.onlyArgument ?? ""); + refuseOutsideArgument(element); assert( !element.borrowsCell, "an instance that Rust takes as &T cannot be in an array or a dictionary: only an argument's slot in the frame keeps it alive", @@ -1761,6 +1778,9 @@ export type EnumerationValues = readonly (string | readonly string[])[] | Readonly>; export class EnumerationType extends NamedType { + get narrowsItsKinds(): boolean { + return true; + } /** `spellings[i]` all parse to variant `i`; the first is the canonical one. */ readonly spellings: readonly (readonly string[])[]; /** Index of the first canonical spelling in `Tables::enumerationValues`. Set by the generator. */ @@ -1842,6 +1862,7 @@ export class Member { assert(!cppKeywords.has(this.fieldName), `${this.fieldName} is a C++ keyword: name the field with rustName`); this.type = options.type; assert(options.expected === undefined || options.expected.length > 0, `expected: ${name} expects nothing`); + if (options.expected !== undefined) refuseExpectedOfRange(name, options.type); this.expected = options.expected; } get name(): string { @@ -1886,7 +1907,7 @@ export class DictionaryType extends NamedType { "fields", ); for (const { name, type } of this.members) { - assert(!type.onlyArgument, `${name}: ${type.onlyArgument}`); + refuseOutsideArgument(type, name); assert(!(type instanceof VariadicType), `${name}: .variadic is only for the last argument`); } assert( @@ -1913,6 +1934,9 @@ export class DictionaryType extends NamedType { get fromJS(): boolean { return !!this.options.fromJS; } + get narrowsItsKinds(): boolean { + return !this.allowArray; + } get allowArray(): boolean { return !!this.options.allowArray; } @@ -2014,9 +2038,12 @@ export class UnionType extends NamedType { ); for (const { name, type } of this.alternatives) { assert(!(type instanceof UnionType), `${name}: list the alternatives of the other union here`); - assert(!(type instanceof OptionalType), `${name}: add b.null as an alternative`); + assert( + !(type instanceof OptionalType), + `${name}: add ${type instanceof OptionalType && type.modifier === "otherAsNull" ? "ignored: b.any" : "b.null"} as an alternative`, + ); assert(!(type instanceof DefaultType), `${name}: put .default() on the union`); - assert(!type.onlyArgument, `${name}: ${type.onlyArgument}`); + refuseOutsideArgument(type, name); assert(!(type instanceof VariadicType), `${name}: .variadic is only for the last argument`); assert(/^[a-z][A-Za-z0-9]*$/.test(name), `${name}: an alternative is named in camelCase`); assert(name !== "tag", "tag: the generated struct has a member of that name"); @@ -2134,10 +2161,6 @@ export interface FunctionOptions extends SignatureOptions { /** What `b.value` takes. */ export type ValueOptions = Pick & { returns: Type }; -/** A range, absent or not: its converter takes the name and no context. */ -const reportsItsOwnName = (type: Type) => - (type instanceof OptionalType || type instanceof DefaultType ? type.inner : type) instanceof RangeType; - export class Signature { readonly args: readonly { name: string; type: Type; expected?: readonly string[] | string }[]; readonly returns: Type; @@ -2158,7 +2181,7 @@ export class Signature { })); for (const [name, types] of Object.entries(expected)) { assert(name in args, `expected: ${name} is not an argument`); - assert(!reportsItsOwnName(args[name]), `expected: ${name} is a range, which reports what Node does`); + refuseExpectedOfRange(name, args[name]); assert(types.length > 0, `expected: ${name} expects nothing`); } this.returns = options.returns ?? new UnitType("undefined"); @@ -2201,7 +2224,7 @@ export class Signature { "b.nativePromiseContext takes the pointer out of the cell: if a later argument throws, nothing releases it", ); assert(!(this.returns instanceof DefaultType), "a result has no default"); - assert(!this.returns.onlyArgument, this.returns.onlyArgument ?? ""); + refuseOutsideArgument(this.returns); assert(!this.rejects || this.returns.holdsJSValue, "rejects: the result is a promise"); assert( !this.returns.holdsJSValue || this.capability !== "none" || hasThisValue, @@ -2227,6 +2250,19 @@ function resultTypeIn(type: Type): ResultType | undefined { } } +function refuseOutsideArgument(type: Type, name?: string): void { + const refusal = type.refusalOutsideArgument; + assert(!refusal, name ? `${name}: ${refusal}` : (refusal ?? "")); +} + +/** The converter of a range takes the name and no context. */ +function refuseExpectedOfRange(name: string, type: Type): void { + let bare = type; + while (bare instanceof OptionalType || bare instanceof DefaultType || bare instanceof WithValueType) + bare = bare.inner; + assert(!(bare instanceof RangeType), `expected: ${name} is a range, which has its own errors`); +} + function refuseResultType(name: string, type: Type): void { const result = resultTypeIn(type); assert(!result, `${name}: ${result?.refusal}`); diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 7b13c7c19690..29bc526b430e 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -300,7 +300,7 @@ export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { objectEcho: $bindgen("bindgen_test.bind.ts", "objectEcho"), absenceEcho: $bindgen("bindgen_test.bind.ts", "absenceEcho"), callbackOrDelay: $bindgen("bindgen_test.bind.ts", "callbackOrDelay"), - presentOnes: $bindgen("bindgen_test.bind.ts", "presentOnes"), + presentArguments: $bindgen("bindgen_test.bind.ts", "presentArguments"), finiteLater: $bindgen("bindgen_test.bind.ts", "finiteLater"), integerOrBigInt: $bindgen("bindgen_test.bind.ts", "integerOrBigInt"), looseInteger: $bindgen("bindgen_test.bind.ts", "looseInteger"), diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index de113eae7187..9d12e4516ef1 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -457,16 +457,18 @@ export const objectEcho = b.fn({ args: { value: b.object }, returns: b.object, c export const TestAbsence = b.union({ missing: b.undefined, nothing: b.null, number: b.f64 }); export const absenceEcho = b.fn({ args: { value: TestAbsence }, returns: TestAbsence }); -export const TestLenientOptions = b.dictionary({ size: b.f64.otherAsNull, callback: b.callable.otherAsNull }); +export const TestOtherAsNullOptions = b.dictionary( + { size: b.f64.otherAsNull, callback: b.callable.otherAsNull, strict: b.f64.optional }, + { allowArray: true }, +); /** Which of its arguments, and of the members of `options`, are of their types. */ -export const presentOnes = b.fn({ +export const presentArguments = b.fn({ args: { number: b.f64.otherAsNull, text: b.string.utf8.otherAsNull, callback: b.callable.otherAsNull, - point: TestPoint.otherAsNull, counter: TestCounter.otherAsNull, - options: TestLenientOptions.otherAsNull, + options: TestOtherAsNullOptions.otherAsNull, }, returns: b.array(b.string), }); diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 79fe3419b408..0bcd4f6a7498 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -10,9 +10,9 @@ use crate::bindgen::{ }; use crate::generated_bindgen::bindgen_test::{ Bun__BindgenTest__doubled, TestAbsence, TestAnything, TestBare, TestBareOptions, TestBracket, - TestChoice, TestColor, TestColorOrRest, TestFailure, TestFlag, TestHandle, TestLenientOptions, - TestNames, TestNodeOptions, TestObjectOrRest, TestOptions, TestOptionsEcho, TestOuter, - TestPoint, TestTextOrBytes, TestValue, js_TestCounter, js_TestError, js_TestNonFinal, + TestChoice, TestColor, TestColorOrRest, TestFailure, TestFlag, TestHandle, TestNames, + TestNodeOptions, TestObjectOrRest, TestOptions, TestOptionsEcho, TestOtherAsNullOptions, + TestOuter, TestTextOrBytes, TestValue, js_TestCounter, js_TestError, js_TestNonFinal, js_TestNote, js_TestRefCounted, js_TestRefCountedNote, }; use crate::{JSGlobalObject, JSString, JSValue, JsClass, JsResult}; @@ -946,25 +946,23 @@ pub(crate) fn absence_echo(value: TestAbsence) -> TestAbsence { value } -pub(crate) fn present_ones( +pub(crate) fn present_arguments( number: Option, text: Option<&[u8]>, callback: Option>, - point: Option, counter: Option<&TestCounter>, - options: Option>, + options: Option>, ) -> Vec { let members = options.map(|options| (options.size, options.callback)); - let (size, member) = members.unwrap_or((None, None)); + let (size, callback_member) = members.unwrap_or((None, None)); [ ("number", number.is_some()), ("text", text.is_some()), ("callback", callback.is_some()), - ("point", point.map(|point| point.x).is_some()), ("counter", counter.is_some()), ("options", members.is_some()), ("options.size", size.is_some()), - ("options.callback", member.is_some()), + ("options.callback", callback_member.is_some()), ] .into_iter() .filter(|(_, is_present)| *is_present) diff --git a/src/runtime/api/MarkdownObject.bind.ts b/src/runtime/api/MarkdownObject.bind.ts index 2ee320bbe01f..24b1be9f9157 100644 --- a/src/runtime/api/MarkdownObject.bind.ts +++ b/src/runtime/api/MarkdownObject.bind.ts @@ -67,7 +67,7 @@ export const RenderCallbacks = b.dictionary( "strikethrough", "text", ]), - ignoresNonObjects, + { allowArray: true }, ); /** The `type` of the elements of each tag. A boolean is ignored. */ @@ -103,7 +103,7 @@ export const ComponentOverrides = b.dictionary( "u", "br", ]), - ignoresNonObjects, + { allowArray: true }, ); const js = { capability: "js", throws: true } as const; @@ -113,14 +113,14 @@ export const markdown = b.namespace({ ansi: { args: { input, theme: AnsiTheme.default({}) }, returns: b.string.utf8, expected, length: 2, ...js }, /** The callbacks run while `input` is read. */ render: { - args: { input: b.any, callbacks: RenderCallbacks.default({}), options: MarkdownOptions.default({}) }, + args: { input: b.any, callbacks: RenderCallbacks.otherAsNull, options: MarkdownOptions.default({}) }, returns: b.string.utf8, length: 3, ...js, }, /** As `render`. */ react: { - args: { input: b.any, components: ComponentOverrides.default({}), options: ReactOptions.default({}) }, + args: { input: b.any, components: ComponentOverrides.otherAsNull, options: ReactOptions.default({}) }, returns: b.any, length: 3, ...js, diff --git a/src/runtime/api/MarkdownObject.rs b/src/runtime/api/MarkdownObject.rs index 5bcf322b7777..2606ed63aacc 100644 --- a/src/runtime/api/MarkdownObject.rs +++ b/src/runtime/api/MarkdownObject.rs @@ -200,7 +200,7 @@ pub(crate) fn html<'a>( pub(crate) fn render<'a>( cx: &mut JsScope<'a>, input: JsLocal<'a>, - callbacks: &RenderCallbacks<'a>, + callbacks: Option>, options: &MarkdownOptions, ) -> JsResult> { let global_this = cx.global(); @@ -230,7 +230,9 @@ pub(crate) fn render<'a>( Err(_) => return Err(global_this.throw_out_of_memory()), }; - js_renderer.extract_callbacks(callbacks); + if let Some(callbacks) = callbacks { + js_renderer.extract_callbacks(&callbacks); + } // Run parser with the JS callback renderer if let Err(err) = md::render_with_renderer(input, options, js_renderer.renderer()) { @@ -247,7 +249,7 @@ pub(crate) fn render<'a>( pub(crate) fn react<'a>( cx: &mut JsScope<'a>, input: JsLocal<'a>, - components: &ComponentOverrides<'a>, + components: Option>, options: &ReactOptions, ) -> JsResult { let global_this = cx.global(); @@ -283,7 +285,7 @@ unsafe extern "C" { fn render_ast( global_this: &JSGlobalObject, input_value: JSValue, - components: &ComponentOverrides<'_>, + components: Option>, options: md::Options, marked_args: &mut MarkedArgumentBuffer, react_version: Option, @@ -315,7 +317,9 @@ fn render_ast( Err(_) => return Err(global_this.throw_out_of_memory()), }; - renderer.extract_components(components); + if let Some(components) = components { + renderer.extract_components(&components); + } if let Err(err) = md::render_with_renderer(input, options, renderer.renderer()) { return Err(parser_err_to_js(global_this, err, input.len())); diff --git a/src/runtime/node/node_crypto_binding.bind.ts b/src/runtime/node/node_crypto_binding.bind.ts index e9ee377e91bf..70bfbfa5cde5 100644 --- a/src/runtime/node/node_crypto_binding.bind.ts +++ b/src/runtime/node/node_crypto_binding.bind.ts @@ -63,8 +63,9 @@ const argon2Parameters = (bytes: b.Type) => ({ export const crypto = b.namespace({ pbkdf2: [ + /** A call of five arguments is this one unless the fifth is a function, so `callback` can be left out. The body reports that. */ { - args: { ...passwordAndSaltHandles, ...pbkdf2Parameters, digest: b.string.utf8, callback: b.callable }, + args: { ...passwordAndSaltHandles, ...pbkdf2Parameters, digest: b.string.utf8, callback: b.callable.optional }, expected: { ...passwordAndSaltNames, digest: ["string"], callback: ["function"] }, rustPath: "crate::node::crypto::pbkdf2", length: 6, diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index 3ea5ea0b28f3..ce844fa9732f 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -930,8 +930,15 @@ pub(crate) fn pbkdf2<'a>( iterations: u32, keylen: u32, digest: &[u8], - callback: JsLocal<'a, Callable>, + callback: Option>, ) -> JsResult<()> { + let Some(callback) = callback else { + return Err(cx.global().throw_invalid_argument_type_value( + b"callback", + b"function", + JSValue::UNDEFINED, + )); + }; let algorithm = pbkdf2::algorithm_from_digest(cx.global(), digest)?; let password = isolate_for_job(cx, password)?; @@ -1130,9 +1137,9 @@ fn argon2_exceeds_allocation_limit(memory: u32, tag_length: u32) -> bool { (memory as usize).saturating_mul(1024) > limit || tag_length as usize > limit } -/// `index` is one of `kArgon2Types` of `crypto.ts`. -fn argon2_variant(index: u8) -> rust_argon2::Variant { - match index { +/// `variant` is a value of `kArgon2Types` of `crypto.ts`. +fn argon2_variant(variant: u8) -> rust_argon2::Variant { + match variant { 0 => rust_argon2::Variant::Argon2d, 1 => rust_argon2::Variant::Argon2i, _ => rust_argon2::Variant::Argon2id, diff --git a/src/runtime/test_runner/timers/FakeTimers.bind.ts b/src/runtime/test_runner/timers/FakeTimers.bind.ts index 5d93ec4b1a14..67f88fad2c9d 100644 --- a/src/runtime/test_runner/timers/FakeTimers.bind.ts +++ b/src/runtime/test_runner/timers/FakeTimers.bind.ts @@ -9,7 +9,7 @@ export const FakeTimersOptions = b.dictionary( { now: b.f64.allowDate.optional }, { allowArray: true, reportedAs: "options" }, ); -/** Jest 26 took `"modern"` or `"legacy"`. It is ignored. */ +/** Jest 26 took `"modern"` or `"legacy"`. The string is ignored. */ export const FakeTimersImplementationOrOptions = b.union({ implementation: b.string.utf8, options: FakeTimersOptions }); export const useFakeTimers = b.fn({ diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 9641655fdcda..602b6327e036 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -121,8 +121,8 @@ itAlways("mistakes in a schema are errors when it loads", () => { [b.i32.withValue, "b.bytes and .withValue are only for an argument"], [b.i32.ifPassed, ".ifPassed is only for an argument"], [b.string.owned, ".owned is only for an argument"], - [b.promisePtr("T"), "the context of a promise reaction is only an argument"], - [b.nativePromiseContext("T"), "the context of a promise reaction is only an argument"], + [b.promisePtr("T"), "the context of a promise reaction is an argument, alone or with .withValue"], + [b.nativePromiseContext("T"), "the context of a promise reaction is an argument, alone or with .withValue"], ] as const) { expect(() => b.array(type)).toThrow(reason); expect(() => b.dictionary({ member: type })).toThrow(`member: ${reason}`); @@ -132,7 +132,9 @@ itAlways("mistakes in a schema are errors when it loads", () => { expect(() => b.union({ alternative: type, nothing: b.null })).toThrow("alternative: "); } expect(() => b.string.owned.optional).toThrow(".owned is only for an argument"); - expect(() => b.promisePtr("T").nullable).toThrow("the context of a promise reaction is only an argument"); + expect(() => b.promisePtr("T").nullable).toThrow("alone or with .withValue"); + expect(() => b.dictionary({ text: b.string.owned.default("") })).toThrow("text: .owned is only for an argument"); + expect(() => b.i32.withValue.withValue).toThrow("already .withValue"); expect(() => b.string.owned.owned).toThrow("already .owned"); expect(() => b.string.owned.local).toThrow(".local follows b.string"); expect(() => b.i32.withValue.optional).toThrow("put .withValue last"); @@ -144,9 +146,16 @@ itAlways("mistakes in a schema are errors when it loads", () => { ); expect(() => b.f64.loose.allowDate).toThrow(".allowDate follows b.f64"); expect(() => b.f64.allowDate.loose).toThrow(".loose follows b.f64"); - expect(() => b.any.otherAsNull).toThrow("b.any already accepts undefined and null"); - expect(() => b.string.loose.otherAsNull).toThrow("any takes every value"); - expect(() => b.i32.otherAsNull).toThrow("an integer type reports a number it does not take"); + for (const type of [b.any, b.string.loose, b.bytes({ from: ["buffer"] }), b.u8.validateInteger(0, 4)]) { + expect(() => type.otherAsNull).toThrow(".otherAsNull goes by the kind of a value: this type takes every kind"); + } + for (const type of [b.i32, b.enumeration(["a"]), b.dictionary({ x: b.f64 })]) { + expect(() => type.otherAsNull).toThrow("this type throws for some values of a kind it takes"); + } + expect(() => b.union({ number: b.f64, text: b.string }).otherAsNull).toThrow( + "a union takes the rest as its last alternative: ignored: b.any", + ); + expect(() => b.union({ number: b.f64.otherAsNull })).toThrow("number: add ignored: b.any as an alternative"); expect(() => b.fn({ args: { context: b.nativePromiseContext("T"), value: b.i32 }, export: "Bun__reaction" })).toThrow( "b.nativePromiseContext takes the pointer out of the cell", ); @@ -154,9 +163,13 @@ itAlways("mistakes in a schema are errors when it loads", () => { ".ifPassed is not for overloads", ); expect(() => b.fn({ returns: b.i32, rejects: true })).toThrow("rejects: the result is a promise"); - expect(() => b.fn({ args: { size: b.u8.validateInteger(0, 4) }, expected: { size: ["number"] } })).toThrow( - "expected: size is a range", - ); + const range = b.u8.validateInteger(0, 4); + for (const size of [range, range.optional, range.default(0), range.optional.withValue, b.u8.enforceRange(0, 4)]) { + expect(() => b.fn({ args: { size }, expected: { size: ["number"] }, capability: "heap" })).toThrow( + "expected: size is a range, which has its own errors", + ); + } + expect(() => b.dictionary({ size: { type: range, expected: ["number"] } })).toThrow("expected: size is a range"); const point = b.dictionary({ x: b.f64 }); for (const later of [b.jsArray, b.callable, b.bufferSource, b.array(b.f64)]) { expect(() => b.union({ point, later, text: b.string })).toThrow("later is never chosen"); @@ -805,26 +818,25 @@ describe("functions", () => { expect(bindgen.isPassed(1, undefined)).toBe(true); }); - it(".otherAsNull is absent for every value the type does not take", () => { - const { presentOnes } = bindgen; - expect(presentOnes()).toEqual([]); - expect(presentOnes(1.5, "", () => {}, { x: 1 }, new bindgen.TestCounter(1), {})).toEqual([ + it(".otherAsNull is absent for every value of a kind the type does not take", () => { + const { presentArguments } = bindgen; + expect(presentArguments()).toEqual([]); + expect(presentArguments(1.5, "", () => {}, new bindgen.TestCounter(1), {})).toEqual([ "number", "text", "callback", - "point", "counter", "options", ]); - expect(presentOnes("1", 1, {}, 1, {}, 1)).toEqual([]); - expect(presentOnes(null, Symbol(), null, "x", null, null)).toEqual([]); - expect(presentOnes(NaN, new String("s"), new Proxy(() => {}, {}))).toEqual(["number", "text", "callback"]); - const options = (value: unknown) => presentOnes(0, 0, 0, 0, 0, value).slice(1); + expect(presentArguments("1", 1, {}, {}, 1)).toEqual([]); + expect(presentArguments(null, Symbol(), null, null, null)).toEqual([]); + expect(presentArguments(NaN, new String("s"), new Proxy(() => {}, {}))).toEqual(["number", "text", "callback"]); + const options = (value: unknown) => presentArguments(0, 0, 0, 0, value).slice(1); expect(options({ size: 1, callback() {} })).toEqual(["options", "options.size", "options.callback"]); expect(options({ size: "1", callback: {} })).toEqual(["options"]); - // An object is a dictionary, whose own members still report what is wrong with them. - expect(() => presentOnes(0, 0, 0, {})).toThrow("TestPoint.x is required"); - expect(presentOnes.length).toBe(0); + expect(options([])).toEqual(["options"]); + expect(() => options({ strict: "1" })).toThrow("TestOtherAsNullOptions.strict must be a number"); + expect(presentArguments.length).toBe(0); }); it(".withValue also gives the argument", () => { @@ -862,7 +874,7 @@ describe("functions", () => { }); }); -describe("a dictionary of a node:* function", () => { +describe("how a dictionary reports its members", () => { it(".validateInteger can be absent", () => { expect(bindgen.nodeOptionsSize({})).toBe(undefined); expect(bindgen.nodeOptionsSize({ size: undefined })).toBe(undefined); diff --git a/test/js/node/crypto/pbkdf2.test.ts b/test/js/node/crypto/pbkdf2.test.ts index 1255785f8023..7ca17b159ebd 100644 --- a/test/js/node/crypto/pbkdf2.test.ts +++ b/test/js/node/crypto/pbkdf2.test.ts @@ -394,6 +394,18 @@ test("pbkdf2 reports its arguments as Node does", () => { message: 'The "callback" argument must be of type function. Received type number (5)', }), ); + expect(() => crypto.pbkdf2("p", "s", 1, 1, 5)).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: 'The "digest" argument must be of type string. Received type number (5)', + }), + ); + expect(() => crypto.pbkdf2("p", "s", 1, 1)).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: 'The "digest" argument must be of type string. Received undefined', + }), + ); for (const [name, args] of [ ["password", [5, "s"]], ["salt", ["p", 5]], From dc4c827474ba41793710f6b6c4d09761bbc072f7 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 02:47:43 +0000 Subject: [PATCH 13/22] Borrow a dictionary argument that can be absent, skip the markdown options that are not passed, and say what to write where `.otherAsNull` is refused --- docs/project/bindgen.mdx | 6 +- src/codegen/bindgen-emit.ts | 15 ++-- src/codegen/bindgen-lib-internal.ts | 40 +++++----- src/css_jsc/css_internals.rs | 26 +++---- src/jsc/bindgen_test.bind.ts | 2 + src/jsc/bindgen_test.rs | 9 ++- src/runtime/api/BunObject.rs | 8 +- src/runtime/api/JSTranspiler.rs | 6 +- src/runtime/api/MarkdownObject.bind.ts | 10 +-- src/runtime/api/MarkdownObject.rs | 81 ++++++++++---------- src/runtime/api/cron.rs | 6 +- src/runtime/node/node_crypto_binding.bind.ts | 2 +- src/runtime/node/node_http_binding.rs | 2 +- test/internal/bindgen.test.ts | 25 ++++-- 14 files changed, 132 insertions(+), 106 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index b0cb47c53cdd..c6f14c2d5e52 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -256,7 +256,7 @@ These come last, in this order (`b.any` already takes `undefined` and `null`, so - `.nullable`: `null` or `undefined` means absent; `Option`. - `.falsyAsNull`: every falsy value means absent; `Option`. - `.emptyStringAsNull`: `undefined`, `null` or `""` means absent, as for `JSValue::get_truthy`; `Option`. -- `.otherAsNull`: every value of a [kind](#unions-and-kinds) the type does not take means absent; `Option`. It is for an argument or a member that is ignored unless it is of the type. A dictionary still reports its members, and an array its elements. It is not for a type that throws for some values of a kind it takes: an integer (`b.f64.otherAsNull` takes every number), an enumeration, a dictionary without `allowArray`. A union writes `ignored: b.any` as its last alternative. +- `.otherAsNull`: every value of a [kind](#unions-and-kinds) the type does not take, or that it refuses (what is not an object, a shared buffer), means absent: what a union would give an `ignored: b.any` written after the type; `Option`. It is for an argument or a member that is ignored unless it is of the type. What is inside a value is still reported: the members of a dictionary, the elements of an array. It is not for a type that throws for a value of a kind it takes: an integer for `1.5` (`b.f64.otherAsNull` takes every number), an enumeration for an unknown string (`b.string.otherAsNull` takes every string), a dictionary without `allowArray` for an array. A union writes `ignored: b.any` as its last alternative. - `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` both work and differ, like `alert`. It also goes on a type that takes `undefined`, like `b.any`. It is only for an argument, and not for one of an overload. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. @@ -317,7 +317,7 @@ JS thread: see "Cross-thread string hazards" in `src/CLAUDE.md` before sending one anywhere. `b.string.allowStringObject` also takes a `String` object, as `.utf8` does. A [class](#classes) is a type too: import it from its `.bind.ts` file. As an argument it is a `&'a` of the struct. -The argument's slot in the frame holds the instance, so it is only for an argument, alone, in a union, or with one of the modifiers above but `.default()`. +The argument's slot in the frame holds the instance, so it is only for an argument, alone, in a union, or with one of the modifiers above except `.default()`. ### Dictionaries @@ -334,7 +334,7 @@ whose type does not accept `undefined` is required. A member can be written a function's `expected` does. A dictionary whose members can all be absent implements `Default`. As an argument, with `.default()` or without, the struct stays in the frame of the thunk and the body borrows it: -`options: &BracesOptions`. Inside `.optional`, a union or an array it is held by value. +`options: &BracesOptions`, or `Option<&BracesOptions>` with one of the modifiers that make it an `Option`. As a member, or inside a union or an array, it is held by value. A member is looked up on the object and its prototype chain, **stopping before the calling realm's `Object.prototype`**, so a polluted `Object.prototype` cannot diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 535af8abeada..4b360069cbfe 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -448,9 +448,9 @@ function cppSourceForEnum(type: EnumerationType): string { const byLength = new Map(); type.spellings.forEach((spellings, index) => { for (const spelling of spellings) { - const compared = byLength.get(spelling.length) ?? []; - byLength.set(spelling.length, compared); - compared.push( + const tests = byLength.get(spelling.length) ?? []; + byLength.set(spelling.length, tests); + tests.push( ` if (string == ${asciiLiteral(spelling)})\n return ${type.cpp}::${type.variants[index]};`, ); } @@ -461,7 +461,7 @@ template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type switch (string.length()) { ${[...byLength] .sort(([a], [b]) => a - b) - .map(([length, compared]) => ` case ${length}:\n${compared.join("\n")}\n break;`) + .map(([length, tests]) => ` case ${length}:\n${tests.join("\n")}\n break;`) .join("\n")} } return std::nullopt; @@ -1388,13 +1388,16 @@ function rustArgumentName(arg: { name: string }): string { return rustIdent(name); } -const rustBorrow = (type: Type) => ((type instanceof DefaultType ? type.inner : type).borrowedByBody ? "&" : ""); const rustExternParameters = (signature: Signature, scope: RustScope) => signature.args.map( arg => `${rustArgumentName(arg)}: ${isPassedByPointer(arg.type) ? "*mut " : ""}${arg.type.rustExternType(scope)}`, ); const rustArguments = (signature: Signature) => - signature.args.map(arg => `${rustBorrow(arg.type)}${rustArgumentName(arg)}`); + signature.args.map(arg => { + const name = rustArgumentName(arg); + if (arg.type instanceof OptionalType && arg.type.inner instanceof DictionaryType) return `${name}.as_ref()`; + return (arg.type instanceof DefaultType ? arg.type.inner : arg.type).borrowedByBody ? `&${name}` : name; + }); /** Each argument gets the call's lifetime here: left to inference in the call, a body could ask for `'static`. */ function rustTakeArguments(args: readonly { name: string; type: Type }[], scope: RustScope): string { // What can return early comes last: by then Rust owns whatever the other arguments hand over. diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 2d926aa79303..433eef8379c1 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -199,7 +199,7 @@ export abstract class Type { get emptyStringAsNull(): OptionalType { return new OptionalType(this, "emptyStringAsNull"); } - /** Every value of a kind it does not take means absent. */ + /** Every value of a kind it does not take, or that it refuses, means absent. */ get otherAsNull(): OptionalType { return new OptionalType(this, "otherAsNull"); } @@ -237,11 +237,11 @@ export abstract class Type { get refuses(): string | null { return null; } - /** It throws for some values of a kind it takes, like `1.5` or an unknown string, so the kind does not tell whether it takes one. */ - get narrowsItsKinds(): boolean { - return false; + /** What to write for `.otherAsNull`, if it throws for a value of a kind it takes, like `1.5` or an unknown string. */ + get narrowsItsKinds(): string | null { + return null; } - /** A C++ condition that is true of the values `.otherAsNull` converts. */ + /** A C++ condition that is true of the values `.otherAsNull` converts in place. */ cppTakes(value: string): string { if (refusesNonObjects(this)) return `${value}.isObject()`; const kinds = this.valueKinds; @@ -436,9 +436,6 @@ interface IntegerInfo { } export class IntegerType extends PrimitiveType { - get narrowsItsKinds(): boolean { - return true; - } readonly info: IntegerInfo; readonly #conversion: "Strict" | "Loose" | `${string}::${string}`; readonly #allowBigInt: boolean; @@ -474,6 +471,9 @@ export class IntegerType extends PrimitiveType { assert(typeof value !== "number" || (value >= min && value <= max), `${value} is not in [${min}, ${max}]`); return super.toCpp(value); } + get narrowsItsKinds(): string { + return "b.f64.otherAsNull takes every number"; + } /** Also a BigInt that is in range, for an argument whose type is `number | bigint`. */ get allowBigInt(): IntegerType { assert( @@ -1349,10 +1349,15 @@ export class OptionalType extends Type { assert(!(inner instanceof WithValueType), "put .withValue last"); if (!inner.passesValue) refuseOutsideArgument(inner); if (modifier === "otherAsNull") { - const byKind = ".otherAsNull goes by the kind of a value"; assert(!(inner instanceof UnionType), "a union takes the rest as its last alternative: ignored: b.any"); - assert(inner.valueKinds !== "any" || refusesNonObjects(inner), `${byKind}: this type takes every kind`); - assert(!inner.narrowsItsKinds, `${byKind}: this type throws for some values of a kind it takes`); + assert( + inner.valueKinds !== "any" || refusesNonObjects(inner), + ".otherAsNull goes by the kind of a value: the conversion of this type decides", + ); + assert( + !inner.narrowsItsKinds, + `.otherAsNull goes by the kind of a value, and this type throws for some of a kind: ${inner.narrowsItsKinds}`, + ); } // `.ifPassed` does not claim `undefined`, so it goes on a type that takes it. if (modifier !== "ifPassed") { @@ -1392,9 +1397,8 @@ export class OptionalType extends Type { get refusalOutsideArgument(): string | null { return this.modifier === "ifPassed" ? ".ifPassed is only for an argument" : super.refusalOutsideArgument; } - /** `.otherAsNull` of a string resolves a rope, and of a dictionary reads its members. */ get conversionThrows(): boolean { - return this.modifier === "otherAsNull" || this.inner.conversionThrows; + return this.inner.conversionThrows; } get valueKinds(): ValueKinds { if (this.inner.valueKinds === "any" || !["optional", "nullable", "ifPassed"].includes(this.modifier)) return "any"; @@ -1778,9 +1782,6 @@ export type EnumerationValues = readonly (string | readonly string[])[] | Readonly>; export class EnumerationType extends NamedType { - get narrowsItsKinds(): boolean { - return true; - } /** `spellings[i]` all parse to variant `i`; the first is the canonical one. */ readonly spellings: readonly (readonly string[])[]; /** Index of the first canonical spelling in `Tables::enumerationValues`. Set by the generator. */ @@ -1807,6 +1808,9 @@ export class EnumerationType extends NamedType { get valueKinds(): ValueKinds { return ["string"]; } + get narrowsItsKinds(): string { + return "b.string.otherAsNull takes every string"; + } get idlType(): string { return `WebCore::IDLEnumeration<${this.cpp}>`; } @@ -1934,8 +1938,8 @@ export class DictionaryType extends NamedType { get fromJS(): boolean { return !!this.options.fromJS; } - get narrowsItsKinds(): boolean { - return !this.allowArray; + get narrowsItsKinds(): string | null { + return this.allowArray ? null : "give the dictionary allowArray"; } get allowArray(): boolean { return !!this.options.allowArray; diff --git a/src/css_jsc/css_internals.rs b/src/css_jsc/css_internals.rs index 4d36a0717dde..62972798c3f9 100644 --- a/src/css_jsc/css_internals.rs +++ b/src/css_jsc/css_internals.rs @@ -22,7 +22,7 @@ pub(crate) fn minify_error_test_with_options<'a>( cx: &mut JsScope<'a>, source: &[u8], _: &[u8], - options: Option, + options: Option<&CssParserOptions>, ) -> JsResult> { testing_impl(cx, source, None, parser_flags(options), TestKind::Minify) } @@ -31,7 +31,7 @@ pub(crate) fn minify_test_with_options<'a>( cx: &mut JsScope<'a>, source: &[u8], _: &[u8], - options: Option, + options: Option<&CssParserOptions>, ) -> JsResult> { testing_impl(cx, source, None, parser_flags(options), TestKind::Minify) } @@ -40,7 +40,7 @@ pub(crate) fn prefix_test_with_options<'a>( cx: &mut JsScope<'a>, source: &[u8], _: &[u8], - options: Option, + options: Option<&CssParserOptions>, ) -> JsResult> { testing_impl(cx, source, None, parser_flags(options), TestKind::Prefix) } @@ -49,7 +49,7 @@ pub(crate) fn test_with_options<'a>( cx: &mut JsScope<'a>, source: &[u8], _: &[u8], - options: Option, + options: Option<&CssParserOptions>, ) -> JsResult> { testing_impl(cx, source, None, parser_flags(options), TestKind::Normal) } @@ -58,7 +58,7 @@ pub(crate) fn minify_test<'a>( cx: &mut JsScope<'a>, source: &[u8], _: &[u8], - options: Option, + options: Option<&BrowserTargets>, ) -> JsResult> { testing_impl( cx, @@ -73,7 +73,7 @@ pub(crate) fn prefix_test<'a>( cx: &mut JsScope<'a>, source: &[u8], _: &[u8], - options: Option, + options: Option<&BrowserTargets>, ) -> JsResult> { testing_impl( cx, @@ -88,7 +88,7 @@ pub(crate) fn test<'a>( cx: &mut JsScope<'a>, source: &[u8], _: &[u8], - options: Option, + options: Option<&BrowserTargets>, ) -> JsResult> { testing_impl( cx, @@ -102,7 +102,7 @@ pub(crate) fn test<'a>( fn testing_impl<'a>( cx: &mut JsScope<'a>, source: &[u8], - targets: Option, + targets: Option<&BrowserTargets>, flags: ParserFlags, test_kind: TestKind, ) -> JsResult> { @@ -190,10 +190,10 @@ fn testing_impl<'a>( } } -fn parser_flags(options: Option) -> ParserFlags { +fn parser_flags(options: Option<&CssParserOptions>) -> ParserFlags { let mut flags = ParserFlags::default(); for flag in options - .and_then(|options| options.flags) + .and_then(|options| options.flags.as_deref()) .unwrap_or_default() { match flag { @@ -203,8 +203,8 @@ fn parser_flags(options: Option) -> ParserFlags { flags } -impl From for Browsers { - fn from(targets: BrowserTargets) -> Self { +impl From<&BrowserTargets> for Browsers { + fn from(targets: &BrowserTargets) -> Self { Browsers { android: targets.android, chrome: targets.chrome, @@ -224,7 +224,7 @@ pub(crate) fn attr_test<'a>( source: &[u8], _: &[u8], minify: bool, - targets: Option, + targets: Option<&BrowserTargets>, ) -> JsResult> { use bun_ast::ImportRecord; use bun_css::{ diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 9d12e4516ef1..8a65267c2fcc 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -467,6 +467,8 @@ export const presentArguments = b.fn({ number: b.f64.otherAsNull, text: b.string.utf8.otherAsNull, callback: b.callable.otherAsNull, + object: b.object.otherAsNull, + numbers: b.array(b.f64).otherAsNull, counter: TestCounter.otherAsNull, options: TestOtherAsNullOptions.otherAsNull, }, diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 0bcd4f6a7498..455e5a969992 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -950,8 +950,10 @@ pub(crate) fn present_arguments( number: Option, text: Option<&[u8]>, callback: Option>, + object: Option>, + numbers: Option>, counter: Option<&TestCounter>, - options: Option>, + options: Option<&TestOtherAsNullOptions<'_>>, ) -> Vec { let members = options.map(|options| (options.size, options.callback)); let (size, callback_member) = members.unwrap_or((None, None)); @@ -959,6 +961,11 @@ pub(crate) fn present_arguments( ("number", number.is_some()), ("text", text.is_some()), ("callback", callback.is_some()), + ("object", object.is_some()), + ( + "numbers", + numbers.is_some_and(|numbers| !numbers.is_empty()), + ), ("counter", counter.is_some()), ("options", members.is_some()), ("options.size", size.is_some()), diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index bcb9298d9ad0..bb5760e52d96 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -360,7 +360,7 @@ fn apply_inspect_options( pub(crate) fn table<'a>( cx: &mut JsScope<'a>, tabular_data: Option>, - options: Option, + options: Option<&InspectOptions>, ) -> JsResult> { print_table(cx, tabular_data, JSValue::UNDEFINED, options) } @@ -370,7 +370,7 @@ pub(crate) fn table_with_properties<'a>( cx: &mut JsScope<'a>, tabular_data: Option>, properties: JsLocal<'a, JSArray>, - options: Option, + options: Option<&InspectOptions>, ) -> JsResult> { print_table(cx, tabular_data, properties.to_js(), options) } @@ -379,7 +379,7 @@ fn print_table( cx: &mut JsScope<'_>, tabular_data: Option>, properties: JSValue, - options: Option, + options: Option<&InspectOptions>, ) -> JsResult> { let global_this = cx.global(); let Some(value) = tabular_data else { @@ -398,7 +398,7 @@ fn print_table( ..Default::default() }; if let Some(options) = options { - apply_inspect_options(&mut format_options, global_this, &options)?; + apply_inspect_options(&mut format_options, global_this, options)?; } // very stable memory address diff --git a/src/runtime/api/JSTranspiler.rs b/src/runtime/api/JSTranspiler.rs index 5e01edc3914a..52aacbd20b90 100644 --- a/src/runtime/api/JSTranspiler.rs +++ b/src/runtime/api/JSTranspiler.rs @@ -162,7 +162,7 @@ impl Config { pub(crate) fn from_js( &mut self, global: &JSGlobalObject, - object: Option>, + object: Option<&TranspilerOptions<'_>>, arena: &Arena, ) -> JsResult<()> { let Some(object) = object else { @@ -221,7 +221,7 @@ impl Config { } } - if let Some(external) = object.external { + if let Some(external) = &object.external { match external { TranspilerExternal::Name(str) => { if !str.is_empty() { @@ -893,7 +893,7 @@ fn export_replacement_value( impl JSTranspiler { pub(crate) fn constructor( cx: &mut JsScope<'_>, - config_arg: Option>, + config_arg: Option<&TranspilerOptions<'_>>, ) -> JsResult<*mut JSTranspiler> { let global = cx.global(); // NOTE: a non-POD field cannot be left uninitialized in a live Box diff --git a/src/runtime/api/MarkdownObject.bind.ts b/src/runtime/api/MarkdownObject.bind.ts index 24b1be9f9157..1346837bb6bb 100644 --- a/src/runtime/api/MarkdownObject.bind.ts +++ b/src/runtime/api/MarkdownObject.bind.ts @@ -32,8 +32,8 @@ const options = { noHtmlSpans: { type: flag, altNames: ["no_html_spans"] }, tagFilter: { type: flag, altNames: ["tag_filter"] }, } as const; -export const MarkdownOptions = b.dictionary(options, ignoresNonObjects); -export const ReactOptions = b.dictionary({ reactVersion: b.f64.otherAsNull, ...options }, ignoresNonObjects); +export const MarkdownOptions = b.dictionary(options, { allowArray: true }); +export const ReactOptions = b.dictionary({ reactVersion: b.f64.otherAsNull, ...options }, { allowArray: true }); export const AnsiTheme = b.dictionary( { colors: flag, hyperlinks: flag, kittyGraphics: flag, light: flag, columns: b.f64.otherAsNull }, @@ -109,18 +109,18 @@ export const ComponentOverrides = b.dictionary( const js = { capability: "js", throws: true } as const; export const markdown = b.namespace({ - html: { args: { input, options: MarkdownOptions.default({}) }, returns: b.string.utf8, expected, ...js }, + html: { args: { input, options: MarkdownOptions.otherAsNull }, returns: b.string.utf8, expected, ...js }, ansi: { args: { input, theme: AnsiTheme.default({}) }, returns: b.string.utf8, expected, length: 2, ...js }, /** The callbacks run while `input` is read. */ render: { - args: { input: b.any, callbacks: RenderCallbacks.otherAsNull, options: MarkdownOptions.default({}) }, + args: { input: b.any, callbacks: RenderCallbacks.otherAsNull, options: MarkdownOptions.otherAsNull }, returns: b.string.utf8, length: 3, ...js, }, /** As `render`. */ react: { - args: { input: b.any, components: ComponentOverrides.otherAsNull, options: ReactOptions.default({}) }, + args: { input: b.any, components: ComponentOverrides.otherAsNull, options: ReactOptions.otherAsNull }, returns: b.any, length: 3, ...js, diff --git a/src/runtime/api/MarkdownObject.rs b/src/runtime/api/MarkdownObject.rs index 2606ed63aacc..b8879a39abc8 100644 --- a/src/runtime/api/MarkdownObject.rs +++ b/src/runtime/api/MarkdownObject.rs @@ -69,7 +69,7 @@ fn parser_err_to_js( } } -/// `md::Options` of a `MarkdownOptions` or a `ReactOptions`, which have these members in common. +/// `md::Options` of an `Option<&MarkdownOptions>` or an `Option<&ReactOptions>`, which have these members in common. macro_rules! parser_options { ($given:expr) => { parser_options!( @@ -90,43 +90,44 @@ macro_rules! parser_options { ) }; ($given:expr; $($flag:ident),*) => {{ - let given = $given; let mut options = md::Options::default(); - match &given.autolinks { - Some(Autolinks::All(all)) => options.permissive_autolinks |= *all, - Some(Autolinks::Each(each)) => { - if let Some(url) = each.url { - options.permissive_url_autolinks = url; - } - if let Some(www) = each.www { - options.permissive_www_autolinks = www; - } - if let Some(email) = each.email { - options.permissive_email_autolinks = email; + if let Some(given) = $given { + match &given.autolinks { + Some(Autolinks::All(all)) => options.permissive_autolinks |= *all, + Some(Autolinks::Each(each)) => { + if let Some(url) = each.url { + options.permissive_url_autolinks = url; + } + if let Some(www) = each.www { + options.permissive_www_autolinks = www; + } + if let Some(email) = each.email { + options.permissive_email_autolinks = email; + } } + None => {} } - None => {} - } - match &given.headings { - Some(Headings::All(all)) => { - options.heading_ids |= *all; - options.autolink_headings |= *all; - } - Some(Headings::Each(each)) => { - if let Some(ids) = each.ids { - options.heading_ids = ids; + match &given.headings { + Some(Headings::All(all)) => { + options.heading_ids |= *all; + options.autolink_headings |= *all; } - if let Some(autolink) = each.autolink { - options.autolink_headings = autolink; + Some(Headings::Each(each)) => { + if let Some(ids) = each.ids { + options.heading_ids = ids; + } + if let Some(autolink) = each.autolink { + options.autolink_headings = autolink; + } } + None => {} } - None => {} + $( + if let Some(value) = given.$flag { + options.$flag = value; + } + )* } - $( - if let Some(value) = given.$flag { - options.$flag = value; - } - )* options }}; } @@ -184,7 +185,7 @@ pub(crate) fn ansi<'a>( pub(crate) fn html<'a>( cx: &mut JsScope<'a>, input: JsBytes<'a>, - options: &MarkdownOptions, + options: Option<&MarkdownOptions>, ) -> JsResult> { let input = input.get(&cx.heap); let input_len = input.len(); @@ -200,8 +201,8 @@ pub(crate) fn html<'a>( pub(crate) fn render<'a>( cx: &mut JsScope<'a>, input: JsLocal<'a>, - callbacks: Option>, - options: &MarkdownOptions, + callbacks: Option<&RenderCallbacks<'a>>, + options: Option<&MarkdownOptions>, ) -> JsResult> { let global_this = cx.global(); let input_value = input.to_js(); @@ -231,7 +232,7 @@ pub(crate) fn render<'a>( }; if let Some(callbacks) = callbacks { - js_renderer.extract_callbacks(&callbacks); + js_renderer.extract_callbacks(callbacks); } // Run parser with the JS callback renderer @@ -249,11 +250,11 @@ pub(crate) fn render<'a>( pub(crate) fn react<'a>( cx: &mut JsScope<'a>, input: JsLocal<'a>, - components: Option>, - options: &ReactOptions, + components: Option<&ComponentOverrides<'a>>, + options: Option<&ReactOptions>, ) -> JsResult { let global_this = cx.global(); - let react_version: u8 = match options.react_version { + let react_version: u8 = match options.and_then(|options| options.react_version) { // react.element (React 18 and older) Some(version) if version as i32 <= 18 => 0, // react.transitional.element (React 19+) @@ -285,7 +286,7 @@ unsafe extern "C" { fn render_ast( global_this: &JSGlobalObject, input_value: JSValue, - components: Option>, + components: Option<&ComponentOverrides<'_>>, options: md::Options, marked_args: &mut MarkedArgumentBuffer, react_version: Option, @@ -318,7 +319,7 @@ fn render_ast( }; if let Some(components) = components { - renderer.extract_components(&components); + renderer.extract_components(components); } if let Err(err) = md::render_with_renderer(input, options, renderer.renderer()) { diff --git a/src/runtime/api/cron.rs b/src/runtime/api/cron.rs index f3181b383874..b07e0a093077 100644 --- a/src/runtime/api/cron.rs +++ b/src/runtime/api/cron.rs @@ -712,7 +712,7 @@ impl CronRegisterJob { } /// Resolve the `{ tz?: string }` option to a `CronTz`. -fn resolve_cron_tz(global: &JSGlobalObject, options: Option>) -> JsResult { +fn resolve_cron_tz(global: &JSGlobalObject, options: Option<&CronOptions<'_>>) -> JsResult { let Some(tz_bytes) = options.and_then(|options| options.tz) else { return Ok(CronTz::Local); }; @@ -1716,7 +1716,7 @@ impl CronJob { cx: &mut JsScope<'a>, (schedule, schedule_value): (&[u8], JsLocal<'a>), handler: JsLocal<'a, Callable>, - options: Option>, + options: Option<&CronOptions<'a>>, ) -> JsResult { let tz = resolve_cron_tz(cx.global(), options)?; let cx = &cx.js_thread(); @@ -1867,7 +1867,7 @@ pub(crate) fn parse( cx: &mut JsScope<'_>, expression: &[u8], from: Option, - options: Option>, + options: Option<&CronOptions<'_>>, ) -> JsResult> { let global = cx.global(); diff --git a/src/runtime/node/node_crypto_binding.bind.ts b/src/runtime/node/node_crypto_binding.bind.ts index 70bfbfa5cde5..9a6429ad85bc 100644 --- a/src/runtime/node/node_crypto_binding.bind.ts +++ b/src/runtime/node/node_crypto_binding.bind.ts @@ -63,7 +63,7 @@ const argon2Parameters = (bytes: b.Type) => ({ export const crypto = b.namespace({ pbkdf2: [ - /** A call of five arguments is this one unless the fifth is a function, so `callback` can be left out. The body reports that. */ + /** `callback` is optional so that a call without one is this overload and reports `digest` first. The body reports the missing `callback`. */ { args: { ...passwordAndSaltHandles, ...pbkdf2Parameters, digest: b.string.utf8, callback: b.callable.optional }, expected: { ...passwordAndSaltNames, digest: ["string"], callback: ["function"] }, diff --git a/src/runtime/node/node_http_binding.rs b/src/runtime/node/node_http_binding.rs index d92dcbc50081..33200070b97f 100644 --- a/src/runtime/node/node_http_binding.rs +++ b/src/runtime/node/node_http_binding.rs @@ -53,7 +53,7 @@ pub(crate) fn upgrade_node_http_response_to_web_socket<'a>( handshake: &[u8], max_payload: u32, flags: i32, - deflate: Option, + deflate: Option<&DeflateParams>, ) -> JsResult { const AUTO_PONG: i32 = 1 << 0; const SKIP_UTF8_VALIDATION: i32 = 1 << 1; diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 602b6327e036..cd77aeec028f 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -147,10 +147,16 @@ itAlways("mistakes in a schema are errors when it loads", () => { expect(() => b.f64.loose.allowDate).toThrow(".allowDate follows b.f64"); expect(() => b.f64.allowDate.loose).toThrow(".loose follows b.f64"); for (const type of [b.any, b.string.loose, b.bytes({ from: ["buffer"] }), b.u8.validateInteger(0, 4)]) { - expect(() => type.otherAsNull).toThrow(".otherAsNull goes by the kind of a value: this type takes every kind"); + expect(() => type.otherAsNull).toThrow( + ".otherAsNull goes by the kind of a value: the conversion of this type decides", + ); } - for (const type of [b.i32, b.enumeration(["a"]), b.dictionary({ x: b.f64 })]) { - expect(() => type.otherAsNull).toThrow("this type throws for some values of a kind it takes"); + for (const [type, remedy] of [ + [b.i32, "b.f64.otherAsNull takes every number"], + [b.enumeration(["a"]), "b.string.otherAsNull takes every string"], + [b.dictionary({ x: b.f64 }), "give the dictionary allowArray"], + ] as const) { + expect(() => type.otherAsNull).toThrow(`this type throws for some of a kind: ${remedy}`); } expect(() => b.union({ number: b.f64, text: b.string }).otherAsNull).toThrow( "a union takes the rest as its last alternative: ignored: b.any", @@ -821,17 +827,20 @@ describe("functions", () => { it(".otherAsNull is absent for every value of a kind the type does not take", () => { const { presentArguments } = bindgen; expect(presentArguments()).toEqual([]); - expect(presentArguments(1.5, "", () => {}, new bindgen.TestCounter(1), {})).toEqual([ + expect(presentArguments(1.5, "", () => {}, [], [1], new bindgen.TestCounter(1), {})).toEqual([ "number", "text", "callback", + "object", + "numbers", "counter", "options", ]); - expect(presentArguments("1", 1, {}, {}, 1)).toEqual([]); - expect(presentArguments(null, Symbol(), null, null, null)).toEqual([]); + expect(presentArguments("1", 1, {}, 1, {}, {}, 1)).toEqual([]); + expect(presentArguments(null, Symbol(), null, null, "1", null, null)).toEqual([]); expect(presentArguments(NaN, new String("s"), new Proxy(() => {}, {}))).toEqual(["number", "text", "callback"]); - const options = (value: unknown) => presentArguments(0, 0, 0, 0, value).slice(1); + expect(() => presentArguments(0, 0, 0, 0, ["1"])).toThrow(TypeError); + const options = (value: unknown) => presentArguments(0, 0, 0, 0, 0, 0, value).slice(1); expect(options({ size: 1, callback() {} })).toEqual(["options", "options.size", "options.callback"]); expect(options({ size: "1", callback: {} })).toEqual(["options"]); expect(options([])).toEqual(["options"]); @@ -874,7 +883,7 @@ describe("functions", () => { }); }); -describe("how a dictionary reports its members", () => { +describe("the members of a dictionary", () => { it(".validateInteger can be absent", () => { expect(bindgen.nodeOptionsSize({})).toBe(undefined); expect(bindgen.nodeOptionsSize({ size: undefined })).toBe(undefined); From 9d189310728af06b5abdef18251d0714e65dd022 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 03:09:20 +0000 Subject: [PATCH 14/22] Keep how an argument reaches the body with its type, and say what to write in the refusals of `.otherAsNull` --- docs/project/bindgen.mdx | 4 ++-- src/codegen/bindgen-emit.ts | 7 +------ src/codegen/bindgen-lib-internal.ts | 17 ++++++++++++++--- src/jsc/bindgen_test.rs | 5 +---- test/internal/bindgen.test.ts | 7 ++++--- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index c6f14c2d5e52..904f931f9fe7 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -256,7 +256,7 @@ These come last, in this order (`b.any` already takes `undefined` and `null`, so - `.nullable`: `null` or `undefined` means absent; `Option`. - `.falsyAsNull`: every falsy value means absent; `Option`. - `.emptyStringAsNull`: `undefined`, `null` or `""` means absent, as for `JSValue::get_truthy`; `Option`. -- `.otherAsNull`: every value of a [kind](#unions-and-kinds) the type does not take, or that it refuses (what is not an object, a shared buffer), means absent: what a union would give an `ignored: b.any` written after the type; `Option`. It is for an argument or a member that is ignored unless it is of the type. What is inside a value is still reported: the members of a dictionary, the elements of an array. It is not for a type that throws for a value of a kind it takes: an integer for `1.5` (`b.f64.otherAsNull` takes every number), an enumeration for an unknown string (`b.string.otherAsNull` takes every string), a dictionary without `allowArray` for an array. A union writes `ignored: b.any` as its last alternative. +- `.otherAsNull`: every value of a [kind](#unions-and-kinds) the type does not take, or that it refuses (what is not an object, a shared buffer), means absent: what a union would give an `ignored: b.any` written after the type; `Option`. It is for an argument or a member that is ignored unless it is of the type. What is inside a value is still reported: the members of a dictionary, the elements of an array. It is not for a type that throws for a value of a kind it takes: an integer for `1.5` (`b.f64.otherAsNull` takes every number), an enumeration for an unknown string (`b.string.allowStringObject.otherAsNull` takes every string), a dictionary without `allowArray` for an array. Nor is it for a type that takes every kind, like a `.loose` one or a `nonObjectAsEmpty` dictionary. A union writes `ignored: b.any` as its last alternative. - `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` both work and differ, like `alert`. It also goes on a type that takes `undefined`, like `b.any`. It is only for an argument, and not for one of an overload. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. @@ -334,7 +334,7 @@ whose type does not accept `undefined` is required. A member can be written a function's `expected` does. A dictionary whose members can all be absent implements `Default`. As an argument, with `.default()` or without, the struct stays in the frame of the thunk and the body borrows it: -`options: &BracesOptions`, or `Option<&BracesOptions>` with one of the modifiers that make it an `Option`. As a member, or inside a union or an array, it is held by value. +`options: &BracesOptions`, or `Option<&BracesOptions>` with one of the modifiers that make it an `Option`. As a member, with `.withValue`, as the value of a setter, or inside a union or an array, it is held by value. A member is looked up on the object and its prototype chain, **stopping before the calling realm's `Object.prototype`**, so a polluted `Object.prototype` cannot diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 4b360069cbfe..9d0ba14e8304 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -1392,12 +1392,7 @@ const rustExternParameters = (signature: Signature, scope: RustScope) => signature.args.map( arg => `${rustArgumentName(arg)}: ${isPassedByPointer(arg.type) ? "*mut " : ""}${arg.type.rustExternType(scope)}`, ); -const rustArguments = (signature: Signature) => - signature.args.map(arg => { - const name = rustArgumentName(arg); - if (arg.type instanceof OptionalType && arg.type.inner instanceof DictionaryType) return `${name}.as_ref()`; - return (arg.type instanceof DefaultType ? arg.type.inner : arg.type).borrowedByBody ? `&${name}` : name; - }); +const rustArguments = (signature: Signature) => signature.args.map(arg => arg.type.rustArgument(rustArgumentName(arg))); /** Each argument gets the call's lifetime here: left to inference in the call, a body could ask for `'static`. */ function rustTakeArguments(args: readonly { name: string; type: Type }[], scope: RustScope): string { // What can return early comes last: by then Rust owns whatever the other arguments hand over. diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 433eef8379c1..d27e846e7863 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -268,6 +268,10 @@ export abstract class Type { get borrowedByBody(): boolean { return false; } + /** What the thunk passes to the body for the argument it holds as `name`. */ + rustArgument(name: string): string { + return this.borrowedByBody ? `&${name}` : name; + } /** `rustFromExtern` is a `bindgen::Result`. Only for an argument. */ get rustFromExternThrows(): boolean { return false; @@ -1352,11 +1356,11 @@ export class OptionalType extends Type { assert(!(inner instanceof UnionType), "a union takes the rest as its last alternative: ignored: b.any"); assert( inner.valueKinds !== "any" || refusesNonObjects(inner), - ".otherAsNull goes by the kind of a value: the conversion of this type decides", + ".otherAsNull goes by the kind of a value, and this type does not: write it without .loose or nonObjectAsEmpty", ); assert( !inner.narrowsItsKinds, - `.otherAsNull goes by the kind of a value, and this type throws for some of a kind: ${inner.narrowsItsKinds}`, + `.otherAsNull goes by the kind of a value, and this type throws for a value of a kind it takes: ${inner.narrowsItsKinds}`, ); } // `.ifPassed` does not claim `undefined`, so it goes on a type that takes it. @@ -1467,6 +1471,10 @@ export class OptionalType extends Type { rustType(scope: RustScope): string { return `Option<${this.inner.rustType(scope)}>`; } + /** The body borrows a dictionary, `Option<&T>`, and takes over a string. */ + rustArgument(name: string): string { + return this.inner instanceof DictionaryType ? `${name}.as_ref()` : name; + } rustFromExtern(expr: string, scope: RustScope): string { if (this.inner.nullRepresentation === "optional") { const inner = this.inner.rustFromExtern("value", scope); @@ -1594,6 +1602,9 @@ export class DefaultType extends Type { get refusalOutsideArgument(): string | null { return this.inner.refusalOutsideArgument; } + rustArgument(name: string): string { + return this.inner.rustArgument(name); + } get conversionThrows(): boolean { return this.inner.conversionThrows; } @@ -1809,7 +1820,7 @@ export class EnumerationType extends NamedType { return ["string"]; } get narrowsItsKinds(): string { - return "b.string.otherAsNull takes every string"; + return "b.string.allowStringObject.otherAsNull takes every string"; } get idlType(): string { return `WebCore::IDLEnumeration<${this.cpp}>`; diff --git a/src/jsc/bindgen_test.rs b/src/jsc/bindgen_test.rs index 455e5a969992..52806c67859f 100644 --- a/src/jsc/bindgen_test.rs +++ b/src/jsc/bindgen_test.rs @@ -962,10 +962,7 @@ pub(crate) fn present_arguments( ("text", text.is_some()), ("callback", callback.is_some()), ("object", object.is_some()), - ( - "numbers", - numbers.is_some_and(|numbers| !numbers.is_empty()), - ), + ("numbers", numbers.map(drop).is_some()), ("counter", counter.is_some()), ("options", members.is_some()), ("options.size", size.is_some()), diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index cd77aeec028f..f623f88936db 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -148,15 +148,15 @@ itAlways("mistakes in a schema are errors when it loads", () => { expect(() => b.f64.allowDate.loose).toThrow(".loose follows b.f64"); for (const type of [b.any, b.string.loose, b.bytes({ from: ["buffer"] }), b.u8.validateInteger(0, 4)]) { expect(() => type.otherAsNull).toThrow( - ".otherAsNull goes by the kind of a value: the conversion of this type decides", + ".otherAsNull goes by the kind of a value, and this type does not: write it without .loose or nonObjectAsEmpty", ); } for (const [type, remedy] of [ [b.i32, "b.f64.otherAsNull takes every number"], - [b.enumeration(["a"]), "b.string.otherAsNull takes every string"], + [b.enumeration(["a"]), "b.string.allowStringObject.otherAsNull takes every string"], [b.dictionary({ x: b.f64 }), "give the dictionary allowArray"], ] as const) { - expect(() => type.otherAsNull).toThrow(`this type throws for some of a kind: ${remedy}`); + expect(() => type.otherAsNull).toThrow(`this type throws for a value of a kind it takes: ${remedy}`); } expect(() => b.union({ number: b.f64, text: b.string }).otherAsNull).toThrow( "a union takes the rest as its last alternative: ignored: b.any", @@ -839,6 +839,7 @@ describe("functions", () => { expect(presentArguments("1", 1, {}, 1, {}, {}, 1)).toEqual([]); expect(presentArguments(null, Symbol(), null, null, "1", null, null)).toEqual([]); expect(presentArguments(NaN, new String("s"), new Proxy(() => {}, {}))).toEqual(["number", "text", "callback"]); + expect(presentArguments(0, 0, 0, 0, [])).toEqual(["number", "numbers"]); expect(() => presentArguments(0, 0, 0, 0, ["1"])).toThrow(TypeError); const options = (value: unknown) => presentArguments(0, 0, 0, 0, 0, 0, value).slice(1); expect(options({ size: 1, callback() {} })).toEqual(["options", "options.size", "options.callback"]); From 52f16cf238146948b1eb297a83fa54b2658c4b97 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 09:11:10 +0000 Subject: [PATCH 15/22] Have a conversion say by what it returns that it threw, compare one spelling of an enumeration, and name public arguments as the docs do --- docs/project/bindgen.mdx | 10 +- src/codegen/bindgen-emit.ts | 105 ++++++----- src/codegen/bindgen-lib-internal.ts | 173 +++++++++--------- src/codegen/bindgen-lib.ts | 3 +- src/codegen/generate-host-exports.ts | 4 +- src/css_jsc/color_js.bind.ts | 5 +- src/css_jsc/color_js.rs | 6 +- src/install_jsc/hosted_git_info_jsc.bind.ts | 2 +- src/install_jsc/hosted_git_info_jsc.rs | 2 +- .../bindings/Bindgen/DictionaryMemberReader.h | 2 +- src/jsc/bindings/Bindgen/IDLConvert.h | 79 ++++---- src/jsc/bindings/Bindgen/IDLConvertBase.h | 7 + .../bindings/Bindgen/IDLConvertIntegerRange.h | 29 ++- src/jsc/bindings/Bindgen/IDLTypes.h | 2 +- src/jsc/bindings/Bindgen/JSBytesView.cpp | 8 +- src/jsc/bindings/Bindgen/JSBytesView.h | 21 ++- src/jsc/bindings/Bindgen/JSStringUTF8View.h | 14 +- src/jsc/bindings/BunIDLConvertBase.h | 39 +++- src/jsc/host_fn.rs | 42 +---- src/runtime/api/BunObject.bind.ts | 30 +-- src/runtime/api/BunObject.rs | 24 +-- src/runtime/api/BunObjectInternals.bind.ts | 1 - .../api/bun/js_bun_spawn_bindings.bind.ts | 1 - src/runtime/api/cron.bind.ts | 13 +- src/runtime/api/cron.rs | 6 +- src/runtime/ffi/ffi.bind.ts | 2 +- src/runtime/ipc_host.bind.ts | 1 - src/runtime/node/node_net_binding.bind.ts | 2 +- src/runtime/node/node_net_binding.rs | 8 +- src/runtime/socket/UpgradedDuplex.bind.ts | 3 +- src/runtime/socket/UpgradedDuplex.rs | 2 +- src/runtime/test_runner/BunTest.bind.ts | 1 - .../test_runner/ScopeFunctions.bind.ts | 5 +- src/runtime/test_runner/ScopeFunctions.rs | 14 +- .../test_runner/timers/FakeTimers.bind.ts | 5 +- src/runtime/test_runner/timers/FakeTimers.rs | 2 +- src/runtime/webcore/Blob.bind.ts | 4 +- src/runtime/webcore/Crypto.bind.ts | 1 - src/runtime/webcore/ObjectURLRegistry.bind.ts | 1 - src/runtime/webcore/fetch.bind.ts | 2 +- src/runtime/webcore/prompt.bind.ts | 5 +- src/sql_jsc/mysql.bind.ts | 2 +- src/sql_jsc/mysql/JSMySQLConnection.rs | 3 +- src/sql_jsc/postgres.bind.ts | 2 +- src/sql_jsc/postgres/PostgresSQLConnection.rs | 3 +- src/sql_jsc/shared/QueryCtorArgs.rs | 3 +- test/internal/bindgen.test.ts | 4 +- test/js/bun/css/color.test.ts | 2 +- test/js/bun/resolve/resolve-test.js | 4 +- test/js/bun/test/describe.test.ts | 10 + .../bun/util/socket-fault-injection.test.ts | 12 +- test/js/node/crypto/crypto-random.test.ts | 1 + test/js/sql/sql-options.test.ts | 8 + 53 files changed, 401 insertions(+), 339 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index 904f931f9fe7..cb76128eb29e 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -256,11 +256,11 @@ These come last, in this order (`b.any` already takes `undefined` and `null`, so - `.nullable`: `null` or `undefined` means absent; `Option`. - `.falsyAsNull`: every falsy value means absent; `Option`. - `.emptyStringAsNull`: `undefined`, `null` or `""` means absent, as for `JSValue::get_truthy`; `Option`. -- `.otherAsNull`: every value of a [kind](#unions-and-kinds) the type does not take, or that it refuses (what is not an object, a shared buffer), means absent: what a union would give an `ignored: b.any` written after the type; `Option`. It is for an argument or a member that is ignored unless it is of the type. What is inside a value is still reported: the members of a dictionary, the elements of an array. It is not for a type that throws for a value of a kind it takes: an integer for `1.5` (`b.f64.otherAsNull` takes every number), an enumeration for an unknown string (`b.string.allowStringObject.otherAsNull` takes every string), a dictionary without `allowArray` for an array. Nor is it for a type that takes every kind, like a `.loose` one or a `nonObjectAsEmpty` dictionary. A union writes `ignored: b.any` as its last alternative. -- `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` both work and differ, like `alert`. It also goes on a type that takes `undefined`, like `b.any`. It is only for an argument, and not for one of an overload. +- `.otherAsNull`: `Option`. A value of a [kind](#unions-and-kinds) the type does not take, or that it refuses (what is not an object, a shared buffer), is `None` and nothing is thrown, as if the type were in a union with `ignored: b.any` after it. It is for an argument or a member that is ignored unless it is of the type. What is inside a value is still reported: the members of a dictionary, the elements of an array. It is not for a type that throws for a value of a kind it takes: an integer for `1.5` (`b.f64.otherAsNull` takes every number), an enumeration for an unknown string (`b.string.allowStringObject.otherAsNull` takes every string), a dictionary without `allowArray` for an array. Nor is it for a type that takes every kind, like a `.loose` one or a `nonObjectAsEmpty` dictionary. A union writes `ignored: b.any` as its last alternative. +- `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` differ: both work, like `alert`, or a test of Node pins an error for each, like `URL.revokeObjectURL`. It also goes on a type that takes `undefined`, like `b.any`. It is only for an argument, and not for one of an overload. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. -`.withValue` on an argument also gives the body the argument itself, `(T, JsLocal<'a>)`: for one that reads the +`.withValue` on an argument also gives the body the argument itself, `(T, JsLocal<'a>)`: for a body that reads the internal fields of an instance, or returns the argument when there is nothing to change. It comes after all of these, and not with `.ifPassed`. @@ -390,8 +390,8 @@ reports its own error takes every kind: `.loose`, `.wrap`, `.enforceRange`, alternative then converts it, and reports what is wrong with it: `1.5` given to `b.union({ integer: b.i32, text: b.string })` "must be an integer", not "must be of type integer or string". The buffer types refuse some values of their kinds (a shared buffer); those go on to the next alternative that takes -the kind and does not refuse the same. A dictionary and `b.object` refuse what is not an object, and it goes on the same way. As the last -alternative to take the kind, they get the value anyway and report it. A value no alternative takes throws `ERR_INVALID_ARG_TYPE` listing them all. +the kind and does not refuse the same. What is not an object skips a dictionary or `b.object` in the same way, unless that is the last +alternative to take its kind, which then reports it. A value no alternative takes throws `ERR_INVALID_ARG_TYPE` listing them all. So the order only matters between alternatives that overlap, and one that takes every value goes last. An alternative that can never be chosen is an error when the schema is diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 9d0ba14e8304..7e9116afc09b 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -8,6 +8,7 @@ import { type ClassMember, ClassType, cppEmplace, + cppReturnIfFailed, DefaultType, DictionaryType, EnumerationType, @@ -218,12 +219,11 @@ function cppConvertToAlternative(type: UnionType, candidates: readonly number[]) .map(i => { const { name, type: alternative } = type.alternatives[i]; const member = `result.${name}`; - const convert = (target: string) => alternative.cppConvertInto(target, "value", "ctx", "", ""); - if (alternative instanceof UnitType) return `result.tag = ${i};\nreturn;`; + const convert = (target: string) => alternative.cppConvertInto(target, "value", "ctx", "", "false"); + if (alternative instanceof UnitType) return `result.tag = ${i};\nreturn true;`; const take = `result.tag = ${i}; -scope.release(); ${alternative.convertsInPlace ? cppEmplace(member, convert) : convert(member)} -return;`; +return true;`; // As the last, its own conversion reports what is not an object. if (refusesNonObjects(alternative)) return i === candidates.at(-1) ? take : `if (value.isObject()) {\n${indent(take, 4)}\n}`; @@ -231,9 +231,8 @@ return;`; return `if (auto converted = Bun::tryConvertIDL<${alternative.idlType}>(*globalObject, value, ctx)) { ${member} = WTF::move(*converted); result.tag = ${i}; - return; -} -RETURN_IF_EXCEPTION(scope, void());`; + return true; +}${alternative.tryConvertThrows ? "\nRETURN_IF_EXCEPTION(scope, false);" : ""}`; return take; }) .join("\n"); @@ -305,7 +304,7 @@ template<> struct WebCore::Converter<${type.idlType}> : Bun::DefaultContextConve ` } template - static void convertInto(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx, ReturnType& result) + static bool convertInto(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx, ReturnType& result) { auto* globalObject = &lexicalGlobalObject; auto& vm = JSC::getVM(globalObject); @@ -313,7 +312,8 @@ template<> struct WebCore::Converter<${type.idlType}> : Bun::DefaultContextConve ${indent(code, 8)} switch (choice) { ${indent(cases.join("\n"), 8)} - }${outcomes.some(mayMatchNone) ? `\n ctx.template throwNoMatchInUnion<${idlTypes}>(*globalObject, scope);` : ""} + } + ${outcomes.some(mayMatchNone) ? `ctx.template throwNoMatchInUnion<${idlTypes}>(*globalObject, scope);\n return false;` : "RELEASE_ASSERT_NOT_REACHED();"} } }; `; @@ -363,7 +363,7 @@ ${type.members.map(member => ` .${member.fieldName} = ${toExtern(`val template<> struct Bun::IDLHumanReadableName<${type.idlType}> : Bun::BaseIDLHumanReadableName { static constexpr auto humanReadableName = std::to_array("${type.name}"); }; -template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue, ${type.cpp}&); +template<> bool Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalObject&, JSC::JSValue, ${type.cpp}&); ${ type.needsLifetime ? `template<> struct WebCore::IDLDictionary<${type.cpp}> : Bun::Bindgen::IDLStackOnlyDictionary<${type.cpp}> {};` @@ -443,17 +443,34 @@ extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${origin.stem}_${names `; } +/** C++ that returns the variant `string` spells, of `candidates` of its length: the characters that tell them apart leave one to compare. */ +function cppMatchSpelling(candidates: readonly (readonly [spelling: string, variant: string])[]): string { + if (candidates.length === 1) { + const [[spelling, variant]] = candidates; + return `if (string == ${asciiLiteral(spelling)})\n return ${variant};\nbreak;`; + } + const groupsAt = (at: number) => { + const groups = new Map(); + for (const candidate of candidates) + groups.set(candidate[0][at], [...(groups.get(candidate[0][at]) ?? []), candidate]); + return groups; + }; + const positions = Array.from(candidates[0][0], (character, at) => at); + const at = positions.reduce((best, at) => (groupsAt(at).size > groupsAt(best).size ? at : best)); + return `switch (string[${at}]) { +${[...groupsAt(at)].map(([character, group]) => `case '${character.replace(/['\\]/, "\\$&")}':\n${indent(cppMatchSpelling(group), 4)}`).join("\n")} +} +break;`; +} + function cppSourceForEnum(type: EnumerationType): string { - // The length rules out most spellings, so few are compared. - const byLength = new Map(); + const byLength = new Map(); type.spellings.forEach((spellings, index) => { - for (const spelling of spellings) { - const tests = byLength.get(spelling.length) ?? []; - byLength.set(spelling.length, tests); - tests.push( - ` if (string == ${asciiLiteral(spelling)})\n return ${type.cpp}::${type.variants[index]};`, - ); - } + for (const spelling of spellings) + byLength.set(spelling.length, [ + ...(byLength.get(spelling.length) ?? []), + [spelling, `${type.cpp}::${type.variants[index]}`], + ]); }); return ` template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type.cpp}>(const WTF::String& string) @@ -461,7 +478,7 @@ template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type switch (string.length()) { ${[...byLength] .sort(([a], [b]) => a - b) - .map(([length, tests]) => ` case ${length}:\n${tests.join("\n")}\n break;`) + .map(([length, candidates]) => ` case ${length}:\n${indent(cppMatchSpelling(candidates), 8)}`) .join("\n")} } return std::nullopt; @@ -508,7 +525,7 @@ function cppSourceForDictionary(type: DictionaryType): string { let nameIndex = 0; const members = type.members.map((member, i) => { const [first, ...others] = member.names.map( - () => `members.get(${nameIndex++});\nRETURN_IF_EXCEPTION(scope, void());`, + () => `members.get(${nameIndex++});\n${cppReturnIfFailed(`!!value${i}`, "false")}`, ); const qualified = type.reportedAs ? `${type.reportedAs}.${member.name}` : member.name; const lines: string[] = []; @@ -520,22 +537,14 @@ function cppSourceForDictionary(type: DictionaryType): string { if (!member.type.takesUndefined) lines.push(`if (value${i}.isUndefined()) [[unlikely]] { ${ctx}.throwRequired(*globalObject, scope); - return; + return false; }`); - lines.push( - member.type.cppConvertInto( - `result.${member.fieldName}`, - `value${i}`, - ctx, - qualified, - "RETURN_IF_EXCEPTION(scope, void());", - ), - ); + lines.push(member.type.cppConvertInto(`result.${member.fieldName}`, `value${i}`, ctx, qualified, "false")); return ` {\n${indent(lines.join("\n"), 8)}\n }`; }); const count = type.names.length; return ` -template<> void Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ${type.cpp}& result) +template<> bool Bun::Bindgen::convertDictionaryInto<${type.cpp}>(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ${type.cpp}& result) { auto* globalObject = &lexicalGlobalObject; auto& vm = JSC::getVM(globalObject); @@ -546,25 +555,26 @@ ${ type.nonObjectAsEmpty ? ` if (!object${type.nonObjectAsEmpty === "typeof" ? " || object->isCallable()" : ""}) { ${indent(type.cppAssign("result", {}), 8)} - return; + return true; }` : ` if (!object) [[unlikely]] { ctx.throwNotObject(*globalObject, scope); - return; + return false; }` } ${ type.allowArray ? "" : ` bool isArray = JSC::isArray(globalObject, value); - RETURN_IF_EXCEPTION(scope, void()); + RETURN_IF_EXCEPTION(scope, false); if (isArray) [[unlikely]] { ctx.throwNotObject(*globalObject, scope); - return; + return false; } ` } Bun::Bindgen::DictionaryMemberReader members(vm, globalObject, object, ${type.firstMemberNameIndex}, ${count}); ${members.join("\n")} + return true; } ${ type.needsLifetime || !type.convertedByValue @@ -585,8 +595,7 @@ extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalOb auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); ${type.cpp} converted; - Bun::Bindgen::convertDictionaryInto(*globalObject, JSC::JSValue::decode(value), converted); - RETURN_IF_EXCEPTION(scope, false); + ${cppReturnIfFailed("Bun::Bindgen::convertDictionaryInto(*globalObject, JSC::JSValue::decode(value), converted)", "false").replace("\n", "\n ")} *result = ${toExtern("converted")}; return true; } @@ -601,20 +610,20 @@ const overloadSuffix = (fn: FunctionDefinition, signature: Signature) => const thunkSymbol = (fn: FunctionDefinition, signature: Signature) => `Bun__Bindgen__${fn.origin!.namespace}__${fn.name.replace(/^_+/, "")}${overloadSuffix(fn, signature)}`; -/** Lines that declare `variable` as what `value` converts to. `check` is that of `cppConvertInto`. */ +/** Lines that declare `variable` as what `value` converts to, and return `failure` if converting throws. */ function cppDeclareConverted( type: Type, variable: string, value: string, ctx: string, name: string, - check: string, + failure: string, ): string[] { - if (!type.convertsInPlace) - return [`auto ${variable} = ${type.cppConvert(value, ctx, name)};`, ...(type.conversionThrows ? [check] : [])]; + if (!type.convertsInPlace && !type.conversionThrows) + return [`auto ${variable} = ${type.cppConvert(value, ctx, name)};`]; return [ `${implementationType(type)} ${variable};`, - ...type.cppConvertInto(variable, value, ctx, name, check).split("\n"), + ...type.cppConvertInto(variable, value, ctx, name, failure).split("\n"), ]; } @@ -650,7 +659,7 @@ function cppCallBody( ); const ctx = cppContext(lines, i, name, expected, value); readLast.push(...type.cppReadLast(`converted${i}`, value)); - lines.push(...cppDeclareConverted(type, `converted${i}`, value, ctx, name, "RETURN_IF_EXCEPTION(scope, {});")); + lines.push(...cppDeclareConverted(type, `converted${i}`, value, ctx, name, "{}")); }); lines.push(...readLast); const args = signature.args.map(({ type }, i) => { @@ -949,7 +958,7 @@ ${ } ${holder} assigned { JSC::JSValue::decode(encodedValue) }; ${cppStaticContext("ctx", `LiteralConversionContext { ${asciiLiteral(`${type.name}.${getter.name}`)} }`)} - ${cppDeclareConverted(getter.type, "value", value, "ctx", getter.name, "RETURN_IF_EXCEPTION(scope, false);").join("\n ")} + ${cppDeclareConverted(getter.type, "value", value, "ctx", getter.name, "false").join("\n ")} RELEASE_AND_RETURN(scope, ${setSymbol}(globalObject, ${getter.isStatic ? "" : "thisObject, "}${toExtern("value")})); } `; @@ -980,7 +989,7 @@ ${ if (JSC::JSValue cached = cachedValue.get()) return JSC::JSValue::encode(cached); JSC::JSValue result = JSC::JSValue::decode(${getSymbol}(globalObject, thisObject)); - RETURN_IF_EXCEPTION(scope, {}); + ${cppReturnIfFailed("!!result", "{}").replace("\n", "\n ")} cachedValue.set(vm, thisObject, result); return JSC::JSValue::encode(result);` } @@ -1055,7 +1064,7 @@ JSC_DEFINE_HOST_FUNCTION(construct${name}, (JSC::JSGlobalObject * globalObject, { ${cppPrologue} JSC::Structure* structure = Bun::Bindgen::structureForNewTarget(globalObject, callFrame, ${type.classIndex}); - RETURN_IF_EXCEPTION(scope, {}); + ${cppReturnIfFailed("!!structure", "{}").replace("\n", "\n ")} ${ type.refCounted && !type.passStructure ? cppCallBody(construct, classSymbolName(type, "construct"), [], call => [ @@ -1225,7 +1234,7 @@ ${takeThis}${cppCallBody(fn.signatures[0], symbol(fn.signatures[0]), leading)} // An overload reads `this` and the callee from the frame. const passed = takeThis ? leading : []; const overloads = fn.signatures.map( - signature => `static JSC::EncodedJSValue ${name}${overloadSuffix(fn, signature)}(${["JSC::JSGlobalObject* globalObject", "JSC::CallFrame* callFrame", ...passed.map(argument => `JSC::JSCell* ${argument}`)].join(", ")}) + signature => `static inline JSC::EncodedJSValue ${name}${overloadSuffix(fn, signature)}(${["JSC::JSGlobalObject* globalObject", "JSC::CallFrame* callFrame", ...passed.map(argument => `JSC::JSCell* ${argument}`)].join(", ")}) { ${cppPrologue} ${cppCallBody(signature, symbol(signature), leading)} diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index d27e846e7863..2f4298065586 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -173,7 +173,9 @@ export function indent(text: string, by: number): string { .join("\n"); } -const cppThenCheck = (statement: string, check: string) => (check ? `${statement}\n${check}` : statement); +/** C++ that returns `failure` if `converted` is false, null or empty, which is how a conversion says that it threw. */ +export const cppReturnIfFailed = (converted: string, failure: string) => + `if (Bun::Bindgen::hasException(scope, ${converted})) [[unlikely]]\n return ${failure};`; /** C++ statements that engage `optional`, a `std::optional`, then fill what it holds: `fill` is given a name for that. */ export function cppEmplace(optional: string, fill: (target: string) => string): string { @@ -237,8 +239,8 @@ export abstract class Type { get refuses(): string | null { return null; } - /** What to write for `.otherAsNull`, if it throws for a value of a kind it takes, like `1.5` or an unknown string. */ - get narrowsItsKinds(): string | null { + /** Set on a type that throws for some values of a kind it takes, like `1.5` or an unknown string: what to write for `.otherAsNull` instead. */ + get otherAsNullRefusal(): string | null { return null; } /** A C++ condition that is true of the values `.otherAsNull` converts in place. */ @@ -252,7 +254,7 @@ export abstract class Type { .map(test => test.replace("cell", `${value}.asCell()`)); return [...ofValue, ...(ofCell.length > 0 ? [`(${value}.isCell() && (${ofCell.join(" || ")}))`] : [])].join(" || "); } - /** Written `.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull`, `.otherAsNull`, `.ifPassed` or `.default()`: as an argument with only such after it, it is one a caller leaves out. */ + /** Written `.optional`, `.nullable`, `.falsyAsNull`, `.emptyStringAsNull`, `.otherAsNull`, `.ifPassed` or `.default()`. `length` leaves out a trailing run of these. */ get isOptional(): boolean { return false; } @@ -260,7 +262,7 @@ export abstract class Type { get passesValue(): boolean { return false; } - /** What a schema that makes it an element, a member, an alternative or a result is told, if it is only for an argument. */ + /** The error for using it anywhere but as an argument, or null. */ get refusalOutsideArgument(): string | null { return this.passesValue ? "b.bytes and .withValue are only for an argument" : null; } @@ -276,10 +278,14 @@ export abstract class Type { get rustFromExternThrows(): boolean { return false; } - /** Converting can throw, so an exception check follows. */ + /** Converting can throw. */ get conversionThrows(): boolean { return true; } + /** Its `tryConvert` can throw: it does more than test the value. */ + get tryConvertThrows(): boolean { + return this.conversionThrows && !this.returnsValueUnchanged; + } /** Holds a JS value: the Rust type is branded with the call. */ get holdsJSValue(): boolean { return this.dependencies.some(type => type.holdsJSValue); @@ -338,9 +344,13 @@ export abstract class Type { get convertsInPlace(): boolean { return false; } - /** C++ statements that convert `value` into `target`, just default-constructed. `check` returns if an exception is pending and follows what can throw; a caller that returns next passes "". */ - cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { - return cppThenCheck(`${target} = ${this.cppConvert(value, ctx, name)};`, this.conversionThrows ? check : ""); + /** C++ statements that convert `value` into `target`, just default-constructed, and return `failure` if that throws. */ + cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { + if (!this.conversionThrows) return `${target} = ${this.cppConvert(value, ctx, name)};`; + return cppReturnIfFailed( + `Bun::convertIDLInto<${this.idlType}>(*globalObject, ${value}, ${ctx}, ${target})`, + failure, + ); } abstract layout(): Layout; @@ -427,6 +437,9 @@ export class BoolType extends PrimitiveType { get conversionThrows(): boolean { return this.valueKinds !== "any"; } + get tryConvertThrows(): boolean { + return false; + } } interface IntegerInfo { @@ -475,7 +488,7 @@ export class IntegerType extends PrimitiveType { assert(typeof value !== "number" || (value >= min && value <= max), `${value} is not in [${min}, ${max}]`); return super.toCpp(value); } - get narrowsItsKinds(): string { + get otherAsNullRefusal(): string { return "b.f64.otherAsNull takes every number"; } /** Also a BigInt that is in range, for an argument whose type is `number | bigint`. */ @@ -570,9 +583,13 @@ export class RangeType extends IntegerType { getHeaders(out: Set): void { out.add("Bindgen/IDLConvertIntegerRange.h"); } - cppConvert(value: string, ctx: string, name: string): string { + /** Its converter takes the name, not a context. */ + cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { const reported = this.modifier === "validateInteger" ? `, ${asciiLiteral(name)}` : ""; - return `WebCore::Converter<${this.idlType}>::convert(*globalObject, ${value}${reported})`; + return cppReturnIfFailed( + `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}${reported}, ${target})`, + failure, + ); } } @@ -598,6 +615,9 @@ export class DoubleType extends PrimitiveType { valueKinds, ); } + get tryConvertThrows(): boolean { + return this.valueKinds === "any"; + } /** A `Date` too, as its time value, which is NaN for an invalid one. */ get allowDate(): DoubleType { assert(this.idlType === "Bun::Bindgen::IDLStrictDouble", ".allowDate follows b.f64"); @@ -851,10 +871,10 @@ export class Utf8Type extends Type { get convertsInPlace(): boolean { return true; } - cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { - return cppThenCheck( - `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target}, "a string"_s);`, - check, + cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { + return cppReturnIfFailed( + `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target}, "a string"_s)`, + failure, ); } cppAssign(target: string, value: unknown): string { @@ -971,7 +991,7 @@ export class BytesType extends Type { get convertsInPlace(): boolean { return true; } - cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { + cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { const spelling = { string: ["a string"], buffer: ["ArrayBuffer", "ArrayBufferView"], blob: ["Blob"] }; const expected = (["string", "buffer", "blob"] as const) .filter(kind => this.#options.from.includes(kind)) @@ -979,14 +999,16 @@ export class BytesType extends Type { if (!this.#options.from.includes("string")) expected[0] = `${/^A/.test(expected[0]) ? "an" : "a"} ${expected[0]}`; const last = expected.pop(); const list = expected.length > 0 ? `${expected.join(", ")} or ${last}` : last!; - const convert = `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target}, ${asciiLiteral(list)})`; - return check ? `if (${convert})\n ${check}` : `${convert};`; + return cppReturnIfFailed( + `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target}, ${asciiLiteral(list)})`, + failure, + ); } cppReadLast(target: string, value: string): string[] { if (!this.#isView) return []; if (this.#options.shared === "copied") return [ - `WTF::MallocSpan ${target}Copy;`, + `Bun::Bindgen::BytesCopy ${target}Copy;`, `if (${target}.${this.#options.held === "borrowed" ? "readCopyingShared" : "copyIfShared"}(*globalObject, ${value}, ${target}Copy)) [[unlikely]]`, ` RETURN_IF_EXCEPTION(scope, {});`, ]; @@ -1087,36 +1109,6 @@ export class BufferSourceType extends Type { } } -/** A function, or a proxy of one, for the body to call or to store. */ -export class CallableType extends Type { - readonly idlType = "Bun::Bindgen::IDLRawCallable"; - readonly tsType = "((...args: any[]) => any)"; - get valueKinds(): ValueKinds { - return ["callable"]; - } - get holdsJSValue(): boolean { - return true; - } - get returnsValueUnchanged(): boolean { - return true; - } - layout(): Layout { - return { size: 8, align: 8 }; - } - rustExternType(): string { - return "JSValue"; - } - rustType(): string { - return "bindgen::JsLocal<'a, bindgen::Callable>"; - } - rustFromExtern(expr: string): string { - return `bindgen::JsLocal::from_extern(${expr}, heap)`; - } - rustToJS(value: string): string { - return `bindgen::to_js::any(heap, ${value})`; - } -} - /** A JS value of some kind, as itself. */ export class LocalType extends Type { readonly idlType: string; @@ -1138,9 +1130,6 @@ export class LocalType extends Type { get refuses(): string | null { return this.#refuses; } - get nullRepresentation() { - return "emptyValue" as const; - } get holdsJSValue(): boolean { return true; } @@ -1165,6 +1154,14 @@ export class LocalType extends Type { } export const objectType = new LocalType("Bun::Bindgen::IDLRawObject", "object", "bun_jsc::JSObject", "any", nonObjects); export const jsArrayType = new LocalType("Bun::Bindgen::IDLRawArray", "any[]", "bun_jsc::JSArray", ["array"], null); +/** A function, or a proxy of one, for the body to call or to store. */ +export const callableType = new LocalType( + "Bun::Bindgen::IDLRawCallable", + "((...args: any[]) => any)", + "bindgen::Callable", + ["callable"], + null, +); export class AnyType extends Type { readonly idlType = "Bun::Bindgen::IDLRawAny"; @@ -1359,8 +1356,8 @@ export class OptionalType extends Type { ".otherAsNull goes by the kind of a value, and this type does not: write it without .loose or nonObjectAsEmpty", ); assert( - !inner.narrowsItsKinds, - `.otherAsNull goes by the kind of a value, and this type throws for a value of a kind it takes: ${inner.narrowsItsKinds}`, + !inner.otherAsNullRefusal, + `.otherAsNull goes by the kind of a value, and this type throws for a value of a kind it takes: ${inner.otherAsNullRefusal}`, ); } // `.ifPassed` does not claim `undefined`, so it goes on a type that takes it. @@ -1383,7 +1380,7 @@ export class OptionalType extends Type { falsyAsNull: "Bun::Bindgen::IDLFalsyAsNull", emptyStringAsNull: "Bun::Bindgen::IDLEmptyStringAsNull", otherAsNull: "Bun::Bindgen::IDLOtherAsNull", - ifPassed: "Bun::Bindgen::IDLNotPassedAsNull", + ifPassed: "Bun::Bindgen::IDLIfPassed", }[this.modifier]; return `${template}<${this.inner.idlType}>`; } @@ -1402,7 +1399,7 @@ export class OptionalType extends Type { return this.modifier === "ifPassed" ? ".ifPassed is only for an argument" : super.refusalOutsideArgument; } get conversionThrows(): boolean { - return this.inner.conversionThrows; + return this.modifier === "otherAsNull" ? this.inner.tryConvertThrows : this.inner.conversionThrows; } get valueKinds(): ValueKinds { if (this.inner.valueKinds === "any" || !["optional", "nullable", "ifPassed"].includes(this.modifier)) return "any"; @@ -1433,18 +1430,16 @@ export class OptionalType extends Type { ifPassed: `!${value}.isEmpty()`, }[this.modifier]; } - /** The converter of a range takes the name, which the converter of `idlType` does not have. */ - cppConvert(value: string, ctx: string, name: string): string { - if (!(this.inner instanceof RangeType)) return super.cppConvert(value, ctx, name); - return `${this.#cppIsPresent(value)} ? std::optional { ${this.inner.cppConvert(value, ctx, name)} } : std::nullopt`; - } - /** What converts in place is null when default-constructed. A pointer to a class instance, which does not, is not. */ - cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { - if (!this.convertsInPlace) return super.cppConvertInto(target, value, ctx, name, check); - const present = this.#cppIsPresent(value); - const convert = (slot: string) => this.inner.cppConvertInto(slot, value, ctx, name, check); + cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { + // One test of the value says both whether it is taken and what it converts to. + if (!this.conversionThrows || (this.modifier === "otherAsNull" && !this.convertsInPlace)) + return super.cppConvertInto(target, value, ctx, name, failure); + const convert = (slot: string) => this.inner.cppConvertInto(slot, value, ctx, name, failure); const body = this.inner.nullRepresentation === "optional" ? cppEmplace(target, convert) : convert(target); - return `if (${present}) {\n${indent(body, 4)}\n}`; + const converted = `if (${this.#cppIsPresent(value)}) {\n${indent(body, 4)}\n}`; + // What converts in place and a `std::optional` are null when default-constructed. A pointer to a class instance is not. + if (this.convertsInPlace || this.inner.nullRepresentation === "optional") return converted; + return `${converted} else {\n ${target} = ${this.inner.idlType}::nullValue();\n}`; } toCpp(value: unknown): string { return value == null ? `${this.idlType}::nullValue()` : this.inner.toCpp(value); @@ -1543,8 +1538,8 @@ export class WithValueType extends Type { cppConvert(value: string, ctx: string, name: string): string { return this.inner.cppConvert(value, ctx, name); } - cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { - return this.inner.cppConvertInto(target, value, ctx, name, check); + cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { + return this.inner.cppConvertInto(target, value, ctx, name, failure); } cppReadLast(target: string, value: string): string[] { return this.inner.cppReadLast(target, value); @@ -1630,12 +1625,12 @@ export class DefaultType extends Type { get convertsInPlace(): boolean { return this.inner.convertsInPlace; } - cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { - if (!this.convertsInPlace) return super.cppConvertInto(target, value, ctx, name, check); + cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { + if (!this.conversionThrows) return super.cppConvertInto(target, value, ctx, name, failure); return `if (${this.cppIsAbsent(value)}) { ${indent(this.cppAssign(target, this.value), 4)} } else { -${indent(this.inner.cppConvertInto(target, value, ctx, name, check), 4)} +${indent(this.inner.cppConvertInto(target, value, ctx, name, failure), 4)} }`; } cppAssign(target: string, value: unknown): string { @@ -1819,7 +1814,7 @@ export class EnumerationType extends NamedType { get valueKinds(): ValueKinds { return ["string"]; } - get narrowsItsKinds(): string { + get otherAsNullRefusal(): string { return "b.string.allowStringObject.otherAsNull takes every string"; } get idlType(): string { @@ -1949,7 +1944,7 @@ export class DictionaryType extends NamedType { get fromJS(): boolean { return !!this.options.fromJS; } - get narrowsItsKinds(): string | null { + get otherAsNullRefusal(): string | null { return this.allowArray ? null : "give the dictionary allowArray"; } get allowArray(): boolean { @@ -1976,8 +1971,11 @@ export class DictionaryType extends NamedType { get convertsInPlace(): boolean { return true; } - cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { - return cppThenCheck(`Bun::Bindgen::convertDictionaryInto(*globalObject, ${value}, ${target}, ${ctx});`, check); + cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { + return cppReturnIfFailed( + `Bun::Bindgen::convertDictionaryInto(*globalObject, ${value}, ${target}, ${ctx})`, + failure, + ); } get reportedAs(): string { return this.options.reportedAs ?? this.name; @@ -2093,10 +2091,10 @@ export class UnionType extends NamedType { get convertsInPlace(): boolean { return true; } - cppConvertInto(target: string, value: string, ctx: string, name: string, check: string): string { - return cppThenCheck( - `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target});`, - check, + cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { + return cppReturnIfFailed( + `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target})`, + failure, ); } get tsType(): string { @@ -2307,7 +2305,7 @@ export class FunctionDefinition { signatures.every( signature => signature instanceof Signature || (signature as FunctionOptions)[key] === options[key], ), - `its overloads have one ${key}`, + `every overload gives ${key} the same value`, ); } assert( @@ -2318,7 +2316,7 @@ export class FunctionDefinition { assert(this.signatures.filter(signature => signature.length !== undefined).length <= 1, "length is given once"); assert( this.signatures.every(signature => signature.rejects === this.signatures[0].rejects), - "its overloads have one rejects", + "every overload gives rejects the same value", ); assert( this.signatures.length === 1 || @@ -2352,7 +2350,9 @@ export class FunctionDefinition { candidates.filter((signature, i) => (types[i] ? test(types[i]) : kind === "undefined")); const ofKind = keep(type => takes(type, kind)); const takers = kind === "undefined" ? keep(type => type.takesUndefined) : ofKind; - const kept = [takers, ofKind, candidates].find(list => list.length > 0)!; + const kept = [takers, ofKind, candidates.filter((signature, i) => types[i]), candidates].find( + list => list.length > 0, + )!; return this.dispatchOn(kept, argument + 1, nodes); }; const kinds = new Set(["undefined" as const, ...distinguishingKinds(types.filter(type => type !== undefined))]); @@ -2713,7 +2713,7 @@ export class ClassType extends NamedType { for (const key of ["privateSymbol", "invalidThis", "name", "enumerable", "configurable"] as const) assert( overloads.every(overload => overload[key] === overloads[0]?.[key]), - `${name}: its overloads have one ${key}`, + `${name}: every overload gives ${key} the same value`, ); assert(!(isStatic && wellKnownSymbol(name)), `${name}: an @@ key is a property of the prototype`); const fn = new FunctionDefinition( @@ -2790,6 +2790,9 @@ export class ClassType extends NamedType { get borrowsCell(): boolean { return true; } + get tryConvertThrows(): boolean { + return false; + } get nullRepresentation() { return "pointer" as const; } diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index d92f56eb1325..5249802bed1d 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -7,7 +7,6 @@ import { BufferSourceType, type BytesOptions, BytesType, - CallableType, type ClassOptions, ClassType, type DictionaryOptions, @@ -53,7 +52,7 @@ export const string = new StringType(); export const any = new AnyType(); /** A function, or a proxy of one, as a `JsLocal<'a, Callable>`. */ -export const callable = new CallableType(); +export { callableType as callable } from "./bindgen-lib-internal.ts"; /** An object, as a `JsLocal<'a, JSObject>`. */ export { objectType as object } from "./bindgen-lib-internal.ts"; diff --git a/src/codegen/generate-host-exports.ts b/src/codegen/generate-host-exports.ts index 325df40f850f..f4d8ddcdc349 100644 --- a/src/codegen/generate-host-exports.ts +++ b/src/codegen/generate-host-exports.ts @@ -24,7 +24,7 @@ // shape impl signature → thunk // ──────────── ───────────────────────────────────────────────────────────────── // lazy (&JSGlobalObject) -> JsResult|JSValue -// → host_fn::host_fn_lazy(g, path::name) +// → host_fn::host_fn_result(g, || path::name(g)) // generic (T0, T1, …) -> R where Tn ∈ {&X, &mut X, *T, scalar} // → unsafe extern "C" fn() -> R { path::name() } // @@ -396,7 +396,7 @@ function emitThunk(e: Export): string { // ABI-identical to non-null `*const JSGlobalObject`; C++ never passes // null here. const body = retIsJsResult - ? ` host_fn::host_fn_lazy(g, ${impl})` + ? ` host_fn::host_fn_result(g, || ${impl}(g))` : ` host_fn::host_fn_lazy_passthrough(g, ${impl})`; return ` // ${loc} diff --git a/src/css_jsc/color_js.bind.ts b/src/css_jsc/color_js.bind.ts index 35086e0f8b58..c393ab2412c3 100644 --- a/src/css_jsc/color_js.bind.ts +++ b/src/css_jsc/color_js.bind.ts @@ -59,10 +59,9 @@ export const ColorOutput = b.union({ const formats = OutputColorFormat.spellings.flat().map(format => `'${format}'`); export const color = b.fn({ - args: { input: ColorInput, format: OutputColorFormat.nullable.default("css") }, - expected: { format: `format must be one of ${formats.slice(0, -1).join(", ")} or ${formats.at(-1)}` }, + args: { input: ColorInput, outputFormat: OutputColorFormat.nullable.default("css") }, + expected: { outputFormat: `outputFormat must be one of ${formats.slice(0, -1).join(", ")} or ${formats.at(-1)}` }, returns: ColorOutput.nullable, capability: "js", throws: true, - length: 2, }); diff --git a/src/css_jsc/color_js.rs b/src/css_jsc/color_js.rs index 30869dc75f68..95edb1f3cc04 100644 --- a/src/css_jsc/color_js.rs +++ b/src/css_jsc/color_js.rs @@ -177,7 +177,7 @@ fn channel_list<'a, const N: usize>( pub(crate) fn color<'a>( cx: &mut JsScope<'a>, input: ColorInput<'a>, - format: OutputColorFormat, + output_format: OutputColorFormat, ) -> bindgen::Result<'a, Option>> { use bun_ast::symbol::Map as SymbolMap; use bun_css as css; @@ -257,7 +257,7 @@ pub(crate) fn color<'a>( return Err(heap.error(format_args!("color() failed to parse {}", kind_name))); } Ok(result) => { - let format: OutputColorFormat = if format == OutputColorFormat::Ansi { + let format: OutputColorFormat = if output_format == OutputColorFormat::Ansi { match OutputSource::color_depth() { // No color terminal, therefore return an empty string ColorDepth::None => return Ok(Some(ColorOutput::Text(BunString::EMPTY))), @@ -266,7 +266,7 @@ pub(crate) fn color<'a>( ColorDepth::C256 => OutputColorFormat::Ansi256, } } else { - format + output_format }; 'formatted: { diff --git a/src/install_jsc/hosted_git_info_jsc.bind.ts b/src/install_jsc/hosted_git_info_jsc.bind.ts index 408d0a5f6db7..0acdf8a207e7 100644 --- a/src/install_jsc/hosted_git_info_jsc.bind.ts +++ b/src/install_jsc/hosted_git_info_jsc.bind.ts @@ -10,7 +10,7 @@ export const parseUrl = b.fn({ }); export const HostedGitInfo = b.dictionary({ - type: { type: b.string, rustName: "host_provider" }, + type: b.string, domain: b.string, project: b.string, user: b.string.nullable, diff --git a/src/install_jsc/hosted_git_info_jsc.rs b/src/install_jsc/hosted_git_info_jsc.rs index d4db7e7c4a5c..909f1de5bf85 100644 --- a/src/install_jsc/hosted_git_info_jsc.rs +++ b/src/install_jsc/hosted_git_info_jsc.rs @@ -41,7 +41,7 @@ pub(crate) fn from_url<'a>( }; Ok(Some(HostedGitInfo { - host_provider: BunString::static_(parsed.host_provider.type_str()), + r#type: BunString::static_(parsed.host_provider.type_str()), domain: BunString::clone_utf8(parsed.host_provider.domain()), project: BunString::clone_utf8(parsed.project()), user: parsed.user().map(BunString::clone_utf8), diff --git a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h index 92316d5d44dd..d5c2fe2c78d4 100644 --- a/src/jsc/bindings/Bindgen/DictionaryMemberReader.h +++ b/src/jsc/bindings/Bindgen/DictionaryMemberReader.h @@ -18,7 +18,7 @@ class DictionaryMemberReader { { } - // May throw. Out of line: inlined, it is most of the size of every generated dictionary converter. + // Empty if it threw. Out of line: inlined, it is most of the size of every generated dictionary converter. NEVER_INLINE JSC::JSValue get(unsigned index); private: diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index 2a58d5407f22..ee64e6e2eb44 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -23,7 +23,7 @@ struct IDLHumanReadableName> : IDLHumanReadableName> {}; template -struct IDLHumanReadableName> +struct IDLHumanReadableName> : IDLHumanReadableName {}; template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { @@ -81,12 +81,12 @@ template struct IDLHumanReadableName -void convertDictionaryInto(JSC::JSGlobalObject&, JSC::JSValue, T& result); +bool convertDictionaryInto(JSC::JSGlobalObject&, JSC::JSValue, T& result); template -void convertDictionaryInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, T& result, Ctx& ctx) +bool convertDictionaryInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, T& result, Ctx& ctx) { // The dictionary names itself. `expected` names the argument. using Context = std::remove_const_t; @@ -94,10 +94,10 @@ void convertDictionaryInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value if (!value.isObject()) [[unlikely]] { auto scope = DECLARE_THROW_SCOPE(JSC::getVM(&globalObject)); ctx.throwNotObject(globalObject, scope); - return; + return false; } } - convertDictionaryInto(globalObject, value, result); + return convertDictionaryInto(globalObject, value, result); } template @@ -127,27 +127,28 @@ template struct BytesConverter : DefaultContextConverter { using Result = typename IDL::ImplementationType; - // `expected` is what `options` takes without `ToString`, for the message. Whether it ran what can throw. + // `expected` is what `options` takes without `ToString`, for the message. False if it threw. template ALWAYS_INLINE static bool convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Result& result, WTF::ASCIILiteral expected) { if constexpr (options.fromString) { - if (value.isString()) { - result.initialize(globalObject, JSC::asString(value)); - return true; - } + if (value.isString()) + return result.initialize(globalObject, JSC::asString(value)); } if constexpr (options.fromBuffer) { if (value.inherits() || value.inherits()) { result.initializeAsBlobOrBuffer(); - return false; + return true; } } if constexpr (options.fromString || options.toStringOtherwise) - convertIntoSlow(globalObject, value, ctx, result, expected); - else if (isBlobOrThrow(globalObject, value, ctx, expected)) + return convertIntoSlow(globalObject, value, ctx, result, expected); + else { + if (!isBlobOrThrow(globalObject, value, ctx, expected)) + return false; result.initializeAsBlobOrBuffer(); - return true; + return true; + } } private: @@ -168,15 +169,17 @@ struct BytesConverter : DefaultContextConverter { } template - NEVER_INLINE static void convertIntoSlow(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Result& result, WTF::ASCIILiteral expected) + NEVER_INLINE static bool convertIntoSlow(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Result& result, WTF::ASCIILiteral expected) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); if constexpr (options.fromBlob) { bool isBlob = checkInMemoryBlob(globalObject, value); - RETURN_IF_EXCEPTION(scope, void()); - if (isBlob) - return result.initializeAsBlobOrBuffer(); + RETURN_IF_EXCEPTION(scope, false); + if (isBlob) { + result.initializeAsBlobOrBuffer(); + return true; + } } bool isRefused; if constexpr (options.toStringOtherwise) @@ -185,10 +188,10 @@ struct BytesConverter : DefaultContextConverter { isRefused = options.refuseStringObject || !value.inherits(); if (isRefused) { ctx.throwTypeMustBe(globalObject, scope, expected); - return; + return false; } JSC::JSString* string = value.toString(&globalObject); - RETURN_IF_EXCEPTION(scope, void()); + RETURN_IF_EXCEPTION(scope, false); RELEASE_AND_RETURN(scope, result.initialize(globalObject, string)); } }; @@ -206,9 +209,13 @@ template<> struct WebCore::Converter static std::optional tryConvert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx&) { - if (value.isString() || value.inherits()) [[likely]] - return value.toWTFString(&globalObject); - return std::nullopt; + if (!value.isString() && !value.inherits()) [[unlikely]] + return std::nullopt; + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto string = value.toWTFString(&globalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + return string; } template @@ -391,8 +398,8 @@ struct WebCore::Converter> }; template -struct WebCore::Converter> - : Bun::DefaultContextConverter> { +struct WebCore::Converter> + : Bun::DefaultContextConverter> { using ReturnType = WebCore::Converter>::ReturnType; @@ -442,9 +449,13 @@ template<> struct WebCore::Converter JSC::JSValue value, Ctx& ctx) { - if (value.isString()) - return value.toWTFString(&globalObject); - return std::nullopt; + if (!value.isString()) + return std::nullopt; + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto string = value.toWTFString(&globalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + return string; } template @@ -465,9 +476,13 @@ struct WebCore::Converter> : Bun::DefaultTryConverte JSC::JSValue value, Ctx& ctx) { - if (JSC::isJSArray(value)) - return Bun::convertIDL::Base>(globalObject, value, ctx); - return std::nullopt; + if (!JSC::isJSArray(value)) + return std::nullopt; + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto array = Bun::convertIDL::Base>(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, std::nullopt); + return array; } template diff --git a/src/jsc/bindings/Bindgen/IDLConvertBase.h b/src/jsc/bindings/Bindgen/IDLConvertBase.h index 106e216d6c74..d44122acd6df 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertBase.h +++ b/src/jsc/bindings/Bindgen/IDLConvertBase.h @@ -52,6 +52,13 @@ struct ElementOf : ContextBase> { } +// Whether a conversion threw, from what it returned: false, null or empty. As WebCore's ConversionResult::hasException. +ALWAYS_INLINE bool hasException(JSC::ThrowScope& scope, bool converted) +{ + EXCEPTION_ASSERT_UNUSED(scope, !!scope.exception() == !converted); + return !converted; +} + // Conversion context where the name of the value being converted is specified as an // ASCIILiteral. Calls Bun::throwError. struct LiteralConversionContext : Detail::ContextBase { diff --git a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h index 13809dc499a7..ba088693c0cf 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h +++ b/src/jsc/bindings/Bindgen/IDLConvertIntegerRange.h @@ -18,31 +18,41 @@ struct IDLValidateInteger : WebCore::IDLType {}; template struct WebCore::Converter> : WebCore::DefaultConverter> { - static T convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + // False if it threw. + static bool convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, T& result) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); double number = value.toNumber(&globalObject); - RETURN_IF_EXCEPTION(scope, 0); - RELEASE_AND_RETURN(scope, static_cast(WebCore::enforceRange(globalObject, number, Min, Max))); + RETURN_IF_EXCEPTION(scope, false); + result = static_cast(WebCore::enforceRange(globalObject, number, Min, Max)); + RETURN_IF_EXCEPTION(scope, false); + return true; } }; template struct WebCore::Converter> : WebCore::DefaultConverter> { - // Inlined so that an int32 in range costs its tests and nothing else. - static T convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, WTF::ASCIILiteral name) + // False if it threw. Inlined so that an int32 in range costs its tests and nothing else. + static bool convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, WTF::ASCIILiteral name, T& result) { if (value.isInt32()) [[likely]] { int64_t number = value.asInt32(); - if (number >= static_cast(Min) && number <= static_cast(Max)) [[likely]] - return static_cast(number); + if (number >= static_cast(Min) && number <= static_cast(Max)) [[likely]] { + result = static_cast(number); + return true; + } } - return convertSlow(globalObject, value, name); + auto converted = convertSlow(globalObject, value, name); + if (!converted) + return false; + result = *converted; + return true; } private: - NEVER_INLINE static T convertSlow(JSC::JSGlobalObject& globalObject, JSC::JSValue value, WTF::ASCIILiteral name) + // Does not take `result`, which can then live in registers. + NEVER_INLINE static std::optional convertSlow(JSC::JSGlobalObject& globalObject, JSC::JSValue value, WTF::ASCIILiteral name) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); @@ -51,6 +61,7 @@ struct WebCore::Converter> : WebCo static_assert(sizeof(T) <= sizeof(Validated)); Validated result = 0; Bun::V::validateInteger(scope, &globalObject, value, name, JSC::jsNumber(Min), JSC::jsNumber(Max), &result); + RETURN_IF_EXCEPTION(scope, std::nullopt); return static_cast(result); } }; diff --git a/src/jsc/bindings/Bindgen/IDLTypes.h b/src/jsc/bindings/Bindgen/IDLTypes.h index 128a7e62f1c5..c060836c3a1d 100644 --- a/src/jsc/bindings/Bindgen/IDLTypes.h +++ b/src/jsc/bindings/Bindgen/IDLTypes.h @@ -47,7 +47,7 @@ struct IDLOtherAsNull : WebCore::IDLNullable {}; // Treats the empty value, which stands for an argument the call does not have, as null. template -struct IDLNotPassedAsNull : WebCore::IDLNullable {}; +struct IDLIfPassed : WebCore::IDLNullable {}; enum class AllowBigInt : bool { No, diff --git a/src/jsc/bindings/Bindgen/JSBytesView.cpp b/src/jsc/bindings/Bindgen/JSBytesView.cpp index 9e3f4918192e..01f1909ffc8e 100644 --- a/src/jsc/bindings/Bindgen/JSBytesView.cpp +++ b/src/jsc/bindings/Bindgen/JSBytesView.cpp @@ -65,19 +65,19 @@ bool isSharedBuffer(JSValue value) return false; } -std::span copyBytes(JSGlobalObject& globalObject, std::span bytes, MallocSpan& copy) +std::span copyBytes(JSGlobalObject& globalObject, std::span bytes, BytesCopy& copy) { if (bytes.empty()) return bytes; // Not a Vector, which holds less than a buffer can. copy = MallocSpan::tryMalloc(bytes.size()); - if (!copy) [[unlikely]] { + if (!*copy) [[unlikely]] { auto scope = DECLARE_THROW_SCOPE(getVM(&globalObject)); throwOutOfMemoryError(&globalObject, scope); return bytes; } - memcpySpan(copy.mutableSpan(), bytes); - return copy.span(); + memcpySpan(copy->mutableSpan(), bytes); + return copy->span(); } } diff --git a/src/jsc/bindings/Bindgen/JSBytesView.h b/src/jsc/bindings/Bindgen/JSBytesView.h index 383d894c0928..8fe4be820c8d 100644 --- a/src/jsc/bindings/Bindgen/JSBytesView.h +++ b/src/jsc/bindings/Bindgen/JSBytesView.h @@ -12,8 +12,11 @@ std::span bytesOfBlobOrBuffer(JSC::JSValue, bool& isShared); bool isSharedBuffer(JSC::JSValue); +// Optional: ~MallocSpan calls free, of null too. +using BytesCopy = std::optional>; + // A copy of the bytes, which `copy` holds, or the bytes themselves if there are none. Allocating it can throw. -std::span copyBytes(JSC::JSGlobalObject&, std::span, WTF::MallocSpan& copy); +std::span copyBytes(JSC::JSGlobalObject&, std::span, BytesCopy& copy); // Whether it holds a Blob: a Blob, or a `Bun.build()` output. Throws for one whose bytes are not in memory: a file. bool checkInMemoryBlob(JSC::JSGlobalObject&, JSC::JSValue); @@ -41,7 +44,7 @@ class JSBlobOrBufferView { } // The bytes of a shared buffer become those of `copy`. Whether it is one: copying can throw. - ALWAYS_INLINE bool copyIfShared(JSC::JSGlobalObject& globalObject, JSC::JSValue argument, WTF::MallocSpan& copy) + ALWAYS_INLINE bool copyIfShared(JSC::JSGlobalObject& globalObject, JSC::JSValue argument, BytesCopy& copy) { if (!m_isBlobOrBuffer || !isSharedBuffer(argument)) [[likely]] return false; @@ -50,7 +53,7 @@ class JSBlobOrBufferView { } // `read` and `copyIfShared` in one look at the buffer. - ALWAYS_INLINE bool readCopyingShared(JSC::JSGlobalObject& globalObject, JSC::JSValue argument, WTF::MallocSpan& copy) + ALWAYS_INLINE bool readCopyingShared(JSC::JSGlobalObject& globalObject, JSC::JSValue argument, BytesCopy& copy) { if (!m_isBlobOrBuffer) return false; @@ -79,9 +82,9 @@ class JSBytesView : public JSBlobOrBufferView { // Not `= default`: see JSStringUTF8View. JSBytesView() {} - void initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) + bool initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) { - m_string.initialize(globalObject, string); + return m_string.initialize(globalObject, string); } std::span bytes() const LIFETIME_BOUND @@ -98,10 +101,14 @@ class JSBlobOrStringOrBuffer { WTF_FORBID_HEAP_ALLOCATION; public: - // Resolving a rope can throw. - void initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) + // False if it threw, which resolving a rope can. + bool initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); m_string = string->value(&globalObject); + RETURN_IF_EXCEPTION(scope, false); + return true; } void initializeAsBlobOrBuffer() {} diff --git a/src/jsc/bindings/Bindgen/JSStringUTF8View.h b/src/jsc/bindings/Bindgen/JSStringUTF8View.h index bd34cee959f5..6f3d7b2bb062 100644 --- a/src/jsc/bindings/Bindgen/JSStringUTF8View.h +++ b/src/jsc/bindings/Bindgen/JSStringUTF8View.h @@ -18,19 +18,21 @@ class JSStringUTF8View { // Not `= default`: value-initialization would zero the buffer first. JSStringUTF8View() {} - // It is large, so it is filled in where it lives. Resolving a rope and converting can throw. - void initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) + // It is large, so it is filled in where it lives. False if it threw, which resolving a rope and converting can. + bool initialize(JSC::JSGlobalObject& globalObject, JSC::JSString* string) { ASSERT(m_isNull); auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); m_view = string->view(&globalObject); - RETURN_IF_EXCEPTION(scope, void()); + RETURN_IF_EXCEPTION(scope, false); m_isNull = false; if (m_view->is8Bit() && WTF::charactersAreAllASCII(m_view->span8())) [[likely]] - return; - if (!convert()) [[unlikely]] - JSC::throwOutOfMemoryError(&globalObject, scope); + return true; + if (convert()) [[likely]] + return true; + JSC::throwOutOfMemoryError(&globalObject, scope); + return false; } void initialize(WTF::ASCIILiteral literal) diff --git a/src/jsc/bindings/BunIDLConvertBase.h b/src/jsc/bindings/BunIDLConvertBase.h index 1c5ab8bb478a..dfaafc608f2e 100644 --- a/src/jsc/bindings/BunIDLConvertBase.h +++ b/src/jsc/bindings/BunIDLConvertBase.h @@ -20,6 +20,22 @@ ALWAYS_INLINE typename WebCore::Converter::ReturnType convertIDL( } } +// False if it threw. A converter that has `convertInto` says so itself, as WebCore's ConversionResult does. After another one the VM is asked. +template +ALWAYS_INLINE bool convertIDLInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Result& result) +{ + if constexpr (requires { WebCore::Converter::convertInto(globalObject, value, ctx, result); }) { + return WebCore::Converter::convertInto(globalObject, value, ctx, result); + } else { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + result = convertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, false); + return true; + } +} + +// std::nullopt if it refused the value or threw. template ALWAYS_INLINE std::optional::ReturnType> tryConvertIDL( JSC::JSGlobalObject& globalObject, @@ -41,25 +57,34 @@ struct DefaultTryConverter : DefaultContextConverter { using typename DefaultContextConverter::ReturnType; // Inlined so that a value of the right type costs its test and nothing else. - template - ALWAYS_INLINE static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + template + ALWAYS_INLINE static bool convertInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Result& result) { auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - auto result = WebCore::Converter::tryConvert(globalObject, value, ctx); - if (result.has_value()) [[likely]] { + auto converted = WebCore::Converter::tryConvert(globalObject, value, ctx); + if (converted.has_value()) [[likely]] { scope.release(); - return WTF::move(*result); + result = WTF::move(*converted); + return true; } - RETURN_IF_EXCEPTION(scope, {}); throwConversionFailedSlow(globalObject, scope, ctx); - return ReturnType {}; + return false; + } + + template + ALWAYS_INLINE static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + ReturnType result {}; + convertInto(globalObject, value, ctx, result); + return result; } private: template NEVER_INLINE static void throwConversionFailedSlow(JSC::JSGlobalObject& globalObject, JSC::ThrowScope& scope, Ctx& ctx) { + RETURN_IF_EXCEPTION(scope, void()); WebCore::Converter::throwConversionFailed(globalObject, scope, ctx); } }; diff --git a/src/jsc/host_fn.rs b/src/jsc/host_fn.rs index 9a146c03a05d..bf24d160dea6 100644 --- a/src/jsc/host_fn.rs +++ b/src/jsc/host_fn.rs @@ -129,58 +129,26 @@ fn debug_exception_assertion(global_this: &JSGlobalObject, value: JSValue, func: // // The thunks of `generate-host-exports.ts` and `generate-jssink.ts` call these. -/// Normalize a host-fn body's return type to `JsResult`. -pub trait IntoHostFnReturn { - fn into_host_fn_return(self) -> JsResult; -} -impl IntoHostFnReturn for JSValue { - #[inline] - fn into_host_fn_return(self) -> JsResult { - Ok(self) - } -} -impl IntoHostFnReturn for JsResult { - #[inline] - fn into_host_fn_return(self) -> JsResult { - self - } -} - /// Runs the body in an `ExceptionValidationScope`. #[track_caller] #[inline] -pub fn host_fn_result( - global: &JSGlobalObject, - f: impl FnOnce() -> R, -) -> JSValue { - to_js_host_call(global, || f().into_host_fn_return()) +pub fn host_fn_result(global: &JSGlobalObject, f: impl FnOnce() -> JsResult) -> JSValue { + to_js_host_call(global, f) } // ──────────────────────── safe codegen-thunk entry points ──────────────────────── -/// `fn(&JSGlobalObject, &CallFrame) -> R`. +/// `fn(&JSGlobalObject, &CallFrame) -> JsResult`. #[track_caller] #[inline] -pub fn host_fn_static( +pub fn host_fn_static( global: &JSGlobalObject, callframe: &CallFrame, - f: impl FnOnce(&JSGlobalObject, &CallFrame) -> R, + f: impl FnOnce(&JSGlobalObject, &CallFrame) -> JsResult, ) -> JSValue { host_fn_result(global, || f(global, callframe)) } -/// Lazy property creator / free getter: `fn(&JSGlobalObject) -> R`. Used by -/// `generate-host-exports.ts` for the `BunObject__createX` / `Process__getX` -/// shape (no `CallFrame`, no `this`). -#[track_caller] -#[inline] -pub fn host_fn_lazy( - global: &JSGlobalObject, - f: impl FnOnce(&JSGlobalObject) -> R, -) -> JSValue { - host_fn_result(global, || f(global)) -} - /// Lazy property creator, no exception scope (bare-`JSValue` impls). #[inline] pub fn host_fn_lazy_passthrough( diff --git a/src/runtime/api/BunObject.bind.ts b/src/runtime/api/BunObject.bind.ts index 2645650ec419..aca7bc2c9e88 100644 --- a/src/runtime/api/BunObject.bind.ts +++ b/src/runtime/api/BunObject.bind.ts @@ -105,16 +105,15 @@ export const InspectOptions = b.dictionary( export const InspectOptionsOrDepth = b.union({ options: InspectOptions, depth: b.f64, ignored: b.any }); /** - * `(value, options)` or `(value, depth, colors)`. `Bun.inspect()` is `""` and `Bun.inspect(undefined)` is + * `(arg, options)` or `(arg, depth, colors)`. `Bun.inspect()` is `""` and `Bun.inspect(undefined)` is * `"undefined"`. */ export const inspect = b.fn({ export: "Bun__BunObject__inspect", - args: { value: b.any.ifPassed, options: InspectOptionsOrDepth, colors: b.bool.loose.nullable }, + args: { arg: b.any.ifPassed, options: InspectOptionsOrDepth, colors: b.bool.loose.nullable }, returns: b.string.utf8, capability: "js", throws: true, - length: 2, }); export const MMapOptions = b.dictionary( @@ -153,18 +152,16 @@ export const openInEditor = b.fn({ }, capability: "heap", throws: true, - length: 1, }); -/** Without a `specifier` or a `from`, `resolveSync` throws and `resolve` rejects. */ -const resolver = { - args: { specifier: b.string.loose.nullable, from: b.string.loose.nullable, esm: b.bool.default(true) }, +/** Without a `moduleId` or a `parent`, `resolveSync` throws and `resolve` rejects. */ +const resolveSignature = { + args: { moduleId: b.string.loose.nullable, parent: b.string.loose.nullable, esm: b.bool.default(true) }, capability: "js", throws: true, - length: 1, } as const; -export const resolve = b.fn({ ...resolver, returns: b.any, rejects: true }); -export const resolveSync = b.fn({ ...resolver, returns: b.string }); +export const resolve = b.fn({ ...resolveSignature, returns: b.any, rejects: true }); +export const resolveSync = b.fn({ ...resolveSignature, returns: b.string }); export const serve = b.fn({ args: { options: b.any }, returns: b.any, capability: "js", throws: true }); @@ -185,17 +182,21 @@ export const sleepSync = b.fn({ /** The table of what is not an object is `""`. */ const tabularData = b.object.otherAsNull; -const printer = { export: "Bun__BunObject__table", returns: b.string.utf8, capability: "js", throws: true } as const; +const tableSignature = { + export: "Bun__BunObject__table", + returns: b.string.utf8, + capability: "js", + throws: true, +} as const; /** `Bun.inspect.table` */ export const table = b.overloads( { - ...printer, + ...tableSignature, args: { tabularData, properties: b.jsArray, options: InspectOptions.otherAsNull }, - length: 3, rustPath: "crate::api::bun_object::table_with_properties", }, { - ...printer, + ...tableSignature, args: { tabularData, options: InspectOptions.otherAsNull }, rustPath: "crate::api::bun_object::table", }, @@ -217,7 +218,6 @@ export const which = b.fn({ returns: b.string.local.nullable, capability: "heap", throws: true, - length: 1, }); export const ZstdCompressionOptions = b.dictionary({ level: b.f64.loose.allowBigInt.optional }, { allowArray: true }); diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs index bb5760e52d96..773c479c69f8 100644 --- a/src/runtime/api/BunObject.rs +++ b/src/runtime/api/BunObject.rs @@ -431,12 +431,12 @@ fn print_table( pub(crate) fn inspect<'a>( cx: &mut JsScope<'a>, - value: Option>, + arg: Option>, options: InspectOptionsOrDepth<'a>, colors: Option, ) -> JsResult> { let global_this = cx.global(); - let Some(value) = value else { + let Some(value) = arg else { return Ok(Vec::new()); }; let value = value.to_js(); @@ -872,32 +872,32 @@ fn resolve_with_args( pub(crate) fn resolve_sync( cx: &mut JsScope<'_>, - specifier: Option, - from: Option, + module_id: Option, + parent: Option, esm: bool, ) -> JsResult { let global_this = cx.global(); - let Some(specifier) = specifier else { - return Err(global_this.throw_invalid_arguments(format_args!("specifier must be a string"))); + let Some(module_id) = module_id else { + return Err(global_this.throw_invalid_arguments(format_args!("moduleId must be a string"))); }; - let Some(from) = from else { - return Err(global_this.throw_invalid_arguments(format_args!("from must be a string"))); + let Some(parent) = parent else { + return Err(global_this.throw_invalid_arguments(format_args!("parent must be a string"))); }; let mode = if esm { ResolveMode::Esm } else { ResolveMode::Require }; - do_resolve_with_args::(global_this, &specifier, &from, mode) + do_resolve_with_args::(global_this, &module_id, &parent, mode) } pub(crate) fn resolve( cx: &mut JsScope<'_>, - specifier: Option, - from: Option, + module_id: Option, + parent: Option, esm: bool, ) -> JsResult { - let resolved = resolve_sync(cx, specifier, from, esm)?; + let resolved = resolve_sync(cx, module_id, parent, esm)?; let global_object = cx.global(); Ok(JSPromise::resolved_promise_value( global_object, diff --git a/src/runtime/api/BunObjectInternals.bind.ts b/src/runtime/api/BunObjectInternals.bind.ts index fe823a24a6f6..60ef3fc9dd9b 100644 --- a/src/runtime/api/BunObjectInternals.bind.ts +++ b/src/runtime/api/BunObjectInternals.bind.ts @@ -7,5 +7,4 @@ export const registerMacro = b.fn({ args: { id: b.f64, callback: b.any }, capability: "heap", throws: true, - length: 1, }); diff --git a/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts b/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts index 06e62d4c4116..7ec70f95db63 100644 --- a/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts +++ b/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts @@ -8,7 +8,6 @@ const spawner = { returns: b.any, capability: "js", throws: true, - length: 1, } as const; export const spawn = b.fn(spawner); export const spawnSync = b.fn(spawner); diff --git a/src/runtime/api/cron.bind.ts b/src/runtime/api/cron.bind.ts index 108a12a88664..440b36c400f2 100644 --- a/src/runtime/api/cron.bind.ts +++ b/src/runtime/api/cron.bind.ts @@ -27,7 +27,10 @@ const reaction = { export const onPromiseResolve = b.fn({ ...reaction, export: "Bun__CronJob__onPromiseResolve" }); export const onPromiseReject = b.fn({ ...reaction, export: "Bun__CronJob__onPromiseReject" }); -export const CronOptions = b.dictionary({ tz: b.string.utf8.nullable }, { allowArray: true, reportedAs: "options" }); +export const CronOptions = b.dictionary( + { tz: b.string.utf8.nullable }, + { allowArray: true, reportedAs: "Bun.cron: options" }, +); const signature = { export: "Bun__Cron__cron", capability: "js", throws: true } as const; @@ -42,7 +45,6 @@ export const cron = b.overloads( title: "Bun.cron() expects a string title as the third argument", }, returns: b.any, - length: 3, rustPath: "crate::api::cron::register_os_job", }, /** An in-process job, whose `cron` is the `schedule` it was given. */ @@ -52,7 +54,6 @@ export const cron = b.overloads( expected: { schedule: "Bun.cron() expects a string cron expression", handler: "Bun.cron(schedule, handler) expects a function handler as the second argument", - options: "Bun.cron: options must be an object", }, returns: CronJob, rustPath: "crate::api::cron::CronJob::register", @@ -70,14 +71,12 @@ export const remove = b.fn({ export const parse = b.fn({ export: "Bun__Cron__parse", - args: { expression: b.string.utf8, from: b.f64.allowDate.nullable, options: CronOptions.nullable }, + args: { expression: b.string.utf8, relativeDate: b.f64.allowDate.nullable, options: CronOptions.nullable }, expected: { expression: "Bun.cron.parse() expects a string cron expression as the first argument", - from: "Bun.cron.parse() expects the second argument to be a Date or number (ms since epoch)", - options: "Bun.cron: options must be an object", + relativeDate: "Bun.cron.parse() expects the second argument to be a Date or number (ms since epoch)", }, returns: b.f64.asDate.nullable, capability: "js", throws: true, - length: 1, }); diff --git a/src/runtime/api/cron.rs b/src/runtime/api/cron.rs index b07e0a093077..f634888d4518 100644 --- a/src/runtime/api/cron.rs +++ b/src/runtime/api/cron.rs @@ -1866,7 +1866,7 @@ pub(crate) fn get_cron_object(global_this: &JSGlobalObject, _obj: &JSObject) -> pub(crate) fn parse( cx: &mut JsScope<'_>, expression: &[u8], - from: Option, + relative_date: Option, options: Option<&CronOptions<'_>>, ) -> JsResult> { let global = cx.global(); @@ -1881,9 +1881,9 @@ pub(crate) fn parse( } }; - let from_ms = from.unwrap_or_else(|| bun_core::time::milli_timestamp() as f64); + let from_ms = relative_date.unwrap_or_else(|| bun_core::time::milli_timestamp() as f64); - // An out-of-range `from` is an invalid argument. next() would report it as "no match". + // An out-of-range `relative_date` is an invalid argument. next() would report it as "no match". if from_ms.is_nan() || from_ms.abs() > jsc::wtf::MAX_ECMASCRIPT_TIME { return Err(global.throw_invalid_arguments(format_args!("Invalid date value"))); } diff --git a/src/runtime/ffi/ffi.bind.ts b/src/runtime/ffi/ffi.bind.ts index fab94f6a639c..dcfe856e8875 100644 --- a/src/runtime/ffi/ffi.bind.ts +++ b/src/runtime/ffi/ffi.bind.ts @@ -11,7 +11,7 @@ export const FFI = b.class({ }, }); -const method = { returns: b.any, length: 1, capability: "js", throws: true } as const; +const method = { returns: b.any, capability: "js", throws: true } as const; // The functions of `Bun.FFI`. One that is given bad options returns the error. export const viewSource = b.fn({ diff --git a/src/runtime/ipc_host.bind.ts b/src/runtime/ipc_host.bind.ts index 99e5c1e0374e..87ca5af2aa49 100644 --- a/src/runtime/ipc_host.bind.ts +++ b/src/runtime/ipc_host.bind.ts @@ -28,7 +28,6 @@ export const emitHandleIPCMessage = b.fn({ export const send = b.fn({ args: { message: b.any, handle: b.any, options: b.any, callback: b.any }, returns: b.bool, - length: 1, capability: "js", throws: true, }); diff --git a/src/runtime/node/node_net_binding.bind.ts b/src/runtime/node/node_net_binding.bind.ts index bb937f92b855..afce1bbb7d30 100644 --- a/src/runtime/node/node_net_binding.bind.ts +++ b/src/runtime/node/node_net_binding.bind.ts @@ -19,7 +19,7 @@ export const setDefaultAutoSelectFamilyAttemptTimeout = b.fn({ export const newDetachedSocket = b.fn({ args: { isSSL: b.bool }, returns: b.any, capability: "js" }); export const doConnect = b.fn({ - args: { prev: SocketHandle, opts: b.any }, + args: { socket: SocketHandle, options: b.any }, returns: b.any, capability: "js", throws: true, diff --git a/src/runtime/node/node_net_binding.rs b/src/runtime/node/node_net_binding.rs index 820142f6d851..d1b34655e479 100644 --- a/src/runtime/node/node_net_binding.rs +++ b/src/runtime/node/node_net_binding.rs @@ -89,13 +89,13 @@ pub(crate) fn new_detached_socket(cx: &mut JsScope<'_>, is_ssl: bool) -> JSValue pub(crate) fn do_connect<'a>( cx: &mut JsScope<'a>, - prev: SocketHandle<'a>, - opts: JsLocal<'a>, + socket: SocketHandle<'a>, + options: JsLocal<'a>, ) -> JsResult { - let (maybe_tcp, maybe_tls) = match prev { + let (maybe_tcp, maybe_tls) = match socket { SocketHandle::Tcp(socket) => (Some(socket.as_ctx_ptr()), None), SocketHandle::Tls(socket) => (None, Some(socket.as_ctx_ptr())), SocketHandle::Ignored(_) => (None, None), }; - Listener::connect_inner(&cx.js_thread(), maybe_tcp, maybe_tls, opts.to_js()) + Listener::connect_inner(&cx.js_thread(), maybe_tcp, maybe_tls, options.to_js()) } diff --git a/src/runtime/socket/UpgradedDuplex.bind.ts b/src/runtime/socket/UpgradedDuplex.bind.ts index 6a80e011e984..d34e5023d9d2 100644 --- a/src/runtime/socket/UpgradedDuplex.bind.ts +++ b/src/runtime/socket/UpgradedDuplex.bind.ts @@ -6,12 +6,11 @@ export default b.module({ rustPath: "crate::socket::upgraded_duplex" }); const listener = { passCallee: true, capability: "js" } as const; /** What is not a buffer is reported to the socket's `error` handler, not thrown. */ -export const DuplexChunk = b.union({ nothing: b.null, bytes: b.bufferSource.allowShared, other: b.any }); +export const DuplexChunk = b.union({ none: b.null, bytes: b.bufferSource.allowShared, other: b.any }); export const onReceivedData = b.fn({ ...listener, export: "Bun__UpgradedDuplex__onReceivedData", args: { data: DuplexChunk }, - length: 0, }); export const onEnd = b.fn({ ...listener, export: "Bun__UpgradedDuplex__onEnd" }); export const onWritable = b.fn({ ...listener, export: "Bun__UpgradedDuplex__onWritable" }); diff --git a/src/runtime/socket/UpgradedDuplex.rs b/src/runtime/socket/UpgradedDuplex.rs index 582b00308e62..8acbcdfa46f7 100644 --- a/src/runtime/socket/UpgradedDuplex.rs +++ b/src/runtime/socket/UpgradedDuplex.rs @@ -806,7 +806,7 @@ pub(crate) fn on_received_data<'a>( let _keep_alive = this.keep_alive(); if !this.origin.get().is_empty() { match data { - DuplexChunk::Nothing => {} + DuplexChunk::None => {} // The bytes are copied before a handler of the socket runs. DuplexChunk::Bytes(buffer) => buffer.with_bytes(&cx.heap, |payload| { this.on_internal_receive_data(payload); diff --git a/src/runtime/test_runner/BunTest.bind.ts b/src/runtime/test_runner/BunTest.bind.ts index 800c081aa7ce..a3a8a92e818c 100644 --- a/src/runtime/test_runner/BunTest.bind.ts +++ b/src/runtime/test_runner/BunTest.bind.ts @@ -16,7 +16,6 @@ const hook = (name: string) => second: `${name}() expects a number or object as the second argument`, third: `${name}() expects a number or object as the third argument`, }, - length: 1, ...js, }); export const beforeAll = hook("beforeAll"); diff --git a/src/runtime/test_runner/ScopeFunctions.bind.ts b/src/runtime/test_runner/ScopeFunctions.bind.ts index 87e08782fdd4..85d18b6904af 100644 --- a/src/runtime/test_runner/ScopeFunctions.bind.ts +++ b/src/runtime/test_runner/ScopeFunctions.bind.ts @@ -10,13 +10,13 @@ export const BunTestOptions = b.dictionary( /** A function here is the callback if none follows it, and else what the test is named after. */ export const DescriptionOrCallback = b.union({ - nothing: b.null, + none: b.null, function: b.callable, text: b.string.utf8.owned, number: b.f64, }); export const CallbackOrOptions = b.union({ - nothing: b.null, + none: b.null, callback: b.callable, timeout: b.f64, options: BunTestOptions, @@ -35,7 +35,6 @@ export const callAsFunction = b.fn({ second: "The second argument must be a function, number, or object", third: "The third argument must be a function, number, or object", }, - length: 1, capability: "js", throws: true, }); diff --git a/src/runtime/test_runner/ScopeFunctions.rs b/src/runtime/test_runner/ScopeFunctions.rs index c0fbed68a06b..cc895c21700a 100644 --- a/src/runtime/test_runner/ScopeFunctions.rs +++ b/src/runtime/test_runner/ScopeFunctions.rs @@ -547,21 +547,21 @@ pub(crate) fn parse_arguments<'a>( // callback, options ( DescriptionOrCallback::Function(callback), - options @ (CallbackOrOptions::Nothing | CallbackOrOptions::Timeout(_) | CallbackOrOptions::Options(_)), - CallbackOrOptions::Nothing, - ) => (DescriptionOrCallback::Nothing, CallbackOrOptions::Callback(callback), options), + options @ (CallbackOrOptions::None | CallbackOrOptions::Timeout(_) | CallbackOrOptions::Options(_)), + CallbackOrOptions::None, + ) => (DescriptionOrCallback::None, CallbackOrOptions::Callback(callback), options), // description // description, callback // description, callback, options (description, callback @ CallbackOrOptions::Callback(_), options) - | (description, callback, options @ CallbackOrOptions::Nothing) => (description, callback, options), + | (description, callback, options @ CallbackOrOptions::None) => (description, callback, options), // description, options, callback (description, options, callback) => (description, callback, options), }; let result_callback: Option = match callback { CallbackOrOptions::Callback(callback) => Some(callback.to_js().with_async_context_if_needed(global)), - CallbackOrOptions::Nothing if cfg.callback != CallbackMode::Require => None, + CallbackOrOptions::None if cfg.callback != CallbackMode::Require => None, _ => { let ordinal = if cfg.kind == FunctionKind::Hook { "first" } else { "second" }; return Err(global.throw(format_args!("{} expects a function as the {} argument", signature, ordinal))); @@ -577,7 +577,7 @@ pub(crate) fn parse_arguments<'a>( let mut timeout_option: Option = None; match options { - CallbackOrOptions::Nothing => {} + CallbackOrOptions::None => {} CallbackOrOptions::Timeout(timeout) => timeout_option = Some(timeout), CallbackOrOptions::Callback(_) => { return Err(global.throw(format_args!( @@ -599,7 +599,7 @@ pub(crate) fn parse_arguments<'a>( } result.description = match description { - DescriptionOrCallback::Nothing => None, + DescriptionOrCallback::None => None, DescriptionOrCallback::Function(callable) => Some(name_of_callable(global, callable, signature)?), DescriptionOrCallback::Text(text) => Some(text.into_vec()), DescriptionOrCallback::Number(number) => Some(bun_core::fmt::FormatDouble::dtoa(&mut [0; 124], number).to_vec()), diff --git a/src/runtime/test_runner/timers/FakeTimers.bind.ts b/src/runtime/test_runner/timers/FakeTimers.bind.ts index 67f88fad2c9d..224a18d117d6 100644 --- a/src/runtime/test_runner/timers/FakeTimers.bind.ts +++ b/src/runtime/test_runner/timers/FakeTimers.bind.ts @@ -10,7 +10,10 @@ export const FakeTimersOptions = b.dictionary( { allowArray: true, reportedAs: "options" }, ); /** Jest 26 took `"modern"` or `"legacy"`. The string is ignored. */ -export const FakeTimersImplementationOrOptions = b.union({ implementation: b.string.utf8, options: FakeTimersOptions }); +export const FakeTimersImplementationOrOptions = b.union({ + implementation: b.string.allowStringObject, + options: FakeTimersOptions, +}); export const useFakeTimers = b.fn({ ...chainable, diff --git a/src/runtime/test_runner/timers/FakeTimers.rs b/src/runtime/test_runner/timers/FakeTimers.rs index 6e617c88bddf..d7ee6e2ac6e3 100644 --- a/src/runtime/test_runner/timers/FakeTimers.rs +++ b/src/runtime/test_runner/timers/FakeTimers.rs @@ -341,7 +341,7 @@ fn set_fake_timer_marker(global: &JSGlobalObject, enabled: bool) -> JsResult<()> pub(crate) fn use_fake_timers( cx: &mut JsScope<'_>, this_value: JsLocal<'_>, - options: Option>, + options: Option, ) -> JsResult { let global = cx.global(); // SAFETY: FFI call into C++ JSMock diff --git a/src/runtime/webcore/Blob.bind.ts b/src/runtime/webcore/Blob.bind.ts index 5e16b8ab6db0..6203f9f19d63 100644 --- a/src/runtime/webcore/Blob.bind.ts +++ b/src/runtime/webcore/Blob.bind.ts @@ -50,7 +50,7 @@ export const BunFileOptions = b.dictionary( { allowArray: true, nonObjectAsEmpty: true, fromJS: true }, ); /** `options` is also what an S3 file takes. */ -export const file = b.fn({ ...js, args: { path: b.any, options: b.any }, returns: Blob, length: 1 }); +export const file = b.fn({ ...js, args: { path: b.any, options: b.any }, returns: Blob }); /** What `Bun.write` itself reads of its options. */ export const BunWriteOptions = b.dictionary( @@ -58,4 +58,4 @@ export const BunWriteOptions = b.dictionary( { allowArray: true, fromJS: true, reportedAs: "options" }, ); /** `options` is also what a write to S3 takes. */ -export const write = b.fn({ ...method, args: { destination: b.any, data: b.any, options: b.any }, length: 1 }); +export const write = b.fn({ ...method, args: { destination: b.any, data: b.any, options: b.any } }); diff --git a/src/runtime/webcore/Crypto.bind.ts b/src/runtime/webcore/Crypto.bind.ts index e71f99860e88..2acca475b9a2 100644 --- a/src/runtime/webcore/Crypto.bind.ts +++ b/src/runtime/webcore/Crypto.bind.ts @@ -37,7 +37,6 @@ export const randomUUIDv7 = b.fn({ returns: EncodedUUID, capability: "heap", throws: true, - length: 2, rustPath: "crate::webcore::crypto::random_uuid_v7", }); diff --git a/src/runtime/webcore/ObjectURLRegistry.bind.ts b/src/runtime/webcore/ObjectURLRegistry.bind.ts index 855533533794..4e2a90991f82 100644 --- a/src/runtime/webcore/ObjectURLRegistry.bind.ts +++ b/src/runtime/webcore/ObjectURLRegistry.bind.ts @@ -11,7 +11,6 @@ export const revokeObjectURL = b.fn({ expected: { url: "revokeObjectURL expects a string" }, capability: "heap", throws: true, - length: 1, }); /** `resolveObjectURL` of `node:buffer`. */ diff --git a/src/runtime/webcore/fetch.bind.ts b/src/runtime/webcore/fetch.bind.ts index 1b9ce0352974..8c469ba2d30d 100644 --- a/src/runtime/webcore/fetch.bind.ts +++ b/src/runtime/webcore/fetch.bind.ts @@ -4,7 +4,7 @@ export default b.module({ rustPath: "crate::webcore::fetch" }); const js = { capability: "js", throws: true } as const; /** An `input` that is neither a `Request` nor, after `ToString`, a URL is read as `init` is. */ -const request = { args: { input: b.any, init: b.any }, returns: b.any, ...js, rejects: true, length: 1 } as const; +const request = { args: { input: b.any, init: b.any }, returns: b.any, ...js, rejects: true } as const; export const fetch = b.fn(request); /** What `session.fetch` of a `Bun.FetchSession` is bound from. */ diff --git a/src/runtime/webcore/prompt.bind.ts b/src/runtime/webcore/prompt.bind.ts index 9846fcb8a56a..f0c263832d8b 100644 --- a/src/runtime/webcore/prompt.bind.ts +++ b/src/runtime/webcore/prompt.bind.ts @@ -5,13 +5,12 @@ export default b.module({ rustPath: "crate::webcore::prompt" }); /** `alert()` shows a message of its own. `alert(undefined)` shows "undefined". */ const message = b.string.loose.utf8.ifPassed; -export const alert = b.fn({ args: { message }, length: 1 }); -export const confirm = b.fn({ args: { message }, returns: b.bool, length: 1 }); +export const alert = b.fn({ args: { message } }); +export const confirm = b.fn({ args: { message }, returns: b.bool }); /** An empty answer returns `defaultValue` as it is. */ export const prompt = b.fn({ args: { message, defaultValue: b.any.ifPassed }, returns: b.any, capability: "js", throws: true, - length: 1, }); diff --git a/src/sql_jsc/mysql.bind.ts b/src/sql_jsc/mysql.bind.ts index d08e0a0a0332..bb3b3272fff2 100644 --- a/src/sql_jsc/mysql.bind.ts +++ b/src/sql_jsc/mysql.bind.ts @@ -31,7 +31,7 @@ export const binding = b.namespace({ ...js, args: { hostname: text, - port: b.i32, + port: b.f64, username: text, password: text, database: text, diff --git a/src/sql_jsc/mysql/JSMySQLConnection.rs b/src/sql_jsc/mysql/JSMySQLConnection.rs index 55650fefae58..191a49c8c23c 100644 --- a/src/sql_jsc/mysql/JSMySQLConnection.rs +++ b/src/sql_jsc/mysql/JSMySQLConnection.rs @@ -253,7 +253,7 @@ impl JSMySQLConnection { pub(crate) fn create_connection<'a>( cx: &mut JsScope<'a>, hostname: &[u8], - port: i32, + port: f64, username: &[u8], password: &[u8], database: &[u8], @@ -275,6 +275,7 @@ impl JSMySQLConnection { ) -> JsResult { let global_object = cx.global(); let vm = global_object.bun_vm().as_mut(); + let port = port as i32; // The connection is the calling script's. let context = global_object.bun_vm().context_of_caller_no_frame(); let Some(args) = ConnectionCtorArgs::parse( diff --git a/src/sql_jsc/postgres.bind.ts b/src/sql_jsc/postgres.bind.ts index 1e03963aaa15..53e6c5ab35c5 100644 --- a/src/sql_jsc/postgres.bind.ts +++ b/src/sql_jsc/postgres.bind.ts @@ -31,7 +31,7 @@ export const binding = b.namespace({ ...js, args: { hostname: text, - port: b.i32, + port: b.f64, username: text, password: text, database: text, diff --git a/src/sql_jsc/postgres/PostgresSQLConnection.rs b/src/sql_jsc/postgres/PostgresSQLConnection.rs index e265f73999e8..71dec48f7d53 100644 --- a/src/sql_jsc/postgres/PostgresSQLConnection.rs +++ b/src/sql_jsc/postgres/PostgresSQLConnection.rs @@ -845,7 +845,7 @@ impl PostgresSQLConnection { pub(crate) fn create_connection<'a>( cx: &mut JsScope<'a>, hostname: &[u8], - port: i32, + port: f64, username: &[u8], password: &[u8], database: &[u8], @@ -868,6 +868,7 @@ pub(crate) fn create_connection<'a>( ) -> JsResult { let global_object = cx.global(); let vm = global_object.bun_vm().as_mut(); + let port = port as i32; // The connection is the calling script's. let context = global_object.bun_vm().context_of_caller_no_frame(); let Some(args) = ConnectionCtorArgs::parse( diff --git a/src/sql_jsc/shared/QueryCtorArgs.rs b/src/sql_jsc/shared/QueryCtorArgs.rs index bcb7dfc3bc83..f176b35ef5cf 100644 --- a/src/sql_jsc/shared/QueryCtorArgs.rs +++ b/src/sql_jsc/shared/QueryCtorArgs.rs @@ -1,5 +1,4 @@ -//! What both the Postgres and MySQL `createQuery(query, values, pendingValue, bigint, simple)` -//! refuse. +//! The limits of a simple query, for `createQuery` of Postgres and MySQL. use crate::jsc::{JSGlobalObject, JSValue, JsResult}; diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index f623f88936db..35a43bc56c2c 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -344,7 +344,7 @@ itAlways("mistakes in a schema are errors when it loads", () => { ], }, }), - ).toThrow("x: its overloads have one privateSymbol"); + ).toThrow("x: every overload gives privateSymbol the same value"); expect(() => b.class({ rustPath: "T", proto: { x: { rustName: "camelCase" } } })).toThrow( "camelCase: rustName is written in snake_case", ); @@ -359,7 +359,7 @@ itAlways("mistakes in a schema are errors when it loads", () => { ], }, }), - ).toThrow("x: its overloads have one invalidThis"); + ).toThrow("x: every overload gives invalidThis the same value"); expect(() => b.class({ rustPath: "T", proto: { "@@x": { getter: b.i32 } } })).toThrow( "@@x: only a function has an @@ key", ); diff --git a/test/js/bun/css/color.test.ts b/test/js/bun/css/color.test.ts index 64477fbebbc0..5484d5bba7c7 100644 --- a/test/js/bun/css/color.test.ts +++ b/test/js/bun/css/color.test.ts @@ -194,7 +194,7 @@ test("invalid format string lists the accepted values", () => { } // Must not leak the internal Rust enum name. expect(message).not.toContain("OutputColorFormat"); - expect(message).toStartWith("format must be one of "); + expect(message).toStartWith("outputFormat must be one of "); // Every accepted spelling should appear in the message, so a user can copy one. for (const ok of [ "ansi", diff --git a/test/js/bun/resolve/resolve-test.js b/test/js/bun/resolve/resolve-test.js index 3ff36505976a..11b8528be50f 100644 --- a/test/js/bun/resolve/resolve-test.js +++ b/test/js/bun/resolve/resolve-test.js @@ -122,7 +122,7 @@ it("Bun.resolve throws TypeError when called without arguments", async () => { expect.unreachable("Should have thrown"); } catch (error) { expect(error).toBeInstanceOf(TypeError); - expect(error.message).toBe("specifier must be a string"); + expect(error.message).toBe("moduleId must be a string"); expect(error.code).toBe("ERR_INVALID_ARG_TYPE"); } }); @@ -133,7 +133,7 @@ it("Bun.resolveSync throws TypeError when called without arguments", () => { expect.unreachable("Should have thrown"); } catch (error) { expect(error).toBeInstanceOf(TypeError); - expect(error.message).toBe("specifier must be a string"); + expect(error.message).toBe("moduleId must be a string"); expect(error.code).toBe("ERR_INVALID_ARG_TYPE"); } }); diff --git a/test/js/bun/test/describe.test.ts b/test/js/bun/test/describe.test.ts index 14ae382892a3..c8d9dfb83370 100644 --- a/test/js/bun/test/describe.test.ts +++ b/test/js/bun/test/describe.test.ts @@ -237,6 +237,16 @@ describe("a proxy of a function is a callback", () => { test("each ran", () => { expect(ran).toEqual(["test", "beforeAll", "beforeEach", "test with a timeout", "afterEach"]); }); + let error: Error | undefined; + try { + // @ts-expect-error + test("a proxy where the options go", proxy("options"), () => {}); + } catch (thrown) { + error = thrown as Error; + } + test("and not options", () => { + expect(error?.message).toBe("test() expects options to be a number or object, not a function"); + }); }); test("deeply nested describe() doesn't overflow the native stack", async () => { diff --git a/test/js/bun/util/socket-fault-injection.test.ts b/test/js/bun/util/socket-fault-injection.test.ts index 9be29cbd971e..511a04bbee2a 100644 --- a/test/js/bun/util/socket-fault-injection.test.ts +++ b/test/js/bun/util/socket-fault-injection.test.ts @@ -12,12 +12,14 @@ describe.skipIf(skip)("socketFaultInjection control surface", () => { test("set() validates syscall", () => { expect(() => fault.set({ syscall: "bogus" as any, action: "errno", errno: "ECONNRESET" })).toThrow( - /rule\.syscall must be/, + /^rule\.syscall must be "recv", "send", /, ); }); test("set() validates action", () => { - expect(() => fault.set({ syscall: "recv", action: "bogus" as any })).toThrow(/rule\.action must be/); + expect(() => fault.set({ syscall: "recv", action: "bogus" as any })).toThrow( + 'rule.action must be "errno", "short", "zero", "excess", or "none"', + ); }); // Only recv/send/writev have a byte count to clamp; arming "short" on any other @@ -66,7 +68,7 @@ describe.skipIf(skip)("socketFaultInjection control surface", () => { test("set() rejects unknown errno name", () => { expect(() => fault.set({ syscall: "recv", action: "errno", errno: "ENOSUCHERR" as any })).toThrow( - /rule\.errno must be/, + /^rule\.errno must be "ECONNRESET", "EPIPE", /, ); }); @@ -118,7 +120,9 @@ describe.skipIf(skip)("socketFaultInjection control surface", () => { // and then never fire. test("set() rejects syscalls that have no fault hook", () => { for (const sc of ["socket", "close", "shutdown"]) { - expect(() => fault.set({ syscall: sc as any, action: "none" })).toThrow(/rule\.syscall must be/); + expect(() => fault.set({ syscall: sc as any, action: "none" })).toThrow( + /^rule\.syscall must be "recv", "send", /, + ); } }); diff --git a/test/js/node/crypto/crypto-random.test.ts b/test/js/node/crypto/crypto-random.test.ts index 7af0df35871a..c2af19015c13 100644 --- a/test/js/node/crypto/crypto-random.test.ts +++ b/test/js/node/crypto/crypto-random.test.ts @@ -8,6 +8,7 @@ describe("arguments are validated as in Node", () => { it("a proxy of a function is a callback", async () => { const called = (call: (callback: (err: Error | null) => void) => void) => new Promise((resolve, reject) => call(new Proxy(err => (err ? reject(err) : resolve()), {}))); + await called(callback => randomBytes(4, callback)); await called(callback => randomInt(5, callback)); await called(callback => randomInt(1, 5, callback)); await called(callback => randomFill(new Uint8Array(4), callback)); diff --git a/test/js/sql/sql-options.test.ts b/test/js/sql/sql-options.test.ts index 1c37858cabcb..25b06beeb08d 100644 --- a/test/js/sql/sql-options.test.ts +++ b/test/js/sql/sql-options.test.ts @@ -795,6 +795,12 @@ for (const [label, value] of [["object", {}], ["string", "require"], ["negative" options.sslMode = value; }); } +results["port fraction"] = await attempt(options => { + options.port += 0.5; +}); +results["port NaN"] = await attempt(options => { + options.port = NaN; +}); console.log(JSON.stringify(results));`, ], env: { ...bunEnv, SQL_URL: `${adapter}://user@127.0.0.1:${port}/db` }, @@ -821,6 +827,8 @@ console.log(JSON.stringify(results));`, "sslMode negative": "ERR_OUT_OF_RANGE", "sslMode out of range": "ERR_OUT_OF_RANGE", "sslMode fraction": "ERR_OUT_OF_RANGE", + "port fraction": refused, + "port NaN": refused, }); expect(exitCode).toBe(0); }); From acb5a416834d6b22c2618c4d684abc97c9f48711 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 09:30:57 +0000 Subject: [PATCH 16/22] Refuse `b.null` outside a union, a range under a modifier in an array and `length` with `export`, and have the copy of shared bytes say that it threw --- docs/project/bindgen.mdx | 9 +- src/codegen/bindgen-emit.ts | 39 ++++----- src/codegen/bindgen-lib-internal.ts | 84 ++++++++++--------- src/codegen/generate-host-exports.ts | 10 +-- src/jsc/bindgen_test.bind.ts | 2 +- src/jsc/bindings/Bindgen/IDLConvertBase.h | 8 +- src/jsc/bindings/Bindgen/JSBytesView.cpp | 9 +- src/jsc/bindings/Bindgen/JSBytesView.h | 19 ++--- src/jsc/bindings/BunIDLConvertBase.h | 2 +- src/jsc/host_fn.rs | 9 +- .../api/bun/js_bun_spawn_bindings.bind.ts | 6 +- src/runtime/timer/Timer.rs | 2 +- test/internal/bindgen.test.ts | 4 + 13 files changed, 99 insertions(+), 104 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index cb76128eb29e..b36fb7b3bf78 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -76,10 +76,11 @@ not compile. To reach the function: - from `src/js`: `$bindgen("bindgen_test.bind.ts", "add")`, which is typed from the schema. An argument's type is what it takes, so one that coerces (`.loose`, `.wrap`, `.enforceRange()`) is `any`. A `.nullable` result, and a `.nullable` getter without a setter, is `T | null`; -- from a C++ static property table: `Bun::Bindgen::Generated::BindgenTest::jsAdd` in `GeneratedBindgenTest.h`; +- from a C++ static property table: `Bun::Bindgen::Generated::BindgenTest::jsAdd` in `GeneratedBindgenTest.h`. The table gives its `.length`, and the schema's `length` is not read; - from Rust, or from C++ that compares host functions: `export: "Bun__CronJob__onPromiseResolve"` gives the host function C linkage under that name. The generated header declares it, and in Rust it is a `JSHostFn`, - `crate::generated_bindgen::cron::Bun__CronJob__onPromiseResolve`, for `JSFunction::create` and `JSValue::then`. + `crate::generated_bindgen::cron::Bun__CronJob__onPromiseResolve`, for `JSFunction::create` and `JSValue::then`. What makes + the function gives its `.length`, so `length` is refused. `passThis: true` adds `this_value: JsLocal<'a>` after the capability, and `passCallee: true` adds `callee: JsLocal<'a>`, the function that was called, after it. @@ -196,7 +197,7 @@ arguments from left to right: same call. It keeps the candidates where the argument can be left out, or if there are none, those whose type takes every kind. 3. A candidate with no argument at that position takes only `undefined`. -4. If no candidate takes the argument, all of them stay, and the next argument decides. +4. If no candidate takes the argument, those that have an argument at that position stay, and the next argument decides. 5. When the arguments run out, the candidate written first wins. Its conversion reports whatever is wrong. @@ -256,7 +257,7 @@ These come last, in this order (`b.any` already takes `undefined` and `null`, so - `.nullable`: `null` or `undefined` means absent; `Option`. - `.falsyAsNull`: every falsy value means absent; `Option`. - `.emptyStringAsNull`: `undefined`, `null` or `""` means absent, as for `JSValue::get_truthy`; `Option`. -- `.otherAsNull`: `Option`. A value of a [kind](#unions-and-kinds) the type does not take, or that it refuses (what is not an object, a shared buffer), is `None` and nothing is thrown, as if the type were in a union with `ignored: b.any` after it. It is for an argument or a member that is ignored unless it is of the type. What is inside a value is still reported: the members of a dictionary, the elements of an array. It is not for a type that throws for a value of a kind it takes: an integer for `1.5` (`b.f64.otherAsNull` takes every number), an enumeration for an unknown string (`b.string.allowStringObject.otherAsNull` takes every string), a dictionary without `allowArray` for an array. Nor is it for a type that takes every kind, like a `.loose` one or a `nonObjectAsEmpty` dictionary. A union writes `ignored: b.any` as its last alternative. +- `.otherAsNull`: a value of a [kind](#unions-and-kinds) the type does not take, or that it refuses (what is not an object, a shared buffer), means absent and nothing is thrown, as if the type were in a union with `ignored: b.any` after it; `Option`. It is for an argument or a member that is ignored unless it is of the type. What is inside a value is still reported: the members of a dictionary, the elements of an array. It is not for a type that throws for a value of a kind it takes: an integer for `1.5` (`b.f64.otherAsNull` takes every number), an enumeration for an unknown string (`b.string.allowStringObject.otherAsNull` takes every string), a dictionary without `allowArray` for an array. Nor is it for a type that takes every kind, like a `.loose` one or a `nonObjectAsEmpty` dictionary. A union writes `ignored: b.any` as its last alternative. - `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` differ: both work, like `alert`, or a test of Node pins an error for each, like `URL.revokeObjectURL`. It also goes on a type that takes `undefined`, like `b.any`. It is only for an argument, and not for one of an overload. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 7e9116afc09b..50c51310461d 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -443,35 +443,29 @@ extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${origin.stem}_${names `; } -/** C++ that returns the variant `string` spells, of `candidates` of its length: the characters that tell them apart leave one to compare. */ +/** The body of a `case`: returns the variant of the candidate that `string` equals. All have its length, so it switches on characters where they differ and compares the one left. */ function cppMatchSpelling(candidates: readonly (readonly [spelling: string, variant: string])[]): string { if (candidates.length === 1) { const [[spelling, variant]] = candidates; return `if (string == ${asciiLiteral(spelling)})\n return ${variant};\nbreak;`; } - const groupsAt = (at: number) => { - const groups = new Map(); - for (const candidate of candidates) - groups.set(candidate[0][at], [...(groups.get(candidate[0][at]) ?? []), candidate]); - return groups; - }; - const positions = Array.from(candidates[0][0], (character, at) => at); - const at = positions.reduce((best, at) => (groupsAt(at).size > groupsAt(best).size ? at : best)); + const [at, groups] = Array.from( + candidates[0][0], + (character, at) => [at, Map.groupBy(candidates, ([spelling]) => spelling[at])] as const, + ).reduce((best, next) => (next[1].size > best[1].size ? next : best)); return `switch (string[${at}]) { -${[...groupsAt(at)].map(([character, group]) => `case '${character.replace(/['\\]/, "\\$&")}':\n${indent(cppMatchSpelling(group), 4)}`).join("\n")} +${[...groups].map(([character, group]) => `case '${character}':\n${indent(cppMatchSpelling(group), 4)}`).join("\n")} } break;`; } function cppSourceForEnum(type: EnumerationType): string { - const byLength = new Map(); - type.spellings.forEach((spellings, index) => { - for (const spelling of spellings) - byLength.set(spelling.length, [ - ...(byLength.get(spelling.length) ?? []), - [spelling, `${type.cpp}::${type.variants[index]}`], - ]); - }); + const byLength = Map.groupBy( + type.spellings.flatMap((spellings, index) => + spellings.map(spelling => [spelling, `${type.cpp}::${type.variants[index]}`] as const), + ), + ([spelling]) => spelling.length, + ); return ` template<> std::optional<${type.cpp}> WebCore::parseEnumerationFromString<${type.cpp}>(const WTF::String& string) { @@ -595,7 +589,7 @@ extern "C" bool Bun__Bindgen__${type.name}__fromJS(JSC::JSGlobalObject* globalOb auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); ${type.cpp} converted; - ${cppReturnIfFailed("Bun::Bindgen::convertDictionaryInto(*globalObject, JSC::JSValue::decode(value), converted)", "false").replace("\n", "\n ")} +${indent(cppReturnIfFailed("Bun::Bindgen::convertDictionaryInto(*globalObject, JSC::JSValue::decode(value), converted)", "false"), 4)} *result = ${toExtern("converted")}; return true; } @@ -619,8 +613,7 @@ function cppDeclareConverted( name: string, failure: string, ): string[] { - if (!type.convertsInPlace && !type.conversionThrows) - return [`auto ${variable} = ${type.cppConvert(value, ctx, name)};`]; + if (!type.conversionThrows) return [`auto ${variable} = ${type.cppConvert(value, ctx)};`]; return [ `${implementationType(type)} ${variable};`, ...type.cppConvertInto(variable, value, ctx, name, failure).split("\n"), @@ -989,7 +982,7 @@ ${ if (JSC::JSValue cached = cachedValue.get()) return JSC::JSValue::encode(cached); JSC::JSValue result = JSC::JSValue::decode(${getSymbol}(globalObject, thisObject)); - ${cppReturnIfFailed("!!result", "{}").replace("\n", "\n ")} +${indent(cppReturnIfFailed("!!result", "{}"), 4)} cachedValue.set(vm, thisObject, result); return JSC::JSValue::encode(result);` } @@ -1064,7 +1057,7 @@ JSC_DEFINE_HOST_FUNCTION(construct${name}, (JSC::JSGlobalObject * globalObject, { ${cppPrologue} JSC::Structure* structure = Bun::Bindgen::structureForNewTarget(globalObject, callFrame, ${type.classIndex}); - ${cppReturnIfFailed("!!structure", "{}").replace("\n", "\n ")} +${indent(cppReturnIfFailed("!!structure", "{}"), 4)} ${ type.refCounted && !type.passStructure ? cppCallBody(construct, classSymbolName(type, "construct"), [], call => [ diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 2f4298065586..c5fa8a5889d0 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -173,9 +173,9 @@ export function indent(text: string, by: number): string { .join("\n"); } -/** C++ that returns `failure` if `converted` is false, null or empty, which is how a conversion says that it threw. */ -export const cppReturnIfFailed = (converted: string, failure: string) => - `if (Bun::Bindgen::hasException(scope, ${converted})) [[unlikely]]\n return ${failure};`; +/** C++ that returns `failure` if `succeeded` is false, null or empty, which is how a call says that it threw. */ +export const cppReturnIfFailed = (succeeded: string, failure: string) => + `if (Bun::Bindgen::hasException(scope, ${succeeded})) [[unlikely]]\n return ${failure};`; /** C++ statements that engage `optional`, a `std::optional`, then fill what it holds: `fill` is given a name for that. */ export function cppEmplace(optional: string, fill: (target: string) => string): string { @@ -239,8 +239,8 @@ export abstract class Type { get refuses(): string | null { return null; } - /** Set on a type that throws for some values of a kind it takes, like `1.5` or an unknown string: what to write for `.otherAsNull` instead. */ - get otherAsNullRefusal(): string | null { + /** What to write in place of `.otherAsNull` on a type that throws for some values of a kind it takes, like `1.5` or an unknown string. Else null. */ + get insteadOfOtherAsNull(): string | null { return null; } /** A C++ condition that is true of the values `.otherAsNull` converts in place. */ @@ -332,8 +332,8 @@ export abstract class Type { toRust(value: unknown, scope: RustScope): string { throw new TypeError(`${this.tsType} cannot have a default`); } - /** A C++ expression that converts `value`. `ctx` is a conversion context, `name` what to call the value. */ - cppConvert(value: string, ctx: string, name: string): string { + /** A C++ expression that converts `value`, for a type whose conversion cannot throw. `ctx` is a conversion context. */ + cppConvert(value: string, ctx: string): string { return `Bun::convertIDL<${this.idlType}>(*globalObject, ${value}, ${ctx})`; } /** C++ lines for once every argument is converted. They read into `target`, converted from `value`, what converting a later argument could have changed. */ @@ -346,7 +346,7 @@ export abstract class Type { } /** C++ statements that convert `value` into `target`, just default-constructed, and return `failure` if that throws. */ cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { - if (!this.conversionThrows) return `${target} = ${this.cppConvert(value, ctx, name)};`; + if (!this.conversionThrows) return `${target} = ${this.cppConvert(value, ctx)};`; return cppReturnIfFailed( `Bun::convertIDLInto<${this.idlType}>(*globalObject, ${value}, ${ctx}, ${target})`, failure, @@ -488,7 +488,7 @@ export class IntegerType extends PrimitiveType { assert(typeof value !== "number" || (value >= min && value <= max), `${value} is not in [${min}, ${max}]`); return super.toCpp(value); } - get otherAsNullRefusal(): string { + get insteadOfOtherAsNull(): string { return "b.f64.otherAsNull takes every number"; } /** Also a BigInt that is in range, for an argument whose type is `number | bigint`. */ @@ -583,7 +583,7 @@ export class RangeType extends IntegerType { getHeaders(out: Set): void { out.add("Bindgen/IDLConvertIntegerRange.h"); } - /** Its converter takes the name, not a context. */ + /** Its converter takes no context. That of `.validateInteger` takes the name. */ cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { const reported = this.modifier === "validateInteger" ? `, ${asciiLiteral(name)}` : ""; return cppReturnIfFailed( @@ -616,7 +616,7 @@ export class DoubleType extends PrimitiveType { ); } get tryConvertThrows(): boolean { - return this.valueKinds === "any"; + return false; } /** A `Date` too, as its time value, which is NaN for an invalid one. */ get allowDate(): DoubleType { @@ -671,7 +671,7 @@ export class UnitType extends Type { get takesUndefined(): boolean { return true; } - cppConvert(): string { + cppConvertInto(): string { throw new TypeError(`b.${this.unit} is only an alternative of a union`); } layout(): Layout { @@ -1009,8 +1009,10 @@ export class BytesType extends Type { if (this.#options.shared === "copied") return [ `Bun::Bindgen::BytesCopy ${target}Copy;`, - `if (${target}.${this.#options.held === "borrowed" ? "readCopyingShared" : "copyIfShared"}(*globalObject, ${value}, ${target}Copy)) [[unlikely]]`, - ` RETURN_IF_EXCEPTION(scope, {});`, + ...cppReturnIfFailed( + `${target}.${this.#options.held === "borrowed" ? "readCopyingShared" : "copyIfShared"}(*globalObject, ${value}, ${target}Copy)`, + "{}", + ).split("\n"), ]; return this.#options.held === "borrowed" ? [`${target}.read(${value});`] : []; } @@ -1356,8 +1358,8 @@ export class OptionalType extends Type { ".otherAsNull goes by the kind of a value, and this type does not: write it without .loose or nonObjectAsEmpty", ); assert( - !inner.otherAsNullRefusal, - `.otherAsNull goes by the kind of a value, and this type throws for a value of a kind it takes: ${inner.otherAsNullRefusal}`, + !inner.insteadOfOtherAsNull, + `.otherAsNull goes by the kind of a value, and this type throws for a value of a kind it takes: ${inner.insteadOfOtherAsNull}`, ); } // `.ifPassed` does not claim `undefined`, so it goes on a type that takes it. @@ -1431,13 +1433,13 @@ export class OptionalType extends Type { }[this.modifier]; } cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { - // One test of the value says both whether it is taken and what it converts to. - if (!this.conversionThrows || (this.modifier === "otherAsNull" && !this.convertsInPlace)) - return super.cppConvertInto(target, value, ctx, name, failure); + // The `tryConvert` of `.otherAsNull` tests the value and converts it in one step. + const convertsInOneStep = this.modifier === "otherAsNull" && !this.convertsInPlace; + if (!this.conversionThrows || convertsInOneStep) return super.cppConvertInto(target, value, ctx, name, failure); const convert = (slot: string) => this.inner.cppConvertInto(slot, value, ctx, name, failure); const body = this.inner.nullRepresentation === "optional" ? cppEmplace(target, convert) : convert(target); const converted = `if (${this.#cppIsPresent(value)}) {\n${indent(body, 4)}\n}`; - // What converts in place and a `std::optional` are null when default-constructed. A pointer to a class instance is not. + // Both are null when default-constructed. Of the rest, a pointer to a class instance is not. if (this.convertsInPlace || this.inner.nullRepresentation === "optional") return converted; return `${converted} else {\n ${target} = ${this.inner.idlType}::nullValue();\n}`; } @@ -1535,8 +1537,8 @@ export class WithValueType extends Type { get convertsInPlace(): boolean { return this.inner.convertsInPlace; } - cppConvert(value: string, ctx: string, name: string): string { - return this.inner.cppConvert(value, ctx, name); + cppConvert(value: string, ctx: string): string { + return this.inner.cppConvert(value, ctx); } cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { return this.inner.cppConvertInto(target, value, ctx, name, failure); @@ -1619,8 +1621,8 @@ export class DefaultType extends Type { private cppIsAbsent(value: string): string { return `${value}.${this.onNull ? "isUndefinedOrNull" : "isUndefined"}()`; } - cppConvert(value: string, ctx: string, name: string): string { - return `${this.cppIsAbsent(value)} ? ${this.toCpp(this.value)} : ${this.inner.cppConvert(value, ctx, name)}`; + cppConvert(value: string, ctx: string): string { + return `${this.cppIsAbsent(value)} ? ${this.toCpp(this.value)} : ${this.inner.cppConvert(value, ctx)}`; } get convertsInPlace(): boolean { return this.inner.convertsInPlace; @@ -1658,7 +1660,10 @@ export class ArrayType extends Type { constructor(element: Type) { super(); assert(!(element instanceof DefaultType), "an element of an array is never absent, so it has no default"); - assert(!(element instanceof RangeType), "a range reports the name of its argument, which an element does not have"); + assert( + !(withoutModifiers(element) instanceof RangeType), + "a range reports the name of its argument, which an element does not have", + ); refuseOutsideArgument(element); assert( !element.borrowsCell, @@ -1814,7 +1819,7 @@ export class EnumerationType extends NamedType { get valueKinds(): ValueKinds { return ["string"]; } - get otherAsNullRefusal(): string { + get insteadOfOtherAsNull(): string { return "b.string.allowStringObject.otherAsNull takes every string"; } get idlType(): string { @@ -1944,7 +1949,7 @@ export class DictionaryType extends NamedType { get fromJS(): boolean { return !!this.options.fromJS; } - get otherAsNullRefusal(): string | null { + get insteadOfOtherAsNull(): string | null { return this.allowArray ? null : "give the dictionary allowArray"; } get allowArray(): boolean { @@ -2091,12 +2096,6 @@ export class UnionType extends NamedType { get convertsInPlace(): boolean { return true; } - cppConvertInto(target: string, value: string, ctx: string, name: string, failure: string): string { - return cppReturnIfFailed( - `WebCore::Converter<${this.idlType}>::convertInto(*globalObject, ${value}, ${ctx}, ${target})`, - failure, - ); - } get tsType(): string { return this.alternatives.map(a => a.type.tsType).join(" | "); } @@ -2270,10 +2269,13 @@ function refuseOutsideArgument(type: Type, name?: string): void { /** The converter of a range takes the name and no context. */ function refuseExpectedOfRange(name: string, type: Type): void { - let bare = type; - while (bare instanceof OptionalType || bare instanceof DefaultType || bare instanceof WithValueType) - bare = bare.inner; - assert(!(bare instanceof RangeType), `expected: ${name} is a range, which has its own errors`); + assert(!(withoutModifiers(type) instanceof RangeType), `expected: ${name} is a range, which has its own errors`); +} + +function withoutModifiers(type: Type): Type { + while (type instanceof OptionalType || type instanceof DefaultType || type instanceof WithValueType) + type = type.inner; + return type; } function refuseResultType(name: string, type: Type): void { @@ -2314,6 +2316,10 @@ export class FunctionDefinition { ); this.signatures = signatures.map(options => (options instanceof Signature ? options : new Signature(options))); assert(this.signatures.filter(signature => signature.length !== undefined).length <= 1, "length is given once"); + assert( + this.exportName === undefined || this.signatures.every(signature => signature.length === undefined), + "length: what makes an exported function gives its length", + ); assert( this.signatures.every(signature => signature.rejects === this.signatures[0].rejects), "every overload gives rejects the same value", @@ -2350,9 +2356,7 @@ export class FunctionDefinition { candidates.filter((signature, i) => (types[i] ? test(types[i]) : kind === "undefined")); const ofKind = keep(type => takes(type, kind)); const takers = kind === "undefined" ? keep(type => type.takesUndefined) : ofKind; - const kept = [takers, ofKind, candidates.filter((signature, i) => types[i]), candidates].find( - list => list.length > 0, - )!; + const kept = [takers, ofKind, candidates.filter((signature, i) => types[i])].find(list => list.length > 0)!; return this.dispatchOn(kept, argument + 1, nodes); }; const kinds = new Set(["undefined" as const, ...distinguishingKinds(types.filter(type => type !== undefined))]); diff --git a/src/codegen/generate-host-exports.ts b/src/codegen/generate-host-exports.ts index f4d8ddcdc349..6369987e0d71 100644 --- a/src/codegen/generate-host-exports.ts +++ b/src/codegen/generate-host-exports.ts @@ -24,7 +24,7 @@ // shape impl signature → thunk // ──────────── ───────────────────────────────────────────────────────────────── // lazy (&JSGlobalObject) -> JsResult|JSValue -// → host_fn::host_fn_result(g, || path::name(g)) +// → host_fn::to_js_host_call(g, || path::name(g)) // generic (T0, T1, …) -> R where Tn ∈ {&X, &mut X, *T, scalar} // → unsafe extern "C" fn() -> R { path::name() } // @@ -396,7 +396,7 @@ function emitThunk(e: Export): string { // ABI-identical to non-null `*const JSGlobalObject`; C++ never passes // null here. const body = retIsJsResult - ? ` host_fn::host_fn_result(g, || ${impl}(g))` + ? ` host_fn::to_js_host_call(g, || ${impl}(g))` : ` host_fn::host_fn_lazy_passthrough(g, ${impl})`; return ` // ${loc} @@ -444,7 +444,7 @@ pub extern "Rust" fn ${e.symbol}(${sig}) -> ${e.ret} { .join(""); const call = e.params.map(p => p.name).join(", "); // If the impl returns `JsResult` and takes a `&JSGlobalObject` - // somewhere, route through `host_fn_result` so the error is mapped; else + // somewhere, route through `to_js_host_call` so the error is mapped; else // forward raw. const globalParam = e.params.find(p => /JSGlobalObject$/.test(p.ty)); const cRet = retIsJsResult ? "JSValue" : e.ret; @@ -452,11 +452,11 @@ pub extern "Rust" fn ${e.symbol}(${sig}) -> ${e.ret} { if (e.isUnsafe) { // Place `unsafe { }` at the call site so the SAFETY comment sits next // to the actual unsafe operation. (Closures inherit the enclosing - // `unsafe` context, so wrapping the whole `host_fn_result(..)` + // `unsafe` context, so wrapping the whole `to_js_host_call(..)` // expression would also compile — this is for clarity, not necessity.) inner = `\n // SAFETY: C++ caller upholds the impl fn's documented safety contract.\n unsafe { ${inner} }`; } - const body = retIsJsResult && globalParam ? `host_fn::host_fn_result(${globalParam.name}, || ${inner})` : inner; + const body = retIsJsResult && globalParam ? `host_fn::to_js_host_call(${globalParam.name}, || ${inner})` : inner; return ` // ${loc} ${emitNoMangle(e.abi, e.symbol, sig, cRet, `${binds} ${body}`)}`; diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 8a65267c2fcc..56b6eea2dfd0 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -530,12 +530,12 @@ export const overloaded = b.overloads( returns: b.string.utf8, rustPath: "crate::bindgen_test::overloaded_text_upper", }, + { args: { number: b.f64 }, returns: b.string.utf8, rustPath: "crate::bindgen_test::overloaded_number" }, { args: { a: b.f64, b: b.f64 }, returns: b.string.utf8, rustPath: "crate::bindgen_test::overloaded_two_numbers", }, - { args: { number: b.f64 }, returns: b.string.utf8, rustPath: "crate::bindgen_test::overloaded_number" }, { args: { list: b.array(b.f64) }, returns: b.string.utf8, rustPath: "crate::bindgen_test::overloaded_list" }, { args: { counter: TestCounter }, diff --git a/src/jsc/bindings/Bindgen/IDLConvertBase.h b/src/jsc/bindings/Bindgen/IDLConvertBase.h index d44122acd6df..11e0dc2da24b 100644 --- a/src/jsc/bindings/Bindgen/IDLConvertBase.h +++ b/src/jsc/bindings/Bindgen/IDLConvertBase.h @@ -52,11 +52,11 @@ struct ElementOf : ContextBase> { } -// Whether a conversion threw, from what it returned: false, null or empty. As WebCore's ConversionResult::hasException. -ALWAYS_INLINE bool hasException(JSC::ThrowScope& scope, bool converted) +// Whether a call threw, from what it returned: false, null or empty. As WebCore's ConversionResult::hasException. +ALWAYS_INLINE bool hasException(JSC::ThrowScope& scope, bool succeeded) { - EXCEPTION_ASSERT_UNUSED(scope, !!scope.exception() == !converted); - return !converted; + EXCEPTION_ASSERT_UNUSED(scope, !!scope.exception() == !succeeded); + return !succeeded; } // Conversion context where the name of the value being converted is specified as an diff --git a/src/jsc/bindings/Bindgen/JSBytesView.cpp b/src/jsc/bindings/Bindgen/JSBytesView.cpp index 01f1909ffc8e..c367b13e0c2f 100644 --- a/src/jsc/bindings/Bindgen/JSBytesView.cpp +++ b/src/jsc/bindings/Bindgen/JSBytesView.cpp @@ -65,19 +65,20 @@ bool isSharedBuffer(JSValue value) return false; } -std::span copyBytes(JSGlobalObject& globalObject, std::span bytes, BytesCopy& copy) +bool copyBytes(JSGlobalObject& globalObject, std::span& bytes, BytesCopy& copy) { if (bytes.empty()) - return bytes; + return true; // Not a Vector, which holds less than a buffer can. copy = MallocSpan::tryMalloc(bytes.size()); if (!*copy) [[unlikely]] { auto scope = DECLARE_THROW_SCOPE(getVM(&globalObject)); throwOutOfMemoryError(&globalObject, scope); - return bytes; + return false; } memcpySpan(copy->mutableSpan(), bytes); - return copy->span(); + bytes = copy->span(); + return true; } } diff --git a/src/jsc/bindings/Bindgen/JSBytesView.h b/src/jsc/bindings/Bindgen/JSBytesView.h index 8fe4be820c8d..3532f9dff87d 100644 --- a/src/jsc/bindings/Bindgen/JSBytesView.h +++ b/src/jsc/bindings/Bindgen/JSBytesView.h @@ -15,8 +15,8 @@ bool isSharedBuffer(JSC::JSValue); // Optional: ~MallocSpan calls free, of null too. using BytesCopy = std::optional>; -// A copy of the bytes, which `copy` holds, or the bytes themselves if there are none. Allocating it can throw. -std::span copyBytes(JSC::JSGlobalObject&, std::span, BytesCopy& copy); +// `bytes` become a copy of themselves, which `copy` holds, unless there are none. False if it threw: allocating can. +bool copyBytes(JSC::JSGlobalObject&, std::span& bytes, BytesCopy& copy); // Whether it holds a Blob: a Blob, or a `Bun.build()` output. Throws for one whose bytes are not in memory: a file. bool checkInMemoryBlob(JSC::JSGlobalObject&, JSC::JSValue); @@ -43,26 +43,25 @@ class JSBlobOrBufferView { m_bytes = bytesOfBlobOrBuffer(argument); } - // The bytes of a shared buffer become those of `copy`. Whether it is one: copying can throw. + // The bytes of a shared buffer become those of `copy`. False if it threw. ALWAYS_INLINE bool copyIfShared(JSC::JSGlobalObject& globalObject, JSC::JSValue argument, BytesCopy& copy) { if (!m_isBlobOrBuffer || !isSharedBuffer(argument)) [[likely]] - return false; - m_bytes = copyBytes(globalObject, bytesOfBlobOrBuffer(argument), copy); - return true; + return true; + m_bytes = bytesOfBlobOrBuffer(argument); + return copyBytes(globalObject, m_bytes, copy); } // `read` and `copyIfShared` in one look at the buffer. ALWAYS_INLINE bool readCopyingShared(JSC::JSGlobalObject& globalObject, JSC::JSValue argument, BytesCopy& copy) { if (!m_isBlobOrBuffer) - return false; + return true; bool isShared = false; m_bytes = bytesOfBlobOrBuffer(argument, isShared); if (!isShared) [[likely]] - return false; - m_bytes = copyBytes(globalObject, m_bytes, copy); - return true; + return true; + return copyBytes(globalObject, m_bytes, copy); } // Null data for a Blob or a buffer that was neither read nor copied: Rust reads it, each time it borrows the bytes. diff --git a/src/jsc/bindings/BunIDLConvertBase.h b/src/jsc/bindings/BunIDLConvertBase.h index dfaafc608f2e..b62bf8d31455 100644 --- a/src/jsc/bindings/BunIDLConvertBase.h +++ b/src/jsc/bindings/BunIDLConvertBase.h @@ -20,7 +20,7 @@ ALWAYS_INLINE typename WebCore::Converter::ReturnType convertIDL( } } -// False if it threw. A converter that has `convertInto` says so itself, as WebCore's ConversionResult does. After another one the VM is asked. +// False if it threw. A converter with `convertInto` reports that itself, as WebCore's ConversionResult does. For any other, this asks the VM. template ALWAYS_INLINE bool convertIDLInto(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx, Result& result) { diff --git a/src/jsc/host_fn.rs b/src/jsc/host_fn.rs index bf24d160dea6..b63ebc73766c 100644 --- a/src/jsc/host_fn.rs +++ b/src/jsc/host_fn.rs @@ -129,13 +129,6 @@ fn debug_exception_assertion(global_this: &JSGlobalObject, value: JSValue, func: // // The thunks of `generate-host-exports.ts` and `generate-jssink.ts` call these. -/// Runs the body in an `ExceptionValidationScope`. -#[track_caller] -#[inline] -pub fn host_fn_result(global: &JSGlobalObject, f: impl FnOnce() -> JsResult) -> JSValue { - to_js_host_call(global, f) -} - // ──────────────────────── safe codegen-thunk entry points ──────────────────────── /// `fn(&JSGlobalObject, &CallFrame) -> JsResult`. @@ -146,7 +139,7 @@ pub fn host_fn_static( callframe: &CallFrame, f: impl FnOnce(&JSGlobalObject, &CallFrame) -> JsResult, ) -> JSValue { - host_fn_result(global, || f(global, callframe)) + to_js_host_call(global, || f(global, callframe)) } /// Lazy property creator, no exception scope (bare-`JSValue` impls). diff --git a/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts b/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts index 7ec70f95db63..16ec274e5041 100644 --- a/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts +++ b/src/runtime/api/bun/js_bun_spawn_bindings.bind.ts @@ -3,11 +3,11 @@ import * as b from "../../../codegen/bindgen-lib.ts"; export default b.module({ rustPath: "crate::api::js_bun_spawn_bindings" }); /** `(cmd, options?)` or `({ cmd, ..options })`. */ -const spawner = { +const spawnSignature = { args: { cmdOrOptions: b.any, options: b.any }, returns: b.any, capability: "js", throws: true, } as const; -export const spawn = b.fn(spawner); -export const spawnSync = b.fn(spawner); +export const spawn = b.fn(spawnSignature); +export const spawnSync = b.fn(spawnSignature); diff --git a/src/runtime/timer/Timer.rs b/src/runtime/timer/Timer.rs index d649a78bcc53..35a1cc459549 100644 --- a/src/runtime/timer/Timer.rs +++ b/src/runtime/timer/Timer.rs @@ -495,7 +495,7 @@ impl DateHeaderTimer { // `generate-host-exports.ts` // scrapes the `// HOST_EXPORT` markers below and emits the seven thunks into -// `generated_host_exports.rs`, each routing through `host_fn::host_fn_result`. +// `generated_host_exports.rs`, each routing through `host_fn::to_js_host_call`. // // C++ callers (`src/jsc/bindings/node/NodeTimers.cpp`, `BunObject.cpp`) declare // these in `headers.h` as `(JSGlobalObject*, EncodedJSValue…) -> EncodedJSValue`. diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 35a43bc56c2c..75778857b4c7 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -113,6 +113,7 @@ itAlways("mistakes in a schema are errors when it loads", () => { expect(() => b.namespace({ f: { [key]: true } })).toThrow(`f: a function of a namespace has no ${key}`); } expect(() => b.array(b.null)).toThrow("an array of nothing but null"); + expect(() => b.array(b.i32.enforceRange(0, 100).nullable)).toThrow("a range reports the name of its argument"); const stringOrBuffer = b.bytes({ from: ["string", "buffer"] }); expect(() => b.array(stringOrBuffer)).toThrow("the bytes of a buffer are only for an argument"); expect(() => b.dictionary({ bytes: stringOrBuffer })).toThrow("the bytes of a buffer are only for an argument"); @@ -777,6 +778,9 @@ describe("functions", () => { expect(made(21)).toBe(42); expect(() => made("21")).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); expect(() => b.fn({ export: "not a name" })).toThrow("export: not a name is not a C identifier"); + expect(() => b.fn({ export: "Bun__name", length: 1 })).toThrow( + "length: what makes an exported function gives its length", + ); }); it("promisePtr and nativePromiseContext give the pointer in the context of a promise reaction", () => { From 86b82ef8032a0cc29c251dfd01991019362d77e5 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 09:43:32 +0000 Subject: [PATCH 17/22] Escape a quote or a backslash in the switch label of an enumeration's parser --- src/codegen/bindgen-emit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 50c51310461d..7e814bbe1262 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -454,7 +454,7 @@ function cppMatchSpelling(candidates: readonly (readonly [spelling: string, vari (character, at) => [at, Map.groupBy(candidates, ([spelling]) => spelling[at])] as const, ).reduce((best, next) => (next[1].size > best[1].size ? next : best)); return `switch (string[${at}]) { -${[...groups].map(([character, group]) => `case '${character}':\n${indent(cppMatchSpelling(group), 4)}`).join("\n")} +${[...groups].map(([character, group]) => `case '${character.replace(/['\\]/, "\\$&")}':\n${indent(cppMatchSpelling(group), 4)}`).join("\n")} } break;`; } From 5745562210791843f8860a3cfe611592a2585911 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 10:07:21 +0000 Subject: [PATCH 18/22] Take `Array.prototype` as an array, as `Array.isArray` does --- src/codegen/bindgen-lib-internal.ts | 2 +- src/jsc/bindings/Bindgen/IDLConvert.h | 4 ++-- test/internal/bindgen.test.ts | 8 ++++---- test/js/bun/console/bun-inspect-table.test.ts | 9 +++++++++ 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index c5fa8a5889d0..44bb80aef01c 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -121,7 +121,7 @@ export const kindTests: Record< bigint: { ofValue: value => `${value}.isBigInt32()`, ofCell: "cell->isHeapBigInt()" }, string: { ofCell: "cell->isString()" }, stringObject: { ofCell: "cell->inherits()" }, - array: { ofCell: "JSC::isJSArray(cell)" }, + array: { ofCell: "cell->inherits()" }, arrayBuffer: { ofCell: "cell->inherits()" }, arrayBufferView: { ofCell: "cell->inherits()" }, callable: { ofCell: "cell->isCallable()" }, diff --git a/src/jsc/bindings/Bindgen/IDLConvert.h b/src/jsc/bindings/Bindgen/IDLConvert.h index ee64e6e2eb44..383b460d74b0 100644 --- a/src/jsc/bindings/Bindgen/IDLConvert.h +++ b/src/jsc/bindings/Bindgen/IDLConvert.h @@ -284,7 +284,7 @@ template<> struct WebCore::Converter : Bun::DefaultTr template static std::optional tryConvert(JSC::JSGlobalObject&, JSC::JSValue value, Ctx&) { - if (!JSC::isJSArray(value)) + if (!value.inherits()) return std::nullopt; return value; } @@ -476,7 +476,7 @@ struct WebCore::Converter> : Bun::DefaultTryConverte JSC::JSValue value, Ctx& ctx) { - if (!JSC::isJSArray(value)) + if (!value.inherits()) return std::nullopt; auto& vm = JSC::getVM(&globalObject); auto scope = DECLARE_THROW_SCOPE(vm); diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 75778857b4c7..afdad65d722e 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -920,6 +920,7 @@ describe("b.callable, b.jsArray, b.object and b.undefined", () => { expect(bindgen.describeHandle(() => {})).toBe("callback"); expect(bindgen.describeHandle([1])).toBe("list"); expect(bindgen.describeHandle(new (class extends Array {})())).toBe("list"); + expect(bindgen.describeHandle(Array.prototype)).toBe("list"); expect(bindgen.describeHandle({ x: 2 })).toBe("point 2"); expect(bindgen.describeHandle(true)).toBe("text true"); expect(bindgen.describeHandle(3)).toBe("text 3"); @@ -1370,10 +1371,8 @@ describe("unions", () => { expect(calls).toEqual([]); }); - it("Array.prototype, which b.array refuses, is not of the array kind", () => { - expect(() => bindgen.describeValue(Array.prototype)).toThrow( - "value must be of type number, boolean, string, or array of integer", - ); + it("Array.prototype, which b.array takes, is of the array kind", () => { + expect(bindgen.describeValue(Array.prototype)).toBe("list []"); }); it("an enumeration takes strings, and b.any what nothing before it took", () => { @@ -1516,6 +1515,7 @@ describe("what is only a result", () => { describe("arrays", () => { it("in", () => { expect(bindgen.sumArray([])).toBe(0); + expect(bindgen.sumArray(Array.prototype)).toBe(0); expect(bindgen.sumArray([1, 2, 3])).toBe(6); expect(bindgen.sumArray(Array.from({ length: 1000 }, (_, i) => i))).toBe(499500); expect(() => bindgen.sumArray(new Set([4, 5]))).toThrow("numbers must be an array of integer"); diff --git a/test/js/bun/console/bun-inspect-table.test.ts b/test/js/bun/console/bun-inspect-table.test.ts index 1572f38be60d..29a5d16c98e0 100644 --- a/test/js/bun/console/bun-inspect-table.test.ts +++ b/test/js/bun/console/bun-inspect-table.test.ts @@ -47,6 +47,15 @@ describe("inspect.table", () => { expect(inspect.table(function () {})).not.toBeEmpty(); }); + it("takes a subclass of Array, and Array.prototype, as the list of properties", () => { + const rows = [{ a: 1, b: 2 }]; + class Properties extends Array {} + expect(inspect.table(rows, Properties.from(["a"]), { colors: false })).toBe( + inspect.table(rows, ["a"], { colors: false }), + ); + expect(inspect.table(rows, Array.prototype, { colors: false })).toBe(inspect.table(rows, [], { colors: false })); + }); + it("a depth option above the cell depth cap prints cells like the default", () => { const rows = [{ a: [1, 2], m: new Map([[1, 2]]), s: new Set([1]), o: { x: 1 } }]; const byDefault = inspect.table(rows); From ac7f2bafa7dc87b6ab346230e483a1fedeb5f2a6 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 10:21:59 +0000 Subject: [PATCH 19/22] Expect `Array.prototype` as a tls option to be an empty array, as in Node --- test/js/bun/net/socket.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index 1a95c4cd96f3..d58b87944327 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -6637,16 +6637,15 @@ it("a view of part of a buffer as a tls option is read as the view", () => { } }); -it("Array.prototype as a tls option is none of the types one takes", () => { +it("Array.prototype as a tls option is an empty array, as in Node", () => { for (const name of ["key", "cert", "ca", "crl"]) { - expect(() => - Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} }, tls: { [name]: Array.prototype } as any }), - ).toThrow( - expect.objectContaining({ - code: "ERR_INVALID_ARG_TYPE", - message: `TLSOptions.${name} must be of type string, ArrayBuffer, Blob, array of , or null`, - }), - ); + using listener = Bun.listen({ + hostname: "127.0.0.1", + port: 0, + socket: { data() {} }, + tls: { [name]: Array.prototype } as any, + }); + expect(listener.port).toBeGreaterThan(0); } }); From 3fcb01a7a7648126148eaa766ae7cd822efcc355 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 19:01:56 +0000 Subject: [PATCH 20/22] Add `requiredArguments`, which throws `ERR_MISSING_ARGS` for a shorter call, and set it on `fetch`, `fetch.preconnect` and the object URL functions --- docs/project/bindgen.mdx | 14 +++++-- src/codegen/bindgen-emit.ts | 16 ++++++-- src/codegen/bindgen-lib-internal.ts | 20 ++++++++-- src/js/internal-for-testing.ts | 4 ++ src/jsc/bindgen_test.bind.ts | 39 ++++++++++++++++++- src/runtime/webcore/ObjectURLRegistry.bind.ts | 12 +++--- src/runtime/webcore/ObjectURLRegistry.rs | 28 +++++-------- src/runtime/webcore/fetch.bind.ts | 10 ++++- test/internal/bindgen.test.ts | 39 +++++++++++++++++++ test/js/web/fetch/fetch-args.test.ts | 2 +- test/js/web/fetch/fetch-session.test.ts | 2 +- 11 files changed, 146 insertions(+), 40 deletions(-) diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index b36fb7b3bf78..ca1601be37d5 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -171,6 +171,14 @@ A function that returns a promise reports an error by rejecting one. With `rejec throws and what the conversion of an argument throws become the reason of a rejected promise (`JSPromise::rejectedPromiseWithCaughtException`), so the body returns `Err` like any other. +### Too few arguments: `requiredArguments` + +An argument that was not passed is `undefined`, so `f()` and `f(undefined)` are the same call and the type of the +argument reports it. With `requiredArguments: 1`, a call that has fewer arguments than that throws `ERR_MISSING_ARGS` +before any is converted, as WebIDL asks of a Web API: `fetch()`, `URL.revokeObjectURL()`. The message names them as +Node's does, `The "url" argument must be specified`, by the first signature of overloads. It also goes on a method +and on `construct`. + ### Overloads `b.overloads(a, b, ..)` is one JS function with several signatures. Each names its @@ -183,7 +191,7 @@ export const read = b.overloads( ); ``` -`passThis`, `passCallee`, `export` and `rejects` belong to the JS function, so every signature gives them the same value. +`passThis`, `passCallee`, `export`, `rejects` and `requiredArguments` belong to the JS function, so every signature gives them the same value. Use it when the type of one argument says what the others are. A union for each argument would accept combinations that mean nothing, and leave the body to sort them out. @@ -211,7 +219,7 @@ that is an integer in range, and nothing else. A failed strict or `.validateInteger` conversion, or a `.loose` one of an integer, throws `ERR_INVALID_ARG_TYPE` or `ERR_OUT_OF_RANGE` naming the argument or the dictionary member; `.wrap`, `.enforceRange` and the other `.loose` conversions throw what WebIDL does. An argument that was not passed is `undefined` and is converted like -any other: nothing looks at how many there are, except `.ifPassed` and a body that takes `b.any.variadic`. +any other: nothing looks at how many there are, except `.ifPassed`, `requiredArguments` and a body that takes `b.any.variadic`. | Schema | Rust, as an argument | Takes | | ------------------------------------------------ | -------------------------------- | ------------------------------------------------------------- | @@ -258,7 +266,7 @@ These come last, in this order (`b.any` already takes `undefined` and `null`, so - `.falsyAsNull`: every falsy value means absent; `Option`. - `.emptyStringAsNull`: `undefined`, `null` or `""` means absent, as for `JSValue::get_truthy`; `Option`. - `.otherAsNull`: a value of a [kind](#unions-and-kinds) the type does not take, or that it refuses (what is not an object, a shared buffer), means absent and nothing is thrown, as if the type were in a union with `ignored: b.any` after it; `Option`. It is for an argument or a member that is ignored unless it is of the type. What is inside a value is still reported: the members of a dictionary, the elements of an array. It is not for a type that throws for a value of a kind it takes: an integer for `1.5` (`b.f64.otherAsNull` takes every number), an enumeration for an unknown string (`b.string.allowStringObject.otherAsNull` takes every string), a dictionary without `allowArray` for an array. Nor is it for a type that takes every kind, like a `.loose` one or a `nonObjectAsEmpty` dictionary. A union writes `ignored: b.any` as its last alternative. -- `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` differ: both work, like `alert`, or a test of Node pins an error for each, like `URL.revokeObjectURL`. It also goes on a type that takes `undefined`, like `b.any`. It is only for an argument, and not for one of an overload. +- `.ifPassed`: absent only if the call has no such argument, and `undefined` is converted like any other value; `Option`. It is for a function where `f()` and `f(undefined)` both work and differ, like `alert`. Where `f()` is an error, see [`requiredArguments`](#too-few-arguments-requiredarguments). It also goes on a type that takes `undefined`, like `b.any`. It is only for an argument, and not for one of an overload. - `.default(value)`: what to use when absent; `T`. `.nullable.default(value)` also applies to `null`. `.withValue` on an argument also gives the body the argument itself, `(T, JsLocal<'a>)`: for a body that reads the diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 7e814bbe1262..eb3f1ba2b6f1 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -1056,7 +1056,7 @@ static JSC_DECLARE_HOST_FUNCTION(construct${name}); JSC_DEFINE_HOST_FUNCTION(construct${name}, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { ${cppPrologue} - JSC::Structure* structure = Bun::Bindgen::structureForNewTarget(globalObject, callFrame, ${type.classIndex}); +${cppThrowIfArgumentsMissing(construct)} JSC::Structure* structure = Bun::Bindgen::structureForNewTarget(globalObject, callFrame, ${type.classIndex}); ${indent(cppReturnIfFailed("!!structure", "{}"), 4)} ${ type.refCounted && !type.passStructure @@ -1185,6 +1185,16 @@ const cppLeadingOfFunction = (fn: FunctionDefinition) => [ ...(fn.passCallee ? ["JSC::JSValue::encode(callFrame->jsCallee())"] : []), ]; +/** `requiredArguments`. As in Node, the message names every one of them, however many the call has. */ +function cppThrowIfArgumentsMissing(signature: Signature): string { + const names = signature.args.slice(0, signature.requiredArguments ?? 0).map(arg => `"${arg.name}"`); + if (names.length === 0) return ""; + const message = `The ${new Intl.ListFormat("en").format(names)} argument${names.length > 1 ? "s" : ""} must be specified`; + return ` if (callFrame->argumentCount() < ${names.length}) [[unlikely]] + return Bun::ERR::MISSING_ARGS(scope, globalObject, ${asciiLiteral(message)}); +`; +} + /** Defines the host function `name`. `takeThis` declares `thisObject`, which is then the thunks' first argument. */ function cppHostFunction( name: string, @@ -1221,7 +1231,7 @@ function cppHostFunctionBody( return `${define} { ${cppPrologue} -${takeThis}${cppCallBody(fn.signatures[0], symbol(fn.signatures[0]), leading)} +${takeThis}${cppThrowIfArgumentsMissing(fn.signatures[0])}${cppCallBody(fn.signatures[0], symbol(fn.signatures[0]), leading)} }`; } // An overload reads `this` and the callee from the frame. @@ -1254,7 +1264,7 @@ ${[...cases.slice(1), cases[0]].join("\n")} ${define} { ${cppPrologue} -${takeThis}${indent(cppDispatch(fn.dispatch), 4)} +${takeThis}${cppThrowIfArgumentsMissing(fn.signatures[0])}${indent(cppDispatch(fn.dispatch), 4)} }`; } diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 44bb80aef01c..0db7d542962e 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -2152,6 +2152,8 @@ export interface SignatureOptions { throws?: boolean; /** The function returns a promise: what the body or the conversion of an argument throws rejects one, and nothing is thrown. */ rejects?: boolean; + /** A call with fewer arguments throws `ERR_MISSING_ARGS`. Without it nothing counts them: one that was not passed is `undefined`. */ + requiredArguments?: number; /** Path of the Rust function. Defaults to the module's path plus the export's name in snake_case. */ rustPath?: string; /** The JS function's `length`. Defaults to the number of required arguments. */ @@ -2179,12 +2181,14 @@ export class Signature { readonly capability: Capability; readonly throws: boolean; readonly rejects: boolean; + readonly requiredArguments: number | undefined; readonly rustPath: string | undefined; readonly length: number | undefined; /** `hasThisValue`: the body is also handed its own JS object. */ constructor(options: SignatureOptions, hasThisValue = false) { this.length = options.length; this.rejects = options.rejects ?? false; + this.requiredArguments = options.requiredArguments; const { args = {}, expected = {} } = options; this.args = Object.entries(args).map(([name, type]) => ({ name, @@ -2238,6 +2242,13 @@ export class Signature { assert(!(this.returns instanceof DefaultType), "a result has no default"); refuseOutsideArgument(this.returns); assert(!this.rejects || this.returns.holdsJSValue, "rejects: the result is a promise"); + assert( + this.requiredArguments === undefined || + (Number.isInteger(this.requiredArguments) && + this.requiredArguments >= 1 && + this.requiredArguments <= this.args.length), + `requiredArguments: ${this.requiredArguments} is not a count of the arguments`, + ); assert( !this.returns.holdsJSValue || this.capability !== "none" || hasThisValue, 'returning a JS value takes capability: "heap" or "js"', @@ -2320,10 +2331,11 @@ export class FunctionDefinition { this.exportName === undefined || this.signatures.every(signature => signature.length === undefined), "length: what makes an exported function gives its length", ); - assert( - this.signatures.every(signature => signature.rejects === this.signatures[0].rejects), - "every overload gives rejects the same value", - ); + for (const key of ["rejects", "requiredArguments"] as const) + assert( + this.signatures.every(signature => signature[key] === this.signatures[0][key]), + `every overload gives ${key} the same value`, + ); assert( this.signatures.length === 1 || !this.signatures.some(signature => signature.args.some(arg => isIfPassed(arg.type))), diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index 29bc526b430e..f66ef946af7d 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -291,6 +291,9 @@ export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { timeValue: $bindgen("bindgen_test.bind.ts", "timeValue"), textIfPassed: $bindgen("bindgen_test.bind.ts", "textIfPassed"), isPassed: $bindgen("bindgen_test.bind.ts", "isPassed"), + isPassedAfterRequired: $bindgen("bindgen_test.bind.ts", "isPassedAfterRequired"), + requiredPair: $bindgen("bindgen_test.bind.ts", "requiredPair"), + requiredOfOverloads: $bindgen("bindgen_test.bind.ts", "requiredOfOverloads"), lengthWithValue: $bindgen("bindgen_test.bind.ts", "lengthWithValue"), counterWithValue: $bindgen("bindgen_test.bind.ts", "counterWithValue"), keptString: $bindgen("bindgen_test.bind.ts", "keptString"), @@ -302,6 +305,7 @@ export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { callbackOrDelay: $bindgen("bindgen_test.bind.ts", "callbackOrDelay"), presentArguments: $bindgen("bindgen_test.bind.ts", "presentArguments"), finiteLater: $bindgen("bindgen_test.bind.ts", "finiteLater"), + requiredFiniteLater: $bindgen("bindgen_test.bind.ts", "requiredFiniteLater"), integerOrBigInt: $bindgen("bindgen_test.bind.ts", "integerOrBigInt"), looseInteger: $bindgen("bindgen_test.bind.ts", "looseInteger"), looseIntegerOrBigInt: $bindgen("bindgen_test.bind.ts", "looseIntegerOrBigInt"), diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 56b6eea2dfd0..7059c14758c8 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -229,7 +229,7 @@ export const TestCounter = b.class({ { args: { by: b.i32 }, returns: b.i32, rustName: "add_number" }, ], forgetTag: { capability: "heap", passThis: true }, - listen: { args: { listener: b.any }, capability: "heap", passThis: true }, + listen: { args: { listener: b.any }, capability: "heap", passThis: true, requiredArguments: 1 }, /** Calls the listener, which may call back into this object, then returns the count. */ notify: { returns: b.i32, capability: "js", throws: true, passThis: true }, /** `other` may be `this`. */ @@ -247,7 +247,7 @@ export const TestCounter = b.class({ /** `mutates`: `increment` and the setter take `&mut self`. */ export const TestMutates = b.class({ rustPath: "crate::bindgen_test::TestMutates", - construct: { args: { count: b.i32 } }, + construct: { args: { count: b.i32 }, requiredArguments: 1 }, proto: { count: { getter: b.i32 }, label: { getter: b.string.utf8, setter: true }, @@ -427,6 +427,32 @@ export const timeValue = b.fn({ args: { time: b.f64.allowDate }, returns: b.f64 /** "none" without the argument. `undefined` is converted, to "undefined". */ export const textIfPassed = b.fn({ args: { text: b.string.loose.utf8.ifPassed }, returns: b.string.utf8 }); export const isPassed = b.fn({ args: { first: b.any, second: b.any.ifPassed }, returns: b.bool }); +export const isPassedAfterRequired = b.fn({ + args: { first: b.any, second: b.any.ifPassed }, + returns: b.bool, + requiredArguments: 1, + rustPath: "crate::bindgen_test::is_passed", +}); +export const requiredPair = b.fn({ + args: { a: b.f64, b: b.f64 }, + returns: b.string.utf8, + requiredArguments: 2, + rustPath: "crate::bindgen_test::overloaded_two_numbers", +}); +export const requiredOfOverloads = b.overloads( + { + args: { text: b.string.utf8, upper: b.bool }, + returns: b.string.utf8, + requiredArguments: 1, + rustPath: "crate::bindgen_test::overloaded_text_upper", + }, + { + args: { number: b.f64 }, + returns: b.string.utf8, + requiredArguments: 1, + rustPath: "crate::bindgen_test::overloaded_number", + }, +); /** `[the text's length, the argument]`. */ export const lengthWithValue = b.fn({ args: { text: b.string.loose.utf8.withValue }, @@ -495,6 +521,15 @@ export const finiteLater = b.fn({ throws: true, rejects: true, }); +export const requiredFiniteLater = b.fn({ + args: { value: b.f64 }, + returns: b.any, + capability: "js", + throws: true, + rejects: true, + requiredArguments: 1, + rustPath: "crate::bindgen_test::finite_later", +}); export const integerOrBigInt = b.fn({ args: { value: b.u32.allowBigInt }, returns: b.u32 }); export const looseInteger = b.fn({ args: { value: b.u16.loose }, returns: b.u16 }); diff --git a/src/runtime/webcore/ObjectURLRegistry.bind.ts b/src/runtime/webcore/ObjectURLRegistry.bind.ts index 4e2a90991f82..451875295e51 100644 --- a/src/runtime/webcore/ObjectURLRegistry.bind.ts +++ b/src/runtime/webcore/ObjectURLRegistry.bind.ts @@ -3,14 +3,16 @@ import { Blob, BlobOrBuildArtifact } from "./Blob.bind.ts"; export default b.module({ rustPath: "crate::webcore::object_url_registry" }); -export const createObjectURL = b.fn({ args: { blob: BlobOrBuildArtifact }, returns: b.string }); +export const createObjectURL = b.fn({ + args: { object: BlobOrBuildArtifact }, + returns: b.string, + requiredArguments: 1, +}); -/** Without an argument it throws `ERR_MISSING_ARGS`, as in Node. */ export const revokeObjectURL = b.fn({ - args: { url: b.string.allowStringObject.ifPassed }, + args: { url: b.string.allowStringObject }, expected: { url: "revokeObjectURL expects a string" }, - capability: "heap", - throws: true, + requiredArguments: 1, }); /** `resolveObjectURL` of `node:buffer`. */ diff --git a/src/runtime/webcore/ObjectURLRegistry.rs b/src/runtime/webcore/ObjectURLRegistry.rs index 8f92c2fe3a6f..8998c8f79578 100644 --- a/src/runtime/webcore/ObjectURLRegistry.rs +++ b/src/runtime/webcore/ObjectURLRegistry.rs @@ -2,9 +2,9 @@ use std::sync::OnceLock; use bun_collections::HashMap; use bun_core::strings; -use bun_jsc::bindgen::{self, JsHeap, JsScope}; +use bun_jsc::bindgen::JsScope; use bun_jsc::virtual_machine::VirtualMachine; -use bun_jsc::{ErrorCode, JSGlobalObject, JSValue, UUID}; +use bun_jsc::{JSGlobalObject, JSValue, UUID}; use bun_threading::Guarded; use crate::generated_bindgen::blob::BlobOrBuildArtifact; @@ -113,33 +113,23 @@ fn uuid_from_pathname(pathname: &[u8]) -> Option { UUID::parse(pathname).ok() } -pub(crate) fn create_object_url(blob: BlobOrBuildArtifact<'_>) -> bun_core::String { +pub(crate) fn create_object_url(object: BlobOrBuildArtifact<'_>) -> bun_core::String { let registry = ObjectURLRegistry::singleton(); - let uuid = registry.register(VirtualMachine::get().as_mut(), blob.blob()); + let uuid = registry.register(VirtualMachine::get().as_mut(), object.blob()); bun_core::String::create_format(format_args!("blob:{}", uuid)) } -pub(crate) fn revoke_object_url<'a>( - heap: &JsHeap<'a>, - url: Option, -) -> bindgen::Result<'a, ()> { - let Some(str) = url else { - return Err(heap.error_with_code( - ErrorCode::MISSING_ARGS, - format_args!("Not enough arguments to 'revokeObjectURL'. Expected 1, got 0."), - )); - }; - if !str.starts_with_ascii(b"blob:") { - return Ok(()); +pub(crate) fn revoke_object_url(url: &bun_core::String) { + if !url.starts_with_ascii(b"blob:") { + return; } - let slice = str.to_utf8(); + let slice = url.to_utf8(); let sliced = slice.slice(); if sliced.len() < b"blob:".len() + UUID::STRING_LENGTH { - return Ok(()); + return; } ObjectURLRegistry::singleton().revoke(&sliced[b"blob:".len()..]); - Ok(()) } pub(crate) fn resolve_object_url(cx: &mut JsScope<'_>, url: &bun_core::String) -> Option { diff --git a/src/runtime/webcore/fetch.bind.ts b/src/runtime/webcore/fetch.bind.ts index 8c469ba2d30d..6c7556e7f346 100644 --- a/src/runtime/webcore/fetch.bind.ts +++ b/src/runtime/webcore/fetch.bind.ts @@ -4,9 +4,15 @@ export default b.module({ rustPath: "crate::webcore::fetch" }); const js = { capability: "js", throws: true } as const; /** An `input` that is neither a `Request` nor, after `ToString`, a URL is read as `init` is. */ -const request = { args: { input: b.any, init: b.any }, returns: b.any, ...js, rejects: true } as const; +const request = { + args: { input: b.any, init: b.any }, + returns: b.any, + ...js, + rejects: true, + requiredArguments: 1, +} as const; export const fetch = b.fn(request); /** What `session.fetch` of a `Bun.FetchSession` is bound from. */ export const sessionFetch = b.fn({ ...request, passThis: true, export: "Bun__Fetch__sessionFetch" }); -export const preconnect = b.fn({ args: { url: b.string.loose }, ...js }); +export const preconnect = b.fn({ args: { url: b.string.loose }, ...js, requiredArguments: 1 }); diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index afdad65d722e..567d0172ca13 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -170,6 +170,13 @@ itAlways("mistakes in a schema are errors when it loads", () => { ".ifPassed is not for overloads", ); expect(() => b.fn({ returns: b.i32, rejects: true })).toThrow("rejects: the result is a promise"); + for (const requiredArguments of [0, 2, 0.5]) + expect(() => b.fn({ args: { a: b.i32 }, requiredArguments })).toThrow( + `requiredArguments: ${requiredArguments} is not a count of the arguments`, + ); + expect(() => b.overloads({ args: { a: b.i32 }, requiredArguments: 1 }, { args: { a: b.string } })).toThrow( + "every overload gives requiredArguments the same value", + ); const range = b.u8.validateInteger(0, 4); for (const size of [range, range.optional, range.default(0), range.optional.withValue, b.u8.enforceRange(0, 4)]) { expect(() => b.fn({ args: { size }, expected: { size: ["number"] }, capability: "heap" })).toThrow( @@ -886,6 +893,38 @@ describe("functions", () => { await expect(thrown).rejects.toThrow("value must be finite"); await expect(refused).rejects.toThrow("value must be a number"); }); + + it("requiredArguments: a call with fewer throws, and undefined that was passed is converted", async () => { + const missing = (...names: string[]) => + expect.objectContaining({ + name: "TypeError", + code: "ERR_MISSING_ARGS", + message: `The ${names.join(" and ")} argument${names.length > 1 ? "s" : ""} must be specified`, + }); + expect(() => bindgen.isPassedAfterRequired()).toThrow(missing('"first"')); + expect(bindgen.isPassedAfterRequired(undefined)).toBe(false); + expect(bindgen.isPassedAfterRequired(undefined, undefined)).toBe(true); + + expect(() => bindgen.requiredPair()).toThrow(missing('"a"', '"b"')); + expect(() => bindgen.requiredPair(1)).toThrow(missing('"a"', '"b"')); + expect(() => bindgen.requiredPair(1, undefined)).toThrow("b must be a number"); + expect(bindgen.requiredPair(1, 2)).toBe("two numbers 1 2"); + + expect(() => bindgen.requiredOfOverloads()).toThrow(missing('"text"')); + expect(() => bindgen.requiredOfOverloads("a")).toThrow("upper must be a boolean"); + expect(bindgen.requiredOfOverloads(1)).toBe("number 1"); + + const rejected = bindgen.requiredFiniteLater(); + expect(rejected).toBeInstanceOf(Promise); + await expect(rejected).rejects.toEqual(missing('"value"')); + expect(await bindgen.requiredFiniteLater(1)).toBe(1); + + const { TestCounter, TestMutates } = bindgen as any; + expect(() => new TestCounter().listen()).toThrow(missing('"listener"')); + expect(() => TestCounter.prototype.listen.call({})).toThrow(expect.objectContaining({ code: "ERR_INVALID_THIS" })); + expect(() => new TestMutates()).toThrow(missing('"count"')); + expect(() => new TestMutates(undefined)).toThrow("count must be a number"); + }); }); describe("the members of a dictionary", () => { diff --git a/test/js/web/fetch/fetch-args.test.ts b/test/js/web/fetch/fetch-args.test.ts index 522fc28739e2..097456433f35 100644 --- a/test/js/web/fetch/fetch-args.test.ts +++ b/test/js/web/fetch/fetch-args.test.ts @@ -164,7 +164,7 @@ describe.concurrent("fetch() early rejections are reported when unhandled", () = } const cases: [name: string, code: string, expectedStderr: string][] = [ - ["no arguments", `fetch()`, "fetch() URL must not be a blank string"], + ["no arguments", `fetch()`, 'The "input" argument must be specified'], ["blank url", `fetch("")`, "fetch() URL must not be a blank string"], ["invalid url", `fetch("not a url")`, "fetch() URL is invalid"], ["unsupported protocol", `fetch("gopher://example.com/")`, "protocol must be http:, https: or s3:"], diff --git a/test/js/web/fetch/fetch-session.test.ts b/test/js/web/fetch/fetch-session.test.ts index 06f873f9bf78..a547a192e5a0 100644 --- a/test/js/web/fetch/fetch-session.test.ts +++ b/test/js/web/fetch/fetch-session.test.ts @@ -739,7 +739,7 @@ describe("session.fetch", () => { test("rejects like fetch() does", async () => { using session = new Bun.FetchSession(); expect(await (session.fetch as any)().catch((e: Error) => e.message)).toBe( - "fetch() URL must not be a blank string.", + 'The "input" argument must be specified', ); using dead = await deadPort(); expect(await session.fetch(`http://127.0.0.1:${dead.port}/`).catch(e => e.code)).toBe("ECONNREFUSED"); From 59f808ff7a5f5a3e1a7b3e14d3ebacda0dbb80bf Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 20:33:58 +0000 Subject: [PATCH 21/22] Report `URL.createObjectURL()` as a wrong type, as Node does --- src/runtime/webcore/ObjectURLRegistry.bind.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/runtime/webcore/ObjectURLRegistry.bind.ts b/src/runtime/webcore/ObjectURLRegistry.bind.ts index 451875295e51..01c661008482 100644 --- a/src/runtime/webcore/ObjectURLRegistry.bind.ts +++ b/src/runtime/webcore/ObjectURLRegistry.bind.ts @@ -3,11 +3,7 @@ import { Blob, BlobOrBuildArtifact } from "./Blob.bind.ts"; export default b.module({ rustPath: "crate::webcore::object_url_registry" }); -export const createObjectURL = b.fn({ - args: { object: BlobOrBuildArtifact }, - returns: b.string, - requiredArguments: 1, -}); +export const createObjectURL = b.fn({ args: { object: BlobOrBuildArtifact }, returns: b.string }); export const revokeObjectURL = b.fn({ args: { url: b.string.allowStringObject }, From 4bd1e3a54752df330e330a9e5e17700d035f186f Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 23 Oct 2026 20:59:53 +0000 Subject: [PATCH 22/22] Count `requiredArguments` in `.length` and the TypeScript types, and refuse `.ifPassed` among them --- docs/project/bindgen.mdx | 10 ++++++--- src/codegen/bindgen-emit.ts | 10 +++++---- src/codegen/bindgen-lib-internal.ts | 25 +++++++++++----------- src/js/internal-for-testing.ts | 2 +- src/jsc/bindgen_test.bind.ts | 2 +- src/jsc/host_fn.rs | 4 +--- test/bundler/transpiler/transpiler.test.js | 5 +++++ test/internal/bindgen.test.ts | 14 +++++++----- test/js/bun/util/filesystem_router.test.ts | 7 ++++++ test/js/web/url/url.test.ts | 5 +++++ 10 files changed, 55 insertions(+), 29 deletions(-) mode change 100755 => 100644 test/js/web/url/url.test.ts diff --git a/docs/project/bindgen.mdx b/docs/project/bindgen.mdx index ca1601be37d5..95b31bdc12f4 100644 --- a/docs/project/bindgen.mdx +++ b/docs/project/bindgen.mdx @@ -175,9 +175,10 @@ throws and what the conversion of an argument throws become the reason of a reje An argument that was not passed is `undefined`, so `f()` and `f(undefined)` are the same call and the type of the argument reports it. With `requiredArguments: 1`, a call that has fewer arguments than that throws `ERR_MISSING_ARGS` -before any is converted, as WebIDL asks of a Web API: `fetch()`, `URL.revokeObjectURL()`. The message names them as -Node's does, `The "url" argument must be specified`, by the first signature of overloads. It also goes on a method -and on `construct`. +before any is converted, as WebIDL asks of a Web API: `URL.revokeObjectURL()`, and `fetch()`, which has `rejects` +and so rejects with it. The message is Node's, `The "url" argument must be specified`. It names all the +`requiredArguments`, however many the call has, as the first signature of overloads calls them. TypeScript and +`.length` count them as required. The key also goes on a method and on `construct`. ### Overloads @@ -254,6 +255,9 @@ types: each is called what it is exported as, and has to be exported. It is used schemas of the crate it is generated into. A member or an alternative also names a C++ field, so one that is a C++ keyword is refused: a member takes a `rustName`. +An array is what `Array.isArray` says is one, `Array.prototype` too, except a proxy of an array: that is an object +like any other. Only a dictionary refuses the proxy along with the array. + Only the promise calls a reaction, with the context it was given, so nothing tests that argument. `b.promisePtr` is the `*mut T` that `JSValue::then` was given. `b.nativePromiseContext` is what `take` of the cell returns: `None` if the pointer was taken back before the promise settled. No argument after it has a conversion that diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index eb3f1ba2b6f1..7afda72f1ee3 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -205,8 +205,7 @@ function layoutAssert(cppType: string, type: Type): string { function humanReadableEnumeration(type: EnumerationType): string { const quoted = type.spellings.map(([canonical]) => asciiLiteral(`"${canonical}"`).slice(1, -3)); - if (quoted.length <= 2) return quoted.join(" or "); - return `${quoted.slice(0, -1).join(", ")}, or ${quoted.at(-1)}`; + return new Intl.ListFormat("en", { type: "disjunction" }).format(quoted); } /** Value-initialization zeroes a struct whose constructor is implicit, and these hold the buffers of string views. */ @@ -1187,7 +1186,7 @@ const cppLeadingOfFunction = (fn: FunctionDefinition) => [ /** `requiredArguments`. As in Node, the message names every one of them, however many the call has. */ function cppThrowIfArgumentsMissing(signature: Signature): string { - const names = signature.args.slice(0, signature.requiredArguments ?? 0).map(arg => `"${arg.name}"`); + const names = signature.args.slice(0, signature.requiredArguments).map(arg => `"${arg.name}"`); if (names.length === 0) return ""; const message = `The ${new Intl.ListFormat("en").format(names)} argument${names.length > 1 ? "s" : ""} must be specified`; return ` if (callFrame->argumentCount() < ${names.length}) [[unlikely]] @@ -1997,7 +1996,10 @@ ${indent( export function dts(files: readonly SchemaFile[]): string { const parameters = (signature: Signature) => { - const lastRequired = signature.args.findLastIndex(arg => !arg.type.takesUndefined); + const lastRequired = Math.max( + signature.args.findLastIndex(arg => !arg.type.takesUndefined), + signature.requiredArguments - 1, + ); return signature.args .map((arg, i) => arg.type instanceof VariadicType diff --git a/src/codegen/bindgen-lib-internal.ts b/src/codegen/bindgen-lib-internal.ts index 0db7d542962e..655b6083a939 100644 --- a/src/codegen/bindgen-lib-internal.ts +++ b/src/codegen/bindgen-lib-internal.ts @@ -1662,7 +1662,7 @@ export class ArrayType extends Type { assert(!(element instanceof DefaultType), "an element of an array is never absent, so it has no default"); assert( !(withoutModifiers(element) instanceof RangeType), - "a range reports the name of its argument, which an element does not have", + "the converter of a range takes no context, which is what names an element", ); refuseOutsideArgument(element); assert( @@ -2068,7 +2068,7 @@ export class UnionType extends NamedType { assert(!cppKeywords.has(name), `${name} is a C++ keyword, and JavaScript never sees the name: pick another`); assert( !(type instanceof RangeType), - `${name}: a range reports the name of its argument, which a union does not know`, + `${name}: the converter of a range takes no context, which is what names an alternative`, ); } const chosen = new Set([...this.alternativesByKind.values()].flat()); @@ -2152,7 +2152,7 @@ export interface SignatureOptions { throws?: boolean; /** The function returns a promise: what the body or the conversion of an argument throws rejects one, and nothing is thrown. */ rejects?: boolean; - /** A call with fewer arguments throws `ERR_MISSING_ARGS`. Without it nothing counts them: one that was not passed is `undefined`. */ + /** A call with fewer arguments throws `ERR_MISSING_ARGS`. Defaults to 0: an argument that was not passed is `undefined`. */ requiredArguments?: number; /** Path of the Rust function. Defaults to the module's path plus the export's name in snake_case. */ rustPath?: string; @@ -2181,14 +2181,14 @@ export class Signature { readonly capability: Capability; readonly throws: boolean; readonly rejects: boolean; - readonly requiredArguments: number | undefined; + readonly requiredArguments: number; readonly rustPath: string | undefined; readonly length: number | undefined; /** `hasThisValue`: the body is also handed its own JS object. */ constructor(options: SignatureOptions, hasThisValue = false) { this.length = options.length; this.rejects = options.rejects ?? false; - this.requiredArguments = options.requiredArguments; + this.requiredArguments = options.requiredArguments ?? 0; const { args = {}, expected = {} } = options; this.args = Object.entries(args).map(([name, type]) => ({ name, @@ -2243,20 +2243,21 @@ export class Signature { refuseOutsideArgument(this.returns); assert(!this.rejects || this.returns.holdsJSValue, "rejects: the result is a promise"); assert( - this.requiredArguments === undefined || - (Number.isInteger(this.requiredArguments) && - this.requiredArguments >= 1 && - this.requiredArguments <= this.args.length), + Number.isInteger(this.requiredArguments) && + this.requiredArguments >= 0 && + this.requiredArguments <= this.args.length, `requiredArguments: ${this.requiredArguments} is not a count of the arguments`, ); + for (const { name, type } of this.args.slice(0, this.requiredArguments)) + assert(!isIfPassed(type), `${name}: .ifPassed is never absent in one of the requiredArguments`); assert( !this.returns.holdsJSValue || this.capability !== "none" || hasThisValue, 'returning a JS value takes capability: "heap" or "js"', ); } - /** The arguments up to the last that is not optional: what a caller writes at least. */ + /** The arguments up to the last that is not optional, and the `requiredArguments`: what a caller writes at least. */ get requiredCount(): number { - return this.args.findLastIndex(arg => !arg.type.isOptional) + 1; + return Math.max(this.args.findLastIndex(arg => !arg.type.isOptional) + 1, this.requiredArguments); } } @@ -2278,7 +2279,7 @@ function refuseOutsideArgument(type: Type, name?: string): void { assert(!refusal, name ? `${name}: ${refusal}` : (refusal ?? "")); } -/** The converter of a range takes the name and no context. */ +/** The converter of a range takes no context. */ function refuseExpectedOfRange(name: string, type: Type): void { assert(!(withoutModifiers(type) instanceof RangeType), `expected: ${name} is a range, which has its own errors`); } diff --git a/src/js/internal-for-testing.ts b/src/js/internal-for-testing.ts index f66ef946af7d..001615018a10 100644 --- a/src/js/internal-for-testing.ts +++ b/src/js/internal-for-testing.ts @@ -293,7 +293,7 @@ export const bindgen = $bindgen("bindgen_test.bind.ts", "add") && { isPassed: $bindgen("bindgen_test.bind.ts", "isPassed"), isPassedAfterRequired: $bindgen("bindgen_test.bind.ts", "isPassedAfterRequired"), requiredPair: $bindgen("bindgen_test.bind.ts", "requiredPair"), - requiredOfOverloads: $bindgen("bindgen_test.bind.ts", "requiredOfOverloads"), + requiredOverloaded: $bindgen("bindgen_test.bind.ts", "requiredOverloaded"), lengthWithValue: $bindgen("bindgen_test.bind.ts", "lengthWithValue"), counterWithValue: $bindgen("bindgen_test.bind.ts", "counterWithValue"), keptString: $bindgen("bindgen_test.bind.ts", "keptString"), diff --git a/src/jsc/bindgen_test.bind.ts b/src/jsc/bindgen_test.bind.ts index 7059c14758c8..ab4e115b8790 100644 --- a/src/jsc/bindgen_test.bind.ts +++ b/src/jsc/bindgen_test.bind.ts @@ -439,7 +439,7 @@ export const requiredPair = b.fn({ requiredArguments: 2, rustPath: "crate::bindgen_test::overloaded_two_numbers", }); -export const requiredOfOverloads = b.overloads( +export const requiredOverloaded = b.overloads( { args: { text: b.string.utf8, upper: b.bool }, returns: b.string.utf8, diff --git a/src/jsc/host_fn.rs b/src/jsc/host_fn.rs index b63ebc73766c..fb29aabf2751 100644 --- a/src/jsc/host_fn.rs +++ b/src/jsc/host_fn.rs @@ -125,12 +125,10 @@ fn debug_exception_assertion(global_this: &JSGlobalObject, value: JSValue, func: ); } -// ──────────────────────── codegen-thunk dispatch helpers ──────────────────────── +// ──────────────────────── safe codegen-thunk entry points ──────────────────────── // // The thunks of `generate-host-exports.ts` and `generate-jssink.ts` call these. -// ──────────────────────── safe codegen-thunk entry points ──────────────────────── - /// `fn(&JSGlobalObject, &CallFrame) -> JsResult`. #[track_caller] #[inline] diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 2d68d35449d2..4724a1bd2a31 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -2758,6 +2758,11 @@ export const after = { a: 1 }; }); }); + it("Array.prototype as external is an empty array", () => { + const transform = external => new Bun.Transpiler({ external }).transformSync('import "a"; 1'); + expect(transform(Array.prototype)).toBe(transform([])); + }); + it("define errors after an earlier config error don't corrupt the log", async () => { // An `external` with two `*` logs one error without aborting the constructor. // The four define-key errors that follow make the log's buffer grow. diff --git a/test/internal/bindgen.test.ts b/test/internal/bindgen.test.ts index 567d0172ca13..f9a6b9c7813b 100644 --- a/test/internal/bindgen.test.ts +++ b/test/internal/bindgen.test.ts @@ -113,7 +113,7 @@ itAlways("mistakes in a schema are errors when it loads", () => { expect(() => b.namespace({ f: { [key]: true } })).toThrow(`f: a function of a namespace has no ${key}`); } expect(() => b.array(b.null)).toThrow("an array of nothing but null"); - expect(() => b.array(b.i32.enforceRange(0, 100).nullable)).toThrow("a range reports the name of its argument"); + expect(() => b.array(b.i32.enforceRange(0, 100).nullable)).toThrow("the converter of a range takes no context"); const stringOrBuffer = b.bytes({ from: ["string", "buffer"] }); expect(() => b.array(stringOrBuffer)).toThrow("the bytes of a buffer are only for an argument"); expect(() => b.dictionary({ bytes: stringOrBuffer })).toThrow("the bytes of a buffer are only for an argument"); @@ -170,10 +170,14 @@ itAlways("mistakes in a schema are errors when it loads", () => { ".ifPassed is not for overloads", ); expect(() => b.fn({ returns: b.i32, rejects: true })).toThrow("rejects: the result is a promise"); - for (const requiredArguments of [0, 2, 0.5]) + for (const requiredArguments of [-1, 2, 0.5]) expect(() => b.fn({ args: { a: b.i32 }, requiredArguments })).toThrow( `requiredArguments: ${requiredArguments} is not a count of the arguments`, ); + expect(() => b.fn({ args: { a: b.any.ifPassed }, requiredArguments: 1 })).toThrow( + "a: .ifPassed is never absent in one of the requiredArguments", + ); + expect(b.fn({ args: { a: b.i32.optional, b: b.i32.optional }, requiredArguments: 1 }).length).toBe(1); expect(() => b.overloads({ args: { a: b.i32 }, requiredArguments: 1 }, { args: { a: b.string } })).toThrow( "every overload gives requiredArguments the same value", ); @@ -910,9 +914,9 @@ describe("functions", () => { expect(() => bindgen.requiredPair(1, undefined)).toThrow("b must be a number"); expect(bindgen.requiredPair(1, 2)).toBe("two numbers 1 2"); - expect(() => bindgen.requiredOfOverloads()).toThrow(missing('"text"')); - expect(() => bindgen.requiredOfOverloads("a")).toThrow("upper must be a boolean"); - expect(bindgen.requiredOfOverloads(1)).toBe("number 1"); + expect(() => bindgen.requiredOverloaded()).toThrow(missing('"text"')); + expect(() => bindgen.requiredOverloaded("a")).toThrow("upper must be a boolean"); + expect(bindgen.requiredOverloaded(1)).toBe("number 1"); const rejected = bindgen.requiredFiniteLater(); expect(rejected).toBeInstanceOf(Promise); diff --git a/test/js/bun/util/filesystem_router.test.ts b/test/js/bun/util/filesystem_router.test.ts index 2e22b5eddfcd..8886fb09657b 100644 --- a/test/js/bun/util/filesystem_router.test.ts +++ b/test/js/bun/util/filesystem_router.test.ts @@ -1300,6 +1300,13 @@ it("throws ERR_STRING_TOO_LONG for an origin or assetPrefix over the string limi }); }); +it("Array.prototype as fileExtensions is an empty array", () => { + const { dir } = make(["index.tsx"]); + const routes = (fileExtensions: string[]) => + Object.keys(new Bun.FileSystemRouter({ dir, style: "nextjs", fileExtensions }).routes); + expect(routes(Array.prototype)).toEqual(routes([])); +}); + it(".query and .params support names that look like array indices", () => { const { dir } = make(["posts.tsx", "[0].tsx"]); const router = new Bun.FileSystemRouter({ dir, style: "nextjs" }); diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts old mode 100755 new mode 100644 index 5683bc2c5338..b43eeb3af4b0 --- a/test/js/web/url/url.test.ts +++ b/test/js/web/url/url.test.ts @@ -620,6 +620,11 @@ describe("object URL prefix check", () => { expect(resolveObjectURL(real)).toBeUndefined(); }); + it("createObjectURL without an argument reports its type, as in Node", () => { + // @ts-expect-error + expect(() => URL.createObjectURL()).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" })); + }); + // `is_string()` admits `StringObject`, so `to_bun_string` can hit a user // `toString` that throws; that must surface as a catchable JS exception. test("revokeObjectURL propagates a throwing toString on a String object", async () => {