Add no_std fs

This commit is contained in:
Richard Feldman 2024-07-04 13:11:44 -04:00
parent 28420e90ad
commit 9234427f30
No known key found for this signature in database
GPG key ID: F1F21AA5B1D9E43B
6 changed files with 693 additions and 40 deletions

13
Cargo.lock generated
View file

@ -941,6 +941,13 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs"
version = "0.0.1"
dependencies = [
"widestring",
]
[[package]]
name = "fs_at"
version = "0.1.10"
@ -4551,6 +4558,12 @@ version = "0.25.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc"
[[package]]
name = "widestring"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
[[package]]
name = "winapi"
version = "0.3.9"

View file

@ -1,40 +1,41 @@
[workspace]
members = [
"crates/compiler/*",
"crates/vendor/*",
"crates/glue",
"crates/cli",
"crates/cli_utils",
"crates/highlight",
"crates/error_macros",
"crates/reporting",
"crates/packaging",
"crates/repl_cli",
"crates/repl_eval",
"crates/repl_test",
"crates/repl_ui",
"crates/repl_wasm",
"crates/repl_expect",
"crates/roc_std",
"crates/test_utils",
"crates/test_utils_dir",
"crates/valgrind",
"crates/tracing",
"crates/utils/*",
"crates/docs",
"crates/docs_cli",
"crates/linker",
"crates/wasi-libc-sys",
"crates/wasm_module",
"crates/wasm_interp",
"crates/language_server",
"crates/compiler/*",
"crates/vendor/*",
"crates/fs",
"crates/glue",
"crates/cli",
"crates/cli_utils",
"crates/highlight",
"crates/error_macros",
"crates/reporting",
"crates/packaging",
"crates/repl_cli",
"crates/repl_eval",
"crates/repl_test",
"crates/repl_ui",
"crates/repl_wasm",
"crates/repl_expect",
"crates/roc_std",
"crates/test_utils",
"crates/test_utils_dir",
"crates/valgrind",
"crates/tracing",
"crates/utils/*",
"crates/docs",
"crates/docs_cli",
"crates/linker",
"crates/wasi-libc-sys",
"crates/wasm_module",
"crates/wasm_interp",
"crates/language_server",
]
exclude = [
"ci/benchmarks/bench-runner",
"ci/repl_basic_test",
# Examples sometimes have Rust hosts in their platforms. The compiler should ignore those.
"examples",
"ci/benchmarks/bench-runner",
"ci/repl_basic_test",
# Examples sometimes have Rust hosts in their platforms. The compiler should ignore those.
"examples",
]
# Needed to be able to run `cargo run -p roc_cli --no-default-features` -
# see www/build.sh for more.
@ -69,7 +70,9 @@ version = "0.0.1"
# change the tag value in this Cargo.toml to point to that tag, and `cargo update`.
# This way, GitHub Actions works and nobody's builds get broken.
# TODO: Switch this back to roc-lang/inkwell once it is updated
inkwell = { git = "https://github.com/roc-lang/inkwell", branch = "inkwell-llvm-16", features = ["llvm16-0"] }
inkwell = { git = "https://github.com/roc-lang/inkwell", branch = "inkwell-llvm-16", features = [
"llvm16-0",
] }
arrayvec = "0.7.2" # update roc_std/Cargo.toml on change
backtrace = "0.3.67"
@ -78,18 +81,27 @@ bincode = "1.3.3"
bitflags = "1.3.2"
bitvec = "1.0.1"
blake3 = "1.3.3"
brotli = "3.3.4" # used for decompressing tarballs over HTTPS, if the server supports brotli
brotli = "3.3.4" # used for decompressing tarballs over HTTPS, if the server supports brotli
bumpalo = { version = "3.12.0", features = ["collections"] }
bytemuck = { version = "1.13.1", features = ["derive"] }
capstone = { version = "0.11.0", default-features = false }
cgmath = "0.18.0"
chrono = "0.4.26"
clap = { version = "4.2.7", default-features = false, features = ["std", "color", "suggestions", "help", "usage", "error-context"] }
clap = { version = "4.2.7", default-features = false, features = [
"std",
"color",
"suggestions",
"help",
"usage",
"error-context",
] }
colored = "2.0.0"
console_error_panic_hook = "0.1.7"
const_format = { version = "0.2.30", features = ["const_generics"] }
copypasta = "0.8.2"
criterion = { git = "https://github.com/Anton-4/criterion.rs", features = ["html_reports"], rev = "30ea0c5" }
criterion = { git = "https://github.com/Anton-4/criterion.rs", features = [
"html_reports",
], rev = "30ea0c5" }
criterion-perf-events = { git = "https://github.com/Anton-4/criterion-perf-events", rev = "0f38c3e" }
crossbeam = "0.8.2"
dircpy = "0.3.14"
@ -102,7 +114,12 @@ fs_extra = "1.3.0"
futures = "0.3.26"
glyph_brush = "0.7.7"
hashbrown = { version = "0.14.3" }
iced-x86 = { version = "1.18.0", default-features = false, features = ["std", "decoder", "op_code_info", "instr_info"] }
iced-x86 = { version = "1.18.0", default-features = false, features = [
"std",
"decoder",
"op_code_info",
"instr_info",
] }
im = "15.1.0"
im-rc = "15.1.0"
indexmap = "2.1.0"
@ -138,12 +155,17 @@ quote = "1.0.23"
rand = "0.8.5"
regex = "1.7.1"
remove_dir_all = "0.8.1"
reqwest = { version = "0.11.23", default-features = false, features = ["blocking", "rustls-tls"] } # default-features=false removes libopenssl as a dependency on Linux, which might not be available!
reqwest = { version = "0.11.23", default-features = false, features = [
"blocking",
"rustls-tls",
] } # default-features=false removes libopenssl as a dependency on Linux, which might not be available!
rlimit = "0.9.1"
rustyline = { git = "https://github.com/roc-lang/rustyline", rev = "e74333c" }
rustyline-derive = { git = "https://github.com/roc-lang/rustyline", rev = "e74333c" }
schemars = "0.8.12"
serde = { version = "1.0.153", features = ["derive"] } # update roc_std/Cargo.toml on change
serde = { version = "1.0.153", features = [
"derive",
] } # update roc_std/Cargo.toml on change
serde-xml-rs = "0.6.0"
serde_json = "1.0.94" # update roc_std/Cargo.toml on change
serial_test = "1.0.0"
@ -191,7 +213,7 @@ debug = true
[profile.release-with-lto]
inherits = "release"
lto = "thin" # TODO: We could consider full here since this is only used for packaged release on github.
lto = "thin" # TODO: We could consider full here since this is only used for packaged release on github.
[profile.debug-full]
inherits = "dev"

