From 96adaf3211d81a1188b60cd980a0059eac8cf26c Mon Sep 17 00:00:00 2001 From: bytedream Date: Sun, 17 Dec 2023 20:17:55 +0100 Subject: [PATCH] initial commit --- .github/workflows/deploy.yml | 51 ++++ .gitignore | 6 + LICENSE | 21 ++ README.md | 38 +++ book.toml | 13 + lua-playground/Cargo.lock | 373 ++++++++++++++++++++++++ lua-playground/Cargo.toml | 10 + lua-playground/build.rs | 23 ++ lua-playground/lua-playground-loader.js | 86 ++++++ lua-playground/src/lib.rs | 18 ++ src/SUMMARY.md | 13 + src/introduction.md | 11 + src/setup.md | 31 ++ src/testing.md | 32 ++ src/tutorial/adding-wasm-logic.md | 37 +++ src/tutorial/calling-from-javascript.md | 189 ++++++++++++ src/tutorial/compiling.md | 8 + src/tutorial/creating-a-project.md | 98 +++++++ src/tutorial/introduction.md | 9 + 19 files changed, 1067 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 book.toml create mode 100644 lua-playground/Cargo.lock create mode 100644 lua-playground/Cargo.toml create mode 100644 lua-playground/build.rs create mode 100644 lua-playground/lua-playground-loader.js create mode 100644 lua-playground/src/lib.rs create mode 100644 src/SUMMARY.md create mode 100644 src/introduction.md create mode 100644 src/setup.md create mode 100644 src/testing.md create mode 100644 src/tutorial/adding-wasm-logic.md create mode 100644 src/tutorial/calling-from-javascript.md create mode 100644 src/tutorial/compiling.md create mode 100644 src/tutorial/creating-a-project.md create mode 100644 src/tutorial/introduction.md diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..c480001 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,51 @@ +name: Deploy +on: + push: + branches: + - main + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install mdbook + run: cargo install mdbook + + - name: Install emscripten + run: sudo apt-get install -y --no-install-recommends emscripten + + - name: Add wasm32-unknown-emscripten toolchain + run: rustup target add wasm32-unknown-emscripten + + - name: Build book + run: mdbook build + + - name: Build playground + run: BOOK_OUTPUT_PATH="$PWD/book" cargo build --release --manifest-path=lua-playground/Cargo.toml --target wasm32-unknown-emscripten + + - name: Setup Pages + uses: actions/configure-pages@v2 + + - name: Upload book + uses: actions/upload-pages-artifact@v1 + with: + path: ./book + + - name: Deploy + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a385676 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +.vscode + +book + +lua-playground/target diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cfa4f6e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) bytedream + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..77b9061 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +
+

Lua in the Browser, with Rust and WebAssembly

+ This smol book describes how to use Lua in the Browser, powered by Rust WebAssembly. +

Read the book

