Skip to content

Commit a3a0dce

Browse files
committed
Enable System Emulation in Web Browsers
User-space emulation has been supported and deployable in WebAssembly since sysprog21#389, but system emulation was not yet available. With sysprog21#508, system emulation was introduced, and later sysprog21#551 added support for trap-and-emulate of guest Linux SDL syscalls, enabling offloading to the host’s SDL backend. This commit bridges these components together to enable full system emulation in the browser. xterm.js is leveraged as the frontend terminal, bridged with the backend VM shell through a custom buffer management mechanism. This mechanism provides a shell experience in the browser that closely resembles a real terminal. To handle terminal input without blocking the browser’s cooperative multitasking model, the original approach of mapping the read() system call to window.prompt() is avoided. Blocking behavior would freeze the browser’s event loop and degrade responsiveness. Instead, input from xterm.js is stored in a shared input buffer. The rv32emu periodically checks this buffer when handling external interrupts, and if input is available, it is read and consumed by the guest OS shell. The SDL graphic and sound backend are also supported. After booting the guest Linux system, users can run graphical applications such as doom-riscv, quake, or smolnes. These programs can be exited using Ctrl+C or their built-in exit funtionality. To reduce the size of the WebAssembly build and for the sake of the modularity, the project is now separated into user and system targets. As a result, two dedicated HTML files and corresponding preload JavaScript files are introduced: - user.html with user-pre.js - system.html with system-pre.js Navigation buttons are added to the index pages of both user and system demos, allowing users to switch easily between the two modes. Note that these navigation buttons are only functional when both user and system demos are deployed together, otherwise, the target pages may not be reachable. To improve usability, logic is implemented to disable and enable the "Run" button at appropriate times, preventing accidental re-execution when the process is already running. Additional improvements: - Ensure xterm.js uses \r\n instead of \n when logging to correctly move the cursor to the beginning of the line. - Add a new src/emsc.h,c to store Emscripten-related variables and function declarations and implementations for better management. This implementation has been tested on the latest versions of Chrome, Firefox, and Safari. To serve user space emulation index page: $ make start-web CC=emcc ENABLE_SDL=1 -j8 To serve system emulation index page: $ make start-web CC=emcc ENABLE_SYSTEM=1 ENABLE_SDL=1 INITRD_SIZE=32 -j8
1 parent 58e975e commit a3a0dce

File tree

13 files changed

+508
-35
lines changed

13 files changed

+508
-35
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,8 +286,8 @@ $(OUT)/emulate.o: CFLAGS += -foptimize-sibling-calls -fomit-frame-pointer -fno-s
286286

287287
include mk/external.mk
288288
include mk/artifact.mk
289-
include mk/wasm.mk
290289
include mk/system.mk
290+
include mk/wasm.mk
291291

292292
all: config $(BUILD_DTB) $(BUILD_DTB2C) $(BIN)
293293

@@ -301,6 +301,7 @@ OBJS := \
301301
riscv.o \
302302
log.o \
303303
elf.o \
304+
emsc.o \
304305
cache.o \
305306
mpool.o \
306307
$(OBJS_EXT) \

assets/wasm/html/system.html

Lines changed: 315 additions & 0 deletions
Large diffs are not rendered by default.

assets/wasm/html/index.html renamed to assets/wasm/html/user.html

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
</select>
122122
<button id="runButton">Run</button>
123123

124+
<button id="toSysEmuButton">
125+
🚀 Navigate to System Emulation
126+
</button>
127+
124128
<div class="emscripten">
125129
<progress value="0" max="100" id="progress" hidden=1></progress>
126130
</div>
@@ -137,6 +141,13 @@
137141
var spinnerElement = document.getElementById('spinner');
138142
var runButton = document.getElementById("runButton");
139143
runButton.addEventListener("click", runButtonClickHandler);
144+
var toSysEmuButton = document.getElementById("toSysEmuButton");
145+
toSysEmuButton.addEventListener("click", toSysEmuButtonClickHandler);
146+
147+
function toSysEmuButtonClickHandler() {
148+
console.log("Navigate to system emulation");
149+
window.location.href = "./system"
150+
}
140151

