diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index c9516d8de7..a4602912c7 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -62,8 +62,6 @@ pub const JSValue = enum(i64) { ) callconv(.C) void; extern fn JSC__JSValue__forEachPropertyNonIndexed(JSValue0: JSValue, arg1: *JSGlobalObject, arg2: ?*anyopaque, ArgFn3: ?*const fn (*JSGlobalObject, ?*anyopaque, *ZigString, JSValue, bool, bool) callconv(.C) void) void; - extern fn JSC__JSValue__forEachProperty(JSValue0: JSValue, arg1: *JSGlobalObject, arg2: ?*anyopaque, ArgFn3: ?*const fn (*JSGlobalObject, ?*anyopaque, *ZigString, JSValue, bool, bool) callconv(.C) void) void; - extern fn JSC__JSValue__forEachPropertyOrdered(JSValue0: JSValue, arg1: *JSGlobalObject, arg2: ?*anyopaque, ArgFn3: ?*const fn (*JSGlobalObject, ?*anyopaque, *ZigString, JSValue, bool, bool) callconv(.C) void) void; pub fn forEachPropertyNonIndexed( this: JSValue, @@ -80,7 +78,7 @@ pub const JSValue = enum(i64) { ctx: ?*anyopaque, callback: PropertyIteratorFn, ) JSError!void { - return bun.jsc.fromJSHostCallGeneric(globalThis, @src(), JSC__JSValue__forEachProperty, .{ this, globalThis, ctx, callback }); + return bun.cpp.JSC__JSValue__forEachProperty(this, globalThis, ctx, callback); } pub fn forEachPropertyOrdered( @@ -89,7 +87,7 @@ pub const JSValue = enum(i64) { ctx: ?*anyopaque, callback: PropertyIteratorFn, ) JSError!void { - return bun.jsc.fromJSHostCallGeneric(globalThis, @src(), JSC__JSValue__forEachPropertyOrdered, .{ this, globalThis, ctx, callback }); + return bun.cpp.JSC__JSValue__forEachPropertyOrdered(this, globalThis, ctx, callback); } extern fn Bun__JSValue__toNumber(value: JSValue, global: *JSGlobalObject) f64; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index afa0178816..1a588e39c0 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -5870,7 +5870,7 @@ restart: } } -void JSC__JSValue__forEachProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)(JSC::JSGlobalObject* arg0, void* ctx, ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol)) +[[ZIG_EXPORT(check_slow)]] void JSC__JSValue__forEachProperty(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)([[ZIG_NONNULL]] JSC::JSGlobalObject* arg0, void* ctx, [[ZIG_NONNULL]] ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol)) { JSC__JSValue__forEachPropertyImpl(JSValue0, globalObject, arg2, iter); } @@ -5880,7 +5880,7 @@ extern "C" void JSC__JSValue__forEachPropertyNonIndexed(JSC::EncodedJSValue JSVa JSC__JSValue__forEachPropertyImpl(JSValue0, globalObject, arg2, iter); } -void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)(JSC::JSGlobalObject* arg0, void* ctx, ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol)) +[[ZIG_EXPORT(check_slow)]] void JSC__JSValue__forEachPropertyOrdered(JSC::EncodedJSValue JSValue0, JSC::JSGlobalObject* globalObject, void* arg2, void (*iter)([[ZIG_NONNULL]] JSC::JSGlobalObject* arg0, void* ctx, [[ZIG_NONNULL]] ZigString* arg2, JSC::EncodedJSValue JSValue3, bool isSymbol, bool isPrivateSymbol)) { JSC::JSValue value = JSC::JSValue::decode(JSValue0); JSC::JSObject* object = value.getObject(); diff --git a/src/bun.js/bindings/root.h b/src/bun.js/bindings/root.h index 8894cfe38a..93e70d3859 100644 --- a/src/bun.js/bindings/root.h +++ b/src/bun.js/bindings/root.h @@ -100,5 +100,6 @@ // can be nothrow | zero_is_throw | check_slow #define ZIG_EXPORT(...) +#define ZIG_NONNULL #endif diff --git a/src/codegen/cppbind.ts b/src/codegen/cppbind.ts index 91237006b4..2ed5ba1fc5 100644 --- a/src/codegen/cppbind.ts +++ b/src/codegen/cppbind.ts @@ -1,8 +1,87 @@ -import { SyntaxNode } from "@lezer/common"; -import { parser as cppParser } from "@lezer/cpp"; -import { mkdir } from "fs/promises"; -import { join, relative } from "path"; -import { bannedTypes, sharedTypes, typeDeclarations } from "./shared-types"; +/* + +cppbind - C++ to Zig binding generator for Bun + +This tool automatically generates Zig bindings for C++ functions marked with [[ZIG_EXPORT(...)]] attributes. +It runs automatically when C++ files change during the build process. + +To run manually: + bun src/codegen/cppbind src build/debug/codegen + +## USAGE + +### Basic Export Tags + +1. **nothrow** - Function that never throws exceptions: + ```cpp + [[ZIG_EXPORT(nothrow)]] void hello_world() { + printf("hello world\n"); + } + ``` + Zig usage: `bun.cpp.hello_world();` + +2. **zero_is_throw** - Function returns JSValue, where .zero indicates an exception: + ```cpp + [[ZIG_EXPORT(zero_is_throw)]] JSValue create_object(JSGlobalObject* globalThis) { + auto scope = DECLARE_THROW_SCOPE(); + // ... + RETURN_IF_EXCEPTION(scope, {}); + return result; + } + ``` + Zig usage: `try bun.cpp.create_object(globalThis);` + +3. **check_slow** - Function that may throw, performs runtime exception checking: + ```cpp + [[ZIG_EXPORT(check_slow)]] void process_data(JSGlobalObject* globalThis) { + auto scope = DECLARE_THROW_SCOPE(); + // ... + RETURN_IF_EXCEPTION(scope, ); + } + ``` + Zig usage: `try bun.cpp.process_data(globalThis);` + +### Parameters + +- **[[ZIG_NONNULL]]** - Mark pointer parameters as non-nullable: + ```cpp + [[ZIG_EXPORT(nothrow)]] void process([[ZIG_NONNULL]] JSGlobalObject* globalThis, + [[ZIG_NONNULL]] JSValue* values, + size_t count) { ... } + ``` + Generates: `pub extern fn process(globalThis: *jsc.JSGlobalObject, values: [*]const jsc.JSValue) void;` + +*/ + +const start = Date.now(); +let isInstalled = false; +try { + const grammarfile = await Bun.file("node_modules/@lezer/cpp/src/cpp.grammar").text(); + isInstalled = true; +} catch (e) {} +if (!isInstalled) { + if (process.argv.includes("--already-installed")) { + console.error("Lezer C++ grammar is not installed. Please run `bun install` to install it."); + process.exit(1); + } + const r = Bun.spawnSync([process.argv[0], "install", "--frozen-lockfile"], { + stdio: ["ignore", "pipe", "pipe"], + }); + if (r.exitCode !== 0) { + console.error(r.stdout.toString()); + console.error(r.stderr.toString()); + process.exit(r.exitCode ?? 1); + } + + const r2 = Bun.spawnSync([...process.argv, "--already-installed"], { stdio: ["inherit", "inherit", "inherit"] }); + process.exit(r2.exitCode ?? 1); +} + +type SyntaxNode = import("@lezer/common").SyntaxNode; +const { parser: cppParser } = await import("@lezer/cpp"); +const { mkdir } = await import("fs/promises"); +const { join, relative } = await import("path"); +const { bannedTypes, sharedTypes, typeDeclarations } = await import("./shared-types"); type Point = { line: number; @@ -31,11 +110,18 @@ type CppType = position: Srcloc; isConst: boolean; isMany: boolean; + isNonNull: boolean; } | { type: "named"; name: string; position: Srcloc; + } + | { + type: "fn"; + parameters: CppParameter[]; + returnType: CppType; + position: Srcloc; }; type PositionedError = { @@ -227,15 +313,41 @@ function processDeclarator( // Recursively peel off pointers if (declarator?.name === "PointerDeclarator") { if (!rootmostType) throwError(nodePosition(declarator, ctx), "no rootmost type provided to PointerDeclarator"); - const isConst = !!declarator.parent?.getChild("const"); + const isConst = !!declarator.parent?.getChild("const") || rootmostType.type === "fn"; + const parentAttributes = declarator.parent?.getChildren("Attribute") ?? []; + const isNonNull = parentAttributes.some(attr => text(attr.getChild("AttributeName")!, ctx) === "ZIG_NONNULL"); return processDeclarator(ctx, declarator, { type: "pointer", child: rootmostType, position: nodePosition(declarator, ctx), isConst, + isNonNull, isMany: false, }); + } else if (declarator?.name === "ReferenceDeclarator") { + throwError(nodePosition(declarator, ctx), "references are not allowed"); + } else if (declarator?.name === "FunctionDeclarator" && !declarator.getChild("Identifier")) { + const lhs = declarator.getChild("ParenthesizedDeclarator"); + const rhs = declarator.getChild("ParameterList"); + if (!lhs || !rhs) { + throwError( + nodePosition(declarator, ctx), + "FunctionDeclarator has neither Identifier nor ParenthesizedDeclarator:\n" + + prettyPrintLezerNode(declarator, ctx.sourceCode), + ); + } + const fnType: CppType = { + type: "fn", + parameters: [], + returnType: rootmostType, + position: nodePosition(declarator, ctx), + }; + for (const arg of rhs.getChildren("ParameterDeclaration")) { + const paramDeclarator = processDeclarator(ctx, arg); + fnType.parameters.push({ type: paramDeclarator.type, name: text(paramDeclarator.final, ctx) }); + } + return processDeclarator(ctx, lhs, fnType); } return { type: rootmostType, final: declarator }; @@ -260,9 +372,6 @@ function processFunction(ctx: ParseContext, node: SyntaxNode, tag: ExportTag): C const paramDeclarator = processDeclarator(ctx, parameter); const name = paramDeclarator.final; - if (name.name === "ReferenceDeclarator") { - throwError(nodePosition(name, ctx), "references are not allowed"); - } if (name.name !== "Identifier") { throwError(nodePosition(name, ctx), "parameter name is not an identifier: " + name.name); } @@ -305,16 +414,20 @@ for (const line of sharedTypesLines) { } const errorsForTypes: Map = new Map(); -function generateZigType(type: CppType, subLevel?: boolean) { +function generateZigType(type: CppType, parent: CppType | null) { if (type.type === "pointer") { - if (type.isMany && type.isConst) return `?[*]const ${generateZigType(type.child, true)}`; - if (type.isMany) return `?[*]${generateZigType(type.child, true)}`; - if (type.isConst) return `?*const ${generateZigType(type.child, true)}`; - return `?*${generateZigType(type.child, true)}`; + const optionalChar = type.isNonNull ? "" : "?"; + const ptrChar = type.isMany ? "[*]" : "*"; + const constChar = type.isConst ? "const " : ""; + return `${optionalChar}${ptrChar}${constChar}${generateZigType(type.child, type)}`; + } + if (type.type === "fn") { + return `fn(${type.parameters.map(p => formatZigName(p.name) + ": " + generateZigType(p.type, null)).join(", ")}) callconv(.C) ${generateZigType(type.returnType, null)}`; } if (type.type === "named" && type.name === "void") { - if (subLevel) return "anyopaque"; - return "void"; + if (parent?.type === "pointer") return "anyopaque"; + if (!parent) return "void"; + throwError(type.position, "void must have a pointer parent or no parent"); } if (type.type === "named") { const bannedType = bannedTypes[type.name]; @@ -350,7 +463,7 @@ function generateZigParameterList(parameters: CppParameter[], globalThisArg?: Cp if (p === globalThisArg) { return `${formatZigName(p.name)}: *jsc.JSGlobalObject`; } else { - return `${formatZigName(p.name)}: ${generateZigType(p.type, false)}`; + return `${formatZigName(p.name)}: ${generateZigType(p.type, null)}`; } }) .join(", "); @@ -526,7 +639,7 @@ function generateZigFn( resultSourceLinks: string[], cfg: Cfg, ): void { - const returnType = generateZigType(fn.returnType); + const returnType = generateZigType(fn.returnType, null); if (resultBindings.length) resultBindings.push(""); resultBindings.push(generateZigSourceComment(cfg, resultSourceLinks, fn)); if (fn.tag === "nothrow") { @@ -539,7 +652,7 @@ function generateZigFn( resultRaw.push(` extern fn ${formatZigName(fn.name)}(${generateZigParameterList(fn.parameters)}) ${returnType};`); let globalThisArg: CppParameter | undefined; for (const param of fn.parameters) { - const type = generateZigType(param.type); + const type = generateZigType(param.type, null); if (type === "?*jsc.JSGlobalObject") { globalThisArg = param; break; @@ -619,6 +732,18 @@ async function main() { const rootDir = args[0]; const dstDir = args[1]; if (!rootDir || !dstDir) { + console.error( + String.raw` + _ _ _ + | | (_) | | + ___ _ __ _ __ | |__ _ _ __ __| | + / __| '_ \| '_ \| '_ \| | '_ \ / _' | + | (__| |_) | |_) | |_) | | | | | (_| | + \___| .__/| .__/|_.__/|_|_| |_|\__,_| + | | | | + |_| |_| +`.slice(1), + ); console.error("Usage: bun src/codegen/cppbind "); process.exit(1); } @@ -676,14 +801,17 @@ async function main() { } const now = Date.now(); - const sin = Math.round(((Math.sin((now / 1000) * 1) + 1) / 2) * 24); + const sin = Math.round(((Math.sin((now / 1000) * 1) + 1) / 2) * 0); console.log( " ".repeat(sin) + (errors.length > 0 ? "✗" : "✓") + " cppbind.ts generated bindings to " + resultFilePath + - (errors.length > 0 ? " with errors" : ""), + (errors.length > 0 ? " with errors" : "") + + " in " + + (now - start) + + "ms", ); if (errors.length > 0) { process.exit(1);