mumuevent/
lib.rs

1// src/lib.rs
2//
3// MuMu "event" plugin: timers (timeout/interval) and per-tick loops.
4// - Host (native): exposes a dynamic entrypoint `Cargo_lock` so it can be
5//   loaded via extend("event").
6// - WASM (web): exposes `register_all(&mut Interpreter)` so the wrapper can
7//   statically bake the plugin in, just like math/array/string/regex.
8//
9// Provides dynamic functions:
10//   • event:timeout(ms, cb)   — one-shot callback after ms
11//   • event:interval(ms, cb)  — repeating callback every ms (returns handle)
12//   • event:stop(handle)      — stops an interval/loop by handle
13//   • event:loop(cb)          — run cb on every interpreter poll tick
14//
15// The plugin also installs a poller that drives timeouts/intervals/loops.
16
17use core_mumu::{Interpreter, Value, FunctionValue};
18use std::{
19    ffi::{c_void, CStr},
20    sync::{Arc, Mutex},
21};
22use std::time::Instant;
23
24mod timeout;
25mod interval;
26mod r#loop; // `loop` is a Rust keyword
27
28// Re-export bridges so users can call them directly if desired.
29pub use timeout::event_timeout_bridge;
30pub use interval::{event_interval_bridge, event_stop_bridge};
31pub use r#loop::event_loop_bridge;
32
33use lazy_static::lazy_static;
34
35lazy_static! {
36    // Timeout tasks are managed locally here; intervals/loops live in their modules.
37    static ref TIMEOUT_TASKS: Mutex<Vec<timeout::TimeoutTask>> = Mutex::new(vec![]);
38}
39
40/// Main poller – executes timeouts, intervals, and endless loops every tick.
41/// Returns how many tasks remain alive (timeouts pending + intervals + loops).
42fn poller_fn(interp: &mut Interpreter) -> usize {
43    let now = Instant::now();
44
45    // ── 1) Timeouts ──────────────────────────────────────────────────────
46    {
47        let mut tasks = TIMEOUT_TASKS.lock().unwrap();
48        let mut expired = Vec::new();
49        for (i, t) in tasks.iter().enumerate() {
50            if now >= t.deadline {
51                let _ = timeout::execute_callback(interp, &t.callback);
52                expired.push(i);
53            }
54        }
55        for &i in expired.iter().rev() {
56            tasks.remove(i);
57        }
58    }
59    let remaining_timeouts = TIMEOUT_TASKS.lock().unwrap().len();
60
61    // ── 2) Intervals ─────────────────────────────────────────────────────
62    {
63        let mut iv = interval::INTERVAL_TASKS.lock().unwrap();
64        for i in (0..iv.len()).rev() {
65            let task = &mut iv[i];
66            if now >= task.next_tick {
67                let _ = interval::execute_interval_callback(interp, &task.callback);
68                if !task.canceled {
69                    task.next_tick = now + task.period;
70                } else {
71                    iv.remove(i);
72                }
73            }
74        }
75    }
76    let remaining_intervals = interval::INTERVAL_TASKS.lock().unwrap().len();
77
78    // ── 3) Endless loops ─────────────────────────────────────────────────
79    let remaining_loops = r#loop::execute_loop_callbacks(interp);
80
81    remaining_timeouts + remaining_intervals + remaining_loops
82}
83
84/// Helper to be called from `timeout.rs` when a new timeout is created.
85pub fn push_timeout_task(task: timeout::TimeoutTask, _interp: &mut Interpreter) {
86    TIMEOUT_TASKS.lock().unwrap().push(task);
87}
88
89/// Register all event:* bridges and install the background poller.
90/// This is used by host builds (via the dynamic entrypoint) **and**
91/// by WASM builds where the wrapper calls this directly.
92pub fn register_all(interp: &mut Interpreter) {
93    // event:timeout
94    {
95        let f = Arc::new(Mutex::new(event_timeout_bridge));
96        interp.register_dynamic_function("event:timeout", f.clone());
97        interp.set_variable(
98            "event:timeout",
99            Value::Function(Box::new(FunctionValue::Named("event:timeout".into()))),
100        );
101    }
102
103    // event:interval
104    {
105        let f = Arc::new(Mutex::new(event_interval_bridge));
106        interp.register_dynamic_function("event:interval", f.clone());
107        interp.set_variable(
108            "event:interval",
109            Value::Function(Box::new(FunctionValue::Named("event:interval".into()))),
110        );
111    }
112
113    // event:stop
114    {
115        let f = Arc::new(Mutex::new(event_stop_bridge));
116        interp.register_dynamic_function("event:stop", f.clone());
117        interp.set_variable(
118            "event:stop",
119            Value::Function(Box::new(FunctionValue::Named("event:stop".into()))),
120        );
121    }
122
123    // event:loop
124    {
125        let f = Arc::new(Mutex::new(event_loop_bridge));
126        interp.register_dynamic_function("event:loop", f.clone());
127        interp.set_variable(
128            "event:loop",
129            Value::Function(Box::new(FunctionValue::Named("event:loop".into()))),
130        );
131    }
132
133    // Install the background poller (drives timeouts/intervals/loops).
134    interp.add_poller(Arc::new(Mutex::new(|intrp: &mut Interpreter| {
135        poller_fn(intrp)
136    })));
137}
138
139/* ──────────────────────────────────────────────────────────────────────────
140   Dynamic loader entrypoint (host/native only)
141   The core loader looks up the symbol **Cargo_lock** exactly.
142   We keep it out of wasm builds so the crate links cleanly to the web target.
143   ──────────────────────────────────────────────────────────────────────── */
144#[cfg(not(target_arch = "wasm32"))]
145#[no_mangle]
146pub unsafe extern "C" fn Cargo_lock(
147    interp_ptr: *mut c_void,
148    extra_str: *const c_void,
149) -> i32 {
150    if interp_ptr.is_null() {
151        return 1;
152    }
153    let interp = &mut *(interp_ptr as *mut Interpreter);
154
155    if interp.is_verbose() {
156        eprintln!("[event] Cargo_lock ⇒ initialising plugin");
157        if !extra_str.is_null() {
158            let c = CStr::from_ptr(extra_str as *const i8);
159            eprintln!("[event] extra arg = '{}'", c.to_string_lossy());
160        }
161    }
162
163    register_all(interp);
164
165    if interp.is_verbose() {
166        eprintln!("[event] Cargo_lock ⇒ registration complete");
167    }
168    0
169}