mirror of
https://github.com/bytedream/litbwraw.git
synced 2025-06-27 18:30:32 +02:00
initial commit
This commit is contained in:
13
src/SUMMARY.md
Normal file
13
src/SUMMARY.md
Normal 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
11
src/introduction.md
Normal 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
31
src/setup.md
Normal 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
32
src/testing.md
Normal 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
|
||||
```
|
37
src/tutorial/adding-wasm-logic.md
Normal file
37
src/tutorial/adding-wasm-logic.md
Normal 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.
|
189
src/tutorial/calling-from-javascript.md
Normal file
189
src/tutorial/calling-from-javascript.md
Normal 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;
|
||||
}
|
||||
}
|
||||
```
|
8
src/tutorial/compiling.md
Normal file
8
src/tutorial/compiling.md
Normal 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
|
||||
```
|
98
src/tutorial/creating-a-project.md
Normal file
98
src/tutorial/creating-a-project.md
Normal 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"
|
||||
```
|
9
src/tutorial/introduction.md
Normal file
9
src/tutorial/introduction.md
Normal 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
|
Reference in New Issue
Block a user