Skip to content

Commit 7f3ec63

Browse files
committed
Support __todebugstring for pretty userdata debug output
Close #681
1 parent f19c6aa commit 7f3ec63

File tree

3 files changed

+61
-13
lines changed

3 files changed

+61
-13
lines changed

src/userdata.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ pub enum MetaMethod {
123123
///
124124
/// This is not an operator, but will be called by methods such as `tostring` and `print`.
125125
ToString,
126+
/// The `__todebugstring` metamethod for debug purposes.
127+
///
128+
/// This is an mlua-specific metamethod that can be used to provide debug representation for
129+
/// userdata.
130+
ToDebugString,
126131
/// The `__pairs` metamethod.
127132
///
128133
/// This is not an operator, but it will be called by the built-in `pairs` function.
@@ -232,6 +237,7 @@ impl MetaMethod {
232237
MetaMethod::NewIndex => "__newindex",
233238
MetaMethod::Call => "__call",
234239
MetaMethod::ToString => "__tostring",
240+
MetaMethod::ToDebugString => "__todebugstring",
235241

236242
#[cfg(any(
237243
feature = "lua55",

src/value.rs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ impl Value {
151151
/// This might invoke the `__tostring` metamethod for non-primitive types (eg. tables,
152152
/// functions).
153153
pub fn to_string(&self) -> Result<String> {
154-
unsafe fn invoke_to_string(vref: &ValueRef) -> Result<String> {
154+
unsafe fn invoke_tostring(vref: &ValueRef) -> Result<String> {
155155
let lua = vref.lua.lock();
156156
let state = lua.state();
157157
let _guard = StackGuard::new(state);
@@ -178,9 +178,9 @@ impl Value {
178178
| Value::Function(Function(vref))
179179
| Value::Thread(Thread(vref, ..))
180180
| Value::UserData(AnyUserData(vref))
181-
| Value::Other(vref) => unsafe { invoke_to_string(vref) },
181+
| Value::Other(vref) => unsafe { invoke_tostring(vref) },
182182
#[cfg(feature = "luau")]
183-
Value::Buffer(crate::Buffer(vref)) => unsafe { invoke_to_string(vref) },
183+
Value::Buffer(crate::Buffer(vref)) => unsafe { invoke_tostring(vref) },
184184
Value::Error(err) => Ok(err.to_string()),
185185
}
186186
}
@@ -545,6 +545,24 @@ impl Value {
545545
ident: usize,
546546
visited: &mut HashSet<*const c_void>,
547547
) -> fmt::Result {
548+
unsafe fn invoke_tostring_dbg(vref: &ValueRef) -> Result<Option<String>> {
549+
let lua = vref.lua.lock();
550+
let state = lua.state();
551+
let _guard = StackGuard::new(state);
552+
check_stack(state, 3)?;
553+
554+
lua.push_ref(vref);
555+
protect_lua!(state, 1, 1, fn(state) {
556+
// Try `__todebugstring` metamethod first, then `__tostring`
557+
if ffi::luaL_callmeta(state, -1, cstr!("__todebugstring")) == 0 {
558+
if ffi::luaL_callmeta(state, -1, cstr!("__tostring")) == 0 {
559+
ffi::lua_pushnil(state);
560+
}
561+
}
562+
})?;
563+
Ok(lua.pop_value().as_string().map(|s| s.to_string_lossy()))
564+
}
565+
548566
match self {
549567
Value::Nil => write!(fmt, "nil"),
550568
Value::Boolean(b) => write!(fmt, "{b}"),
@@ -561,15 +579,17 @@ impl Value {
561579
t @ Value::Table(_) => write!(fmt, "table: {:?}", t.to_pointer()),
562580
f @ Value::Function(_) => write!(fmt, "function: {:?}", f.to_pointer()),
563581
t @ Value::Thread(_) => write!(fmt, "thread: {:?}", t.to_pointer()),
564-
u @ Value::UserData(ud) => {
565-
// Try `__name/__type` first then `__tostring`
566-
let name = ud.type_name().ok().flatten();
567-
let s = name
568-
.map(|name| format!("{name}: {:?}", u.to_pointer()))
569-
.or_else(|| u.to_string().ok())
570-
.unwrap_or_else(|| format!("userdata: {:?}", u.to_pointer()));
571-
write!(fmt, "{s}")
572-
}
582+
u @ Value::UserData(ud) => unsafe {
583+
// Try converting to a (debug) string first, with fallback to `__name/__type`
584+
match invoke_tostring_dbg(&ud.0) {
585+
Ok(Some(s)) => write!(fmt, "{s}"),
586+
_ => {
587+
let name = ud.type_name().ok().flatten();
588+
let name = name.as_deref().unwrap_or("userdata");
589+
write!(fmt, "{name}: {:?}", u.to_pointer())
590+
}
591+
}
592+
},
573593
#[cfg(feature = "luau")]
574594
buf @ Value::Buffer(_) => write!(fmt, "buffer: {:?}", buf.to_pointer()),
575595
Value::Error(e) if recursive => write!(fmt, "{e:?}"),

tests/value.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use std::collections::HashMap;
22
use std::os::raw::c_void;
33
use std::ptr;
44

5-
use mlua::{Error, LightUserData, Lua, MultiValue, Result, UserData, UserDataMethods, Value};
5+
use mlua::{
6+
Error, LightUserData, Lua, MultiValue, Result, UserData, UserDataMethods, UserDataRegistry, Value,
7+
};
68

79
#[test]
810
fn test_value_eq() -> Result<()> {
@@ -218,6 +220,26 @@ fn test_debug_format() -> Result<()> {
218220
.map(Value::UserData)?;
219221
assert!(format!("{ud:#?}").starts_with("HashMap<i32, String>:"));
220222

223+
struct ToDebugUserData;
224+
impl UserData for ToDebugUserData {
225+
fn register(registry: &mut UserDataRegistry<Self>) {
226+
registry.add_meta_method("__tostring", |_, _, ()| Ok("regular-string"));
227+
registry.add_meta_method("__todebugstring", |_, _, ()| Ok("debug-string"));
228+
}
229+
}
230+
let debug_ud = Value::UserData(lua.create_userdata(ToDebugUserData)?);
231+
assert_eq!(debug_ud.to_string()?, "regular-string");
232+
assert_eq!(format!("{debug_ud:#?}"), "debug-string");
233+
234+
struct ToStringUserData;
235+
impl UserData for ToStringUserData {
236+
fn register(registry: &mut UserDataRegistry<Self>) {
237+
registry.add_meta_method("__tostring", |_, _, ()| Ok("to-string-only"));
238+
}
239+
}
240+
let tostring_only_ud = Value::UserData(lua.create_userdata(ToStringUserData)?);
241+
assert_eq!(format!("{tostring_only_ud:#?}"), "tostring-only");
242+
221243
Ok(())
222244
}
223245

0 commit comments

Comments
 (0)