Skip to content

Commit 6741e41

Browse files
committed
Auto merge of #47102 - Diggsey:wasm-syscall, r=alexcrichton
Implement extensible syscall interface for wasm Currently it's possible to run tests with the native wasm target, but it's not possible to tell whether they pass or to capture the output, because libstd throws away stdout, stderr and the exit code. While advanced libstd features should probably require more specific targets (eg. wasm-unknown-web) I think even the unknown target should at least support basic I/O. Any solution is constrained by these factors: - It must not be javascript specific - There must not be too strong coupling between libstd and the host environment (because it's an "unknown" target) - WebAssembly does not allow "optional" imports - all imports *must* be resolved. - WebAssembly does not support calling the host environment through any channel *other* than imports. The best solution I could find to these constraints was to give libstd a single required import, and implement a syscall-style interface through that import. Each syscall is designed such that a no-op implementation gives the most reasonable fallback behaviour. This means that the following import table would be perfectly valid: ```javascript imports.env = { rust_wasm_syscall: function(index, data) {} } ``` Currently I have implemented these system calls: - Read from stdin - Write to stdout/stderr - Set the exit code - Get command line arguments - Get environment variable - Set environment variable - Get time It need not be extended beyond this set if being able to run tests for this target is the only goal. edit: As part of this PR I had to make a further change. Previously, the rust entry point would be automatically called when the webassembly module was instantiated. This was problematic because from the javascript side it was impossible to call exported functions, access program memory or get a reference to the instance. To solve this, ~I changed the default behaviour to not automatically call the entry point, and added a crate-level attribute to regain the old behaviour. (`#![wasm_auto_run]`)~ I disabled this behaviour when building tests.
2 parents 56733bc + 0e6601f commit 6741e41

File tree

12 files changed

+365
-198
lines changed

12 files changed

+365
-198
lines changed

config.toml.example

+5
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@
312312
# bootstrap)
313313
#codegen-backends = ["llvm"]
314314

315+
# Flag indicating whether `libstd` calls an imported function to hande basic IO
316+
# when targetting WebAssembly. Enable this to debug tests for the `wasm32-unknown-unknown`
317+
# target, as without this option the test output will not be captured.
318+
#wasm-syscall = false
319+
315320
# =============================================================================
316321
# Options for specific targets
317322
#

src/bootstrap/config.rs

+3
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ pub struct Config {
107107
pub debug_jemalloc: bool,
108108
pub use_jemalloc: bool,
109109
pub backtrace: bool, // support for RUST_BACKTRACE
110+
pub wasm_syscall: bool,
110111

111112
// misc
112113
pub low_priority: bool,
@@ -282,6 +283,7 @@ struct Rust {
282283
test_miri: Option<bool>,
283284
save_toolstates: Option<String>,
284285
codegen_backends: Option<Vec<String>>,
286+
wasm_syscall: Option<bool>,
285287
}
286288

287289
/// TOML representation of how each build target is configured.
@@ -463,6 +465,7 @@ impl Config {
463465
set(&mut config.rust_dist_src, rust.dist_src);
464466
set(&mut config.quiet_tests, rust.quiet_tests);
465467
set(&mut config.test_miri, rust.test_miri);
468+
set(&mut config.wasm_syscall, rust.wasm_syscall);
466469
config.rustc_parallel_queries = rust.experimental_parallel_queries.unwrap_or(false);
467470
config.rustc_default_linker = rust.default_linker.clone();
468471
config.musl_root = rust.musl_root.clone().map(PathBuf::from);

src/bootstrap/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,9 @@ impl Build {
423423
if self.config.profiler {
424424
features.push_str(" profiler");
425425
}
426+
if self.config.wasm_syscall {
427+
features.push_str(" wasm_syscall");
428+
}
426429
features
427430
}
428431

src/bootstrap/test.rs

+8
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,14 @@ impl Step for Crate {
12861286
cargo.env(format!("CARGO_TARGET_{}_RUNNER", envify(&target)),
12871287
build.config.nodejs.as_ref().expect("nodejs not configured"));
12881288
} else if target.starts_with("wasm32") {
1289+
// Warn about running tests without the `wasm_syscall` feature enabled.
1290+
// The javascript shim implements the syscall interface so that test
1291+
// output can be correctly reported.
1292+
if !build.config.wasm_syscall {
1293+
println!("Libstd was built without `wasm_syscall` feature enabled: \
1294+
test output may not be visible.");
1295+
}
1296+
12891297
// On the wasm32-unknown-unknown target we're using LTO which is
12901298
// incompatible with `-C prefer-dynamic`, so disable that here
12911299
cargo.env("RUSTC_NO_PREFER_DYNAMIC", "1");

src/etc/wasm32-shim.js

+84-65
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,76 @@ let m = new WebAssembly.Module(buffer);
2828

2929
let memory = null;
3030

31+
function viewstruct(data, fields) {
32+
return new Uint32Array(memory.buffer).subarray(data/4, data/4 + fields);
33+
}
34+
3135
function copystr(a, b) {
32-
if (memory === null) {
33-
return null
34-
}
35-
let view = new Uint8Array(memory.buffer).slice(a, a + b);
36+
let view = new Uint8Array(memory.buffer).subarray(a, a + b);
3637
return String.fromCharCode.apply(null, view);
3738
}
3839

40+
function syscall_write([fd, ptr, len]) {
41+
let s = copystr(ptr, len);
42+
switch (fd) {
43+
case 1: process.stdout.write(s); break;
44+
case 2: process.stderr.write(s); break;
45+
}
46+
}
47+
48+
function syscall_exit([code]) {
49+
process.exit(code);
50+
}
51+
52+
function syscall_args(params) {
53+
let [ptr, len] = params;
54+
55+
// Calculate total required buffer size
56+
let totalLen = -1;
57+
for (let i = 2; i < process.argv.length; ++i) {
58+
totalLen += Buffer.byteLength(process.argv[i]) + 1;
59+
}
60+
if (totalLen < 0) { totalLen = 0; }
61+
params[2] = totalLen;
62+
63+
// If buffer is large enough, copy data
64+
if (len >= totalLen) {
65+
let view = new Uint8Array(memory.buffer);
66+
for (let i = 2; i < process.argv.length; ++i) {
67+
let value = process.argv[i];
68+
Buffer.from(value).copy(view, ptr);
69+
ptr += Buffer.byteLength(process.argv[i]) + 1;
70+
}
71+
}
72+
}
73+
74+
function syscall_getenv(params) {
75+
let [keyPtr, keyLen, valuePtr, valueLen] = params;
76+
77+
let key = copystr(keyPtr, keyLen);
78+
let value = process.env[key];
79+
80+
if (value == null) {
81+
params[4] = 0xFFFFFFFF;
82+
} else {
83+
let view = new Uint8Array(memory.buffer);
84+
let totalLen = Buffer.byteLength(value);
85+
params[4] = totalLen;
86+
if (valueLen >= totalLen) {
87+
Buffer.from(value).copy(view, valuePtr);
88+
}
89+
}
90+
}
91+
92+
function syscall_time(params) {
93+
let t = Date.now();
94+
let secs = Math.floor(t / 1000);
95+
let millis = t % 1000;
96+
params[1] = Math.floor(secs / 0x100000000);
97+
params[2] = secs % 0x100000000;
98+
params[3] = Math.floor(millis * 1000000);
99+
}
100+
39101
let imports = {};
40102
imports.env = {
41103
// These are generated by LLVM itself for various intrinsic calls. Hopefully
@@ -48,68 +110,25 @@ imports.env = {
48110
log10: Math.log10,
49111
log10f: Math.log10,
50112

51-
// These are called in src/libstd/sys/wasm/stdio.rs and are used when
52-
// debugging is enabled.
53-
rust_wasm_write_stdout: function(a, b) {
54-
let s = copystr(a, b);
55-
if (s !== null) {
56-
process.stdout.write(s);
57-
}
58-
},
59-
rust_wasm_write_stderr: function(a, b) {
60-
let s = copystr(a, b);
61-
if (s !== null) {
62-
process.stderr.write(s);
63-
}
64-
},
65-
66-
// These are called in src/libstd/sys/wasm/args.rs and are used when
67-
// debugging is enabled.
68-
rust_wasm_args_count: function() {
69-
if (memory === null)
70-
return 0;
71-
return process.argv.length - 2;
72-
},
73-
rust_wasm_args_arg_size: function(i) {
74-
return Buffer.byteLength(process.argv[i + 2]);
75-
},
76-
rust_wasm_args_arg_fill: function(idx, ptr) {
77-
let arg = process.argv[idx + 2];
78-
let view = new Uint8Array(memory.buffer);
79-
Buffer.from(arg).copy(view, ptr);
80-
},
81-
82-
// These are called in src/libstd/sys/wasm/os.rs and are used when
83-
// debugging is enabled.
84-
rust_wasm_getenv_len: function(a, b) {
85-
let key = copystr(a, b);
86-
if (key === null) {
87-
return -1;
113+
rust_wasm_syscall: function(index, data) {
114+
switch (index) {
115+
case 1: syscall_write(viewstruct(data, 3)); return true;
116+
case 2: syscall_exit(viewstruct(data, 1)); return true;
117+
case 3: syscall_args(viewstruct(data, 3)); return true;
118+
case 4: syscall_getenv(viewstruct(data, 5)); return true;
119+
case 6: syscall_time(viewstruct(data, 4)); return true;
120+
default:
121+
console.log("Unsupported syscall: " + index);
122+
return false;
88123
}
89-
if (!(key in process.env)) {
90-
return -1;
91-
}
92-
return Buffer.byteLength(process.env[key]);
93-
},
94-
rust_wasm_getenv_data: function(a, b, ptr) {
95-
let key = copystr(a, b);
96-
let value = process.env[key];
97-
let view = new Uint8Array(memory.buffer);
98-
Buffer.from(value).copy(view, ptr);
99-
},
100-
};
101-
102-
let module_imports = WebAssembly.Module.imports(m);
103-
104-
for (var i = 0; i < module_imports.length; i++) {
105-
let imp = module_imports[i];
106-
if (imp.module != 'env') {
107-
continue
108124
}
109-
if (imp.name == 'memory' && imp.kind == 'memory') {
110-
memory = new WebAssembly.Memory({initial: 20});
111-
imports.env.memory = memory;
112-
}
113-
}
125+
};
114126

115127
let instance = new WebAssembly.Instance(m, imports);
128+
memory = instance.exports.memory;
129+
try {
130+
instance.exports.main();
131+
} catch (e) {
132+
console.error(e);
133+
process.exit(101);
134+
}

src/librustc_trans/back/write.rs

+2-4
Original file line numberDiff line numberDiff line change
@@ -824,9 +824,7 @@ fn binaryen_assemble(cgcx: &CodegenContext,
824824
if cgcx.debuginfo != config::NoDebugInfo {
825825
options.debuginfo(true);
826826
}
827-
if cgcx.crate_types.contains(&config::CrateTypeExecutable) {
828-
options.start("main");
829-
}
827+
830828
options.stack(1024 * 1024);
831829
options.import_memory(cgcx.wasm_import_memory);
832830
let assembled = input.and_then(|input| {
@@ -1452,7 +1450,7 @@ fn start_executing_work(tcx: TyCtxt,
14521450
target_pointer_width: tcx.sess.target.target.target_pointer_width.clone(),
14531451
binaryen_linker: tcx.sess.linker_flavor() == LinkerFlavor::Binaryen,
14541452
debuginfo: tcx.sess.opts.debuginfo,
1455-
wasm_import_memory: wasm_import_memory,
1453+
wasm_import_memory,
14561454
assembler_cmd,
14571455
};
14581456

src/libstd/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ jemalloc = ["alloc_jemalloc"]
4848
force_alloc_system = []
4949
panic-unwind = ["panic_unwind"]
5050
profiler = ["profiler_builtins"]
51+
wasm_syscall = []

src/libstd/sys/wasm/args.rs

+5-33
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
use ffi::OsString;
1212
use marker::PhantomData;
13-
use mem;
1413
use vec;
14+
use sys::ArgsSysCall;
1515

1616
pub unsafe fn init(_argc: isize, _argv: *const *const u8) {
1717
// On wasm these should always be null, so there's nothing for us to do here
@@ -21,38 +21,10 @@ pub unsafe fn cleanup() {
2121
}
2222

2323
pub fn args() -> Args {
24-
// When the runtime debugging is enabled we'll link to some extra runtime
25-
// functions to actually implement this. These are for now just implemented
26-
// in a node.js script but they're off by default as they're sort of weird
27-
// in a web-wasm world.
28-
if !super::DEBUG {
29-
return Args {
30-
iter: Vec::new().into_iter(),
31-
_dont_send_or_sync_me: PhantomData,
32-
}
33-
}
34-
35-
// You'll find the definitions of these in `src/etc/wasm32-shim.js`. These
36-
// are just meant for debugging and should not be relied on.
37-
extern {
38-
fn rust_wasm_args_count() -> usize;
39-
fn rust_wasm_args_arg_size(a: usize) -> usize;
40-
fn rust_wasm_args_arg_fill(a: usize, ptr: *mut u8);
41-
}
42-
43-
unsafe {
44-
let cnt = rust_wasm_args_count();
45-
let mut v = Vec::with_capacity(cnt);
46-
for i in 0..cnt {
47-
let n = rust_wasm_args_arg_size(i);
48-
let mut data = vec![0; n];
49-
rust_wasm_args_arg_fill(i, data.as_mut_ptr());
50-
v.push(mem::transmute::<Vec<u8>, OsString>(data));
51-
}
52-
Args {
53-
iter: v.into_iter(),
54-
_dont_send_or_sync_me: PhantomData,
55-
}
24+
let v = ArgsSysCall::perform();
25+
Args {
26+
iter: v.into_iter(),
27+
_dont_send_or_sync_me: PhantomData,
5628
}
5729
}
5830

0 commit comments

Comments
 (0)