Skip to content
This repository has been archived by the owner on Mar 26, 2024. It is now read-only.

Frontend

NatrixAeria edited this page Jul 5, 2019 · 42 revisions

Diese Seite soll NICHT „Jan's Blog“ werden! Mitmachen tut nicht weh!


Der Frontend ist in Javascript und Rust(WebAssembly) geschrieben. Es wird ohne emscripten kompiliert, sondern stattdessen über wasm32-unknown-unknown und anschließend mit wasm-bindgen bearbeitet.

Entwicklungsort

wasm-branch: https://github.com/TrueDoctor/DiscoBot/tree/wasm/webhogg/wasm

Support

Aktuell wird leider nicht Firefox (selbst nicht die nightly versionen) unterstützt, weil dieser keine ausreichend ausgeprägte Unterstützung für die OffscreenCanvas-Technologie bietet. Getestet wird auf der neuesten Version von Chromium (state 22.06.2019: 74.0.3729) und Chrome Beta.

Installation

Ihr wollt Ratatosk bei euch lokal bauen und hosten? Kein Problem! (*hüstel*)
Es werden nur folgende Dinge benötigt:

  • Rust oder rustup
  • Cargo (normalerweise beim Rust-Paket dabei)
  • Ein Betriebssystem, das sh unterstützt
  • wasm-bindgen, ein Tool zum generieren von Bindings für WebAssembly
  • das binaryen-Paket (binaryen-git im AUR) für wasm-opt
  • ein webserver (python geht natürlich auch)

Ihr clonet euch das Projekt mit git clone https://github.com/TrueDoctor/DiscoBot. Nun geht ihr auf den gewünschten Branch. Im Pfad webhogg/wasm liegt das webhogg Frontend. Dort muss nur einmal die Datei build ausgeführt werden (./build oder sh build). Fertig! Jetzt muss nur noch ein Server gehostet werden. Am einfachsten geht das über das Python-Modul http.server, also ruft ihr python3 -m http.server <port> laufen und könnt jetzt in eurem Chromium unter http://localhost:<port> euer Werk bestaunen.
Wichtig ist zurzeit noch, dass man den Offscreen Canvas unter chrome://flags/#enable-experimental-web-platform-features aktiviert.

Prost!

Was, wenn ich einen Buildfehler habe?

Tja Pech gehabt.
Naja ihr habt immernoch die möglichkeit Jan zu PMmen, aber das ist nicht nötig, wenn ihr folgenden Fehler habt:

it looks like the Rust project used to create this wasm file was linked against
a different version of wasm-bindgen than this binary:

  rust wasm file: w.x.yz
     this binary: a.b.cd (blablabla)

wichtig ist, dass die von euch installierte Version von wasm-bindgen die gleiche ist, wie die, die in Cargo.toml steht. Das heißt, ihr habt zwei Möglichkeiten: ihr schreibt in die Cargo.toml eure Version von wasm-bindgen -V oder ihr up-/downdatet eurer wasm-bindgen auf die Version im Cargo.toml

Aktuelle Themen

Implementation von shared memory in threads

Die Idee ist, den Speicher zwischen den beiden in den verschiedenen Workern laufenden Webassembly Instanzen zu teilen:

Dies geschieht über die SharedArrayBuffers, an denen wohl unter anderem wegen spectre leider wenig gearbeitet wird, weshalb ihre Funktionalität recht schlecht (ganz besonders im Bereich wasm-threading) dokumentiert ist.

Unser bisheriges Vorgehen ist, im main-thread den Speicher zu erstellen:

let sharedMemory = new WebAssembly.Memory({
         initial: 1000,  // Speicher ist in 1024-Byte Blöcken angegeben
         maximum: 1000,
         shared: true
});

dieser wird dann über worker.postMessage(sharedMemory); an die Worker geschickt. Diese übergieben den sharedMemony an die von wasm-bindgen generierten init-Funktion. Damit die init-Funktion als zweites Argument den sharedMemory nimmt, muss die wasm binary entsprechend gelinkt werden. Das klappt leider bisher noch nicht so ganz… hierfür ist nachwievor help acquired!

