From fd5e726daebfdf28b00df5c4a5d6a8be07a483df Mon Sep 17 00:00:00 2001 From: Lokathor Date: Sun, 17 Jan 2021 19:22:12 -0700 Subject: [PATCH 01/11] starting the web. --- book_src/SUMMARY.md | 2 ++ book_src/web_nonsense/index.md | 25 +++++++++++++++++ .../web_nonsense/web_gl_with_bare_wasm.md | 28 +++++++++++++++++++ web_crate/Cargo.toml | 8 ++++++ web_crate/src/lib.rs | 7 +++++ 5 files changed, 70 insertions(+) create mode 100644 book_src/web_nonsense/index.md create mode 100644 book_src/web_nonsense/web_gl_with_bare_wasm.md create mode 100644 web_crate/Cargo.toml create mode 100644 web_crate/src/lib.rs diff --git a/book_src/SUMMARY.md b/book_src/SUMMARY.md index 66891db..84959ee 100644 --- a/book_src/SUMMARY.md +++ b/book_src/SUMMARY.md @@ -6,5 +6,7 @@ * [Win32 Cleanup](opening_a_window/win32_cleanup.md) * [Loading OpenGL](loading_opengl/index.md) * [Win32](loading_opengl/win32.md) +* [Web Nonsense](web_nonsense/index.md) + * [WebGL with bare WASM](web_nonsense/web_gl_with_bare_wasm.md) * [Appendix](appendix/index.md) * [UTF-16 Literals](appendix/utf16_literals.md) diff --git a/book_src/web_nonsense/index.md b/book_src/web_nonsense/index.md new file mode 100644 index 0000000..865e01d --- /dev/null +++ b/book_src/web_nonsense/index.md @@ -0,0 +1,25 @@ + +# Web Nonsense + +The internet is nonsense, and it's all a pile of crazy. +In fact, all of computing is a mistake. +Teaching rocks to think!? +Of course it went badly. + +That said, people really like to run stuff in the browser. +It's very nice to end users if they can just open a web page and not have to install a whole thing. + +If you want to run Rust code in a browser you compile it to [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly), +or WASM for short, +which is an output target the same as compiling for windows x86_64, +or linux arm, +or any other target. + +Even then, Wasm is strongly sandboxed, and it cannot directly interact with the world. +Not only do you have to bind to some external functions on the Rust side, +you have to *write those eternal functions yourself* in javascript. +This is a bit of a bother, +and so, +*for this one target platform*, +we'll first see how to do it ourselves, +and then we'll see how to leverage the most common crate for targeting wasm. diff --git a/book_src/web_nonsense/web_gl_with_bare_wasm.md b/book_src/web_nonsense/web_gl_with_bare_wasm.md new file mode 100644 index 0000000..3df745c --- /dev/null +++ b/book_src/web_nonsense/web_gl_with_bare_wasm.md @@ -0,0 +1,28 @@ + +# Web GL with bare Wasm + +## Toolchain Setup + +Before we even begin, we'll need to take a few extra steps to have the right compiler and tools available. + +In addition to having Rust installed, we need to install the `wasm32-unknown-unknown` target: + +> rustup target add wasm32-unknown-unknown + +In addition, you may wish to obtain the `wasm-opt` tool from their [GitHub repo](https://github.com/WebAssembly/binaryen), +though it's not required. + +You also might wish to obtain the `wasm-strip` tool from [The WebAssembly Binary Toolkit](https://github.com/WebAssembly/wabt) (WABT). +It lets you strip debugging symbols and such from the program, reducing the size by quite a bit. +You can also do this without an extra tool via a Nightly `rustc` flag. + +Once you've compiled your program to wasm you'll also need some way to display it. +Unfortunately, you can't simply open a local file in your browser using a `file://` address. +This is fine for a plain HTML file, +but browsers (rightly) get more paranoid every day, +so they don't support wasm execution in pages loaded through a file address. +If you have your own favorite way to spin up a local server that can serve static files that's fine. +If you don't already have such a thing (I didn't), then you can try [devserver](https://crates.io/crates/devserver). + +> cargo install devserver + diff --git a/web_crate/Cargo.toml b/web_crate/Cargo.toml new file mode 100644 index 0000000..fe4092d --- /dev/null +++ b/web_crate/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "triangle-from-scratch-web-crate" +version = "0.1.0" +authors = ["Lokathor "] +edition = "2018" +license = "Zlib OR Apache-2.0 OR MIT" + +[dependencies] diff --git a/web_crate/src/lib.rs b/web_crate/src/lib.rs new file mode 100644 index 0000000..74be4bb --- /dev/null +++ b/web_crate/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} From 93d12464672477ea58297d670a13d8dbb68070f9 Mon Sep 17 00:00:00 2001 From: Lokathor Date: Sun, 17 Jan 2021 22:15:24 -0700 Subject: [PATCH 02/11] expand the gitignore. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 2139fb8..416ddd1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ # Added by cargo /target + +web_crate/target/ From 770e37de58bc13fab8dc0e24e2031662634c6d7c Mon Sep 17 00:00:00 2001 From: Lokathor Date: Sun, 17 Jan 2021 22:15:36 -0700 Subject: [PATCH 03/11] clear the canvas to blue. --- book_src/SUMMARY.md | 4 +- .../web_nonsense/web_gl_with_bare_wasm.md | 28 -- book_src/{web_nonsense => web_stuff}/index.md | 7 +- book_src/web_stuff/web_gl_with_bare_wasm.md | 249 ++++++++++++++++++ web_crate/Cargo.lock | 5 + web_crate/Cargo.toml | 6 +- web_crate/index.html | 39 +++ web_crate/src/lib.rs | 17 +- 8 files changed, 313 insertions(+), 42 deletions(-) delete mode 100644 book_src/web_nonsense/web_gl_with_bare_wasm.md rename book_src/{web_nonsense => web_stuff}/index.md (78%) create mode 100644 book_src/web_stuff/web_gl_with_bare_wasm.md create mode 100644 web_crate/Cargo.lock create mode 100644 web_crate/index.html diff --git a/book_src/SUMMARY.md b/book_src/SUMMARY.md index 84959ee..594fdda 100644 --- a/book_src/SUMMARY.md +++ b/book_src/SUMMARY.md @@ -6,7 +6,7 @@ * [Win32 Cleanup](opening_a_window/win32_cleanup.md) * [Loading OpenGL](loading_opengl/index.md) * [Win32](loading_opengl/win32.md) -* [Web Nonsense](web_nonsense/index.md) - * [WebGL with bare WASM](web_nonsense/web_gl_with_bare_wasm.md) +* [Web Stuff](web_stuff/index.md) + * [WebGL with bare WASM](web_stuff/web_gl_with_bare_wasm.md) * [Appendix](appendix/index.md) * [UTF-16 Literals](appendix/utf16_literals.md) diff --git a/book_src/web_nonsense/web_gl_with_bare_wasm.md b/book_src/web_nonsense/web_gl_with_bare_wasm.md deleted file mode 100644 index 3df745c..0000000 --- a/book_src/web_nonsense/web_gl_with_bare_wasm.md +++ /dev/null @@ -1,28 +0,0 @@ - -# Web GL with bare Wasm - -## Toolchain Setup - -Before we even begin, we'll need to take a few extra steps to have the right compiler and tools available. - -In addition to having Rust installed, we need to install the `wasm32-unknown-unknown` target: - -> rustup target add wasm32-unknown-unknown - -In addition, you may wish to obtain the `wasm-opt` tool from their [GitHub repo](https://github.com/WebAssembly/binaryen), -though it's not required. - -You also might wish to obtain the `wasm-strip` tool from [The WebAssembly Binary Toolkit](https://github.com/WebAssembly/wabt) (WABT). -It lets you strip debugging symbols and such from the program, reducing the size by quite a bit. -You can also do this without an extra tool via a Nightly `rustc` flag. - -Once you've compiled your program to wasm you'll also need some way to display it. -Unfortunately, you can't simply open a local file in your browser using a `file://` address. -This is fine for a plain HTML file, -but browsers (rightly) get more paranoid every day, -so they don't support wasm execution in pages loaded through a file address. -If you have your own favorite way to spin up a local server that can serve static files that's fine. -If you don't already have such a thing (I didn't), then you can try [devserver](https://crates.io/crates/devserver). - -> cargo install devserver - diff --git a/book_src/web_nonsense/index.md b/book_src/web_stuff/index.md similarity index 78% rename from book_src/web_nonsense/index.md rename to book_src/web_stuff/index.md index 865e01d..7e3b240 100644 --- a/book_src/web_nonsense/index.md +++ b/book_src/web_stuff/index.md @@ -1,12 +1,7 @@ # Web Nonsense -The internet is nonsense, and it's all a pile of crazy. -In fact, all of computing is a mistake. -Teaching rocks to think!? -Of course it went badly. - -That said, people really like to run stuff in the browser. +People really like to run stuff in the browser. It's very nice to end users if they can just open a web page and not have to install a whole thing. If you want to run Rust code in a browser you compile it to [WebAssembly](https://en.wikipedia.org/wiki/WebAssembly), diff --git a/book_src/web_stuff/web_gl_with_bare_wasm.md b/book_src/web_stuff/web_gl_with_bare_wasm.md new file mode 100644 index 0000000..f11724d --- /dev/null +++ b/book_src/web_stuff/web_gl_with_bare_wasm.md @@ -0,0 +1,249 @@ + +# Web GL with bare Wasm + +Before we begin I should give a big thanks to [kettle11](https://github.com/kettle11), +who made the [hello_triangle_wasm_rust](https://github.com/kettle11/hello_triangle_wasm_rust) example for me. + +## Toolchain Setup + +Before we even begin, we'll need to take a few extra steps to have the right compiler and tools available. + +In addition to having Rust installed, we need to install the `wasm32-unknown-unknown` target: + +> rustup target add wasm32-unknown-unknown + +In addition, you may wish to obtain the `wasm-opt` tool from their [GitHub repo](https://github.com/WebAssembly/binaryen), +though it's not required. + +You also might wish to obtain the `wasm-strip` tool from [The WebAssembly Binary Toolkit](https://github.com/WebAssembly/wabt) (WABT). +It lets you strip debugging symbols and such from the program, reducing the size by quite a bit. +You can also do this without an extra tool via a Nightly `rustc` flag. + +Once you've compiled your program to wasm you'll also need some way to display it. +Unfortunately, you can't simply open a local file in your browser using a `file://` address. +This is fine for a plain HTML file, +but browsers (rightly) get more paranoid every day, +so they don't support wasm execution in pages loaded through a file address. +If you don't already have such a thing (I didn't), then you can try [devserver](https://crates.io/crates/devserver). + +> cargo install devserver + +If you already have your own favorite way to spin up a local server that can serve static files, that's fine too. + +## Separate Folder + +This will have a new non-standard requirements, +so I'm going to put it in a `web_crate/` directory. + +First it needs its own `Cargo.toml` file: +```toml +[package] +name = "triangle-from-scratch-web-crate" +version = "0.1.0" +authors = ["Lokathor "] +edition = "2018" +license = "Zlib OR Apache-2.0 OR MIT" +``` + +Now also, to make a wasm library like we need we have to tell Rust that the [crate-type](https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-crate-type-field) +will be `cdylib`: +```toml +[lib] +crate-type = ["cdylib"] +``` + +Personally I also like to turn on [link-time optimization](https://doc.rust-lang.org/cargo/reference/profiles.html#lto) with release builds, +not because it's required, +but just because I'm willing to spend some extra compile time to get a performance edge. +The winner here is "thin", +which provides almost all the benefit for a minimal amount of additional time and memory taken to compile. + +```toml +[profile.release] +lto = "thin" +``` + +Now we're set. + +## The Wasm Library + +As you can sorta already see, our "program" isn't actually going to be built as an executable. +Instead, it's going to be built as a C-compatible library that the JavaScript of the webpage will load and use. +This means that instead of writing a `main.rs` with an optional `lib.rs`, +we put 100% of the code into `lib.rs` right from the start. + +```rust +// lib.rs + +#[no_mangle] +pub extern "C" fn start() { + // nothing yet! +} +``` + +Note the use of the [no_mangle](https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute) attribute. +This totally disables the usual name mangling that Rust does. +It allows for the function to be called by external code that doesn't know Rust's special naming scheme, which is good, +but there can only be a single function with a given name anywhere. +In other words, if some other function named `start` with no mangling existed *anywhere* in our project, +or in any of our dependencies, then we'd get a compilation error. +That's why name mangling is on by default. + +Also note that we have to declare that our `start` function uses the `extern "C"` ABI, +this will give us the correct calling convention when communicating between JavaScript and Wasm. + +When JavaScript loads up our wasm module, the `start` function will be called. +This will allow our program to do whatever it wants to do, +similar to the `main` function in a normal program. + +## The Web Page + +Okay now we need a webpage for the user to display and have the wasm go. + +I'm absolutely not a web development person, +but I know just enough to throw some HTML together by hand: + +```html + + + + Hello. + + +``` + +Next we start the local server and go to the page. +``` +D:\dev\triangle-from-scratch>cd web_crate + +D:\dev\triangle-from-scratch\web_crate>devserver + +Serving [D:\dev\triangle-from-scratch\web_crate\] at [ https://localhost:8080 ] or [ http://localhost:8080 ] +Automatic reloading is enabled! +Stop with Ctrl+C +``` +And it says "Hello." in the middle of the page. +We'll just leave that open in one console and it'll automatically reload files as necessary. + +Now we build our wasm module (note the `--target` argument): +``` +D:\dev\triangle-from-scratch\web_crate>cargo build --release --target wasm32-unknown-unknown + Compiling triangle-from-scratch-web-crate v0.1.0 (D:\dev\triangle-from-scratch\web_crate) + Finished release [optimized] target(s) in 1.16s +``` + +which makes a file: `target/wasm32-unknown-unknown/release/triangle_from_scratch_web_crate.wasm` + +(If we hadn't used the `--release` flag, then it'd be in `target/wasm32-unknown-unknown/debug/` instead.) + +Now we have to alter our page to load the wasm via a script: +```html + + + + Hello. + + + + +``` + +What's going on here? +Well, you should sure read the [Loading and running WebAssembly code](https://developer.mozilla.org/en-US/docs/WebAssembly/Loading_and_running) +tutorial on the Mozilla Developer Network (MDN) page. + +* First we call [WebAssembly.instantiateStreaming()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming) + * The first argument is whatever will give us the wasm stream. + In this case, a call to [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + * The second argument is the "import object", which lets us provide things to the wasm code. + At the moment we don't provide anything to the wasm, so we use an empty object. +* This gives a `Promise`, so we use the `then` method to do something to the results. + It's similar to Rust's async/await and Future stuff. + Except it's not quite the same, they tell me. + I don't really know JavaScript, but I'm kinda just nodding and smiling as we go. +* When acting on the results, + `results.module` is the [web assembly module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module) + and `results.instance` is the [web assembly instance](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Instance). + The module isn't too helpful to us right now, + but by using the instance we can call our `start` function (or any other non-mangled public function). + +## Make The Wasm Do Something + +It's not too exciting for nothing to happen. +Let's have the wasm clear the canvas to a non-white color. + +First we expand the script on the web page. +What we need to do is give the wasm code some functions to let it interact with the outside world. +```html + +``` + +Now our `importObject` has an `env` field. +Each function declared in here will be accessible to the wasm as an external function. +One of them sets up the canvas and WebGL context. +The other clears the canvas to a nice blue color. + +Now we can call these from the Rust code: +```rust +mod js { + extern "C" { + pub fn setup_canvas(); + pub fn clear_to_blue(); + } +} + +#[no_mangle] +pub extern "C" fn start() { + unsafe { + js::setup_canvas(); + js::clear_to_blue(); + } +} +``` + +And we'll see a blue canvas! + +## Drawing A Triangle + +We need a little more wasm/js interaction than that to make a triangle. + +TODO diff --git a/web_crate/Cargo.lock b/web_crate/Cargo.lock new file mode 100644 index 0000000..18a754a --- /dev/null +++ b/web_crate/Cargo.lock @@ -0,0 +1,5 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "triangle-from-scratch-web-crate" +version = "0.1.0" diff --git a/web_crate/Cargo.toml b/web_crate/Cargo.toml index fe4092d..eb708e8 100644 --- a/web_crate/Cargo.toml +++ b/web_crate/Cargo.toml @@ -5,4 +5,8 @@ authors = ["Lokathor "] edition = "2018" license = "Zlib OR Apache-2.0 OR MIT" -[dependencies] +[lib] +crate-type = ["cdylib"] + +[profile.release] +lto = "thin" diff --git a/web_crate/index.html b/web_crate/index.html new file mode 100644 index 0000000..7da1baf --- /dev/null +++ b/web_crate/index.html @@ -0,0 +1,39 @@ + + + + Hello. + + + + \ No newline at end of file diff --git a/web_crate/src/lib.rs b/web_crate/src/lib.rs index 74be4bb..bb49fcb 100644 --- a/web_crate/src/lib.rs +++ b/web_crate/src/lib.rs @@ -1,7 +1,14 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); +mod js { + extern "C" { + pub fn setup_canvas(); + pub fn clear_to_blue(); + } +} + +#[no_mangle] +pub extern "C" fn start() { + unsafe { + js::setup_canvas(); + js::clear_to_blue(); } } From cfe95d9569a0171db98002c683dec5da971130fe Mon Sep 17 00:00:00 2001 From: Lokathor Date: Sun, 17 Jan 2021 22:27:18 -0700 Subject: [PATCH 04/11] tool advice. --- book_src/web_stuff/web_gl_with_bare_wasm.md | 46 +++++++++++++++++---- web_crate/.cargo/config.toml | 2 + web_crate/index.html | 8 ++-- web_crate/src/lib.rs | 8 ++-- 4 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 web_crate/.cargo/config.toml diff --git a/book_src/web_stuff/web_gl_with_bare_wasm.md b/book_src/web_stuff/web_gl_with_bare_wasm.md index f11724d..f55435d 100644 --- a/book_src/web_stuff/web_gl_with_bare_wasm.md +++ b/book_src/web_stuff/web_gl_with_bare_wasm.md @@ -187,7 +187,7 @@ What we need to do is give the wasm code some functions to let it interact with var gl; var canvas; - function setup_canvas() { + function setupCanvas() { console.log("Setting up the canvas..."); let canvas = document.getElementById("my_canvas"); gl = canvas.getContext("webgl"); @@ -197,15 +197,15 @@ What we need to do is give the wasm code some functions to let it interact with } } - function clear_to_blue() { + function clearToBlue() { gl.clearColor(0.1, 0.1, 0.9, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); } var importObject = { env: { - setup_canvas: setup_canvas, - clear_to_blue: clear_to_blue, + setupCanvas: setupCanvas, + clearToBlue: clearToBlue, } }; @@ -226,22 +226,52 @@ Now we can call these from the Rust code: ```rust mod js { extern "C" { - pub fn setup_canvas(); - pub fn clear_to_blue(); + pub fn setupCanvas(); + pub fn clearToBlue(); } } #[no_mangle] pub extern "C" fn start() { unsafe { - js::setup_canvas(); - js::clear_to_blue(); + js::setupCanvas(); + js::clearToBlue(); } } ``` And we'll see a blue canvas! +Note that JavaScript convention doesn't use `snake_case` naming, +they use `camelCase` naming. + +Also, when we want to rebuild our wasm module we have to use the whole +`cargo build --release --target wasm32-unknown-unknown` +each time. +Horrible. +Let's make a `.cargo/config.toml` file in our `web_stuff` crate folder. +Then we can set the default build target to be for wasm: +```toml +[build] +target = "wasm32-unknown-unknown" +``` +Now `cargo build` and `cargo build --release` will pick the `wasm32-unknown-unknown` target by default. + +Also, it's a little annoying to have to manually rebuild our wasm when the HTML pages reloads automatically. +To fix this, we can get `cargo-watch` + +> cargo install cargo-watch + +And then run a cargo-watch instance to automatically rebuild the code as necessary: +``` +cargo watch -c -x "build --release" +``` +The `-c` clears the terminal each time the watch restarts so that you never look at old output by accident. + +The `-x "build --release"` executes "cargo build --release" each time `cargo-watch` detects a change. + +Now we will always have both the latest HTML *and* wasm in our browser page. + ## Drawing A Triangle We need a little more wasm/js interaction than that to make a triangle. diff --git a/web_crate/.cargo/config.toml b/web_crate/.cargo/config.toml new file mode 100644 index 0000000..f4e8c00 --- /dev/null +++ b/web_crate/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/web_crate/index.html b/web_crate/index.html index 7da1baf..2c7bf0b 100644 --- a/web_crate/index.html +++ b/web_crate/index.html @@ -6,7 +6,7 @@ var gl; var canvas; - function setup_canvas() { + function setupCanvas() { console.log("Setting up the canvas..."); let canvas = document.getElementById("my_canvas"); gl = canvas.getContext("webgl"); @@ -16,15 +16,15 @@ } } - function clear_to_blue() { + function clearToBlue() { gl.clearColor(0.1, 0.1, 0.9, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); } var importObject = { env: { - setup_canvas: setup_canvas, - clear_to_blue: clear_to_blue, + setupCanvas: setupCanvas, + clearToBlue: clearToBlue, } }; diff --git a/web_crate/src/lib.rs b/web_crate/src/lib.rs index bb49fcb..9a993b4 100644 --- a/web_crate/src/lib.rs +++ b/web_crate/src/lib.rs @@ -1,14 +1,14 @@ mod js { extern "C" { - pub fn setup_canvas(); - pub fn clear_to_blue(); + pub fn setupCanvas(); + pub fn clearToBlue(); } } #[no_mangle] pub extern "C" fn start() { unsafe { - js::setup_canvas(); - js::clear_to_blue(); + js::setupCanvas(); + js::clearToBlue(); } } From 271144f85cfdb61f7eac708f3ebbb46dd501ab33 Mon Sep 17 00:00:00 2001 From: Lokathor Date: Mon, 18 Jan 2021 17:08:21 -0700 Subject: [PATCH 05/11] web-lesson-01 completed --- book_src/web_stuff/web_gl_with_bare_wasm.md | 412 +++++++++++++++++++- web_crate/.cargo/config.toml | 2 + web_crate/index.html | 76 +++- web_crate/src/lib.rs | 142 ++++++- 4 files changed, 618 insertions(+), 14 deletions(-) diff --git a/book_src/web_stuff/web_gl_with_bare_wasm.md b/book_src/web_stuff/web_gl_with_bare_wasm.md index f55435d..f2144b2 100644 --- a/book_src/web_stuff/web_gl_with_bare_wasm.md +++ b/book_src/web_stuff/web_gl_with_bare_wasm.md @@ -244,12 +244,16 @@ And we'll see a blue canvas! Note that JavaScript convention doesn't use `snake_case` naming, they use `camelCase` naming. +The naming style isn't significant to the compiler, it's just a convention. -Also, when we want to rebuild our wasm module we have to use the whole +## Workflow Tweaks + +When we want to rebuild our wasm module we have to use the whole `cargo build --release --target wasm32-unknown-unknown` each time. Horrible. -Let's make a `.cargo/config.toml` file in our `web_stuff` crate folder. +Let's make a [.cargo/config.toml](https://doc.rust-lang.org/cargo/reference/config.html) +file in our `web_stuff` crate folder. Then we can set the default build target to be for wasm: ```toml [build] @@ -257,6 +261,22 @@ target = "wasm32-unknown-unknown" ``` Now `cargo build` and `cargo build --release` will pick the `wasm32-unknown-unknown` target by default. +Also, here is where we can easily pass the flag for `rustc` to strip the symbols from the output: +```toml +[build] +target = "wasm32-unknown-unknown" +rustflags = ["-Zstrip=symbols"] +``` +The `-Z` part means that it's an unstable flag, so we can only do it with Nightly. +If you want to strip the symbols but stick to Stable Rust you'll have to get the +`wasm-strip` tool from the [wabt](https://github.com/WebAssembly/wabt) +toolkit that I mentioned before. +Stripping the symbols just makes the output smaller, so there's less to send over the network. +In a small example like ours, it changes the final output size from 308 bytes to 161 bytes. +Our code isn't doing too much, in terms of instructions, +so just putting in the debug symbols is a hefty percentage of the overall bytes taken. +We'll have another look when our program is doing a bit more to see if it's still a big difference. + Also, it's a little annoying to have to manually rebuild our wasm when the HTML pages reloads automatically. To fix this, we can get `cargo-watch` @@ -274,6 +294,390 @@ Now we will always have both the latest HTML *and* wasm in our browser page. ## Drawing A Triangle -We need a little more wasm/js interaction than that to make a triangle. +We need a little more wasm/js interaction than what we have to make a complete triangle. +Let's check out the additional stuff we'll need for a proper triangle draw. + +If you want a larger WebGL tutorial you should check out [the one on MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial). +We won't go into all of it, but there's some good stuff there. + +WebGL is based on OpenGL ES 2.0, +which is based on OpenGL 2.0, +so if you already know about GL stuff, this will probably look very familiar. + +### The Rust Code + +What we want is for our rust code to look something like this: +```rust +#[no_mangle] +pub extern "C" fn start() { + unsafe { + js::setupCanvas(); + + let vertex_data = [-0.2_f32, 0.5, 0.0, -0.5, -0.4, 0.0, 0.5, -0.1, 0.0]; + let vertex_buffer = js::createBuffer(); + js::bindBuffer(GL_ARRAY_BUFFER, vertex_buffer); + js::bufferDataF32( + GL_ARRAY_BUFFER, + vertex_data.as_ptr(), + vertex_data.len(), + GL_STATIC_DRAW, + ); + + let index_data = [0_u16, 1, 2]; + let index_buffer = js::createBuffer(); + js::bindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer); + js::bufferDataU16( + GL_ELEMENT_ARRAY_BUFFER, + index_data.as_ptr(), + index_data.len(), + GL_STATIC_DRAW, + ); + + let vertex_shader_text = " + attribute vec3 vertex_position; + void main(void) { + gl_Position = vec4(vertex_position, 1.0); + }"; + let vertex_shader = js::createShader(GL_VERTEX_SHADER); + js::shaderSource( + vertex_shader, + vertex_shader_text.as_bytes().as_ptr(), + vertex_shader_text.len(), + ); + js::compileShader(vertex_shader); + + let fragment_shader_text = " + void main() { + gl_FragColor = vec4(1.0, 0.5, 0.313, 1.0); + }"; + let fragment_shader = js::createShader(GL_FRAGMENT_SHADER); + js::shaderSource( + fragment_shader, + fragment_shader_text.as_bytes().as_ptr(), + fragment_shader_text.len(), + ); + js::compileShader(fragment_shader); + + let shader_program = js::createProgram(); + js::attachShader(shader_program, vertex_shader); + js::attachShader(shader_program, fragment_shader); + js::linkProgram(shader_program); + js::useProgram(shader_program); + + let name = "vertex_position"; + let attrib_location = js::getAttribLocation( + shader_program, + name.as_bytes().as_ptr(), + name.len(), + ); + assert!(attrib_location != GLuint::MAX); + js::enableVertexAttribArray(attrib_location); + js::vertexAttribPointer(attrib_location, 3, GL_FLOAT, false, 0, 0); + + js::clearColor(0.37, 0.31, 0.86, 1.0); + js::clear(GL_COLOR_BUFFER_BIT); + js::drawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0); + } +} +``` + +I don't want to cover too many details of how WebGL works right now because we're mostly focusing on the Wasm stuff, +but here are the broad steps: +* Initialize the canvas +* Bind a buffer as the ARRAY_BUFFER and then place our vertex data into it. +* Bind a buffer as the ELEMENT_ARRAY_BUFFER and then give it our index data. +* Create a vertex shader +* Create a fragment shader +* Create a program, connect the two shaders, then link, then use. +* Get the location of the vertex_position attribute, + enable that location, + and then point the location at the correct position within our vertex array. +* Clear the screen to our background color. +* Draw the triangle. + +If you're used to OpenGL, +or even to graphics programming using some other API, +this should all feel quite familiar. + +To support our `start` function we need to have quite a few more `extern` declarations, +and also `const` declarations: +```rust +pub type GLenum = u32; +pub type GLbitmask = u32; +pub type GLuint = u32; +pub type GLint = i32; +pub type GLsizei = i32; +// Note(kettle11): GLintptr should be an i64, but Wasm can't pass those, so for +// now just use an i32. +pub type GLintptr = i32; + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct JSObject(u32); +impl JSObject { + pub const fn null() -> Self { + JSObject(0) + } +} + +use constants::*; +mod constants { + //! Values taken from the [WebGL Constants page](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants). + //! + //! All names here have the `GL_` prefix added. + + use super::{GLbitmask, GLenum}; + + pub const GL_ARRAY_BUFFER: GLenum = 0x8892; + pub const GL_ELEMENT_ARRAY_BUFFER: GLenum = 0x8893; + pub const GL_FLOAT: GLenum = 0x1406; + pub const GL_FRAGMENT_SHADER: GLenum = 0x8B30; + pub const GL_STATIC_DRAW: GLenum = 0x88E4; + pub const GL_TRIANGLES: GLenum = 0x0004; + pub const GL_UNSIGNED_SHORT: GLenum = 0x1403; + pub const GL_VERTEX_SHADER: GLenum = 0x8B31; + + pub const GL_COLOR_BUFFER_BIT: GLbitmask = 0x00004000; +} + +mod js { + //! Holds our `extern "C"` declarations for javascript interactions. + + use super::*; + + extern "C" { + pub fn setupCanvas(); + + // + + pub fn attachShader(program: JSObject, shader: JSObject); + pub fn bindBuffer(target: GLenum, id: JSObject); + pub fn bufferDataF32( + target: GLenum, data_ptr: *const f32, data_length: usize, usage: GLenum, + ); + pub fn bufferDataU16( + target: GLenum, data_ptr: *const u16, data_length: usize, usage: GLenum, + ); + pub fn clear(mask: GLbitmask); + pub fn clearColor(r: f32, g: f32, b: f32, a: f32); + pub fn compileShader(program: JSObject); + pub fn createBuffer() -> JSObject; + pub fn createProgram() -> JSObject; + pub fn createShader(shader_type: GLenum) -> JSObject; + pub fn drawElements( + mode: GLenum, count: GLsizei, type_: GLenum, offset: GLintptr, + ); + pub fn enableVertexAttribArray(index: GLuint); + pub fn getAttribLocation( + program: JSObject, name: *const u8, name_length: usize, + ) -> GLuint; + pub fn linkProgram(program: JSObject); + pub fn shaderSource( + shader: JSObject, source: *const u8, source_length: usize, + ); + pub fn useProgram(program: JSObject); + pub fn vertexAttribPointer( + index: GLuint, size: GLint, type_: GLenum, normalized: bool, + stride: GLsizei, pointer: GLintptr, + ); + } +} +``` + +This is pretty normal stuff, except the `JsObject` thing. +What's going on there? + +Well, we can't pass a whole javascript object over the C FFI. +What even is a javascript object, anyway? +I dunno, some sort of hash... thing... with fields. +It doesn't matter. +The point is that it's a type that you *can't* pass over the C FFI. +That's mostly fine, except that we need to communicate with GL about them. + +What we'll do is store all our javascript objects in a list out in javascript-land, +and then in the WASM we just use the *index values* into that list to name the javascript objects when we need to. + +### The JavaScript Code + +On the javascript side of things, we mostly add a bunch of boring functions, +but a few are interesting. + +First we set up a few more variables we'll use. +We have the `gl` and `canvas` from before, +but now we'll need to make the javascript and wasm memory interact, +and we'll also need to track javascript objects that the wasm knows about. +Since we need to transfer strings between wasm and javascript, +we'll need a [TextDecoder](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder). +```html + + + + + diff --git a/web_crate/src/lib.rs b/web_crate/src/lib.rs index 9a993b4..4d57136 100644 --- a/web_crate/src/lib.rs +++ b/web_crate/src/lib.rs @@ -1,7 +1,81 @@ +pub type GLenum = u32; +pub type GLbitmask = u32; +pub type GLuint = u32; +pub type GLint = i32; +pub type GLsizei = i32; +// Note(kettle11): GLintptr should be an i64, but Wasm can't pass those, so for +// now just use an i32. +pub type GLintptr = i32; + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct JSObject(u32); +impl JSObject { + pub const fn null() -> Self { + JSObject(0) + } +} + +use constants::*; +mod constants { + //! Values taken from the [WebGL Constants page](https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Constants). + //! + //! All names here have the `GL_` prefix added. + + use super::{GLbitmask, GLenum}; + + pub const GL_ARRAY_BUFFER: GLenum = 0x8892; + pub const GL_ELEMENT_ARRAY_BUFFER: GLenum = 0x8893; + pub const GL_FLOAT: GLenum = 0x1406; + pub const GL_FRAGMENT_SHADER: GLenum = 0x8B30; + pub const GL_STATIC_DRAW: GLenum = 0x88E4; + pub const GL_TRIANGLES: GLenum = 0x0004; + pub const GL_UNSIGNED_SHORT: GLenum = 0x1403; + pub const GL_VERTEX_SHADER: GLenum = 0x8B31; + + pub const GL_COLOR_BUFFER_BIT: GLbitmask = 0x00004000; +} + mod js { + //! Holds our `extern "C"` declarations for javascript interactions. + + use super::*; + extern "C" { pub fn setupCanvas(); - pub fn clearToBlue(); + + // + + pub fn attachShader(program: JSObject, shader: JSObject); + pub fn bindBuffer(target: GLenum, id: JSObject); + pub fn bufferDataF32( + target: GLenum, data_ptr: *const f32, data_length: usize, usage: GLenum, + ); + pub fn bufferDataU16( + target: GLenum, data_ptr: *const u16, data_length: usize, usage: GLenum, + ); + pub fn clear(mask: GLbitmask); + pub fn clearColor(r: f32, g: f32, b: f32, a: f32); + pub fn compileShader(program: JSObject); + pub fn createBuffer() -> JSObject; + pub fn createProgram() -> JSObject; + pub fn createShader(shader_type: GLenum) -> JSObject; + pub fn drawElements( + mode: GLenum, count: GLsizei, type_: GLenum, offset: GLintptr, + ); + pub fn enableVertexAttribArray(index: GLuint); + pub fn getAttribLocation( + program: JSObject, name: *const u8, name_length: usize, + ) -> GLuint; + pub fn linkProgram(program: JSObject); + pub fn shaderSource( + shader: JSObject, source: *const u8, source_length: usize, + ); + pub fn useProgram(program: JSObject); + pub fn vertexAttribPointer( + index: GLuint, size: GLint, type_: GLenum, normalized: bool, + stride: GLsizei, pointer: GLintptr, + ); } } @@ -9,6 +83,70 @@ mod js { pub extern "C" fn start() { unsafe { js::setupCanvas(); - js::clearToBlue(); + + let vertex_data = [-0.2_f32, 0.5, 0.0, -0.5, -0.4, 0.0, 0.5, -0.1, 0.0]; + let vertex_buffer = js::createBuffer(); + js::bindBuffer(GL_ARRAY_BUFFER, vertex_buffer); + js::bufferDataF32( + GL_ARRAY_BUFFER, + vertex_data.as_ptr(), + vertex_data.len(), + GL_STATIC_DRAW, + ); + + let index_data = [0_u16, 1, 2]; + let index_buffer = js::createBuffer(); + js::bindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer); + js::bufferDataU16( + GL_ELEMENT_ARRAY_BUFFER, + index_data.as_ptr(), + index_data.len(), + GL_STATIC_DRAW, + ); + + let vertex_shader_text = " + attribute vec3 vertex_position; + void main(void) { + gl_Position = vec4(vertex_position, 1.0); + }"; + let vertex_shader = js::createShader(GL_VERTEX_SHADER); + js::shaderSource( + vertex_shader, + vertex_shader_text.as_bytes().as_ptr(), + vertex_shader_text.len(), + ); + js::compileShader(vertex_shader); + + let fragment_shader_text = " + void main() { + gl_FragColor = vec4(1.0, 0.5, 0.313, 1.0); + }"; + let fragment_shader = js::createShader(GL_FRAGMENT_SHADER); + js::shaderSource( + fragment_shader, + fragment_shader_text.as_bytes().as_ptr(), + fragment_shader_text.len(), + ); + js::compileShader(fragment_shader); + + let shader_program = js::createProgram(); + js::attachShader(shader_program, vertex_shader); + js::attachShader(shader_program, fragment_shader); + js::linkProgram(shader_program); + js::useProgram(shader_program); + + let name = "vertex_position"; + let attrib_location = js::getAttribLocation( + shader_program, + name.as_bytes().as_ptr(), + name.len(), + ); + assert!(attrib_location != GLuint::MAX); + js::enableVertexAttribArray(attrib_location); + js::vertexAttribPointer(attrib_location, 3, GL_FLOAT, false, 0, 0); + + js::clearColor(0.37, 0.31, 0.86, 1.0); + js::clear(GL_COLOR_BUFFER_BIT); + js::drawElements(GL_TRIANGLES, 3, GL_UNSIGNED_SHORT, 0); } } From 3732cc89924c490c7970cd09c8b44d9866dec6da Mon Sep 17 00:00:00 2001 From: Lokathor Date: Mon, 18 Jan 2021 17:25:12 -0700 Subject: [PATCH 06/11] Update book_src/web_stuff/index.md Co-authored-by: Joe Clay <27cupsofcoffee@gmail.com> --- book_src/web_stuff/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book_src/web_stuff/index.md b/book_src/web_stuff/index.md index 7e3b240..12de01d 100644 --- a/book_src/web_stuff/index.md +++ b/book_src/web_stuff/index.md @@ -12,7 +12,7 @@ or any other target. Even then, Wasm is strongly sandboxed, and it cannot directly interact with the world. Not only do you have to bind to some external functions on the Rust side, -you have to *write those eternal functions yourself* in javascript. +you have to *write those external functions yourself* in javascript. This is a bit of a bother, and so, *for this one target platform*, From ef607285518bc56a52a8f05694fb28c7013a344e Mon Sep 17 00:00:00 2001 From: Lokathor Date: Mon, 18 Jan 2021 17:38:09 -0700 Subject: [PATCH 07/11] note that wasm-bindgen is the common crate in this domain. --- book_src/web_stuff/web_gl_with_bare_wasm.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/book_src/web_stuff/web_gl_with_bare_wasm.md b/book_src/web_stuff/web_gl_with_bare_wasm.md index f2144b2..aa463d1 100644 --- a/book_src/web_stuff/web_gl_with_bare_wasm.md +++ b/book_src/web_stuff/web_gl_with_bare_wasm.md @@ -4,6 +4,14 @@ Before we begin I should give a big thanks to [kettle11](https://github.com/kettle11), who made the [hello_triangle_wasm_rust](https://github.com/kettle11/hello_triangle_wasm_rust) example for me. +Also, I should probably have an extra reminder at the top of this lesson: +This is the "doing it all yourself" style. +Much of the "Rust for Wasm" ecosystem uses a crate called [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/introduction.html). +In the same way that, if you "just want to open a window" you would often reach for `winit` or `sdl2` or something, +if you "just want to show something in the browser" you'll often use `wasm-bindgen` (and the crates that go with it). +People will at least *expect* that you're using `wasm-bindgen` if you get lost and need to ask someone for help. +They've got a book of their own, with many many examples, so have a look there if that's what you wanna do. + ## Toolchain Setup Before we even begin, we'll need to take a few extra steps to have the right compiler and tools available. From 080aec314b9cae5978ae6c80fd71acaa0d628eb3 Mon Sep 17 00:00:00 2001 From: Lokathor Date: Mon, 18 Jan 2021 17:38:57 -0700 Subject: [PATCH 08/11] don't say "before we begin" twice. --- book_src/web_stuff/web_gl_with_bare_wasm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/book_src/web_stuff/web_gl_with_bare_wasm.md b/book_src/web_stuff/web_gl_with_bare_wasm.md index aa463d1..c72ee4a 100644 --- a/book_src/web_stuff/web_gl_with_bare_wasm.md +++ b/book_src/web_stuff/web_gl_with_bare_wasm.md @@ -1,7 +1,7 @@ # Web GL with bare Wasm -Before we begin I should give a big thanks to [kettle11](https://github.com/kettle11), +I should give a big thanks to [kettle11](https://github.com/kettle11), who made the [hello_triangle_wasm_rust](https://github.com/kettle11/hello_triangle_wasm_rust) example for me. Also, I should probably have an extra reminder at the top of this lesson: From 80fcee98d0973daa97f3320c065a99dc16c4d541 Mon Sep 17 00:00:00 2001 From: Lokathor Date: Mon, 18 Jan 2021 18:04:38 -0700 Subject: [PATCH 09/11] make the inability to use file:// more obvious --- book_src/web_stuff/web_gl_with_bare_wasm.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/book_src/web_stuff/web_gl_with_bare_wasm.md b/book_src/web_stuff/web_gl_with_bare_wasm.md index c72ee4a..60b5adf 100644 --- a/book_src/web_stuff/web_gl_with_bare_wasm.md +++ b/book_src/web_stuff/web_gl_with_bare_wasm.md @@ -28,7 +28,9 @@ It lets you strip debugging symbols and such from the program, reducing the size You can also do this without an extra tool via a Nightly `rustc` flag. Once you've compiled your program to wasm you'll also need some way to display it. -Unfortunately, you can't simply open a local file in your browser using a `file://` address. + +**Unfortunately, you can't simply open a local file in your browser using a `file://` address.** + This is fine for a plain HTML file, but browsers (rightly) get more paranoid every day, so they don't support wasm execution in pages loaded through a file address. From 6a92ae8d8cf86ef9421585dd2464413cd84708a0 Mon Sep 17 00:00:00 2001 From: Lokathor Date: Mon, 18 Jan 2021 18:29:16 -0700 Subject: [PATCH 10/11] canvas tag inside the body. --- book_src/web_stuff/web_gl_with_bare_wasm.md | 11 ++++++----- web_crate/index.html | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/book_src/web_stuff/web_gl_with_bare_wasm.md b/book_src/web_stuff/web_gl_with_bare_wasm.md index 60b5adf..5aef121 100644 --- a/book_src/web_stuff/web_gl_with_bare_wasm.md +++ b/book_src/web_stuff/web_gl_with_bare_wasm.md @@ -115,9 +115,10 @@ but I know just enough to throw some HTML together by hand: ```html - - Hello. + + + ``` @@ -149,9 +150,9 @@ which makes a file: `target/wasm32-unknown-unknown/release/triangle_from_scratch Now we have to alter our page to load the wasm via a script: ```html - - Hello. + +