Zmienne globalne w WebAssembly

0

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.

0

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?

Da się zrobić robiąc zmienne globalne za pomocą static oraz funkcje, które będą czytały i mutowały te zmienne (w bloku unsafe).

I na początku tak robiłem, bo nie widziałem innego sposobu, jednak potem doczytałem, to okazało się, że nie muszę tak robić, jak robię. I że wasm-bindgen daje mi bardziej przezroczyste mechanizmy.
Jeśli masz strukturę Universe w Rust:, to wasm-bindgen tworzy ci po stronie JSa coś takiego mniej więcej (co dokładnie ci tworzy, to polecam zobaczyć bindingi, które tworzy ci wasm-bindgen, bo w rzeczywistości to nieco bardziej skomplikowane):

class Universe {
  constructor() {
    const ret = wasm.universe_new();
    this.__wbg_ptr = ret >>> 0; // przypisz adres pamięci do zmiennej this.__wbg_ptr;
    return this;
  }
  tick() {
    const ret = wasm.universe_tick(this.__wbg_ptr);
    return ret >>> 0;
  }
}

czyli w JS:

const universe = new Universe(); // tworzy obiekt w JS oraz tworzy przy okazji odpowiednią strukturę w Rust
universe.tick(); // wywołuje metodę obiektu JS, która wywołuje odpowiednią funkcję w Rust, podając mu adres do selfa

wewnętrznie wywołuje funkcję w wasm universe_tick, podając mu wskaźnik do utworzonego w pamięci Wasm obiektu (wskaźnik ten jest trzymany w obiekcie JS jako this.__wbg_ptr).

Czyli zamiast zmiennych globalnych, masz alokowane na stercie Wasm struktury, do których JS trzyma tylko interfejs i adres w pamięci (w uproszczeniu, bo w bindingach widać, że i po stronie JSa jest coś trzymane).

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.

Przekazywany jest wskaźnik (liczba oznaczająca indeks w pamięci Wasm), nie obiekt. Czyli jest jakaś struktura po stronie Rust, natomiast po stronie JS masz taką kukiełkę, tj. obiekt JS, który sam nie zawiera logiki poza tym, że przekazuje informacje dalej do funkcji Wasm, podając im wskaźnik na selfa.

Chyba, że przekazujesz coś, co jest faktycznie trzymane po stronie JSa albo następuje jakieś kopiowanie danych (czy np. przy przekazywaniu stringów odpowiednie ich zakodowanie/zdekodowanie z pamięci). Wtedy po stronie JSa też coś może się dziać więcej i mogą być trzymane jakieś dane. Ale to już możesz zobaczyć w bindingach (zastanawiam się, jak to wpływa na wydajność takie manipulacje danymi, ale to już trzeba by w indywidualnych przypadkach zobaczyć).

0

Da się zrobić robiąc zmienne globalne za pomocą static oraz funkcje, które będą czytały i mutowały te zmienne (w bloku unsafe).

Oczywiście nic nie stoi na przeszkodzie, żebym zgłębił i wypróbował opisany przez Ciebie sposób, jednak wydaje mi się, że unsafe powinno się używać tylko w bardzo wyjątkowych przypadkach, a tak normalnie, to powinno się tego unikać. Konieczność utrzymywania globalnych danych chyba nie jest takim "wyjątkowym przypadkiem", dlatego zakładam, że istnieje jakiś zalecany sposób.

Jeśli masz strukturę Universe w Rust:, to wasm-bindgen tworzy ci po stronie JSa coś takiego mniej więcej (co dokładnie ci tworzy, to polecam zobaczyć bindingi, które tworzy ci wasm-bindgen, bo w rzeczywistości to nieco bardziej skomplikowane):

Ja kompiluje poleceniem wasm-pack build --target web i klasa Universe po stronie JS wygląda następująco, czyli odpowiada temu, co napisałeś:

export class Universe {

    static __wrap(ptr) {
        ptr = ptr >>> 0;
        const obj = Object.create(Universe.prototype);
        obj.__wbg_ptr = ptr;
        UniverseFinalization.register(obj, obj.__wbg_ptr, obj);
        return obj;
    }

    __destroy_into_raw() {
        const ptr = this.__wbg_ptr;
        this.__wbg_ptr = 0;
        UniverseFinalization.unregister(this);
        return ptr;
    }

    free() {
        const ptr = this.__destroy_into_raw();
        wasm.__wbg_universe_free(ptr);
    }
    /**
    */
    tick() {
        wasm.tick2(this.__wbg_ptr);
    }
    /**
    * @returns {Universe}
    */
    static new() {
        const ret = wasm.universe_new();
        return Universe.__wrap(ret);
    }
    /**
    * @returns {string}
    */
    render() {
        let deferred1_0;
        let deferred1_1;
        try {
            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
            wasm.universe_render(retptr, this.__wbg_ptr);
            var r0 = getInt32Memory0()[retptr / 4 + 0];
            var r1 = getInt32Memory0()[retptr / 4 + 1];
            deferred1_0 = r0;
            deferred1_1 = r1;
            return getStringFromWasm0(r0, r1);
        } finally {
            wasm.__wbindgen_add_to_stack_pointer(16);
            wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
        }
    }
    /**
    * @returns {string}
    */
    render1() {
        let deferred1_0;
        let deferred1_1;
        try {
            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
            wasm.universe_render1(retptr, this.__wbg_ptr);
            var r0 = getInt32Memory0()[retptr / 4 + 0];
            var r1 = getInt32Memory0()[retptr / 4 + 1];
            deferred1_0 = r0;
            deferred1_1 = r1;
            return getStringFromWasm0(r0, r1);
        } finally {
            wasm.__wbindgen_add_to_stack_pointer(16);
            wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
        }
    }
}

