Skip to content

Commit 5d27cb9

Browse files
committed
Add optional __namecall optimization for Luau
Add `UserDataRegistry::enable_namecall()` hint to set `__namecall` metamethod to enable Luau-specific method resolution optimization.
1 parent c70a636 commit 5d27cb9

File tree

7 files changed

+127
-5
lines changed

7 files changed

+127
-5
lines changed

benches/benchmark.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,9 @@ fn userdata_call_method_complex(c: &mut Criterion) {
376376
this.0 += by;
377377
Ok(this.0)
378378
});
379+
380+
#[cfg(feature = "luau")]
381+
registry.enable_namecall();
379382
}
380383
}
381384

src/state/raw.rs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ use crate::userdata::{
2828
use crate::util::{
2929
assert_stack, check_stack, get_destructed_userdata_metatable, get_internal_userdata, get_main_state,
3030
get_metatable_ptr, get_userdata, init_error_registry, init_internal_metatable, pop_error,
31-
push_internal_userdata, push_string, push_table, rawset_field, safe_pcall, safe_xpcall, short_type_name,
32-
StackGuard, WrappedFailure,
31+
push_internal_userdata, push_string, push_table, push_userdata, rawset_field, safe_pcall, safe_xpcall,
32+
short_type_name, StackGuard, WrappedFailure,
3333
};
3434
use crate::value::{Nil, Value};
3535

@@ -928,7 +928,7 @@ impl RawLua {
928928
// We generate metatable first to make sure it *always* available when userdata pushed
929929
let mt_id = get_metatable_id()?;
930930
let protect = !self.unlikely_memory_error();
931-
crate::util::push_userdata(state, data, protect)?;
931+
push_userdata(state, data, protect)?;
932932
ffi::lua_rawgeti(state, ffi::LUA_REGISTRYINDEX, mt_id);
933933
ffi::lua_setmetatable(state, -2);
934934

@@ -1056,6 +1056,18 @@ impl RawLua {
10561056
field_setters_index = Some(ffi::lua_absindex(state, -1));
10571057
}
10581058

1059+
// Create methods namecall table
1060+
#[cfg_attr(not(feature = "luau"), allow(unused_mut))]
1061+
let mut methods_map = None;
1062+
#[cfg(feature = "luau")]
1063+
if registry.enable_namecall {
1064+
let map: &mut rustc_hash::FxHashMap<_, crate::types::CallbackPtr> =
1065+
methods_map.get_or_insert_with(Default::default);
1066+
for (k, m) in &registry.methods {
1067+
map.insert(k.as_bytes().to_vec(), &**m);
1068+
}
1069+
}
1070+
10591071
let mut methods_index = None;
10601072
let methods_nrec = registry.methods.len();
10611073
#[cfg(feature = "async")]
@@ -1103,6 +1115,7 @@ impl RawLua {
11031115
field_getters_index,
11041116
field_setters_index,
11051117
methods_index,
1118+
methods_map,
11061119
)?;
11071120

11081121
// Update stack guard to keep metatable after return

src/types.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ unsafe impl Send for LightUserData {}
3838
unsafe impl Sync for LightUserData {}
3939

4040
#[cfg(feature = "send")]
41-
pub(crate) type Callback = Box<dyn Fn(&RawLua, c_int) -> Result<c_int> + Send + 'static>;
41+
type CallbackFn<'a> = dyn Fn(&RawLua, c_int) -> Result<c_int> + Send + 'a;
4242

4343
#[cfg(not(feature = "send"))]
44-
pub(crate) type Callback = Box<dyn Fn(&RawLua, c_int) -> Result<c_int> + 'static>;
44+
type CallbackFn<'a> = dyn Fn(&RawLua, c_int) -> Result<c_int> + 'a;
45+
46+
pub(crate) type Callback = Box<CallbackFn<'static>>;
47+
pub(crate) type CallbackPtr = *const CallbackFn<'static>;
4548

4649
pub(crate) type ScopedCallback<'s> = Box<dyn Fn(&RawLua, c_int) -> Result<c_int> + 's>;
4750

src/userdata/registry.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ pub(crate) struct RawUserDataRegistry {
5656
pub(crate) destructor: ffi::lua_CFunction,
5757
pub(crate) type_id: Option<TypeId>,
5858
pub(crate) type_name: StdString,
59+
60+
#[cfg(feature = "luau")]
61+
pub(crate) enable_namecall: bool,
5962
}
6063

6164
impl UserDataType {
@@ -100,6 +103,8 @@ impl<T> UserDataRegistry<T> {
100103
destructor: super::util::destroy_userdata_storage::<T>,
101104
type_id: r#type.type_id(),
102105
type_name: short_type_name::<T>(),
106+
#[cfg(feature = "luau")]
107+
enable_namecall: false,
103108
};
104109

105110
UserDataRegistry {
@@ -110,6 +115,23 @@ impl<T> UserDataRegistry<T> {
110115
}
111116
}
112117

118+
/// Enables support for the namecall optimization in Luau.
119+
///
120+
/// This enables methods resolution optimization in Luau for complex userdata types with methods
121+
/// and field getters. When enabled, Luau will use a faster lookup path for method calls when a
122+
/// specific syntax is used (e.g. `obj:method()`.
123+
///
124+
/// This optimization does not play well with async methods, custom `__index` metamethod and
125+
/// field getters as functions. So, it is disabled by default.
126+
///
127+
/// Use with caution.
128+
#[doc(hidden)]
129+
#[cfg(feature = "luau")]
130+
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
131+
pub fn enable_namecall(&mut self) {
132+
self.raw.enable_namecall = true;
133+
}
134+
113135
fn box_method<M, A, R>(&self, name: &str, method: M) -> Callback
114136
where
115137
M: Fn(&Lua, &T, A) -> Result<R> + MaybeSend + 'static,

src/userdata/util.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ use std::marker::PhantomData;
44
use std::os::raw::c_int;
55
use std::ptr;
66

7+
use rustc_hash::FxHashMap;
8+
79
use super::UserDataStorage;
810
use crate::error::{Error, Result};
11+
use crate::types::CallbackPtr;
912
use crate::util::{get_userdata, rawget_field, rawset_field, take_userdata};
1013

1114
// This is a trick to check if a type is `Sync` or not.
@@ -244,6 +247,7 @@ pub(crate) unsafe fn init_userdata_metatable(
244247
field_getters: Option<c_int>,
245248
field_setters: Option<c_int>,
246249
methods: Option<c_int>,
250+
_methods_map: Option<FxHashMap<Vec<u8>, CallbackPtr>>, // Used only in Luau for `__namecall`
247251
) -> Result<()> {
248252
if field_getters.is_some() || methods.is_some() {
249253
// Push `__index` generator function
@@ -267,6 +271,13 @@ pub(crate) unsafe fn init_userdata_metatable(
267271
}
268272

269273
rawset_field(state, metatable, "__index")?;
274+
275+
#[cfg(feature = "luau")]
276+
if let Some(methods_map) = _methods_map {
277+
// In Luau we can speedup method calls by providing a dedicated `__namecall` metamethod
278+
push_userdata_metatable_namecall(state, methods_map)?;
279+
rawset_field(state, metatable, "__namecall")?;
280+
}
270281
}
271282

272283
if let Some(field_setters) = field_setters {
@@ -425,6 +436,36 @@ unsafe fn init_userdata_metatable_newindex(state: *mut ffi::lua_State) -> Result
425436
})
426437
}
427438

439+
#[cfg(feature = "luau")]
440+
unsafe fn push_userdata_metatable_namecall(
441+
state: *mut ffi::lua_State,
442+
methods_map: FxHashMap<Vec<u8>, CallbackPtr>,
443+
) -> Result<()> {
444+
unsafe extern "C-unwind" fn namecall(state: *mut ffi::lua_State) -> c_int {
445+
let name = ffi::lua_namecallatom(state, ptr::null_mut());
446+
if name.is_null() {
447+
ffi::luaL_error(state, cstr!("attempt to call an unknown method"));
448+
}
449+
let name_cs = std::ffi::CStr::from_ptr(name);
450+
let methods_map = get_userdata::<FxHashMap<Vec<u8>, CallbackPtr>>(state, ffi::lua_upvalueindex(1));
451+
let callback_ptr = match (*methods_map).get(name_cs.to_bytes()) {
452+
Some(ptr) => *ptr,
453+
#[rustfmt::skip]
454+
None => ffi::luaL_error(state, cstr!("attempt to call an unknown method '%s'"), name),
455+
};
456+
crate::state::callback_error_ext(state, ptr::null_mut(), true, |extra, nargs| {
457+
let rawlua = (*extra).raw_lua();
458+
(*callback_ptr)(rawlua, nargs)
459+
})
460+
}
461+
462+
// Automatic destructor is provided for any Luau userdata
463+
crate::util::push_userdata(state, methods_map, true)?;
464+
protect_lua!(state, 1, 1, |state| {
465+
ffi::lua_pushcclosured(state, namecall, cstr!("__namecall"), 1);
466+
})
467+
}
468+
428469
// This method is called by Lua GC when it's time to collect the userdata.
429470
//
430471
// This method is usually used to collect internal userdata.

src/util/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,8 @@ pub(crate) unsafe fn init_error_registry(state: *mut ffi::lua_State) -> Result<(
402402
"__ipairs",
403403
#[cfg(feature = "luau")]
404404
"__iter",
405+
#[cfg(feature = "luau")]
406+
"__namecall",
405407
#[cfg(feature = "lua54")]
406408
"__close",
407409
] {

tests/userdata.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,3 +1307,41 @@ fn test_userdata_wrappers() -> Result<()> {
13071307

13081308
Ok(())
13091309
}
1310+
1311+
#[cfg(feature = "luau")]
1312+
#[test]
1313+
fn test_userdata_namecall() -> Result<()> {
1314+
let lua = Lua::new();
1315+
1316+
struct MyUserData;
1317+
1318+
impl UserData for MyUserData {
1319+
fn register(registry: &mut mlua::UserDataRegistry<Self>) {
1320+
registry.add_method("method", |_, _, ()| Ok("method called"));
1321+
registry.add_field_method_get("field", |_, _| Ok("field value"));
1322+
1323+
registry.add_meta_method(MetaMethod::Index, |_, _, key: StdString| Ok(key));
1324+
1325+
registry.enable_namecall();
1326+
}
1327+
}
1328+
1329+
let ud = lua.create_userdata(MyUserData)?;
1330+
lua.globals().set("ud", &ud)?;
1331+
lua.load(
1332+
r#"
1333+
assert(ud:method() == "method called")
1334+
assert(ud.field == "field value")
1335+
assert(ud.dynamic_field == "dynamic_field")
1336+
local ok, err = pcall(function() return ud:dynamic_field() end)
1337+
assert(tostring(err):find("attempt to call an unknown method 'dynamic_field'") ~= nil)
1338+
"#,
1339+
)
1340+
.exec()?;
1341+
1342+
ud.destroy()?;
1343+
let err = lua.load("ud:method()").exec().unwrap_err();
1344+
assert!(err.to_string().contains("userdata has been destructed"));
1345+
1346+
Ok(())
1347+
}

0 commit comments

Comments
 (0)