From 42323f8995681fbcd749c63b0da88cda8546c924 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 11:53:19 -0700 Subject: [PATCH 01/29] Port `SocketConfig` to bindings generator (For internal tracking: fixes STAB-1471, STAB-1472, STAB-1473, STAB-1474, STAB-1475, STAB-1476) --- src/bun.js/api/bun/socket.zig | 18 +- src/bun.js/api/bun/socket/Handlers.zig | 306 ++++++++---------- src/bun.js/api/bun/socket/Listener.zig | 300 ++++++++--------- .../api/bun/socket/SocketConfig.bindv2.ts | 92 ++++++ src/bun.js/api/server/SSLConfig.zig | 28 +- src/bun.js/bindings/Bindgen/ExternTraits.h | 2 +- src/bun.js/bindings/Bindgen/ExternUnion.h | 46 ++- src/bun.js/bindings/ZigString.zig | 21 +- src/codegen/bindgenv2/internal/enumeration.ts | 31 +- src/codegen/bindgenv2/tsconfig.json | 7 +- src/memory.zig | 32 ++ 11 files changed, 503 insertions(+), 380 deletions(-) create mode 100644 src/bun.js/api/bun/socket/SocketConfig.bindv2.ts diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 369ffb5f155..438e0555392 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -574,8 +574,8 @@ pub fn NewSocket(comptime ssl: bool) type { // clean onOpen callback so only called in the first handshake and not in every renegotiation // on servers this would require a different approach but it's not needed because our servers will not call handshake multiple times // servers don't support renegotiation - this.handlers.?.onOpen.unprotect(); - this.handlers.?.onOpen = .zero; + handlers.onOpen.unprotect(); + handlers.onOpen = .zero; } } else { // call handhsake callback with authorized and authorization error if has one @@ -1349,14 +1349,12 @@ pub fn NewSocket(comptime ssl: bool) type { return globalObject.throw("Expected \"socket\" option", .{}); }; - var prev_handlers = this.getHandlers(); - - const handlers = try Handlers.fromJS(globalObject, socket_obj, prev_handlers.is_server); - - prev_handlers.unprotect(); - this.handlers.?.* = handlers; // TODO: this is a memory leak - this.handlers.?.withAsyncContextIfNeeded(globalObject); - this.handlers.?.protect(); + var this_handlers = this.getHandlers(); + var handlers = try Handlers.fromJS(globalObject, socket_obj, this_handlers.is_server); + this_handlers.unprotect(); + handlers.protect(); + handlers.withAsyncContextIfNeeded(globalObject); + this_handlers.* = handlers; // TODO: this is a memory leak return .js_undefined; } diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index d902d677dd7..fa5baf7112f 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -18,7 +18,19 @@ active_connections: u32 = 0, is_server: bool, promise: jsc.Strong.Optional = .empty, -protection_count: bun.DebugOnly(u32) = if (Environment.isDebug) 0, +protection_count: if (Environment.ci_assert) u32 else void = if (Environment.ci_assert) 0, + +const callback_fields = .{ + "onOpen", + "onClose", + "onData", + "onWritable", + "onTimeout", + "onConnectError", + "onEnd", + "onError", + "onHandshake", +}; pub fn markActive(this: *Handlers) void { Listener.log("markActive", .{}); @@ -103,53 +115,50 @@ pub fn callErrorHandler(this: *Handlers, thisValue: JSValue, args: *const [2]JSV return true; } -pub fn fromJS(globalObject: *jsc.JSGlobalObject, opts: jsc.JSValue, is_server: bool) bun.JSError!Handlers { - var handlers = Handlers{ +/// Does not `protect` the handlers. +pub fn fromJS( + globalObject: *jsc.JSGlobalObject, + opts: jsc.JSValue, + is_server: bool, +) bun.JSError!Handlers { + var generated: jsc.generated.SocketConfigHandlers = try .fromJS(globalObject, opts); + defer generated.deinit(); + return .fromGenerated(globalObject, &generated, is_server); +} + +pub fn fromGenerated( + globalObject: *jsc.JSGlobalObject, + generated: *const jsc.generated.SocketConfigHandlers, + is_server: bool, +) bun.JSError!Handlers { + var result: Handlers = .{ .vm = globalObject.bunVM(), .globalObject = globalObject, .is_server = is_server, + .binary_type = switch (generated.binary_type) { + .arraybuffer => .ArrayBuffer, + .buffer => .Buffer, + .uint8array => .Uint8Array, + }, }; - - if (opts.isEmptyOrUndefinedOrNull() or opts.isBoolean() or !opts.isObject()) { - return globalObject.throwInvalidArguments("Expected \"socket\" to be an object", .{}); - } - - const pairs = .{ - .{ "onData", "data" }, - .{ "onWritable", "drain" }, - .{ "onOpen", "open" }, - .{ "onClose", "close" }, - .{ "onTimeout", "timeout" }, - .{ "onConnectError", "connectError" }, - .{ "onEnd", "end" }, - .{ "onError", "error" }, - .{ "onHandshake", "handshake" }, - }; - inline for (pairs) |pair| { - if (try opts.getTruthyComptime(globalObject, pair.@"1")) |callback_value| { - if (!callback_value.isCell() or !callback_value.isCallable()) { - return globalObject.throwInvalidArguments("Expected \"{s}\" callback to be a function", .{pair[1]}); - } - - @field(handlers, pair.@"0") = callback_value; + inline for (callback_fields) |field| { + const value = @field(generated, field); + if (value.isUndefinedOrNull()) {} else if (!value.isCallable()) { + return globalObject.throwInvalidArguments( + "Expected \"{s}\" callback to be a function", + .{field}, + ); + } else { + @field(result, field) = value; } } - - if (handlers.onData == .zero and handlers.onWritable == .zero) { - return globalObject.throwInvalidArguments("Expected at least \"data\" or \"drain\" callback", .{}); + if (result.onData == .zero and result.onWritable == .zero) { + return globalObject.throwInvalidArguments( + "Expected at least \"data\" or \"drain\" callback", + .{}, + ); } - - if (try opts.getTruthy(globalObject, "binaryType")) |binary_type_value| { - if (!binary_type_value.isString()) { - return globalObject.throwInvalidArguments("Expected \"binaryType\" to be a string", .{}); - } - - handlers.binary_type = try BinaryType.fromJSValue(globalObject, binary_type_value) orelse { - return globalObject.throwInvalidArguments("Expected 'binaryType' to be 'ArrayBuffer', 'Uint8Array', or 'Buffer'", .{}); - }; - } - - return handlers; + return result; } pub fn unprotect(this: *Handlers) void { @@ -157,7 +166,7 @@ pub fn unprotect(this: *Handlers) void { return; } - if (comptime Environment.isDebug) { + if (comptime Environment.ci_assert) { bun.assert(this.protection_count > 0); this.protection_count -= 1; } @@ -173,17 +182,7 @@ pub fn unprotect(this: *Handlers) void { } pub fn withAsyncContextIfNeeded(this: *Handlers, globalObject: *jsc.JSGlobalObject) void { - inline for (.{ - "onOpen", - "onClose", - "onData", - "onWritable", - "onTimeout", - "onConnectError", - "onEnd", - "onError", - "onHandshake", - }) |field| { + inline for (callback_fields) |field| { const value = @field(this, field); if (value != .zero) { @field(this, field) = value.withAsyncContextIfNeeded(globalObject); @@ -192,7 +191,7 @@ pub fn withAsyncContextIfNeeded(this: *Handlers, globalObject: *jsc.JSGlobalObje } pub fn protect(this: *Handlers) void { - if (comptime Environment.isDebug) { + if (comptime Environment.ci_assert) { this.protection_count += 1; } this.onOpen.protect(); @@ -206,6 +205,7 @@ pub fn protect(this: *Handlers) void { this.onHandshake.protect(); } +/// `handlers` is always `protect`ed in this struct. pub const SocketConfig = struct { hostname_or_unix: jsc.ZigString.Slice, port: ?u16 = null, @@ -218,6 +218,23 @@ pub const SocketConfig = struct { reusePort: bool = false, ipv6Only: bool = false, + /// Deinitializes everything and `unprotect`s `handlers`. + pub fn deinit(this: *SocketConfig) void { + this.handlers.unprotect(); + this.deinitExcludingHandlers(); + this.handlers = undefined; + } + + /// Deinitializes everything but does not `unprotect` `handlers`. + pub fn deinitExcludingHandlers(this: *SocketConfig) void { + this.hostname_or_unix.deinit(); + bun.memory.deinit(&this.ssl); + const handlers = this.handlers; + this.* = undefined; + // make sure pointers to `this.handlers` are still valid + this.handlers = handlers; + } + pub fn socketFlags(this: *const SocketConfig) i32 { var flags: i32 = if (this.exclusive) uws.LIBUS_LISTEN_EXCLUSIVE_PORT @@ -236,137 +253,68 @@ pub const SocketConfig = struct { return flags; } - pub fn fromJS(vm: *jsc.VirtualMachine, opts: jsc.JSValue, globalObject: *jsc.JSGlobalObject, is_server: bool) bun.JSError!SocketConfig { - var hostname_or_unix: jsc.ZigString.Slice = jsc.ZigString.Slice.empty; - errdefer hostname_or_unix.deinit(); - var port: ?u16 = null; - var fd: ?bun.FileDescriptor = null; - var exclusive = false; - var allowHalfOpen = false; - var reusePort = false; - var ipv6Only = false; - - var ssl: ?SSLConfig = null; - var default_data = JSValue.zero; - - if (try opts.getTruthy(globalObject, "tls")) |tls| { - if (!tls.isBoolean()) { - ssl = try SSLConfig.fromJS(vm, globalObject, tls); - } else if (tls.toBoolean()) { - ssl = SSLConfig.zero; - } - } - - errdefer bun.memory.deinit(&ssl); - - hostname_or_unix: { - if (try opts.getTruthy(globalObject, "fd")) |fd_| { - if (fd_.isNumber()) { - fd = fd_.asFileDescriptor(); - break :hostname_or_unix; - } - } - - if (try opts.getStringish(globalObject, "unix")) |unix_socket| { - defer unix_socket.deref(); - - hostname_or_unix = try unix_socket.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator); - - if (strings.hasPrefixComptime(hostname_or_unix.slice(), "file://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "unix://") or strings.hasPrefixComptime(hostname_or_unix.slice(), "sock://")) { - // The memory allocator relies on the pointer address to - // free it, so if we simply moved the pointer up it would - // cause an issue when freeing it later. - const moved_bytes = try bun.default_allocator.dupeZ(u8, hostname_or_unix.slice()[7..]); - hostname_or_unix.deinit(); - hostname_or_unix = ZigString.Slice.init(bun.default_allocator, moved_bytes); - } - - if (hostname_or_unix.len > 0) { - break :hostname_or_unix; - } - } - - if (try opts.getBooleanLoose(globalObject, "exclusive")) |exclusive_| { - exclusive = exclusive_; - } - if (try opts.getBooleanLoose(globalObject, "allowHalfOpen")) |allow_half_open| { - allowHalfOpen = allow_half_open; - } - - if (try opts.getBooleanLoose(globalObject, "reusePort")) |reuse_port| { - reusePort = reuse_port; - } + pub fn fromGenerated( + vm: *jsc.VirtualMachine, + global: *jsc.JSGlobalObject, + generated: *const jsc.generated.SocketConfig, + is_server: bool, + ) bun.JSError!SocketConfig { + var result: SocketConfig = .{ + .hostname_or_unix = .empty, + .fd = if (generated.fd) |fd| .fromUV(fd) else null, + .ssl = switch (generated.tls) { + .none => null, + .boolean => |b| if (b) .zero else null, + .object => |*ssl| try .fromGenerated(vm, global, ssl), + }, + .handlers = try .fromGenerated(global, &generated.handlers, is_server), + .default_data = if (generated.data.isUndefined()) .zero else generated.data, + }; + result.handlers.protect(); + errdefer result.deinit(); - if (try opts.getBooleanLoose(globalObject, "ipv6Only")) |ipv6_only| { - ipv6Only = ipv6_only; + if (result.fd != null) {} else if (generated.unix_.get()) |unix| { + if (unix.length() == 0) { + return global.throwInvalidArguments("\"unix\" cannot be empty", .{}); } - - if (try opts.getStringish(globalObject, "hostname") orelse try opts.getStringish(globalObject, "host")) |hostname| { - defer hostname.deref(); - - var port_value = try opts.get(globalObject, "port") orelse JSValue.zero; - hostname_or_unix = try hostname.toUTF8WithoutRef(bun.default_allocator).cloneIfNeeded(bun.default_allocator); - - if (port_value.isEmptyOrUndefinedOrNull() and hostname_or_unix.len > 0) { - const parsed_url = bun.URL.parse(hostname_or_unix.slice()); - if (parsed_url.getPort()) |port_num| { - port_value = JSValue.jsNumber(port_num); - if (parsed_url.hostname.len > 0) { - const moved_bytes = try bun.default_allocator.dupeZ(u8, parsed_url.hostname); - hostname_or_unix.deinit(); - hostname_or_unix = ZigString.Slice.init(bun.default_allocator, moved_bytes); - } - } - } - - if (port_value.isEmptyOrUndefinedOrNull()) { - return globalObject.throwInvalidArguments("Expected \"port\" to be a number between 0 and 65535", .{}); - } - - const porti32 = try port_value.coerceToInt32(globalObject); - if (porti32 < 0 or porti32 > 65535) { - return globalObject.throwInvalidArguments("Expected \"port\" to be a number between 0 and 65535", .{}); - } - - port = @intCast(porti32); - - if (hostname_or_unix.len == 0) { - return globalObject.throwInvalidArguments("Expected \"hostname\" to be a non-empty string", .{}); - } - - if (hostname_or_unix.len > 0) { - break :hostname_or_unix; - } + result.hostname_or_unix = unix.toUTF8(bun.default_allocator); + const slice = result.hostname_or_unix.slice(); + if (strings.hasPrefixComptime(slice, "file://") or + strings.hasPrefixComptime(slice, "unix://") or + strings.hasPrefixComptime(slice, "sock://")) + { + const without_prefix = try bun.default_allocator.dupe(u8, slice[7..]); + result.hostname_or_unix.deinit(); + result.hostname_or_unix = .init(bun.default_allocator, without_prefix); } - - if (hostname_or_unix.len == 0) { - return globalObject.throwInvalidArguments("Expected \"unix\" or \"hostname\" to be a non-empty string", .{}); + } else if (generated.hostname.get()) |hostname| { + if (hostname.length() == 0) { + return global.throwInvalidArguments("\"hostname\" cannot be non-empty", .{}); } - - return globalObject.throwInvalidArguments("Expected either \"hostname\" or \"unix\"", .{}); - } - - var handlers = try Handlers.fromJS(globalObject, try opts.get(globalObject, "socket") orelse JSValue.zero, is_server); - - if (try opts.fastGet(globalObject, .data)) |default_data_value| { - default_data = default_data_value; + result.hostname_or_unix = hostname.toUTF8(bun.default_allocator); + const slice = result.hostname_or_unix.slice(); + result.port = generated.port orelse bun.URL.parse(slice).getPort() orelse { + return global.throwInvalidArguments("Missing \"port\"", .{}); + }; + result.exclusive = generated.exclusive; + result.allowHalfOpen = generated.allow_half_open; + result.reusePort = generated.reuse_port; + result.ipv6Only = generated.ipv6_only; + } else { + return global.throwInvalidArguments("Expected either \"hostname\" or \"unix\"", .{}); } + return result; + } - handlers.withAsyncContextIfNeeded(globalObject); - handlers.protect(); - - return SocketConfig{ - .hostname_or_unix = hostname_or_unix, - .port = port, - .fd = fd, - .ssl = ssl, - .handlers = handlers, - .default_data = default_data, - .exclusive = exclusive, - .allowHalfOpen = allowHalfOpen, - .reusePort = reusePort, - .ipv6Only = ipv6Only, - }; + pub fn fromJS( + vm: *jsc.VirtualMachine, + opts: jsc.JSValue, + globalObject: *jsc.JSGlobalObject, + is_server: bool, + ) bun.JSError!SocketConfig { + var generated: jsc.generated.SocketConfig = try .fromJS(globalObject, opts); + defer generated.deinit(); + return .fromGenerated(vm, globalObject, &generated, is_server); } }; diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 84afd1b5e88..43f0d6b952c 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -92,12 +92,10 @@ pub fn reload(this: *Listener, globalObject: *jsc.JSGlobalObject, callframe: *js }; var handlers = try Handlers.fromJS(globalObject, socket_obj, this.handlers.is_server); - - var prev_handlers = &this.handlers; - prev_handlers.unprotect(); + this.handlers.unprotect(); + handlers.protect(); handlers.withAsyncContextIfNeeded(globalObject); this.handlers = handlers; // TODO: this is a memory leak - this.handlers.protect(); return .js_undefined; } @@ -111,69 +109,71 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa const vm = jsc.VirtualMachine.get(); var socket_config = try SocketConfig.fromJS(vm, opts, globalObject, true); + defer socket_config.deinitExcludingHandlers(); - var hostname_or_unix = socket_config.hostname_or_unix; - const port = socket_config.port; - var ssl = socket_config.ssl; - var handlers = socket_config.handlers; - var protos: ?[]const u8 = null; + const handlers = &socket_config.handlers; + // Only unprotect handlers if there's an error; otherwise we put them in a `Listener` and still + // want them protected. + errdefer handlers.unprotect(); + const hostname_or_unix = &socket_config.hostname_or_unix; + const port = socket_config.port; + const ssl = if (socket_config.ssl) |*ssl| ssl else null; const ssl_enabled = ssl != null; - const socket_flags = socket_config.socketFlags(); - defer if (ssl) |*_ssl| _ssl.deinit(); - - if (Environment.isWindows) { - if (port == null) { - // we check if the path is a named pipe otherwise we try to connect using AF_UNIX - const slice = hostname_or_unix.slice(); - var buf: bun.PathBuffer = undefined; - if (normalizePipeName(slice, buf[0..])) |pipe_name| { - const connection: Listener.UnixOrHost = .{ .unix = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice() }; - if (ssl_enabled) { - if (ssl.?.protos) |p| { - protos = std.mem.span(p); - } - } - var socket = Listener{ - .handlers = handlers, - .connection = connection, - .ssl = ssl_enabled, - .socket_context = null, - .listener = .none, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p)) else null, - }; - - vm.eventLoop().ensureWaker(); - socket.handlers.protect(); + if (Environment.isWindows and port == null) { + // we check if the path is a named pipe otherwise we try to connect using AF_UNIX + var buf: bun.PathBuffer = undefined; + if (normalizePipeName(hostname_or_unix.slice(), buf[0..])) |pipe_name| { + const connection: Listener.UnixOrHost = .{ + .unix = bun.handleOom(hostname_or_unix.intoOwnedSlice(bun.default_allocator)), + }; - if (socket_config.default_data != .zero) { - socket.strong_data = .create(socket_config.default_data, globalObject); - } + var socket: Listener = .{ + .handlers = handlers.*, + .connection = connection, + .ssl = ssl_enabled, + .socket_context = null, + .listener = .none, + .protos = if (ssl) |s| s.takeProtos() else null, + }; - var this: *Listener = bun.handleOom(handlers.vm.allocator.create(Listener)); - this.* = socket; - //TODO: server_name is not supported on named pipes, I belive its , lets wait for someone to ask for it + vm.eventLoop().ensureWaker(); - 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_ptr, this) catch { - this.deinit(); - return globalObject.throwInvalidArguments("Failed to listen at {s}", .{pipe_name}); - }, - }; + if (socket_config.default_data != .zero) { + socket.strong_data = .create(socket_config.default_data, globalObject); + } - const this_value = this.toJS(globalObject); - this.strong_self.set(globalObject, this_value); - this.poll_ref.ref(handlers.vm); + const this: *Listener = bun.handleOom(handlers.vm.allocator.create(Listener)); + this.* = socket; + // TODO: server_name is not supported on named pipes, I belive its , lets wait for + // someone to ask for it + errdefer this.deinit(); + + 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 return globalObject.throwInvalidArguments( + "Failed to listen at {s}", + .{pipe_name}, + ), + }; - return this_value; - } + const this_value = this.toJS(globalObject); + this.strong_self.set(globalObject, this_value); + this.poll_ref.ref(handlers.vm); + return this_value; } } - const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |*some_ssl| + + const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |some_ssl| some_ssl.asUSockets() else .{}; @@ -185,12 +185,10 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa true => uws.SocketContext.createSSLContext(uws.Loop.get(), @sizeOf(usize), ctx_opts, &create_err), false => uws.SocketContext.createNoSSLContext(uws.Loop.get(), @sizeOf(usize)), } orelse { - var err = globalObject.createErrorInstance("Failed to listen on {s}:{d}", .{ hostname_or_unix.slice(), port orelse 0 }); - defer { - socket_config.handlers.unprotect(); - hostname_or_unix.deinit(); - } - + var err = globalObject.createErrorInstance( + "Failed to listen on {s}:{d}", + .{ hostname_or_unix.slice(), port orelse 0 }, + ); const errno = @intFromEnum(bun.sys.getErrno(@as(c_int, -1))); if (errno != 0) { err.put(globalObject, ZigString.static("errno"), JSValue.jsNumber(errno)); @@ -198,15 +196,10 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa err.put(globalObject, ZigString.static("code"), ZigString.init(@tagName(str)).toJS(globalObject)); } } - return globalObject.throwValue(err); }; if (ssl_enabled) { - if (ssl.?.protos) |p| { - protos = std.mem.span(p); - } - uws.NewSocketHandler(true).configure( socket_context, true, @@ -242,11 +235,14 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa ); } + const hostname = bun.handleOom(hostname_or_unix.intoOwnedSlice(bun.default_allocator)); var connection: Listener.UnixOrHost = if (port) |port_| .{ - .host = .{ .host = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice(), .port = port_ }, - } else if (socket_config.fd) |fd| .{ .fd = fd } else .{ - .unix = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice(), - }; + .host = .{ + .host = hostname, + .port = port_, + }, + } else if (socket_config.fd) |fd| .{ .fd = fd } else .{ .unix = hostname }; + var errno: c_int = 0; const listen_socket: *uws.ListenSocket = brk: { switch (connection) { @@ -278,17 +274,12 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa }, } } orelse { - defer { - hostname_or_unix.deinit(); - socket_context.free(ssl_enabled); - } - - const err = globalObject.createErrorInstance("Failed to listen at {s}", .{bun.span(hostname_or_unix.slice())}); + const err = globalObject.createErrorInstance("Failed to listen at {s}", .{hostname}); log("Failed to listen {d}", .{errno}); if (errno != 0) { err.put(globalObject, ZigString.static("syscall"), try bun.String.createUTF8ForJS(globalObject, "listen")); err.put(globalObject, ZigString.static("errno"), JSValue.jsNumber(errno)); - err.put(globalObject, ZigString.static("address"), hostname_or_unix.toZigString().toJS(globalObject)); + err.put(globalObject, ZigString.static("address"), ZigString.initUTF8(hostname).toJS(globalObject)); if (port) |p| err.put(globalObject, ZigString.static("port"), .jsNumber(p)); if (bun.sys.SystemErrno.init(errno)) |str| { err.put(globalObject, ZigString.static("code"), ZigString.init(@tagName(str)).toJS(globalObject)); @@ -297,26 +288,25 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa return globalObject.throwValue(err); }; - var socket = Listener{ - .handlers = handlers, + var socket: Listener = .{ + .handlers = handlers.*, .connection = connection, .ssl = ssl_enabled, .socket_context = socket_context, .listener = .{ .uws = listen_socket }, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p)) else null, + .protos = if (ssl) |s| s.takeProtos() else null, }; - socket.handlers.protect(); - if (socket_config.default_data != .zero) { socket.strong_data = .create(socket_config.default_data, globalObject); } if (ssl) |ssl_config| { if (ssl_config.server_name) |server_name| { - const slice = bun.asByteSlice(server_name); - if (slice.len > 0) + const slice = std.mem.span(server_name); + if (slice.len > 0) { socket.socket_context.?.addServerName(true, server_name, ctx_opts); + } } } @@ -452,7 +442,6 @@ fn doStop(this: *Listener, force_close: bool) void { if (this.handlers.active_connections == 0) { this.poll_ref.unref(this.handlers.vm); - this.handlers.unprotect(); // deiniting the context will also close the listener if (this.socket_context) |ctx| { this.socket_context = null; @@ -561,18 +550,19 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock } const vm = globalObject.bunVM(); - const socket_config = try SocketConfig.fromJS(vm, opts, globalObject, false); + var socket_config = try SocketConfig.fromJS(vm, opts, globalObject, true); + defer socket_config.deinitExcludingHandlers(); - var hostname_or_unix = socket_config.hostname_or_unix; - const port = socket_config.port; - var ssl = socket_config.ssl; - var handlers = socket_config.handlers; - var default_data = socket_config.default_data; + const handlers = &socket_config.handlers; + // Only unprotect handlers if there's an error; otherwise we put them in a `TCPSocket` or + // `TLSSocket` and still want them protected. + errdefer handlers.unprotect(); - var protos: ?[]const u8 = null; - var server_name: ?[]const u8 = null; + const hostname_or_unix = &socket_config.hostname_or_unix; + const port = socket_config.port; + const ssl = if (socket_config.ssl) |*ssl| ssl else null; const ssl_enabled = ssl != null; - defer if (ssl) |*some_ssl| some_ssl.deinit(); + const default_data = socket_config.default_data; vm.eventLoop().ensureWaker(); @@ -583,12 +573,15 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock break :blk .{ .fd = fd }; } } - if (port) |_| { - break :blk .{ .host = .{ .host = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice(), .port = port.? } }; - } - - break :blk .{ .unix = bun.handleOom(hostname_or_unix.cloneIfNeeded(bun.default_allocator)).slice() }; + const host = bun.handleOom(hostname_or_unix.intoOwnedSlice(bun.default_allocator)); + break :blk if (port) |port_| .{ + .host = .{ + .host = host, + .port = port_, + }, + } else .{ .unix = host }; }; + errdefer connection.deinit(); if (Environment.isWindows) { var buf: bun.PathBuffer = undefined; @@ -610,7 +603,8 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock const osfd: uv.uv_os_fd_t = @ptrFromInt(@as(usize, @intCast(uvfd))); if (bun.windows.GetFileType(osfd) == bun.windows.FILE_TYPE_PIPE) { // yay its a named pipe lets make it a libuv fd - connection.fd = bun.FD.fromNative(osfd).makeLibUVOwned() catch @panic("failed to allocate file descriptor"); + connection.fd = bun.FD.fromNative(osfd).makeLibUVOwned() catch + @panic("failed to allocate file descriptor"); break :brk true; } } @@ -621,8 +615,8 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock if (isNamedPipe) { default_data.ensureStillAlive(); - var handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); - handlers_ptr.* = handlers; + const handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); + handlers_ptr.* = handlers.*; var promise = jsc.JSPromise.create(globalObject); const promise_value = promise.toJS(); @@ -631,14 +625,15 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock if (ssl_enabled) { var tls = if (prev_maybe_tls) |prev| blk: { if (prev.handlers) |prev_handlers| { - bun.destroy(prev_handlers); + prev_handlers.unprotect(); + handlers.vm.allocator.destroy(prev_handlers); } bun.assert(prev.this_value != .zero); prev.handlers = handlers_ptr; bun.assert(prev.socket.socket == .detached); prev.connection = connection; - prev.protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p)) else null; - prev.server_name = server_name; + prev.protos = if (ssl) |s| s.takeProtos() else null; + prev.server_name = if (ssl) |s| s.takeServerName() else null; prev.socket_context = null; break :blk prev; } else TLSSocket.new(.{ @@ -647,28 +642,33 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock .this_value = .zero, .socket = TLSSocket.Socket.detached, .connection = connection, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p)) else null, - .server_name = server_name, + .protos = if (ssl) |s| s.takeProtos() else null, + .server_name = if (ssl) |s| s.takeProtos() else null, .socket_context = null, }); + TLSSocket.js.dataSetCached(tls.getThisValue(globalObject), globalObject, default_data); tls.poll_ref.ref(handlers.vm); tls.ref(); - if (connection == .unix) { - const named_pipe = WindowsNamedPipeContext.connect(globalObject, pipe_name.?, ssl, .{ .tls = tls }) catch { - return promise_value; - }; - tls.socket = TLSSocket.Socket.fromNamedPipe(named_pipe); - } else { - // fd - const named_pipe = WindowsNamedPipeContext.open(globalObject, connection.fd, ssl, .{ .tls = tls }) catch { - return promise_value; - }; - tls.socket = TLSSocket.Socket.fromNamedPipe(named_pipe); - } + + const named_pipe = WindowsNamedPipeContext.connect( + globalObject, + switch (connection) { + .unix => pipe_name.?, + .fd => |fd| fd, + else => unreachable, + }, + ssl, + .{ .tls = tls }, + ) catch return promise_value; + tls.socket = TLSSocket.Socket.fromNamedPipe(named_pipe); } else { var tcp = if (prev_maybe_tcp) |prev| blk: { bun.assert(prev.this_value != .zero); + if (prev.handlers) |prev_handlers| { + prev_handlers.unprotect(); + handlers.vm.allocator.destroy(prev_handlers); + } prev.handlers = handlers_ptr; bun.assert(prev.socket.socket == .detached); bun.assert(prev.connection == null); @@ -690,24 +690,23 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock TCPSocket.js.dataSetCached(tcp.getThisValue(globalObject), globalObject, default_data); tcp.poll_ref.ref(handlers.vm); - if (connection == .unix) { - const named_pipe = WindowsNamedPipeContext.connect(globalObject, pipe_name.?, null, .{ .tcp = tcp }) catch { - return promise_value; - }; - tcp.socket = TCPSocket.Socket.fromNamedPipe(named_pipe); - } else { - // fd - const named_pipe = WindowsNamedPipeContext.open(globalObject, connection.fd, null, .{ .tcp = tcp }) catch { - return promise_value; - }; - tcp.socket = TCPSocket.Socket.fromNamedPipe(named_pipe); - } + const named_pipe = WindowsNamedPipeContext.connectl( + globalObject, + switch (connection) { + .unix => pipe_name.?, + .fd => |fd| fd, + else => unreachable, + }, + null, + .{ .tcp = tcp }, + ) catch return promise_value; + tcp.socket = TCPSocket.Socket.fromNamedPipe(named_pipe); } return promise_value; } } - const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |*some_ssl| + const ctx_opts: uws.SocketContext.BunSocketContextOptions = if (ssl) |some_ssl| some_ssl.asUSockets() else .{}; @@ -722,18 +721,10 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock .syscall = bun.String.static("connect"), .code = if (port == null) bun.String.static("ENOENT") else bun.String.static("ECONNREFUSED"), }; - handlers.unprotect(); - connection.deinit(); return globalObject.throwValue(err.toErrorInstance(globalObject)); }; if (ssl_enabled) { - if (ssl.?.protos) |p| { - protos = std.mem.span(p); - } - if (ssl.?.server_name) |s| { - server_name = bun.handleOom(bun.default_allocator.dupe(u8, s[0..bun.len(s)])); - } uws.NewSocketHandler(true).configure(socket_context, true, *TLSSocket, NewSocket(true)); } else { uws.NewSocketHandler(false).configure(socket_context, true, *TCPSocket, NewSocket(false)); @@ -741,8 +732,8 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock default_data.ensureStillAlive(); - var handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); - handlers_ptr.* = handlers; + const handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); + handlers_ptr.* = handlers.*; handlers_ptr.is_server = false; var promise = jsc.JSPromise.create(globalObject); @@ -752,15 +743,22 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock switch (ssl_enabled) { inline else => |is_ssl_enabled| { const SocketType = NewSocket(is_ssl_enabled); - const maybe_previous: ?*SocketType = if (is_ssl_enabled) prev_maybe_tls else prev_maybe_tcp; + const maybe_previous: ?*SocketType = if (is_ssl_enabled) + prev_maybe_tls + else + prev_maybe_tcp; const socket = if (maybe_previous) |prev| blk: { bun.assert(prev.this_value != .zero); + if (prev.handlers) |prev_handlers| { + prev_handlers.unprotect(); + handlers.vm.allocator.destroy(prev_handlers); + } prev.handlers = handlers_ptr; bun.assert(prev.socket.socket == .detached); prev.connection = connection; - prev.protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p)) else null; - prev.server_name = server_name; + prev.protos = if (ssl) |s| s.takeProtos() else null; + prev.server_name = if (ssl) |s| s.takeServerName() else null; prev.socket_context = socket_context; break :blk prev; } else bun.new(SocketType, .{ @@ -769,19 +767,23 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock .this_value = .zero, .socket = SocketType.Socket.detached, .connection = connection, - .protos = if (protos) |p| bun.handleOom(bun.default_allocator.dupe(u8, p)) else null, - .server_name = server_name, + .protos = if (ssl) |s| s.takeProtos() else null, + .server_name = if (ssl) |s| s.takeProtos() else null, .socket_context = socket_context, // owns the socket context }); socket.ref(); SocketType.js.dataSetCached(socket.getThisValue(globalObject), globalObject, default_data); socket.flags.allow_half_open = socket_config.allowHalfOpen; socket.doConnect(connection) catch { - socket.handleConnectError(@intFromEnum(if (port == null) bun.sys.SystemErrno.ENOENT else bun.sys.SystemErrno.ECONNREFUSED)); + socket.handleConnectError(@intFromEnum(if (port == null) + bun.sys.SystemErrno.ENOENT + else + bun.sys.SystemErrno.ECONNREFUSED)); return promise_value; }; - // if this is from node:net there's surface where the user can .ref() and .deref() before the connection starts. make sure we honor that here. + // if this is from node:net there's surface where the user can .ref() and .deref() + // before the connection starts. make sure we honor that here. // in the Bun.connect path, this will always be true at this point in time. if (socket.ref_pollref_on_connect) socket.poll_ref.ref(handlers.vm); diff --git a/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts new file mode 100644 index 00000000000..9f0a7d62933 --- /dev/null +++ b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts @@ -0,0 +1,92 @@ +import * as b from "bindgenv2"; +import { SSLConfig } from "../../server/SSLConfig.bindv2"; + +export const BinaryType = b.enumeration("SocketConfigBinaryType", [ + ["arraybuffer", "ArrayBuffer"], + ["buffer", "Buffer"], + ["uint8array", "Uint8Array"], +]); + +export const Handlers = b.dictionary( + { + name: "SocketConfigHandlers", + userFacingName: "SocketHandler", + generateConversionFunction: true, + }, + { + open: { type: b.RawAny, internalName: "onOpen" }, + close: { type: b.RawAny, internalName: "onClose" }, + error: { type: b.RawAny, internalName: "onError" }, + data: { type: b.RawAny, internalName: "onData" }, + drain: { type: b.RawAny, internalName: "onWritable" }, + handshake: { type: b.RawAny, internalName: "onHandshake" }, + end: { type: b.RawAny, internalName: "onEnd" }, + connectError: { type: b.RawAny, internalName: "onConnectError" }, + timeout: { type: b.RawAny, internalName: "onTimeout" }, + binaryType: { + type: BinaryType, + default: "buffer", + internalName: "binary_type", + }, + }, +); + +export const TLS = b.union("SocketConfigTLS", { + none: b.null, + boolean: b.bool, + object: SSLConfig, +}); + +export const SocketConfig = b.dictionary( + { + name: "SocketConfig", + userFacingName: "SocketOptions", + generateConversionFunction: true, + }, + { + socket: { + type: Handlers, + internalName: "handlers", + }, + data: b.RawAny, + allowHalfOpen: { + type: b.bool, + default: false, + internalName: "allow_half_open", + }, + hostname: { + type: b.String.optional, + altNames: ["host"], + }, + port: b.u16.optional, + tls: TLS, + exclusive: { + type: b.bool, + default: false, + }, + reusePort: { + type: b.bool, + default: false, + internalName: "reuse_port", + }, + ipv6Only: { + type: b.bool, + default: false, + internalName: "ipv6_only", + }, + unix: { + type: b.String.optional, + internalName: "unix_", // `unix` is a predefined C macro... + }, + fd: b.i32.optional, + localAddress: { + type: b.String.optional, + internalName: "local_address", + }, + localPort: { + type: b.u16, + default: 0, + internalName: "local_port", + }, + }, +); diff --git a/src/bun.js/api/server/SSLConfig.zig b/src/bun.js/api/server/SSLConfig.zig index 2e4063451f5..764d2cc25d1 100644 --- a/src/bun.js/api/server/SSLConfig.zig +++ b/src/bun.js/api/server/SSLConfig.zig @@ -92,10 +92,10 @@ pub fn asUSockets(this: *const SSLConfig) uws.SocketContext.BunSocketContextOpti } 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)) { + inline for (comptime std.meta.fields(SSLConfig)) |field| { + const first = @field(this, field.name); + const second = @field(other, field.name); + switch (field.type) { ?[*:0]const u8 => { const a = first orelse return second == null; const b = second orelse return false; @@ -206,6 +206,14 @@ pub fn fromJS( ) bun.JSError!?SSLConfig { var generated: jsc.generated.SSLConfig = try .fromJS(global, value); defer generated.deinit(); + return .fromGenerated(vm, global, &generated); +} + +pub fn fromGenerated( + vm: *jsc.VirtualMachine, + global: *jsc.JSGlobalObject, + generated: *const jsc.generated.SSLConfig, +) bun.JSError!?SSLConfig { var result: SSLConfig = zero; errdefer result.deinit(); var any = false; @@ -381,6 +389,18 @@ fn handleSingleFile( }; } +pub fn takeProtos(this: *SSLConfig) ?[]const u8 { + defer this.protos = null; + const protos = this.protos orelse return null; + return bun.handleOom(bun.memory.dropSentinel(protos, bun.default_allocator)); +} + +pub fn takeServerName(this: *SSLConfig) ?[]const u8 { + defer this.server_name = null; + const server_name = this.server_name orelse return null; + return bun.handleOom(bun.memory.dropSentinel(server_name, bun.default_allocator)); +} + const std = @import("std"); const bun = @import("bun"); diff --git a/src/bun.js/bindings/Bindgen/ExternTraits.h b/src/bun.js/bindings/Bindgen/ExternTraits.h index af9d63f8dbd..3d7ff48a8ab 100644 --- a/src/bun.js/bindings/Bindgen/ExternTraits.h +++ b/src/bun.js/bindings/Bindgen/ExternTraits.h @@ -70,8 +70,8 @@ struct ExternVariant { explicit ExternVariant(std::variant&& variant) : tag(static_cast(variant.index())) + , data(std::move(variant)) { - data.initFromVariant(std::move(variant)); } }; diff --git a/src/bun.js/bindings/Bindgen/ExternUnion.h b/src/bun.js/bindings/Bindgen/ExternUnion.h index 9a92950fdb4..bfab8307294 100644 --- a/src/bun.js/bindings/Bindgen/ExternUnion.h +++ b/src/bun.js/bindings/Bindgen/ExternUnion.h @@ -5,33 +5,31 @@ #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_DEFINE_EXTERN_UNION(T0, ...) \ + template \ + union ExternUnion { \ + BUN_BINDGEN_DETAIL_FOREACH( \ + BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD, \ + T0 __VA_OPT__(, ) __VA_ARGS__) \ + ExternUnion(std::variant&& variant) { \ + using This = std::decay_t; \ + static_assert(std::is_trivially_copyable_v); \ + 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_FIELD(Type) Type alternative##Type; #define BUN_BINDGEN_DETAIL_EXTERN_UNION_VISIT(Type) \ if constexpr (std::is_same_v) { \ if (index == ::Bun::Bindgen::Detail::indexOf##Type) { \ diff --git a/src/bun.js/bindings/ZigString.zig b/src/bun.js/bindings/ZigString.zig index b1fe333040c..aa62c4849b1 100644 --- a/src/bun.js/bindings/ZigString.zig +++ b/src/bun.js/bindings/ZigString.zig @@ -306,7 +306,7 @@ pub const ZigString = extern struct { pub const Slice = struct { allocator: NullableAllocator = .{}, - ptr: [*]const u8 = undefined, + ptr: [*]const u8 = &.{}, len: u32 = 0, pub fn reportExtraMemory(this: *const Slice, vm: *jsc.VM) void { @@ -365,6 +365,25 @@ pub const ZigString = extern struct { return .{ .allocator = .init(allocator), .ptr = duped.ptr, .len = this.len }; } + /// Converts this `ZigString.Slice` into a `[]const u8`, guaranteed to be allocated by + /// `allocator`. + /// + /// This method sets `this` to an empty string. If you don't need the original string, + /// this method may be more efficient than `toOwned`, which always allocates memory. + pub fn intoOwnedSlice(this: *Slice, allocator: std.mem.Allocator) OOM![]const u8 { + defer this.* = .{}; + if (this.allocator.get()) |this_allocator| blk: { + if (allocator.vtable != this_allocator.vtable) break :blk; + // Can add support for more allocators here + if (allocator.vtable == bun.default_allocator.vtable) { + return this.slice(); + } + } + defer this.deinit(); + return (try this.toOwned(allocator)).slice(); + } + + /// Note that the returned slice is not guaranteed to be allocated by `allocator`. pub fn cloneIfNeeded(this: Slice, allocator: std.mem.Allocator) bun.OOM!Slice { if (this.isAllocated()) { return this; diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 8da8ed147a6..97d2f343bd8 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -11,17 +11,29 @@ import { abstract class EnumType extends NamedType {} -export function enumeration(name: string, values: string[]): EnumType { - if (values.length === 0) { +/** + * If `values[x]` is an array, all elements of that array will map to the same underlying integral + * value (that is, `x`). Essentially, they become different spellings of the same enum value. + */ +export function enumeration(name: string, values: (string | string[])[]): EnumType { + const uniqueValues = values.map((v, i) => { + if (typeof v !== "object") return v; + if (v.length === 0) throw RangeError(`enum value cannot be empty (index ${i})`); + return v[0]; + }); + if (uniqueValues.length === 0) { throw RangeError("enum cannot be empty: " + name); } - if (values.length > 1n << 32n) { + if (uniqueValues.length > 1n << 32n) { throw RangeError("too many enum values: " + name); } + const valueMap = new Map( + values.map(v => (typeof v === "object" ? v : [v])).flatMap((arr, i) => arr.map(v => [v, i])), + ); const valueSet = new Set(); const cppMemberSet = new Set(); - for (const value of values) { + for (const value of uniqueValues) { if (valueSet.size === valueSet.add(value).size) { throw RangeError(`duplicate enum value in ${name}: ${util.inspect(value)}`); } @@ -53,8 +65,8 @@ export function enumeration(name: string, values: string[]): EnumType { return `bindgen_generated.${name}`; } toCpp(value: string): string { - const index = values.indexOf(value); - if (index === -1) { + const index = valueMap.get(value); + if (index == null) { throw RangeError(`not a member of this enumeration: ${value}`); } return `::Bun::Bindgen::Generated::${name}::${cppMembers[index]}`; @@ -128,11 +140,10 @@ export function enumeration(name: string, values: string[]): EnumType { template<> std::optional<${qualifiedName}> WebCore::parseEnumerationFromString<${qualifiedName}>(const WTF::String& stringVal) { - static constexpr ::std::array<${pairType}, ${values.length}> mappings { + static constexpr ::std::array<${pairType}, ${valueMap.size}> mappings { ${joinIndented( 12, - values - .map<[string, number]>((value, i) => [value, i]) + Array.from(valueMap.entries()) .sort() .map(([value, i]) => { return `${pairType} { @@ -169,7 +180,7 @@ export function enumeration(name: string, values: string[]): EnumType { pub const ${name} = enum(u32) { ${joinIndented( 10, - values.map(value => `@${toQuotedLiteral(value)},`), + uniqueValues.map(value => `@${toQuotedLiteral(value)},`), )} }; diff --git a/src/codegen/bindgenv2/tsconfig.json b/src/codegen/bindgenv2/tsconfig.json index 2f087e44734..d41931371c2 100644 --- a/src/codegen/bindgenv2/tsconfig.json +++ b/src/codegen/bindgenv2/tsconfig.json @@ -5,7 +5,10 @@ "noPropertyAccessFromIndexSignature": true, "noImplicitAny": true, "noImplicitThis": true, - "exactOptionalPropertyTypes": true + "exactOptionalPropertyTypes": true, + "paths": { + "bindgenv2": ["./lib.ts"] + } }, - "include": ["**/*.ts", "../helpers.ts"] + "include": ["**/*.ts", "../helpers.ts", "../../**/*.bindv2.ts"] } diff --git a/src/memory.zig b/src/memory.zig index 3fc59df81ef..f1afe72b713 100644 --- a/src/memory.zig +++ b/src/memory.zig @@ -170,6 +170,38 @@ pub fn rebaseSlice(slice: []const u8, old_base: [*]const u8, new_base: [*]const return new_base[offset..][0..slice.len]; } +/// Removes the sentinel from a sentinel-terminated slice or many-item pointer. The resulting +/// non-sentinel-terminated slice can be freed with `allocator.free`. +/// +/// `ptr` must be `[:x]T` or `[*:x]T`, or their const equivalents, and it must have been allocated +/// by `allocator`. +/// +/// Most allocators will perform this operation without allocating any memory, but unlike a simple +/// cast, this function will not cause issues with allocators that need to know the exact size of +/// the allocation to free it. +pub fn dropSentinel(ptr: anytype, allocator: std.mem.Allocator) blk: { + var info = @typeInfo(@TypeOf(ptr)); + info.pointer.size = .slice; + info.pointer.sentinel_ptr = null; + break :blk bun.OOM!@Type(info); +} { + const info = @typeInfo(@TypeOf(ptr)).pointer; + const Child = info.child; + if (comptime info.sentinel_ptr == null) { + @compileError("pointer must have sentinel"); + } + + const slice = switch (comptime info.size) { + .many => std.mem.span(ptr), + .slice => ptr, + else => @compileError("only slices and many-item pointers are supported"), + }; + + if (allocator.remap(@constCast(slice), slice.len)) |new| return new; + defer allocator.free(slice); + return allocator.dupe(Child, slice); +} + const std = @import("std"); const Allocator = std.mem.Allocator; From 2436617bebab332030a39e2ae210e8038c111295 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 19:01:04 +0000 Subject: [PATCH 02/29] [autofix.ci] apply automated fixes --- .../api/bun/socket/SocketConfig.bindv2.ts | 6 +-- src/bun.js/bindings/Bindgen/ExternUnion.h | 43 ++++++++++--------- test/internal/ban-limits.json | 2 +- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts index 9f0a7d62933..12e8389fb33 100644 --- a/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts +++ b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts @@ -2,9 +2,9 @@ import * as b from "bindgenv2"; import { SSLConfig } from "../../server/SSLConfig.bindv2"; export const BinaryType = b.enumeration("SocketConfigBinaryType", [ - ["arraybuffer", "ArrayBuffer"], - ["buffer", "Buffer"], - ["uint8array", "Uint8Array"], + ["arraybuffer", "ArrayBuffer"], + ["buffer", "Buffer"], + ["uint8array", "Uint8Array"], ]); export const Handlers = b.dictionary( diff --git a/src/bun.js/bindings/Bindgen/ExternUnion.h b/src/bun.js/bindings/Bindgen/ExternUnion.h index bfab8307294..f7552605fa7 100644 --- a/src/bun.js/bindings/Bindgen/ExternUnion.h +++ b/src/bun.js/bindings/Bindgen/ExternUnion.h @@ -5,27 +5,28 @@ #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__) \ - ExternUnion(std::variant&& variant) { \ - using This = std::decay_t; \ - static_assert(std::is_trivially_copyable_v); \ - 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_DEFINE_EXTERN_UNION(T0, ...) \ + template \ + union ExternUnion { \ + BUN_BINDGEN_DETAIL_FOREACH( \ + BUN_BINDGEN_DETAIL_EXTERN_UNION_FIELD, \ + T0 __VA_OPT__(, ) __VA_ARGS__) \ + ExternUnion(std::variant&& variant) \ + { \ + using This = std::decay_t; \ + static_assert(std::is_trivially_copyable_v); \ + 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 diff --git a/test/internal/ban-limits.json b/test/internal/ban-limits.json index 4da3f15cf3d..6419b6c49c8 100644 --- a/test/internal/ban-limits.json +++ b/test/internal/ban-limits.json @@ -10,7 +10,7 @@ ".stdDir()": 41, ".stdFile()": 16, "// autofix": 165, - ": [^=]+= undefined,$": 256, + ": [^=]+= undefined,$": 255, "== alloc.ptr": 0, "== allocator.ptr": 0, "@import(\"bun\").": 0, From 74a159c90f7388b201e2f39141a50b2ed3569375 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 12:14:00 -0700 Subject: [PATCH 03/29] Fix Windows --- src/bun.js/api/bun/socket/Listener.zig | 50 +++++++++++++++----------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 43f0d6b952c..ddfcea63907 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -651,16 +651,21 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock tls.poll_ref.ref(handlers.vm); tls.ref(); - const named_pipe = WindowsNamedPipeContext.connect( - globalObject, - switch (connection) { - .unix => pipe_name.?, - .fd => |fd| fd, - else => unreachable, - }, - ssl, - .{ .tls = tls }, - ) catch return promise_value; + const named_pipe = switch (connection) { + .unix => WindowsNamedPipeContext.connect( + globalObject, + pipe_name.?, + if (ssl) |s| s.* else null, + .{ .tls = tls }, + ) catch return promise_value, + .fd => |fd| WindowsNamedPipeContext.open( + globalObject, + fd, + if (ssl) |s| s.* else null, + .{ .tls = tls }, + ) catch return promise_value, + else => unreachable, + }; tls.socket = TLSSocket.Socket.fromNamedPipe(named_pipe); } else { var tcp = if (prev_maybe_tcp) |prev| blk: { @@ -690,16 +695,21 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock TCPSocket.js.dataSetCached(tcp.getThisValue(globalObject), globalObject, default_data); tcp.poll_ref.ref(handlers.vm); - const named_pipe = WindowsNamedPipeContext.connectl( - globalObject, - switch (connection) { - .unix => pipe_name.?, - .fd => |fd| fd, - else => unreachable, - }, - null, - .{ .tcp = tcp }, - ) catch return promise_value; + const named_pipe = switch (connection) { + .unix => WindowsNamedPipeContext.connect( + globalObject, + pipe_name.?, + null, + .{ .tcp = tcp }, + ) catch return promise_value, + .fd => |fd| WindowsNamedPipeContext.open( + globalObject, + fd, + null, + .{ .tcp = tcp }, + ) catch return promise_value, + else => unreachable, + }; tcp.socket = TCPSocket.Socket.fromNamedPipe(named_pipe); } return promise_value; From 95fe4ece7b54c199265ddf76aaaa87b3539a6bba Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 14:44:53 -0700 Subject: [PATCH 04/29] bindgenv2: add `T.nullable.loose` --- src/bun.js/api/bun/socket/Handlers.zig | 2 +- .../api/bun/socket/SocketConfig.bindv2.ts | 13 +------ src/bun.js/bindings/BunIDLConvert.h | 26 +++++++++++++ src/bun.js/bindings/BunIDLHumanReadable.h | 4 ++ src/bun.js/bindings/BunIDLTypes.h | 13 +++---- .../bindings/webcore/JSDOMConvertNullable.h | 6 +-- src/codegen/bindgenv2/internal/base.ts | 2 + src/codegen/bindgenv2/internal/dictionary.ts | 1 - src/codegen/bindgenv2/internal/optional.ts | 39 +++++++++++++++++-- .../bun/http/bun-listen-connect-args.test.ts | 12 ------ test/js/bun/net/socket.test.ts | 2 +- 11 files changed, 78 insertions(+), 42 deletions(-) diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index fa5baf7112f..4ca9b116b08 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -289,7 +289,7 @@ pub const SocketConfig = struct { } } else if (generated.hostname.get()) |hostname| { if (hostname.length() == 0) { - return global.throwInvalidArguments("\"hostname\" cannot be non-empty", .{}); + return global.throwInvalidArguments("\"hostname\" cannot be empty", .{}); } result.hostname_or_unix = hostname.toUTF8(bun.default_allocator); const slice = result.hostname_or_unix.slice(); diff --git a/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts index 12e8389fb33..ed2f81940e2 100644 --- a/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts +++ b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts @@ -55,7 +55,7 @@ export const SocketConfig = b.dictionary( internalName: "allow_half_open", }, hostname: { - type: b.String.optional, + type: b.String.nullable.loose, altNames: ["host"], }, port: b.u16.optional, @@ -75,18 +75,9 @@ export const SocketConfig = b.dictionary( internalName: "ipv6_only", }, unix: { - type: b.String.optional, + type: b.String.nullable.loose, internalName: "unix_", // `unix` is a predefined C macro... }, fd: b.i32.optional, - localAddress: { - type: b.String.optional, - internalName: "local_address", - }, - localPort: { - type: b.u16, - default: 0, - internalName: "local_port", - }, }, ); diff --git a/src/bun.js/bindings/BunIDLConvert.h b/src/bun.js/bindings/BunIDLConvert.h index 4aa4963f7fe..57d0e2f0504 100644 --- a/src/bun.js/bindings/BunIDLConvert.h +++ b/src/bun.js/bindings/BunIDLConvert.h @@ -69,6 +69,32 @@ template<> struct WebCore::Converter } }; +template +struct WebCore::Converter> + : Bun::DefaultTryConverter> { + + using ReturnType = WebCore::Converter>::ReturnType; + + template + static std::optional tryConvert( + JSC::JSGlobalObject& globalObject, + JSC::JSValue value, + Ctx& ctx) + { + if (!value.toBoolean(&globalObject)) + return IDL::nullValue(); + return Bun::tryConvertIDL(globalObject, value, ctx); + } + + template + static ReturnType convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + if (!value.toBoolean(&globalObject)) + return IDL::nullValue(); + return Bun::convertIDL(globalObject, value, ctx); + } +}; + template<> struct WebCore::Converter : Bun::DefaultTryConverter { diff --git a/src/bun.js/bindings/BunIDLHumanReadable.h b/src/bun.js/bindings/BunIDLHumanReadable.h index 91e322d8d5a..23908771509 100644 --- a/src/bun.js/bindings/BunIDLHumanReadable.h +++ b/src/bun.js/bindings/BunIDLHumanReadable.h @@ -105,6 +105,10 @@ struct IDLHumanReadableName> : BaseIDLHumanReadableNam "undefined"); }; +template +struct IDLHumanReadableName> + : IDLHumanReadableName> {}; + template struct IDLHumanReadableName> : BaseIDLHumanReadableName { static constexpr bool hasPreposition = true; diff --git a/src/bun.js/bindings/BunIDLTypes.h b/src/bun.js/bindings/BunIDLTypes.h index aab971072b4..27a03df2022 100644 --- a/src/bun.js/bindings/BunIDLTypes.h +++ b/src/bun.js/bindings/BunIDLTypes.h @@ -26,18 +26,17 @@ struct IDLRawAny : WebCore::IDLType { 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"); } -}; +struct IDLStrictNull : WebCore::IDLType {}; // For use in unions, to represent an optional union. -struct IDLStrictUndefined : WebCore::IDLType { - static constexpr auto humanReadableName() { return std::to_array("undefined"); } -}; +struct IDLStrictUndefined : WebCore::IDLType {}; + +// Treats all falsy values as null. +template +struct IDLLooseNullable : WebCore::IDLNullable {}; template struct IDLStrictInteger : WebCore::IDLInteger {}; diff --git a/src/bun.js/bindings/webcore/JSDOMConvertNullable.h b/src/bun.js/bindings/webcore/JSDOMConvertNullable.h index 40821ca8d7b..83b73e5a302 100644 --- a/src/bun.js/bindings/webcore/JSDOMConvertNullable.h +++ b/src/bun.js/bindings/webcore/JSDOMConvertNullable.h @@ -81,11 +81,7 @@ template struct Converter> : DefaultConverter(lexicalGlobalObject, value, ctx); - if (result.has_value()) { - return std::move(*result); - } - return std::nullopt; + return Bun::tryConvertIDL(lexicalGlobalObject, value, ctx); } template diff --git a/src/codegen/bindgenv2/internal/base.ts b/src/codegen/bindgenv2/internal/base.ts index c696a6ebd7c..4162f711ae1 100644 --- a/src/codegen/bindgenv2/internal/base.ts +++ b/src/codegen/bindgenv2/internal/base.ts @@ -5,10 +5,12 @@ import type { NullableType, OptionalType } from "./optional"; export type CodeStyle = "compact" | "pretty"; export abstract class Type { + /** Treats `undefined` as a not-provided value. */ get optional(): OptionalType { return require("./optional").optional(this); } + /** Treats `null` or `undefined` as a not-provided value. */ get nullable(): NullableType { return require("./optional").nullable(this); } diff --git a/src/codegen/bindgenv2/internal/dictionary.ts b/src/codegen/bindgenv2/internal/dictionary.ts index 92446469415..add4a58875d 100644 --- a/src/codegen/bindgenv2/internal/dictionary.ts +++ b/src/codegen/bindgenv2/internal/dictionary.ts @@ -436,7 +436,6 @@ function memberConversion( function basicPermitsUndefined(type: Type): boolean { return ( type instanceof optional.OptionalType || - type instanceof optional.NullableType || type === optional.undefined || type === optional.null || isAny(type) diff --git a/src/codegen/bindgenv2/internal/optional.ts b/src/codegen/bindgenv2/internal/optional.ts index 74235b6fbe4..ece1ebcd2c7 100644 --- a/src/codegen/bindgenv2/internal/optional.ts +++ b/src/codegen/bindgenv2/internal/optional.ts @@ -3,6 +3,7 @@ import { CodeStyle, Type } from "./base"; export abstract class OptionalType extends Type {} +/** Treats `undefined` as a not-provided value. */ export function optional(payload: Type): OptionalType { if (isAny(payload)) { throw RangeError("`Any` types are already optional"); @@ -26,19 +27,25 @@ export function optional(payload: Type): OptionalType { })(); } -export abstract class NullableType extends Type {} +export abstract class NullableType extends OptionalType {} +/** Treats `null` or `undefined` as a not-provided value. */ export function nullable(payload: Type): NullableType { - const AsOptional = optional(payload); + const asOptional = optional(payload); return new (class extends NullableType { + /** Treats all falsy values as null. */ + get loose(): LooseNullableType { + return looseNullable(payload); + } + get idlType() { return `::WebCore::IDLNullable<${payload.idlType}>`; } get bindgenType() { - return AsOptional.bindgenType; + return asOptional.bindgenType; } zigType(style?: CodeStyle) { - return AsOptional.zigType(style); + return asOptional.zigType(style); } toCpp(value: any): string { if (value == null) { @@ -49,6 +56,30 @@ export function nullable(payload: Type): NullableType { })(); } +export abstract class LooseNullableType extends NullableType {} + +/** Treats all falsy values as null. */ +export function looseNullable(payload: Type): LooseNullableType { + const asNullable = nullable(payload); + return new (class extends LooseNullableType { + get idlType() { + return `::Bun::IDLLooseNullable<${payload.idlType}>`; + } + get bindgenType() { + return asNullable.bindgenType; + } + zigType(style?: CodeStyle) { + return asNullable.zigType(style); + } + toCpp(value: any): string { + if (!value) { + return `::Bun::IDLLooseNullable<${payload.idlType}>::nullValue()`; + } + return payload.toCpp(value); + } + })(); +} + /** For use in unions, to represent an optional union. */ const Undefined = new (class extends Type { get idlType() { diff --git a/test/js/bun/http/bun-listen-connect-args.test.ts b/test/js/bun/http/bun-listen-connect-args.test.ts index 43bf85c5fa6..50affc2624e 100644 --- a/test/js/bun/http/bun-listen-connect-args.test.ts +++ b/test/js/bun/http/bun-listen-connect-args.test.ts @@ -34,18 +34,6 @@ describe.if(!isWindows)("unix socket", () => { unix: Math.random().toString(32).slice(2, 15) + ".sock", hostname: false, }, - { - unix: Math.random().toString(32).slice(2, 15) + ".sock", - hostname: Buffer.from(""), - }, - { - unix: Math.random().toString(32).slice(2, 15) + ".sock", - hostname: Buffer.alloc(0), - }, - { - unix: "unix://" + Math.random().toString(32).slice(2, 15) + ".sock", - hostname: Buffer.alloc(0), - }, ]; for (const args of permutations) { diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index 5cb839f3df5..0fd82eea1ed 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -97,7 +97,7 @@ describe.concurrent("socket", () => { data() {}, }, }), - ).toThrow(`Expected \"port\" to be a number between 0 and 65535`); + ).toThrow(`SocketOptions.port must be a number`); }); it("should keep process alive only when active", async () => { From 439aaacf2afaa6da2b32318d2e3d28c3983e5ac6 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 17:06:28 -0700 Subject: [PATCH 05/29] bindgenv2: add `.loose` for primitives --- src/bun.js/api/bun/socket/Handlers.zig | 1 + src/bun.js/api/bun/socket/Listener.zig | 2 +- .../api/bun/socket/SocketConfig.bindv2.ts | 2 +- src/bun.js/bindings/BunIDLConvertNumbers.h | 17 +++ src/bun.js/bindings/BunIDLTypes.h | 4 + src/codegen/bindgenv2/internal/dictionary.ts | 2 + src/codegen/bindgenv2/internal/optional.ts | 24 ++-- src/codegen/bindgenv2/internal/primitives.ts | 136 +++++++++++++++--- test/js/bun/net/socket.test.ts | 2 +- 9 files changed, 161 insertions(+), 29 deletions(-) diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index 4ca9b116b08..764800d232a 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -270,6 +270,7 @@ pub const SocketConfig = struct { .handlers = try .fromGenerated(global, &generated.handlers, is_server), .default_data = if (generated.data.isUndefined()) .zero else generated.data, }; + result.handlers.withAsyncContextIfNeeded(global); result.handlers.protect(); errdefer result.deinit(); diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index ddfcea63907..8291579d4be 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -93,8 +93,8 @@ pub fn reload(this: *Listener, globalObject: *jsc.JSGlobalObject, callframe: *js var handlers = try Handlers.fromJS(globalObject, socket_obj, this.handlers.is_server); this.handlers.unprotect(); - handlers.protect(); handlers.withAsyncContextIfNeeded(globalObject); + handlers.protect(); this.handlers = handlers; // TODO: this is a memory leak return .js_undefined; diff --git a/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts index ed2f81940e2..2e4c78a9e9e 100644 --- a/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts +++ b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts @@ -58,7 +58,7 @@ export const SocketConfig = b.dictionary( type: b.String.nullable.loose, altNames: ["host"], }, - port: b.u16.optional, + port: b.u16.loose.nullable, tls: TLS, exclusive: { type: b.bool, diff --git a/src/bun.js/bindings/BunIDLConvertNumbers.h b/src/bun.js/bindings/BunIDLConvertNumbers.h index d2e5025220e..500363817af 100644 --- a/src/bun.js/bindings/BunIDLConvertNumbers.h +++ b/src/bun.js/bindings/BunIDLConvertNumbers.h @@ -172,3 +172,20 @@ struct WebCore::Converter : Bun::DefaultTryConverter +struct WebCore::Converter> + : Bun::DefaultContextConverter> { + + template + static T convert(JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) + { + auto& vm = JSC::getVM(&globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto numeric = value.toNumeric(&globalObject); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN( + scope, + Bun::convertIDL>(globalObject, numeric, ctx)); + } +}; diff --git a/src/bun.js/bindings/BunIDLTypes.h b/src/bun.js/bindings/BunIDLTypes.h index 27a03df2022..17a2d9f83e0 100644 --- a/src/bun.js/bindings/BunIDLTypes.h +++ b/src/bun.js/bindings/BunIDLTypes.h @@ -45,6 +45,10 @@ struct IDLFiniteDouble : WebCore::IDLDouble {}; struct IDLStrictBoolean : WebCore::IDLBoolean {}; struct IDLStrictString : WebCore::IDLDOMString {}; +// Converts to a number first. +template +struct IDLLooseInteger : IDLStrictInteger {}; + template struct IDLOrderedUnion : WebCore::IDLType> {}; diff --git a/src/codegen/bindgenv2/internal/dictionary.ts b/src/codegen/bindgenv2/internal/dictionary.ts index add4a58875d..e2a7030c018 100644 --- a/src/codegen/bindgenv2/internal/dictionary.ts +++ b/src/codegen/bindgenv2/internal/dictionary.ts @@ -436,6 +436,8 @@ function memberConversion( function basicPermitsUndefined(type: Type): boolean { return ( type instanceof optional.OptionalType || + type instanceof optional.NullableType || + type instanceof optional.LooseNullableType || type === optional.undefined || type === optional.null || isAny(type) diff --git a/src/codegen/bindgenv2/internal/optional.ts b/src/codegen/bindgenv2/internal/optional.ts index ece1ebcd2c7..b18a1a398e6 100644 --- a/src/codegen/bindgenv2/internal/optional.ts +++ b/src/codegen/bindgenv2/internal/optional.ts @@ -1,6 +1,10 @@ import { isAny } from "./any"; import { CodeStyle, Type } from "./base"; +function bindgenOptional(payload: Type): string { + return `bindgen.BindgenOptional(${payload.bindgenType})`; +} + export abstract class OptionalType extends Type {} /** Treats `undefined` as a not-provided value. */ @@ -13,7 +17,7 @@ export function optional(payload: Type): OptionalType { return `::WebCore::IDLOptional<${payload.idlType}>`; } get bindgenType() { - return `bindgen.BindgenOptional(${payload.bindgenType})`; + return bindgenOptional(payload); } zigType(style?: CodeStyle) { return payload.optionalZigType(style); @@ -27,14 +31,15 @@ export function optional(payload: Type): OptionalType { })(); } -export abstract class NullableType extends OptionalType {} +export abstract class NullableType extends Type { + abstract loose: LooseNullableType; +} /** Treats `null` or `undefined` as a not-provided value. */ export function nullable(payload: Type): NullableType { - const asOptional = optional(payload); return new (class extends NullableType { /** Treats all falsy values as null. */ - get loose(): LooseNullableType { + get loose() { return looseNullable(payload); } @@ -42,10 +47,10 @@ export function nullable(payload: Type): NullableType { return `::WebCore::IDLNullable<${payload.idlType}>`; } get bindgenType() { - return asOptional.bindgenType; + return bindgenOptional(payload); } zigType(style?: CodeStyle) { - return asOptional.zigType(style); + return payload.optionalZigType(style); } toCpp(value: any): string { if (value == null) { @@ -56,20 +61,19 @@ export function nullable(payload: Type): NullableType { })(); } -export abstract class LooseNullableType extends NullableType {} +export abstract class LooseNullableType extends Type {} /** Treats all falsy values as null. */ export function looseNullable(payload: Type): LooseNullableType { - const asNullable = nullable(payload); return new (class extends LooseNullableType { get idlType() { return `::Bun::IDLLooseNullable<${payload.idlType}>`; } get bindgenType() { - return asNullable.bindgenType; + return bindgenOptional(payload); } zigType(style?: CodeStyle) { - return asNullable.zigType(style); + return payload.optionalZigType(style); } toCpp(value: any): string { if (!value) { diff --git a/src/codegen/bindgenv2/internal/primitives.ts b/src/codegen/bindgenv2/internal/primitives.ts index 72d24405d73..87113988671 100644 --- a/src/codegen/bindgenv2/internal/primitives.ts +++ b/src/codegen/bindgenv2/internal/primitives.ts @@ -3,6 +3,11 @@ import util from "node:util"; import { CodeStyle, Type } from "./base"; export const bool: Type = new (class extends Type { + /** Converts to a boolean, as if by calling `Boolean`. */ + get loose() { + return LooseBool; + } + get idlType() { return "::Bun::IDLStrictBoolean"; } @@ -18,11 +23,36 @@ export const bool: Type = new (class extends Type { } })(); -function makeUnsignedType(width: number): Type { +export const LooseBool: Type = new (class extends Type { + get idlType() { + return "::WebCore::IDLBoolean"; + } + get bindgenType() { + return bool.bindgenType; + } + zigType(style?: CodeStyle) { + return bool.zigType(style); + } + toCpp(value: boolean): string { + return bool.toCpp(value); + } +})(); + +export abstract class IntegerType extends Type { + abstract loose: LooseIntegerType; + abstract cppType: string; +} + +function makeUnsignedType(width: number): IntegerType { assert(Number.isInteger(width) && width > 0); - return new (class extends Type { + return new (class extends IntegerType { + /** Converts to a number first. */ + get loose() { + return looseUnsignedTypes[width]; + } + get idlType() { - return `::Bun::IDLStrictInteger<::std::uint${width}_t>`; + return `::Bun::IDLStrictInteger<${this.cppType}>`; } get bindgenType() { return `bindgen.BindgenU${width}`; @@ -30,6 +60,9 @@ function makeUnsignedType(width: number): Type { zigType(style?: CodeStyle) { return `u${width}`; } + get cppType() { + return `::std::uint${width}_t`; + } toCpp(value: number | bigint): string { assert(typeof value === "bigint" || Number.isSafeInteger(value)); const intValue = BigInt(value); @@ -41,11 +74,16 @@ function makeUnsignedType(width: number): Type { })(); } -function makeSignedType(width: number): Type { +function makeSignedType(width: number): IntegerType { assert(Number.isInteger(width) && width > 0); - return new (class extends Type { + return new (class extends IntegerType { + /** Tries to convert to a number first. */ + get loose() { + return looseSignedTypes[width]; + } + get idlType() { - return `::Bun::IDLStrictInteger<::std::int${width}_t>`; + return `::Bun::IDLStrictInteger<${this.cppType}>`; } get bindgenType() { return `bindgen.BindgenI${width}`; @@ -53,6 +91,9 @@ function makeSignedType(width: number): Type { zigType(style?: CodeStyle) { return `i${width}`; } + get cppType() { + return `::std::int${width}_t`; + } toCpp(value: number | bigint): string { assert(typeof value === "bigint" || Number.isSafeInteger(value)); const intValue = BigInt(value); @@ -69,19 +110,67 @@ function makeSignedType(width: number): Type { })(); } -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 u8: IntegerType = makeUnsignedType(8); +export const u16: IntegerType = makeUnsignedType(16); +export const u32: IntegerType = makeUnsignedType(32); +export const u64: IntegerType = makeUnsignedType(64); + +export const i8: IntegerType = makeSignedType(8); +export const i16: IntegerType = makeSignedType(16); +export const i32: IntegerType = makeSignedType(32); +export const i64: IntegerType = makeSignedType(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 abstract class LooseIntegerType extends Type {} + +function makeLooseIntegerType(strict: IntegerType): LooseIntegerType { + return new (class extends LooseIntegerType { + get idlType() { + return `::Bun::IDLLooseInteger<${strict.cppType}>`; + } + get bindgenType() { + return strict.bindgenType; + } + zigType(style?: CodeStyle) { + return strict.zigType(style); + } + toCpp(value: number | bigint): string { + return strict.toCpp(value); + } + })(); +} + +export const LooseU8: LooseIntegerType = makeLooseIntegerType(u8); +export const LooseU16: LooseIntegerType = makeLooseIntegerType(u16); +export const LooseU32: LooseIntegerType = makeLooseIntegerType(u32); +export const LooseU64: LooseIntegerType = makeLooseIntegerType(u64); + +export const LooseI8: LooseIntegerType = makeLooseIntegerType(i8); +export const LooseI16: LooseIntegerType = makeLooseIntegerType(i16); +export const LooseI32: LooseIntegerType = makeLooseIntegerType(i32); +export const LooseI64: LooseIntegerType = makeLooseIntegerType(i64); + +const looseUnsignedTypes: { [width: number]: LooseIntegerType } = { + 8: LooseU8, + 16: LooseU16, + 32: LooseU32, + 64: LooseU64, +}; + +const looseSignedTypes: { [width: number]: LooseIntegerType } = { + 8: LooseI8, + 16: LooseI16, + 32: LooseI32, + 64: LooseI64, +}; export const f64: Type = new (class extends Type { + /** Does not allow NaN or infinities. */ get finite() { - return finiteF64; + return FiniteF64; + } + /** Converts to a number, as if by calling `Number`. */ + get loose() { + return LooseF64; } get idlType() { @@ -107,7 +196,7 @@ export const f64: Type = new (class extends Type { } })(); -export const finiteF64: Type = new (class extends Type { +export const FiniteF64: Type = new (class extends Type { get idlType() { return "::Bun::IDLFiniteDouble"; } @@ -123,3 +212,18 @@ export const finiteF64: Type = new (class extends Type { return util.inspect(value); } })(); + +export const LooseF64: Type = new (class extends Type { + get idlType() { + return "::WebCore::IDLUnrestrictedDouble"; + } + get bindgenType() { + return f64.bindgenType; + } + zigType(style?: CodeStyle) { + return f64.zigType(style); + } + toCpp(value: number): string { + return f64.toCpp(value); + } +})(); diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index 0fd82eea1ed..8291cb8019e 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -97,7 +97,7 @@ describe.concurrent("socket", () => { data() {}, }, }), - ).toThrow(`SocketOptions.port must be a number`); + ).toThrow("SocketOptions.port must be in the range [0, 65535] (received -1234)"); }); it("should keep process alive only when active", async () => { From 84ef46c0e98c844437345f23a66020ccbdb44fee Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 17:30:42 -0700 Subject: [PATCH 06/29] Fix tests --- src/bun.js/api/bun/socket/Handlers.zig | 8 ++------ test/js/bun/net/socket.test.ts | 2 +- ...-connect-custom-lookup-non-string-address.mjs | 16 ++-------------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index 764800d232a..10ede690a57 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -275,9 +275,7 @@ pub const SocketConfig = struct { errdefer result.deinit(); if (result.fd != null) {} else if (generated.unix_.get()) |unix| { - if (unix.length() == 0) { - return global.throwInvalidArguments("\"unix\" cannot be empty", .{}); - } + bun.assertf(unix.length() > 0, "truthy bindgen string shouldn't be empty", .{}); result.hostname_or_unix = unix.toUTF8(bun.default_allocator); const slice = result.hostname_or_unix.slice(); if (strings.hasPrefixComptime(slice, "file://") or @@ -289,9 +287,7 @@ pub const SocketConfig = struct { result.hostname_or_unix = .init(bun.default_allocator, without_prefix); } } else if (generated.hostname.get()) |hostname| { - if (hostname.length() == 0) { - return global.throwInvalidArguments("\"hostname\" cannot be empty", .{}); - } + bun.assertf(hostname.length() > 0, "truthy bindgen string shouldn't be empty", .{}); result.hostname_or_unix = hostname.toUTF8(bun.default_allocator); const slice = result.hostname_or_unix.slice(); result.port = generated.port orelse bun.URL.parse(slice).getPort() orelse { diff --git a/test/js/bun/net/socket.test.ts b/test/js/bun/net/socket.test.ts index 8291cb8019e..905b45afd75 100644 --- a/test/js/bun/net/socket.test.ts +++ b/test/js/bun/net/socket.test.ts @@ -97,7 +97,7 @@ describe.concurrent("socket", () => { data() {}, }, }), - ).toThrow("SocketOptions.port must be in the range [0, 65535] (received -1234)"); + ).toThrow("port must be in the range [0, 65535]"); }); it("should keep process alive only when active", async () => { diff --git a/test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs b/test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs index d81232cb244..db3964d884b 100644 --- a/test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs +++ b/test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs @@ -15,13 +15,7 @@ describe('when family is ipv4', () => { lookup: brokenCustomLookup, family: 4 }; - - const socket = net.connect(options, common.mustNotCall()); - socket.on('error', (err) => { - t.assert.strictEqual(err.code, 'ERR_INVALID_IP_ADDRESS'); - - done(); - }); + expect(() => net.connect(options, common.mustNotCall())).toThrow('hostname must be a string'); }); }); @@ -33,12 +27,6 @@ describe('when family is ipv6', () => { lookup: brokenCustomLookup, family: 6 }; - - const socket = net.connect(options, common.mustNotCall()); - socket.on('error', (err) => { - t.assert.strictEqual(err.code, 'ERR_INVALID_IP_ADDRESS'); - - done(); - }); + expect(() => net.connect(options, common.mustNotCall())).toThrow('hostname must be a string'); }); }); From 0fa9dbe562f64837035de5fdadbb1d1f92e72fab Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 17:35:39 -0700 Subject: [PATCH 07/29] Fix `server_name` --- src/bun.js/api/bun/socket/Listener.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 8291579d4be..63dfc44da2e 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -643,7 +643,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock .socket = TLSSocket.Socket.detached, .connection = connection, .protos = if (ssl) |s| s.takeProtos() else null, - .server_name = if (ssl) |s| s.takeProtos() else null, + .server_name = if (ssl) |s| s.takeServerName() else null, .socket_context = null, }); @@ -778,7 +778,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock .socket = SocketType.Socket.detached, .connection = connection, .protos = if (ssl) |s| s.takeProtos() else null, - .server_name = if (ssl) |s| s.takeProtos() else null, + .server_name = if (ssl) |s| s.takeServerName() else null, .socket_context = socket_context, // owns the socket context }); socket.ref(); From 96ec56f8c7fa368787f8ad1f7d5d063c62fa8ed4 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 17:37:27 -0700 Subject: [PATCH 08/29] Reorder initializers --- src/bun.js/bindings/Bindgen/ExternTraits.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/bun.js/bindings/Bindgen/ExternTraits.h b/src/bun.js/bindings/Bindgen/ExternTraits.h index 3d7ff48a8ab..dcb007eff39 100644 --- a/src/bun.js/bindings/Bindgen/ExternTraits.h +++ b/src/bun.js/bindings/Bindgen/ExternTraits.h @@ -69,8 +69,8 @@ struct ExternVariant { static_assert(sizeof...(Args) - 1 <= std::numeric_limits::max()); explicit ExternVariant(std::variant&& variant) - : tag(static_cast(variant.index())) - , data(std::move(variant)) + : data(std::move(variant)) + , tag(static_cast(variant.index())) { } }; From d6fad204476e46798bba18939cabdf42047c4a41 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 17:55:59 -0700 Subject: [PATCH 09/29] Detect duplicate enum values --- src/codegen/bindgenv2/internal/enumeration.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 97d2f343bd8..9ade74f7fb7 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -27,9 +27,16 @@ export function enumeration(name: string, values: (string | string[])[]): EnumTy if (uniqueValues.length > 1n << 32n) { throw RangeError("too many enum values: " + name); } - const valueMap = new Map( - values.map(v => (typeof v === "object" ? v : [v])).flatMap((arr, i) => arr.map(v => [v, i])), - ); + + const indexedValues = values + .map(v => (typeof v === "object" ? v : [v])) + .flatMap((arr, i) => arr.map((v): [string, number] => [v, i])); + const valueMap = new Map(); + for (const [value, index] of indexedValues) { + if (valueMap.size === valueMap.set(value, index).size) { + throw RangeError(`duplicate enum value: ${util.inspect(value)}`); + } + } const valueSet = new Set(); const cppMemberSet = new Set(); From aaf1160b6b3df851a523fb62c82c0b1d85fea3c4 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 18:02:05 -0700 Subject: [PATCH 10/29] Fix return type --- src/bun.js/bindings/BunIDLConvert.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun.js/bindings/BunIDLConvert.h b/src/bun.js/bindings/BunIDLConvert.h index 57d0e2f0504..79b39cf31cd 100644 --- a/src/bun.js/bindings/BunIDLConvert.h +++ b/src/bun.js/bindings/BunIDLConvert.h @@ -76,7 +76,7 @@ struct WebCore::Converter> using ReturnType = WebCore::Converter>::ReturnType; template - static std::optional tryConvert( + static std::optional tryConvert( JSC::JSGlobalObject& globalObject, JSC::JSValue value, Ctx& ctx) From 4bc6f44722e35705bab72aaa3c898fae6e893faa Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 18:23:45 -0700 Subject: [PATCH 11/29] Remove duplicate uniqueness check --- src/bun.js/api/bun/socket/Listener.zig | 2 +- src/codegen/bindgenv2/internal/enumeration.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 63dfc44da2e..072a4850b22 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -185,7 +185,7 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa true => uws.SocketContext.createSSLContext(uws.Loop.get(), @sizeOf(usize), ctx_opts, &create_err), false => uws.SocketContext.createNoSSLContext(uws.Loop.get(), @sizeOf(usize)), } orelse { - var err = globalObject.createErrorInstance( + const err = globalObject.createErrorInstance( "Failed to listen on {s}:{d}", .{ hostname_or_unix.slice(), port orelse 0 }, ); diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 9ade74f7fb7..fec401938f3 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -38,12 +38,8 @@ export function enumeration(name: string, values: (string | string[])[]): EnumTy } } - const valueSet = new Set(); const cppMemberSet = new Set(); for (const value of uniqueValues) { - 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]+/) From ef13d5d10156d16462429405c722de451dafa070 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 18:33:12 -0700 Subject: [PATCH 12/29] Attempt to fix memory leak --- src/bun.js/api/bun/socket.zig | 52 ++++++-------------------- src/bun.js/api/bun/socket/Handlers.zig | 37 +++++++++++++----- src/bun.js/api/bun/socket/Listener.zig | 28 +++++++------- 3 files changed, 53 insertions(+), 64 deletions(-) diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 438e0555392..31470ed9b96 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1349,12 +1349,10 @@ pub fn NewSocket(comptime ssl: bool) type { return globalObject.throw("Expected \"socket\" option", .{}); }; - var this_handlers = this.getHandlers(); - var handlers = try Handlers.fromJS(globalObject, socket_obj, this_handlers.is_server); - this_handlers.unprotect(); - handlers.protect(); - handlers.withAsyncContextIfNeeded(globalObject); - this_handlers.* = handlers; // TODO: this is a memory leak + const this_handlers = this.getHandlers(); + const handlers = try Handlers.fromJS(globalObject, socket_obj, this_handlers.is_server); + this_handlers.deinit(); + this_handlers.* = handlers; return .js_undefined; } @@ -1396,7 +1394,7 @@ pub fn NewSocket(comptime ssl: bool) type { return .zero; } - var handlers = try Handlers.fromJS(globalObject, socket_obj, this.isServer()); + const handlers = try Handlers.fromJS(globalObject, socket_obj, this.isServer()); if (globalObject.hasException()) { return .zero; @@ -1433,10 +1431,8 @@ pub fn NewSocket(comptime ssl: bool) type { const options = socket_config.asUSockets(); const ext_size = @sizeOf(WrappedSocket); - var handlers_ptr = bun.handleOom(bun.default_allocator.create(Handlers)); - handlers.withAsyncContextIfNeeded(globalObject); + const handlers_ptr = bun.handleOom(bun.default_allocator.create(Handlers)); handlers_ptr.* = handlers; - handlers_ptr.protect(); var tls = bun.new(TLSSocket, .{ .ref_count = .init(), .handlers = handlers_ptr, @@ -1487,7 +1483,7 @@ pub fn NewSocket(comptime ssl: bool) type { tls.deref(); - handlers_ptr.unprotect(); + handlers_ptr.deinit(); bun.default_allocator.destroy(handlers_ptr); // If BoringSSL gave us an error code, let's use it. @@ -1512,29 +1508,9 @@ pub fn NewSocket(comptime ssl: bool) type { const new_context = new_socket.context().?; tls.socket_context = new_context; // owns the new tls context that have a ref from the old one tls.ref(); - const vm = handlers.vm; - - var raw_handlers_ptr = bun.handleOom(bun.default_allocator.create(Handlers)); - raw_handlers_ptr.* = blk: { - const this_handlers = this.getHandlers(); - break :blk .{ - .vm = vm, - .globalObject = globalObject, - .onOpen = this_handlers.onOpen, - .onClose = this_handlers.onClose, - .onData = this_handlers.onData, - .onWritable = this_handlers.onWritable, - .onTimeout = this_handlers.onTimeout, - .onConnectError = this_handlers.onConnectError, - .onEnd = this_handlers.onEnd, - .onError = this_handlers.onError, - .onHandshake = this_handlers.onHandshake, - .binary_type = this_handlers.binary_type, - .is_server = this_handlers.is_server, - }; - }; - raw_handlers_ptr.protect(); + const raw_handlers_ptr = bun.handleOom(bun.default_allocator.create(Handlers)); + raw_handlers_ptr.* = this.getHandlers().clone(); const raw = bun.new(TLSSocket, .{ .ref_count = .init(), @@ -1931,7 +1907,8 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C return globalObject.throw("Expected \"socket\" option", .{}); }; - var handlers = try Handlers.fromJS(globalObject, socket_obj, false); + const is_server = false; // A duplex socket is always handled as a client + const handlers = try Handlers.fromJS(globalObject, socket_obj, is_server); var ssl_opts: ?jsc.API.ServerConfig.SSLConfig = null; if (try opts.getTruthy(globalObject, "tls")) |tls| { @@ -1951,13 +1928,8 @@ pub fn jsUpgradeDuplexToTLS(globalObject: *jsc.JSGlobalObject, callframe: *jsc.C default_data.ensureStillAlive(); } - const is_server = false; // A duplex socket is always handled as a client - - var handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); + const handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); handlers_ptr.* = handlers; - handlers_ptr.is_server = is_server; - handlers_ptr.withAsyncContextIfNeeded(globalObject); - handlers_ptr.protect(); var tls = bun.new(TLSSocket, .{ .ref_count = .init(), .handlers = handlers_ptr, diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index 10ede690a57..798e12c053a 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -34,7 +34,6 @@ const callback_fields = .{ pub fn markActive(this: *Handlers) void { Listener.log("markActive", .{}); - this.active_connections += 1; } @@ -90,7 +89,7 @@ pub fn markInactive(this: *Handlers) void { listen_socket.strong_self.deinit(); } } else { - this.unprotect(); + this.deinit(); bun.default_allocator.destroy(this); } } @@ -158,10 +157,18 @@ pub fn fromGenerated( .{}, ); } + result.withAsyncContextIfNeeded(globalObject); + result.protect(); return result; } -pub fn unprotect(this: *Handlers) void { +pub fn deinit(this: *Handlers) void { + this.unprotect(); + this.promise.deinit(); + this.* = undefined; +} + +fn unprotect(this: *Handlers) void { if (this.vm.isShuttingDown()) { return; } @@ -181,7 +188,7 @@ pub fn unprotect(this: *Handlers) void { this.onHandshake.unprotect(); } -pub fn withAsyncContextIfNeeded(this: *Handlers, globalObject: *jsc.JSGlobalObject) void { +fn withAsyncContextIfNeeded(this: *Handlers, globalObject: *jsc.JSGlobalObject) void { inline for (callback_fields) |field| { const value = @field(this, field); if (value != .zero) { @@ -190,7 +197,7 @@ pub fn withAsyncContextIfNeeded(this: *Handlers, globalObject: *jsc.JSGlobalObje } } -pub fn protect(this: *Handlers) void { +fn protect(this: *Handlers) void { if (comptime Environment.ci_assert) { this.protection_count += 1; } @@ -205,6 +212,20 @@ pub fn protect(this: *Handlers) void { this.onHandshake.protect(); } +pub fn clone(this: *const Handlers) Handlers { + var result: Handlers = .{ + .vm = this.vm, + .globalObject = this.globalObject, + .binary_type = this.binary_type, + .is_server = this.is_server, + }; + inline for (callback_fields) |field| { + @field(result, field) = @field(this, field); + } + result.protect(); + return result; +} + /// `handlers` is always `protect`ed in this struct. pub const SocketConfig = struct { hostname_or_unix: jsc.ZigString.Slice, @@ -220,12 +241,12 @@ pub const SocketConfig = struct { /// Deinitializes everything and `unprotect`s `handlers`. pub fn deinit(this: *SocketConfig) void { - this.handlers.unprotect(); + this.handlers.deinit(); this.deinitExcludingHandlers(); this.handlers = undefined; } - /// Deinitializes everything but does not `unprotect` `handlers`. + /// Deinitializes everything except `handlers`. pub fn deinitExcludingHandlers(this: *SocketConfig) void { this.hostname_or_unix.deinit(); bun.memory.deinit(&this.ssl); @@ -270,8 +291,6 @@ pub const SocketConfig = struct { .handlers = try .fromGenerated(global, &generated.handlers, is_server), .default_data = if (generated.data.isUndefined()) .zero else generated.data, }; - result.handlers.withAsyncContextIfNeeded(global); - result.handlers.protect(); errdefer result.deinit(); if (result.fd != null) {} else if (generated.unix_.get()) |unix| { diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 072a4850b22..2e41761d2ef 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -91,11 +91,9 @@ pub fn reload(this: *Listener, globalObject: *jsc.JSGlobalObject, callframe: *js return globalObject.throw("Expected \"socket\" object", .{}); }; - var handlers = try Handlers.fromJS(globalObject, socket_obj, this.handlers.is_server); - this.handlers.unprotect(); - handlers.withAsyncContextIfNeeded(globalObject); - handlers.protect(); - this.handlers = handlers; // TODO: this is a memory leak + const handlers = try Handlers.fromJS(globalObject, socket_obj, this.handlers.is_server); + this.handlers.deinit(); + this.handlers = handlers; return .js_undefined; } @@ -112,9 +110,9 @@ pub fn listen(globalObject: *jsc.JSGlobalObject, opts: JSValue) bun.JSError!JSVa defer socket_config.deinitExcludingHandlers(); const handlers = &socket_config.handlers; - // Only unprotect handlers if there's an error; otherwise we put them in a `Listener` and still - // want them protected. - errdefer handlers.unprotect(); + // Only deinit handlers if there's an error; otherwise we put them in a `Listener` and + // need them to stay alive. + errdefer handlers.deinit(); const hostname_or_unix = &socket_config.hostname_or_unix; const port = socket_config.port; @@ -477,7 +475,7 @@ pub fn deinit(this: *Listener) void { this.strong_data.deinit(); this.poll_ref.unref(this.handlers.vm); bun.assert(this.listener == .none); - this.handlers.unprotect(); + this.handlers.deinit(); if (this.handlers.active_connections > 0) { if (this.socket_context) |ctx| { @@ -554,9 +552,9 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock defer socket_config.deinitExcludingHandlers(); const handlers = &socket_config.handlers; - // Only unprotect handlers if there's an error; otherwise we put them in a `TCPSocket` or - // `TLSSocket` and still want them protected. - errdefer handlers.unprotect(); + // Only deinit handlers if there's an error; otherwise we put them in a `TCPSocket` or + // `TLSSocket` and need them to stay alive. + errdefer handlers.deinit(); const hostname_or_unix = &socket_config.hostname_or_unix; const port = socket_config.port; @@ -625,7 +623,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock if (ssl_enabled) { var tls = if (prev_maybe_tls) |prev| blk: { if (prev.handlers) |prev_handlers| { - prev_handlers.unprotect(); + prev_handlers.deinit(); handlers.vm.allocator.destroy(prev_handlers); } bun.assert(prev.this_value != .zero); @@ -671,7 +669,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock var tcp = if (prev_maybe_tcp) |prev| blk: { bun.assert(prev.this_value != .zero); if (prev.handlers) |prev_handlers| { - prev_handlers.unprotect(); + prev_handlers.deinit(); handlers.vm.allocator.destroy(prev_handlers); } prev.handlers = handlers_ptr; @@ -761,7 +759,7 @@ pub fn connectInner(globalObject: *jsc.JSGlobalObject, prev_maybe_tcp: ?*TCPSock const socket = if (maybe_previous) |prev| blk: { bun.assert(prev.this_value != .zero); if (prev.handlers) |prev_handlers| { - prev_handlers.unprotect(); + prev_handlers.deinit(); handlers.vm.allocator.destroy(prev_handlers); } prev.handlers = handlers_ptr; From b5b016a168c48ac26394f38d1d3b58b59496e608 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 19:21:52 -0700 Subject: [PATCH 13/29] Use `isArray` --- src/codegen/bindgenv2/internal/enumeration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index fec401938f3..0800ff0323f 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -17,7 +17,7 @@ abstract class EnumType extends NamedType {} */ export function enumeration(name: string, values: (string | string[])[]): EnumType { const uniqueValues = values.map((v, i) => { - if (typeof v !== "object") return v; + if (!Array.isArray(v)) return v; if (v.length === 0) throw RangeError(`enum value cannot be empty (index ${i})`); return v[0]; }); @@ -29,7 +29,7 @@ export function enumeration(name: string, values: (string | string[])[]): EnumTy } const indexedValues = values - .map(v => (typeof v === "object" ? v : [v])) + .map(v => (Array.isArray(v) ? v : [v])) .flatMap((arr, i) => arr.map((v): [string, number] => [v, i])); const valueMap = new Map(); for (const [value, index] of indexedValues) { From d7b5a6ca2d7cb50922fe4462f192beee661dde40 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Thu, 16 Oct 2025 19:27:24 -0700 Subject: [PATCH 14/29] Fix `quotedValues` --- src/codegen/bindgenv2/internal/enumeration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 0800ff0323f..67518c9a0e7 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -79,7 +79,7 @@ export function enumeration(name: string, values: (string | string[])[]): EnumTy return true; } get cppHeader() { - const quotedValues = values.map(v => `"${v}"`); + const quotedValues = uniqueValues.map(v => `"${v}"`); let humanReadableName; if (quotedValues.length == 0) { assert(false); // unreachable From b8ea07a6b94b1c9985abc8e1f8397398149c3348 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 10:52:19 -0700 Subject: [PATCH 15/29] Remove useless check --- src/codegen/bindgenv2/internal/enumeration.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 67518c9a0e7..0081d8e6885 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -24,9 +24,6 @@ export function enumeration(name: string, values: (string | string[])[]): EnumTy if (uniqueValues.length === 0) { throw RangeError("enum cannot be empty: " + name); } - if (uniqueValues.length > 1n << 32n) { - throw RangeError("too many enum values: " + name); - } const indexedValues = values .map(v => (Array.isArray(v) ? v : [v])) From 312a35de5812a53cd47f1eaef7f8c1aff8e628a2 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 10:52:27 -0700 Subject: [PATCH 16/29] Use uniform allocator for Handlers --- src/bun.js/api/bun/socket.zig | 9 +++++---- src/bun.js/api/bun/socket/Handlers.zig | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/bun.js/api/bun/socket.zig b/src/bun.js/api/bun/socket.zig index 31470ed9b96..f7c151f9b5d 100644 --- a/src/bun.js/api/bun/socket.zig +++ b/src/bun.js/api/bun/socket.zig @@ -1431,7 +1431,7 @@ pub fn NewSocket(comptime ssl: bool) type { const options = socket_config.asUSockets(); const ext_size = @sizeOf(WrappedSocket); - const handlers_ptr = bun.handleOom(bun.default_allocator.create(Handlers)); + const handlers_ptr = bun.handleOom(handlers.vm.allocator.create(Handlers)); handlers_ptr.* = handlers; var tls = bun.new(TLSSocket, .{ .ref_count = .init(), @@ -1509,8 +1509,9 @@ pub fn NewSocket(comptime ssl: bool) type { tls.socket_context = new_context; // owns the new tls context that have a ref from the old one tls.ref(); - const raw_handlers_ptr = bun.handleOom(bun.default_allocator.create(Handlers)); - raw_handlers_ptr.* = this.getHandlers().clone(); + const this_handlers = this.getHandlers(); + const raw_handlers_ptr = bun.handleOom(this_handlers.vm.allocator.create(Handlers)); + raw_handlers_ptr.* = this_handlers.clone(); const raw = bun.new(TLSSocket, .{ .ref_count = .init(), @@ -1537,7 +1538,7 @@ pub fn NewSocket(comptime ssl: bool) type { tls.markActive(); // we're unrefing the original instance and refing the TLS instance - tls.poll_ref.ref(this.getHandlers().vm); + tls.poll_ref.ref(this_handlers.vm); // mark both instances on socket data if (new_socket.ext(WrappedSocket)) |ctx| { diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index 798e12c053a..62d560e33e5 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -90,7 +90,7 @@ pub fn markInactive(this: *Handlers) void { } } else { this.deinit(); - bun.default_allocator.destroy(this); + this.vm.allocator.destroy(this); } } } From 41e7a24c40b2dc488d38e81d8a3df1e451f3fd79 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 10:56:59 -0700 Subject: [PATCH 17/29] Fix Listener allocator --- src/bun.js/api/bun/socket/Listener.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index 2e41761d2ef..a833be11469 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -475,6 +475,7 @@ pub fn deinit(this: *Listener) void { this.strong_data.deinit(); this.poll_ref.unref(this.handlers.vm); bun.assert(this.listener == .none); + const vm = this.handlers.vm; this.handlers.deinit(); if (this.handlers.active_connections > 0) { @@ -493,7 +494,7 @@ pub fn deinit(this: *Listener) void { this.protos = null; bun.default_allocator.free(protos); } - bun.default_allocator.destroy(this); + handlers.vm.allocator.destroy(this); } pub fn getConnectionsCount(this: *Listener, _: *jsc.JSGlobalObject) JSValue { From b4ef2318ad8a8d25d20d8515bae5834b5d2b4dfe Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 10:59:29 -0700 Subject: [PATCH 18/29] =?UTF-8?q?`handlers.vm`=20=E2=86=92=20`vm`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/bun.js/api/bun/socket/Listener.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index a833be11469..fbb71e74296 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -494,7 +494,7 @@ pub fn deinit(this: *Listener) void { this.protos = null; bun.default_allocator.free(protos); } - handlers.vm.allocator.destroy(this); + vm.allocator.destroy(this); } pub fn getConnectionsCount(this: *Listener, _: *jsc.JSGlobalObject) JSValue { From fccfb90f3b0347d5f7ec4b7de1f07ce44fd6bd23 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 13:27:32 -0700 Subject: [PATCH 19/29] Fix deinit --- src/bun.js/api/bun/socket/Handlers.zig | 3 ++- src/bun.js/api/bun/socket/Listener.zig | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index 62d560e33e5..a5cd60605dc 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -89,8 +89,9 @@ pub fn markInactive(this: *Handlers) void { listen_socket.strong_self.deinit(); } } else { + const vm = this.vm; this.deinit(); - this.vm.allocator.destroy(this); + vm.allocator.destroy(this); } } } diff --git a/src/bun.js/api/bun/socket/Listener.zig b/src/bun.js/api/bun/socket/Listener.zig index fbb71e74296..4f058cdb053 100644 --- a/src/bun.js/api/bun/socket/Listener.zig +++ b/src/bun.js/api/bun/socket/Listener.zig @@ -473,10 +473,9 @@ pub fn deinit(this: *Listener) void { log("deinit", .{}); this.strong_self.deinit(); this.strong_data.deinit(); - this.poll_ref.unref(this.handlers.vm); - bun.assert(this.listener == .none); const vm = this.handlers.vm; - this.handlers.deinit(); + this.poll_ref.unref(vm); + bun.assert(this.listener == .none); if (this.handlers.active_connections > 0) { if (this.socket_context) |ctx| { @@ -494,6 +493,7 @@ pub fn deinit(this: *Listener) void { this.protos = null; bun.default_allocator.free(protos); } + this.handlers.deinit(); vm.allocator.destroy(this); } From 909bb689697c652554daf44d9ab1170d9c459c13 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 13:36:16 -0700 Subject: [PATCH 20/29] Remove outdated comment --- src/bun.js/api/bun/socket/Handlers.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index a5cd60605dc..f9f5f71a41b 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -115,7 +115,6 @@ pub fn callErrorHandler(this: *Handlers, thisValue: JSValue, args: *const [2]JSV return true; } -/// Does not `protect` the handlers. pub fn fromJS( globalObject: *jsc.JSGlobalObject, opts: jsc.JSValue, From f1db3bbb2e6d8174cf5a11a7cd4c18dfddf28f1e Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 13:48:51 -0700 Subject: [PATCH 21/29] Fix leak when `Handlers.fromGenerated` fails --- src/bun.js/api/bun/socket/Handlers.zig | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index f9f5f71a41b..ff78852e1c9 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -280,16 +280,20 @@ pub const SocketConfig = struct { generated: *const jsc.generated.SocketConfig, is_server: bool, ) bun.JSError!SocketConfig { - var result: SocketConfig = .{ - .hostname_or_unix = .empty, - .fd = if (generated.fd) |fd| .fromUV(fd) else null, - .ssl = switch (generated.tls) { + var result: SocketConfig = blk: { + var ssl: ?SSLConfig = switch (generated.tls) { .none => null, .boolean => |b| if (b) .zero else null, .object => |*ssl| try .fromGenerated(vm, global, ssl), - }, - .handlers = try .fromGenerated(global, &generated.handlers, is_server), - .default_data = if (generated.data.isUndefined()) .zero else generated.data, + }; + errdefer bun.memory.deinit(&ssl); + break :blk .{ + .hostname_or_unix = .empty, + .fd = if (generated.fd) |fd| .fromUV(fd) else null, + .ssl = ssl, + .handlers = try .fromGenerated(global, &generated.handlers, is_server), + .default_data = if (generated.data.isUndefined()) .zero else generated.data, + }; }; errdefer result.deinit(); From b0441637aecbd7a1a95c081bf7429df1e46f516c Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 14:06:29 -0700 Subject: [PATCH 22/29] Improve sorting --- src/codegen/bindgenv2/internal/enumeration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 0081d8e6885..89836363977 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -144,7 +144,7 @@ export function enumeration(name: string, values: (string | string[])[]): EnumTy ${joinIndented( 12, Array.from(valueMap.entries()) - .sort() + .sort(([v1, i1], [v2, i2]) => v1 < v2 ? -1 : 1) .map(([value, i]) => { return `${pairType} { ${toASCIILiteral(value)}, From ee5fd2d79321b3e8238a62c1ca7a009c0d5be08c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:41:04 +0000 Subject: [PATCH 23/29] [autofix.ci] apply automated fixes --- src/codegen/bindgenv2/internal/enumeration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 89836363977..472d42836ae 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -144,7 +144,7 @@ export function enumeration(name: string, values: (string | string[])[]): EnumTy ${joinIndented( 12, Array.from(valueMap.entries()) - .sort(([v1, i1], [v2, i2]) => v1 < v2 ? -1 : 1) + .sort(([v1, i1], [v2, i2]) => (v1 < v2 ? -1 : 1)) .map(([value, i]) => { return `${pairType} { ${toASCIILiteral(value)}, From bcbf1df4c0dc1c8c159cb065b462a14c181274ab Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 16:29:50 -0700 Subject: [PATCH 24/29] Use `readonly` --- src/codegen/bindgenv2/internal/enumeration.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 472d42836ae..07fa8f5a9e5 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -15,8 +15,11 @@ abstract class EnumType extends NamedType {} * If `values[x]` is an array, all elements of that array will map to the same underlying integral * value (that is, `x`). Essentially, they become different spellings of the same enum value. */ -export function enumeration(name: string, values: (string | string[])[]): EnumType { - const uniqueValues = values.map((v, i) => { +export function enumeration( + name: string, + values: readonly (string | readonly string[])[], +): EnumType { + const uniqueValues: string[] = values.map((v, i) => { if (!Array.isArray(v)) return v; if (v.length === 0) throw RangeError(`enum value cannot be empty (index ${i})`); return v[0]; From 13cc00b185b6a96723c7438734bf2280fd403836 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 16:31:26 -0700 Subject: [PATCH 25/29] Better error message --- src/codegen/bindgenv2/internal/enumeration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 07fa8f5a9e5..1145ac3d02f 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -70,7 +70,7 @@ export function enumeration( toCpp(value: string): string { const index = valueMap.get(value); if (index == null) { - throw RangeError(`not a member of this enumeration: ${value}`); + throw RangeError(`not a member of this ${name}: ${util.inspect(value)}`); } return `::Bun::Bindgen::Generated::${name}::${cppMembers[index]}`; } From 19218067325e809f5b1e739d282eba7c87c1a51b Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 16:32:14 -0700 Subject: [PATCH 26/29] Simplify destructuring --- src/codegen/bindgenv2/internal/enumeration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 1145ac3d02f..1233cc06475 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -147,7 +147,7 @@ export function enumeration( ${joinIndented( 12, Array.from(valueMap.entries()) - .sort(([v1, i1], [v2, i2]) => (v1 < v2 ? -1 : 1)) + .sort(([v1], [v2]) => (v1 < v2 ? -1 : 1)) .map(([value, i]) => { return `${pairType} { ${toASCIILiteral(value)}, From 718161a63e66a6bac6f04cc9c6b707db35ed12bd Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 17:06:36 -0700 Subject: [PATCH 27/29] Fix error message --- src/codegen/bindgenv2/internal/enumeration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/codegen/bindgenv2/internal/enumeration.ts b/src/codegen/bindgenv2/internal/enumeration.ts index 1233cc06475..0e4ec52c534 100644 --- a/src/codegen/bindgenv2/internal/enumeration.ts +++ b/src/codegen/bindgenv2/internal/enumeration.ts @@ -70,7 +70,7 @@ export function enumeration( toCpp(value: string): string { const index = valueMap.get(value); if (index == null) { - throw RangeError(`not a member of this ${name}: ${util.inspect(value)}`); + throw RangeError(`not a member of ${name}: ${util.inspect(value)}`); } return `::Bun::Bindgen::Generated::${name}::${cppMembers[index]}`; } From 89b551ebc08bbe5ecbc2ca4812994c79a7ddb90a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 17 Oct 2025 17:46:39 -0700 Subject: [PATCH 28/29] Add explanatory comment --- src/bun.js/api/bun/socket/Handlers.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/bun.js/api/bun/socket/Handlers.zig b/src/bun.js/api/bun/socket/Handlers.zig index ff78852e1c9..1f441b4bcc1 100644 --- a/src/bun.js/api/bun/socket/Handlers.zig +++ b/src/bun.js/api/bun/socket/Handlers.zig @@ -297,7 +297,9 @@ pub const SocketConfig = struct { }; errdefer result.deinit(); - if (result.fd != null) {} else if (generated.unix_.get()) |unix| { + if (result.fd != null) { + // If a user passes a file descriptor then prefer it over hostname or unix + } else if (generated.unix_.get()) |unix| { bun.assertf(unix.length() > 0, "truthy bindgen string shouldn't be empty", .{}); result.hostname_or_unix = unix.toUTF8(bun.default_allocator); const slice = result.hostname_or_unix.slice(); From 2a8ad7ae8ecf608e42e8ec6cb6557efac0ffe943 Mon Sep 17 00:00:00 2001 From: "taylor.fish" Date: Fri, 17 Oct 2025 18:41:06 -0700 Subject: [PATCH 29/29] Convert hostname to string --- .../api/bun/socket/SocketConfig.bindv2.ts | 2 +- src/codegen/bindgenv2/internal/string.ts | 23 +++++++++++++++++++ ...nnect-custom-lookup-non-string-address.mjs | 16 +++++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts index 2e4c78a9e9e..fb421cd548b 100644 --- a/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts +++ b/src/bun.js/api/bun/socket/SocketConfig.bindv2.ts @@ -55,7 +55,7 @@ export const SocketConfig = b.dictionary( internalName: "allow_half_open", }, hostname: { - type: b.String.nullable.loose, + type: b.String.loose.nullable.loose, altNames: ["host"], }, port: b.u16.loose.nullable, diff --git a/src/codegen/bindgenv2/internal/string.ts b/src/codegen/bindgenv2/internal/string.ts index 1363942d464..cfd4712f889 100644 --- a/src/codegen/bindgenv2/internal/string.ts +++ b/src/codegen/bindgenv2/internal/string.ts @@ -2,6 +2,11 @@ import assert from "node:assert"; import { CodeStyle, Type, toASCIILiteral } from "./base"; export const String: Type = new (class extends Type { + /** Converts to a string, as if by calling `String`. */ + get loose() { + return LooseString; + } + get idlType() { return "::Bun::IDLStrictString"; } @@ -19,3 +24,21 @@ export const String: Type = new (class extends Type { return toASCIILiteral(value); } })(); + +export const LooseString: Type = new (class extends Type { + get idlType() { + return "::Bun::IDLDOMString"; + } + get bindgenType() { + return String.bindgenType; + } + zigType(style?: CodeStyle) { + return String.zigType(style); + } + optionalZigType(style?: CodeStyle) { + return String.optionalZigType(style); + } + toCpp(value: string): string { + return String.toCpp(value); + } +})(); diff --git a/test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs b/test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs index db3964d884b..d81232cb244 100644 --- a/test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs +++ b/test/js/node/test/parallel/test-net-connect-custom-lookup-non-string-address.mjs @@ -15,7 +15,13 @@ describe('when family is ipv4', () => { lookup: brokenCustomLookup, family: 4 }; - expect(() => net.connect(options, common.mustNotCall())).toThrow('hostname must be a string'); + + const socket = net.connect(options, common.mustNotCall()); + socket.on('error', (err) => { + t.assert.strictEqual(err.code, 'ERR_INVALID_IP_ADDRESS'); + + done(); + }); }); }); @@ -27,6 +33,12 @@ describe('when family is ipv6', () => { lookup: brokenCustomLookup, family: 6 }; - expect(() => net.connect(options, common.mustNotCall())).toThrow('hostname must be a string'); + + const socket = net.connect(options, common.mustNotCall()); + socket.on('error', (err) => { + t.assert.strictEqual(err.code, 'ERR_INVALID_IP_ADDRESS'); + + done(); + }); }); });