Native Fusion reference
On this page
- How It Works
- Macros
- #[matchbox_fn] : Expose a Rust function as a BoxLang BIF
- #[matchbox_class] : Expose a Rust struct as a BoxLang native object
- #[matchbox_methods] : Generate a method dispatcher for a native class
- #[matchbox_module] : Module marker (no-op)
- Registration Entry Points
- register_bifs() : standalone functions
- register_classes() : native object constructors
- Manual BIF Signature
- Project Layout
- Using External Rust Crates
- Limitations
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
- Create a
native/directory alongside your.bxsentry script. - Write one or more
.rsfiles insidenative/. - Each
.rsfile exports aregister_bifs()and/orregister_classes()function. - 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 type | VM conversion applied by the macro |
|---|---|
f64 | args[i].as_number() |
i32 | args[i].as_int() |
bool | args[i].as_bool() |
String | vm.to_string(args[i]) |
BxValue | passed 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 jsfor browsers and--target wasifor WASI hosts. - Return types other than
f64from#[matchbox_fn]need a manual BIF. - Native object property access requires
get_propertyinBxNativeObject.
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.