Tegyük fel, hogy felmerül az igény a ~-ra, azaz hogy egy programból kell tudnunk hívni WASM eljárásokat, és vice-versa, azoknak is kell hívniuk a mi natív függvényeinket. Na erre nem találtam épkézláb megoldást.
- Az egyik ilyen a wasm-micro-runtime, de hogy ez mennyire szar, azt semmi sem szemlélteni jobban, minthogy félezer fájl, 9 MEGA az nekik "micro"... Hát nekem hányadék bloatware.
- Ott volt aztán még a wasm3, ami méretét tekintve már inkább elfogadható, viszont agyfasz az API-ja, és sajnos nincs már karbantartva, mert a fejlesztőjének a házát szarrábombázták a ruszkik.
- Van még a Linux kernelbe ágyazható cervus, szintén elhalt, 7 éve bottal sem piszkálták, pedig nyitott jegyek vannak rajta.
- ...stb.
És akkor ott van még az is, hogy ezek egyike sem biztonságos, mert ezekkel BÁRMILYEN dll / so dinamikusan betölthető és futtatható, így a beágyazó programnak esélye sincs kordában tartani a benne futó szkriptet. Szóval nem az igazi.
Egy szónak is száz a vége, már megint az lett, hogy megírtam a libet magamnak, mert amit mindenki más használ, az nekem nem tetszik. Hát, ez van.
Előnyei:
- ha az első a micro, na akkor ez nano, merthogy 9.5 Mega helyett csak 116 Kilobájt az egész (kb. 1900 SLoC)
- mégis megvalósítja a teljes WASM MVP (Minimal Viable Product) szabványt
- egyetlen függőségmentes ANSI C / C++ fejlécfájl (na jó, libm kell neki, de a WA_NOLIBM define kikapcsol két utasítást és úgy már tényleg csak compiler built-inek elegek)
- pofonegyszerűen használható, faék egyszerű az API-ja
- maximális kontroll afelett, hogy mit csinálhatnak a szkriptek (mennyi memóriát foglalhatnak, milyen függvényeket hívhatnak stb.)
- bound check és hasonló nyalánságok (futás idejű típusellenőrzésen kívül szinte minden)
- baromi gyors, a többihez képest lobog a hajad tőle (két okból: bulk memóriafoglalás kev;s bufferre; és az értelmetlen típusellenőrzés helyett (ami O(N)) csak az elemszámot ellenőrzni (ami O(1))
- a többi megoldás memóriaigényének 10%-val is beéri (nem, nem írtam el, ugyanazt a wasm modult futtatni tized annyi memóriát fogyaszt)
- a többivel ellentétben memleak mentes (ez ráadásul garantált, mivel nem foglalgat agyba-főbe kis memóriablokkokat, hanem mindössze egy fél maréknyi buffert használ)
- nem támogat semmiféle emscripten, WASI meg hasonló 3rd party baromságot, így a szkript nem tud kitörni a sandboxából. Nincs mivel.
- fordítható debugger támogatással, példa wadbg debugger GUI applikáció is van hozzá, ennek demonstrálására (kicsi SDL-es, portolható cucc).
Példa használatra:
/* függvénykönyvtár behúzása, stb módra, nem kell linkelni semmivel */ #define WA_IMPLEMENTATION #include "wa.h" /* wasm bináris bitkolbásza, fordította "Clang --target=wasm32", yours truly */ uint8_t wasmbinary[1234] = { ... }; /* futás idejű linkelő tábla és kontextus */ RTLink link[] = { /* név ki be proto */ { "malloc", &malloc, 0, WA_ll }, /* host -> WASM */ { "realloc", &realloc, 0, WA_lll }, /* host -> WASM */ { "free", &free, 0, WA_vl }, /* host -> WASM */ { "add", NULL, -1, WA_fff }, /* WASM -> host */ { "sub", NULL, -1, WA_fff }, /* WASM -> host */ { "globvar", &globvar, 0, WA_i }, /* megosztott változó */ { 0 } }; Module m; /* betöltés, inicializálás */ wa_init(&m, wasmbinary, 1234, link); /* egy WASM függvény hívása C-ből. Elöször kikeressük a szimbólumot (ha nem felejtettük el, hogy a link[] tömb hányadik sora, akkor keresgélés helyett csak simán O(1) lekérés) */ int addfunc = cavinton_klub? wa_sym(&m, "add") : link[3].fidx; /* bemeneti paraméterek átadása és hívás */ wa_push_f32(&m, 1.0); wa_push_f32(&m, 2.0); ret = wa_call(&m, addfunc); printf("Eztetet adta vissza: %f\n", ret.f32); /* többet nincs mit elmondani, kérem kapcsoljaki */ wa_free(&m);
Ennyi. Ennél faékebb egyszerűségű és könnyebben integráható API-t nem bírtam kiagyalni. Négy függvény init / push / call / free és kész (oké, a push-ból van több változat, mindenféle típushoz, de akkor is 12 függvény cakk-um-pakk). A valós idejű linkelés meg egy struct tömbbel zajlik, aminek négy mezője van:
- első a szimbólum (UTF-8 sztring)
- második egy memóriacím, ezt te adod meg akkor, ha a WASM-nek akarod átadni
- harmadik egy függvényindex, ezeket meg a WASM adja meg, hogy meg tudd hívni
- negyedik meg egy prototípust kódoló bitmaszk
Bármelyik globális WASM változó láthatóvá tehető host oldalon, ha felvesszük a táblába a WASM szimbólum nevét egy host memóriacímmel. Másik irány is működik, ekkor "extern"-nek kell definiálni a WASM modulban.
De ami biztonság szempontjából nagyon fontos, a WASM szkript nem tud semmi mást meghívni, csak azokat, amik szerepelnek a listán.
Viszont ott bármilyen függvénnyel működik, de azért lehetőség van saját dispatcher megadására is egy WA_DISPATCH define-nal.
Szabad és Nyílt Forráskódú, licensze a megengedő MIT.
- 1070 megtekintés
Hozzászólások
Wow! Szép! Játszadozok WASM-mal, bár én csak saját megbízható kódot futtatok böngészőben, ezért elég volt eddig C-ből natívra fordítani, hogy böngészőn kívül is futtassam.
Amire hasznos lehetne még, ha tudnánk debuggolni is a WASM-ot. Lehetne szerinted a megoldásoddal?
- A hozzászóláshoz be kell jelentkezni
én csak saját megbízható kódot futtatok böngészőben
Ez nem az. Ez arra való, hogy behúzod ezt az egy fejlécfájlt a forrásodba, és akkor a programodban tudsz WASM binárisokat futtatni. (Azaz olyan, mintha a programod lenne a böngésző.)
ezért elég volt eddig C-ből natívra fordítani, hogy böngészőn kívül is futtassam.
A natív nem mindig opció. Ez tipikusan arra van, amikor szkriptet kell tudnod futtatni a programodból. Kb. mintha Luát ágyaznád be, csak annál sokkal kissebb és egyszerűbb. Erre kézenfekvő bájtkódot használni, mert nem kell hozzá lexer, tokenizáló, AST, fittyfene.
A rendelkezésre álló bájtkód fajtákból meg a WASM azért jó, mert rengeteg nyelvet le tudsz fordítani WASM bájtkódra, pl. Javascriptet, de akár Luát is. Szóval ha van egy ilyen bájtkód VM-ed, akkor bármilyen szkriptet tudsz vele futtatni, ráadásul oprendszer és platform függetlenül.
Amire hasznos lehetne még, ha tudnánk debuggolni is a WASM-ot. Lehetne szerinted a megoldásoddal?
Hogyne. Ha defineolod a DEBUG-ot, mielőtt behúznád a wa.h-t, akkor ilyen részletes kimenete lesz. Ez a teljes WASM binárist kidebuggolja neked (amolyan objdump / readelf szerűen), valamint minden egyes API hívás is generál egy kimenetet.
Ha magát az utasításvégrehajtást akarnád lépésenként debuggolni, az is egyszerűen belerakható, mivel nagyon kicsi és átláható az egész forrása: a wa_interpret-ben van egy ciklus, ez olvassa be és értelmezi egyesével a bájtkódot (minden iteráció pontosan egy utasítást hajt végre). Ide kell csak berakni pár printf-et, és máris pontosan láthatod, mikor mi hajtódik végre.
De igazából még egy rendes debugger is rittyenthető lenne köré egész könnyedén. Annyi módosítás kellene csak, hogy a wa_interpret-ből ki kell venni a while-t és átnevezni wa_step-re, aztán meg berakni helyette egy ilyen új wa_interpret-et:
static int wa_interpret(Module *m) { while(!m->err_code && m->pc < m->byte_count) wa_step(m); }
Ezzel teljesen működőképes és visszafelé kompatíbilis maradna, a debuggernek meg csak annyi lenne a dolga, hogy a wa_interpret helyett a wa_step-et hívogná egyesével, a hívások között meg kidumpolhatja a Module-t. Ja, simán megoldható, és nem is lenne hülyeség egy fullos WASM debuggert csinálni belőle.
- A hozzászóláshoz be kell jelentkezni
Offtopic: ret, m, wa, wa_push_f32, fidx, stb. csodálatos nevek, amik kiakasztanak. Mintha számítana, hogy hány karakter a változó, de legalább brutálisan ront az olvashatóságon. ... tudom az összes kód ilyen...
- A hozzászóláshoz be kell jelentkezni
A "wa_" prefix a libnévből jön, a "push" nem lesz hosszabb, a "ret" helyett nem lehet "return" mert az kulcsszó, ami meg a "u32", "f32" stb. röviditéseket illeti, tessék a ByteAlliance-nél reklamálni, miért így definiálták a WASM speckóban.
Mintha számítana, hogy hány karakter a változó
Igen, nagyon is számít. Nem tudtam volna másfél nap alatt nulláról megcsinálni ezt a libet, ha "m" helyett mindig az gépeltem volna, hogy "ModuleInstantiatedFromWASMBinary". Gyorsan gépelek, de azért ennyire még én sem, hetekig tartott volna.
de legalább brutálisan ront az olvashatóságon.
Offtopik: szerintem meg épp hogy drasztikusan javít az olvashatóságon, ki nem állhatom az olyan forrásokat, ahol fél képernyőt elfoglal egyetlen változónév. Sokkal könnyebben átlátható az "fidx", mint az "IndexOfTheFunctionToBeCalled". Az előbbire elég csak rápillantanom, míg utóbbinál megakadok, és oda kell figyelnem, hogy végigolvassam (mert mi van ha nem is az a változó van ott, hanem mondjuk az "IndexOfTheFunctionToBeExported"?)
- A hozzászóláshoz be kell jelentkezni
Nem arról van szó hogy hosszúnak kell lennie, hanem arról lényegretörő legyen és könnyen olvasható. Nyilván sok gyakorlás kell ahhoz hogy pont jó neveket találjunk ki. Én amikor kódolok minden második leütésem a tab, mert automatikusan kitalálja hogy mit akarok.. Így nem érzem hogy számítani, hogy hány karakter a változó, no meg 90-es években is volt ugye a ctrl-c+v.
Nem szeretek másokat szívatni, az egybetűs változónevekkel, így mástól is elvárom hogy ne szívasson. Programozás nem gyors író verseny, fontosabb számomra a kódminőség.
Kapcsolódó: Tiszta kód elvek - Összefoglaló - Elnevezések
Nyilván amit a környezet vagy a külső lib ad azt nem tudod befolyásolni. Őket se értem, de ez van. :)
- A hozzászóláshoz be kell jelentkezni
Én amikor kódolok minden második leütésem a tab, mert automatikusan kitalálja hogy mit akarok..
Direkt olyan példát írtam, aminél nem működik az autocompletition. Szerintem észre se vetted, mi a küönbség a két változónév között, akkor nem írtad volna ezt (most nyilván visszaolvasod mit írtam, alaposan megfigyeled a két nevet, és közben kizökkensz jelen poszt olvasásából emiatt...)
Ezen kívül semmit sem javít az olvashatóságon a tab, továbbra is baromira nehéz lesz olvasni az ilyen kódot. Egyszerűen sokáig tart, mire felismered a változóneveket. 4-5 karakteres változóneveknél elég egy pillantás is, hogy észrevedd a különbséget, 20-30 karakteres neveknél meg mindig kizökkensz, mert alapos odafigyelésre van szükség.
Nem szeretek másokat szívatni, az egybetűs változónevekkel, így mástól is elvárom hogy ne szívasson.
Nehogy már ne tudd kitalálni a "Module m" definícióból és a kontextusból, hogy az "m" modult jelent. Mi abban a szivatás, hogy a leggyakrabban használt változó neve rövid, a ritkán használté meg hosszabb?
Őket se értem
Mégegyszer, ők is tisztában vannak vele, hogy a hosszú változónevekkel sokkal jobban szivatod az embereket, mert write-only lesz tőle a kódod, sokáig tart elolvasni, nehéz lesz dekódolni és értelmezni. Ne a változónevekkel akard elmagyarázni, hogy mit is csinál a programod.
Teszt: vedd észre a különbséget:
fidx
gidx
vs.
Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu
Taumatawhakatangihangakoauauotamaloaturipukakapikimaungahoronukupokaiwhenuakitanatahu
Mennyi ideig tartott az első és mennyi ideig a második esetben? (Nem tenyereltem rá a billentyűzetre, ez egy létező név.)
- A hozzászóláshoz be kell jelentkezni
Tetszik, meg fogom nézni. Már csak az a kérdés, hogy honnan van ennyi időd. :)
- A hozzászóláshoz be kell jelentkezni
Jó vicc, folyton azt érzem, hogy sosincs elég időm... Keveset alszok, asszem.
- A hozzászóláshoz be kell jelentkezni
Na kérem, pár újdonság:
- WA_DEBUGGER define asch kolléga kérésére, pofon egyszerű, de full professzionális interfész lett végül, doksi. A használatát demonstrálandó összedobtam egy GUI-s debugger alkalmazást, így látható, mire képes. Pár óra alatt rittyentettem össze az appot, ne várjatok tőle szofitikált UI-t, de működik, minden infót megmutat, diszasszemblál és még a breakpointokat is be lehet állítani vele.
- WA_MAXMEM define-al meg lehet adni, hogy maximálisan mennyi memóriát ehetnek az insztanszok.
- WA_ALLOW_GROW define hatására nem foglalja le előre a címteret, hanem menet közben dinamikusan növeli, amikor kell (az WA_MAXMEM itt is érvényes).
- WA_MAXALLOC define-ban megadható a shared bufferek száma. Ez úgy működik, mint a valódi procikon az MMU, a címtérfordítás a hoszt és a modul között teljesen automatikus. A különség annyi, hogy igazi hardveren fix méretű lapok vannak, itt meg WA_MAXALLOC darab változó méretű. Ezt a nem szabványos fícsört használva feleslegessé válik a bufferek másolgatása a hoszt és a modul között (JavaScript Module.HEAPU8.set ugye... na miért lassú a JS?)
- A hozzászóláshoz be kell jelentkezni
Faragtam még rajta egy kicsit:
- lett WA_NOFLOAT define opció, ha meg van adva, akkor teljesen lebegőpontos arithmetika mentes, így mikrokontollerekre is könnyedén lefordítható (valamint durván lecsökkenti a lefordított kód méretét is).
- javítottam kicsit a memóriafoglálásán, kevesebb memóriát eszik.
- globális változók láthatóvá tehetők host oldalon, ehhez csak egyetlen sort kell megberakni az RTLink táblába, illetve másik irányba, WASM oldali "extern" is ugyanígy működik.
- ennek megfelelően bővült a debugger, lett BRK_GET és BRK_SET breakpoint (globális változó olvasása / írása), valamint
- a wadbg app is utánna lett húzva, és kapott egy "Globals" tabot, ahol az összes globális változó címe és épp aktuális értéke megcsodálható.
A teljes forrás most 116 Kilobájt, 1826 SLoC, és tovább nem is tervezem bővíteni, már mindent tud, amit bele akartam rakni. Nem akarom, hogy bloateddé váljon, a cél az, hogy akár mikrokontrollerekre is lefordítható maradjon. Hibajavítás még lehetséges, ha valaki bugot talál benne.
- A hozzászóláshoz be kell jelentkezni
Hát ez küzdős volt, de sikerült feltunningolni annyira, hogy minden CLang "-O3" optimalizáló által generált kódot képes már futtatni.
Újdonságok:
- MVP-n túl minden olyan extension, ami -O3 optimalizált kódhoz kell
- kiterjesztett opkódok
- data segment kezelés (memory.init extension miatt kellett)
- új címzési módszer, direkt elérés
Ez azért volt szopó, mert csomó minden egyáltalán nincs dokumentálva (például hogy ilyenkor nincs külön sign extend utasítás, hanem feltételezi, hogy a load már sign extendel magától, vagy hogy a "call memcpy" hívást kiterjesztett 0xFC0A opkódnak fordítja 0x10 call helyett, vagy hogy a call néha 0x12-es opkód tail optimalizált esetén...)
Az új címtérrel már négyre bővült a lehetőségek száma (statikus, egyből mindent allokál; dinamikus, úgy allokál, ahogy szükség van rá; megosztott bufferek és most direkt). Ez úgy működik, hogy a modul címterét kettévágja: a WASM-ban megadott memory.limit (vagy a felhasználó által beállított WA_MAXMEM) alatti címeket dinamikusan allokálja, pont mint a másik dinamikus címzésnél, az e feletti címeket azonban direktben leképezi a host címterére. Ez azért jó, mert így mindegy, hogy a host vagy a modul allokálta-e a buffer-t szabány malloc-al, mindkettő pont ugyanúgy tudja használni annélkül, hogy másolgatni kéne a memóriát a host heap és a modul heap között, vagy hogy címfordításra lenne szükség (ami O(N) a megosztott bufferek számának függvényében). Ez a lehető leghatékonyabb megoldás, hátránya, hogy nincs boundary check, sem semmiféle más ellenőrzés, szóval ésszel kell használni.
A doksit is frissítettem, beraktam pár ASCII artot a címzési módszerek szemléltetésére, remélem így érhető, mikor melyiket érdemes használni.
- A hozzászóláshoz be kell jelentkezni