use mlua::{Function, Lua, Result, Value};
use std::collections::HashSet;
#[derive(Clone, Debug)]
pub enum LuaPortable {
Nil,
Bool(bool),
Int(i64),
Float(f64),
String(Vec<u8>),
Table(Vec<(LuaPortable, LuaPortable)>),
Function(Vec<u8>),
}
#[derive(Debug)]
pub struct PortableError {
pub message: String,
pub path: Vec<String>,
}
impl PortableError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
path: Vec::new(),
}
}
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.path.insert(0, context.into());
self
}
pub fn format_message(&self) -> String {
if self.path.is_empty() {
self.message.clone()
} else {
format!("at {}: {}", self.path.join("."), self.message)
}
}
}
impl std::fmt::Display for PortableError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.format_message())
}
}
impl From<PortableError> for mlua::Error {
fn from(err: PortableError) -> Self {
mlua::Error::RuntimeError(err.format_message())
}
}
pub struct SerializeContext {
seen_tables: HashSet<usize>,
seen_functions: HashSet<usize>,
path: Vec<String>,
max_depth: usize,
current_depth: usize,
}
impl SerializeContext {
pub fn new() -> Self {
Self {
seen_tables: HashSet::new(),
seen_functions: HashSet::new(),
path: Vec::new(),
max_depth: 100,
current_depth: 0,
}
}
fn push_path(&mut self, segment: impl Into<String>) {
self.path.push(segment.into());
self.current_depth += 1;
}
fn pop_path(&mut self) {
self.path.pop();
self.current_depth = self.current_depth.saturating_sub(1);
}
fn check_depth(&self) -> std::result::Result<(), PortableError> {
if self.current_depth > self.max_depth {
Err(PortableError {
message: format!(
"Maximum recursion depth ({}) exceeded. \
This may indicate circular references or deeply nested structures.",
self.max_depth
),
path: self.path.clone(),
})
} else {
Ok(())
}
}
fn check_table_cycle(&mut self, ptr: usize) -> std::result::Result<(), PortableError> {
if !self.seen_tables.insert(ptr) {
Err(PortableError {
message: "Circular reference detected in table. \
Tables that reference themselves cannot be passed to parallel functions."
.to_string(),
path: self.path.clone(),
})
} else {
Ok(())
}
}
fn remove_table(&mut self, ptr: usize) {
self.seen_tables.remove(&ptr);
}
fn check_function_cycle(&mut self, ptr: usize) -> std::result::Result<(), PortableError> {
if !self.seen_functions.insert(ptr) {
Err(PortableError {
message: "Circular reference detected in function. \
Functions that reference themselves cannot be passed to parallel functions."
.to_string(),
path: self.path.clone(),
})
} else {
Ok(())
}
}
fn remove_function(&mut self, ptr: usize) {
self.seen_functions.remove(&ptr);
}
fn current_path(&self) -> String {
if self.path.is_empty() {
"<root>".to_string()
} else {
self.path.join(".")
}
}
}
impl Default for SerializeContext {
fn default() -> Self {
Self::new()
}
}
impl LuaPortable {
pub fn from_lua(value: &Value, lua: &Lua) -> Result<Self> {
let mut ctx = SerializeContext::new();
Self::from_lua_with_context(value, lua, &mut ctx).map_err(|e| e.into())
}
#[allow(clippy::only_used_in_recursion)]
fn from_lua_with_context(
value: &Value,
lua: &Lua,
ctx: &mut SerializeContext,
) -> std::result::Result<Self, PortableError> {
ctx.check_depth()?;
match value {
Value::Nil => Ok(LuaPortable::Nil),
Value::Boolean(b) => Ok(LuaPortable::Bool(*b)),
Value::Integer(i) => Ok(LuaPortable::Int(*i)),
Value::Number(n) => Ok(LuaPortable::Float(*n)),
Value::String(s) => Ok(LuaPortable::String(s.as_bytes().to_vec())),
Value::Table(t) => {
let ptr = t.to_pointer() as usize;
ctx.check_table_cycle(ptr)?;
let mut entries = Vec::new();
for pair in t.pairs::<Value, Value>() {
let (k, v) = pair.map_err(|e| PortableError::new(e.to_string()))?;
let key_repr = value_short_repr(&k);
ctx.push_path(format!("[{}]", key_repr));
let portable_key = Self::from_lua_with_context(&k, lua, ctx)
.map_err(|e| e.with_context(format!("in table key [{}]", key_repr)))?;
let portable_val = Self::from_lua_with_context(&v, lua, ctx)
.map_err(|e| e.with_context(format!("in table value at [{}]", key_repr)))?;
ctx.pop_path();
entries.push((portable_key, portable_val));
}
ctx.remove_table(ptr);
Ok(LuaPortable::Table(entries))
}
Value::Function(f) => {
let ptr = f.to_pointer() as usize;
ctx.check_function_cycle(ptr)?;
let bytecode = f.dump(true);
ctx.remove_function(ptr);
Ok(LuaPortable::Function(bytecode))
}
Value::UserData(ud) => {
let type_name = ud
.metatable()
.ok()
.and_then(|mt| mt.get::<String>("__name").ok())
.unwrap_or_else(|| "unknown".to_string());
Err(PortableError::new(format!(
"UserData of type '{}' cannot be passed to parallel functions. \
UserData represents Rust objects that cannot be safely shared across threads. \
Consider extracting the data you need into a plain Lua table first.",
type_name
)))
}
Value::Thread(_) => Err(PortableError::new(
"Lua threads (coroutines) cannot be passed to parallel functions. \
Coroutines have execution state that cannot be serialized.",
)),
Value::LightUserData(_) => Err(PortableError::new(
"Light userdata (raw pointers) cannot be passed to parallel functions. \
Pointers are not valid across thread boundaries.",
)),
Value::Error(e) => Err(PortableError::new(format!(
"Lua error values cannot be passed to parallel functions: {}",
e
))),
_ => Err(PortableError::new(format!(
"Unsupported Lua value type at {}. \
Only nil, boolean, number, string, table, and function values are supported.",
ctx.current_path()
))),
}
}
pub fn to_lua(&self, lua: &Lua) -> Result<Value> {
match self {
LuaPortable::Nil => Ok(Value::Nil),
LuaPortable::Bool(b) => Ok(Value::Boolean(*b)),
LuaPortable::Int(i) => Ok(Value::Integer(*i)),
LuaPortable::Float(n) => Ok(Value::Number(*n)),
LuaPortable::String(s) => Ok(Value::String(lua.create_string(s)?)),
LuaPortable::Table(entries) => {
let t = lua.create_table()?;
for (k, v) in entries {
t.set(k.to_lua(lua)?, v.to_lua(lua)?)?;
}
Ok(Value::Table(t))
}
LuaPortable::Function(bytecode) => {
let func: Function = lua.load(bytecode).into_function()?;
Ok(Value::Function(func))
}
}
}
}
fn value_short_repr(value: &Value) -> String {
match value {
Value::Nil => "nil".to_string(),
Value::Boolean(b) => b.to_string(),
Value::Integer(i) => i.to_string(),
Value::Number(n) => format!("{:.4}", n),
Value::String(s) => {
let bytes = s.as_bytes();
let s_str = std::str::from_utf8(&bytes).unwrap_or("<invalid utf8>");
if s_str.len() > 20 {
format!("\"{}...\"", &s_str[..17])
} else {
format!("\"{}\"", s_str)
}
}
Value::Table(_) => "<table>".to_string(),
Value::Function(_) => "<function>".to_string(),
Value::Thread(_) => "<thread>".to_string(),
Value::UserData(_) => "<userdata>".to_string(),
Value::LightUserData(_) => "<lightuserdata>".to_string(),
Value::Error(e) => format!("<error: {}>", e),
_ => "<unknown>".to_string(),
}
}