Skip to content

Commit a8873d8

Browse files
authored
Using mmap to Read Files and Write to Files Directly (#4)
1 parent 87828ee commit a8873d8

6 files changed

Lines changed: 124 additions & 50 deletions

File tree

TODO.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
# High Priority
1111

12+
- E2E testing for the linker. Should also ensure deterministic output.
1213
- Add a `disable_hygiene` attribute to `#[js_sys]` to reduce the compile-time of the output to an
1314
absolute minimum. This can avoid all `interpolate`s.
1415
- Escape namespaces and function names if they are not valid JS identifiers.
@@ -33,6 +34,7 @@
3334
- Store them next to the output file.
3435
- Pass an environment variable from a `build.rs` pointing to the target folder and go from there.
3536
This seems to have failed. No build script instruction can reach the linker on Wasm.
37+
- Memory-mapped file reading should lock files to make it safe.
3638

3739
# Medium Priority
3840

@@ -41,8 +43,6 @@
4143
- Provide an absolutely minimal allocator.
4244
- The `js_sys` proc-macro should remove the `extern "C" { ... }` part of the input on error to avoid
4345
triggering the `unsafe` requirement downstream.
44-
- Optimize linker file interactions by using memory mapped files instead of reading and writing
45-
everything into memory.
4646

4747
[Emscripten's]:
4848
https://github.com/emscripten-core/emscripten/blob/28bcb86466a273859b8adb43cb167b97e05e145d/src/lib/libstrings.js
@@ -85,6 +85,7 @@ This is a list of upstream issues that could make our lives significantly easier
8585
- `TextDe/Encoder` could support `SharedArrayBuffer`s:
8686
- [Chrome Bug](https://issues.chromium.org/issues/40102463)
8787
- [Firefox Bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1561594)
88+
- `wasm-encoder` `io::Write` support: [bytecodealliance/wasm-tools#778]
8889

8990
[llvm/llvm-project#136594]: https://github.com/llvm/llvm-project/issues/136594
9091
[rust-lang/rust#136382]: https://github.com/rust-lang/rust/issues/136382
@@ -100,3 +101,4 @@ This is a list of upstream issues that could make our lives significantly easier
100101
[rust-lang/rfcs#3834]: https://github.com/rust-lang/rfcs/pull/3834
101102
[rust-lang/rust#136096]: https://github.com/rust-lang/rust/issues/136096
102103
[rust-lang/rust#133508]: https://github.com/rust-lang/rust/issues/133508
104+
[bytecodealliance/wasm-tools#778]: https://github.com/bytecodealliance/wasm-tools/issues/778

host/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ indoc = "2"
1414
itertools = { version = "0.14", default-features = false }
1515
js-bindgen-ld-shared = { path = "ld-shared" }
1616
js-bindgen-macro-shared = { path = "macro-shared" }
17+
memmap2 = "0.9"
1718
object = { version = "0.38", default-features = false, features = ["archive", "read_core", "std"] }
1819
prettyplease = "0.2"
1920
proc-macro2 = { version = "1", default-features = false }
@@ -27,6 +28,8 @@ wasm-encoder = { version = "0.244", default-features = false, features = ["wasmp
2728
wasmparser = { version = "0.244", default-features = false }
2829

2930
[patch.crates-io]
31+
# Detect unsupported platforms at run-time: https://github.com/RazrFalcon/memmap2-rs/issues/160.
32+
memmap2 = { git = "https://github.com/daxpedda/memmap2-rs", branch = "stable-unsupported" }
3033
# Proc-macro bug: https://github.com/oli-obk/ui_test/issues/361.
3134
ui_test = { git = "https://github.com/daxpedda/ui_test", branch = "js-bindgen" }
3235

host/js-sys-macro/src/tests/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
mod function;
22

33
use std::ffi::OsStr;
4-
use std::io::Cursor;
4+
use std::io::{self, Cursor};
55
use std::path::Path;
66
use std::process::Command;
77
use std::{env, fs};
@@ -182,6 +182,7 @@ fn inner(tmp: &Path, source: &str) -> Result<(String, String)> {
182182
js_bindgen_ld_shared::assembly_to_object(
183183
OsStr::new("wasm32"),
184184
assembly,
185+
&mut io::sink(),
185186
)?;
186187
}
187188
Payload::CustomSection(c)

host/ld-shared/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ name = "js-bindgen-ld-shared"
44
rust-version = "1.91"
55

66
[dependencies]
7+
memmap2 = { workspace = true }
78
object = { workspace = true }
89
wasmparser = { workspace = true }
910

host/ld-shared/src/lib.rs

Lines changed: 71 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
use std::ffi::OsStr;
22
use std::fmt::{self, Debug, Formatter};
3-
use std::fs;
4-
use std::io::{Error, Write};
3+
use std::fs::File;
4+
use std::io::{self, Error, ErrorKind, Read, Write};
5+
use std::ops::Deref;
56
use std::path::Path;
67
use std::process::{Command, Stdio};
78

9+
use memmap2::Mmap;
810
use object::read::archive::ArchiveFile;
911
use wasmparser::CustomSectionReader;
1012

1113
/// Currently this simply passes the LLVM s-format assembly to `llvm-mc` to
1214
/// convert to an object file the linker can consume.
13-
pub fn assembly_to_object(arch_str: &OsStr, assembly: &[u8]) -> Result<Vec<u8>, Error> {
15+
pub fn assembly_to_object(
16+
arch_str: &OsStr,
17+
assembly: &[u8],
18+
output: &mut dyn Write,
19+
) -> Result<(), Error> {
1420
let mut child = Command::new("llvm-mc")
1521
.arg(format!("-arch={}", arch_str.display()))
1622
// In the future we will switch to something supporting auto-detection.
@@ -27,41 +33,54 @@ pub fn assembly_to_object(arch_str: &OsStr, assembly: &[u8]) -> Result<Vec<u8>,
2733
.ok_or_else(|| Error::other("`llvm-mc` process should have `stdin`"))?;
2834
stdin.write_all(assembly)?;
2935

30-
let output = child.wait_with_output()?;
36+
let status = child.wait()?;
3137

32-
if output.status.success() {
33-
Ok(output.stdout)
38+
let mut child_stdout = child
39+
.stdout
40+
.ok_or_else(|| Error::other("`llvm-mc` process should have `stdout`"))?;
41+
42+
if status.success() {
43+
io::copy(&mut child_stdout, output)?;
44+
Ok(())
3445
} else {
3546
eprintln!(
3647
"------ llvm-mc input -------\n{}",
3748
String::from_utf8_lossy(assembly)
3849
);
3950

40-
if !output.stdout.is_empty() {
51+
let mut stdout = Vec::new();
52+
child_stdout.read_to_end(&mut stdout)?;
53+
54+
if !stdout.is_empty() {
4155
eprintln!(
4256
"------ llvm-mc stdout ------\n{}",
43-
String::from_utf8_lossy(&output.stdout)
57+
String::from_utf8_lossy(&stdout)
4458
);
4559

46-
if !output.stdout.ends_with(b"\n") {
60+
if !stdout.ends_with(b"\n") {
4761
eprintln!();
4862
}
4963
}
5064

51-
if !output.stderr.is_empty() {
65+
let mut stderr = Vec::new();
66+
child
67+
.stderr
68+
.ok_or_else(|| Error::other("`llvm-mc` process should have `stderr`"))?
69+
.read_to_end(&mut stderr)?;
70+
71+
if !stderr.is_empty() {
5272
eprintln!(
5373
"------ llvm-mc stderr ------\n{}",
54-
String::from_utf8_lossy(&output.stderr)
74+
String::from_utf8_lossy(&stderr)
5575
);
5676

57-
if !output.stderr.ends_with(b"\n") {
77+
if !stderr.ends_with(b"\n") {
5878
eprintln!();
5979
}
6080
}
6181

6282
Err(Error::other(format!(
63-
"`llvm-mc` process failed with status: {}",
64-
output.status
83+
"`llvm-mc` process failed with status: {status}"
6584
)))
6685
}
6786
}
@@ -73,7 +92,7 @@ pub fn ld_input_parser<E>(
7392
// We found a UNIX archive.
7493
if input.as_encoded_bytes().ends_with(b".rlib") {
7594
let archive_path = Path::new(&input);
76-
let archive_data = match fs::read(archive_path) {
95+
let archive_data = match ReadFile::new(archive_path) {
7796
Ok(archive_data) => archive_data,
7897
Err(error) => {
7998
eprintln!(
@@ -130,7 +149,7 @@ pub fn ld_input_parser<E>(
130149
}
131150
} else if input.as_encoded_bytes().ends_with(b".o") {
132151
let object_path = Path::new(&input);
133-
let object = match fs::read(object_path) {
152+
let object = match ReadFile::new(object_path) {
134153
Ok(object) => object,
135154
Err(error) => {
136155
eprintln!(
@@ -147,6 +166,42 @@ pub fn ld_input_parser<E>(
147166
Ok(())
148167
}
149168

169+
pub struct ReadFile(ReadInner);
170+
171+
enum ReadInner {
172+
Mmap(Mmap),
173+
File(Vec<u8>),
174+
}
175+
176+
impl ReadFile {
177+
pub fn new(path: &Path) -> Result<Self, Error> {
178+
let mut file = File::open(path)?;
179+
// SAFETY: the file is not mutated while the mapping is in use.
180+
let result = unsafe { Mmap::map(&file) };
181+
182+
match result {
183+
Ok(mmap) => Ok(Self(ReadInner::Mmap(mmap))),
184+
Err(error) if matches!(error.kind(), ErrorKind::Unsupported) => {
185+
let mut output = Vec::new();
186+
file.read_to_end(&mut output)?;
187+
Ok(Self(ReadInner::File(output)))
188+
}
189+
Err(error) => Err(error),
190+
}
191+
}
192+
}
193+
194+
impl Deref for ReadFile {
195+
type Target = [u8];
196+
197+
fn deref(&self) -> &Self::Target {
198+
match &self.0 {
199+
ReadInner::Mmap(mmap) => mmap.deref(),
200+
ReadInner::File(data) => data.as_slice(),
201+
}
202+
}
203+
}
204+
150205
#[derive(Clone)]
151206
pub struct CustomSectionParser<'cs> {
152207
name: &'cs str,

host/ld/src/main.rs

Lines changed: 43 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ mod wasm_ld;
33
use std::borrow::Cow;
44
use std::convert::Infallible;
55
use std::ffi::{OsStr, OsString};
6-
use std::fmt::Write;
6+
use std::fs::File;
7+
use std::io::{BufWriter, Write};
78
use std::path::Path;
89
use std::process::{self, Command};
910
use std::{env, fs};
1011

1112
use hashbrown::{HashMap, HashSet};
1213
use itertools::{Itertools, Position};
13-
use js_bindgen_ld_shared::CustomSectionParser;
14+
use js_bindgen_ld_shared::{CustomSectionParser, ReadFile};
1415
use wasm_encoder::{EntityType, ImportSection, Module, RawSection, Section};
1516
use wasmparser::{Encoding, Parser, Payload, TypeRef};
1617

@@ -148,11 +149,14 @@ fn process_object(
148149
// ensures freshness:
149150
// https://doc.rust-lang.org/1.92.0/nightly-rustc/cargo/core/compiler/fingerprint/index.html#fingerprints-and-unithashs
150151
if !asm_path.exists() {
151-
let asm_object = js_bindgen_ld_shared::assembly_to_object(arch_str, assembly)
152+
let mut asm_file = BufWriter::new(
153+
File::create(&asm_path).expect("output assembly object should be writable"),
154+
);
155+
156+
js_bindgen_ld_shared::assembly_to_object(arch_str, assembly, &mut asm_file)
152157
.expect("compiling assembly should be valid");
153158

154-
fs::write(&asm_path, asm_object)
155-
.expect("writing assembly object file should succeed");
159+
asm_file.into_inner().unwrap().sync_all().unwrap();
156160
}
157161

158162
add_args.push(asm_path.into());
@@ -166,7 +170,7 @@ fn post_processing(output_path: &Path, main_memory: MainMemory<'_>) {
166170
// Unfortunately we don't receive the final output path adjustments Cargo makes.
167171
// So for the JS file we just figure it out ourselves.
168172
let package = env::var_os("CARGO_CRATE_NAME").expect("`CARGO_CRATE_NAME` should be present");
169-
let wasm_input = fs::read(output_path).expect("output file should be readable");
173+
let wasm_input = ReadFile::new(output_path).expect("output file should be readable");
170174
let mut wasm_output = Vec::new();
171175

172176
let mut found_import: HashMap<&str, HashMap<&str, &str>> = HashMap::new();
@@ -396,12 +400,15 @@ fn post_processing(output_path: &Path, main_memory: MainMemory<'_>) {
396400
"missing JS embed: {expected_embed:?}"
397401
);
398402

399-
fs::write(output_path, wasm_output).expect("output Wasm file should be writable");
400-
401-
let mut js_output = String::new();
403+
let mut js_output = BufWriter::new(
404+
File::create(output_path.with_file_name(package).with_extension("js"))
405+
.expect("output JS file should be writable"),
406+
);
402407

403408
// Create our `WebAssembly.Memory`.
404-
js_output.push_str("const memory = new WebAssembly.Memory({ ");
409+
js_output
410+
.write_all(b"const memory = new WebAssembly.Memory({ ")
411+
.unwrap();
405412

406413
if memory.memory64 {
407414
write!(js_output, "initial: {}n", memory.initial).unwrap();
@@ -418,18 +425,18 @@ fn post_processing(output_path: &Path, main_memory: MainMemory<'_>) {
418425
}
419426

420427
if memory.memory64 {
421-
js_output.push_str(", address: 'i64'");
428+
js_output.write_all(b", address: 'i64'").unwrap();
422429
}
423430

424431
if memory.shared {
425-
js_output.push_str(", shared: true");
432+
js_output.write_all(b", shared: true").unwrap();
426433
}
427434

428-
js_output.push_str(" })\n\n");
435+
js_output.write_all(b" })\n\n").unwrap();
429436

430437
// Output requested embedded JS.
431438
if !found_embed.is_empty() {
432-
js_output.push_str("const jsEmbed = {\n");
439+
js_output.write_all(b"const jsEmbed = {\n").unwrap();
433440

434441
for (package, embeds) in found_embed {
435442
writeln!(js_output, "\t{package}: {{").unwrap();
@@ -438,25 +445,27 @@ fn post_processing(output_path: &Path, main_memory: MainMemory<'_>) {
438445
write!(js_output, "\t\t\"{name}\": ").unwrap();
439446

440447
for (position, line) in js.lines().with_position() {
441-
js_output.push_str(line);
448+
js_output.write_all(line.as_bytes()).unwrap();
442449

443450
if let Position::First | Position::Middle = position {
444-
js_output.push_str("\n\t\t");
451+
js_output.write_all(b"\n\t\t").unwrap();
445452
}
446453
}
447454

448-
js_output.push_str(",\n");
455+
js_output.write_all(b",\n").unwrap();
449456
}
450457

451-
js_output.push_str("\t},\n");
458+
js_output.write_all(b"\t},\n").unwrap();
452459
}
453460

454-
js_output.push_str("}\n\n");
461+
js_output.write_all(b"}\n\n").unwrap();
455462
}
456463

457464
// Create our `importObject`.
458-
js_output.push_str("export const importObject = {\n");
459-
js_output.push_str("\tjs_bindgen: { memory },\n");
465+
js_output
466+
.write_all(b"export const importObject = {\n")
467+
.unwrap();
468+
js_output.write_all(b"\tjs_bindgen: { memory },\n").unwrap();
460469

461470
for (module, names) in found_import {
462471
writeln!(js_output, "\t{module}: {{").unwrap();
@@ -465,24 +474,27 @@ fn post_processing(output_path: &Path, main_memory: MainMemory<'_>) {
465474
write!(js_output, "\t\t\"{name}\": ").unwrap();
466475

467476
for (position, line) in js.lines().with_position() {
468-
js_output.push_str(line);
477+
js_output.write_all(line.as_bytes()).unwrap();
469478

470479
if let Position::First | Position::Middle = position {
471-
js_output.push_str("\n\t\t");
480+
js_output.write_all(b"\n\t\t").unwrap();
472481
}
473482
}
474483

475-
js_output.push_str(",\n");
484+
js_output.write_all(b",\n").unwrap();
476485
}
477486

478-
js_output.push_str("\t},\n");
487+
js_output.write_all(b"\t},\n").unwrap();
479488
}
480489

481-
js_output.push_str("}\n");
490+
js_output.write_all(b"}\n").unwrap();
482491

483-
fs::write(
484-
output_path.with_file_name(package).with_extension("js"),
485-
js_output,
486-
)
487-
.expect("output JS file should be writable");
492+
js_output.into_inner().unwrap().sync_all().unwrap();
493+
494+
// We could write into the file directly, but `wasm-encoder` doesn't support
495+
// `io::Write`: https://github.com/bytecodealliance/wasm-tools/issues/778.
496+
//
497+
// When it does, we should rename the old file and write to a new file. This way
498+
// we can keep parsing and writing at the same time without allocating memory.
499+
fs::write(output_path, wasm_output).expect("output Wasm file should be writable");
488500
}

0 commit comments

Comments
 (0)