141152
var elfDropdown = document.getElementById("elfDropdown");
142153
for (var i = 0; i < elfFiles.length; i++) {
@@ -163,11 +174,7 @@
163174
element.scrollTop = element.scrollHeight;
164175
}
165176
Module._indirect_rv_halt();
166-
/* important to add some delay for waiting cancellation of main loop before next run */
167-
/* Otherwise, get error: only one main loop can be existed */
168-
setTimeout(() => {
169-
Module['onRuntimeInitialized'](target_elf);
170-
}, 1000);
177+
Module['run_user'](target_elf);
171178
}
172179

173180
var Module = {

assets/wasm/js/pre.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

assets/wasm/js/system-pre.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Module["noInitialRun"] = true;
2+
3+
Module["run_system"] = function (cli_param) {
4+
callMain(cli_param.split(" "));
5+
};
6+
7+
// index.html's preRun needs to access this, thus declaring as global
8+
let term;
9+
10+
Module["onRuntimeInitialized"] = function () {
11+
const input_buf_ptr = Module._get_input_buf();
12+
const input_buf_cap = Module._get_input_buf_cap();
13+
14+
term = new Terminal({
15+
cols: 120,
16+
rows: 11,
17+
});
18+
term.open(document.getElementById("terminal"));
19+
20+
term.onKey(({ key, domEvent }) => {
21+
22+
let heap = new Uint8Array(
23+
Module.HEAPU8.buffer,
24+
input_buf_ptr,
25+
key.length,
26+
);
27+
28+
for (let i = 0; i < key.length && i < input_buf_cap; i++) {
29+
heap[i] = key.charCodeAt(i);
30+
}
31+
// Fill zero
32+
for (let i = key.length; i < input_buf_cap; i++) {
33+
heap[i] = 0;
34+
}
35+
36+
Module._set_input_buf_size(key.length);
37+
38+
term.scrollToBottom();
39+
});
40+
};

assets/wasm/js/user-pre.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Module["noInitialRun"] = true;
2+
3+
Module["run_user"] = function (target_elf) {
4+
if (target_elf === undefined) {
5+
console.warn("target elf executable is undefined");
6+
return;
7+
}
8+
9+
callMain([target_elf]);
10+
};

mk/wasm.mk

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ deps_emcc :=
33
ASSETS := assets/wasm
44
WEB_HTML_RESOURCES := $(ASSETS)/html
55
WEB_JS_RESOURCES := $(ASSETS)/js
6-
EXPORTED_FUNCS := _main,_indirect_rv_halt
6+
EXPORTED_FUNCS := _main,_indirect_rv_halt,_get_input_buf,_get_input_buf_cap,_set_input_buf_size
77
DEMO_DIR := demo
88
WEB_FILES := $(BIN).js \
99
$(BIN).wasm \
@@ -29,7 +29,19 @@ CFLAGS_emcc += -sINITIAL_MEMORY=2GB \
2929
-s"EXPORTED_FUNCTIONS=$(EXPORTED_FUNCS)" \
3030
-sSTACK_SIZE=4MB \
3131
-sPTHREAD_POOL_SIZE=navigator.hardwareConcurrency \
32-
--embed-file build/jit-bf.elf@/jit-bf.elf \
32+
--embed-file build/timidity@/etc/timidity \
33+
-DMEM_SIZE=0x20000000 \
34+
-DCYCLE_PER_STEP=2000000 \
35+
-O3 \
36+
-w
37+
38+
ifeq ($(call has, SYSTEM), 1)
39+
CFLAGS_emcc += --embed-file build/linux-image/Image@Image \
40+
--embed-file build/linux-image/[email protected] \
41+
--embed-file build/minimal.dtb@/minimal.dtb \
42+
--pre-js $(WEB_JS_RESOURCES)/system-pre.js
43+
else
44+
CFLAGS_emcc += --embed-file build/jit-bf.elf@/jit-bf.elf \
3345
--embed-file build/coro.elf@/coro.elf \
3446
--embed-file build/fibonacci.elf@/fibonacci.elf \
3547
--embed-file build/hello.elf@/hello.elf \
@@ -40,12 +52,9 @@ CFLAGS_emcc += -sINITIAL_MEMORY=2GB \
4052
--embed-file build/riscv32@/riscv32 \
4153
--embed-file build/DOOM1.WAD@/DOOM1.WAD \
4254
--embed-file build/id1/pak0.pak@/id1/pak0.pak \
43-
--embed-file build/timidity@/etc/timidity \
44-
-DMEM_SIZE=0x60000000 \
45-
-DCYCLE_PER_STEP=2000000 \
46-
--pre-js $(WEB_JS_RESOURCES)/pre.js \
47-
-O3 \
48-
-w
55+
--pre-js $(WEB_JS_RESOURCES)/user-pre.js
56+
endif
57+
4958

5059
$(OUT)/elf_list.js: tools/gen-elf-list-js.py
5160
$(Q)tools/gen-elf-list-js.py > $@
@@ -132,11 +141,22 @@ define cp-web-file
132141
endef
133142

134143
# WEB_FILES could be cleaned and recompiled, thus do not mix these two files into WEB_FILES
135-
STATIC_WEB_FILES := $(WEB_HTML_RESOURCES)/index.html \
136-
$(WEB_JS_RESOURCES)/coi-serviceworker.min.js
144+
STATIC_WEB_FILES := $(WEB_JS_RESOURCES)/coi-serviceworker.min.js
145+
ifeq ($(call has, SYSTEM), 1)
146+
STATIC_WEB_FILES += $(WEB_HTML_RESOURCES)/system.html
147+
else
148+
STATIC_WEB_FILES += $(WEB_HTML_RESOURCES)/user.html
149+
endif
150+
151+
start_web_deps := check-demo-dir-exist $(BIN)
152+
ifeq ($(call has, SYSTEM), 1)
153+
start_web_deps += $(BUILD_DTB) $(BUILD_DTB2C)
154+
endif
137155

138-
start-web: check-demo-dir-exist $(BIN)
156+
start-web: $(start_web_deps)
157+
$(Q)rm -f $(DEMO_DIR)/*.html
139158
$(foreach T, $(WEB_FILES), $(call cp-web-file, $(T)))
140159
$(foreach T, $(STATIC_WEB_FILES), $(call cp-web-file, $(T)))
160+
$(Q)mv $(DEMO_DIR)/*.html $(DEMO_DIR)/index.html
141161
$(Q)python3 -m http.server --bind $(DEMO_IP) $(DEMO_PORT) --directory $(DEMO_DIR)
142162
endif

src/devices/uart.c

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
#include <string.h>
1212
#include <unistd.h>
1313

14+
#if defined(__EMSCRIPTEN__)
15+
#include "emsc.h"
16+
#endif
17+
1418
#include "uart.h"
1519
/* Emulate 8250 (plain, without loopback mode support) */
1620

@@ -33,15 +37,42 @@ void u8250_update_interrupts(u8250_state_t *uart)
3337
uart->current_intr = ilog2(uart->pending_intrs);
3438
}
3539

