.claude/skills/implementing-jsc-classes-rust
Bun's JavaScriptCore Class Bindings Generator
Bridge JavaScript and Rust through .classes.ts definitions and Rust implementations.
Architecture
- JavaScript Interface Definition (
.classes.tsfiles) - Rust Implementation (
.rsfiles) - Generated Code —
src/codegen/generate-classes.tsemits C++ + Rust into${BUN_CODEGEN_DIR}/generated_classes.rs,include!d ascrate::generated_classesinbun_runtime. Runbun bdto regenerate.
Class Definition (.classes.ts)
export default [
define({
name: "Glob",
construct: true,
finalize: true,
hasPendingActivity: true,
proto: {
scan: { fn: "scan", length: 1 },
match: { fn: "match", length: 1 },
},
}),
];
Options:
construct: Has a publicnew Foo()constructorfinalize: Needs cleanup beyondDrop(rarely — see Finalize below)hasPendingActivity: GC keep-alive while async work is in flightproto: Methods (fn:), getters (getter: true, optionallycache: true)values: [...]: WriteBarrier slots for JS values the native side holds (callbacks, buffers)
Rust Implementation
use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult};
use std::sync::atomic::{AtomicUsize, Ordering};
#[bun_jsc::JsClass]
pub struct Glob {
pattern: Box<[u8]>,
has_pending_activity: AtomicUsize,
}
impl Glob {
pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<Box<Glob>> {
let arg = frame.argument(0);
let pattern = bun_core::String::from_js(arg, global)?.to_utf8_bytes().into();
Ok(Box::new(Glob { pattern, has_pending_activity: AtomicUsize::new(0) }))
}
#[bun_jsc::host_fn(method)]
pub fn r#match(&self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue> {
// ...
Ok(JSValue::TRUE)
}
pub fn has_pending_activity(&self) -> bool {
self.has_pending_activity.load(Ordering::SeqCst) > 0
}
}
Canonical signatures
| Hook | Signature |
|---|---|
| constructor | pub fn constructor(global: &JSGlobalObject, frame: &CallFrame) -> JsResult<Box<Self>> |
method (fn:) | pub fn name(&self, global: &JSGlobalObject, frame: &CallFrame) -> JsResult<JSValue> |
| getter | pub fn get_x(this: &Self, global: &JSGlobalObject) -> JsResult<JSValue> |
| finalize | pub fn finalize(self: Box<Self>) — or omit; the blanket JsFinalize just drops |
| hasPendingActivity | pub fn has_pending_activity(&self) -> bool |
A missing or mis-typed hook is a compile error in cargo check -p bun_runtime — the generated code calls the inherent method directly.
Hooking into the generated module
#[bun_jsc::JsClass] on the struct implements the JsClass trait (to_js, from_js, from_js_direct, get_constructor) by binding the C++ externs. Attribute knobs: no_constructor, no_finalize, estimated_size.
The codegen also emits a js_$T module with the cached-value accessors. Re-export it when you need *_set_cached / *_get_cached or detach_ptr:
pub use crate::generated_classes::js_Glob as js;
// or
bun_jsc::impl_js_class_via_generated!(Archive => crate::generated_classes::js_Archive);
The js_$T module surface:
pub fn from_js(value: JSValue) -> Option<NonNull<T>>;
pub fn from_js_direct(value: JSValue) -> Option<NonNull<T>>;
pub fn get_constructor(global: &JSGlobalObject) -> JSValue;
pub fn to_js(this: *mut T, global: &JSGlobalObject) -> JSValue; // ownership transfer
pub fn detach_ptr(value: JSValue);
// per cached getter / `values: [...]` entry:
pub fn <field>_set_cached(this_value: JSValue, global: &JSGlobalObject, value: JSValue);
pub fn <field>_get_cached(this_value: JSValue) -> Option<JSValue>;
Finalize
Most classes need nothing — #[bun_jsc::JsClass] wires the blanket JsFinalize whose default is drop(Box<Self>). Override only when you must release a JS handle or defer to a heap helper:
pub fn finalize(self: Box<Self>) {
bun_ptr::finalize_js_box(self, |this| this.this_value.with_mut(|v| v.finalize()));
}
Override with an inherent method, never impl JsFinalize for T.
Holding JS values
Never store raw JSValue in a struct field. Declare a slot in .classes.ts (values: ["callback"] or a cache: true getter) and read/write it through js::callback_set_cached(this_value, global, v) / js::callback_get_cached(this_value). The slot is a WriteBarrier visited by the GC, so the value stays alive without a Strong.
Reference implementations
src/runtime/api/glob.rs+Glob.classes.ts— constructor, methods,hasPendingActivity, default finalizesrc/runtime/api/cron.rs+cron.classes.ts—noConstructor, cached getter,values: [...], custom finalizesrc/runtime/image/Image.rs:56— thepub use crate::generated_classes::js_Image as js;one-linersrc/jsc/host_fn.rs— the host-fn adapters the codegen dispatches throughsrc/jsc_macros/lib.rs—#[bun_jsc::JsClass]proc-macro source
