implementing-jsc-classes-rust

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

Creates JavaScript classes using Bun's Rust bindings generator (.classes.ts). Use when implementing new JS APIs in Rust with JSC integration, prototypes, or constructors.

.claude/skills/implementing-jsc-classes-rust/SKILL.md

Download bundle ↓
main · e8750e31 bundle fileScanned 2026-09-15

SKILL.md

1,410 tokens · o200k_base · 5,572 bytes

Source excerpt starting at line 1.
---name: implementing-jsc-classes-rustdescription: Creates JavaScript classes using Bun's Rust bindings generator (.classes.ts). Use when implementing new JS APIs in Rust with JSC integration, prototypes, or constructors.--- # Bun's JavaScriptCore Class Bindings Generator Bridge JavaScript and Rust through `.classes.ts` definitions and Rust implementations. ## Architecture 1. **JavaScript Interface Definition** (`.classes.ts` files)2. **Rust Implementation** (`.rs` files)3. **Generated Code** — `src/codegen/generate-classes.ts` emits C++ + Rust into `${BUN_CODEGEN_DIR}/generated_classes.rs`, `include!`d as `crate::generated_classes` in `bun_runtime`. Run `bun bd` to regenerate. ## Class Definition (.classes.ts) ```typescriptexport 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 public `new Foo()` constructor- `finalize`: Needs cleanup beyond `Drop` (rarely — see Finalize below)- `hasPendingActivity`: GC keep-alive while async work is in flight- `proto`: Methods (`fn:`), getters (`getter: true`, optionally `cache: true`)- `values: [...]`: WriteBarrier slots for JS values the native side holds (callbacks, buffers) ## Rust Implementation ```rustuse 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`: ```rustpub use crate::generated_classes::js_Glob as js;// orbun_jsc::impl_js_class_via_generated!(Archive => crate::generated_classes::js_Archive);``` The `js_$T` module surface: ```rustpub 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 transferpub 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: ```rustpub 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 finalize- `src/runtime/api/cron.rs` + `cron.classes.ts` — `noConstructor`, cached getter, `values: [...]`, custom finalize- `src/runtime/image/Image.rs:56` — the `pub use crate::generated_classes::js_Image as js;` one-liner- `src/jsc/host_fn.rs` — the host-fn adapters the codegen dispatches through- `src/jsc_macros/lib.rs` — `#[bun_jsc::JsClass]` proc-macro source 
Discovery context

Discovered by repository scan. No exact path reference found in the snapshot’s root CLAUDE.md.