Przekazywany jest wskaźnik (liczba oznaczająca indeks w pamięci Wasm), nie obiekt. Czyli jest jakaś struktura po stronie Rust, natomiast po stronie JS masz taką kukiełkę, tj. obiekt JS, który sam nie zawiera logiki poza tym, że przekazuje informacje dalej do funkcji Wasm, podając im wskaźnik na selfa.

Chyba, że przekazujesz coś, co jest faktycznie trzymane po stronie JSa albo następuje jakieś kopiowanie danych (czy np. przy przekazywaniu stringów odpowiednie ich zakodowanie/zdekodowanie z pamięci). Wtedy po stronie JSa też coś może się dziać więcej i mogą być trzymane jakieś dane. Ale to już możesz zobaczyć w bindingach (zastanawiam się, jak to wpływa na wydajność takie manipulacje danymi, ale to już trzeba by w indywidualnych przypadkach zobaczyć).

No i doszliśmy do sedna sprawy. Jak widać, Sam obiekt jest w pamięci WASM, na potrzeby logiki biznesowej w Rust, ale jego tworzeniem, istnieniem i odwołaniem zarządza JavaScript, a nie Rust. Moim zdaniem, to jest przekombinowane i niepotrzebne. Nawet nie patrząc na to, jak to wygląda wydajnościowo, chodzi o samą ideę.

Jak widać, wykonanie kroku animacji jest w funkcji tick() i składa się z trzech etapów:

  1. Utworzenie kopii planszy - linia let mut next = self.cells.clone();.
  2. Wykonanie obliczeń na kopii mając dane w oryginale - dwie zagnieżdżone pętle for.
  3. Podstawienie kopii na miejsce oryginału - linia self.cells = next;. Nie wiem tylko, czy ta linia kopiuje next do self.cells, czy tylko podstawia wskaźnik na tablicę będącą kopią i oddaje własność tej tablicy obiektowi self.

Załóżmy, że mi się odwidzi i będę chciał zmienić działanie gry na następujące, gdzie pozbędę się tworzenia kopii planszy przy każdym kroku obliczeniowym:

  1. Istnieją dwie instancje obiektu Universeo tej samej wielkości, nazwijmy uni_0 i uni_1 i do tego jest jedna zmienna globalna bool, nazwijmy ją even_step.
  2. Wykonanie jednego kroku zmienia even_step na stan przeciwny, a potem, jeżeli ma wartość false, za dane źródłowe mam uni_0, wyniki obliczenia zapisuję w uni_1, a jeżeli even_step=true, to dane źródłowe są w uni_1, wyniki w uni_0. Inaczej mówiąc, co krok obliczeniowy, wymienione obiekty zamieniają się rolami i nie tworzę każdorazowo dodatkowych obiektów.
  3. Funkcja render() tworzy tekst na podstawie uni_0 lub uni_1 na podstawie bieżącego stanu even_step.

Moim zdaniem, naturalne jest, że taką modyfikację wykonuję tylko i wyłącznie po stronie WebASM, a w JS nic nie ruszam. Ale okazuje się, ze akurat w tym przykładzie, to po stronie JS muszę mieć dostęp do utworzenia obu obiektów i do przełącznika. Da się oczywiście inaczej, czyli utworzyć taką strukturę, która w sobie ma dwie instancje Universe i jedną zmienną boolean. Na przykład taka:

#[wasm_bindgen]
pub struct global {
    uni_0: Universe,
    uni_1: Universe,
    even_step: boolean,
}

Wtedy, to obojętnie, co wymyślę i będę chciał zrobić, to w razie czego zmieniam tylko powyższą strukturę i już nic nie zmieniam po stronie JS, jednak wciąż muszę pamiętać, że wszystkie funkcje operujące na tych zmiennych to albo muszą pożyczać dostęp do tej struktury przez parametr, albo być funkcjami zaimplementowanymi w tej strukturze zamiast bezpośrednio wywoływanymi. Czy to właśnie tak się najczęściej robi?

Natomiast w C++ to ja faktycznie, zmianę zrobiłbym po stronie samego WASM, bo to po stronie Wasm jest utworzenie i utrzymanie tych obiektów. Co więcej, WASM w C++ działa inaczej niż zwykły program, a główna różnica jest taka, że przejście funkcji int main(void) nie kończy programu, a więc tą funkcję można wykorzystać do przygotowania globalnych obiektów. No i w przypadku C++, strona JS w ogóle nie bierze udziału w tworzeniu i zarządzaniu obiektami. Oczywiście, dałoby się coś takiego zrobić, czyli zarezerwować pamięć funkcją malloc i uzyskać adres bajtu do JavaScript, potem ten numer przekazywać do każdej funkcji, ale to tym bardziej byłoby przekombinowane, bez sensu i jeszcze mogłoby nieco obniżyć wydajność.

Gdybym tą przykładową grę w życie tworzył w C++, to tylko bym dopisał jeszcze jeden wskaźnik na Universe, zmodyfikował funkcję tick3() i render3() i już po sprawie.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.