diff --git a/benches/benchmark.rs b/benches/benchmark.rs index 8fd0876..f9711e7 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -209,6 +209,42 @@ fn create_userdata(c: &mut Criterion) { }); } +fn userdata_index(c: &mut Criterion) { + struct UserData(i64); + impl LuaUserData for UserData { + fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(mlua::MetaMethod::Index, move |_, _, index: String| { + Ok(index) + }); + } + } + + let lua = Lua::new(); + lua.globals().set("userdata", UserData(10)).unwrap(); + + c.bench_function("index [table userdata] 10", |b| { + b.iter_batched_ref( + || { + collect_gc_twice(&lua); + lua.load( + r#" + function() + for i = 1,10 do + local v = userdata.test + end + end"#, + ) + .eval::() + .unwrap() + }, + |function| { + function.call::<_, ()>(()).unwrap(); + }, + BatchSize::SmallInput, + ); + }); +} + fn call_userdata_method(c: &mut Criterion) { struct UserData(i64); impl LuaUserData for UserData { @@ -283,6 +319,7 @@ criterion_group! { call_concat_callback, create_registry_values, create_userdata, + userdata_index, call_userdata_method, call_async_userdata_method, } diff --git a/src/util.rs b/src/util.rs index 22eb6fe..75caa9f 100644 --- a/src/util.rs +++ b/src/util.rs @@ -9,7 +9,7 @@ use once_cell::sync::Lazy; use rustc_hash::FxHashMap; use crate::error::{Error, Result}; -use crate::ffi; +use crate::ffi::{self, lua_error}; static METATABLE_CACHE: Lazy> = Lazy::new(|| { let mut map = FxHashMap::with_capacity_and_hasher(32, Default::default()); @@ -345,6 +345,112 @@ pub unsafe fn get_gc_userdata(state: *mut ffi::lua_State, index: c_int) ud } +unsafe extern "C" fn isfunction_impl(state: *mut ffi::lua_State) -> c_int { + // stack: var + ffi::luaL_checkstack(state, 1, ptr::null()); + + let t = ffi::lua_type(state, -1); + ffi::lua_pop(state, 1); + ffi::lua_pushboolean(state, if t == ffi::LUA_TFUNCTION { 1 } else { 0 }); + + 1 +} + +unsafe extern "C" fn error_impl(state: *mut ffi::lua_State) -> c_int { + // stack: message + ffi::luaL_checkstack(state, 1, ptr::null()); + + lua_error(state); +} + +pub unsafe fn init_userdata_metatable_index(state: *mut ffi::lua_State) -> Result<()> { + protect_lua!(state, 0, 1, |state| { + let ret = ffi::luaL_dostring( + state, + cstr!( + r#" + return function (isfunction, error) + return function (__index, field_getters, methods) + return function (self, key) + if field_getters ~= nil then + local field_getter = field_getters[key] + if field_getter ~= nil then + return field_getter(self) + end + end + + if methods ~= nil then + local method = methods[key] + if method ~= nil then + return method + end + end + + if isfunction(__index) then + return __index(self, key) + elseif __index == nil then + error('attempt to get an unknown field \'' .. key .. '\'') + else + return __index[key] + end + end + end + end + "# + ), + ); + if ret != ffi::LUA_OK { + ffi::lua_error(state); + } + ffi::lua_pushcfunction(state, isfunction_impl); + ffi::lua_pushcfunction(state, error_impl); + ffi::lua_call(state, 2, 1); + })?; + + Ok(()) +} + +pub unsafe fn init_userdata_metatable_newindex(state: *mut ffi::lua_State) -> Result<()> { + protect_lua!(state, 0, 1, |state| { + let ret = ffi::luaL_dostring( + state, + cstr!( + r#" + return function (isfunction, error) + return function (__newindex, field_setters) + return function (self, key, value) + if field_setters ~= nil then + local field_setter = field_setters[key] + if field_setter ~= nil then + field_setter(self, value) + return + end + end + + if isfunction(__newindex) then + __newindex(self, key, value) + elseif __newindex == nil then + error('attempt to set an unknown field \'' .. key .. '\'') + else + __newindex[key] = value + end + end + end + end + "# + ), + ); + if ret != ffi::LUA_OK { + ffi::lua_error(state); + } + ffi::lua_pushcfunction(state, isfunction_impl); + ffi::lua_pushcfunction(state, error_impl); + ffi::lua_call(state, 2, 1); + })?; + + Ok(()) +} + // Populates the given table with the appropriate members to be a userdata metatable for the given type. // This function takes the given table at the `metatable` index, and adds an appropriate `__gc` member // to it for the given type and a `__metatable` entry to protect the table from script access. @@ -360,99 +466,13 @@ pub unsafe fn init_userdata_metatable( field_setters: Option, methods: Option, ) -> Result<()> { - // Wrapper to lookup in `field_getters` first, then `methods`, ending original `__index`. - // Used only if `field_getters` or `methods` set. - unsafe extern "C" fn meta_index_impl(state: *mut ffi::lua_State) -> c_int { - // stack: self, key - ffi::luaL_checkstack(state, 2, ptr::null()); - - // lookup in `field_getters` table - if ffi::lua_isnil(state, ffi::lua_upvalueindex(2)) == 0 { - ffi::lua_pushvalue(state, -1); // `key` arg - if ffi::lua_rawget(state, ffi::lua_upvalueindex(2)) != ffi::LUA_TNIL { - ffi::lua_insert(state, -3); // move function - ffi::lua_pop(state, 1); // remove `key` - ffi::lua_call(state, 1, 1); - return 1; - } - ffi::lua_pop(state, 1); // pop the nil value - } - // lookup in `methods` table - if ffi::lua_isnil(state, ffi::lua_upvalueindex(3)) == 0 { - ffi::lua_pushvalue(state, -1); // `key` arg - if ffi::lua_rawget(state, ffi::lua_upvalueindex(3)) != ffi::LUA_TNIL { - ffi::lua_insert(state, -3); - ffi::lua_pop(state, 2); - return 1; - } - ffi::lua_pop(state, 1); // pop the nil value - } - - // lookup in `__index` - ffi::lua_pushvalue(state, ffi::lua_upvalueindex(1)); - match ffi::lua_type(state, -1) { - ffi::LUA_TNIL => { - ffi::lua_pop(state, 1); // pop the nil value - let field = ffi::lua_tostring(state, -1); - ffi::luaL_error(state, cstr!("attempt to get an unknown field '%s'"), field); - } - ffi::LUA_TTABLE => { - ffi::lua_insert(state, -2); - ffi::lua_gettable(state, -2); - } - ffi::LUA_TFUNCTION => { - ffi::lua_insert(state, -3); - ffi::lua_call(state, 2, 1); - } - _ => unreachable!(), - } - - 1 - } - - // Similar to `meta_index_impl`, checks `field_setters` table first, then `__newindex` metamethod. - // Used only if `field_setters` set. - unsafe extern "C" fn meta_newindex_impl(state: *mut ffi::lua_State) -> c_int { - // stack: self, key, value - ffi::luaL_checkstack(state, 2, ptr::null()); - - // lookup in `field_setters` table - ffi::lua_pushvalue(state, -2); // `key` arg - if ffi::lua_rawget(state, ffi::lua_upvalueindex(2)) != ffi::LUA_TNIL { - ffi::lua_remove(state, -3); // remove `key` - ffi::lua_insert(state, -3); // move function - ffi::lua_call(state, 2, 0); - return 0; - } - ffi::lua_pop(state, 1); // pop the nil value - - // lookup in `__newindex` - ffi::lua_pushvalue(state, ffi::lua_upvalueindex(1)); - match ffi::lua_type(state, -1) { - ffi::LUA_TNIL => { - ffi::lua_pop(state, 1); // pop the nil value - let field = ffi::lua_tostring(state, -2); - ffi::luaL_error(state, cstr!("attempt to set an unknown field '%s'"), field); - } - ffi::LUA_TTABLE => { - ffi::lua_insert(state, -3); - ffi::lua_settable(state, -3); - } - ffi::LUA_TFUNCTION => { - ffi::lua_insert(state, -4); - ffi::lua_call(state, 3, 0); - } - _ => unreachable!(), - } - - 0 - } - ffi::lua_pushvalue(state, metatable); if field_getters.is_some() || methods.is_some() { + init_userdata_metatable_index(state)?; + push_string(state, "__index")?; - let index_type = ffi::lua_rawget(state, -2); + let index_type = ffi::lua_rawget(state, -3); match index_type { ffi::LUA_TNIL | ffi::LUA_TTABLE | ffi::LUA_TFUNCTION => { for &idx in &[field_getters, methods] { @@ -462,9 +482,8 @@ pub unsafe fn init_userdata_metatable( ffi::lua_pushnil(state); } } - protect_lua!(state, 3, 1, fn(state) { - ffi::lua_pushcclosure(state, meta_index_impl, 3); - })?; + + protect_lua!(state, 4, 1, |state| ffi::lua_call(state, 3, 1))?; } _ => mlua_panic!("improper __index type {}", index_type), } @@ -473,14 +492,15 @@ pub unsafe fn init_userdata_metatable( } if let Some(field_setters) = field_setters { + init_userdata_metatable_newindex(state)?; + push_string(state, "__newindex")?; - let newindex_type = ffi::lua_rawget(state, -2); + let newindex_type = ffi::lua_rawget(state, -3); match newindex_type { ffi::LUA_TNIL | ffi::LUA_TTABLE | ffi::LUA_TFUNCTION => { ffi::lua_pushvalue(state, field_setters); - protect_lua!(state, 2, 1, fn(state) { - ffi::lua_pushcclosure(state, meta_newindex_impl, 2); - })?; + + protect_lua!(state, 3, 1, |state| ffi::lua_call(state, 2, 1))?; } _ => mlua_panic!("improper __newindex type {}", newindex_type), } diff --git a/tests/async.rs b/tests/async.rs index 096dd22..77bc576 100644 --- a/tests/async.rs +++ b/tests/async.rs @@ -7,13 +7,14 @@ use std::sync::{ Arc, }; use std::time::Duration; +use std::unreachable; use futures_timer::Delay; use futures_util::stream::TryStreamExt; use mlua::{ - Error, Function, Lua, LuaOptions, MetaMethod, Result, StdLib, Table, TableExt, Thread, - UserData, UserDataMethods, Value, + Error, ExternalError, Function, Lua, LuaOptions, MetaMethod, Result, StdLib, Table, TableExt, + Thread, ToLua, UserData, UserDataMethods, Value, }; #[tokio::test] @@ -304,6 +305,44 @@ async fn test_async_userdata() -> Result<()> { Delay::new(Duration::from_millis(n)).await; Ok(format!("elapsed:{}ms", n)) }); + + #[cfg(not(feature = "lua51"))] + methods.add_async_meta_method(MetaMethod::Index, |lua, data, key: String| async move { + Delay::new(Duration::from_millis(10)).await; + + match key.as_str() { + "ms" => Ok(data.0.load(Ordering::Relaxed).to_lua(lua)?), + "s" => Ok(((data.0.load(Ordering::Relaxed) as f64) / 1000.0).to_lua(lua)?), + _ => Ok(Value::Nil), + } + }); + + #[cfg(not(feature = "lua51"))] + methods.add_async_meta_method( + MetaMethod::NewIndex, + |_, data, (key, value): (String, Value)| async move { + Delay::new(Duration::from_millis(10)).await; + + match key.as_str() { + "ms" | "s" => { + let value = match value { + Value::Integer(value) => value as f64, + Value::Number(value) => value, + _ => Err("wrong type for value".to_lua_err())?, + }; + let value = match key.as_str() { + "ms" => value, + "s" => value * 1000.0, + _ => unreachable!(), + }; + data.0.store(value as u64, Ordering::Relaxed); + + Ok(()) + } + _ => Err(format!("key '{}' not found", key).to_lua_err()), + } + }, + ); } } @@ -329,6 +368,12 @@ async fn test_async_userdata() -> Result<()> { r#" userdata:set_value(15) assert(userdata() == "elapsed:15ms") + + userdata.ms = 2000 + assert(userdata.s == 2) + + userdata.s = 15 + assert(userdata.ms == 15000) "#, ) .exec_async()