From 4306e6e97818919f829266d336fbdc05271286c2 Mon Sep 17 00:00:00 2001 From: Alex Orlenko Date: Fri, 26 May 2023 23:52:48 +0100 Subject: [PATCH] Add `Value::to_string()` method similar to `luaL_tolstring`. It uses `__tostring` metamethod if set. Closes #279 --- mlua-sys/src/lua51/compat.rs | 10 +++--- mlua-sys/src/lua52/compat.rs | 10 +++--- mlua-sys/src/lua53/lauxlib.rs | 8 ++++- mlua-sys/src/luau/compat.rs | 10 +++--- src/macros.rs | 7 ++++- src/value.rs | 34 +++++++++++++++++++++ tests/value.rs | 57 ++++++++++++++++++++++++++++++++++- 7 files changed, 118 insertions(+), 18 deletions(-) diff --git a/mlua-sys/src/lua51/compat.rs b/mlua-sys/src/lua51/compat.rs index c67dbe0..5216d33 100644 --- a/mlua-sys/src/lua51/compat.rs +++ b/mlua-sys/src/lua51/compat.rs @@ -486,10 +486,10 @@ pub unsafe fn luaL_traceback( lua_concat(L, lua_gettop(L) - top); } -pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char { +pub unsafe fn luaL_tolstring(L: *mut lua_State, mut idx: c_int, len: *mut usize) -> *const c_char { + idx = lua_absindex(L, idx); if luaL_callmeta(L, idx, cstr!("__tostring")) == 0 { - let t = lua_type(L, idx); - match t { + match lua_type(L, idx) { LUA_TNIL => { lua_pushliteral(L, "nil"); } @@ -503,7 +503,7 @@ pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> lua_pushliteral(L, "true"); } } - _ => { + t => { let tt = luaL_getmetafield(L, idx, cstr!("__name")); let name = if tt == LUA_TSTRING { lua_tostring(L, -1) @@ -512,7 +512,7 @@ pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> }; lua_pushfstring(L, cstr!("%s: %p"), name, lua_topointer(L, idx)); if tt != LUA_TNIL { - lua_replace(L, -2); + lua_replace(L, -2); // remove '__name' } } }; diff --git a/mlua-sys/src/lua52/compat.rs b/mlua-sys/src/lua52/compat.rs index b834ab1..9a75a01 100644 --- a/mlua-sys/src/lua52/compat.rs +++ b/mlua-sys/src/lua52/compat.rs @@ -205,10 +205,10 @@ pub unsafe fn luaL_newmetatable(L: *mut lua_State, tname: *const c_char) -> c_in } } -pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char { +pub unsafe fn luaL_tolstring(L: *mut lua_State, mut idx: c_int, len: *mut usize) -> *const c_char { + idx = lua_absindex(L, idx); if luaL_callmeta(L, idx, cstr!("__tostring")) == 0 { - let t = lua_type(L, idx); - match t { + match lua_type(L, idx) { LUA_TNIL => { lua_pushliteral(L, "nil"); } @@ -222,7 +222,7 @@ pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> lua_pushliteral(L, "true"); } } - _ => { + t => { let tt = luaL_getmetafield(L, idx, cstr!("__name")); let name = if tt == LUA_TSTRING { lua_tostring(L, -1) @@ -231,7 +231,7 @@ pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> }; lua_pushfstring(L, cstr!("%s: %p"), name, lua_topointer(L, idx)); if tt != LUA_TNIL { - lua_replace(L, -2); + lua_replace(L, -2); // remove '__name' } } }; diff --git a/mlua-sys/src/lua53/lauxlib.rs b/mlua-sys/src/lua53/lauxlib.rs index cc83c61..0d1679f 100644 --- a/mlua-sys/src/lua53/lauxlib.rs +++ b/mlua-sys/src/lua53/lauxlib.rs @@ -25,7 +25,8 @@ extern "C" { pub fn luaL_getmetafield(L: *mut lua_State, obj: c_int, e: *const c_char) -> c_int; pub fn luaL_callmeta(L: *mut lua_State, obj: c_int, e: *const c_char) -> c_int; - pub fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char; + #[link_name = "luaL_tolstring"] + pub fn luaL_tolstring_(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char; pub fn luaL_argerror(L: *mut lua_State, arg: c_int, extramsg: *const c_char) -> c_int; pub fn luaL_checklstring(L: *mut lua_State, arg: c_int, l: *mut usize) -> *const c_char; pub fn luaL_optlstring( @@ -167,6 +168,11 @@ pub unsafe fn luaL_getmetatable(L: *mut lua_State, n: *const c_char) { lua::lua_getfield(L, lua::LUA_REGISTRYINDEX, n); } +#[inline(always)] +pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char { + luaL_tolstring_(L, lua::lua_absindex(L, idx), len) +} + // luaL_opt would be implemented here but it is undocumented, so it's omitted #[inline(always)] diff --git a/mlua-sys/src/luau/compat.rs b/mlua-sys/src/luau/compat.rs index 043245a..f08882d 100644 --- a/mlua-sys/src/luau/compat.rs +++ b/mlua-sys/src/luau/compat.rs @@ -436,10 +436,10 @@ pub unsafe fn luaL_traceback( lua_concat(L, lua_gettop(L) - top); } -pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char { +pub unsafe fn luaL_tolstring(L: *mut lua_State, mut idx: c_int, len: *mut usize) -> *const c_char { + idx = lua_absindex(L, idx); if luaL_callmeta(L, idx, cstr!("__tostring")) == 0 { - let t = lua_type(L, idx); - match t { + match lua_type(L, idx) { LUA_TNIL => { lua_pushliteral(L, "nil"); } @@ -453,7 +453,7 @@ pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> lua_pushliteral(L, "true"); } } - _ => { + t => { let tt = luaL_getmetafield(L, idx, cstr!("__name")); let name = if tt == LUA_TSTRING { lua_tostring(L, -1) @@ -462,7 +462,7 @@ pub unsafe fn luaL_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> }; lua_pushfstring(L, cstr!("%s: %p"), name, lua_topointer(L, idx)); if tt != LUA_TNIL { - lua_replace(L, -2); + lua_replace(L, -2); // remove '__name' } } }; diff --git a/src/macros.rs b/src/macros.rs index eb49c15..38c7726 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -103,7 +103,12 @@ macro_rules! protect_lua { ($state:expr, $nargs:expr, $nresults:expr, fn($state_inner:ident) $code:expr) => {{ unsafe extern "C" fn do_call($state_inner: *mut ffi::lua_State) -> ::std::os::raw::c_int { $code; - $nresults + let nresults = $nresults; + if nresults == ::ffi::LUA_MULTRET { + ffi::lua_gettop($state_inner) + } else { + nresults + } } crate::util::protect_lua_call($state, $nargs, do_call) diff --git a/src/value.rs b/src/value.rs index 2678e4b..77ff45c 100644 --- a/src/value.rs +++ b/src/value.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use std::iter::{self, FromIterator}; use std::ops::Index; use std::os::raw::c_void; +use std::string::String as StdString; use std::sync::Arc; use std::{fmt, ptr, slice, str, vec}; @@ -21,6 +22,7 @@ use crate::table::Table; use crate::thread::Thread; use crate::types::{Integer, LightUserData, Number}; use crate::userdata::AnyUserData; +use crate::util::{check_stack, StackGuard}; /// A dynamically typed Lua value. The `String`, `Table`, `Function`, `Thread`, and `UserData` /// variants contain handle types into the internal Lua state. It is a logic error to mix handle @@ -129,6 +131,38 @@ impl<'lua> Value<'lua> { } } + /// Converts the value to a string. + /// + /// If the value has a metatable with a `__tostring` method, then it will be called to get the result. + pub fn to_string(&self) -> Result { + match self { + Value::Nil => Ok("nil".to_string()), + Value::Boolean(b) => Ok(b.to_string()), + Value::LightUserData(ud) if ud.0.is_null() => Ok("null".to_string()), + Value::LightUserData(ud) => Ok(format!("lightuserdata: {:p}", ud.0)), + Value::Integer(i) => Ok(i.to_string()), + Value::Number(n) => Ok(n.to_string()), + #[cfg(feature = "luau")] + Value::Vector(x, y, z) => Ok(format!("vector({x}, {y}, {z})")), + Value::String(s) => Ok(s.to_str()?.to_string()), + Value::Table(Table(r)) + | Value::Function(Function(r)) + | Value::Thread(Thread(r)) + | Value::UserData(AnyUserData(r)) => unsafe { + let state = r.lua.state(); + let _guard = StackGuard::new(state); + check_stack(state, 3)?; + + r.lua.push_ref(r); + protect_lua!(state, 1, 1, fn(state) { + ffi::luaL_tolstring(state, -1, ptr::null_mut()); + })?; + Ok(String(r.lua.pop_ref()).to_str()?.to_string()) + }, + Value::Error(err) => Ok(err.to_string()), + } + } + // Compares two values. // Used to sort values for Debug printing. pub(crate) fn cmp(&self, other: &Self) -> Ordering { diff --git a/tests/value.rs b/tests/value.rs index 439a490..ad5001e 100644 --- a/tests/value.rs +++ b/tests/value.rs @@ -1,6 +1,8 @@ +use std::os::raw::c_void; use std::ptr; +use std::string::String as StdString; -use mlua::{Lua, MultiValue, Result, Value}; +use mlua::{Error, LightUserData, Lua, MultiValue, Result, UserData, UserDataMethods, Value}; #[test] fn test_value_eq() -> Result<()> { @@ -84,3 +86,56 @@ fn test_multi_value() { multi_value.clear(); assert!(multi_value.is_empty()); } + +#[test] +fn test_value_to_string() -> Result<()> { + let lua = Lua::new(); + + assert_eq!(Value::Nil.to_string()?, "nil"); + assert_eq!(Value::Boolean(true).to_string()?, "true"); + assert_eq!(Value::NULL.to_string()?, "null"); + assert_eq!( + Value::LightUserData(LightUserData(0x1 as *const c_void as *mut _)).to_string()?, + "lightuserdata: 0x1" + ); + assert_eq!(Value::Integer(1).to_string()?, "1"); + assert_eq!(Value::Number(34.59).to_string()?, "34.59"); + #[cfg(feature = "luau")] + assert_eq!( + Value::Vector(10.0, 11.1, 12.2).to_string()?, + "vector(10, 11.1, 12.2)" + ); + assert_eq!( + Value::String(lua.create_string("hello")?).to_string()?, + "hello" + ); + + let table: Value = lua.load("{}").eval()?; + assert!(table.to_string()?.starts_with("table:")); + let table: Value = lua + .load("setmetatable({}, {__tostring = function() return 'test table' end})") + .eval()?; + assert_eq!(table.to_string()?, "test table"); + + let func: Value = lua.load("function() end").eval()?; + assert!(func.to_string()?.starts_with("function:")); + + let thread: Value = lua.load("coroutine.create(function() end)").eval()?; + assert!(thread.to_string()?.starts_with("thread:")); + + lua.register_userdata_type::(|reg| { + reg.add_meta_method("__tostring", |_, this, ()| Ok(this.clone())); + })?; + let ud: Value = Value::UserData(lua.create_any_userdata(String::from("string userdata"))?); + assert_eq!(ud.to_string()?, "string userdata"); + + struct MyUserData; + impl UserData for MyUserData {} + let ud: Value = Value::UserData(lua.create_userdata(MyUserData)?); + assert!(ud.to_string()?.starts_with("MyUserData:")); + + let err = Value::Error(Error::RuntimeError("test error".to_string())); + assert_eq!(err.to_string()?, "runtime error: test error"); + + Ok(()) +}