#[cfg(target_arch = "wasm32")]
mod browser {
use std::sync::{Arc, Mutex};
use wasm_bindgen::prelude::*;
use crate::abi::Arch;
use crate::fs::{DevFs, MountTable, ProcFs, SysFs, TmpFs};
use crate::kernel::Kernel;
use crate::loader::{ProcessSpec, load_static};
use crate::vcpu::Backend;
use crate::vcpu::GuestMemory;
use crate::vcpu::mem::PAGE_SIZE;
const GUEST_BASE: u64 = 0x1_0000;
const MEM_BYTES: u64 = 256 * 1024 * 1024;
const MAX_ROOTFS_BYTES: u64 = 512 * 1024 * 1024;
struct CaptureSink(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for CaptureSink {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
struct RunOutcome {
arch: Option<&'static str>,
exit_code: Option<i32>,
stdout: String,
stderr: String,
error: Option<String>,
unsupported: Vec<(u64, u64)>,
}
impl RunOutcome {
fn failed(msg: String) -> Self {
Self {
arch: None,
exit_code: None,
stdout: String::new(),
stderr: String::new(),
error: Some(msg),
unsupported: Vec::new(),
}
}
fn to_json(&self) -> String {
let exit_code = self
.exit_code
.map_or_else(|| "null".to_string(), |c| c.to_string());
let arch = self
.arch
.map_or_else(|| "null".to_string(), |a| format!("\"{a}\""));
let error = self
.error
.as_deref()
.map_or_else(|| "null".to_string(), |e| format!("\"{}\"", json_escape(e)));
let mut unsupported = String::from("[");
for (i, (nr, count)) in self.unsupported.iter().enumerate() {
if i > 0 {
unsupported.push(',');
}
unsupported.push_str(&format!("{{\"nr\":{nr},\"count\":{count}}}"));
}
unsupported.push(']');
format!(
"{{\"ok\":{},\"arch\":{arch},\"exit_code\":{exit_code},\"stdout\":\"{}\",\"stderr\":\"{}\",\"error\":{error},\"unsupported_syscalls\":{unsupported}}}",
self.error.is_none(),
json_escape(&self.stdout),
json_escape(&self.stderr),
)
}
}
#[wasm_bindgen]
#[must_use]
pub fn run_elf(bytes: &[u8]) -> String {
console_error_panic_hook::set_once();
run(bytes, Vec::new()).to_json()
}
#[wasm_bindgen]
#[must_use]
pub fn run_elf_with_argv(bytes: &[u8], argv: Vec<String>) -> String {
console_error_panic_hook::set_once();
run(bytes, argv).to_json()
}
#[wasm_bindgen]
pub struct Terminal {
vm: crate::vm::Vm,
}
#[wasm_bindgen]
impl Terminal {
#[wasm_bindgen(constructor)]
pub fn new(rootfs_targz: &[u8], argv: Vec<String>) -> Result<Terminal, JsError> {
console_error_panic_hook::set_once();
let argv = if argv.is_empty() {
vec!["/bin/busybox".to_string(), "sh".to_string()]
} else {
argv
};
let tar = crate::fs::tar::gunzip(rootfs_targz, MAX_ROOTFS_BYTES)
.map_err(|e| JsError::new(&e))?;
let vm = crate::vm::Vm::boot_squashfs(&tar, argv, MEM_BYTES)
.map_err(|e| JsError::new(&e))?;
Ok(Self { vm })
}
pub fn write_stdin(&mut self, bytes: &[u8]) {
self.vm.write_stdin(bytes);
}
pub fn close_stdin(&mut self) {
self.vm.close_stdin();
}
#[must_use]
pub fn pump(&mut self) -> Vec<u8> {
match self.vm.pump() {
Ok(step) => {
let mut out = step.stdout;
out.extend_from_slice(&step.stderr);
out
}
Err(_) => Vec::new(),
}
}
#[must_use]
pub fn is_running(&self) -> bool {
self.vm.exit_code().is_none()
}
#[must_use]
pub fn exit_code(&self) -> i32 {
self.vm.exit_code().unwrap_or(-1)
}
}
fn detect_arch(elf: &[u8]) -> Option<Arch> {
const EM_X86_64: u16 = 62;
const EM_AARCH64: u16 = 183;
let machine = u16::from_le_bytes([*elf.get(18)?, *elf.get(19)?]);
match machine {
EM_AARCH64 => Some(Arch::Aarch64),
EM_X86_64 => Some(Arch::X86_64),
_ => None,
}
}
fn select_backend(arch: Arch) -> Result<Box<dyn Backend>, String> {
match arch {
Arch::Aarch64 => crate::vcpu::interp::InterpBackend::new(arch)
.map(|b| Box::new(b) as Box<dyn Backend>)
.map_err(|e| e.to_string()),
Arch::X86_64 => crate::vcpu::interp_x86::X86Backend::new(arch)
.map(|b| Box::new(b) as Box<dyn Backend>)
.map_err(|e| e.to_string()),
}
}
fn run(elf: &[u8], argv: Vec<String>) -> RunOutcome {
let Some(arch) = detect_arch(elf) else {
return RunOutcome::failed(
"not a recognized ELF64 image (expected an EM_AARCH64 or EM_X86_64 e_machine)"
.to_string(),
);
};
let arch_str = arch.as_str();
let mut mem = GuestMemory::new(GUEST_BASE, MEM_BYTES);
let argv = if argv.is_empty() {
vec!["prog".to_string()]
} else {
argv
};
let spec = ProcessSpec {
argv,
envp: vec![
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(),
"HOME=/root".to_string(),
"TERM=xterm".to_string(),
"PWD=/".to_string(),
],
};
let img = match load_static(&mut mem, elf, &spec) {
Ok(i) => i,
Err(e) => {
let mut o = RunOutcome::failed(e.to_string());
o.arch = Some(arch_str);
return o;
}
};
let mid = page_down(img.program_break + (img.stack_bottom - img.program_break) / 2);
let backend = match select_backend(arch) {
Ok(b) => b,
Err(e) => {
let mut o = RunOutcome::failed(e);
o.arch = Some(arch_str);
return o;
}
};
let vcpu = match backend.new_vcpu(img.entry, img.stack_pointer) {
Ok(v) => v,
Err(e) => {
let mut o = RunOutcome::failed(e.to_string());
o.arch = Some(arch_str);
return o;
}
};
let mut mounts = MountTable::new();
mounts.mount("/", Box::new(TmpFs::new()));
mounts.mount("/tmp", Box::new(TmpFs::new()));
mounts.mount("/dev", Box::new(DevFs::new()));
mounts.mount("/proc", Box::new(ProcFs::new()));
mounts.mount("/sys", Box::new(SysFs::new()));
let stdout_buf = Arc::new(Mutex::new(Vec::new()));
let stderr_buf = Arc::new(Mutex::new(Vec::new()));
let mut kernel = Kernel::new(arch, mounts);
kernel.set_stdout(Box::new(CaptureSink(stdout_buf.clone())));
kernel.set_stderr(Box::new(CaptureSink(stderr_buf.clone())));
kernel.set_cwd("/");
kernel.set_heap(img.program_break, mid);
kernel.set_mmap_area(img.stack_bottom, mid);
let result = kernel.run(vcpu, mem);
let stdout = String::from_utf8_lossy(&stdout_buf.lock().unwrap()).into_owned();
let stderr = String::from_utf8_lossy(&stderr_buf.lock().unwrap()).into_owned();
let unsupported = kernel
.unsupported()
.iter()
.map(|(&nr, &c)| (nr, c))
.collect();
let (exit_code, error) = match result {
Ok(code) => (Some(code), None),
Err(e) => (None, Some(e.to_string())),
};
RunOutcome {
arch: Some(arch_str),
exit_code,
stdout,
stderr,
error,
unsupported,
}
}
fn page_down(v: u64) -> u64 {
v - v % PAGE_SIZE
}
}
#[cfg(target_arch = "wasm32")]
pub use browser::{Terminal, run_elf, run_elf_with_argv};