mirror of
https://github.com/mlua-rs/mlua
synced 2026-06-08 16:05:43 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 247208edb1 | |||
| e08768cc5e | |||
| 5b38af9746 | |||
| 54907f80c5 | |||
| ae512f2b49 | |||
| 53c159b6cb | |||
| 2beca6ebe1 | |||
| 09da7a41e5 | |||
| bad20374ad | |||
| 40b507c3ec | |||
| 537cc995f6 | |||
| 5d27cb91b2 | |||
| c70a636ca9 | |||
| 13ff0ca798 | |||
| 44f49e35d6 | |||
| e1ee4058a6 | |||
| f06d0020ea | |||
| d399559d30 | |||
| 30735d5ff1 | |||
| 75c23e5853 | |||
| 347856b806 | |||
| 774a63bece | |||
| c481c87eac | |||
| 85b280a9d6 | |||
| db7b782d3c | |||
| 5f38445558 | |||
| df0a44d405 | |||
| f0806a6d62 |
@@ -1,3 +1,18 @@
|
||||
## 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>`
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mlua"
|
||||
version = "0.11.2" # 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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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)'] }
|
||||
|
||||
@@ -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
@@ -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
@@ -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"))
|
||||
|
||||
+121
-17
@@ -37,6 +37,7 @@ use crate::{buffer::Buffer, chunk::Compiler};
|
||||
use {
|
||||
crate::types::LightUserData,
|
||||
std::future::{self, Future},
|
||||
std::task::Poll,
|
||||
};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -631,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
|
||||
///
|
||||
@@ -695,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1150,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.
|
||||
@@ -1159,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> {
|
||||
@@ -2071,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
@@ -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,
|
||||
|
||||
+60
-21
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ®istry.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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
] {
|
||||
|
||||
+8
-8
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
+4
-2
@@ -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;
|
||||
})?;
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
+22
-1
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user