Skip to content

Flame Binder (Rust Integration)

Flame is designed from the ground up to be the ultimate embedding and metaprogramming language for Rust.

When Rust developers need a scripting layer for a game engine, a plugin system for a tool, or dynamic business rules, they frequently consider dynamic languages like Lua. However, Lua lacks static typing, compile-time safety, and a modern memory model. Flame gives you:

  • Static Type Safety: Catch script bugs at compile-time before they crash your host engine.
  • Metaprogramming & Annotations: First-class @Annotation support to inspect metadata, decorate functions, and structure extensible plugin architectures.
  • Zero-Friction Rust Interop: Seamless type translation via flamebinder::Value and native Rust closure registration.
  • Full Project Embedding: Embed single .fm scripts or entire multi-file packages and projects built with fmp build.

Add flamebinder to your Cargo.toml:

[dependencies]
flamebinder = "0.1.3"

You can customize the capabilities compiled into your embedded Flame runtime:

[dependencies]
# Default includes the standard library (networking, utilities, async timers)
flamebinder = { version = "0.1.3", features = ["std"] }
# Or enable full OS automation, platform hardware, and camera capabilities
# flamebinder = { version = "0.1.3", features = ["full"] }
Feature Flag Included Modules & Capabilities
std (default) Standard library utilities, collections, HTTP/WebSocket networking, and json
full Includes std plus OS automation, system hardware, and camera APIs

Games written in Rust (using Bevy, macroquad, or custom engines) can embed Flame to run mod scripts, custom weapon stats, enemy AI behaviors, and dialogue trees without recompiling the main game executable.

CLI tools, web servers, and desktop utilities can expose a clean plugin API where users write safe Flame scripts that invoke host functions.

Formulas and rules that change frequently (pricing models, access control policies, game balances) can be loaded dynamically from Flame files.


1. In-Process VM Script Execution (Binder)

Section titled “1. In-Process VM Script Execution (Binder)”

The Binder struct manages an in-process Flame VM execution engine (Runner).

You can load scripts directly from strings using .load_source() (ideal for embedded code, modding, and automated tests) or from external files using Binder::load():

