docs: ticket 05 (script-engine-research) aufloesen, ticket 13 graduieren

This commit is contained in:
2026-08-11 15:45:38 +02:00
parent 9f9279d926
commit c8cde87db1
4 changed files with 317 additions and 1 deletions

View File

@@ -1,7 +1,7 @@
# 05 — Research: Script-Engine für Mod-Stufe (c) # 05 — Research: Script-Engine für Mod-Stufe (c)
Type: research Type: research
Status: open Status: resolved
Blocked by: — Blocked by: —
## Question ## Question
@@ -19,3 +19,33 @@ Fakten sammeln, keine Entscheidung treffen. Zu klären:
- Reifegrad: Wartungsstand, Release-Frequenz, Größe des Ökosystems, Beispiele aus echten Spielen. - Reifegrad: Wartungsstand, Release-Frequenz, Größe des Ökosystems, Beispiele aus echten Spielen.
Ergebnis: Vergleichstabelle plus Empfehlung mit Begründung; die Entscheidung fällt der Mensch später. Ergebnis: Vergleichstabelle plus Empfehlung mit Begründung; die Entscheidung fällt der Mensch später.
## Answer
Vollständiger Bericht: [`research/05-script-engine.md`](../research/05-script-engine.md)
(209 Zeilen, geprüft am 2026-08-11). **Empfehlung: Rhai** — begründet, nicht entschieden.
- **Fehlermeldungen**: Rhai liefert Zeile *und* Spalte plus Call-Chain. Passt direkt zur
`Result`-Fehlerstrategie aus Ticket 04.
- **Typ-Brücke**: `#[derive(CustomType)]`, nur `Clone` nötig — gegen `impl UserData` pro Feld bei mlua.
- **Hot-Reload**: AST-Tausch bei überlebendem `Scope`.
- **Build**: kein `cc`, kein `libc` im Baum. Gemessen 44 Dependencies und 45,5 s gegen 166/304 Deps und
262 s/356 s bei den WASM-Kandidaten.
- **Sandbox, der schärfste Einzelbefund**: Ein Lua-Script hat auf dieser Maschine unter `Lua::new()`
**und** unter `StdLib::ALL_SAFE` per `io.open` eine echte Datei gelesen. `ALL_SAFE` ist `(1<<30)-1`
und meint *speichersicher*, nicht *fähigkeitsbeschränkt*. Rhai hat Datei-, Netz- und Prozesszugriff
gar nicht erst in der Standard-Engine. Beide brauchen Härtung, bei Rhai kostet ein vergessener
Handgriff weniger. Rhais Defaults sind aber auch nicht harmlos: `FileModuleResolver` lädt `./*.rhai`
von der Platte, `eval()` ist an, CPU- und Memory-Limits sind aus.
- **Gegenseite, ehrlich**: Lua gewinnt bei der Vertrautheit deutlich (Factorio, GMod, WoW, Teardown,
Noita, Don't Starve, Luanti, Dota 2). Ist das Ziel „möglichst viele fremde Mods" statt „gut moddbar
für einen kleinen Kreis", kippt die Empfehlung.
- **Rhai-Risiken**: Bus-Faktor 1; Unsoundness-Issue #894 offen seit 2024; Pflicht-Dependency
`smartstring` seit 2026-08-10 unmaintained (RUSTSEC-2026-0249); VM-Rewrite im Gang.
- **WASM scheidet für 1.0 aus** — nicht technisch (beste Sandbox), sondern wegen Compile-Pflicht für
Modder, Fehlermeldungen ohne Quellzeile und Build-Overhead. Falls später doch: `wasmi`, nicht wasmtime.
- **Nicht verifizierbar und so gekennzeichnet**: kein ausgeliefertes Spiel mit Rhai-Modding, kein
Rust-Spiel mit mlua, kein kommerzielles Spiel mit WASM-Modding auffindbar.
**Folge für die Map**: Reden Scripts ausschließlich über das Effekt-Vokabular, ist die Engine-Wahl
weit umkehrbarer als sie aussieht. Die Entscheidung selbst ist zu Ticket 13 graduiert und hängt an 01.

View File

@@ -0,0 +1,29 @@
# 13 — Script-Engine wählen und Sandbox härten
Type: grilling
Status: open
Blocked by: 01
## Question
Rhai oder mlua — und wie wird die gewählte Engine abgedichtet?
Ticket 05 hat die Fakten geliefert und Rhai empfohlen. Die Entscheidung steht noch aus, und sie hängt
bewusst an 01: Reden Scripts ausschließlich über das Effekt-Vokabular, ist die Wahl später umkehrbar;
reichen sie tiefer in den Game-State, ist sie es nicht. Zu klären:
- **Ist das Effekt-Vokabular die einzige Schnittstelle für Scripts?** Wenn ja, ist die Engine-Wahl eine
Implementierungsentscheidung statt einer Architekturentscheidung.
- **Zielgruppe**: „gut moddbar für einen kleinen Kreis" spricht für Rhai, „möglichst viele fremde Mods"
für Lua. Das ist die Frage, an der die Empfehlung kippt.
- **Vertraust du fremden Mods?** Bei feindlichen Mods ist Luau der einzige echte Sandbox — zum Preis
einer C++17-Toolchain und eines Lua-*Dialekts*.
- **Härtungs-Checkliste** für die gewählte Engine festlegen. Bei Rhai mindestens: `FileModuleResolver`
abschalten, `eval()` deaktivieren, CPU- und Memory-Limits setzen. Bei mlua: niemals `Lua::new()`,
sondern `new_with` mit ausgewählten Libs, `load`/`loadstring`/`dofile`/`require` entfernen,
`ChunkMode::Text` erzwingen.
- **Wie geht das Projekt mit den Rhai-Risiken um** — Bus-Faktor 1, Unsoundness-Issue #894 seit 2024,
`smartstring` unmaintained (RUSTSEC-2026-0249)? Akzeptiert, beobachtet, oder Ausschlusskriterium?
- **Wann fällt die Entscheidung endgültig** — jetzt, oder erst wenn Mod-Stufe (c) gebaut wird?
Ergebnis: gewählte Engine mit Begründung, Härtungs-Checkliste, und die Regel, was Scripts sehen dürfen.

View File

@@ -14,6 +14,7 @@ dort als Konvention im Body. Zuordnung Ticket → Gitea-Issue:
| 04 Fehlerstrategie | [#5](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/5) | 09 Save-Schema | [#10](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/10) | | 04 Fehlerstrategie | [#5](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/5) | 09 Save-Schema | [#10](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/10) |
| 05 Research Script-Engine | [#6](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/6) | 10 Raum-Prefab-Format | [#11](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/11) | | 05 Research Script-Engine | [#6](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/6) | 10 Raum-Prefab-Format | [#11](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/11) |
| 11 UI-Gerüst | [#12](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/12) | 12 Prototyp ratatui | [#13](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/13) | | 11 UI-Gerüst | [#12](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/12) | 12 Prototyp ratatui | [#13](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/13) |
| 13 Script-Engine wählen | [#14](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/14) | | |
## Destination ## Destination
@@ -46,10 +47,15 @@ Feststehende Rahmenentscheidungen aus der Charter-Session:
| Fehler | `Result` statt `panic!` — kaputte Mod-TOMLs sind der Normalfall, nicht der Ausnahmefall | | Fehler | `Result` statt `panic!` — kaputte Mod-TOMLs sind der Normalfall, nicht der Ausnahmefall |
| Save | TOML, Mod-Liste im Header, fehlender Mod = Warnung und Verwerfen, kein harter Abbruch | | Save | TOML, Mod-Liste im Header, fehlender Mod = Warnung und Verwerfen, kein harter Abbruch |
**Leitplanke aus Ticket 05**: Scripts reden ausschließlich über das Effekt-Vokabular, nie direkt mit dem
Game-State. Solange das gilt, ist die Script-Engine austauschbar — Ticket 01 entscheidet damit indirekt
auch über Ticket 13.
## Decisions so far ## Decisions so far
<!-- eine Zeile pro geschlossenem Ticket --> <!-- eine Zeile pro geschlossenem Ticket -->
- [05 — Research: Script-Engine](issues/05-research-script-engine.md) ([#6](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/6)) — Empfehlung Rhai wegen Fehlermeldungen mit Zeile und Spalte, billiger Typ-Brücke, AST-Hot-Reload und Build ohne `cc`. Gemessen: mlua liest unter `StdLib::ALL_SAFE` per `io.open` echte Dateien — „safe" heißt dort speichersicher, nicht fähigkeitsbeschränkt. Lua gewinnt bei der Modder-Vertrautheit; WASM scheidet für 1.0 aus. **Kernfolge: reden Scripts nur über das Effekt-Vokabular, ist die Engine-Wahl umkehrbar.** Entscheidung offen in Ticket 13.
- [06 — Research: ratatui-App-Architektur](issues/06-research-ratatui-architektur.md) ([#7](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/7)) — Screen-Stack über zentralem `Game`-State statt Elm oder Component-Trait; `game/` bleibt ratatui-frei; Popups per `Clear` auf Sub-`Rect` statt Widget-Stack; blockierendes `event::read()` ohne Tick; Dungeon-Grid ist Eigenbau, es gibt kein Widget dafür; ratatui 0.30.2 mit MSRV 1.88; `KeyEventKind::Press`-Filter unter Windows Pflicht. Voller Bericht in [research/06](research/06-ratatui-architektur.md). - [06 — Research: ratatui-App-Architektur](issues/06-research-ratatui-architektur.md) ([#7](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/7)) — Screen-Stack über zentralem `Game`-State statt Elm oder Component-Trait; `game/` bleibt ratatui-frei; Popups per `Clear` auf Sub-`Rect` statt Widget-Stack; blockierendes `event::read()` ohne Tick; Dungeon-Grid ist Eigenbau, es gibt kein Widget dafür; ratatui 0.30.2 mit MSRV 1.88; `KeyEventKind::Press`-Filter unter Windows Pflicht. Voller Bericht in [research/06](research/06-ratatui-architektur.md).
## Not yet specified ## Not yet specified

View File

@@ -0,0 +1,251 @@
# 05 — Research: Script-Engine für Mod-Stufe (c)
Stand: 2026-08-11. Antwort auf `issues/05-research-script-engine.md`.
**Keine Entscheidung — Fakten plus begründete Empfehlung.**
## Methodik
Zwei Arten von Aussagen, überall getrennt gekennzeichnet:
- **[gemessen]** — auf dieser Maschine ausgeführt: `cargo 1.96.0` / `rustc 1.96.0`,
Host `x86_64-pc-windows-msvc`, LLVM 22.1.2. Versionen live über `cargo generate-lockfile`
gegen crates.io aufgelöst, Dependency-Zahlen aus `Cargo.lock` (`name =`-Einträge, gleiche
Methode für alle Kandidaten), Build-Zeiten mit `Measure-Command` über `cargo build` (debug,
leerer `fn main(){}`, eigener `CARGO_TARGET_DIR`).
- **[Quelle]** — aus Primärquelle zitiert, URL jeweils direkt dabei und in der Quellenliste.
Was nicht verifiziert werden konnte, steht explizit unter „Nicht verifiziert".
## Aufgelöste Versionen [gemessen, 2026-08-11]
| Crate | Aufgelöste Version | Release-Datum [Quelle] |
|---|---|---|
| `rhai` | **1.25.1** | 2026-05-29 — crates.io API |
| `mlua` (`lua54`, `vendored`) | **0.12.0** | 2026-07-05 — crates.io API |
| `wasmtime` | **47.0.3** | 2026-07-31 — crates.io API |
| `wasmi` | **1.1.0** | 2026-06-12 — crates.io API |
| `extism` | **1.30.0** | 2026-06-04 — crates.io API |
---
## Vergleichstabelle
| Kriterium | **Rhai 1.25.1** | **Lua 5.4 via mlua 0.12** | **WASM (wasmtime 47 / extism 1.30 / wasmi 1.1)** |
|---|---|---|---|
| **C-Toolchain unter Windows** | Keine. Kein `cc`, `libc`, `bindgen` oder `pkg-config` im Baum — reines Rust. | Ja, aber faktisch gratis: `vendored` kompiliert die Lua-C-Quellen über `cc`. [gemessen] hat `mlua-sys` hier eine echte `lua5.4.lib` erzeugt, **ohne dass `cl.exe` im PATH lag** — das `cc`-Crate findet MSVC über `vswhere` (VS 18.6 installiert). Der MSVC-Linker ist für den `*-windows-msvc`-Rust-Toolchain ohnehin Pflicht. | `wasmtime` hat `cc` als Build-Dep, praktisch aber irrelevant (msvc = Tier 1). `wasmi` ist reines Rust und `no_std`-fähig. |
| **Dependencies (Cargo.lock)** [gemessen] | **44** | **31** | `wasmi` **29** · `wasmtime` **166** · `extism` **304** |
| **Cold-Build debug** [gemessen] | **45,5 s** | **17,2 s** (erster Lauf mit kaltem Registry-Cache: 29,1 s) | `wasmi` **24,5 s** · `wasmtime` **262,5 s** · `extism` **356,1 s** (letztere zwei mit kaltem Registry-Cache, enthalten Download) |
| **MSRV** | 1.66 [Quelle: lib.rs] | 1.88 [Quelle: lib.rs] | `wasmi` 1.86 · `wasmtime` **1.95** (Edition 2024) — aggressiv, erzwingt Rust-Upgrades |
| **Typ-Brücke zu Rust-Structs** | `#[derive(CustomType)]` oder `CustomType`+`TypeBuilder`. **Einzige Anforderung: `Clone`** (mit `sync`-Feature zusätzlich `Send+Sync`). Mutable Game-State über `Rc<RefCell<_>>`-Closures (Control-Layer-Pattern) oder `&mut`-Erstargument. | `impl UserData` mit `add_field_method_get/set`, `add_method`, `add_method_mut`, `add_meta_method`. Mehr Zeremonie als Rhai, dafür sehr ausgereift. Für `!Send`/nicht-`'static`-Typen braucht es `Lua::scope`. Serde-Brücke über Feature `serialize`. | Am teuersten. Core-WASM kennt nur i32/i64/f32/f64 + Linear Memory — **jeder Struct muss serialisiert werden**. Extism: `#[plugin_fn] fn f(Json(x): Json<T>) -> FnResult<Json<U>>` (wenig Boilerplate, JSON-Kosten pro Call). wasmtime: WIT-IDL + `wit-bindgen` + `Guest`-Impls, Aufwand ähnlich protobuf. Kein Shared Memory — Daten werden kopiert. |
| **Sandbox — Default** | **Gut, aber zwei Fallen.** Keine Datei-/Netz-/Prozess-Funktionen in irgendeinem der 15 Built-in-Packages; Dateizugriff ist ein separates Opt-in-Crate (`rhai-fs`). ABER: (1) der Default-`FileModuleResolver` **lädt `./*.rhai` von der Platte** — `import "evil"` funktioniert auf einer blanken `Engine::new()`; (2) `eval()` ist eingebaut und aktiv. | **Schlecht.** [gemessen] Auf `Lua::new()` **und** auf `StdLib::ALL_SAFE` sind `io`, `os`, `package`, `require`, `dofile`, `load` vorhanden; `io.open` hat hier eine echte Datei gelesen, `os.execute` ist erreichbar. `ALL_SAFE` = `(1<<30)-1` und schließt nur `debug` und `ffi` aus — es bedeutet *speichersicher*, **nicht** *fähigkeitsbeschränkt*. | **Am stärksten.** „no raw access to system calls or other forms of I/O; the only thing a WebAssembly instance can do is what is available through interfaces it has been explicitly linked with." Ein Modul ohne Imports kann nichts. WASI ist Opt-in und default-deny. |
| **Sandbox — abschaltbar / härtbar** | `set_module_resolver(DummyModuleResolver::new())`, `disable_symbol("eval")`, `set_max_operations`, `on_progress`, `set_max_string_size/array_size/map_size`, `set_strict_variables`, Features `no_module`/`no_function`/`no_closure`. **CPU- und Memory-Limits sind per Default 0 = unbegrenzt.** | `Lua::new_with(StdLib::STRING\|TABLE\|MATH, …)` — [gemessen] danach sind `io`, `os`, `package`, `require`, `debug` weg (`load` und `print` bleiben, Base-Lib ist immer geladen). `set_memory_limit` [gemessen: greift, „memory error: not enough memory"], `set_hook(every_nth_instruction)` [gemessen: stoppt `while true do end`]. Luau-Backend bietet zusätzlich `Lua::sandbox()`. | `Config::consume_fuel` (deterministisch, inkompatibel mit Winch) oder Epoch-Interruption („cannot be avoided by malicious wasm code"), `StoreLimits` für Speicher. Caveat wörtlich: „Epochs (and fuel) do not assist in handling WebAssembly code blocked in a call to the host." |
| **Sandbox — bekannte Löcher** | GHSA-67fv-9r7g-432h (High, 2024, ≤1.18.0, gefixt). **Issue #894 „Rhai is unsound (use after free)" seit 2024-07-03 offen** — Host-API-Footgun, kein Script-Escape. | mlua-README selbst: „mlua does not provide absolute safety even without using `unsafe`. This library contains a huge amount of unsafe code." | **46 publizierte Advisories** bei wasmtime, davon **4 critical / 4 high**; zwei Criticals von 2026-04-09 sind echte Sandbox-Escapes (aarch64-Cranelift-Miscompile, Winch-Memory-Access). Der JIT ist in der TCB. Monatlicher Patch-Treadmill. |
| **Fehler mit Zeilennummer** | **Ja, Zeile + Spalte**, auch in verschachtelten Funktionen mit Call-Chain: `Array index 99 out of bounds: only 3 elements in array (line 3, position 11)`. API: `Position::line()` / `Position::position()`. ⚠️ 16-Bit-Auflösung, max. 65 535 Zeilen. | **Ja, Chunk-Name + Zeile + Traceback.** [gemessen]:<br>`[string "core/mods/evil.lua"]:2: attempt to index a nil value (local 't')`<br>`stack traceback:`<br>` [C]: in metamethod 'index'`<br>` [string "core/mods/evil.lua"]:2: in main chunk`<br>Syntaxfehler ebenso mit Zeile. | **Deutlich schlechter.** `WasmBacktrace` liefert Funktionsname + `module_offset`/`func_offset`. Quell-Zeile nur, wenn der Modder mit DWARF-Debuginfo gebaut hat *und* du `FrameInfo::symbols()` auswertest. Realistisch: Funktionsname + Byte-Offset. |
| **Hot-Reload** | **Trivial.** Engine ist zustandslos gegenüber dem `AST`; `AST` in `Rc<RefCell<_>>` neu kompilieren und tauschen. `Scope`-State überlebt den Reload. Chirurgisch möglich über `retain_functions` + `+=`. | **Ja.** [gemessen] Chunk im selben `Lua`-State neu ausführen → `tick()` liefert `v1 -> v2`. Alternativ neuen `Lua`-State bauen. | Möglich (neu kompilieren + frischer `Store`+`Instance`), aber Kompilierung ist teuer. `Module::deserialize` ist **`unsafe`** — „Arbitrary input could […] replace valid compiled code with any other valid compiled code, meaning that this can trivially be used to execute arbitrary code otherwise." Also niemals von Mods gelieferte `.cwasm` laden. |
| **Vertrautheit für Hobby-Modder** | Gering. Rhai-ähnliche Syntax (Rust/JS-nah), aber ein Modder muss sie neu lernen. Kein Spiel bekannt, das Rhai für Modding einsetzt (siehe „Nicht verifiziert"). | **Klar überlegen.** Factorio (Lua 5.2), Garry's Mod (5.1), Teardown (5.1), Luanti, Dota 2 VScript, WoW, Noita, Don't Starve, DFHack, Widelands, Balatro (via LÖVE). Alle mit offizieller oder de-facto Lua-Modding-API. | **Am schlechtesten.** Es gibt kein „Textdatei editieren und neu laden": Extism-Doku sagt, jedes Plugin „must be compiled to WebAssembly […] before it can be run in an Extism Host". Der Modder braucht Rust/Go/Zig/AssemblyScript-Toolchain. Die JS-Ausweichroute (Extism JS PDK = QuickJS-in-WASM, Javy-Fork) braucht zusätzlich Binaryen und kann „nur pure JS", ohne Node-/Browser-APIs. |
| **Reifegrad / Wartung** | Sehr aktiv, aber **Ein-Mann-Projekt** (Stephen Chung). ~4 Minor-Releases/Jahr seit 2 Jahren, letzter Commit 2026-08-11. 5 596 Stars, 316 Reverse-Deps. Hauptnutzer sind Config-/Policy-Engines (handlebars, casbin, apollo-router), nicht Spiele. Ein Bytecode-VM-Rewrite scheint im Gang → Verhaltens-Churn erwartbar. | Aktiv, breit genutzt. 2 826 Stars, letzter Push 2026-07-28, 438 direkte Dependents. Referenz auf Windows: **WezTerm** baut mit exakt `features = ["vendored","lua54",…]`. Produktions-Track-Record liegt in Tooling (WezTerm, Yazi, Vector/VRL, LibAFL), nicht in Spielen. | wasmtime: Bytecode Alliance, **fester Monatsrelease am 20.**, LTS nur alle 12 Versionen (36, 48) — Nicht-LTS-Releases fallen nach **2 Monaten** aus dem Support. extism (Dylibso): unregelmäßig, hängt an wasmtime **43** (bereits außerhalb des Supportfensters). wasmi: von der Stellar Development Foundation gesponsert, 2.0 seit Monaten in Beta. `cargo component` wird offiziell deprecated; Component Model ist **nicht 1.0**. |
| **Performance** | AST-Walking-Interpreter, „No bytecodes/JIT". Offiziell: „In general, Rhai is roughly 2x slower than Python 3." Für rundenbasierte Logik irrelevant. | Lua 5.4 ist eine der schnellsten Skriptsprachen; hier ebenfalls irrelevant. | JIT (Cranelift) am schnellsten, aber Pulley-Interpreter „~10x order-of-magnitude slowdown". Irrelevant für ein Terminalspiel. |
---
## Empfehlung
**Rhai** — mit klar benannten Gegenargumenten, weil die Entscheidung nicht eindeutig ist.
### Warum Rhai
1. **Es passt zur Fehlerstrategie aus der Map.** „`Result` statt `panic!` — kaputte Mod-TOMLs
sind der Normalfall." Rhai liefert Parse- *und* Laufzeitfehler mit Zeile und Spalte, inklusive
Call-Chain, als `EvalAltResult` — das fügt sich ohne Übersetzungsschicht in die bestehende
Fehlerbehandlung ein. WASM fällt an genau dieser Stelle durch: ein Modder bekäme Funktionsname
und Byte-Offset statt einer Zeilennummer.
2. **Die Typ-Brücke ist die billigste.** `#[derive(CustomType)]` und „the only requirement of a
type to work with Rhai is `Clone`" gegen mlua's `impl UserData` mit `add_field_method_get`/
`add_method_mut` pro Feld. Da Stufe (c) laut Map auf der Typ-Registry aufsetzt und das
Effekt-Vokabular ohnehin die Mod-API *ist*, ist die Zahl der zu brückenden Typen klein — aber
jeder gesparte Handgriff zählt bei einem Solo-Projekt.
3. **Der Default ist sicherer.** Das ist der schärfste Einzelunterschied: [gemessen] hat ein
Lua-Script auf `Lua::new()` *und* auf `StdLib::ALL_SAFE` eine echte Datei über `io.open`
gelesen; `os.execute` war erreichbar. Bei Rhai gibt es diese Funktionen in der Standard-Engine
überhaupt nicht — Dateizugriff ist ein separates Crate. Beide brauchen Härtung, aber ein
vergessener Handgriff kostet bei Rhai weniger.
4. **Kein C im Build, kleinste Toolchain-Angriffsfläche.** Rhai zieht kein `cc`, `libc` oder
`bindgen`. Praktisch ist das unter Windows-MSVC ein schwaches Argument — [gemessen] baute
`mlua` mit `vendored` hier problemlos und sogar schneller (17,2 s gegen 45,5 s), weil der
MSVC-Compiler für den Rust-Toolchain ohnehin installiert sein muss. Das Argument zählt erst
bei `x86_64-pc-windows-gnu` oder bei CI-Images ohne Build-Tools.
5. **Lernziel Rust.** Die Map nennt „Rust lernen **und** Spiel bauen" als Motivation. Rhais API
ist durchgehend Rust-idiomatisch (Traits, Derive-Makros, `Result`); mlua zwingt zusätzlich zum
Verständnis der Lua-C-API-Semantik (Stack, `longjmp`-Verhalten, `Lua::scope`-Lifetimes).
6. **Hot-Reload ist ein Zweizeiler** und `Scope`-State überlebt den Tausch — genau das Verhalten,
das man beim Iterieren an einem Mod will.
### Was gegen Rhai spricht (und für Lua)
- **Vertrautheit ist Lua's stärkstes Argument, und es ist ein starkes.** Factorio, Garry's Mod,
WoW, Teardown, Noita, Don't Starve, Luanti, Dota 2 — ein Hobby-Modder, der schon einmal etwas
gemoddet hat, kann mit hoher Wahrscheinlichkeit Lua. Für Rhai muss er neu anfangen. Wenn das
Ziel „möglichst viele fremde Mods" ist statt „gut moddbar für mich und ein paar Leute",
kippt die Empfehlung zu Lua.
- **Bus-Faktor 1.** Rhai wird im Wesentlichen von einer Person entwickelt.
- **Offene Unsoundness (#894, seit 2024-07-03)** und die Pflicht-Dependency **`smartstring` ist
seit 2026-08-10 als unmaintained gemeldet** (RUSTSEC-2026-0249, Repo am 2026-05-03 archiviert).
`cargo deny` schlägt damit heute an.
- **Ein Bytecode-VM-Rewrite scheint im Gang** (offene Issues „VM: Fuzz errors detected",
„VM: Modify switch statement processing") → Verhaltensänderungen in kommenden Releases.
- Kein einziges verifizierbares ausgeliefertes Spiel nutzt Rhai fürs Modding.
### Warum WASM für 1.0 ausscheidet
Nicht wegen der Technik — die Sandbox ist objektiv die beste der drei und die einzige, die
gegen wirklich bösartigen Code hält. Sondern wegen drei Dingen, die direkt gegen die Ziele
dieses Projekts laufen:
1. **Der Compile-Schritt tötet die Zielgruppe.** Ein Modder, der heute 30 Zeilen Lua schreiben
würde, müsste eine Rust-/TinyGo-/AssemblyScript-Toolchain installieren und ein Binary
ausliefern. Selbst der „einfache" JS-Weg (Extism JS PDK) braucht Binaryen und einen Build.
2. **Fehlermeldungen ohne Zeilennummer** widersprechen der Fehlerstrategie der Map.
3. **Der Build-Overhead ist eine Größenordnung höher** — [gemessen] 166 bzw. 304 Dependencies und
262 s bzw. 356 s Cold-Build gegen 44/45,5 s bei Rhai. Dazu wasmtimes MSRV 1.95 und ein
Support-Fenster von 2 Monaten für Nicht-LTS-Releases.
Falls WASM später doch relevant wird (z. B. um Mods aus dem Netz zu laden, wo echte Isolation
zählt): **`wasmi`** ist der interessantere Kandidat als wasmtime — reines Rust, `no_std`, 29
Dependencies, MSRV 1.86, mit Fuel und `StoreLimits`. Für ein Terminalspiel braucht es keinen JIT.
### Härtungs-Checkliste, falls Rhai gewählt wird
Alle vier sind nötig, weil die Defaults sie nicht mitbringen:
1. `engine.set_module_resolver(DummyModuleResolver::new())` — sonst liest ein Script `.rhai`-Dateien
vom Dateisystem (empirisch nachgewiesen).
2. `engine.disable_symbol("eval")`.
3. `set_max_operations(...)` und/oder `on_progress(...)` — CPU-Limit ist per Default **aus**.
4. `set_max_string_size` / `set_max_array_size` / `set_max_map_size` — Memory-Limits sind per
Default **aus**.
5. Niemals das Feature `unchecked` aktivieren.
6. Für ein Single-Thread-Terminalspiel das Feature `sync` **weglassen** und `Rc<RefCell<_>>`
nutzen — `sync` erzwingt Locking bei jedem Funktionsaufruf.
Analog für mlua, falls es doch Lua wird: `Lua::new_with(StdLib::STRING|TABLE|MATH, …)`
statt `ALL_SAFE`, plus `set_memory_limit` und `set_hook(every_nth_instruction)`. Factorio
veröffentlicht seine eigene Sandbox-Liste (`loadfile`, `dofile`, `coroutine`, `io`, `os`
entfernt, `debug` auf `getinfo`/`traceback` reduziert, `require` auf das Mod-Verzeichnis
gepatcht) — das ist die am besten erprobte Vorlage, die es gibt.
### Anmerkung zur Umkehrbarkeit
Laut Map ist das Effekt-Vokabular die Mod-API und Scripting setzt auf der Typ-Registry auf.
Wenn Scripts ausschließlich über dieses Vokabular mit dem Spiel reden — also Effekte
zurückgeben statt `Game` direkt zu manipulieren — ist die Engine-Wahl **deutlich umkehrbarer,
als sie aussieht**. Die Brücke wäre dann eine dünne Adapterschicht, kein Fundament. Das spricht
dafür, die Entscheidung nicht zu überdenken und Stufe (c) bewusst schmal zu halten.
---
## Nicht verifiziert
Explizit offen geblieben — nicht geraten:
- **Kein ausgeliefertes kommerzielles Spiel mit Rhai-Modding gefunden.** `github.com/rhaiscript/awesome-rhai`
gibt 404, weder rhai.rs noch das README führen eine „Who uses Rhai"-Liste. Nächste Treffer sind
HEMTT (Arma-Modding-Toolchain, nutzt Rhai für Hooks) und Bevy-Integrationen (`bevy_mod_scripting`,
`bevy_scriptum`).
- **Kein ausgeliefertes Rust-Spiel mit mlua gefunden.** Der Track-Record liegt in Tooling
(WezTerm, Yazi, Vector). WezTerm ist aber ein belastbarer Windows-`vendored`-Präzedenzfall.
- **Kein ausgeliefertes kommerzielles Spiel mit WASM-Modding gefunden.** Veloren hat ein
WASM-Plugin-System, warnt aber selbst: „we currently make no stability guarantees about APIs,
tooling, etc." Ob Veloren inzwischen auf WASI 0.2 / wasmtime umgestellt hat, ließ sich aus
Veloren-Primärquellen **nicht** bestätigen.
- **Build-Zeiten sind Einzelmessungen auf einer Maschine.** Der Registry-Cache-Zustand war nicht
überall gleich (`wasmtime`/`extism` liefen mit kaltem Cache, enthalten also Downloadzeit). Die
Dependency-Zahlen sind dagegen exakt und methodisch identisch erhoben.
- **Rhai-Zahlen im Detail:** Die parallele Erhebung zählte über `cargo tree -e normal` 24
Dependencies und maß 77 s Cold-Debug-Build; die Tabelle oben nennt 44 (Cargo.lock, inkl.
plattform-gegateter Einträge) und 45,5 s (warmer Registry-Cache). Beide Methoden sind korrekt,
sie messen Unterschiedliches. Innerhalb der Tabelle ist durchgängig dieselbe Methode verwendet.
- **Rhai-Hot-Reload-Randfall:** ob Closures, die aus einem alten `AST` stammen und Rust-seitig
noch gehalten werden, nach dem Verwerfen dieses `AST` korrekt funktionieren, sagt das Buch nicht.
- **Lua-Bytecode als Escape-Vektor:** die einschlägige Warnung im Lua-Referenzhandbuch (maliziös
konstruierte Binary-Chunks können den Interpreter zum Absturz bringen) ließ sich über die
abrufbaren Manual-Abschnitte nicht wörtlich belegen. Praktische Konsequenz bleibt unabhängig
davon: Mod-Scripts nur im Textmodus laden, nie vorkompilierte Chunks.
---
## Quellen
**crates.io / Registry**
- https://crates.io/api/v1/crates/rhai
- https://crates.io/api/v1/crates/rhai/1.25.1/dependencies
- https://crates.io/api/v1/crates/rhai/reverse_dependencies
- https://crates.io/api/v1/crates/mlua
- https://crates.io/api/v1/crates/wasmtime
- https://crates.io/api/v1/crates/wasmi
- https://crates.io/api/v1/crates/extism
- https://lib.rs/crates/rhai
- https://lib.rs/crates/mlua
**Rhai**
- https://rhai.rs/book/rust/custom-types.html — „The only requirement of a type to work with Rhai is `Clone`."
- https://rhai.rs/book/rust/build-type.html — `CustomType` / `TypeBuilder`
- https://rhai.rs/book/plugins/module.html — `#[export_module]`
- https://rhai.rs/book/patterns/control.html — Control-Layer-Pattern (`Rc<RefCell<_>>`)
- https://rhai.rs/book/safety/sandbox.html — „Rhai is sand-boxed so a script can never read from outside its own environment"
- https://rhai.rs/book/safety/index.html — Don't-Panic-Garantie, Warnung zu `unchecked`
- https://rhai.rs/book/safety/max-operations.html · https://rhai.rs/book/safety/progress.html
- https://rhai.rs/book/rust/modules/resolvers/index.html · https://docs.rs/rhai/latest/rhai/module_resolvers/struct.FileModuleResolver.html
- https://rhai.rs/book/language/eval.html — „eval is evil"
- https://rhai.rs/book/engine/strict-var.html · https://rhai.rs/book/engine/scope.html · https://rhai.rs/book/engine/ast.html
- https://rhai.rs/book/patterns/hot-reload.html
- https://rhai.rs/book/patterns/multi-threading.html — Kosten des `sync`-Features
- https://rhai.rs/book/about/benchmarks.html — „roughly 2x slower than Python 3"
- https://rhai.rs/book/about/non-design.html — „No bytecodes/JIT"
- https://rhai.rs/book/rust/packages/builtin.html · https://rhai.rs/book/start/features.html
- https://docs.rs/rhai/latest/rhai/struct.Position.html · https://docs.rs/rhai/latest/rhai/enum.EvalAltResult.html · https://docs.rs/rhai/latest/rhai/struct.Engine.html
- https://github.com/rhaiscript/rhai — Repo; Issue #894 „Rhai is unsound (use after free)", offen seit 2024-07-03
- https://github.com/rhaiscript/rhai-fs — separates Opt-in-Crate für Dateizugriff
- https://rustsec.org/advisories/RUSTSEC-2026-0249.html — `smartstring` unmaintained, 2026-08-10
- https://hemtt.dev/rhai/index.html — Arma-Modding-Toolchain mit Rhai
- https://docs.rs/crate/bevy_mod_scripting/latest · https://github.com/jarkonik/bevy_scriptum
**mlua / Lua**
- https://github.com/mlua-rs/mlua — README: `vendored`, Plattformen, „mlua does not provide absolute safety…"
- https://raw.githubusercontent.com/mlua-rs/mlua/main/src/stdlib.rs — `ALL_SAFE = StdLib((1 << 30) - 1)`
- https://docs.rs/mlua/latest/mlua/struct.Lua.html — `new_with`, `sandbox`, `set_memory_limit`, `set_hook`, `scope`, `load`
- https://docs.rs/mlua/latest/mlua/struct.StdLib.html
- https://docs.rs/mlua/latest/mlua/enum.Error.html
- https://docs.rs/mlua/latest/mlua/trait.UserData.html
- https://raw.githubusercontent.com/wezterm/wezterm/main/config/Cargo.toml — `features = ["vendored","lua54","async","send","serialize"]`
- https://lua-api.factorio.com/latest/auxiliary/libraries.html — Factorios Sandbox-Liste (Lua 5.2)
- https://wiki.facepunch.com/gmod/ (Lua 5.1) · https://www.teardowngame.com/modding/api.html (Lua 5.1)
- https://api.luanti.org/ · https://developer.valvesoftware.com/wiki/Dota_2_Workshop_Tools/Scripting
- https://docs.dfhack.org/en/stable/docs/Lua%20API.html · https://www.widelands.org/documentation/lua_index/
- https://noita.wiki.gg/wiki/Modding:_Lua_API · https://luau.org/
**WASM**
- https://docs.wasmtime.dev/security.html — „no raw access to system calls or other forms of I/O…"
- https://docs.wasmtime.dev/stability-release.html — Monatsrelease am 20., LTS alle 12 Versionen, 2 Monate Support sonst
- https://docs.wasmtime.dev/stability-tiers.html · https://docs.wasmtime.dev/stability-platform-support.html
- https://docs.wasmtime.dev/examples-pulley.html — „~10x order-of-magnitude slowdown"
- https://docs.wasmtime.dev/examples-debugging-native-debugger.html
- https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html — Fuel, Epoch, deren Grenzen
- https://docs.rs/wasmtime/latest/wasmtime/struct.Module.html — `deserialize` ist `unsafe`
- https://docs.rs/wasmtime/latest/wasmtime/struct.WasmBacktrace.html · https://docs.rs/wasmtime/latest/wasmtime/struct.FrameInfo.html
- https://docs.rs/wasmtime-wasi/latest/wasmtime_wasi/struct.WasiCtxBuilder.html — default-deny
- https://github.com/bytecodealliance/wasmtime/security/advisories — 46 Advisories, 4 critical / 4 high
- https://extism.org/docs/concepts/pdk — „must be compiled to WebAssembly … before it can be run"
- https://extism.org/docs/concepts/plug-in-system · https://docs.rs/extism/latest/extism/
- https://github.com/extism/js-pdk — QuickJS-in-WASM, Javy-Fork, braucht Binaryen
- https://github.com/bytecodealliance/javy
- https://component-model.bytecodealliance.org/language-support/building-a-simple-component/rust.html — `cargo component` wird deprecated
- https://bytecodealliance.org/articles/the-road-to-component-model-1-0 — Component Model ist nicht 1.0
- https://bytecodealliance.org/articles/WASI-0.3
- https://github.com/wasmi-labs/wasmi · https://docs.rs/wasmi/latest/wasmi/
- https://book.veloren.net/contributors/modders/writing-a-plugin.html — „we currently make no stability guarantees"
- https://tartanllama.xyz/posts/wasm-plugins/ — durchgearbeitetes WASM-Plugin-Beispiel (Demo, kein Produkt)