NOTE
Este documento é para mantenedores e contribuidores do Bun, e descreve detalhes internos de implementação.O novo gerador de bindings, introduzido ao código em dezembro de 2024, procura por *.bind.ts para encontrar definições de funções e classes, e gera código glue para interoperabilidade entre JavaScript e código nativo.
Atualmente existem outros geradores de código e sistemas que alcançam propósitos similares. Os seguintes serão eventualmente completamente substituídos em favor deste:
- "Gerador de classes", convertendo
*.classes.tspara classes personalizadas. - "JS2Native", permitindo chamadas ad-hoc de
src/jspara código nativo.
Criando funções JS em Zig
Dado um arquivo implementando uma função simples, como add:
pub fn add(global: *jsc.JSGlobalObject, a: i32, b: i32) !i32 {
return std.math.add(i32, a, b) catch {
// Funções de binding podem retornar `error.OutOfMemory` e `error.JSError`.
// Outros como `error.Overflow` de `std.math.add` devem ser convertidos.
// Lembre-se de ser descritivo.
return global.throwPretty("Integer overflow while adding", .{});
};
}
const gen = bun.gen.math; // "math" sendo o nome base deste arquivo
const std = @import("std");
const bun = @import("bun");
const jsc = bun.jsc;Então descreva o esquema da API usando uma função .bind.ts. O arquivo de binding fica próximo ao arquivo Zig.
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
a: t.i32,
b: t.i32.default(1),
},
ret: t.i32,
});Esta declaração de função é equivalente a:
/**
* Lança erro se zero argumentos forem fornecidos.
* Envolve números fora do intervalo usando módulo.
*/
declare function add(a: number, b: number = 1): number;O gerador de código fornecerá bun.gen.math.jsAdd, que é a implementação da função nativa. Para passar para JavaScript, use bun.gen.math.createAddCallback(global). Arquivos JS em src/js/ podem usar $bindgenFn("math.bind.ts", "add") para obter um handle para a implementação.
Strings
O tipo para receber strings é um dos t.DOMString, t.ByteString, e t.USVString. Estes mapeiam diretamente para suas contrapartes WebIDL, e têm lógica de conversão ligeiramente diferente. Bindgen passará BunString para código nativo em todos os casos.
Na dúvida, use DOMString.
t.UTF8String pode ser usado no lugar de t.DOMString, mas chamará bun.String.toUTF8. O callback nativo recebe []const u8 (dados WTF-8) passado para o código nativo, liberando-o após o retorno da função.
Resumos da especificação WebIDL:
- ByteString só pode conter caracteres latin1 válidos. Não é seguro assumir que bun.String já está no formato de 8 bits, mas é extremamente provável.
- USVString não conterá pares substitutos inválidos, também conhecido como texto que pode ser representado corretamente em UTF-8.
- DOMString é a estratégia mais flexível mas também mais recomendada.
Variantes de Função
Um variants pode especificar múltiplas variantes (também conhecidas como sobrecargas).
import { t, fn } from "bindgen";
export const action = fn({
variants: [
{
args: {
a: t.i32,
},
ret: t.i32,
},
{
args: {
a: t.DOMString,
},
ret: t.DOMString,
},
],
});Em Zig, cada variante recebe um número, baseado na ordem que o esquema define.
fn action1(a: i32) i32 {
return a;
}
fn action2(a: bun.String) bun.String {
return a;
}t.dictionary
Um dictionary é uma definição para um objeto JavaScript, tipicamente como entradas de função. Para saídas de função, geralmente é uma ideia mais inteligente declarar um tipo de classe para adicionar funções e desestruturação.
Enumerações
Para usar o tipo de enumeração do WebIDL, use:
t.stringEnum: Criar e gerar código para um novo tipo de enum.t.zigEnum: Derivar um tipo bindgen de um enum existente no código.
Um exemplo de stringEnum como usado em fmt.zig / bun:internal-for-testing:
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 fortemente encoraja usar kebab case para valores de enumeração, para ser consistente com APIs Web existentes.
Derivando enums de código Zig
TODO: zigEnum
t.oneOf
Um oneOf é uma união entre dois ou mais tipos. É representado por union(enum) em Zig.
TODO:
Atributos
Existe um conjunto de atributos que podem ser encadeados em tipos t.*. Em todos os tipos existem:
.required, apenas em parâmetros de dicionário.optional, apenas em argumentos de função.default(T)
Quando um valor é opcional, é reduzido para um opcional Zig.
Dependendo do tipo, há mais atributos disponíveis. Veja as definições de tipo no auto-complete para mais detalhes. Note que apenas um dos três acima pode ser aplicado, e devem ser aplicados no final.
Atributos de Inteiro
Tipos de inteiro permitem personalizar o comportamento de overflow com clamp ou enforceRange:
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// forçar no intervalo i32
a: t.i32.enforceRange(),
// limitar ao intervalo u16
b: t.u16,
// forçar em intervalo arbitrário, com um padrão se não fornecido
c: t.i32.enforceRange(0, 1000).default(5),
// limitar ao intervalo arbitrário, ou nulo
d: t.u16.clamp(0, 10).optional,
},
ret: t.i32,
});Várias funções validadoras do Node.js como validateInteger, validateNumber, e outras estão disponíveis. Use-as ao implementar APIs Node.js, para que as mensagens de erro correspondam 1:1 ao que o Node faria.
Ao contrário de enforceRange, que é retirado do WebIDL, as funções validate* são muito mais estritas na entrada que aceitam. Por exemplo, o validador numérico do Node verifica typeof value === 'number', enquanto WebIDL usa ToNumber para conversão com perda.
import { t, fn } from "bindgen";
export const add = fn({
args: {
global: t.globalObject,
// lançar erro se não for fornecido um número
a: t.f64.validateNumber(),
// válido no intervalo i32
a: t.i32.validateInt32(),
// f64 dentro do intervalo de inteiros seguros
b: t.f64.validateInteger(),
// f64 no intervalo dado
c: t.f64.validateNumber(-10000, 10000),
},
ret: t.i32,
});Callbacks
TODO
Classes
TODO