mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-05-22 18:30:33 +02:00
More concise class
This commit is contained in:
parent
d7579d7be8
commit
62200ff61d
@ -299,18 +299,20 @@
|
||||
- [WebAssembly basics](webassembly.md)
|
||||
- [Load a Wasm module](webassembly/load-wasm-module.md)
|
||||
- [Expose a method](webassembly/expose-method.md)
|
||||
- [Error handling for exposed methods](webassembly/expose-method/error-handling.md)
|
||||
- [Import Method](webassembly/import-method.md)
|
||||
- [Error handling for imported methods](webassembly/import-method/error-handling.md)
|
||||
- [web-sys](webassembly/import-method/web-sys.md)
|
||||
- [Expose user-defined Rust types](webassembly/expose-rust-type.md)
|
||||
- [Import user-defined Javascript types](webassembly/import-js-type.md)
|
||||
- [Error handling](webassembly/error-handling.md)
|
||||
- [Error handling for imported methods](webassembly/error-handling/imported-methods.md)
|
||||
- [Error handling for exported methods](webassembly/error-handling/exported-methods.md)
|
||||
- [Limitations](webassembly/limitations.md)
|
||||
- [Borrow Checker](webassembly/limitations/borrow-checker.md)
|
||||
- [Closures](webassembly/limitations/closures.md)
|
||||
- [Async](webassembly/async.md)
|
||||
- [Exercises](exercises/webassembly/webassembly.md)
|
||||
- [Camera](exercises/webassembly/camera.md)
|
||||
- [Game Of Life](exercises/webassembly/game-of-life.md)
|
||||
|
||||
# Final Words
|
||||
|
||||
@ -335,3 +337,6 @@
|
||||
- [Bare Metal Rust Afternoon](exercises/bare-metal/solutions-afternoon.md)
|
||||
- [Concurrency Morning](exercises/concurrency/solutions-morning.md)
|
||||
- [Concurrency Afternoon](exercises/concurrency/solutions-afternoon.md)
|
||||
- [Webassembly](exercises/webassembly/webassembly.md)
|
||||
- [Camera](exercises/webassembly/solutions-camera.md)
|
||||
- [Game Of Life](exercises/webassembly/solutions-game-of-life.md)
|
@ -1 +1,13 @@
|
||||
# Exercises
|
||||
|
||||
There are two exercises for Webassembly, they both live in their own repository inside of
|
||||
[rust-wasm-template](../rust-wasm-template).
|
||||
|
||||
- [The Camera Exercise](camera.md) will give you access to the camera on your computer and offer you to
|
||||
apply transformations on the frames it captures.
|
||||
- [The Game Of Life Exercise](game-of-life.md) will have you implement _John Conway's Game Of Life_ using Webassembly.
|
||||
|
||||
You can find the solutions here:
|
||||
|
||||
- [Camera](solutions-camera.md)
|
||||
- [Game Of Life](solutions-game-of-life.md)
|
||||
|
@ -22,14 +22,15 @@ tokio = { version = "1.29.1", features = ["sync"] }
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.4"
|
||||
features = [
|
||||
'CanvasRenderingContext2d',
|
||||
'CssStyleDeclaration',
|
||||
'Document',
|
||||
'Element',
|
||||
'ImageData',
|
||||
'CanvasRenderingContext2d',
|
||||
'HtmlCanvasElement',
|
||||
'HtmlSelectElement',
|
||||
'HtmlElement',
|
||||
'HtmlSelectElement',
|
||||
'Node',
|
||||
'Response',
|
||||
'Window',
|
||||
]
|
||||
|
@ -1,78 +1,36 @@
|
||||
# Async
|
||||
|
||||
Rust methods in WebAssembly can be declared async. Once called, they will be scheduled on the browser's event loop.
|
||||
An event handler can for instance be implemented with a tokio channel.
|
||||
|
||||
Instead of `tokio::spawn`, `wasm_bindgen` provides `wasm_bindgen_futures::spawn_local`.
|
||||
|
||||
Let's create a class that waits for messages on a channel to rotate an HTML element:
|
||||
Rust methods in WebAssembly can be declared async.
|
||||
|
||||
```rust
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use tokio::sync::mpsc::{channel, Sender};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RotateSide {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::Response;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Rotator {
|
||||
sender: Sender<RotateSide>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Rotator {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(element: web_sys::HtmlElement) -> Rotator {
|
||||
let (sender, mut receiver) = channel::<RotateSide>(1);
|
||||
spawn_local(async move {
|
||||
let mut rotation = 0;
|
||||
while let Some(rotate_side) = receiver.recv().await {
|
||||
match rotate_side {
|
||||
RotateSide::Left => rotation -= 45,
|
||||
RotateSide::Right => rotation += 45,
|
||||
}
|
||||
element.set_inner_html(&rotation.to_string());
|
||||
let style = element.style();
|
||||
style
|
||||
.set_property("transform", &format!("rotate({rotation}deg)"))
|
||||
.expect("Failed to rotate");
|
||||
}
|
||||
});
|
||||
Rotator { sender }
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn rotate(&self, msg: String) -> Result<(), JsValue> {
|
||||
let rotate_side = match msg.as_str() {
|
||||
"ArrowLeft" => RotateSide::Left,
|
||||
"ArrowRight" => RotateSide::Right,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
self.sender
|
||||
.send(rotate_side)
|
||||
.await
|
||||
.map_err(|e| JsValue::from_str(&format!("Receiver dropped {:?}", e)))
|
||||
}
|
||||
pub async fn get_current_page() -> Result<JsValue, JsValue> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_value = JsFuture::from(window.fetch_with_str("")).await?;
|
||||
let resp: Response = resp_value.dyn_into().unwrap();
|
||||
let text = JsFuture::from(resp.text()?).await?;
|
||||
Ok(text)
|
||||
}
|
||||
```
|
||||
|
||||
Let's call it from Javascript
|
||||
|
||||
```javascript
|
||||
import init, {Rotator} from '/wasm/project.js';
|
||||
import init, { get_current_page} from '/wasm/project.js';
|
||||
|
||||
(async () => {
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
const wasmoutput = document.querySelector('#wasmoutput');
|
||||
const rotator = new Rotator(wasmoutput);
|
||||
document.body.addEventListener('keydown', async (e) => {
|
||||
await rotator.rotate(e.key);
|
||||
});
|
||||
console.log(await get_current_page());
|
||||
})();
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- Async methods are scheduled on the Javascript event loop.
|
||||
- Instead of `tokio::spawn`, `wasm_bindgen` provides `wasm_bindgen_futures::spawn_local`.
|
||||
- We use `JsFuture::from` to convert Javascript futures to Rust futures that we can `.await`.
|
||||
|
||||
</details>
|
7
src/webassembly/error-handling.md
Normal file
7
src/webassembly/error-handling.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Error handling
|
||||
|
||||
In this chapter we cover error handling both on the Rust side for imported Javascript methods
|
||||
and on the Javascript side for imported Rust methods.
|
||||
|
||||
- [Error handling for imported methods](error-handling/imported-methods.md)
|
||||
- [Error handling for exported methods](error-handling/exported-methods.md)
|
@ -24,13 +24,12 @@ pub fn str_to_int(s: &str) -> Option<i32> {
|
||||
Javascript, click on the wasm output box to parse the string:
|
||||
|
||||
```javascript
|
||||
import init, {set_panic_hook, str_to_int} from '/wasm/project.js';
|
||||
import init, {str_to_int} from '/wasm/project.js';
|
||||
|
||||
|
||||
(async () => {
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
set_panic_hook();
|
||||
const wasmoutput = document.querySelector('#wasmoutput');
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
@ -24,6 +24,6 @@ pub fn add(a: i32, b: i32) -> i32 {
|
||||
|
||||
<details>
|
||||
|
||||
* `set_panic_hook` is a convenient setup method that adds debug information to stack traces when a Wasm module panics. Don't use it in prod builds because it is rather
|
||||
* `set_panic_hook` is a convenient setup method that adds debug information to stack traces when a Wasm module panics. Don't use it in prod builds because it tends to bloat the bundle size.
|
||||
|
||||
</details>
|
@ -2,40 +2,26 @@
|
||||
|
||||
Similarily to methods, types can be exposed from Rust to Javascript with the `#[wasm_bindgen]` macro.
|
||||
|
||||
Members that implement `Copy` can be public and directly accessed from Javascript.
|
||||
|
||||
```rust
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Counter {
|
||||
name: String,
|
||||
pub count: u8,
|
||||
}
|
||||
```
|
||||
|
||||
Methods can also be exported
|
||||
|
||||
```rust
|
||||
#[wasm_bindgen]
|
||||
impl Counter {
|
||||
// Constructor will be called in JS when using `new Counter(name, count)`
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(name: String, count: u8) -> Counter {
|
||||
Counter { name, count }
|
||||
}
|
||||
|
||||
pub fn increment(&mut self) {
|
||||
self.count += 1;
|
||||
}
|
||||
|
||||
// Getter for the name
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
// Setter for the name
|
||||
#[wasm_bindgen(setter)]
|
||||
pub fn set_name(&mut self, name: String) {
|
||||
self.name = name;
|
||||
@ -43,23 +29,18 @@ impl Counter {
|
||||
}
|
||||
```
|
||||
|
||||
Add this button to the HTML file
|
||||
|
||||
```html
|
||||
<button id="button">Increment</button>
|
||||
```
|
||||
|
||||
Javascript to use the `Counter`
|
||||
Javascript to use the `Counter`.
|
||||
|
||||
```javascript
|
||||
import init, { set_panic_hook, Counter } from "/wasm/project.js";
|
||||
import init, { Counter } from "/wasm/project.js";
|
||||
|
||||
(async () => {
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
set_panic_hook();
|
||||
const wasmOutput = document.querySelector("#wasmoutput");
|
||||
const button = document.querySelector("#button");
|
||||
const button = document.createElement("button");
|
||||
button.textContent = "increment";
|
||||
document.body.appendChild(button);
|
||||
const counter = new Counter("ButtonCounter", 42);
|
||||
wasmOutput.textContent = counter.count;
|
||||
button.addEventListener("click", () => {
|
||||
@ -75,5 +56,5 @@ import init, { set_panic_hook, Counter } from "/wasm/project.js";
|
||||
|
||||
- `pub` members must implement copy
|
||||
- Type parameters and lifetime annotations are not supported yet
|
||||
|
||||
- Members that implement `Copy` can be public and directly accessed from Javascript.
|
||||
</details>
|
||||
|
@ -3,67 +3,66 @@
|
||||
User-defined Javascript types can be imported by declaring the relevant methods as `extern "C"` just like
|
||||
other foreign functions.
|
||||
|
||||
For instance, let's declare a class `OutputBox`
|
||||
|
||||
```javascript
|
||||
import init, {set_panic_hook, edit_box} from '/wasm/project.js';
|
||||
import init, { edit_box } from "/wasm/project.js";
|
||||
|
||||
class OutputBox {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.lastText = null;
|
||||
}
|
||||
|
||||
setText(text) {
|
||||
this.element.innerHTML = text;
|
||||
}
|
||||
|
||||
get currentText() {
|
||||
return this.element.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
window.OutputBox = OutputBox;
|
||||
|
||||
(async () => {
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
set_panic_hook();
|
||||
const wasmoutput = document.querySelector('#wasmoutput');
|
||||
const outputBox = new OutputBox(wasmoutput);
|
||||
const input = document.createElement('input');
|
||||
document.body.appendChild(input);
|
||||
wasmoutput.onclick = () => {
|
||||
const inputValue = input.value;
|
||||
edit_box(outputBox, inputValue);
|
||||
};
|
||||
})();
|
||||
window.OutputBox = class {
|
||||
constructor(element) {
|
||||
this.element = element;
|
||||
this.lastText = null;
|
||||
}
|
||||
setText(text) {
|
||||
this.element.innerHTML = text;
|
||||
}
|
||||
get currentText() {
|
||||
return this.element.innerHTML;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
It can be imported as such in Rust
|
||||
|
||||
```rust
|
||||
use wasm_bindgen::prelude::*;
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
pub type OutputBox;
|
||||
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(text: i32) -> OutputBox;
|
||||
|
||||
pub fn new(element: web_sys::HtmlElement) -> OutputBox;
|
||||
#[wasm_bindgen(method)]
|
||||
pub fn setText(this: &OutputBox, text: &str);
|
||||
|
||||
// Has to return owned
|
||||
#[wasm_bindgen(method, getter)]
|
||||
pub fn lastText(this: &OutputBox) -> Option<String>;
|
||||
|
||||
#[wasm_bindgen(method, setter)]
|
||||
pub fn set_lastText(this: &OutputBox, text: Option<String>);
|
||||
|
||||
#[wasm_bindgen(method, getter)]
|
||||
pub fn currentText(this: &OutputBox) -> String;
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- Getters and Setters have to be declared with an added parameter in the proc macro.
|
||||
- `null` and `undefined` can be both represented by `Option::None`
|
||||
|
||||
Try it in action:
|
||||
|
||||
```javascript
|
||||
(async () => {
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
const wasmoutput = document.querySelector("#wasmoutput");
|
||||
const outputBox = new OutputBox(wasmoutput);
|
||||
const input = document.createElement("input");
|
||||
document.body.appendChild(input);
|
||||
wasmoutput.onclick = () => {
|
||||
const inputValue = input.value;
|
||||
edit_box(outputBox, inputValue);
|
||||
};
|
||||
})();
|
||||
```
|
||||
|
||||
```rust
|
||||
#[wasm_bindgen]
|
||||
pub fn edit_box(output_box: &OutputBox, text: &str) {
|
||||
match text {
|
||||
@ -81,9 +80,4 @@ pub fn edit_box(output_box: &OutputBox, text: &str) {
|
||||
}
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
* Getters and Setters have to be declared with an added parameter in the proc macro.
|
||||
* `null` and `undefined` can be both represented by `Option::None`
|
||||
|
||||
</details>
|
@ -1,19 +1,13 @@
|
||||
# Import a Javascript method
|
||||
|
||||
Since Wasm runs in the browser, we will want to interact directly with Javascript APIs from Rust.
|
||||
For instance `println!` will not log to the javascript console, so we need to use `console.log`.
|
||||
Similarly, we want to be able to call `alert`. This works the same way as FFIs with C.
|
||||
Methods from javascript can be imported directly as `extern "C"` bindings.
|
||||
|
||||
```rust
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
fn alert(s: &str);
|
||||
|
||||
// `js_namespace` will get values inside of a nested object in window. Here, `window.console.log`
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
pub fn log(s: &str);
|
||||
|
||||
// jsMethod is a user defined method defined in the `window` object
|
||||
pub fn jsMethod();
|
||||
}
|
||||
|
||||
@ -45,4 +39,8 @@ window.jsMethod = jsMethod;
|
||||
|
||||
<details>
|
||||
|
||||
Since Wasm runs in the browser, we will want to interact directly with Javascript APIs from Rust.
|
||||
For instance `println!` will not log to the javascript console, so we need to use `console.log`.
|
||||
Similarly, we want to be able to call `alert`. This works the same way as FFIs with C.
|
||||
|
||||
</details>
|
@ -22,18 +22,16 @@ pub fn add_a_cat() -> Result<(), JsValue> {
|
||||
```
|
||||
|
||||
```javascript
|
||||
import init, {set_panic_hook, add_a_cat} from '/wasm/project.js';
|
||||
|
||||
import init, { add_a_cat } from "/wasm/project.js";
|
||||
|
||||
(async () => {
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
set_panic_hook();
|
||||
const button = document.createElement("button");
|
||||
button.textContent = "Add a cat";
|
||||
document.body.appendChild(button);
|
||||
button.addEventListener("click", () => {
|
||||
add_a_cat();
|
||||
});
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
const button = document.createElement("button");
|
||||
button.textContent = "Add a cat";
|
||||
document.body.appendChild(button);
|
||||
button.addEventListener("click", () => {
|
||||
add_a_cat();
|
||||
});
|
||||
})();
|
||||
```
|
||||
|
@ -1,60 +1,40 @@
|
||||
# Borrow Checker
|
||||
|
||||
When we export a Rust type to Javascript and the pass an instance of this type to a method that takes ownership of it, the javascript variable will be cleared and dereferencing it will throw a runtime error.
|
||||
This essentially implements the borrow checker at Runtime in Javascript.
|
||||
When we export a Rust type to Javascript we need to beware about the borrow checker on the Javascript side.
|
||||
|
||||
```rust
|
||||
#[wasm_bindgen]
|
||||
pub struct MultiCounter {
|
||||
// We use the counter from the previous slide
|
||||
counters: Vec<Counter>,
|
||||
}
|
||||
|
||||
pub struct RustType {}
|
||||
#[wasm_bindgen]
|
||||
impl MultiCounter {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new() -> MultiCounter {
|
||||
MultiCounter { counters: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn increment(&mut self) {
|
||||
for counter in &mut self.counters {
|
||||
counter.increment();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_counter(&mut self, counter: Counter) {
|
||||
self.counter.push(counter);
|
||||
impl RustType {
|
||||
#[wasm_bindgen]
|
||||
pub fn new() -> RustType {
|
||||
RustType {}
|
||||
}
|
||||
}
|
||||
#[wasm_bindgen]
|
||||
pub fn takes_struct(s: RustType) -> RustType {
|
||||
s
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
import init, {set_panic_hook, Counter, MultiCounter} from '/wasm/project.js';
|
||||
import init, {RustType, takes_struct} from '/wasm/project.js';
|
||||
|
||||
(async () => {
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
set_panic_hook();
|
||||
const wasmOutput = document.querySelector("#wasmoutput");
|
||||
const button = document.querySelector("#button");
|
||||
|
||||
const counter = new Counter("ButtonCounter", 42);
|
||||
counter.increment();
|
||||
// Works fine
|
||||
wasmOutput.textContent = counter.count;
|
||||
|
||||
const multiCounter = new MultiCounter();
|
||||
// Move counter into the MultiCounter
|
||||
multiCounter.add_counter(counter);
|
||||
// Error: Open console
|
||||
counter.increment();
|
||||
const rustType = RustType.new();
|
||||
const moved = takes_struct(rustType);
|
||||
console.log(moved);
|
||||
console.log(rustType);
|
||||
})();
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
* `counter` is moved before the second call, so the code panics
|
||||
* Ownership rules must be respected
|
||||
* `rustType` is moved so it points to null in the second log.
|
||||
* Ownership rules must be respected even in Javascript.
|
||||
* Integral types in JS that are automatically translated to Rust do not have those constraints. `(String, Vec, etc.)`
|
||||
|
||||
</details>
|
@ -1,20 +1,17 @@
|
||||
# Closures
|
||||
|
||||
Closures can be returned to Rust and executed on the Wasm runtime.
|
||||
Closures created in Rust have to be returned to so they won't be dropped.
|
||||
|
||||
```rust
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
fn setInterval(closure: &Closure<dyn FnMut()>, millis: u32) -> f64;
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct ClosureHandle {
|
||||
closure: Closure<dyn FnMut()>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn timeout_set_seconds(elem: web_sys::HtmlElement) -> ClosureHandle {
|
||||
let seconds = Rc::new(RefCell::new(0usize));
|
||||
@ -30,24 +27,23 @@ pub fn timeout_set_seconds(elem: web_sys::HtmlElement) -> ClosureHandle {
|
||||
```
|
||||
|
||||
```javascript
|
||||
import init, {set_panic_hook, timeout_set_seconds} from '/wasm/project.js';
|
||||
import init, { timeout_set_seconds } from "/wasm/project.js";
|
||||
|
||||
(async () => {
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
const wasmOutput = document.querySelector("#wasmoutput");
|
||||
timeout_set_seconds(wasmOutput);
|
||||
// Run the init method to initiate the WebAssembly module.
|
||||
await init();
|
||||
const wasmOutput = document.querySelector("#wasmoutput");
|
||||
timeout_set_seconds(wasmOutput);
|
||||
})();
|
||||
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
* Since the function that creates the closure keeps its ownership, the closure would be dropped if we did't return it.
|
||||
* Returning ownership allows the JS runtime to manage the lifetime of the closure and to collect it when it can.
|
||||
* Try returning nothing from the method.
|
||||
* Closures can only be passed by reference to Wasm functions.
|
||||
* This is why we pass `&Closure` to `setInterval`.
|
||||
* This is also why we need to create `ClosureHandle` to return the closure.
|
||||
- Since the function that creates the closure keeps its ownership, the closure would be dropped if we did't return it.
|
||||
- Returning ownership allows the JS runtime to manage the lifetime of the closure and to collect it when it can.
|
||||
- Try returning nothing from the method.
|
||||
- Closures can only be passed by reference to Wasm functions.
|
||||
- This is why we pass `&Closure` to `setInterval`.
|
||||
- This is also why we need to create `ClosureHandle` to return the closure.
|
||||
|
||||
</details>
|
Loading…
x
Reference in New Issue
Block a user