Skip to content

Commit

Permalink
feat: new binding generator (#15638)
Browse files Browse the repository at this point in the history
  • Loading branch information
paperdave authored Dec 10, 2024
1 parent 38325aa commit b39632c
Show file tree
Hide file tree
Showing 83 changed files with 3,980 additions and 578 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ scripts/env.local
sign.*.json
sign.json
src/bake/generated.ts
src/generated_enum_extractor.zig
src/bun.js/bindings-obj
src/bun.js/bindings/GeneratedJS2Native.zig
src/bun.js/bindings/GeneratedBindings.zig
src/bun.js/debug-bindings-obj
src/deps/zig-clap/.gitattributes
src/deps/zig-clap/.github
Expand Down
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"editor.tabSize": 4,
"editor.defaultFormatter": "xaver.clang-format",
},
"clangd.arguments": ["-header-insertion=never"],
"clangd.arguments": ["-header-insertion=never", "-no-unused-includes"],

// JavaScript
"prettier.enable": true,
Expand Down
13 changes: 13 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,19 @@ pub fn build(b: *Build) !void {
.{ .os = .windows, .arch = .x86_64 },
});
}

// zig build enum-extractor
{
// const step = b.step("enum-extractor", "Extract enum definitions (invoked by a code generator)");
// const exe = b.addExecutable(.{
// .name = "enum_extractor",
// .root_source_file = b.path("./src/generated_enum_extractor.zig"),
// .target = b.graph.host,
// .optimize = .Debug,
// });
// const run = b.addRunArtifact(exe);
// step.dependOn(&run.step);
}
}

