Skip to main content

warp_runtime/
standalone.rs

1//! A standalone executable is the warp-runtime stub carrying a program's machine code:
2//! `<executable><machine code, deflated><its length, u64 little-endian><TRAILER_MAGIC><zeros to a multiple of 16>`, then, in a
3//! Mach-O executable, its code signature: the machine code lies inside the __LINKEDIT segment, so the executable signs
4//! and verifies cleanly (`codesign --verify --strict`, macho.rs). At start the executable looks for the trailer where
5//! its program would end and runs what it carries.
6use crate::c_calls::CHandles;
7use crate::engine::{limited_store, metered_config, ticking};
8use crate::fuel::{time_limit_from_environment, DEFAULT_FUEL};
9use crate::host_words::{FOREIGN_CALL, HANDLER_PREFIX};
10use crate::macho;
11use crate::system_signals::{NEW_EMPTY, NEW_INT};
12use std::fs::File;
13use std::io::{Read, Seek, SeekFrom};
14use std::ops::Range;
15use std::path::Path;
16use std::time::Duration;
17use wasmtime::{Engine, Instance, Linker, Module, Result, Store, Val};
18
19const TRAILER_MAGIC: &[u8; 8] = b"WRPCwasm";
20const TRAILER_BYTES: usize = 16;
21/// The carried machine code is deflated: most of it is padding to page boundaries and tables (180 KB → 45 KB)
22const COMPRESSION_LEVEL: u8 = 9;
23/// The carried program ends on this boundary, where codesign puts the signature
24const ALIGNMENT: usize = 16;
25/// The exported functions a program starts at, in order of preference
26pub const ENTRY_POINTS: [&str; 2] = ["main", "_start"];
27/// The memory print and the host words read texts from
28const MEMORY_EXPORT: &str = "memory";
29/// An app carrying a site loads it in a hidden window and prints its text (tests), as `warp app --check`
30#[cfg(all(feature = "webview", target_os = "macos"))]
31const CHECK_FLAG: &str = "--check";
32
33/// Whether the stub looks up this export of a program: its entry point, memory, handlers and the constructors of their
34/// `event`. warp build drops every other export, and the functions only those reached (tree shaking, card g_gFs8)
35pub fn stub_calls_export(name: &str) -> bool {
36	ENTRY_POINTS.contains(&name)
37		|| [MEMORY_EXPORT, NEW_EMPTY, NEW_INT].contains(&name)
38		|| name.starts_with(HANDLER_PREFIX)
39}
40
41/// The exports the stub looks up of a program calling python or js: those of every program, and what the answers are
42/// built with (gc_values.rs)
43pub fn stub_calls_export_with_foreign_calls(name: &str) -> bool {
44	stub_calls_export(name) || crate::gc_values::BUILDERS.contains(&name) || name == crate::gc_values::TEXT_HEAP
45}
46
47/// The engine an executable's machine code is compiled for (warp build) and run with (the stub): no address map, the
48/// table from machine code to wasm offsets (an eighth of the machine code); a trap names the functions without them.
49/// An executable counts no fuel; it stops only at a time limit WARP_TIME_LIMIT sets.
50pub fn standalone_engine() -> Result<Engine> {
51	let mut config = metered_config(false);
52	config.generate_address_map(false);
53	Engine::new(&config)
54}
55
56/// `runtime` (an executable, with or without a program) carrying `machine_code` instead. A Mach-O executable comes
57/// back unsigned: sign it (`codesign --sign -`, which `warp build` runs), else macOS refuses to start it.
58pub fn with_machine_code(runtime: &[u8], machine_code: &[u8]) -> Vec<u8> {
59	let mut executable = without_machine_code(runtime);
60	let compressed = miniz_oxide::deflate::compress_to_vec(machine_code, COMPRESSION_LEVEL);
61	executable.extend_from_slice(&compressed);
62	executable.extend_from_slice(&(compressed.len() as u64).to_le_bytes());
63	executable.extend_from_slice(TRAILER_MAGIC);
64	executable.resize(executable.len().next_multiple_of(ALIGNMENT), 0);
65	macho::extend_last_segment_to_end(&mut executable);
66	executable
67}
68
69/// `runtime` carrying a page's site: an app showing it in the system's web view (card build-links); sign it as above
70pub fn with_site(runtime: &[u8], site: &[crate::site_files::SiteFile]) -> Vec<u8> {
71	with_machine_code(runtime, &crate::site_files::packed(site))
72}
73
74/// The executable without its code signature and without the program it carries
75pub fn without_machine_code(executable: &[u8]) -> Vec<u8> {
76	let mut unsigned = macho::unsigned(executable);
77	if let Some(carried) = carried_range(&unsigned) {
78		unsigned.truncate(carried.start);
79	}
80	unsigned
81}
82
83/// Where the program in `executable` lies: it ends with the trailer, before the code signature if there is one
84fn carried_range(executable: &[u8]) -> Option<Range<usize>> {
85	let end = macho::signature_offset(executable).unwrap_or(executable.len());
86	let (trailer_start, length) = trailer_before(executable.get(..end)?)?;
87	Some(trailer_start.checked_sub(length)?..trailer_start)
88}
89
90/// The trailer at the end of `bytes` (after its zero padding): where it starts, and the length of the program it closes
91fn trailer_before(bytes: &[u8]) -> Option<(usize, usize)> {
92	let padding = bytes.iter().rev().take(ALIGNMENT - 1).take_while(|&&byte| byte == 0).count();
93	let trailer_start = (bytes.len() - padding).checked_sub(TRAILER_BYTES)?;
94	let trailer: &[u8; TRAILER_BYTES] = bytes[trailer_start..trailer_start + TRAILER_BYTES].try_into().ok()?;
95	(&trailer[8..] == TRAILER_MAGIC)
96		.then(|| (trailer_start, u64::from_le_bytes(trailer[..8].try_into().unwrap()) as usize))
97}
98
99/// The machine code the executable at `path` carries, reading only its load commands and end when it carries none
100pub fn embedded_machine_code(path: &Path) -> Option<Vec<u8>> {
101	let mut file = File::open(path).ok()?;
102	let size = file.metadata().ok()?.len() as usize;
103	let header = read_at(&mut file, 0, size.min(macho::MAX_HEADER_BYTES))?;
104	let end = macho::signature_offset(&header).unwrap_or(size);
105	let tail_start = end.checked_sub(TRAILER_BYTES + ALIGNMENT)?;
106	let (trailer_start, length) = trailer_before(&read_at(&mut file, tail_start, end - tail_start)?)?;
107	let compressed = read_at(&mut file, (tail_start + trailer_start).checked_sub(length)?, length)?;
108	miniz_oxide::inflate::decompress_to_vec(&compressed).ok()
109}
110
111fn read_at(file: &mut File, offset: usize, length: usize) -> Option<Vec<u8>> {
112	file.seek(SeekFrom::Start(offset as u64)).ok()?;
113	let mut bytes = vec![0u8; length];
114	file.read_exact(&mut bytes).ok()?;
115	Some(bytes)
116}
117
118/// When this executable carries a program: run it and give the exit code; a page's site it shows (card build-links)
119pub fn run_carried_program() -> Option<i32> {
120	let executable = std::env::current_exe().ok()?;
121	let machine_code = embedded_machine_code(&executable)?;
122	if let Some(site) = crate::site_files::unpacked(&machine_code) {
123		return Some(show_site(&executable, site));
124	}
125	Some(match run(&machine_code) {
126		Ok(()) => 0,
127		Err(failure) => {
128			eprintln!("{failure:#}");
129			1
130		}
131	})
132}
133
134/// The carried site in a web view window titled as the executable; `--check` prints its text from a hidden one
135#[cfg(all(feature = "webview", target_os = "macos"))]
136fn show_site(executable: &Path, site: Vec<crate::site_files::SiteFile>) -> i32 {
137	let title = executable.file_stem().map(|stem| stem.to_string_lossy().into_owned()).unwrap_or_default();
138	let check = std::env::args().any(|argument| argument == CHECK_FLAG);
139	match crate::webview::show(&title, check, Box::new(move |path| crate::site_files::served(&site, path))) {
140		Ok(()) => 0,
141		Err(failure) => {
142			eprintln!("{failure}");
143			1
144		}
145	}
146}
147
148#[cfg(not(all(feature = "webview", target_os = "macos")))]
149fn show_site(_: &Path, _: Vec<crate::site_files::SiteFile>) -> i32 {
150	eprintln!("this app shows a page, and this warp-runtime has no web view (macOS only for now)");
151	1
152}
153
154/// Run machine code compiled with warp's settings (`warp build`): its host words, libm, libc, print and the C functions
155/// it carries are linked, its result is what it printed (build makes the program print its value)
156pub fn run(carried: &[u8]) -> Result<()> {
157	let (c_functions, machine_code) = crate::c_signatures::split_c_functions(carried).map_err(wasmtime::Error::msg)?;
158	// an executable is the user's app (a game, a server): it runs without a time limit unless WARP_TIME_LIMIT sets one
159	let time_limit = time_limit_from_environment();
160	let engine = running_engine(time_limit)?;
161	// SAFETY: the machine code is warp's own output, appended to this executable by `warp build`
162	let module = unsafe { Module::deserialize(&engine, machine_code)? };
163	if let Ran::Exited = run_module(&engine, &module, &c_functions, time_limit)? {
164		use std::io::Write;
165		let _ = std::io::stdout().flush();
166		std::process::exit(crate::system_signals::take_exit_code().unwrap_or(0)); // `exit(code)` (P121)
167	}
168	Ok(())
169}
170
171/// The standalone engine, ticking when a time limit ends the run
172pub fn running_engine(time_limit: Option<Duration>) -> Result<Engine> {
173	Ok(match time_limit {
174		Some(_) => ticking(standalone_engine()?),
175		None => standalone_engine()?,
176	})
177}
178
179/// How a program's run ended: main returned its results, or the program called `exit(code)`
180pub enum Ran {
181	Returned { store: Store<CHandles>, instance: Instance, results: Vec<Val> },
182	Exited,
183}
184
185/// Run `module` with the stub's host words, libm, libc, print and `c_functions` linked: from main through the handlers
186/// it listens with
187pub fn run_module(
188	engine: &Engine,
189	module: &Module,
190	c_functions: &[crate::c_signatures::CFunction],
191	time_limit: Option<Duration>,
192) -> Result<Ran> {
193	let mut linker = Linker::new(engine);
194	crate::host_words::link_host_words(&mut linker)?;
195	crate::output::link_output(&mut linker)?;
196	crate::libm::link_libm(&mut linker)?;
197	crate::c_calls::link_libc_functions(&mut linker)?;
198	for function in c_functions {
199		crate::c_calls::link_carried_function(&mut linker, engine, function)?;
200	}
201	linker.func_wrap(
202		crate::host_words::HOST_LIBRARY,
203		FOREIGN_CALL,
204		|mut caller: wasmtime::Caller<'_, CHandles>, runtime, module, member, call, arguments| {
205			crate::gc_values::foreign_call(&mut caller, [runtime, module, member, call, arguments])
206		},
207	)?;
208	let mut store = limited_store(engine, CHandles::default(), DEFAULT_FUEL, time_limit.unwrap_or(Duration::MAX));
209	let instance = linker.instantiate(&mut store, module)?;
210	let main = ENTRY_POINTS
211		.iter()
212		.find_map(|name| instance.get_func(&mut store, name))
213		.ok_or_else(|| wasmtime::Error::msg(format!("the program has no entry point {ENTRY_POINTS:?}")))?;
214	let mut results = vec![Val::AnyRef(None); main.ty(&store).results().len()];
215	crate::system_signals::allow_staying();
216	let outcome = main
217		.call(&mut store, &[], &mut results)
218		.and_then(|_| crate::system_signals::stay_while_listening(&mut store, &instance));
219	let outcome = crate::system_signals::with_exit_handler(outcome, &mut store, |store, name| {
220		instance.get_func(&mut *store, name)
221	})?;
222	Ok(match outcome {
223		Some(()) => Ran::Returned { store, instance, results },
224		None => Ran::Exited,
225	})
226}
227
228/// The imports a standalone executable provides: (module, name); C functions beyond them it carries (c_signatures.rs)
229pub fn provided_imports() -> impl Iterator<Item = (&'static str, &'static str)> {
230	let host_words =
231		crate::host_words::BASIC_HOST_WORDS.into_iter().map(|word| (crate::host_words::HOST_LIBRARY, word));
232	let libm = crate::libm::libm_names().map(|name| (crate::libm::LIBM, name));
233	host_words.chain(libm).chain(crate::c_calls::libc_names()).chain([
234		(crate::output::WASI_LIBRARY, crate::output::FD_WRITE),
235		(crate::host_words::HOST_LIBRARY, FOREIGN_CALL),
236	])
237}