Compare commits

..

No commits in common. "main" and "v0.8.5" have entirely different histories.
main ... v0.8.5

146 changed files with 1158 additions and 5614 deletions

4
.gitattributes vendored
View file

@ -1,5 +1,9 @@
* text=auto
# Temporarily highlight luau as normal lua files
# until we get native linguist support for Luau
*.luau linguist-language=Lua
# Ensure all lua files use LF
*.lua eol=lf
*.luau eol=lf

View file

@ -23,8 +23,11 @@ jobs:
with:
components: rustfmt
- name: Install Just
uses: extractions/setup-just@v2
- name: Install Tooling
uses: CompeyDev/setup-rokit@v0.1.2
uses: ok-nick/setup-aftman@v0.4.2
- name: Check Formatting
run: just fmt-check
@ -37,8 +40,11 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Just
uses: extractions/setup-just@v2
- name: Install Tooling
uses: CompeyDev/setup-rokit@v0.1.2
uses: ok-nick/setup-aftman@v0.4.2
- name: Analyze
run: just analyze
@ -82,20 +88,17 @@ jobs:
- name: Build
run: |
cargo build \
--workspace \
--locked --all-features \
--target ${{ matrix.cargo-target }}
- name: Lint
run: |
cargo clippy \
--workspace \
--locked --all-features \
--target ${{ matrix.cargo-target }}
- name: Test
run: |
cargo test \
--lib --workspace \
--locked --all-features \
--target ${{ matrix.cargo-target }}

View file

@ -27,26 +27,26 @@ jobs:
file: crates/lune/Cargo.toml
field: package.version
# dry-run:
# name: Dry-run
# needs: ["init"]
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
dry-run:
name: Dry-run
needs: ["init"]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
# - name: Install Rust
# uses: dtolnay/rust-toolchain@stable
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
# - name: Publish (dry-run)
# uses: katyo/publish-crates@v2
# with:
# dry-run: true
# check-repo: true
# registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Publish (dry-run)
uses: katyo/publish-crates@v2
with:
dry-run: true
check-repo: true
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
build:
needs: ["init"] # , "dry-run"]
needs: ["init", "dry-run"]
strategy:
fail-fast: false
matrix:
@ -112,7 +112,7 @@ jobs:
release-github:
name: Release (GitHub)
runs-on: ubuntu-latest
needs: ["init", "build"] # , "dry-run", "build"]
needs: ["init", "dry-run", "build"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
@ -139,20 +139,20 @@ jobs:
files: ./releases/*.zip
draft: true
# release-crates:
# name: Release (crates.io)
# runs-on: ubuntu-latest
# needs: ["init", "dry-run", "build"]
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
release-crates:
name: Release (crates.io)
runs-on: ubuntu-latest
needs: ["init", "dry-run", "build"]
steps:
- name: Checkout repository
uses: actions/checkout@v4
# - name: Install Rust
# uses: dtolnay/rust-toolchain@stable
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
# - name: Publish crates
# uses: katyo/publish-crates@v2
# with:
# dry-run: false
# check-repo: true
# registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
- name: Publish crates
uses: katyo/publish-crates@v2
with:
dry-run: false
check-repo: true
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View file

@ -129,7 +129,7 @@ end
]]
print("Sending 4 pings to google 🌏")
local result = process.exec("ping", {
local result = process.spawn("ping", {
"google.com",
"-c 4",
})

View file

@ -28,8 +28,8 @@ end)
for _ = 1, 5 do
local start = os.clock()
socket:send(tostring(1))
local response = socket:next()
socket.send(tostring(1))
local response = socket.next()
local elapsed = os.clock() - start
print(`Got response '{response}' in {elapsed * 1_000} milliseconds`)
task.wait(1 - elapsed)
@ -38,7 +38,7 @@ end
-- Everything went well, and we are done with the socket, so we can close it
print("Closing web socket...")
socket:close()
socket.close()
task.cancel(forceExit)
print("Done! 🌙")

View file

@ -15,9 +15,9 @@ local handle = net.serve(PORT, {
handleWebSocket = function(socket)
print("Got new web socket connection!")
repeat
local message = socket:next()
local message = socket.next()
if message ~= nil then
socket:send("Echo - " .. message)
socket.send("Echo - " .. message)
end
until message == nil
print("Web socket disconnected.")

View file

@ -8,125 +8,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## `0.9.0`
### Breaking changes
- Added two new process spawning functions - `process.create` and `process.exec`, removing the previous `process.spawn` API completely. ([#211])
To migrate from `process.spawn`, use the new `process.exec` API which retains the same behavior as the old function.
The new `process.create` function is a non-blocking process creation API and can be used to interactively
read and write stdio of the process.
```lua
local child = process.create("program", {
"cli-argument",
"other-cli-argument"
})
-- Writing to stdin
child.stdin:write("Hello from Lune!")
-- Reading from stdout
local data = child.stdout:read()
print(buffer.tostring(data))
```
- WebSocket methods in `net.socket` and `net.serve` now use standard Lua method calling convention and colon syntax.
This means `socket.send(...)` is now `socket:send(...)`, `socket.close(...)` is now `socket:close(...)`, and so on.
- `Runtime::run` now returns a more useful value instead of an `ExitCode` ([#178])
### Changed
- Documentation comments for several standard library properties have been improved ([#248], [#250])
- Error messages no longer contain redundant or duplicate stack trace information
[#178]: https://github.com/lune-org/lune/pull/178
[#211]: https://github.com/lune-org/lune/pull/211
[#248]: https://github.com/lune-org/lune/pull/248
[#250]: https://github.com/lune-org/lune/pull/250
## `0.8.9` - October 7th, 2024
### Changed
- Updated to Luau version `0.640`
## `0.8.8` - August 22nd, 2024
### Fixed
- Fixed errors when deserializing `Lighting.AttributesSerialize` by updating `rbx-dom` dependencies ([#245])
[#245]: https://github.com/lune-org/lune/pull/245
## `0.8.7` - August 10th, 2024
### Added
- Added a compression level option to `serde.compress` ([#224])
- Added missing vector methods to the `roblox` library ([#228])
### Changed
- Updated to Luau version `0.635`
- Updated to rbx-dom database version `0.634`
### Fixed
- Fixed `fs.readDir` with trailing forward-slash on Windows ([#220])
- Fixed `__type` and `__tostring` metamethods not always being respected when formatting tables
[#220]: https://github.com/lune-org/lune/pull/220
[#224]: https://github.com/lune-org/lune/pull/224
[#228]: https://github.com/lune-org/lune/pull/228
## `0.8.6` - June 23rd, 2024
### Added
- Added a builtin API for hashing and calculating HMACs as part of the `serde` library ([#193])
Basic usage:
```lua
local serde = require("@lune/serde")
local hash = serde.hash("sha256", "a message to hash")
local hmac = serde.hmac("sha256", "a message to hash", "a secret string")
print(hash)
print(hmac)
```
The returned hashes are sequences of lowercase hexadecimal digits. The following algorithms are supported:
`md5`, `sha1`, `sha224`, `sha256`, `sha384`, `sha512`, `sha3-224`, `sha3-256`, `sha3-384`, `sha3-512`, `blake3`
- Added two new options to `luau.load`:
- `codegenEnabled` - whether or not codegen should be enabled for the loaded chunk.
- `injectGlobals` - whether or not to inject globals into a passed `environment`.
By default, globals are injected and codegen is disabled.
Check the documentation for the `luau` standard library for more information.
- Implemented support for floor division operator / `__idiv` for the `Vector2` and `Vector3` types in the `roblox` standard library ([#196])
- Fixed the `_VERSION` global containing an incorrect Lune version string.
### Changed
- Sandboxing and codegen in the Luau VM is now fully enabled, resulting in up to 2x or faster code execution.
This should not result in any behavior differences in Lune, but if it does, please open an issue.
- Improved formatting of custom error objects (such as when `fs.readFile` returns an error) when printed or formatted using `stdio.format`.
### Fixed
- Fixed `__type` and `__tostring` metamethods on userdatas and tables not being respected when printed or formatted using `stdio.format`.
[#193]: https://github.com/lune-org/lune/pull/193
[#196]: https://github.com/lune-org/lune/pull/196
## `0.8.5` - June 1st, 2024
### Changed

1281
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,6 @@ members = [
"crates/lune-std-stdio",
"crates/lune-std-task",
"crates/lune-utils",
"crates/mlua-luau-scheduler",
]
# Profile for building the release binary, with the following options set:

View file

@ -33,7 +33,7 @@ Lune provides fully asynchronous APIs wherever possible, and is built in Rust
## Features
- 🌙 Strictly minimal but powerful interface that is easy to read and remember, just like Luau itself
- 🧰 Fully featured APIs for the filesystem, networking, stdio, all included in the small (~5mb zipped) executable
- 🧰 Fully featured APIs for the filesystem, networking, stdio, all included in the small (~5mb) executable
- 📚 World-class documentation, on the web _or_ directly in your editor, no network connection necessary
- 🏡 Familiar runtime environment for Roblox developers, with an included 1-to-1 task scheduler port
- ✏️ Optional built-in library for manipulating Roblox place & model files, and their instances

4
aftman.toml Normal file
View file

@ -0,0 +1,4 @@
[tools]
luau-lsp = "JohnnyMorganz/luau-lsp@1.29.0"
selene = "Kampfkarren/selene@0.27.1"
stylua = "JohnnyMorganz/StyLua@0.20.0"

View file

@ -1,6 +1,6 @@
[package]
name = "lune-roblox"
version = "0.1.4"
version = "0.1.1"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,17 +13,17 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua = { version = "0.9.7", features = ["luau"] }
glam = "0.27"
rand = "0.8"
thiserror = "1.0"
once_cell = "1.17"
rbx_binary = "1.0.0"
rbx_dom_weak = "3.0.0"
rbx_reflection = "5.0.0"
rbx_reflection_database = "1.0.0"
rbx_xml = "1.0.0"
rbx_binary = "0.7.3"
rbx_dom_weak = "2.6.0"
rbx_reflection = "4.4.0"
rbx_reflection_database = "0.2.9"
rbx_xml = "0.13.2"
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }

View file

@ -47,7 +47,6 @@ pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
| DomType::CFrame
| DomType::Color3
| DomType::ColorSequence
| DomType::EnumItem
| DomType::Float32
| DomType::Float64
| DomType::Font

View file

@ -51,7 +51,7 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
DomValue::Float32(n) => Ok(LuaValue::Number(*n as f64)),
DomValue::String(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::BinaryString(s) => Ok(LuaValue::String(lua.create_string(s)?)),
DomValue::ContentId(s) => Ok(LuaValue::String(
DomValue::Content(s) => Ok(LuaValue::String(
lua.create_string(AsRef::<str>::as_ref(s))?,
)),
@ -104,8 +104,8 @@ impl<'lua> LuaToDomValue<'lua> for LuaValue<'lua> {
(LuaValue::String(s), DomType::BinaryString) => {
Ok(DomValue::BinaryString(s.as_ref().into()))
}
(LuaValue::String(s), DomType::ContentId) => {
Ok(DomValue::ContentId(s.to_str()?.to_string().into()))
(LuaValue::String(s), DomType::Content) => {
Ok(DomValue::Content(s.to_str()?.to_string().into()))
}
// NOTE: Some values are either optional or default and we
@ -200,8 +200,6 @@ impl<'lua> DomValueToLua<'lua> for LuaAnyUserData<'lua> {
DomValue::Color3(value) => dom_to_userdata!(lua, value => Color3),
DomValue::Color3uint8(value) => dom_to_userdata!(lua, value => Color3),
DomValue::ColorSequence(value) => dom_to_userdata!(lua, value => ColorSequence),
DomValue::Content(value) => dom_to_userdata!(lua, value => Content),
DomValue::EnumItem(value) => dom_to_userdata!(lua, value => EnumItem),
DomValue::Faces(value) => dom_to_userdata!(lua, value => Faces),
DomValue::Font(value) => dom_to_userdata!(lua, value => Font),
DomValue::NumberRange(value) => dom_to_userdata!(lua, value => NumberRange),
@ -258,8 +256,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
DomType::Color3 => userdata_to_dom!(self as Color3 => dom::Color3),
DomType::Color3uint8 => userdata_to_dom!(self as Color3 => dom::Color3uint8),
DomType::ColorSequence => userdata_to_dom!(self as ColorSequence => dom::ColorSequence),
DomType::Content => userdata_to_dom!(self as Content => dom::Content),
DomType::EnumItem => userdata_to_dom!(self as EnumItem => dom::EnumItem),
DomType::Enum => userdata_to_dom!(self as EnumItem => dom::Enum),
DomType::Faces => userdata_to_dom!(self as Faces => dom::Faces),
DomType::Font => userdata_to_dom!(self as Font => dom::Font),
DomType::NumberRange => userdata_to_dom!(self as NumberRange => dom::NumberRange),
@ -317,7 +314,7 @@ impl<'lua> LuaToDomValue<'lua> for LuaAnyUserData<'lua> {
value if value.is::<CFrame>() => userdata_to_dom!(value as CFrame => dom::CFrame),
value if value.is::<Color3>() => userdata_to_dom!(value as Color3 => dom::Color3),
value if value.is::<ColorSequence>() => userdata_to_dom!(value as ColorSequence => dom::ColorSequence),
value if value.is::<EnumItem>() => userdata_to_dom!(value as EnumItem => dom::EnumItem),
value if value.is::<Enum>() => userdata_to_dom!(value as EnumItem => dom::Enum),
value if value.is::<Faces>() => userdata_to_dom!(value as Faces => dom::Faces),
value if value.is::<Font>() => userdata_to_dom!(value as Font => dom::Font),
value if value.is::<Instance>() => userdata_to_dom!(value as Instance => dom::Ref),

View file

@ -19,9 +19,7 @@ impl DomValueExt for DomType {
Color3uint8 => "Color3uint8",
ColorSequence => "ColorSequence",
Content => "Content",
ContentId => "ContentId",
Enum => "Enum",
EnumItem => "EnumItem",
Faces => "Faces",
Float32 => "Float32",
Float64 => "Float64",

View file

@ -1,120 +0,0 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::{Content as DomContent, ContentType};
use lune_utils::TableBuilder;
use crate::{exports::LuaExportsTable, instance::Instance};
use super::{super::*, EnumItem};
/**
An implementation of the [Content](https://create.roblox.com/docs/reference/engine/datatypes/Content) Roblox datatype.
This implements all documented properties, methods & constructors of the Content type as of April 2025.
*/
#[derive(Debug, Clone, PartialEq)]
pub struct Content(ContentType);
impl LuaExportsTable<'_> for Content {
const EXPORT_NAME: &'static str = "Content";
fn create_exports_table(lua: &'_ Lua) -> LuaResult<LuaTable<'_>> {
let from_uri = |_, uri: String| Ok(Self(ContentType::Uri(uri)));
let from_object = |_, obj: LuaUserDataRef<Instance>| {
let database = rbx_reflection_database::get();
let instance_descriptor = database
.classes
.get("Instance")
.expect("the reflection database should always have Instance in it");
let param_descriptor = database.classes.get(obj.get_class_name()).expect(
"you should not be able to construct an Instance that is not known to Lune",
);
if database.has_superclass(param_descriptor, instance_descriptor) {
Err(LuaError::runtime("the provided object is a descendant class of 'Instance', expected one that was only an 'Object'"))
} else {
Ok(Content(ContentType::Object(obj.dom_ref)))
}
};
TableBuilder::new(lua)?
.with_value("none", Content(ContentType::None))?
.with_function("fromUri", from_uri)?
.with_function("fromObject", from_object)?
.build_readonly()
}
}
impl LuaUserData for Content {
fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
fields.add_field_method_get("SourceType", |_, this| {
let variant_name = match &this.0 {
ContentType::None => "None",
ContentType::Uri(_) => "Uri",
ContentType::Object(_) => "Object",
other => {
return Err(LuaError::runtime(format!(
"cannot get SourceType: unknown ContentType variant '{other:?}'"
)))
}
};
Ok(EnumItem::from_enum_name_and_name(
"ContentSourceType",
variant_name,
))
});
fields.add_field_method_get("Uri", |_, this| {
if let ContentType::Uri(uri) = &this.0 {
Ok(Some(uri.to_owned()))
} else {
Ok(None)
}
});
fields.add_field_method_get("Object", |_, this| {
if let ContentType::Object(referent) = &this.0 {
Ok(Instance::new_opt(*referent))
} else {
Ok(None)
}
});
}
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
}
}
impl fmt::Display for Content {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Regardless of the actual content of the Content, Roblox just emits
// `Content` when casting it to a string. We do not do that.
write!(f, "Content(")?;
match &self.0 {
ContentType::None => write!(f, "None")?,
ContentType::Uri(uri) => write!(f, "Uri={uri}")?,
ContentType::Object(_) => write!(f, "Object")?,
other => write!(f, "UnknownType({other:?})")?,
}
write!(f, ")")
}
}
impl From<DomContent> for Content {
fn from(value: DomContent) -> Self {
Self(value.value().clone())
}
}
impl From<Content> for DomContent {
fn from(value: Content) -> Self {
match value.0 {
ContentType::None => Self::none(),
ContentType::Uri(uri) => Self::from_uri(uri),
ContentType::Object(referent) => Self::from_referent(referent),
other => unimplemented!("unknown variant of ContentType: {other:?}"),
}
}
}