+
+ +## 🛠 Building the Book + +The book is made using [`mdbook`](https://github.com/rust-lang-nursery/mdBook): +```shell +$ cargo install mdbook +``` +Make sure the `cargo install` directory is in your `$PATH` so that you can run +the binary. + +To build it, simply run this command from this directory: +```shell +$ mdbook build +``` +This will build the book and output files into the `book` directory. From +there you can navigate to the `index.html` file to view it in your browser. + +You could also run the following command to automatically build the book whenever you make changes to it in the `src` directory: +```shell +$ mdbook serve +``` + +This book also contains a little demo/repl which is able to execute arbitrary Lua code in the browser via WebAssembly. +To build the required files, run the following command: +```shell +$ BOOK_OUTPUT_PATH="$PWD/book" cargo build --release --target wasm32-unknown-emscripten --manifest-path=lua-playground/Cargo.toml +``` +Make sure to run this command _after_ you build the book. +Also, you have to re-run it everytime when the book is rebuilt. + +## ⚖ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for more details. diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..dcfbe1c --- /dev/null +++ b/book.toml @@ -0,0 +1,13 @@ +[book] +authors = ["bytedream"] +language = "en" +multilingual = false +src = "src" +title = "Lua in the Browser, with Rust and WebAssembly" + +[output.html] +additional-js = ["lua-playground/lua-playground-loader.js"] +git-repository-url = "https://github.com/bytedream/litbwraw" + +[output.html.playground] +editable = true diff --git a/lua-playground/Cargo.lock b/lua-playground/Cargo.lock new file mode 100644 index 0000000..4e01b4b --- /dev/null +++ b/lua-playground/Cargo.lock @@ -0,0 +1,373 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "lua-playground" +version = "0.1.0" +dependencies = [ + "mlua", +] + +[[package]] +name = "lua-src" +version = "546.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da0daa7eee611a4c30c8f5ee31af55266e26e573971ba9336d2993e2da129b2" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.5.3+29b0b28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2bb89013916ce5c949f01a1fbd6d435a58e1d980767a791d755911211d792d" +dependencies = [ + "cc", + "which", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mlua" +version = "0.9.2" +source = "git+https://github.com/khvzak/mlua.git#0b9a85e1838bed67ae69f11b42de84bcf19da80c" +dependencies = [ + "bstr", + "mlua-sys", + "num-traits", + "once_cell", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.4.0" +source = "git+https://github.com/khvzak/mlua.git#0b9a85e1838bed67ae69f11b42de84bcf19da80c" +dependencies = [ + "cc", + "cfg-if", + "lua-src", + "luajit-src", + "pkg-config", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "serde" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.193" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "which" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bf3ea8596f3a0dd5980b46430f2058dfe2c36a27ccfbb1845d6fbfcd9ba6e14" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/lua-playground/Cargo.toml b/lua-playground/Cargo.toml new file mode 100644 index 0000000..5e74e39 --- /dev/null +++ b/lua-playground/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "lua-playground" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +mlua = { git = "https://github.com/khvzak/mlua.git", features = ["lua51"] } diff --git a/lua-playground/build.rs b/lua-playground/build.rs new file mode 100644 index 0000000..72cc1cb --- /dev/null +++ b/lua-playground/build.rs @@ -0,0 +1,23 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + let book_output_path = env::var("BOOK_OUTPUT_PATH").map_or(None, Some); + + let out_dir = env::var("OUT_DIR").unwrap(); + let pkg_name = env::var("CARGO_PKG_NAME").unwrap(); + let target_path = PathBuf::from(out_dir) + .parent() + .unwrap() + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf(); + + println!("cargo:rustc-link-arg=-sEXPORTED_RUNTIME_METHODS=['cwrap','ccall']"); + println!("cargo:rustc-link-arg=-sEXPORT_ES6=1"); + println!("cargo:rustc-link-arg=-sERROR_ON_UNDEFINED_SYMBOLS=0"); + println!("cargo:rustc-link-arg=--no-entry"); + println!("cargo:rustc-link-arg=-o{}.js", book_output_path.map(PathBuf::from).unwrap_or(target_path).join(pkg_name).to_string_lossy()); +} diff --git a/lua-playground/lua-playground-loader.js b/lua-playground/lua-playground-loader.js new file mode 100644 index 0000000..a1f4e3a --- /dev/null +++ b/lua-playground/lua-playground-loader.js @@ -0,0 +1,86 @@ +let luaStdout = null; +let luaStderr = null; + +async function run_lua_code(elem) { + let result; + if (!elem.nextElementSibling) { + result = document.createElement('code'); + result.classList.add('result', 'hljs', 'language-bash'); + elem.after(result) + } else { + result = elem.nextElementSibling; + } + result.innerHTML = ''; + + if (window.luaInstance === undefined) { + let wasm; + try { + wasm = await import(window.rootPath + '/lua-playground.js'); + } catch (e) { + result.innerText = 'Failed to load wasm module: ' + e.toString(); + return; + } + + const module = { + print(msg) { + if (luaStdout) luaStdout(msg) + }, + printErr(msg) { + if (luaStderr) luaStderr(msg) + } + } + const luaPlayground = await wasm.default(module); + + window.luaInstance = luaPlayground.ccall('lua_new', 'number', [], []); + window.luaExecute = luaPlayground.cwrap('lua_execute', null, ['number', 'string']); + } + + luaStdout = (msg) => result.innerHTML += msg + '
'; + luaStderr = (msg) => result.innerHTML += msg + '
'; + window.luaExecute(window.luaInstance, ace.edit(elem).getValue()); + luaStdout = null; + luaStderr = null; +} + +function main() { + const inputElements = document.querySelectorAll('.language-lua.editable'); + + for (const inputElem of inputElements) { + const editor = ace.edit(inputElem); + + /* adds the run and reset button */ + const buttons = inputElem.previousElementSibling; + const resetButton = document.createElement('button'); + resetButton.classList.add('fa', 'fa-history', 'reset-button'); + resetButton.title = 'Undo changes'; + resetButton.ariaLabel = 'Undo changes'; + resetButton.onclick = () => editor.setValue(editor.originalCode.trim(), -1); + buttons.prepend(resetButton); + const runButton = document.createElement('button'); + runButton.classList.add('fa', 'fa-play', 'play-button'); + runButton.title = 'Run this code'; + runButton.ariaLabel = 'Run this code'; + runButton.onclick = () => run_lua_code(inputElem); + buttons.append(runButton); + + /* i don't know why, but the editor always has an extra newline. when selecting it and trimming it, the newline + gets removed */ + editor.setValue(editor.originalCode.trim(), -1); + } +} + +function reloadES6() { + window.rootPath = document.currentScript.src.replace(/lua-playground\/lua-playground-loader\.js.*/, '') + const injectScript = document.createElement('script'); + injectScript.type = 'module'; + injectScript.src = document.currentScript.src; + document.body.append(injectScript); +} + +// this script is not loaded as es6 module, so it has to "elevate" itself to an es6 module by re-injecting itself with +// the `reloadES6` function +if (window.rootPath) { + main() +} else { + reloadES6() +} diff --git a/lua-playground/src/lib.rs b/lua-playground/src/lib.rs new file mode 100644 index 0000000..245f916 --- /dev/null +++ b/lua-playground/src/lib.rs @@ -0,0 +1,18 @@ +use std::ffi::{c_char, CStr}; +use mlua::Lua; + +#[no_mangle] +pub extern "C" fn lua_new() -> *mut Lua { + let lua = Lua::new(); + Box::into_raw(Box::new(lua)) +} + +#[no_mangle] +pub unsafe extern "C" fn lua_execute(lua: *mut Lua, to_execute: *const c_char) { + let lua: &mut Lua = &mut *lua; + let to_execute = CStr::from_ptr(to_execute); + + if let Err(err) = lua.load(&to_execute.to_string_lossy().to_string()).exec() { + eprintln!("{}", err) + } +} diff --git a/src/SUMMARY.md b/src/SUMMARY.md new file mode 100644 index 0000000..b428a22 --- /dev/null +++ b/src/SUMMARY.md @@ -0,0 +1,13 @@ +# Summary + +[Introduction](./introduction.md) + +- [Setup](./setup.md) + +- [Tutorial](tutorial/introduction.md) + - [Creating a project](tutorial/creating-a-project.md) + - [Adding wasm logic](tutorial/adding-wasm-logic.md) + - [Compiling](tutorial/compiling.md) + - [Calling from Javascript](tutorial/calling-from-javascript.md) + +- [Testing](./testing.md) diff --git a/src/introduction.md b/src/introduction.md new file mode 100644 index 0000000..2aba1f3 --- /dev/null +++ b/src/introduction.md @@ -0,0 +1,11 @@ +# Lua in the Browser, with Rust 🦀 and WebAssembly + +This smol book describes how to use Lua in the Browser, powered by Rust WebAssembly. + +> You should have basic knowledge of Rust, Rust FFI and Javascript, the book will not explain language features or constructs that are irrelevant to Rust WebAssembly. + +--- + +```lua,editable +print("Hello from WebAssembly Lua!") +``` diff --git a/src/setup.md b/src/setup.md new file mode 100644 index 0000000..4052bae --- /dev/null +++ b/src/setup.md @@ -0,0 +1,31 @@ +# Setup + +Before we can start developing, a few prerequisites must be fulfilled. + +## The Rust toolchain + +```shell +rustup target add wasm32-unknown-emscripten +``` + +## The Emscripten compiler + +To build for the `wasm32-unknown-emscripten` target, you need the [emscripten](https://emscripten.org/) compiler toolchain. + +General install instructions are available [here](https://emscripten.org/docs/getting_started/downloads.html) or you look if your package manager has an emscripten package (some examples provided below). + +_Debian_ +```shell +sudo apt install emscripten +``` + +_Arch Linux_ +```shell +sudo pacman -S emscripten + +# arch does not add the path to the emscripten executables to PATH, so it must be +# explicitly added. +# you probably want to add this to your bashrc (or any other file which permanently +# adds this to PATH) to make it permanently available +export PATH=$PATH:/usr/lib/emscripten +``` diff --git a/src/testing.md b/src/testing.md new file mode 100644 index 0000000..0c448fc --- /dev/null +++ b/src/testing.md @@ -0,0 +1,32 @@ +# Testing + +Testing is not very different from testing any other ordinary Rust crate. + +When running tests, Rust tries to execute the generated Javascript glue directly which will result in an error. +You have to specify the test runner which executes the Javascript, either in the `.cargo/config.toml` file (described [here]()) or via the `CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER` env variable to `node --experimental-default-type=module`. +
+If your crate is a library, you also have to remove the `-o.js` compiler option as it modifies the output filename which the Rust test suite can't track. +Because the `test` subcommand compiles the tests as normal binaries, the Emscripten compiler automatically creates the js glue. + +> Also, in the current stable Rust, you have to set the `-sERROR_ON_UNDEFINED_SYMBOLS=0` compiler option in order to avoid test compilation errors. This is due to an incompatibility between emscripten and the internal Rust libc crate ([rust-lang/rust#116655](https://github.com/rust-lang/rust/issues/116655)) but a fix for it should land in Rust 1.75 ([rust-lang/rust#116527](https://github.com/rust-lang/rust/pull/116527)). +> Alternatively you can use the nightly toolchain, the fix is already present there. + +With this done, we can create a simple test: +```rust,ignore +#[cfg(test)] +mod tests { + #[test] + fn lua_test() { + let lua = mlua::Lua::new(); + lua.load("print(\"test\")").exec().unwrap(); + } +} + +``` + +And then run it: +```shell +# you can omit '--target wasm32-unknown-emscripten' if you added the .cargo/config.toml +# file as describe in the "Setup" section +cargo test --target wasm32-unknown-emscripten +``` diff --git a/src/tutorial/adding-wasm-logic.md b/src/tutorial/adding-wasm-logic.md new file mode 100644 index 0000000..f126e28 --- /dev/null +++ b/src/tutorial/adding-wasm-logic.md @@ -0,0 +1,37 @@ +# Adding wasm logic + +Adding logic on the wasm / Rust side is very much just like writing a (C compatible) shared library. + +Let's begin simple. +This function creates a [Lua](https://docs.rs/mlua/latest/mlua/struct.Lua.html) instance and returns the raw pointer to it. +```rust,ignore +#[no_mangle] +pub extern "C" fn lua_new() -> *mut mlua::Lua { + let lua = mlua::Lua::new(); + Box::into_raw(Box::new(lua)) +} +``` + +Alright, good. +Now we have a Lua instance, but no way to use it, so let us create one. +
+The function takes the pointer to the Lua struct we create in the `new_lua` function as well as an arbitrary string, which should be lua code, as parameters. +It then executes this string via the Lua instance and may write to `stderr` if an error occurs. +```rust,ignore +#[no_mangle] +pub unsafe extern "C" fn lua_execute(lua: *mut mlua::Lua, to_execute: *const std::ffi::c_char) { + // casting the raw pointer of the created lua instance back to a usable Rust struct + let lua: &mut mlua::Lua = &mut *lua; + // converting the c string into a `CStr` (which then can be converted to a `String`) + let to_execute = std::ffi::CStr::from_ptr(to_execute); + + // execute the input code via the lua interpreter + if let Err(err) = lua.load(&to_execute.to_string_lossy().to_string()).exec() { + // because emscripten wraps stderr, we are able to catch the error on the js + // side just fine + eprintln!("{}", err) + } +} +``` + +Okay, this looks great! In theory. So let's head over to the next page to see how to compile the code to make it actually usable via Javascript. diff --git a/src/tutorial/calling-from-javascript.md b/src/tutorial/calling-from-javascript.md new file mode 100644 index 0000000..7fab8f5 --- /dev/null +++ b/src/tutorial/calling-from-javascript.md @@ -0,0 +1,189 @@ +# Calling from Javascript + +> The following code examples are expecting that the compiled glue and wasm files are available as `target/wasm32-unknown-emscripten/debug/my-project.js` and `target/wasm32-unknown-emscripten/debug/my-project.wasm`. + +## Browser + +> Note that opening the `.html` file as normal file in your browser will prevent the wasm from loading. +> You have to serve it with a webserver. `python3 -m http.server` is a good tool for this. + +The following html page will be used as reference in the Javascript code. +```html + + + + My Project + + +
+

Code

+ + +
+
+
+

Stderr

+
+
+
+
+

Stdout

+
+
+
+ + +``` + +First things first, we need to load the compiled wasm file. +For this, we import the Javascript glue that is generated when compiling and loads and configures the actual wasm file. +A custom configuration is fully optional, but needed if you want to do things like catching stdio. +The configuration is done via the [Module](https://emscripten.org/docs/api_reference/module.html) object. +```javascript +// importing the glue +const wasm = await import('./target/wasm32-unknown-emscripten/debug/my-project.js'); +// creating a custom configuration. `print` is equal to stdout, `printErr` is equal to +// stderr +const module = { + print(str) { + const stdout = document.getElementById('stdout'); + const line = document.createElement('p'); + line.innerText = str; + stdout.appendChild(line); + }, + printErr(str) { + const stderr = document.getElementById('stderr'); + const line = document.createElement('p'); + line.innerText = str; + stderr.appendChild(line); + } +}; +// this loads the wasm file and exposes the `ccall` and `cwrap` functions whic we'll +// use in the following code +const myProject = await wasm.default(module); +``` + +With the library loaded, it's time to call our first function, `lua_new`. +This is done via the emscripten [ccall](https://emscripten.org/docs/api_reference/preamble.js.html#ccall) function. +It takes the function name we want to execute, its return type, the function parameter types and the parameters as arguments. +
+This will return the raw pointer (as js number) to the address where the [Lua](https://docs.rs/mlua/latest/mlua/struct.Lua.html) struct, we created in the Rust code, resides. +```javascript +const luaInstance = myProject.ccall('lua_new', 'number', [], []); +``` + +Next up, lets make the `lua_execute` function callable. +This time we're using the emscripten [cwrap](https://emscripten.org/docs/api_reference/preamble.js.html#cwrap) function. +It wraps a normal Javascript function around the ffi call to the wasm `lua_execute` function, which is the recommended way to handle functions which are invoked multiple times. +It takes the function name we want to execute, its return type and the function parameters as arguments. +```javascript +const luaExecute = myProject.cwrap('lua_execute', null, ['number', 'string']); +``` + +With this all set up, we are able to call any Lua code via WebAssembly, right in the browser. Great! +```javascript +luaExecute(luaInstance, 'print("Hello Lua Wasm")'); +``` + +
+ Full example as html page with Javascript + + ```html + + + + My Project + + + +
+ + +
+
+
+

Stderr

+
+
+
+
+

Stdout

+
+
+
+ + +``` +
+ +## NodeJS + +> The nodejs implementation is not very different from the browser implementation, so the actions done aren't as detailed described as above. +Please read the [Browser](#browser) section first if you want more detailed information. + +```javascript +class MyProject { + #instance; + #luaExecute; + #stdout; + #stderr; + + static async init(): Promise { + const myProject = new MyProject(); + + const wasm = await import('./target/wasm32-unknown-emscripten/debug/my-project.js'); + const module = { + print(str) { + if (myProject.#stdout) myProject.#stdout(str); + }, + printErr(str) { + if (myProject.#stderr) myProject.#stderr(str); + } + }; + const lib = await wasm.default(module); + + myProject.#instance = lib.ccall('lua_new', 'number', [], []); + myProject.#luaExecute = lib.cwrap('lua_execute', null, ['number', 'string']); + + return myProject; + } + + execute(code, stdout, stderr) { + if (stdout) this.#stdout = stdout; + if (stderr) this.#stderr = stderr; + + this.#luaExecute(this.#instance, code); + + if (stdout) this.#stdout = null; + if (stderr) this.#stderr = null; + } +} +``` diff --git a/src/tutorial/compiling.md b/src/tutorial/compiling.md new file mode 100644 index 0000000..9d89ced --- /dev/null +++ b/src/tutorial/compiling.md @@ -0,0 +1,8 @@ +# Compiling + +Before we can use our Rust code, we have to compile it first. +```shell +# you can omit '--target wasm32-unknown-emscripten' if you added the .cargo/config.toml +# file as describe in the "Setup" section +cargo build --target wasm32-unknown-emscripten +``` diff --git a/src/tutorial/creating-a-project.md b/src/tutorial/creating-a-project.md new file mode 100644 index 0000000..a01eb7a --- /dev/null +++ b/src/tutorial/creating-a-project.md @@ -0,0 +1,98 @@ +# Creating a project + +## Create the project package + +First, you need to create a normal Rust package. +This can either be a binary or library crate, they are working nearly the same. + +As binary: +```shell +cargo init --bin my-package . +``` + +As library +```shell +cargo init --lib my-package . +``` + +## Configure files + +Before you can start writing actual code you have to set up some files in the newly created library directory. + +### `Cargo.toml` + +The `mlua` dependency is the actual lua library which we'll use. +The features `lua51`, `lua53`, `lua54` and `luau` are wasm compatible lua version (`lua53` is currently broken because I accidentally removed a function in the PR which added wasm support, oops). +```toml +[package] +name = "my-project" +version = "0.1.0" +edition = "2021" + +[dependencies] +mlua = { git = "https://github.com/khvzak/mlua.git", features = ["lua51"] } +``` + +> If your crate is a library, you have to additionally add this: +> ```toml +> [lib] +> crate-type = ["cdylib"] +> ``` +> This must be done because the emscripten compiler expects the package to behave like a normal C shared library. + + +### `build.rs` + +You need to set some additional compiler options to be able to call your wasm code from Javascript: +- `-sEXPORTED_RUNTIME_METHODS=['cwrap','ccall']`: this exports the `cwrap` and `ccall` Javascript functions which allows us to call our library methods +- `-sEXPORT_ES6=1`: this makes the created js glue ES6 compatible. It is not mandatory in general but needed as this tutorial/examples utilizes ES6 imports +- `-sERROR_ON_UNDEFINED_SYMBOLS=0` (_optional for binary crates_): this ignores undefined symbols. Typically undefined symbols are not really undefined but the linker just can't find them, which is always the case if your crate is a library + +> If your package is a library, you have to add some additional options: +> - `--no-entry`: this defines that the compiled wasm has no main function +> - `-o.js`: by default, only a `.wasm` file is created, but some js glue is needed to call the built wasm file (and the wasm file needs some functions of the glue too). This creates the glue `.js` file and changes the name of the wasm output file to `.wasm`. This must be removed when running tests because it changes the output filename and the Rust test suite can't track this + +The best way to do this is by specifying the args in a `build.rs` file which guarantees that they are set when compiling: +```rust,ignore +fn main() { + println!("cargo:rustc-link-arg=-sEXPORTED_RUNTIME_METHODS=['cwrap','ccall']"); + println!("cargo:rustc-link-arg=-sEXPORT_ES6=1"); +} +``` + +> If your package is a library, add the additionally required options to your `build.rs`: +> ```rust,ignore +> let out_dir = std::env::var("OUT_DIR").unwrap(); +> let pkg_name = std::env::var("CARGO_PKG_NAME").unwrap(); +> +> // the output files should be placed in the "root" build directory (e.g. +> // target/wasm32-unknown-emscripten/debug) but there is no env variable which +> // provides this path, so it must be extracted this way +> +> let target_path = std::path::PathBuf::from(out_dir) +> .parent() +> .unwrap() +> .parent() +> .unwrap() +> .parent() +> .unwrap() +> .join(pkg_name); +> +> println!("cargo:rustc-link-arg=-sERROR_ON_UNDEFINED_SYMBOLS=0"); +> println!("cargo:rustc-link-arg=--no-entry"); +> println!("cargo:rustc-link-arg=-o{}.js", target_path.to_string_lossy()); +> ``` + +### `.cargo/config.toml` (optional) + +Here you can set the default target to `wasm32-unknown-emscripten`, so you don't have to specify the `--target wasm32-unknown-emscripten` flag everytime you want to compile your project. +
+You can also set the default runner binary here which is useful when running tests, as Rust tries to execute the generated js glue directly which obviously doesn't work because a Javascript file is not an executable. + +```toml +[build] +target = "wasm32-unknown-emscripten" + +[target.wasm32-unknown-emscripten] +runner = "node --experimental-default-type=module" +``` diff --git a/src/tutorial/introduction.md b/src/tutorial/introduction.md new file mode 100644 index 0000000..55d7c7b --- /dev/null +++ b/src/tutorial/introduction.md @@ -0,0 +1,9 @@ +# Tutorial + +This tutorial covers how to set up a simple project, adding logic to it and calling it from a Javascript (browser) application. + +We will build a simple wasm binary which is able to execute arbitrary Lua input (a repl, basically). + +## What will be covered? +- How to set up a project (it's a bit more than just `cargo init`) +- Calling the created wasm file from the browser