Skip to content

Commit b39632c

Browse files
authored
feat: new binding generator (#15638)
1 parent 38325aa commit b39632c

File tree

83 files changed

+3980
-578
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+3980
-578
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,10 @@ scripts/env.local
116116
sign.*.json
117117
sign.json
118118
src/bake/generated.ts
119+
src/generated_enum_extractor.zig
119120
src/bun.js/bindings-obj
120121
src/bun.js/bindings/GeneratedJS2Native.zig
122+
src/bun.js/bindings/GeneratedBindings.zig
121123
src/bun.js/debug-bindings-obj
122124
src/deps/zig-clap/.gitattributes
123125
src/deps/zig-clap/.github

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"editor.tabSize": 4,
6464
"editor.defaultFormatter": "xaver.clang-format",
6565
},
66-
"clangd.arguments": ["-header-insertion=never"],
66+
"clangd.arguments": ["-header-insertion=never", "-no-unused-includes"],
6767

6868
// JavaScript
6969
"prettier.enable": true,

build.zig

+13
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,19 @@ pub fn build(b: *Build) !void {
327327
.{ .os = .windows, .arch = .x86_64 },
328328
});
329329
}
330+
331+
// zig build enum-extractor
332+
{
333+
// const step = b.step("enum-extractor", "Extract enum definitions (invoked by a code generator)");
334+
// const exe = b.addExecutable(.{
335+
// .name = "enum_extractor",
336+
// .root_source_file = b.path("./src/generated_enum_extractor.zig"),
337+
// .target = b.graph.host,
338+
// .optimize = .Debug,
339+
// });
340+
// const run = b.addRunArtifact(exe);
341+
// step.dependOn(&run.step);
342+
}
330343
}
331344

