1
0
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:
sakex 2023-08-17 18:27:20 +02:00
parent d7579d7be8
commit 62200ff61d
14 changed files with 146 additions and 217 deletions

View File

@ -299,18 +299,20 @@
- [WebAssembly basics](webassembly.md) - [WebAssembly basics](webassembly.md)
- [Load a Wasm module](webassembly/load-wasm-module.md) - [Load a Wasm module](webassembly/load-wasm-module.md)
- [Expose a method](webassembly/expose-method.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) - [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) - [web-sys](webassembly/import-method/web-sys.md)
- [Expose user-defined Rust types](webassembly/expose-rust-type.md) - [Expose user-defined Rust types](webassembly/expose-rust-type.md)
- [Import user-defined Javascript types](webassembly/import-js-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) - [Limitations](webassembly/limitations.md)
- [Borrow Checker](webassembly/limitations/borrow-checker.md) - [Borrow Checker](webassembly/limitations/borrow-checker.md)
- [Closures](webassembly/limitations/closures.md) - [Closures](webassembly/limitations/closures.md)
- [Async](webassembly/async.md) - [Async](webassembly/async.md)
- [Exercises](exercises/webassembly/webassembly.md) - [Exercises](exercises/webassembly/webassembly.md)
- [Camera](exercises/webassembly/camera.md) - [Camera](exercises/webassembly/camera.md)
- [Game Of Life](exercises/webassembly/game-of-life.md)
# Final Words # Final Words
@ -335,3 +337,6 @@
- [Bare Metal Rust Afternoon](exercises/bare-metal/solutions-afternoon.md) - [Bare Metal Rust Afternoon](exercises/bare-metal/solutions-afternoon.md)
- [Concurrency Morning](exercises/concurrency/solutions-morning.md) - [Concurrency Morning](exercises/concurrency/solutions-morning.md)
- [Concurrency Afternoon](exercises/concurrency/solutions-afternoon.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)

View File

@ -1 +1,13 @@
# Exercises # 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)

View File

@ -22,14 +22,15 @@ tokio = { version = "1.29.1", features = ["sync"] }
[dependencies.web-sys] [dependencies.web-sys]
version = "0.3.4" version = "0.3.4"
features = [ features = [
'CanvasRenderingContext2d',
'CssStyleDeclaration', 'CssStyleDeclaration',
'Document', 'Document',
'Element', 'Element',
'ImageData', 'ImageData',
'CanvasRenderingContext2d',
'HtmlCanvasElement', 'HtmlCanvasElement',
'HtmlSelectElement',
'HtmlElement', 'HtmlElement',
'HtmlSelectElement',
'Node', 'Node',
'Response',
'Window', 'Window',
] ]

View File

