Native Fusion

On this page

Native Fusion — Rust Interop Reference

Native Fusion is the mechanism that lets you write Rust functions and classes and expose them as first-class BoxLang Built-In Functions (BIFs) and native objects. Everything is statically linked into your final binary — no shared libraries, no FFI boilerplate at the BoxLang call site.

Native Fusion is available in native builds only (--target native).
It is not available in WASM targets.


How It Works

  1. Create a native/ directory alongside your .bxs entry script.
  2. Write one or more .rs files inside native/.
  3. Each .rs file exports a register_bifs() and/or register_classes() function.
  4. Run matchbox --target native app.bxs.

MatchBox detects native/, compiles the Rust files together with the VM, merges all BIF and class registrations, and produces a single binary.


Macros

All interop macros are re-exported from the matchbox_vm crate — no separate matchbox_macros import is needed:

use matchbox_vm::{matchbox_fn, matchbox_class, matchbox_methods};

#[matchbox_fn] — Expose a Rust function as a BoxLang BIF

Annotate a plain Rust function with typed parameters. The macro generates a corresponding {name}_wrapper function that carries the low-level BIF signature expected by the VM (fn(&mut dyn BxVM, &[BxValue]) -> Result<BxValue, String>).

The wrapper handles argument-count validation and type coercion automatically.

Usage

use matchbox_vm::{matchbox_fn, types::{BxNativeFunction}};
use std::collections::HashMap;

#[matchbox_fn]
pub fn add(a: f64, b: f64) -> f64 {
    a + b
}

pub fn register_bifs() -> HashMap<String, BxNativeFunction> {
    let mut map = HashMap::new();
    // Register the generated wrapper, not the original function.
    map.insert("add".to_string(), add_wrapper as BxNativeFunction);
    map
}

BoxLang:

result = add(10, 20)
println(result)   // 30

Supported Parameter Types

Rust parameter typeVM conversion applied by the macro
f64args[i].as_number()
i32args[i].as_int()
boolargs[i].as_bool()
Stringvm.to_string(args[i])
BxValuepassed through unchanged

The return value is always wrapped as BxValue::new_number(result as f64). For BIFs that need to return other types (strings, null, objects), write a manual BIF instead (see Manual BIF Signature).

What the Macro Generates

// Your function — unchanged:
pub fn add(a: f64, b: f64) -> f64 { a + b }

// Generated wrapper:
pub fn add_wrapper(
    vm: &mut dyn matchbox_vm::types::BxVM,
    args: &[matchbox_vm::types::BxValue],
) -> Result<matchbox_vm::types::BxValue, String> {
    if args.len() != 2 {
        return Err(format!("add requires 2 arguments, got {}", args.len()));
    }
    let a = args[0].as_number();
    let b = args[1].as_number();
    let result = add(a, b);
    Ok(matchbox_vm::types::BxValue::new_number(result as f64))
}

#[matchbox_class] — Expose a Rust struct as a BoxLang native object

Annotate a struct to auto-implement the BxNativeObject trait. The generated implementation delegates get_property, set_property, and call_method to a dispatch_method function that is generated by #[matchbox_methods].

Always pair #[matchbox_class] with a #[matchbox_methods] impl block.

Usage

use matchbox_vm::{matchbox_class, matchbox_methods, types::{BxValue, BxVM, BxNativeFunction, BxNativeObject}};
use std::collections::HashMap;
use std::rc::Rc;
use std::cell::RefCell;

#[matchbox_class]
#[derive(Debug)]
pub struct Counter {
    pub value: f64,
}

#[matchbox_methods]
impl Counter {
    pub fn increment(&mut self) -> f64 {
        self.value += 1.0;
        self.value
    }

    pub fn add(&mut self, n: f64) -> f64 {
        self.value += n;
        self.value
    }

    pub fn get(&self) -> f64 {
        self.value
    }
}

// Constructor — registered separately, not via #[matchbox_fn].
pub fn create_counter(vm: &mut dyn BxVM, args: &[BxValue]) -> Result<BxValue, String> {
    let initial = args.first().map(|v| v.as_number()).unwrap_or(0.0);
    let obj = Counter { value: initial };
    let id = vm.native_object_new(Rc::new(RefCell::new(obj)));
    Ok(BxValue::new_ptr(id))
}

pub fn register_classes() -> HashMap<String, BxNativeFunction> {
    let mut map = HashMap::new();
    // Key format: "<module_filename>.<ClassName>"
    // This file is native/counter.rs → module name is "counter"
    map.insert("counter.Counter".to_string(), create_counter as BxNativeFunction);
    map
}

