Files
bun.sh/src/codegen/cppbind.ts

830 lines
28 KiB
TypeScript

/*
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
extern "C" [[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
extern "C" [[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
extern "C" [[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;
column: number;
};
type Srcloc = {
file: string;
start: Point;
end: Point;
};
type CppFn = {
name: string;
returnType: CppType;
parameters: CppParameter[];
position: Srcloc;
tag: ExportTag;
};
type CppParameter = {
type: CppType;
name: string;
};
type CppType =
| {
type: "pointer";
child: 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 = {
position: Srcloc;
message: string;
notes: { position: Srcloc; message: string }[];
};
const errors: PositionedError[] = [];
function appendError(position: Srcloc, message: string): PositionedError {
const error: PositionedError = { position, message, notes: [] };
errors.push(error);
return error;
}
function appendErrorFromCatch(error: unknown, position: Srcloc): PositionedError {
if (error instanceof PositionedErrorClass) {
errors.push(error);
return error;
}
if (error instanceof Error) {
return appendError(position, error.message);
}
return appendError(position, "unknown error: " + JSON.stringify(error));
}
function throwError(position: Srcloc, message: string): never {
throw new PositionedErrorClass(position, message);
}
class PositionedErrorClass extends Error {
notes: { position: Srcloc; message: string }[] = [];
constructor(
public position: Srcloc,
message: string,
) {
super(message);
}
}
// Lezer works with offsets, but our errors need line/column. This utility handles the conversion.
class LineInfo {
private lineStarts: number[];
constructor(private source: string) {
this.lineStarts = [0];
for (let i = 0; i < source.length; i++) {
if (source[i] === "\n") {
this.lineStarts.push(i + 1);
}
}
}
get(offset: number): Point {
// A binary search would be faster, but this is fine for files of this size.
let line = 1;
let lineStart = 0;
for (let i = this.lineStarts.length - 1; i >= 0; i--) {
if (this.lineStarts[i] <= offset) {
line = i + 1;
lineStart = this.lineStarts[i];
break;
}
}
const column = offset - lineStart + 1;
return { line, column };
}
}
// A context object to pass around file-specific parsing information.
type ParseContext = {
file: string;
sourceCode: string;
lineInfo: LineInfo;
};
function nodePosition(node: SyntaxNode, ctx: ParseContext): Srcloc {
return {
file: ctx.file,
start: ctx.lineInfo.get(node.from),
end: ctx.lineInfo.get(node.to),
};
}
const text = (node: SyntaxNode, ctx: ParseContext) => ctx.sourceCode.slice(node.from, node.to);
function assertNever(value: never): never {
throw new Error("assertNever");
}
export function prettyPrintLezerNode(node: SyntaxNode, sourceCode: string): string {
const lines: string[] = [];
const printRecursive = (currentNode: SyntaxNode, prefix: string, isLast: boolean) => {
// Determine the connector shape
const connector = isLast ? "└─ " : "├─ ";
const linePrefix = prefix + connector;
// Get the node's text, escape newlines, and truncate for readability
const nodeText = sourceCode.slice(currentNode.from, currentNode.to);
let truncatedText = nodeText.replace(/\n/g, "\\n");
if (truncatedText.length > 50) {
truncatedText = truncatedText.slice(0, 50) + "...";
}
// Format and add the current node's line
lines.push(`${linePrefix}${currentNode.name} [${currentNode.from}..${currentNode.to}] "${truncatedText}"`);
if (currentNode.name === "CompoundStatement") {
lines.push(prefix + " └─ ...");
return;
}
// Prepare the prefix for the children
const childPrefix = prefix + (isLast ? " " : "│ ");
// Recurse for children
const children: SyntaxNode[] = [];
const cursor = currentNode.cursor();
if (cursor.firstChild()) {
do {
children.push(cursor.node);
} while (cursor.nextSibling());
}
children.forEach((child, index) => {
printRecursive(child, childPrefix, index === children.length - 1);
});
};
// Start the process for the root node without any prefix/connector
const rootText = sourceCode.slice(node.from, node.to).replace(/\n/g, "\\n").slice(0, 50);
lines.push(`${node.name} [${node.from}..${node.to}] "${rootText}${rootText.length === 50 ? "..." : ""}"`);
const children: SyntaxNode[] = [];
const cursor = node.cursor();
if (cursor.firstChild()) {
do {
children.push(cursor.node);
} while (cursor.nextSibling());
}
children.forEach((child, index) => {
printRecursive(child, "", index === children.length - 1);
});
return lines.join("\n");
}
function getChildren(node: SyntaxNode): SyntaxNode[] {
const children: SyntaxNode[] = [];
let child = node.firstChild;
while (child) {
children.push(child);
child = child.nextSibling;
}
return children;
}
const allowedLezerTypes = new Set(["PrimitiveType", "ScopedTypeIdentifier", "TypeIdentifier", "SizedTypeSpecifier"]);
function processRootmostType(ctx: ParseContext, node: SyntaxNode): CppType {
const children = getChildren(node);
for (const child of children) {
if (allowedLezerTypes.has(child.type.name)) {
return { type: "named", name: text(child, ctx), position: nodePosition(child, ctx) };
}
}
throwError(nodePosition(node, ctx), "no valid type found:\n" + prettyPrintLezerNode(node, ctx.sourceCode));
}
function processDeclarator(
ctx: ParseContext,
node: SyntaxNode, // Initially a FunctionDefinition/ParameterDeclaration, then recursively a Declarator variant
rootmostType?: CppType,
): { type: CppType; final: SyntaxNode } {
// Initial entry point with a definition/declaration, find the top-level declarator
if (node.name === "FunctionDefinition" || node.name === "ParameterDeclaration") {
rootmostType ??= processRootmostType(ctx, node);
} else {
if (!rootmostType)
throwError(
nodePosition(node, ctx),
"no rootmost type provided to declarator:\n" + prettyPrintLezerNode(node, ctx.sourceCode),
);
}
const children = getChildren(node);
const declarators = children.filter(child => child.name.endsWith("Declarator") || child.name === "Identifier");
if (declarators.length !== 1) {
throwError(
nodePosition(node, ctx),
"no or multiple declarators found:\n" + prettyPrintLezerNode(node, ctx.sourceCode),
);
}
const declarator = declarators[0]!;
// 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") || 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 };
}
function processFunction(ctx: ParseContext, node: SyntaxNode, tag: ExportTag): CppFn {
// `node` is a FunctionDefinition
const declarator = processDeclarator(ctx, node);
const final = declarator.final;
if (final.name !== "FunctionDeclarator") {
throwError(nodePosition(final, ctx), "not a function_declarator: " + final.name);
}
const nameNode = final.getChild("Identifier");
if (!nameNode) throwError(nodePosition(final, ctx), "no name found:\n" + prettyPrintLezerNode(final, ctx.sourceCode));
const parameterList = final.getChild("ParameterList");
if (!parameterList) throwError(nodePosition(final, ctx), "no parameter list found");
const parameters: CppParameter[] = [];
for (const parameter of parameterList.getChildren("ParameterDeclaration")) {
const paramDeclarator = processDeclarator(ctx, parameter);
const name = paramDeclarator.final;
if (name.name !== "Identifier") {
throwError(nodePosition(name, ctx), "parameter name is not an identifier: " + name.name);
}
parameters.push({ type: paramDeclarator.type, name: text(name, ctx) });
}
for (let i = 0; i < parameters.length; i++) {
const param = parameters[i];
const next = parameters[i + 1];
if (param.type.type === "pointer" && next?.type.type === "named" && next.type.name === "size_t") {
param.type.isMany = true;
i++;
}
}
return {
returnType: declarator.type,
name: text(nameNode, ctx),
parameters,
position: nodePosition(nameNode, ctx),
tag,
};
}
type ExportTag = "check_slow" | "zero_is_throw" | "false_is_throw" | "null_is_throw" | "nothrow";
const sharedTypesText = await Bun.file("src/codegen/shared-types.ts").text();
const sharedTypesLines = sharedTypesText.split("\n");
let sharedTypesLine = 0;
let sharedTypesColumn = 0;
let sharedTypesColumnEnd = 0;
for (const line of sharedTypesLines) {
sharedTypesLine++;
if (line.includes("export const sharedTypes")) {
sharedTypesColumn = line.indexOf("sharedTypes") + 1;
sharedTypesColumnEnd = sharedTypesColumn + "sharedTypes".length;
break;
}
}
const errorsForTypes: Map<string, PositionedError> = new Map();
function generateZigType(type: CppType, parent: CppType | null) {
if (type.type === "pointer") {
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 (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];
if (bannedType) {
appendError(type.position, bannedType);
return "anyopaque";
}
const sharedType = sharedTypes[type.name];
if (sharedType) return sharedType;
const error = errorsForTypes.has(type.name)
? errorsForTypes.get(type.name)!
: appendError(
{
file: "src/codegen/shared-types.ts",
start: { line: sharedTypesLine, column: sharedTypesColumn },
end: { line: sharedTypesLine, column: sharedTypesColumnEnd },
},
"sharedTypes is missing type: " + JSON.stringify(type.name),
);
errorsForTypes.set(type.name, error);
error.notes.push({ position: type.position, message: "used in exported function here" });
return "anyopaque";
}
assertNever(type);
}
function formatZigName(name: string): string {
if (name.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) return name;
return "@" + JSON.stringify(name);
}
function generateZigParameterList(parameters: CppParameter[], globalThisArg?: CppParameter): string {
return parameters
.map(p => {
if (p === globalThisArg) {
return `${formatZigName(p.name)}: *jsc.JSGlobalObject`;
} else {
return `${formatZigName(p.name)}: ${generateZigType(p.type, null)}`;
}
})
.join(", ");
}
function generateZigSourceComment(cfg: Cfg, resultSourceLinks: string[], fn: CppFn): string {
const fileName = relative(cfg.dstDir, fn.position.file);
resultSourceLinks.push(`${fn.name}:${fileName}:${fn.position.start.line}:${fn.position.start.column}`);
return `/// Source: ${fn.name}`;
}
function closest(node: SyntaxNode | null, type: string): SyntaxNode | null {
while (node) {
if (node.name === type) return node;
node = node.parent;
}
return null;
}
type CppParser = typeof cppParser;
async function processFile(parser: CppParser, file: string, allFunctions: CppFn[]) {
const sourceCode = await Bun.file(file).text();
if (!sourceCode.includes("[[ZIG_EXPORT(")) return;
const sourceCodeLines = sourceCode.split("\n");
const manualFindLines = new Set<number>();
for (let i = 0; i < sourceCodeLines.length; i++) {
if (sourceCodeLines[i].includes("[[ZIG_EXPORT(")) {
manualFindLines.add(i + 1);
}
}
const tree = parser.parse(sourceCode);
const lineInfo = new LineInfo(sourceCode);
const ctx: ParseContext = { file, sourceCode, lineInfo };
if (!tree) {
appendError({ file, start: { line: 0, column: 0 }, end: { line: 0, column: 0 } }, "no tree found");
for (const lineNumber of manualFindLines) {
const lineContent = sourceCodeLines[lineNumber - 1];
const column = lineContent.indexOf("[[ZIG_EXPORT(") + 3;
appendError(
{
file,
start: { line: lineNumber, column },
end: { line: lineNumber, column: column + "ZIG_EXPORT(".length },
},
"ZIG_EXPORT found, but Lezer failed to parse the file.",
);
}
return;
}
const queryFoundLines = new Set<number>();
tree.iterate({
enter: nodeRef => {
if (nodeRef.name !== "FunctionDefinition") {
return true; // Continue traversal
}
// console.log(
// `\n--- Found ZIG_EXPORT on function in ${file} at line ${lineInfo.get(nodeRef.node.from).line} ---\n`,
// );
// // Use the new pretty-printer to log the tree structure of the matched function
// console.log(prettyPrintLezerNode(nodeRef.node, ctx.sourceCode));
// console.log(`-------------------------------------------------------------------\n`);
const fnNode = nodeRef.node;
let zigExportAttr: SyntaxNode | null = null;
let tagIdentifier: SyntaxNode | null = null;
for (const attr of fnNode.getChildren("Attribute")) {
const attrNameNode = attr.getChild("AttributeName");
if (attrNameNode && text(attrNameNode, ctx) === "ZIG_EXPORT") {
zigExportAttr = attr;
const args = attr.getChild("AttributeArgs");
if (args) {
tagIdentifier = args.getChild("Identifier");
}
break;
}
}
if (!zigExportAttr || !tagIdentifier) {
return false; // Not an exported function, prune search
}
queryFoundLines.add(lineInfo.get(zigExportAttr.from).line);
// disabled because lezer parses (extern "C") seperately to the function definition / block
/* const linkage = closest(fnNode, "LinkageSpecification");
const linkageString = linkage?.getChild("String");
if (!linkage || !linkageString || text(linkageString, ctx) !== '"C"') {
appendError(
nodePosition(fnNode, ctx),
'exported function must be extern "C":\n' +
(linkage ? prettyPrintLezerNode(linkage, ctx.sourceCode) : "no linkage"),
);
} */
const tagStr = text(tagIdentifier, ctx);
let tag: ExportTag | undefined;
if (
tagStr === "nothrow" ||
tagStr === "zero_is_throw" ||
tagStr === "check_slow" ||
tagStr === "false_is_throw" ||
tagStr === "null_is_throw"
) {
tag = tagStr;
} else if (tagStr === "print") {
console.log(prettyPrintLezerNode(fnNode, ctx.sourceCode));
appendError(nodePosition(tagIdentifier, ctx), "'print' tags are only for debugging cppbind");
tag = "nothrow";
} else {
appendError(
nodePosition(tagIdentifier, ctx),
"tag must be nothrow, zero_is_throw, check_slow, false_is_throw, or null_is_throw: " + tagStr,
);
tag = "nothrow";
}
try {
const result = processFunction(ctx, fnNode, tag);
allFunctions.push(result);
} catch (e) {
appendErrorFromCatch(e, nodePosition(fnNode, ctx));
}
return false; // Don't descend into function body
},
});
for (const lineNumber of manualFindLines) {
if (!queryFoundLines.has(lineNumber)) {
const lineContent = sourceCodeLines[lineNumber - 1];
const column = lineContent.indexOf("[[ZIG_EXPORT(") + 3;
const position: Srcloc = {
file,
start: { line: lineNumber, column },
end: { line: lineNumber, column: column + "ZIG_EXPORT(".length },
};
appendError(
position,
"ZIG_EXPORT was found on this line, but the Lezer parser did not find a valid C++ attribute on a function definition. Ensure it's in the form `[[ZIG_EXPORT(tag)]]` before a function definition.",
);
}
}
}
async function renderError(position: Srcloc, message: string, label: string, color: string) {
const fileContent = await Bun.file(position.file).text();
const lines = fileContent.split("\n");
const line = lines[position.start.line - 1];
if (line === undefined) return;
console.error(
`\x1b[m${position.file}:${position.start.line}:${position.start.column}: ${color}\x1b[1m${label}:\x1b[m ${message}`,
);
const before = `${position.start.line} | ${line.substring(0, position.start.column - 1)}`;
const after = line.substring(position.start.column - 1);
console.error(`\x1b[90m${before}${after}\x1b[m`);
let length = position.start.line === position.end.line ? position.end.column - position.start.column : 1;
console.error(`\x1b[m${" ".repeat(Bun.stringWidth(before))}${color}^${"~".repeat(Math.max(length - 1, 0))}\x1b[m`);
}
type Cfg = {
dstDir: string;
};
function generateZigFn(
fn: CppFn,
resultRaw: string[],
resultBindings: string[],
resultSourceLinks: string[],
cfg: Cfg,
): void {
let returnType = generateZigType(fn.returnType, null);
if (resultBindings.length) resultBindings.push("");
resultBindings.push(generateZigSourceComment(cfg, resultSourceLinks, fn));
if (fn.tag === "nothrow") {
resultBindings.push(
`pub extern fn ${formatZigName(fn.name)}(${generateZigParameterList(fn.parameters)}) ${returnType};`,
);
return;
}
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, null);
if (type === "?*jsc.JSGlobalObject") {
globalThisArg = param;
break;
}
}
if (!globalThisArg) throwError(fn.position, "no globalThis argument found (required for " + fn.tag + ")");
if (fn.tag === "check_slow") {
if (returnType === "jsc.JSValue") {
appendError(
fn.position,
"Use ZIG_EXPORT(zero_is_throw) instead of ZIG_EXPORT(check_slow) for functions that return JSValue",
);
}
resultBindings.push(
`pub fn ${formatZigName(fn.name)}(${generateZigParameterList(fn.parameters, globalThisArg)}) error{JSError}!${returnType} {`,
` if (comptime Environment.ci_assert) {`,
` var scope: jsc.CatchScope = undefined;`,
` scope.init(${formatZigName(globalThisArg.name)}, @src());`,
` defer scope.deinit();`,
``,
` const result = raw.${formatZigName(fn.name)}(${fn.parameters.map(p => formatZigName(p.name)).join(", ")});`,
` try scope.returnIfException();`,
` return result;`,
` } else {`,
` const result = raw.${formatZigName(fn.name)}(${fn.parameters.map(p => formatZigName(p.name)).join(", ")});`,
` if (Bun__RETURN_IF_EXCEPTION(${formatZigName(globalThisArg.name)})) return error.JSError;`,
` return result;`,
` }`,
`}`,
);
return;
}
let equalsValue: string;
if (fn.tag === "zero_is_throw") {
equalsValue = ".zero";
if (returnType !== "jsc.JSValue") {
appendError(fn.position, "ZIG_EXPORT(zero_is_throw) is only allowed for functions that return JSValue");
}
} else if (fn.tag === "false_is_throw") {
equalsValue = "false";
if (returnType !== "bool") {
appendError(fn.position, "ZIG_EXPORT(false_is_throw) is only allowed for functions that return bool");
}
returnType = "void";
} else if (fn.tag === "null_is_throw") {
equalsValue = "null";
if (!returnType.startsWith("?*")) {
appendError(fn.position, "ZIG_EXPORT(null_is_throw) is only allowed for functions that return optional pointer");
}
returnType = returnType.slice(1);
} else assertNever(fn.tag);
resultBindings.push(
`pub fn ${formatZigName(fn.name)}(${generateZigParameterList(fn.parameters, globalThisArg)}) error{JSError}!${returnType} {`,
` if (comptime Environment.ci_assert) {`,
` var scope: jsc.ExceptionValidationScope = undefined;`,
` scope.init(${formatZigName(globalThisArg.name)}, @src());`,
` defer scope.deinit();`,
``,
` const value = raw.${formatZigName(fn.name)}(${fn.parameters.map(p => formatZigName(p.name)).join(", ")});`,
` scope.assertExceptionPresenceMatches(value == ${equalsValue});`,
` return if (value == ${equalsValue}) error.JSError ${fn.tag === "false_is_throw" ? "" : "else value"}${fn.tag === "null_is_throw" ? ".?" : ""};`,
` } else {`,
` const value = raw.${formatZigName(fn.name)}(${fn.parameters.map(p => formatZigName(p.name)).join(", ")});`,
` if (value == ${equalsValue}) return error.JSError;`,
...(fn.tag === "false_is_throw" ? [] : [` return value${fn.tag === "null_is_throw" ? ".?" : ""};`]),
` }`,
`}`,
);
return;
}
async function readFileOrEmpty(file: string): Promise<string> {
try {
const fileContents = await Bun.file(file).text();
return fileContents;
} catch (e) {
return "";
}
}
async function main() {
const args = process.argv.slice(2);
const dstDir = args[1];
if (!dstDir) {
console.error(
String.raw`
_ _ _
| | (_) | |
___ _ __ _ __ | |__ _ _ __ __| |
/ __| '_ \| '_ \| '_ \| | '_ \ / _' |
| (__| |_) | |_) | |_) | | | | | (_| |
\___| .__/| .__/|_.__/|_|_| |_|\__,_|
| | | |
|_| |_|
`.slice(1),
);
console.error("Usage: bun src/codegen/cppbind src build/debug/codegen");
process.exit(1);
}
await mkdir(dstDir, { recursive: true });
const parser = cppParser;
const allCppFiles = (await Bun.file("cmake/sources/CxxSources.txt").text())
.trim()
.split("\n")
.map(q => q.trim())
.filter(q => !!q)
.filter(q => !q.startsWith("#"));
const allFunctions: CppFn[] = [];
await Promise.all(allCppFiles.map(file => processFile(parser, file, allFunctions)));
allFunctions.sort((a, b) => (a.position.file < b.position.file ? -1 : a.position.file > b.position.file ? 1 : 0));
const resultRaw: string[] = [];
const resultBindings: string[] = [];
const resultSourceLinks: string[] = [];
for (const fn of allFunctions) {
try {
generateZigFn(fn, resultRaw, resultBindings, resultSourceLinks, { dstDir });
} catch (e) {
appendErrorFromCatch(e, fn.position);
}
}
for (const message of errors) {
await renderError(message.position, message.message, "error", "\x1b[31m");
for (const note of message.notes) {
await renderError(note.position, note.message, "note", "\x1b[36m");
}
console.error();
}
const resultFilePath = join(dstDir, "cpp.zig");
const resultContents =
typeDeclarations +
"\n" +
resultBindings.join("\n") +
"\n\nconst raw = struct {\n" +
resultRaw.join("\n") +
"\n};\n";
if ((await readFileOrEmpty(resultFilePath)) !== resultContents) {
await Bun.write(resultFilePath, resultContents);
}
const resultSourceLinksFilePath = join(dstDir, "cpp.source-links");
const resultSourceLinksContents = resultSourceLinks.join("\n");
if ((await readFileOrEmpty(resultSourceLinksFilePath)) !== resultSourceLinksContents) {
await Bun.write(resultSourceLinksFilePath, resultSourceLinksContents);
const now = Date.now();
const sin = Math.round(((Math.sin((now / 1000) * 1) + 1) / 2) * 0);
if (process.env.CI) {
console.log(
" ".repeat(sin) +
(errors.length > 0 ? "✗" : "✓") +
" cppbind.ts generated bindings to " +
resultFilePath +
(errors.length > 0 ? " with errors" : "") +
" in " +
(now - start) +
"ms",
);
}
}
if (errors.length > 0) {
process.exit(1);
}
}
// Run the main function
await main();