NOTE
Dieses Dokument ist für Maintainer und Contributors von Bun und beschreibt interne Implementierungsdetails.Der neue Bindings-Generator, der im Dezember 2024 in die Codebasis eingeführt wurde, scannt nach *.bind.ts, um Funktions- und Klassendefinitionen zu finden, und generiert Glue-Code zur Interoperabilität zwischen JavaScript und nativem Code.
Es gibt derzeit andere Code-Generatoren und Systeme, die ähnliche Zwecke erfüllen. Die folgenden werden jedoch alle schließlich zugunsten dieses einen vollständig ausgemustert:
- "Klassen-Generator", der
*.classes.tsfür benutzerdefinierte Klassen konvertiert. - "JS2Native", das Ad-hoc-Aufrufe von
src/jszu nativem Code ermöglicht.
JS-Funktionen in Zig erstellen
Gegeben eine Datei, die eine einfache Funktion implementiert, wie add:
pub fn add(global: *jsc.JSGlobalObject, a: i32, b: i32) !i32 {
return std.math.add(i32, a, b) catch {
// Binding-Funktionen können error.OutOfMemory und error.JSError zurückgeben.
// Andere wie error.Overflow von std.math.add müssen konvertiert werden.
// Denken Sie daran, beschreibend zu sein.
return global.throwPretty("Integer overflow while adding", .{});
};
}
const gen = bun.gen.math; // "math" ist der Basisname dieser Datei
const std = @import("std");
const bun = @import("bun");
const jsc = bun.jsc;Beschreiben Sie dann das API-Schema mit einer .bind.ts Funktion. Die Binding-Datei befindet sich neben der Zig-Datei.
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
a: t.i32,
b: t.i32.default(1),
},
ret: t.i32,
});Diese Funktionsdeklaration ist äquivalent zu:
/**
* Wirft, wenn null Argumente bereitgestellt werden.
* Wickelt Zahlen außerhalb des Bereichs mit Modulo.
*/
declare function add(a: number, b: number = 1): number;Der Code-Generator stellt bun.gen.math.jsAdd bereit, was die native Funktionsimplementierung ist. Um an JavaScript zu übergeben, verwenden Sie bun.gen.math.createAddCallback(global). JS-Dateien in src/js/ können $bindgenFn("math.bind.ts", "add") verwenden, um einen Handle zur Implementierung zu erhalten.
Strings
Der Typ zum Empfangen von Strings ist einer von t.DOMString, t.ByteString, und t.USVString. Diese entsprechen direkt ihren WebIDL-Gegenstücken und haben leicht unterschiedliche Konvertierungslogik. Bindgen wird in allen Fällen BunString an nativen Code übergeben.
Verwenden Sie im Zweifelsfall DOMString.
t.UTF8String kann anstelle von t.DOMString verwendet werden, ruft jedoch bun.String.toUTF8 auf. Der native Callback erhält []const u8 (WTF-8-Daten), die an nativen Code übergeben werden, und gibt sie nach Rückkehr der Funktion frei.
TLDRs von der WebIDL-Spezifikation:
- ByteString kann nur gültige latin1-Zeichen enthalten. Es ist nicht sicher anzunehmen, dass bun.String bereits im 8-Bit-Format ist, aber es ist äußerst wahrscheinlich.
- USVString enthält keine ungültigen Surrogatpaare, also Text, der korrekt in UTF-8 dargestellt werden kann.
- DOMString ist die lockerste, aber auch am meisten empfohlene Strategie.
Funktionsvarianten
Ein variants kann mehrere Varianten (auch bekannt als Überladungen) spezifizieren.
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 erhält jede Variante eine Nummer, basierend auf der Reihenfolge, in der das Schema definiert ist.
fn action1(a: i32) i32 {
return a;
}
fn action2(a: bun.String) bun.String {
return a;
}t.dictionary
Ein dictionary ist eine Definition für ein JavaScript-Objekt, typischerweise als Funktionseingaben. Für Funktionsausgaben ist es normalerweise eine klügere Idee, einen Klassentyp zu deklarieren, um Funktionen und Destrukturierung hinzuzufügen.
Enumerationen
Um WebIDs Enumeration Typ zu verwenden, verwenden Sie entweder:
t.stringEnum: Erstellen und codegennen Sie einen neuen Enum-Typ.t.zigEnum: Leiten Sie einen Bindgen-Typ von einem vorhandenen Enum in der Codebasis ab.
Ein Beispiel für stringEnum wie in fmt.zig / bun:internal-for-testing verwendet:
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 empfiehlt dringend die Verwendung von Kebab-Case für Enumerationswerte, um mit bestehenden Web-APIs konsistent zu sein.
Enums von Zig-Code ableiten
TODO: zigEnum
t.oneOf
Ein oneOf ist eine Vereinigung zwischen zwei oder mehr Typen. Es wird durch union(enum) in Zig dargestellt.
TODO:
Attribute
Es gibt einen Satz von Attributen, die an t.* Typen angehängt werden können. Bei allen Typen gibt es:
.required, nur in Dictionary-Parametern.optional, nur in Funktionsargumenten.default(T)
Wenn ein Wert optional ist, wird er zu einem Zig-Optional reduziert.
Je nach Typ stehen weitere Attribute zur Verfügung. Weitere Details finden Sie in den Typdefinitionen in der Autovervollständigung. Beachten Sie, dass nur eines der oben genannten drei angewendet werden kann und sie am Ende angewendet werden müssen.
Integer-Attribute
Integer-Typen ermöglichen die Anpassung des Overflow-Verhaltens mit clamp oder enforceRange
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// im i32-Bereich erzwingen
a: t.i32.enforceRange(),
// auf u16-Bereich klemmen
b: t.u16,
// im beliebigen Bereich erzwingen, mit einem Standardwert, falls nicht angegeben
c: t.i32.enforceRange(0, 1000).default(5),
// auf beliebigen Bereich klemmen, oder null
d: t.u16.clamp(0, 10).optional,
},
ret: t.i32,
});Verschiedene Node.js-Validator-Funktionen wie validateInteger, validateNumber und mehr sind verfügbar. Verwenden Sie diese bei der Implementierung von Node.js-APIs, damit die Fehlermeldungen 1:1 mit dem übereinstimmen, was Node tun würde.
Im Gegensatz zu enforceRange, das von WebIDL übernommen ist, sind validate*-Funktionen bei den akzeptierten Eingaben viel strenger. Zum Beispiel überprüft Nodes numerischer Validator typeof value === 'number', während WebIDL ToNumber für verlustbehaftete Konvertierung verwendet.
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// werfen, wenn keine Zahl gegeben ist
a: t.f64.validateNumber(),
// gültig im i32-Bereich
a: t.i32.validateInt32(),
// f64 im Bereich sicherer Integer
b: t.f64.validateInteger(),
// f64 im angegebenen Bereich
c: t.f64.validateNumber(-10000, 10000),
},
ret: t.i32,
});Callbacks
TODO
Klassen
TODO