Skip to content

Commit 1152519

Browse files
committed
Add add_method_once and add_async_method_once UserData methods (experimental).
They will allow implementing userdata methods that can be called only once, destructing userdata instance during the call.
1 parent 3a2fd1e commit 1152519

File tree

3 files changed

+108
-4
lines changed

3 files changed

+108
-4
lines changed

src/userdata.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::string::String;
1212
use crate::table::{Table, TablePairs};
1313
use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti};
1414
use crate::types::{MaybeSend, ValueRef};
15-
use crate::util::{check_stack, get_userdata, push_string, take_userdata, StackGuard};
15+
use crate::util::{check_stack, get_userdata, push_string, short_type_name, take_userdata, StackGuard};
1616
use crate::value::Value;
1717

1818
#[cfg(feature = "async")]
@@ -273,6 +273,29 @@ pub trait UserDataMethods<T> {
273273
A: FromLuaMulti,
274274
R: IntoLuaMulti;
275275

276+
/// Add a method which accepts `T` as the first parameter.
277+
///
278+
/// The userdata `T` will be moved out of the userdata container. This is useful for
279+
/// methods that need to consume the userdata.
280+
///
281+
/// The method can be called only once per userdata instance, subsequent calls will result in a
282+
/// [`Error::UserDataDestructed`] error.
283+
#[doc(hidden)]
284+
fn add_method_once<M, A, R>(&mut self, name: impl Into<StdString>, method: M)
285+
where
286+
T: 'static,
287+
M: Fn(&Lua, T, A) -> Result<R> + MaybeSend + 'static,
288+
A: FromLuaMulti,
289+
R: IntoLuaMulti,
290+
{
291+
let name = name.into();
292+
let method_name = format!("{}.{name}", short_type_name::<T>());
293+
self.add_function(name, move |lua, (ud, args): (AnyUserData, A)| {
294+
let this = (ud.take()).map_err(|err| Error::bad_self_argument(&method_name, err))?;
295+
method(lua, this, args)
296+
});
297+
}
298+
276299
/// Add an async method which accepts a `&T` as the first parameter and returns [`Future`].
277300
///
278301
/// Refer to [`add_method`] for more information about the implementation.
@@ -303,6 +326,34 @@ pub trait UserDataMethods<T> {
303326
MR: Future<Output = Result<R>> + MaybeSend + 'static,
304327
R: IntoLuaMulti;
305328

329+
/// Add an async method which accepts a `T` as the first parameter and returns [`Future`].
330+
///
331+
/// The userdata `T` will be moved out of the userdata container. This is useful for
332+
/// methods that need to consume the userdata.
333+
///
334+
/// The method can be called only once per userdata instance, subsequent calls will result in a
335+
/// [`Error::UserDataDestructed`] error.
336+
#[cfg(feature = "async")]
337+
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
338+
#[doc(hidden)]
339+
fn add_async_method_once<M, A, MR, R>(&mut self, name: impl Into<StdString>, method: M)
340+
where
341+
T: 'static,
342+
M: Fn(Lua, T, A) -> MR + MaybeSend + 'static,
343+
A: FromLuaMulti,
344+
MR: Future<Output = Result<R>> + MaybeSend + 'static,
345+
R: IntoLuaMulti,
346+
{
347+
let name = name.into();
348+
let method_name = format!("{}.{name}", short_type_name::<T>());
349+
self.add_async_function(name, move |lua, (ud, args): (AnyUserData, A)| {
350+
match (ud.take()).map_err(|err| Error::bad_self_argument(&method_name, err)) {
351+
Ok(this) => either::Either::Left(method(lua, this, args)),
352+
Err(err) => either::Either::Right(async move { Err(err) }),
353+
}
354+
});
355+
}
356+
306357
/// Add a regular method as a function which accepts generic arguments.
307358
///
308359
/// The first argument will be a [`AnyUserData`] of type `T` if the method is called with Lua

tests/async.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,9 +423,9 @@ async fn test_async_thread_pool() -> Result<()> {
423423

424424
#[tokio::test]
425425
async fn test_async_userdata() -> Result<()> {
426-
struct MyUserData(u64);
426+
struct MyUserdata(u64);
427427

428-
impl UserData for MyUserData {
428+
impl UserData for MyUserdata {
429429
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
430430
methods.add_async_method("get_value", |_, data, ()| async move {
431431
sleep_ms(10).await;
@@ -438,6 +438,11 @@ async fn test_async_userdata() -> Result<()> {
438438
Ok(())
439439
});
440440

441+
methods.add_async_method_once("take_value", |_, data, ()| async move {
442+
sleep_ms(10).await;
443+
Ok(data.0)
444+
});
445+
441446
methods.add_async_function("sleep", |_, n| async move {
442447
sleep_ms(n).await;
443448
Ok(format!("elapsed:{}ms", n))
@@ -479,7 +484,7 @@ async fn test_async_userdata() -> Result<()> {
479484
let lua = Lua::new();
480485
let globals = lua.globals();
481486

482-
let userdata = lua.create_userdata(MyUserData(11))?;
487+
let userdata = lua.create_userdata(MyUserdata(11))?;
483488
globals.set("userdata", &userdata)?;
484489

485490
lua.load(
@@ -518,6 +523,21 @@ async fn test_async_userdata() -> Result<()> {
518523
#[cfg(not(any(feature = "lua51", feature = "luau")))]
519524
assert_eq!(userdata.call_async::<String>(()).await?, "elapsed:24ms");
520525

526+
// Take value
527+
let userdata2 = lua.create_userdata(MyUserdata(0))?;
528+
globals.set("userdata2", userdata2)?;
529+
lua.load("assert(userdata:take_value() == 24)")
530+
.exec_async()
531+
.await?;
532+
match lua.load("userdata2.take_value(userdata)").exec_async().await {
533+
Err(Error::CallbackError { cause, .. }) => {
534+
let err = cause.to_string();
535+
assert!(err.contains("bad argument `self` to `MyUserdata.take_value`"));
536+
assert!(err.contains("userdata has been destructed"));
537+
}
538+
r => panic!("expected Err(CallbackError), got {r:?}"),
539+
}
540+
521541
Ok(())
522542
}
523543

tests/userdata.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,39 @@ fn test_userdata_destroy() -> Result<()> {
428428
Ok(())
429429
}
430430

431+
#[test]
432+
fn test_userdata_method_once() -> Result<()> {
433+
struct MyUserdata(Arc<i64>);
434+
435+
impl UserData for MyUserdata {
436+
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
437+
methods.add_method_once("take_value", |_, this, ()| Ok(*this.0));
438+
}
439+
}
440+
441+
let lua = Lua::new();
442+
let rc = Arc::new(42);
443+
let userdata = lua.create_userdata(MyUserdata(rc.clone()))?;
444+
lua.globals().set("userdata", &userdata)?;
445+
446+
// Control userdata
447+
let userdata2 = lua.create_userdata(MyUserdata(rc.clone()))?;
448+
lua.globals().set("userdata2", userdata2)?;
449+
450+
assert_eq!(lua.load("userdata:take_value()").eval::<i64>()?, 42);
451+
match lua.load("userdata2.take_value(userdata)").eval::<i64>() {
452+
Err(Error::CallbackError { cause, .. }) => {
453+
let err = cause.to_string();
454+
assert!(err.contains("bad argument `self` to `MyUserdata.take_value`"));
455+
assert!(err.contains("userdata has been destructed"));
456+
}
457+
r => panic!("expected Err(CallbackError), got {r:?}"),
458+
}
459+
assert_eq!(Arc::strong_count(&rc), 2);
460+
461+
Ok(())
462+
}
463+
431464
#[test]
432465
fn test_user_values() -> Result<()> {
433466
struct MyUserData;

0 commit comments

Comments
 (0)