diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 64e8000..7eea48f 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -5,137 +5,22 @@ on: - workflow_dispatch jobs: - test-build: - name: Build & Test - strategy: - matrix: - os: - - ubuntu-20.04 - - macos-11 - - windows-2019 - runs-on: ${{ matrix.os }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Build - run: cargo build --all-features - - name: Test - uses: bp3d-actions/cargo@main - with: - check-name: cargo test (${{ matrix.os }}) - command: test - args: --all-features --no-fail-fast - token: ${{ secrets.GITHUB_TOKEN }} - - clippy: - name: Check | Clippy - if: ${{ always() }} - needs: test-build - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: clippy - - name: Run check - uses: bp3d-actions/clippy-check@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features - - audit: - name: Check | Audit - if: ${{ always() }} - needs: test-build - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Install Audit Tool - run: cargo install cargo-audit - - name: Run check - uses: bp3d-actions/audit-check@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - - fmt: - name: Format Code - if: ${{ always() && github.ref != 'refs/heads/master' }} - needs: - - clippy - - audit - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt - - name: Run code formatter - uses: bp3d-actions/rustfmt-check@main - with: - token: ${{ secrets.GITHUB_TOKEN }} - - version: - name: Get Version - needs: test-build - runs-on: ubuntu-latest - outputs: - name: ${{ steps.version.outputs.name }} - version: ${{ steps.version.outputs.version }} - isnew: ${{ steps.version.outputs.isnew }} - ispre: ${{ steps.version.outputs.ispre }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Get Version - id: version - uses: bp3d-actions/cargo-version@main - with: - mode: get - token: ${{ secrets.GITHUB_TOKEN }} - - create-pre-release: - name: Create Pre Release - needs: version - if: github.ref == 'refs/heads/develop' && needs.version.outputs.isnew == 'true' && needs.version.outputs.ispre == 'true' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Setup cargo - run: cargo login ${{ secrets.RELEASE_TOKEN }} - - name: Publish - run: cargo publish - - name: Create - uses: ncipollo/release-action@main - with: - tag: ${{ needs.version.outputs.version }} - commit: ${{ github.ref }} - prerelease: true - name: ${{ needs.version.outputs.name }} release ${{ needs.version.outputs.version }} - body: "[Link to crates.io](https://crates.io/crates/${{ needs.version.outputs.name }})" - - create-release-pr: - name: Create Release Pull Request - needs: version - if: needs.version.outputs.isnew == 'true' && needs.version.outputs.ispre == 'false' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Create Pull Request - uses: repo-sync/pull-request@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - destination_branch: master - pr_title: Release ${{ needs.version.outputs.version }} + Test: + uses: BlockProject3D/workflows/.github/workflows/Build_Test.yml@main + with: + rust_components: rust-src + + Analyze: + uses: BlockProject3D/workflows/.github/workflows/Analyze.yml@main + needs: Test + + Coverage: + uses: BlockProject3D/workflows/.github/workflows/Coverage.yml@main + with: + rust_components: rust-src + + Release: + uses: BlockProject3D/workflows/.github/workflows/Release.yml@main + needs: Test + secrets: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf2e432..b4df58a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,43 +6,7 @@ on: - master jobs: - version: - name: Get Version - runs-on: ubuntu-latest - outputs: - name: ${{ steps.version.outputs.name }} - version: ${{ steps.version.outputs.version }} - isnew: ${{ steps.version.outputs.isnew }} - ispre: ${{ steps.version.outputs.ispre }} - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Get Version - id: version - uses: bp3d-actions/cargo-version@main - with: - mode: get - token: ${{ secrets.GITHUB_TOKEN }} - - create-release: - name: Release - needs: version - if: needs.version.outputs.isnew == 'true' && needs.version.outputs.ispre == 'false' - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - - name: Setup cargo - run: cargo login ${{ secrets.RELEASE_TOKEN }} - - name: Publish - run: cargo publish - - name: Create - uses: ncipollo/release-action@main - with: - tag: ${{ needs.version.outputs.version }} - commit: ${{ github.ref }} - prerelease: false - name: ${{ needs.version.outputs.name }} release ${{ needs.version.outputs.version }} - body: "[Link to crates.io](https://crates.io/crates/${{ needs.version.outputs.name }})" + Publish: + uses: BlockProject3D/workflows/.github/workflows/Publish.yml@main + secrets: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..784cf44 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "LuaJIT"] + path = LuaJIT + url = https://github.com/LuaJIT/LuaJIT.git diff --git a/Cargo.toml b/Cargo.toml index 35fff1b..dd2060f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,8 @@ -[package] -name = "test" -version = "0.1.0" -authors = ["Yuri Edward "] -edition = "2018" -description = "" -license = "BSD-3-Clause" -repository = "https://gitlab.com/bp3d/test" -readme = "./README.MD" -keywords = [] -categories = [] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] - -[features] - +[workspace] +members = [ + "build", + "codegen", + "core", + "shell/core", + "shell/proto" +] diff --git a/LuaJIT b/LuaJIT new file mode 160000 index 0000000..eec7a80 --- /dev/null +++ b/LuaJIT @@ -0,0 +1 @@ +Subproject commit eec7a8016c3381b949b5d84583800d05897fa960 diff --git a/build/Cargo.toml b/build/Cargo.toml new file mode 100644 index 0000000..1f9791d --- /dev/null +++ b/build/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "bp3d-lua-build" +version = "1.0.0-rc.1.0.0" +edition = "2024" +publish = false + +[dependencies] +bp3d-os = { version = "2.2.3", features = ["fs"] } +phf = { version = "0.11.3", features = ["macros"] } +cc = "1.2.15" diff --git a/build/src/build/interface.rs b/build/src/build/interface.rs new file mode 100644 index 0000000..6e26ed2 --- /dev/null +++ b/build/src/build/interface.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::BuildInfo; +use crate::util::CommandRunner; +use std::path::PathBuf; + +pub struct Lib { + pub name: String, + pub path: PathBuf, + pub dynamic: bool, +} + +pub trait Build { + fn build(info: &BuildInfo, runner: &CommandRunner) -> std::io::Result<()>; + fn post_build(info: &BuildInfo, runner: &CommandRunner) -> std::io::Result<()>; + fn get_linked_lib(info: &BuildInfo) -> Lib; + + fn run(info: &BuildInfo) -> std::io::Result { + Self::build(info, &CommandRunner::new("failed to build LuaJIT"))?; + Self::post_build(info, &CommandRunner::new("failed to post-build LuaJIT"))?; + Ok(Self::get_linked_lib(info)) + } +} diff --git a/build/src/build/linux.rs b/build/src/build/linux.rs new file mode 100644 index 0000000..d056c1b --- /dev/null +++ b/build/src/build/linux.rs @@ -0,0 +1,74 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::BuildInfo; +use crate::build::Build; +use crate::build::interface::Lib; +use crate::util::CommandRunner; +use std::process::Command; + +pub struct Linux; + +impl Build for Linux { + fn build(info: &BuildInfo, runner: &CommandRunner) -> std::io::Result<()> { + let soname = format!("TARGET_SONAME=libbp3d-luajit-{}.so", info.version()); + runner.run( + Command::new("make") + .arg(soname) + .current_dir(info.build_dir()), + ) + } + + fn post_build(info: &BuildInfo, _: &CommandRunner) -> std::io::Result<()> { + let filename = format!("libbp3d-luajit-{}.so", info.version()); + let path_to_so = info.build_dir().join("src").join("libluajit.so"); + let path_to_dylib = info.build_dir().join(&filename); + std::fs::copy(&path_to_so, path_to_dylib)?; + let path_to_dylib2 = info.target_dir().join(filename); + std::fs::copy(&path_to_so, path_to_dylib2)?; + std::fs::remove_file(path_to_so)?; + Ok(()) + } + + fn get_linked_lib(info: &BuildInfo) -> Lib { + if info.dynamic() { + let name = format!("bp3d-luajit-{}", info.version()); + Lib { + name, + path: info.build_dir().into(), + dynamic: true, + } + } else { + Lib { + name: "luajit".into(), + path: info.build_dir().join("src"), + dynamic: false, + } + } + } +} diff --git a/build/src/build/mac.rs b/build/src/build/mac.rs new file mode 100644 index 0000000..26b0a97 --- /dev/null +++ b/build/src/build/mac.rs @@ -0,0 +1,88 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::build::Build; +use crate::build::interface::Lib; +use crate::util::CommandRunner; +use crate::{BuildInfo, Target}; +use std::io::Error; +use std::process::Command; + +pub struct MacOS; + +impl Build for MacOS { + fn build(info: &BuildInfo, runner: &CommandRunner) -> std::io::Result<()> { + match info.target() { + Target::MacAarch64 => runner.run( + Command::new("make") + .env("MACOSX_DEPLOYMENT_TARGET", "11.0") + .current_dir(info.build_dir()), + ), + Target::MacAmd64 => runner.run( + Command::new("make") + .env("MACOSX_DEPLOYMENT_TARGET", "10.11") + .current_dir(info.build_dir()), + ), + _ => Err(Error::other("unsupported target")), + } + } + + fn post_build(info: &BuildInfo, runner: &CommandRunner) -> std::io::Result<()> { + let filename = format!("libbp3d-luajit-{}.dylib", info.version()); + let filename2 = format!("@rpath/libbp3d-luajit-{}.dylib", info.version()); + let path_to_so = info.build_dir().join("src"); + runner.run( + Command::new("install_name_tool") + .args(["-id", &filename2, "libluajit.so"]) + .current_dir(&path_to_so), + )?; + let path_to_dylib = info.build_dir().join(&filename); + std::fs::copy(path_to_so.join("libluajit.so"), path_to_dylib)?; + let path_to_dylib2 = info.target_dir().join(filename); + std::fs::copy(path_to_so.join("libluajit.so"), path_to_dylib2)?; + std::fs::remove_file(path_to_so.join("libluajit.so"))?; + Ok(()) + } + + fn get_linked_lib(info: &BuildInfo) -> Lib { + if info.dynamic() { + let name = format!("bp3d-luajit-{}", info.version()); + Lib { + name, + path: info.build_dir().into(), + dynamic: true, + } + } else { + Lib { + name: "luajit".into(), + path: info.build_dir().join("src"), + dynamic: false, + } + } + } +} diff --git a/build/src/build/mod.rs b/build/src/build/mod.rs new file mode 100644 index 0000000..16a8518 --- /dev/null +++ b/build/src/build/mod.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod interface; +mod linux; +mod mac; +mod windows; + +pub use interface::*; + +pub use linux::Linux; +pub use mac::MacOS; +pub use windows::Windows; diff --git a/build/src/build/windows.rs b/build/src/build/windows.rs new file mode 100644 index 0000000..0b551f7 --- /dev/null +++ b/build/src/build/windows.rs @@ -0,0 +1,83 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::BuildInfo; +use crate::build::Build; +use crate::build::interface::Lib; +use crate::util::CommandRunner; +use std::io::Error; +use std::process::Command; + +pub struct Windows; + +impl Build for Windows { + fn build(info: &BuildInfo, runner: &CommandRunner) -> std::io::Result<()> { + let mut cmd = Command::new("cmd"); + cmd.arg("/C").arg("msvcbuild.bat"); + let cl = cc::windows_registry::find_tool(info.target_name(), "cl.exe") + .ok_or(Error::other("unable to find cl.exe"))?; + for (k, v) in cl.env() { + cmd.env(k, v); + } + if info.target_name().contains("aarch64") { + cmd.env("VSCMD_ARG_HOST_ARCH", "arm64"); + cmd.env("VSCMD_ARG_TGT_ARCH", "arm64"); + } + if !info.dynamic() { + cmd.arg("static"); + } + let dllname = format!("libbp3d-luajit-{}.dll", info.version()); + let libname = format!("libbp3d-luajit-{}.lib", info.version()); + cmd.env("LJDLLNAME", dllname); + cmd.env("LJLIBNAME", libname); + runner.run(cmd.current_dir(info.build_dir().join("src"))) + } + + fn post_build(info: &BuildInfo, _: &CommandRunner) -> std::io::Result<()> { + if !info.dynamic() { + //Nothing to be done in non-dynamic builds. + return Ok(()); + } + let dllname = format!("libbp3d-luajit-{}.dll", info.version()); + let path_to_dll = info.build_dir().join("src").join(&dllname); + let path_to_dylib = info.build_dir().join(&dllname); + std::fs::copy(&path_to_dll, path_to_dylib)?; + let path_to_dylib2 = info.target_dir().join(dllname); + std::fs::copy(path_to_dll, path_to_dylib2)?; + Ok(()) + } + + fn get_linked_lib(info: &BuildInfo) -> Lib { + let libname = format!("libbp3d-luajit-{}", info.version()); + Lib { + name: libname, + path: info.build_dir().join("src"), + dynamic: info.dynamic(), + } + } +} diff --git a/build/src/info.rs b/build/src/info.rs new file mode 100644 index 0000000..54621f3 --- /dev/null +++ b/build/src/info.rs @@ -0,0 +1,99 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::Target; +use crate::build::{Build, Lib, Linux, MacOS, Windows}; +use std::io::Error; +use std::path::{Path, PathBuf}; + +pub struct BuildInfoBase<'a> { + pub dynamic: bool, + pub target_name: &'a str, + pub build_dir: &'a Path, + pub manifest: &'a Path, +} + +pub struct BuildInfo<'a> { + base: BuildInfoBase<'a>, + target_dir: PathBuf, + crate_version: String, +} + +const VERSION: &str = "version = \""; + +impl<'a> BuildInfo<'a> { + pub fn new(base: BuildInfoBase<'a>) -> std::io::Result { + let manifest = std::fs::read_to_string(base.manifest)?; + let target_dir = base.build_dir.join("../../../.."); + let start = manifest + .find(VERSION) + .ok_or(Error::other("failed to find crate version"))?; + let version = &manifest[start + VERSION.len()..]; + let end = version + .find("\"") + .ok_or(Error::other("failed to find crate version"))?; + Ok(Self { + base, + target_dir, + crate_version: String::from(&version[..end]), + }) + } + + pub fn build_dir(&self) -> &Path { + self.base.build_dir + } + + pub fn dynamic(&self) -> bool { + self.base.dynamic + } + + pub fn target(&self) -> Target { + Target::get(self.base.target_name) + } + + pub fn target_name(&self) -> &str { + self.base.target_name + } + + pub fn target_dir(&self) -> &Path { + &self.target_dir + } + + pub fn version(&self) -> &str { + &self.crate_version + } + + pub fn build(self) -> std::io::Result { + match self.target() { + Target::MacAmd64 | Target::MacAarch64 => MacOS::run(&self), + Target::Linux => Linux::run(&self), + Target::Windows => Windows::run(&self), + Target::Unsupported => Err(Error::other("unsupported target")), + } + } +} diff --git a/build/src/lib.rs b/build/src/lib.rs new file mode 100644 index 0000000..abc51c8 --- /dev/null +++ b/build/src/lib.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod build; +mod info; +mod patch; +mod target; +mod util; + +pub use crate::info::{BuildInfo, BuildInfoBase}; +pub use crate::patch::Patch; +pub use crate::target::Target; diff --git a/build/src/patch.rs b/build/src/patch.rs new file mode 100644 index 0000000..98bf3c9 --- /dev/null +++ b/build/src/patch.rs @@ -0,0 +1,123 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::util::CommandRunner; +use bp3d_os::fs::CopyOptions; +use std::ffi::OsStr; +use std::fs::File; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; + +pub struct Summary { + patches: Vec, +} + +impl Summary { + pub fn write(&self, out: &Path) -> std::io::Result<()> { + let mut file = File::create(out)?; + writeln!(&mut file, "const PATCH_LIST: &[&str] = &[")?; + for patch in &self.patches { + writeln!(&mut file, " \"{}\",", patch)?; + } + writeln!(&mut file, "];")?; + Ok(()) + } +} + +pub struct Patch { + src_path: PathBuf, + patch_dir: PathBuf, + patch_list: Vec, +} + +impl Patch { + pub fn new(patch_dir: &Path, luajit_src: &Path) -> std::io::Result { + let patch_dir = bp3d_os::fs::get_absolute_path(patch_dir)?; + let src_path = bp3d_os::fs::get_absolute_path(luajit_src)?; + CommandRunner::new("failed to revert").run( + Command::new("git") + .args(["checkout", "."]) + .current_dir(&src_path), + )?; + Ok(Patch { + patch_dir, + src_path, + patch_list: Vec::new(), + }) + } + + pub fn apply(&mut self, name: &str) -> std::io::Result<()> { + CommandRunner::new("failed to patch").run( + Command::new("git") + .args([ + OsStr::new("apply"), + self.patch_dir.join(format!("{}.patch", name)).as_os_str(), + ]) + .current_dir(&self.src_path), + )?; + self.patch_list.push(name.into()); + Ok(()) + } + + pub fn get_patch_list(&self) -> impl Iterator { + self.patch_list.iter().map(|v| &**v) + } + + pub fn apply_all<'a>( + mut self, + patches: impl IntoIterator, + out_path: &Path, + ) -> std::io::Result { + for patch in patches { + self.apply(patch)?; + } + if !out_path.is_dir() { + bp3d_os::fs::copy( + &self.src_path, + out_path, + CopyOptions::new().exclude(OsStr::new(".git")), + )?; + } + Ok(Summary { + patches: self.get_patch_list().map(String::from).collect(), + }) + } +} + +impl Drop for Patch { + fn drop(&mut self) { + CommandRunner::new("failed to revert") + .run( + Command::new("git") + .args(["checkout", "."]) + .current_dir(&self.src_path), + ) + .unwrap(); + } +} diff --git a/build/src/target.rs b/build/src/target.rs new file mode 100644 index 0000000..a78d06c --- /dev/null +++ b/build/src/target.rs @@ -0,0 +1,54 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use phf::phf_map; + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum Target { + MacAmd64, + MacAarch64, + Linux, + Windows, + Unsupported, +} + +static TARGET_MAP: phf::Map<&'static str, Target> = phf_map! { + "aarch64-apple-darwin" => Target::MacAarch64, + "aarch64-unknown-linux-gnu" => Target::Linux, + "i686-pc-windows-msvc" => Target::Windows, + "x86_64-pc-windows-msvc" => Target::Windows, + "aarch64-pc-windows-msvc" => Target::Windows, + "x86_64-apple-darwin" => Target::MacAmd64, + "x86_64-unknown-linux-gnu" => Target::Linux +}; + +impl Target { + pub fn get(name: &str) -> Target { + *TARGET_MAP.get(name).unwrap_or(&Target::Unsupported) + } +} diff --git a/build/src/util.rs b/build/src/util.rs new file mode 100644 index 0000000..ea0c5c6 --- /dev/null +++ b/build/src/util.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::io::Error; +use std::process::Command; + +pub struct CommandRunner { + msg: &'static str, +} + +impl CommandRunner { + pub fn new(msg: &'static str) -> CommandRunner { + Self { msg } + } + + pub fn run(&self, cmd: &mut Command) -> std::io::Result<()> { + let stat = cmd.status()?; + if stat.success() { + Ok(()) + } else { + Err(Error::other(self.msg)) + } + } +} diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml new file mode 100644 index 0000000..a0f31e1 --- /dev/null +++ b/codegen/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "bp3d-lua-codegen" +version = "1.0.0-rc.1.0.0" +edition = "2021" #Due to gen be a reserved keyword! +publish = false + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0" +syn = { version = "2.0.98", features = ["full"] } +proc-macro2 = "1.0.26" diff --git a/codegen/src/gen/from_param.rs b/codegen/src/gen/from_param.rs new file mode 100644 index 0000000..ea7b150 --- /dev/null +++ b/codegen/src/gen/from_param.rs @@ -0,0 +1,230 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::parser::enums::EnumVariant; +use crate::parser::structs::StructField; +use crate::parser::Parser; +use proc_macro2::{Ident, TokenStream}; +use quote::{quote, ToTokens}; +use syn::Generics; + +pub struct FromParam { + name: Ident, + generics: Generics, + is_index_based: bool, + is_simple_enum: bool, +} + +impl FromParam { + pub fn new(name: Ident, generics: Generics) -> Self { + Self { + name, + generics, + is_index_based: false, + is_simple_enum: true, + } + } +} + +pub struct Field { + name: Ident, + from_param: TokenStream, + try_from_param: TokenStream, +} + +impl Parser for FromParam { + type ParsedField = Field; + type ParsedVariant = Field; + + fn parse_field(&mut self, field: StructField) -> Self::ParsedField { + let name = field.unique_name; + let ty = field.ty; + self.is_index_based = field.unique_name_is_index; + let reader = if field.unique_name_is_index { + let index = field.index + 1; //Lua indices starts at 1, not 0. + quote! { bp3d_lua::ffi::lua::lua_rawgeti(vm.as_ptr(), index, #index as _) } + } else { + quote! { bp3d_lua::ffi::lua::lua_getfield(vm.as_ptr(), index, bp3d_lua::c_stringify!(#name).as_ptr()) } + }; + Field { + name: name.clone(), + from_param: quote! { + #reader; + top += 1; + let #name: #ty = bp3d_lua::vm::function::FromParam::from_param(vm, top); + }, + try_from_param: quote! { + unsafe { #reader }; + top += 1; + let #name: #ty = bp3d_lua::vm::function::FromParam::try_from_param(vm, top)?; + }, + } + } + + fn parse_variant(&mut self, variant: EnumVariant) -> Self::ParsedVariant { + match variant { + EnumVariant::SingleField(v) => { + self.is_simple_enum = false; + let ty = v.field.ty; + let name = self.name.clone(); + let variant = v.unique_name; + Field { + name: variant.clone(), + from_param: quote! { + match <#ty as bp3d_lua::vm::function::FromParam>::try_from_param(vm, index) { + Some(v) => return #name::#variant(v), + None => () + }; + }, + try_from_param: quote! { + match <#ty as bp3d_lua::vm::function::FromParam>::try_from_param(vm, index) { + Some(v) => return Some(#name::#variant(v)), + None => () + }; + }, + } + } + EnumVariant::MultiField(_) => panic!("Multi-field enum variants are not supported"), + EnumVariant::None(variant) => { + let name = self.name.clone(); + //FIXME: Use decapitalize + maybe allow configuration for which case to use + let vname = variant.to_string().to_lowercase(); + Field { + name: variant.clone(), + from_param: quote! { + match enum_name == #vname { + true => return #name::#variant, + false => () + }; + }, + try_from_param: quote! { + match enum_name == #vname { + true => return Some(#name::#variant), + false => () + }; + }, + } + } + } + } + + fn gen_struct(self, parsed: Vec) -> TokenStream { + let name = self.name; + let generics = self.generics; + let lifetime = generics + .lifetimes() + .next() + .map(|v| v.into_token_stream()) + .unwrap_or(quote! { '_ }); + let from_params = parsed.iter().map(|field| &field.from_param); + let try_from_params = parsed.iter().map(|field| &field.try_from_param); + let values = parsed.iter().map(|field| &field.name); + let end = if self.is_index_based { + quote! { + #name(#(#values),*) + } + } else { + quote! { + #name { #(#values),* } + } + }; + quote! { + impl #generics bp3d_lua::vm::function::FromParam<#lifetime> for #name #generics { + unsafe fn from_param(vm: &#lifetime bp3d_lua::vm::Vm, index: i32) -> Self { + unsafe { bp3d_lua::ffi::laux::luaL_checktype(vm.as_ptr(), index, bp3d_lua::ffi::lua::Type::Table) }; + let mut top = vm.top(); + #(#from_params)* + #end + } + + fn try_from_param(vm: &#lifetime bp3d_lua::vm::Vm, index: i32) -> Option { + bp3d_lua::vm::value::util::check_type_equals(vm, index, bp3d_lua::ffi::lua::Type::Table).ok()?; + let mut top = vm.top(); + let mut f = || -> Option { + #(#try_from_params)* + Some(#end) + }; + match f() { + Some(v) => Some(v), + None => { + // Reset stack position. + unsafe { bp3d_lua::ffi::lua::lua_settop(vm.as_ptr(), top) }; + None + } + } + } + } + + unsafe impl #generics bp3d_lua::util::core::SimpleDrop for #name #generics { } + } + } + + fn gen_enum(self, parsed: Vec) -> TokenStream { + let name = self.name; + let generics = self.generics; + let lifetime = generics + .lifetimes() + .next() + .map(|v| v.into_token_stream()) + .unwrap_or(quote! { '_ }); + let from_params = parsed.iter().map(|field| &field.from_param); + let try_from_params = parsed.iter().map(|field| &field.try_from_param); + let start_from_param = if self.is_simple_enum { + quote! { + let enum_name = <&str as bp3d_lua::vm::function::FromParam>::from_param(vm, index); + } + } else { + quote! {} + }; + let start_try_from_param = if self.is_simple_enum { + quote! { + let enum_name = <&str as bp3d_lua::vm::function::FromParam>::try_from_param(vm, index)?; + } + } else { + quote! {} + }; + quote! { + impl #generics bp3d_lua::vm::function::FromParam<#lifetime> for #name #generics { + unsafe fn from_param(vm: &#lifetime bp3d_lua::vm::Vm, index: i32) -> Self { + #start_from_param + #(#from_params)* + bp3d_lua::ffi::laux::luaL_error(vm.as_ptr(), c"Unable to find a type satisfying constraints".as_ptr()); + std::hint::unreachable_unchecked() + } + + fn try_from_param(vm: &#lifetime bp3d_lua::vm::Vm, index: i32) -> Option { + #start_try_from_param + #(#try_from_params)* + None + } + } + + unsafe impl #generics bp3d_lua::util::core::SimpleDrop for #name #generics { } + } + } +} diff --git a/codegen/src/gen/into_lua.rs b/codegen/src/gen/into_lua.rs new file mode 100644 index 0000000..7e0a508 --- /dev/null +++ b/codegen/src/gen/into_lua.rs @@ -0,0 +1,113 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::parser::enums::EnumVariant; +use crate::parser::structs::StructField; +use crate::parser::Parser; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::{Generics, Index}; + +pub struct IntoLua { + pub name: Ident, + pub generics: Generics, +} + +impl IntoLua { + pub fn new(name: Ident, generics: Generics) -> Self { + Self { name, generics } + } +} + +impl Parser for IntoLua { + type ParsedField = TokenStream; + type ParsedVariant = TokenStream; + + fn parse_field(&mut self, field: StructField) -> Self::ParsedField { + if field.unique_name_is_index { + let name_idx = Index::from(field.index); + // Table indices starts at 1 rather than 0 in Lua. + let index = (field.index + 1) as i32; + quote! { + tbl.set(#index, self.#name_idx).unwrap(); + } + } else { + let name = field.unique_name; + quote! { + tbl.set(bp3d_lua::c_stringify!(#name), self.#name).unwrap(); + } + } + } + + fn parse_variant(&mut self, variant: EnumVariant) -> Self::ParsedVariant { + match variant { + EnumVariant::SingleField(v) => { + let name = v.unique_name; + let ty = v.field.ty; + quote! { + Self::#name(v) => <#ty as bp3d_lua::vm::value::IntoLua>::into_lua(v, vm), + } + } + EnumVariant::MultiField(_) => panic!("Multi-field enum variants are not supported"), + EnumVariant::None(name) => { + let str = name.to_string(); + quote! { + Self::#name => <&str as bp3d_lua::vm::value::IntoLua>::into_lua(#str, vm), + } + } + } + } + + fn gen_struct(self, parsed: Vec) -> TokenStream { + let name = self.name; + let generics = self.generics; + quote! { + unsafe impl #generics bp3d_lua::vm::value::IntoLua for #name #generics { + fn into_lua(self, vm: &bp3d_lua::vm::Vm) -> u16 { + let mut tbl = bp3d_lua::vm::table::Table::new(vm); + #(#parsed)* + 1 + } + } + } + } + + fn gen_enum(self, parsed: Vec) -> TokenStream { + let name = self.name; + let generics = self.generics; + quote! { + unsafe impl #generics bp3d_lua::vm::value::IntoLua for #name #generics { + fn into_lua(self, vm: &bp3d_lua::vm::Vm) -> u16 { + match self { + #(#parsed)* + } + } + } + } + } +} diff --git a/codegen/src/gen/into_param.rs b/codegen/src/gen/into_param.rs new file mode 100644 index 0000000..f7507a3 --- /dev/null +++ b/codegen/src/gen/into_param.rs @@ -0,0 +1,99 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::gen::IntoLua; +use crate::parser::enums::EnumVariant; +use crate::parser::structs::StructField; +use crate::parser::Parser; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::Generics; + +pub struct IntoParam(IntoLua); + +impl IntoParam { + pub fn new(name: Ident, generics: Generics) -> Self { + Self(IntoLua::new(name, generics)) + } +} + +impl Parser for IntoParam { + type ParsedField = TokenStream; + type ParsedVariant = TokenStream; + + fn parse_field(&mut self, field: StructField) -> Self::ParsedField { + self.0.parse_field(field) + } + + fn parse_variant(&mut self, variant: EnumVariant) -> Self::ParsedVariant { + match variant { + EnumVariant::SingleField(v) => { + let name = v.unique_name; + let ty = v.field.ty; + quote! { + Self::#name(v) => <#ty as bp3d_lua::vm::function::IntoParam>::into_param(v, vm), + } + } + EnumVariant::MultiField(_) => panic!("Multi-field enum variants are not supported"), + EnumVariant::None(name) => { + let str = name.to_string(); + quote! { + Self::#name => <&str as bp3d_lua::vm::function::IntoParam>::into_param(#str, vm), + } + } + } + } + + fn gen_struct(self, parsed: Vec) -> TokenStream { + let name = self.0.name; + let generics = self.0.generics; + quote! { + unsafe impl #generics bp3d_lua::vm::function::IntoParam for #name #generics { + fn into_param(self, vm: &bp3d_lua::vm::Vm) -> i32 { + let mut tbl = bp3d_lua::vm::table::Table::new(vm); + #(#parsed)* + 1 + } + } + } + } + + fn gen_enum(self, parsed: Vec) -> TokenStream { + let name = self.0.name; + let generics = self.0.generics; + quote! { + unsafe impl #generics bp3d_lua::vm::function::IntoParam for #name #generics { + fn into_param(self, vm: &bp3d_lua::vm::Vm) -> i32 { + match self { + #(#parsed)* + } + } + } + } + } +} diff --git a/codegen/src/gen/lua_type.rs b/codegen/src/gen/lua_type.rs new file mode 100644 index 0000000..4aade3a --- /dev/null +++ b/codegen/src/gen/lua_type.rs @@ -0,0 +1,100 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::parser::enums::EnumVariant; +use crate::parser::structs::StructField; +use crate::parser::Parser; +use proc_macro2::{Ident, TokenStream}; +use quote::quote; +use syn::Generics; + +pub struct LuaType { + name: Ident, + generics: Generics, +} + +impl LuaType { + pub fn new(name: Ident, generics: Generics) -> Self { + Self { name, generics } + } +} + +impl Parser for LuaType { + type ParsedField = TokenStream; + type ParsedVariant = Vec; + + fn parse_field(&mut self, field: StructField) -> Self::ParsedField { + let ty = field.ty; + quote! { + types.append(&mut <#ty as bp3d_lua::vm::util::LuaType>::lua_type()); + } + } + + fn parse_variant(&mut self, field: EnumVariant) -> Self::ParsedVariant { + let mut tokens = Vec::new(); + match field { + EnumVariant::SingleField(v) => tokens.push(self.parse_field(v.field)), + EnumVariant::MultiField(v) => { + for v in v.fields { + tokens.push(self.parse_field(v)); + } + } + EnumVariant::None(_) => (), + } + tokens + } + + fn gen_struct(self, parsed: Vec) -> TokenStream { + let name = self.name; + let generics = self.generics; + quote! { + impl #generics bp3d_lua::vm::util::LuaType for #name #generics { + fn lua_type() -> Vec { + let mut types = Vec::new(); + #(#parsed)* + types + } + } + } + } + + fn gen_enum(self, parsed: Vec) -> TokenStream { + let name = self.name; + let generics = self.generics; + let tokens = parsed.into_iter().map(|v| quote! { #(#v)* }); + quote! { + impl #generics bp3d_lua::vm::util::LuaType for #name #generics { + fn lua_type() -> Vec { + let mut types = Vec::new(); + #(#tokens)* + types + } + } + } + } +} diff --git a/codegen/src/gen/mod.rs b/codegen/src/gen/mod.rs new file mode 100644 index 0000000..9f18191 --- /dev/null +++ b/codegen/src/gen/mod.rs @@ -0,0 +1,39 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//TODO: FromLua generator + +mod from_param; +mod into_lua; +mod into_param; +mod lua_type; + +pub use from_param::FromParam; +pub use into_lua::IntoLua; +pub use into_param::IntoParam; +pub use lua_type::LuaType; diff --git a/codegen/src/lib.rs b/codegen/src/lib.rs new file mode 100644 index 0000000..8221c41 --- /dev/null +++ b/codegen/src/lib.rs @@ -0,0 +1,101 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod gen; +mod parser; + +use crate::gen::{FromParam, IntoLua, IntoParam, LuaType}; +use crate::parser::Parser; +use proc_macro::TokenStream; +use proc_macro2::Ident; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +#[proc_macro_derive(FromParam)] +pub fn from_param(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + generics, + .. + } = parse_macro_input!(input); + FromParam::new(ident, generics).parse(data).into() +} + +#[proc_macro_derive(IntoParam)] +pub fn into_param(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + generics, + .. + } = parse_macro_input!(input); + IntoParam::new(ident, generics).parse(data).into() +} + +#[proc_macro_derive(IntoLua)] +pub fn into_lua(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + generics, + .. + } = parse_macro_input!(input); + IntoLua::new(ident, generics).parse(data).into() +} + +#[proc_macro_derive(LuaType)] +pub fn lua_type(input: TokenStream) -> TokenStream { + let DeriveInput { + ident, + data, + generics, + .. + } = parse_macro_input!(input); + LuaType::new(ident, generics).parse(data).into() +} + +#[proc_macro] +pub fn decl_lua_plugin(input: TokenStream) -> TokenStream { + let ident = parse_macro_input!(input as Ident); + let crate_name = std::env::var("CARGO_PKG_NAME").unwrap(); + let func_name = format!( + "bp3d_lua_{}_register_{}", + crate_name.replace("-", "_"), + ident + ); + let func = Ident::new(&func_name, ident.span()); + let q = quote! { + #[no_mangle] + extern "C" fn #func(l: bp3d_lua::ffi::lua::State, error: *mut bp3d_lua::module::error::Error) -> bool { + let vm = unsafe { bp3d_lua::vm::Vm::from_raw(l) }; + unsafe { bp3d_lua::module::run_lua_register(&vm, #ident, &mut *error) } + } + }; + q.into() +} diff --git a/codegen/src/parser/enums.rs b/codegen/src/parser/enums.rs new file mode 100644 index 0000000..94b564d --- /dev/null +++ b/codegen/src/parser/enums.rs @@ -0,0 +1,82 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::parser::structs::{StructField, StructParser}; +use proc_macro2::Ident; +use syn::{Fields, Variant}; + +pub struct EnumVariantSingle { + pub unique_name: Ident, + pub field: StructField, +} + +pub struct EnumVariantMulti { + pub unique_name: Ident, + pub fields: Vec, +} + +#[allow(clippy::large_enum_variant)] +pub enum EnumVariant { + SingleField(EnumVariantSingle), + MultiField(EnumVariantMulti), + None(Ident), +} + +pub struct EnumParser; + +impl EnumParser { + pub fn parse(&mut self, variant: Variant) -> EnumVariant { + let unique_name = variant.ident; + match variant.fields { + Fields::Named(v) => { + let mut parser = StructParser::new(); + let fields = v.named.into_iter().map(|v| parser.parse(v)); + EnumVariant::MultiField(EnumVariantMulti { + unique_name, + fields: fields.collect(), + }) + } + Fields::Unnamed(v) => { + let mut parser = StructParser::new(); + if v.unnamed.len() == 1 { + EnumVariant::SingleField(EnumVariantSingle { + unique_name, + field: parser.parse(v.unnamed.into_iter().next().unwrap()), + }) + } else { + let fields = v.unnamed.into_iter().map(|v| parser.parse(v)); + EnumVariant::MultiField(EnumVariantMulti { + unique_name, + fields: fields.collect(), + }) + } + } + Fields::Unit => EnumVariant::None(unique_name), + } + } +} diff --git a/codegen/src/parser/interface.rs b/codegen/src/parser/interface.rs new file mode 100644 index 0000000..673ef0f --- /dev/null +++ b/codegen/src/parser/interface.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::parser::enums::{EnumParser, EnumVariant}; +use crate::parser::structs::{StructField, StructParser}; +use proc_macro2::TokenStream; +use syn::Data; + +pub trait Parser: Sized { + type ParsedField; + type ParsedVariant; + + fn parse_field(&mut self, field: StructField) -> Self::ParsedField; + fn parse_variant(&mut self, variant: EnumVariant) -> Self::ParsedVariant; + + fn gen_struct(self, parsed: Vec) -> TokenStream; + fn gen_enum(self, parsed: Vec) -> TokenStream; + + fn parse(mut self, data: Data) -> TokenStream { + match data { + Data::Struct(v) => { + let mut parser = StructParser::new(); + let mut parsed = Vec::new(); + for v in v.fields { + parsed.push(self.parse_field(parser.parse(v))); + } + self.gen_struct(parsed) + } + Data::Enum(v) => { + let mut parser = EnumParser; + let mut parsed = Vec::new(); + for v in v.variants { + parsed.push(self.parse_variant(parser.parse(v))); + } + self.gen_enum(parsed) + } + _ => panic!("Unsupported type"), + } + } +} diff --git a/codegen/src/parser/mod.rs b/codegen/src/parser/mod.rs new file mode 100644 index 0000000..c1686d2 --- /dev/null +++ b/codegen/src/parser/mod.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod enums; +mod interface; +pub mod structs; + +pub use interface::*; diff --git a/codegen/src/parser/structs.rs b/codegen/src/parser/structs.rs new file mode 100644 index 0000000..ef52b82 --- /dev/null +++ b/codegen/src/parser/structs.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use proc_macro2::{Ident, Span}; +use syn::{Field, Type}; + +pub struct StructField { + pub unique_name: Ident, + pub ty: Type, + pub index: usize, + pub unique_name_is_index: bool, +} + +pub struct StructParser { + cur_index: usize, +} + +impl StructParser { + pub fn new() -> Self { + Self { cur_index: 0 } + } + + pub fn parse(&mut self, field: Field) -> StructField { + let index = Ident::new(&format!("value_{}", self.cur_index), Span::call_site()); + self.cur_index += 1; + let unique_name = field.ident.clone().unwrap_or(index); + StructField { + unique_name, + ty: field.ty, + index: self.cur_index - 1, + unique_name_is_index: field.ident.is_none(), + } + } +} diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 0000000..15d4391 --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "bp3d-lua" +version = "1.0.0-rc.5.0.0" +authors = ["Yuri Edward "] +edition = "2021" # Possible very-little performance improvement with 2024 (2.35 vs 2.37/2.4) +description = "Lua wrapper and base library for BP3D." +license = "BSD-3-Clause" +repository = "https://gitlab.com/bp3d/lua" +readme = "../README.MD" +keywords = ["lua"] +categories = ["graphics"] +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bp3d-util = { version = "2.2.0", features = ["simple-error", "format", "string"] } +bp3d-debug = "1.0.2" +bp3d-os = { version = "2.2.3", features = [], optional = true } +time = { version = "0.3.41", features = ["formatting"], optional = true } +itertools = { version = "0.14.0" } +bp3d-lua-codegen = { version = "1.0.0-rc.1.0.0", path = "../codegen", optional = true } + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.59.0", features = ["Win32_System_Threading", "Win32_System_Kernel", "Win32_System_Diagnostics", "Win32_System_Diagnostics_Debug"], optional = true } + +[target.'cfg(unix)'.dependencies] +libc = { version = "0.2.170", optional = true } + +[dev-dependencies] +bp3d-lua-codegen = { version = "1.0.0-rc.1.0.0", path = "../codegen" } +trybuild = "1.0.110" + +[build-dependencies] +bp3d-lua-build = { version = "1.0.0-rc.1.0.0", path = "../build" } +rustc_version = { version = "0.4.1", optional = true } + +[features] +interrupt = ["libc", "windows-sys", "root-vm"] +dynamic = [] +module = ["libs-core", "bp3d-lua-codegen", "rustc_version"] +root-vm = [] +util-method = [] +util-namespace = [] +util-thread = [] +util-module = ["module", "bp3d-os/module"] +libs-core = ["util-namespace"] +libs = ["libs-core", "time", "bp3d-os/time"] +send = [] diff --git a/core/build.rs b/core/build.rs new file mode 100644 index 0000000..3e39500 --- /dev/null +++ b/core/build.rs @@ -0,0 +1,112 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua_build::build::Lib; +use bp3d_lua_build::{BuildInfo, BuildInfoBase, Patch}; +use std::path::{Path, PathBuf}; + +#[cfg(feature = "dynamic")] +const DYNAMIC: bool = true; +#[cfg(not(feature = "dynamic"))] +const DYNAMIC: bool = false; + +const PATCH_LIST: &[&str] = &[ + "lib_init", // Disable unsafe/un-sandboxed libs. + "lj_disable_jit", // Disable global JIT state changes from Lua code. + "disable_lua_load", // Disable loadstring, dostring, etc from base lib. + "lua_ext", // Ext library such as lua_ext_tab_len, etc. + "lua_load_no_bc", // Treat all inputs as strings (no bytecode allowed). + "windows_set_lib_names", // Allow setting LJLIBNAME and LJDLLNAME from environment variables. + "lua_ext_ccatch_error", // Throw lua errors which cannot be catched from lua standard + // pcall/xpcall but only from lua_pcall C API. + "lua_ext_provenance", // lua_ext_getprovenance for registry key safety. +]; + +fn apply_patches(luajit_build_path: &Path, _summary_path: &Path) -> std::io::Result<()> { + let _summary = Patch::new( + &Path::new("..").join("patch"), + &Path::new("..").join("LuaJIT"), + )? + .apply_all(PATCH_LIST.iter().copied(), luajit_build_path)?; + #[cfg(feature = "libs")] + { + _summary.write(_summary_path)?; + println!( + "cargo:rustc-env=BP3D_LUA_PATCH_SUMMARY_FILE={}", + _summary_path.display() + ); + } + Ok(()) +} + +fn run_build(build_dir: &Path) -> std::io::Result { + let manifest = std::env::var_os("CARGO_MANIFEST_PATH") + .map(PathBuf::from) + .expect("Failed to read manifest path"); + let target_name = std::env::var("TARGET").expect("Failed to read build target"); + let base = BuildInfoBase { + dynamic: DYNAMIC, + target_name: &target_name, + build_dir, + manifest: &manifest, + }; + BuildInfo::new(base)?.build() +} + +fn main() { + // Rerun this script if any of the patch files changed. + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../patch"); + + // Create build directory. + let out = std::env::var_os("OUT_DIR").expect("Failed to acquire output directory"); + let out_path = Path::new(&out).join("luajit-build"); + + // Apply patches to LuaJIT source code. + apply_patches(&out_path, &Path::new(&out).join("patch_summary.rs")) + .expect("Failed to patch LuaJIT"); + + // Copy the source directory to the build directory. + println!("Internal LuaJIT build directory: {}", out_path.display()); + + // Build LuaJIT. + let lib = run_build(&out_path).expect("Failed to build LuaJIT"); + + // Attempt to setup linkage. + println!("cargo:rustc-link-search=native={}", lib.path.display()); + if lib.dynamic { + println!("cargo:rustc-link-lib=dylib={}", lib.name); + } else { + println!("cargo:rustc-link-lib=static={}", lib.name); + } + #[cfg(feature = "module")] + println!( + "cargo:rustc-env=RUSTC_VERSION={}", + rustc_version::version().unwrap() + ); +} diff --git a/core/src/ffi/ext.rs b/core/src/ffi/ext.rs new file mode 100644 index 0000000..c159845 --- /dev/null +++ b/core/src/ffi/ext.rs @@ -0,0 +1,88 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{RawInteger, RawNumber, State}; +use std::ffi::{c_int, c_void}; + +pub type MSize = u32; + +//--------------- +// Value reading +//--------------- +extern "C" { + pub fn lua_ext_fast_checknumber(l: State, numarg: c_int) -> RawNumber; + pub fn lua_ext_fast_checkinteger(l: State, numarg: c_int) -> RawInteger; + pub fn lua_ext_fast_checkboolean(l: State, numarg: c_int) -> c_int; +} + +//----------------- +// 64 bit integers +//----------------- +extern "C" { + pub fn lua_ext_checkinteger64(l: State, numarg: c_int) -> i64; + pub fn lua_ext_checkuinteger64(l: State, numarg: c_int) -> u64; + pub fn lua_ext_getinteger64(l: State, numarg: c_int, out: *mut i64) -> c_int; + pub fn lua_ext_getuinteger64(l: State, numarg: c_int, out: *mut u64) -> c_int; + pub fn lua_ext_pushinteger64(l: State, value: i64) -> c_int; + pub fn lua_ext_pushuinteger64(l: State, value: u64) -> c_int; + pub fn lua_ext_tointeger64(l: State, numarg: c_int) -> i64; + pub fn lua_ext_touinteger64(l: State, numarg: c_int) -> u64; +} + +//------- +// Other +//------- +extern "C" { + pub fn lua_ext_tab_len(l: State, idx: c_int, outsize: *mut MSize) -> c_int; + pub fn lua_ext_ccatch_error(l: State) -> u32; + + pub fn lua_ext_getprovenance(l: State) -> u64; +} + +//----- +// JIT +//----- +extern "C" { + /// Sets the global mode of the JIT. + pub fn lua_ext_setjitmode(l: State, mode: c_int) -> c_int; + + /// Returns global flags of the JIT. + pub fn lua_ext_getjitflags(l: State) -> u32; + + /// Sets global JIT flags. + pub fn lua_ext_setjitflags(l: State, flags: u32) -> c_int; +} + +//--------------------- +// Named registry keys +//--------------------- +extern "C" { + pub fn lua_ext_keyreg_get() -> *mut c_void; + pub fn lua_ext_keyreg_ref(ptr: *mut c_void) -> *mut c_void; + pub fn lua_ext_keyreg_unref() -> *mut c_void; +} diff --git a/core/src/ffi/jit.rs b/core/src/ffi/jit.rs new file mode 100644 index 0000000..9b4ac5c --- /dev/null +++ b/core/src/ffi/jit.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::ffi::c_int; + +pub const MODE_FLUSH: c_int = 0x0200; +pub const MODE_ON: c_int = 0x0100; +pub const MODE_OFF: c_int = 0x0000; + +pub const F_ON: u32 = 0x00000001; + +pub const F_CPU: u32 = 0x00000010; + +#[cfg(target_arch = "x86_64")] +mod x86_64 { + use crate::ffi::jit::F_CPU; + + pub const F_SSE3: u32 = F_CPU; + pub const F_SSE4_1: u32 = F_CPU << 1; + pub const F_BMI2: u32 = F_CPU << 2; +} + +#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] +mod arm { + use crate::ffi::jit::F_CPU; + + pub const F_ARMV6_: u32 = F_CPU; + pub const F_ARMV6T2_: u32 = F_CPU << 1; + pub const F_ARMV7: u32 = F_CPU << 2; + pub const F_ARMV8: u32 = F_CPU << 3; + pub const F_VFPV2: u32 = F_CPU << 4; + pub const F_VFPV3: u32 = F_CPU << 5; + + pub const F_ARMV6: u32 = F_ARMV6_ | F_ARMV6T2_ | F_ARMV7 | F_ARMV8; + pub const F_ARMV6T2: u32 = F_ARMV6T2_ | F_ARMV7 | F_ARMV8; + pub const F_VFP: u32 = F_VFPV2 | F_VFPV3; +} + +#[cfg(any(target_arch = "aarch64", target_arch = "arm"))] +pub use arm::*; + +#[cfg(target_arch = "x86_64")] +pub use x86_64::*; + +pub const F_OPT: u32 = 0x00010000; +pub const F_OPT_MASK: u32 = 0x0fff0000; + +pub const F_OPT_FOLD: u32 = F_OPT; +pub const F_OPT_CSE: u32 = F_OPT << 1; +pub const F_OPT_DCE: u32 = F_OPT << 2; +pub const F_OPT_FWD: u32 = F_OPT << 3; +pub const F_OPT_DSE: u32 = F_OPT << 4; +pub const F_OPT_NARROW: u32 = F_OPT << 5; +pub const F_OPT_LOOP: u32 = F_OPT << 6; +pub const F_OPT_ABC: u32 = F_OPT << 7; +pub const F_OPT_SINK: u32 = F_OPT << 8; +pub const F_OPT_FUSE: u32 = F_OPT << 9; +pub const F_OPT_FMA: u32 = F_OPT << 10; + +pub const F_OPT_0: u32 = 0; +pub const F_OPT_1: u32 = F_OPT_FOLD | F_OPT_CSE | F_OPT_DCE; +pub const F_OPT_2: u32 = F_OPT_1 | F_OPT_NARROW | F_OPT_LOOP; +pub const F_OPT_3: u32 = F_OPT_2 | F_OPT_FWD | F_OPT_DSE | F_OPT_ABC | F_OPT_SINK | F_OPT_FUSE; +pub const F_OPT_DEFAULT: u32 = F_OPT_3; diff --git a/core/src/ffi/laux.rs b/core/src/ffi/laux.rs new file mode 100644 index 0000000..c8aee89 --- /dev/null +++ b/core/src/ffi/laux.rs @@ -0,0 +1,124 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{RawInteger, RawNumber, State, ThreadStatus, Type}; +use std::ffi::{c_char, c_int, c_void}; + +//-------------------- +// State manipulation +//-------------------- +extern "C" { + pub fn luaL_newstate() -> State; + pub fn luaL_openlibs(l: State); +} + +//---------------- +// Error handling +//---------------- +extern "C" { + pub fn luaL_error(l: State, fmt: *const c_char, ...) -> c_int; + pub fn luaL_typerror(l: State, narg: c_int, tname: *const c_char) -> c_int; + pub fn luaL_argerror(l: State, numarg: c_int, extramsg: *const c_char) -> c_int; + + pub fn luaL_traceback(l: State, l1: State, msg: *const c_char, level: c_int); +} + +//--------------- +// Value reading +//--------------- +extern "C" { + pub fn luaL_checklstring(l: State, numarg: c_int, len: *mut usize) -> *const c_char; + pub fn luaL_optlstring( + l: State, + numarg: c_int, + def: *const c_char, + len: *mut usize, + ) -> *const c_char; + + pub fn luaL_checknumber(l: State, numarg: c_int) -> RawNumber; + pub fn luaL_optnumber(l: State, narg: c_int, def: RawNumber) -> RawNumber; + + pub fn luaL_checkinteger(l: State, numarg: c_int) -> RawInteger; + pub fn luaL_optinteger(l: State, narg: c_int, def: RawInteger) -> RawInteger; + + pub fn luaL_checkstack(l: State, sz: c_int, msg: *const c_char); + pub fn luaL_checktype(l: State, narg: c_int, t: Type); + pub fn luaL_checkany(l: State, narg: c_int); + + pub fn luaL_checkudata(l: State, ud: c_int, tname: *const c_char) -> *mut c_void; + pub fn luaL_testudata(l: State, ud: c_int, tname: *const c_char) -> *mut c_void; +} + +//------------------------ +// Metatable manipulation +//------------------------ +extern "C" { + pub fn luaL_newmetatable(l: State, tname: *const c_char) -> c_int; + pub fn luaL_setmetatable(l: State, tname: *const c_char); + pub fn luaL_getmetafield(l: State, obj: c_int, e: *const c_char) -> c_int; + pub fn luaL_callmeta(l: State, obj: c_int, e: *const c_char) -> c_int; +} + +//------------------------- +// Miscellaneous functions +//------------------------- +extern "C" { + pub fn luaL_where(l: State, lvl: c_int); + + pub fn luaL_checkoption( + l: State, + narg: c_int, + def: *const c_char, + lst: *const *const c_char, + ) -> c_int; +} + +//---------- +// Registry +//---------- +pub const NOREF: c_int = -2; +pub const REFNIL: c_int = -1; + +extern "C" { + pub fn luaL_ref(l: State, t: c_int) -> c_int; + pub fn luaL_unref(l: State, t: c_int, r: c_int); +} + +//------------------ +// Loading lua code +//------------------ +extern "C" { + pub fn luaL_loadfile(l: State, filename: *const c_char) -> ThreadStatus; + pub fn luaL_loadbuffer( + l: State, + buff: *const c_char, + sz: usize, + name: *const c_char, + ) -> ThreadStatus; + pub fn luaL_loadstring(l: State, s: *const c_char) -> ThreadStatus; +} diff --git a/core/src/ffi/lua.rs b/core/src/ffi/lua.rs new file mode 100644 index 0000000..a0ba236 --- /dev/null +++ b/core/src/ffi/lua.rs @@ -0,0 +1,257 @@ +// Copyright (c) 2026, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::ffi::{c_char, c_double, c_int, c_void}; + +/// The maximum size of a lua chunkname. This is used by Vm::run_named_code for optimization. +pub const IDSIZE: usize = 60; + +pub const REGISTRYINDEX: c_int = -10000; +pub const ENVIRONINDEX: c_int = -10001; +pub const GLOBALSINDEX: c_int = -10002; + +pub const fn upvalueindex(i: c_int) -> c_int { + GLOBALSINDEX - i +} + +#[repr(i32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ThreadStatus { + Ok = 0, + Yield = 1, + ErrRun = 2, + ErrSyntax = 3, + ErrMem = 4, + ErrErr = 5, + ErrCcatch = 6, +} + +#[repr(transparent)] +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct State(*mut c_void); + +pub type CFunction = extern "C-unwind" fn(l: State) -> i32; + +pub type Reader = extern "C-unwind" fn(l: State, ud: *mut c_void, sz: *mut usize) -> *const c_char; + +pub type Writer = extern "C" fn(l: State, p: *const c_void, sz: usize, ud: *mut c_void) -> c_int; + +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Type { + None = -1, + Nil = 0, + Boolean = 1, + LightUserdata = 2, + Number = 3, + String = 4, + Table = 5, + Function = 6, + Userdata = 7, + Thread = 8, + Cdata = 10, +} + +pub type RawNumber = c_double; +pub type RawInteger = isize; + +//-------------------- +// State manipulation +//-------------------- +extern "C" { + pub fn lua_close(l: State); + + pub fn lua_atpanic(l: State, panicf: CFunction) -> CFunction; + + pub fn lua_newthread(l: State) -> State; +} + +//-------------------------- +// Basic stack manipulation +//-------------------------- +extern "C" { + pub fn lua_gettop(l: State) -> c_int; + pub fn lua_settop(l: State, idx: c_int); + pub fn lua_pushvalue(l: State, idx: c_int); + pub fn lua_remove(l: State, idx: c_int); + pub fn lua_insert(l: State, idx: c_int); + pub fn lua_replace(l: State, idx: c_int); + pub fn lua_checkstack(l: State, sz: c_int) -> c_int; + + pub fn lua_xmove(from: State, to: State, n: c_int); +} + +//------------------------------- +// Access functions (stack -> C) +//------------------------------- +extern "C" { + pub fn lua_isnumber(l: State, idx: c_int) -> c_int; + pub fn lua_isstring(l: State, idx: c_int) -> c_int; + pub fn lua_iscfunction(l: State, idx: c_int) -> c_int; + pub fn lua_isuserdata(l: State, idx: c_int) -> c_int; + pub fn lua_type(l: State, idx: c_int) -> Type; + pub fn lua_typename(l: State, tp: c_int) -> *const c_char; + + pub fn lua_equal(l: State, idx1: c_int, idx2: c_int) -> c_int; + pub fn lua_rawequal(l: State, idx1: c_int, idx2: c_int) -> c_int; + pub fn lua_lessthan(l: State, idx1: c_int, idx2: c_int) -> c_int; + + pub fn lua_tonumber(l: State, idx: c_int) -> RawNumber; + pub fn lua_tonumberx(l: State, idx: c_int, ok: *mut c_int) -> RawNumber; + pub fn lua_tointeger(l: State, idx: c_int) -> RawInteger; + pub fn lua_tointegerx(l: State, idx: c_int, ok: *mut c_int) -> RawInteger; + pub fn lua_toboolean(l: State, idx: c_int) -> c_int; + pub fn lua_tolstring(l: State, idx: c_int, len: *mut usize) -> *const c_char; + pub fn lua_objlen(l: State, idx: c_int) -> usize; + pub fn lua_tocfunction(l: State, idx: c_int) -> CFunction; + pub fn lua_touserdata(l: State, idx: c_int) -> *mut c_void; + pub fn lua_tothread(l: State, idx: c_int) -> State; + pub fn lua_topointer(l: State, idx: c_int) -> *const c_void; +} + +//------------------------------- +// Push functions (C -> stack) +//------------------------------- +extern "C" { + pub fn lua_pushnil(l: State); + pub fn lua_pushnumber(l: State, n: RawNumber); + pub fn lua_pushinteger(l: State, n: RawInteger); + pub fn lua_pushlstring(l: State, s: *const c_char, len: usize); + pub fn lua_pushstring(l: State, s: *const c_char); + //LUA_API const char *(lua_pushvfstring) (lua_State *L, const char *fmt, va_list argp); + pub fn lua_pushfstring(l: State, fmt: *const c_char, ...) -> *const c_char; + pub fn lua_pushcclosure(l: State, fun: CFunction, n: c_int); + pub fn lua_pushboolean(l: State, b: c_int); + pub fn lua_pushlightuserdata(l: State, p: *mut c_void); + pub fn lua_pushthread(l: State); +} + +//------------------------------- +// Get functions (Lua -> stack) +//------------------------------- +extern "C" { + pub fn lua_gettable(l: State, idx: c_int); + pub fn lua_getfield(l: State, idx: c_int, k: *const c_char); + pub fn lua_rawget(l: State, idx: c_int); + pub fn lua_rawgeti(l: State, idx: c_int, n: c_int); + pub fn lua_createtable(l: State, narr: c_int, nrec: c_int); + pub fn lua_newuserdata(l: State, sz: usize) -> *mut c_void; + pub fn lua_getmetatable(l: State, objindex: c_int) -> c_int; + pub fn lua_getfenv(l: State, idx: c_int); +} + +//------------------------------- +// Set functions (stack -> Lua) +//------------------------------- +extern "C" { + pub fn lua_settable(l: State, idx: c_int); + pub fn lua_setfield(l: State, idx: c_int, k: *const c_char); + pub fn lua_rawset(l: State, idx: c_int); + pub fn lua_rawseti(l: State, idx: c_int, n: c_int); + pub fn lua_setmetatable(l: State, objindex: c_int) -> c_int; + pub fn lua_setfenv(l: State, idx: c_int) -> c_int; +} + +//----------------------------------------------------- +// `load' and `call' functions (load and run Lua code) +//----------------------------------------------------- +extern "C" { + pub fn lua_call(l: State, nargs: c_int, nresults: c_int); + pub fn lua_pcall(l: State, nargs: c_int, nresults: c_int, errfunc: c_int) -> ThreadStatus; + pub fn lua_cpcall(l: State, func: CFunction, ud: *mut c_void) -> c_int; + pub fn lua_load( + l: State, + reader: Reader, + dt: *mut c_void, + chunkname: *const c_char + ) -> ThreadStatus; + + pub fn lua_loadx( + l: State, + reader: Reader, + dt: *mut c_void, + chunkname: *const c_char, + mode: *const c_char + ) -> ThreadStatus; + + pub fn lua_dump(l: State, writer: Writer, data: *mut c_void) -> c_int; +} + +//--------------------- +// Coroutine functions +//--------------------- +extern "C" { + pub fn lua_isyieldable(l: State) -> c_int; + pub fn lua_yield(l: State, nresults: c_int) -> c_int; + pub fn lua_resume(l: State, narg: c_int) -> ThreadStatus; + pub fn lua_status(l: State) -> ThreadStatus; +} + +//----------------------------------------- +// Garbage-collection function and options +//----------------------------------------- +#[repr(i32)] +pub enum Gc { + Stop = 0, + Restart = 1, + Collect = 2, + Count = 3, + Countb = 4, + Step = 5, + SetPause = 6, + SetStepMul = 7, + IsRunning = 9, +} + +extern "C" { + pub fn lua_gc(l: State, what: Gc, data: c_int) -> c_int; +} + +//------------------------- +// Miscellaneous functions +//------------------------- +pub type Debug = *mut c_void; +pub type Hook = extern "C-unwind" fn(l: State, debug: Debug); + +const HOOKCALL: c_int = 0; +const HOOKRET: c_int = 1; +const HOOKLINE: c_int = 2; +const HOOKCOUNT: c_int = 3; + +pub const MASKCALL: c_int = 1 << HOOKCALL; +pub const MASKRET: c_int = 1 << HOOKRET; +pub const MASKLINE: c_int = 1 << HOOKLINE; +pub const MASKCOUNT: c_int = 1 << HOOKCOUNT; + +extern "C" { + pub fn lua_error(l: State) -> c_int; + pub fn lua_next(l: State, idx: c_int) -> c_int; + pub fn lua_concat(l: State, n: c_int); + + pub fn lua_sethook(l: State, hook: Option, mask: c_int, count: c_int) -> c_int; +} diff --git a/core/src/ffi/mod.rs b/core/src/ffi/mod.rs new file mode 100644 index 0000000..e0b9bbd --- /dev/null +++ b/core/src/ffi/mod.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod ext; +pub mod jit; +pub mod laux; +pub mod lua; diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 0000000..2f1ff68 --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,36 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod ffi; +pub mod libs; +mod r#macro; +pub mod util; +pub mod vm; + +#[cfg(feature = "module")] +pub mod module; diff --git a/core/src/libs/files/chroot.rs b/core/src/libs/files/chroot.rs new file mode 100644 index 0000000..8032014 --- /dev/null +++ b/core/src/libs/files/chroot.rs @@ -0,0 +1,225 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::core::destructor::Pool; +use crate::vm::registry::named::Key; +use crate::vm::registry::types::LuaRef; +use crate::vm::value::types::RawPtr; +use crate::vm::Vm; +use bp3d_debug::trace; +use std::borrow::Cow; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fmt::Display; +use std::ops::{BitAnd, BitOr}; +use std::path::Path; + +const CHROOT: Key> = Key::new("__chroot__"); + +pub fn with_chroot(vm: &Vm, f: impl FnOnce(&Path) -> R) -> R { + let s = CHROOT.push(vm); + match s { + None => f(Path::new("")), + Some(v) => { + let p = Path::new(unsafe { OsStr::from_encoded_bytes_unchecked(v.get()) }); + f(p) + } + } +} + +pub fn set_chroot(vm: &Vm, path: &Path) { + let mut bytes = path.as_os_str().as_encoded_bytes(); + if bytes[bytes.len() - 1] == b'/' { + bytes = &bytes[..bytes.len() - 1]; + } + let r = crate::vm::registry::lua_ref::LuaRef::new(vm, bytes); + CHROOT.set(r); +} + +#[derive(Debug)] +pub struct SandboxError; + +impl Display for SandboxError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "attempt to escape the sandbox") + } +} + +impl std::error::Error for SandboxError {} + +pub fn is_escaping(path: &str) -> bool { + let mut level = 0; + for component in path.split('/') { + if component == ".." { + if level == 0 { + return true; + } + level -= 2; + } else if component == "." || component == "" { + } else { + level += 1; + } + } + trace!({ level }, "unsandbox {}", path); + level < 0 +} + +pub fn unsandbox<'a>(vm: &Vm, path: &'a str) -> Result, SandboxError> { + if is_escaping(path) { + return Err(SandboxError); + } + if path.len() > 0 && path.as_bytes()[0] == b'/' { + return Ok(Cow::Owned(with_chroot(vm, |root| root.join(&path[1..])))); + } + Ok(Cow::Borrowed(Path::new(path))) +} + +pub fn sandbox<'a>(vm: &Vm, path: &'a Path) -> Result, SandboxError> { + let pos = with_chroot(vm, |root| { + let src = path.as_os_str().as_encoded_bytes(); + let root = root.as_os_str().as_encoded_bytes(); + if !(src.len() >= root.len()) || !src.starts_with(root) { + return 0; + } + root.len() + }); + if pos == 0 { + return Err(SandboxError); + } + #[cfg(windows)] + let src = &path.as_os_str().as_encoded_bytes()[pos..]; + #[cfg(unix)] + let mut src = &path.as_os_str().as_encoded_bytes()[pos..]; + if src.len() == 0 { + return Ok(Cow::Borrowed("/")); + } + #[cfg(windows)] + { + let src = String::from_utf8_lossy(src).replace("\\", "/"); + if src.as_bytes()[0] != b'/' { + let src = &src[pos - 1..]; + return Ok(Cow::Owned(src.into())); + } else { + return Ok(Cow::Owned(src)); + } + } + #[cfg(unix)] + { + if src[0] != b'/' { + src = &path.as_os_str().as_encoded_bytes()[pos - 1..]; + } + return Ok(String::from_utf8_lossy(src)); + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct Permissions(u8); + +impl Permissions { + pub const R: Permissions = Permissions(0x1); + pub const W: Permissions = Permissions(0x2); + pub const X: Permissions = Permissions(0x4); + + pub const NONE: Permissions = Permissions(0x0); +} + +impl BitOr for Permissions { + type Output = Permissions; + + fn bitor(self, rhs: Self) -> Self::Output { + Permissions(self.0 | rhs.0) + } +} + +impl BitAnd for Permissions { + type Output = bool; + + fn bitand(self, rhs: Self) -> Self::Output { + self.0 & rhs.0 != 0 + } +} + +const PERMISSIONS: Key>> = Key::new("__permissions__"); + +#[derive(Default)] +struct PermissionManager { + permissions: HashMap, +} + +impl PermissionManager { + fn set_permissions(&mut self, path: &str, perms: Permissions) { + self.permissions.insert(path.into(), perms); + } + + fn get_permissions(&self, path: &str) -> Option { + self.permissions.get(path).cloned() + } + + fn set_permissions_vm(vm: &Vm, path: &str, permissions: Permissions) { + let mut value = PERMISSIONS.push(vm); + if value.is_none() { + let ptr = Pool::attach_send(vm, Box::new(PermissionManager::default())); + let r = crate::vm::registry::lua_ref::LuaRef::new(vm, RawPtr::new(ptr)); + PERMISSIONS.set(r); + value = PERMISSIONS.push(vm); + } + let value = value.unwrap(); + unsafe { (&mut *value.get().as_mut_ptr()).set_permissions(path, permissions) }; + } +} + +pub fn set_access(vm: &Vm, path: &str, perms: Permissions) { + if !path.starts_with("/") { + return; + } + PermissionManager::set_permissions_vm(vm, path, perms); +} + +pub fn access(vm: &Vm, path: &str) -> Permissions { + if !path.starts_with("/") { + return Permissions::NONE; + } + let perms = PERMISSIONS.push(vm); + if perms.is_none() { + return Permissions::NONE; + } + let perms = perms.unwrap().get(); + let perms = unsafe { &*perms.as_ptr() }; + let mut path = path; + while path.len() > 0 { + if let Some(perms) = perms.get_permissions(path) { + return perms; + } + let id = path.rfind('/'); + match id { + Some(pos) => path = &path[..pos], + None => break, + } + } + perms.get_permissions("/").unwrap_or(Permissions::NONE) +} diff --git a/core/src/libs/files/file.rs b/core/src/libs/files/file.rs new file mode 100644 index 0000000..c56c7bc --- /dev/null +++ b/core/src/libs/files/file.rs @@ -0,0 +1,150 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::libs::files::chroot::Permissions; +use crate::libs::files::SandboxPath; +use crate::vm::value::types::UInt53; +use crate::{decl_lib_func, decl_userdata, impl_userdata_mut}; +use bp3d_util::simple_error; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Seek, Write}; + +const MAX_BUF_SIZE: usize = 65535; + +simple_error! { + Error { + Io(std::io::Error) => "io error: {}", + TooLarge(usize) => "internal buffer overflow ({})", + Sandbox => "sandbox error", + Permission => "permission denied" + } +} + +decl_userdata! { + pub struct FileWrapper { + file: File, + is_ro: bool, + buffer: [u8; MAX_BUF_SIZE] + } +} + +decl_lib_func! { + fn open(vm: &Vm, path: SandboxPath, mode: &str) -> Result { + let perms = path.access(vm); + let mut opts = OpenOptions::new(); + let mut is_ro = true; + if mode.contains("r") { + if !(perms & Permissions::R) { + return Err(Error::Permission); + } + opts.read(true); + } + if mode.contains("w") { + if !(perms & Permissions::W) { + return Err(Error::Permission); + } + is_ro = false; + opts.write(true); + } + if mode.contains("a") { + if !(perms & Permissions::W) || !(perms & Permissions::R) { + return Err(Error::Permission); + } + is_ro = false; + opts.append(true); + } + let path = path.to_path(vm).map_err(|_| Error::Sandbox)?; + let file = opts.open(path).map_err(Error::Io)?; + Ok(FileWrapper { + file, + is_ro, + buffer: [0; MAX_BUF_SIZE] + }) + } +} + +decl_lib_func! { + fn create(vm: &Vm, path: SandboxPath) -> Result { + let perms = path.access(vm); + if !(perms & Permissions::W) { + return Err(Error::Permission); + } + if !(perms & Permissions::R) { + return Err(Error::Permission); + } + let path = path.to_path(vm).map_err(|_| Error::Sandbox)?; + let file = File::create(path).map_err(Error::Io)?; + Ok(FileWrapper { + file, + is_ro: false, + buffer: [0; MAX_BUF_SIZE] + }) + } +} + +impl_userdata_mut! { + impl FileWrapper { + fn read(this: &mut FileWrapper, size: UInt53) -> Result, Error> { + if size.to_usize() >= MAX_BUF_SIZE { + return Err(Error::TooLarge(size.to_usize())); + } + let len = this.file.read(&mut this.buffer[..size.to_usize()]).map_err(Error::Io)?; + if len == 0 { + return Ok(None); + } + Ok(Some(&this.buffer[..len])) + } + + fn write(this: &mut FileWrapper, buf: &[u8]) -> Result { + if this.is_ro { + return Err(Error::Permission); + } + this.file.write(buf).map(UInt53::from_usize_lossy).map_err(Error::Io) + } + + fn seek_from_start(this: &mut FileWrapper, pos: u64) -> std::io::Result { + this.file.seek(std::io::SeekFrom::Start(pos)) + } + + fn seek_from_end(this: &mut FileWrapper, pos: i64) -> std::io::Result { + this.file.seek(std::io::SeekFrom::End(pos)) + } + + fn seek_from_cursor(this: &mut FileWrapper, pos: i64) -> std::io::Result { + this.file.seek(std::io::SeekFrom::Current(pos)) + } + + fn size(this: &FileWrapper) -> std::io::Result { + this.file.metadata().map(|m| m.len()) + } + } + static { + [fn open]; + [fn create]; + } +} diff --git a/core/src/libs/files/interface.rs b/core/src/libs/files/interface.rs new file mode 100644 index 0000000..f9165d0 --- /dev/null +++ b/core/src/libs/files/interface.rs @@ -0,0 +1,265 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::libs::files::chroot::{access, sandbox, unsandbox, Permissions, SandboxError}; +use crate::libs::files::path::PathWrapper; +use crate::util::core::SimpleDrop; +use crate::vm::function::{FromParam, IntoParam}; +use crate::vm::util::LuaType; +use crate::vm::value::{FromLua, IntoLua}; +use crate::vm::Vm; +use bp3d_debug::error; +use std::borrow::Cow; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Eq, PartialEq)] +pub enum SandboxPath<'a> { + String(&'a str), + Path(&'a Path), +} + +#[derive(Debug, Eq, PartialEq)] +pub enum SandboxPathBuf { + String(String), + Path(PathBuf), +} + +impl From> for SandboxPathBuf { + fn from(value: SandboxPath<'_>) -> Self { + match value { + SandboxPath::String(v) => SandboxPathBuf::String(v.into()), + SandboxPath::Path(v) => SandboxPathBuf::Path(v.into()), + } + } +} + +impl SandboxPathBuf { + /// Creates a new [SandboxPathBuf] from an existing string. + pub fn from_str(vm: &Vm, path: String) -> Result { + unsandbox(vm, &path)?; + Ok(SandboxPathBuf::String(path)) + } + + /// Creates a new [SandboxPathBuf] from an existing string. + /// + /// If the given string cannot be represented in the current sandbox configuration, a nil value + /// will be passed to Lua. + pub fn from_str_unchecked(path: String) -> SandboxPathBuf { + SandboxPathBuf::String(path) + } + + /// Creates a new [SandboxPathBuf] from an existing [PathBuf]. + pub fn from_path<'a>(vm: &Vm, path: PathBuf) -> Result { + sandbox(vm, &path)?; + Ok(SandboxPathBuf::Path(path)) + } + + /// Creates a new [SandboxPathBuf] from an existing [PathBuf]. + /// + /// This function allows passing paths from Rust to Lua which are outside the sandbox. + /// Use with caution. + pub fn from_path_unchecked(path: PathBuf) -> SandboxPathBuf { + SandboxPathBuf::Path(path) + } + + pub fn as_path(&self) -> SandboxPath<'_> { + match self { + SandboxPathBuf::String(v) => SandboxPath::String(v), + SandboxPathBuf::Path(v) => SandboxPath::Path(v), + } + } + + /// Returns the underlying path as raw [OsStr]. This function does not interpret the path + /// according to the current sandbox configuration. + pub fn as_os_str(&self) -> &OsStr { + match self { + SandboxPathBuf::String(v) => v.as_ref(), + SandboxPathBuf::Path(v) => v.as_os_str(), + } + } +} + +impl SandboxPath<'_> { + /// Creates a new [SandboxPath] from an existing string. + pub fn from_str<'a>(vm: &Vm, path: &'a str) -> Result, SandboxError> { + unsandbox(vm, path)?; + Ok(SandboxPath::String(path)) + } + + /// Creates a new [SandboxPath] from an existing string. + /// + /// If the given string cannot be represented in the current sandbox configuration, a nil value + /// will be passed to Lua. + pub fn from_str_unchecked(path: &str) -> SandboxPath<'_> { + SandboxPath::String(path) + } + + /// Creates a new [SandboxPath] from an existing [Path]. + pub fn from_path<'a>(vm: &Vm, path: &'a Path) -> Result, SandboxError> { + sandbox(vm, path)?; + Ok(SandboxPath::Path(path)) + } + + /// Creates a new [SandboxPath] from an existing [Path]. + /// + /// This function allows passing paths from Rust to Lua which are outside the sandbox. + /// Use with caution. + pub fn from_path_unchecked(path: &Path) -> SandboxPath<'_> { + SandboxPath::Path(path) + } + + /// Returns the underlying path as raw [OsStr]. This function does not interpret the path + /// according to the current sandbox configuration. + pub fn as_os_str(&self) -> &OsStr { + match self { + SandboxPath::String(v) => v.as_ref(), + SandboxPath::Path(v) => v.as_os_str(), + } + } + + pub fn to_str(&self, vm: &Vm) -> Result, SandboxError> { + match self { + SandboxPath::String(v) => { + unsandbox(vm, v)?; + Ok(Cow::Borrowed(v)) + } + SandboxPath::Path(v) => sandbox(vm, v), + } + } + + pub fn to_path(&self, vm: &Vm) -> Result, SandboxError> { + match self { + SandboxPath::String(v) => unsandbox(vm, *v), + SandboxPath::Path(v) => Ok(Cow::Borrowed(v)), + } + } + + pub fn access(&self, vm: &Vm) -> Permissions { + let path = match self.to_str(vm) { + Ok(v) => v, + Err(e) => { + error!("failed to read permissions for {:?}: {}", self, e); + return Permissions::NONE; + } + }; + access(vm, &path) + } + + pub fn is_relative(&self) -> bool { + match self { + SandboxPath::String(v) => !v.starts_with("/"), + SandboxPath::Path(v) => !v.starts_with("/"), + } + } +} + +impl<'a> FromLua<'a> for SandboxPath<'a> { + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + let wrapper: crate::vm::Result<&PathWrapper> = FromLua::from_lua(vm, index); + if let Ok(wrapper) = wrapper { + return SandboxPath::Path(wrapper.path()); + } + let s: &str = FromLua::from_lua_unchecked(vm, index); + SandboxPath::String(s) + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let wrapper: crate::vm::Result<&PathWrapper> = FromLua::from_lua(vm, index); + if let Ok(wrapper) = wrapper { + return Ok(SandboxPath::Path(wrapper.path())); + } + let s: &str = FromLua::from_lua(vm, index)?; + Ok(SandboxPath::String(s)) + } +} + +impl FromLua<'_> for SandboxPathBuf { + unsafe fn from_lua_unchecked(vm: &'_ Vm, index: i32) -> Self { + SandboxPath::from_lua_unchecked(vm, index).into() + } + + fn from_lua(vm: &'_ Vm, index: i32) -> crate::vm::Result { + SandboxPath::from_lua(vm, index).map(SandboxPathBuf::from) + } +} + +unsafe impl IntoLua for SandboxPath<'_> { + fn into_lua(self, vm: &Vm) -> u16 { + match self { + SandboxPath::String(v) => unsandbox(vm, v) + .map(|path| PathWrapper::new(path.into())) + .ok() + .into_lua(vm), + SandboxPath::Path(v) => PathWrapper::new(v.into()).into_lua(vm), + } + } +} + +unsafe impl SimpleDrop for SandboxPath<'_> {} + +impl LuaType for SandboxPath<'_> {} + +impl<'a> FromParam<'a> for SandboxPath<'a> { + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + let wrapper: Option<&PathWrapper> = FromParam::try_from_param(vm, index); + if let Some(wrapper) = wrapper { + return SandboxPath::Path(wrapper.path()); + } + let s: &str = FromParam::from_param(vm, index); + SandboxPath::String(s) + } + + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + let wrapper: Option<&PathWrapper> = FromParam::try_from_param(vm, index); + if let Some(wrapper) = wrapper { + return Some(SandboxPath::Path(wrapper.path())); + } + let wrapper: Option<&str> = FromParam::try_from_param(vm, index); + wrapper.map(|v| SandboxPath::String(v)) + } +} + +unsafe impl IntoParam for SandboxPath<'_> { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self, vm) as _ + } +} + +unsafe impl IntoParam for SandboxPathBuf { + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self.as_path(), vm) as _ + } +} + +unsafe impl IntoLua for SandboxPathBuf { + fn into_lua(self, vm: &Vm) -> u16 { + IntoLua::into_lua(self.as_path(), vm) + } +} diff --git a/core/src/libs/files/lib.rs b/core/src/libs/files/lib.rs new file mode 100644 index 0000000..1e4dbd2 --- /dev/null +++ b/core/src/libs/files/lib.rs @@ -0,0 +1,232 @@ +// Copyright (c) 2026, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::libs::files::chroot::{access, sandbox, Permissions}; +use crate::libs::files::SandboxPath; +use crate::vm::table::Table; +use bp3d_util::simple_error; +use std::fs::File; +use std::io::{ErrorKind, Read}; + +const MAX_FILE_SIZE: usize = 5000000; //5Mb + +simple_error! { + Error { + Io(std::io::Error) => "io error: {}", + Sandbox => "sandbox error", + TooLarge(usize) => "file is too large ({})", + Memory => "memory error", + Permission => "permission denied", + Unsupported => "unsupported operation" + } +} + +decl_lib_func! { + pub fn read_text(vm: &Vm, path: SandboxPath) -> Result { + if !(path.access(vm) & Permissions::R) { + return Err(Error::Permission); + } + let path = path.to_path(vm).map_err(|_| Error::Sandbox)?; + let mut file = File::open(path).map_err(Error::Io)?; + let size = file.metadata().map(|m| m.len() as usize).ok().unwrap_or(usize::MAX); + if size > MAX_FILE_SIZE { + return Err(Error::TooLarge(size)); + } + let mut s = String::new(); + s.try_reserve_exact(size).map_err(|_| Error::Memory)?; + file.read_to_string(&mut s).map_err(Error::Io)?; + Ok(s) + } +} + +decl_lib_func! { + pub fn write_text(vm: &Vm, path: SandboxPath, data: &str) -> Result<(), Error> { + if !(path.access(vm) & Permissions::W) { + return Err(Error::Permission); + } + let path = path.to_path(vm).map_err(|_| Error::Sandbox)?; + std::fs::write(path, data).map_err(Error::Io) + } +} + +decl_lib_func! { + pub fn copy_file(vm: &Vm, src_path: SandboxPath, dst_path: SandboxPath) -> Result<(), Error> { + if !(src_path.access(vm) & Permissions::R) { + return Err(Error::Permission); + } + if !(dst_path.access(vm) & Permissions::W) { + return Err(Error::Permission); + } + let src_path = src_path.to_path(vm).map_err(|_| Error::Sandbox)?; + let dst_path = dst_path.to_path(vm).map_err(|_| Error::Sandbox)?; + if let Some(parent) = dst_path.parent() { + std::fs::create_dir_all(parent).map_err(Error::Io)?; + } + std::fs::copy(src_path, dst_path).map(|_| ()).map_err(Error::Io) + } +} + +decl_lib_func! { + pub fn symlink(vm: &Vm, src_path: SandboxPath, dst_path: SandboxPath) -> Result<(), Error> { + #[cfg(unix)] + { + if src_path.is_relative() { + let first_part = dst_path.to_path(vm).map_err(|_| Error::Sandbox)?; + let path = first_part.join(src_path.as_os_str()); + let path = sandbox(vm, &path).map_err(|_| Error::Sandbox)?; + if !(access(vm, &path) & Permissions::R) { + return Err(Error::Permission); + } + } else if !(src_path.access(vm) & Permissions::R) { + return Err(Error::Permission); + } + if !(dst_path.access(vm) & Permissions::W) { + return Err(Error::Permission); + } + let src_path = src_path.to_path(vm).map_err(|_| Error::Sandbox)?; + let dst_path = dst_path.to_path(vm).map_err(|_| Error::Sandbox)?; + return std::os::unix::fs::symlink(src_path, dst_path).map(|_| ()).map_err(Error::Io); + } + #[cfg(windows)] + return Err(Error::Unsupported); + } +} + +decl_lib_func! { + pub fn delete_dir(vm: &Vm, path: SandboxPath) -> Result<(), Error> { + if !(path.access(vm) & Permissions::W) { + return Err(Error::Permission); + } + let path = path.to_path(vm).map_err(|_| Error::Sandbox)?; + std::fs::remove_dir_all(path).map_err(Error::Io) + } +} + +decl_lib_func! { + pub fn create_dir(vm: &Vm, path: SandboxPath) -> Result<(), Error> { + if !(path.access(vm) & Permissions::W) { + return Err(Error::Permission); + } + let path = path.to_path(vm).map_err(|_| Error::Sandbox)?; + std::fs::create_dir_all(path).map_err(Error::Io) + } +} + +decl_lib_func! { + pub fn exists(vm: &Vm, path: SandboxPath) -> bool { + path.to_path(vm).map(|v| v.exists()).unwrap_or(false) + } +} + +decl_lib_func! { + pub fn list<'a>(vm: &Vm, path: SandboxPath) -> Result, Error> { + if !(path.access(vm) & Permissions::R) { + return Err(Error::Permission); + } + let path = path.to_path(vm).map_err(|_| Error::Sandbox)?; + let mut tbl = Table::new(vm); + let files = path.read_dir().map_err(Error::Io)?; + for file in files { + let file = file.map_err(Error::Io)?; + let path = file.path(); + let name = file.file_name(); + let ty = file.file_type().map_err(Error::Io)?; + let mut subt = Table::with_capacity(vm, 0, 4); + subt.set(c"path", SandboxPath::from_path_unchecked(&path)).unwrap(); + subt.set(c"name", name.as_encoded_bytes()).unwrap(); + if ty.is_dir() { + subt.set(c"type", "dir").unwrap(); + } else if ty.is_file() { + subt.set(c"type", "file").unwrap(); + } else if ty.is_symlink() { + subt.set(c"type", "symlink").unwrap(); + } else { + subt.set(c"type", "other").unwrap(); + } + tbl.push(subt).unwrap(); + } + Ok(tbl) + } +} + +decl_lib_func! { + pub fn lua_access<'a>(vm: &Vm, path: SandboxPath) -> crate::vm::Result> { + let perms = path.access(vm); + let mut tbl = Table::new(vm); + if perms & Permissions::R { + tbl.set(c"r", true)?; + } else { + tbl.set(c"r", false)?; + } + if perms & Permissions::W { + tbl.set(c"w", true)?; + } else { + tbl.set(c"w", false)?; + } + if perms & Permissions::X { + tbl.set(c"x", true)?; + } else { + tbl.set(c"x", false)?; + } + Ok(tbl) + } +} + +decl_lib_func! { + pub fn delete(vm: &Vm, path: SandboxPath) -> Result<(), Error> { + if !(path.access(vm) & Permissions::W) { + return Err(Error::Permission); + } + let path = path.to_path(vm).map_err(|_| Error::Sandbox)?; + std::fs::remove_file(path).map_err(Error::Io) + } +} + +decl_lib_func! { + pub fn rename(vm: &Vm, src: SandboxPath, dst: SandboxPath) -> Result<(), Error> { + if !(src.access(vm) & Permissions::W) { + return Err(Error::Permission); + } + if !(src.access(vm) & Permissions::R) { + return Err(Error::Permission); + } + if !(dst.access(vm) & Permissions::W) { + return Err(Error::Permission); + } + let src_path = src.to_path(vm).map_err(|_| Error::Sandbox)?; + let dst_path = dst.to_path(vm).map_err(|_| Error::Sandbox)?; + if !src_path.exists() { + return Err(Error::Io(std::io::Error::new(ErrorKind::NotFound, "source file not found"))); + } + if dst_path.exists() { + return Err(Error::Io(std::io::Error::new(ErrorKind::AlreadyExists, "destination file already exists"))); + } + std::fs::rename(src_path, dst_path).map_err(Error::Io) + } +} diff --git a/core/src/libs/files/mod.rs b/core/src/libs/files/mod.rs new file mode 100644 index 0000000..60dc6cc --- /dev/null +++ b/core/src/libs/files/mod.rs @@ -0,0 +1,64 @@ +// Copyright (c) 2026, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod chroot; +mod file; +mod interface; +mod lib; +mod path; + +use crate::libs::files::file::FileWrapper; +use crate::libs::files::path::PathWrapper; +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::function::types::RFunction; +pub use interface::{SandboxPath, SandboxPathBuf}; + +pub struct Files; + +impl Lib for Files { + const NAMESPACE: &'static str = "bp3d.files"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add_userdata::(c"Path", crate::vm::userdata::case::Camel)?; + namespace.add_userdata::(c"File", crate::vm::userdata::case::Camel)?; + namespace.add([ + ("readText", RFunction::wrap(lib::read_text)), + ("writeText", RFunction::wrap(lib::write_text)), + ("copyFile", RFunction::wrap(lib::copy_file)), + ("symlink", RFunction::wrap(lib::symlink)), + ("exists", RFunction::wrap(lib::exists)), + ("list", RFunction::wrap(lib::list)), + ("createDir", RFunction::wrap(lib::create_dir)), + ("deleteDir", RFunction::wrap(lib::delete_dir)), + ("access", RFunction::wrap(lib::lua_access)), + ("delete", RFunction::wrap(lib::delete)), + ("rename", RFunction::wrap(lib::rename)) + ]) + } +} diff --git a/core/src/libs/files/path.rs b/core/src/libs/files/path.rs new file mode 100644 index 0000000..364e0c9 --- /dev/null +++ b/core/src/libs/files/path.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2026, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::libs::files::chroot::{sandbox, unsandbox, SandboxError}; +use crate::libs::files::SandboxPath; +use crate::{decl_lib_func, decl_userdata, impl_userdata}; +use std::borrow::Cow; +use std::path::PathBuf; + +decl_userdata!(pub struct PathWrapper(PathBuf)); + +impl PathWrapper { + pub fn new(path: PathBuf) -> Self { + Self(path) + } + + pub fn path(&self) -> &PathBuf { + &self.0 + } +} + +decl_lib_func! { + fn new(vm: &Vm, path: &str) -> Result { + unsandbox(vm, path).map(|v| PathWrapper(v.into())) + } +} + +impl_userdata! { + impl PathWrapper { + fn join(this: &PathWrapper, vm: &Vm, other: SandboxPath) -> Result { + let path = other.to_str(vm)?; + Ok(PathWrapper(this.0.join(path.as_ref()))) + } + + fn with_extension(this: &PathWrapper, extension: &str) -> PathWrapper { + PathWrapper(this.0.with_extension(extension)) + } + + fn with_name(this: &PathWrapper, name: &str) -> PathWrapper { + let mut path = this.0.clone(); + path.set_file_name(name); + PathWrapper(path) + } + + fn name(this: &PathWrapper) -> Option { + this.0.file_name().map(|v| v.to_string_lossy().into()) + } + + fn extension(this: &PathWrapper) -> Option { + this.0.extension().map(|v| v.to_string_lossy().into()) + } + + fn __tostring<'a>(this: &PathWrapper, vm: &Vm) -> Cow<'a, str> { + sandbox(vm, &this.0).unwrap_or(Cow::Borrowed("")) + } + + fn __eq(this: &PathWrapper, other: SandboxPath) -> bool { + SandboxPath::Path(this.path()) == other + } + } + static { + [fn new]; + } +} diff --git a/core/src/libs/interface.rs b/core/src/libs/interface.rs new file mode 100644 index 0000000..49078ee --- /dev/null +++ b/core/src/libs/interface.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::util::Namespace; +use crate::vm::core::debug::DebugRegistry; +use crate::vm::Vm; +use bp3d_debug::info; + +pub trait Lib { + const NAMESPACE: &'static str; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()>; + + fn register(&self, vm: &Vm) -> crate::vm::Result<()> { + info!("Adding lib '{}'...", Self::NAMESPACE); + DebugRegistry::add::(vm); + let mut namespace = Namespace::new(vm, Self::NAMESPACE)?; + self.load(&mut namespace)?; + Ok(()) + } +} + +macro_rules! impl_tuple_lib { + ($($t: ident: $id: tt),*) => { + impl<$($t: $crate::libs::Lib),*> $crate::libs::Lib for ($($t),*) { + const NAMESPACE: &'static str = ""; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + $( + self.$id.load(namespace)?; + )* + Ok(()) + } + + fn register(&self, vm: &Vm) -> crate::vm::Result<()> { + $( + self.$id.register(vm)?; + )* + Ok(()) + } + } + }; +} + +impl_tuple_lib!(T: 0, T1: 1); +impl_tuple_lib!(T: 0, T1: 1, T2: 2); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14, T15: 15); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14, T15: 15, T16: 16); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14, T15: 15, T16: 16, T17: 17); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14, T15: 15, T16: 16, T17: 17, T18: 18); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14, T15: 15, T16: 16, T17: 17, T18: 18, T19: 19); +impl_tuple_lib!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9, T10: 10, T11: 11, T12: 12, T13: 13, T14: 14, T15: 15, T16: 16, T17: 17, T18: 18, T19: 19, T20: 20); diff --git a/core/src/libs/lua/base.rs b/core/src/libs/lua/base.rs new file mode 100644 index 0000000..befb670 --- /dev/null +++ b/core/src/libs/lua/base.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::table::Table; + +include!(env!("BP3D_LUA_PATCH_SUMMARY_FILE")); + +pub struct Base; + +impl Lib for Base { + const NAMESPACE: &'static str = "bp3d.lua"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add([("name", "bp3d-lua"), ("version", env!("CARGO_PKG_VERSION"))])?; + let mut patches = Table::with_capacity(namespace.vm(), PATCH_LIST.len(), 0); + for (i, name) in PATCH_LIST.iter().enumerate() { + // Lua indices starts at 1 not 0. + patches.set(i + 1, *name)?; + } + namespace.add([("patches", patches)]) + } +} diff --git a/core/src/libs/lua/call.rs b/core/src/libs/lua/call.rs new file mode 100644 index 0000000..07ebad6 --- /dev/null +++ b/core/src/libs/lua/call.rs @@ -0,0 +1,63 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::libs::interface::Lib; +use crate::util::Namespace; +use crate::vm::error::Error; +use crate::vm::function::types::RFunction; +use crate::vm::value::any::{AnyParam, UncheckedAnyReturn}; +use crate::vm::value::types::Function; + +decl_lib_func! { + fn pcall(vm: &Vm, func: Function) -> UncheckedAnyReturn { + let top = vm.top(); + true.into_param(vm); + let ret = func.call::(()); + let new_top = vm.top(); + match ret { + Ok(_) => unsafe { UncheckedAnyReturn::new(vm, (new_top - top) as _) }, + Err(e) => { + match e { + Error::Runtime(e) => unsafe { UncheckedAnyReturn::new(vm, (false, e.backtrace()).into_param(vm)) }, + e => unsafe { UncheckedAnyReturn::new(vm, (false, e.to_string()).into_param(vm)) } + } + } + } + } +} + +pub struct Call; + +impl Lib for Call { + const NAMESPACE: &'static str = "bp3d.lua"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add([("pcall", RFunction::wrap(pcall))]) + } +} diff --git a/core/src/libs/lua/debug.rs b/core/src/libs/lua/debug.rs new file mode 100644 index 0000000..79d495c --- /dev/null +++ b/core/src/libs/lua/debug.rs @@ -0,0 +1,133 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::core::debug::DebugRegistry; +use crate::vm::core::iter::start; +use crate::vm::error::Error; +use crate::vm::function::types::RFunction; +use crate::vm::table::Table; +use crate::vm::userdata::util::{get_metatable_by_name, get_static_table_by_name}; +use crate::vm::value::any::Any; +use std::ffi::CString; +use std::str::FromStr; + +decl_lib_func! { + fn dump_stack(vm: &Vm, start_index: i32) -> crate::vm::Result> { + let mut tbl = Table::new(vm); + let iter = start::(vm, start_index); + for value in iter { + match value { + Ok(v) => tbl.push(v.to_string())?, + Err(e) => tbl.push(e.to_string())?, + } + } + Ok(tbl) + } +} + +decl_lib_func! { + fn dump_libs(vm: &Vm) -> crate::vm::Result> { + let mut tbl = Table::new(vm); + if let Some(vv) = DebugRegistry::list(vm, crate::vm::core::debug::Lib) { + for v in vv { + tbl.push(v)?; + } + } + Ok(tbl) + } +} + +decl_lib_func! { + fn dump_classes(vm: &Vm) -> crate::vm::Result> { + let mut tbl = Table::new(vm); + if let Some(vv) = DebugRegistry::list(vm, crate::vm::core::debug::Class) { + for v in vv { + tbl.push(v)?; + } + } + Ok(tbl) + } +} + +decl_lib_func! { + fn dump_static_table<'a>(vm: &Vm, class: &str) -> crate::vm::Result>> { + let str = CString::from_str(class).map_err(|_| Error::Null)?; + let mut tbl = match get_static_table_by_name(vm, &str) { + Some(tbl) => tbl, + None => return Ok(None), + }; + let mut out = Table::new(vm); + for (k, _) in tbl.iter() { + let name = k.get::<&str>()?; + out.push(name)?; + } + Ok(Some(out)) + } +} + +decl_lib_func! { + fn dump_meta_table<'a>(vm: &Vm, class: &str) -> crate::vm::Result> { + let str = CString::from_str(class).map_err(|_| Error::Null)?; + let mut tbl = get_metatable_by_name(vm, &str).ok_or(Error::Unknown)?; + let mut out = Table::new(vm); + for (k, _) in tbl.iter() { + let name = k.get::<&str>()?; + out.push(name)?; + } + Ok(out) + } +} + +decl_lib_func! { + fn dump_class_name(vm: &Vm, class: &str) -> crate::vm::Result { + let str = CString::from_str(class).map_err(|_| Error::Null)?; + let tbl = get_metatable_by_name(vm, &str).ok_or(Error::Unknown)?; + let class_name: &str = tbl.get(c"__metatable")?; + Ok(class_name.into()) + } +} + +pub struct Debug; + +impl Lib for Debug { + const NAMESPACE: &'static str = "bp3d.lua.debug"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add([ + ("dumpStack", RFunction::wrap(dump_stack)), + ("dumpLibs", RFunction::wrap(dump_libs)), + ("dumpClasses", RFunction::wrap(dump_classes)), + ("dumpStaticTable", RFunction::wrap(dump_static_table)), + ("dumpMetaTable", RFunction::wrap(dump_meta_table)), + ("dumpClassName", RFunction::wrap(dump_class_name)), + ]) + } +} diff --git a/core/src/libs/lua/load.rs b/core/src/libs/lua/load.rs new file mode 100644 index 0000000..eef364f --- /dev/null +++ b/core/src/libs/lua/load.rs @@ -0,0 +1,121 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::libs::files::chroot::Permissions; +use crate::libs::files::SandboxPath; +use crate::libs::interface::Lib; +use crate::util::Namespace; +use crate::vm::core::load::{Code, Script}; +use crate::vm::function::types::RFunction; +use crate::vm::value::any::{AnyParam, UncheckedAnyReturn}; +use crate::vm::value::types::Function; +use bp3d_util::simple_error; + +decl_lib_func! { + fn run_string(vm: &Vm, s: &str, chunkname: Option<&str>) -> crate::vm::Result { + let top = vm.top(); + let ret = match chunkname { + None => vm.run_code::(s), + Some(name) => vm.run::(Code::new(name, s.as_bytes())) + }; + ret.map(|_| unsafe { UncheckedAnyReturn::new(vm, (vm.top() - top) as _) }) + } +} + +decl_lib_func! { + fn load_string<'a>(vm: &Vm, s: &str, chunkname: Option<&str>) -> (Option>, Option) { + match chunkname { + None => match vm.load_code(s) { + Ok(v) => (Some(v), None), + Err(v) => (None, Some(v.to_string())) + }, + Some(name) => match vm.load(Code::new(name, s.as_bytes())) { + Ok(v) => (Some(v), None), + Err(v) => (None, Some(v.to_string())) + } + } + } +} + +simple_error! { + Error { + Sandbox => "attempt to escape the sandbox", + Permission => "permission denied", + (impl From) Io(std::io::Error) => "io error: {}", + (impl From) Vm(crate::vm::error::Error) => "lua error: {}" + } +} + +decl_lib_func! { + fn load_file<'a> (vm: &Vm, path: SandboxPath) -> (Option>, Option) { + if !(path.access(vm) & Permissions::X) { + return (None, Some("permission denied".into())) + } + let path = match path.to_path(vm) { + Ok(v) => v, + Err(e) => return (None, Some(e.to_string())) + }; + let script = match Script::from_path(path) { + Ok(v) => v, + Err(e) => return (None, Some(e.to_string())) + }; + match vm.load(script) { + Ok(v) => (Some(v), None), + Err(e) => (None, Some(e.to_string())) + } + } +} + +decl_lib_func! { + fn run_file<'a> (vm: &Vm, path: SandboxPath) -> Result { + if !(path.access(vm) & Permissions::X) { + return Err(Error::Permission) + } + let path = path.to_path(vm).map_err(|_| Error::Sandbox)?; + let script = Script::from_path(path)?; + let top = vm.top(); + vm.run::(script)?; + unsafe { Ok(UncheckedAnyReturn::new(vm, (vm.top() - top) as _)) } + } +} + +pub struct Load; + +impl Lib for Load { + const NAMESPACE: &'static str = "bp3d.lua"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add([ + ("runString", RFunction::wrap(run_string)), + ("loadString", RFunction::wrap(load_string)), + ("loadFile", RFunction::wrap(load_file)), + ("runFile", RFunction::wrap(run_file)), + ]) + } +} diff --git a/core/src/libs/lua/mod.rs b/core/src/libs/lua/mod.rs new file mode 100644 index 0000000..1a4569d --- /dev/null +++ b/core/src/libs/lua/mod.rs @@ -0,0 +1,48 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod base; +mod call; +mod load; +mod options; +pub mod require; + +mod debug; +#[cfg(feature = "util-module")] +mod module; + +pub use base::Base; +pub use call::Call; +pub use debug::Debug; +pub use load::Load; +pub use require::Require; + +pub use options::Lua; + +#[cfg(feature = "util-module")] +pub use module::Module; diff --git a/core/src/libs/lua/module.rs b/core/src/libs/lua/module.rs new file mode 100644 index 0000000..7dd6118 --- /dev/null +++ b/core/src/libs/lua/module.rs @@ -0,0 +1,80 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::libs::Lib; +use crate::util::module::Result; +use crate::util::module::{Error, ModuleManager}; +use crate::util::Namespace; +use crate::vm::core::destructor::Pool; +use crate::vm::function::types::RFunction; +use crate::vm::registry::named::Key; +use crate::vm::registry::types::LuaRef; +use crate::vm::value::types::RawPtr; +use crate::vm::Vm; + +static KEY: Key>> = Key::new("__module_manager__"); + +fn register_module_manager(vm: &Vm) { + let value = KEY.push(vm); + if value.is_some() { + panic!("A ModuleManager is already registered in the current Vm"); + } + let manager = ModuleManager::new(); + let mut value = Box::new(manager); + let value2 = &mut *value as *mut ModuleManager; + Pool::attach_post_close(vm, || { + let value = value; + drop(value); + }); + let value = crate::vm::registry::lua_ref::LuaRef::new(vm, RawPtr::new(value2)); + KEY.set(value); +} + +decl_lib_func! { + fn load_module(vm: &Vm, lib: &str, plugin: &str) -> Result<()> { + let manager = KEY.push(vm); + if let Some(manager) = manager { + unsafe { (&mut *manager.get().as_mut_ptr()).load(lib, plugin, vm) }?; + Ok(()) + } else { + Err(Error::NotRegistered) + } + } +} + +pub struct Module; + +impl Lib for Module { + const NAMESPACE: &'static str = "bp3d.lua.module"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + register_module_manager(namespace.vm()); + namespace.add([("load", RFunction::wrap(load_module))]) + } +} diff --git a/core/src/libs/lua/options.rs b/core/src/libs/lua/options.rs new file mode 100644 index 0000000..2c3aec7 --- /dev/null +++ b/core/src/libs/lua/options.rs @@ -0,0 +1,54 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::libs::lua::base::Base; +use crate::libs::lua::call::Call; +use crate::libs::lua::load::Load; +use crate::libs::lua::require::{Provider, Require}; +use crate::libs::Lib; +use crate::vm::closure::arc::Shared; + +#[derive(Default)] +pub struct Lua { + pub(super) provider: Option>, +} + +impl Lua { + pub fn new() -> Self { + Self::default() + } + + pub fn provider(mut self, provider: Shared) -> Self { + self.provider = Some(provider); + self + } + + pub fn build(self) -> impl Lib { + (Base, Call, Load, Require(self.provider.unwrap_or_default())) + } +} diff --git a/core/src/libs/lua/require.rs b/core/src/libs/lua/require.rs new file mode 100644 index 0000000..817eb8c --- /dev/null +++ b/core/src/libs/lua/require.rs @@ -0,0 +1,100 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_closure; +use crate::libs::interface::Lib; +use crate::util::Namespace; +use crate::vm::closure::arc::{Arc, Shared}; +use crate::vm::core::debug::DebugRegistry; +use crate::vm::value::any::{AnyParam, UncheckedAnyReturn}; +use crate::vm::Vm; +use bp3d_util::simple_error; +use std::collections::HashMap; +use std::sync::RwLock; + +simple_error! { + pub Error { + (impl From) Vm(crate::vm::error::Error) => "lua error: {}", + InvalidSyntax => "invalid syntax for require", + UnknownSource(String) => "unknown source name {}" + } +} + +pub trait Source: Send + Sync { + fn run(&self, vm: &Vm, path: &str, full_path: &str) -> crate::vm::Result; +} + +#[derive(Default)] +pub struct Provider(RwLock>>); + +impl Provider { + pub fn new() -> Self { + Provider::default() + } + + pub fn add_source(&self, name: String, source: S) { + let mut guard = self.0.write().unwrap(); + guard.insert(name, Box::new(source)); + } + + pub fn require(&self, vm: &Vm, path: &str) -> Result { + let id = path.find('.').ok_or(Error::InvalidSyntax)?; + let source = &path[..id]; + let guard = self.0.read().unwrap(); + let src = guard + .get(source) + .ok_or_else(|| Error::UnknownSource(source.into()))?; + let ret = src.run(vm, &path[id + 1..], path)?; + Ok(ret) + } +} + +decl_closure! { + fn require |provider: Arc| (vm: &Vm, path: &str) -> Result { + let top = vm.top(); + provider.require(vm, path)?; + unsafe { Ok(UncheckedAnyReturn::new(vm, (vm.top() - top) as _)) } + } +} + +pub struct Require(pub Shared); + +impl Lib for Require { + const NAMESPACE: &'static str = "bp3d.lua"; + + fn load(&self, _: &mut Namespace) -> crate::vm::Result<()> { + std::unreachable!() + } + + fn register(&self, vm: &Vm) -> crate::vm::Result<()> { + DebugRegistry::add::(vm); + let rc = Arc::from_rust(vm, self.0.clone()); + let mut namespace = Namespace::new(vm, "bp3d.lua")?; + namespace.add([("require", require(rc))]) + } +} diff --git a/core/src/libs/mod.rs b/core/src/libs/mod.rs new file mode 100644 index 0000000..aa0f187 --- /dev/null +++ b/core/src/libs/mod.rs @@ -0,0 +1,51 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// The bp3d-lua core library. +#[cfg(feature = "libs")] +pub mod lua; + +/// Utility toolkit. +#[cfg(feature = "libs")] +pub mod util; + +/// OS toolkit. +#[cfg(feature = "libs")] +pub mod os; + +#[cfg(feature = "libs")] +pub mod files; + +#[cfg(feature = "libs-core")] +mod interface; + +//TODO: threading (sandbox with max number of threads) +// make sure thread join is time-limited. + +#[cfg(feature = "libs-core")] +pub use interface::*; diff --git a/core/src/libs/os/compat.rs b/core/src/libs/os/compat.rs new file mode 100644 index 0000000..09d6312 --- /dev/null +++ b/core/src/libs/os/compat.rs @@ -0,0 +1,207 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::function::types::RFunction; +use crate::vm::function::IntoParam; +use crate::vm::table::Table; +use crate::vm::Vm; +use bp3d_os::time::{LocalUtcOffset, MonthExt}; +use bp3d_util::simple_error; +use std::time::Instant; +use time::format_description::parse; +use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset}; + +enum TableOrString<'a> { + Table(Table<'a>), + String(String), +} + +unsafe impl IntoParam for TableOrString<'_> { + fn into_param(self, vm: &Vm) -> i32 { + match self { + TableOrString::Table(t) => t.into_param(vm), + TableOrString::String(s) => s.into_param(vm), + } + } +} + +fn get_std_offset() -> UtcOffset { + let now = OffsetDateTime::now_utc(); + let jan = PrimitiveDateTime::new( + Date::from_calendar_date(now.year(), Month::January, 1).unwrap(), + Time::MIDNIGHT, + ) + .assume_utc(); + let jul = PrimitiveDateTime::new( + Date::from_calendar_date(now.year(), Month::July, 1).unwrap(), + Time::MIDNIGHT, + ) + .assume_utc(); + let offset_jan = UtcOffset::local_offset_at(jan).unwrap(); + let offset_jul = UtcOffset::local_offset_at(jul).unwrap(); + std::cmp::max(offset_jan, offset_jul) +} + +const REPLACEMENTS: &[(&str, &str)] = &[ + ("[", "[["), + ("%%", "%"), + ("%a", "[weekday repr:short]"), + ("%A", "[weekday repr:long]"), + ("%b", "[month repr:short]"), + ("%B", "[month repr:long]"), + ("%d", "[day]"), + ("%H", "[hour repr:24]"), + ("%I", "[hour repr:12]"), + ("%M", "[minute]"), + ("%m", "[month]"), + ("%p", "[period]"), + ("%S", "[second]"), + ("%w", "[weekday]"), + ("%Y", "[year]"), + ("%y", "[year repr:last_two]"), +]; + +decl_lib_func! { + fn date<'a>(vm: &Vm, format: Option<&str>, time: Option) -> Option> { + let mut format = format.unwrap_or("%c"); + let mut time = time.map(OffsetDateTime::from_unix_timestamp).unwrap_or(Ok(OffsetDateTime::now_utc())).ok()?; + if format.starts_with('!') { + format = &format[1..]; + } else { + let offset = UtcOffset::local_offset_at(time)?; + time = time.to_offset(offset); + } + if format == "*t" { + let std_offset = get_std_offset(); + let mut table = Table::new(vm); + table.set(c"sec", time.second()).unwrap(); + table.set(c"min", time.minute()).unwrap(); + table.set(c"hour", time.hour()).unwrap(); + table.set(c"day", time.day()).unwrap(); + table.set(c"month", time.month() as u8).unwrap(); + table.set(c"year", time.year()).unwrap(); + table.set(c"wday", time.weekday() as u8 + 1).unwrap(); + table.set(c"yday", time.to_julian_day()).unwrap(); + table.set(c"isdst", time.offset() < std_offset).unwrap(); + Some(TableOrString::Table(table)) + } else { + let mut format = String::from(format); + for (k, v) in REPLACEMENTS { + format = format.replace(k, v); + } + let format = parse(format.as_str()).ok()?; + time.format(&format).map(TableOrString::String).ok() + } + } +} + +simple_error! { + TimeFormatError { + (impl From)Vm(crate::vm::error::Error) => "vm error: {}", + InvalidMonthIndex(u8) => "invalid month index {}", + (impl From)Time(time::error::ComponentRange) => "out of range error: {}" + } +} + +fn get_time_from_table(table: Table) -> Result { + let year: i32 = table.get(c"year")?; + let month: u8 = table.get(c"month")?; + let day: u8 = table.get(c"day")?; + let date = Date::from_calendar_date( + year, + Month::from_index(month).ok_or(TimeFormatError::InvalidMonthIndex(month))?, + day, + )?; + let hour: Option = table.get(c"hour")?; + let minute: Option = table.get(c"min")?; + let second: Option = table.get(c"sec")?; + let mut hour = hour.unwrap_or(12); + let minute = minute.unwrap_or(0); + let second = second.unwrap_or(0); + let dst: Option = table.get(c"isdst")?; + let dst = dst.unwrap_or(false); + // Consider DST to be always +1H, this may not always be true but is true in most countries. + if dst { + hour += 1; + } + let time = Time::from_hms(hour, minute, second)?; + let time = PrimitiveDateTime::new(date, time).assume_utc(); + Ok(time) +} + +decl_lib_func! { + fn time(table: Option) -> Result { + match table { + Some(table) => get_time_from_table(table).map(|v| v.unix_timestamp()), + None => Ok(OffsetDateTime::now_utc().unix_timestamp()) + } + } +} + +decl_lib_func! { + fn difftime(a: i64, b: Option) -> Option { + let a = OffsetDateTime::from_unix_timestamp(a).ok()?; + let b = OffsetDateTime::from_unix_timestamp(b.unwrap_or(0)).ok()?; + Some((a - b).as_seconds_f64()) + } +} + +thread_local! { + static NOW: Instant = Instant::now(); +} + +decl_lib_func! { + fn clock() -> f64 { + NOW.with(|v| v.elapsed().as_secs_f64()) + } +} + +decl_lib_func! { + fn getenv(key: &str) -> Option> { + std::env::var_os(key).map(|v| v.into_encoded_bytes().into_boxed_slice()) + } +} + +pub struct Compat; + +impl Lib for Compat { + const NAMESPACE: &'static str = "os"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add([ + ("date", RFunction::wrap(date)), + ("time", RFunction::wrap(time)), + ("clock", RFunction::wrap(clock)), + ("difftime", RFunction::wrap(difftime)), + ("getenv", RFunction::wrap(getenv)), + ]) + } +} diff --git a/core/src/libs/os/instant.rs b/core/src/libs/os/instant.rs new file mode 100644 index 0000000..cea5b7f --- /dev/null +++ b/core/src/libs/os/instant.rs @@ -0,0 +1,61 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::function::types::RFunction; +use crate::{decl_lib_func, decl_userdata, impl_userdata}; + +decl_userdata!(struct InstantWrapper(bp3d_os::time::Instant)); + +impl_userdata! { + impl InstantWrapper { + fn elapsed(this: &InstantWrapper) -> f64 { + this.0.elapsed().as_secs_f64() + } + } +} + +decl_lib_func! { + fn now() -> InstantWrapper { + InstantWrapper(bp3d_os::time::Instant::now()) + } +} + +pub struct Instant; + +impl Lib for Instant { + const NAMESPACE: &'static str = "bp3d.os.instant"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace + .vm() + .register_userdata::(crate::vm::userdata::case::Camel)?; + namespace.add([("now", RFunction::wrap(now))]) + } +} diff --git a/core/src/libs/os/mod.rs b/core/src/libs/os/mod.rs new file mode 100644 index 0000000..48f9a5b --- /dev/null +++ b/core/src/libs/os/mod.rs @@ -0,0 +1,35 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod compat; +mod instant; +mod time; + +pub use compat::Compat; +pub use instant::Instant; +pub use time::Time; diff --git a/core/src/libs/os/time.rs b/core/src/libs/os/time.rs new file mode 100644 index 0000000..35bac99 --- /dev/null +++ b/core/src/libs/os/time.rs @@ -0,0 +1,194 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::function::types::RFunction; +use crate::vm::function::IntoParam; +use crate::vm::table::Table; +use crate::vm::value::IntoLua; +use crate::vm::Vm; +use crate::{decl_lib_func, decl_userdata, impl_userdata}; +use bp3d_os::time::{LocalOffsetDateTime, MonthExt}; +use bp3d_util::simple_error; +use time::error::{ComponentRange, Format, InvalidFormatDescription}; +use time::format_description::parse; +use time::{Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, UtcOffset}; + +simple_error! { + FormatError { + (impl From)InvalidDescription(InvalidFormatDescription) => "invalid format description: {}", + (impl From)Format(Format) => "format error: {}" + } +} + +decl_userdata!(struct OffsetDateTimeWrapper(OffsetDateTime)); + +impl_userdata! { + impl OffsetDateTimeWrapper { + fn format(this: &OffsetDateTimeWrapper, format: &str) -> Result { + let desc = parse(format)?; + let str = this.0.format(&desc)?; + Ok(str) + } + + fn __add(this: &OffsetDateTimeWrapper, duration: f64) -> Option { + this.0.checked_add(Duration::seconds_f64(duration)).map(OffsetDateTimeWrapper) + } + + fn __sub(this: &OffsetDateTimeWrapper, other: &OffsetDateTimeWrapper) -> f64 { + (this.0 - other.0).as_seconds_f64() + } + + fn __gt(this: &OffsetDateTimeWrapper, other: &OffsetDateTimeWrapper) -> bool { + this.0 > other.0 + } + + fn __ge(this: &OffsetDateTimeWrapper, other: &OffsetDateTimeWrapper) -> bool { + this.0 >= other.0 + } + + fn __lt(this: &OffsetDateTimeWrapper, other: &OffsetDateTimeWrapper) -> bool { + this.0 < other.0 + } + + fn __le(this: &OffsetDateTimeWrapper, other: &OffsetDateTimeWrapper) -> bool { + this.0 <= other.0 + } + + fn get_date<'a>(this: &OffsetDateTimeWrapper, vm: &Vm) -> crate::vm::Result> { + let mut table = Table::with_capacity(vm, 0, 3); + table.set(c"year", this.0.year())?; + table.set(c"month", this.0.month() as u8)?; + table.set(c"day", this.0.day())?; + Ok(table) + } + + fn get_time<'a>(this: &OffsetDateTimeWrapper, vm: &Vm) -> crate::vm::Result> { + let mut table = Table::with_capacity(vm, 0, 3); + table.set(c"hour", this.0.hour())?; + table.set(c"minute", this.0.minute())?; + table.set(c"second", this.0.second())?; + Ok(table) + } + + fn get_offset<'a>(this: &OffsetDateTimeWrapper, vm: &Vm) -> crate::vm::Result> { + let mut table = Table::with_capacity(vm, 0, 3); + table.set(c"hours", this.0.offset().whole_hours())?; + table.set(c"minutes", this.0.offset().whole_minutes())?; + table.set(c"seconds", this.0.offset().whole_seconds())?; + Ok(table) + } + } + + static { + [fn new]; + [fn from_unix_timestamp]; + } +} + +unsafe impl IntoParam for OffsetDateTime { + fn into_param(self, vm: &Vm) -> i32 { + OffsetDateTimeWrapper(self).into_param(vm) + } +} + +unsafe impl IntoLua for OffsetDateTime { + fn into_lua(self, vm: &Vm) -> u16 { + OffsetDateTimeWrapper(self).into_lua(vm) + } +} + +decl_lib_func! { + fn now_utc() -> OffsetDateTime { + OffsetDateTime::now_utc() + } +} + +decl_lib_func! { + fn now_local() -> Option { + OffsetDateTime::now_local() + } +} + +decl_lib_func! { + fn from_unix_timestamp(timestamp: i64) -> Result { + OffsetDateTime::from_unix_timestamp(timestamp) + } +} + +simple_error! { + DateTimeError { + (impl From)Vm(crate::vm::error::Error) => "vm error: {}", + InvalidMonthIndex(u8) => "invalid month index {}", + (impl From)Time(ComponentRange) => "out of range error: {}" + } +} + +decl_lib_func! { + fn new(table: Table) -> Result { + let year: i32 = table.get(c"year")?; + let month: u8 = table.get(c"month")?; + let day: u8 = table.get(c"day")?; + let date = Date::from_calendar_date(year, Month::from_index(month).ok_or(DateTimeError::InvalidMonthIndex(month))?, day)?; + let hour: Option = table.get(c"hour")?; + let minute: Option = table.get(c"min")?; + let second: Option = table.get(c"sec")?; + let hour = hour.unwrap_or(12); + let minute = minute.unwrap_or(0); + let second = second.unwrap_or(0); + let time = time::Time::from_hms(hour, minute, second)?; + let offset: Option
= table.get(c"offset")?; + if let Some(offset) = offset { + let offset_hours: i8 = offset.get(c"hours")?; + let offset_minutes: i8 = offset.get(c"minutes")?; + let offset_seconds: i8 = offset.get(c"seconds")?; + let offset = UtcOffset::from_hms(offset_hours, offset_minutes, offset_seconds)?; + Ok(OffsetDateTime::new_in_offset(date, time, offset)) + } else { + Ok(PrimitiveDateTime::new(date, time).assume_utc()) + } + } +} + +pub struct Time; + +impl Lib for Time { + const NAMESPACE: &'static str = "bp3d.os.time"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add_userdata::( + c"OffsetDateTime", + crate::vm::userdata::case::Camel, + )?; + namespace.add([ + ("nowUtc", RFunction::wrap(now_utc)), + ("nowLocal", RFunction::wrap(now_local)), + ]) + } +} diff --git a/core/src/libs/util/mod.rs b/core/src/libs/util/mod.rs new file mode 100644 index 0000000..85b4870 --- /dev/null +++ b/core/src/libs/util/mod.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod num; +mod string; +mod table; +mod utf8; + +pub use num::Num; +pub use string::String; +pub use table::Table; +pub use utf8::Utf8; + +// Workaround for language defect #22259. +#[allow(non_upper_case_globals)] +pub const Util: (Table, String, Utf8, Num) = (Table, String, Utf8, Num); diff --git a/core/src/libs/util/num.rs b/core/src/libs/util/num.rs new file mode 100644 index 0000000..45a8223 --- /dev/null +++ b/core/src/libs/util/num.rs @@ -0,0 +1,104 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::ffi::lua::RawNumber; +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::function::types::RFunction; +use crate::vm::value::any::Any; +use crate::vm::value::types::{Int53, UInt53}; + +decl_lib_func! { + fn eq(a: RawNumber, b: RawNumber, epsilon: RawNumber) -> bool { + (a - b).abs() <= epsilon + } +} + +decl_lib_func! { + fn parsenumber(value: &str) -> (Option, Option) { + match value.parse() { + Ok(n) => (Some(n), None), + Err(e) => (None, Some(e.to_string())) + } + } +} + +decl_lib_func! { + fn parseint64(value: &str) -> (Option, Option) { + match value.parse() { + Ok(n) => (Some(n), None), + Err(e) => (None, Some(e.to_string())) + } + } +} + +decl_lib_func! { + fn parseuint64(value: &str) -> (Option, Option) { + match value.parse() { + Ok(n) => (Some(n), None), + Err(e) => (None, Some(e.to_string())) + } + } +} + +decl_lib_func! { + fn toistring(val: Any) -> crate::vm::Result { + let val = val.to_integer()?; + Ok(val.to_string()) + } +} + +decl_lib_func! { + fn toustring(val: Any) -> crate::vm::Result { + let val = val.to_uinteger()?; + Ok(val.to_string()) + } +} + +pub struct Num; + +impl Lib for Num { + const NAMESPACE: &'static str = "bp3d.util.num"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add([("UINT53_MAX", UInt53::MAX), ("UINT53_MIN", UInt53::MIN)])?; + namespace.add([("INT53_MAX", Int53::MAX), ("INT53_MIN", Int53::MIN)])?; + namespace.add([("UINT64_MAX", u64::MAX), ("UINT64_MIN", u64::MIN)])?; + namespace.add([("INT64_MAX", i64::MAX), ("INT64_MIN", i64::MIN)])?; + namespace.add([("NAN", f64::NAN), ("EPSILON", f64::EPSILON)])?; + namespace.add([ + ("toistring", RFunction::wrap(toistring)), + ("toustring", RFunction::wrap(toustring)), + ("eq", RFunction::wrap(eq)), + ("parsenumber", RFunction::wrap(parsenumber)), + ("parseint64", RFunction::wrap(parseint64)), + ("parseuint64", RFunction::wrap(parseuint64)), + ]) + } +} diff --git a/core/src/libs/util/string.rs b/core/src/libs/util/string.rs new file mode 100644 index 0000000..516b037 --- /dev/null +++ b/core/src/libs/util/string.rs @@ -0,0 +1,97 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::function::types::RFunction; +use crate::vm::table::Table; +use bp3d_util::string::BufTools; +use std::borrow::Cow; + +decl_lib_func! { + fn starts_with(src: &[u8], prefix: &[u8]) -> bool { + src.starts_with(prefix) + } +} + +decl_lib_func! { + fn ends_with(src: &[u8], suffix: &[u8]) -> bool { + src.ends_with(suffix) + } +} + +decl_lib_func! { + fn contains(src: &[u8], needle: &[u8]) -> bool { + if needle.is_empty() { + return true; + } + src.windows(needle.len()).any(|window| window == needle) + } +} + +decl_lib_func! { + fn split<'a>(vm: &Vm, src: &[u8], pattern: u8) -> crate::vm::Result> { + let split = src.split(|v| *v == pattern); + let mut tbl = Table::new(vm); + for (i, v) in split.enumerate() { + // Indices starts at 1 in lua. + tbl.set(i + 1, v)?; + } + Ok(tbl) + } +} + +decl_lib_func! { + fn capitalise(src: &[u8]) -> Cow<'_, [u8]> { + src.capitalise_ascii() + } +} + +decl_lib_func! { + fn decapitalise(src: &[u8]) -> Cow<'_, [u8]> { + src.decapitalise_ascii() + } +} + +pub struct String; + +impl Lib for String { + const NAMESPACE: &'static str = "bp3d.util.string"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add([ + ("contains", RFunction::wrap(contains)), + ("split", RFunction::wrap(split)), + ("capitalise", RFunction::wrap(capitalise)), + ("decapitalise", RFunction::wrap(decapitalise)), + ("startsWith", RFunction::wrap(starts_with)), + ("endsWith", RFunction::wrap(ends_with)), + ]) + } +} diff --git a/core/src/libs/util/table.rs b/core/src/libs/util/table.rs new file mode 100644 index 0000000..b10fc8a --- /dev/null +++ b/core/src/libs/util/table.rs @@ -0,0 +1,184 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::function::types::RFunction; +use crate::vm::table::Table as LuaTable; +use crate::vm::value::any::Any; +use crate::vm::Vm; +use bp3d_util::simple_error; + +fn update_rec(vm: &Vm, mut dst: LuaTable, mut src: LuaTable) -> crate::vm::Result<()> { + for (k, v) in src.iter() { + let k = k.to_any()?; + let v = v.to_any()?; + match v { + Any::Table(v) => vm.scope(|_| { + let dst1: Option = dst.get_any(k.clone())?; + match dst1 { + None => { + let tbl = LuaTable::new(vm); + update_rec(vm, tbl.clone(), v)?; + dst.set_any(k, tbl)?; + } + Some(v1) => update_rec(vm, v1, v)?, + } + Ok(()) + })?, + _ => dst.set_any(k, v)?, + } + } + Ok(()) +} + +decl_lib_func! { + fn update(vm: &Vm, dst: LuaTable, src: LuaTable) -> crate::vm::Result<()> { + update_rec(vm, dst, src) + } +} + +decl_lib_func! { + fn concat(vm: &Vm, dst: LuaTable) -> crate::vm::Result<()> { + let mut dst = dst; + let iter = crate::vm::core::iter::start::(vm, 2); + for res in iter { + let mut src = res?; + for (_, v) in src.iter() { + let v = v.to_any()?; + dst.push(v)?; + } + } + Ok(()) + } +} + +decl_lib_func! { + fn copy<'a>(vm: &Vm, src: LuaTable) -> crate::vm::Result> { + let tbl = crate::vm::table::Table::new(vm); + update_rec(vm, tbl.clone(), src)?; + Ok(tbl) + } +} + +decl_lib_func! { + fn count(src: LuaTable) -> u32 { + src.len() as _ + } +} + +fn to_string_rec(prefix: String, mut table: LuaTable) -> crate::vm::Result> { + let mut lines = Vec::new(); + for (k, v) in table.iter() { + let k = k.to_any()?; + let v = v.to_any()?; + match v { + Any::Table(v) => { + lines.push(format!("{}:", k)); + lines.extend(to_string_rec(prefix.clone() + " ", v)?); + } + v => lines.push(format!("{}: {}", k, v)), + } + } + Ok(lines) +} + +decl_lib_func! { + fn to_string(src: LuaTable) -> crate::vm::Result { + to_string_rec("".into(), src).map(|v| v.join("\n")) + } +} + +decl_lib_func! { + fn contains(src: LuaTable, value: Any) -> crate::vm::Result { + let mut src = src; + for (_, v) in src.iter() { + let v = v.to_any()?; + if v == value { + return Ok(true) + } + } + Ok(false) + } +} + +decl_lib_func! { + fn contains_key(src: LuaTable, key: Any) -> crate::vm::Result { + let mut src = src; + for (k, _) in src.iter() { + let k = k.to_any()?; + if k == key { + return Ok(true) + } + } + Ok(false) + } +} + +simple_error! { + ProtectError { + NewIndex => "attempt to set value into protected table." + } +} + +decl_lib_func! { + fn __newindex() -> Result<(), ProtectError> { + Err(ProtectError::NewIndex) + } +} + +decl_lib_func! { + fn protect<'a>(vm: &Vm, src: LuaTable) -> crate::vm::Result> { + let mut wrapper = LuaTable::new(vm); + let mut metatable = LuaTable::new(vm); + metatable.set(c"__index", src)?; + metatable.set(c"__newindex", RFunction::wrap(__newindex))?; + wrapper.set_metatable(metatable); + Ok(wrapper) + } +} + +pub struct Table; + +impl Lib for Table { + const NAMESPACE: &'static str = "bp3d.util.table"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add([ + ("update", RFunction::wrap(update)), + ("count", RFunction::wrap(count)), + ("tostring", RFunction::wrap(to_string)), + ("contains", RFunction::wrap(contains)), + ("containsKey", RFunction::wrap(contains_key)), + ("protect", RFunction::wrap(protect)), + ("copy", RFunction::wrap(copy)), + ("concat", RFunction::wrap(concat)), + ]) + } +} diff --git a/core/src/libs/util/utf8.rs b/core/src/libs/util/utf8.rs new file mode 100644 index 0000000..68942a2 --- /dev/null +++ b/core/src/libs/util/utf8.rs @@ -0,0 +1,139 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::decl_lib_func; +use crate::libs::Lib; +use crate::util::Namespace; +use crate::vm::function::types::RFunction; +use crate::vm::table::Table; +use bp3d_util::string::StrTools; +use std::borrow::Cow; + +decl_lib_func! { + fn contains(src: &str, needle: &str) -> bool { + src.contains(needle) + } +} + +decl_lib_func! { + fn split<'a>(vm: &Vm, src: &str, pattern: &str) -> crate::vm::Result> { + let split = src.split(pattern); + let mut tbl = Table::new(vm); + for (i, v) in split.enumerate() { + // Indices starts at 1 in lua. + tbl.set(i + 1, v)?; + } + Ok(tbl) + } +} + +decl_lib_func! { + fn replace(src: &str, pattern: &str, replacement: &str) -> String { + src.replace(pattern, replacement) + } +} + +decl_lib_func! { + fn count(src: &str) -> u32 { + src.chars().count() as u32 + } +} + +decl_lib_func! { + fn char_at(src: &str, pos: u32) -> Option { + src.chars().nth(pos as usize).map(|v| v as u32) + } +} + +decl_lib_func! { + fn from_string(src: &[u8]) -> Option<&str> { + std::str::from_utf8(src).ok() + } +} + +decl_lib_func! { + fn from_string_lossy(src: &[u8]) -> Cow<'_, str> { + String::from_utf8_lossy(src) + } +} + +decl_lib_func! { + fn capitalise(src: &str) -> Cow<'_, str> { + src.capitalise() + } +} + +decl_lib_func! { + fn decapitalise(src: &str) -> Cow<'_, str> { + src.decapitalise() + } +} + +decl_lib_func! { + fn upper(src: &str) -> String { + src.to_uppercase() + } +} + +decl_lib_func! { + fn lower(src: &str) -> String { + src.to_lowercase() + } +} + +decl_lib_func! { + fn sub(src: &str, start: u32, end: Option) -> &str { + match end { + None => src.sub_nearest((start as usize)..), + Some(v) => src.sub_nearest((start as usize)..(v as usize)) + } + } +} + +pub struct Utf8; + +impl Lib for Utf8 { + const NAMESPACE: &'static str = "bp3d.util.utf8"; + + fn load(&self, namespace: &mut Namespace) -> crate::vm::Result<()> { + namespace.add([ + ("contains", RFunction::wrap(contains)), + ("split", RFunction::wrap(split)), + ("replace", RFunction::wrap(replace)), + ("count", RFunction::wrap(count)), + ("charAt", RFunction::wrap(char_at)), + ("fromString", RFunction::wrap(from_string)), + ("fromStringLossy", RFunction::wrap(from_string_lossy)), + ("capitalise", RFunction::wrap(capitalise)), + ("decapitalise", RFunction::wrap(decapitalise)), + ("upper", RFunction::wrap(upper)), + ("lower", RFunction::wrap(lower)), + ("sub", RFunction::wrap(sub)), + ]) + } +} diff --git a/core/src/macro/closure.rs b/core/src/macro/closure.rs new file mode 100644 index 0000000..5076564 --- /dev/null +++ b/core/src/macro/closure.rs @@ -0,0 +1,71 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[macro_export] +macro_rules! decl_closure { + ( + $vis: vis fn $fn_name: ident $(<$lifetime: lifetime>)? |$upvalue_name: ident: $upvalue_ty: ty| ($name: ident: &Vm$(, $($arg_name: ident: $arg_ty: ty),*)?) -> $ret_ty: ty $code: block + ) => { + $vis fn $fn_name(upvalue: $upvalue_ty) -> $crate::vm::closure::types::RClosure<$upvalue_ty> { + extern "C-unwind" fn _cfunc(l: $crate::ffi::lua::State) -> i32 { + fn _func $(<$lifetime>)? ($name: &$($lifetime)? $crate::vm::Vm, $upvalue_name: <$upvalue_ty as $crate::vm::closure::Upvalue>::From<'_>$(, $($arg_name: $arg_ty),*)?) -> $ret_ty $code + use $crate::vm::function::IntoParam; + let vm = unsafe { $crate::vm::Vm::from_raw(l) }; + #[inline(always)] + extern "C-unwind" fn _vmfunc $(<$lifetime>)? (vm: &$($lifetime)? $crate::vm::Vm) -> i32 { + let $upvalue_name: <$upvalue_ty as $crate::vm::closure::Upvalue>::From<'_> = unsafe { $crate::vm::closure::FromUpvalue::from_upvalue(vm, 1) }; + $($crate::decl_from_param_unchecked!(vm, 1, $($arg_name: $arg_ty)*);)? + let ret = _func(vm, $upvalue_name $(, $($arg_name),*)?); + ret.into_param(vm) as _ + } + _vmfunc(&vm) + } + $crate::vm::closure::types::RClosure::new(_cfunc, upvalue) + } + }; + ( + $vis: vis fn $fn_name: ident $(<$lifetime: lifetime>)? |$upvalue_name: ident: $upvalue_ty: ty| ($($arg_name: ident: $arg_ty: ty),*) -> $ret_ty: ty $code: block + ) => { + $vis fn $fn_name(upvalue: $upvalue_ty) -> $crate::vm::closure::types::RClosure<$upvalue_ty> { + extern "C-unwind" fn _cfunc(l: $crate::ffi::lua::State) -> i32 { + fn _func $(<$lifetime>)? ($upvalue_name: <$upvalue_ty as $crate::vm::closure::Upvalue>::From<'_>, $($arg_name: $arg_ty),*) -> $ret_ty $code + use $crate::vm::function::IntoParam; + let vm = unsafe { $crate::vm::Vm::from_raw(l) }; + #[inline(always)] + extern "C-unwind" fn _vmfunc $(<$lifetime>)? (vm: &$($lifetime)? $crate::vm::Vm) -> i32 { + let $upvalue_name: <$upvalue_ty as $crate::vm::closure::Upvalue>::From<'_> = unsafe { $crate::vm::closure::FromUpvalue::from_upvalue(vm, 1) }; + $crate::decl_from_param_unchecked!(vm, 1, $($arg_name: $arg_ty)*); + let ret = _func($upvalue_name, $($arg_name),*); + ret.into_param(vm) as _ + } + _vmfunc(&vm) + } + $crate::vm::closure::types::RClosure::new(_cfunc, upvalue) + } + }; +} diff --git a/core/src/macro/lib_func.rs b/core/src/macro/lib_func.rs new file mode 100644 index 0000000..860abd0 --- /dev/null +++ b/core/src/macro/lib_func.rs @@ -0,0 +1,63 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[macro_export] +macro_rules! decl_lib_func { + ( + $vis: vis fn $fn_name: ident $(<$lifetime: lifetime>)? ($name: ident: &Vm$(, $($arg_name: ident: $arg_ty: ty),*)?) -> $ret_ty: ty $code: block + ) => { + $vis extern "C-unwind" fn $fn_name(l: $crate::ffi::lua::State) -> i32 { + fn _func $(<$lifetime>)? ($name: &$($lifetime)? $crate::vm::Vm$(, $($arg_name: $arg_ty),*)?) -> $ret_ty $code + use $crate::vm::function::IntoParam; + let vm = unsafe { $crate::vm::Vm::from_raw(l) }; + #[inline(always)] + extern "C-unwind" fn _vmfunc $(<$lifetime>)? (vm: &$($lifetime)? $crate::vm::Vm) -> i32 { + $($crate::decl_from_param_unchecked!(vm, 1, $($arg_name: $arg_ty)*);)? + let ret = _func(vm $(, $($arg_name),*)?); + ret.into_param(vm) as _ + } + _vmfunc(&vm) + } + }; + ( + $vis: vis fn $fn_name: ident $(<$lifetime: lifetime>)? ($($arg_name: ident: $arg_ty: ty),*) -> $ret_ty: ty $code: block + ) => { + $vis extern "C-unwind" fn $fn_name(l: $crate::ffi::lua::State) -> i32 { + fn _func $(<$lifetime>)? ($($arg_name: $arg_ty),*) -> $ret_ty $code + use $crate::vm::function::IntoParam; + let vm = unsafe { $crate::vm::Vm::from_raw(l) }; + #[inline(always)] + extern "C-unwind" fn _vmfunc $(<$lifetime>)? (vm: &$($lifetime)? $crate::vm::Vm) -> i32 { + $crate::decl_from_param_unchecked!(vm, 1, $($arg_name: $arg_ty)*); + let ret = _func($($arg_name),*); + ret.into_param(vm) as _ + } + _vmfunc(&vm) + } + }; +} diff --git a/core/src/macro/mod.rs b/core/src/macro/mod.rs new file mode 100644 index 0000000..afc840c --- /dev/null +++ b/core/src/macro/mod.rs @@ -0,0 +1,111 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod closure; +mod lib_func; +mod userdata; +mod userdata_func; + +#[macro_export] +macro_rules! c_stringify { + ($str: ident) => { + unsafe { std::ffi::CStr::from_ptr(concat!(stringify!($str), "\0").as_ptr() as _) } + }; +} + +#[macro_export] +macro_rules! impl_simple_registry_value_static { + ($($(<$($generics: ident)*>)? ($t: ty) => $v: ty;)*) => { + $( + impl $(<$($generics),*>)? $crate::vm::registry::lua_ref::SimpleRegistryValue for $t { + type Value<'a> = $v; + } + )* + }; +} + +#[macro_export] +macro_rules! impl_registry_value { + ($reg_ty: ty => $value_ty: ident) => { + impl $crate::vm::registry::Value for $reg_ty { + type Value<'a> = $value_ty<'a>; + + #[inline(always)] + unsafe fn from_registry(vm: &Vm, index: i32) -> Self::Value<'_> { + unsafe { $value_ty::from_lua_unchecked(vm, index) } + } + + #[inline(always)] + fn push_registry(value: Self::Value<'_>) -> R { + unsafe { R::from_index(value.vm, value.index()) } + } + + #[inline(always)] + unsafe fn set_registry(key: &impl Set, value: Self::Value<'_>) { + key.set(value.vm, value.index()) + } + } + }; +} + +/// This macro is unsafe and should not be used from safe code directly. It is intended as a +/// building block for other macros. +#[macro_export] +macro_rules! decl_from_param_unchecked { + ( + $vm: ident, $start_index: literal, + ) => { + }; + + ( + $vm: ident, $start_index: literal, $arg_name: ident: $arg_ty: ty + ) => { + use $crate::vm::function::FromParam; + let $arg_name: $arg_ty = unsafe { FromParam::from_param($vm, $start_index) }; + }; + + ( + $vm: ident, $start_index: literal, $($arg_name: ident: $arg_ty: ty)* + ) => { + use $crate::vm::function::FromParam; + let mut index = $start_index; + $crate::decl_from_param_unchecked!(_from_param $vm, index, $(($arg_name: $arg_ty))*); + }; + + (_from_param $vm: ident, $index: ident, ) => { }; + + (_from_param $vm: ident, $index: ident, ($arg_name: ident: $arg_ty: ty)) => { + let $arg_name: $arg_ty = unsafe { FromParam::from_param($vm, $index) }; + }; + + (_from_param $vm: ident, $index: ident, ($arg_name: ident: $arg_ty: ty) $(($arg_name2: ident: $arg_ty2: ty))*) => { + let $arg_name: $arg_ty = unsafe { FromParam::from_param($vm, $index) }; + $index += 1; + $crate::decl_from_param_unchecked!(_from_param $vm, $index, $(($arg_name2: $arg_ty2))*); + }; +} diff --git a/core/src/macro/userdata.rs b/core/src/macro/userdata.rs new file mode 100644 index 0000000..c926c87 --- /dev/null +++ b/core/src/macro/userdata.rs @@ -0,0 +1,144 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[macro_export] +macro_rules! _impl_userdata_static { + ($registry: ident field $field_name: ident = $field_value: expr) => { + $registry.add_field($crate::c_stringify!($field_name), $field_value)?; + }; + ($registry: ident fn $function_name: ident) => { + $registry.add_static_field( + $crate::c_stringify!($function_name), + $crate::vm::function::types::RFunction::wrap($function_name), + )?; + }; +} + +#[macro_export] +macro_rules! _impl_userdata { + ($obj_name: ident, $($fn_name: ident),* { $([$($static_registry_tokens: tt)*];)* }) => { + impl $crate::vm::userdata::UserData for $obj_name { + fn register(registry: &$crate::vm::userdata::core::Registry) -> std::result::Result<(), $crate::vm::userdata::Error> { + $( + let f = $obj_name::$fn_name()?; + registry.add_method(f); + )* + use $crate::vm::userdata::AddGcMethod; + (&$crate::vm::userdata::core::AddGcMethodAuto::<$obj_name>::default()).add_gc_method(registry); + $( + $crate::_impl_userdata_static!(registry $($static_registry_tokens)*); + )* + Ok(()) + } + } + }; +} + +#[macro_export] +macro_rules! decl_userdata { + ( + $(#[$meta: meta])* + $vis: vis struct $name:ident + ) => { + $(#[$meta])* + $vis struct $name; + + unsafe impl $crate::vm::userdata::UserDataType for $name { + const CLASS_NAME: &'static std::ffi::CStr = $crate::c_stringify!($name); + const FULL_TYPE: &'static std::ffi::CStr = unsafe { std::ffi::CStr::from_ptr(concat!(module_path!(), "::", stringify!($name), "\0").as_ptr() as _) }; + } + }; + ( + $(#[$meta: meta])* + $vis: vis struct $name:ident { $($struct_decl: tt)* } + ) => { + $(#[$meta])* + $vis struct $name { $($struct_decl)* } + + unsafe impl $crate::vm::userdata::UserDataType for $name { + const CLASS_NAME: &'static std::ffi::CStr = $crate::c_stringify!($name); + const FULL_TYPE: &'static std::ffi::CStr = unsafe { std::ffi::CStr::from_ptr(concat!(module_path!(), "::", stringify!($name), "\0").as_ptr() as _) }; + } + }; + ( + $(#[$meta: meta])* + $vis: vis struct $name:ident($($struct_decl: tt)*) + ) => { + $(#[$meta])* + $vis struct $name($($struct_decl)*); + + unsafe impl $crate::vm::userdata::UserDataType for $name { + const CLASS_NAME: &'static std::ffi::CStr = $crate::c_stringify!($name); + const FULL_TYPE: &'static std::ffi::CStr = unsafe { std::ffi::CStr::from_ptr(concat!(module_path!(), "::", stringify!($name), "\0").as_ptr() as _) }; + } + }; +} + +#[macro_export] +macro_rules! impl_userdata { + ( + impl $obj_name: ident { + $( + $vis: vis fn $fn_name: ident $(<$lifetime: lifetime>)? ($this: ident: &$obj_name2: ident $($tokens: tt)*) -> $ret_ty: ty $code: block + )* + } + + $(static { $([$($static_registry_tokens: tt)*];)* })? + ) => { + $( + $crate::decl_userdata_func! { + $vis fn $fn_name $(<$lifetime>)? ($this: &$obj_name $($tokens)*) -> $ret_ty $code + } + )* + + $crate::_impl_userdata!($obj_name, $($fn_name),* { $($([$($static_registry_tokens)*];)*)? }); + + unsafe impl $crate::vm::userdata::UserDataImmutable for $obj_name {} + }; +} + +#[macro_export] +macro_rules! impl_userdata_mut { + ( + impl $obj_name: ident { + $( + $vis: vis fn $fn_name: ident $(<$lifetime: lifetime>)? ($($tokens: tt)*) -> $ret_ty: ty $code: block + )* + } + + $(static { $([$($static_registry_tokens: tt)*];)* })? + ) => { + $( + $crate::decl_userdata_func! { + $vis fn $fn_name $(<$lifetime>)? ($($tokens)*) -> $ret_ty $code + } + )* + + $crate::_impl_userdata!($obj_name, $($fn_name),* { $($([$($static_registry_tokens)*];)*)? }); + }; +} diff --git a/core/src/macro/userdata_func.rs b/core/src/macro/userdata_func.rs new file mode 100644 index 0000000..10fea03 --- /dev/null +++ b/core/src/macro/userdata_func.rs @@ -0,0 +1,107 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[macro_export] +macro_rules! decl_userdata_func { + ( + $vis: vis fn $fn_name: ident $(<$lifetime: lifetime>)? ($this: ident: &mut $obj_name: ident$(, $($arg_name: ident: $arg_ty: ty),*)?) -> $ret_ty: ty $code: block + ) => { + impl $obj_name { + $vis fn $fn_name() -> std::result::Result<$crate::vm::userdata::core::Function, $crate::vm::userdata::Error> { + extern "C-unwind" fn _cfunc(l: $crate::ffi::lua::State) -> i32 { + fn _func $(<$lifetime>)? ($this: &mut $obj_name$(, $($arg_name: $arg_ty),*)?) -> $ret_ty $code + use $crate::vm::function::IntoParam; + let this_ptr = unsafe { $crate::ffi::laux::luaL_checkudata(l, 1, <$obj_name as $crate::vm::userdata::UserDataType>::FULL_TYPE.as_ptr()) } as *mut $obj_name; + let vm = unsafe { $crate::vm::Vm::from_raw(l) }; + #[inline(always)] + extern "C-unwind" fn _vmfunc $(<$lifetime>)? (this_ptr: *mut $obj_name, vm: &$($lifetime)? $crate::vm::Vm) -> i32 { + $($crate::decl_from_param_unchecked!(vm, 2, $($arg_name: $arg_ty)*);)? + let ret = _func(unsafe { &mut *this_ptr } $(, $($arg_name),*)?); + ret.into_param(vm) as _ + } + _vmfunc(this_ptr, &vm) + } + let mut f = $crate::vm::userdata::core::Builder::new($crate::c_stringify!($fn_name), _cfunc); + f.mutable(); + f.arg::<&$obj_name>(); + $($(f.arg::<$arg_ty>();)*)? + unsafe { f.build() } + } + } + }; + ( + $vis: vis fn $fn_name: ident $(<$lifetime: lifetime>)? ($this: ident: &$obj_name: ident, $name: ident: &Vm$(, $($arg_name: ident: $arg_ty: ty),*)?) -> $ret_ty: ty $code: block + ) => { + impl $obj_name { + $vis fn $fn_name() -> std::result::Result<$crate::vm::userdata::core::Function, $crate::vm::userdata::Error> { + extern "C-unwind" fn _cfunc(l: $crate::ffi::lua::State) -> i32 { + fn _func $(<$lifetime>)? ($this: & $($lifetime)? $obj_name, $name: &$($lifetime)? $crate::vm::Vm$(, $($arg_name: $arg_ty),*)?) -> $ret_ty $code + use $crate::vm::function::IntoParam; + let this_ptr = unsafe { $crate::ffi::laux::luaL_checkudata(l, 1, <$obj_name as $crate::vm::userdata::UserDataType>::FULL_TYPE.as_ptr()) } as *const $obj_name; + let vm = unsafe { $crate::vm::Vm::from_raw(l) }; + #[inline(always)] + extern "C-unwind" fn _vmfunc $(<$lifetime>)? (this_ptr: *const $obj_name, vm: &$($lifetime)? $crate::vm::Vm) -> i32 { + $($crate::decl_from_param_unchecked!(vm, 2, $($arg_name: $arg_ty)*);)? + let ret = _func(unsafe { &*this_ptr }, vm $(, $($arg_name),*)?); + ret.into_param(vm) as _ + } + _vmfunc(this_ptr, &vm) + } + let mut f = $crate::vm::userdata::core::Builder::new($crate::c_stringify!($fn_name), _cfunc); + f.arg::<&$obj_name>(); + $($(f.arg::<$arg_ty>();)*)? + unsafe { f.build() } + } + } + }; + ( + $vis: vis fn $fn_name: ident $(<$lifetime: lifetime>)? ($this: ident: &$obj_name: ident$(, $($arg_name: ident: $arg_ty: ty),*)?) -> $ret_ty: ty $code: block + ) => { + impl $obj_name { + $vis fn $fn_name() -> std::result::Result<$crate::vm::userdata::core::Function, $crate::vm::userdata::Error> { + extern "C-unwind" fn _cfunc(l: $crate::ffi::lua::State) -> i32 { + fn _func $(<$lifetime>)? ($this: & $($lifetime)? $obj_name$(, $($arg_name: $arg_ty),*)?) -> $ret_ty $code + use $crate::vm::function::IntoParam; + let this_ptr = unsafe { $crate::ffi::laux::luaL_checkudata(l, 1, <$obj_name as $crate::vm::userdata::UserDataType>::FULL_TYPE.as_ptr()) } as *const $obj_name; + let vm = unsafe { $crate::vm::Vm::from_raw(l) }; + #[inline(always)] + extern "C-unwind" fn _vmfunc $(<$lifetime>)? (this_ptr: *const $obj_name, vm: &$($lifetime)? $crate::vm::Vm) -> i32 { + $($crate::decl_from_param_unchecked!(vm, 2, $($arg_name: $arg_ty)*);)? + let ret = _func(unsafe { &*this_ptr } $(, $($arg_name),*)?); + ret.into_param(vm) as _ + } + _vmfunc(this_ptr, &vm) + } + let mut f = $crate::vm::userdata::core::Builder::new($crate::c_stringify!($fn_name), _cfunc); + f.arg::<&$obj_name>(); + $($(f.arg::<$arg_ty>();)*)? + unsafe { f.build() } + } + } + }; +} diff --git a/core/src/module/error.rs b/core/src/module/error.rs new file mode 100644 index 0000000..ebb6b93 --- /dev/null +++ b/core/src/module/error.rs @@ -0,0 +1,116 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::Type; +use std::ffi::c_char; + +pub const STRING_BUF_LEN: usize = 4096; + +#[derive(Copy, Clone)] +#[repr(i32)] +pub enum ErrorType { + None = 0, + Utf8 = -1, + Type = -2, + Syntax = -3, + Runtime = -4, + Memory = -5, + Unknown = -6, + Error = -7, + Null = -8, + MultiValue = -9, + UnsupportedType = -10, + Loader = -11, + ParseFloat = -12, + ParseInt = -13, + UncatchableRuntime = -14, + BadThreadState = -15, + UserDataArgsEmpty = 1, + UserDataMutViolation = 2, + UserDataGc = 3, + UserDataMetatable = 5, + UserDataMultiValueField = 6, + UserDataAlreadyRegistered = 7, + UserDataAlignment = 8, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct Utf8Error { + pub ty: ErrorType, + pub valid_up_to: usize, + pub error_len: i16, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct TypeError { + pub ty: ErrorType, + pub expected: Type, + pub actual: Type, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct UnsupportedType { + pub ty: ErrorType, + pub actual: Type, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct String { + pub ty: ErrorType, + pub data: [u8; STRING_BUF_LEN], + pub len: usize, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct StaticString { + pub ty: ErrorType, + pub data: *const c_char, +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct Alignment { + pub ty: ErrorType, + pub alignment: usize, +} + +#[repr(C)] +pub union Error { + pub ty: ErrorType, + pub string: String, + pub type_mismatch: TypeError, + pub utf8: Utf8Error, + pub unsupported_type: UnsupportedType, + pub static_string: StaticString, + pub alignment: Alignment, +} diff --git a/core/src/module/mod.rs b/core/src/module/mod.rs new file mode 100644 index 0000000..dda8fc0 --- /dev/null +++ b/core/src/module/mod.rs @@ -0,0 +1,132 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod error; + +/// The BP3D Lua & LuaJIT version. +pub static VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// The version of the time library used by bp3d-lua. +pub static TIME_VERSION: &str = "0.3.41"; + +/// The macro which generates a plugin entry point. +pub use bp3d_lua_codegen::decl_lua_plugin; + +/// Helper function to run the [register](crate::libs::Lib::register) function of a +/// [Lib](crate::libs::Lib). +/// +/// This function automatically translates the Rust result type to the C FFI compatible type. +pub fn run_lua_register( + vm: &crate::vm::Vm, + lib: impl crate::libs::Lib, + error: &mut error::Error, +) -> bool { + use crate::vm::error::Error; + use bp3d_util::format::MemBufStr; + use std::fmt::Write; + let res = lib.register(vm); + match res { + Ok(()) => true, + Err(e) => { + match e { + Error::InvalidUtf8(e) => { + error.ty = error::ErrorType::Utf8; + // Option is not FFI safe, so use i16. + error.utf8.error_len = e.error_len().map(|v| v as i16).unwrap_or(-1); + error.utf8.valid_up_to = e.valid_up_to(); + } + Error::Type(e) => { + error.ty = error::ErrorType::Type; + error.type_mismatch.actual = e.actual; + error.type_mismatch.expected = e.expected; + } + Error::Syntax(e) => { + error.ty = error::ErrorType::Syntax; + let mut msg = + unsafe { MemBufStr::wrap(&mut error.string.len, &mut error.string.data) }; + let _ = write!(msg, "{}", e); + } + Error::Runtime(e) => { + error.ty = error::ErrorType::Runtime; + let mut msg = + unsafe { MemBufStr::wrap(&mut error.string.len, &mut error.string.data) }; + let _ = write!(msg, "{}", e); + } + Error::Memory => error.ty = error::ErrorType::Memory, + Error::Unknown => error.ty = error::ErrorType::Unknown, + Error::Error => error.ty = error::ErrorType::Error, + Error::Null => error.ty = error::ErrorType::Null, + Error::MultiValue => error.ty = error::ErrorType::MultiValue, + Error::UserData(e) => match e { + crate::vm::userdata::Error::ArgsEmpty => { + error.ty = error::ErrorType::UserDataArgsEmpty + } + crate::vm::userdata::Error::MutViolation(e) => { + error.ty = error::ErrorType::UserDataMutViolation; + error.static_string.data = e.as_ptr(); + } + crate::vm::userdata::Error::Gc => error.ty = error::ErrorType::UserDataGc, + crate::vm::userdata::Error::Metatable => { + error.ty = error::ErrorType::UserDataMetatable + } + crate::vm::userdata::Error::MultiValueField => { + error.ty = error::ErrorType::UserDataMultiValueField + } + crate::vm::userdata::Error::AlreadyRegistered(e) => { + error.ty = error::ErrorType::UserDataAlreadyRegistered; + error.static_string.data = e.as_ptr(); + } + crate::vm::userdata::Error::Alignment(e) => { + error.ty = error::ErrorType::UserDataAlignment; + error.alignment.alignment = e; + } + }, + Error::UnsupportedType(e) => { + error.ty = error::ErrorType::UnsupportedType; + error.unsupported_type.actual = e; + } + Error::Loader(e) => { + error.ty = error::ErrorType::Loader; + let mut msg = + unsafe { MemBufStr::wrap(&mut error.string.len, &mut error.string.data) }; + let _ = write!(msg, "{}", e); + } + Error::ParseInt => error.ty = error::ErrorType::ParseInt, + Error::ParseFloat => error.ty = error::ErrorType::ParseFloat, + Error::UncatchableRuntime(e) => { + error.ty = error::ErrorType::UncatchableRuntime; + let mut msg = + unsafe { MemBufStr::wrap(&mut error.string.len, &mut error.string.data) }; + let _ = write!(msg, "{}", e); + } + Error::BadThreadState => error.ty = error::ErrorType::BadThreadState, + } + false + } + } +} diff --git a/core/src/util/core.rs b/core/src/util/core.rs new file mode 100644 index 0000000..4a7ba0a --- /dev/null +++ b/core/src/util/core.rs @@ -0,0 +1,84 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! Core rust utilities module. + +use core::fmt; +use std::borrow::Cow; +use std::error::Error; +use std::ffi::{CStr, CString, OsStr}; +use std::fmt::Display; +use std::path::Path; + +pub trait AnyStr { + fn to_str(&self) -> crate::vm::Result>; +} + +impl AnyStr for &str { + fn to_str(&self) -> crate::vm::Result> { + Ok(Cow::Owned( + CString::new(&**self).map_err(|_| crate::vm::error::Error::Null)?, + )) + } +} + +impl AnyStr for &CStr { + #[inline(always)] + fn to_str(&self) -> crate::vm::Result> { + Ok(Cow::Borrowed(&**self)) + } +} + +/// Represents a type which can be trivially dropped (i.e. no Drop implementation). +/// +/// # Safety +/// +/// This is UB to implement this trait on types which are not trivially dropped. +pub unsafe trait SimpleDrop {} + +unsafe impl SimpleDrop for *mut T {} +unsafe impl SimpleDrop for *const T {} +unsafe impl SimpleDrop for bool {} +unsafe impl SimpleDrop for &str {} +unsafe impl SimpleDrop for Option {} +unsafe impl SimpleDrop for Result {} +unsafe impl SimpleDrop for &T {} +unsafe impl SimpleDrop for &[u8] {} +unsafe impl SimpleDrop for &OsStr {} +unsafe impl SimpleDrop for &Path {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct TryFromIntError; + +impl Display for TryFromIntError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.write_str("out of range integral type conversion attempted") + } +} + +impl Error for TryFromIntError {} diff --git a/core/src/util/function.rs b/core/src/util/function.rs new file mode 100644 index 0000000..9a30d16 --- /dev/null +++ b/core/src/util/function.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::core::util::{pcall, push_error_handler}; +use crate::vm::registry::core::Key; +use crate::vm::registry::types::Function; +use crate::vm::value::{FromLua, IntoLua}; +use crate::vm::Vm; + +/// This represents a Lua callback. +pub struct LuaFunction(Key); + +impl LuaFunction { + pub fn create(f: crate::vm::value::types::Function) -> Self { + Self(Key::new(f)) + } + + pub fn call<'a, R: FromLua<'a>>( + &self, + vm: &'a Vm, + value: impl IntoLua, + ) -> crate::vm::Result { + let pos = unsafe { push_error_handler(vm.as_ptr()) }; + unsafe { self.0.as_raw().push(vm) }; + let num_values = value.into_lua(vm); + unsafe { pcall(vm, num_values as _, R::num_values() as _, pos)? }; + R::from_lua(vm, -(R::num_values() as i32)) + } + + #[inline(always)] + pub fn delete(self, vm: &Vm) { + self.0.delete(vm) + } +} diff --git a/core/src/util/method.rs b/core/src/util/method.rs new file mode 100644 index 0000000..09bcdb5 --- /dev/null +++ b/core/src/util/method.rs @@ -0,0 +1,70 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::util::core::AnyStr; +use crate::vm::core::util::{pcall, push_error_handler}; +use crate::vm::registry::core::Key; +use crate::vm::registry::types::{Function, Table}; +use crate::vm::value::{FromLua, IntoLua}; +use crate::vm::Vm; + +pub struct LuaMethod { + obj: Key
, + method: Key, +} + +impl LuaMethod { + pub fn create( + obj: crate::vm::table::Table, + method_name: impl AnyStr, + ) -> crate::vm::Result { + let method: crate::vm::value::types::Function = obj.get(method_name)?; + Ok(Self { + method: Key::new(method), + obj: Key::new(obj), + }) + } + + pub fn call<'a, R: FromLua<'a>>( + &self, + vm: &'a Vm, + value: impl IntoLua, + ) -> crate::vm::Result { + let pos = unsafe { push_error_handler(vm.as_ptr()) }; + unsafe { self.method.as_raw().push(vm) }; + unsafe { self.obj.as_raw().push(vm) }; + let num_values = value.into_lua(vm); + unsafe { pcall(vm, (num_values + 1) as _, R::num_values() as _, pos)? }; + R::from_lua(vm, -(R::num_values() as i32)) + } + + pub fn delete(self, vm: &Vm) { + self.method.delete(vm); + self.obj.delete(vm); + } +} diff --git a/core/src/util/mod.rs b/core/src/util/mod.rs new file mode 100644 index 0000000..8952a04 --- /dev/null +++ b/core/src/util/mod.rs @@ -0,0 +1,46 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod core; +mod function; +#[cfg(feature = "util-method")] +mod method; +#[cfg(feature = "util-module")] +pub mod module; +#[cfg(feature = "util-namespace")] +mod namespace; +#[cfg(feature = "util-thread")] +pub mod thread; + +pub use function::LuaFunction; +#[cfg(feature = "util-method")] +pub use method::LuaMethod; +#[cfg(feature = "util-namespace")] +pub use namespace::Namespace; +#[cfg(feature = "util-thread")] +pub use thread::LuaThread; diff --git a/core/src/util/module.rs b/core/src/util/module.rs new file mode 100644 index 0000000..6e8da3c --- /dev/null +++ b/core/src/util/module.rs @@ -0,0 +1,235 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::module::error::ErrorType; +use crate::module::{TIME_VERSION, VERSION}; +use crate::vm::error::{RuntimeError, TypeError, Utf8Error}; +use crate::vm::Vm; +use bp3d_debug::{error, info}; +use bp3d_os::module::library::Library; +use bp3d_os::module::loader::ModuleHandle; +use bp3d_os::module::loader::ModuleLoader; +use bp3d_os::module::Module; +use bp3d_util::simple_error; +use std::collections::HashSet; +use std::ffi::CStr; + +simple_error! { + pub Error { + PluginAlreadyLoaded(String) => "plugin {} is already loaded", + LibNotFound(String) => "library not found: {}", + PluginNotFound(String) => "plugin not found: {}", + NotRegistered => "ModuleManager not registered", + (impl From)Vm(crate::vm::error::Error) => "vm error: {}", + (impl From)Module(bp3d_os::module::error::Error) => "module error: {}" + } +} + +pub type Result = std::result::Result; + +type PluginFunc = + extern "C" fn(l: crate::ffi::lua::State, error: *mut crate::module::error::Error) -> bool; + +unsafe fn get_string( + err: &crate::module::error::Error, + f: impl FnOnce(String) -> crate::vm::error::Error, +) -> crate::vm::error::Error { + match std::str::from_utf8(&err.string.data[..err.string.len]) { + Ok(v) => f(v.into()), + Err(e) => crate::vm::error::Error::InvalidUtf8(e.into()), + } +} + +unsafe fn convert_module_error_to_vm_error( + err: crate::module::error::Error, +) -> crate::vm::error::Error { + match err.ty { + ErrorType::Utf8 => crate::vm::error::Error::InvalidUtf8(Utf8Error { + valid_up_to: err.utf8.valid_up_to, + error_len: if err.utf8.error_len < 0 { + None + } else { + Some(err.utf8.error_len as u8) + }, + }), + ErrorType::Type => crate::vm::error::Error::Type(TypeError { + expected: err.type_mismatch.expected, + actual: err.type_mismatch.actual, + }), + ErrorType::Syntax => get_string(&err, crate::vm::error::Error::Syntax), + ErrorType::Runtime => get_string(&err, |v| { + crate::vm::error::Error::Runtime(RuntimeError::new(v)) + }), + ErrorType::UncatchableRuntime => get_string(&err, |v| { + crate::vm::error::Error::UncatchableRuntime(RuntimeError::new(v)) + }), + ErrorType::Memory => crate::vm::error::Error::Memory, + ErrorType::Unknown => crate::vm::error::Error::Unknown, + ErrorType::Error => crate::vm::error::Error::Error, + ErrorType::Null => crate::vm::error::Error::Null, + ErrorType::MultiValue => crate::vm::error::Error::MultiValue, + ErrorType::UnsupportedType => { + crate::vm::error::Error::UnsupportedType(err.unsupported_type.actual) + } + ErrorType::Loader => get_string(&err, crate::vm::error::Error::Loader), + ErrorType::ParseFloat => crate::vm::error::Error::ParseFloat, + ErrorType::ParseInt => crate::vm::error::Error::ParseInt, + ErrorType::UserDataArgsEmpty => { + crate::vm::error::Error::UserData(crate::vm::userdata::Error::ArgsEmpty) + } + ErrorType::UserDataMutViolation => crate::vm::error::Error::UserData( + crate::vm::userdata::Error::MutViolation(CStr::from_ptr(err.static_string.data)), + ), + ErrorType::UserDataGc => crate::vm::error::Error::UserData(crate::vm::userdata::Error::Gc), + ErrorType::UserDataMetatable => { + crate::vm::error::Error::UserData(crate::vm::userdata::Error::Metatable) + } + ErrorType::UserDataMultiValueField => { + crate::vm::error::Error::UserData(crate::vm::userdata::Error::MultiValueField) + } + ErrorType::UserDataAlreadyRegistered => crate::vm::error::Error::UserData( + crate::vm::userdata::Error::AlreadyRegistered(CStr::from_ptr(err.static_string.data)), + ), + ErrorType::UserDataAlignment => crate::vm::error::Error::UserData( + crate::vm::userdata::Error::Alignment(err.alignment.alignment), + ), + ErrorType::None => std::hint::unreachable_unchecked(), + ErrorType::BadThreadState => crate::vm::error::Error::BadThreadState, + } +} + +pub struct ModuleManager { + set: HashSet, +} + +// This is safe because ModuleManager does not use thread locals or mutable globals of some kind. +// The hard work is mostly done by syscalls or static read-only memory. +unsafe impl Send for ModuleManager {} + +impl ModuleManager { + fn load_plugin( + vm: &Vm, + module: &Module, + name: &str, + lib: &str, + plugin: &str, + ) -> Result<()> { + let func_name = format!("bp3d_lua_{}_register_{}", lib, plugin); + let sym = unsafe { module.lib().load_symbol::(func_name) }? + .ok_or_else(|| Error::PluginNotFound(name.into()))?; + let mut err = crate::module::error::Error { + ty: ErrorType::None, + }; + if !sym.call(vm.as_ptr(), &mut err) { + return Err(Error::Vm(unsafe { convert_module_error_to_vm_error(err) })); + } + Ok(()) + } + + fn load_dynamic(&mut self, lib: &str, plugin: &str, vm: &Vm) -> Result<()> { + let name = format!("{}::{}", lib, plugin); + if self.set.contains(&name) { + return Err(Error::PluginAlreadyLoaded(name)); + } + let mut lock = ModuleLoader::lock(); + let module = unsafe { lock.load(lib) }?; + info!( + "Loaded dynamic module {:?}-{:?}", + module.get().get_metadata_key("NAME"), + module.get().get_metadata_key("VERSION") + ); + Self::load_plugin(vm, module.get(), &name, &lib.replace("-", "_"), plugin)?; + info!("Loaded plugin {}", name); + self.set.insert(name); + Ok(()) + } + + fn load_builtin(&mut self, lib: &str, plugin: &str, vm: &Vm) -> Result { + let name = format!("{}::{}", lib, plugin); + if self.set.contains(&name) { + return Err(Error::PluginAlreadyLoaded(name)); + } + let mut lock = ModuleLoader::lock(); + let module = match unsafe { lock.load_builtin(lib) } { + Ok(v) => v, + Err(e) => { + return match e { + bp3d_os::module::error::Error::NotFound(_) => Ok(false), + e => Err(Error::Module(e)), + } + } + }; + info!( + "Loaded builtin module {:?}-{:?}", + module.get().get_metadata_key("NAME"), + module.get().get_metadata_key("VERSION") + ); + Self::load_plugin(vm, module.get(), &name, &lib.replace("-", "_"), plugin)?; + info!("Loaded plugin {}", name); + self.set.insert(name); + Ok(true) + } + + pub fn load(&mut self, lib: &str, plugin: &str, vm: &Vm) -> Result<()> { + if !self.load_builtin(lib, plugin, vm)? { + self.load_dynamic(lib, plugin, vm)?; + } + Ok(()) + } + + pub fn new() -> Self { + let mut lock = ModuleLoader::lock(); + #[cfg(feature = "send")] + lock.add_public_dependency("bp3d-lua", VERSION, ["send"]); + #[cfg(not(feature = "send"))] + lock.add_public_dependency("bp3d-lua", VERSION, ["-send"]); + lock.add_public_dependency("time", TIME_VERSION, ["*"]); + Self { + set: Default::default(), + } + } +} + +impl Default for ModuleManager { + fn default() -> Self { + Self::new() + } +} + +impl Drop for ModuleManager { + fn drop(&mut self) { + for plugin in self.set.drain() { + let mut it = plugin.split("::"); + let lib = it.next().unwrap(); + let plugin = it.next().unwrap(); + if let Err(e) = ModuleLoader::lock().unload(lib) { + error!("Failed to unload plugin {}::{}: {}", lib, plugin, e); + } + } + } +} diff --git a/core/src/util/namespace.rs b/core/src/util/namespace.rs new file mode 100644 index 0000000..bbe0c60 --- /dev/null +++ b/core/src/util/namespace.rs @@ -0,0 +1,123 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::lua_settop; +use crate::util::core::AnyStr; +use crate::vm::registry::core::Key; +use crate::vm::table::Table; +use crate::vm::userdata::util::get_static_table; +use crate::vm::userdata::{NameConvert, UserData}; +use crate::vm::value::IntoLua; +use crate::vm::Vm; +use bp3d_debug::info; + +pub struct Namespace<'a> { + vm: &'a Vm, + table: Table<'a>, +} + +impl<'a> Namespace<'a> { + fn from_table<'b>( + vm: &'a Vm, + table: Table<'a>, + names: impl Iterator, + ) -> crate::vm::Result { + let key = Key::::new(table); + let key = vm.scope(|vm| { + for name in names { + let mut table = key.push(vm); + let tbl: Option
= table.get(name)?; + let tab = match tbl { + Some(v) => v, + None => { + table.set(name, Table::new(vm))?; + table.get(name)? + } + }; + key.set(tab); + } + Ok(key) + })?; + let table = key.push(vm); + key.delete(vm); + Ok(Self { vm, table }) + } + + pub fn new(vm: &'a Vm, path: &str) -> crate::vm::Result { + let mut names = path.split("."); + let name = names.next().expect("Attempt to build an empty namespace"); + let value: Option> = vm.get_global(name)?; + let table = match value { + Some(table) => table, + None => { + vm.set_global(name, Table::new(vm))?; + vm.get_global(name)? + } + }; + Self::from_table(vm, table, names) + } + + pub fn add<'b, T: IntoLua>( + &mut self, + items: impl IntoIterator, + ) -> crate::vm::Result<()> { + for (name, item) in items { + self.table.set(name, item)?; + } + Ok(()) + } + + pub fn add_userdata( + &mut self, + name: impl AnyStr, + case: impl NameConvert, + ) -> crate::vm::Result<()> { + info!( + "Adding userdata type {:?} as {:?}", + T::CLASS_NAME, + name.to_str()? + ); + self.vm.register_userdata::(case)?; + self.table.set( + name, + get_static_table::(self.vm).map(|v| unsafe { v.to_table() }), + )?; + Ok(()) + } + + pub fn vm(&self) -> &'a Vm { + self.vm + } +} + +impl Drop for Namespace<'_> { + fn drop(&mut self) { + // Clear the table which should be on top of the stack. + unsafe { lua_settop(self.vm.as_ptr(), -2) }; + } +} diff --git a/core/src/util/thread.rs b/core/src/util/thread.rs new file mode 100644 index 0000000..e7847d9 --- /dev/null +++ b/core/src/util/thread.rs @@ -0,0 +1,57 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::registry::core::Key; +use crate::vm::Vm; + +pub struct LuaThread { + key: Key, + thread: crate::vm::thread::core::Thread<'static>, +} + +impl LuaThread { + pub fn create(value: crate::vm::thread::value::Thread) -> Self { + let thread = + unsafe { crate::vm::thread::core::Thread::from_raw(value.as_thread().as_ptr()) }; + Self { + key: Key::new(value), + thread, + } + } + + //TODO: Check if this is indeed safe. + #[inline(always)] + pub fn as_thread(&self) -> &crate::vm::thread::core::Thread<'static> { + &self.thread + } + + #[inline(always)] + pub fn delete(self, vm: &Vm) { + self.key.delete(vm) + } +} diff --git a/core/src/vm/closure/arc.rs b/core/src/vm/closure/arc.rs new file mode 100644 index 0000000..4061c6b --- /dev/null +++ b/core/src/vm/closure/arc.rs @@ -0,0 +1,76 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::util::core::SimpleDrop; +use crate::vm::closure::{FromUpvalue, IntoUpvalue, Upvalue}; +use crate::vm::value::types::RawPtr; +use crate::vm::Vm; +use std::ops::Deref; + +pub type Shared = std::sync::Arc; + +#[repr(transparent)] +pub struct Arc(*const T); + +#[repr(transparent)] +pub struct Ref<'a, T: Send + Sync>(&'a T); + +unsafe impl SimpleDrop for Ref<'_, T> {} + +impl Deref for Ref<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl<'a, T: Send + Sync> FromUpvalue<'a> for Ref<'a, T> { + #[inline(always)] + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self { + Ref(&*RawPtr::::from_upvalue(vm, index).as_ptr()) + } +} + +impl Upvalue for Arc { + type From<'a> = crate::vm::closure::rc::Ref<'a, T>; +} + +impl IntoUpvalue for Arc { + #[inline(always)] + fn into_upvalue(self, vm: &Vm) -> u16 { + RawPtr::new(self.0 as *mut T).into_upvalue(vm) + } +} + +impl Arc { + #[inline(always)] + pub fn from_rust(vm: &Vm, rc: Shared) -> Arc { + Arc(crate::vm::core::destructor::Pool::attach_send(vm, rc)) + } +} diff --git a/core/src/vm/closure/context.rs b/core/src/vm/closure/context.rs new file mode 100644 index 0000000..0195dc7 --- /dev/null +++ b/core/src/vm/closure/context.rs @@ -0,0 +1,315 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! Second version of the context tool. + +use crate::ffi::laux::luaL_error; +use crate::ffi::lua::lua_newuserdata; +use crate::util::core::SimpleDrop; +use crate::vm::closure::{FromUpvalue, IntoUpvalue, Upvalue}; +use crate::vm::registry::core::RawKey; +use crate::vm::value::types::RawPtr; +use crate::vm::Vm; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; + +pub struct Cell { + ptr: *mut *const T, +} + +#[cfg(feature = "send")] +impl Cell { + pub fn new(ctx: Context) -> Self { + Self { ptr: ctx.ptr } + } +} + +#[cfg(not(feature = "send"))] +impl Cell { + pub fn new(ctx: Context) -> Self { + Self { ptr: ctx.ptr } + } +} + +impl Cell { + pub fn bind<'a>(&mut self, obj: &'a T) -> Guard<'a, T> { + unsafe { *self.ptr = obj as _ }; + Guard { + useless: PhantomData, + ud: self.ptr, + } + } + + /// Unsafely binds a reference to this [Cell]. + /// + /// # Arguments + /// + /// * `obj`: the reference to the context object. + /// + /// returns: () + /// + /// # Safety + /// + /// The given object must be valid until a call to unbind, the [bind](Cell::bind) function must + /// also never be called until a call to [unbind_unchecked](Cell::unbind_unchecked). If any of + /// these constrains are not satisfied, this function is UB. + #[inline(always)] + pub unsafe fn bind_unchecked(&mut self, obj: &T) { + *self.ptr = obj as _; + } + + /// Unsafely unbinds the current reference bound in this [Cell]. + /// + /// # Safety + /// + /// No [Guard] should exist at this point otherwise this is UB. + #[inline(always)] + pub unsafe fn unbind_unchecked(&mut self) { + *self.ptr = std::ptr::null(); + } +} + +pub struct CellMut { + ptr: *mut *const T, +} + +#[cfg(feature = "send")] +impl CellMut { + pub fn new(ctx: ContextMut) -> Self { + Self { ptr: ctx.0.ptr } + } +} + +#[cfg(not(feature = "send"))] +impl CellMut { + pub fn new(ctx: ContextMut) -> Self { + Self { ptr: ctx.0.ptr } + } +} + +impl CellMut { + pub fn bind<'a>(&mut self, obj: &'a mut T) -> Guard<'a, T> { + unsafe { *self.ptr = obj as _ }; + Guard { + useless: PhantomData, + ud: self.ptr, + } + } + + /// Unsafely binds a reference to this [CellMut]. + /// + /// # Arguments + /// + /// * `obj`: the reference to the context object. + /// + /// returns: () + /// + /// # Safety + /// + /// The given object must be valid until a call to unbind, the [bind](CellMut::bind) function must + /// also never be called until a call to [unbind_unchecked](CellMut::unbind_unchecked). If any of + /// these constrains are not satisfied, this function is UB. + #[inline(always)] + pub unsafe fn bind_unchecked(&mut self, obj: &mut T) { + *self.ptr = obj as _; + } + + /// Unsafely unbinds the current reference bound in this [CellMut]. + /// + /// # Safety + /// + /// No [Guard] should exist at this point otherwise this is UB. + #[inline(always)] + pub unsafe fn unbind_unchecked(&mut self) { + *self.ptr = std::ptr::null(); + } +} + +pub struct Context { + key: RawKey, + ptr: *mut *const T, +} + +impl Clone for Context { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Context {} + +pub struct ContextMut(Context); + +impl Clone for ContextMut { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for ContextMut {} + +impl Context { + pub fn new(vm: &Vm) -> Self { + let (ptr, key) = unsafe { + let ptr = lua_newuserdata(vm.as_ptr(), 8); + std::ptr::write(ptr as *mut u64, 0); + (ptr, RawKey::from_top(vm)) + }; + Self { + key, + ptr: ptr as *mut *const T, + } + } +} + +impl ContextMut { + pub fn new(vm: &Vm) -> Self { + Self(Context::new(vm)) + } +} + +#[repr(transparent)] +pub struct Guard<'a, T> { + ud: *mut *const T, + useless: PhantomData<&'a T>, +} + +impl Drop for Guard<'_, T> { + #[inline(always)] + fn drop(&mut self) { + unsafe { + *self.ud = std::ptr::null(); + } + } +} + +#[repr(transparent)] +pub struct Ref<'a, T>(&'a T); + +#[repr(transparent)] +pub struct MutPtr(*mut *mut T); + +impl MutPtr { + pub fn borrow<'a>(self) -> Mut<'a, T> { + let value = unsafe { &mut **self.0 }; + unsafe { *self.0 = std::ptr::null_mut() }; + Mut { value, ptr: self.0 } + } +} + +impl Deref for Ref<'_, T> { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.0 + } +} + +pub struct Mut<'a, T> { + value: &'a mut T, + ptr: *mut *mut T, +} + +impl<'a, T> Drop for Mut<'a, T> { + #[inline(always)] + fn drop(&mut self) { + unsafe { *self.ptr = self.value as *mut _ }; + } +} + +impl Deref for Mut<'_, T> { + type Target = T; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.value + } +} + +impl DerefMut for Mut<'_, T> { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + self.value + } +} + +unsafe impl SimpleDrop for Ref<'_, T> {} +unsafe impl SimpleDrop for MutPtr {} + +impl<'a, T: 'static> FromUpvalue<'a> for Ref<'a, T> { + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self { + let ptr: RawPtr<*const T> = FromUpvalue::from_upvalue(vm, index); + if (*ptr.as_ptr()).is_null() { + luaL_error( + vm.as_ptr(), + c"Context is not available in this function.".as_ptr(), + ); + // luaL_error raises a lua exception and unwinds, so this cannot be reached. + std::hint::unreachable_unchecked(); + } + Ref(&**ptr.as_ptr()) + } +} + +impl<'a, T: 'static> FromUpvalue<'a> for MutPtr { + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self { + let ptr: RawPtr<*mut T> = FromUpvalue::from_upvalue(vm, index); + if (*ptr.as_ptr()).is_null() { + luaL_error( + vm.as_ptr(), + c"Context is not available in this function.".as_ptr(), + ); + // luaL_error raises a lua exception and unwinds, so this cannot be reached. + std::hint::unreachable_unchecked(); + } + MutPtr(ptr.as_mut_ptr()) + } +} + +impl IntoUpvalue for Context { + fn into_upvalue(self, vm: &Vm) -> u16 { + unsafe { self.key.push(vm) }; + 1 + } +} + +impl IntoUpvalue for ContextMut { + fn into_upvalue(self, vm: &Vm) -> u16 { + unsafe { self.0.key.push(vm) }; + 1 + } +} + +impl Upvalue for Context { + type From<'a> = Ref<'a, T>; +} + +impl Upvalue for ContextMut { + type From<'a> = MutPtr; +} diff --git a/core/src/vm/closure/core.rs b/core/src/vm/closure/core.rs new file mode 100644 index 0000000..614926f --- /dev/null +++ b/core/src/vm/closure/core.rs @@ -0,0 +1,161 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::GLOBALSINDEX; +use crate::vm::closure::{FromUpvalue, IntoUpvalue, Upvalue}; +use crate::vm::function::IntoParam; +use crate::vm::userdata::AnyUserData; +use crate::vm::value::types::RawPtr; +use crate::vm::value::{FromLua, IntoLua}; +use crate::vm::Vm; +use std::ffi::OsStr; +use std::path::Path; + +macro_rules! impl_from_upvalue_using_from_lua_unchecked { + ($($t: ty),*) => { + $( + impl FromUpvalue<'_> for $t { + #[inline(always)] + unsafe fn from_upvalue(vm: &Vm, index: i32) -> Self { + <$t>::from_lua_unchecked(vm, GLOBALSINDEX - index) + } + } + + impl Upvalue for $t { + type From<'a> = $t; + } + )* + }; +} + +impl<'a> FromUpvalue<'a> for &'a str { + #[inline(always)] + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self { + FromLua::from_lua_unchecked(vm, GLOBALSINDEX - index) + } +} + +impl Upvalue for &str { + type From<'a> = &'a str; +} + +impl<'a> FromUpvalue<'a> for &'a [u8] { + #[inline(always)] + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self { + FromLua::from_lua_unchecked(vm, GLOBALSINDEX - index) + } +} + +impl Upvalue for &[u8] { + type From<'a> = &'a [u8]; +} + +#[cfg(target_pointer_width = "64")] +impl_from_upvalue_using_from_lua_unchecked!(i64, u64); + +impl_from_upvalue_using_from_lua_unchecked!(i8, u8, i16, u16, i32, u32, f32, f64, bool); + +impl FromUpvalue<'_> for RawPtr { + #[inline(always)] + unsafe fn from_upvalue(vm: &Vm, index: i32) -> Self { + RawPtr::from_lua(vm, GLOBALSINDEX - index) + } +} + +impl IntoUpvalue for T { + #[inline(always)] + fn into_upvalue(self, vm: &Vm) -> u16 { + self.into_param(vm) as _ + } +} + +impl IntoUpvalue for RawPtr { + fn into_upvalue(self, vm: &Vm) -> u16 { + self.into_lua(vm); + 1 + } +} + +impl Upvalue for RawPtr { + type From<'a> = RawPtr; +} + +impl<'a> FromUpvalue<'a> for &'a OsStr { + #[inline(always)] + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self { + OsStr::from_encoded_bytes_unchecked(FromUpvalue::from_upvalue(vm, index)) + } +} + +impl IntoUpvalue for &OsStr { + #[inline(always)] + fn into_upvalue(self, vm: &Vm) -> u16 { + self.as_encoded_bytes().into_upvalue(vm) + } +} + +impl Upvalue for &OsStr { + type From<'a> = &'a OsStr; +} + +impl<'a> FromUpvalue<'a> for &'a Path { + #[inline(always)] + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self { + Path::new(OsStr::from_encoded_bytes_unchecked( + FromUpvalue::from_upvalue(vm, index), + )) + } +} + +impl IntoUpvalue for &Path { + #[inline(always)] + fn into_upvalue(self, vm: &Vm) -> u16 { + self.as_os_str().into_upvalue(vm) + } +} + +impl Upvalue for &Path { + type From<'a> = &'a Path; +} + +impl<'a> FromUpvalue<'a> for AnyUserData<'a> { + #[inline(always)] + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self { + AnyUserData::from_raw(vm, GLOBALSINDEX - index) + } +} + +impl IntoUpvalue for AnyUserData<'_> { + fn into_upvalue(self, vm: &Vm) -> u16 { + self.into_lua(vm) + } +} + +impl Upvalue for AnyUserData<'_> { + type From<'a> = AnyUserData<'a>; +} diff --git a/core/src/vm/closure/interface.rs b/core/src/vm/closure/interface.rs new file mode 100644 index 0000000..b68a477 --- /dev/null +++ b/core/src/vm/closure/interface.rs @@ -0,0 +1,57 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::util::core::SimpleDrop; +use crate::vm::Vm; + +/// This trait represents a closure parameter. +pub trait FromUpvalue<'a>: Sized + SimpleDrop { + /// Reads this value from the given lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to read from. + /// * `index`: index of the parameter to read. + /// + /// returns: Self + /// + /// # Safety + /// + /// Calling this function outside the body of a [CFunction](crate::ffi::lua::CFunction) is UB. + /// Calling this function in a non-POF segment of that CFunction is also UB. Finally, if the + /// type of the value at index `index` is not of [Self], calling this function is UB. + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self; +} + +pub trait IntoUpvalue: Upvalue { + fn into_upvalue(self, vm: &Vm) -> u16; +} + +pub trait Upvalue { + type From<'a>: FromUpvalue<'a>; +} diff --git a/core/src/vm/closure/mod.rs b/core/src/vm/closure/mod.rs new file mode 100644 index 0000000..5f39d49 --- /dev/null +++ b/core/src/vm/closure/mod.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod arc; +pub mod context; +mod core; +mod interface; +pub mod rc; +mod rust; +pub mod types; + +pub use interface::*; diff --git a/core/src/vm/closure/rc.rs b/core/src/vm/closure/rc.rs new file mode 100644 index 0000000..8a87d78 --- /dev/null +++ b/core/src/vm/closure/rc.rs @@ -0,0 +1,76 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::util::core::SimpleDrop; +use crate::vm::closure::{FromUpvalue, IntoUpvalue, Upvalue}; +use crate::vm::value::types::RawPtr; +use crate::vm::Vm; +use std::ops::Deref; + +pub type Shared = std::rc::Rc; + +#[repr(transparent)] +pub struct Rc(*const T); + +#[repr(transparent)] +pub struct Ref<'a, T>(&'a T); + +unsafe impl SimpleDrop for Ref<'_, T> {} + +impl Deref for Ref<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.0 + } +} + +impl<'a, T> FromUpvalue<'a> for Ref<'a, T> { + #[inline(always)] + unsafe fn from_upvalue(vm: &'a Vm, index: i32) -> Self { + Ref(&*RawPtr::::from_upvalue(vm, index).as_ptr()) + } +} + +impl Upvalue for Rc { + type From<'a> = Ref<'a, T>; +} + +impl IntoUpvalue for Rc { + #[inline(always)] + fn into_upvalue(self, vm: &Vm) -> u16 { + RawPtr::new(self.0 as *mut T).into_upvalue(vm) + } +} + +impl Rc { + #[inline(always)] + pub fn from_rust(vm: &Vm, rc: Shared) -> Rc { + Rc(crate::vm::core::destructor::Pool::attach(vm, rc)) + } +} diff --git a/core/src/vm/closure/rust.rs b/core/src/vm/closure/rust.rs new file mode 100644 index 0000000..452cf78 --- /dev/null +++ b/core/src/vm/closure/rust.rs @@ -0,0 +1,139 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::luaL_error; +use crate::ffi::lua::{lua_newuserdata, lua_touserdata, State}; +use crate::vm::closure::types::RClosure; +use crate::vm::closure::FromUpvalue; +use crate::vm::function::{FromParam, IntoParam}; +use crate::vm::userdata::AnyUserData; +use crate::vm::value::types::RawPtr; +use crate::vm::Vm; +use std::marker::PhantomData; + +pub struct Guard<'a, F> { + ptr: *mut *const F, + #[allow(unused)] // Hold the box until the guard should drop and delete the Box with it. + bx: Box, + vm: PhantomData<&'a Vm>, +} + +impl<'a, F> Drop for Guard<'a, F> { + fn drop(&mut self) { + unsafe { *self.ptr = std::ptr::null() }; + } +} + +impl<'a> RClosure> { + pub fn from_rust_temporary<'b, T, R, F: Fn(T) -> R + 'a>( + vm: &'a Vm, + fun: F, + ) -> (RClosure>, Guard<'a, F>) + where + T: FromParam<'b>, + R: IntoParam, + { + let bx = Box::new(fun); + let rptr = &*bx as *const F; + unsafe { lua_newuserdata(vm.as_ptr(), size_of::<*const F>()) }; + let ptr = unsafe { lua_touserdata(vm.as_ptr(), -1) } as *mut *const F; + unsafe { *ptr = rptr }; + let value = unsafe { AnyUserData::from_raw(vm, vm.top()) }; + extern "C-unwind" fn _cfunc<'a, T, R, F: Fn(T) -> R>(l: State) -> i32 + where + T: FromParam<'a>, + R: IntoParam, + { + let vm = unsafe { Vm::from_raw(l) }; + let upvalue: RawPtr<*const F> = unsafe { FromUpvalue::from_upvalue(&vm, 1) }; + let args: T = unsafe { FromParam::from_param(std::mem::transmute(&vm), 1) }; + let ptr = unsafe { *upvalue.as_ptr() }; + if ptr.is_null() { + unsafe { + luaL_error( + vm.as_ptr(), + c"Attempt to call a dropped temporary rust closure".as_ptr(), + ) + }; + unsafe { std::hint::unreachable_unchecked() }; + } + let res = unsafe { (*ptr)(args) }; + res.into_param(&vm) as _ + } + ( + RClosure::new(_cfunc::, value), + Guard { + ptr, + bx, + vm: PhantomData, + }, + ) + } +} + +impl RClosure> { + fn __from_rust<'a, T, R, F: Fn(T) -> R + 'static>(ptr: *mut F) -> Self + where + T: FromParam<'a>, + R: IntoParam, + { + extern "C-unwind" fn _cfunc<'a, T, R, F: Fn(T) -> R>(l: State) -> i32 + where + T: FromParam<'a>, + R: IntoParam, + { + let vm = unsafe { Vm::from_raw(l) }; + let upvalue: RawPtr = unsafe { FromUpvalue::from_upvalue(&vm, 1) }; + let args: T = unsafe { FromParam::from_param(std::mem::transmute(&vm), 1) }; + let res = unsafe { (*upvalue.as_ptr())(args) }; + res.into_param(&vm) as _ + } + RClosure::new(_cfunc::, RawPtr::new(ptr as _)) + } + + #[cfg(feature = "send")] + pub fn from_rust<'a, T, R, F: Fn(T) -> R + 'static>(vm: &Vm, fun: F) -> Self + where + T: FromParam<'a>, + R: IntoParam, + F: Send, + { + let ptr = crate::vm::core::destructor::Pool::attach_send(vm, Box::new(fun)); + Self::__from_rust(ptr) + } + + #[cfg(not(feature = "send"))] + pub fn from_rust<'a, T, R, F: Fn(T) -> R + 'static>(vm: &Vm, fun: F) -> Self + where + T: FromParam<'a>, + R: IntoParam, + { + let ptr = crate::vm::core::destructor::Pool::attach(vm, Box::new(fun)); + Self::__from_rust(ptr) + } +} diff --git a/core/src/vm/closure/types.rs b/core/src/vm/closure/types.rs new file mode 100644 index 0000000..6bcbed0 --- /dev/null +++ b/core/src/vm/closure/types.rs @@ -0,0 +1,61 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_pushcclosure, CFunction}; +use crate::vm::closure::IntoUpvalue; +use crate::vm::value::IntoLua; +use crate::vm::Vm; + +pub use super::rust::Guard as RClosureGuard; + +pub struct RClosure { + func: CFunction, + upvalue: T, +} + +impl RClosure { + /// Creates a new [RClosure]. + /// + /// # Arguments + /// + /// * `func`: the [CFunction] to be associated with an upvalue. + /// * `upvalue`: the upvalue to bind to the [CFunction]. + /// + /// returns: RClosure + pub fn new(func: CFunction, upvalue: T) -> Self { + Self { func, upvalue } + } +} + +unsafe impl IntoLua for RClosure { + fn into_lua(self, vm: &Vm) -> u16 { + let num = self.upvalue.into_upvalue(vm); + unsafe { lua_pushcclosure(vm.as_ptr(), self.func, num as _) }; + 1 + } +} diff --git a/core/src/vm/core/debug.rs b/core/src/vm/core/debug.rs new file mode 100644 index 0000000..16ccea2 --- /dev/null +++ b/core/src/vm/core/debug.rs @@ -0,0 +1,114 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::core::destructor::Pool; +use crate::vm::registry::lua_ref::LuaRef as LiveLuaRef; +use crate::vm::registry::named::Key; +use crate::vm::registry::types::LuaRef; +use crate::vm::userdata::UserData; +use crate::vm::value::types::RawPtr; +use crate::vm::Vm; +use std::collections::HashMap; + +pub trait DebugItemType { + const NAME: &'static str; +} + +pub struct Lib; +pub struct Class; + +impl DebugItemType for Lib { + const NAME: &'static str = "Lib"; +} + +impl DebugItemType for Class { + const NAME: &'static str = "Class"; +} + +pub trait DebugItem { + fn describe() -> String; +} + +#[cfg(feature = "libs-core")] +impl DebugItem for T { + fn describe() -> String { + let lib_name = std::any::type_name::(); + let lib_namespace = T::NAMESPACE; + let desc = format!("{}: {}", lib_name, lib_namespace); + desc + } +} + +impl DebugItem for T { + fn describe() -> String { + T::FULL_TYPE.to_string_lossy().into() + } +} + +static DBG_REG: Key>> = Key::new("__debug_registry__"); + +pub struct DebugRegistry { + map: HashMap<&'static str, Vec>, +} + +impl DebugRegistry { + fn add_internal + ?Sized, T: DebugItemType>(&mut self) { + self.map + .entry(T::NAME) + .or_insert_with(Vec::new) + .push(D::describe()); + } + + fn list_internal(&self, _: T) -> Option> { + self.map.get(T::NAME).cloned() + } + + fn get(vm: &Vm) -> RawPtr { + if let None = DBG_REG.push(vm) { + let ptr = Pool::attach_send( + vm, + Box::new(DebugRegistry { + map: HashMap::new(), + }), + ); + DBG_REG.set(LiveLuaRef::new(vm, RawPtr::new(ptr))); + } + let ptr = DBG_REG.push(vm).unwrap().get(); + ptr + } + + pub fn add + ?Sized, T: DebugItemType>(vm: &Vm) { + let ptr = Self::get(vm); + unsafe { (*ptr.as_mut_ptr()).add_internal::() }; + } + + pub fn list(vm: &Vm, ty: impl DebugItemType) -> Option> { + let ptr = Self::get(vm); + unsafe { (*ptr.as_ptr()).list_internal(ty) } + } +} diff --git a/core/src/vm/core/destructor.rs b/core/src/vm/core/destructor.rs new file mode 100644 index 0000000..fcf5011 --- /dev/null +++ b/core/src/vm/core/destructor.rs @@ -0,0 +1,220 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::registry::lua_ref::LuaRef as LiveLuaRef; +use crate::vm::registry::named::Key; +use crate::vm::registry::types::LuaRef; +use crate::vm::value::types::RawPtr; +use crate::vm::Vm; +use bp3d_debug::debug; +use std::sync::Arc; + +/// This trait represents a value which can be attached to a [Pool](Pool). +pub trait RawSend: Send { + type Ptr: Copy; + + fn into_raw(self) -> Self::Ptr; + + /// Deletes the raw pointer. + /// + /// # Safety + /// + /// This function must be called with the same pointer that originated from the same type using + /// the [into_raw](Raw::into_raw) method. + unsafe fn delete(ptr: Self::Ptr); +} + +/// This trait represents a value which can be attached to a [Pool](Pool). +pub trait Raw { + type Ptr: Copy; + + fn into_raw(self) -> Self::Ptr; + + /// Deletes the raw pointer. + /// + /// # Safety + /// + /// This function must be called with the same pointer that originated from the same type using + /// the [into_raw](Raw::into_raw) method. + unsafe fn delete(ptr: Self::Ptr); +} + +impl RawSend for T { + type Ptr = T::Ptr; + + fn into_raw(self) -> Self::Ptr { + T::into_raw(self) + } + + unsafe fn delete(ptr: Self::Ptr) { + T::delete(ptr) + } +} + +impl Raw for Box { + type Ptr = *mut T; + + fn into_raw(self) -> Self::Ptr { + Box::into_raw(self) + } + + unsafe fn delete(ptr: Self::Ptr) { + drop(Box::from_raw(ptr)) + } +} + +impl Raw for std::rc::Rc { + type Ptr = *const T; + + fn into_raw(self) -> Self::Ptr { + std::rc::Rc::into_raw(self) + } + + unsafe fn delete(ptr: Self::Ptr) { + drop(std::rc::Rc::from_raw(ptr)) + } +} + +impl RawSend for Arc { + type Ptr = *const T; + + fn into_raw(self) -> Self::Ptr { + Arc::into_raw(self) + } + + unsafe fn delete(ptr: Self::Ptr) { + drop(Arc::from_raw(ptr)) + } +} + +static DESTRUCTOR_POOL: Key>> = Key::new("__destructor_pool__"); + +pub struct Pool { + leaked: Vec>, + post_close: Vec>, + is_send: bool, +} + +impl Pool { + pub fn new(is_send: bool) -> Self { + Self { + leaked: Vec::new(), + post_close: Vec::new(), + is_send, + } + } + + /// Inserts this pool in the given Vm. + /// + /// # Safety + /// + /// This is only safe to be called on [RootVm](crate::vm::RootVm) construction. + pub unsafe fn new_in_vm(vm: &mut Vm, is_send: bool) { + let b = Box::leak(Box::new(Pool::new(is_send))); + let ptr = RawPtr::new(b as *mut Pool); + DESTRUCTOR_POOL.set(LiveLuaRef::new(vm, ptr)); + } + + /// Extracts a destructor pool from the given [Vm]. + /// + /// # Safety + /// + /// The returned reference must not be aliased. + unsafe fn _from_vm(vm: &Vm) -> RawPtr { + let ptr = DESTRUCTOR_POOL.push(vm).unwrap(); + ptr.get() + } + + pub fn from_vm(vm: &mut Vm) -> &mut Self { + unsafe { &mut *Self::_from_vm(vm).as_mut_ptr() } + } + + pub fn attach_send(vm: &Vm, raw: R) -> R::Ptr + where + R::Ptr: 'static, + { + let ptr = unsafe { Self::_from_vm(vm) }; + unsafe { (*ptr.as_mut_ptr()).attach_mut_send(raw) } + } + + pub fn attach(vm: &Vm, raw: R) -> R::Ptr + where + R::Ptr: 'static, + { + let ptr = unsafe { Self::_from_vm(vm) }; + unsafe { (*ptr.as_mut_ptr()).attach_mut(raw) } + } + + pub fn attach_mut_send(&mut self, raw: R) -> R::Ptr + where + R::Ptr: 'static, + { + let ptr = R::into_raw(raw); + self.leaked.push(Box::new(move || { + unsafe { R::delete(ptr) }; + })); + ptr + } + + pub fn attach_mut(&mut self, raw: R) -> R::Ptr + where + R::Ptr: 'static, + { + if self.is_send { + panic!("Attempt to attach !Send type to Send destructor Pool: this is forbidden!") + } + let ptr = R::into_raw(raw); + self.leaked.push(Box::new(move || { + unsafe { R::delete(ptr) }; + })); + ptr + } + + pub fn attach_post_close(vm: &Vm, f: impl FnOnce() + Send + 'static) { + let ptr = unsafe { Self::_from_vm(vm) }; + unsafe { (*ptr.as_mut_ptr()).attach_mut_post_close(f) } + } + + pub fn attach_mut_post_close(&mut self, f: impl FnOnce() + Send + 'static) { + self.post_close.push(Box::new(f)); + } + + pub fn extract_post_close(vm: &mut Vm) -> Vec> { + std::mem::replace(&mut Self::from_vm(vm).post_close, Vec::new()) + } +} + +impl Drop for Pool { + fn drop(&mut self) { + debug!({ num = self.leaked.len() }, "Deleting leaked pointers..."); + let v = std::mem::take(&mut self.leaked); + for f in v { + f() + } + } +} diff --git a/core/src/vm/core/interface.rs b/core/src/vm/core/interface.rs new file mode 100644 index 0000000..1580fe2 --- /dev/null +++ b/core/src/vm/core/interface.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{State, ThreadStatus}; + +pub trait LoadString { + fn load_string(&self, l: State) -> ThreadStatus; +} + +pub trait Load { + fn load(self, l: State) -> ThreadStatus; +} diff --git a/core/src/vm/core/interrupt/mod.rs b/core/src/vm/core/interrupt/mod.rs new file mode 100644 index 0000000..c660f2e --- /dev/null +++ b/core/src/vm/core/interrupt/mod.rs @@ -0,0 +1,72 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +//! This module contains tools to allow interrupting a root Vm. + +mod vm; + +#[cfg(unix)] +mod unix; + +#[cfg(windows)] +mod windows; + +pub use vm::InterruptibleRootVm; + +#[cfg(unix)] +pub use unix::Signal; + +#[cfg(windows)] +pub use windows::Signal; + +unsafe impl Send for Signal {} +unsafe impl Sync for Signal {} + +use bp3d_util::simple_error; +use std::thread::JoinHandle; + +simple_error! { + pub Error { + AlreadyInterrupting => "attempt to interrupt a Vm while interrupting a different Vm", + IncorrectThread => "attempt to interrupt a Vm from the wrong thread", + Timeout => "the lua hook did not trigger in the requested time (is the JIT enabled?)", + Unknown => "unknown system error" + } +} + +pub fn spawn_interruptible( + f: impl FnOnce(&mut InterruptibleRootVm) -> R + Send + 'static, +) -> (Signal, JoinHandle) { + let (send, recv) = std::sync::mpsc::channel(); + let handle = std::thread::spawn(move || { + let mut vm = InterruptibleRootVm::new(crate::vm::core::UnSendRootVm::new()); + send.send(Signal::create(&mut vm)).unwrap(); + f(&mut vm) + }); + (recv.recv().unwrap(), handle) +} diff --git a/core/src/vm/core/interrupt/unix.rs b/core/src/vm/core/interrupt/unix.rs new file mode 100644 index 0000000..42f745d --- /dev/null +++ b/core/src/vm/core/interrupt/unix.rs @@ -0,0 +1,162 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::ext::lua_ext_ccatch_error; +use crate::ffi::lua::{ + lua_pushstring, lua_sethook, Debug, State, MASKCALL, MASKCOUNT, MASKLINE, MASKRET, +}; +use crate::vm::core::interrupt::{Error, InterruptibleRootVm}; +use crate::vm::Vm; +use bp3d_debug::{error, warning}; +use libc::{c_int, pthread_kill, pthread_self, pthread_t, SIGUSR1}; +use std::mem::MaybeUninit; +use std::ops::Deref; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex, Once}; +use std::thread::ThreadId; +use std::time::Duration; + +pub struct Signal { + l: State, + thread: ThreadId, + th: pthread_t, + alive: Arc, +} + +struct SigState { + l: State, + thread: ThreadId, + return_chan: std::sync::mpsc::Sender>, + notify_chan: std::sync::mpsc::Sender<()>, +} + +unsafe impl Send for SigState {} + +static SIG_STATE: Mutex> = Mutex::new(None); + +extern "C-unwind" fn lua_interrupt(l: State, _: Debug) { + { + let mut state = SIG_STATE.lock().unwrap(); + if let Some(sig) = state.take() { + if let Err(e) = sig.notify_chan.send(()) { + error!({error=?e}, "Failed to notify interrupt signal") + } + } + } + unsafe { + lua_sethook(l, None, 0, 0); + lua_pushstring(l, c"interrupted".as_ptr()); + lua_ext_ccatch_error(l); + } +} + +extern "C" fn signal_handler(_: c_int) { + let res = SIG_STATE.try_lock(); + match res { + Ok(v) => { + if let Some(v) = &*v { + let current_id = std::thread::current().id(); + if current_id != v.thread { + v.return_chan.send(Err(Error::IncorrectThread)).unwrap(); + return; + } + // Run the hook 1 instruction later. + unsafe { + lua_sethook( + v.l, + Some(lua_interrupt), + MASKCOUNT | MASKCALL | MASKLINE | MASKRET, + 1, + ) + }; + v.return_chan.send(Ok(())).unwrap(); + } + } + Err(e) => { + error!({error=?e}, "Attempt to interrupt a Vm while interrupting a different Vm"); + } + } +} + +static SIG_BOUND: Once = Once::new(); + +impl Signal { + pub fn create>(vm: &mut InterruptibleRootVm) -> Self { + let alive = InterruptibleRootVm::get_alive(vm).clone(); + let th = unsafe { pthread_self() }; + let l = vm.as_ptr(); + let thread = std::thread::current().id(); + SIG_BOUND.call_once(|| { + let mut sig: libc::sigaction = unsafe { MaybeUninit::zeroed().assume_init() }; + sig.sa_sigaction = signal_handler as _; + let ret = unsafe { libc::sigaction(SIGUSR1, &sig as _, std::ptr::null_mut()) }; + assert_eq!(ret, 0); + }); + Self { + l, + thread, + th, + alive, + } + } + + pub fn send(&self, duration: Duration) -> Result<(), Error> { + if !self.alive.load(std::sync::atomic::Ordering::SeqCst) { + return Ok(()); + } + let (send, recv) = std::sync::mpsc::channel(); + let (send2, recv2) = std::sync::mpsc::channel(); + { + let mut lock = SIG_STATE + .try_lock() + .map_err(|_| Error::AlreadyInterrupting)?; + *lock = Some(SigState { + l: self.l, + thread: self.thread, + return_chan: send, + notify_chan: send2, + }); + } + let ret = unsafe { pthread_kill(self.th, SIGUSR1) }; + if ret != 0 { + return Err(Error::Unknown); + } + recv.recv().unwrap()?; + match recv2.recv_timeout(duration) { + Ok(()) => Ok(()), + Err(e) => { + warning!({error=?e}, "Error attempting to wait for interrupt notification"); + { + let mut guard = SIG_STATE.lock().unwrap(); + *guard = None; + } + Err(Error::Timeout) + } + } + } +} diff --git a/core/src/vm/core/interrupt/vm.rs b/core/src/vm/core/interrupt/vm.rs new file mode 100644 index 0000000..a5c602e --- /dev/null +++ b/core/src/vm/core/interrupt/vm.rs @@ -0,0 +1,75 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::Vm; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +pub struct InterruptibleRootVm { + vm: T, + alive: Arc, + useless: PhantomData<*const ()>, // This is to ensure InterruptibleVm is never Send nor Sync. +} + +impl InterruptibleRootVm { + pub fn new(vm: T) -> Self { + Self { + vm, + alive: Arc::new(AtomicBool::new(true)), + useless: PhantomData, + } + } + + pub fn get_alive(this: &Self) -> &Arc { + &this.alive + } +} + +impl> Deref for InterruptibleRootVm { + type Target = Vm; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.vm + } +} + +impl> DerefMut for InterruptibleRootVm { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.vm + } +} + +impl Drop for InterruptibleRootVm { + fn drop(&mut self) { + self.alive.store(false, std::sync::atomic::Ordering::SeqCst); + } +} diff --git a/core/src/vm/core/interrupt/windows.rs b/core/src/vm/core/interrupt/windows.rs new file mode 100644 index 0000000..b758f57 --- /dev/null +++ b/core/src/vm/core/interrupt/windows.rs @@ -0,0 +1,131 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use super::{Error, InterruptibleRootVm}; +use crate::ffi::ext::lua_ext_ccatch_error; +use crate::ffi::lua::{ + lua_pushstring, lua_sethook, Debug, State, MASKCALL, MASKCOUNT, MASKLINE, MASKRET, +}; +use crate::vm::Vm; +use bp3d_debug::{error, warning}; +use std::ops::Deref; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use windows_sys::Win32::Foundation::HANDLE; +use windows_sys::Win32::System::Diagnostics::Debug::{GetThreadContext, CONTEXT}; +use windows_sys::Win32::System::Threading::{GetCurrentThread, ResumeThread, SuspendThread}; + +static SIG_STATE: Mutex>> = Mutex::new(None); + +extern "C-unwind" fn lua_interrupt(l: State, _: Debug) { + { + let mut state = SIG_STATE.lock().unwrap(); + if let Some(sig) = state.take() { + if let Err(e) = sig.send(()) { + error!({error=?e}, "Failed to notify interrupt signal") + } + } + } + unsafe { + lua_sethook(l, None, 0, 0); + lua_pushstring(l, c"interrupted".as_ptr()); + lua_ext_ccatch_error(l); + } +} + +pub struct Signal { + l: State, + th: HANDLE, + alive: Arc, +} + +impl Signal { + pub fn create>(vm: &mut InterruptibleRootVm) -> Self { + let alive = InterruptibleRootVm::get_alive(vm).clone(); + let th = unsafe { GetCurrentThread() }; + let l = vm.as_ptr(); + Self { l, th, alive } + } + + pub fn send(&self, duration: Duration) -> Result<(), Error> { + if !self.alive.load(std::sync::atomic::Ordering::SeqCst) { + return Ok(()); + } + let (send2, recv2) = std::sync::mpsc::channel(); + { + let mut lock = SIG_STATE + .try_lock() + .map_err(|_| Error::AlreadyInterrupting)?; + *lock = Some(send2); + } + if self.th == unsafe { GetCurrentThread() } { + // If somehow the system thread that ineterrupts the Vm is the same as the one which started the Vm, then directly set the hook. + unsafe { + lua_sethook( + self.l, + Some(lua_interrupt), + MASKCOUNT | MASKCALL | MASKLINE | MASKRET, + 1, + ); + } + } else { + unsafe { + let mut ctx: CONTEXT = std::mem::zeroed(); + // Requests to suspend the thread. + if SuspendThread(self.th) == u32::MAX { + //(DWORD) -1 + return Err(Error::Unknown); + } + // This call forces synchronization with the thread to be suspended. + if GetThreadContext(self.th, &mut ctx as _) == 0 { + return Err(Error::Unknown); + } + lua_sethook( + self.l, + Some(lua_interrupt), + MASKCOUNT | MASKCALL | MASKLINE | MASKRET, + 1, + ); + // Resume the thread. + let _ = ResumeThread(self.th); + } + } + match recv2.recv_timeout(duration) { + Ok(()) => Ok(()), + Err(e) => { + warning!({error=?e}, "Error attempting to wait for interrupt notification"); + { + let mut guard = SIG_STATE.lock().unwrap(); + *guard = None; + } + Err(Error::Timeout) + } + } + } +} diff --git a/core/src/vm/core/iter.rs b/core/src/vm/core/iter.rs new file mode 100644 index 0000000..ac24255 --- /dev/null +++ b/core/src/vm/core/iter.rs @@ -0,0 +1,58 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::value::FromLua; +use crate::vm::Vm; +use std::marker::PhantomData; + +pub struct Iter<'a, T> { + vm: &'a Vm, + index: i32, + useless: PhantomData, +} + +impl<'a, T: FromLua<'a>> Iterator for Iter<'a, T> { + type Item = crate::vm::Result; + + fn next(&mut self) -> Option { + if self.index > self.vm.top() { + return None; + } + let item = T::from_lua(self.vm, self.index); + self.index += 1; + Some(item) + } +} + +pub fn start<'a, T: FromLua<'a>>(vm: &'a Vm, start_index: i32) -> Iter<'a, T> { + Iter { + vm, + index: start_index, + useless: PhantomData, + } +} diff --git a/core/src/vm/core/jit.rs b/core/src/vm/core/jit.rs new file mode 100644 index 0000000..4761d5b --- /dev/null +++ b/core/src/vm/core/jit.rs @@ -0,0 +1,315 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::ext::{lua_ext_getjitflags, lua_ext_setjitflags, lua_ext_setjitmode}; +use crate::ffi::jit; +use crate::vm::{RootVm, Vm}; +use std::ffi::c_int; +use std::fmt::{Display, Formatter}; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct CpuArm { + pub v6: bool, + pub v6t2: bool, + pub v7: bool, + pub v8: bool, + pub vfpv2: bool, + pub vfpv3: bool, +} + +impl CpuArm { + pub fn has_vfp(&self) -> bool { + self.vfpv2 || self.vfpv3 + } +} + +impl Display for CpuArm { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if !self.v6 && !self.v7 && !self.v8 && !self.v6t2 && !self.vfpv2 && !self.vfpv3 { + write!(f, "ARM CPU, no features")?; + } else { + write!(f, "ARM CPU, features:")?; + if self.v6 { + write!(f, " ARMV6")?; + } + if self.v6t2 { + write!(f, " ARMV6T2")?; + } + if self.v7 { + write!(f, " ARMV7")?; + } + if self.v8 { + write!(f, " ARMV8")?; + } + if self.vfpv2 { + write!(f, " VFPV2")?; + } + if self.vfpv3 { + write!(f, " VFPV3")?; + } + } + Ok(()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct CpuX86 { + pub sse3: bool, + pub sse4_1: bool, + pub bmi2: bool, +} + +impl Display for CpuX86 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if !self.sse3 && !self.sse4_1 && !self.bmi2 { + write!(f, "X86 CPU, no features")?; + } else { + write!(f, "X86 CPU, features:")?; + if self.sse3 { + write!(f, " SSE3")?; + } + if self.sse4_1 { + write!(f, " SSE4_1")?; + } + if self.bmi2 { + write!(f, " BMI2")?; + } + } + Ok(()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum Cpu { + X86(CpuX86), + Arm(CpuArm), +} + +impl Display for Cpu { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Cpu::X86(cpu) => cpu.fmt(f), + Cpu::Arm(cpu) => cpu.fmt(f), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct Opts { + pub fold: bool, + pub cse: bool, + pub dce: bool, + pub fwd: bool, + pub dse: bool, + pub narrow: bool, + pub loop1: bool, + pub abc: bool, + pub sink: bool, + pub fuse: bool, + pub fma: bool, +} + +impl Display for Opts { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "optimizations:")?; + if self.fold { + write!(f, " fold")?; + } + if self.cse { + write!(f, " cse")?; + } + if self.dce { + write!(f, " dce")?; + } + if self.fwd { + write!(f, " fwd")?; + } + if self.dse { + write!(f, " dse")?; + } + if self.narrow { + write!(f, " narrow")?; + } + if self.loop1 { + write!(f, " loop")?; + } + if self.abc { + write!(f, " abc")?; + } + if self.sink { + write!(f, " sink")?; + } + if self.fuse { + write!(f, " fuse")?; + } + if self.fma { + write!(f, " fma")?; + } + Ok(()) + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +pub enum OptLevel { + O0, + O1, + O2, + #[default] + O3, + Unknown, +} + +impl Display for OptLevel { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + OptLevel::O0 => write!(f, "O0"), + OptLevel::O1 => write!(f, "O1"), + OptLevel::O2 => write!(f, "O2"), + OptLevel::O3 => write!(f, "O3"), + OptLevel::Unknown => write!(f, "Unknown"), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct JitOptions { + cur_flag_set: u32, + mode: c_int, + opt_level_changed: bool, +} + +impl JitOptions { + /// Read JIT options for the given [Vm](Vm). + /// + /// # Arguments + /// + /// * `vm`: the [Vm] instance to read options for. + /// + /// returns: JitOptions + pub fn get(vm: &Vm) -> JitOptions { + Self { + cur_flag_set: unsafe { lua_ext_getjitflags(vm.as_ptr()) }, + mode: -1, + opt_level_changed: false, + } + } + + pub fn is_enabled(&self) -> bool { + (self.cur_flag_set & jit::F_ON) != 0 + } + + pub fn cpu(&self) -> Cpu { + #[cfg(target_arch = "x86_64")] + let cpu = Cpu::X86(CpuX86 { + sse3: (self.cur_flag_set & jit::F_SSE3) != 0, + sse4_1: (self.cur_flag_set & jit::F_SSE4_1) != 0, + bmi2: (self.cur_flag_set & jit::F_BMI2) != 0, + }); + #[cfg(any(target_arch = "aarch64", target_arch = "arm"))] + let cpu = Cpu::Arm(CpuArm { + v6: (self.cur_flag_set & jit::F_ARMV6_) != 0, + v6t2: (self.cur_flag_set & jit::F_ARMV6T2_) != 0, + v7: (self.cur_flag_set & jit::F_ARMV7) != 0, + v8: (self.cur_flag_set & jit::F_ARMV8) != 0, + vfpv2: (self.cur_flag_set & jit::F_VFPV2) != 0, + vfpv3: (self.cur_flag_set & jit::F_VFPV3) != 0, + }); + cpu + } + + pub fn opts(&self) -> Opts { + Opts { + fold: (self.cur_flag_set & jit::F_OPT_FOLD) != 0, + cse: (self.cur_flag_set & jit::F_OPT_CSE) != 0, + dce: (self.cur_flag_set & jit::F_OPT_DCE) != 0, + fwd: (self.cur_flag_set & jit::F_OPT_FWD) != 0, + dse: (self.cur_flag_set & jit::F_OPT_DSE) != 0, + narrow: (self.cur_flag_set & jit::F_OPT_NARROW) != 0, + loop1: (self.cur_flag_set & jit::F_OPT_LOOP) != 0, + abc: (self.cur_flag_set & jit::F_OPT_ABC) != 0, + sink: (self.cur_flag_set & jit::F_OPT_SINK) != 0, + fuse: (self.cur_flag_set & jit::F_OPT_FUSE) != 0, + fma: (self.cur_flag_set & jit::F_OPT_FMA) != 0, + } + } + + pub fn opt_level(&self) -> OptLevel { + if (self.cur_flag_set & jit::F_OPT_3) == jit::F_OPT_3 { + return OptLevel::O3; + } + if (self.cur_flag_set & jit::F_OPT_2) == jit::F_OPT_2 { + return OptLevel::O2; + } + if (self.cur_flag_set & jit::F_OPT_1) == jit::F_OPT_1 { + return OptLevel::O1; + } + if (self.cur_flag_set & jit::F_OPT_MASK) == jit::F_OPT_0 { + return OptLevel::O0; + } + OptLevel::Unknown + } + + pub fn flush(&mut self) { + self.mode = jit::MODE_FLUSH; + } + + pub fn disable(&mut self) { + if self.is_enabled() { + self.mode = jit::MODE_OFF; + } + } + + pub fn enable(&mut self) { + if !self.is_enabled() { + self.mode = jit::MODE_ON; + } + } + + pub fn set_opt_level(&mut self, level: OptLevel) { + self.cur_flag_set &= !jit::F_OPT_MASK; + match level { + OptLevel::O0 | OptLevel::Unknown => self.cur_flag_set |= jit::F_OPT_0, + OptLevel::O1 => self.cur_flag_set |= jit::F_OPT_1, + OptLevel::O2 => self.cur_flag_set |= jit::F_OPT_2, + OptLevel::O3 => self.cur_flag_set |= jit::F_OPT_3, + } + self.opt_level_changed = true; + } + + pub fn apply(self, vm: &mut RootVm) { + if self.mode != -1 { + assert_eq!(unsafe { lua_ext_setjitmode(vm.as_ptr(), self.mode) }, 0); + } + if self.opt_level_changed { + assert_eq!( + unsafe { lua_ext_setjitflags(vm.as_ptr(), self.cur_flag_set) }, + 0 + ); + } + } +} diff --git a/core/src/vm/core/load.rs b/core/src/vm/core/load.rs new file mode 100644 index 0000000..5365c4c --- /dev/null +++ b/core/src/vm/core/load.rs @@ -0,0 +1,186 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::{luaL_loadbuffer, luaL_loadstring}; +use crate::ffi::lua::{lua_load, State, ThreadStatus}; +use crate::vm::core::util::{ChunkName, ChunkNameBuilder}; +use crate::vm::core::{Load, LoadString}; +use crate::vm::util::lua_rust_error; +use std::ffi::{c_char, c_void, CStr, CString, OsStr}; +use std::fmt::Write; +use std::fs::File; +use std::path::Path; + +impl LoadString for &CStr { + #[inline(always)] + fn load_string(&self, l: State) -> ThreadStatus { + unsafe { luaL_loadstring(l, self.as_ptr()) } + } +} + +impl LoadString for &str { + fn load_string(&self, l: State) -> ThreadStatus { + let s = CString::new(*self); + match s { + Ok(v) => (&*v).load_string(l), + Err(_) => ThreadStatus::ErrSyntax, + } + } +} + +pub struct Code<'a> { + name: &'a str, + code: &'a [u8], +} + +impl<'a> Code<'a> { + pub fn new(name: &'a str, code: &'a [u8]) -> Self { + Self { name, code } + } +} + +impl Load for Code<'_> { + fn load(self, l: State) -> ThreadStatus { + let mut builder = ChunkNameBuilder::new(); + let _ = write!(&mut builder, "={}", self.name); + let name = builder.build(); + unsafe { + luaL_loadbuffer( + l, + self.code.as_ptr() as _, + self.code.len(), + name.cstr().as_ptr(), + ) + } + } +} + +impl Load for T { + fn load(self, l: State) -> ThreadStatus { + self.load_string(l) + } +} + +pub trait Custom { + type Error: std::error::Error; + + fn read_data(&mut self) -> Result<&[u8], Self::Error>; +} + +/// Bind a custom Rust loader to Lua. +/// +/// # Safety +/// +/// This is UB to call outside a [Load] trait implementation. +pub unsafe fn load_custom( + l: State, + chunk_name: ChunkName, + mut custom: T, +) -> ThreadStatus { + extern "C-unwind" fn _reader( + l: State, + ud: *mut c_void, + sz: *mut usize, + ) -> *const c_char { + let obj = ud as *mut T; + unsafe { + let res = (*obj).read_data(); + match res { + Err(e) => { + lua_rust_error(l, e); + } + Ok(v) => { + *sz = v.len(); + v.as_ptr() as _ + } + } + } + } + lua_load( + l, + _reader::, + &mut custom as *mut T as _, + chunk_name.cstr().as_ptr(), + ) +} + +const BUF_SIZE: usize = 8192; + +pub struct Read { + inner: T, + buffer: [u8; BUF_SIZE], + len: usize, +} + +impl Read { + pub fn new(inner: T) -> Self { + Self { + inner, + buffer: [0; BUF_SIZE], + len: 0, + } + } +} + +impl Custom for Read { + type Error = std::io::Error; + + fn read_data(&mut self) -> Result<&[u8], Self::Error> { + self.len = self.inner.read(&mut self.buffer[..])?; + Ok(&self.buffer[..self.len]) + } +} + +pub struct Script { + file: File, + chunk_name: ChunkName, +} + +impl Script { + pub fn from_path(path: impl AsRef) -> std::io::Result { + let mut builder = ChunkNameBuilder::new(); + let file_name = path + .as_ref() + .file_name() + .unwrap_or(OsStr::new("unnamed")) + .to_str() + .unwrap_or("not-unicode"); + let _ = write!(&mut builder, "@{}", file_name); + let file = File::open(path)?; + Ok(Self { + file, + chunk_name: builder.build(), + }) + } +} + +impl Load for Script { + fn load(self, l: State) -> ThreadStatus { + unsafe { load_custom(l, self.chunk_name, Read::new(self.file)) } + } +} diff --git a/core/src/vm/core/mod.rs b/core/src/vm/core/mod.rs new file mode 100644 index 0000000..18f6b2f --- /dev/null +++ b/core/src/vm/core/mod.rs @@ -0,0 +1,50 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod debug; +pub mod destructor; +mod interface; +pub mod iter; +pub mod load; +#[cfg(feature = "root-vm")] +mod root_vm; +pub mod util; +mod vm; + +#[cfg(feature = "root-vm")] +pub mod jit; + +#[cfg(feature = "interrupt")] +pub mod interrupt; + +pub use interface::*; + +#[cfg(feature = "root-vm")] +pub use root_vm::*; + +pub use vm::Vm; diff --git a/core/src/vm/core/root_vm/common.rs b/core/src/vm/core/root_vm/common.rs new file mode 100644 index 0000000..ebda285 --- /dev/null +++ b/core/src/vm/core/root_vm/common.rs @@ -0,0 +1,82 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::{luaL_newstate, luaL_openlibs}; +use crate::ffi::lua::lua_close; +use crate::vm::core::destructor::Pool; +use crate::vm::registry::named::{handle_root_vm_init, handle_root_vm_uninit}; +use crate::vm::Vm; +use bp3d_debug::debug; + +#[cfg(not(feature = "send"))] +thread_local! { + // WTF?! The compiler should be smart enough to do this on its own! Another compiler defect! + static HAS_VM: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +#[repr(transparent)] +pub struct UnsafeRootVm(pub Vm); + +impl UnsafeRootVm { + pub fn new(is_send: bool) -> UnsafeRootVm { + #[cfg(not(feature = "send"))] + if HAS_VM.get() { + panic!("A VM already exists for this thread.") + } + let l = unsafe { luaL_newstate() }; + unsafe { luaL_openlibs(l) }; + #[cfg(not(feature = "send"))] + HAS_VM.set(true); + let mut vm = UnsafeRootVm(unsafe { Vm::from_raw(l) }); + handle_root_vm_init(); + unsafe { Pool::new_in_vm(&mut vm.0, is_send) }; + vm.0.run_code::<()>(c"table.unpack = unpack").unwrap(); + vm + } +} + +impl Drop for UnsafeRootVm { + fn drop(&mut self) { + debug!("Deleting destructor pool"); + let funcs = Pool::extract_post_close(&mut self.0); + unsafe { + drop(Box::from_raw(Pool::from_vm(&mut self.0))); + } + handle_root_vm_uninit(); + unsafe { + debug!("Closing Lua VM..."); + lua_close(self.0.as_ptr()); + } + #[cfg(not(feature = "send"))] + HAS_VM.set(false); + debug!("Running post VM hooks..."); + for func in funcs { + func() + } + } +} diff --git a/core/src/vm/core/root_vm/mod.rs b/core/src/vm/core/root_vm/mod.rs new file mode 100644 index 0000000..88ffef9 --- /dev/null +++ b/core/src/vm/core/root_vm/mod.rs @@ -0,0 +1,49 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod common; + +#[cfg(feature = "send")] +mod send; + +mod unsend; + +// --> Static type aliases <-- + +pub type UnSendRootVm = unsend::RootVm; + +#[cfg(feature = "send")] +pub type SendRootVm = send::RootVm; + +// --> Automatic type aliases <-- + +#[cfg(not(feature = "send"))] +pub type RootVm = unsend::RootVm; + +#[cfg(feature = "send")] +pub type RootVm = send::RootVm; diff --git a/core/src/vm/core/root_vm/send.rs b/core/src/vm/core/root_vm/send.rs new file mode 100644 index 0000000..758deb9 --- /dev/null +++ b/core/src/vm/core/root_vm/send.rs @@ -0,0 +1,61 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::core::root_vm::common::UnsafeRootVm; +use crate::vm::Vm; +use std::ops::{Deref, DerefMut}; + +pub struct RootVm { + vm: UnsafeRootVm, +} + +impl RootVm { + pub fn new() -> RootVm { + RootVm { + vm: UnsafeRootVm::new(true), + } + } +} + +unsafe impl Send for RootVm {} + +impl Deref for RootVm { + type Target = Vm; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.vm.0 + } +} + +impl DerefMut for RootVm { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.vm.0 + } +} diff --git a/core/src/vm/core/root_vm/unsend.rs b/core/src/vm/core/root_vm/unsend.rs new file mode 100644 index 0000000..e983bfa --- /dev/null +++ b/core/src/vm/core/root_vm/unsend.rs @@ -0,0 +1,59 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::core::root_vm::common::UnsafeRootVm; +use crate::vm::Vm; +use std::ops::{Deref, DerefMut}; + +pub struct RootVm { + vm: UnsafeRootVm, +} + +impl RootVm { + pub fn new() -> RootVm { + RootVm { + vm: UnsafeRootVm::new(false), + } + } +} + +impl Deref for RootVm { + type Target = Vm; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.vm.0 + } +} + +impl DerefMut for RootVm { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.vm.0 + } +} diff --git a/core/src/vm/core/util.rs b/core/src/vm/core/util.rs new file mode 100644 index 0000000..b61f0b3 --- /dev/null +++ b/core/src/vm/core/util.rs @@ -0,0 +1,217 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::{luaL_callmeta, luaL_traceback}; +use crate::ffi::lua::IDSIZE; +use crate::ffi::lua::{ + lua_gettop, lua_isstring, lua_pcall, lua_pushcclosure, lua_pushlstring, lua_remove, + lua_tolstring, lua_type, State, ThreadStatus, Type, +}; +use crate::vm::error::{Error, RuntimeError}; +use crate::vm::value::FromLua; +use crate::vm::Vm; +use bp3d_util::format::FixedBufStr; +use std::ffi::{c_int, CStr}; + +const TRACEBACK_NONE: &[u8] = b"\n"; +extern "C-unwind" fn error_handler(l: State) -> c_int { + unsafe { + let ty = lua_type(l, 1); + if ty != Type::String { + // Non-string error object? Try metamethod. + if (ty == Type::Nil || ty == Type::None) + || luaL_callmeta(l, 1, c"__tostring".as_ptr()) != 1 + || lua_isstring(l, -1) != 1 + { + // Object does not turn into a string remove it alongside the return value of + // __tostring. + lua_remove(l, 1); + lua_remove(l, 1); + // Push a place-holder string to avoid the rust code from crashing because the stack + // would be empty otherwise. + lua_pushlstring(l, TRACEBACK_NONE.as_ptr() as _, TRACEBACK_NONE.len()); + return 1; + } + // Remove the object from the stack so that error message becomes now index 1. + lua_remove(l, 1); + } + // Call traceback with the actual error message as a string which should push onto the stack + // the stacktrace as a string. + luaL_traceback(l, l, lua_tolstring(l, 1, std::ptr::null_mut()), 1); + // Remove the original error message string from the stack. + lua_remove(l, 1); + 1 + } +} + +/// Pushes the error handler on the Lua stack and return its absolute stack index. +/// +/// The error handler replaces the error message by a full traceback using [luaL_traceback]. +/// +/// # Arguments +/// +/// * `l`: the lua State on which to push the error handler function. +/// +/// returns: i32 +/// +/// # Safety +/// +/// You must ensure that the error handler function is NEVER called outside the context of an error. +#[inline(always)] +pub unsafe fn push_error_handler(l: State) -> c_int { + unsafe { + lua_pushcclosure(l, error_handler, 0); + lua_gettop(l) + } +} + +/// Calls the lua function at the top of the stack in a protected environment. +/// +/// # Arguments +/// +/// * `vm`: the [Vm] instance on which to call the function. +/// * `nargs`: the number of arguments push on top of the stack. +/// * `nreturns`: the number of returns expected from the function call. +/// * `handler_pos`: the absolute position of the handler on the stack. +/// +/// returns: Result<(), Error> +/// +/// # Safety +/// +/// This function shall not be used without [push_error_handler]. This is also UB if `nargs` does +/// not match the count of arguments push on top of the stack. If the error handler is not the first +/// item on the stack, before function and function arguments, this is UB. +pub unsafe fn pcall( + vm: &Vm, + nargs: c_int, + nreturns: c_int, + handler_pos: c_int, +) -> crate::vm::Result<()> { + let l = vm.as_ptr(); + unsafe { + // Call the function created by load_code. + let res = lua_pcall(l, nargs, nreturns, handler_pos); + // At this point the stack should no longer have the function but still has the error + // handler and R::num_values results. + // First remove error handler as we no longer need it. + lua_remove(l, handler_pos); + match res { + ThreadStatus::Ok => Ok(()), + ThreadStatus::ErrRun => { + // We've got a runtime error when executing the function so read the full stack + // trace produced by luaL_traceback and remove it from the stack. + let full_traceback: &str = FromLua::from_lua(vm, -1)?; + lua_remove(l, -1); + Err(Error::Runtime(RuntimeError::new(full_traceback.into()))) + } + ThreadStatus::ErrMem => Err(Error::Memory), + ThreadStatus::ErrErr => Err(Error::Error), + ThreadStatus::ErrCcatch => { + // We've got a runtime error when executing the function so read the full stack + // trace produced by luaL_traceback and remove it from the stack. + let full_traceback: &str = FromLua::from_lua(vm, -1)?; + lua_remove(l, -1); + Err(Error::UncatchableRuntime(RuntimeError::new( + full_traceback.into(), + ))) + } + _ => Err(Error::Unknown), + } + } +} + +/// Handles a syntax error. A syntax error is an error which may occur as part of a lua_load family +/// of functions. +/// +/// # Arguments +/// +/// * `vm`: the [Vm] instance which has produced the syntax error. +/// * `res`: the result of the load family of function. +/// * `handler_pos`: the absolute position of the error handler on the stack. +/// +/// returns: Result<(), Error> +/// +/// # Safety +/// +/// Calling this function with a `handler_pos` which does not correspond to the actual error handler +/// C function is UB. This is also UB if the res is not the result of a load function. +pub unsafe fn handle_syntax_error(vm: &Vm, res: ThreadStatus) -> crate::vm::Result<()> { + match res { + ThreadStatus::Ok => Ok(()), + ThreadStatus::ErrSyntax => { + // If we've got an error, read it and clear the stack. + let str: &str = FromLua::from_lua(vm, -1)?; + unsafe { lua_remove(vm.as_ptr(), -1) }; + Err(Error::Syntax(str.into())) + } + ThreadStatus::ErrRun => { + // If we've got an error, read it and clear the stack. + let str: &str = FromLua::from_lua(vm, -1)?; + unsafe { lua_remove(vm.as_ptr(), -1) }; + Err(Error::Loader(str.into())) + } + ThreadStatus::ErrMem => Err(Error::Memory), + _ => Err(Error::Unknown), + } +} + +#[derive(Default)] +pub struct ChunkNameBuilder { + inner: FixedBufStr<59>, +} + +impl ChunkNameBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn build(self) -> ChunkName { + let mut buf = FixedBufStr::::new(); + unsafe { + buf.write(self.inner.str().as_bytes()); + buf.write(b"\0"); + } + ChunkName { inner: buf } + } +} + +impl std::fmt::Write for ChunkNameBuilder { + fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> { + self.inner.write_str(s) + } +} + +pub struct ChunkName { + inner: FixedBufStr<60>, +} + +impl ChunkName { + pub fn cstr(&self) -> &CStr { + unsafe { CStr::from_bytes_with_nul_unchecked(self.inner.str().as_bytes()) } + } +} diff --git a/core/src/vm/core/vm.rs b/core/src/vm/core/vm.rs new file mode 100644 index 0000000..5442ef7 --- /dev/null +++ b/core/src/vm/core/vm.rs @@ -0,0 +1,203 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{ + lua_getfield, lua_gettop, lua_isyieldable, lua_pushnil, lua_remove, lua_setfield, lua_settop, + State, ThreadStatus, GLOBALSINDEX, REGISTRYINDEX, +}; +use crate::util::core::AnyStr; +use crate::vm::core::debug::DebugRegistry; +use crate::vm::core::util::{handle_syntax_error, pcall, push_error_handler}; +use crate::vm::core::{Load, LoadString}; +use crate::vm::error::Error; +use crate::vm::thread::core::Thread; +use crate::vm::userdata::core::Registry; +use crate::vm::userdata::{NameConvert, UserData}; +use crate::vm::value::types::Function; +use crate::vm::value::{FromLua, IntoLua}; +use bp3d_debug::{info, warning}; + +#[repr(transparent)] +pub struct Vm { + l: State, +} + +impl Vm { + /// Constructs a [Vm] by wrapping an existing lua [State]. + /// + /// # Arguments + /// + /// * `l`: the lua [State] to wrap. + /// + /// # Safety + /// + /// The given lua [State] must have been created from a [RootVm]. It is considered UB to wrap + /// a lua VM allocated outside bp3d-lua. It is also considered UB to wrap a VM which has been + /// allocated with a different set of patches. + #[inline(always)] + pub unsafe fn from_raw(l: State) -> Self { + Self { l } + } + + pub fn scope crate::vm::Result>( + &self, + f: F, + ) -> crate::vm::Result { + let top = self.top(); + let r = f(self); + unsafe { lua_settop(self.l, top) }; + r + } + + pub fn register_userdata(&self, case: impl NameConvert) -> crate::vm::Result<()> { + info!("Adding userdata type {:?}", T::CLASS_NAME); + DebugRegistry::add::(self); + let reg = unsafe { Registry::::new(self, case) }.map_err(Error::UserData)?; + let res = T::register(®).map_err(Error::UserData); + match res { + Ok(_) => Ok(()), + Err(e) => { + warning!( + "Failed to register userdata type {:?}: {}", + T::CLASS_NAME, + e + ); + unsafe { + lua_pushnil(self.l); + lua_setfield(self.l, REGISTRYINDEX, T::FULL_TYPE.as_ptr()); + } + Err(e) + } + } + } + + /// Returns the absolute stack index for the given index. + #[inline(always)] + pub fn get_absolute_index(&self, index: i32) -> i32 { + if index < 0 { + unsafe { lua_gettop(self.l) + index + 1 } + } else { + index + } + } + + /// Returns the top of the lua stack. + #[inline(always)] + pub fn top(&self) -> i32 { + unsafe { lua_gettop(self.l) } + } + + /// Clears the lua stack. + #[inline(always)] + pub fn clear(&mut self) { + unsafe { + lua_settop(self.l, 0); + } + } + + #[inline(always)] + pub fn as_ptr(&self) -> State { + self.l + } + + pub fn set_global(&self, name: impl AnyStr, value: impl IntoLua) -> crate::vm::Result<()> { + value.into_lua(self); + unsafe { + lua_setfield(self.as_ptr(), GLOBALSINDEX, name.to_str()?.as_ptr()); + } + Ok(()) + } + + pub fn get_global<'a, R: FromLua<'a>>(&'a self, name: impl AnyStr) -> crate::vm::Result { + unsafe { + lua_getfield(self.as_ptr(), GLOBALSINDEX, name.to_str()?.as_ptr()); + } + R::from_lua(self, -1) + } + + pub fn run_code<'a, R: FromLua<'a>>(&'a self, code: impl LoadString) -> crate::vm::Result { + let l = self.as_ptr(); + unsafe { + // Push error handler and the get the stack position of it. + let handler_pos = push_error_handler(l); + // Push the lua code. + let res = code.load_string(l); + if res != ThreadStatus::Ok { + lua_remove(l, handler_pos); + } + handle_syntax_error(self, res)?; + pcall(self, 0, R::num_values() as _, handler_pos)?; + } + // Read and return the result of the function from the stack. + FromLua::from_lua(self, -(R::num_values() as i32)) + } + + pub fn load_code(&self, code: impl LoadString) -> crate::vm::Result> { + let l = self.as_ptr(); + unsafe { + // Push the lua code. + let res = code.load_string(l); + handle_syntax_error(self, res)?; + Ok(FromLua::from_lua_unchecked(self, -1)) + } + } + + pub fn run<'a, R: FromLua<'a>>(&'a self, obj: impl Load) -> crate::vm::Result { + let l = self.as_ptr(); + let handler_pos = unsafe { push_error_handler(l) }; + let res = obj.load(l); + unsafe { + if res != ThreadStatus::Ok { + lua_remove(l, handler_pos); + } + handle_syntax_error(self, res)?; + pcall(self, 0, R::num_values() as _, handler_pos)?; + } + // Read and return the result of the function from the stack. + FromLua::from_lua(self, -(R::num_values() as i32)) + } + + pub fn load(&self, obj: impl Load) -> crate::vm::Result> { + let l = self.as_ptr(); + let res = obj.load(l); + unsafe { + handle_syntax_error(self, res)?; + Ok(FromLua::from_lua_unchecked(self, -1)) + } + } + + /// Attempts to turn this Vm into a lua [Thread] if it is yieldable, otherwise returns None. + pub fn as_thread(&self) -> Option> { + let yieldable = unsafe { lua_isyieldable(self.as_ptr()) }; + if yieldable == 1 { + Some(unsafe { Thread::from_raw(self.as_ptr()) }) + } else { + None + } + } +} diff --git a/core/src/vm/error.rs b/core/src/vm/error.rs new file mode 100644 index 0000000..688a837 --- /dev/null +++ b/core/src/vm/error.rs @@ -0,0 +1,168 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_type, Type}; +use crate::vm::Vm; +use bp3d_util::simple_error; +use std::fmt::{Display, Formatter}; + +#[derive(Debug, Copy, Clone)] +pub struct TypeError { + pub expected: Type, + pub actual: Type, +} + +impl TypeError { + pub fn from_stack(expected: Type, vm: &Vm, index: i32) -> Error { + Error::Type(Self { + expected, + actual: unsafe { lua_type(vm.as_ptr(), index) }, + }) + } +} + +impl Display for TypeError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "expected {:?}, got {:?}", self.expected, self.actual) + } +} + +#[derive(Debug, Clone)] +pub struct RuntimeError { + traceback: String, + index: usize, +} + +impl RuntimeError { + pub fn new(traceback: String) -> Self { + let id = traceback.find('\n').unwrap(); + Self { + traceback, + index: id, + } + } + + pub fn msg(&self) -> &str { + &self.traceback[..self.index] + } + + pub fn stacktrace(&self) -> &str { + &self.traceback[self.index + 1..] + } + + pub fn backtrace(&self) -> &str { + &self.traceback + } +} + +impl Display for RuntimeError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.msg()) + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Utf8Error { + // A re-usable error type is needed for modules so duplicate the one from std. + pub valid_up_to: usize, + pub error_len: Option, +} + +impl From for Utf8Error { + fn from(value: std::str::Utf8Error) -> Self { + Utf8Error { + valid_up_to: value.valid_up_to(), + error_len: value.error_len().map(|v| v as u8), + } + } +} + +impl Utf8Error { + pub const fn valid_up_to(&self) -> usize { + self.valid_up_to + } + + pub const fn error_len(&self) -> Option { + match self.error_len { + Some(len) => Some(len as usize), + None => None, + } + } +} + +impl Display for Utf8Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if let Some(error_len) = self.error_len { + write!( + f, + "invalid utf-8 sequence of {} bytes from index {}", + error_len, self.valid_up_to + ) + } else { + write!( + f, + "incomplete utf-8 byte sequence from index {}", + self.valid_up_to + ) + } + } +} + +simple_error! { + pub Error { + InvalidUtf8(Utf8Error) => "invalid UTF8 string: {}", + Type(TypeError) => "type error: {}", + Syntax(String) => "syntax error: {}", + Runtime(RuntimeError) => "runtime error: {}", + UncatchableRuntime(RuntimeError) => "uncatchable runtime error: {}", + Memory => "memory allocation error", + Unknown => "unknown error", + Error => "error in error handler", + Null => "string contains a null character", + MultiValue => "only one value is supported by this API", + UserData(crate::vm::userdata::Error) => "userdata: {}", + UnsupportedType(Type) => "unsupported lua type: {:?}", + Loader(String) => "loader error: {}", + ParseFloat => "parse float error", + ParseInt => "parse int error", + BadThreadState => "attempt to set the function of a thread in suspended or dead state" + } +} + +impl Error { + pub fn is_uncatchable(&self) -> bool { + matches!(self, Error::UncatchableRuntime(_)) + } + + pub fn into_runtime(self) -> Option { + match self { + Error::Runtime(e) => Some(e), + _ => None, + } + } +} diff --git a/core/src/vm/function/core.rs b/core/src/vm/function/core.rs new file mode 100644 index 0000000..c1f9db1 --- /dev/null +++ b/core/src/vm/function/core.rs @@ -0,0 +1,461 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::ext::{ + lua_ext_checkinteger64, lua_ext_checkuinteger64, lua_ext_fast_checkboolean, + lua_ext_fast_checkinteger, lua_ext_fast_checknumber, lua_ext_pushinteger64, + lua_ext_pushuinteger64, +}; +use crate::ffi::laux::{ + luaL_checkinteger, luaL_checklstring, luaL_checknumber, luaL_checkudata, luaL_testudata, +}; +use crate::ffi::lua::{ + lua_isnumber, lua_pushboolean, lua_pushinteger, lua_pushnil, lua_pushnumber, lua_toboolean, + lua_tointeger, lua_tonumber, lua_type, RawInteger, RawNumber, Type, +}; +use crate::util::core::SimpleDrop; +use crate::vm::function::{FromParam, IntoParam}; +use crate::vm::userdata::UserData; +use crate::vm::util::{lua_rust_error, LuaType, TypeName}; +use crate::vm::value::types::{Boolean, Integer, Number}; +use crate::vm::value::{FromLua, IntoLua}; +use crate::vm::Vm; +use std::borrow::Cow; +use std::error::Error; +use std::slice; + +impl<'a, T: FromParam<'a> + SimpleDrop> FromParam<'a> for Option { + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + let l = vm.as_ptr(); + let ty = lua_type(l, index); + if ty == Type::Nil || ty == Type::None { + None + } else { + Some(T::from_param(vm, index)) + } + } + + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + let l = vm.as_ptr(); + let ty = unsafe { lua_type(l, index) }; + if ty == Type::Nil || ty == Type::None { + None + } else { + Some(T::try_from_param(vm, index)) + } + } +} + +impl LuaType for &str {} + +impl<'a> FromParam<'a> for &'a str { + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + let mut len: usize = 0; + let str = luaL_checklstring(vm.as_ptr(), index, &mut len as _); + let slice = slice::from_raw_parts(str as *const u8, len); + match std::str::from_utf8(slice) { + Ok(v) => v, + Err(e) => { + lua_rust_error(vm.as_ptr(), e); + } + } + } + + #[inline(always)] + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + FromLua::from_lua(vm, index).ok() + } +} + +impl LuaType for &[u8] {} + +impl<'a> FromParam<'a> for &'a [u8] { + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + let mut len: usize = 0; + let str = luaL_checklstring(vm.as_ptr(), index, &mut len as _); + let slice = slice::from_raw_parts(str as *const u8, len); + slice + } + + #[inline(always)] + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + FromLua::from_lua(vm, index).ok() + } +} + +unsafe impl SimpleDrop for () {} + +impl LuaType for () {} + +impl FromParam<'_> for () { + unsafe fn from_param(_: &'_ Vm, _: i32) -> Self { + () + } + + fn try_from_param(_: &'_ Vm, _: i32) -> Option { + Some(()) + } +} + +unsafe impl IntoParam for &str { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self, vm) as _ + } +} + +unsafe impl IntoParam for &[u8] { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self, vm) as _ + } +} + +unsafe impl IntoParam for String { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + (&*self).into_param(vm) + } +} + +unsafe impl IntoParam for Box<[u8]> { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + self.as_ref().into_param(vm) + } +} + +unsafe impl<'a, T: IntoParam + Clone> IntoParam for Cow<'a, T> +where + &'a T: IntoParam, +{ + fn into_param(self, vm: &Vm) -> i32 { + match self { + Cow::Borrowed(v) => v.into_param(vm), + Cow::Owned(v) => v.into_param(vm), + } + } +} + +macro_rules! impl_integer_ty { + ($($t: ty),*) => { + $( + unsafe impl SimpleDrop for $t {} + + impl LuaType for $t { + fn lua_type() -> Vec { + vec![TypeName::Some(std::any::type_name::())] + } + } + )* + }; +} + +macro_rules! impl_integer { + ($($t: ty),*) => { + impl_integer_ty!($($t),*); + $( + impl FromParam<'_> for $t { + #[inline(always)] + unsafe fn from_param(vm: &Vm, index: i32) -> Self { + lua_ext_fast_checkinteger(vm.as_ptr(), index) as _ + } + + #[inline(always)] + fn try_from_param(vm: &Vm, index: i32) -> Option { + FromLua::from_lua(vm, index).ok() + } + } + + unsafe impl IntoParam for $t { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + unsafe { + lua_pushinteger(vm.as_ptr(), self as _); + 1 + } + } + } + )* + }; +} + +macro_rules! impl_integer_64 { + ($t: ty, $func: ident, $push_func: ident) => { + #[cfg(target_pointer_width = "64")] + impl_integer_ty!($t); + + #[cfg(target_pointer_width = "64")] + impl FromParam<'_> for $t { + #[inline(always)] + unsafe fn from_param(vm: &'_ Vm, index: i32) -> Self { + $func(vm.as_ptr(), index) as _ + } + + #[inline(always)] + fn try_from_param(vm: &'_ Vm, index: i32) -> Option { + FromLua::from_lua(vm, index).ok() + } + } + + #[cfg(target_pointer_width = "64")] + unsafe impl IntoParam for $t { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + unsafe { $push_func(vm.as_ptr(), self as _) } + } + } + }; +} + +impl_integer_64!(i64, lua_ext_checkinteger64, lua_ext_pushinteger64); +impl_integer_64!(u64, lua_ext_checkuinteger64, lua_ext_pushuinteger64); + +impl_integer_64!(isize, lua_ext_checkinteger64, lua_ext_pushinteger64); +impl_integer_64!(usize, lua_ext_checkuinteger64, lua_ext_pushuinteger64); + +#[cfg(target_pointer_width = "32")] +impl_integer!(isize, usize); + +impl_integer!(i8, u8, i16, u16, i32, u32); + +macro_rules! impl_float { + ($($t: ty),*) => { + $( + unsafe impl SimpleDrop for $t {} + + impl LuaType for $t { + fn lua_type() -> Vec { + vec![TypeName::Some(std::any::type_name::())] + } + } + + impl FromParam<'_> for $t { + #[inline(always)] + unsafe fn from_param(vm: &Vm, index: i32) -> Self { + lua_ext_fast_checknumber(vm.as_ptr(), index) as _ + } + + #[inline(always)] + fn try_from_param(vm: &Vm, index: i32) -> Option { + FromLua::from_lua(vm, index).ok() + } + } + + unsafe impl IntoParam for $t { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + unsafe { + lua_pushnumber(vm.as_ptr(), self as _); + 1 + } + } + } + )* + }; +} + +impl_float!(f32, f64); + +impl LuaType for bool {} + +impl FromParam<'_> for bool { + #[inline(always)] + unsafe fn from_param(vm: &'_ Vm, index: i32) -> Self { + lua_ext_fast_checkboolean(vm.as_ptr(), index) != 0 + } + + #[inline(always)] + fn try_from_param(vm: &'_ Vm, index: i32) -> Option { + FromLua::from_lua(vm, index).ok() + } +} + +unsafe impl IntoParam for bool { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + unsafe { lua_pushboolean(vm.as_ptr(), if self { 1 } else { 0 }) }; + 1 + } +} + +unsafe impl IntoParam for Result { + fn into_param(self, vm: &Vm) -> i32 { + match self { + Ok(v) => v.into_param(vm), + Err(e) => unsafe { + lua_rust_error(vm.as_ptr(), e); + }, + } + } +} + +unsafe impl IntoParam for Option { + fn into_param(self, vm: &Vm) -> i32 { + match self { + None => unsafe { + lua_pushnil(vm.as_ptr()); + 1 + }, + Some(v) => v.into_param(vm), + } + } +} + +unsafe impl IntoParam for () { + #[inline(always)] + fn into_param(self, _: &Vm) -> i32 { + 0 + } +} + +impl LuaType for &T { + fn lua_type() -> Vec { + vec![TypeName::Some(unsafe { + T::FULL_TYPE.to_str().unwrap_unchecked() + })] + } +} + +impl<'a, T: UserData> FromParam<'a> for &'a T { + #[inline(always)] + unsafe fn from_param(vm: &'a Vm, index: i32) -> &'a T { + let obj_ptr = luaL_checkudata(vm.as_ptr(), index, T::FULL_TYPE.as_ptr()) as *const T; + unsafe { &*obj_ptr } + } + + #[inline(always)] + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + let ptr = unsafe { luaL_testudata(vm.as_ptr(), index, T::FULL_TYPE.as_ptr()) } as *const T; + if ptr.is_null() { + None + } else { + Some(unsafe { &*ptr }) + } + } +} + +unsafe impl IntoParam for T { + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self, vm) as _ + } +} + +macro_rules! impl_into_param_tuple { + ($($name: ident: $name2: tt),*) => { + unsafe impl<$($name: IntoParam),*> IntoParam for ($($name),*) { + fn into_param(self, vm: &Vm) -> i32 { + $( + self.$name2.into_param(vm) + + )* + 0 + } + } + }; +} + +impl_into_param_tuple!(T: 0, T1: 1); +impl_into_param_tuple!(T: 0, T1: 1, T2: 2); +impl_into_param_tuple!(T: 0, T1: 1, T2: 2, T3: 3); +impl_into_param_tuple!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4); +impl_into_param_tuple!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5); +impl_into_param_tuple!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6); +impl_into_param_tuple!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7); +impl_into_param_tuple!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8); +impl_into_param_tuple!(T: 0, T1: 1, T2: 2, T3: 3, T4: 4, T5: 5, T6: 6, T7: 7, T8: 8, T9: 9); + +impl<'a> FromParam<'a> for Number { + #[inline(always)] + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + Self(luaL_checknumber(vm.as_ptr(), index)) + } + + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + let l = vm.as_ptr(); + unsafe { + if lua_isnumber(l, index) == 1 { + Some(Self(lua_tonumber(l, index))) + } else { + None + } + } + } +} + +impl<'a> FromParam<'a> for Integer { + #[inline(always)] + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + Self(luaL_checkinteger(vm.as_ptr(), index)) + } + + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + let l = vm.as_ptr(); + unsafe { + if lua_isnumber(l, index) == 1 { + Some(Self(lua_tointeger(l, index))) + } else { + None + } + } + } +} + +impl<'a> FromParam<'a> for Boolean { + #[inline(always)] + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + Self(lua_toboolean(vm.as_ptr(), index) == 1) + } + + #[inline(always)] + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + unsafe { Some(Self::from_param(vm, index)) } + } +} + +unsafe impl IntoParam for Number { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + unsafe { lua_pushnumber(vm.as_ptr(), self.0) } + 1 + } +} + +unsafe impl IntoParam for Integer { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + unsafe { lua_pushinteger(vm.as_ptr(), self.0) } + 1 + } +} + +unsafe impl IntoParam for Boolean { + fn into_param(self, vm: &Vm) -> i32 { + unsafe { lua_pushboolean(vm.as_ptr(), if self.0 { 1 } else { 0 }) } + 1 + } +} diff --git a/core/src/vm/function/interface.rs b/core/src/vm/function/interface.rs new file mode 100644 index 0000000..9538039 --- /dev/null +++ b/core/src/vm/function/interface.rs @@ -0,0 +1,86 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::util::core::SimpleDrop; +use crate::vm::util::LuaType; +use crate::vm::Vm; + +/// This trait represents a function return value. +/// +/// # Safety +/// +/// When implementing this trait, ensure that the number returned by +/// [into_param](IntoParam::into_param) is EXACTLY equal to the number of values pushed onto the lua +/// stack. If more or fewer than advertised values exists on the stack after the call then the impl +/// is considered UB. +pub unsafe trait IntoParam: Sized { + /// Turns self into a function return parameter. + /// + /// This function returns the number of parameters pushed onto the lua stack. Returns -1 in + /// case of a lua_yield. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to push this value to. + /// + /// returns: i32 + fn into_param(self, vm: &Vm) -> i32; +} + +/// This trait represents a function parameter. +pub trait FromParam<'a>: Sized + SimpleDrop + LuaType { + /// Reads this value from the given lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to read from. + /// * `index`: index of the parameter to read. This index is guaranteed to be absolute. + /// + /// returns: Self + /// + /// # Safety + /// + /// Calling this function outside the body of a [CFunction](crate::ffi::lua::CFunction) is UB. + /// Calling this function in a non-POF segment of that CFunction is also UB. If the index is not + /// absolute, this function may cause UB. + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self; + + /// Attempts to read this value from the given lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to read from. + /// * `index`: index of the parameter to read. This index is guaranteed to be absolute. + /// + /// returns: Self + /// + /// # Errors + /// + /// Returns a [None] value if this value cannot be read from the lua stack. + fn try_from_param(vm: &'a Vm, index: i32) -> Option; +} diff --git a/core/src/vm/function/mod.rs b/core/src/vm/function/mod.rs new file mode 100644 index 0000000..0f7fbf0 --- /dev/null +++ b/core/src/vm/function/mod.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod core; +mod interface; +pub mod types; + +pub use interface::*; diff --git a/core/src/vm/function/types.rs b/core/src/vm/function/types.rs new file mode 100644 index 0000000..ff5fddc --- /dev/null +++ b/core/src/vm/function/types.rs @@ -0,0 +1,50 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_pushcclosure, CFunction}; +use crate::vm::value::IntoLua; +use crate::vm::Vm; + +pub struct RFunction(CFunction); + +impl RFunction { + #[inline(always)] + pub fn wrap(inner: CFunction) -> Self { + Self(inner) + } +} + +unsafe impl IntoLua for RFunction { + fn into_lua(self, vm: &Vm) -> u16 { + let l = vm.as_ptr(); + unsafe { + lua_pushcclosure(l, self.0, 0); + } + 1 + } +} diff --git a/core/src/vm/mod.rs b/core/src/vm/mod.rs new file mode 100644 index 0000000..f254c9b --- /dev/null +++ b/core/src/vm/mod.rs @@ -0,0 +1,45 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod closure; +pub mod core; +pub mod error; +pub mod function; +pub mod registry; +pub mod table; +pub mod thread; +pub mod userdata; +pub mod util; +pub mod value; + +pub use core::Vm; + +#[cfg(feature = "root-vm")] +pub use core::RootVm; + +pub type Result = std::result::Result; diff --git a/core/src/vm/registry/core.rs b/core/src/vm/registry/core.rs new file mode 100644 index 0000000..6f37f2a --- /dev/null +++ b/core/src/vm/registry/core.rs @@ -0,0 +1,193 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::{luaL_ref, luaL_unref}; +use crate::ffi::lua::{lua_rawgeti, lua_rawseti, REGISTRYINDEX}; +#[cfg(feature = "send")] +use crate::vm::registry::send_key::VmCheckedRawKey; +use crate::vm::registry::{FromIndex, Set, Value}; +use crate::vm::value::util::move_value_top; +use crate::vm::Vm; +use std::ffi::c_int; +use std::marker::PhantomData; + +//TODO: Check if key can be a NonZeroI32. + +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct RawKey(c_int); + +impl RawKey { + /// Returns the raw key. + #[inline(always)] + pub fn as_int(&self) -> c_int { + self.0 + } + + /// Wraps a raw integer as a registry key. + #[inline(always)] + pub fn from_int(v: c_int) -> RawKey { + RawKey(v) + } + + /// Pushes the lua value associated to this registry key on the lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to attach the produced lua value to. + /// + /// returns: ::Value + /// + /// # Safety + /// + /// This is UB to call if the key is invalid or already freed. + #[inline(always)] + pub unsafe fn push(&self, vm: &Vm) { + lua_rawgeti(vm.as_ptr(), REGISTRYINDEX, self.0); + } + + /// Deletes this registry key from the specified [Vm]. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to unregister from. + /// + /// returns: () + /// + /// # Safety + /// + /// This is UB to call if the registry key is invalid or was already freed. + #[inline(always)] + pub unsafe fn delete(self, vm: &Vm) { + luaL_unref(vm.as_ptr(), REGISTRYINDEX, self.0); + } + + /// Creates a new [RawKey] from the top of the lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] instance representing the lua stack. + /// + /// returns: RegistryKey + /// + /// # Safety + /// + /// This is UB to call if the stack is empty. + #[inline(always)] + pub unsafe fn from_top(vm: &Vm) -> RawKey { + let key = unsafe { luaL_ref(vm.as_ptr(), REGISTRYINDEX) }; + RawKey(key) + } +} + +impl FromIndex for RawKey { + unsafe fn from_index(vm: &Vm, index: i32) -> Self { + move_value_top(vm, index); + Self::from_top(vm) + } +} + +impl Set for RawKey { + unsafe fn set(&self, vm: &Vm, index: i32) { + move_value_top(vm, index); + lua_rawseti(vm.as_ptr(), REGISTRYINDEX, self.0); + } +} + +pub struct Key { + #[cfg(feature = "send")] + raw: VmCheckedRawKey, + #[cfg(not(feature = "send"))] + raw: RawKey, + useless: PhantomData<*const T>, +} + +#[cfg(feature = "send")] +unsafe impl Send for Key {} + +impl Key { + /// Pushes the lua value associated to this registry key on the lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to attach the produced lua value to. + /// + /// returns: ::Value + #[inline(always)] + pub fn push<'a>(&self, vm: &'a Vm) -> T::Value<'a> { + unsafe { + self.raw.push(vm); + T::from_registry(vm, -1) + } + } + + /// Pushes the lua value associated to this registry key on the lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to attach the produced lua value to. + /// + /// returns: ::Value + #[inline(always)] + pub fn as_raw(&self) -> RawKey { + #[cfg(feature = "send")] + return self.raw.as_raw(); + #[cfg(not(feature = "send"))] + return self.raw; + } + + /// Deletes this registry key from the specified [Vm]. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to unregister from. + /// + /// returns: () + #[inline(always)] + pub fn delete(self, vm: &Vm) { + #[cfg(feature = "send")] + self.raw.delete(vm); + #[cfg(not(feature = "send"))] + unsafe { + self.raw.delete(vm) + }; + } + + #[inline(always)] + pub fn new(value: T::Value<'_>) -> Key { + Key { + raw: T::push_registry(value), + useless: PhantomData, + } + } + + #[inline(always)] + pub fn set(&self, value: T::Value<'_>) { + unsafe { T::set_registry(&self.raw, value) } + } +} diff --git a/core/src/vm/registry/interface.rs b/core/src/vm/registry/interface.rs new file mode 100644 index 0000000..c9f9f7b --- /dev/null +++ b/core/src/vm/registry/interface.rs @@ -0,0 +1,113 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::Vm; + +/// This trait represents a generic key which can be constructed from an index on the lua stack. +pub trait FromIndex { + /// Constructs a new instance of this generic key from the given vm and index. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] instance to manipulate. + /// * `index`: the index of the value on the lua stack. + /// + /// returns: Self + /// + /// # Safety + /// + /// This function removes the value at index `index` and so assumes no more references exists + /// to it, failure to ensure this is UB. + /// + /// When using [Key](crate::vm::registry::core::Key), the type of the lua value at index `index` + /// must be the same as the type of key, if not this is UB. + unsafe fn from_index(vm: &Vm, index: i32) -> Self; +} + +/// This trait represents a generic key which can be set from an index on the lua stack. +pub trait Set { + /// Sets the value of this generic key from the given vm and index. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] instance to manipulate. + /// * `index`: the index of the value on the lua stack. + /// + /// returns: () + /// + /// # Safety + /// + /// This function removes the value at index `index` and so assumes no more references exists + /// to it, failure to ensure this is UB. The function also assumes this generic key still + /// exists in the registry table. Finally, this assumes this key does not conflict with a + /// different one. + /// + /// When using [Key](crate::vm::registry::core::Key), the type of the lua value at index `index` + /// must be the same as the type of key, if not this is UB. + unsafe fn set(&self, vm: &Vm, index: i32); +} + +pub trait Value: 'static { + type Value<'a>; + + /// Reads the upvalue at the given location on the lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to read from. + /// * `index`: the index of the value. This index is not guaranteed to be absolute. + /// + /// returns: Self::Value + /// + /// # Safety + /// + /// This function assumes the value at the top of the stack is of type `Self`. This function is + /// UB otherwise. + unsafe fn from_registry(vm: &Vm, index: i32) -> Self::Value<'_>; + + /// Intializes a new generic key from the given value. + /// + /// This function should call R::from_index with a matching index and [Vm] instance. + fn push_registry(value: Self::Value<'_>) -> R; + + /// Assign this value to the given generic registry key. + /// + /// This function should call key.set with a matching index and [Vm] instance. + /// + /// # Arguments + /// + /// * `key`: the key to update. + /// * `value`: the new value. + /// + /// returns: () + /// + /// # Safety + /// + /// This function assumes the generic key still exists in the registry table. + unsafe fn set_registry(key: &impl Set, value: Self::Value<'_>); +} diff --git a/core/src/vm/registry/lua_ref.rs b/core/src/vm/registry/lua_ref.rs new file mode 100644 index 0000000..dae56ec --- /dev/null +++ b/core/src/vm/registry/lua_ref.rs @@ -0,0 +1,217 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_replace, lua_settop}; +use crate::impl_simple_registry_value_static; +use crate::vm::registry::{FromIndex, Set}; +use crate::vm::value::types::RawPtr; +use crate::vm::value::{FromLua, IntoLua}; +use crate::vm::Vm; +use std::marker::PhantomData; + +/// Represents a simple value type which can be manipulated by [LuaRef]. +/// +/// # Notes +/// +/// * The definition of a simple type in bp3d-lua is a type which does not hold a reference to +/// the [Vm]. This is typically the case of primitives like strings, integers, numbers, etc. +/// Types such as tables or functions are called complex in bp3d-lua as they require constant +/// interactions with the Lua stack represented by a [Vm] in order to operate on them. +/// +/// * For complex types, no wrapper is needed as they already have a reference to the attached [Vm] +/// in their value type. Instead, complex types which can be saved in the registry directly +/// implements the registry [Value](crate::vm::registry::Value) trait. +/// +/// # Safety +/// +/// This trait always assumes a single lua value is managed. If the underlying [SimpleValue] +/// manipulates multiple stack indices on the given lua [Vm], the implementation is considered UB. +pub unsafe trait SimpleValue<'a> { + fn into_lua(self, vm: &'a Vm); + + /// Extracts an instance of `Self` from the given Lua [Vm] and index. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] where to read the object from. + /// * `index`: the index of the object on the stack. + /// + /// returns: Self + /// + /// # Safety + /// + /// This function assumes the given index is valid for [Vm] and that the object on the stack at + /// the given index is already of type `Self`. If any of these assumptions are broken, this + /// function is UB. + unsafe fn from_lua(vm: &'a Vm, index: i32) -> Self; +} + +/// Marks a type as being compatible with the [LuaRef](crate::vm::registry::types::LuaRef) based +/// registry system for simple types. +pub trait SimpleRegistryValue { + type Value<'a>: SimpleValue<'a>; +} + +pub struct LuaRef<'a, T> { + vm: &'a Vm, + index: i32, + useless: PhantomData, +} + +impl<'a, T: SimpleValue<'a>> LuaRef<'a, T> { + pub fn new(vm: &'a Vm, value: T) -> Self { + value.into_lua(vm); + Self { + vm, + index: vm.top(), + useless: PhantomData, + } + } + + /// Constructs a [LuaRef] from a raw index on a Lua [Vm]. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] where to read the lightuserdata from. + /// * `index`: the index of the lightuserdata pointer on the stack. + /// + /// # Safety + /// + /// Calling this function assumes the given index is absolute and is valid for [Vm] and that + /// the object on the stack at the given index is already of type `T`. If any of + /// these assumptions are not respected, this function is UB. + #[inline(always)] + pub unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + Self { + vm, + index, + useless: PhantomData, + } + } + + /// Returns the registry value from the lua stack. + #[inline(always)] + pub fn get<'b: 'a>(&'b self) -> T { + unsafe { SimpleValue::<'b>::from_lua(self.vm, self.index) } + } + + /// Returns the registry value from the lua stack. + /// + /// # Safety + /// + /// This function assumes the returned registry value won't be set while being borrowed. + #[inline(always)] + pub unsafe fn get_unchecked(&self) -> T { + unsafe { T::from_lua(self.vm, self.index) } + } + + pub fn set(&mut self, value: T) { + value.into_lua(self.vm); + unsafe { lua_replace(self.vm.as_ptr(), self.index) }; + } +} + +impl<'a, T> Drop for LuaRef<'a, T> { + fn drop(&mut self) { + // Remove the object from the lua stack if it is on top of the stack. + if self.index == self.vm.top() { + unsafe { + lua_settop(self.vm.as_ptr(), -2); + } + } + } +} + +impl super::Value for super::types::LuaRef { + type Value<'a> = LuaRef<'a, T::Value<'a>>; + + unsafe fn from_registry(vm: &Vm, index: i32) -> Self::Value<'_> { + LuaRef::from_lua_unchecked(vm, vm.get_absolute_index(index)) + } + + fn push_registry(value: Self::Value<'_>) -> R { + unsafe { + let r = R::from_index(value.vm, value.index); + // Avoid calling the destructor which may try to pop an already popped value. + std::mem::forget(value); + r + } + } + + unsafe fn set_registry(key: &impl Set, value: Self::Value<'_>) { + key.set(value.vm, value.index); + // Avoid calling the destructor which may try to pop an already popped value. + std::mem::forget(value); + } +} + +unsafe impl<'a, T> SimpleValue<'a> for T +where + T: FromLua<'a> + IntoLua, +{ + #[inline(always)] + fn into_lua(self, vm: &'a Vm) { + // This ensures the safety guarentee still holds. + assert_eq!(IntoLua::into_lua(self, vm), 1); + } + + #[inline(always)] + unsafe fn from_lua(vm: &'a Vm, index: i32) -> Self { + T::from_lua_unchecked(vm, index) + } +} + +unsafe impl SimpleValue<'_> for RawPtr { + #[inline(always)] + fn into_lua(self, vm: &Vm) { + IntoLua::into_lua(self, vm); + } + + #[inline(always)] + unsafe fn from_lua(vm: &Vm, index: i32) -> Self { + unsafe { RawPtr::from_lua(vm, index) } + } +} + +impl_simple_registry_value_static! { + (RawPtr) => RawPtr; + (&str) => &'a str; + (f32) => f32; + (f64) => f64; + (i8) => i8; + (i16) => i16; + (i32) => i32; + (i64) => i64; + (u8) => u8; + (u16) => u16; + (u32) => u32; + (u64) => u64; + (String) => String; + (&[u8]) => &'a [u8]; + (Box<[u8]>) => Box<[u8]>; +} diff --git a/core/src/vm/registry/mod.rs b/core/src/vm/registry/mod.rs new file mode 100644 index 0000000..cee6be6 --- /dev/null +++ b/core/src/vm/registry/mod.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod core; +mod interface; +pub mod lua_ref; +pub mod named; +pub mod types; + +#[cfg(feature = "send")] +mod send_key; + +pub use interface::*; diff --git a/core/src/vm/registry/named.rs b/core/src/vm/registry/named.rs new file mode 100644 index 0000000..75a3616 --- /dev/null +++ b/core/src/vm/registry/named.rs @@ -0,0 +1,239 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::ext::lua_ext_keyreg_get; +use crate::ffi::lua::{ + lua_insert, lua_pushlightuserdata, lua_rawget, lua_rawset, lua_settop, lua_type, State, Type, + REGISTRYINDEX, +}; +use crate::vm::registry::{Set, Value}; +use crate::vm::value::util::move_value_top; +use crate::vm::Vm; +use std::collections::HashMap; +use std::ffi::c_void; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Mutex; + +#[derive(Debug)] +pub struct RawKey { + ptr: *const c_void, + // This may not always work, but unfortunately Rust TypeId is broken across modules. + // Fortunately, the generic type which is used with this is always a static lifetime + // which must implement the Value trait which limits the number of possible types. + ty: fn() -> &'static str, + registered: AtomicBool, + register_lock: Mutex, +} + +unsafe impl Send for RawKey {} +unsafe impl Sync for RawKey {} + +impl RawKey { + /// Pushes the value associated with this key on the lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] instance to manipulate. + /// + /// returns: () + /// + /// # Safety + /// + /// This is UB to call if the key was not registered in the given [Vm] using + /// [register](RawKey::register). + pub fn push(&self, vm: &Vm) { + check_register_key_unique(self); + let l = vm.as_ptr(); + unsafe { + lua_pushlightuserdata(l, self.ptr as _); + lua_rawget(l, REGISTRYINDEX); + } + } + + pub fn ty(&self) -> &'static str { + (self.ty)() + } + + pub const fn new(name: &str, ty: fn() -> &'static str) -> Self { + // This is a re-write of https://github.com/BPXFormat/bpx-rs/blob/develop/src/hash.rs + // in const context. + let mut val: u64 = 5381; + let bytes = name.as_bytes(); + // Unreadable algorithm: see the hash loop in bpx-rs for the readable variant. + let mut i = 0; + while i != bytes.len() { + let temp1 = val.wrapping_shl(5); + let temp2 = temp1.wrapping_add(val); + val = temp2.wrapping_add(bytes[i] as u64); + i += 1; + } + // And now a hack to turn u64 into ptr (btw, do NOT dereference it). + Self { + ptr: val as usize as *const c_void, + ty, + registered: AtomicBool::new(false), + register_lock: Mutex::new(false), + } + } +} + +unsafe fn rawsetp(l: State, idx: i32, key: *const c_void) { + lua_pushlightuserdata(l, key as _); + lua_insert(l, -2); // Move key after value; + lua_rawset(l, idx); +} + +impl Set for RawKey { + unsafe fn set(&self, vm: &Vm, index: i32) { + check_register_key_unique(self); + let l = vm.as_ptr(); + move_value_top(vm, index); + rawsetp(l, REGISTRYINDEX, self.ptr); + } +} + +fn check_register_key_unique(key: &RawKey) { + if key.registered.load(Ordering::Relaxed) { + return; + } + let mut registered = key.register_lock.lock().unwrap(); + if *registered { + return; + } + let registry = unsafe { voidp_to_ref(lua_ext_keyreg_get()) }; + let mut lock = registry.lock().unwrap(); + if let Some(ty) = lock.get(&(key.ptr as _)) { + if *ty != key.ty() { + panic!("Attempt to register a duplicate key"); + } + } + lock.insert(key.ptr as _, key.ty()); + *registered = true; + key.registered.store(true, Ordering::Relaxed); +} + +type NamedKeyRegistry = Mutex>; + +unsafe fn voidp_to_ref(p: *mut c_void) -> &'static NamedKeyRegistry { + assert!(!p.is_null()); + unsafe { &*(p as *const NamedKeyRegistry) } +} + +#[cfg(feature = "root-vm")] +unsafe fn voidp_to_ptr(p: *mut c_void) -> *mut NamedKeyRegistry { + assert!(!p.is_null()); + p as *mut NamedKeyRegistry +} + +#[cfg(feature = "root-vm")] +fn ref_to_voidp(r: &'static NamedKeyRegistry) -> *mut c_void { + r as *const NamedKeyRegistry as *mut c_void +} + +#[cfg(feature = "root-vm")] +pub(crate) fn handle_root_vm_init() { + use crate::ffi::ext::lua_ext_keyreg_ref; + use bp3d_debug::debug; + let ptr = ref_to_voidp(Box::leak(Box::new(Mutex::new(HashMap::new())))); + // Pointer set in lua_ext_keyreg_ref to avoid TOCTOU. + let ptr = unsafe { lua_ext_keyreg_ref(ptr) }; + if ptr.is_null() { + debug!("Set up new named key registry..."); + } else { + debug!("Named key registry already exists"); + unsafe { drop(Box::from_raw(voidp_to_ptr(ptr))) }; + } +} + +#[cfg(feature = "root-vm")] +pub(crate) fn handle_root_vm_uninit() { + use crate::ffi::ext::lua_ext_keyreg_unref; + use bp3d_debug::debug; + // Pointer reset to NULL in lua_ext_keyreg_unref to avoid TOCTOU. + let ptr = unsafe { lua_ext_keyreg_unref() }; + if !ptr.is_null() { + debug!("Closing named key registry..."); + unsafe { drop(Box::from_raw(voidp_to_ptr(ptr))) }; + } else { + debug!("Named key registry is still in use"); + } +} + +pub struct Key { + raw: RawKey, + useless: PhantomData<*const T>, +} + +unsafe impl Send for Key {} +unsafe impl Sync for Key {} + +impl Key { + /// Pushes the lua value associated to this registry key on the lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to attach the produced lua value to. + /// + /// returns: ::Value + pub fn push<'a>(&self, vm: &'a Vm) -> Option> { + unsafe { + self.raw.push(vm); + if lua_type(vm.as_ptr(), -1) == Type::Nil { + lua_settop(vm.as_ptr(), -2); // Pop the nil from the stack. + return None; + } + Some(T::from_registry(vm, -1)) + } + } + + /// Pushes the lua value associated to this registry key on the lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to attach the produced lua value to. + /// + /// returns: ::Value + #[inline(always)] + pub fn as_raw(&self) -> &RawKey { + &self.raw + } + + #[inline(always)] + pub const fn new(name: &str) -> Key { + Key { + raw: RawKey::new(name, std::any::type_name::), + useless: PhantomData, + } + } + + #[inline(always)] + pub fn set(&self, value: T::Value<'_>) { + unsafe { T::set_registry(&self.raw, value) } + } +} diff --git a/core/src/vm/registry/send_key.rs b/core/src/vm/registry/send_key.rs new file mode 100644 index 0000000..50bbc4a --- /dev/null +++ b/core/src/vm/registry/send_key.rs @@ -0,0 +1,80 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::ext::lua_ext_getprovenance; +use crate::vm::registry::core::RawKey; +use crate::vm::registry::{FromIndex, Set}; +use crate::vm::Vm; + +pub struct VmCheckedRawKey { + raw: RawKey, + provenance: u64, +} + +impl VmCheckedRawKey { + pub fn push(&self, vm: &Vm) { + assert_eq!( + unsafe { lua_ext_getprovenance(vm.as_ptr()) }, + self.provenance + ); + unsafe { self.raw.push(vm) } + } + + pub fn delete(self, vm: &Vm) { + assert_eq!( + unsafe { lua_ext_getprovenance(vm.as_ptr()) }, + self.provenance + ); + unsafe { self.raw.delete(vm) } + } + + #[inline(always)] + pub fn as_raw(&self) -> RawKey { + self.raw + } +} + +impl FromIndex for VmCheckedRawKey { + unsafe fn from_index(vm: &Vm, index: i32) -> Self { + let raw = RawKey::from_index(vm, index); + Self { + provenance: unsafe { lua_ext_getprovenance(vm.as_ptr()) }, + raw, + } + } +} + +impl Set for VmCheckedRawKey { + unsafe fn set(&self, vm: &Vm, index: i32) { + assert_eq!( + unsafe { lua_ext_getprovenance(vm.as_ptr()) }, + self.provenance + ); + self.raw.set(vm, index); + } +} diff --git a/core/src/vm/registry/types.rs b/core/src/vm/registry/types.rs new file mode 100644 index 0000000..6a0107a --- /dev/null +++ b/core/src/vm/registry/types.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::marker::PhantomData; + +pub struct Table; +pub struct Function; +pub struct Thread; +pub struct LuaRef(PhantomData); diff --git a/core/src/vm/table/core.rs b/core/src/vm/table/core.rs new file mode 100644 index 0000000..5781a6e --- /dev/null +++ b/core/src/vm/table/core.rs @@ -0,0 +1,200 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::ext::{lua_ext_tab_len, MSize}; +use crate::ffi::lua::{ + lua_createtable, lua_gettable, lua_gettop, lua_objlen, lua_pushvalue, lua_rawseti, + lua_setmetatable, lua_settable, lua_topointer, +}; +use crate::vm::table::iter::Iter; +use crate::vm::table::traits::{GetTable, SetTable}; +use crate::vm::value::util::{check_get_metatable, check_push_single}; +use crate::vm::value::{FromLua, IntoLua}; +use crate::vm::Vm; +use std::fmt::{Debug, Display}; + +pub struct Table<'a> { + pub(super) vm: &'a Vm, + index: i32, +} + +impl Clone for Table<'_> { + fn clone(&self) -> Self { + unsafe { lua_pushvalue(self.vm.as_ptr(), self.index) }; + Table { + vm: self.vm, + index: self.vm.top(), + } + } +} + +impl PartialEq for Table<'_> { + fn eq(&self, other: &Self) -> bool { + self.uid() == other.uid() + } +} + +impl Eq for Table<'_> {} + +impl Display for Table<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "table@{:X}", self.uid()) + } +} + +impl Debug for Table<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Table({:?})", self.index) + } +} + +impl<'a> Table<'a> { + /// Creates a table from a raw Vm and index. + /// + /// # Arguments + /// + /// * `vm`: the vm to link to. + /// * `index`: the index on the lua stack. + /// + /// returns: Table + /// + /// # Safety + /// + /// Must ensure that index points to a table and is absolute. If index is not absolute then + /// using the produced table is UB. If the index points to any other type then using the produced + /// table is also UB. + #[inline(always)] + pub unsafe fn from_raw(vm: &'a Vm, index: i32) -> Self { + Self { vm, index } + } + + /// Returns a unique identifier to that table across the Vm it is attached to. + #[inline(always)] + pub fn uid(&self) -> usize { + unsafe { lua_topointer(self.vm.as_ptr(), self.index) as _ } + } + + pub fn new(vm: &'a Vm) -> Self { + unsafe { lua_createtable(vm.as_ptr(), 0, 0) }; + let index = unsafe { lua_gettop(vm.as_ptr()) }; + Self { vm, index } + } + + pub fn with_capacity(vm: &'a Vm, array_capacity: usize, non_array_capcity: usize) -> Self { + unsafe { lua_createtable(vm.as_ptr(), array_capacity as _, non_array_capcity as _) }; + let index = unsafe { lua_gettop(vm.as_ptr()) }; + Self { vm, index } + } + + pub fn len(&self) -> usize { + let mut size: MSize = 0; + let ret = unsafe { lua_ext_tab_len(self.vm.as_ptr(), self.index, &mut size) }; + if ret == 0 { + return size as _; + } + Iter::from_raw(self.vm, self.index).count() as _ + } + + pub fn set_metatable(&mut self, other: Table) { + other.into_lua(self.vm); + unsafe { lua_setmetatable(self.vm.as_ptr(), self.index) }; + } + + pub fn get_metatable(&self) -> Option> { + unsafe { check_get_metatable(self.vm, self.index) } + } + + /// Returns the absolute index of this table on the Lua stack. + #[inline(always)] + pub fn index(&self) -> i32 { + self.index + } + + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Creates a new iterator for this table. + /// + /// This function borrows mutably to avoid messing up the Lua stack while iterating. + pub fn iter(&mut self) -> Iter<'_> { + Iter::from_raw(self.vm, self.index) + } + + pub fn get<'b, T: FromLua<'b>>(&'b self, key: impl GetTable) -> crate::vm::Result { + if T::num_values() != 1 { + return Err(crate::vm::error::Error::MultiValue); + } + unsafe { + key.get_table(self.vm.as_ptr(), self.index)?; + T::from_lua(self.vm, -1) + } + } + + pub fn set(&mut self, key: impl SetTable, value: impl IntoLua) -> crate::vm::Result<()> { + unsafe { + check_push_single(self.vm, value)?; + key.set_table(self.vm.as_ptr(), self.index)?; + } + Ok(()) + } + + pub fn get_any<'b, T: FromLua<'b>>(&'b self, key: impl IntoLua) -> crate::vm::Result { + if T::num_values() != 1 { + return Err(crate::vm::error::Error::MultiValue); + } + unsafe { + check_push_single(self.vm, key)?; + lua_gettable(self.vm.as_ptr(), self.index); + T::from_lua(self.vm, -1) + } + } + + pub fn set_any(&mut self, key: impl IntoLua, value: impl IntoLua) -> crate::vm::Result<()> { + unsafe { + check_push_single(self.vm, key)?; + check_push_single(self.vm, value)?; + lua_settable(self.vm.as_ptr(), self.index); + } + Ok(()) + } + + pub fn push(&mut self, value: impl IntoLua) -> crate::vm::Result<()> { + unsafe { + let len = lua_objlen(self.vm.as_ptr(), self.index); + check_push_single(self.vm, value)?; + lua_rawseti(self.vm.as_ptr(), self.index, len as i32 + 1); + } + Ok(()) + } + + pub fn collect>(self) -> crate::vm::Result { + T::from_lua(self.vm, self.index) + } +} diff --git a/core/src/vm/table/immutable.rs b/core/src/vm/table/immutable.rs new file mode 100644 index 0000000..6075404 --- /dev/null +++ b/core/src/vm/table/immutable.rs @@ -0,0 +1,140 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::table::iter::Iter; +use crate::vm::table::traits::GetTable; +use crate::vm::table::Table; +use crate::vm::value::{FromLua, ImmutableValue, IntoLua}; +use crate::vm::Vm; +use std::fmt::Display; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ImmutableTable<'a>(Table<'a>); + +impl Display for ImmutableTable<'_> { + #[inline(always)] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Table::fmt(&self.0, f) + } +} + +impl<'a> From> for ImmutableTable<'a> { + #[inline(always)] + fn from(value: Table<'a>) -> Self { + Self(value) + } +} + +impl<'a> ImmutableTable<'a> { + /// Creates a table from a raw Vm and index. + /// + /// # Arguments + /// + /// * `vm`: the vm to link to. + /// * `index`: the index on the lua stack. + /// + /// returns: Table + /// + /// # Safety + /// + /// Must ensure that index points to a table and is absolute. If index is not absolute then + /// using the produced table is UB. If the index points to any other type then using the produced + /// table is also UB. + #[inline(always)] + pub unsafe fn from_raw(vm: &'a Vm, index: i32) -> Self { + Self(Table::from_raw(vm, index)) + } + + /// Returns a unique identifier to that table across the Vm it is attached to. + #[inline(always)] + pub fn uid(&self) -> usize { + self.0.uid() + } + + #[inline(always)] + pub fn len(&self) -> usize { + self.0.len() + } + + #[inline(always)] + pub fn get_metatable(&self) -> Option> { + self.0.get_metatable().map(ImmutableTable) + } + + /// Returns the absolute index of this table on the Lua stack. + #[inline(always)] + pub fn index(&self) -> i32 { + self.0.index() + } + + #[inline(always)] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Creates a new iterator for this table. + /// + /// This function borrows mutably to avoid messing up the Lua stack while iterating. + #[inline(always)] + pub fn iter(&mut self) -> Iter<'_> { + self.0.iter() + } + + #[inline(always)] + pub fn get<'b, T: FromLua<'b> + ImmutableValue>( + &'b self, + key: impl GetTable, + ) -> crate::vm::Result { + self.0.get(key) + } + + #[inline(always)] + pub fn get_any<'b, T: FromLua<'b> + ImmutableValue>( + &'b self, + key: impl IntoLua, + ) -> crate::vm::Result { + self.0.get_any(key) + } + + #[inline(always)] + pub fn collect + ImmutableValue>(self) -> crate::vm::Result { + self.0.collect() + } + + /// Returns the underlying Lua Table. + /// + /// # Safety + /// + /// This function violates the contract of this type and is only intended to be used in order to + /// send the table to Lua. Note that you must not use this to expose a UserData metatable as + /// otherwise Lua could override functions like __gc and cause all kinds of UB. + #[inline(always)] + pub unsafe fn to_table(self) -> Table<'a> { + self.0 + } +} diff --git a/core/src/vm/table/interface.rs b/core/src/vm/table/interface.rs new file mode 100644 index 0000000..e02722f --- /dev/null +++ b/core/src/vm/table/interface.rs @@ -0,0 +1,250 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::luaL_checktype; +use crate::ffi::lua::{ + lua_getfield, lua_rawgeti, lua_rawseti, lua_setfield, lua_type, State, Type, +}; +use crate::impl_registry_value; +use crate::util::core::{AnyStr, SimpleDrop}; +use crate::vm::function::{FromParam, IntoParam}; +use crate::vm::registry::{FromIndex, Set}; +use crate::vm::table::traits::{GetTable, SetTable}; +use crate::vm::table::{ImmutableTable, Table}; +use crate::vm::util::{LuaType, TypeName}; +use crate::vm::value::util::{check_type_equals, check_value_top}; +use crate::vm::value::{FromLua, ImmutableValue, IntoLua}; +use crate::vm::Vm; +use std::collections::{BTreeMap, HashMap}; +use std::hash::Hash; + +unsafe impl SimpleDrop for Table<'_> {} + +impl<'a> FromParam<'a> for Table<'a> { + #[inline(always)] + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + luaL_checktype(vm.as_ptr(), index, Type::Table); + Table::from_raw(vm, index) + } + + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + if unsafe { lua_type(vm.as_ptr(), index) } != Type::Table { + return None; + } + Some(unsafe { Table::from_raw(vm, index) }) + } +} + +impl<'a> FromLua<'a> for Table<'a> { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + Table::from_raw(vm, vm.get_absolute_index(index)) + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + check_type_equals(vm, index, Type::Table)?; + Ok(unsafe { Table::from_raw(vm, vm.get_absolute_index(index)) }) + } +} + +impl LuaType for ImmutableTable<'_> { + fn lua_type() -> Vec { + vec![TypeName::Some("table")] + } +} + +unsafe impl SimpleDrop for ImmutableTable<'_> {} +unsafe impl ImmutableValue for ImmutableTable<'_> {} + +impl<'a> FromLua<'a> for ImmutableTable<'a> { + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + Self::from_raw(vm, vm.get_absolute_index(index)) + } + + #[inline(always)] + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + Table::from_lua(vm, index).map(Into::into) + } +} + +impl<'a> FromParam<'a> for ImmutableTable<'a> { + #[inline(always)] + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + Table::from_param(vm, index).into() + } + + #[inline(always)] + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + Table::try_from_param(vm, index).map(Into::into) + } +} + +unsafe impl IntoParam for Table<'_> { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self, vm) as _ + } +} + +unsafe impl IntoLua for Table<'_> { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + check_value_top(self.vm, vm, self.index()) + } +} + +impl LuaType for Table<'_> { + fn lua_type() -> Vec { + vec![TypeName::Some("table")] + } +} + +impl_registry_value!(crate::vm::registry::types::Table => Table); + +impl GetTable for T { + unsafe fn get_table(self, l: State, index: i32) -> crate::vm::Result<()> { + lua_getfield(l, index, self.to_str()?.as_ptr()); + Ok(()) + } +} + +macro_rules! impl_get_set_table { + ($($t: ty),*) => { + $( + impl GetTable for $t { + unsafe fn get_table(self, l: State, index: i32) -> crate::vm::Result<()> { + lua_rawgeti(l, index, self as _); + Ok(()) + } + } + + impl SetTable for $t { + unsafe fn set_table(self, l: State, index: i32) -> crate::vm::Result<()> { + lua_rawseti(l, index, self as _); + Ok(()) + } + } + )* + }; +} + +impl_get_set_table!(i8, i16, i32, i64, u8, u16, u32, u64, usize, isize); + +impl SetTable for T { + unsafe fn set_table(self, l: State, index: i32) -> crate::vm::Result<()> { + lua_setfield(l, index, self.to_str()?.as_ptr()); + Ok(()) + } +} + +impl<'a, T: 'static> FromLua<'a> for Vec +where + for<'b> T: FromLua<'b>, +{ + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + let mut tbl = Table::from_lua_unchecked(vm, index); + let mut vec = Vec::new(); + for (_, v) in tbl.iter() { + let vv = v.get_unchecked(); + vec.push(vv); + } + vec + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let mut tbl = Table::from_lua(vm, index)?; + let mut vec = Vec::new(); + for (_, v) in tbl.iter() { + let vv = v.get()?; + vec.push(vv); + } + Ok(vec) + } +} + +unsafe impl ImmutableValue for Vec {} + +impl<'a, K: 'static, V: 'static> FromLua<'a> for HashMap +where + for<'b> K: FromLua<'b> + Hash + Eq, + for<'b> V: FromLua<'b>, +{ + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + let mut tbl = Table::from_lua_unchecked(vm, index); + let mut map = HashMap::new(); + for (k, v) in tbl.iter() { + let kk = k.get_unchecked(); + let vv = v.get_unchecked(); + map.insert(kk, vv); + } + map + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let mut tbl = Table::from_lua(vm, index)?; + let mut map = HashMap::new(); + for (k, v) in tbl.iter() { + let kk = k.get()?; + let vv = v.get()?; + map.insert(kk, vv); + } + Ok(map) + } +} + +unsafe impl ImmutableValue for HashMap {} + +impl<'a, K: 'static, V: 'static> FromLua<'a> for BTreeMap +where + for<'b> K: FromLua<'b> + Ord, + for<'b> V: FromLua<'b>, +{ + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + let mut tbl = Table::from_lua_unchecked(vm, index); + let mut map = BTreeMap::new(); + for (k, v) in tbl.iter() { + let kk = k.get_unchecked(); + let vv = v.get_unchecked(); + map.insert(kk, vv); + } + map + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let mut tbl = Table::from_lua(vm, index)?; + let mut map = BTreeMap::new(); + for (k, v) in tbl.iter() { + let kk = k.get()?; + let vv = v.get()?; + map.insert(kk, vv); + } + Ok(map) + } +} + +unsafe impl ImmutableValue for BTreeMap {} diff --git a/core/src/vm/table/iter.rs b/core/src/vm/table/iter.rs new file mode 100644 index 0000000..6fd8bfe --- /dev/null +++ b/core/src/vm/table/iter.rs @@ -0,0 +1,93 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_next, lua_pushnil, lua_settop}; +use crate::vm::value::types::Unknown; +use crate::vm::Vm; + +pub struct Iter<'a> { + vm: &'a Vm, + index: i32, + has_ended: bool, + has_started: bool, + last_top: i32, +} + +impl<'a> Iter<'a> { + pub(super) fn from_raw(vm: &'a Vm, index: i32) -> Self { + // Push a nil value on the stack to allow the iterator to work. + unsafe { lua_pushnil(vm.as_ptr()) }; + Self { + vm, + index, + has_ended: false, + has_started: false, + last_top: vm.top(), + } + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = (Unknown<'a>, Unknown<'a>); + + fn next(&mut self) -> Option { + if self.has_started { + // This ensures the iterator remains safe. + if self.vm.top() != self.last_top { + panic!( + "Attempt to iterate on moved values (expected Vm top: {}, got: {})", + self.last_top, + self.vm.top() + ); + } + // Pop the last value on the stack which corresponds to the last value from lua_next. + // Only if the iterator was started. + unsafe { lua_settop(self.vm.as_ptr(), -2) }; + } + let ret = unsafe { lua_next(self.vm.as_ptr(), self.index) }; + self.last_top = self.vm.top(); + self.has_started = true; + if ret != 0 { + let value = unsafe { Unknown::from_raw(self.vm, self.last_top - 1) }; + let key = unsafe { Unknown::from_raw(self.vm, self.last_top) }; + Some((value, key)) + } else { + self.has_ended = true; + None + } + } +} + +impl Drop for Iter<'_> { + fn drop(&mut self) { + if !self.has_ended { + // If the iterator did not reach the end, clear key-value pair from the lua stack. + unsafe { lua_settop(self.vm.as_ptr(), -3) }; + } + } +} diff --git a/core/src/vm/table/mod.rs b/core/src/vm/table/mod.rs new file mode 100644 index 0000000..1d10f79 --- /dev/null +++ b/core/src/vm/table/mod.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod core; +mod immutable; +mod interface; +mod iter; +pub mod traits; + +pub use core::Table; + +pub use immutable::ImmutableTable; diff --git a/core/src/vm/table/traits.rs b/core/src/vm/table/traits.rs new file mode 100644 index 0000000..0602d33 --- /dev/null +++ b/core/src/vm/table/traits.rs @@ -0,0 +1,77 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::State; + +pub trait GetTable { + /// Attempts to push the value identified by key `self` contained in table at index `index` onto + /// the lua stack. + /// + /// # Arguments + /// + /// * `l`: the lua state. + /// * `index`: the index of the table to read from. + /// + /// returns: () + /// + /// # Errors + /// + /// This returns an [Error](crate::vm::error::Error) if the key identified by `self` is not + /// valid for the table at index `index` on the lua stack. + /// + /// # Safety + /// + /// The lua state pointer `l` must not be NULL and must point to a valid [Vm], and the index + /// `index` must point to a valid [Table] on the lua stack. If any of these invariants are not + /// respected, this function is UB. + unsafe fn get_table(self, l: State, index: i32) -> crate::vm::Result<()>; +} + +pub trait SetTable { + /// Attempts to write the value on top of the lua stack into the key identified by `self` in + /// the table at index `index`. + /// + /// # Arguments + /// + /// * `l`: the lua state. + /// * `index`: the index of the table to read from. + /// + /// returns: () + /// + /// # Errors + /// + /// This returns an [Error](crate::vm::error::Error) if the key identified by `self` is not + /// valid for the table at index `index` on the lua stack. + /// + /// # Safety + /// + /// The lua state pointer `l` must not be NULL and must point to a valid [Vm], and the index + /// `index` must point to a valid [Table] on the lua stack. If any of these invariants are not + /// respected, this function is UB. + unsafe fn set_table(self, l: State, index: i32) -> crate::vm::Result<()>; +} diff --git a/core/src/vm/thread/core.rs b/core/src/vm/thread/core.rs new file mode 100644 index 0000000..b3f0d46 --- /dev/null +++ b/core/src/vm/thread/core.rs @@ -0,0 +1,169 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::luaL_error; +use crate::ffi::lua::{ + lua_isyieldable, lua_remove, lua_resume, lua_status, lua_yield, ThreadStatus, +}; +use crate::vm::error::{Error, RuntimeError}; +use crate::vm::function::IntoParam; +use crate::vm::value::{FromLua, IntoLua}; +use crate::vm::Vm; +use std::fmt::{Debug, Display}; +use std::marker::PhantomData; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum State { + Suspended, + Finished, +} + +pub struct Output { + pub state: State, + pub data: T, +} + +pub struct Thread<'a> { + vm: Vm, + useless: PhantomData<&'a ()>, +} + +impl PartialEq for Thread<'_> { + fn eq(&self, other: &Self) -> bool { + self.uid() == other.uid() + } +} + +impl Eq for Thread<'_> {} + +impl Display for Thread<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "thread@{:X}", self.uid()) + } +} + +impl Debug for Thread<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + f.write_str("Thread") + } +} + +impl<'a> Thread<'a> { + /// Creates a thread object from an existing lua thread stack. + /// + /// # Arguments + /// + /// * `l`: the existing raw lua [State](crate::ffi::lua::State). + /// + /// returns: Thread + /// + /// # Safety + /// + /// Must ensure that l is a valid lua thread stack. If not, the resulting object is UB. + #[inline(always)] + pub unsafe fn from_raw(l: crate::ffi::lua::State) -> Self { + Self { + vm: Vm::from_raw(l), + useless: PhantomData, + } + } + + #[inline(always)] + pub fn as_ptr(&self) -> crate::ffi::lua::State { + self.vm.as_ptr() + } + + /// Returns a unique identifier to that table across the Vm it is attached to. + #[allow(clippy::missing_transmute_annotations)] + #[inline(always)] + pub fn uid(&self) -> usize { + unsafe { std::mem::transmute(self.vm.as_ptr()) } + } + + #[inline(always)] + pub fn status(&self) -> ThreadStatus { + unsafe { lua_status(self.vm.as_ptr()) } + } + + pub fn resume<'b, T: FromLua<'b>>(&'b self, args: impl IntoLua) -> crate::vm::Result> + where + T: 'static, /* This clause ensures that a future call to collectgarbage or resume does + not free a lua value which would be borrowed by a previous call to resume */ + { + let num = args.into_lua(&self.vm); + let top = self.vm.top(); + let res = unsafe { lua_resume(self.vm.as_ptr(), num as _) }; + match res { + ThreadStatus::Ok => { + let data = T::from_lua(&self.vm, top)?; + Ok(Output { + state: State::Finished, + data, + }) + } + ThreadStatus::Yield => { + let data = T::from_lua(&self.vm, top)?; + Ok(Output { + state: State::Suspended, + data, + }) + } + ThreadStatus::ErrRun => { + // We've got a runtime error when executing the function. + // TODO: In the future, might be great to traceback the thread as well. + let error_message: &str = FromLua::from_lua(&self.vm, -1)?; + unsafe { lua_remove(self.vm.as_ptr(), -1) }; + Err(Error::Runtime(RuntimeError::new( + String::from(error_message) + "\n", + ))) + } + ThreadStatus::ErrMem => Err(Error::Memory), + ThreadStatus::ErrErr => Err(Error::Error), + _ => std::unreachable!(), + } + } +} + +pub struct Yield(pub T); + +unsafe impl IntoParam for Yield { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + unsafe { + if lua_isyieldable(vm.as_ptr()) != 1 { + luaL_error( + vm.as_ptr(), + c"attempt to yield a non-thread stack object".as_ptr(), + ); + } + let num = self.0.into_param(vm); + lua_yield(vm.as_ptr(), num); + -1 + } + } +} diff --git a/core/src/vm/thread/interface.rs b/core/src/vm/thread/interface.rs new file mode 100644 index 0000000..b36d3f7 --- /dev/null +++ b/core/src/vm/thread/interface.rs @@ -0,0 +1,112 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::luaL_checktype; +use crate::ffi::lua::Type; +use crate::impl_registry_value; +use crate::util::core::SimpleDrop; +use crate::vm::function::{FromParam, IntoParam}; +use crate::vm::registry::{FromIndex, Set}; +use crate::vm::thread::value::{ImmutableThread, Thread}; +use crate::vm::util::LuaType; +use crate::vm::value::util::{check_type_equals, check_value_top}; +use crate::vm::value::{FromLua, ImmutableValue, IntoLua}; +use crate::vm::Vm; + +unsafe impl ImmutableValue for ImmutableThread<'_> {} + +impl<'a> FromLua<'a> for ImmutableThread<'a> { + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + ImmutableThread::from_raw(vm, vm.get_absolute_index(index)) + } + + #[inline(always)] + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + Thread::from_lua(vm, index).map(Into::into) + } +} + +impl<'a> FromLua<'a> for Thread<'a> { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + Thread::from_raw(vm, vm.get_absolute_index(index)) + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + check_type_equals(vm, index, Type::Thread)?; + unsafe { Ok(Thread::from_raw(vm, vm.get_absolute_index(index))) } + } +} + +unsafe impl SimpleDrop for Thread<'_> {} + +impl LuaType for Thread<'_> {} + +unsafe impl SimpleDrop for ImmutableThread<'_> {} + +impl LuaType for ImmutableThread<'_> {} + +impl<'a> FromParam<'a> for Thread<'a> { + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + luaL_checktype(vm.as_ptr(), index, Type::Thread); + Thread::from_raw(vm, vm.get_absolute_index(index)) + } + + #[inline(always)] + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + Thread::from_lua(vm, index).ok() + } +} + +impl<'a> FromParam<'a> for ImmutableThread<'a> { + #[inline(always)] + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + Thread::from_param(vm, index).into() + } + + #[inline(always)] + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + ImmutableThread::from_lua(vm, index).ok() + } +} + +unsafe impl IntoParam for Thread<'_> { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self, vm) as _ + } +} + +unsafe impl IntoLua for Thread<'_> { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + check_value_top(self.vm, vm, self.index()) + } +} + +impl_registry_value!(crate::vm::registry::types::Thread => Thread); diff --git a/src/lib.rs b/core/src/vm/thread/mod.rs similarity index 94% rename from src/lib.rs rename to core/src/vm/thread/mod.rs index 250e249..40f4740 100644 --- a/src/lib.rs +++ b/core/src/vm/thread/mod.rs @@ -1,4 +1,4 @@ -// Copyright (c) 2021, BlockProject 3D +// Copyright (c) 2025, BlockProject 3D // // All rights reserved. // @@ -26,3 +26,6 @@ // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +pub mod core; +mod interface; +pub mod value; diff --git a/core/src/vm/thread/value.rs b/core/src/vm/thread/value.rs new file mode 100644 index 0000000..c451384 --- /dev/null +++ b/core/src/vm/thread/value.rs @@ -0,0 +1,185 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{ + lua_newthread, lua_pushvalue, lua_tothread, lua_type, lua_xmove, ThreadStatus, Type, +}; +use crate::vm::thread::core; +use crate::vm::value::types::Function; +use crate::vm::value::util::move_value_top; +use crate::vm::Vm; +use std::fmt::{Debug, Display}; + +/// Represents a thread object value on a lua stack. +pub struct Thread<'a> { + pub(super) vm: &'a Vm, + index: i32, + thread: core::Thread<'static>, +} + +impl Clone for Thread<'_> { + fn clone(&self) -> Self { + unsafe { lua_pushvalue(self.vm.as_ptr(), self.index) }; + Thread { + vm: self.vm, + index: self.vm.top(), + thread: unsafe { core::Thread::from_raw(self.thread.as_ptr()) }, + } + } +} + +impl PartialEq for Thread<'_> { + fn eq(&self, other: &Self) -> bool { + self.thread.eq(&other.thread) + } +} + +impl Eq for Thread<'_> {} + +impl Display for Thread<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "thread@{:X}", self.thread.uid()) + } +} + +impl Debug for Thread<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Thread({:?})", self.index) + } +} + +impl<'a> Thread<'a> { + /// Creates a thread value from a raw Vm and index on `vm` stack. + /// + /// # Arguments + /// + /// * `vm`: the vm to link to. + /// * `index`: the index on the lua stack. + /// + /// returns: Table + /// + /// # Safety + /// + /// Must ensure that index points to a thread value and is absolute. If index is not absolute + /// then using the produced thread value is UB. If the index points to any other type then + /// using the produced thread value is also UB. + pub unsafe fn from_raw(vm: &'a Vm, index: i32) -> Self { + Self { + vm, + index, + thread: core::Thread::from_raw(lua_tothread(vm.as_ptr(), index)), + } + } + + pub fn new(vm: &'a Vm) -> Self { + let thread = unsafe { core::Thread::from_raw(lua_newthread(vm.as_ptr())) }; + Self { + vm, + index: vm.top(), + thread, + } + } + + pub fn set_function(&self, function: Function<'a>) -> crate::vm::Result<()> { + if self.thread.status() != ThreadStatus::Ok { + return Err(crate::vm::error::Error::BadThreadState); + } + move_value_top(self.vm, function.index()); + unsafe { + lua_xmove(self.vm.as_ptr(), self.thread.as_ptr(), 1); + } + unsafe { + assert_eq!(lua_type(self.thread.as_ptr(), -1), Type::Function); + }; + Ok(()) + } + + /// Returns the absolute index of this table on the Lua stack. + #[inline(always)] + pub fn index(&self) -> i32 { + self.index + } + + /// Returns the thread stack object attached to this thread value. + #[inline(always)] + pub fn as_thread(&self) -> &core::Thread<'a> { + //TODO: Check if this is safe as thread lifetime duration should be as long as thread value + // on the Lua stack; the same goes for ImmutableThread. + &self.thread + } +} + +/// Represents a thread object value on a lua stack. +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct ImmutableThread<'a>(Thread<'a>); + +impl Display for ImmutableThread<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "thread@{:X}", self.0.thread.uid()) + } +} + +impl<'a> From> for ImmutableThread<'a> { + #[inline(always)] + fn from(value: Thread<'a>) -> Self { + Self(value) + } +} + +impl<'a> ImmutableThread<'a> { + /// Creates a thread value from a raw Vm and index on `vm` stack. + /// + /// # Arguments + /// + /// * `vm`: the vm to link to. + /// * `index`: the index on the lua stack. + /// + /// returns: Table + /// + /// # Safety + /// + /// Must ensure that index points to a thread value and is absolute. If index is not absolute + /// then using the produced thread value is UB. If the index points to any other type then + /// using the produced thread value is also UB. + #[inline(always)] + pub unsafe fn from_raw(vm: &'a Vm, index: i32) -> Self { + Self(Thread::from_raw(vm, index)) + } + + /// Returns the absolute index of this table on the Lua stack. + #[inline(always)] + pub fn index(&self) -> i32 { + self.0.index + } + + /// Returns the thread stack object attached to this thread value. + #[inline(always)] + pub fn as_thread(&self) -> &core::Thread<'a> { + &self.0.thread + } +} diff --git a/core/src/vm/userdata/any.rs b/core/src/vm/userdata/any.rs new file mode 100644 index 0000000..10dfc30 --- /dev/null +++ b/core/src/vm/userdata/any.rs @@ -0,0 +1,318 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::luaL_testudata; +use crate::ffi::lua::{ + lua_pushvalue, lua_replace, lua_settop, lua_topointer, lua_touserdata, lua_type, Type, +}; +use crate::util::core::{AnyStr, SimpleDrop}; +use crate::util::LuaFunction; +use crate::vm::error::{Error, TypeError}; +use crate::vm::table::ImmutableTable; +use crate::vm::userdata::{UserData, UserDataImmutable}; +use crate::vm::util::{LuaType, TypeName}; +use crate::vm::value::types::Function; +use crate::vm::value::util::{check_get_metatable, check_value_top}; +use crate::vm::value::{FromLua, ImmutableValue, IntoLua}; +use crate::vm::Vm; +use std::ffi::c_void; +use std::fmt::{Debug, Display}; + +pub struct AnyUserData<'a> { + vm: &'a Vm, + index: i32, +} + +impl Clone for AnyUserData<'_> { + fn clone(&self) -> Self { + unsafe { lua_pushvalue(self.vm.as_ptr(), self.index) }; + AnyUserData { + vm: self.vm, + index: self.vm.top(), + } + } +} + +impl PartialEq for AnyUserData<'_> { + fn eq(&self, other: &Self) -> bool { + self.uid() == other.uid() + } +} + +impl Eq for AnyUserData<'_> {} + +//Stupid fmt name in Rust which causes conflicts... +fn internal_display(ud: &AnyUserData, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let res = ud.vm.scope(|_| { + let res: crate::vm::Result<&str> = ud + .call_method("__tostring", ()) + .or_else(|_| ud.call_method("tostring", ())); + match res { + Ok(v) => { + let type_name = ud.get_type_name()?; + Ok(write!(f, "{}({})", type_name, v)) + } + Err(e) => Err(e), + } + }); + match res { + Ok(v) => v, + Err(_) => write!( + f, + "userdata@{:X}", + unsafe { lua_touserdata(ud.vm.as_ptr(), ud.index) } as usize + ), + } +} + +impl Display for AnyUserData<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + internal_display(self, f) + } +} + +impl Debug for AnyUserData<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "UserData({:?})", self.index) + } +} + +impl<'a> AnyUserData<'a> { + /// Creates an AnyUserData from a raw Vm and index. + /// + /// # Arguments + /// + /// * `vm`: the vm to link to. + /// * `index`: the index on the lua stack. + /// + /// returns: Table + /// + /// # Safety + /// + /// Must ensure that index points to a UserData and is absolute. If index is not absolute then + /// using the produced object is UB. If the index points to any other type then using the produced + /// object is also UB. + #[inline(always)] + pub unsafe fn from_raw(vm: &'a Vm, index: i32) -> Self { + Self { vm, index } + } + + /// Returns the underlying userdata pointer. + pub fn as_ptr(&self) -> *const c_void { + unsafe { lua_topointer(self.vm.as_ptr(), self.index) } + } + + /// Returns a unique identifier to that table across the Vm it is attached to. + pub fn uid(&self) -> usize { + unsafe { lua_topointer(self.vm.as_ptr(), self.index) as _ } + } + + /// Returns a reference to this UserData value cast to `T`. + #[inline(always)] + pub fn get(&self) -> crate::vm::Result<&T> { + crate::vm::value::FromLua::from_lua(self.vm, self.index) + } + + /// Returns a mutable reference to a UserData value. + /// + /// # Safety + /// + /// The caller is responsible for guaranteeing that only a single reference to this object is + /// created. That is no other references to this underlying userdata value must exist in Rust + /// code otherwise using this function is UB. + pub unsafe fn get_mut(&mut self) -> crate::vm::Result<&mut T> { + let this_ptr = + unsafe { luaL_testudata(self.vm.as_ptr(), self.index, T::FULL_TYPE.as_ptr()) } + as *mut T; + if this_ptr.is_null() { + return Err(Error::Type(TypeError { + expected: Type::Userdata, + actual: unsafe { lua_type(self.vm.as_ptr(), self.index) }, + })); + } + Ok(unsafe { &mut *this_ptr }) + } + + pub fn get_metatable(&self) -> Option> { + unsafe { check_get_metatable(self.vm, self.index).map(ImmutableTable::from) } + } + + pub fn get_type_name(&self) -> crate::vm::Result<&str> { + let tbl = self.get_metatable().ok_or(Error::Type(TypeError { + expected: Type::Table, + actual: Type::None, + }))?; + let value: &str = tbl.get(c"__metatable")?; + let value2: &'a str = unsafe { std::mem::transmute(value) }; + unsafe { lua_replace(self.vm.as_ptr(), -2) }; + Ok(value2) + } + + pub fn call_method<'b, T: FromLua<'b>>( + &'b self, + name: impl AnyStr, + args: impl IntoLua, + ) -> crate::vm::Result { + let tbl = self.get_metatable().ok_or(Error::Type(TypeError { + expected: Type::Table, + actual: Type::None, + }))?; + let f: Function = tbl.get(name)?; + let f = LuaFunction::create(f); + unsafe { lua_settop(self.vm.as_ptr(), -2) }; // Pop the metatatble from the stack. + let res: T = f.call(self.vm, (self, args))?; + f.delete(self.vm); + Ok(res) + } +} + +impl<'a> FromLua<'a> for AnyUserData<'a> { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + AnyUserData::from_raw(vm, vm.get_absolute_index(index)) + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let ty = unsafe { lua_type(vm.as_ptr(), index) }; + if ty == Type::Userdata { + Ok(unsafe { AnyUserData::from_raw(vm, vm.get_absolute_index(index)) }) + } else { + Err(Error::Type(TypeError { + expected: Type::Userdata, + actual: ty, + })) + } + } +} + +unsafe impl IntoLua for &AnyUserData<'_> { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + check_value_top(self.vm, vm, self.index) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ImmutableAnyUserData<'a>(AnyUserData<'a>); + +impl Display for ImmutableAnyUserData<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + internal_display(&self.0, f) + } +} + +impl<'a> From> for ImmutableAnyUserData<'a> { + #[inline(always)] + fn from(value: AnyUserData<'a>) -> Self { + Self(value) + } +} + +impl<'a> ImmutableAnyUserData<'a> { + /// Creates an AnyUserData from a raw Vm and index. + /// + /// # Arguments + /// + /// * `vm`: the vm to link to. + /// * `index`: the index on the lua stack. + /// + /// returns: Table + /// + /// # Safety + /// + /// Must ensure that index points to a UserData and is absolute. If index is not absolute then + /// using the produced object is UB. If the index points to any other type then using the produced + /// object is also UB. + #[inline(always)] + pub unsafe fn from_raw(vm: &'a Vm, index: i32) -> Self { + Self(AnyUserData::from_raw(vm, index)) + } + + /// Returns the underlying userdata pointer. + pub fn as_ptr(&self) -> *const c_void { + self.0.as_ptr() + } + + /// Returns a unique identifier to that table across the Vm it is attached to. + #[inline(always)] + pub fn uid(&self) -> usize { + self.0.uid() + } + + /// Returns a reference to this UserData value cast to `T`. + #[inline(always)] + pub fn get(&self) -> crate::vm::Result<&T> { + self.0.get() + } + + #[inline(always)] + pub fn get_metatable(&self) -> Option> { + self.0.get_metatable() + } + + #[inline(always)] + pub fn get_type_name(&self) -> crate::vm::Result<&str> { + self.0.get_type_name() + } + + #[inline(always)] + pub fn call_method<'b, T: FromLua<'b> + ImmutableValue>( + &'b self, + name: impl AnyStr, + args: impl IntoLua, + ) -> crate::vm::Result { + self.0.call_method(name, args) + } +} + +unsafe impl ImmutableValue for ImmutableAnyUserData<'_> {} + +impl<'a> FromLua<'a> for ImmutableAnyUserData<'a> { + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + Self::from_raw(vm, vm.get_absolute_index(index)) + } + + #[inline(always)] + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + AnyUserData::from_lua(vm, index).map(Into::into) + } +} + +unsafe impl SimpleDrop for ImmutableAnyUserData<'_> {} +unsafe impl SimpleDrop for AnyUserData<'_> {} +impl LuaType for AnyUserData<'_> { + fn lua_type() -> Vec { + vec![TypeName::Some("userdata")] + } +} + +impl LuaType for ImmutableAnyUserData<'_> { + fn lua_type() -> Vec { + vec![TypeName::Some("userdata")] + } +} diff --git a/core/src/vm/userdata/case.rs b/core/src/vm/userdata/case.rs new file mode 100644 index 0000000..ee4caf5 --- /dev/null +++ b/core/src/vm/userdata/case.rs @@ -0,0 +1,91 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::userdata::NameConvert; +use bp3d_util::string::BufTools; +use itertools::Itertools; +use std::borrow::Cow; +use std::ffi::{CStr, CString}; + +fn to_string_lossy(bytes: Cow<[u8]>) -> Cow { + match bytes { + Cow::Borrowed(v) => String::from_utf8_lossy(v), + Cow::Owned(v) => String::from(&*String::from_utf8_lossy(&v)).into(), + } +} + +pub struct Snake; + +impl NameConvert for Snake { + fn name_convert(&self, name: &'static CStr) -> Cow<'static, CStr> { + Cow::Borrowed(name) + } +} + +pub struct Camel; + +impl NameConvert for Camel { + fn name_convert(&self, name: &'static CStr) -> Cow<'static, CStr> { + let s = match name.to_str() { + Ok(v) => v, + // Return the same unconverted string if we failed. + Err(_) => return Cow::Borrowed(name), + }; + let s: String = s + .split("_") + .enumerate() + .map(|(i, v)| { + if i != 0 { + v.as_bytes().capitalise_ascii() + } else { + v.as_bytes().into() + } + }) + .map(to_string_lossy) + .join(""); + CString::new(s).unwrap().into() + } +} + +pub struct Pascal; + +impl NameConvert for Pascal { + fn name_convert(&self, name: &'static CStr) -> Cow<'static, CStr> { + let s = match name.to_str() { + Ok(v) => v, + // Return the same unconverted string if we failed. + Err(_) => return Cow::Borrowed(name), + }; + let s: String = s + .split("_") + .map(|v| v.as_bytes().capitalise_ascii()) + .map(to_string_lossy) + .join(""); + CString::new(s).unwrap().into() + } +} diff --git a/core/src/vm/userdata/core.rs b/core/src/vm/userdata/core.rs new file mode 100644 index 0000000..c49e438 --- /dev/null +++ b/core/src/vm/userdata/core.rs @@ -0,0 +1,329 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::{luaL_checkudata, luaL_newmetatable}; +use crate::ffi::lua::{ + lua_getmetatable, lua_pushcclosure, lua_pushlightuserdata, lua_pushnil, lua_pushvalue, + lua_rawget, lua_setfield, lua_setmetatable, lua_settop, lua_touserdata, lua_type, CFunction, + State, Type, GLOBALSINDEX, +}; +use crate::vm::table::Table; +use crate::vm::userdata::{AddGcMethod, Error, LuaDrop, NameConvert, UserData}; +use crate::vm::util::{LuaType, TypeName}; +use crate::vm::value::IntoLua; +use crate::vm::Vm; +use bp3d_debug::{debug, trace, warning}; +use std::cell::OnceCell; +use std::ffi::{c_void, CStr}; +use std::marker::PhantomData; + +#[derive(Copy, Clone)] +pub struct Function { + pub name: &'static CStr, + pub func: CFunction, +} + +pub struct Builder { + is_mutable: bool, + args: Vec, + f: Function, +} + +impl Builder { + pub fn new(name: &'static CStr, func: CFunction) -> Builder { + Builder { + is_mutable: false, + args: Vec::new(), + f: Function { name, func }, + } + } + + pub fn mutable(&mut self) -> &mut Self { + self.is_mutable = true; + self + } + + pub fn arg(&mut self) -> &mut Self { + for ty in T::lua_type() { + self.args.push(ty); + } + self + } + + /// Checks and builds this userdata function + /// + /// # Safety + /// + /// All function arguments must be added through the arg function, if not calling this function + /// is considered UB. + pub unsafe fn build(&self) -> Result { + if self.args.is_empty() { + return Err(Error::ArgsEmpty); + } + if self.f.name == c"__gc" { + return Err(Error::Gc); + } + if self.f.name == c"__metatable" { + return Err(Error::Metatable); + } + if self.is_mutable { + let initial = &self.args[0]; + for v in self.args.iter().skip(1) { + if v == &TypeName::Some("function") + || v == &TypeName::Some("table") + || v == &TypeName::Some("userdata") + { + // Forbid functions, tables and userdata in mutable userdata types. + // This is to ensure no mutable userdata may call back into itself. + return Err(Error::MutViolation(self.f.name)); + } + if initial == v { + return Err(Error::MutViolation(self.f.name)); + } + } + } + Ok(self.f) + } +} + +pub struct Registry<'a, T: UserData, C: NameConvert> { + vm: &'a Vm, + useless: PhantomData, + has_gc: OnceCell<()>, + has_index: OnceCell<()>, + has_static: OnceCell<()>, + case: C, +} + +impl<'a, T: UserData, C: NameConvert> Registry<'a, T, C> { + /// Creates a new [Registry] from the given Vm. + /// + /// # Arguments + /// + /// * `vm`: the vm in which to register the userdata metatable. + /// * `case`: the case converter to apply to each name to be registered. + /// + /// returns: Result, Error> + /// + /// # Safety + /// + /// Running operations on the vm after calling this method is UB unless this [Registry] object + /// is dropped. + pub unsafe fn new(vm: &'a Vm, case: C) -> Result { + if align_of::() > 8 { + return Err(Error::Alignment(align_of::())); + } + Table::new(vm); + let res = unsafe { luaL_newmetatable(vm.as_ptr(), T::FULL_TYPE.as_ptr()) }; + // Pop the userdata metatable alongside its statics table from the stack. + if res != 1 { + unsafe { lua_settop(vm.as_ptr(), -3) }; + return Err(Error::AlreadyRegistered(T::CLASS_NAME)); + } + let reg = Registry { + vm, + useless: PhantomData, + has_gc: OnceCell::new(), + has_index: OnceCell::new(), + has_static: OnceCell::new(), + case, + }; + reg.add_field(c"__metatable", T::CLASS_NAME.to_str().unwrap_unchecked()) + .unwrap_unchecked(); + Ok(reg) + } + + fn add_field(&self, name: &'static CStr, value: impl IntoLua) -> Result<(), Error> { + trace!("Set metatable field {:?}", name); + let num = value.into_lua(self.vm); + if num > 1 { + unsafe { lua_settop(self.vm.as_ptr(), -(num as i32) - 1) }; + return Err(Error::MultiValueField); + } + unsafe { + lua_setfield(self.vm.as_ptr(), -2, name.as_ptr()); + } + Ok(()) + } + + pub fn add_static_field(&self, name: &'static CStr, value: impl IntoLua) -> Result<(), Error> { + let _ = self.has_static.set(()); + let mut static_table = unsafe { Table::from_raw(self.vm, -3) }; + static_table + .set(&*self.case.name_convert(name), value) + .map_err(|_| Error::MultiValueField)?; + Ok(()) + } + + fn add_index_metamethod(&self, f: CFunction) { + warning!({UD=?T::CLASS_NAME}, "Overriding __index on an UserData object may worsen performance of method calls by introducing an additional indirection on the __index metamethod"); + extern "C-unwind" fn __index(l: State) -> i32 { + unsafe { + lua_getmetatable(l, 1); + lua_pushvalue(l, 2); + lua_rawget(l, -2); + let ty = lua_type(l, -1); + if ty != Type::Nil { + return 1; + } + // Pop both the metatatble and the rawget value from the stack before running the + // custom __index method. + lua_settop(l, -3); + let f: CFunction = std::mem::transmute(lua_touserdata(l, GLOBALSINDEX - 1)); + f(l) + } + } + unsafe { + lua_pushlightuserdata(self.vm.as_ptr(), f as *mut c_void); + lua_pushcclosure(self.vm.as_ptr(), __index, 1); + lua_setfield(self.vm.as_ptr(), -2, c"__index".as_ptr()); + } + self.has_index.set(()).unwrap(); + } + + pub fn add_method(&self, f: Function) { + if &f.name.to_bytes() == b"__index" { + self.add_index_metamethod(f.func); + return; + } + unsafe { + lua_pushcclosure(self.vm.as_ptr(), f.func, 0); + if &f.name.to_bytes()[..2] == b"__" { + lua_setfield(self.vm.as_ptr(), -2, f.name.as_ptr()); + } else { + lua_setfield( + self.vm.as_ptr(), + -2, + self.case.name_convert(f.name).as_ptr(), + ); + } + } + } + + pub fn add_gc_method(&self) { + if std::mem::needs_drop::() { + extern "C-unwind" fn run_drop(l: State) -> i32 { + unsafe { + let udata = luaL_checkudata(l, 1, T::FULL_TYPE.as_ptr()) as *mut T; + lua_pushnil(l); + lua_setmetatable(l, 1); + std::ptr::drop_in_place(udata); + } + 0 + } + self.add_method(Function { + name: c"__gc", + func: run_drop::, + }); + debug!({UD=?T::CLASS_NAME}, "Type registered with simple Drop"); + } else { + debug!({UD=?T::CLASS_NAME}, "Type does not need any drop behavior"); + } + self.has_gc.set(()).unwrap(); + } +} + +impl Registry<'_, T, C> { + pub fn add_gc_method_with_lua_drop(&self) { + extern "C-unwind" fn run_lua_drop(l: State) -> i32 { + unsafe { + let udata = luaL_checkudata(l, 1, T::FULL_TYPE.as_ptr()) as *mut T; + lua_pushnil(l); + lua_setmetatable(l, 1); + (*udata).lua_drop(&Vm::from_raw(l)); + } + 0 + } + extern "C-unwind" fn run_lua_drop_full(l: State) -> i32 { + unsafe { + let udata = luaL_checkudata(l, 1, T::FULL_TYPE.as_ptr()) as *mut T; + lua_pushnil(l); + lua_setmetatable(l, 1); + (*udata).lua_drop(&Vm::from_raw(l)); + std::ptr::drop_in_place(udata); + } + 0 + } + if std::mem::needs_drop::() { + self.add_method(Function { + name: c"__gc", + func: run_lua_drop_full::, + }); + debug!({UD=?T::CLASS_NAME}, "Type registered with Drop and LuaDrop"); + } else { + self.add_method(Function { + name: c"__gc", + func: run_lua_drop::, + }); + debug!({UD=?T::CLASS_NAME}, "Type registered with LuaDrop"); + } + self.has_gc.set(()).unwrap(); + } +} + +pub struct AddGcMethodAuto(PhantomData); + +impl Default for AddGcMethodAuto { + fn default() -> Self { + AddGcMethodAuto(PhantomData) + } +} + +impl AddGcMethod for AddGcMethodAuto { + fn add_gc_method(&self, reg: &Registry) { + reg.add_gc_method_with_lua_drop(); + } +} + +impl AddGcMethod for &AddGcMethodAuto { + fn add_gc_method(&self, reg: &Registry) { + reg.add_gc_method(); + } +} + +impl Drop for Registry<'_, T, C> { + fn drop(&mut self) { + if std::mem::needs_drop::() && self.has_gc.get().is_none() { + warning!("No __gc method registered on a userdata type which needs drop!"); + // No __gc method found in object that needs it force add it before finishing it. + self.add_gc_method(); + } + unsafe { + if self.has_index.get().is_none() { + lua_pushvalue(self.vm.as_ptr(), -1); + lua_setfield(self.vm.as_ptr(), -2, c"__index".as_ptr()); + } + if self.has_static.get().is_some() { + lua_pushvalue(self.vm.as_ptr(), -2); // Push the static table. + lua_setfield(self.vm.as_ptr(), -2, c"__static".as_ptr()); + } + // Pop the userdata metatable alongside its statics table from the stack. + lua_settop(self.vm.as_ptr(), -3); + } + } +} diff --git a/core/src/vm/userdata/error.rs b/core/src/vm/userdata/error.rs new file mode 100644 index 0000000..a55343b --- /dev/null +++ b/core/src/vm/userdata/error.rs @@ -0,0 +1,42 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_util::simple_error; +use std::ffi::CStr; + +simple_error! { + pub Error { + ArgsEmpty => "no arguments specified in function, please add at least one argument matching the type of self", + MutViolation(&'static CStr) => "violation of the unique type rule for mutable method {:?}", + Gc => "__gc meta-method is reserved for internal use, if you need Vm access in drop, please use LuaDrop", + Metatable => "__metatable is set for security reasons and cannot be altered", + MultiValueField => "multi-value fields are not supported", + AlreadyRegistered(&'static CStr) => "class name {:?} has already been registered", + Alignment(usize) => "too strict alignment required ({} bytes), max is 8 bytes" + } +} diff --git a/core/src/vm/userdata/interface.rs b/core/src/vm/userdata/interface.rs new file mode 100644 index 0000000..69baa96 --- /dev/null +++ b/core/src/vm/userdata/interface.rs @@ -0,0 +1,77 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::userdata::{core::Registry, Error}; +use crate::vm::Vm; +use std::borrow::Cow; +use std::ffi::CStr; + +/// This trait represents all types of UserData. An UserData is a type with a maximum alignment of 8 +/// with its memory tied to the Lua GC. +#[cfg(feature = "send")] +pub trait UserData: Send + Sized + UserDataType { + fn register(registry: &Registry) -> Result<(), Error>; +} + +/// This trait represents all types of UserData. An UserData is a type with a maximum alignment of 8 +/// with its memory tied to the Lua GC. +#[cfg(not(feature = "send"))] +pub trait UserData: Sized + UserDataType { + fn register(registry: &Registry) -> Result<(), Error>; +} + +/// This trait represents the type identification of an userdata in a Lua VM. +/// +/// # Safety +/// +/// This is UB if FULL_TYPE is duplicated with another userdata type. On the other hand no +/// additional restriction is imposed on CLASS_NAME. +pub unsafe trait UserDataType { + const CLASS_NAME: &'static CStr; + const FULL_TYPE: &'static CStr; +} + +/// This trait represents an UserData which is never borrowed mutably (excluding interior mutability +/// patterns). +/// +/// # Safety +/// +/// This is UB to implement on UserData types which may be borrowed mutably. +pub unsafe trait UserDataImmutable: UserData {} + +pub trait LuaDrop { + fn lua_drop(&self, vm: &Vm); +} + +pub trait AddGcMethod { + fn add_gc_method(&self, reg: &Registry); +} + +pub trait NameConvert { + fn name_convert(&self, name: &'static CStr) -> Cow<'static, CStr>; +} diff --git a/core/src/vm/userdata/mod.rs b/core/src/vm/userdata/mod.rs new file mode 100644 index 0000000..07ccc3d --- /dev/null +++ b/core/src/vm/userdata/mod.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod any; +pub mod case; +pub mod core; +mod error; +mod interface; +pub mod util; + +pub use any::{AnyUserData, ImmutableAnyUserData}; +pub use error::Error; +pub use interface::*; diff --git a/core/src/vm/userdata/util.rs b/core/src/vm/userdata/util.rs new file mode 100644 index 0000000..099eecf --- /dev/null +++ b/core/src/vm/userdata/util.rs @@ -0,0 +1,112 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_getfield, lua_replace, lua_settop, lua_type, Type, REGISTRYINDEX}; +use crate::vm::table::ImmutableTable; +use crate::vm::userdata::UserData; +use crate::vm::Vm; +use std::ffi::CStr; + +/// Returns the static table attached to the given UserData type. +/// +/// The static table contains all static fields and functions added to the type at registration +/// time. +/// +/// This function returns None when the UserData identified with type `T` does not have any static +/// members. +/// +/// # Arguments +/// +/// * `vm`: the [Vm] the UserData type is attached to. +/// +/// returns: Option
+pub fn get_static_table(vm: &Vm) -> Option> { + get_static_table_by_name(vm, T::FULL_TYPE) +} + +/// Returns the static table attached to the given UserData type. +/// +/// The static table contains all static fields and functions added to the type at registration +/// time. +/// +/// This function returns None when the UserData identified with type `T` does not have any static +/// members. +/// +/// # Arguments +/// +/// * `vm`: the [Vm] the UserData type is attached to. +/// * `name`: the name of the UserData type. +/// +/// returns: Option +pub fn get_static_table_by_name<'a>(vm: &'a Vm, name: &CStr) -> Option> { + let val = unsafe { + lua_getfield(vm.as_ptr(), REGISTRYINDEX, name.as_ptr()); + lua_getfield(vm.as_ptr(), -1, c"__static".as_ptr()); + lua_replace(vm.as_ptr(), -2); + if lua_type(vm.as_ptr(), -1) == Type::Nil { + // No static table exists on the given userdata object, skip... + lua_settop(vm.as_ptr(), -2); + return None; + } + ImmutableTable::from_raw(vm, vm.top()) + }; + Some(val) +} + +/// Returns the metatable attached to the given UserData type. +/// +/// # Arguments +/// +/// * `vm`: the [Vm] the UserData type is attached to. +/// * `name`: the name of the UserData type. +/// +/// returns: Option +pub fn get_metatable(vm: &Vm) -> Option> { + get_metatable_by_name(vm, T::FULL_TYPE) +} + +/// Returns the metatable attached to the given UserData type. +/// +/// # Arguments +/// +/// * `vm`: the [Vm] the UserData type is attached to. +/// * `name`: the name of the UserData type. +/// +/// returns: Option +pub fn get_metatable_by_name<'a>(vm: &'a Vm, name: &CStr) -> Option> { + let val = unsafe { + lua_getfield(vm.as_ptr(), REGISTRYINDEX, name.as_ptr()); + if lua_type(vm.as_ptr(), -1) == Type::Nil { + // No metatable exists on the given userdata object, skip... + lua_settop(vm.as_ptr(), -2); + return None; + } + ImmutableTable::from_raw(vm, vm.top()) + }; + Some(val) +} diff --git a/core/src/vm/util.rs b/core/src/vm/util.rs new file mode 100644 index 0000000..30d80b8 --- /dev/null +++ b/core/src/vm/util.rs @@ -0,0 +1,77 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_error, lua_pushlstring, State}; +use std::error::Error; + +#[derive(Debug, PartialEq, Eq)] +pub enum TypeName { + Some(&'static str), + None, +} + +pub trait LuaType { + /// Returns the closest rust type matching this lua value. + fn lua_type() -> Vec { + vec![TypeName::Some(std::any::type_name::())] + } +} + +impl LuaType for Option { + fn lua_type() -> Vec { + let mut v = T::lua_type(); + v.push(TypeName::None); + v + } +} + +/// Converts a Rust error to a Lua error. This function does not return as it unwinds using luajit. +/// +/// # Arguments +/// +/// * `l`: the lua State on which to raise the lua exception. +/// * `error`: the Rust error to be converted. +/// +/// returns: ! +/// +/// # Safety +/// +/// It is UB to call this function outside a lua [CFunction](crate::ffi::lua::CFunction). +pub unsafe fn lua_rust_error(l: State, error: E) -> ! { + // At this point the function is assumed to be a non-POF (error and String). + let s = format!("rust error: {}", error); + lua_pushlstring(l, s.as_ptr() as _, s.len()); + // Drop both the error and the error string. + // Very important as lua_error does not return. + drop(error); + drop(s); + // Now the function should be back what Rust calls a POF. + lua_error(l); + // If this is reached, then lua_error has silently failed. + std::unreachable!() +} diff --git a/core/src/vm/value/any.rs b/core/src/vm/value/any.rs new file mode 100644 index 0000000..f615637 --- /dev/null +++ b/core/src/vm/value/any.rs @@ -0,0 +1,289 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_pushnil, lua_toboolean, lua_tonumber, lua_type, Type}; +use crate::util::core::SimpleDrop; +use crate::vm::error::{Error, TypeError}; +use crate::vm::function::{FromParam, IntoParam}; +use crate::vm::table::{ImmutableTable, Table}; +use crate::vm::thread::value::{ImmutableThread, Thread}; +use crate::vm::userdata::{AnyUserData, ImmutableAnyUserData}; +use crate::vm::util::{lua_rust_error, LuaType}; +use crate::vm::value::function::Function; +use crate::vm::value::{FromLua, ImmutableValue, IntoLua}; +use crate::vm::Vm; +use std::fmt::Display; +use std::str::FromStr; + +pub type Any<'a> = AnyValue<'a, Table<'a>, AnyUserData<'a>, Thread<'a>>; +pub type AnyImmutable<'a> = + AnyValue<'a, ImmutableTable<'a>, ImmutableAnyUserData<'a>, ImmutableThread<'a>>; + +#[derive(Debug, PartialEq, Clone)] +pub enum AnyValue<'a, T, U, R> { + None, + Nil, + Number(f64), + Int64(i64), + UInt64(u64), + Boolean(bool), + String(&'a str), + Buffer(&'a [u8]), + Function(Function<'a>), + Table(T), + UserData(U), + Thread(R), +} + +impl Eq for AnyValue<'_, T, U, R> {} + +impl Display for AnyValue<'_, T, U, R> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AnyValue::None => f.write_str(""), + AnyValue::Nil => f.write_str("nil"), + AnyValue::Number(v) => write!(f, "{}", v), + AnyValue::Boolean(v) => write!(f, "{}", v), + AnyValue::String(v) => write!(f, "{}", v), + AnyValue::Buffer(v) => write!(f, "{:?}", v), + AnyValue::Function(v) => write!(f, "{}", v), + AnyValue::Table(v) => write!(f, "{}", v), + AnyValue::UserData(v) => write!(f, "{}", v), + AnyValue::Thread(v) => write!(f, "{}", v), + AnyValue::Int64(v) => write!(f, "{}", v), + AnyValue::UInt64(v) => write!(f, "{}", v), + } + } +} + +impl AnyValue<'_, T, U, R> { + pub fn ty(&self) -> Type { + match self { + AnyValue::None => Type::None, + AnyValue::Nil => Type::Nil, + AnyValue::Number(_) => Type::Number, + AnyValue::Boolean(_) => Type::Boolean, + AnyValue::String(_) => Type::String, + AnyValue::Buffer(_) => Type::String, + AnyValue::Function(_) => Type::Function, + AnyValue::Table(_) => Type::Table, + AnyValue::UserData(_) => Type::Userdata, + AnyValue::Thread(_) => Type::Thread, + AnyValue::Int64(_) => Type::Cdata, + AnyValue::UInt64(_) => Type::Cdata, + } + } + + pub fn to_number(&self) -> Result { + match self { + AnyValue::Number(v) => Ok(*v), + AnyValue::String(v) => { + crate::ffi::lua::RawNumber::from_str(v).map_err(|_| Error::ParseFloat) + } + _ => Err(Error::Type(TypeError { + expected: Type::Number, + actual: self.ty(), + })), + } + } + + pub fn to_integer(&self) -> Result { + match self { + AnyValue::Number(v) => Ok(*v as _), + AnyValue::String(v) => i64::from_str(v).map_err(|_| Error::ParseInt), + AnyValue::Int64(v) => Ok(*v), + AnyValue::UInt64(v) => Ok(*v as _), + _ => Err(Error::Type(TypeError { + expected: Type::Number, + actual: self.ty(), + })), + } + } + + pub fn to_uinteger(&self) -> Result { + match self { + AnyValue::Number(v) => Ok(*v as _), + AnyValue::String(v) => u64::from_str(v).map_err(|_| Error::ParseInt), + AnyValue::Int64(v) => Ok(*v as _), + AnyValue::UInt64(v) => Ok(*v), + _ => Err(Error::Type(TypeError { + expected: Type::Number, + actual: self.ty(), + })), + } + } +} + +unsafe impl IntoLua for Any<'_> { + fn into_lua(self, vm: &Vm) -> u16 { + match self { + AnyValue::None => 0, + AnyValue::Nil => { + unsafe { lua_pushnil(vm.as_ptr()) }; + 1 + } + AnyValue::Number(v) => v.into_lua(vm), + AnyValue::Boolean(v) => v.into_lua(vm), + AnyValue::String(v) => v.into_lua(vm), + AnyValue::Buffer(v) => v.into_lua(vm), + AnyValue::Function(v) => v.into_lua(vm), + AnyValue::Table(v) => v.into_lua(vm), + AnyValue::UserData(_) => 0, + AnyValue::Thread(_) => 0, + AnyValue::Int64(v) => v.into_lua(vm), + AnyValue::UInt64(v) => v.into_lua(vm), + } + } +} + +unsafe impl IntoParam for Any<'_> { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self, vm) as _ + } +} + +impl<'a, T: FromLua<'a>, U: FromLua<'a>, R: FromLua<'a>> FromLua<'a> for AnyValue<'a, T, U, R> { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + Self::from_lua(vm, index).unwrap_unchecked() + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let ty = unsafe { lua_type(vm.as_ptr(), index) }; + match ty { + Type::None => Ok(AnyValue::None), + Type::Nil => Ok(AnyValue::Nil), + Type::Boolean => { + let value = unsafe { lua_toboolean(vm.as_ptr(), index) }; + Ok(AnyValue::Boolean(value == 1)) + } + Type::LightUserdata => Err(Error::UnsupportedType(ty)), + Type::Number => { + let value = unsafe { lua_tonumber(vm.as_ptr(), index) }; + Ok(AnyValue::Number(value)) + } + Type::String => { + let buffer: &[u8] = unsafe { FromLua::from_lua_unchecked(vm, index) }; + match std::str::from_utf8(buffer) { + Ok(s) => Ok(AnyValue::String(s)), + Err(_) => Ok(AnyValue::Buffer(buffer)), + } + } + Type::Table => Ok(unsafe { AnyValue::Table(FromLua::from_lua_unchecked(vm, index)) }), + Type::Function => { + Ok(unsafe { AnyValue::Function(FromLua::from_lua_unchecked(vm, index)) }) + } + Type::Userdata => { + Ok(unsafe { AnyValue::UserData(FromLua::from_lua_unchecked(vm, index)) }) + } + Type::Thread => Ok(unsafe { AnyValue::Thread(FromLua::from_lua_unchecked(vm, index)) }), + Type::Cdata => i64::from_lua(vm, index) + .map(AnyValue::Int64) + .or_else(|_| u64::from_lua(vm, index).map(AnyValue::UInt64)) + .map_err(|_| Error::UnsupportedType(ty)), + } + } +} + +unsafe impl SimpleDrop for AnyValue<'_, T, U, R> {} + +impl LuaType for AnyValue<'_, T, U, R> {} + +impl< + 'a, + T: FromLua<'a> + SimpleDrop + LuaType, + U: FromLua<'a> + SimpleDrop + LuaType, + R: FromLua<'a> + SimpleDrop + LuaType, + > FromParam<'a> for AnyValue<'a, T, U, R> +{ + #[inline(always)] + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + match FromLua::from_lua(vm, index) { + Ok(v) => v, + Err(e) => lua_rust_error(vm.as_ptr(), e), + } + } + + #[inline(always)] + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + FromLua::from_lua(vm, index).ok() + } +} + +unsafe impl ImmutableValue for AnyImmutable<'_> {} + +/// A marker struct to run lua code which may return any number of values on the stack. +pub struct AnyParam; + +impl FromLua<'_> for AnyParam { + #[inline(always)] + unsafe fn from_lua_unchecked(_: &Vm, _: i32) -> Self { + AnyParam + } + + #[inline(always)] + fn from_lua(_: &Vm, _: i32) -> crate::vm::Result { + Ok(AnyParam) + } + + #[inline(always)] + fn num_values() -> i16 { + -1 + } +} + +/// A raw primitive to return arbitrary count of values from a C function. +pub struct UncheckedAnyReturn(i32); + +impl UncheckedAnyReturn { + /// Construct a [UncheckedAnyReturn]. + /// + /// # Panic + /// + /// This function panics when the count of arguments is greater than the lua stack size itself. + /// + /// # Safety + /// + /// It is UB to run any operation which may alter the lua stack after constructing this + /// primitive. Using this to return the metatable of an UserData is also UB. + pub unsafe fn new(vm: &Vm, count: i32) -> Self { + let top = vm.top(); + if count > top as _ { + panic!() + } + UncheckedAnyReturn(count) + } +} + +unsafe impl IntoParam for UncheckedAnyReturn { + #[inline(always)] + fn into_param(self, _: &Vm) -> i32 { + self.0 as _ + } +} diff --git a/core/src/vm/value/core.rs b/core/src/vm/value/core.rs new file mode 100644 index 0000000..d0d4e33 --- /dev/null +++ b/core/src/vm/value/core.rs @@ -0,0 +1,510 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::ext::{ + lua_ext_getinteger64, lua_ext_getuinteger64, lua_ext_pushinteger64, lua_ext_pushuinteger64, + lua_ext_tointeger64, lua_ext_touinteger64, +}; +use crate::ffi::laux::{luaL_setmetatable, luaL_testudata}; +use crate::ffi::lua::{ + lua_newuserdata, lua_pushboolean, lua_pushinteger, lua_pushlstring, lua_pushnil, + lua_pushnumber, lua_settop, lua_toboolean, lua_tointeger, lua_tointegerx, lua_tolstring, + lua_tonumber, lua_tonumberx, lua_touserdata, lua_type, Type, +}; +use crate::vm::error::{Error, TypeError}; +use crate::vm::userdata::{UserData, UserDataImmutable}; +use crate::vm::value::types::{Boolean, Integer, Number}; +use crate::vm::value::util::check_type_equals; +use crate::vm::value::{FromLua, ImmutableValue, IntoLua}; +use crate::vm::Vm; +use std::borrow::Cow; + +impl<'a> FromLua<'a> for &'a str { + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + let mut len: usize = 0; + let s = lua_tolstring(vm.as_ptr(), index, &mut len as _); + let slice = std::slice::from_raw_parts(s as _, len); + std::str::from_utf8_unchecked(slice) + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let l = vm.as_ptr(); + unsafe { + let ty = lua_type(l, index); + match ty { + Type::String => { + let mut len: usize = 0; + let s = lua_tolstring(l, index, &mut len as _); + let slice = std::slice::from_raw_parts(s as _, len); + std::str::from_utf8(slice).map_err(|e| Error::InvalidUtf8(e.into())) + } + _ => Err(Error::Type(TypeError { + expected: Type::String, + actual: ty, + })), + } + } + } +} + +unsafe impl ImmutableValue for &str {} + +impl<'a> FromLua<'a> for &'a [u8] { + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + let mut len: usize = 0; + let str = lua_tolstring(vm.as_ptr(), index, &mut len as _); + let slice = std::slice::from_raw_parts(str as *const u8, len); + slice + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let l = vm.as_ptr(); + unsafe { + let ty = lua_type(l, index); + match ty { + Type::String => { + let mut len: usize = 0; + let s = lua_tolstring(l, index, &mut len as _); + let slice = std::slice::from_raw_parts(s as *const u8, len); + Ok(slice) + } + _ => Err(Error::Type(TypeError { + expected: Type::String, + actual: ty, + })), + } + } + } +} + +unsafe impl ImmutableValue for &[u8] {} + +impl FromLua<'_> for String { + unsafe fn from_lua_unchecked(vm: &'_ Vm, index: i32) -> Self { + let s: &str = FromLua::from_lua_unchecked(vm, index); + s.into() + } + + fn from_lua(vm: &'_ Vm, index: i32) -> crate::vm::Result { + let s: &str = FromLua::from_lua(vm, index)?; + Ok(s.into()) + } +} + +unsafe impl ImmutableValue for String {} + +impl FromLua<'_> for Box<[u8]> { + unsafe fn from_lua_unchecked(vm: &'_ Vm, index: i32) -> Self { + let bytes: &[u8] = FromLua::from_lua_unchecked(vm, index); + bytes.into() + } + + fn from_lua(vm: &'_ Vm, index: i32) -> crate::vm::Result { + let bytes: &[u8] = FromLua::from_lua(vm, index)?; + Ok(bytes.into()) + } +} + +unsafe impl ImmutableValue for Box<[u8]> {} + +macro_rules! impl_from_lua { + ($t: ty, $expected: ident, $func: ident, $push_func: ident, $($ret: tt)*) => { + impl FromLua<'_> for $t { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &Vm, index: i32) -> Self { + $func(vm.as_ptr(), index) $($ret)* + } + + fn from_lua(vm: &Vm, index: i32) -> crate::vm::Result { + check_type_equals(vm, index, Type::$expected)?; + Ok(unsafe { $func(vm.as_ptr(), index) $($ret)* }) + } + } + + unsafe impl IntoLua for $t { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + unsafe { + $push_func(vm.as_ptr(), self as _); + 1 + } + } + } + + unsafe impl ImmutableValue for $t {} + }; +} + +macro_rules! impl_from_lua_64 { + ($t: ty, $get_func: ident, $func: ident, $push_func: ident) => { + #[cfg(target_pointer_width = "64")] + impl FromLua<'_> for $t { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &'_ Vm, index: i32) -> Self { + $func(vm.as_ptr(), index) as _ + } + + fn from_lua(vm: &'_ Vm, index: i32) -> crate::vm::Result { + let mut out = 0; + let val = unsafe { $get_func(vm.as_ptr(), index, &mut out) }; + if val == 1 { + Ok(out as _) + } else { + Err(TypeError::from_stack(Type::Number, vm, index)) + } + } + } + + #[cfg(target_pointer_width = "64")] + unsafe impl IntoLua for $t { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + unsafe { $push_func(vm.as_ptr(), self as _) as _ } + } + } + + unsafe impl ImmutableValue for $t {} + }; +} + +impl_from_lua_64!( + i64, + lua_ext_getinteger64, + lua_ext_tointeger64, + lua_ext_pushinteger64 +); +impl_from_lua_64!( + u64, + lua_ext_getuinteger64, + lua_ext_touinteger64, + lua_ext_pushuinteger64 +); + +impl_from_lua_64!( + isize, + lua_ext_getinteger64, + lua_ext_tointeger64, + lua_ext_pushinteger64 +); +impl_from_lua_64!( + usize, + lua_ext_getuinteger64, + lua_ext_touinteger64, + lua_ext_pushuinteger64 +); + +#[cfg(target_pointer_width = "32")] +impl_from_lua!(isize, Number, lua_tointeger, lua_pushinteger, as _); + +#[cfg(target_pointer_width = "32")] +impl_from_lua!(usize, Number, lua_tointeger, lua_pushinteger, as _); + +impl_from_lua!(i8, Number, lua_tointeger, lua_pushinteger, as _); +impl_from_lua!(u8, Number, lua_tointeger, lua_pushinteger, as _); +impl_from_lua!(i16, Number, lua_tointeger, lua_pushinteger, as _); +impl_from_lua!(u16, Number, lua_tointeger, lua_pushinteger, as _); +impl_from_lua!(i32, Number, lua_tointeger, lua_pushinteger, as _); +impl_from_lua!(u32, Number, lua_tointeger, lua_pushinteger, as _); + +impl_from_lua!(f32, Number, lua_tonumber, lua_pushnumber, as _); +impl_from_lua!(f64, Number, lua_tonumber, lua_pushnumber, as _); + +impl_from_lua!(bool, Boolean, lua_toboolean, lua_pushboolean, == 1); + +impl FromLua<'_> for () { + #[inline(always)] + unsafe fn from_lua_unchecked(_: &'_ Vm, _: i32) -> Self {} + + #[inline(always)] + fn from_lua(_vm: &Vm, _: i32) -> crate::vm::Result<()> { + Ok(()) + } + + #[inline(always)] + fn num_values() -> i16 { + 0 + } +} + +unsafe impl ImmutableValue for () {} + +impl<'a, T: UserDataImmutable> FromLua<'a> for &'a T { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + &*(lua_touserdata(vm.as_ptr(), index) as *const T) + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let this_ptr = + unsafe { luaL_testudata(vm.as_ptr(), index, T::FULL_TYPE.as_ptr()) } as *const T; + if this_ptr.is_null() { + return Err(Error::Type(TypeError { + expected: Type::Userdata, + actual: unsafe { lua_type(vm.as_ptr(), index) }, + })); + } + Ok(unsafe { &*this_ptr }) + } +} + +unsafe impl ImmutableValue for &T {} + +macro_rules! impl_from_lua_tuple { + ($($name: ident: $name2: ident ($name3: tt)),*) => { + unsafe impl<$($name: ImmutableValue),*> ImmutableValue for ($($name),*) {} + + impl<'a, $($name: FromLua<'a>),*> FromLua<'a> for ($($name),*) { + #[inline(always)] + fn num_values() -> i16 { + $($name::num_values()+)* + 0 + } + + unsafe fn from_lua_unchecked(vm: &'a Vm, mut index: i32) -> Self { + impl_from_lua_tuple!(_from_lua_unchecked vm, index, $($name2: $name),*); + ($($name2),*) + } + + fn from_lua(vm: &'a Vm, mut index: i32) -> crate::vm::Result<($($name),*)> { + impl_from_lua_tuple!(_from_lua vm, index, $($name2: $name),*); + Ok(($($name2),*)) + } + } + + unsafe impl<$($name: IntoLua),*> IntoLua for ($($name),*) { + fn into_lua(self, vm: &Vm) -> u16 { + $( + self.$name3.into_lua(vm) + + )* + 0 + } + } + }; + + (_from_lua_unchecked $vm: ident, $index: ident, $name2: ident: $name: ident) => { + let $name2: $name = FromLua::from_lua_unchecked($vm, $index); + }; + + (_from_lua_unchecked $vm: ident, $index: ident, $name2: ident: $name: ident, $($name3: ident: $name4: ident),*) => { + let $name2: $name = FromLua::from_lua_unchecked($vm, $index); + $index += 1; + impl_from_lua_tuple!(_from_lua_unchecked $vm, $index, $($name3: $name4),*); + }; + + (_from_lua $vm: ident, $index: ident, $name2: ident: $name: ident) => { + let $name2: $name = FromLua::from_lua($vm, $index)?; + }; + + (_from_lua $vm: ident, $index: ident, $name2: ident: $name: ident, $($name3: ident: $name4: ident),*) => { + let $name2: $name = FromLua::from_lua($vm, $index)?; + $index += 1; + impl_from_lua_tuple!(_from_lua $vm, $index, $($name3: $name4),*); + }; +} + +impl_from_lua_tuple!(T: t (0), T1: t1 (1)); +impl_from_lua_tuple!(T: t (0), T1: t1 (1), T2: t2 (2)); +impl_from_lua_tuple!(T: t (0), T1: t1 (1), T2: t2 (2), T3: t3 (3)); +impl_from_lua_tuple!(T: t (0), T1: t1 (1), T2: t2 (2), T3: t3 (3), T4: t4 (4)); +impl_from_lua_tuple!(T: t (0), T1: t1 (1), T2: t2 (2), T3: t3 (3), T4: t4 (4), T5: t5 (5)); +impl_from_lua_tuple!(T: t (0), T1: t1 (1), T2: t2 (2), T3: t3 (3), T4: t4 (4), T5: t5 (5), T6: t6 (6)); +impl_from_lua_tuple!(T: t (0), T1: t1 (1), T2: t2 (2), T3: t3 (3), T4: t4 (4), T5: t5 (5), T6: t6 (6), T7: t7 (7)); +impl_from_lua_tuple!(T: t (0), T1: t1 (1), T2: t2 (2), T3: t3 (3), T4: t4 (4), T5: t5 (5), T6: t6 (6), T7: t7 (7), T8: t8 (8)); +impl_from_lua_tuple!(T: t (0), T1: t1 (1), T2: t2 (2), T3: t3 (3), T4: t4 (4), T5: t5 (5), T6: t6 (6), T7: t7 (7), T8: t8 (8), T9: t9 (9)); + +impl<'a, T: FromLua<'a>> FromLua<'a> for Option { + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self { + let ty = unsafe { lua_type(vm.as_ptr(), index) }; + if ty == Type::Nil { + // Clear the nil value at the top of the stack. + if index == -1 { + unsafe { lua_settop(vm.as_ptr(), -2) }; + } + None + } else { + Some(FromLua::from_lua_unchecked(vm, index)) + } + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + let ty = unsafe { lua_type(vm.as_ptr(), index) }; + if ty == Type::Nil { + // Clear the nil value at the top of the stack. + if index == -1 { + unsafe { lua_settop(vm.as_ptr(), -2) }; + } + Ok(None) + } else { + Ok(Some(FromLua::from_lua(vm, index)?)) + } + } +} + +unsafe impl ImmutableValue for Option {} + +unsafe impl IntoLua for T { + fn into_lua(self, vm: &Vm) -> u16 { + let userdata = unsafe { lua_newuserdata(vm.as_ptr(), size_of::()) } as *mut T; + unsafe { userdata.write(self) }; + unsafe { luaL_setmetatable(vm.as_ptr(), T::FULL_TYPE.as_ptr()) }; + 1 + } +} + +unsafe impl IntoLua for () { + #[inline(always)] + fn into_lua(self, _: &Vm) -> u16 { + 0 + } +} + +unsafe impl IntoLua for Option { + fn into_lua(self, vm: &Vm) -> u16 { + match self { + None => unsafe { + lua_pushnil(vm.as_ptr()); + 1 + }, + Some(v) => v.into_lua(vm), + } + } +} + +unsafe impl IntoLua for &str { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + self.as_bytes().into_lua(vm) + } +} + +unsafe impl IntoLua for &[u8] { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + unsafe { lua_pushlstring(vm.as_ptr(), self.as_ptr() as _, self.len()) }; + 1 + } +} + +unsafe impl IntoLua for String { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + (&*self).into_lua(vm) + } +} + +unsafe impl IntoLua for Box<[u8]> { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + self.as_ref().into_lua(vm) + } +} + +unsafe impl<'a, T: IntoLua + Clone> IntoLua for Cow<'a, T> +where + &'a T: IntoLua, +{ + fn into_lua(self, vm: &Vm) -> u16 { + match self { + Cow::Borrowed(v) => v.into_lua(vm), + Cow::Owned(v) => v.into_lua(vm), + } + } +} + +impl FromLua<'_> for Integer { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &'_ Vm, index: i32) -> Self { + Integer(lua_tointeger(vm.as_ptr(), index)) + } + + fn from_lua(vm: &'_ Vm, index: i32) -> crate::vm::Result { + let mut ok = 0; + let num = unsafe { lua_tointegerx(vm.as_ptr(), index, &mut ok) }; + if ok != 1 { + Err(TypeError::from_stack(Type::Number, vm, index)) + } else { + Ok(Integer(num)) + } + } +} + +unsafe impl ImmutableValue for Integer {} + +impl FromLua<'_> for Number { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &'_ Vm, index: i32) -> Self { + Number(lua_tonumber(vm.as_ptr(), index)) + } + + fn from_lua(vm: &'_ Vm, index: i32) -> crate::vm::Result { + let mut ok = 0; + let num = unsafe { lua_tonumberx(vm.as_ptr(), index, &mut ok) }; + if ok != 1 { + Err(TypeError::from_stack(Type::Number, vm, index)) + } else { + Ok(Number(num)) + } + } +} + +unsafe impl ImmutableValue for Number {} + +impl FromLua<'_> for Boolean { + unsafe fn from_lua_unchecked(vm: &'_ Vm, index: i32) -> Self { + Boolean(unsafe { lua_toboolean(vm.as_ptr(), index) == 1 }) + } + + fn from_lua(vm: &'_ Vm, index: i32) -> crate::vm::Result { + Ok(Boolean(unsafe { lua_toboolean(vm.as_ptr(), index) == 1 })) + } +} + +unsafe impl ImmutableValue for Boolean {} + +unsafe impl IntoLua for Number { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + unsafe { lua_pushnumber(vm.as_ptr(), self.0) } + 1 + } +} + +unsafe impl IntoLua for Integer { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + unsafe { lua_pushinteger(vm.as_ptr(), self.0) } + 1 + } +} + +unsafe impl IntoLua for Boolean { + fn into_lua(self, vm: &Vm) -> u16 { + unsafe { lua_pushboolean(vm.as_ptr(), if self.0 { 1 } else { 0 }) } + 1 + } +} diff --git a/core/src/vm/value/function.rs b/core/src/vm/value/function.rs new file mode 100644 index 0000000..8d8f6e4 --- /dev/null +++ b/core/src/vm/value/function.rs @@ -0,0 +1,154 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::laux::luaL_checktype; +use crate::ffi::lua::{lua_pushvalue, lua_topointer, Type}; +use crate::impl_registry_value; +use crate::util::core::SimpleDrop; +use crate::vm::core::util::{pcall, push_error_handler}; +use crate::vm::function::{FromParam, IntoParam}; +use crate::vm::registry::{FromIndex, Set}; +use crate::vm::util::{LuaType, TypeName}; +use crate::vm::value::util::{check_type_equals, check_value_top}; +use crate::vm::value::{FromLua, ImmutableValue, IntoLua}; +use crate::vm::Vm; +use std::fmt::{Debug, Display}; + +pub struct Function<'a> { + vm: &'a Vm, + index: i32, +} + +impl Clone for Function<'_> { + fn clone(&self) -> Self { + unsafe { lua_pushvalue(self.vm.as_ptr(), self.index) }; + Function { + vm: self.vm, + index: self.vm.top(), + } + } +} + +impl PartialEq for Function<'_> { + fn eq(&self, other: &Self) -> bool { + let a = unsafe { lua_topointer(self.vm.as_ptr(), self.index) }; + let b = unsafe { lua_topointer(other.vm.as_ptr(), other.index) }; + a == b + } +} + +impl Eq for Function<'_> {} + +impl Display for Function<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "function@{:X}", + unsafe { lua_topointer(self.vm.as_ptr(), self.index) } as usize + ) + } +} + +impl Debug for Function<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "LuaFunction({:?})", self.index) + } +} + +unsafe impl SimpleDrop for Function<'_> {} + +impl LuaType for Function<'_> { + fn lua_type() -> Vec { + vec![TypeName::Some("function")] + } +} + +impl<'a> FromParam<'a> for Function<'a> { + unsafe fn from_param(vm: &'a Vm, index: i32) -> Self { + unsafe { luaL_checktype(vm.as_ptr(), index, Type::Function) }; + Function { vm, index } + } + + fn try_from_param(vm: &'a Vm, index: i32) -> Option { + FromLua::from_lua(vm, index).ok() + } +} + +unsafe impl IntoParam for Function<'_> { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self, vm) as _ + } +} + +unsafe impl IntoLua for Function<'_> { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + check_value_top(self.vm, vm, self.index()) + } +} + +impl Function<'_> { + pub fn call<'b, R: FromLua<'b>>(&'b self, value: impl IntoLua) -> crate::vm::Result { + let pos = unsafe { push_error_handler(self.vm.as_ptr()) }; + unsafe { + lua_pushvalue(self.vm.as_ptr(), self.index); + } + let num_values = value.into_lua(self.vm); + unsafe { pcall(self.vm, num_values as _, R::num_values() as _, pos)? }; + R::from_lua(self.vm, -(R::num_values() as i32)) + } + + /// Returns the absolute index of this function on the Lua stack. + #[inline(always)] + pub fn index(&self) -> i32 { + self.index + } +} + +impl<'a> FromLua<'a> for Function<'a> { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Function<'a> { + Function { + vm, + index: vm.get_absolute_index(index), + } + } + + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result { + check_type_equals(vm, index, Type::Function)?; + Ok(Function { + vm, + index: vm.get_absolute_index(index), + }) + } +} + +impl_registry_value!(crate::vm::registry::types::Function => Function); + +unsafe impl ImmutableValue for Function<'_> {} diff --git a/core/src/vm/value/integer53.rs b/core/src/vm/value/integer53.rs new file mode 100644 index 0000000..ffbf2a2 --- /dev/null +++ b/core/src/vm/value/integer53.rs @@ -0,0 +1,249 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::ext::lua_ext_fast_checkinteger; +use crate::ffi::lua::{lua_pushinteger, lua_tointeger, RawInteger, Type}; +use crate::util::core::{SimpleDrop, TryFromIntError}; +use crate::vm::function::{FromParam, IntoParam}; +use crate::vm::util::{LuaType, TypeName}; +use crate::vm::value::util::check_type_equals; +use crate::vm::value::{FromLua, ImmutableValue, IntoLua}; +use crate::vm::Vm; + +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone, Debug)] +pub struct Int53(i64); + +#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Copy, Clone, Debug)] +pub struct UInt53(u64); + +impl Int53 { + pub const MIN: Int53 = Int53(-(2 << 51)); + pub const MAX: Int53 = Int53((2 << 51) - 1); + + pub const fn from_isize_lossy(value: isize) -> Int53 { + if (value as i64) < Self::MIN.0 { + Self::MIN + } else if (value as i64) > Self::MAX.0 { + Self::MAX + } else { + Int53(value as _) + } + } + + pub const fn from_i64_lossy(value: i64) -> Int53 { + if value < Self::MIN.0 { + Self::MIN + } else if value > Self::MAX.0 { + Self::MAX + } else { + Int53(value) + } + } + + #[inline(always)] + pub const fn to_i64(self) -> i64 { + self.0 + } + + #[inline(always)] + pub const fn to_isize(self) -> isize { + self.0 as _ + } +} + +impl UInt53 { + pub const MIN: UInt53 = UInt53(0); + pub const MAX: UInt53 = UInt53((2 << 52) - 1); + + #[inline(always)] + pub const fn from_u64_lossy(value: u64) -> UInt53 { + UInt53(value & Self::MAX.0) + } + + #[inline(always)] + pub const fn from_usize_lossy(value: usize) -> UInt53 { + UInt53((value as u64) & Self::MAX.0) + } + + #[inline(always)] + pub const fn to_u64(self) -> u64 { + self.0 + } + + #[inline(always)] + pub const fn to_usize(self) -> usize { + self.0 as _ + } +} + +impl From for i64 { + #[inline(always)] + fn from(val: Int53) -> i64 { + val.to_i64() + } +} + +impl From for u64 { + #[inline(always)] + fn from(val: UInt53) -> u64 { + val.to_u64() + } +} + +impl TryFrom for UInt53 { + type Error = TryFromIntError; + + fn try_from(value: Int53) -> Result { + if value.0 < 0 { + Err(TryFromIntError) + } else { + Ok(UInt53(value.0 as u64)) + } + } +} + +impl TryFrom for Int53 { + type Error = TryFromIntError; + + fn try_from(value: UInt53) -> Result { + if value.0 > Int53::MAX.0 as _ { + Err(TryFromIntError) + } else { + Ok(Int53(value.0 as i64)) + } + } +} + +impl TryFrom for Int53 { + type Error = TryFromIntError; + + fn try_from(value: i64) -> Result { + if value > Int53::MAX.0 || value < Int53::MIN.0 { + Err(TryFromIntError) + } else { + Ok(Int53(value)) + } + } +} + +impl TryFrom for Int53 { + type Error = TryFromIntError; + + fn try_from(value: u64) -> Result { + if value > Int53::MAX.0 as _ { + Err(TryFromIntError) + } else { + Ok(Int53(value as _)) + } + } +} + +impl TryFrom for UInt53 { + type Error = TryFromIntError; + + fn try_from(value: i64) -> Result { + if value < 0 || value > UInt53::MAX.0 as _ { + Err(TryFromIntError) + } else { + Ok(UInt53(value as _)) + } + } +} + +impl TryFrom for UInt53 { + type Error = TryFromIntError; + + fn try_from(value: u64) -> Result { + if value > UInt53::MAX.0 as _ { + Err(TryFromIntError) + } else { + Ok(UInt53(value as _)) + } + } +} + +macro_rules! impl_from_lua { + ($t: ty, $expected: ident, $func: ident, $push_func: ident) => { + unsafe impl SimpleDrop for $t {} + + impl LuaType for $t { + fn lua_type() -> Vec { + vec![TypeName::Some(std::any::type_name::())] + } + } + + impl FromLua<'_> for $t { + #[inline(always)] + unsafe fn from_lua_unchecked(vm: &Vm, index: i32) -> Self { + Self($func(vm.as_ptr(), index) as _) + } + + fn from_lua(vm: &Vm, index: i32) -> crate::vm::Result { + check_type_equals(vm, index, Type::$expected)?; + Ok(Self(unsafe { $func(vm.as_ptr(), index) as _ })) + } + } + + unsafe impl IntoLua for $t { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + unsafe { + $push_func(vm.as_ptr(), self.0 as _); + 1 + } + } + } + + unsafe impl ImmutableValue for $t {} + + impl FromParam<'_> for $t { + #[inline(always)] + unsafe fn from_param(vm: &Vm, index: i32) -> Self { + Self(lua_ext_fast_checkinteger(vm.as_ptr(), index) as _) + } + + #[inline(always)] + fn try_from_param(vm: &Vm, index: i32) -> Option { + FromLua::from_lua(vm, index).ok() + } + } + + unsafe impl IntoParam for $t { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + unsafe { + lua_pushinteger(vm.as_ptr(), self.0 as _); + 1 + } + } + } + }; +} + +impl_from_lua!(Int53, Number, lua_tointeger, lua_pushinteger); +impl_from_lua!(UInt53, Number, lua_tointeger, lua_pushinteger); diff --git a/core/src/vm/value/interface.rs b/core/src/vm/value/interface.rs new file mode 100644 index 0000000..918d5ac --- /dev/null +++ b/core/src/vm/value/interface.rs @@ -0,0 +1,92 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::vm::Vm; + +// WTF this is broken, if you do not indent then it becomes unreadable. I chose readability over +// un-readability. +#[allow(clippy::doc_overindented_list_items)] +pub trait FromLua<'a>: Sized { + /// Reads the value at the specified index in the given [Vm]. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to read from. + /// * `index`: the index at which to try reading the value from. The index does not have to be + /// absolute. + /// + /// returns: Result + /// + /// # Safety + /// + /// This function assumes the type of the value at index `index` is already of the expected type, + /// if not, calling this function is UB. + unsafe fn from_lua_unchecked(vm: &'a Vm, index: i32) -> Self; + + /// Attempt to read the value at the specified index in the given [Vm]. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to read from. + /// * `index`: the index at which to try reading the value from. The index does not have to be + /// absolute. + /// + /// returns: Result + fn from_lua(vm: &'a Vm, index: i32) -> crate::vm::Result; + + /// Returns the number of values to be expected on the lua stack, after reading this value. + #[inline(always)] + fn num_values() -> i16 { + 1 + } +} + +/// This trait represents a value convertible to lua outside Rust function calls. For lua values +/// returned by Rust functions, see [IntoParam](crate::vm::function::IntoParam). +/// +/// # Safety +/// +/// When implementing this trait, ensure that the number returned by +/// [into_lua](IntoLua::into_lua) is EXACTLY equal to the number of values pushed onto the lua +/// stack. If more or fewer than advertised values exists on the stack after the call then the impl +/// is considered UB. +pub unsafe trait IntoLua: Sized { + /// Attempt to push self onto the top of the stack in the given [Vm]. + /// + /// Returns the number values pushed into the lua stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] to push into. + /// + /// returns: u16 number of elements pushed onto the Lua stack. + fn into_lua(self, vm: &Vm) -> u16; +} + +/// Marker trait that represents a value which cannot be mutated. +pub unsafe trait ImmutableValue {} diff --git a/core/src/vm/value/mod.rs b/core/src/vm/value/mod.rs new file mode 100644 index 0000000..a1fb77e --- /dev/null +++ b/core/src/vm/value/mod.rs @@ -0,0 +1,39 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +pub mod any; +mod core; +mod function; +mod integer53; +mod interface; +mod raw_ptr; +pub mod types; +mod unknown; +pub mod util; + +pub use interface::*; diff --git a/core/src/vm/value/raw_ptr.rs b/core/src/vm/value/raw_ptr.rs new file mode 100644 index 0000000..ad5ca65 --- /dev/null +++ b/core/src/vm/value/raw_ptr.rs @@ -0,0 +1,92 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_pushlightuserdata, lua_touserdata}; +use crate::util::core::SimpleDrop; +use crate::vm::value::{ImmutableValue, IntoLua}; +use crate::vm::Vm; + +#[derive(Debug)] +pub struct RawPtr(*const T); + +unsafe impl SimpleDrop for RawPtr {} + +impl Clone for RawPtr { + #[inline(always)] + fn clone(&self) -> Self { + Self(self.0) + } +} + +impl Copy for RawPtr {} + +impl RawPtr { + #[inline(always)] + pub fn new(ptr: *mut T) -> Self { + Self(ptr) + } + + /// Returns the raw underlying pointer. + #[inline(always)] + pub fn as_ptr(&self) -> *const T { + self.0 + } + + /// Returns the raw underlying pointer. + #[inline(always)] + pub fn as_mut_ptr(&self) -> *mut T { + self.0 as *mut T + } + + /// Extracts a [RawPtr] from the given Lua [Vm] index. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] where to read the lightuserdata from. + /// * `index`: the index of the lightuserdata pointer on the stack. + /// + /// # Safety + /// + /// Calling this function assumes the given index is valid for [Vm] and the lightuserdata + /// pointer points to an instance of T. If any of these assumptions are not respected, + /// this function is UB. + #[inline(always)] + pub unsafe fn from_lua(vm: &Vm, index: i32) -> Self { + Self(lua_touserdata(vm.as_ptr(), index) as _) + } +} + +unsafe impl IntoLua for RawPtr { + #[inline(always)] + fn into_lua(self, vm: &Vm) -> u16 { + unsafe { lua_pushlightuserdata(vm.as_ptr(), self.0 as _) }; + 1 + } +} + +unsafe impl ImmutableValue for RawPtr {} diff --git a/core/src/vm/value/types.rs b/core/src/vm/value/types.rs new file mode 100644 index 0000000..7ad5eaa --- /dev/null +++ b/core/src/vm/value/types.rs @@ -0,0 +1,58 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{RawInteger, RawNumber}; +use crate::util::core::SimpleDrop; +use crate::vm::util::LuaType; + +pub use super::function::Function; +pub use super::raw_ptr::RawPtr; +pub use super::unknown::Unknown; + +pub use super::integer53::*; + +#[derive(Copy, Clone, PartialOrd, PartialEq, Debug)] +pub struct Number(pub RawNumber); + +unsafe impl SimpleDrop for Number {} + +impl LuaType for Number {} + +#[derive(Copy, Clone, PartialOrd, PartialEq, Debug, Eq, Ord, Hash)] +pub struct Integer(pub RawInteger); + +unsafe impl SimpleDrop for Integer {} + +impl LuaType for Integer {} + +#[derive(Copy, Clone, PartialOrd, PartialEq, Debug, Eq, Ord, Hash)] +pub struct Boolean(pub bool); + +unsafe impl SimpleDrop for Boolean {} + +impl LuaType for Boolean {} diff --git a/core/src/vm/value/unknown.rs b/core/src/vm/value/unknown.rs new file mode 100644 index 0000000..1092315 --- /dev/null +++ b/core/src/vm/value/unknown.rs @@ -0,0 +1,114 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{lua_replace, lua_type, Type}; +use crate::vm::function::IntoParam; +use crate::vm::value::any::Any; +use crate::vm::value::util::check_value_top; +use crate::vm::value::{FromLua, IntoLua}; +use crate::vm::Vm; +use std::fmt::Debug; + +pub struct Unknown<'a> { + vm: &'a Vm, + index: i32, +} + +impl Debug for Unknown<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Table({:?})", self.index) + } +} + +impl<'a> Unknown<'a> { + /// Attempts to create an [Unknown] typed value from a specific index on the stack. + /// + /// # Arguments + /// + /// * `vm`: the [Vm] object this value is attached to. + /// * `index`: the index of the value on the stack. + /// + /// returns: Unknown + /// + /// # Safety + /// + /// The given stack index must be absolute, if not this is UB. Using this to return the + /// metatable of an UserData is also UB. + pub unsafe fn from_raw(vm: &'a Vm, index: i32) -> Self { + Self { vm, index } + } + + /// Interprets the underlying reference on the lua stack as the specified Rust type. + pub fn get>(&self) -> crate::vm::Result { + T::from_lua(self.vm, self.index) + } + + /// Interprets the underlying reference on the lua stack as the specified Rust type. + /// + /// # Safety + /// + /// This function assumes the type of the value at index `index` is already of the expected type, + /// if not, calling this function is UB. + pub unsafe fn get_unchecked>(&self) -> T { + T::from_lua_unchecked(self.vm, self.index) + } + + pub fn ty(&self) -> Type { + unsafe { lua_type(self.vm.as_ptr(), self.index) } + } + + pub fn to_any(self) -> crate::vm::Result> { + Any::from_lua(self.vm, self.index) + } + + pub fn set(&mut self, value: impl IntoLua) { + value.into_lua(self.vm); + unsafe { lua_replace(self.vm.as_ptr(), self.index) }; + } + + pub fn index(&self) -> i32 { + self.index + } +} + +unsafe impl IntoLua for Unknown<'_> { + fn into_lua(self, vm: &Vm) -> u16 { + if self.ty() == Type::None { + // None is not a value, do not operate the stack or UB. + return 0; // No value exists on the stack so IntoLua returns 0 values. + } + check_value_top(self.vm, vm, self.index) + } +} + +unsafe impl IntoParam for Unknown<'_> { + #[inline(always)] + fn into_param(self, vm: &Vm) -> i32 { + IntoLua::into_lua(self, vm) as _ + } +} diff --git a/core/src/vm/value/util.rs b/core/src/vm/value/util.rs new file mode 100644 index 0000000..cc22808 --- /dev/null +++ b/core/src/vm/value/util.rs @@ -0,0 +1,146 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::ffi::lua::{ + lua_getmetatable, lua_gettop, lua_pushnil, lua_pushvalue, lua_replace, lua_settop, lua_type, + Type, +}; +use crate::vm::error::{Error, TypeError}; +use crate::vm::table::Table; +use crate::vm::value::IntoLua; +use crate::vm::Vm; + +/// Ensures the given lua value at index is of a specified type. +#[inline(always)] +pub fn check_type_equals(vm: &Vm, index: i32, expected: Type) -> crate::vm::Result<()> { + let ty = unsafe { lua_type(vm.as_ptr(), index) }; + if ty == expected { + //FIXME: likely branch + Ok(()) + } else { + Err(Error::Type(TypeError { + expected, + actual: ty, + })) + } +} + +/// Ensures the given lua value at index is at the top of the stack. +/// If the value at index is not at the top of the stack, this function moves it to the top and +/// replaces the original index by a nil value. +#[inline(always)] +pub fn move_value_top(vm: &Vm, index: i32) { + let index = vm.get_absolute_index(index); + if index != vm.top() { + let l = vm.as_ptr(); + unsafe { + lua_pushvalue(l, index); + lua_pushnil(l); + lua_replace(l, index); // Replace the value at index by a nil. + } + } +} + +/// Ensures a single value is pushed onto the lua stack, this function automatically reverts the +/// stack if value pushed more than 1 element onto the stack. +/// +/// # Arguments +/// +/// * `vm`: the vm to operate on. +/// * `value`: the value to be placed on the lua stack. +pub fn check_push_single(vm: &Vm, value: impl IntoLua) -> crate::vm::Result<()> { + let nums = value.into_lua(vm); + if nums != 1 { + // Clear the stack. + unsafe { lua_settop(vm.as_ptr(), -(nums as i32) - 1) }; + return Err(Error::MultiValue); + } + Ok(()) +} + +/// Attempts to retrieve the metatable attached to the object at index `index`. +/// +/// If no metatable is found, this function automatically pops the value if needed and returns +/// [None]. +/// +/// # Arguments +/// +/// * `vm`: the [Vm] instance to extract the metatable from. +/// * `index`: the object index inside the `vm` [Vm]. +/// +/// returns: Option
+/// +/// # Safety +/// +/// This should never be used to pass the metatable of a [userdata](crate::vm::userdata) type to +/// Lua or even modify the returned metatable. If any of these are not maintained this is UB. +pub unsafe fn check_get_metatable(vm: &Vm, index: i32) -> Option> { + unsafe { lua_getmetatable(vm.as_ptr(), index) }; + let ty = unsafe { lua_type(vm.as_ptr(), -1) }; + if ty == Type::Table { + return Some(unsafe { Table::from_raw(vm, vm.top()) }); + } + if ty != Type::None { + // A none type is special and implies no value was pushed on the stack. + unsafe { lua_settop(vm.as_ptr(), -2) }; // Pops the value from the stack. + } + None +} + +/// Pushes the value at `index` to the top of the stack if both [Vm] objects points to the exact +/// same State. +/// +/// # Arguments +/// +/// * `src_vm`: the source [Vm] object. +/// * `dst_vm`: the target [Vm] object. +/// * `index`: the index on source [Vm]. +pub fn check_push_value(src_vm: &Vm, dst_vm: &Vm, index: i32) -> u16 { + assert!(src_vm.as_ptr() == dst_vm.as_ptr()); + unsafe { lua_pushvalue(src_vm.as_ptr(), index) }; + 1 +} + +/// Pushes the value at `index` to the top of the stack if both [Vm] objects points to the exact +/// same State and if index is not already at the top of the stack. If `index` is already at the +/// top of the stack, this function doesn't perform any operations on the Lua stack identified by +/// `dst_vm`. +/// +/// # Arguments +/// +/// * `src_vm`: the source [Vm] object. +/// * `dst_vm`: the target [Vm] object. +/// * `index`: the index on source [Vm]. Must be absolute. +pub fn check_value_top(src_vm: &Vm, dst_vm: &Vm, index: i32) -> u16 { + assert!(src_vm.as_ptr() == dst_vm.as_ptr()); + let top = unsafe { lua_gettop(src_vm.as_ptr()) }; + if top != index { + unsafe { lua_pushvalue(src_vm.as_ptr(), index) }; + } + 1 +} diff --git a/core/tests/lua/basic.lua b/core/tests/lua/basic.lua new file mode 100644 index 0000000..02d21cc --- /dev/null +++ b/core/tests/lua/basic.lua @@ -0,0 +1,5 @@ +function main() + error("nope") +end + +return 1 + main() diff --git a/core/tests/lua/broken.lua b/core/tests/lua/broken.lua new file mode 100644 index 0000000..62e484b --- /dev/null +++ b/core/tests/lua/broken.lua @@ -0,0 +1,4 @@ +function bro + end + +return + diff --git a/core/tests/lua/class.lua b/core/tests/lua/class.lua new file mode 100644 index 0000000..076c4e7 --- /dev/null +++ b/core/tests/lua/class.lua @@ -0,0 +1,60 @@ +--- @param name string +--- @param parent table? +function AbstractClass(name, parent) + local class = {} + if not class.init then + class.init = function(_) end + end + class.__name = name + class.__index = class + if parent then + setmetatable(class, parent) + end + return class +end + +--- @param name string +--- @param parent table? +function Class(name, parent) + local class = {} + if not class.init then + class.init = function(_) end + end + class.__name = name + class.new = function(...) + local obj = {} + setmetatable(obj, class) + obj:init(...) + return obj + end + class.__index = class + if parent then + setmetatable(class, parent) + end + return class +end + +--- @class Parent +--- @field init function +local Parent = AbstractClass("Parent") + +function Parent:value() + return 42 +end + +--- @class Child: Parent +--- @field new function +local Child = Class("Child", Parent) + +function Child:init(a) + Parent.init(self) + self._a = a +end + +function Child:value2() + return self:value() + self._a +end + +local obj = Child.new(42) +assert(obj:value2() == 84) +assert(obj:value() == 42) diff --git a/core/tests/safety/test_destructor_arc_not_send.rs b/core/tests/safety/test_destructor_arc_not_send.rs new file mode 100644 index 0000000..c2fd522 --- /dev/null +++ b/core/tests/safety/test_destructor_arc_not_send.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::marker::PhantomData; +use bp3d_lua::vm::RootVm; +use bp3d_lua::vm::closure::arc::Shared; + +// Should be !Send +#[derive(Debug)] +struct UnSendType(PhantomData<*const ()>); + +fn main() { + let vm = RootVm::new(); + let obj = Shared::new(UnSendType(PhantomData)); + bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, obj); +} diff --git a/core/tests/safety/test_destructor_arc_not_send.stderr b/core/tests/safety/test_destructor_arc_not_send.stderr new file mode 100644 index 0000000..8958bcb --- /dev/null +++ b/core/tests/safety/test_destructor_arc_not_send.stderr @@ -0,0 +1,22 @@ +error[E0277]: the trait bound `std::sync::Arc: RawSend` is not satisfied + --> tests/safety/test_destructor_arc_not_send.rs:40:60 + | +40 | bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, obj); + | ------------------------------------------------- ^^^ the trait `RawSend` is not implemented for `std::sync::Arc` + | | + | required by a bound introduced by this call + | + = help: the trait `RawSend` is implemented for `std::sync::Arc` +note: required by a bound in `Pool::attach_send` + --> src/vm/core/destructor.rs + | + | pub fn attach_send(vm: &Vm, raw: R) -> R::Ptr + | ^^^^^^^ required by this bound in `Pool::attach_send` + +error[E0277]: the trait bound `std::sync::Arc: RawSend` is not satisfied + --> tests/safety/test_destructor_arc_not_send.rs:40:5 + | +40 | bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, obj); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `RawSend` is not implemented for `std::sync::Arc` + | + = help: the trait `RawSend` is implemented for `std::sync::Arc` diff --git a/core/tests/safety/test_destructor_arc_not_sync.rs b/core/tests/safety/test_destructor_arc_not_sync.rs new file mode 100644 index 0000000..41fce9d --- /dev/null +++ b/core/tests/safety/test_destructor_arc_not_sync.rs @@ -0,0 +1,43 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::marker::PhantomData; +use bp3d_lua::vm::RootVm; +use bp3d_lua::vm::closure::arc::Shared; + +// Should be Send but !Sync +#[derive(Debug)] +struct UnSyncType(PhantomData<*const ()>); + +unsafe impl Send for UnSyncType {} + +fn main() { + let vm = RootVm::new(); + let obj = Shared::new(UnSyncType(PhantomData)); + bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, obj); +} diff --git a/core/tests/safety/test_destructor_arc_not_sync.stderr b/core/tests/safety/test_destructor_arc_not_sync.stderr new file mode 100644 index 0000000..8f2fe57 --- /dev/null +++ b/core/tests/safety/test_destructor_arc_not_sync.stderr @@ -0,0 +1,22 @@ +error[E0277]: the trait bound `std::sync::Arc: RawSend` is not satisfied + --> tests/safety/test_destructor_arc_not_sync.rs:42:60 + | +42 | bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, obj); + | ------------------------------------------------- ^^^ the trait `RawSend` is not implemented for `std::sync::Arc` + | | + | required by a bound introduced by this call + | + = help: the trait `RawSend` is implemented for `std::sync::Arc` +note: required by a bound in `Pool::attach_send` + --> src/vm/core/destructor.rs + | + | pub fn attach_send(vm: &Vm, raw: R) -> R::Ptr + | ^^^^^^^ required by this bound in `Pool::attach_send` + +error[E0277]: the trait bound `std::sync::Arc: RawSend` is not satisfied + --> tests/safety/test_destructor_arc_not_sync.rs:42:5 + | +42 | bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, obj); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `RawSend` is not implemented for `std::sync::Arc` + | + = help: the trait `RawSend` is implemented for `std::sync::Arc` diff --git a/core/tests/safety/test_destructor_box_not_send.rs b/core/tests/safety/test_destructor_box_not_send.rs new file mode 100644 index 0000000..6acafd2 --- /dev/null +++ b/core/tests/safety/test_destructor_box_not_send.rs @@ -0,0 +1,39 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::marker::PhantomData; +use bp3d_lua::vm::RootVm; + +// Should be !Send +#[derive(Debug)] +struct UnSendType(PhantomData<*const ()>); + +fn main() { + let vm = RootVm::new(); + bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, Box::new(UnSendType(PhantomData))); +} diff --git a/core/tests/safety/test_destructor_box_not_send.stderr b/core/tests/safety/test_destructor_box_not_send.stderr new file mode 100644 index 0000000..6e9a9fb --- /dev/null +++ b/core/tests/safety/test_destructor_box_not_send.stderr @@ -0,0 +1,25 @@ +error[E0277]: `*const ()` cannot be sent between threads safely + --> tests/safety/test_destructor_box_not_send.rs:38:5 + | +38 | bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, Box::new(UnSendType(PhantomData))); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `*const ()` cannot be sent between threads safely + | + = help: within `UnSendType`, the trait `Send` is not implemented for `*const ()` + = help: the trait `RawSend` is implemented for `std::sync::Arc` +note: required because it appears within the type `PhantomData<*const ()>` + --> $RUST/core/src/marker.rs + | + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `UnSendType` + --> tests/safety/test_destructor_box_not_send.rs:34:8 + | +34 | struct UnSendType(PhantomData<*const ()>); + | ^^^^^^^^^^ + = note: required for `Unique` to implement `Send` +note: required because it appears within the type `Box` + --> $RUST/alloc/src/boxed.rs + | + | pub struct Box< + | ^^^ + = note: required for `Box` to implement `RawSend` diff --git a/core/tests/safety/test_destructor_rc_not_send.rs b/core/tests/safety/test_destructor_rc_not_send.rs new file mode 100644 index 0000000..e359ba4 --- /dev/null +++ b/core/tests/safety/test_destructor_rc_not_send.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::marker::PhantomData; +use bp3d_lua::vm::RootVm; +use bp3d_lua::vm::closure::rc::Shared; + +// Should be !Send +#[derive(Debug)] +struct UnSendType(PhantomData<*const ()>); + +fn main() { + let vm = RootVm::new(); + let obj = Shared::new(UnSendType(PhantomData)); + bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, obj); +} diff --git a/core/tests/safety/test_destructor_rc_not_send.stderr b/core/tests/safety/test_destructor_rc_not_send.stderr new file mode 100644 index 0000000..e18fa59 --- /dev/null +++ b/core/tests/safety/test_destructor_rc_not_send.stderr @@ -0,0 +1,9 @@ +error[E0277]: `std::rc::Rc` cannot be sent between threads safely + --> tests/safety/test_destructor_rc_not_send.rs:40:5 + | +40 | bp3d_lua::vm::core::destructor::Pool::attach_send(&vm, obj); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `std::rc::Rc` cannot be sent between threads safely + | + = help: the trait `Send` is not implemented for `std::rc::Rc` + = help: the trait `RawSend` is implemented for `std::sync::Arc` + = note: required for `std::rc::Rc` to implement `RawSend` diff --git a/core/tests/safety/test_multi_root_vms_not_send.rs b/core/tests/safety/test_multi_root_vms_not_send.rs new file mode 100644 index 0000000..10ab1ec --- /dev/null +++ b/core/tests/safety/test_multi_root_vms_not_send.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::vm::core::UnSendRootVm; + +fn main() { + let root = UnSendRootVm::new(); + std::thread::spawn(move || { + // Should fail to build because UnSendRootVm is not supposed to be Send. + root.run_code::<()>(c"").unwrap(); + }); +} diff --git a/core/tests/safety/test_multi_root_vms_not_send.stderr b/core/tests/safety/test_multi_root_vms_not_send.stderr new file mode 100644 index 0000000..de1e116 --- /dev/null +++ b/core/tests/safety/test_multi_root_vms_not_send.stderr @@ -0,0 +1,48 @@ +error[E0277]: `*mut c_void` cannot be sent between threads safely + --> tests/safety/test_multi_root_vms_not_send.rs:33:24 + | +33 | std::thread::spawn(move || { + | ------------------ ^------ + | | | + | _____|__________________within this `{closure@$DIR/tests/safety/test_multi_root_vms_not_send.rs:33:24: 33:31}` + | | | + | | required by a bound introduced by this call +34 | | // Should fail to build because UnSendRootVm is not supposed to be Send. +35 | | root.run_code::<()>(c"").unwrap(); +36 | | }); + | |_____^ `*mut c_void` cannot be sent between threads safely + | + = help: within `{closure@$DIR/tests/safety/test_multi_root_vms_not_send.rs:33:24: 33:31}`, the trait `Send` is not implemented for `*mut c_void` +note: required because it appears within the type `bp3d_lua::ffi::lua::State` + --> src/ffi/lua.rs + | + | pub struct State(*mut c_void); + | ^^^^^ +note: required because it appears within the type `bp3d_lua::vm::Vm` + --> src/vm/core/vm.rs + | + | pub struct Vm { + | ^^ +note: required because it appears within the type `bp3d_lua::vm::core::root_vm::common::UnsafeRootVm` + --> src/vm/core/root_vm/common.rs + | + | pub struct UnsafeRootVm(pub Vm); + | ^^^^^^^^^^^^ +note: required because it appears within the type `bp3d_lua::vm::core::root_vm::unsend::RootVm` + --> src/vm/core/root_vm/unsend.rs + | + | pub struct RootVm { + | ^^^^^^ +note: required because it's used within this closure + --> tests/safety/test_multi_root_vms_not_send.rs:33:24 + | +33 | std::thread::spawn(move || { + | ^^^^^^^ +note: required by a bound in `spawn` + --> $RUST/std/src/thread/mod.rs + | + | pub fn spawn(f: F) -> JoinHandle + | ----- required by a bound in this function +... + | F: Send + 'static, + | ^^^^ required by this bound in `spawn` diff --git a/core/tests/safety/test_rclosure_not_send.rs b/core/tests/safety/test_rclosure_not_send.rs new file mode 100644 index 0000000..7138fda --- /dev/null +++ b/core/tests/safety/test_rclosure_not_send.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use std::marker::PhantomData; +use bp3d_lua::vm::RootVm; +use bp3d_lua::vm::closure::types::RClosure; + +// Should be !Send +#[derive(Debug)] +struct UnSendType(PhantomData<*const ()>); + +fn main() { + let ty = UnSendType(PhantomData); + let vm = RootVm::new(); + RClosure::from_rust(&vm, move |val: f32| format!("this is a test: {} - {:?}", val, ty)); +} diff --git a/core/tests/safety/test_rclosure_not_send.stderr b/core/tests/safety/test_rclosure_not_send.stderr new file mode 100644 index 0000000..87bdd48 --- /dev/null +++ b/core/tests/safety/test_rclosure_not_send.stderr @@ -0,0 +1,34 @@ +error[E0277]: `*const ()` cannot be sent between threads safely + --> tests/safety/test_rclosure_not_send.rs:40:30 + | +40 | RClosure::from_rust(&vm, move |val: f32| format!("this is a test: {} - {:?}", val, ty)); + | ------------------- ---------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | | | + | | `*const ()` cannot be sent between threads safely + | | within this `{closure@$DIR/tests/safety/test_rclosure_not_send.rs:40:30: 40:45}` + | required by a bound introduced by this call + | + = help: within `{closure@$DIR/tests/safety/test_rclosure_not_send.rs:40:30: 40:45}`, the trait `Send` is not implemented for `*const ()` +note: required because it appears within the type `PhantomData<*const ()>` + --> $RUST/core/src/marker.rs + | + | pub struct PhantomData; + | ^^^^^^^^^^^ +note: required because it appears within the type `UnSendType` + --> tests/safety/test_rclosure_not_send.rs:35:8 + | +35 | struct UnSendType(PhantomData<*const ()>); + | ^^^^^^^^^^ +note: required because it's used within this closure + --> tests/safety/test_rclosure_not_send.rs:40:30 + | +40 | RClosure::from_rust(&vm, move |val: f32| format!("this is a test: {} - {:?}", val, ty)); + | ^^^^^^^^^^^^^^^ +note: required by a bound in `closure::rust::>>::from_rust` + --> src/vm/closure/rust.rs + | + | pub fn from_rust<'a, T, R, F: Fn(T) -> R + 'static>(vm: &Vm, fun: F) -> Self + | --------- required by a bound in this associated function +... + | F: Send, + | ^^^^ required by this bound in `closure::rust::>>::from_rust` diff --git a/core/tests/safety/test_scope_safe_1.rs b/core/tests/safety/test_scope_safe_1.rs new file mode 100644 index 0000000..47cad54 --- /dev/null +++ b/core/tests/safety/test_scope_safe_1.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::vm::RootVm; + +fn main() { + let vm = RootVm::new(); + let _ = vm.scope(|vm| { + // Should not build as get_global is limited to vm.scope. + vm.get_global::<&str>(c"MY_GLOBAL") + }); +} diff --git a/core/tests/safety/test_scope_safe_1.stderr b/core/tests/safety/test_scope_safe_1.stderr new file mode 100644 index 0000000..102a19b --- /dev/null +++ b/core/tests/safety/test_scope_safe_1.stderr @@ -0,0 +1,10 @@ +error: lifetime may not live long enough + --> tests/safety/test_scope_safe_1.rs:35:9 + | +33 | let _ = vm.scope(|vm| { + | --- return type of closure is Result<&'2 str, bp3d_lua::vm::error::Error> + | | + | has type `&'1 bp3d_lua::vm::Vm` +34 | // Should not build as get_global is limited to vm.scope. +35 | vm.get_global::<&str>(c"MY_GLOBAL") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2` diff --git a/core/tests/safety/test_scope_safe_2.rs b/core/tests/safety/test_scope_safe_2.rs new file mode 100644 index 0000000..fa71640 --- /dev/null +++ b/core/tests/safety/test_scope_safe_2.rs @@ -0,0 +1,37 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::vm::RootVm; + +fn main() { + let vm = RootVm::new(); + let _ = vm.scope(|_| { + // Should not build as get_global is limited to vm.scope. + vm.get_global::<&str>(c"MY_GLOBAL") + }); +} diff --git a/core/tests/safety/test_scope_safe_2.stderr b/core/tests/safety/test_scope_safe_2.stderr new file mode 100644 index 0000000..cdac37f --- /dev/null +++ b/core/tests/safety/test_scope_safe_2.stderr @@ -0,0 +1,22 @@ +error[E0597]: `vm` does not live long enough + --> tests/safety/test_scope_safe_2.rs:35:9 + | +32 | let vm = RootVm::new(); + | -- binding `vm` declared here +33 | let _ = vm.scope(|_| { + | --- value captured here +34 | // Should not build as get_global is limited to vm.scope. +35 | vm.get_global::<&str>(c"MY_GLOBAL") + | ^^--------------------------------- + | | + | borrowed value does not live long enough + | returning this value requires that `vm` is borrowed for `'static` +36 | }); +37 | } + | - `vm` dropped here while still borrowed + | +note: requirements that the value outlives `'static` introduced here + --> src/vm/core/vm.rs + | + | pub fn scope crate::vm::Result>( + | ^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ diff --git a/core/tests/safety/test_scope_safe_3.rs b/core/tests/safety/test_scope_safe_3.rs new file mode 100644 index 0000000..b25d9a5 --- /dev/null +++ b/core/tests/safety/test_scope_safe_3.rs @@ -0,0 +1,41 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::vm::RootVm; +use bp3d_lua::vm::value::types::Function; + +fn main() { + let vm = RootVm::new(); + let func: Function = vm.get_global(c"MY_GLOBAL").unwrap(); + let _ = vm.scope(|_| { + vm.set_global(c"MY_GLOBAL_2", func).unwrap(); + Ok(()) + }); + // Should fail because func was moved inside scope. + vm.set_global(c"MY_GLOBAL_3", func).unwrap(); +} diff --git a/core/tests/safety/test_scope_safe_3.stderr b/core/tests/safety/test_scope_safe_3.stderr new file mode 100644 index 0000000..37e2df8 --- /dev/null +++ b/core/tests/safety/test_scope_safe_3.stderr @@ -0,0 +1,17 @@ +error[E0382]: use of moved value: `func` + --> tests/safety/test_scope_safe_3.rs:40:35 + | +34 | let func: Function = vm.get_global(c"MY_GLOBAL").unwrap(); + | ---- move occurs because `func` has type `bp3d_lua::vm::value::types::Function<'_>`, which does not implement the `Copy` trait +35 | let _ = vm.scope(|_| { + | --- value moved into closure here +36 | vm.set_global(c"MY_GLOBAL_2", func).unwrap(); + | ---- variable moved due to use in closure +... +40 | vm.set_global(c"MY_GLOBAL_3", func).unwrap(); + | ^^^^ value used here after move + | +help: consider cloning the value if the performance cost is acceptable + | +36 | vm.set_global(c"MY_GLOBAL_2", func.clone()).unwrap(); + | ++++++++ diff --git a/core/tests/safety/test_vm_threads_unsafe_type.rs b/core/tests/safety/test_vm_threads_unsafe_type.rs new file mode 100644 index 0000000..3d37b64 --- /dev/null +++ b/core/tests/safety/test_vm_threads_unsafe_type.rs @@ -0,0 +1,38 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::vm::core::UnSendRootVm; +use bp3d_lua::vm::thread::value::Thread; + +fn main() { + let vm = UnSendRootVm::new(); + let thread: Thread = vm.get_global(c"CO").unwrap(); + // Should fail to build because &str could be freed/overwritten by luajit after a subsequent + // resume. + let _: &str = thread.as_thread().resume(()).unwrap().data; +} diff --git a/core/tests/safety/test_vm_threads_unsafe_type.stderr b/core/tests/safety/test_vm_threads_unsafe_type.stderr new file mode 100644 index 0000000..77b5286 --- /dev/null +++ b/core/tests/safety/test_vm_threads_unsafe_type.stderr @@ -0,0 +1,49 @@ +error[E0597]: `vm` does not live long enough + --> tests/safety/test_vm_threads_unsafe_type.rs:34:26 + | +33 | let vm = UnSendRootVm::new(); + | -- binding `vm` declared here +34 | let thread: Thread = vm.get_global(c"CO").unwrap(); + | ^^ borrowed value does not live long enough +... +37 | let _: &str = thread.as_thread().resume(()).unwrap().data; + | ----------------------------- argument requires that `vm` is borrowed for `'static` +38 | } + | - `vm` dropped here while still borrowed + | +note: requirements that the value outlives `'static` introduced here + --> src/vm/core/vm.rs + | + | pub fn get_global<'a, R: FromLua<'a>>(&'a self, name: impl AnyStr) -> crate::vm::Result { + | ^^^^^^^^^^^ + | + ::: src/vm/thread/core.rs + | + | pub fn resume<'b, T: FromLua<'b>>(&'b self, args: impl IntoLua) -> crate::vm::Result> + | ^^^^^^^^^^^ + | where + | T: 'static, /* This clause ensures that a future call to collectgarbage or resume does + | ^^^^^^^ + +error[E0597]: `thread` does not live long enough + --> tests/safety/test_vm_threads_unsafe_type.rs:37:19 + | +34 | let thread: Thread = vm.get_global(c"CO").unwrap(); + | ------ binding `thread` declared here +... +37 | let _: &str = thread.as_thread().resume(()).unwrap().data; + | ^^^^^^----------------------- + | | + | borrowed value does not live long enough + | argument requires that `thread` is borrowed for `'static` +38 | } + | - `thread` dropped here while still borrowed + | +note: requirements that the value outlives `'static` introduced here + --> src/vm/thread/core.rs + | + | pub fn resume<'b, T: FromLua<'b>>(&'b self, args: impl IntoLua) -> crate::vm::Result> + | ^^^^^^^^^^^ + | where + | T: 'static, /* This clause ensures that a future call to collectgarbage or resume does + | ^^^^^^^ diff --git a/core/tests/test_multi_root_vms.rs b/core/tests/test_multi_root_vms.rs new file mode 100644 index 0000000..3851fcf --- /dev/null +++ b/core/tests/test_multi_root_vms.rs @@ -0,0 +1,157 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "send")] + +use bp3d_lua::vm::core::destructor::Pool; +use bp3d_lua::vm::registry::core::Key; +use bp3d_lua::vm::registry::types; +use bp3d_lua::vm::value::types::Function; +use bp3d_lua::vm::RootVm; + +#[test] +fn test_multi_root_vms_basic() { + let root1 = RootVm::new(); + let root2 = RootVm::new(); + let handle1 = std::thread::spawn(move || { + root1.run_code::<()>(c"function Test() end").unwrap(); + let glb: Function = root1.get_global("Test").unwrap(); + glb.call::<()>(()).unwrap(); + (Key::::new(glb), root1) + }); + let handle2 = std::thread::spawn(move || { + root2.run_code::<()>(c"function Test2() end").unwrap(); + let glb: Option = root2.get_global("Test").unwrap(); + assert!(glb.is_none()); + let glb: Function = root2.get_global("Test2").unwrap(); + glb.call::<()>(()).unwrap(); + (Key::::new(glb), root2) + }); + let (key1, root1) = handle1.join().unwrap(); + let (key2, root2) = handle2.join().unwrap(); + let fn1 = key1.push(&root1); + let fn2 = key2.push(&root2); + fn1.call::<()>(()).unwrap(); + fn2.call::<()>(()).unwrap(); +} + +#[test] +#[should_panic] +fn test_multi_root_vms_panic_1() { + let root1 = RootVm::new(); + let root2 = RootVm::new(); + let handle1 = std::thread::spawn(move || { + root1.run_code::<()>(c"function Test() end").unwrap(); + let glb: Function = root1.get_global("Test").unwrap(); + (Key::::new(glb), root1) + }); + let handle2 = std::thread::spawn(move || { + root2.run_code::<()>(c"function Test2() end").unwrap(); + let glb: Function = root2.get_global("Test2").unwrap(); + (Key::::new(glb), root2) + }); + let (key1, _) = handle1.join().unwrap(); + let (_, root2) = handle2.join().unwrap(); + key1.push(&root2); +} + +#[test] +#[should_panic] +fn test_multi_root_vms_panic_2() { + let root1 = RootVm::new(); + let root2 = RootVm::new(); + let handle1 = std::thread::spawn(move || { + root1.run_code::<()>(c"function Test() end").unwrap(); + let glb: Function = root1.get_global("Test").unwrap(); + (Key::::new(glb), root1) + }); + let handle2 = std::thread::spawn(move || { + root2.run_code::<()>(c"function Test2() end").unwrap(); + let glb: Function = root2.get_global("Test2").unwrap(); + (Key::::new(glb), root2) + }); + let (_, root1) = handle1.join().unwrap(); + let (key2, _) = handle2.join().unwrap(); + key2.push(&root1); +} + +#[test] +#[should_panic] +fn test_multi_root_vms_panic_3() { + let root1 = RootVm::new(); + let handle1 = std::thread::spawn(move || { + root1.run_code::<()>(c"function Test() end").unwrap(); + let glb: Function = root1.get_global("Test").unwrap(); + Key::::new(glb) + }); + let key = handle1.join().unwrap(); + let root2 = RootVm::new(); + key.push(&root2); +} + +#[test] +#[should_panic] +fn test_multi_root_vms_panic_4() { + let root1 = RootVm::new(); + let handle1 = std::thread::spawn(move || { + root1.run_code::<()>(c"function Test() end").unwrap(); + let glb: Function = root1.get_global("Test").unwrap(); + Key::::new(glb) + }); + let key = handle1.join().unwrap(); + let root2 = RootVm::new(); + key.delete(&root2); +} + +#[test] +#[should_panic] +fn test_multi_root_vms_panic_5() { + let key = { + let root1 = RootVm::new(); + root1.run_code::<()>(c"function Test() end").unwrap(); + let glb: Function = root1.get_global("Test").unwrap(); + Key::::new(glb) + }; + { + let root2 = RootVm::new(); + key.delete(&root2); + } +} + +#[test] +#[should_panic] +fn test_multi_root_vms_not_send_destructor() { + let root1 = RootVm::new(); + Pool::attach(&root1, Box::new(())); +} + +#[test] +fn test_multi_root_vms_send_destructor() { + let root1 = RootVm::new(); + Pool::attach_send(&root1, Box::new(())); +} diff --git a/core/tests/test_vm_backtrace.rs b/core/tests/test_vm_backtrace.rs new file mode 100644 index 0000000..4ddd9ee --- /dev/null +++ b/core/tests/test_vm_backtrace.rs @@ -0,0 +1,76 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "root-vm")] + +use bp3d_lua::decl_lib_func; +use bp3d_lua::vm::function::types::RFunction; +use bp3d_lua::vm::value::types::Function; +use bp3d_lua::vm::RootVm; +use bp3d_util::simple_error; + +simple_error! { + pub Error { + Useless => "useless function called" + } +} + +decl_lib_func! { + fn error_func() -> Result<(), Error> { + Err(Error::Useless) + } +} + +#[test] +fn test_vm_backtrace() { + let vm = RootVm::new(); + let top = vm.top(); + vm.set_global(c"error_func", RFunction::wrap(error_func)) + .unwrap(); + vm.run_code::<()>( + c" + local function raise() + error_func() + end + + local function a() + raise() + end + + function main() + a() + end + ", + ) + .unwrap(); + let func: Function = vm.get_global(c"main").unwrap(); + let err = func.call::<()>(()).unwrap_err().into_runtime().unwrap(); + assert_eq!(err.msg(), "rust error: useless function called"); + assert_eq!(err.backtrace(), "rust error: useless function called\nstack traceback:\n\t[C]: in function 'error_func'\n\t[string \"...\"]:3: in function 'raise'\n\t[string \"...\"]:7: in function 'a'\n\t[string \"...\"]:11: in function <[string \"...\"]:10>"); + assert_eq!(vm.top(), top + 1); +} diff --git a/core/tests/test_vm_closures.rs b/core/tests/test_vm_closures.rs new file mode 100644 index 0000000..5924074 --- /dev/null +++ b/core/tests/test_vm_closures.rs @@ -0,0 +1,178 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(all(feature = "root-vm", feature = "util-namespace"))] + +use bp3d_lua::decl_closure; +use bp3d_lua::util::Namespace; +use bp3d_lua::vm::closure::context::{CellMut, ContextMut}; +use bp3d_lua::vm::closure::types::RClosure; +use bp3d_lua::vm::RootVm; +use std::cell::Cell; + +struct TestContext { + value: i32, + value3: Vec, +} + +decl_closure! { + fn context_set_value |ctx: ContextMut| (val: i32) -> () { + let mut ctx = ctx.borrow(); + ctx.value = val; + } +} + +decl_closure! { + fn context_push |ctx: ContextMut| (val: u64) -> () { + let mut ctx = ctx.borrow(); + ctx.value3.push(val); + } +} + +decl_closure! { + fn context_pop |ctx: ContextMut| () -> Option { + let mut ctx = ctx.borrow(); + ctx.value3.pop() + } +} + +decl_closure! { + fn test |upvalue: &str| (val: f32) -> String { + format!("{}: {}", upvalue, val) + } +} + +#[test] +fn test_vm_fast_closure() { + let vm = RootVm::new(); + let top = vm.top(); + vm.set_global(c"test", test("this is a test")).unwrap(); + assert_eq!(top, vm.top()); + let s: &str = vm.run_code(c"return test(42.42)").unwrap(); + assert_eq!(s, "this is a test: 42.42"); +} + +#[test] +fn test_vm_rust_closure() { + let vm = RootVm::new(); + let top = vm.top(); + let closure = RClosure::from_rust(&vm, |val: f32| format!("this is a test: {}", val)); + vm.set_global(c"test", closure).unwrap(); + assert_eq!(top, vm.top()); + let s: &str = vm.run_code(c"return test(42.42)").unwrap(); + assert_eq!(s, "this is a test: 42.42"); +} + +#[test] +fn test_vm_context() { + let vm = RootVm::new(); + let top = vm.top(); + let ctx = ContextMut::new(&vm); + { + let mut namespace = Namespace::new(&vm, "context").unwrap(); + namespace + .add([ + ("push", context_push(ctx)), + ("pop", context_pop(ctx)), + ("set_value", context_set_value(ctx)), + ]) + .unwrap(); + } + assert_eq!(top, vm.top()); + let res = vm.run_code::<()>(c"context.set_value(42)"); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().into_runtime().unwrap().msg(), + "[string \"context.set_value(42)\"]:1: Context is not available in this function." + ); + let mut obj = TestContext { + value: 0, + value3: vec![], + }; + let mut cell = CellMut::new(ctx); + { + let _obj = cell.bind(&mut obj); + vm.run_code::<()>(c"context.set_value(42)").unwrap(); + } + let res = vm.run_code::<()>(c"context.set_value(84)"); + assert!(res.is_err()); + assert_eq!( + res.unwrap_err().into_runtime().unwrap().msg(), + "[string \"context.set_value(84)\"]:1: Context is not available in this function." + ); + assert_eq!(obj.value, 42); + { + let _obj = cell.bind(&mut obj); + vm.run_code::<()>(c"assert(context.pop() == nil)").unwrap(); + vm.run_code::<()>(c"context.push(1)").unwrap(); + vm.run_code::<()>(c"context.push(2)").unwrap(); + vm.run_code::<()>(c"context.push(3)").unwrap(); + } + assert_eq!(obj.value3.len(), 3); + { + let _obj = cell.bind(&mut obj); + vm.run_code::<()>(c"assert(context.pop() == 3)").unwrap(); + vm.run_code::<()>(c"assert(context.pop() == 2)").unwrap(); + vm.run_code::<()>(c"assert(context.pop() == 1)").unwrap(); + vm.run_code::<()>(c"assert(context.pop() == nil)").unwrap(); + vm.run_code::<()>(c"assert(context.pop() == nil)").unwrap(); + } + assert_eq!(obj.value3.len(), 0); + assert_eq!(top, vm.top()); +} + +#[test] +fn test_vm_rust_closure_2() { + let vm = RootVm::new(); + let top = vm.top(); + let (closure, _guard) = + RClosure::from_rust_temporary(&vm, |val: f32| format!("this is a test: {}", val)); + vm.set_global(c"test", closure).unwrap(); + assert_eq!(top, vm.top()); + let s: &str = vm.run_code(c"return test(42.42)").unwrap(); + assert_eq!(s, "this is a test: 42.42"); +} + +#[test] +fn test_vm_rust_closure_3() { + let vm = RootVm::new(); + let value = Cell::new(0); + vm.scope(|vm| { + let br = &value; + let (fun, _guard) = RClosure::from_rust_temporary(vm, |()| br.get()); + vm.set_global(c"test", fun)?; + let (fun2, _guard) = RClosure::from_rust_temporary(vm, |val: i32| br.set(val)); + vm.set_global(c"test2", fun2)?; + vm.run_code::<()>(c"assert(test() == 0)")?; + vm.run_code::<()>(c"test2(42)") + }) + .unwrap(); + assert!(vm.run_code::<()>(c"test()").is_err()); + assert!(vm.run_code::<()>(c"test2(4242)").is_err()); + assert_eq!(value.get(), 42); +} diff --git a/core/tests/test_vm_custom_structs.rs b/core/tests/test_vm_custom_structs.rs new file mode 100644 index 0000000..d30085d --- /dev/null +++ b/core/tests/test_vm_custom_structs.rs @@ -0,0 +1,118 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "root-vm")] + +use bp3d_lua::decl_lib_func; +use bp3d_lua::vm::function::types::RFunction; +use bp3d_lua::vm::RootVm; +use bp3d_lua_codegen::{FromParam, IntoParam}; +use bp3d_lua_codegen::{IntoLua, LuaType}; + +#[derive(FromParam, LuaType, IntoParam)] +struct Test1<'a>(&'a str, i32); + +#[derive(FromParam, LuaType, IntoParam, IntoLua)] +struct Test2<'a> { + name: &'a str, + value: i32, +} + +#[derive(FromParam, LuaType, IntoParam)] +struct TestStatic { + value1: f32, + value2: i32, +} + +decl_lib_func! { + fn test(test1: Test1, test2: Test2, st: TestStatic) -> String { + format!("{} {}: {}, {}, (v1: {}, v2: {})", test1.0, test2.name, test1.1, test2.value, st.value1, st.value2) + } +} + +decl_lib_func! { + fn test2(name: &str) -> Test2<'_> { + Test2 { name, value: 42 } + } +} + +decl_lib_func! { + fn test3<'a>(name: &'a str, name2: &str) -> Test2<'a> { + println!("{}", name2); + Test2 { name, value: 42 } + } +} + +#[test] +fn test_custom_structs_basic() { + let vm = RootVm::new(); + let top = vm.top(); + vm.set_global(c"test", RFunction::wrap(test)).unwrap(); + vm.set_global(c"test2", RFunction::wrap(test2)).unwrap(); + vm.set_global(c"test3", RFunction::wrap(test3)).unwrap(); + let out = vm + .run_code::<&str>( + c" + local test1 = { 'value', 42 } + local test2 = { name = 'of', value = 64 } + local st = { value1 = 42.42, value2 = 32 } + return test(test1, test2, st) + ", + ) + .unwrap(); + assert_eq!(out, "value of: 42, 64, (v1: 42.42, v2: 32)"); + vm.set_global( + c"test", + Test2 { + name: "whatever", + value: 42, + }, + ) + .unwrap(); + let out = vm.run_code::<&str>(c"return test.name").unwrap(); + assert_eq!(out, "whatever"); + let out = vm.run_code::(c"return test.value").unwrap(); + assert_eq!(out, 42); + vm.run_code::<()>( + c" + local t2 = test2('test') + assert(t2.name == 'test') + assert(t2.value == 42) + ", + ) + .unwrap(); + vm.run_code::<()>( + c" + local t2 = test3('test42', 'test2') + assert(t2.name == 'test42') + assert(t2.value == 42) + ", + ) + .unwrap(); + assert_eq!(top + 3, vm.top()) +} diff --git a/core/tests/test_vm_destructor.rs b/core/tests/test_vm_destructor.rs new file mode 100644 index 0000000..2a9cb20 --- /dev/null +++ b/core/tests/test_vm_destructor.rs @@ -0,0 +1,81 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "root-vm")] + +use bp3d_lua::decl_lib_func; +use bp3d_lua::vm::function::types::RFunction; +use bp3d_lua::vm::RootVm; + +struct ValueWithDrop; +impl ValueWithDrop { + pub fn print(&self) { + println!("ValueWithDrop") + } +} +impl Drop for ValueWithDrop { + fn drop(&mut self) { + println!("Dropping!"); + } +} + +decl_lib_func! { + fn test_c_function(name: &str, value: f64) -> String { + let drop = ValueWithDrop; + drop.print(); + format!("Hello {} ({})", name, value) + } +} + +#[test] +fn test_vm_destructor() { + let mut vm = RootVm::new(); + vm.set_global(c"test_c_function", RFunction::wrap(test_c_function)) + .unwrap(); + let time = std::time::Instant::now(); + let res = vm.run_code::<&str>(c"return test_c_function('this is a test\\xFF', 0.42)"); + assert!(res.is_err()); + let err = res.unwrap_err().into_runtime().unwrap(); + assert_eq!( + err.msg(), + "rust error: invalid utf-8 sequence of 1 bytes from index 14" + ); + assert!(vm + .run_code::<&str>(c"return test_c_function('this is a test', 0.42)") + .is_ok()); + let s = vm + .run_code::<&str>(c"return test_c_function('this is a test', 0.42)") + .unwrap(); + assert_eq!(s, "Hello this is a test (0.42)"); + assert!(vm + .run_code::(c"return test_c_function('this is a test', 0.42)") + .is_err()); + vm.clear(); + let time = time.elapsed(); + println!("time: {:?}", time); +} diff --git a/core/tests/test_vm_files.rs b/core/tests/test_vm_files.rs new file mode 100644 index 0000000..35c65e0 --- /dev/null +++ b/core/tests/test_vm_files.rs @@ -0,0 +1,158 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::libs::files::chroot::{access, set_access, set_chroot, Permissions}; +use bp3d_lua::libs::files::{Files, SandboxPath}; +use bp3d_lua::libs::Lib; +use bp3d_lua::vm::RootVm; +use std::path::Path; + +#[test] +fn test_vm_files() { + let vm = RootVm::new(); + Files.register(&vm).unwrap(); + set_chroot(&vm, Path::new("/root")); // for testing only + let path: SandboxPath = vm + .run_code(c"return bp3d.files.Path.new('/myfile')") + .unwrap(); + let path = path.to_path(&vm).unwrap(); + assert_eq!(path, Path::new("/root/myfile")); + let spath: SandboxPath = vm + .run_code(c"return bp3d.files.Path.new('/data'):join('myfile'):withExtension('txt')") + .unwrap(); + let path = spath.to_path(&vm).unwrap(); + assert_eq!(path, Path::new("/root/data/myfile.txt")); + let path = spath.to_str(&vm).unwrap(); + assert_eq!(path, "/data/myfile.txt"); + let path: SandboxPath = vm.run_code(c"return bp3d.files.Path.new('/')").unwrap(); + let path = path.to_path(&vm).unwrap(); + assert_eq!(path, Path::new("/root")); +} + +#[test] +fn test_vm_files_security() { + let vm = RootVm::new(); + Files.register(&vm).unwrap(); + set_chroot(&vm, Path::new("/root")); // for testing only + vm.run_code::<()>(c"return bp3d.files.Path.new('../myfile')") + .unwrap_err(); + vm.run_code::<()>(c"return bp3d.files.Path.new('/../myfile')") + .unwrap_err(); + vm.run_code::<()>(c"return bp3d.files.Path.new('/./../myfile')") + .unwrap_err(); + vm.run_code::<()>(c"return bp3d.files.Path.new('.././myfile/.')") + .unwrap_err(); + vm.run_code::<()>(c"return bp3d.files.Path.new('/data/../myfile')") + .unwrap(); + vm.run_code::<()>(c"return bp3d.files.Path.new('/data/../../myfile')") + .unwrap_err(); + vm.run_code::<()>(c"return bp3d.files.Path.new('/../data/myfile')") + .unwrap_err(); +} + +#[test] +fn test_vm_files_permissions() { + let vm = RootVm::new(); + Files.register(&vm).unwrap(); + set_chroot(&vm, Path::new("/root")); // for testing only + set_access(&vm, "/rodata", Permissions::R); + set_access(&vm, "/rwdata", Permissions::R | Permissions::W); + set_access( + &vm, + "/data/myfile.lua", + Permissions::R | Permissions::W | Permissions::X, + ); + assert_eq!(access(&vm, "/rodata/myfile.txt"), Permissions::R); + assert_eq!(access(&vm, "/rodata.txt"), Permissions::NONE); + assert_eq!(access(&vm, "/"), Permissions::NONE); + assert_eq!(access(&vm, "/rodata"), Permissions::R); + assert_eq!(access(&vm, "/rwdata"), Permissions::R | Permissions::W); + assert_eq!( + access(&vm, "/rwdata/myfile.txt"), + Permissions::R | Permissions::W + ); + assert_eq!(access(&vm, "/data"), Permissions::NONE); + assert_eq!(access(&vm, "/data/myfile"), Permissions::NONE); + assert_eq!(access(&vm, "/data/myfile.txt"), Permissions::NONE); + assert_eq!( + access(&vm, "/data/myfile.lua"), + Permissions::R | Permissions::W | Permissions::X + ); +} + +#[test] +fn test_vm_files_permissions2() { + let vm = RootVm::new(); + Files.register(&vm).unwrap(); + set_chroot(&vm, Path::new("/root")); // for testing only + set_access(&vm, "/", Permissions::R); + set_access(&vm, "/target", Permissions::R | Permissions::W); + set_access(&vm, "/bp3d-build", Permissions::R | Permissions::X); + assert_eq!( + access(&vm, "/target/myfile"), + Permissions::R | Permissions::W + ); + assert_eq!( + access(&vm, "/bp3d-build/myfile.lua"), + Permissions::R | Permissions::X + ); + assert_eq!( + access(&vm, "/bp3d-build/package/myfile.lua"), + Permissions::R | Permissions::X + ); + assert_eq!( + access(&vm, "/target/aarch64/data/1/myfile"), + Permissions::R | Permissions::W + ); + assert_eq!(access(&vm, "/myfile"), Permissions::R); + assert_eq!(access(&vm, "/obj/data/1/myfile"), Permissions::R); +} + +#[test] +fn test_vm_simple_chroot() { + let vm = RootVm::new(); + Files.register(&vm).unwrap(); + set_chroot(&vm, Path::new(".")); + let path: SandboxPath = vm + .run_code(c"return bp3d.files.Path.new('/myfile')") + .unwrap(); + let path = path.to_path(&vm).unwrap(); + assert_eq!(path, Path::new("./myfile")); +} + +#[test] +fn test_vm_simple_chroot2() { + let vm = RootVm::new(); + Files.register(&vm).unwrap(); + set_chroot(&vm, Path::new("./")); + let path: SandboxPath = vm + .run_code(c"return bp3d.files.Path.new('/myfile')") + .unwrap(); + let path = path.to_path(&vm).unwrap(); + assert_eq!(path, Path::new("./myfile")); +} diff --git a/core/tests/test_vm_functions.rs b/core/tests/test_vm_functions.rs new file mode 100644 index 0000000..9a26245 --- /dev/null +++ b/core/tests/test_vm_functions.rs @@ -0,0 +1,60 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "root-vm")] + +use bp3d_lua::vm::value::types::Function; +use bp3d_lua::vm::RootVm; + +#[test] +fn test_vm_function_1_arg() { + let mut vm = RootVm::new(); + let top = vm.top(); + let f: Function = vm + .run_code(c"return function(value) return 'this is a test ' .. value end") + .unwrap(); + let str: &str = f.call(42.42).unwrap(); + assert_eq!(str, "this is a test 42.42"); + let str: &str = f.call(42).unwrap(); + assert_eq!(str, "this is a test 42"); + assert_eq!(vm.top(), top + 3); // Function + 2 results + vm.clear(); +} + +#[test] +fn test_vm_function_2_args() { + let mut vm = RootVm::new(); + let top = vm.top(); + let f: Function = vm.run_code(c"return function(value, value2) return 'this ' .. value .. ' is a test ' .. tostring(value2) end").unwrap(); + let str: &str = f.call((42.42, false)).unwrap(); + assert_eq!(str, "this 42.42 is a test false"); + let str: &str = f.call((42, true)).unwrap(); + assert_eq!(str, "this 42 is a test true"); + assert_eq!(vm.top(), top + 3); // Function + 2 results + vm.clear(); +} diff --git a/core/tests/test_vm_integer53.rs b/core/tests/test_vm_integer53.rs new file mode 100644 index 0000000..fd39960 --- /dev/null +++ b/core/tests/test_vm_integer53.rs @@ -0,0 +1,68 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::vm::value::types::{Int53, UInt53}; +use bp3d_lua::vm::RootVm; + +#[test] +fn test_vm_u53() { + let vm = RootVm::new(); + let top = vm.top(); + let val: UInt53 = vm.run_code(c"return 2^53-1").unwrap(); + assert_eq!(val, UInt53::MAX); + vm.set_global(c"UINT53_MAX", UInt53::MAX).unwrap(); + assert_eq!( + vm.run_code::<&str>(c"return tostring(UINT53_MAX)").unwrap(), + "9.007199254741e+15" + ); + vm.run_code::<()>(c"assert(UINT53_MAX == 2^53-1)").unwrap(); + assert_eq!(top + 2, vm.top()); +} + +#[test] +fn test_vm_i53() { + let vm = RootVm::new(); + let top = vm.top(); + let val: Int53 = vm.run_code(c"return 2^52-1").unwrap(); + assert_eq!(val, Int53::MAX); + let val: Int53 = vm.run_code(c"return -2^52").unwrap(); + assert_eq!(val, Int53::MIN); + vm.set_global(c"INT53_MAX", Int53::MAX).unwrap(); + vm.set_global(c"INT53_MIN", Int53::MIN).unwrap(); + assert_eq!( + vm.run_code::<&str>(c"return tostring(INT53_MAX)").unwrap(), + "4.5035996273705e+15" + ); + assert_eq!( + vm.run_code::<&str>(c"return tostring(INT53_MIN)").unwrap(), + "-4.5035996273705e+15" + ); + vm.run_code::<()>(c"assert(INT53_MAX == 2^52-1)").unwrap(); + vm.run_code::<()>(c"assert(INT53_MIN == -2^52)").unwrap(); + assert_eq!(top + 4, vm.top()); +} diff --git a/core/tests/test_vm_integer64.rs b/core/tests/test_vm_integer64.rs new file mode 100644 index 0000000..65ce2cf --- /dev/null +++ b/core/tests/test_vm_integer64.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::ffi::lua::Type; +use bp3d_lua::vm::value::any::Any; +use bp3d_lua::vm::value::util::check_type_equals; +use bp3d_lua::vm::RootVm; + +#[test] +fn test_vm_u64() { + let vm = RootVm::new(); + let top = vm.top(); + let val: u64 = vm.run_code(c"return 2ULL^64ULL-1ULL").unwrap(); + assert!(check_type_equals(&vm, -1, Type::Cdata).is_ok()); + assert_eq!(val, u64::MAX); + vm.set_global(c"UINT64_MAX", u64::MAX).unwrap(); + assert_eq!( + vm.run_code::<&str>(c"return tostring(UINT64_MAX)").unwrap(), + "18446744073709551615ULL" + ); + vm.run_code::<()>(c"assert(UINT64_MAX == 2ULL^64ULL-1ULL)") + .unwrap(); + assert_eq!(top + 2, vm.top()); +} + +#[test] +fn test_vm_i64() { + let vm = RootVm::new(); + let top = vm.top(); + let val: i64 = vm.run_code(c"return 2LL^63LL-1LL").unwrap(); + assert!(check_type_equals(&vm, -1, Type::Cdata).is_ok()); + assert_eq!(val, i64::MAX); + let val: i64 = vm.run_code(c"return -2LL^63LL").unwrap(); + assert!(check_type_equals(&vm, -1, Type::Cdata).is_ok()); + assert_eq!(val, i64::MIN); + vm.set_global(c"INT64_MAX", i64::MAX).unwrap(); + vm.set_global(c"INT64_MIN", i64::MIN).unwrap(); + assert_eq!( + vm.run_code::<&str>(c"return tostring(INT64_MAX)").unwrap(), + "9223372036854775807LL" + ); + assert_eq!( + vm.run_code::<&str>(c"return tostring(INT64_MIN)").unwrap(), + "-9223372036854775808LL" + ); + vm.run_code::<()>(c"assert(INT64_MAX == 2LL^63LL-1LL)") + .unwrap(); + vm.run_code::<()>(c"assert(INT64_MIN == -2LL^63LL)") + .unwrap(); + assert_eq!(top + 4, vm.top()); +} + +#[test] +fn test_vm_i64_any() { + let vm = RootVm::new(); + let top = vm.top(); + let val: Any = vm.run_code(c"return 2ULL^64ULL-1ULL").unwrap(); + let val2: Any = vm.run_code(c"return 2LL^63LL-1LL").unwrap(); + assert_eq!(val.ty(), Type::Cdata); + assert_eq!(val, Any::UInt64(u64::MAX)); + assert_eq!(val2.ty(), Type::Cdata); + assert_eq!(val2, Any::Int64(i64::MAX)); + assert_eq!(top + 2, vm.top()); +} diff --git a/core/tests/test_vm_interrupt.rs b/core/tests/test_vm_interrupt.rs new file mode 100644 index 0000000..204e63d --- /dev/null +++ b/core/tests/test_vm_interrupt.rs @@ -0,0 +1,55 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "interrupt")] + +use bp3d_lua::libs::Lib; +use bp3d_lua::vm::core::interrupt; +use std::time::Duration; + +#[test] +fn test_vm_interrupt() { + let (signal, handle) = interrupt::spawn_interruptible(|vm| { + bp3d_lua::libs::os::Compat.register(vm).unwrap(); + // Run the malicious code. + vm.run_code::<()>( + c" + local tbl = {} + while (true) do + local time = os.date('!%x %H:%M:%S') + table.insert(tbl, time) + end + ", + ) + }); + // Give the chance to the thread to run and pump a bit of RAM. + std::thread::sleep(Duration::from_millis(500)); + signal.send(Duration::from_secs(2)).unwrap(); + let res = handle.join().unwrap(); + assert!(res.is_err()); +} diff --git a/core/tests/test_vm_jit_options.rs b/core/tests/test_vm_jit_options.rs new file mode 100644 index 0000000..e51a4b8 --- /dev/null +++ b/core/tests/test_vm_jit_options.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::vm::core::jit::{JitOptions, OptLevel}; +use bp3d_lua::vm::RootVm; + +#[test] +fn test_vm_get_jit_options() { + let vm = RootVm::new(); + let jit = JitOptions::get(&vm); + let optimizations = jit.opts(); + let cpu = jit.cpu(); + let is_on = jit.is_enabled(); + assert_eq!(is_on, true); + assert_eq!(jit.opt_level(), OptLevel::default()); + println!("{} {}", cpu, optimizations); +} + +#[test] +fn test_vm_set_jit_options() { + let mut vm = RootVm::new(); + let mut jit = JitOptions::get(&vm); + assert_eq!(jit.opt_level(), OptLevel::default()); + jit.set_opt_level(OptLevel::O0); + assert_eq!(jit.opt_level(), OptLevel::O0); + jit.apply(&mut vm); + let jit = JitOptions::get(&vm); + assert_eq!(jit.opt_level(), OptLevel::O0); +} + +#[test] +fn test_vm_disable_jit() { + let mut vm = RootVm::new(); + let mut jit = JitOptions::get(&vm); + assert!(jit.is_enabled()); + jit.disable(); + jit.apply(&mut vm); + let jit = JitOptions::get(&vm); + assert!(!jit.is_enabled()); +} diff --git a/core/tests/test_vm_libs.rs b/core/tests/test_vm_libs.rs new file mode 100644 index 0000000..c8ce303 --- /dev/null +++ b/core/tests/test_vm_libs.rs @@ -0,0 +1,356 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(all(feature = "root-vm", feature = "libs", feature = "module"))] + +use bp3d_lua::libs::lua::{Lua, Module}; +use bp3d_lua::libs::util::Util; +use bp3d_lua::libs::Lib; +use bp3d_lua::module::VERSION; +use bp3d_lua::vm::RootVm; + +#[test] +fn test_vm_lib_lua() { + let vm = RootVm::new(); + let top = vm.top(); + Lua::new().build().register(&vm).unwrap(); + Module.register(&vm).unwrap(); + vm.set_global("BP3D_LUA_CRATE_VERSION", VERSION).unwrap(); + vm.run_code::<()>( + c" + assert(bp3d.lua.name == 'bp3d-lua') + assert(bp3d.lua.version == BP3D_LUA_CRATE_VERSION) + assert(#bp3d.lua.patches == 8) + local func = bp3d.lua.loadString('return 1 + 1') + assert(func) + assert(func() == 2) + local func, err = bp3d.lua.loadString('ret a + 2') + assert(func == nil) + assert(err == \"syntax error: [string \\\"ret a + 2\\\"]:1: '=' expected near 'a'\") + assert(bp3d.lua.runString('return 1 + 1') == 2) + ", + ) + .unwrap(); + let err = vm + .run_code::<()>(c"bp3d.lua.require \"not.existing.file\"") + .unwrap_err() + .into_runtime() + .unwrap(); + assert_eq!(err.msg(), "rust error: unknown source name not"); + vm.run_code::<()>( + c" + local function test() + bp3d.lua.require \"not.existing.file\" + end + local flag, err = bp3d.lua.pcall(test) + assert(not flag) + print(err) + assert(err ~= '') + ", + ) + .unwrap(); + let err = vm + .run_code::<()>(c"bp3d.lua.module.load('broken', 'broken2')") + .unwrap_err() + .into_runtime() + .unwrap(); + assert_eq!( + err.msg(), + "rust error: module error: module not found (broken)" + ); + assert_eq!(vm.top(), top); +} + +#[test] +fn test_vm_lib_util() { + let mut vm = RootVm::new(); + let top = vm.top(); + Util.register(&mut vm).unwrap(); + vm.run_code::<()>( + c" + local src = { + a = 1, + b = 2 + } + local dst = { + c = 3 + } + bp3d.util.table.update(dst, src) + assert(dst.a == 1) + assert(dst.b == 2) + assert(dst.c == 3) + assert(bp3d.util.table.count(dst) == 3) + assert(bp3d.util.table.count(src) == 2) + assert(bp3d.util.table.contains(dst, 1)) + assert(bp3d.util.table.contains(dst, 2)) + assert(bp3d.util.table.contains(dst, 3)) + assert(bp3d.util.table.containsKey(dst, 'a')) + assert(bp3d.util.table.containsKey(dst, 'b')) + assert(bp3d.util.table.containsKey(dst, 'c')) + local str = bp3d.util.table.tostring(dst) .. '\\n' + assert(bp3d.util.string.contains(str, 'a: 1\\n')) + assert(bp3d.util.string.contains(str, 'b: 2\\n')) + assert(bp3d.util.string.contains(str, 'c: 3\\n')) + local str = bp3d.util.table.tostring(dst) + local tbl = bp3d.util.string.split(str, 0x0A) + assert(#tbl == 3) + assert(bp3d.util.table.contains(tbl, 'a: 1')) + assert(bp3d.util.table.contains(tbl, 'b: 2')) + assert(bp3d.util.table.contains(tbl, 'c: 3')) + ", + ) + .unwrap(); + vm.run_code::<()>( + c" + local utf8 = bp3d.util.utf8 + assert(utf8.fromString('abc') ~= nil) + assert(utf8.count('abc') == 3) + local tbl = utf8.split('a;b;c;d', ';') + assert(#tbl == 4) + assert(tbl[1] == 'a') + assert(tbl[2] == 'b') + assert(tbl[3] == 'c') + assert(tbl[4] == 'd') + assert(utf8.charAt('abc', 0) == 0x61) + assert(utf8.charAt('abc', 1) == 0x62) + assert(utf8.charAt('abc', 2) == 0x63) + local s = '我是' + assert(utf8.sub(s, 1) == '是') + ", + ) + .unwrap(); + vm.run_code::<()>( + c" + local tbl = { value = 42 } + local protected = bp3d.util.table.protect(tbl) + assert(protected.value == 42) + ", + ) + .unwrap(); + vm.run_code::<()>( + c" + local tbl = { value = 42 } + local protected = bp3d.util.table.protect(tbl) + protected.value = 84 + ", + ) + .unwrap_err(); + vm.run_code::<()>( + c" + local src = { value = 42, adding = { a = 1 } } + local dst = { value = 42, adding = { } } + bp3d.util.table.update(dst, src) + assert(dst.value == 42) + assert(dst.adding.a == 1) + local dst2 = { value = 42 } + bp3d.util.table.update(dst2, src) + assert(dst2.value == 42) + assert(dst2.adding.a == 1) + ", + ) + .unwrap(); + vm.run_code::<()>( + c" + local src = { value = 42, adding = { a = 1 } } + local dst = bp3d.util.table.copy(src) + assert(dst.value == 42) + assert(dst.adding.a == 1) + dst.adding.b = 2 + dst.b = 84 + assert(dst.b == 84) + assert(src.b == nil) + assert(dst.adding.b == 2) + assert(src.adding.b == nil) + ", + ) + .unwrap(); + vm.run_code::<()>( + c" + local list = { 1, 2, 3, 4 } + local list2 = { 5, 6, 7, 8 } + bp3d.util.table.concat(list, list2) + assert(#list == 8) + local str = bp3d.util.table.tostring(list) + assert(str == '1: 1\\n2: 2\\n3: 3\\n4: 4\\n5: 5\\n6: 6\\n7: 7\\n8: 8') + ", + ) + .unwrap(); + assert_eq!(vm.top(), top); +} + +#[test] +fn test_vm_lib_os_time() { + let mut vm = RootVm::new(); + bp3d_lua::libs::os::Time.register(&mut vm).unwrap(); + vm.run_code::<()>( + c" + time = bp3d.os.time.nowLocal() + time2 = bp3d.os.time.nowUtc() + ", + ) + .unwrap(); + std::thread::sleep(std::time::Duration::from_millis(500)); + vm.run_code::<()>( + c" + local function testDateTime(a, b) + local ymd = a:getDate() + local ymd2 = b:getDate() + assert(ymd.year == ymd2.year) + assert(ymd.month == ymd2.month) + assert(ymd.day == ymd2.day) + end + local now2 = bp3d.os.time.nowUtc() + local now = bp3d.os.time.nowLocal() + if (now ~= nil and time ~= nil) then + assert(now > time) + end + assert(now2 > time2) + if (now ~= nil and time ~= nil) then + testDateTime(now, time) + end + testDateTime(now2, time2) + ", + ) + .unwrap(); +} + +#[test] +fn test_vm_lib_os_time_2() { + let mut vm = RootVm::new(); + bp3d_lua::libs::os::Time.register(&mut vm).unwrap(); + vm.run_code::<()>( + c" + local OffsetDateTime = bp3d.os.time.OffsetDateTime + local dt = OffsetDateTime.new({year = 1900, month = 12, day = 1}) + local date = dt:getDate() + assert(date.year == 1900) + assert(date.month == 12) + assert(date.day == 1) + ", + ) + .unwrap(); +} + +#[test] +fn test_vm_lib_os_instant() { + let mut vm = RootVm::new(); + bp3d_lua::libs::os::Instant.register(&mut vm).unwrap(); + vm.run_code::<()>( + c" + instant = bp3d.os.instant.now() + ", + ) + .unwrap(); + std::thread::sleep(std::time::Duration::from_millis(500)); + vm.run_code::<()>( + c" + local diff = instant:elapsed() + assert((diff - 0.5) < 0.2) + ", + ) + .unwrap(); +} + +#[test] +fn test_vm_lib_os() { + let mut vm = RootVm::new(); + bp3d_lua::libs::os::Compat.register(&mut vm).unwrap(); + vm.run_code::<()>( + c" + clock = os.clock() + ", + ) + .unwrap(); + std::thread::sleep(std::time::Duration::from_millis(500)); + vm.run_code::<()>( + c" + local now = os.clock() + assert((clock - now) < 0.1) + ", + ) + .unwrap(); + let s = vm + .run_code::<&str>( + c" + return os.date('!%H:%M:%S') + ", + ) + .unwrap(); + assert!(s.contains(":")); + assert!(!s.contains("[")); + assert!(!s.contains("]")); +} + +#[test] +fn test_vm_lib_debug() { + let mut vm = RootVm::new(); + bp3d_lua::libs::lua::Debug.register(&mut vm).unwrap(); + vm.run_code::<()>( + c" + local debug = bp3d.lua.debug + local libs = debug.dumpLibs(); + assert(#libs == 1) + assert(libs[1] == 'bp3d_lua::libs::lua::debug::Debug: bp3d.lua.debug') + local classes = debug.dumpClasses(); + assert(#classes == 0) + local stack = debug.dumpStack(0); + assert(#stack > 0) + ", + ) + .unwrap(); +} + +#[test] +fn test_vm_lib_util_num() { + let mut vm = RootVm::new(); + Util.register(&mut vm).unwrap(); + let val = vm + .run_code::<&str>(c"return bp3d.util.num.toistring(bp3d.util.num.INT53_MAX)") + .unwrap(); + assert_eq!(val, "4503599627370495"); + let val = vm + .run_code::<&str>(c"return bp3d.util.num.toistring(bp3d.util.num.INT53_MIN)") + .unwrap(); + assert_eq!(val, "-4503599627370496"); + let val = vm + .run_code::<&str>(c"return bp3d.util.num.toustring(bp3d.util.num.UINT53_MAX)") + .unwrap(); + assert_eq!(val, "9007199254740991"); + let val = vm + .run_code::<&str>(c"return bp3d.util.num.toistring(bp3d.util.num.INT64_MIN)") + .unwrap(); + assert_eq!(val, "-9223372036854775808"); + let val = vm + .run_code::<&str>(c"return bp3d.util.num.toistring(bp3d.util.num.INT64_MAX)") + .unwrap(); + assert_eq!(val, "9223372036854775807"); + let val = vm + .run_code::<&str>(c"return bp3d.util.num.toustring(bp3d.util.num.UINT64_MAX)") + .unwrap(); + assert_eq!(val, "18446744073709551615"); +} diff --git a/core/tests/test_vm_multivalue.rs b/core/tests/test_vm_multivalue.rs new file mode 100644 index 0000000..d217348 --- /dev/null +++ b/core/tests/test_vm_multivalue.rs @@ -0,0 +1,39 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "root-vm")] + +use bp3d_lua::vm::RootVm; + +#[test] +fn test_vm_multivalue() { + let vm = RootVm::new(); + let (i, n): (i32, &str) = vm.run_code(c"return 123, 'this is a test'").unwrap(); + assert_eq!(i, 123); + assert_eq!(n, "this is a test"); +} diff --git a/core/tests/test_vm_registry.rs b/core/tests/test_vm_registry.rs new file mode 100644 index 0000000..dd1b86a --- /dev/null +++ b/core/tests/test_vm_registry.rs @@ -0,0 +1,128 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(all(feature = "root-vm", feature = "util-method"))] + +use bp3d_lua::util::{LuaFunction, LuaMethod}; +use bp3d_lua::vm::registry::core::Key; +use bp3d_lua::vm::registry::lua_ref::LuaRef as LiveLuaRef; +use bp3d_lua::vm::registry::types::LuaRef; +use bp3d_lua::vm::table::Table; +use bp3d_lua::vm::value::types::Function; +use bp3d_lua::vm::RootVm; +use std::ffi::CStr; + +const METHODS: &CStr = c" +local obj = { ctx = 'this is a test' } + +function obj:greeting() + return 'Hello ' .. self.ctx +end + +return obj +"; + +#[test] +fn test_vm_registry_method() { + let mut vm = RootVm::new(); + let top = vm.top(); + let obj: Table = vm.run_code(METHODS).unwrap(); + let method = LuaMethod::create(obj, c"greeting").unwrap(); + let str: &str = method.call(&vm, ()).unwrap(); + assert_eq!(str, "Hello this is a test"); + method.delete(&vm); + assert_eq!(vm.top(), top + 1); // 1 result + vm.clear(); +} + +#[test] +fn test_vm_registry_function() { + let vm = RootVm::new(); + let top = vm.top(); + let obj: Function = vm + .run_code(c"return function() return 'Hello world' end") + .unwrap(); + let f = LuaFunction::create(obj); + assert_eq!(vm.top(), top); // The function should have been popped from the stack following the + // call to LuaFunction + let str: &str = f.call(&vm, ()).unwrap(); + assert_eq!(str, "Hello world"); + assert_eq!(vm.top(), top + 1); // 1 result +} + +#[test] +fn test_vm_registry_string() { + let vm = RootVm::new(); + let top = vm.top(); + let r = LiveLuaRef::new(&vm, "this is a test"); + let key: Key> = Key::new(r); + assert_eq!(vm.top(), top); // The string should have been popped from the stack like any normal + // registry creation operation. + { + let motherfuckingrust = key.push(&vm); + let value = motherfuckingrust.get(); + assert_eq!(value, "this is a test"); + } + // LuaRef automatically pops from the stack on drop, as simple values which are stored in the + // registry cannot be de-allocated by luajit. + assert_eq!(vm.top(), top); +} + +#[test] +fn test_vm_registry_string_modify() { + let vm = RootVm::new(); + let top = vm.top(); + let r = LiveLuaRef::new(&vm, "this is a test"); + let key: Key> = Key::new(r); + assert_eq!(vm.top(), top); + let mut value = key.push(&vm); + assert_eq!(value.get(), "this is a test"); + value.set("one more test"); + assert_eq!(value.get(), "one more test"); + key.set(value); + assert_eq!(vm.top(), top); + { + let motherfuckingrust = key.push(&vm); + let value = motherfuckingrust.get(); + assert_eq!(value, "one more test"); + } + assert_eq!(vm.top(), top); +} + +#[test] +fn test_vm_registry_integer() { + let vm = RootVm::new(); + let top = vm.top(); + let r = LiveLuaRef::new(&vm, 42); + let key: Key> = Key::new(r); + { + let value = key.push(&vm).get(); + assert_eq!(value, 42); + } + assert_eq!(vm.top(), top); +} diff --git a/core/tests/test_vm_run.rs b/core/tests/test_vm_run.rs new file mode 100644 index 0000000..20fb07e --- /dev/null +++ b/core/tests/test_vm_run.rs @@ -0,0 +1,100 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "root-vm")] + +use bp3d_lua::ffi::lua::{State, ThreadStatus}; +use bp3d_lua::vm::core::load::{load_custom, Code, Script}; +use bp3d_lua::vm::core::util::ChunkNameBuilder; +use bp3d_lua::vm::core::Load; +use bp3d_lua::vm::{RootVm, Vm}; +use std::fmt::Write; + +struct BrokenReader; + +impl bp3d_lua::vm::core::load::Custom for BrokenReader { + type Error = bp3d_lua::vm::error::Error; + + fn read_data(&mut self) -> Result<&[u8], Self::Error> { + Err(bp3d_lua::vm::error::Error::Error) + } +} + +impl Load for BrokenReader { + fn load(self, l: State) -> ThreadStatus { + let mut builder = ChunkNameBuilder::new(); + let _ = write!(&mut builder, "broken"); + unsafe { load_custom(l, builder.build(), BrokenReader) } + } +} + +fn run_assert_err(vm: &Vm, obj: impl Load, err_msg: &str) { + let res = vm.run::<()>(obj); + assert!(res.is_err()); + let err = res.unwrap_err().into_runtime().unwrap(); + assert_eq!(err.msg(), err_msg); +} + +#[test] +fn test_vm_run() { + let vm = RootVm::new(); + let top = vm.top(); + run_assert_err( + &vm, + Code::new("test", b"return 1 + b"), + "test:1: attempt to perform arithmetic on global 'b' (a nil value)", + ); + run_assert_err( + &vm, + c"return 1 + b", + "[string \"return 1 + b\"]:1: attempt to perform arithmetic on global 'b' (a nil value)", + ); + run_assert_err(&vm, Code::new("this is an amazingly long text which should get truncated我", b"return 1 + b"), "this is an amazingly long text which should get truncated:1: attempt to perform arithmetic on global 'b' (a nil value)"); + let err = vm.run::<()>(BrokenReader).unwrap_err(); + assert_eq!( + err.to_string(), + "loader error: rust error: error in error handler" + ); + run_assert_err( + &vm, + Script::from_path("./tests/lua/basic.lua").unwrap(), + "basic.lua:2: nope", + ); + let err = vm + .run::<()>(Script::from_path("./tests/lua/broken.lua").unwrap()) + .unwrap_err(); + assert_eq!( + err.to_string(), + "syntax error: broken.lua:2: '(' expected near 'end'" + ); + vm.run::<()>(Script::from_path("./tests/lua/class.lua").unwrap()) + .unwrap(); + let func = vm.load_code(c"return 1 + b").unwrap(); + func.call::<()>(()).unwrap_err(); + assert_eq!(vm.top(), top + 1); +} diff --git a/core/tests/test_vm_run_string.rs b/core/tests/test_vm_run_string.rs new file mode 100644 index 0000000..b5d83b7 --- /dev/null +++ b/core/tests/test_vm_run_string.rs @@ -0,0 +1,58 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "root-vm")] + +use bp3d_lua::decl_lib_func; +use bp3d_lua::vm::function::types::RFunction; +use bp3d_lua::vm::RootVm; + +decl_lib_func! { + fn dostring(vm: &Vm, code: &str) -> bp3d_lua::vm::Result<()> { + let ret = vm.run_code::<()>(code); + println!("Ran code: {}", code); + ret + } +} + +#[test] +fn test_run_string() { + let vm = RootVm::new(); + let res = vm.run_code::<()>(c"dostring('test')"); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().to_string(), "runtime error: [string \"dostring('test')\"]:1: attempt to call global 'dostring' (a nil value)"); + vm.set_global(c"dostring", RFunction::wrap(dostring)) + .unwrap(); + assert!(vm.run_code::<()>(c"dostring('test')").is_err()); + assert!(vm + .run_code::<()>(c"dostring('print(\"whatever 123\")')") + .is_ok()); + assert!(vm.run_code::<()>(c"dostring('root = 42')").is_ok()); + let val: u32 = vm.get_global("root").unwrap(); + assert_eq!(val, 42); +} diff --git a/core/tests/test_vm_safety.rs b/core/tests/test_vm_safety.rs new file mode 100644 index 0000000..63c5152 --- /dev/null +++ b/core/tests/test_vm_safety.rs @@ -0,0 +1,33 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[test] +fn test_vm_safety() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/safety/*.rs"); +} diff --git a/core/tests/test_vm_tables.rs b/core/tests/test_vm_tables.rs new file mode 100644 index 0000000..d75b574 --- /dev/null +++ b/core/tests/test_vm_tables.rs @@ -0,0 +1,113 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "root-vm")] + +use bp3d_lua::vm::table::Table; +use bp3d_lua::vm::RootVm; + +#[test] +fn test_vm_tables() { + let mut vm = RootVm::new(); + let top = vm.top(); + vm.scope(|vm| { + let mut tbl = Table::new(vm); + tbl.set(c"a", 0.42)?; + tbl.set(c"b", "My great string")?; + let mut new_table = Table::new(vm); + new_table.set(c"whatever", 42)?; + let s: &str = tbl.get(c"b")?; + assert_eq!(s, "My great string"); + tbl.set(c"sub", new_table)?; + assert_eq!(tbl.len(), 3); + vm.set_global(c"myTable", tbl) + }) + .unwrap(); + let new_top = vm.top(); + assert_eq!(top, new_top); + let v = vm.run_code::(c"return myTable.c"); + assert!(v.is_err()); + let v = vm.run_code::(c"return myTable.a"); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), 0.42); + let v = vm.run_code::<&str>(c"return myTable.b"); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), "My great string"); + let v = vm.run_code::(c"return myTable.sub.whatever"); + assert!(v.is_ok()); + assert_eq!(v.unwrap(), 42); + vm.clear(); + let new_top_1 = vm.top(); + assert_eq!(new_top, new_top_1); + vm.scope(|vm| { + let tbl: Table = vm.get_global("myTable")?; + assert_eq!(tbl.len(), 3); + let v: f64 = tbl.get(c"a")?; + assert_eq!(v, 0.42); + let v = vm.run_code::<&str>(c"return myTable.b")?; + assert_eq!(v, "My great string"); + { + let v: f64 = tbl.get(c"a")?; + assert_eq!(v, 0.42); + } + assert_eq!(v, "My great string"); + Ok(()) + }) + .unwrap(); + assert_eq!(vm.top(), new_top); +} + +#[test] +fn test_vm_tables_collect_simple() { + let vm = RootVm::new(); + let top = vm.top(); + let mut tbl = Table::new(&vm); + tbl.push("Hello").unwrap(); + tbl.push("world").unwrap(); + let res: Vec = tbl.collect().unwrap(); + assert_eq!(res, vec!["Hello", "world"]); + assert_eq!(vm.top(), top + 1); // One value on the stack (the original table 'tbl') +} + +#[test] +fn test_vm_tables_collect_complex() { + let vm = RootVm::new(); + let top = vm.top(); + let mut tbl = Table::new(&vm); + let mut sub = Table::new(&vm); + sub.push("Hello").unwrap(); + sub.push("World").unwrap(); + tbl.push(sub).unwrap(); + let mut sub2 = Table::new(&vm); + sub2.push("Hello").unwrap(); + sub2.push("World").unwrap(); + tbl.push(sub2).unwrap(); + let res: Vec> = tbl.collect().unwrap(); + assert_eq!(res, vec![vec!["Hello", "World"], vec!["Hello", "World"]]); + assert_eq!(vm.top(), top + 1); // One value on the stack (the original table 'tbl') +} diff --git a/core/tests/test_vm_threads.rs b/core/tests/test_vm_threads.rs new file mode 100644 index 0000000..b4bbbfc --- /dev/null +++ b/core/tests/test_vm_threads.rs @@ -0,0 +1,271 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::vm::closure::rc::{Rc, Shared}; +use bp3d_lua::vm::core::UnSendRootVm; +use bp3d_lua::vm::function::types::RFunction; +use bp3d_lua::vm::thread::core::{State, Yield}; +use bp3d_lua::vm::thread::value::Thread; +use bp3d_lua::vm::value::types::Function; +use bp3d_lua::{decl_closure, decl_lib_func}; +use std::cell::Cell; + +decl_closure! { + fn increment |val: Rc>| () -> () { + val.set(val.get() + 1); + } +} + +#[test] +fn test_vm_threads_yield_lua() { + let vm = UnSendRootVm::new(); + assert!(vm.as_thread().is_none()); + let obj = Shared::new(Cell::new(0)); + vm.set_global(c"increment", increment(Rc::from_rust(&vm, obj.clone()))) + .unwrap(); + vm.run_code::<()>( + c" + CO = coroutine.create(function() + increment() + local value = coroutine.yield() + if (value == 42) then + increment() + end + end) + ", + ) + .unwrap(); + let thread: Thread = vm.get_global(c"CO").unwrap(); + assert_eq!(obj.get(), 0); + assert_eq!( + thread.as_thread().resume::<()>(()).unwrap().state, + State::Suspended + ); + assert_eq!(obj.get(), 1); + assert_eq!( + thread.as_thread().resume::<()>(42).unwrap().state, + State::Finished + ); + assert_eq!(obj.get(), 2); + // A finished thread will fail to resume. + assert!(thread.as_thread().resume::<()>(()).is_err()); + assert!(thread.as_thread().resume::<()>(true).is_err()); + assert!(thread.as_thread().resume::<()>(()).is_err()); +} + +decl_lib_func! { + fn my_yield() -> Yield<()> { + Yield(()) + } +} + +decl_lib_func! { + fn my_yield2(v: i32) -> Yield { + Yield(v) + } +} + +#[test] +fn test_vm_threads_yield_rust_fail() { + let vm = UnSendRootVm::new(); + assert!(vm.as_thread().is_none()); + vm.set_global(c"my_yield", RFunction::wrap(my_yield)) + .unwrap(); + let res = vm + .run_code::<()>(c"my_yield()") + .unwrap_err() + .into_runtime() + .unwrap(); + assert_eq!( + res.msg(), + "[string \"my_yield()\"]:1: attempt to yield a non-thread stack object" + ); +} + +#[test] +fn test_vm_threads_yield_rust() { + let vm = UnSendRootVm::new(); + assert!(vm.as_thread().is_none()); + let obj = Shared::new(Cell::new(0)); + vm.set_global(c"increment", increment(Rc::from_rust(&vm, obj.clone()))) + .unwrap(); + vm.set_global(c"my_yield", RFunction::wrap(my_yield)) + .unwrap(); + vm.run_code::<()>( + c" + CO = coroutine.create(function() + increment() + local value = my_yield() + if (value == 42) then + increment() + end + end) + ", + ) + .unwrap(); + let thread: Thread = vm.get_global(c"CO").unwrap(); + assert_eq!(obj.get(), 0); + assert_eq!( + thread.as_thread().resume::<()>(()).unwrap().state, + State::Suspended + ); + assert_eq!(obj.get(), 1); + assert_eq!( + thread.as_thread().resume::<()>(42).unwrap().state, + State::Finished + ); + assert_eq!(obj.get(), 2); + // A finished thread will fail to resume. + assert!(thread.as_thread().resume::<()>(()).is_err()); + assert!(thread.as_thread().resume::<()>(()).is_err()); + assert!(thread.as_thread().resume::<()>(()).is_err()); +} + +#[test] +fn test_vm_threads_with_yield_value_lua() { + let vm = UnSendRootVm::new(); + assert!(vm.as_thread().is_none()); + let obj = Shared::new(Cell::new(0)); + vm.set_global(c"increment", increment(Rc::from_rust(&vm, obj.clone()))) + .unwrap(); + vm.run_code::<()>( + c" + CO = coroutine.create(function() + increment() + coroutine.yield(1) + increment() + return 42 + end) + ", + ) + .unwrap(); + let thread: Thread = vm.get_global(c"CO").unwrap(); + assert_eq!(obj.get(), 0); + assert_eq!(thread.as_thread().resume::(()).unwrap().data, 1); + assert_eq!(thread.as_thread().resume::(()).unwrap().data, 42); + assert_eq!(obj.get(), 2); +} + +#[test] +fn test_vm_threads_with_yield_value_rust() { + let vm = UnSendRootVm::new(); + assert!(vm.as_thread().is_none()); + let obj = Shared::new(Cell::new(0)); + vm.set_global(c"increment", increment(Rc::from_rust(&vm, obj.clone()))) + .unwrap(); + vm.set_global(c"my_yield", RFunction::wrap(my_yield2)) + .unwrap(); + vm.run_code::<()>( + c" + CO = coroutine.create(function() + increment() + my_yield(5) + increment() + return 42 + end) + ", + ) + .unwrap(); + let thread: Thread = vm.get_global(c"CO").unwrap(); + assert_eq!(obj.get(), 0); + assert_eq!(thread.as_thread().resume::(()).unwrap().data, 5); + assert_eq!(thread.as_thread().resume::(()).unwrap().data, 42); + assert_eq!(obj.get(), 2); +} + +#[test] +fn test_vm_threads_with_yield_value_unsafe() { + let vm = UnSendRootVm::new(); + assert!(vm.as_thread().is_none()); + let obj = Shared::new(Cell::new(0)); + vm.set_global(c"increment", increment(Rc::from_rust(&vm, obj.clone()))) + .unwrap(); + vm.run_code::<()>( + c" + CO = coroutine.create(function() + increment() + coroutine.yield(\"test\") + increment() + coroutine.yield(\"test2\") + increment() + collectgarbage() + return \"test3\" + end) + ", + ) + .unwrap(); + let thread: Thread = vm.get_global(c"CO").unwrap(); + assert_eq!(obj.get(), 0); + let s: String = thread.as_thread().resume(()).unwrap().data; + let s2: String = thread.as_thread().resume(()).unwrap().data; + let s3: String = thread.as_thread().resume(()).unwrap().data; + vm.run_code::<()>(c"CO = nil; collectgarbage()").unwrap(); + vm.run_code::<()>( + c" + CO = coroutine.create(function() + increment() + coroutine.yield(\"test\") + increment() + coroutine.yield(\"test2\") + increment() + collectgarbage() + return \"test3\" + end) + ", + ) + .unwrap(); + assert_eq!(s, "test"); + assert_eq!(s2, "test2"); + assert_eq!(s3, "test3"); + assert_eq!(obj.get(), 3); +} + +#[test] +fn test_vm_threads_set_function() { + let vm = UnSendRootVm::new(); + let top = vm.top(); + assert!(vm.as_thread().is_none()); + let obj = Shared::new(Cell::new(0)); + vm.set_global(c"increment", increment(Rc::from_rust(&vm, obj.clone()))) + .unwrap(); + vm.run_code::<()>(c"function ThreadMain() increment() coroutine.yield() increment() end") + .unwrap(); + let main_fn: Function = vm.get_global(c"ThreadMain").unwrap(); + let thread = Thread::new(&vm); + thread.set_function(main_fn).unwrap(); + assert_eq!( + thread.as_thread().resume::<()>(()).unwrap().state, + State::Suspended + ); + assert_eq!( + thread.as_thread().resume::<()>(()).unwrap().state, + State::Finished + ); + assert_eq!(obj.get(), 2); + assert_eq!(vm.top() - top, 2) +} diff --git a/core/tests/test_vm_userdata.rs b/core/tests/test_vm_userdata.rs new file mode 100644 index 0000000..a603292 --- /dev/null +++ b/core/tests/test_vm_userdata.rs @@ -0,0 +1,546 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#![cfg(feature = "root-vm")] + +use bp3d_lua::ffi::lua::RawNumber; +use bp3d_lua::util::Namespace; +use bp3d_lua::vm::function::types::RFunction; +use bp3d_lua::vm::userdata::{AnyUserData, LuaDrop}; +use bp3d_lua::vm::{RootVm, Vm}; +use bp3d_lua::{decl_lib_func, decl_userdata, impl_userdata, impl_userdata_mut}; +use std::sync::Mutex; + +static MUTEX: Mutex<()> = Mutex::new(()); + +static mut DROP_COUNTER: i32 = 0; +static mut LUA_DROP_COUNTER: i32 = 0; + +decl_userdata!(pub struct MyInt(i64)); + +impl LuaDrop for MyInt { + fn lua_drop(&self, _: &Vm) { + unsafe { + LUA_DROP_COUNTER += 1; + } + } +} + +impl Drop for MyInt { + fn drop(&mut self) { + unsafe { + DROP_COUNTER += 1; + } + } +} + +impl_userdata! { + impl MyInt { + fn tonumber(this: &MyInt) -> RawNumber { + this.0 as _ + } + + fn tostring(this: &MyInt) -> String { + this.0.to_string() + } + + fn __eq(this: &MyInt, other: &MyInt) -> bool { + this.0 == other.0 + } + + fn __lt(this: &MyInt, other: &MyInt) -> bool { + this.0 < other.0 + } + + fn __gt(this: &MyInt, other: &MyInt) -> bool { + this.0 > other.0 + } + + fn __add(this: &MyInt, other: &MyInt) -> MyInt { + MyInt(this.0 + other.0) + } + + fn __index(this: &MyInt, other: u8) -> Option { + if other == 0 { + Some(this.0 as _) + } else { + None + } + } + } + + static { + [fn new]; + } +} + +decl_userdata! { + #[derive(Debug)] + pub struct BrokenObject +} + +impl_userdata_mut! { + impl BrokenObject { + // this should blow up at init time + fn replace(this: &mut BrokenObject, other: &BrokenObject) -> () { + println!("this: {:?}, other: {:?}", this, other) + } + } +} + +decl_userdata!(pub struct BrokenObject2(pub u128)); + +impl_userdata! { + impl BrokenObject2 { + } +} + +decl_userdata! { + #[derive(Debug)] + pub struct BrokenObject3 +} + +impl_userdata! { + impl BrokenObject3 { + fn __gc(this: &BrokenObject3) -> () { + println!("{:?}", this); + } + } +} + +decl_userdata! { + #[derive(Debug)] + pub struct BrokenObject4 +} + +impl_userdata! { + impl BrokenObject4 { + fn __metatable(this: &BrokenObject3) -> () { + println!("{:?}", this); + } + } +} + +decl_lib_func! { + fn my_int(i: i64) -> MyInt { + MyInt(i) + } +} + +decl_lib_func! { + fn new(i: i64) -> MyInt { + MyInt(i) + } +} + +#[test] +fn test_vm_userdata_forgot_reg() { + let vm = RootVm::new(); + vm.set_global(c"MyInt", RFunction::wrap(my_int)).unwrap(); + vm.run_code::<()>(c"a = MyInt(123)").unwrap(); + vm.run_code::<()>(c"b = MyInt(456)").unwrap(); + assert!(vm.run_code::(c"return a < b").is_err()); + assert!(vm.run_code::(c"return a + b").is_err()); +} + +#[test] +fn test_vm_userdata_error_handling() { + let vm = RootVm::new(); + let top = vm.top(); + vm.register_userdata::(bp3d_lua::vm::userdata::case::Snake) + .unwrap(); + assert_eq!(top, vm.top()); + let res = vm.register_userdata::(bp3d_lua::vm::userdata::case::Snake); + assert!(res.is_err()); + let msg = res.unwrap_err().to_string(); + assert_eq!( + msg, + "userdata: violation of the unique type rule for mutable method \"replace\"" + ); + assert_eq!(top, vm.top()); + let res = vm.register_userdata::(bp3d_lua::vm::userdata::case::Snake); + assert!(res.is_err()); + let msg = res.unwrap_err().to_string(); + assert_eq!( + msg, + "userdata: too strict alignment required (16 bytes), max is 8 bytes" + ); + assert_eq!(top, vm.top()); + let res = vm.register_userdata::(bp3d_lua::vm::userdata::case::Snake); + assert!(res.is_err()); + let msg = res.unwrap_err().to_string(); + assert_eq!(msg, "userdata: __gc meta-method is reserved for internal use, if you need Vm access in drop, please use LuaDrop"); + assert_eq!(top, vm.top()); + let res = vm.register_userdata::(bp3d_lua::vm::userdata::case::Snake); + assert_eq!(top, vm.top()); + assert!(res.is_err()); + let msg = res.unwrap_err().to_string(); + assert_eq!( + msg, + "userdata: class name \"MyInt\" has already been registered" + ); + assert_eq!(top, vm.top()); + let res = vm.register_userdata::(bp3d_lua::vm::userdata::case::Snake); + assert!(res.is_err()); + let msg = res.unwrap_err().to_string(); + assert_eq!( + msg, + "userdata: __metatable is set for security reasons and cannot be altered" + ); + assert_eq!(top, vm.top()); +} + +fn test_vm_userdata_base(vm: &Vm) { + unsafe { + DROP_COUNTER = 0; + LUA_DROP_COUNTER = 0; + } + let top = vm.top(); + vm.register_userdata::(bp3d_lua::vm::userdata::case::Snake) + .unwrap(); + assert_eq!(top, vm.top()); + vm.set_global(c"MyInt", RFunction::wrap(my_int)).unwrap(); + assert_eq!(top, vm.top()); + vm.run_code::<()>(c"a = MyInt(123)").unwrap(); + vm.run_code::<()>(c"b = MyInt(456)").unwrap(); + vm.run_code::<()>(c"c = MyInt(456)").unwrap(); + assert_eq!(vm.run_code::(c"return a == b").unwrap(), false); + assert_eq!(vm.run_code::(c"return b == c").unwrap(), true); + assert_eq!(vm.run_code::(c"return a < b").unwrap(), true); + assert_eq!(vm.run_code::(c"return b > a").unwrap(), true); + assert_eq!(vm.run_code::<&MyInt>(c"return a + b").unwrap().0, 579); + assert_eq!( + vm.run_code::<&str>(c"return (a + b):tostring()").unwrap(), + "579" + ); + assert_eq!( + vm.run_code::(c"return (a + b):tonumber()") + .unwrap(), + 579.0 + ); + assert_eq!( + vm.run_code::(c"return a.tonumber(b)").unwrap(), + 456.0 + ); + assert_eq!(top + 8, vm.top()); +} + +fn test_vm_userdata_base2(vm: &Vm) { + unsafe { + DROP_COUNTER = 0; + LUA_DROP_COUNTER = 0; + } + let top = vm.top(); + { + let mut namespace = Namespace::new(vm, "_G").unwrap(); + namespace + .add_userdata::("MyInt", bp3d_lua::vm::userdata::case::Snake) + .unwrap(); + } + assert_eq!(top, vm.top()); + vm.run_code::<()>(c"a = MyInt.new(123)").unwrap(); + vm.run_code::<()>(c"b = MyInt.new(456)").unwrap(); + vm.run_code::<()>(c"c = MyInt.new(456)").unwrap(); + assert_eq!(vm.run_code::(c"return a == b").unwrap(), false); + assert_eq!(vm.run_code::(c"return b == c").unwrap(), true); + assert_eq!(vm.run_code::(c"return a < b").unwrap(), true); + assert_eq!(vm.run_code::(c"return b > a").unwrap(), true); + assert_eq!(vm.run_code::<&MyInt>(c"return a + b").unwrap().0, 579); + assert_eq!( + vm.run_code::<&str>(c"return (a + b):tostring()").unwrap(), + "579" + ); + assert_eq!( + vm.run_code::(c"return (a + b):tonumber()") + .unwrap(), + 579.0 + ); + assert_eq!( + vm.run_code::(c"return a.tonumber(b)").unwrap(), + 456.0 + ); + assert_eq!(top + 8, vm.top()); +} + +fn test_vm_userdata_base3(vm: &Vm) { + unsafe { + DROP_COUNTER = 0; + LUA_DROP_COUNTER = 0; + } + let top = vm.top(); + { + let mut namespace = Namespace::new(vm, "_G").unwrap(); + namespace + .add_userdata::("MyInt", bp3d_lua::vm::userdata::case::Camel) + .unwrap(); + } + assert_eq!(top, vm.top()); + vm.run_code::<()>(c"a = MyInt.new(123)").unwrap(); + vm.run_code::<()>(c"b = MyInt.new(456)").unwrap(); + vm.run_code::<()>(c"c = MyInt.new(456)").unwrap(); + assert_eq!(vm.run_code::(c"return a == b").unwrap(), false); + assert_eq!(vm.run_code::(c"return b == c").unwrap(), true); + assert_eq!(vm.run_code::(c"return a < b").unwrap(), true); + assert_eq!(vm.run_code::(c"return b > a").unwrap(), true); + assert_eq!(vm.run_code::<&MyInt>(c"return a + b").unwrap().0, 579); + assert_eq!( + vm.run_code::<&str>(c"return (a + b):tostring()").unwrap(), + "579" + ); + assert_eq!( + vm.run_code::(c"return (a + b):tonumber()") + .unwrap(), + 579.0 + ); + assert_eq!( + vm.run_code::(c"return a.tonumber(b)").unwrap(), + 456.0 + ); + assert_eq!(top + 8, vm.top()); +} + +#[test] +fn test_vm_userdata() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + let top = vm.top(); + test_vm_userdata_base(&vm); + assert_eq!(top + 8, vm.top()); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security1() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base(&vm); + vm.run_code::<()>(c"getmetatable(a).__gc = function() print(\"Lua has hacked Rust\") end") + .unwrap_err(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security2() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base(&vm); + vm.run_code::<()>(c"a.__gc = function() print(\"Lua has hacked Rust\") end") + .unwrap_err(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security3() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base(&vm); + vm.run_code::<()>(c"setmetatable(a, nil)").unwrap_err(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security4() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base(&vm); + vm.run_code::<()>( + c" + local func = a.tonumber + local tbl = {} + tbl.tonumber = func + tbl:tonumber() + ", + ) + .unwrap_err(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security5() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base(&vm); + vm.run_code::<()>( + c" + rawset(a, '__gc', nil) + ", + ) + .unwrap_err(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_call_method() { + let _guard = MUTEX.lock(); + let vm = RootVm::new(); + let top = vm.top(); + vm.register_userdata::(bp3d_lua::vm::userdata::case::Snake) + .unwrap(); + vm.set_global("MY_INT", MyInt(123456)).unwrap(); + let ud: AnyUserData = vm.get_global("MY_INT").unwrap(); + let val: &str = ud.call_method("tostring", ()).unwrap(); + let val2: RawNumber = ud.call_method("tonumber", ()).unwrap(); + assert_eq!(val, "123456"); + assert_eq!(val2, 123456.0); + assert_eq!(vm.top(), top + 3); +} + +#[test] +fn test_vm_userdata_display() { + let _guard = MUTEX.lock(); + let vm = RootVm::new(); + let top = vm.top(); + vm.register_userdata::(bp3d_lua::vm::userdata::case::Snake) + .unwrap(); + vm.set_global("MY_INT", MyInt(123456)).unwrap(); + let ud: AnyUserData = vm.get_global("MY_INT").unwrap(); + let s = ud.to_string(); + assert_eq!(s, "MyInt(123456)"); + assert_eq!(vm.top(), top + 1); +} + +#[test] +fn test_vm_userdata_statics() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base2(&vm); + vm.run_code::<()>( + c" + MyInt.__gc = function() print(\"Lua has hacked Rust\") end + MyInt.__metatable = function() print(\"Lua has hacked Rust\") end + ", + ) + .unwrap(); + let ud: AnyUserData = vm.get_global("a").unwrap(); + let s = ud.to_string(); + assert_eq!(s, "MyInt(123)"); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_statics_2() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base2(&vm); + vm.run_code::<()>( + c" + assert(a[0] == 123) + assert(a[1] == nil) + assert(b[0] == 456) + ", + ) + .unwrap(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security6() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base(&vm); + vm.run_code::<()>(c"a.__index.__gc = function() print(\"Lua has hacked Rust\") end") + .unwrap_err(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security7() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base2(&vm); + vm.run_code::<()>(c"getmetatable(a).__gc = function() print(\"Lua has hacked Rust\") end") + .unwrap_err(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security8() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base3(&vm); + vm.run_code::<()>(c"getmetatable(a).__gc = function() error(\"Lua has hacked Rust\") end") + .unwrap_err(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security9() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base(&vm); + vm.run_code::<()>(c"a:__gc()").unwrap(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} + +#[test] +fn test_vm_userdata_security10() { + let _guard = MUTEX.lock(); + { + let vm = RootVm::new(); + test_vm_userdata_base(&vm); + vm.run_code::<()>(c"a.__gc(a)").unwrap(); + } + assert_eq!(unsafe { DROP_COUNTER }, 6); + assert_eq!(unsafe { LUA_DROP_COUNTER }, 6); +} diff --git a/definitions/bp3d.files.lua b/definitions/bp3d.files.lua new file mode 100644 index 0000000..10365fb --- /dev/null +++ b/definitions/bp3d.files.lua @@ -0,0 +1,179 @@ +-- Copyright (c) 2025, BlockProject 3D +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, +-- are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, +-- this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, +-- this list of conditions and the following disclaimer in the documentation +-- and/or other materials provided with the distribution. +-- * Neither the name of BlockProject 3D nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- @meta bp3d.files + +bp3d = {} +bp3d.files = {} + +--- @class Path +Path = {} + +--- Join this path with a new component. +--- +--- @param other string | Path path component to join with. +--- @return Path +function Path:join(other) end + +--- Changes the extension of this path. +--- +--- @param ext string the new extension to set. +--- @return Path +function Path:withExtension(ext) end + +--- Changes the file name of this path. +--- +--- @param name string the new file name to set. +--- @return Path +function Path:withName(name) end + +--- @return string whatever the file name. +function Path:name() end + +--- @return string whatever the file extension. +function Path:extension() end + +--- Creates a new Path object. +--- +--- @param path string the path to wrap. +--- @return Path +Path.new = function(path) end + +--- @class File +File = {} + +--- Read a block of data from the file. Returns a byte string of the content. +--- +--- @param size number number of bytes to read. +--- @return string +function File:read(size) end + +--- Writes the given block of data to the file. Returns the number of bytes written. +--- +--- @param data string byte string representing the block of data. +--- @return number +function File:write(data) end + +--- Seeks from the start of the file. +--- +--- @param pos number (uint64_t LuaJIT cdata type) +--- @return number uint64_t LuaJIT cdata type +function File:seekFromStart(pos) end + +--- Seeks from the end of the file. +--- +--- @param pos number (int64_t LuaJIT cdata type) +--- @return number uint64_t LuaJIT cdata type +function File:seekFromEnd(pos) end + +--- Seeks from the current cursor position. +--- +--- @param pos number (int64_t LuaJIT cdata type) +--- @return number uint64_t LuaJIT cdata type +function File:seekFromCursor(pos) end + +--- Returns the size of the file. +--- +--- @return number uint64_t LuaJIT cdata type +function File:size() end + +--- Opens a new file for read/write or append. +--- +--- @param path string | Path the path of the file. +--- @param mode string the file mode, r for read, w for write and a for append. +--- @return File +File.open = function(path, mode) end + +--- Creates a new file for writing. +--- +--- @param path string | Path the path of the file. +--- @return File +File.create = function(path) end + +--- Read a text file. +--- +--- @param path string | Path the path to read from. +--- @return string +bp3d.files.readText = function(path) end + +--- Write a text file. +--- +--- @param path string | Path the path to write to. +--- @param data string the file content. +bp3d.files.writeText = function(path, data) end + +--- Copy a file. +--- +--- @param src string | Path the source path. +--- @param dst string | Path the destination path. +bp3d.files.copyFile = function(src, dst) end + +--- Creeates a symlink. +--- +--- @param src string | Path the source path. +--- @param dst string | Path the destination path. +bp3d.files.symlink = function(src, dst) end + +--- Creeates the directory at the specified path. +--- +--- @param path string | Path +bp3d.files.deleteDir = function(path) end + +--- Creeates a directory at the specified path. +--- +--- @param path string | Path +bp3d.files.createDir = function(path) end + +--- Returns true if the path exists. +--- +--- @param path string | Path +--- @return boolean +bp3d.files.exists = function(path) end + +--- List files and folders under the specified directory. +--- +--- @param path string | Path +--- @return [{ path: Path, name: string, type: "dir" | "file" | "symlink" | "other" }] +bp3d.files.list = function(path) end + +--- Returns the permissions of the specified file path. +--- +--- @param path string | Path +--- @return { r: boolean, w: boolean, x: boolean } +bp3d.files.access = function(path) end + +--- Deletes a single file. +--- +--- @param path string | Path file path. +bp3d.files.delete = function(path) end + +--- Renames a file. This function fails if src does not exist or if dst already exists. +--- +--- @param src string | Path the source path. +--- @param dst string | Path the destination path. +bp3d.files.rename = function(src, dst) end diff --git a/definitions/bp3d.lua.debug.lua b/definitions/bp3d.lua.debug.lua new file mode 100644 index 0000000..8589298 --- /dev/null +++ b/definitions/bp3d.lua.debug.lua @@ -0,0 +1,67 @@ +-- Copyright (c) 2025, BlockProject 3D +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, +-- are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, +-- this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, +-- this list of conditions and the following disclaimer in the documentation +-- and/or other materials provided with the distribution. +-- * Neither the name of BlockProject 3D nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- @meta bp3d.lua.debug + +bp3d = {} +bp3d.lua = {} +bp3d.lua.debug = {} + +--- Dump the content of the Lua stack starting at a given index. +--- +--- @param startIndex number the index at which to start dumping the stack. +--- @return [string] whatever list of all variables on the stack. +bp3d.lua.debug.dumpStack = function(startIndex) end + +--- Dump all libs registered within the current Lua VM. +--- +--- @return [string] +bp3d.lua.debug.dumpLibs = function() end + +--- Dump all UserData classes registered within the current Lua VM. +--- +--- @return [string] +bp3d.lua.debug.dumpClasses = function() end + +--- Dump the static members of the given UserData class.. +--- +--- @param class string the UserData class to dump. +--- @return [string] +bp3d.lua.debug.dumpStaticTable = function(class) end + +--- Dump the instance members of the given UserData class.. +--- +--- @param class string the UserData class to dump. +--- @return [string] +bp3d.lua.debug.dumpMetaTable = function(class) end + +--- Dump the class name of the given UserData class.. +--- +--- @param class string the UserData class to dump. +--- @return [string] +bp3d.lua.debug.dumpClassName = function(class) end diff --git a/definitions/bp3d.lua.lua b/definitions/bp3d.lua.lua new file mode 100644 index 0000000..b25621f --- /dev/null +++ b/definitions/bp3d.lua.lua @@ -0,0 +1,117 @@ +-- Copyright (c) 2025, BlockProject 3D +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, +-- are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, +-- this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, +-- this list of conditions and the following disclaimer in the documentation +-- and/or other materials provided with the distribution. +-- * Neither the name of BlockProject 3D nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- @meta bp3d.lua + +bp3d = {} + +--- @class bp3d.lua +--- @field name string The name of the Lua engine used. +--- @field version string The version string of the Lua engine following SemVer notation. +--- @field patches string[] The list of patches applied to the underlying LuaJIT interpreter. +bp3d.lua = {} + +--- Protect call to a Lua function and extract traceback information in case of error. +--- +--- @param func function the function to call in a protected environment. +--- @return [boolean, (any[] | string)] whatever true if the call succeeded, false otherwise followed by the error backtrace. +bp3d.lua.pcall = function(func) end + +--- Runs a Lua string and raises an error if the execution failed. +--- +--- @raises syntax or runtime error. +--- @param s string to run. +--- @param chunkname string? optional chunkname. +--- @return any[] whatever outputs returned by the function which was executed. +bp3d.lua.runString = function(s, chunkname) end + +--- Loads a Lua string and raises an error on broken syntax. +--- This function does not run the string. +--- +--- @raises syntax error. +--- @param s string to load/compile. +--- @param chunkname string? optional chunkname. +--- @return function whatever the compiled function. +bp3d.lua.loadString = function(s, chunkname) end + +--- Loads a Lua file and raises an error on broken syntax. +--- This function does not run the string. +--- +--- # Sandboxing +--- +--- This function uses a chroot to limit the location of the file which can loaded. +--- This avoids arbitrary code execution on files which the user does not whish to make visible to the Lua engine. +--- +--- @raises syntax error. +--- @param path string | Path path to the file to load/compile. +--- @return function whatever the compiled function. +bp3d.lua.loadFile = function(path) end + +--- Runs a Lua file and raises an error if the execution failed. +--- +--- # Sandboxing +--- +--- This function uses a chroot to limit the location of the file which can loaded. +--- This avoids arbitrary code execution on files which the user does not whish to make visible to the Lua engine. +--- +--- @raises syntax or runtime error. +--- @param path string | Path path to the file to load/compile and run. +--- @return any[] whatever outputs returned by the function which was executed. +bp3d.lua.runFile = function(path) end + +--- Runs a Lua file and raises an error if the execution failed. +--- Unlike runFile this function allows to omit script file extension and uses a search algorithm to be defined by the +--- host application instead of the bp3d-lua engine. +--- The search algorithm is optional, if no search algorithm is provided by the host application, user of the bp3d-lua +--- engine, then this function will be set to nil. +--- +--- # Sandboxing +--- +--- To control which directories are allowed to contain runnable lua files, the search algorithm is left to the host +--- application. +--- +--- @raises syntax or runtime error. +--- @param path string path to the script library file to load/compile and run. Path separator is '.' like standard +--- require function. +--- @return any[] whatever outputs returned by the function which was executed. +bp3d.lua.require = function(path) end + +bp3d.lua.module = {} + +--- Loads a native module into the current underlying LuaJIT interpreter. +--- File extensions and other platform specific prefixes are pre-applied by the underlying search algorithm and must +--- not be specified in the lib or plugin arguments to this function. +--- +--- # Sandboxing +--- +--- This function limits the modules search paths to a set of folders specified at the application level. +--- This limits arbitrary native code execution and essentially forbids arbitrary native code injection by Lua code. +--- +--- @param lib string the name of the library to load. +--- @param plugin string the name of the plugin in the native library to load. +bp3d.lua.module.load = function(lib, plugin) end diff --git a/definitions/bp3d.lua.shell.lua b/definitions/bp3d.lua.shell.lua new file mode 100644 index 0000000..08c34db --- /dev/null +++ b/definitions/bp3d.lua.shell.lua @@ -0,0 +1,73 @@ +-- Copyright (c) 2025, BlockProject 3D +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, +-- are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, +-- this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, +-- this list of conditions and the following disclaimer in the documentation +-- and/or other materials provided with the distribution. +-- * Neither the name of BlockProject 3D nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- @meta bp3d.lua.shell + +bp3d = {} +bp3d.lua = {} +bp3d.lua.shell = {} + +--- Looks for a value named `name` in _G and build autocompletions to be sent to the frontend for rendering purposes. +--- +--- @raises runtime error if the value with name `name` cannot be found. +--- @param name string the name of the key in _G to build autocompletions for. +--- @param metatable boolean true to enable listing recursive metatables, false otherwise. +bp3d.lua.shell.buildCompletions = function(name, metatable) end + +--- Removes all autocompletions which were added for `name`. +--- +--- @raises runtime error if the value with name `name` cannot be found. +--- @param name string the name of the key in _G to remove autocompletions for. +--- @param metatable boolean true to enable listing recursive metatables, false otherwise. +bp3d.lua.shell.deleteCompletions = function(name, metatable) end + +--- Schedules a lua thread to be resumed/started after a specified time as a whole number in milliseconds. +--- +---@param thread thread the thread to resume/start. +---@param after_ms integer 32 bits unsigned integer number of milliseconds after now at which to run the thread. +bp3d.lua.shell.scheduleIn = function(thread, after_ms) end + +--- Schedules a lua thread to be resumed/started at a regular time interval specified as a whole number in milliseconds. +--- +---@param thread thread the thread to resume/start. +---@param period_ms integer 32 bits unsigned integer interval in milliseconds at which to run the thread. +bp3d.lua.shell.schedulePeriodically = function(thread, period_ms) end + +--- Binds a Lua function to the given event name. +--- +---@param name string the name of the event to bind to. +---@param func function the lua function to run every time this event is raised. +bp3d.lua.shell.bindEvent = function(name, func) end + +--- Unbinds the Lua function from the given event name. +--- +---@param name string the name of the event to unbind the lua function from. +bp3d.lua.shell.unbindEvent = function(name) end + +--- Requests exit of the shell application from lua code. +bp3d.lua.shell.requestExit = function() end diff --git a/definitions/bp3d.os.lua b/definitions/bp3d.os.lua new file mode 100644 index 0000000..4312d12 --- /dev/null +++ b/definitions/bp3d.os.lua @@ -0,0 +1,137 @@ +-- Copyright (c) 2025, BlockProject 3D +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, +-- are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, +-- this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, +-- this list of conditions and the following disclaimer in the documentation +-- and/or other materials provided with the distribution. +-- * Neither the name of BlockProject 3D nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- @meta bp3d.os + +bp3d = {} +bp3d.os = {} + +bp3d.os.instant = {} + +--- Creates a new instance of a Instant object. +--- @return Instant whatever a new Instant object. +bp3d.os.instant.now = function() end + +--- @class Instant +Instant = {} + +--- @return number whatever number of seconds since the creation of this object. +function Instant:elapsed() end + +bp3d.os.time = {} + +--- @return OffsetDateTime +bp3d.os.time.nowUtc = function() end + +--- @return OffsetDateTime +bp3d.os.time.nowLocal = function() end + +--- @class Offset +--- @field hours integer +--- @field minutes integer +--- @field seconds integer +Offset = {} + +--- @class Time +--- @field hour integer +--- @field minute integer +--- @field second integer +Time = {} + +--- @class Date +--- @field year integer +--- @field month integer +--- @field day integer +Date = {} + +--- @class RawComponents +--- @field year integer +--- @field month integer +--- @field day integer +--- @field hour integer? +--- @field min integer? +--- @field sec integer? +--- @field offset Offset? +RawComponents = {} + +bp3d.os.time.OffsetDateTime = {} + +--- Creates a new OffsetDateTime from its raw components. +--- +--- @param table RawComponents manual definition of a date time with an optional offset. +--- @return OffsetDateTime +bp3d.os.time.OffsetDateTime.new = function(table) end + +--- Constructs a new OffsetDateTime from a unix timestamp in seconds. +--- +--- @param timestamp integer the unix timestamp in seconds. +--- @return OffsetDateTime +bp3d.os.time.OffsetDateTime.fromUnixTimestamp = function(timestamp) end + +--- @class OffsetDateTime +OffsetDateTime = {} + +--- Formats this OffsetDateTime following a given format string. +--- The format string should match with the semantics of the Rust time crate. +--- +--- @param format string the format string. +--- @return string whatever formatted string. +function OffsetDateTime:format(format) end + +--- @param duration number duration in seconds. +--- @return OffsetDateTime +function OffsetDateTime:__add(duration) end + +--- @param other OffsetDateTime other operand to subtract with. +--- @return number whatever the dureation in seconds of (self - other). +function OffsetDateTime:__sub(other) end + +--- @param other OffsetDateTime other operand to compare with. +--- @return boolean whatever true if self > other, false otherwise. +function OffsetDateTime:__gt(other) end + +--- @param other OffsetDateTime other operand to compare with. +--- @return boolean whatever true if self >= other, false otherwise. +function OffsetDateTime:__ge(other) end + +--- @param other OffsetDateTime other operand to compare with. +--- @return boolean whatever true if self < other, false otherwise. +function OffsetDateTime:__lt(other) end + +--- @param other OffsetDateTime other operand to compare with. +--- @return boolean whatever true if self <= other, false otherwise. +function OffsetDateTime:__le(other) end + +--- @return Date whatever the date component of this OffsetDateTime. +function OffsetDateTime:getDate() end + +--- @return Time whatever the time component of this OffsetDateTime. +function OffsetDateTime:getTime() end + +--- @return Offset whatever the UTC offset component of this OffsetDateTime. +function OffsetDateTime:getOffset() end diff --git a/definitions/bp3d.util.lua b/definitions/bp3d.util.lua new file mode 100644 index 0000000..2cd095d --- /dev/null +++ b/definitions/bp3d.util.lua @@ -0,0 +1,287 @@ +-- Copyright (c) 2025, BlockProject 3D +-- +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without modification, +-- are permitted provided that the following conditions are met: +-- +-- * Redistributions of source code must retain the above copyright notice, +-- this list of conditions and the following disclaimer. +-- * Redistributions in binary form must reproduce the above copyright notice, +-- this list of conditions and the following disclaimer in the documentation +-- and/or other materials provided with the distribution. +-- * Neither the name of BlockProject 3D nor the names of its contributors +-- may be used to endorse or promote products derived from this software +-- without specific prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +--- @meta bp3d.util + +bp3d = {} +bp3d.util = {} +bp3d.util.string = {} +bp3d.util.table = {} +bp3d.util.utf8 = {} +bp3d.util.num = {} + +--- Checks if the given `src` string starts with the prefix `prefix`. +--- +--- @param src string the source string. +--- @param prefix string the string to search for. +--- @return boolean whatever true if `src` starts with `prefix`, false otherwise. +bp3d.util.string.startsWith = function(src, prefix) end + +--- Checks if the given `src` string ends with the suffix `suffix`. +--- +--- @param src string the source string. +--- @param suffix string the string to search for. +--- @return boolean whatever true if `src` ends with `suffix`, false otherwise. +bp3d.util.string.endsWith = function(src, suffix) end + +--- Checks if the given sub string `needle` can be found in `src`. +--- +--- @param src string the source string. +--- @param needle string the string to search for. +--- @return boolean whatever true if `needle` was found in `src`, false otherwise. +bp3d.util.string.contains = function(src, needle) end + +--- Splits the given string `src` using the separator `pattern`. +--- +--- @param src string input source string. +--- @param pattern integer separator character. +--- @return string[] +bp3d.util.string.split = function(src, pattern) end + +--- Capitalises the given string. +--- +--- Note: This function ignores UTF-8 characters. +--- +--- @param src string the string to capitalise. +--- @return string +bp3d.util.string.capitalise = function(src) end + +--- De-capitalises the given string. +--- +--- Note: This function ignores UTF-8 characters. +--- +--- @param src string the string to decapitalise. +--- @return string +bp3d.util.string.decapitalise = function(src) end + +--- Merges the keys of 1 table into another. +--- +--- @param dst table the destination table. +--- @param src table the source table to merge. +bp3d.util.table.update = function(dst, src) end + +--- Concatenates N source tables into a destination table. +--- +--- @param dst table the destination table which should have the keys of all source tables. +--- @param ... table a list of all source table to concatenate in `dst`. +bp3d.util.table.concat = function(dst, ...) end + +--- Deep-copies the table given as argument and return a new table. +--- +--- @param src table input table to deep-copy. +--- @return table +bp3d.util.table.copy = function(src) end + +--- @param src table input table to compute length of. +--- @return integer whatever the number of items in `src`. +--- This function is optimized to choose either the fast objlen method or a slow iterator based on if the table has +--- a hash component or not. +bp3d.util.table.count = function(src) end + +--- @param src table input table. +--- @return string whatever a string listing all key-value pairs in the table for quick display purposes. +bp3d.util.table.tostring = function(src) end + +--- @param src table input table. +--- @param value any value to search for. +--- @return boolean whatever true if value was found in the table, false otherwise. +bp3d.util.table.contains = function(src, value) end + +--- @param src table input table. +--- @param key any key to search for. +--- @return boolean whatever true if key was found in the table, false otherwise. +bp3d.util.table.containsKey = function(src, key) end + +--- Protects the given input table. A protected table is a read-only table where writing would result into a runtime +--- error. +--- +--- @param src table input table. +--- @return table +bp3d.util.table.protect = function(src) end + +--- Checks if the given sub string `needle` can be found in `src`. +--- +--- Note: This function wil error if any of the inputs are not UTF-8 encoded. +--- +--- @param src string the source string. +--- @param needle string the string to search for. +--- @return boolean whatever true if `needle` was found in `src`, false otherwise. +bp3d.util.utf8.contains = function(src, needle) end + +--- Splits the given string `src` using the separator `pattern`. +--- +--- Note: This function wil error if any of the inputs are not UTF-8 encoded. +--- +--- @param src string input source string. +--- @param pattern string separator string. +--- @return string[] +bp3d.util.utf8.split = function(src, pattern) end + +--- Replace all instances of `pattern` in the given string `src` by the replacement string `replacement`. +--- +--- Note: This function wil error if any of the inputs are not UTF-8 encoded. +--- +--- @param src string input source string. +--- @param pattern string search string. +--- @param replacement string replacement string +bp3d.util.utf8.replace = function(src, pattern, replacement) end + +--- Count the number of unicode characters in the source string. +--- +--- Note: This function wil error if any of the inputs are not UTF-8 encoded. This function does not handle unicode +--- ligatures. +--- +--- @param src string input source string. +--- @return integer +bp3d.util.utf8.count = function(src) end + +--- Extract a unicode character from the given source string. +--- +--- Note: This function wil error if any of the inputs are not UTF-8 encoded. This function does not handle unicode +--- ligatures. +--- +--- @param src string input source string. +--- @param pos integer character index. +--- @return integer +bp3d.util.utf8.charAt = function(src, pos) end + +--- Checks if the given input string is valid UTF-8. +--- +--- @param src string input source string. +--- @return string? whatever nil if the input string contains invalud UTF-8 codes, otherwise returns `src`. +bp3d.util.utf8.fromString = function(src) end + +--- Converts the input string to a valid UTF-8 string by replacing all invalid UTF-8 codes by U+FFFD. +--- +--- @param src string input source string. +--- @return string +bp3d.util.utf8.fromStringLossy = function(src) end + +--- Capitalises the given string. +--- +--- Note: This function wil error if any of the inputs are not UTF-8 encoded. +--- +--- @param src string the string to capitalise. +--- @return string +bp3d.util.utf8.capitalise = function(src) end + +--- De-capitalises the given string. +--- +--- Note: This function wil error if any of the inputs are not UTF-8 encoded. +--- +--- @param src string the string to decapitalise. +--- @return string +bp3d.util.utf8.decapitalise = function(src) end + +--- Change case of all characters in the given input string to upper. +--- +--- Note: This function wil error if any of the inputs are not UTF-8 encoded. +--- +--- @param src string the input string. +--- @return string +bp3d.util.utf8.upper = function(src) end + +--- Change case of all characters in the given input string to lower. +--- +--- Note: This function wil error if any of the inputs are not UTF-8 encoded. +--- +--- @param src string the input string. +--- @return string +bp3d.util.utf8.lower = function(src) end + +--- Extracts a sub-string from the given input source string. +--- +--- Note: This function wil error if `src` is not UTF-8 encoded. This function will never return a broken UTF-8 string. +--- In case `start` points inside a multi-byte UTF-8 sequence, the start position is advanced to the next valid UTF-8 +--- sequence. In case `end1` points inside a multi-byte UTF-8 sequence, the end position is moved backwards to the +--- previous valid UTF-8 sequence. +--- +--- @param src string the input string. +--- @param start integer start position in the input string. +--- @param end1 integer end position in the input string. +--- @return string +bp3d.util.utf8.sub = function(src, start, end1) end + +--- @class bp3d.util.num +--- @field INT53_MIN number -2^52 +--- @field INT53_MAX number 2^52-1 +--- @field UINT53_MIN number 0 +--- @field UINT53_MAX number 2^53-1 +--- @field INT64_MIN number -2^63; this is actually not a number but a special LuaJIT cdata type +--- @field INT64_MAX number 2^63-1; this is actually not a number but a special LuaJIT cdata type +--- @field UINT64_MIN number 0; this is actually not a number but a special LuaJIT cdata type +--- @field UINT64_MAX number 2^64-1; this is actually not a number but a special LuaJIT cdata type +--- @field NAN number nan +--- @field EPSILON number represents the maximum possible precision of a Lua floating-point + +--- Converts the input value to an integer string. +--- +--- @param value any +--- @return string +bp3d.util.num.toistring = function(value) end + +--- Converts the input value to an unsigned integer string. +--- +--- @param value any +--- @return string +bp3d.util.num.toustring = function(value) end + +--- Tests equality between two floating-point numbers. +--- +--- @param a number first operand +--- @param b number second operand +--- @param epsilon number maximum difference between a and b +--- @return boolean +bp3d.util.num.eq = function(a, b, epsilon) end + +--- Parse a Lua floating-point number from the input string. +--- +--- If the number was successfully parsed, returns the number and nil, +--- otherwise returns nil and a string error message. +--- +--- @param str string +--- @return [number?, string?] +bp3d.util.num.parsenumber = function(str) end + +--- Parse a LuaJIT true 64 bits integer from the input string. +--- +--- If the number was successfully parsed, returns the integer and nil, +--- otherwise returns nil and a string error message. +--- +--- @param str string +--- @return [number?, string?] +bp3d.util.num.parseint64 = function(str) end + +--- Parse a LuaJIT true 64 bits unsigned integer from the input string. +--- +--- If the number was successfully parsed, returns the integer and nil, +--- otherwise returns nil and a string error message. +--- +--- @param str string +--- @return [number?, string?] +bp3d.util.num.parseuint64 = function(str) end diff --git a/patch/disable_lua_load.patch b/patch/disable_lua_load.patch new file mode 100644 index 0000000..73d1598 --- /dev/null +++ b/patch/disable_lua_load.patch @@ -0,0 +1,108 @@ +diff --git a/src/lib_base.c b/src/lib_base.c +index 5d1b88a9..f8419349 100644 +--- a/src/lib_base.c ++++ b/src/lib_base.c +@@ -361,103 +361,6 @@ LJLIB_ASM_(xpcall) LJLIB_REC(.) + + /* -- Base library: load Lua code ----------------------------------------- */ + +-static int load_aux(lua_State *L, int status, int envarg) +-{ +- if (status == LUA_OK) { +- /* +- ** Set environment table for top-level function. +- ** Don't do this for non-native bytecode, which returns a prototype. +- */ +- if (tvistab(L->base+envarg-1) && tvisfunc(L->top-1)) { +- GCfunc *fn = funcV(L->top-1); +- GCtab *t = tabV(L->base+envarg-1); +- setgcref(fn->c.env, obj2gco(t)); +- lj_gc_objbarrier(L, fn, t); +- } +- return 1; +- } else { +- setnilV(L->top-2); +- return 2; +- } +-} +- +-LJLIB_CF(loadfile) +-{ +- GCstr *fname = lj_lib_optstr(L, 1); +- GCstr *mode = lj_lib_optstr(L, 2); +- int status; +- lua_settop(L, 3); /* Ensure env arg exists. */ +- status = luaL_loadfilex(L, fname ? strdata(fname) : NULL, +- mode ? strdata(mode) : NULL); +- return load_aux(L, status, 3); +-} +- +-static const char *reader_func(lua_State *L, void *ud, size_t *size) +-{ +- UNUSED(ud); +- luaL_checkstack(L, 2, "too many nested functions"); +- copyTV(L, L->top++, L->base); +- lua_call(L, 0, 1); /* Call user-supplied function. */ +- L->top--; +- if (tvisnil(L->top)) { +- *size = 0; +- return NULL; +- } else if (tvisstr(L->top) || tvisnumber(L->top)) { +- copyTV(L, L->base+4, L->top); /* Anchor string in reserved stack slot. */ +- return lua_tolstring(L, 5, size); +- } else { +- lj_err_caller(L, LJ_ERR_RDRSTR); +- return NULL; +- } +-} +- +-LJLIB_CF(load) +-{ +- GCstr *name = lj_lib_optstr(L, 2); +- GCstr *mode = lj_lib_optstr(L, 3); +- int status; +- if (L->base < L->top && +- (tvisstr(L->base) || tvisnumber(L->base) || tvisbuf(L->base))) { +- const char *s; +- MSize len; +- if (tvisbuf(L->base)) { +- SBufExt *sbx = bufV(L->base); +- s = sbx->r; +- len = sbufxlen(sbx); +- if (!name) name = &G(L)->strempty; /* Buffers are not NUL-terminated. */ +- } else { +- GCstr *str = lj_lib_checkstr(L, 1); +- s = strdata(str); +- len = str->len; +- } +- lua_settop(L, 4); /* Ensure env arg exists. */ +- status = luaL_loadbufferx(L, s, len, name ? strdata(name) : s, +- mode ? strdata(mode) : NULL); +- } else { +- lj_lib_checkfunc(L, 1); +- lua_settop(L, 5); /* Reserve a slot for the string from the reader. */ +- status = lua_loadx(L, reader_func, NULL, name ? strdata(name) : "=(load)", +- mode ? strdata(mode) : NULL); +- } +- return load_aux(L, status, 4); +-} +- +-LJLIB_CF(loadstring) +-{ +- return lj_cf_load(L); +-} +- +-LJLIB_CF(dofile) +-{ +- GCstr *fname = lj_lib_optstr(L, 1); +- setnilV(L->top); +- L->top = L->base+1; +- if (luaL_loadfile(L, fname ? strdata(fname) : NULL) != LUA_OK) +- lua_error(L); +- lua_call(L, 0, LUA_MULTRET); +- return (int)(L->top - L->base) - 1; +-} +- + /* -- Base library: GC control -------------------------------------------- */ + + LJLIB_CF(gcinfo) diff --git a/patch/lib_init.patch b/patch/lib_init.patch new file mode 100644 index 0000000..3bed121 --- /dev/null +++ b/patch/lib_init.patch @@ -0,0 +1,43 @@ +diff --git a/src/lib_init.c b/src/lib_init.c +index 01cecf2f..8ed34a94 100644 +--- a/src/lib_init.c ++++ b/src/lib_init.c +@@ -17,25 +17,14 @@ + + static const luaL_Reg lj_lib_load[] = { + { "", luaopen_base }, +- { LUA_LOADLIBNAME, luaopen_package }, + { LUA_TABLIBNAME, luaopen_table }, +- { LUA_IOLIBNAME, luaopen_io }, +- { LUA_OSLIBNAME, luaopen_os }, + { LUA_STRLIBNAME, luaopen_string }, + { LUA_MATHLIBNAME, luaopen_math }, +- { LUA_DBLIBNAME, luaopen_debug }, + { LUA_BITLIBNAME, luaopen_bit }, + { LUA_JITLIBNAME, luaopen_jit }, + { NULL, NULL } + }; + +-static const luaL_Reg lj_lib_preload[] = { +-#if LJ_HASFFI +- { LUA_FFILIBNAME, luaopen_ffi }, +-#endif +- { NULL, NULL } +-}; +- + LUALIB_API void luaL_openlibs(lua_State *L) + { + const luaL_Reg *lib; +@@ -44,12 +33,5 @@ LUALIB_API void luaL_openlibs(lua_State *L) + lua_pushstring(L, lib->name); + lua_call(L, 1, 0); + } +- luaL_findtable(L, LUA_REGISTRYINDEX, "_PRELOAD", +- sizeof(lj_lib_preload)/sizeof(lj_lib_preload[0])-1); +- for (lib = lj_lib_preload; lib->func; lib++) { +- lua_pushcfunction(L, lib->func); +- lua_setfield(L, -2, lib->name); +- } +- lua_pop(L, 1); + } + diff --git a/patch/lj_disable_jit.patch b/patch/lj_disable_jit.patch new file mode 100644 index 0000000..27a4ef9 --- /dev/null +++ b/patch/lj_disable_jit.patch @@ -0,0 +1,659 @@ +diff --git a/src/lib_jit.c b/src/lib_jit.c +index fd8e585b..882f4425 100644 +--- a/src/lib_jit.c ++++ b/src/lib_jit.c +@@ -43,19 +43,15 @@ + static int setjitmode(lua_State *L, int mode) + { + int idx = 0; +- if (L->base == L->top || tvisnil(L->base)) { /* jit.on/off/flush([nil]) */ +- mode |= LUAJIT_MODE_ENGINE; +- } else { +- /* jit.on/off/flush(func|proto, nil|true|false) */ +- if (tvisfunc(L->base) || tvisproto(L->base)) +- idx = 1; +- else if (!tvistrue(L->base)) /* jit.on/off/flush(true, nil|true|false) */ +- goto err; +- if (L->base+1 < L->top && tvisbool(L->base+1)) +- mode |= boolV(L->base+1) ? LUAJIT_MODE_ALLFUNC : LUAJIT_MODE_ALLSUBFUNC; +- else +- mode |= LUAJIT_MODE_FUNC; +- } ++ /* jit.on/off/flush(func|proto, nil|true|false) */ ++ if (tvisfunc(L->base) || tvisproto(L->base)) ++ idx = 1; ++ else if (!tvistrue(L->base)) /* jit.on/off/flush(true, nil|true|false) */ ++ goto err; ++ if (L->base+1 < L->top && tvisbool(L->base+1)) ++ mode |= boolV(L->base+1) ? LUAJIT_MODE_ALLFUNC : LUAJIT_MODE_ALLSUBFUNC; ++ else ++ mode |= LUAJIT_MODE_FUNC; + if (luaJIT_setmode(L, idx, mode) != 1) { + if ((mode & LUAJIT_MODE_MASK) == LUAJIT_MODE_ENGINE) + lj_err_caller(L, LJ_ERR_NOJIT); +@@ -67,16 +63,25 @@ static int setjitmode(lua_State *L, int mode) + + LJLIB_CF(jit_on) + { ++ if (L->base == L->top || tvisnil(L->base)) { ++ luaL_error(L, "attempt to apply global jit state, this is forbidden by sandbox"); ++ } + return setjitmode(L, LUAJIT_MODE_ON); + } + + LJLIB_CF(jit_off) + { ++ if (L->base == L->top || tvisnil(L->base)) { ++ luaL_error(L, "attempt to apply global jit state, this is forbidden by sandbox"); ++ } + return setjitmode(L, LUAJIT_MODE_OFF); + } + + LJLIB_CF(jit_flush) + { ++ if (L->base == L->top || tvisnil(L->base)) { ++ luaL_error(L, "attempt to apply global jit state, this is forbidden by sandbox"); ++ } + #if LJ_HASJIT + if (L->base < L->top && tvisnumber(L->base)) { + int traceno = lj_lib_checkint(L, 1); +@@ -87,535 +92,29 @@ LJLIB_CF(jit_flush) + return setjitmode(L, LUAJIT_MODE_FLUSH); + } + +-#if LJ_HASJIT +-/* Push a string for every flag bit that is set. */ +-static void flagbits_to_strings(lua_State *L, uint32_t flags, uint32_t base, +- const char *str) +-{ +- for (; *str; base <<= 1, str += 1+*str) +- if (flags & base) +- setstrV(L, L->top++, lj_str_new(L, str+1, *(uint8_t *)str)); +-} +-#endif +- +-LJLIB_CF(jit_status) +-{ +-#if LJ_HASJIT +- jit_State *J = L2J(L); +- L->top = L->base; +- setboolV(L->top++, (J->flags & JIT_F_ON) ? 1 : 0); +- flagbits_to_strings(L, J->flags, JIT_F_CPU, JIT_F_CPUSTRING); +- flagbits_to_strings(L, J->flags, JIT_F_OPT, JIT_F_OPTSTRING); +- return (int)(L->top - L->base); +-#else +- setboolV(L->top++, 0); +- return 1; +-#endif +-} +- +-LJLIB_CF(jit_security) +-{ +- int idx = lj_lib_checkopt(L, 1, -1, LJ_SECURITY_MODESTRING); +- setintV(L->top++, ((LJ_SECURITY_MODE >> (2*idx)) & 3)); +- return 1; +-} +- +-LJLIB_CF(jit_attach) +-{ +-#ifdef LUAJIT_DISABLE_VMEVENT +- luaL_error(L, "vmevent API disabled"); +-#else +- GCfunc *fn = lj_lib_checkfunc(L, 1); +- GCstr *s = lj_lib_optstr(L, 2); +- luaL_findtable(L, LUA_REGISTRYINDEX, LJ_VMEVENTS_REGKEY, LJ_VMEVENTS_HSIZE); +- if (s) { /* Attach to given event. */ +- const uint8_t *p = (const uint8_t *)strdata(s); +- uint32_t h = s->len; +- while (*p) h = h ^ (lj_rol(h, 6) + *p++); +- lua_pushvalue(L, 1); +- lua_rawseti(L, -2, VMEVENT_HASHIDX(h)); +- G(L)->vmevmask = VMEVENT_NOCACHE; /* Invalidate cache. */ +- } else { /* Detach if no event given. */ +- setnilV(L->top++); +- while (lua_next(L, -2)) { +- L->top--; +- if (tvisfunc(L->top) && funcV(L->top) == fn) { +- setnilV(lj_tab_set(L, tabV(L->top-2), L->top-1)); +- } +- } +- } +-#endif +- return 0; +-} +- +-LJLIB_PUSH(top-5) LJLIB_SET(os) +-LJLIB_PUSH(top-4) LJLIB_SET(arch) +-LJLIB_PUSH(top-3) LJLIB_SET(version_num) ++LJLIB_PUSH(top-4) LJLIB_SET(os) ++LJLIB_PUSH(top-3) LJLIB_SET(arch) + LJLIB_PUSH(top-2) LJLIB_SET(version) + + #include "lj_libdef.h" + +-/* -- jit.util.* functions ------------------------------------------------ */ +- +-#define LJLIB_MODULE_jit_util +- +-/* -- Reflection API for Lua functions ------------------------------------ */ +- +-static void setintfield(lua_State *L, GCtab *t, const char *name, int32_t val) +-{ +- setintV(lj_tab_setstr(L, t, lj_str_newz(L, name)), val); +-} +- +-/* local info = jit.util.funcinfo(func [,pc]) */ +-LJLIB_CF(jit_util_funcinfo) +-{ +- GCproto *pt = lj_lib_checkLproto(L, 1, 1); +- if (pt) { +- BCPos pc = (BCPos)lj_lib_optint(L, 2, 0); +- GCtab *t; +- lua_createtable(L, 0, 16); /* Increment hash size if fields are added. */ +- t = tabV(L->top-1); +- setintfield(L, t, "linedefined", pt->firstline); +- setintfield(L, t, "lastlinedefined", pt->firstline + pt->numline); +- setintfield(L, t, "stackslots", pt->framesize); +- setintfield(L, t, "params", pt->numparams); +- setintfield(L, t, "bytecodes", (int32_t)pt->sizebc); +- setintfield(L, t, "gcconsts", (int32_t)pt->sizekgc); +- setintfield(L, t, "nconsts", (int32_t)pt->sizekn); +- setintfield(L, t, "upvalues", (int32_t)pt->sizeuv); +- if (pc < pt->sizebc) +- setintfield(L, t, "currentline", lj_debug_line(pt, pc)); +- lua_pushboolean(L, (pt->flags & PROTO_VARARG)); +- lua_setfield(L, -2, "isvararg"); +- lua_pushboolean(L, (pt->flags & PROTO_CHILD)); +- lua_setfield(L, -2, "children"); +- setstrV(L, L->top++, proto_chunkname(pt)); +- lua_setfield(L, -2, "source"); +- lj_debug_pushloc(L, pt, pc); +- lua_setfield(L, -2, "loc"); +- setprotoV(L, lj_tab_setstr(L, t, lj_str_newlit(L, "proto")), pt); +- } else { +- GCfunc *fn = funcV(L->base); +- GCtab *t; +- lua_createtable(L, 0, 4); /* Increment hash size if fields are added. */ +- t = tabV(L->top-1); +- if (!iscfunc(fn)) +- setintfield(L, t, "ffid", fn->c.ffid); +- setintptrV(lj_tab_setstr(L, t, lj_str_newlit(L, "addr")), +- (intptr_t)(void *)fn->c.f); +- setintfield(L, t, "upvalues", fn->c.nupvalues); +- } +- return 1; +-} ++/* -- JIT compiler configuration ----------------------------------------- */ + +-/* local ins, m = jit.util.funcbc(func, pc) */ +-LJLIB_CF(jit_util_funcbc) +-{ +- GCproto *pt = lj_lib_checkLproto(L, 1, 0); +- BCPos pc = (BCPos)lj_lib_checkint(L, 2); +- if (pc < pt->sizebc) { +- BCIns ins = proto_bc(pt)[pc]; +- BCOp op = bc_op(ins); +- lj_assertL(op < BC__MAX, "bad bytecode op %d", op); +- setintV(L->top, ins); +- setintV(L->top+1, lj_bc_mode[op]); +- L->top += 2; +- return 2; +- } +- return 0; ++LUA_API uint32_t lua_ext_getjitflags(lua_State *L) { ++ return L2J(L)->flags; + } + +-/* local k = jit.util.funck(func, idx) */ +-LJLIB_CF(jit_util_funck) +-{ +- GCproto *pt = lj_lib_checkLproto(L, 1, 0); +- ptrdiff_t idx = (ptrdiff_t)lj_lib_checkint(L, 2); +- if (idx >= 0) { +- if (idx < (ptrdiff_t)pt->sizekn) { +- copyTV(L, L->top-1, proto_knumtv(pt, idx)); +- return 1; ++LUA_API int lua_ext_setjitflags(lua_State *L, uint32_t flags) { ++ jit_State *J = L2J(L); ++ uint32_t current = J->flags; ++ if ((current & JIT_F_ON) != (flags & JIT_F_ON)) { ++ // we have attempted to change the global JIT mode outside of the setjitmode ext function, error. ++ return -1; + } +- } else { +- if (~idx < (ptrdiff_t)pt->sizekgc) { +- GCobj *gc = proto_kgc(pt, idx); +- setgcV(L, L->top-1, gc, ~gc->gch.gct); +- return 1; +- } +- } +- return 0; +-} +- +-/* local name = jit.util.funcuvname(func, idx) */ +-LJLIB_CF(jit_util_funcuvname) +-{ +- GCproto *pt = lj_lib_checkLproto(L, 1, 0); +- uint32_t idx = (uint32_t)lj_lib_checkint(L, 2); +- if (idx < pt->sizeuv) { +- setstrV(L, L->top-1, lj_str_newz(L, lj_debug_uvname(pt, idx))); +- return 1; +- } +- return 0; +-} +- +-/* -- Reflection API for traces ------------------------------------------- */ +- +-#if LJ_HASJIT +- +-/* Check trace argument. Must not throw for non-existent trace numbers. */ +-static GCtrace *jit_checktrace(lua_State *L) +-{ +- TraceNo tr = (TraceNo)lj_lib_checkint(L, 1); +- jit_State *J = L2J(L); +- if (tr > 0 && tr < J->sizetrace) +- return traceref(J, tr); +- return NULL; +-} +- +-/* Names of link types. ORDER LJ_TRLINK */ +-static const char *const jit_trlinkname[] = { +- "none", "root", "loop", "tail-recursion", "up-recursion", "down-recursion", +- "interpreter", "return", "stitch" +-}; +- +-/* local info = jit.util.traceinfo(tr) */ +-LJLIB_CF(jit_util_traceinfo) +-{ +- GCtrace *T = jit_checktrace(L); +- if (T) { +- GCtab *t; +- lua_createtable(L, 0, 8); /* Increment hash size if fields are added. */ +- t = tabV(L->top-1); +- setintfield(L, t, "nins", (int32_t)T->nins - REF_BIAS - 1); +- setintfield(L, t, "nk", REF_BIAS - (int32_t)T->nk); +- setintfield(L, t, "link", T->link); +- setintfield(L, t, "nexit", T->nsnap); +- setstrV(L, L->top++, lj_str_newz(L, jit_trlinkname[T->linktype])); +- lua_setfield(L, -2, "linktype"); +- /* There are many more fields. Add them only when needed. */ +- return 1; +- } +- return 0; +-} +- +-/* local m, ot, op1, op2, prev = jit.util.traceir(tr, idx) */ +-LJLIB_CF(jit_util_traceir) +-{ +- GCtrace *T = jit_checktrace(L); +- IRRef ref = (IRRef)lj_lib_checkint(L, 2) + REF_BIAS; +- if (T && ref >= REF_BIAS && ref < T->nins) { +- IRIns *ir = &T->ir[ref]; +- int32_t m = lj_ir_mode[ir->o]; +- setintV(L->top-2, m); +- setintV(L->top-1, ir->ot); +- setintV(L->top++, (int32_t)ir->op1 - (irm_op1(m)==IRMref ? REF_BIAS : 0)); +- setintV(L->top++, (int32_t)ir->op2 - (irm_op2(m)==IRMref ? REF_BIAS : 0)); +- setintV(L->top++, ir->prev); +- return 5; +- } +- return 0; +-} +- +-/* local k, t [, slot] = jit.util.tracek(tr, idx) */ +-LJLIB_CF(jit_util_tracek) +-{ +- GCtrace *T = jit_checktrace(L); +- IRRef ref = (IRRef)lj_lib_checkint(L, 2) + REF_BIAS; +- if (T && ref >= T->nk && ref < REF_BIAS) { +- IRIns *ir = &T->ir[ref]; +- int32_t slot = -1; +- if (ir->o == IR_KSLOT) { +- slot = ir->op2; +- ir = &T->ir[ir->op1]; +- } +-#if LJ_HASFFI +- if (ir->o == IR_KINT64) ctype_loadffi(L); +-#endif +- lj_ir_kvalue(L, L->top-2, ir); +- setintV(L->top-1, (int32_t)irt_type(ir->t)); +- if (slot == -1) +- return 2; +- setintV(L->top++, slot); +- return 3; +- } +- return 0; +-} +- +-/* local snap = jit.util.tracesnap(tr, sn) */ +-LJLIB_CF(jit_util_tracesnap) +-{ +- GCtrace *T = jit_checktrace(L); +- SnapNo sn = (SnapNo)lj_lib_checkint(L, 2); +- if (T && sn < T->nsnap) { +- SnapShot *snap = &T->snap[sn]; +- SnapEntry *map = &T->snapmap[snap->mapofs]; +- MSize n, nent = snap->nent; +- GCtab *t; +- lua_createtable(L, nent+2, 0); +- t = tabV(L->top-1); +- setintV(lj_tab_setint(L, t, 0), (int32_t)snap->ref - REF_BIAS); +- setintV(lj_tab_setint(L, t, 1), (int32_t)snap->nslots); +- for (n = 0; n < nent; n++) +- setintV(lj_tab_setint(L, t, (int32_t)(n+2)), (int32_t)map[n]); +- setintV(lj_tab_setint(L, t, (int32_t)(nent+2)), (int32_t)SNAP(255, 0, 0)); +- return 1; +- } +- return 0; +-} +- +-/* local mcode, addr, loop = jit.util.tracemc(tr) */ +-LJLIB_CF(jit_util_tracemc) +-{ +- GCtrace *T = jit_checktrace(L); +- if (T && T->mcode != NULL) { +- setstrV(L, L->top-1, lj_str_new(L, (const char *)T->mcode, T->szmcode)); +- setintptrV(L->top++, (intptr_t)(void *)T->mcode); +- setintV(L->top++, T->mcloop); +- return 3; +- } +- return 0; +-} +- +-/* local addr = jit.util.traceexitstub([tr,] exitno) */ +-LJLIB_CF(jit_util_traceexitstub) +-{ +-#ifdef EXITSTUBS_PER_GROUP +- ExitNo exitno = (ExitNo)lj_lib_checkint(L, 1); +- jit_State *J = L2J(L); +- if (exitno < EXITSTUBS_PER_GROUP*LJ_MAX_EXITSTUBGR) { +- setintptrV(L->top-1, (intptr_t)(void *)exitstub_addr(J, exitno)); +- return 1; +- } +-#else +- if (L->top > L->base+1) { /* Don't throw for one-argument variant. */ +- GCtrace *T = jit_checktrace(L); +- ExitNo exitno = (ExitNo)lj_lib_checkint(L, 2); +- ExitNo maxexit = T->root ? T->nsnap+1 : T->nsnap; +- if (T && T->mcode != NULL && exitno < maxexit) { +- setintptrV(L->top-1, (intptr_t)(void *)exitstub_trace_addr(T, exitno)); +- return 1; +- } +- } +-#endif +- return 0; +-} +- +-/* local addr = jit.util.ircalladdr(idx) */ +-LJLIB_CF(jit_util_ircalladdr) +-{ +- uint32_t idx = (uint32_t)lj_lib_checkint(L, 1); +- if (idx < IRCALL__MAX) { +- ASMFunction func = lj_ir_callinfo[idx].func; +- setintptrV(L->top-1, (intptr_t)(void *)lj_ptr_strip(func)); +- return 1; +- } +- return 0; +-} +- +-#endif +- +-#include "lj_libdef.h" +- +-static int luaopen_jit_util(lua_State *L) +-{ +- LJ_LIB_REG(L, NULL, jit_util); +- return 1; +-} +- +-/* -- jit.opt module ------------------------------------------------------ */ +- +-#if LJ_HASJIT +- +-#define LJLIB_MODULE_jit_opt +- +-/* Parse optimization level. */ +-static int jitopt_level(jit_State *J, const char *str) +-{ +- if (str[0] >= '0' && str[0] <= '9' && str[1] == '\0') { +- uint32_t flags; +- if (str[0] == '0') flags = JIT_F_OPT_0; +- else if (str[0] == '1') flags = JIT_F_OPT_1; +- else if (str[0] == '2') flags = JIT_F_OPT_2; +- else flags = JIT_F_OPT_3; +- J->flags = (J->flags & ~JIT_F_OPT_MASK) | flags; +- return 1; /* Ok. */ +- } +- return 0; /* No match. */ +-} +- +-/* Parse optimization flag. */ +-static int jitopt_flag(jit_State *J, const char *str) +-{ +- const char *lst = JIT_F_OPTSTRING; +- uint32_t opt; +- int set = 1; +- if (str[0] == '+') { +- str++; +- } else if (str[0] == '-') { +- str++; +- set = 0; +- } else if (str[0] == 'n' && str[1] == 'o') { +- str += str[2] == '-' ? 3 : 2; +- set = 0; +- } +- for (opt = JIT_F_OPT; ; opt <<= 1) { +- size_t len = *(const uint8_t *)lst; +- if (len == 0) +- break; +- if (strncmp(str, lst+1, len) == 0 && str[len] == '\0') { +- if (set) J->flags |= opt; else J->flags &= ~opt; +- return 1; /* Ok. */ +- } +- lst += 1+len; +- } +- return 0; /* No match. */ +-} +- +-/* Parse optimization parameter. */ +-static int jitopt_param(jit_State *J, const char *str) +-{ +- const char *lst = JIT_P_STRING; +- int i; +- for (i = 0; i < JIT_P__MAX; i++) { +- size_t len = *(const uint8_t *)lst; +- lj_assertJ(len != 0, "bad JIT_P_STRING"); +- if (strncmp(str, lst+1, len) == 0 && str[len] == '=') { +- int32_t n = 0; +- const char *p = &str[len+1]; +- while (*p >= '0' && *p <= '9') +- n = n*10 + (*p++ - '0'); +- if (*p) return 0; /* Malformed number. */ +- J->param[i] = n; +- if (i == JIT_P_hotloop) +- lj_dispatch_init_hotcount(J2G(J)); +- return 1; /* Ok. */ +- } +- lst += 1+len; +- } +- return 0; /* No match. */ +-} +- +-/* jit.opt.start(flags...) */ +-LJLIB_CF(jit_opt_start) +-{ +- jit_State *J = L2J(L); +- int nargs = (int)(L->top - L->base); +- if (nargs == 0) { +- J->flags = (J->flags & ~JIT_F_OPT_MASK) | JIT_F_OPT_DEFAULT; +- } else { +- int i; +- for (i = 1; i <= nargs; i++) { +- const char *str = strdata(lj_lib_checkstr(L, i)); +- if (!jitopt_level(J, str) && +- !jitopt_flag(J, str) && +- !jitopt_param(J, str)) +- lj_err_callerv(L, LJ_ERR_JITOPT, str); +- } +- } +- return 0; +-} +- +-#include "lj_libdef.h" +- +-#endif +- +-/* -- jit.profile module -------------------------------------------------- */ +- +-#if LJ_HASPROFILE +- +-#define LJLIB_MODULE_jit_profile +- +-/* Not loaded by default, use: local profile = require("jit.profile") */ +- +-#define KEY_PROFILE_THREAD (U64x(80000000,00000000)|'t') +-#define KEY_PROFILE_FUNC (U64x(80000000,00000000)|'f') +- +-static void jit_profile_callback(lua_State *L2, lua_State *L, int samples, +- int vmstate) +-{ +- TValue key; +- cTValue *tv; +- key.u64 = KEY_PROFILE_FUNC; +- tv = lj_tab_get(L, tabV(registry(L)), &key); +- if (tvisfunc(tv)) { +- char vmst = (char)vmstate; +- int status; +- setfuncV(L2, L2->top++, funcV(tv)); +- setthreadV(L2, L2->top++, L); +- setintV(L2->top++, samples); +- setstrV(L2, L2->top++, lj_str_new(L2, &vmst, 1)); +- status = lua_pcall(L2, 3, 0, 0); /* callback(thread, samples, vmstate) */ +- if (status) { +- if (G(L2)->panic) G(L2)->panic(L2); +- exit(EXIT_FAILURE); +- } +- lj_trace_abort(G(L2)); +- } +-} +- +-/* profile.start(mode, cb) */ +-LJLIB_CF(jit_profile_start) +-{ +- GCtab *registry = tabV(registry(L)); +- GCstr *mode = lj_lib_optstr(L, 1); +- GCfunc *func = lj_lib_checkfunc(L, 2); +- lua_State *L2 = lua_newthread(L); /* Thread that runs profiler callback. */ +- TValue key; +- /* Anchor thread and function in registry. */ +- key.u64 = KEY_PROFILE_THREAD; +- setthreadV(L, lj_tab_set(L, registry, &key), L2); +- key.u64 = KEY_PROFILE_FUNC; +- setfuncV(L, lj_tab_set(L, registry, &key), func); +- lj_gc_anybarriert(L, registry); +- luaJIT_profile_start(L, mode ? strdata(mode) : "", +- (luaJIT_profile_callback)jit_profile_callback, L2); +- return 0; +-} +- +-/* profile.stop() */ +-LJLIB_CF(jit_profile_stop) +-{ +- GCtab *registry; +- TValue key; +- luaJIT_profile_stop(L); +- registry = tabV(registry(L)); +- key.u64 = KEY_PROFILE_THREAD; +- setnilV(lj_tab_set(L, registry, &key)); +- key.u64 = KEY_PROFILE_FUNC; +- setnilV(lj_tab_set(L, registry, &key)); +- lj_gc_anybarriert(L, registry); +- return 0; +-} +- +-/* dump = profile.dumpstack([thread,] fmt, depth) */ +-LJLIB_CF(jit_profile_dumpstack) +-{ +- lua_State *L2 = L; +- int arg = 0; +- size_t len; +- int depth; +- GCstr *fmt; +- const char *p; +- if (L->top > L->base && tvisthread(L->base)) { +- L2 = threadV(L->base); +- arg = 1; +- } +- fmt = lj_lib_checkstr(L, arg+1); +- depth = lj_lib_checkint(L, arg+2); +- p = luaJIT_profile_dumpstack(L2, strdata(fmt), depth, &len); +- lua_pushlstring(L, p, len); +- return 1; +-} +- +-#include "lj_libdef.h" +- +-static int luaopen_jit_profile(lua_State *L) +-{ +- LJ_LIB_REG(L, NULL, jit_profile); +- return 1; ++ J->flags = flags; ++ return 0; + } + +-#endif +- + /* -- JIT compiler initialization ----------------------------------------- */ + + #if LJ_HASJIT +@@ -725,20 +224,9 @@ LUALIB_API int luaopen_jit(lua_State *L) + #endif + lua_pushliteral(L, LJ_OS_NAME); + lua_pushliteral(L, LJ_ARCH_NAME); +- lua_pushinteger(L, LUAJIT_VERSION_NUM); /* Deprecated. */ + lua_pushliteral(L, LUAJIT_VERSION); + LJ_LIB_REG(L, LUA_JITLIBNAME, jit); +-#if LJ_HASPROFILE +- lj_lib_prereg(L, LUA_JITLIBNAME ".profile", luaopen_jit_profile, +- tabref(L->env)); +-#endif +-#ifndef LUAJIT_DISABLE_JITUTIL +- lj_lib_prereg(L, LUA_JITLIBNAME ".util", luaopen_jit_util, tabref(L->env)); +-#endif +-#if LJ_HASJIT +- LJ_LIB_REG(L, "jit.opt", jit_opt); +-#endif +- L->top -= 2; ++ L->top -= 1; + return 1; + } + +diff --git a/src/lj_dispatch.c b/src/lj_dispatch.c +index 78608316..94f0077e 100644 +--- a/src/lj_dispatch.c ++++ b/src/lj_dispatch.c +@@ -241,6 +241,24 @@ static void setptmode_all(global_State *g, GCproto *pt, int mode) + } + #endif + ++LUA_API int lua_ext_setjitmode(lua_State *L, int mode) { ++ global_State *g = G(L); ++ lj_trace_abort(g); /* Abort recording on any state change. */ ++ /* Avoid pulling the rug from under our own feet. */ ++ if ((g->hookmask & HOOK_GC)) ++ return -1; ++ if ((mode & LUAJIT_MODE_FLUSH)) { ++ lj_trace_flushall(L); ++ } else { ++ if (!(mode & LUAJIT_MODE_ON)) ++ G2J(g)->flags &= ~(uint32_t)JIT_F_ON; ++ else ++ G2J(g)->flags |= (uint32_t)JIT_F_ON; ++ lj_dispatch_update(g); ++ } ++ return 0; ++} ++ + /* Public API function: control the JIT engine. */ + int luaJIT_setmode(lua_State *L, int idx, int mode) + { diff --git a/patch/lua_ext.patch b/patch/lua_ext.patch new file mode 100644 index 0000000..8fa4016 --- /dev/null +++ b/patch/lua_ext.patch @@ -0,0 +1,214 @@ +diff --git a/src/lj_api.c b/src/lj_api.c +index e9fc25b4..68996ee0 100644 +--- a/src/lj_api.c ++++ b/src/lj_api.c +@@ -9,6 +9,8 @@ + #define lj_api_c + #define LUA_CORE + ++#include ++ + #include "lj_obj.h" + #include "lj_gc.h" + #include "lj_err.h" +@@ -25,6 +27,8 @@ + #include "lj_vm.h" + #include "lj_strscan.h" + #include "lj_strfmt.h" ++#include "lj_cdata.h" ++#include "lualib.h" + + /* -- Common helper functions --------------------------------------------- */ + +@@ -1317,3 +1321,191 @@ LUA_API void lua_setallocf(lua_State *L, lua_Alloc f, void *ud) + g->allocf = f; + } + ++/*------------*/ ++/* Extensions */ ++/*------------*/ ++ ++LUALIB_API int lua_ext_tab_len(lua_State* L, int idx, MSize* outsize) ++{ ++ TValue* o = index2adr(L, idx); ++ GCtab* tab = tabV(o); ++ if (LJ_LIKELY(tab->hmask == 0)) ++ return lj_tab_len(tab); ++ // Return an error as the table has a hash part. ++ return -1; ++} ++ ++LUALIB_API lua_Number lua_ext_fast_checknumber(lua_State *L, int idx) ++{ ++ cTValue *o = index2adr(L, idx); ++ if (LJ_LIKELY(tvisnumber(o))) ++ return numberVnum(o); ++ else ++ lj_err_argt(L, idx, LUA_TNUMBER); ++ return 0; ++} ++ ++LUALIB_API lua_Integer lua_ext_fast_checkinteger(lua_State *L, int idx) ++{ ++ cTValue *o = index2adr(L, idx); ++ lua_Number n; ++ if (LJ_LIKELY(tvisint(o))) { ++ return intV(o); ++ } else if (LJ_LIKELY(tvisnum(o))) { ++ n = numV(o); ++ } else { ++ lj_err_argt(L, idx, LUA_TNUMBER); ++ } ++#if LJ_64 ++ return (lua_Integer)n; ++#else ++ return lj_num2int(n); ++#endif ++} ++ ++LUALIB_API int lua_ext_fast_checkboolean(lua_State *L, int idx) ++{ ++ cTValue *o = index2adr(L, idx); ++ if (LJ_LIKELY(tvisbool(o))) { ++ return (int)intV(o); ++ } else { ++ lj_err_argt(L, idx, LUA_TNUMBER); ++ } ++} ++ ++#if LJ_64 ++LUALIB_API int64_t lua_ext_checkinteger64(lua_State *L, int idx) { ++ cTValue *o = index2adr(L, idx); ++ if (LJ_LIKELY(tviscdata(o))) { ++ GCcdata *cd = cdataV(o); ++ if (LJ_LIKELY(cd->ctypeid == CTID_INT64)) { ++ return (int64_t)cdataptr(cd); ++ } ++ } ++ return (int64_t)lua_ext_fast_checkinteger(L, idx); ++} ++ ++LUALIB_API uint64_t lua_ext_checkuinteger64(lua_State *L, int idx) { ++ cTValue *o = index2adr(L, idx); ++ if (LJ_LIKELY(tviscdata(o))) { ++ GCcdata *cd = cdataV(o); ++ if (LJ_LIKELY(cd->ctypeid == CTID_UINT64)) { ++ return (int64_t)cdataptr(cd); ++ } ++ } ++ return (uint64_t)lua_ext_fast_checkinteger(L, idx); ++} ++ ++LUALIB_API int lua_ext_getinteger64(lua_State *L, int idx, int64_t *out) { ++ cTValue *o = index2adr(L, idx); ++ if (LJ_LIKELY(tviscdata(o))) { ++ GCcdata *cd = cdataV(o); ++ if (LJ_LIKELY(cd->ctypeid == CTID_INT64)) { ++ *out = *(int64_t*)cdataptr(cd); ++ return 1; ++ } else { ++ return 0; ++ } ++ } else if (LJ_LIKELY(tvisint(o))) { ++ *out = (int64_t)intV(o); ++ return 1; ++ } else if (LJ_LIKELY(tvisnum(o))) { ++ *out = (int64_t)numV(o); ++ return 1; ++ } ++ return 0; ++} ++ ++LUALIB_API int lua_ext_getuinteger64(lua_State *L, int idx, uint64_t *out) { ++ cTValue *o = index2adr(L, idx); ++ if (LJ_LIKELY(tviscdata(o))) { ++ GCcdata *cd = cdataV(o); ++ if (LJ_LIKELY(cd->ctypeid == CTID_UINT64)) { ++ *out = *(uint64_t*)cdataptr(cd); ++ return 1; ++ } else { ++ return 0; ++ } ++ } else if (LJ_LIKELY(tvisint(o))) { ++ *out = (uint64_t)intV(o); ++ return 1; ++ } else if (LJ_LIKELY(tvisnum(o))) { ++ *out = (uint64_t)numV(o); ++ return 1; ++ } ++ return 0; ++} ++ ++LUALIB_API int64_t lua_ext_tointeger64(lua_State *L, int idx) { ++ cTValue *o = index2adr(L, idx); ++ if (LJ_LIKELY(tviscdata(o))) { ++ GCcdata *cd = cdataV(o); ++ if (LJ_LIKELY(cd->ctypeid == CTID_INT64)) { ++ return *(int64_t*)cdataptr(cd); ++ } ++ } ++ return (int64_t)lua_tointeger(L, idx); ++} ++ ++LUALIB_API uint64_t lua_ext_touinteger64(lua_State *L, int idx) { ++ cTValue *o = index2adr(L, idx); ++ if (LJ_LIKELY(tviscdata(o))) { ++ GCcdata *cd = cdataV(o); ++ if (LJ_LIKELY(cd->ctypeid == CTID_UINT64)) { ++ return *(uint64_t*)cdataptr(cd); ++ } ++ } ++ return (uint64_t)lua_tointeger(L, idx); ++} ++ ++LUALIB_API int lua_ext_pushinteger64(lua_State *L, int64_t in) { ++ ctype_loadffi(L); ++ GCcdata *cd = lj_cdata_new_(L, CTID_INT64, 8); ++ *(int64_t*)cdataptr(cd) = in; ++ TValue *o = L->top; ++ setcdataV(L, o, cd); ++ incr_top(L); ++ return 1; ++} ++ ++LUALIB_API int lua_ext_pushuinteger64(lua_State *L, uint64_t in) { ++ ctype_loadffi(L); ++ GCcdata *cd = lj_cdata_new_(L, CTID_UINT64, 8); ++ *(uint64_t*)cdataptr(cd) = in; ++ TValue *o = L->top; ++ setcdataV(L, o, cd); ++ incr_top(L); ++ return 1; ++} ++#endif ++ ++static void* _Atomic BP3D_LUA_EXT_KEYREG = NULL; ++static atomic_uint_fast32_t BP3D_LUA_EXT_KEYREG_REFS = 0; ++ ++LUALIB_API void* lua_ext_keyreg_get() { ++ return (void*)BP3D_LUA_EXT_KEYREG; ++} ++ ++LUALIB_API void* lua_ext_keyreg_ref(void* ptr) { ++ int flag = 0; ++ if (BP3D_LUA_EXT_KEYREG_REFS == 0) { ++ BP3D_LUA_EXT_KEYREG = ptr; ++ flag = 1; ++ } ++ ++BP3D_LUA_EXT_KEYREG_REFS; ++ if (flag == 1) { ++ return NULL; ++ } else { ++ return ptr; ++ } ++} ++ ++LUALIB_API void* lua_ext_keyreg_unref() { ++ unsigned int refs = (unsigned int)--BP3D_LUA_EXT_KEYREG_REFS; ++ if (refs == 0) { ++ void* ptr = (void*)BP3D_LUA_EXT_KEYREG; ++ BP3D_LUA_EXT_KEYREG = 0; ++ return ptr; ++ } ++ return NULL; ++} diff --git a/patch/lua_ext_ccatch_error.patch b/patch/lua_ext_ccatch_error.patch new file mode 100644 index 0000000..b053521 --- /dev/null +++ b/patch/lua_ext_ccatch_error.patch @@ -0,0 +1,118 @@ +diff --git a/src/lj_err.c b/src/lj_err.c +index 03b5030b..8a1377e6 100644 +--- a/src/lj_err.c ++++ b/src/lj_err.c +@@ -175,7 +175,7 @@ static void *err_unwind(lua_State *L, void *stopcf, int errcode) + case FRAME_PCALLH: /* FF pcall() frame inside hook. */ + if (errcode) { + global_State *g; +- if (errcode == LUA_YIELD) { ++ if (errcode == LUA_YIELD || errcode == LUA_ERRCCATCH) { + frame = frame_prevd(frame); + break; + } +@@ -825,7 +825,7 @@ LJ_NOINLINE void lj_err_mem(lua_State *L) + } + + /* Find error function for runtime errors. Requires an extra stack traversal. */ +-static ptrdiff_t finderrfunc(lua_State *L) ++static ptrdiff_t finderrfunc(lua_State *L, int errcode) + { + cTValue *frame = L->base-1, *bot = tvref(L->stack)+LJ_FR2; + void *cf = L->cframe; +@@ -864,6 +864,10 @@ static ptrdiff_t finderrfunc(lua_State *L) + break; + case FRAME_PCALL: + case FRAME_PCALLH: ++ if (errcode == LUA_ERRCCATCH) { ++ frame = frame_prevd(frame); ++ break; ++ } + if (frame_func(frame_prevd(frame))->c.ffid == FF_xpcall) + return savestack(L, frame_prevd(frame)+1); /* xpcall's errorfunc. */ + return 0; +@@ -878,7 +882,7 @@ static ptrdiff_t finderrfunc(lua_State *L) + /* Runtime error. */ + LJ_NOINLINE void LJ_FASTCALL lj_err_run(lua_State *L) + { +- ptrdiff_t ef = (LJ_HASJIT && tvref(G(L)->jit_base)) ? 0 : finderrfunc(L); ++ ptrdiff_t ef = (LJ_HASJIT && tvref(G(L)->jit_base)) ? 0 : finderrfunc(L, LUA_ERRRUN); + if (ef) { + TValue *errfunc, *top; + lj_state_checkstack(L, LUA_MINSTACK * 2); /* Might raise new error. */ +@@ -899,6 +903,29 @@ LJ_NOINLINE void LJ_FASTCALL lj_err_run(lua_State *L) + lj_err_throw(L, LUA_ERRRUN); + } + ++LJ_NOINLINE void LJ_FASTCALL lj_err_ccatch(lua_State *L) ++{ ++ ptrdiff_t ef = (LJ_HASJIT && tvref(G(L)->jit_base)) ? 0 : finderrfunc(L, LUA_ERRCCATCH); ++ if (ef) { ++ TValue *errfunc, *top; ++ lj_state_checkstack(L, LUA_MINSTACK * 2); /* Might raise new error. */ ++ lj_trace_abort(G(L)); ++ errfunc = restorestack(L, ef); ++ top = L->top; ++ if (!tvisfunc(errfunc) || L->status == LUA_ERRERR) { ++ setstrV(L, top-1, lj_err_str(L, LJ_ERR_ERRERR)); ++ lj_err_throw(L, LUA_ERRERR); ++ } ++ L->status = LUA_ERRERR; ++ copyTV(L, top+LJ_FR2, top-1); ++ copyTV(L, top-1, errfunc); ++ if (LJ_FR2) setnilV(top++); ++ L->top = top+1; ++ lj_vm_call(L, top, 1+1); /* Stack: |errfunc|msg| -> |msg| */ ++ } ++ lj_err_throw(L, LUA_ERRCCATCH); ++} ++ + /* Stack overflow error. */ + void LJ_FASTCALL lj_err_stkov(lua_State *L) + { +@@ -912,6 +939,8 @@ LJ_NOINLINE void LJ_FASTCALL lj_err_trace(lua_State *L, int errcode) + { + if (errcode == LUA_ERRRUN) + lj_err_run(L); ++ else if (errcode == LUA_ERRCCATCH) ++ lj_err_ccatch(L); + else + lj_err_throw(L, errcode); + } +@@ -1122,6 +1151,12 @@ LUA_API int lua_error(lua_State *L) + return 0; /* unreachable */ + } + ++LUA_API int lua_ext_ccatch_error(lua_State *L) ++{ ++ lj_err_ccatch(L); ++ return 0; /* unreachable */ ++} ++ + LUALIB_API int luaL_argerror(lua_State *L, int narg, const char *msg) + { + err_argmsg(L, narg, msg); +diff --git a/src/lj_err.h b/src/lj_err.h +index 0cb945b0..37b80a42 100644 +--- a/src/lj_err.h ++++ b/src/lj_err.h +@@ -25,6 +25,7 @@ LJ_FUNCA_NORET void LJ_FASTCALL lj_err_throw(lua_State *L, int errcode); + LJ_FUNC_NORET void lj_err_mem(lua_State *L); + LJ_FUNC_NORET void LJ_FASTCALL lj_err_stkov(lua_State *L); + LJ_FUNC_NORET void LJ_FASTCALL lj_err_run(lua_State *L); ++LJ_FUNC_NORET void LJ_FASTCALL lj_err_ccatch(lua_State *L); + #if LJ_HASJIT + LJ_FUNCA_NORET void LJ_FASTCALL lj_err_trace(lua_State *L, int errcode); + #endif +diff --git a/src/lua.h b/src/lua.h +index 6d1634d1..0eb0917b 100644 +--- a/src/lua.h ++++ b/src/lua.h +@@ -46,6 +46,7 @@ + #define LUA_ERRSYNTAX 3 + #define LUA_ERRMEM 4 + #define LUA_ERRERR 5 ++#define LUA_ERRCCATCH 6 + + + typedef struct lua_State lua_State; diff --git a/patch/lua_ext_provenance.patch b/patch/lua_ext_provenance.patch new file mode 100644 index 0000000..ef86497 --- /dev/null +++ b/patch/lua_ext_provenance.patch @@ -0,0 +1,31 @@ +diff --git a/src/lj_obj.h b/src/lj_obj.h +index 855727bf..351cbcb7 100644 +--- a/src/lj_obj.h ++++ b/src/lj_obj.h +@@ -700,6 +700,7 @@ struct lua_State { + GCRef env; /* Thread environment (table of globals). */ + void *cframe; /* End of C stack frame chain. */ + MSize stacksize; /* True stack size (incl. LJ_STACK_EXTRA). */ ++ uint64_t provenance; + }; + + #define G(L) (mref(L->glref, global_State)) +diff --git a/src/lj_state.c b/src/lj_state.c +index d8fc545a..99fdb804 100644 +--- a/src/lj_state.c ++++ b/src/lj_state.c +@@ -264,6 +264,7 @@ LUA_API lua_State *lua_newstate(lua_Alloc allocf, void *allocd) + memset(GG, 0, sizeof(GG_State)); + L = &GG->L; + g = &GG->g; ++ L->provenance = prng.u[0]; + L->gct = ~LJ_TTHREAD; + L->marked = LJ_GC_WHITE0 | LJ_GC_FIXED | LJ_GC_SFIXED; /* Prevent free. */ + L->dummy_ffid = FF_C; +@@ -380,3 +381,6 @@ void LJ_FASTCALL lj_state_free(global_State *g, lua_State *L) + lj_mem_freet(g, L); + } + ++LUA_API uint64_t lua_ext_getprovenance(lua_State *L) { ++ return L->provenance; ++} diff --git a/patch/lua_load_no_bc.patch b/patch/lua_load_no_bc.patch new file mode 100644 index 0000000..635638b --- /dev/null +++ b/patch/lua_load_no_bc.patch @@ -0,0 +1,77 @@ +diff --git a/src/lj_lex.c b/src/lj_lex.c +index a986aeb8..a8427739 100644 +--- a/src/lj_lex.c ++++ b/src/lj_lex.c +@@ -397,7 +397,6 @@ static LexToken lex_scan(LexState *ls, TValue *tv) + /* Setup lexer state. */ + int lj_lex_setup(lua_State *L, LexState *ls) + { +- int header = 0; + ls->L = L; + ls->fs = NULL; + ls->pe = ls->p = NULL; +@@ -413,33 +412,6 @@ int lj_lex_setup(lua_State *L, LexState *ls) + ls->endmark = 0; + ls->fr2 = LJ_FR2; /* Generate native bytecode by default. */ + lex_next(ls); /* Read-ahead first char. */ +- if (ls->c == 0xef && ls->p + 2 <= ls->pe && (uint8_t)ls->p[0] == 0xbb && +- (uint8_t)ls->p[1] == 0xbf) { /* Skip UTF-8 BOM (if buffered). */ +- ls->p += 2; +- lex_next(ls); +- header = 1; +- } +- if (ls->c == '#') { /* Skip POSIX #! header line. */ +- do { +- lex_next(ls); +- if (ls->c == LEX_EOF) return 0; +- } while (!lex_iseol(ls)); +- lex_newline(ls); +- header = 1; +- } +- if (ls->c == LUA_SIGNATURE[0]) { /* Bytecode dump. */ +- if (header) { +- /* +- ** Loading bytecode with an extra header is disabled for security +- ** reasons. This may circumvent the usual check for bytecode vs. +- ** Lua code by looking at the first char. Since this is a potential +- ** security violation no attempt is made to echo the chunkname either. +- */ +- setstrV(L, L->top++, lj_err_str(L, LJ_ERR_BCBAD)); +- lj_err_throw(L, LUA_ERRSYNTAX); +- } +- return 1; +- } + return 0; + } + +diff --git a/src/lj_load.c b/src/lj_load.c +index 828bf8ae..7b7f1478 100644 +--- a/src/lj_load.c ++++ b/src/lj_load.c +@@ -30,24 +30,10 @@ static TValue *cpparser(lua_State *L, lua_CFunction dummy, void *ud) + LexState *ls = (LexState *)ud; + GCproto *pt; + GCfunc *fn; +- int bc; + UNUSED(dummy); + cframe_errfunc(L->cframe) = -1; /* Inherit error function. */ +- bc = lj_lex_setup(L, ls); +- if (ls->mode) { +- int xmode = 1; +- const char *mode = ls->mode; +- char c; +- while ((c = *mode++)) { +- if (c == (bc ? 'b' : 't')) xmode = 0; +- if (c == (LJ_FR2 ? 'W' : 'X')) ls->fr2 = !LJ_FR2; +- } +- if (xmode) { +- setstrV(L, L->top++, lj_err_str(L, LJ_ERR_XMODE)); +- lj_err_throw(L, LUA_ERRSYNTAX); +- } +- } +- pt = bc ? lj_bcread(ls) : lj_parse(ls); ++ (void)lj_lex_setup(L, ls); ++ pt = lj_parse(ls); + if (ls->fr2 == LJ_FR2) { + fn = lj_func_newL_empty(L, pt, tabref(L->env)); + /* Don't combine above/below into one statement. */ diff --git a/patch/luajit.patch b/patch/luajit.patch new file mode 100644 index 0000000..c5d883f --- /dev/null +++ b/patch/luajit.patch @@ -0,0 +1,32 @@ +diff --git a/src/luajit.c b/src/luajit.c +index 2e2e9150..c76fe839 100644 +--- a/src/luajit.c ++++ b/src/luajit.c +@@ -149,22 +149,13 @@ static void print_version(void) + fputs(LUAJIT_VERSION " -- " LUAJIT_COPYRIGHT ". " LUAJIT_URL "\n", stdout); + } + ++#include "lj_jit.h" ++uint32_t lua_ext_getjitflags(lua_State *L); ++ + static void print_jit_status(lua_State *L) + { +- int n; +- const char *s; +- lua_getfield(L, LUA_REGISTRYINDEX, "_LOADED"); +- lua_getfield(L, -1, "jit"); /* Get jit.* module table. */ +- lua_remove(L, -2); +- lua_getfield(L, -1, "status"); +- lua_remove(L, -2); +- n = lua_gettop(L); +- lua_call(L, 0, LUA_MULTRET); +- fputs(lua_toboolean(L, n) ? "JIT: ON" : "JIT: OFF", stdout); +- for (n++; (s = lua_tostring(L, n)); n++) { +- putc(' ', stdout); +- fputs(s, stdout); +- } ++ uint32_t flags = lua_ext_getjitflags(L); ++ fputs(flags & JIT_F_ON ? "JIT: ON" : "JIT: OFF", stdout); + putc('\n', stdout); + lua_settop(L, 0); /* clear stack */ + } diff --git a/patch/windows_set_lib_names.patch b/patch/windows_set_lib_names.patch new file mode 100644 index 0000000..c2a56b7 --- /dev/null +++ b/patch/windows_set_lib_names.patch @@ -0,0 +1,22 @@ +diff --git a/src/msvcbuild.bat b/src/msvcbuild.bat +index 69c0c61a..f06df5e9 100644 +--- a/src/msvcbuild.bat ++++ b/src/msvcbuild.bat +@@ -16,7 +16,7 @@ + @setlocal + @rem Add more debug flags here, e.g. DEBUGCFLAGS=/DLUA_USE_ASSERT + @set DEBUGCFLAGS= +-@set LJCOMPILE=cl /nologo /c /O2 /W3 /D_CRT_SECURE_NO_DEPRECATE /D_CRT_STDIO_INLINE=__declspec(dllexport)__inline ++@set LJCOMPILE=cl /nologo /c /std:c11 /experimental:c11atomics /O2 /W3 /D_CRT_SECURE_NO_DEPRECATE /D_CRT_STDIO_INLINE=__declspec(dllexport)__inline + @set LJDYNBUILD=/DLUA_BUILD_AS_DLL /MD + @set LJDYNBUILD_DEBUG=/DLUA_BUILD_AS_DLL /MDd + @set LJCOMPILETARGET=/Zi +@@ -29,8 +29,6 @@ + @set DASMDIR=..\dynasm + @set DASM=%DASMDIR%\dynasm.lua + @set DASC=vm_x64.dasc +-@set LJDLLNAME=lua51.dll +-@set LJLIBNAME=lua51.lib + @set ALL_LIB=lib_base.c lib_math.c lib_bit.c lib_string.c lib_table.c lib_io.c lib_os.c lib_package.c lib_debug.c lib_jit.c lib_ffi.c lib_buffer.c + + @setlocal diff --git a/shell/core/Cargo.toml b/shell/core/Cargo.toml new file mode 100644 index 0000000..0d230a0 --- /dev/null +++ b/shell/core/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "bp3d-lua-shell" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +bp3d-lua = { version = "1.0.0-rc.4.0.0", path = "../../core", features = ["root-vm", "util-module", "util-thread", "libs", "dynamic", "interrupt"] } +bp3d-debug = "1.0.2" +bp3d-net = { version = "1.0.0-rc.2.1.2", features = ["ipc"] } +tokio = { version = "1.45.1", features = ["full"] } +bp3d-util = { version = "2.2.0", features = ["result"] } +bp3d-lua-shell-proto = { version = "0.1.0", path = "../proto" } +bp3d-proto = "1.0.0-rc.5.0.1" +clap = { version = "4.5.4", features = ["derive"] } +bp3d-os = { version = "2.2.3", features = ["time", "shell"] } diff --git a/shell/core/build.rs b/shell/core/build.rs new file mode 100644 index 0000000..c5bc1b2 --- /dev/null +++ b/shell/core/build.rs @@ -0,0 +1,32 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +fn main() { + println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path"); + println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks"); +} diff --git a/shell/core/src/autocomplete.rs b/shell/core/src/autocomplete.rs new file mode 100644 index 0000000..73ad3ac --- /dev/null +++ b/shell/core/src/autocomplete.rs @@ -0,0 +1,68 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::vm::value::any::Any; + +#[derive(Debug)] +pub enum Mode { + AddUpdate(Vec), + Delete(Vec), +} + +#[derive(Debug, Eq, PartialEq)] +pub enum Type { + Function, + Attribute, +} + +#[derive(Debug)] +pub struct Item { + pub name: String, + pub ty: Type, +} + +impl Item { + pub fn from_lua(name: &str, val: &Any) -> Self { + match val { + Any::Function(_) => Item { + name: name.into(), + ty: Type::Function, + }, + _ => Item { + name: name.into(), + ty: Type::Attribute, + }, + } + } +} + +#[derive(Debug)] +pub struct Completions { + pub path: String, + pub items: Vec, +} diff --git a/shell/core/src/core.rs b/shell/core/src/core.rs new file mode 100644 index 0000000..f45a0d0 --- /dev/null +++ b/shell/core/src/core.rs @@ -0,0 +1,140 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +const MAX_SIZE: usize = 4096; + +use crate::lua::{Args, Lua}; +use bp3d_debug::{debug, error, info}; +use bp3d_lua_shell_proto::send; +use bp3d_net::ipc::util::Message; +use bp3d_net::ipc::{Client, Server}; +use bp3d_os::shell::{Event, SendChannel, Shell}; +use bp3d_proto::message::FromBytes; +use bp3d_util::result::ResultExt; +use tokio::sync::mpsc; + +async fn client_task(lua: &mut Lua, client: Client) -> bp3d_proto::message::Result { + let mut msg = Message::new(MAX_SIZE); + loop { + tokio::select! { + res = client.recv(&mut msg) => { + res?; + if msg.is_empty() { + break; + } + let data: &[u8] = &msg; + //Nice weird broken syntax because Rust type inference is even more broken than ever. + let msg = ::from_bytes(data)?.into_inner(); + match msg.msg { + send::Message::Terminate => return Ok(true), + send::Message::RunCode(v) => lua.send(v).await, + send::Message::RunFile(v) => lua.send(v).await + } + }, + Some(b) = lua.next_msg() => { + msg.set_size(0); + b.write(&mut msg)?; + client.send(&msg).await?; + } + } + } + client.close().await?; + Ok(false) +} + +pub async fn run(args: Args, name: &str) { + info!("starting lua VM"); + let mut lua = Lua::new(args); + info!("starting IPC server"); + let mut server = Server::create(name) + .await + .expect_exit("Failed to create IPC server", 1); + while let Ok(client) = server.accept().await { + debug!("client connected"); + match client_task(&mut lua, client).await { + Err(e) => error!("client message error: {}", e), + Ok(flag) => { + debug!("client nominal exit"); + if flag { + break; + } + } + } + } + info!("terminating lua VM..."); + lua.exit().await; +} + +struct ChannelWrapper(mpsc::Sender); + +impl SendChannel for ChannelWrapper { + fn send(&self, event: Event) { + self.0.blocking_send(event).unwrap(); + } +} + +pub async fn run_interactive(args: Args) { + info!("starting lua VM"); + let mut lua = Lua::new(args); + let (tx, mut rx) = mpsc::channel::(64); + let app = Shell::new("lua> ", ChannelWrapper(tx)); + loop { + tokio::select! { + res = rx.recv() => { + match res { + None => { + error!("Shell application has prematurely closed"); + break; + }, + Some(event) => match event { + Event::CommandReceived(str) => { + lua.send(send::RunCode { + name: None, + code: &str + }).await; + }, + Event::ExitRequested => { + debug!("exit requested"); + break; + } + } + } + } + Some(b) = lua.next_msg() => { + debug!("{:?}", b); + if b.has_exited() { + break; + } + } + } + } + info!("Terminating lua VM..."); + lua.exit().await; + info!("Terminating shell application..."); + app.exit(); +} diff --git a/shell/core/src/data.rs b/shell/core/src/data.rs new file mode 100644 index 0000000..a0a1705 --- /dev/null +++ b/shell/core/src/data.rs @@ -0,0 +1,43 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::data_out::OutData; +use tokio::sync::mpsc; + +#[derive(Clone)] +pub struct DataOut(mpsc::Sender>); + +impl DataOut { + pub fn new(sender: mpsc::Sender>) -> Self { + Self(sender) + } + + pub fn send(&self, data: T) { + self.0.blocking_send(Box::new(data)).unwrap(); + } +} diff --git a/shell/core/src/data_in.rs b/shell/core/src/data_in.rs new file mode 100644 index 0000000..e18106e --- /dev/null +++ b/shell/core/src/data_in.rs @@ -0,0 +1,77 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::data::DataOut; +use crate::lua::Args; +use bp3d_lua::vm::Vm; +use std::fmt::Debug; + +pub trait InData: Send + Debug { + fn handle(&mut self, args: &Args, vm: &Vm, out: &DataOut) -> bool; +} + +pub trait NetInData { + fn to_in_data(self) -> Box; +} + +#[derive(Debug)] +pub struct RunCode { + pub name: Option, + pub code: String, +} + +#[derive(Debug)] +pub struct RunFile { + pub path: String, +} + +impl<'a> NetInData for bp3d_lua_shell_proto::send::RunFile<'a> { + fn to_in_data(self) -> Box { + Box::new(RunFile { + path: self.path.into(), + }) + } +} + +impl<'a> NetInData for bp3d_lua_shell_proto::send::RunCode<'a> { + fn to_in_data(self) -> Box { + Box::new(RunCode { + name: self.name.map(|v| v.into()), + code: self.code.into(), + }) + } +} + +#[derive(Debug)] +pub struct Exit; + +impl InData for Exit { + fn handle(&mut self, _: &Args, _: &Vm, _: &DataOut) -> bool { + true + } +} diff --git a/shell/core/src/data_out.rs b/shell/core/src/data_out.rs new file mode 100644 index 0000000..90a2b87 --- /dev/null +++ b/shell/core/src/data_out.rs @@ -0,0 +1,129 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::autocomplete::{Mode, Type}; +use bp3d_lua_shell_proto::completion; +use bp3d_lua_shell_proto::recv; +use bp3d_net::ipc::util::Message; +use bp3d_proto::message::WriteSelf; +use std::fmt::Debug; + +pub trait OutData: Send + Debug { + fn write(&self, msg: &mut Message) -> bp3d_proto::message::Result<()>; + fn has_exited(&self) -> bool { + false + } +} + +#[derive(Debug)] +pub struct End; + +impl OutData for End { + fn write(&self, msg: &mut Message) -> bp3d_proto::message::Result<()> { + recv::Main { + hdr: recv::Header::new().set_type(recv::Type::End).to_ref(), + msg: recv::Message::End, + } + .write_self(msg) + } + + fn has_exited(&self) -> bool { + true + } +} + +#[derive(Debug)] +pub struct Log(pub &'static str, pub String); + +impl OutData for Log { + fn write(&self, msg: &mut Message) -> bp3d_proto::message::Result<()> { + recv::Main { + hdr: recv::Header::new().set_type(recv::Type::Log).to_ref(), + msg: recv::Log { + source: self.0, + msg: &self.1, + }, + } + .write_self(msg) + } +} + +#[derive(Debug)] +pub struct Autocomplete(pub Mode); + +impl OutData for Autocomplete { + fn write(&self, msg: &mut Message) -> bp3d_proto::message::Result<()> { + match &self.0 { + Mode::AddUpdate(v) => { + let mut items = completion::AddUpdateItems::new(Vec::::new()); + for completion in v { + let mut items2 = completion::ListItems::new(Vec::::new()); + for item in &completion.items { + let ty = match item.ty { + Type::Function => completion::Type::Function, + Type::Attribute => completion::Type::Attribute, + }; + items2.write_item(&completion::Item { + hdr: completion::Header::new().set_type(ty).to_ref(), + name: &item.name, + })?; + } + items.write_item(&completion::List { + path: &completion.path, + items: items2.to_ref(), + })?; + } + recv::Main { + hdr: recv::Header::new() + .set_type(recv::Type::AutocompleteAddUpdate) + .to_ref(), + msg: completion::AddUpdate { + items: items.to_ref(), + }, + } + .write_self(msg)?; + } + Mode::Delete(v) => { + let mut items = completion::DeleteItems::new(Vec::::new()); + for path in v { + items.write_item(&completion::Path { path })?; + } + recv::Main { + hdr: recv::Header::new() + .set_type(recv::Type::AutocompleteDelete) + .to_ref(), + msg: completion::Delete { + items: items.to_ref(), + }, + } + .write_self(msg)?; + } + } + Ok(()) + } +} diff --git a/shell/core/src/lib1/autocomplete_api.rs b/shell/core/src/lib1/autocomplete_api.rs new file mode 100644 index 0000000..e16c0d1 --- /dev/null +++ b/shell/core/src/lib1/autocomplete_api.rs @@ -0,0 +1,130 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::autocomplete::{Completions, Item, Mode}; +use crate::data::DataOut; +use bp3d_lua::decl_closure; +use bp3d_lua::vm::closure::rc::Rc; +use bp3d_lua::vm::table::ImmutableTable; +use bp3d_lua::vm::value::any::Any; +use std::collections::HashSet; + +fn get_capacity(val: &Any) -> usize { + match val { + Any::Function(_) => 0, + Any::Table(v) => v.len(), + Any::UserData(_) => 1, + _ => 0, + } +} + +fn list_table_completions( + set: &mut HashSet, + path: Vec, + root: &mut Vec, + mut value: ImmutableTable, + metatables: bool, +) -> bp3d_lua::vm::Result<()> { + if set.contains(&value.uid()) { + return Ok(()); + } + for (k, v) in value.iter() { + let k = k.to_any()?; + let v = v.to_any()?; + match k { + Any::String(name) => { + let c = get_capacity(&v); + if c > 0 { + let mut path = path.clone(); + path.push(name.into()); + root.push(Completions { + path: path.join("."), + items: Vec::with_capacity(c), + }); + list_completions(set, path, root, v, metatables)?; + } else { + root.last_mut() + .unwrap() + .items + .push(Item::from_lua(name, &v)); + } + } + _ => continue, + } + } + if metatables { + if let Some(tbl) = value.get_metatable() { + list_table_completions(set, path, root, tbl, metatables)?; + } + } + set.insert(value.uid()); + Ok(()) +} + +fn list_completions( + set: &mut HashSet, + path: Vec, + root: &mut Vec, + value: Any, + metatables: bool, +) -> bp3d_lua::vm::Result<()> { + match value { + Any::Table(v) => list_table_completions(set, path, root, v.into(), metatables), + Any::UserData(v) => { + if let Some(tbl) = v.get_metatable() { + // We assume userdata have a single metatable (following current bp3d-lua pattern). + list_table_completions(set, path, root, tbl, false)?; + } + Ok(()) + } + _ => Ok(()), + } +} + +decl_closure! { + pub fn build_completions |ch: Rc| (lua: &Vm, name: &str, metatables: bool) -> bp3d_lua::vm::Result<()> { + let value: Any = lua.get_global(name)?; + let mut root = Vec::new(); + let mut set = HashSet::new(); + list_completions(&mut set, vec![name.into()], &mut root, value, metatables)?; + ch.send(crate::data_out::Autocomplete(Mode::AddUpdate(root))); + Ok(()) + } +} + +decl_closure! { + pub fn delete_completions |ch: Rc| (lua: &Vm, name: &str, metatables: bool) -> bp3d_lua::vm::Result<()> { + let value: Any = lua.get_global(name)?; + let mut root = Vec::new(); + let mut set = HashSet::new(); + list_completions(&mut set, vec![name.into()], &mut root, value, metatables)?; + let base = root.into_iter().map(|v| v.path); + ch.send(crate::data_out::Autocomplete(Mode::Delete(base.collect()))); + Ok(()) + } +} diff --git a/shell/core/src/lib1/mod.rs b/shell/core/src/lib1/mod.rs new file mode 100644 index 0000000..3c725a6 --- /dev/null +++ b/shell/core/src/lib1/mod.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::data::DataOut; +use crate::lib1::autocomplete_api::{build_completions, delete_completions}; +use crate::lib1::scheduler_api::{schedule_in, schedule_periodically}; +use crate::scheduler::SchedulerPtr; +use bp3d_debug::info; +use bp3d_lua::decl_closure; +use bp3d_lua::libs::Lib; +use bp3d_lua::util::Namespace; +use bp3d_lua::vm::closure::rc::{Rc, Shared}; +use std::cell::Cell; + +mod autocomplete_api; +mod scheduler_api; + +decl_closure! { + fn request_exit |running: Rc>| () -> () { + info!("Lua has requested exit"); + running.set(false); + } +} + +pub struct Shell { + log_ch: Shared, + scheduler: Shared, + running: Shared>, +} + +impl Shell { + pub fn new( + log_ch: DataOut, + scheduler: Shared, + running: Shared>, + ) -> Shell { + Self { + log_ch: log_ch.into(), + scheduler, + running, + } + } +} + +impl Lib for Shell { + const NAMESPACE: &'static str = "bp3d.lua.shell"; + + fn load(&self, namespace: &mut Namespace) -> bp3d_lua::vm::Result<()> { + let rc = Rc::from_rust(namespace.vm(), self.log_ch.clone()); + let rc1 = Rc::from_rust(namespace.vm(), self.log_ch.clone()); + let r1 = Rc::from_rust(namespace.vm(), self.scheduler.clone()); + let r2 = Rc::from_rust(namespace.vm(), self.scheduler.clone()); + let running = Rc::from_rust(namespace.vm(), self.running.clone()); + namespace.add([ + ("buildCompletions", build_completions(rc)), + ("deleteCompletions", delete_completions(rc1)), + ])?; + namespace.add([ + ("scheduleIn", schedule_in(r1)), + ("schedulePeriodically", schedule_periodically(r2)), + ])?; + namespace.add([("requestExit", request_exit(running))]) + } +} diff --git a/shell/core/src/lib1/scheduler_api.rs b/shell/core/src/lib1/scheduler_api.rs new file mode 100644 index 0000000..0d430d6 --- /dev/null +++ b/shell/core/src/lib1/scheduler_api.rs @@ -0,0 +1,44 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::scheduler::SchedulerPtr; +use bp3d_lua::decl_closure; +use bp3d_lua::vm::closure::rc::Rc; +use bp3d_lua::vm::thread::value::Thread; + +decl_closure! { + pub fn schedule_in |scheduler: Rc| (thread: Thread, after_ms: u32) -> () { + scheduler.schedule_in(thread, after_ms); + } +} + +decl_closure! { + pub fn schedule_periodically |scheduler: Rc| (thread: Thread, period_ms: u32) -> () { + scheduler.schedule_periodically(thread, period_ms); + } +} diff --git a/shell/core/src/lua.rs b/shell/core/src/lua.rs new file mode 100644 index 0000000..cdc540f --- /dev/null +++ b/shell/core/src/lua.rs @@ -0,0 +1,224 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::data::DataOut; +use crate::data_in::{Exit, InData, NetInData, RunCode, RunFile}; +use crate::data_out::{End, Log, OutData}; +use crate::lib1::Shell; +use crate::scheduler::SchedulerPtr; +use bp3d_debug::{debug, error, info, trace, warning}; +use bp3d_lua::libs; +use bp3d_lua::libs::Lib; +use bp3d_lua::libs::files::chroot::set_chroot; +use bp3d_lua::libs::lua::Module; +use bp3d_lua::vm::Vm; +use bp3d_lua::vm::core::interrupt::{Signal, spawn_interruptible}; +use bp3d_lua::vm::core::jit::JitOptions; +use bp3d_lua::vm::core::load::{Code, Script}; +use bp3d_lua::vm::value::any::Any; +use bp3d_os::module::loader::ModuleLoader; +use std::cell::Cell; +use std::path::PathBuf; +use std::rc::Rc; +use std::thread::JoinHandle; +use std::time::Duration; +use tokio::sync::mpsc; + +const CHANNEL_BUFFER: usize = 32; + +pub struct Args { + pub data: PathBuf, + pub lua: PathBuf, + pub modules: Vec, + pub main_script: Option, +} + +pub struct Lua { + signal: Signal, + handle: JoinHandle<()>, + exec_queue: mpsc::Sender>, + out_queue: mpsc::Receiver>, +} + +impl Lua { + pub async fn send(&self, net_data: T) { + let data = net_data.to_in_data(); + self.exec_queue.send(data).await.unwrap(); + } + + pub async fn exit(self) { + if let Err(_) = self.exec_queue.send(Box::new(Exit)).await { + self.handle.join().unwrap(); + warning!("Attempt to exit already exited Lua thread"); + return; + } + // Leave 50ms for the thread to terminate nominally before killing the VM. + tokio::time::sleep(Duration::from_millis(50)).await; + // This call will either immediately return because the thread is already dead (expected), + // otherwise it will pthread_kill and attempt to inject a lua hook which will later throw + // a lua uncatchable error which should bring the VM down. + let res = self.signal.send(Duration::from_secs(10)); + match res { + Ok(_) => self.handle.join().unwrap(), + Err(e) => error!("Error attempting to terminate VM thread: {}", e), + } + } + + pub async fn next_msg(&mut self) -> Option> { + self.out_queue.recv().await + } + + fn handle_value(res: bp3d_lua::vm::Result, logger: &DataOut) -> bool { + match res { + Ok(v) => { + logger.send(Log("output", v.to_string())); + false + } + Err(e) => { + if e.is_uncatchable() { + logger.send(Log("kill", e.to_string())); + error!("Received VM termination error: {}", e); + true + } else { + logger.send(Log("error", e.to_string())); + error!("Failed to run code: {}", e); + false + } + } + } + } + + pub fn new(args: Args) -> Self { + let (exec_queue, mut receiver) = mpsc::channel(CHANNEL_BUFFER); + let (logger, out_queue) = mpsc::channel(CHANNEL_BUFFER); + let (signal, handle) = spawn_interruptible(move |vm| { + let logger = DataOut::new(logger); + let scheduler = Rc::new(SchedulerPtr::new()); + let running = Rc::new(Cell::new(true)); + info!("Loading VM libraries..."); + if let Err(e) = libs::lua::Debug.register(vm) { + error!("Failed to load debug library: {}", e); + } + if let Err(e) = (libs::os::Compat, libs::os::Instant, libs::os::Time).register(vm) { + error!("Failed to load OS library: {}", e); + } + if let Err(e) = libs::util::Util.register(vm) { + error!("Failed to load util library: {}", e); + } + set_chroot(&vm, &args.data); + if let Err(e) = libs::lua::Lua::new().build().register(vm) { + error!("Failed to load base library: {}", e); + } + info!("Loading bp3d-lua-shell library..."); + if let Err(e) = + Shell::new(logger.clone(), scheduler.clone(), running.clone()).register(vm) + { + error!("Failed to load shell library: {}", e); + } + { + let mut loader = ModuleLoader::lock(); + for path in &args.modules { + trace!("Adding search path: {:?}", path); + loader.add_search_path(path.clone()); + } + } + if let Err(e) = Module.register(vm) { + error!("Failed to load module manager: {}", e); + } + let jit = JitOptions::get(vm); + if jit.is_enabled() { + info!("JIT: ON ({})", jit.opt_level()); + info!("{}", jit.opts()); + info!("{}", jit.cpu()); + } else { + info!("JIT: OFF") + } + if let Some(main_script) = &args.main_script { + vm.scope(|vm| { + Ok(RunFile { + path: main_script.clone(), + } + .handle(&args, vm, &logger)) + }) + .unwrap(); + } + while running.get() { + // First handle IPC events + while let Some(command) = receiver.try_recv().ok() { + // Nice type-inference breakage with this box. + trace!("received command: {:?}", command); + let ret = vm + .scope(|vm| Ok((command as Box).handle(&args, vm, &logger))) + .unwrap(); + trace!({ ret }, "command handled"); + if ret { + running.set(false); + break; + } + } + // Now run the scheduler + scheduler.step(vm, &logger); + // Wait for next cycle + std::thread::sleep(Duration::from_millis(1)); + } + logger.send(End); + }); + Self { + signal, + handle, + exec_queue, + out_queue, + } + } +} + +impl InData for RunCode { + fn handle(&mut self, _: &Args, vm: &Vm, out: &DataOut) -> bool { + match &self.name { + Some(name) => Lua::handle_value(vm.run(Code::new(name, self.code.as_bytes())), out), + None => Lua::handle_value(vm.run_code(&*self.code), out), + } + } +} + +impl InData for RunFile { + fn handle(&mut self, args: &Args, vm: &Vm, out: &DataOut) -> bool { + let path = args.lua.join(&self.path); + debug!("Loading script file: {:?}...", path); + let script = match Script::from_path(&path) { + Ok(script) => script, + Err(e) => { + error!("Error loading lua script: {}", e); + out.send(Log("file", e.to_string())); + return false; + } + }; + debug!("Running script file: {:?}...", path); + Lua::handle_value(vm.run(script), out) + } +} diff --git a/shell/core/src/main.rs b/shell/core/src/main.rs new file mode 100644 index 0000000..c69bcfd --- /dev/null +++ b/shell/core/src/main.rs @@ -0,0 +1,86 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_debug::trace; +use clap::Parser; +use std::path::PathBuf; + +mod autocomplete; +mod core; +mod data; +mod data_in; +mod data_out; +mod lib1; +mod lua; +mod scheduler; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Cli { + #[arg(short = 'n', long = "name", help = "Name of IPC server.")] + pub name: Option, + + #[arg(short = 'r', long = "root", help = "Path to lua root directory.")] + pub root: Option, + + #[arg(short = 'm', long = "modules", help = "Path to modules directory.")] + pub modules: Option, + + #[arg(long = "it", help = "Run in interactive mode.")] + pub interactive: bool, + + #[arg(help = "Path to main script to start at Vm startup in the root directory.")] + pub main_script: Option, +} + +#[tokio::main] +async fn main() { + let args = Cli::parse(); + let root = args.root.unwrap_or(PathBuf::from("./")); + let mut modules = Vec::new(); + if let Some(path) = args.modules { + modules.push(path); + } + modules.push(PathBuf::from("./target/debug")); + let largs = lua::Args { + data: root.join("data"), + lua: root.join("src"), + modules, + main_script: args.main_script, + }; + if args.interactive { + core::run_interactive(largs).await; + } else { + core::run( + largs, + args.name.as_ref().map(|v| &**v).unwrap_or("bp3d-lua-shell"), + ) + .await; + } + trace!("Application end"); +} diff --git a/shell/core/src/scheduler.rs b/shell/core/src/scheduler.rs new file mode 100644 index 0000000..757f40b --- /dev/null +++ b/shell/core/src/scheduler.rs @@ -0,0 +1,169 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::data::DataOut; +use crate::data_out::Log; +use bp3d_debug::{error, warning}; +use bp3d_lua::util::LuaThread; +use bp3d_lua::vm::Vm; +use bp3d_lua::vm::thread::core::State; +use bp3d_lua::vm::thread::value::Thread; +use bp3d_os::time::Instant; +use std::cell::RefCell; +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +struct Task { + at_ms: u64, + period_ms: Option, + thread: LuaThread, +} + +impl Eq for Task {} + +impl Ord for Task { + fn cmp(&self, other: &Self) -> Ordering { + self.at_ms.cmp(&other.at_ms) + } +} + +impl PartialEq for Task { + fn eq(&self, other: &Self) -> bool { + self.thread.as_thread() == other.thread.as_thread() + } +} + +impl PartialOrd for Task { + fn partial_cmp(&self, other: &Self) -> Option { + other.at_ms.partial_cmp(&self.at_ms) + } +} + +struct Scheduler { + main: BinaryHeap, + instant: Instant, +} + +impl Scheduler { + pub fn schedule_in(&mut self, value: Thread, after_ms: u32) { + let task = Task { + at_ms: self.instant.elapsed().as_millis() as u64 + after_ms as u64, + period_ms: None, + thread: LuaThread::create(value), + }; + self.main.push(task); + } + + pub fn schedule_periodically(&mut self, value: Thread, period_ms: u32) { + let task = Task { + at_ms: self.instant.elapsed().as_millis() as u64 + period_ms as u64, + period_ms: Some(period_ms), + thread: LuaThread::create(value), + }; + self.main.push(task); + } + + pub fn step(&mut self, vm: &Vm, logger: &DataOut) { + let time = self.instant.elapsed().as_millis() as u64; + while let Some(task) = self.main.peek() { + if time >= task.at_ms { + let mut task = unsafe { self.main.pop().unwrap_unchecked() }; + let out = match task.thread.as_thread().resume::>(()) { + Ok(v) => v, + Err(e) => { + error!( + "{}: Failed to schedule lua thread: {:?}", + task.thread.as_thread(), + e + ); + logger.send(Log("scheduler", e.to_string())); + task.thread.delete(vm); + continue; + } + }; + if let Some(new_time_ms) = out.data { + if out.state == State::Suspended { + if task.period_ms.is_some() { + task.period_ms = Some(new_time_ms); + } + task.at_ms = time + new_time_ms as u64; + self.main.push(task); + continue; + } else { + warning!( + "{}: Attempt to change period or time of terminated task.", + task.thread.as_thread() + ); + task.thread.delete(vm); + continue; + } + } + if let Some(period_ms) = task.period_ms { + if out.state == State::Suspended { + task.at_ms = time + period_ms as u64; + self.main.push(task); + } else { + warning!( + "{}: Attempt to re-schedule terminated task.", + task.thread.as_thread() + ); + task.thread.delete(vm); + } + } + } else { + break; + } + } + } +} + +pub struct SchedulerPtr(RefCell); + +impl SchedulerPtr { + pub fn new() -> Self { + Self(RefCell::new(Scheduler { + main: BinaryHeap::new(), + instant: Instant::now(), + })) + } + + pub fn schedule_in(&self, value: Thread, after_ms: u32) { + let mut inner = self.0.borrow_mut(); + inner.schedule_in(value, after_ms); + } + + pub fn schedule_periodically(&self, value: Thread, period_ms: u32) { + let mut inner = self.0.borrow_mut(); + inner.schedule_periodically(value, period_ms); + } + + pub fn step(&self, vm: &Vm, logger: &DataOut) { + let mut inner = self.0.borrow_mut(); + inner.step(vm, logger); + } +} diff --git a/shell/proto/Cargo.toml b/shell/proto/Cargo.toml new file mode 100644 index 0000000..dc029b2 --- /dev/null +++ b/shell/proto/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bp3d-lua-shell-proto" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +bp3d-proto = "1.0.0-rc.5.1.0" + +[build-dependencies] +bp3d-protoc = { version = "1.0.0-rc.5.0.1", features = ["api-rust"] } diff --git a/shell/proto/build.rs b/shell/proto/build.rs new file mode 100644 index 0000000..4f33577 --- /dev/null +++ b/shell/proto/build.rs @@ -0,0 +1,34 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_protoc::api::generate_rust; +use std::path::Path; + +fn main() { + generate_rust(Path::new("./protoc.toml")).unwrap(); +} diff --git a/shell/proto/protoc.toml b/shell/proto/protoc.toml new file mode 100644 index 0000000..94ddf28 --- /dev/null +++ b/shell/proto/protoc.toml @@ -0,0 +1,12 @@ +[package] +name = "" +path = "src" + +[options.recv] +union-set-discriminant = true + +[options.send] +union-set-discriminant = true + +[options.completion] +list-wrappers = true diff --git a/shell/proto/src/completion.json5 b/shell/proto/src/completion.json5 new file mode 100644 index 0000000..4a4e3b8 --- /dev/null +++ b/shell/proto/src/completion.json5 @@ -0,0 +1,105 @@ +{ + name: "completion", + enums: [ + { + name: "Type", + variants: { + "Function": 0, + "Attribute": 1, + "Subtree": 2 + } + } + ], + structs: [ + { + name: "Header", + fields: [ + { + name: "type", + raw: { + type: "unsigned", + bits: 8 + }, + view: { + type: "enum", + name: "Type" + } + } + ] + } + ], + messages: [ + { + name: "Item", + fields: [ + { + name: "hdr", + item_type: "Header" + }, + { + name: "name", + value: { + type: "string" + } + } + ] + }, + { + name: "Path", + fields: [ + { + name: "path", + value: { + type: "string" + } + } + ] + }, + { + name: "List", + fields: [ + { + name: "path", + value: { + type: "string" + } + }, + { + name: "items", + value: { + type: "list", + max_len: 255, + item_type: "Item", + nested: true + } + } + ] + }, + { + name: "AddUpdate", + fields: [ + { + name: "items", + value: { + type: "list", + max_len: 255, + item_type: "List" + } + } + ] + }, + { + name: "Delete", + fields: [ + { + name: "items", + value: { + type: "list", + max_len: 255, + item_type: "Path" + } + } + ] + } + ] +} diff --git a/shell/proto/src/lib.rs b/shell/proto/src/lib.rs new file mode 100644 index 0000000..4e2b475 --- /dev/null +++ b/shell/proto/src/lib.rs @@ -0,0 +1,31 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +include!(env!("BP3D_PROTOC_SEND")); +include!(env!("BP3D_PROTOC_RECV")); +include!(env!("BP3D_PROTOC_COMPLETION")); diff --git a/shell/proto/src/recv.json5 b/shell/proto/src/recv.json5 new file mode 100644 index 0000000..a86ab53 --- /dev/null +++ b/shell/proto/src/recv.json5 @@ -0,0 +1,84 @@ +{ + name: "recv", + imports: [ + { protocol: "completion", type: "AddUpdate" }, + { protocol: "completion", type: "Delete" } + ], + enums: [ + { + name: "Type", + variants: { + "End": 0, + "Log": 1, + "AutocompleteAddUpdate": 2, + "AutocompleteDelete": 3 + } + } + ], + structs: [ + { + name: "Header", + fields: [ + { + name: "type", + raw: { + type: "unsigned", + bits: 8 + }, + view: { + type: "enum", + name: "Type" + } + } + ] + } + ], + messages: [ + { + name: "Log", + fields: [ + { + name: "source", + value: { + type: "string" + } + }, + { + name: "msg", + value: { + type: "string" + } + } + ] + }, + { + name: "Main", + fields: [ + { + name: "hdr", + item_type: "Header" + }, + { + name: "msg", + header: "hdr", + value: { + type: "union", + name: "Message" + } + } + ] + } + ], + unions: [ + { + name: "Message", + discriminant: "Header.type", + cases: [ + { name: "End", case: "End" }, + { name: "Log", case: "Log", item_type: "Log" }, + { name: "AutocompleteAddUpdate", case: "AutocompleteAddUpdate", item_type: "AddUpdate" }, + { name: "AutocompleteDelete", case: "AutocompleteDelete", item_type: "Delete" } + ] + } + ] +} diff --git a/shell/proto/src/send.json5 b/shell/proto/src/send.json5 new file mode 100644 index 0000000..a154ca4 --- /dev/null +++ b/shell/proto/src/send.json5 @@ -0,0 +1,90 @@ +{ + name: "send", + enums: [ + { + name: "Type", + variants: { + "Terminate": 0, + "RunCode": 1, + "RunFile": 2 + } + } + ], + structs: [ + { + name: "Header", + fields: [ + { + name: "type", + raw: { + type: "unsigned", + bits: 8 + }, + view: { + type: "enum", + name: "Type" + } + } + ] + } + ], + messages: [ + { + name: "RunFile", + fields: [ + { + name: "path", + value: { + type: "string" + } + } + ] + }, + { + name: "RunCode", + fields: [ + { + name: "name", + optional: true, + value: { + type: "string" + } + }, + { + name: "code", + value: { + type: "string" + } + } + ] + }, + { + name: "Main", + fields: [ + { + name: "hdr", + item_type: "Header" + }, + { + name: "msg", + header: "hdr", + value: { + type: "union", + name: "Message" + } + } + ] + } + ], + unions: [ + { + name: "Message", + discriminant: "Header.type", + cases: [ + { name: "Terminate", case: "Terminate" }, + { name: "RunCode", case: "RunCode", item_type: "RunCode" }, + { name: "RunFile", case: "RunFile", item_type: "RunFile" } + ] + } + ] +} diff --git a/testbin/.cargo/config b/testbin/.cargo/config new file mode 100644 index 0000000..9b9ea9c --- /dev/null +++ b/testbin/.cargo/config @@ -0,0 +1,15 @@ +[target.x86_64-unknown-linux-gnu.lua] +rustc-link-search = [] +rustc-link-lib = [] + +[target.aarch64-apple-darwin.lua] +rustc-link-search = [] +rustc-link-lib = [] + +[target.aarch64-pc-windows-msvc.lua] +rustc-link-search = [] +rustc-link-lib = [] + +[target.x86_64-pc-windows-msvc.lua] +rustc-link-search = [] +rustc-link-lib = [] diff --git a/testbin/Cargo.toml b/testbin/Cargo.toml new file mode 100644 index 0000000..d9ecfba --- /dev/null +++ b/testbin/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "testbin" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +mlua = { version = "0.11.1", features = ["luajit"] } +bp3d-lua = { version = "1.0.0-rc.4.0.0", path = "../core", features = ["root-vm"] } +bp3d-os = { version = "2.2.3", features = ["time"] } + +[workspace] +members = [] diff --git a/testbin/src/context.rs b/testbin/src/context.rs new file mode 100644 index 0000000..169b1b2 --- /dev/null +++ b/testbin/src/context.rs @@ -0,0 +1,117 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::decl_closure; +use bp3d_lua::vm::closure::context::{CellMut, ContextMut}; +use bp3d_lua::vm::RootVm; +use mlua::{Lua, UserDataMethods}; +use std::time::Duration; + +struct TestContext { + value3: Vec, +} + +decl_closure! { + fn context_push |ctx: ContextMut| (val: u64) -> () { + let mut ctx = ctx.borrow(); + ctx.value3.push(val); + } +} + +decl_closure! { + fn context_pop |ctx: ContextMut| () -> Option { + let mut ctx = ctx.borrow(); + ctx.value3.pop() + } +} + +pub fn test_context_mlua() -> Duration { + let lua = Lua::new(); + lua.register_userdata_type::(|reg| { + reg.add_method_mut("push", |_, this, val: u64| { + this.value3.push(val); + Ok(()) + }); + reg.add_method_mut("pop", |_, this, _: ()| Ok(this.value3.pop())); + }) + .unwrap(); + let mut ctx = TestContext { value3: Vec::new() }; + let time = bp3d_os::time::Instant::now(); + for _ in 0..20000 { + lua.scope(|l| { + let ud = l.create_any_userdata_ref_mut(&mut ctx).unwrap(); + lua.globals().set("ctx", ud).unwrap(); + lua.load("assert(ctx:pop() == nil)").eval::<()>().unwrap(); + lua.load("ctx:push(1)").eval::<()>().unwrap(); + lua.load("ctx:push(2)").eval::<()>().unwrap(); + lua.load("ctx:push(3)").eval::<()>().unwrap(); + Ok(()) + }) + .unwrap(); + lua.scope(|l| { + let ud = l.create_any_userdata_ref_mut(&mut ctx).unwrap(); + lua.globals().set("ctx", ud).unwrap(); + lua.load("assert(ctx:pop() == 3)").eval::<()>().unwrap(); + lua.load("assert(ctx:pop() == 2)").eval::<()>().unwrap(); + lua.load("assert(ctx:pop() == 1)").eval::<()>().unwrap(); + lua.load("assert(ctx:pop() == nil)").eval::<()>().unwrap(); + lua.load("assert(ctx:pop() == nil)").eval::<()>().unwrap(); + Ok(()) + }) + .unwrap(); + } + time.elapsed() +} + +pub fn test_context_vm() -> Duration { + let vm = RootVm::new(); + let ctx = ContextMut::new(&vm); + vm.set_global(c"context_push", context_push(ctx)).unwrap(); + vm.set_global(c"context_pop", context_pop(ctx)).unwrap(); + let mut obj = TestContext { value3: vec![] }; + let mut ctx = CellMut::new(ctx); + let time = bp3d_os::time::Instant::now(); + for _ in 0..20000 { + { + let _obj = ctx.bind(&mut obj); + vm.run_code::<()>(c"assert(context_pop() == nil)").unwrap(); + vm.run_code::<()>(c"context_push(1)").unwrap(); + vm.run_code::<()>(c"context_push(2)").unwrap(); + vm.run_code::<()>(c"context_push(3)").unwrap(); + } + { + let _obj = ctx.bind(&mut obj); + vm.run_code::<()>(c"assert(context_pop() == 3)").unwrap(); + vm.run_code::<()>(c"assert(context_pop() == 2)").unwrap(); + vm.run_code::<()>(c"assert(context_pop() == 1)").unwrap(); + vm.run_code::<()>(c"assert(context_pop() == nil)").unwrap(); + vm.run_code::<()>(c"assert(context_pop() == nil)").unwrap(); + } + } + time.elapsed() +} diff --git a/testbin/src/context_opt.rs b/testbin/src/context_opt.rs new file mode 100644 index 0000000..c80af4e --- /dev/null +++ b/testbin/src/context_opt.rs @@ -0,0 +1,143 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use bp3d_lua::decl_closure; +use bp3d_lua::vm::closure::context::{CellMut, ContextMut}; +use bp3d_lua::vm::value::types::Function as LuaFunction; +use bp3d_lua::vm::RootVm; +use mlua::{Function, Lua, UserDataMethods}; +use std::time::Duration; + +struct TestContext { + value3: Vec, +} + +decl_closure! { + fn context_push |ctx: ContextMut| (val: u32) -> () { + let mut ctx = ctx.borrow(); + ctx.value3.push(val); + } +} + +decl_closure! { + fn context_pop |ctx1: ContextMut| () -> Option { + let mut ctx = ctx1.borrow(); + ctx.value3.pop() + } +} + +pub fn test_context_mlua() -> Duration { + let lua = Lua::new(); + lua.register_userdata_type::(|reg| { + reg.add_method_mut("push", |_, this, val: u32| { + this.value3.push(val); + Ok(()) + }); + reg.add_method_mut("pop", |_, this, _: ()| Ok(this.value3.pop())); + }) + .unwrap(); + lua.load( + " + function part1(ctx) + assert(ctx:pop() == nil) + ctx:push(1) + ctx:push(2) + ctx:push(3) + end + function part2(ctx) + assert(ctx:pop() == 3) + assert(ctx:pop() == 2) + assert(ctx:pop() == 1) + assert(ctx:pop() == nil) + assert(ctx:pop() == nil) + end + ", + ) + .eval::<()>() + .unwrap(); + let part1: Function = lua.globals().get("part1").unwrap(); + let part2: Function = lua.globals().get("part2").unwrap(); + let mut ctx = TestContext { value3: Vec::new() }; + let time = bp3d_os::time::Instant::now(); + for _ in 0..20000 { + lua.scope(|l| { + let ud = l.create_any_userdata_ref_mut(&mut ctx).unwrap(); + part1.call::<()>(ud).unwrap(); + Ok(()) + }) + .unwrap(); + lua.scope(|l| { + let ud = l.create_any_userdata_ref_mut(&mut ctx).unwrap(); + part2.call::<()>(ud).unwrap(); + Ok(()) + }) + .unwrap(); + } + time.elapsed() +} + +pub fn test_context_vm() -> Duration { + let vm = RootVm::new(); + let ctx = ContextMut::new(&vm); + vm.set_global(c"context_push", context_push(ctx)).unwrap(); + vm.set_global(c"context_pop", context_pop(ctx)).unwrap(); + vm.run_code::<()>( + c" + function part1() + assert(context_pop() == nil) + context_push(1) + context_push(2) + context_push(3) + end + function part2() + assert(context_pop() == 3) + assert(context_pop() == 2) + assert(context_pop() == 1) + assert(context_pop() == nil) + assert(context_pop() == nil) + end + ", + ) + .unwrap(); + let part1: LuaFunction = vm.get_global("part1").unwrap(); + let part2: LuaFunction = vm.get_global("part2").unwrap(); + let mut obj = TestContext { value3: vec![] }; + let mut ctx = CellMut::new(ctx); + let time = bp3d_os::time::Instant::now(); + for _ in 0..20000 { + { + let _obj = ctx.bind(&mut obj); + part1.call::<()>(()).unwrap(); + } + { + let _obj = ctx.bind(&mut obj); + part2.call::<()>(()).unwrap(); + } + } + time.elapsed() +} diff --git a/testbin/src/main.rs b/testbin/src/main.rs new file mode 100644 index 0000000..9268866 --- /dev/null +++ b/testbin/src/main.rs @@ -0,0 +1,145 @@ +// Copyright (c) 2025, BlockProject 3D +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of BlockProject 3D nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +mod context; +mod context_opt; + +use bp3d_lua::decl_lib_func; +use bp3d_lua::vm::function::types::RFunction; +use bp3d_lua::vm::RootVm; +use mlua::Lua; +use std::time::Duration; + +struct ValueWithDrop; +impl ValueWithDrop { + pub fn print(&self) {} +} +impl Drop for ValueWithDrop { + fn drop(&mut self) {} +} + +decl_lib_func! { + fn test_c_function(name: &str, value: f64) -> String { + let drop = ValueWithDrop; + drop.print(); + format!("Hello {} ({})", name, value) + } +} + +fn test_vm_destructor() -> Duration { + let mut vm = RootVm::new(); + vm.set_global(c"test_c_function", RFunction::wrap(test_c_function)) + .unwrap(); + let time = bp3d_os::time::Instant::now(); + for _ in 0..20000 { + let res = vm.run_code::<&str>(c"return test_c_function('this is a test\\xFF', 0.42)"); + assert!(res.is_err()); + assert!(vm + .run_code::<&str>(c"return test_c_function('this is a test', 0.42)") + .is_ok()); + let s = vm + .run_code::<&str>(c"return test_c_function('this is a test', 0.42)") + .unwrap(); + assert_eq!(s, "Hello this is a test (0.42)"); + vm.clear(); + } + time.elapsed() +} + +fn test_vm_mlua() -> Duration { + let lua = Lua::new(); + let f = lua + .create_function(|_, (name, value): (String, f64)| { + let drop = ValueWithDrop; + drop.print(); + Ok(format!("Hello {} ({})", name, value)) + }) + .unwrap(); + lua.globals().set("test_c_function", f).unwrap(); + let time = bp3d_os::time::Instant::now(); + for _ in 0..20000 { + let res: mlua::Result = lua + .load("return test_c_function('this is a test\\xFF', 0.42)") + .call(()); + assert!(res.is_err()); + assert!(lua + .load("return test_c_function('this is a test', 0.42)") + .call::(()) + .is_ok()); + let s: String = lua + .load("return test_c_function('this is a test', 0.42)") + .call(()) + .unwrap(); + assert_eq!(s, "Hello this is a test (0.42)"); + } + time.elapsed() +} + +fn main() { + const RUNS: u32 = 10; + let mut lua = Duration::new(0, 0); + let mut mlua = Duration::new(0, 0); + let mut ctx_lua = Duration::new(0, 0); + let mut ctx_mlua = Duration::new(0, 0); + let mut ctx_lua_opt = Duration::new(0, 0); + let mut ctx_mlua_opt = Duration::new(0, 0); + + for _ in 0..RUNS { + lua += test_vm_destructor(); + mlua += test_vm_mlua(); + ctx_lua += context::test_context_vm(); + ctx_mlua += context::test_context_mlua(); + ctx_lua_opt += context_opt::test_context_vm(); + ctx_mlua_opt += context_opt::test_context_mlua(); + } + + lua /= RUNS; + mlua /= RUNS; + ctx_lua /= RUNS; + ctx_mlua /= RUNS; + ctx_lua_opt /= RUNS; + ctx_mlua_opt /= RUNS; + + println!("average tools.lua (basic): {:?}", lua); + println!("average mlua (basic): {:?}", mlua); + assert!(lua < mlua); + println!("average diff (basic): {:?}", mlua - lua); + + println!("average tools.lua (context): {:?}", ctx_lua); + println!("average mlua (context): {:?}", ctx_mlua); + assert!(ctx_lua < ctx_mlua); + println!("average diff (context): {:?}", ctx_mlua - ctx_lua); + + println!("average tools.lua (context_opt): {:?}", ctx_lua_opt); + println!("average mlua (context_opt): {:?}", ctx_mlua_opt); + assert!(ctx_lua_opt < ctx_mlua_opt); + println!( + "average diff (context_opt): {:?}", + ctx_mlua_opt - ctx_lua_opt + ); +}