// The binding generator to rule them all. // Converts binding definition files (.bind.ts) into C++ and Zig code. // // Generated bindings are available in `bun.generated..*` in Zig, // or `Generated::::*` in C++ from including `Generated.h`. import assert from "node:assert"; import fs from "node:fs"; import * as path from "node:path"; import { ArgStrategyChildItem, CodeWriter, Func, NodeValidator, Struct, TypeImpl, alignForward, cAbiIntegerLimits, cAbiTypeName, cap, extDispatchVariant, extInternalDispatchVariant, extJsFunction, files, inspect, isFunc, pascal, snake, src, str, typeHashToNamespace, typeHashToReachableType, zid, type CAbiType, type DictionaryField, type ReturnStrategy, type TypeKind, type Variant, } from "./bindgen-lib-internal"; import { argParse, readdirRecursiveWithExclusionsAndExtensionsSync, writeIfNotChanged } from "./helpers"; // arg parsing let { "codegen-root": codegenRoot, debug } = argParse(["codegen-root", "debug"]); if (debug === "false" || debug === "0" || debug == "OFF") debug = false; if (!codegenRoot) { console.error("Missing --codegen-root=..."); process.exit(1); } function resolveVariantStrategies(vari: Variant, name: string) { let argIndex = 0; let communicationStruct: Struct | undefined; for (const arg of vari.args) { if (arg.type.isVirtualArgument() && vari.globalObjectArg === undefined) { vari.globalObjectArg = argIndex; } argIndex += 1; // If `extern struct` can represent this type, that is the simplest way to cross the C-ABI boundary. const isNullable = arg.type.flags.optional && !("default" in arg.type.flags); const abiType = !isNullable && arg.type.canDirectlyMapToCAbi(); if (abiType) { arg.loweringStrategy = { // This does not work in release builds, possibly due to a Zig 0.13 bug // regarding by-value extern structs in C functions. // type: cAbiTypeInfo(abiType)[0] > 8 ? "c-abi-pointer" : "c-abi-value", // Always pass an argument by-pointer for now. type: abiType === "*anyopaque" || abiType === "*JSGlobalObject" ? "c-abi-value" : "c-abi-pointer", abiType, }; continue; } communicationStruct ??= new Struct(); const prefix = `${arg.name}`; const children = isNullable ? resolveNullableArgumentStrategy(arg.type, prefix, communicationStruct) : resolveComplexArgumentStrategy(arg.type, prefix, communicationStruct); arg.loweringStrategy = { type: "uses-communication-buffer", prefix, children, }; } if (vari.globalObjectArg === undefined) { vari.globalObjectArg = "hidden"; } return_strategy: { if (vari.ret.kind === "undefined") { vari.returnStrategy = { type: "void" }; break return_strategy; } if (vari.ret.kind === "any") { vari.returnStrategy = { type: "jsvalue" }; break return_strategy; } const abiType = vari.ret.canDirectlyMapToCAbi(); if (abiType) { vari.returnStrategy = { type: "basic-out-param", abiType, }; break return_strategy; } } communicationStruct?.reorderForSmallestSize(); communicationStruct?.assignGeneratedName(name); vari.communicationStruct = communicationStruct; } function resolveNullableArgumentStrategy( type: TypeImpl, prefix: string, communicationStruct: Struct, ): ArgStrategyChildItem[] { assert(type.flags.optional && !("default" in type.flags)); communicationStruct.add(`${prefix}Set`, "bool"); return resolveComplexArgumentStrategy(type, `${prefix}Value`, communicationStruct); } function resolveComplexArgumentStrategy( type: TypeImpl, prefix: string, communicationStruct: Struct, ): ArgStrategyChildItem[] { const abiType = type.canDirectlyMapToCAbi(); if (abiType) { communicationStruct.add(prefix, abiType); return [ { type: "c-abi-compatible", abiType, }, ]; } switch (type.kind) { default: throw new Error(`TODO: resolveComplexArgumentStrategy for ${type.kind}`); } } function emitCppCallToVariant(name: string, variant: Variant, dispatchFunctionName: string) { cpp.line(`auto& vm = JSC::getVM(global);`); cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); if (variant.minRequiredArgs > 0) { cpp.line(`size_t argumentCount = callFrame->argumentCount();`); cpp.line(`if (argumentCount < ${variant.minRequiredArgs}) {`); cpp.line(` return JSC::throwVMError(global, throwScope, createNotEnoughArgumentsError(global));`); cpp.line(`}`); } const communicationStruct = variant.communicationStruct; if (communicationStruct) { cpp.line(`${communicationStruct.name()} buf;`); communicationStruct.emitCpp(cppInternal, communicationStruct.name()); } let i = 0; for (const arg of variant.args) { const type = arg.type; if (type.isVirtualArgument()) continue; if (type.isIgnoredUndefinedType()) { i += 1; continue; } const exceptionContext: ExceptionContext = { type: "argument", argumentIndex: i, name: arg.name, functionName: name, }; const strategy = arg.loweringStrategy!; assert(strategy); const get = variant.minRequiredArgs > i ? "uncheckedArgument" : "argument"; cpp.line(`JSC::EnsureStillAliveScope arg${i} = callFrame->${get}(${i});`); let storageLocation; let needDeclare = true; switch (strategy.type) { case "c-abi-pointer": case "c-abi-value": storageLocation = "arg" + cap(arg.name); break; case "uses-communication-buffer": storageLocation = `buf.${strategy.prefix}`; needDeclare = false; break; default: throw new Error(`TODO: emitCppCallToVariant for ${inspect(strategy)}`); } const jsValueRef = `arg${i}.value()`; /** If JavaScript may pass null or undefined */ const isOptionalToUser = type.flags.optional || "default" in type.flags; /** If the final representation may include null */ const isNullable = type.flags.optional && !("default" in type.flags); if (isOptionalToUser) { if (needDeclare) { addHeaderForType(type); cpp.line(`${type.cppName()} ${storageLocation};`); } const isUndefinedOrNull = type.flags.nonNull ? "isUndefined" : "isUndefinedOrNull"; if (isNullable) { assert(strategy.type === "uses-communication-buffer"); cpp.line(`if ((${storageLocation}Set = !${jsValueRef}.${isUndefinedOrNull}())) {`); storageLocation = `${storageLocation}Value`; } else { cpp.line(`if (!${jsValueRef}.${isUndefinedOrNull}()) {`); } cpp.indent(); emitConvertValue(storageLocation, arg.type, jsValueRef, exceptionContext, "assign"); cpp.dedent(); if ("default" in type.flags) { cpp.line(`} else {`); cpp.indent(); cpp.add(`${storageLocation} = `); type.emitCppDefaultValue(cpp); cpp.line(";"); cpp.dedent(); } else { assert(isNullable); } cpp.line(`}`); } else { emitConvertValue(storageLocation, arg.type, jsValueRef, exceptionContext, needDeclare ? "declare" : "assign"); } i += 1; } const returnStrategy = variant.returnStrategy!; switch (returnStrategy.type) { case "jsvalue": cpp.line(`return ${dispatchFunctionName}(`); break; case "basic-out-param": cpp.line(`${cAbiTypeName(returnStrategy.abiType)} out;`); cpp.line(`if (!${dispatchFunctionName}(`); break; case "void": cpp.line(`if (!${dispatchFunctionName}(`); break; default: throw new Error(`TODO: emitCppCallToVariant for ${inspect(returnStrategy)}`); } let emittedFirstArgument = false; function addCommaAfterArgument() { if (emittedFirstArgument) { cpp.line(","); } else { emittedFirstArgument = true; } } const totalArgs = variant.args.length; i = 0; cpp.indent(); if (variant.globalObjectArg === "hidden") { addCommaAfterArgument(); cpp.add("global"); } for (const arg of variant.args) { i += 1; if (arg.type.isIgnoredUndefinedType()) continue; if (arg.type.isVirtualArgument()) { switch (arg.type.kind) { case "zigVirtualMachine": case "globalObject": addCommaAfterArgument(); cpp.add("global"); break; default: throw new Error(`TODO: emitCppCallToVariant for ${inspect(arg.type)}`); } } else { const storageLocation = `arg${cap(arg.name)}`; const strategy = arg.loweringStrategy!; switch (strategy.type) { case "c-abi-pointer": addCommaAfterArgument(); cpp.add(`&${storageLocation}`); break; case "c-abi-value": addCommaAfterArgument(); cpp.add(`${storageLocation}`); break; case "uses-communication-buffer": break; default: throw new Error(`TODO: emitCppCallToVariant for ${inspect(strategy)}`); } } } if (communicationStruct) { addCommaAfterArgument(); cpp.add("&buf"); } switch (returnStrategy.type) { case "jsvalue": cpp.dedent(); if (totalArgs === 0) { cpp.trimLastNewline(); } cpp.line(");"); break; case "void": cpp.dedent(); cpp.line(")) {"); cpp.line(` return {};`); cpp.line("}"); cpp.line("return JSC::JSValue::encode(JSC::jsUndefined());"); break; case "basic-out-param": addCommaAfterArgument(); cpp.add("&out"); cpp.line(); cpp.dedent(); cpp.line(")) {"); cpp.line(` return {};`); cpp.line("}"); const simpleType = getSimpleIdlType(variant.ret); if (simpleType) { cpp.line(`return JSC::JSValue::encode(WebCore::toJS<${simpleType}>(*global, out));`); break; } switch (variant.ret.kind) { case "UTF8String": throw new Error("Memory lifetime is ambiguous when returning UTF8String"); case "DOMString": case "USVString": case "ByteString": cpp.line( `return JSC::JSValue::encode(WebCore::toJS(*global, out.toWTFString()));`, ); break; } break; default: throw new Error(`TODO: emitCppCallToVariant for ${inspect(returnStrategy)}`); } } /** If a simple IDL type mapping exists, it also currently means there is a direct C ABI mapping */ function getSimpleIdlType(type: TypeImpl): string | undefined { const map: { [K in TypeKind]?: string } = { boolean: "WebCore::IDLBoolean", undefined: "WebCore::IDLUndefined", usize: "WebCore::IDLUnsignedLongLong", u8: "WebCore::IDLOctet", u16: "WebCore::IDLUnsignedShort", u32: "WebCore::IDLUnsignedLong", u64: "WebCore::IDLUnsignedLongLong", i8: "WebCore::IDLByte", i16: "WebCore::IDLShort", i32: "WebCore::IDLLong", i64: "WebCore::IDLLongLong", }; let entry = map[type.kind]; if (!entry) { switch (type.kind) { case "f64": entry = type.flags.finite // ? "WebCore::IDLDouble" : "WebCore::IDLUnrestrictedDouble"; break; case "stringEnum": type.lowersToNamedType; // const cType = cAbiTypeForEnum(type.data.length); // entry = map[cType as IntegerTypeKind]!; entry = `WebCore::IDLEnumeration<${type.cppClassName()}>`; break; default: return; } } if (type.flags.range) { const { range, nodeValidator } = type.flags; if ((range[0] === "enforce" && range[1] !== "abi") || nodeValidator) { if (nodeValidator) assert(nodeValidator === NodeValidator.validateInteger); // TODO? const [abiMin, abiMax] = cAbiIntegerLimits(type.kind as CAbiType); let [_, min, max] = range as [string, bigint | number | "abi", bigint | number | "abi"]; if (min === "abi") min = abiMin; if (max === "abi") max = abiMax; headers.add("BindgenCustomEnforceRange.h"); entry = `Bun::BindgenCustomEnforceRange<${cAbiTypeName(type.kind as CAbiType)}, ${min}, ${max}, Bun::BindgenCustomEnforceRangeKind::${ nodeValidator ? "Node" : "Web" }>`; } else { const rangeAdaptor = { "clamp": "WebCore::IDLClampAdaptor", "enforce": "WebCore::IDLEnforceRangeAdaptor", }[range[0]]; assert(rangeAdaptor); entry = `${rangeAdaptor}<${entry}>`; } } return entry; } type ExceptionContext = | { type: "none" } | { type: "argument"; argumentIndex: number; name: string; functionName: string }; function emitConvertValue( storageLocation: string, type: TypeImpl, jsValueRef: string, exceptionContext: ExceptionContext, decl: "declare" | "assign", ) { if (decl === "declare") { addHeaderForType(type); } const simpleType = getSimpleIdlType(type); if (simpleType) { const cAbiType = type.canDirectlyMapToCAbi(); assert(cAbiType); let exceptionHandler: ExceptionHandler | undefined; switch (exceptionContext.type) { case "none": break; case "argument": exceptionHandler = getArgumentExceptionHandler( type, exceptionContext.argumentIndex, exceptionContext.name, exceptionContext.functionName, ); } switch (type.kind) { } if (decl === "declare") { cpp.add(`${type.cppName()} `); } let exceptionHandlerText = exceptionHandler ? `, ${exceptionHandler.params} { ${exceptionHandler.body} }` : ""; cpp.line(`${storageLocation} = WebCore::convert<${simpleType}>(*global, ${jsValueRef}${exceptionHandlerText});`); if (type.flags.range && type.flags.range[0] === "clamp" && type.flags.range[1] !== "abi") { emitRangeModifierCheck(cAbiType, storageLocation, type.flags.range); } cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); } else { switch (type.kind) { case "any": { if (decl === "declare") { cpp.add(`${type.cppName()} `); } cpp.line(`${storageLocation} = JSC::JSValue::encode(${jsValueRef});`); break; } case "USVString": case "DOMString": case "ByteString": { const temp = cpp.nextTemporaryName("wtfString"); cpp.line(`WTF::String ${temp} = WebCore::convert(*global, ${jsValueRef});`); cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); if (decl === "declare") { cpp.add(`${type.cppName()} `); } cpp.line(`${storageLocation} = Bun::toString(${temp});`); break; } case "UTF8String": { const temp = cpp.nextTemporaryName("wtfString"); cpp.line(`WTF::String ${temp} = WebCore::convert(*global, ${jsValueRef});`); cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); if (decl === "declare") { cpp.add(`${type.cppName()} `); } cpp.line(`${storageLocation} = Bun::toString(${temp});`); break; } case "dictionary": { if (decl === "declare") { cpp.line(`${type.cppName()} ${storageLocation};`); } cpp.line(`auto did_convert = convert${type.cppInternalName()}(&${storageLocation}, global, ${jsValueRef});`); cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); cpp.line(`if (!did_convert) return {};`); break; } default: throw new Error(`TODO: emitConvertValue for Type ${type.kind}`); } } } interface ExceptionHandler { /** @example "[](JSC::JSGlobalObject& global, ThrowScope& scope)" */ params: string; /** @example "WebCore::throwTypeError(global, scope)" */ body: string; } function getArgumentExceptionHandler(type: TypeImpl, argumentIndex: number, name: string, functionName: string) { const { nodeValidator } = type.flags; if (nodeValidator) { switch (nodeValidator) { case NodeValidator.validateInteger: headers.add("ErrorCode.h"); return { params: `[]()`, body: `return ${str(name)}_s;`, }; default: throw new Error(`TODO: implement exception thrower for node validator ${nodeValidator}`); } } switch (type.kind) { case "zigEnum": case "stringEnum": { return { params: `[](JSC::JSGlobalObject& global, JSC::ThrowScope& scope)`, body: `WebCore::throwArgumentMustBeEnumError(${[ `global`, `scope`, `${argumentIndex}`, `${str(name)}_s`, `${str(type.name())}_s`, `${str(functionName)}_s`, `WebCore::expectedEnumerationValues<${type.cppClassName()}>()`, ].join(", ")});`, }; break; } } } /** * The built in WebCore range adaptors do not support arbitrary ranges, but that * is something we want to have. They aren't common, so they are just tacked * onto the webkit one. */ function emitRangeModifierCheck( cAbiType: CAbiType, storageLocation: string, range: ["clamp" | "enforce", bigint, bigint], ) { const [kind, min, max] = range; if (kind === "clamp") { cpp.line(`if (${storageLocation} < ${min}) ${storageLocation} = ${min};`); cpp.line(`else if (${storageLocation} > ${max}) ${storageLocation} = ${max};`); } else { // Implemented in BindgenCustomEnforceRange throw new Error(`This should not be called for 'enforceRange' types.`); } } function addHeaderForType(type: TypeImpl) { if (type.lowersToNamedType() && type.ownerFile) { headers.add(`Generated${pascal(type.ownerFile)}.h`); } } function emitConvertDictionaryFunction(type: TypeImpl) { assert(type.kind === "dictionary"); const fields = type.data as DictionaryField[]; addHeaderForType(type); cpp.line(`// Internal dictionary parse for ${type.name()}`); cpp.line( `bool convert${type.cppInternalName()}(${type.cppName()}* result, JSC::JSGlobalObject* global, JSC::JSValue value) {`, ); cpp.indent(); cpp.line(`auto& vm = JSC::getVM(global);`); cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); cpp.line(`bool isNullOrUndefined = value.isUndefinedOrNull();`); cpp.line(`auto* object = isNullOrUndefined ? nullptr : value.getObject();`); cpp.line(`if (!isNullOrUndefined && !object) [[unlikely]] {`); cpp.line(` throwTypeError(global, throwScope);`); cpp.line(` return false;`); cpp.line(`}`); cpp.line(`JSC::JSValue propValue;`); for (const field of fields) { const { key, type: fieldType } = field; cpp.line("// " + key); cpp.line(`if (isNullOrUndefined) {`); cpp.line(` propValue = JSC::jsUndefined();`); cpp.line(`} else {`); headers.add("ObjectBindings.h"); cpp.line( ` propValue = Bun::getIfPropertyExistsPrototypePollutionMitigation(vm, global, object, JSC::Identifier::fromString(vm, ${str(key)}_s));`, ); cpp.line(` RETURN_IF_EXCEPTION(throwScope, false);`); cpp.line(`}`); cpp.line(`if (!propValue.isUndefined()) {`); cpp.indent(); emitConvertValue(`result->${key}`, fieldType, "propValue", { type: "none" }, "assign"); cpp.dedent(); cpp.line(`} else {`); cpp.indent(); if (type.flags.required) { cpp.line(`throwTypeError(global, throwScope);`); cpp.line(`return false;`); } else if ("default" in fieldType.flags) { cpp.add(`result->${key} = `); fieldType.emitCppDefaultValue(cpp); cpp.line(";"); } else { throw new Error(`TODO: optional dictionary field`); } cpp.dedent(); cpp.line(`}`); } cpp.line(`return true;`); cpp.dedent(); cpp.line(`}`); cpp.line(); } function emitZigStruct(type: TypeImpl) { zig.add(`pub const ${type.name()} = `); switch (type.kind) { case "zigEnum": case "stringEnum": { const signPrefix = "u"; const tagType = `${signPrefix}${alignForward(type.data.length, 8)}`; zig.line(`enum(${tagType}) {`); zig.indent(); for (const value of type.data) { zig.line(`${snake(value)},`); } zig.dedent(); zig.line("};"); return; } } const externLayout = type.canDirectlyMapToCAbi(); if (externLayout) { if (typeof externLayout === "string") { zig.line(externLayout + ";"); } else { externLayout.emitZig(zig, "with-semi"); } return; } switch (type.kind) { case "dictionary": { zig.line("struct {"); zig.indent(); for (const { key, type: fieldType } of type.data as DictionaryField[]) { zig.line(` ${snake(key)}: ${zigTypeName(fieldType)},`); } zig.dedent(); zig.line(`};`); break; } default: { throw new Error(`TODO: emitZigStruct for Type ${type.kind}`); } } } function emitCppStructHeader(w: CodeWriter, type: TypeImpl) { if (type.kind === "zigEnum" || type.kind === "stringEnum") { emitCppEnumHeader(w, type); return; } const externLayout = type.canDirectlyMapToCAbi(); if (externLayout) { if (typeof externLayout === "string") { w.line(`typedef ${externLayout} ${type.name()};`); console.warn("should this really be done lol", type); } else { externLayout.emitCpp(w, type.name()); w.line(); } return; } switch (type.kind) { default: { throw new Error(`TODO: emitZigStruct for Type ${type.kind}`); } } } function emitCppEnumHeader(w: CodeWriter, type: TypeImpl) { assert(type.kind === "zigEnum" || type.kind === "stringEnum"); assert(type.kind === "stringEnum"); // TODO assert(type.data.length > 0); const signPrefix = "u"; const intBits = alignForward(type.data.length, 8); const tagType = `${signPrefix}int${intBits}_t`; w.line(`enum class ${type.name()} : ${tagType} {`); for (const value of type.data) { w.line(` ${pascal(value)},`); } w.line(`};`); w.line(); } // This function assumes in the WebCore namespace function emitConvertEnumFunction(w: CodeWriter, type: TypeImpl) { assert(type.kind === "zigEnum" || type.kind === "stringEnum"); assert(type.kind === "stringEnum"); // TODO assert(type.data.length > 0); const name = "Generated::" + type.cppName(); headers.add("JavaScriptCore/JSCInlines.h"); headers.add("JavaScriptCore/JSString.h"); headers.add("wtf/NeverDestroyed.h"); headers.add("wtf/SortedArrayMap.h"); w.line(`String convertEnumerationToString(${name} enumerationValue) {`); w.indent(); w.line(` static const NeverDestroyed values[] = {`); w.indent(); for (const value of type.data) { w.line(` MAKE_STATIC_STRING_IMPL(${str(value)}),`); } w.dedent(); w.line(` };`); w.line(` return values[static_cast(enumerationValue)];`); w.dedent(); w.line(`}`); w.line(); w.line(`template<> JSString* convertEnumerationToJS(JSC::JSGlobalObject& global, ${name} enumerationValue) {`); w.line(` return jsStringWithCache(global.vm(), convertEnumerationToString(enumerationValue));`); w.line(`}`); w.line(); w.line(`template<> std::optional<${name}> parseEnumerationFromString<${name}>(const String& stringValue)`); w.line(`{`); w.line(` static constexpr std::pair mappings[] = {`); for (const value of type.data) { w.line(` { ${str(value)}_s, ${name}::${pascal(value)} },`); } w.line(` };`); w.line(` static constexpr SortedArrayMap enumerationMapping { mappings };`); w.line(` if (auto* enumerationValue = enumerationMapping.tryGet(stringValue); enumerationValue) [[likely]]`); w.line(` return *enumerationValue;`); w.line(` return std::nullopt;`); w.line(`}`); w.line(); w.line( `template<> std::optional<${name}> parseEnumeration<${name}>(JSGlobalObject& lexicalGlobalObject, JSValue value)`, ); w.line(`{`); w.line(` return parseEnumerationFromString<${name}>(value.toWTFString(&lexicalGlobalObject));`); w.line(`}`); w.line(); w.line(`template<> ASCIILiteral expectedEnumerationValues<${name}>()`); w.line(`{`); w.line(` return ${str(type.data.map(value => `${str(value)}`).join(", "))}_s;`); w.line(`}`); w.line(); } function zigTypeName(type: TypeImpl): string { let name = zigTypeNameInner(type); if (type.flags.optional) { name = "?" + name; } return name; } function zigTypeNameInner(type: TypeImpl): string { if (type.lowersToNamedType()) { const namespace = typeHashToNamespace.get(type.hash()); return namespace ? `${namespace}.${type.name()}` : type.name(); } switch (type.kind) { case "USVString": case "DOMString": case "ByteString": case "UTF8String": return "bun.String"; case "boolean": return "bool"; case "usize": return "usize"; case "globalObject": case "zigVirtualMachine": return "*JSC.JSGlobalObject"; default: const cAbiType = type.canDirectlyMapToCAbi(); if (cAbiType) { if (typeof cAbiType === "string") { return cAbiType; } return cAbiType.name(); } throw new Error(`TODO: emitZigTypeName for Type ${type.kind}`); } } function returnStrategyCppType(strategy: ReturnStrategy): string { switch (strategy.type) { case "basic-out-param": case "void": return "bool"; // true=success, false=exception case "jsvalue": return "JSC::EncodedJSValue"; default: throw new Error( `TODO: returnStrategyCppType for ${Bun.inspect(strategy satisfies never, { colors: Bun.enableANSIColors })}`, ); } } function returnStrategyZigType(strategy: ReturnStrategy): string { switch (strategy.type) { case "basic-out-param": case "void": return "bool"; // true=success, false=exception case "jsvalue": return "JSC.JSValue"; default: throw new Error( `TODO: returnStrategyZigType for ${Bun.inspect(strategy satisfies never, { colors: Bun.enableANSIColors })}`, ); } } function emitNullableZigDecoder(w: CodeWriter, prefix: string, type: TypeImpl, children: ArgStrategyChildItem[]) { assert(children.length > 0); const indent = children[0].type !== "c-abi-compatible"; w.add(`if (${prefix}_set)`); if (indent) { w.indent(); } else { w.add(` `); } emitComplexZigDecoder(w, prefix + "_value", type, children); if (indent) { w.line(); w.dedent(); } else { w.add(` `); } w.add(`else`); if (indent) { w.indent(); } else { w.add(` `); } w.add(`null`); if (indent) w.dedent(); } function emitComplexZigDecoder(w: CodeWriter, prefix: string, type: TypeImpl, children: ArgStrategyChildItem[]) { assert(children.length > 0); if (children[0].type === "c-abi-compatible") { w.add(`${prefix}`); return; } switch (type.kind) { default: throw new Error(`TODO: emitComplexZigDecoder for Type ${type.kind}`); } } type DistinguishablePrimitive = "undefined" | "string" | "number" | "boolean" | "object"; type DistinguishStrategy = DistinguishablePrimitive; function typeCanDistinguish(t: TypeImpl[]) { const seen: Record = { undefined: false, string: false, number: false, boolean: false, object: false, }; let strategies: DistinguishStrategy[] = []; for (const type of t) { let primitive: DistinguishablePrimitive | null = null; if (type.kind === "undefined") { primitive = "undefined"; } else if (type.isStringType()) { primitive = "string"; } else if (type.isNumberType()) { primitive = "number"; } else if (type.kind === "boolean") { primitive = "boolean"; } else if (type.isObjectType()) { primitive = "object"; } if (primitive) { if (seen[primitive]) { return null; } seen[primitive] = true; strategies.push(primitive); continue; } return null; // TODO: } return strategies; } /** This is an arbitrary classifier to allow consistent sorting for distinguishing arguments */ function typeDistinguishmentWeight(type: TypeImpl): number { if (type.kind === "undefined") { return 100; } if (type.isObjectType()) { return 10; } if (type.isStringType()) { return 5; } if (type.isNumberType()) { return 3; } if (type.kind === "boolean") { return -1; } return 0; } function getDistinguishCode(strategy: DistinguishStrategy, type: TypeImpl, value: string) { switch (strategy) { case "string": return { condition: `${value}.isString()`, canThrow: false }; case "number": return { condition: `${value}.isNumber()`, canThrow: false }; case "boolean": return { condition: `${value}.isBoolean()`, canThrow: false }; case "object": return { condition: `${value}.isObject()`, canThrow: false }; case "undefined": return { condition: `${value}.isUndefined()`, canThrow: false }; default: throw new Error(`TODO: getDistinguishCode for ${strategy}`); } } /** The variation selector implementation decides which variation dispatch to call. */ function emitCppVariationSelector(fn: Func, namespaceVar: string) { let minRequiredArgs = Infinity; let maxArgs = 0; const variationsByArgumentCount = new Map(); const pushToList = (argCount: number, vari: Variant) => { assert(typeof argCount === "number"); let list = variationsByArgumentCount.get(argCount); if (!list) { list = []; variationsByArgumentCount.set(argCount, list); } list.push(vari); }; for (const vari of fn.variants) { const vmra = vari.minRequiredArgs; minRequiredArgs = Math.min(minRequiredArgs, vmra); maxArgs = Math.max(maxArgs, vari.args.length); const allArgCount = vari.args.filter(arg => !arg.type.isVirtualArgument()).length; pushToList(vmra, vari); if (allArgCount != vmra) { pushToList(allArgCount, vari); } } cpp.line(`auto& vm = JSC::getVM(global);`); cpp.line(`auto throwScope = DECLARE_THROW_SCOPE(vm);`); if (minRequiredArgs > 0) { cpp.line(`size_t argumentCount = std::min(callFrame->argumentCount(), ${maxArgs});`); cpp.line(`if (argumentCount < ${minRequiredArgs}) {`); cpp.line(` return JSC::throwVMError(global, throwScope, createNotEnoughArgumentsError(global));`); cpp.line(`}`); } const sorted = [...variationsByArgumentCount.entries()] .map(([key, value]) => ({ argCount: key, variants: value })) .sort((a, b) => b.argCount - a.argCount); let argCountI = 0; for (const { argCount, variants } of sorted) { argCountI++; const checkArgCount = argCountI < sorted.length && argCount !== minRequiredArgs; if (checkArgCount) { cpp.line(`if (argumentCount >= ${argCount}) {`); cpp.indent(); } if (variants.length === 1) { cpp.line( `RELEASE_AND_RETURN(throwScope, ${extInternalDispatchVariant(namespaceVar, fn.name, variants[0].suffix)}(global, callFrame));`, ); } else { let argIndex = 0; let strategies: DistinguishStrategy[] | null = null; while (argIndex < argCount) { strategies = typeCanDistinguish( variants.map(v => v.args.filter(v => !v.type.isVirtualArgument())[argIndex].type), ); if (strategies) { break; } argIndex++; } if (!strategies) { const err = new Error( `\x1b[0mVariations with ${argCount} required arguments must have at least one argument that can distinguish between them.\n` + `Variations:\n${variants.map(v => ` ${inspect(v.args.filter(a => !a.type.isVirtualArgument()).map(x => x.type))}`).join("\n")}`, ); err.stack = `Error: ${err.message}\n${fn.snapshot}`; throw err; } const getArgument = minRequiredArgs > 0 ? "uncheckedArgument" : "argument"; cpp.line(`JSC::JSValue distinguishingValue = callFrame->${getArgument}(${argIndex});`); const sortedVariants = variants .map((v, i) => ({ variant: v, type: v.args.filter(a => !a.type.isVirtualArgument())[argIndex].type, strategy: strategies[i], })) .sort((a, b) => typeDistinguishmentWeight(a.type) - typeDistinguishmentWeight(b.type)); for (const { variant: v, strategy: s } of sortedVariants) { const arg = v.args[argIndex]; const { condition, canThrow } = getDistinguishCode(s, arg.type, "distinguishingValue"); cpp.line(`if (${condition}) {`); cpp.indent(); cpp.line( `RELEASE_AND_RETURN(throwScope, ${extInternalDispatchVariant(namespaceVar, fn.name, v.suffix)}(global, callFrame));`, ); cpp.dedent(); cpp.line(`}`); if (canThrow) { cpp.line(`RETURN_IF_EXCEPTION(throwScope, {});`); } } } if (checkArgCount) { cpp.dedent(); cpp.line(`}`); } } } // BEGIN MAIN CODE GENERATION // Search for all .bind.ts files const unsortedFiles = readdirRecursiveWithExclusionsAndExtensionsSync(src, ["node_modules", ".git"], [".bind.ts"]); // Sort for deterministic output for (const fileName of [...unsortedFiles].sort()) { const zigFile = path.relative(src, fileName.replace(/\.bind\.ts$/, ".zig")); const zigFilePath = path.join(src, zigFile); let file = files.get(zigFile); if (!fs.existsSync(zigFilePath)) { // It would be nice if this would generate the file with the correct boilerplate const bindName = path.basename(fileName); throw new Error( `${bindName} is missing a corresponding Zig file at ${zigFile}. Please create it and make sure it matches signatures in ${bindName}.`, ); } if (!file) { file = { functions: [], typedefs: [] }; files.set(zigFile, file); } const exports = import.meta.require(fileName); // Mark all exported TypeImpl as reachable for (let [key, value] of Object.entries(exports)) { if (value == null || typeof value !== "object") continue; if (value instanceof TypeImpl) { value.assignName(key); value.markReachable(); file.typedefs.push({ name: key, type: value }); } if (value[isFunc]) { const func = value as Func; func.name = key; } } for (const fn of file.functions) { if (fn.name === "") { const err = new Error(`This function definition needs to be exported`); err.stack = `Error: ${err.message}\n${fn.snapshot}`; throw err; } } } const zig = new CodeWriter(); const zigInternal = new CodeWriter(); // TODO: split each *.bind file into a separate .cpp file const cpp = new CodeWriter(); const cppInternal = new CodeWriter(); const headers = new Set(); zig.line('const bun = @import("bun");'); zig.line("const JSC = bun.JSC;"); zig.line("const JSHostFunctionType = JSC.JSHostFn;\n"); zigInternal.line("const binding_internals = struct {"); zigInternal.indent(); cpp.line("namespace Generated {"); cpp.line(); cppInternal.line('// These "Arguments" definitions are for communication between C++ and Zig.'); cppInternal.line('// Field layout depends on implementation details in "bindgen.ts", and'); cppInternal.line("// is not intended for usage outside generated binding code."); headers.add("root.h"); headers.add("IDLTypes.h"); headers.add("JSDOMBinding.h"); headers.add("JSDOMConvertBase.h"); headers.add("JSDOMConvertBoolean.h"); headers.add("JSDOMConvertNumbers.h"); headers.add("JSDOMConvertStrings.h"); headers.add("JSDOMExceptionHandling.h"); headers.add("JSDOMOperation.h"); /** * Indexed by `zigFile`, values are the generated zig identifier name, without * collisions. */ const fileMap = new Map(); const fileNames = new Set(); for (const [filename, { functions, typedefs }] of files) { const basename = path.basename(filename, ".zig"); let varName = basename; if (fileNames.has(varName)) { throw new Error(`File name collision: ${basename}.zig`); } fileNames.add(varName); fileMap.set(filename, varName); if (functions.length === 0) continue; for (const td of typedefs) { typeHashToNamespace.set(td.type.hash(), varName); } for (const fn of functions) { for (const vari of fn.variants) { for (const arg of vari.args) { arg.type.markReachable(); } } } } let needsWebCore = false; for (const type of typeHashToReachableType.values()) { // Emit convert functions for compound types in the Generated namespace switch (type.kind) { case "dictionary": emitConvertDictionaryFunction(type); break; case "stringEnum": case "zigEnum": needsWebCore = true; break; } } for (const [filename, { functions, typedefs }] of files) { const namespaceVar = fileMap.get(filename)!; assert(namespaceVar, `namespaceVar not found for ${filename}, ${inspect(fileMap)}`); zigInternal.line(`const import_${namespaceVar} = @import(${str(path.relative(src + "/bun.js", filename))});`); zig.line(`/// Generated for "src/${filename}"`); zig.line(`pub const ${namespaceVar} = struct {`); zig.indent(); for (const fn of functions) { cpp.line(`// Dispatch for \"fn ${zid(fn.name)}(...)\" in \"src/${fn.zigFile}\"`); const externName = extJsFunction(namespaceVar, fn.name); // C++ forward declarations let variNum = 1; for (const vari of fn.variants) { resolveVariantStrategies( vari, `${pascal(namespaceVar)}${pascal(fn.name)}Arguments${fn.variants.length > 1 ? variNum : ""}`, ); const dispatchName = extDispatchVariant(namespaceVar, fn.name, variNum); const internalDispatchName = extInternalDispatchVariant(namespaceVar, fn.name, variNum); const args: string[] = []; if (vari.globalObjectArg === "hidden") { args.push("JSC::JSGlobalObject*"); } for (const arg of vari.args) { if (arg.type.isIgnoredUndefinedType()) continue; const strategy = arg.loweringStrategy!; switch (strategy.type) { case "c-abi-pointer": addHeaderForType(arg.type); args.push(`const ${arg.type.cppName()}*`); break; case "c-abi-value": addHeaderForType(arg.type); args.push(arg.type.cppName()); break; case "uses-communication-buffer": break; default: throw new Error(`TODO: C++ dispatch function for ${inspect(strategy)}`); } } const { communicationStruct } = vari; if (communicationStruct) { args.push(`${communicationStruct.name()}*`); } const returnStrategy = vari.returnStrategy!; if (returnStrategy.type === "basic-out-param") { args.push(cAbiTypeName(returnStrategy.abiType) + "*"); } cpp.line(`extern "C" ${returnStrategyCppType(vari.returnStrategy!)} ${dispatchName}(${args.join(", ")});`); if (fn.variants.length > 1) { // Emit separate variant dispatch functions cpp.line( `extern "C" SYSV_ABI JSC::EncodedJSValue ${internalDispatchName}(JSC::JSGlobalObject* global, JSC::CallFrame* callFrame)`, ); cpp.line(`{`); cpp.indent(); cpp.resetTemporaries(); emitCppCallToVariant(fn.name, vari, dispatchName); cpp.dedent(); cpp.line(`}`); } variNum += 1; } // Public function zig.line( `pub const ${zid("js" + cap(fn.name))} = @extern(*const JSHostFunctionType, .{ .name = ${str(externName)} });`, ); // Generated JSC host function cpp.line( `extern "C" SYSV_ABI JSC::EncodedJSValue ${externName}(JSC::JSGlobalObject* global, JSC::CallFrame* callFrame)`, ); cpp.line(`{`); cpp.indent(); cpp.resetTemporaries(); if (fn.variants.length === 1) { emitCppCallToVariant(fn.name, fn.variants[0], extDispatchVariant(namespaceVar, fn.name, 1)); } else { emitCppVariationSelector(fn, namespaceVar); } cpp.dedent(); cpp.line(`}`); cpp.line(); // Generated Zig dispatch functions variNum = 1; for (const vari of fn.variants) { const dispatchName = extDispatchVariant(namespaceVar, fn.name, variNum); const args: string[] = []; const returnStrategy = vari.returnStrategy!; const { communicationStruct } = vari; if (communicationStruct) { zigInternal.add(`const ${communicationStruct.name()} = `); communicationStruct.emitZig(zigInternal, "with-semi"); } assert(vari.globalObjectArg !== undefined); let globalObjectArg = ""; if (vari.globalObjectArg === "hidden") { args.push(`global: *JSC.JSGlobalObject`); globalObjectArg = "global"; } let argNum = 0; for (const arg of vari.args) { if (arg.type.isIgnoredUndefinedType()) continue; let argName = `arg_${snake(arg.name)}`; if (vari.globalObjectArg === argNum) { if (arg.type.kind !== "globalObject") { argName = "global"; } globalObjectArg = argName; } argNum += 1; arg.zigMappedName = argName; const strategy = arg.loweringStrategy!; switch (strategy.type) { case "c-abi-pointer": args.push(`${argName}: *const ${zigTypeName(arg.type)}`); break; case "c-abi-value": args.push(`${argName}: ${zigTypeName(arg.type)}`); break; case "uses-communication-buffer": break; default: throw new Error(`TODO: zig dispatch function for ${inspect(strategy)}`); } } assert(globalObjectArg, `globalObjectArg not found from ${vari.globalObjectArg}`); if (communicationStruct) { args.push(`buf: *${communicationStruct.name()}`); } if (returnStrategy.type === "basic-out-param") { args.push(`out: *${zigTypeName(vari.ret)}`); } zigInternal.line(`export fn ${zid(dispatchName)}(${args.join(", ")}) ${returnStrategyZigType(returnStrategy)} {`); zigInternal.indent(); zigInternal.line( `if (!@hasDecl(import_${namespaceVar}${fn.zigPrefix.length > 0 ? "." + fn.zigPrefix.slice(0, -1) : ""}, ${str(fn.name + vari.suffix)}))`, ); zigInternal.line( ` @compileError(${str(`Missing binding declaration "${fn.zigPrefix}${fn.name + vari.suffix}" in "${path.basename(filename)}"`)});`, ); for (const arg of vari.args) { if (arg.type.kind === "UTF8String") { zigInternal.line(`const ${arg.zigMappedName}_utf8 = ${arg.zigMappedName}.toUTF8(bun.default_allocator);`); zigInternal.line(`defer ${arg.zigMappedName}_utf8.deinit();`); } } switch (returnStrategy.type) { case "jsvalue": zigInternal.add(`return JSC.toJSHostCall(${globalObjectArg}, @src(), `); break; case "basic-out-param": zigInternal.add(`out.* = @as(bun.JSError!${returnStrategy.abiType}, `); break; case "void": zigInternal.add(`@as(bun.JSError!void, `); break; } zigInternal.add(`${zid("import_" + namespaceVar)}.${fn.zigPrefix}${fn.name + vari.suffix}`); if (returnStrategy.type === "jsvalue") { zigInternal.line(", .{"); } else { zigInternal.line("("); } zigInternal.indent(); for (const arg of vari.args) { const argName = arg.zigMappedName!; if (arg.type.isIgnoredUndefinedType()) continue; if (arg.type.isVirtualArgument()) { switch (arg.type.kind) { case "zigVirtualMachine": zigInternal.line(`${argName}.bunVM(),`); break; case "globalObject": zigInternal.line(`${argName},`); break; default: throw new Error("unexpected"); } continue; } const strategy = arg.loweringStrategy!; switch (strategy.type) { case "c-abi-pointer": if (arg.type.kind === "UTF8String") { zigInternal.line(`${argName}_utf8.slice(),`); break; } zigInternal.line(`${argName}.*,`); break; case "c-abi-value": zigInternal.line(`${argName},`); break; case "uses-communication-buffer": const prefix = `buf.${snake(arg.name)}`; const type = arg.type; const isNullable = type.flags.optional && !("default" in type.flags); if (isNullable) emitNullableZigDecoder(zigInternal, prefix, type, strategy.children); else emitComplexZigDecoder(zigInternal, prefix, type, strategy.children); zigInternal.line(`,`); break; default: throw new Error(`TODO: zig dispatch function for ${inspect(strategy satisfies never)}`); } } zigInternal.dedent(); switch (returnStrategy.type) { case "jsvalue": zigInternal.line(`});`); break; case "basic-out-param": case "void": zigInternal.line(`)) catch |err| switch (err) {`); zigInternal.line(` error.JSError => return false,`); zigInternal.line(` error.OutOfMemory => ${globalObjectArg}.throwOutOfMemory() catch return false,`); zigInternal.line(`};`); zigInternal.line(`return true;`); break; } zigInternal.dedent(); zigInternal.line(`}`); variNum += 1; } } if (functions.length > 0) { zig.line(); } for (const fn of functions) { // Wrapper to init JSValue const wrapperName = zid("create" + cap(fn.name) + "Callback"); const minArgCount = fn.variants.reduce((acc, vari) => Math.min(acc, vari.args.length), Number.MAX_SAFE_INTEGER); zig.line(`pub fn ${wrapperName}(global: *JSC.JSGlobalObject) callconv(JSC.conv) JSC.JSValue {`); zig.line( ` return JSC.host_fn.NewRuntimeFunction(global, JSC.ZigString.static(${str(fn.name)}), ${minArgCount}, js${cap(fn.name)}, false, false, null);`, ); zig.line(`}`); } if (typedefs.length > 0) { zig.line(); } for (const td of typedefs) { emitZigStruct(td.type); } zig.dedent(); zig.line(`};`); zig.line(); } cpp.line("} // namespace Generated"); cpp.line(); if (needsWebCore) { cpp.line(`namespace WebCore {`); cpp.line(); for (const [type, reachableType] of typeHashToReachableType) { switch (reachableType.kind) { case "zigEnum": case "stringEnum": emitConvertEnumFunction(cpp, reachableType); break; } } cpp.line(`} // namespace WebCore`); cpp.line(); } zigInternal.dedent(); zigInternal.line("};"); zigInternal.line(); zigInternal.line("comptime {"); zigInternal.line(` if (bun.Environment.export_cpp_apis) {`); zigInternal.line(' for (@typeInfo(binding_internals).@"struct".decls) |decl| {'); zigInternal.line(" _ = &@field(binding_internals, decl.name);"); zigInternal.line(" }"); zigInternal.line(" }"); zigInternal.line("}"); writeIfNotChanged( path.join(codegenRoot, "GeneratedBindings.cpp"), [...headers].map(name => `#include ${str(name)}\n`).join("") + "\n" + cppInternal.buffer + "\n" + cpp.buffer, ); writeIfNotChanged(path.join(src, "bun.js/bindings/GeneratedBindings.zig"), zig.buffer + zigInternal.buffer); // Headers for (const [filename, { functions, typedefs }] of files) { const namespaceVar = fileMap.get(filename)!; const header = new CodeWriter(); const headerIncludes = new Set(); let needsWebCoreNamespace = false; headerIncludes.add("root.h"); header.line(`namespace {`); header.line(); for (const fn of functions) { const externName = extJsFunction(namespaceVar, fn.name); header.line(`extern "C" SYSV_ABI JSC::EncodedJSValue ${externName}(JSC::JSGlobalObject*, JSC::CallFrame*);`); } header.line(); header.line(`} // namespace`); header.line(); header.line(`namespace Generated {`); header.line(); header.line(`/// Generated binding code for src/${filename}`); header.line(`namespace ${namespaceVar} {`); header.line(); for (const td of typedefs) { emitCppStructHeader(header, td.type); switch (td.type.kind) { case "zigEnum": case "stringEnum": case "dictionary": needsWebCoreNamespace = true; break; } } for (const fn of functions) { const externName = extJsFunction(namespaceVar, fn.name); header.line(`constexpr auto* js${cap(fn.name)} = &${externName};`); } header.line(); header.line(`} // namespace ${namespaceVar}`); header.line(); header.line(`} // namespace Generated`); header.line(); if (needsWebCoreNamespace) { header.line(`namespace WebCore {`); header.line(); for (const td of typedefs) { switch (td.type.kind) { case "zigEnum": case "stringEnum": headerIncludes.add("JSDOMConvertEnumeration.h"); const basename = td.type.name(); const name = `Generated::${namespaceVar}::${basename}`; header.line(`// Implement WebCore::IDLEnumeration trait for ${basename}`); header.line(`String convertEnumerationToString(${name});`); header.line(`template<> JSC::JSString* convertEnumerationToJS(JSC::JSGlobalObject&, ${name});`); header.line(`template<> std::optional<${name}> parseEnumerationFromString<${name}>(const String&);`); header.line( `template<> std::optional<${name}> parseEnumeration<${name}>(JSC::JSGlobalObject&, JSC::JSValue);`, ); header.line(`template<> ASCIILiteral expectedEnumerationValues<${name}>();`); header.line(); break; case "dictionary": // TODO: // header.line(`// Implement WebCore::IDLDictionary trait for ${td.type.name()}`); // header.line( // "template<> FetchRequestInit convertDictionary(JSC::JSGlobalObject&, JSC::JSValue);", // ); // header.line(); break; default: } } header.line(`} // namespace WebCore`); } header.buffer = "#pragma once\n" + [...headerIncludes].map(name => `#include ${str(name)}\n`).join("") + "\n" + header.buffer; writeIfNotChanged(path.join(codegenRoot, `Generated${pascal(namespaceVar)}.h`), header.buffer); }