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

View File

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

View File

@ -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',
]

View File

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

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
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';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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