Expose Registrations as a userdata module
Assisted-by: GLM-5.3 via pi
diff --git a/src/parallax.rs b/src/parallax.rs
index bf9e60e..828b796 100644
--- a/src/parallax.rs
+++ b/src/parallax.rs
@@ -1,15 +1,14 @@
use camino::Utf8Path;
use camino::Utf8PathBuf;
use std::mem;
-use std::sync::Arc;
-use std::sync::Mutex;
-use std::sync::MutexGuard;
+use mlua::AnyUserData;
use mlua::ErrorContext as _;
use mlua::Function;
use mlua::Lua;
use mlua::LuaSerdeExt as _;
use mlua::Table;
+use mlua::UserData;
use mlua::Value;
use crate::Error;
@@ -19,8 +18,10 @@ use crate::spec::Setup;
const FENNEL_LUA: &str = include_str!("../vendor/fennel.lua");
-/// What a config has registered so far, filled in by the module's
-/// registration functions as the config runs.
+/// What a config has registered so far, filled in as the config
+/// runs. It lives Lua-side as the `parallax` module itself: configs
+/// call its methods, and Rust borrows it back out once the config
+/// finishes.
#[derive(Default)]
struct Registrations {
setup: Option<Setup>,
@@ -28,6 +29,60 @@ struct Registrations {
flows: Vec<Flow>,
}
+impl Registrations {
+ fn setup(&mut self, lua: &Lua, table: Value) -> Result<(), mlua::Error> {
+ let Value::Table(table) = table else {
+ return Err(error_at(lua, "setup takes a table"));
+ };
+ if self.setup.is_some() {
+ return Err(error_at(lua, "setup must be called exactly once"));
+ }
+ let setup: Setup = lua
+ .from_value(Value::Table(table))
+ .map_err(|error| error_at(lua, &error_message(&error)))?;
+ if setup.viewports.is_empty() {
+ return Err(error_at(lua, "viewports must not be empty"));
+ }
+ self.setup = Some(setup);
+ Ok(())
+ }
+
+ fn server(&mut self, lua: &Lua, call: Value) -> Result<(), mlua::Error> {
+ let Value::Function(call) = call else {
+ return Err(error_at(lua, "server takes a function"));
+ };
+ if self.server.is_some() {
+ return Err(error_at(lua, "server must be called exactly once"));
+ }
+ self.server = Some(call);
+ Ok(())
+ }
+
+ fn flow(&mut self, lua: &Lua, name: Value, call: Value) -> Result<(), mlua::Error> {
+ let name = match name {
+ Value::String(name) => match name.to_str() {
+ Ok(name) => NonEmptyString::new(name.to_owned()),
+ Err(_) => None,
+ },
+ _ => None,
+ };
+ let call = match call {
+ Value::Function(call) => Some(call),
+ _ => None,
+ };
+ let (Some(name), Some(call)) = (name, call) else {
+ return Err(error_at(lua, "flow takes a non-empty name and a function"));
+ };
+ self.flows.push(Flow { name, call });
+ Ok(())
+ }
+}
+
+/// Storage only. The module exposes plain functions, not methods,
+/// because configs destructure it and call them bare — `(setup {...})`
+/// — with no self to bind a userdata method to.
+impl UserData for Registrations {}
+
/// A loaded parallax: the executable form of a config. The server
/// function and flows are owned handles into the embedded Lua state,
/// which stays alive in this struct; fields are ordered so the handles
@@ -55,15 +110,13 @@ impl Parallax {
// safe default omits. Configs are the user's own trusted files,
// so the full stdlib is fine here.
let lua = unsafe { Lua::unsafe_new() };
- let stash = Arc::new(Mutex::new(Registrations::default()));
let filename = path.as_str().to_owned();
let Registrations {
setup,
server,
flows,
- } = run_config(&lua, &source, &filename, &stash)
- .with_context(|_| format!("loading {path}"))?;
+ } = run_config(&lua, &source, &filename).with_context(|_| format!("loading {path}"))?;
let setup = setup.ok_or_else(|| Error::ConfigRegistration {
path: path.to_owned(),
@@ -107,80 +160,43 @@ fn default_name(path: &Utf8Path) -> String {
}
/// Builds the parallax module in Rust and registers it as the
-/// `parallax` entry in package.preload, so configs can require it.
-fn install_module(lua: &Lua, stash: &Arc<Mutex<Registrations>>) -> Result<(), mlua::Error> {
- let module = lua.create_table()?;
- module.set("setup", setup_fn(lua, stash.clone())?)?;
- module.set("server", server_fn(lua, stash.clone())?)?;
- module.set("flow", flow_fn(lua, stash.clone())?)?;
+/// Builds the `parallax` module in package.preload — a table of
+/// registration functions backed by the Registrations userdata — and
+/// returns the handle so the caller can borrow the registrations back
+/// out after the config runs. The functions raise errors while the
+/// config's stack frame is alive, so failures point at the call site.
+fn install_module(lua: &Lua) -> Result<AnyUserData, mlua::Error> {
+ let module = lua.create_userdata(Registrations::default())?;
+
+ let setup = {
+ let module = module.clone();
+ lua.create_function(move |lua, table: Value| {
+ module.borrow_mut::<Registrations>()?.setup(lua, table)
+ })?
+ };
+ let server = {
+ let module = module.clone();
+ lua.create_function(move |lua, call: Value| {
+ module.borrow_mut::<Registrations>()?.server(lua, call)
+ })?
+ };
+ let flow = {
+ let module = module.clone();
+ lua.create_function(move |lua, (name, call): (Value, Value)| {
+ module.borrow_mut::<Registrations>()?.flow(lua, name, call)
+ })?
+ };
+ let table = lua.create_table()?;
+ table.set("setup", setup)?;
+ table.set("server", server)?;
+ table.set("flow", flow)?;
+
+ let lookup = lua.create_function(move |_, _name: String| Ok(table.clone()))?;
let package: Table = lua.globals().get("package")?;
let preload: Table = package.get("preload")?;
- let lookup = lua.create_function(move |_, _name: String| Ok(module.clone()))?;
preload.set("parallax", lookup)?;
- Ok(())
-}
-
-/// The registration functions raise errors while the config's stack
-/// frame is alive, so failures point at the registration call site.
-fn setup_fn(lua: &Lua, stash: Arc<Mutex<Registrations>>) -> Result<Function, mlua::Error> {
- lua.create_function(move |lua, table: Value| {
- let Value::Table(table) = table else {
- return Err(error_at(lua, "setup takes a table"));
- };
- if lock(&stash)?.setup.is_some() {
- return Err(error_at(lua, "setup must be called exactly once"));
- }
- let setup: Setup = lua
- .from_value(Value::Table(table))
- .map_err(|error| error_at(lua, &error_message(&error)))?;
- if setup.viewports.is_empty() {
- return Err(error_at(lua, "viewports must not be empty"));
- }
- lock(&stash)?.setup = Some(setup);
- Ok(())
- })
-}
-
-fn server_fn(lua: &Lua, stash: Arc<Mutex<Registrations>>) -> Result<Function, mlua::Error> {
- lua.create_function(move |lua, call: Value| {
- let Value::Function(call) = call else {
- return Err(error_at(lua, "server takes a function"));
- };
- let mut registrations = lock(&stash)?;
- if registrations.server.is_some() {
- return Err(error_at(lua, "server must be called exactly once"));
- }
- registrations.server = Some(call);
- Ok(())
- })
-}
-
-fn flow_fn(lua: &Lua, stash: Arc<Mutex<Registrations>>) -> Result<Function, mlua::Error> {
- lua.create_function(move |lua, (name, call): (Value, Value)| {
- let name = match name {
- Value::String(name) => match name.to_str() {
- Ok(name) => NonEmptyString::new(name.to_owned()),
- Err(_) => None,
- },
- _ => None,
- };
- let call = match call {
- Value::Function(call) => Some(call),
- _ => None,
- };
- let (Some(name), Some(call)) = (name, call) else {
- return Err(error_at(lua, "flow takes a non-empty name and a function"));
- };
- lock(&stash)?.flows.push(Flow { name, call });
- Ok(())
- })
-}
-
-fn lock(stash: &Arc<Mutex<Registrations>>) -> Result<MutexGuard<'_, Registrations>, mlua::Error> {
- stash
- .lock()
- .map_err(|_| mlua::Error::runtime("the registration state was poisoned"))
+ Ok(module)
}
fn error_at(lua: &Lua, message: &str) -> mlua::Error {
@@ -219,14 +235,9 @@ fn caller_position(lua: &Lua) -> Option<String> {
/// Loads the Fennel compiler, runs the config, and returns everything
/// it registered. Every failure surfaces as an mlua error; the caller
/// annotates it with the config's path.
-fn run_config(
- lua: &Lua,
- source: &str,
- filename: &str,
- stash: &Arc<Mutex<Registrations>>,
-) -> Result<Registrations, mlua::Error> {
+fn run_config(lua: &Lua, source: &str, filename: &str) -> Result<Registrations, mlua::Error> {
let fennel: Table = lua.load(FENNEL_LUA).set_name("fennel.lua").eval()?;
- install_module(lua, stash)?;
+ let module = install_module(lua)?;
// Configs end with a registration call, which compiles to a tail
// call; tail elimination destroys the config's stack frame, hiding
@@ -248,7 +259,9 @@ fn run_config(
.set_name(format!("@{filename}"))
.eval::<()>()?;
- Ok(mem::take(&mut *lock(stash)?))
+ // A failed borrow means a config re-entered registration while it
+ // was already being mutated — surface it instead of hanging.
+ Ok(mem::take(&mut *module.borrow_mut::<Registrations>()?))
}
// The index arithmetic in this helper is safe because `index` comes from