Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Macro Overview

Macros are contributor ergonomics over the static JS surface backend, not a separate runtime registration system.

Macros sit on top of static specs, mutator-bound builders, centralized bootstrap, and startup/first-run benchmark ratchets.

Macros must be zero-cost at runtime. Generated code should look like handwritten static specs:

static MATH_SPEC: NamespaceSpec = NamespaceSpec {
    name: "Math",
    methods: &[
        MethodSpec {
            name: "abs",
            length: 1,
            attrs: Attr::builtin_function(),
            call: NativeCall::Static(math_abs),
        },
    ],
};

The available macro surface is in otter-macros:

  • #[js_namespace] generates namespace specs from inline Rust modules;
  • #[js_class] generates constructor/prototype class specs;
  • raft! generates grouped namespace specs without helper attributes.

#[js_namespace] example:

use otter_macros::js_namespace;

#[js_namespace(name = "Math", spec = MATH_SPEC)]
mod math {
    #[js_fn(name = "abs", length = 1)]
    pub fn abs(ctx: &mut NativeCtx<'_>, args: &[Value]) -> Result<Value, NativeError> {
        math_abs(ctx, args)
    }
}

The macro emits a public MATH_SPEC: NamespaceSpec and private static method metadata that can be passed to the JS surface builders or installed by the centralized bootstrap registry.

#[js_class] separates ordinary instance methods from JavaScript static methods:

use otter_macros::js_class;

#[js_class(name = "Point", spec = POINT_SPEC)]
mod point {
    #[js_constructor(length = 1)]
    pub fn construct(ctx: &mut NativeCtx<'_>, args: &[Value]) -> Result<Value, NativeError> {
        create_point(ctx, args)
    }

    #[js_method(name = "valueOf", length = 0)]
    pub fn value_of(ctx: &mut NativeCtx<'_>, args: &[Value]) -> Result<Value, NativeError> {
        point_value_of(ctx, args)
    }

    #[js_static_method(name = "from", length = 1)]
    pub fn from(ctx: &mut NativeCtx<'_>, args: &[Value]) -> Result<Value, NativeError> {
        point_from(ctx, args)
    }

    #[js_getter(name = "x")]
    pub fn get_x(ctx: &mut NativeCtx<'_>, args: &[Value]) -> Result<Value, NativeError> {
        point_get_x(ctx, args)
    }
}

Here #[js_method] installs Point.prototype.valueOf, while #[js_static_method] installs Point.from. Both still use the NativeCall::Static Rust function-pointer path.

raft! is for grouped namespace declarations:

otter_macros::raft! {
    pub static MATH_SPEC: namespace("Math") {
        methods: [
            "abs" => math_abs, length = 1;
        ]
    }
}

Remaining planned macro scope:

  • macro coverage for constants/data properties where the generated shape is still inspectable;
  • broader migration only after benchmark ratchets are in place.

Deferred until their backend APIs are stable:

  • #[dive] or equivalent async native binding sugar;
  • host-owned object surface macros;
  • hosted-module loader macros;
  • GC trace derive macros.

Macros must not hide capability enforcement, bootstrap order, async host-op scheduling, or important control flow. Prefer manual code when those concerns are the main behavior.

Generated Shape Contract

Macro expansion must produce:

  • static spec records;
  • ordinary Rust functions with explicit exported JS names and arity;
  • static native call targets for builtins by default;
  • builder/bootstrap calls through the centralized registry.

Macro expansion must not produce:

  • runtime registries or metadata parsing;
  • per-call allocation;
  • hidden global mutation;
  • hidden permission checks;
  • hidden async scheduling;
  • captures of RuntimeCx, NativeCtx, Value, Frame, Gc<T>, or Local<'gc, T> across .await.

Example Source And Expansion

Source:

#[js_namespace(name = "Math", spec = MATH_SPEC)]
mod math {
    #[js_fn(name = "abs", length = 1)]
    pub fn abs(ctx: &mut NativeCtx<'_>, args: &[Value]) -> Result<Value, NativeError> {
        math_abs(ctx, args)
    }
}

Required generated shape:

static MATH_SPEC: NamespaceSpec = NamespaceSpec {
    name: "Math",
    methods: &[MethodSpec {
        name: "abs",
        length: 1,
        attrs: Attr::builtin_function(),
        call: NativeCall::Static(math::abs),
    }],
    accessors: &[],
    constants: &[],
    attrs: Attr::global_binding(),
};

Keep manual specs for capability gates, delicate bootstrap order, async scheduling, host-owned object lifetimes, or API shapes not covered by the current macros.

Prefer Manual Code When

  • capability enforcement is the main behavior;
  • bootstrap or install order needs careful review;
  • async scheduling or cancellation behavior is non-trivial;
  • the macro would hide object graph, rooting, or external-memory lifetime decisions.