llvm / llvm/llvm-project

compilation to a wasm (browser app) fails for returning structs bigger 8bytes

Open
#201,933 1 comment 0 reactions 0 assignees View on GitHub
backend:WebAssembly
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

Use google lens, or translate with your AI ;)

Kaputter Pfad
Methoden, die einen Struct >8 Bytes returnen UND über ein Interface-Vtable aufgerufen werden. Konkret: list = 12 Bytes ({i32 size, i32 cap, ptr data}). Direkter Aufruf einer Klassen-Methode (c.tile_tags() auf konkretem Typ) funktioniert; indirekter via Interface (view.tile_tags()) crasht auf WASM.

Was Native anders macht als WASM
x86-64 ABI für struct-Returns ≤16 Bytes:

Callee packt struct in zwei Register (rax = lo 8 Bytes, rdx = hi 8 Bytes)
Caller liest beide aus
Bei list (12 Bytes): size+cap → rax, data ptr → rdx
LLVM macht das automatisch für beide Seiten — auch bei indirekten Calls, weil das IR-Signatur (call %list_string %fnptr(...)) auf beiden Seiten gleich ist → konsistente Behandlung
WASM ABI für struct-Returns >8 Bytes:

WASM hat kein Multi-Value-Return. Convention: Compiler schreibt die Funktion um zu void foo(ptr sret out, args...) — Caller alloziert einen Slot, übergibt seinen Pointer als erstes Argument, Callee schreibt das Ergebnis dort hin
LLVM macht diese Umschreibung (sret-Demotion) erst beim Codegen, nicht im IR
Bei direktem Call: LLVM transformiert Definition und Call-Site konsistent
Bei indirektem Call via Function-Pointer: LLVM hat nur das IR vor sich — es sieht keine Function-Definition, weiß nicht ob die Zielfunktion sret nutzt oder nicht. Caller-Codegen ratet, Callee-Codegen weiß → kann mismatchen
Symptom des Mismatch:

Caller schreibt: "ruf dich auf, gib mir 12 Bytes zurück" — packed-i64 Style
Callee erwartet: "ich kriege einen sret-Pointer als Arg, du den Rest" — sret Style
Ergebnis: erste 8 Bytes (size+cap) landen wo immer der Caller hinhört → size=3 korrekt. Die letzten 4 Bytes (data ptr) sind im Vakuum → data ptr = Garbage → at: [list, 0] greift auf eine Random-Memory-Adresse → WASM-Trap "index out of bounds".
Mein Fix-Versuch (nicht erfolgreich bisher)
Compiler generiert pro Methode einen Stub-Wrapper: void stub(ptr sret, ptr this) { tmp = call real(this); memcpy(sret, &tmp) }
Vtable enthält Pointer zum Stub statt zur Original-Methode
Call-Site nutzt explizit sret-Form: call void stub(ptr sret %slot, ptr %obj)
Beide Seiten haben jetzt explizit sret im IR — LLVM hat keinen Demotion-Spielraum mehr. Sollte funktionieren.

Tut's aber nicht auf WASM — size kommt durch, data ptr immer noch korrupt. Heißt: irgendwo im Pfad Caller→Stub→Real-Fn→Stub→Caller verschwinden die letzten 4 Bytes immer noch. Das könnte am inneren direkten Call im Stub liegen (%v = call %list_string @real(this)) — dort macht LLVM wieder seine eigene Demotion, vielleicht inkonsistent.

Mein letzter Edit ändert das innere store %list_string %v, ptr %sret auf explizites memcpy(sret, tmp_slot, 12) — das umgeht das Struct-Value-Handling. Müsste ich noch im Browser testen.

Klar gesagt: AIML's interface-polymorphism mit Container-Return-Typen funktioniert auf WASM nicht zuverlässig. Auf native ist es OK. Der saubere Wurzel-Fix ist ein voller Compiler-Pfad der weder im Stub noch in der Call-Site auf LLVM-Auto-ABI vertraut — alle struct-Returns gehen explizit über sret-out-Parameter, von Definition bis Call-Site.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.