BoxLang (instantiate with new rust:<module>.<Class>(...)):

c = new rust:counter.Counter(0)
c.increment()
c.add(9)
println(c.get())   // 10

#[matchbox_methods] — Generate a method dispatcher for a native class

Annotate an impl block to generate a dispatch_method function that routes BoxLang method calls to the correct Rust method by name (case-insensitive).

#[matchbox_fn] type-coercion rules apply to each method's parameters.

Usage

#[matchbox_methods]
impl MyStruct {
    pub fn process(&self, input: f64) -> f64 {
        input * self.factor
    }
}

BoxLang calls obj.process(42) and the dispatcher resolves it to MyStruct::process(&self, 42.0).

What the Macro Generates

impl MyStruct {
    fn dispatch_method(
        &mut self,
        vm: &mut dyn BxVM,
        name: &str,
        args: &[BxValue],
    ) -> Result<BxValue, String> {
        match name.to_lowercase().as_str() {
            "process" => {
                if args.len() != 1 { return Err(...); }
                let input = args[0].as_number();
                let result = self.process(input);
                Ok(BxValue::new_number(result as f64))
            }
            _ => Err(format!("Method {} not found", name)),
        }
    }
}

#[matchbox_module] — Module marker (no-op)

A no-op attribute that serves as documentation: apply it to the top-level mod or impl to indicate the file is a Native Fusion module. Currently passes through unchanged.

#[matchbox_module]
mod my_native_module { ... }

Registration Entry Points

Every .rs file in native/ may export two entry-point functions. MatchBox calls them both at startup.

register_bifs() — standalone functions

pub fn register_bifs() -> HashMap<String, BxNativeFunction> {
    let mut map = HashMap::new();
    map.insert("my_bif".to_string(), my_bif_wrapper as BxNativeFunction);
    map
}

BIF names must be unique across all files in native/.

register_classes() — native object constructors

pub fn register_classes() -> HashMap<String, BxNativeFunction> {
    let mut map = HashMap::new();
    // Key: "<module>.<ClassName>" where <module> is the filename without .rs
    map.insert("mymodule.MyClass".to_string(), create_my_class as BxNativeFunction);
    map
}

BoxLang uses the key to resolve new rust:mymodule.MyClass(...).


Manual BIF Signature

When you need full control (e.g. returning a string, null, or an object pointer), skip #[matchbox_fn] and write the low-level signature directly:

use matchbox_vm::types::{BxNativeFunction, BxValue, BxVM};
use std::collections::HashMap;

pub fn greet(vm: &mut dyn BxVM, args: &[BxValue]) -> Result<BxValue, String> {
    if args.is_empty() {
        return Err("greet: expected one argument".to_string());
    }
    let name = vm.to_string(args[0]);
    Ok(BxValue::new_string(format!("Hello, {}!", name)))
}

pub fn register_bifs() -> HashMap<String, BxNativeFunction> {
    let mut map = HashMap::new();
    map.insert("greet".to_string(), greet as BxNativeFunction);
    map
}

Project Layout

my_app/
├── app.bxs         ← BoxLang entry point
└── native/
    ├── math.rs     ← exports register_bifs()
    ├── counter.rs  ← exports register_bifs() + register_classes()
    └── crypto.rs   ← exports register_classes()

Multiple files are supported as long as BIF and class names are unique across all files.


Using External Rust Crates

Add a Cargo.toml to the native/ directory to pull in dependencies:

[package]
name    = "native"
version = "0.1.0"
edition = "2021"

[dependencies]
serde       = { version = "1", features = ["derive"] }
serde_json  = "1"
base64      = "0.22"

MatchBox compiles this as a standard Cargo project and links the output into the final binary.


Limitations

FeatureStatus
Native builds (--target native)✅
WASM builds (--target js / --target wasm)❌ Not supported
Multiple .rs files in native/✅
External Rust crates via native/Cargo.toml✅
Return types other than f64 from #[matchbox_fn]⚠️ Use manual BIF signature
Mutable self in #[matchbox_methods]✅ Works — dispatcher takes &mut self
Property access on native objects (obj.field)⚠️ Override get_property in BxNativeObject

See native-builds.md for the complete native build reference and differences-from-boxlang.md for the full list of features not available in MatchBox.

Edit this page Download Markdown Last updated Sep 30, 2026, 8:17:14 PM