Skip to content

Commit 362f857

Browse files
authored
Remove system-interface from wasmtime-wasi (#13109)
* Remove `system-interface` from `wasi-common` This commit removes the `system-interface` dependency from `wasi-common`, reimplementing necessary functionality within the crate itself as appropriate. The goal of this commit is to trim our dependency tree. The `system-interface` crate has not received an update in over a year and continues to pull in an older `rustix` dependency for example. Additionally I've personally found it confusing and surprising in the past to trace through all the layers of abstractions from `wasmtime-wasi` to the OS, and I'd like to start slimming this down to be more local within Wasmtime rather than depending on a tree of crates. The `system-interface` crate is a relatively thin wrapper around `cap-std`-style crates providing a platform-agnostic API. This sometimes fits what WASI wants, and sometimes doesn't. For example all reads/writes to files within WASI currently require that Wasmtime maintains a file cursor itself meaning that the underlying OS file cursor doesn't actually matter. Reads and writes through the `system-interface` abstraction, however, keep the cursor up-to-date to have the same semantics across Unix and Windows which differ in the behavior of the underlying syscalls. This is unnecessarily adds overhead to Wasmtime's implementation of these APIs where they're otherwise not required. Effectively `system-interface` is not receiving much maintenance (old dependency on `rustix` has persisted for ~1 year), its an extra layer of abstraction to debug when issues arise, and its abstractions are not always the best fit for WASI's semantics meaning that it can add performance overhead. The replacement of inlining implementations within Wasmtime is not too too costly and, personally, I view as worth it. I'll note that this doesn't delete the `system-interface` crate entirely. That would require removing it from `wasi-common` as well, which is the subject of #13108. * Portability fixes prtest:full * More portability fixes * Allow some more errors on Windows * Try to fix test on Windows * Fix test issue * Limit pwritev2 to linux
1 parent 60ff5ec commit 362f857

File tree

13 files changed

+330
-100
lines changed

13 files changed

+330
-100
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/test-programs/src/bin/p1_file_write.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ unsafe fn test_file_long_write(dir_fd: wasip1::Fd, filename: &str, blocking_mode
125125
}],
126126
);
127127
assert!(
128-
res == Err(wasip1::ERRNO_BADF) || res == Err(wasip1::ERRNO_PERM),
128+
res == Err(wasip1::ERRNO_BADF)
129+
|| res == Err(wasip1::ERRNO_PERM)
130+
|| res == Err(wasip1::ERRNO_ACCES),
129131
"bad result {res:?}"
130132
)
131133
}

crates/test-programs/src/bin/p1_path_open_read_write.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ unsafe fn test_path_open_read_write(dir_fd: wasip1::Fd, blocking_mode: BlockingM
5151
.err()
5252
.expect("read of writeonly fails"),
5353
wasip1::ERRNO_PERM,
54-
wasip1::ERRNO_BADF
54+
wasip1::ERRNO_BADF,
55+
wasip1::ERRNO_ACCES
5556
);
5657

5758
wasip1::fd_close(f_readonly).expect("close readonly");
@@ -89,7 +90,8 @@ unsafe fn test_path_open_read_write(dir_fd: wasip1::Fd, blocking_mode: BlockingM
8990
.err()
9091
.expect("read of writeonly fails"),
9192
wasip1::ERRNO_PERM,
92-
wasip1::ERRNO_BADF
93+
wasip1::ERRNO_BADF,
94+
wasip1::ERRNO_ACCES
9395
);
9496
let bytes_written = blocking_mode
9597
.write(f_writeonly, &[ciovec])

crates/test-programs/src/bin/p3_filesystem_file_read_write.rs

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,41 +12,43 @@ impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
1212
let (dir, _) = &preopens[0];
1313

