WebAssembly enpowered eForth on web browsers. Is it faster? Is it more portable? Yes, and Yes.
Well, on my aged laptop, the impression is pretty exciting! It's at least 5x faster than pure Javascript implementation on a browser and at 60% speed of C/C++ natively compiled code on CPU. It was at 25% of native a year ago but as Javascript JIT improves, it now runs faster as well. Not bad at all! On the portability end, though not exactly plug-and-play but some simple alteration turned my C code web-enabled. Of course, WASM has yet to integrate with the front-end well enough, so updating DOM is a different feat. If we want to venture beyond being a terminal app some UI glue is still required.
Regardless, it brought me warm smiles seeing eForth run in a browser. Better yet, it's straight from C/C++ source code. Other popular scripting languages such as Python, Ruby are trending toward WASM/WASI implementation as well. However, unlike FORTH, they depend mostly on JIT without a built-in compiler, the interpreter-in-an-interpreter design will likely cap the top-end performance (i.e. stuck at 10~15% of native speed, so far).
With WASM, the interoperability between different languages become a thing of the near future. If words can be compiled directly into WASM opcodes, OS and peripherals can be accessed through WASI, adding an interactive graphical front-end, weForth can become a worthy scripting alternative for Web.
- supports 32-bit float
- call interface from FORTH into Javascript functions
- integrated with Web graphics (SDL2, WebGL) and Physics Engine (Jolt)
- built-in editor, IDE-style interactive front-end
- access to ss, dict, and VM memory (via WebAssembly.Memory) from Javascript
- browser can fetch from self-host web server (i.g. python3 -m http.server)
- can run parallel in Web Worker threads
Serving as the core of demos, the templates under ~/template directory are used in different built shown below. They are organized by the Makefile
-
eforth.html - vanilla weForth, one single-threaded HTML. A good place to start.
-
ceforth.html - weForth, now with integrated editor, single-threaded.
-
weforth.html - weForth runs in a worker thread, can also do fancy GUI stuffs now Javascript module/files are included in the HTML for added functionality
+ weforth_helper.js - vocabulary lookup table + weforth_worker.js - worker thread proxy object + weforth_sleep.js - sleep/delay support for async environment + weforth_logo.js - Turtle Graphic implementation + weforth_jolt.js - Jolt Physics Engine proxy object + file_io.js - file IO support + jolt_core.js - Jolt + THREE.js implementation + jolt_vehicle.js - Wheeled Vehicle class for simulator
The following Forth scripts under ~/tests/forth are also included for GUI integration demo
+ forth/logo.fs - Turtle Graphics + forth/jolt.fs - Jolt Physics Engine + forth/shapes.fs - random Jolt shape generator + forth/whisker.fs - Whisker Robots generator + forth/vehicle.fs - 2-wheel bikes, and 4-wheel cars generator
Build | HowTo | Try Online | Note |
---|---|---|---|
Bare-bone eForth on Web | make zero | eforth.html | -O2 works OK, -O3 Emscripten spits wrong code |
Single WASM file | make one | ceforth.html | |
Extra Web Worker thread | make two | weforth.html | for demo, enter> s" forth/whisker.fs" included 40 bots |
python3 -m http.server
http://localhost:8000/tests/eforth.html, ceforth.html or weforth.html
To communicate between Forth and Javascript engine, weForth implemented a word 'JS' and a function call_js(). They utilize Emscripten EM_JS macro that postMessage from C++ backend-side to onmessage on browser-side. Depends on your need, handler can be very simple to complicated.
this.onmessage = e=>{
if (e.data[0]=='js') this.eval(e.data[1])
}
> 54321 s" alert('%d ... hello world!')" JS⏎
const ex = (ops)=>Function( ///< scope limiting eval
`"use strict"; this.update('${ops}')`
).bind(logo)()
vm.onmessage = e=>{ /// * wait for worker's response
let k = e.data[0], v = e.data[1]
switch (k) {
case 'cmd': to_txt(v); break
case 'dc' : show_dict(v); break
case 'us' : usr.innerHTML = v; break
case 'ss' : ss.innerHTML = v; break
case 'mm' : mm.innerHTML = v; ok=1; break
case 'js' : ex(v); break
default: console.log('onmessage.error=>'+e)
}
}
> s" forth/logo.fs" included⏎
> : seg FD 30 RT ;⏎
> : color 2* PC ;⏎
> : daz 100 0 do i color i seg loop ;⏎
> daz⏎
make debug
read tests/ceforth.wasm.txt (really long)
Benchmark (on my aged IBM X230 w Intel i5-3470@3.2GHz)
Simple 10M tests
: xx 9999 FOR 34 DROP NEXT ;⏎
: yy 999 FOR xx NEXT ;⏎
: zz MS NEGATE yy MS + ;⏎
zz⏎
Note:
- eForth.js uses JS straight, can do floating-points
- uEforth v7 uses Asm.js, build Forth up with JS "assembly".
- weForth v1 uses token indirected threaded
- weForth+switch(op), is 2x slower than just function pointers.
- weForth v1.2 without yield in nest(), speeds up 3x.
- WASM -O3 => err functions (wa.*) not found
- FireFox v122, is 2x faster than v120
- Chrome is about 10% slower than FireFox
- weForth 4.2 w float32-enabled, runs faster than int32, but why?
- weForth 4.2 w float32/real-time is slower due to message passing
- weForth 4.2 w float32/real-time is even slower without WebGL, why?
- inter-VM communication
- Emscripten WASM Worker (for faster call and shared memory), link
- ChaCha20-Poly1305, IETF spec.
- ChaCha20 cipher lib. See JS cryptography libs [libsodium.js](http + A 16-node AES cipher AES - 5 different modes comparison
- Physics Engine
- add network system (SD_net)
- review WebSerial
- review wasmtime (CLI), perf+hotspot (profiling)
- review DragonRuby/mRuby (SDL) => 3D preferred
- review R3, Forth CPU visualizer (SDL) => 3D preferred
- review GraFORTH spec.
- File system (FS/IndexedDB)
- Editor => CodeMirrow chosen
- 2D graphic (SDL_gfx, SDL_image)
- Character graphic (SDL_ttf or HTML5)
- 3D graphic (GL) => Three.js WebGL
- Audio (SDL_media)
- use WASM stack as ss (brk, sbrk)
- macro-assembler
- SDL2
- WebGL
- Physics Engine