332345
pub fn addMultiCheck(

bun.lockb

416 Bytes
Binary file not shown.

cmake/targets/BuildBun.cmake

+37-4
Original file line numberDiff line numberDiff line change
@@ -318,13 +318,13 @@ register_command(
318318
TARGET
319319
bun-bake-codegen
320320
COMMENT
321-
"Bundling Kit Runtime"
321+
"Bundling Bake Runtime"
322322
COMMAND
323323
${BUN_EXECUTABLE}
324324
run
325325
${BUN_BAKE_RUNTIME_CODEGEN_SCRIPT}
326326
--debug=${DEBUG}
327-
--codegen_root=${CODEGEN_PATH}
327+
--codegen-root=${CODEGEN_PATH}
328328
SOURCES
329329
${BUN_BAKE_RUNTIME_SOURCES}
330330
${BUN_BAKE_RUNTIME_CODEGEN_SOURCES}
@@ -334,6 +334,39 @@ register_command(
334334
${BUN_BAKE_RUNTIME_OUTPUTS}
335335
)
336336

337+
set(BUN_BINDGEN_SCRIPT ${CWD}/src/codegen/bindgen.ts)
338+
339+
file(GLOB_RECURSE BUN_BINDGEN_SOURCES ${CONFIGURE_DEPENDS}
340+
${CWD}/src/**/*.bind.ts
341+
)
342+
343+
set(BUN_BINDGEN_CPP_OUTPUTS
344+
${CODEGEN_PATH}/GeneratedBindings.cpp
345+
)
346+
347+
set(BUN_BINDGEN_ZIG_OUTPUTS
348+
${CWD}/src/bun.js/bindings/GeneratedBindings.zig
349+
)
350+
351+
register_command(
352+
TARGET
353+
bun-binding-generator
354+
COMMENT
355+
"Processing \".bind.ts\" files"
356+
COMMAND
357+
${BUN_EXECUTABLE}
358+
run
359+
${BUN_BINDGEN_SCRIPT}
360+
--debug=${DEBUG}
361+
--codegen-root=${CODEGEN_PATH}
362+
SOURCES
363+
${BUN_BINDGEN_SOURCES}
364+
${BUN_BINDGEN_SCRIPT}
365+
OUTPUTS
366+
${BUN_BINDGEN_CPP_OUTPUTS}
367+
${BUN_BINDGEN_ZIG_OUTPUTS}
368+
)
369+
337370
set(BUN_JS_SINK_SCRIPT ${CWD}/src/codegen/generate-jssink.ts)
338371

339372
set(BUN_JS_SINK_SOURCES
@@ -385,7 +418,6 @@ set(BUN_OBJECT_LUT_OUTPUTS
385418
${CODEGEN_PATH}/NodeModuleModule.lut.h
386419
)
387420

388-
389421
macro(WEBKIT_ADD_SOURCE_DEPENDENCIES _source _deps)
390422
set(_tmp)
391423
get_source_file_property(_tmp ${_source} OBJECT_DEPENDS)
@@ -461,6 +493,7 @@ list(APPEND BUN_ZIG_SOURCES
461493
${CWD}/build.zig
462494
${CWD}/root.zig
463495
${CWD}/root_wasm.zig
496+
${BUN_BINDGEN_ZIG_OUTPUTS}
464497
)
465498

466499
set(BUN_ZIG_GENERATED_SOURCES
@@ -482,7 +515,6 @@ endif()
482515

483516
set(BUN_ZIG_OUTPUT ${BUILD_PATH}/bun-zig.o)
484517

485-
486518
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|ARM|arm64|ARM64|aarch64|AARCH64")
487519
if(APPLE)
488520
set(ZIG_CPU "apple_m1")
@@ -606,6 +638,7 @@ list(APPEND BUN_CPP_SOURCES
606638
${BUN_JS_SINK_OUTPUTS}
607639
${BUN_JAVASCRIPT_OUTPUTS}
608640
${BUN_OBJECT_LUT_OUTPUTS}
641+
${BUN_BINDGEN_CPP_OUTPUTS}
609642
)
610643

611644
if(WIN32)

docs/nav.ts

+3
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,9 @@ export default {
402402
page("project/building-windows", "Building Windows", {
403403
description: "Learn how to setup a development environment for contributing to the Windows build of Bun.",
404404
}),
405+
page("project/bindgen", "Bindgen", {
406+
description: "About the bindgen code generator",
407+
}),
405408
page("project/licensing", "License", {
406409
description: `Bun is a MIT-licensed project with a large number of statically-linked dependencies with various licenses.`,
407410
}),

docs/project/bindgen.md

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
{% callout %}
2+
3+
This document is for maintainers and contributors to Bun, and describes internal implementation details.
4+
5+
{% /callout %}
6+
7+
The new bindings generator, introduced to the codebase in Dec 2024, scans for
8+
`*.bind.ts` to find function and class definition, and generates glue code to
9+
interop between JavaScript and native code.
10+
11+
There are currently other code generators and systems that achieve similar
12+
purposes. The following will all eventually be completely phased out in favor of
13+
this one:
14+
15+
- "Classes generator", converting `*.classes.ts` for custom classes.
16+
- "JS2Native", allowing ad-hoc calls from `src/js` to native code.
17+
18+
## Creating JS Functions in Zig
19+
20+
Given a file implementing a simple function, such as `add`
21+
22+
```zig#src/bun.js/math.zig
23+
pub fn add(global: *JSC.JSGlobalObject, a: i32, b: i32) !i32 {
24+
return std.math.add(i32, a, b) catch {
25+
// Binding functions can return `error.OutOfMemory` and `error.JSError`.
26+
// Others like `error.Overflow` from `std.math.add` must be converted.
27+
// Remember to be descriptive.
28+
return global.throwPretty("Integer overflow while adding", .{});
29+
};
30+
}
31+
32+
const gen = bun.gen.math; // "math" being this file's basename
33+
34+
const std = @import("std");
35+
const bun = @import("root").bun;
36+
const JSC = bun.JSC;
37+
```
38+
39+
Then describe the API schema using a `.bind.ts` function. The binding file goes next to the Zig file.
40+
41+
```ts#src/bun.js/math.bind.ts
42+
import { t, fn } from 'bindgen';
43+
44+
export const add = fn({
45+
args: {
46+
global: t.globalObject,
47+
a: t.i32,
48+
b: t.i32.default(1),
49+
},
50+
ret: t.i32,
51+
});
52+
```
53+
54+
This function declaration is equivalent to:
55+
56+
```ts
57+
/**
58+
* Throws if zero arguments are provided.
59+
* Wraps out of range numbers using modulo.
60+
*/
61+
declare function add(a: number, b: number = 1): number;
62+
```
63+
64+
The code generator will provide `bun.gen.math.jsAdd`, which is the native function implementation. To pass to JavaScript, use `bun.gen.math.createAddCallback(global)`
65+
66+
## Strings
67+
68+
The type for receiving strings is one of [`t.DOMString`](https://webidl.spec.whatwg.org/#idl-DOMString), [`t.ByteString`](https://webidl.spec.whatwg.org/#idl-ByteString), and [`t.USVString`](https://webidl.spec.whatwg.org/#idl-USVString). These map directly to their WebIDL counterparts, and have slightly different conversion logic. Bindgen will pass BunString to native code in all cases.
69+
70+
When in doubt, use DOMString.
71+
72+
`t.UTF8String` can be used in place of `t.DOMString`, but will call `bun.String.toUTF8`. The native callback gets `[]const u8` (WTF-8 data) passed to native code, freeing it after the function returns.
73+
74+
TLDRs from WebIDL spec:
75+
76+
- ByteString can only contain valid latin1 characters. It is not safe to assume bun.String is already in 8-bit format, but it is extremely likely.
77+
- USVString will not contain invalid surrogate pairs, aka text that can be represented correctly in UTF-8.
78+
- DOMString is the loosest but also most recommended strategy.
79+
80+
## Function Variants
81+
82+
A `variants` can specify multiple variants (also known as overloads).
83+
84+
```ts#src/bun.js/math.bind.ts
85+
import { t, fn } from 'bindgen';
86+
87+
export const action = fn({
88+
variants: [
89+
{
90+
args: {
91+
a: t.i32,
92+
},
93+
ret: t.i32,
94+
},
95+
{
96+
args: {
97+
a: t.DOMString,
98+
},
99+
ret: t.DOMString,
100+
},
101+
]
102+
});
103+
```
104+
105+
In Zig, each variant gets a number, based on the order the schema defines.
106+
107+
```
108+
fn action1(a: i32) i32 {
109+
return a;
110+
}
111+
112+
fn action2(a: bun.String) bun.String {
113+
return a;
114+
}
115+
```
116+
117+
## `t.dictionary`
118+
119+
A `dictionary` is a definition for a JavaScript object, typically as a function inputs. For function outputs, it is usually a smarter idea to declare a class type to add functions and destructuring.
120+
121+
## Enumerations
122+
123+
To use [WebIDL's enumeration](https://webidl.spec.whatwg.org/#idl-enums) type, use either:
124+
125+
- `t.stringEnum`: Create and codegen a new enum type.
126+
- `t.zigEnum`: Derive a bindgen type off of an existing enum in the codebase.
127+
128+
An example of `stringEnum` as used in `fmt.zig` / `bun:internal-for-testing`
129+
130+
```ts
131+
export const Formatter = t.stringEnum(
132+
"highlight-javascript",
133+
"escape-powershell",
134+
);
135+
136+
export const fmtString = fn({
137+
args: {
138+
global: t.globalObject,
139+
code: t.UTF8String,
140+
formatter: Formatter,
141+
},
142+
ret: t.DOMString,
143+
});
144+
```
145+
146+
WebIDL strongly encourages using kebab case for enumeration values, to be consistent with existing Web APIs.
147+
148+
### Deriving enums from Zig code
149+
150+
TODO: zigEnum
151+
152+
## `t.oneOf`
153+
154+
A `oneOf` is a union between two or more types. It is represented by `union(enum)` in Zig.
155+
156+
TODO:
157+
158+
## Attributes
159+
160+
There are set of attributes that can be chained onto `t.*` types. On all types there are:
161+
162+
- `.required`, in dictionary parameters only
163+
- `.optional`, in function arguments only
164+
- `.default(T)`
165+
166+
When a value is optional, it is lowered to a Zig optional.
167+
168+
Depending on the type, there are more attributes available. See the type definitions in auto-complete for more details. Note that one of the above three can only be applied, and they must be applied at the end.
169+
170+
### Integer Attributes
171+
172+
Integer types allow customizing the overflow behavior with `clamp` or `enforceRange`
173+
174+
```ts
175+
import { t, fn } from "bindgen";
176+
177+
export const add = fn({
178+
args: {
179+
global: t.globalObject,
180+
// enforce in i32 range
181+
a: t.i32.enforceRange(),
182+
// clamp to u16 range
183+
c: t.u16,
184+
// enforce in arbitrary range, with a default if not provided
185+
b: t.i32.enforceRange(0, 1000).default(5),
186+
// clamp to arbitrary range, or null
187+
d: t.u16.clamp(0, 10).optional,
188+
},
189+
ret: t.i32,
190+
});
191+
```
192+
193+
## Callbacks
194+
195+
TODO
196+
197+
## Classes
198+
199+
TODO

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"react": "^18.3.1",
2222
"react-dom": "^18.3.1",
2323
"source-map-js": "^1.2.0",
24-
"typescript": "^5.4.5",
24+
"typescript": "^5.7.2",
2525
"caniuse-lite": "^1.0.30001620",
2626
"autoprefixer": "^10.4.19",
2727
"@mdn/browser-compat-data": "~5.5.28"

packages/bun-types/ambient.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare module "*.txt" {
2+
var text: string;
3+
export = text;
4+
}
5+
6+
declare module "*.toml" {
7+
var contents: any;
8+
export = contents;
9+
}

packages/bun-types/globals.d.ts

+2-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
export {};
2-
31
type _ReadableStream<T> = typeof globalThis extends {
42
onerror: any;
53
ReadableStream: infer T;
@@ -141,16 +139,6 @@ import type { TextDecoder as NodeTextDecoder, TextEncoder as NodeTextEncoder } f
141139
import type { MessagePort } from "worker_threads";
142140
import type { WebSocket as _WebSocket } from "ws";
143141

144-
declare module "*.txt" {
145-
var text: string;
146-
export = text;
147-
}
148-
149-
declare module "*.toml" {
150-
var contents: any;
151-
export = contents;
152-
}
153-
154142
declare global {
155143
var Bun: typeof import("bun");
156144

@@ -1835,10 +1823,10 @@ declare global {
18351823
readonly main: boolean;
18361824

18371825
/** Alias of `import.meta.dir`. Exists for Node.js compatibility */
1838-
readonly dirname: string;
1826+
dirname: string;
18391827

18401828
/** Alias of `import.meta.path`. Exists for Node.js compatibility */
1841-
readonly filename: string;
1829+
filename: string;
18421830
}
18431831

18441832
/**

packages/bun-types/index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@
2020
/// <reference path="./sqlite.d.ts" />
2121
/// <reference path="./wasm.d.ts" />
2222
/// <reference path="./deprecated.d.ts" />
23+
/// <reference path="./ambient.d.ts" />

0 commit comments

Comments
 (0)