Native Fusion reference

On this page

Native Fusion links Rust functions and objects into your BoxLang application. It supports every MatchBox target: native, browser/JavaScript, WASI and ESP32.

Rust code and dependencies must support the selected target. Browser builds cannot use desktop OS APIs, and ESP32 code must fit the device. WASI remains experimental.


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.
Native Fusion detected! Target: native. Building hybrid artifact...
...
Native Fusion binary produced: app

MatchBox detects native/, generates a temporary Cargo project that contains the .rs files, the bytecode and the VM, merges all BIF and class registrations, builds it with Cargo for the selected target, and writes the artifact.

You need Rust, the target's toolchain and the MatchBox source checkout used to build the CLI. Builds may run offline; cache dependencies first.


Macros

All interop macros are re-exported from the matchbox_vm crate, so 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_ptr(vm.string_new(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

Declare dependencies in native/Cargo.toml:

[dependencies]
semver = "1"

MatchBox merges them into the generated build. Relative path dependencies resolve against native/. Do not redeclare crates supplied by Fusion, such as matchbox_vm.

Reusable native functionality can also live in a module.


Limitations

  • All targets support Fusion, but Rust APIs and dependencies must match the target.
  • Use --target js for browsers and --target wasi for WASI hosts.
  • Return types other than f64 from #[matchbox_fn] need a manual BIF.
  • Native object property access requires 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 Oct 27, 2026, 1:45:34 PM Report an issue