pub fn addMultiCheck(
Expand Down
Binary file modified bun.lockb
Binary file not shown.
41 changes: 37 additions & 4 deletions cmake/targets/BuildBun.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,13 @@ register_command(
TARGET
bun-bake-codegen
COMMENT
"Bundling Kit Runtime"
"Bundling Bake Runtime"
COMMAND
${BUN_EXECUTABLE}
run
${BUN_BAKE_RUNTIME_CODEGEN_SCRIPT}
--debug=${DEBUG}
--codegen_root=${CODEGEN_PATH}
--codegen-root=${CODEGEN_PATH}
SOURCES
${BUN_BAKE_RUNTIME_SOURCES}
${BUN_BAKE_RUNTIME_CODEGEN_SOURCES}
Expand All @@ -334,6 +334,39 @@ register_command(
${BUN_BAKE_RUNTIME_OUTPUTS}
)

set(BUN_BINDGEN_SCRIPT ${CWD}/src/codegen/bindgen.ts)

file(GLOB_RECURSE BUN_BINDGEN_SOURCES ${CONFIGURE_DEPENDS}
${CWD}/src/**/*.bind.ts
)

set(BUN_BINDGEN_CPP_OUTPUTS
${CODEGEN_PATH}/GeneratedBindings.cpp
)

set(BUN_BINDGEN_ZIG_OUTPUTS
${CWD}/src/bun.js/bindings/GeneratedBindings.zig
)

register_command(
TARGET
bun-binding-generator
COMMENT
"Processing \".bind.ts\" files"
COMMAND
${BUN_EXECUTABLE}
run
${BUN_BINDGEN_SCRIPT}
--debug=${DEBUG}
--codegen-root=${CODEGEN_PATH}
SOURCES
${BUN_BINDGEN_SOURCES}
${BUN_BINDGEN_SCRIPT}
OUTPUTS
${BUN_BINDGEN_CPP_OUTPUTS}
${BUN_BINDGEN_ZIG_OUTPUTS}
)

set(BUN_JS_SINK_SCRIPT ${CWD}/src/codegen/generate-jssink.ts)

set(BUN_JS_SINK_SOURCES
Expand Down Expand Up @@ -385,7 +418,6 @@ set(BUN_OBJECT_LUT_OUTPUTS
${CODEGEN_PATH}/NodeModuleModule.lut.h
)


macro(WEBKIT_ADD_SOURCE_DEPENDENCIES _source _deps)
set(_tmp)
get_source_file_property(_tmp ${_source} OBJECT_DEPENDS)
Expand Down Expand Up @@ -461,6 +493,7 @@ list(APPEND BUN_ZIG_SOURCES
${CWD}/build.zig
${CWD}/root.zig
${CWD}/root_wasm.zig
${BUN_BINDGEN_ZIG_OUTPUTS}
)

set(BUN_ZIG_GENERATED_SOURCES
Expand All @@ -482,7 +515,6 @@ endif()

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


if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|ARM|arm64|ARM64|aarch64|AARCH64")
if(APPLE)
set(ZIG_CPU "apple_m1")
Expand Down Expand Up @@ -606,6 +638,7 @@ list(APPEND BUN_CPP_SOURCES
${BUN_JS_SINK_OUTPUTS}
${BUN_JAVASCRIPT_OUTPUTS}
${BUN_OBJECT_LUT_OUTPUTS}
${BUN_BINDGEN_CPP_OUTPUTS}
)

if(WIN32)
Expand Down
3 changes: 3 additions & 0 deletions docs/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,9 @@ export default {
page("project/building-windows", "Building Windows", {
description: "Learn how to setup a development environment for contributing to the Windows build of Bun.",
}),
page("project/bindgen", "Bindgen", {
description: "About the bindgen code generator",
}),
page("project/licensing", "License", {
description: `Bun is a MIT-licensed project with a large number of statically-linked dependencies with various licenses.`,
}),
Expand Down
199 changes: 199 additions & 0 deletions docs/project/bindgen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
{% callout %}

This document is for maintainers and contributors to Bun, and describes internal implementation details.

{% /callout %}

The new bindings generator, introduced to the codebase in Dec 2024, scans for
`*.bind.ts` to find function and class definition, and generates glue code to
interop between JavaScript and native code.

There are currently other code generators and systems that achieve similar
purposes. The following will all eventually be completely phased out in favor of
this one:

- "Classes generator", converting `*.classes.ts` for custom classes.
- "JS2Native", allowing ad-hoc calls from `src/js` to native code.

## Creating JS Functions in Zig

Given a file implementing a simple function, such as `add`

```zig#src/bun.js/math.zig
pub fn add(global: *JSC.JSGlobalObject, a: i32, b: i32) !i32 {
return std.math.add(i32, a, b) catch {
// Binding functions can return `error.OutOfMemory` and `error.JSError`.
// Others like `error.Overflow` from `std.math.add` must be converted.
// Remember to be descriptive.
return global.throwPretty("Integer overflow while adding", .{});
};
}
const gen = bun.gen.math; // "math" being this file's basename
const std = @import("std");
const bun = @import("root").bun;
const JSC = bun.JSC;
```

Then describe the API schema using a `.bind.ts` function. The binding file goes next to the Zig file.

```ts#src/bun.js/math.bind.ts
import { t, fn } from 'bindgen';

export const add = fn({
args: {
global: t.globalObject,
a: t.i32,
b: t.i32.default(1),
},
ret: t.i32,
});
```

This function declaration is equivalent to:

```ts
/**
* Throws if zero arguments are provided.
* Wraps out of range numbers using modulo.
*/
declare function add(a: number, b: number = 1): number;
```

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)`

## Strings

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.

When in doubt, use DOMString.

`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.

TLDRs from WebIDL spec:

- 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.
- USVString will not contain invalid surrogate pairs, aka text that can be represented correctly in UTF-8.
- DOMString is the loosest but also most recommended strategy.

## Function Variants

A `variants` can specify multiple variants (also known as overloads).

```ts#src/bun.js/math.bind.ts
import { t, fn } from 'bindgen';

export const action = fn({
variants: [
{
args: {
a: t.i32,
},
ret: t.i32,
},
{
args: {
a: t.DOMString,
},
ret: t.DOMString,
},
]
});
```

In Zig, each variant gets a number, based on the order the schema defines.

```
fn action1(a: i32) i32 {
return a;
}
fn action2(a: bun.String) bun.String {
return a;
}
```

## `t.dictionary`

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.

## Enumerations

To use [WebIDL's enumeration](https://webidl.spec.whatwg.org/#idl-enums) type, use either:

- `t.stringEnum`: Create and codegen a new enum type.
- `t.zigEnum`: Derive a bindgen type off of an existing enum in the codebase.

An example of `stringEnum` as used in `fmt.zig` / `bun:internal-for-testing`

```ts
export const Formatter = t.stringEnum(
"highlight-javascript",
"escape-powershell",
);

export const fmtString = fn({
args: {
global: t.globalObject,
code: t.UTF8String,
formatter: Formatter,
},
ret: t.DOMString,
});
```

WebIDL strongly encourages using kebab case for enumeration values, to be consistent with existing Web APIs.

### Deriving enums from Zig code

TODO: zigEnum

## `t.oneOf`

A `oneOf` is a union between two or more types. It is represented by `union(enum)` in Zig.

TODO:

## Attributes

There are set of attributes that can be chained onto `t.*` types. On all types there are:

- `.required`, in dictionary parameters only
- `.optional`, in function arguments only
- `.default(T)`

When a value is optional, it is lowered to a Zig optional.

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.

### Integer Attributes

Integer types allow customizing the overflow behavior with `clamp` or `enforceRange`

```ts
import { t, fn } from "bindgen";

export const add = fn({
args: {
global: t.globalObject,
// enforce in i32 range
a: t.i32.enforceRange(),
// clamp to u16 range
c: t.u16,
// enforce in arbitrary range, with a default if not provided
b: t.i32.enforceRange(0, 1000).default(5),
// clamp to arbitrary range, or null
d: t.u16.clamp(0, 10).optional,
},
ret: t.i32,
});
```

## Callbacks

TODO

## Classes

TODO
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"source-map-js": "^1.2.0",
"typescript": "^5.4.5",
"typescript": "^5.7.2",
"caniuse-lite": "^1.0.30001620",
"autoprefixer": "^10.4.19",
"@mdn/browser-compat-data": "~5.5.28"
Expand Down
9 changes: 9 additions & 0 deletions packages/bun-types/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare module "*.txt" {
var text: string;
export = text;
}

declare module "*.toml" {
var contents: any;
export = contents;
}
16 changes: 2 additions & 14 deletions packages/bun-types/globals.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export {};

type _ReadableStream<T> = typeof globalThis extends {
onerror: any;
ReadableStream: infer T;
Expand Down Expand Up @@ -141,16 +139,6 @@ import type { TextDecoder as NodeTextDecoder, TextEncoder as NodeTextEncoder } f
import type { MessagePort } from "worker_threads";
import type { WebSocket as _WebSocket } from "ws";

declare module "*.txt" {
var text: string;
export = text;
}

declare module "*.toml" {
var contents: any;
export = contents;
}

declare global {
var Bun: typeof import("bun");

Expand Down Expand Up @@ -1835,10 +1823,10 @@ declare global {
readonly main: boolean;

/** Alias of `import.meta.dir`. Exists for Node.js compatibility */
readonly dirname: string;
dirname: string;

/** Alias of `import.meta.path`. Exists for Node.js compatibility */
readonly filename: string;
filename: string;
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/bun-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
/// <reference path="./sqlite.d.ts" />
/// <reference path="./wasm.d.ts" />
/// <reference path="./deprecated.d.ts" />
/// <reference path="./ambient.d.ts" />
Loading

0 comments on commit b39632c

Please sign in to comment.