// 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) { 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"); }