Compare commits

...

40 Commits

Author SHA1 Message Date
Alex Orlenko 247208edb1 v0.11.4 2025-09-28 23:46:55 +01:00
Alex Orlenko e08768cc5e Derive Default for Value (clippy) 2025-09-28 23:42:12 +01:00
Alex Orlenko 5b38af9746 AsyncCallFuture is Unpin 2025-09-19 10:00:28 +01:00
Alex Orlenko 54907f80c5 Add SerializableValue to lib and prelude exports 2025-09-12 12:40:43 +01:00
Alex Orlenko ae512f2b49 Remove const from SerializableValue (it's not really useful) 2025-09-12 12:40:00 +01:00
Alex Orlenko 53c159b6cb Unhide Value::to_serializable 2025-09-12 11:49:43 +01:00
Alex Orlenko 2beca6ebe1 Add test for Table::for_each_value 2025-09-12 11:49:37 +01:00
Alex Orlenko 09da7a41e5 Add new serde option "detect_mixed_tables"
This option would allow detecting mixed tables (with array-like and map-like entries or several borders)
to encoding them chosing the best method (as a map or as a table).
2025-09-12 11:11:18 +01:00
Alex Orlenko bad20374ad Simplify Table::clear method
There is no need to traverse array part, lua_next will cover everything
2025-09-08 23:37:28 +01:00
Alex Orlenko 40b507c3ec Add ObjectLike::get_path helper 2025-09-04 19:12:44 +01:00
Andrew Dunbar 537cc995f6 Copyedit English in README.md (#639) 2025-09-04 14:59:24 +01:00
Alex Orlenko 5d27cb91b2 Add optional __namecall optimization for Luau
Add `UserDataRegistry::enable_namecall()` hint to set `__namecall` metamethod to enable Luau-specific method resolution optimization.
2025-09-02 00:53:12 +01:00
Alex Orlenko c70a636ca9 Remove newlines from yield_with examples 2025-08-30 12:51:53 +01:00
Alex Orlenko 13ff0ca798 v0.11.3 2025-08-29 23:11:21 +01:00
Alex Orlenko 44f49e35d6 Update CHANGELOG 2025-08-29 00:18:06 +01:00
Alex Orlenko e1ee4058a6 Add new benchmark to measure complex userdata method calls 2025-08-28 23:56:03 +01:00
Alex Orlenko f06d0020ea Add test to emulate method through field 2025-08-28 23:50:18 +01:00
Alex Orlenko d399559d30 Add Lua::yield_with to allow yielding Rust async functions and exchange values between Lua coroutine and Rust.
This functionality is similar to `coroutine.yield` and `coroutine.resume` without C restrictions.
2025-08-28 18:41:24 +01:00
Alex Orlenko 30735d5ff1 Fix thread recovery when pushing a bad arg
We should not erase thread stack if a bad argument is pushed before resuming the thread.
2025-08-25 23:07:37 +01:00
Alex Orlenko 75c23e5853 Add lua_cpcall to Luau ffi (0.688+) 2025-08-25 12:54:17 +01:00
Alex Orlenko 347856b806 Do not try to yield at non-yielable points in Luau interrupt
In particular we cannot yeild across metamethod/C-call boundaries.
This behaviour matches with Lua 5.3+ yielding from hooks only at safe points.
Closes #632
2025-08-25 12:19:50 +01:00
Alex Orlenko 774a63bece Add Buffer::cursor() method
This can be useful for providing access to buffers through core IO traits.
2025-08-24 11:29:01 +01:00
Alex Orlenko c481c87eac Add Lua::create_buffer_with_capacity method
This allow creating a preallocated buffer with specified size initialized to zero.
2025-08-23 22:38:55 +01:00
Alex Orlenko 85b280a9d6 Update nightly Rust error message matching 2025-08-23 09:40:13 +01:00
Alex Orlenko db7b782d3c Remove lifetimes from short type names 2025-08-23 09:13:31 +01:00
Alex Orlenko 5f38445558 Fix warnings 2025-08-20 16:25:06 +01:00
Alex Orlenko df0a44d405 Make Lua reference values cheap to clone
Instead of locking the VM and making a copy on auxiliary thread, track number of references using Rust ref counter.
This should also help reducing number of used references (they are limited to to 1M usually) on auxiliary thread.
2025-08-20 12:05:37 +01:00
Alex Orlenko f0806a6d62 Lower fastpath table creation limit to 1 << 26
When Lua is configured without memory restrictions, we use fastpath for table creation (unprotected mode).
In generally it's safe as long as we `abort()` on allocation failure.
However some Lua versions have additional restrictions on table size that we need to adhere in mlua too.
Probably Luau has the lowest limits.
Fixes #627
2025-08-13 22:49:40 +01:00
Alex Orlenko 3516f4c6ca v0.11.2 2025-08-10 00:53:45 +01:00
Alex Orlenko ca73583714 Update CHANGELOG 2025-08-10 00:53:01 +01:00
Alex Orlenko 36560435f7 Add push_into_stack_multi fastpath to Variadic 2025-08-10 00:35:51 +01:00
Alex Orlenko 763c2b2564 Update repl example: don't print newline if no values returned 2025-08-10 00:20:20 +01:00
Alex Orlenko bafdb6138c Update dependencies 2025-08-10 00:19:54 +01:00
Alex Orlenko c9d6a610e1 mlua-sys: v0.8.3 2025-08-10 00:11:05 +01:00
Alex Orlenko bd63f63bc9 Use ascii lowercase for module aliases
This matches with Luau 0.686 changes
2025-08-09 19:14:31 +01:00
piz-ewing c035c23a15 fix: normalize_chunk_name handles Windows paths with drive letter (#623)
Co-authored-by: ewing <ewing@MacBook-Pro.local>
2025-08-04 22:34:36 +01:00
Alex Orlenko cb153a52b2 Make Luau registered aliases case-insensitive
Executing `require("@my_module")` or `require("@My_Module")` should give the same result and use case-insensitive name.
See #620 for details
2025-07-26 22:23:16 +01:00
Alex Orlenko b1c69d3005 Use to_bits comparison to check if a float value can be represented as an integer losslessly.
This allows to simplify the code while still maintaining "negative zeros" edge case.
Thanks @JasonHise for the suggestion.
2025-07-25 21:25:08 +01:00
Alex Orlenko 841bd332e4 Fix LuaJIT negative zero tests 2025-07-25 15:24:04 +01:00
Alex Orlenko 815d1bd7c9 Better handling negative zeros to match Lua 5.3+ behavior
In Lua 5.3+ the function `lua_isinteger` returns "false" for -0.0 numbers.
In earlier Lua versions we should follow the same behavior to avoid losing the sign when converting to Integer.
Close #618
2025-07-25 14:32:47 +01:00
44 changed files with 1404 additions and 190 deletions
+22
View File
@@ -1,3 +1,25 @@
## v0.11.4 (Sep 29, 2025)
- Make `Value::to_serializable` public
- Add new serde option `detect_mixed_tables` (to encode mixed array+map tables)
- Add `ObjectLike::get_path` helper (for tables and userdata)
## v0.11.3 (Aug 30, 2025)
- Add `Lua::yield_with` to use as `coroutine.yield` functional replacement in async functions for any Lua
- Do not try to yield at non-yielable points in Luau interrupt (#632)
- Add `Buffer::cursor` method (Luau)
- Add `Lua::create_buffer_with_capacity` method (Luau)
- Make Lua reference values cheap to clone (only increments ref count)
- Fix panic on large (>67M entries) table creation
## v0.11.2 (Aug 10, 2025)
- Faster stack push for `Variadic<T>`
- Fix handling Windows paths with drive letter in Luau require (#623)
- Make Luau registered aliases ascii case-insensitive (#620)
- Fix deserializing negative zeros `-0.0` (#618)
## v0.11.1 (Jul 15, 2025)
- Fixed bug exhausting Lua auxiliary stack and leaving it without reserve (#615)
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "mlua"
version = "0.11.1" # remember to update mlua_derive
version = "0.11.4" # remember to update mlua_derive
authors = ["Aleksandr Orlenko <zxteam@pm.me>", "kyren <catherine@kyju.org>"]
rust-version = "1.79.0"
edition = "2021"
@@ -62,7 +62,7 @@ parking_lot = { version = "0.12", features = ["arc_lock"] }
anyhow = { version = "1.0", optional = true }
rustversion = "1.0"
ffi = { package = "mlua-sys", version = "0.8.0", path = "mlua-sys" }
ffi = { package = "mlua-sys", version = "0.8.3", path = "mlua-sys" }
[dev-dependencies]
trybuild = "1.0"
@@ -78,8 +78,8 @@ tempfile = "3"
static_assertions = "1.0"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = { version = "0.6", features = ["async_tokio"] }
rustyline = "16.0"
criterion = { version = "0.7", features = ["async_tokio"] }
rustyline = "17.0"
tokio = { version = "1.0", features = ["full"] }
[lints.rust]
+4 -4
View File
@@ -17,14 +17,14 @@
[Benchmarks]: https://github.com/khvzak/script-bench-rs
[FAQ]: FAQ.md
`mlua` is a set of bindings to the [Lua](https://www.lua.org) programming language for Rust with a goal to provide a
`mlua` is a set of bindings to the [Lua](https://www.lua.org) programming language for Rust with a goal of providing a
_safe_ (as much as possible), high level, easy to use, practical and flexible API.
Started as an `rlua` fork, `mlua` supports Lua 5.4, 5.3, 5.2, 5.1 (including LuaJIT) and [Luau] and allows writing native Lua modules in Rust as well as using Lua in a standalone mode.
`mlua` is tested on Windows/macOS/Linux including module mode in [GitHub Actions] on `x86_64` platforms and cross-compilation to `aarch64` (other targets are also supported).
WebAssembly (WASM) is supported through `wasm32-unknown-emscripten` target for all Lua/Luau versions excluding JIT.
WebAssembly (WASM) is supported through the `wasm32-unknown-emscripten` target for all Lua/Luau versions excluding JIT.
[GitHub Actions]: https://github.com/mlua-rs/mlua/actions
[Luau]: https://luau.org
@@ -33,7 +33,7 @@ WebAssembly (WASM) is supported through `wasm32-unknown-emscripten` target for a
### Feature flags
`mlua` uses feature flags to reduce the amount of dependencies and compiled code, and allow to choose only required set of features.
`mlua` uses feature flags to reduce the number of dependencies and compiled code, and allow choosing only the required set of features.
Below is a list of the available feature flags. By default `mlua` does not enable any features.
* `lua54`: enable Lua [5.4] support
@@ -270,7 +270,7 @@ remain usable after a user generated panic, and such panics should not break int
leak Lua stack space. This is mostly important to safely use `mlua` types in Drop impls, as you should not be
using panics for general error handling.
Below is a list of `mlua` behaviors that should be considered a bug.
Below is a list of `mlua` behaviors that should be considered bugs.
If you encounter them, a bug report would be very welcome:
+ If you can cause UB with `mlua` without typing the word "unsafe", this is a bug.
+54
View File
@@ -128,6 +128,22 @@ fn table_traversal_sequence(c: &mut Criterion) {
});
}
fn table_ref_clone(c: &mut Criterion) {
let lua = Lua::new();
let t = lua.create_table().unwrap();
c.bench_function("table [ref clone]", |b| {
b.iter_batched(
|| collect_gc_twice(&lua),
|_| {
let _t2 = t.clone();
},
BatchSize::SmallInput,
);
});
}
fn function_create(c: &mut Criterion) {
let lua = Lua::new();
@@ -350,6 +366,42 @@ fn userdata_call_method(c: &mut Criterion) {
});
}
// A userdata method call that goes through an implicit `__index` function
fn userdata_call_method_complex(c: &mut Criterion) {
struct UserData(u64);
impl LuaUserData for UserData {
fn register(registry: &mut LuaUserDataRegistry<Self>) {
registry.add_field_method_get("val", |_, this| Ok(this.0));
registry.add_method_mut("inc_by", |_, this, by: u64| {
this.0 += by;
Ok(this.0)
});
#[cfg(feature = "luau")]
registry.enable_namecall();
}
}
let lua = Lua::new();
let ud = lua.create_userdata(UserData(0)).unwrap();
let inc_by = lua
.load("function(ud, s) return ud:inc_by(s) end")
.eval::<LuaFunction>()
.unwrap();
c.bench_function("userdata [call method complex]", |b| {
b.iter_batched(
|| {
collect_gc_twice(&lua);
},
|_| {
inc_by.call::<()>((&ud, 1)).unwrap();
},
BatchSize::SmallInput,
);
});
}
fn userdata_async_call_method(c: &mut Criterion) {
struct UserData(i64);
impl LuaUserData for UserData {
@@ -399,6 +451,7 @@ criterion_group! {
table_traversal_pairs,
table_traversal_for_each,
table_traversal_sequence,
table_ref_clone,
function_create,
function_call_sum,
@@ -413,6 +466,7 @@ criterion_group! {
userdata_create,
userdata_call_index,
userdata_call_method,
userdata_call_method_complex,
userdata_async_call_method,
}
+10 -8
View File
@@ -20,14 +20,16 @@ fn main() {
match lua.load(&line).eval::<MultiValue>() {
Ok(values) => {
editor.add_history_entry(line).unwrap();
println!(
"{}",
values
.iter()
.map(|value| format!("{:#?}", value))
.collect::<Vec<_>>()
.join("\t")
);
if values.len() > 0 {
println!(
"{}",
values
.iter()
.map(|value| format!("{:#?}", value))
.collect::<Vec<_>>()
.join("\t")
);
}
break;
}
Err(Error::SyntaxError {
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "mlua-sys"
version = "0.8.2"
version = "0.8.3"
authors = ["Aleksandr Orlenko <zxteam@pm.me>"]
rust-version = "1.71"
edition = "2021"
@@ -41,7 +41,7 @@ cfg-if = "1.0"
pkg-config = "0.3.17"
lua-src = { version = ">= 548.1.0, < 548.2.0", optional = true }
luajit-src = { version = ">= 210.6.0, < 210.7.0", optional = true }
luau0-src = { version = "0.15.4", optional = true }
luau0-src = { version = "0.15.6", optional = true }
[lints.rust]
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(raw_dylib)'] }
+2 -1
View File
@@ -186,7 +186,8 @@ pub unsafe fn lua_isinteger(L: *mut lua_State, idx: c_int) -> c_int {
if lua_type(L, idx) == LUA_TNUMBER {
let n = lua_tonumber(L, idx);
let i = lua_tointeger(L, idx);
if (n - i as lua_Number).abs() < lua_Number::EPSILON {
// Lua 5.3+ returns "false" for `-0.0`
if n.to_bits() == (i as lua_Number).to_bits() {
return 1;
}
}
+2 -1
View File
@@ -51,7 +51,8 @@ pub unsafe fn lua_isinteger(L: *mut lua_State, idx: c_int) -> c_int {
if lua_type(L, idx) == LUA_TNUMBER {
let n = lua_tonumber(L, idx);
let i = lua_tointeger(L, idx);
if (n - i as lua_Number).abs() < lua_Number::EPSILON {
// Lua 5.3+ returns "false" for `-0.0`
if n.to_bits() == (i as lua_Number).to_bits() {
return 1;
}
}
+2 -1
View File
@@ -120,7 +120,8 @@ pub unsafe fn lua_isinteger(L: *mut lua_State, idx: c_int) -> c_int {
if lua_type(L, idx) == LUA_TNUMBER {
let n = lua_tonumber(L, idx);
let i = lua_tointeger(L, idx);
if (n - i as lua_Number).abs() < lua_Number::EPSILON {
// Lua 5.3+ returns "false" for `-0.0`
if n.to_bits() == (i as lua_Number).to_bits() {
return 1;
}
}
+1
View File
@@ -235,6 +235,7 @@ unsafe extern "C-unwind" {
) -> c_int;
pub fn lua_call(L: *mut lua_State, nargs: c_int, nresults: c_int);
pub fn lua_pcall(L: *mut lua_State, nargs: c_int, nresults: c_int, errfunc: c_int) -> c_int;
pub fn lua_cpcall(L: *mut lua_State, f: lua_CFunction, ud: *mut c_void) -> c_int;
//
// Coroutine functions
+79 -4
View File
@@ -1,3 +1,5 @@
use std::io;
#[cfg(feature = "serde")]
use serde::ser::{Serialize, Serializer};
@@ -50,13 +52,18 @@ impl Buffer {
#[track_caller]
pub fn write_bytes(&self, offset: usize, bytes: &[u8]) {
let lua = self.0.lua.lock();
let data = unsafe {
let (buf, size) = self.as_raw_parts(&lua);
std::slice::from_raw_parts_mut(buf, size)
};
let data = self.as_slice_mut(&lua);
data[offset..offset + bytes.len()].copy_from_slice(bytes);
}
/// Returns an adaptor implementing [`io::Read`], [`io::Write`] and [`io::Seek`] over the
/// buffer.
///
/// Buffer operations are infallible, none of the read/write functions will return a Err.
pub fn cursor(self) -> impl io::Read + io::Write + io::Seek {
BufferCursor(self, 0)
}
pub(crate) fn as_slice(&self, lua: &RawLua) -> &[u8] {
unsafe {
let (buf, size) = self.as_raw_parts(lua);
@@ -64,6 +71,14 @@ impl Buffer {
}
}
#[allow(clippy::mut_from_ref)]
fn as_slice_mut(&self, lua: &RawLua) -> &mut [u8] {
unsafe {
let (buf, size) = self.as_raw_parts(lua);
std::slice::from_raw_parts_mut(buf, size)
}
}
#[cfg(feature = "luau")]
unsafe fn as_raw_parts(&self, lua: &RawLua) -> (*mut u8, usize) {
let mut size = 0usize;
@@ -78,6 +93,66 @@ impl Buffer {
}
}
struct BufferCursor(Buffer, usize);
impl io::Read for BufferCursor {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let lua = self.0 .0.lua.lock();
let data = self.0.as_slice(&lua);
if self.1 == data.len() {
return Ok(0);
}
let len = buf.len().min(data.len() - self.1);
buf[..len].copy_from_slice(&data[self.1..self.1 + len]);
self.1 += len;
Ok(len)
}
}
impl io::Write for BufferCursor {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let lua = self.0 .0.lua.lock();
let data = self.0.as_slice_mut(&lua);
if self.1 == data.len() {
return Ok(0);
}
let len = buf.len().min(data.len() - self.1);
data[self.1..self.1 + len].copy_from_slice(&buf[..len]);
self.1 += len;
Ok(len)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl io::Seek for BufferCursor {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
let lua = self.0 .0.lua.lock();
let data = self.0.as_slice(&lua);
let new_offset = match pos {
io::SeekFrom::Start(offset) => offset as i64,
io::SeekFrom::End(offset) => data.len() as i64 + offset,
io::SeekFrom::Current(offset) => self.1 as i64 + offset,
};
if new_offset < 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid seek to a negative position",
));
}
if new_offset as usize > data.len() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid seek to a position beyond the end of the buffer",
));
}
self.1 = new_offset as usize;
Ok(self.1 as u64)
}
}
#[cfg(feature = "serde")]
impl Serialize for Buffer {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
+3 -7
View File
@@ -18,7 +18,7 @@ use {
crate::traits::LuaNativeAsyncFn,
crate::types::AsyncCallback,
std::future::{self, Future},
std::pin::Pin,
std::pin::{pin, Pin},
std::task::{Context, Poll},
};
@@ -669,13 +669,9 @@ impl<R: FromLuaMulti> Future for AsyncCallFuture<R> {
type Output = Result<R>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// Safety: We're not moving any pinned data
let this = unsafe { self.get_unchecked_mut() };
let this = self.get_mut();
match &mut this.0 {
Ok(thread) => {
let pinned_thread = unsafe { Pin::new_unchecked(thread) };
pinned_thread.poll(cx)
}
Ok(thread) => pin!(thread).poll(cx),
Err(err) => Poll::Ready(Err(err.clone())),
}
}
+4 -1
View File
@@ -142,7 +142,10 @@ pub use crate::{thread::AsyncThread, traits::LuaNativeAsyncFn};
#[cfg(feature = "serde")]
#[doc(inline)]
pub use crate::serde::{de::Options as DeserializeOptions, ser::Options as SerializeOptions, LuaSerdeExt};
pub use crate::{
serde::{de::Options as DeserializeOptions, ser::Options as SerializeOptions, LuaSerdeExt},
value::SerializableValue,
};
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
+21 -4
View File
@@ -129,7 +129,7 @@ impl TextRequirer {
}
fn normalize_chunk_name(chunk_name: &str) -> &str {
if let Some((path, line)) = chunk_name.split_once(':') {
if let Some((path, line)) = chunk_name.rsplit_once(':') {
if line.parse::<u32>().is_ok() {
return path;
}
@@ -567,10 +567,26 @@ pub(super) fn create_require_function<R: Require + MaybeSend + 'static>(
1
}
let (error, r#type) = unsafe {
lua.exec_raw::<(Function, Function)>((), move |state| {
unsafe extern "C-unwind" fn to_lowercase(state: *mut ffi::lua_State) -> c_int {
let s = ffi::luaL_checkstring(state, 1);
let s = CStr::from_ptr(s);
if !s.to_bytes().iter().any(|&c| c.is_ascii_uppercase()) {
// If the string does not contain any uppercase ASCII letters, return it as is
return 1;
}
callback_error_ext(state, ptr::null_mut(), true, |extra, _| {
let s = (s.to_bytes().iter())
.map(|&c| c.to_ascii_lowercase())
.collect::<bstr::BString>();
(*extra).raw_lua().push(s).map(|_| 1)
})
}
let (error, r#type, to_lowercase) = unsafe {
lua.exec_raw::<(Function, Function, Function)>((), move |state| {
ffi::lua_pushcfunctiond(state, error, cstr!("error"));
ffi::lua_pushcfunctiond(state, r#type, cstr!("type"));
ffi::lua_pushcfunctiond(state, to_lowercase, cstr!("to_lowercase"));
})
}?;
@@ -583,6 +599,7 @@ pub(super) fn create_require_function<R: Require + MaybeSend + 'static>(
env.raw_set("LOADER_CACHE", loader_cache)?;
env.raw_set("error", error)?;
env.raw_set("type", r#type)?;
env.raw_set("to_lowercase", to_lowercase)?;
lua.load(
r#"
@@ -592,7 +609,7 @@ pub(super) fn create_require_function<R: Require + MaybeSend + 'static>(
end
-- Check if the module (path) is explicitly registered
local maybe_result = REGISTERED_MODULES[path]
local maybe_result = REGISTERED_MODULES[to_lowercase(path)]
if maybe_result ~= nil then
return maybe_result
end
+9
View File
@@ -297,6 +297,15 @@ impl<T: IntoLua> IntoLuaMulti for Variadic<T> {
fn into_lua_multi(self, lua: &Lua) -> Result<MultiValue> {
MultiValue::from_lua_iter(lua, self)
}
unsafe fn push_into_stack_multi(self, lua: &RawLua) -> Result<c_int> {
let nresults = self.len() as i32;
check_stack(lua.state(), nresults + 1)?;
for value in self.0 {
value.push_into_stack(lua)?;
}
Ok(nresults)
}
}
impl<T: FromLua> FromLuaMulti for Variadic<T> {
+2 -1
View File
@@ -36,5 +36,6 @@ pub use crate::{AsyncThread as LuaAsyncThread, LuaNativeAsyncFn};
#[cfg(feature = "serde")]
#[doc(no_inline)]
pub use crate::{
DeserializeOptions as LuaDeserializeOptions, LuaSerdeExt, SerializeOptions as LuaSerializeOptions,
DeserializeOptions as LuaDeserializeOptions, LuaSerdeExt, SerializableValue as LuaSerializableValue,
SerializeOptions as LuaSerializeOptions,
};
+41 -9
View File
@@ -15,11 +15,12 @@ use crate::userdata::AnyUserData;
use crate::value::Value;
/// A struct for deserializing Lua values into Rust values.
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct Deserializer {
value: Value,
options: Options,
visited: Rc<RefCell<FxHashSet<*const c_void>>>,
len: Option<usize>, // A length hint for sequences
}
/// A struct with options to change default deserializer behavior.
@@ -54,6 +55,19 @@ pub struct Options {
///
/// Default: **false**
pub encode_empty_tables_as_array: bool,
/// If true, enable detection of mixed tables.
///
/// A mixed table is a table that has both array-like and map-like entries or several borders.
/// See [`The Length Operator`] documentation for details about borders.
///
/// When this option is disabled, a table with a non-zero length (with one or more borders) will
/// be always encoded as an array.
///
/// Default: **false**
///
/// [`The Length Operator`]: https://www.lua.org/manual/5.4/manual.html#3.4.7
pub detect_mixed_tables: bool,
}
impl Default for Options {
@@ -70,6 +84,7 @@ impl Options {
deny_recursive_tables: true,
sort_keys: false,
encode_empty_tables_as_array: false,
detect_mixed_tables: false,
}
}
@@ -108,6 +123,15 @@ impl Options {
self.encode_empty_tables_as_array = enabled;
self
}
/// Sets [`detect_mixed_tables`] option.
///
/// [`detect_mixed_tables`]: #structfield.detect_mixed_tables
#[must_use]
pub const fn detect_mixed_tables(mut self, enable: bool) -> Self {
self.detect_mixed_tables = enable;
self
}
}
impl Deserializer {
@@ -121,7 +145,7 @@ impl Deserializer {
Deserializer {
value,
options,
visited: Rc::new(RefCell::new(FxHashSet::default())),
..Default::default()
}
}
@@ -130,8 +154,14 @@ impl Deserializer {
value,
options,
visited,
..Default::default()
}
}
fn with_len(mut self, len: usize) -> Self {
self.len = Some(len);
self
}
}
impl<'de> serde::Deserializer<'de> for Deserializer {
@@ -155,11 +185,13 @@ impl<'de> serde::Deserializer<'de> for Deserializer {
Ok(s) => visitor.visit_str(&s),
Err(_) => visitor.visit_bytes(&s.as_bytes()),
},
Value::Table(ref t) if t.raw_len() > 0 || t.is_array() => self.deserialize_seq(visitor),
Value::Table(ref t) if self.options.encode_empty_tables_as_array && t.is_empty() => {
self.deserialize_seq(visitor)
Value::Table(ref t) => {
if let Some(len) = t.encode_as_array(self.options) {
self.with_len(len).deserialize_seq(visitor)
} else {
self.deserialize_map(visitor)
}
}
Value::Table(_) => self.deserialize_map(visitor),
Value::LightUserData(ud) if ud.0.is_null() => visitor.visit_none(),
Value::UserData(ud) if ud.is_serializable() => {
serde_userdata(ud, |value| value.deserialize_any(visitor))
@@ -270,14 +302,14 @@ impl<'de> serde::Deserializer<'de> for Deserializer {
Value::Table(t) => {
let _guard = RecursionGuard::new(&t, &self.visited);
let len = t.raw_len();
let len = self.len.unwrap_or_else(|| t.raw_len());
let mut deserializer = SeqDeserializer {
seq: t.sequence_values(),
seq: t.sequence_values().with_len(len),
options: self.options,
visited: self.visited,
};
let seq = visitor.visit_seq(&mut deserializer)?;
if deserializer.seq.count() == 0 {
if deserializer.seq.next().is_none() {
Ok(seq)
} else {
Err(de::Error::invalid_length(len, &"fewer elements in the table"))
+123 -17
View File
@@ -37,6 +37,7 @@ use crate::{buffer::Buffer, chunk::Compiler};
use {
crate::types::LightUserData,
std::future::{self, Future},
std::task::Poll,
};
#[cfg(feature = "serde")]
@@ -358,6 +359,8 @@ impl Lua {
if cfg!(feature = "luau") && !modname.starts_with('@') {
return Err(Error::runtime("module name must begin with '@'"));
}
#[cfg(feature = "luau")]
let modname = modname.to_ascii_lowercase();
unsafe {
self.exec_raw::<()>(value, |state| {
ffi::luaL_getsubtable(state, ffi::LUA_REGISTRYINDEX, LOADED_MODULES_KEY);
@@ -629,13 +632,13 @@ impl Lua {
///
/// Any Luau code is guaranteed to call this handler "eventually"
/// (in practice this can happen at any function call or at any loop iteration).
/// This is similar to `Lua::set_hook` but in more simplified form.
///
/// The provided interrupt function can error, and this error will be propagated through
/// the Luau code that was executing at the time the interrupt was triggered.
/// Also this can be used to implement continuous execution limits by instructing Luau VM to
/// yield by returning [`VmState::Yield`].
///
/// This is similar to `Lua::set_hook` but in more simplified form.
/// yield by returning [`VmState::Yield`]. The yield will happen only at yieldable points
/// of execution (not across metamethod/C-call boundaries).
///
/// # Example
///
@@ -693,7 +696,10 @@ impl Lua {
match result {
VmState::Continue => {}
VmState::Yield => {
ffi::lua_yield(state, 0);
// We can yield only at yieldable points, otherwise ignore and continue
if ffi::lua_isyieldable(state) != 0 {
ffi::lua_yield(state, 0);
}
}
}
}
@@ -1148,7 +1154,7 @@ impl Lua {
}
}
/// Create and return an interned Lua string.
/// Creates and returns an interned Lua string.
///
/// Lua strings can be arbitrary `[u8]` data including embedded nulls, so in addition to `&str`
/// and `&String`, you can also pass plain `&[u8]` here.
@@ -1157,27 +1163,32 @@ impl Lua {
unsafe { self.lock().create_string(s) }
}
/// Create and return a Luau [buffer] object from a byte slice of data.
/// Creates and returns a Luau [buffer] object from a byte slice of data.
///
/// [buffer]: https://luau.org/library#buffer-library
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub fn create_buffer(&self, buf: impl AsRef<[u8]>) -> Result<Buffer> {
pub fn create_buffer(&self, data: impl AsRef<[u8]>) -> Result<Buffer> {
let lua = self.lock();
let state = lua.state();
let data = data.as_ref();
unsafe {
if lua.unlikely_memory_error() {
crate::util::push_buffer(state, buf.as_ref(), false)?;
return Ok(Buffer(lua.pop_ref()));
}
let _sg = StackGuard::new(state);
check_stack(state, 3)?;
crate::util::push_buffer(state, buf.as_ref(), true)?;
Ok(Buffer(lua.pop_ref()))
let (ptr, buffer) = lua.create_buffer_with_capacity(data.len())?;
ptr.copy_from_nonoverlapping(data.as_ptr(), data.len());
Ok(buffer)
}
}
/// Creates and returns a Luau [buffer] object with the specified size.
///
/// Size limit is 1GB. All bytes will be initialized to zero.
///
/// [buffer]: https://luau.org/library#buffer-library
#[cfg(any(feature = "luau", doc))]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub fn create_buffer_with_capacity(&self, size: usize) -> Result<Buffer> {
unsafe { Ok(self.lock().create_buffer_with_capacity(size)?.1) }
}
/// Creates and returns a new empty table.
#[inline]
pub fn create_table(&self) -> Result<Table> {
@@ -2069,6 +2080,101 @@ impl Lua {
LightUserData(&ASYNC_POLL_TERMINATE as *const u8 as *mut std::os::raw::c_void)
}
#[cfg(feature = "async")]
#[inline(always)]
pub(crate) fn poll_yield() -> LightUserData {
static ASYNC_POLL_YIELD: u8 = 0;
LightUserData(&ASYNC_POLL_YIELD as *const u8 as *mut std::os::raw::c_void)
}
/// Suspends the current async function, returning the provided arguments to caller.
///
/// This function is similar to [`coroutine.yield`] but allow yeilding Rust functions
/// and passing values to the caller.
/// Please note that you cannot cross [`Thread`] boundaries (e.g. calling `yield_with` on one
/// thread and resuming on another).
///
/// # Examples
///
/// Async iterator:
///
/// ```
/// # use mlua::{Lua, Result};
/// #
/// async fn generator(lua: Lua, _: ()) -> Result<()> {
/// for i in 0..10 {
/// lua.yield_with::<()>(i).await?;
/// }
/// Ok(())
/// }
///
/// fn main() -> Result<()> {
/// let lua = Lua::new();
/// lua.globals().set("generator", lua.create_async_function(generator)?)?;
///
/// lua.load(r#"
/// local n = 0
/// for i in coroutine.wrap(generator) do
/// n = n + i
/// end
/// assert(n == 45)
/// "#)
/// .exec()
/// }
/// ```
///
/// Exchange values on yield:
///
/// ```
/// # use mlua::{Lua, Result, Value};
/// #
/// async fn pingpong(lua: Lua, mut val: i32) -> Result<()> {
/// loop {
/// val = lua.yield_with::<i32>(val).await? + 1;
/// }
/// Ok(())
/// }
///
/// # fn main() -> Result<()> {
/// let lua = Lua::new();
///
/// let co = lua.create_thread(lua.create_async_function(pingpong)?)?;
/// assert_eq!(co.resume::<i32>(1)?, 1);
/// assert_eq!(co.resume::<i32>(2)?, 3);
/// assert_eq!(co.resume::<i32>(3)?, 4);
///
/// # Ok(())
/// # }
/// ```
///
/// [`coroutine.yield`]: https://www.lua.org/manual/5.4/manual.html#pdf-coroutine.yield
#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
pub async fn yield_with<R: FromLuaMulti>(&self, args: impl IntoLuaMulti) -> Result<R> {
let mut args = Some(args.into_lua_multi(self)?);
future::poll_fn(move |_cx| match args.take() {
Some(args) => unsafe {
let lua = self.lock();
lua.push(Self::poll_yield())?; // yield marker
if args.len() <= 1 {
lua.push(args.front())?;
} else {
lua.push(lua.create_sequence_from(&args)?)?;
}
lua.push(args.len())?;
Poll::Pending
},
None => unsafe {
let lua = self.lock();
let state = lua.state();
let _sg = StackGuard::with_top(state, 0);
let nvals = ffi::lua_gettop(state);
Poll::Ready(R::from_stack_multi(nvals, &lua))
},
})
.await
}
/// Returns a weak reference to the Lua instance.
///
/// This is useful for creating a reference to the Lua instance that does not prevent it from
+1 -1
View File
@@ -64,7 +64,7 @@ pub(crate) struct ExtraData {
pub(super) wrapped_failure_top: usize,
// Pool of `Thread`s (coroutines) for async execution
#[cfg(feature = "async")]
pub(super) thread_pool: Vec<c_int>,
pub(super) thread_pool: Vec<crate::types::ValueRefIndex>,
// Address of `WrappedFailure` metatable
pub(super) wrapped_failure_mt_ptr: *const c_void,
+61 -22
View File
@@ -28,8 +28,8 @@ use crate::userdata::{
use crate::util::{
assert_stack, check_stack, get_destructed_userdata_metatable, get_internal_userdata, get_main_state,
get_metatable_ptr, get_userdata, init_error_registry, init_internal_metatable, pop_error,
push_internal_userdata, push_string, push_table, rawset_field, safe_pcall, safe_xpcall, short_type_name,
StackGuard, WrappedFailure,
push_internal_userdata, push_string, push_table, push_userdata, rawset_field, safe_pcall, safe_xpcall,
short_type_name, StackGuard, WrappedFailure,
};
use crate::value::{Nil, Value};
@@ -523,6 +523,20 @@ impl RawLua {
Ok(String(self.pop_ref()))
}
#[cfg(feature = "luau")]
pub(crate) unsafe fn create_buffer_with_capacity(&self, size: usize) -> Result<(*mut u8, crate::Buffer)> {
let state = self.state();
if self.unlikely_memory_error() {
let ptr = crate::util::push_buffer(state, size, false)?;
return Ok((ptr, crate::Buffer(self.pop_ref())));
}
let _sg = StackGuard::new(state);
check_stack(state, 3)?;
let ptr = crate::util::push_buffer(state, size, true)?;
Ok((ptr, crate::Buffer(self.pop_ref())))
}
/// See [`Lua::create_table_with_capacity`]
pub(crate) unsafe fn create_table_with_capacity(&self, narr: usize, nrec: usize) -> Result<Table> {
let state = self.state();
@@ -624,7 +638,7 @@ impl RawLua {
#[cfg(feature = "async")]
pub(crate) unsafe fn create_recycled_thread(&self, func: &Function) -> Result<Thread> {
if let Some(index) = (*self.extra.get()).thread_pool.pop() {
let thread_state = ffi::lua_tothread(self.ref_thread(), index);
let thread_state = ffi::lua_tothread(self.ref_thread(), *index.0);
ffi::lua_xpush(self.ref_thread(), thread_state, func.0.index);
#[cfg(feature = "luau")]
@@ -645,8 +659,9 @@ impl RawLua {
pub(crate) unsafe fn recycle_thread(&self, thread: &mut Thread) {
let extra = &mut *self.extra.get();
if extra.thread_pool.len() < extra.thread_pool.capacity() {
extra.thread_pool.push(thread.0.index);
thread.0.drop = false; // Prevent thread from being garbage collected
if let Some(index) = thread.0.index_count.take() {
extra.thread_pool.push(index);
}
}
}
@@ -728,7 +743,7 @@ impl RawLua {
let n = ffi::lua_tonumber(state, idx);
match num_traits::cast(n) {
Some(i) if (n - (i as Number)).abs() < Number::EPSILON => Value::Integer(i),
Some(i) if n.to_bits() == (i as Number).to_bits() => Value::Integer(i),
_ => Value::Number(n),
}
}
@@ -827,13 +842,6 @@ impl RawLua {
ValueRef::new(self, index)
}
#[inline]
pub(crate) unsafe fn clone_ref(&self, vref: &ValueRef) -> ValueRef {
ffi::lua_pushvalue(self.ref_thread(), vref.index);
let index = (*self.extra.get()).ref_stack_pop();
ValueRef::new(self, index)
}
pub(crate) unsafe fn drop_ref(&self, vref: &ValueRef) {
let ref_thread = self.ref_thread();
mlua_debug_assert!(
@@ -920,7 +928,7 @@ impl RawLua {
// We generate metatable first to make sure it *always* available when userdata pushed
let mt_id = get_metatable_id()?;
let protect = !self.unlikely_memory_error();
crate::util::push_userdata(state, data, protect)?;
push_userdata(state, data, protect)?;
ffi::lua_rawgeti(state, ffi::LUA_REGISTRYINDEX, mt_id);
ffi::lua_setmetatable(state, -2);
@@ -1048,6 +1056,18 @@ impl RawLua {
field_setters_index = Some(ffi::lua_absindex(state, -1));
}
// Create methods namecall table
#[cfg_attr(not(feature = "luau"), allow(unused_mut))]
let mut methods_map = None;
#[cfg(feature = "luau")]
if registry.enable_namecall {
let map: &mut rustc_hash::FxHashMap<_, crate::types::CallbackPtr> =
methods_map.get_or_insert_with(Default::default);
for (k, m) in &registry.methods {
map.insert(k.as_bytes().to_vec(), &**m);
}
}
let mut methods_index = None;
let methods_nrec = registry.methods.len();
#[cfg(feature = "async")]
@@ -1095,6 +1115,7 @@ impl RawLua {
field_getters_index,
field_setters_index,
methods_index,
methods_map,
)?;
// Update stack guard to keep metatable after return
@@ -1270,6 +1291,13 @@ impl RawLua {
let mut ctx = Context::from_waker(rawlua.waker());
match fut.as_mut().map(|fut| fut.as_mut().poll(&mut ctx)) {
Some(Poll::Pending) => {
let fut_nvals = ffi::lua_gettop(state);
if fut_nvals >= 3 && ffi::lua_tolightuserdata(state, -3) == Lua::poll_yield().0 {
// We have some values to yield
ffi::lua_pushnil(state);
ffi::lua_replace(state, -4);
return Ok(3);
}
ffi::lua_pushnil(state);
ffi::lua_pushlightuserdata(state, Lua::poll_pending().0);
Ok(2)
@@ -1340,6 +1368,7 @@ impl RawLua {
local poll = get_poll(...)
local nres, res, res2 = poll()
while true do
-- Poll::Ready branch, `nres` is the number of results
if nres ~= nil then
if nres == 0 then
return
@@ -1355,10 +1384,20 @@ impl RawLua {
return unpack(res, nres)
end
end
-- `res` is a "pending" value
-- `yield` can return a signal to drop the future that we should propagate
-- to the poller
nres, res, res2 = poll(yield(res))
-- Poll::Pending branch
if res2 == nil then
-- `res` is a "pending" value
-- `yield` can return a signal to drop the future that we should propagate
-- to the poller
nres, res, res2 = poll(yield(res))
elseif res2 == 0 then
nres, res, res2 = poll(yield())
elseif res2 == 1 then
nres, res, res2 = poll(yield(res))
else
nres, res, res2 = poll(yield(unpack(res, res2)))
end
end
"#,
)
@@ -1370,14 +1409,14 @@ impl RawLua {
#[cfg(feature = "async")]
#[inline]
pub(crate) unsafe fn waker(&self) -> &Waker {
(*self.extra.get()).waker.as_ref()
pub(crate) fn waker(&self) -> &Waker {
unsafe { (*self.extra.get()).waker.as_ref() }
}
#[cfg(feature = "async")]
#[inline]
pub(crate) unsafe fn set_waker(&self, waker: NonNull<Waker>) -> NonNull<Waker> {
mem::replace(&mut (*self.extra.get()).waker, waker)
pub(crate) fn set_waker(&self, waker: NonNull<Waker>) -> NonNull<Waker> {
unsafe { mem::replace(&mut (*self.extra.get()).waker, waker) }
}
}
+116 -31
View File
@@ -6,7 +6,7 @@ use std::string::String as StdString;
use crate::error::{Error, Result};
use crate::function::Function;
use crate::state::{LuaGuard, RawLua};
use crate::state::{LuaGuard, RawLua, WeakLua};
use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, ObjectLike};
use crate::types::{Integer, LuaType, ValueRef};
use crate::util::{assert_stack, check_stack, get_metatable_ptr, StackGuard};
@@ -416,14 +416,7 @@ impl Table {
lua.push_ref(&self.0);
// Clear array part
for i in 1..=ffi::lua_rawlen(state, -1) {
ffi::lua_pushnil(state);
ffi::lua_rawseti(state, -2, i as Integer);
}
// Clear hash part
// It must be safe as long as we don't use invalid keys
// This is safe as long as we don't assign new keys
ffi::lua_pushnil(state);
while ffi::lua_next(state, -2) != 0 {
ffi::lua_pop(state, 1); // pop value
@@ -675,16 +668,25 @@ impl Table {
guard: self.0.lua.lock(),
table: self,
index: 1,
len: None,
_phantom: PhantomData,
}
}
/// Iterates over the sequence part of the table, invoking the given closure on each value.
///
/// This methods is similar to [`Table::sequence_values`], but optimized for performance.
#[doc(hidden)]
pub fn for_each_value<V>(&self, mut f: impl FnMut(V) -> Result<()>) -> Result<()>
where
V: FromLua,
{
pub fn for_each_value<V: FromLua>(&self, f: impl FnMut(V) -> Result<()>) -> Result<()> {
self.for_each_value_by_len(None, f)
}
fn for_each_value_by_len<V: FromLua>(
&self,
len: impl Into<Option<usize>>,
mut f: impl FnMut(V) -> Result<()>,
) -> Result<()> {
let len = len.into();
let lua = self.0.lua.lock();
let state = lua.state();
unsafe {
@@ -692,9 +694,14 @@ impl Table {
check_stack(state, 4)?;
lua.push_ref(&self.0);
let len = ffi::lua_rawlen(state, -1);
for i in 1..=len {
ffi::lua_rawgeti(state, -1, i as _);
for i in 1.. {
if len.map(|len| i > len).unwrap_or(false) {
break;
}
let t = ffi::lua_rawgeti(state, -1, i as _);
if len.is_none() && t == ffi::LUA_TNIL {
break;
}
f(V::from_stack(-1, &lua)?)?;
ffi::lua_pop(state, 1);
}
@@ -727,8 +734,9 @@ impl Table {
Ok(())
}
/// Checks if the table has the array metatable attached.
#[cfg(feature = "serde")]
pub(crate) fn is_array(&self) -> bool {
fn has_array_metatable(&self) -> bool {
let lua = self.0.lua.lock();
let state = lua.state();
unsafe {
@@ -744,6 +752,70 @@ impl Table {
}
}
/// If the table is an array, returns the number of non-nil elements and max index.
///
/// Returns `None` if the table is not an array.
///
/// This operation has O(n) complexity.
#[cfg(feature = "serde")]
fn find_array_len(&self) -> Option<(usize, usize)> {
let lua = self.0.lua.lock();
let ref_thread = lua.ref_thread();
unsafe {
let _sg = StackGuard::new(ref_thread);
let (mut count, mut max_index) = (0, 0);
ffi::lua_pushnil(ref_thread);
while ffi::lua_next(ref_thread, self.0.index) != 0 {
if ffi::lua_type(ref_thread, -2) != ffi::LUA_TNUMBER {
return None;
}
let k = ffi::lua_tonumber(ref_thread, -2);
if k.trunc() != k || k < 1.0 {
return None;
}
max_index = std::cmp::max(max_index, k as usize);
count += 1;
ffi::lua_pop(ref_thread, 1);
}
Some((count, max_index))
}
}
/// Determines if the table should be encoded as an array or a map.
///
/// The algorithm is the following:
/// 1. If `detect_mixed_tables` is enabled, iterate over all keys in the table checking is they
/// all are positive integers. If non-array key is found, return `None` (encode as map).
/// Otherwise check the sparsity of the array. Too sparse arrays are encoded as maps.
///
/// 2. If `detect_mixed_tables` is disabled, check if the table has a positive length or has the
/// array metatable. If so, encode as array. If the table is empty and
/// `encode_empty_tables_as_array` is enabled, encode as array.
///
/// Returns the length of the array if it should be encoded as an array.
#[cfg(feature = "serde")]
pub(crate) fn encode_as_array(&self, options: crate::serde::de::Options) -> Option<usize> {
if options.detect_mixed_tables {
if let Some((len, max_idx)) = self.find_array_len() {
// If the array is too sparse, serialize it as a map instead
if len < 10 || len * 2 >= max_idx {
return Some(max_idx);
}
}
} else {
let len = self.raw_len();
if len > 0 || self.has_array_metatable() {
return Some(len);
}
if options.encode_empty_tables_as_array && self.is_empty() {
return Some(0);
}
}
None
}
#[cfg(feature = "luau")]
#[inline(always)]
fn check_readonly_write(&self, lua: &RawLua) -> Result<()> {
@@ -884,7 +956,7 @@ impl ObjectLike for Table {
R: FromLuaMulti,
{
// Convert table to a function and call via pcall that respects the `__call` metamethod.
Function(self.0.copy()).call(args)
Function(self.0.clone()).call(args)
}
#[cfg(feature = "async")]
@@ -893,7 +965,7 @@ impl ObjectLike for Table {
where
R: FromLuaMulti,
{
Function(self.0.copy()).call_async(args)
Function(self.0.clone()).call_async(args)
}
#[inline]
@@ -941,7 +1013,17 @@ impl ObjectLike for Table {
#[inline]
fn to_string(&self) -> Result<StdString> {
Value::Table(Table(self.0.copy())).to_string()
Value::Table(Table(self.0.clone())).to_string()
}
#[inline]
fn to_value(&self) -> Value {
Value::Table(self.clone())
}
#[inline]
fn weak_lua(&self) -> &WeakLua {
&self.0.lua
}
}
@@ -977,6 +1059,15 @@ impl<'a> SerializableTable<'a> {
}
}
impl<V> TableSequence<'_, V> {
/// Sets the length (hint) of the sequence.
#[cfg(feature = "serde")]
pub(crate) fn with_len(mut self, len: usize) -> Self {
self.len = Some(len);
self
}
}
#[cfg(feature = "serde")]
impl Serialize for SerializableTable<'_> {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error>
@@ -998,14 +1089,10 @@ impl Serialize for SerializableTable<'_> {
let _guard = RecursionGuard::new(self.table, visited);
// Array
let len = self.table.raw_len();
if len > 0
|| self.table.is_array()
|| (self.options.encode_empty_tables_as_array && self.table.is_empty())
{
if let Some(len) = self.table.encode_as_array(self.options) {
let mut seq = serializer.serialize_seq(Some(len))?;
let mut serialize_err = None;
let res = self.table.for_each_value::<Value>(|value| {
let res = self.table.for_each_value_by_len::<Value>(len, |value| {
let skip = check_value_for_skip(&value, self.options, visited)
.map_err(|err| Error::SerializeError(err.to_string()))?;
if skip {
@@ -1129,13 +1216,11 @@ pub struct TableSequence<'a, V> {
guard: LuaGuard,
table: &'a Table,
index: Integer,
len: Option<usize>,
_phantom: PhantomData<V>,
}
impl<V> Iterator for TableSequence<'_, V>
where
V: FromLua,
{
impl<V: FromLua> Iterator for TableSequence<'_, V> {
type Item = Result<V>;
fn next(&mut self) -> Option<Self::Item> {
@@ -1149,7 +1234,7 @@ where
lua.push_ref(&self.table.0);
match ffi::lua_rawgeti(state, -1, self.index) {
ffi::LUA_TNIL => None,
ffi::LUA_TNIL if self.index as usize > self.len.unwrap_or(0) => None,
_ => {
self.index += 1;
Some(V::from_stack(-1, lua))
+5 -5
View File
@@ -156,7 +156,6 @@ impl Thread {
let thread_state = self.state();
unsafe {
let _sg = StackGuard::new(state);
let _thread_sg = StackGuard::with_top(thread_state, 0);
let nargs = args.push_into_stack_multi(&lua)?;
if nargs > 0 {
@@ -165,6 +164,7 @@ impl Thread {
pushed_nargs += nargs;
}
let _thread_sg = StackGuard::with_top(thread_state, 0);
let (_, nresults) = self.resume_inner(&lua, pushed_nargs)?;
check_stack(state, nresults + 1)?;
ffi::lua_xmove(thread_state, state, nresults);
@@ -192,12 +192,12 @@ impl Thread {
let thread_state = self.state();
unsafe {
let _sg = StackGuard::new(state);
let _thread_sg = StackGuard::with_top(thread_state, 0);
check_stack(state, 1)?;
error.push_into_stack(&lua)?;
ffi::lua_xmove(state, thread_state, 1);
let _thread_sg = StackGuard::with_top(thread_state, 0);
let (_, nresults) = self.resume_inner(&lua, ffi::LUA_RESUMEERROR)?;
check_stack(state, nresults + 1)?;
ffi::lua_xmove(thread_state, state, nresults);
@@ -604,7 +604,7 @@ impl<R: FromLuaMulti> Future for AsyncThread<R> {
if status.is_yielded() {
if !(nresults == 1 && is_poll_pending(thread_state)) {
// Ignore value returned via yield()
// Ignore values returned via yield()
cx.waker().wake_by_ref();
}
return Poll::Pending;
@@ -635,7 +635,7 @@ struct WakerGuard<'lua, 'a> {
impl<'lua, 'a> WakerGuard<'lua, 'a> {
#[inline]
pub fn new(lua: &'lua RawLua, waker: &'a Waker) -> Result<WakerGuard<'lua, 'a>> {
let prev = unsafe { lua.set_waker(NonNull::from(waker)) };
let prev = lua.set_waker(NonNull::from(waker));
Ok(WakerGuard {
lua,
prev,
@@ -647,7 +647,7 @@ impl<'lua, 'a> WakerGuard<'lua, 'a> {
#[cfg(feature = "async")]
impl Drop for WakerGuard<'_, '_> {
fn drop(&mut self) {
unsafe { self.lua.set_waker(self.prev) };
self.lua.set_waker(self.prev);
}
}
+42 -2
View File
@@ -5,9 +5,9 @@ use std::sync::Arc;
use crate::error::{Error, Result};
use crate::multi::MultiValue;
use crate::private::Sealed;
use crate::state::{Lua, RawLua};
use crate::state::{Lua, RawLua, WeakLua};
use crate::types::MaybeSend;
use crate::util::{check_stack, short_type_name};
use crate::util::{check_stack, parse_lookup_path, short_type_name};
use crate::value::Value;
#[cfg(feature = "async")]
@@ -200,10 +200,50 @@ pub trait ObjectLike: Sealed {
where
R: FromLuaMulti;
/// Look up a value by a path of keys.
///
/// The syntax is similar to accessing nested tables in Lua, with additional support for
/// `?` operator to perform safe navigation.
///
/// For example, the path `a[1].c` is equivalent to `table.a[1].c` in Lua.
/// With `?` operator, `a[1]?.c` is equivalent to `table.a[1] and table.a[1].c or nil` in Lua.
///
/// Bracket notation rules:
/// - `[123]` - integer keys
/// - `["string key"]` or `['string key']` - string keys (must be quoted)
/// - String keys support escape sequences: `\"`, `\'`, `\\`
fn get_path<V: FromLua>(&self, path: &str) -> Result<V> {
let mut current = self.to_value();
for (key, safe_nil) in parse_lookup_path(path)? {
current = match current {
Value::Table(table) => table.get::<Value>(key),
Value::UserData(ud) => ud.get::<Value>(key),
_ => {
let type_name = current.type_name();
let err = format!("attempt to index a {type_name} value with key '{key}'");
Err(Error::runtime(err))
}
}?;
if safe_nil && (current == Value::Nil || current == Value::NULL) {
break;
}
}
let lua = self.weak_lua().lock();
V::from_lua(current, lua.lua())
}
/// Converts the object to a string in a human-readable format.
///
/// This might invoke the `__tostring` metamethod.
fn to_string(&self) -> Result<StdString>;
/// Converts the object to a Lua value.
fn to_value(&self) -> Value;
/// Gets a reference to the associated Lua state.
#[doc(hidden)]
fn weak_lua(&self) -> &WeakLua;
}
/// A trait for types that can be used as Lua functions.
+8 -2
View File
@@ -20,6 +20,9 @@ pub use either::Either;
pub use registry_key::RegistryKey;
pub(crate) use value_ref::ValueRef;
#[cfg(feature = "async")]
pub(crate) use value_ref::ValueRefIndex;
/// Type of Lua integer numbers.
pub type Integer = ffi::lua_Integer;
/// Type of Lua floating point numbers.
@@ -35,10 +38,13 @@ unsafe impl Send for LightUserData {}
unsafe impl Sync for LightUserData {}
#[cfg(feature = "send")]
pub(crate) type Callback = Box<dyn Fn(&RawLua, c_int) -> Result<c_int> + Send + 'static>;
type CallbackFn<'a> = dyn Fn(&RawLua, c_int) -> Result<c_int> + Send + 'a;
#[cfg(not(feature = "send"))]
pub(crate) type Callback = Box<dyn Fn(&RawLua, c_int) -> Result<c_int> + 'static>;
type CallbackFn<'a> = dyn Fn(&RawLua, c_int) -> Result<c_int> + 'a;
pub(crate) type Callback = Box<CallbackFn<'static>>;
pub(crate) type CallbackPtr = *const CallbackFn<'static>;
pub(crate) type ScopedCallback<'s> = Box<dyn Fn(&RawLua, c_int) -> Result<c_int> + 's>;
+28 -23
View File
@@ -1,22 +1,39 @@
use std::fmt;
use std::os::raw::{c_int, c_void};
use super::XRc;
use crate::state::{RawLua, WeakLua};
/// A reference to a Lua (complex) value stored in the Lua auxiliary thread.
#[derive(Clone)]
pub struct ValueRef {
pub(crate) lua: WeakLua,
// Keep index separate to avoid additional indirection when accessing it.
pub(crate) index: c_int,
pub(crate) drop: bool,
// If `index_count` is `None`, the value does not need to be destroyed.
pub(crate) index_count: Option<ValueRefIndex>,
}
/// A reference to a Lua value index in the auxiliary thread.
/// It's cheap to clone and can be used to track the number of references to a value.
#[derive(Clone)]
pub(crate) struct ValueRefIndex(pub(crate) XRc<c_int>);
impl From<c_int> for ValueRefIndex {
#[inline]
fn from(index: c_int) -> Self {
ValueRefIndex(XRc::new(index))
}
}
impl ValueRef {
#[inline]
pub(crate) fn new(lua: &RawLua, index: c_int) -> Self {
pub(crate) fn new(lua: &RawLua, index: impl Into<ValueRefIndex>) -> Self {
let index = index.into();
ValueRef {
lua: lua.weak().clone(),
index,
drop: true,
index: *index.0,
index_count: Some(index),
}
}
@@ -25,16 +42,6 @@ impl ValueRef {
let lua = self.lua.lock();
unsafe { ffi::lua_topointer(lua.ref_thread(), self.index) }
}
/// Returns a copy of the value, which is valid as long as the original value is held.
#[inline]
pub(crate) fn copy(&self) -> Self {
ValueRef {
lua: self.lua.clone(),
index: self.index,
drop: false,
}
}
}
impl fmt::Debug for ValueRef {
@@ -43,17 +50,15 @@ impl fmt::Debug for ValueRef {
}
}
impl Clone for ValueRef {
fn clone(&self) -> Self {
unsafe { self.lua.lock().clone_ref(self) }
}
}
impl Drop for ValueRef {
fn drop(&mut self) {
if self.drop {
if let Some(lua) = self.lua.try_lock() {
unsafe { lua.drop_ref(self) };
if let Some(ValueRefIndex(index)) = self.index_count.take() {
// It's guaranteed that the inner value returns exactly once.
// This means in particular that the value is not dropped.
if XRc::into_inner(index).is_some() {
if let Some(lua) = self.lua.try_lock() {
unsafe { lua.drop_ref(self) };
}
}
}
}
+16 -5
View File
@@ -1,6 +1,7 @@
use std::string::String as StdString;
use crate::error::{Error, Result};
use crate::state::WeakLua;
use crate::table::Table;
use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, ObjectLike};
use crate::userdata::AnyUserData;
@@ -15,14 +16,14 @@ impl ObjectLike for AnyUserData {
fn get<V: FromLua>(&self, key: impl IntoLua) -> Result<V> {
// `lua_gettable` method used under the hood can work with any Lua value
// that has `__index` metamethod
Table(self.0.copy()).get_protected(key)
Table(self.0.clone()).get_protected(key)
}
#[inline]
fn set(&self, key: impl IntoLua, value: impl IntoLua) -> Result<()> {
// `lua_settable` method used under the hood can work with any Lua value
// that has `__newindex` metamethod
Table(self.0.copy()).set_protected(key, value)
Table(self.0.clone()).set_protected(key, value)
}
#[inline]
@@ -30,7 +31,7 @@ impl ObjectLike for AnyUserData {
where
R: FromLuaMulti,
{
Function(self.0.copy()).call(args)
Function(self.0.clone()).call(args)
}
#[cfg(feature = "async")]
@@ -39,7 +40,7 @@ impl ObjectLike for AnyUserData {
where
R: FromLuaMulti,
{
Function(self.0.copy()).call_async(args)
Function(self.0.clone()).call_async(args)
}
#[inline]
@@ -88,6 +89,16 @@ impl ObjectLike for AnyUserData {
#[inline]
fn to_string(&self) -> Result<StdString> {
Value::UserData(AnyUserData(self.0.copy())).to_string()
Value::UserData(self.clone()).to_string()
}
#[inline]
fn to_value(&self) -> Value {
Value::UserData(self.clone())
}
#[inline]
fn weak_lua(&self) -> &WeakLua {
&self.0.lua
}
}
+22
View File
@@ -56,6 +56,9 @@ pub(crate) struct RawUserDataRegistry {
pub(crate) destructor: ffi::lua_CFunction,
pub(crate) type_id: Option<TypeId>,
pub(crate) type_name: StdString,
#[cfg(feature = "luau")]
pub(crate) enable_namecall: bool,
}
impl UserDataType {
@@ -100,6 +103,8 @@ impl<T> UserDataRegistry<T> {
destructor: super::util::destroy_userdata_storage::<T>,
type_id: r#type.type_id(),
type_name: short_type_name::<T>(),
#[cfg(feature = "luau")]
enable_namecall: false,
};
UserDataRegistry {
@@ -110,6 +115,23 @@ impl<T> UserDataRegistry<T> {
}
}
/// Enables support for the namecall optimization in Luau.
///
/// This enables methods resolution optimization in Luau for complex userdata types with methods
/// and field getters. When enabled, Luau will use a faster lookup path for method calls when a
/// specific syntax is used (e.g. `obj:method()`.
///
/// This optimization does not play well with async methods, custom `__index` metamethod and
/// field getters as functions. So, it is disabled by default.
///
/// Use with caution.
#[doc(hidden)]
#[cfg(feature = "luau")]
#[cfg_attr(docsrs, doc(cfg(feature = "luau")))]
pub fn enable_namecall(&mut self) {
self.raw.enable_namecall = true;
}
fn box_method<M, A, R>(&self, name: &str, method: M) -> Callback
where
M: Fn(&Lua, &T, A) -> Result<R> + MaybeSend + 'static,
+41
View File
@@ -4,8 +4,11 @@ use std::marker::PhantomData;
use std::os::raw::c_int;
use std::ptr;
use rustc_hash::FxHashMap;
use super::UserDataStorage;
use crate::error::{Error, Result};
use crate::types::CallbackPtr;
use crate::util::{get_userdata, rawget_field, rawset_field, take_userdata};
// This is a trick to check if a type is `Sync` or not.
@@ -244,6 +247,7 @@ pub(crate) unsafe fn init_userdata_metatable(
field_getters: Option<c_int>,
field_setters: Option<c_int>,
methods: Option<c_int>,
_methods_map: Option<FxHashMap<Vec<u8>, CallbackPtr>>, // Used only in Luau for `__namecall`
) -> Result<()> {
if field_getters.is_some() || methods.is_some() {
// Push `__index` generator function
@@ -267,6 +271,13 @@ pub(crate) unsafe fn init_userdata_metatable(
}
rawset_field(state, metatable, "__index")?;
#[cfg(feature = "luau")]
if let Some(methods_map) = _methods_map {
// In Luau we can speedup method calls by providing a dedicated `__namecall` metamethod
push_userdata_metatable_namecall(state, methods_map)?;
rawset_field(state, metatable, "__namecall")?;
}
}
if let Some(field_setters) = field_setters {
@@ -425,6 +436,36 @@ unsafe fn init_userdata_metatable_newindex(state: *mut ffi::lua_State) -> Result
})
}
#[cfg(feature = "luau")]
unsafe fn push_userdata_metatable_namecall(
state: *mut ffi::lua_State,
methods_map: FxHashMap<Vec<u8>, CallbackPtr>,
) -> Result<()> {
unsafe extern "C-unwind" fn namecall(state: *mut ffi::lua_State) -> c_int {
let name = ffi::lua_namecallatom(state, ptr::null_mut());
if name.is_null() {
ffi::luaL_error(state, cstr!("attempt to call an unknown method"));
}
let name_cs = std::ffi::CStr::from_ptr(name);
let methods_map = get_userdata::<FxHashMap<Vec<u8>, CallbackPtr>>(state, ffi::lua_upvalueindex(1));
let callback_ptr = match (*methods_map).get(name_cs.to_bytes()) {
Some(ptr) => *ptr,
#[rustfmt::skip]
None => ffi::luaL_error(state, cstr!("attempt to call an unknown method '%s'"), name),
};
crate::state::callback_error_ext(state, ptr::null_mut(), true, |extra, nargs| {
let rawlua = (*extra).raw_lua();
(*callback_ptr)(rawlua, nargs)
})
}
// Automatic destructor is provided for any Luau userdata
crate::util::push_userdata(state, methods_map, true)?;
protect_lua!(state, 1, 1, |state| {
ffi::lua_pushcclosured(state, namecall, cstr!("__namecall"), 1);
})
}
// This method is called by Lua GC when it's time to collect the userdata.
//
// This method is usually used to collect internal userdata.
+2
View File
@@ -402,6 +402,8 @@ pub(crate) unsafe fn init_error_registry(state: *mut ffi::lua_State) -> Result<(
"__ipairs",
#[cfg(feature = "luau")]
"__iter",
#[cfg(feature = "luau")]
"__namecall",
#[cfg(feature = "lua54")]
"__close",
] {
+8 -8
View File
@@ -9,6 +9,7 @@ pub(crate) use error::{
error_traceback, error_traceback_thread, init_error_registry, pop_error, protect_lua_call,
protect_lua_closure, WrappedFailure,
};
pub(crate) use path::parse_path as parse_lookup_path;
pub(crate) use short_names::short_type_name;
pub(crate) use types::TypeKey;
pub(crate) use userdata::{
@@ -101,15 +102,13 @@ pub(crate) unsafe fn push_string(state: *mut ffi::lua_State, s: &[u8], protect:
// Uses 3 stack spaces (when protect), does not call checkstack.
#[cfg(feature = "luau")]
#[inline(always)]
pub(crate) unsafe fn push_buffer(state: *mut ffi::lua_State, b: &[u8], protect: bool) -> Result<()> {
let data = if protect {
protect_lua!(state, 0, 1, |state| ffi::lua_newbuffer(state, b.len()))?
pub(crate) unsafe fn push_buffer(state: *mut ffi::lua_State, size: usize, protect: bool) -> Result<*mut u8> {
let data = if protect || size > const { 1024 * 1024 * 1024 } {
protect_lua!(state, 0, 1, |state| ffi::lua_newbuffer(state, size))?
} else {
ffi::lua_newbuffer(state, b.len())
ffi::lua_newbuffer(state, size)
};
let buf = slice::from_raw_parts_mut(data as *mut u8, b.len());
buf.copy_from_slice(b);
Ok(())
Ok(data as *mut u8)
}
// Uses 3 stack spaces, does not call checkstack.
@@ -122,7 +121,7 @@ pub(crate) unsafe fn push_table(
) -> Result<()> {
let narr: c_int = narr.try_into().unwrap_or(c_int::MAX);
let nrec: c_int = nrec.try_into().unwrap_or(c_int::MAX);
if protect || narr >= const { 1 << 30 } || nrec >= const { 1 << 27 } {
if protect || narr >= const { 1 << 26 } || nrec >= const { 1 << 26 } {
protect_lua!(state, 0, 1, |state| ffi::lua_createtable(state, narr, nrec))
} else {
ffi::lua_createtable(state, narr, nrec);
@@ -329,6 +328,7 @@ pub(crate) fn linenumber_to_usize(n: c_int) -> Option<usize> {
}
mod error;
mod path;
mod short_names;
mod types;
mod userdata;
+255
View File
@@ -0,0 +1,255 @@
use std::borrow::Cow;
use std::fmt;
use std::iter::Peekable;
use std::str::CharIndices;
use crate::error::{Error, Result};
use crate::state::Lua;
use crate::traits::IntoLua;
use crate::types::Integer;
use crate::value::Value;
#[derive(Debug)]
pub(crate) enum PathKey<'a> {
Str(Cow<'a, str>),
Int(Integer),
}
impl fmt::Display for PathKey<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PathKey::Str(s) => write!(f, "{}", s),
PathKey::Int(i) => write!(f, "{}", i),
}
}
}
impl IntoLua for PathKey<'_> {
fn into_lua(self, lua: &Lua) -> Result<Value> {
match self {
PathKey::Str(s) => Ok(Value::String(lua.create_string(s.as_ref())?)),
PathKey::Int(i) => Ok(Value::Integer(i)),
}
}
}
// Parses a path like `a.b[3]?.c["d"]` into segments of `(key, safe_nil)`.
pub(crate) fn parse_path<'a>(path: &'a str) -> Result<Vec<(PathKey<'a>, bool)>> {
fn read_ident<'a>(path: &'a str, chars: &mut Peekable<CharIndices<'a>>) -> (Cow<'a, str>, bool) {
let mut safe_nil = false;
let start = chars.peek().map(|&(i, _)| i).unwrap_or(path.len());
let mut end = start;
while let Some(&(pos, c)) = chars.peek() {
if c == '.' || c == '?' || c.is_ascii_whitespace() || c == '[' {
if c == '?' {
safe_nil = true;
chars.next(); // consume '?'
}
break;
}
end = pos + c.len_utf8();
chars.next();
}
(Cow::Borrowed(&path[start..end]), safe_nil)
}
let mut segments = Vec::new();
let mut chars = path.char_indices().peekable();
while let Some(&(pos, next)) = chars.peek() {
match next {
'.' => {
// Dot notation: identifier
chars.next();
let (key, safe_nil) = read_ident(path, &mut chars);
if key.is_empty() {
return Err(Error::runtime(format!("empty key in path at position {pos}")));
}
segments.push((PathKey::Str(key), safe_nil));
}
'[' => {
// Bracket notation: either integer or quoted string
chars.next();
let key = match chars.peek() {
Some(&(pos, c @ '0'..='9' | c @ '-')) => {
// Integer key
let negative = c == '-';
if negative {
chars.next(); // consume '-'
}
let mut num: Option<Integer> = None;
while let Some(&(_, c @ '0'..='9')) = chars.peek() {
let new_num = num
.unwrap_or(0)
.checked_mul(10)
.and_then(|n| n.checked_add((c as u8 - b'0') as Integer))
.ok_or_else(|| {
Error::runtime(format!("integer overflow in path at position {pos}"))
})?;
num = Some(new_num);
chars.next(); // consume digit
}
match num {
Some(n) if negative => PathKey::Int(-n),
Some(n) => PathKey::Int(n),
None => {
let err = format!("invalid integer in path at position {pos}");
return Err(Error::runtime(err));
}
}
}
Some((_, '\'' | '"')) => {
// Quoted string
PathKey::Str(unquote_string(path, &mut chars)?)
}
Some((_, ']')) => {
return Err(Error::runtime(format!("empty key in path at position {pos}")));
}
Some((pos, c)) => {
let err = format!("unexpected character '{c}' in path at position {pos}");
return Err(Error::runtime(err));
}
None => {
return Err(Error::runtime("unexpected end of path"));
}
};
// Expect closing bracket
let mut safe_nil = false;
match chars.next() {
Some((_, ']')) => {
// Check for optional safe-nil operator
if let Some(&(_, '?')) = chars.peek() {
safe_nil = true;
chars.next(); // consume '?'
}
}
Some((pos, c)) => {
let err = format!("expected ']' in path at position {pos}, found '{c}'");
return Err(Error::runtime(err));
}
None => {
return Err(Error::runtime("unexpected end of path"));
}
}
segments.push((key, safe_nil));
}
c if c.is_ascii_whitespace() => {
chars.next(); // Skip whitespace
}
_ if segments.is_empty() => {
// First segment without dot/bracket notation
let (key_cow, safe_nil) = read_ident(path, &mut chars);
if key_cow.is_empty() {
return Err(Error::runtime(format!("empty key in path at position {pos}")));
}
segments.push((PathKey::Str(key_cow), safe_nil));
}
c => {
let err = format!("unexpected character '{c}' in path at position {pos}");
return Err(Error::runtime(err));
}
}
}
Ok(segments)
}
fn unquote_string<'a>(path: &'a str, chars: &mut Peekable<CharIndices<'a>>) -> Result<Cow<'a, str>> {
let (start_pos, first_quote) = chars.next().unwrap();
let mut result = String::new();
loop {
match chars.next() {
Some((pos, '\\')) => {
if result.is_empty() {
// First escape found, copy everything up to this point
result.push_str(&path[start_pos + 1..pos]);
}
match chars.next() {
Some((_, '\\')) => result.push('\\'),
Some((_, '"')) => result.push('"'),
Some((_, '\'')) => result.push('\''),
Some((_, other)) => {
result.push('\\');
result.push(other);
}
None => continue, // will be handled by outer loop
}
}
Some((pos, c)) if c == first_quote => {
if !result.is_empty() {
return Ok(Cow::Owned(result));
}
// No escapes, return borrowed slice
return Ok(Cow::Borrowed(&path[start_pos + 1..pos]));
}
Some((_, c)) => {
if !result.is_empty() {
result.push(c);
}
// If no escapes yet, continue tracking for potential borrowed slice
}
None => {
let err = format!("unexpected end of string at position {start_pos}");
return Err(Error::runtime(err));
}
}
}
}
#[cfg(test)]
mod tests {
use super::{parse_path, PathKey};
#[test]
fn test_parse_path() {
// Test valid paths
let path = parse_path("a.b[3]?.c['d']").unwrap();
assert_eq!(path.len(), 5);
assert!(matches!(path[0], (PathKey::Str(ref s), false) if s == "a"));
assert!(matches!(path[1], (PathKey::Str(ref s), false) if s == "b"));
assert!(matches!(path[2], (PathKey::Int(3), true)));
assert!(matches!(path[3], (PathKey::Str(ref s), false) if s == "c"));
assert!(matches!(path[4], (PathKey::Str(ref s), false) if s == "d"));
// Test empty path
let path = parse_path("").unwrap();
assert_eq!(path.len(), 0);
let path = parse_path(" ").unwrap();
assert_eq!(path.len(), 0);
// Test invalid dot syntax
let err = parse_path("a..b").unwrap_err().to_string();
assert_eq!(err, "runtime error: empty key in path at position 1");
let err = parse_path("a.b.").unwrap_err().to_string();
assert_eq!(err, "runtime error: empty key in path at position 3");
// Test invalid bracket syntax
let err = parse_path("a[unclosed").unwrap_err().to_string();
assert_eq!(
err,
"runtime error: unexpected character 'u' in path at position 2"
);
let err = parse_path("a[]").unwrap_err().to_string();
assert_eq!(err, "runtime error: empty key in path at position 1");
let err = parse_path(r#"a["unclosed"#).unwrap_err().to_string();
assert_eq!(err, "runtime error: unexpected end of string at position 2");
let err = parse_path(r#"a["#).unwrap_err().to_string();
assert_eq!(err, "runtime error: unexpected end of path");
let err = parse_path(r#"a[123"#).unwrap_err().to_string();
assert_eq!(err, "runtime error: unexpected end of path");
let err = parse_path(r#"a['bla'123"#).unwrap_err().to_string();
assert_eq!(
err,
"runtime error: expected ']' in path at position 7, found '1'"
);
let err = parse_path(r#"a["bla"]x"#).unwrap_err().to_string();
assert_eq!(
err,
"runtime error: unexpected character 'x' in path at position 8"
);
// Test bad integers
let err = parse_path("a[99999999999999999999]").unwrap_err().to_string();
assert_eq!(err, "runtime error: integer overflow in path at position 2");
let err = parse_path("a[-]").unwrap_err().to_string();
assert_eq!(err, "runtime error: invalid integer in path at position 2");
}
}
+18 -8
View File
@@ -1,6 +1,6 @@
//! Mostly copied from [bevy_utils]
//! Inspired by bevy's [disqualified]
//!
//! [bevy_utils]: https://github.com/bevyengine/bevy/blob/main/crates/bevy_utils/src/short_names.rs
//! [disqualified]: https://github.com/bevyengine/disqualified/blob/main/src/short_name.rs
use std::any::type_name;
@@ -23,8 +23,7 @@ pub(crate) fn short_type_name<T: ?Sized>() -> String {
while index < end_of_string {
let rest_of_string = full_name.get(index..end_of_string).unwrap_or_default();
// Collapse everything up to the next special character,
// then skip over it
// Collapse everything up to the next special character, then skip over it
if let Some(special_character_index) =
rest_of_string.find(|c: char| [' ', '<', '>', '(', ')', '[', ']', ',', ';'].contains(&c))
{
@@ -32,11 +31,16 @@ pub(crate) fn short_type_name<T: ?Sized>() -> String {
parsed_name += collapse_type_name(segment_to_collapse);
// Insert the special character
let special_character = &rest_of_string[special_character_index..=special_character_index];
parsed_name.push_str(special_character);
parsed_name += special_character;
// Remove lifetimes like <'_> or <'_, '_, ...>
if parsed_name.ends_with("<'_>") || parsed_name.ends_with("<'_, ") {
_ = parsed_name.split_off(parsed_name.len() - 4);
}
match special_character {
">" | ")" | "]" if rest_of_string[special_character_index + 1..].starts_with("::") => {
parsed_name.push_str("::");
parsed_name += "::";
// Move the index past the "::"
index += special_character_index + 3;
}
@@ -53,14 +57,18 @@ pub(crate) fn short_type_name<T: ?Sized>() -> String {
}
#[inline(always)]
fn collapse_type_name(string: &str) -> &str {
string.rsplit("::").next().unwrap()
fn collapse_type_name(segment: &str) -> &str {
segment.rsplit("::").next().unwrap()
}
#[cfg(test)]
mod tests {
use super::short_type_name;
use std::collections::HashMap;
use std::marker::PhantomData;
struct MyData<'a, 'b>(PhantomData<&'a &'b ()>);
struct MyDataT<'a, T>(PhantomData<&'a T>);
#[test]
fn tests() {
@@ -73,5 +81,7 @@ mod tests {
"HashMap<String, Option<[i32; 3]>>"
);
assert_eq!(short_type_name::<dyn Fn(i32) -> i32>(), "dyn Fn(i32) -> i32");
assert_eq!(short_type_name::<MyDataT<&str>>(), "MyDataT<&str>");
assert_eq!(short_type_name::<(&MyData, [MyData])>(), "(MyData, [MyData])");
}
}
+17 -12
View File
@@ -28,9 +28,10 @@ use {
/// The non-primitive variants (eg. string/table/function/thread/userdata) contain handle types
/// into the internal Lua state. It is a logic error to mix handle types between separate
/// `Lua` instances, and doing so will result in a panic.
#[derive(Clone)]
#[derive(Clone, Default)]
pub enum Value {
/// The Lua value `nil`.
#[default]
Nil,
/// The Lua value `true` or `false`.
Boolean(bool),
@@ -491,7 +492,6 @@ impl Value {
/// This allows customizing serialization behavior using serde.
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
#[doc(hidden)]
pub fn to_serializable(&self) -> SerializableValue<'_> {
SerializableValue::new(self, Default::default(), None)
}
@@ -580,12 +580,6 @@ impl Value {
}
}
impl Default for Value {
fn default() -> Self {
Self::Nil
}
}
impl fmt::Debug for Value {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
if fmt.alternate() {
@@ -684,7 +678,7 @@ impl<'a> SerializableValue<'a> {
///
/// Default: **true**
#[must_use]
pub const fn deny_unsupported_types(mut self, enabled: bool) -> Self {
pub fn deny_unsupported_types(mut self, enabled: bool) -> Self {
self.options.deny_unsupported_types = enabled;
self
}
@@ -695,7 +689,7 @@ impl<'a> SerializableValue<'a> {
///
/// Default: **true**
#[must_use]
pub const fn deny_recursive_tables(mut self, enabled: bool) -> Self {
pub fn deny_recursive_tables(mut self, enabled: bool) -> Self {
self.options.deny_recursive_tables = enabled;
self
}
@@ -704,7 +698,7 @@ impl<'a> SerializableValue<'a> {
///
/// Default: **false**
#[must_use]
pub const fn sort_keys(mut self, enabled: bool) -> Self {
pub fn sort_keys(mut self, enabled: bool) -> Self {
self.options.sort_keys = enabled;
self
}
@@ -713,10 +707,21 @@ impl<'a> SerializableValue<'a> {
///
/// Default: **false**
#[must_use]
pub const fn encode_empty_tables_as_array(mut self, enabled: bool) -> Self {
pub fn encode_empty_tables_as_array(mut self, enabled: bool) -> Self {
self.options.encode_empty_tables_as_array = enabled;
self
}
/// If true, enable detection of mixed tables.
///
/// A mixed table is a table that has both array-like and map-like entries or several borders.
///
/// Default: **false**
#[must_use]
pub fn detect_mixed_tables(mut self, enabled: bool) -> Self {
self.options.detect_mixed_tables = enabled;
self
}
}
#[cfg(feature = "serde")]
+33 -1
View File
@@ -8,7 +8,7 @@ use futures_util::stream::TryStreamExt;
use tokio::sync::Mutex;
use mlua::{
Error, Function, Lua, LuaOptions, MultiValue, ObjectLike, Result, StdLib, Table, UserData,
Error, Function, Lua, LuaOptions, MultiValue, ObjectLike, Result, StdLib, Table, ThreadStatus, UserData,
UserDataMethods, UserDataRef, Value,
};
@@ -667,3 +667,35 @@ async fn test_async_hook() -> Result<()> {
Ok(())
}
#[test]
fn test_async_yield_with() -> Result<()> {
let lua = Lua::new();
let func = lua.create_async_function(|lua, (mut a, mut b): (i32, i32)| async move {
let zero = lua.yield_with::<MultiValue>(()).await?;
assert!(zero.is_empty());
let one = lua.yield_with::<MultiValue>(a + b).await?;
assert_eq!(one.len(), 1);
for _ in 0..3 {
(a, b) = lua.yield_with((a + b, a * b)).await?;
}
Ok((0, 0))
})?;
let thread = lua.create_thread(func)?;
let zero = thread.resume::<MultiValue>((2, 3))?; // function arguments
assert!(zero.is_empty());
let one = thread.resume::<i32>(())?; // value of "zero" is passed here
assert_eq!(one, 5);
assert_eq!(thread.resume::<(i32, i32)>(1)?, (5, 6)); // value of "one" is passed here
assert_eq!(thread.resume::<(i32, i32)>((10, 11))?, (21, 110));
assert_eq!(thread.resume::<(i32, i32)>((11, 12))?, (23, 132));
assert_eq!(thread.resume::<(i32, i32)>((12, 13))?, (0, 0));
assert_eq!(thread.status(), ThreadStatus::Finished);
Ok(())
}
+68 -2
View File
@@ -1,5 +1,7 @@
#![cfg(feature = "luau")]
use std::io::{Read, Seek, SeekFrom, Write};
use mlua::{Lua, Result, Value};
#[test]
@@ -41,7 +43,7 @@ fn test_buffer() -> Result<()> {
}
#[test]
#[should_panic(expected = "range end index 14 out of range for slice of length 13")]
#[should_panic(expected = "out of range for slice of length 13")]
fn test_buffer_out_of_bounds_read() {
let lua = Lua::new();
let buf = lua.create_buffer(b"hello, world!").unwrap();
@@ -49,9 +51,73 @@ fn test_buffer_out_of_bounds_read() {
}
#[test]
#[should_panic(expected = "range end index 16 out of range for slice of length 13")]
#[should_panic(expected = "out of range for slice of length 13")]
fn test_buffer_out_of_bounds_write() {
let lua = Lua::new();
let buf = lua.create_buffer(b"hello, world!").unwrap();
buf.write_bytes(14, b"!!");
}
#[test]
fn create_large_buffer() {
let lua = Lua::new();
let err = lua.create_buffer_with_capacity(1_073_741_824 + 1).unwrap_err(); // 1GB
assert!(err.to_string().contains("memory allocation error"));
// Normal buffer is okay
let buf = lua.create_buffer_with_capacity(1024 * 1024).unwrap();
assert_eq!(buf.len(), 1024 * 1024);
}
#[test]
fn test_buffer_cursor() -> Result<()> {
let lua = Lua::new();
let mut cursor = lua.create_buffer(b"hello, world")?.cursor();
let mut data = Vec::new();
cursor.read_to_end(&mut data)?;
assert_eq!(data, b"hello, world");
// No more data to read
let mut one = [0u8; 1];
assert_eq!(cursor.read(&mut one)?, 0);
// Seek to start
cursor.seek(SeekFrom::Start(0))?;
cursor.read_exact(&mut one)?;
assert_eq!(one, [b'h']);
// Seek to end -5
cursor.seek(SeekFrom::End(-5))?;
let mut five = [0u8; 5];
cursor.read_exact(&mut five)?;
assert_eq!(&five, b"world");
// Seek to current -1
cursor.seek(SeekFrom::Current(-1))?;
cursor.read_exact(&mut one)?;
assert_eq!(one, [b'd']);
// Invalid seek
assert!(cursor.seek(SeekFrom::Current(-100)).is_err());
assert!(cursor.seek(SeekFrom::End(1)).is_err());
// Write data
let buf = lua.create_buffer_with_capacity(100)?;
cursor = buf.clone().cursor();
cursor.write_all(b"hello, ...")?;
cursor.seek(SeekFrom::Current(-3))?;
cursor.write_all(b"Rust!")?;
assert_eq!(&buf.read_bytes::<12>(0), b"hello, Rust!");
// Writing beyond the end of the buffer does nothing
cursor.seek(SeekFrom::End(0))?;
assert_eq!(cursor.write(b".")?, 0);
// Flush is no-op
cursor.flush()?;
Ok(())
}
+9
View File
@@ -330,6 +330,15 @@ fn test_interrupts() -> Result<()> {
assert_eq!(yield_count.load(Ordering::Relaxed), 7);
assert_eq!(co.status(), ThreadStatus::Finished);
// Test no yielding at non-yieldable points
yield_count.store(0, Ordering::Relaxed);
let co = lua.create_thread(lua.create_function(|lua, arg: Value| {
(lua.load("return (function(x) return x end)(...)")).call::<Value>(arg)
})?)?;
let res = co.resume::<String>("abc")?;
assert_eq!(res, "abc".to_string());
assert_eq!(yield_count.load(Ordering::Relaxed), 3);
//
// Test errors in interrupts
//
+5
View File
@@ -179,6 +179,11 @@ fn test_require_with_config() {
let res = run_require(&lua, "./tests/luau/require/with_config/src/alias_requirer").unwrap();
assert_eq!("result from dependency", get_str(&res, 1));
// RequirePathWithAlias (case-insensitive)
let res2 = run_require(&lua, "./tests/luau/require/with_config/src/alias_requirer_uc").unwrap();
assert_eq!("result from dependency", get_str(&res2, 1));
assert_eq!(res.to_pointer(), res2.to_pointer());
// RequirePathWithParentAlias
let res = run_require(&lua, "./tests/luau/require/with_config/src/parent_alias_requirer").unwrap();
assert_eq!("result from other_dependency", get_str(&res, 1));
@@ -0,0 +1 @@
return require("@DeP")
+4 -2
View File
@@ -382,7 +382,8 @@ fn test_scope_userdata_ref() -> Result<()> {
modify_userdata(&lua, &ud)?;
// We can only borrow userdata scoped
assert!((matches!(ud.borrow::<MyUserData>(), Err(Error::UserDataTypeMismatch))));
#[rustfmt::skip]
assert!(matches!(ud.borrow::<MyUserData>(), Err(Error::UserDataTypeMismatch)));
ud.borrow_scoped::<MyUserData, ()>(|ud_inst| {
assert_eq!(ud_inst.0.get(), 2);
})?;
@@ -419,7 +420,8 @@ fn test_scope_userdata_ref_mut() -> Result<()> {
let ud = scope.create_userdata_ref_mut(&mut data)?;
modify_userdata(&lua, &ud)?;
assert!((matches!(ud.borrow_mut::<MyUserData>(), Err(Error::UserDataTypeMismatch))));
#[rustfmt::skip]
assert!(matches!(ud.borrow_mut::<MyUserData>(), Err(Error::UserDataTypeMismatch)));
ud.borrow_mut_scoped::<MyUserData, ()>(|ud_inst| {
ud_inst.0 += 10;
})?;
+39
View File
@@ -269,6 +269,45 @@ fn test_serialize_empty_table() -> LuaResult<()> {
Ok(())
}
#[test]
fn test_serialize_mixed_table() -> LuaResult<()> {
let lua = Lua::new();
// Check that sparse array is serialized similarly when using direct serialization
// and via `Lua::from_value`
let table = lua.load("{1,2,3,nil,5}").eval::<Value>()?;
let json1 = serde_json::to_string(&table).unwrap();
let json2 = lua.from_value::<serde_json::Value>(table)?;
assert_eq!(json1, json2.to_string());
// A table with several borders should be correctly encoded when `detect_mixed_tables` is enabled
let table = lua
.load(
r#"
local t = {1,2,3,nil,5,6}
t[10] = 10
return t
"#,
)
.eval::<Value>()?;
let json = serde_json::to_string(&table.to_serializable().detect_mixed_tables(true)).unwrap();
assert_eq!(json, r#"[1,2,3,null,5,6,null,null,null,10]"#);
// A mixed table with both array-like and map-like entries
let table = lua.load(r#"{1,2,3, key="value"}"#).eval::<Value>()?;
let json = serde_json::to_string(&table).unwrap();
assert_eq!(json, r#"[1,2,3]"#);
let json = serde_json::to_string(&table.to_serializable().detect_mixed_tables(true)).unwrap();
assert_eq!(json, r#"{"1":1,"2":2,"3":3,"key":"value"}"#);
// A mixed table with duplicate keys of different types
let table = lua.load(r#"{1,2,3, ["1"]="value"}"#).eval::<Value>()?;
let json = serde_json::to_string(&table.to_serializable().detect_mixed_tables(true)).unwrap();
assert_eq!(json, r#"{"1":1,"2":2,"3":3,"1":"value"}"#);
Ok(())
}
#[test]
fn test_to_value_struct() -> LuaResult<()> {
let lua = Lua::new();
+106
View File
@@ -61,6 +61,15 @@ fn test_table() -> Result<()> {
Ok(())
}
#[test]
#[cfg(target_os = "linux")] // Linux allow overcommiting the memory (relevant for CI)
fn test_table_with_large_capacity() {
let lua = Lua::new();
let t = lua.create_table_with_capacity(1 << 26, 1 << 26);
assert!(t.is_ok());
}
#[test]
fn test_table_push_pop() -> Result<()> {
let lua = Lua::new();
@@ -263,6 +272,22 @@ fn test_table_for_each() -> Result<()> {
Ok(())
}
#[test]
fn test_table_for_each_value() -> Result<()> {
let lua = Lua::new();
let table = lua.load("{1, 2, 3, 4, 5, nil, 7}").eval::<Table>()?;
let mut sum = 0;
table.for_each_value::<i32>(|v| {
sum += v;
Ok(())
})?;
// Iterations stops at the first nil
assert_eq!(sum, 1 + 2 + 3 + 4 + 5);
Ok(())
}
#[test]
fn test_table_scope() -> Result<()> {
let lua = Lua::new();
@@ -473,3 +498,84 @@ fn test_table_object_like() -> Result<()> {
Ok(())
}
#[test]
fn test_table_get_path() -> Result<()> {
let lua = Lua::new();
// Create a nested table structure
let table = lua
.load(
r#"
{
a = {
b = {
c = "hello",
d = 42
},
[1] = "first",
["special key"] = "special value"
},
abc = "top level",
x = {},
["🚀"] = "rocket",
[1] = {
["nested-key"] = {
[42] = {
final = "hello!",
},
},
["key\"with\"quotes"] = "value1",
["key'with'quotes"] = "value2",
["key\\with\\backslashes"] = "value3",
[-2] = "negative index",
},
}
"#,
)
.eval::<Table>()?;
// Test basic dot notation
assert_eq!(table.get_path::<String>(".a.b.c")?, "hello");
assert_eq!(table.get_path::<String>("a.b.c")?, "hello");
assert_eq!(table.get_path::<i32>("a.b.d")?, 42);
assert_eq!(table.get_path::<String>("abc")?, "top level");
// Test bracket notation with integer keys
assert_eq!(table.get_path::<String>("a[1]")?, "first");
assert_eq!(table.get_path::<String>("[1][-2]")?, "negative index");
// Test bracket notation with string keys
assert_eq!(table.get_path::<String>("a[\"special key\"]")?, "special value");
assert_eq!(table.get_path::<String>("a['special key']")?, "special value");
assert_eq!(table.get_path::<String>(r#"[1]["key\"with\"quotes"]"#)?, "value1");
assert_eq!(table.get_path::<String>(r#"[1]['key"with"quotes']"#)?, "value1");
assert_eq!(table.get_path::<String>(r#"[1]['key\'with\'quotes']"#)?, "value2");
assert_eq!(
table.get_path::<String>(r#"[1]["key\\with\\backslashes"]"#)?,
"value3"
);
// Test mixed notation
assert_eq!(table.get_path::<String>("[1].nested-key[42].final")?, "hello!");
// Test unicode keys
assert_eq!(table.get_path::<String>("🚀")?, "rocket");
// Test empty path returns the table itself
assert_eq!(table.get_path::<Table>("")?, table);
// Test safe navigation
assert_eq!(table.get_path::<String>("a?.b.c")?, "hello");
assert_eq!(table.get_path::<Value>("x.y?.z")?, Value::Nil);
assert_eq!(table.get_path::<Value>("[1].nested-key[43]?.final")?, Value::Nil);
// Test path with whitespace
assert_eq!(table.get_path::<String>(" .a [\"b\"] .c ")?, "hello");
// Test indexing non-indexable value
let err = table.get_path::<String>("abc.c").unwrap_err().to_string();
assert_eq!(err, "runtime error: attempt to index a string value with key 'c'");
Ok(())
}
+26
View File
@@ -602,6 +602,21 @@ fn test_num_conversion() -> Result<()> {
assert_eq!(lua.unpack::<i128>(lua.pack(1i128 << 64)?)?, 1i128 << 64);
// Negative zero
let negative_zero = lua.load("-0.0").eval::<f64>()?;
assert_eq!(negative_zero, 0.0);
// LuaJIT treats -0.0 as a positive zero
#[cfg(not(feature = "luajit"))]
assert!(negative_zero.is_sign_negative());
// In Lua <5.3 all numbers are floats
#[cfg(not(any(feature = "lua54", feature = "lua53", feature = "luajit")))]
{
let negative_zero = lua.load("-0").eval::<f64>()?;
assert_eq!(negative_zero, 0.0);
assert!(negative_zero.is_sign_negative());
}
Ok(())
}
@@ -1227,6 +1242,17 @@ fn test_register_module() -> Result<()> {
res.unwrap_err().to_string(),
"runtime error: module name must begin with '@'"
);
// Luau registered modules (aliases) are case-insensitive
let res = lua.register_module("@My_Module", &t);
assert!(res.is_ok());
lua.load(
r#"
local my_module = require("@MY_MODule")
assert(my_module.name == "my_module")
"#,
)
.exec()?;
}
Ok(())
+22 -1
View File
@@ -1,6 +1,6 @@
use std::panic::catch_unwind;
use mlua::{Error, Function, Lua, Result, Thread, ThreadStatus};
use mlua::{Error, Function, IntoLua, Lua, Result, Thread, ThreadStatus, Value};
#[test]
fn test_thread() -> Result<()> {
@@ -252,3 +252,24 @@ fn test_thread_resume_error() -> Result<()> {
Ok(())
}
#[test]
fn test_thread_resume_bad_arg() -> Result<()> {
let lua = Lua::new();
struct BadArg;
impl IntoLua for BadArg {
fn into_lua(self, _lua: &Lua) -> Result<Value> {
Err(Error::runtime("bad arg"))
}
}
let f = lua.create_thread(lua.create_function(|_, ()| Ok("okay"))?)?;
let res = f.resume::<()>((123, BadArg));
assert!(matches!(res, Err(Error::RuntimeError(msg)) if msg == "bad arg"));
let res = f.resume::<String>(()).unwrap();
assert_eq!(res, "okay");
Ok(())
}
+66 -1
View File
@@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicI64, Ordering};
use mlua::{
AnyUserData, Error, ExternalError, Function, Lua, MetaMethod, Nil, ObjectLike, Result, String, UserData,
UserDataFields, UserDataMethods, UserDataRef, Value, Variadic,
UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value, Variadic,
};
#[test]
@@ -525,6 +525,11 @@ fn test_fields() -> Result<()> {
Ok(())
});
// Field that emulates method
fields.add_field_function_get("val_fget", |lua, ud| {
lua.create_function(move |_, ()| Ok(ud.borrow::<MyUserData>()?.0))
});
// Use userdata "uservalue" storage
fields.add_field_function_get("uval", |_, ud| ud.user_value::<Option<String>>());
fields.add_field_function_set("uval", |_, ud, s: Option<String>| ud.set_user_value(s));
@@ -537,6 +542,10 @@ fn test_fields() -> Result<()> {
})
})
}
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
methods.add_method("dummy", |_, _, ()| Ok(()));
}
}
globals.set("ud", MyUserData(7))?;
@@ -546,6 +555,7 @@ fn test_fields() -> Result<()> {
assert(ud.val == 7)
ud.val = 10
assert(ud.val == 10)
assert(ud:val_fget() == 10)
assert(ud.uval == nil)
ud.uval = "hello"
@@ -1297,3 +1307,58 @@ fn test_userdata_wrappers() -> Result<()> {
Ok(())
}
#[cfg(feature = "luau")]
#[test]
fn test_userdata_namecall() -> Result<()> {
let lua = Lua::new();
struct MyUserData;
impl UserData for MyUserData {
fn register(registry: &mut mlua::UserDataRegistry<Self>) {
registry.add_method("method", |_, _, ()| Ok("method called"));
registry.add_field_method_get("field", |_, _| Ok("field value"));
registry.add_meta_method(MetaMethod::Index, |_, _, key: StdString| Ok(key));
registry.enable_namecall();
}
}
let ud = lua.create_userdata(MyUserData)?;
lua.globals().set("ud", &ud)?;
lua.load(
r#"
assert(ud:method() == "method called")
assert(ud.field == "field value")
assert(ud.dynamic_field == "dynamic_field")
local ok, err = pcall(function() return ud:dynamic_field() end)
assert(tostring(err):find("attempt to call an unknown method 'dynamic_field'") ~= nil)
"#,
)
.exec()?;
ud.destroy()?;
let err = lua.load("ud:method()").exec().unwrap_err();
assert!(err.to_string().contains("userdata has been destructed"));
Ok(())
}
#[test]
fn test_userdata_get_path() -> Result<()> {
let lua = Lua::new();
struct MyUd;
impl UserData for MyUd {
fn register(registry: &mut UserDataRegistry<Self>) {
registry.add_field("value", "userdata_value");
}
}
let ud = lua.create_userdata(MyUd)?;
assert_eq!(ud.get_path::<String>(".value")?, "userdata_value");
Ok(())
}