mirror of
https://github.com/mlua-rs/mlua
synced 2026-06-08 16:05:43 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 247208edb1 | |||
| e08768cc5e | |||
| 5b38af9746 | |||
| 54907f80c5 | |||
| ae512f2b49 | |||
| 53c159b6cb | |||
| 2beca6ebe1 | |||
| 09da7a41e5 | |||
| bad20374ad | |||
| 40b507c3ec | |||
| 537cc995f6 | |||
| 5d27cb91b2 | |||
| c70a636ca9 |
@@ -1,3 +1,9 @@
|
||||
## 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
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mlua"
|
||||
version = "0.11.3" # 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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -376,6 +376,9 @@ fn userdata_call_method_complex(c: &mut Criterion) {
|
||||
this.0 += by;
|
||||
Ok(this.0)
|
||||
});
|
||||
|
||||
#[cfg(feature = "luau")]
|
||||
registry.enable_namecall();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-7
@@ -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
@@ -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")))]
|
||||
|
||||
+2
-1
@@ -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
@@ -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"))
|
||||
|
||||
+2
-2
@@ -2100,7 +2100,7 @@ impl Lua {
|
||||
///
|
||||
/// ```
|
||||
/// # use mlua::{Lua, Result};
|
||||
///
|
||||
/// #
|
||||
/// async fn generator(lua: Lua, _: ()) -> Result<()> {
|
||||
/// for i in 0..10 {
|
||||
/// lua.yield_with::<()>(i).await?;
|
||||
@@ -2127,7 +2127,7 @@ impl Lua {
|
||||
///
|
||||
/// ```
|
||||
/// # use mlua::{Lua, Result, Value};
|
||||
///
|
||||
/// #
|
||||
/// async fn pingpong(lua: Lua, mut val: i32) -> Result<()> {
|
||||
/// loop {
|
||||
/// val = lua.yield_with::<i32>(val).await? + 1;
|
||||
|
||||
+16
-3
@@ -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};
|
||||
|
||||
@@ -928,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);
|
||||
|
||||
@@ -1056,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 ®istry.methods {
|
||||
map.insert(k.as_bytes().to_vec(), &**m);
|
||||
}
|
||||
}
|
||||
|
||||
let mut methods_index = None;
|
||||
let methods_nrec = registry.methods.len();
|
||||
#[cfg(feature = "async")]
|
||||
@@ -1103,6 +1115,7 @@ impl RawLua {
|
||||
field_getters_index,
|
||||
field_setters_index,
|
||||
methods_index,
|
||||
methods_map,
|
||||
)?;
|
||||
|
||||
// Update stack guard to keep metatable after return
|
||||
|
||||
+113
-28
@@ -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<()> {
|
||||
@@ -943,6 +1015,16 @@ impl ObjectLike for Table {
|
||||
fn to_string(&self) -> Result<StdString> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapped [`Table`] with customized serialization behavior.
|
||||
@@ -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))
|
||||
|
||||
+42
-2
@@ -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.
|
||||
|
||||
+5
-2
@@ -38,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>;
|
||||
|
||||
|
||||
+12
-1
@@ -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;
|
||||
@@ -88,6 +89,16 @@ impl ObjectLike for AnyUserData {
|
||||
|
||||
#[inline]
|
||||
fn to_string(&self) -> Result<StdString> {
|
||||
Value::UserData(AnyUserData(self.0.clone())).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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
] {
|
||||
|
||||
@@ -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::{
|
||||
@@ -327,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;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
+17
-12
@@ -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")]
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -272,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();
|
||||
@@ -482,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(())
|
||||
}
|
||||
|
||||
+56
-1
@@ -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]
|
||||
@@ -1307,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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user