diff --git a/CHANGELOG.md b/CHANGELOG.md index 72dfb93..8207518 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,42 @@ 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). -## Unreleased +## `0.7.7` - August 23rd, 2023 ### Added +- Added a [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) to Lune. ([#83]) + + This allows you to run scripts within Lune without writing files! + + Example usage, inside your favorite terminal: + + ```bash + # 1. Run the Lune executable, without any arguments + lune + + # 2. You will be shown the current Lune version and a blank prompt arrow: + Lune v0.7.7 + > + + # 3. Start typing, and hit enter when you want to run your script! + # Your script will run until completion and output things along the way. + > print(2 + 3) + 5 + > print("Hello, lune changelog!") + Hello, lune changelog! + + # 4. You can also set variables that will get preserved between runs. + # Note that local variables do not get preserved here. + > myVariable = 123 + > print(myVariable) + 123 + + # 5. Press either of these key combinations to exit the REPL: + # - Ctrl + D + # - Ctrl + C + ``` + - Added a new `luau` built-in library for manually compiling and loading Luau source code. ([#82]) Example usage: @@ -31,14 +63,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 callableFn2() ``` -- Implemented support for a variable number of arguments for `CFrame` methods in the `roblox` built-in library. ([#85]) +### Changed + +- Update to Luau version `0.591` +- Lune's internal task scheduler and `require` functionality has been completely rewritten.
+ The new scheduler is much more stable, conforms to a larger test suite, and has a few additional benefits: + + - Built-in libraries are now lazily loaded, meaning nothing gets allocated until the built-in library gets loaded using `require("@lune/builtin-name")`. This also improves startup times slightly. + - Spawned processes using `process.spawn` now run on different thread(s), freeing up resources for the main thread where luau runs. + - Serving requests using `net.serve` now processes requests on background threads, also freeing up resources. In the future, this will also allow us to offload heavy tasks such as compression/decompression to background threads. + - Groundwork for custom / user-defined require aliases has been implemented, as well as absolute / cwd-relative requires. These will both be exposed as options and be made available to use some time in the future. + +- When using the `serde` built-in library, keys are now sorted during serialization. This means that the output of `encode` is now completely deterministic, and wont cause issues when committing generated files to git etc. ### Fixed - Fixed not being able to pass arguments to the thread using `coroutine.resume`. ([#86]) +- Fixed a large number of long-standing issues, from the task scheduler rewrite: + + - Fixed `require` hanging indefinitely when the module being require-d uses an async function in its main body. + - Fixed background tasks (such as `net.serve`) not keeping Lune alive even if there are no lua threads to run. + - Fixed spurious panics and error messages such as `Tried to resume next queued future but none are queued`. + - Fixed not being able to catch non-string errors properly, errors were accidentally being wrapped in an opaque `userdata` type. [#82]: https://github.com/filiptibell/lune/pull/82 -[#85]: https://github.com/filiptibell/lune/pull/85 +[#83]: https://github.com/filiptibell/lune/pull/83 [#86]: https://github.com/filiptibell/lune/pull/86 ## `0.7.6` - August 9th, 2023 diff --git a/Cargo.lock b/Cargo.lock index 10c8d8e..a3744ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -73,9 +73,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayref" @@ -145,13 +145,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -162,9 +162,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -201,9 +201,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "blake2b_simd" @@ -296,9 +296,9 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] @@ -340,9 +340,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.21" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c27cdf28c0f604ba3f512b0c9a409f8de8513e4816705deb0498b627e7c3a3fd" +checksum = "fb690e81c7840c0d7aade59f242ea3b41b9bc27bcd5997890e7702ae4b32e487" dependencies = [ "clap_builder", "clap_derive", @@ -351,9 +351,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.21" +version = "4.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a9f1ab5e9f01a9b81f202e8562eb9a10de70abf9eaeac1be465c28b75aa4aa" +checksum = "5ed2e96bc16d8d740f6f48d663eddf4b8a0983e79210fd55479b7bcd0a69860e" dependencies = [ "anstream", "anstyle", @@ -370,7 +370,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -488,9 +488,9 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "deranged" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" [[package]] name = "dialoguer" @@ -573,9 +573,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -607,9 +607,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070" +checksum = "837c0466252947ada828b975e12daf82e18bb5444e4df87be6038d4469e2a3d2" dependencies = [ "serde", ] @@ -664,9 +664,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", @@ -710,7 +710,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -774,9 +774,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "glam" @@ -792,9 +792,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -872,9 +872,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -1086,22 +1086,22 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "luau0-src" -version = "0.6.0+luau588" +version = "0.7.1+luau591" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c628f5525cc62a89a2d478b2ee619c77b35da55c8e3231752f3b8fe528a6c49" +checksum = "2fb600eccdbc0bb69e746fddb756559a67b5fcfc01c8a142c6853fec76b6bfc7" dependencies = [ "cc", ] [[package]] name = "lune" -version = "0.7.6" +version = "0.7.7" dependencies = [ "anyhow", "async-compression", @@ -1124,6 +1124,7 @@ dependencies = [ "mlua", "once_cell", "os_str_bytes", + "path-clean", "pin-project", "rand", "rbx_binary", @@ -1142,6 +1143,8 @@ dependencies = [ "tokio", "tokio-tungstenite", "toml", + "tracing", + "tracing-subscriber", "urlencoding", ] @@ -1174,6 +1177,15 @@ dependencies = [ "twox-hash", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.5.0" @@ -1208,9 +1220,9 @@ dependencies = [ [[package]] name = "mlua" -version = "0.9.0-rc.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a6500a9fb74b519a85ac206cd57f9f91b270ce39d6cb12ab06a8ed29c3563d" +checksum = "6c3a7a7ff4481ec91b951a733390211a8ace1caba57266ccb5f4d4966704e560" dependencies = [ "bstr", "erased-serde", @@ -1224,9 +1236,9 @@ dependencies = [ [[package]] name = "mlua-sys" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa5b61f6c943d77dd6ab5f670865670f65b978400127c8bf31c2df7d6e76289a" +checksum = "3ec8b54eddb76093069cce9eeffb4c7b3a1a0fe66962d7bd44c4867928149ca3" dependencies = [ "cc", "cfg-if", @@ -1255,6 +1267,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1286,9 +1308,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -1323,6 +1345,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.1" @@ -1343,7 +1371,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -1352,6 +1380,12 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "path-clean" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" + [[package]] name = "percent-encoding" version = "2.3.0" @@ -1375,7 +1409,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1407,7 +1441,7 @@ dependencies = [ "line-wrap", "quick-xml", "serde", - "time 0.3.25", + "time 0.3.27", ] [[package]] @@ -1447,7 +1481,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "097bf8b99121dfb8c75eed54dfbdbdb1d53e372c53d2353e8a15aad2a479249d" dependencies = [ "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1461,9 +1495,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1535,7 +1569,7 @@ dependencies = [ "log", "plist", "winapi", - "winreg", + "winreg 0.10.1", ] [[package]] @@ -1654,8 +1688,17 @@ checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.3.6", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -1666,9 +1709,15 @@ checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.7.4", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.7.4" @@ -1677,9 +1726,9 @@ checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ "base64 0.21.2", "bytes", @@ -1710,8 +1759,8 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.22.6", - "winreg", + "webpki-roots 0.25.2", + "winreg 0.50.0", ] [[package]] @@ -1786,11 +1835,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.7" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", @@ -1805,7 +1854,7 @@ checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" dependencies = [ "log", "ring", - "rustls-webpki 0.101.3", + "rustls-webpki 0.101.4", "sct", ] @@ -1820,9 +1869,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.100.1" +version = "0.100.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab" dependencies = [ "ring", "untrusted", @@ -1830,9 +1879,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.3" +version = "0.101.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261e9e0888cba427c3316e6322805653c9425240b6fd96cee7cb671ab70ab8d0" +checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" dependencies = [ "ring", "untrusted", @@ -1844,7 +1893,7 @@ version = "12.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "994eca4bca05c87e86e15d90fc7a91d1be64b4482b38cb2d27474568fe7c9db9" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "cfg-if", "clipboard-win", "fd-lock", @@ -1915,9 +1964,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.183" +version = "1.0.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1" dependencies = [ "serde_derive", ] @@ -1934,20 +1983,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "indexmap 2.0.0", "itoa", @@ -2015,6 +2064,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "shell-words" version = "1.1.0" @@ -2032,9 +2090,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -2166,9 +2224,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -2177,9 +2235,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", @@ -2199,22 +2257,32 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] @@ -2245,15 +2313,15 @@ dependencies = [ [[package]] name = "time" -version = "0.3.25" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" dependencies = [ "deranged", "itoa", "serde", "time-core", - "time-macros 0.2.11", + "time-macros 0.2.13", ] [[package]] @@ -2274,9 +2342,9 @@ dependencies = [ [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" dependencies = [ "time-core", ] @@ -2311,9 +2379,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.30.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3ce25f50619af8b0aec2eb23deebe84249e19e2ddd393a6e16e3300a6dadfd" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ "backtrace", "bytes", @@ -2325,6 +2393,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.3", "tokio-macros", + "tracing", "windows-sys 0.48.0", ] @@ -2336,7 +2405,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -2427,9 +2496,21 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "tracing-core" version = "0.1.31" @@ -2437,6 +2518,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2555,6 +2666,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" @@ -2619,7 +2736,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -2653,7 +2770,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2674,34 +2791,21 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - [[package]] name = "webpki-roots" version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" dependencies = [ - "rustls-webpki 0.100.1", + "rustls-webpki 0.100.2", ] +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + [[package]] name = "winapi" version = "0.3.9" @@ -2739,7 +2843,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -2757,7 +2861,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -2777,17 +2881,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -2798,9 +2902,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -2810,9 +2914,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -2822,9 +2926,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -2834,9 +2938,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -2846,9 +2950,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -2858,9 +2962,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -2870,15 +2974,15 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.4" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acaaa1190073b2b101e15083c38ee8ec891b5e05cbee516521e94ec008f61e64" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] @@ -2892,6 +2996,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "xml-rs" version = "0.8.16" diff --git a/Cargo.toml b/Cargo.toml index fa3c198..1aab077 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lune" -version = "0.7.6" +version = "0.7.7" edition = "2021" license = "MPL-2.0" repository = "https://github.com/filiptibell/lune" @@ -71,18 +71,17 @@ async-trait = "0.1" dialoguer = "0.10" dunce = "1.0" lz4_flex = "0.11" +path-clean = "1.0" pin-project = "1.0" os_str_bytes = "6.4" urlencoding = "2.1" ### RUNTIME -mlua = { version = "0.9.0-beta.3", features = [ - "luau", - "luau-jit", - "serialize", -] } -tokio = { version = "1.24", features = ["full"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +mlua = { version = "0.9.0", features = ["luau", "luau-jit", "serialize"] } +tokio = { version = "1.24", features = ["full", "tracing"] } ### SERDE diff --git a/src/cli/repl.rs b/src/cli/repl.rs index 6639424..71b4923 100644 --- a/src/cli/repl.rs +++ b/src/cli/repl.rs @@ -32,7 +32,7 @@ pub async fn show_interface() -> Result { let mut prompt_state = PromptState::Regular; let mut source_code = String::new(); - let lune_instance = Lune::new(); + let mut lune_instance = Lune::new(); loop { let prompt = match prompt_state { diff --git a/src/lune/builtins/date_time.rs b/src/lune/builtins/datetime/mod.rs similarity index 100% rename from src/lune/builtins/date_time.rs rename to src/lune/builtins/datetime/mod.rs diff --git a/src/lune/lua/fs/copy.rs b/src/lune/builtins/fs/copy.rs similarity index 99% rename from src/lune/lua/fs/copy.rs rename to src/lune/builtins/fs/copy.rs index 00f37c3..677d0b2 100644 --- a/src/lune/lua/fs/copy.rs +++ b/src/lune/builtins/fs/copy.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use mlua::prelude::*; use tokio::fs; -use super::FsWriteOptions; +use super::options::FsWriteOptions; pub struct CopyContents { // Vec<(relative depth, path)> diff --git a/src/lune/lua/fs/metadata.rs b/src/lune/builtins/fs/metadata.rs similarity index 100% rename from src/lune/lua/fs/metadata.rs rename to src/lune/builtins/fs/metadata.rs diff --git a/src/lune/builtins/fs.rs b/src/lune/builtins/fs/mod.rs similarity index 96% rename from src/lune/builtins/fs.rs rename to src/lune/builtins/fs/mod.rs index fd2db01..210608d 100644 --- a/src/lune/builtins/fs.rs +++ b/src/lune/builtins/fs/mod.rs @@ -4,10 +4,15 @@ use std::path::{PathBuf, MAIN_SEPARATOR}; use mlua::prelude::*; use tokio::fs; -use crate::lune::lua::{ - fs::{copy, FsMetadata, FsWriteOptions}, - table::TableBuilder, -}; +use crate::lune::util::TableBuilder; + +mod copy; +mod metadata; +mod options; + +use copy::copy; +use metadata::FsMetadata; +use options::FsWriteOptions; pub fn create(lua: &'static Lua) -> LuaResult { TableBuilder::new(lua)? diff --git a/src/lune/lua/fs/options.rs b/src/lune/builtins/fs/options.rs similarity index 100% rename from src/lune/lua/fs/options.rs rename to src/lune/builtins/fs/options.rs diff --git a/src/lune/builtins/luau.rs b/src/lune/builtins/luau/mod.rs similarity index 85% rename from src/lune/builtins/luau.rs rename to src/lune/builtins/luau/mod.rs index 6d0c7e4..b8c989b 100644 --- a/src/lune/builtins/luau.rs +++ b/src/lune/builtins/luau/mod.rs @@ -1,13 +1,13 @@ use mlua::prelude::*; -use crate::lune::lua::{ - luau::{LuauCompileOptions, LuauLoadOptions}, - table::TableBuilder, -}; +use crate::lune::util::TableBuilder; + +mod options; +use options::{LuauCompileOptions, LuauLoadOptions}; const BYTECODE_ERROR_BYTE: u8 = 0; -pub fn create(lua: &'static Lua) -> LuaResult { +pub fn create(lua: &Lua) -> LuaResult { TableBuilder::new(lua)? .with_function("compile", compile_source)? .with_function("load", load_source)? diff --git a/src/lune/lua/luau/options.rs b/src/lune/builtins/luau/options.rs similarity index 100% rename from src/lune/lua/luau/options.rs rename to src/lune/builtins/luau/options.rs diff --git a/src/lune/builtins/mod.rs b/src/lune/builtins/mod.rs index 312305d..2fc47cf 100644 --- a/src/lune/builtins/mod.rs +++ b/src/lune/builtins/mod.rs @@ -1,12 +1,85 @@ -pub mod fs; -pub mod luau; -pub mod net; -pub mod process; -pub mod serde; -pub mod stdio; -pub mod task; -pub mod top_level; -pub mod date_time; +use std::str::FromStr; + +use mlua::prelude::*; + +mod fs; +mod luau; +mod net; +mod process; +mod serde; +mod stdio; +mod task; #[cfg(feature = "roblox")] -pub mod roblox; +mod roblox; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub enum LuneBuiltin { + Fs, + Luau, + Net, + Task, + Process, + Serde, + Stdio, + #[cfg(feature = "roblox")] + Roblox, +} + +impl<'lua> LuneBuiltin +where + 'lua: 'static, // FIXME: Remove static lifetime bound here when builtin libraries no longer need it +{ + pub fn name(&self) -> &'static str { + match self { + Self::Fs => "fs", + Self::Luau => "luau", + Self::Net => "net", + Self::Task => "task", + Self::Process => "process", + Self::Serde => "serde", + Self::Stdio => "stdio", + #[cfg(feature = "roblox")] + Self::Roblox => "roblox", + } + } + + pub fn create(&self, lua: &'lua Lua) -> LuaResult> { + let res = match self { + Self::Fs => fs::create(lua), + Self::Luau => luau::create(lua), + Self::Net => net::create(lua), + Self::Task => task::create(lua), + Self::Process => process::create(lua), + Self::Serde => serde::create(lua), + Self::Stdio => stdio::create(lua), + #[cfg(feature = "roblox")] + Self::Roblox => roblox::create(lua), + }; + match res { + Ok(v) => v.into_lua_multi(lua), + Err(e) => Err(e.context(format!( + "Failed to create builtin library '{}'", + self.name() + ))), + } + } +} + +impl FromStr for LuneBuiltin { + type Err = String; + fn from_str(s: &str) -> Result { + match s.trim().to_ascii_lowercase().as_str() { + "fs" => Ok(Self::Fs), + "luau" => Ok(Self::Luau), + "net" => Ok(Self::Net), + "task" => Ok(Self::Task), + "process" => Ok(Self::Process), + "serde" => Ok(Self::Serde), + "stdio" => Ok(Self::Stdio), + #[cfg(feature = "roblox")] + "roblox" => Ok(Self::Roblox), + _ => Err(format!("Unknown builtin library '{s}'")), + } + } +} diff --git a/src/lune/lua/net/client.rs b/src/lune/builtins/net/client.rs similarity index 56% rename from src/lune/lua/net/client.rs rename to src/lune/builtins/net/client.rs index 2120c58..23636ff 100644 --- a/src/lune/lua/net/client.rs +++ b/src/lune/builtins/net/client.rs @@ -5,6 +5,8 @@ use mlua::prelude::*; use hyper::{header::HeaderName, http::HeaderValue, HeaderMap}; use reqwest::{IntoUrl, Method, RequestBuilder}; +const REGISTRY_KEY: &str = "NetClient"; + pub struct NetClientBuilder { builder: reqwest::ClientBuilder, } @@ -44,6 +46,35 @@ impl NetClient { pub fn request(&self, method: Method, url: U) -> RequestBuilder { self.0.request(method, url) } + + pub fn into_registry(self, lua: &Lua) { + lua.set_named_registry_value(REGISTRY_KEY, self) + .expect("Failed to store NetClient in lua registry"); + } + + pub fn from_registry(lua: &Lua) -> Self { + lua.named_registry_value(REGISTRY_KEY) + .expect("Failed to get NetClient from lua registry") + } } impl LuaUserData for NetClient {} + +impl<'lua> FromLua<'lua> for NetClient { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + if let LuaValue::UserData(ud) = value { + if let Ok(ctx) = ud.borrow::() { + return Ok(ctx.clone()); + } + } + unreachable!("NetClient should only be used from registry") + } +} + +impl<'lua> From<&'lua Lua> for NetClient { + fn from(value: &'lua Lua) -> Self { + value + .named_registry_value(REGISTRY_KEY) + .expect("Missing require context in lua registry") + } +} diff --git a/src/lune/lua/net/config.rs b/src/lune/builtins/net/config.rs similarity index 100% rename from src/lune/lua/net/config.rs rename to src/lune/builtins/net/config.rs diff --git a/src/lune/builtins/net.rs b/src/lune/builtins/net/mod.rs similarity index 56% rename from src/lune/builtins/net.rs rename to src/lune/builtins/net/mod.rs index 0be55b5..615136d 100644 --- a/src/lune/builtins/net.rs +++ b/src/lune/builtins/net/mod.rs @@ -2,32 +2,34 @@ use std::collections::HashMap; use mlua::prelude::*; -use console::style; -use hyper::{ - header::{CONTENT_ENCODING, CONTENT_LENGTH}, - Server, -}; -use tokio::{sync::mpsc, task}; +use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH}; -use crate::lune::lua::{ - net::{ - NetClient, NetClientBuilder, NetLocalExec, NetService, NetWebSocket, RequestConfig, - ServeConfig, - }, - serde::{decompress, CompressDecompressFormat, EncodeDecodeConfig, EncodeDecodeFormat}, - table::TableBuilder, - task::{TaskScheduler, TaskSchedulerAsyncExt}, +use crate::lune::{scheduler::Scheduler, util::TableBuilder}; + +use self::server::create_server; + +use super::serde::{ + compress_decompress::{decompress, CompressDecompressFormat}, + encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat}, }; +mod client; +mod config; +mod processing; +mod response; +mod server; +mod websocket; + +use client::{NetClient, NetClientBuilder}; +use config::{RequestConfig, ServeConfig}; +use server::bind_to_localhost; +use websocket::NetWebSocket; + pub fn create(lua: &'static Lua) -> LuaResult { - // Create a reusable client for performing our - // web requests and store it in the lua registry, - // allowing us to reuse headers and internal structs - let client = NetClientBuilder::new() + NetClientBuilder::new() .headers(&[("User-Agent", create_user_agent_header())])? - .build()?; - lua.set_named_registry_value("net.client", client)?; - // Create the global table for net + .build()? + .into_registry(lua); TableBuilder::new(lua)? .with_function("jsonEncode", net_json_encode)? .with_function("jsonDecode", net_json_decode)? @@ -59,12 +61,12 @@ fn net_json_decode<'lua>(lua: &'lua Lua, json: LuaString<'lua>) -> LuaResult( - lua: &'static Lua, - config: RequestConfig<'lua>, -) -> LuaResult> { +async fn net_request<'lua>(lua: &'lua Lua, config: RequestConfig<'lua>) -> LuaResult> +where + 'lua: 'static, // FIXME: Get rid of static lifetime bound here +{ // Create and send the request - let client: LuaUserDataRef = lua.named_registry_value("net.client")?; + let client = NetClient::from_registry(lua); let mut request = client.request(config.method, &config.url); for (query, value) in config.query { request = request.query(&[(query.to_str()?, value.to_str()?)]); @@ -122,73 +124,28 @@ async fn net_request<'lua>( .build_readonly() } -async fn net_socket<'lua>(lua: &'static Lua, url: String) -> LuaResult { +async fn net_socket<'lua>(lua: &'lua Lua, url: String) -> LuaResult +where + 'lua: 'static, // FIXME: Get rid of static lifetime bound here +{ let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?; NetWebSocket::new(ws).into_lua_table(lua) } async fn net_serve<'lua>( - lua: &'static Lua, + lua: &'lua Lua, (port, config): (u16, ServeConfig<'lua>), -) -> LuaResult> { - // Note that we need to use a mpsc here and not - // a oneshot channel since we move the sender - // into our table with the stop function - let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1); - let server_request_callback = lua.create_registry_value(config.handle_request)?; - let server_websocket_callback = config.handle_web_socket.map(|handler| { - lua.create_registry_value(handler) - .expect("Failed to store websocket handler") - }); +) -> LuaResult> +where + 'lua: 'static, // FIXME: Get rid of static lifetime bound here +{ let sched = lua - .app_data_ref::<&TaskScheduler>() - .expect("Missing task scheduler - make sure it is added as a lua app data before the first scheduler resumption"); - // Bind first to make sure that we can bind to this address - let bound = match Server::try_bind(&([127, 0, 0, 1], port).into()) { - Err(e) => { - return Err(LuaError::external(format!( - "Failed to bind to localhost on port {port}\n{}", - format!("{e}").replace( - "error creating server listener: ", - &format!("{}", style("> ").dim()) - ) - ))); - } - Ok(bound) => bound, - }; - // Register a background task to prevent the task scheduler from - // exiting early and start up our web server on the bound address - let task = sched.register_background_task(); - let server = bound - .http1_only(true) // Web sockets can only use http1 - .http1_keepalive(true) // Web sockets must be kept alive - .executor(NetLocalExec) - .serve(NetService::new( - lua, - server_request_callback, - server_websocket_callback, - )) - .with_graceful_shutdown(async move { - task.unregister(Ok(())); - shutdown_rx - .recv() - .await - .expect("Server was stopped instantly"); - shutdown_rx.close(); - }); - // Spawn a new tokio task so we don't block - task::spawn_local(server); - // Create a new read-only table that contains methods - // for manipulating server behavior and shutting it down - let handle_stop = move |_, _: ()| match shutdown_tx.try_send(()) { - Ok(_) => Ok(()), - Err(_) => Err(LuaError::RuntimeError( - "Server has already been stopped".to_string(), - )), - }; - TableBuilder::new(lua)? - .with_function("stop", handle_stop)? - .build_readonly() + .app_data_ref::<&Scheduler>() + .expect("Lua struct is missing scheduler"); + + let builder = bind_to_localhost(port)?; + + create_server(lua, &sched, config, builder) } fn net_url_encode<'lua>( diff --git a/src/lune/builtins/net/processing.rs b/src/lune/builtins/net/processing.rs new file mode 100644 index 0000000..837fd3e --- /dev/null +++ b/src/lune/builtins/net/processing.rs @@ -0,0 +1,101 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use hyper::{body::to_bytes, Body, Request}; + +use mlua::prelude::*; + +use crate::lune::util::TableBuilder; + +static ID_COUNTER: AtomicUsize = AtomicUsize::new(0); + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub(super) struct ProcessedRequestId(usize); + +impl ProcessedRequestId { + pub fn new() -> Self { + // NOTE: This may overflow after a couple billion requests, + // but that's completely fine... unless a request is still + // alive after billions more arrive and need to be handled + Self(ID_COUNTER.fetch_add(1, Ordering::Relaxed)) + } +} + +pub(super) struct ProcessedRequest { + pub id: ProcessedRequestId, + method: String, + path: String, + query: Vec<(String, String)>, + headers: Vec<(String, Vec)>, + body: Vec, +} + +impl ProcessedRequest { + pub async fn from_request(req: Request) -> LuaResult { + let (head, body) = req.into_parts(); + + // FUTURE: We can do extra processing like async decompression here + let body = match to_bytes(body).await { + Err(_) => return Err(LuaError::runtime("Failed to read request body bytes")), + Ok(b) => b.to_vec(), + }; + + let method = head.method.to_string().to_ascii_uppercase(); + + let mut path = head.uri.path().to_string(); + if path.is_empty() { + path = "/".to_string(); + } + + let query = head + .uri + .query() + .unwrap_or_default() + .split('&') + .filter_map(|q| q.split_once('=')) + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + let mut headers = Vec::new(); + let mut header_name = String::new(); + for (name_opt, value) in head.headers.into_iter() { + if let Some(name) = name_opt { + header_name = name.to_string(); + } + headers.push((header_name.clone(), value.as_bytes().to_vec())) + } + + let id = ProcessedRequestId::new(); + + Ok(Self { + id, + method, + path, + query, + headers, + body, + }) + } + + pub fn into_lua_table(self, lua: &Lua) -> LuaResult { + // FUTURE: Make inner tables for query keys that have multiple values? + let query = lua.create_table_with_capacity(0, self.query.len())?; + for (key, value) in self.query.into_iter() { + query.set(key, value)?; + } + + let headers = lua.create_table_with_capacity(0, self.headers.len())?; + for (key, value) in self.headers.into_iter() { + headers.set(key, lua.create_string(value)?)?; + } + + let body = lua.create_string(self.body)?; + + TableBuilder::new(lua)? + .with_value("method", self.method)? + .with_value("path", self.path)? + .with_value("query", query)? + .with_value("headers", headers)? + .with_value("body", body)? + .build_readonly() + } +} diff --git a/src/lune/lua/net/response.rs b/src/lune/builtins/net/response.rs similarity index 74% rename from src/lune/lua/net/response.rs rename to src/lune/builtins/net/response.rs index fa2e748..d14646a 100644 --- a/src/lune/lua/net/response.rs +++ b/src/lune/builtins/net/response.rs @@ -9,7 +9,7 @@ pub enum NetServeResponseKind { Table, } -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct NetServeResponse { kind: NetServeResponseKind, status: u16, @@ -81,26 +81,3 @@ impl<'lua> FromLua<'lua> for NetServeResponse { } } } - -impl<'lua> IntoLua<'lua> for NetServeResponse { - fn into_lua(self, lua: &'lua Lua) -> LuaResult> { - if self.headers.len() > i32::MAX as usize { - return Err(LuaError::ToLuaConversionError { - from: "NetServeResponse", - to: "table", - message: Some("Too many header values".to_string()), - }); - } - let body = self.body.map(|b| lua.create_string(b)).transpose()?; - let headers = lua.create_table_with_capacity(0, self.headers.len())?; - for (key, value) in self.headers { - headers.set(key, lua.create_string(&value)?)?; - } - let table = lua.create_table_with_capacity(0, 3)?; - table.set("status", self.status)?; - table.set("headers", headers)?; - table.set("body", body)?; - table.set_readonly(true); - Ok(LuaValue::Table(table)) - } -} diff --git a/src/lune/builtins/net/server.rs b/src/lune/builtins/net/server.rs new file mode 100644 index 0000000..167f10b --- /dev/null +++ b/src/lune/builtins/net/server.rs @@ -0,0 +1,203 @@ +use std::{collections::HashMap, convert::Infallible, net::SocketAddr, sync::Arc}; + +use hyper::{ + server::{conn::AddrIncoming, Builder}, + service::{make_service_fn, service_fn}, + Server, +}; + +use hyper_tungstenite::{is_upgrade_request, upgrade, HyperWebsocket}; +use mlua::prelude::*; +use tokio::sync::{mpsc, oneshot, Mutex}; + +use crate::lune::{ + scheduler::Scheduler, + util::{traits::LuaEmitErrorExt, TableBuilder}, +}; + +use super::{ + config::ServeConfig, processing::ProcessedRequest, response::NetServeResponse, + websocket::NetWebSocket, +}; + +pub(super) fn bind_to_localhost(port: u16) -> LuaResult> { + let addr = match SocketAddr::try_from(([127, 0, 0, 1], port)) { + Ok(a) => a, + Err(e) => { + return Err(LuaError::external(format!( + "Failed to bind to localhost on port {port}\n{e}" + ))) + } + }; + match Server::try_bind(&addr) { + Ok(b) => Ok(b), + Err(e) => Err(LuaError::external(format!( + "Failed to bind to localhost on port {port}\n{}", + e.to_string() + .replace("error creating server listener: ", "> ") + ))), + } +} + +pub(super) fn create_server<'lua>( + lua: &'lua Lua, + sched: &'lua Scheduler, + config: ServeConfig<'lua>, + builder: Builder, +) -> LuaResult> +where + 'lua: 'static, // FIXME: Get rid of static lifetime bound here +{ + // Note that we need to use a mpsc here and not + // a oneshot channel since we move the sender + // into our table with the stop function + let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1); + + // Communicate between background thread(s) and main lua thread using mpsc and oneshot + let (tx_request, mut rx_request) = mpsc::channel::(64); + let (tx_websocket, mut rx_websocket) = mpsc::channel::(64); + let tx_request_arc = Arc::new(tx_request); + let tx_websocket_arc = Arc::new(tx_websocket); + + let response_senders = Arc::new(Mutex::new(HashMap::new())); + let response_senders_bg = Arc::clone(&response_senders); + let response_senders_lua = Arc::clone(&response_senders_bg); + + // Create our background service which will accept + // requests, do some processing, then forward to lua + let has_websocket_handler = config.handle_web_socket.is_some(); + let hyper_make_service = make_service_fn(move |_| { + let tx_request = Arc::clone(&tx_request_arc); + let tx_websocket = Arc::clone(&tx_websocket_arc); + let response_senders = Arc::clone(&response_senders_bg); + + let handler = service_fn(move |mut req| { + let tx_request = Arc::clone(&tx_request); + let tx_websocket = Arc::clone(&tx_websocket); + let response_senders = Arc::clone(&response_senders); + async move { + // FUTURE: Improve error messages when lua is busy and queue is full + if has_websocket_handler && is_upgrade_request(&req) { + let (response, ws) = match upgrade(&mut req, None) { + Err(_) => return Err(LuaError::runtime("Failed to upgrade websocket")), + Ok(v) => v, + }; + if (tx_websocket.send(ws).await).is_err() { + return Err(LuaError::runtime("Lua handler is busy")); + } + Ok(response) + } else { + let processed = ProcessedRequest::from_request(req).await?; + let request_id = processed.id; + if (tx_request.send(processed).await).is_err() { + return Err(LuaError::runtime("Lua handler is busy")); + } + let (response_tx, response_rx) = oneshot::channel::(); + response_senders + .lock() + .await + .insert(request_id, response_tx); + match response_rx.await { + Err(_) => Err(LuaError::runtime("Internal Server Error")), + Ok(r) => r.into_response(), + } + } + } + }); + + async move { Ok::<_, Infallible>(handler) } + }); + + // Start up our service + sched.spawn(async move { + let result = builder + .http1_only(true) // Web sockets can only use http1 + .http1_keepalive(true) // Web sockets must be kept alive + .serve(hyper_make_service) + .with_graceful_shutdown(async move { + shutdown_rx.recv().await; + }); + if let Err(e) = result.await { + eprintln!("Net serve error: {e}") + } + }); + + // Spawn a local thread with access to lua and the same lifetime + sched.spawn_local(async move { + loop { + // Wait for either a request or a websocket to handle, + // if we got neither it means both channels were dropped + // and our server has stopped, either gracefully or panic + let (req, sock) = tokio::select! { + req = rx_request.recv() => (req, None), + sock = rx_websocket.recv() => (None, sock), + }; + + // NOTE: The closure here is not really necessary, we + // make the closure so that we can use the `?` operator + let handle_req_or_sock = || async { + match (req, sock) { + (None, None) => Ok::<_, LuaError>(true), + (Some(req), _) => { + let req_id = req.id; + let req_handler = config.handle_request.clone(); + let req_table = req.into_lua_table(lua)?; + + let thread_id = sched.push_back(lua, req_handler, req_table)?; + let thread_res = sched.wait_for_thread(lua, thread_id).await?; + + let response = NetServeResponse::from_lua_multi(thread_res, lua)?; + let response_sender = response_senders_lua + .lock() + .await + .remove(&req_id) + .expect("Response channel was removed unexpectedly"); + + // NOTE: We ignore the error here, if the sender is no longer + // being listened to its because our client disconnected during + // handler being called, which is fine and should not emit errors + response_sender.send(response).ok(); + + Ok(false) + } + (_, Some(sock)) => { + let sock = sock.await.into_lua_err()?; + + let sock_handler = config + .handle_web_socket + .as_ref() + .cloned() + .expect("Got web socket but web socket handler is missing"); + let sock_table = NetWebSocket::new(sock).into_lua_table(lua)?; + + // NOTE: Web socket handler does not need to send any + // response back, the websocket upgrade response is + // automatically sent above in the background thread(s) + let thread_id = sched.push_back(lua, sock_handler, sock_table)?; + let _thread_res = sched.wait_for_thread(lua, thread_id).await?; + + Ok(false) + } + } + }; + + match handle_req_or_sock().await { + Ok(true) => break, + Ok(false) => continue, + Err(e) => lua.emit_error(e), + } + } + }); + + // Create a new read-only table that contains methods + // for manipulating server behavior and shutting it down + let handle_stop = move |_, _: ()| match shutdown_tx.try_send(()) { + Ok(_) => Ok(()), + Err(_) => Err(LuaError::RuntimeError( + "Server has already been stopped".to_string(), + )), + }; + TableBuilder::new(lua)? + .with_function("stop", handle_stop)? + .build_readonly() +} diff --git a/src/lune/lua/net/websocket.rs b/src/lune/builtins/net/websocket.rs similarity index 89% rename from src/lune/lua/net/websocket.rs rename to src/lune/builtins/net/websocket.rs index 1057b1b..2bd7c5a 100644 --- a/src/lune/lua/net/websocket.rs +++ b/src/lune/builtins/net/websocket.rs @@ -22,7 +22,7 @@ use hyper_tungstenite::{ }; use tokio_tungstenite::MaybeTlsStream; -use crate::lune::lua::table::TableBuilder; +use crate::lune::util::TableBuilder; const WEB_SOCKET_IMPL_LUA: &str = r#" return freeze(setmetatable({ @@ -89,20 +89,19 @@ where type NetWebSocketStreamClient = MaybeTlsStream; impl NetWebSocket { pub fn into_lua_table(self, lua: &'static Lua) -> LuaResult { + let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?; + let table_freeze = lua + .globals() + .get::<_, LuaTable>("table")? + .get::<_, LuaFunction>("freeze")?; let socket_env = TableBuilder::new(lua)? .with_value("websocket", self)? .with_function("close_code", close_code::)? .with_async_function("close", close::)? .with_async_function("send", send::)? .with_async_function("next", next::)? - .with_value( - "setmetatable", - lua.named_registry_value::("tab.setmeta")?, - )? - .with_value( - "freeze", - lua.named_registry_value::("tab.freeze")?, - )? + .with_value("setmetatable", setmetatable)? + .with_value("freeze", table_freeze)? .build_readonly()?; Self::into_lua_table_with_env(lua, socket_env) } @@ -111,20 +110,19 @@ impl NetWebSocket { type NetWebSocketStreamServer = Upgraded; impl NetWebSocket { pub fn into_lua_table(self, lua: &'static Lua) -> LuaResult { + let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?; + let table_freeze = lua + .globals() + .get::<_, LuaTable>("table")? + .get::<_, LuaFunction>("freeze")?; let socket_env = TableBuilder::new(lua)? .with_value("websocket", self)? .with_function("close_code", close_code::)? .with_async_function("close", close::)? .with_async_function("send", send::)? .with_async_function("next", next::)? - .with_value( - "setmetatable", - lua.named_registry_value::("tab.setmeta")?, - )? - .with_value( - "freeze", - lua.named_registry_value::("tab.freeze")?, - )? + .with_value("setmetatable", setmetatable)? + .with_value("freeze", table_freeze)? .build_readonly()?; Self::into_lua_table_with_env(lua, socket_env) } diff --git a/src/lune/builtins/process.rs b/src/lune/builtins/process.rs deleted file mode 100644 index 51e7c6f..0000000 --- a/src/lune/builtins/process.rs +++ /dev/null @@ -1,296 +0,0 @@ -use std::{ - collections::HashMap, - env::{self, consts}, - path::{self, PathBuf}, - process::{ExitCode, Stdio}, -}; - -use directories::UserDirs; -use dunce::canonicalize; -use mlua::prelude::*; -use os_str_bytes::RawOsString; -use tokio::process::Command; - -use crate::lune::lua::{ - process::pipe_and_inherit_child_process_stdio, table::TableBuilder, task::TaskScheduler, -}; - -const PROCESS_EXIT_IMPL_LUA: &str = r#" -exit(...) -yield() -"#; - -pub fn create(lua: &'static Lua, args_vec: Vec) -> LuaResult { - let cwd_str = { - let cwd = canonicalize(env::current_dir()?)?; - let cwd_str = cwd.to_string_lossy().to_string(); - if !cwd_str.ends_with(path::MAIN_SEPARATOR) { - format!("{cwd_str}{}", path::MAIN_SEPARATOR) - } else { - cwd_str - } - }; - // Create constants for OS & processor architecture - let os = lua.create_string(&consts::OS.to_lowercase())?; - let arch = lua.create_string(&consts::ARCH.to_lowercase())?; - // Create readonly args array - let args_tab = TableBuilder::new(lua)? - .with_sequential_values(args_vec)? - .build_readonly()?; - // Create proxied table for env that gets & sets real env vars - let env_tab = TableBuilder::new(lua)? - .with_metatable( - TableBuilder::new(lua)? - .with_function(LuaMetaMethod::Index.name(), process_env_get)? - .with_function(LuaMetaMethod::NewIndex.name(), process_env_set)? - .with_function(LuaMetaMethod::Iter.name(), process_env_iter)? - .build_readonly()?, - )? - .build_readonly()?; - // Create our process exit function, this is a bit involved since - // we have no way to yield from c / rust, we need to load a lua - // chunk that will set the exit code and yield for us instead - let process_exit_env_yield: LuaFunction = lua.named_registry_value("co.yield")?; - let process_exit_env_exit: LuaFunction = lua.create_function(|lua, code: Option| { - let exit_code = code.map_or(ExitCode::SUCCESS, ExitCode::from); - let sched = lua - .app_data_ref::<&TaskScheduler>() - .expect("Missing task scheduler - make sure it is added as a lua app data before the first scheduler resumption"); - sched.set_exit_code(exit_code); - Ok(()) - })?; - let process_exit = lua - .load(PROCESS_EXIT_IMPL_LUA) - .set_name("=process.exit") - .set_environment( - TableBuilder::new(lua)? - .with_value("yield", process_exit_env_yield)? - .with_value("exit", process_exit_env_exit)? - .build_readonly()?, - ) - .into_function()?; - // Create the full process table - TableBuilder::new(lua)? - .with_value("os", os)? - .with_value("arch", arch)? - .with_value("args", args_tab)? - .with_value("cwd", cwd_str)? - .with_value("env", env_tab)? - .with_value("exit", process_exit)? - .with_async_function("spawn", process_spawn)? - .build_readonly() -} - -fn process_env_get<'lua>( - lua: &'lua Lua, - (_, key): (LuaValue<'lua>, String), -) -> LuaResult> { - match env::var_os(key) { - Some(value) => { - let raw_value = RawOsString::new(value); - Ok(LuaValue::String( - lua.create_string(raw_value.as_raw_bytes())?, - )) - } - None => Ok(LuaValue::Nil), - } -} - -fn process_env_set<'lua>( - _: &'lua Lua, - (_, key, value): (LuaValue<'lua>, String, Option), -) -> LuaResult<()> { - // Make sure key is valid, otherwise set_var will panic - if key.is_empty() { - Err(LuaError::RuntimeError("Key must not be empty".to_string())) - } else if key.contains('=') { - Err(LuaError::RuntimeError( - "Key must not contain the equals character '='".to_string(), - )) - } else if key.contains('\0') { - Err(LuaError::RuntimeError( - "Key must not contain the NUL character".to_string(), - )) - } else { - match value { - Some(value) => { - // Make sure value is valid, otherwise set_var will panic - if value.contains('\0') { - Err(LuaError::RuntimeError( - "Value must not contain the NUL character".to_string(), - )) - } else { - env::set_var(&key, &value); - Ok(()) - } - } - None => { - env::remove_var(&key); - Ok(()) - } - } - } -} - -fn process_env_iter<'lua>( - lua: &'lua Lua, - (_, _): (LuaValue<'lua>, ()), -) -> LuaResult> { - let mut vars = env::vars_os().collect::>().into_iter(); - lua.create_function_mut(move |lua, _: ()| match vars.next() { - Some((key, value)) => { - let raw_key = RawOsString::new(key); - let raw_value = RawOsString::new(value); - Ok(( - LuaValue::String(lua.create_string(raw_key.as_raw_bytes())?), - LuaValue::String(lua.create_string(raw_value.as_raw_bytes())?), - )) - } - None => Ok((LuaValue::Nil, LuaValue::Nil)), - }) -} - -async fn process_spawn<'lua>( - lua: &'static Lua, - (mut program, args, options): (String, Option>, Option>), -) -> LuaResult> { - // Parse any given options or create defaults - let (child_cwd, child_envs, child_shell, child_stdio_inherit) = match options { - Some(options) => { - let mut cwd = env::current_dir()?; - let mut envs = HashMap::new(); - let mut shell = None; - let mut inherit = false; - match options.raw_get("cwd")? { - LuaValue::Nil => {} - LuaValue::String(s) => { - cwd = PathBuf::from(s.to_string_lossy().to_string()); - // Substitute leading tilde (~) for the actual home dir - if cwd.starts_with("~") { - if let Some(user_dirs) = UserDirs::new() { - cwd = user_dirs.home_dir().join(cwd.strip_prefix("~").unwrap()) - } - }; - if !cwd.exists() { - return Err(LuaError::RuntimeError( - "Invalid value for option 'cwd' - path does not exist".to_string(), - )); - } - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'cwd' - expected 'string', got '{}'", - value.type_name() - ))) - } - } - match options.raw_get("env")? { - LuaValue::Nil => {} - LuaValue::Table(t) => { - for pair in t.pairs::() { - let (k, v) = pair?; - envs.insert(k, v); - } - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'env' - expected 'table', got '{}'", - value.type_name() - ))) - } - } - match options.raw_get("shell")? { - LuaValue::Nil => {} - LuaValue::String(s) => shell = Some(s.to_string_lossy().to_string()), - LuaValue::Boolean(true) => { - shell = match env::consts::FAMILY { - "unix" => Some("/bin/sh".to_string()), - "windows" => Some("/bin/sh".to_string()), - _ => None, - }; - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'shell' - expected 'true' or 'string', got '{}'", - value.type_name() - ))) - } - } - match options.raw_get("stdio")? { - LuaValue::Nil => {} - LuaValue::String(s) => { - match s.to_str()? { - "inherit" => { - inherit = true; - }, - "default" => { - inherit = false; - } - _ => return Err(LuaError::RuntimeError( - format!("Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'", s.to_string_lossy()), - )) - } - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'stdio' - expected 'string', got '{}'", - value.type_name() - ))) - } - } - Ok::<_, LuaError>((cwd, envs, shell, inherit)) - } - None => Ok((env::current_dir()?, HashMap::new(), None, false)), - }?; - // Run a shell using the command param if wanted - let child_args = if let Some(shell) = child_shell { - let shell_args = match args { - Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))], - None => vec!["-c".to_string(), program], - }; - program = shell; - Some(shell_args) - } else { - args - }; - // Create command with the wanted options - let mut cmd = match child_args { - None => Command::new(program), - Some(args) => { - let mut cmd = Command::new(program); - cmd.args(args); - cmd - } - }; - // Set dir to run in and env variables - cmd.current_dir(child_cwd); - cmd.envs(child_envs); - // Spawn the child process - let child = cmd - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - // Inherit the output and stderr if wanted - let result = if child_stdio_inherit { - pipe_and_inherit_child_process_stdio(child).await - } else { - let output = child.wait_with_output().await?; - Ok((output.status, output.stdout, output.stderr)) - }; - // Extract result - let (status, stdout, stderr) = result?; - // NOTE: If an exit code was not given by the child process, - // we default to 1 if it yielded any error output, otherwise 0 - let code = status.code().unwrap_or(match stderr.is_empty() { - true => 0, - false => 1, - }); - // Construct and return a readonly lua table with results - TableBuilder::new(lua)? - .with_value("ok", code == 0)? - .with_value("code", code)? - .with_value("stdout", lua.create_string(&stdout)?)? - .with_value("stderr", lua.create_string(&stderr)?)? - .build_readonly() -} diff --git a/src/lune/builtins/process/mod.rs b/src/lune/builtins/process/mod.rs new file mode 100644 index 0000000..212d325 --- /dev/null +++ b/src/lune/builtins/process/mod.rs @@ -0,0 +1,219 @@ +use std::{ + env::{self, consts}, + path, + process::{ExitStatus, Stdio}, +}; + +use dunce::canonicalize; +use mlua::prelude::*; +use os_str_bytes::RawOsString; + +use crate::lune::{scheduler::Scheduler, util::TableBuilder}; + +mod tee_writer; + +mod pipe_inherit; +use pipe_inherit::pipe_and_inherit_child_process_stdio; + +mod options; +use options::ProcessSpawnOptions; + +const PROCESS_EXIT_IMPL_LUA: &str = r#" +exit(...) +yield() +"#; + +pub fn create(lua: &'static Lua) -> LuaResult { + let cwd_str = { + let cwd = canonicalize(env::current_dir()?)?; + let cwd_str = cwd.to_string_lossy().to_string(); + if !cwd_str.ends_with(path::MAIN_SEPARATOR) { + format!("{cwd_str}{}", path::MAIN_SEPARATOR) + } else { + cwd_str + } + }; + // Create constants for OS & processor architecture + let os = lua.create_string(&consts::OS.to_lowercase())?; + let arch = lua.create_string(&consts::ARCH.to_lowercase())?; + // Create readonly args array + let args_vec = lua + .app_data_ref::>() + .ok_or_else(|| LuaError::runtime("Missing args vec in Lua app data"))? + .clone(); + let args_tab = TableBuilder::new(lua)? + .with_sequential_values(args_vec)? + .build_readonly()?; + // Create proxied table for env that gets & sets real env vars + let env_tab = TableBuilder::new(lua)? + .with_metatable( + TableBuilder::new(lua)? + .with_function(LuaMetaMethod::Index.name(), process_env_get)? + .with_function(LuaMetaMethod::NewIndex.name(), process_env_set)? + .with_function(LuaMetaMethod::Iter.name(), process_env_iter)? + .build_readonly()?, + )? + .build_readonly()?; + // Create our process exit function, this is a bit involved since + // we have no way to yield from c / rust, we need to load a lua + // chunk that will set the exit code and yield for us instead + let coroutine_yield = lua + .globals() + .get::<_, LuaTable>("coroutine")? + .get::<_, LuaFunction>("yield")?; + let set_scheduler_exit_code = lua.create_function(|lua, code: Option| { + let sched = lua + .app_data_ref::<&Scheduler>() + .expect("Lua struct is missing scheduler"); + sched.set_exit_code(code.unwrap_or_default()); + Ok(()) + })?; + let process_exit = lua + .load(PROCESS_EXIT_IMPL_LUA) + .set_name("=process.exit") + .set_environment( + TableBuilder::new(lua)? + .with_value("yield", coroutine_yield)? + .with_value("exit", set_scheduler_exit_code)? + .build_readonly()?, + ) + .into_function()?; + // Create the full process table + TableBuilder::new(lua)? + .with_value("os", os)? + .with_value("arch", arch)? + .with_value("args", args_tab)? + .with_value("cwd", cwd_str)? + .with_value("env", env_tab)? + .with_value("exit", process_exit)? + .with_async_function("spawn", process_spawn)? + .build_readonly() +} + +fn process_env_get<'lua>( + lua: &'lua Lua, + (_, key): (LuaValue<'lua>, String), +) -> LuaResult> { + match env::var_os(key) { + Some(value) => { + let raw_value = RawOsString::new(value); + Ok(LuaValue::String( + lua.create_string(raw_value.as_raw_bytes())?, + )) + } + None => Ok(LuaValue::Nil), + } +} + +fn process_env_set<'lua>( + _: &'lua Lua, + (_, key, value): (LuaValue<'lua>, String, Option), +) -> LuaResult<()> { + // Make sure key is valid, otherwise set_var will panic + if key.is_empty() { + Err(LuaError::RuntimeError("Key must not be empty".to_string())) + } else if key.contains('=') { + Err(LuaError::RuntimeError( + "Key must not contain the equals character '='".to_string(), + )) + } else if key.contains('\0') { + Err(LuaError::RuntimeError( + "Key must not contain the NUL character".to_string(), + )) + } else { + match value { + Some(value) => { + // Make sure value is valid, otherwise set_var will panic + if value.contains('\0') { + Err(LuaError::RuntimeError( + "Value must not contain the NUL character".to_string(), + )) + } else { + env::set_var(&key, &value); + Ok(()) + } + } + None => { + env::remove_var(&key); + Ok(()) + } + } + } +} + +fn process_env_iter<'lua>( + lua: &'lua Lua, + (_, _): (LuaValue<'lua>, ()), +) -> LuaResult> { + let mut vars = env::vars_os().collect::>().into_iter(); + lua.create_function_mut(move |lua, _: ()| match vars.next() { + Some((key, value)) => { + let raw_key = RawOsString::new(key); + let raw_value = RawOsString::new(value); + Ok(( + LuaValue::String(lua.create_string(raw_key.as_raw_bytes())?), + LuaValue::String(lua.create_string(raw_value.as_raw_bytes())?), + )) + } + None => Ok((LuaValue::Nil, LuaValue::Nil)), + }) +} + +async fn process_spawn( + lua: &Lua, + (program, args, options): (String, Option>, ProcessSpawnOptions), +) -> LuaResult { + /* + Spawn the new process in the background, letting the tokio + runtime place it on a different thread if possible / necessary + + Note that we have to use our scheduler here, we can't + use anything like tokio::task::spawn because our lua + scheduler will not drive those futures to completion + */ + let sched = lua + .app_data_ref::<&Scheduler>() + .expect("Lua struct is missing scheduler"); + + let (status, stdout, stderr) = sched + .spawn(spawn_command(program, args, options)) + .await + .expect("Failed to receive result of spawned process")?; + + // NOTE: If an exit code was not given by the child process, + // we default to 1 if it yielded any error output, otherwise 0 + let code = status.code().unwrap_or(match stderr.is_empty() { + true => 0, + false => 1, + }); + + // Construct and return a readonly lua table with results + TableBuilder::new(lua)? + .with_value("ok", code == 0)? + .with_value("code", code)? + .with_value("stdout", lua.create_string(&stdout)?)? + .with_value("stderr", lua.create_string(&stderr)?)? + .build_readonly() +} + +async fn spawn_command( + program: String, + args: Option>, + options: ProcessSpawnOptions, +) -> LuaResult<(ExitStatus, Vec, Vec)> { + let inherit_stdio = options.inherit_stdio; + + let child = options + .into_command(program, args) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + if inherit_stdio { + pipe_and_inherit_child_process_stdio(child).await + } else { + let output = child.wait_with_output().await?; + Ok((output.status, output.stdout, output.stderr)) + } +} diff --git a/src/lune/builtins/process/options.rs b/src/lune/builtins/process/options.rs new file mode 100644 index 0000000..688caa3 --- /dev/null +++ b/src/lune/builtins/process/options.rs @@ -0,0 +1,177 @@ +use std::{ + collections::HashMap, + env::{self}, + path::PathBuf, +}; + +use directories::UserDirs; +use mlua::prelude::*; +use tokio::process::Command; + +#[derive(Debug, Clone, Default)] +pub struct ProcessSpawnOptions { + pub(crate) cwd: Option, + pub(crate) envs: HashMap, + pub(crate) shell: Option, + pub(crate) inherit_stdio: bool, +} + +impl<'lua> FromLua<'lua> for ProcessSpawnOptions { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + let mut this = Self::default(); + let value = match value { + LuaValue::Nil => return Ok(this), + LuaValue::Table(t) => t, + _ => { + return Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "ProcessSpawnOptions", + message: Some(format!( + "Invalid spawn options - expected table, got {}", + value.type_name() + )), + }) + } + }; + + /* + If we got a working directory to use: + + 1. Substitute leading tilde (~) for the users home dir + 2. Make sure it exists + */ + match value.get("cwd")? { + LuaValue::Nil => {} + LuaValue::String(s) => { + let mut cwd = PathBuf::from(s.to_str()?); + if let Ok(stripped) = cwd.strip_prefix("~") { + let user_dirs = UserDirs::new().ok_or_else(|| { + LuaError::runtime( + "Invalid value for option 'cwd' - failed to get home directory", + ) + })?; + cwd = user_dirs.home_dir().join(stripped) + } + if !cwd.exists() { + return Err(LuaError::runtime( + "Invalid value for option 'cwd' - path does not exist", + )); + }; + this.cwd = Some(cwd); + } + value => { + return Err(LuaError::RuntimeError(format!( + "Invalid type for option 'cwd' - expected string, got '{}'", + value.type_name() + ))) + } + } + + /* + If we got environment variables, make sure they are strings + */ + match value.get("env")? { + LuaValue::Nil => {} + LuaValue::Table(e) => { + for pair in e.pairs::() { + let (k, v) = pair.context("Environment variables must be strings")?; + this.envs.insert(k, v); + } + } + value => { + return Err(LuaError::RuntimeError(format!( + "Invalid type for option 'env' - expected table, got '{}'", + value.type_name() + ))) + } + } + + /* + If we got a shell to use: + + 1. When given as a string, use that literally + 2. When set to true, use a default shell for the platform + */ + match value.get("shell")? { + LuaValue::Nil => {} + LuaValue::String(s) => this.shell = Some(s.to_string_lossy().to_string()), + LuaValue::Boolean(true) => { + this.shell = match env::consts::FAMILY { + "unix" => Some("/bin/sh".to_string()), + "windows" => Some("/bin/sh".to_string()), + _ => None, + }; + } + value => { + return Err(LuaError::RuntimeError(format!( + "Invalid type for option 'shell' - expected 'true' or 'string', got '{}'", + value.type_name() + ))) + } + } + + /* + If we got options for stdio handling, make sure its one of the constant values + */ + match value.get("stdio")? { + LuaValue::Nil => {} + LuaValue::String(s) => match s.to_str()? { + "inherit" => this.inherit_stdio = true, + "default" => this.inherit_stdio = false, + _ => { + return Err(LuaError::RuntimeError(format!( + "Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'", + s.to_string_lossy() + ))) + } + }, + value => { + return Err(LuaError::RuntimeError(format!( + "Invalid type for option 'stdio' - expected 'string', got '{}'", + value.type_name() + ))) + } + } + + Ok(this) + } +} + +impl ProcessSpawnOptions { + pub fn into_command(self, program: impl Into, args: Option>) -> Command { + let mut program = program.into(); + + // Run a shell using the command param if wanted + let pargs = match self.shell { + None => args, + Some(shell) => { + let shell_args = match args { + Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))], + None => vec!["-c".to_string(), program.to_string()], + }; + program = shell.to_string(); + Some(shell_args) + } + }; + + // Create command with the wanted options + let mut cmd = match pargs { + None => Command::new(program), + Some(args) => { + let mut cmd = Command::new(program); + cmd.args(args); + cmd + } + }; + + // Set dir to run in and env variables + if let Some(cwd) = self.cwd { + cmd.current_dir(cwd); + } + if !self.envs.is_empty() { + cmd.envs(self.envs); + } + + cmd + } +} diff --git a/src/lune/lua/process/mod.rs b/src/lune/builtins/process/pipe_inherit.rs similarity index 96% rename from src/lune/lua/process/mod.rs rename to src/lune/builtins/process/pipe_inherit.rs index b74cd00..0e4b9a3 100644 --- a/src/lune/lua/process/mod.rs +++ b/src/lune/builtins/process/pipe_inherit.rs @@ -3,8 +3,7 @@ use std::process::ExitStatus; use mlua::prelude::*; use tokio::{io, process::Child, task}; -mod tee_writer; -use tee_writer::AsyncTeeWriter; +use super::tee_writer::AsyncTeeWriter; pub async fn pipe_and_inherit_child_process_stdio( mut child: Child, diff --git a/src/lune/lua/process/tee_writer.rs b/src/lune/builtins/process/tee_writer.rs similarity index 100% rename from src/lune/lua/process/tee_writer.rs rename to src/lune/builtins/process/tee_writer.rs diff --git a/src/lune/builtins/roblox.rs b/src/lune/builtins/roblox/mod.rs similarity index 93% rename from src/lune/builtins/roblox.rs rename to src/lune/builtins/roblox/mod.rs index 2141d5c..9d86927 100644 --- a/src/lune/builtins/roblox.rs +++ b/src/lune/builtins/roblox/mod.rs @@ -1,25 +1,28 @@ use mlua::prelude::*; use once_cell::sync::OnceCell; -use crate::roblox::{ - self, - document::{Document, DocumentError, DocumentFormat, DocumentKind}, - instance::Instance, - reflection::Database as ReflectionDatabase, +use crate::{ + lune::util::TableBuilder, + roblox::{ + self, + document::{Document, DocumentError, DocumentFormat, DocumentKind}, + instance::Instance, + reflection::Database as ReflectionDatabase, + }, }; use tokio::task; -use crate::lune::lua::table::TableBuilder; - static REFLECTION_DATABASE: OnceCell = OnceCell::new(); pub fn create(lua: &'static Lua) -> LuaResult { let mut roblox_constants = Vec::new(); + let roblox_module = roblox::module(lua)?; for pair in roblox_module.pairs::() { roblox_constants.push(pair?); } + TableBuilder::new(lua)? .with_values(roblox_constants)? .with_async_function("deserializePlace", deserialize_place)? diff --git a/src/lune/lua/serde/compress_decompress.rs b/src/lune/builtins/serde/compress_decompress.rs similarity index 100% rename from src/lune/lua/serde/compress_decompress.rs rename to src/lune/builtins/serde/compress_decompress.rs diff --git a/src/lune/lua/serde/encode_decode.rs b/src/lune/builtins/serde/encode_decode.rs similarity index 99% rename from src/lune/lua/serde/encode_decode.rs rename to src/lune/builtins/serde/encode_decode.rs index edbcc1b..1f83ce4 100644 --- a/src/lune/lua/serde/encode_decode.rs +++ b/src/lune/builtins/serde/encode_decode.rs @@ -10,6 +10,7 @@ const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new() .serialize_unit_to_null(false); const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new() + .sort_keys(true) .deny_recursive_tables(false) .deny_unsupported_types(true); diff --git a/src/lune/builtins/serde.rs b/src/lune/builtins/serde/mod.rs similarity index 84% rename from src/lune/builtins/serde.rs rename to src/lune/builtins/serde/mod.rs index 5069337..4e76bce 100644 --- a/src/lune/builtins/serde.rs +++ b/src/lune/builtins/serde/mod.rs @@ -1,11 +1,12 @@ use mlua::prelude::*; -use crate::lune::lua::{ - serde::{ - compress, decompress, CompressDecompressFormat, EncodeDecodeConfig, EncodeDecodeFormat, - }, - table::TableBuilder, -}; +pub(super) mod compress_decompress; +pub(super) mod encode_decode; + +use compress_decompress::{compress, decompress, CompressDecompressFormat}; +use encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat}; + +use crate::lune::util::TableBuilder; pub fn create(lua: &'static Lua) -> LuaResult { TableBuilder::new(lua)? diff --git a/src/lune/builtins/stdio.rs b/src/lune/builtins/stdio/mod.rs similarity index 52% rename from src/lune/builtins/stdio.rs rename to src/lune/builtins/stdio/mod.rs index acd3727..139a643 100644 --- a/src/lune/builtins/stdio.rs +++ b/src/lune/builtins/stdio/mod.rs @@ -1,59 +1,68 @@ -use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select}; use mlua::prelude::*; + +use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select}; use tokio::{ io::{self, AsyncWriteExt}, task, }; -use crate::lune::lua::{ - stdio::{ - formatting::{ - format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str, - }, - prompt::{PromptKind, PromptOptions, PromptResult}, +use crate::lune::util::{ + formatting::{ + format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str, }, - table::TableBuilder, + TableBuilder, }; -pub fn create(lua: &'static Lua) -> LuaResult { +mod prompt; +use prompt::{PromptKind, PromptOptions, PromptResult}; + +pub fn create(lua: &'static Lua) -> LuaResult> { TableBuilder::new(lua)? - .with_function("color", |_, color: String| { - let ansi_string = format_style(style_from_color_str(&color)?); - Ok(ansi_string) - })? - .with_function("style", |_, style: String| { - let ansi_string = format_style(style_from_style_str(&style)?); - Ok(ansi_string) - })? - .with_function("format", |_, args: LuaMultiValue| { - pretty_format_multi_value(&args) - })? - .with_async_function("write", |_, s: LuaString| async move { - let mut stdout = io::stdout(); - stdout.write_all(s.as_bytes()).await?; - stdout.flush().await?; - Ok(()) - })? - .with_async_function("ewrite", |_, s: LuaString| async move { - let mut stderr = io::stderr(); - stderr.write_all(s.as_bytes()).await?; - stderr.flush().await?; - Ok(()) - })? - .with_async_function("prompt", |_, options: PromptOptions| async move { - task::spawn_blocking(move || prompt(options)) - .await - .into_lua_err()? - })? + .with_function("color", stdio_color)? + .with_function("style", stdio_style)? + .with_function("format", stdio_format)? + .with_async_function("write", stdio_write)? + .with_async_function("ewrite", stdio_ewrite)? + .with_async_function("prompt", stdio_prompt)? .build_readonly() } -fn prompt_theme() -> ColorfulTheme { - ColorfulTheme::default() +fn stdio_color(_: &Lua, color: String) -> LuaResult { + let ansi_string = format_style(style_from_color_str(&color)?); + Ok(ansi_string) +} + +fn stdio_style(_: &Lua, color: String) -> LuaResult { + let ansi_string = format_style(style_from_style_str(&color)?); + Ok(ansi_string) +} + +fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult { + pretty_format_multi_value(&args) +} + +async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { + let mut stdout = io::stdout(); + stdout.write_all(s.as_bytes()).await?; + stdout.flush().await?; + Ok(()) +} + +async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> { + let mut stderr = io::stderr(); + stderr.write_all(s.as_bytes()).await?; + stderr.flush().await?; + Ok(()) +} + +async fn stdio_prompt(_: &Lua, options: PromptOptions) -> LuaResult { + task::spawn_blocking(move || prompt(options)) + .await + .into_lua_err()? } fn prompt(options: PromptOptions) -> LuaResult { - let theme = prompt_theme(); + let theme = ColorfulTheme::default(); match options.kind { PromptKind::Text => { let input: String = Input::with_theme(&theme) @@ -74,7 +83,7 @@ fn prompt(options: PromptOptions) -> LuaResult { Ok(PromptResult::Boolean(result)) } PromptKind::Select => { - let chosen = Select::with_theme(&prompt_theme()) + let chosen = Select::with_theme(&theme) .with_prompt(&options.text.unwrap_or_default()) .items(&options.options.expect("Missing options in prompt options")) .interact_opt()?; @@ -84,7 +93,7 @@ fn prompt(options: PromptOptions) -> LuaResult { }) } PromptKind::MultiSelect => { - let chosen = MultiSelect::with_theme(&prompt_theme()) + let chosen = MultiSelect::with_theme(&theme) .with_prompt(&options.text.unwrap_or_default()) .items(&options.options.expect("Missing options in prompt options")) .interact_opt()?; diff --git a/src/lune/lua/stdio/prompt.rs b/src/lune/builtins/stdio/prompt.rs similarity index 100% rename from src/lune/lua/stdio/prompt.rs rename to src/lune/builtins/stdio/prompt.rs diff --git a/src/lune/builtins/task.rs b/src/lune/builtins/task.rs deleted file mode 100644 index 98a0383..0000000 --- a/src/lune/builtins/task.rs +++ /dev/null @@ -1,168 +0,0 @@ -use mlua::prelude::*; - -use crate::lune::lua::{ - async_ext::LuaAsyncExt, - table::TableBuilder, - task::{ - LuaThreadOrFunction, LuaThreadOrTaskReference, TaskKind, TaskReference, TaskScheduler, - TaskSchedulerScheduleExt, - }, -}; - -const SPAWN_IMPL_LUA: &str = r#" -scheduleNext(thread()) -local task = scheduleNext(...) -yield() -return task -"#; - -pub fn create(lua: &'static Lua) -> LuaResult> { - lua.app_data_ref::<&TaskScheduler>() - .expect("Missing task scheduler in app data"); - /* - 1. Schedule the current thread at the front - 2. Schedule the wanted task arg at the front, - the previous schedule now comes right after - 3. Give control over to the scheduler, which will - resume the above tasks in order when its ready - - The spawn function needs special treatment, - we need to yield right away to allow the - spawned task to run until first yield - */ - let task_spawn_env_yield: LuaFunction = lua.named_registry_value("co.yield")?; - let task_spawn = lua - .load(SPAWN_IMPL_LUA) - .set_name("task.spawn") - .set_environment( - TableBuilder::new(lua)? - .with_function("thread", |lua, _: ()| Ok(lua.current_thread()))? - .with_value("yield", task_spawn_env_yield)? - .with_function( - "scheduleNext", - |lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| { - let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); - sched.schedule_blocking(tof.into_thread(lua)?, args) - }, - )? - .build_readonly()?, - ) - .into_function()?; - // Functions in the built-in coroutine library also need to be - // replaced, these are a bit different than the ones above because - // calling resume or the function that wrap returns must return - // whatever lua value(s) that the thread or task yielded back - let globals = lua.globals(); - let coroutine = globals.get::<_, LuaTable>("coroutine")?; - coroutine.set("status", lua.create_function(coroutine_status)?)?; - coroutine.set("resume", lua.create_function(coroutine_resume)?)?; - coroutine.set("wrap", lua.create_function(coroutine_wrap)?)?; - // All good, return the task scheduler lib - TableBuilder::new(lua)? - .with_value("wait", lua.create_waiter_function()?)? - .with_value("spawn", task_spawn)? - .with_function("cancel", task_cancel)? - .with_function("defer", task_defer)? - .with_function("delay", task_delay)? - .build_readonly() -} - -/* - Basic task functions -*/ - -fn task_cancel(lua: &Lua, task: LuaUserDataRef) -> LuaResult<()> { - let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); - sched.remove_task(*task)?; - Ok(()) -} - -fn task_defer( - lua: &Lua, - (tof, args): (LuaThreadOrFunction, LuaMultiValue), -) -> LuaResult { - let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); - sched.schedule_blocking_deferred(tof.into_thread(lua)?, args) -} - -fn task_delay( - lua: &Lua, - (secs, tof, args): (f64, LuaThreadOrFunction, LuaMultiValue), -) -> LuaResult { - let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); - sched.schedule_blocking_after_seconds(secs, tof.into_thread(lua)?, args) -} - -/* - Coroutine library overrides for compat with task scheduler -*/ - -fn coroutine_status<'a>( - lua: &'a Lua, - value: LuaThreadOrTaskReference<'a>, -) -> LuaResult> { - Ok(match value { - LuaThreadOrTaskReference::Thread(thread) => { - let get_status: LuaFunction = lua.named_registry_value("co.status")?; - get_status.call(thread)? - } - LuaThreadOrTaskReference::TaskReference(task) => { - let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); - sched - .get_task_status(task) - .unwrap_or_else(|| lua.create_string("dead").unwrap()) - } - }) -} - -fn coroutine_resume<'lua>( - lua: &'lua Lua, - (value, arguments): (LuaThreadOrTaskReference, LuaMultiValue<'lua>), -) -> LuaResult<(bool, LuaMultiValue<'lua>)> { - let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); - if sched.current_task().is_none() { - return Err(LuaError::RuntimeError( - "No current task to inherit".to_string(), - )); - } - let current = sched.current_task().unwrap(); - let result = match value { - LuaThreadOrTaskReference::Thread(t) => { - let task = sched.create_task(TaskKind::Instant, t, Some(arguments), true)?; - sched.resume_task(task, None) - } - LuaThreadOrTaskReference::TaskReference(t) => sched.resume_task(t, Some(Ok(arguments))), - }; - sched.force_set_current_task(Some(current)); - match result { - Ok(rets) => Ok((true, rets.1)), - Err(e) => Ok((false, e.into_lua_multi(lua)?)), - } -} - -fn coroutine_wrap<'lua>(lua: &'lua Lua, func: LuaFunction) -> LuaResult> { - let task = lua.app_data_ref::<&TaskScheduler>().unwrap().create_task( - TaskKind::Instant, - lua.create_thread(func)?, - None, - false, - )?; - lua.create_function(move |lua, args: LuaMultiValue| { - let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); - if sched.current_task().is_none() { - return Err(LuaError::RuntimeError( - "No current task to inherit".to_string(), - )); - } - let current = sched.current_task().unwrap(); - let result = lua - .app_data_ref::<&TaskScheduler>() - .unwrap() - .resume_task(task, Some(Ok(args))); - sched.force_set_current_task(Some(current)); - match result { - Ok(rets) => Ok(rets.1), - Err(e) => Err(e), - } - }) -} diff --git a/src/lune/builtins/task/mod.rs b/src/lune/builtins/task/mod.rs new file mode 100644 index 0000000..189df32 --- /dev/null +++ b/src/lune/builtins/task/mod.rs @@ -0,0 +1,126 @@ +use std::time::Duration; + +use mlua::prelude::*; + +use tokio::time::{self, Instant}; + +use crate::lune::{scheduler::Scheduler, util::TableBuilder}; + +mod tof; +use tof::LuaThreadOrFunction; + +/* + The spawn function needs special treatment, + we need to yield right away to allow the + spawned task to run until first yield + + 1. Schedule this current thread at the front + 2. Schedule given thread/function at the front, + the previous schedule now comes right after + 3. Give control over to the scheduler, which will + resume the above tasks in order when its ready +*/ +const SPAWN_IMPL_LUA: &str = r#" +push(currentThread()) +local thread = push(...) +yield() +return thread +"#; + +pub fn create(lua: &'static Lua) -> LuaResult> { + let coroutine_running = lua + .globals() + .get::<_, LuaTable>("coroutine")? + .get::<_, LuaFunction>("running")?; + let coroutine_yield = lua + .globals() + .get::<_, LuaTable>("coroutine")? + .get::<_, LuaFunction>("yield")?; + let push_front = + lua.create_function(|lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| { + let thread = tof.into_thread(lua)?; + let sched = lua + .app_data_ref::<&Scheduler>() + .expect("Lua struct is missing scheduler"); + sched.push_front(lua, thread.clone(), args)?; + Ok(thread) + })?; + let task_spawn_env = TableBuilder::new(lua)? + .with_value("currentThread", coroutine_running)? + .with_value("yield", coroutine_yield)? + .with_value("push", push_front)? + .build_readonly()?; + let task_spawn = lua + .load(SPAWN_IMPL_LUA) + .set_name("task.spawn") + .set_environment(task_spawn_env) + .into_function()?; + + TableBuilder::new(lua)? + .with_function("cancel", task_cancel)? + .with_function("defer", task_defer)? + .with_function("delay", task_delay)? + .with_value("spawn", task_spawn)? + .with_async_function("wait", task_wait)? + .build_readonly() +} + +fn task_cancel(lua: &Lua, thread: LuaThread) -> LuaResult<()> { + let close = lua + .globals() + .get::<_, LuaTable>("coroutine")? + .get::<_, LuaFunction>("close")?; + match close.call(thread) { + Err(LuaError::CoroutineInactive) => Ok(()), + Err(e) => Err(e), + Ok(()) => Ok(()), + } +} + +fn task_defer<'lua>( + lua: &'lua Lua, + (tof, args): (LuaThreadOrFunction<'lua>, LuaMultiValue<'_>), +) -> LuaResult> { + let thread = tof.into_thread(lua)?; + let sched = lua + .app_data_ref::<&Scheduler>() + .expect("Lua struct is missing scheduler"); + sched.push_back(lua, thread.clone(), args)?; + Ok(thread) +} + +// FIXME: `self` escapes outside of method because we are borrowing `tof` and +// `args` when we call `schedule_future_thread` in the lua function body below +// For now we solve this by using the 'static lifetime bound in the impl +fn task_delay<'lua>( + lua: &'lua Lua, + (secs, tof, args): (f64, LuaThreadOrFunction<'lua>, LuaMultiValue<'lua>), +) -> LuaResult> +where + 'lua: 'static, +{ + let thread = tof.into_thread(lua)?; + let sched = lua + .app_data_ref::<&Scheduler>() + .expect("Lua struct is missing scheduler"); + + let thread2 = thread.clone(); + sched.spawn_thread(lua, thread.clone(), async move { + let duration = Duration::from_secs_f64(secs); + time::sleep(duration).await; + sched.push_back(lua, thread2, args)?; + Ok(()) + })?; + + Ok(thread) +} + +async fn task_wait(_: &Lua, secs: Option) -> LuaResult { + let duration = Duration::from_secs_f64(secs.unwrap_or_default()); + + let before = Instant::now(); + time::sleep(duration).await; + let after = Instant::now(); + + Ok((after - before).as_secs_f64()) +} diff --git a/src/lune/builtins/task/tof.rs b/src/lune/builtins/task/tof.rs new file mode 100644 index 0000000..e63cd2b --- /dev/null +++ b/src/lune/builtins/task/tof.rs @@ -0,0 +1,30 @@ +use mlua::prelude::*; + +#[derive(Clone)] +pub(super) enum LuaThreadOrFunction<'lua> { + Thread(LuaThread<'lua>), + Function(LuaFunction<'lua>), +} + +impl<'lua> LuaThreadOrFunction<'lua> { + pub(super) fn into_thread(self, lua: &'lua Lua) -> LuaResult> { + match self { + Self::Thread(t) => Ok(t), + Self::Function(f) => lua.create_thread(f), + } + } +} + +impl<'lua> FromLua<'lua> for LuaThreadOrFunction<'lua> { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + match value { + LuaValue::Thread(t) => Ok(Self::Thread(t)), + LuaValue::Function(f) => Ok(Self::Function(f)), + value => Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "LuaThreadOrFunction", + message: Some("Expected thread or function".to_string()), + }), + } + } +} diff --git a/src/lune/builtins/top_level.rs b/src/lune/builtins/top_level.rs deleted file mode 100644 index a101fb0..0000000 --- a/src/lune/builtins/top_level.rs +++ /dev/null @@ -1,80 +0,0 @@ -use mlua::prelude::*; -use std::io::{self, Write as _}; - -#[cfg(feature = "roblox")] -use crate::roblox::datatypes::extension::RobloxUserdataTypenameExt; - -use crate::lune::lua::{ - stdio::formatting::{format_label, pretty_format_multi_value}, - task::TaskReference, -}; - -pub fn print(_: &Lua, args: LuaMultiValue) -> LuaResult<()> { - let formatted = format!("{}\n", pretty_format_multi_value(&args)?); - let mut stdout = io::stdout(); - stdout.write_all(formatted.as_bytes())?; - stdout.flush()?; - Ok(()) -} - -pub fn warn(_: &Lua, args: LuaMultiValue) -> LuaResult<()> { - let formatted = format!( - "{}\n{}", - format_label("warn"), - pretty_format_multi_value(&args)? - ); - let mut stdout = io::stdout(); - stdout.write_all(formatted.as_bytes())?; - stdout.flush()?; - Ok(()) -} - -// HACK: We need to preserve the default behavior of -// the lua error function, for pcall and such, which -// is really tricky to do from scratch so we will -// just proxy the default function here instead - -pub fn error(lua: &Lua, (arg, level): (LuaValue, Option)) -> LuaResult<()> { - let error: LuaFunction = lua.named_registry_value("error")?; - let trace: LuaFunction = lua.named_registry_value("dbg.trace")?; - error.call(( - LuaError::CallbackError { - traceback: format!("override traceback:{}", trace.call::<_, String>(())?), - cause: LuaError::external(format!( - "{}\n{}", - format_label("error"), - pretty_format_multi_value(&arg.into_lua_multi(lua)?)? - )) - .into(), - }, - level, - ))?; - Ok(()) -} - -pub fn proxy_type<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult> { - if let LuaValue::UserData(u) = &value { - if u.is::() { - return lua.create_string("thread"); - } - } - lua.named_registry_value::("type")?.call(value) -} - -pub fn proxy_typeof<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult> { - if let LuaValue::UserData(u) = &value { - if u.is::() { - return lua.create_string("thread"); - } - #[cfg(feature = "roblox")] - { - if let Some(type_name) = u.roblox_type_name() { - return lua.create_string(type_name); - } - } - } - lua.named_registry_value::("typeof")? - .call(value) -} - -// TODO: Add an override for tostring that formats errors in a nicer way diff --git a/src/lune/error.rs b/src/lune/error.rs index c4d9f8c..bde101a 100644 --- a/src/lune/error.rs +++ b/src/lune/error.rs @@ -5,7 +5,7 @@ use std::{ use mlua::prelude::*; -use crate::lune::lua::stdio::formatting::pretty_format_luau_error; +use crate::lune::util::formatting::pretty_format_luau_error; /** An opaque error type for formatted lua errors. @@ -16,6 +16,8 @@ pub struct LuneError { disable_colors: bool, } +// TODO: Rename this struct to "RuntimeError" instead for +// the next breaking release, it's a more fitting name impl LuneError { /** Enables colorization of the error message when formatted using the [`Display`] trait. @@ -64,6 +66,15 @@ impl From for LuneError { } } +impl From<&LuaError> for LuneError { + fn from(value: &LuaError) -> Self { + Self { + error: value.clone(), + disable_colors: false, + } + } +} + impl Display for LuneError { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { write!( diff --git a/src/lune/globals/g_table.rs b/src/lune/globals/g_table.rs new file mode 100644 index 0000000..8c007c8 --- /dev/null +++ b/src/lune/globals/g_table.rs @@ -0,0 +1,5 @@ +use mlua::prelude::*; + +pub fn create(lua: &Lua) -> LuaResult> { + lua.create_table() +} diff --git a/src/lune/globals/mod.rs b/src/lune/globals/mod.rs new file mode 100644 index 0000000..48e1deb --- /dev/null +++ b/src/lune/globals/mod.rs @@ -0,0 +1,28 @@ +use mlua::prelude::*; + +use super::util::TableBuilder; + +mod g_table; +mod print; +mod require; +mod r#typeof; +mod version; +mod warn; + +pub fn inject_all(lua: &'static Lua) -> LuaResult<()> { + let all = TableBuilder::new(lua)? + .with_value("_G", g_table::create(lua)?)? + .with_value("_VERSION", version::create(lua)?)? + .with_value("print", print::create(lua)?)? + .with_value("require", require::create(lua)?)? + .with_value("typeof", r#typeof::create(lua)?)? + .with_value("warn", warn::create(lua)?)? + .build_readonly()?; + + for res in all.pairs() { + let (key, value): (LuaValue, LuaValue) = res.unwrap(); + lua.globals().set(key, value)?; + } + + Ok(()) +} diff --git a/src/lune/globals/print.rs b/src/lune/globals/print.rs new file mode 100644 index 0000000..57b849b --- /dev/null +++ b/src/lune/globals/print.rs @@ -0,0 +1,14 @@ +use mlua::prelude::*; +use tokio::io::{self, AsyncWriteExt}; + +use crate::lune::{scheduler::LuaSchedulerExt, util::formatting::pretty_format_multi_value}; + +pub fn create(lua: &'static Lua) -> LuaResult> { + lua.create_async_function(|_, args: LuaMultiValue| async move { + let formatted = format!("{}\n", pretty_format_multi_value(&args)?); + let mut stdout = io::stdout(); + stdout.write_all(formatted.as_bytes()).await?; + stdout.flush().await?; + Ok(()) + }) +} diff --git a/src/lune/globals/require/alias.rs b/src/lune/globals/require/alias.rs new file mode 100644 index 0000000..f8fd15e --- /dev/null +++ b/src/lune/globals/require/alias.rs @@ -0,0 +1,16 @@ +use mlua::prelude::*; + +use super::context::*; + +pub(super) async fn require<'lua, 'ctx>( + _ctx: &'ctx RequireContext<'lua>, + alias: &str, + name: &str, +) -> LuaResult> +where + 'lua: 'ctx, +{ + Err(LuaError::runtime(format!( + "TODO: Support require for built-in libraries (tried to require '{name}' with alias '{alias}')" + ))) +} diff --git a/src/lune/globals/require/builtin.rs b/src/lune/globals/require/builtin.rs new file mode 100644 index 0000000..c4550dd --- /dev/null +++ b/src/lune/globals/require/builtin.rs @@ -0,0 +1,14 @@ +use mlua::prelude::*; + +use super::context::*; + +pub(super) async fn require<'lua, 'ctx>( + ctx: &'ctx RequireContext<'lua>, + name: &str, +) -> LuaResult> +where + 'lua: 'ctx, + 'lua: 'static, // FIXME: Remove static lifetime bound here when builtin libraries no longer need it +{ + ctx.load_builtin(name) +} diff --git a/src/lune/globals/require/context.rs b/src/lune/globals/require/context.rs new file mode 100644 index 0000000..5f17f00 --- /dev/null +++ b/src/lune/globals/require/context.rs @@ -0,0 +1,310 @@ +use std::{ + collections::HashMap, + env, + path::{Path, PathBuf}, + sync::Arc, +}; + +use mlua::prelude::*; +use tokio::{ + fs, + sync::{ + broadcast::{self, Sender}, + Mutex as AsyncMutex, + }, +}; + +use crate::lune::{ + builtins::LuneBuiltin, + scheduler::{IntoLuaThread, Scheduler}, +}; + +/** + Context containing cached results for all `require` operations. + + The cache uses absolute paths, so any given relative + path will first be transformed into an absolute path. +*/ +#[derive(Debug, Clone)] +pub(super) struct RequireContext<'lua> { + lua: &'lua Lua, + use_cwd_relative_paths: bool, + working_directory: PathBuf, + cache_builtins: Arc>>>, + cache_results: Arc>>>, + cache_pending: Arc>>>, +} + +impl<'lua> RequireContext<'lua> { + /** + Creates a new require context for the given [`Lua`] struct. + + Note that this require context is global and only one require + context should be created per [`Lua`] struct, creating more + than one context may lead to undefined require-behavior. + */ + pub fn new(lua: &'lua Lua) -> Self { + // FUTURE: We could load some kind of config or env var + // to check if we should be using cwd-relative paths + let cwd = env::current_dir().expect("Failed to get current working directory"); + Self { + lua, + use_cwd_relative_paths: false, + working_directory: cwd, + cache_builtins: Arc::new(AsyncMutex::new(HashMap::new())), + cache_results: Arc::new(AsyncMutex::new(HashMap::new())), + cache_pending: Arc::new(AsyncMutex::new(HashMap::new())), + } + } + + /** + Resolves the given `source` and `path` into require paths + to use, based on the current require context settings. + + This will resolve path segments such as `./`, `../`, ..., and + if the resolved path is not an absolute path, will create an + absolute path by prepending the current working directory. + */ + pub fn resolve_paths( + &self, + source: impl AsRef, + path: impl AsRef, + ) -> LuaResult<(PathBuf, PathBuf)> { + let path = if self.use_cwd_relative_paths { + PathBuf::from(path.as_ref()) + } else { + PathBuf::from(source.as_ref()) + .parent() + .ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))? + .join(path.as_ref()) + }; + + let rel_path = path_clean::clean(path); + let abs_path = if rel_path.is_absolute() { + rel_path.to_path_buf() + } else { + self.working_directory.join(&rel_path) + }; + + Ok((rel_path, abs_path)) + } + + /** + Checks if the given path has a cached require result. + */ + pub fn is_cached(&self, abs_path: impl AsRef) -> LuaResult { + let is_cached = self + .cache_results + .try_lock() + .expect("RequireContext may not be used from multiple threads") + .contains_key(abs_path.as_ref()); + Ok(is_cached) + } + + /** + Checks if the given path is currently being used in `require`. + */ + pub fn is_pending(&self, abs_path: impl AsRef) -> LuaResult { + let is_pending = self + .cache_pending + .try_lock() + .expect("RequireContext may not be used from multiple threads") + .contains_key(abs_path.as_ref()); + Ok(is_pending) + } + + /** + Gets the resulting value from the require cache. + + Will panic if the path has not been cached, use [`is_cached`] first. + */ + pub fn get_from_cache(&self, abs_path: impl AsRef) -> LuaResult> { + let results = self + .cache_results + .try_lock() + .expect("RequireContext may not be used from multiple threads"); + + let cached = results + .get(abs_path.as_ref()) + .expect("Path does not exist in results cache"); + match cached { + Err(e) => Err(e.clone()), + Ok(k) => { + let multi_vec = self + .lua + .registry_value::>(k) + .expect("Missing require result in lua registry"); + Ok(LuaMultiValue::from_vec(multi_vec)) + } + } + } + + /** + Waits for the resulting value from the require cache. + + Will panic if the path has not been cached, use [`is_cached`] first. + */ + pub async fn wait_for_cache( + &self, + abs_path: impl AsRef, + ) -> LuaResult> { + let mut thread_recv = { + let pending = self + .cache_pending + .try_lock() + .expect("RequireContext may not be used from multiple threads"); + let thread_id = pending + .get(abs_path.as_ref()) + .expect("Path is not currently pending require"); + thread_id.subscribe() + }; + + thread_recv.recv().await.into_lua_err()?; + + self.get_from_cache(abs_path.as_ref()) + } + + async fn load( + &self, + abs_path: impl AsRef, + rel_path: impl AsRef, + ) -> LuaResult { + let abs_path = abs_path.as_ref(); + let rel_path = rel_path.as_ref(); + + let sched = self + .lua + .app_data_ref::<&Scheduler>() + .expect("Lua struct is missing scheduler"); + + // Read the file at the given path, try to parse and + // load it into a new lua thread that we can schedule + let file_contents = fs::read(&abs_path).await?; + let file_thread = self + .lua + .load(file_contents) + .set_name(rel_path.to_string_lossy().to_string()) + .into_function()? + .into_lua_thread(self.lua)?; + + // Schedule the thread to run, wait for it to finish running + let thread_id = sched.push_back(self.lua, file_thread, ())?; + let thread_res = sched.wait_for_thread(self.lua, thread_id).await; + + // Return the result of the thread, storing any lua value(s) in the registry + match thread_res { + Err(e) => Err(e), + Ok(v) => { + let multi_vec = v.into_vec(); + let multi_key = self + .lua + .create_registry_value(multi_vec) + .expect("Failed to store require result in registry - out of memory"); + Ok(multi_key) + } + } + } + + /** + Loads (requires) the file at the given path. + */ + pub async fn load_with_caching( + &self, + abs_path: impl AsRef, + rel_path: impl AsRef, + ) -> LuaResult> { + let abs_path = abs_path.as_ref(); + let rel_path = rel_path.as_ref(); + + // Set this abs path as currently pending + let (broadcast_tx, _) = broadcast::channel(1); + self.cache_pending + .try_lock() + .expect("RequireContext may not be used from multiple threads") + .insert(abs_path.to_path_buf(), broadcast_tx); + + // Try to load at this abs path + let load_res = self.load(abs_path, rel_path).await; + let load_val = match &load_res { + Err(e) => Err(e.clone()), + Ok(k) => { + let multi_vec = self + .lua + .registry_value::>(k) + .expect("Failed to fetch require result from registry"); + Ok(LuaMultiValue::from_vec(multi_vec)) + } + }; + + // NOTE: We use the async lock and not try_lock here because + // some other thread may be wanting to insert into the require + // cache at the same time, and that's not an actual error case + self.cache_results + .lock() + .await + .insert(abs_path.to_path_buf(), load_res); + + // Remove the pending thread id from the require context, + // broadcast a message to let any listeners know that this + // path has now finished the require process and is cached + let broadcast_tx = self + .cache_pending + .try_lock() + .expect("RequireContext may not be used from multiple threads") + .remove(abs_path) + .expect("Pending require broadcaster was unexpectedly removed"); + broadcast_tx.send(()).ok(); + + load_val + } + + /** + Loads (requires) the builtin with the given name. + */ + pub fn load_builtin(&self, name: impl AsRef) -> LuaResult> + where + 'lua: 'static, // FIXME: Remove static lifetime bound here when builtin libraries no longer need it + { + let builtin: LuneBuiltin = match name.as_ref().parse() { + Err(e) => return Err(LuaError::runtime(e)), + Ok(b) => b, + }; + + let mut cache = self + .cache_builtins + .try_lock() + .expect("RequireContext may not be used from multiple threads"); + + if let Some(res) = cache.get(&builtin) { + return match res { + Err(e) => return Err(e.clone()), + Ok(key) => { + let multi_vec = self + .lua + .registry_value::>(key) + .expect("Missing builtin result in lua registry"); + Ok(LuaMultiValue::from_vec(multi_vec)) + } + }; + }; + + let result = builtin.create(self.lua); + + cache.insert( + builtin, + match result.clone() { + Err(e) => Err(e), + Ok(multi) => { + let multi_vec = multi.into_vec(); + let multi_key = self + .lua + .create_registry_value(multi_vec) + .expect("Failed to store require result in registry - out of memory"); + Ok(multi_key) + } + }, + ); + + result + } +} diff --git a/src/lune/globals/require/mod.rs b/src/lune/globals/require/mod.rs new file mode 100644 index 0000000..f54ab21 --- /dev/null +++ b/src/lune/globals/require/mod.rs @@ -0,0 +1,98 @@ +use mlua::prelude::*; + +use crate::lune::{scheduler::LuaSchedulerExt, util::TableBuilder}; + +mod context; +use context::RequireContext; + +mod alias; +mod builtin; +mod path; + +const REQUIRE_IMPL: &str = r#" +return require(source(), ...) +"#; + +pub fn create(lua: &'static Lua) -> LuaResult> { + lua.set_app_data(RequireContext::new(lua)); + + /* + Require implementation needs a few workarounds: + + - Async functions run outside of the lua resumption cycle, + so the current lua thread, as well as its stack/debug info + is not available, meaning we have to use a normal function + + - Using the async require function directly in another lua function + would mean yielding across the metamethod/c-call boundary, meaning + we have to first load our two functions into a normal lua chunk + and then load that new chunk into our final require function + + Also note that we inspect the stack at level 2: + + 1. The current c / rust function + 2. The wrapper lua chunk defined above + 3. The lua chunk we are require-ing from + */ + + let require_fn = lua.create_async_function(require)?; + let get_source_fn = lua.create_function(move |lua, _: ()| match lua.inspect_stack(2) { + None => Err(LuaError::runtime( + "Failed to get stack info for require source", + )), + Some(info) => match info.source().source { + None => Err(LuaError::runtime( + "Stack info is missing source for require", + )), + Some(source) => lua.create_string(source.as_bytes()), + }, + })?; + + let require_env = TableBuilder::new(lua)? + .with_value("source", get_source_fn)? + .with_value("require", require_fn)? + .build_readonly()?; + + lua.load(REQUIRE_IMPL) + .set_name("require") + .set_environment(require_env) + .into_function() +} + +async fn require<'lua>( + lua: &'lua Lua, + (source, path): (LuaString<'lua>, LuaString<'lua>), +) -> LuaResult> +where + 'lua: 'static, // FIXME: Remove static lifetime bound here when builtin libraries no longer need it +{ + let source = source + .to_str() + .into_lua_err() + .context("Failed to parse require source as string")? + .to_string(); + + let path = path + .to_str() + .into_lua_err() + .context("Failed to parse require path as string")? + .to_string(); + + let context = lua + .app_data_ref() + .expect("Failed to get RequireContext from app data"); + + if let Some(builtin_name) = path + .strip_prefix("@lune/") + .map(|name| name.to_ascii_lowercase()) + { + builtin::require(&context, &builtin_name).await + } else if let Some(aliased_path) = path.strip_prefix('@') { + let (alias, name) = aliased_path.split_once('/').ok_or(LuaError::runtime( + "Require with custom alias must contain '/' delimiter", + ))?; + alias::require(&context, alias, name).await + } else { + path::require(&context, &source, &path).await + } +} diff --git a/src/lune/globals/require/path.rs b/src/lune/globals/require/path.rs new file mode 100644 index 0000000..0e443fd --- /dev/null +++ b/src/lune/globals/require/path.rs @@ -0,0 +1,98 @@ +use std::path::{Path, PathBuf}; + +use mlua::prelude::*; + +use super::context::*; + +pub(super) async fn require<'lua, 'ctx>( + ctx: &'ctx RequireContext<'lua>, + source: &str, + path: &str, +) -> LuaResult> +where + 'lua: 'ctx, +{ + let (abs_path, rel_path) = ctx.resolve_paths(source, path)?; + + // 1. Try to require the exact path + if let Ok(res) = require_inner(ctx, &abs_path, &rel_path).await { + return Ok(res); + } + + // 2. Try to require the path with an added "luau" extension + let (luau_abs_path, luau_rel_path) = ( + append_extension(&abs_path, "luau"), + append_extension(&rel_path, "luau"), + ); + if let Ok(res) = require_inner(ctx, &luau_abs_path, &luau_rel_path).await { + return Ok(res); + } + + // 3. Try to require the path with an added "lua" extension + let (lua_abs_path, lua_rel_path) = ( + append_extension(&abs_path, "lua"), + append_extension(&rel_path, "lua"), + ); + if let Ok(res) = require_inner(ctx, &lua_abs_path, &lua_rel_path).await { + return Ok(res); + } + + // We didn't find any direct file paths, look + // for directories with "init" files in them... + let abs_init = abs_path.join("init"); + let rel_init = rel_path.join("init"); + + // 4. Try to require the init path with an added "luau" extension + let (luau_abs_init, luau_rel_init) = ( + append_extension(&abs_init, "luau"), + append_extension(&rel_init, "luau"), + ); + if let Ok(res) = require_inner(ctx, &luau_abs_init, &luau_rel_init).await { + return Ok(res); + } + + // 5. Try to require the init path with an added "lua" extension + let (lua_abs_init, lua_rel_init) = ( + append_extension(&abs_init, "lua"), + append_extension(&rel_init, "lua"), + ); + if let Ok(res) = require_inner(ctx, &lua_abs_init, &lua_rel_init).await { + return Ok(res); + } + + // Nothing left to try, throw an error + Err(LuaError::runtime(format!( + "No file exist at the path '{}'", + rel_path.display() + ))) +} + +async fn require_inner<'lua, 'ctx>( + ctx: &'ctx RequireContext<'lua>, + abs_path: impl AsRef, + rel_path: impl AsRef, +) -> LuaResult> +where + 'lua: 'ctx, +{ + let abs_path = abs_path.as_ref(); + let rel_path = rel_path.as_ref(); + + if ctx.is_cached(abs_path)? { + ctx.get_from_cache(abs_path) + } else if ctx.is_pending(abs_path)? { + ctx.wait_for_cache(&abs_path).await + } else { + ctx.load_with_caching(&abs_path, &rel_path).await + } +} + +fn append_extension(path: impl Into, ext: &'static str) -> PathBuf { + let mut new = path.into(); + match new.extension() { + // FUTURE: There's probably a better way to do this than converting to a lossy string + Some(e) => new.set_extension(format!("{}.{ext}", e.to_string_lossy())), + None => new.set_extension(ext), + }; + new +} diff --git a/src/lune/globals/typeof.rs b/src/lune/globals/typeof.rs new file mode 100644 index 0000000..f4f4631 --- /dev/null +++ b/src/lune/globals/typeof.rs @@ -0,0 +1,27 @@ +use mlua::prelude::*; + +use crate::roblox::datatypes::extension::RobloxUserdataTypenameExt; + +const REGISTRY_KEY: &str = "LuauTypeof"; + +pub fn create(lua: &Lua) -> LuaResult> { + let original = lua.globals().get::<_, LuaFunction>("typeof")?; + #[cfg(feature = "roblox")] + { + lua.set_named_registry_value(REGISTRY_KEY, original) + .expect("Failed to store typeof function in registry"); + lua.create_function(|lua, value: LuaValue| { + if let LuaValue::UserData(u) = &value { + if let Some(type_name) = u.roblox_type_name() { + return lua.create_string(type_name); + } + } + let original_fn: LuaFunction = lua + .named_registry_value(REGISTRY_KEY) + .expect("Missing typeof function in registry"); + original_fn.call(value) + }) + } + #[cfg(not(feature = "roblox"))] + original +} diff --git a/src/lune/globals/version.rs b/src/lune/globals/version.rs new file mode 100644 index 0000000..3f72b5c --- /dev/null +++ b/src/lune/globals/version.rs @@ -0,0 +1,24 @@ +use mlua::prelude::*; + +pub fn create(lua: &Lua) -> LuaResult> { + let luau_version_full = lua + .globals() + .get::<_, LuaString>("_VERSION") + .expect("Missing _VERSION global"); + + let luau_version = luau_version_full + .to_str()? + .strip_prefix("Luau 0.") + .expect("_VERSION global is formatted incorrectly") + .trim(); + + if luau_version.is_empty() { + panic!("_VERSION global is missing version number") + } + + lua.create_string(format!( + "Lune {lune}+{luau}", + lune = env!("CARGO_PKG_VERSION"), + luau = luau_version, + )) +} diff --git a/src/lune/globals/warn.rs b/src/lune/globals/warn.rs new file mode 100644 index 0000000..ba98b8a --- /dev/null +++ b/src/lune/globals/warn.rs @@ -0,0 +1,21 @@ +use mlua::prelude::*; +use tokio::io::{self, AsyncWriteExt}; + +use crate::lune::{ + scheduler::LuaSchedulerExt, + util::formatting::{format_label, pretty_format_multi_value}, +}; + +pub fn create(lua: &'static Lua) -> LuaResult> { + lua.create_async_function(|_, args: LuaMultiValue| async move { + let formatted = format!( + "{}\n{}", + format_label("warn"), + pretty_format_multi_value(&args)? + ); + let mut stdout = io::stderr(); + stdout.write_all(formatted.as_bytes()).await?; + stdout.flush().await?; + Ok(()) + }) +} diff --git a/src/lune/importer/mod.rs b/src/lune/importer/mod.rs deleted file mode 100644 index de3cdf6..0000000 --- a/src/lune/importer/mod.rs +++ /dev/null @@ -1,42 +0,0 @@ -use mlua::prelude::*; - -mod require; -mod require_waker; - -use crate::lune::builtins::{self, top_level}; - -pub fn create(lua: &'static Lua, args: Vec) -> LuaResult<()> { - // Create all builtins - let builtins = vec![ - ("fs", builtins::fs::create(lua)?), - ("net", builtins::net::create(lua)?), - ("process", builtins::process::create(lua, args)?), - ("serde", builtins::serde::create(lua)?), - ("stdio", builtins::stdio::create(lua)?), - ("task", builtins::task::create(lua)?), - ("luau", builtins::luau::create(lua)?), - #[cfg(feature = "roblox")] - ("roblox", builtins::roblox::create(lua)?), - ]; - - // Create our importer (require) with builtins - let require_fn = require::create(lua, builtins)?; - - // Create all top-level globals - let globals = vec![ - ("require", require_fn), - ("print", lua.create_function(top_level::print)?), - ("warn", lua.create_function(top_level::warn)?), - ("error", lua.create_function(top_level::error)?), - ("type", lua.create_function(top_level::proxy_type)?), - ("typeof", lua.create_function(top_level::proxy_typeof)?), - ]; - - // Set top-level globals - let lua_globals = lua.globals(); - for (name, global) in globals { - lua_globals.set(name, global)?; - } - - Ok(()) -} diff --git a/src/lune/importer/require.rs b/src/lune/importer/require.rs deleted file mode 100644 index 113c593..0000000 --- a/src/lune/importer/require.rs +++ /dev/null @@ -1,319 +0,0 @@ -use std::{ - cell::RefCell, - collections::{HashMap, HashSet}, - env::current_dir, - path::{self, PathBuf}, - sync::Arc, -}; - -use mlua::prelude::*; -use tokio::fs; -use tokio::sync::Mutex as AsyncMutex; - -use crate::lune::lua::{ - table::TableBuilder, - task::{TaskScheduler, TaskSchedulerScheduleExt}, -}; - -use super::require_waker::{RequireWakerFuture, RequireWakerState}; - -const REQUIRE_IMPL_LUA: &str = r#" -local source = info(1, "s") -if source == '[string "require"]' then - source = info(2, "s") -end -load(context, source, ...) -return yield() -"#; - -type RequireWakersVec<'lua> = Vec>>>; - -fn append_extension_and_canonicalize( - path: impl Into, - ext: &'static str, -) -> Result { - let mut new = path.into(); - match new.extension() { - // FUTURE: There's probably a better way to do this than converting to a lossy string - Some(e) => new.set_extension(format!("{}.{ext}", e.to_string_lossy())), - None => new.set_extension(ext), - }; - dunce::canonicalize(new) -} - -#[derive(Debug, Clone, Default)] -struct RequireContext<'lua> { - // NOTE: We need to use arc here so that mlua clones - // the reference and not the entire inner value(s) - builtins: Arc>>, - cached: Arc>>>>, - wakers: Arc>>>, - locks: Arc>>, - pwd: String, -} - -impl<'lua> RequireContext<'lua> { - pub fn new(lua: &'lua Lua, builtins_vec: Vec<(K, V)>) -> LuaResult - where - K: Into, - V: IntoLua<'lua>, - { - let mut pwd = current_dir() - .expect("Failed to access current working directory") - .to_string_lossy() - .to_string(); - if !pwd.ends_with(path::MAIN_SEPARATOR) { - pwd = format!("{pwd}{}", path::MAIN_SEPARATOR) - } - let mut builtins = HashMap::new(); - for (key, value) in builtins_vec { - builtins.insert(key.into(), value.into_lua_multi(lua)?); - } - Ok(Self { - pwd, - builtins: Arc::new(builtins), - ..Default::default() - }) - } - - pub fn is_locked(&self, absolute_path: &str) -> bool { - self.locks.borrow().contains(absolute_path) - } - - pub fn set_locked(&self, absolute_path: &str) -> bool { - self.locks.borrow_mut().insert(absolute_path.to_string()) - } - - pub fn set_unlocked(&self, absolute_path: &str) -> bool { - self.locks.borrow_mut().remove(absolute_path) - } - - pub fn try_acquire_lock_sync(&self, absolute_path: &str) -> bool { - if self.is_locked(absolute_path) { - false - } else { - self.set_locked(absolute_path); - true - } - } - - pub fn set_cached(&self, absolute_path: &str, result: &LuaResult>) { - self.cached - .borrow_mut() - .insert(absolute_path.to_string(), result.clone()); - if let Some(wakers) = self.wakers.borrow_mut().remove(absolute_path) { - for waker in wakers { - waker - .try_lock() - .expect("Failed to lock waker") - .finalize(result.clone()); - } - } - } - - pub fn wait_for_cache(self, absolute_path: &str) -> RequireWakerFuture<'lua> { - let state = RequireWakerState::new(); - let fut = RequireWakerFuture::new(&state); - self.wakers - .borrow_mut() - .entry(absolute_path.to_string()) - .or_insert_with(Vec::new) - .push(Arc::clone(&state)); - fut - } - - pub fn get_paths( - &self, - require_source: String, - require_path: String, - ) -> LuaResult<(String, String)> { - if require_path.starts_with('@') { - return Ok((require_path.clone(), require_path)); - } - let path_relative_to_pwd = PathBuf::from( - &require_source - .trim_start_matches("[string \"") - .trim_end_matches("\"]"), - ) - .parent() - .unwrap() - .join(&require_path); - // Try to normalize and resolve relative path segments such as './' and '../' - let file_path = match ( - append_extension_and_canonicalize(&path_relative_to_pwd, "luau"), - append_extension_and_canonicalize(&path_relative_to_pwd, "lua"), - ) { - (Ok(luau), _) => luau, - (_, Ok(lua)) => lua, - // If we did not find a luau/lua file at the wanted path, - // we should also look for "init" files in directories - _ => { - let init_dir_path = path_relative_to_pwd.join("init"); - match ( - append_extension_and_canonicalize(&init_dir_path, "luau"), - append_extension_and_canonicalize(&init_dir_path, "lua"), - ) { - (Ok(luau), _) => luau, - (_, Ok(lua)) => lua, - _ => { - return Err(LuaError::RuntimeError(format!( - "File does not exist at path '{require_path}'" - ))); - } - } - } - }; - let absolute = file_path.to_string_lossy().to_string(); - let relative = absolute.trim_start_matches(&self.pwd).to_string(); - Ok((absolute, relative)) - } -} - -impl<'lua> LuaUserData for RequireContext<'lua> {} - -fn load_builtin<'lua>( - _lua: &'lua Lua, - context: RequireContext<'lua>, - module_name: String, - _has_acquired_lock: bool, -) -> LuaResult> { - match context.builtins.get(&module_name) { - Some(module) => Ok(module.clone()), - None => Err(LuaError::RuntimeError(format!( - "No builtin module exists with the name '{}'", - module_name - ))), - } -} - -async fn load_file<'lua>( - lua: &'lua Lua, - context: RequireContext<'lua>, - absolute_path: String, - relative_path: String, - has_acquired_lock: bool, -) -> LuaResult> { - let cached = { context.cached.borrow().get(&absolute_path).cloned() }; - match cached { - Some(cached) => cached, - None => { - if !has_acquired_lock { - return context.wait_for_cache(&absolute_path).await; - } - // Try to read the wanted file, note that we use bytes instead of reading - // to a string since lua scripts are not necessarily valid utf-8 strings - let contents = fs::read(&absolute_path).await.into_lua_err()?; - // Use a name without extensions for loading the chunk, some - // other code assumes the require path is without extensions - let path_relative_no_extension = relative_path - .trim_end_matches(".lua") - .trim_end_matches(".luau"); - // Load the file into a thread - let loaded_func = lua - .load(contents) - .set_name(path_relative_no_extension) - .into_function()?; - let loaded_thread = lua.create_thread(loaded_func)?; - // Run the thread and wait for completion using the native task scheduler waker - let task_fut = { - let sched = lua.app_data_ref::<&TaskScheduler>().unwrap(); - let task = sched.schedule_blocking(loaded_thread, LuaMultiValue::new())?; - sched.wait_for_task_completion(task) - }; - // Wait for the thread to finish running, cache + return our result, - // notify any other threads that are also waiting on this to finish - let rets = task_fut.await; - context.set_cached(&absolute_path, &rets); - rets - } - } -} - -async fn load<'lua>( - lua: &'lua Lua, - context: LuaUserDataRef<'lua, RequireContext<'lua>>, - absolute_path: String, - relative_path: String, - has_acquired_lock: bool, -) -> LuaResult> { - let result = if absolute_path == relative_path && absolute_path.starts_with('@') { - if let Some(module_name) = absolute_path.strip_prefix("@lune/") { - load_builtin( - lua, - context.clone(), - module_name.to_string(), - has_acquired_lock, - ) - } else { - // FUTURE: '@' can be used a special prefix for users to set their own - // paths relative to a project file, similar to typescript paths config - // https://www.typescriptlang.org/tsconfig#paths - Err(LuaError::RuntimeError( - "Require paths prefixed by '@' are not yet supported".to_string(), - )) - } - } else { - load_file( - lua, - context.clone(), - absolute_path.to_string(), - relative_path, - has_acquired_lock, - ) - .await - }; - if has_acquired_lock { - context.set_unlocked(&absolute_path); - } - result -} - -pub fn create(lua: &'static Lua, builtins: Vec<(K, V)>) -> LuaResult -where - K: Clone + Into, - V: Clone + IntoLua<'static>, -{ - let require_context = RequireContext::new(lua, builtins)?; - let require_yield: LuaFunction = lua.named_registry_value("co.yield")?; - let require_info: LuaFunction = lua.named_registry_value("dbg.info")?; - let require_print: LuaFunction = lua.named_registry_value("print")?; - - let require_env = TableBuilder::new(lua)? - .with_value("context", require_context)? - .with_value("yield", require_yield)? - .with_value("info", require_info)? - .with_value("print", require_print)? - .with_function( - "load", - |lua, - (context, require_source, require_path): ( - LuaUserDataRef, - String, - String, - )| { - let (absolute_path, relative_path) = - context.get_paths(require_source, require_path)?; - // NOTE: We can not acquire the lock in the async part of the require - // load process since several requires may have happened for the - // same path before the async load task even gets a chance to run - let has_lock = context.try_acquire_lock_sync(&absolute_path); - let fut = load(lua, context, absolute_path, relative_path, has_lock); - let sched = lua - .app_data_ref::<&TaskScheduler>() - .expect("Missing task scheduler as a lua app data"); - sched.queue_async_task_inherited(lua.current_thread(), None, async { - let rets = fut.await?; - let mult = rets.into_lua_multi(lua)?; - Ok(Some(mult)) - }) - }, - )? - .build_readonly()?; - - let require_fn_lua = lua - .load(REQUIRE_IMPL_LUA) - .set_name("require") - .set_environment(require_env) - .into_function()?; - Ok(require_fn_lua) -} diff --git a/src/lune/importer/require_waker.rs b/src/lune/importer/require_waker.rs deleted file mode 100644 index 4a43b87..0000000 --- a/src/lune/importer/require_waker.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{ - future::Future, - pin::Pin, - sync::Arc, - task::{Context, Poll, Waker}, -}; - -use tokio::sync::Mutex as AsyncMutex; - -use mlua::prelude::*; - -#[derive(Debug, Clone)] -pub(super) struct RequireWakerState<'lua> { - rets: Option>>, - waker: Option, -} - -impl<'lua> RequireWakerState<'lua> { - pub fn new() -> Arc> { - Arc::new(AsyncMutex::new(RequireWakerState { - rets: None, - waker: None, - })) - } - - pub fn finalize(&mut self, rets: LuaResult>) { - self.rets = Some(rets); - if let Some(waker) = self.waker.take() { - waker.wake(); - } - } -} - -#[derive(Debug)] -pub(super) struct RequireWakerFuture<'lua> { - state: Arc>>, -} - -impl<'lua> RequireWakerFuture<'lua> { - pub fn new(state: &Arc>>) -> Self { - Self { - state: Arc::clone(state), - } - } -} - -impl<'lua> Clone for RequireWakerFuture<'lua> { - fn clone(&self) -> Self { - Self { - state: Arc::clone(&self.state), - } - } -} - -impl<'lua> Future for RequireWakerFuture<'lua> { - type Output = LuaResult>; - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut shared_state = self.state.try_lock().unwrap(); - if let Some(rets) = shared_state.rets.clone() { - Poll::Ready(rets) - } else { - shared_state.waker = Some(cx.waker().clone()); - Poll::Pending - } - } -} diff --git a/src/lune/lua/async_ext.rs b/src/lune/lua/async_ext.rs deleted file mode 100644 index afdd29b..0000000 --- a/src/lune/lua/async_ext.rs +++ /dev/null @@ -1,93 +0,0 @@ -use async_trait::async_trait; -use futures_util::Future; -use mlua::prelude::*; - -use crate::lune::{lua::table::TableBuilder, lua::task::TaskScheduler}; - -use super::task::TaskSchedulerAsyncExt; - -const ASYNC_IMPL_LUA: &str = r#" -resumeAsync(...) -return yield() -"#; - -const WAIT_IMPL_LUA: &str = r#" -resumeAfter(...) -return yield() -"#; - -#[async_trait(?Send)] -pub trait LuaAsyncExt { - fn create_async_function<'lua, A, R, F, FR>(self, func: F) -> LuaResult> - where - A: FromLuaMulti<'static>, - R: IntoLuaMulti<'static>, - F: 'static + Fn(&'lua Lua, A) -> FR, - FR: 'static + Future>; - - fn create_waiter_function<'lua>(self) -> LuaResult>; -} - -impl LuaAsyncExt for &'static Lua { - /** - Creates a function callable from Lua that runs an async - closure and returns the results of it to the call site. - */ - fn create_async_function<'lua, A, R, F, FR>(self, func: F) -> LuaResult> - where - A: FromLuaMulti<'static>, - R: IntoLuaMulti<'static>, - F: 'static + Fn(&'lua Lua, A) -> FR, - FR: 'static + Future>, - { - let async_env_yield: LuaFunction = self.named_registry_value("co.yield")?; - let async_env = TableBuilder::new(self)? - .with_value("yield", async_env_yield)? - .with_function("resumeAsync", move |lua: &Lua, args: A| { - let thread = lua.current_thread(); - let fut = func(lua, args); - let sched = lua - .app_data_ref::<&TaskScheduler>() - .expect("Missing task scheduler as a lua app data"); - sched.queue_async_task(thread, None, async { - let rets = fut.await?; - let mult = rets.into_lua_multi(lua)?; - Ok(Some(mult)) - }) - })? - .build_readonly()?; - let async_func = self - .load(ASYNC_IMPL_LUA) - .set_name("async") - .set_environment(async_env) - .into_function()?; - Ok(async_func) - } - - /** - Creates a special async function that waits the - desired amount of time, inheriting the guid of the - current thread / task for proper cancellation. - - This will yield the lua thread calling the function until the - desired time has passed and the scheduler resumes the thread. - */ - fn create_waiter_function<'lua>(self) -> LuaResult> { - let async_env_yield: LuaFunction = self.named_registry_value("co.yield")?; - let async_env = TableBuilder::new(self)? - .with_value("yield", async_env_yield)? - .with_function("resumeAfter", move |lua: &Lua, duration: Option| { - let sched = lua - .app_data_ref::<&TaskScheduler>() - .expect("Missing task scheduler as a lua app data"); - sched.schedule_wait(lua.current_thread(), duration) - })? - .build_readonly()?; - let async_func = self - .load(WAIT_IMPL_LUA) - .set_name("wait") - .set_environment(async_env) - .into_function()?; - Ok(async_func) - } -} diff --git a/src/lune/lua/create.rs b/src/lune/lua/create.rs deleted file mode 100644 index 2a97474..0000000 --- a/src/lune/lua/create.rs +++ /dev/null @@ -1,184 +0,0 @@ -use mlua::{prelude::*, Compiler as LuaCompiler}; - -/* - - Level 0 is the call to info - - Level 1 is the load call in create() below where we load this into a function - - Level 2 is the call to the trace, which we also want to skip, so start at 3 - - Also note that we must match the mlua traceback format here so that we - can pattern match and beautify it properly later on when outputting it -*/ -const TRACE_IMPL_LUA: &str = r#" -local lines = {} -for level = 3, 16 do - local parts = {} - local source, line, name = info(level, "sln") - if source then - push(parts, source) - else - break - end - if line == -1 then - line = nil - end - if name and #name <= 0 then - name = nil - end - if line then - push(parts, format("%d", line)) - end - if name and #parts > 1 then - push(parts, format(" in function '%s'", name)) - elseif name then - push(parts, format("in function '%s'", name)) - end - if #parts > 0 then - push(lines, concat(parts, ":")) - end -end -if #lines > 0 then - return concat(lines, "\n") -else - return nil -end -"#; - -/** - Stores the following globals in the Lua registry: - - | Registry Name | Global | - |-----------------|-------------------| - | `"print"` | `print` | - | `"error"` | `error` | - | `"type"` | `type` | - | `"typeof"` | `typeof` | - | `"pcall"` | `pcall` | - | `"xpcall"` | `xpcall` | - | `"tostring"` | `tostring` | - | `"tonumber"` | `tonumber` | - | `"co.yield"` | `coroutine.yield` | - | `"co.close"` | `coroutine.close` | - | `"tab.pack"` | `table.pack` | - | `"tab.unpack"` | `table.unpack` | - | `"tab.freeze"` | `table.freeze` | - | `"tab.getmeta"` | `getmetatable` | - | `"tab.setmeta"` | `setmetatable` | - | `"dbg.info"` | `debug.info` | - | `"dbg.trace"` | `debug.traceback` | - - These globals can then be modified safely from other runtime code. -*/ -fn store_globals_in_registry(lua: &Lua) -> LuaResult<()> { - // Extract some global tables that we will extract - // built-in functions from and store in the registry - let globals = lua.globals(); - let debug: LuaTable = globals.get("debug")?; - let string: LuaTable = globals.get("string")?; - let table: LuaTable = globals.get("table")?; - let coroutine: LuaTable = globals.get("coroutine")?; - - // Store original lua global functions in the registry so we can use - // them later without passing them around and dealing with lifetimes - lua.set_named_registry_value("print", globals.get::<_, LuaFunction>("print")?)?; - lua.set_named_registry_value("error", globals.get::<_, LuaFunction>("error")?)?; - lua.set_named_registry_value("type", globals.get::<_, LuaFunction>("type")?)?; - lua.set_named_registry_value("typeof", globals.get::<_, LuaFunction>("typeof")?)?; - lua.set_named_registry_value("xpcall", globals.get::<_, LuaFunction>("xpcall")?)?; - lua.set_named_registry_value("pcall", globals.get::<_, LuaFunction>("pcall")?)?; - lua.set_named_registry_value("tostring", globals.get::<_, LuaFunction>("tostring")?)?; - lua.set_named_registry_value("tonumber", globals.get::<_, LuaFunction>("tonumber")?)?; - lua.set_named_registry_value("co.status", coroutine.get::<_, LuaFunction>("status")?)?; - lua.set_named_registry_value("co.yield", coroutine.get::<_, LuaFunction>("yield")?)?; - lua.set_named_registry_value("co.close", coroutine.get::<_, LuaFunction>("close")?)?; - lua.set_named_registry_value("dbg.info", debug.get::<_, LuaFunction>("info")?)?; - lua.set_named_registry_value("tab.pack", table.get::<_, LuaFunction>("pack")?)?; - lua.set_named_registry_value("tab.unpack", table.get::<_, LuaFunction>("unpack")?)?; - lua.set_named_registry_value("tab.freeze", table.get::<_, LuaFunction>("freeze")?)?; - lua.set_named_registry_value( - "tab.getmeta", - globals.get::<_, LuaFunction>("getmetatable")?, - )?; - lua.set_named_registry_value( - "tab.setmeta", - globals.get::<_, LuaFunction>("setmetatable")?, - )?; - - // Create a trace function that can be called to obtain a full stack trace from - // lua, this is not possible to do from rust when using our manual scheduler - let dbg_trace_env = lua.create_table_with_capacity(0, 1)?; - dbg_trace_env.set("info", debug.get::<_, LuaFunction>("info")?)?; - dbg_trace_env.set("push", table.get::<_, LuaFunction>("insert")?)?; - dbg_trace_env.set("concat", table.get::<_, LuaFunction>("concat")?)?; - dbg_trace_env.set("format", string.get::<_, LuaFunction>("format")?)?; - - let dbg_trace_fn = lua - .load(TRACE_IMPL_LUA) - .set_name("=dbg.trace") - .set_environment(dbg_trace_env) - .into_function()?; - lua.set_named_registry_value("dbg.trace", dbg_trace_fn)?; - - Ok(()) -} - -/** - Sets the `_VERSION` global to a value matching the string `Lune x.y.z+w` where - `x.y.z` is the current Lune runtime version and `w` is the current Luau version -*/ -fn set_global_version(lua: &Lua) -> LuaResult<()> { - let luau_version_full = lua - .globals() - .get::<_, LuaString>("_VERSION") - .expect("Missing _VERSION global"); - let luau_version = luau_version_full - .to_str()? - .strip_prefix("Luau 0.") - .expect("_VERSION global is formatted incorrectly") - .trim(); - if luau_version.is_empty() { - panic!("_VERSION global is missing version number") - } - lua.globals().set( - "_VERSION", - lua.create_string(&format!( - "Lune {lune}+{luau}", - lune = env!("CARGO_PKG_VERSION"), - luau = luau_version, - ))?, - ) -} - -/** - Creates a _G table that is separate from our built-in globals -*/ -fn set_global_table(lua: &Lua) -> LuaResult<()> { - lua.globals().set("_G", lua.create_table()?) -} - -/** - Enables JIT and sets default compiler settings for the Lua struct. -*/ -fn init_compiler_settings(lua: &Lua) { - lua.enable_jit(true); - lua.set_compiler( - LuaCompiler::default() - .set_coverage_level(0) - .set_debug_level(1) - .set_optimization_level(1), - ); -} - -/** - Creates a new [`mlua::Lua`] struct with compiler, - registry, and globals customized for the Lune runtime. - - Refer to the source code for additional details and specifics. -*/ -pub fn create() -> LuaResult { - let lua = Lua::new(); - init_compiler_settings(&lua); - store_globals_in_registry(&lua)?; - set_global_version(&lua)?; - set_global_table(&lua)?; - Ok(lua) -} diff --git a/src/lune/lua/fs/mod.rs b/src/lune/lua/fs/mod.rs deleted file mode 100644 index 82378d7..0000000 --- a/src/lune/lua/fs/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod copy; -mod metadata; -mod options; - -pub use copy::copy; -pub use metadata::FsMetadata; -pub use options::FsWriteOptions; diff --git a/src/lune/lua/luau/mod.rs b/src/lune/lua/luau/mod.rs deleted file mode 100644 index bf42f1f..0000000 --- a/src/lune/lua/luau/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod options; - -pub use options::{LuauCompileOptions, LuauLoadOptions}; diff --git a/src/lune/lua/mod.rs b/src/lune/lua/mod.rs deleted file mode 100644 index 944ea44..0000000 --- a/src/lune/lua/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod create; - -pub mod async_ext; -pub mod fs; -pub mod luau; -pub mod net; -pub mod process; -pub mod serde; -pub mod stdio; -pub mod table; -pub mod task; - -pub use create::create as create_lune_lua; diff --git a/src/lune/lua/net/mod.rs b/src/lune/lua/net/mod.rs deleted file mode 100644 index c52b780..0000000 --- a/src/lune/lua/net/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod client; -mod config; -mod response; -mod server; -mod websocket; - -pub use client::{NetClient, NetClientBuilder}; -pub use config::{RequestConfig, ServeConfig}; -pub use response::{NetServeResponse, NetServeResponseKind}; -pub use server::{NetLocalExec, NetService}; -pub use websocket::NetWebSocket; diff --git a/src/lune/lua/net/server.rs b/src/lune/lua/net/server.rs deleted file mode 100644 index d3428df..0000000 --- a/src/lune/lua/net/server.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::{ - future::Future, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; - -use mlua::prelude::*; - -use hyper::{body::to_bytes, server::conn::AddrStream, service::Service}; -use hyper::{Body, Request, Response}; -use hyper_tungstenite::{is_upgrade_request as is_ws_upgrade_request, upgrade as ws_upgrade}; -use tokio::task; - -use crate::lune::{ - lua::table::TableBuilder, - lua::task::{TaskScheduler, TaskSchedulerAsyncExt, TaskSchedulerScheduleExt}, -}; - -use super::{NetServeResponse, NetWebSocket}; - -// Hyper service implementation for net, lots of boilerplate here -// but make_svc and make_svc_function do not work for what we need - -pub struct NetServiceInner( - &'static Lua, - Arc, - Arc>, -); - -impl Service> for NetServiceInner { - type Response = Response; - type Error = LuaError; - type Future = Pin>>>; - - fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, mut req: Request) -> Self::Future { - let lua = self.0; - if self.2.is_some() && is_ws_upgrade_request(&req) { - // Websocket upgrade request + websocket handler exists, - // we should now upgrade this connection to a websocket - // and then call our handler with a new socket object - let kopt = self.2.clone(); - let key = kopt.as_ref().as_ref().unwrap(); - let handler: LuaFunction = lua.registry_value(key).expect("Missing websocket handler"); - let (response, ws) = ws_upgrade(&mut req, None).expect("Failed to upgrade websocket"); - // This should be spawned as a registered task, otherwise - // the scheduler may exit early and cancel this even though what - // we want here is a long-running task that keeps the program alive - let sched = lua - .app_data_ref::<&TaskScheduler>() - .expect("Missing task scheduler"); - let task = sched.register_background_task(); - task::spawn_local(async move { - // Create our new full websocket object, then - // schedule our handler to get called asap - let ws = ws.await.into_lua_err()?; - let sock = NetWebSocket::new(ws).into_lua_table(lua)?; - let sched = lua - .app_data_ref::<&TaskScheduler>() - .expect("Missing task scheduler"); - let result = sched.schedule_blocking( - lua.create_thread(handler)?, - LuaMultiValue::from_vec(vec![LuaValue::Table(sock)]), - ); - task.unregister(Ok(())); - result - }); - Box::pin(async move { Ok(response) }) - } else { - // Got a normal http request or no websocket handler - // exists, just call the http request handler - let key = self.1.clone(); - let (parts, body) = req.into_parts(); - Box::pin(async move { - // Convert request body into bytes, extract handler - let bytes = to_bytes(body).await.into_lua_err()?; - let handler: LuaFunction = lua.registry_value(&key)?; - // Create a readonly table for the request query params - let query_params = TableBuilder::new(lua)? - .with_values( - parts - .uri - .query() - .unwrap_or_default() - .split('&') - .filter_map(|q| q.split_once('=')) - .collect(), - )? - .build_readonly()?; - // Do the same for headers - let header_map = TableBuilder::new(lua)? - .with_values( - parts - .headers - .iter() - .map(|(name, value)| { - (name.to_string(), value.to_str().unwrap().to_string()) - }) - .collect(), - )? - .build_readonly()?; - // Create a readonly table with request info to pass to the handler - let request = TableBuilder::new(lua)? - .with_value("path", parts.uri.path())? - .with_value("query", query_params)? - .with_value("method", parts.method.as_str())? - .with_value("headers", header_map)? - .with_value("body", lua.create_string(&bytes)?)? - .build_readonly()?; - let response: LuaResult = handler.call(request); - // Send below errors to task scheduler so that they can emit properly - let lua_error = match response { - Ok(r) => match r.into_response() { - Ok(res) => return Ok(res), - Err(err) => err, - }, - Err(err) => err, - }; - lua.app_data_ref::<&TaskScheduler>() - .expect("Missing task scheduler") - .forward_lua_error(lua_error); - Ok(Response::builder() - .status(500) - .body(Body::from("Internal Server Error")) - .unwrap()) - }) - } - } -} - -pub struct NetService( - &'static Lua, - Arc, - Arc>, -); - -impl NetService { - pub fn new( - lua: &'static Lua, - callback_http: LuaRegistryKey, - callback_websocket: Option, - ) -> Self { - Self(lua, Arc::new(callback_http), Arc::new(callback_websocket)) - } -} - -impl Service<&AddrStream> for NetService { - type Response = NetServiceInner; - type Error = hyper::Error; - type Future = Pin>>>; - - fn poll_ready(&mut self, _: &mut Context) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, _: &AddrStream) -> Self::Future { - let lua = self.0; - let key1 = self.1.clone(); - let key2 = self.2.clone(); - Box::pin(async move { Ok(NetServiceInner(lua, key1, key2)) }) - } -} - -#[derive(Clone, Copy, Debug)] -pub struct NetLocalExec; - -impl hyper::rt::Executor for NetLocalExec -where - F: std::future::Future + 'static, // not requiring `Send` -{ - fn execute(&self, fut: F) { - task::spawn_local(fut); - } -} diff --git a/src/lune/lua/serde/mod.rs b/src/lune/lua/serde/mod.rs deleted file mode 100644 index ab554ef..0000000 --- a/src/lune/lua/serde/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod compress_decompress; -mod encode_decode; - -pub use compress_decompress::{compress, decompress, CompressDecompressFormat}; -pub use encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat}; diff --git a/src/lune/lua/stdio/mod.rs b/src/lune/lua/stdio/mod.rs deleted file mode 100644 index 1e6ac81..0000000 --- a/src/lune/lua/stdio/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod formatting; -pub mod prompt; diff --git a/src/lune/lua/table/mod.rs b/src/lune/lua/table/mod.rs deleted file mode 100644 index 88239ac..0000000 --- a/src/lune/lua/table/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod builder; - -pub use builder::TableBuilder; diff --git a/src/lune/lua/task/ext/async_ext.rs b/src/lune/lua/task/ext/async_ext.rs deleted file mode 100644 index 681c01a..0000000 --- a/src/lune/lua/task/ext/async_ext.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::time::Duration; - -use async_trait::async_trait; - -use futures_util::Future; -use mlua::prelude::*; -use tokio::time::{sleep, Instant}; - -use crate::lune::lua::task::TaskKind; - -use super::super::{ - scheduler::TaskReference, scheduler::TaskScheduler, scheduler_handle::TaskSchedulerAsyncHandle, - scheduler_message::TaskSchedulerMessage, -}; - -/* - ────────────────────────────────────────────────────────── - Trait definition - same as the implementation, ignore this - - We use traits here to prevent misuse of certain scheduler - APIs, making importing of them as intentional as possible - ────────────────────────────────────────────────────────── -*/ -#[async_trait(?Send)] -pub trait TaskSchedulerAsyncExt<'fut> { - fn register_background_task(&self) -> TaskSchedulerAsyncHandle; - - fn schedule_async<'sched, R, F, FR>( - &'sched self, - thread: LuaThread<'_>, - func: F, - ) -> LuaResult - where - 'sched: 'fut, - R: IntoLuaMulti<'static>, - F: 'static + Fn(&'static Lua) -> FR, - FR: 'static + Future>; - - fn schedule_wait( - &'fut self, - reference: LuaThread<'_>, - duration: Option, - ) -> LuaResult; -} - -/* - ──────────────────── - Trait implementation - ──────────────────── -*/ -#[async_trait(?Send)] -impl<'fut> TaskSchedulerAsyncExt<'fut> for TaskScheduler<'fut> { - /** - Registers a new background task with the task scheduler. - - The returned [`TaskSchedulerAsyncHandle`] must have its - [`TaskSchedulerAsyncHandle::unregister`] method called - upon completion of the background task to prevent - the task scheduler from running indefinitely. - */ - fn register_background_task(&self) -> TaskSchedulerAsyncHandle { - let sender = self.futures_tx.clone(); - sender - .send(TaskSchedulerMessage::Spawned) - .unwrap_or_else(|e| { - panic!( - "\ - \nFailed to unregister background task - this is an internal error! \ - \nPlease report it at {} \ - \nDetails: {e} \ - ", - env!("CARGO_PKG_REPOSITORY") - ) - }); - TaskSchedulerAsyncHandle::new(sender) - } - - /** - Schedules a lua thread or function - to be resumed after running a future. - - The given lua thread or function will be resumed - using the optional arguments returned by the future. - */ - fn schedule_async<'sched, R, F, FR>( - &'sched self, - thread: LuaThread<'_>, - func: F, - ) -> LuaResult - where - 'sched: 'fut, // Scheduler must live at least as long as the future - R: IntoLuaMulti<'static>, - F: 'static + Fn(&'static Lua) -> FR, - FR: 'static + Future>, - { - self.queue_async_task(thread, None, async move { - match func(self.lua).await { - Ok(res) => match res.into_lua_multi(self.lua) { - Ok(multi) => Ok(Some(multi)), - Err(e) => Err(e), - }, - Err(e) => Err(e), - } - }) - } - - /** - Schedules a task reference to be resumed after a certain amount of time. - - The given task will be resumed with the elapsed time as its one and only argument. - */ - fn schedule_wait( - &'fut self, - thread: LuaThread<'_>, - duration: Option, - ) -> LuaResult { - let reference = self.create_task(TaskKind::Future, thread, None, true)?; - // Insert the future - let futs = self - .futures - .try_lock() - .expect("Tried to add future to queue during futures resumption"); - futs.push(Box::pin(async move { - let before = Instant::now(); - sleep(Duration::from_secs_f64( - duration.unwrap_or_default().max(0.0), - )) - .await; - let elapsed_secs = before.elapsed().as_secs_f64(); - let args = elapsed_secs.into_lua_multi(self.lua).unwrap(); - (Some(reference), Ok(Some(args))) - })); - Ok(reference) - } -} diff --git a/src/lune/lua/task/ext/mod.rs b/src/lune/lua/task/ext/mod.rs deleted file mode 100644 index c6a6958..0000000 --- a/src/lune/lua/task/ext/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod async_ext; -mod resume_ext; -mod schedule_ext; - -pub use async_ext::TaskSchedulerAsyncExt; -pub use resume_ext::TaskSchedulerResumeExt; -pub use schedule_ext::TaskSchedulerScheduleExt; diff --git a/src/lune/lua/task/ext/resume_ext.rs b/src/lune/lua/task/ext/resume_ext.rs deleted file mode 100644 index 5dd9e07..0000000 --- a/src/lune/lua/task/ext/resume_ext.rs +++ /dev/null @@ -1,180 +0,0 @@ -use std::time::Duration; - -use async_trait::async_trait; - -use mlua::prelude::*; - -use futures_util::StreamExt; -use tokio::time::sleep; - -use super::super::{ - scheduler_message::TaskSchedulerMessage, scheduler_state::TaskSchedulerState, TaskScheduler, -}; - -/* - ────────────────────────────────────────────────────────── - Trait definition - same as the implementation, ignore this - - We use traits here to prevent misuse of certain scheduler - APIs, making importing of them as intentional as possible - ────────────────────────────────────────────────────────── -*/ -#[async_trait(?Send)] -pub trait TaskSchedulerResumeExt { - async fn resume_queue(&self) -> TaskSchedulerState; -} - -/* - ──────────────────── - Trait implementation - ──────────────────── -*/ -#[async_trait(?Send)] -impl TaskSchedulerResumeExt for TaskScheduler<'_> { - /** - Resumes the task scheduler queue. - - This will run any spawned or deferred Lua tasks in a blocking manner. - - Once all spawned and / or deferred Lua tasks have finished running, - this will process delayed tasks, waiting tasks, and native Rust - futures concurrently, awaiting the first one to be ready for resumption. - */ - async fn resume_queue(&self) -> TaskSchedulerState { - let current = TaskSchedulerState::new(self); - if current.num_blocking > 0 { - // 1. Blocking tasks - resume_next_blocking_task(self, None) - } else if current.num_futures > 0 || current.num_background > 0 { - // 2. Async and/or background tasks - tokio::select! { - result = resume_next_async_task(self) => result, - result = receive_next_message(self) => result, - } - } else { - // 3. No tasks left, here we sleep one millisecond in case - // the caller of resume_queue accidentally calls this in - // a busy loop to prevent cpu usage from going to 100% - sleep(Duration::from_millis(1)).await; - TaskSchedulerState::new(self) - } - } -} - -/* - ──────────────────────────────────────────────────────────────── - Private functions for the trait that operate on the task scheduler - - These could be implemented as normal methods but if we put them in the - trait they become public, and putting them in the task scheduler's - own implementation block will clutter that up unnecessarily - ──────────────────────────────────────────────────────────────── -*/ - -/** - Resumes the next queued Lua task, if one exists, blocking - the current thread until it either yields or finishes. -*/ -fn resume_next_blocking_task<'sched, 'args>( - scheduler: &TaskScheduler<'sched>, - override_args: Option>>, -) -> TaskSchedulerState -where - 'args: 'sched, -{ - match { - let mut queue_guard = scheduler.tasks_queue_blocking.borrow_mut(); - let task = queue_guard.pop_front(); - drop(queue_guard); - task - } { - None => TaskSchedulerState::new(scheduler), - Some(task) => match scheduler.resume_task(task, override_args) { - Err(task_err) => { - scheduler.wake_completed_task(task, Err(task_err.clone())); - TaskSchedulerState::err(scheduler, task_err) - } - Ok(rets) if rets.0 == LuaThreadStatus::Unresumable => { - scheduler.wake_completed_task(task, Ok(rets.1)); - TaskSchedulerState::new(scheduler) - } - Ok(_) => TaskSchedulerState::new(scheduler), - }, - } -} - -/** - Awaits the first available queued future, and resumes its associated - Lua task which will be ready for resumption when that future wakes. - - Panics if there are no futures currently queued. - - Use [`TaskScheduler::next_queue_future_exists`] - to check if there are any queued futures. -*/ -async fn resume_next_async_task(scheduler: &TaskScheduler<'_>) -> TaskSchedulerState { - let (task, result) = { - let mut futs = scheduler - .futures - .try_lock() - .expect("Tried to resume next queued future while already resuming or modifying"); - futs.next() - .await - .expect("Tried to resume next queued future but none are queued") - }; - // The future might not return a reference that it wants to resume - if let Some(task) = task { - // Promote this future task to a blocking task and resume it - // right away, also taking care to not borrow mutably twice - // by dropping this guard before trying to resume it - let mut queue_guard = scheduler.tasks_queue_blocking.borrow_mut(); - queue_guard.push_front(task); - drop(queue_guard); - } - resume_next_blocking_task(scheduler, result.transpose()) -} - -/** - Awaits the next background task registration - message, if any messages exist in the queue. - - This is a no-op if there are no background tasks left running - and / or the background task messages channel was closed. -*/ -async fn receive_next_message(scheduler: &TaskScheduler<'_>) -> TaskSchedulerState { - let message_opt = { - let mut rx = scheduler.futures_rx.lock().await; - rx.recv().await - }; - if let Some(message) = message_opt { - match message { - TaskSchedulerMessage::NewBlockingTaskReady => TaskSchedulerState::new(scheduler), - TaskSchedulerMessage::NewLuaErrorReady(err) => TaskSchedulerState::err(scheduler, err), - TaskSchedulerMessage::Spawned => { - let prev = scheduler.futures_background_count.get(); - scheduler.futures_background_count.set(prev + 1); - TaskSchedulerState::new(scheduler) - } - TaskSchedulerMessage::Terminated(result) => { - let prev = scheduler.futures_background_count.get(); - scheduler.futures_background_count.set(prev - 1); - if prev == 0 { - panic!( - r#" - Terminated a background task without it running - this is an internal error! - Please report it at {} - "#, - env!("CARGO_PKG_REPOSITORY") - ) - } - if let Err(e) = result { - TaskSchedulerState::err(scheduler, e) - } else { - TaskSchedulerState::new(scheduler) - } - } - } - } else { - TaskSchedulerState::new(scheduler) - } -} diff --git a/src/lune/lua/task/ext/schedule_ext.rs b/src/lune/lua/task/ext/schedule_ext.rs deleted file mode 100644 index 244a344..0000000 --- a/src/lune/lua/task/ext/schedule_ext.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::time::Duration; - -use mlua::prelude::*; -use tokio::time::sleep; - -use super::super::{scheduler::TaskKind, scheduler::TaskReference, scheduler::TaskScheduler}; - -/* - ────────────────────────────────────────────────────────── - Trait definition - same as the implementation, ignore this - - We use traits here to prevent misuse of certain scheduler - APIs, making importing of them as intentional as possible - ────────────────────────────────────────────────────────── -*/ -pub trait TaskSchedulerScheduleExt { - fn schedule_blocking( - &self, - thread: LuaThread<'_>, - thread_args: LuaMultiValue<'_>, - ) -> LuaResult; - - fn schedule_blocking_deferred( - &self, - thread: LuaThread<'_>, - thread_args: LuaMultiValue<'_>, - ) -> LuaResult; - - fn schedule_blocking_after_seconds( - &self, - after_secs: f64, - thread: LuaThread<'_>, - thread_args: LuaMultiValue<'_>, - ) -> LuaResult; -} - -/* - ──────────────────── - Trait implementation - ──────────────────── -*/ -impl TaskSchedulerScheduleExt for TaskScheduler<'_> { - /** - Schedules a lua thread or function to resume ***first*** during this - resumption point, ***skipping ahead*** of any other currently queued tasks. - - The given lua thread or function will be resumed - using the given `thread_args` as its argument(s). - */ - fn schedule_blocking( - &self, - thread: LuaThread<'_>, - thread_args: LuaMultiValue<'_>, - ) -> LuaResult { - self.queue_blocking_task(TaskKind::Instant, thread, Some(thread_args)) - } - - /** - Schedules a lua thread or function to resume ***after all*** - currently resuming tasks, during this resumption point. - - The given lua thread or function will be resumed - using the given `thread_args` as its argument(s). - */ - fn schedule_blocking_deferred( - &self, - thread: LuaThread<'_>, - thread_args: LuaMultiValue<'_>, - ) -> LuaResult { - self.queue_blocking_task(TaskKind::Deferred, thread, Some(thread_args)) - } - - /** - Schedules a lua thread or function to - be resumed after waiting asynchronously. - - The given lua thread or function will be resumed - using the given `thread_args` as its argument(s). - */ - fn schedule_blocking_after_seconds( - &self, - after_secs: f64, - thread: LuaThread<'_>, - thread_args: LuaMultiValue<'_>, - ) -> LuaResult { - self.queue_async_task(thread, Some(thread_args), async move { - sleep(Duration::from_secs_f64(after_secs)).await; - Ok(None) - }) - } -} diff --git a/src/lune/lua/task/mod.rs b/src/lune/lua/task/mod.rs deleted file mode 100644 index 6f8df44..0000000 --- a/src/lune/lua/task/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod ext; -mod proxy; -mod scheduler; -mod scheduler_handle; -mod scheduler_message; -mod scheduler_state; -mod task_kind; -mod task_reference; -mod task_waiter; - -pub use ext::*; -pub use proxy::*; -pub use scheduler::*; -pub use scheduler_handle::*; -pub use scheduler_state::*; diff --git a/src/lune/lua/task/proxy.rs b/src/lune/lua/task/proxy.rs deleted file mode 100644 index ee48a6e..0000000 --- a/src/lune/lua/task/proxy.rs +++ /dev/null @@ -1,118 +0,0 @@ -use mlua::prelude::*; - -use super::TaskReference; - -/* - Proxy enum to deal with both threads & functions -*/ - -#[derive(Debug, Clone)] -pub enum LuaThreadOrFunction<'lua> { - Thread(LuaThread<'lua>), - Function(LuaFunction<'lua>), -} - -impl<'lua> LuaThreadOrFunction<'lua> { - pub fn into_thread(self, lua: &'lua Lua) -> LuaResult> { - match self { - Self::Thread(t) => Ok(t), - Self::Function(f) => lua.create_thread(f), - } - } -} - -impl<'lua> From> for LuaThreadOrFunction<'lua> { - fn from(value: LuaThread<'lua>) -> Self { - Self::Thread(value) - } -} - -impl<'lua> From> for LuaThreadOrFunction<'lua> { - fn from(value: LuaFunction<'lua>) -> Self { - Self::Function(value) - } -} - -impl<'lua> FromLua<'lua> for LuaThreadOrFunction<'lua> { - fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { - match value { - LuaValue::Thread(t) => Ok(Self::Thread(t)), - LuaValue::Function(f) => Ok(Self::Function(f)), - value => Err(LuaError::FromLuaConversionError { - from: value.type_name(), - to: "LuaThreadOrFunction", - message: Some(format!( - "Expected thread or function, got '{}'", - value.type_name() - )), - }), - } - } -} - -impl<'lua> IntoLua<'lua> for LuaThreadOrFunction<'lua> { - fn into_lua(self, _: &'lua Lua) -> LuaResult> { - match self { - Self::Thread(t) => Ok(LuaValue::Thread(t)), - Self::Function(f) => Ok(LuaValue::Function(f)), - } - } -} - -/* - Proxy enum to deal with both threads & task scheduler task references -*/ - -#[derive(Debug, Clone)] -pub enum LuaThreadOrTaskReference<'lua> { - Thread(LuaThread<'lua>), - TaskReference(TaskReference), -} - -impl<'lua> From> for LuaThreadOrTaskReference<'lua> { - fn from(value: LuaThread<'lua>) -> Self { - Self::Thread(value) - } -} - -impl<'lua> From for LuaThreadOrTaskReference<'lua> { - fn from(value: TaskReference) -> Self { - Self::TaskReference(value) - } -} - -impl<'lua> FromLua<'lua> for LuaThreadOrTaskReference<'lua> { - fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult { - let tname = value.type_name(); - match value { - LuaValue::Thread(t) => Ok(Self::Thread(t)), - LuaValue::UserData(u) => { - if let Ok(task) = - LuaUserDataRef::::from_lua(LuaValue::UserData(u), lua) - { - Ok(Self::TaskReference(*task)) - } else { - Err(LuaError::FromLuaConversionError { - from: tname, - to: "thread", - message: Some(format!("Expected thread, got '{tname}'")), - }) - } - } - _ => Err(LuaError::FromLuaConversionError { - from: tname, - to: "thread", - message: Some(format!("Expected thread, got '{tname}'")), - }), - } - } -} - -impl<'lua> IntoLua<'lua> for LuaThreadOrTaskReference<'lua> { - fn into_lua(self, lua: &'lua Lua) -> LuaResult> { - match self { - Self::TaskReference(t) => t.into_lua(lua), - Self::Thread(t) => Ok(LuaValue::Thread(t)), - } - } -} diff --git a/src/lune/lua/task/scheduler.rs b/src/lune/lua/task/scheduler.rs deleted file mode 100644 index c3ba01c..0000000 --- a/src/lune/lua/task/scheduler.rs +++ /dev/null @@ -1,480 +0,0 @@ -use core::panic; -use std::{ - cell::{Cell, RefCell}, - collections::{HashMap, VecDeque}, - process::ExitCode, - sync::Arc, -}; - -use futures_util::{future::LocalBoxFuture, stream::FuturesUnordered, Future}; -use mlua::prelude::*; - -use tokio::sync::{mpsc, Mutex as AsyncMutex}; - -use super::{ - scheduler_message::TaskSchedulerMessage, - task_waiter::{TaskWaiterFuture, TaskWaiterState}, -}; -pub use super::{task_kind::TaskKind, task_reference::TaskReference}; - -type TaskFutureRets<'fut> = LuaResult>>; -type TaskFuture<'fut> = LocalBoxFuture<'fut, (Option, TaskFutureRets<'fut>)>; - -/// A struct representing a task contained in the task scheduler -#[derive(Debug)] -pub struct Task { - kind: TaskKind, - thread: LuaRegistryKey, - args: LuaRegistryKey, -} - -/// A task scheduler that implements task queues -/// with instant, deferred, and delayed tasks -#[derive(Debug)] -pub struct TaskScheduler<'fut> { - /* - Lots of cell and refcell here, however we need full interior mutability and never outer - since the scheduler struct may be accessed from lua more than once at the same time. - - An example of this is the implementation of coroutine.resume, which instantly resumes the given - task, where the task getting resumed may also create new scheduler tasks during its resumption. - - The same goes for values used during resumption of futures (`futures` and `futures_rx`) - which must use async-aware mutexes to be cancellation safe across await points. - */ - // Internal state & flags - pub(super) lua: &'static Lua, - pub(super) guid: Cell, - pub(super) exit_code: Cell>, - // Blocking tasks - pub(super) tasks: RefCell>, - pub(super) tasks_count: Cell, - pub(super) tasks_current: Cell>, - pub(super) tasks_queue_blocking: RefCell>, - pub(super) tasks_waiter_states: - RefCell>>>>>, - pub(super) tasks_current_lua_error: Arc>>, - // Future tasks & objects for waking - pub(super) futures: AsyncMutex>>, - pub(super) futures_count: Cell, - pub(super) futures_background_count: Cell, - pub(super) futures_tx: mpsc::UnboundedSender, - pub(super) futures_rx: AsyncMutex>, -} - -impl<'fut> TaskScheduler<'fut> { - /** - Creates a new task scheduler. - */ - pub fn new(lua: &'static Lua) -> LuaResult { - let (tx, rx) = mpsc::unbounded_channel(); - let tasks_current_lua_error = Arc::new(AsyncMutex::new(None)); - let tasks_current_lua_error_inner = tasks_current_lua_error.clone(); - lua.set_interrupt(move |_| { - match tasks_current_lua_error_inner.try_lock().unwrap().take() { - Some(err) => Err(err), - None => Ok(LuaVmState::Continue), - } - }); - Ok(Self { - lua, - guid: Cell::new(0), - exit_code: Cell::new(None), - tasks: RefCell::new(HashMap::new()), - tasks_count: Cell::new(0), - tasks_current: Cell::new(None), - tasks_queue_blocking: RefCell::new(VecDeque::new()), - tasks_waiter_states: RefCell::new(HashMap::new()), - tasks_current_lua_error, - futures: AsyncMutex::new(FuturesUnordered::new()), - futures_tx: tx, - futures_rx: AsyncMutex::new(rx), - futures_count: Cell::new(0), - futures_background_count: Cell::new(0), - }) - } - - /** - Consumes and leaks the task scheduler, - returning a static reference `&'static TaskScheduler`. - - This function is useful when the task scheduler object is - supposed to live for the remainder of the program's life. - - Note that dropping the returned reference will cause a memory leak. - */ - pub fn into_static(self) -> &'static Self { - Box::leak(Box::new(self)) - } - - /** - Stores the exit code for the task scheduler. - - This will be passed back to the Rust thread that is running the task scheduler, - in the [`TaskSchedulerState`] returned on resumption of the task scheduler queue. - - Setting this exit code will signal to that thread that it - should stop resuming tasks, and gracefully terminate the program. - */ - pub fn set_exit_code(&self, code: ExitCode) { - self.exit_code.set(Some(code)); - } - - /** - Forwards a lua error to be emitted as soon as possible, - after any current blocking / queued tasks have been resumed. - - Useful when an async function may call into Lua and get a - result back, without erroring out of the entire async block. - */ - pub fn forward_lua_error(&self, err: LuaError) { - let sender = self.futures_tx.clone(); - sender - .send(TaskSchedulerMessage::NewLuaErrorReady(err)) - .unwrap_or_else(|e| { - panic!( - "\ - \nFailed to forward lua error - this is an internal error! \ - \nPlease report it at {} \ - \nDetails: {e} \ - ", - env!("CARGO_PKG_REPOSITORY") - ) - }); - } - - /** - Forces the current task to be set to the given reference. - - Useful if a task is to be resumed externally but full - compatibility with the task scheduler is still necessary. - */ - pub(crate) fn force_set_current_task(&self, reference: Option) { - self.tasks_current.set(reference); - } - - /** - Checks if a task still exists in the scheduler. - - A task may no longer exist in the scheduler if it has been manually - cancelled and removed by calling [`TaskScheduler::cancel_task()`]. - */ - #[allow(dead_code)] - pub fn contains_task(&self, reference: TaskReference) -> bool { - self.tasks.borrow().contains_key(&reference) - } - - /** - Returns the currently running task, if any. - */ - pub fn current_task(&self) -> Option { - self.tasks_current.get() - } - - /** - Returns the status of a specific task, if it exists in the scheduler. - */ - pub fn get_task_status(&self, reference: TaskReference) -> Option { - self.tasks.borrow().get(&reference).map(|task| { - let status: LuaFunction = self - .lua - .named_registry_value("co.status") - .expect("Missing coroutine status function in registry"); - let thread: LuaThread = self - .lua - .registry_value(&task.thread) - .expect("Task thread missing from registry"); - status - .call(thread) - .expect("Task thread failed to call status") - }) - } - - /** - Creates a new task, storing a new Lua thread - for it, as well as the arguments to give the - thread on resumption, in the Lua registry. - - Note that this task will ***not*** resume on its - own, it needs to be used together with either the - scheduling functions or [`TaskScheduler::resume_task`]. - */ - pub fn create_task( - &self, - kind: TaskKind, - thread: LuaThread<'_>, - thread_args: Option>, - inherit_current_guid: bool, - ) -> LuaResult { - // Store the thread and its arguments in the registry - // NOTE: We must convert to a vec since multis - // can't be stored in the registry directly - let task_args_vec: Option> = thread_args.map(|opt| opt.into_vec()); - let task_args_key: LuaRegistryKey = self.lua.create_registry_value(task_args_vec)?; - let task_thread_key: LuaRegistryKey = self.lua.create_registry_value(thread)?; - // Create the full task struct - let task = Task { - kind, - thread: task_thread_key, - args: task_args_key, - }; - // Create the task ref to use - let guid = if inherit_current_guid { - self.current_task() - .ok_or_else(|| LuaError::RuntimeError("No current guid to inherit".to_string()))? - .id() - } else { - let guid = self.guid.get(); - self.guid.set(guid + 1); - guid - }; - let reference = TaskReference::new(kind, guid); - // Increment the corresponding task counter - match kind { - TaskKind::Future => self.futures_count.set(self.futures_count.get() + 1), - _ => self.tasks_count.set(self.tasks_count.get() + 1), - } - // Add the task to the scheduler - { - let mut tasks = self.tasks.borrow_mut(); - tasks.insert(reference, task); - } - Ok(reference) - } - - /** - Cancels a task, if the task still exists in the scheduler. - - It is possible to hold one or more task references that point - to a task that no longer exists in the scheduler, and calling - this method with one of those references will return `false`. - */ - pub fn remove_task(&self, reference: TaskReference) -> LuaResult { - /* - Remove the task from the task list and the Lua registry - - This is all we need to do since resume_task will always - ignore resumption of any task that no longer exists there - - This does lead to having some amount of "junk" futures that will - build up in the queue but these will get cleaned up and not block - the program from exiting since the scheduler only runs until there - are no tasks left in the task list, the futures do not matter there - */ - let mut found = false; - let mut tasks = self.tasks.borrow_mut(); - // Unfortunately we have to loop through to find which task - // references to remove instead of removing directly since - // tasks can switch kinds between instant, deferred, future - let tasks_to_remove: Vec<_> = tasks - .keys() - .filter(|task_ref| task_ref.id() == reference.id()) - .copied() - .collect(); - for task_ref in &tasks_to_remove { - if let Some(task) = tasks.remove(task_ref) { - // Decrement the corresponding task counter - match task.kind { - TaskKind::Future => self.futures_count.set(self.futures_count.get() - 1), - _ => self.tasks_count.set(self.tasks_count.get() - 1), - } - // NOTE: We need to close the thread here to - // make 100% sure that nothing can resume it - let close: LuaFunction = self.lua.named_registry_value("co.close")?; - let thread: LuaThread = self.lua.registry_value(&task.thread)?; - close.call(thread)?; - self.lua.remove_registry_value(task.thread)?; - self.lua.remove_registry_value(task.args)?; - found = true; - } - } - Ok(found) - } - - /** - Resumes a task, if the task still exists in the scheduler. - - A task may no longer exist in the scheduler if it has been manually - cancelled and removed by calling [`TaskScheduler::cancel_task()`]. - - This will be a no-op if the task no longer exists. - */ - pub fn resume_task<'a, 'r>( - &self, - reference: TaskReference, - override_args: Option>>, - ) -> LuaResult<(LuaThreadStatus, LuaMultiValue<'r>)> - where - 'a: 'r, - { - // Fetch and check if the task was removed, if it got - // removed it means it was intentionally cancelled - let task = { - let mut tasks = self.tasks.borrow_mut(); - match tasks.remove(&reference) { - Some(task) => task, - None => return Ok((LuaThreadStatus::Unresumable, LuaMultiValue::new())), - } - }; - // Decrement the corresponding task counter - match task.kind { - TaskKind::Future => self.futures_count.set(self.futures_count.get() - 1), - _ => self.tasks_count.set(self.tasks_count.get() - 1), - } - // Fetch and remove the thread to resume + its arguments - let thread: LuaThread = self.lua.registry_value(&task.thread)?; - let thread_args: Option = { - self.lua - .registry_value::>>(&task.args) - .expect("Failed to get stored args for task") - .map(LuaMultiValue::from_vec) - }; - self.lua.remove_registry_value(task.thread)?; - self.lua.remove_registry_value(task.args)?; - // We got everything we need and our references - // were cleaned up properly, resume the thread - self.tasks_current.set(Some(reference)); - let rets = match override_args { - Some(override_res) => match override_res { - Ok(args) => thread.resume(args), - Err(e) => { - // NOTE: Setting this error here means that when the thread - // is resumed it will error instantly, so we don't need - // to call it with proper args, empty args is fine - self.tasks_current_lua_error.try_lock().unwrap().replace(e); - thread.resume(()) - } - }, - None => match thread_args { - Some(args) => thread.resume(args), - None => thread.resume(()), - }, - }; - self.tasks_current.set(None); - match rets { - Ok(rets) => Ok((thread.status(), rets)), - Err(e) => Err(e), - } - } - - /** - Queues a new blocking task to run on the task scheduler. - */ - pub(crate) fn queue_blocking_task( - &self, - kind: TaskKind, - thread: LuaThread<'_>, - thread_args: Option>, - ) -> LuaResult { - if kind == TaskKind::Future { - panic!("Tried to schedule future using normal task schedule method") - } - let task_ref = self.create_task(kind, thread, thread_args, false)?; - // Add the task to the front of the queue, unless it - // should be deferred, in that case add it to the back - let mut queue = self.tasks_queue_blocking.borrow_mut(); - let num_prev_blocking_tasks = queue.len(); - if kind == TaskKind::Deferred { - queue.push_back(task_ref); - } else { - queue.push_front(task_ref); - } - /* - If we had any previous task and are currently async - waiting on tasks, we should send a signal to wake up - and run the new blocking task that was just queued - - This can happen in cases such as an async http - server waking up from a connection and then wanting to - run a lua callback in response, to create the.. response - */ - if num_prev_blocking_tasks == 0 { - self.futures_tx - .send(TaskSchedulerMessage::NewBlockingTaskReady) - .expect("Futures waker channel was closed") - } - Ok(task_ref) - } - - /** - Queues a new future to run on the task scheduler. - */ - pub(crate) fn queue_async_task( - &self, - thread: LuaThread<'_>, - thread_args: Option>, - fut: impl Future> + 'fut, - ) -> LuaResult { - let task_ref = self.create_task(TaskKind::Future, thread, thread_args, false)?; - let futs = self - .futures - .try_lock() - .expect("Tried to add future to queue during futures resumption"); - futs.push(Box::pin(async move { - let result = fut.await; - (Some(task_ref), result) - })); - Ok(task_ref) - } - - /** - Queues a new future to run on the task scheduler, - inheriting the task id of the currently running task. - */ - pub(crate) fn queue_async_task_inherited( - &self, - thread: LuaThread<'_>, - thread_args: Option>, - fut: impl Future> + 'fut, - ) -> LuaResult { - let task_ref = self.create_task(TaskKind::Future, thread, thread_args, true)?; - let futs = self - .futures - .try_lock() - .expect("Tried to add future to queue during futures resumption"); - futs.push(Box::pin(async move { - let result = fut.await; - (Some(task_ref), result) - })); - Ok(task_ref) - } - - /** - Waits for a task to complete. - - Panics if the task is not currently in the scheduler. - */ - pub(crate) async fn wait_for_task_completion( - &self, - reference: TaskReference, - ) -> LuaResult { - if !self.tasks.borrow().contains_key(&reference) { - panic!("Task does not exist in scheduler") - } - let state = TaskWaiterState::new(); - { - let mut all_states = self.tasks_waiter_states.borrow_mut(); - all_states - .entry(reference) - .or_insert_with(Vec::new) - .push(Arc::clone(&state)); - } - TaskWaiterFuture::new(&state).await - } - - /** - Wakes a task that has been completed and may have external code - waiting on it using [`TaskScheduler::wait_for_task_completion`]. - */ - pub(super) fn wake_completed_task( - &self, - reference: TaskReference, - result: LuaResult>, - ) { - if let Some(waiter_states) = self.tasks_waiter_states.borrow_mut().remove(&reference) { - for waiter_state in waiter_states { - waiter_state.try_lock().unwrap().finalize(result.clone()); - } - } - } -} diff --git a/src/lune/lua/task/scheduler_handle.rs b/src/lune/lua/task/scheduler_handle.rs deleted file mode 100644 index 08993fc..0000000 --- a/src/lune/lua/task/scheduler_handle.rs +++ /dev/null @@ -1,46 +0,0 @@ -use core::panic; - -use mlua::prelude::*; - -use tokio::sync::mpsc; - -use super::scheduler_message::TaskSchedulerMessage; - -/** - A handle to a registered asynchronous background task. - - [`TaskSchedulerAsyncHandle::unregister`] must be - called upon completion of the background task to - prevent the task scheduler from running indefinitely. -*/ -#[must_use = "Background tasks must be unregistered"] -#[derive(Debug)] -pub struct TaskSchedulerAsyncHandle { - unregistered: bool, - sender: mpsc::UnboundedSender, -} - -impl TaskSchedulerAsyncHandle { - pub fn new(sender: mpsc::UnboundedSender) -> Self { - Self { - unregistered: false, - sender, - } - } - - pub fn unregister(mut self, result: LuaResult<()>) { - self.unregistered = true; - self.sender - .send(TaskSchedulerMessage::Terminated(result)) - .unwrap_or_else(|_| { - panic!( - "\ - \nFailed to unregister background task - this is an internal error! \ - \nPlease report it at {} \ - \nDetails: Manual \ - ", - env!("CARGO_PKG_REPOSITORY") - ) - }); - } -} diff --git a/src/lune/lua/task/scheduler_message.rs b/src/lune/lua/task/scheduler_message.rs deleted file mode 100644 index cddc015..0000000 --- a/src/lune/lua/task/scheduler_message.rs +++ /dev/null @@ -1,11 +0,0 @@ -use mlua::prelude::*; - -/// Internal message enum for the task scheduler, used to notify -/// futures to wake up and schedule their respective blocking tasks -#[derive(Debug, Clone)] -pub enum TaskSchedulerMessage { - NewBlockingTaskReady, - NewLuaErrorReady(LuaError), - Spawned, - Terminated(LuaResult<()>), -} diff --git a/src/lune/lua/task/scheduler_state.rs b/src/lune/lua/task/scheduler_state.rs deleted file mode 100644 index f36e889..0000000 --- a/src/lune/lua/task/scheduler_state.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::{fmt, process::ExitCode}; - -use mlua::prelude::*; - -use super::scheduler::TaskScheduler; - -/// Struct representing the current state of the task scheduler -#[derive(Debug, Clone)] -#[must_use = "Scheduler state must be checked after every resumption"] -pub struct TaskSchedulerState { - pub(super) lua_error: Option, - pub(super) exit_code: Option, - pub(super) num_blocking: usize, - pub(super) num_futures: usize, - pub(super) num_background: usize, -} - -impl TaskSchedulerState { - pub(super) fn new(sched: &TaskScheduler) -> Self { - Self { - lua_error: None, - exit_code: sched.exit_code.get(), - num_blocking: sched.tasks_count.get(), - num_futures: sched.futures_count.get(), - num_background: sched.futures_background_count.get(), - } - } - - pub(super) fn err(sched: &TaskScheduler, err: LuaError) -> Self { - let mut this = Self::new(sched); - this.lua_error = Some(err); - this - } - - /** - Returns a clone of the error from - this task scheduler result, if any. - */ - pub fn get_lua_error(&self) -> Option { - self.lua_error.clone() - } - - /** - Returns a clone of the exit code from - this task scheduler result, if any. - */ - pub fn get_exit_code(&self) -> Option { - self.exit_code - } - - /** - Returns `true` if the task scheduler still - has blocking lua threads left to run. - */ - pub fn is_blocking(&self) -> bool { - self.num_blocking > 0 - } - - /** - Returns `true` if the task scheduler has finished all - blocking lua tasks, but still has yielding tasks running. - */ - pub fn is_yielding(&self) -> bool { - self.num_blocking == 0 && self.num_futures > 0 - } - - /** - Returns `true` if the task scheduler has finished all - lua threads, but still has background tasks running. - */ - pub fn is_background(&self) -> bool { - self.num_blocking == 0 && self.num_futures == 0 && self.num_background > 0 - } - - /** - Returns `true` if the task scheduler is done, - meaning it has no lua threads left to run, and - no spawned tasks are running in the background. - - Also returns `true` if a task has requested to exit the process. - */ - pub fn is_done(&self) -> bool { - self.exit_code.is_some() - || (self.num_blocking == 0 && self.num_futures == 0 && self.num_background == 0) - } -} - -impl fmt::Display for TaskSchedulerState { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let status = if self.is_blocking() { - "Busy" - } else if self.is_yielding() { - "Yielding" - } else if self.is_background() { - "Background" - } else { - "Done" - }; - let code = match self.get_exit_code() { - Some(code) => format!("{code:?}"), - None => "-".to_string(), - }; - let err = match self.get_lua_error() { - Some(e) => format!("{e:?}") - .as_bytes() - .chunks(42) // Kinda arbitrary but should fit in most terminals - .enumerate() - .map(|(idx, buf)| { - format!( - "{}{}{}{}{}", - if idx == 0 { "" } else { "\n│ " }, - if idx == 0 { - "".to_string() - } else { - " ".repeat(16) - }, - if idx == 0 { "" } else { " │ " }, - String::from_utf8_lossy(buf), - if buf.len() == 42 { " │" } else { "" }, - ) - }) - .collect::(), - None => "-".to_string(), - }; - let parts = vec![ - format!("Status │ {status}"), - format!("Tasks active │ {}", self.num_blocking), - format!("Tasks background │ {}", self.num_background), - format!("Status code │ {code}"), - format!("Lua error │ {err}"), - ]; - let lengths = parts - .iter() - .map(|part| { - part.lines() - .next() - .unwrap() - .trim_end_matches(" │") - .chars() - .count() - }) - .collect::>(); - let longest = &parts - .iter() - .enumerate() - .fold(0, |acc, (index, _)| acc.max(lengths[index])); - let sep = "─".repeat(longest + 2); - writeln!(f, "┌{}┐", &sep)?; - for (index, part) in parts.iter().enumerate() { - writeln!( - f, - "│ {}{} │", - part.trim_end_matches(" │"), - " ".repeat( - longest - - part - .lines() - .last() - .unwrap() - .trim_end_matches(" │") - .chars() - .count() - ) - )?; - if index < parts.len() - 1 { - writeln!(f, "┝{}┥", &sep)?; - } - } - write!(f, "└{}┘", &sep)?; - Ok(()) - } -} diff --git a/src/lune/lua/task/task_kind.rs b/src/lune/lua/task/task_kind.rs deleted file mode 100644 index 6a82f49..0000000 --- a/src/lune/lua/task/task_kind.rs +++ /dev/null @@ -1,39 +0,0 @@ -use std::fmt; - -/// Enum representing different kinds of tasks -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum TaskKind { - Instant, - Deferred, - Future, -} - -#[allow(dead_code)] -impl TaskKind { - pub fn is_instant(&self) -> bool { - *self == Self::Instant - } - - pub fn is_deferred(&self) -> bool { - *self == Self::Deferred - } - - pub fn is_blocking(&self) -> bool { - *self != Self::Future - } - - pub fn is_future(&self) -> bool { - *self == Self::Future - } -} - -impl fmt::Display for TaskKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let name: &'static str = match self { - TaskKind::Instant => "Instant", - TaskKind::Deferred => "Deferred", - TaskKind::Future => "Future", - }; - write!(f, "{name}") - } -} diff --git a/src/lune/lua/task/task_reference.rs b/src/lune/lua/task/task_reference.rs deleted file mode 100644 index de1967a..0000000 --- a/src/lune/lua/task/task_reference.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::{ - fmt, - hash::{Hash, Hasher}, -}; - -use mlua::prelude::*; - -use super::task_kind::TaskKind; - -/// A lightweight, copyable struct that represents a -/// task in the scheduler and is accessible from Lua -#[derive(Debug, Clone, Copy)] -pub struct TaskReference { - kind: TaskKind, - guid: usize, -} - -impl TaskReference { - pub const fn new(kind: TaskKind, guid: usize) -> Self { - Self { kind, guid } - } - - pub const fn id(&self) -> usize { - self.guid - } -} - -impl fmt::Display for TaskReference { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.guid == 0 { - write!(f, "TaskReference(MAIN)") - } else { - write!(f, "TaskReference({} - {})", self.kind, self.guid) - } - } -} - -impl Eq for TaskReference {} -impl PartialEq for TaskReference { - fn eq(&self, other: &Self) -> bool { - self.guid == other.guid - } -} - -impl Hash for TaskReference { - fn hash(&self, state: &mut H) { - self.guid.hash(state); - } -} - -impl LuaUserData for TaskReference {} diff --git a/src/lune/lua/task/task_waiter.rs b/src/lune/lua/task/task_waiter.rs deleted file mode 100644 index 3be53b2..0000000 --- a/src/lune/lua/task/task_waiter.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{ - future::Future, - pin::Pin, - sync::Arc, - task::{Context, Poll, Waker}, -}; - -use tokio::sync::Mutex as AsyncMutex; - -use mlua::prelude::*; - -#[derive(Debug, Clone)] -pub(super) struct TaskWaiterState<'fut> { - rets: Option>>, - waker: Option, -} - -impl<'fut> TaskWaiterState<'fut> { - pub fn new() -> Arc> { - Arc::new(AsyncMutex::new(TaskWaiterState { - rets: None, - waker: None, - })) - } - - pub fn finalize(&mut self, rets: LuaResult>) { - self.rets = Some(rets); - if let Some(waker) = self.waker.take() { - waker.wake(); - } - } -} - -#[derive(Debug)] -pub(super) struct TaskWaiterFuture<'fut> { - state: Arc>>, -} - -impl<'fut> TaskWaiterFuture<'fut> { - pub fn new(state: &Arc>>) -> Self { - Self { - state: Arc::clone(state), - } - } -} - -impl<'fut> Clone for TaskWaiterFuture<'fut> { - fn clone(&self) -> Self { - Self { - state: Arc::clone(&self.state), - } - } -} - -impl<'fut> Future for TaskWaiterFuture<'fut> { - type Output = LuaResult>; - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut shared_state = self.state.try_lock().unwrap(); - if let Some(rets) = shared_state.rets.clone() { - Poll::Ready(rets) - } else { - shared_state.waker = Some(cx.waker().clone()); - Poll::Pending - } - } -} diff --git a/src/lune/mod.rs b/src/lune/mod.rs index a5050b7..49c8a45 100644 --- a/src/lune/mod.rs +++ b/src/lune/mod.rs @@ -1,107 +1,82 @@ use std::process::ExitCode; -use lua::task::{TaskScheduler, TaskSchedulerResumeExt, TaskSchedulerScheduleExt}; -use mlua::prelude::*; -use tokio::task::LocalSet; - -pub mod builtins; -pub mod importer; -pub mod lua; +use mlua::Lua; +mod builtins; mod error; +mod globals; +mod scheduler; + +pub(crate) mod util; + +use self::scheduler::{LuaSchedulerExt, Scheduler}; pub use error::LuneError; -#[derive(Clone, Debug, Default)] +// TODO: Rename this struct to "Runtime" instead for the +// next breaking release, it's a more fitting name and +// will probably be more obvious when browsing files +#[derive(Debug, Clone)] pub struct Lune { + lua: &'static Lua, + scheduler: &'static Scheduler<'static>, args: Vec, } impl Lune { /** - Creates a new Lune script runner. + Creates a new Lune runtime, with a new Luau VM and task scheduler. */ + #[allow(clippy::new_without_default)] pub fn new() -> Self { - Self::default() + /* + FUTURE: Stop leaking these when we have removed the lifetime + on the scheduler and can place them in lua app data using arc + + See the scheduler struct for more notes + */ + let lua = Lua::new().into_static(); + let scheduler = Scheduler::new().into_static(); + + lua.set_scheduler(scheduler); + globals::inject_all(lua).expect("Failed to inject lua globals"); + + Self { + lua, + scheduler, + args: Vec::new(), + } } /** - Arguments to give in `process.args` for a Lune script. + Sets arguments to give in `process.args` for Lune scripts. */ pub fn with_args(mut self, args: V) -> Self where V: Into>, { self.args = args.into(); + self.lua.set_app_data(self.args.clone()); self } /** - Runs a Lune script. + Runs a Lune script inside of the current runtime. - This will create a new sandboxed Luau environment with the configured - globals and arguments, running inside of a [`tokio::task::LocalSet`]. - - Some Lune globals may spawn separate tokio tasks on other threads, but the Luau - environment itself is guaranteed to run on a single thread in the local set. - - Note that this will create a static Lua instance and task scheduler that will - both live for the remainer of the program, and that this leaks memory using - [`Box::leak`] that will then get deallocated when the program exits. + This will preserve any modifications to global values / context. */ pub async fn run( - &self, + &mut self, script_name: impl AsRef, script_contents: impl AsRef<[u8]>, ) -> Result { - self.run_inner(script_name, script_contents) - .await - .map_err(LuneError::from) - } - - async fn run_inner( - &self, - script_name: impl AsRef, - script_contents: impl AsRef<[u8]>, - ) -> Result { - // Create our special lune-flavored Lua object with extra registry values - let lua = lua::create_lune_lua()?.into_static(); - // Create our task scheduler and all globals - // NOTE: Some globals require the task scheduler to exist on startup - let sched = TaskScheduler::new(lua)?.into_static(); - lua.set_app_data(sched); - importer::create(lua, self.args.clone())?; - // Create the main thread and schedule it - let main_chunk = lua + let main = self + .lua .load(script_contents.as_ref()) - .set_name(script_name.as_ref()) - .into_function()?; - let main_thread = lua.create_thread(main_chunk)?; - let main_thread_args = LuaValue::Nil.into_lua_multi(lua)?; - sched.schedule_blocking(main_thread, main_thread_args)?; - // Keep running the scheduler until there are either no tasks - // left to run, or until a task requests to exit the process - let exit_code = LocalSet::new() - .run_until(async move { - let mut got_error = false; - loop { - let result = sched.resume_queue().await; - if let Some(err) = result.get_lua_error() { - eprintln!("{}", LuneError::from(err)); - got_error = true; - } - if result.is_done() { - if let Some(exit_code) = result.get_exit_code() { - break exit_code; - } else if got_error { - break ExitCode::FAILURE; - } else { - break ExitCode::SUCCESS; - } - } - } - }) - .await; - Ok(exit_code) + .set_name(script_name.as_ref()); + + self.scheduler.push_back(self.lua, main, ())?; + + Ok(self.scheduler.run_to_completion(self.lua).await) } } diff --git a/src/lune/scheduler/impl_async.rs b/src/lune/scheduler/impl_async.rs new file mode 100644 index 0000000..019b743 --- /dev/null +++ b/src/lune/scheduler/impl_async.rs @@ -0,0 +1,138 @@ +use futures_util::Future; +use mlua::prelude::*; +use tokio::{ + sync::oneshot::{self, Receiver}, + task, +}; + +use super::{IntoLuaThread, Scheduler}; + +impl<'fut> Scheduler<'fut> { + /** + Checks if there are any futures to run, for + lua futures and background futures respectively. + */ + pub(super) fn has_futures(&self) -> (bool, bool) { + ( + self.futures_lua + .try_lock() + .expect("Failed to lock lua futures for check") + .len() + > 0, + self.futures_background + .try_lock() + .expect("Failed to lock background futures for check") + .len() + > 0, + ) + } + + /** + Schedules a plain future to run in the background. + + This will potentially spawn the future on a different thread, using + [`task::spawn`], meaning the provided future must implement [`Send`]. + + Returns a [`Receiver`] which may be `await`-ed + to retrieve the result of the spawned future. + + This [`Receiver`] may be safely ignored if the result of the + spawned future is not needed, the future will run either way. + */ + pub fn spawn(&self, fut: F) -> Receiver + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let (tx, rx) = oneshot::channel(); + + let handle = task::spawn(async move { + let res = fut.await; + tx.send(res).ok(); + }); + + // NOTE: We must spawn a future on our scheduler which awaits + // the handle from tokio to start driving our future properly + let futs = self + .futures_background + .try_lock() + .expect("Failed to lock futures queue for background tasks"); + futs.push(Box::pin(async move { + handle.await.ok(); + })); + + // NOTE: We might be resuming lua futures, need to signal that a + // new background future is ready to break out of futures resumption + self.state.message_sender().send_spawned_background_future(); + + rx + } + + /** + Equivalent to [`spawn`], except the future is only + spawned on the Lune scheduler, and on the main thread. + */ + pub fn spawn_local(&self, fut: F) -> Receiver + where + F: Future + 'static, + F::Output: 'static, + { + let (tx, rx) = oneshot::channel(); + + let futs = self + .futures_background + .try_lock() + .expect("Failed to lock futures queue for background tasks"); + futs.push(Box::pin(async move { + let res = fut.await; + tx.send(res).ok(); + })); + + // NOTE: We might be resuming lua futures, need to signal that a + // new background future is ready to break out of futures resumption + self.state.message_sender().send_spawned_background_future(); + + rx + } + + /** + Schedules the given `thread` to run when the given `fut` completes. + + If the given future returns a [`LuaError`], that error will be passed to the given `thread`. + */ + pub fn spawn_thread( + &'fut self, + lua: &'fut Lua, + thread: impl IntoLuaThread<'fut>, + fut: F, + ) -> LuaResult<()> + where + FR: IntoLuaMulti<'fut>, + F: Future> + 'fut, + { + let thread = thread.into_lua_thread(lua)?; + let futs = self.futures_lua.try_lock().expect( + "Failed to lock futures queue - \ + can't schedule future lua threads during futures resumption", + ); + + futs.push(Box::pin(async move { + match fut.await.and_then(|rets| rets.into_lua_multi(lua)) { + Err(e) => { + self.push_err(lua, thread, e) + .expect("Failed to schedule future err thread"); + } + Ok(v) => { + self.push_back(lua, thread, v) + .expect("Failed to schedule future thread"); + } + } + })); + + // NOTE: We might be resuming background futures, need to signal that a + // new background future is ready to break out of futures resumption + self.state.message_sender().send_spawned_lua_future(); + + Ok(()) + } +} diff --git a/src/lune/scheduler/impl_runner.rs b/src/lune/scheduler/impl_runner.rs new file mode 100644 index 0000000..b4c1c61 --- /dev/null +++ b/src/lune/scheduler/impl_runner.rs @@ -0,0 +1,260 @@ +use std::{process::ExitCode, sync::Arc}; + +use futures_util::StreamExt; +use mlua::prelude::*; + +use tokio::task::LocalSet; +use tracing::debug; + +use crate::lune::util::traits::LuaEmitErrorExt; + +use super::Scheduler; + +impl<'fut> Scheduler<'fut> { + /** + Runs all lua threads to completion. + */ + fn run_lua_threads(&self, lua: &Lua) { + if self.state.has_exit_code() { + return; + } + + let mut count = 0; + + // Pop threads from the scheduler until there are none left + while let Some(thread) = self + .pop_thread() + .expect("Failed to pop thread from scheduler") + { + // Deconstruct the scheduler thread into its parts + let thread_id = thread.id(); + let (thread, args) = thread.into_inner(lua); + + // Make sure this thread is still resumable, it might have + // been resumed somewhere else or even have been cancelled + if thread.status() != LuaThreadStatus::Resumable { + continue; + } + + // Resume the thread, ensuring that the schedulers + // current thread id is set correctly for error catching + self.state.set_current_thread_id(Some(thread_id)); + let res = thread.resume::<_, LuaMultiValue>(args); + self.state.set_current_thread_id(None); + + count += 1; + + // If we got any resumption (lua-side) error, increment + // the error count of the scheduler so we can exit with + // a non-zero exit code, and print it out to stderr + if let Err(err) = &res { + self.state.increment_error_count(); + lua.emit_error(err.clone()); + } + + // If the thread has finished running completely, + // send results of final resume to any listeners + if thread.status() != LuaThreadStatus::Resumable { + // NOTE: Threads that were spawned to resume + // with an error will not have a result sender + if let Some(sender) = self.thread_senders.borrow_mut().remove(&thread_id) { + if sender.receiver_count() > 0 { + let stored = match res { + Err(e) => Err(e), + Ok(v) => Ok(Arc::new(lua.create_registry_value(v.into_vec()).expect( + "Failed to store thread results in registry - out of memory", + ))), + }; + sender + .send(stored) + .expect("Failed to broadcast thread results"); + } + } + } + + if self.state.has_exit_code() { + break; + } + } + + if count > 0 { + debug! { + %count, + "resumed lua" + } + } + } + + /** + Runs the next lua future to completion. + + Panics if no lua future is queued. + */ + async fn run_future_lua(&self) { + let mut futs = self + .futures_lua + .try_lock() + .expect("Failed to lock lua futures for resumption"); + assert!(futs.len() > 0, "No lua futures are queued"); + futs.next().await; + } + + /** + Runs the next background future to completion. + + Panics if no background future is queued. + */ + async fn run_future_background(&self) { + let mut futs = self + .futures_background + .try_lock() + .expect("Failed to lock background futures for resumption"); + assert!(futs.len() > 0, "No background futures are queued"); + futs.next().await; + } + + /** + Runs as many futures as possible, until a new lua thread + is ready, or an exit code has been set for the scheduler. + + ### Implementation details + + Running futures on our scheduler consists of a couple moving parts: + + 1. An unordered futures queue for lua (main thread, local) futures + 2. An unordered futures queue for background (multithreaded, 'static lifetime) futures + 3. A signal for breaking out of futures resumption + + The two unordered futures queues need to run concurrently, + but since `FuturesUnordered` returns instantly if it does + not currently have any futures queued on it, we need to do + this branching loop, checking if each queue has futures first. + + We also need to listen for our signal, to see if we should break out of resumption: + + * Always break out of resumption if a new lua thread is ready + * Always break out of resumption if an exit code has been set + * Break out of lua futures resumption if we have a new background future + * Break out of background futures resumption if we have a new lua future + + We need to listen for both future queues concurrently, + and break out whenever the other corresponding queue has + a new future, since the other queue may resume sooner. + */ + async fn run_futures(&self) { + let (mut has_lua, mut has_background) = self.has_futures(); + if !has_lua && !has_background { + return; + } + + let mut rx = self.state.message_receiver(); + let mut count = 0; + + while has_lua || has_background { + if has_lua && has_background { + tokio::select! { + _ = self.run_future_lua() => {}, + _ = self.run_future_background() => {}, + msg = rx.recv() => { + if let Some(msg) = msg { + if msg.should_break_futures() { + break; + } + } + } + } + count += 1; + } else if has_lua { + tokio::select! { + _ = self.run_future_lua() => {}, + msg = rx.recv() => { + if let Some(msg) = msg { + if msg.should_break_lua_futures() { + break; + } + } + } + } + count += 1; + } else if has_background { + tokio::select! { + _ = self.run_future_background() => {}, + msg = rx.recv() => { + if let Some(msg) = msg { + if msg.should_break_background_futures() { + break; + } + } + } + } + count += 1; + } + (has_lua, has_background) = self.has_futures(); + } + + if count > 0 { + debug! { + %count, + "resumed lua futures" + } + } + } + + /** + Runs the scheduler to completion in a [`LocalSet`], + both normal lua threads and futures, prioritizing + lua threads over completion of any pending futures. + + Will emit lua output and errors to stdout and stderr. + */ + pub async fn run_to_completion(&self, lua: &Lua) -> ExitCode { + if let Some(code) = self.state.exit_code() { + return ExitCode::from(code); + } + + let set = LocalSet::new(); + let _guard = set.enter(); + + loop { + // 1. Run lua threads until exit or there are none left + self.run_lua_threads(lua); + + // 2. If we got a manual exit code from lua we should + // not try to wait for any pending futures to complete + if self.state.has_exit_code() { + break; + } + + // 3. Keep resuming futures until there are no futures left to + // resume, or until we manually break out of resumption for any + // reason, this may be because a future spawned a new lua thread + self.run_futures().await; + + // 4. Once again, check for an exit code, in case a future sets one + if self.state.has_exit_code() { + break; + } + + // 5. If we have no lua threads or futures remaining, + // we have now run the scheduler until completion + let (has_future_lua, has_future_background) = self.has_futures(); + if !has_future_lua && !has_future_background && !self.has_thread() { + break; + } + } + + if let Some(code) = self.state.exit_code() { + debug! { + %code, + "scheduler ran to completion" + }; + ExitCode::from(code) + } else if self.state.has_errored() { + debug!("scheduler ran to completion, with failure"); + ExitCode::FAILURE + } else { + debug!("scheduler ran to completion, with success"); + ExitCode::SUCCESS + } + } +} diff --git a/src/lune/scheduler/impl_threads.rs b/src/lune/scheduler/impl_threads.rs new file mode 100644 index 0000000..850d3c2 --- /dev/null +++ b/src/lune/scheduler/impl_threads.rs @@ -0,0 +1,181 @@ +use std::sync::Arc; + +use mlua::prelude::*; + +use super::{ + thread::{SchedulerThread, SchedulerThreadId, SchedulerThreadSender}, + IntoLuaThread, Scheduler, +}; + +impl<'fut> Scheduler<'fut> { + /** + Checks if there are any lua threads to run. + */ + pub(super) fn has_thread(&self) -> bool { + !self + .threads + .try_borrow() + .expect("Failed to borrow threads vec") + .is_empty() + } + + /** + Pops the next thread to run, from the front of the scheduler. + + Returns `None` if there are no threads left to run. + */ + pub(super) fn pop_thread(&self) -> LuaResult> { + match self + .threads + .try_borrow_mut() + .into_lua_err() + .context("Failed to borrow threads vec")? + .pop_front() + { + Some(thread) => Ok(Some(thread)), + None => Ok(None), + } + } + + /** + Schedules the `thread` to be resumed with the given [`LuaError`]. + */ + pub fn push_err<'a>( + &self, + lua: &'a Lua, + thread: impl IntoLuaThread<'a>, + err: LuaError, + ) -> LuaResult<()> { + let thread = thread.into_lua_thread(lua)?; + let args = LuaMultiValue::new(); // Will be resumed with error, don't need real args + + let thread = SchedulerThread::new(lua, thread, args); + let thread_id = thread.id(); + + self.state.set_thread_error(thread_id, err); + self.threads + .try_borrow_mut() + .into_lua_err() + .context("Failed to borrow threads vec")? + .push_front(thread); + + // NOTE: We might be resuming futures, need to signal that a + // new lua thread is ready to break out of futures resumption + self.state.message_sender().send_pushed_lua_thread(); + + Ok(()) + } + + /** + Schedules the `thread` to be resumed with the given `args` + right away, before any other currently scheduled threads. + */ + pub fn push_front<'a>( + &self, + lua: &'a Lua, + thread: impl IntoLuaThread<'a>, + args: impl IntoLuaMulti<'a>, + ) -> LuaResult { + let thread = thread.into_lua_thread(lua)?; + let args = args.into_lua_multi(lua)?; + + let thread = SchedulerThread::new(lua, thread, args); + let thread_id = thread.id(); + + self.threads + .try_borrow_mut() + .into_lua_err() + .context("Failed to borrow threads vec")? + .push_front(thread); + + // NOTE: We might be resuming the same thread several times and + // pushing it to the scheduler several times before it is done, + // and we should only ever create one result sender per thread + self.thread_senders + .borrow_mut() + .entry(thread_id) + .or_insert_with(|| SchedulerThreadSender::new(1)); + + // NOTE: We might be resuming futures, need to signal that a + // new lua thread is ready to break out of futures resumption + self.state.message_sender().send_pushed_lua_thread(); + + Ok(thread_id) + } + + /** + Schedules the `thread` to be resumed with the given `args` + after all other current threads have been resumed. + */ + pub fn push_back<'a>( + &self, + lua: &'a Lua, + thread: impl IntoLuaThread<'a>, + args: impl IntoLuaMulti<'a>, + ) -> LuaResult { + let thread = thread.into_lua_thread(lua)?; + let args = args.into_lua_multi(lua)?; + + let thread = SchedulerThread::new(lua, thread, args); + let thread_id = thread.id(); + + self.threads + .try_borrow_mut() + .into_lua_err() + .context("Failed to borrow threads vec")? + .push_back(thread); + + // NOTE: We might be resuming the same thread several times and + // pushing it to the scheduler several times before it is done, + // and we should only ever create one result sender per thread + self.thread_senders + .borrow_mut() + .entry(thread_id) + .or_insert_with(|| SchedulerThreadSender::new(1)); + + // NOTE: We might be resuming futures, need to signal that a + // new lua thread is ready to break out of futures resumption + self.state.message_sender().send_pushed_lua_thread(); + + Ok(thread_id) + } + + /** + Waits for the given thread to finish running, and returns its result. + */ + pub async fn wait_for_thread<'a>( + &self, + lua: &'a Lua, + thread_id: SchedulerThreadId, + ) -> LuaResult> { + let mut recv = { + let senders = self.thread_senders.borrow(); + let sender = senders + .get(&thread_id) + .expect("Tried to wait for thread that is not queued"); + sender.subscribe() + }; + let res = match recv.recv().await { + Err(_) => panic!("Sender was dropped while waiting for {thread_id:?}"), + Ok(r) => r, + }; + match res { + Err(e) => Err(e), + Ok(k) => { + let vals = lua + .registry_value::>(&k) + .expect("Received invalid registry key for thread"); + + // NOTE: This is not strictly necessary, mlua can clean + // up registry values on its own, but doing this will add + // some extra safety and clean up registry values faster + if let Some(key) = Arc::into_inner(k) { + lua.remove_registry_value(key) + .expect("Failed to remove registry key for thread"); + } + + Ok(LuaMultiValue::from_vec(vals)) + } + } + } +} diff --git a/src/lune/scheduler/message.rs b/src/lune/scheduler/message.rs new file mode 100644 index 0000000..4d05343 --- /dev/null +++ b/src/lune/scheduler/message.rs @@ -0,0 +1,98 @@ +use std::sync::{MutexGuard, TryLockError}; + +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + +use super::state::SchedulerState; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub(crate) enum SchedulerMessage { + ExitCodeSet, + PushedLuaThread, + SpawnedLuaFuture, + SpawnedBackgroundFuture, +} + +impl SchedulerMessage { + pub fn should_break_futures(self) -> bool { + matches!(self, Self::ExitCodeSet | Self::PushedLuaThread) + } + + pub fn should_break_lua_futures(self) -> bool { + self.should_break_futures() || matches!(self, Self::SpawnedBackgroundFuture) + } + + pub fn should_break_background_futures(self) -> bool { + self.should_break_futures() || matches!(self, Self::SpawnedLuaFuture) + } +} + +/** + A message sender for the scheduler. + + As long as this sender is not dropped, the scheduler + will be kept alive, waiting for more messages to arrive. +*/ +pub(crate) struct SchedulerMessageSender(UnboundedSender); + +impl SchedulerMessageSender { + /** + Creates a new message sender for the scheduler. + */ + pub fn new(state: &SchedulerState) -> Self { + Self( + state + .message_sender + .lock() + .expect("Scheduler state was poisoned") + .clone(), + ) + } + + pub fn send_exit_code_set(&self) { + self.0.send(SchedulerMessage::ExitCodeSet).ok(); + } + + pub fn send_pushed_lua_thread(&self) { + self.0.send(SchedulerMessage::PushedLuaThread).ok(); + } + + pub fn send_spawned_lua_future(&self) { + self.0.send(SchedulerMessage::SpawnedLuaFuture).ok(); + } + + pub fn send_spawned_background_future(&self) { + self.0.send(SchedulerMessage::SpawnedBackgroundFuture).ok(); + } +} + +/** + A message receiver for the scheduler. + + Only one message receiver may exist per scheduler. +*/ +pub(crate) struct SchedulerMessageReceiver<'a>(MutexGuard<'a, UnboundedReceiver>); + +impl<'a> SchedulerMessageReceiver<'a> { + /** + Creates a new message receiver for the scheduler. + + Panics if the message receiver is already being used. + */ + pub fn new(state: &'a SchedulerState) -> Self { + Self(match state.message_receiver.try_lock() { + Err(TryLockError::Poisoned(_)) => panic!("Sheduler state was poisoned"), + Err(TryLockError::WouldBlock) => { + panic!("Message receiver may only be borrowed once at a time") + } + Ok(guard) => guard, + }) + } + + // NOTE: Holding this lock across await points is fine, since we + // can only ever create lock exactly one SchedulerMessageReceiver + // See above constructor for details on this + #[allow(clippy::await_holding_lock)] + pub async fn recv(&mut self) -> Option { + self.0.recv().await + } +} diff --git a/src/lune/scheduler/mod.rs b/src/lune/scheduler/mod.rs new file mode 100644 index 0000000..e153234 --- /dev/null +++ b/src/lune/scheduler/mod.rs @@ -0,0 +1,116 @@ +use std::{ + cell::RefCell, + collections::{HashMap, VecDeque}, + pin::Pin, + sync::Arc, +}; + +use futures_util::{stream::FuturesUnordered, Future}; +use mlua::prelude::*; +use tokio::sync::Mutex as AsyncMutex; + +mod message; +mod state; +mod thread; +mod traits; + +mod impl_async; +mod impl_runner; +mod impl_threads; + +pub use self::thread::SchedulerThreadId; +pub use self::traits::*; + +use self::{ + state::SchedulerState, + thread::{SchedulerThread, SchedulerThreadSender}, +}; + +type SchedulerFuture<'fut> = Pin + 'fut>>; + +/** + Scheduler for Lua threads and futures. + + This scheduler can be cheaply cloned and the underlying state + and data will remain unchanged and accessible from all clones. +*/ +#[derive(Debug, Clone)] +pub(crate) struct Scheduler<'fut> { + state: Arc, + threads: Arc>>, + thread_senders: Arc>>, + /* + FUTURE: Get rid of these, let the tokio runtime handle running + and resumption of futures completely, just use our scheduler + state and receiver to know when we have run to completion. + If we have no senders left, we have run to completion. + + Once we no longer store futures in our scheduler, we can + get rid of the lifetime on it, store it in our lua app + data as a Weak, together with a Weak. + + In our lua async functions we can then get a reference to this, + upgrade it to an Arc and Arc to extend lifetimes, + and hopefully get rid of Box::leak and 'static lifetimes for good. + + Relevant comment on the mlua repository: + https://github.com/khvzak/mlua/issues/169#issuecomment-1138863979 + */ + futures_lua: Arc>>>, + futures_background: Arc>>>, +} + +impl<'fut> Scheduler<'fut> { + /** + Creates a new scheduler. + */ + pub fn new() -> Self { + Self { + state: Arc::new(SchedulerState::new()), + threads: Arc::new(RefCell::new(VecDeque::new())), + thread_senders: Arc::new(RefCell::new(HashMap::new())), + futures_lua: Arc::new(AsyncMutex::new(FuturesUnordered::new())), + futures_background: Arc::new(AsyncMutex::new(FuturesUnordered::new())), + } + } + + /** + Sets the luau interrupt for this scheduler. + + This will propagate errors from any lua-spawned + futures back to the lua threads that spawned them. + */ + pub fn set_interrupt_for(&self, lua: &Lua) { + // Propagate errors given to the scheduler back to their lua threads + // FUTURE: Do profiling and anything else we need inside of this interrupt + let state = self.state.clone(); + lua.set_interrupt(move |_| { + if let Some(id) = state.get_current_thread_id() { + if let Some(err) = state.get_thread_error(id) { + return Err(err); + } + } + Ok(LuaVmState::Continue) + }); + } + + /** + Sets the exit code for the scheduler. + + This will stop the scheduler from resuming any more lua threads or futures. + + Panics if the exit code is set more than once. + */ + pub fn set_exit_code(&self, code: impl Into) { + assert!( + self.state.exit_code().is_none(), + "Exit code may only be set exactly once" + ); + self.state.set_exit_code(code.into()); + } + + #[doc(hidden)] + pub fn into_static(self) -> &'static Self { + Box::leak(Box::new(self)) + } +} diff --git a/src/lune/scheduler/state.rs b/src/lune/scheduler/state.rs new file mode 100644 index 0000000..d6682f1 --- /dev/null +++ b/src/lune/scheduler/state.rs @@ -0,0 +1,176 @@ +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering}, + Arc, Mutex, + }, +}; + +use mlua::Error as LuaError; + +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + +use super::{ + message::{SchedulerMessage, SchedulerMessageReceiver, SchedulerMessageSender}, + SchedulerThreadId, +}; + +/** + Internal state for a [`Scheduler`]. + + This scheduler state uses atomic operations for everything + except lua error storage, and is completely thread safe. +*/ +#[derive(Debug)] +pub(crate) struct SchedulerState { + exit_state: AtomicBool, + exit_code: AtomicU8, + num_resumptions: AtomicUsize, + num_errors: AtomicUsize, + thread_id: Arc>>, + thread_errors: Arc>>, + pub(super) message_sender: Arc>>, + pub(super) message_receiver: Arc>>, +} + +impl SchedulerState { + /** + Creates a new scheduler state. + */ + pub fn new() -> Self { + let (message_sender, message_receiver) = unbounded_channel(); + + Self { + exit_state: AtomicBool::new(false), + exit_code: AtomicU8::new(0), + num_resumptions: AtomicUsize::new(0), + num_errors: AtomicUsize::new(0), + thread_id: Arc::new(Mutex::new(None)), + thread_errors: Arc::new(Mutex::new(HashMap::new())), + message_sender: Arc::new(Mutex::new(message_sender)), + message_receiver: Arc::new(Mutex::new(message_receiver)), + } + } + + /** + Increments the total lua error count for the scheduler. + + This is used to determine if the scheduler should exit with + a non-zero exit code, when no exit code is explicitly set. + */ + pub fn increment_error_count(&self) { + self.num_errors.fetch_add(1, Ordering::Relaxed); + } + + /** + Checks if there have been any lua errors. + + This is used to determine if the scheduler should exit with + a non-zero exit code, when no exit code is explicitly set. + */ + pub fn has_errored(&self) -> bool { + self.num_errors.load(Ordering::SeqCst) > 0 + } + + /** + Gets the currently set exit code for the scheduler, if any. + */ + pub fn exit_code(&self) -> Option { + if self.exit_state.load(Ordering::SeqCst) { + Some(self.exit_code.load(Ordering::SeqCst)) + } else { + None + } + } + + /** + Checks if the scheduler has an explicit exit code set. + */ + pub fn has_exit_code(&self) -> bool { + self.exit_state.load(Ordering::SeqCst) + } + + /** + Sets the explicit exit code for the scheduler. + */ + pub fn set_exit_code(&self, code: impl Into) { + self.exit_state.store(true, Ordering::SeqCst); + self.exit_code.store(code.into(), Ordering::SeqCst); + self.message_sender().send_exit_code_set(); + } + + /** + Gets the currently running lua scheduler thread id, if any. + */ + pub fn get_current_thread_id(&self) -> Option { + *self + .thread_id + .lock() + .expect("Failed to lock current thread id") + } + + /** + Sets the currently running lua scheduler thread id. + + This must be set to `Some(id)` just before resuming a lua thread, + and `None` while no lua thread is being resumed. If set to `Some` + while the current thread id is also `Some`, this will panic. + + Must only be set once per thread id, although this + is not checked at runtime for performance reasons. + */ + pub fn set_current_thread_id(&self, id: Option) { + self.num_resumptions.fetch_add(1, Ordering::Relaxed); + let mut thread_id = self + .thread_id + .lock() + .expect("Failed to lock current thread id"); + assert!( + id.is_none() || thread_id.is_none(), + "Current thread id can not be overwritten" + ); + *thread_id = id; + } + + /** + Gets the [`LuaError`] (if any) for the given `id`. + + Note that this removes the error from the scheduler state completely. + */ + pub fn get_thread_error(&self, id: SchedulerThreadId) -> Option { + let mut thread_errors = self + .thread_errors + .lock() + .expect("Failed to lock thread errors"); + thread_errors.remove(&id) + } + + /** + Sets a [`LuaError`] for the given `id`. + + Note that this will replace any already existing [`LuaError`]. + */ + pub fn set_thread_error(&self, id: SchedulerThreadId, err: LuaError) { + let mut thread_errors = self + .thread_errors + .lock() + .expect("Failed to lock thread errors"); + thread_errors.insert(id, err); + } + + /** + Creates a new message sender for the scheduler. + */ + pub fn message_sender(&self) -> SchedulerMessageSender { + SchedulerMessageSender::new(self) + } + + /** + Tries to borrow the message receiver for the scheduler. + + Panics if the message receiver is already being used. + */ + pub fn message_receiver(&self) -> SchedulerMessageReceiver { + SchedulerMessageReceiver::new(self) + } +} diff --git a/src/lune/scheduler/thread.rs b/src/lune/scheduler/thread.rs new file mode 100644 index 0000000..ed8d083 --- /dev/null +++ b/src/lune/scheduler/thread.rs @@ -0,0 +1,105 @@ +use std::sync::Arc; + +use mlua::prelude::*; +use tokio::sync::broadcast::Sender; + +/** + Type alias for a broadcast [`Sender`], which will + broadcast the result and return values of a lua thread. + + The return values are stored in the lua registry as a + `Vec>`, and the registry key pointing to + those values will be sent using the broadcast sender. +*/ +pub type SchedulerThreadSender = Sender>>; + +/** + Unique, randomly generated id for a scheduler thread. +*/ +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +pub struct SchedulerThreadId(usize); + +impl From<&LuaThread<'_>> for SchedulerThreadId { + fn from(value: &LuaThread) -> Self { + // HACK: We rely on the debug format of mlua + // thread refs here, but currently this is the + // only way to get a proper unique id using mlua + let addr_string = format!("{value:?}"); + let addr = addr_string + .strip_prefix("Thread(Ref(0x") + .expect("Invalid thread address format - unknown prefix") + .split_once(')') + .map(|(s, _)| s) + .expect("Invalid thread address format - missing ')'"); + let id = usize::from_str_radix(addr, 16) + .expect("Failed to parse thread address as hexadecimal into usize"); + Self(id) + } +} + +/** + Container for registry keys that point to a thread and thread arguments. +*/ +#[derive(Debug)] +pub(super) struct SchedulerThread { + thread_id: SchedulerThreadId, + key_thread: LuaRegistryKey, + key_args: LuaRegistryKey, +} + +impl SchedulerThread { + /** + Creates a new scheduler thread container from the given thread and arguments. + + May fail if an allocation error occurs, is not fallible otherwise. + */ + pub(super) fn new<'lua>( + lua: &'lua Lua, + thread: LuaThread<'lua>, + args: LuaMultiValue<'lua>, + ) -> Self { + let args_vec = args.into_vec(); + let thread_id = SchedulerThreadId::from(&thread); + + let key_thread = lua + .create_registry_value(thread) + .expect("Failed to store thread in registry - out of memory"); + let key_args = lua + .create_registry_value(args_vec) + .expect("Failed to store thread args in registry - out of memory"); + + Self { + thread_id, + key_thread, + key_args, + } + } + + /** + Extracts the inner thread and args from the container. + */ + pub(super) fn into_inner(self, lua: &Lua) -> (LuaThread<'_>, LuaMultiValue<'_>) { + let thread = lua + .registry_value(&self.key_thread) + .expect("Failed to get thread from registry"); + let args_vec = lua + .registry_value(&self.key_args) + .expect("Failed to get thread args from registry"); + + let args = LuaMultiValue::from_vec(args_vec); + + lua.remove_registry_value(self.key_thread) + .expect("Failed to remove thread from registry"); + lua.remove_registry_value(self.key_args) + .expect("Failed to remove thread args from registry"); + + (thread, args) + } + + /** + Retrieves the unique, randomly generated id for this scheduler thread. + */ + pub(super) fn id(&self) -> SchedulerThreadId { + self.thread_id + } +} diff --git a/src/lune/scheduler/traits.rs b/src/lune/scheduler/traits.rs new file mode 100644 index 0000000..f95c253 --- /dev/null +++ b/src/lune/scheduler/traits.rs @@ -0,0 +1,118 @@ +use futures_util::Future; +use mlua::prelude::*; + +use super::Scheduler; + +const ASYNC_IMPL_LUA: &str = r#" +schedule(...) +return yield() +"#; + +/** + Trait for extensions to the [`Lua`] struct, allowing + for access to the scheduler without having to import + it or handle registry / app data references manually. +*/ +pub(crate) trait LuaSchedulerExt<'lua> { + /** + Sets the scheduler for the [`Lua`] struct. + */ + fn set_scheduler(&'lua self, scheduler: &'lua Scheduler); + + /** + Creates a function callable from Lua that runs an async + closure and returns the results of it to the call site. + */ + fn create_async_function(&'lua self, func: F) -> LuaResult> + where + A: FromLuaMulti<'lua>, + R: IntoLuaMulti<'lua>, + F: Fn(&'lua Lua, A) -> FR + 'lua, + FR: Future> + 'lua; +} + +// FIXME: `self` escapes outside of method because we are borrowing `func` +// when we call `schedule_future_thread` in the lua function body below +// For now we solve this by using the 'static lifetime bound in the impl +impl<'lua> LuaSchedulerExt<'lua> for Lua +where + 'lua: 'static, +{ + fn set_scheduler(&'lua self, scheduler: &'lua Scheduler) { + self.set_app_data(scheduler); + scheduler.set_interrupt_for(self); + } + + fn create_async_function(&'lua self, func: F) -> LuaResult> + where + A: FromLuaMulti<'lua>, + R: IntoLuaMulti<'lua>, + F: Fn(&'lua Lua, A) -> FR + 'lua, + FR: Future> + 'lua, + { + self.app_data_ref::<&Scheduler>() + .expect("Lua must have a scheduler to create async functions"); + + let async_env = self.create_table_with_capacity(0, 2)?; + + async_env.set( + "yield", + self.globals() + .get::<_, LuaTable>("coroutine")? + .get::<_, LuaFunction>("yield")?, + )?; + + async_env.set( + "schedule", + LuaFunction::wrap(move |lua: &Lua, args: A| { + let thread = lua.current_thread(); + let future = func(lua, args); + let sched = lua + .app_data_ref::<&Scheduler>() + .expect("Lua struct is missing scheduler"); + sched.spawn_thread(lua, thread, future)?; + Ok(()) + }), + )?; + + let async_func = self + .load(ASYNC_IMPL_LUA) + .set_name("async") + .set_environment(async_env) + .into_function()?; + Ok(async_func) + } +} + +/** + Trait for any struct that can be turned into an [`LuaThread`] + and given to the scheduler, implemented for the following types: + + - Lua threads ([`LuaThread`]) + - Lua functions ([`LuaFunction`]) + - Lua chunks ([`LuaChunk`]) +*/ +pub trait IntoLuaThread<'lua> { + /** + Converts the value into a lua thread. + */ + fn into_lua_thread(self, lua: &'lua Lua) -> LuaResult>; +} + +impl<'lua> IntoLuaThread<'lua> for LuaThread<'lua> { + fn into_lua_thread(self, _: &'lua Lua) -> LuaResult> { + Ok(self) + } +} + +impl<'lua> IntoLuaThread<'lua> for LuaFunction<'lua> { + fn into_lua_thread(self, lua: &'lua Lua) -> LuaResult> { + lua.create_thread(self) + } +} + +impl<'lua, 'a> IntoLuaThread<'lua> for LuaChunk<'lua, 'a> { + fn into_lua_thread(self, lua: &'lua Lua) -> LuaResult> { + lua.create_thread(self.into_function()?) + } +} diff --git a/src/lune/lua/stdio/formatting.rs b/src/lune/util/formatting.rs similarity index 97% rename from src/lune/lua/stdio/formatting.rs rename to src/lune/util/formatting.rs index 8437299..165ceb4 100644 --- a/src/lune/lua/stdio/formatting.rs +++ b/src/lune/util/formatting.rs @@ -4,8 +4,6 @@ use console::{colors_enabled, set_colors_enabled, style, Style}; use mlua::prelude::*; use once_cell::sync::Lazy; -use crate::lune::lua::task::TaskReference; - const MAX_FORMAT_DEPTH: usize = 4; const INDENT: &str = " "; @@ -174,12 +172,7 @@ pub fn pretty_format_value( LuaValue::Thread(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to(""))?, LuaValue::Function(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to(""))?, LuaValue::UserData(u) => { - if u.is::() { - // Task references must be transparent - // to lua and pretend to be normal lua - // threads for compatibility purposes - write!(buffer, "{}", COLOR_PURPLE.apply_to(""))? - } else if let Some(s) = call_userdata_tostring_metamethod(u) { + if let Some(s) = call_userdata_tostring_metamethod(u) { write!(buffer, "{s}")? } else { write!(buffer, "{}", COLOR_PURPLE.apply_to(""))? diff --git a/src/lune/util/mod.rs b/src/lune/util/mod.rs new file mode 100644 index 0000000..38984af --- /dev/null +++ b/src/lune/util/mod.rs @@ -0,0 +1,6 @@ +mod table_builder; + +pub mod formatting; +pub mod traits; + +pub use table_builder::TableBuilder; diff --git a/src/lune/lua/table/builder.rs b/src/lune/util/table_builder.rs similarity index 59% rename from src/lune/lua/table/builder.rs rename to src/lune/util/table_builder.rs index b320707..25ded8e 100644 --- a/src/lune/lua/table/builder.rs +++ b/src/lune/util/table_builder.rs @@ -1,25 +1,26 @@ +#![allow(dead_code)] + use std::future::Future; use mlua::prelude::*; -use crate::lune::lua::async_ext::LuaAsyncExt; +use crate::lune::scheduler::LuaSchedulerExt; -pub struct TableBuilder { - lua: &'static Lua, - tab: LuaTable<'static>, +pub struct TableBuilder<'lua> { + lua: &'lua Lua, + tab: LuaTable<'lua>, } -#[allow(dead_code)] -impl TableBuilder { - pub fn new(lua: &'static Lua) -> LuaResult { +impl<'lua> TableBuilder<'lua> { + pub fn new(lua: &'lua Lua) -> LuaResult { let tab = lua.create_table()?; Ok(Self { lua, tab }) } pub fn with_value(self, key: K, value: V) -> LuaResult where - K: IntoLua<'static>, - V: IntoLua<'static>, + K: IntoLua<'lua>, + V: IntoLua<'lua>, { self.tab.raw_set(key, value)?; Ok(self) @@ -27,8 +28,8 @@ impl TableBuilder { pub fn with_values(self, values: Vec<(K, V)>) -> LuaResult where - K: IntoLua<'static>, - V: IntoLua<'static>, + K: IntoLua<'lua>, + V: IntoLua<'lua>, { for (key, value) in values { self.tab.raw_set(key, value)?; @@ -38,7 +39,7 @@ impl TableBuilder { pub fn with_sequential_value(self, value: V) -> LuaResult where - V: IntoLua<'static>, + V: IntoLua<'lua>, { self.tab.raw_push(value)?; Ok(self) @@ -46,7 +47,7 @@ impl TableBuilder { pub fn with_sequential_values(self, values: Vec) -> LuaResult where - V: IntoLua<'static>, + V: IntoLua<'lua>, { for value in values { self.tab.raw_push(value)?; @@ -54,40 +55,47 @@ impl TableBuilder { Ok(self) } - pub fn with_metatable(self, table: LuaTable) -> LuaResult { - self.tab.set_metatable(Some(table)); - Ok(self) - } - pub fn with_function(self, key: K, func: F) -> LuaResult where - K: IntoLua<'static>, - A: FromLuaMulti<'static>, - R: IntoLuaMulti<'static>, - F: 'static + Fn(&'static Lua, A) -> LuaResult, + K: IntoLua<'lua>, + A: FromLuaMulti<'lua>, + R: IntoLuaMulti<'lua>, + F: Fn(&'lua Lua, A) -> LuaResult + 'static, { let f = self.lua.create_function(func)?; self.with_value(key, LuaValue::Function(f)) } - pub fn with_async_function(self, key: K, func: F) -> LuaResult - where - K: IntoLua<'static>, - A: FromLuaMulti<'static>, - R: IntoLuaMulti<'static>, - F: 'static + Fn(&'static Lua, A) -> FR, - FR: 'static + Future>, - { - let f = self.lua.create_async_function(func)?; - self.with_value(key, LuaValue::Function(f)) + pub fn with_metatable(self, table: LuaTable) -> LuaResult { + self.tab.set_metatable(Some(table)); + Ok(self) } - pub fn build_readonly(self) -> LuaResult> { + pub fn build_readonly(self) -> LuaResult> { self.tab.set_readonly(true); Ok(self.tab) } - pub fn build(self) -> LuaResult> { + pub fn build(self) -> LuaResult> { Ok(self.tab) } } + +// FIXME: Remove static lifetime bound here when `create_async_function` +// no longer needs it to compile, then move this into the above impl +impl<'lua> TableBuilder<'lua> +where + 'lua: 'static, +{ + pub fn with_async_function(self, key: K, func: F) -> LuaResult + where + K: IntoLua<'lua>, + A: FromLuaMulti<'lua>, + R: IntoLuaMulti<'lua>, + F: Fn(&'lua Lua, A) -> FR + 'lua, + FR: Future> + 'lua, + { + let f = self.lua.create_async_function(func)?; + self.with_value(key, LuaValue::Function(f)) + } +} diff --git a/src/lune/util/traits.rs b/src/lune/util/traits.rs new file mode 100644 index 0000000..32c514d --- /dev/null +++ b/src/lune/util/traits.rs @@ -0,0 +1,15 @@ +use mlua::prelude::*; + +use super::formatting::format_label; +use crate::LuneError; + +pub trait LuaEmitErrorExt { + fn emit_error(&self, err: LuaError); +} + +impl LuaEmitErrorExt for Lua { + fn emit_error(&self, err: LuaError) { + // NOTE: LuneError will pretty-format this error + eprintln!("{}\n{}", format_label("error"), LuneError::from(err)); + } +} diff --git a/src/main.rs b/src/main.rs index d5b3430..cd13300 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,9 +19,12 @@ use console::style; #[tokio::main(flavor = "multi_thread")] async fn main() -> ExitCode { - let logger_env = env_logger::Env::default().default_filter_or("error"); - env_logger::Builder::from_env(logger_env) - .format_timestamp(None) + tracing_subscriber::fmt() + .compact() + .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) + .with_target(true) + .with_timer(tracing_subscriber::fmt::time::uptime()) + .with_level(true) .init(); match Cli::parse().run().await { Ok(code) => code, diff --git a/src/roblox/datatypes/types/axes.rs b/src/roblox/datatypes/types/axes.rs index 74d9ba3..0db23f3 100644 --- a/src/roblox/datatypes/types/axes.rs +++ b/src/roblox/datatypes/types/axes.rs @@ -3,6 +3,8 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::Axes as DomAxes; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, EnumItem}; /** @@ -17,53 +19,58 @@ pub struct Axes { pub(crate) z: bool, } -impl Axes { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|_, args: LuaMultiValue| { - let mut x = false; - let mut y = false; - let mut z = false; - let mut check = |e: &EnumItem| { - if e.parent.desc.name == "Axis" { - match &e.name { - name if name == "X" => x = true, - name if name == "Y" => y = true, - name if name == "Z" => z = true, - _ => {} - } - } else if e.parent.desc.name == "NormalId" { - match &e.name { - name if name == "Left" || name == "Right" => x = true, - name if name == "Top" || name == "Bottom" => y = true, - name if name == "Front" || name == "Back" => z = true, - _ => {} - } +impl LuaExportsTable<'_> for Axes { + const EXPORT_NAME: &'static str = "Axes"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let axes_new = |_, args: LuaMultiValue| { + let mut x = false; + let mut y = false; + let mut z = false; + + let mut check = |e: &EnumItem| { + if e.parent.desc.name == "Axis" { + match &e.name { + name if name == "X" => x = true, + name if name == "Y" => y = true, + name if name == "Z" => z = true, + _ => {} } - }; - for (index, arg) in args.into_iter().enumerate() { - if let LuaValue::UserData(u) = arg { - if let Ok(e) = u.borrow::() { - check(&e); - } else { - return Err(LuaError::RuntimeError(format!( - "Expected argument #{} to be an EnumItem, got userdata", - index - ))); - } - } else { - return Err(LuaError::RuntimeError(format!( - "Expected argument #{} to be an EnumItem, got {}", - index, - arg.type_name() - ))); + } else if e.parent.desc.name == "NormalId" { + match &e.name { + name if name == "Left" || name == "Right" => x = true, + name if name == "Top" || name == "Bottom" => y = true, + name if name == "Front" || name == "Back" => z = true, + _ => {} } } - Ok(Axes { x, y, z }) - })?, - )?; - Ok(()) + }; + + for (index, arg) in args.into_iter().enumerate() { + if let LuaValue::UserData(u) = arg { + if let Ok(e) = u.borrow::() { + check(&e); + } else { + return Err(LuaError::RuntimeError(format!( + "Expected argument #{} to be an EnumItem, got userdata", + index + ))); + } + } else { + return Err(LuaError::RuntimeError(format!( + "Expected argument #{} to be an EnumItem, got {}", + index, + arg.type_name() + ))); + } + } + + Ok(Axes { x, y, z }) + }; + + TableBuilder::new(lua)? + .with_function("new", axes_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/brick_color.rs b/src/roblox/datatypes/types/brick_color.rs index a6aaf60..8fa6f86 100644 --- a/src/roblox/datatypes/types/brick_color.rs +++ b/src/roblox/datatypes/types/brick_color.rs @@ -4,6 +4,8 @@ use mlua::prelude::*; use rand::seq::SliceRandom; use rbx_dom_weak::types::BrickColor as DomBrickColor; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, Color3}; /** @@ -20,57 +22,58 @@ pub struct BrickColor { pub(crate) rgb: (u8, u8, u8), } -impl BrickColor { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { +impl LuaExportsTable<'_> for BrickColor { + const EXPORT_NAME: &'static str = "BrickColor"; + + fn create_exports_table(lua: &Lua) -> LuaResult { type ArgsNumber = u16; type ArgsName = String; type ArgsRgb = (u8, u8, u8); type ArgsColor3<'lua> = LuaUserDataRef<'lua, Color3>; - datatype_table.set( - "new", - lua.create_function(|lua, args: LuaMultiValue| { - if let Ok(number) = ArgsNumber::from_lua_multi(args.clone(), lua) { - Ok(color_from_number(number)) - } else if let Ok(name) = ArgsName::from_lua_multi(args.clone(), lua) { - Ok(color_from_name(name)) - } else if let Ok((r, g, b)) = ArgsRgb::from_lua_multi(args.clone(), lua) { - Ok(color_from_rgb(r, g, b)) - } else if let Ok(color) = ArgsColor3::from_lua_multi(args.clone(), lua) { - Ok(Self::from(*color)) - } else { - // FUTURE: Better error message here using given arg types - Err(LuaError::RuntimeError( - "Invalid arguments to constructor".to_string(), - )) - } - })?, - )?; - datatype_table.set( - "palette", - lua.create_function(|_, index: u16| { - if index == 0 { - Err(LuaError::RuntimeError("Invalid index".to_string())) - } else if let Some(number) = BRICK_COLOR_PALETTE.get((index - 1) as usize) { - Ok(color_from_number(*number)) - } else { - Err(LuaError::RuntimeError("Invalid index".to_string())) - } - })?, - )?; - datatype_table.set( - "random", - lua.create_function(|_, ()| { - let number = BRICK_COLOR_PALETTE.choose(&mut rand::thread_rng()); - Ok(color_from_number(*number.unwrap())) - })?, - )?; + + let brick_color_new = |lua, args: LuaMultiValue| { + if let Ok(number) = ArgsNumber::from_lua_multi(args.clone(), lua) { + Ok(color_from_number(number)) + } else if let Ok(name) = ArgsName::from_lua_multi(args.clone(), lua) { + Ok(color_from_name(name)) + } else if let Ok((r, g, b)) = ArgsRgb::from_lua_multi(args.clone(), lua) { + Ok(color_from_rgb(r, g, b)) + } else if let Ok(color) = ArgsColor3::from_lua_multi(args.clone(), lua) { + Ok(Self::from(*color)) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + let brick_color_palette = |_, index: u16| { + if index == 0 { + Err(LuaError::RuntimeError("Invalid index".to_string())) + } else if let Some(number) = BRICK_COLOR_PALETTE.get((index - 1) as usize) { + Ok(color_from_number(*number)) + } else { + Err(LuaError::RuntimeError("Invalid index".to_string())) + } + }; + + let brick_color_random = |_, ()| { + let number = BRICK_COLOR_PALETTE.choose(&mut rand::thread_rng()); + Ok(color_from_number(*number.unwrap())) + }; + + let mut builder = TableBuilder::new(lua)? + .with_function("new", brick_color_new)? + .with_function("palette", brick_color_palette)? + .with_function("random", brick_color_random)?; + for (name, number) in BRICK_COLOR_CONSTRUCTORS { - datatype_table.set( - *name, - lua.create_function(|_, ()| Ok(color_from_number(*number)))?, - )?; + let f = |_, ()| Ok(color_from_number(*number)); + builder = builder.with_function(*name, f)?; } - Ok(()) + + builder.build_readonly() } } diff --git a/src/roblox/datatypes/types/cframe.rs b/src/roblox/datatypes/types/cframe.rs index 3c6a550..80705b6 100644 --- a/src/roblox/datatypes/types/cframe.rs +++ b/src/roblox/datatypes/types/cframe.rs @@ -2,9 +2,11 @@ use core::fmt; use std::ops; use glam::{EulerRot, Mat4, Quat, Vec3}; -use mlua::{prelude::*, Variadic}; +use mlua::prelude::*; use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3}; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, Vector3}; /** @@ -35,79 +37,61 @@ impl CFrame { fn inverse(&self) -> Self { Self(self.0.inverse()) } +} + +impl LuaExportsTable<'_> for CFrame { + const EXPORT_NAME: &'static str = "CFrame"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let cframe_angles = |_, (rx, ry, rz): (f32, f32, f32)| { + Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz))) + }; + + let cframe_from_axis_angle = + |_, (v, r): (LuaUserDataRef, f32)| Ok(CFrame(Mat4::from_axis_angle(v.0, r))); + + let cframe_from_euler_angles_xyz = |_, (rx, ry, rz): (f32, f32, f32)| { + Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz))) + }; + + let cframe_from_euler_angles_yxz = |_, (rx, ry, rz): (f32, f32, f32)| { + Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz))) + }; + + let cframe_from_matrix = |_, + (pos, rx, ry, rz): ( + LuaUserDataRef, + LuaUserDataRef, + LuaUserDataRef, + Option>, + )| { + Ok(CFrame(Mat4::from_cols( + rx.0.extend(0.0), + ry.0.extend(0.0), + rz.map(|r| r.0) + .unwrap_or_else(|| rx.0.cross(ry.0).normalize()) + .extend(0.0), + pos.0.extend(1.0), + ))) + }; + + let cframe_from_orientation = |_, (rx, ry, rz): (f32, f32, f32)| { + Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz))) + }; + + let cframe_look_at = |_, + (from, to, up): ( + LuaUserDataRef, + LuaUserDataRef, + Option>, + )| { + Ok(CFrame(look_at( + from.0, + to.0, + up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0, + ))) + }; - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - // Constants - datatype_table.set("identity", CFrame(Mat4::IDENTITY))?; - // Strict args constructors - datatype_table.set( - "lookAt", - lua.create_function( - |_, - (from, to, up): ( - LuaUserDataRef, - LuaUserDataRef, - Option>, - )| { - Ok(CFrame(look_at( - from.0, - to.0, - up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0, - ))) - }, - )?, - )?; - datatype_table.set( - "fromEulerAnglesXYZ", - lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| { - Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz))) - })?, - )?; - datatype_table.set( - "fromEulerAnglesYXZ", - lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| { - Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz))) - })?, - )?; - datatype_table.set( - "Angles", - lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| { - Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz))) - })?, - )?; - datatype_table.set( - "fromOrientation", - lua.create_function(|_, (rx, ry, rz): (f32, f32, f32)| { - Ok(CFrame(Mat4::from_euler(EulerRot::YXZ, ry, rx, rz))) - })?, - )?; - datatype_table.set( - "fromAxisAngle", - lua.create_function(|_, (v, r): (LuaUserDataRef, f32)| { - Ok(CFrame(Mat4::from_axis_angle(v.0, r))) - })?, - )?; - datatype_table.set( - "fromMatrix", - lua.create_function( - |_, - (pos, rx, ry, rz): ( - LuaUserDataRef, - LuaUserDataRef, - LuaUserDataRef, - Option>, - )| { - Ok(CFrame(Mat4::from_cols( - rx.0.extend(0.0), - ry.0.extend(0.0), - rz.map(|r| r.0) - .unwrap_or_else(|| rx.0.cross(ry.0).normalize()) - .extend(0.0), - pos.0.extend(1.0), - ))) - }, - )?, - )?; // Dynamic args constructor type ArgsPos<'lua> = LuaUserDataRef<'lua, Vector3>; type ArgsLook<'lua> = ( @@ -115,48 +99,59 @@ impl CFrame { LuaUserDataRef<'lua, Vector3>, Option>, ); + type ArgsPosXYZ = (f32, f32, f32); type ArgsPosXYZQuat = (f32, f32, f32, f32, f32, f32, f32); type ArgsMatrix = (f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32, f32); - datatype_table.set( - "new", - lua.create_function(|lua, args: LuaMultiValue| { - if args.clone().into_vec().is_empty() { - Ok(CFrame(Mat4::IDENTITY)) - } else if let Ok(pos) = ArgsPos::from_lua_multi(args.clone(), lua) { - Ok(CFrame(Mat4::from_translation(pos.0))) - } else if let Ok((from, to, up)) = ArgsLook::from_lua_multi(args.clone(), lua) { - Ok(CFrame(look_at( - from.0, - to.0, - up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0, - ))) - } else if let Ok((x, y, z)) = ArgsPosXYZ::from_lua_multi(args.clone(), lua) { - Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z)))) - } else if let Ok((x, y, z, qx, qy, qz, qw)) = - ArgsPosXYZQuat::from_lua_multi(args.clone(), lua) - { - Ok(CFrame(Mat4::from_rotation_translation( - Quat::from_array([qx, qy, qz, qw]), - Vec3::new(x, y, z), - ))) - } else if let Ok((x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22)) = - ArgsMatrix::from_lua_multi(args, lua) - { - Ok(CFrame(Mat4::from_cols_array_2d(&[ - [r00, r01, r02, 0.0], - [r10, r11, r12, 0.0], - [r20, r21, r22, 0.0], - [x, y, z, 1.0], - ]))) - } else { - // FUTURE: Better error message here using given arg types - Err(LuaError::RuntimeError( - "Invalid arguments to constructor".to_string(), - )) - } - })?, - ) + + let cframe_new = |lua, args: LuaMultiValue| { + if args.clone().into_vec().is_empty() { + Ok(CFrame(Mat4::IDENTITY)) + } else if let Ok(pos) = ArgsPos::from_lua_multi(args.clone(), lua) { + Ok(CFrame(Mat4::from_translation(pos.0))) + } else if let Ok((from, to, up)) = ArgsLook::from_lua_multi(args.clone(), lua) { + Ok(CFrame(look_at( + from.0, + to.0, + up.as_deref().unwrap_or(&Vector3(Vec3::Y)).0, + ))) + } else if let Ok((x, y, z)) = ArgsPosXYZ::from_lua_multi(args.clone(), lua) { + Ok(CFrame(Mat4::from_translation(Vec3::new(x, y, z)))) + } else if let Ok((x, y, z, qx, qy, qz, qw)) = + ArgsPosXYZQuat::from_lua_multi(args.clone(), lua) + { + Ok(CFrame(Mat4::from_rotation_translation( + Quat::from_array([qx, qy, qz, qw]), + Vec3::new(x, y, z), + ))) + } else if let Ok((x, y, z, r00, r01, r02, r10, r11, r12, r20, r21, r22)) = + ArgsMatrix::from_lua_multi(args, lua) + { + Ok(CFrame(Mat4::from_cols_array_2d(&[ + [r00, r01, r02, 0.0], + [r10, r11, r12, 0.0], + [r20, r21, r22, 0.0], + [x, y, z, 1.0], + ]))) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("Angles", cframe_angles)? + .with_value("identity", CFrame(Mat4::IDENTITY))? + .with_function("fromAxisAngle", cframe_from_axis_angle)? + .with_function("fromEulerAnglesXYZ", cframe_from_euler_angles_xyz)? + .with_function("fromEulerAnglesYXZ", cframe_from_euler_angles_yxz)? + .with_function("fromMatrix", cframe_from_matrix)? + .with_function("fromOrientation", cframe_from_orientation)? + .with_function("lookAt", cframe_look_at)? + .with_function("new", cframe_new)? + .build_readonly() } } @@ -210,46 +205,38 @@ impl LuaUserData for CFrame { translation, ))) }); - methods.add_method( - "ToWorldSpace", - |_, this, rhs: Variadic>| { - Ok(Variadic::from_iter(rhs.into_iter().map(|cf| *this * *cf))) - }, - ); - methods.add_method( - "ToObjectSpace", - |_, this, rhs: Variadic>| { - let inverse = this.inverse(); - Ok(Variadic::from_iter(rhs.into_iter().map(|cf| inverse * *cf))) - }, - ); + methods.add_method("ToWorldSpace", |_, this, rhs: LuaUserDataRef| { + Ok(*this * *rhs) + }); + methods.add_method("ToObjectSpace", |_, this, rhs: LuaUserDataRef| { + let inverse = this.inverse(); + Ok(inverse * *rhs) + }); methods.add_method( "PointToWorldSpace", - |_, this, rhs: Variadic>| { - Ok(Variadic::from_iter(rhs.into_iter().map(|v3| *this * *v3))) - }, + |_, this, rhs: LuaUserDataRef| Ok(*this * *rhs), ); methods.add_method( "PointToObjectSpace", - |_, this, rhs: Variadic>| { + |_, this, rhs: LuaUserDataRef| { let inverse = this.inverse(); - Ok(Variadic::from_iter(rhs.into_iter().map(|v3| inverse * *v3))) + Ok(inverse * *rhs) }, ); methods.add_method( "VectorToWorldSpace", - |_, this, rhs: Variadic>| { + |_, this, rhs: LuaUserDataRef| { let result = *this - Vector3(this.position()); - Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3))) + Ok(result * *rhs) }, ); methods.add_method( "VectorToObjectSpace", - |_, this, rhs: Variadic>| { + |_, this, rhs: LuaUserDataRef| { let inverse = this.inverse(); let result = inverse - Vector3(inverse.position()); - Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3))) + Ok(result * *rhs) }, ); #[rustfmt::skip] diff --git a/src/roblox/datatypes/types/color3.rs b/src/roblox/datatypes/types/color3.rs index c96e092..8026875 100644 --- a/src/roblox/datatypes/types/color3.rs +++ b/src/roblox/datatypes/types/color3.rs @@ -5,6 +5,8 @@ use glam::Vec3; use mlua::prelude::*; use rbx_dom_weak::types::{Color3 as DomColor3, Color3uint8 as DomColor3uint8}; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::super::*; /** @@ -22,88 +24,87 @@ pub struct Color3 { pub(crate) b: f32, } -impl Color3 { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|_, (r, g, b): (Option, Option, Option)| { - Ok(Color3 { - r: r.unwrap_or_default(), - g: g.unwrap_or_default(), - b: b.unwrap_or_default(), - }) - })?, - )?; - datatype_table.set( - "fromRGB", - lua.create_function(|_, (r, g, b): (Option, Option, Option)| { - Ok(Color3 { - r: (r.unwrap_or_default() as f32) / 255f32, - g: (g.unwrap_or_default() as f32) / 255f32, - b: (b.unwrap_or_default() as f32) / 255f32, - }) - })?, - )?; - datatype_table.set( - "fromHSV", - lua.create_function(|_, (h, s, v): (f32, f32, f32)| { - // https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c - let i = (h * 6.0).floor(); - let f = h * 6.0 - i; - let p = v * (1.0 - s); - let q = v * (1.0 - f * s); - let t = v * (1.0 - (1.0 - f) * s); +impl LuaExportsTable<'_> for Color3 { + const EXPORT_NAME: &'static str = "Color3"; - let (r, g, b) = match (i % 6.0) as u8 { - 0 => (v, t, p), - 1 => (q, v, p), - 2 => (p, v, t), - 3 => (p, q, v), - 4 => (t, p, v), - 5 => (v, p, q), - _ => unreachable!(), - }; + fn create_exports_table(lua: &Lua) -> LuaResult { + let color3_from_rgb = |_, (r, g, b): (Option, Option, Option)| { + Ok(Color3 { + r: (r.unwrap_or_default() as f32) / 255f32, + g: (g.unwrap_or_default() as f32) / 255f32, + b: (b.unwrap_or_default() as f32) / 255f32, + }) + }; - Ok(Color3 { r, g, b }) - })?, - )?; - datatype_table.set( - "fromHex", - lua.create_function(|_, hex: String| { - let trimmed = hex.trim_start_matches('#').to_ascii_uppercase(); - let chars = if trimmed.len() == 3 { - ( - u8::from_str_radix(&trimmed[..1].repeat(2), 16), - u8::from_str_radix(&trimmed[1..2].repeat(2), 16), - u8::from_str_radix(&trimmed[2..3].repeat(2), 16), - ) - } else if trimmed.len() == 6 { - ( - u8::from_str_radix(&trimmed[..2], 16), - u8::from_str_radix(&trimmed[2..4], 16), - u8::from_str_radix(&trimmed[4..6], 16), - ) - } else { - return Err(LuaError::RuntimeError(format!( - "Hex color string must be 3 or 6 characters long, got {} character{}", - trimmed.len(), - if trimmed.len() == 1 { "" } else { "s" } - ))); - }; - match chars { - (Ok(r), Ok(g), Ok(b)) => Ok(Color3 { - r: (r as f32) / 255f32, - g: (g as f32) / 255f32, - b: (b as f32) / 255f32, - }), - _ => Err(LuaError::RuntimeError(format!( - "Hex color string '{}' contains invalid character", - trimmed - ))), - } - })?, - )?; - Ok(()) + let color3_from_hsv = |_, (h, s, v): (f32, f32, f32)| { + // https://axonflux.com/handy-rgb-to-hsl-and-rgb-to-hsv-color-model-c + let i = (h * 6.0).floor(); + let f = h * 6.0 - i; + let p = v * (1.0 - s); + let q = v * (1.0 - f * s); + let t = v * (1.0 - (1.0 - f) * s); + + let (r, g, b) = match (i % 6.0) as u8 { + 0 => (v, t, p), + 1 => (q, v, p), + 2 => (p, v, t), + 3 => (p, q, v), + 4 => (t, p, v), + 5 => (v, p, q), + _ => unreachable!(), + }; + + Ok(Color3 { r, g, b }) + }; + + let color3_from_hex = |_, hex: String| { + let trimmed = hex.trim_start_matches('#').to_ascii_uppercase(); + let chars = if trimmed.len() == 3 { + ( + u8::from_str_radix(&trimmed[..1].repeat(2), 16), + u8::from_str_radix(&trimmed[1..2].repeat(2), 16), + u8::from_str_radix(&trimmed[2..3].repeat(2), 16), + ) + } else if trimmed.len() == 6 { + ( + u8::from_str_radix(&trimmed[..2], 16), + u8::from_str_radix(&trimmed[2..4], 16), + u8::from_str_radix(&trimmed[4..6], 16), + ) + } else { + return Err(LuaError::RuntimeError(format!( + "Hex color string must be 3 or 6 characters long, got {} character{}", + trimmed.len(), + if trimmed.len() == 1 { "" } else { "s" } + ))); + }; + match chars { + (Ok(r), Ok(g), Ok(b)) => Ok(Color3 { + r: (r as f32) / 255f32, + g: (g as f32) / 255f32, + b: (b as f32) / 255f32, + }), + _ => Err(LuaError::RuntimeError(format!( + "Hex color string '{}' contains invalid character", + trimmed + ))), + } + }; + + let color3_new = |_, (r, g, b): (Option, Option, Option)| { + Ok(Color3 { + r: r.unwrap_or_default(), + g: g.unwrap_or_default(), + b: b.unwrap_or_default(), + }) + }; + + TableBuilder::new(lua)? + .with_function("fromRGB", color3_from_rgb)? + .with_function("fromHSV", color3_from_hsv)? + .with_function("fromHex", color3_from_hex)? + .with_function("new", color3_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/color_sequence.rs b/src/roblox/datatypes/types/color_sequence.rs index 154aebc..a8eb8ab 100644 --- a/src/roblox/datatypes/types/color_sequence.rs +++ b/src/roblox/datatypes/types/color_sequence.rs @@ -5,6 +5,8 @@ use rbx_dom_weak::types::{ ColorSequence as DomColorSequence, ColorSequenceKeypoint as DomColorSequenceKeypoint, }; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, Color3, ColorSequenceKeypoint}; /** @@ -17,52 +19,56 @@ pub struct ColorSequence { pub(crate) keypoints: Vec, } -impl ColorSequence { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { +impl LuaExportsTable<'_> for ColorSequence { + const EXPORT_NAME: &'static str = "ColorSequence"; + + fn create_exports_table(lua: &Lua) -> LuaResult { type ArgsColor<'lua> = LuaUserDataRef<'lua, Color3>; type ArgsColors<'lua> = (LuaUserDataRef<'lua, Color3>, LuaUserDataRef<'lua, Color3>); type ArgsKeypoints<'lua> = Vec>; - datatype_table.set( - "new", - lua.create_function(|lua, args: LuaMultiValue| { - if let Ok(color) = ArgsColor::from_lua_multi(args.clone(), lua) { - Ok(ColorSequence { - keypoints: vec![ - ColorSequenceKeypoint { - time: 0.0, - color: *color, - }, - ColorSequenceKeypoint { - time: 1.0, - color: *color, - }, - ], - }) - } else if let Ok((c0, c1)) = ArgsColors::from_lua_multi(args.clone(), lua) { - Ok(ColorSequence { - keypoints: vec![ - ColorSequenceKeypoint { - time: 0.0, - color: *c0, - }, - ColorSequenceKeypoint { - time: 1.0, - color: *c1, - }, - ], - }) - } else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) { - Ok(ColorSequence { - keypoints: keypoints.iter().map(|k| **k).collect(), - }) - } else { - // FUTURE: Better error message here using given arg types - Err(LuaError::RuntimeError( - "Invalid arguments to constructor".to_string(), - )) - } - })?, - ) + + let color_sequence_new = |lua, args: LuaMultiValue| { + if let Ok(color) = ArgsColor::from_lua_multi(args.clone(), lua) { + Ok(ColorSequence { + keypoints: vec![ + ColorSequenceKeypoint { + time: 0.0, + color: *color, + }, + ColorSequenceKeypoint { + time: 1.0, + color: *color, + }, + ], + }) + } else if let Ok((c0, c1)) = ArgsColors::from_lua_multi(args.clone(), lua) { + Ok(ColorSequence { + keypoints: vec![ + ColorSequenceKeypoint { + time: 0.0, + color: *c0, + }, + ColorSequenceKeypoint { + time: 1.0, + color: *c1, + }, + ], + }) + } else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) { + Ok(ColorSequence { + keypoints: keypoints.iter().map(|k| **k).collect(), + }) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("new", color_sequence_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/color_sequence_keypoint.rs b/src/roblox/datatypes/types/color_sequence_keypoint.rs index 201bde7..a90b6e7 100644 --- a/src/roblox/datatypes/types/color_sequence_keypoint.rs +++ b/src/roblox/datatypes/types/color_sequence_keypoint.rs @@ -3,6 +3,8 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::ColorSequenceKeypoint as DomColorSequenceKeypoint; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, Color3}; /** @@ -16,18 +18,20 @@ pub struct ColorSequenceKeypoint { pub(crate) color: Color3, } -impl ColorSequenceKeypoint { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|_, (time, color): (f32, LuaUserDataRef)| { - Ok(ColorSequenceKeypoint { - time, - color: *color, - }) - })?, - )?; - Ok(()) +impl LuaExportsTable<'_> for ColorSequenceKeypoint { + const EXPORT_NAME: &'static str = "ColorSequenceKeypoint"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let color_sequence_keypoint_new = |_, (time, color): (f32, LuaUserDataRef)| { + Ok(ColorSequenceKeypoint { + time, + color: *color, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", color_sequence_keypoint_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/faces.rs b/src/roblox/datatypes/types/faces.rs index f21e248..b23ff72 100644 --- a/src/roblox/datatypes/types/faces.rs +++ b/src/roblox/datatypes/types/faces.rs @@ -3,6 +3,8 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::Faces as DomFaces; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, EnumItem}; /** @@ -20,59 +22,64 @@ pub struct Faces { pub(crate) front: bool, } -impl Faces { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|_, args: LuaMultiValue| { - let mut right = false; - let mut top = false; - let mut back = false; - let mut left = false; - let mut bottom = false; - let mut front = false; - let mut check = |e: &EnumItem| { - if e.parent.desc.name == "NormalId" { - match &e.name { - name if name == "Right" => right = true, - name if name == "Top" => top = true, - name if name == "Back" => back = true, - name if name == "Left" => left = true, - name if name == "Bottom" => bottom = true, - name if name == "Front" => front = true, - _ => {} - } - } - }; - for (index, arg) in args.into_iter().enumerate() { - if let LuaValue::UserData(u) = arg { - if let Ok(e) = u.borrow::() { - check(&e); - } else { - return Err(LuaError::RuntimeError(format!( - "Expected argument #{} to be an EnumItem, got userdata", - index - ))); - } - } else { - return Err(LuaError::RuntimeError(format!( - "Expected argument #{} to be an EnumItem, got {}", - index, - arg.type_name() - ))); +impl LuaExportsTable<'_> for Faces { + const EXPORT_NAME: &'static str = "Faces"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let faces_new = |_, args: LuaMultiValue| { + let mut right = false; + let mut top = false; + let mut back = false; + let mut left = false; + let mut bottom = false; + let mut front = false; + + let mut check = |e: &EnumItem| { + if e.parent.desc.name == "NormalId" { + match &e.name { + name if name == "Right" => right = true, + name if name == "Top" => top = true, + name if name == "Back" => back = true, + name if name == "Left" => left = true, + name if name == "Bottom" => bottom = true, + name if name == "Front" => front = true, + _ => {} } } - Ok(Faces { - right, - top, - back, - left, - bottom, - front, - }) - })?, - )?; - Ok(()) + }; + + for (index, arg) in args.into_iter().enumerate() { + if let LuaValue::UserData(u) = arg { + if let Ok(e) = u.borrow::() { + check(&e); + } else { + return Err(LuaError::RuntimeError(format!( + "Expected argument #{} to be an EnumItem, got userdata", + index + ))); + } + } else { + return Err(LuaError::RuntimeError(format!( + "Expected argument #{} to be an EnumItem, got {}", + index, + arg.type_name() + ))); + } + } + + Ok(Faces { + right, + top, + back, + left, + bottom, + front, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", faces_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/font.rs b/src/roblox/datatypes/types/font.rs index 8c5a19d..d3235d8 100644 --- a/src/roblox/datatypes/types/font.rs +++ b/src/roblox/datatypes/types/font.rs @@ -6,6 +6,8 @@ use rbx_dom_weak::types::{ Font as DomFont, FontStyle as DomFontStyle, FontWeight as DomFontWeight, }; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, EnumItem}; /** @@ -34,66 +36,65 @@ impl Font { cached_id: None, }) } +} - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function( - |_, (family, weight, style): (String, Option, Option)| { - Ok(Font { - family, - weight: weight.unwrap_or_default(), - style: style.unwrap_or_default(), - cached_id: None, - }) - }, - )?, - )?; - datatype_table.set( - "fromEnum", - lua.create_function(|_, value: LuaUserDataRef| { - if value.parent.desc.name == "Font" { - match Font::from_enum_item(&value) { - Some(props) => Ok(props), - None => Err(LuaError::RuntimeError(format!( - "Found unknown Font '{}'", - value.name - ))), - } - } else { - Err(LuaError::RuntimeError(format!( - "Expected argument #1 to be a Font, got {}", - value.parent.desc.name - ))) +impl LuaExportsTable<'_> for Font { + const EXPORT_NAME: &'static str = "Font"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let font_from_enum = |_, value: LuaUserDataRef| { + if value.parent.desc.name == "Font" { + match Font::from_enum_item(&value) { + Some(props) => Ok(props), + None => Err(LuaError::RuntimeError(format!( + "Found unknown Font '{}'", + value.name + ))), } - })?, - )?; - datatype_table.set( - "fromName", - lua.create_function( - |_, (file, weight, style): (String, Option, Option)| { - Ok(Font { - family: format!("rbxasset://fonts/families/{}.json", file), - weight: weight.unwrap_or_default(), - style: style.unwrap_or_default(), - cached_id: None, - }) - }, - )?, - )?; - datatype_table.set( - "fromId", - lua.create_function( - |_, (id, weight, style): (i32, Option, Option)| { - Ok(Font { - family: format!("rbxassetid://{}", id), - weight: weight.unwrap_or_default(), - style: style.unwrap_or_default(), - cached_id: None, - }) - }, - )?, - ) + } else { + Err(LuaError::RuntimeError(format!( + "Expected argument #1 to be a Font, got {}", + value.parent.desc.name + ))) + } + }; + + let font_from_name = + |_, (file, weight, style): (String, Option, Option)| { + Ok(Font { + family: format!("rbxasset://fonts/families/{}.json", file), + weight: weight.unwrap_or_default(), + style: style.unwrap_or_default(), + cached_id: None, + }) + }; + + let font_from_id = + |_, (id, weight, style): (i32, Option, Option)| { + Ok(Font { + family: format!("rbxassetid://{}", id), + weight: weight.unwrap_or_default(), + style: style.unwrap_or_default(), + cached_id: None, + }) + }; + + let font_new = + |_, (family, weight, style): (String, Option, Option)| { + Ok(Font { + family, + weight: weight.unwrap_or_default(), + style: style.unwrap_or_default(), + cached_id: None, + }) + }; + + TableBuilder::new(lua)? + .with_function("fromEnum", font_from_enum)? + .with_function("fromName", font_from_name)? + .with_function("fromId", font_from_id)? + .with_function("new", font_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/number_range.rs b/src/roblox/datatypes/types/number_range.rs index 065097d..b3b5ac9 100644 --- a/src/roblox/datatypes/types/number_range.rs +++ b/src/roblox/datatypes/types/number_range.rs @@ -3,6 +3,8 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::NumberRange as DomNumberRange; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::super::*; /** @@ -16,20 +18,23 @@ pub struct NumberRange { pub(crate) max: f32, } -impl NumberRange { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|_, (min, max): (f32, Option)| { - Ok(match max { - Some(max) => NumberRange { - min: min.min(max), - max: min.max(max), - }, - None => NumberRange { min, max: min }, - }) - })?, - ) +impl LuaExportsTable<'_> for NumberRange { + const EXPORT_NAME: &'static str = "NumberRange"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let number_range_new = |_, (min, max): (f32, Option)| { + Ok(match max { + Some(max) => NumberRange { + min: min.min(max), + max: min.max(max), + }, + None => NumberRange { min, max: min }, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", number_range_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/number_sequence.rs b/src/roblox/datatypes/types/number_sequence.rs index 740a2da..f6b80bb 100644 --- a/src/roblox/datatypes/types/number_sequence.rs +++ b/src/roblox/datatypes/types/number_sequence.rs @@ -5,6 +5,8 @@ use rbx_dom_weak::types::{ NumberSequence as DomNumberSequence, NumberSequenceKeypoint as DomNumberSequenceKeypoint, }; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, NumberSequenceKeypoint}; /** @@ -17,56 +19,60 @@ pub struct NumberSequence { pub(crate) keypoints: Vec, } -impl NumberSequence { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { +impl LuaExportsTable<'_> for NumberSequence { + const EXPORT_NAME: &'static str = "NumberSequence"; + + fn create_exports_table(lua: &Lua) -> LuaResult { type ArgsColor = f32; type ArgsColors = (f32, f32); type ArgsKeypoints<'lua> = Vec>; - datatype_table.set( - "new", - lua.create_function(|lua, args: LuaMultiValue| { - if let Ok(value) = ArgsColor::from_lua_multi(args.clone(), lua) { - Ok(NumberSequence { - keypoints: vec![ - NumberSequenceKeypoint { - time: 0.0, - value, - envelope: 0.0, - }, - NumberSequenceKeypoint { - time: 1.0, - value, - envelope: 0.0, - }, - ], - }) - } else if let Ok((v0, v1)) = ArgsColors::from_lua_multi(args.clone(), lua) { - Ok(NumberSequence { - keypoints: vec![ - NumberSequenceKeypoint { - time: 0.0, - value: v0, - envelope: 0.0, - }, - NumberSequenceKeypoint { - time: 1.0, - value: v1, - envelope: 0.0, - }, - ], - }) - } else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) { - Ok(NumberSequence { - keypoints: keypoints.iter().map(|k| **k).collect(), - }) - } else { - // FUTURE: Better error message here using given arg types - Err(LuaError::RuntimeError( - "Invalid arguments to constructor".to_string(), - )) - } - })?, - ) + + let number_sequence_new = |lua, args: LuaMultiValue| { + if let Ok(value) = ArgsColor::from_lua_multi(args.clone(), lua) { + Ok(NumberSequence { + keypoints: vec![ + NumberSequenceKeypoint { + time: 0.0, + value, + envelope: 0.0, + }, + NumberSequenceKeypoint { + time: 1.0, + value, + envelope: 0.0, + }, + ], + }) + } else if let Ok((v0, v1)) = ArgsColors::from_lua_multi(args.clone(), lua) { + Ok(NumberSequence { + keypoints: vec![ + NumberSequenceKeypoint { + time: 0.0, + value: v0, + envelope: 0.0, + }, + NumberSequenceKeypoint { + time: 1.0, + value: v1, + envelope: 0.0, + }, + ], + }) + } else if let Ok(keypoints) = ArgsKeypoints::from_lua_multi(args, lua) { + Ok(NumberSequence { + keypoints: keypoints.iter().map(|k| **k).collect(), + }) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("new", number_sequence_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/number_sequence_keypoint.rs b/src/roblox/datatypes/types/number_sequence_keypoint.rs index f57d25b..be9aa1c 100644 --- a/src/roblox/datatypes/types/number_sequence_keypoint.rs +++ b/src/roblox/datatypes/types/number_sequence_keypoint.rs @@ -3,6 +3,8 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::NumberSequenceKeypoint as DomNumberSequenceKeypoint; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::super::*; /** @@ -17,19 +19,21 @@ pub struct NumberSequenceKeypoint { pub(crate) envelope: f32, } -impl NumberSequenceKeypoint { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|_, (time, value, envelope): (f32, f32, Option)| { - Ok(NumberSequenceKeypoint { - time, - value, - envelope: envelope.unwrap_or_default(), - }) - })?, - )?; - Ok(()) +impl LuaExportsTable<'_> for NumberSequenceKeypoint { + const EXPORT_NAME: &'static str = "NumberSequenceKeypoint"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let number_sequence_keypoint_new = |_, (time, value, envelope): (f32, f32, Option)| { + Ok(NumberSequenceKeypoint { + time, + value, + envelope: envelope.unwrap_or_default(), + }) + }; + + TableBuilder::new(lua)? + .with_function("new", number_sequence_keypoint_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/physical_properties.rs b/src/roblox/datatypes/types/physical_properties.rs index 482a750..198c9ce 100644 --- a/src/roblox/datatypes/types/physical_properties.rs +++ b/src/roblox/datatypes/types/physical_properties.rs @@ -3,6 +3,8 @@ use core::fmt; use mlua::prelude::*; use rbx_dom_weak::types::CustomPhysicalProperties as DomCustomPhysicalProperties; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, EnumItem}; /** @@ -32,51 +34,52 @@ impl PhysicalProperties { elasticity_weight: props.5, }) } +} - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { +impl LuaExportsTable<'_> for PhysicalProperties { + const EXPORT_NAME: &'static str = "PhysicalProperties"; + + fn create_exports_table(lua: &Lua) -> LuaResult { type ArgsMaterial<'lua> = LuaUserDataRef<'lua, EnumItem>; type ArgsNumbers = (f32, f32, f32, Option, Option); - datatype_table.set( - "new", - lua.create_function(|lua, args: LuaMultiValue| { - if let Ok(value) = ArgsMaterial::from_lua_multi(args.clone(), lua) { - if value.parent.desc.name == "Material" { - match PhysicalProperties::from_material(&value) { - Some(props) => Ok(props), - None => Err(LuaError::RuntimeError(format!( - "Found unknown Material '{}'", - value.name - ))), - } - } else { - Err(LuaError::RuntimeError(format!( - "Expected argument #1 to be a Material, got {}", - value.parent.desc.name - ))) + + let physical_properties_new = |lua, args: LuaMultiValue| { + if let Ok(value) = ArgsMaterial::from_lua_multi(args.clone(), lua) { + if value.parent.desc.name == "Material" { + match PhysicalProperties::from_material(&value) { + Some(props) => Ok(props), + None => Err(LuaError::RuntimeError(format!( + "Found unknown Material '{}'", + value.name + ))), } - } else if let Ok(( + } else { + Err(LuaError::RuntimeError(format!( + "Expected argument #1 to be a Material, got {}", + value.parent.desc.name + ))) + } + } else if let Ok((density, friction, elasticity, friction_weight, elasticity_weight)) = + ArgsNumbers::from_lua_multi(args, lua) + { + Ok(PhysicalProperties { density, friction, + friction_weight: friction_weight.unwrap_or(1.0), elasticity, - friction_weight, - elasticity_weight, - )) = ArgsNumbers::from_lua_multi(args, lua) - { - Ok(PhysicalProperties { - density, - friction, - friction_weight: friction_weight.unwrap_or(1.0), - elasticity, - elasticity_weight: elasticity_weight.unwrap_or(1.0), - }) - } else { - // FUTURE: Better error message here using given arg types - Err(LuaError::RuntimeError( - "Invalid arguments to constructor".to_string(), - )) - } - })?, - ) + elasticity_weight: elasticity_weight.unwrap_or(1.0), + }) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("new", physical_properties_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/ray.rs b/src/roblox/datatypes/types/ray.rs index c2958d1..68aba6e 100644 --- a/src/roblox/datatypes/types/ray.rs +++ b/src/roblox/datatypes/types/ray.rs @@ -4,6 +4,8 @@ use glam::Vec3; use mlua::prelude::*; use rbx_dom_weak::types::Ray as DomRay; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, Vector3}; /** @@ -26,19 +28,23 @@ impl Ray { let dot_product = lhs.dot(norm).max(0.0); self.origin + norm * dot_product } +} - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function( - |_, (origin, direction): (LuaUserDataRef, LuaUserDataRef)| { - Ok(Ray { - origin: origin.0, - direction: direction.0, - }) - }, - )?, - ) +impl LuaExportsTable<'_> for Ray { + const EXPORT_NAME: &'static str = "Ray"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let ray_new = + |_, (origin, direction): (LuaUserDataRef, LuaUserDataRef)| { + Ok(Ray { + origin: origin.0, + direction: direction.0, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", ray_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/rect.rs b/src/roblox/datatypes/types/rect.rs index 315def7..b305184 100644 --- a/src/roblox/datatypes/types/rect.rs +++ b/src/roblox/datatypes/types/rect.rs @@ -5,6 +5,8 @@ use glam::Vec2; use mlua::prelude::*; use rbx_dom_weak::types::Rect as DomRect; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, Vector2}; /** @@ -28,33 +30,37 @@ impl Rect { } } -impl Rect { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { +impl LuaExportsTable<'_> for Rect { + const EXPORT_NAME: &'static str = "Rect"; + + fn create_exports_table(lua: &Lua) -> LuaResult { type ArgsVector2s<'lua> = ( Option>, Option>, ); type ArgsNums = (Option, Option, Option, Option); - datatype_table.set( - "new", - lua.create_function(|lua, args: LuaMultiValue| { - if let Ok((min, max)) = ArgsVector2s::from_lua_multi(args.clone(), lua) { - Ok(Rect::new( - min.map(|m| *m).unwrap_or_default().0, - max.map(|m| *m).unwrap_or_default().0, - )) - } else if let Ok((x0, y0, x1, y1)) = ArgsNums::from_lua_multi(args, lua) { - let min = Vec2::new(x0.unwrap_or_default(), y0.unwrap_or_default()); - let max = Vec2::new(x1.unwrap_or_default(), y1.unwrap_or_default()); - Ok(Rect::new(min, max)) - } else { - // FUTURE: Better error message here using given arg types - Err(LuaError::RuntimeError( - "Invalid arguments to constructor".to_string(), - )) - } - })?, - ) + + let rect_new = |lua, args: LuaMultiValue| { + if let Ok((min, max)) = ArgsVector2s::from_lua_multi(args.clone(), lua) { + Ok(Rect::new( + min.map(|m| *m).unwrap_or_default().0, + max.map(|m| *m).unwrap_or_default().0, + )) + } else if let Ok((x0, y0, x1, y1)) = ArgsNums::from_lua_multi(args, lua) { + let min = Vec2::new(x0.unwrap_or_default(), y0.unwrap_or_default()); + let max = Vec2::new(x1.unwrap_or_default(), y1.unwrap_or_default()); + Ok(Rect::new(min, max)) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("new", rect_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/region3.rs b/src/roblox/datatypes/types/region3.rs index 16e97ce..9c9bdf0 100644 --- a/src/roblox/datatypes/types/region3.rs +++ b/src/roblox/datatypes/types/region3.rs @@ -4,6 +4,8 @@ use glam::{Mat4, Vec3}; use mlua::prelude::*; use rbx_dom_weak::types::Region3 as DomRegion3; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, CFrame, Vector3}; /** @@ -18,19 +20,20 @@ pub struct Region3 { pub(crate) max: Vec3, } -impl Region3 { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function( - |_, (min, max): (LuaUserDataRef, LuaUserDataRef)| { - Ok(Region3 { - min: min.0, - max: max.0, - }) - }, - )?, - ) +impl LuaExportsTable<'_> for Region3 { + const EXPORT_NAME: &'static str = "Region3"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let region3_new = |_, (min, max): (LuaUserDataRef, LuaUserDataRef)| { + Ok(Region3 { + min: min.0, + max: max.0, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", region3_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/region3int16.rs b/src/roblox/datatypes/types/region3int16.rs index 887aced..d64ffde 100644 --- a/src/roblox/datatypes/types/region3int16.rs +++ b/src/roblox/datatypes/types/region3int16.rs @@ -4,6 +4,8 @@ use glam::IVec3; use mlua::prelude::*; use rbx_dom_weak::types::Region3int16 as DomRegion3int16; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, Vector3int16}; /** @@ -18,19 +20,21 @@ pub struct Region3int16 { pub(crate) max: IVec3, } -impl Region3int16 { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function( - |_, (min, max): (LuaUserDataRef, LuaUserDataRef)| { - Ok(Region3int16 { - min: min.0, - max: max.0, - }) - }, - )?, - ) +impl LuaExportsTable<'_> for Region3int16 { + const EXPORT_NAME: &'static str = "Region3int16"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let region3int16_new = + |_, (min, max): (LuaUserDataRef, LuaUserDataRef)| { + Ok(Region3int16 { + min: min.0, + max: max.0, + }) + }; + + TableBuilder::new(lua)? + .with_function("new", region3int16_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/udim.rs b/src/roblox/datatypes/types/udim.rs index 9fc4bc6..b390c0c 100644 --- a/src/roblox/datatypes/types/udim.rs +++ b/src/roblox/datatypes/types/udim.rs @@ -4,6 +4,8 @@ use std::ops; use mlua::prelude::*; use rbx_dom_weak::types::UDim as DomUDim; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::super::*; /** @@ -21,17 +23,22 @@ impl UDim { pub(super) fn new(scale: f32, offset: i32) -> Self { Self { scale, offset } } +} - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|_, (scale, offset): (Option, Option)| { - Ok(UDim { - scale: scale.unwrap_or_default(), - offset: offset.unwrap_or_default(), - }) - })?, - ) +impl LuaExportsTable<'_> for UDim { + const EXPORT_NAME: &'static str = "UDim"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let udim_new = |_, (scale, offset): (Option, Option)| { + Ok(UDim { + scale: scale.unwrap_or_default(), + offset: offset.unwrap_or_default(), + }) + }; + + TableBuilder::new(lua)? + .with_function("new", udim_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/udim2.rs b/src/roblox/datatypes/types/udim2.rs index 597b980..4a41f98 100644 --- a/src/roblox/datatypes/types/udim2.rs +++ b/src/roblox/datatypes/types/udim2.rs @@ -5,6 +5,8 @@ use glam::Vec2; use mlua::prelude::*; use rbx_dom_weak::types::UDim2 as DomUDim2; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, UDim}; /** @@ -18,52 +20,53 @@ pub struct UDim2 { pub(crate) y: UDim, } -impl UDim2 { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "fromScale", - lua.create_function(|_, (x, y): (Option, Option)| { - Ok(UDim2 { - x: UDim::new(x.unwrap_or_default(), 0), - y: UDim::new(y.unwrap_or_default(), 0), - }) - })?, - )?; - datatype_table.set( - "fromOffset", - lua.create_function(|_, (x, y): (Option, Option)| { - Ok(UDim2 { - x: UDim::new(0f32, x.unwrap_or_default()), - y: UDim::new(0f32, y.unwrap_or_default()), - }) - })?, - )?; +impl LuaExportsTable<'_> for UDim2 { + const EXPORT_NAME: &'static str = "UDim2"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let udim2_from_offset = |_, (x, y): (Option, Option)| { + Ok(UDim2 { + x: UDim::new(0f32, x.unwrap_or_default()), + y: UDim::new(0f32, y.unwrap_or_default()), + }) + }; + + let udim2_from_scale = |_, (x, y): (Option, Option)| { + Ok(UDim2 { + x: UDim::new(x.unwrap_or_default(), 0), + y: UDim::new(y.unwrap_or_default(), 0), + }) + }; + type ArgsUDims<'lua> = ( Option>, Option>, ); type ArgsNums = (Option, Option, Option, Option); - datatype_table.set( - "new", - lua.create_function(|lua, args: LuaMultiValue| { - if let Ok((x, y)) = ArgsUDims::from_lua_multi(args.clone(), lua) { - Ok(UDim2 { - x: x.map(|x| *x).unwrap_or_default(), - y: y.map(|y| *y).unwrap_or_default(), - }) - } else if let Ok((sx, ox, sy, oy)) = ArgsNums::from_lua_multi(args, lua) { - Ok(UDim2 { - x: UDim::new(sx.unwrap_or_default(), ox.unwrap_or_default()), - y: UDim::new(sy.unwrap_or_default(), oy.unwrap_or_default()), - }) - } else { - // FUTURE: Better error message here using given arg types - Err(LuaError::RuntimeError( - "Invalid arguments to constructor".to_string(), - )) - } - })?, - ) + let udim2_new = |lua, args: LuaMultiValue| { + if let Ok((x, y)) = ArgsUDims::from_lua_multi(args.clone(), lua) { + Ok(UDim2 { + x: x.map(|x| *x).unwrap_or_default(), + y: y.map(|y| *y).unwrap_or_default(), + }) + } else if let Ok((sx, ox, sy, oy)) = ArgsNums::from_lua_multi(args, lua) { + Ok(UDim2 { + x: UDim::new(sx.unwrap_or_default(), ox.unwrap_or_default()), + y: UDim::new(sy.unwrap_or_default(), oy.unwrap_or_default()), + }) + } else { + // FUTURE: Better error message here using given arg types + Err(LuaError::RuntimeError( + "Invalid arguments to constructor".to_string(), + )) + } + }; + + TableBuilder::new(lua)? + .with_function("fromOffset", udim2_from_offset)? + .with_function("fromScale", udim2_from_scale)? + .with_function("new", udim2_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/vector2.rs b/src/roblox/datatypes/types/vector2.rs index 78a0a54..0eaad3d 100644 --- a/src/roblox/datatypes/types/vector2.rs +++ b/src/roblox/datatypes/types/vector2.rs @@ -5,6 +5,8 @@ use glam::{Vec2, Vec3}; use mlua::prelude::*; use rbx_dom_weak::types::Vector2 as DomVector2; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::super::*; /** @@ -17,23 +19,24 @@ use super::super::*; #[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Vector2(pub Vec2); -impl Vector2 { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - // Constants - datatype_table.set("xAxis", Vector2(Vec2::X))?; - datatype_table.set("yAxis", Vector2(Vec2::Y))?; - datatype_table.set("zero", Vector2(Vec2::ZERO))?; - datatype_table.set("one", Vector2(Vec2::ONE))?; - // Constructors - datatype_table.set( - "new", - lua.create_function(|_, (x, y): (Option, Option)| { - Ok(Vector2(Vec2 { - x: x.unwrap_or_default(), - y: y.unwrap_or_default(), - })) - })?, - ) +impl LuaExportsTable<'_> for Vector2 { + const EXPORT_NAME: &'static str = "Vector2"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let vector2_new = |_, (x, y): (Option, Option)| { + Ok(Vector2(Vec2 { + x: x.unwrap_or_default(), + y: y.unwrap_or_default(), + })) + }; + + TableBuilder::new(lua)? + .with_value("xAxis", Vector2(Vec2::X))? + .with_value("yAxis", Vector2(Vec2::Y))? + .with_value("zero", Vector2(Vec2::ZERO))? + .with_value("one", Vector2(Vec2::ONE))? + .with_function("new", vector2_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/vector2int16.rs b/src/roblox/datatypes/types/vector2int16.rs index 2ae21c8..1193428 100644 --- a/src/roblox/datatypes/types/vector2int16.rs +++ b/src/roblox/datatypes/types/vector2int16.rs @@ -5,6 +5,8 @@ use glam::IVec2; use mlua::prelude::*; use rbx_dom_weak::types::Vector2int16 as DomVector2int16; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::super::*; /** @@ -17,17 +19,20 @@ use super::super::*; #[derive(Debug, Clone, Copy, PartialEq)] pub struct Vector2int16(pub IVec2); -impl Vector2int16 { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|_, (x, y): (Option, Option)| { - Ok(Vector2int16(IVec2 { - x: x.unwrap_or_default() as i32, - y: y.unwrap_or_default() as i32, - })) - })?, - ) +impl LuaExportsTable<'_> for Vector2int16 { + const EXPORT_NAME: &'static str = "Vector2int16"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let vector2int16_new = |_, (x, y): (Option, Option)| { + Ok(Vector2int16(IVec2 { + x: x.unwrap_or_default() as i32, + y: y.unwrap_or_default() as i32, + })) + }; + + TableBuilder::new(lua)? + .with_function("new", vector2int16_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/vector3.rs b/src/roblox/datatypes/types/vector3.rs index f275a41..a7ba383 100644 --- a/src/roblox/datatypes/types/vector3.rs +++ b/src/roblox/datatypes/types/vector3.rs @@ -5,6 +5,8 @@ use glam::Vec3; use mlua::prelude::*; use rbx_dom_weak::types::Vector3 as DomVector3; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::{super::*, EnumItem}; /** @@ -20,74 +22,73 @@ use super::{super::*, EnumItem}; #[derive(Debug, Clone, Copy, PartialEq)] pub struct Vector3(pub Vec3); -impl Vector3 { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - // Constants - datatype_table.set("xAxis", Vector3(Vec3::X))?; - datatype_table.set("yAxis", Vector3(Vec3::Y))?; - datatype_table.set("zAxis", Vector3(Vec3::Z))?; - datatype_table.set("zero", Vector3(Vec3::ZERO))?; - datatype_table.set("one", Vector3(Vec3::ONE))?; - // Constructors - datatype_table.set( - "fromAxis", - lua.create_function(|_, normal_id: LuaUserDataRef| { - if normal_id.parent.desc.name == "Axis" { - Ok(match normal_id.name.as_str() { - "X" => Vector3(Vec3::X), - "Y" => Vector3(Vec3::Y), - "Z" => Vector3(Vec3::Z), - name => { - return Err(LuaError::RuntimeError(format!( - "Axis '{}' is not known", - name - ))) - } - }) - } else { - Err(LuaError::RuntimeError(format!( - "EnumItem must be a Axis, got {}", - normal_id.parent.desc.name - ))) - } - })?, - )?; - datatype_table.set( - "fromNormalId", - lua.create_function(|_, normal_id: LuaUserDataRef| { - if normal_id.parent.desc.name == "NormalId" { - Ok(match normal_id.name.as_str() { - "Left" => Vector3(Vec3::X), - "Top" => Vector3(Vec3::Y), - "Front" => Vector3(-Vec3::Z), - "Right" => Vector3(-Vec3::X), - "Bottom" => Vector3(-Vec3::Y), - "Back" => Vector3(Vec3::Z), - name => { - return Err(LuaError::RuntimeError(format!( - "NormalId '{}' is not known", - name - ))) - } - }) - } else { - Err(LuaError::RuntimeError(format!( - "EnumItem must be a NormalId, got {}", - normal_id.parent.desc.name - ))) - } - })?, - )?; - datatype_table.set( - "new", - lua.create_function(|_, (x, y, z): (Option, Option, Option)| { - Ok(Vector3(Vec3 { - x: x.unwrap_or_default(), - y: y.unwrap_or_default(), - z: z.unwrap_or_default(), - })) - })?, - ) +impl LuaExportsTable<'_> for Vector3 { + const EXPORT_NAME: &'static str = "Vector3"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let vector3_from_axis = |_, normal_id: LuaUserDataRef| { + if normal_id.parent.desc.name == "Axis" { + Ok(match normal_id.name.as_str() { + "X" => Vector3(Vec3::X), + "Y" => Vector3(Vec3::Y), + "Z" => Vector3(Vec3::Z), + name => { + return Err(LuaError::RuntimeError(format!( + "Axis '{}' is not known", + name + ))) + } + }) + } else { + Err(LuaError::RuntimeError(format!( + "EnumItem must be a Axis, got {}", + normal_id.parent.desc.name + ))) + } + }; + + let vector3_from_normal_id = |_, normal_id: LuaUserDataRef| { + if normal_id.parent.desc.name == "NormalId" { + Ok(match normal_id.name.as_str() { + "Left" => Vector3(Vec3::X), + "Top" => Vector3(Vec3::Y), + "Front" => Vector3(-Vec3::Z), + "Right" => Vector3(-Vec3::X), + "Bottom" => Vector3(-Vec3::Y), + "Back" => Vector3(Vec3::Z), + name => { + return Err(LuaError::RuntimeError(format!( + "NormalId '{}' is not known", + name + ))) + } + }) + } else { + Err(LuaError::RuntimeError(format!( + "EnumItem must be a NormalId, got {}", + normal_id.parent.desc.name + ))) + } + }; + + let vector3_new = |_, (x, y, z): (Option, Option, Option)| { + Ok(Vector3(Vec3 { + x: x.unwrap_or_default(), + y: y.unwrap_or_default(), + z: z.unwrap_or_default(), + })) + }; + + TableBuilder::new(lua)? + .with_value("xAxis", Vector3(Vec3::X))? + .with_value("yAxis", Vector3(Vec3::Y))? + .with_value("zAxis", Vector3(Vec3::Z))? + .with_value("zero", Vector3(Vec3::ZERO))? + .with_value("one", Vector3(Vec3::ONE))? + .with_function("fromAxis", vector3_from_axis)? + .with_function("fromNormalId", vector3_from_normal_id)? + .with_function("new", vector3_new)? + .build_readonly() } } diff --git a/src/roblox/datatypes/types/vector3int16.rs b/src/roblox/datatypes/types/vector3int16.rs index e8d6f08..d62e8ff 100644 --- a/src/roblox/datatypes/types/vector3int16.rs +++ b/src/roblox/datatypes/types/vector3int16.rs @@ -5,6 +5,8 @@ use glam::IVec3; use mlua::prelude::*; use rbx_dom_weak::types::Vector3int16 as DomVector3int16; +use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable}; + use super::super::*; /** @@ -17,18 +19,21 @@ use super::super::*; #[derive(Debug, Clone, Copy, PartialEq)] pub struct Vector3int16(pub IVec3); -impl Vector3int16 { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|_, (x, y, z): (Option, Option, Option)| { - Ok(Vector3int16(IVec3 { - x: x.unwrap_or_default() as i32, - y: y.unwrap_or_default() as i32, - z: z.unwrap_or_default() as i32, - })) - })?, - ) +impl LuaExportsTable<'_> for Vector3int16 { + const EXPORT_NAME: &'static str = "Vector3int16"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let vector3int16_new = |_, (x, y, z): (Option, Option, Option)| { + Ok(Vector3int16(IVec3 { + x: x.unwrap_or_default() as i32, + y: y.unwrap_or_default() as i32, + z: z.unwrap_or_default() as i32, + })) + }; + + TableBuilder::new(lua)? + .with_function("new", vector3int16_new)? + .build_readonly() } } diff --git a/src/roblox/exports.rs b/src/roblox/exports.rs new file mode 100644 index 0000000..2a8f834 --- /dev/null +++ b/src/roblox/exports.rs @@ -0,0 +1,68 @@ +use mlua::prelude::*; + +/** + Trait for any item that should be exported as part of the `roblox` built-in library. + + This may be an enum or a struct that should export constants and/or constructs. + + ### Example usage + + ```rs + use mlua::prelude::*; + + struct MyType(usize); + + impl MyType { + pub fn new(n: usize) -> Self { + Self(n) + } + } + + impl LuaExportsTable<'_> for MyType { + const EXPORT_NAME: &'static str = "MyType"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let my_type_new = |lua, n: Option| { + Self::new(n.unwrap_or_default()) + }; + + TableBuilder::new(lua)? + .with_function("new", my_type_new)? + .build_readonly() + } + } + + impl LuaUserData for MyType { + // ... + } + ``` +*/ +pub trait LuaExportsTable<'lua> { + const EXPORT_NAME: &'static str; + + fn create_exports_table(lua: &'lua Lua) -> LuaResult>; +} + +/** + Exports a single item that implements the [`LuaExportsTable`] trait. + + Returns the name of the export, as well as the export table. + + ### Example usage + + ```rs + let lua: mlua::Lua::new(); + + let (name1, table1) = export::(lua)?; + let (name2, table2) = export::(lua)?; + ``` +*/ +pub fn export<'lua, T>(lua: &'lua Lua) -> LuaResult<(&'static str, LuaValue<'lua>)> +where + T: LuaExportsTable<'lua>, +{ + Ok(( + T::EXPORT_NAME, + ::create_exports_table(lua)?.into_lua(lua)?, + )) +} diff --git a/src/roblox/instance/mod.rs b/src/roblox/instance/mod.rs index 9f99267..1640b1f 100644 --- a/src/roblox/instance/mod.rs +++ b/src/roblox/instance/mod.rs @@ -12,7 +12,11 @@ use rbx_dom_weak::{ Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom, }; -use crate::roblox::shared::instance::{class_exists, class_is_a}; +use crate::{ + lune::util::TableBuilder, + roblox::exports::LuaExportsTable, + roblox::shared::instance::{class_exists, class_is_a}, +}; pub(crate) mod base; pub(crate) mod data_model; @@ -686,21 +690,24 @@ impl Instance { } } -impl Instance { - pub(crate) fn make_table(lua: &Lua, datatype_table: &LuaTable) -> LuaResult<()> { - datatype_table.set( - "new", - lua.create_function(|lua, class_name: String| { - if class_exists(&class_name) { - Instance::new_orphaned(class_name).into_lua(lua) - } else { - Err(LuaError::RuntimeError(format!( - "Failed to create Instance - '{}' is not a valid class name", - class_name - ))) - } - })?, - ) +impl LuaExportsTable<'_> for Instance { + const EXPORT_NAME: &'static str = "Instance"; + + fn create_exports_table(lua: &Lua) -> LuaResult { + let instance_new = |lua, class_name: String| { + if class_exists(&class_name) { + Instance::new_orphaned(class_name).into_lua(lua) + } else { + Err(LuaError::RuntimeError(format!( + "Failed to create Instance - '{}' is not a valid class name", + class_name + ))) + } + }; + + TableBuilder::new(lua)? + .with_function("new", instance_new)? + .build_readonly() } } diff --git a/src/roblox/mod.rs b/src/roblox/mod.rs index b640d39..6b0f938 100644 --- a/src/roblox/mod.rs +++ b/src/roblox/mod.rs @@ -1,63 +1,59 @@ use mlua::prelude::*; -use crate::roblox::instance::Instance; +use crate::lune::util::TableBuilder; pub mod datatypes; pub mod document; pub mod instance; pub mod reflection; +pub(crate) mod exports; pub(crate) mod shared; -fn make(lua: &Lua, f: F) -> LuaResult -where - F: Fn(&Lua, &LuaTable) -> LuaResult<()>, -{ - let tab = lua.create_table()?; - f(lua, &tab)?; - tab.set_readonly(true); - Ok(LuaValue::Table(tab)) -} +use exports::export; -#[rustfmt::skip] -fn make_all_datatypes(lua: &Lua) -> LuaResult> { - use datatypes::types::*; +fn create_all_exports(lua: &Lua) -> LuaResult> { + use datatypes::types::*; + use instance::Instance; Ok(vec![ - // Datatypes - ("Axes", make(lua, Axes::make_table)?), - ("BrickColor", make(lua, BrickColor::make_table)?), - ("CFrame", make(lua, CFrame::make_table)?), - ("Color3", make(lua, Color3::make_table)?), - ("ColorSequence", make(lua, ColorSequence::make_table)?), - ("ColorSequenceKeypoint", make(lua, ColorSequenceKeypoint::make_table)?), - ("Faces", make(lua, Faces::make_table)?), - ("Font", make(lua, Font::make_table)?), - ("NumberRange", make(lua, NumberRange::make_table)?), - ("NumberSequence", make(lua, NumberSequence::make_table)?), - ("NumberSequenceKeypoint", make(lua, NumberSequenceKeypoint::make_table)?), - ("PhysicalProperties", make(lua, PhysicalProperties::make_table)?), - ("Ray", make(lua, Ray::make_table)?), - ("Rect", make(lua, Rect::make_table)?), - ("UDim", make(lua, UDim::make_table)?), - ("UDim2", make(lua, UDim2::make_table)?), - ("Region3", make(lua, Region3::make_table)?), - ("Region3int16", make(lua, Region3int16::make_table)?), - ("Vector2", make(lua, Vector2::make_table)?), - ("Vector2int16", make(lua, Vector2int16::make_table)?), - ("Vector3", make(lua, Vector3::make_table)?), - ("Vector3int16", make(lua, Vector3int16::make_table)?), - // Classes - ("Instance", make(lua, Instance::make_table)?), - // Singletons + // Datatypes + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + export::(lua)?, + // Classes + export::(lua)?, + // Singletons ("Enum", Enums.into_lua(lua)?), ]) } pub fn module(lua: &Lua) -> LuaResult { - let exports = lua.create_table()?; - for (name, tab) in make_all_datatypes(lua)? { - exports.set(name, tab)?; - } - exports.set_readonly(true); - Ok(exports) + // FUTURE: We can probably create these lazily as users + // index the main exports (this return value) table and + // save some memory and startup time. The full exports + // table is quite big and probably won't get any smaller + // since we impl all roblox constructors for each datatype. + let exports = create_all_exports(lua)?; + TableBuilder::new(lua)? + .with_values(exports)? + .build_readonly() } diff --git a/src/tests.rs b/src/tests.rs index d88c047..dbe5de2 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -20,7 +20,7 @@ macro_rules! create_tests { // The rest of the test logic can continue as normal let full_name = format!("tests/{}.luau", $value); let script = read_to_string(&full_name).await?; - let lune = Lune::new().with_args( + let mut lune = Lune::new().with_args( ARGS .clone() .iter() @@ -67,6 +67,7 @@ create_tests! { process_spawn: "process/spawn", require_async: "require/tests/async", + require_async_background: "require/tests/async_background", require_async_concurrent: "require/tests/async_concurrent", require_async_sequential: "require/tests/async_sequential", require_builtins: "require/tests/builtins", @@ -79,13 +80,13 @@ create_tests! { require_siblings: "require/tests/siblings", global_g_table: "globals/_G", - // TODO: Uncomment this test, it is commented out right - // now to let CI pass so that we can make a new release - // global_coroutine: "globals/coroutine", + global_version: "globals/_VERSION", + global_coroutine: "globals/coroutine", + global_error: "globals/error", global_pcall: "globals/pcall", global_type: "globals/type", global_typeof: "globals/typeof", - global_version: "globals/version", + global_warn: "globals/warn", serde_compression_files: "serde/compression/files", serde_compression_roundtrip: "serde/compression/roundtrip", diff --git a/tests/globals/version.luau b/tests/globals/_VERSION.luau similarity index 100% rename from tests/globals/version.luau rename to tests/globals/_VERSION.luau diff --git a/tests/globals/coroutine.luau b/tests/globals/coroutine.luau index 8db0c42..7239bb7 100644 --- a/tests/globals/coroutine.luau +++ b/tests/globals/coroutine.luau @@ -1,5 +1,3 @@ -local task = require("@lune/task") - -- Coroutines should return true, ret values OR false, error local function pass() @@ -55,7 +53,9 @@ local success2 = coroutine.resume(thread2) assert(success1 == false, "Coroutine resume on dead coroutines should return false") assert(success2 == false, "Coroutine resume on dead coroutines should return false") --- Wait should work inside native lua coroutines +-- Task library wait should work inside native lua coroutines + +local task = require("@lune/task") local flag: boolean = false coroutine.resume(coroutine.create(function() diff --git a/tests/globals/error.luau b/tests/globals/error.luau new file mode 100644 index 0000000..903bf34 --- /dev/null +++ b/tests/globals/error.luau @@ -0,0 +1,15 @@ +local errValue = newproxy(false) + +local success, result = pcall(function() + error({ + Inner = errValue, + }) +end) + +assert(not success, "Pcall succeeded when erorred") + +assert(result ~= nil, "Pcall did not return error") +assert(type(result) == "table", "Pcall error value should have been a table") + +assert(result.Inner ~= nil, "Pcall error table should contain inner value") +assert(result.Inner == errValue, "Pcall error table should have correct inner value") diff --git a/tests/globals/pcall.luau b/tests/globals/pcall.luau index 7293879..fa983cb 100644 --- a/tests/globals/pcall.luau +++ b/tests/globals/pcall.luau @@ -7,7 +7,10 @@ local PORT = 9090 -- NOTE: This must be different from local function test(f, ...) local success, message = pcall(f, ...) assert(not success, "Function did not throw an error") - assert(type(message) == "userdata", "Pcall did not return a proper error") + assert( + type(message) == "string" or type(message) == "userdata", + "Pcall did not return a proper error" + ) end -- These are not async but should be pcallable @@ -24,7 +27,7 @@ local handle = net.serve(PORT, function() return "" end) -task.delay(0, function() +task.delay(0.25, function() handle.stop() end) diff --git a/tests/globals/warn.luau b/tests/globals/warn.luau new file mode 100644 index 0000000..b561dd7 --- /dev/null +++ b/tests/globals/warn.luau @@ -0,0 +1,5 @@ +assert(warn ~= nil, "Missing 'warn' global") +assert( + type(warn) == "function", + string.format("Global 'warn' should be a function, got '%s'", tostring(type(warn))) +) diff --git a/tests/net/serve/requests.luau b/tests/net/serve/requests.luau index 522d35f..1d033a1 100644 --- a/tests/net/serve/requests.luau +++ b/tests/net/serve/requests.luau @@ -1,33 +1,47 @@ local net = require("@lune/net") local process = require("@lune/process") +local stdio = require("@lune/stdio") local task = require("@lune/task") local PORT = 8080 local URL = `http://127.0.0.1:{PORT}` local RESPONSE = "Hello, lune!" -local thread = task.delay(0.2, function() - task.spawn(error, "Serve must not block the current thread") +-- Serve should not block the thread from continuing + +local thread = task.delay(1, function() + stdio.ewrite("Serve must not block the current thread\n") + task.wait(1) process.exit(1) end) local handle = net.serve(PORT, function(request) - -- info("Request:", request) - -- info("Responding with", RESPONSE) + -- print("Request:", request) + -- print("Responding with", RESPONSE) assert(request.path == "/some/path") assert(request.query.key == "param2") assert(request.query.key2 == "param3") return RESPONSE end) +task.cancel(thread) + +-- Serve should respond to a request we send to it + +local thread2 = task.delay(1, function() + stdio.ewrite("Serve should respond to requests in a reasonable amount of time\n") + task.wait(1) + process.exit(1) +end) + local response = net.request(URL .. "/some/path?key=param1&key=param2&key2=param3").body assert(response == RESPONSE, "Invalid response from server") -task.cancel(thread) -handle.stop() +task.cancel(thread2) -- Stopping is not guaranteed to happen instantly since it is async, but -- it should happen on the next yield, so we wait the minimum amount here +handle.stop() task.wait() -- Sending a net request may error if there was diff --git a/tests/net/serve/websockets.luau b/tests/net/serve/websockets.luau index 0e7e997..ad53a38 100644 --- a/tests/net/serve/websockets.luau +++ b/tests/net/serve/websockets.luau @@ -1,35 +1,27 @@ local net = require("@lune/net") local process = require("@lune/process") +local stdio = require("@lune/stdio") local task = require("@lune/task") local PORT = 8081 -local URL = `http://127.0.0.1:{PORT}` local WS_URL = `ws://127.0.0.1:{PORT}` local REQUEST = "Hello from client!" local RESPONSE = "Hello, lune!" -local thread = task.delay(0.2, function() - task.spawn(error, "Serve must not block the current thread") +-- Serve should not block the thread from continuing + +local thread = task.delay(1, function() + stdio.ewrite("Serve must not block the current thread\n") + task.wait(1) process.exit(1) end) ---[[ - Serve should also take a full config with handler functions - - A server should also be able to start on a previously closed port -]] - -local handle = net.serve(PORT, function(request) - return RESPONSE -end) - -task.cancel(thread) -handle.stop() -task.wait() - -local handle2 = net.serve(PORT, { +local handle = net.serve(PORT, { handleRequest = function() - return RESPONSE + stdio.ewrite("Web socket should upgrade automatically, not pass to the request handler\n") + task.wait(1) + process.exit(1) + return "unreachable" end, handleWebSocket = function(socket) local socketMessage = socket.next() @@ -39,10 +31,16 @@ local handle2 = net.serve(PORT, { end, }) -local response = net.request(URL).body -assert(response == RESPONSE, "Invalid response from server") +task.cancel(thread) + +-- Web socket responses should also be responded to + +local thread2 = task.delay(1, function() + stdio.ewrite("Serve should respond to websockets in a reasonable amount of time\n") + task.wait(1) + process.exit(1) +end) --- Web socket client should work local socket = net.socket(WS_URL) socket.send(REQUEST) @@ -53,6 +51,8 @@ assert(socketMessage == RESPONSE, "Invalid web socket response from server") socket.close() +task.cancel(thread2) + -- Wait for the socket to close and make sure we can't send messages afterwards task.wait() local success3, err2 = (pcall :: any)(socket.send, "") @@ -64,4 +64,4 @@ assert( ) -- Stop the server to end the test -handle2.stop() +handle.stop() diff --git a/tests/process/spawn.luau b/tests/process/spawn.luau index 6878e8e..c3537df 100644 --- a/tests/process/spawn.luau +++ b/tests/process/spawn.luau @@ -1,12 +1,21 @@ local process = require("@lune/process") +local stdio = require("@lune/stdio") local task = require("@lune/task") --- Spawning a child process should work with options +-- Spawning a child process should work, with options + +local thread = task.delay(1, function() + stdio.ewrite("Spawning a process should take a reasonable amount of time\n") + task.wait(1) + process.exit(1) +end) local result = process.spawn("ls", { "-a", }) +task.cancel(thread) + assert(result.ok, "Failed to spawn child process") assert(result.stderr == "", "Stderr was not empty") @@ -85,6 +94,12 @@ assert(homeDir1 == homeDir2, "Home dirs did not match when performing tilde subs local SLEEP_DURATION = 1 / 4 local SLEEP_SAMPLES = 2 +local thread2 = task.delay(SLEEP_DURATION * 1.5, function() + stdio.ewrite("Spawning a sleep process should take a reasonable amount of time\n") + task.wait(1) + process.exit(1) +end) + local sleepStart = os.clock() local sleepCounter = 0 for i = 1, SLEEP_SAMPLES, 1 do @@ -97,9 +112,15 @@ while sleepCounter < SLEEP_SAMPLES do task.wait() end +task.cancel(thread2) + local sleepElapsed = os.clock() - sleepStart assert( - (sleepElapsed >= SLEEP_DURATION) and (sleepElapsed < SLEEP_DURATION * 1.5), + sleepElapsed >= SLEEP_DURATION, + "Spawning a process that does blocking sleep did not sleep enough" +) +assert( + sleepElapsed < SLEEP_DURATION * 1.5, "Coroutine yielded the main lua thread during process yield" ) diff --git a/tests/require/tests/async_background.luau b/tests/require/tests/async_background.luau new file mode 100644 index 0000000..ce35227 --- /dev/null +++ b/tests/require/tests/async_background.luau @@ -0,0 +1,51 @@ +local net = require("@lune/net") +local process = require("@lune/process") +local stdio = require("@lune/stdio") +local task = require("@lune/task") + +-- Spawn an asynchronous background task (eg. web server) + +local PORT = 8082 + +task.delay(3, function() + stdio.ewrite("Test did not complete in time\n") + task.wait(1) + process.exit(1) +end) + +local handle = net.serve(PORT, function(request) + return "" +end) + +-- Require modules same way we did in the async_concurrent and async_sequential tests + +local module3 +local module4 + +task.defer(function() + module4 = require("./modules/async") +end) + +task.spawn(function() + module3 = require("./modules/async") +end) + +local _module1 = require("./modules/async") +local _module2 = require("./modules/async") + +task.wait(1) + +assert(type(module3) == "table", "Required module3 did not return a table") +assert(module3.Foo == "Bar", "Required module3 did not contain correct values") +assert(module3.Hello == "World", "Required module3 did not contain correct values") + +assert(type(module4) == "table", "Required module4 did not return a table") +assert(module4.Foo == "Bar", "Required module4 did not contain correct values") +assert(module4.Hello == "World", "Required module4 did not contain correct values") + +assert(module3 == module4, "Required modules should point to the same return value") + +-- Stop the server and exit successfully + +handle.stop() +process.exit(0) diff --git a/tests/roblox/datatypes/CFrame.luau b/tests/roblox/datatypes/CFrame.luau index 85b40bb..6ac32e0 100644 --- a/tests/roblox/datatypes/CFrame.luau +++ b/tests/roblox/datatypes/CFrame.luau @@ -103,9 +103,6 @@ local offset = CFrame.new(0, 0, -5) assert(offset:ToWorldSpace(offset).Z == offset.Z * 2) assert(offset:ToObjectSpace(offset).Z == 0) -assert(select("#", offset:ToWorldSpace(offset, offset, offset)) == 3) -assert(select("#", offset:ToObjectSpace(offset, offset, offset)) == 3) - local world = CFrame.fromOrientation(0, math.rad(90), 0) * CFrame.new(0, 0, -5) local world2 = CFrame.fromOrientation(0, -math.rad(90), 0) * CFrame.new(0, 0, -5) assertEq(CFrame.identity:ToObjectSpace(world), world) diff --git a/tests/serde/json/source.luau b/tests/serde/json/source.luau index 0919cd0..53dcca1 100644 --- a/tests/serde/json/source.luau +++ b/tests/serde/json/source.luau @@ -1,15 +1,15 @@ -local JSON_STRING = [[{"Hello":"World","Inner":{"Array":[1,2,3]},"Foo":"Bar"}]] +local JSON_STRING = [[{"Foo":"Bar","Hello":"World","Inner":{"Array":[1,3,2]}}]] local JSON_STRING_PRETTY = [[{ + "Foo": "Bar", "Hello": "World", "Inner": { "Array": [ 1, - 2, - 3 + 3, + 2 ] - }, - "Foo": "Bar" + } }]] return { diff --git a/tests/task/wait.luau b/tests/task/wait.luau index 08d727a..bc72fe4 100644 --- a/tests/task/wait.luau +++ b/tests/task/wait.luau @@ -1,9 +1,11 @@ +local process = require("@lune/process") local stdio = require("@lune/stdio") local task = require("@lune/task") -- Wait should be accurate down to at least 10ms +-- on Windows, and 6ms on Linux and / or macOS -local EPSILON = 10 / 1_000 +local EPSILON = if process.os == "windows" then 10 / 1_000 else 6 / 1_000 local function test(expected: number) local start = os.clock() @@ -41,7 +43,10 @@ local function measure(duration: number) end end -measure(1 / 100) +-- About 20ms is the shortest safe sleep time on Windows, but +-- Linux and macOS can do down to about 10ms or less safely +measure(if process.os == "windows" then 15 / 1_000 else 5 / 1_000) + measure(1 / 60) measure(1 / 30) measure(1 / 20)