initial commit

This commit is contained in:
bytedream 2023-12-17 20:17:55 +01:00
commit 96adaf3211
19 changed files with 1067 additions and 0 deletions

51
.github/workflows/deploy.yml vendored Normal file
View File

@ -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

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.idea
.vscode
book
lua-playground/target

21
LICENSE Normal file
View File

@ -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.

38
README.md Normal file
View File

@ -0,0 +1,38 @@
<div align="center">
<h1>Lua in the Browser, with Rust and WebAssembly</h1>
<strong>This smol book describes how to use Lua in the Browser, powered by Rust WebAssembly.</strong>
<h3><a href="https://bytedream.github.io/litbwraw/">Read the book</a></h3>
</div>
## 🛠 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.

13
book.toml Normal file
View File

@ -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

373
lua-playground/Cargo.lock generated Normal file
View File

@ -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"

10
lua-playground/Cargo.toml Normal file
View File

@ -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"] }

23
lua-playground/build.rs Normal file
View File

@ -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());
}

View File

@ -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 + '<br>';
luaStderr = (msg) => result.innerHTML += msg + '<br>';
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()
}

18
lua-playground/src/lib.rs Normal file
View File

@ -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)
}
}

13
src/SUMMARY.md Normal file
View File

@ -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)

11
src/introduction.md Normal file
View File

@ -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!")
```

31
src/setup.md Normal file
View File

@ -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
```

32
src/testing.md Normal file
View File

@ -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`.
<br>
If your crate is a library, you also have to remove the `-o<library name>.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
```

View File

@ -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.
<br>
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.

View File

@ -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
<!DOCTYPE html>
<html>
<head>
<title>My Project</title>
</head>
<body>
<div>
<h3>Code</h3>
<textarea id="code"></textarea>
<button>Execute</button>
</div>
<div style="display: flex">
<div>
<h3>Stderr</h3>
<div id="stderr" />
</div>
<hr>
<div>
<h3>Stdout</h3>
<div id="stdout" />
</div>
</div>
</body>
</html>
```
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.
<br>
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")');
```
<details>
<summary>Full example as html page with Javascript</summary>
```html
<!DOCTYPE html>
<html>
<head>
<title>My Project</title>
<script type="module">
const wasm = await import('./target/wasm32-unknown-emscripten/debug/my-project.js');
const stdout = document.getElementById('stdout');
const stderr = document.getElementById('stderr');
const module = {
print(str) {
const line = document.createElement('p');
line.innerText = str;
stdout.appendChild(line);
},
printErr(str) {
const line = document.createElement('p');
line.innerText = str;
stderr.appendChild(line);
}
};
const myProject = await wasm.default(module);
const luaInstance = myProject.ccall('lua_new', 'number', [], []);
const luaExecute = myProject.cwrap('lua_execute', null, ['number', 'string']);
window.execute = () => {
// clear the output
stdout.innerHTML = '';
stderr.innerHTML = '';
const code = document.getElementById('code').value;
luaExecute(luaInstance, code);
}
</script>
</head>
<body>
<div>
<textarea id="code"></textarea>
<button onclick="execute()">Execute</button>
</div>
<div style="display: flex">
<div>
<h3>Stderr</h3>
<div id="stderr" />
</div>
<hr>
<div>
<h3>Stdout</h3>
<div id="stdout" />
</div>
</div>
</body>
</html>
```
</details>
## 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<MyProject> {
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;
}
}
```

View File

@ -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
```

View File

@ -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<library name>.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 `<library name>.js` file and changes the name of the wasm output file to `<library name>.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.
<br>
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"
```

View File

@ -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