40+
#if defined(__EMSCRIPTEN__)
41+
#define INPUT_BUF_MAX_CAP 16
42+
static char input_buf[INPUT_BUF_MAX_CAP];
43+
static uint8_t input_buf_start = 0;
44+
uint8_t input_buf_size = 0;
45+
46+
char *get_input_buf()
47+
{
48+
return input_buf;
49+
}
50+
51+
uint8_t get_input_buf_cap()
52+
{
53+
return INPUT_BUF_MAX_CAP;
54+
}
55+
56+
void set_input_buf_size(uint8_t size)
57+
{
58+
input_buf_size = size;
59+
}
60+
#endif
61+
3662
void u8250_check_ready(u8250_state_t *uart)
3763
{
3864
if (uart->in_ready)
3965
return;
4066

67+
#if defined(__EMSCRIPTEN__)
68+
if (input_buf_size)
69+
uart->in_ready = true;
70+
#else
4171
struct pollfd pfd = {uart->in_fd, POLLIN, 0};
4272
poll(&pfd, 1, 0);
4373
if (pfd.revents & POLLIN)
4474
uart->in_ready = true;
75+
#endif
4576
}
4677

4778
static void u8250_handle_out(u8250_state_t *uart, uint8_t value)
@@ -57,12 +88,19 @@ static uint8_t u8250_handle_in(u8250_state_t *uart)
5788
if (!uart->in_ready)
5889
return value;
5990

