diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c3a2ad77..662a04a3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,8 +1,8 @@ -name: Documentation (dev) +name: Documentation (main) on: push: - branches: [dev] + branches: [main] workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b67adccb..9fcaecee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -203,7 +203,7 @@ jobs: needs: build strategy: matrix: - lua: [lua54, luajit] + lua: [lua55, lua54, luajit] defaults: run: shell: msys2 {0} @@ -212,7 +212,7 @@ jobs: - uses: actions/checkout@main - name: Install Rust & Lua run: | - pacman -S --noconfirm mingw-w64-x86_64-rust mingw-w64-x86_64-lua mingw-w64-x86_64-luajit mingw-w64-x86_64-pkg-config + pacman -S --noconfirm mingw-w64-x86_64-rust mingw-w64-x86_64-lua54 mingw-w64-x86_64-lua mingw-w64-x86_64-luajit mingw-w64-x86_64-pkg-config - name: Run ${{ matrix.lua }} module tests run: | (cd tests/module && cargo build --release --features "${{ matrix.lua }}") diff --git a/CHANGELOG.md b/CHANGELOG.md index 257ee8fa..6a0850c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +## v0.12.0 (Jul 05, 2026) + +Changes since v0.12.0-rc.2 + +- Create root re-exports are refactored (only essentials are re-exported, the rest is in the new submodules) +- Added `ThreadStatus::Normal` and `Thread::is_normal` (mimic `coroutine.status`) +- Added `Lua::set_jit_options` with support of Luau JIT inliner (Luau) +- Added `Value::as_vector`/`Value::is_vector` (Luau) +- Added `Table::remove` +- `serde`: tables with the array metatable are always encoded as arrays (incl. `detect_mixed_tables` option) +- impl `Hash` for `BorrowedStr`/`BorrowedBytes` +- `Lua::current_thread` resolves implicit async threads to their root owner (#706) +- Bugfixes and improvements + +## v0.12.0-rc.2 (Jun 06, 2026) + +- Add `#[derive(UserData)]` and `#[mlua::userdata_impl]` macros +- Support thread create/resume/yield callbacks for all Lua versions (including Luau) +- Support `to_alias_override`/`to_alias_fallback` in `Require` trait (Luau) +- Prevent `XRc` overflow when dropping `RawLua` with foreign Lua state +- implement `Not` for `StdLib` (#699) +- Fix `String::to_pointer` return NULL in Lua <5.4 + +## v0.12.0-rc.1 (Apr 21, 2026) + +- Rust 2024 edition +- Removed `Error::ToLuaConversionError` variant as it was unused (and not practically useful) +- New modules to group data types: `chunk`, `debug`, `error`, `function`, `table`, `string`, `state`, `thread`, `userdata`, `luau` +- Support `__todebugstring` metamethod for pretty formatting userdata value (for debugging) +- New `MaybeSync` trait that is required for userdata types +- Removed lifetime from `BorrowedStr` and `BorrowedBytes` +- New `Thread` methods: `is_resumable`, `is_running`, `is_finished`, `is_error` +- Added `Thread::state` to get raw Lua state pointer +- Luau `TextRequirer` is renamed to `FsRequirer` +- GC interface refactor: `Lua::gc_inc/Lua::gc_gen` is replaced with `gc_set_mode` +- Added `GcIncParams` and `GcGenParams` for GC tuning +- New `UserDataMethods::add_method_once` and `UserDataMethods::add_async_method_once` +- Initial Luau integer64 type support +- Changed interface of `Function::wrap/wrap_mut/wrap_async` to support any Error type +- Changed `AnyUserData::type_name` to return `LuaString` instead +- Added `UserDataOwned` wrapper to take ownership of userdata `T` and implements `FromLua` + ## v0.11.6 (Jan 27, 2026) - Added Lua 5.5 support (`lua55` feature flag) diff --git a/Cargo.toml b/Cargo.toml index 7eceb7f4..406fcf9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mlua" -version = "0.12.0-dev.1" # remember to update mlua_derive +version = "0.12.0" # remember to update mlua_derive authors = ["Aleksandr Orlenko ", "kyren "] rust-version = "1.88" edition = "2024" @@ -42,7 +42,7 @@ async = ["dep:futures-util"] send = ["error-send"] error-send = [] serde = ["dep:serde", "dep:erased-serde", "dep:serde-value", "bstr/serde"] -macros = ["mlua_derive/macros"] +macros = ["mlua_derive/macros", "dep:inventory"] anyhow = ["dep:anyhow", "error-send"] userdata-wrappers = ["parking_lot/send_guard"] @@ -50,7 +50,7 @@ userdata-wrappers = ["parking_lot/send_guard"] serialize = ["serde"] [dependencies] -mlua_derive = { version = "=0.11.0", optional = true, path = "mlua_derive" } +mlua_derive = { version = "=0.12.0", optional = true, path = "mlua_derive" } bstr = { version = "1.0", features = ["std"], default-features = false } either = "1.0" num-traits = { version = "0.2.14" } @@ -61,9 +61,10 @@ erased-serde = { version = "0.4", optional = true } serde-value = { version = "0.7", optional = true } parking_lot = { version = "0.12", features = ["arc_lock"] } anyhow = { version = "1.0", optional = true } +inventory = { version = "0.3", optional = true } libc = "0.2" -ffi = { package = "mlua-sys", version = "0.10.0", path = "mlua-sys" } +ffi = { package = "mlua-sys", version = "0.11.0", path = "mlua-sys" } [dev-dependencies] trybuild = "1.0" @@ -77,14 +78,14 @@ static_assertions = "1.0" hyper = { version = "1.2", features = ["full"] } hyper-util = { version = "0.1.3", features = ["full"] } http-body-util = "0.1.1" -reqwest = { version = "0.12", features = ["json"] } +reqwest = { version = "0.13", features = ["json"] } tempfile = "3" -criterion = { version = "0.7", features = ["async_tokio"] } -rustyline = "17.0" +criterion = { version = "0.8", features = ["async_tokio"] } +rustyline = "18.0" tokio = { version = "1.0", features = ["full"] } [lints.rust] -unexpected_cfgs = { level = "allow", check-cfg = ['cfg(tarpaulin_include)'] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tarpaulin_include)', 'cfg(force_memory_limit)'] } [[bench]] name = "benchmark" diff --git a/README.md b/README.md index c2e2eba2..bcc307ff 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [docs.rs]: https://docs.rs/mlua [Coverage Status]: https://codecov.io/gh/mlua-rs/mlua/branch/main/graph/badge.svg?token=99339FS1CG [codecov.io]: https://codecov.io/gh/mlua-rs/mlua -[MSRV]: https://img.shields.io/badge/rust-1.79+-brightgreen.svg?&logo=rust +[MSRV]: https://img.shields.io/badge/rust-1.88+-brightgreen.svg?&logo=rust [Guided Tour] | [Benchmarks] | [FAQ] @@ -49,8 +49,8 @@ Below is a list of the available feature flags. By default `mlua` does not enabl * `vendored`: build static Lua(JIT) libraries from sources during `mlua` compilation using [lua-src] or [luajit-src] * `module`: enable module mode (building loadable `cdylib` library for Lua) * `async`: enable async/await support (any executor can be used, eg. [tokio] or [async-std]) -* `send`: make `mlua::Lua: Send + Sync` (adds [`Send`] requirement to `mlua::Function` and `mlua::UserData`) -* `error-send`: make `mlua:Error: Send + Sync` +* `send`: make `mlua::Lua: Send + Sync` (adds [`Send`] requirement to `mlua::Function` and `Send + Sync` to `mlua::UserData`) +* `error-send`: make `mlua::Error: Send + Sync` * `serde`: add serialization and deserialization support to `mlua` types using [serde] * `macros`: enable procedural macros (such as `chunk!`) * `anyhow`: enable `anyhow::Error` conversion into Lua @@ -125,13 +125,13 @@ my_project $ LUA_LIB=$HOME/tmp/lua-5.2.4/src LUA_LIB_NAME=lua LUA_LINK=static ca Just enable the `vendored` feature and cargo will automatically build and link the specified Lua/LuaJIT version. This is the easiest way to get started with `mlua`. ### Standalone mode -In standalone mode, `mlua` allows adding scripting support to your application with a gently configured Lua runtime to ensure safety and soundness. +In standalone mode, `mlua` allows adding scripting support to your application with a properly configured Lua runtime to ensure safety and soundness. Add to `Cargo.toml`: ``` toml [dependencies] -mlua = { version = "0.11", features = ["lua54", "vendored"] } +mlua = { version = "0.12", features = ["lua54", "vendored"] } ``` `main.rs` @@ -166,7 +166,7 @@ Add to `Cargo.toml`: crate-type = ["cdylib"] [dependencies] -mlua = { version = "0.11", features = ["lua54", "module"] } +mlua = { version = "0.12", features = ["lua54", "module"] } ``` `lib.rs`: diff --git a/docs/UserData.md b/docs/UserData.md new file mode 100644 index 00000000..15db89b5 --- /dev/null +++ b/docs/UserData.md @@ -0,0 +1,171 @@ +Implements the [`UserData`] trait for a Rust type. + +This derive macro generates an implementation of [`UserData`] that exposes +struct fields to Lua and integrates with `#[mlua::userdata_impl]` for +registering methods. + +Named fields are exposed as readable and writable fields in Lua by default. +Use `#[lua(...)]` on individual fields or methods to control how they are +registered. + +```rust,ignore +use mlua::{Lua, Result, UserData}; + +#[derive(UserData)] +struct Rectangle { + length: u32, + width: u32, +} + +#[mlua::userdata_impl] +impl Rectangle { + #[lua(infallible)] + fn new(length: u32, width: u32) -> Self { + Self { length, width } + } + + #[lua(getter, name = "area", infallible)] + fn calculate_area(&self) -> u32 { + self.length * self.width + } + + fn diagonal(&self) -> Result { + Ok(((self.length.pow(2) + self.width.pow(2)) as f64).sqrt()) + } +} +``` + +# Struct field attributes + +Each named field can be annotated with `#[lua(...)]`: + +| Attribute | Description | +| -------------- | ----------------------------------------------------- | +| `get` | Expose a getter. The field becomes readable from Lua. | +| `set` | Expose a setter. The field becomes writable from Lua. | +| `skip` | Do not expose this field. | +| `name = "..."` | Override the Lua-facing name for the field. | + +If neither `get` nor `set` is specified, both are enabled. + +Fields exposed as readable (via `get` or by default) must implement `Clone`. +The generated getter clones the field value when accessed from Lua. + +# Methods registration + +Use `#[mlua::userdata_impl]` on an `impl` block to register methods, +metamethods, and constants. All items in the block are registered automatically, +regardless of visibility. + +## Method detection + +The receiver type determines how a method is registered: + +| Receiver | Registration | +| ----------- | ----------------- | +| `&self` | `add_method` | +| `&mut self` | `add_method_mut` | +| `self` | `add_method_once` | +| None | `add_function` | + +A first parameter of type `&Lua` (or `&mlua::Lua`) is treated as the +Lua state reference and passed automatically. + +## Method and constant attributes + +Each item in the impl block can be annotated with `#[lua(...)]`: + +| Attribute | Applies to | Description | +| -------------- | ------------------ | -------------------------------------------------------------------------------------- | +| `skip` | Methods, constants | Exclude this item from registration. | +| `name = "..."` | Methods, constants | Override the Lua-facing name. | +| `infallible` | Methods | Wrap the return value in `Ok(...)`. | +| `getter` | Methods | Register as a field getter. Must take `&self` and no Lua-facing arguments. | +| `setter` | Methods | Register as a field setter. Must take `&[mut] self` and one value argument. | +| `field` | Methods, constants | Register as a static field. Methods must take no receiver and no Lua-facing arguments. | +| `meta` | Methods, constants | Register as a metamethod. May be combined with `field` for meta static fields. | + +At most one of `getter`, `setter`, `field` may be specified on a method. + +## Constants + +Constants in an `#[mlua::userdata_impl]` block are registered as static +fields: + +```rust,ignore +#[mlua::userdata_impl] +impl MyType { + const VERSION: &str = "1.0"; + const COUNT: u32 = 42; +} +``` + +Use `#[lua(meta)]` on a constant to register it as a meta static field. + +## Metamethods + +Annotate a method with `#[lua(meta)]` to register it as a Lua metamethod. +The metamethod name is inferred from the function name when it starts with +`__`. Use `name = "..."` to specify the name explicitly. + +```rust,ignore +#[mlua::userdata_impl] +impl MyType { + #[lua(meta, infallible)] + fn __add(&self, other: &Self) -> Self { ... } + + #[lua(meta, name = "__call", infallible)] + fn construct(this: mlua::AnyUserData, value: u32) -> Self { ... } +} +``` + +A metamethod with a `self` receiver is registered via `add_meta_method` +and behaves like a regular method. A metamethod without a receiver is +registered via `add_meta_function` and receives exactly the values Lua passes. +Declare every argument Lua provides, in order: + +- Binary metamethods (`__add`, `__sub`, `__eq`, `__concat`, ...) receive both + operands, so both must be declared. This is also the way to support reversed + operands (e.g. `2 + obj`), where the userdata is the second argument. +- Method-style metamethods (`__call`, `__index`, `__newindex`, ...) receive the + object as their first argument. When registered without a `self` receiver + (for example a constructor invoked as `MyType(value)`), declare that leading + argument explicitly (typically `mlua::AnyUserData`), even if it is ignored. + +```rust,ignore +#[mlua::userdata_impl] +impl Vec2 { + // No receiver: both operands are declared and passed directly by Lua. + #[lua(meta, infallible, name = "__add")] + fn add(a: &Vec2, b: &Vec2) -> Vec2 { ... } +} +``` + +## Reference parameters + +Reference parameters in method signatures are automatically mapped to +the appropriate callback wrapper types: + +| Parameter type | Callback type | +| -------------- | ------------------- | +| `&str` | `BorrowedStr` | +| `&[u8]` | `BorrowedBytes` | +| `&T` | `UserDataRef` | +| `&mut T` | `UserDataRefMut` | + +## Async methods + +Async methods are supported and registered via the corresponding async +variants (`add_async_method`, `add_async_method_mut`, etc.). + +# Limitations + +Generics are not supported. Wrap a generic type in a concrete newtype +instead. + +Union types cannot derive `UserData`. + +Enum types are accepted but generate no field registrations. All method +registration must be done via `#[mlua::userdata_impl]`. + +[`UserData`]: crate::UserData diff --git a/docs/chunk.md b/docs/chunk.md new file mode 100644 index 00000000..f8dcc1d9 --- /dev/null +++ b/docs/chunk.md @@ -0,0 +1,45 @@ +Create a type that implements [`AsChunk`] and can capture Rust variables. + +This macro allows to write Lua code directly in Rust code. + +Rust variables can be referenced from Lua using `$` prefix, as shown in the example below. +User's Rust types needs to implement [`UserData`] or [`IntoLua`] traits. + +Captured variables are **moved** into the chunk. + +```rust +use mlua::{Lua, Result, chunk}; + +fn main() -> Result<()> { + let lua = Lua::new(); + let name = "Rustacean"; + lua.load(chunk! { + print("hello, " .. $name) + }).exec() +} +``` + +## Syntax issues + +Since the Rust tokenizer will tokenize Lua code, this imposes some restrictions. +The main thing to remember is: + +- Use double quoted strings (`""`) instead of single quoted strings (`''`). + + (Single quoted strings only work if they contain a single character, since in Rust, + `'a'` is a character literal). + +Other minor limitations: + +- Certain escape codes in string literals don't work. (Specifically: `\a`, `\b`, `\f`, `\v`, + `\123` (octal escape codes), `\u`, and `\U`). + + These are accepted: : `\\`, `\n`, `\t`, `\r`, `\xAB` (hex escape codes), and `\0`. + +- The `//` (floor division) operator is unusable, as its start a comment. + +Everything else should work. + +[`AsChunk`]: crate::chunk::AsChunk +[`UserData`]: crate::UserData +[`IntoLua`]: crate::IntoLua diff --git a/docs/lua_module.md b/docs/lua_module.md new file mode 100644 index 00000000..5c5e84e6 --- /dev/null +++ b/docs/lua_module.md @@ -0,0 +1,41 @@ +Registers Lua module entrypoint. + +You can register multiple entrypoints as required. + +```rust,ignore +use mlua::{Lua, Result, Table}; + +#[mlua::lua_module] +fn my_module(lua: &Lua) -> Result { + let exports = lua.create_table()?; + exports.set("hello", "world")?; + Ok(exports) +} +``` + +Internally in the code above the compiler defines C function `luaopen_my_module`. + +You can also pass options to the attribute: + +* name - name of the module, defaults to the name of the function + +```rust,ignore +#[mlua::lua_module(name = "alt_module")] +fn my_module(lua: &Lua) -> Result
{ + ... +} +``` + +* skip_memory_check - skip memory allocation checks for some operations. + +In module mode, mlua runs in an unknown environment and cannot tell whether there are any memory +limits or not. As a result, some operations that require memory allocation run in protected +mode. Setting this attribute will improve performance of such operations with risk of having +uncaught exceptions and memory leaks. + +```rust,ignore +#[mlua::lua_module(skip_memory_check)] +fn my_module(lua: &Lua) -> Result
{ + ... +} +``` diff --git a/docs/release_notes/v0.10.md b/docs/release_notes/v0.10.md index db01e8b2..2684a4f3 100644 --- a/docs/release_notes/v0.10.md +++ b/docs/release_notes/v0.10.md @@ -1,6 +1,6 @@ ## mlua v0.10 release notes -The v0.10 version of mlua has goal to improve the user experience while keeping the same performance and safety guarantees. +The v0.10 version of mlua has a goal to improve the user experience while keeping the same performance and safety guarantees. This document highlights the most notable features. For a full list of changes, see the [CHANGELOG]. [CHANGELOG]: https://github.com/mlua-rs/mlua/blob/main/CHANGELOG.md @@ -40,7 +40,7 @@ assert_eq!(lua.globals().get::("i")?, 20); Under the hood, to synchronize access to the Lua state, mlua uses [`ReentrantMutex`] which can be recursively locked by a single thread. Only one thread can execute Lua code at a time, but it's possible to share Lua values between threads. -This has some performance penalties (about 10-20%) compared to the lock free mode. This flag is disabled by default and does not supported in module mode. +This has some performance penalties (about 10-20%) compared to the lock free mode. This flag is disabled by default and is not supported in module mode. [`ReentrantMutex`]: https://docs.rs/parking_lot/latest/parking_lot/type.ReentrantMutex.html @@ -144,7 +144,7 @@ The following `Scope` methods were changed: Instead, scope has comprehensive support for borrowed userdata: `create_any_userdata_ref`, `create_any_userdata_ref_mut`, `create_userdata_ref`, `create_userdata_ref_mut`. `UserDataRef` and `UserDataRefMut` are no longer acceptable for scoped userdata access as they require owned underlying data. -In mlua v0.9 this can cause read-after-free bug in some edge cases. +In mlua v0.9 this could cause a read-after-free bug in some edge cases. To temporarily borrow underlying data, the `AnyUserData::borrow_scoped` and `AnyUserData::borrow_mut_scoped` methods were introduced: diff --git a/docs/release_notes/v0.9.md b/docs/release_notes/v0.9.md index 7f3a0327..9de0ce2d 100644 --- a/docs/release_notes/v0.9.md +++ b/docs/release_notes/v0.9.md @@ -152,9 +152,9 @@ It will automatically trigger JIT compilation for new Lua chunks. To disable it, #### 1. Better error reporting -When calling a Rust function from Lua and passing wrong arguments, previous mlua versions reported a error message without any context or reference to the particular argument. +When calling a Rust function from Lua and passing wrong arguments, previous mlua versions reported an error message without any context or reference to the particular argument. -In v0.9 it reports a error message with the argument index and expected type: +In v0.9 it reports an error message with the argument index and expected type: ```rust let func = lua.create_function(|_, _a: i32| Ok(()))?; @@ -327,7 +327,7 @@ Under the hood a new function `luaopen_alt_module` will be created for the Lua m - `skip_memory_check` - skip memory allocation checks for some operations. -In module mode, mlua runs in unknown environment and cannot say are there any memory limits or not. As result, some operations that require memory allocation runs in +In module mode, mlua runs in an unknown environment and cannot tell whether there are any memory limits or not. As a result, some operations that require memory allocation run in protected mode. Setting this attribute will improve performance of such operations with risk of having uncaught exceptions and memory leaks. #### Improved Windows target diff --git a/examples/async_http_server.rs b/examples/async_http_server.rs index f5057ed6..95c64a8b 100644 --- a/examples/async_http_server.rs +++ b/examples/async_http_server.rs @@ -11,7 +11,7 @@ use hyper::{Request, Response}; use hyper_util::rt::TokioIo; use tokio::net::TcpListener; -use mlua::{Error as LuaError, Function, Lua, String as LuaString, Table, UserData, UserDataMethods, chunk}; +use mlua::{Error as LuaError, Function, Lua, LuaString, Table, UserData, UserDataMethods, chunk}; /// Wrapper around incoming request that implements UserData struct LuaRequest(SocketAddr, Request); diff --git a/examples/userdata.rs b/examples/userdata.rs index 6a21e90b..25b70a8d 100644 --- a/examples/userdata.rs +++ b/examples/userdata.rs @@ -1,45 +1,46 @@ -use mlua::{Lua, MetaMethod, Result, UserData, chunk}; +use mlua::{Lua, Result, UserData, chunk}; -#[derive(Default)] +#[derive(Default, UserData)] struct Rectangle { length: u32, width: u32, } -impl UserData for Rectangle { - fn add_fields>(fields: &mut F) { - fields.add_field_method_get("length", |_, this| Ok(this.length)); - fields.add_field_method_set("length", |_, this, val| { - this.length = val; - Ok(()) - }); - fields.add_field_method_get("width", |_, this| Ok(this.width)); - fields.add_field_method_set("width", |_, this, val| { - this.width = val; - Ok(()) - }); +#[mlua::userdata_impl] +impl Rectangle { + const NAME: &str = "Rectangle"; + + #[lua(infallible)] + fn new(length: u32, width: u32) -> Self { + Self { length, width } + } + + #[lua(getter, name = "area", infallible)] + fn calculate_area(&self) -> u32 { + self.length * self.width } - fn add_methods>(methods: &mut M) { - methods.add_method("area", |_, this, ()| Ok(this.length * this.width)); - methods.add_method("diagonal", |_, this, ()| { - Ok((this.length.pow(2) as f64 + this.width.pow(2) as f64).sqrt()) - }); + fn diagonal(&self) -> Result { + Ok((self.length.pow(2) as f64 + self.width.pow(2) as f64).sqrt()) + } - // Constructor - methods.add_meta_function(MetaMethod::Call, |_, ()| Ok(Rectangle::default())); + // Constructor via `__call` metamethod + #[lua(meta, infallible)] + fn __call(length: u32, width: u32) -> Self { + Rectangle::new(length, width) } } fn main() -> Result<()> { let lua = Lua::new(); - let rectangle = Rectangle::default(); + lua.globals().set("Rectangle", lua.create_proxy::()?)?; lua.load(chunk! { - local rect = $rectangle() - rect.width = 10 - rect.length = 5 - assert(rect:area() == 50) - assert(rect:diagonal() - 11.1803 < 0.0001) + local rect = Rectangle(10, 5) + rect.width = rect.width + 5 + rect.length = rect.length + 5 + assert(rect.NAME == "Rectangle") + assert(rect.area == 150) + assert(math.floor(rect:diagonal()) == 18) }) .exec() } diff --git a/mlua-sys/Cargo.toml b/mlua-sys/Cargo.toml index 2230af75..81a988e2 100644 --- a/mlua-sys/Cargo.toml +++ b/mlua-sys/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mlua-sys" -version = "0.10.0" +version = "0.11.0" authors = ["Aleksandr Orlenko "] rust-version = "1.88" edition = "2024" @@ -41,9 +41,9 @@ libc = "0.2" cc = "1.0" cfg-if = "1.0" pkg-config = "0.3.17" -lua-src = { version = ">= 550.0.0, < 550.1.0", optional = true } -luajit-src = { version = ">= 210.6.0, < 210.7.0", optional = true } -luau0-src = { version = "0.18.0", optional = true } +lua-src = { version = ">= 550.1.0, < 550.2.0", optional = true } +luajit-src = { version = ">= 210.7.0, < 210.8.0", optional = true } +luau0-src = { version = "0.20.6", optional = true } [lints.rust] -unexpected_cfgs = { level = "allow", check-cfg = ['cfg(raw_dylib)'] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(raw_dylib)'] } diff --git a/mlua-sys/build/main.rs b/mlua-sys/build/main.rs index 5bb8ddbe..7fe0fe3e 100644 --- a/mlua-sys/build/main.rs +++ b/mlua-sys/build/main.rs @@ -13,6 +13,10 @@ cfg_if::cfg_if! { include!("main_inner.rs"); } else if #[cfg(all(feature = "luau", not(any(feature = "lua55", feature = "lua54", feature = "lua53", feature = "lua52", feature = "lua51", feature = "luajit"))))] { include!("main_inner.rs"); + } else if #[cfg(not(any(feature = "lua55", feature = "lua54", feature = "lua53", feature = "lua52", feature = "lua51", feature = "luajit", feature = "luau")))] { + fn main() { + compile_error!("No Lua feature enabled. Please enable one of: lua55, lua54, lua53, lua52, lua51, luajit, luajit52, luau"); + } } else { fn main() { compile_error!("You can enable only one of the features: lua55, lua54, lua53, lua52, lua51, luajit, luajit52, luau"); diff --git a/mlua-sys/src/lua55/lauxlib.rs b/mlua-sys/src/lua55/lauxlib.rs index 13a8524f..03de0f03 100644 --- a/mlua-sys/src/lua55/lauxlib.rs +++ b/mlua-sys/src/lua55/lauxlib.rs @@ -190,10 +190,11 @@ pub unsafe fn luaL_loadbufferenv( status } +#[allow(unused_variables, unreachable_code)] pub unsafe fn luaL_makeseed(L: *mut lua_State) -> c_uint { - #[cfg(macos)] + #[cfg(target_os = "macos")] return libc::arc4random(); - #[cfg(linux)] + #[cfg(target_os = "linux")] { let mut seed = 0u32; let buf = &mut seed as *mut _ as *mut c_void; diff --git a/mlua-sys/src/luau/compat.rs b/mlua-sys/src/luau/compat.rs index bf2a7b95..c284d2e9 100644 --- a/mlua-sys/src/luau/compat.rs +++ b/mlua-sys/src/luau/compat.rs @@ -12,6 +12,11 @@ use super::luacode::*; pub const LUA_RESUMEERROR: c_int = -1; +// Keep in sync with Bytecode.h +const LBC_VERSION_MAX: u8 = 11; +const LBC_TYPE_VERSION_MIN: u8 = 1; +const LBC_TYPE_VERSION_MAX: u8 = 3; + unsafe fn compat53_reverse(L: *mut lua_State, mut a: c_int, mut b: c_int) { while a < b { lua_pushvalue(L, a); @@ -368,6 +373,24 @@ pub unsafe fn luaL_newmetatable(L: *mut lua_State, tname: *const c_char) -> c_in } } +// Detects whether a chunk is Luau bytecode or text source. +pub unsafe fn luaL_isbytecode(data: *const c_char, size: usize) -> bool { + if size == 0 { + return false; + } + match *data as u8 { + b if b < b'\t' => true, // bytecode + b if b <= LBC_VERSION_MAX => { + let types_version = (size >= 2).then(|| *data.add(1) as u8); + match types_version { + Some(LBC_TYPE_VERSION_MIN..=LBC_TYPE_VERSION_MAX) => true, // bytecode + _ => false, // text + } + } + _ => false, // text + } +} + pub unsafe fn luaL_loadbufferenv( L: *mut lua_State, data: *const c_char, @@ -384,19 +407,21 @@ pub unsafe fn luaL_loadbufferenv( free(*(data as *mut *mut c_char) as *mut c_void); } - let chunk_is_text = size == 0 || (*data as u8) >= b'\t'; + let is_bytecode = luaL_isbytecode(data, size); if !mode.is_null() { let modeb = CStr::from_ptr(mode).to_bytes(); - if !chunk_is_text && !modeb.contains(&b'b') { + let allow_binary = modeb.contains(&b'b'); + let allow_text = modeb.contains(&b't'); + if is_bytecode && !allow_binary { lua_pushfstring(L, cstr!("attempt to load a binary chunk (mode is '%s')"), mode); return LUA_ERRSYNTAX; - } else if chunk_is_text && !modeb.contains(&b't') { + } else if !is_bytecode && !allow_text { lua_pushfstring(L, cstr!("attempt to load a text chunk (mode is '%s')"), mode); return LUA_ERRSYNTAX; } } - let status = if chunk_is_text { + let status = if !is_bytecode { if env < 0 { env -= 1; } diff --git a/mlua-sys/src/luau/lauxlib.rs b/mlua-sys/src/luau/lauxlib.rs index f57c3cdc..0328aaf0 100644 --- a/mlua-sys/src/luau/lauxlib.rs +++ b/mlua-sys/src/luau/lauxlib.rs @@ -38,8 +38,10 @@ unsafe extern "C-unwind" { #[link_name = "luaL_checkinteger"] pub fn luaL_checkinteger_(L: *mut lua_State, narg: c_int) -> c_int; + pub fn luaL_checkinteger64(L: *mut lua_State, narg: c_int) -> i64; #[link_name = "luaL_optinteger"] pub fn luaL_optinteger_(L: *mut lua_State, narg: c_int, def: c_int) -> c_int; + pub fn luaL_optinteger64(L: *mut lua_State, narg: c_int, def: i64) -> i64; pub fn luaL_checkunsigned(L: *mut lua_State, narg: c_int) -> lua_Unsigned; pub fn luaL_optunsigned(L: *mut lua_State, narg: c_int, def: lua_Unsigned) -> lua_Unsigned; diff --git a/mlua-sys/src/luau/lua.rs b/mlua-sys/src/luau/lua.rs index 98ee5bc7..7fc6b19b 100644 --- a/mlua-sys/src/luau/lua.rs +++ b/mlua-sys/src/luau/lua.rs @@ -65,14 +65,15 @@ pub const LUA_TBOOLEAN: c_int = 1; pub const LUA_TLIGHTUSERDATA: c_int = 2; pub const LUA_TNUMBER: c_int = 3; -pub const LUA_TVECTOR: c_int = 4; +pub const LUA_TINTEGER: c_int = 4; +pub const LUA_TVECTOR: c_int = 5; -pub const LUA_TSTRING: c_int = 5; -pub const LUA_TTABLE: c_int = 6; -pub const LUA_TFUNCTION: c_int = 7; -pub const LUA_TUSERDATA: c_int = 8; -pub const LUA_TTHREAD: c_int = 9; -pub const LUA_TBUFFER: c_int = 10; +pub const LUA_TSTRING: c_int = 6; +pub const LUA_TTABLE: c_int = 7; +pub const LUA_TFUNCTION: c_int = 8; +pub const LUA_TUSERDATA: c_int = 9; +pub const LUA_TTHREAD: c_int = 10; +pub const LUA_TBUFFER: c_int = 11; /// Guaranteed number of Lua stack slots available to a C function. pub const LUA_MINSTACK: c_int = 20; @@ -153,6 +154,7 @@ unsafe extern "C-unwind" { pub fn lua_tounsignedx(L: *mut lua_State, idx: c_int, isnum: *mut c_int) -> lua_Unsigned; pub fn lua_tovector(L: *mut lua_State, idx: c_int) -> *const c_float; pub fn lua_toboolean(L: *mut lua_State, idx: c_int) -> c_int; + pub fn lua_tointeger64(L: *mut lua_State, idx: c_int, isinteger: *mut c_int) -> i64; pub fn lua_tolstring(L: *mut lua_State, idx: c_int, len: *mut usize) -> *const c_char; pub fn lua_tostringatom(L: *mut lua_State, idx: c_int, atom: *mut c_int) -> *const c_char; pub fn lua_tolstringatom( @@ -182,6 +184,7 @@ unsafe extern "C-unwind" { pub fn lua_pushnumber(L: *mut lua_State, n: lua_Number); #[link_name = "lua_pushinteger"] pub fn lua_pushinteger_(L: *mut lua_State, n: c_int); + pub fn lua_pushinteger64(L: *mut lua_State, n: i64); pub fn lua_pushunsigned(L: *mut lua_State, n: lua_Unsigned); #[cfg(not(feature = "luau-vector4"))] pub fn lua_pushvector(L: *mut lua_State, x: c_float, y: c_float, z: c_float); @@ -412,6 +415,11 @@ pub unsafe fn lua_isboolean(L: *mut lua_State, n: c_int) -> c_int { (lua_type(L, n) == LUA_TBOOLEAN) as c_int } +#[inline(always)] +pub unsafe fn lua_isinteger64(L: *mut lua_State, n: c_int) -> c_int { + (lua_type(L, n) == LUA_TINTEGER) as c_int +} + #[inline(always)] pub unsafe fn lua_isvector(L: *mut lua_State, n: c_int) -> c_int { (lua_type(L, n) == LUA_TVECTOR) as c_int @@ -501,6 +509,12 @@ pub type lua_Coverage = unsafe extern "C-unwind" fn( size: usize, ); +pub type lua_CounterFunction = + unsafe extern "C-unwind" fn(context: *mut c_void, function: *const c_char, linedefined: c_int); + +pub type lua_CounterValue = + unsafe extern "C-unwind" fn(context: *mut c_void, kind: c_int, line: c_int, hits: u64); + unsafe extern "C-unwind" { pub fn lua_stackdepth(L: *mut lua_State) -> c_int; pub fn lua_getinfo(L: *mut lua_State, level: c_int, what: *const c_char, ar: *mut lua_Debug) -> c_int; @@ -515,6 +529,14 @@ unsafe extern "C-unwind" { pub fn lua_getcoverage(L: *mut lua_State, funcindex: c_int, context: *mut c_void, callback: lua_Coverage); + pub fn lua_getcounters( + L: *mut lua_State, + funcindex: c_int, + context: *mut c_void, + functionvisit: lua_CounterFunction, + countervisit: lua_CounterValue, + ); + pub fn lua_debugtrace(L: *mut lua_State) -> *const c_char; } @@ -551,8 +573,8 @@ pub struct lua_Callbacks { /// gets called when L is created (LP == parent) or destroyed (LP == NULL) pub userthread: Option, - /// gets called when a string is created; returned atom can be retrieved via tostringatom - pub useratom: Option i16>, + /// gets called when a string is created to assign an atom id + pub useratom: Option i16>, /// gets called when BREAK instruction is encountered pub debugbreak: Option, diff --git a/mlua-sys/src/luau/luacode.rs b/mlua-sys/src/luau/luacode.rs index cd7ec376..1d74d453 100644 --- a/mlua-sys/src/luau/luacode.rs +++ b/mlua-sys/src/luau/luacode.rs @@ -80,6 +80,7 @@ unsafe extern "C" { pub fn luau_set_compile_constant_nil(cons: *mut lua_CompileConstant); pub fn luau_set_compile_constant_boolean(cons: *mut lua_CompileConstant, b: c_int); pub fn luau_set_compile_constant_number(cons: *mut lua_CompileConstant, n: f64); + pub fn luau_set_compile_constant_integer64(cons: *mut lua_CompileConstant, l: i64); pub fn luau_set_compile_constant_vector(cons: *mut lua_CompileConstant, x: f32, y: f32, z: f32, w: f32); pub fn luau_set_compile_constant_string(cons: *mut lua_CompileConstant, s: *const c_char, l: usize); } diff --git a/mlua-sys/src/luau/luacodegen.rs b/mlua-sys/src/luau/luacodegen.rs index 9e063ed2..0802c2c0 100644 --- a/mlua-sys/src/luau/luacodegen.rs +++ b/mlua-sys/src/luau/luacodegen.rs @@ -8,4 +8,7 @@ unsafe extern "C-unwind" { pub fn luau_codegen_supported() -> c_int; pub fn luau_codegen_create(state: *mut lua_State); pub fn luau_codegen_compile(state: *mut lua_State, idx: c_int); + + pub fn luau_enable_jit_inliner(state: *mut lua_State); + pub fn luau_disable_jit_inliner(state: *mut lua_State); } diff --git a/mlua-sys/src/luau/lualib.rs b/mlua-sys/src/luau/lualib.rs index a28a2a61..02ccf561 100644 --- a/mlua-sys/src/luau/lualib.rs +++ b/mlua-sys/src/luau/lualib.rs @@ -14,6 +14,7 @@ pub const LUA_UTF8LIBNAME: *const c_char = cstr!("utf8"); pub const LUA_MATHLIBNAME: *const c_char = cstr!("math"); pub const LUA_DBLIBNAME: *const c_char = cstr!("debug"); pub const LUA_VECLIBNAME: *const c_char = cstr!("vector"); +pub const LUA_INTLIBNAME: *const c_char = cstr!("integer"); unsafe extern "C-unwind" { pub fn luaopen_base(L: *mut lua_State) -> c_int; @@ -27,6 +28,7 @@ unsafe extern "C-unwind" { pub fn luaopen_math(L: *mut lua_State) -> c_int; pub fn luaopen_debug(L: *mut lua_State) -> c_int; pub fn luaopen_vector(L: *mut lua_State) -> c_int; + pub fn luaopen_integer(L: *mut lua_State) -> c_int; // open all builtin libraries pub fn luaL_openlibs(L: *mut lua_State); diff --git a/mlua_derive/Cargo.toml b/mlua_derive/Cargo.toml index 74d3c1ad..f19be27c 100644 --- a/mlua_derive/Cargo.toml +++ b/mlua_derive/Cargo.toml @@ -1,8 +1,9 @@ [package] name = "mlua_derive" -version = "0.11.0" +version = "0.12.0" authors = ["Aleksandr Orlenko "] -edition = "2021" +rust-version = "1.88" +edition = "2024" description = "Procedural macros for the mlua crate." repository = "https://github.com/mlua-rs/mlua" keywords = ["lua", "mlua"] @@ -12,13 +13,9 @@ license = "MIT" proc-macro = true [features] -macros = ["proc-macro-error2", "itertools", "regex", "once_cell"] +macros = [] [dependencies] quote = "1.0" proc-macro2 = { version = "1.0", features = ["span-locations"] } -proc-macro-error2 = { version = "2.0.1", optional = true } syn = { version = "2.0", features = ["full"] } -itertools = { version = "0.14", optional = true } -regex = { version = "1.4", optional = true } -once_cell = { version = "1.0", optional = true } diff --git a/mlua_derive/src/chunk.rs b/mlua_derive/src/chunk.rs deleted file mode 100644 index ee9b8221..00000000 --- a/mlua_derive/src/chunk.rs +++ /dev/null @@ -1,105 +0,0 @@ -use proc_macro::{TokenStream, TokenTree}; - -use crate::token::{Pos, Token, Tokens}; - -#[derive(Debug, Clone)] -pub(crate) struct Capture { - key: Token, - rust: TokenTree, -} - -impl Capture { - fn new(key: Token, rust: TokenTree) -> Self { - Self { key, rust } - } - - /// Token string inside `chunk!` - pub(crate) fn key(&self) -> &Token { - &self.key - } - - /// As rust variable, e.g. `x` - pub(crate) fn as_rust(&self) -> &TokenTree { - &self.rust - } -} - -#[derive(Debug)] -pub(crate) struct Captures(Vec); - -impl Captures { - pub(crate) fn new() -> Self { - Self(Vec::new()) - } - - pub(crate) fn add(&mut self, token: &Token) -> Capture { - let tt = token.tree(); - let key = token.clone(); - - match self.0.iter().find(|arg| arg.key() == &key) { - Some(arg) => arg.clone(), - None => { - let arg = Capture::new(key, tt.clone()); - self.0.push(arg.clone()); - arg - } - } - } - - pub(crate) fn captures(&self) -> &[Capture] { - &self.0 - } -} - -#[derive(Debug)] -pub(crate) struct Chunk { - source: String, - caps: Captures, -} - -impl Chunk { - pub(crate) fn new(tokens: TokenStream) -> Self { - let tokens = Tokens::retokenize(tokens); - - let mut source = String::new(); - let mut caps = Captures::new(); - - let mut pos: Option = None; - for t in tokens { - if t.is_cap() { - caps.add(&t); - } - - let (line, col) = (t.start().line, t.start().column); - let (prev_line, prev_col) = pos - .take() - .map(|lc| (lc.line, lc.column)) - .unwrap_or_else(|| (line, col)); - - #[allow(clippy::comparison_chain)] - if line > prev_line { - source.push('\n'); - } else if line == prev_line { - for _ in 0..col.saturating_sub(prev_col) { - source.push(' '); - } - } - source.push_str(&t.to_string()); - - pos = Some(t.end()); - } - - Self { - source: source.trim_end().to_string(), - caps, - } - } - - pub(crate) fn source(&self) -> &str { - &self.source - } - - pub(crate) fn captures(&self) -> &[Capture] { - self.caps.captures() - } -} diff --git a/mlua_derive/src/chunk/mod.rs b/mlua_derive/src/chunk/mod.rs new file mode 100644 index 00000000..ae38d5a7 --- /dev/null +++ b/mlua_derive/src/chunk/mod.rs @@ -0,0 +1,159 @@ +use std::ops::Deref; + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{ToTokens, quote}; + +use self::token::{Pos, Token, Tokens}; + +mod token; + +#[derive(Debug, Clone)] +pub(crate) struct Capture(Token); + +impl Deref for Capture { + type Target = Token; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Capture { + fn new(token: &Token) -> Self { + Self(token.clone()) + } + + pub(crate) fn name(&self) -> String { + self.0.to_string() + } +} + +impl ToTokens for Capture { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ts: TokenStream = self.0.tree().clone().into(); + tokens.extend(TokenStream2::from(ts)); + } +} + +#[derive(Debug)] +pub(crate) struct Captures(Vec); + +impl Captures { + pub(crate) fn new() -> Self { + Self(Vec::new()) + } + + pub(crate) fn add(&mut self, token: &Token) { + if self.0.iter().any(|arg| &**arg == token) { + return; + } + self.0.push(Capture::new(token)); + } + + pub(crate) fn captures(&self) -> &[Capture] { + &self.0 + } +} + +#[derive(Debug)] +pub(crate) struct Chunk { + source: String, + caps: Captures, +} + +impl Chunk { + pub(crate) fn new(tokens: TokenStream) -> Result { + let tokens = Tokens::retokenize(tokens)?; + + let mut source = String::new(); + let mut caps = Captures::new(); + + let mut prev_end: Option = None; + for t in tokens { + if t.is_cap() { + caps.add(&t); + } + + let (line, col) = (t.start().line, t.start().column); + if let Some(prev) = prev_end { + if line > prev.line { + source.push('\n'); + source.push_str(&" ".repeat(col.saturating_sub(1))); + } else if line == prev.line { + source.push_str(&" ".repeat(col.saturating_sub(prev.column))); + } + } else { + source.push_str(&" ".repeat(col.saturating_sub(1))); + } + source.push_str(&t.to_string()); + + prev_end = Some(t.end()); + } + + let source = source.trim_end().to_string(); + Ok(Self { source, caps }) + } + + pub(crate) fn captures(&self) -> &[Capture] { + self.caps.captures() + } + + pub(crate) fn expand(&self) -> TokenStream2 { + let source = &self.source; + + let caps_len = self.captures().len(); + let caps = self.captures().iter().map(|cap| { + let cap_name = cap.name(); + quote! { env.raw_set(#cap_name, #cap)?; } + }); + + quote! {{ + use mlua::chunk::{AsChunk, ChunkMode}; + use mlua::{Lua, Result, Table}; + use ::std::borrow::Cow; + use ::std::cell::Cell; + use ::std::io::Result as IoResult; + + struct InnerChunk Result
>(Cell>); + + impl AsChunk for InnerChunk + where + F: FnOnce(&Lua) -> Result
, + { + fn environment(&self, lua: &Lua) -> Result> { + if #caps_len > 0 { + if let Some(make_env) = self.0.take() { + return make_env(lua).map(Some); + } + } + Ok(None) + } + + fn mode(&self) -> Option { + Some(ChunkMode::Text) + } + + fn source<'a>(&self) -> IoResult> { + Ok(Cow::Borrowed((#source).as_bytes())) + } + } + + let make_env = move |lua: &Lua| -> Result
{ + let globals = lua.globals(); + let env = lua.create_table()?; + let meta = lua.create_table()?; + meta.raw_set("__index", &globals)?; + meta.raw_set("__newindex", &globals)?; + + // Add captured variables + #(#caps)* + + env.set_metatable(Some(meta))?; + Ok(env) + }; + + InnerChunk(Cell::new(Some(make_env))) + }} + } +} diff --git a/mlua_derive/src/token.rs b/mlua_derive/src/chunk/token.rs similarity index 53% rename from mlua_derive/src/token.rs rename to mlua_derive/src/chunk/token.rs index c6ce7c97..da7a14c8 100644 --- a/mlua_derive/src/token.rs +++ b/mlua_derive/src/chunk/token.rs @@ -1,12 +1,10 @@ use std::cmp::{Eq, PartialEq}; +use std::convert::TryFrom; use std::fmt::{self, Display, Formatter}; use std::vec::IntoIter; -use itertools::Itertools; -use once_cell::sync::Lazy; use proc_macro::{Delimiter, Span, TokenStream, TokenTree}; -use proc_macro2::Span as Span2; -use regex::Regex; +use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; #[derive(Clone, Copy, Debug)] pub(crate) struct Pos { @@ -34,49 +32,21 @@ impl Pos { } } -fn span_pos(span: &Span) -> (Pos, Pos) { +fn span_pos(span: &Span) -> Result<(Pos, Pos), TokenStream2> { let span2: Span2 = (*span).into(); let start = span2.start(); let end = span2.end(); - // In stable, line/column information is not provided - // and set to 0 (line is 1-indexed) + // Rust 1.88 stabilized Span APIs, so this branch must be unreachable if start.line == 0 || end.line == 0 { - return fallback_span_pos(span); + return Err(syn::Error::new( + Span2::call_site(), + "cannot retrieve span location information; mlua requires nightly Rust or stable >= 1.88", + ) + .to_compile_error()); } - (Pos::new(start.line, start.column), Pos::new(end.line, end.column)) -} - -fn parse_pos(span: &Span) -> Option<(usize, usize)> { - // Workaround to somehow retrieve location information in span in stable rust :( - - static RE: Lazy = Lazy::new(|| Regex::new(r"bytes\(([0-9]+)\.\.([0-9]+)\)").unwrap()); - - match RE.captures(&format!("{span:?}")) { - Some(caps) => match (caps.get(1), caps.get(2)) { - (Some(start), Some(end)) => Some(( - match start.as_str().parse() { - Ok(v) => v, - _ => return None, - }, - match end.as_str().parse() { - Ok(v) => v, - _ => return None, - }, - )), - _ => None, - }, - None => None, - } -} - -fn fallback_span_pos(span: &Span) -> (Pos, Pos) { - let (start, end) = match parse_pos(span) { - Some(v) => v, - None => proc_macro_error2::abort_call_site!("Cannot retrieve span information; please use nightly"), - }; - (Pos::new(1, start), Pos::new(1, end)) + Ok((Pos::new(start.line, start.column), Pos::new(end.line, end.column))) } /// Attribute of token. @@ -106,32 +76,32 @@ impl PartialEq for Token { impl Eq for Token {} impl Token { - fn new(tree: TokenTree) -> Self { - let (start, end) = span_pos(&tree.span()); - Self { - source: tree.to_string(), + fn new(tree: TokenTree) -> Result { + let (start, end) = span_pos(&tree.span())?; + let source = tree.span().source_text().unwrap_or_else(|| tree.to_string()); + Ok(Self { + source, start, end, tree, attr: TokenAttr::None, - } + }) } - fn new_delim(source: String, tree: TokenTree, open: bool) -> Self { - let (start, end) = span_pos(&tree.span()); + fn new_delim(source: String, tree: TokenTree, open: bool) -> Result { + let (start, end) = span_pos(&tree.span())?; let (start, end) = if open { (start, start.right()) } else { (end.left(), end) }; - - Self { + Ok(Self { source, tree, start, end, attr: TokenAttr::None, - } + }) } pub(crate) fn tree(&self) -> &TokenTree { @@ -164,24 +134,33 @@ impl Token { pub(crate) struct Tokens(pub(crate) Vec); impl Tokens { - pub(crate) fn retokenize(tt: TokenStream) -> Tokens { - Tokens( - tt.into_iter() - .flat_map(Tokens::from) - .peekable() - .batching(|iter| { - // Find variable tokens - let t = iter.next()?; - if t.is("$") { - // `$` + `ident` => `$ident` - let t = iter.next().expect("$ must trail an identifier"); - Some(t.attr(TokenAttr::Cap)) - } else { - Some(t) - } - }) - .collect(), - ) + pub(crate) fn retokenize(tt: TokenStream) -> Result { + let mut flat = Vec::new(); + for tree in tt { + flat.extend(Tokens::try_from(tree)?); + } + + let mut tokens = Vec::new(); + let mut iter = flat.into_iter(); + while let Some(t) = iter.next() { + // Find variable tokens: `$` + `ident` => `$ident` + if t.is("$") { + if let Some(next) = iter.next() + && matches!(next.tree, TokenTree::Ident(_)) + { + tokens.push(next.attr(TokenAttr::Cap)); + } else { + return Err(syn::Error::new( + t.tree.span().into(), + "`$` must be followed by an identifier", + ) + .to_compile_error()); + } + } else { + tokens.push(t); + } + } + Ok(Tokens(tokens)) } } @@ -194,8 +173,10 @@ impl IntoIterator for Tokens { } } -impl From for Tokens { - fn from(tt: TokenTree) -> Self { +impl TryFrom for Tokens { + type Error = TokenStream2; + + fn try_from(tt: TokenTree) -> Result { let tts = match tt.clone() { TokenTree::Group(g) => { let (b, e) = match g.delimiter() { @@ -206,15 +187,16 @@ impl From for Tokens { }; let (b, e) = (b.into(), e.into()); - vec![Token::new_delim(b, tt.clone(), true)] - .into_iter() - .chain(g.stream().into_iter().flat_map(Tokens::from)) - .chain(vec![Token::new_delim(e, tt, false)]) - .collect() + let mut result = vec![Token::new_delim(b, tt.clone(), true)?]; + for inner in g.stream() { + result.extend(Tokens::try_from(inner)?); + } + result.push(Token::new_delim(e, tt, false)?); + result } - _ => vec![Token::new(tt)], + _ => vec![Token::new(tt)?], }; - Tokens(tts) + Ok(Tokens(tts)) } } diff --git a/mlua_derive/src/from_lua.rs b/mlua_derive/src/from_lua.rs index e74eb868..16556caa 100644 --- a/mlua_derive/src/from_lua.rs +++ b/mlua_derive/src/from_lua.rs @@ -1,31 +1,34 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, DeriveInput}; +use syn::ext::IdentExt; +use syn::{DeriveInput, parse_macro_input, parse_quote}; pub fn from_lua(input: TokenStream) -> TokenStream { - let DeriveInput { ident, generics, .. } = parse_macro_input!(input as DeriveInput); + let DeriveInput { + ident, mut generics, .. + } = parse_macro_input!(input as DeriveInput); - let ident_str = ident.to_string(); - let (impl_generics, ty_generics, _) = generics.split_for_impl(); - let where_clause = match &generics.where_clause { - Some(where_clause) => quote! { #where_clause, Self: 'static + Clone }, - None => quote! { where Self: 'static + Clone }, - }; + let ident_str = ident.unraw().to_string(); + generics + .make_where_clause() + .predicates + .push(parse_quote!(Self: 'static + Clone)); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); quote! { - impl #impl_generics ::mlua::FromLua for #ident #ty_generics #where_clause { - #[inline] - fn from_lua(value: ::mlua::Value, _: &::mlua::Lua) -> ::mlua::Result { - match value { - ::mlua::Value::UserData(ud) => Ok(ud.borrow::()?.clone()), - _ => Err(::mlua::Error::FromLuaConversionError { - from: value.type_name(), - to: #ident_str.to_string(), - message: None, - }), - } + impl #impl_generics ::mlua::FromLua for #ident #ty_generics #where_clause { + #[inline] + fn from_lua(value: ::mlua::Value, _: &::mlua::Lua) -> ::mlua::Result { + match value { + ::mlua::Value::UserData(ud) => Ok(ud.borrow::()?.clone()), + _ => Err(::mlua::Error::FromLuaConversionError { + from: value.type_name(), + to: #ident_str.to_string(), + message: None, + }), + } + } } - } } .into() } diff --git a/mlua_derive/src/lib.rs b/mlua_derive/src/lib.rs index f7d04803..1031e13c 100644 --- a/mlua_derive/src/lib.rs +++ b/mlua_derive/src/lib.rs @@ -1,148 +1,32 @@ use proc_macro::TokenStream; -use proc_macro2::{Ident, Span}; -use quote::quote; -use syn::meta::ParseNestedMeta; -use syn::{parse_macro_input, ItemFn, LitStr, Result}; -#[cfg(feature = "macros")] -use { - crate::chunk::Chunk, proc_macro::TokenTree, proc_macro2::TokenStream as TokenStream2, - proc_macro_error2::proc_macro_error, -}; +mod module; -#[derive(Default)] -struct ModuleAttributes { - name: Option, - skip_memory_check: bool, -} +#[cfg(feature = "macros")] +use crate::chunk::Chunk; -impl ModuleAttributes { - fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> { - if meta.path.is_ident("name") { - match meta.value() { - Ok(value) => { - self.name = Some(value.parse::()?.parse()?); - } - Err(_) => { - return Err(meta.error("`name` attribute must have a value")); - } - } - } else if meta.path.is_ident("skip_memory_check") { - if meta.value().is_ok() { - return Err(meta.error("`skip_memory_check` attribute have no values")); - } - self.skip_memory_check = true; - } else { - return Err(meta.error("unsupported module attribute")); +#[cfg(feature = "macros")] +macro_rules! try_compile { + ($expr:expr) => { + match $expr { + Ok(val) => val, + Err(err) => return err.to_compile_error().into(), } - Ok(()) - } + }; } #[proc_macro_attribute] pub fn lua_module(attr: TokenStream, item: TokenStream) -> TokenStream { - let mut args = ModuleAttributes::default(); - if !attr.is_empty() { - let args_parser = syn::meta::parser(|meta| args.parse(meta)); - parse_macro_input!(attr with args_parser); - } - - let func = parse_macro_input!(item as ItemFn); - let func_name = &func.sig.ident; - let module_name = args.name.unwrap_or_else(|| func_name.clone()); - let ext_entrypoint_name = Ident::new(&format!("luaopen_{module_name}"), Span::call_site()); - let skip_memory_check = if args.skip_memory_check { - quote! { lua.skip_memory_check(true); } - } else { - quote! {} - }; - - let wrapped = quote! { - mlua::require_module_feature!(); - - #func - - #[no_mangle] - unsafe extern "C-unwind" fn #ext_entrypoint_name(state: *mut mlua::lua_State) -> ::std::os::raw::c_int { - mlua::Lua::entrypoint1(state, move |lua| { - #skip_memory_check - #func_name(lua) - }) - } - }; - - wrapped.into() -} - -#[cfg(feature = "macros")] -fn to_ident(tt: &TokenTree) -> TokenStream2 { - let s: TokenStream = tt.clone().into(); - s.into() + module::lua_module(attr, item) } #[cfg(feature = "macros")] #[proc_macro] -#[proc_macro_error] pub fn chunk(input: TokenStream) -> TokenStream { - let chunk = Chunk::new(input); - - let source = chunk.source(); - - let caps_len = chunk.captures().len(); - let caps = chunk.captures().iter().map(|cap| { - let cap_name = cap.as_rust().to_string(); - let cap = to_ident(cap.as_rust()); - quote! { env.raw_set(#cap_name, #cap)?; } - }); - - let wrapped_code = quote! {{ - use mlua::{AsChunk, ChunkMode, Lua, Result, Table}; - use ::std::borrow::Cow; - use ::std::cell::Cell; - use ::std::io::Result as IoResult; - - struct InnerChunk Result
>(Cell>); - - impl AsChunk for InnerChunk - where - F: FnOnce(&Lua) -> Result
, - { - fn environment(&self, lua: &Lua) -> Result> { - if #caps_len > 0 { - if let Some(make_env) = self.0.take() { - return make_env(lua).map(Some); - } - } - Ok(None) - } - - fn mode(&self) -> Option { - Some(ChunkMode::Text) - } - - fn source<'a>(&self) -> IoResult> { - Ok(Cow::Borrowed((#source).as_bytes())) - } - } - - let make_env = move |lua: &Lua| -> Result
{ - let globals = lua.globals(); - let env = lua.create_table()?; - let meta = lua.create_table()?; - meta.raw_set("__index", &globals)?; - meta.raw_set("__newindex", &globals)?; - - // Add captured variables - #(#caps)* - - env.set_metatable(Some(meta))?; - Ok(env) - }; - - InnerChunk(Cell::new(Some(make_env))) - }}; - - wrapped_code.into() + match Chunk::new(input) { + Ok(chunk) => chunk.expand().into(), + Err(err) => err.into(), + } } #[cfg(feature = "macros")] @@ -151,9 +35,23 @@ pub fn from_lua(input: TokenStream) -> TokenStream { from_lua::from_lua(input) } +/// Derive macro for implementing `UserData` for a Rust type. +#[cfg(feature = "macros")] +#[proc_macro_derive(UserData, attributes(lua))] +pub fn userdata(item: TokenStream) -> TokenStream { + userdata::userdata_type(item) +} + +/// Attribute macro for exposing impl block methods to Lua userdata. +#[cfg(feature = "macros")] +#[proc_macro_attribute] +pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { + userdata::userdata_impl::userdata_impl(attr, item) +} + #[cfg(feature = "macros")] mod chunk; #[cfg(feature = "macros")] mod from_lua; #[cfg(feature = "macros")] -mod token; +mod userdata; diff --git a/mlua_derive/src/module.rs b/mlua_derive/src/module.rs new file mode 100644 index 00000000..bad7bf04 --- /dev/null +++ b/mlua_derive/src/module.rs @@ -0,0 +1,68 @@ +use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; +use quote::quote; +use syn::meta::ParseNestedMeta; +use syn::{ItemFn, LitStr, Result, parse_macro_input}; + +#[derive(Default)] +struct ModuleAttributes { + name: Option, + skip_memory_check: bool, +} + +impl ModuleAttributes { + fn parse(&mut self, meta: ParseNestedMeta) -> Result<()> { + if meta.path.is_ident("name") { + match meta.value() { + Ok(value) => { + self.name = Some(value.parse::()?.parse()?); + } + Err(_) => { + return Err(meta.error("`name` attribute must have a value")); + } + } + } else if meta.path.is_ident("skip_memory_check") { + if meta.value().is_ok() { + return Err(meta.error("`skip_memory_check` attribute have no values")); + } + self.skip_memory_check = true; + } else { + return Err(meta.error("unsupported module attribute")); + } + Ok(()) + } +} + +pub fn lua_module(attr: TokenStream, item: TokenStream) -> TokenStream { + let mut args = ModuleAttributes::default(); + if !attr.is_empty() { + let args_parser = syn::meta::parser(|meta| args.parse(meta)); + parse_macro_input!(attr with args_parser); + } + + let func = parse_macro_input!(item as ItemFn); + let func_name = &func.sig.ident; + let module_name = args.name.unwrap_or_else(|| func_name.clone()); + let ext_entrypoint_name = Ident::new(&format!("luaopen_{module_name}"), Span::call_site()); + let skip_memory_check = if args.skip_memory_check { + quote! { lua.skip_memory_check(true); } + } else { + quote! {} + }; + + let wrapped = quote! { + mlua::require_module_feature!(); + + #func + + #[unsafe(no_mangle)] + unsafe extern "C-unwind" fn #ext_entrypoint_name(state: *mut mlua::lua_State) -> ::std::os::raw::c_int { + mlua::Lua::entrypoint1(state, move |lua| { + #skip_memory_check + #func_name(lua) + }) + } + }; + + wrapped.into() +} diff --git a/mlua_derive/src/userdata/attr.rs b/mlua_derive/src/userdata/attr.rs new file mode 100644 index 00000000..1dfa2263 --- /dev/null +++ b/mlua_derive/src/userdata/attr.rs @@ -0,0 +1,94 @@ +use proc_macro2::Span; +use syn::ext::IdentExt; +use syn::meta::ParseNestedMeta; +use syn::{Ident, LitStr, Result}; + +/// Parsed `#[lua(...)]` attribute. +/// +/// Some flags are context-dependent: +/// - Struct fields: `get`, `set`, `name`, `skip` +/// - Impl methods: `getter`, `setter`, `field`, `meta`, `infallible`, `name`, `skip` +#[derive(Default)] +pub(crate) struct LuaAttr { + pub(crate) span: Option, + pub(crate) name: Option, + pub(crate) infallible: bool, + pub(crate) skip: bool, + + // Struct field context flags + pub(crate) get: bool, + pub(crate) set: bool, + + // Impl method context flags + pub(crate) getter: bool, + pub(crate) setter: bool, + pub(crate) field: bool, + pub(crate) meta: bool, +} + +impl LuaAttr { + pub(crate) fn parse_inner(&mut self, meta: ParseNestedMeta) -> Result<()> { + match &meta.path { + path if path.is_ident("skip") => { + if meta.value().is_ok() { + return Err(meta.error("`skip` does not take a value")); + } + self.skip = true; + } + path if path.is_ident("infallible") => { + if meta.value().is_ok() { + return Err(meta.error("`infallible` does not take a value")); + } + self.infallible = true; + } + path if path.is_ident("get") => self.get = true, + path if path.is_ident("set") => self.set = true, + path if path.is_ident("getter") => self.getter = true, + path if path.is_ident("setter") => self.setter = true, + path if path.is_ident("field") => self.field = true, + path if path.is_ident("meta") => self.meta = true, + path if path.is_ident("name") => { + let value = meta.value()?; + let lit: LitStr = value.parse()?; + self.name = Some(lit.value()); + } + _ => { + return Err(meta.error( + "unsupported lua attribute, expected: ".to_string() + + "`skip`, `infallible`, `get`, `set`, `getter`, `setter`, `field`, `meta`, `name`", + )); + } + } + Ok(()) + } + + /// Returns the effective Lua name. + pub(crate) fn name(&self, ident: &Ident) -> String { + self.name.clone().unwrap_or_else(|| ident.unraw().to_string()) + } + + /// Returns the span to use for error reporting. + pub(crate) fn span(&self) -> Span { + self.span.unwrap_or_else(Span::call_site) + } + + /// Returns the effective Lua metamethod name. + /// + /// If `name` is set via attribute, use it. Otherwise, if the function name + /// starts with `__`, use that. Returns an error if neither is available. + pub(crate) fn effective_meta_name(&self, fn_ident: &Ident) -> Result { + if let Some(ref name) = self.name { + return Ok(name.clone()); + } + let fn_name = fn_ident.unraw().to_string(); + if fn_name.starts_with("__") { + return Ok(fn_name); + } + Err(syn::Error::new( + fn_ident.span(), + format!( + "could not infer metamethod name from `{fn_name}`, either add `name = \"...\"` to `#[lua(meta, ...)]` or prefix the function with `__`" + ), + )) + } +} diff --git a/mlua_derive/src/userdata/mod.rs b/mlua_derive/src/userdata/mod.rs new file mode 100644 index 00000000..15ae11a0 --- /dev/null +++ b/mlua_derive/src/userdata/mod.rs @@ -0,0 +1,166 @@ +mod attr; +pub(crate) mod userdata_impl; + +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::ext::IdentExt; +use syn::spanned::Spanned; +use syn::{Attribute, Data, DeriveInput, Error, Fields, FieldsNamed, Meta, parse_macro_input}; + +use self::attr::LuaAttr; + +/// Wrap registration tokens with any `#[cfg]`/`#[cfg_attr]` attributes from the original item. +pub(crate) fn with_cfg(tokens: proc_macro2::TokenStream, attrs: &[Attribute]) -> proc_macro2::TokenStream { + let cfgs: Vec<_> = (attrs.iter()) + .filter(|attr| attr.path().is_ident("cfg") || attr.path().is_ident("cfg_attr")) + .collect(); + if cfgs.is_empty() { + return tokens; + } + quote! { + #(#cfgs)* + #tokens + } +} + +/// Parse all `#[lua(...)]` attributes on a field, merging them into one `LuaAttr`. +fn parse_field_lua_attr(attrs: &[Attribute]) -> syn::Result { + let mut lua_attr = LuaAttr::default(); + for attr in attrs { + if !attr.path().is_ident("lua") { + continue; + } + match &attr.meta { + Meta::List(_) => { + lua_attr.span = Some(attr.span()); + attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + validate_field_lua_attr(&lua_attr)?; + } + Meta::Path(_) => {} + Meta::NameValue(_) => { + return Err(syn::Error::new_spanned( + attr, + "`#[lua = \"...\"]` is not supported: use `#[lua(attr = \"...\")]`", + )); + } + } + } + Ok(lua_attr) +} + +fn validate_field_lua_attr(attr: &LuaAttr) -> syn::Result<()> { + for (set, name) in [ + (attr.getter, "getter"), + (attr.setter, "setter"), + (attr.field, "field"), + (attr.meta, "meta"), + (attr.infallible, "infallible"), + ] { + if set { + return Err(syn::Error::new( + attr.span(), + format!("`{name}` is not valid for struct fields"), + )); + } + } + Ok(()) +} + +pub fn userdata_type(item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as DeriveInput); + let type_name = &input.ident; + + let named_fields: Option<&FieldsNamed> = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => Some(fields), + Fields::Unnamed(_) | Fields::Unit => None, + }, + Data::Enum(_) => None, + Data::Union(_) => { + return Error::new_spanned(&input, "`#[derive(UserData)]` cannot be applied to unions") + .to_compile_error() + .into(); + } + }; + + // Check for generic parameters (not supported) + let has_generics = !input.generics.params.is_empty(); + if has_generics { + return Error::new_spanned( + &input.generics, + "`#[derive(UserData)]` does not support generic type parameters. Wrap the generic type in a concrete newtype instead." + ) + .to_compile_error() + .into(); + } + + let mut field_registrations = Vec::new(); + if let Some(fields) = &named_fields { + for field in &fields.named { + let field_name = field.ident.as_ref().unwrap(); + + let lua_attr = try_compile!(parse_field_lua_attr(&field.attrs)); + if lua_attr.skip { + continue; + } + + let lua_name = lua_attr.name.unwrap_or_else(|| field_name.unraw().to_string()); + + // Assume get/set by default (unless explicitly specified) + let (has_get, has_set) = if lua_attr.get || lua_attr.set { + (lua_attr.get, lua_attr.set) + } else { + (true, true) + }; + + if has_get { + let tokens = quote! { + registry.add_field_method_get(#lua_name, |_lua, this| Ok(this.#field_name.clone())); + }; + field_registrations.push(with_cfg(tokens, &field.attrs)); + } + if has_set { + let tokens = quote! { + registry.add_field_method_set(#lua_name, |_lua, this, val| { + this.#field_name = val; + Ok(()) + }); + }; + field_registrations.push(with_cfg(tokens, &field.attrs)); + } + } + } + + let registration_type_name = format_ident!("__MluaUserDataRegistration_{type_name}"); + let register_fields_fn_name = format_ident!("__mlua_register_{type_name}_fields"); + + let output = quote! { + #[doc(hidden)] + #[allow(non_camel_case_types)] + struct #registration_type_name { + register: fn(&mut ::mlua::userdata::UserDataRegistry<#type_name>), + } + + ::mlua::__inventory::collect!(#registration_type_name); + + #[allow(non_snake_case)] + fn #register_fields_fn_name(registry: &mut ::mlua::userdata::UserDataRegistry<#type_name>) { + use ::mlua::userdata::UserDataFields as _; + #(#field_registrations)* + } + + ::mlua::__inventory::submit! { + #registration_type_name { register: #register_fields_fn_name } + } + + impl ::mlua::userdata::UserData for #type_name { + fn register(registry: &mut ::mlua::userdata::UserDataRegistry) { + for item in ::mlua::__inventory::iter::<#registration_type_name> { + (item.register)(registry); + } + } + } + }; + + output.into() +} diff --git a/mlua_derive/src/userdata/userdata_impl.rs b/mlua_derive/src/userdata/userdata_impl.rs new file mode 100644 index 00000000..8bed5b67 --- /dev/null +++ b/mlua_derive/src/userdata/userdata_impl.rs @@ -0,0 +1,818 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use proc_macro::TokenStream; +use proc_macro2::{Span as Span2, TokenStream as TokenStream2}; +use quote::{format_ident, quote}; +use syn::spanned::Spanned; +use syn::{ + Attribute, FnArg, Ident, ImplItem, ItemImpl, Meta, Signature, Type, parse_macro_input, parse_quote, +}; + +use super::attr::LuaAttr; +use super::with_cfg; + +/// `&T` reference types that mlua provides as wrapper types via `FromLua`. +static BORROW_WRAPPERS: &[(&str, &str)] = &[ + ("str", "::mlua::string::BorrowedStr"), + ("[u8]", "::mlua::string::BorrowedBytes"), +]; + +enum SelfKind { + Ref(RefKind), + Owned, + None, +} + +enum RefKind { + Ref, + Mut, + OptionRef, + OptionMut, +} + +struct ArgInfo { + ident: Ident, + userdata_ref: Option, + callback_type: Type, +} + +struct MethodInfo { + self_kind: SelfKind, + lua: Option, + args: Vec, +} + +/// Extract the inner type from a reference type. +fn ref_inner_type(ty: &Type) -> Type { + match ty { + Type::Reference(ref_ty) => (*ref_ty.elem).clone(), + _ => ty.clone(), + } +} + +/// How the `Lua` context parameter is passed to the method. +enum LuaArg { + Ref, + Owned, +} + +/// Check if the type is `Lua` or `mlua::Lua`. +fn is_lua_type(ty: &Type) -> bool { + let Type::Path(p) = ty else { return false }; + match p.path.segments.len() { + 1 => p.path.segments[0].ident == "Lua", + 2 => p.path.segments[0].ident == "mlua" && p.path.segments[1].ident == "Lua", + _ => false, + } +} + +/// Classify the method's `Lua` context parameter, if present. +fn lua_arg_kind(ty: &Type) -> Option { + match ty { + Type::Reference(r) if r.mutability.is_none() && is_lua_type(&r.elem) => Some(LuaArg::Ref), + ty if is_lua_type(ty) => Some(LuaArg::Owned), + _ => None, + } +} + +/// Classify a `&[mut] T` parameter, returning the callback wrapper type. +/// +/// Known borrow types come from the mapping table `BORROW_WRAPPERS`. +/// Everything else gets `UserDataRef[Mut]`. +fn classify_ref_type(ty: &Type) -> Option { + let Type::Reference(ref_ty) = ty else { return None }; + + // Check known borrow wrappers: + // - For `&T` check the path name + // - For `&[T]` unpack the slice and format the element as `[T]` for lookup + if ref_ty.mutability.is_none() { + let lookup_name: Option = match &*ref_ty.elem { + Type::Path(path) => path.path.segments.last().map(|seg| seg.ident.to_string()), + Type::Slice(slice) => { + if let Type::Path(path) = &*slice.elem { + path.path.segments.last().map(|seg| format!("[{}]", seg.ident)) + } else { + None + } + } + _ => None, + }; + if let Some(ref name) = lookup_name { + for &(inner, wrapper) in BORROW_WRAPPERS { + if name == inner { + let wrapper = syn::parse_str(wrapper).expect("invalid wrapper type"); + return Some(wrapper); + } + } + } + } + // Mutable references to slices are not supported. + if matches!(&*ref_ty.elem, Type::Slice(_)) && ref_ty.mutability.is_some() { + return None; + } + + let inner = ref_inner_type(ty); + if ref_ty.mutability.is_none() { + Some(parse_quote! { ::mlua::userdata::UserDataRef<#inner> }) + } else { + Some(parse_quote! { ::mlua::userdata::UserDataRefMut<#inner> }) + } +} + +/// If `ty` is `Option`, return the inner type. +fn try_unwrap_option(ty: &Type) -> Option<&Type> { + let Type::Path(type_path) = ty else { return None }; + let segment = type_path.path.segments.last()?; + if segment.ident != "Option" { + return None; + } + let syn::PathArguments::AngleBracketed(args) = &segment.arguments else { + return None; + }; + if args.args.len() != 1 { + return None; + } + let syn::GenericArgument::Type(inner) = &args.args[0] else { + return None; + }; + Some(inner) +} + +/// Analyze method signature. +/// +/// Determine `self` kind and collect the callback arguments. +/// Auto-detects `Lua` (owned or reference) as the first non-self parameter. +fn analyze_self_and_args(sig: &Signature) -> syn::Result { + let mut self_kind = SelfKind::None; + let mut lua = None; + let mut args = Vec::new(); + let mut check_first_typed = true; + + for param in &sig.inputs { + match param { + FnArg::Receiver(recv) if recv.reference.is_some() && recv.mutability.is_some() => { + self_kind = SelfKind::Ref(RefKind::Mut); + } + FnArg::Receiver(recv) if recv.reference.is_some() => { + self_kind = SelfKind::Ref(RefKind::Ref); + } + FnArg::Receiver(_) => { + self_kind = SelfKind::Owned; + } + FnArg::Typed(typed) => { + if check_first_typed { + check_first_typed = false; + if let Some(kind) = lua_arg_kind(&typed.ty) { + lua = Some(kind); + continue; + } + } + + let ident = match &*typed.pat { + syn::Pat::Ident(pat_ident) => pat_ident.ident.clone(), + syn::Pat::Wild(_) => { + // For wildcards we generate a unique identifier to avoid collisions with other + // parameters. + Ident::new(&format!("__mlua_arg_{}", args.len()), Span2::mixed_site()) + } + _ => { + return Err(syn::Error::new_spanned( + &typed.pat, + "`#[mlua::userdata_impl]` requires a named parameter or `_`; destructuring patterns are not supported", + )); + } + }; + + let arg_type = &*typed.ty; + let mut option_inner = None; + let ref_kind = match arg_type { + Type::Reference(r) if r.mutability.is_some() => Some(RefKind::Mut), + Type::Reference(_) => Some(RefKind::Ref), + _ => { + // Check if it's `Option<&T>` or `Option<&mut T>` + option_inner = try_unwrap_option(arg_type); + option_inner.and_then(|inner| match inner { + Type::Reference(r) if r.mutability.is_some() => Some(RefKind::OptionMut), + Type::Reference(_) => Some(RefKind::OptionRef), + _ => None, + }) + } + }; + let callback_type = match &ref_kind { + Some(RefKind::OptionRef | RefKind::OptionMut) => { + match classify_ref_type(option_inner.unwrap()) { + Some(ty) => parse_quote! { Option<#ty> }, + None => { + return Err(syn::Error::new_spanned( + arg_type, + "this reference type is not supported as a callback parameter", + )); + } + } + } + Some(_) => match classify_ref_type(arg_type) { + Some(ty) => ty, + None => { + return Err(syn::Error::new_spanned( + arg_type, + "this reference type is not supported as a callback parameter", + )); + } + }, + None => arg_type.clone(), + }; + args.push(ArgInfo { + ident, + userdata_ref: ref_kind, + callback_type, + }); + } + } + } + + Ok(MethodInfo { self_kind, lua, args }) +} + +fn strip_item_attrs(attrs: &[Attribute]) -> Vec { + (attrs.iter()) + .filter(|attr| !attr.path().is_ident("lua")) + .cloned() + .collect() +} + +fn parse_lua_attr(attrs: &[Attribute]) -> syn::Result { + let mut lua_attr = LuaAttr::default(); + for attr in attrs { + if !attr.path().is_ident("lua") { + continue; + } + match &attr.meta { + Meta::List(_) => { + lua_attr.span = Some(attr.span()); + attr.parse_nested_meta(|meta| lua_attr.parse_inner(meta))?; + validate_lua_attr(&lua_attr)?; + } + Meta::Path(_) => {} + Meta::NameValue(_) => { + return Err(syn::Error::new_spanned( + attr, + "`#[lua = \"...\"]` is not supported: use `#[lua(attr = \"...\")]`", + )); + } + } + } + Ok(lua_attr) +} + +fn validate_lua_attr(attr: &LuaAttr) -> syn::Result<()> { + for (set, name) in [(attr.get, "get"), (attr.set, "set")] { + if set { + return Err(syn::Error::new( + attr.span(), + format!("`{name}` is not valid for methods"), + )); + } + } + Ok(()) +} + +pub fn userdata_impl(attr: TokenStream, item: TokenStream) -> TokenStream { + if !attr.is_empty() { + return syn::Error::new_spanned( + proc_macro2::TokenStream::from(attr), + "`#[userdata_impl]` does not accept arguments", + ) + .to_compile_error() + .into(); + } + + let mut input = parse_macro_input!(item as ItemImpl); + + // Generic impl blocks are not supported + if !input.generics.params.is_empty() { + return syn::Error::new_spanned( + &input.generics, + "`#[mlua::userdata_impl]` does not support generic impl blocks.", + ) + .to_compile_error() + .into(); + } + if let Some(where_clause) = &input.generics.where_clause { + return syn::Error::new_spanned( + where_clause, + "`#[mlua::userdata_impl]` does not support `where` clauses on the impl block.", + ) + .to_compile_error() + .into(); + } + + let type_path = match &*input.self_ty { + Type::Path(type_path) => &type_path.path, + _ => { + return syn::Error::new_spanned(&input.self_ty, "`#[userdata_impl]` requires a simple path type") + .to_compile_error() + .into(); + } + }; + let type_name = (type_path.segments) + .last() + .map(|seg| seg.ident.clone()) + .ok_or_else(|| syn::Error::new_spanned(&input.self_ty, "cannot determine type name")); + let type_name = try_compile!(type_name); + + static COUNTER: AtomicUsize = AtomicUsize::new(0); + let unique_suffix = COUNTER.fetch_add(1, Ordering::Relaxed); + let register_fn_name = format_ident!("__mlua_register_{type_name}_{unique_suffix}"); + let registration_type_name = format_ident!("__MluaUserDataRegistration_{type_name}"); + + let mut registration_calls = Vec::new(); + for item in &input.items { + match item { + ImplItem::Const(const_item) => { + let lua_attr = try_compile!(parse_lua_attr(&const_item.attrs)); + if lua_attr.skip { + continue; + } + if lua_attr.getter || lua_attr.setter { + return syn::Error::new( + lua_attr.span(), + "const items do not support `getter` or `setter`", + ) + .to_compile_error() + .into(); + } + let const_name = &const_item.ident; + let lua_name = lua_attr.name(const_name); + let tokens = if lua_attr.meta { + quote! { + registry.add_meta_field(#lua_name, #type_path::#const_name); + } + } else { + quote! { + registry.add_field(#lua_name, #type_path::#const_name); + } + }; + registration_calls.push(with_cfg(tokens, &const_item.attrs)); + } + ImplItem::Fn(method) => { + let lua_attr = try_compile!(parse_lua_attr(&method.attrs)); + if lua_attr.skip { + continue; + } + + // Validate mutually exclusive role flags. + // `getter`, `setter`, `field` are exclusive. + // `meta` on its own means a metamethod. + // `meta` combined with `field` means a meta static field. + // `meta` with `getter` or `setter` is invalid. + let primary = [lua_attr.getter, lua_attr.setter, lua_attr.field]; + let primary_count = primary.iter().filter(|&&x| x).count(); + if primary_count > 1 { + return syn::Error::new( + lua_attr.span(), + "at most one of `getter`, `setter`, `field` can be specified", + ) + .to_compile_error() + .into(); + } + if lua_attr.meta && primary_count == 1 && !lua_attr.field { + return syn::Error::new(lua_attr.span(), "`meta` can only be combined with `field`") + .to_compile_error() + .into(); + } + + let fn_name = &method.sig.ident; + let info = try_compile!(analyze_self_and_args(&method.sig)); + let is_async = method.sig.asyncness.is_some(); + + // Owned `Lua` is only available to async callbacks. + if !is_async && matches!(info.lua, Some(LuaArg::Owned)) { + return syn::Error::new_spanned( + &method.sig, + "owned `Lua` parameter is only supported for `async` methods (use `&Lua` instead)", + ) + .to_compile_error() + .into(); + } + + if lua_attr.getter { + if is_async { + return syn::Error::new_spanned(&method.sig, "async field getter is not supported") + .to_compile_error() + .into(); + } + if !matches!(info.self_kind, SelfKind::Ref(RefKind::Ref)) { + return syn::Error::new_spanned(&method.sig, "field getter must take `&self`") + .to_compile_error() + .into(); + } + if !info.args.is_empty() { + return syn::Error::new_spanned( + &method.sig, + "field getter must not take additional arguments", + ) + .to_compile_error() + .into(); + } + let tokens = gen_field_getter(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + continue; + } + if lua_attr.setter { + if is_async { + return syn::Error::new_spanned(&method.sig, "async field setter is not supported") + .to_compile_error() + .into(); + } + if !matches!(info.self_kind, SelfKind::Ref(_)) { + return syn::Error::new_spanned(&method.sig, "field setter must take `&[mut] self`") + .to_compile_error() + .into(); + } + if info.args.len() != 1 { + return syn::Error::new_spanned( + &method.sig, + "field setter must take exactly one value argument", + ) + .to_compile_error() + .into(); + } + let tokens = gen_field_setter(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + continue; + } + if lua_attr.field { + if is_async { + return syn::Error::new_spanned(&method.sig, "async field function is not supported") + .to_compile_error() + .into(); + } + if !matches!(info.self_kind, SelfKind::None) { + return syn::Error::new_spanned(&method.sig, "field function must not take `self`") + .to_compile_error() + .into(); + } + if !info.args.is_empty() { + return syn::Error::new_spanned( + &method.sig, + "field function must not take arguments", + ) + .to_compile_error() + .into(); + } + let lua_name = lua_attr.name(fn_name); + let tokens = if lua_attr.meta { + quote! { + registry.add_meta_field(#lua_name, #type_path::#fn_name()); + } + } else { + quote! { + registry.add_field(#lua_name, #type_path::#fn_name()); + } + }; + registration_calls.push(with_cfg(tokens, &method.attrs)); + continue; + } + + if lua_attr.meta { + if matches!(info.self_kind, SelfKind::Owned) { + return syn::Error::new_spanned( + &method.sig, + "meta methods cannot take `self`, use `&[mut] self` instead", + ) + .to_compile_error() + .into(); + } + if is_async { + let tokens = gen_async_meta(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + } else { + let tokens = gen_meta(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + } + continue; + } + + if is_async { + let tokens = gen_async_regular_method(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + } else { + let tokens = gen_regular_method(type_path, fn_name, &lua_attr, &info); + registration_calls.push(with_cfg(tokens, &method.attrs)); + } + } + _ => {} + } + } + + for item in &mut input.items { + match item { + ImplItem::Const(c) => c.attrs = strip_item_attrs(&c.attrs), + ImplItem::Fn(m) => m.attrs = strip_item_attrs(&m.attrs), + _ => {} + } + } + input.attrs = strip_item_attrs(&input.attrs); + + let output = quote! { + #[allow(non_snake_case)] + fn #register_fn_name(registry: &mut ::mlua::userdata::UserDataRegistry<#type_path>) { + use ::mlua::userdata::{UserDataFields as _, UserDataMethods as _}; + #(#registration_calls)* + } + + ::mlua::__inventory::submit! { + #registration_type_name { register: #register_fn_name } + } + + #input + }; + + output.into() +} + +/// Generate the closure argument destructuring pattern. +fn gen_closure_destructure(info: &MethodInfo) -> TokenStream2 { + if info.args.is_empty() { + return quote! { () }; + } + let idents: Vec<_> = (info.args) + .iter() + .map(|a| { + let ident = &a.ident; + if matches!(a.userdata_ref, Some(RefKind::Mut | RefKind::OptionMut)) { + quote! { mut #ident } + } else { + quote! { #ident } + } + }) + .collect(); + let types: Vec<_> = info.args.iter().map(|a| &a.callback_type).collect(); + quote! { (#(#idents),*): (#(#types),*) } +} + +/// Generate the call-site expression for a single argument. +fn gen_arg_token(arg: &ArgInfo) -> TokenStream2 { + let ident = &arg.ident; + match arg.userdata_ref { + Some(RefKind::Ref) => quote! { &*#ident }, + Some(RefKind::Mut) => quote! { &mut *#ident }, + Some(RefKind::OptionRef) => quote! { #ident.as_ref().map(|r| &**r) }, + Some(RefKind::OptionMut) => quote! { #ident.as_mut().map(|r| &mut **r) }, + None => quote! { #ident }, + } +} + +/// Generate call arguments for invoking the original method. +fn gen_call_args(info: &MethodInfo) -> TokenStream2 { + let mut call_args: Vec = Vec::new(); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); + + match info.self_kind { + SelfKind::None => {} + _ => call_args.push(quote! { #this }), + } + + if info.lua.is_some() { + call_args.push(quote! { #lua }); + } + + for arg in &info.args { + call_args.push(gen_arg_token(arg)); + } + + quote! { #(#call_args),* } +} + +/// Generate call arguments for invoking the original async method. +fn gen_async_call_args(info: &MethodInfo) -> TokenStream2 { + let mut call_args: Vec = Vec::new(); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); + + match info.self_kind { + SelfKind::None => {} + SelfKind::Ref(RefKind::Mut | RefKind::OptionMut) => call_args.push(quote! { &mut #this }), + SelfKind::Ref(_) => call_args.push(quote! { &#this }), + SelfKind::Owned => call_args.push(quote! { #this }), + } + + match info.lua { + Some(LuaArg::Ref) => call_args.push(quote! { &#lua }), + Some(LuaArg::Owned) => call_args.push(quote! { #lua }), + None => {} + } + + for arg in &info.args { + call_args.push(gen_arg_token(arg)); + } + + quote! { #(#call_args),* } +} + +/// Generate the closure params for the registration callback. +fn gen_closure_params(info: &MethodInfo) -> TokenStream2 { + let destructure = gen_closure_destructure(info); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); + + match info.self_kind { + SelfKind::None => quote! { |#lua, #destructure| }, + _ => quote! { |#lua, #this, #destructure| }, + } +} + +/// Generate the closure params for an async registration callback. +fn gen_async_closure_params(info: &MethodInfo) -> TokenStream2 { + let destructure = gen_closure_destructure(info); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); + + match info.self_kind { + SelfKind::None => quote! { |#lua, #destructure| }, + SelfKind::Ref(RefKind::Mut) => quote! { |#lua, mut #this, #destructure| }, + _ => quote! { |#lua, #this, #destructure| }, + } +} + +fn gen_field_getter( + type_path: &syn::Path, + fn_name: &Ident, + lua_attr: &LuaAttr, + info: &MethodInfo, +) -> TokenStream2 { + let lua_name = lua_attr.name(fn_name); + let call_args = gen_call_args(info); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); + + if lua_attr.infallible { + return quote! { + registry.add_field_method_get(#lua_name, |#lua, #this| { + let _ = #lua; // silence unused variable warning + Ok(#type_path::#fn_name(#call_args)) + }); + }; + } + + quote! { + registry.add_field_method_get(#lua_name, |#lua, #this| { + let _ = #lua; // silence unused variable warning + #type_path::#fn_name(#call_args) + }); + } +} + +fn gen_field_setter( + type_path: &syn::Path, + fn_name: &Ident, + lua_attr: &LuaAttr, + info: &MethodInfo, +) -> TokenStream2 { + let lua_name = lua_attr.name(fn_name); + let call_args = gen_call_args(info); + let this = Ident::new("this", Span2::mixed_site()); + let lua = Ident::new("lua", Span2::mixed_site()); + + if lua_attr.infallible { + let val_ident = info.args.first().map(|a| &a.ident); + return quote! { + registry.add_field_method_set(#lua_name, |#lua, #this, #val_ident| { + let _ = #lua; // silence unused variable warning + Ok(#type_path::#fn_name(#call_args)) + }); + }; + } + + let val_ident = info.args.first().map(|a| &a.ident); + quote! { + registry.add_field_method_set(#lua_name, |#lua, #this, #val_ident| { + let _ = #lua; // silence unused variable warning + #type_path::#fn_name(#call_args) + }); + } +} + +fn gen_meta(type_path: &syn::Path, fn_name: &Ident, lua_attr: &LuaAttr, info: &MethodInfo) -> TokenStream2 { + let meta_name = match lua_attr.effective_meta_name(fn_name) { + Ok(name) => name, + Err(err) => return err.to_compile_error(), + }; + let closure_params = gen_closure_params(info); + let call_args = gen_call_args(info); + let fn_path = quote! { #type_path::#fn_name }; + + let body = if lua_attr.infallible { + quote! { Ok(#fn_path(#call_args)) } + } else { + quote! { #fn_path(#call_args) } + }; + match info.self_kind { + SelfKind::None => quote! { + registry.add_meta_function(#meta_name, #closure_params { #body }); + }, + SelfKind::Ref(RefKind::Mut) => quote! { + registry.add_meta_method_mut(#meta_name, #closure_params { #body }); + }, + _ => quote! { + registry.add_meta_method(#meta_name, #closure_params { #body }); + }, + } +} + +fn gen_regular_method( + type_path: &syn::Path, + fn_name: &Ident, + lua_attr: &LuaAttr, + info: &MethodInfo, +) -> TokenStream2 { + let fn_path = quote! { #type_path::#fn_name }; + let closure_params = gen_closure_params(info); + let call_args = gen_call_args(info); + let lua_name = lua_attr.name(fn_name); + + let body = if lua_attr.infallible { + quote! { Ok(#fn_path(#call_args)) } + } else { + quote! { #fn_path(#call_args) } + }; + match info.self_kind { + SelfKind::Ref(RefKind::Mut | RefKind::OptionMut) => quote! { + registry.add_method_mut(#lua_name, #closure_params { #body }); + }, + SelfKind::Ref(_) => quote! { + registry.add_method(#lua_name, #closure_params { #body }); + }, + SelfKind::Owned => quote! { + registry.add_method_once(#lua_name, #closure_params { #body }); + }, + SelfKind::None => quote! { + registry.add_function(#lua_name, #closure_params { #body }); + }, + } +} + +fn gen_async_regular_method( + type_path: &syn::Path, + fn_name: &Ident, + lua_attr: &LuaAttr, + info: &MethodInfo, +) -> TokenStream2 { + let fn_path = quote! { #type_path::#fn_name }; + let closure_params = gen_async_closure_params(info); + let call_args = gen_async_call_args(info); + let lua_name = lua_attr.name(fn_name); + + let body = if lua_attr.infallible { + quote! { async move { Ok(#fn_path(#call_args).await) } } + } else { + quote! { async move { #fn_path(#call_args).await } } + }; + match info.self_kind { + SelfKind::Ref(RefKind::Mut | RefKind::OptionMut) => quote! { + registry.add_async_method_mut(#lua_name, #closure_params #body); + }, + SelfKind::Ref(_) => quote! { + registry.add_async_method(#lua_name, #closure_params #body); + }, + SelfKind::Owned => quote! { + registry.add_async_method_once(#lua_name, #closure_params #body); + }, + SelfKind::None => quote! { + registry.add_async_function(#lua_name, #closure_params #body); + }, + } +} + +fn gen_async_meta( + type_path: &syn::Path, + fn_name: &Ident, + lua_attr: &LuaAttr, + info: &MethodInfo, +) -> TokenStream2 { + let meta_name = match lua_attr.effective_meta_name(fn_name) { + Ok(name) => name, + Err(err) => return err.to_compile_error(), + }; + let closure_params = gen_async_closure_params(info); + let call_args = gen_async_call_args(info); + let fn_path = quote! { #type_path::#fn_name }; + + let body = if lua_attr.infallible { + quote! { async move { Ok(#fn_path(#call_args).await) } } + } else { + quote! { async move { #fn_path(#call_args).await } } + }; + match info.self_kind { + SelfKind::None => quote! { + registry.add_async_meta_function(#meta_name, #closure_params #body); + }, + SelfKind::Ref(RefKind::Mut) => quote! { + registry.add_async_meta_method_mut(#meta_name, #closure_params #body); + }, + _ => quote! { + registry.add_async_meta_method(#meta_name, #closure_params #body); + }, + } +} diff --git a/src/buffer.rs b/src/buffer.rs index 070391fe..d256c626 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -37,6 +37,10 @@ impl Buffer { /// Reads given number of bytes from the buffer at the given offset. /// /// Offset is 0-based. + /// + /// # Panics + /// + /// Panics if `offset + N` is greater than the buffer length. #[track_caller] pub fn read_bytes(&self, offset: usize) -> [u8; N] { let lua = self.0.lua.lock(); @@ -49,6 +53,10 @@ impl Buffer { /// Writes given bytes to the buffer at the given offset. /// /// Offset is 0-based. + /// + /// # Panics + /// + /// Panics if `offset + bytes.len()` is greater than the buffer length. #[track_caller] pub fn write_bytes(&self, offset: usize, bytes: &[u8]) { let lua = self.0.lua.lock(); @@ -59,7 +67,7 @@ impl Buffer { /// 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. + /// Buffer operations are infallible, none of the read/write functions will return an Err. pub fn cursor(self) -> impl io::Read + io::Write + io::Seek { BufferCursor(self, 0) } diff --git a/src/chunk.rs b/src/chunk.rs index 3aedfb43..ddc41a54 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -1,3 +1,10 @@ +//! Lua chunk loading and execution. +//! +//! This module provides types for loading Lua source code or bytecode into a [`Chunk`], +//! configuring how it is compiled and executed, and converting it into a callable [`Function`]. +//! +//! Chunks can be loaded from strings, byte slices, or files via the [`AsChunk`] trait. + use std::borrow::Cow; use std::collections::HashMap; use std::ffi::CString; @@ -153,6 +160,7 @@ pub enum ChunkMode { /// Represents a constant value that can be used by Luau compiler. #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] +#[non_exhaustive] #[derive(Clone, Debug)] pub enum CompileConstant { Nil, @@ -393,14 +401,11 @@ impl Compiler { use std::os::raw::{c_char, c_int}; use std::ptr; - let vector_lib = self.vector_lib.clone(); - let vector_lib = vector_lib.and_then(|lib| CString::new(lib).ok()); + let vector_lib = (self.vector_lib.as_deref()).and_then(|lib| CString::new(lib).ok()); let vector_lib = vector_lib.as_ref(); - let vector_ctor = self.vector_ctor.clone(); - let vector_ctor = vector_ctor.and_then(|ctor| CString::new(ctor).ok()); + let vector_ctor = (self.vector_ctor.as_deref()).and_then(|ctor| CString::new(ctor).ok()); let vector_ctor = vector_ctor.as_ref(); - let vector_type = self.vector_type.clone(); - let vector_type = vector_type.and_then(|t| CString::new(t).ok()); + let vector_type = (self.vector_type.as_deref()).and_then(|t| CString::new(t).ok()); let vector_type = vector_type.as_ref(); macro_rules! vec2cstring_ptr { @@ -732,11 +737,7 @@ impl Chunk<'_> { .unwrap_or(source); let name = Self::convert_name(self.name.clone())?; - let env = match &self.env { - Ok(Some(env)) => Some(env), - Ok(None) => None, - Err(err) => return Err(err.clone()), - }; + let env = self.env.as_ref().map_err(Error::clone)?.as_ref(); self.lua.lock().load_chunk(Some(&name), env, None, &source) } @@ -750,7 +751,7 @@ impl Chunk<'_> { return ChunkMode::Binary; } #[cfg(feature = "luau")] - if *source.first().unwrap_or(&u8::MAX) < b'\n' { + if unsafe { ffi::luaL_isbytecode(source.as_ptr().cast(), source.len()) } { return ChunkMode::Binary; } } @@ -779,7 +780,6 @@ impl Chunk<'_> { /// /// The resulted `IntoLua` implementation will convert the chunk into a Lua function without /// executing it. - #[doc(hidden)] #[track_caller] pub fn wrap(chunk: impl AsChunk) -> impl IntoLua { WrappedChunk { diff --git a/src/conversion.rs b/src/conversion.rs index b74343db..70c34829 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -4,7 +4,7 @@ use std::ffi::{CStr, CString, OsStr, OsString}; use std::hash::{BuildHasher, Hash}; use std::os::raw::c_int; use std::path::{Path, PathBuf}; -use std::{mem, slice, str}; +use std::{slice, str}; use bstr::{BStr, BString, ByteVec}; use num_traits::cast; @@ -16,7 +16,7 @@ use crate::string::{BorrowedBytes, BorrowedStr, LuaString}; use crate::table::Table; use crate::thread::Thread; use crate::traits::{FromLua, IntoLua, ShortTypeName as _}; -use crate::types::{Either, LightUserData, MaybeSend, RegistryKey}; +use crate::types::{Either, LightUserData, MaybeSend, MaybeSync, RegistryKey}; use crate::userdata::{AnyUserData, UserData}; use crate::value::{Nil, Value}; @@ -86,91 +86,79 @@ impl FromLua for LuaString { } } -impl IntoLua for BorrowedStr<'_> { +impl IntoLua for BorrowedStr { #[inline] fn into_lua(self, _: &Lua) -> Result { - Ok(Value::String(self.borrow.into_owned())) + Ok(Value::String(LuaString(self.vref))) } #[inline] unsafe fn push_into_stack(self, lua: &RawLua) -> Result<()> { - lua.push_ref(&self.borrow.0); + lua.push_ref(&self.vref); Ok(()) } } -impl IntoLua for &BorrowedStr<'_> { +impl IntoLua for &BorrowedStr { #[inline] fn into_lua(self, _: &Lua) -> Result { - Ok(Value::String(self.borrow.clone().into_owned())) + Ok(Value::String(LuaString(self.vref.clone()))) } #[inline] unsafe fn push_into_stack(self, lua: &RawLua) -> Result<()> { - lua.push_ref(&self.borrow.0); + lua.push_ref(&self.vref); Ok(()) } } -impl FromLua for BorrowedStr<'_> { +impl FromLua for BorrowedStr { fn from_lua(value: Value, lua: &Lua) -> Result { let s = LuaString::from_lua(value, lua)?; - let BorrowedStr { buf, _lua, .. } = BorrowedStr::try_from(&s)?; - let buf = unsafe { mem::transmute::<&str, &'static str>(buf) }; - let borrow = Cow::Owned(s); - Ok(Self { buf, borrow, _lua }) + BorrowedStr::try_from(&s) } unsafe fn from_stack(idx: c_int, lua: &RawLua) -> Result { let s = LuaString::from_stack(idx, lua)?; - let BorrowedStr { buf, _lua, .. } = BorrowedStr::try_from(&s)?; - let buf = unsafe { mem::transmute::<&str, &'static str>(buf) }; - let borrow = Cow::Owned(s); - Ok(Self { buf, borrow, _lua }) + BorrowedStr::try_from(&s) } } -impl IntoLua for BorrowedBytes<'_> { +impl IntoLua for BorrowedBytes { #[inline] fn into_lua(self, _: &Lua) -> Result { - Ok(Value::String(self.borrow.into_owned())) + Ok(Value::String(LuaString(self.vref))) } #[inline] unsafe fn push_into_stack(self, lua: &RawLua) -> Result<()> { - lua.push_ref(&self.borrow.0); + lua.push_ref(&self.vref); Ok(()) } } -impl IntoLua for &BorrowedBytes<'_> { +impl IntoLua for &BorrowedBytes { #[inline] fn into_lua(self, _: &Lua) -> Result { - Ok(Value::String(self.borrow.clone().into_owned())) + Ok(Value::String(LuaString(self.vref.clone()))) } #[inline] unsafe fn push_into_stack(self, lua: &RawLua) -> Result<()> { - lua.push_ref(&self.borrow.0); + lua.push_ref(&self.vref); Ok(()) } } -impl FromLua for BorrowedBytes<'_> { +impl FromLua for BorrowedBytes { fn from_lua(value: Value, lua: &Lua) -> Result { let s = LuaString::from_lua(value, lua)?; - let BorrowedBytes { buf, _lua, .. } = BorrowedBytes::from(&s); - let buf = unsafe { mem::transmute::<&[u8], &'static [u8]>(buf) }; - let borrow = Cow::Owned(s); - Ok(Self { buf, borrow, _lua }) + Ok(BorrowedBytes::from(&s)) } unsafe fn from_stack(idx: c_int, lua: &RawLua) -> Result { let s = LuaString::from_stack(idx, lua)?; - let BorrowedBytes { buf, _lua, .. } = BorrowedBytes::from(&s); - let buf = unsafe { mem::transmute::<&[u8], &'static [u8]>(buf) }; - let borrow = Cow::Owned(s); - Ok(Self { buf, borrow, _lua }) + Ok(BorrowedBytes::from(&s)) } } @@ -294,7 +282,7 @@ impl FromLua for AnyUserData { } } -impl IntoLua for T { +impl IntoLua for T { #[inline] fn into_lua(self, lua: &Lua) -> Result { Ok(Value::UserData(lua.create_userdata(self)?)) @@ -535,7 +523,10 @@ impl IntoLua for &str { impl IntoLua for Cow<'_, str> { #[inline] fn into_lua(self, lua: &Lua) -> Result { - Ok(Value::String(lua.create_string(self.as_bytes())?)) + match self { + Cow::Borrowed(s) => s.into_lua(lua), + Cow::Owned(s) => s.into_lua(lua), + } } } @@ -597,7 +588,10 @@ impl IntoLua for &CStr { impl IntoLua for Cow<'_, CStr> { #[inline] fn into_lua(self, lua: &Lua) -> Result { - Ok(Value::String(lua.create_string(self.to_bytes())?)) + match self { + Cow::Borrowed(s) => s.into_lua(lua), + Cow::Owned(s) => s.into_lua(lua), + } } } @@ -686,6 +680,8 @@ impl IntoLua for &OsStr { Ok(Value::String(lua.create_string(self.as_bytes())?)) } + // On non-Unix platforms `OsStr` is not guaranteed to be valid Unicode, invalid sequences are + // replaced with `U+FFFD` rather than erroring. #[cfg(not(unix))] #[inline] fn into_lua(self, lua: &Lua) -> Result { @@ -818,6 +814,13 @@ macro_rules! lua_convert_int { }); } } + #[cfg(feature = "luau")] + if type_id == ffi::LUA_TINTEGER { + let i = ffi::lua_tointeger64(state, idx, std::ptr::null_mut()); + return cast(i).ok_or_else(|| { + Error::from_lua_conversion("integer", stringify!($x), "out of range".to_string()) + }); + } // Fallback to default Self::from_lua(lua.stack_value(idx, Some(type_id)), lua.lua()) } @@ -901,17 +904,21 @@ where fn from_lua(value: Value, _lua: &Lua) -> Result { match value { #[cfg(feature = "luau")] - #[rustfmt::skip] - Value::Vector(v) if N == crate::Vector::SIZE => unsafe { - use std::{mem, ptr}; - let mut arr: [mem::MaybeUninit; N] = mem::MaybeUninit::uninit().assume_init(); - ptr::write(arr[0].as_mut_ptr() , T::from_lua(Value::Number(v.x() as _), _lua)?); - ptr::write(arr[1].as_mut_ptr(), T::from_lua(Value::Number(v.y() as _), _lua)?); - ptr::write(arr[2].as_mut_ptr(), T::from_lua(Value::Number(v.z() as _), _lua)?); + Value::Vector(v) if N == crate::Vector::SIZE => { + use std::mem::MaybeUninit; + let x = T::from_lua(Value::Number(v.x() as _), _lua)?; + let y = T::from_lua(Value::Number(v.y() as _), _lua)?; + let z = T::from_lua(Value::Number(v.z() as _), _lua)?; #[cfg(feature = "luau-vector4")] - ptr::write(arr[3].as_mut_ptr(), T::from_lua(Value::Number(v.w() as _), _lua)?); - Ok(mem::transmute_copy(&arr)) - }, + let w = T::from_lua(Value::Number(v.w() as _), _lua)?; + let mut arr: [MaybeUninit; N] = [const { MaybeUninit::uninit() }; N]; + arr[0].write(x); + arr[1].write(y); + arr[2].write(z); + #[cfg(feature = "luau-vector4")] + arr[3].write(w); + Ok(arr.map(|e| unsafe { e.assume_init() })) + } Value::Table(table) => { let vec = table.sequence_values().collect::>>()?; vec.try_into().map_err(|vec: Vec| { @@ -1112,36 +1119,23 @@ impl FromLua for Either { #[inline] fn from_lua(value: Value, lua: &Lua) -> Result { let value_type_name = value.type_name(); - // Try the left type first - match L::from_lua(value.clone(), lua) { - Ok(l) => Ok(Either::Left(l)), - // Try the right type - Err(_) => match R::from_lua(value, lua).map(Either::Right) { - Ok(r) => Ok(r), - Err(_) => Err(Error::from_lua_conversion( - value_type_name, - Self::type_name(), - None, - )), - }, - } + L::from_lua(value.clone(), lua) + .map(Either::Left) + .or_else(|_| R::from_lua(value, lua).map(Either::Right)) + .map_err(|_| Error::from_lua_conversion(value_type_name, Self::type_name(), None)) } #[inline] unsafe fn from_stack(idx: c_int, lua: &RawLua) -> Result { - match L::from_stack(idx, lua) { - Ok(l) => Ok(Either::Left(l)), - Err(_) => match R::from_stack(idx, lua).map(Either::Right) { - Ok(r) => Ok(r), - Err(_) => { - let state = lua.state(); - let from_type_name = CStr::from_ptr(ffi::lua_typename(state, ffi::lua_type(state, idx))) - .to_str() - .unwrap_or("unknown"); - let err = Error::from_lua_conversion(from_type_name, Self::type_name(), None); - Err(err) - } - }, - } + L::from_stack(idx, lua) + .map(Either::Left) + .or_else(|_| R::from_stack(idx, lua).map(Either::Right)) + .map_err(|_| { + let state = lua.state(); + let from_type_name = CStr::from_ptr(ffi::lua_typename(state, ffi::lua_type(state, idx))) + .to_str() + .unwrap_or("unknown"); + Error::from_lua_conversion(from_type_name, Self::type_name(), None) + }) } } diff --git a/src/debug.rs b/src/debug.rs index a8527e7b..964d4528 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,8 +1,11 @@ //! Lua debugging interface. //! //! This module provides access to the Lua debug interface, allowing inspection of the call stack, -//! and function information. The main types are [`Debug`] for accessing debug information and -//! [`HookTriggers`] for configuring debug hooks. +//! and function information. The main type is [`struct@Debug`] for accessing debug information. +#![cfg_attr( + not(feature = "luau"), + doc = "\nDebug hooks are configured with [`HookTriggers`]." +)] use std::borrow::Cow; use std::os::raw::c_int; @@ -99,10 +102,7 @@ impl<'a> Debug<'a> { DebugNames { name: ptr_to_lossy_str((*self.ar).name), #[cfg(not(feature = "luau"))] - name_what: match ptr_to_str((*self.ar).namewhat) { - Some("") => None, - val => val, - }, + name_what: ptr_to_str((*self.ar).namewhat).filter(|s| !s.is_empty()), #[cfg(feature = "luau")] name_what: None, } @@ -312,6 +312,7 @@ impl HookTriggers { /// Returns an instance of `HookTriggers` with [`on_calls`] trigger set. /// /// [`on_calls`]: #structfield.on_calls + #[must_use] pub const fn on_calls(mut self) -> Self { self.on_calls = true; self @@ -320,6 +321,7 @@ impl HookTriggers { /// Returns an instance of `HookTriggers` with [`on_returns`] trigger set. /// /// [`on_returns`]: #structfield.on_returns + #[must_use] pub const fn on_returns(mut self) -> Self { self.on_returns = true; self @@ -328,6 +330,7 @@ impl HookTriggers { /// Returns an instance of `HookTriggers` with [`every_line`] trigger set. /// /// [`every_line`]: #structfield.every_line + #[must_use] pub const fn every_line(mut self) -> Self { self.every_line = true; self @@ -336,6 +339,7 @@ impl HookTriggers { /// Returns an instance of `HookTriggers` with [`every_nth_instruction`] trigger set. /// /// [`every_nth_instruction`]: #structfield.every_nth_instruction + #[must_use] pub const fn every_nth_instruction(mut self, n: u32) -> Self { self.every_nth_instruction = Some(n); self @@ -379,9 +383,7 @@ impl std::ops::BitOr for HookTriggers { self.on_calls |= rhs.on_calls; self.on_returns |= rhs.on_returns; self.every_line |= rhs.every_line; - if self.every_nth_instruction.is_none() && rhs.every_nth_instruction.is_some() { - self.every_nth_instruction = rhs.every_nth_instruction; - } + self.every_nth_instruction = self.every_nth_instruction.or(rhs.every_nth_instruction); self } } diff --git a/src/error.rs b/src/error.rs index c5b5e150..0e1a13b8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,8 @@ +//! Lua error handling. +//! +//! This module provides the [`Error`] type returned by all fallible `mlua` operations, together +//! with extension traits for adapting Rust errors for use within Lua. + use std::error::Error as StdError; use std::fmt; use std::io::Error as IoError; @@ -285,7 +290,7 @@ impl fmt::Display for Error { // Try to find local traceback within the full traceback if let Some(pos) = full_traceback.find(traceback) { write!(fmt, "{}", &full_traceback[..pos])?; - writeln!(fmt, ">{}", &full_traceback[pos..].trim_end())?; + writeln!(fmt, ">{}", full_traceback[pos..].trim_end())?; } else { writeln!(fmt, "{}", full_traceback.trim_end())?; } @@ -340,22 +345,35 @@ impl Error { /// Wraps an external error object. #[inline] pub fn external>>(err: T) -> Self { - Error::ExternalError(err.into().into()) + let boxed = err.into(); + match boxed.downcast::() { + Ok(err) => *err, + Err(boxed) => Error::ExternalError(boxed.into()), + } } - /// Attempts to downcast the external error object to a concrete type by reference. + /// Attempts to downcast to a concrete external error type by reference. + /// + /// The search descends through wrapping layers following the same path as [`Error::chain`]. pub fn downcast_ref(&self) -> Option<&T> where T: StdError + 'static, { match self { Error::ExternalError(err) => err.downcast_ref(), - Error::WithContext { cause, .. } => Self::downcast_ref(cause), + Error::BadArgument { cause, .. } + | Error::CallbackError { cause, .. } + | Error::WithContext { cause, .. } => Self::downcast_ref(cause), _ => None, } } - /// An iterator over the chain of nested errors wrapped by this Error. + /// An iterator over the chain of nested errors wrapped by this `Error`. + /// + /// Iteration starts with `self` and descends through wrapping layers. + /// A bare [`Error::ExternalError`] wrapper is skipped in favor of the error it wraps. + /// The chain stops at the innermost error and does not follow the external error's own + /// [`StdError::source`] chain. pub fn chain(&self) -> impl Iterator { Chain { root: self, @@ -550,10 +568,8 @@ impl<'a> Iterator for Chain<'a> { #[cfg(test)] mod assertions { - use super::*; - #[cfg(not(feature = "error-send"))] - static_assertions::assert_not_impl_any!(Error: Send, Sync); + static_assertions::assert_not_impl_any!(super::Error: Send, Sync); #[cfg(feature = "send")] - static_assertions::assert_impl_all!(Error: Send, Sync); + static_assertions::assert_impl_all!(super::Error: Send, Sync); } diff --git a/src/function.rs b/src/function.rs index 9d1d7c9d..8081ec9f 100644 --- a/src/function.rs +++ b/src/function.rs @@ -3,12 +3,6 @@ //! This module provides types for working with Lua functions from Rust, including //! both Lua-defined functions and native Rust callbacks. //! -//! # Main Types -//! -//! - [`Function`] - A handle to a Lua function that can be called from Rust. -//! - [`FunctionInfo`] - Debug information about a function (name, source, line numbers, etc.). -//! - [`CoverageInfo`] - Code coverage data for Luau functions (requires `luau` feature). -//! //! # Calling Functions //! //! Use [`Function::call`] to invoke a Lua function synchronously: @@ -81,12 +75,13 @@ use std::cell::RefCell; use std::os::raw::{c_int, c_void}; +use std::result::Result as StdResult; use std::{mem, ptr, slice}; -use crate::error::{Error, Result}; +use crate::error::{Error, ExternalError, ExternalResult, Result}; use crate::state::Lua; use crate::table::Table; -use crate::traits::{FromLuaMulti, IntoLua, IntoLuaMulti, LuaNativeFn, LuaNativeFnMut}; +use crate::traits::{FromLuaMulti, IntoLua, IntoLuaMulti}; use crate::types::{Callback, LuaType, MaybeSend, ValueRef}; use crate::util::{ StackGuard, assert_stack, check_stack, linenumber_to_usize, pop_error, ptr_to_lossy_str, ptr_to_str, @@ -96,7 +91,6 @@ use crate::value::Value; #[cfg(feature = "async")] use { crate::thread::AsyncThread, - crate::traits::LuaNativeAsyncFn, crate::types::AsyncCallback, std::future::{self, Future}, std::pin::{Pin, pin}, @@ -246,7 +240,7 @@ impl Function { /// # } /// ``` /// - /// [`AsyncThread`]: crate::AsyncThread + /// [`AsyncThread`]: crate::thread::AsyncThread #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] pub fn call_async(&self, args: impl IntoLuaMulti) -> AsyncCallFuture @@ -258,6 +252,7 @@ impl Function { lua.create_recycled_thread(self).and_then(|th| { let mut th = th.into_async(args)?; th.set_recyclable(true); + lua.update_thread_ownership(th.thread(), Some(lua.state())); Ok(th) }) }) @@ -462,10 +457,7 @@ impl Function { FunctionInfo { name: ptr_to_lossy_str(ar.name).map(|s| s.into_owned()), #[cfg(not(feature = "luau"))] - name_what: match ptr_to_str(ar.namewhat) { - Some("") => None, - val => val, - }, + name_what: ptr_to_str(ar.namewhat).filter(|s| !s.is_empty()), #[cfg(feature = "luau")] name_what: None, what: ptr_to_str(ar.what).unwrap_or("main"), @@ -547,22 +539,15 @@ impl Function { where F: FnMut(CoverageInfo), { - use std::ffi::CStr; - use std::os::raw::c_char; - unsafe extern "C-unwind" fn callback( data: *mut c_void, - function: *const c_char, + function: *const std::os::raw::c_char, line_defined: c_int, depth: c_int, hits: *const c_int, size: usize, ) { - let function = if !function.is_null() { - Some(CStr::from_ptr(function).to_string_lossy().to_string()) - } else { - None - }; + let function = ptr_to_lossy_str(function).map(|s| s.into_owned()); let rust_callback = &*(data as *const RefCell); if let Ok(mut rust_callback) = rust_callback.try_borrow_mut() { // Call the Rust callback with CoverageInfo @@ -633,33 +618,35 @@ struct WrappedFunction(pub(crate) Callback); struct WrappedAsyncFunction(pub(crate) AsyncCallback); impl Function { - /// Wraps a Rust function or closure, returning an opaque type that implements [`IntoLua`] + /// Wraps a Rust function or closure, returning an opaque type that implements the [`IntoLua`] /// trait. #[inline] - pub fn wrap(func: F) -> impl IntoLua + pub fn wrap(func: F) -> impl IntoLua where - F: LuaNativeFn> + MaybeSend + 'static, + F: LuaNativeFn> + MaybeSend + 'static, A: FromLuaMulti, R: IntoLuaMulti, + E: ExternalError, { WrappedFunction(Box::new(move |lua, nargs| unsafe { let args = A::from_stack_args(nargs, 1, None, lua)?; - func.call(args)?.push_into_stack_multi(lua) + func.call(args).into_lua_err()?.push_into_stack_multi(lua) })) } /// Wraps a Rust mutable closure, returning an opaque type that implements [`IntoLua`] trait. - pub fn wrap_mut(func: F) -> impl IntoLua + pub fn wrap_mut(func: F) -> impl IntoLua where - F: LuaNativeFnMut> + MaybeSend + 'static, + F: LuaNativeFnMut> + MaybeSend + 'static, A: FromLuaMulti, R: IntoLuaMulti, + E: ExternalError, { let func = RefCell::new(func); WrappedFunction(Box::new(move |lua, nargs| unsafe { let mut func = func.try_borrow_mut().map_err(|_| Error::RecursiveMutCallback)?; let args = A::from_stack_args(nargs, 1, None, lua)?; - func.call(args)?.push_into_stack_multi(lua) + func.call(args).into_lua_err()?.push_into_stack_multi(lua) })) } @@ -672,6 +659,7 @@ impl Function { pub fn wrap_raw(func: F) -> impl IntoLua where F: LuaNativeFn + MaybeSend + 'static, + F::Output: IntoLuaMulti, A: FromLuaMulti, { WrappedFunction(Box::new(move |lua, nargs| unsafe { @@ -688,6 +676,7 @@ impl Function { pub fn wrap_raw_mut(func: F) -> impl IntoLua where F: LuaNativeFnMut + MaybeSend + 'static, + F::Output: IntoLuaMulti, A: FromLuaMulti, { let func = RefCell::new(func); @@ -702,11 +691,12 @@ impl Function { /// trait. #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] - pub fn wrap_async(func: F) -> impl IntoLua + pub fn wrap_async(func: F) -> impl IntoLua where - F: LuaNativeAsyncFn> + MaybeSend + 'static, + F: LuaNativeAsyncFn> + MaybeSend + 'static, A: FromLuaMulti, R: IntoLuaMulti, + E: ExternalError, { WrappedAsyncFunction(Box::new(move |rawlua, nargs| unsafe { let args = match A::from_stack_args(nargs, 1, None, rawlua) { @@ -715,7 +705,7 @@ impl Function { }; let lua = rawlua.lua(); let fut = func.call(args); - Box::pin(async move { fut.await?.push_into_stack_multi(lua.raw_lua()) }) + Box::pin(async move { fut.await.into_lua_err()?.push_into_stack_multi(lua.raw_lua()) }) })) } @@ -729,6 +719,7 @@ impl Function { pub fn wrap_raw_async(func: F) -> impl IntoLua where F: LuaNativeAsyncFn + MaybeSend + 'static, + F::Output: IntoLuaMulti, A: FromLuaMulti, { WrappedAsyncFunction(Box::new(move |rawlua, nargs| unsafe { @@ -788,6 +779,95 @@ impl Future for AsyncCallFuture { } } +/// A trait for types that can be used as Lua functions. +pub trait LuaNativeFn { + type Output; + + fn call(&self, args: A) -> Self::Output; +} + +/// A trait for types with mutable state that can be used as Lua functions. +pub trait LuaNativeFnMut { + type Output; + + fn call(&mut self, args: A) -> Self::Output; +} + +/// A trait for types that returns a future and can be used as Lua functions. +#[cfg(feature = "async")] +#[cfg_attr(docsrs, doc(cfg(feature = "async")))] +pub trait LuaNativeAsyncFn { + type Output; + + fn call(&self, args: A) -> impl Future + MaybeSend + 'static; +} + +macro_rules! impl_lua_native_fn { + ($($A:ident),*) => { + impl LuaNativeFn<($($A,)*)> for FN + where + FN: Fn($($A,)*) -> R + MaybeSend + 'static, + ($($A,)*): FromLuaMulti, + { + type Output = R; + + #[allow(non_snake_case)] + fn call(&self, args: ($($A,)*)) -> Self::Output { + let ($($A,)*) = args; + self($($A,)*) + } + } + + impl LuaNativeFnMut<($($A,)*)> for FN + where + FN: FnMut($($A,)*) -> R + MaybeSend + 'static, + ($($A,)*): FromLuaMulti, + { + type Output = R; + + #[allow(non_snake_case)] + fn call(&mut self, args: ($($A,)*)) -> Self::Output { + let ($($A,)*) = args; + self($($A,)*) + } + } + + #[cfg(feature = "async")] + impl LuaNativeAsyncFn<($($A,)*)> for FN + where + FN: Fn($($A,)*) -> Fut + MaybeSend + 'static, + ($($A,)*): FromLuaMulti, + Fut: Future + MaybeSend + 'static, + { + type Output = R; + + #[allow(non_snake_case)] + fn call(&self, args: ($($A,)*)) -> impl Future + MaybeSend + 'static { + let ($($A,)*) = args; + self($($A,)*) + } + } + }; +} + +impl_lua_native_fn!(); +impl_lua_native_fn!(A); +impl_lua_native_fn!(A, B); +impl_lua_native_fn!(A, B, C); +impl_lua_native_fn!(A, B, C, D); +impl_lua_native_fn!(A, B, C, D, E); +impl_lua_native_fn!(A, B, C, D, E, F); +impl_lua_native_fn!(A, B, C, D, E, F, G); +impl_lua_native_fn!(A, B, C, D, E, F, G, H); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); +impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); + #[cfg(test)] mod assertions { use super::*; diff --git a/src/lib.rs b/src/lib.rs index fa3bd1d0..f1b8acd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,102 +47,110 @@ //! //! # `Send` and `Sync` support //! -//! By default `mlua` is `!Send`. This can be changed by enabling `feature = "send"` that adds -//! `Send` requirement to Rust functions and [`UserData`] types. +//! By default `mlua` is `!Send`. This can be changed by enabling `feature = "send"` that adds a +//! `Send` requirement to Rust functions and a `Send + Sync` requirement to [`UserData`] types. +//! A `Send`-only userdata types must therefore be wrapped (e.g. in a `Mutex`) or created through a +//! [`Scope`] to be used with the `send` feature. //! //! In this case [`Lua`] object and their types can be send or used from other threads. Internally //! access to Lua VM is synchronized using a reentrant mutex that can be locked many times within //! the same thread. //! //! [Lua programming language]: https://www.lua.org/ -//! [executing]: crate::Chunk::exec -//! [evaluating]: crate::Chunk::eval +//! [executing]: crate::chunk::Chunk::exec +//! [evaluating]: crate::chunk::Chunk::eval //! [globals]: crate::Lua::globals //! [`Future`]: std::future::Future //! [`serde::Serialize`]: https://docs.serde.rs/serde/ser/trait.Serialize.html //! [`serde::Deserialize`]: https://docs.serde.rs/serde/de/trait.Deserialize.html +//! [`AsyncThread`]: crate::thread::AsyncThread // Deny warnings inside doc tests / examples. When this isn't present, rustdoc doesn't show *any* // warnings at all. #![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr(not(send), allow(clippy::arc_with_non_send_sync))] +#![cfg_attr(not(feature = "send"), allow(clippy::arc_with_non_send_sync))] #![allow(unsafe_op_in_unsafe_fn)] #[macro_use] mod macros; mod buffer; -mod chunk; mod conversion; -mod error; -#[cfg(any(feature = "luau", doc))] -mod luau; mod memory; mod multi; mod scope; -mod state; mod stdlib; -mod string; -mod thread; mod traits; mod types; -mod userdata; mod util; mod value; mod vector; +pub mod chunk; pub mod debug; +pub mod error; pub mod function; +#[cfg(any(feature = "luau", doc))] +#[cfg_attr(docsrs, doc(cfg(feature = "luau")))] +pub mod luau; pub mod prelude; +pub mod state; +pub mod string; pub mod table; +pub mod thread; +pub mod userdata; pub use bstr::BString; pub use ffi::{self, lua_CFunction, lua_State}; +#[cfg(feature = "macros")] +#[doc(hidden)] +pub use inventory as __inventory; -pub use crate::chunk::{AsChunk, Chunk, ChunkMode}; -pub use crate::error::{Error, ErrorContext, ExternalError, ExternalResult, Result}; +#[doc(inline)] +pub use crate::error::{Error, Result}; +pub use crate::error::{ErrorContext, ExternalError, ExternalResult}; +#[doc(inline)] pub use crate::function::Function; pub use crate::multi::{MultiValue, Variadic}; pub use crate::scope::Scope; -pub use crate::state::{GCMode, Lua, LuaOptions, WeakLua}; +#[doc(inline)] +pub use crate::state::{Lua, LuaOptions, WeakLua}; pub use crate::stdlib::StdLib; -pub use crate::string::{BorrowedBytes, BorrowedStr, LuaString, LuaString as String}; +#[doc(inline)] +pub use crate::string::LuaString; +pub use crate::string::{BorrowedBytes, BorrowedStr}; +#[doc(inline)] pub use crate::table::Table; -pub use crate::thread::{Thread, ThreadStatus}; -pub use crate::traits::{ - FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, LuaNativeFn, LuaNativeFnMut, ObjectLike, -}; +#[doc(inline)] +pub use crate::thread::Thread; +#[doc(inline)] +pub use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti, ObjectLike}; pub use crate::types::{ - AppDataRef, AppDataRefMut, Either, Integer, LightUserData, MaybeSend, Number, RegistryKey, VmState, + AppDataRef, AppDataRefMut, Either, Integer, LightUserData, MaybeSend, MaybeSync, Number, RegistryKey, + VmState, }; +#[doc(inline)] +pub use crate::userdata::{AnyUserData, UserData}; pub use crate::userdata::{ - AnyUserData, MetaMethod, UserData, UserDataFields, UserDataMetatable, UserDataMethods, UserDataRef, - UserDataRefMut, UserDataRegistry, + MetaMethod, UserDataFields, UserDataMethods, UserDataOwned, UserDataRef, UserDataRefMut, UserDataRegistry, }; pub use crate::value::{Nil, Value}; +/// Deprecated alias to [`LuaString`]. +#[deprecated(since = "0.12.0", note = "use `mlua::LuaString` instead")] +#[doc(hidden)] +pub type String = crate::string::LuaString; + #[cfg(not(feature = "luau"))] pub use crate::debug::HookTriggers; #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] -pub use crate::{ - buffer::Buffer, - chunk::{CompileConstant, Compiler}, - luau::{HeapDump, NavigateError, Require, TextRequirer}, - vector::Vector, -}; - -#[cfg(feature = "async")] -#[cfg_attr(docsrs, doc(cfg(feature = "async")))] -pub use crate::{thread::AsyncThread, traits::LuaNativeAsyncFn}; +pub use crate::{buffer::Buffer, vector::Vector}; #[cfg(feature = "serde")] #[doc(inline)] -pub use crate::{ - serde::{LuaSerdeExt, de::Options as DeserializeOptions, ser::Options as SerializeOptions}, - value::SerializableValue, -}; +pub use crate::{serde::LuaSerdeExt, value::SerializableValue}; #[cfg(feature = "serde")] #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] @@ -153,54 +161,7 @@ pub mod serde; #[macro_use] extern crate mlua_derive; -/// Create a type that implements [`AsChunk`] and can capture Rust variables. -/// -/// This macro allows to write Lua code directly in Rust code. -/// -/// Rust variables can be referenced from Lua using `$` prefix, as shown in the example below. -/// User's Rust types needs to implement [`UserData`] or [`IntoLua`] traits. -/// -/// Captured variables are **moved** into the chunk. -/// -/// ``` -/// use mlua::{Lua, Result, chunk}; -/// -/// fn main() -> Result<()> { -/// let lua = Lua::new(); -/// let name = "Rustacean"; -/// lua.load(chunk! { -/// print("hello, " .. $name) -/// }).exec() -/// } -/// ``` -/// -/// ## Syntax issues -/// -/// Since the Rust tokenizer will tokenize Lua code, this imposes some restrictions. -/// The main thing to remember is: -/// -/// - Use double quoted strings (`""`) instead of single quoted strings (`''`). -/// -/// (Single quoted strings only work if they contain a single character, since in Rust, -/// `'a'` is a character literal). -/// -/// - Using Lua comments `--` is not desirable in **stable** Rust and can have bad side effects. -/// -/// This is because procedural macros have Line/Column information available only in -/// **nightly** Rust. Instead, Lua chunks represented as a big single line of code in stable Rust. -/// -/// As workaround, Rust comments `//` can be used. -/// -/// Other minor limitations: -/// -/// - Certain escape codes in string literals don't work. (Specifically: `\a`, `\b`, `\f`, `\v`, -/// `\123` (octal escape codes), `\u`, and `\U`). -/// -/// These are accepted: : `\\`, `\n`, `\t`, `\r`, `\xAB` (hex escape codes), and `\0`. -/// -/// - The `//` (floor division) operator is unusable, as its start a comment. -/// -/// Everything else should work. +#[doc = include_str!("../docs/chunk.md")] #[cfg(feature = "macros")] #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::chunk; @@ -213,47 +174,19 @@ pub use mlua_derive::chunk; #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use mlua_derive::FromLua; -/// Registers Lua module entrypoint. -/// -/// You can register multiple entrypoints as required. -/// -/// ```ignore -/// use mlua::{Lua, Result, Table}; -/// -/// #[mlua::lua_module] -/// fn my_module(lua: &Lua) -> Result
{ -/// let exports = lua.create_table()?; -/// exports.set("hello", "world")?; -/// Ok(exports) -/// } -/// ``` -/// -/// Internally in the code above the compiler defines C function `luaopen_my_module`. -/// -/// You can also pass options to the attribute: -/// -/// * name - name of the module, defaults to the name of the function -/// -/// ```ignore -/// #[mlua::lua_module(name = "alt_module")] -/// fn my_module(lua: &Lua) -> Result
{ -/// ... -/// } -/// ``` -/// -/// * skip_memory_check - skip memory allocation checks for some operations. -/// -/// In module mode, mlua runs in unknown environment and cannot say are there any memory -/// limits or not. As result, some operations that require memory allocation runs in -/// protected mode. Setting this attribute will improve performance of such operations -/// with risk of having uncaught exceptions and memory leaks. +#[doc = include_str!("../docs/UserData.md")] +#[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +pub use mlua_derive::UserData; + +/// Registers items in an `impl` block as methods/fields of a [`UserData`](trait@UserData) type. /// -/// ```ignore -/// #[mlua::lua_module(skip_memory_check)] -/// fn my_module(lua: &Lua) -> Result
{ -/// ... -/// } -/// ``` +/// See the [`UserData`](derive@UserData) derive macro documentation for usage details. +#[cfg(feature = "macros")] +#[cfg_attr(docsrs, doc(cfg(feature = "macros")))] +pub use mlua_derive::userdata_impl; + +#[doc = include_str!("../docs/lua_module.md")] #[cfg(all(feature = "mlua_derive", any(feature = "module", doc)))] #[cfg_attr(docsrs, doc(cfg(feature = "module")))] pub use mlua_derive::lua_module; diff --git a/src/luau/heap_dump.rs b/src/luau/heap_dump.rs index 0189845c..ed2a57b6 100644 --- a/src/luau/heap_dump.rs +++ b/src/luau/heap_dump.rs @@ -34,12 +34,13 @@ impl HeapDump { } ffi::lua_gcdump(state, file as *mut _, Some(category_name)); libc::fseek(file, 0, libc::SEEK_END); - let len = libc::ftell(file) as usize; + let len = libc::ftell(file); libc::rewind(file); if len > 0 { + let len = len as usize; buf.reserve(len); - libc::fread(buf.as_mut_ptr() as *mut _, 1, len, file); - buf.set_len(len); + let n = libc::fread(buf.as_mut_ptr() as *mut _, 1, len, file); + buf.set_len(n); } libc::fclose(file); } @@ -64,7 +65,8 @@ impl HeapDump { /// Returns a mapping from object type to (count, total size in bytes). /// - /// If `category` is provided, only objects in that category are considered. + /// If `category` is provided, only objects in that category are considered. An unknown category + /// yields an empty map. pub fn size_by_type<'a>(&'a self, category: Option<&str>) -> HashMap<&'a str, (usize, u64)> { self.size_by_type_inner(category).unwrap_or_default() } @@ -103,6 +105,9 @@ impl HeapDump { } /// Returns a mapping from userdata type to (count, total size in bytes). + /// + /// If `category` is provided, only objects in that category are considered. An unknown category + /// yields an empty map. pub fn size_by_userdata<'a>(&'a self, category: Option<&str>) -> HashMap<&'a str, (usize, u64)> { self.size_by_userdata_inner(category).unwrap_or_default() } diff --git a/src/luau/json.rs b/src/luau/json.rs index ce17a20e..b8892aec 100644 --- a/src/luau/json.rs +++ b/src/luau/json.rs @@ -52,8 +52,7 @@ impl<'a> Json<'a> { } pub(crate) fn as_u64(&self) -> Option { - self.as_i64() - .and_then(|i| if i >= 0 { Some(i as u64) } else { None }) + self.as_i64().and_then(|i| u64::try_from(i).ok()) } pub(crate) fn as_array(&self) -> Option<&[Json<'a>]> { @@ -74,8 +73,7 @@ impl<'a> Json<'a> { pub(crate) fn parse<'a>(s: &'a str) -> Result, &'static str> { let s = s.trim_ascii(); let mut chars = s.char_indices().peekable(); - let value = parse_value(s, &mut chars)?; - Ok(value) + parse_value(s, &mut chars) } fn parse_value<'a>(s: &'a str, chars: &mut Peekable) -> Result, &'static str> { diff --git a/src/luau/mod.rs b/src/luau/mod.rs index 4015a7c4..5bb54069 100644 --- a/src/luau/mod.rs +++ b/src/luau/mod.rs @@ -1,3 +1,10 @@ +//! Luau-specific extensions and types. +//! +//! This module provides Luau-specific functionality including custom [`require`] implementations, +//! heap memory analysis, and Luau VM integration utilities. +//! +//! [`require`]: crate::Lua::create_require_function + use std::ffi::{CStr, CString}; use std::os::raw::c_int; use std::ptr; @@ -10,7 +17,7 @@ use crate::traits::{FromLuaMulti, IntoLua}; use crate::types::MaybeSend; pub use heap_dump::HeapDump; -pub use require::{NavigateError, Require, TextRequirer}; +pub use require::{FsRequirer, NavigateError, Require}; // Since Luau has some missing standard functions, we re-implement them here @@ -86,7 +93,7 @@ impl Lua { } // Enable default `require` implementation - let require = self.create_require_function(require::TextRequirer::new())?; + let require = self.create_require_function(FsRequirer::new())?; self.globals().raw_set("require", require)?; Ok(()) diff --git a/src/luau/require.rs b/src/luau/require.rs index 86b23a12..efd852f9 100644 --- a/src/luau/require.rs +++ b/src/luau/require.rs @@ -12,14 +12,17 @@ use crate::state::{Lua, callback_error_ext}; use crate::table::Table; use crate::types::MaybeSend; -// TODO: Rename to FsRequirer -pub use fs::TextRequirer; +pub use fs::FsRequirer; /// An error that can occur during navigation in the Luau `require-by-string` system. #[derive(Debug, Clone)] +#[non_exhaustive] pub enum NavigateError { + /// The path is ambiguous (more than one candidate matches). Ambiguous, + /// The requested path could not be found. NotFound, + /// Another error occurred during navigation. Other(Error), } @@ -62,12 +65,30 @@ pub trait Require { /// Resets the internal state to point at an aliased module. /// - /// This function received an exact path from a configuration file. + /// This function receives an exact path from a configuration file. /// It's only called when an alias's path cannot be resolved relative to its /// configuration file. fn jump_to_alias(&mut self, path: &str) -> StdResult<(), NavigateError>; - // Navigate to parent directory + /// Provides an initial alias override opportunity prior to searching for + /// configuration files. + /// + /// If `Ok(())` is returned, alias resolution stops here and the internal state + /// must point at the aliased location. + fn to_alias_override(&mut self, _alias: &str) -> StdResult<(), NavigateError> { + Err(NavigateError::NotFound) + } + + /// Provides a final opportunity to resolve an alias if it cannot be found in + /// configuration files. + /// + /// If `Ok(())` is returned, alias resolution stops here and the internal state + /// must point at the aliased location. + fn to_alias_fallback(&mut self, _alias: &str) -> StdResult<(), NavigateError> { + Err(NavigateError::NotFound) + } + + /// Navigates to the parent directory of the current requirer. fn to_parent(&mut self) -> StdResult<(), NavigateError>; /// Navigate to the given child directory. @@ -193,6 +214,30 @@ pub(super) unsafe extern "C-unwind" fn init_config(config: *mut ffi::luarequire_ }) } + unsafe extern "C-unwind" fn to_alias_override( + state: *mut ffi::lua_State, + ctx: *mut c_void, + alias_unprefixed: *const c_char, + ) -> ffi::luarequire_NavigateResult { + let mut this = try_borrow_mut!(state, ctx); + let alias = CStr::from_ptr(alias_unprefixed).to_string_lossy(); + callback_error_ext(state, ptr::null_mut(), true, move |_, _| { + this.to_alias_override(&alias).into_nav_result() + }) + } + + unsafe extern "C-unwind" fn to_alias_fallback( + state: *mut ffi::lua_State, + ctx: *mut c_void, + alias_unprefixed: *const c_char, + ) -> ffi::luarequire_NavigateResult { + let mut this = try_borrow_mut!(state, ctx); + let alias = CStr::from_ptr(alias_unprefixed).to_string_lossy(); + callback_error_ext(state, ptr::null_mut(), true, move |_, _| { + this.to_alias_fallback(&alias).into_nav_result() + }) + } + unsafe extern "C-unwind" fn to_parent( state: *mut ffi::lua_State, ctx: *mut c_void, @@ -299,8 +344,8 @@ pub(super) unsafe extern "C-unwind" fn init_config(config: *mut ffi::luarequire_ (*config).is_require_allowed = is_require_allowed; (*config).reset = reset; (*config).jump_to_alias = jump_to_alias; - (*config).to_alias_override = None; - (*config).to_alias_fallback = None; + (*config).to_alias_override = Some(to_alias_override); + (*config).to_alias_fallback = Some(to_alias_fallback); (*config).to_parent = to_parent; (*config).to_child = to_child; (*config).is_module_present = is_module_present; @@ -318,7 +363,7 @@ pub(super) unsafe extern "C-unwind" fn init_config(config: *mut ffi::luarequire_ fn detect_config_format(data: &[u8]) -> ConfigStatus { let data = data.trim_ascii(); if data.starts_with(b"{") { - let data = &data[1..].trim_ascii_start(); + let data = data[1..].trim_ascii_start(); if data.starts_with(b"\"") || data == b"}" { return ConfigStatus::PresentJson; } diff --git a/src/luau/require/fs.rs b/src/luau/require/fs.rs index 6588b02c..61250485 100644 --- a/src/luau/require/fs.rs +++ b/src/luau/require/fs.rs @@ -12,7 +12,7 @@ use super::{NavigateError, Require}; /// The standard implementation of Luau `require-by-string` navigation. #[derive(Default, Debug)] -pub struct TextRequirer { +pub struct FsRequirer { /// An absolute path to the current Luau module (not mapped to a physical file) abs_path: PathBuf, /// A relative path to the current Luau module (not mapped to a physical file) @@ -22,7 +22,7 @@ pub struct TextRequirer { resolved_path: Option, } -impl TextRequirer { +impl FsRequirer { /// The prefix used for chunk names in the require system. /// Only chunk names starting with this prefix are allowed to be used in `require`. const CHUNK_PREFIX: &str = "@"; @@ -36,7 +36,7 @@ impl TextRequirer { /// The filename for the Luau configuration file. const LUAU_CONFIG_FILENAME: &str = ".config.luau"; - /// Creates a new `TextRequirer` instance. + /// Creates a new `FsRequirer` instance. pub fn new() -> Self { Self::default() } @@ -114,7 +114,7 @@ impl TextRequirer { } } -impl Require for TextRequirer { +impl Require for FsRequirer { fn is_require_allowed(&self, chunk_name: &str) -> bool { chunk_name.starts_with(Self::CHUNK_PREFIX) } @@ -208,13 +208,15 @@ impl Require for TextRequirer { } fn has_config(&self) -> bool { - self.abs_path.is_dir() && self.abs_path.join(Self::LUAURC_CONFIG_FILENAME).is_file() - || self.abs_path.is_dir() && self.abs_path.join(Self::LUAU_CONFIG_FILENAME).is_file() + self.abs_path.is_dir() + && (self.abs_path.join(Self::LUAURC_CONFIG_FILENAME).is_file() + || self.abs_path.join(Self::LUAU_CONFIG_FILENAME).is_file()) } fn config(&self) -> IoResult> { - if self.abs_path.join(Self::LUAURC_CONFIG_FILENAME).is_file() { - return fs::read(self.abs_path.join(Self::LUAURC_CONFIG_FILENAME)); + let path = self.abs_path.join(Self::LUAURC_CONFIG_FILENAME); + if path.is_file() { + return fs::read(path); } fs::read(self.abs_path.join(Self::LUAU_CONFIG_FILENAME)) } @@ -231,7 +233,7 @@ impl Require for TextRequirer { mod tests { use std::path::Path; - use super::TextRequirer; + use super::FsRequirer; #[test] fn test_path_normalize() { @@ -267,7 +269,7 @@ mod tests { // '..' disappears if path is absolute and component is non-erasable ("/../", "/"), ] { - let path = TextRequirer::normalize_path(input.as_ref()); + let path = FsRequirer::normalize_path(input.as_ref()); assert_eq!( &path, expected.as_ref() as &Path, diff --git a/src/memory.rs b/src/memory.rs index a484e277..bdea1e06 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -50,7 +50,7 @@ impl MemoryState { #[inline] pub(crate) fn set_memory_limit(&mut self, limit: usize) -> usize { let prev_limit = self.memory_limit; - self.memory_limit = limit as isize; + self.memory_limit = limit.min(isize::MAX as usize) as isize; prev_limit as usize } diff --git a/src/multi.rs b/src/multi.rs index f82f4cb5..10b16046 100644 --- a/src/multi.rs +++ b/src/multi.rs @@ -1,6 +1,5 @@ use std::collections::{VecDeque, vec_deque}; use std::iter::FromIterator; -use std::mem; use std::ops::{Deref, DerefMut}; use std::os::raw::c_int; use std::result::Result as StdResult; @@ -180,10 +179,8 @@ impl IntoIterator for MultiValue { type IntoIter = vec_deque::IntoIter; #[inline] - fn into_iter(mut self) -> Self::IntoIter { - let deque = mem::take(&mut self.0); - mem::forget(self); - deque.into_iter() + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() } } diff --git a/src/prelude.rs b/src/prelude.rs index 23cdf85d..a8099960 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -3,38 +3,54 @@ #[doc(no_inline)] pub use crate::{ AnyUserData as LuaAnyUserData, BorrowedBytes as LuaBorrowedBytes, BorrowedStr as LuaBorrowedStr, - Chunk as LuaChunk, Either as LuaEither, Error as LuaError, ErrorContext as LuaErrorContext, + Either as LuaEither, Error as LuaError, ErrorContext as LuaErrorContext, ExternalError as LuaExternalError, ExternalResult as LuaExternalResult, FromLua, FromLuaMulti, - Function as LuaFunction, GCMode as LuaGCMode, Integer as LuaInteger, IntoLua, IntoLuaMulti, - LightUserData as LuaLightUserData, Lua, LuaNativeFn, LuaNativeFnMut, LuaOptions, LuaString, - MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, Number as LuaNumber, - ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, StdLib as LuaStdLib, - Table as LuaTable, Thread as LuaThread, ThreadStatus as LuaThreadStatus, UserData as LuaUserData, - UserDataFields as LuaUserDataFields, UserDataMetatable as LuaUserDataMetatable, - UserDataMethods as LuaUserDataMethods, UserDataRef as LuaUserDataRef, - UserDataRefMut as LuaUserDataRefMut, UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, - Variadic as LuaVariadic, VmState as LuaVmState, WeakLua, function::FunctionInfo as LuaFunctionInfo, + Function as LuaFunction, Integer as LuaInteger, IntoLua, IntoLuaMulti, LightUserData as LuaLightUserData, + Lua, LuaOptions, LuaString, MetaMethod as LuaMetaMethod, MultiValue as LuaMultiValue, Nil as LuaNil, + Number as LuaNumber, ObjectLike as LuaObjectLike, RegistryKey as LuaRegistryKey, Result as LuaResult, + StdLib as LuaStdLib, Table as LuaTable, Thread as LuaThread, UserData as LuaUserData, + UserDataFields as LuaUserDataFields, UserDataMethods as LuaUserDataMethods, + UserDataOwned as LuaUserDataOwned, UserDataRef as LuaUserDataRef, UserDataRefMut as LuaUserDataRefMut, + UserDataRegistry as LuaUserDataRegistry, Value as LuaValue, Variadic as LuaVariadic, + VmState as LuaVmState, WeakLua, chunk::AsChunk as AsLuaChunk, chunk::Chunk as LuaChunk, + chunk::ChunkMode as LuaChunkMode, function::FunctionInfo as LuaFunctionInfo, function::LuaNativeFn, + function::LuaNativeFnMut, state::GcIncParams as LuaGcIncParams, state::GcMode as LuaGcMode, table::TablePairs as LuaTablePairs, table::TableSequence as LuaTableSequence, + thread::ThreadEvent as LuaThreadEvent, thread::ThreadStatus as LuaThreadStatus, + thread::ThreadTriggers as LuaThreadTriggers, userdata::UserDataMetatable as LuaUserDataMetatable, }; #[cfg(not(feature = "luau"))] #[doc(no_inline)] -pub use crate::HookTriggers as LuaHookTriggers; +pub use crate::debug::HookTriggers as LuaHookTriggers; -#[cfg(feature = "luau")] +#[cfg(any(feature = "lua54", feature = "lua55"))] +#[doc(no_inline)] +pub use crate::state::GcGenParams as LuaGcGenParams; + +#[cfg(any(feature = "luau", doc))] #[doc(no_inline)] pub use crate::{ - CompileConstant as LuaCompileConstant, NavigateError as LuaNavigateError, Require as LuaRequire, - TextRequirer as LuaTextRequirer, Vector as LuaVector, + Buffer as LuaBuffer, Vector as LuaVector, + chunk::{CompileConstant as LuaCompileConstant, Compiler as LuaCompiler}, + function::CoverageInfo as LuaCoverageInfo, + luau::{ + FsRequirer as LuaFsRequirer, HeapDump as LuaHeapDump, NavigateError as LuaNavigateError, + Require as LuaRequire, + }, }; +#[cfg(any(feature = "luau-jit", doc))] +#[doc(no_inline)] +pub use crate::state::JitOptions as LuauJitOptions; + #[cfg(feature = "async")] #[doc(no_inline)] -pub use crate::{AsyncThread as LuaAsyncThread, LuaNativeAsyncFn}; +pub use crate::{function::LuaNativeAsyncFn, thread::AsyncThread as LuaAsyncThread}; #[cfg(feature = "serde")] #[doc(no_inline)] pub use crate::{ - DeserializeOptions as LuaDeserializeOptions, LuaSerdeExt, SerializableValue as LuaSerializableValue, - SerializeOptions as LuaSerializeOptions, + LuaSerdeExt, SerializableValue as LuaSerializableValue, + serde::DeserializeOptions as LuaDeserializeOptions, serde::SerializeOptions as LuaSerializeOptions, }; diff --git a/src/scope.rs b/src/scope.rs index fa8cafaf..af0621fa 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -156,38 +156,7 @@ impl<'scope, 'env: 'scope> Scope<'scope, 'env> { where T: UserData + 'env, { - let state = self.lua.state(); - unsafe { - let _sg = StackGuard::new(state); - check_stack(state, 3)?; - - // We don't write the data to the userdata until pushing the metatable - let protect = !self.lua.unlikely_memory_error(); - #[cfg(feature = "luau")] - let ud_ptr = { - let data = UserDataStorage::new_scoped(data); - util::push_userdata(state, data, protect)? - }; - #[cfg(not(feature = "luau"))] - let ud_ptr = util::push_uninit_userdata::>(state, protect)?; - - // Push the metatable and register it with no TypeId - let mut registry = UserDataRegistry::new_unique(self.lua.lua(), ud_ptr as *mut _); - T::register(&mut registry); - self.lua.push_userdata_metatable(registry.into_raw())?; - let mt_ptr = ffi::lua_topointer(state, -1); - self.lua.register_userdata_metatable(mt_ptr, None); - - // Write data to the pointer and attach metatable - #[cfg(not(feature = "luau"))] - std::ptr::write(ud_ptr, UserDataStorage::new_scoped(data)); - ffi::lua_setmetatable(state, -2); - - let ud = AnyUserData(self.lua.pop_ref()); - self.seal_userdata::(&ud); - - Ok(ud) - } + self.create_any_userdata(data, T::register) } /// Creates a Lua userdata object from a custom Rust type. diff --git a/src/serde/mod.rs b/src/serde/mod.rs index 1b85a763..21edc6ff 100644 --- a/src/serde/mod.rs +++ b/src/serde/mod.rs @@ -37,8 +37,8 @@ pub trait LuaSerdeExt: Sealed { fn null(&self) -> Value; /// A metatable attachable to a Lua table to systematically encode it as Array (instead of Map). - /// As result, encoded Array will contain only sequence part of the table, with the same length - /// as the `#` operator on that table. + /// As a result, encoded Array will contain only sequence part of the table, with the same + /// length as the `#` operator on that table. /// /// # Example /// @@ -101,7 +101,8 @@ pub trait LuaSerdeExt: Sealed { /// # Example /// /// ``` - /// use mlua::{Lua, Result, LuaSerdeExt, SerializeOptions}; + /// use mlua::serde::SerializeOptions; + /// use mlua::{Lua, Result, LuaSerdeExt}; /// /// fn main() -> Result<()> { /// let lua = Lua::new(); @@ -151,7 +152,8 @@ pub trait LuaSerdeExt: Sealed { /// # Example /// /// ``` - /// use mlua::{Lua, Result, LuaSerdeExt, DeserializeOptions}; + /// use mlua::serde::DeserializeOptions; + /// use mlua::{Lua, Result, LuaSerdeExt}; /// use serde::Deserialize; /// /// #[derive(Deserialize, Debug, PartialEq)] @@ -242,7 +244,5 @@ static ARRAY_METATABLE_REGISTRY_KEY: u8 = 0; pub mod de; pub mod ser; -#[doc(inline)] -pub use de::Deserializer; -#[doc(inline)] -pub use ser::Serializer; +pub use de::{Deserializer, Options as DeserializeOptions}; +pub use ser::{Options as SerializeOptions, Serializer}; diff --git a/src/state.rs b/src/state.rs index 9c364a2e..a43fe72f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,3 +1,8 @@ +//! Lua state management. +//! +//! This module provides the main [`Lua`] state handle together with state-specific +//! configuration and garbage collector controls. + use std::any::TypeId; use std::cell::{BorrowError, BorrowMutError, RefCell}; use std::marker::PhantomData; @@ -17,11 +22,11 @@ use crate::scope::Scope; use crate::stdlib::StdLib; use crate::string::LuaString; use crate::table::Table; -use crate::thread::Thread; +use crate::thread::{Thread, ThreadEvent, ThreadTriggers}; use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti}; use crate::types::{ - AppDataRef, AppDataRefMut, ArcReentrantMutexGuard, Integer, LuaType, MaybeSend, Number, ReentrantMutex, - ReentrantMutexGuard, RegistryKey, VmState, XRc, XWeak, + AppDataRef, AppDataRefMut, ArcReentrantMutexGuard, Integer, LuaType, MaybeSend, MaybeSync, Number, + ReentrantMutex, ReentrantMutexGuard, RegistryKey, VmState, XRc, XWeak, }; use crate::userdata::{AnyUserData, UserData, UserDataProxy, UserDataRegistry, UserDataStorage}; use crate::util::{StackGuard, assert_stack, check_stack, protect_lua_closure, push_string, rawset_field}; @@ -44,6 +49,7 @@ use { use serde::Serialize; pub(crate) use extra::ExtraData; +#[doc(hidden)] pub use raw::RawLua; pub(crate) use util::callback_error_ext; @@ -62,20 +68,135 @@ pub struct WeakLua(XWeak>); pub(crate) struct LuaGuard(ArcReentrantMutexGuard); -/// Mode of the Lua garbage collector (GC). +/// Tuning parameters for the incremental GC collector. /// -/// In Lua 5.4 GC can work in two modes: incremental and generational. -/// Previous Lua versions support only incremental GC. +/// Each field is an [`Option`]: `None` leaves the corresponding parameter unchanged, while +/// `Some(v)` sets it. Units and ranges depend on the Lua version, check the Lua reference manual +/// for details. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Default)] +pub struct GcIncParams { + /// Pause between successive GC cycles, expressed as a percentage of live memory. + #[cfg(not(feature = "luau"))] + #[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))] + pub pause: Option, + + /// Target heap size as a percentage of live data, controlling how aggressively + /// the GC reclaims memory (`LUA_GCSETGOAL`). + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + pub goal: Option, + + /// GC work performed per unit of memory allocated. + pub step_multiplier: Option, + + /// Granularity of each GC step. + /// + /// The unit is version-dependent, check the Lua reference manual for details. + #[cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))))] + pub step_size: Option, +} + +impl GcIncParams { + /// Sets the `pause` parameter. + #[cfg(not(feature = "luau"))] + #[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))] + #[must_use] + pub fn pause(mut self, v: c_int) -> Self { + self.pause = Some(v); + self + } + + /// Sets the `goal` parameter. + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + #[must_use] + pub fn goal(mut self, v: c_int) -> Self { + self.goal = Some(v); + self + } + + /// Sets the `step_multiplier` parameter. + #[must_use] + pub fn step_multiplier(mut self, v: c_int) -> Self { + self.step_multiplier = Some(v); + self + } + + /// Sets the `step_size` parameter. + #[cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))] + #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))))] + #[must_use] + pub fn step_size(mut self, v: c_int) -> Self { + self.step_size = Some(v); + self + } +} + +/// Tuning parameters for the generational GC collector (Lua 5.4+). /// -/// More information can be found in the Lua [documentation]. +/// Each field is an [`Option`]: `None` leaves the corresponding parameter unchanged, while +/// `Some(v)` sets it. Units and ranges depend on the Lua version, check the reference manual +/// for details. +#[cfg(any(feature = "lua55", feature = "lua54"))] +#[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54"))))] +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Default)] +pub struct GcGenParams { + /// Frequency of minor (young-generation) collection steps. + pub minor_multiplier: Option, + + /// Threshold controlling how large the young generation can grow before triggering + /// a shift from minor to major collection. + pub minor_to_major: Option, + + /// Threshold controlling how much the major collection must shrink the heap before + /// switching back to minor (young-generation) collection. + #[cfg(feature = "lua55")] + #[cfg_attr(docsrs, doc(cfg(feature = "lua55")))] + pub major_to_minor: Option, +} + +#[cfg(any(feature = "lua55", feature = "lua54"))] +impl GcGenParams { + /// Sets the `minor_multiplier` parameter. + #[must_use] + pub fn minor_multiplier(mut self, v: c_int) -> Self { + self.minor_multiplier = Some(v); + self + } + + /// Sets the `minor_to_major` threshold. + #[must_use] + pub fn minor_to_major(mut self, v: c_int) -> Self { + self.minor_to_major = Some(v); + self + } + + /// Sets the `major_to_minor` parameter. + #[cfg(feature = "lua55")] + #[cfg_attr(docsrs, doc(cfg(feature = "lua55")))] + #[must_use] + pub fn major_to_minor(mut self, v: c_int) -> Self { + self.major_to_minor = Some(v); + self + } +} + +/// Lua garbage collector (GC) operating mode. /// -/// [documentation]: https://www.lua.org/manual/5.4/manual.html#2.5 -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum GCMode { - Incremental, +/// Use [`Lua::gc_set_mode`] to switch the collector mode and/or tune its parameters. +#[non_exhaustive] +#[derive(Clone, Debug)] +pub enum GcMode { + /// Incremental mark-and-sweep + Incremental(GcIncParams), + + /// Generational #[cfg(any(feature = "lua55", feature = "lua54"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54"))))] - Generational, + Generational(GcGenParams), } /// Controls Lua interpreter behavior such as Rust panics handling. @@ -143,6 +264,38 @@ impl LuaOptions { } } +/// Luau JIT options +#[cfg(any(feature = "luau-jit", doc))] +#[cfg_attr(docsrs, doc(cfg(feature = "luau-jit")))] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct JitOptions { + inliner: bool, +} + +#[cfg(any(feature = "luau-jit", doc))] +impl Default for JitOptions { + fn default() -> Self { + const { Self::new() } + } +} + +#[cfg(any(feature = "luau-jit", doc))] +impl JitOptions { + /// Creates default JIT options. + pub const fn new() -> Self { + JitOptions { inliner: false } + } + + /// Toggles the runtime bytecode inliner. + /// + /// Disabled by default. Changing this option does not affect already loaded functions. + #[must_use] + pub const fn inliner(mut self, enabled: bool) -> Self { + self.inliner = enabled; + self + } +} + impl Drop for Lua { fn drop(&mut self) { if self.collect_garbage { @@ -337,30 +490,27 @@ impl Lua { R::from_stack_multi(nresults, &lua) } - /// Runs callback with the inner RawLua value. It can be used to manually push and get values on - /// the stack. + /// Calls provided function passing a reference to the [`RawLua`] handle. /// - /// This function is safe because all unsafe actions with RawLua can only be done with unsafe + /// Provided [`RawLua`] handle can be used to manually pushing/popping values to/from the stack. /// /// # Example /// ``` - /// # use mlua::{Lua, Result, FromLua, IntoLua}; + /// # use mlua::{Lua, Result, FromLua, IntoLua, IntoLuaMulti}; /// # fn main() -> Result<()> { /// let lua = Lua::new(); /// let n: i32 = { - /// let num = 11i32; - /// lua.exec_raw_lua(|lua| { - /// unsafe { - /// ::push_into_stack(num, lua)?; + /// let nums = (3, 4, 5); + /// lua.exec_raw_lua(|rawlua| unsafe { + /// nums.push_into_stack_multi(rawlua)?; + /// let mut sum = 0; + /// for _ in 0..3 { + /// sum += rawlua.pop::()?; /// } - /// - /// let n = unsafe { - /// ::from_stack(-1, lua)? - /// }; - /// Result::Ok(n) + /// Result::Ok(sum) /// }) /// }?; - /// assert_eq!(n, 11); + /// assert_eq!(n, 12); /// # Ok(()) /// # } /// ``` @@ -437,31 +587,6 @@ impl Lua { Ok(()) } - #[doc(hidden)] - #[deprecated(since = "0.11.0", note = "Use `register_module` instead")] - #[cfg(not(feature = "luau"))] - #[cfg(not(tarpaulin_include))] - pub fn load_from_function(&self, modname: &str, func: Function) -> Result { - let loaded = unsafe { - self.exec_raw::
((), |state| { - ffi::luaL_getsubtable(state, ffi::LUA_REGISTRYINDEX, ffi::LUA_LOADED_TABLE); - })? - }; - - let value = match loaded.raw_get(modname)? { - Value::Nil => { - let result = match func.call(modname)? { - Value::Nil => Value::Boolean(true), - res => res, - }; - loaded.raw_set(modname, &result)?; - result - } - res => res, - }; - T::from_lua(value, self) - } - /// Unloads module `modname`. /// /// This method does not support unloading binary Lua modules since they are internally cached @@ -657,7 +782,7 @@ impl Lua { pub fn remove_hook(&self) { let lua = self.lock(); unsafe { - ffi::lua_sethook(lua.state(), None, 0, 0); + lua.remove_thread_hook(lua.state()); } } @@ -679,7 +804,8 @@ impl Lua { /// /// ``` /// # use std::sync::{Arc, atomic::{AtomicU64, Ordering}}; - /// # use mlua::{Lua, Result, ThreadStatus, VmState}; + /// # use mlua::thread::ThreadStatus; + /// # use mlua::{Lua, Result, VmState}; /// # #[cfg(feature = "luau")] /// # fn main() -> Result<()> { /// let lua = Lua::new(); @@ -758,92 +884,92 @@ impl Lua { } } - /// Sets a thread creation callback that will be called when a thread is created. - #[cfg(any(feature = "luau", doc))] - #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] - pub fn set_thread_creation_callback(&self, callback: F) + /// Sets a callback invoked when thread lifecycle events occur. + /// + /// `triggers` controls which events trigger the callback, see [`ThreadTriggers`] for more + /// details. + /// + /// Only one callback can be registered at a time. Calling this again replaces the previous + /// callback and its triggers. + /// + /// If the callback returns an error, it's propagated out of the operation that triggered the + /// event. For a [`ThreadEvent::Yield`], the yielded values are discarded. + /// + /// # Example + /// + /// Subscribe only to yield events: + /// + /// ``` + /// # use mlua::thread::{ThreadTriggers, ThreadEvent}; + /// # use mlua::{Lua, Result}; + /// # fn main() -> Result<()> { + /// let lua = Lua::new(); + /// lua.set_thread_event_callback( + /// ThreadTriggers::ON_YIELD, + /// |_lua, event| { + /// if let ThreadEvent::Yield(thread) = event { + /// println!("thread yielded"); + /// } + /// Ok(()) + /// }, + /// ); + /// # Ok(()) + /// # } + /// ``` + pub fn set_thread_event_callback(&self, triggers: ThreadTriggers, callback: F) where - F: Fn(&Lua, Thread) -> Result<()> + MaybeSend + 'static, + F: Fn(&Lua, ThreadEvent) -> Result<()> + MaybeSend + 'static, { let lua = self.lock(); unsafe { - (*lua.extra.get()).thread_creation_callback = Some(XRc::new(callback)); - (*ffi::lua_callbacks(lua.main_state())).userthread = Some(Self::userthread_proc); + (*lua.extra.get()).thread_triggers = triggers; + (*lua.extra.get()).thread_event_callback = Some(XRc::new(callback)); + #[cfg(feature = "luau")] + { + let proc = Self::userthread_proc as _; + (*ffi::lua_callbacks(lua.main_state())).userthread = triggers.on_create.then_some(proc); + } } } - /// Sets a thread collection callback that will be called when a thread is destroyed. + /// Removes the thread event callback previously set by [`Lua::set_thread_event_callback`]. /// - /// Luau GC does not support exceptions during collection, so the callback must be - /// non-panicking. If the callback panics, the program will be aborted. - #[cfg(any(feature = "luau", doc))] - #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] - pub fn set_thread_collection_callback(&self, callback: F) - where - F: Fn(crate::LightUserData) + MaybeSend + 'static, - { + /// This function has no effect if a callback was not previously set. + pub fn remove_thread_event_callback(&self) { let lua = self.lock(); + let extra = lua.extra.get(); unsafe { - (*lua.extra.get()).thread_collection_callback = Some(XRc::new(callback)); - (*ffi::lua_callbacks(lua.main_state())).userthread = Some(Self::userthread_proc); + (*extra).thread_triggers = ThreadTriggers::new(); + (*extra).thread_event_callback = None; + #[cfg(feature = "luau")] + { + (*ffi::lua_callbacks(lua.main_state())).userthread = None; + } } } #[cfg(feature = "luau")] unsafe extern "C-unwind" fn userthread_proc(parent: *mut ffi::lua_State, child: *mut ffi::lua_State) { - let extra = ExtraData::get(child); - if !parent.is_null() { - // Thread is created - let callback = match (*extra).thread_creation_callback { - Some(ref cb) => cb.clone(), - None => return, - }; - if XRc::strong_count(&callback) > 2 { - return; // Don't allow recursion - } - ffi::lua_pushthread(child); - ffi::lua_xmove(child, (*extra).ref_thread, 1); - let value = Thread((*extra).raw_lua().pop_ref_thread(), child); - callback_error_ext(parent, extra, false, move |extra, _| { - callback((*extra).lua(), value) - }) - } else { - // Thread is about to be collected - let callback = match (*extra).thread_collection_callback { - Some(ref cb) => cb.clone(), - None => return, - }; - - // We need to wrap the callback call in non-unwind function as it's not safe to unwind when - // Luau GC is running. - // This will trigger `abort()` if the callback panics. - unsafe extern "C" fn run_callback( - callback: *const crate::types::ThreadCollectionCallback, - value: *mut ffi::lua_State, - ) { - (*callback)(crate::LightUserData(value as _)); - } - - (*extra).running_gc = true; - run_callback(&callback, child); - (*extra).running_gc = false; + // Only handle thread creation + if parent.is_null() { + return; } - } - /// Removes any thread creation or collection callbacks previously set by - /// [`Lua::set_thread_creation_callback`] or [`Lua::set_thread_collection_callback`]. - /// - /// This function has no effect if a thread callbacks were not previously set. - #[cfg(any(feature = "luau", doc))] - #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] - pub fn remove_thread_callbacks(&self) { - let lua = self.lock(); - unsafe { - let extra = lua.extra.get(); - (*extra).thread_creation_callback = None; - (*extra).thread_collection_callback = None; - (*ffi::lua_callbacks(lua.main_state())).userthread = None; + let extra = ExtraData::get(child); + if !(*extra).thread_triggers.on_create || !(*extra).thread_event_state.is_null() { + return; } + let callback = match &(*extra).thread_event_callback { + Some(cb) => cb.clone(), + _ => return, + }; + ffi::lua_pushthread(child); + ffi::lua_xmove(child, (*extra).ref_thread, 1); + let thread = Thread((*extra).raw_lua().pop_ref_thread(), child); + callback_error_ext(parent, extra, false, move |extra, _| { + let _guard = crate::thread::ThreadEventGuard::new((*extra).raw_lua(), child); + callback((*extra).lua(), ThreadEvent::Create(thread)) + }) } /// Sets the warning function to be used by Lua to emit warnings. @@ -896,11 +1022,11 @@ impl Lua { #[cfg(any(feature = "lua55", feature = "lua54"))] #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54"))))] pub fn warning(&self, msg: impl AsRef, incomplete: bool) { - let msg = msg.as_ref(); - let mut bytes = vec![0; msg.len() + 1]; - bytes[..msg.len()].copy_from_slice(msg.as_bytes()); - let real_len = bytes.iter().position(|&c| c == 0).unwrap(); - bytes.truncate(real_len); + let msg = msg.as_ref().as_bytes(); + let end = msg.iter().position(|&c| c == 0).unwrap_or(msg.len()); + let mut bytes = Vec::with_capacity(end + 1); + bytes.extend_from_slice(&msg[..end]); + bytes.push(0); let lua = self.lock(); unsafe { ffi::lua_warning(lua.state(), bytes.as_ptr() as *const _, incomplete as c_int); @@ -909,8 +1035,8 @@ impl Lua { /// Gets information about the interpreter runtime stack at the given level. /// - /// This function calls callback `f`, passing the [`Debug`] structure that can be used to get - /// information about the function executing at a given level. + /// This function calls callback `f`, passing the [`struct@Debug`] structure that can be used to + /// get information about the function executing at a given level. /// Level `0` is the current running function, whereas level `n+1` is the function that has /// called level `n` (except for tail calls, which do not count in the stack). pub fn inspect_stack(&self, level: usize, f: impl FnOnce(&Debug) -> R) -> Option { @@ -993,24 +1119,34 @@ impl Lua { feature = "lua52", feature = "luau" ))] + #[cfg_attr( + docsrs, + doc(cfg(any( + feature = "lua55", + feature = "lua54", + feature = "lua53", + feature = "lua52", + feature = "luau" + ))) + )] pub fn gc_is_running(&self) -> bool { let lua = self.lock(); unsafe { ffi::lua_gc(lua.main_state(), ffi::LUA_GCISRUNNING, 0) != 0 } } - /// Stop the Lua GC from running + /// Stops the Lua GC from running. pub fn gc_stop(&self) { let lua = self.lock(); unsafe { ffi::lua_gc(lua.main_state(), ffi::LUA_GCSTOP, 0) }; } - /// Restarts the Lua GC if it is not running + /// Restarts the Lua GC if it is not running. pub fn gc_restart(&self) { let lua = self.lock(); unsafe { ffi::lua_gc(lua.main_state(), ffi::LUA_GCRESTART, 0) }; } - /// Perform a full garbage-collection cycle. + /// Performs a full garbage-collection cycle. /// /// It may be necessary to call this function twice to collect all currently unreachable /// objects. Once to finish the current gc cycle, and once to start and finish the next cycle. @@ -1023,153 +1159,126 @@ impl Lua { } } - /// Steps the garbage collector one indivisible step. + /// Performs a basic step of garbage collection. /// - /// Returns `true` if this has finished a collection cycle. - pub fn gc_step(&self) -> Result { - self.gc_step_kbytes(0) - } - - /// Steps the garbage collector as though memory had been allocated. + /// In incremental mode, a basic step corresponds to the current step size. In generational + /// mode, a basic step performs a full minor collection or an incremental step, if the collector + /// has scheduled one. /// - /// if `kbytes` is 0, then this is the same as calling `gc_step`. Returns true if this step has - /// finished a collection cycle. - pub fn gc_step_kbytes(&self, kbytes: c_int) -> Result { + /// In incremental mode, returns `true` if this step has finished a collection cycle. + /// In generational mode, returns `true` if the step finished a major collection. + pub fn gc_step(&self) -> Result { let lua = self.lock(); let state = lua.main_state(); unsafe { check_stack(state, 3)?; protect_lua!(state, 0, 0, |state| { - ffi::lua_gc(state, ffi::LUA_GCSTEP, kbytes) != 0 + ffi::lua_gc(state, ffi::LUA_GCSTEP, 0) != 0 }) } } - /// Sets the `pause` value of the collector. - /// - /// Returns the previous value of `pause`. More information can be found in the Lua - /// [documentation]. + /// Switches the GC to the given mode with the provided parameters. /// - /// For Luau this parameter sets GC goal + /// Returns the previous [`GcMode`]. Only the collector *mode* is reported, the returned value's + /// parameter fields are always `None`. /// - /// [documentation]: https://www.lua.org/manual/5.4/manual.html#2.5 - pub fn gc_set_pause(&self, pause: c_int) -> c_int { - let lua = self.lock(); - let state = lua.main_state(); - unsafe { - #[cfg(feature = "lua55")] - return ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPPAUSE, pause); - - #[cfg(not(any(feature = "lua55", feature = "luau")))] - return ffi::lua_gc(state, ffi::LUA_GCSETPAUSE, pause); - - #[cfg(feature = "luau")] - return ffi::lua_gc(state, ffi::LUA_GCSETGOAL, pause); - } - } - - /// Sets the `step multiplier` value of the collector. + /// If the collector is internally stopped, the mode cannot be changed and the requested mode is + /// returned as-is. /// - /// Returns the previous value of the `step multiplier`. More information can be found in the - /// Lua [documentation]. - /// - /// [documentation]: https://www.lua.org/manual/5.4/manual.html#2.5 - pub fn gc_set_step_multiplier(&self, step_multiplier: c_int) -> c_int { - let lua = self.lock(); - unsafe { - #[cfg(feature = "lua55")] - return ffi::lua_gc( - lua.main_state(), - ffi::LUA_GCPARAM, - ffi::LUA_GCPSTEPMUL, - step_multiplier, - ); - - #[cfg(not(feature = "lua55"))] - return ffi::lua_gc(lua.main_state(), ffi::LUA_GCSETSTEPMUL, step_multiplier); - } - } - - /// Changes the collector to incremental mode with the given parameters. + /// # Examples /// - /// Returns the previous mode (always `GCMode::Incremental` in Lua < 5.4). - /// More information can be found in the Lua [documentation]. + /// Switch to generational mode (Lua 5.4+): + /// ```ignore + /// let prev = lua.gc_set_mode(GcMode::Generational(GcGenParams::default())); + /// ``` /// - /// [documentation]: https://www.lua.org/manual/5.4/manual.html#2.5.1 - pub fn gc_inc(&self, pause: c_int, step_multiplier: c_int, step_size: c_int) -> GCMode { + /// Switch to incremental mode with custom parameters: + /// ```ignore + /// lua.gc_set_mode(GcMode::Incremental( + /// GcIncParams::default().step_multiplier(100) + /// )); + /// ``` + pub fn gc_set_mode(&self, mode: GcMode) -> GcMode { let lua = self.lock(); let state = lua.main_state(); - #[cfg(any( - feature = "lua53", - feature = "lua52", - feature = "lua51", - feature = "luajit", - feature = "luau" - ))] - unsafe { - if pause > 0 { - #[cfg(not(feature = "luau"))] - ffi::lua_gc(state, ffi::LUA_GCSETPAUSE, pause); - #[cfg(feature = "luau")] - ffi::lua_gc(state, ffi::LUA_GCSETGOAL, pause); - } - - if step_multiplier > 0 { - ffi::lua_gc(state, ffi::LUA_GCSETSTEPMUL, step_multiplier); - } - + match mode { + #[cfg(feature = "lua55")] + GcMode::Incremental(params) => unsafe { + if let Some(v) = params.pause { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPPAUSE, v); + } + if let Some(v) = params.step_multiplier { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPSTEPMUL, v); + } + if let Some(v) = params.step_size { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPSTEPSIZE, v); + } + match ffi::lua_gc(state, ffi::LUA_GCINC) { + ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), + _ => GcMode::Incremental(GcIncParams::default()), + } + }, + #[cfg(feature = "lua54")] + GcMode::Incremental(params) => unsafe { + let pause = params.pause.unwrap_or(0); + let step_mul = params.step_multiplier.unwrap_or(0); + let step_size = params.step_size.unwrap_or(0); + match ffi::lua_gc(state, ffi::LUA_GCINC, pause, step_mul, step_size) { + ffi::LUA_GCGEN => GcMode::Generational(GcGenParams::default()), + _ => GcMode::Incremental(GcIncParams::default()), + } + }, + #[cfg(any(feature = "lua53", feature = "lua52", feature = "lua51", feature = "luajit"))] + GcMode::Incremental(params) => unsafe { + if let Some(v) = params.pause { + ffi::lua_gc(state, ffi::LUA_GCSETPAUSE, v); + } + if let Some(v) = params.step_multiplier { + ffi::lua_gc(state, ffi::LUA_GCSETSTEPMUL, v); + } + GcMode::Incremental(GcIncParams::default()) + }, #[cfg(feature = "luau")] - if step_size > 0 { - ffi::lua_gc(state, ffi::LUA_GCSETSTEPSIZE, step_size); - } - #[cfg(not(feature = "luau"))] - let _ = step_size; // Ignored - - GCMode::Incremental - } - - #[cfg(feature = "lua55")] - let prev_mode = unsafe { - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPPAUSE, pause); - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPSTEPMUL, step_multiplier); - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPSTEPSIZE, step_size); - ffi::lua_gc(state, ffi::LUA_GCINC) - }; - #[cfg(feature = "lua54")] - let prev_mode = unsafe { ffi::lua_gc(state, ffi::LUA_GCINC, pause, step_multiplier, step_size) }; - #[cfg(any(feature = "lua55", feature = "lua54"))] - match prev_mode { - ffi::LUA_GCINC => GCMode::Incremental, - ffi::LUA_GCGEN => GCMode::Generational, - _ => unreachable!(), - } - } + GcMode::Incremental(params) => unsafe { + if let Some(v) = params.goal { + ffi::lua_gc(state, ffi::LUA_GCSETGOAL, v); + } + if let Some(v) = params.step_multiplier { + ffi::lua_gc(state, ffi::LUA_GCSETSTEPMUL, v); + } + if let Some(v) = params.step_size { + ffi::lua_gc(state, ffi::LUA_GCSETSTEPSIZE, v); + } + GcMode::Incremental(GcIncParams::default()) + }, - /// Changes the collector to generational mode with the given parameters. - /// - /// Returns the previous mode. More information about the generational GC - /// can be found in the Lua 5.4 [documentation][lua_doc]. - /// - /// [lua_doc]: https://www.lua.org/manual/5.4/manual.html#2.5.2 - #[cfg(any(feature = "lua55", feature = "lua54"))] - #[cfg_attr(docsrs, doc(cfg(any(feature = "lua55", feature = "lua54"))))] - pub fn gc_gen(&self, minor_multiplier: c_int, major_multiplier: c_int) -> GCMode { - let lua = self.lock(); - let state = lua.main_state(); - #[cfg(feature = "lua55")] - let prev_mode = unsafe { - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMINORMUL, minor_multiplier); - ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMINORMAJOR, major_multiplier); - // TODO: LUA_GCPMAJORMINOR - ffi::lua_gc(state, ffi::LUA_GCGEN) - }; - #[cfg(not(feature = "lua55"))] - let prev_mode = unsafe { ffi::lua_gc(state, ffi::LUA_GCGEN, minor_multiplier, major_multiplier) }; - match prev_mode { - ffi::LUA_GCGEN => GCMode::Generational, - ffi::LUA_GCINC => GCMode::Incremental, - _ => unreachable!(), + #[cfg(feature = "lua55")] + GcMode::Generational(params) => unsafe { + if let Some(v) = params.minor_multiplier { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMINORMUL, v); + } + if let Some(v) = params.minor_to_major { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMINORMAJOR, v); + } + if let Some(v) = params.major_to_minor { + ffi::lua_gc(state, ffi::LUA_GCPARAM, ffi::LUA_GCPMAJORMINOR, v); + } + match ffi::lua_gc(state, ffi::LUA_GCGEN) { + ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), + _ => GcMode::Generational(GcGenParams::default()), + } + }, + #[cfg(feature = "lua54")] + GcMode::Generational(params) => unsafe { + let minor = params.minor_multiplier.unwrap_or(0); + let minor_to_major = params.minor_to_major.unwrap_or(0); + match ffi::lua_gc(state, ffi::LUA_GCGEN, minor, minor_to_major) { + ffi::LUA_GCINC => GcMode::Incremental(GcIncParams::default()), + _ => GcMode::Generational(GcGenParams::default()), + } + }, } } @@ -1197,6 +1306,23 @@ impl Lua { unsafe { (*lua.extra.get()).enable_jit = enable }; } + /// Configures JIT options for this Lua VM. + #[cfg(any(feature = "luau-jit", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau-jit")))] + pub fn set_jit_options(&self, options: JitOptions) { + let lua = self.lock(); + unsafe { + let state = lua.main_state(); + if options.inliner { + let _ = Self::set_fflag("LuauCallFeedback", true); + let _ = Self::set_fflag("LuauEmitCallFeedback", true); + ffi::luau_enable_jit_inliner(state); + } else { + ffi::luau_disable_jit_inliner(state); + } + } + } + /// Sets Luau feature flag (global setting). /// /// See https://github.com/luau-lang/luau/blob/master/CONTRIBUTING.md#feature-flags for details. @@ -1218,7 +1344,7 @@ impl Lua { /// similar on the returned builder. Code is not even parsed until one of these methods is /// called. /// - /// [`Chunk::exec`]: crate::Chunk::exec + /// [`Chunk::exec`]: crate::chunk::Chunk::exec #[track_caller] pub fn load<'a>(&self, chunk: impl AsChunk + 'a) -> Chunk<'a> { self.load_with_location(chunk, Location::caller()) @@ -1456,7 +1582,7 @@ impl Lua { /// } /// ``` /// - /// [`AsyncThread`]: crate::AsyncThread + /// [`AsyncThread`]: crate::thread::AsyncThread #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] pub fn create_async_function(&self, func: F) -> Result @@ -1492,7 +1618,7 @@ impl Lua { #[inline] pub fn create_userdata(&self, data: T) -> Result where - T: UserData + MaybeSend + 'static, + T: UserData + MaybeSend + MaybeSync + 'static, { unsafe { self.lock().make_userdata(UserDataStorage::new(data)) } } @@ -1503,7 +1629,7 @@ impl Lua { #[inline] pub fn create_ser_userdata(&self, data: T) -> Result where - T: UserData + Serialize + MaybeSend + 'static, + T: UserData + Serialize + MaybeSend + MaybeSync + 'static, { unsafe { self.lock().make_userdata(UserDataStorage::new_ser(data)) } } @@ -1518,7 +1644,7 @@ impl Lua { #[inline] pub fn create_any_userdata(&self, data: T) -> Result where - T: MaybeSend + 'static, + T: MaybeSend + MaybeSync + 'static, { unsafe { self.lock().make_any_userdata(UserDataStorage::new(data)) } } @@ -1531,7 +1657,7 @@ impl Lua { #[inline] pub fn create_ser_any_userdata(&self, data: T) -> Result where - T: Serialize + MaybeSend + 'static, + T: Serialize + MaybeSend + MaybeSync + 'static, { unsafe { (self.lock()).make_any_userdata(UserDataStorage::new_ser(data)) } } @@ -1711,6 +1837,16 @@ impl Lua { let lua = self.lock(); let state = lua.state(); unsafe { + // If this thread is implicit (created by `call_async`), return the root user-owned thread + // instead. + #[cfg(feature = "async")] + if let Some(&owner) = (*lua.extra.get()).thread_ownership_map.get(&state) { + assert_stack(owner, 1); + ffi::lua_pushthread(owner); + ffi::lua_xmove(owner, lua.ref_thread(), 1); + return Thread(lua.pop_ref_thread(), owner); + } + let _sg = StackGuard::new(state); assert_stack(state, 1); ffi::lua_pushthread(state); @@ -1784,7 +1920,7 @@ impl Lua { lua.push_value(&v)?; let mut isint = 0; let i = ffi::lua_tointegerx(state, -1, &mut isint); - if isint == 0 { None } else { Some(i) } + (isint != 0).then_some(i) }, }) } @@ -1806,7 +1942,7 @@ impl Lua { lua.push_value(&v)?; let mut isnum = 0; let n = ffi::lua_tonumberx(state, -1, &mut isnum); - if isnum == 0 { None } else { Some(n) } + (isnum != 0).then_some(n) }, }) } diff --git a/src/state/extra.rs b/src/state/extra.rs index d761c9cb..9f273572 100644 --- a/src/state/extra.rs +++ b/src/state/extra.rs @@ -12,7 +12,8 @@ use rustc_hash::FxHashMap; use crate::error::Result; use crate::state::RawLua; use crate::stdlib::StdLib; -use crate::types::{AppData, ReentrantMutex, XRc}; +use crate::thread::ThreadTriggers; +use crate::types::{AppData, ReentrantMutex, ThreadEventCallback, XRc}; use crate::userdata::RawUserDataRegistry; use crate::util::{TypeKey, WrappedFailure, get_internal_metatable, push_internal_userdata}; @@ -64,7 +65,10 @@ 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, + pub(super) thread_pool: Vec, + // Map for implicit threads to root user-owned Thread + #[cfg(feature = "async")] + pub(super) thread_ownership_map: FxHashMap<*mut ffi::lua_State, *mut ffi::lua_State>, // Address of `WrappedFailure` metatable pub(super) wrapped_failure_mt_ptr: *const c_void, @@ -77,14 +81,15 @@ pub(crate) struct ExtraData { pub(super) hook_callback: Option, #[cfg(not(feature = "luau"))] pub(super) hook_triggers: crate::debug::HookTriggers, + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + pub(super) hook_removed_while_yielded: bool, #[cfg(any(feature = "lua55", feature = "lua54"))] pub(super) warn_callback: Option, #[cfg(feature = "luau")] pub(super) interrupt_callback: Option, - #[cfg(feature = "luau")] - pub(super) thread_creation_callback: Option, - #[cfg(feature = "luau")] - pub(super) thread_collection_callback: Option, + pub(super) thread_triggers: ThreadTriggers, + pub(super) thread_event_callback: Option, + pub(super) thread_event_state: *mut ffi::lua_State, #[cfg(feature = "luau")] pub(crate) running_gc: bool, @@ -175,6 +180,8 @@ impl ExtraData { wrapped_failure_top: 0, #[cfg(feature = "async")] thread_pool: Vec::new(), + #[cfg(feature = "async")] + thread_ownership_map: FxHashMap::default(), wrapped_failure_mt_ptr, #[cfg(feature = "async")] waker: NonNull::from(noop_waker_ref()), @@ -182,14 +189,15 @@ impl ExtraData { hook_callback: None, #[cfg(not(feature = "luau"))] hook_triggers: Default::default(), + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + hook_removed_while_yielded: false, #[cfg(any(feature = "lua55", feature = "lua54"))] warn_callback: None, #[cfg(feature = "luau")] interrupt_callback: None, - #[cfg(feature = "luau")] - thread_creation_callback: None, - #[cfg(feature = "luau")] - thread_collection_callback: None, + thread_triggers: ThreadTriggers::default(), + thread_event_callback: None, + thread_event_state: ptr::null_mut(), #[cfg(feature = "luau")] sandboxed: false, #[cfg(feature = "luau")] diff --git a/src/state/raw.rs b/src/state/raw.rs index 4b2fa2da..79588fdd 100644 --- a/src/state/raw.rs +++ b/src/state/raw.rs @@ -1,7 +1,7 @@ use std::any::TypeId; use std::cell::{Cell, UnsafeCell}; use std::ffi::CStr; -use std::mem; +use std::mem::{self, ManuallyDrop}; use std::os::raw::{c_char, c_int, c_void}; use std::panic::resume_unwind; use std::ptr::{self, NonNull}; @@ -15,11 +15,11 @@ use crate::state::util::callback_error_ext; use crate::stdlib::StdLib; use crate::string::LuaString; use crate::table::Table; -use crate::thread::Thread; -use crate::traits::IntoLua; +use crate::thread::{Thread, ThreadTriggers}; +use crate::traits::{FromLua, IntoLua}; use crate::types::{ AppDataRef, AppDataRefMut, Callback, CallbackUpvalue, DestructedUserdata, Integer, LightUserData, - LuaType, MaybeSend, ReentrantMutex, RegistryKey, ValueRef, XRc, + LuaType, MaybeSend, ReentrantMutex, RegistryKey, ThreadEventCallback, ValueRef, XRc, }; use crate::userdata::{ AnyUserData, MetaMethod, RawUserDataRegistry, UserData, UserDataRegistry, UserDataStorage, @@ -50,13 +50,13 @@ use { std::task::{Context, Poll, Waker}, }; -/// An inner Lua struct which holds a raw Lua state. +/// An internal Lua struct which holds a raw Lua state. #[doc(hidden)] pub struct RawLua { // The state is dynamic and depends on context pub(super) state: Cell<*mut ffi::lua_State>, pub(super) main_state: Option>, - pub(super) extra: XRc>, + pub(super) extra: ManuallyDrop>>, owned: bool, } @@ -82,6 +82,9 @@ impl Drop for RawLua { if !mem_state.is_null() { drop(Box::from_raw(mem_state)); } + + // Drop the `ExtraData` reference after `lua_close` has collected the registry entry + ManuallyDrop::drop(&mut self.extra); } } } @@ -245,7 +248,7 @@ impl RawLua { state: Cell::new(state), // Make sure that we don't store current state as main state (if it's not available) main_state: get_main_state(state).and_then(NonNull::new), - extra: XRc::clone(&extra), + extra: ManuallyDrop::new(XRc::clone(&extra)), owned, })); (*extra.get()).set_lua(&rawlua); @@ -301,13 +304,13 @@ impl RawLua { #[cfg(not(feature = "luau"))] if is_safe { let curr_libs = (*self.extra.get()).libs; - if (curr_libs ^ (curr_libs | libs)).contains(StdLib::PACKAGE) { + if libs.contains(StdLib::PACKAGE) && !curr_libs.contains(StdLib::PACKAGE) { mlua_expect!(self.lua().disable_c_modules(), "Error disabling C modules"); } } #[cfg(feature = "luau")] let _ = is_safe; - unsafe { (*self.extra.get()).libs |= libs }; + (*self.extra.get()).libs |= libs; res } @@ -424,6 +427,10 @@ impl RawLua { if event == ffi::LUA_HOOKCOUNT || event == ffi::LUA_HOOKLINE { #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] if ffi::lua_isyieldable(state) != 0 { + if ffi::lua_gethook(state).is_none() { + let extra = ExtraData::get(state); + (*extra).hook_removed_while_yielded = true; + } ffi::lua_yield(state, 0); } #[cfg(any(feature = "lua52", feature = "lua51", feature = "luajit"))] @@ -515,6 +522,41 @@ impl RawLua { Ok(()) } + #[cfg(not(feature = "luau"))] + #[inline] + pub(crate) unsafe fn remove_thread_hook(&self, thread_state: *mut ffi::lua_State) { + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + if ffi::lua_status(thread_state) == ffi::LUA_YIELD && Self::has_hook_yielded_frame(thread_state) { + (*self.extra.get()).hook_removed_while_yielded = true; + } + ffi::lua_sethook(thread_state, None, 0, 0); + } + + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + unsafe fn has_hook_yielded_frame(thread_state: *mut ffi::lua_State) -> bool { + let mut ar = mem::zeroed::(); + ffi::lua_getstack(thread_state, 0, &mut ar) != 0 + && ffi::lua_getinfo(thread_state, cstr!("S"), &mut ar) != 0 + && !ar.what.is_null() + && CStr::from_ptr(ar.what).to_bytes() != b"C" + } + + pub(crate) unsafe fn is_hook_yielded(&self, thread_state: *mut ffi::lua_State) -> bool { + #[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] + { + if ffi::lua_gethook(thread_state).is_none() && !(*self.extra.get()).hook_removed_while_yielded { + return false; + } + Self::has_hook_yielded_frame(thread_state) + } + + #[cfg(not(any(feature = "lua55", feature = "lua54", feature = "lua53")))] + { + let _ = thread_state; + false + } + } + /// See [`Lua::create_string`] pub(crate) unsafe fn create_string(&self, s: &[u8]) -> Result { let state = self.state(); @@ -640,7 +682,7 @@ impl RawLua { let protect = !self.unlikely_memory_error(); #[cfg(feature = "luau")] - let protect = protect || (*self.extra.get()).thread_creation_callback.is_some(); + let protect = protect || self.thread_event_triggers().on_create; let thread_state = if !protect { ffi::lua_newthread(state) @@ -653,6 +695,17 @@ impl RawLua { self.set_thread_hook(thread_state, HookKind::Global)?; let thread = Thread(self.pop_ref(), thread_state); + + // Exec creation callback for non-Luau (Luau handles this via `userthread_proc`) + #[cfg(not(feature = "luau"))] + if self.thread_event_triggers().on_create && self.thread_event_state().is_null() { + let extra = self.extra.get(); + if let Some(cb) = (*extra).thread_event_callback.clone() { + let _guard = crate::thread::ThreadEventGuard::new(self, thread_state); + cb((*extra).lua(), crate::thread::ThreadEvent::Create(thread.clone()))?; + } + } + ffi::lua_xpush(self.ref_thread(), thread_state, func.0.index); Ok(thread) } @@ -661,7 +714,7 @@ impl RawLua { #[cfg(feature = "async")] pub(crate) unsafe fn create_recycled_thread(&self, func: &Function) -> Result { if let Some(index) = (*self.extra.get()).thread_pool.pop() { - let thread_state = ffi::lua_tothread(self.ref_thread(), *index.0); + let thread_state = ffi::lua_tothread(self.ref_thread(), index); ffi::lua_xpush(self.ref_thread(), thread_state, func.0.index); #[cfg(feature = "luau")] @@ -677,17 +730,55 @@ impl RawLua { self.create_thread(func) } + /// Updates the ownership of the given implicit thread to the root user-owned thread. + /// + /// If `owner` is `None`, the thread is removed from the ownership map. + #[cfg(feature = "async")] + pub(crate) unsafe fn update_thread_ownership(&self, th: &Thread, owner: Option<*mut ffi::lua_State>) { + let extra = &mut *self.extra.get(); + let th_state = th.state(); + match owner { + Some(owner) => { + let new_owner = (extra.thread_ownership_map).get(&owner).copied().unwrap_or(owner); + extra.thread_ownership_map.insert(th_state, new_owner); + } + None => { + extra.thread_ownership_map.remove(&th_state); + } + } + } + /// Returns the thread to the pool for later use. #[cfg(feature = "async")] 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() - && let Some(index) = thread.0.index_count.take() + && let Some(index) = thread.0.take_index() { extra.thread_pool.push(index); } } + #[inline(always)] + pub(crate) unsafe fn thread_event_triggers(&self) -> ThreadTriggers { + (*self.extra.get()).thread_triggers + } + + #[inline(always)] + pub(crate) unsafe fn thread_event_callback(&self) -> Option { + (*self.extra.get()).thread_event_callback.clone() + } + + #[inline(always)] + pub(crate) unsafe fn thread_event_state(&self) -> *mut ffi::lua_State { + (*self.extra.get()).thread_event_state + } + + #[inline(always)] + pub(crate) unsafe fn set_thread_event_state(&self, state: *mut ffi::lua_State) { + (*self.extra.get()).thread_event_state = state; + } + /// Pushes a primitive type value onto the Lua stack. pub(crate) unsafe fn push_primitive_type(&self) -> bool { match T::TYPE_ID { @@ -731,14 +822,27 @@ impl RawLua { /// Pushes a value that implements `IntoLua` onto the Lua stack. /// /// Uses up to 2 stack spaces to push a single value, does not call `checkstack`. + #[allow(clippy::missing_safety_doc)] #[inline(always)] pub unsafe fn push(&self, value: impl IntoLua) -> Result<()> { value.push_into_stack(self) } + /// Pops a value that implements [`FromLua`] from the top of the Lua stack. + /// + /// Uses up to 1 stack space, does not call `checkstack`. + #[allow(clippy::missing_safety_doc)] + #[inline(always)] + pub unsafe fn pop(&self) -> Result { + let v = R::from_stack(-1, self)?; + ffi::lua_pop(self.state(), 1); + Ok(v) + } + /// Pushes a `Value` (by reference) onto the Lua stack. /// - /// Uses 2 stack spaces, does not call `checkstack`. + /// Uses up to 2 stack spaces, does not call `checkstack`. + #[allow(clippy::missing_safety_doc)] pub unsafe fn push_value(&self, value: &Value) -> Result<()> { let state = self.state(); match value { @@ -773,6 +877,7 @@ impl RawLua { /// Pops a value from the Lua stack. /// /// Uses up to 1 stack spaces, does not call `checkstack`. + #[allow(clippy::missing_safety_doc)] #[inline] pub unsafe fn pop_value(&self) -> Value { let value = self.stack_value(-1, None); @@ -803,15 +908,22 @@ impl RawLua { #[cfg(any(feature = "lua52", feature = "lua51", feature = "luajit", feature = "luau"))] ffi::LUA_TNUMBER => { - use crate::types::Number; - let n = ffi::lua_tonumber(state, idx); match num_traits::cast(n) { - Some(i) if n.to_bits() == (i as Number).to_bits() => Value::Integer(i), + Some(i) if n.to_bits() == (i as crate::types::Number).to_bits() => Value::Integer(i), _ => Value::Number(n), } } + #[cfg(feature = "luau")] + ffi::LUA_TINTEGER => { + let i = ffi::lua_tointeger64(state, idx, ptr::null_mut()); + match num_traits::cast(i) { + Some(i) => Value::Integer(i), + _ => Value::Number(i as crate::types::Number), + } + } + #[cfg(feature = "luau")] ffi::LUA_TVECTOR => { let v = ffi::lua_tovector(state, idx); @@ -949,7 +1061,7 @@ impl RawLua { // Check if userdata/metatable is already registered let type_id = TypeId::of::(); if let Some(&table_id) = (*self.extra.get()).registered_userdata_t.get(&type_id) { - return Ok(table_id as Integer); + return Ok(table_id); } // Create a new metatable from `UserData` definition @@ -968,7 +1080,7 @@ impl RawLua { // Check if userdata/metatable is already registered let type_id = TypeId::of::(); if let Some(&table_id) = (*self.extra.get()).registered_userdata_t.get(&type_id) { - return Ok(table_id as Integer); + return Ok(table_id); } // Check if metatable creation is pending or create an empty metatable otherwise @@ -983,7 +1095,7 @@ impl RawLua { unsafe fn make_userdata_with_metatable( &self, data: UserDataStorage, - get_metatable_id: impl FnOnce() -> Result, + get_metatable_id: impl FnOnce() -> Result, ) -> Result { let state = self.state(); let _sg = StackGuard::new(state); @@ -993,7 +1105,7 @@ impl RawLua { let mt_id = get_metatable_id()?; let protect = !self.unlikely_memory_error(); push_userdata(state, data, protect)?; - ffi::lua_rawgeti(state, ffi::LUA_REGISTRYINDEX, mt_id); + ffi::lua_rawgeti(state, ffi::LUA_REGISTRYINDEX, mt_id as _); ffi::lua_setmetatable(state, -2); // Set empty environment for Lua 5.1 @@ -1011,7 +1123,7 @@ impl RawLua { Ok(AnyUserData(self.pop_ref())) } - pub(crate) unsafe fn create_userdata_metatable(&self, registry: RawUserDataRegistry) -> Result { + pub(crate) unsafe fn create_userdata_metatable(&self, registry: RawUserDataRegistry) -> Result { let state = self.state(); let type_id = registry.type_id; @@ -1027,7 +1139,7 @@ impl RawLua { } self.register_userdata_metatable(mt_ptr, type_id); - Ok(id as Integer) + Ok(id) } pub(crate) unsafe fn push_userdata_metatable(&self, mut registry: RawUserDataRegistry) -> Result<()> { @@ -1584,6 +1696,11 @@ unsafe fn load_std_libs(state: *mut ffi::lua_State, libs: StdLib) -> Result<()> requiref(state, ffi::LUA_VECLIBNAME, ffi::luaopen_vector, 1)?; } + #[cfg(feature = "luau")] + if libs.contains(StdLib::INTEGER) { + requiref(state, ffi::LUA_INTLIBNAME, ffi::luaopen_integer, 1)?; + } + if libs.contains(StdLib::MATH) { requiref(state, ffi::LUA_MATHLIBNAME, ffi::luaopen_math, 1)?; } diff --git a/src/stdlib.rs b/src/stdlib.rs index 46eb33bc..e1c32e9a 100644 --- a/src/stdlib.rs +++ b/src/stdlib.rs @@ -1,4 +1,4 @@ -use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign}; +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; /// Flags describing the set of lua standard libraries to load. #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] @@ -73,10 +73,15 @@ impl StdLib { #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] pub const VECTOR: StdLib = StdLib(1 << 10); + /// [`integer`](https://luau.org/library#integer-library) library + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + pub const INTEGER: StdLib = StdLib(1 << 11); + /// [`jit`](http://luajit.org/ext_jit.html) library #[cfg(any(feature = "luajit", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luajit")))] - pub const JIT: StdLib = StdLib(1 << 11); + pub const JIT: StdLib = StdLib(1 << 12); /// (**unsafe**) [`ffi`](http://luajit.org/ext_ffi.html) library #[cfg(any(feature = "luajit", doc))] @@ -139,3 +144,10 @@ impl BitXorAssign for StdLib { *self = StdLib(self.0 ^ rhs.0) } } + +impl Not for StdLib { + type Output = Self; + fn not(self) -> Self::Output { + StdLib(!self.0) + } +} diff --git a/src/string.rs b/src/string.rs index 0a1eff91..18a40807 100644 --- a/src/string.rs +++ b/src/string.rs @@ -1,8 +1,12 @@ -use std::borrow::{Borrow, Cow}; +//! Lua string handling. +//! +//! This module provides types for working with Lua strings from Rust. + +use std::borrow::Borrow; use std::hash::{Hash, Hasher}; use std::ops::Deref; use std::os::raw::{c_int, c_void}; -use std::{cmp, fmt, slice, str}; +use std::{cmp, fmt, mem, slice, str}; use crate::error::{Error, Result}; use crate::state::Lua; @@ -19,12 +23,15 @@ use { /// Handle to an internal Lua string. /// /// Unlike Rust strings, Lua strings may not be valid UTF-8. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct LuaString(pub(crate) ValueRef); impl LuaString { /// Get a [`BorrowedStr`] if the Lua string is valid UTF-8. /// + /// The returned `BorrowedStr` holds a strong reference to the Lua state to guarantee the + /// validity of the underlying data. + /// /// # Examples /// /// ``` @@ -42,7 +49,7 @@ impl LuaString { /// # } /// ``` #[inline] - pub fn to_str(&self) -> Result> { + pub fn to_str(&self) -> Result { BorrowedStr::try_from(self) } @@ -85,8 +92,9 @@ impl LuaString { /// Get the bytes that make up this string. /// - /// The returned slice will not contain the terminating null byte, but will contain any null - /// bytes embedded into the Lua string. + /// The returned `BorrowedStr` holds a strong reference to the Lua state to guarantee the + /// validity of the underlying data. The data will not contain the terminating null byte, but + /// will contain any null bytes embedded into the Lua string. /// /// # Examples /// @@ -101,16 +109,16 @@ impl LuaString { /// # } /// ``` #[inline] - pub fn as_bytes(&self) -> BorrowedBytes<'_> { + pub fn as_bytes(&self) -> BorrowedBytes { BorrowedBytes::from(self) } /// Get the bytes that make up this string, including the trailing null byte. - pub fn as_bytes_with_nul(&self) -> BorrowedBytes<'_> { - let BorrowedBytes { buf, borrow, _lua } = BorrowedBytes::from(self); + pub fn as_bytes_with_nul(&self) -> BorrowedBytes { + let BorrowedBytes { buf, vref, _lua } = BorrowedBytes::from(self); // Include the trailing null byte (it's always present but excluded by default) let buf = unsafe { slice::from_raw_parts((*buf).as_ptr(), (*buf).len() + 1) }; - BorrowedBytes { buf, borrow, _lua } + BorrowedBytes { buf, vref, _lua } } // Does not return the terminating null byte @@ -141,7 +149,10 @@ impl LuaString { /// Typically this function is used only for hashing and debug information. #[inline] pub fn to_pointer(&self) -> *const c_void { - self.0.to_pointer() + // In Lua < 5.4 (excluding Luau), string pointers are NULL + // Use alternative approach + let lua = self.0.lua.lock(); + unsafe { ffi::lua_tostring(lua.ref_thread(), self.0.index) as *const c_void } } } @@ -175,12 +186,6 @@ where } } -impl PartialEq for LuaString { - fn eq(&self, other: &LuaString) -> bool { - self.as_bytes() == other.as_bytes() - } -} - impl Eq for LuaString {} impl PartialOrd for LuaString @@ -233,14 +238,14 @@ impl fmt::Display for Display<'_> { } /// A borrowed string (`&str`) that holds a strong reference to the Lua state. -pub struct BorrowedStr<'a> { +pub struct BorrowedStr { // `buf` points to a readonly memory managed by Lua - pub(crate) buf: &'a str, - pub(crate) borrow: Cow<'a, LuaString>, + pub(crate) buf: &'static str, + pub(crate) vref: ValueRef, pub(crate) _lua: Lua, } -impl Deref for BorrowedStr<'_> { +impl Deref for BorrowedStr { type Target = str; #[inline(always)] @@ -249,33 +254,39 @@ impl Deref for BorrowedStr<'_> { } } -impl Borrow for BorrowedStr<'_> { +impl Borrow for BorrowedStr { #[inline(always)] fn borrow(&self) -> &str { self.buf } } -impl AsRef for BorrowedStr<'_> { +impl AsRef for BorrowedStr { #[inline(always)] fn as_ref(&self) -> &str { self.buf } } -impl fmt::Display for BorrowedStr<'_> { +impl Hash for BorrowedStr { + fn hash(&self, state: &mut H) { + self.buf.hash(state); + } +} + +impl fmt::Display for BorrowedStr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.buf.fmt(f) } } -impl fmt::Debug for BorrowedStr<'_> { +impl fmt::Debug for BorrowedStr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.buf.fmt(f) } } -impl PartialEq for BorrowedStr<'_> +impl PartialEq for BorrowedStr where T: AsRef, { @@ -284,9 +295,9 @@ where } } -impl Eq for BorrowedStr<'_> {} +impl Eq for BorrowedStr {} -impl PartialOrd for BorrowedStr<'_> +impl PartialOrd for BorrowedStr where T: AsRef, { @@ -295,33 +306,33 @@ where } } -impl Ord for BorrowedStr<'_> { +impl Ord for BorrowedStr { fn cmp(&self, other: &Self) -> cmp::Ordering { self.buf.cmp(other.buf) } } -impl<'a> TryFrom<&'a LuaString> for BorrowedStr<'a> { +impl TryFrom<&LuaString> for BorrowedStr { type Error = Error; #[inline] - fn try_from(value: &'a LuaString) -> Result { - let BorrowedBytes { buf, borrow, _lua } = BorrowedBytes::from(value); + fn try_from(value: &LuaString) -> Result { + let BorrowedBytes { buf, vref, _lua } = BorrowedBytes::from(value); let buf = str::from_utf8(buf).map_err(|e| Error::from_lua_conversion("string", "&str", e.to_string()))?; - Ok(Self { buf, borrow, _lua }) + Ok(Self { buf, vref, _lua }) } } /// A borrowed byte slice (`&[u8]`) that holds a strong reference to the Lua state. -pub struct BorrowedBytes<'a> { +pub struct BorrowedBytes { // `buf` points to a readonly memory managed by Lua - pub(crate) buf: &'a [u8], - pub(crate) borrow: Cow<'a, LuaString>, + pub(crate) buf: &'static [u8], + pub(crate) vref: ValueRef, pub(crate) _lua: Lua, } -impl Deref for BorrowedBytes<'_> { +impl Deref for BorrowedBytes { type Target = [u8]; #[inline(always)] @@ -330,27 +341,33 @@ impl Deref for BorrowedBytes<'_> { } } -impl Borrow<[u8]> for BorrowedBytes<'_> { +impl Borrow<[u8]> for BorrowedBytes { #[inline(always)] fn borrow(&self) -> &[u8] { self.buf } } -impl AsRef<[u8]> for BorrowedBytes<'_> { +impl AsRef<[u8]> for BorrowedBytes { #[inline(always)] fn as_ref(&self) -> &[u8] { self.buf } } -impl fmt::Debug for BorrowedBytes<'_> { +impl Hash for BorrowedBytes { + fn hash(&self, state: &mut H) { + self.buf.hash(state); + } +} + +impl fmt::Debug for BorrowedBytes { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.buf.fmt(f) } } -impl PartialEq for BorrowedBytes<'_> +impl PartialEq for BorrowedBytes where T: AsRef<[u8]>, { @@ -359,9 +376,9 @@ where } } -impl Eq for BorrowedBytes<'_> {} +impl Eq for BorrowedBytes {} -impl PartialOrd for BorrowedBytes<'_> +impl PartialOrd for BorrowedBytes where T: AsRef<[u8]>, { @@ -370,13 +387,13 @@ where } } -impl Ord for BorrowedBytes<'_> { +impl Ord for BorrowedBytes { fn cmp(&self, other: &Self) -> cmp::Ordering { self.buf.cmp(other.buf) } } -impl<'a> IntoIterator for &'a BorrowedBytes<'_> { +impl<'a> IntoIterator for &'a BorrowedBytes { type Item = &'a u8; type IntoIter = slice::Iter<'a, u8>; @@ -385,12 +402,14 @@ impl<'a> IntoIterator for &'a BorrowedBytes<'_> { } } -impl<'a> From<&'a LuaString> for BorrowedBytes<'a> { +impl From<&LuaString> for BorrowedBytes { #[inline] - fn from(value: &'a LuaString) -> Self { + fn from(value: &LuaString) -> Self { let (buf, _lua) = unsafe { value.to_slice() }; - let borrow = Cow::Borrowed(value); - Self { buf, borrow, _lua } + let vref = value.0.clone(); + // SAFETY: The `buf` is valid for the lifetime of the Lua state and occupied slot index + let buf = unsafe { mem::transmute::<&[u8], &'static [u8]>(buf) }; + Self { buf, vref, _lua } } } diff --git a/src/table.rs b/src/table.rs index 694e9927..e93610ff 100644 --- a/src/table.rs +++ b/src/table.rs @@ -3,12 +3,6 @@ //! Tables are Lua's primary data structure, used for arrays, dictionaries, objects, modules, //! and more. This module provides types for creating and manipulating Lua tables from Rust. //! -//! # Main Types -//! -//! - [`Table`] - A handle to a Lua table. -//! - [`TablePairs`] - An iterator over key-value pairs in a table. -//! - [`TableSequence`] - An iterator over the array (sequence) portion of a table. -//! //! # Basic Operations //! //! Tables support key-value access similar to Rust's `HashMap`: @@ -347,6 +341,54 @@ impl Table { } } + /// Removes a key from the table. + /// + /// If `key` is an integer, mlua shifts down the elements from `table[key+1]`, + /// and erases element `table[key]`. The complexity is `O(n)` in the worst case, + /// where `n` is the table length. + /// + /// For other key types this is equivalent to setting `table[key] = nil`. + /// + /// This might invoke the `__len`, `__index` and `__newindex` metamethods. + /// Use the [`raw_remove`] method if that is not desired. + /// + /// [`raw_remove`]: Table::raw_remove + pub fn remove(&self, key: impl IntoLua) -> Result<()> { + // Fast track (skip protected call) + if !self.has_metatable() { + return self.raw_remove(key); + } + + let lua = self.0.lua.lock(); + let key = key.into_lua(lua.lua())?; + match key { + Value::Integer(idx) => { + let size = self.len()?; + if idx < 1 || idx > size { + return Err(Error::runtime("index out of bounds")); + } + + let state = lua.state(); + unsafe { + let _sg = StackGuard::new(state); + check_stack(state, 4)?; + + lua.push_ref(&self.0); + protect_lua!(state, 1, 0, |state| { + for i in idx..size { + // table[i] = table[i+1] + ffi::lua_geti(state, -1, i + 1); + ffi::lua_seti(state, -2, i); + } + ffi::lua_pushnil(state); + ffi::lua_seti(state, -2, size); + }) + } + } + _ => self.set(key, Nil), + } + } + /// Compares two tables for equality. /// /// Tables are compared by reference first. @@ -570,6 +612,7 @@ impl Table { #[cfg(not(feature = "luau"))] { let state = lua.state(); + let _sg = StackGuard::new(state); check_stack(state, 4)?; lua.push_ref(&self.0); @@ -784,10 +827,8 @@ impl Table { ffi::lua_pushnil(state); while ffi::lua_next(state, -2) != 0 { let k = K::from_stack(-2, &lua)?; - let v = V::from_stack(-1, &lua)?; + let v = lua.pop::()?; f(k, v)?; - // Keep key for next iteration - ffi::lua_pop(state, 1); } } Ok(()) @@ -860,8 +901,7 @@ impl Table { if len.is_none() && t == ffi::LUA_TNIL { break; } - f(V::from_stack(-1, &lua)?)?; - ffi::lua_pop(state, 1); + f(lua.pop::()?)?; } } Ok(()) @@ -944,17 +984,22 @@ impl Table { /// 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 + /// 1. If the table has the array metatable attached, always encode it as an array. + /// + /// 2. 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. + /// 3. If `detect_mixed_tables` is disabled, check if the table has a positive length. 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 { + if self.has_array_metatable() { + return Some(self.raw_len()); + } 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 @@ -964,7 +1009,7 @@ impl Table { } } else { let len = self.raw_len(); - if len > 0 || self.has_array_metatable() { + if len > 0 { return Some(len); } if options.encode_empty_tables_as_array && self.is_empty() { diff --git a/src/thread.rs b/src/thread.rs index 6941dc65..1ce31dc2 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -1,3 +1,40 @@ +//! Lua thread (coroutine) handling. +//! +//! This module provides types for creating and working with Lua coroutines from Rust. +//! Coroutines allow cooperative multitasking within a single Lua state by suspending and +//! resuming execution at well-defined yield points. +//! +//! # Basic Usage +//! +//! Threads are created via [`Lua::create_thread`] and driven by calling [`Thread::resume`]: +//! +//! ```rust +//! # use mlua::{Lua, Result, Thread}; +//! # fn main() -> Result<()> { +//! let lua = Lua::new(); +//! let thread: Thread = lua.load(r#" +//! coroutine.create(function(a, b) +//! coroutine.yield(a + b) +//! return a * b +//! end) +//! "#).eval()?; +//! +//! assert_eq!(thread.resume::((3, 4))?, 7); +//! assert_eq!(thread.resume::(())?, 12); +//! # Ok(()) +//! # } +//! ``` +//! +//! # Async Support +//! +//! When the `async` feature is enabled, a [`Thread`] can be converted into an [`AsyncThread`] +//! via [`Thread::into_async`], which implements both [`Future`] and [`Stream`]. +//! This integrates Lua coroutines naturally with Rust async runtimes such as Tokio. +//! +//! [`Lua::create_thread`]: crate::Lua::create_thread +//! [`Future`]: std::future::Future +//! [`Stream`]: futures_util::stream::Stream + use std::fmt; use std::os::raw::{c_int, c_void}; @@ -26,6 +63,93 @@ use { }, }; +/// Controls which thread lifecycle events trigger the callback. +#[derive(Clone, Copy, Debug, Default)] +#[non_exhaustive] +pub struct ThreadTriggers { + /// Trigger the callback when a new thread is created. + /// + /// On Luau this fires for every thread creation, on other Lua versions only for threads created + /// via [`Lua::create_thread`](crate::Lua::create_thread). + pub on_create: bool, + /// Trigger the callback before a thread is resumed via [`Thread::resume`] (or an async resume + /// driven by mlua). It does not fire for a `coroutine.resume` performed inside Lua code. + pub on_resume: bool, + /// Trigger the callback after a thread yields back to a [`Thread::resume`] driven by mlua. + /// It does not fire for a yield consumed by a `coroutine.resume` inside Lua code. + pub on_yield: bool, +} + +impl ThreadTriggers { + /// An instance of [`ThreadTriggers`] with `on_create` trigger set. + pub const ON_CREATE: Self = Self::new().on_create(); + + /// An instance of [`ThreadTriggers`] with `on_resume` trigger set. + pub const ON_RESUME: Self = Self::new().on_resume(); + + /// An instance of [`ThreadTriggers`] with `on_yield` trigger set. + pub const ON_YIELD: Self = Self::new().on_yield(); + + /// Returns a new instance of `ThreadTriggers` with all triggers disabled. + pub const fn new() -> Self { + Self { + on_create: false, + on_resume: false, + on_yield: false, + } + } + + /// Returns an instance of `ThreadTriggers` with `on_create` trigger set. + #[must_use] + pub const fn on_create(mut self) -> Self { + self.on_create = true; + self + } + + /// Returns an instance of `ThreadTriggers` with `on_resume` trigger set. + #[must_use] + pub const fn on_resume(mut self) -> Self { + self.on_resume = true; + self + } + + /// Returns an instance of `ThreadTriggers` with `on_yield` trigger set. + #[must_use] + pub const fn on_yield(mut self) -> Self { + self.on_yield = true; + self + } +} + +impl std::ops::BitOr for ThreadTriggers { + type Output = Self; + + fn bitor(mut self, rhs: Self) -> Self::Output { + self.on_create |= rhs.on_create; + self.on_resume |= rhs.on_resume; + self.on_yield |= rhs.on_yield; + self + } +} + +impl std::ops::BitOrAssign for ThreadTriggers { + fn bitor_assign(&mut self, rhs: Self) { + *self = *self | rhs; + } +} + +/// Represents a thread (coroutine) event. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum ThreadEvent { + /// A new thread was created. + Create(Thread), + /// A thread is about to be resumed via [`Thread::resume`]. + Resume(Thread), + /// A thread has just yielded. + Yield(Thread), +} + /// Status of a Lua thread (coroutine). #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum ThreadStatus { @@ -35,6 +159,11 @@ pub enum ThreadStatus { Resumable, /// The thread is currently running. Running, + /// The thread is active but not running. + /// + /// This is the case when the thread has resumed another thread (which has not yet + /// returned or yielded). + Normal, /// The thread has finished executing. Finished, /// The thread has raised a Lua error during execution. @@ -49,19 +178,13 @@ pub enum ThreadStatus { enum ThreadStatusInner { New(c_int), Running, + Normal, Yielded(c_int), Finished, Error, } impl ThreadStatusInner { - #[cfg(feature = "async")] - #[inline(always)] - fn is_resumable(self) -> bool { - matches!(self, ThreadStatusInner::New(_) | ThreadStatusInner::Yielded(_)) - } - - #[cfg(feature = "async")] #[inline(always)] fn is_yielded(self) -> bool { matches!(self, ThreadStatusInner::Yielded(_)) @@ -69,7 +192,7 @@ impl ThreadStatusInner { } /// Handle to an internal Lua thread (coroutine). -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct Thread(pub(crate) ValueRef, pub(crate) *mut ffi::lua_State); #[cfg(feature = "send")] @@ -90,9 +213,61 @@ pub struct AsyncThread { recycle: bool, } +pub(crate) struct ThreadEventGuard<'a> { + lua: &'a RawLua, + prev_state: *mut ffi::lua_State, +} + +impl<'a> ThreadEventGuard<'a> { + #[inline] + pub(crate) unsafe fn new(lua: &'a RawLua, thread_state: *mut ffi::lua_State) -> Self { + let guard = ThreadEventGuard { + lua, + prev_state: lua.thread_event_state(), + }; + lua.set_thread_event_state(thread_state); + guard + } +} + +impl Drop for ThreadEventGuard<'_> { + #[inline] + fn drop(&mut self) { + unsafe { self.lua.set_thread_event_state(self.prev_state) }; + } +} + +#[inline] +fn check_thread_reentrancy(thread_state: *mut ffi::lua_State, lua: &RawLua) -> Result<()> { + if thread_state == unsafe { lua.thread_event_state() } { + let err = "cannot resume or reset a thread from within its own event callback"; + return Err(Error::runtime(err)); + } + Ok(()) +} + +#[inline] +unsafe fn exec_thread_event( + lua: &RawLua, + enabled: bool, + thread_state: *mut ffi::lua_State, + event: impl FnOnce() -> ThreadEvent, +) -> Result { + if enabled + && lua.thread_event_state().is_null() + && let Some(cb) = lua.thread_event_callback() + { + let _guard = ThreadEventGuard::new(lua, thread_state); + cb(lua.lua(), event())?; + return Ok(true); + } + Ok(false) +} + impl Thread { - /// Returns reference to the Lua state that this thread is associated with. - #[doc(hidden)] + /// Returns the raw pointer to the Lua state that this thread is associated with. + /// + /// The pointer is valid only while this [`Thread`] is alive. #[inline(always)] pub fn state(&self) -> *mut ffi::lua_State { self.1 @@ -147,28 +322,45 @@ impl Thread { R: FromLuaMulti, { let lua = self.0.lua.lock(); - let mut pushed_nargs = match self.status_inner(&lua) { - ThreadStatusInner::New(nargs) | ThreadStatusInner::Yielded(nargs) => nargs, - _ => return Err(Error::CoroutineUnresumable), - }; + check_thread_reentrancy(self.state(), &lua)?; + let (mut pushed_nargs, mut hook_yielded) = self.resumable_state(&lua)?; let state = lua.state(); let thread_state = self.state(); unsafe { let _sg = StackGuard::new(state); - let nargs = args.push_into_stack_multi(&lua)?; - if nargs > 0 { - check_stack(thread_state, nargs)?; - ffi::lua_xmove(state, thread_state, nargs); - pushed_nargs += nargs; + // If the resume callback runs, it may touch this thread, so re-read the argument count + let on_resume = lua.thread_event_triggers().on_resume; + if exec_thread_event(&lua, on_resume, thread_state, || { + ThreadEvent::Resume(self.clone()) + })? { + (pushed_nargs, hook_yielded) = self.resumable_state(&lua)?; + } + + if !hook_yielded { + let nargs = args.push_into_stack_multi(&lua)?; + if nargs > 0 { + check_stack(thread_state, nargs)?; + ffi::lua_xmove(state, thread_state, nargs); + pushed_nargs += nargs; + } + } + + let mut thread_sg = StackGuard::with_top(thread_state, 0); + let (status, nresults) = self.resume_inner(&lua, pushed_nargs)?; + if status.is_yielded() && self.is_hook_yielded(&lua) { + debug_assert_eq!(nresults, 0); + thread_sg.keep(ffi::lua_gettop(thread_state)); } - 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); + // Exec thread yield callback + let on_yield = lua.thread_event_triggers().on_yield && status.is_yielded(); + exec_thread_event(&lua, on_yield, thread_state, || ThreadEvent::Yield(self.clone()))?; + R::from_stack_multi(nresults, &lua) } } @@ -176,13 +368,14 @@ impl Thread { /// Resumes execution of this thread, immediately raising an error. /// /// This is a Luau specific extension. - #[cfg(feature = "luau")] + #[cfg(any(feature = "luau", doc))] #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] pub fn resume_error(&self, error: impl crate::IntoLua) -> Result where R: FromLuaMulti, { let lua = self.0.lua.lock(); + check_thread_reentrancy(self.state(), &lua)?; match self.status_inner(&lua) { ThreadStatusInner::New(_) | ThreadStatusInner::Yielded(_) => {} _ => return Err(Error::CoroutineUnresumable), @@ -193,15 +386,26 @@ impl Thread { unsafe { let _sg = StackGuard::new(state); + // Exec thread resume callback + let on_resume = lua.thread_event_triggers().on_resume; + exec_thread_event(&lua, on_resume, thread_state, || { + ThreadEvent::Resume(self.clone()) + })?; + 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)?; + let (status, nresults) = self.resume_inner(&lua, ffi::LUA_RESUMEERROR)?; + check_stack(state, nresults + 1)?; ffi::lua_xmove(thread_state, state, nresults); + // Exec thread yield callback + let on_yield = lua.thread_event_triggers().on_yield && status.is_yielded(); + exec_thread_event(&lua, on_yield, thread_state, || ThreadEvent::Yield(self.clone()))?; + R::from_stack_multi(nresults, &lua) } } @@ -237,6 +441,7 @@ impl Thread { match self.status_inner(&self.0.lua.lock()) { ThreadStatusInner::New(_) | ThreadStatusInner::Yielded(_) => ThreadStatus::Resumable, ThreadStatusInner::Running => ThreadStatus::Running, + ThreadStatusInner::Normal => ThreadStatus::Normal, ThreadStatusInner::Finished => ThreadStatus::Finished, ThreadStatusInner::Error => ThreadStatus::Error, } @@ -253,15 +458,81 @@ impl Thread { let top = unsafe { ffi::lua_gettop(thread_state) }; match status { ffi::LUA_YIELD => ThreadStatusInner::Yielded(top), - ffi::LUA_OK if top > 0 => ThreadStatusInner::New(top - 1), - ffi::LUA_OK => ThreadStatusInner::Finished, + ffi::LUA_OK => { + // Active call frames mean this thread has resumed another (still-running) thread. + // Without frames it's new or finished. + let mut ar = const { unsafe { std::mem::zeroed::() } }; + #[cfg(not(feature = "luau"))] + let has_frames = unsafe { ffi::lua_getstack(thread_state, 0, &mut ar) != 0 }; + #[cfg(feature = "luau")] + let has_frames = unsafe { ffi::lua_getinfo(thread_state, 0, cstr!(""), &mut ar) != 0 }; + if has_frames { + ThreadStatusInner::Normal + } else if top > 0 { + ThreadStatusInner::New(top - 1) + } else { + ThreadStatusInner::Finished + } + } _ => ThreadStatusInner::Error, } } + /// Returns the pending argument count and whether the thread was interrupted by a hook. + #[inline] + fn resumable_state(&self, lua: &RawLua) -> Result<(c_int, bool)> { + match self.status_inner(lua) { + ThreadStatusInner::New(nargs) => Ok((nargs, false)), + ThreadStatusInner::Yielded(nargs) => { + let hook_yielded = self.is_hook_yielded(lua); + Ok((if hook_yielded { 0 } else { nargs }, hook_yielded)) + } + _ => Err(Error::CoroutineUnresumable), + } + } + + /// Distinguishes a hook interruption from a normal yield. + fn is_hook_yielded(&self, lua: &RawLua) -> bool { + unsafe { lua.is_hook_yielded(self.state()) } + } + + /// Returns `true` if this thread is resumable (meaning it can be resumed by calling + /// [`Thread::resume`]). + #[inline(always)] + pub fn is_resumable(&self) -> bool { + self.status() == ThreadStatus::Resumable + } + + /// Returns `true` if this thread is currently running. + #[inline(always)] + pub fn is_running(&self) -> bool { + self.status() == ThreadStatus::Running + } + + /// Returns `true` if this thread is active but not running. + /// + /// This is the case when the thread has resumed another thread that has not yet returned + /// or yielded. + #[inline(always)] + pub fn is_normal(&self) -> bool { + self.status() == ThreadStatus::Normal + } + + /// Returns `true` if this thread has finished executing. + #[inline(always)] + pub fn is_finished(&self) -> bool { + self.status() == ThreadStatus::Finished + } + + /// Returns `true` if this thread has raised a Lua error during execution. + #[inline(always)] + pub fn is_error(&self) -> bool { + self.status() == ThreadStatus::Error + } + /// Sets a hook function that will periodically be called as Lua code executes. /// - /// This function is similar or [`Lua::set_hook`] except that it sets for the thread. + /// This function is similar to [`Lua::set_hook`] except that it sets the hook for the thread. /// You can have multiple hooks for different threads. /// /// To remove a hook call [`Thread::remove_hook`]. @@ -286,16 +557,16 @@ impl Thread { #[cfg(not(feature = "luau"))] #[cfg_attr(docsrs, doc(cfg(not(feature = "luau"))))] pub fn remove_hook(&self) { - let _lua = self.0.lua.lock(); + let lua = self.0.lua.lock(); unsafe { - ffi::lua_sethook(self.state(), None, 0, 0); + lua.remove_thread_hook(self.state()); } } /// Resets a thread /// /// In [Lua 5.4]: cleans its call stack and closes all pending to-be-closed variables. - /// Returns a error in case of either the original error that stopped the thread or errors + /// Returns an error in case of either the original error that stopped the thread or errors /// in closing methods. /// /// In Luau: resets to the initial state of a newly created Lua thread. @@ -308,6 +579,7 @@ impl Thread { /// [Lua 5.4]: https://www.lua.org/manual/5.4/manual.html#lua_closethread pub fn reset(&self, func: Function) -> Result<()> { let lua = self.0.lua.lock(); + check_thread_reentrancy(self.state(), &lua)?; let thread_state = self.state(); unsafe { let status = self.status_inner(&lua); @@ -335,6 +607,7 @@ impl Thread { Ok(()) } ThreadStatusInner::Running => Err(Error::runtime("cannot reset a running thread")), + ThreadStatusInner::Normal => Err(Error::runtime("cannot reset a normal thread")), ThreadStatusInner::Finished => Ok(()), #[cfg(not(any(feature = "lua55", feature = "lua54", feature = "luau")))] ThreadStatusInner::Yielded(_) | ThreadStatusInner::Error => { @@ -416,19 +689,20 @@ impl Thread { R: FromLuaMulti, { let lua = self.0.lua.lock(); - if !self.status_inner(&lua).is_resumable() { - return Err(Error::CoroutineUnresumable); - } + check_thread_reentrancy(self.state(), &lua)?; + let (_, hook_yielded) = self.resumable_state(&lua)?; let state = lua.state(); let thread_state = self.state(); unsafe { let _sg = StackGuard::new(state); - let nargs = args.push_into_stack_multi(&lua)?; - if nargs > 0 { - check_stack(thread_state, nargs)?; - ffi::lua_xmove(state, thread_state, nargs); + if !hook_yielded { + let nargs = args.push_into_stack_multi(&lua)?; + if nargs > 0 { + check_stack(thread_state, nargs)?; + ffi::lua_xmove(state, thread_state, nargs); + } } Ok(AsyncThread { @@ -449,6 +723,8 @@ impl Thread { /// Please note that Luau links environment table with chunk when loading it into Lua state. /// Therefore you need to load chunks into a thread to link with the thread environment. /// + /// [`Lua::sandbox`]: crate::Lua::sandbox + /// /// # Examples /// /// ``` @@ -502,12 +778,6 @@ impl fmt::Debug for Thread { } } -impl PartialEq for Thread { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - impl LuaType for Thread { const TYPE_ID: c_int = ffi::LUA_TTHREAD; } @@ -518,30 +788,35 @@ impl AsyncThread { pub(crate) fn set_recyclable(&mut self, recyclable: bool) { self.recycle = recyclable; } + + #[inline(always)] + pub(crate) fn thread(&self) -> &Thread { + &self.thread + } } #[cfg(feature = "async")] impl Drop for AsyncThread { fn drop(&mut self) { - #[allow(clippy::collapsible_if)] - if self.recycle { - if let Some(lua) = self.thread.0.lua.try_lock() { - unsafe { - let mut status = self.thread.status_inner(&lua); - if matches!(status, ThreadStatusInner::Yielded(0)) { - // The thread is dropped while yielded, resume it with the "terminate" signal - ffi::lua_pushlightuserdata(self.thread.1, crate::Lua::poll_terminate().0); - if let Ok((new_status, _)) = self.thread.resume_inner(&lua, 1) { - // `new_status` should always be `ThreadStatusInner::Yielded(0)` - status = new_status; - } + if self.recycle + && let Some(lua) = self.thread.0.lua.try_lock() + { + unsafe { + let mut status = self.thread.status_inner(&lua); + if matches!(status, ThreadStatusInner::Yielded(0)) && !self.thread.is_hook_yielded(&lua) { + // The thread is dropped while yielded, resume it with the "terminate" signal + ffi::lua_pushlightuserdata(self.thread.1, crate::Lua::poll_terminate().0); + if let Ok((new_status, _)) = self.thread.resume_inner(&lua, 1) { + // `new_status` should always be `ThreadStatusInner::Yielded(0)` + status = new_status; } + } - // For Lua 5.4 this also closes all pending to-be-closed variables - if self.thread.reset_inner(status).is_ok() { - lua.recycle_thread(&mut self.thread); - } + // For Lua 5.4 this also closes all pending to-be-closed variables + if self.thread.reset_inner(status).is_ok() { + lua.recycle_thread(&mut self.thread); } + lua.update_thread_ownership(&self.thread, None); } } } @@ -553,31 +828,58 @@ impl Stream for AsyncThread { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let lua = self.thread.0.lua.lock(); - let nargs = match self.thread.status_inner(&lua) { - ThreadStatusInner::New(nargs) | ThreadStatusInner::Yielded(nargs) => nargs, - _ => return Poll::Ready(None), + check_thread_reentrancy(self.thread.state(), &lua)?; + let mut nargs = match self.thread.resumable_state(&lua) { + Ok((nargs, _)) => nargs, + Err(_) => return Poll::Ready(None), }; let state = lua.state(); let thread_state = self.thread.state(); unsafe { let _sg = StackGuard::new(state); - let _thread_sg = StackGuard::with_top(thread_state, 0); + let mut thread_sg = StackGuard::with_top(thread_state, 0); let _wg = WakerGuard::new(&lua, cx.waker()); + // If the resume callback runs, it may touch this thread, so re-read the argument count + let on_resume = lua.thread_event_triggers().on_resume; + if exec_thread_event(&lua, on_resume, thread_state, || { + ThreadEvent::Resume(self.thread.clone()) + })? { + nargs = match self.thread.resumable_state(&lua) { + Ok((nargs, _)) => nargs, + Err(_) => return Poll::Ready(None), + }; + } + let (status, nresults) = (self.thread).resume_inner(&lua, nargs)?; + let hook_yielded = status.is_yielded() && self.thread.is_hook_yielded(&lua); + if hook_yielded { + debug_assert_eq!(nresults, 0); + thread_sg.keep(ffi::lua_gettop(thread_state)); + } - if status.is_yielded() { - if nresults == 1 && is_poll_pending(thread_state) { - return Poll::Pending; - } - // Continue polling - cx.waker().wake_by_ref(); + if status.is_yielded() && !hook_yielded && nresults == 1 && is_poll_pending(thread_state) { + // Exec thread yield callback + let on_yield = lua.thread_event_triggers().on_yield; + exec_thread_event(&lua, on_yield, thread_state, || { + ThreadEvent::Yield(self.thread.clone()) + })?; + return Poll::Pending; } check_stack(state, nresults + 1)?; ffi::lua_xmove(thread_state, state, nresults); + if status.is_yielded() { + let on_yield = lua.thread_event_triggers().on_yield; + exec_thread_event(&lua, on_yield, thread_state, || { + ThreadEvent::Yield(self.thread.clone()) + })?; + // Continue polling + cx.waker().wake_by_ref(); + } + Poll::Ready(Some(R::from_stack_multi(nresults, &lua))) } } @@ -589,22 +891,41 @@ impl Future for AsyncThread { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let lua = self.thread.0.lua.lock(); - let nargs = match self.thread.status_inner(&lua) { - ThreadStatusInner::New(nargs) | ThreadStatusInner::Yielded(nargs) => nargs, - _ => return Poll::Ready(Err(Error::CoroutineUnresumable)), - }; + check_thread_reentrancy(self.thread.state(), &lua)?; + let (mut nargs, _) = self.thread.resumable_state(&lua)?; let state = lua.state(); let thread_state = self.thread.state(); unsafe { let _sg = StackGuard::new(state); - let _thread_sg = StackGuard::with_top(thread_state, 0); + let mut thread_sg = StackGuard::with_top(thread_state, 0); let _wg = WakerGuard::new(&lua, cx.waker()); + // If the resume callback runs, it may touch this thread, so re-read the argument count + let on_resume = lua.thread_event_triggers().on_resume; + if exec_thread_event(&lua, on_resume, thread_state, || { + ThreadEvent::Resume(self.thread.clone()) + })? { + (nargs, _) = self.thread.resumable_state(&lua)?; + } + let (status, nresults) = self.thread.resume_inner(&lua, nargs)?; + let hook_yielded = status.is_yielded() && self.thread.is_hook_yielded(&lua); + if hook_yielded { + debug_assert_eq!(nresults, 0); + thread_sg.keep(ffi::lua_gettop(thread_state)); + } if status.is_yielded() { - if !(nresults == 1 && is_poll_pending(thread_state)) { + let pending = !hook_yielded && nresults == 1 && is_poll_pending(thread_state); + + // Exec thread yield callback + let on_yield = lua.thread_event_triggers().on_yield; + exec_thread_event(&lua, on_yield, thread_state, || { + ThreadEvent::Yield(self.thread.clone()) + })?; + + if !pending { // Ignore values returned via yield() cx.waker().wake_by_ref(); } diff --git a/src/traits.rs b/src/traits.rs index 93429108..405a95f7 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,3 +1,8 @@ +//! Core conversion and extension traits. +//! +//! This module provides the fundamental traits for converting values between Rust and Lua, +//! and for defining native Lua callable functions. + use std::os::raw::c_int; use std::sync::Arc; @@ -5,12 +10,11 @@ use crate::error::{Error, Result}; use crate::multi::MultiValue; use crate::private::Sealed; use crate::state::{Lua, RawLua, WeakLua}; -use crate::types::MaybeSend; use crate::util::{check_stack, parse_lookup_path, short_type_name}; use crate::value::Value; #[cfg(feature = "async")] -use {crate::function::AsyncCallFuture, std::future::Future}; +use crate::function::AsyncCallFuture; /// Trait for types convertible to [`Value`]. pub trait IntoLua: Sized { @@ -245,97 +249,6 @@ pub trait ObjectLike: Sealed { fn weak_lua(&self) -> &WeakLua; } -/// A trait for types that can be used as Lua functions. -pub trait LuaNativeFn { - type Output: IntoLuaMulti; - - fn call(&self, args: A) -> Self::Output; -} - -/// A trait for types with mutable state that can be used as Lua functions. -pub trait LuaNativeFnMut { - type Output: IntoLuaMulti; - - fn call(&mut self, args: A) -> Self::Output; -} - -/// A trait for types that returns a future and can be used as Lua functions. -#[cfg(feature = "async")] -pub trait LuaNativeAsyncFn { - type Output: IntoLuaMulti; - - fn call(&self, args: A) -> impl Future + MaybeSend + 'static; -} - -macro_rules! impl_lua_native_fn { - ($($A:ident),*) => { - impl LuaNativeFn<($($A,)*)> for FN - where - FN: Fn($($A,)*) -> R + MaybeSend + 'static, - ($($A,)*): FromLuaMulti, - R: IntoLuaMulti, - { - type Output = R; - - #[allow(non_snake_case)] - fn call(&self, args: ($($A,)*)) -> Self::Output { - let ($($A,)*) = args; - self($($A,)*) - } - } - - impl LuaNativeFnMut<($($A,)*)> for FN - where - FN: FnMut($($A,)*) -> R + MaybeSend + 'static, - ($($A,)*): FromLuaMulti, - R: IntoLuaMulti, - { - type Output = R; - - #[allow(non_snake_case)] - fn call(&mut self, args: ($($A,)*)) -> Self::Output { - let ($($A,)*) = args; - self($($A,)*) - } - } - - #[cfg(feature = "async")] - impl LuaNativeAsyncFn<($($A,)*)> for FN - where - FN: Fn($($A,)*) -> Fut + MaybeSend + 'static, - ($($A,)*): FromLuaMulti, - Fut: Future + MaybeSend + 'static, - R: IntoLuaMulti, - { - type Output = R; - - #[allow(non_snake_case)] - fn call(&self, args: ($($A,)*)) -> impl Future + MaybeSend + 'static { - let ($($A,)*) = args; - self($($A,)*) - } - } - }; -} - -impl_lua_native_fn!(); -impl_lua_native_fn!(A); -impl_lua_native_fn!(A, B); -impl_lua_native_fn!(A, B, C); -impl_lua_native_fn!(A, B, C, D); -impl_lua_native_fn!(A, B, C, D, E); -impl_lua_native_fn!(A, B, C, D, E, F); -impl_lua_native_fn!(A, B, C, D, E, F, G); -impl_lua_native_fn!(A, B, C, D, E, F, G, H); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); -impl_lua_native_fn!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); - pub(crate) trait ShortTypeName { #[inline(always)] fn type_name() -> String { diff --git a/src/types.rs b/src/types.rs index d05d35c2..6c7704db 100644 --- a/src/types.rs +++ b/src/types.rs @@ -20,9 +20,6 @@ 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. @@ -70,6 +67,7 @@ pub(crate) type AsyncCallbackUpvalue = Upvalue; pub(crate) type AsyncPollUpvalue = Upvalue>>>; /// Type to set next Lua VM action after executing interrupt or hook function. +#[non_exhaustive] pub enum VmState { Continue, /// Yield the current thread. @@ -96,17 +94,11 @@ pub(crate) type InterruptCallback = XRc Result + Send>; #[cfg(all(not(feature = "send"), feature = "luau"))] pub(crate) type InterruptCallback = XRc Result>; -#[cfg(all(feature = "send", feature = "luau"))] -pub(crate) type ThreadCreationCallback = XRc Result<()> + Send>; - -#[cfg(all(not(feature = "send"), feature = "luau"))] -pub(crate) type ThreadCreationCallback = XRc Result<()>>; - -#[cfg(all(feature = "send", feature = "luau"))] -pub(crate) type ThreadCollectionCallback = XRc; +#[cfg(feature = "send")] +pub(crate) type ThreadEventCallback = XRc Result<()> + Send>; -#[cfg(all(not(feature = "send"), feature = "luau"))] -pub(crate) type ThreadCollectionCallback = XRc; +#[cfg(not(feature = "send"))] +pub(crate) type ThreadEventCallback = XRc Result<()>>; #[cfg(feature = "send")] #[cfg(any(feature = "lua55", feature = "lua54"))] @@ -128,6 +120,22 @@ pub trait MaybeSend {} #[cfg(not(feature = "send"))] impl MaybeSend for T {} +/// Adds a `Sync` requirement to userdata types when the `send` feature is enabled. +/// +/// It is automatically implemented for all applicable types. +#[cfg(feature = "send")] +pub trait MaybeSync: Sync {} +#[cfg(feature = "send")] +impl MaybeSync for T {} + +/// Adds a `Sync` requirement to userdata types when the `send` feature is enabled. +/// +/// It is automatically implemented for all applicable types. +#[cfg(not(feature = "send"))] +pub trait MaybeSync {} +#[cfg(not(feature = "send"))] +impl MaybeSync for T {} + pub(crate) struct DestructedUserdata; pub(crate) trait LuaType { diff --git a/src/types/value_ref.rs b/src/types/value_ref.rs index 564c28f0..a6551ebc 100644 --- a/src/types/value_ref.rs +++ b/src/types/value_ref.rs @@ -1,42 +1,34 @@ -use std::fmt; use std::os::raw::{c_int, c_void}; +use std::{fmt, ptr}; use super::XRc; use crate::state::{RawLua, WeakLua}; +use self::ref_count::RefCount; + /// 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, - // If `index_count` is `None`, the value does not need to be destroyed. - pub(crate) index_count: Option, -} - -/// 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); - -impl From for ValueRefIndex { - #[inline] - fn from(index: c_int) -> Self { - ValueRefIndex(XRc::new(index)) - } + count: RefCount, } impl ValueRef { #[inline] - pub(crate) fn new(lua: &RawLua, index: impl Into) -> Self { - let index = index.into(); + pub(crate) fn new(lua: &RawLua, index: c_int) -> Self { ValueRef { lua: lua.weak().clone(), - index: *index.0, - index_count: Some(index), + index, + count: RefCount::unique(), } } + #[cfg(feature = "async")] + #[inline] + pub(crate) fn take_index(&mut self) -> Option { + self.count.take(self.index) + } + #[inline] pub(crate) fn to_pointer(&self) -> *const c_void { let lua = self.lua.lock(); @@ -44,6 +36,17 @@ impl ValueRef { } } +impl Clone for ValueRef { + #[inline] + fn clone(&self) -> Self { + ValueRef { + lua: self.lua.clone(), + index: self.index, + count: self.count.clone_shared(), + } + } +} + impl fmt::Debug for ValueRef { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Ref({:p})", self.to_pointer()) @@ -52,14 +55,10 @@ impl fmt::Debug for ValueRef { impl Drop for ValueRef { fn drop(&mut 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() - && let Some(lua) = self.lua.try_lock() - { - unsafe { lua.drop_ref(self) } - } + if self.count.drop_is_last() + && let Some(lua) = self.lua.try_lock() + { + unsafe { lua.drop_ref(self) } } } } @@ -74,3 +73,154 @@ impl PartialEq for ValueRef { unsafe { ffi::lua_rawequal(lua.ref_thread(), self.index, other.index) == 1 } } } + +// The counter is a pure refcount token. +type Unit = (); +const UNIQUE: *mut Unit = ptr::without_provenance_mut(1); +const NONE: *mut Unit = ptr::null_mut(); + +impl RefCount { + #[inline] + fn unique() -> Self { + Self::from_raw(UNIQUE) + } + + #[inline] + fn clone_shared(&self) -> RefCount { + let mut current = self.load(); + loop { + if current != UNIQUE { + if current != NONE { + unsafe { XRc::increment_strong_count(current as *const Unit) }; + } + return RefCount::from_raw(current); + } + // Lazily allocate the shared counter + let shared = XRc::into_raw(XRc::new(())) as *mut Unit; + match self.promote(shared) { + Ok(()) => { + unsafe { XRc::increment_strong_count(shared as *const Unit) }; + return RefCount::from_raw(shared); + } + Err(actual) => { + unsafe { drop(XRc::from_raw(shared as *const Unit)) }; + current = actual; + } + } + } + } + + /// Takes the slot if it's solely owned by `self`. + /// + /// Returns `None` if the slot is still shared or already non-owning. + #[cfg(feature = "async")] + fn take(&mut self, index: c_int) -> Option { + match self.load() { + current if current == UNIQUE => { + self.swap_none(); + Some(index) + } + current if current == NONE => None, + current => { + // Shared: recyclable only if no other owner remains + let rc = unsafe { XRc::from_raw(current as *const Unit) }; + if XRc::strong_count(&rc) == 1 { + drop(rc); + self.swap_none(); + Some(index) + } else { + let _ = XRc::into_raw(rc); // still shared + None + } + } + } + } + + /// Drops the reference and returns `true` if it was the last owner of the slot + /// (so the slot must be freed). + #[inline] + fn drop_is_last(&mut self) -> bool { + let current = self.load(); + if current == UNIQUE { + true + } else if current == NONE { + false + } else { + unsafe { XRc::into_inner(XRc::from_raw(current as *const Unit)).is_some() } + } + } +} + +#[cfg(feature = "send")] +mod ref_count { + use std::sync::atomic::{AtomicPtr, Ordering}; + + use super::Unit; + + pub(super) struct RefCount(AtomicPtr); + + impl RefCount { + #[inline] + pub(super) fn from_raw(ptr: *mut Unit) -> Self { + RefCount(AtomicPtr::new(ptr)) + } + + #[inline] + pub(super) fn load(&self) -> *mut Unit { + self.0.load(Ordering::Acquire) + } + + /// Replaces the `UNIQUE` tag with the freshly allocated shared counter (`new`). + /// + /// Returns `Err(current)` if another thread promoted first. + #[inline] + pub(super) fn promote(&self, new: *mut Unit) -> Result<(), *mut Unit> { + self.0 + .compare_exchange(super::UNIQUE, new, Ordering::AcqRel, Ordering::Acquire) + .map(|_| ()) + } + + #[cfg(feature = "async")] + #[inline] + pub(super) fn swap_none(&self) -> *mut Unit { + self.0.swap(super::NONE, Ordering::AcqRel) + } + } +} + +#[cfg(not(feature = "send"))] +mod ref_count { + use std::cell::Cell; + + use super::Unit; + + pub(super) struct RefCount(Cell<*mut Unit>); + + impl RefCount { + #[inline] + pub(super) fn from_raw(ptr: *mut Unit) -> Self { + RefCount(Cell::new(ptr)) + } + + #[inline] + pub(super) fn load(&self) -> *mut Unit { + self.0.get() + } + + /// Replaces the `UNIQUE` tag with the freshly allocated shared counter `new`. + /// + /// Never fails. + #[inline] + pub(super) fn promote(&self, new: *mut Unit) -> Result<(), *mut Unit> { + debug_assert_eq!(self.0.get(), super::UNIQUE); + self.0.set(new); + Ok(()) + } + + #[cfg(feature = "async")] + #[inline] + pub(super) fn swap_none(&self) -> *mut Unit { + self.0.replace(super::NONE) + } + } +} diff --git a/src/userdata.rs b/src/userdata.rs index c8d8c19b..912efa22 100644 --- a/src/userdata.rs +++ b/src/userdata.rs @@ -1,16 +1,21 @@ +//! Lua userdata handling. +//! +//! This module provides types for creating and working with Lua userdata from Rust. + use std::any::TypeId; use std::ffi::CStr; use std::fmt; use std::hash::Hash; use std::os::raw::{c_char, c_void}; +use crate::Either; use crate::error::{Error, Result}; use crate::function::Function; use crate::state::Lua; use crate::string::LuaString; use crate::table::{Table, TablePairs}; use crate::traits::{FromLua, FromLuaMulti, IntoLua, IntoLuaMulti}; -use crate::types::{MaybeSend, ValueRef}; +use crate::types::{MaybeSend, MaybeSync, ValueRef}; use crate::util::{StackGuard, check_stack, get_userdata, push_string, short_type_name, take_userdata}; use crate::value::Value; @@ -25,7 +30,7 @@ use { // Re-export for convenience pub(crate) use cell::UserDataStorage; -pub use r#ref::{UserDataRef, UserDataRefMut}; +pub use r#ref::{UserDataOwned, UserDataRef, UserDataRefMut}; pub use registry::UserDataRegistry; pub(crate) use registry::{RawUserDataRegistry, UserDataProxy}; pub(crate) use util::{ @@ -123,6 +128,11 @@ pub enum MetaMethod { /// /// This is not an operator, but will be called by methods such as `tostring` and `print`. ToString, + /// The `__todebugstring` metamethod for debug purposes. + /// + /// This is an mlua-specific metamethod that can be used to provide debug representation for + /// userdata. + ToDebugString, /// The `__pairs` metamethod. /// /// This is not an operator, but it will be called by the built-in `pairs` function. @@ -232,6 +242,7 @@ impl MetaMethod { MetaMethod::NewIndex => "__newindex", MetaMethod::Call => "__call", MetaMethod::ToString => "__tostring", + MetaMethod::ToDebugString => "__todebugstring", #[cfg(any( feature = "lua55", @@ -264,8 +275,7 @@ impl MetaMethod { pub(crate) fn validate(name: &str) -> Result<&str> { match name { - "__gc" => Err(Error::MetaMethodRestricted(name.to_string())), - "__metatable" => Err(Error::MetaMethodRestricted(name.to_string())), + "__gc" | "__metatable" => Err(Error::MetaMethodRestricted(name.to_string())), _ if name.starts_with("__mlua") => Err(Error::MetaMethodRestricted(name.to_string())), name => Ok(name), } @@ -316,9 +326,8 @@ pub trait UserDataMethods { /// The userdata `T` will be moved out of the userdata container. This is useful for /// methods that need to consume the userdata. /// - /// The method can be called only once per userdata instance, subsequent calls will result in a - /// [`Error::UserDataDestructed`] error. - #[doc(hidden)] + /// The method can be called only once per userdata instance. A subsequent call returns an + /// [`Error::BadArgument`] for `self` whose cause is [`Error::UserDataDestructed`]. fn add_method_once(&mut self, name: impl Into, method: M) where T: 'static, @@ -369,11 +378,10 @@ pub trait UserDataMethods { /// The userdata `T` will be moved out of the userdata container. This is useful for /// methods that need to consume the userdata. /// - /// The method can be called only once per userdata instance, subsequent calls will result in a - /// [`Error::UserDataDestructed`] error. + /// The method can be called only once per userdata instance. A subsequent call returns an + /// [`Error::BadArgument`] for `self` whose cause is [`Error::UserDataDestructed`]. #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] - #[doc(hidden)] fn add_async_method_once(&mut self, name: impl Into, method: M) where T: 'static, @@ -482,7 +490,10 @@ pub trait UserDataMethods { /// /// [`add_meta_method_mut`]: UserDataMethods::add_meta_method_mut #[cfg(all(feature = "async", not(any(feature = "lua51", feature = "luau"))))] - #[cfg_attr(docsrs, doc(cfg(feature = "async")))] + #[cfg_attr( + docsrs, + doc(cfg(all(feature = "async", not(any(feature = "lua51", feature = "luau"))))) + )] fn add_async_meta_method_mut(&mut self, name: impl Into, method: M) where T: 'static, @@ -705,7 +716,7 @@ pub trait UserData: Sized { /// /// [`is`]: crate::AnyUserData::is /// [`borrow`]: crate::AnyUserData::borrow -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, PartialEq)] pub struct AnyUserData(pub(crate) ValueRef); impl AnyUserData { @@ -914,7 +925,7 @@ impl AnyUserData { } ffi::lua_rawgeti(state, -1, n as ffi::lua_Integer); - V::from_lua(lua.pop_value(), lua.lua()) + V::from_stack(-1, &lua) } } @@ -1020,8 +1031,8 @@ impl AnyUserData { /// Returns a type name of this userdata (from a metatable field). /// - /// If no type name is set, returns `None`. - pub fn type_name(&self) -> Result> { + /// If no type name is set, returns `userdata`. + pub fn type_name(&self) -> Result { let lua = self.0.lua.lock(); let state = lua.state(); unsafe { @@ -1038,8 +1049,8 @@ impl AnyUserData { ffi::luaL_getmetafield(state, -1, MetaMethod::Type.as_cstr().as_ptr()) }; match name_type { - ffi::LUA_TSTRING => Ok(Some(LuaString(lua.pop_ref()).to_str()?.to_owned())), - _ => Ok(None), + ffi::LUA_TSTRING => Ok(LuaString(lua.pop_ref())), + _ => lua.create_string(b"userdata"), } } } @@ -1055,8 +1066,8 @@ impl AnyUserData { return Ok(false); } - if mt.contains_key("__eq")? { - return mt.get::("__eq")?.call((self, other)); + if let Some(eq) = mt.get::>("__eq")? { + return eq.call((self, other)); } Ok(false) @@ -1071,10 +1082,52 @@ impl AnyUserData { // Userdata must be registered and not destructed let _ = lua.get_userdata_ref_type_id(&self.0)?; let ud = &*get_userdata::>(lua.ref_thread(), self.0.index); - Ok::<_, Error>((*ud).is_serializable()) + Ok::<_, Error>(ud.is_serializable()) }; is_serializable().unwrap_or(false) } + + unsafe fn invoke_tostring_dbg(&self) -> Result> { + let lua = self.0.lua.lock(); + let state = lua.state(); + let _guard = StackGuard::new(state); + check_stack(state, 3)?; + + lua.push_ref(&self.0); + protect_lua!(state, 1, 1, fn(state) { + // Try `__todebugstring` metamethod first, then `__tostring` + #[allow(clippy::collapsible_if)] + if ffi::luaL_callmeta(state, -1, cstr!("__todebugstring")) == 0 { + if ffi::luaL_callmeta(state, -1, cstr!("__tostring")) == 0 { + ffi::lua_pushnil(state); + } + } + })?; + Ok(lua.pop_value().as_string().map(|s| s.to_string_lossy())) + } + + pub(crate) fn fmt_pretty(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + // Try converting to a (debug) string first, with fallback to `__name/__type` + match unsafe { self.invoke_tostring_dbg() } { + Ok(Some(s)) => write!(fmt, "{s}"), + _ => { + let name = self.type_name().ok(); + let name = (name.as_ref()) + .map(|s| Either::Left(s.display())) + .unwrap_or(Either::Right("userdata")); + write!(fmt, "{name}: {:?}", self.to_pointer()) + } + } + } +} + +impl fmt::Debug for AnyUserData { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + if fmt.alternate() { + return self.fmt_pretty(fmt); + } + fmt.debug_tuple("AnyUserData").field(&self.0).finish() + } } /// Handle to a [`AnyUserData`] metatable. @@ -1171,7 +1224,7 @@ impl AnyUserData { /// Wraps any Rust type, returning an opaque type that implements [`IntoLua`] trait. /// /// This function uses [`Lua::create_any_userdata`] under the hood. - pub fn wrap(data: T) -> impl IntoLua { + pub fn wrap(data: T) -> impl IntoLua { WrappedUserdata(move |lua| lua.create_any_userdata(data)) } @@ -1181,7 +1234,7 @@ impl AnyUserData { /// This function uses [`Lua::create_ser_any_userdata`] under the hood. #[cfg(feature = "serde")] #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] - pub fn wrap_ser(data: T) -> impl IntoLua { + pub fn wrap_ser(data: T) -> impl IntoLua { WrappedUserdata(move |lua| lua.create_ser_any_userdata(data)) } } diff --git a/src/userdata/cell.rs b/src/userdata/cell.rs index 58f72465..f0058fd8 100644 --- a/src/userdata/cell.rs +++ b/src/userdata/cell.rs @@ -1,4 +1,4 @@ -use std::cell::{RefCell, UnsafeCell}; +use std::cell::RefCell; #[cfg(feature = "serde")] use serde::ser::{Serialize, Serializer}; @@ -6,14 +6,14 @@ use serde::ser::{Serialize, Serializer}; use crate::error::{Error, Result}; use crate::types::XRc; -use super::lock::{RawLock, UserDataLock}; +use super::lock::{RawLock, RwLock, UserDataLock}; use super::r#ref::{UserDataRef, UserDataRefMut}; #[cfg(all(feature = "serde", not(feature = "send")))] type DynSerialize = dyn erased_serde::Serialize; #[cfg(all(feature = "serde", feature = "send"))] -type DynSerialize = dyn erased_serde::Serialize + Send; +type DynSerialize = dyn erased_serde::Serialize + Send + Sync; pub(crate) enum UserDataStorage { Owned(UserDataVariant), @@ -23,9 +23,9 @@ pub(crate) enum UserDataStorage { // A enum for storing userdata values. // It's stored inside a Lua VM and protected by the outer `ReentrantMutex`. pub(crate) enum UserDataVariant { - Default(XRc>), + Default(XRc>), #[cfg(feature = "serde")] - Serializable(XRc>>, bool), // bool is `is_sync` + Serializable(XRc>>), } impl Clone for UserDataVariant { @@ -34,7 +34,7 @@ impl Clone for UserDataVariant { match self { Self::Default(inner) => Self::Default(XRc::clone(inner)), #[cfg(feature = "serde")] - Self::Serializable(inner, is_sync) => Self::Serializable(XRc::clone(inner), *is_sync), + Self::Serializable(inner) => Self::Serializable(XRc::clone(inner)), } } } @@ -42,10 +42,12 @@ impl Clone for UserDataVariant { impl UserDataVariant { #[inline(always)] pub(super) fn try_borrow_scoped(&self, f: impl FnOnce(&T) -> R) -> Result { - // We don't need to check for `T: Sync` because when this method is used (internally), - // Lua mutex is already locked. - // If non-`Sync` userdata is already borrowed by another thread (via `UserDataRef`), it will be - // exclusively locked. + // Shared (read) lock is always correct for in-place borrows: + // - this method is called internally while the Lua mutex is held, ensuring exclusive Lua-level + // access per call frame + // - with `send` feature, all owned userdata satisfies `T: Sync`, so simultaneous shared references + // from multiple threads are sound + // - without `send` feature, single-threaded execution makes shared lock safe for any `T` let _guard = (self.raw_lock().try_lock_shared_guarded()).map_err(|_| Error::UserDataBorrowError)?; Ok(f(unsafe { &*self.as_ptr() })) } @@ -78,10 +80,12 @@ impl UserDataVariant { return Err(Error::UserDataBorrowMutError); } Ok(match self { - Self::Default(inner) => XRc::into_inner(inner).unwrap().value.into_inner(), + Self::Default(inner) => XRc::into_inner(inner).unwrap().into_inner(), #[cfg(feature = "serde")] - Self::Serializable(inner, _) => unsafe { - let raw = Box::into_raw(XRc::into_inner(inner).unwrap().value.into_inner()); + Self::Serializable(inner) => unsafe { + // The serde variant erases `T` to `Box`, so we + // must cast the raw pointer back to recover the concrete type. + let raw = Box::into_raw(XRc::into_inner(inner).unwrap().into_inner()); *Box::from_raw(raw as *mut T) }, }) @@ -92,25 +96,25 @@ impl UserDataVariant { match self { Self::Default(inner) => XRc::strong_count(inner), #[cfg(feature = "serde")] - Self::Serializable(inner, _) => XRc::strong_count(inner), + Self::Serializable(inner) => XRc::strong_count(inner), } } #[inline(always)] pub(super) fn raw_lock(&self) -> &RawLock { match self { - Self::Default(inner) => &inner.raw_lock, + Self::Default(inner) => unsafe { inner.raw() }, #[cfg(feature = "serde")] - Self::Serializable(inner, _) => &inner.raw_lock, + Self::Serializable(inner) => unsafe { inner.raw() }, } } #[inline(always)] pub(super) fn as_ptr(&self) -> *mut T { match self { - Self::Default(inner) => inner.value.get(), + Self::Default(inner) => inner.data_ptr(), #[cfg(feature = "serde")] - Self::Serializable(inner, _) => unsafe { &mut **(inner.value.get() as *mut Box) }, + Self::Serializable(inner) => unsafe { (&mut **inner.data_ptr()) as *mut DynSerialize as *mut T }, } } } @@ -119,51 +123,16 @@ impl UserDataVariant { impl Serialize for UserDataStorage<()> { fn serialize(&self, serializer: S) -> std::result::Result { match self { - Self::Owned(variant @ UserDataVariant::Serializable(inner, is_sync)) => unsafe { - #[cfg(feature = "send")] - if *is_sync { - let _guard = (variant.raw_lock().try_lock_shared_guarded()) - .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; - (*inner.value.get()).serialize(serializer) - } else { - let _guard = (variant.raw_lock().try_lock_exclusive_guarded()) - .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; - (*inner.value.get()).serialize(serializer) - } - #[cfg(not(feature = "send"))] - { - let _ = is_sync; - let _guard = (variant.raw_lock().try_lock_shared_guarded()) - .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; - (*inner.value.get()).serialize(serializer) - } + Self::Owned(variant @ UserDataVariant::Serializable(inner)) => unsafe { + let _guard = (variant.raw_lock().try_lock_shared_guarded()) + .map_err(|_| serde::ser::Error::custom(Error::UserDataBorrowError))?; + (*inner.data_ptr()).serialize(serializer) }, _ => Err(serde::ser::Error::custom("cannot serialize ")), } } } -/// A type that provides interior mutability for a userdata value (thread-safe). -pub(crate) struct UserDataCell { - raw_lock: RawLock, - value: UnsafeCell, -} - -#[cfg(feature = "send")] -unsafe impl Send for UserDataCell {} -#[cfg(feature = "send")] -unsafe impl Sync for UserDataCell {} - -impl UserDataCell { - #[inline(always)] - fn new(value: T) -> Self { - UserDataCell { - raw_lock: RawLock::INIT, - value: UnsafeCell::new(value), - } - } -} - pub(crate) enum ScopedUserDataVariant { Ref(*const T), RefMut(RefCell<*mut T>), @@ -184,7 +153,7 @@ impl Drop for ScopedUserDataVariant { impl UserDataStorage { #[inline(always)] pub(crate) fn new(data: T) -> Self { - Self::Owned(UserDataVariant::Default(XRc::new(UserDataCell::new(data)))) + Self::Owned(UserDataVariant::Default(XRc::new(RwLock::new(data)))) } #[inline(always)] @@ -201,11 +170,10 @@ impl UserDataStorage { #[inline(always)] pub(crate) fn new_ser(data: T) -> Self where - T: Serialize + crate::types::MaybeSend, + T: Serialize + crate::types::MaybeSend + crate::types::MaybeSync, { let data = Box::new(data) as Box; - let is_sync = super::util::is_sync::(); - let variant = UserDataVariant::Serializable(XRc::new(UserDataCell::new(data)), is_sync); + let variant = UserDataVariant::Serializable(XRc::new(RwLock::new(data))); Self::Owned(variant) } diff --git a/src/userdata/lock.rs b/src/userdata/lock.rs index e0e5d1af..901a557b 100644 --- a/src/userdata/lock.rs +++ b/src/userdata/lock.rs @@ -1,6 +1,4 @@ pub(crate) trait UserDataLock { - const INIT: Self; - fn is_locked(&self) -> bool; fn try_lock_shared(&self) -> bool; fn try_lock_exclusive(&self) -> bool; @@ -48,12 +46,12 @@ impl Drop for LockGuard<'_, L> { } } -pub(crate) use lock_impl::RawLock; +pub(crate) use lock_impl::{RawLock, RwLock}; #[cfg(not(feature = "send"))] #[cfg(not(tarpaulin_include))] mod lock_impl { - use std::cell::Cell; + use std::cell::{Cell, UnsafeCell}; // Positive values represent the number of read references. // Negative values represent the number of write references (only one allowed). @@ -62,9 +60,6 @@ mod lock_impl { const UNUSED: isize = 0; impl super::UserDataLock for RawLock { - #[allow(clippy::declare_interior_mutable_const)] - const INIT: Self = Cell::new(UNUSED); - #[inline(always)] fn is_locked(&self) -> bool { self.get() != UNUSED @@ -72,7 +67,7 @@ mod lock_impl { #[inline(always)] fn try_lock_shared(&self) -> bool { - let flag = self.get().wrapping_add(1); + let flag = self.get().checked_add(1).expect("userdata lock count overflow"); if flag <= UNUSED { return false; } @@ -104,41 +99,71 @@ mod lock_impl { self.set(flag + 1); } } + + /// A cheap single-threaded read-write lock pairing a `parking_lot::RwLock` type. + pub(crate) struct RwLock { + lock: RawLock, + data: UnsafeCell, + } + + impl RwLock { + /// Creates a new `RwLock` containing the given value. + #[inline(always)] + pub(crate) fn new(value: T) -> Self { + RwLock { + lock: RawLock::new(UNUSED), + data: UnsafeCell::new(value), + } + } + + /// Returns a reference to the underlying raw lock. + #[inline(always)] + pub(crate) unsafe fn raw(&self) -> &RawLock { + &self.lock + } + + /// Returns a raw pointer to the underlying data. + #[inline(always)] + pub(crate) fn data_ptr(&self) -> *mut T { + self.data.get() + } + + /// Consumes this `RwLock`, returning the underlying data. + #[inline(always)] + pub(crate) fn into_inner(self) -> T { + self.data.into_inner() + } + } } #[cfg(feature = "send")] mod lock_impl { - use parking_lot::lock_api::RawRwLock; - - pub(crate) type RawLock = parking_lot::RawRwLock; + pub(crate) use parking_lot::{RawRwLock as RawLock, RwLock}; impl super::UserDataLock for RawLock { - #[allow(clippy::declare_interior_mutable_const)] - const INIT: Self = ::INIT; - #[inline(always)] fn is_locked(&self) -> bool { - RawRwLock::is_locked(self) + parking_lot::lock_api::RawRwLock::is_locked(self) } #[inline(always)] fn try_lock_shared(&self) -> bool { - RawRwLock::try_lock_shared(self) + parking_lot::lock_api::RawRwLock::try_lock_shared(self) } #[inline(always)] fn try_lock_exclusive(&self) -> bool { - RawRwLock::try_lock_exclusive(self) + parking_lot::lock_api::RawRwLock::try_lock_exclusive(self) } #[inline(always)] unsafe fn unlock_shared(&self) { - RawRwLock::unlock_shared(self) + parking_lot::lock_api::RawRwLock::unlock_shared(self) } #[inline(always)] unsafe fn unlock_exclusive(&self) { - RawRwLock::unlock_exclusive(self) + parking_lot::lock_api::RawRwLock::unlock_exclusive(self) } } } diff --git a/src/userdata/ref.rs b/src/userdata/ref.rs index 48f67c28..131b84d6 100644 --- a/src/userdata/ref.rs +++ b/src/userdata/ref.rs @@ -7,12 +7,11 @@ use crate::error::{Error, Result}; use crate::state::{Lua, RawLua}; use crate::traits::FromLua; use crate::userdata::AnyUserData; -use crate::util::get_userdata; +use crate::util::{check_stack, get_userdata, take_userdata}; use crate::value::Value; use super::cell::{UserDataStorage, UserDataVariant}; use super::lock::{LockGuard, RawLock, UserDataLock}; -use super::util::is_sync; #[cfg(feature = "userdata-wrappers")] use { @@ -63,11 +62,10 @@ impl TryFrom> for UserDataRef { #[inline] fn try_from(variant: UserDataVariant) -> Result { - let guard = if cfg!(not(feature = "send")) || is_sync::() { - variant.raw_lock().try_lock_shared_guarded() - } else { - variant.raw_lock().try_lock_exclusive_guarded() - }; + // Shared (read) lock is always correct: + // - with `send` feature, `T: Sync` is guaranteed by the `MaybeSync` bound on userdata creation + // - without `send` feature, single-threaded access makes shared lock safe for any `T` + let guard = variant.raw_lock().try_lock_shared_guarded(); let guard = guard.map_err(|_| Error::UserDataBorrowError)?; let guard = unsafe { mem::transmute::, LockGuard<'static, _>>(guard) }; Ok(UserDataRef::from_parts(UserDataRefInner::Default(variant), guard)) @@ -442,6 +440,66 @@ impl DerefMut for UserDataRefMutInner { } } +/// A wrapper type that takes ownership of a userdata value. +/// +/// It implements [`FromLua`] and can be used to receive a typed userdata from Lua by taking +/// ownership of it. +/// The original Lua userdata is marked as destructed and cannot be used further. +pub struct UserDataOwned(pub T); + +impl Deref for UserDataOwned { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for UserDataOwned { + #[inline] + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl fmt::Debug for UserDataOwned { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl fmt::Display for UserDataOwned { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl FromLua for UserDataOwned { + fn from_lua(value: Value, _: &Lua) -> Result { + try_value_to_userdata::(value)?.take().map(UserDataOwned) + } + + unsafe fn from_stack(idx: c_int, lua: &RawLua) -> Result { + let state = lua.state(); + let type_id = lua.get_userdata_type_id::(state, idx)?; + match type_id { + Some(type_id) if type_id == TypeId::of::() => { + let ud = get_userdata::>(state, idx); + if (*ud).has_exclusive_access() { + check_stack(state, 1)?; + take_userdata::>(state, idx) + .into_inner() + .map(UserDataOwned) + } else { + Err(Error::UserDataBorrowMutError) + } + } + _ => Err(Error::UserDataTypeMismatch), + } + } +} + #[inline] fn try_value_to_userdata(value: Value) -> Result { match value { @@ -466,6 +524,10 @@ mod assertions { static_assertions::assert_impl_all!(UserDataRefMut<()>: Sync, Send); #[cfg(feature = "send")] static_assertions::assert_not_impl_all!(UserDataRefMut>: Send, Sync); + #[cfg(feature = "send")] + static_assertions::assert_impl_all!(UserDataOwned<()>: Send, Sync); + #[cfg(feature = "send")] + static_assertions::assert_not_impl_all!(UserDataOwned>: Send, Sync); #[cfg(not(feature = "send"))] static_assertions::assert_not_impl_all!(UserDataRef<()>: Send, Sync); diff --git a/src/userdata/registry.rs b/src/userdata/registry.rs index e16bec6d..c5138140 100644 --- a/src/userdata/registry.rs +++ b/src/userdata/registry.rs @@ -654,6 +654,12 @@ macro_rules! lua_userdata_impl { // A special proxy object for UserData pub(crate) struct UserDataProxy(pub(crate) PhantomData); +// `UserDataProxy` holds no real `T` value, only a type marker, so it is always safe to send/share. +#[cfg(feature = "send")] +unsafe impl Send for UserDataProxy {} +#[cfg(feature = "send")] +unsafe impl Sync for UserDataProxy {} + lua_userdata_impl!(UserDataProxy); #[cfg(all(feature = "userdata-wrappers", not(feature = "send")))] diff --git a/src/userdata/util.rs b/src/userdata/util.rs index d42eaaed..6c5f0f8f 100644 --- a/src/userdata/util.rs +++ b/src/userdata/util.rs @@ -1,6 +1,4 @@ use std::any::TypeId; -use std::cell::Cell; -use std::marker::PhantomData; use std::os::raw::c_int; use std::ptr; @@ -11,35 +9,6 @@ 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. -// It uses leaked specialization feature from stdlib. -struct IsSync<'a, T> { - is_sync: &'a Cell, - _marker: PhantomData, -} - -impl Clone for IsSync<'_, T> { - fn clone(&self) -> Self { - self.is_sync.set(false); - IsSync { - is_sync: self.is_sync, - _marker: PhantomData, - } - } -} - -impl Copy for IsSync<'_, T> {} - -pub(crate) fn is_sync() -> bool { - let is_sync = Cell::new(true); - let _ = [IsSync:: { - is_sync: &is_sync, - _marker: PhantomData, - }] - .clone(); - is_sync.get() -} - // Userdata type hints, used to match types of wrapped userdata #[derive(Clone, Copy)] pub(crate) struct TypeIdHints { diff --git a/src/util/error.rs b/src/util/error.rs index 297754d0..c84902de 100644 --- a/src/util/error.rs +++ b/src/util/error.rs @@ -197,7 +197,7 @@ where F: FnOnce(*mut ffi::lua_State) -> R, R: Copy, { - struct Params { + struct Params { function: Option, result: MaybeUninit, nresults: c_int, diff --git a/src/util/mod.rs b/src/util/mod.rs index 0741f12c..899e6964 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -117,14 +117,10 @@ pub(crate) unsafe fn push_external_string( } if protect { - let res = protect_lua!(state, 0, 1, move |state| { + // Lua free external string on error + protect_lua!(state, 0, 1, move |state| { ffi::lua_pushexternalstring(state, s_ptr, s_len, Some(dealloc), bytes_ud as *mut _); - }); - if res.is_err() { - // Deallocate on error - drop(Box::from_raw(bytes_ud)); - return res; - } + })?; } else { ffi::lua_pushexternalstring(state, s_ptr, s_len, Some(dealloc), bytes_ud as *mut _); } @@ -282,7 +278,7 @@ pub(crate) unsafe fn to_string(state: *mut ffi::lua_State, index: c_int) -> Stri } ffi::LUA_TNUMBER => { let mut isint = 0; - let i = ffi::lua_tointegerx(state, -1, &mut isint); + let i = ffi::lua_tointegerx(state, index, &mut isint); if isint == 0 { ffi::lua_tonumber(state, index).to_string() } else { @@ -349,10 +345,7 @@ pub(crate) unsafe fn ptr_to_lossy_str<'a>(input: *const c_char) -> Option Option { - match n { - n if n < 0 => None, - n => Some(n as usize), - } + usize::try_from(n).ok() } mod error; diff --git a/src/value.rs b/src/value.rs index 1250a3f7..d273dad7 100644 --- a/src/value.rs +++ b/src/value.rs @@ -7,7 +7,7 @@ use num_traits::FromPrimitive; use crate::error::{Error, Result}; use crate::function::Function; -use crate::string::{BorrowedStr, LuaString}; +use crate::string::LuaString; use crate::table::Table; use crate::thread::Thread; use crate::types::{Integer, LightUserData, Number, ValueRef}; @@ -38,7 +38,7 @@ pub enum Value { LightUserData(LightUserData), /// An integer number. /// - /// Any Lua number convertible to a `Integer` will be represented as this variant. + /// Any Lua number convertible to an `Integer` will be represented as this variant. Integer(Integer), /// A floating point number. Number(Number), @@ -128,18 +128,13 @@ impl Value { #[inline] pub fn to_pointer(&self) -> *const c_void { match self { - Value::String(LuaString(vref)) => { - // In Lua < 5.4 (excluding Luau), string pointers are NULL - // Use alternative approach - let lua = vref.lua.lock(); - unsafe { ffi::lua_tostring(lua.ref_thread(), vref.index) as *const c_void } - } Value::LightUserData(ud) => ud.0, Value::Table(Table(vref)) | Value::Function(Function(vref)) | Value::Thread(Thread(vref, ..)) | Value::UserData(AnyUserData(vref)) | Value::Other(vref) => vref.to_pointer(), + Value::String(s) => s.to_pointer(), #[cfg(feature = "luau")] Value::Buffer(crate::Buffer(vref)) => vref.to_pointer(), _ => ptr::null(), @@ -151,7 +146,7 @@ impl Value { /// This might invoke the `__tostring` metamethod for non-primitive types (eg. tables, /// functions). pub fn to_string(&self) -> Result { - unsafe fn invoke_to_string(vref: &ValueRef) -> Result { + unsafe fn invoke_tostring(vref: &ValueRef) -> Result { let lua = vref.lua.lock(); let state = lua.state(); let _guard = StackGuard::new(state); @@ -178,9 +173,9 @@ impl Value { | Value::Function(Function(vref)) | Value::Thread(Thread(vref, ..)) | Value::UserData(AnyUserData(vref)) - | Value::Other(vref) => unsafe { invoke_to_string(vref) }, + | Value::Other(vref) => unsafe { invoke_tostring(vref) }, #[cfg(feature = "luau")] - Value::Buffer(crate::Buffer(vref)) => unsafe { invoke_to_string(vref) }, + Value::Buffer(crate::Buffer(vref)) => unsafe { invoke_tostring(vref) }, Value::Error(err) => Ok(err.to_string()), } } @@ -352,31 +347,6 @@ impl Value { } } - /// Cast the value to [`BorrowedStr`]. - /// - /// If the value is a [`LuaString`], try to convert it to [`BorrowedStr`] or return `None` - /// otherwise. - #[deprecated( - since = "0.11.0", - note = "This method does not follow Rust naming convention. Use `as_string().and_then(|s| s.to_str().ok())` instead." - )] - #[inline] - pub fn as_str(&self) -> Option> { - self.as_string().and_then(|s| s.to_str().ok()) - } - - /// Cast the value to [`String`]. - /// - /// If the value is a [`LuaString`], converts it to [`String`] or returns `None` otherwise. - #[deprecated( - since = "0.11.0", - note = "This method does not follow Rust naming convention. Use `as_string().map(|s| s.to_string_lossy())` instead." - )] - #[inline] - pub fn as_string_lossy(&self) -> Option { - self.as_string().map(|s| s.to_string_lossy()) - } - /// Returns `true` if the value is a Lua [`Table`]. #[inline] pub fn is_table(&self) -> bool { @@ -445,6 +415,31 @@ impl Value { } } + /// Cast the value to a [`Vector`]. + /// + /// If the value is a [`Vector`], returns it or `None` otherwise. + /// + /// [`Vector`]: crate::Vector + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + #[inline] + pub fn as_vector(&self) -> Option { + match self { + Value::Vector(v) => Some(*v), + _ => None, + } + } + + /// Returns `true` if the value is a [`Vector`]. + /// + /// [`Vector`]: crate::Vector + #[cfg(any(feature = "luau", doc))] + #[cfg_attr(docsrs, doc(cfg(feature = "luau")))] + #[inline] + pub fn is_vector(&self) -> bool { + self.as_vector().is_some() + } + /// Cast the value to a [`Buffer`]. /// /// If the value is [`Buffer`], returns it or `None` otherwise. @@ -498,14 +493,6 @@ impl Value { // Compares two values. // Used to sort values for Debug printing. pub(crate) fn sort_cmp(&self, other: &Self) -> Ordering { - fn cmp_num(a: Number, b: Number) -> Ordering { - match (a, b) { - _ if a < b => Ordering::Less, - _ if a > b => Ordering::Greater, - _ => Ordering::Equal, - } - } - match (self, other) { // Nil (Value::Nil, Value::Nil) => Ordering::Equal, @@ -521,9 +508,11 @@ impl Value { (_, Value::Boolean(_)) => Ordering::Greater, // Integer && Number (Value::Integer(a), Value::Integer(b)) => a.cmp(b), - (Value::Integer(a), Value::Number(b)) => cmp_num(*a as Number, *b), - (Value::Number(a), Value::Integer(b)) => cmp_num(*a, *b as Number), - (Value::Number(a), Value::Number(b)) => cmp_num(*a, *b), + (Value::Integer(a), Value::Number(b)) => (*a as Number).partial_cmp(b).unwrap_or(Ordering::Equal), + (Value::Number(a), Value::Integer(b)) => { + a.partial_cmp(&(*b as Number)).unwrap_or(Ordering::Equal) + } + (Value::Number(a), Value::Number(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal), (Value::Integer(_) | Value::Number(_), _) => Ordering::Less, (_, Value::Integer(_) | Value::Number(_)) => Ordering::Greater, // Vector (Luau) @@ -561,15 +550,7 @@ impl Value { t @ Value::Table(_) => write!(fmt, "table: {:?}", t.to_pointer()), f @ Value::Function(_) => write!(fmt, "function: {:?}", f.to_pointer()), t @ Value::Thread(_) => write!(fmt, "thread: {:?}", t.to_pointer()), - u @ Value::UserData(ud) => { - // Try `__name/__type` first then `__tostring` - let name = ud.type_name().ok().flatten(); - let s = name - .map(|name| format!("{name}: {:?}", u.to_pointer())) - .or_else(|| u.to_string().ok()) - .unwrap_or_else(|| format!("userdata: {:?}", u.to_pointer())); - write!(fmt, "{s}") - } + Value::UserData(ud) => ud.fmt_pretty(fmt), #[cfg(feature = "luau")] buf @ Value::Buffer(_) => write!(fmt, "buffer: {:?}", buf.to_pointer()), Value::Error(e) if recursive => write!(fmt, "{e:?}"), diff --git a/tests/async.rs b/tests/async.rs index 22df2ab3..40df52b0 100644 --- a/tests/async.rs +++ b/tests/async.rs @@ -7,7 +7,7 @@ use futures_util::stream::TryStreamExt; use tokio::sync::Mutex; use mlua::{ - Error, Function, Lua, LuaOptions, MultiValue, ObjectLike, Result, StdLib, Table, ThreadStatus, UserData, + Error, Function, Lua, LuaOptions, MultiValue, ObjectLike, Result, StdLib, Table, Thread, UserData, UserDataMethods, UserDataRef, Value, }; @@ -41,7 +41,7 @@ async fn test_async_function_wrap() -> Result<()> { let f = Function::wrap_async(|s: String| async move { tokio::task::yield_now().await; - Ok(s) + Ok::<_, Error>(s) }); lua.globals().set("f", f)?; let res: String = lua.load(r#"f("hello")"#).eval_async().await?; @@ -687,6 +687,49 @@ async fn test_async_hook() -> Result<()> { Ok(()) } +#[tokio::test] +#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] +async fn test_async_hook_yield_preserves_stack() -> Result<()> { + use std::future::{Future, poll_fn}; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::task::Poll; + + let lua = Lua::new(); + + let thread = lua.create_thread( + lua.load( + r#" + local x = 40 + local y = 2 + return x + y + "#, + ) + .into_function()?, + )?; + + let yielded = Arc::new(AtomicBool::new(false)); + let yielded2 = yielded.clone(); + thread.set_hook(mlua::HookTriggers::EVERY_LINE, move |lua, debug| { + if debug.current_line() == Some(4) && !yielded2.swap(true, Ordering::Relaxed) { + lua.remove_hook(); + return Ok(mlua::VmState::Yield); + } + Ok(mlua::VmState::Continue) + })?; + + let mut thread = Box::pin(thread.into_async::(())?); + poll_fn(|cx| { + assert!(thread.as_mut().poll(cx).is_pending()); + Poll::Ready(()) + }) + .await; + assert!(yielded.load(Ordering::Relaxed)); + lua.gc_collect()?; + assert_eq!(thread.await?, 42); + + Ok(()) +} + #[test] fn test_async_yield_with() -> Result<()> { let lua = Lua::new(); @@ -714,7 +757,21 @@ fn test_async_yield_with() -> Result<()> { 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); + assert!(thread.is_finished()); + + Ok(()) +} + +#[tokio::test] +async fn test_async_current_thread() -> Result<()> { + let lua = Lua::new(); + + let get_inner_thread = lua.create_async_function(move |lua, ()| async move { + let f = lua.create_async_function(move |lua, ()| async move { Ok(lua.current_thread()) })?; + f.call_async::(()).await + })?; + let inner_thread = get_inner_thread.call_async::(()).await?; + assert_eq!(inner_thread, lua.current_thread()); Ok(()) } diff --git a/tests/chunk.rs b/tests/chunk.rs index 3f7b4849..b442e68b 100644 --- a/tests/chunk.rs +++ b/tests/chunk.rs @@ -1,6 +1,8 @@ +#[cfg(not(target_os = "wasi"))] use std::{fs, io}; -use mlua::{Chunk, ChunkMode, Lua, Result}; +use mlua::chunk::{Chunk, ChunkMode}; +use mlua::{Lua, Result}; #[test] fn test_chunk_methods() -> Result<()> { @@ -85,7 +87,7 @@ fn test_chunk_macro() -> Result<()> { data.raw_set("num", 1)?; let ud = mlua::AnyUserData::wrap("hello"); - let f = mlua::Function::wrap(|| Ok(())); + let f = mlua::Function::wrap(|| Ok::<_, mlua::Error>(())); lua.globals().set("g", 123)?; @@ -109,13 +111,28 @@ fn test_chunk_macro() -> Result<()> { assert_eq!(lua.globals().get::("s")?, 321); + // Check line numbers in error reporting + match lua + .load(mlua::chunk! { + local x = 1 + -- comment + error("boom") + }) + .exec() + { + Err(mlua::Error::RuntimeError(ref msg)) => { + assert!(msg.contains(":3:"), "expected line 3, got: {msg}"); + } + other => panic!("expected RuntimeError, got {other:?}"), + } + Ok(()) } #[cfg(feature = "luau")] #[test] fn test_compiler() -> Result<()> { - let compiler = mlua::Compiler::new() + let compiler = mlua::chunk::Compiler::new() .set_optimization_level(2) .set_debug_level(2) .set_type_info_level(1) @@ -142,7 +159,8 @@ fn test_compiler() -> Result<()> { #[cfg(feature = "luau")] #[test] fn test_compiler_library_constants() { - use mlua::{Compiler, Vector}; + use mlua::Vector; + use mlua::chunk::Compiler; let compiler = Compiler::new() .set_optimization_level(2) diff --git a/tests/compile.rs b/tests/compile.rs index c8ee4511..1c85ffb3 100644 --- a/tests/compile.rs +++ b/tests/compile.rs @@ -21,4 +21,29 @@ fn test_compilation() { t.compile_fail("tests/compile/non_send.rs"); #[cfg(not(feature = "send"))] t.pass("tests/compile/non_send.rs"); + + #[cfg(feature = "macros")] + { + t.compile_fail("tests/compile/chunk_dollar_non_ident.rs"); + t.compile_fail("tests/compile/userdata_getter_and_meta.rs"); + t.compile_fail("tests/compile/userdata_getter_and_setter.rs"); + t.compile_fail("tests/compile/userdata_getter_mut_self.rs"); + t.compile_fail("tests/compile/userdata_getter_extra_arg.rs"); + t.compile_fail("tests/compile/userdata_setter_ref_self.rs"); + t.compile_fail("tests/compile/userdata_mut_slice_arg.rs"); + t.compile_fail("tests/compile/userdata_setter_no_value.rs"); + t.compile_fail("tests/compile/userdata_static_with_self.rs"); + t.compile_fail("tests/compile/userdata_meta_owned_self.rs"); + t.compile_fail("tests/compile/userdata_destructuring_arg.rs"); + t.compile_fail("tests/compile/userdata_generic_impl.rs"); + t.compile_fail("tests/compile/userdata_const_getter.rs"); + t.compile_fail("tests/compile/userdata_field_with_args.rs"); + } + + #[cfg(all(feature = "macros", feature = "async"))] + { + t.compile_fail("tests/compile/userdata_getter_async.rs"); + t.compile_fail("tests/compile/userdata_setter_async.rs"); + t.compile_fail("tests/compile/userdata_field_async.rs"); + } } diff --git a/tests/compile/async_any_userdata_method.stderr b/tests/compile/async_any_userdata_method.stderr index 3e01c45b..116b64f7 100644 --- a/tests/compile/async_any_userdata_method.stderr +++ b/tests/compile/async_any_userdata_method.stderr @@ -1,14 +1,18 @@ error[E0596]: cannot borrow `s` as mutable, as it is a captured variable in a `Fn` closure --> tests/compile/async_any_userdata_method.rs:9:49 | - 8 | let mut s = &s; - | ----- `s` declared here, outside the closure - 9 | reg.add_async_method("t", |_, this, ()| async { - | ------------- ^^^^^ cannot borrow as mutable - | | - | in this closure -10 | s = &*this; - | - mutable borrow occurs due to use of `s` in closure + 8 | let mut s = &s; + | ----- `s` declared here, outside the closure + 9 | reg.add_async_method("t", |_, this, ()| async { + | - ------------- ^^^^^ cannot borrow as mutable + | | | + | _____________| in this closure + | | +10 | | s = &*this; + | | - mutable borrow occurs due to use of `s` in closure +11 | | Ok(()) +12 | | }); + | |__________- expects `Fn` instead of `FnMut` error[E0373]: async block may outlive the current function, but it borrows `this`, which is owned by the current function --> tests/compile/async_any_userdata_method.rs:9:49 diff --git a/tests/compile/chunk_dollar_non_ident.rs b/tests/compile/chunk_dollar_non_ident.rs new file mode 100644 index 00000000..47350132 --- /dev/null +++ b/tests/compile/chunk_dollar_non_ident.rs @@ -0,0 +1,4 @@ +use mlua::chunk; +fn main() { + let _ = chunk! { $42 }; +} diff --git a/tests/compile/chunk_dollar_non_ident.stderr b/tests/compile/chunk_dollar_non_ident.stderr new file mode 100644 index 00000000..480bc0f9 --- /dev/null +++ b/tests/compile/chunk_dollar_non_ident.stderr @@ -0,0 +1,5 @@ +error: `$` must be followed by an identifier + --> tests/compile/chunk_dollar_non_ident.rs:3:22 + | +3 | let _ = chunk! { $42 }; + | ^ diff --git a/tests/compile/lua_norefunwindsafe.stderr b/tests/compile/lua_norefunwindsafe.stderr index 814ae6b6..4094cd2b 100644 --- a/tests/compile/lua_norefunwindsafe.stderr +++ b/tests/compile/lua_norefunwindsafe.stderr @@ -1,28 +1,28 @@ -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/lua_norefunwindsafe.rs:7:18 | 7 | catch_unwind(|| lua.create_table().unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `Lua`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` + = help: within `Lua`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` --> $CARGO/lock_api-$VERSION/src/remutex.rs | | pub struct ReentrantMutex { | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` +note: required because it appears within the type `alloc::sync::ArcInner>` --> $RUST/alloc/src/sync.rs | | struct ArcInner { | ^^^^^^^^ -note: required because it appears within the type `PhantomData>>` +note: required because it appears within the type `PhantomData>>` --> $RUST/core/src/marker.rs | | pub struct PhantomData; | ^^^^^^^^^^^ -note: required because it appears within the type `Arc>` +note: required because it appears within the type `Arc>` --> $RUST/alloc/src/sync.rs | | pub struct Arc< @@ -63,22 +63,22 @@ note: required because it appears within the type `lock_api::remutex::RawReentra | | pub struct RawReentrantMutex { | ^^^^^^^^^^^^^^^^^ -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` --> $CARGO/lock_api-$VERSION/src/remutex.rs | | pub struct ReentrantMutex { | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` +note: required because it appears within the type `alloc::sync::ArcInner>` --> $RUST/alloc/src/sync.rs | | struct ArcInner { | ^^^^^^^^ -note: required because it appears within the type `PhantomData>>` +note: required because it appears within the type `PhantomData>>` --> $RUST/core/src/marker.rs | | pub struct PhantomData; | ^^^^^^^^^^^ -note: required because it appears within the type `Arc>` +note: required because it appears within the type `Arc>` --> $RUST/alloc/src/sync.rs | | pub struct Arc< diff --git a/tests/compile/ref_nounwindsafe.stderr b/tests/compile/ref_nounwindsafe.stderr index 49032555..757083df 100644 --- a/tests/compile/ref_nounwindsafe.stderr +++ b/tests/compile/ref_nounwindsafe.stderr @@ -1,24 +1,24 @@ -error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary +error[E0277]: the type `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary --> tests/compile/ref_nounwindsafe.rs:8:18 | 8 | catch_unwind(move || table.set("a", "b").unwrap()); - | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary + | ------------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `UnsafeCell` may contain interior mutability and a reference may not be safely transferable across a catch_unwind boundary | | | required by a bound introduced by this call | - = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` + = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` --> $CARGO/lock_api-$VERSION/src/remutex.rs | | pub struct ReentrantMutex { | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` +note: required because it appears within the type `alloc::sync::ArcInner>` --> $RUST/alloc/src/sync.rs | | struct ArcInner { | ^^^^^^^^ - = note: required for `NonNull>>` to implement `UnwindSafe` -note: required because it appears within the type `std::sync::Weak>` + = note: required for `NonNull>>` to implement `UnwindSafe` +note: required because it appears within the type `std::sync::Weak>` --> $RUST/alloc/src/sync.rs | | pub struct Weak< @@ -57,7 +57,7 @@ error[E0277]: the type `UnsafeCell` may contain interior mutability and a | | | required by a bound introduced by this call | - = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` + = help: within `alloc::sync::ArcInner>`, the trait `RefUnwindSafe` is not implemented for `UnsafeCell` note: required because it appears within the type `Cell` --> $RUST/core/src/cell.rs | @@ -68,18 +68,18 @@ note: required because it appears within the type `lock_api::remutex::RawReentra | | pub struct RawReentrantMutex { | ^^^^^^^^^^^^^^^^^ -note: required because it appears within the type `lock_api::remutex::ReentrantMutex` +note: required because it appears within the type `lock_api::remutex::ReentrantMutex` --> $CARGO/lock_api-$VERSION/src/remutex.rs | | pub struct ReentrantMutex { | ^^^^^^^^^^^^^^ -note: required because it appears within the type `alloc::sync::ArcInner>` +note: required because it appears within the type `alloc::sync::ArcInner>` --> $RUST/alloc/src/sync.rs | | struct ArcInner { | ^^^^^^^^ - = note: required for `NonNull>>` to implement `UnwindSafe` -note: required because it appears within the type `std::sync::Weak>` + = note: required for `NonNull>>` to implement `UnwindSafe` +note: required because it appears within the type `std::sync::Weak>` --> $RUST/alloc/src/sync.rs | | pub struct Weak< diff --git a/tests/compile/userdata_const_getter.rs b/tests/compile/userdata_const_getter.rs new file mode 100644 index 00000000..12584106 --- /dev/null +++ b/tests/compile/userdata_const_getter.rs @@ -0,0 +1,10 @@ +#[derive(Default, mlua::UserData)] +struct Foo; + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter)] + const X: u32 = 42; +} + +fn main() {} diff --git a/tests/compile/userdata_const_getter.stderr b/tests/compile/userdata_const_getter.stderr new file mode 100644 index 00000000..f005b009 --- /dev/null +++ b/tests/compile/userdata_const_getter.stderr @@ -0,0 +1,5 @@ +error: const items do not support `getter` or `setter` + --> tests/compile/userdata_const_getter.rs:6:5 + | +6 | #[lua(getter)] + | ^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_destructuring_arg.rs b/tests/compile/userdata_destructuring_arg.rs new file mode 100644 index 00000000..8bd206e8 --- /dev/null +++ b/tests/compile/userdata_destructuring_arg.rs @@ -0,0 +1,14 @@ +#[derive(mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(infallible)] + fn takes_pattern(&self, (a, b): (u32, u32)) -> u32 { + self.x + a + b + } +} + +fn main() {} diff --git a/tests/compile/userdata_destructuring_arg.stderr b/tests/compile/userdata_destructuring_arg.stderr new file mode 100644 index 00000000..1028d6eb --- /dev/null +++ b/tests/compile/userdata_destructuring_arg.stderr @@ -0,0 +1,5 @@ +error: `#[mlua::userdata_impl]` requires a named parameter or `_`; destructuring patterns are not supported + --> tests/compile/userdata_destructuring_arg.rs:9:29 + | +9 | fn takes_pattern(&self, (a, b): (u32, u32)) -> u32 { + | ^^^^^^ diff --git a/tests/compile/userdata_field_async.rs b/tests/compile/userdata_field_async.rs new file mode 100644 index 00000000..fd2b5332 --- /dev/null +++ b/tests/compile/userdata_field_async.rs @@ -0,0 +1,14 @@ +use mlua::Result; + +#[derive(Clone, Debug, mlua::UserData)] +struct Foo; + +#[mlua::userdata_impl] +impl Foo { + #[lua(field)] + async fn description() -> Result { + Ok("foo".into()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_field_async.stderr b/tests/compile/userdata_field_async.stderr new file mode 100644 index 00000000..a41d49fb --- /dev/null +++ b/tests/compile/userdata_field_async.stderr @@ -0,0 +1,13 @@ +error: async field function is not supported + --> tests/compile/userdata_field_async.rs:9:5 + | +9 | async fn description() -> Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `mlua::Result` + --> tests/compile/userdata_field_async.rs:1:5 + | +1 | use mlua::Result; + | ^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/compile/userdata_field_with_args.rs b/tests/compile/userdata_field_with_args.rs new file mode 100644 index 00000000..8757548c --- /dev/null +++ b/tests/compile/userdata_field_with_args.rs @@ -0,0 +1,14 @@ +#[derive(Default, mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(field)] + fn as_name(name: &str) -> String { + name.to_string() + } +} + +fn main() {} diff --git a/tests/compile/userdata_field_with_args.stderr b/tests/compile/userdata_field_with_args.stderr new file mode 100644 index 00000000..a7501385 --- /dev/null +++ b/tests/compile/userdata_field_with_args.stderr @@ -0,0 +1,5 @@ +error: field function must not take arguments + --> tests/compile/userdata_field_with_args.rs:9:5 + | +9 | fn as_name(name: &str) -> String { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_generic_impl.rs b/tests/compile/userdata_generic_impl.rs new file mode 100644 index 00000000..d9f62dcf --- /dev/null +++ b/tests/compile/userdata_generic_impl.rs @@ -0,0 +1,14 @@ +#[derive(mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(infallible)] + fn get(&self) -> u32 { + self.x + } +} + +fn main() {} diff --git a/tests/compile/userdata_generic_impl.stderr b/tests/compile/userdata_generic_impl.stderr new file mode 100644 index 00000000..db15e78c --- /dev/null +++ b/tests/compile/userdata_generic_impl.stderr @@ -0,0 +1,5 @@ +error: `#[mlua::userdata_impl]` does not support generic impl blocks. + --> tests/compile/userdata_generic_impl.rs:7:5 + | +7 | impl Foo { + | ^^^ diff --git a/tests/compile/userdata_getter_and_meta.rs b/tests/compile/userdata_getter_and_meta.rs new file mode 100644 index 00000000..4da36885 --- /dev/null +++ b/tests/compile/userdata_getter_and_meta.rs @@ -0,0 +1,12 @@ +#[derive(Default, mlua::UserData)] +struct Foo; + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter, meta)] + fn bar(&self) -> mlua::Result { + Ok(42) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_and_meta.stderr b/tests/compile/userdata_getter_and_meta.stderr new file mode 100644 index 00000000..e8a0af7d --- /dev/null +++ b/tests/compile/userdata_getter_and_meta.stderr @@ -0,0 +1,5 @@ +error: `meta` can only be combined with `field` + --> tests/compile/userdata_getter_and_meta.rs:6:5 + | +6 | #[lua(getter, meta)] + | ^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_and_setter.rs b/tests/compile/userdata_getter_and_setter.rs new file mode 100644 index 00000000..2232dedf --- /dev/null +++ b/tests/compile/userdata_getter_and_setter.rs @@ -0,0 +1,14 @@ +#[derive(Default, mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter, setter)] + fn x(&self) -> mlua::Result { + Ok(self.x) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_and_setter.stderr b/tests/compile/userdata_getter_and_setter.stderr new file mode 100644 index 00000000..8f3709e4 --- /dev/null +++ b/tests/compile/userdata_getter_and_setter.stderr @@ -0,0 +1,5 @@ +error: at most one of `getter`, `setter`, `field` can be specified + --> tests/compile/userdata_getter_and_setter.rs:8:5 + | +8 | #[lua(getter, setter)] + | ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_async.rs b/tests/compile/userdata_getter_async.rs new file mode 100644 index 00000000..8d99f81d --- /dev/null +++ b/tests/compile/userdata_getter_async.rs @@ -0,0 +1,14 @@ +use mlua::Result; + +#[derive(Clone, Debug, mlua::UserData)] +struct Foo(u64); + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter)] + async fn value(&self) -> Result { + Ok(self.0) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_async.stderr b/tests/compile/userdata_getter_async.stderr new file mode 100644 index 00000000..fb8184d4 --- /dev/null +++ b/tests/compile/userdata_getter_async.stderr @@ -0,0 +1,13 @@ +error: async field getter is not supported + --> tests/compile/userdata_getter_async.rs:9:5 + | +9 | async fn value(&self) -> Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `mlua::Result` + --> tests/compile/userdata_getter_async.rs:1:5 + | +1 | use mlua::Result; + | ^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/compile/userdata_getter_extra_arg.rs b/tests/compile/userdata_getter_extra_arg.rs new file mode 100644 index 00000000..fd218f0d --- /dev/null +++ b/tests/compile/userdata_getter_extra_arg.rs @@ -0,0 +1,14 @@ +#[derive(Default, mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter)] + fn x(&self, extra: u32) -> mlua::Result { + Ok(self.x + extra) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_extra_arg.stderr b/tests/compile/userdata_getter_extra_arg.stderr new file mode 100644 index 00000000..0e1b2060 --- /dev/null +++ b/tests/compile/userdata_getter_extra_arg.stderr @@ -0,0 +1,5 @@ +error: field getter must not take additional arguments + --> tests/compile/userdata_getter_extra_arg.rs:9:5 + | +9 | fn x(&self, extra: u32) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_getter_mut_self.rs b/tests/compile/userdata_getter_mut_self.rs new file mode 100644 index 00000000..96d977c7 --- /dev/null +++ b/tests/compile/userdata_getter_mut_self.rs @@ -0,0 +1,14 @@ +#[derive(Default, mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(getter)] + fn x(&mut self) -> mlua::Result { + Ok(self.x) + } +} + +fn main() {} diff --git a/tests/compile/userdata_getter_mut_self.stderr b/tests/compile/userdata_getter_mut_self.stderr new file mode 100644 index 00000000..db160943 --- /dev/null +++ b/tests/compile/userdata_getter_mut_self.stderr @@ -0,0 +1,5 @@ +error: field getter must take `&self` + --> tests/compile/userdata_getter_mut_self.rs:9:5 + | +9 | fn x(&mut self) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_meta_owned_self.rs b/tests/compile/userdata_meta_owned_self.rs new file mode 100644 index 00000000..438d9172 --- /dev/null +++ b/tests/compile/userdata_meta_owned_self.rs @@ -0,0 +1,12 @@ +#[derive(Default, mlua::UserData)] +struct Foo; + +#[mlua::userdata_impl] +impl Foo { + #[lua(meta)] + fn __gc(self) -> mlua::Result<()> { + Ok(()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_meta_owned_self.stderr b/tests/compile/userdata_meta_owned_self.stderr new file mode 100644 index 00000000..b0157f2f --- /dev/null +++ b/tests/compile/userdata_meta_owned_self.stderr @@ -0,0 +1,5 @@ +error: meta methods cannot take `self`, use `&[mut] self` instead + --> tests/compile/userdata_meta_owned_self.rs:7:5 + | +7 | fn __gc(self) -> mlua::Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_mut_slice_arg.rs b/tests/compile/userdata_mut_slice_arg.rs new file mode 100644 index 00000000..4ef8ec79 --- /dev/null +++ b/tests/compile/userdata_mut_slice_arg.rs @@ -0,0 +1,11 @@ +#[derive(Default, mlua::UserData)] +struct Foo(Vec); + +#[mlua::userdata_impl] +impl Foo { + fn first(&self, data: &mut [u8]) -> mlua::Result { + Ok(data[0]) + } +} + +fn main() {} diff --git a/tests/compile/userdata_mut_slice_arg.stderr b/tests/compile/userdata_mut_slice_arg.stderr new file mode 100644 index 00000000..2b763cd6 --- /dev/null +++ b/tests/compile/userdata_mut_slice_arg.stderr @@ -0,0 +1,5 @@ +error: this reference type is not supported as a callback parameter + --> tests/compile/userdata_mut_slice_arg.rs:6:27 + | +6 | fn first(&self, data: &mut [u8]) -> mlua::Result { + | ^^^^^^^^^ diff --git a/tests/compile/userdata_setter_async.rs b/tests/compile/userdata_setter_async.rs new file mode 100644 index 00000000..180f425d --- /dev/null +++ b/tests/compile/userdata_setter_async.rs @@ -0,0 +1,15 @@ +use mlua::Result; + +#[derive(Clone, Debug, mlua::UserData)] +struct Foo(u64); + +#[mlua::userdata_impl] +impl Foo { + #[lua(setter)] + async fn set_value(&mut self, val: u64) -> Result<()> { + self.0 = val; + Ok(()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_setter_async.stderr b/tests/compile/userdata_setter_async.stderr new file mode 100644 index 00000000..3125c1a0 --- /dev/null +++ b/tests/compile/userdata_setter_async.stderr @@ -0,0 +1,13 @@ +error: async field setter is not supported + --> tests/compile/userdata_setter_async.rs:9:5 + | +9 | async fn set_value(&mut self, val: u64) -> Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +warning: unused import: `mlua::Result` + --> tests/compile/userdata_setter_async.rs:1:5 + | +1 | use mlua::Result; + | ^^^^^^^^^^^^ + | + = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default diff --git a/tests/compile/userdata_setter_no_value.rs b/tests/compile/userdata_setter_no_value.rs new file mode 100644 index 00000000..5218c8d6 --- /dev/null +++ b/tests/compile/userdata_setter_no_value.rs @@ -0,0 +1,14 @@ +#[derive(Default, mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(setter)] + fn set_x(&mut self) -> mlua::Result<()> { + Ok(()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_setter_no_value.stderr b/tests/compile/userdata_setter_no_value.stderr new file mode 100644 index 00000000..4b521419 --- /dev/null +++ b/tests/compile/userdata_setter_no_value.stderr @@ -0,0 +1,5 @@ +error: field setter must take exactly one value argument + --> tests/compile/userdata_setter_no_value.rs:9:5 + | +9 | fn set_x(&mut self) -> mlua::Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_setter_ref_self.rs b/tests/compile/userdata_setter_ref_self.rs new file mode 100644 index 00000000..b88ea1e1 --- /dev/null +++ b/tests/compile/userdata_setter_ref_self.rs @@ -0,0 +1,15 @@ +#[derive(Default, mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(setter)] + fn set_x(self, val: u32) -> mlua::Result<()> { + let _ = val; + Ok(()) + } +} + +fn main() {} diff --git a/tests/compile/userdata_setter_ref_self.stderr b/tests/compile/userdata_setter_ref_self.stderr new file mode 100644 index 00000000..d15604f8 --- /dev/null +++ b/tests/compile/userdata_setter_ref_self.stderr @@ -0,0 +1,5 @@ +error: field setter must take `&[mut] self` + --> tests/compile/userdata_setter_ref_self.rs:9:5 + | +9 | fn set_x(self, val: u32) -> mlua::Result<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/compile/userdata_static_with_self.rs b/tests/compile/userdata_static_with_self.rs new file mode 100644 index 00000000..ad2ea1dc --- /dev/null +++ b/tests/compile/userdata_static_with_self.rs @@ -0,0 +1,14 @@ +#[derive(Default, mlua::UserData)] +struct Foo { + x: u32, +} + +#[mlua::userdata_impl] +impl Foo { + #[lua(field)] + fn get_x(&self) -> mlua::Result { + Ok(self.x) + } +} + +fn main() {} diff --git a/tests/compile/userdata_static_with_self.stderr b/tests/compile/userdata_static_with_self.stderr new file mode 100644 index 00000000..56b4b419 --- /dev/null +++ b/tests/compile/userdata_static_with_self.stderr @@ -0,0 +1,5 @@ +error: field function must not take `self` + --> tests/compile/userdata_static_with_self.rs:9:5 + | +9 | fn get_x(&self) -> mlua::Result { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/error.rs b/tests/error.rs index 09bdd5b1..8023dd1e 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -1,4 +1,5 @@ use std::error::Error as _; +use std::sync::Arc; use std::{fmt, io}; use mlua::{Error, ErrorContext, Lua, Result}; @@ -77,6 +78,44 @@ fn test_error_chain() -> Result<()> { Ok(()) } +#[test] +fn test_error_downcast() -> Result<()> { + let lua = Lua::new(); + + let func = lua.create_function(|_, ()| Err::<(), _>(Error::external(io::Error::other("boom"))))?; + let err = func.call::<()>(()).unwrap_err(); + assert!(matches!(err, Error::CallbackError { .. })); + assert!(err.downcast_ref::().is_some()); + assert!(err.downcast_ref::().is_none()); + + let bad_arg = Error::BadArgument { + to: Some("func".to_string()), + pos: 1, + name: Some("arg".to_string()), + cause: Arc::new(Error::external(io::Error::new( + io::ErrorKind::InvalidInput, + "bad", + ))), + }; + assert!(bad_arg.downcast_ref::().is_some()); + assert!(bad_arg.downcast_ref::().is_none()); + + Ok(()) +} + +#[test] +fn test_external_error() { + // `Error::external` should preserve `mlua::Error` + let runtime_err = Error::runtime("test error"); + let converted = Error::external(runtime_err); + assert!(matches!(converted, Error::RuntimeError(ref msg) if msg == "test error")); + + // Other errors should become `ExternalError` + let converted = Error::external(io::Error::other("other error")); + assert!(matches!(converted, Error::ExternalError(_))); + assert!(converted.downcast_ref::().is_some()); +} + #[cfg(feature = "anyhow")] #[test] fn test_error_anyhow() -> Result<()> { diff --git a/tests/function.rs b/tests/function.rs index d01e4fa9..a227f73b 100644 --- a/tests/function.rs +++ b/tests/function.rs @@ -1,3 +1,6 @@ +use std::fmt; +use std::result::Result as StdResult; + use mlua::{Error, Function, Lua, LuaString, Result, Table, Variadic}; #[test] @@ -236,7 +239,7 @@ fn test_function_dump() -> Result<()> { fn test_function_coverage() -> Result<()> { let lua = Lua::new(); - lua.set_compiler(mlua::Compiler::default().set_coverage_level(1)); + lua.set_compiler(mlua::chunk::Compiler::default().set_coverage_level(1)); let f = lua .load( @@ -343,7 +346,7 @@ fn test_function_deep_clone() -> Result<()> { fn test_function_wrap() -> Result<()> { let lua = Lua::new(); - let f = Function::wrap(|s: LuaString, n| Ok(s.to_str().unwrap().repeat(n))); + let f = Function::wrap(|s: LuaString, n| Ok::<_, Error>(s.to_str().unwrap().repeat(n))); lua.globals().set("f", f)?; lua.load(r#"assert(f("hello", 2) == "hellohello")"#) .exec() @@ -361,11 +364,40 @@ fn test_function_wrap() -> Result<()> { .exec() .unwrap(); + // Return external error + #[derive(Debug)] + struct MyError(String); + impl fmt::Display for MyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "MyError: {}", self.0) + } + } + impl std::error::Error for MyError {} + + let fext = Function::wrap(|s: String| -> StdResult { + if s == "bad" { + return Err(MyError("bad input".into())); + } + Ok(format!("ok: {s}")) + }); + lua.globals().set("fext", fext)?; + lua.load(r#"assert(fext("hello") == "ok: hello")"#) + .exec() + .unwrap(); + lua.load( + r#" + local ok, err = pcall(fext, "bad") + assert(not ok and tostring(err):find("MyError: bad input")) + "#, + ) + .exec() + .unwrap(); + // Mutable callback let mut i = 0; let fmut = Function::wrap_mut(move || { i += 1; - Ok(i) + Ok::<_, Error>(i) }); lua.globals().set("fmut", fmut)?; lua.load(r#"fmut(); fmut(); assert(fmut() == 3)"#).exec().unwrap(); @@ -385,7 +417,7 @@ fn test_function_wrap() -> Result<()> { // Check recursive mut callback error let fmut = Function::wrap_mut(|f: Function| match f.call::<()>(&f) { Err(Error::CallbackError { cause, .. }) => match cause.as_ref() { - Error::RecursiveMutCallback { .. } => Ok(()), + Error::RecursiveMutCallback { .. } => Ok::<_, Error>(()), other => panic!("incorrect result: {other:?}"), }, other => panic!("incorrect result: {other:?}"), diff --git a/tests/hooks.rs b/tests/hooks.rs index c5d7da32..8a6270a4 100644 --- a/tests/hooks.rs +++ b/tests/hooks.rs @@ -3,7 +3,8 @@ use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Mutex}; -use mlua::{DebugEvent, Error, HookTriggers, Lua, Result, ThreadStatus, Value, VmState}; +use mlua::debug::DebugEvent; +use mlua::{Error, HookTriggers, Lua, Result, Value, VmState}; #[test] fn test_hook_triggers() { @@ -280,19 +281,53 @@ fn test_hook_yield() -> Result<()> { assert!(co.resume::<()>(()).is_ok()); assert!(co.resume::<()>(()).is_ok()); assert!(co.resume::<()>(()).is_ok()); - assert!(co.status() == ThreadStatus::Finished); + assert!(co.is_finished()); } #[cfg(any(feature = "lua51", feature = "lua52", feature = "luajit"))] { assert!( matches!(co.resume::<()>(()), Err(Error::RuntimeError(err)) if err.contains("attempt to yield from a hook")) ); - assert!(co.status() == ThreadStatus::Error); + assert!(co.is_error()); } Ok(()) } +#[test] +#[cfg(any(feature = "lua55", feature = "lua54", feature = "lua53"))] +fn test_hook_yield_preserves_stack() -> Result<()> { + let lua = Lua::new(); + + let func = lua + .load( + r#" + local x = { value = 40 } + local y = 2 + return x.value + y + "#, + ) + .into_function()?; + let co = lua.create_thread(func)?; + + let yielded = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let yielded2 = yielded.clone(); + co.set_hook(HookTriggers::EVERY_LINE, move |_lua, debug| { + if debug.current_line() == Some(4) && !yielded2.swap(true, Ordering::Relaxed) { + return Ok(VmState::Yield); + } + Ok(VmState::Continue) + })?; + + co.resume::<()>(())?; + assert!(yielded.load(Ordering::Relaxed)); + co.remove_hook(); + lua.gc_collect()?; + assert_eq!(co.resume::(())?, 42); + + Ok(()) +} + #[test] fn test_global_hook() -> Result<()> { let lua = Lua::new(); @@ -320,7 +355,7 @@ fn test_global_hook() -> Result<()> { thread.resume::<()>(()).unwrap(); lua.remove_global_hook(); thread.resume::<()>(()).unwrap(); - assert_eq!(thread.status(), ThreadStatus::Finished); + assert!(thread.is_finished()); assert_eq!(counter.load(Ordering::Relaxed), 3); Ok(()) diff --git a/tests/luau.rs b/tests/luau.rs index 8f745768..f97dfa0f 100644 --- a/tests/luau.rs +++ b/tests/luau.rs @@ -1,14 +1,11 @@ #![cfg(feature = "luau")] -use std::cell::Cell; use std::fmt::Debug; -use std::os::raw::c_void; use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicPtr, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicU64, Ordering}; -use mlua::{ - Compiler, Error, Function, Lua, LuaOptions, Result, StdLib, Table, ThreadStatus, Value, Vector, VmState, -}; +use mlua::chunk::Compiler; +use mlua::{Error, Function, Lua, LuaOptions, ObjectLike, Result, StdLib, Table, Value, Vector, VmState}; #[test] fn test_version() -> Result<()> { @@ -22,10 +19,11 @@ fn test_version() -> Result<()> { fn test_vectors() -> Result<()> { let lua = Lua::new(); - let v: Vector = lua + let v: Value = lua .load("vector.create(1, 2, 3) + vector.create(3, 2, 1)") .eval()?; - assert_eq!(v, [4.0, 4.0, 4.0]); + assert!(v.is_vector()); + assert_eq!(v.as_vector().unwrap(), [4.0, 4.0, 4.0]); // Test conversion into Rust array let v: [f64; 3] = lua.load("vector.create(1, 2, 3)").eval()?; @@ -62,10 +60,11 @@ fn test_vectors() -> Result<()> { fn test_vectors() -> Result<()> { let lua = Lua::new(); - let v: Vector = lua + let v: Value = lua .load("vector.create(1, 2, 3, 4) + vector.create(4, 3, 2, 1)") .eval()?; - assert_eq!(v, [5.0, 5.0, 5.0, 5.0]); + assert!(v.is_vector()); + assert_eq!(v.as_vector().unwrap(), [5.0, 5.0, 5.0, 5.0]); // Test conversion into Rust array let v: [f64; 4] = lua.load("vector.create(1, 2, 3, 4)").eval()?; @@ -99,7 +98,6 @@ fn test_vectors() -> Result<()> { Ok(()) } -#[cfg(not(feature = "luau-vector4"))] #[test] fn test_vector_metatable() -> Result<()> { let lua = Lua::new(); @@ -324,11 +322,11 @@ fn test_interrupts() -> Result<()> { .into_function()?, )?; co.resume::<()>(())?; - assert_eq!(co.status(), ThreadStatus::Resumable); + assert!(co.is_resumable()); let result: i32 = co.resume(())?; assert_eq!(result, 6); assert_eq!(yield_count.load(Ordering::Relaxed), 7); - assert_eq!(co.status(), ThreadStatus::Finished); + assert!(co.is_finished()); // Test no yielding at non-yieldable points yield_count.store(0, Ordering::Relaxed); @@ -359,83 +357,28 @@ fn test_fflags() { assert!(Lua::set_fflag("UnknownFlag", true).is_err()); } +#[cfg(feature = "luau-jit")] #[test] -fn test_thread_events() -> Result<()> { +fn test_jit_inliner() -> Result<()> { let lua = Lua::new(); + lua.set_jit_options(mlua::state::JitOptions::new().inliner(true)); - let count = Arc::new(AtomicU64::new(0)); - let thread_data: Arc<(AtomicPtr, AtomicBool)> = Arc::new(Default::default()); - - let (count2, thread_data2) = (count.clone(), thread_data.clone()); - lua.set_thread_creation_callback(move |_, thread| { - count2.fetch_add(1, Ordering::Relaxed); - (thread_data2.0).store(thread.to_pointer() as *mut _, Ordering::Relaxed); - thread_data2.1.store(false, Ordering::Relaxed); - Ok(()) - }); - let (count3, thread_data3) = (count.clone(), thread_data.clone()); - lua.set_thread_collection_callback(move |thread_ptr| { - count3.fetch_add(1, Ordering::Relaxed); - if thread_data3.0.load(Ordering::Relaxed) == thread_ptr.0 { - thread_data3.1.store(true, Ordering::Relaxed); - } - }); - - let t = lua.create_thread(lua.load("return 123").into_function()?)?; - assert_eq!(count.load(Ordering::Relaxed), 1); - let t_ptr = t.to_pointer(); - assert_eq!(t_ptr, thread_data.0.load(Ordering::Relaxed)); - assert!(!thread_data.1.load(Ordering::Relaxed)); - - // Thead will be destroyed after GC cycle - drop(t); - lua.gc_collect()?; - assert_eq!(count.load(Ordering::Relaxed), 2); - assert_eq!(t_ptr, thread_data.0.load(Ordering::Relaxed)); - assert!(thread_data.1.load(Ordering::Relaxed)); - - // Check that recursion is not allowed - let count4 = count.clone(); - lua.set_thread_creation_callback(move |lua, _value| { - count4.fetch_add(1, Ordering::Relaxed); - let _ = lua.create_thread(lua.load("return 123").into_function().unwrap())?; - Ok(()) - }); - let t = lua.create_thread(lua.load("return 123").into_function()?)?; - assert_eq!(count.load(Ordering::Relaxed), 3); - - lua.remove_thread_callbacks(); - drop(t); - lua.gc_collect()?; - assert_eq!(count.load(Ordering::Relaxed), 3); - - // Test error inside callback - lua.set_thread_creation_callback(move |_, _| Err(Error::runtime("error when processing thread event"))); - let result = lua.create_thread(lua.load("return 123").into_function()?); - assert!(result.is_err()); - assert!( - matches!(result, Err(Error::RuntimeError(err)) if err.contains("error when processing thread event")) - ); - - // Test context switch when running Lua script - let count = Cell::new(0); - lua.set_thread_creation_callback(move |_, _| { - count.set(count.get() + 1); - if count.get() == 2 { - return Err(Error::runtime("thread limit exceeded")); - } - Ok(()) - }); - let result = lua + // An inlinable helper called in a hot loop. + let sum = lua .load( r#" - local co = coroutine.wrap(function() return coroutine.create(print) end) - co() - "#, + local function add(a, b) + return a + b + end + local sum = 0 + for i = 1, 1000 do + sum = add(sum, i) + end + return sum + "#, ) - .exec(); - assert!(result.is_err()); - assert!(matches!(result, Err(Error::RuntimeError(err)) if err.contains("thread limit exceeded"))); + .eval::()?; + assert_eq!(sum, 500500); Ok(()) } @@ -535,5 +478,23 @@ fn test_heap_dump() -> Result<()> { Ok(()) } +#[test] +fn test_integer64_type() -> Result<()> { + let lua = Lua::new(); + + _ = Lua::set_fflag("LuauIntegerType2", true); + + let integer_lib = lua.globals().get::
("integer")?; + let n = integer_lib.call_function::("create", 42)?; + assert_eq!(n, 42); + + let n: i64 = lua.load("return 42i").eval()?; + assert_eq!(n, 42); + let n: i64 = lua.load("return -42i").eval()?; + assert_eq!(n, -42); + + Ok(()) +} + #[path = "luau/require.rs"] mod require; diff --git a/tests/luau/require.rs b/tests/luau/require.rs index ba79fb6a..ebdfcb05 100644 --- a/tests/luau/require.rs +++ b/tests/luau/require.rs @@ -1,7 +1,8 @@ use std::io::Result as IoResult; use std::result::Result as StdResult; -use mlua::{Error, FromLua, IntoLua, Lua, MultiValue, NavigateError, Require, Result, TextRequirer, Value}; +use mlua::luau::{FsRequirer, NavigateError, Require}; +use mlua::{Error, FromLua, IntoLua, Lua, MultiValue, Result, Value}; fn run_require(lua: &Lua, path: impl IntoLua) -> Result { lua.load(r#"return require(...)"#).call(path) @@ -65,7 +66,7 @@ fn test_require_errors() { assert!((res.unwrap_err().to_string()).contains("@ is not a valid alias")); // Test throwing mlua::Error - struct MyRequire(TextRequirer); + struct MyRequire(FsRequirer); impl Require for MyRequire { fn is_require_allowed(&self, chunk_name: &str) -> bool { @@ -109,9 +110,7 @@ fn test_require_errors() { } } - let require = lua - .create_require_function(MyRequire(TextRequirer::new())) - .unwrap(); + let require = lua.create_require_function(MyRequire(FsRequirer::new())).unwrap(); lua.globals().set("require", require).unwrap(); let res = lua.load(r#"return require('./a/relative/path')"#).exec(); assert!((res.unwrap_err().to_string()).contains("test error")); @@ -252,6 +251,152 @@ fn test_require_with_config_luau() { test_require_with_config_inner("with_config_luau"); } +#[test] +fn test_alias_override() { + let lua = Lua::new(); + + struct OverrideRequire(FsRequirer); + + impl Require for OverrideRequire { + fn is_require_allowed(&self, chunk_name: &str) -> bool { + self.0.is_require_allowed(chunk_name) + } + + fn reset(&mut self, chunk_name: &str) -> StdResult<(), NavigateError> { + self.0.reset(chunk_name) + } + + fn jump_to_alias(&mut self, path: &str) -> StdResult<(), NavigateError> { + self.0.jump_to_alias(path) + } + + fn to_alias_override(&mut self, alias: &str) -> StdResult<(), NavigateError> { + if alias == "testoverride" { + self.0.jump_to_alias("./tests/luau/require/without_config") + } else { + Err(NavigateError::NotFound) + } + } + + fn to_parent(&mut self) -> StdResult<(), NavigateError> { + self.0.to_parent() + } + + fn to_child(&mut self, name: &str) -> StdResult<(), NavigateError> { + self.0.to_child(name) + } + + fn has_module(&self) -> bool { + self.0.has_module() + } + + fn cache_key(&self) -> String { + self.0.cache_key() + } + + fn has_config(&self) -> bool { + self.0.has_config() + } + + fn config(&self) -> IoResult> { + self.0.config() + } + + fn loader(&self, lua: &Lua) -> Result { + self.0.loader(lua) + } + } + + let require_fn = lua + .create_require_function(OverrideRequire(FsRequirer::new())) + .unwrap(); + lua.globals().set("require", require_fn).unwrap(); + + // to_alias_override intercepts before config-file search + let res = run_require(&lua, "@testoverride/dependency").unwrap(); + assert_eq!("result from dependency", get_str(&res, 1)); + + // Different sub-path through the same alias + let res = run_require(&lua, "@testoverride/module").unwrap(); + assert_eq!("required into module", get_str(&res, 2)); + + // Aliases not handled by the override still fail normally + let res = run_require(&lua, "@unknown_alias_xyz/anything"); + assert!(res.is_err()); + assert!((res.unwrap_err().to_string()).contains("@unknown_alias_xyz is not a valid alias")); +} + +#[test] +fn test_alias_fallback() { + let lua = Lua::new(); + + struct FallbackRequire(FsRequirer); + + impl Require for FallbackRequire { + fn is_require_allowed(&self, chunk_name: &str) -> bool { + self.0.is_require_allowed(chunk_name) + } + + fn reset(&mut self, chunk_name: &str) -> StdResult<(), NavigateError> { + self.0.reset(chunk_name) + } + + fn jump_to_alias(&mut self, path: &str) -> StdResult<(), NavigateError> { + self.0.jump_to_alias(path) + } + + fn to_alias_fallback(&mut self, alias: &str) -> StdResult<(), NavigateError> { + if alias == "testfallback" { + self.0.jump_to_alias("./tests/luau/require/without_config") + } else { + Err(NavigateError::NotFound) + } + } + + fn to_parent(&mut self) -> StdResult<(), NavigateError> { + self.0.to_parent() + } + + fn to_child(&mut self, name: &str) -> StdResult<(), NavigateError> { + self.0.to_child(name) + } + + fn has_module(&self) -> bool { + self.0.has_module() + } + + fn cache_key(&self) -> String { + self.0.cache_key() + } + + fn has_config(&self) -> bool { + self.0.has_config() + } + + fn config(&self) -> IoResult> { + self.0.config() + } + + fn loader(&self, lua: &Lua) -> Result { + self.0.loader(lua) + } + } + + let require_fn = lua + .create_require_function(FallbackRequire(FsRequirer::new())) + .unwrap(); + lua.globals().set("require", require_fn).unwrap(); + + // to_alias_fallback catches after config-file search misses + let res = run_require(&lua, "@testfallback/dependency").unwrap(); + assert_eq!("result from dependency", get_str(&res, 1)); + + // Aliases not handled by the fallback still fail + let res = run_require(&lua, "@unknown_alias_xyz/anything"); + assert!(res.is_err()); + assert!((res.unwrap_err().to_string()).contains("@unknown_alias_xyz is not a valid alias")); +} + #[cfg(all(feature = "async", not(windows)))] #[tokio::test] async fn test_async_require() -> Result<()> { diff --git a/tests/memory.rs b/tests/memory.rs index 930aa95d..e8cd7e2b 100644 --- a/tests/memory.rs +++ b/tests/memory.rs @@ -1,6 +1,10 @@ use std::sync::Arc; -use mlua::{Error, GCMode, Lua, Result, UserData}; +use mlua::state::{GcIncParams, GcMode}; +use mlua::{Error, Lua, Result, UserData}; + +#[cfg(any(feature = "lua54", feature = "lua55"))] +use mlua::state::GcGenParams; #[test] fn test_memory_limit() -> Result<()> { @@ -74,8 +78,14 @@ fn test_gc_control() -> Result<()> { #[cfg(any(feature = "lua55", feature = "lua54"))] { - assert_eq!(lua.gc_gen(0, 0), GCMode::Incremental); - assert_eq!(lua.gc_inc(0, 0, 0), GCMode::Generational); + assert!(matches!( + lua.gc_set_mode(GcMode::Generational(GcGenParams::default())), + GcMode::Incremental(_) + )); + assert!(matches!( + lua.gc_set_mode(GcMode::Incremental(GcIncParams::default())), + GcMode::Generational(_) + )); } #[cfg(any( @@ -93,7 +103,17 @@ fn test_gc_control() -> Result<()> { assert!(lua.gc_is_running()); } - assert_eq!(lua.gc_inc(200, 100, 13), GCMode::Incremental); + assert!(matches!( + lua.gc_set_mode(GcMode::Incremental({ + let p = GcIncParams::default().step_multiplier(100); + #[cfg(not(feature = "luau"))] + let p = p.pause(200); + #[cfg(feature = "luau")] + let p = p.goal(200); + p + })), + GcMode::Incremental(_) + )); struct MyUserdata(#[allow(unused)] Arc<()>); impl UserData for MyUserdata {} @@ -110,6 +130,36 @@ fn test_gc_control() -> Result<()> { Ok(()) } +#[cfg(any(feature = "lua54", feature = "lua55"))] +#[test] +fn test_gc_set_mode_in_finalizer() -> Result<()> { + use std::sync::atomic::{AtomicBool, Ordering}; + + let lua = Lua::new(); + + // While finalizers are running, the GC is internally stopped and `lua_gc` rejects all + // options + let switched = Arc::new(AtomicBool::new(false)); + let switched2 = switched.clone(); + let finalizer = lua.create_function(move |lua, ()| { + let mode = lua.gc_set_mode(GcMode::Generational(GcGenParams::default())); + switched2.store(matches!(mode, GcMode::Generational(_)), Ordering::Relaxed); + Ok(()) + })?; + lua.globals().set("finalizer", finalizer)?; + lua.load("setmetatable({}, { __gc = finalizer })").exec()?; + lua.globals().raw_remove("finalizer")?; + + lua.gc_collect()?; + lua.gc_collect()?; + assert!( + switched.load(Ordering::Relaxed), + "gc_set_mode did not complete in finalizer" + ); + + Ok(()) +} + #[cfg(any(feature = "lua53", feature = "lua52"))] #[test] fn test_gc_error() { diff --git a/tests/send.rs b/tests/send.rs index f9803f5b..2f10466a 100644 --- a/tests/send.rs +++ b/tests/send.rs @@ -1,50 +1,7 @@ #![cfg(feature = "send")] -use std::cell::UnsafeCell; -use std::marker::PhantomData; - -use mlua::{AnyUserData, Error, Lua, ObjectLike, Result, UserData, UserDataMethods, UserDataRef}; -use static_assertions::{assert_impl_all, assert_not_impl_all}; - -#[test] -fn test_userdata_multithread_access_send_only() -> Result<()> { - let lua = Lua::new(); - - // This type is `Send` but not `Sync`. - struct MyUserData(String, PhantomData>); - assert_impl_all!(MyUserData: Send); - assert_not_impl_all!(MyUserData: Sync); - - impl UserData for MyUserData { - fn add_methods>(methods: &mut M) { - methods.add_method("method", |lua, this, ()| { - let ud = lua.globals().get::("ud")?; - assert_eq!(ud.call_method::("method2", ())?, "method2"); - Ok(this.0.clone()) - }); - - methods.add_method("method2", |_, _, ()| Ok("method2")); - } - } - - lua.globals() - .set("ud", MyUserData("hello".to_string(), PhantomData))?; - - // We acquired the exclusive reference. - let ud = lua.globals().get::>("ud")?; - - std::thread::scope(|s| { - s.spawn(|| { - let res = lua.globals().get::>("ud"); - assert!(matches!(res, Err(Error::UserDataBorrowError))); - }); - }); - - drop(ud); - lua.load("ud:method()").exec().unwrap(); - - Ok(()) -} +use mlua::{AnyUserData, Lua, ObjectLike, Result, UserData, UserDataMethods, UserDataRef}; +use static_assertions::assert_impl_all; #[test] fn test_userdata_multithread_access_sync() -> Result<()> { @@ -74,13 +31,11 @@ fn test_userdata_multithread_access_sync() -> Result<()> { std::thread::scope(|s| { s.spawn(|| { // Getting another shared reference for `Sync` type is allowed. - // FIXME: does not work due to https://github.com/rust-lang/rust/pull/135634 - // let _ = lua.globals().get::>("ud").unwrap(); + let _ = lua.globals().get::>("ud").unwrap(); }); }); - // FIXME: does not work due to https://github.com/rust-lang/rust/pull/135634 - // lua.load("ud:method()").exec().unwrap(); + lua.load("ud:method()").exec().unwrap(); Ok(()) } diff --git a/tests/serde.rs b/tests/serde.rs index d3cc00ed..15508a29 100644 --- a/tests/serde.rs +++ b/tests/serde.rs @@ -4,9 +4,9 @@ use std::collections::HashMap; use std::error::Error as StdError; use bstr::BString; +use mlua::serde::{DeserializeOptions, SerializeOptions}; use mlua::{ - AnyUserData, DeserializeOptions, Error, ExternalResult, IntoLua, Lua, LuaSerdeExt, Result as LuaResult, - SerializeOptions, UserData, Value, + AnyUserData, Error, ExternalResult, IntoLua, Lua, LuaSerdeExt, Result as LuaResult, UserData, Value, }; use serde::{Deserialize, Serialize}; @@ -305,6 +305,12 @@ fn test_serialize_mixed_table() -> LuaResult<()> { 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"}"#); + // Array metatable takes precedence + let table = lua.load(r#"{1,2,3, key="value"}"#).eval::()?; + (table.as_table().unwrap()).set_metatable(Some(lua.array_metatable()))?; + let json = serde_json::to_string(&table.to_serializable().detect_mixed_tables(true)).unwrap(); + assert_eq!(json, r#"[1,2,3]"#); + Ok(()) } diff --git a/tests/string.rs b/tests/string.rs index 158a5b5f..6802d906 100644 --- a/tests/string.rs +++ b/tests/string.rs @@ -5,27 +5,37 @@ use mlua::{Lua, LuaString, Result}; #[test] fn test_string_compare() { - fn with_str(s: &str, f: F) { - f(Lua::new().create_string(s).unwrap()); + let lua = Lua::new(); + + fn with_str(lua: &Lua, s: &str, f: F) { + f(lua.create_string(s).unwrap()); } // Tests that all comparisons we want to have are usable - with_str("teststring", |t| assert_eq!(t, "teststring")); // &str - with_str("teststring", |t| assert_eq!(t, b"teststring")); // &[u8] - with_str("teststring", |t| assert_eq!(t, b"teststring".to_vec())); // Vec - with_str("teststring", |t| assert_eq!(t, "teststring".to_string())); // String - with_str("teststring", |t| assert_eq!(t, t)); // mlua::String - with_str("teststring", |t| assert_eq!(t, Cow::from(b"teststring".as_ref()))); // Cow (borrowed) - with_str("bla", |t| assert_eq!(t, Cow::from(b"bla".to_vec()))); // Cow (owned) + with_str(&lua, "teststring", |t| assert_eq!(t, "teststring")); // &str + with_str(&lua, "teststring", |t| assert_eq!(t, b"teststring")); // &[u8] + with_str(&lua, "teststring", |t| assert_eq!(t, b"teststring".to_vec())); // Vec + with_str(&lua, "teststring", |t| assert_eq!(t, "teststring".to_string())); // String + with_str(&lua, "teststring", |t| assert_eq!(t, t)); // mlua::String + with_str(&lua, "teststring", |t| { + assert_eq!(t, Cow::from(b"teststring".as_ref())) // Cow (borrowed) + }); + with_str(&lua, "bla", |t| assert_eq!(t, Cow::from(b"bla".to_vec()))); // Cow (owned) // Test ordering - with_str("a", |a| { + with_str(&lua, "a", |a| { assert!(!(a < a)); assert!(!(a > a)); }); - with_str("a", |a| assert!(a < "b")); - with_str("a", |a| assert!(a < b"b")); - with_str("a", |a| with_str("b", |b| assert!(a < b))); + with_str(&lua, "a", |a| assert!(a < "b")); + with_str(&lua, "a", |a| assert!(a < b"b")); + with_str(&lua, "a", |a| with_str(&lua, "b", |b| assert!(a < b))); + + // Long strings (not interned by Lua) + let long_str = "abc".repeat(100); + with_str(&lua, &long_str, |s1| { + with_str(&lua, &long_str, |s2| assert_eq!(s1, s2)) + }); } #[test] diff --git a/tests/table.rs b/tests/table.rs index e0bc5f44..f4aaf9b0 100644 --- a/tests/table.rs +++ b/tests/table.rs @@ -142,6 +142,38 @@ fn test_table_insert_remove() -> Result<()> { Ok(()) } +#[test] +fn test_table_remove_metatable() -> Result<()> { + let lua = Lua::new(); + + let inner = lua.create_sequence_from([1, 2, 3, 4, 5])?; + let mt = lua.create_table()?; + mt.set("__index", &inner)?; + mt.set("__newindex", &inner)?; + mt.set("__len", { + let inner = inner.clone(); + lua.create_function(move |_, ()| Ok(inner.raw_len()))? + })?; + + let t = lua.create_table()?; + t.set_metatable(Some(mt))?; + + t.remove(2)?; // removes value `2` + assert_eq!(t.len()?, 4); + assert_eq!( + inner.pairs().collect::>>()?, + vec![(1, 1), (2, 3), (3, 4), (4, 5)] + ); + + // Remove non-integer key + t.set("abc", "abcdef")?; + assert_eq!(inner.get::("abc")?, "abcdef"); + t.remove("abc")?; + assert_eq!(inner.get::("abc")?, Value::Nil); + + Ok(()) +} + #[test] fn test_table_clear() -> Result<()> { let lua = Lua::new(); diff --git a/tests/tests.rs b/tests/tests.rs index 47151387..ae5b6545 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -5,9 +5,10 @@ use std::panic::{AssertUnwindSafe, catch_unwind}; use std::sync::Arc; use std::{error, f32, f64, fmt}; +use mlua::chunk::ChunkMode; use mlua::{ - ChunkMode, Error, ExternalError, Function, Lua, LuaOptions, Nil, Result, StdLib, Table, UserData, Value, - Variadic, ffi, + Error, ExternalError, Function, Lua, LuaOptions, Nil, Result, StdLib, Table, UserData, Value, Variadic, + ffi, }; #[test] @@ -132,9 +133,9 @@ fn test_exec() -> Result<()> { fn test_eval() -> Result<()> { let lua = Lua::new(); - assert_eq!(lua.load("1 + 1").eval::()?, 2); + assert_eq!(lua.load("\t1 + 1").eval::()?, 2); assert_eq!(lua.load("false == false").eval::()?, true); - assert_eq!(lua.load("return 1 + 2").eval::()?, 3); + assert_eq!(lua.load("\nreturn 1 + 2").eval::()?, 3); match lua.load("if true then").eval::<()>() { Err(Error::SyntaxError { incomplete_input: true, @@ -187,7 +188,7 @@ fn test_load_mode() -> Result<()> { #[cfg(not(feature = "luau"))] let bytecode = lua.load("return 1 + 1").into_function()?.dump(true); #[cfg(feature = "luau")] - let bytecode = mlua::Compiler::new().compile("return 1 + 1")?; + let bytecode = mlua::chunk::Compiler::new().compile("return 1 + 1")?; assert_eq!(lua.load(&bytecode).eval::()?, 2); assert_eq!(lua.load(&bytecode).set_mode(ChunkMode::Binary).eval::()?, 2); match lua.load(&bytecode).set_mode(ChunkMode::Text).exec() { @@ -1374,7 +1375,7 @@ fn test_inspect_stack() -> Result<()> { local function baz(a, b, c, ...) return stack_info() end - assert(baz() == 'DebugStack { num_ups: 1, num_params: 3, is_vararg: true }') + assert(baz() == 'DebugStack { num_upvalues: 1, num_params: 3, is_vararg: true }') "#, ) .exec()?; @@ -1387,7 +1388,7 @@ fn test_inspect_stack() -> Result<()> { local function baz(a, b, c, ...) return stack_info() end - assert(baz() == 'DebugStack { num_ups: 1 }') + assert(baz() == 'DebugStack { num_upvalues: 1 }') "#, ) .exec()?; diff --git a/tests/thread.rs b/tests/thread.rs index 71eb24c9..a8b52b2d 100644 --- a/tests/thread.rs +++ b/tests/thread.rs @@ -1,6 +1,9 @@ use std::panic::catch_unwind; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; -use mlua::{Error, Function, IntoLua, Lua, Result, Thread, ThreadStatus, Value}; +use mlua::thread::{ThreadEvent, ThreadStatus, ThreadTriggers}; +use mlua::{Error, Function, IntoLua, Lua, Result, Thread, Value}; #[test] fn test_thread() -> Result<()> { @@ -21,17 +24,17 @@ fn test_thread() -> Result<()> { .eval()?, )?; - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(0)?, 0); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(1)?, 1); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(2)?, 3); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(3)?, 6); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(4)?, 10); - assert_eq!(thread.status(), ThreadStatus::Finished); + assert!(thread.is_finished()); let accumulate = lua.create_thread( lua.load( @@ -50,9 +53,9 @@ fn test_thread() -> Result<()> { accumulate.resume::<()>(i)?; } assert_eq!(accumulate.resume::(4)?, 10); - assert_eq!(accumulate.status(), ThreadStatus::Resumable); + assert!(accumulate.is_resumable()); assert!(accumulate.resume::<()>("error").is_err()); - assert_eq!(accumulate.status(), ThreadStatus::Error); + assert!(accumulate.is_error()); let thread = lua .load( @@ -65,7 +68,7 @@ fn test_thread() -> Result<()> { "#, ) .eval::()?; - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(thread.resume::(())?, 42); let thread: Thread = lua @@ -92,7 +95,7 @@ fn test_thread() -> Result<()> { // Already running thread must be unresumable let thread = lua.create_thread(lua.create_function(|lua, ()| { - assert_eq!(lua.current_thread().status(), ThreadStatus::Running); + assert!(lua.current_thread().is_running()); let result = lua.current_thread().resume::<()>(()); assert!( matches!(result, Err(Error::CoroutineUnresumable)), @@ -103,6 +106,32 @@ fn test_thread() -> Result<()> { let result = thread.resume::<()>(()); assert!(result.is_ok(), "unexpected result: {result:?}"); + // A thread that has resumed another thread (still running) is "normal". + let check_outer = lua.create_function(|lua, ()| { + let outer: Thread = lua.globals().get("outer")?; + assert!(outer.is_normal()); + assert!( + matches!(outer.resume::<()>(()), Err(Error::CoroutineUnresumable)), + "resuming a `normal` thread must be unresumable", + ); + Ok(()) + })?; + lua.globals().set("check_outer", check_outer)?; + let outer = lua.create_thread( + lua.load( + r#" + function() + local inner = coroutine.create(function() check_outer() end) + assert(coroutine.resume(inner)) + end + "#, + ) + .eval()?, + )?; + lua.globals().set("outer", &outer)?; + outer.resume::<()>(())?; + assert!(outer.is_finished()); + Ok(()) } @@ -123,12 +152,12 @@ fn test_thread_reset() -> Result<()> { assert!(thread.reset(func.clone()).is_ok()); for _ in 0..2 { - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); let _ = thread.resume::(MyUserData(arc.clone()))?; - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); assert_eq!(Arc::strong_count(&arc), 2); thread.resume::<()>(())?; - assert_eq!(thread.status(), ThreadStatus::Finished); + assert!(thread.is_finished()); thread.reset(func.clone())?; lua.gc_collect()?; assert_eq!(Arc::strong_count(&arc), 1); @@ -138,21 +167,21 @@ fn test_thread_reset() -> Result<()> { let func: Function = lua.load(r#"function(ud) error("test error") end"#).eval()?; let thread = lua.create_thread(func.clone())?; let _ = thread.resume::(MyUserData(arc.clone())); - assert_eq!(thread.status(), ThreadStatus::Error); + assert!(thread.is_error()); assert_eq!(Arc::strong_count(&arc), 2); #[cfg(any(feature = "lua55", feature = "lua54"))] { assert!(thread.reset(func.clone()).is_err()); // Reset behavior has changed in Lua v5.4.4 // It's became possible to force reset thread by popping error object - assert!(matches!(thread.status(), ThreadStatus::Finished)); + assert!(thread.is_finished()); assert!(thread.reset(func.clone()).is_ok()); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); } #[cfg(any(feature = "lua55", feature = "lua54", feature = "luau"))] { assert!(thread.reset(func.clone()).is_ok()); - assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); } // Try reset running thread @@ -275,3 +304,252 @@ fn test_thread_resume_bad_arg() -> Result<()> { Ok(()) } + +#[test] +fn test_thread_event_create() -> Result<()> { + let lua = Lua::new(); + + let created = Arc::new(AtomicBool::new(false)); + let created2 = created.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_CREATE, move |_lua, event| { + assert!(matches!(event, ThreadEvent::Create(_))); + created2.store(true, Ordering::Relaxed); + Ok(()) + }); + + let _thread = lua.create_thread(lua.create_function(|_, ()| Ok(()))?)?; + assert!(created.load(Ordering::Relaxed)); + + Ok(()) +} + +#[test] +fn test_thread_event_create_recursive() -> Result<()> { + let lua = Lua::new(); + + let count = Arc::new(AtomicU32::new(0)); + let count2 = count.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_CREATE, move |lua, event| { + assert!(matches!(event, ThreadEvent::Create(_))); + count2.fetch_add(1, Ordering::Relaxed); + // Creating a thread inside the callback + let _ = lua.create_thread(lua.load("return 321").into_function().unwrap())?; + Ok(()) + }); + + let _t = lua.create_thread(lua.load("return 123").into_function()?)?; + assert_eq!(count.load(Ordering::Relaxed), 1); + + Ok(()) +} + +#[test] +fn test_thread_event_create_error() -> Result<()> { + let lua = Lua::new(); + + lua.set_thread_event_callback(ThreadTriggers::ON_CREATE, move |_, _| Err(Error::runtime("blah"))); + + let result = lua.create_thread(lua.load("return 123").into_function()?); + assert!(result.is_err()); + assert!(matches!(result, Err(Error::RuntimeError(err)) if err.contains("blah"))); + + Ok(()) +} + +#[test] +fn test_thread_event_resume() -> Result<()> { + let lua = Lua::new(); + + let count = Arc::new(AtomicBool::new(false)); + let count2 = count.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_RESUME, move |_lua, event| { + assert!(matches!(event, ThreadEvent::Resume(_))); + count2.store(true, Ordering::Relaxed); + Ok(()) + }); + + let thread = lua.create_thread(lua.load("return 42").into_function()?)?; + thread.resume::<()>(())?; + + assert!(count.load(Ordering::Relaxed)); + Ok(()) +} + +#[test] +fn test_thread_event_resume_error() -> Result<()> { + let lua = Lua::new(); + + lua.set_thread_event_callback(ThreadTriggers::ON_RESUME, move |_lua, _event| { + Err(Error::runtime("abort resume")) + }); + + let thread = lua.create_thread(lua.load("return 42").into_function()?)?; + let err = thread.resume::<()>(()).unwrap_err(); + assert!(matches!(err, Error::RuntimeError(msg) if msg == "abort resume")); + assert!(thread.is_resumable()); + + Ok(()) +} + +#[test] +fn test_thread_event_yield() -> Result<()> { + let lua = Lua::new(); + + let count = Arc::new(AtomicBool::new(false)); + let count2 = count.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_YIELD, move |_lua, event| { + assert!(matches!(event, ThreadEvent::Yield(_))); + count2.store(true, Ordering::Relaxed); + Ok(()) + }); + + let thread = lua.create_thread(lua.load("coroutine.yield(1) return 2").into_function()?)?; + let val = thread.resume::(())?; + assert_eq!(val, 1); + assert!(count.load(Ordering::Relaxed)); + + // Reset flag and resume to completion + count.store(false, Ordering::Relaxed); + let val = thread.resume::(())?; + assert_eq!(val, 2); + // Yield hook should not fire on the final return + assert!(!count.load(Ordering::Relaxed)); + assert!(thread.is_finished()); + + Ok(()) +} + +#[test] +fn test_thread_event_yield_error() -> Result<()> { + let lua = Lua::new(); + + lua.set_thread_event_callback(ThreadTriggers::ON_YIELD, move |_lua, _event| { + Err(Error::runtime("yield error")) + }); + + let thread = lua.create_thread(lua.load("coroutine.yield(1)").into_function()?)?; + let err = thread.resume::<()>(()).unwrap_err(); + assert!(matches!(err, Error::RuntimeError(msg) if msg == "yield error")); + + Ok(()) +} + +#[test] +fn test_thread_event_reentrant_resume() -> Result<()> { + let lua = Lua::new(); + + // Self-resume from within the yield callback is not allowed + let reentered = Arc::new(AtomicBool::new(false)); + let reentered2 = reentered.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_YIELD, move |_lua, event| { + if let ThreadEvent::Yield(thread) = event + && !reentered2.swap(true, Ordering::Relaxed) + { + assert_eq!(thread.status(), ThreadStatus::Resumable); + assert!(thread.is_resumable()); + let err = thread.resume::(()).unwrap_err(); + assert!( + matches!(&err, Error::RuntimeError(msg) if msg.contains("from within its own event callback")), + "unexpected error: {err:?}" + ); + } + Ok(()) + }); + + let thread = lua.create_thread( + lua.load("coroutine.yield(1, 2, 3, 4, 5) return 7") + .into_function()?, + )?; + + let vals = thread.resume::<(i32, i32, i32, i32, i32)>(())?; + assert_eq!(vals, (1, 2, 3, 4, 5)); + assert!(reentered.load(Ordering::Relaxed)); + + assert_eq!(thread.resume::(())?, 7); + assert!(thread.is_finished()); + + Ok(()) +} + +#[test] +fn test_thread_event_swap() -> Result<()> { + let lua = Lua::new(); + + let count = Arc::new(AtomicU32::new(0)); + let count2 = count.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_RESUME, move |_lua, _event| { + count2.fetch_add(1, Ordering::Relaxed); + Ok(()) + }); + + let thread = lua.create_thread(lua.load("coroutine.yield(1) return 2").into_function()?)?; + thread.resume::(())?; + assert_eq!(count.load(Ordering::Relaxed), 1); + + // Replace callback with a new one + let count3 = Arc::new(AtomicU32::new(0)); + let count4 = count3.clone(); + lua.set_thread_event_callback(ThreadTriggers::new().on_resume(), move |_lua, _event| { + count4.fetch_add(10, Ordering::Relaxed); + Ok(()) + }); + + thread.resume::(())?; + assert_eq!(count.load(Ordering::Relaxed), 1); + assert_eq!(count3.load(Ordering::Relaxed), 10); + + // Remove callback + lua.remove_thread_event_callback(); + thread.reset(lua.load("return 0").into_function()?)?; + thread.resume::<()>(())?; + assert_eq!(count3.load(Ordering::Relaxed), 10); // unchanged + + Ok(()) +} + +#[cfg(feature = "luau")] +#[test] +fn test_thread_event_luau_resume_error() -> Result<()> { + let lua = Lua::new(); + + let fired = Arc::new(AtomicBool::new(false)); + let fired2 = fired.clone(); + lua.set_thread_event_callback(ThreadTriggers::ON_RESUME, move |_lua, event| { + assert!(matches!(event, ThreadEvent::Resume(_))); + fired2.store(true, Ordering::Relaxed); + Ok(()) + }); + + let thread = lua.create_thread(lua.load("return 42").into_function()?)?; + let _ = thread.resume_error::<()>("test error"); + assert!(fired.load(Ordering::Relaxed)); + + Ok(()) +} + +#[cfg(feature = "luau")] +#[test] +fn test_thread_event_create_from_lua() -> Result<()> { + let lua = Lua::new(); + + let count = std::cell::Cell::new(0); + lua.set_thread_event_callback(ThreadTriggers::ON_CREATE, move |_, _| { + count.set(count.get() + 1); + if count.get() == 2 { + return Err(Error::runtime("thread limit exceeded")); + } + Ok(()) + }); + let result = lua + .load( + r#" + local co = coroutine.wrap(function() return coroutine.create(print) end) + co() + "#, + ) + .exec(); + assert!(result.is_err()); + assert!(matches!(result, Err(Error::RuntimeError(err)) if err.contains("thread limit exceeded"))); + + Ok(()) +} diff --git a/tests/types.rs b/tests/types.rs index 0cd775bc..6475acd6 100644 --- a/tests/types.rs +++ b/tests/types.rs @@ -1,6 +1,6 @@ use std::os::raw::c_void; -use mlua::{Function, LightUserData, Lua, LuaString, Number, Result, Thread}; +use mlua::{Error, Function, LightUserData, Lua, LuaString, Number, Result, Thread}; #[test] fn test_lightuserdata() -> Result<()> { @@ -30,7 +30,7 @@ fn test_boolean_type_metatable() -> Result<()> { let lua = Lua::new(); let mt = lua.create_table()?; - mt.set("__add", Function::wrap(|a, b| Ok(a || b)))?; + mt.set("__add", Function::wrap(|a, b| Ok::<_, mlua::Error>(a || b)))?; assert_eq!(lua.type_metatable::(), None); lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::().unwrap(), mt); @@ -51,7 +51,7 @@ fn test_lightuserdata_type_metatable() -> Result<()> { mt.set( "__add", Function::wrap(|a: LightUserData, b: LightUserData| { - Ok(LightUserData((a.0 as usize + b.0 as usize) as *mut c_void)) + Ok::<_, Error>(LightUserData((a.0 as usize + b.0 as usize) as *mut c_void)) }), )?; lua.set_type_metatable::(Some(mt.clone())); @@ -79,7 +79,10 @@ fn test_number_type_metatable() -> Result<()> { let lua = Lua::new(); let mt = lua.create_table()?; - mt.set("__call", Function::wrap(|n1: f64, n2: f64| Ok(n1 * n2)))?; + mt.set( + "__call", + Function::wrap(|n1: f64, n2: f64| Ok::<_, Error>(n1 * n2)), + )?; lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::().unwrap(), mt); @@ -96,7 +99,7 @@ fn test_string_type_metatable() -> Result<()> { let mt = lua.create_table()?; mt.set( "__add", - Function::wrap(|a: String, b: String| Ok(format!("{a}{b}"))), + Function::wrap(|a: String, b: String| Ok::<_, Error>(format!("{a}{b}"))), )?; lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::().unwrap(), mt); @@ -113,7 +116,7 @@ fn test_function_type_metatable() -> Result<()> { let mt = lua.create_table()?; mt.set( "__index", - Function::wrap(|_: Function, key: String| Ok(format!("function.{key}"))), + Function::wrap(|_: Function, key: String| Ok::<_, Error>(format!("function.{key}"))), )?; lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::(), Some(mt)); @@ -132,7 +135,7 @@ fn test_thread_type_metatable() -> Result<()> { let mt = lua.create_table()?; mt.set( "__index", - Function::wrap(|_: Thread, key: String| Ok(format!("thread.{key}"))), + Function::wrap(|_: Thread, key: String| Ok::<_, Error>(format!("thread.{key}"))), )?; lua.set_type_metatable::(Some(mt.clone())); assert_eq!(lua.type_metatable::(), Some(mt)); diff --git a/tests/userdata.rs b/tests/userdata.rs index df5ed1f4..69bf1912 100644 --- a/tests/userdata.rs +++ b/tests/userdata.rs @@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicI64, Ordering}; use mlua::{ AnyUserData, Error, ExternalError, Function, Lua, LuaString, MetaMethod, Nil, ObjectLike, Result, - UserData, UserDataFields, UserDataMethods, UserDataRef, UserDataRegistry, Value, Variadic, + UserData, UserDataFields, UserDataMethods, UserDataOwned, UserDataRef, UserDataRegistry, Value, Variadic, }; #[test] @@ -733,6 +733,43 @@ fn test_metatable() -> Result<()> { Ok(()) } +#[test] +fn test_userdata_type_name() -> Result<()> { + struct MyUserData; + impl UserData for MyUserData {} + + struct MyUserdataCustom; + impl UserData for MyUserdataCustom { + fn add_fields>(fields: &mut F) { + fields.add_meta_field_with(MetaMethod::Type, |_| Ok("MyCustomName")); + } + } + + // mlua always sets __name/__type; override with a non-string to test the "userdata" fallback + struct MyUserdataInvalid; + impl UserData for MyUserdataInvalid { + fn add_fields>(fields: &mut F) { + fields.add_meta_field_with(MetaMethod::Type, |_| Ok(42_i64)); + } + } + + let lua = Lua::new(); + + // Default is the Rust type name + let ud = lua.create_userdata(MyUserData)?; + assert_eq!(ud.type_name()?, "MyUserData"); + + // Custom name from metatable + let ud = lua.create_userdata(MyUserdataCustom)?; + assert_eq!(ud.type_name()?, "MyCustomName"); + + // Invalid type name should fallback to "userdata" + let ud = lua.create_userdata(MyUserdataInvalid)?; + assert_eq!(ud.type_name()?.to_str()?, "userdata"); + + Ok(()) +} + #[test] fn test_userdata_proxy() -> Result<()> { struct MyUserData(i64); @@ -949,10 +986,11 @@ fn test_userdata_derive() -> Result<()> { // More complex struct where generics and where clause + #[rustfmt::skip] #[derive(Clone, Copy, mlua::FromLua)] struct MyUserData2<'a, T: ?Sized>(&'a T) where - T: Copy; + T: Copy,; // trailing comma is needed for testing lua.register_userdata_type::>(|reg| { reg.add_function("val", |_, this: MyUserData2<'static, i32>| Ok(*this.0)); @@ -1376,7 +1414,7 @@ fn test_userdata_namecall() -> Result<()> { struct MyUserData; impl UserData for MyUserData { - fn register(registry: &mut mlua::UserDataRegistry) { + fn register(registry: &mut UserDataRegistry) { registry.add_method("method", |_, _, ()| Ok("method called")); registry.add_field_method_get("field", |_, _| Ok("field value")); @@ -1422,3 +1460,49 @@ fn test_userdata_get_path() -> Result<()> { Ok(()) } + +#[test] +fn test_userdata_owned() -> Result<()> { + #[derive(Debug)] + struct MyUserdata(Arc); + + impl UserData for MyUserdata { + fn register(registry: &mut UserDataRegistry) { + registry.add_method("num", |_, this, ()| Ok(*this.0)); + } + } + + let lua = Lua::new(); + let rc = Arc::new(42); + + // It takes ownership and destructs the Lua userdata + let ud = lua.create_userdata(MyUserdata(rc.clone()))?; + assert_eq!(Arc::strong_count(&rc), 2); + let owned: UserDataOwned = lua.convert(&ud)?; + assert_eq!(*owned.0.0, 42); + drop(owned); + assert_eq!(Arc::strong_count(&rc), 1); + match ud.borrow::() { + Err(Error::UserDataDestructed) => {} + r => panic!("expected UserDataDestructed, got {:?}", r), + } + + // Cannot take while borrowed + let rc = Arc::new(7); + let ud = lua.create_userdata(MyUserdata(rc.clone()))?; + let borrowed = ud.borrow::()?; + match lua.convert::>(&ud) { + Err(Error::UserDataBorrowMutError) => {} + r => panic!("expected UserDataBorrowMutError, got {:?}", r), + } + drop(borrowed); + + // Works as a function parameter + let f = lua.create_function(|_, owned: UserDataOwned| Ok(*owned.0.0))?; + let rc = Arc::new(55); + let ud = lua.create_userdata(MyUserdata(rc.clone()))?; + assert_eq!(f.call::(ud)?, 55); + assert_eq!(Arc::strong_count(&rc), 1); // dropped after call + + Ok(()) +} diff --git a/tests/userdata_macro.rs b/tests/userdata_macro.rs new file mode 100644 index 00000000..a681cef7 --- /dev/null +++ b/tests/userdata_macro.rs @@ -0,0 +1,718 @@ +#![cfg(feature = "macros")] + +use mlua::{AnyUserData, Lua, Result, UserData}; + +#[derive(Default, Clone, Debug, UserData)] +struct Rectangle { + length: u32, + #[lua] + width: u32, + + #[lua(get, name = "version")] + version_ro: u32, + + #[lua(skip)] + _internal: u64, +} + +#[mlua::userdata_impl] +impl Rectangle { + const TYPE_NAME: &str = "Rectangle"; + + #[lua(infallible)] + fn new(length: u32, width: u32) -> Self { + Rectangle { + length, + width, + version_ro: 1, + _internal: 0, + } + } + + fn area(&self) -> Result { + Ok(self.length * self.width) + } + + #[lua(getter, infallible, name = "perimeter")] + fn calculate_perimeter(&self) -> u32 { + 2 * (self.length + self.width) + } + + #[lua(infallible)] + fn diagonal(&self) -> f64 { + (self.length.pow(2) as f64 + self.width.pow(2) as f64).sqrt() + } + + fn scale(&mut self, factor: u32) -> Result<()> { + self.length *= factor; + self.width *= factor; + Ok(()) + } + + #[lua(setter, name = "size", infallible)] + fn set_size(&mut self, _lua: &Lua, size: u32) { + self.length = size; + self.width = size; + } + + #[lua(field)] + fn description() -> &'static str { + "A rectangle shape" + } + + #[lua(meta)] + fn __tostring(&self) -> Result { + Ok(format!("Rectangle({}x{})", self.length, self.width)) + } + + #[lua(meta, name = "__call")] + fn call() -> Result { + Ok(Rectangle::default()) + } + + #[lua(meta, infallible, name = "__add")] + fn add(&self, other: &Rectangle) -> Rectangle { + Rectangle { + length: self.length + other.length, + width: self.width + other.width, + ..Default::default() + } + } + + #[lua(infallible)] + fn maybe_add(&self, other: Option<&Rectangle>) -> Rectangle { + match other { + Some(other) => Rectangle { + length: self.length + other.length, + width: self.width + other.width, + ..Default::default() + }, + None => self.clone(), + } + } + + #[lua(meta, field, name = "__answer")] + fn answer() -> u32 { + 42 + } + + #[lua(skip)] + #[allow(unused)] + fn helper() -> u32 { + 42 + } + + fn default_size() -> Result<(u32, u32)> { + Ok((100, 100)) + } + + fn get_width(&self, lua: &Lua) -> Result { + let _ = lua.globals().len(); + Ok(self.width) + } + + #[lua(getter, name = "lua_version")] + fn get_lua_version(&self, lua: &::mlua::Lua) -> Result { + // `::mlua::Lua` is used to check that the type is correctly resolved in macros + lua.globals().get("_VERSION") + } + + fn into_tuple(self) -> Result<(u32, u32)> { + Ok((self.length, self.width)) + } + + fn greet(&self, name: &str) -> Result { + Ok(format!("Hello, {name}!")) + } + + fn maybe_greet(&self, name: Option<&str>) -> Result { + match name { + Some(name) => Ok(format!("Hello, {name}!")), + None => Ok("Hello!".to_string()), + } + } + + fn transfer_length(&mut self, other: &mut Rectangle) -> Result<()> { + other.length += self.length; + self.length = 0; + Ok(()) + } +} + +#[mlua::userdata_impl] +impl Rectangle { + #[lua(infallible)] + fn double_length(&self) -> u32 { + self.length * 2 + } +} + +fn make_lua() -> Lua { + let lua = unsafe { Lua::unsafe_new() }; + lua.globals() + .set("Rectangle", lua.create_proxy::().unwrap()) + .unwrap(); + lua +} + +#[test] +fn test_rectangle() { + let lua = make_lua(); + + // Basic fields, getters, setters, methods + lua.load( + r#" + rect = Rectangle.new(5, 10, 3) + assert(rect.length == 5, "length should be 5") + assert(rect.width == 10, "width should be 10") + assert(rect.perimeter == 30, "perimeter should be 30") + + -- read-only field + assert(rect.version == 1, "version should be 1") + local ok, err = pcall(function() rect.version = 2 end) + assert(not ok, "version should be read-only") + + -- skipped + assert(rect._internal == nil, "_internal should be nil") + assert(rect.helper == nil, "skipped method should be nil") + + rect.length = 15 + rect.width = 20 + assert(rect.length == 15, "length should be updated to 15") + assert(rect.width == 20, "width should be updated to 20") + assert(rect.perimeter == 70, "perimeter should be updated to 70") + assert(rect:area() == 300, "area should return 300") + + rect:scale(2) + assert(rect.length == 30, "length should be scaled to 30") + assert(rect.width == 40, "width should be scaled to 40") + assert(rect:diagonal() == 50.0, "diagonal should be 50.0") + + rect.size = 7 + assert(rect.length == 7, "length should be updated to 7") + assert(rect.width == 7, "width should be updated to 7") + + -- static / associated items + assert(rect.TYPE_NAME == 'Rectangle', "TYPE_NAME should be 'Rectangle'") + assert(rect.description == 'A rectangle shape', "description should be 'A rectangle shape'") + local w, h = rect.default_size() + assert(w == 100, "default_size width should be 100") + assert(h == 100, "default_size height should be 100") + + -- meta methods + local r1 = Rectangle.new(5, 10, 0) + assert(tostring(r1) == 'Rectangle(5x10)', "__tostring should return 'Rectangle(5x10)'") + local r2 = r1() + assert(r2:area() == 0, "__call should create a default rect") + local r3 = Rectangle.new(3, 4, 0) + local r4 = r1 + r3 + assert(r4.length == 8, "__add length should be 5 + 3 = 8") + assert(r4.width == 14, "__add width should be 10 + 4 = 14") + + -- Option<&T> wrapped parameter + local r5 = r1:maybe_add(r3) + assert(r5.length == 8, "maybe_add with arg length should be 5 + 3 = 8") + assert(r5.width == 14, "maybe_add with arg width should be 10 + 4 = 14") + local r6 = r1:maybe_add() + assert(r6.length == 5, "maybe_add with nil is no-op") + assert(r6.width == 10, "maybe_add with nil is no-op") + + -- method with &mut self and &mut Rectangle param + rect = Rectangle.new(5, 10, 3) + other = Rectangle.new(2, 3, 0) + rect:transfer_length(other) + assert(rect.length == 0, "length should be 0 after transfer") + assert(other.length == 7, "other length should be 7 after transfer") + assert(other:double_length() == 14, "double_length should be 14") + + -- meta field + if _VERSION:match("Lua ") then + local mt = debug.getmetatable(rect) + assert(mt.__answer == 42, "__answer meta field should be 42") + end + + assert(rect.lua_version == _VERSION, "lua_version should match Lua's _VERSION") + + -- Consuming method + local w, h = rect:into_tuple() + assert(w == 0, "into_tuple width should be 0") + assert(h == 10, "into_tuple height should be 7") + local ok, err = pcall(function() rect:area() end) + assert(not ok and tostring(err):match("userdata has been destructed"), "rect should be consumed and unusable after into_tuple") + + -- Custom methods + assert(other:greet("User") == "Hello, User!", "greet should return 'Hello, User!'") + assert(other:maybe_greet("User") == "Hello, User!", "maybe_greet with arg should return 'Hello, User!'") + assert(other:maybe_greet() == "Hello!", "maybe_greet with nil should return 'Hello!'") + "#, + ) + .exec() + .unwrap(); +} + +#[derive(Clone, Debug, UserData)] +enum Color { + Red, + Green, + Blue, +} + +fn make_lua_color() -> Lua { + let lua = Lua::new(); + lua.globals() + .set("Color", lua.create_proxy::().unwrap()) + .unwrap(); + lua +} + +#[mlua::userdata_impl] +impl Color { + #[lua(infallible)] + fn new(r: u8, g: u8, b: u8) -> Self { + if r > 0 && g == 0 && b == 0 { + Color::Red + } else if g > 0 && r == 0 && b == 0 { + Color::Green + } else { + Color::Blue + } + } + + #[lua(infallible)] + fn name(&self) -> String { + match self { + Color::Red => "red".into(), + Color::Green => "green".into(), + Color::Blue => "blue".into(), + } + } + + #[lua(meta, infallible)] + fn __tostring(&self) -> String { + self.name() + } +} + +#[test] +fn test_color() { + let lua = make_lua_color(); + lua.load( + r#" + red = Color.new(255, 0, 0) + green = Color.new(0, 255, 0) + blue = Color.new(0, 0, 255) + + assert(red:name() == 'red', "red name should be 'red'") + assert(green:name() == 'green', "green name should be 'green'") + assert(blue:name() == 'blue', "blue name should be 'blue'") + + assert(tostring(red) == 'red', "red tostring should be 'red'") + assert(tostring(green) == 'green', "green tostring should be 'green'") + assert(tostring(blue) == 'blue', "blue tostring should be 'blue'") + "#, + ) + .exec() + .unwrap(); +} + +#[derive(Clone, Debug, UserData)] +struct Point(i32, i32); + +fn make_lua_point() -> Lua { + let lua = Lua::new(); + lua.globals() + .set("Point", lua.create_proxy::().unwrap()) + .unwrap(); + lua +} + +#[mlua::userdata_impl] +impl Point { + #[lua(infallible)] + fn new(x: i32, y: i32) -> Self { + Point(x, y) + } + + fn x(&self) -> Result { + Ok(self.0) + } + + fn y(&self) -> Result { + Ok(self.1) + } + + fn distance(&self, other: &Point) -> Result { + let dx = (self.0 - other.0) as f64; + let dy = (self.1 - other.1) as f64; + Ok((dx * dx + dy * dy).sqrt()) + } +} + +#[test] +fn test_point() { + let lua = make_lua_point(); + lua.load( + r#" + p1 = Point.new(0, 0) + p2 = Point.new(3, 4) + + assert(p1:x() == 0, "p1.x should be 0") + assert(p1:y() == 0, "p1.y should be 0") + assert(p2:x() == 3, "p2.x should be 3") + assert(p2:y() == 4, "p2.y should be 4") + + assert(p1:distance(p2) == 5.0, "distance should be 5.0") + "#, + ) + .exec() + .unwrap(); +} + +#[derive(Clone, Debug, UserData)] +struct Bytes(Vec); + +#[mlua::userdata_impl] +impl Bytes { + #[lua(meta, name = "__type")] + const TYPE: &str = "MyBytes"; + + #[lua(infallible)] + fn new(data: &[u8]) -> Self { + Bytes(data.to_vec()) + } + + fn first(&self) -> Result> { + Ok(self.0.first().copied()) + } + + fn len(&self) -> Result { + Ok(self.0.len()) + } +} + +#[test] +fn test_known_borrow_wrappers() -> Result<()> { + let lua = Lua::new(); + lua.globals() + .set("Bytes", lua.create_proxy::().unwrap()) + .unwrap(); + lua.load( + r#" + local b = Bytes.new('abc') + assert(b:first() == 97, "first should return 97 ('a')") + assert(b:len() == 3, "len should return 3") + + if _VERSION:match("Luau") then + assert(typeof(b) == 'MyBytes', "type should be MyBytes in Luau") + end + "#, + ) + .exec() + .unwrap(); + Ok(()) +} + +#[derive(Clone, Copy, Debug, PartialEq, UserData)] +struct Vec2 { + x: i32, + y: i32, +} + +#[mlua::userdata_impl] +impl Vec2 { + #[lua(infallible)] + fn new(x: i32, y: i32) -> Self { + Vec2 { x, y } + } + + #[lua(meta, infallible, name = "__add")] + fn add(this: &Vec2, other: &Vec2) -> Vec2 { + Vec2 { + x: this.x + other.x, + y: this.y + other.y, + } + } + + #[lua(meta, infallible, name = "__eq")] + fn eq(this: &Vec2, other: &Vec2) -> bool { + this == other + } + + #[lua(meta, infallible, name = "__call")] + fn call(_lua: &Lua, _proxy: AnyUserData, x: i32, y: i32) -> Vec2 { + Self::new(x, y) + } +} + +#[test] +fn test_static_metamethods() { + let lua = Lua::new(); + lua.globals() + .set("Vec2", lua.create_proxy::().unwrap()) + .unwrap(); + lua.load( + r#" + local a = Vec2.new(1, 2) + local b = Vec2.new(3, 4) + + local c = a + b + assert(c.x == 4, "__add x should be 1 + 3 = 4, got " .. tostring(c.x)) + assert(c.y == 6, "__add y should be 2 + 4 = 6, got " .. tostring(c.y)) + + assert(a == Vec2.new(1, 2), "__eq should report vectors equal") + assert(a ~= b, "__eq should report vectors unequal") + + -- `__call` on the proxy + local d = Vec2(7, 14) + assert(d.x == 7 and d.y == 14, "__call should build Vec2(7, 14)") + "#, + ) + .exec() + .unwrap(); +} + +#[derive(Clone, Debug, UserData)] +struct Hygiene { + value: i32, + r#type: i32, +} + +#[mlua::userdata_impl] +impl Hygiene { + #[lua(infallible)] + fn new(value: i32) -> Self { + Hygiene { value, r#type: 7 } + } + + // `this` must not clash with the generated receiver binding. + #[lua(infallible)] + fn add_this(&self, this: i32) -> i32 { + self.value + this + } + + // `lua` must not clash either. + #[lua(infallible)] + fn add_lua(&self, lua: i32) -> i32 { + self.value + lua + } + + #[lua(infallible)] + fn r#double_type(&self) -> i32 { + self.r#type * 2 + } +} + +#[test] +fn test_param_name_hygiene() { + let lua = Lua::new(); + lua.globals() + .set("Hygiene", lua.create_proxy::().unwrap()) + .unwrap(); + lua.load( + r#" + local h = Hygiene.new(10) + + -- reserved parameter names must not clash with generated bindings + assert(h:add_this(5) == 15, "add_this should be 10 + 5 = 15") + assert(h:add_lua(3) == 13, "add_lua should be 10 + 3 = 13") + + -- the `r#` prefix must not leak into Lua names + assert(h.type == 7, "field r#type should be exposed as 'type'") + h.type = 10 + assert(h.type == 10, "field r#type setter should update via 'type'") + assert(h:double_type() == 20, "method r#double_type should be callable as 'double_type'") + assert(h["r#type"] == nil, "the raw prefix must not leak into the Lua name") + "#, + ) + .exec() + .unwrap(); +} + +#[derive(Clone, Debug, UserData)] +struct Wild { + n: i32, +} + +#[mlua::userdata_impl] +impl Wild { + #[lua(infallible)] + fn new(n: i32) -> Self { + Wild { n } + } + + #[lua(infallible)] + fn ignore_one(&self, _: i32) -> i32 { + self.n + } + + #[lua(infallible)] + fn add_second(&self, _: i32, other: &Wild) -> i32 { + self.n + other.n + } +} + +#[test] +fn test_wildcard_params() { + let lua = Lua::new(); + lua.globals() + .set("Wild", lua.create_proxy::().unwrap()) + .unwrap(); + lua.load( + r#" + local w = Wild.new(7) + assert(w:ignore_one(99) == 7, "single wildcard arg should be ignored") + assert(w:add_second(1, Wild.new(5)) == 12, "named arg after a wildcard should work") + assert(not pcall(function() return w:ignore_one({}) end), "wildcard arg is still type-checked") + "#, + ) + .exec() + .unwrap(); +} + +#[cfg(feature = "async")] +mod async_tests { + use mlua::{Lua, Result, UserData}; + + #[derive(Clone, Debug, UserData)] + struct AsyncCounter(u64); + + #[mlua::userdata_impl] + impl AsyncCounter { + #[lua(infallible)] + fn new() -> Self { + AsyncCounter(0) + } + + async fn get_value(&self) -> Result { + Ok(self.0) + } + + async fn set_value(&mut self, value: u64) -> Result<()> { + self.0 = value; + Ok(()) + } + + async fn take_value(self) -> Result { + Ok(self.0) + } + + #[lua(infallible)] + async fn get_value_infallible(&self) -> u64 { + self.0 + } + + async fn multiply(&self, factor: u64) -> Result { + Ok(self.0 * factor) + } + + async fn add(&self, this: u64, lua: u64) -> Result { + Ok(self.0 + this + lua) + } + + async fn default_value() -> Result { + Ok(42) + } + + async fn lua_version(lua: &Lua, extra: Option<&str>) -> Result { + (lua.globals().get("_VERSION")).map(|s: String| s + extra.unwrap_or("")) + } + + async fn lua_version_owned(lua: Lua) -> Result { + lua.globals().get("_VERSION") + } + + #[cfg(not(any(feature = "lua51", feature = "luau")))] + #[lua(meta)] + async fn __tostring(&self) -> Result { + Ok(format!("Counter({})", self.0)) + } + } + + #[tokio::test] + async fn test_async_methods() { + let lua = Lua::new(); + lua.globals() + .set("AsyncCounter", lua.create_proxy::().unwrap()) + .unwrap(); + + lua.load( + r#" + local c = AsyncCounter.new() + c:set_value(10) + local val = c:get_value() + assert(val == 10, "expected 10, got " .. tostring(val)) + local doubled = c:multiply(3) + assert(doubled == 30, "expected 30, got " .. tostring(doubled)) + local summed = c:add(1, 2) + assert(summed == 13, "expected 10 + 1 + 2 = 13, got " .. tostring(summed)) + local inf = c:get_value_infallible() + assert(inf == 10, "expected infallible 10, got " .. tostring(inf)) + "#, + ) + .exec_async() + .await + .unwrap(); + } + + #[tokio::test] + async fn test_async_lua_param() { + let lua = Lua::new(); + lua.globals() + .set("AsyncCounter", lua.create_proxy::().unwrap()) + .unwrap(); + + lua.load( + r#" + local c = AsyncCounter.new() + assert(type(AsyncCounter.lua_version()) == "string") + assert(string.sub(AsyncCounter.lua_version(" extra"), -5) == "extra", "expected 'extra' at the end") + assert(type(AsyncCounter.lua_version_owned()) == "string") + "#, + ) + .exec_async() + .await + .unwrap(); + } + + #[tokio::test] + async fn test_async_consume() { + let lua = Lua::new(); + lua.globals() + .set("AsyncCounter", lua.create_proxy::().unwrap()) + .unwrap(); + + lua.load( + r#" + local c = AsyncCounter.new() + c:set_value(42) + local val = c:take_value() + assert(val == 42) + local ok, err = pcall(function() c:get_value() end) + assert(not ok and tostring(err):match("userdata has been destructed")) + "#, + ) + .exec_async() + .await + .unwrap(); + } + + #[cfg(not(any(feature = "lua51", feature = "luau")))] + #[tokio::test] + async fn test_async_meta() { + let lua = Lua::new(); + lua.globals() + .set("AsyncCounter", lua.create_proxy::().unwrap()) + .unwrap(); + + lua.load( + r#" + local c = AsyncCounter.new() + c:set_value(7) + assert(tostring(c) == "Counter(7)") + "#, + ) + .exec_async() + .await + .unwrap(); + } +} diff --git a/tests/value.rs b/tests/value.rs index 43ec708f..9ed3b2bf 100644 --- a/tests/value.rs +++ b/tests/value.rs @@ -2,7 +2,10 @@ use std::collections::HashMap; use std::os::raw::c_void; use std::ptr; -use mlua::{Error, LightUserData, Lua, MultiValue, Result, UserData, UserDataMethods, Value}; +use mlua::{ + AnyUserData, Error, LightUserData, Lua, MultiValue, Result, UserData, UserDataMethods, UserDataRegistry, + Value, +}; #[test] fn test_value_eq() -> Result<()> { @@ -62,7 +65,7 @@ fn test_value_eq() -> Result<()> { assert!(!table1.to_pointer().is_null()); assert!(!ptr::eq(table1.to_pointer(), table2.to_pointer())); - assert!(ptr::eq(string1.to_pointer(), string2.to_pointer())); + assert!(ptr::eq(string1.to_pointer(), string2.to_pointer()) && !string1.to_pointer().is_null()); assert!(ptr::eq(func1.to_pointer(), func2.to_pointer())); assert!(num1.to_pointer().is_null()); @@ -218,6 +221,31 @@ fn test_debug_format() -> Result<()> { .map(Value::UserData)?; assert!(format!("{ud:#?}").starts_with("HashMap:")); + struct ToDebugUserData; + impl UserData for ToDebugUserData { + fn register(registry: &mut UserDataRegistry) { + registry.add_meta_method("__tostring", |_, _, ()| Ok("regular-string")); + registry.add_meta_method("__todebugstring", |_, _, ()| Ok("debug-string")); + } + } + let debug_ud = Value::UserData(lua.create_userdata(ToDebugUserData)?); + assert_eq!(debug_ud.to_string()?, "regular-string"); + assert_eq!(format!("{debug_ud:#?}"), "debug-string"); + + struct ToStringUserData; + impl UserData for ToStringUserData { + fn register(registry: &mut UserDataRegistry) { + registry.add_meta_method("__tostring", |_, _, ()| Ok("regular-string")); + } + } + let tostring_only_ud = Value::UserData(lua.create_userdata(ToStringUserData)?); + assert_eq!(format!("{tostring_only_ud:#?}"), "regular-string"); + + // Check that `AnyUsedata` pretty debug format is same as for `Value::UserData` + let any_ud: AnyUserData = lua.create_userdata(ToDebugUserData)?; + let value_ud = Value::UserData(any_ud.clone()); + assert_eq!(format!("{any_ud:#?}"), format!("{value_ud:#?}")); + Ok(()) }