Wasm rust: Difference between revisions
(15 intermediate revisions by the same user not shown) | |||
Line 138: | Line 138: | ||
body.append_child(&title) | body.append_child(&title) | ||
.expect("Could not append title to body"); | .expect("Could not append title to body"); | ||
} | |||
</syntaxhighlight> | |||
We just need to change the web apps index.js to use our stuff. | |||
<syntaxhighlight lang="json"> | |||
import * as wasm from "wasm-dot"; | |||
wasm.run(); | |||
</syntaxhighlight> | |||
Pretty easy to get going I think | |||
=Going Back to Rust= | |||
==Using Macros== | |||
Building a DOM is a bit repetitive and it would be better to write some macros to do this for us. I always long breaks between some of the languages and although recruiters suggest on the last 2 years are the only relevant ones, I disagree because it is like riding a bicycle. But here is a refresher. Here is how it is written | |||
<syntaxhighlight lang="rust"> | |||
macro_rules! my_macro { | |||
() => { | |||
{ // Do Stuff } | |||
}; | |||
} | |||
</syntaxhighlight> | |||
And a working example | |||
<syntaxhighlight lang="rust"> | |||
macro_rules! my_macro { | |||
($var:literal) => {{ | |||
concat!("# ", $var) | |||
}}; | |||
} | |||
fn main() { | |||
let a = my_macro!("Hello, world!"); | |||
println!("{}", a); | |||
} | |||
/* | |||
Outputs | |||
# Hello, world! | |||
*/ | |||
</syntaxhighlight> | |||
We can make it variadic by doing the following | |||
<syntaxhighlight lang="rust"> | |||
macro_rules! my_macro { | |||
($($var:literal),*) => { | |||
{ concat!( $("# ", $var, "\n",)+ ) } | |||
}; | |||
} | |||
fn main() { | |||
let a = my_macro!("Hello, world! 1", "Hello, world! 2"); | |||
println!("{}", a); | |||
} | |||
/* | |||
Now outputs | |||
# Hello, world! 1 | |||
# Hello, world! 2 | |||
*/ | |||
</syntaxhighlight> | |||
==Back to the Code== | |||
Here are the help macros from Ben Lovy | |||
<syntaxhighlight lang="rust"> | |||
macro_rules! append_attrs { | |||
($document:ident, $el:ident, $( $attr:expr ),* ) => { | |||
$( | |||
let attr = $document.create_attribute($attr.0)?; | |||
attr.set_value($attr.1); | |||
$el.set_attribute_node(&attr)?; | |||
)* | |||
} | |||
} | |||
</syntaxhighlight> | |||
We can may some support macros to help too. I really like the approach Ben takes when explaining | |||
<syntaxhighlight lang="rust"> | |||
macro_rules! append_text_child { | |||
($document:ident, $el:ident, $text:expr ) => { | |||
let text = $document.create_text_node($text); | |||
$el.append_child(&text)?; | |||
}; | |||
} | |||
macro_rules! create_element_attrs { | |||
($document:ident, $type:expr, $( $attr:expr ),* ) => {{ | |||
let el = $document.create_element($type)?; | |||
append_attrs!($document, el, $( $attr ),*); | |||
el} | |||
} | |||
} | |||
macro_rules! append_element_attrs { | |||
($document:ident, $parent:ident, $type:expr, $( $attr:expr ),* ) => { | |||
let el = create_element_attrs!($document, $type, $( $attr ),* ); | |||
$parent.append_child(&el)?; | |||
} | |||
} | |||
macro_rules! append_text_element_attrs { | |||
($document:ident, $parent:ident, $type:expr, $text:expr, $( $attr:expr ),*) => { | |||
let el = create_element_attrs!($document, $type, $( $attr ),* ); | |||
append_text_child!($document, el, $text); | |||
$parent.append_child(&el)?; | |||
} | |||
} | |||
</syntaxhighlight> | |||
So here is the html we originally had<br> | |||
[[File:Wasm rust html.png]]<br> | |||
And here is the new code using our new macros<br> | |||
<syntaxhighlight lang="rust"> | |||
type Result<T> = std::result::Result<T, JsValue>; | |||
fn get_document() -> Result<Document> { | |||
let window = web_sys::window().unwrap(); | |||
Ok(window.document().unwrap()) | |||
} | |||
fn mount_app(document: &Document, body: &HtmlElement) -> Result<()> { | |||
append_text_element_attrs!(document, body, "h1", "DOT",); | |||
Ok(()) | |||
} | |||
#[wasm_bindgen] | |||
pub fn run() -> Result<()> { | |||
let document = get_document()?; | |||
let body = document.body().unwrap(); | |||
mount_app(&document, &body)?; | |||
Ok(()) | |||
} | |||
</syntaxhighlight> | |||
This is the html we are looking to build form dev.io<br> | |||
[[File:Example canvas rust code.png]]<br> | |||
And we can do this now easily with the following | |||
<syntaxhighlight lang="rust"> | |||
const STARTING_SIZE: u32 = 5; | |||
fn mount_controls(document: &Document, parent: &HtmlElement) -> Result<()> { | |||
// containing div | |||
let div = create_element_attrs!(document, "div", ("id", "rxcanvas")); | |||
// span | |||
append_text_element_attrs!( | |||
document, | |||
div, | |||
"span", | |||
&format!("{}", STARTING_SIZE), | |||
("id", "size-output") | |||
); | |||
// input | |||
append_element_attrs!( | |||
document, | |||
div, | |||
"input", | |||
("id", "size"), | |||
("type", "range"), | |||
("min", "5"), | |||
("max", "100"), | |||
("step", "5") | |||
); | |||
// label | |||
append_text_element_attrs!(document, div, "label", "- Size", ("for", "size")); | |||
// canvas | |||
mount_canvas(&document, &div)?; | |||
parent.append_child(&div)?; | |||
Ok(()) | |||
} | |||
</syntaxhighlight> | |||
===Update Page=== | |||
===Update the Canvas=== | |||
This code does the update for drawing the dot. | |||
<syntaxhighlight lang="rust"> | |||
// draw dot | |||
fn update_canvas(document: &Document, size: u32) -> Result<()> { | |||
// grab canvas | |||
let canvas = document | |||
.get_element_by_id("dot-canvas") | |||
.unwrap() | |||
.dyn_into::<web_sys::HtmlCanvasElement>()?; | |||
// resize canvas to size * 2 | |||
let canvas_dim = size * 2; | |||
canvas.set_width(canvas_dim); | |||
canvas.set_height(canvas_dim); | |||
let context = canvas | |||
.get_context("2d")? | |||
.unwrap() | |||
.dyn_into::<web_sys::CanvasRenderingContext2d>()?; | |||
// draw | |||
context.clear_rect(0.0, 0.0, canvas.width().into(), canvas.height().into()); | |||
// create shape of radius 'size' around center point (size, size) | |||
context.begin_path(); | |||
context.arc( | |||
size.into(), | |||
size.into(), | |||
size.into(), | |||
0.0, | |||
2.0 * std::f64::consts::PI, | |||
)?; | |||
context.fill(); | |||
context.stroke(); | |||
Ok(()) | |||
} | |||
</syntaxhighlight> | |||
===Update the Span=== | |||
And the Span | |||
<syntaxhighlight lang="rust"> | |||
// update the size-output span | |||
fn update_span(document: &Document, new_size: u32) -> Result<()> { | |||
let span = document.get_element_by_id("size-output").unwrap(); | |||
span.set_text_content(Some(&format!("{}", new_size))); | |||
Ok(()) | |||
} | |||
</syntaxhighlight> | |||
===And Update All=== | |||
This calls the other two and what is attached to the listener. | |||
<syntaxhighlight lang="rust"> | |||
// given a new size, sets all relevant DOM elements | |||
fn update_all() -> Result<()> { | |||
// get new size | |||
let document = get_document()?; | |||
let new_size = document | |||
.get_element_by_id("size") | |||
.unwrap() | |||
.dyn_into::<web_sys::HtmlInputElement>()? | |||
.value() | |||
.parse::<u32>() | |||
.expect("Could not parse slider value"); | |||
update_canvas(&document, new_size)?; | |||
update_span(&document, new_size)?; | |||
Ok(()) | |||
} | |||
</syntaxhighlight> | |||
==Listening for Events== | |||
So a closure allows you to wrap a function created into another function. We attach the listener to the range control with the id of size | |||
<syntaxhighlight lang="rust"> | |||
fn attach_listener(document: &Document) -> Result<()> { | |||
// listen for size change events | |||
update_all()?; // call once for initial render before any changes | |||
let callback = Closure::wrap(Box::new(move |_evt: web_sys::Event| { | |||
update_all().expect("Could not update"); | |||
}) as Box<dyn Fn(_)>); | |||
document | |||
.get_element_by_id("size") | |||
.unwrap() | |||
.dyn_into::<web_sys::HtmlInputElement>()? | |||
.set_onchange(Some(callback.as_ref().unchecked_ref())); | |||
callback.forget(); | |||
Ok(()) | |||
} | } | ||
</syntaxhighlight> | </syntaxhighlight> |
Latest revision as of 02:40, 29 September 2024
Introduction
Getting started was not easy. This was more due to the npm half of the deal.
create-wasm-app
There is something called create-wasm-app which creates a template for rust. When I used it I could not get it to work. To fix this I had to first install the app locally and the create a package.json with a dependency
# Install
npm install -g create-wasm-app
# List where it is
npm list -g
// Mine was /home/iwiseman/.nvm/versions/node/v22.8.0/lib
Now make the package.json
{
"dependencies": {
"create-wasm-app": "/home/iwiseman/.nvm/versions/node/v22.8.0/lib/node_modules/create-wasm-app"
}
}
Now create the app
npm init wasm-app www
Next we try and run the code with
npm run start
And we get the following error
Googling as we do we find we can add a flag to Nodejs to use legacy ssl
...
"scripts": {
"build": "webpack --config webpack.config.js",
"start": "NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server"
},
...
Now we go http://localhost:8081/
Clearly anything around ssl and legacy are perhaps not a way forward so I updated packages to latest
"devDependencies": {
"hello-wasm-pack": "^0.1.0",
"webpack": "^5.95.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.1.0",
"copy-webpack-plugin": "^12.0.2"
}
This updates from webpack 4 to 5 and breaks the plugin. First off the CopyWebpackPlugin format has changed from
plugins: [
new CopyWebpackPlugin(['index.html'])
],
To
plugins: [
new CopyWebpackPlugin({
patterns: ["index.html"],
}),
],
Now we have fixed that, lets hit that start button. And the result is
Well this was easy to fix. You google The module seem to be a WebAssembly module and hey presto the answer is found. I struggle to read documents as a general rule but looking at the error webpack produced in the image, it is pretty clear already what to do. I wonder if my frustration with reading has closed my mind to reading. And there is not point in having a mind if you don't change it. On with the answer in webpack.config.js.
...
module.exports = {
experiments: {
asyncWebAssembly: true,
},
...
And here is the file structure I used in this case where www is the node project and pkg contains the rust assembly
Running Going Forward
All that out of the way the next thing is to look at messing with the dom. Don't forget to get changes to your rust you need to run webpack. So in the web app you need to do
wasm-pack build
npm run start
An example
This is taken from Ben Lovy on dev.io and demonstrates enough to get started. First we need to add the dependency on web-sys to the toml.
[dependencies]
wasm-bindgen = "0.2"
[dependencies.web-sys]
version = "0.3"
features = [
"Attr",
"CanvasRenderingContext2d",
"Document",
"Element",
"Event",
"EventTarget",
"HtmlCanvasElement",
"HtmlElement",
"HtmlInputElement",
"Node",
"Text",
"Window",
]
And now the code
use wasm_bindgen::prelude::*;
use web_sys::{Document, HtmlElement};
#[wasm_bindgen]
pub fn run() {
// get window/document/body
let window = web_sys::window().expect("Could not get window");
let document = window.document().expect("Could not get document");
let body = document.body().expect("Could not get body");
mount_app(&document, &body);
}
fn mount_app(document: &Document, body: &HtmlElement) {
mount_title(&document, &body);
}
// Create a title
fn mount_title(document: &Document, body: &HtmlElement) {
// create title element
let title = document
.create_element("h1")
.expect("Could not create element");
let title_text = document.create_text_node("DOT"); // always succeeds
title
.append_child(&title_text)
.expect("Could not append child to title");
// append to body
body.append_child(&title)
.expect("Could not append title to body");
}
We just need to change the web apps index.js to use our stuff.
import * as wasm from "wasm-dot";
wasm.run();
Pretty easy to get going I think
Going Back to Rust
Using Macros
Building a DOM is a bit repetitive and it would be better to write some macros to do this for us. I always long breaks between some of the languages and although recruiters suggest on the last 2 years are the only relevant ones, I disagree because it is like riding a bicycle. But here is a refresher. Here is how it is written
macro_rules! my_macro {
() => {
{ // Do Stuff }
};
}
And a working example
macro_rules! my_macro {
($var:literal) => {{
concat!("# ", $var)
}};
}
fn main() {
let a = my_macro!("Hello, world!");
println!("{}", a);
}
/*
Outputs
# Hello, world!
*/
We can make it variadic by doing the following
macro_rules! my_macro {
($($var:literal),*) => {
{ concat!( $("# ", $var, "\n",)+ ) }
};
}
fn main() {
let a = my_macro!("Hello, world! 1", "Hello, world! 2");
println!("{}", a);
}
/*
Now outputs
# Hello, world! 1
# Hello, world! 2
*/
Back to the Code
Here are the help macros from Ben Lovy
macro_rules! append_attrs {
($document:ident, $el:ident, $( $attr:expr ),* ) => {
$(
let attr = $document.create_attribute($attr.0)?;
attr.set_value($attr.1);
$el.set_attribute_node(&attr)?;
)*
}
}
We can may some support macros to help too. I really like the approach Ben takes when explaining
macro_rules! append_text_child {
($document:ident, $el:ident, $text:expr ) => {
let text = $document.create_text_node($text);
$el.append_child(&text)?;
};
}
macro_rules! create_element_attrs {
($document:ident, $type:expr, $( $attr:expr ),* ) => {{
let el = $document.create_element($type)?;
append_attrs!($document, el, $( $attr ),*);
el}
}
}
macro_rules! append_element_attrs {
($document:ident, $parent:ident, $type:expr, $( $attr:expr ),* ) => {
let el = create_element_attrs!($document, $type, $( $attr ),* );
$parent.append_child(&el)?;
}
}
macro_rules! append_text_element_attrs {
($document:ident, $parent:ident, $type:expr, $text:expr, $( $attr:expr ),*) => {
let el = create_element_attrs!($document, $type, $( $attr ),* );
append_text_child!($document, el, $text);
$parent.append_child(&el)?;
}
}
So here is the html we originally had
And here is the new code using our new macros
type Result<T> = std::result::Result<T, JsValue>;
fn get_document() -> Result<Document> {
let window = web_sys::window().unwrap();
Ok(window.document().unwrap())
}
fn mount_app(document: &Document, body: &HtmlElement) -> Result<()> {
append_text_element_attrs!(document, body, "h1", "DOT",);
Ok(())
}
#[wasm_bindgen]
pub fn run() -> Result<()> {
let document = get_document()?;
let body = document.body().unwrap();
mount_app(&document, &body)?;
Ok(())
}
This is the html we are looking to build form dev.io
And we can do this now easily with the following
const STARTING_SIZE: u32 = 5;
fn mount_controls(document: &Document, parent: &HtmlElement) -> Result<()> {
// containing div
let div = create_element_attrs!(document, "div", ("id", "rxcanvas"));
// span
append_text_element_attrs!(
document,
div,
"span",
&format!("{}", STARTING_SIZE),
("id", "size-output")
);
// input
append_element_attrs!(
document,
div,
"input",
("id", "size"),
("type", "range"),
("min", "5"),
("max", "100"),
("step", "5")
);
// label
append_text_element_attrs!(document, div, "label", "- Size", ("for", "size"));
// canvas
mount_canvas(&document, &div)?;
parent.append_child(&div)?;
Ok(())
}
Update Page
Update the Canvas
This code does the update for drawing the dot.
// draw dot
fn update_canvas(document: &Document, size: u32) -> Result<()> {
// grab canvas
let canvas = document
.get_element_by_id("dot-canvas")
.unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()?;
// resize canvas to size * 2
let canvas_dim = size * 2;
canvas.set_width(canvas_dim);
canvas.set_height(canvas_dim);
let context = canvas
.get_context("2d")?
.unwrap()
.dyn_into::<web_sys::CanvasRenderingContext2d>()?;
// draw
context.clear_rect(0.0, 0.0, canvas.width().into(), canvas.height().into());
// create shape of radius 'size' around center point (size, size)
context.begin_path();
context.arc(
size.into(),
size.into(),
size.into(),
0.0,
2.0 * std::f64::consts::PI,
)?;
context.fill();
context.stroke();
Ok(())
}
Update the Span
And the Span
// update the size-output span
fn update_span(document: &Document, new_size: u32) -> Result<()> {
let span = document.get_element_by_id("size-output").unwrap();
span.set_text_content(Some(&format!("{}", new_size)));
Ok(())
}
And Update All
This calls the other two and what is attached to the listener.
// given a new size, sets all relevant DOM elements
fn update_all() -> Result<()> {
// get new size
let document = get_document()?;
let new_size = document
.get_element_by_id("size")
.unwrap()
.dyn_into::<web_sys::HtmlInputElement>()?
.value()
.parse::<u32>()
.expect("Could not parse slider value");
update_canvas(&document, new_size)?;
update_span(&document, new_size)?;
Ok(())
}
Listening for Events
So a closure allows you to wrap a function created into another function. We attach the listener to the range control with the id of size
fn attach_listener(document: &Document) -> Result<()> {
// listen for size change events
update_all()?; // call once for initial render before any changes
let callback = Closure::wrap(Box::new(move |_evt: web_sys::Event| {
update_all().expect("Could not update");
}) as Box<dyn Fn(_)>);
document
.get_element_by_id("size")
.unwrap()
.dyn_into::<web_sys::HtmlInputElement>()?
.set_onchange(Some(callback.as_ref().unchecked_ref()));
callback.forget();
Ok(())
}