1414
let filename = "test.txt";
15-
let file = dir
16-
.open_at(
17-
PathFlags::empty(),
18-
filename.to_string(),
19-
OpenFlags::CREATE,
20-
DescriptorFlags::READ | DescriptorFlags::WRITE,
21-
)
22-
.await
23-
.unwrap();
24-
let (mut data_tx, data_rx) = wit_stream::new();
25-
join!(
26-
async {
27-
file.write_via_stream(data_rx, 5).await.unwrap();
28-
},
29-
async {
30-
let remaining = data_tx.write_all(b"Hello, ".to_vec()).await;
31-
assert!(remaining.is_empty());
32-
let remaining = data_tx.write_all(b"World!".to_vec()).await;
33-
assert!(remaining.is_empty());
34-
drop(data_tx);
35-
},
36-
);
37-
let (data_rx, data_fut) = file.read_via_stream(0);
38-
let contents = data_rx.collect().await;
39-
data_fut.await.unwrap();
40-
assert_eq!(
41-
String::from_utf8_lossy(&contents),
42-
"\0\0\0\0\0Hello, World!"
43-
);
15+
{
16+
let file = dir
17+
.open_at(
18+
PathFlags::empty(),
19+
filename.to_string(),
20+
OpenFlags::CREATE,
21+
DescriptorFlags::READ | DescriptorFlags::WRITE,
22+
)
23+
.await
24+
.unwrap();
25+
let (mut data_tx, data_rx) = wit_stream::new();
26+
join!(
27+
async {
28+
file.write_via_stream(data_rx, 5).await.unwrap();
29+
},
30+
async {
31+
let remaining = data_tx.write_all(b"Hello, ".to_vec()).await;
32+
assert!(remaining.is_empty());
33+
let remaining = data_tx.write_all(b"World!".to_vec()).await;
34+
assert!(remaining.is_empty());
35+
drop(data_tx);
36+
},
37+
);
38+
let (data_rx, data_fut) = file.read_via_stream(0);
39+
let contents = data_rx.collect().await;
40+
data_fut.await.unwrap();
41+
assert_eq!(
42+
String::from_utf8_lossy(&contents),
43+
"\0\0\0\0\0Hello, World!"
44+
);
4445

45-
// Test that file read streams behave like other read streams.
46-
let (data_rx, data_fut) = file.read_via_stream(5);
47-
let contents = data_rx.collect().await;
48-
data_fut.await.unwrap();
49-
assert_eq!(String::from_utf8_lossy(&contents), "Hello, World!");
46+
// Test that file read streams behave like other read streams.
47+
let (data_rx, data_fut) = file.read_via_stream(5);
48+
let contents = data_rx.collect().await;
49+
data_fut.await.unwrap();
50+
assert_eq!(String::from_utf8_lossy(&contents), "Hello, World!");
51+
}
5052

5153
dir.unlink_file_at(filename.to_string()).await.unwrap();
5254
Ok(())

crates/wasi/Cargo.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ io-lifetimes = { workspace = true }
3434
fs-set-times = { workspace = true }
3535
bitflags = { workspace = true }
3636
async-trait = { workspace = true }
37-
system-interface = { workspace = true}
3837
futures = { workspace = true }
3938
url = { workspace = true }
4039
rand = { workspace = true, features = ['std_rng', 'thread_rng'] }
40+
cfg-if = { workspace = true }
4141

4242
[dev-dependencies]
4343
tokio = { workspace = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net", "macros", "fs"] }
@@ -54,9 +54,17 @@ rustix = { workspace = true, features = ["event", "fs", "net"] }
5454

5555
[target.'cfg(windows)'.dependencies]
5656
io-extras = { workspace = true }
57-
windows-sys = { workspace = true }
5857
rustix = { workspace = true, features = ["event", "net"] }
5958

59+
[target.'cfg(windows)'.dependencies.windows-sys]
60+
workspace = true
61+
features = [
62+
"Wdk_Storage_FileSystem",
63+
"Win32_Foundation",
64+
"Win32_Storage_FileSystem",
65+
"Win32_System_IO",
66+
]
67+
6068
[features]
6169
default = ["p1", "p2"]
6270
p0 = ["p1"]

crates/wasi/src/filesystem.rs

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ use tracing::debug;
88
use wasmtime::component::{HasData, Resource, ResourceTable};
99
use wasmtime::error::Context as _;
1010

11+
#[cfg(unix)]
12+
pub(crate) mod unix;
13+
#[cfg(unix)]
14+
pub(crate) use unix as sys;
15+
#[cfg(windows)]
16+
pub(crate) mod windows;
17+
#[cfg(windows)]
18+
pub(crate) use windows as sys;
19+
1120
/// A helper struct which implements [`HasData`] for the `wasi:filesystem` APIs.
1221
///
1322
/// This can be useful when directly calling `add_to_linker` functions directly,
@@ -350,6 +359,16 @@ impl From<&cap_std::fs::Metadata> for MetadataHashValue {
350359
}
351360
}
352361

