From f14f3b03bb97703d2c81e0a377fe84d990aaf14e Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 3 Oct 2025 17:10:28 -0700 Subject: [PATCH] Add new bindings generator; port `SSLConfig` (#23169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new generator for JS → Zig bindings. The bulk of the conversion is done in C++, after which the data is transformed into an FFI-safe representation, passed to Zig, and then finally transformed into idiomatic Zig types. In its current form, the new bindings generator supports: * Signed and unsigned integers * Floats (plus a “finite” variant that disallows NaN and infinities) * Strings * ArrayBuffer (accepts ArrayBuffer, TypedArray, or DataView) * Blob * Optional types * Nullable types (allows null, whereas Optional only allows undefined) * Arrays * User-defined string enumerations * User-defined unions (fields can optionally be named to provide a better experience in Zig) * Null and undefined, for use in unions (can more efficiently represent optional/nullable unions than wrapping a union in an optional) * User-defined dictionaries (arbitrary key-value pairs; expects a JS object and parses it into a struct) * Default values for dictionary members * Alternative names for dictionary members (e.g., to support both `serverName` and `servername` without taking up twice the space) * Descriptive error messages * Automatic `fromJS` functions in Zig for dictionaries * Automatic `deinit` functions for the generated Zig types Although this bindings generator has many features not present in `bindgen.ts`, it does not yet implement all of `bindgen.ts`'s functionality, so for the time being, it has been named `bindgenv2`, and its configuration is specified in `.bindv2.ts` files. Once all `bindgen.ts`'s functionality has been incorporated, it will be renamed. This PR ports `SSLConfig` to use the new bindings generator; see `SSLConfig.bindv2.ts`. (For internal tracking: fixes STAB-1319, STAB-1322, STAB-1323, STAB-1324) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Alistair Smith --- .gitignore | 1 + .prettierrc | 6 + build.zig | 5 + bun.lock | 12 +- cmake/Options.cmake | 5 + cmake/Sources.json | 8 + cmake/targets/BuildBun.cmake | 55 ++ package.json | 10 +- packages/bun-types/bun.d.ts | 4 - packages/bun-usockets/src/libusockets.h | 6 +- src/allocators/mimalloc.zig | 3 +- src/bake/DevServer.bind.ts | 2 +- src/bake/tsconfig.json | 1 + src/bun.js.zig | 1 + src/bun.js/Strong.zig | 2 +- src/bun.js/api/bun/socket.zig | 74 +- src/bun.js/api/bun/socket/Handlers.zig | 25 +- src/bun.js/api/bun/socket/Listener.zig | 44 +- src/bun.js/api/bun/ssl_wrapper.zig | 2 +- src/bun.js/api/server.zig | 2 +- src/bun.js/api/server/SSLConfig.bindv2.ts | 90 ++ src/bun.js/api/server/SSLConfig.zig | 871 ++++++------------ src/bun.js/api/server/ServerConfig.zig | 2 +- src/bun.js/api/sql.classes.ts | 4 +- src/bun.js/bindgen.zig | 254 +++++ src/bun.js/bindings/Bindgen.h | 11 + src/bun.js/bindings/Bindgen/ExternTraits.h | 152 +++ src/bun.js/bindings/Bindgen/ExternUnion.h | 89 ++ .../bindings/Bindgen/ExternVectorTraits.h | 149 +++ src/bun.js/bindings/Bindgen/IDLConvert.h | 19 + src/bun.js/bindings/Bindgen/IDLConvertBase.h | 74 ++ src/bun.js/bindings/Bindgen/IDLTypes.h | 31 + src/bun.js/bindings/Bindgen/Macros.h | 36 + src/bun.js/bindings/BunIDLConvert.h | 280 ++++++ src/bun.js/bindings/BunIDLConvertBase.h | 77 ++ src/bun.js/bindings/BunIDLConvertBlob.h | 36 + src/bun.js/bindings/BunIDLConvertContext.h | 252 +++++ src/bun.js/bindings/BunIDLConvertNumbers.h | 174 ++++ src/bun.js/bindings/BunIDLHumanReadable.h | 139 +++ src/bun.js/bindings/BunIDLTypes.h | 87 ++ src/bun.js/bindings/ConcatCStrings.h | 78 ++ src/bun.js/bindings/IDLTypes.h | 56 +- src/bun.js/bindings/JSValue.zig | 21 +- src/bun.js/bindings/MimallocWTFMalloc.h | 103 +++ .../bindings/{Strong.cpp => StrongRef.cpp} | 1 + src/bun.js/bindings/StrongRef.h | 23 + src/bun.js/bindings/ZigGlobalObject.cpp | 4 +- src/bun.js/bindings/bindings.cpp | 102 +- src/bun.js/bindings/headers-handwritten.h | 3 +- src/bun.js/bindings/headers.h | 2 +- src/bun.js/bindings/webcore/JSDOMConvert.h | 1 + .../bindings/webcore/JSDOMConvertBase.h | 2 + .../bindings/webcore/JSDOMConvertDictionary.h | 12 +- .../webcore/JSDOMConvertEnumeration.h | 36 + .../bindings/webcore/JSDOMConvertNullable.h | 29 + .../bindings/webcore/JSDOMConvertOptional.h | 105 +++ .../bindings/webcore/JSDOMConvertSequences.h | 356 +++++-- src/bun.js/bindings/webcore/JSDOMURL.cpp | 0 src/bun.js/jsc.zig | 3 + src/bun.js/jsc/array_buffer.zig | 48 +- src/bun.zig | 2 +- src/codegen/bindgen-lib.ts | 5 +- src/codegen/bindgenv2/internal/any.ts | 42 + src/codegen/bindgenv2/internal/array.ts | 32 + src/codegen/bindgenv2/internal/base.ts | 134 +++ src/codegen/bindgenv2/internal/dictionary.ts | 451 +++++++++ src/codegen/bindgenv2/internal/enumeration.ts | 182 ++++ src/codegen/bindgenv2/internal/interfaces.ts | 40 + src/codegen/bindgenv2/internal/optional.ts | 84 ++ src/codegen/bindgenv2/internal/primitives.ts | 125 +++ src/codegen/bindgenv2/internal/string.ts | 21 + src/codegen/bindgenv2/internal/union.ts | 185 ++++ src/codegen/bindgenv2/lib.ts | 10 + src/codegen/bindgenv2/script.ts | 185 ++++ src/codegen/bindgenv2/tsconfig.json | 11 + src/codegen/bundle-modules.ts | 4 +- src/codegen/class-definitions.ts | 4 +- src/codegen/helpers.ts | 17 +- src/codegen/replacements.ts | 4 +- src/deps/uws/SocketContext.zig | 6 +- src/meta.zig | 2 + src/meta/tagged_union.zig | 236 +++++ src/napi/napi.zig | 14 +- src/node-fallbacks/build-fallbacks.ts | 6 +- src/string.zig | 7 +- src/string/{WTFStringImpl.zig => wtf.zig} | 0 src/tsconfig.json | 5 +- test/js/bun/http/bun-server.test.ts | 4 +- test/js/bun/http/serve.test.ts | 6 +- test/js/bun/net/tcp-server.test.ts | 4 +- tsconfig.base.json | 2 +- 91 files changed, 5015 insertions(+), 895 deletions(-) create mode 100644 src/bun.js/api/server/SSLConfig.bindv2.ts create mode 100644 src/bun.js/bindgen.zig create mode 100644 src/bun.js/bindings/Bindgen.h create mode 100644 src/bun.js/bindings/Bindgen/ExternTraits.h create mode 100644 src/bun.js/bindings/Bindgen/ExternUnion.h create mode 100644 src/bun.js/bindings/Bindgen/ExternVectorTraits.h create mode 100644 src/bun.js/bindings/Bindgen/IDLConvert.h create mode 100644 src/bun.js/bindings/Bindgen/IDLConvertBase.h create mode 100644 src/bun.js/bindings/Bindgen/IDLTypes.h create mode 100644 src/bun.js/bindings/Bindgen/Macros.h create mode 100644 src/bun.js/bindings/BunIDLConvert.h create mode 100644 src/bun.js/bindings/BunIDLConvertBase.h create mode 100644 src/bun.js/bindings/BunIDLConvertBlob.h create mode 100644 src/bun.js/bindings/BunIDLConvertContext.h create mode 100644 src/bun.js/bindings/BunIDLConvertNumbers.h create mode 100644 src/bun.js/bindings/BunIDLHumanReadable.h create mode 100644 src/bun.js/bindings/BunIDLTypes.h create mode 100644 src/bun.js/bindings/ConcatCStrings.h create mode 100644 src/bun.js/bindings/MimallocWTFMalloc.h rename src/bun.js/bindings/{Strong.cpp => StrongRef.cpp} (98%) create mode 100644 src/bun.js/bindings/StrongRef.h create mode 100644 src/bun.js/bindings/webcore/JSDOMConvertOptional.h mode change 100755 => 100644 src/bun.js/bindings/webcore/JSDOMURL.cpp create mode 100644 src/codegen/bindgenv2/internal/any.ts create mode 100644 src/codegen/bindgenv2/internal/array.ts create mode 100644 src/codegen/bindgenv2/internal/base.ts create mode 100644 src/codegen/bindgenv2/internal/dictionary.ts create mode 100644 src/codegen/bindgenv2/internal/enumeration.ts create mode 100644 src/codegen/bindgenv2/internal/interfaces.ts create mode 100644 src/codegen/bindgenv2/internal/optional.ts create mode 100644 src/codegen/bindgenv2/internal/primitives.ts create mode 100644 src/codegen/bindgenv2/internal/string.ts create mode 100644 src/codegen/bindgenv2/internal/union.ts create mode 100644 src/codegen/bindgenv2/lib.ts create mode 100755 src/codegen/bindgenv2/script.ts create mode 100644 src/codegen/bindgenv2/tsconfig.json create mode 100644 src/meta/tagged_union.zig rename src/string/{WTFStringImpl.zig => wtf.zig} (100%) diff --git a/.gitignore b/.gitignore index 3f71c2acc9..b0c2fa643d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .env .envrc .eslintcache +.gdb_history .idea .next .ninja_deps diff --git a/.prettierrc b/.prettierrc index c9da1bd439..14285ca704 100644 --- a/.prettierrc +++ b/.prettierrc @@ -19,6 +19,12 @@ "options": { "printWidth": 80 } + }, + { + "files": ["src/codegen/bindgenv2/**/*.ts", "*.bindv2.ts"], + "options": { + "printWidth": 100 + } } ] } diff --git a/build.zig b/build.zig index fa76a2138c..0f45dce6e1 100644 --- a/build.zig +++ b/build.zig @@ -49,6 +49,7 @@ const BunBuildOptions = struct { enable_logs: bool = false, enable_asan: bool, enable_valgrind: bool, + use_mimalloc: bool, tracy_callstack_depth: u16, reported_nodejs_version: Version, /// To make iterating on some '@embedFile's faster, we load them at runtime @@ -97,6 +98,7 @@ const BunBuildOptions = struct { opts.addOption(bool, "enable_logs", this.enable_logs); opts.addOption(bool, "enable_asan", this.enable_asan); opts.addOption(bool, "enable_valgrind", this.enable_valgrind); + opts.addOption(bool, "use_mimalloc", this.use_mimalloc); opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{}", .{this.reported_nodejs_version})); opts.addOption(bool, "zig_self_hosted_backend", this.no_llvm); opts.addOption(bool, "override_no_export_cpp_apis", this.override_no_export_cpp_apis); @@ -270,6 +272,7 @@ pub fn build(b: *Build) !void { .enable_logs = b.option(bool, "enable_logs", "Enable logs in release") orelse false, .enable_asan = b.option(bool, "enable_asan", "Enable asan") orelse false, .enable_valgrind = b.option(bool, "enable_valgrind", "Enable valgrind") orelse false, + .use_mimalloc = b.option(bool, "use_mimalloc", "Use mimalloc as default allocator") orelse false, .llvm_codegen_threads = b.option(u32, "llvm_codegen_threads", "Number of threads to use for LLVM codegen") orelse 1, }; @@ -500,6 +503,7 @@ fn addMultiCheck( .no_llvm = root_build_options.no_llvm, .enable_asan = root_build_options.enable_asan, .enable_valgrind = root_build_options.enable_valgrind, + .use_mimalloc = root_build_options.use_mimalloc, .override_no_export_cpp_apis = root_build_options.override_no_export_cpp_apis, }; @@ -720,6 +724,7 @@ fn addInternalImports(b: *Build, mod: *Module, opts: *BunBuildOptions) void { // Generated code exposed as individual modules. inline for (.{ .{ .file = "ZigGeneratedClasses.zig", .import = "ZigGeneratedClasses" }, + .{ .file = "bindgen_generated.zig", .import = "bindgen_generated" }, .{ .file = "ResolvedSourceTag.zig", .import = "ResolvedSourceTag" }, .{ .file = "ErrorCode.zig", .import = "ErrorCode" }, .{ .file = "runtime.out.js", .enable = opts.shouldEmbedCode() }, diff --git a/bun.lock b/bun.lock index be4ab107ae..fedf606d75 100644 --- a/bun.lock +++ b/bun.lock @@ -8,14 +8,14 @@ "@lezer/cpp": "^1.1.3", "@types/bun": "workspace:*", "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8", - "esbuild": "^0.21.4", - "mitata": "^0.1.11", + "esbuild": "^0.21.5", + "mitata": "^0.1.14", "peechy": "0.4.34", - "prettier": "^3.5.3", - "prettier-plugin-organize-imports": "^4.0.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "source-map-js": "^1.2.0", + "source-map-js": "^1.2.1", "typescript": "5.9.2", }, }, @@ -284,7 +284,7 @@ "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], - "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.2.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg=="], + "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.3.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw=="], "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 1e9b664321..93a3698563 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -202,4 +202,9 @@ optionx(USE_WEBKIT_ICU BOOL "Use the ICU libraries from WebKit" DEFAULT ${DEFAUL optionx(ERROR_LIMIT STRING "Maximum number of errors to show when compiling C++ code" DEFAULT "100") +# This is not an `option` because setting this variable to OFF is experimental +# and unsupported. This replaces the `use_mimalloc` variable previously in +# bun.zig, and enables C++ code to also be aware of the option. +set(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR ON) + list(APPEND CMAKE_ARGS -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) diff --git a/cmake/Sources.json b/cmake/Sources.json index cd86d86989..5ae4930693 100644 --- a/cmake/Sources.json +++ b/cmake/Sources.json @@ -31,6 +31,14 @@ "output": "BindgenSources.txt", "paths": ["src/**/*.bind.ts"] }, + { + "output": "BindgenV2Sources.txt", + "paths": ["src/**/*.bindv2.ts"] + }, + { + "output": "BindgenV2InternalSources.txt", + "paths": ["src/codegen/bindgenv2/**/*.ts"] + }, { "output": "ZigSources.txt", "paths": ["src/**/*.zig"] diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 31b007050c..945f8074fb 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -395,6 +395,54 @@ register_command( ${BUN_BAKE_RUNTIME_OUTPUTS} ) +set(BUN_BINDGENV2_SCRIPT ${CWD}/src/codegen/bindgenv2/script.ts) + +absolute_sources(BUN_BINDGENV2_SOURCES ${CWD}/cmake/sources/BindgenV2Sources.txt) +# These sources include the script itself. +absolute_sources(BUN_BINDGENV2_INTERNAL_SOURCES + ${CWD}/cmake/sources/BindgenV2InternalSources.txt) +string(REPLACE ";" "," BUN_BINDGENV2_SOURCES_COMMA_SEPARATED + "${BUN_BINDGENV2_SOURCES}") + +execute_process( + COMMAND ${BUN_EXECUTABLE} run ${BUN_BINDGENV2_SCRIPT} + --command=list-outputs + --sources=${BUN_BINDGENV2_SOURCES_COMMA_SEPARATED} + --codegen-path=${CODEGEN_PATH} + RESULT_VARIABLE bindgen_result + OUTPUT_VARIABLE bindgen_outputs +) +if(${bindgen_result}) + message(FATAL_ERROR "bindgenv2/script.ts exited with non-zero status") +endif() +foreach(output IN LISTS bindgen_outputs) + if(output MATCHES "\.cpp$") + list(APPEND BUN_BINDGENV2_CPP_OUTPUTS ${output}) + elseif(output MATCHES "\.zig$") + list(APPEND BUN_BINDGENV2_ZIG_OUTPUTS ${output}) + else() + message(FATAL_ERROR "unexpected bindgen output: [${output}]") + endif() +endforeach() + +register_command( + TARGET + bun-bindgen-v2 + COMMENT + "Generating bindings (v2)" + COMMAND + ${BUN_EXECUTABLE} run ${BUN_BINDGENV2_SCRIPT} + --command=generate + --codegen-path=${CODEGEN_PATH} + --sources=${BUN_BINDGENV2_SOURCES_COMMA_SEPARATED} + SOURCES + ${BUN_BINDGENV2_SOURCES} + ${BUN_BINDGENV2_INTERNAL_SOURCES} + OUTPUTS + ${BUN_BINDGENV2_CPP_OUTPUTS} + ${BUN_BINDGENV2_ZIG_OUTPUTS} +) + set(BUN_BINDGEN_SCRIPT ${CWD}/src/codegen/bindgen.ts) absolute_sources(BUN_BINDGEN_SOURCES ${CWD}/cmake/sources/BindgenSources.txt) @@ -573,6 +621,7 @@ set(BUN_ZIG_GENERATED_SOURCES ${BUN_ZIG_GENERATED_CLASSES_OUTPUTS} ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_CPP_OUTPUTS} + ${BUN_BINDGENV2_ZIG_OUTPUTS} ) # In debug builds, these are not embedded, but rather referenced at runtime. @@ -636,6 +685,7 @@ register_command( -Denable_logs=$,true,false> -Denable_asan=$,true,false> -Denable_valgrind=$,true,false> + -Duse_mimalloc=$,true,false> -Dllvm_codegen_threads=${LLVM_ZIG_CODEGEN_THREADS} -Dversion=${VERSION} -Dreported_nodejs_version=${NODEJS_VERSION} @@ -712,6 +762,7 @@ list(APPEND BUN_CPP_SOURCES ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_OBJECT_LUT_OUTPUTS} ${BUN_BINDGEN_CPP_OUTPUTS} + ${BUN_BINDGENV2_CPP_OUTPUTS} ) if(WIN32) @@ -849,6 +900,10 @@ if(WIN32) ) endif() +if(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR) + target_compile_definitions(${bun} PRIVATE USE_MIMALLOC=1) +endif() + target_compile_definitions(${bun} PRIVATE _HAS_EXCEPTIONS=0 LIBUS_USE_OPENSSL=1 diff --git a/package.json b/package.json index 737d6c33f4..372a9d596e 100644 --- a/package.json +++ b/package.json @@ -11,14 +11,14 @@ "@lezer/cpp": "^1.1.3", "@types/bun": "workspace:*", "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8", - "esbuild": "^0.21.4", - "mitata": "^0.1.11", + "esbuild": "^0.21.5", + "mitata": "^0.1.14", "peechy": "0.4.34", - "prettier": "^3.5.3", - "prettier-plugin-organize-imports": "^4.0.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "source-map-js": "^1.2.0", + "source-map-js": "^1.2.1", "typescript": "5.9.2" }, "resolutions": { diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 406e3d8466..3425900c86 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -3707,10 +3707,6 @@ declare module "bun" { */ secureOptions?: number | undefined; // Value is a numeric bitmask of the `SSL_OP_*` options - keyFile?: string; - - certFile?: string; - ALPNProtocols?: string | BufferSource; ciphers?: string; diff --git a/packages/bun-usockets/src/libusockets.h b/packages/bun-usockets/src/libusockets.h index a5156f700c..0e746a0388 100644 --- a/packages/bun-usockets/src/libusockets.h +++ b/packages/bun-usockets/src/libusockets.h @@ -226,11 +226,11 @@ struct us_bun_socket_context_options_t { const char *ca_file_name; const char *ssl_ciphers; int ssl_prefer_low_memory_usage; /* Todo: rename to prefer_low_memory_usage and apply for TCP as well */ - const char **key; + const char * const *key; unsigned int key_count; - const char **cert; + const char * const *cert; unsigned int cert_count; - const char **ca; + const char * const *ca; unsigned int ca_count; unsigned int secure_options; int reject_unauthorized; diff --git a/src/allocators/mimalloc.zig b/src/allocators/mimalloc.zig index a486fe2abf..3d9e9b2e07 100644 --- a/src/allocators/mimalloc.zig +++ b/src/allocators/mimalloc.zig @@ -216,8 +216,7 @@ pub extern fn mi_new_reallocn(p: ?*anyopaque, newcount: usize, size: usize) ?*an pub const MI_SMALL_WSIZE_MAX = @as(c_int, 128); pub const MI_SMALL_SIZE_MAX = MI_SMALL_WSIZE_MAX * @import("std").zig.c_translation.sizeof(?*anyopaque); pub const MI_ALIGNMENT_MAX = (@as(c_int, 16) * @as(c_int, 1024)) * @as(c_ulong, 1024); - -const MI_MAX_ALIGN_SIZE = 16; +pub const MI_MAX_ALIGN_SIZE = 16; pub fn mustUseAlignedAlloc(alignment: std.mem.Alignment) bool { return alignment.toByteUnits() > MI_MAX_ALIGN_SIZE; diff --git a/src/bake/DevServer.bind.ts b/src/bake/DevServer.bind.ts index d56758a4a6..df89b41feb 100644 --- a/src/bake/DevServer.bind.ts +++ b/src/bake/DevServer.bind.ts @@ -1,5 +1,5 @@ // @ts-ignore -import { fn, t } from "../codegen/bindgen-lib"; +import { fn, t } from "bindgen"; export const getDeinitCountForTesting = fn({ args: {}, ret: t.usize, diff --git a/src/bake/tsconfig.json b/src/bake/tsconfig.json index 11e0dce4dd..8ab4d5832d 100644 --- a/src/bake/tsconfig.json +++ b/src/bake/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable", "DOM.AsyncIterable"], + "baseUrl": ".", "paths": { "bun-framework-react/*": ["./bun-framework-react/*"], "bindgen": ["../codegen/bindgen-lib"] diff --git a/src/bun.js.zig b/src/bun.js.zig index 13ca71282d..fb59390ea0 100644 --- a/src/bun.js.zig +++ b/src/bun.js.zig @@ -1,6 +1,7 @@ pub const jsc = @import("./bun.js/jsc.zig"); pub const webcore = @import("./bun.js/webcore.zig"); pub const api = @import("./bun.js/api.zig"); +pub const bindgen = @import("./bun.js/bindgen.zig"); pub const Run = struct { ctx: Command.Context, diff --git a/src/bun.js/Strong.zig b/src/bun.js/Strong.zig index a23b68627a..5c098b88eb 100644 --- a/src/bun.js/Strong.zig +++ b/src/bun.js/Strong.zig @@ -114,7 +114,7 @@ pub const Optional = struct { } }; -const Impl = opaque { +pub const Impl = opaque { pub fn init(global: *jsc.JSGlobalObject, value: jsc.JSValue) *Impl { jsc.markBinding(@src()); return Bun__StrongRef__new(global, value); diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index fb18864c97..8fa141fb59 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1412,23 +1412,12 @@ pub fn NewSocket(comptime ssl: bool) type { } var ssl_opts: ?jsc.API.ServerConfig.SSLConfig = null; - defer { - if (!success) { - if (ssl_opts) |*ssl_config| { - ssl_config.deinit(); - } - } - } if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (tls.isBoolean()) { - if (tls.toBoolean()) { - ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; - } - } else { - if (try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls)) |ssl_config| { - ssl_opts = ssl_config; - } + if (!tls.isBoolean()) { + ssl_opts = try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls); + } else if (tls.toBoolean()) { + ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; } } @@ -1436,9 +1425,10 @@ pub fn NewSocket(comptime ssl: bool) type { return .zero; } - if (ssl_opts == null) { + const socket_config = &(ssl_opts orelse { return globalObject.throw("Expected \"tls\" option", .{}); - } + }); + defer socket_config.deinit(); var default_data = JSValue.zero; if (try opts.fastGet(globalObject, .data)) |default_data_value| { @@ -1449,14 +1439,7 @@ pub fn NewSocket(comptime ssl: bool) type { return .zero; } - var socket_config = ssl_opts.?; - ssl_opts = null; - defer socket_config.deinit(); const options = socket_config.asUSockets(); - - const protos = socket_config.protos; - const protos_len = socket_config.protos_len; - const ext_size = @sizeOf(WrappedSocket); var handlers_ptr = bun.handleOom(bun.default_allocator.create(Handlers)); @@ -1470,8 +1453,14 @@ pub fn NewSocket(comptime ssl: bool) type { .socket = TLSSocket.Socket.detached, .connection = if (this.connection) |c| c.clone() else null, .wrapped = .tls, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p[0..protos_len])) else null, - .server_name = if (socket_config.server_name) |server_name| bun.handleOom(bun.default_allocator.dupe(u8, server_name[0..bun.len(server_name)])) else null, + .protos = if (socket_config.protos) |p| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(p))) + else + null, + .server_name = if (socket_config.server_name) |sn| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(sn))) + else + null, .socket_context = null, // only set after the wrapTLS .flags = .{ .is_active = false, @@ -1955,19 +1944,15 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C var ssl_opts: ?jsc.API.ServerConfig.SSLConfig = null; if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (tls.isBoolean()) { - if (tls.toBoolean()) { - ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; - } - } else { - if (try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls)) |ssl_config| { - ssl_opts = ssl_config; - } + if (!tls.isBoolean()) { + ssl_opts = try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), globalObject, tls); + } else if (tls.toBoolean()) { + ssl_opts = jsc.API.ServerConfig.SSLConfig.zero; } } - if (ssl_opts == null) { + const socket_config = &(ssl_opts orelse { return globalObject.throw("Expected \"tls\" option", .{}); - } + }); var default_data = JSValue.zero; if (try opts.fastGet(globalObject, .data)) |default_data_value| { @@ -1975,11 +1960,6 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C default_data.ensureStillAlive(); } - const socket_config = ssl_opts.?; - - const protos = socket_config.protos; - const protos_len = socket_config.protos_len; - const is_server = false; // A duplex socket is always handled as a client var handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); @@ -1994,8 +1974,14 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C .socket = TLSSocket.Socket.detached, .connection = null, .wrapped = .tls, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p[0..protos_len])) else null, - .server_name = if (socket_config.server_name) |server_name| bun.handleOom(bun.default_allocator.dupe(u8, server_name[0..bun.len(server_name)])) else null, + .protos = if (socket_config.protos) |p| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(p))) + else + null, + .server_name = if (socket_config.server_name) |sn| + bun.handleOom(bun.default_allocator.dupe(u8, std.mem.span(sn))) + else + null, .socket_context = null, // only set after the wrapTLS }); const tls_js_value = tls.getThisValue(globalObject); @@ -2006,7 +1992,7 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C .tls = tls, .vm = globalObject.bunVM(), .task = undefined, - .ssl_config = socket_config, + .ssl_config = socket_config.*, }); tls.ref(); diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index b87975827e..b874d9f4f2 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -210,7 +210,7 @@ pub const SocketConfig = struct { hostname_or_unix: jsc.ZigString.Slice, port: ?u16 = null, fd: ?bun.FileDescriptor = null, - ssl: ?jsc.API.ServerConfig.SSLConfig = null, + ssl: ?SSLConfig = null, handlers: Handlers, default_data: jsc.JSValue = .zero, exclusive: bool = false, @@ -246,26 +246,18 @@ pub const SocketConfig = struct { var reusePort = false; var ipv6Only = false; - var ssl: ?jsc.API.ServerConfig.SSLConfig = null; + var ssl: ?SSLConfig = null; var default_data = JSValue.zero; if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (tls.isBoolean()) { - if (tls.toBoolean()) { - ssl = jsc.API.ServerConfig.SSLConfig.zero; - } - } else { - if (try jsc.API.ServerConfig.SSLConfig.fromJS(vm, globalObject, tls)) |ssl_config| { - ssl = ssl_config; - } + if (!tls.isBoolean()) { + ssl = try SSLConfig.fromJS(vm, globalObject, tls); + } else if (tls.toBoolean()) { + ssl = SSLConfig.zero; } } - errdefer { - if (ssl != null) { - ssl.?.deinit(); - } - } + errdefer bun.memory.deinit(&ssl); hostname_or_unix: { if (try opts.getTruthy(globalObject, "fd")) |fd_| { @@ -382,9 +374,10 @@ const bun = @import("bun"); const Environment = bun.Environment; const strings = bun.strings; const uws = bun.uws; +const Listener = bun.api.Listener; +const SSLConfig = bun.api.ServerConfig.SSLConfig; const jsc = bun.jsc; const JSValue = jsc.JSValue; const ZigString = jsc.ZigString; const BinaryType = jsc.ArrayBuffer.BinaryType; -const Listener = jsc.API.Listener; diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 755bcb16b6..84afd1b5e8 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -132,7 +132,7 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa const connection: Listener.UnixOrHost = .{ .unix = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice() }; if (ssl_enabled) { if (ssl.?.protos) |p| { - protos = p[0..ssl.?.protos_len]; + protos = std.mem.span(p); } } var socket = Listener{ @@ -156,9 +156,10 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa this.* = socket; //TODO: server_name is not supported on named pipes, I belive its , lets wait for someone to ask for it + const ssl_ptr = if (ssl) |*s| s else null; this.listener = .{ // we need to add support for the backlog parameter on listen here we use the default value of nodejs - .namedPipe = WindowsNamedPipeListeningContext.listen(globalObject, pipe_name, 511, ssl, this) catch { + .namedPipe = WindowsNamedPipeListeningContext.listen(globalObject, pipe_name, 511, ssl_ptr, this) catch { this.deinit(); return globalObject.throwInvalidArguments("Failed to listen at {s}", .{pipe_name}); }, @@ -172,8 +173,8 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa } } } - const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl != null) - jsc.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |*some_ssl| + some_ssl.asUSockets() else .{}; @@ -203,7 +204,7 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa if (ssl_enabled) { if (ssl.?.protos) |p| { - protos = p[0..ssl.?.protos_len]; + protos = std.mem.span(p); } uws.NewSocketHandler(true).configure( @@ -411,10 +412,12 @@ pub fn addServerName(this: *Listener, global: *jsc.JSGlobalObject, hostname: JSV return global.throwInvalidArguments("hostname pattern cannot be empty", .{}); } - if (try jsc.API.ServerConfig.SSLConfig.fromJS(jsc.VirtualMachine.get(), global, tls)) |ssl_config| { + if (try SSLConfig.fromJS(jsc.VirtualMachine.get(), global, tls)) |ssl_config| { // to keep nodejs compatibility, we allow to replace the server name this.socket_context.?.removeServerName(true, server_name); this.socket_context.?.addServerName(true, server_name, ssl_config.asUSockets()); + var ssl_config_mut = ssl_config; + ssl_config_mut.deinit(); } return .js_undefined; @@ -569,7 +572,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock var protos: ?[]const u8 = null; var server_name: ?[]const u8 = null; const ssl_enabled = ssl != null; - defer if (ssl != null) ssl.?.deinit(); + defer if (ssl) |*some_ssl| some_ssl.deinit(); vm.eventLoop().ensureWaker(); @@ -704,8 +707,8 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock } } - const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl != null) - jsc.API.ServerConfig.SSLConfig.asUSockets(ssl.?) + const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |*some_ssl| + some_ssl.asUSockets() else .{}; @@ -726,7 +729,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock if (ssl_enabled) { if (ssl.?.protos) |p| { - protos = p[0..ssl.?.protos_len]; + protos = std.mem.span(p); } if (ssl.?.server_name) |s| { server_name = bun.handleOom(bun.default_allocator.dupe(u8, s[0..bun.len(s)])); @@ -907,7 +910,13 @@ pub const WindowsNamedPipeListeningContext = if (Environment.isWindows) struct { this.uvPipe.close(onPipeClosed); } - pub fn listen(globalThis: *jsc.JSGlobalObject, path: []const u8, backlog: i32, ssl_config: ?jsc.API.ServerConfig.SSLConfig, listener: *Listener) !*WindowsNamedPipeListeningContext { + pub fn listen( + globalThis: *jsc.JSGlobalObject, + path: []const u8, + backlog: i32, + ssl_config: ?*const SSLConfig, + listener: *Listener, + ) !*WindowsNamedPipeListeningContext { const this = WindowsNamedPipeListeningContext.new(.{ .globalThis = globalThis, .vm = globalThis.bunVM(), @@ -917,7 +926,7 @@ pub const WindowsNamedPipeListeningContext = if (Environment.isWindows) struct { if (ssl_config) |ssl_options| { bun.BoringSSL.load(); - const ctx_opts: uws.SocketContext.BunSocketContextOptions = jsc.API.ServerConfig.SSLConfig.asUSockets(ssl_options); + const ctx_opts: uws.SocketContext.BunSocketContextOptions = ssl_options.asUSockets(); var err: uws.create_bun_socket_error_t = .none; // Create SSL context using uSockets to match behavior of node.js const ctx = ctx_opts.createSSLContext(&err) orelse return error.InvalidOptions; // invalid options @@ -984,13 +993,18 @@ const bun = @import("bun"); const Async = bun.Async; const Environment = bun.Environment; const Output = bun.Output; -const api = bun.api; const default_allocator = bun.default_allocator; const strings = bun.strings; const uws = bun.uws; const BoringSSL = bun.BoringSSL.c; const uv = bun.windows.libuv; +const api = bun.api; +const Handlers = bun.api.SocketHandlers; +const TCPSocket = bun.api.TCPSocket; +const TLSSocket = bun.api.TLSSocket; +const SSLConfig = bun.api.ServerConfig.SSLConfig; + const NewSocket = api.socket.NewSocket; const SocketConfig = api.socket.SocketConfig; const WindowsNamedPipeContext = api.socket.WindowsNamedPipeContext; @@ -1000,7 +1014,3 @@ const JSGlobalObject = jsc.JSGlobalObject; const JSValue = jsc.JSValue; const ZigString = jsc.ZigString; const NodePath = jsc.Node.path; - -const Handlers = jsc.API.SocketHandlers; -const TCPSocket = jsc.API.TCPSocket; -const TLSSocket = jsc.API.TLSSocket; diff --git a/src/bun.js/api/bun/ssl_wrapper.zig b/src/bun.js/api/bun/ssl_wrapper.zig index 829c4dea43..819107fa9e 100644 --- a/src/bun.js/api/bun/ssl_wrapper.zig +++ b/src/bun.js/api/bun/ssl_wrapper.zig @@ -93,7 +93,7 @@ pub fn SSLWrapper(comptime T: type) type { pub fn init(ssl_options: jsc.API.ServerConfig.SSLConfig, is_client: bool, handlers: Handlers) !This { bun.BoringSSL.load(); - const ctx_opts: uws.SocketContext.BunSocketContextOptions = jsc.API.ServerConfig.SSLConfig.asUSockets(ssl_options); + const ctx_opts: uws.SocketContext.BunSocketContextOptions = ssl_options.asUSockets(); var err: uws.create_bun_socket_error_t = .none; // Create SSL context using uSockets to match behavior of node.js const ctx = ctx_opts.createSSLContext(&err) orelse return error.InvalidOptions; // invalid options diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index c90540d795..1448dc2e52 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -2741,7 +2741,7 @@ pub fn NewServer(protocol_enum: enum { http, https }, development_kind: enum { d // apply SNI routes if any if (this.config.sni) |*sni| { for (sni.slice()) |*sni_ssl_config| { - const sni_servername: [:0]const u8 = std.mem.span(sni_ssl_config.server_name); + const sni_servername: [:0]const u8 = std.mem.span(sni_ssl_config.server_name.?); if (sni_servername.len > 0) { app.addServerNameWithOptions(sni_servername, sni_ssl_config.asUSockets()) catch { if (!globalThis.hasException()) { diff --git a/src/bun.js/api/server/SSLConfig.bindv2.ts b/src/bun.js/api/server/SSLConfig.bindv2.ts new file mode 100644 index 0000000000..04a3f0b0f1 --- /dev/null +++ b/src/bun.js/api/server/SSLConfig.bindv2.ts @@ -0,0 +1,90 @@ +import * as b from "bindgenv2"; + +export const SSLConfigSingleFile = b.union("SSLConfigSingleFile", { + string: b.String, + buffer: b.ArrayBuffer, + file: b.Blob, +}); + +export const SSLConfigFile = b.union("SSLConfigFile", { + none: b.null, + string: b.String, + buffer: b.ArrayBuffer, + file: b.Blob, + array: b.Array(SSLConfigSingleFile), +}); + +export const ALPNProtocols = b.union("ALPNProtocols", { + none: b.null, + string: b.String, + buffer: b.ArrayBuffer, +}); + +export const SSLConfig = b.dictionary( + { + name: "SSLConfig", + userFacingName: "TLSOptions", + generateConversionFunction: true, + }, + { + passphrase: b.String.nullable, + dhParamsFile: { + type: b.String.nullable, + internalName: "dh_params_file", + }, + serverName: { + type: b.String.nullable, + internalName: "server_name", + altNames: ["servername"], + }, + lowMemoryMode: { + type: b.bool, + default: false, + internalName: "low_memory_mode", + }, + rejectUnauthorized: { + type: b.bool.nullable, + internalName: "reject_unauthorized", + }, + requestCert: { + type: b.bool, + default: false, + internalName: "request_cert", + }, + ca: SSLConfigFile, + cert: SSLConfigFile, + key: SSLConfigFile, + secureOptions: { + type: b.u32, + default: 0, + internalName: "secure_options", + }, + keyFile: { + type: b.String.nullable, + internalName: "key_file", + }, + certFile: { + type: b.String.nullable, + internalName: "cert_file", + }, + caFile: { + type: b.String.nullable, + internalName: "ca_file", + }, + ALPNProtocols: { + type: ALPNProtocols, + internalName: "alpn_protocols", + }, + ciphers: b.String.nullable, + clientRenegotiationLimit: { + type: b.u32, + default: 0, + internalName: "client_renegotiation_limit", + }, + clientRenegotiationWindow: { + type: b.u32, + default: 0, + internalName: "client_renegotiation_window", + }, + }, +); diff --git a/src/bun.js/api/server/SSLConfig.zig b/src/bun.js/api/server/SSLConfig.zig index fe309c8d4e..3d36e04b5c 100644 --- a/src/bun.js/api/server/SSLConfig.zig +++ b/src/bun.js/api/server/SSLConfig.zig @@ -1,65 +1,60 @@ const SSLConfig = @This(); -server_name: [*c]const u8 = null, +server_name: ?[*:0]const u8 = null, -key_file_name: [*c]const u8 = null, -cert_file_name: [*c]const u8 = null, +key_file_name: ?[*:0]const u8 = null, +cert_file_name: ?[*:0]const u8 = null, -ca_file_name: [*c]const u8 = null, -dh_params_file_name: [*c]const u8 = null, +ca_file_name: ?[*:0]const u8 = null, +dh_params_file_name: ?[*:0]const u8 = null, -passphrase: [*c]const u8 = null, +passphrase: ?[*:0]const u8 = null, -key: ?[][*c]const u8 = null, -key_count: u32 = 0, - -cert: ?[][*c]const u8 = null, -cert_count: u32 = 0, - -ca: ?[][*c]const u8 = null, -ca_count: u32 = 0, +key: ?[][*:0]const u8 = null, +cert: ?[][*:0]const u8 = null, +ca: ?[][*:0]const u8 = null, secure_options: u32 = 0, request_cert: i32 = 0, reject_unauthorized: i32 = 0, ssl_ciphers: ?[*:0]const u8 = null, protos: ?[*:0]const u8 = null, -protos_len: usize = 0, client_renegotiation_limit: u32 = 0, client_renegotiation_window: u32 = 0, requires_custom_request_ctx: bool = false, is_using_default_ciphers: bool = true, low_memory_mode: bool = false, -const BlobFileContentResult = struct { - data: [:0]const u8, - - fn init(comptime fieldname: []const u8, js_obj: jsc.JSValue, global: *jsc.JSGlobalObject) bun.JSError!?BlobFileContentResult { - { - const body = try jsc.WebCore.Body.Value.fromJS(global, js_obj); - if (body == .Blob and body.Blob.store != null and body.Blob.store.?.data == .file) { - var fs: jsc.Node.fs.NodeFS = .{}; - const read = fs.readFileWithOptions(.{ .path = body.Blob.store.?.data.file.pathlike }, .sync, .null_terminated); - switch (read) { - .err => { - return global.throwValue(read.err.toJS(global)); - }, - else => { - const str = read.result.null_terminated; - if (str.len > 0) { - return .{ .data = str }; - } - return global.throwInvalidArguments(std.fmt.comptimePrint("Invalid {s} file", .{fieldname}), .{}); - }, - } - } - } - - return null; - } +const ReadFromBlobError = bun.JSError || error{ + NullStore, + NotAFile, + EmptyFile, }; -pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { +fn readFromBlob( + global: *jsc.JSGlobalObject, + blob: *bun.webcore.Blob, +) ReadFromBlobError![:0]const u8 { + const store = blob.store orelse return error.NullStore; + const file = switch (store.data) { + .file => |f| f, + else => return error.NotAFile, + }; + var fs: jsc.Node.fs.NodeFS = .{}; + const maybe = fs.readFileWithOptions( + .{ .path = file.pathlike }, + .sync, + .null_terminated, + ); + const result = switch (maybe) { + .result => |result| result, + .err => |err| return global.throwValue(err.toJS(global)), + }; + if (result.null_terminated.len == 0) return error.EmptyFile; + return bun.default_allocator.dupeZ(u8, result.null_terminated); +} + +pub fn asUSockets(this: *const SSLConfig) uws.SocketContext.BunSocketContextOptions { var ctx_opts: uws.SocketContext.BunSocketContextOptions = .{}; if (this.key_file_name != null) @@ -76,15 +71,15 @@ pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { if (this.key) |key| { ctx_opts.key = key.ptr; - ctx_opts.key_count = this.key_count; + ctx_opts.key_count = @intCast(key.len); } if (this.cert) |cert| { ctx_opts.cert = cert.ptr; - ctx_opts.cert_count = this.cert_count; + ctx_opts.cert_count = @intCast(cert.len); } if (this.ca) |ca| { ctx_opts.ca = ca.ptr; - ctx_opts.ca_count = this.ca_count; + ctx_opts.ca_count = @intCast(ca.len); } if (this.ssl_ciphers != null) { @@ -96,595 +91,295 @@ pub fn asUSockets(this: SSLConfig) uws.SocketContext.BunSocketContextOptions { return ctx_opts; } -pub fn isSame(thisConfig: *const SSLConfig, otherConfig: *const SSLConfig) bool { - { //strings - const fields = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "ssl_ciphers", - "protos", - }; - - inline for (fields) |field| { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - if (lhs != null and rhs != null) { - if (!stringsEqual(lhs, rhs)) - return false; - } else if (lhs != null or rhs != null) { - return false; - } - } - } - - { - //numbers - const fields = .{ "secure_options", "request_cert", "reject_unauthorized", "low_memory_mode" }; - - inline for (fields) |field| { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - if (lhs != rhs) - return false; - } - } - - { - // complex fields - const fields = .{ "key", "ca", "cert" }; - inline for (fields) |field| { - const lhs_count = @field(thisConfig, field ++ "_count"); - const rhs_count = @field(otherConfig, field ++ "_count"); - if (lhs_count != rhs_count) - return false; - if (lhs_count > 0) { - const lhs = @field(thisConfig, field); - const rhs = @field(otherConfig, field); - for (0..lhs_count) |i| { - if (!stringsEqual(lhs.?[i], rhs.?[i])) - return false; +pub fn isSame(this: *const SSLConfig, other: *const SSLConfig) bool { + inline for (comptime std.meta.fieldNames(SSLConfig)) |field| { + const first = @field(this, field); + const second = @field(other, field); + switch (@FieldType(SSLConfig, field)) { + ?[*:0]const u8 => { + const a = first orelse return second == null; + const b = second orelse return false; + if (!stringsEqual(a, b)) return false; + }, + ?[][*:0]const u8 => { + const slice1 = first orelse return second == null; + const slice2 = second orelse return false; + if (slice1.len != slice2.len) return false; + for (slice1, slice2) |a, b| { + if (!stringsEqual(a, b)) return false; } - } + }, + else => if (first != second) return false, } } - return true; } -fn stringsEqual(a: [*c]const u8, b: [*c]const u8) bool { +fn stringsEqual(a: [*:0]const u8, b: [*:0]const u8) bool { const lhs = bun.asByteSlice(a); const rhs = bun.asByteSlice(b); return strings.eqlLong(lhs, rhs, true); } -pub fn deinit(this: *SSLConfig) void { - const fields = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "protos", - }; - - if (!this.is_using_default_ciphers) { - if (this.ssl_ciphers) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - } - - inline for (fields) |field| { - if (@field(this, field)) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - @field(this, field) = ""; - } - } - - if (this.cert) |cert| { - for (0..this.cert_count) |i| { - const slice = std.mem.span(cert[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(cert); - this.cert = null; - } - - if (this.key) |key| { - for (0..this.key_count) |i| { - const slice = std.mem.span(key[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(key); - this.key = null; - } - - if (this.ca) |ca| { - for (0..this.ca_count) |i| { - const slice = std.mem.span(ca[i]); - if (slice.len > 0) { - bun.freeSensitive(bun.default_allocator, slice); - } - } - - bun.default_allocator.free(ca); - this.ca = null; +fn freeStrings(slice: *?[][*:0]const u8) void { + const inner = slice.* orelse return; + for (inner) |string| { + bun.freeSensitive(bun.default_allocator, std.mem.span(string)); } + bun.default_allocator.free(inner); + slice.* = null; } + +fn freeString(string: *?[*:0]const u8) void { + const inner = string.* orelse return; + bun.freeSensitive(bun.default_allocator, std.mem.span(inner)); + string.* = null; +} + +pub fn deinit(this: *SSLConfig) void { + bun.meta.useAllFields(SSLConfig, .{ + .server_name = freeString(&this.server_name), + .key_file_name = freeString(&this.key_file_name), + .cert_file_name = freeString(&this.cert_file_name), + .ca_file_name = freeString(&this.ca_file_name), + .dh_params_file_name = freeString(&this.dh_params_file_name), + .passphrase = freeString(&this.passphrase), + .key = freeStrings(&this.key), + .cert = freeStrings(&this.cert), + .ca = freeStrings(&this.ca), + .secure_options = {}, + .request_cert = {}, + .reject_unauthorized = {}, + .ssl_ciphers = freeString(&this.ssl_ciphers), + .protos = freeString(&this.protos), + .client_renegotiation_limit = {}, + .client_renegotiation_window = {}, + .requires_custom_request_ctx = {}, + .is_using_default_ciphers = {}, + .low_memory_mode = {}, + }); +} + +fn cloneStrings(slice: ?[][*:0]const u8) ?[][*:0]const u8 { + const inner = slice orelse return null; + const result = bun.handleOom(bun.default_allocator.alloc([*:0]const u8, inner.len)); + for (inner, result) |string, *out| { + out.* = bun.handleOom(bun.default_allocator.dupeZ(u8, std.mem.span(string))); + } + return result; +} + +fn cloneString(string: ?[*:0]const u8) ?[*:0]const u8 { + return bun.handleOom(bun.default_allocator.dupeZ(u8, std.mem.span(string orelse return null))); +} + pub fn clone(this: *const SSLConfig) SSLConfig { - var cloned: SSLConfig = .{ + return .{ + .server_name = cloneString(this.server_name), + .key_file_name = cloneString(this.key_file_name), + .cert_file_name = cloneString(this.cert_file_name), + .ca_file_name = cloneString(this.ca_file_name), + .dh_params_file_name = cloneString(this.dh_params_file_name), + .passphrase = cloneString(this.passphrase), + .key = cloneStrings(this.key), + .cert = cloneStrings(this.cert), + .ca = cloneStrings(this.ca), .secure_options = this.secure_options, .request_cert = this.request_cert, .reject_unauthorized = this.reject_unauthorized, + .ssl_ciphers = cloneString(this.ssl_ciphers), + .protos = cloneString(this.protos), .client_renegotiation_limit = this.client_renegotiation_limit, .client_renegotiation_window = this.client_renegotiation_window, .requires_custom_request_ctx = this.requires_custom_request_ctx, .is_using_default_ciphers = this.is_using_default_ciphers, .low_memory_mode = this.low_memory_mode, - .protos_len = this.protos_len, }; - const fields_cloned_by_memcopy = .{ - "server_name", - "key_file_name", - "cert_file_name", - "ca_file_name", - "dh_params_file_name", - "passphrase", - "protos", - }; - - if (!this.is_using_default_ciphers) { - if (this.ssl_ciphers) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - if (slice.len > 0) { - cloned.ssl_ciphers = bun.handleOom(bun.default_allocator.dupeZ(u8, slice)); - } else { - cloned.ssl_ciphers = null; - } - } - } - - inline for (fields_cloned_by_memcopy) |field| { - if (@field(this, field)) |slice_ptr| { - const slice = std.mem.span(slice_ptr); - @field(cloned, field) = bun.handleOom(bun.default_allocator.dupeZ(u8, slice)); - } - } - - const array_fields_cloned_by_memcopy = .{ - "cert", - "key", - "ca", - }; - inline for (array_fields_cloned_by_memcopy) |field| { - if (@field(this, field)) |array| { - const cloned_array = bun.handleOom(bun.default_allocator.alloc([*c]const u8, @field(this, field ++ "_count"))); - @field(cloned, field) = cloned_array; - @field(cloned, field ++ "_count") = @field(this, field ++ "_count"); - for (0..@field(this, field ++ "_count")) |i| { - const slice = std.mem.span(array[i]); - if (slice.len > 0) { - cloned_array[i] = bun.handleOom(bun.default_allocator.dupeZ(u8, slice)); - } else { - cloned_array[i] = ""; - } - } - } - } - return cloned; } pub const zero = SSLConfig{}; -pub fn fromJS(vm: *jsc.VirtualMachine, global: *jsc.JSGlobalObject, obj: jsc.JSValue) bun.JSError!?SSLConfig { - var result = zero; +pub fn fromJS( + vm: *jsc.VirtualMachine, + global: *jsc.JSGlobalObject, + value: jsc.JSValue, +) bun.JSError!?SSLConfig { + var generated: jsc.generated.SSLConfig = try .fromJS(global, value); + defer generated.deinit(); + var result: SSLConfig = zero; errdefer result.deinit(); - - var arena: bun.ArenaAllocator = bun.ArenaAllocator.init(bun.default_allocator); - defer arena.deinit(); - - if (!obj.isObject()) { - return global.throwInvalidArguments("tls option expects an object", .{}); - } - var any = false; - result.reject_unauthorized = @intFromBool(vm.getTLSRejectUnauthorized()); - - // Required - if (try obj.getTruthy(global, "keyFile")) |key_file_name| { - var sliced = try key_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.key_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.key_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Unable to access keyFile path", .{}); - } - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "key")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = try js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = try js_obj.getIndex(global, @intCast(i)); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("key", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - result.requires_custom_request_ctx = true; - any = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all keys - result.key = native_array; - return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.key = native_array; - } - - result.key_count = valid_count; - } - } else if (try BlobFileContentResult.init("key", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.key = native_array; - result.key_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.key = native_array; - result.key_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.key = native_array; - return global.throwInvalidArguments("key argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - } - - if (try obj.getTruthy(global, "certFile")) |cert_file_name| { - var sliced = try cert_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.cert_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.cert_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Unable to access certFile path", .{}); - } - any = true; - result.requires_custom_request_ctx = true; - } - } - - if (try obj.getTruthy(global, "ALPNProtocols")) |protocols| { - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), protocols)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - result.protos = try bun.default_allocator.dupeZ(u8, sliced); - result.protos_len = sliced.len; - } - - any = true; - result.requires_custom_request_ctx = true; - } else { - return global.throwInvalidArguments("ALPNProtocols argument must be an string, Buffer or TypedArray", .{}); - } - } - - if (try obj.getTruthy(global, "cert")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = try js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = try js_obj.getIndex(global, @intCast(i)); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = try bun.default_allocator.dupeZ(u8, sliced); - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("cert", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - result.requires_custom_request_ctx = true; - any = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all certs - result.cert = native_array; - return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.cert = native_array; - } - - result.cert_count = valid_count; - } - } else if (try BlobFileContentResult.init("cert", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.cert = native_array; - result.cert_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.cert = native_array; - result.cert_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.cert = native_array; - return global.throwInvalidArguments("cert argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - } - - if (try obj.getBooleanStrict(global, "requestCert")) |request_cert| { - result.request_cert = if (request_cert) 1 else 0; + if (generated.passphrase.get()) |passphrase| { + result.passphrase = passphrase.toOwnedSliceZ(bun.default_allocator); any = true; } - - if (try obj.getBooleanStrict(global, "rejectUnauthorized")) |reject_unauthorized| { - result.reject_unauthorized = if (reject_unauthorized) 1 else 0; + if (generated.dh_params_file.get()) |dh_params_file| { + result.dh_params_file_name = try handlePath(global, "dhParamsFile", dh_params_file); any = true; } - - if (try obj.getTruthy(global, "ciphers")) |ssl_ciphers| { - var sliced = try ssl_ciphers.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.ssl_ciphers = try bun.default_allocator.dupeZ(u8, sliced.slice()); - result.is_using_default_ciphers = false; - any = true; - result.requires_custom_request_ctx = true; - } - } - if (result.is_using_default_ciphers) { - result.ssl_ciphers = global.bunVM().rareData().tlsDefaultCiphers() orelse null; + if (generated.server_name.get()) |server_name| { + result.server_name = server_name.toOwnedSliceZ(bun.default_allocator); + result.requires_custom_request_ctx = true; } - if (try obj.getTruthy(global, "serverName") orelse try obj.getTruthy(global, "servername")) |server_name| { - var sliced = try server_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.server_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - any = true; - result.requires_custom_request_ctx = true; - } + result.low_memory_mode = generated.low_memory_mode; + result.reject_unauthorized = @intFromBool( + generated.reject_unauthorized orelse vm.getTLSRejectUnauthorized(), + ); + result.request_cert = @intFromBool(generated.request_cert); + result.secure_options = generated.secure_options; + any = any or + result.low_memory_mode or + generated.reject_unauthorized != null or + generated.request_cert or + result.secure_options != 0; + + result.ca = try handleFileForField(global, "ca", &generated.ca); + result.cert = try handleFileForField(global, "cert", &generated.cert); + result.key = try handleFileForField(global, "key", &generated.key); + result.requires_custom_request_ctx = result.requires_custom_request_ctx or + result.ca != null or + result.cert != null or + result.key != null; + + if (generated.key_file.get()) |key_file| { + result.key_file_name = try handlePath(global, "keyFile", key_file); + result.requires_custom_request_ctx = true; + } + if (generated.cert_file.get()) |cert_file| { + result.cert_file_name = try handlePath(global, "certFile", cert_file); + result.requires_custom_request_ctx = true; + } + if (generated.ca_file.get()) |ca_file| { + result.ca_file_name = try handlePath(global, "caFile", ca_file); + result.requires_custom_request_ctx = true; } - if (try obj.getTruthy(global, "ca")) |js_obj| { - if (js_obj.jsType().isArray()) { - const count = try js_obj.getLength(global); - if (count > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, count); - - var valid_count: u32 = 0; - for (0..count) |i| { - const item = try js_obj.getIndex(global, @intCast(i)); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), item)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[valid_count] = bun.default_allocator.dupeZ(u8, sliced) catch unreachable; - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } - } else if (try BlobFileContentResult.init("ca", item, global)) |content| { - if (content.data.len > 0) { - native_array[valid_count] = content.data.ptr; - valid_count += 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - // mark and free all CA's - result.cert = native_array; - result.deinit(); - return null; - } - } else { - // mark and free all CA's - result.cert = native_array; - return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } - - if (valid_count == 0) { - bun.default_allocator.free(native_array); - } else { - result.ca = native_array; - } - - result.ca_count = valid_count; - } - } else if (try BlobFileContentResult.init("ca", js_obj, global)) |content| { - if (content.data.len > 0) { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - native_array[0] = content.data.ptr; - result.ca = native_array; - result.ca_count = 1; - any = true; - result.requires_custom_request_ctx = true; - } else { - result.deinit(); - return null; - } - } else { - const native_array = try bun.default_allocator.alloc([*c]const u8, 1); - if (try jsc.Node.StringOrBuffer.fromJS(global, arena.allocator(), js_obj)) |sb| { - defer sb.deinit(); - const sliced = sb.slice(); - if (sliced.len > 0) { - native_array[0] = try bun.default_allocator.dupeZ(u8, sliced); - any = true; - result.requires_custom_request_ctx = true; - result.ca = native_array; - result.ca_count = 1; - } else { - bun.default_allocator.free(native_array); - } - } else { - // mark and free all certs - result.ca = native_array; - return global.throwInvalidArguments("ca argument must be an string, Buffer, TypedArray, BunFile or an array containing string, Buffer, TypedArray or BunFile", .{}); - } - } + const protocols = switch (generated.alpn_protocols) { + .none => null, + .string => |*ref| ref.get().toOwnedSliceZ(bun.default_allocator), + .buffer => |*ref| blk: { + const buffer: jsc.ArrayBuffer = ref.get().asArrayBuffer(); + break :blk try bun.default_allocator.dupeZ(u8, buffer.byteSlice()); + }, + }; + if (protocols) |some_protocols| { + result.protos = some_protocols; + result.requires_custom_request_ctx = true; + } + if (generated.ciphers.get()) |ciphers| { + result.ssl_ciphers = ciphers.toOwnedSliceZ(bun.default_allocator); + result.is_using_default_ciphers = false; + result.requires_custom_request_ctx = true; } - if (try obj.getTruthy(global, "caFile")) |ca_file_name| { - var sliced = try ca_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.ca_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.ca_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Invalid caFile path", .{}); - } - } + result.client_renegotiation_limit = generated.client_renegotiation_limit; + result.client_renegotiation_window = generated.client_renegotiation_window; + any = any or + result.requires_custom_request_ctx or + result.client_renegotiation_limit != 0 or + generated.client_renegotiation_window != 0; + + // We don't need to deinit `result` if `any` is false. + return if (any) result else null; +} + +fn handlePath( + global: *jsc.JSGlobalObject, + comptime field: []const u8, + string: bun.string.WTFStringImpl, +) bun.JSError![:0]const u8 { + const name = string.toOwnedSliceZ(bun.default_allocator); + errdefer bun.freeSensitive(bun.default_allocator, name); + if (std.posix.system.access(name, std.posix.F_OK) != 0) { + return global.throwInvalidArguments( + std.fmt.comptimePrint("Unable to access {s} path", .{field}), + .{}, + ); } - // Optional - if (any) { - if (try obj.getTruthy(global, "secureOptions")) |secure_options| { - if (secure_options.isNumber()) { - result.secure_options = secure_options.toU32(); - } - } + return name; +} - if (try obj.getTruthy(global, "clientRenegotiationLimit")) |client_renegotiation_limit| { - if (client_renegotiation_limit.isNumber()) { - result.client_renegotiation_limit = client_renegotiation_limit.toU32(); - } - } +fn handleFileForField( + global: *jsc.JSGlobalObject, + comptime field: []const u8, + file: *const jsc.generated.SSLConfigFile, +) bun.JSError!?[][*:0]const u8 { + return handleFile(global, file) catch |err| switch (err) { + error.JSError => return error.JSError, + error.OutOfMemory => return error.OutOfMemory, + error.EmptyFile => return global.throwInvalidArguments( + std.fmt.comptimePrint("TLSOptions.{s} is an empty file", .{field}), + .{}, + ), + error.NullStore, error.NotAFile => return global.throwInvalidArguments( + std.fmt.comptimePrint( + "TLSOptions.{s} is not a valid BunFile (non-BunFile `Blob`s are not supported)", + .{field}, + ), + .{}, + ), + }; +} - if (try obj.getTruthy(global, "clientRenegotiationWindow")) |client_renegotiation_window| { - if (client_renegotiation_window.isNumber()) { - result.client_renegotiation_window = client_renegotiation_window.toU32(); - } - } - - if (try obj.getTruthy(global, "dhParamsFile")) |dh_params_file_name| { - var sliced = try dh_params_file_name.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.dh_params_file_name = try bun.default_allocator.dupeZ(u8, sliced.slice()); - if (std.posix.system.access(result.dh_params_file_name, std.posix.F_OK) != 0) { - return global.throwInvalidArguments("Invalid dhParamsFile path", .{}); - } - } - } - - if (try obj.getTruthy(global, "passphrase")) |passphrase| { - var sliced = try passphrase.toSlice(global, bun.default_allocator); - defer sliced.deinit(); - if (sliced.len > 0) { - result.passphrase = try bun.default_allocator.dupeZ(u8, sliced.slice()); - } - } - - if (try obj.get(global, "lowMemoryMode")) |low_memory_mode| { - if (low_memory_mode.isBoolean() or low_memory_mode.isUndefined()) { - result.low_memory_mode = low_memory_mode.toBoolean(); - any = true; - } else { - return global.throw("Expected lowMemoryMode to be a boolean", .{}); - } - } - } - - if (!any) - return null; +fn handleFile( + global: *jsc.JSGlobalObject, + file: *const jsc.generated.SSLConfigFile, +) ReadFromBlobError!?[][*:0]const u8 { + const single = try handleSingleFile(global, switch (file.*) { + .none => return null, + .string => |*ref| .{ .string = ref.get() }, + .buffer => |*ref| .{ .buffer = ref.get() }, + .file => |*ref| .{ .file = ref.get() }, + .array => |*list| return try handleFileArray(global, list.items()), + }); + errdefer bun.freeSensitive(bun.default_allocator, single); + const result = try bun.default_allocator.alloc([*:0]const u8, 1); + result[0] = single; return result; } +fn handleFileArray( + global: *jsc.JSGlobalObject, + elements: []const jsc.generated.SSLConfigSingleFile, +) ReadFromBlobError!?[][*:0]const u8 { + if (elements.len == 0) return null; + var result: bun.collections.ArrayListDefault([*:0]const u8) = try .initCapacity(elements.len); + errdefer { + for (result.items()) |string| { + bun.freeSensitive(bun.default_allocator, std.mem.span(string)); + } + result.deinit(); + } + for (elements) |*elem| { + result.appendAssumeCapacity(try handleSingleFile(global, switch (elem.*) { + .string => |*ref| .{ .string = ref.get() }, + .buffer => |*ref| .{ .buffer = ref.get() }, + .file => |*ref| .{ .file = ref.get() }, + })); + } + return try result.toOwnedSlice(); +} + +fn handleSingleFile( + global: *jsc.JSGlobalObject, + file: union(enum) { + string: bun.string.WTFStringImpl, + buffer: *jsc.JSCArrayBuffer, + file: *bun.webcore.Blob, + }, +) ReadFromBlobError![:0]const u8 { + return switch (file) { + .string => |string| string.toOwnedSliceZ(bun.default_allocator), + .buffer => |jsc_buffer| blk: { + const buffer: jsc.ArrayBuffer = jsc_buffer.asArrayBuffer(); + break :blk try bun.default_allocator.dupeZ(u8, buffer.byteSlice()); + }, + .file => |blob| try readFromBlob(global, blob), + }; +} + const std = @import("std"); const bun = @import("bun"); diff --git a/src/bun.js/api/server/ServerConfig.zig b/src/bun.js/api/server/ServerConfig.zig index 5e347924d5..8a1caca83d 100644 --- a/src/bun.js/api/server/ServerConfig.zig +++ b/src/bun.js/api/server/ServerConfig.zig @@ -931,7 +931,7 @@ pub fn fromJS( if (args.ssl_config == null) { args.ssl_config = ssl_config; } else { - if (ssl_config.server_name == null or std.mem.span(ssl_config.server_name).len == 0) { + if ((ssl_config.server_name orelse "")[0] == 0) { defer ssl_config.deinit(); return global.throwInvalidArguments("SNI tls object must have a serverName", .{}); } diff --git a/src/bun.js/api/sql.classes.ts b/src/bun.js/api/sql.classes.ts index 3fdfe17a8d..ee1405ca47 100644 --- a/src/bun.js/api/sql.classes.ts +++ b/src/bun.js/api/sql.classes.ts @@ -1,7 +1,7 @@ -import { define } from "../../codegen/class-definitions"; +import { ClassDefinition, define } from "../../codegen/class-definitions"; const types = ["PostgresSQL", "MySQL"]; -const classes = []; +const classes: ClassDefinition[] = []; for (const type of types) { classes.push( define({ diff --git a/src/bun.js/bindgen.zig b/src/bun.js/bindgen.zig new file mode 100644 index 0000000000..be303cb6ff --- /dev/null +++ b/src/bun.js/bindgen.zig @@ -0,0 +1,254 @@ +pub fn BindgenTrivial(comptime T: type) type { + return struct { + pub const ZigType = T; + pub const ExternType = T; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return extern_value; + } + }; +} + +pub const BindgenBool = BindgenTrivial(bool); +pub const BindgenU8 = BindgenTrivial(u8); +pub const BindgenI8 = BindgenTrivial(i8); +pub const BindgenU16 = BindgenTrivial(u16); +pub const BindgenI16 = BindgenTrivial(i16); +pub const BindgenU32 = BindgenTrivial(u32); +pub const BindgenI32 = BindgenTrivial(i32); +pub const BindgenU64 = BindgenTrivial(u64); +pub const BindgenI64 = BindgenTrivial(i64); +pub const BindgenF64 = BindgenTrivial(f64); +pub const BindgenRawAny = BindgenTrivial(jsc.JSValue); + +pub const BindgenStrongAny = struct { + pub const ZigType = jsc.Strong; + pub const ExternType = ?*jsc.Strong.Impl; + pub const OptionalZigType = ZigType.Optional; + pub const OptionalExternType = ExternType; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return .{ .impl = extern_value.? }; + } + + pub fn convertOptionalFromExtern(extern_value: OptionalExternType) OptionalZigType { + return .{ .impl = extern_value }; + } +}; + +/// This represents both `IDLNull` and `IDLMonostateUndefined`. +pub const BindgenNull = struct { + pub const ZigType = void; + pub const ExternType = u8; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + _ = extern_value; + } +}; + +pub fn BindgenOptional(comptime Child: type) type { + return struct { + pub const ZigType = if (@hasDecl(Child, "OptionalZigType")) + Child.OptionalZigType + else + ?Child.ZigType; + + pub const ExternType = if (@hasDecl(Child, "OptionalExternType")) + Child.OptionalExternType + else + ExternTaggedUnion(&.{ u8, Child.ExternType }); + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + if (comptime @hasDecl(Child, "OptionalExternType")) { + return Child.convertOptionalFromExtern(extern_value); + } + if (extern_value.tag == 0) { + return null; + } + bun.assert_eql(extern_value.tag, 1); + return Child.convertFromExtern(extern_value.data.@"1"); + } + }; +} + +pub const BindgenString = struct { + pub const ZigType = bun.string.WTFString; + pub const ExternType = ?bun.string.WTFStringImpl; + pub const OptionalZigType = ZigType.Optional; + pub const OptionalExternType = ExternType; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return .adopt(extern_value.?); + } + + pub fn convertOptionalFromExtern(extern_value: OptionalExternType) OptionalZigType { + return .adopt(extern_value); + } +}; + +pub fn BindgenUnion(comptime children: []const type) type { + var tagged_field_types: [children.len]type = undefined; + var untagged_field_types: [children.len]type = undefined; + for (&tagged_field_types, &untagged_field_types, children) |*tagged, *untagged, *child| { + tagged.* = child.ZigType; + untagged.* = child.ExternType; + } + + const tagged_field_types_const = tagged_field_types; + const untagged_field_types_const = untagged_field_types; + const zig_type = bun.meta.TaggedUnion(&tagged_field_types_const); + const extern_type = ExternTaggedUnion(&untagged_field_types_const); + + return struct { + pub const ZigType = zig_type; + pub const ExternType = extern_type; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + const tag: std.meta.Tag(ZigType) = @enumFromInt(extern_value.tag); + return switch (tag) { + inline else => |t| @unionInit( + ZigType, + @tagName(t), + children[@intFromEnum(t)].convertFromExtern( + @field(extern_value.data, @tagName(t)), + ), + ), + }; + } + }; +} + +pub fn ExternTaggedUnion(comptime field_types: []const type) type { + if (comptime field_types.len > std.math.maxInt(u8)) { + @compileError("too many union fields"); + } + return extern struct { + data: ExternUnion(field_types), + tag: u8, + }; +} + +fn ExternUnion(comptime field_types: []const type) type { + var info = @typeInfo(bun.meta.TaggedUnion(field_types)); + info.@"union".tag_type = null; + info.@"union".layout = .@"extern"; + info.@"union".decls = &.{}; + return @Type(info); +} + +pub fn BindgenArray(comptime Child: type) type { + return struct { + pub const ZigType = bun.collections.ArrayListDefault(Child.ZigType); + pub const ExternType = ExternArrayList(Child.ExternType); + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + const length: usize = @intCast(extern_value.length); + const capacity: usize = @intCast(extern_value.capacity); + + const data = extern_value.data orelse return .init(); + bun.assertf( + length <= capacity, + "length ({d}) should not exceed capacity ({d})", + .{ length, capacity }, + ); + var unmanaged: std.ArrayListUnmanaged(Child.ExternType) = .{ + .items = data[0..length], + .capacity = capacity, + }; + + if (comptime !bun.use_mimalloc) { + // Don't reuse memory in this case; it would be freed by the wrong allocator. + } else if (comptime Child.ZigType == Child.ExternType) { + return .fromUnmanaged(.{}, unmanaged); + } else if (comptime @sizeOf(Child.ZigType) <= @sizeOf(Child.ExternType) and + @alignOf(Child.ZigType) <= bun.allocators.mimalloc.MI_MAX_ALIGN_SIZE) + { + // We can reuse the allocation, but we still need to convert the elements. + var storage: []u8 = @ptrCast(unmanaged.allocatedSlice()); + + // Convert the elements. + for (0..length) |i| { + // Zig doesn't have a formal aliasing model, so we should be maximally + // pessimistic. + var old_elem: Child.ExternType = undefined; + @memcpy( + std.mem.asBytes(&old_elem), + storage[i * @sizeOf(Child.ExternType) ..][0..@sizeOf(Child.ExternType)], + ); + const new_elem = Child.convertFromExtern(old_elem); + @memcpy( + storage[i * @sizeOf(Child.ZigType) ..][0..@sizeOf(Child.ZigType)], + std.mem.asBytes(&new_elem), + ); + } + + const new_size_is_multiple = + comptime @sizeOf(Child.ExternType) % @sizeOf(Child.ZigType) == 0; + const new_capacity = if (comptime new_size_is_multiple) + capacity * (@sizeOf(Child.ExternType) / @sizeOf(Child.ZigType)) + else blk: { + const new_capacity = storage.len / @sizeOf(Child.ZigType); + const new_alloc_size = new_capacity * @sizeOf(Child.ZigType); + if (new_alloc_size != storage.len) { + // Allocation isn't a multiple of `@sizeOf(Child.ZigType)`; we have to + // resize it. + storage = bun.handleOom( + bun.default_allocator.realloc(storage, new_alloc_size), + ); + } + break :blk new_capacity; + }; + + const items_ptr: [*]Child.ZigType = @ptrCast(@alignCast(storage.ptr)); + const new_unmanaged: std.ArrayListUnmanaged(Child.ZigType) = .{ + .items = items_ptr[0..length], + .capacity = new_capacity, + }; + return .fromUnmanaged(.{}, new_unmanaged); + } + + defer unmanaged.deinit( + if (bun.use_mimalloc) bun.default_allocator else std.heap.raw_c_allocator, + ); + var result = bun.handleOom(ZigType.initCapacity(length)); + for (unmanaged.items) |*item| { + result.appendAssumeCapacity(Child.convertFromExtern(item.*)); + } + return result; + } + }; +} + +fn ExternArrayList(comptime Child: type) type { + return extern struct { + data: ?[*]Child, + length: c_uint, + capacity: c_uint, + }; +} + +fn BindgenExternalShared(comptime T: type) type { + return struct { + pub const ZigType = bun.ptr.ExternalShared(T); + pub const ExternType = ?*T; + pub const OptionalZigType = ZigType.Optional; + pub const OptionalExternType = ExternType; + + pub fn convertFromExtern(extern_value: ExternType) ZigType { + return .adopt(extern_value.?); + } + + pub fn convertOptionalFromExtern(extern_value: OptionalExternType) OptionalZigType { + return .adopt(extern_value); + } + }; +} + +pub const BindgenArrayBuffer = BindgenExternalShared(jsc.JSCArrayBuffer); +pub const BindgenBlob = BindgenExternalShared(webcore.Blob); + +const bun = @import("bun"); +const std = @import("std"); + +const jsc = bun.bun_js.jsc; +const webcore = bun.bun_js.webcore; diff --git a/src/bun.js/bindings/Bindgen.h b/src/bun.js/bindings/Bindgen.h new file mode 100644 index 0000000000..e1bcb166af --- /dev/null +++ b/src/bun.js/bindings/Bindgen.h @@ -0,0 +1,11 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "Bindgen/ExternTraits.h" +#include "Bindgen/IDLTypes.h" +#include "Bindgen/IDLConvertBase.h" diff --git a/src/bun.js/bindings/Bindgen/ExternTraits.h b/src/bun.js/bindings/Bindgen/ExternTraits.h new file mode 100644 index 0000000000..af9d63f8db --- /dev/null +++ b/src/bun.js/bindings/Bindgen/ExternTraits.h @@ -0,0 +1,152 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ExternUnion.h" + +namespace Bun::Bindgen { + +template +struct ExternTraits; + +template +struct TrivialExtern { + using ExternType = T; + + static ExternType convertToExtern(T&& cppValue) + { + return std::move(cppValue); + } +}; + +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; +template<> struct ExternTraits : TrivialExtern {}; + +enum ExternNullPtr : std::uint8_t {}; + +template<> struct ExternTraits { + using ExternType = ExternNullPtr; + + static ExternType convertToExtern(std::nullptr_t cppValue) + { + return ExternType { 0 }; + } +}; + +template<> struct ExternTraits { + using ExternType = ExternNullPtr; + + static ExternType convertToExtern(std::monostate cppValue) + { + return ExternType { 0 }; + } +}; + +template +struct ExternVariant { + ExternUnion data; + std::uint8_t tag; + + static_assert(sizeof...(Args) > 0); + static_assert(sizeof...(Args) - 1 <= std::numeric_limits::max()); + + explicit ExternVariant(std::variant&& variant) + : tag(static_cast(variant.index())) + { + data.initFromVariant(std::move(variant)); + } +}; + +template +struct ExternTraits> { + using ExternType = ExternVariant::ExternType...>; + + static ExternType convertToExtern(std::variant&& cppValue) + { + using VariantOfExtern = std::variant::ExternType...>; + return ExternType { std::visit([](auto&& arg) -> VariantOfExtern { + using ArgType = std::decay_t; + return { ExternTraits::convertToExtern(std::move(arg)) }; + }, + std::move(cppValue)) }; + } +}; + +template +struct ExternTraits> { + using ExternType = ExternVariant::ExternType>; + + static ExternType convertToExtern(std::optional&& cppValue) + { + using StdVariant = std::variant::ExternType>; + if (!cppValue) { + return ExternType { StdVariant { ExternNullPtr {} } }; + } + return ExternType { StdVariant { ExternTraits::convertToExtern(std::move(*cppValue)) } }; + } +}; + +template<> struct ExternTraits { + using ExternType = WTF::StringImpl*; + + static ExternType convertToExtern(WTF::String&& cppValue) + { + return cppValue.releaseImpl().leakRef(); + } +}; + +template<> struct ExternTraits { + using ExternType = JSC::EncodedJSValue; + + static ExternType convertToExtern(JSC::JSValue cppValue) + { + return JSC::JSValue::encode(cppValue); + } +}; + +template<> struct ExternTraits { + using ExternType = JSC::JSValue*; + + static ExternType convertToExtern(Bun::StrongRef&& cppValue) + { + return cppValue.release(); + } +}; + +template struct ExternTraits> { + using ExternType = T*; + + static ExternType convertToExtern(WTF::Ref&& cppValue) + { + return &cppValue.leakRef(); + } +}; + +template struct ExternTraits> { + using ExternType = T*; + + static ExternType convertToExtern(WTF::RefPtr&& cppValue) + { + return cppValue.leakRef(); + } +}; + +} diff --git a/src/bun.js/bindings/Bindgen/ExternUnion.h b/src/bun.js/bindings/Bindgen/ExternUnion.h new file mode 100644 index 0000000000..9a92950fdb --- /dev/null +++ b/src/bun.js/bindings/Bindgen/ExternUnion.h @@ -0,0 +1,89 @@ +#pragma once +#include +#include +#include +#include +#include "Macros.h" + +#define BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, ...) \ + template \ + union ExternUnion { \ + BUN_BINDGEN_DETAIL_FOREACH( \ + BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD, \ + T0 __VA_OPT__(, ) __VA_ARGS__) \ + void initFromVariant( \ + std::variant&& variant) \ + { \ + const std::size_t index = variant.index(); \ + std::visit([this, index](auto&& arg) { \ + using Arg = std::decay_t; \ + BUN_BINDGEN_DETAIL_FOREACH( \ + BUN_BINDGEN_DETAIL_EXTERN_UNION_VISIT, \ + T0 __VA_OPT__(, ) __VA_ARGS__) \ + }, \ + std::move(variant)); \ + } \ + } + +#define BUN_BINDGEN_DETAIL_EXTERN_UNION_TEMPLATE_PARAM(Type) , typename Type +#define BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD(Type) \ + static_assert(std::is_trivially_copyable_v); \ + Type alternative##Type; +#define BUN_BINDGEN_DETAIL_EXTERN_UNION_VISIT(Type) \ + if constexpr (std::is_same_v) { \ + if (index == ::Bun::Bindgen::Detail::indexOf##Type) { \ + alternative##Type = std::move(arg); \ + return; \ + } \ + } + +namespace Bun::Bindgen { +namespace Detail { +// For use in macros. +static constexpr std::size_t indexOfT0 = 0; +static constexpr std::size_t indexOfT1 = 1; +static constexpr std::size_t indexOfT2 = 2; +static constexpr std::size_t indexOfT3 = 3; +static constexpr std::size_t indexOfT4 = 4; +static constexpr std::size_t indexOfT5 = 5; +static constexpr std::size_t indexOfT6 = 6; +static constexpr std::size_t indexOfT7 = 7; +static constexpr std::size_t indexOfT8 = 8; +static constexpr std::size_t indexOfT9 = 9; +static constexpr std::size_t indexOfT10 = 10; +static constexpr std::size_t indexOfT11 = 11; +static constexpr std::size_t indexOfT12 = 12; +static constexpr std::size_t indexOfT13 = 13; +static constexpr std::size_t indexOfT14 = 14; +static constexpr std::size_t indexOfT15 = 15; +} + +template +union ExternUnion; + +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7, T8); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14); +BUN_BINDGEN_DETAIL_DEFINE_EXTERN_UNION( + T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15); +} diff --git a/src/bun.js/bindings/Bindgen/ExternVectorTraits.h b/src/bun.js/bindings/Bindgen/ExternVectorTraits.h new file mode 100644 index 0000000000..f6f62bc8f6 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/ExternVectorTraits.h @@ -0,0 +1,149 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +#if ASAN_ENABLED && __has_include() +#include +#endif + +namespace Bun::Bindgen { + +template +struct ExternTraits; + +template +struct ExternVector { + T* data; + // WTF::Vector stores the length and capacity as `unsigned`. We can save space by using that + // instead of `std::size_t` here. + unsigned length; + unsigned capacity; +}; + +namespace Detail { +template +void asanSetBufferSizeToFullCapacity(T* buffer, std::size_t length, std::size_t capacity) +{ +#if ASAN_ENABLED + // Without this, ASan will complain if Zig touches memory in the range + // [storage + length, storage + capacity), which will always happen when freeing the + // memory in Debug mode when Zig writes 0xaa to it. + __sanitizer_annotate_contiguous_container( + buffer, // beg + buffer + capacity, // end + buffer + length, // old_mid + buffer + capacity // new_mid + ); +#endif +} +} + +template +struct ExternTraits> { +private: + using CPPType = WTF::Vector; + using ExternElement = ExternTraits::ExternType; + +public: + using ExternType = ExternVector; + + static ExternType convertToExtern(CPPType&& cppValue) + { + if constexpr (std::is_same_v) { + // We can reuse the allocation. + alignas(CPPType) std::byte cppStorage[sizeof(CPPType)]; + // This prevents the contents from being freed or destructed. + CPPType* const vec = new (cppStorage) CPPType { std::move(cppValue) }; + T* const buffer = vec->mutableSpan().data(); + const std::size_t length = vec->size(); + const std::size_t capacity = vec->capacity(); + Detail::asanSetBufferSizeToFullCapacity(buffer, length, capacity); + + return ExternType { + .data = vec->mutableSpan().data(), + .length = static_cast(length), + .capacity = static_cast(capacity), + }; + } else if constexpr (sizeof(ExternElement) <= sizeof(T) + && alignof(ExternElement) <= MimallocMalloc::maxAlign) { + + // We can reuse the allocation, but we still need to convert the elements. + alignas(CPPType) std::byte cppStorage[sizeof(CPPType)]; + // Prevent the memory from being freed. + CPPType* const vec = new (cppStorage) CPPType { std::move(cppValue) }; + const std::size_t length = vec->size(); + const std::size_t capacity = vec->capacity(); + const std::size_t allocSize = capacity * sizeof(T); + + T* const buffer = vec->mutableSpan().data(); + Detail::asanSetBufferSizeToFullCapacity(buffer, length, capacity); + std::byte* storage = reinterpret_cast(buffer); + + // Convert the elements. + for (std::size_t i = 0; i < length; ++i) { + T* oldPtr = std::launder(reinterpret_cast(storage + i * sizeof(T))); + ExternElement newElem { ExternTraits::convertToExtern(std::move(*oldPtr)) }; + oldPtr->~T(); + new (storage + i * sizeof(ExternElement)) ExternElement { std::move(newElem) }; + } + + std::size_t newCapacity {}; + std::size_t newAllocSize {}; + + static constexpr bool newSizeIsMultiple = sizeof(T) % sizeof(ExternElement) == 0; + if (newSizeIsMultiple) { + newCapacity = capacity * (sizeof(T) / sizeof(ExternElement)); + newAllocSize = allocSize; + } else { + newCapacity = allocSize / sizeof(ExternElement); + newAllocSize = newCapacity * sizeof(ExternElement); + if (newAllocSize != allocSize) { + static_assert(std::is_trivially_copyable_v); + storage = static_cast( + MimallocMalloc::realloc(storage, newCapacity * sizeof(ExternElement))); + } + } + +#if __cpp_lib_start_lifetime_as >= 202207L + ExternElement* data = std::start_lifetime_as_array(storage, newCapacity); +#else + // We need to start the lifetime of an object of type "array of `capacity` + // `ExternElement`" without invalidating the object representation. Without + // `std::start_lifetime_as_array`, one way to do this is to use a no-op `memmove`, + // which implicitly creates objects, plus `std::launder` to obtain a pointer to + // the created object. + std::memmove(storage, storage, newAllocSize); + ExternElement* data = std::launder(reinterpret_cast(storage)); +#endif + return ExternType { + .data = data, + .length = static_cast(length), + .capacity = static_cast(newCapacity), + }; + } + + const std::size_t length = cppValue.size(); + const std::size_t newAllocSize = sizeof(ExternElement) * length; + ExternElement* memory = reinterpret_cast( + alignof(ExternElement) > MimallocMalloc::maxAlign + ? MimallocMalloc::alignedMalloc(newAllocSize, alignof(ExternElement)) + : MimallocMalloc::malloc(newAllocSize)); + for (std::size_t i = 0; i < length; ++i) { + new (memory + i) ExternElement { + ExternTraits::convertToExtern(std::move(cppValue[i])), + }; + } + return ExternType { + .data = memory, + .length = static_cast(length), + .capacity = static_cast(length), + }; + } +}; + +} diff --git a/src/bun.js/bindings/Bindgen/IDLConvert.h b/src/bun.js/bindings/Bindgen/IDLConvert.h new file mode 100644 index 0000000000..669b74c2d8 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/IDLConvert.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include "IDLTypes.h" +#include "IDLConvertBase.h" + +namespace Bun { +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("any"); +}; +} + +template<> struct WebCore::Converter + : WebCore::DefaultConverter { + + static Bun::StrongRef convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + return Bun::StrongRef { Bun__StrongRef__new(&globalObject, JSC::JSValue::encode(value)) }; + } +}; diff --git a/src/bun.js/bindings/Bindgen/IDLConvertBase.h b/src/bun.js/bindings/Bindgen/IDLConvertBase.h new file mode 100644 index 0000000000..e73a664c1f --- /dev/null +++ b/src/bun.js/bindings/Bindgen/IDLConvertBase.h @@ -0,0 +1,74 @@ +#pragma once +#include +#include +#include + +namespace Bun::Bindgen { + +namespace Detail { + +template +struct ContextBase : Bun::IDLConversionContextBase { + template + void throwGenericTypeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + Bun::throwError( + &global, + scope, + ErrorCode::ERR_INVALID_ARG_TYPE, + std::forward(message)); + } + + template + void throwGenericRangeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + Bun::throwError(&global, scope, ErrorCode::ERR_OUT_OF_RANGE, std::forward(message)); + } +}; + +template +struct ElementOf : ContextBase> { + using ElementContext = ElementOf>; + + explicit ElementOf(Parent parent) + : m_parent(std::move(parent)) + { + } + + auto source() + { + return WTF::makeString("element of "_s, m_parent.source()); + } + +private: + Parent m_parent; +}; + +} + +// Conversion context where the name of the value being converted is specified as an +// ASCIILiteral. Calls Bun::throwError. +struct LiteralConversionContext : Detail::ContextBase { + using ElementContext = Detail::ElementOf; + + explicit consteval LiteralConversionContext(WTF::ASCIILiteral name) + : m_name(name) + { + } + + auto source() + { + return m_name; + } + +private: + const WTF::ASCIILiteral m_name; +}; + +} diff --git a/src/bun.js/bindings/Bindgen/IDLTypes.h b/src/bun.js/bindings/Bindgen/IDLTypes.h new file mode 100644 index 0000000000..d0275d9fb0 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/IDLTypes.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include + +namespace Bun::Bindgen { + +// See also: Bun::IDLRawAny +struct IDLStrongAny : WebCore::IDLType { + using NullableType = Bun::StrongRef; + using NullableInnerParameterType = NullableType; + + static inline std::nullptr_t nullValue() { return nullptr; } + template static inline bool isNullValue(U&& value) { return !value; } + template static inline U&& extractValueFromNullable(U&& value) + { + return std::forward(value); + } +}; + +template +struct IsIDLStrongAny : std::integral_constant::value> {}; + +// Dictionaries that contain raw `JSValue`s must live on the stack. +template +struct IDLStackOnlyDictionary : WebCore::IDLType { + using SequenceStorageType = void; + using ParameterType = const T&; + using NullableParameterType = const T&; +}; + +} diff --git a/src/bun.js/bindings/Bindgen/Macros.h b/src/bun.js/bindings/Bindgen/Macros.h new file mode 100644 index 0000000000..f70d2d8120 --- /dev/null +++ b/src/bun.js/bindings/Bindgen/Macros.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include + +#define BUN_BINDGEN_DETAIL_FOREACH(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH2(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH2(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH3(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH3(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH4(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH4(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH5(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH5(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH6(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH6(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH7(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH7(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH8(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH8(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH9(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH9(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH10(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH10(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH11(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH11(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH12(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH12(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH13(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH13(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH14(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH14(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH15(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH15(macro, arg, ...) macro(arg) \ + __VA_OPT__(BUN_BINDGEN_DETAIL_FOREACH16(macro, __VA_ARGS__)) +#define BUN_BINDGEN_DETAIL_FOREACH16(macro, arg, ...) macro(arg) \ + __VA_OPT__(static_assert(false, "Bindgen/Macros.h: too many items")) diff --git a/src/bun.js/bindings/BunIDLConvert.h b/src/bun.js/bindings/BunIDLConvert.h new file mode 100644 index 0000000000..4aa4963f7f --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvert.h @@ -0,0 +1,280 @@ +#pragma once +#include "BunIDLTypes.h" +#include "BunIDLConvertBase.h" +#include "BunIDLConvertNumbers.h" +#include "BunIDLHumanReadable.h" +#include "JSDOMConvert.h" +#include +#include +#include + +template<> struct WebCore::Converter : WebCore::DefaultConverter { + static JSC::JSValue convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + return value; + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefinedOrNull()) { + return nullptr; + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNull(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefined()) { + return std::monostate {}; + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotUndefined(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isBoolean()) { + return value.asBoolean(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBoolean(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isString()) { + return value.toWTFString(&globalObject); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotString(globalObject, scope); + } +}; + +template +struct WebCore::Converter> : Bun::DefaultTryConverter> { + template + static std::optional::ImplementationType> tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (JSC::isJSArray(value)) { + return Bun::convert::Base>(globalObject, value, ctx); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.template throwNotArray(globalObject, scope); + } +}; + +template<> struct WebCore::Converter + : Bun::DefaultTryConverter { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + if (auto* jsBuffer = JSC::toUnsharedArrayBuffer(vm, value)) { + return jsBuffer; + } + if (auto* jsView = JSC::jsDynamicCast(value)) { + return jsView->unsharedBuffer(); + } + if (auto* jsDataView = JSC::jsDynamicCast(value)) { + return jsDataView->unsharedBuffer(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBufferSource(globalObject, scope); + } +}; + +template +struct WebCore::Converter> + : Bun::DefaultTryConverter> { +private: + using Base = Bun::DefaultTryConverter>; + +public: + using typename Base::ReturnType; + + static constexpr bool conversionHasSideEffects + = (WebCore::Converter::conversionHasSideEffects || ...); + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + using Last = std::tuple_element_t>; + if constexpr (requires { + WebCore::Converter::tryConvert(globalObject, value, ctx); + }) { + return Base::convert(globalObject, value, ctx); + } else { + return convertWithInfallibleLast( + globalObject, + value, + ctx, + std::make_index_sequence {}); + } + } + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + std::optional result; + auto tryAlternative = [&]() -> bool { + auto alternativeResult = Bun::tryConvertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, true); + if (!alternativeResult.has_value()) { + return false; + } + result = ReturnType { std::move(*alternativeResult) }; + return true; + }; + (tryAlternative.template operator()() || ...); + return result; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.template throwNoMatchInUnion(globalObject, scope); + } + +private: + template + static ReturnType convertWithInfallibleLast( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx, + std::index_sequence) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + std::optional result; + auto tryAlternative = [&]() -> bool { + using T = std::tuple_element_t>; + if constexpr (index == sizeof...(IDL) - 1) { + auto alternativeResult = Bun::convertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, true); + result = ReturnType { std::move(alternativeResult) }; + return true; + } else { + auto alternativeResult = Bun::tryConvertIDL(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, true); + if (!alternativeResult.has_value()) { + return false; + } + result = ReturnType { std::move(*alternativeResult) }; + return true; + } + }; + bool done = (tryAlternative.template operator()() || ...); + ASSERT(done); + if (!result.has_value()) { + // Exception + return {}; + } + return std::move(*result); + } +}; diff --git a/src/bun.js/bindings/BunIDLConvertBase.h b/src/bun.js/bindings/BunIDLConvertBase.h new file mode 100644 index 0000000000..7d53546df0 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertBase.h @@ -0,0 +1,77 @@ +#pragma once +#include "BunIDLConvertContext.h" +#include "JSDOMConvertBase.h" +#include +#include +#include +#include + +namespace Bun { + +template +typename WebCore::Converter::ReturnType convertIDL( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) +{ + if constexpr (WebCore::Converter::takesContext) { + return WebCore::Converter::convert(globalObject, value, ctx); + } else { + return WebCore::Converter::convert(globalObject, value); + } +} + +template +std::optional::ReturnType> tryConvertIDL( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) +{ + if constexpr (WebCore::Converter::takesContext) { + return WebCore::Converter::tryConvert(globalObject, value, ctx); + } else { + return WebCore::Converter::tryConvert(globalObject, value); + } +} + +template +struct DefaultContextConverter : WebCore::DefaultConverter { + using typename WebCore::DefaultConverter::ReturnType; + + static constexpr bool takesContext = true; + + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value) + { + auto ctx = DefaultConversionContext {}; + return WebCore::Converter::convert(globalObject, value, ctx); + } +}; + +template +struct DefaultTryConverter : DefaultContextConverter { + using typename DefaultContextConverter::ReturnType; + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto result = WebCore::Converter::tryConvert(globalObject, value, ctx); + RETURN_IF_EXCEPTION(scope, {}); + if (result.has_value()) { + return std::move(*result); + } + WebCore::Converter::throwConversionFailed(globalObject, scope, ctx); + return ReturnType {}; + } + + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value) + { + auto ctx = DefaultConversionContext {}; + return WebCore::Converter::tryConvert(globalObject, value, ctx); + } +}; + +} diff --git a/src/bun.js/bindings/BunIDLConvertBlob.h b/src/bun.js/bindings/BunIDLConvertBlob.h new file mode 100644 index 0000000000..2e57c6f453 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertBlob.h @@ -0,0 +1,36 @@ +#pragma once +#include "BunIDLTypes.h" +#include "BunIDLConvertBase.h" +#include "blob.h" +#include "ZigGeneratedClasses.h" + +namespace Bun { +struct IDLBlobRef : IDLBunInterface {}; +} + +template<> struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (auto* jsBlob = JSC::jsDynamicCast(value)) { + if (void* wrapped = jsBlob->wrapped()) { + return static_cast(wrapped); + } + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotBlob(globalObject, scope); + } +}; diff --git a/src/bun.js/bindings/BunIDLConvertContext.h b/src/bun.js/bindings/BunIDLConvertContext.h new file mode 100644 index 0000000000..5dde10a730 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertContext.h @@ -0,0 +1,252 @@ +#pragma once +#include "BunIDLHumanReadable.h" +#include +#include +#include + +namespace Bun { + +namespace Detail { +struct IDLConversionContextMarker {}; +} + +template +concept IDLConversionContext = std::derived_from; + +namespace Detail { +template +struct IDLUnionForDiagnostic { + using Type = IDLOrderedUnion; +}; + +template +struct IDLUnionForDiagnostic { + using Type = IDLOrderedUnion; +}; + +template +struct IDLUnionForDiagnostic { + using Type = IDLOrderedUnion; +}; +} + +template +struct IDLConversionContextBase : Detail::IDLConversionContextMarker { + void throwRequired(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeErrorWithPredicate(global, scope, "is required"_s); + } + + void throwNumberNotFinite(JSC::JSGlobalObject& global, JSC::ThrowScope& scope, double value) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString("must be finite (received "_s, value, ')')); + } + + void throwNumberNotInteger(JSC::JSGlobalObject& global, JSC::ThrowScope& scope, double value) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString("must be an integer (received "_s, value, ')')); + } + + template + void throwIntegerOutOfRange( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + Int value, + Limit min, + Limit max) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString( + "must be in the range ["_s, + min, + ", "_s, + max, + "] (received "_s, + value, + ')')); + } + + template + void throwBigIntOutOfRange( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + Limit min, + Limit max) + { + derived().throwRangeErrorWithPredicate( + global, + scope, + WTF::makeString( + "must be in the range ["_s, + min, + ", "_s, + max, + ']')); + } + + void throwNotNumber(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a number"_s); + } + + void throwNotString(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a string"_s); + } + + void throwNotBoolean(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a boolean"_s); + } + + void throwNotObject(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "an object"_s); + } + + void throwNotNull(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "null"_s); + } + + void throwNotUndefined(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "undefined"_s); + } + + void throwNotBufferSource(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "an ArrayBuffer or TypedArray"_s); + } + + void throwNotBlob(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "a Blob"_s); + } + + template + void throwNotArray(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, "an array"_s); + } + + template + void throwNotArray(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe( + global, + scope, + WTF::makeString("an array of "_s, idlHumanReadableName())); + } + + template + void throwBadEnumValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwRangeErrorWithPredicate(global, scope, "is not a valid enumeration value"_s); + } + + template + void throwBadEnumValue(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeMustBe(global, scope, idlHumanReadableName()); + } + + template + requires(sizeof...(Alternatives) > 0) + void throwNoMatchInUnion(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + using Union = Detail::IDLUnionForDiagnostic::Type; + derived().throwTypeErrorWithPredicate( + global, + scope, + WTF::makeString("must be of type "_s, idlHumanReadableName())); + } + + template + void throwNoMatchInUnion(JSC::JSGlobalObject& global, JSC::ThrowScope& scope) + { + derived().throwTypeErrorWithPredicate(global, scope, "is of an unsupported type"_s); + } + + template + void throwTypeMustBe( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& expectedNounPhrase) + { + derived().throwTypeErrorWithPredicate( + global, + scope, + WTF::makeString("must be "_s, std::forward(expectedNounPhrase))); + } + + template + void throwTypeErrorWithPredicate( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& predicate) + { + derived().throwGenericTypeError( + global, + scope, + WTF::makeString(derived().source(), ' ', std::forward(predicate))); + } + + template + void throwRangeErrorWithPredicate( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& predicate) + { + derived().throwGenericRangeError( + global, + scope, + WTF::makeString(derived().source(), ' ', std::forward(predicate))); + } + + template + void throwGenericTypeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + JSC::throwTypeError(&global, scope, std::forward(message)); + } + + template + void throwGenericRangeError( + JSC::JSGlobalObject& global, + JSC::ThrowScope& scope, + String&& message) + { + JSC::throwRangeError(&global, scope, std::forward(message)); + } + + using ElementContext = Derived; + + // When converting a sequence, the result of this function will be used as the context for + // converting each element of the sequence. + auto contextForElement() + { + return typename Derived::ElementContext { derived() }; + } + +private: + Derived& derived() { return *static_cast(this); } +}; + +// Default conversion context: throws a plain TypeError or RangeError with the message +// "value must be ...". See also Bindgen::LiteralConversionContext, which uses Bun::throwError. +struct DefaultConversionContext : IDLConversionContextBase { + WTF::ASCIILiteral source() { return "value"_s; } +}; + +} diff --git a/src/bun.js/bindings/BunIDLConvertNumbers.h b/src/bun.js/bindings/BunIDLConvertNumbers.h new file mode 100644 index 0000000000..d2e5025220 --- /dev/null +++ b/src/bun.js/bindings/BunIDLConvertNumbers.h @@ -0,0 +1,174 @@ +#pragma once +#include "BunIDLTypes.h" +#include "BunIDLConvertBase.h" +#include +#include +#include +#include +#include + +namespace Bun::Detail { +template +std::optional tryBigIntToInt(JSC::JSValue value) +{ + static constexpr std::int64_t minInt = std::numeric_limits::min(); + static constexpr std::int64_t maxInt = std::numeric_limits::max(); + using ComparisonResult = JSC::JSBigInt::ComparisonResult; + if (JSC::JSBigInt::compare(value, minInt) != ComparisonResult::LessThan + && JSC::JSBigInt::compare(value, maxInt) != ComparisonResult::GreaterThan) { + return static_cast(JSC::JSBigInt::toBigInt64(value)); + } + return std::nullopt; +} + +template +std::optional tryBigIntToInt(JSC::JSValue value) +{ + static constexpr std::uint64_t minInt = 0; + static constexpr std::uint64_t maxInt = std::numeric_limits::max(); + using ComparisonResult = JSC::JSBigInt::ComparisonResult; + if (JSC::JSBigInt::compare(value, minInt) != ComparisonResult::LessThan + && JSC::JSBigInt::compare(value, maxInt) != ComparisonResult::GreaterThan) { + return static_cast(JSC::JSBigInt::toBigUInt64(value)); + } + return std::nullopt; +} +} + +template + requires(sizeof(T) <= sizeof(std::uint64_t)) +struct WebCore::Converter> + : Bun::DefaultTryConverter> { + + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + static constexpr auto minInt = std::numeric_limits::min(); + static constexpr auto maxInt = std::numeric_limits::max(); + static constexpr auto maxSafeInteger = 9007199254740991LL; + + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (value.isInt32()) { + auto intValue = value.asInt32(); + if (intValue >= minInt && intValue <= maxInt) { + return intValue; + } + ctx.throwIntegerOutOfRange(globalObject, scope, intValue, minInt, maxInt); + return {}; + } + + using Largest = std::conditional_t, std::int64_t, std::uint64_t>; + if (value.isBigInt()) { + if (auto result = Bun::Detail::tryBigIntToInt(value)) { + return *result; + } + if constexpr (maxInt < std::numeric_limits::max()) { + if (auto result = Bun::Detail::tryBigIntToInt(value)) { + ctx.throwIntegerOutOfRange(globalObject, scope, *result, minInt, maxInt); + } + } + ctx.throwBigIntOutOfRange(globalObject, scope, minInt, maxInt); + return {}; + } + + if (!value.isNumber()) { + return std::nullopt; + } + + double number = value.asNumber(); + if (number > maxSafeInteger || number < -maxSafeInteger) { + ctx.throwNumberNotInteger(globalObject, scope, number); + return {}; + } + auto intVal = static_cast(number); + if (intVal != number) { + ctx.throwNumberNotInteger(globalObject, scope, number); + return {}; + } + if constexpr (maxInt >= static_cast(maxSafeInteger)) { + if (std::signed_integral || intVal >= 0) { + return static_cast(intVal); + } + } else if (intVal >= static_cast(minInt) + && intVal <= static_cast(maxInt)) { + return static_cast(intVal); + } + ctx.throwIntegerOutOfRange(globalObject, scope, intVal, minInt, maxInt); + return {}; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } +}; + +template<> +struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isNumber()) { + return value.asNumber(); + } + return std::nullopt; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } +}; + +template<> +struct WebCore::Converter : Bun::DefaultTryConverter { + static constexpr bool conversionHasSideEffects = false; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!value.isNumber()) { + return std::nullopt; + } + double number = value.asNumber(); + if (std::isnan(number) || std::isinf(number)) { + ctx.throwNumberNotFinite(globalObject, scope, number); + return std::nullopt; + } + return number; + } + + template + static void throwConversionFailed( + JSC::JSGlobalObject& globalObject, + JSC::ThrowScope& scope, + Ctx& ctx) + { + ctx.throwNotNumber(globalObject, scope); + } +}; diff --git a/src/bun.js/bindings/BunIDLHumanReadable.h b/src/bun.js/bindings/BunIDLHumanReadable.h new file mode 100644 index 0000000000..91e322d8d5 --- /dev/null +++ b/src/bun.js/bindings/BunIDLHumanReadable.h @@ -0,0 +1,139 @@ +#pragma once +#include "BunIDLTypes.h" +#include "ConcatCStrings.h" +#include +#include +#include + +namespace Bun { + +template +struct IDLHumanReadableName; + +template +concept HasIDLHumanReadableName = requires { IDLHumanReadableName::humanReadableName; }; + +struct BaseIDLHumanReadableName { + static constexpr bool isDisjunction = false; + static constexpr bool hasPreposition = false; +}; + +template +static constexpr WTF::ASCIILiteral idlHumanReadableName() +{ + static_assert(IDLHumanReadableName::humanReadableName.back() == '\0'); + return WTF::ASCIILiteral::fromLiteralUnsafe( + IDLHumanReadableName::humanReadableName.data()); +} + +namespace Detail { +template +static constexpr auto nestedHumanReadableName() +{ + static constexpr auto& name = IDLHumanReadableName::humanReadableName; + if constexpr (IDLHumanReadableName::isDisjunction) { + return Bun::concatCStrings("<", name, ">"); + } else { + return name; + } +} + +template +static constexpr auto separatorForHumanReadableBinaryDisjunction() +{ + if constexpr (IDLHumanReadableName::hasPreposition) { + return std::to_array(", or "); + } else { + return std::to_array(" or "); + } +} +} + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("null"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("undefined"); +}; + +template + requires std::derived_from +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("boolean"); +}; + +template + requires WebCore::IsIDLInteger::value +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("integer"); +}; + +template + requires WebCore::IsIDLFloatingPoint::value +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("number"); +}; + +template + requires WebCore::IsIDLString::value +struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("string"); +}; + +// Will generally be overridden by each specific enumeration type. +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("enumeration (string)"); +}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = true; + static constexpr auto humanReadableName = Bun::concatCStrings( + Detail::nestedHumanReadableName(), + Detail::separatorForHumanReadableBinaryDisjunction(), + "null"); +}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = true; + static constexpr auto humanReadableName = Bun::concatCStrings( + Detail::nestedHumanReadableName(), + Detail::separatorForHumanReadableBinaryDisjunction(), + "undefined"); +}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool hasPreposition = true; + static constexpr auto humanReadableName + = Bun::concatCStrings("array of ", Detail::nestedHumanReadableName()); +}; + +// Will generally be overridden by each specific dictionary type. +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("dictionary (object)"); +}; + +template +struct IDLHumanReadableName> : IDLHumanReadableName {}; + +template +struct IDLHumanReadableName> : BaseIDLHumanReadableName { + static constexpr bool isDisjunction = sizeof...(IDL) > 1; + static constexpr auto humanReadableName + = Bun::joinCStringsAsList(Detail::nestedHumanReadableName()...); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("ArrayBuffer"); +}; + +template<> struct IDLHumanReadableName : BaseIDLHumanReadableName { + static constexpr auto humanReadableName = std::to_array("Blob"); +}; + +} diff --git a/src/bun.js/bindings/BunIDLTypes.h b/src/bun.js/bindings/BunIDLTypes.h new file mode 100644 index 0000000000..aab971072b --- /dev/null +++ b/src/bun.js/bindings/BunIDLTypes.h @@ -0,0 +1,87 @@ +#pragma once +#include "IDLTypes.h" +#include +#include +#include +#include +#include + +namespace WTF { +struct CrashOnOverflow; +} + +namespace Bun { + +struct MimallocMalloc; + +// Like `IDLAny`, but always stored as a raw `JSValue`. This should only be +// used in contexts where the `JSValue` will be stored on the stack. +struct IDLRawAny : WebCore::IDLType { + // Storage in a sequence is explicitly unsupported, as this would create a + // `Vector`, whose contents are invisible to the GC. + using SequenceStorageType = void; + using NullableType = JSC::JSValue; + using NullableParameterType = JSC::JSValue; + using NullableInnerParameterType = JSC::JSValue; + static NullableType nullValue() { return JSC::jsUndefined(); } + static bool isNullValue(const NullableType& value) { return value.isUndefined(); } + static ImplementationType extractValueFromNullable(const NullableType& value) { return value; } + static constexpr auto humanReadableName() { return std::to_array("any"); } +}; + +// For use in unions, to represent a nullable union. +struct IDLStrictNull : WebCore::IDLType { + static constexpr auto humanReadableName() { return std::to_array("null"); } +}; + +// For use in unions, to represent an optional union. +struct IDLStrictUndefined : WebCore::IDLType { + static constexpr auto humanReadableName() { return std::to_array("undefined"); } +}; + +template +struct IDLStrictInteger : WebCore::IDLInteger {}; +struct IDLStrictDouble : WebCore::IDLUnrestrictedDouble {}; +struct IDLFiniteDouble : WebCore::IDLDouble {}; +struct IDLStrictBoolean : WebCore::IDLBoolean {}; +struct IDLStrictString : WebCore::IDLDOMString {}; + +template +struct IDLOrderedUnion : WebCore::IDLType> {}; + +namespace Detail { +template +using IDLMimallocSequence = WebCore::IDLSequence< + IDL, + WTF::Vector< + typename IDL::SequenceStorageType, + 0, + WTF::CrashOnOverflow, + 16, + MimallocMalloc>>; +} + +template +struct IDLArray : Detail::IDLMimallocSequence { + using Base = Detail::IDLMimallocSequence; +}; + +template> +struct IDLBunInterface : WebCore::IDLType, RefDerefTraits>> { + using NullableType = WTF::RefPtr, RefDerefTraits>; + using NullableInnerParameterType = NullableType; + + static inline std::nullptr_t nullValue() { return nullptr; } + template static inline bool isNullValue(U&& value) { return !value; } + template static inline U&& extractValueFromNullable(U&& value) + { + return std::forward(value); + } +}; + +struct IDLArrayBufferRef : IDLBunInterface {}; + +// Defined in BunIDLConvertBlob.h +struct IDLBlobRef; + +} diff --git a/src/bun.js/bindings/ConcatCStrings.h b/src/bun.js/bindings/ConcatCStrings.h new file mode 100644 index 0000000000..2b0a65cd73 --- /dev/null +++ b/src/bun.js/bindings/ConcatCStrings.h @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include +#include + +namespace Bun { + +namespace Detail { +template +static constexpr bool isCharArray = false; + +template +static constexpr bool isCharArray = true; + +template +static constexpr bool isCharArray> = true; + +// Intentionally not defined, to force consteval to fail. +void stringIsNotNullTerminated(); +} + +template + requires(Detail::isCharArray> && ...) +consteval auto concatCStrings(T&&... nullTerminatedCharArrays) +{ + std::array) - 1) + ...) + 1> result; + auto it = result.begin(); + auto append = [&it](auto&& arg) { + if (std::end(arg)[-1] != '\0') { + // This will cause consteval to fail. + Detail::stringIsNotNullTerminated(); + } + it = std::copy(std::begin(arg), std::end(arg) - 1, it); + }; + (append(nullTerminatedCharArrays), ...); + result.back() = '\0'; + return result; +} + +namespace Detail { +template +consteval auto listSeparatorForIndex() +{ + if constexpr (length == 2) { + return std::to_array(" or "); + } else if constexpr (index == length - 1) { + return std::to_array(", or "); + } else { + return std::to_array(", "); + } +} + +template +consteval auto joinCStringsAsList(std::index_sequence, T&& first, Rest&&... rest) +{ + return concatCStrings( + first, + concatCStrings( + listSeparatorForIndex(), + std::forward(rest))...); +} +} + +template + requires(Detail::isCharArray> && ...) +consteval auto joinCStringsAsList(T&&... nullTerminatedCharArrays) +{ + if constexpr (sizeof...(T) == 0) { + return std::to_array(""); + } else { + return Detail::joinCStringsAsList( + std::make_index_sequence {}, + std::forward(nullTerminatedCharArrays)...); + } +} + +} diff --git a/src/bun.js/bindings/IDLTypes.h b/src/bun.js/bindings/IDLTypes.h index 3ebea7e596..4e9cbd03c0 100644 --- a/src/bun.js/bindings/IDLTypes.h +++ b/src/bun.js/bindings/IDLTypes.h @@ -28,6 +28,7 @@ #include "StringAdaptors.h" #include #include +#include #include #include #include @@ -76,6 +77,7 @@ struct IDLType { static NullableType nullValue() { return std::nullopt; } static bool isNullValue(const NullableType& value) { return !value; } static ImplementationType extractValueFromNullable(const NullableType& value) { return value.value(); } + static ImplementationType extractValueFromNullable(NullableType&& value) { return std::move(value.value()); } template using NullableTypeWithLessPadding = Markable; template @@ -84,6 +86,8 @@ struct IDLType { static bool isNullType(const NullableTypeWithLessPadding& value) { return !value; } template static ImplementationType extractValueFromNullable(const NullableTypeWithLessPadding& value) { return value.value(); } + template + static ImplementationType extractValueFromNullable(NullableTypeWithLessPadding&& value) { return std::move(value.value()); } }; // IDLUnsupportedType is a special type that serves as a base class for currently unsupported types. @@ -94,8 +98,12 @@ struct IDLUnsupportedType : IDLType { struct IDLNull : IDLType { }; +// See also: Bun::IDLRawAny, Bun::Bindgen::IDLStrongAny struct IDLAny : IDLType> { - using SequenceStorageType = JSC::JSValue; + // SequenceStorageType must be left as JSC::Strong; otherwise + // IDLSequence would yield a Vector, whose contents + // are invisible to the GC. + // [do not uncomment] using SequenceStorageType = JSC::JSValue; using ParameterType = JSC::JSValue; using NullableParameterType = JSC::JSValue; @@ -247,18 +255,23 @@ template struct IDLNullable : IDLType { template static inline auto extractValueFromNullable(U&& value) -> decltype(T::extractValueFromNullable(std::forward(value))) { return T::extractValueFromNullable(std::forward(value)); } }; -template struct IDLSequence : IDLType> { - using InnerType = T; - - using ParameterType = const Vector&; - using NullableParameterType = const std::optional>&; +// Like `IDLNullable`, but does not permit `null`, only `undefined`. +template struct IDLOptional : IDLNullable { }; -template struct IDLFrozenArray : IDLType> { +template> +struct IDLSequence : IDLType { using InnerType = T; - using ParameterType = const Vector&; - using NullableParameterType = const std::optional>&; + using ParameterType = const VectorType&; + using NullableParameterType = const std::optional&; +}; + +template struct IDLFrozenArray : IDLType> { + using InnerType = T; + + using ParameterType = const Vector&; + using NullableParameterType = const std::optional>&; }; template struct IDLRecord : IDLType>> { @@ -282,6 +295,31 @@ template struct IDLUnion : IDLType> { using TypeList = brigand::list; + // If `SequenceStorageType` and `ImplementationType` are different for any + // type in `Ts`, this union should not be allowed to be stored in a + // sequence. Sequence elements are stored on the heap (in a `Vector`), so + // if `SequenceStorageType` and `ImplementationType` differ for some type, + // this is an indication that the `ImplementationType` should not be stored + // on the heap (e.g., because it is or contains a raw `JSValue`). When this + // is the case, we indicate that the union itself should not be stored on + // the heap by defining its `SequenceStorageType` as void. + // + // Note that we cannot define `SequenceStorageType` as + // `std::variant`, as this would cause + // sequence conversion to fail to compile, because + // `std::variant` is not convertible to + // `std::variant`. + // + // A potential avenue for future work would be to extend the IDL type + // traits interface to allow defining custom conversions from + // `ImplementationType` to `SequenceStorageType`, and to properly propagate + // `SequenceStorageType` in other types like `IDLDictionary`; however, one + // should keep in mind that some types may still disallow heap storage + // entirely by defining `SequenceStorageType` as void. + using SequenceStorageType = std::conditional_t< + (std::is_same_v && ...), + std::variant, + void>; using ParameterType = const std::variant&; using NullableParameterType = const std::optional>&; }; diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index a8afc337f4..76b66be73b 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -1133,25 +1133,16 @@ pub const JSValue = enum(i64) { return bun.cpp.JSC__JSValue__toMatch(this, global, other); } - extern fn JSC__JSValue__asArrayBuffer_(this: JSValue, global: *JSGlobalObject, out: *ArrayBuffer) bool; - pub fn asArrayBuffer_(this: JSValue, global: *JSGlobalObject, out: *ArrayBuffer) bool { - return JSC__JSValue__asArrayBuffer_(this, global, out); - } + extern fn JSC__JSValue__asArrayBuffer(this: JSValue, global: *JSGlobalObject, out: *ArrayBuffer) bool; pub fn asArrayBuffer(this: JSValue, global: *JSGlobalObject) ?ArrayBuffer { - var out: ArrayBuffer = .{ - .offset = 0, - .len = 0, - .byte_len = 0, - .shared = false, - .typed_array_type = .Uint8Array, - }; - - if (this.asArrayBuffer_(global, &out)) { - out.value = this; + var out: ArrayBuffer = undefined; + // `ptr` might not get set if the ArrayBuffer is empty, so make sure it starts out with a + // defined value. + out.ptr = &.{}; + if (JSC__JSValue__asArrayBuffer(this, global, &out)) { return out; } - return null; } extern fn JSC__JSValue__fromInt64NoTruncate(globalObject: *JSGlobalObject, i: i64) JSValue; diff --git a/src/bun.js/bindings/MimallocWTFMalloc.h b/src/bun.js/bindings/MimallocWTFMalloc.h new file mode 100644 index 0000000000..32e43713ba --- /dev/null +++ b/src/bun.js/bindings/MimallocWTFMalloc.h @@ -0,0 +1,103 @@ +#pragma once +#include +#include +#include +#include +#include +#include "mimalloc.h" +#include "mimalloc/types.h" + +namespace Bun { +// For use with WTF types like WTF::Vector. +struct MimallocMalloc { +#if USE(MIMALLOC) + static constexpr std::size_t maxAlign = MI_MAX_ALIGN_SIZE; +#else + static constexpr std::size_t maxAlign = alignof(std::max_align_t); +#endif + + static void* malloc(std::size_t size) + { + void* result = tryMalloc(size); + if (!result) CRASH(); + return result; + } + + static void* tryMalloc(std::size_t size) + { +#if USE(MIMALLOC) + return mi_malloc(size); +#else + return std::malloc(size); +#endif + } + + static void* zeroedMalloc(std::size_t size) + { + void* result = tryZeroedMalloc(size); + if (!result) CRASH(); + return result; + } + + static void* tryZeroedMalloc(std::size_t size) + { +#if USE(MIMALLOC) + return mi_zalloc(size); +#else + return std::calloc(size, 1); +#endif + } + + static void* alignedMalloc(std::size_t size, std::size_t alignment) + { + void* result = tryAlignedMalloc(size, alignment); + if (!result) CRASH(); + return result; + } + + static void* tryAlignedMalloc(std::size_t size, std::size_t alignment) + { + ASSERT(alignment > 0); + ASSERT((alignment & (alignment - 1)) == 0); // ensure power of two + ASSERT(((alignment - 1) & size) == 0); // ensure size multiple of alignment +#if USE(MIMALLOC) + return mi_malloc_aligned(size, alignment); +#elif !OS(WINDOWS) + return std::aligned_alloc(alignment, size); +#else + LOG_ERROR("cannot allocate memory with alignment %zu", alignment); + return nullptr; +#endif + } + + static void* realloc(void* p, std::size_t size) + { + void* result = tryRealloc(p, size); + if (!result) CRASH(); + return result; + } + + static void* tryRealloc(void* p, std::size_t size) + { +#if USE(MIMALLOC) + return mi_realloc(p, size); +#else + return std::realloc(p, size); +#endif + } + + static void free(void* p) + { +#if USE(MIMALLOC) + mi_free(p); +#else + std::free(p); +#endif + } + + static constexpr ALWAYS_INLINE std::size_t nextCapacity(std::size_t capacity) + { + return std::max(capacity + capacity / 2, capacity + 1); + } +}; +} diff --git a/src/bun.js/bindings/Strong.cpp b/src/bun.js/bindings/StrongRef.cpp similarity index 98% rename from src/bun.js/bindings/Strong.cpp rename to src/bun.js/bindings/StrongRef.cpp index d4ce228f4a..8466df5cfa 100644 --- a/src/bun.js/bindings/Strong.cpp +++ b/src/bun.js/bindings/StrongRef.cpp @@ -1,4 +1,5 @@ #include "root.h" +#include "StrongRef.h" #include #include #include "BunClientData.h" diff --git a/src/bun.js/bindings/StrongRef.h b/src/bun.js/bindings/StrongRef.h new file mode 100644 index 0000000000..9726d6895a --- /dev/null +++ b/src/bun.js/bindings/StrongRef.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include + +extern "C" void Bun__StrongRef__delete(JSC::JSValue* _Nonnull handleSlot); +extern "C" JSC::JSValue* Bun__StrongRef__new(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); +extern "C" JSC::EncodedJSValue Bun__StrongRef__get(JSC::JSValue* _Nonnull handleSlot); +extern "C" void Bun__StrongRef__set(JSC::JSValue* _Nonnull handleSlot, JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue); +extern "C" void Bun__StrongRef__clear(JSC::JSValue* _Nonnull handleSlot); + +namespace Bun { + +struct StrongRefDeleter { + // `std::unique_ptr` will never call this with a null pointer. + void operator()(JSC::JSValue* _Nonnull handleSlot) + { + Bun__StrongRef__delete(handleSlot); + } +}; + +using StrongRef = std::unique_ptr; + +} diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 21141e1935..76d2538f59 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -58,6 +58,7 @@ #include "AddEventListenerOptions.h" #include "AsyncContextFrame.h" #include "BunClientData.h" +#include "BunIDLConvert.h" #include "BunObject.h" #include "GeneratedBunObject.h" #include "BunPlugin.h" @@ -2080,10 +2081,9 @@ extern "C" bool ReadableStream__tee(JSC::EncodedJSValue possibleReadableStream, RETURN_IF_EXCEPTION(scope, false); if (!returnedValue) return false; - auto results = Detail::SequenceConverter::convert(*lexicalGlobalObject, *returnedValue); + auto results = convert>>(*lexicalGlobalObject, *returnedValue); RETURN_IF_EXCEPTION(scope, false); - ASSERT(results.size() == 2); *possibleReadableStream1 = JSValue::encode(results[0]); *possibleReadableStream2 = JSValue::encode(results[1]); return true; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index df1ddfe46e..e41c9d6979 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -32,6 +32,7 @@ #include "WebCoreJSBuiltins.h" #include "JavaScriptCore/AggregateError.h" +#include "JavaScriptCore/ArrayBufferView.h" #include "JavaScriptCore/BytecodeIndex.h" #include "JavaScriptCore/CodeBlock.h" #include "JavaScriptCore/Completion.h" @@ -3023,16 +3024,19 @@ JSC::EncodedJSValue JSC__JSValue__values(JSC::JSGlobalObject* globalObject, JSC: return JSValue::encode(JSC::objectValues(vm, globalObject, value)); } -bool JSC__JSValue__asArrayBuffer_(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, - Bun__ArrayBuffer* arg2) +bool JSC__JSValue__asArrayBuffer( + JSC::EncodedJSValue encodedValue, + JSC::JSGlobalObject* globalObject, + Bun__ArrayBuffer* out) { - ASSERT_NO_PENDING_EXCEPTION(arg1); - JSC::JSValue value = JSC::JSValue::decode(JSValue0); + ASSERT_NO_PENDING_EXCEPTION(globalObject); + JSC::JSValue value = JSC::JSValue::decode(encodedValue); if (!value || !value.isCell()) [[unlikely]] { return false; } auto type = value.asCell()->type(); + void* data = nullptr; switch (type) { case JSC::JSType::Uint8ArrayType: @@ -3048,60 +3052,56 @@ bool JSC__JSValue__asArrayBuffer_(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObj case JSC::JSType::Float64ArrayType: case JSC::JSType::BigInt64ArrayType: case JSC::JSType::BigUint64ArrayType: { - JSC::JSArrayBufferView* typedArray = JSC::jsCast(value); - arg2->len = typedArray->length(); - arg2->byte_len = typedArray->byteLength(); - // the offset is already set by vector() - // https://github.com/oven-sh/bun/issues/561 - arg2->offset = 0; - arg2->cell_type = type; - arg2->ptr = (char*)typedArray->vectorWithoutPACValidation(); - arg2->_value = JSValue::encode(value); - return true; + JSC::JSArrayBufferView* view = JSC::jsCast(value); + data = view->vector(); + out->len = view->length(); + out->byte_len = view->byteLength(); + out->cell_type = type; + out->shared = view->isShared(); + break; } case JSC::JSType::ArrayBufferType: { - JSC::ArrayBuffer* typedArray = JSC::jsCast(value)->impl(); - arg2->len = typedArray->byteLength(); - arg2->byte_len = typedArray->byteLength(); - arg2->offset = 0; - arg2->cell_type = JSC::JSType::ArrayBufferType; - arg2->ptr = (char*)typedArray->data(); - arg2->shared = typedArray->isShared(); - arg2->_value = JSValue::encode(value); - return true; + JSC::ArrayBuffer* buffer = JSC::jsCast(value)->impl(); + data = buffer->data(); + out->len = buffer->byteLength(); + out->byte_len = buffer->byteLength(); + out->cell_type = JSC::JSType::ArrayBufferType; + out->shared = buffer->isShared(); + break; } case JSC::JSType::ObjectType: case JSC::JSType::FinalObjectType: { if (JSC::JSArrayBufferView* view = JSC::jsDynamicCast(value)) { - arg2->len = view->length(); - arg2->byte_len = view->byteLength(); - arg2->offset = 0; - arg2->cell_type = view->type(); - arg2->ptr = (char*)view->vectorWithoutPACValidation(); - arg2->_value = JSValue::encode(value); - return true; - } - - if (JSC::JSArrayBuffer* jsBuffer = JSC::jsDynamicCast(value)) { + data = view->vector(); + out->len = view->length(); + out->byte_len = view->byteLength(); + out->cell_type = view->type(); + out->shared = view->isShared(); + } else if (JSC::JSArrayBuffer* jsBuffer = JSC::jsDynamicCast(value)) { JSC::ArrayBuffer* buffer = jsBuffer->impl(); if (!buffer) return false; - arg2->len = buffer->byteLength(); - arg2->byte_len = buffer->byteLength(); - arg2->offset = 0; - arg2->cell_type = JSC::JSType::ArrayBufferType; - arg2->ptr = (char*)buffer->data(); - arg2->_value = JSValue::encode(value); - return true; + data = buffer->data(); + out->len = buffer->byteLength(); + out->byte_len = buffer->byteLength(); + out->cell_type = JSC::JSType::ArrayBufferType; + out->shared = buffer->isShared(); + } else { + return false; } break; } default: { - break; + return false; } } - - return false; + out->_value = JSValue::encode(value); + if (data) { + // Avoid setting `ptr` to null; the corresponding Zig field is a non-optional pointer. + // The caller should have already set `ptr` to a zero-length array. + out->ptr = static_cast(data); + } + return true; } CPP_DECL JSC::EncodedJSValue JSC__JSValue__createEmptyArray(JSC::JSGlobalObject* arg0, size_t length) @@ -6885,3 +6885,19 @@ CPP_DECL [[ZIG_EXPORT(nothrow)]] unsigned int Bun__CallFrame__getLineNumber(JSC: return lineColumn.line; } + +extern "C" void JSC__ArrayBuffer__ref(JSC::ArrayBuffer* self) { self->ref(); } +extern "C" void JSC__ArrayBuffer__deref(JSC::ArrayBuffer* self) { self->deref(); } +extern "C" void JSC__ArrayBuffer__asBunArrayBuffer(JSC::ArrayBuffer* self, Bun__ArrayBuffer* out) +{ + const std::size_t byteLength = self->byteLength(); + if (void* data = self->data()) { + // Avoid setting `ptr` to null; it's a non-optional pointer in Zig. + out->ptr = static_cast(data); + } + out->len = byteLength; + out->byte_len = byteLength; + out->_value = 0; + out->cell_type = JSC::JSType::ArrayBufferType; + out->shared = self->isShared(); +} diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 5020140860..b5f56ffa0a 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -322,11 +322,10 @@ BunString toStringView(WTF::StringView view); typedef struct { char* ptr; - size_t offset; size_t len; size_t byte_len; - uint8_t cell_type; int64_t _value; + uint8_t cell_type; bool shared; } Bun__ArrayBuffer; diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h index f02d203054..59c5a0b4a0 100644 --- a/src/bun.js/bindings/headers.h +++ b/src/bun.js/bindings/headers.h @@ -195,7 +195,7 @@ CPP_DECL uint32_t JSC__JSMap__size(JSC::JSMap* arg0, JSC::JSGlobalObject* arg1); #pragma mark - JSC::JSValue CPP_DECL void JSC__JSValue__then(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue JSValue2, SYSV_ABI JSC::EncodedJSValue(* ArgFn3)(JSC::JSGlobalObject* arg0, JSC::CallFrame* arg1), SYSV_ABI JSC::EncodedJSValue(* ArgFn4)(JSC::JSGlobalObject* arg0, JSC::CallFrame* arg1)); -CPP_DECL bool JSC__JSValue__asArrayBuffer_(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, Bun__ArrayBuffer* arg2); +CPP_DECL bool JSC__JSValue__asArrayBuffer(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, Bun__ArrayBuffer* arg2); CPP_DECL unsigned char JSC__JSValue__asBigIntCompare(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* arg1, JSC::EncodedJSValue JSValue2); CPP_DECL JSC::JSCell* JSC__JSValue__asCell(JSC::EncodedJSValue JSValue0); CPP_DECL JSC::JSInternalPromise* JSC__JSValue__asInternalPromise(JSC::EncodedJSValue JSValue0); diff --git a/src/bun.js/bindings/webcore/JSDOMConvert.h b/src/bun.js/bindings/webcore/JSDOMConvert.h index 24c95835e2..c8ebcbac44 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvert.h +++ b/src/bun.js/bindings/webcore/JSDOMConvert.h @@ -39,6 +39,7 @@ #include "JSDOMConvertNullable.h" #include "JSDOMConvertNumbers.h" #include "JSDOMConvertObject.h" +#include "JSDOMConvertOptional.h" #include "JSDOMConvertRecord.h" #include "JSDOMConvertSequences.h" #include "JSDOMConvertSerializedScriptValue.h" diff --git a/src/bun.js/bindings/webcore/JSDOMConvertBase.h b/src/bun.js/bindings/webcore/JSDOMConvertBase.h index cd4b872c28..05233c9cc3 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertBase.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertBase.h @@ -268,6 +268,8 @@ template struct DefaultConverter { // is something having a converter that does JSC::JSValue::toBoolean. // toBoolean() in JS can't call arbitrary functions. static constexpr bool conversionHasSideEffects = true; + + static constexpr bool takesContext = false; }; // Conversion from JSValue -> Implementation for variadic arguments diff --git a/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h b/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h index 87f1a1c468..305063eb34 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertDictionary.h @@ -31,11 +31,21 @@ namespace WebCore { // Specialized by generated code for IDL dictionary conversion. -template T convertDictionary(JSC::JSGlobalObject&, JSC::JSValue); +template T convertDictionary(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value); template struct Converter> : DefaultConverter> { using ReturnType = T; + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value) + { + if (value.isObject()) { + return convert(lexicalGlobalObject, value); + } + return std::nullopt; + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) { return convertDictionary(lexicalGlobalObject, value); diff --git a/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h b/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h index 71df8e0298..52ef8d2bf9 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h @@ -28,6 +28,7 @@ #include "IDLTypes.h" #include "JSDOMConvertBase.h" #include "JSDOMGlobalObject.h" +#include "BunIDLConvertBase.h" namespace WebCore { @@ -41,7 +42,42 @@ template ASCIILiteral expectedEnumerationValues(); template JSC::JSString* convertEnumerationToJS(JSC::JSGlobalObject&, T); template struct Converter> : DefaultConverter> { + static constexpr bool takesContext = true; + + // `tryConvert` for enumerations is strict: it returns null if the value is not a string. + template + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isString()) { + return parseEnumeration(lexicalGlobalObject, value); + } + return std::nullopt; + } + + // When converting with Context, the conversion is stricter: non-strings are disallowed. + template + static T convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto throwScope = DECLARE_THROW_SCOPE(vm); + if (!value.isString()) { + ctx.throwNotString(lexicalGlobalObject, throwScope); + return {}; + } + auto result = parseEnumeration(lexicalGlobalObject, value); + RETURN_IF_EXCEPTION(throwScope, {}); + if (result.has_value()) { + return std::move(*result); + } + ctx.template throwBadEnumValue>(lexicalGlobalObject, throwScope); + return {}; + } + template + requires(!Bun::IDLConversionContext>) static T convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower = ExceptionThrower()) { auto& vm = JSC::getVM(&lexicalGlobalObject); diff --git a/src/bun.js/bindings/webcore/JSDOMConvertNullable.h b/src/bun.js/bindings/webcore/JSDOMConvertNullable.h index 549d126bb4..40821ca8d7 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertNullable.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertNullable.h @@ -30,6 +30,7 @@ #include "JSDOMConvertInterface.h" #include "JSDOMConvertNumbers.h" #include "JSDOMConvertStrings.h" +#include "BunIDLConvertBase.h" namespace WebCore { @@ -58,6 +59,10 @@ struct NullableConversionType { template struct Converter> : DefaultConverter> { using ReturnType = typename Detail::NullableConversionType::Type; + static constexpr bool conversionHasSideEffects = WebCore::Converter::conversionHasSideEffects; + + static constexpr bool takesContext = true; + // 1. If Type(V) is not Object, and the conversion to an IDL value is being performed // due to V being assigned to an attribute whose type is a nullable callback function // that is annotated with [LegacyTreatNonObjectAsNull], then return the IDL nullable @@ -68,6 +73,29 @@ template struct Converter> : DefaultConverter + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefinedOrNull()) + return T::nullValue(); + auto result = Bun::tryConvertIDL(lexicalGlobalObject, value, ctx); + if (result.has_value()) { + return std::move(*result); + } + return std::nullopt; + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + if (value.isUndefinedOrNull()) + return T::nullValue(); + return Bun::convertIDL(lexicalGlobalObject, value, ctx); + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) { if (value.isUndefinedOrNull()) @@ -87,6 +115,7 @@ template struct Converter> : DefaultConverter::convert(lexicalGlobalObject, value, globalObject); } template + requires(!Bun::IDLConversionContext>) static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower) { if (value.isUndefinedOrNull()) diff --git a/src/bun.js/bindings/webcore/JSDOMConvertOptional.h b/src/bun.js/bindings/webcore/JSDOMConvertOptional.h new file mode 100644 index 0000000000..6052fd9ff6 --- /dev/null +++ b/src/bun.js/bindings/webcore/JSDOMConvertOptional.h @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2016 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "IDLTypes.h" +#include "JSDOMConvertNullable.h" + +namespace WebCore { + +template struct Converter> : DefaultConverter> { + using ReturnType = typename Converter>::ReturnType; + + static constexpr bool conversionHasSideEffects = WebCore::Converter::conversionHasSideEffects; + + static constexpr bool takesContext = true; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& lexicalGlobalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (value.isUndefined()) + return T::nullValue(); + auto result = Bun::tryConvertIDL(lexicalGlobalObject, value, ctx); + if (result.has_value()) { + return std::move(*result); + } + return std::nullopt; + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + if (value.isUndefined()) + return T::nullValue(); + return Bun::convertIDL(lexicalGlobalObject, value, ctx); + } + + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value); + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSC::JSObject& thisObject) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, thisObject); + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSDOMGlobalObject& globalObject) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, globalObject); + } + template + requires(!Bun::IDLConversionContext>) + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, std::forward(exceptionThrower)); + } + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSC::JSObject& thisObject, ExceptionThrower&& exceptionThrower) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, thisObject, std::forward(exceptionThrower)); + } + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, JSDOMGlobalObject& globalObject, ExceptionThrower&& exceptionThrower) + { + if (value.isUndefined()) + return T::nullValue(); + return Converter::convert(lexicalGlobalObject, value, globalObject, std::forward(exceptionThrower)); + } +}; + +} // namespace WebCore diff --git a/src/bun.js/bindings/webcore/JSDOMConvertSequences.h b/src/bun.js/bindings/webcore/JSDOMConvertSequences.h index 36818b187a..8b93310323 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertSequences.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertSequences.h @@ -33,43 +33,187 @@ #include #include #include +#include +#include +#include +#include "BunIDLConvertBase.h" namespace WebCore { namespace Detail { -template +template +struct SequenceTraits; + +template +struct SequenceTraits< + IDLType, + Vector< + typename IDLType::SequenceStorageType, + inlineCapacity, + OverflowHandler, + minCapacity, + Malloc>> { + + using VectorType = Vector< + typename IDLType::SequenceStorageType, + inlineCapacity, + OverflowHandler, + minCapacity, + Malloc>; + + static void reserveExact( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!sequence.tryReserveCapacity(size)) { + // FIXME: Is the right exception to throw? + throwTypeError(&lexicalGlobalObject, scope); + return; + } + } + + static void reserveEstimated( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) + { + reserveExact(lexicalGlobalObject, sequence, size); + } + + template + static void append( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t index, + T&& element) + { + ASSERT(index == sequence.size()); + if constexpr (std::is_same_v, JSC::JSValue>) { + // `JSValue` should not be stored on the heap. + sequence.append(JSC::Strong { JSC::getVM(&lexicalGlobalObject), element }); + } else { + sequence.append(std::forward(element)); + } + } +}; + +template +struct SequenceTraits> { + using VectorType = std::array; + + static void reserveExact( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (size != arraySize) { + throwTypeError(&lexicalGlobalObject, scope); + } + } + + static void reserveEstimated( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t size) {} + + template + static void append( + JSC::JSGlobalObject& lexicalGlobalObject, + VectorType& sequence, + size_t index, + T&& element) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (index >= arraySize) { + throwTypeError(&lexicalGlobalObject, scope); + } + sequence[index] = std::forward(element); + } +}; + +template> struct GenericSequenceConverter { - using ReturnType = Vector; + using Traits = SequenceTraits; + using ReturnType = Traits::VectorType; + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, Ctx& ctx) + { + return convert(lexicalGlobalObject, object, ReturnType(), ctx); + } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object) { - return convert(lexicalGlobalObject, object, ReturnType()); + auto ctx = Bun::DefaultConversionContext {}; + return convert(lexicalGlobalObject, object, ctx); + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, ReturnType&& result, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + size_t index = 0; + auto elementCtx = ctx.contextForElement(); + forEachInIterable(&lexicalGlobalObject, object, [&result, &index, &elementCtx](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { + auto scope = DECLARE_THROW_SCOPE(vm); + + // auto convertedValue = Converter::convert(*lexicalGlobalObject, nextValue); + auto convertedValue = Bun::convertIDL(*lexicalGlobalObject, nextValue, elementCtx); + RETURN_IF_EXCEPTION(scope, ); + Traits::append(*lexicalGlobalObject, result, index++, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, ); + }); + + RETURN_IF_EXCEPTION(scope, {}); + // This could be the case if `VectorType` is `std::array`. + if (index != result.size()) { + throwTypeError(&lexicalGlobalObject, scope); + } + return WTFMove(result); } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, ReturnType&& result) { - forEachInIterable(&lexicalGlobalObject, object, [&result](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { - auto scope = DECLARE_THROW_SCOPE(vm); - - auto convertedValue = Converter::convert(*lexicalGlobalObject, nextValue); - RETURN_IF_EXCEPTION(scope, ); - result.append(WTFMove(convertedValue)); - }); - return WTFMove(result); + auto ctx = Bun::DefaultConversionContext {}; + return convert(lexicalGlobalObject, object, WTFMove(result), ctx); } template + requires(!Bun::IDLConversionContext>) static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, ExceptionThrower&& exceptionThrower = ExceptionThrower()) { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ReturnType result; - forEachInIterable(&lexicalGlobalObject, object, [&result, &exceptionThrower](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { + size_t index = 0; + forEachInIterable(&lexicalGlobalObject, object, [&result, &index, &exceptionThrower](JSC::VM& vm, JSC::JSGlobalObject* lexicalGlobalObject, JSC::JSValue nextValue) { auto scope = DECLARE_THROW_SCOPE(vm); auto convertedValue = Converter::convert(*lexicalGlobalObject, nextValue, std::forward(exceptionThrower)); RETURN_IF_EXCEPTION(scope, ); - result.append(WTFMove(convertedValue)); + Traits::append(*lexicalGlobalObject, result, index++, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, ); }); + + RETURN_IF_EXCEPTION(scope, {}); + // This could be the case if `VectorType` is `std::array`. + if (index != result.size()) { + throwTypeError(&lexicalGlobalObject, scope); + } return WTFMove(result); } @@ -80,13 +224,24 @@ struct GenericSequenceConverter { static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, JSC::JSValue method, ReturnType&& result) { - forEachInIterable(lexicalGlobalObject, object, method, [&result](JSC::VM& vm, JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue nextValue) { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + size_t index = 0; + forEachInIterable(lexicalGlobalObject, object, method, [&result, &index](JSC::VM& vm, JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue nextValue) { auto scope = DECLARE_THROW_SCOPE(vm); auto convertedValue = Converter::convert(lexicalGlobalObject, nextValue); RETURN_IF_EXCEPTION(scope, ); - result.append(WTFMove(convertedValue)); + Traits::append(lexicalGlobalObject, result, index++, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, ); }); + + RETURN_IF_EXCEPTION(scope, {}); + // This could be the case if `VectorType` is `std::array`. + if (index != result.size()) { + throwTypeError(&lexicalGlobalObject, scope); + } return WTFMove(result); } }; @@ -95,9 +250,10 @@ struct GenericSequenceConverter { // FIXME: This is only implemented for the IDLFloatingPointTypes and IDLLong. To add // support for more numeric types, add an overload of Converter::convert that // takes a JSGlobalObject, ThrowScope and double as its arguments. -template +template> struct NumericSequenceConverter { - using GenericConverter = GenericSequenceConverter; + using Traits = SequenceTraits; + using GenericConverter = GenericSequenceConverter; using ReturnType = typename GenericConverter::ReturnType; static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope, JSC::JSArray* array, unsigned length, JSC::IndexingType indexingType, ReturnType&& result) @@ -107,9 +263,10 @@ struct NumericSequenceConverter { auto indexValue = array->butterfly()->contiguousInt32().at(array, i).get(); ASSERT(!indexValue || indexValue.isInt32()); if (!indexValue) - result.append(0); + Traits::append(lexicalGlobalObject, result, i, 0); else - result.append(indexValue.asInt32()); + Traits::append(lexicalGlobalObject, result, i, indexValue.asInt32()); + RETURN_IF_EXCEPTION(scope, {}); } return WTFMove(result); } @@ -119,12 +276,13 @@ struct NumericSequenceConverter { for (unsigned i = 0; i < length; i++) { double doubleValue = array->butterfly()->contiguousDouble().at(array, i); if (std::isnan(doubleValue)) - result.append(0); + Traits::append(lexicalGlobalObject, result, i, 0); else { auto convertedValue = Converter::convert(lexicalGlobalObject, scope, doubleValue); RETURN_IF_EXCEPTION(scope, {}); - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, convertedValue); + RETURN_IF_EXCEPTION(scope, {}); } } return WTFMove(result); @@ -150,20 +308,23 @@ struct NumericSequenceConverter { unsigned length = array->length(); ReturnType result; + // If we're not an int32/double array, it's possible that converting a // JSValue to a number could cause the iterator protocol to change, hence, // we may need more capacity, or less. In such cases, we use the length // as a proxy for the capacity we will most likely need (it's unlikely that // a program is written with a valueOf that will augment the iterator protocol). // If we are an int32/double array, then length is precisely the capacity we need. - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } - JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; - if (indexingType != JSC::Int32Shape && indexingType != JSC::DoubleShape) + bool isLengthExact = indexingType == JSC::Int32Shape || indexingType == JSC::DoubleShape; + if (isLengthExact) { + Traits::reserveExact(lexicalGlobalObject, result, length); + } else { + Traits::reserveEstimated(lexicalGlobalObject, result, length); + } + RETURN_IF_EXCEPTION(scope, {}); + + if (!isLengthExact) RELEASE_AND_RETURN(scope, GenericConverter::convert(lexicalGlobalObject, object, WTFMove(result))); return convertArray(lexicalGlobalObject, scope, array, length, indexingType, WTFMove(result)); @@ -189,50 +350,52 @@ struct NumericSequenceConverter { // as a proxy for the capacity we will most likely need (it's unlikely that // a program is written with a valueOf that will augment the iterator protocol). // If we are an int32/double array, then length is precisely the capacity we need. - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } - JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; - if (indexingType != JSC::Int32Shape && indexingType != JSC::DoubleShape) + bool isLengthExact = indexingType == JSC::Int32Shape || indexingType == JSC::DoubleShape; + if (isLengthExact) { + Traits::reserveExact(lexicalGlobalObject, result, length); + } else { + Traits::reserveEstimated(lexicalGlobalObject, result, length); + } + RETURN_IF_EXCEPTION(scope, {}); + + if (!isLengthExact) RELEASE_AND_RETURN(scope, GenericConverter::convert(lexicalGlobalObject, object, method, WTFMove(result))); return convertArray(lexicalGlobalObject, scope, array, length, indexingType, WTFMove(result)); } }; -template +template> struct SequenceConverter { - using GenericConverter = GenericSequenceConverter; + using Traits = SequenceTraits; + using GenericConverter = GenericSequenceConverter; using ReturnType = typename GenericConverter::ReturnType; - static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array) + template + static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array, Ctx& ctx) { auto& vm = lexicalGlobalObject.vm(); auto scope = DECLARE_THROW_SCOPE(vm); unsigned length = array->length(); ReturnType result; - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } + Traits::reserveExact(lexicalGlobalObject, result, length); + RETURN_IF_EXCEPTION(scope, {}); JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; + auto elementCtx = ctx.contextForElement(); if (indexingType == JSC::ContiguousShape) { for (unsigned i = 0; i < length; i++) { auto indexValue = array->butterfly()->contiguous().at(array, i).get(); if (!indexValue) indexValue = JSC::jsUndefined(); - auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + // auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + auto convertedValue = Bun::convertIDL(lexicalGlobalObject, indexValue, elementCtx); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); } return result; } @@ -244,15 +407,22 @@ struct SequenceConverter { if (!indexValue) indexValue = JSC::jsUndefined(); - auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + // auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue); + auto convertedValue = Bun::convertIDL(lexicalGlobalObject, indexValue, elementCtx); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); } return result; } + static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array) + { + auto ctx = Bun::DefaultConversionContext {}; + return convertArray(lexicalGlobalObject, array, ctx); + } + template + requires(!Bun::IDLConversionContext>) static ReturnType convertArray(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSArray* array, ExceptionThrower&& exceptionThrower = ExceptionThrower()) { auto& vm = lexicalGlobalObject.vm(); @@ -260,11 +430,8 @@ struct SequenceConverter { unsigned length = array->length(); ReturnType result; - if (!result.tryReserveCapacity(length)) { - // FIXME: Is the right exception to throw? - throwTypeError(&lexicalGlobalObject, scope); - return {}; - } + Traits::reserveExact(lexicalGlobalObject, result, length); + RETURN_IF_EXCEPTION(scope, {}); JSC::IndexingType indexingType = array->indexingType() & JSC::IndexingShapeMask; @@ -276,8 +443,8 @@ struct SequenceConverter { auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue, std::forward(exceptionThrower)); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, {}); } return result; } @@ -291,37 +458,59 @@ struct SequenceConverter { auto convertedValue = Converter::convert(lexicalGlobalObject, indexValue, std::forward(exceptionThrower)); RETURN_IF_EXCEPTION(scope, {}); - - result.append(convertedValue); + Traits::append(lexicalGlobalObject, result, i, WTFMove(convertedValue)); + RETURN_IF_EXCEPTION(scope, {}); } return result; } + template + static ReturnType convertObject(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (Converter::conversionHasSideEffects) + RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object, ctx))); + + if (!JSC::isJSArray(object)) + RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object, ctx))); + + JSC::JSArray* array = JSC::asArray(object); + if (!array->isIteratorProtocolFastAndNonObservable()) + RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object, ctx))); + + RELEASE_AND_RETURN(scope, (convertArray(lexicalGlobalObject, array, ctx))); + } + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (auto* object = value.getObject()) { + RELEASE_AND_RETURN(scope, (convertObject(lexicalGlobalObject, object, ctx))); + } + ctx.throwTypeMustBe(lexicalGlobalObject, scope, "a sequence"_s); + return {}; + } + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ASCIILiteral functionName = {}, ASCIILiteral argumentName = {}) { auto& vm = JSC::getVM(&lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - if (!value.isObject()) { - throwSequenceTypeError(lexicalGlobalObject, scope, functionName, argumentName); - return {}; + if (auto* object = value.getObject()) { + auto ctx = Bun::DefaultConversionContext {}; + RELEASE_AND_RETURN(scope, (convertObject(lexicalGlobalObject, object, ctx))); } - - JSC::JSObject* object = JSC::asObject(value); - if (Converter::conversionHasSideEffects) - RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object))); - - if (!JSC::isJSArray(object)) - RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object))); - - JSC::JSArray* array = JSC::asArray(object); - if (!array->isIteratorProtocolFastAndNonObservable()) - RELEASE_AND_RETURN(scope, (GenericConverter::convert(lexicalGlobalObject, object))); - - RELEASE_AND_RETURN(scope, (convertArray(lexicalGlobalObject, array))); + throwSequenceTypeError(lexicalGlobalObject, scope, functionName, argumentName); + return {}; } template + requires(!Bun::IDLConversionContext>) static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower = ExceptionThrower(), @@ -442,22 +631,31 @@ struct SequenceConverter { } -template struct Converter> : DefaultConverter> { - using ReturnType = typename Detail::SequenceConverter::ReturnType; +template +struct Converter> : DefaultConverter> { + using ReturnType = typename Detail::SequenceConverter::ReturnType; + + static constexpr bool takesContext = true; + + template + static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, Ctx& ctx) + { + return Detail::SequenceConverter::convert(lexicalGlobalObject, value, ctx); + } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ASCIILiteral functionName = {}, ASCIILiteral argumentName = {}) { - return Detail::SequenceConverter::convert(lexicalGlobalObject, value, functionName, argumentName); + return Detail::SequenceConverter::convert(lexicalGlobalObject, value, functionName, argumentName); } static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSObject* object, JSC::JSValue method) { - return Detail::SequenceConverter::convert(lexicalGlobalObject, object, method); + return Detail::SequenceConverter::convert(lexicalGlobalObject, object, method); } template static ReturnType convert(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value, ExceptionThrower&& exceptionThrower, ASCIILiteral functionName = {}, ASCIILiteral argumentName = {}) { - return Detail::SequenceConverter::convert(lexicalGlobalObject, value, std::forward(exceptionThrower), functionName, argumentName); + return Detail::SequenceConverter::convert(lexicalGlobalObject, value, std::forward(exceptionThrower), functionName, argumentName); } }; diff --git a/src/bun.js/bindings/webcore/JSDOMURL.cpp b/src/bun.js/bindings/webcore/JSDOMURL.cpp old mode 100755 new mode 100644 diff --git a/src/bun.js/jsc.zig b/src/bun.js/jsc.zig index 4ad4818864..0e42512c45 100644 --- a/src/bun.js/jsc.zig +++ b/src/bun.js/jsc.zig @@ -44,6 +44,7 @@ pub const AnyPromise = @import("./bindings/AnyPromise.zig").AnyPromise; pub const array_buffer = @import("./jsc/array_buffer.zig"); pub const ArrayBuffer = array_buffer.ArrayBuffer; pub const MarkedArrayBuffer = array_buffer.MarkedArrayBuffer; +pub const JSCArrayBuffer = array_buffer.JSCArrayBuffer; pub const CachedBytecode = @import("./bindings/CachedBytecode.zig").CachedBytecode; pub const CallFrame = @import("./bindings/CallFrame.zig").CallFrame; pub const CommonAbortReason = @import("./bindings/CommonAbortReason.zig").CommonAbortReason; @@ -276,5 +277,7 @@ pub const math = struct { } }; +pub const generated = @import("bindgen_generated"); + const bun = @import("bun"); const std = @import("std"); diff --git a/src/bun.js/jsc/array_buffer.zig b/src/bun.js/jsc/array_buffer.zig index 9751b476b6..19b8cde91e 100644 --- a/src/bun.js/jsc/array_buffer.zig +++ b/src/bun.js/jsc/array_buffer.zig @@ -1,10 +1,9 @@ pub const ArrayBuffer = extern struct { ptr: [*]u8 = &[0]u8{}, - offset: usize = 0, len: usize = 0, byte_len: usize = 0, - typed_array_type: jsc.JSValue.JSType = .Cell, value: jsc.JSValue = jsc.JSValue.zero, + typed_array_type: jsc.JSValue.JSType = .Cell, shared: bool = false, // require('buffer').kMaxLength. @@ -132,7 +131,7 @@ pub const ArrayBuffer = extern struct { } }; - pub const empty = ArrayBuffer{ .offset = 0, .len = 0, .byte_len = 0, .typed_array_type = .Uint8Array, .ptr = undefined }; + pub const empty = ArrayBuffer{ .len = 0, .byte_len = 0, .typed_array_type = .Uint8Array, .ptr = &.{} }; pub const name = "Bun__ArrayBuffer"; pub const Stream = std.io.FixedBufferStream([]u8); @@ -186,11 +185,7 @@ pub const ArrayBuffer = extern struct { extern "c" fn Bun__createArrayBufferForCopy(*jsc.JSGlobalObject, ptr: ?*const anyopaque, len: usize) jsc.JSValue; pub fn fromTypedArray(ctx: *jsc.JSGlobalObject, value: jsc.JSValue) ArrayBuffer { - var out: ArrayBuffer = .{}; - const was = value.asArrayBuffer_(ctx, &out); - bun.assert(was); - out.value = value; - return out; + return value.asArrayBuffer(ctx).?; } extern "c" fn JSArrayBuffer__fromDefaultAllocator(*jsc.JSGlobalObject, ptr: [*]u8, len: usize) jsc.JSValue; @@ -207,7 +202,7 @@ pub const ArrayBuffer = extern struct { } pub fn fromBytes(bytes: []u8, typed_array_type: jsc.JSValue.JSType) ArrayBuffer { - return ArrayBuffer{ .offset = 0, .len = @as(u32, @intCast(bytes.len)), .byte_len = @as(u32, @intCast(bytes.len)), .typed_array_type = typed_array_type, .ptr = bytes.ptr }; + return ArrayBuffer{ .len = @as(u32, @intCast(bytes.len)), .byte_len = @as(u32, @intCast(bytes.len)), .typed_array_type = typed_array_type, .ptr = bytes.ptr }; } pub fn toJSUnchecked(this: ArrayBuffer, ctx: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue { @@ -320,7 +315,7 @@ pub const ArrayBuffer = extern struct { /// new ArrayBuffer(view.buffer, view.byteOffset, view.byteLength) /// ``` pub inline fn byteSlice(this: *const @This()) []u8 { - return this.ptr[this.offset..][0..this.byte_len]; + return this.ptr[0..this.byte_len]; } /// The equivalent of @@ -331,15 +326,19 @@ pub const ArrayBuffer = extern struct { pub const slice = byteSlice; pub inline fn asU16(this: *const @This()) []u16 { - return std.mem.bytesAsSlice(u16, @as([*]u16, @ptrCast(@alignCast(this.ptr)))[this.offset..this.byte_len]); + return @alignCast(this.asU16Unaligned()); } pub inline fn asU16Unaligned(this: *const @This()) []align(1) u16 { - return std.mem.bytesAsSlice(u16, @as([*]align(1) u16, @ptrCast(@alignCast(this.ptr)))[this.offset..this.byte_len]); + return @ptrCast(this.ptr[0 .. this.byte_len / @sizeOf(u16) * @sizeOf(u16)]); } pub inline fn asU32(this: *const @This()) []u32 { - return std.mem.bytesAsSlice(u32, @as([*]u32, @ptrCast(@alignCast(this.ptr)))[this.offset..this.byte_len]); + return @alignCast(this.asU32Unaligned()); + } + + pub inline fn asU32Unaligned(this: *const @This()) []align(1) u32 { + return @ptrCast(this.ptr[0 .. this.byte_len / @sizeOf(u32) * @sizeOf(u32)]); } pub const BinaryType = enum(u4) { @@ -652,6 +651,29 @@ pub fn makeTypedArrayWithBytesNoCopy(globalObject: *jsc.JSGlobalObject, arrayTyp return bun.jsc.fromJSHostCall(globalObject, @src(), Bun__makeTypedArrayWithBytesNoCopy, .{ globalObject, arrayType, ptr, len, deallocator, deallocatorContext }); } +/// Corresponds to `JSC::ArrayBuffer`. +pub const JSCArrayBuffer = opaque { + const Self = @This(); + + extern fn JSC__ArrayBuffer__asBunArrayBuffer(self: *Self, out: *ArrayBuffer) void; + extern fn JSC__ArrayBuffer__ref(self: *Self) void; + extern fn JSC__ArrayBuffer__deref(self: *Self) void; + + pub const Ref = bun.ptr.ExternalShared(Self); + + pub const external_shared_descriptor = struct { + pub const ref = JSC__ArrayBuffer__ref; + pub const deref = JSC__ArrayBuffer__deref; + }; + + pub fn asArrayBuffer(self: *Self) ArrayBuffer { + var out: ArrayBuffer = undefined; + out.ptr = &.{}; // `ptr` might not get set if the ArrayBuffer is empty + JSC__ArrayBuffer__asBunArrayBuffer(self, &out); + return out; + } +}; + const std = @import("std"); const bun = @import("bun"); diff --git a/src/bun.zig b/src/bun.zig index a47f334f01..7e24954e94 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -8,7 +8,7 @@ const bun = @This(); pub const Environment = @import("./env.zig"); -pub const use_mimalloc = true; +pub const use_mimalloc = @import("build_options").use_mimalloc; pub const default_allocator: std.mem.Allocator = allocators.c_allocator; /// Zero-sized type whose `allocator` method returns `default_allocator`. pub const DefaultAllocator = allocators.Default; diff --git a/src/codegen/bindgen-lib.ts b/src/codegen/bindgen-lib.ts index d7133b7b69..37a7d6cddb 100644 --- a/src/codegen/bindgen-lib.ts +++ b/src/codegen/bindgen-lib.ts @@ -33,7 +33,8 @@ export type Type< type TypeFlag = boolean | "opt-nonnull" | null; -interface BaseTypeProps { +// This needs to be exported to avoid error TS4023. +export interface BaseTypeProps { [isType]: true | [T, K]; /** * Optional means the value may be omitted from a parameter definition. @@ -334,7 +335,7 @@ interface FuncOptionsWithVariant extends FuncMetadata { variants: FuncVariant[]; } type FuncWithoutOverloads = FuncMetadata & FuncVariant; -type FuncOptions = FuncOptionsWithVariant | FuncWithoutOverloads; +export type FuncOptions = FuncOptionsWithVariant | FuncWithoutOverloads; export interface FuncMetadata { /** diff --git a/src/codegen/bindgenv2/internal/any.ts b/src/codegen/bindgenv2/internal/any.ts new file mode 100644 index 0000000000..8ca3199762 --- /dev/null +++ b/src/codegen/bindgenv2/internal/any.ts @@ -0,0 +1,42 @@ +import { CodeStyle, Type } from "./base"; + +export const RawAny: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLRawAny"; + } + get bindgenType() { + return "bindgen.BindgenRawAny"; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.jsc.JSValue"; + } + toCpp(value: any): string { + throw RangeError("`RawAny` cannot have a default value"); + } +})(); + +export const StrongAny: Type = new (class extends Type { + get idlType() { + return "::Bun::Bindgen::IDLStrongAny"; + } + get bindgenType() { + return "bindgen.BindgenStrongAny"; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.jsc.Strong"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: any): string { + throw RangeError("`StrongAny` cannot have a default value"); + } +})(); + +export function isAny(type: Type): boolean { + return type === RawAny || type === StrongAny; +} + +export function hasRawAny(type: Type): boolean { + return type === RawAny || type.dependencies.some(hasRawAny); +} diff --git a/src/codegen/bindgenv2/internal/array.ts b/src/codegen/bindgenv2/internal/array.ts new file mode 100644 index 0000000000..51444b8c6c --- /dev/null +++ b/src/codegen/bindgenv2/internal/array.ts @@ -0,0 +1,32 @@ +import { hasRawAny } from "./any"; +import { CodeStyle, Type } from "./base"; + +export abstract class ArrayType extends Type {} + +export function Array(elemType: Type): ArrayType { + if (hasRawAny(elemType)) { + throw RangeError("arrays cannot contain `RawAny` (use `StrongAny`)"); + } + return new (class extends ArrayType { + get idlType() { + return `::Bun::IDLArray<${elemType.idlType}>`; + } + get bindgenType() { + return `bindgen.BindgenArray(${elemType.bindgenType})`; + } + zigType(style?: CodeStyle) { + return `bun.collections.ArrayListDefault(${elemType.zigType(style)})`; + } + toCpp(value: any[]): string { + const args = `${value.map(elem => elemType.toCpp(elem)).join(", ")}`; + return `${this.idlType}::ImplementationType { ${args} }`; + } + get dependencies() { + return [elemType]; + } + getHeaders(result: Set): void { + result.add("Bindgen/ExternVectorTraits.h"); + elemType.getHeaders(result); + } + })(); +} diff --git a/src/codegen/bindgenv2/internal/base.ts b/src/codegen/bindgenv2/internal/base.ts new file mode 100644 index 0000000000..c696a6ebd7 --- /dev/null +++ b/src/codegen/bindgenv2/internal/base.ts @@ -0,0 +1,134 @@ +import util from "node:util"; +import type { NullableType, OptionalType } from "./optional"; + +/** Default is "compact". */ +export type CodeStyle = "compact" | "pretty"; + +export abstract class Type { + get optional(): OptionalType { + return require("./optional").optional(this); + } + + get nullable(): NullableType { + return require("./optional").nullable(this); + } + + abstract readonly idlType: string; + abstract readonly bindgenType: string; + + /** + * This can be overridden to make the generated code clearer. If overridden, it must return an + * expression that evaluates to the same type as `${this.bindgenType}.ZigType`; it should not + * actually change the type. + */ + zigType(style?: CodeStyle): string { + return this.bindgenType + ".ZigType"; + } + + /** This must be overridden if bindgen.zig defines a custom `OptionalZigType`. */ + optionalZigType(style?: CodeStyle): string { + return `?${this.zigType(style)}`; + } + + /** Converts a JS value into a C++ expression. Used for default values. */ + abstract toCpp(value: any): string; + + /** Other types that this type contains or otherwise depends on. */ + get dependencies(): readonly Type[] { + return []; + } + + /** Headers required by users of this type. */ + getHeaders(result: Set): void { + for (const type of this.dependencies) { + type.getHeaders(result); + } + } +} + +export abstract class NamedType extends Type { + abstract readonly name: string; + get cppHeader(): string | null { + return null; + } + get cppSource(): string | null { + return null; + } + get zigSource(): string | null { + return null; + } + // These getters are faster than `.cppHeader != null` etc. + get hasCppHeader(): boolean { + return false; + } + get hasCppSource(): boolean { + return false; + } + get hasZigSource(): boolean { + return false; + } + getHeaders(result: Set): void { + result.add(`Generated${this.name}.h`); + } +} + +export function validateName(name: string): void { + const reservedPrefixes = ["IDL", "Bindgen", "Extern", "Generated", "MemberType"]; + const reservedNames = ["Bun", "WTF", "JSC", "WebCore", "Self"]; + if (!/^[A-Z]/.test(name)) { + throw RangeError(`name must start with a capital letter: ${name}`); + } + if (/[^a-zA-Z0-9_]/.test(name)) { + throw RangeError(`name may only contain letters, numbers, and underscores: ${name}`); + } + if (reservedPrefixes.some(s => name.startsWith(s))) { + throw RangeError(`name starts with reserved prefix: ${name}`); + } + if (reservedNames.includes(name)) { + throw RangeError(`cannot use reserved name: ${name}`); + } +} + +export function headersForTypes(types: readonly Type[]): string[] { + const headers = new Set(); + for (const type of types) { + type.getHeaders(headers); + } + return Array.from(headers); +} + +export function dedent(text: string): string { + const commonIndent = Math.min( + ...Array.from(text.matchAll(/\n( *)[^ \n]/g) ?? []).map(m => m[1].length), + ); + text = text.trim(); + if (commonIndent > 0 && commonIndent !== Infinity) { + text = text.replaceAll("\n" + " ".repeat(commonIndent), "\n"); + } + return text.replace(/^ +$/gm, ""); +} + +/** Converts indents from 2 spaces to 4. */ +export function reindent(text: string): string { + return dedent(text).replace(/^ +/gm, "$&$&"); +} + +/** Does not indent the first line. */ +export function addIndent(amount: number, text: string): string { + return text.replaceAll("\n", "\n" + " ".repeat(amount)); +} + +export function joinIndented(amount: number, pieces: readonly string[]): string { + return addIndent(amount, pieces.map(dedent).join("\n")); +} + +export function toQuotedLiteral(value: string): string { + return `"${util.inspect(value).slice(1, -1).replaceAll('"', '\\"')}"`; +} + +export function toASCIILiteral(value: string): string { + if (value[Symbol.iterator]().some(c => c.charCodeAt(0) >= 128)) { + throw RangeError(`string must be ASCII: ${util.inspect(value)}`); + } + return `${toQuotedLiteral(value)}_s`; +} diff --git a/src/codegen/bindgenv2/internal/dictionary.ts b/src/codegen/bindgenv2/internal/dictionary.ts new file mode 100644 index 0000000000..9244646941 --- /dev/null +++ b/src/codegen/bindgenv2/internal/dictionary.ts @@ -0,0 +1,451 @@ +import { hasRawAny, isAny } from "./any"; +import { + addIndent, + CodeStyle, + dedent, + headersForTypes, + joinIndented, + NamedType, + reindent, + toASCIILiteral, + toQuotedLiteral, + Type, + validateName, +} from "./base"; +import * as optional from "./optional"; +import { isUnion } from "./union"; + +export interface DictionaryMember { + type: Type; + /** Optional default value to use when this member is missing or undefined. */ + default?: any; + /** The name used in generated Zig/C++ code. Defaults to the public JS name. */ + internalName?: string; + /** Alternative JavaScript names for this member. */ + altNames?: string[]; +} + +export interface DictionaryMembers { + readonly [name: string]: Type | DictionaryMember; +} + +export interface DictionaryInstance { + readonly [name: string]: any; +} + +export abstract class DictionaryType extends NamedType {} + +interface DictionaryOptions { + name: string; + /** Used in error messages. Defaults to `name`. */ + userFacingName?: string; + /** Whether to generate a Zig `fromJS` function. */ + generateConversionFunction?: boolean; +} + +export function dictionary( + nameOrOptions: string | DictionaryOptions, + members: DictionaryMembers, +): DictionaryType { + let name: string; + let userFacingName: string; + let generateConversionFunction = false; + if (typeof nameOrOptions === "string") { + name = nameOrOptions; + userFacingName = name; + } else { + name = nameOrOptions.name; + userFacingName = nameOrOptions.userFacingName ?? name; + generateConversionFunction = !!nameOrOptions.generateConversionFunction; + } + validateName(name); + const fullMembers = Object.entries(members).map( + ([name, value]) => new FullDictionaryMember(name, value), + ); + + return new (class extends DictionaryType { + get name() { + return name; + } + get idlType() { + return `::Bun::Bindgen::Generated::IDL${name}`; + } + get bindgenType() { + return `bindgen_generated.internal.${name}`; + } + zigType(style?: CodeStyle) { + return `bindgen_generated.${name}`; + } + get dependencies() { + return fullMembers.map(m => m.type); + } + + toCpp(value: DictionaryInstance): string { + for (const memberName of Object.keys(value)) { + if (!(memberName in members)) throw RangeError(`unexpected key: ${memberName}`); + } + return reindent(`${name} { + ${joinIndented( + 8, + fullMembers.map(memberInfo => { + let memberValue; + if (Object.hasOwn(value, memberInfo.name)) { + memberValue = value[memberInfo.name]; + } else if (memberInfo.hasDefault) { + memberValue = memberInfo.default; + } else if (!permitsUndefined(memberInfo.type)) { + throw RangeError(`missing key: ${memberInfo.name}`); + } + const internalName = memberInfo.internalName; + return `.${internalName} = ${memberInfo.type.toCpp(memberValue)},`; + }), + )} + }`); + } + + get hasCppHeader() { + return true; + } + get cppHeader() { + return reindent(` + #pragma once + #include "Bindgen.h" + #include "JSDOMConvertDictionary.h" + ${headersForTypes(Object.values(fullMembers).map(m => m.type)) + .map(headerName => `#include <${headerName}>\n` + " ".repeat(8)) + .join("")} + namespace Bun { + namespace Bindgen { + namespace Generated { + struct ${name} { + ${joinIndented( + 10, + fullMembers.map((memberInfo, i) => { + return ` + using MemberType${i} = ${memberInfo.type.idlType}::ImplementationType; + MemberType${i} ${memberInfo.internalName}; + `; + }), + )} + }; + using IDL${name} = ::WebCore::IDLDictionary<${name}>; + struct Extern${name} { + ${joinIndented( + 10, + fullMembers.map((memberInfo, i) => { + return ` + using MemberType${i} = ExternTraits<${name}::MemberType${i}>::ExternType; + MemberType${i} ${memberInfo.internalName}; + `; + }), + )} + };${(() => { + if (!generateConversionFunction) { + return ""; + } + const result = dedent(` + extern "C" bool bindgenConvertJSTo${name}( + ::JSC::JSGlobalObject* globalObject, + ::JSC::EncodedJSValue value, + Extern${name}* result); + `); + return addIndent(8, "\n" + result); + })()} + } + + template<> struct ExternTraits { + using ExternType = Generated::Extern${name}; + static ExternType convertToExtern(Generated::${name}&& cppValue) + { + return ExternType { + ${joinIndented( + 14, + fullMembers.map((memberInfo, i) => { + const cppType = `Generated::${name}::MemberType${i}`; + const cppValue = `::std::move(cppValue.${memberInfo.internalName})`; + const rhs = `ExternTraits<${cppType}>::convertToExtern(${cppValue})`; + return `.${memberInfo.internalName} = ${rhs},`; + }), + )} + }; + } + }; + } + + template<> + struct IDLHumanReadableName<::WebCore::IDLDictionary> + : BaseIDLHumanReadableName { + static constexpr auto humanReadableName + = ::std::to_array(${toQuotedLiteral(userFacingName)}); + }; + } + + template<> Bun::Bindgen::Generated::${name} + WebCore::convertDictionary( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value); + + ${(() => { + if (!hasRawAny(this)) { + return ""; + } + const code = ` + template<> struct WebCore::IDLDictionary<::Bun::Bindgen::Generated::${name}> + : ::Bun::Bindgen::IDLStackOnlyDictionary<::Bun::Bindgen::Generated::${name}> {}; + `; + return joinIndented(8, [code]); + })()} + `); + } + + get hasCppSource() { + return true; + } + get cppSource() { + return reindent(` + #include "root.h" + #include "Generated${name}.h" + #include "Bindgen/IDLConvert.h" + #include + + template<> Bun::Bindgen::Generated::${name} + WebCore::convertDictionary( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value) + { + ::JSC::VM& vm = globalObject.vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + auto ctx = Bun::Bindgen::LiteralConversionContext { ${toASCIILiteral(userFacingName)} }; + auto* object = value.getObject(); + if (!object) [[unlikely]] { + ctx.throwNotObject(globalObject, throwScope); + return {}; + } + ::Bun::Bindgen::Generated::${name} result; + ${joinIndented( + 10, + fullMembers.map((m, i) => memberConversion(userFacingName, m, i)), + )} + return result; + } + + ${(() => { + if (!generateConversionFunction) { + return ""; + } + const result = ` + namespace Bun::Bindgen::Generated { + extern "C" bool bindgenConvertJSTo${name}( + ::JSC::JSGlobalObject* globalObject, + ::JSC::EncodedJSValue value, + Extern${name}* result) + { + ::JSC::VM& vm = globalObject->vm(); + auto throwScope = DECLARE_THROW_SCOPE(vm); + ${name} convertedValue = ::WebCore::convert>( + *globalObject, + JSC::JSValue::decode(value) + ); + RETURN_IF_EXCEPTION(throwScope, false); + *result = ExternTraits<${name}>::convertToExtern(::std::move(convertedValue)); + return true; + } + } + `; + return joinIndented(8, [result]); + })()} + `); + } + + get hasZigSource() { + return true; + } + get zigSource() { + return reindent(` + pub const ${name} = struct { + const Self = @This(); + + ${joinIndented( + 10, + fullMembers.map(memberInfo => { + return `${memberInfo.internalName}: ${memberInfo.type.zigType("pretty")},`; + }), + )} + + pub fn deinit(self: *Self) void { + ${joinIndented( + 12, + fullMembers.map(memberInfo => { + return `bun.memory.deinit(&self.${memberInfo.internalName});`; + }), + )} + self.* = undefined; + }${(() => { + if (!generateConversionFunction) { + return ""; + } + const result = dedent(` + pub fn fromJS(globalThis: *jsc.JSGlobalObject, value: jsc.JSValue) bun.JSError!Self { + var scope: jsc.ExceptionValidationScope = undefined; + scope.init(globalThis, @src()); + defer scope.deinit(); + var extern_result: Extern${name} = undefined; + const success = bindgenConvertJSTo${name}(globalThis, value, &extern_result); + scope.assertExceptionPresenceMatches(!success); + return if (success) + Bindgen${name}.convertFromExtern(extern_result) + else + error.JSError; + } + `); + return addIndent(10, "\n" + result); + })()} + }; + + pub const Bindgen${name} = struct { + const Self = @This(); + pub const ZigType = ${name}; + pub const ExternType = Extern${name}; + pub fn convertFromExtern(extern_value: Self.ExternType) Self.ZigType { + return .{ + ${joinIndented( + 14, + fullMembers.map(memberInfo => { + const internalName = memberInfo.internalName; + const bindgenType = memberInfo.type.bindgenType; + const rhs = `${bindgenType}.convertFromExtern(extern_value.${internalName})`; + return `.${internalName} = ${rhs},`; + }), + )} + }; + } + }; + + const Extern${name} = extern struct { + ${joinIndented( + 10, + fullMembers.map(memberInfo => { + return `${memberInfo.internalName}: ${memberInfo.type.bindgenType}.ExternType,`; + }), + )} + }; + + extern fn bindgenConvertJSTo${name}( + globalObject: *jsc.JSGlobalObject, + value: jsc.JSValue, + result: *Extern${name}, + ) bool; + + const bindgen_generated = @import("bindgen_generated"); + const bun = @import("bun"); + const bindgen = bun.bun_js.bindgen; + const jsc = bun.bun_js.jsc; + `); + } + })(); +} + +class FullDictionaryMember { + names: string[]; + internalName: string; + type: Type; + hasDefault: boolean = false; + default?: any; + + constructor(name: string, member: Type | DictionaryMember) { + if (member instanceof Type) { + this.names = [name]; + this.internalName = name; + this.type = member; + } else { + this.names = [name, ...(member.altNames ?? [])]; + this.internalName = member.internalName ?? name; + this.type = member.type; + this.hasDefault = Object.hasOwn(member, "default"); + this.default = member.default; + } + } + + get name(): string { + return this.names[0]; + } +} + +function memberConversion( + userFacingDictName: string, + memberInfo: FullDictionaryMember, + memberIndex: number, +): string { + const i = memberIndex; + const internalName = memberInfo.internalName; + const idlType = memberInfo.type.idlType; + const qualifiedName = `${userFacingDictName}.${memberInfo.name}`; + + const start = ` + ::JSC::JSValue value${i}; + auto ctx${i} = Bun::Bindgen::LiteralConversionContext { ${toASCIILiteral(qualifiedName)} }; + do { + ${joinIndented( + 6, + memberInfo.names.map((memberName, altNameIndex) => { + let result = ""; + if (altNameIndex > 0) { + result = `if (!value${i}.isUndefined()) break;\n`; + } + result += dedent(` + value${i} = object->get( + &globalObject, + ::JSC::Identifier::fromString(vm, ${toASCIILiteral(memberName)})); + RETURN_IF_EXCEPTION(throwScope, {}); + `); + return result; + }), + )} + } while (false); + `; + + let end: string; + if (memberInfo.hasDefault) { + end = ` + if (value${i}.isUndefined()) { + result.${internalName} = ${memberInfo.type.toCpp(memberInfo.default)}; + } else { + result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); + RETURN_IF_EXCEPTION(throwScope, {}); + } + `; + } else if (permitsUndefined(memberInfo.type)) { + end = ` + result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); + RETURN_IF_EXCEPTION(throwScope, {}); + `; + } else { + end = ` + if (value${i}.isUndefined()) { + ctx${i}.throwRequired(globalObject, throwScope); + return {}; + } + result.${internalName} = Bun::convertIDL<${idlType}>(globalObject, value${i}, ctx${i}); + RETURN_IF_EXCEPTION(throwScope, {}); + `; + } + const body = dedent(start) + "\n" + dedent(end); + return addIndent(2, "{\n" + body) + "\n}"; +} + +function basicPermitsUndefined(type: Type): boolean { + return ( + type instanceof optional.OptionalType || + type instanceof optional.NullableType || + type === optional.undefined || + type === optional.null || + isAny(type) + ); +} + +function permitsUndefined(type: Type): boolean { + if (isUnion(type)) { + return type.dependencies.some(basicPermitsUndefined); + } + return basicPermitsUndefined(type); +} diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts new file mode 100644 index 0000000000..8da8ed147a --- /dev/null +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -0,0 +1,182 @@ +import assert from "node:assert"; +import util from "node:util"; +import { + CodeStyle, + joinIndented, + NamedType, + reindent, + toASCIILiteral, + toQuotedLiteral, +} from "./base"; + +abstract class EnumType extends NamedType {} + +export function enumeration(name: string, values: string[]): EnumType { + if (values.length === 0) { + throw RangeError("enum cannot be empty: " + name); + } + if (values.length > 1n << 32n) { + throw RangeError("too many enum values: " + name); + } + + const valueSet = new Set(); + const cppMemberSet = new Set(); + for (const value of values) { + if (valueSet.size === valueSet.add(value).size) { + throw RangeError(`duplicate enum value in ${name}: ${util.inspect(value)}`); + } + let cppName = "k"; + cppName += value + .split(/[^A-Za-z0-9]+/) + .filter(x => x) + .map(s => s[0].toUpperCase() + s.slice(1)) + .join(""); + if (cppMemberSet.size === cppMemberSet.add(cppName).size) { + let i = 2; + while (cppMemberSet.size === cppMemberSet.add(cppName + i).size) { + ++i; + } + } + } + const cppMembers = Array.from(cppMemberSet); + return new (class extends EnumType { + get name() { + return name; + } + get idlType() { + return `::Bun::Bindgen::Generated::IDL${name}`; + } + get bindgenType() { + return `bindgen_generated.internal.${name}`; + } + zigType(style?: CodeStyle) { + return `bindgen_generated.${name}`; + } + toCpp(value: string): string { + const index = values.indexOf(value); + if (index === -1) { + throw RangeError(`not a member of this enumeration: ${value}`); + } + return `::Bun::Bindgen::Generated::${name}::${cppMembers[index]}`; + } + + get hasCppHeader() { + return true; + } + get cppHeader() { + const quotedValues = values.map(v => `"${v}"`); + let humanReadableName; + if (quotedValues.length == 0) { + assert(false); // unreachable + } else if (quotedValues.length == 1) { + humanReadableName = quotedValues[0]; + } else if (quotedValues.length == 2) { + humanReadableName = quotedValues[0] + " or " + quotedValues[1]; + } else { + humanReadableName = + quotedValues.slice(0, -1).join(", ") + ", or " + quotedValues[quotedValues.length - 1]; + } + + return reindent(` + #pragma once + #include "Bindgen/ExternTraits.h" + #include "JSDOMConvertEnumeration.h" + + namespace Bun { + namespace Bindgen { + namespace Generated { + enum class ${name} : ::std::uint32_t { + ${joinIndented( + 10, + cppMembers.map(memberName => `${memberName},`), + )} + }; + using IDL${name} = ::WebCore::IDLEnumeration; + } + template<> struct ExternTraits : TrivialExtern {}; + } + template<> + struct IDLHumanReadableName<::WebCore::IDLEnumeration> + : BaseIDLHumanReadableName { + static constexpr auto humanReadableName + = std::to_array(${toQuotedLiteral(humanReadableName)}); + }; + } + + template<> std::optional + WebCore::parseEnumerationFromString( + const WTF::String&); + + template<> std::optional + WebCore::parseEnumeration( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value); + `); + } + + get hasCppSource() { + return true; + } + get cppSource() { + const qualifiedName = "Bun::Bindgen::Generated::" + name; + const pairType = `::std::pair<::WTF::ComparableASCIILiteral, ::${qualifiedName}>`; + return reindent(` + #include "root.h" + #include "Generated${name}.h" + #include + + template<> std::optional<${qualifiedName}> + WebCore::parseEnumerationFromString<${qualifiedName}>(const WTF::String& stringVal) + { + static constexpr ::std::array<${pairType}, ${values.length}> mappings { + ${joinIndented( + 12, + values + .map<[string, number]>((value, i) => [value, i]) + .sort() + .map(([value, i]) => { + return `${pairType} { + ${toASCIILiteral(value)}, + ::${qualifiedName}::${cppMembers[i]}, + },`; + }), + )} + }; + static constexpr ::WTF::SortedArrayMap enumerationMapping { mappings }; + if (auto* enumerationValue = enumerationMapping.tryGet(stringVal)) [[likely]] { + return *enumerationValue; + } + return std::nullopt; + } + + template<> std::optional<${qualifiedName}> + WebCore::parseEnumeration<${qualifiedName}>( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value) + { + return parseEnumerationFromString<::${qualifiedName}>( + value.toWTFString(&globalObject) + ); + } + `); + } + + get hasZigSource() { + return true; + } + get zigSource() { + return reindent(` + pub const ${name} = enum(u32) { + ${joinIndented( + 10, + values.map(value => `@${toQuotedLiteral(value)},`), + )} + }; + + pub const Bindgen${name} = bindgen.BindgenTrivial(${name}); + const bun = @import("bun"); + const bindgen = bun.bun_js.bindgen; + `); + } + })(); +} diff --git a/src/codegen/bindgenv2/internal/interfaces.ts b/src/codegen/bindgenv2/internal/interfaces.ts new file mode 100644 index 0000000000..21584ba0f4 --- /dev/null +++ b/src/codegen/bindgenv2/internal/interfaces.ts @@ -0,0 +1,40 @@ +import { CodeStyle, Type } from "./base"; + +export const ArrayBuffer = new (class extends Type { + get idlType() { + return `::Bun::IDLArrayBufferRef`; + } + get bindgenType() { + return `bindgen.BindgenArrayBuffer`; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.jsc.JSCArrayBuffer.Ref"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: any): string { + throw RangeError("default values for `ArrayBuffer` are not supported"); + } +})(); + +export const Blob = new (class extends Type { + get idlType() { + return `::Bun::IDLBlobRef`; + } + get bindgenType() { + return `bindgen.BindgenBlob`; + } + zigType(style?: CodeStyle) { + return "bun.bun_js.webcore.Blob.Ref"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: any): string { + throw RangeError("default values for `Blob` are not supported"); + } + getHeaders(result: Set): void { + result.add("BunIDLConvertBlob.h"); + } +})(); diff --git a/src/codegen/bindgenv2/internal/optional.ts b/src/codegen/bindgenv2/internal/optional.ts new file mode 100644 index 0000000000..74235b6fbe --- /dev/null +++ b/src/codegen/bindgenv2/internal/optional.ts @@ -0,0 +1,84 @@ +import { isAny } from "./any"; +import { CodeStyle, Type } from "./base"; + +export abstract class OptionalType extends Type {} + +export function optional(payload: Type): OptionalType { + if (isAny(payload)) { + throw RangeError("`Any` types are already optional"); + } + return new (class extends OptionalType { + get idlType() { + return `::WebCore::IDLOptional<${payload.idlType}>`; + } + get bindgenType() { + return `bindgen.BindgenOptional(${payload.bindgenType})`; + } + zigType(style?: CodeStyle) { + return payload.optionalZigType(style); + } + toCpp(value: any): string { + if (value === undefined) { + return `::WebCore::IDLOptional<${payload.idlType}>::nullValue()`; + } + return payload.toCpp(value); + } + })(); +} + +export abstract class NullableType extends Type {} + +export function nullable(payload: Type): NullableType { + const AsOptional = optional(payload); + return new (class extends NullableType { + get idlType() { + return `::WebCore::IDLNullable<${payload.idlType}>`; + } + get bindgenType() { + return AsOptional.bindgenType; + } + zigType(style?: CodeStyle) { + return AsOptional.zigType(style); + } + toCpp(value: any): string { + if (value == null) { + return `::WebCore::IDLNullable<${payload.idlType}>::nullValue()`; + } + return payload.toCpp(value); + } + })(); +} + +/** For use in unions, to represent an optional union. */ +const Undefined = new (class extends Type { + get idlType() { + return `::Bun::IDLStrictUndefined`; + } + get bindgenType() { + return `bindgen.BindgenNull`; + } + zigType(style?: CodeStyle) { + return "void"; + } + toCpp(value: undefined): string { + return `{}`; + } +})(); + +/** For use in unions, to represent a nullable union. */ +const Null = new (class extends Type { + get idlType() { + return `::Bun::IDLStrictNull`; + } + get bindgenType() { + return `bindgen.BindgenNull`; + } + zigType(style?: CodeStyle) { + return "void"; + } + toCpp(value: null): string { + return `nullptr`; + } +})(); + +export { Null as null, Undefined as undefined }; diff --git a/src/codegen/bindgenv2/internal/primitives.ts b/src/codegen/bindgenv2/internal/primitives.ts new file mode 100644 index 0000000000..72d24405d7 --- /dev/null +++ b/src/codegen/bindgenv2/internal/primitives.ts @@ -0,0 +1,125 @@ +import assert from "node:assert"; +import util from "node:util"; +import { CodeStyle, Type } from "./base"; + +export const bool: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLStrictBoolean"; + } + get bindgenType() { + return `bindgen.BindgenBool`; + } + zigType(style?: CodeStyle) { + return "bool"; + } + toCpp(value: boolean): string { + assert(typeof value === "boolean"); + return value ? "true" : "false"; + } +})(); + +function makeUnsignedType(width: number): Type { + assert(Number.isInteger(width) && width > 0); + return new (class extends Type { + get idlType() { + return `::Bun::IDLStrictInteger<::std::uint${width}_t>`; + } + get bindgenType() { + return `bindgen.BindgenU${width}`; + } + zigType(style?: CodeStyle) { + return `u${width}`; + } + toCpp(value: number | bigint): string { + assert(typeof value === "bigint" || Number.isSafeInteger(value)); + const intValue = BigInt(value); + if (intValue < 0) throw RangeError("unsigned int cannot be negative"); + const max = 1n << BigInt(width); + if (intValue >= max) throw RangeError("integer out of range"); + return intValue.toString(); + } + })(); +} + +function makeSignedType(width: number): Type { + assert(Number.isInteger(width) && width > 0); + return new (class extends Type { + get idlType() { + return `::Bun::IDLStrictInteger<::std::int${width}_t>`; + } + get bindgenType() { + return `bindgen.BindgenI${width}`; + } + zigType(style?: CodeStyle) { + return `i${width}`; + } + toCpp(value: number | bigint): string { + assert(typeof value === "bigint" || Number.isSafeInteger(value)); + const intValue = BigInt(value); + const max = 1n << BigInt(width - 1); + const min = -max; + if (intValue >= max || intValue < min) { + throw RangeError("integer out of range"); + } + if (width === 64 && intValue === min) { + return `(${intValue + 1n} - 1)`; + } + return intValue.toString(); + } + })(); +} + +export const u8: Type = makeUnsignedType(8); +export const u16: Type = makeUnsignedType(16); +export const u32: Type = makeUnsignedType(32); +export const u64: Type = makeUnsignedType(64); + +export const i8: Type = makeSignedType(8); +export const i16: Type = makeSignedType(16); +export const i32: Type = makeSignedType(32); +export const i64: Type = makeSignedType(64); + +export const f64: Type = new (class extends Type { + get finite() { + return finiteF64; + } + + get idlType() { + return "::Bun::IDLStrictDouble"; + } + get bindgenType() { + return `bindgen.BindgenF64`; + } + zigType(style?: CodeStyle) { + return `f64`; + } + toCpp(value: number): string { + assert(typeof value === "number"); + if (Number.isNaN(value)) { + return "::std::numeric_limits::quiet_NaN()"; + } else if (value === Infinity) { + return "::std::numeric_limits::infinity()"; + } else if (value === -Infinity) { + return "-::std::numeric_limits::infinity()"; + } else { + return util.inspect(value); + } + } +})(); + +export const finiteF64: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLFiniteDouble"; + } + get bindgenType() { + return f64.bindgenType; + } + zigType(style?: CodeStyle) { + return f64.zigType(style); + } + toCpp(value: number): string { + assert(typeof value === "number"); + if (!Number.isFinite(value)) throw RangeError("number must be finite"); + return util.inspect(value); + } +})(); diff --git a/src/codegen/bindgenv2/internal/string.ts b/src/codegen/bindgenv2/internal/string.ts new file mode 100644 index 0000000000..1363942d46 --- /dev/null +++ b/src/codegen/bindgenv2/internal/string.ts @@ -0,0 +1,21 @@ +import assert from "node:assert"; +import { CodeStyle, Type, toASCIILiteral } from "./base"; + +export const String: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLStrictString"; + } + get bindgenType() { + return "bindgen.BindgenString"; + } + zigType(style?: CodeStyle) { + return "bun.string.WTFString"; + } + optionalZigType(style?: CodeStyle) { + return this.zigType(style) + ".Optional"; + } + toCpp(value: string): string { + assert(typeof value === "string"); + return toASCIILiteral(value); + } +})(); diff --git a/src/codegen/bindgenv2/internal/union.ts b/src/codegen/bindgenv2/internal/union.ts new file mode 100644 index 0000000000..e452f8b1f0 --- /dev/null +++ b/src/codegen/bindgenv2/internal/union.ts @@ -0,0 +1,185 @@ +import assert from "node:assert"; +import { + CodeStyle, + dedent, + headersForTypes, + joinIndented, + NamedType, + reindent, + Type, + validateName, +} from "./base"; + +export interface NamedAlternatives { + readonly [name: string]: Type; +} + +export interface UnionInstance { + readonly type: Type; + readonly value: any; +} + +export abstract class AnonymousUnionType extends Type {} +export abstract class NamedUnionType extends NamedType {} + +export function isUnion(type: Type): boolean { + return type instanceof AnonymousUnionType || type instanceof NamedUnionType; +} + +export function union(alternatives: Type[]): AnonymousUnionType; +export function union(name: string, alternatives: NamedAlternatives): NamedUnionType; + +/** + * The order of types in this union is significant. Each type is tried in order, and the first one + * that successfully converts determines the active field in the corresponding Zig tagged union. + * + * This means that it is an error to specify `RawAny` or `StrongAny` as anything other than the + * last alternative, as conversion to any subsequent types would never be attempted. + */ +export function union( + alternativesOrName: Type[] | string, + maybeNamedAlternatives?: NamedAlternatives, +): AnonymousUnionType | NamedUnionType { + let alternatives: Type[]; + + function toCpp(value: UnionInstance): string { + assert(alternatives.includes(value.type)); + return `${value.type.idlType}::ImplementationType { ${value.type.toCpp(value.value)} }`; + } + + function getUnionType() { + return `::Bun::IDLOrderedUnion<${alternatives.map(a => a.idlType).join(", ")}>`; + } + + function validateAlternatives(name?: string) { + const suffix = name == null ? "" : `: ${name}`; + if (alternatives.length === 0) { + throw RangeError("union cannot be empty" + suffix); + } + } + + if (typeof alternativesOrName !== "string") { + alternatives = alternativesOrName.slice(); + validateAlternatives(); + // anonymous union (neither union nor fields are named) + return new (class extends AnonymousUnionType { + get idlType() { + return getUnionType(); + } + get bindgenType() { + return `bindgen.BindgenUnion(&.{ ${alternatives.map(a => a.bindgenType).join(", ")} })`; + } + zigType(style?: CodeStyle) { + if (style !== "pretty") { + return `bun.meta.TaggedUnion(&.{ ${alternatives.map(a => a.zigType()).join(", ")} })`; + } + return dedent(`bun.meta.TaggedUnion(&.{ + ${joinIndented( + 10, + alternatives.map(a => a.zigType("pretty") + ","), + )} + })`); + } + get dependencies() { + return Object.freeze(alternatives); + } + toCpp(value: UnionInstance): string { + return toCpp(value); + } + })(); + } + + assert(maybeNamedAlternatives !== undefined); + const namedAlternatives: NamedAlternatives = maybeNamedAlternatives; + const name: string = alternativesOrName; + validateName(name); + alternatives = Object.values(namedAlternatives); + validateAlternatives(name); + // named union (both union and fields are named) + return new (class extends NamedUnionType { + get name() { + return name; + } + get idlType() { + return `::Bun::Bindgen::Generated::IDL${name}`; + } + get bindgenType() { + return `bindgen_generated.internal.${name}`; + } + zigType(style?: CodeStyle) { + return `bindgen_generated.${name}`; + } + get dependencies() { + return Object.freeze(alternatives); + } + toCpp(value: UnionInstance): string { + return toCpp(value); + } + + get hasCppHeader() { + return true; + } + get cppHeader() { + return reindent(` + #pragma once + #include "Bindgen/IDLTypes.h" + ${headersForTypes(alternatives) + .map(headerName => `#include <${headerName}>\n` + " ".repeat(8)) + .join("")} + namespace Bun::Bindgen::Generated { + using IDL${name} = ${getUnionType()}; + using ${name} = IDL${name}::ImplementationType; + } + `); + } + + get hasZigSource() { + return true; + } + get zigSource() { + return reindent(` + pub const ${name} = union(enum) { + ${joinIndented( + 10, + Object.entries(namedAlternatives).map(([altName, altType]) => { + return `${altName}: ${altType.zigType("pretty")},`; + }), + )} + + pub fn deinit(self: *@This()) void { + switch (std.meta.activeTag(self.*)) { + inline else => |tag| bun.memory.deinit(&@field(self, @tagName(tag))), + } + self.* = undefined; + } + }; + + pub const Bindgen${name} = struct { + const Self = @This(); + pub const ZigType = ${name}; + pub const ExternType = bindgen.ExternTaggedUnion(&.{ ${alternatives + .map(a => a.bindgenType + ".ExternType") + .join(", ")} }); + pub fn convertFromExtern(extern_value: Self.ExternType) Self.ZigType { + return switch (extern_value.tag) { + ${joinIndented( + 14, + Object.entries(namedAlternatives).map(([altName, altType], i) => { + const bindgenType = altType.bindgenType; + const innerRhs = `${bindgenType}.convertFromExtern(extern_value.data.@"${i}")`; + return `${i} => .{ .${altName} = ${innerRhs} },`; + }), + )} + else => unreachable, + }; + } + }; + + const bindgen_generated = @import("bindgen_generated"); + const std = @import("std"); + const bun = @import("bun"); + const bindgen = bun.bun_js.bindgen; + `); + } + })(); +} diff --git a/src/codegen/bindgenv2/lib.ts b/src/codegen/bindgenv2/lib.ts new file mode 100644 index 0000000000..ce92a319e9 --- /dev/null +++ b/src/codegen/bindgenv2/lib.ts @@ -0,0 +1,10 @@ +// organize-imports-ignore +export { bool, u8, u16, u32, u64, i8, i16, i32, i64, f64 } from "./internal/primitives"; +export { RawAny, StrongAny } from "./internal/any"; +export { String } from "./internal/string"; +export { optional, nullable, undefined, null } from "./internal/optional"; +export { union } from "./internal/union"; +export { dictionary } from "./internal/dictionary"; +export { enumeration } from "./internal/enumeration"; +export { Array } from "./internal/array"; +export { ArrayBuffer, Blob } from "./internal/interfaces"; diff --git a/src/codegen/bindgenv2/script.ts b/src/codegen/bindgenv2/script.ts new file mode 100755 index 0000000000..6d4e96e197 --- /dev/null +++ b/src/codegen/bindgenv2/script.ts @@ -0,0 +1,185 @@ +#!/usr/bin/env bun +import * as helpers from "../helpers"; +import { NamedType, Type } from "./internal/base"; + +const USAGE = `\ +Usage: script.ts [options] + +Options (all required): + --command= Command to run (see below) + --sources= Comma-separated list of *.bindv2.ts files + --codegen-path= Path to build/*/codegen + +Commands: + list-outputs List files that will be generated, separated by semicolons (for CMake) + generate Generate all files +`; + +let codegenPath: string; +let sources: string[]; + +function getNamedExports(): NamedType[] { + return sources.flatMap(path => { + const exports = import.meta.require(path); + return Object.values(exports).filter(v => v instanceof NamedType); + }); +} + +function getNamedDependencies(type: Type, result: Set): void { + for (const dependency of type.dependencies) { + if (dependency instanceof NamedType) { + result.add(dependency); + } + getNamedDependencies(dependency, result); + } +} + +function cppHeaderPath(type: NamedType): string { + return `${codegenPath}/Generated${type.name}.h`; +} + +function cppSourcePath(type: NamedType): string { + return `${codegenPath}/Generated${type.name}.cpp`; +} + +function zigSourcePath(typeOrNamespace: NamedType | string): string { + let ns: string; + if (typeof typeOrNamespace === "string") { + ns = typeOrNamespace; + } else { + ns = toZigNamespace(typeOrNamespace.name); + } + return `${codegenPath}/bindgen_generated/${ns}.zig`; +} + +function toZigNamespace(name: string): string { + const result = name + .replace(/([^A-Z_])([A-Z])/g, "$1_$2") + .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2") + .toLowerCase(); + if (result === name) { + return result + "_namespace"; + } + return result; +} + +function listOutputs(): void { + const outputs: string[] = [`${codegenPath}/bindgen_generated.zig`]; + for (const type of getNamedExports()) { + if (type.hasCppSource) outputs.push(cppSourcePath(type)); + if (type.hasZigSource) outputs.push(zigSourcePath(type)); + } + process.stdout.write(outputs.join(";")); +} + +function generate(): void { + const names = new Set(); + const zigRoot: string[] = []; + const zigRootInternal: string[] = []; + + const namedExports = getNamedExports(); + { + const namedDependencies = new Set(); + for (const type of namedExports) { + getNamedDependencies(type, namedDependencies); + } + const namedExportsSet = new Set(namedExports); + for (const type of namedDependencies) { + if (!namedExportsSet.has(type)) { + console.error(`error: named type must be exported: ${type.name}`); + process.exit(1); + } + } + const namedTypeNames = new Set(); + for (const type of namedExports) { + if (namedTypeNames.size == namedTypeNames.add(type.name).size) { + console.error(`error: multiple types with same name: ${type.name}`); + process.exit(1); + } + } + } + + for (const type of namedExports) { + const zigNamespace = toZigNamespace(type.name); + const size = names.size; + names.add(type.name); + names.add(zigNamespace); + if (names.size !== size + 2) { + console.error(`error: duplicate name: ${type.name}`); + process.exit(1); + } + + const cppHeader = type.cppHeader; + const cppSource = type.cppSource; + const zigSource = type.zigSource; + if (cppHeader) { + helpers.writeIfNotChanged(cppHeaderPath(type), cppHeader); + } + if (cppSource) { + helpers.writeIfNotChanged(cppSourcePath(type), cppSource); + } + if (zigSource) { + zigRoot.push( + `pub const ${zigNamespace} = @import("./bindgen_generated/${zigNamespace}.zig");`, + `pub const ${type.name} = ${zigNamespace}.${type.name};`, + "", + ); + zigRootInternal.push(`pub const ${type.name} = ${zigNamespace}.Bindgen${type.name};`); + helpers.writeIfNotChanged(zigSourcePath(zigNamespace), zigSource); + } + } + + helpers.writeIfNotChanged( + `${codegenPath}/bindgen_generated.zig`, + [ + ...zigRoot, + `pub const internal = struct {`, + ...zigRootInternal.map(s => " " + s), + `};`, + "", + ].join("\n"), + ); +} + +function main(): void { + const args = helpers.argParse(["command", "codegen-path", "sources", "help"]); + if (Object.keys(args).length === 0) { + process.stderr.write(USAGE); + process.exit(1); + } + const { command, "codegen-path": codegenPathArg, sources: sourcesArg, help } = args; + if (help != null) { + process.stdout.write(USAGE); + process.exit(0); + } + + if (typeof codegenPathArg !== "string") { + console.error("error: missing --codegen-path"); + process.exit(1); + } + codegenPath = codegenPathArg; + + if (typeof sourcesArg !== "string") { + console.error("error: missing --sources"); + process.exit(1); + } + sources = sourcesArg.split(",").filter(x => x); + + switch (command) { + case "list-outputs": + listOutputs(); + break; + case "generate": + generate(); + break; + default: + if (typeof command === "string") { + console.error("error: unknown command: " + command); + } else { + console.error("error: missing --command"); + } + process.exit(1); + } +} + +main(); diff --git a/src/codegen/bindgenv2/tsconfig.json b/src/codegen/bindgenv2/tsconfig.json new file mode 100644 index 0000000000..2f087e4473 --- /dev/null +++ b/src/codegen/bindgenv2/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "noUnusedLocals": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitAny": true, + "noImplicitThis": true, + "exactOptionalPropertyTypes": true + }, + "include": ["**/*.ts", "../helpers.ts"] +} diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 18a5941d02..a89b703fb5 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -114,7 +114,7 @@ for (let i = 0; i < nativeStartIndex; i++) { `Cannot use ESM import statement within builtin modules. Use require("${imp.path}") instead. See src/js/README.md (from ${moduleList[i]})`, ); err.name = "BunError"; - err.fileName = moduleList[i]; + err["fileName"] = moduleList[i]; throw err; } } @@ -125,7 +125,7 @@ for (let i = 0; i < nativeStartIndex; i++) { `Using \`export default\` AND named exports together in builtin modules is unsupported. See src/js/README.md (from ${moduleList[i]})`, ); err.name = "BunError"; - err.fileName = moduleList[i]; + err["fileName"] = moduleList[i]; throw err; } let importStatements: string[] = []; diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index 985cc01053..a1792fdb38 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -286,7 +286,7 @@ export function define( Object.entries(klass) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => { - v.DOMJIT = undefined; + v["DOMJIT"] = undefined; return [k, v]; }), ), @@ -294,7 +294,7 @@ export function define( Object.entries(proto) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => { - v.DOMJIT = undefined; + v["DOMJIT"] = undefined; return [k, v]; }), ), diff --git a/src/codegen/helpers.ts b/src/codegen/helpers.ts index e6868ccd61..4a9665b4b4 100644 --- a/src/codegen/helpers.ts +++ b/src/codegen/helpers.ts @@ -132,15 +132,20 @@ export function pascalCase(string: string) { } export function argParse(keys: string[]): any { - const options = {}; + const options: { [key: string]: boolean | string } = {}; for (const arg of process.argv.slice(2)) { if (!arg.startsWith("--")) { - console.error("Unknown argument " + arg); + console.error("error: unknown argument: " + arg); process.exit(1); } - const split = arg.split("="); - const value = split[1] || "true"; - options[split[0].slice(2)] = value; + const splitPos = arg.indexOf("="); + let name = arg; + let value: boolean | string = true; + if (splitPos !== -1) { + name = arg.slice(0, splitPos); + value = arg.slice(splitPos + 1); + } + options[name.slice(2)] = value; } const unknown = new Set(Object.keys(options)); @@ -148,7 +153,7 @@ export function argParse(keys: string[]): any { unknown.delete(key); } for (const key of unknown) { - console.error("Unknown argument: --" + key); + console.error("error: unknown argument: --" + key); } if (unknown.size > 0) process.exit(1); return options; diff --git a/src/codegen/replacements.ts b/src/codegen/replacements.ts index fd1f3438ad..a0b43be968 100644 --- a/src/codegen/replacements.ts +++ b/src/codegen/replacements.ts @@ -253,7 +253,7 @@ export function applyReplacements(src: string, length: number) { } } - const id = registerNativeCall(kind, args[0], args[1], is_create_fn ? args[2] : undefined); + const id = registerNativeCall(kind, 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") { @@ -305,7 +305,7 @@ export function applyReplacements(src: string, length: number) { throw new Error(`$${name} takes two string arguments, but got '$${name}${inner.result}'`); } - const id = registerNativeCall("bind", args[0], args[1], undefined); + const id = registerNativeCall("bind", args[0], args[1], null); return [slice.slice(0, match.index) + "__intrinsic__lazy(" + id + ")", inner.rest, true]; } else { diff --git a/src/deps/uws/SocketContext.zig b/src/deps/uws/SocketContext.zig index d2737f270f..2672402e7e 100644 --- a/src/deps/uws/SocketContext.zig +++ b/src/deps/uws/SocketContext.zig @@ -229,11 +229,11 @@ pub const SocketContext = opaque { ca_file_name: [*c]const u8 = null, ssl_ciphers: [*c]const u8 = null, ssl_prefer_low_memory_usage: i32 = 0, - key: ?[*]?[*:0]const u8 = null, + key: ?[*]const ?[*:0]const u8 = null, key_count: u32 = 0, - cert: ?[*]?[*:0]const u8 = null, + cert: ?[*]const ?[*:0]const u8 = null, cert_count: u32 = 0, - ca: ?[*]?[*:0]const u8 = null, + ca: ?[*]const ?[*:0]const u8 = null, ca_count: u32 = 0, secure_options: u32 = 0, reject_unauthorized: i32 = 0, diff --git a/src/meta.zig b/src/meta.zig index 964235a26f..89658ea945 100644 --- a/src/meta.zig +++ b/src/meta.zig @@ -195,6 +195,8 @@ fn CreateUniqueTuple(comptime N: comptime_int, comptime types: [N]type) type { }); } +pub const TaggedUnion = @import("./meta/tagged_union.zig").TaggedUnion; + pub fn hasStableMemoryLayout(comptime T: type) bool { const tyinfo = @typeInfo(T); return switch (tyinfo) { diff --git a/src/meta/tagged_union.zig b/src/meta/tagged_union.zig new file mode 100644 index 0000000000..0d32507926 --- /dev/null +++ b/src/meta/tagged_union.zig @@ -0,0 +1,236 @@ +fn deinitImpl(comptime Union: type, value: *Union) void { + switch (std.meta.activeTag(value.*)) { + inline else => |tag| bun.memory.deinit(&@field(value, @tagName(tag))), + } + value.* = undefined; +} + +/// Creates a tagged union with fields corresponding to `field_types`. The fields are named +/// @"0", @"1", @"2", etc. +pub fn TaggedUnion(comptime field_types: []const type) type { + // Types created with @Type can't contain decls, so in order to have a `deinit` method, we + // have to do it this way... + return switch (comptime field_types.len) { + 0 => @compileError("cannot create an empty tagged union"), + 1 => union(enum) { + @"0": field_types[0], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 2 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 3 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 4 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 5 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 6 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 7 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 8 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 9 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 10 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 11 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 12 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 13 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 14 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + @"13": field_types[13], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 15 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + @"13": field_types[13], + @"14": field_types[14], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + 16 => union(enum) { + @"0": field_types[0], + @"1": field_types[1], + @"2": field_types[2], + @"3": field_types[3], + @"4": field_types[4], + @"5": field_types[5], + @"6": field_types[6], + @"7": field_types[7], + @"8": field_types[8], + @"9": field_types[9], + @"10": field_types[10], + @"11": field_types[11], + @"12": field_types[12], + @"13": field_types[13], + @"14": field_types[14], + @"15": field_types[15], + pub fn deinit(self: *@This()) void { + deinitImpl(@This(), self); + } + }, + else => @compileError("too many union fields"), + }; +} + +const bun = @import("bun"); +const std = @import("std"); diff --git a/src/napi/napi.zig b/src/napi/napi.zig index d961c52a8f..779e0acaf3 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -832,7 +832,7 @@ pub export fn napi_get_typedarray_info( maybe_length: ?*usize, maybe_data: ?*[*]u8, maybe_arraybuffer: ?*napi_value, - maybe_byte_offset: ?*usize, + maybe_byte_offset: ?*usize, // note: this is always 0 ) napi_status { log("napi_get_typedarray_info", .{}); const env = env_ orelse { @@ -859,7 +859,10 @@ pub export fn napi_get_typedarray_info( arraybuffer.set(env, JSValue.c(jsc.C.JSObjectGetTypedArrayBuffer(env.toJS().ref(), typedarray.asObjectRef(), null))); if (maybe_byte_offset) |byte_offset| - byte_offset.* = array_buffer.offset; + // `jsc.ArrayBuffer` used to have an `offset` field, but it was always 0 because `ptr` + // already had the offset applied. See . + //byte_offset.* = array_buffer.offset; + byte_offset.* = 0; return env.ok(); } pub extern fn napi_create_dataview(env: napi_env, length: usize, arraybuffer: napi_value, byte_offset: usize, result: *napi_value) napi_status; @@ -881,7 +884,7 @@ pub export fn napi_get_dataview_info( maybe_bytelength: ?*usize, maybe_data: ?*[*]u8, maybe_arraybuffer: ?*napi_value, - maybe_byte_offset: ?*usize, + maybe_byte_offset: ?*usize, // note: this is always 0 ) napi_status { log("napi_get_dataview_info", .{}); const env = env_ orelse { @@ -900,7 +903,10 @@ pub export fn napi_get_dataview_info( arraybuffer.set(env, JSValue.c(jsc.C.JSObjectGetTypedArrayBuffer(env.toJS().ref(), dataview.asObjectRef(), null))); if (maybe_byte_offset) |byte_offset| - byte_offset.* = array_buffer.offset; + // `jsc.ArrayBuffer` used to have an `offset` field, but it was always 0 because `ptr` + // already had the offset applied. See . + //byte_offset.* = array_buffer.offset; + byte_offset.* = 0; return env.ok(); } diff --git a/src/node-fallbacks/build-fallbacks.ts b/src/node-fallbacks/build-fallbacks.ts index bb5d23b6ee..8e06d0d548 100644 --- a/src/node-fallbacks/build-fallbacks.ts +++ b/src/node-fallbacks/build-fallbacks.ts @@ -5,13 +5,13 @@ import { basename, extname } from "path"; const allFiles = fs.readdirSync(".").filter(f => f.endsWith(".js")); const outdir = process.argv[2]; const builtins = Module.builtinModules; -let commands = []; +let commands: Promise[] = []; -let moduleFiles = []; +let moduleFiles: string[] = []; for (const name of allFiles) { const mod = basename(name, extname(name)).replaceAll(".", "/"); const file = allFiles.find(f => f.startsWith(mod)); - moduleFiles.push(file); + moduleFiles.push(file as string); } for (let fileIndex = 0; fileIndex < allFiles.length; fileIndex++) { diff --git a/src/string.zig b/src/string.zig index b7524f0792..17d70e05e5 100644 --- a/src/string.zig +++ b/src/string.zig @@ -6,8 +6,9 @@ pub const PathString = @import("./string/PathString.zig").PathString; pub const SmolStr = @import("./string/SmolStr.zig").SmolStr; pub const StringBuilder = @import("./string/StringBuilder.zig"); pub const StringJoiner = @import("./string/StringJoiner.zig"); -pub const WTFStringImpl = @import("./string/WTFStringImpl.zig").WTFStringImpl; -pub const WTFStringImplStruct = @import("./string/WTFStringImpl.zig").WTFStringImplStruct; +pub const WTFString = @import("./string/wtf.zig").WTFString; +pub const WTFStringImpl = @import("./string/wtf.zig").WTFStringImpl; +pub const WTFStringImplStruct = @import("./string/wtf.zig").WTFStringImplStruct; pub const Tag = enum(u8) { /// String is not valid. Observed on some failed operations. @@ -47,7 +48,7 @@ pub const String = extern struct { pub const empty = String{ .tag = .Empty, .value = .{ .ZigString = .Empty } }; pub const dead = String{ .tag = .Dead, .value = .{ .Dead = {} } }; - pub const StringImplAllocator = @import("./string/WTFStringImpl.zig").StringImplAllocator; + pub const StringImplAllocator = @import("./string/wtf.zig").StringImplAllocator; pub fn toInt32(this: *const String) ?i32 { const val = bun.cpp.BunString__toInt32(this); diff --git a/src/string/WTFStringImpl.zig b/src/string/wtf.zig similarity index 100% rename from src/string/WTFStringImpl.zig rename to src/string/wtf.zig diff --git a/src/tsconfig.json b/src/tsconfig.json index 3f63af9f7a..9fd93876cd 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -4,10 +4,11 @@ // Path remapping "baseUrl": ".", "paths": { - "bindgen": ["./codegen/bindgen-lib.ts"] + "bindgen": ["./codegen/bindgen-lib.ts"], + "bindgenv2": ["./codegen/bindgenv2/lib.ts"] } }, "include": ["**/*.ts", "**/*.tsx"], // separate projects have extra settings that only apply in those scopes - "exclude": ["js", "bake"] + "exclude": ["js", "bake", "init", "create", "bun.js/bindings/libuv"] } diff --git a/test/js/bun/http/bun-server.test.ts b/test/js/bun/http/bun-server.test.ts index c4b6c70273..68922965e7 100644 --- a/test/js/bun/http/bun-server.test.ts +++ b/test/js/bun/http/bun-server.test.ts @@ -110,7 +110,7 @@ describe.concurrent("Server", () => { }, port: 0, }); - }).toThrow("tls option expects an object"); + }).toThrow("TLSOptions must be an object"); }); }); @@ -125,7 +125,7 @@ describe.concurrent("Server", () => { }, port: 0, }); - }).not.toThrow("tls option expects an object"); + }).not.toThrow("TLSOptions must be an object"); }); }); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index d52b25ba71..b25fa22e19 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -1611,7 +1611,7 @@ describe.concurrent("should error with invalid options", async () => { requestCert: "invalid", }, }); - }).toThrow('The "requestCert" property must be of type boolean, got string'); + }).toThrow("TLSOptions.requestCert must be a boolean"); }); it("rejectUnauthorized", () => { expect(() => { @@ -1624,7 +1624,7 @@ describe.concurrent("should error with invalid options", async () => { rejectUnauthorized: "invalid", }, }); - }).toThrow('The "rejectUnauthorized" property must be of type boolean, got string'); + }).toThrow("TLSOptions.rejectUnauthorized must be a boolean"); }); it("lowMemoryMode", () => { expect(() => { @@ -1638,7 +1638,7 @@ describe.concurrent("should error with invalid options", async () => { lowMemoryMode: "invalid", }, }); - }).toThrow("Expected lowMemoryMode to be a boolean"); + }).toThrow("TLSOptions.lowMemoryMode must be a boolean"); }); it("multiple missing server name", () => { expect(() => { diff --git a/test/js/bun/net/tcp-server.test.ts b/test/js/bun/net/tcp-server.test.ts index 6cd76e1ca5..b9e74a8267 100644 --- a/test/js/bun/net/tcp-server.test.ts +++ b/test/js/bun/net/tcp-server.test.ts @@ -71,7 +71,7 @@ it("should not allow invalid tls option", () => { hostname: "localhost", tls: value, }); - }).toThrow("tls option expects an object"); + }).toThrow("TLSOptions must be an object"); }); }); @@ -89,7 +89,7 @@ it("should allow using false, null or undefined tls option", () => { hostname: "localhost", tls: value, }); - }).not.toThrow("tls option expects an object"); + }).not.toThrow("TLSOptions must be an object"); }); }); diff --git a/tsconfig.base.json b/tsconfig.base.json index a28d20e3fa..1da8c6b919 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -24,7 +24,7 @@ "noFallthroughCasesInSwitch": true, "isolatedModules": true, - // Stricter type-checking + // Less strict type-checking "noUnusedLocals": false, "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false,