mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
Add a new generator for JS → Zig bindings. The bulk of the conversion is done in C++, after which the data is transformed into an FFI-safe representation, passed to Zig, and then finally transformed into idiomatic Zig types. In its current form, the new bindings generator supports: * Signed and unsigned integers * Floats (plus a “finite” variant that disallows NaN and infinities) * Strings * ArrayBuffer (accepts ArrayBuffer, TypedArray, or DataView) * Blob * Optional types * Nullable types (allows null, whereas Optional only allows undefined) * Arrays * User-defined string enumerations * User-defined unions (fields can optionally be named to provide a better experience in Zig) * Null and undefined, for use in unions (can more efficiently represent optional/nullable unions than wrapping a union in an optional) * User-defined dictionaries (arbitrary key-value pairs; expects a JS object and parses it into a struct) * Default values for dictionary members * Alternative names for dictionary members (e.g., to support both `serverName` and `servername` without taking up twice the space) * Descriptive error messages * Automatic `fromJS` functions in Zig for dictionaries * Automatic `deinit` functions for the generated Zig types Although this bindings generator has many features not present in `bindgen.ts`, it does not yet implement all of `bindgen.ts`'s functionality, so for the time being, it has been named `bindgenv2`, and its configuration is specified in `.bindv2.ts` files. Once all `bindgen.ts`'s functionality has been incorporated, it will be renamed. This PR ports `SSLConfig` to use the new bindings generator; see `SSLConfig.bindv2.ts`. (For internal tracking: fixes STAB-1319, STAB-1322, STAB-1323, STAB-1324) --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Alistair Smith <hi@alistair.sh>
304 lines
8.5 KiB
TypeScript
304 lines
8.5 KiB
TypeScript
import { connect, listen, SocketHandler, TCPSocketListener } from "bun";
|
|
import { describe, expect, it } from "bun:test";
|
|
import { expectMaxObjectTypeCount, isWindows } from "harness";
|
|
|
|
type Resolve = (value?: unknown) => void;
|
|
type Reject = (reason?: any) => void;
|
|
const decoder = new TextDecoder();
|
|
|
|
it("remoteAddress works", async () => {
|
|
var resolve: Resolve, reject: Reject;
|
|
var remaining = 2;
|
|
var prom = new Promise<void>((resolve1, reject1) => {
|
|
resolve = () => {
|
|
if (--remaining === 0) resolve1();
|
|
};
|
|
reject = reject1;
|
|
});
|
|
using server = Bun.listen({
|
|
socket: {
|
|
open(ws) {
|
|
try {
|
|
expect(ws.remoteAddress).toBe("127.0.0.1");
|
|
resolve();
|
|
} catch (e) {
|
|
reject(e);
|
|
|
|
return;
|
|
}
|
|
},
|
|
close() {},
|
|
data() {},
|
|
},
|
|
port: 0,
|
|
hostname: "127.0.0.1",
|
|
});
|
|
|
|
await Bun.connect({
|
|
socket: {
|
|
open(ws) {
|
|
try {
|
|
// windows returns the ipv6 address
|
|
expect(ws.remoteAddress).toMatch(/127.0.0.1/);
|
|
resolve();
|
|
} catch (e) {
|
|
reject(e);
|
|
return;
|
|
} finally {
|
|
ws.end();
|
|
}
|
|
},
|
|
data() {},
|
|
close() {},
|
|
},
|
|
hostname: server.hostname,
|
|
port: server.port,
|
|
});
|
|
await prom;
|
|
});
|
|
|
|
it("should not allow invalid tls option", () => {
|
|
[1, "string", Symbol("symbol")].forEach(value => {
|
|
expect(() => {
|
|
// @ts-ignore
|
|
using server = Bun.listen({
|
|
socket: {
|
|
open(ws) {},
|
|
close() {},
|
|
data() {},
|
|
},
|
|
port: 0,
|
|
hostname: "localhost",
|
|
tls: value,
|
|
});
|
|
}).toThrow("TLSOptions must be an object");
|
|
});
|
|
});
|
|
|
|
it("should allow using false, null or undefined tls option", () => {
|
|
[false, null, undefined].forEach(value => {
|
|
expect(() => {
|
|
// @ts-ignore
|
|
using server = Bun.listen({
|
|
socket: {
|
|
open(ws) {},
|
|
close() {},
|
|
data() {},
|
|
},
|
|
port: 0,
|
|
hostname: "localhost",
|
|
tls: value,
|
|
});
|
|
}).not.toThrow("TLSOptions must be an object");
|
|
});
|
|
});
|
|
|
|
it("echo server 1 on 1", async () => {
|
|
// wrap it in a separate closure so the GC knows to clean it up
|
|
// the sockets & listener don't escape the closure
|
|
await (async function () {
|
|
let resolve: Resolve, reject: Reject, serverResolve: Resolve, serverReject: Reject;
|
|
const prom = new Promise((resolve1, reject1) => {
|
|
resolve = resolve1;
|
|
reject = reject1;
|
|
});
|
|
const serverProm = new Promise((resolve1, reject1) => {
|
|
serverResolve = resolve1;
|
|
serverReject = reject1;
|
|
});
|
|
|
|
let serverData: any, clientData: any;
|
|
const handlers = {
|
|
open(socket) {
|
|
socket.data.counter = 1;
|
|
if (!socket.data?.isServer) {
|
|
clientData = socket.data;
|
|
clientData.sendQueue = ["client: Hello World! " + 0];
|
|
if (!socket.write("client: Hello World! " + 0)) {
|
|
socket.data = { pending: "server: Hello World! " + 0 };
|
|
}
|
|
} else {
|
|
serverData = socket.data;
|
|
serverData.sendQueue = ["server: Hello World! " + 0];
|
|
}
|
|
|
|
if (clientData) clientData.other = serverData;
|
|
if (serverData) serverData.other = clientData;
|
|
if (clientData) clientData.other = serverData;
|
|
if (serverData) serverData.other = clientData;
|
|
},
|
|
data(socket, buffer) {
|
|
const msg = `${socket.data.isServer ? "server:" : "client:"} Hello World! ${socket.data.counter++}`;
|
|
socket.data.sendQueue.push(msg);
|
|
|
|
expect(decoder.decode(buffer)).toBe(socket.data.other.sendQueue.pop());
|
|
|
|
if (socket.data.counter > 10) {
|
|
if (!socket.data.finished) {
|
|
socket.data.finished = true;
|
|
if (socket.data.isServer) {
|
|
setTimeout(() => {
|
|
serverResolve();
|
|
socket.end();
|
|
}, 1);
|
|
} else {
|
|
setTimeout(() => {
|
|
resolve();
|
|
socket.end();
|
|
}, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!socket.write(msg)) {
|
|
socket.data.pending = msg;
|
|
return;
|
|
}
|
|
},
|
|
error(socket, error) {
|
|
reject(error);
|
|
},
|
|
drain(socket) {
|
|
reject(new Error("Unexpected backpressure"));
|
|
},
|
|
} as SocketHandler<any>;
|
|
|
|
using server: TCPSocketListener<any> | undefined = listen({
|
|
socket: handlers,
|
|
hostname: "localhost",
|
|
port: 0,
|
|
|
|
data: {
|
|
isServer: true,
|
|
counter: 0,
|
|
},
|
|
});
|
|
const clientProm = connect({
|
|
socket: handlers,
|
|
hostname: "localhost",
|
|
port: server.port,
|
|
data: {
|
|
counter: 0,
|
|
},
|
|
});
|
|
await Promise.all([prom, clientProm, serverProm]);
|
|
})();
|
|
});
|
|
|
|
describe("tcp socket binaryType", () => {
|
|
const binaryType = ["arraybuffer", "uint8array", "buffer"] as const;
|
|
for (const type of binaryType) {
|
|
it(type, async () => {
|
|
// wrap it in a separate closure so the GC knows to clean it up
|
|
// the sockets & listener don't escape the closure
|
|
await (async function () {
|
|
let resolve: Resolve, reject: Reject, serverResolve: Resolve, serverReject: Reject;
|
|
const prom = new Promise((resolve1, reject1) => {
|
|
resolve = resolve1;
|
|
reject = reject1;
|
|
});
|
|
const serverProm = new Promise((resolve1, reject1) => {
|
|
serverResolve = resolve1;
|
|
serverReject = reject1;
|
|
});
|
|
|
|
let serverData: any, clientData: any;
|
|
const handlers = {
|
|
open(socket) {
|
|
socket.data.counter = 1;
|
|
if (!socket.data?.isServer) {
|
|
clientData = socket.data;
|
|
clientData.sendQueue = ["client: Hello World! " + 0];
|
|
if (!socket.write("client: Hello World! " + 0)) {
|
|
socket.data = { pending: "server: Hello World! " + 0 };
|
|
}
|
|
} else {
|
|
serverData = socket.data;
|
|
serverData.sendQueue = ["server: Hello World! " + 0];
|
|
}
|
|
|
|
if (clientData) clientData.other = serverData;
|
|
if (serverData) serverData.other = clientData;
|
|
if (clientData) clientData.other = serverData;
|
|
if (serverData) serverData.other = clientData;
|
|
},
|
|
data(socket, buffer) {
|
|
expect(
|
|
buffer instanceof
|
|
(type === "arraybuffer"
|
|
? ArrayBuffer
|
|
: type === "uint8array"
|
|
? Uint8Array
|
|
: type === "buffer"
|
|
? Buffer
|
|
: Error),
|
|
).toBe(true);
|
|
const msg = `${socket.data.isServer ? "server:" : "client:"} Hello World! ${socket.data.counter++}`;
|
|
socket.data.sendQueue.push(msg);
|
|
|
|
expect(decoder.decode(buffer)).toBe(socket.data.other.sendQueue.pop());
|
|
|
|
if (socket.data.counter > 10) {
|
|
if (!socket.data.finished) {
|
|
socket.data.finished = true;
|
|
if (socket.data.isServer) {
|
|
setTimeout(() => {
|
|
serverResolve();
|
|
socket.end();
|
|
}, 1);
|
|
} else {
|
|
setTimeout(() => {
|
|
resolve();
|
|
socket.end();
|
|
}, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!socket.write(msg)) {
|
|
socket.data.pending = msg;
|
|
return;
|
|
}
|
|
},
|
|
error(socket, error) {
|
|
reject(error);
|
|
},
|
|
drain(socket) {
|
|
reject(new Error("Unexpected backpressure"));
|
|
},
|
|
|
|
binaryType: type,
|
|
} as SocketHandler<any>;
|
|
|
|
using server: TCPSocketListener<any> | undefined = listen({
|
|
socket: handlers,
|
|
hostname: "localhost",
|
|
port: 0,
|
|
data: {
|
|
isServer: true,
|
|
counter: 0,
|
|
},
|
|
});
|
|
|
|
const clientProm = connect({
|
|
socket: handlers,
|
|
hostname: "localhost",
|
|
port: server.port,
|
|
data: {
|
|
counter: 0,
|
|
},
|
|
});
|
|
|
|
await Promise.all([prom, clientProm, serverProm]);
|
|
})();
|
|
});
|
|
}
|
|
});
|
|
|
|
it("should not leak memory", async () => {
|
|
// assert we don't leak the sockets
|
|
// we expect 1 or 2 because that's the prototype / structure
|
|
await expectMaxObjectTypeCount(expect, "Listener", 2);
|
|
await expectMaxObjectTypeCount(expect, "TCPSocket", isWindows ? 3 : 2);
|
|
});
|