Stawiam pierwsze kroki w Rust, w Linux, wszystko poinstalowałem, co potrzeba, interesuje mnie Rust w kontekście WebASM, ale jako zwykły program konsolowy na komputer również.
Najpierw spróbowałem uruchomić jakiś przykład typu "hello world" według tego opisu: https://devenv.pl/rust-webassembly-jak-to-dziala/
Poinstalowałem wszystko, co potrzeba, jednak gdy doszedłem do etapu z NPM, pojawił się jakiś błąd, bo kiedyś coś pokręciłem z NPM. Okazuje się, że po wasm-pack build
potrzebne są jakiś bliżej nieokreślone czynności, żeby dostać po prostu w jednym folderze cały projekt gotowy do wprowadzenia na serwer HTTP. Po co wrzucać jakieś pliki do "repozytorium NPM"? Widać, że autor niepotrzebnie skomplikował sprawę. Do czego tu potrzebny Npde.js i NPM, tego nie roumiem. Dałem sobie z tym spokój i poszukałem innego sposobu utworzenia WebASM w Rust.
Spróbowałem tego sposobu: https://developer.mozilla.org/en-US/docs/WebAssembly/Rust_to_Wasm i ten sposób pięknie zadziałał, po wykonaniu od początku do utworzenia pliku HTML. Po uruchomieniu na localhost przykład w pełni zadziałał.
Ten przykład nie przedstawia, jak zrobić stan globalny, więc poszukałem dalej i znalazłem to: https://rustwasm.github.io/docs/book/game-of-life/hello-world.html Skopiowałem kod HTML i Rust tak, jak jest. Przedstawiony sposób uruchomienia jest taki sam, jak na devenv.pl, ale ja utworzyłem projekt tak, jak na developer.mozilla.org, wymagało to niewielkiej modyfikacji pliku HTML.
Zacząłem analizować i dalej przerabiać kod, dopisałem też komentarze, co się dzieje. Mam doświadczenie w WebAssembly z Emscripten i Cheerp, gdzie po stronie WASM pisze się w języku C++. W przypadku C++ nie ma żadnego problemu ze stanem globalnym i interakcją WASM<->JS poprzez wywoływanie funkcji.
natomiast tutaj mam wrażenie, że plansza do gry jest zrobiona po stronie JavaScript i ten obiekt jest przekazywany do WASM przy kazdym wywołaniu.
W tej chwili mam taki plik index.html
:
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>hello-wasm example</title>
</head>
<body>
<input type="button" value="Wywolanie 1" onclick="Button1()">
<input type="button" value="Wywolanie 2" onclick="Button2()">
<input type="button" value="Wywolanie 3" onclick="Button3()">
<pre id="game-of-life-canvas"></pre>
<script type="text/javascript">
const pre = document.getElementById("game-of-life-canvas");
let universe;
// Przepisanie wlasciwosci obiektu "window" do zmiennej globalnej
function JSInit()
{
universe = window.universe;
}
function Button1()
{
pre.textContent = universe.render1();
universe.tick();
}
function Button2()
{
pre.textContent = render2(universe);
tick2(universe);
}
function Button3()
{
pre.textContent = render3();
tick3();
}
</script>
<script type="module">
// Tu musza byc wymienione wszystkie funkcje i obiekty, ktore beda potrzebne w JavaScript
import init, { tick2, render2, tick3, render3, Universe } from "./pkg/gamelife.js";
console.log("Ladowanie gry w zycie");
init().then(() => {
// Uczynienie obiektow i funnkcji z modulu jako ogolnie dostepne
window.universe = Universe.new();
window.tick2 = tick2;
window.render2 = render2;
window.tick3 = tick3;
window.render3 = render3;
// Przepisanie wlasciwosci obiektu "window" do zmiennej globalnej
window.JSInit();
console.log("Gra w zycie gotowa");
});
</script>
</body>
</html>
Plik lib.rs
mam teraz taki:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[repr(u8)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Cell {
Dead = 0,
Alive = 1,
}
#[wasm_bindgen]
pub struct Universe {
width: u32,
height: u32,
cells: Vec<Cell>,
}
impl Universe {
fn get_index(&self, row: u32, column: u32) -> usize {
(row * self.width + column) as usize
}
fn live_neighbor_count(&self, row: u32, column: u32) -> u8 {
let mut count = 0;
for delta_row in [self.height - 1, 0, 1].iter().cloned() {
for delta_col in [self.width - 1, 0, 1].iter().cloned() {
if delta_row == 0 && delta_col == 0 {
continue;
}
let neighbor_row = (row + delta_row) % self.height;
let neighbor_col = (column + delta_col) % self.width;
let idx = self.get_index(neighbor_row, neighbor_col);
count += self.cells[idx] as u8;
}
}
count
}
}
/// Public methods, exported to JavaScript.
#[wasm_bindgen]
impl Universe {
// Nastepny krok jako metoda obiektu klasy Universe
pub fn tick(&mut self) {
let mut next = self.cells.clone();
for row in 0..self.height {
for col in 0..self.width {
let idx = self.get_index(row, col);
let cell = self.cells[idx];
let live_neighbors = self.live_neighbor_count(row, col);
let next_cell = match (cell, live_neighbors) {
// Rule 1: Any live cell with fewer than two live neighbours
// dies, as if caused by underpopulation.
(Cell::Alive, x) if x < 2 => Cell::Dead,
// Rule 2: Any live cell with two or three live neighbours
// lives on to the next generation.
(Cell::Alive, 2) | (Cell::Alive, 3) => Cell::Alive,
// Rule 3: Any live cell with more than three live
// neighbours dies, as if by overpopulation.
(Cell::Alive, x) if x > 3 => Cell::Dead,
// Rule 4: Any dead cell with exactly three live neighbours
// becomes a live cell, as if by reproduction.
(Cell::Dead, 3) => Cell::Alive,
// All other cells remain in the same state.
(otherwise, _) => otherwise,
};
next[idx] = next_cell;
}
}
self.cells = next;
}
pub fn new() -> Universe {
let width = 32;
let height = 32;
let cells = (0..width * height)
.map(|i| {
if i % 2 == 0 || i % 7 == 0 {
Cell::Alive
} else {
Cell::Dead
}
})
.collect();
Universe {
width,
height,
cells,
}
}
// Renderowanie tekstowe jako metoda obiektu klasy Universe, modyfikuje dzialanie to_string()
pub fn render(&self) -> String {
self.to_string()
}
// Renderowanie tekstowe jako metoda obiektu klasy Universe, bezposrednia implementacja metody
pub fn render1(&self) -> String {
let mut var_x: String = "".to_owned();
for line in self.cells.as_slice().chunks(self.width as usize) {
for &cell in line {
let symbol = if cell == Cell::Dead { '.' } else { '#' };
var_x.push(symbol);
}
var_x.push('\n');
//var_x = [var_x, "\n".to_owned()].join("");
}
var_x
}
}
// Nastepny krok jako funkcja przyjmujaca referencje do obiektu jako parametr z mozliwoscia modyfikacji
#[wasm_bindgen]
pub fn tick2(uni : &mut Universe)
{
uni.tick();
}
// Renderowanie tekstowe jako funkcja przyjmujaca referencje do obiektu jako parametr bez mozliwosci modyfikacji
#[wasm_bindgen]
pub fn render2(uni : &Universe) -> String {
let mut var_x: String = "".to_owned();
for line in uni.cells.as_slice().chunks(uni.width as usize) {
for &cell in line {
let symbol = if cell == Cell::Dead { '_' } else { '@' };
var_x.push(symbol);
}
var_x.push('\n');
}
var_x
}
// Nastepny krok jako funkcja bez parametru, ma zostac dokonany krok na zmiennej globalnej
#[wasm_bindgen]
pub fn tick3()
{
// ????????????
}
// Renderowanie tekstowe jako funkcja bez parametru, powinna zwrocic tekst z obiektu globalnego
#[wasm_bindgen]
pub fn render3() -> String {
// ???????????????????????
let var_x: String = "Render".to_owned();
var_x
}
use std::fmt;
// Modyfikacja formatowania tekstu w to_string() dla klasy Universe
impl fmt::Display for Universe {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for line in self.cells.as_slice().chunks(self.width as usize) {
for &cell in line {
let symbol = if cell == Cell::Dead { '.' } else { '#' };
write!(f, "{}", symbol)?;
}
write!(f, "\n")?;
}
Ok(())
}
}
Jak widać, zachowałem oryginalne funkcje render()
i tick()
. Dorobiłem funkcję render1()
, która robi to samo, co render()
, ale nie redefiniuje działania funkcji to_string()
, tylko sama układa zwracany tekst. To działa.
Poszedłem dalej, czyli utworzyłem funkcje render2()
i tick2()
, które są funkcjami wywoływanymi bezpośrednio, nie są metodami struktury, jednak do tych funkcji trzeba przekazywać obiekt planszy jako parametr. Przekazanie parametru realizuję po stronie JavaScript i to działa.
Problem jest z funkcjami render3()
i tick3()
, które nie przyjmują żadnych parametrów i tych funkcji dotyczy pytanie. Co zrobić, jak to napisać, żeby Po prostu wywoływać same funkcje bez przekazywania obiektów, a zapamiętanie i zmiany stanu były realizowane wyłącznie po stronie WASM/Rust? Chodzi o to, żeby nie tworzyć obiektów po stronie JavaScript, tylko żeby wszystko działo się po stronie WASM/Rust? Chodzi o to, ze jedynym interfejsem są funkcje, które wywołuję od strony JavaScript, gdzie funkcje tick()
zmienia stan globalnego obiektu, a funkcja render3()
zwraca tekstowe odwzorowanie stanu obiektu? Wtedy gra powinna działać nawet po zrezygnowaniu z publicznej widoczności typu struktury Universe
, czyli zamienić #[wasm_bindgen] pub struct Universe
na zwyczajne struct Universe
.
A jeżeli powiedzmy w Rust chciałbym zrobić jakąś apkę desktopową, gdzie na formatce jest jeden przycisk i jedno pole tekstowe, a przycisk przy każdym kliknięciu zmienia wartość liczby zapamiętanej globalnie (bez każdorazowego odczytu tej liczby z formatki), to jak to zrobić? Problem będzie identyczny. W przypadku C++, C#, i Java nie ma z tym problemu, robi się to za pomocą globalnego pola klasy lub zmiennej globalnej programu i tyle.