11
crates/fs/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "fs"
description = "no_std filesystem access. These operations all use native OS strings and do not allocate."
authors.workspace = true
edition.workspace = true
license.workspace = true
version.workspace = true
[target.'cfg(windows)'.dependencies]
widestring = { version = "1.1.0", default-features = false }

559
crates/fs/src/file.rs Normal file
View file

@ -0,0 +1,559 @@
use crate::native_path::NativePath;
use core::{ffi::CStr, fmt, mem::MaybeUninit};
#[cfg(windows)]
use widestring::U16CString;
#[cfg(unix)]
use core::ffi::{c_char, c_int};
#[cfg(unix)]
pub struct File {
fd: i32,
}
#[cfg(windows)]
pub struct File {
handle: isize,
}
#[cfg(unix)]
extern "C" {
fn open(pathname: *const c_char, flags: c_int, ...) -> c_int;
fn close(fd: c_int) -> c_int;
fn read(fd: c_int, buf: *mut MaybeUninit<u8>, count: usize) -> isize;
fn write(fd: c_int, buf: *const u8, count: usize) -> isize;
fn mkstemp(template: *mut c_char) -> c_int;
fn unlink(pathname: *const c_char) -> c_int;
}
#[cfg(unix)]
impl Drop for File {
fn drop(&mut self) {
unsafe {
close(self.fd);
}
}
}
#[cfg(windows)]
extern "system" {
fn CreateFileW(
lpFileName: *const u16,
dwDesiredAccess: u32,
dwShareMode: u32,
lpSecurityAttributes: *mut core::ffi::c_void,
dwCreationDisposition: u32,
dwFlagsAndAttributes: u32,
hTemplateFile: isize,
) -> isize;
fn CloseHandle(hObject: isize) -> i32;
fn ReadFile(
hFile: isize,
lpBuffer: *mut MaybeUninit<u8>,
nNumberOfBytesToRead: u32,
lpNumberOfBytesRead: *mut u32,
lpOverlapped: *mut core::ffi::c_void,
) -> i32;
fn WriteFile(
hFile: isize,
lpBuffer: *const u8,
nNumberOfBytesToWrite: u32,
lpNumberOfBytesWritten: *mut u32,
lpOverlapped: *mut core::ffi::c_void,
) -> i32;
fn DeleteFileW(lpFileName: *const u16) -> i32;
fn GetTempPathW(nBufferLength: u32, lpBuffer: *mut u16) -> u32;
fn CreateDirectoryW(
lpPathName: *const u16,
lpSecurityAttributes: *mut core::ffi::c_void,
) -> i32;
}
#[cfg(windows)]
impl Drop for File {
fn drop(&mut self) {
unsafe {
CloseHandle(self.handle);
}
}
}
#[cfg_attr(unix, repr(i32))]
#[cfg_attr(windows, repr(u32))]
#[derive(Copy, Clone, PartialEq, Eq)]
#[non_exhaustive] // There are tons of other error numbers that functions which return this might return!
pub enum FileIoErr {
/// "File not found" is the same code (namely, 2) on both UNIX and Windows:
/// ENOENT on UNIX: https://www.man7.org/linux/man-pages/man3/errno.3.html
/// ERROR_FILE_NOT_FOUND on Windows: // https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
NotFound = 2,
#[cfg(unix)]
/// EACCES: https://www.man7.org/linux/man-pages/man3/errno.3.html
AccessDenied = 13,
#[cfg(windows)]
/// ERROR_ACCESS_DENIED: https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
AccessDenied = 5,
}
impl fmt::Debug for FileIoErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FileIoErr::NotFound => write!(f, "NotFound ({})", *self as i32),
#[cfg(unix)]
FileIoErr::AccessDenied => write!(f, "AccessDenied ({})", *self as i32),
#[cfg(windows)]
FileIoErr::AccessDenied => write!(f, "AccessDenied ({})", *self as i32),
}
}
}
impl FileIoErr {
#[cfg(target_os = "macos")]
fn most_recent() -> FileIoErr {
extern "C" {
fn __error() -> *mut FileIoErr;
}
unsafe { *__error() }
}
#[cfg(all(target_family = "unix", not(target_os = "macos")))]
fn most_recent() -> FileIoErr {
extern "C" {
fn __errno_location() -> *mut FileIoErr;
}
unsafe { errno = *__errno_location() }
}
#[cfg(windows)]
fn most_recent() -> FileIoErr {
extern "system" {
fn GetLastError() -> u32;
}
unsafe { FileIoErr(GetLastError()) }
}
}
//// OPEN FILE ////
#[cfg(unix)]
impl File {
const O_WRONLY: c_int = 1;
const O_RDWR: c_int = 2;
/// source: https://github.com/apple-open-source/macos/blob/8038f956fee603c486e75adbf93cac7a66064f02/Libc/exclave/sys/fcntl.h#L104
#[cfg(target_os = "macos")]
const O_CREAT: c_int = 512;
#[cfg(not(target_os = "macos"))]
const O_CREAT: c_int = 64;
pub fn open(path: &NativePath) -> Result<Self, FileIoErr> {
let fd = unsafe { open(path.inner.as_ptr(), Self::O_RDWR) };
if fd != -1 {
Ok(File { fd })
} else {
Err(FileIoErr::most_recent())
}
}
pub fn create(path: &NativePath) -> Result<Self, FileIoErr> {
let fd = unsafe {
open(
path.inner.as_ptr(),
Self::O_CREAT | Self::O_WRONLY,
0o644, // read/write mode
)
};
if fd != -1 {
Ok(File { fd })
} else {
Err(FileIoErr::most_recent())
}
}
}
#[cfg(windows)]
impl File {
const GENERIC_WRITE: u32 = 0x40000000;
const CREATE_ALWAYS: u32 = 2;
const FILE_SHARE_WRITE: u32 = 0x00000002;
pub fn open(path: &NativePath) -> Result<Self, FileIoErr> {
let handle = unsafe {
CreateFileW(
path.inner.as_ptr(),
Self::GENERIC_READ | Self::GENERIC_WRITE,
Self::FILE_SHARE_READ | Self::FILE_SHARE_WRITE,
core::ptr::null_mut(),
Self::OPEN_EXISTING,
0,
0,
)
};
if handle != -1 {
Ok(File { handle })
} else {
Err(FileIoErr::errno())
}
}
pub fn create(path: &NativePath) -> Result<Self, FileIoErr> {
let handle = unsafe {
CreateFileW(
path.inner.as_ptr(),
Self::GENERIC_READ | Self::GENERIC_WRITE,
Self::FILE_SHARE_READ | Self::FILE_SHARE_WRITE,
core::ptr::null_mut(),
Self::CREATE_ALWAYS,
0,
0,
)
};
if handle != -1 {
Ok(File { handle })
} else {
Err(FileIoErr::errno())
}
}
}
//// REMOVE ////
#[cfg(unix)]
impl File {
/// Returns whether it succeeded.
pub fn remove(path: &NativePath) -> bool {
unsafe { unlink(path.inner.as_ptr()) == 0 }
}
}
#[cfg(windows)]
impl File {
/// Returns whether it succeeded.
pub fn remove(path: &NativePath) -> bool {
unsafe { DeleteFileW(path.inner.as_ptr()) != 0 }
}
}
//// TEMPFILE ////
#[cfg(unix)]
impl File {
/// Create a tempfile, open it as a File, pass that File and its generated path
/// to the given function, and then delete it after the function returns.
pub fn with_tempfile<T>(
run: impl FnOnce(Result<(&NativePath, &mut Self), FileIoErr>) -> T,
) -> T {
const TEMPLATE: &[u8] = b"/tmp/roc_tempfile_XXXXXX\0";
let mut template = [0; TEMPLATE.len()];
template.copy_from_slice(TEMPLATE);
// mkstemp replaces the Xs in the template with chars that result in a unique path.
let fd = unsafe { mkstemp(template.as_mut_ptr().cast()) };
if fd != -1 {
let path_cstr = unsafe { CStr::from_bytes_with_nul_unchecked(&template) };
let native_path: &NativePath = path_cstr.into();
let mut file = File { fd };
// Since we pass an owned File, it will get closed automatically once dropped.
// This in turn will result in the file getting deleted, since we already unlinked it.
let answer = run(Ok((native_path, &mut file)));
// Unlink the file now that we're done with it.
unsafe {
unlink(template.as_ptr().cast());
}
answer
} else {
// Here we assume that since mkstemp errored out, the file was not created
// and we shouldn't attempt to unlink it.
run(Err(FileIoErr::most_recent()))
}
}
#[cfg(windows)]
/// Create a tempfile, open it as a File, pass that File and its generated path
/// to the given function, and then delete it after the function returns.
pub fn with_tempfile<T>(run: impl FnOnce(Result<(&NativePath, Self), FileIoErr>) -> T) -> T {
let mut temp_path_buf = [0u16; 261];
let temp_path_len =
unsafe { GetTempPathW(temp_path_buf.len() as u32, temp_path_buf.as_mut_ptr()) };
if temp_path_len == 0 || temp_path_len > temp_path_buf.len() as u32 {
let native_path =
NativePath::new(U16CString::from_vec_with_nul(temp_path_buf.to_vec()).unwrap());
return run(&native_path, None);
}
let mut template = Vec::from(&temp_path_buf[..temp_path_len as usize]);
template.extend_from_slice(&[
b't' as u16,
b'e' as u16,
b'm' as u16,
b'p' as u16,
b'f' as u16,
b'i' as u16,
b'l' as u16,
b'e' as u16,
b'.' as u16,
b't' as u16,
b'm' as u16,
b'p' as u16,
0,
]);
let mut temp_file_name = [0u16; 261];
let result = unsafe {
GetTempFileNameW(
temp_path_buf.as_ptr(),
U16CString::from_str("tmp").unwrap().as_ptr(),
0,
temp_file_name.as_mut_ptr(),
)
};
if result == 0 {
let native_path =
NativePath::new(U16CString::from_vec_with_nul(temp_file_name.to_vec()).unwrap());
return run(&native_path, None);
}
let handle = unsafe {
CreateFileW(
temp_file_name.as_ptr(),
0x40000000, // GENERIC_WRITE
0,
core::ptr::null_mut(),
1, // CREATE_ALWAYS
0x80, // FILE_ATTRIBUTE_TEMPORARY
0,
)
};
if handle != -1 {
// Since we pass an owned File, it will get closed automatically once dropped.
// This in turn will result in the file getting deleted, since Windows sets
// tempfiles to delete once the handle is closed.
run(Some(File { handle }))
} else {
// Here we assume that since CreateFileW errored out, the file was not created
// and we shouldn't attempt to delete it.
run(None)
}
}
}
//// READ FILE ////
impl File {
#[cfg(unix)]
pub fn read_into<'buf>(
&mut self,
buf: &'buf mut [MaybeUninit<u8>],
) -> Result<&'buf mut [u8], FileIoErr> {
let bytes_read = unsafe { read(self.fd, buf.as_mut_ptr(), buf.len()) };
if bytes_read >= 0 {
// Return the subset of the buf that we actually initialized.
let buf_ptr = buf.as_mut_ptr();
let len = bytes_read as usize;
Ok(unsafe { core::slice::from_raw_parts_mut(buf_ptr.cast(), len) })
} else {
Err(FileIoErr::most_recent())
}
}
#[cfg(windows)]
pub fn read_into<'buf>(
&mut self,
buf: &'buf mut [MaybeUninit<u8>],
) -> Result<&'buf mut [u8], FileIoErr> {
let mut bytes_read: u32 = 0;
let result = unsafe {
ReadFile(
self.handle,
buf.as_mut_ptr(),
buf.len() as u32,
&mut bytes_read,
core::ptr::null_mut(),
)
};
if result >= 0 {
let buf_ptr = buf.as_mut_ptr();
let len = bytes_read as usize;
Ok(unsafe { core::slice::from_raw_parts_mut(buf_ptr.cast(), len) })
} else {
Err(FileIoErr::most_recent())
}
}
}
//// WRITE FILE ////
impl File {
#[cfg(unix)]
pub fn write(&mut self, buf: &[u8]) -> Result<usize, FileIoErr> {
let bytes_written = unsafe { write(self.fd, buf.as_ptr(), buf.len()) };
if bytes_written >= 0 {
Ok(bytes_written as usize)
} else {
Err(FileIoErr::most_recent())
}
}
#[cfg(windows)]
pub fn write(&mut self, buf: &[u8]) -> Result<usize, FileIoErr> {
let mut bytes_written: u32 = 0;
let result = unsafe {
WriteFile(
self.handle,
buf.as_ptr(),
buf.len() as u32,
&mut bytes_written,
core::ptr::null_mut(),
)
};
if result != 0 {
Ok(bytes_written as usize)
} else {
Err(FileIoErr::most_recent())
}
}
}
#[cfg(test)]
mod tests {
use super::{File, FileIoErr};
use crate::native_path::NativePath;
use core::mem::MaybeUninit;
#[cfg(unix)]
use core::ffi::CStr;
#[cfg(windows)]
use widestring::U16CString;
#[cfg(unix)]
fn str_to_cstr(s: &str) -> &CStr {
CStr::from_bytes_with_nul(s.as_bytes()).expect("CStr conversion failed")
}
#[cfg(windows)]
fn str_to_u16cstr(s: &str) -> &widestring::U16CStr {
widestring::U16CString::from_str(s)
.expect("U16CStr conversion failed")
.as_ref()
}
#[cfg(unix)]
fn mock_path(path: &str) -> &NativePath {
str_to_cstr(path).into()
}
#[cfg(windows)]
fn mock_path(path: &str) -> &NativePath {
str_to_u16cstr(path).into()
}
#[test]
fn file_not_found() {
let file_result = File::open(mock_path("test_file_that_should_not_exist\0"));
assert_eq!(
file_result.map(|_| ()),
Err(FileIoErr::NotFound),
"File should not exist: test_file_that_should_not_exist"
);
}
#[test]
fn create_write_read_delete() {
let path = mock_path("roc_test_read_file\0");
let mut file = File::create(&path).unwrap();
// Write some data to the file
let write_data: &[u8] = &[42u8; 10];
let write_result = file.write(write_data);
assert_eq!(
write_result,
Ok(write_data.len()),
"Failed to write all data to the file: roc_test_read_file"
);
// Close the file by dropping the file descriptor
drop(file);
// Reopen the file
let mut file = File::open(path).unwrap();
let mut input_buf = [MaybeUninit::uninit(); 10];
let result = file.read_into(&mut input_buf);
assert_eq!(
result.map(|mut_slice| &*mut_slice),
Ok(write_data),
"Failed to read all the data from the file: roc_test_read_file"
);
// Close the file by dropping the file descriptor
drop(file);
// Remove the file
let result = File::remove(&path);
assert!(result, "Failed to remove the file: roc_test_read_file");
// Verify that the file no longer exists
let file_result = File::open(path);
assert_eq!(
file_result.map(|_| ()),
Err(FileIoErr::NotFound),
"File should not exist after removal: roc_test_read_file"
);
}
#[test]
fn tempfile() {
const DATA: &[u8] = &[42; 10];
let buf = &mut [MaybeUninit::uninit(); DATA.len()];
let answer = File::with_tempfile(|result| {
let (native_path, file) = result.unwrap();
// Write some data to the tempfile
assert_eq!(
Ok(DATA.len()),
file.write(DATA),
"Failed to write all the bytes to the tempfile"
);
// Reopen the tempfile with a fresh file descriptor, and read the data back in from it
File::open(native_path).unwrap().read_into(buf)
});
// The data read should match the data written
assert_eq!(
Ok(DATA),
answer.map(|mut_slice| &*mut_slice),
"Data read from the file does not match data written to the file"
);
}
}

7
crates/fs/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
#![cfg_attr(not(any(debug_assertions, test)), no_std)]
mod file;
mod native_path;
pub use file::*;
pub use native_path::*;

View file

@ -0,0 +1,41 @@
use core::{fmt, mem};
#[cfg(unix)]
use core::ffi::CStr;
#[cfg(windows)]
use widestring::U16CStr;
#[cfg(unix)]
#[repr(transparent)]
pub struct NativePath {
pub(crate) inner: CStr,
}
#[cfg(windows)]
#[repr(transparent)]
pub struct NativePath {
pub(crate) inner: U16CStr,
}
impl fmt::Debug for NativePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.inner.fmt(f)
}
}
#[cfg(unix)]
impl<'a> From<&'a CStr> for &'a NativePath {
fn from(c_str: &'a CStr) -> Self {
// Safety: Self is repr(transparent)
unsafe { mem::transmute(c_str) }
}
}
#[cfg(windows)]
impl<'a> From<&'a U16CStr> for &'a NativePath {
fn from(inner: &'a U16CStr) -> Self {
// Safety: Self is repr(transparent)
unsafe { mem::transmute(c_str) }
}
}