91+
#if defined(__EMSCRIPTEN__)
92+
value = (uint8_t) input_buf[input_buf_start];
93+
input_buf_start++;
94+
if (--input_buf_size == 0)
95+
input_buf_start = 0;
96+
#else
6097
if (read(uart->in_fd, &value, 1) < 0)
6198
rv_log_error("Failed to read UART input: %s", strerror(errno));
99+
#endif
62100
uart->in_ready = false;
63-
u8250_check_ready(uart);
64101

65-
if (value == 1) { /* start of heading (Ctrl-a) */
102+
if (value == 1) { /* start of heading (Ctrl-a) */
103+
u8250_check_ready(uart);
66104
if (getchar() == 120) { /* keyboard x */
67105
rv_log_info("RISC-V emulator is destroyed");
68106
exit(EXIT_SUCCESS);

src/emsc.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* rv32emu is freely redistributable under the MIT License. See the file
3+
* "LICENSE" for information on usage and redistribution of this file.
4+
*/
5+
6+
#pragma once
7+
8+
#include <emscripten.h>
9+
10+
/* To terminate the main loop of CPU */
11+
void indirect_rv_halt();
12+
13+
#if RV32_HAS(SYSTEM) && !RV32_HAS(ELF_LOADER)
14+
/* To bridge xterm.js terminal with UART */
15+
extern uint8_t input_buf_size;
16+
17+
char *get_input_buf();
18+
uint8_t get_input_buf_cap();
19+
void set_input_buf_size(uint8_t size);
20+
#endif
21+
22+
/* To enable/disable run button in index.html to prevent re-execution
23+
* when the process is already running.
24+
*/
25+
void enable_run_button();
26+
void disable_run_button();

src/emulate.c

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,6 @@
1010
#include <stdlib.h>
1111
#include <string.h>
1212

13-
#ifdef __EMSCRIPTEN__
14-
#include <emscripten.h>
15-
#endif
16-
1713
#if RV32_HAS(EXT_F)
1814
#include <math.h>
1915
#include "softfp.h"
@@ -23,6 +19,10 @@
2319
extern struct target_ops gdbstub_ops;
2420
#endif
2521

22+
#if defined(__EMSCRIPTEN__)
23+
#include "emsc.h"
24+
#endif
25+
2626
#include "decode.h"
2727
#include "mpool.h"
2828
#include "riscv.h"
@@ -1009,6 +1009,9 @@ static void rv_check_interrupt(riscv_t *rv)
10091009
if (peripheral_update_ctr-- == 0) {
10101010
peripheral_update_ctr = 64;
10111011

1012+
#if defined(__EMSCRIPTEN__)
1013+
escape_seq:
1014+
#endif
10121015
u8250_check_ready(PRIV(rv)->uart);
10131016
if (PRIV(rv)->uart->in_ready)
10141017
emu_update_uart_interrupts(rv);
@@ -1031,6 +1034,11 @@ static void rv_check_interrupt(riscv_t *rv)
10311034
break;
10321035
case (SUPERVISOR_EXTERNAL_INTR & 0xf):
10331036
SET_CAUSE_AND_TVAL_THEN_TRAP(rv, SUPERVISOR_EXTERNAL_INTR, 0);
1037+
#if defined(__EMSCRIPTEN__)
1038+
/* escape sequence has more than 1 byte */
1039+
if (input_buf_size)
1040+
goto escape_seq;
1041+
#endif
10341042
break;
10351043
default:
10361044
break;
@@ -1174,6 +1182,7 @@ void rv_step(void *arg)
11741182
emscripten_cancel_main_loop();
11751183
rv_delete(rv); /* clean up and reuse memory */
11761184
rv_log_info("RISC-V emulator is destroyed");
1185+
enable_run_button();
11771186
}
11781187
#endif
11791188
}

0 commit comments

Comments
 (0)