Im Disassembly mit shared memory ist vor allem folgende Zeile wichtig:

(import "wbg" "memory" (memory $1 (shared 17 16384)))

Kompilert wird das ganze vereinfacht mit:

cargo build --target wasm32-unknown-unknown
wasm-bindgen --remove-producers-section --remove-name-section
# wichtig ist hier das --enable-threads
wasm-opt --memory-packing --remove-memory --remove-unused-brs --enable-threads -Oz

wobei der Linker (wahrscheinlich wasm-ld) mit folgenden Argumenten ausgeführt wird (als Hilfe kann man einfach wasm-ld --help eingeben):

wasm-ld --no-entry
        --allow-undefined
        --strip-all
        --export-dynamic
        --import-memory  # WICHTIG
        --shared-memory  # WICHTIG
        --max-memory=1073741824  # (2^30) WICHTIG
        --threads  # NICHT wichtig. Der Linker läuft nur multi-threaded

Die Linkerargumente werden unter .cargo/config spezifiziert. Entscheidend ist, dass man nicht code-kiddy-mäßig einfach irgendwelche Internetforen rezitiert (*hust* https://github.com/K0IN/WebassemblySimpleMultithreading/blob/master/compile.bat) und --import-table als Argument mitangibt, ohne zu wissen, was das eigentlich ist (WebAssembly.Table).

Und YAAAY 🎉 wir haben einen RuntimeError:

Uncaught (in promise) RuntimeError: unreachable
    at wasm-function[67]:445
    at wasm-function[28]:15
    at Function.__exports.start_graphics (http://localhost:8080/bin/webhogg-wasm.js:28:21)
    at http://localhost:8080/pkg/worker.js:11:26
(anonymous) @ wasm-0004bcba-67:195
(anonymous) @ wasm-0004bcba-28:8
__exports.start_graphics @ webhogg-wasm.js:28
(anonymous) @ worker.js:11
Promise.then (async)
onmessage @ worker.js:8

Das ist insofern „besser“, als dass kein Compiler Fehler mehr erscheint…

Jetzt gilt es, den Fehler einzugrenzen. Nach vielem herumprobieren merkt man, dass Aufrufe nach Javascript, wie z.B. console.log und nutzen von Strings ein buggy Verhalten haben. Das macht es natürlich schwer zu debuggen. Aber es gibt immernoch die möglichkeit, dass exportierte Funktionen Werte zurückgeben. Also lassen wir die in der Schleife laufenden Funktionen loop_graphics und loop_logic Werte zurückgeben, um so eine Interaktion zwischen Nutzerebene und wasm herzustellen. Hier ein Beispiel, um zu demonstrieren, dass shared memory tatsächlich funktioniert:

#[wasm_bindgen]
pub fn loop_logic(val: usize) -> u32 {
    unsafe {
        let b: Box<u32> = Box::from_raw(64000 as *mut u32);
        let b = Box::leak(b);
        *b
    }
}
#[wasm_bindgen]
pub fn loop_graphics(val: usize) -> u32 {
    unsafe {
        let b: Box<u32> = Box::from_raw(64000 as *mut u32);
        let b = Box::leak(b);
        *b += 1;
        *b
    }
}

und in der Ausgabe sieht man, dass logic den Wert tatsächlich auslesen kann:

worker.js:25 logic counter:  0
worker.js:18 graphics counter:  1
worker.js:25 logic counter:  1
worker.js:18 graphics counter:  2
worker.js:25 logic counter:  2
worker.js:18 graphics counter:  3
worker.js:25 logic counter:  3
worker.js:18 graphics counter:  4
worker.js:25 logic counter:  4

Wenn man jetzt in den wasm code sieht, dann erkennt man, dass er stark geschrumpft und übersichtlicher geworden ist:

(module
 (type $0 (func (param i32)))
 (type $1 (func (param i32) (result i32)))
 (type $2 (func))
 (import "wbg" "memory" (memory $1 (shared 17 16384)))
 (import "wbg" "__wbindgen_object_drop_ref" (func $fimport$0 (param i32)))
 (export "start_graphics" (func $0))
 (export "loop_graphics" (func $1))
 (export "start_logic" (func $3))
 (export "loop_logic" (func $2))
 (func $0 (; 1 ;) (type $0) (param $0 i32)
  (i32.store
   (i32.const 64000)
   (i32.const 0)
  )
  (if
   (i32.ge_u
    (local.get $0)
    (i32.const 36)
   )
   (call $fimport$0
    (local.get $0)
   )
  )
 )
 (func $1 (; 2 ;) (type $1) (param $0 i32) (result i32)
  (i32.store
   (i32.const 64000)
   (local.tee $0
    (i32.add
     (i32.load
      (i32.const 64000)
     )
     (i32.const 1)
    )
   )
  )
  (local.get $0)
 )
 (func $2 (; 3 ;) (type $1) (param $0 i32) (result i32)
  (i32.load
   (i32.const 64000)
  )
 )
 (func $3 (; 4 ;) (type $2)
  (nop)
 )
)

Globale Variablen sind übrigens auch möglich und der Weg, wie wir ganze in Zukunft wohl handeln wollen:

static mut number: u32 = 0;

#[wasm_bindgen]
pub fn loop_logic(val: usize) -> u32 {
    unsafe { number }
}
#[wasm_bindgen]
pub fn loop_graphics(val: usize) -> u32 {
    unsafe {
        number += 1;
        number
    }
}

Problematisch ist es allerdings im Moment noch, wenn ich Mutexes nutzen will:

static mut MEMORY: Option<Mutex<SharedMemory>> = None;

pub unsafe fn init_memory() { MEMORY = SharedMemory::default() }

Das gibt aus unerklärlichen Gründen einen RuntimeError: unreachable. Vermutlich ist der allocator an dem Schlamassel Schuld, dass so gut wie alles in der Ausführung einen RuntimeError wirft, aber wer weiß. Jan hat bereits versucht, den allocator auf wee_alloc zu wechseln, aber das hat nichts geändert, außer dass die binary 5KB größer wurde…

Jan ist seeehr verwirrt 😕

😢

😭

PLEEEEAAASEEEEE

😢

SOME HELP PLEEEAAASEEE! 😭 😭 😭 😭


Okay... 😥 *phew* durchatmen.

Wenn wir debuggen wollen, können wir leider nicht console.log(&str/String) nutzen, aber irgendweshalb console.log(u32). Aber immerhin haben wir jetzt irgendeine möglichkeit, zu debuggen.

Schon einfache Dinge, wie Strings geben Fehler: :x:

// gibt einen Fehler!
for i in "hallo welt".to_owned().bytes() {
    logger::log_num(i.into());
}

Während folgendes fehlerfrei läuft: :heavy_check_mark:

// läuft!
for i in &[50, 60, 70, 80] {
    logger::log_num(*i);
}

Bei dem String-Beispiel ist wohl das .to_owned Schuld am Absturz, denn wenn wir den .bytes() Iterator über einen &str statt über einen String laufen lassen, stürzt das Programm nicht mehr ab:

// läuft!
for i in "hallo welt".bytes() {
    logger::log_num(i.into());
}

Allerdings kommt der unerwartete Output:

104
0
0
0
0
0
…

Das erste Byte ist richtig, aber der Rest sind einfach Nullen?! Was soll das?


See Also

Navigation

Brainstorming:
      Sessions Liste 📃
      Letzte Session ◀️
      Nächste Session ▶️
      Last Design-Session 👈
      Next Design-Session 👉
      Dunkle Seite 🌈
Design:
      Sound 🎧
      Grafikdesign 🤺
      Animationen 🎞️
      Gamedesign 📝
Programmierung:
      Gamelogik ⚙️
      Frontend 👾
      Backend 🗄️
Spielprotokoll 🧻

Clone this wiki locally