Scope And Purpose Of This Post
Getting New Tools
Creating The Rust Package
- The Cargo.toml ...
- Uses 2018 edition instead of more recent 2021 edition.
- Uses an opt-level that optimizes for size rather than speed, and the whole point of me resorting to wasm was speed.
- Is missing some package fields (license, description, etc), thus causing build warning messages.
- There is a `src/utils.rs` with a panic hook thingy that I don't think fully worked (or at the least, gives some long warning messages polluting your build messages that made me think it wasn't working).
- A mild inconvenience is that the `cargo generate --git https://github.com/rustwasm/wasm-pack-template.git` creates a new git repo, and I want the typical thing of adding some rust to an existing git repo.
But, it does some crucial things okay:
- The Cargo.toml has a good crate-type value and the essential wasm-bindgen and wasm-bindgen-test dependencies, but you probably want to update them to something more recent.
- The `src/lib.rs` has some nice examples:
- `use wasm_bindgen::prelude::*;` to `use` typical wasm_bindgen stuff.
- An example of declaring and using a JS-land function (alert).
- An example of exporting a function back to JS-land.
I think I ended up doing a `cargo new --lib` and deciding what to bring over from the wasm-pack-template and other examples (sorry, no recorded links).
My ktcalc DiceSim Cargo.toml has a few more dependencies than a barebones wasm-oriented Cargo.toml; the snippet below has some commentary...
name = "dice_sim" # not entirely sure if it should be snake case
version = "0.0.0"
edition = "2021"
authors = ["Jacob Egner <JacobEgner@example.com>"]
repository = "https://github.com/jmegner/KT21Calculator"
license = "unlicense"
description = "Monte Carlo dice simulator for damage calculations in skirmish games."
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
getrandom = { version = "0.2", features = ["js"] } # needed for `rand` crate to work for wasm
js-sys = "0.3.66" # create/use JS things like Map
num = "0.4.1" # for Num trait, not wasm/JS stuff
rand = "0.8.5" # for Monte Carlo stuff
serde = { version = "1.0", features = ["derive"] } # useful for automating things that cross the JS-WASM boundary
tsify = "0.4.5" # one library to help generate good TypeScript types; ts-rs may be better
wasm-bindgen = "0.2.89" # necessary for all of this JS-WASM stuff
[dev-dependencies]
wasm-bindgen-test = "0.3.39" # I don't think I used it, but that is because I didn't do any testing
[profile.release]
opt-level = 3 # go fast, not small code size
For future wasm stuff, I think I would copy and modify my DiceSim Cargo.toml.
Sidenote: For understanding how Rust modules work, I found "Clear explanation of Rust’s module system" to be much more useful than the official doc (book, reference).
Building Wasm And TS Outputs
"build:react": "react-scripts build",
"build:wasm": "cd src/DiceSim && wasm-pack build --target web",
Writing TypeScript/Rust Stuff To Cross JS-Wasm Boundary
Functions
Exporting rust functions is pretty easy, just put a `#[wasm_bindgen]` or `#[wasm_bindgen(js_name = RenamedFunction)]` above your function. It's the data types for the arguments and return values that are tricky.
For importing functions from javascript land, you need to declare the javascript function inside a `extern "C"` block before using it. I do not have guidance on how to find these functions and what their function signatures are.
In the rust code below, we import `alert` and export `greet`.
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello from rust compiled to wasm.");
}
Your Classes
- I have a firmer grasp on what is going on when a data type and its member functions are defined in Rust than TypeScript. I don't have to worry about hidden details that impact performance, and the whole point of resorting to Rust is performance.
- The Rust code is in a better place to be reused in other contexts. If I want to use my Rust dice simulator for something else, it would be weird to have TypeScript baggage to bring along or replace.
- The Rust code is the lower level thing, and the TypeScript web app is the higher level thing. In general, it is better for higher level things to know about lower level things they are using then for lower level things to know about the current higher level thing that is using it.
For defining a class to be used in JS-land, in theory something like the below would work...
pub struct OriginalClass {
#[wasm_bindgen(js_name = renamedField)]
pub original_field: i32,
}
#[wasm_bindgen(js_class = RenamedClass)]
impl OriginalClass {
#[wasm_bindgen(constructor)]
pub fn new() -> OriginalClass {
OriginalClass { original_field: 1 }
}
#[wasm_bindgen(js_name = renamedIncrement)]
pub fn original_increment(&mut self, val: i32) -> i32 {
self.original_field += val;
self.original_field
}
}
free(): void;
/**
*/
constructor();
/**
* @param {number} val
* @returns {number}
*/
renamedIncrement(val: number): number;
/**
*/
renamedField: number;
}
Standard Classes
The tsify Way
use std::collections::HashMap;
use tsify::Tsify;
#[derive(Tsify, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct ProbMap(pub HashMap<i32, f64>);
I didn't want `Record<number,number>`. Pull request 31 would solve this, but tsify does not seem to be actively maintained.
The js_sys Way
fn to_js_map(&self) -> js_sys::Map;
}
impl<KeyType, ValType> ToJsMap for HashMap<KeyType, ValType>
where
JsValue: From<KeyType> + From<ValType>,
KeyType: Copy,
ValType: Copy,
{
fn to_js_map(&self) -> js_sys::Map {
let js_map = js_sys::Map::new();
for (key, val) in self.iter() {
js_map.set(&JsValue::from(*key), &JsValue::from(*val));
}
js_map
}
}
Quirks Of Generated .js And .ts Files
"extends": "react-app",
"rules": {},
"overrides": [
{
"files": [
"**/pkg/*.js"
],
"rules": {
"eqeqeq": "off",
"no-new-func": "off",
"no-restricted-globals": "off",
"no-undef": "off"
}
}
]
},
Test.render(<BrowserRouter><App /></BrowserRouter>);
});
Initializing/Loading The Wasm Thingy
ReactDOM.render(
<App/>,
document.getElementById('root')
);
});
Deploying To GitHub Pages Via GitHub Actions
- uses: jetli/wasm-pack-action@v0.4.0
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
env:
CI: false
- run: git config user.name github-actions
- run: git config user.email github-actions@github.com
- run: git --work-tree build add --all
- run: git commit -m "Automatic Deploy action run by github-actions"
- run: git push origin HEAD:gh-pages --force
- uses: jetli/wasm-pack-action@v0.4.0
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build:wasm
- run: npm test
No comments:
Post a Comment