362+
#[derive(Copy, Clone, Debug)]
363+
pub(crate) enum Advice {
364+
Normal,
365+
Sequential,
366+
Random,
367+
WillNeed,
368+
DontNeed,
369+
NoReuse,
370+
}
371+
353372
#[cfg(unix)]
354373
fn from_raw_os_error(err: Option<i32>) -> Option<ErrorCode> {
355374
use rustix::io::Errno as RustixErrno;
@@ -509,25 +528,9 @@ impl Descriptor {
509528
}
510529

511530
pub(crate) async fn get_flags(&self) -> Result<DescriptorFlags, ErrorCode> {
512-
use system_interface::fs::{FdFlags, GetSetFdFlags};
513-
514-
fn get_from_fdflags(flags: FdFlags) -> DescriptorFlags {
515-
let mut out = DescriptorFlags::empty();
516-
if flags.contains(FdFlags::DSYNC) {
517-
out |= DescriptorFlags::REQUESTED_WRITE_SYNC;
518-
}
519-
if flags.contains(FdFlags::RSYNC) {
520-
out |= DescriptorFlags::DATA_INTEGRITY_SYNC;
521-
}
522-
if flags.contains(FdFlags::SYNC) {
523-
out |= DescriptorFlags::FILE_INTEGRITY_SYNC;
524-
}
525-
out
526-
}
527531
match self {
528532
Self::File(f) => {
529-
let flags = f.run_blocking(|f| f.get_fd_flags()).await?;
530-
let mut flags = get_from_fdflags(flags);
533+
let mut flags = f.run_blocking(|f| sys::get_flags(f)).await?;
531534
if f.open_mode.contains(OpenMode::READ) {
532535
flags |= DescriptorFlags::READ;
533536
}
@@ -537,8 +540,7 @@ impl Descriptor {
537540
Ok(flags)
538541
}
539542
Self::Dir(d) => {
540-
let flags = d.run_blocking(|d| d.get_fd_flags()).await?;
541-
let mut flags = get_from_fdflags(flags);
543+
let mut flags = d.run_blocking(|d| sys::get_flags(d)).await?;
542544
if d.open_mode.contains(OpenMode::READ) {
543545
flags |= DescriptorFlags::READ;
544546
}
@@ -747,10 +749,9 @@ impl File {
747749
&self,
748750
offset: u64,
749751
len: u64,
750-
advice: system_interface::fs::Advice,
752+
advice: Advice,
751753
) -> Result<(), ErrorCode> {
752-
use system_interface::fs::FileIoExt as _;
753-
self.run_blocking(move |f| f.advise(offset, len, advice))
754+
self.run_blocking(move |f| sys::advise(f, offset, len, advice))
754755
.await?;
755756
Ok(())
756757
}
@@ -930,7 +931,6 @@ impl Dir {
930931
allow_blocking_current_thread: bool,
931932
) -> Result<Descriptor, ErrorCode> {
932933
use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsMaybeDirExt};
933-
use system_interface::fs::{FdFlags, GetSetFdFlags};
934934

935935
if !self.perms.contains(DirPerms::READ) {
936936
return Err(ErrorCode::NotPermitted);
@@ -1023,18 +1023,14 @@ impl Dir {
10231023

10241024
let opened = self
10251025
.run_blocking::<_, std::io::Result<OpenResult>>(move |d| {
1026-
let mut opened = d.open_with(&path, &opts)?;
1026+
let opened = d.open_with(&path, &opts)?;
10271027
if opened.metadata()?.is_dir() {
10281028
Ok(OpenResult::Dir(cap_std::fs::Dir::from_std_file(
10291029
opened.into_std(),
10301030
)))
10311031
} else if oflags.contains(OpenFlags::DIRECTORY) {
10321032
Ok(OpenResult::NotDir)
10331033
} else {
1034-
// FIXME cap-std needs a nonblocking open option so that files reads and writes
1035-
// are nonblocking. Instead we set it after opening here:
1036-
let set_fd_flags = opened.new_set_fd_flags(FdFlags::NONBLOCK)?;
1037-
opened.set_fd_flags(set_fd_flags)?;
10381034
Ok(OpenResult::File(opened))
10391035
}
10401036
})

crates/wasi/src/filesystem/unix.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use crate::filesystem::{Advice, DescriptorFlags};
2+
use io_lifetimes::AsFilelike;
3+
use rustix::fs::{OFlags, fcntl_getfl, fcntl_setfl};
4+
use rustix::io::write;
5+
use std::fs::File;
6+
use std::io;
7+
use std::os::fd::AsFd;
8+
use std::os::unix::fs::FileExt;
9+
10+
pub(crate) fn get_flags(file: impl AsFd) -> io::Result<DescriptorFlags> {
11+
let flags = fcntl_getfl(file)?;
12+
let mut ret = DescriptorFlags::empty();
13+
ret.set(
14+
DescriptorFlags::REQUESTED_WRITE_SYNC,
15+
flags.contains(OFlags::DSYNC),
16+
);
17+
ret.set(
18+
DescriptorFlags::FILE_INTEGRITY_SYNC,
19+
flags.contains(OFlags::SYNC),
20+
);
21+
#[cfg(not(any(target_vendor = "apple", target_os = "freebsd")))]
22+
ret.set(
23+
DescriptorFlags::DATA_INTEGRITY_SYNC,
24+
flags.contains(OFlags::RSYNC),
25+
);
26+
Ok(ret)
27+
}
28+
29+
pub(crate) fn advise(file: impl AsFd, offset: u64, len: u64, advice: Advice) -> io::Result<()> {
30+
cfg_if::cfg_if! {
31+
if #[cfg(target_vendor = "apple")] {
32+
match advice {
33+
Advice::WillNeed => {
34+
rustix::fs::fcntl_rdadvise(file, offset, len)?;
35+
}
36+
Advice::Normal |
37+
Advice::Sequential |
38+
Advice::Random |
39+
Advice::DontNeed |
40+
Advice::NoReuse => {}
41+
}
42+
} else if #[cfg(any(target_os = "linux", target_os = "android"))] {
43+
use std::num::NonZeroU64;
44+
let advice = match advice {
45+
Advice::Normal => rustix::fs::Advice::Normal,
46+
Advice::Sequential => rustix::fs::Advice::Sequential,
47+
Advice::Random => rustix::fs::Advice::Random,
48+
Advice::WillNeed => rustix::fs::Advice::WillNeed,
49+
Advice::DontNeed => rustix::fs::Advice::DontNeed,
50+
Advice::NoReuse => rustix::fs::Advice::NoReuse,
51+
};
52+
rustix::fs::fadvise(file, offset, NonZeroU64::new(len), advice)?;
53+
} else {
54+
// noop on other platforms
55+
let _ = (file, offset, len, advice);
56+
}
57+
}
58+
Ok(())
59+
}
60+
61+
pub(crate) fn append_cursor_unspecified(file: impl AsFd, data: &[u8]) -> io::Result<usize> {
62+
// On Linux, use `pwritev2`.
63+
#[cfg(target_os = "linux")]
64+
{
65+
use rustix::io::{Errno, ReadWriteFlags, pwritev2};
66+
use std::io::IoSlice;
67+
68+
let iovs = [IoSlice::new(data)];
69+
match pwritev2(&file, &iovs, 0, ReadWriteFlags::APPEND) {
70+
Err(Errno::NOSYS) | Err(Errno::NOTSUP) => {}
71+
otherwise => return Ok(otherwise?),
72+
}
73+
}
74+
75+
// Otherwise use `F_SETFL` to switch the file description to append
76+
// mode, do the write, and switch back. This is not atomic with
77+
// respect to other users of the file description, but WASI isn't fully
78+
// threaded right now anyway.
79+
let old_flags = fcntl_getfl(&file)?;
80+
fcntl_setfl(&file, old_flags | OFlags::APPEND)?;
81+
let result = write(&file, data);
82+
fcntl_setfl(&file, old_flags).unwrap();
83+
Ok(result?)
84+
}
85+
86+
pub(crate) fn write_at_cursor_unspecified(
87+
file: impl AsFd,
88+
data: &[u8],
89+
pos: u64,
90+
) -> io::Result<usize> {
91+
file.as_filelike_view::<File>().write_at(data, pos)
92+
}
93+
94+
pub(crate) fn read_at_cursor_unspecified(
95+
file: impl AsFd,
96+
buf: &mut [u8],
97+
pos: u64,
98+
) -> io::Result<usize> {
99+
file.as_filelike_view::<File>().read_at(buf, pos)
100+
}

0 commit comments

Comments
 (0)