mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
1125 lines
31 KiB
TypeScript
1125 lines
31 KiB
TypeScript
// While working on this file, it is important to have very rigorous errors
|
|
// and checking on input data. The goal is to allow people not aware of
|
|
// various footguns in JavaScript, C++, and the bindings generator to
|
|
// always produce correct code, or bail with an error.
|
|
import { expect } from "bun:test";
|
|
import type { FuncOptions, Type, t } from "./bindgen-lib";
|
|
import * as path from "node:path";
|
|
import assert from "node:assert";
|
|
|
|
export const src = path.join(import.meta.dirname, "../");
|
|
|
|
export type TypeKind = keyof typeof t;
|
|
|
|
export let allFunctions: Func[] = [];
|
|
export let files = new Map<string, File>();
|
|
/** A reachable type is one that is required for code generation */
|
|
export let typeHashToReachableType = new Map<string, TypeImpl>();
|
|
export let typeHashToStruct = new Map<string, Struct>();
|
|
export let typeHashToNamespace = new Map<string, string>();
|
|
export let structHashToSelf = new Map<string, Struct>();
|
|
|
|
/** String literal */
|
|
export const str = (v: any) => JSON.stringify(v);
|
|
/** Capitalize */
|
|
export const cap = (s: string) => s[0].toUpperCase() + s.slice(1);
|
|
/** Escape a Zig Identifier */
|
|
export const zid = (s: string) => (s.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/) ? s : "@" + str(s));
|
|
/** Snake Case */
|
|
export const snake = (s: string) =>
|
|
s[0].toLowerCase() +
|
|
s
|
|
.slice(1)
|
|
.replace(/([A-Z])/g, "_$1")
|
|
.replace(/-/g, "_")
|
|
.toLowerCase();
|
|
/** Camel Case */
|
|
export const camel = (s: string) =>
|
|
s[0].toLowerCase() + s.slice(1).replace(/[_-](\w)?/g, (_, letter) => letter?.toUpperCase() ?? "");
|
|
/** Pascal Case */
|
|
export const pascal = (s: string) => cap(camel(s));
|
|
|
|
// Return symbol names of extern values (must be equivalent between C++ and Zig)
|
|
|
|
/** The JS Host function, aka fn (*JSC.JSGlobalObject, *JSC.CallFrame) JSValue.MaybeException */
|
|
export const extJsFunction = (namespaceVar: string, fnLabel: string) =>
|
|
`bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}`;
|
|
/** Each variant gets a dispatcher function. */
|
|
export const extDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: number) =>
|
|
`bindgen_${cap(namespaceVar)}_dispatch${cap(fnLabel)}${variantNumber}`;
|
|
export const extInternalDispatchVariant = (namespaceVar: string, fnLabel: string, variantNumber: string | number) =>
|
|
`bindgen_${cap(namespaceVar)}_js${cap(fnLabel)}_v${variantNumber}`;
|
|
|
|
interface TypeDataDefs {
|
|
/** The name */
|
|
ref: string;
|
|
|
|
sequence: {
|
|
element: TypeImpl;
|
|
repr: "slice";
|
|
};
|
|
record: {
|
|
value: TypeImpl;
|
|
repr: "kv-slices";
|
|
};
|
|
zigEnum: {
|
|
file: string;
|
|
impl: string;
|
|
};
|
|
stringEnum: string[];
|
|
oneOf: TypeImpl[];
|
|
dictionary: DictionaryField[];
|
|
}
|
|
type TypeData<K extends TypeKind> = K extends keyof TypeDataDefs ? TypeDataDefs[K] : any;
|
|
|
|
export const enum NodeValidator {
|
|
validateInteger = "validateInteger",
|
|
}
|
|
|
|
interface Flags {
|
|
nodeValidator?: NodeValidator;
|
|
optional?: boolean;
|
|
required?: boolean;
|
|
nonNull?: boolean;
|
|
default?: any;
|
|
range?: ["clamp" | "enforce", bigint, bigint] | ["clamp" | "enforce", "abi", "abi"];
|
|
finite?: boolean;
|
|
}
|
|
|
|
export interface DictionaryField {
|
|
key: string;
|
|
type: TypeImpl;
|
|
}
|
|
|
|
export declare const isType: unique symbol;
|
|
|
|
const numericTypes = new Set(["f64", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "usize"]);
|
|
|
|
/**
|
|
* Implementation of the Type interface. All types are immutable and hashable.
|
|
* Hashes de-duplicate structure and union definitions. Flags do not account for
|
|
* the hash, so `oneOf(A, B)` and `oneOf(A, B).optional` will point to the same
|
|
* generated struct type, the purpose of the flags are to inform receivers like
|
|
* `t.dictionary` and `fn` to mark uses as optional or provide default values.
|
|
*/
|
|
export class TypeImpl<K extends TypeKind = TypeKind> {
|
|
kind: K;
|
|
data: TypeData<K>;
|
|
flags: Flags;
|
|
/** Access via .name(). */
|
|
nameDeduplicated: string | null | undefined = undefined;
|
|
/** Access via .hash() */
|
|
#hash: string | undefined = undefined;
|
|
ownerFile: string;
|
|
|
|
declare [isType]: true;
|
|
|
|
constructor(kind: K, data: TypeData<K>, flags: Flags = {}) {
|
|
this.kind = kind;
|
|
this.data = data;
|
|
this.flags = flags;
|
|
this.ownerFile = path.basename(stackTraceFileName(snapshotCallerLocation()), ".bind.ts");
|
|
}
|
|
|
|
isVirtualArgument() {
|
|
return this.kind === "globalObject" || this.kind === "zigVirtualMachine";
|
|
}
|
|
|
|
hash() {
|
|
if (this.#hash) {
|
|
return this.#hash;
|
|
}
|
|
let h = `${this.kind}:`;
|
|
switch (this.kind) {
|
|
case "ref":
|
|
throw new Error("TODO");
|
|
case "sequence":
|
|
h += this.data.element.hash();
|
|
break;
|
|
case "record":
|
|
h += this.data.value.hash();
|
|
break;
|
|
case "zigEnum":
|
|
h += `${this.data.file}:${this.data.impl}`;
|
|
break;
|
|
case "stringEnum":
|
|
h += this.data.join(",");
|
|
break;
|
|
case "oneOf":
|
|
h += this.data.map(t => t.hash()).join(",");
|
|
break;
|
|
case "dictionary":
|
|
h += this.data.map(({ key, required, type }) => `${key}:${required}:${type.hash()}`).join(",");
|
|
break;
|
|
}
|
|
let hash = String(Bun.hash(h));
|
|
this.#hash = hash;
|
|
return hash;
|
|
}
|
|
|
|
/**
|
|
* If this type lowers to a named type (struct, union, enum)
|
|
*/
|
|
lowersToNamedType() {
|
|
switch (this.kind) {
|
|
case "ref":
|
|
throw new Error("TODO");
|
|
case "sequence":
|
|
case "record":
|
|
case "oneOf":
|
|
case "dictionary":
|
|
case "stringEnum":
|
|
case "zigEnum":
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
canDirectlyMapToCAbi(): CAbiType | null {
|
|
let kind = this.kind;
|
|
switch (kind) {
|
|
case "ref":
|
|
throw new Error("TODO");
|
|
case "any":
|
|
return "JSValue";
|
|
case "ByteString":
|
|
case "DOMString":
|
|
case "USVString":
|
|
case "UTF8String":
|
|
return "bun.String";
|
|
case "boolean":
|
|
return "bool";
|
|
case "strictBoolean":
|
|
return "bool";
|
|
case "f64":
|
|
case "i8":
|
|
case "i16":
|
|
case "i32":
|
|
case "i64":
|
|
case "u8":
|
|
case "u16":
|
|
case "u32":
|
|
case "u64":
|
|
case "usize":
|
|
return kind;
|
|
case "globalObject":
|
|
case "zigVirtualMachine":
|
|
return "*JSGlobalObject";
|
|
case "stringEnum":
|
|
return cAbiTypeForEnum(this.data.length);
|
|
case "zigEnum":
|
|
throw new Error("TODO");
|
|
case "undefined":
|
|
return "u0";
|
|
case "oneOf": // `union(enum)`
|
|
case "UTF8String": // []const u8
|
|
case "record": // undecided how to lower records
|
|
case "sequence": // []const T
|
|
return null;
|
|
case "externalClass":
|
|
throw new Error("TODO");
|
|
return "*anyopaque";
|
|
case "dictionary": {
|
|
let existing = typeHashToStruct.get(this.hash());
|
|
if (existing) return existing;
|
|
existing = new Struct();
|
|
for (const { key, type } of this.data as DictionaryField[]) {
|
|
if (type.flags.optional && !("default" in type.flags)) {
|
|
return null; // ?T
|
|
}
|
|
const repr = type.canDirectlyMapToCAbi();
|
|
if (!repr) return null;
|
|
|
|
existing.add(key, repr);
|
|
}
|
|
existing.reorderForSmallestSize();
|
|
if (!structHashToSelf.has(existing.hash())) {
|
|
structHashToSelf.set(existing.hash(), existing);
|
|
}
|
|
existing.assignName(this.name());
|
|
typeHashToStruct.set(this.hash(), existing);
|
|
return existing;
|
|
}
|
|
case "sequence": {
|
|
return null;
|
|
}
|
|
default: {
|
|
throw new Error("unexpected: " + (kind satisfies never));
|
|
}
|
|
}
|
|
}
|
|
|
|
name() {
|
|
if (this.nameDeduplicated) {
|
|
return this.nameDeduplicated;
|
|
}
|
|
const hash = this.hash();
|
|
const existing = typeHashToReachableType.get(hash);
|
|
if (existing) return (this.nameDeduplicated = existing.nameDeduplicated ??= this.#generateName());
|
|
return (this.nameDeduplicated = `anon_${this.kind}_${hash}`);
|
|
}
|
|
|
|
cppInternalName() {
|
|
const name = this.name();
|
|
const cAbiType = this.canDirectlyMapToCAbi();
|
|
const namespace = typeHashToNamespace.get(this.hash());
|
|
if (cAbiType) {
|
|
if (typeof cAbiType === "string") {
|
|
return cAbiType;
|
|
}
|
|
}
|
|
return namespace ? `${namespace}${name}` : name;
|
|
}
|
|
|
|
cppClassName() {
|
|
assert(this.lowersToNamedType(), `Does not lower to named type: ${inspect(this)}`);
|
|
const name = this.name();
|
|
const namespace = typeHashToNamespace.get(this.hash());
|
|
return namespace ? `${namespace}::${cap(name)}` : name;
|
|
}
|
|
|
|
cppName() {
|
|
const name = this.name();
|
|
const cAbiType = this.canDirectlyMapToCAbi();
|
|
const namespace = typeHashToNamespace.get(this.hash());
|
|
if (cAbiType && typeof cAbiType === "string" && this.kind !== "zigEnum" && this.kind !== "stringEnum") {
|
|
return cAbiTypeName(cAbiType);
|
|
}
|
|
return namespace ? `${namespace}::${cap(name)}` : name;
|
|
}
|
|
|
|
#generateName() {
|
|
return `bindgen_${this.ownerFile}_${this.hash()}`;
|
|
}
|
|
|
|
/**
|
|
* Name assignment is done to give readable names.
|
|
* The first name to a unique hash wins.
|
|
*/
|
|
assignName(name: string) {
|
|
if (this.nameDeduplicated) return;
|
|
const hash = this.hash();
|
|
const existing = typeHashToReachableType.get(hash);
|
|
if (existing) {
|
|
this.nameDeduplicated = existing.nameDeduplicated ??= name;
|
|
return;
|
|
}
|
|
this.nameDeduplicated = name;
|
|
}
|
|
|
|
markReachable() {
|
|
if (!this.lowersToNamedType()) return;
|
|
const hash = this.hash();
|
|
const existing = typeHashToReachableType.get(hash);
|
|
this.nameDeduplicated ??= existing?.name() ?? `anon_${this.kind}_${hash}`;
|
|
if (!existing) typeHashToReachableType.set(hash, this);
|
|
|
|
switch (this.kind) {
|
|
case "ref":
|
|
throw new Error("TODO");
|
|
case "sequence":
|
|
this.data.element.markReachable();
|
|
break;
|
|
case "record":
|
|
this.data.value.markReachable();
|
|
break;
|
|
case "oneOf":
|
|
for (const type of this.data as TypeImpl[]) {
|
|
type.markReachable();
|
|
}
|
|
break;
|
|
case "dictionary":
|
|
for (const { type } of this.data as DictionaryField[]) {
|
|
type.markReachable();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
#rangeModifier(min: undefined | number | bigint, max: undefined | number | bigint, kind: "clamp" | "enforce") {
|
|
if (this.flags.range) {
|
|
throw new Error("This type already has a range modifier set");
|
|
}
|
|
|
|
// cAbiIntegerLimits throws on non-integer types
|
|
const range = cAbiIntegerLimits(this.kind as CAbiType);
|
|
const abiMin = BigInt(range[0]);
|
|
const abiMax = BigInt(range[1]);
|
|
if (min === undefined) {
|
|
min = abiMin;
|
|
max = abiMax;
|
|
} else {
|
|
if (max === undefined) {
|
|
throw new Error("Expected min and max to be both set or both unset");
|
|
}
|
|
min = BigInt(min);
|
|
max = BigInt(max);
|
|
|
|
if (min < abiMin || min > abiMax) {
|
|
throw new Error(`Expected integer in range ${range}, got ${inspect(min)}`);
|
|
}
|
|
if (max < abiMin || max > abiMax) {
|
|
throw new Error(`Expected integer in range ${range}, got ${inspect(max)}`);
|
|
}
|
|
if (min > max) {
|
|
throw new Error(`Expected min <= max, got ${inspect(min)} > ${inspect(max)}`);
|
|
}
|
|
}
|
|
|
|
return new TypeImpl(this.kind, this.data, {
|
|
...this.flags,
|
|
range: min === BigInt(range[0]) && max === BigInt(range[1]) ? [kind, "abi", "abi"] : [kind, min, max],
|
|
});
|
|
}
|
|
|
|
assertDefaultIsValid(value: unknown) {
|
|
switch (this.kind) {
|
|
case "DOMString":
|
|
case "ByteString":
|
|
case "USVString":
|
|
case "UTF8String":
|
|
if (typeof value !== "string") {
|
|
throw new Error(`Expected string, got ${inspect(value)}`);
|
|
}
|
|
break;
|
|
case "boolean":
|
|
if (typeof value !== "boolean") {
|
|
throw new Error(`Expected boolean, got ${inspect(value)}`);
|
|
}
|
|
break;
|
|
case "f64":
|
|
if (typeof value !== "number") {
|
|
throw new Error(`Expected number, got ${inspect(value)}`);
|
|
}
|
|
break;
|
|
case "usize":
|
|
case "u8":
|
|
case "u16":
|
|
case "u32":
|
|
case "u64":
|
|
case "i8":
|
|
case "i16":
|
|
case "i32":
|
|
case "i64":
|
|
const range = this.flags.range?.slice(1) ?? cAbiIntegerLimits(this.kind);
|
|
if (typeof value === "number") {
|
|
if (value % 1 !== 0) {
|
|
throw new Error(`Expected integer, got ${inspect(value)}`);
|
|
}
|
|
if (value >= Number.MAX_SAFE_INTEGER || value <= Number.MIN_SAFE_INTEGER) {
|
|
throw new Error(
|
|
`Specify default ${this.kind} outside of max safe integer range as a BigInt to avoid precision loss`,
|
|
);
|
|
}
|
|
if (value < Number(range[0]) || value > Number(range[1])) {
|
|
throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`);
|
|
}
|
|
} else if (typeof value === "bigint") {
|
|
if (value < BigInt(range[0]) || value > BigInt(range[1])) {
|
|
throw new Error(`Expected integer in range [${range[0]}, ${range[1]}], got ${inspect(value)}`);
|
|
}
|
|
} else {
|
|
throw new Error(`Expected integer, got ${inspect(value)}`);
|
|
}
|
|
break;
|
|
case "dictionary":
|
|
if (typeof value !== "object" || value === null) {
|
|
throw new Error(`Expected object, got ${inspect(value)}`);
|
|
}
|
|
for (const { key, type } of this.data as DictionaryField[]) {
|
|
if (key in value) {
|
|
type.assertDefaultIsValid(value[key]);
|
|
} else if (type.flags.required) {
|
|
throw new Error(`Missing key ${key} in dictionary`);
|
|
}
|
|
}
|
|
break;
|
|
case "undefined":
|
|
assert(value === undefined, `Expected undefined, got ${inspect(value)}`);
|
|
break;
|
|
default:
|
|
throw new Error(`TODO: set default value on type ${this.kind}`);
|
|
}
|
|
}
|
|
|
|
emitCppDefaultValue(w: CodeWriter) {
|
|
const value = this.flags.default;
|
|
switch (this.kind) {
|
|
case "boolean":
|
|
w.add(value ? "true" : "false");
|
|
break;
|
|
case "f64":
|
|
w.add(String(value));
|
|
break;
|
|
case "usize":
|
|
case "u8":
|
|
case "u16":
|
|
case "u32":
|
|
case "u64":
|
|
case "i8":
|
|
case "i16":
|
|
case "i32":
|
|
case "i64":
|
|
w.add(String(value));
|
|
break;
|
|
case "dictionary":
|
|
const struct = this.structType();
|
|
w.line(`${this.cppName()} {`);
|
|
w.indent();
|
|
for (const { name } of struct.fields) {
|
|
w.add(`.${name} = `);
|
|
const type = this.data.find(f => f.key === name)!.type;
|
|
type.emitCppDefaultValue(w);
|
|
w.line(",");
|
|
}
|
|
w.dedent();
|
|
w.add(`}`);
|
|
break;
|
|
case "DOMString":
|
|
case "ByteString":
|
|
case "USVString":
|
|
case "UTF8String":
|
|
if (typeof value === "string") {
|
|
w.add("Bun::BunStringEmpty");
|
|
} else {
|
|
throw new Error(`TODO: non-empty string default`);
|
|
}
|
|
break;
|
|
case "undefined":
|
|
throw new Error("Zero-sized type");
|
|
default:
|
|
throw new Error(`TODO: set default value on type ${this.kind}`);
|
|
}
|
|
}
|
|
|
|
structType() {
|
|
const direct = this.canDirectlyMapToCAbi();
|
|
assert(typeof direct !== "string");
|
|
if (direct) return direct;
|
|
throw new Error("TODO: generate non-extern struct for representing this data type");
|
|
}
|
|
|
|
isIgnoredUndefinedType() {
|
|
return this.kind === "undefined";
|
|
}
|
|
|
|
isStringType() {
|
|
return (
|
|
this.kind === "DOMString" || this.kind === "ByteString" || this.kind === "USVString" || this.kind === "UTF8String"
|
|
);
|
|
}
|
|
|
|
isNumberType() {
|
|
return numericTypes.has(this.kind);
|
|
}
|
|
|
|
isObjectType() {
|
|
return this.kind === "externalClass" || this.kind === "dictionary";
|
|
}
|
|
|
|
[Symbol.toStringTag] = "Type";
|
|
[Bun.inspect.custom](depth, options, inspect) {
|
|
return (
|
|
`${options.stylize("Type", "special")} ${
|
|
this.lowersToNamedType() && this.nameDeduplicated
|
|
? options.stylize(JSON.stringify(this.nameDeduplicated), "string") + " "
|
|
: ""
|
|
}${options.stylize(
|
|
`[${this.kind}${["required", "optional", "nullable"]
|
|
.filter(k => this.flags[k])
|
|
.map(x => ", " + x)
|
|
.join("")}]`,
|
|
"regexp",
|
|
)}` +
|
|
(this.data
|
|
? " " +
|
|
inspect(this.data, {
|
|
...options,
|
|
depth: options.depth === null ? null : options.depth - 1,
|
|
}).replace(/\n/g, "\n")
|
|
: "")
|
|
);
|
|
}
|
|
|
|
// Public interface definition API
|
|
get optional() {
|
|
if (this.flags.required) {
|
|
throw new Error("Cannot derive optional on a required type");
|
|
}
|
|
if (this.flags.default) {
|
|
throw new Error("Cannot derive optional on a something with a default value (default implies optional)");
|
|
}
|
|
return new TypeImpl(this.kind, this.data, {
|
|
...this.flags,
|
|
optional: true,
|
|
});
|
|
}
|
|
|
|
get finite() {
|
|
if (this.kind !== "f64") {
|
|
throw new Error("finite can only be used on f64");
|
|
}
|
|
if (this.flags.finite) {
|
|
throw new Error("This type already has finite set");
|
|
}
|
|
return new TypeImpl(this.kind, this.data, {
|
|
...this.flags,
|
|
finite: true,
|
|
});
|
|
}
|
|
|
|
get required() {
|
|
if (this.flags.required) {
|
|
throw new Error("This type already has required set");
|
|
}
|
|
if (this.flags.required) {
|
|
throw new Error("Cannot derive required on an optional type");
|
|
}
|
|
return new TypeImpl(this.kind, this.data, {
|
|
...this.flags,
|
|
required: true,
|
|
});
|
|
}
|
|
|
|
default(def: any) {
|
|
if ("default" in this.flags) {
|
|
throw new Error("This type already has a default value");
|
|
}
|
|
if (this.flags.required) {
|
|
throw new Error("Cannot derive default on a required type");
|
|
}
|
|
this.assertDefaultIsValid(def);
|
|
return new TypeImpl(this.kind, this.data, {
|
|
...this.flags,
|
|
default: def,
|
|
});
|
|
}
|
|
|
|
clamp(min?: number | bigint, max?: number | bigint) {
|
|
return this.#rangeModifier(min, max, "clamp");
|
|
}
|
|
|
|
enforceRange(min?: number | bigint, max?: number | bigint) {
|
|
return this.#rangeModifier(min, max, "enforce");
|
|
}
|
|
|
|
get nonNull() {
|
|
if (this.flags.nonNull) {
|
|
throw new Error("Cannot derive nonNull on a nonNull type");
|
|
}
|
|
return new TypeImpl(this.kind, this.data, {
|
|
...this.flags,
|
|
nonNull: true,
|
|
});
|
|
}
|
|
|
|
validateInt32(min?: number, max?: number) {
|
|
if (this.kind !== "i32") {
|
|
throw new Error("validateInt32 can only be used on i32 or u32");
|
|
}
|
|
const rangeInfo = cAbiIntegerLimits("i32");
|
|
return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]);
|
|
}
|
|
|
|
validateUint32(min?: number, max?: number) {
|
|
if (this.kind !== "u32") {
|
|
throw new Error("validateUint32 can only be used on i32 or u32");
|
|
}
|
|
const rangeInfo = cAbiIntegerLimits("u32");
|
|
return this.validateInteger(min ?? rangeInfo[0], max ?? rangeInfo[1]);
|
|
}
|
|
|
|
validateInteger(min?: number | bigint, max?: number | bigint) {
|
|
min ??= Number.MIN_SAFE_INTEGER;
|
|
max ??= Number.MAX_SAFE_INTEGER;
|
|
const enforceRange = this.#rangeModifier(min, max, "enforce") as TypeImpl;
|
|
enforceRange.flags.nodeValidator = NodeValidator.validateInteger;
|
|
return enforceRange;
|
|
}
|
|
}
|
|
|
|
export function cAbiIntegerLimits(type: CAbiType) {
|
|
switch (type) {
|
|
case "u8":
|
|
return [0, 255];
|
|
case "u16":
|
|
return [0, 65535];
|
|
case "u32":
|
|
return [0, 4294967295];
|
|
case "u64":
|
|
return [0, 18446744073709551615n];
|
|
case "usize":
|
|
return [0, 18446744073709551615n];
|
|
case "i8":
|
|
return [-128, 127];
|
|
case "i16":
|
|
return [-32768, 32767];
|
|
case "i32":
|
|
return [-2147483648, 2147483647];
|
|
case "i64":
|
|
return [-9223372036854775808n, 9223372036854775807n];
|
|
case "f64":
|
|
return [-Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
|
|
default:
|
|
throw new Error(`Unexpected type ${type}`);
|
|
}
|
|
}
|
|
|
|
export function cAbiTypeForEnum(length: number): CAbiType {
|
|
return ("u" + alignForward(length, 8)) as CAbiType;
|
|
}
|
|
|
|
export function inspect(value: any) {
|
|
return Bun.inspect(value, { colors: Bun.enableANSIColors });
|
|
}
|
|
|
|
export function oneOfImpl(types: TypeImpl[]): TypeImpl {
|
|
const out: TypeImpl[] = [];
|
|
for (const type of types) {
|
|
if (type.kind === "oneOf") {
|
|
out.push(...type.data);
|
|
} else {
|
|
if (type.flags.default) {
|
|
throw new Error(
|
|
"Union type cannot include a default value. Instead, set a default value on the union type itself",
|
|
);
|
|
}
|
|
if (type.isVirtualArgument()) {
|
|
throw new Error(`t.${type.kind} can only be used as a function argument type`);
|
|
}
|
|
out.push(type);
|
|
}
|
|
}
|
|
return new TypeImpl("oneOf", out);
|
|
}
|
|
|
|
export function dictionaryImpl(record: Record<string, TypeImpl>): TypeImpl {
|
|
const out: DictionaryField[] = [];
|
|
for (const key in record) {
|
|
const type = record[key];
|
|
if (type.isVirtualArgument()) {
|
|
throw new Error(`t.${type.kind} can only be used as a function argument type`);
|
|
}
|
|
out.push({
|
|
key,
|
|
type: type,
|
|
});
|
|
}
|
|
return new TypeImpl("dictionary", out);
|
|
}
|
|
|
|
export const isFunc = Symbol("isFunc");
|
|
|
|
export interface Func {
|
|
[isFunc]: true;
|
|
name: string;
|
|
zigPrefix: string;
|
|
snapshot: string;
|
|
zigFile: string;
|
|
variants: Variant[];
|
|
}
|
|
|
|
export interface Variant {
|
|
suffix: string;
|
|
args: Arg[];
|
|
ret: TypeImpl;
|
|
returnStrategy?: ReturnStrategy;
|
|
argStruct?: Struct;
|
|
globalObjectArg?: number | "hidden";
|
|
minRequiredArgs: number;
|
|
communicationStruct?: Struct;
|
|
}
|
|
|
|
export interface Arg {
|
|
name: string;
|
|
type: TypeImpl;
|
|
loweringStrategy?: ArgStrategy;
|
|
zigMappedName?: string;
|
|
}
|
|
|
|
/**
|
|
* The strategy for moving arguments over the ABI boundary are computed before
|
|
* any code is generated so that the proper definitions can be easily made,
|
|
* while allow new special cases to be added.
|
|
*/
|
|
export type ArgStrategy =
|
|
// The argument is communicated as a C parameter
|
|
| { type: "c-abi-pointer"; abiType: CAbiType }
|
|
// The argument is communicated as a C parameter
|
|
| { type: "c-abi-value"; abiType: CAbiType }
|
|
// The data is added as a field on `.communicationStruct`
|
|
| {
|
|
type: "uses-communication-buffer";
|
|
/**
|
|
* Unique prefix for fields. For example, moving an optional over the ABI
|
|
* boundary uses two fields, `bool {prefix}_set` and `T {prefix}_value`.
|
|
*/
|
|
prefix: string;
|
|
/**
|
|
* For compound complex types, such as `?union(enum) { a: u32, b:
|
|
* bun.String }`, the child item is assigned the prefix
|
|
* `{prefix_of_optional}_value`. The interpretation of this array depends
|
|
* on `arg.type.kind`.
|
|
*/
|
|
children: ArgStrategyChildItem[];
|
|
};
|
|
|
|
export type ArgStrategyChildItem =
|
|
| {
|
|
type: "c-abi-compatible";
|
|
abiType: CAbiType;
|
|
}
|
|
| {
|
|
type: "uses-communication-buffer";
|
|
prefix: string;
|
|
children: ArgStrategyChildItem[];
|
|
};
|
|
/**
|
|
* In addition to moving a payload over, an additional bit of information
|
|
* crosses the ABI boundary indicating if the function threw an exception.
|
|
*
|
|
* For simplicity, the possibility of any Zig binding returning an error/calling
|
|
* `throw` is assumed and there isnt a way to disable the exception check.
|
|
*/
|
|
export type ReturnStrategy =
|
|
// JSValue is special cased because it encodes exception as 0x0
|
|
| { type: "jsvalue" }
|
|
// Return value doesnt exist. function returns a boolean indicating success/error.
|
|
| { type: "void" }
|
|
// For primitives and simple structures where direct assignment into a
|
|
// pointer is possible. function returns a boolean indicating success/error.
|
|
| { type: "basic-out-param"; abiType: CAbiType };
|
|
|
|
export interface File {
|
|
functions: Func[];
|
|
typedefs: TypeDef[];
|
|
}
|
|
|
|
export interface TypeDef {
|
|
name: string;
|
|
type: TypeImpl;
|
|
}
|
|
|
|
export function registerFunction(opts: FuncOptions) {
|
|
const snapshot = snapshotCallerLocation();
|
|
const filename = stackTraceFileName(snapshot);
|
|
expect(filename).toEndWith(".bind.ts");
|
|
const zigFile = path.relative(src, filename.replace(/\.bind\.ts$/, ".zig"));
|
|
let file = files.get(zigFile);
|
|
if (!file) {
|
|
file = { functions: [], typedefs: [] };
|
|
files.set(zigFile, file);
|
|
}
|
|
const variants: Variant[] = [];
|
|
if ("variants" in opts) {
|
|
let i = 1;
|
|
for (const variant of opts.variants) {
|
|
const { minRequiredArgs } = validateVariant(variant);
|
|
variants.push({
|
|
args: Object.entries(variant.args).map(([name, type]) => ({ name, type })) as Arg[],
|
|
ret: variant.ret as TypeImpl,
|
|
suffix: `${i}`,
|
|
minRequiredArgs,
|
|
} as unknown as Variant);
|
|
i++;
|
|
}
|
|
} else {
|
|
const { minRequiredArgs } = validateVariant(opts);
|
|
variants.push({
|
|
suffix: "",
|
|
args: Object.entries(opts.args).map(([name, type]) => ({ name, type })) as Arg[],
|
|
ret: opts.ret as TypeImpl,
|
|
minRequiredArgs,
|
|
});
|
|
}
|
|
|
|
const func: Func = {
|
|
[isFunc]: true,
|
|
name: "",
|
|
zigPrefix: opts.implNamespace ? `${opts.implNamespace}.` : "",
|
|
snapshot,
|
|
zigFile,
|
|
variants,
|
|
};
|
|
allFunctions.push(func);
|
|
file.functions.push(func);
|
|
return func;
|
|
}
|
|
|
|
function validateVariant(variant: any) {
|
|
let minRequiredArgs = 0;
|
|
let seenOptionalArgument = false;
|
|
let i = 0;
|
|
|
|
for (const [name, type] of Object.entries(variant.args) as [string, TypeImpl][]) {
|
|
if (!(type instanceof TypeImpl)) {
|
|
throw new Error(`Expected type for argument ${name}, got ${inspect(type)}`);
|
|
}
|
|
i += 1;
|
|
if (type.isVirtualArgument()) {
|
|
continue;
|
|
}
|
|
if (!type.flags.optional && !("default" in type.flags)) {
|
|
if (seenOptionalArgument) {
|
|
throw new Error(`Required argument ${name} cannot follow an optional argument`);
|
|
}
|
|
minRequiredArgs++;
|
|
} else {
|
|
seenOptionalArgument = true;
|
|
}
|
|
}
|
|
|
|
return { minRequiredArgs };
|
|
}
|
|
|
|
function snapshotCallerLocation(): string {
|
|
const stack = new Error().stack!;
|
|
const lines = stack.split("\n");
|
|
let i = 1;
|
|
for (; i < lines.length; i++) {
|
|
if (!lines[i].includes(import.meta.dir)) {
|
|
return lines[i];
|
|
}
|
|
}
|
|
throw new Error("Couldn't find caller location in stack trace");
|
|
}
|
|
|
|
function stackTraceFileName(line: string): string {
|
|
const match = /(?:at\s+|\()(.:?[^:\n(\)]*)[^(\n]*$/i.exec(line);
|
|
assert(match, `Couldn't extract filename from stack trace line: ${line}`);
|
|
return match[1].replaceAll("\\", "/");
|
|
}
|
|
|
|
export type CAbiType =
|
|
| "*anyopaque"
|
|
| "*JSGlobalObject"
|
|
| "JSValue"
|
|
| "JSValue.MaybeException"
|
|
| "u0"
|
|
| "bun.String"
|
|
| "bool"
|
|
| "u8"
|
|
| "u16"
|
|
| "u32"
|
|
| "u64"
|
|
| "usize"
|
|
| "i8"
|
|
| "i16"
|
|
| "i32"
|
|
| "i64"
|
|
| "f64"
|
|
| Struct;
|
|
|
|
export function cAbiTypeInfo(type: CAbiType): [size: number, align: number] {
|
|
if (typeof type !== "string") {
|
|
return type.abiInfo();
|
|
}
|
|
switch (type) {
|
|
case "u0":
|
|
return [0, 0]; // no-op
|
|
case "bool":
|
|
case "u8":
|
|
case "i8":
|
|
return [1, 1];
|
|
case "u16":
|
|
case "i16":
|
|
return [2, 2];
|
|
case "u32":
|
|
case "i32":
|
|
return [4, 4];
|
|
case "usize":
|
|
case "u64":
|
|
case "i64":
|
|
case "f64":
|
|
return [8, 8];
|
|
case "*anyopaque":
|
|
case "*JSGlobalObject":
|
|
case "JSValue":
|
|
case "JSValue.MaybeException":
|
|
return [8, 8]; // pointer size
|
|
case "bun.String":
|
|
return [24, 8];
|
|
default:
|
|
throw new Error("unexpected: " + (type satisfies never));
|
|
}
|
|
}
|
|
|
|
export function cAbiTypeName(type: CAbiType) {
|
|
if (typeof type !== "string") {
|
|
return type.name();
|
|
}
|
|
return (
|
|
{
|
|
"*anyopaque": "void*",
|
|
"*JSGlobalObject": "JSC::JSGlobalObject*",
|
|
"JSValue": "JSValue",
|
|
"JSValue.MaybeException": "JSValue",
|
|
"bool": "bool",
|
|
"u8": "uint8_t",
|
|
"u16": "uint16_t",
|
|
"u32": "uint32_t",
|
|
"u64": "uint64_t",
|
|
"i8": "int8_t",
|
|
"i16": "int16_t",
|
|
"i32": "int32_t",
|
|
"i64": "int64_t",
|
|
"f64": "double",
|
|
"usize": "size_t",
|
|
"bun.String": "BunString",
|
|
u0: "void",
|
|
} satisfies Record<Extract<CAbiType, string>, string>
|
|
)[type];
|
|
}
|
|
|
|
export function alignForward(size: number, alignment: number) {
|
|
return Math.floor((size + alignment - 1) / alignment) * alignment;
|
|
}
|
|
|
|
export class Struct {
|
|
fields: StructField[] = [];
|
|
#hash?: string;
|
|
#name?: string;
|
|
namespace?: string;
|
|
|
|
abiInfo(): [size: number, align: number] {
|
|
let size = 0;
|
|
let align = 0;
|
|
for (const field of this.fields) {
|
|
size = alignForward(size, field.naturalAlignment);
|
|
size += field.size;
|
|
align = Math.max(align, field.naturalAlignment);
|
|
}
|
|
return [size, align];
|
|
}
|
|
|
|
reorderForSmallestSize() {
|
|
// for conistency sort by alignment, then size, then name
|
|
this.fields.sort((a, b) => {
|
|
if (a.naturalAlignment !== b.naturalAlignment) {
|
|
return a.naturalAlignment - b.naturalAlignment;
|
|
}
|
|
if (a.size !== b.size) {
|
|
return a.size - b.size;
|
|
}
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
}
|
|
|
|
hash() {
|
|
return (this.#hash ??= String(
|
|
Bun.hash(
|
|
this.fields
|
|
.map(f => {
|
|
if (f.type instanceof Struct) {
|
|
return f.name + `:` + f.type.hash();
|
|
}
|
|
return f.name + `:` + f.type;
|
|
})
|
|
.join(","),
|
|
),
|
|
));
|
|
}
|
|
|
|
name() {
|
|
if (this.#name) return this.#name;
|
|
const hash = this.hash();
|
|
const existing = structHashToSelf.get(hash);
|
|
if (existing && existing !== this) return (this.#name = existing.name());
|
|
return (this.#name = `anon_extern_struct_${hash}`);
|
|
}
|
|
|
|
toString() {
|
|
return this.namespace ? `${this.namespace}.${this.name()}` : this.name();
|
|
}
|
|
|
|
assignName(name: string) {
|
|
if (this.#name) return;
|
|
const hash = this.hash();
|
|
const existing = structHashToSelf.get(hash);
|
|
if (existing && existing.#name) name = existing.#name;
|
|
this.#name = name;
|
|
if (existing) existing.#name = name;
|
|
}
|
|
|
|
assignGeneratedName(name: string) {
|
|
if (this.#name) return;
|
|
this.assignName(name);
|
|
}
|
|
|
|
add(name: string, cType: CAbiType) {
|
|
const [size, naturalAlignment] = cAbiTypeInfo(cType);
|
|
this.fields.push({ name, type: cType, size, naturalAlignment });
|
|
}
|
|
|
|
emitZig(zig: CodeWriter, semi: "with-semi" | "no-semi") {
|
|
zig.line("extern struct {");
|
|
zig.indent();
|
|
for (const field of this.fields) {
|
|
zig.line(`${snake(field.name)}: ${field.type},`);
|
|
}
|
|
zig.dedent();
|
|
zig.line("}" + (semi === "with-semi" ? ";" : ""));
|
|
}
|
|
|
|
emitCpp(cpp: CodeWriter, structName: string) {
|
|
cpp.line(`struct ${structName} {`);
|
|
cpp.indent();
|
|
for (const field of this.fields) {
|
|
cpp.line(`${cAbiTypeName(field.type)} ${field.name};`);
|
|
}
|
|
cpp.dedent();
|
|
cpp.line("};");
|
|
}
|
|
}
|
|
|
|
export interface StructField {
|
|
/** camel case */
|
|
name: string;
|
|
type: CAbiType;
|
|
size: number;
|
|
naturalAlignment: number;
|
|
}
|
|
|
|
export class CodeWriter {
|
|
level = 0;
|
|
buffer = "";
|
|
|
|
temporaries = new Set<string>();
|
|
|
|
line(s?: string) {
|
|
this.add((s ?? "") + "\n");
|
|
}
|
|
|
|
add(s: string) {
|
|
this.buffer += (this.buffer.endsWith("\n") ? " ".repeat(this.level) : "") + s;
|
|
}
|
|
|
|
indent() {
|
|
this.level += 1;
|
|
}
|
|
|
|
dedent() {
|
|
this.level -= 1;
|
|
}
|
|
|
|
trimLastNewline() {
|
|
this.buffer = this.buffer.trimEnd();
|
|
}
|
|
|
|
resetTemporaries() {
|
|
this.temporaries.clear();
|
|
}
|
|
|
|
nextTemporaryName(label: string) {
|
|
let i = 0;
|
|
let name = `${label}_${i}`;
|
|
while (this.temporaries.has(name)) {
|
|
i++;
|
|
name = `${label}_${i}`;
|
|
}
|
|
this.temporaries.add(name);
|
|
return name;
|
|
}
|
|
}
|