View file

@ -1,7 +1,7 @@
use core::fmt;
use mlua::prelude::*;
use rbx_dom_weak::types::EnumItem as DomEnumItem;
use rbx_dom_weak::types::Enum as DomEnum;
use super::{super::*, Enum};
@ -100,18 +100,8 @@ impl PartialEq for EnumItem {
}
}
impl From<EnumItem> for DomEnumItem {
impl From<EnumItem> for DomEnum {
fn from(v: EnumItem) -> Self {
DomEnumItem {
ty: v.parent.desc.name.to_string(),
value: v.value,
}
}
}
impl From<DomEnumItem> for EnumItem {
fn from(value: DomEnumItem) -> Self {
EnumItem::from_enum_name_and_value(value.ty, value.value)
.expect("cannot convert rbx_type::EnumItem with unknown type into EnumItem")
DomEnum::from_u32(v.value)
}
}

View file

@ -4,7 +4,6 @@ mod cframe;
mod color3;
mod color_sequence;
mod color_sequence_keypoint;
mod content;
mod r#enum;
mod r#enum_item;
mod r#enums;
@ -31,7 +30,6 @@ pub use cframe::CFrame;
pub use color3::Color3;
pub use color_sequence::ColorSequence;
pub use color_sequence_keypoint::ColorSequenceKeypoint;
pub use content::Content;
pub use faces::Faces;
pub use font::Font;
pub use number_range::NumberRange;

View file

@ -52,9 +52,6 @@ impl LuaUserData for Vector2 {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
// Methods
methods.add_method("Angle", |_, this, rhs: LuaUserDataRef<Vector2>| {
Ok(this.0.angle_between(rhs.0))
});
methods.add_method("Cross", |_, this, rhs: LuaUserDataRef<Vector2>| {
let this_v3 = Vec3::new(this.0.x, this.0.y, 0f32);
let rhs_v3 = Vec3::new(rhs.0.x, rhs.0.y, 0f32);
@ -63,14 +60,6 @@ impl LuaUserData for Vector2 {
methods.add_method("Dot", |_, this, rhs: LuaUserDataRef<Vector2>| {
Ok(this.0.dot(rhs.0))
});
methods.add_method(
"FuzzyEq",
|_, this, (rhs, epsilon): (LuaUserDataRef<Vector2>, f32)| {
let eq_x = (rhs.0.x - this.0.x).abs() <= epsilon;
let eq_y = (rhs.0.y - this.0.y).abs() <= epsilon;
Ok(eq_x && eq_y)
},
);
methods.add_method(
"Lerp",
|_, this, (rhs, alpha): (LuaUserDataRef<Vector2>, f32)| {
@ -83,10 +72,6 @@ impl LuaUserData for Vector2 {
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector2>| {
Ok(Vector2(this.0.min(rhs.0)))
});
methods.add_method("Abs", |_, this, ()| Ok(Vector2(this.0.abs())));
methods.add_method("Ceil", |_, this, ()| Ok(Vector2(this.0.ceil())));
methods.add_method("Floor", |_, this, ()| Ok(Vector2(this.0.floor())));
methods.add_method("Sign", |_, this, ()| Ok(Vector2(this.0.signum())));
// Metamethods
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);

View file

@ -133,10 +133,6 @@ impl LuaUserData for Vector3 {
methods.add_method("Min", |_, this, rhs: LuaUserDataRef<Vector3>| {
Ok(Vector3(this.0.min(rhs.0)))
});
methods.add_method("Abs", |_, this, ()| Ok(Vector3(this.0.abs())));
methods.add_method("Ceil", |_, this, ()| Ok(Vector3(this.0.ceil())));
methods.add_method("Floor", |_, this, ()| Ok(Vector3(this.0.floor())));
methods.add_method("Sign", |_, this, ()| Ok(Vector3(this.0.signum())));
// Metamethods
methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);

View file

@ -65,7 +65,7 @@ impl DocumentKind {
for child_ref in dom.root().children() {
if let Some(child_inst) = dom.get_by_ref(*child_ref) {
has_top_level_child = true;
if class_is_a_service(child_inst.class).unwrap_or(false) {
if class_is_a_service(&child_inst.class).unwrap_or(false) {
has_top_level_service = true;
break;
}

View file

@ -1,6 +1,6 @@
use rbx_dom_weak::{
types::{Ref as DomRef, VariantType as DomType},
ustr, Instance as DomInstance, WeakDom,
Instance as DomInstance, WeakDom,
};
use crate::shared::instance::class_is_a;
@ -18,8 +18,8 @@ pub fn postprocess_dom_for_model(dom: &mut WeakDom) {
remove_matching_prop(inst, DomType::UniqueId, "HistoryId");
// Similar story with ScriptGuid - this is used
// in the studio-only cloud script drafts feature
if class_is_a(inst.class, "LuaSourceContainer").unwrap_or(false) {
inst.properties.remove(&ustr("ScriptGuid"));
if class_is_a(&inst.class, "LuaSourceContainer").unwrap_or(false) {
inst.properties.remove("ScriptGuid");
}
});
}
@ -41,8 +41,7 @@ where
}
fn remove_matching_prop(inst: &mut DomInstance, ty: DomType, name: &'static str) {
let name = &ustr(name);
if inst.properties.get(name).is_some_and(|u| u.ty() == ty) {
if inst.properties.get(name).map_or(false, |u| u.ty() == ty) {
inst.properties.remove(name);
}
}

View file

@ -71,7 +71,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
"FindFirstAncestorWhichIsA",
|lua, this, class_name: String| {
ensure_not_destroyed(this)?;
this.find_ancestor(|child| class_is_a(child.class, &class_name).unwrap_or(false))
this.find_ancestor(|child| class_is_a(&child.class, &class_name).unwrap_or(false))
.into_lua(lua)
},
);
@ -104,7 +104,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
|lua, this, (class_name, recursive): (String, Option<bool>)| {
ensure_not_destroyed(this)?;
let predicate =
|child: &DomInstance| class_is_a(child.class, &class_name).unwrap_or(false);
|child: &DomInstance| class_is_a(&child.class, &class_name).unwrap_or(false);
if matches!(recursive, Some(true)) {
this.find_descendant(predicate).into_lua(lua)
} else {
@ -113,7 +113,8 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
},
);
m.add_method("IsA", |_, this, class_name: String| {
Ok(class_is_a(this.class_name, class_name).unwrap_or(false))
ensure_not_destroyed(this)?;
Ok(class_is_a(&this.class_name, class_name).unwrap_or(false))
});
m.add_method(
"IsAncestorOf",
@ -216,21 +217,20 @@ fn instance_property_get<'lua>(
this: &Instance,
prop_name: String,
) -> LuaResult<LuaValue<'lua>> {
ensure_not_destroyed(this)?;
match prop_name.as_str() {
"ClassName" => return this.get_class_name().into_lua(lua),
"Name" => {
return this.get_name().into_lua(lua);
}
"Parent" => {
return this.get_parent().into_lua(lua);
}
_ => {}
}
ensure_not_destroyed(this)?;
if prop_name.as_str() == "Name" {
return this.get_name().into_lua(lua);
}
if let Some(info) = find_property_info(this.class_name, &prop_name) {
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
if let Some(prop) = this.get_property(&prop_name) {
if let DomValue::Enum(enum_value) = prop {
let enum_name = info.enum_name.ok_or_else(|| {
@ -275,7 +275,7 @@ fn instance_property_get<'lua>(
} else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
Ok(LuaValue::UserData(lua.create_userdata(inst)?))
} else if let Some(getter) = InstanceRegistry::find_property_getter(lua, this, &prop_name) {
getter.call(*this)
getter.call(this.clone())
} else if let Some(method) = InstanceRegistry::find_method(lua, this, &prop_name) {
Ok(LuaValue::Function(method))
} else {
@ -321,17 +321,17 @@ fn instance_property_set<'lua>(
}
type Parent<'lua> = Option<LuaUserDataRef<'lua, Instance>>;
let parent = Parent::from_lua(prop_value, lua)?;
this.set_parent(parent.map(|p| *p));
this.set_parent(parent.map(|p| p.clone()));
return Ok(());
}
_ => {}
}
if let Some(info) = find_property_info(this.class_name, &prop_name) {
if let Some(info) = find_property_info(&this.class_name, &prop_name) {
if let Some(enum_name) = info.enum_name {
match LuaUserDataRef::<EnumItem>::from_lua(prop_value, lua) {
Ok(given_enum) if given_enum.parent.desc.name == enum_name => {
this.set_property(prop_name, DomValue::EnumItem((*given_enum).clone().into()));
this.set_property(prop_name, DomValue::Enum((*given_enum).clone().into()));
Ok(())
}
Ok(given_enum) => Err(LuaError::RuntimeError(format!(
@ -354,7 +354,7 @@ fn instance_property_set<'lua>(
)))
}
} else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
setter.call((*this, prop_value))
setter.call((this.clone(), prop_value))
} else {
Err(LuaError::RuntimeError(format!(
"{prop_name} is not a valid member of {this}",

View file

@ -26,7 +26,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
### See Also
* [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult<Instance> {
get_or_create_property_ref_instance(this, "Workspace", "Workspace")
@ -37,7 +37,7 @@ fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult<Instance> {
### See Also
* [`GetService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#GetService)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> LuaResult<Instance> {
if matches!(class_is_a_service(&service_name), None | Some(false)) {
@ -48,7 +48,7 @@ fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> Lua
Ok(service)
} else {
let service = Instance::new_orphaned(service_name);
service.set_parent(Some(*this));
service.set_parent(Some(this.clone()));
Ok(service)
}
}
@ -58,7 +58,7 @@ fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> Lua
### See Also
* [`FindService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#FindService)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn data_model_find_service(
_: &Lua,

View file

@ -11,7 +11,7 @@ use mlua::prelude::*;
use once_cell::sync::Lazy;
use rbx_dom_weak::{
types::{Attributes as DomAttributes, Ref as DomRef, Variant as DomValue},
ustr, Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, Ustr, WeakDom,
Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom,
};
use lune_utils::TableBuilder;
@ -34,10 +34,10 @@ const PROPERTY_NAME_TAGS: &str = "Tags";
static INTERNAL_DOM: Lazy<Mutex<WeakDom>> =
Lazy::new(|| Mutex::new(WeakDom::new(DomInstanceBuilder::new("ROOT"))));
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub struct Instance {
pub(crate) dom_ref: DomRef,
pub(crate) class_name: Ustr,
pub(crate) class_name: String,
}
impl Instance {
@ -45,26 +45,38 @@ impl Instance {
Creates a new `Instance` from an existing dom object ref.
Panics if the instance does not exist in the internal dom,
or if the given dom object ref points to the internal dom root.
or if the given dom object ref points to the dom root.
**WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking.
*/
#[must_use]
pub fn new(dom_ref: DomRef) -> Self {
Self::new_opt(dom_ref).expect("Failed to find instance in document")
pub(crate) fn new(dom_ref: DomRef) -> Self {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let instance = dom
.get_by_ref(dom_ref)
.expect("Failed to find instance in document");
assert!(
!(instance.referent() == dom.root_ref()),
"Instances can not be created from dom roots"
);
Self {
dom_ref,
class_name: instance.class.clone(),
}
}
/**
Creates a new `Instance` from a dom object ref, if the instance exists.
Panics if the given dom object ref points to the internal dom root.
Panics if the given dom object ref points to the dom root.
**WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking.
*/
#[must_use]
pub fn new_opt(dom_ref: DomRef) -> Option<Self> {
pub(crate) fn new_opt(dom_ref: DomRef) -> Option<Self> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
if let Some(instance) = dom.get_by_ref(dom_ref) {
@ -75,7 +87,7 @@ impl Instance {
Some(Self {
dom_ref,
class_name: instance.class,
class_name: instance.class.clone(),
})
} else {
None
@ -85,25 +97,24 @@ impl Instance {
/**
Creates a new orphaned `Instance` with a given class name.
An orphaned instance is an instance at the root of Lune's internal weak dom.
An orphaned instance is an instance at the root of a weak dom.
**WARNING:** Creating a new instance requires locking the internal dom,
any existing lock must first be released to prevent any deadlocking.
*/
#[must_use]
pub fn new_orphaned(class_name: impl AsRef<str>) -> Self {
pub(crate) fn new_orphaned(class_name: impl AsRef<str>) -> Self {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let class_name = class_name.as_ref();
let instance = DomInstanceBuilder::new(class_name);
let instance = DomInstanceBuilder::new(class_name.to_string());
let dom_root = dom.root_ref();
let dom_ref = dom.insert(dom_root, instance);
Self {
dom_ref,
class_name: ustr(class_name),
class_name: class_name.to_string(),
}
}
@ -111,11 +122,10 @@ impl Instance {
Creates a new orphaned `Instance` by transferring
it from an external weak dom to the internal one.
An orphaned instance is an instance at the root of Lune's internal weak dom.
An orphaned instance is an instance at the root of a weak dom.
Panics if the given dom ref is the root dom ref of the external weak dom.
*/
#[must_use]
pub fn from_external_dom(external_dom: &mut WeakDom, external_dom_ref: DomRef) -> Self {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let dom_root = dom.root_ref();
@ -141,12 +151,6 @@ impl Instance {
cloned
}
/**
Clones multiple instances to an external weak dom.
This will place the instances as children of the
root of the weak dom, and return their referents.
*/
pub fn clone_multiple_into_external_dom(
referents: &[DomRef],
external_dom: &mut WeakDom,
@ -170,7 +174,7 @@ impl Instance {
### See Also
* [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
#[must_use]
pub fn clone_instance(&self) -> Self {
@ -194,7 +198,7 @@ impl Instance {
### See Also
* [`Destroy`](https://create.roblox.com/docs/reference/engine/classes/Instance#Destroy)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn destroy(&mut self) -> bool {
if self.is_destroyed() {
@ -221,7 +225,7 @@ impl Instance {
### See Also
* [`Instance::Destroy`] for more info about what happens when an instance gets destroyed
* [`ClearAllChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClearAllChildren)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn clear_all_children(&mut self) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -241,10 +245,10 @@ impl Instance {
### See Also
* [`IsA`](https://create.roblox.com/docs/reference/engine/classes/Instance#IsA)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn is_a(&self, class_name: impl AsRef<str>) -> bool {
class_is_a(self.class_name, class_name).unwrap_or(false)
class_is_a(&self.class_name, class_name).unwrap_or(false)
}
/**
@ -254,7 +258,7 @@ impl Instance {
### See Also
* [`ClassName`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClassName)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
#[must_use]
pub fn get_class_name(&self) -> &str {
@ -266,7 +270,7 @@ impl Instance {
### See Also
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_name(&self) -> String {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -282,7 +286,7 @@ impl Instance {
### See Also
* [`Name`](https://create.roblox.com/docs/reference/engine/classes/Instance#Name)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn set_name(&self, name: impl Into<String>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -297,12 +301,15 @@ impl Instance {
### See Also
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_parent(&self) -> Option<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let parent_ref = dom.get_by_ref(self.dom_ref)?.parent();
let parent_ref = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.parent();
if parent_ref == dom.root_ref() {
None
@ -317,11 +324,11 @@ impl Instance {
If the provided parent is [`None`] the instance will become orphaned.
An orphaned instance is an instance at the root of Lune's internal weak dom.
An orphaned instance is an instance at the root of a weak dom.
### See Also
* [`Parent`](https://create.roblox.com/docs/reference/engine/classes/Instance#Parent)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn set_parent(&self, parent: Option<Instance>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -341,7 +348,7 @@ impl Instance {
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document")
.properties
.get(&ustr(name.as_ref()))
.get(name.as_ref())
.cloned()
}
@ -358,7 +365,7 @@ impl Instance {
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document")
.properties
.insert(ustr(name.as_ref()), value);
.insert(name.as_ref().to_string(), value);
}
/**
@ -366,7 +373,7 @@ impl Instance {
### See Also
* [`GetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttribute)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_attribute(&self, name: impl AsRef<str>) -> Option<DomValue> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -374,7 +381,7 @@ impl Instance {
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
{
attributes.get(name.as_ref()).cloned()
} else {
@ -387,7 +394,7 @@ impl Instance {
### See Also
* [`GetAttributes`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetAttributes)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_attributes(&self) -> BTreeMap<String, DomValue> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -395,7 +402,7 @@ impl Instance {
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get(&ustr(PROPERTY_NAME_ATTRIBUTES))
inst.properties.get(PROPERTY_NAME_ATTRIBUTES)
{
attributes.clone().into_iter().collect()
} else {
@ -408,7 +415,7 @@ impl Instance {
### See Also
* [`SetAttribute`](https://create.roblox.com/docs/reference/engine/classes/Instance#SetAttribute)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn set_attribute(&self, name: impl AsRef<str>, value: DomValue) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -422,14 +429,14 @@ impl Instance {
value => value,
};
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
{
attributes.insert(name.as_ref().to_string(), value);
} else {
let mut attributes = DomAttributes::new();
attributes.insert(name.as_ref().to_string(), value);
inst.properties.insert(
ustr(PROPERTY_NAME_ATTRIBUTES),
PROPERTY_NAME_ATTRIBUTES.to_string(),
DomValue::Attributes(attributes),
);
}
@ -449,11 +456,11 @@ impl Instance {
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Attributes(attributes)) =
inst.properties.get_mut(&ustr(PROPERTY_NAME_ATTRIBUTES))
inst.properties.get_mut(PROPERTY_NAME_ATTRIBUTES)
{
attributes.remove(name.as_ref());
if attributes.is_empty() {
inst.properties.remove(&ustr(PROPERTY_NAME_ATTRIBUTES));
inst.properties.remove(PROPERTY_NAME_ATTRIBUTES);
}
}
}
@ -463,18 +470,18 @@ impl Instance {
### See Also
* [`AddTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#AddTag)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn add_tag(&self, name: impl AsRef<str>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
tags.push(name.as_ref());
} else {
inst.properties.insert(
ustr(PROPERTY_NAME_TAGS),
PROPERTY_NAME_TAGS.to_string(),
DomValue::Tags(vec![name.as_ref().to_string()].into()),
);
}
@ -485,14 +492,14 @@ impl Instance {
### See Also
* [`GetTags`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#GetTags)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_tags(&self) -> Vec<String> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
tags.iter().map(ToString::to_string).collect()
} else {
Vec::new()
@ -504,14 +511,14 @@ impl Instance {
### See Also
* [`HasTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#HasTag)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn has_tag(&self, name: impl AsRef<str>) -> bool {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get(&ustr(PROPERTY_NAME_TAGS)) {
if let Some(DomValue::Tags(tags)) = inst.properties.get(PROPERTY_NAME_TAGS) {
let name = name.as_ref();
tags.iter().any(|tag| tag == name)
} else {
@ -524,19 +531,21 @@ impl Instance {
### See Also
* [`RemoveTag`](https://create.roblox.com/docs/reference/engine/classes/CollectionService#RemoveTag)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn remove_tag(&self, name: impl AsRef<str>) {
let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
let inst = dom
.get_by_ref_mut(self.dom_ref)
.expect("Failed to find instance in document");
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(&ustr(PROPERTY_NAME_TAGS)) {
if let Some(DomValue::Tags(tags)) = inst.properties.get_mut(PROPERTY_NAME_TAGS) {
let name = name.as_ref();
let mut new_tags = tags.iter().map(ToString::to_string).collect::<Vec<_>>();
new_tags.retain(|tag| tag != name);
inst.properties
.insert(ustr(PROPERTY_NAME_TAGS), DomValue::Tags(new_tags.into()));
inst.properties.insert(
PROPERTY_NAME_TAGS.to_string(),
DomValue::Tags(new_tags.into()),
);
}
}
@ -548,7 +557,7 @@ impl Instance {
### See Also
* [`GetChildren`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetChildren)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_children(&self) -> Vec<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -571,7 +580,7 @@ impl Instance {
### See Also
* [`GetDescendants`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetDescendants)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_descendants(&self) -> Vec<Instance> {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -605,7 +614,7 @@ impl Instance {
### See Also
* [`GetFullName`](https://create.roblox.com/docs/reference/engine/classes/Instance#GetFullName)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn get_full_name(&self) -> String {
let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
@ -695,7 +704,7 @@ impl Instance {
### See Also
* [`FindFirstDescendant`](https://create.roblox.com/docs/reference/engine/classes/Instance#FindFirstDescendant)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
pub fn find_descendant<F>(&self, predicate: F) -> Option<Instance>
where

View file

@ -27,8 +27,9 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M)
}
fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
if let Some(Variant::MaterialColors(inner)) = instance.get_property("MaterialColors") {
inner
if let Some(Variant::MaterialColors(material_colors)) = instance.get_property("MaterialColors")
{
material_colors
} else {
MaterialColors::default()
}
@ -39,7 +40,7 @@ fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
### See Also
* [`GetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#GetMaterialColor)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn terrain_get_material_color(_: &Lua, this: &Instance, material: EnumItem) -> LuaResult<Color3> {
let material_colors = get_or_create_material_colors(this);
@ -64,7 +65,7 @@ fn terrain_get_material_color(_: &Lua, this: &Instance, material: EnumItem) -> L
### See Also
* [`SetMaterialColor`](https://create.roblox.com/docs/reference/engine/classes/Terrain#SetMaterialColor)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn terrain_set_material_color(
_: &Lua,

View file

@ -16,7 +16,7 @@ pub fn add_fields<'lua, F: LuaUserDataFields<'lua, Instance>>(f: &mut F) {
### See Also
* [`Terrain`](https://create.roblox.com/docs/reference/engine/classes/Workspace#Terrain)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn workspace_get_terrain(_: &Lua, this: &Instance) -> LuaResult<Instance> {
get_or_create_property_ref_instance(this, "Terrain", "Terrain")
@ -27,7 +27,7 @@ fn workspace_get_terrain(_: &Lua, this: &Instance) -> LuaResult<Instance> {
### See Also
* [`CurrentCamera`](https://create.roblox.com/docs/reference/engine/classes/Workspace#CurrentCamera)
on the Roblox Developer Hub
on the Roblox Developer Hub
*/
fn workspace_get_camera(_: &Lua, this: &Instance) -> LuaResult<Instance> {
get_or_create_property_ref_instance(this, "CurrentCamera", "Camera")

View file

@ -25,7 +25,6 @@ fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
export::<Color3>(lua)?,
export::<ColorSequence>(lua)?,
export::<ColorSequenceKeypoint>(lua)?,
export::<Content>(lua)?,
export::<Faces>(lua)?,
export::<Font>(lua)?,
export::<NumberRange>(lua)?,

View file

@ -122,7 +122,7 @@ pub(crate) fn get_or_create_property_ref_instance(
Ok(inst)
} else {
let inst = Instance::new_orphaned(class_name);
inst.set_parent(Some(*this));
inst.set_parent(Some(this.clone()));
this.set_property(prop_name, DomValue::Ref(inst.dom_ref));
Ok(inst)
}

View file

@ -23,7 +23,7 @@ pub fn make_list_writer() -> Box<ListWriter> {
})
}
/*
/**
Userdata metamethod implementations
Note that many of these return [`LuaResult`] even though they don't

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-datetime"
version = "0.1.3"
version = "0.1.1"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,10 +13,10 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua = { version = "0.9.7", features = ["luau"] }
thiserror = "1.0"
chrono = "0.4.38"
chrono_lc = "0.1.6"
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }

View file

@ -60,7 +60,7 @@ where
}
}
/*
/**
Conversion methods between `DateTimeValues` and plain lua tables
Note that the `IntoLua` implementation here uses a read-only table,
@ -117,7 +117,7 @@ impl IntoLua<'_> for DateTimeValues {
}
}
/*
/**
Conversion methods between chrono's timezone-aware `DateTime` to
and from our non-timezone-aware `DateTimeValues` values struct
*/

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-fs"
version = "0.1.2"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,11 +13,11 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua = { version = "0.9.7", features = ["luau"] }
bstr = "1.9"
tokio = { version = "1", default-features = false, features = ["fs"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-datetime = { version = "0.1.2", path = "../lune-std-datetime" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }
lune-std-datetime = { version = "0.1.0", path = "../lune-std-datetime" }

25
crates/lune-std-fs/src/lib.rs Executable file → Normal file
View file

@ -1,7 +1,7 @@
#![allow(clippy::cargo_common_metadata)]
use std::io::ErrorKind as IoErrorKind;
use std::path::PathBuf;
use std::path::{PathBuf, MAIN_SEPARATOR};
use bstr::{BString, ByteSlice};
use mlua::prelude::*;
@ -50,16 +50,29 @@ async fn fs_read_dir(_: &Lua, path: String) -> LuaResult<Vec<String>> {
let mut dir_strings = Vec::new();
let mut dir = fs::read_dir(&path).await.into_lua_err()?;
while let Some(dir_entry) = dir.next_entry().await.into_lua_err()? {
if let Some(dir_name_str) = dir_entry.file_name().to_str() {
dir_strings.push(dir_name_str.to_owned());
if let Some(dir_path_str) = dir_entry.path().to_str() {
dir_strings.push(dir_path_str.to_owned());
} else {
return Err(LuaError::RuntimeError(format!(
"File name could not be converted into a string: '{}'",
dir_entry.file_name().to_string_lossy()
"File path could not be converted into a string: '{}'",
dir_entry.path().display()
)));
}
}
Ok(dir_strings)
let mut dir_string_prefix = path;
if !dir_string_prefix.ends_with(MAIN_SEPARATOR) {
dir_string_prefix.push(MAIN_SEPARATOR);
}
let dir_strings_no_prefix = dir_strings
.iter()
.map(|inner_path| {
inner_path
.trim()
.trim_start_matches(&dir_string_prefix)
.to_owned()
})
.collect::<Vec<_>>();
Ok(dir_strings_no_prefix)
}
async fn fs_write_file(_: &Lua, (path, contents): (String, BString)) -> LuaResult<()> {

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-luau"
version = "0.1.2"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,6 +13,6 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau", "luau-jit"] }
mlua = { version = "0.9.7", features = ["luau"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }

View file

@ -2,7 +2,7 @@
use mlua::prelude::*;
use lune_utils::{jit::JitStatus, TableBuilder};
use lune_utils::TableBuilder;
mod options;
@ -44,47 +44,26 @@ fn load_source<'lua>(
(source, options): (LuaString<'lua>, LuauLoadOptions),
) -> LuaResult<LuaFunction<'lua>> {
let mut chunk = lua.load(source.as_bytes()).set_name(options.debug_name);
let env_changed = options.environment.is_some();
if let Some(custom_environment) = options.environment {
let environment = lua.create_table()?;
if let Some(environment) = options.environment {
let environment_with_globals = lua.create_table()?;
// Inject all globals into the environment
if options.inject_globals {
for pair in lua.globals().pairs() {
let (key, value): (LuaValue, LuaValue) = pair?;
environment.set(key, value)?;
}
if let Some(global_metatable) = lua.globals().get_metatable() {
environment.set_metatable(Some(global_metatable));
}
} else if let Some(custom_metatable) = custom_environment.get_metatable() {
// Since we don't need to set the global metatable,
// we can just set a custom metatable if it exists
environment.set_metatable(Some(custom_metatable));
if let Some(meta) = environment.get_metatable() {
environment_with_globals.set_metatable(Some(meta));
}
// Inject the custom environment
for pair in custom_environment.pairs() {
for pair in lua.globals().pairs() {
let (key, value): (LuaValue, LuaValue) = pair?;
environment.set(key, value)?;
environment_with_globals.set(key, value)?;
}
chunk = chunk.set_environment(environment);
for pair in environment.pairs() {
let (key, value): (LuaValue, LuaValue) = pair?;
environment_with_globals.set(key, value)?;
}
chunk = chunk.set_environment(environment_with_globals);
}
// Enable JIT if codegen is enabled and the environment hasn't
// changed, otherwise disable JIT since it'll fall back anyways
lua.enable_jit(options.codegen_enabled && !env_changed);
let function = chunk.into_function()?;
lua.enable_jit(
lua.app_data_ref::<JitStatus>()
.ok_or(LuaError::runtime(
"Failed to get current JitStatus ref from AppData",
))?
.enabled(),
);
Ok(function)
chunk.into_function()
}

View file

@ -79,11 +79,13 @@ impl<'lua> FromLua<'lua> for LuauCompileOptions {
}
}
/**
Options for loading Lua source code.
*/
#[derive(Debug, Clone)]
pub struct LuauLoadOptions<'lua> {
pub(crate) debug_name: String,
pub(crate) environment: Option<LuaTable<'lua>>,
pub(crate) inject_globals: bool,
pub(crate) codegen_enabled: bool,
}
impl Default for LuauLoadOptions<'_> {
@ -91,8 +93,6 @@ impl Default for LuauLoadOptions<'_> {
Self {
debug_name: DEFAULT_DEBUG_NAME.to_string(),
environment: None,
inject_globals: true,
codegen_enabled: false,
}
}
}
@ -112,21 +112,11 @@ impl<'lua> FromLua<'lua> for LuauLoadOptions<'lua> {
options.environment = Some(environment);
}
if let Some(inject_globals) = t.get("injectGlobals")? {
options.inject_globals = inject_globals;
}
if let Some(codegen_enabled) = t.get("codegenEnabled")? {
options.codegen_enabled = codegen_enabled;
}
options
}
LuaValue::String(s) => Self {
debug_name: s.to_string_lossy().to_string(),
environment: None,
inject_globals: true,
codegen_enabled: false,
},
_ => {
return Err(LuaError::FromLuaConversionError {

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-net"
version = "0.1.2"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,8 +13,8 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
mlua = { version = "0.9.7", features = ["luau"] }
mlua-luau-scheduler = "0.0.2"
bstr = "1.9"
futures-util = "0.3"
@ -35,5 +35,5 @@ tokio = { version = "1", default-features = false, features = [
"macros",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std-serde = { version = "0.1.2", path = "../lune-std-serde" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }
lune-std-serde = { version = "0.1.0", path = "../lune-std-serde" }

View file

@ -65,9 +65,9 @@ async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
res.await?.into_lua_table(lua)
}
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaValue> {
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaTable> {
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
NetWebSocket::new(ws).into_lua(lua)
NetWebSocket::new(ws).into_lua_table(lua)
}
async fn net_serve<'lua>(

View file

@ -18,7 +18,6 @@ impl LuaRequest {
let path = self.head.uri.path().to_string();
let body = lua.create_string(&self.body)?;
#[allow(clippy::mutable_key_type)]
let query: HashMap<LuaString, LuaString> = self
.head
.uri
@ -33,7 +32,6 @@ impl LuaRequest {
})
.collect::<LuaResult<_>>()?;
#[allow(clippy::mutable_key_type)]
let headers: HashMap<LuaString, LuaString> = self
.head
.headers

View file

@ -40,13 +40,13 @@ impl Service<Request<Incoming>> for Svc {
lua.spawn_local(async move {
let sock = sock.await.unwrap();
let lua_sock = NetWebSocket::new(sock);
let lua_val = lua_sock.into_lua(&lua_inner).unwrap();
let lua_tab = lua_sock.into_lua_table(&lua_inner).unwrap();
let handler_websocket: LuaFunction =
keys.websocket_handler(&lua_inner).unwrap().unwrap();
lua_inner
.push_thread_back(handler_websocket, lua_val)
.push_thread_back(handler_websocket, lua_tab)
.unwrap();
});

View file

@ -23,6 +23,29 @@ use hyper_tungstenite::{
WebSocketStream,
};
use lune_utils::TableBuilder;
// Wrapper implementation for compatibility and changing colon syntax to dot syntax
const WEB_SOCKET_IMPL_LUA: &str = r#"
return freeze(setmetatable({
close = function(...)
return websocket:close(...)
end,
send = function(...)
return websocket:send(...)
end,
next = function(...)
return websocket:next(...)
end,
}, {
__index = function(self, key)
if key == "closeCode" then
return websocket.closeCode
end
end,
}))
"#;
#[derive(Debug)]
pub struct NetWebSocket<T> {
close_code_exists: Arc<AtomicBool>,
@ -102,6 +125,25 @@ where
let mut ws = self.write_stream.lock().await;
ws.close().await.into_lua_err()
}
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?;
let table_freeze = lua
.globals()
.get::<_, LuaTable>("table")?
.get::<_, LuaFunction>("freeze")?;
let env = TableBuilder::new(lua)?
.with_value("websocket", self.clone())?
.with_value("setmetatable", setmetatable)?
.with_value("freeze", table_freeze)?
.build_readonly()?;
lua.load(WEB_SOCKET_IMPL_LUA)
.set_name("websocket")
.set_environment(env)
.eval()
}
}
impl<T> LuaUserData for NetWebSocket<T>

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-process"
version = "0.1.3"
version = "0.1.1"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,16 +13,13 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
mlua = { version = "0.9.7", features = ["luau"] }
mlua-luau-scheduler = "0.0.2"
directories = "5.0"
pin-project = "1.0"
os_str_bytes = { version = "7.0", features = ["conversions"] }
bstr = "1.9"
bytes = "1.6.0"
tokio = { version = "1", default-features = false, features = [
"io-std",
"io-util",
@ -31,4 +28,4 @@ tokio = { version = "1", default-features = false, features = [
"sync",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }

View file

@ -1,33 +1,27 @@
#![allow(clippy::cargo_common_metadata)]
use std::{
cell::RefCell,
env::{
self,
consts::{ARCH, OS},
},
path::MAIN_SEPARATOR,
process::Stdio,
rc::Rc,
sync::Arc,
};
use mlua::prelude::*;
use lune_utils::TableBuilder;
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
use options::ProcessSpawnOptionsStdio;
use os_str_bytes::RawOsString;
use stream::{ChildProcessReader, ChildProcessWriter};
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
use tokio::io::AsyncWriteExt;
mod options;
mod stream;
mod tee_writer;
mod wait_for_child;
use self::options::ProcessSpawnOptions;
use self::wait_for_child::wait_for_child;
use self::wait_for_child::{wait_for_child, WaitForChildResult};
use lune_utils::path::get_current_dir;
@ -48,13 +42,8 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
cwd_str.push(MAIN_SEPARATOR);
}
// Create constants for OS & processor architecture
let os = lua.create_string(OS.to_lowercase())?;
let arch = lua.create_string(ARCH.to_lowercase())?;
let endianness = lua.create_string(if cfg!(target_endian = "big") {
"big"
} else {
"little"
})?;
let os = lua.create_string(&OS.to_lowercase())?;
let arch = lua.create_string(&ARCH.to_lowercase())?;
// Create readonly args array
let args_vec = lua
.app_data_ref::<Vec<String>>()
@ -80,13 +69,11 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_value("os", os)?
.with_value("arch", arch)?
.with_value("endianness", endianness)?
.with_value("args", args_tab)?
.with_value("cwd", cwd_str)?
.with_value("env", env_tab)?
.with_value("exit", process_exit)?
.with_async_function("exec", process_exec)?
.with_function("create", process_create)?
.with_async_function("spawn", process_spawn)?
.build_readonly()
}
@ -154,16 +141,11 @@ fn process_env_iter<'lua>(
})
}
async fn process_exec(
async fn process_spawn(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
let res = lua
.spawn(async move {
let cmd = spawn_command_with_stdin(program, args, options.clone()).await?;
wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await
})
.await?;
let res = lua.spawn(spawn_command(program, args, options)).await?;
/*
NOTE: If an exit code was not given by the child process,
@ -186,104 +168,30 @@ async fn process_exec(
.build_readonly()
}
#[allow(clippy::await_holding_refcell_ref)]
fn process_create(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
// We do not want the user to provide stdio options for process.create,
// so we reset the options, regardless of what the user provides us
let mut spawn_options = options.clone();
spawn_options.stdio = ProcessSpawnOptionsStdio::default();
let (code_tx, code_rx) = tokio::sync::broadcast::channel(4);
let code_rx_rc = Rc::new(RefCell::new(code_rx));
let child = spawn_command(program, args, spawn_options)?;
let child_arc = Arc::new(RwLock::new(child));
let child_arc_clone = Arc::clone(&child_arc);
let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write());
let stdin = child_lock.stdin.take().unwrap();
let stdout = child_lock.stdout.take().unwrap();
let stderr = child_lock.stderr.take().unwrap();
let child_arc_inner = Arc::clone(&child_arc);
// Spawn a background task to wait for the child to exit and send the exit code
let status_handle = tokio::spawn(async move {
let res = child_arc_inner.write().await.wait().await;
if let Ok(output) = res {
let code = output.code().unwrap_or_default();
code_tx
.send(code)
.expect("ExitCode receiver was unexpectedly dropped");
}
});
TableBuilder::new(lua)?
.with_value("stdout", ChildProcessReader(stdout))?
.with_value("stderr", ChildProcessReader(stderr))?
.with_value("stdin", ChildProcessWriter(stdin))?
.with_async_function("kill", move |_, ()| {
// First, stop the status task so the RwLock is dropped
status_handle.abort();
let child_arc_clone = Arc::clone(&child_arc);
// Then get another RwLock to write to the child process and kill it
async move { Ok(child_arc_clone.write().await.kill().await?) }
})?
.with_async_function("status", move |lua, ()| {
let code_rx_rc_clone = Rc::clone(&code_rx_rc);
async move {
// Exit code of 9 corresponds to SIGKILL, which should be the only case where
// the receiver gets suddenly dropped
let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9);
TableBuilder::new(lua)?
.with_value("code", code)?
.with_value("ok", code == 0)?
.build_readonly()
}
})?
.build_readonly()
}
async fn spawn_command_with_stdin(
async fn spawn_command(
program: String,
args: Option<Vec<String>>,
mut options: ProcessSpawnOptions,
) -> LuaResult<Child> {
) -> LuaResult<WaitForChildResult> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
let stdin = options.stdio.stdin.take();
let mut child = spawn_command(program, args, options)?;
let mut child = options
.into_command(program, args)
.stdin(if stdin.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
if let Some(stdin) = stdin {
let mut child_stdin = child.stdin.take().unwrap();
child_stdin.write_all(&stdin).await.into_lua_err()?;
}
Ok(child)
}
fn spawn_command(
program: String,
args: Option<Vec<String>>,
options: ProcessSpawnOptions,
) -> LuaResult<Child> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
let child = options
.into_command(program, args)
.stdin(Stdio::piped())
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
Ok(child)
wait_for_child(child, stdout, stderr).await
}

View file

@ -1,58 +0,0 @@
use bstr::BString;
use bytes::BytesMut;
use mlua::prelude::*;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
const CHUNK_SIZE: usize = 8;
#[derive(Debug, Clone)]
pub struct ChildProcessReader<R: AsyncRead>(pub R);
#[derive(Debug, Clone)]
pub struct ChildProcessWriter<W: AsyncWrite>(pub W);
impl<R: AsyncRead + Unpin> ChildProcessReader<R> {
pub async fn read(&mut self, chunk_size: Option<usize>) -> LuaResult<Vec<u8>> {
let mut buf = BytesMut::with_capacity(chunk_size.unwrap_or(CHUNK_SIZE));
self.0.read_buf(&mut buf).await?;
Ok(buf.to_vec())
}
pub async fn read_to_end(&mut self) -> LuaResult<Vec<u8>> {
let mut buf = vec![];
self.0.read_to_end(&mut buf).await?;
Ok(buf)
}
}
impl<R: AsyncRead + Unpin + 'static> LuaUserData for ChildProcessReader<R> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("read", |lua, this, chunk_size: Option<usize>| async move {
let buf = this.read(chunk_size).await?;
if buf.is_empty() {
return Ok(LuaValue::Nil);
}
Ok(LuaValue::String(lua.create_string(buf)?))
});
methods.add_async_method_mut("readToEnd", |lua, this, ()| async {
Ok(lua.create_string(this.read_to_end().await?))
});
}
}
impl<W: AsyncWrite + Unpin> ChildProcessWriter<W> {
pub async fn write(&mut self, data: BString) -> LuaResult<()> {
self.0.write_all(data.as_ref()).await?;
Ok(())
}
}
impl<W: AsyncWrite + Unpin + 'static> LuaUserData for ChildProcessWriter<W> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("write", |_, this, data| async { this.write(data).await });
}
}

View file

@ -33,7 +33,7 @@ where
}
}
impl<W> AsyncWrite for AsyncTeeWriter<'_, W>
impl<'a, W> AsyncWrite for AsyncTeeWriter<'a, W>
where
W: AsyncWrite + Unpin,
{

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-regex"
version = "0.1.2"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,9 +13,9 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua = { version = "0.9.7", features = ["luau"] }
regex = "1.10"
self_cell = "1.0"
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }

View file

@ -81,7 +81,7 @@ impl LuaUserData for LuaCaptures {
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.num_captures()));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(format!("{}", this.num_captures()))
Ok(format!("RegexCaptures({})", this.num_captures()))
});
}

View file

@ -47,7 +47,7 @@ impl LuaUserData for LuaMatch {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::Len, |_, this, ()| Ok(this.range().len()));
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(this.slice().to_string())
Ok(format!("RegexMatch({})", this.slice()))
});
}
}

View file

@ -66,7 +66,7 @@ impl LuaUserData for LuaRegex {
);
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
Ok(this.inner.as_str().to_string())
Ok(format!("Regex({})", this.inner.as_str()))
});
}

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-roblox"
version = "0.1.4"
version = "0.1.1"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,12 +13,11 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
mlua = { version = "0.9.7", features = ["luau"] }
mlua-luau-scheduler = "0.0.2"
once_cell = "1.17"
rbx_cookie = { version = "0.1.4", default-features = false }
roblox_install = "1.0.0"
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-roblox = { version = "0.1.4", path = "../lune-roblox" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }
lune-roblox = { version = "0.1.1", path = "../lune-roblox" }

View file

@ -13,7 +13,6 @@ use lune_roblox::{
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
use lune_utils::TableBuilder;
use roblox_install::RobloxStudio;
/**
Creates the `roblox` standard library module.
@ -40,10 +39,6 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.with_function("getReflectionDatabase", get_reflection_database)?
.with_function("implementProperty", implement_property)?
.with_function("implementMethod", implement_method)?
.with_function("studioApplicationPath", studio_application_path)?
.with_function("studioContentPath", studio_content_path)?
.with_function("studioPluginPath", studio_plugin_path)?
.with_function("studioBuiltinPluginPath", studio_builtin_plugin_path)?
.build_readonly()
}
@ -77,7 +72,7 @@ async fn serialize_place<'lua>(
lua: &'lua Lua,
(data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let data_model = *data_model;
let data_model = (*data_model).clone();
let fut = lua.spawn_blocking(move || {
let doc = Document::from_data_model_instance(data_model)?;
let bytes = doc.to_bytes_with_format(match as_xml {
@ -94,7 +89,7 @@ async fn serialize_model<'lua>(
lua: &'lua Lua,
(instances, as_xml): (Vec<LuaUserDataRef<'lua, Instance>>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let instances = instances.iter().map(|i| **i).collect();
let instances = instances.iter().map(|i| (*i).clone()).collect();
let fut = lua.spawn_blocking(move || {
let doc = Document::from_instance_array(instances)?;
let bytes = doc.to_bytes_with_format(match as_xml {
@ -152,27 +147,3 @@ fn implement_method(
InstanceRegistry::insert_method(lua, &class_name, &method_name, method).into_lua_err()?;
Ok(())
}
fn studio_application_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.application_path().display().to_string())
.map_err(LuaError::external)
}
fn studio_content_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.content_path().display().to_string())
.map_err(LuaError::external)
}
fn studio_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.plugins_path().display().to_string())
.map_err(LuaError::external)
}
fn studio_builtin_plugin_path(_: &Lua, _: ()) -> LuaResult<String> {
RobloxStudio::locate()
.map(|rs| rs.built_in_plugins_path().display().to_string())
.map_err(LuaError::external)
}

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-serde"
version = "0.1.2"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,7 +13,7 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau", "serialize"] }
mlua = { version = "0.9.7", features = ["luau", "serialize"] }
async-compression = { version = "0.4", features = [
"tokio",
@ -23,25 +23,15 @@ async-compression = { version = "0.4", features = [
"zlib",
] }
bstr = "1.9"
lz4 = "1.26"
lz4 = "1.24"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_yaml = "0.9"
toml = { version = "0.8", features = ["preserve_order"] }
digest = "0.10.7"
hmac = "0.12.1"
md-5 = "0.10.6"
sha1 = "0.10.6"
sha2 = "0.10.8"
sha3 = "0.10.8"
# This feature MIGHT break due to the unstable nature of the digest crate.
# Check before updating it.
blake3 = { version = "=1.5.0", features = ["traits-preview"] }
tokio = { version = "1", default-features = false, features = [
"rt",
"io-util",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }

View file

@ -13,7 +13,6 @@ use async_compression::{
BrotliDecoder, BrotliEncoder, GzipDecoder, GzipEncoder, ZlibDecoder, ZlibEncoder,
},
Level::Best as CompressionQuality,
Level::Precise as PreciseCompressionQuality,
};
/**
@ -117,10 +116,9 @@ impl<'lua> FromLua<'lua> for CompressDecompressFormat {
Errors when the compression fails.
*/
pub async fn compress(
pub async fn compress<'lua>(
source: impl AsRef<[u8]>,
format: CompressDecompressFormat,
level: Option<i32>,
) -> LuaResult<Vec<u8>> {
if let CompressDecompressFormat::LZ4 = format {
let source = source.as_ref().to_vec();
@ -132,22 +130,18 @@ pub async fn compress(
let mut bytes = Vec::new();
let reader = BufReader::new(source.as_ref());
let compression_quality = match level {
Some(l) => PreciseCompressionQuality(l),
None => CompressionQuality,
};
match format {
CompressDecompressFormat::Brotli => {
let mut encoder = BrotliEncoder::with_quality(reader, compression_quality);
let mut encoder = BrotliEncoder::with_quality(reader, CompressionQuality);
copy(&mut encoder, &mut bytes).await?;
}
CompressDecompressFormat::GZip => {
let mut encoder = GzipEncoder::with_quality(reader, compression_quality);
let mut encoder = GzipEncoder::with_quality(reader, CompressionQuality);
copy(&mut encoder, &mut bytes).await?;
}
CompressDecompressFormat::ZLib => {
let mut encoder = ZlibEncoder::with_quality(reader, compression_quality);
let mut encoder = ZlibEncoder::with_quality(reader, CompressionQuality);
copy(&mut encoder, &mut bytes).await?;
}
CompressDecompressFormat::LZ4 => unreachable!(),
@ -163,7 +157,7 @@ pub async fn compress(
Errors when the decompression fails.
*/
pub async fn decompress(
pub async fn decompress<'lua>(
source: impl AsRef<[u8]>,
format: CompressDecompressFormat,
) -> LuaResult<Vec<u8>> {

View file

@ -1,260 +0,0 @@
use std::fmt::Write;
use bstr::BString;
use md5::Md5;
use mlua::prelude::*;
use blake3::Hasher as Blake3;
use sha1::Sha1;
use sha2::{Sha224, Sha256, Sha384, Sha512};
use sha3::{Sha3_224, Sha3_256, Sha3_384, Sha3_512};
pub struct HashOptions {
algorithm: HashAlgorithm,
message: BString,
secret: Option<BString>,
// seed: Option<BString>,
}
#[derive(Debug, Clone, Copy)]
enum HashAlgorithm {
Md5,
Sha1,
// SHA-2 variants
Sha2_224,
Sha2_256,
Sha2_384,
Sha2_512,
// SHA-3 variants
Sha3_224,
Sha3_256,
Sha3_384,
Sha3_512,
// Blake3
Blake3,
}
impl HashAlgorithm {
pub const ALL: [Self; 11] = [
Self::Md5,
Self::Sha1,
Self::Sha2_224,
Self::Sha2_256,
Self::Sha2_384,
Self::Sha2_512,
Self::Sha3_224,
Self::Sha3_256,
Self::Sha3_384,
Self::Sha3_512,
Self::Blake3,
];
pub const fn name(self) -> &'static str {
match self {
Self::Md5 => "md5",
Self::Sha1 => "sha1",
Self::Sha2_224 => "sha224",
Self::Sha2_256 => "sha256",
Self::Sha2_384 => "sha384",
Self::Sha2_512 => "sha512",
Self::Sha3_224 => "sha3-224",
Self::Sha3_256 => "sha3-256",
Self::Sha3_384 => "sha3-384",
Self::Sha3_512 => "sha3-512",
Self::Blake3 => "blake3",
}
}
}
impl HashOptions {
/**
Computes the hash for the `message` using whatever `algorithm` is
contained within this struct and returns it as a string of hex digits.
*/
#[inline]
#[must_use = "hashing a message is useless without using the resulting hash"]
pub fn hash(self) -> String {
use digest::Digest;
let message = self.message;
let bytes = match self.algorithm {
HashAlgorithm::Md5 => Md5::digest(message).to_vec(),
HashAlgorithm::Sha1 => Sha1::digest(message).to_vec(),
HashAlgorithm::Sha2_224 => Sha224::digest(message).to_vec(),
HashAlgorithm::Sha2_256 => Sha256::digest(message).to_vec(),
HashAlgorithm::Sha2_384 => Sha384::digest(message).to_vec(),
HashAlgorithm::Sha2_512 => Sha512::digest(message).to_vec(),
HashAlgorithm::Sha3_224 => Sha3_224::digest(message).to_vec(),
HashAlgorithm::Sha3_256 => Sha3_256::digest(message).to_vec(),
HashAlgorithm::Sha3_384 => Sha3_384::digest(message).to_vec(),
HashAlgorithm::Sha3_512 => Sha3_512::digest(message).to_vec(),
HashAlgorithm::Blake3 => Blake3::digest(message).to_vec(),
};
// We don't want to return raw binary data generally, since that's not
// what most people want a hash for. So we have to make a hex string.
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
})
}
/**
Computes the HMAC for the `message` using whatever `algorithm` and
`secret` are contained within this struct. The computed value is
returned as a string of hex digits.
# Errors
If the `secret` is not provided or is otherwise invalid.
*/
#[inline]
pub fn hmac(self) -> LuaResult<String> {
use hmac::{Hmac, Mac, SimpleHmac};
let secret = self
.secret
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "string or buffer",
message: Some("Argument #3 missing or nil".to_string()),
})?;
/*
These macros exist to remove what would ultimately be dozens of
repeating lines. Essentially, there's several step to processing
HMacs, which expands into the 3 lines you see below. However,
the Hmac struct is specialized towards eager block-based processes.
In order to support anything else, like blake3, there's a second
type named `SimpleHmac`. This results in duplicate macros like
there are below.
*/
macro_rules! hmac {
($Type:ty) => {{
let mut mac: Hmac<$Type> = Hmac::new_from_slice(&secret).into_lua_err()?;
mac.update(&self.message);
mac.finalize().into_bytes().to_vec()
}};
}
macro_rules! hmac_no_blocks {
($Type:ty) => {{
let mut mac: SimpleHmac<$Type> =
SimpleHmac::new_from_slice(&secret).into_lua_err()?;
mac.update(&self.message);
mac.finalize().into_bytes().to_vec()
}};
}
let bytes = match self.algorithm {
HashAlgorithm::Md5 => hmac!(Md5),
HashAlgorithm::Sha1 => hmac!(Sha1),
HashAlgorithm::Sha2_224 => hmac!(Sha224),
HashAlgorithm::Sha2_256 => hmac!(Sha256),
HashAlgorithm::Sha2_384 => hmac!(Sha384),
HashAlgorithm::Sha2_512 => hmac!(Sha512),
HashAlgorithm::Sha3_224 => hmac!(Sha3_224),
HashAlgorithm::Sha3_256 => hmac!(Sha3_256),
HashAlgorithm::Sha3_384 => hmac!(Sha3_384),
HashAlgorithm::Sha3_512 => hmac!(Sha3_512),
HashAlgorithm::Blake3 => hmac_no_blocks!(Blake3),
};
Ok(bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
}))
}
}
impl<'lua> FromLua<'lua> for HashAlgorithm {
fn from_lua(value: LuaValue<'lua>, _lua: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::String(str) = value {
/*
Casing tends to vary for algorithms, so rather than force
people to remember it we'll just accept any casing.
*/
let str = str.to_str()?.to_ascii_lowercase();
match str.as_str() {
"md5" => Ok(Self::Md5),
"sha1" => Ok(Self::Sha1),
"sha2-224" | "sha2_224" | "sha224" => Ok(Self::Sha2_224),
"sha2-256" | "sha2_256" | "sha256" => Ok(Self::Sha2_256),
"sha2-384" | "sha2_384" | "sha384" => Ok(Self::Sha2_384),
"sha2-512" | "sha2_512" | "sha512" => Ok(Self::Sha2_512),
"sha3-224" | "sha3_224" => Ok(Self::Sha3_224),
"sha3-256" | "sha3_256" => Ok(Self::Sha3_256),
"sha3-384" | "sha3_384" => Ok(Self::Sha3_384),
"sha3-512" | "sha3_512" => Ok(Self::Sha3_512),
"blake3" => Ok(Self::Blake3),
_ => Err(LuaError::FromLuaConversionError {
from: "string",
to: "HashAlgorithm",
message: Some(format!(
"Invalid hashing algorithm '{str}', valid kinds are:\n{}",
HashAlgorithm::ALL
.into_iter()
.map(HashAlgorithm::name)
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "HashAlgorithm",
message: None,
})
}
}
}
impl<'lua> FromLuaMulti<'lua> for HashOptions {
fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
let algorithm = values
.pop_front()
.map(|value| HashAlgorithm::from_lua(value, lua))
.transpose()?
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "HashAlgorithm",
message: Some("Argument #1 missing or nil".to_string()),
})?;
let message = values
.pop_front()
.map(|value| BString::from_lua(value, lua))
.transpose()?
.ok_or_else(|| LuaError::FromLuaConversionError {
from: "nil",
to: "string or buffer",
message: Some("Argument #2 missing or nil".to_string()),
})?;
let secret = values
.pop_front()
.map(|value| BString::from_lua(value, lua))
.transpose()?;
// let seed = values
// .pop_front()
// .map(|value| BString::from_lua(value, lua))
// .transpose()?;
Ok(HashOptions {
algorithm,
message,
secret,
// seed,
})
}
}

View file

@ -7,11 +7,9 @@ use lune_utils::TableBuilder;
mod compress_decompress;
mod encode_decode;
mod hash;
pub use self::compress_decompress::{compress, decompress, CompressDecompressFormat};
pub use self::encode_decode::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
pub use self::hash::HashOptions;
/**
Creates the `serde` standard library module.
@ -26,8 +24,6 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.with_function("decode", serde_decode)?
.with_async_function("compress", serde_compress)?
.with_async_function("decompress", serde_decompress)?
.with_function("hash", hash_message)?
.with_function("hmac", hmac_message)?
.build_readonly()
}
@ -46,9 +42,9 @@ fn serde_decode(lua: &Lua, (format, bs): (EncodeDecodeFormat, BString)) -> LuaRe
async fn serde_compress(
lua: &Lua,
(format, bs, level): (CompressDecompressFormat, BString, Option<i32>),
(format, bs): (CompressDecompressFormat, BString),
) -> LuaResult<LuaString> {
let bytes = compress(bs, format, level).await?;
let bytes = compress(bs, format).await?;
lua.create_string(bytes)
}
@ -59,11 +55,3 @@ async fn serde_decompress(
let bytes = decompress(bs, format).await?;
lua.create_string(bytes)
}
fn hash_message(lua: &Lua, options: HashOptions) -> LuaResult<LuaString> {
lua.create_string(options.hash())
}
fn hmac_message(lua: &Lua, options: HashOptions) -> LuaResult<LuaString> {
lua.create_string(options.hmac()?)
}

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-stdio"
version = "0.1.2"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -14,12 +14,12 @@ workspace = true
[dependencies]
dialoguer = "0.11"
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
mlua = { version = "0.9.7", features = ["luau"] }
mlua-luau-scheduler = "0.0.2"
tokio = { version = "1", default-features = false, features = [
"io-std",
"io-util",
] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }

View file

@ -194,14 +194,14 @@ pub fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
prompt = prompt.default(b);
};
let result = prompt
.with_prompt(options.text.expect("Missing text in prompt options"))
.with_prompt(&options.text.expect("Missing text in prompt options"))
.interact()
.into_lua_err()?;
Ok(PromptResult::Boolean(result))
}
PromptKind::Select => {
let chosen = Select::with_theme(&theme)
.with_prompt(options.text.unwrap_or_default())
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()
.into_lua_err()?;
@ -212,7 +212,7 @@ pub fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
}
PromptKind::MultiSelect => {
let chosen = MultiSelect::with_theme(&theme)
.with_prompt(options.text.unwrap_or_default())
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()
.into_lua_err()?;

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std-task"
version = "0.1.2"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,9 +13,9 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
mlua = { version = "0.9.7", features = ["luau"] }
mlua-luau-scheduler = "0.0.2"
tokio = { version = "1", default-features = false, features = ["time"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-utils = { version = "0.1.0", path = "../lune-utils" }

View file

@ -33,6 +33,12 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.set_environment(task_delay_env)
.into_function()?;
// Overwrite resume & wrap functions on the coroutine global
// with ones that are compatible with our scheduler
let co = lua.globals().get::<_, LuaTable>("coroutine")?;
co.set("resume", fns.resume.clone())?;
co.set("wrap", fns.wrap.clone())?;
TableBuilder::new(lua)?
.with_value("cancel", fns.cancel)?
.with_value("defer", fns.defer)?

View file

@ -1,6 +1,6 @@
[package]
name = "lune-std"
version = "0.1.5"
version = "0.1.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -38,22 +38,22 @@ stdio = ["dep:lune-std-stdio"]
task = ["dep:lune-std-task"]
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
mlua = { version = "0.9.7", features = ["luau"] }
mlua-luau-scheduler = "0.0.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", default-features = false, features = ["fs", "sync"] }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-utils = { version = "0.1.1", path = "../lune-utils" }
lune-std-datetime = { optional = true, version = "0.1.3", path = "../lune-std-datetime" }
lune-std-fs = { optional = true, version = "0.1.2", path = "../lune-std-fs" }
lune-std-luau = { optional = true, version = "0.1.2", path = "../lune-std-luau" }
lune-std-net = { optional = true, version = "0.1.2", path = "../lune-std-net" }
lune-std-process = { optional = true, version = "0.1.3", path = "../lune-std-process" }
lune-std-regex = { optional = true, version = "0.1.2", path = "../lune-std-regex" }
lune-std-roblox = { optional = true, version = "0.1.4", path = "../lune-std-roblox" }
lune-std-serde = { optional = true, version = "0.1.2", path = "../lune-std-serde" }
lune-std-stdio = { optional = true, version = "0.1.2", path = "../lune-std-stdio" }
lune-std-task = { optional = true, version = "0.1.2", path = "../lune-std-task" }
lune-std-datetime = { optional = true, version = "0.1.1", path = "../lune-std-datetime" }
lune-std-fs = { optional = true, version = "0.1.0", path = "../lune-std-fs" }
lune-std-luau = { optional = true, version = "0.1.0", path = "../lune-std-luau" }
lune-std-net = { optional = true, version = "0.1.0", path = "../lune-std-net" }
lune-std-process = { optional = true, version = "0.1.1", path = "../lune-std-process" }
lune-std-regex = { optional = true, version = "0.1.0", path = "../lune-std-regex" }
lune-std-roblox = { optional = true, version = "0.1.1", path = "../lune-std-roblox" }
lune-std-serde = { optional = true, version = "0.1.0", path = "../lune-std-serde" }
lune-std-stdio = { optional = true, version = "0.1.0", path = "../lune-std-stdio" }
lune-std-task = { optional = true, version = "0.1.0", path = "../lune-std-task" }

View file

@ -150,9 +150,9 @@ impl RequireContext {
self.get_from_cache(lua, abs_path.as_ref())
}
async fn load(
async fn load<'lua>(
&self,
lua: &Lua,
lua: &'lua Lua,
abs_path: impl AsRef<Path>,
rel_path: impl AsRef<Path>,
) -> LuaResult<LuaRegistryKey> {

View file

@ -1,6 +1,6 @@
[package]
name = "lune-utils"
version = "0.1.3"
version = "0.1.1"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -13,7 +13,7 @@ path = "src/lib.rs"
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau", "async"] }
mlua = { version = "0.9.7", features = ["luau", "async"] }
tokio = { version = "1", default-features = false, features = ["fs"] }
@ -22,5 +22,3 @@ dunce = "1.0"
once_cell = "1.17"
path-clean = "1.0"
pathdiff = "0.2"
parking_lot = "0.12.3"
semver = "1.0"

View file

@ -26,11 +26,6 @@ static STYLED_STACK_END: Lazy<String> = Lazy::new(|| {
)
});
// NOTE: We indent using 4 spaces instead of tabs since
// these errors are most likely to be displayed in a terminal
// or some kind of live output - and tabs don't work well there
const STACK_TRACE_INDENT: &str = " ";
/**
Error components parsed from a [`LuaError`].
@ -91,7 +86,7 @@ impl fmt::Display for ErrorComponents {
let trace = self.trace.as_ref().unwrap();
writeln!(f, "{}", *STYLED_STACK_BEGIN)?;
for line in trace.lines() {
writeln!(f, "{STACK_TRACE_INDENT}{line}")?;
writeln!(f, "\t{line}")?;
}
writeln!(f, "{}", *STYLED_STACK_END)?;
}
@ -129,7 +124,7 @@ impl From<LuaError> for ErrorComponents {
}
// We will then try to extract any stack trace
let mut trace = if let LuaError::CallbackError {
let trace = if let LuaError::CallbackError {
ref traceback,
ref cause,
} = *error
@ -152,45 +147,6 @@ impl From<LuaError> for ErrorComponents {
None
};
// Sometimes, we can get duplicate stack trace lines that only
// mention "[C]", without a function name or path, and these can
// be safely ignored / removed if the following line has more info
if let Some(trace) = &mut trace {
let lines = trace.lines_mut();
loop {
let first_is_c_and_empty = lines
.first()
.is_some_and(|line| line.source().is_c() && line.is_empty());
let second_is_c_and_nonempty = lines
.get(1)
.is_some_and(|line| line.source().is_c() && !line.is_empty());
if first_is_c_and_empty && second_is_c_and_nonempty {
lines.remove(0);
} else {
break;
}
}
}
// Finally, we do some light postprocessing to remove duplicate
// information, such as the location prefix in the error message
if let Some(message) = messages.last_mut() {
if let Some(line) = trace
.iter()
.flat_map(StackTrace::lines)
.find(|line| line.source().is_lua())
{
let location_prefix = format!(
"[string \"{}\"]:{}:",
line.path().unwrap(),
line.line_number().unwrap()
);
if message.starts_with(&location_prefix) {
*message = message[location_prefix.len()..].trim().to_string();
}
}
}
ErrorComponents { messages, trace }
}
}

View file

@ -39,24 +39,6 @@ pub enum StackTraceSource {
Lua,
}
impl StackTraceSource {
/**
Returns `true` if the error originated from a C / Rust function, `false` otherwise.
*/
#[must_use]
pub const fn is_c(self) -> bool {
matches!(self, Self::C)
}
/**
Returns `true` if the error originated from a Lua (user) function, `false` otherwise.
*/
#[must_use]
pub const fn is_lua(self) -> bool {
matches!(self, Self::Lua)
}
}
/**
Stack trace line parsed from a [`LuaError`].
*/
@ -100,20 +82,6 @@ impl StackTraceLine {
pub fn function_name(&self) -> Option<&str> {
self.function_name.as_deref()
}
/**
Returns `true` if the stack trace line contains no "useful" information, `false` otherwise.
Useful information is determined as one of:
- A path
- A line number
- A function name
*/
#[must_use]
pub const fn is_empty(&self) -> bool {
self.path.is_none() && self.line_number.is_none() && self.function_name.is_none()
}
}
impl FromStr for StackTraceLine {
@ -177,14 +145,6 @@ impl StackTrace {
pub fn lines(&self) -> &[StackTraceLine] {
&self.lines
}
/**
Returns the individual stack trace lines, mutably.
*/
#[must_use]
pub fn lines_mut(&mut self) -> &mut Vec<StackTraceLine> {
&mut self.lines
}
}
impl FromStr for StackTrace {

View file

@ -2,7 +2,7 @@ use mlua::prelude::*;
use crate::fmt::ErrorComponents;
fn new_lua_runtime_error() -> LuaResult<()> {
fn new_lua_result() -> LuaResult<()> {
let lua = Lua::new();
lua.globals()
@ -17,34 +17,13 @@ fn new_lua_runtime_error() -> LuaResult<()> {
lua.load("f()").set_name("chunk_name").eval()
}
fn new_lua_script_error() -> LuaResult<()> {
let lua = Lua::new();
lua.load(
"local function inner()\
\n error(\"oh no, a script error\")\
\nend\
\n\
\nlocal function outer()\
\n inner()\
\nend\
\n\
\nouter()\
",
)
.set_name("chunk_name")
.eval()
}
// Tests for error context stack
mod context {
use super::*;
#[test]
fn preserves_original() {
let lua_error = new_lua_runtime_error()
.context("additional context")
.unwrap_err();
let lua_error = new_lua_result().context("additional context").unwrap_err();
let components = ErrorComponents::from(lua_error);
assert_eq!(components.messages()[0], "additional context");
@ -55,7 +34,7 @@ mod context {
fn preserves_levels() {
// NOTE: The behavior in mlua is to preserve a single level of context
// and not all levels (context gets replaced on each call to `context`)
let lua_error = new_lua_runtime_error()
let lua_error = new_lua_result()
.context("level 1")
.context("level 2")
.context("level 3")
@ -75,7 +54,7 @@ mod error_components {
#[test]
fn message() {
let lua_error = new_lua_runtime_error().unwrap_err();
let lua_error = new_lua_result().unwrap_err();
let components = ErrorComponents::from(lua_error);
assert_eq!(components.messages()[0], "oh no, a runtime error");
@ -83,7 +62,7 @@ mod error_components {
#[test]
fn stack_begin_end() {
let lua_error = new_lua_runtime_error().unwrap_err();
let lua_error = new_lua_result().unwrap_err();
let formatted = format!("{}", ErrorComponents::from(lua_error));
assert!(formatted.contains("Stack Begin"));
@ -92,7 +71,7 @@ mod error_components {
#[test]
fn stack_lines() {
let lua_error = new_lua_runtime_error().unwrap_err();
let lua_error = new_lua_result().unwrap_err();
let components = ErrorComponents::from(lua_error);
let mut lines = components.trace().unwrap().lines().iter();
@ -104,47 +83,3 @@ mod error_components {
assert_eq!(line_2, "Script 'chunk_name', Line 1");
}
}
// Tests for general formatting
mod general {
use super::*;
#[test]
fn message_does_not_contain_location() {
let lua_error = new_lua_script_error().unwrap_err();
let components = ErrorComponents::from(lua_error);
let trace = components.trace().unwrap();
let first_message = components.messages().first().unwrap();
let first_lua_stack_line = trace
.lines()
.iter()
.find(|line| line.source().is_lua())
.unwrap();
let location_prefix = format!(
"[string \"{}\"]:{}:",
first_lua_stack_line.path().unwrap(),
first_lua_stack_line.line_number().unwrap()
);
assert!(!first_message.starts_with(&location_prefix));
}
#[test]
fn no_redundant_c_mentions() {
let lua_error = new_lua_script_error().unwrap_err();
let components = ErrorComponents::from(lua_error);
let trace = components.trace().unwrap();
let c_stack_lines = trace
.lines()
.iter()
.filter(|line| line.source().is_c())
.collect::<Vec<_>>();
assert_eq!(c_stack_lines.len(), 1); // Just the "error" call
}
}

View file

@ -1,12 +1,7 @@
use mlua::prelude::*;
use crate::fmt::ErrorComponents;
use super::{
metamethods::{
call_table_tostring_metamethod, call_userdata_tostring_metamethod,
get_table_type_metavalue, get_userdata_type_metavalue,
},
metamethods::{call_table_tostring_metamethod, call_userdata_tostring_metamethod},
style::{COLOR_CYAN, COLOR_GREEN, COLOR_MAGENTA, COLOR_YELLOW},
};
@ -61,39 +56,19 @@ pub(crate) fn format_value_styled(value: &LuaValue, prefer_plain: bool) -> Strin
LuaValue::Function(_) => COLOR_MAGENTA.apply_to("<function>").to_string(),
LuaValue::LightUserData(_) => COLOR_MAGENTA.apply_to("<pointer>").to_string(),
LuaValue::UserData(u) => {
let formatted = format_typename_and_tostringed(
"userdata",
get_userdata_type_metavalue(u),
call_userdata_tostring_metamethod(u),
);
COLOR_MAGENTA.apply_to(formatted).to_string()
if let Some(s) = call_userdata_tostring_metamethod(u) {
s
} else {
COLOR_MAGENTA.apply_to("<userdata>").to_string()
}
}
LuaValue::Table(t) => {
let formatted = format_typename_and_tostringed(
"table",
get_table_type_metavalue(t),
call_table_tostring_metamethod(t),
);
COLOR_MAGENTA.apply_to(formatted).to_string()
if let Some(s) = call_table_tostring_metamethod(t) {
s
} else {
COLOR_MAGENTA.apply_to("<table>").to_string()
}
}
LuaValue::Error(e) => COLOR_MAGENTA
.apply_to(format!(
"<LuaError(\n{})>",
ErrorComponents::from(e.clone())
))
.to_string(),
}
}
fn format_typename_and_tostringed(
fallback: &'static str,
typename: Option<String>,
tostringed: Option<String>,
) -> String {
match (typename, tostringed) {
(Some(typename), Some(tostringed)) => format!("<{typename}({tostringed})>"),
(Some(typename), None) => format!("<{typename}>"),
(None, Some(tostringed)) => format!("<{tostringed}>"),
(None, None) => format!("<{fallback}>"),
_ => COLOR_MAGENTA.apply_to("<?>").to_string(),
}
}

View file

@ -1,37 +1,29 @@
use mlua::prelude::*;
pub fn get_table_type_metavalue<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
let s = tab
.get_metatable()?
.get::<_, LuaString>(LuaMetaMethod::Type.name())
.ok()?;
let s = s.to_str().ok()?;
Some(s.to_string())
}
pub fn get_userdata_type_metavalue<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
let s = tab
.get_metatable()
.ok()?
.get::<LuaString>(LuaMetaMethod::Type.name())
.ok()?;
let s = s.to_str().ok()?;
Some(s.to_string())
}
pub fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
tab.get_metatable()?
.get::<_, LuaFunction>(LuaMetaMethod::ToString.name())
.ok()?
.call(tab)
.ok()
let f = match tab.get_metatable() {
None => None,
Some(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) {
Ok(method) => Some(method),
Err(_) => None,
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
}
pub fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
tab.get_metatable()
.ok()?
.get::<LuaFunction>(LuaMetaMethod::ToString.name())
.ok()?
.call(tab)
.ok()
let f = match tab.get_metatable() {
Err(_) => None,
Ok(meta) => match meta.get::<LuaFunction>(LuaMetaMethod::ToString.name()) {
Ok(method) => Some(method),
Err(_) => None,
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
}

View file

@ -1,9 +1,11 @@
use std::{collections::HashSet, sync::Arc};
use std::{
collections::HashSet,
sync::{Arc, Mutex},
};
use console::{colors_enabled as get_colors_enabled, set_colors_enabled};
use mlua::prelude::*;
use once_cell::sync::Lazy;
use parking_lot::ReentrantMutex;
mod basic;
mod config;
@ -18,7 +20,7 @@ pub use self::config::ValueFormatConfig;
// NOTE: Since the setting for colors being enabled is global,
// and these functions may be called in parallel, we use this global
// lock to make sure that we don't mess up the colors for other threads.
static COLORS_LOCK: Lazy<Arc<ReentrantMutex<()>>> = Lazy::new(|| Arc::new(ReentrantMutex::new(())));
static COLORS_LOCK: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(())));
/**
Formats a Lua value into a pretty string using the given config.
@ -26,7 +28,7 @@ static COLORS_LOCK: Lazy<Arc<ReentrantMutex<()>>> = Lazy::new(|| Arc::new(Reentr
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> String {
let _guard = COLORS_LOCK.lock();
let _guard = COLORS_LOCK.lock().unwrap();
let were_colors_enabled = get_colors_enabled();
set_colors_enabled(were_colors_enabled && config.colors_enabled);
@ -46,7 +48,7 @@ pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> Stri
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn pretty_format_multi_value(values: &LuaMultiValue, config: &ValueFormatConfig) -> String {
let _guard = COLORS_LOCK.lock();
let _guard = COLORS_LOCK.lock().unwrap();
let were_colors_enabled = get_colors_enabled();
set_colors_enabled(were_colors_enabled && config.colors_enabled);

View file

@ -4,7 +4,6 @@ use std::fmt::{self, Write as _};
use mlua::prelude::*;
use super::metamethods::{call_table_tostring_metamethod, get_table_type_metavalue};
use super::{
basic::{format_value_styled, lua_value_as_plain_string_key},
config::ValueFormatConfig,
@ -47,12 +46,7 @@ pub(crate) fn format_value_recursive(
let mut buffer = String::new();
if let LuaValue::Table(ref t) = value {
if let Some(formatted) = format_typename_and_tostringed(
get_table_type_metavalue(t),
call_table_tostring_metamethod(t),
) {
write!(buffer, "{formatted}")?;
} else if depth >= config.max_depth {
if depth >= config.max_depth {
write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?;
} else if !visited.insert(LuaValueId::from(t)) {
write!(buffer, "{}", STYLE_DIM.apply_to("{ recursive }"))?;
@ -170,15 +164,3 @@ fn format_table(
})
.collect()
}
fn format_typename_and_tostringed(
typename: Option<String>,
tostringed: Option<String>,
) -> Option<String> {
match (typename, tostringed) {
(Some(typename), Some(tostringed)) => Some(format!("<{typename}({tostringed})>")),
(Some(typename), None) => Some(format!("<{typename}>")),
(None, Some(tostringed)) => Some(tostringed),
(None, None) => None,
}
}

View file

@ -1,30 +0,0 @@
#[derive(Debug, Clone, Copy, Default)]
pub struct JitStatus(bool);
impl JitStatus {
#[must_use]
pub fn new(enabled: bool) -> Self {
Self(enabled)
}
pub fn set_status(&mut self, enabled: bool) {
self.0 = enabled;
}
#[must_use]
pub fn enabled(self) -> bool {
self.0
}
}
impl From<JitStatus> for bool {
fn from(val: JitStatus) -> Self {
val.enabled()
}
}
impl From<bool> for JitStatus {
fn from(val: bool) -> Self {
Self::new(val)
}
}

View file

@ -4,7 +4,6 @@ mod table_builder;
mod version_string;
pub mod fmt;
pub mod jit;
pub mod path;
pub use self::table_builder::TableBuilder;

View file

@ -2,7 +2,6 @@ use std::sync::Arc;
use mlua::prelude::*;
use once_cell::sync::Lazy;
use semver::Version;
static LUAU_VERSION: Lazy<Arc<String>> = Lazy::new(create_luau_version_string);
@ -21,10 +20,12 @@ pub fn get_version_string(lune_version: impl AsRef<str>) -> String {
let lune_version = lune_version.as_ref();
assert!(!lune_version.is_empty(), "Lune version string is empty");
match Version::parse(lune_version) {
Ok(semver) => format!("Lune {semver}+{}", *LUAU_VERSION),
Err(e) => panic!("Lune version string is not valid semver: {e}"),
}
assert!(
lune_version.chars().all(is_valid_version_char),
"Lune version string contains invalid characters"
);
format!("Lune {lune_version}+{}", *LUAU_VERSION)
}
fn create_luau_version_string() -> Arc<String> {

View file

@ -1,6 +1,6 @@
[package]
name = "lune"
version = "0.8.9"
version = "0.8.5"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
@ -50,8 +50,8 @@ cli = ["dep:clap", "dep:include_dir", "dep:rustyline", "dep:zip_next"]
workspace = true
[dependencies]
mlua = { version = "0.9.9", features = ["luau"] }
mlua-luau-scheduler = { version = "0.0.2", path = "../mlua-luau-scheduler" }
mlua = { version = "0.9.7", features = ["luau"] }
mlua-luau-scheduler = "0.0.2"
anyhow = "1.0"
console = "0.15"
@ -59,7 +59,6 @@ dialoguer = "0.11"
directories = "5.0"
futures-util = "0.3"
once_cell = "1.17"
self_cell = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
@ -71,9 +70,9 @@ reqwest = { version = "0.11", default-features = false, features = [
"rustls-tls",
] }
lune-std = { optional = true, version = "0.1.5", path = "../lune-std" }
lune-roblox = { optional = true, version = "0.1.4", path = "../lune-roblox" }
lune-utils = { version = "0.1.3", path = "../lune-utils" }
lune-std = { optional = true, version = "0.1.2", path = "../lune-std" }
lune-roblox = { optional = true, version = "0.1.1", path = "../lune-roblox" }
lune-utils = { version = "0.1.1", path = "../lune-utils" }
### CLI

View file

@ -1,4 +1,4 @@
use std::{env, process::ExitCode};
use std::process::ExitCode;
use anyhow::{Context, Result};
use clap::Parser;
@ -40,27 +40,17 @@ impl RunCommand {
(file_display_name, file_contents)
};
// Create a new lune runtime with all globals & run the script
let mut rt = Runtime::new()
// Create a new lune object with all globals & run the script
let result = Runtime::new()
.with_args(self.script_args)
// Enable JIT compilation unless it was requested to be disabled
.with_jit(
!matches!(
env::var("LUNE_LUAU_JIT").ok(),
Some(jit_enabled) if jit_enabled == "0" || jit_enabled == "false" || jit_enabled == "off"
)
);
let result = rt
.run(&script_display_name, strip_shebang(script_contents))
.await;
Ok(match result {
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
Ok((code, _)) => ExitCode::from(code),
Ok(code) => code,
})
}
}

View file

@ -64,8 +64,8 @@ pub fn discover_script_path(path: impl AsRef<str>, in_home_dir: bool) -> Result<
// NOTE: We use metadata directly here to try to
// avoid accessing the file path more than once
let file_meta = file_path.metadata();
let is_file = file_meta.as_ref().is_ok_and(Metadata::is_file);
let is_dir = file_meta.as_ref().is_ok_and(Metadata::is_dir);
let is_file = file_meta.as_ref().map_or(false, Metadata::is_file);
let is_dir = file_meta.as_ref().map_or(false, Metadata::is_dir);
let is_abs = file_path.is_absolute();
let ext = file_path.extension();
if is_file {

View file

@ -1,6 +1,7 @@
#![allow(clippy::missing_panics_doc)]
use std::{
process::ExitCode,
rc::Rc,
sync::{
atomic::{AtomicBool, Ordering},
@ -8,100 +9,15 @@ use std::{
},
};
use lune_utils::jit::JitStatus;
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
use self_cell::self_cell;
use mlua::Lua;
use mlua_luau_scheduler::Scheduler;
use super::{RuntimeError, RuntimeResult};
// NOTE: We need to use self_cell to create a self-referential
// struct storing both the Lua VM and the scheduler. The scheduler
// needs to be created at the same time so that we can also create
// and inject the scheduler functions which will be used across runs.
self_cell! {
struct RuntimeInner {
owner: Rc<Lua>,
#[covariant]
dependent: Scheduler,
}
}
impl RuntimeInner {
fn create() -> LuaResult<Self> {
let lua = Rc::new(Lua::new());
lua.set_app_data(Rc::downgrade(&lua));
lua.set_app_data(Vec::<String>::new());
Self::try_new(lua, |lua| {
let sched = Scheduler::new(lua);
let fns = Functions::new(lua)?;
// Overwrite some globals that are not compatible with our scheduler
let co = lua.globals().get::<_, LuaTable>("coroutine")?;
co.set("resume", fns.resume.clone())?;
co.set("wrap", fns.wrap.clone())?;
// Inject all the globals that are enabled
#[cfg(any(
feature = "std-datetime",
feature = "std-fs",
feature = "std-luau",
feature = "std-net",
feature = "std-process",
feature = "std-regex",
feature = "std-roblox",
feature = "std-serde",
feature = "std-stdio",
feature = "std-task",
))]
{
lune_std::set_global_version(lua, env!("CARGO_PKG_VERSION"));
lune_std::inject_globals(lua)?;
}
// Sandbox the Luau VM and make it go zooooooooom
lua.sandbox(true)?;
// _G table needs to be injected again after sandboxing,
// otherwise it will be read-only and completely unusable
#[cfg(any(
feature = "std-datetime",
feature = "std-fs",
feature = "std-luau",
feature = "std-net",
feature = "std-process",
feature = "std-regex",
feature = "std-roblox",
feature = "std-serde",
feature = "std-stdio",
feature = "std-task",
))]
{
let g_table = lune_std::LuneStandardGlobal::GTable;
lua.globals().set(g_table.name(), g_table.create(lua)?)?;
}
Ok(sched)
})
}
fn lua(&self) -> &Lua {
self.borrow_owner()
}
fn scheduler(&self) -> &Scheduler {
self.borrow_dependent()
}
}
/**
A Lune runtime.
*/
#[derive(Debug)]
pub struct Runtime {
inner: RuntimeInner,
jit_status: JitStatus,
lua: Rc<Lua>,
args: Vec<String>,
}
impl Runtime {
@ -113,9 +29,30 @@ impl Runtime {
#[must_use]
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let lua = Rc::new(Lua::new());
lua.set_app_data(Rc::downgrade(&lua));
lua.set_app_data(Vec::<String>::new());
#[cfg(any(
feature = "std-datetime",
feature = "std-fs",
feature = "std-luau",
feature = "std-net",
feature = "std-process",
feature = "std-regex",
feature = "std-roblox",
feature = "std-serde",
feature = "std-stdio",
feature = "std-task",
))]
{
lune_std::inject_globals(&lua).expect("Failed to inject globals");
}
Self {
inner: RuntimeInner::create().expect("Failed to create runtime"),
jit_status: JitStatus::default(),
lua,
args: Vec::new(),
}
}
@ -123,22 +60,12 @@ impl Runtime {
Sets arguments to give in `process.args` for Lune scripts.
*/
#[must_use]
pub fn with_args<A, S>(self, args: A) -> Self
pub fn with_args<V>(mut self, args: V) -> Self
where
A: IntoIterator<Item = S>,
S: Into<String>,
V: Into<Vec<String>>,
{
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
self.inner.lua().set_app_data(args);
self
}
/**
Enables or disables JIT compilation.
*/
#[must_use]
pub fn with_jit(mut self, jit_status: impl Into<JitStatus>) -> Self {
self.jit_status = jit_status.into();
self.args = args.into();
self.lua.set_app_data(self.args.clone());
self
}
@ -155,41 +82,35 @@ impl Runtime {
&mut self,
script_name: impl AsRef<str>,
script_contents: impl AsRef<[u8]>,
) -> RuntimeResult<(u8, Vec<LuaValue>)> {
let lua = self.inner.lua();
let sched = self.inner.scheduler();
) -> RuntimeResult<ExitCode> {
// Create a new scheduler for this run
let sched = Scheduler::new(&self.lua);
// Add error callback to format errors nicely + store status
let got_any_error = Arc::new(AtomicBool::new(false));
let got_any_inner = Arc::clone(&got_any_error);
self.inner.scheduler().set_error_callback(move |e| {
sched.set_error_callback(move |e| {
got_any_inner.store(true, Ordering::SeqCst);
eprintln!("{}", RuntimeError::from(e));
});
// Enable / disable the JIT as requested and store the current status as AppData
lua.set_app_data(self.jit_status);
lua.enable_jit(self.jit_status.enabled());
// Load our "main" thread
let main = lua
let main = self
.lua
.load(script_contents.as_ref())
.set_name(script_name.as_ref());
// Run it on our scheduler until it and any other spawned threads complete
let main_thread_id = sched.push_thread_back(main, ())?;
sched.push_thread_back(main, ())?;
sched.run().await;
let main_thread_res = match sched.get_thread_result(main_thread_id) {
Some(res) => res,
None => LuaValue::Nil.into_lua_multi(lua),
}?;
Ok((
sched
.get_exit_code()
.unwrap_or(u8::from(got_any_error.load(Ordering::SeqCst))),
main_thread_res.into_vec(),
))
// Return the exit code - default to FAILURE if we got any errors
Ok(sched.get_exit_code().unwrap_or({
if got_any_error.load(Ordering::SeqCst) {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}))
}
}

View file

@ -29,15 +29,16 @@ pub async fn run(patched_bin: impl AsRef<[u8]>) -> Result<ExitCode> {
let args = env::args().skip(1).collect::<Vec<_>>();
let meta = Metadata::from_bytes(patched_bin).expect("must be a standalone binary");
let mut rt = Runtime::new().with_args(args);
let result = rt.run("STANDALONE", meta.bytecode).await;
let result = Runtime::new()
.with_args(args)
.run("STANDALONE", meta.bytecode)
.await;
Ok(match result {
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
Ok((code, _)) => ExitCode::from(code),
Ok(code) => code,
})
}

View file

@ -31,21 +31,19 @@ macro_rules! create_tests {
// The rest of the test logic can continue as normal
let full_name = format!("{}/tests/{}.luau", workspace_dir.display(), $value);
let script = read_to_string(&full_name).await?;
let mut lune = Runtime::new()
.with_jit(true)
.with_args(
ARGS
.clone()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
);
let mut lune = Runtime::new().with_args(
ARGS
.clone()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
);
let script_name = full_name
.trim_end_matches(".luau")
.trim_end_matches(".lua")
.to_string();
let (exit_code, _) = lune.run(&script_name, &script).await?;
Ok(ExitCode::from(exit_code))
let exit_code = lune.run(&script_name, &script).await?;
Ok(exit_code)
}
)* }
}
@ -115,7 +113,6 @@ create_tests! {
luau_compile: "luau/compile",
luau_load: "luau/load",
luau_options: "luau/options",
luau_safeenv: "luau/safeenv",
}
#[cfg(feature = "std-net")]
@ -140,16 +137,13 @@ create_tests! {
process_cwd: "process/cwd",
process_env: "process/env",
process_exit: "process/exit",
process_exec_async: "process/exec/async",
process_exec_basic: "process/exec/basic",
process_exec_cwd: "process/exec/cwd",
process_exec_no_panic: "process/exec/no_panic",
process_exec_shell: "process/exec/shell",
process_exec_stdin: "process/exec/stdin",
process_exec_stdio: "process/exec/stdio",
process_spawn_non_blocking: "process/create/non_blocking",
process_spawn_status: "process/create/status",
process_spawn_stream: "process/create/stream",
process_spawn_async: "process/spawn/async",
process_spawn_basic: "process/spawn/basic",
process_spawn_cwd: "process/spawn/cwd",
process_spawn_no_panic: "process/spawn/no_panic",
process_spawn_shell: "process/spawn/shell",
process_spawn_stdin: "process/spawn/stdin",
process_spawn_stdio: "process/spawn/stdio",
}
#[cfg(feature = "std-regex")]
@ -167,7 +161,6 @@ create_tests! {
roblox_datatype_color3: "roblox/datatypes/Color3",
roblox_datatype_color_sequence: "roblox/datatypes/ColorSequence",
roblox_datatype_color_sequence_keypoint: "roblox/datatypes/ColorSequenceKeypoint",
roblox_datatype_content: "roblox/datatypes/Content",
roblox_datatype_enum: "roblox/datatypes/Enum",
roblox_datatype_faces: "roblox/datatypes/Faces",
roblox_datatype_font: "roblox/datatypes/Font",
@ -237,8 +230,6 @@ create_tests! {
serde_json_encode: "serde/json/encode",
serde_toml_decode: "serde/toml/decode",
serde_toml_encode: "serde/toml/encode",
serde_hashing_hash: "serde/hashing/hash",
serde_hashing_hmac: "serde/hashing/hmac",
}
#[cfg(feature = "std-stdio")]

View file

@ -1,67 +0,0 @@
[package]
name = "mlua-luau-scheduler"
version = "0.0.2"
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/lune-org/lune"
description = "Luau-based async scheduler, using mlua and async-executor"
readme = "README.md"
keywords = ["async", "luau", "scheduler"]
categories = ["async"]
[lib]
path = "src/lib.rs"
[lints]
workspace = true
[dependencies]
async-executor = "1.8"
blocking = "1.5"
concurrent-queue = "2.4"
derive_more = "0.99"
event-listener = "4.0"
futures-lite = "2.2"
rustc-hash = "1.1"
tracing = "0.1"
mlua = { version = "0.9.9", features = [
"luau",
"luau-jit",
"async",
"serialize",
] }
[dev-dependencies]
async-fs = "2.1"
async-io = "2.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-tracy = "0.11"
[[example]]
name = "basic_sleep"
test = true
[[example]]
name = "basic_spawn"
test = true
[[example]]
name = "callbacks"
test = true
[[example]]
name = "exit_code"
test = true
[[example]]
name = "lots_of_threads"
test = true
[[example]]
name = "scheduler_ordering"
test = true
[[example]]
name = "tracy"
test = false

View file

@ -1,78 +0,0 @@
<!-- markdownlint-disable MD033 -->
<!-- markdownlint-disable MD041 -->
# `mlua-luau-scheduler`
An async scheduler for Luau, using [`mlua`][mlua] and built on top of [`async-executor`][async-executor].
This crate is runtime-agnostic and is compatible with any async runtime, including [Tokio][tokio], [smol][smol], [async-std][async-std], and others. </br>
However, since many dependencies are shared with [smol][smol], depending on it over other runtimes may be preferred.
[async-executor]: https://crates.io/crates/async-executor
[async-std]: https://async.rs
[mlua]: https://crates.io/crates/mlua
[smol]: https://github.com/smol-rs/smol
[tokio]: https://tokio.rs
## Example Usage
### 1. Import dependencies
```rs
use std::time::{Duration, Instant};
use std::io::ErrorKind;
use async_io::{block_on, Timer};
use async_fs::read_to_string;
use mlua::prelude::*;
use mlua_luau_scheduler::*;
```
### 2. Set up Lua environment
```rs
let lua = Lua::new();
lua.globals().set(
"sleep",
lua.create_async_function(|_, duration: f64| async move {
let before = Instant::now();
let after = Timer::after(Duration::from_secs_f64(duration)).await;
Ok((after - before).as_secs_f64())
})?,
)?;
lua.globals().set(
"readFile",
lua.create_async_function(|lua, path: String| async move {
// Spawn background task that does not take up resources on the lua thread
// Normally, futures in mlua can not be shared across threads, but this can
let task = lua.spawn(async move {
match read_to_string(path).await {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
});
task.await.into_lua_err()
})?,
)?;
```
### 3. Set up scheduler, run threads
```rs
let sched = Scheduler::new(&lua)?;
// We can create multiple lua threads ...
let sleepThread = lua.load("sleep(0.1)");
let fileThread = lua.load("readFile(\"Cargo.toml\")");
// ... spawn them both onto the scheduler ...
sched.push_thread_front(sleepThread, ());
sched.push_thread_front(fileThread, ());
// ... and run until they finish
block_on(sched.run());
```

View file

@ -1,45 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::time::{Duration, Instant};
use async_io::{block_on, Timer};
use mlua::prelude::*;
use mlua_luau_scheduler::Scheduler;
const MAIN_SCRIPT: &str = include_str!("./lua/basic_sleep.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
lua.globals().set(
"sleep",
lua.create_async_function(|_, duration: f64| async move {
let before = Instant::now();
let after = Timer::after(Duration::from_secs_f64(duration)).await;
Ok((after - before).as_secs_f64())
})?,
)?;
// Load the main script into a scheduler
let sched = Scheduler::new(&lua);
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
Ok(())
}
#[test]
fn test_basic_sleep() -> LuaResult<()> {
main()
}

View file

@ -1,64 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::io::ErrorKind;
use async_fs::read_to_string;
use async_io::block_on;
use mlua::prelude::*;
use mlua_luau_scheduler::{LuaSpawnExt, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/basic_spawn.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
lua.globals().set(
"readFile",
lua.create_async_function(|lua, path: String| async move {
// Spawn background task that does not take up resources on the Lua thread
let task = lua.spawn(async move {
match read_to_string(path).await {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(None),
Err(e) => Err(e),
}
});
// Wait for it to complete
let result = task.await.into_lua_err();
// We can also spawn local tasks that do take up resources
// on the Lua thread, but that do not have the Send bound
if result.is_ok() {
lua.spawn_local(async move {
println!("File read successfully!");
});
}
result
})?,
)?;
// Load the main script into a scheduler
let sched = Scheduler::new(&lua);
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
Ok(())
}
#[test]
fn test_basic_spawn() -> LuaResult<()> {
main()
}

View file

@ -1,48 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::cargo_common_metadata)]
use mlua::prelude::*;
use mlua_luau_scheduler::Scheduler;
use async_io::block_on;
const MAIN_SCRIPT: &str = include_str!("./lua/callbacks.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
// Create a new scheduler with custom callbacks
let sched = Scheduler::new(&lua);
sched.set_error_callback(|e| {
println!(
"Captured error from Lua!\n{}\n{e}\n{}",
"-".repeat(15),
"-".repeat(15)
);
});
// Load the main script into the scheduler, and keep track of the thread we spawn
let main = lua.load(MAIN_SCRIPT);
let id = sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
// We should have gotten the error back from our script
assert!(sched.get_thread_result(id).unwrap().is_err());
Ok(())
}
#[test]
fn test_callbacks() -> LuaResult<()> {
main()
}

View file

@ -1,43 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::cargo_common_metadata)]
use async_io::block_on;
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/exit_code.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
let sched = Scheduler::new(&lua);
let fns = Functions::new(&lua)?;
lua.globals().set("exit", fns.exit)?;
// Load the main script into the scheduler
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
// Verify that we got a correct exit code
let code = sched.get_exit_code().unwrap_or_default();
assert_eq!(code, 1);
Ok(())
}
#[test]
fn test_exit_code() -> LuaResult<()> {
main()
}

View file

@ -1,51 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::time::Duration;
use async_io::{block_on, Timer};
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/lots_of_threads.luau");
const ONE_NANOSECOND: Duration = Duration::from_nanos(1);
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
let sched = Scheduler::new(&lua);
let fns = Functions::new(&lua)?;
lua.globals().set("spawn", fns.spawn)?;
lua.globals().set(
"sleep",
lua.create_async_function(|_, ()| async move {
// Obviously we can't sleep for a single nanosecond since
// this uses OS scheduling under the hood, but we can try
Timer::after(ONE_NANOSECOND).await;
Ok(())
})?,
)?;
// Load the main script into the scheduler
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
Ok(())
}
#[test]
fn test_lots_of_threads() -> LuaResult<()> {
main()
}

View file

@ -1,13 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
print("Sleeping for 3 seconds...")
sleep(1)
print("1 second passed")
sleep(1)
print("2 seconds passed")
sleep(1)
print("3 seconds passed")

View file

@ -1,17 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
local _, err = pcall(function()
local file = readFile("Cargo.toml")
if file ~= nil then
print("Cargo.toml found!")
print("Contents:")
print(file)
else
print("Cargo.toml not found!")
end
end)
if err ~= nil then
print("Error while reading file: " .. err)
end

View file

@ -1,4 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
error("Oh no! Something went very very wrong!")

View file

@ -1,8 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
print("Setting exit code manually")
exit(1)
error("unreachable")

View file

@ -1,29 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
local NUM_BATCHES = 10
local NUM_THREADS = 100_000
print(`Spawning {NUM_BATCHES * NUM_THREADS} threads split into {NUM_BATCHES} batches\n`)
local before = os.clock()
for i = 1, NUM_BATCHES do
print(`Batch {i} of {NUM_BATCHES}`)
local thread = coroutine.running()
local counter = 0
for j = 1, NUM_THREADS do
spawn(function()
sleep(0.1)
counter += 1
if counter == NUM_THREADS then
spawn(thread)
end
end)
end
coroutine.yield()
end
local after = os.clock()
print(`\nSpawned {NUM_BATCHES * NUM_THREADS} sleeping threads in {after - before}s`)

View file

@ -1,34 +0,0 @@
--!nocheck
--!nolint UnknownGlobal
local nums = {}
local function insert(n: number)
table.insert(nums, n)
print(n)
end
insert(1)
-- Defer will run at the end of the resumption cycle, but without yielding
defer(function()
insert(5)
end)
-- Spawn will instantly run up until the first yield, and must then be resumed manually ...
spawn(function()
insert(2)
coroutine.yield()
error("unreachable code")
end)
-- ... unless calling functions created using `lua.create_async_function(...)`,
-- which will resume their calling thread with their result automatically
spawn(function()
insert(3)
sleep(1)
insert(6)
end)
insert(4)
return nums

View file

@ -1,56 +0,0 @@
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::time::{Duration, Instant};
use async_io::{block_on, Timer};
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/scheduler_ordering.luau");
pub fn main() -> LuaResult<()> {
tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_target(false)
.without_time()
.init();
// Set up persistent Lua environment
let lua = Lua::new();
let sched = Scheduler::new(&lua);
let fns = Functions::new(&lua)?;
lua.globals().set("spawn", fns.spawn)?;
lua.globals().set("defer", fns.defer)?;
lua.globals().set(
"sleep",
lua.create_async_function(|_, duration: Option<f64>| async move {
let duration = duration.unwrap_or_default().max(1.0 / 250.0);
let before = Instant::now();
let after = Timer::after(Duration::from_secs_f64(duration)).await;
Ok((after - before).as_secs_f64())
})?,
)?;
// Load the main script into the scheduler, and keep track of the thread we spawn
let main = lua.load(MAIN_SCRIPT);
let id = sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
// We should have gotten proper values back from our script
let res = sched.get_thread_result(id).unwrap().unwrap();
let nums = Vec::<usize>::from_lua_multi(res, &lua)?;
assert_eq!(nums, vec![1, 2, 3, 4, 5, 6]);
Ok(())
}
#[test]
fn test_scheduler_ordering() -> LuaResult<()> {
main()
}

View file

@ -1,61 +0,0 @@
/*
NOTE: This example is the same as "lots_of_threads", but with tracy set up for performance profiling.
How to run:
1. Install tracy
- Follow the instructions at https://github.com/wolfpld/tracy
- Or install via something like homebrew: `brew install tracy`
2. Run the server (`tracy`) in a terminal
3. Run the example in another terminal
- `export RUST_LOG=trace`
- `cargo run --example tracy`
*/
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::cargo_common_metadata)]
use std::time::Duration;
use async_io::{block_on, Timer};
use tracing_subscriber::layer::SubscriberExt;
use tracing_tracy::{client::Client as TracyClient, TracyLayer};
use mlua::prelude::*;
use mlua_luau_scheduler::{Functions, Scheduler};
const MAIN_SCRIPT: &str = include_str!("./lua/lots_of_threads.luau");
const ONE_NANOSECOND: Duration = Duration::from_nanos(1);
pub fn main() -> LuaResult<()> {
let _client = TracyClient::start();
let _ = tracing::subscriber::set_global_default(
tracing_subscriber::registry().with(TracyLayer::default()),
);
// Set up persistent Lua environment
let lua = Lua::new();
let sched = Scheduler::new(&lua);
let fns = Functions::new(&lua)?;
lua.globals().set("spawn", fns.spawn)?;
lua.globals().set(
"sleep",
lua.create_async_function(|_, ()| async move {
// Obviously we can't sleep for a single nanosecond since
// this uses OS scheduling under the hood, but we can try
Timer::after(ONE_NANOSECOND).await;
Ok(())
})?,
)?;
// Load the main script into the scheduler
let main = lua.load(MAIN_SCRIPT);
sched.push_thread_front(main, ())?;
// Run until completion
block_on(sched.run());
Ok(())
}

View file

@ -1,45 +0,0 @@
use std::{cell::RefCell, rc::Rc};
use mlua::prelude::*;
type ErrorCallback = Box<dyn Fn(LuaError) + Send + 'static>;
#[derive(Clone)]
pub(crate) struct ThreadErrorCallback {
inner: Rc<RefCell<Option<ErrorCallback>>>,
}
impl ThreadErrorCallback {
pub fn new() -> Self {
Self {
inner: Rc::new(RefCell::new(None)),
}
}
pub fn replace(&self, callback: impl Fn(LuaError) + Send + 'static) {
self.inner.borrow_mut().replace(Box::new(callback));
}
pub fn clear(&self) {
self.inner.borrow_mut().take();
}
pub fn call(&self, error: &LuaError) {
if let Some(cb) = &*self.inner.borrow() {
cb(error.clone());
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn default_error_callback(e: LuaError) {
eprintln!("{e}");
}
impl Default for ThreadErrorCallback {
fn default() -> Self {
let this = Self::new();
this.replace(default_error_callback);
this
}
}

View file

@ -1,31 +0,0 @@
use std::{cell::Cell, rc::Rc};
use event_listener::Event;
#[derive(Debug, Clone)]
pub(crate) struct Exit {
code: Rc<Cell<Option<u8>>>,
event: Rc<Event>,
}
impl Exit {
pub fn new() -> Self {
Self {
code: Rc::new(Cell::new(None)),
event: Rc::new(Event::new()),
}
}
pub fn set(&self, code: u8) {
self.code.set(Some(code));
self.event.notify(usize::MAX);
}
pub fn get(&self) -> Option<u8> {
self.code.get()
}
pub async fn listen(&self) {
self.event.listen().await;
}
}

View file

@ -1,279 +0,0 @@
#![allow(clippy::too_many_lines)]
use mlua::prelude::*;
use crate::{
error_callback::ThreadErrorCallback,
queue::{DeferredThreadQueue, SpawnedThreadQueue},
result_map::ThreadResultMap,
thread_id::ThreadId,
traits::LuaSchedulerExt,
util::{is_poll_pending, LuaThreadOrFunction, ThreadResult},
};
const ERR_METADATA_NOT_ATTACHED: &str = "\
Lua state does not have scheduler metadata attached!\
\nThis is most likely caused by creating functions outside of a scheduler.\
\nScheduler functions must always be created from within an active scheduler.\
";
const EXIT_IMPL_LUA: &str = r"
exit(...)
yield()
";
const WRAP_IMPL_LUA: &str = r"
local t = create(...)
return function(...)
local r = { resume(t, ...) }
if r[1] then
return select(2, unpack(r))
else
error(r[2], 2)
end
end
";
/**
A collection of lua functions that may be called to interact with a [`Scheduler`].
Note that these may all be implemented using [`LuaSchedulerExt`], however, this struct
is implemented using internal (non-public) APIs, and generally has better performance.
*/
pub struct Functions<'lua> {
/**
Implementation of `coroutine.resume` that handles async polling properly.
Defers onto the scheduler queue if the thread calls an async function.
*/
pub resume: LuaFunction<'lua>,
/**
Implementation of `coroutine.wrap` that handles async polling properly.
Defers onto the scheduler queue if the thread calls an async function.
*/
pub wrap: LuaFunction<'lua>,
/**
Resumes a function / thread once instantly, and runs until first yield.
Spawns onto the scheduler queue if not completed.
*/
pub spawn: LuaFunction<'lua>,
/**
Defers a function / thread onto the scheduler queue.
Does not resume instantly, only adds to the queue.
*/
pub defer: LuaFunction<'lua>,
/**
Cancels a function / thread, removing it from the queue.
*/
pub cancel: LuaFunction<'lua>,
/**
Exits the scheduler, stopping all other threads and closing the scheduler.
Yields the calling thread to ensure that it does not continue.
*/
pub exit: LuaFunction<'lua>,
}
impl<'lua> Functions<'lua> {
/**
Creates a new collection of Lua functions that may be called to interact with a [`Scheduler`].
# Errors
Errors when out of memory, or if default Lua globals are missing.
# Panics
Panics when the given [`Lua`] instance does not have an attached [`Scheduler`].
*/
pub fn new(lua: &'lua Lua) -> LuaResult<Self> {
let spawn_queue = lua
.app_data_ref::<SpawnedThreadQueue>()
.expect(ERR_METADATA_NOT_ATTACHED)
.clone();
let defer_queue = lua
.app_data_ref::<DeferredThreadQueue>()
.expect(ERR_METADATA_NOT_ATTACHED)
.clone();
let error_callback = lua
.app_data_ref::<ThreadErrorCallback>()
.expect(ERR_METADATA_NOT_ATTACHED)
.clone();
let result_map = lua
.app_data_ref::<ThreadResultMap>()
.expect(ERR_METADATA_NOT_ATTACHED)
.clone();
let resume_queue = defer_queue.clone();
let resume_map = result_map.clone();
let resume =
lua.create_function(move |lua, (thread, args): (LuaThread, LuaMultiValue)| {
let _span = tracing::trace_span!("Scheduler::fn_resume").entered();
match thread.resume::<_, LuaMultiValue>(args.clone()) {
Ok(v) => {
if v.get(0).is_some_and(is_poll_pending) {
// Pending, defer to scheduler and return nil
resume_queue.push_item(lua, &thread, args)?;
(true, LuaValue::Nil).into_lua_multi(lua)
} else {
// Not pending, store the value if thread is done
if thread.status() != LuaThreadStatus::Resumable {
let id = ThreadId::from(&thread);
if resume_map.is_tracked(id) {
let res = ThreadResult::new(Ok(v.clone()), lua);
resume_map.insert(id, res);
}
}
(true, v).into_lua_multi(lua)
}
}
Err(e) => {
// Not pending, store the error
let id = ThreadId::from(&thread);
if resume_map.is_tracked(id) {
let res = ThreadResult::new(Err(e.clone()), lua);
resume_map.insert(id, res);
}
(false, e.to_string()).into_lua_multi(lua)
}
}
})?;
let wrap_env = lua.create_table_from(vec![
("resume", resume.clone()),
("error", lua.globals().get::<_, LuaFunction>("error")?),
("select", lua.globals().get::<_, LuaFunction>("select")?),
("unpack", lua.globals().get::<_, LuaFunction>("unpack")?),
(
"create",
lua.globals()
.get::<_, LuaTable>("coroutine")?
.get::<_, LuaFunction>("create")?,
),
])?;
let wrap = lua
.load(WRAP_IMPL_LUA)
.set_name("=__scheduler_wrap")
.set_environment(wrap_env)
.into_function()?;
let spawn_map = result_map.clone();
let spawn = lua.create_function(
move |lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| {
let _span = tracing::trace_span!("Scheduler::fn_spawn").entered();
let thread = tof.into_thread(lua)?;
if thread.status() == LuaThreadStatus::Resumable {
// NOTE: We need to resume the thread once instantly for correct behavior,
// and only if we get the pending value back we can spawn to async executor
match thread.resume::<_, LuaMultiValue>(args.clone()) {
Ok(v) => {
if v.get(0).is_some_and(is_poll_pending) {
spawn_queue.push_item(lua, &thread, args)?;
} else {
// Not pending, store the value if thread is done
if thread.status() != LuaThreadStatus::Resumable {
let id = ThreadId::from(&thread);
if spawn_map.is_tracked(id) {
let res = ThreadResult::new(Ok(v), lua);
spawn_map.insert(id, res);
}
}
}
}
Err(e) => {
error_callback.call(&e);
// Not pending, store the error
let id = ThreadId::from(&thread);
if spawn_map.is_tracked(id) {
let res = ThreadResult::new(Err(e), lua);
spawn_map.insert(id, res);
}
}
};
}
Ok(thread)
},
)?;
let defer = lua.create_function(
move |lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| {
let _span = tracing::trace_span!("Scheduler::fn_defer").entered();
let thread = tof.into_thread(lua)?;
if thread.status() == LuaThreadStatus::Resumable {
defer_queue.push_item(lua, &thread, args)?;
}
Ok(thread)
},
)?;
let close = lua
.globals()
.get::<_, LuaTable>("coroutine")?
.get::<_, LuaFunction>("close")?;
let close_key = lua.create_registry_value(close)?;
let cancel = lua.create_function(move |lua, thread: LuaThread| {
let _span = tracing::trace_span!("Scheduler::fn_cancel").entered();
let close: LuaFunction = lua.registry_value(&close_key)?;
match close.call(thread) {
Err(LuaError::CoroutineInactive) | Ok(()) => Ok(()),
Err(e) => Err(e),
}
})?;
let exit_env = lua.create_table_from(vec![
(
"exit",
lua.create_function(|lua, code: Option<u8>| {
let _span = tracing::trace_span!("Scheduler::fn_exit").entered();
let code = code.unwrap_or_default();
lua.set_exit_code(code);
Ok(())
})?,
),
(
"yield",
lua.globals()
.get::<_, LuaTable>("coroutine")?
.get::<_, LuaFunction>("yield")?,
),
])?;
let exit = lua
.load(EXIT_IMPL_LUA)
.set_name("=__scheduler_exit")
.set_environment(exit_env)
.into_function()?;
Ok(Self {
resume,
wrap,
spawn,
defer,
cancel,
exit,
})
}
}
impl Functions<'_> {
/**
Injects [`Scheduler`]-compatible functions into the given [`Lua`] instance.
This will overwrite the following functions:
- `coroutine.resume`
- `coroutine.wrap`
# Errors
Errors when out of memory, or if default Lua globals are missing.
*/
pub fn inject_compat(&self, lua: &Lua) -> LuaResult<()> {
let co: LuaTable = lua.globals().get("coroutine")?;
co.set("resume", self.resume.clone())?;
co.set("wrap", self.wrap.clone())?;
Ok(())
}
}

View file

@ -1,18 +0,0 @@
#![allow(clippy::cargo_common_metadata)]
mod error_callback;
mod exit;
mod functions;
mod queue;
mod result_map;
mod scheduler;
mod status;
mod thread_id;
mod traits;
mod util;
pub use functions::Functions;
pub use scheduler::Scheduler;
pub use status::Status;
pub use thread_id::ThreadId;
pub use traits::{IntoLuaThread, LuaSchedulerExt, LuaSpawnExt};

View file

@ -1,139 +0,0 @@
use std::{pin::Pin, rc::Rc};
use concurrent_queue::ConcurrentQueue;
use derive_more::{Deref, DerefMut};
use event_listener::Event;
use futures_lite::{Future, FutureExt};
use mlua::prelude::*;
use crate::{traits::IntoLuaThread, util::ThreadWithArgs, ThreadId};
/**
Queue for storing [`LuaThread`]s with associated arguments.
Provides methods for pushing and draining the queue, as
well as listening for new items being pushed to the queue.
*/
#[derive(Debug, Clone)]
pub(crate) struct ThreadQueue {
queue: Rc<ConcurrentQueue<ThreadWithArgs>>,
event: Rc<Event>,
}
impl ThreadQueue {
pub fn new() -> Self {
let queue = Rc::new(ConcurrentQueue::unbounded());
let event = Rc::new(Event::new());
Self { queue, event }
}
pub fn push_item<'lua>(
&self,
lua: &'lua Lua,
thread: impl IntoLuaThread<'lua>,
args: impl IntoLuaMulti<'lua>,
) -> LuaResult<ThreadId> {
let thread = thread.into_lua_thread(lua)?;
let args = args.into_lua_multi(lua)?;
tracing::trace!("pushing item to queue with {} args", args.len());
let id = ThreadId::from(&thread);
let stored = ThreadWithArgs::new(lua, thread, args)?;
self.queue.push(stored).into_lua_err()?;
self.event.notify(usize::MAX);
Ok(id)
}
#[inline]
pub fn drain_items<'outer, 'lua>(
&'outer self,
lua: &'lua Lua,
) -> impl Iterator<Item = (LuaThread<'lua>, LuaMultiValue<'lua>)> + 'outer
where
'lua: 'outer,
{
self.queue.try_iter().map(|stored| stored.into_inner(lua))
}
#[inline]
pub async fn wait_for_item(&self) {
if self.queue.is_empty() {
let listener = self.event.listen();
// NOTE: Need to check again, we could have gotten
// new queued items while creating our listener
if self.queue.is_empty() {
listener.await;
}
}
}
#[inline]
pub fn is_empty(&self) -> bool {
self.queue.is_empty()
}
}
/**
Alias for [`ThreadQueue`], providing a newtype to store in Lua app data.
*/
#[derive(Debug, Clone, Deref, DerefMut)]
pub(crate) struct SpawnedThreadQueue(ThreadQueue);
impl SpawnedThreadQueue {
pub fn new() -> Self {
Self(ThreadQueue::new())
}
}
/**
Alias for [`ThreadQueue`], providing a newtype to store in Lua app data.
*/
#[derive(Debug, Clone, Deref, DerefMut)]
pub(crate) struct DeferredThreadQueue(ThreadQueue);
impl DeferredThreadQueue {
pub fn new() -> Self {
Self(ThreadQueue::new())
}
}
pub type LocalBoxFuture<'fut> = Pin<Box<dyn Future<Output = ()> + 'fut>>;
/**
Queue for storing local futures.
Provides methods for pushing and draining the queue, as
well as listening for new items being pushed to the queue.
*/
#[derive(Debug, Clone)]
pub(crate) struct FuturesQueue<'fut> {
queue: Rc<ConcurrentQueue<LocalBoxFuture<'fut>>>,
event: Rc<Event>,
}
impl<'fut> FuturesQueue<'fut> {
pub fn new() -> Self {
let queue = Rc::new(ConcurrentQueue::unbounded());
let event = Rc::new(Event::new());
Self { queue, event }
}
pub fn push_item(&self, fut: impl Future<Output = ()> + 'fut) {
let _ = self.queue.push(fut.boxed_local());
self.event.notify(usize::MAX);
}
pub fn drain_items<'outer>(
&'outer self,
) -> impl Iterator<Item = LocalBoxFuture<'fut>> + 'outer {
self.queue.try_iter()
}
pub async fn wait_for_item(&self) {
if self.queue.is_empty() {
self.event.listen().await;
}
}
}

View file

@ -1,64 +0,0 @@
#![allow(clippy::inline_always)]
use std::{cell::RefCell, rc::Rc};
use event_listener::Event;
// NOTE: This is the hash algorithm that mlua also uses, so we
// are not adding any additional dependencies / bloat by using it.
use rustc_hash::{FxHashMap, FxHashSet};
use crate::{thread_id::ThreadId, util::ThreadResult};
#[derive(Clone)]
pub(crate) struct ThreadResultMap {
tracked: Rc<RefCell<FxHashSet<ThreadId>>>,
results: Rc<RefCell<FxHashMap<ThreadId, ThreadResult>>>,
events: Rc<RefCell<FxHashMap<ThreadId, Rc<Event>>>>,
}
impl ThreadResultMap {
pub fn new() -> Self {
Self {
tracked: Rc::new(RefCell::new(FxHashSet::default())),
results: Rc::new(RefCell::new(FxHashMap::default())),
events: Rc::new(RefCell::new(FxHashMap::default())),
}
}
#[inline(always)]
pub fn track(&self, id: ThreadId) {
self.tracked.borrow_mut().insert(id);
}
#[inline(always)]
pub fn is_tracked(&self, id: ThreadId) -> bool {
self.tracked.borrow().contains(&id)
}
pub fn insert(&self, id: ThreadId, result: ThreadResult) {
debug_assert!(self.is_tracked(id), "Thread must be tracked");
self.results.borrow_mut().insert(id, result);
if let Some(event) = self.events.borrow_mut().remove(&id) {
event.notify(usize::MAX);
}
}
pub async fn listen(&self, id: ThreadId) {
debug_assert!(self.is_tracked(id), "Thread must be tracked");
if !self.results.borrow().contains_key(&id) {
let listener = {
let mut events = self.events.borrow_mut();
let event = events.entry(id).or_insert_with(|| Rc::new(Event::new()));
event.listen()
};
listener.await;
}
}
pub fn remove(&self, id: ThreadId) -> Option<ThreadResult> {
let res = self.results.borrow_mut().remove(&id)?;
self.tracked.borrow_mut().remove(&id);
self.events.borrow_mut().remove(&id);
Some(res)
}
}

Some files were not shown because too many files have changed in this diff Show more