Files
bun.sh/src/codegen/generate-js2native.ts
taylor.fish 07cd45deae Refactor Zig imports and file structure (part 1) (#21270)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-07-22 17:51:38 -07:00

264 lines
8.0 KiB
TypeScript

// This file implements the global state for $zig and $cpp preprocessor macros
// as well as all the code it generates.
//
// For the actual parsing, see replacements.ts
import path, { basename, sep } from "path";
import { cap, readdirRecursiveWithExclusionsAndExtensionsSync } from "./helpers";
//
interface NativeCall {
id: number;
type: NativeCallType;
filename: string;
symbol: string;
is_wrapped: boolean;
}
interface WrapperCall {
type: NativeCallType;
wrap_kind: "new-function";
symbol_target: string;
symbol_generated: string;
display_name: string;
call_length: number;
filename: string;
}
type NativeCallType = "zig" | "cpp" | "bind";
const nativeCalls: NativeCall[] = [];
const wrapperCalls: WrapperCall[] = [];
const sourceFiles = readdirRecursiveWithExclusionsAndExtensionsSync(
path.join(import.meta.dir, "../"),
["deps", "node_modules", "WebKit"],
[".cpp", ".zig", ".bind.ts"],
);
function callBaseName(x: string) {
return x.split(/[^A-Za-z0-9]/g).pop()!;
}
function resolveNativeFileId(call_type: NativeCallType, filename: string) {
const ext = call_type === "bind" ? ".bind.ts" : `.${call_type}`;
if (!filename.endsWith(ext)) {
throw new Error(`Expected filename for $${call_type} to have ${ext} extension, got ${JSON.stringify(filename)}`);
}
filename = filename.replaceAll("/", sep);
const resolved = sourceFiles.find(file => file.endsWith(sep + filename));
if (!resolved) {
const fnName = call_type === "bind" ? "bindgenFn" : call_type;
throw new Error(`Could not find file ${filename} in $${fnName} call`);
}
if (call_type === "zig") {
return resolved;
}
return filename;
}
export function registerNativeCall(
call_type: NativeCallType,
filename: string,
symbol: string,
create_fn_len: null | number,
) {
const resolved_filename = resolveNativeFileId(call_type, filename);
const maybe_wrapped_symbol = create_fn_len != null ? "js2native_wrap_" + symbol.replace(/[^A-Za-z]/g, "_") : symbol;
const existing = nativeCalls.find(
call =>
call.is_wrapped == (create_fn_len != null) &&
call.filename === resolved_filename &&
call.symbol === maybe_wrapped_symbol,
);
if (existing) {
return existing.id;
}
const id = nativeCalls.length;
nativeCalls.push({
id,
type: create_fn_len != null ? "cpp" : call_type,
filename: resolved_filename,
symbol: maybe_wrapped_symbol,
is_wrapped: create_fn_len != null,
});
if (create_fn_len != null) {
wrapperCalls.push({
type: call_type,
wrap_kind: "new-function",
symbol_target: symbol,
symbol_generated: "js2native_wrap_" + symbol.replace(/[^A-Za-z]/g, "_"),
display_name: callBaseName(symbol),
call_length: create_fn_len,
filename: resolved_filename,
});
}
return id;
}
function symbol(call: Pick<NativeCall, "type" | "symbol" | "filename">) {
return call.type === "zig"
? `JS2Zig__${call.filename ? normalizeSymbolPathPrefix(call.filename) + "_" : ""}${call.symbol.replace(/[^A-Za-z]/g, "_")}`
: call.symbol;
}
function normalizeSymbolPathPrefix(input: string) {
input = path.resolve(input);
const bunDir = path.resolve(path.join(import.meta.dir, "..", ".."));
if (input.startsWith(bunDir)) {
input = input.slice(bunDir.length);
}
return input.replaceAll(".zig", "_zig_").replace(/[^A-Za-z]/g, "_");
}
function cppPointer(call: NativeCall) {
return `&${symbol(call)}`;
}
export function getJS2NativeCPP() {
const files = [
...new Set(nativeCalls.filter(x => x.filename.endsWith(".cpp")).map(x => x.filename.replace(/.cpp$/, ".h"))),
];
const externs: string[] = [];
const nativeCallStrings = nativeCalls
.filter(x => x.type === "zig")
.flatMap(
call => (
externs.push(`extern "C" SYSV_ABI JSC::EncodedJSValue ${symbol(call)}_workaround(Zig::GlobalObject*);` + "\n"),
[
`static ALWAYS_INLINE JSC::JSValue ${symbol(call)}(Zig::GlobalObject* global) {`,
` return JSValue::decode(${symbol(call)}_workaround(global));`,
`}` + "\n\n",
]
),
);
const wrapperCallStrings = wrapperCalls.map(x => {
if (x.wrap_kind === "new-function") {
return [
(x.type === "zig" &&
externs.push(
`BUN_DECLARE_HOST_FUNCTION(${symbol({
type: "zig",
symbol: x.symbol_target,
filename: x.filename,
})});`,
),
"") || "",
`static ALWAYS_INLINE JSC::JSValue ${x.symbol_generated}(Zig::GlobalObject* globalObject) {`,
` return JSC::JSFunction::create(globalObject->vm(), globalObject, ${x.call_length}, ${JSON.stringify(
x.display_name,
)}_s, ${symbol({
type: x.type,
symbol: x.symbol_target,
filename: x.filename,
})}, JSC::ImplementationVisibility::Public);`,
`}`,
].join("\n");
}
throw new Error(`Unknown wrap kind ${x.wrap_kind}`);
});
return [
`#pragma once`,
`#include "root.h"`,
...files.map(filename => `#include ${JSON.stringify(filename)}`),
...externs,
"\n" + "namespace JS2NativeGenerated {",
"using namespace Bun;",
"using namespace JSC;",
"using namespace WebCore;" + "\n",
...nativeCallStrings,
...wrapperCallStrings,
...nativeCalls
.filter(x => x.type === "bind")
.map(
x =>
`extern "C" SYSV_ABI JSC::EncodedJSValue js2native_bindgen_${basename(x.filename.replace(/\.bind\.ts$/, ""))}_${x.symbol}(Zig::GlobalObject*);`,
),
`typedef JSC::JSValue (*JS2NativeFunction)(Zig::GlobalObject*);`,
`static ALWAYS_INLINE JSC::JSValue callJS2Native(int32_t index, Zig::GlobalObject* global) {`,
` switch(index) {`,
...nativeCalls.map(
x =>
` case ${x.id}: return ${
x.type === "bind"
? `JSC::JSValue::decode(js2native_bindgen_${basename(x.filename.replace(/\.bind\.ts$/, ""))}_${x.symbol}(global))`
: `${symbol(x)}(global)`
};`,
),
` default:`,
` __builtin_unreachable();`,
` }`,
`}`,
`#define JS2NATIVE_COUNT ${nativeCalls.length}`,
"}",
].join("\n");
}
export function getJS2NativeZig(gs2NativeZigPath: string) {
return [
"//! This file is generated by src/codegen/generate-js2native.ts based on seen calls to the $zig() JS macro",
`const bun = @import("bun");`,
`const jsc = bun.jsc;`,
...nativeCalls
.filter(x => x.type === "zig")
.flatMap(call => [
`export fn ${symbol(call)}_workaround(global: *jsc.JSGlobalObject) callconv(jsc.conv) jsc.JSValue {`,
` return jsc.toJSHostCall(global, @src(), @import(${JSON.stringify(path.relative(path.dirname(gs2NativeZigPath), call.filename))}).${call.symbol}, .{global});`,
"}",
]),
...wrapperCalls
.filter(x => x.type === "zig")
.flatMap(x => [
`export fn ${symbol({
type: "zig",
symbol: x.symbol_target,
filename: x.filename,
})}(global: *jsc.JSGlobalObject, call_frame: *jsc.CallFrame) callconv(jsc.conv) jsc.JSValue {`,
` const function = @import(${JSON.stringify(path.relative(path.dirname(gs2NativeZigPath), x.filename))});`,
` return @call(.always_inline, jsc.toJSHostFn(function.${x.symbol_target}), .{global, call_frame});`,
"}",
]),
"comptime {",
...nativeCalls
.filter(x => x.type === "bind")
.flatMap(x => {
const base = basename(x.filename.replace(/\.bind\.ts$/, ""));
return [
` @export(&bun.gen.${base}.create${cap(x.symbol)}Callback, .{ .name = ${JSON.stringify(
`js2native_bindgen_${base}_${x.symbol}`,
)} });`,
];
}),
"}",
].join("\n");
}
export function getJS2NativeDTS() {
return [
"declare type NativeFilenameCPP = " +
sourceFiles
.filter(x => x.endsWith("cpp"))
.map(x => JSON.stringify(basename(x)))
.join("|"),
"declare type NativeFilenameZig = " +
sourceFiles
.filter(x => x.endsWith("zig"))
.map(x => JSON.stringify(basename(x)))
.join("|"),
"",
].join("\n");
}