Native Fusion
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 — 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
- 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.
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 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_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
| Feature | Status |
|---|---|
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.