use flamebinder::{Binder, Value, ValueExt};
fn main() -> Result<(), String> {
let mut binder = Binder::new();
// 1. Load Flame script code directly in-process
binder.load_source(r#"
export fn start_quest(hero: String, level: Int) -> Int {
println($"[Quest] Embarking {hero} on a Level {level} quest!");
return level * 100;
}
"#, "quest.fm")?;
// 2. Prepare typed arguments
let args = vec![Value::from("Hero"), Value::from(50)];
// 3. Call the exported Flame function
let result = binder.call("start_quest", args)?;
// 4. Safely extract the return value:
// Option A: Using ValueExt (.to_int() -> Option<i64>)
if let Some(reward) = result.to_int() {
println!("Quest reward: {} XP", reward);
}
// Option B: Using Value (.as_int() -> Result<i64, String>)
let reward = result.as_int()?;
println!("Total XP: {}", reward);
Ok(())
}

2. Exposing Host Rust Functions to Flame (register_fn)

Section titled “2. Exposing Host Rust Functions to Flame (register_fn)”

Host applications can register native Rust closures that scripts can call directly:

use flamebinder::{Binder, Value, ValueExt};
fn main() -> Result<(), String> {
let mut binder = Binder::new();
// Register a native Rust function into the Flame environment
binder.register_fn("spawn_monster", |args| {
// Ergonomic Option-based extraction via ValueExt:
let monster_type = args.get(0).and_then(|v| v.to_str()).unwrap_or("Goblin");
let level = args.get(1).and_then(|v| v.to_int()).unwrap_or(1);
println!("⚡ Rust Engine: Spawning {} at level {}", monster_type, level);
// Return a result value back to Flame
Ok(Value::from(1001)) // e.g. entity ID
});
// Execute Flame code calling the native host function
binder.load_source(r#"
export fn trigger_ambush() -> Int {
let entity_id = spawn_monster("Dragon", 50);
return entity_id;
}
"#, "mod.fm")?;
let entity_id = binder.call("trigger_ambush", vec![])?;
println!("Spawned entity ID: {:?}", entity_id.to_int());
Ok(())
}

For high-assurance systems where invalid types must fail immediately with clear diagnostic errors, use the Result-based accessors (.as_str(), .as_int(), .as_float(), .as_bool()):

binder.register_fn("calculate_damage", |args| {
// Returns Err immediately if the argument is missing or has the wrong type
let base_dmg = args.get(0).ok_or("missing base_damage (arg 0)")?.as_int()?;
let multiplier = args.get(1).ok_or("missing multiplier (arg 1)")?.as_float()?;
let is_critical = args.get(2).map_or(Ok(false), |v| v.as_bool())?;
let total = (base_dmg as f64) * multiplier * if is_critical { 2.0 } else { 1.0 };
Ok(Value::from(total))
});

3. Evaluating Dynamic Expressions (eval) & Globals

Section titled “3. Evaluating Dynamic Expressions (eval) & Globals”

You can inject global state from host Rust code and dynamically evaluate Flame expressions:

use flamebinder::{Binder, Value, ValueExt};
fn main() -> Result<(), String> {
let mut binder = Binder::new();
// Inject state from host Rust code
binder.set_global("player_level", Value::from(12));
binder.set_global("multiplier", Value::from(2.5));
// Evaluate dynamic Flame expressions in-process
let xp = binder.eval("player_level * 100 * multiplier")?;
// Extract with ValueExt (.to_float()) or Value (.as_float()?)
println!("Total XP: {}", xp.to_float().unwrap_or(0.0)); // 3000.0
// Read global variables back out of the environment
if let Some(level) = binder.get_global("player_level") {
println!("Player level: {:?}", level.to_int());
}
Ok(())
}

4. Embedding Full Flame Projects (load_project)

Section titled “4. Embedding Full Flame Projects (load_project)”

Instead of just standalone files, you can embed an entire multi-file project with its packages, modules, and flame.toml:

use flamebinder::Binder;
fn main() -> Result<(), String> {
// Automatically resolves flame.toml, loads .flame/pkg packages, and compiles src/
let mut binder = Binder::load_project("./game_scripts")?;
// Call functions across the loaded project
let result = binder.call("init_game_world", vec![])?;
println!("World initialized: {:?}", result);
Ok(())
}

5. Running with Compiled Runtimes (CompiledRuntime)

Section titled “5. Running with Compiled Runtimes (CompiledRuntime)”

When an application is built using fmp build, you can invoke Flame scripts using that compiled native binary:

use flamebinder::CompiledRuntime;
fn main() -> Result<(), String> {
// Resolves target/release/<app> from the project directory
let runtime = CompiledRuntime::from_project("./game_project", true)?;
// Run dynamic mod scripts through the compiled runtime binary
let output = runtime.run_script_stdout("mods/custom_boss.fm", &[])?;
println!("Script stdout:\n{}", output);
Ok(())
}

flamebinder re-exports Value and provides two complementary type-safe extraction patterns:

  1. Option-Based Pattern via ValueExt: Ideal for .unwrap_or(), fallback defaults, and if let Some(...) pattern matching.
  2. Result-Based Pattern via Value: Ideal for idiomatic Rust error propagation with ?.

Value implements standard From conversions for common Rust types:

use flamebinder::Value;
use std::collections::HashMap;
// Primitive scalar conversions:
let v_int: Value = 42i64.into(); // or Value::from(42)
let v_float: Value = 3.14f64.into(); // or Value::from(3.14)
let v_bool: Value = true.into(); // or Value::from(true)
let v_str: Value = "Flame".into(); // or Value::from("Flame")
// Complex data structures:
let v_list: Value = vec![Value::from(1), Value::from(2)].into();
let mut map = HashMap::new();
map.insert("name".to_string(), Value::from("Excalibur"));
map.insert("attack".to_string(), Value::from(85));
let v_formula: Value = map.into();
// Raw binary byte buffer:
let v_bytes: Value = Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]);
use flamebinder::{Value, ValueExt};
// --- Pattern A: Option-Based (via ValueExt) ---
let opt_int: Option<i64> = v_int.to_int(); // Some(42)
let opt_str: Option<&str> = v_str.to_str(); // Some("Flame")
let opt_bool: Option<bool> = v_bool.to_bool(); // Some(true)
let opt_float: Option<f64> = v_float.to_float(); // Some(3.14)
let opt_list: Option<&[Value]> = v_list.as_list();
let opt_map: Option<&HashMap<String, Value>> = v_formula.as_formula();
// --- Pattern B: Result-Based (via Value, with `?`) ---
let num: i64 = v_int.as_int()?; // 42
let text: &str = v_str.as_str()?; // "Flame"
let flag: bool = v_bool.as_bool()?; // true
let pi: f64 = v_float.as_float()?; // 3.14
let bytes: Vec<u8> = v_bytes.as_bytes()?; // vec![0xDE, 0xAD, 0xBE, 0xEF]

Here is a full, self-contained example you can paste into your src/main.rs:

use flamebinder::{Binder, Value, ValueExt};
use std::collections::HashMap;
fn main() -> Result<(), String> {
let mut binder = Binder::new();
// 1. Expose host game engine API
binder.register_fn("log_event", |args| {
let msg = args.get(0).and_then(|v| v.to_str()).unwrap_or("");
println!("[HOST] {}", msg);
Ok(Value::Nil)
});
// 2. Load script
binder.load_source(r#"
export fn generate_loot(player_level: Int) -> formula {
log_event($"Generating loot for level {player_level}");
return formula {
item: "Shadow Blade",
power: player_level * 15,
sockets: [1, 2, 3]
};
}
"#, "loot.fm")?;
// 3. Execute function
let loot = binder.call("generate_loot", vec![Value::from(10)])?;
// 4. Inspect formula fields
if let Some(fields) = loot.as_formula() {
let item_name = fields.get("item").and_then(|v| v.to_str()).unwrap_or("Unknown");
let power = fields.get("power").and_then(|v| v.to_int()).unwrap_or(0);
println!("Acquired: {} (Power: {})", item_name, power);
if let Some(sockets) = fields.get("sockets").and_then(|v| v.as_list()) {
println!("Sockets available: {}", sockets.len());
}
}
Ok(())
}