Skip to main content

scope_world/
scope_world.rs

1//! Lending a `&mut World` to Lua with `Lua::scope`, and exposing live
2//! sub-references to its entities with `AnyUserData::delegate`.
3//!
4//! Run with:
5//!
6//! ```text
7//! cargo run -p lua-rs-runtime --example scope_world
8//! ```
9//!
10//! This is the shape a game engine (e.g. Bevy) uses to drive scripts: the
11//! scheduler owns the `World` and lends it to a system for one tick. The
12//! script gets to mutate it through Lua for the duration of the call, then
13//! the borrow goes back to Rust. If a script squirrels a handle away and
14//! tries to use it after the tick, the call fails cleanly instead of
15//! touching a dangling pointer.
16
17use lua_rs_runtime::{AnyUserData, Lua, Result, UserData, UserDataMethods};
18
19/// A component on an entity.
20#[derive(Default)]
21struct Position {
22    x: f64,
23    y: f64,
24}
25
26impl UserData for Position {
27    fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
28        m.add_field_method_get("x", |_, this| Ok(this.x));
29        m.add_field_method_get("y", |_, this| Ok(this.y));
30        m.add_field_method_set("x", |_, this, v: f64| {
31            this.x = v;
32            Ok(())
33        });
34        m.add_field_method_set("y", |_, this, v: f64| {
35            this.y = v;
36            Ok(())
37        });
38    }
39}
40
41/// The thing the host owns and lends to scripts for one tick.
42#[derive(Default)]
43struct World {
44    entities: Vec<Position>,
45}
46
47impl UserData for World {
48    fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
49        m.add_method_mut("spawn", |_, this, ()| {
50            this.entities.push(Position::default());
51            Ok((this.entities.len() - 1) as i64)
52        });
53        m.add_method("count", |_, this, ()| Ok(this.entities.len() as i64));
54
55        // `world:position(i)` returns a sub-userdata holding a live `&mut
56        // Position`, re-borrowed from the World on every field access. No
57        // clone, no write-back: the script mutates the real component.
58        m.add_function("position", |lua, (this, idx): (AnyUserData, i64)| {
59            let i = idx as usize;
60            this.delegate::<World, Position, _>(lua, move |w| &mut w.entities[i])
61        });
62    }
63}
64
65fn main() -> Result<()> {
66    let lua = Lua::new();
67    let mut world = World::default();
68
69    let count: i64 = lua.scope(|s| {
70        let world_ud = s.create_userdata_ref_mut(&lua, &mut world)?;
71        lua.globals().set("world", &world_ud)?;
72
73        lua.load(
74            r#"
75            local a = world:spawn()
76            local b = world:spawn()
77
78            -- Direct mutation through a live sub-reference.
79            local pa = world:position(a)
80            pa.x, pa.y = 10, 20
81
82            local pb = world:position(b)
83            pb.x = pa.x + 5      -- reading pa here re-borrows; both are short-lived
84
85            -- Stash one to prove it dies with the scope.
86            escaped = world:position(a)
87
88            return world:count()
89        "#,
90        )
91        .eval()
92    })?;
93
94    println!("spawned {count} entities");
95    // The mutations are visible to Rust after the scope returns.
96    for (i, e) in world.entities.iter().enumerate() {
97        println!("entity {i} = ({}, {})", e.x, e.y);
98    }
99
100    // The handle the script stashed on `escaped` is now invalid. Reading a
101    // field raises a Lua error rather than touching the released `&mut World`.
102    // We surface the message through Lua's own `pcall` + `tostring`, since the
103    // error payload is a Lua value.
104    let (ok, msg): (bool, String) = lua
105        .load("local ok, e = pcall(function() return escaped.x end); return ok, tostring(e)")
106        .eval()?;
107    assert!(!ok, "stashed handle should be unusable after the scope");
108    println!("post-scope use of a stashed handle -> {msg}");
109
110    Ok(())
111}