@ -1,78 +1,36 @@
# Async # Async
Rust methods in WebAssembly can be declared async. Once called, they will be scheduled on the browser's event loop. Rust methods in WebAssembly can be declared async.
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 ```rust
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local; use wasm_bindgen_futures::JsFuture;
use tokio::sync::mpsc::{channel, Sender}; use web_sys::Response;
#[derive(Debug)]
enum RotateSide {
Left,
Right,
}
#[wasm_bindgen] #[wasm_bindgen]
pub struct Rotator { pub async fn get_current_page() -> Result<JsValue, JsValue> {
sender: Sender<RotateSide>, let window = web_sys::window().unwrap();
} let resp_value = JsFuture::from(window.fetch_with_str("")).await?;
let resp: Response = resp_value.dyn_into().unwrap();
#[wasm_bindgen] let text = JsFuture::from(resp.text()?).await?;
impl Rotator { Ok(text)
#[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)))
}
} }
``` ```
Let's call it from Javascript
```javascript ```javascript
import init, {Rotator} from '/wasm/project.js'; import init, { get_current_page} from '/wasm/project.js';
(async () => { (async () => {
// Run the init method to initiate the WebAssembly module.
await init(); await init();
const wasmoutput = document.querySelector('#wasmoutput'); console.log(await get_current_page());
const rotator = new Rotator(wasmoutput);
document.body.addEventListener('keydown', async (e) => {
await rotator.rotate(e.key);
});
})(); })();
``` ```
<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>

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

View File

@ -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, click on the wasm output box to parse the string:
```javascript ```javascript
import init, {set_panic_hook, str_to_int} from '/wasm/project.js'; import init, {str_to_int} from '/wasm/project.js';
(async () => { (async () => {
// Run the init method to initiate the WebAssembly module. // Run the init method to initiate the WebAssembly module.
await init(); await init();
set_panic_hook();
const wasmoutput = document.querySelector('#wasmoutput'); const wasmoutput = document.querySelector('#wasmoutput');
const input = document.createElement('input'); const input = document.createElement('input');
input.type = 'text'; input.type = 'text';

View File

@ -24,6 +24,6 @@ pub fn add(a: i32, b: i32) -> i32 {
<details> <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> </details>

View File

@ -2,40 +2,26 @@
Similarily to methods, types can be exposed from Rust to Javascript with the `#[wasm_bindgen]` macro. 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 ```rust
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[wasm_bindgen] #[wasm_bindgen]
pub struct Counter { pub struct Counter {
name: String, name: String,
pub count: u8, pub count: u8,
} }
```
Methods can also be exported
```rust
#[wasm_bindgen] #[wasm_bindgen]
impl Counter { impl Counter {
// Constructor will be called in JS when using `new Counter(name, count)`
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(name: String, count: u8) -> Counter { pub fn new(name: String, count: u8) -> Counter {
Counter { name, count } Counter { name, count }
} }
pub fn increment(&mut self) { pub fn increment(&mut self) {
self.count += 1; self.count += 1;
} }
// Getter for the name
#[wasm_bindgen(getter)] #[wasm_bindgen(getter)]
pub fn name(&self) -> String { pub fn name(&self) -> String {
self.name.clone() self.name.clone()
} }
// Setter for the name
#[wasm_bindgen(setter)] #[wasm_bindgen(setter)]
pub fn set_name(&mut self, name: String) { pub fn set_name(&mut self, name: String) {
self.name = name; self.name = name;
@ -43,23 +29,18 @@ impl Counter {
} }
``` ```
Add this button to the HTML file Javascript to use the `Counter`.
```html
<button id="button">Increment</button>
```
Javascript to use the `Counter`
```javascript ```javascript
import init, { set_panic_hook, Counter } from "/wasm/project.js"; import init, { Counter } from "/wasm/project.js";
(async () => { (async () => {
// Run the init method to initiate the WebAssembly module. // Run the init method to initiate the WebAssembly module.
await init(); await init();
set_panic_hook();
const wasmOutput = document.querySelector("#wasmoutput"); 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); const counter = new Counter("ButtonCounter", 42);
wasmOutput.textContent = counter.count; wasmOutput.textContent = counter.count;
button.addEventListener("click", () => { button.addEventListener("click", () => {
@ -75,5 +56,5 @@ import init, { set_panic_hook, Counter } from "/wasm/project.js";
- `pub` members must implement copy - `pub` members must implement copy
- Type parameters and lifetime annotations are not supported yet - Type parameters and lifetime annotations are not supported yet
- Members that implement `Copy` can be public and directly accessed from Javascript.
</details> </details>

View File

@ -3,67 +3,66 @@
User-defined Javascript types can be imported by declaring the relevant methods as `extern "C"` just like User-defined Javascript types can be imported by declaring the relevant methods as `extern "C"` just like
other foreign functions. other foreign functions.
For instance, let's declare a class `OutputBox`
```javascript ```javascript
import init, {set_panic_hook, edit_box} from '/wasm/project.js'; import init, { edit_box } from "/wasm/project.js";
class OutputBox { window.OutputBox = class {
constructor(element) { constructor(element) {
this.element = element; this.element = element;
this.lastText = null; this.lastText = null;
} }
setText(text) {
setText(text) { this.element.innerHTML = text;
this.element.innerHTML = text; }
} get currentText() {
return this.element.innerHTML;
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);
};
})();
``` ```
It can be imported as such in Rust It can be imported as such in Rust
```rust ```rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
pub type OutputBox; pub type OutputBox;
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(text: i32) -> OutputBox; pub fn new(element: web_sys::HtmlElement) -> OutputBox;
#[wasm_bindgen(method)] #[wasm_bindgen(method)]
pub fn setText(this: &OutputBox, text: &str); pub fn setText(this: &OutputBox, text: &str);
// Has to return owned
#[wasm_bindgen(method, getter)] #[wasm_bindgen(method, getter)]
pub fn lastText(this: &OutputBox) -> Option<String>; pub fn lastText(this: &OutputBox) -> Option<String>;
#[wasm_bindgen(method, setter)] #[wasm_bindgen(method, setter)]
pub fn set_lastText(this: &OutputBox, text: Option<String>); pub fn set_lastText(this: &OutputBox, text: Option<String>);
#[wasm_bindgen(method, getter)] #[wasm_bindgen(method, getter)]
pub fn currentText(this: &OutputBox) -> String; 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] #[wasm_bindgen]
pub fn edit_box(output_box: &OutputBox, text: &str) { pub fn edit_box(output_box: &OutputBox, text: &str) {
match text { 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> </details>

View File

@ -1,19 +1,13 @@
# Import a Javascript method # Import a Javascript method
Since Wasm runs in the browser, we will want to interact directly with Javascript APIs from Rust. Methods from javascript can be imported directly as `extern "C"` bindings.
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.
```rust ```rust
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
fn alert(s: &str); 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)] #[wasm_bindgen(js_namespace = console)]
pub fn log(s: &str); pub fn log(s: &str);
// jsMethod is a user defined method defined in the `window` object
pub fn jsMethod(); pub fn jsMethod();
} }
@ -45,4 +39,8 @@ window.jsMethod = jsMethod;
<details> <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> </details>

View File

@ -22,18 +22,16 @@ pub fn add_a_cat() -> Result<(), JsValue> {
``` ```
```javascript ```javascript
import init, {set_panic_hook, add_a_cat} from '/wasm/project.js'; import init, { add_a_cat } from "/wasm/project.js";
(async () => { (async () => {
// Run the init method to initiate the WebAssembly module. // Run the init method to initiate the WebAssembly module.
await init(); await init();
set_panic_hook(); const button = document.createElement("button");
const button = document.createElement("button"); button.textContent = "Add a cat";
button.textContent = "Add a cat"; document.body.appendChild(button);
document.body.appendChild(button); button.addEventListener("click", () => {
button.addEventListener("click", () => { add_a_cat();
add_a_cat(); });
});
})(); })();
``` ```

View File

@ -1,60 +1,40 @@
# Borrow Checker # 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. When we export a Rust type to Javascript we need to beware about the borrow checker on the Javascript side.
This essentially implements the borrow checker at Runtime in Javascript.
```rust ```rust
#[wasm_bindgen] #[wasm_bindgen]
pub struct MultiCounter { pub struct RustType {}
// We use the counter from the previous slide
counters: Vec<Counter>,
}
#[wasm_bindgen] #[wasm_bindgen]
impl MultiCounter { impl RustType {
#[wasm_bindgen(constructor)] #[wasm_bindgen]
pub fn new() -> MultiCounter { pub fn new() -> RustType {
MultiCounter { counters: Vec::new() } RustType {}
}
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);
} }
} }
#[wasm_bindgen]
pub fn takes_struct(s: RustType) -> RustType {
s
}
``` ```
```javascript ```javascript
import init, {set_panic_hook, Counter, MultiCounter} from '/wasm/project.js'; import init, {RustType, takes_struct} from '/wasm/project.js';
(async () => { (async () => {
// Run the init method to initiate the WebAssembly module. // Run the init method to initiate the WebAssembly module.
await init(); await init();
set_panic_hook(); const rustType = RustType.new();
const wasmOutput = document.querySelector("#wasmoutput"); const moved = takes_struct(rustType);
const button = document.querySelector("#button"); console.log(moved);
console.log(rustType);
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();
})(); })();
``` ```
<details> <details>
* `counter` is moved before the second call, so the code panics * `rustType` is moved so it points to null in the second log.
* Ownership rules must be respected * 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> </details>

View File

@ -1,20 +1,17 @@
# Closures # 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 ```rust
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
fn setInterval(closure: &Closure<dyn FnMut()>, millis: u32) -> f64; fn setInterval(closure: &Closure<dyn FnMut()>, millis: u32) -> f64;
} }
#[wasm_bindgen] #[wasm_bindgen]
pub struct ClosureHandle { pub struct ClosureHandle {
closure: Closure<dyn FnMut()>, closure: Closure<dyn FnMut()>,
} }
#[wasm_bindgen] #[wasm_bindgen]
pub fn timeout_set_seconds(elem: web_sys::HtmlElement) -> ClosureHandle { pub fn timeout_set_seconds(elem: web_sys::HtmlElement) -> ClosureHandle {
let seconds = Rc::new(RefCell::new(0usize)); let seconds = Rc::new(RefCell::new(0usize));
@ -30,24 +27,23 @@ pub fn timeout_set_seconds(elem: web_sys::HtmlElement) -> ClosureHandle {
``` ```
```javascript ```javascript
import init, {set_panic_hook, timeout_set_seconds} from '/wasm/project.js'; import init, { timeout_set_seconds } from "/wasm/project.js";
(async () => { (async () => {
// Run the init method to initiate the WebAssembly module. // Run the init method to initiate the WebAssembly module.
await init(); await init();
const wasmOutput = document.querySelector("#wasmoutput"); const wasmOutput = document.querySelector("#wasmoutput");
timeout_set_seconds(wasmOutput); timeout_set_seconds(wasmOutput);
})(); })();
``` ```
<details> <details>
* Since the function that creates the closure keeps its ownership, the closure would be dropped if we did't return it. - 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. - 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. - Try returning nothing from the method.
* Closures can only be passed by reference to Wasm functions. - Closures can only be passed by reference to Wasm functions.
* This is why we pass `&Closure` to `setInterval`. - This is why we pass `&Closure` to `setInterval`.
* This is also why we need to create `ClosureHandle` to return the closure. - This is also why we need to create `ClosureHandle` to return the closure.
</details> </details>