From 90ba703506dcd5df19df20fa7cdb0fd5f27e0fd8 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 15:55:24 +0300 Subject: [PATCH 01/31] colors: drop libc invocations for `var_os` Signed-off-by: NotAShelf Change-Id: I572acd49ef687c8049d43a7ba42bd12c6a6a6964 --- src/colors.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/colors.rs b/src/colors.rs index 7c65944..0fca89e 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -37,8 +37,8 @@ impl Colors { } pub static COLORS: LazyLock = LazyLock::new(|| { - const NO_COLOR: *const libc::c_char = c"NO_COLOR".as_ptr(); - let is_no_color = unsafe { !libc::getenv(NO_COLOR).is_null() }; + // Only presence matters; value is irrelevant per the NO_COLOR spec + let is_no_color = std::env::var_os("NO_COLOR").is_some(); Colors::new(is_no_color) }); From 72de00771f3e238bc04c72c84e4b808e41e518ce Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 16:58:16 +0300 Subject: [PATCH 02/31] desktop: replace `libc::getenv` with `env::var_os` Signed-off-by: NotAShelf Change-Id: Ie94268934435b70bfbcd81e4676600206a6a6964 --- src/desktop.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index 501e967..ea863b4 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -1,27 +1,22 @@ -use std::{ffi::CStr, fmt::Write}; +use std::{ffi::OsStr, fmt::Write}; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_desktop_info() -> String { - // Retrieve the environment variables and handle Result types - let desktop_str = unsafe { - let ptr = libc::getenv(c"XDG_CURRENT_DESKTOP".as_ptr()); - if ptr.is_null() { - "Unknown" - } else { - let s = CStr::from_ptr(ptr).to_str().unwrap_or("Unknown"); - s.strip_prefix("none+").unwrap_or(s) - } - }; + let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP"); + let session_os = std::env::var_os("XDG_SESSION_TYPE"); + + let desktop_raw = desktop_os + .as_deref() + .and_then(OsStr::to_str) + .unwrap_or("Unknown"); + let desktop_str = desktop_raw.strip_prefix("none+").unwrap_or(desktop_raw); - let backend_str = unsafe { - let ptr = libc::getenv(c"XDG_SESSION_TYPE".as_ptr()); - if ptr.is_null() { - "Unknown" - } else { - let s = CStr::from_ptr(ptr).to_str().unwrap_or("Unknown"); - if s.is_empty() { "Unknown" } else { s } - } + let session_raw = session_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); + let backend_str = if session_raw.is_empty() { + "Unknown" + } else { + session_raw }; // Pre-calculate capacity: desktop_len + " (" + backend_len + ")" From f6f1b3003a725c40216dd5ead395472bfca99ae7 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 16:59:04 +0300 Subject: [PATCH 03/31] syscall: wrap Linux syscalls directly to replace libc Signed-off-by: NotAShelf Change-Id: I8bb8f3b2e3966cfa2324c0cf8eb447386a6a6964 --- src/syscall.rs | 200 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/src/syscall.rs b/src/syscall.rs index 0c8634b..cedc68a 100644 --- a/src/syscall.rs +++ b/src/syscall.rs @@ -123,6 +123,63 @@ pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize { } } +/// Direct syscall to write to a file descriptor +/// +/// # Returns +/// +/// Number of bytes written or -1 on error +/// +/// # Safety +/// +/// The caller must ensure that: +/// +/// - `buf` points to a valid readable buffer of at least `count` bytes +/// - `fd` is a valid open file descriptor +#[inline] +#[must_use] +pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize { + #[cfg(target_arch = "x86_64")] + unsafe { + let ret: i64; + std::arch::asm!( + "syscall", + in("rax") 1i64, // SYS_write + in("rdi") fd, + in("rsi") buf, + in("rdx") count, + lateout("rax") ret, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as isize + } + } + #[cfg(target_arch = "aarch64")] + unsafe { + let ret: i64; + std::arch::asm!( + "svc #0", + in("x8") 64i64, // SYS_write + in("x0") fd, + in("x1") buf, + in("x2") count, + lateout("x0") ret, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as isize + } + } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + { + compile_error!("Unsupported architecture for inline assembly syscalls"); + } +} + /// Direct syscall to close a file descriptor /// /// # Safety @@ -169,6 +226,149 @@ pub unsafe fn sys_close(fd: i32) -> i32 { } } +/// Raw buffer for the `uname(2)` syscall. +/// +/// Linux ABI hasfive fields of `[i8; 65]`: sysname, nodename, release, version, +/// machine. The `domainname` field (GNU extension, `[i8; 65]`) follows but is +/// not used, nor any useful to us here. +#[repr(C)] +#[allow(dead_code)] +pub struct UtsNameBuf { + pub sysname: [i8; 65], + pub nodename: [i8; 65], + pub release: [i8; 65], + pub version: [i8; 65], + pub machine: [i8; 65], + pub domainname: [i8; 65], // GNU extension, included for correct struct size +} + +/// Direct `uname(2)` syscall +/// +/// # Returns +/// +/// 0 on success, negative on error +/// +/// # Safety +/// +/// The caller must ensure `buf` points to a valid `UtsNameBuf`. +#[inline] +#[allow(dead_code)] +pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 { + #[cfg(target_arch = "x86_64")] + unsafe { + let ret: i64; + std::arch::asm!( + "syscall", + in("rax") 63i64, // SYS_uname + in("rdi") buf, + lateout("rax") ret, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + #[cfg(target_arch = "aarch64")] + unsafe { + let ret: i64; + std::arch::asm!( + "svc #0", + in("x8") 160i64, // SYS_uname + in("x0") buf, + lateout("x0") ret, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + { + compile_error!("Unsupported architecture for inline assembly syscalls"); + } +} + +/// Raw buffer for the `statfs(2)` syscall. +/// +/// Linux ABI (`x86_64` and `aarch64`): the fields we use are at the same +/// offsets on both architectures. Only the fields needed for disk usage are +/// declared; the remainder of the 120-byte struct is covered by `_pad`. +#[repr(C)] +pub struct StatfsBuf { + pub f_type: i64, + pub f_bsize: i64, + pub f_blocks: u64, + pub f_bfree: u64, + pub f_bavail: u64, + pub f_files: u64, + pub f_ffree: u64, + pub f_fsid: [i32; 2], + pub f_namelen: i64, + pub f_frsize: i64, + pub f_flags: i64, + + #[allow(clippy::pub_underscore_fields, reason = "This is not a public API")] + pub _pad: [i64; 4], +} + +/// Direct `statfs(2)` syscall +/// +/// # Returns +/// +/// 0 on success, negative errno on error +/// +/// # Safety +/// +/// The caller must ensure that: +/// +/// - `path` points to a valid null-terminated string +/// - `buf` points to a valid `StatfsBuf` +#[inline] +pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { + #[cfg(target_arch = "x86_64")] + unsafe { + let ret: i64; + std::arch::asm!( + "syscall", + in("rax") 137i64, // SYS_statfs + in("rdi") path, + in("rsi") buf, + lateout("rax") ret, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + #[cfg(target_arch = "aarch64")] + unsafe { + let ret: i64; + std::arch::asm!( + "svc #0", + in("x8") 43i64, // SYS_statfs + in("x0") path, + in("x1") buf, + lateout("x0") ret, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + { + compile_error!("Unsupported architecture for inline assembly syscalls"); + } +} + /// Read entire file using direct syscalls. This avoids libc overhead and can be /// significantly faster for small files. /// From 99f7be5aac84c4982d807211f821867abfd8af01 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 17:02:26 +0300 Subject: [PATCH 04/31] microfetch-lib: use home-made syscall wrappers over `libc::uname` Signed-off-by: NotAShelf Change-Id: I162c771dce2714fa73db3e2e5dc8dcb36a6a6964 --- src/lib.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 1e0f9f3..b989c97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,42 +5,44 @@ pub mod syscall; pub mod system; pub mod uptime; -use std::mem::MaybeUninit; +use std::{ffi::CStr, mem::MaybeUninit}; -/// Wrapper for `libc::utsname` with safe accessor methods -pub struct UtsName(libc::utsname); +use crate::syscall::{UtsNameBuf, sys_uname}; + +/// Wrapper for `utsname` with safe accessor methods +pub struct UtsName(UtsNameBuf); impl UtsName { - /// Calls `uname` syscall and returns a `UtsName` wrapper + /// Calls `uname(2)` syscall and returns a `UtsName` wrapper /// /// # Errors /// /// Returns an error if the `uname` syscall fails pub fn uname() -> Result { let mut uts = MaybeUninit::uninit(); - if unsafe { libc::uname(uts.as_mut_ptr()) } != 0 { + if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { return Err(std::io::Error::last_os_error()); } Ok(Self(unsafe { uts.assume_init() })) } #[must_use] - pub const fn nodename(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.nodename.as_ptr()) } + pub const fn nodename(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.nodename.as_ptr()) } } #[must_use] - pub const fn sysname(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.sysname.as_ptr()) } + pub const fn sysname(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.sysname.as_ptr()) } } #[must_use] - pub const fn release(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.release.as_ptr()) } + pub const fn release(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.release.as_ptr()) } } #[must_use] - pub const fn machine(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.machine.as_ptr()) } + pub const fn machine(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.machine.as_ptr()) } } } From 17cd7530d61646e881b2523f5111ef422fa971f7 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 17:03:09 +0300 Subject: [PATCH 05/31] uptime: construct a raw buffer for the sysinfo syscall; drop `libc::sysinfo` Signed-off-by: NotAShelf Change-Id: I63887d9f73e6fed670e497086d30d2a46a6a6964 --- src/uptime.rs | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/uptime.rs b/src/uptime.rs index 095af7d..c6c4b26 100644 --- a/src/uptime.rs +++ b/src/uptime.rs @@ -17,14 +17,41 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { unsafe { std::str::from_utf8_unchecked(&buf[i..]) } } -/// Direct `sysinfo` syscall using inline assembly +/// Raw buffer for the `sysinfo(2)` syscall. +/// +/// In the Linux ABI `uptime` is a `long` at offset 0. The remaining fields are +/// not needed, but are declared to give the struct its correct size (112 bytes +/// on 64-bit Linux). +/// +/// The layout matches the kernel's `struct sysinfo` *exactly*: +/// `mem_unit` ends at offset 108, then 4 bytes of implicit padding to 112. +#[repr(C)] +struct SysInfo { + uptime: i64, + loads: [u64; 3], + totalram: u64, + freeram: u64, + sharedram: u64, + bufferram: u64, + totalswap: u64, + freeswap: u64, + procs: u16, + _pad: u16, + _pad2: u32, // alignment padding to reach 8-byte boundary for totalhigh + totalhigh: u64, + freehigh: u64, + mem_unit: u32, + // 4 bytes implicit trailing padding to reach 112 bytes total; no field + // needed +} + +/// Direct `sysinfo(2)` syscall using inline assembly /// /// # Safety /// -/// This function uses inline assembly to make a direct syscall. /// The caller must ensure the sysinfo pointer is valid. #[inline] -unsafe fn sys_sysinfo(info: *mut libc::sysinfo) -> i64 { +unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { #[cfg(target_arch = "x86_64")] { let ret: i64; @@ -59,7 +86,7 @@ unsafe fn sys_sysinfo(info: *mut libc::sysinfo) -> i64 { #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] { - unsafe { libc::sysinfo(info) as i64 } + compile_error!("Unsupported architecture for inline assembly syscalls"); } } From 99e033c41530ea1793bfdd4081ddeebd9fc92e82 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 17:05:09 +0300 Subject: [PATCH 06/31] system: use `std::env::var_os` over `libc` helpers Signed-off-by: NotAShelf Change-Id: I2747d4b065095314a1d454a204a251c96a6a6964 --- src/main.rs | 7 +++++-- src/system.rs | 45 ++++++++++++++++++++++----------------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/main.rs b/src/main.rs index caa58ec..1ee5d7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,12 +101,15 @@ fn print_system_info( let len = cursor.position() as usize; // Direct syscall to avoid stdout buffering allocation - let written = unsafe { libc::write(libc::STDOUT_FILENO, buf.as_ptr().cast(), len) }; + let written = unsafe { syscall::sys_write(1, buf.as_ptr(), len) }; if written < 0 { return Err(io::Error::last_os_error().into()); } if written as usize != len { - return Err(io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout").into()); + return Err( + io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout") + .into(), + ); } Ok(()) } diff --git a/src/system.rs b/src/system.rs index ba8fe79..e75daa8 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,18 +1,19 @@ -use std::{ffi::CStr, fmt::Write as _, io, mem::MaybeUninit}; +use std::{ffi::OsStr, fmt::Write as _, io, mem::MaybeUninit}; -use crate::{UtsName, colors::COLORS, syscall::read_file_fast}; +use crate::{ + UtsName, + colors::COLORS, + syscall::{StatfsBuf, read_file_fast, sys_statfs}, +}; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_username_and_hostname(utsname: &UtsName) -> String { - let username = unsafe { - let ptr = libc::getenv(c"USER".as_ptr()); - if ptr.is_null() { - "unknown_user" - } else { - CStr::from_ptr(ptr).to_str().unwrap_or("unknown_user") - } - }; + let username_os = std::env::var_os("USER"); + let username = username_os + .as_deref() + .and_then(OsStr::to_str) + .unwrap_or("unknown_user"); let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); let capacity = COLORS.yellow.len() @@ -38,16 +39,13 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_shell() -> String { - unsafe { - let ptr = libc::getenv(c"SHELL".as_ptr()); - if ptr.is_null() { - return "unknown_shell".into(); - } - - let bytes = CStr::from_ptr(ptr).to_bytes(); - let start = bytes.iter().rposition(|&b| b == b'/').map_or(0, |i| i + 1); - let name = std::str::from_utf8_unchecked(&bytes[start..]); - name.into() + let shell_os = std::env::var_os("SHELL"); + let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); + let start = shell.rfind('/').map_or(0, |i| i + 1); + if shell.is_empty() { + "unknown_shell".into() + } else { + shell[start..].into() } } @@ -59,15 +57,16 @@ pub fn get_shell() -> String { #[cfg_attr(feature = "hotpath", hotpath::measure)] #[allow(clippy::cast_precision_loss)] pub fn get_root_disk_usage() -> Result { - let mut vfs = MaybeUninit::uninit(); + let mut vfs = MaybeUninit::::uninit(); let path = b"/\0"; - if unsafe { libc::statvfs(path.as_ptr().cast(), vfs.as_mut_ptr()) } != 0 { + if unsafe { sys_statfs(path.as_ptr(), vfs.as_mut_ptr()) } != 0 { return Err(io::Error::last_os_error()); } let vfs = unsafe { vfs.assume_init() }; - let block_size = vfs.f_bsize; + #[allow(clippy::cast_sign_loss)] + let block_size = vfs.f_bsize as u64; let total_blocks = vfs.f_blocks; let available_blocks = vfs.f_bavail; From a798c5d3e88a42267c3206a79c13a72d08f2677a Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 17:06:51 +0300 Subject: [PATCH 07/31] chore: drop libc from crate dependencies Signed-off-by: NotAShelf Change-Id: I1a4110c5b79f7cbb3a7d964e3a5a80c66a6a6964 --- Cargo.lock | 1 - Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4deada2..48a6768 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,7 +571,6 @@ version = "0.4.13" dependencies = [ "criterion", "hotpath", - "libc", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6b1ae09..5db56fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ path = "src/main.rs" [dependencies] hotpath = { optional = true, version = "0.13.0" } -libc = "0.2.183" [dev-dependencies] criterion = "0.8.1" From 0c294d348bbd649e9bfe791a0f6a7855ecfef6bf Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 17:08:06 +0300 Subject: [PATCH 08/31] various: fix clippy lints Signed-off-by: NotAShelf Change-Id: Icbd2ba104c66907208cb37adf0c3915a6a6a6964 --- src/main.rs | 4 +++- src/system.rs | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1ee5d7c..ebc1e83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,12 +99,14 @@ fn print_system_info( {blue} ▟█▘ ▜█▖ {cyan}▝█▛ {cyan} {blue}Colors{reset}  {colors}\n\n" )?; - let len = cursor.position() as usize; + let len = + usize::try_from(cursor.position()).expect("cursor position fits usize"); // Direct syscall to avoid stdout buffering allocation let written = unsafe { syscall::sys_write(1, buf.as_ptr(), len) }; if written < 0 { return Err(io::Error::last_os_error().into()); } + #[allow(clippy::cast_sign_loss)] // non-negative verified by the guard above if written as usize != len { return Err( io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout") diff --git a/src/system.rs b/src/system.rs index e75daa8..9dd1ab3 100644 --- a/src/system.rs +++ b/src/system.rs @@ -157,12 +157,12 @@ pub fn get_memory_usage() -> Result { } #[allow(clippy::cast_precision_loss)] - let total_memory_gb = total_memory_kb as f64 / 1024.0 / 1024.0; + let total_gb = total_memory_kb as f64 / 1024.0 / 1024.0; #[allow(clippy::cast_precision_loss)] - let available_memory_gb = available_memory_kb as f64 / 1024.0 / 1024.0; - let used_memory_gb = total_memory_gb - available_memory_gb; + let available_gb = available_memory_kb as f64 / 1024.0 / 1024.0; + let used_memory_gb = total_gb - available_gb; - Ok((used_memory_gb, total_memory_gb)) + Ok((used_memory_gb, total_gb)) } let (used_memory, total_memory) = parse_memory_info()?; From 1dd02def4bbb10f495e94ee626ab7ee6f608ad60 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Thu, 26 Mar 2026 15:43:40 -0400 Subject: [PATCH 09/31] lib: fix aarch64 build by casting `UtsNameBuf` field pointers to `c_char` (#55) --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b989c97..ab21d03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,21 +28,21 @@ impl UtsName { #[must_use] pub const fn nodename(&self) -> &CStr { - unsafe { CStr::from_ptr(self.0.nodename.as_ptr()) } + unsafe { CStr::from_ptr(self.0.nodename.as_ptr().cast()) } } #[must_use] pub const fn sysname(&self) -> &CStr { - unsafe { CStr::from_ptr(self.0.sysname.as_ptr()) } + unsafe { CStr::from_ptr(self.0.sysname.as_ptr().cast()) } } #[must_use] pub const fn release(&self) -> &CStr { - unsafe { CStr::from_ptr(self.0.release.as_ptr()) } + unsafe { CStr::from_ptr(self.0.release.as_ptr().cast()) } } #[must_use] pub const fn machine(&self) -> &CStr { - unsafe { CStr::from_ptr(self.0.machine.as_ptr()) } + unsafe { CStr::from_ptr(self.0.machine.as_ptr().cast()) } } } From b4d3b9203a125c27a23a303b6cbedef78f344af6 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 17:26:47 +0300 Subject: [PATCH 10/31] microfetch: consume library exports in binary crate Signed-off-by: NotAShelf Change-Id: Id6c10efbb185fc44fcf22e6ab5c94a266a6a6964 --- src/main.rs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index ebc1e83..351e338 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,11 @@ -mod colors; -mod desktop; -mod release; -mod syscall; -mod system; -mod uptime; - use std::io::{self, Cursor, Write}; -pub use microfetch_lib::UtsName; - -use crate::{ - colors::print_dots, +use microfetch_lib::{ + UtsName, + colors::{COLORS, print_dots}, desktop::get_desktop_info, release::{get_os_pretty_name, get_system_info}, + syscall::sys_write, system::{ get_memory_usage, get_root_disk_usage, @@ -64,8 +57,6 @@ struct Fields { fn print_system_info( fields: &Fields, ) -> Result<(), Box> { - use crate::colors::COLORS; - let Fields { user_info, os_name, @@ -102,7 +93,7 @@ fn print_system_info( let len = usize::try_from(cursor.position()).expect("cursor position fits usize"); // Direct syscall to avoid stdout buffering allocation - let written = unsafe { syscall::sys_write(1, buf.as_ptr(), len) }; + let written = unsafe { sys_write(1, buf.as_ptr(), len) }; if written < 0 { return Err(io::Error::last_os_error().into()); } From 928452faba58a261f78e32c5e549dfe5ad8caf16 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Wed, 25 Mar 2026 17:40:37 +0300 Subject: [PATCH 11/31] chore: enable explicit ABI for rustfmt Signed-off-by: NotAShelf Change-Id: I8886d8c13a09231ad45f0a075e1308766a6a6964 --- .rustfmt.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.rustfmt.toml b/.rustfmt.toml index 9d5c77e..ec5369d 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -2,7 +2,7 @@ condense_wildcard_suffixes = true doc_comment_code_block_width = 80 edition = "2024" # Keep in sync with Cargo.toml. enum_discrim_align_threshold = 60 -force_explicit_abi = false +force_explicit_abi = true force_multiline_blocks = true format_code_in_doc_comments = true format_macro_matchers = true From 1408ca9f389bd13ef7e3a5e3d636fafad3d848f3 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 10:55:22 +0300 Subject: [PATCH 12/31] treewide: break into multiple crates Signed-off-by: NotAShelf Change-Id: Ifabe7af523ece1354b301b1a321659ee6a6a6964 --- .gitignore | 2 +- Cargo.lock | 426 +++++++------------ Cargo.toml | 45 +- crates/asm/Cargo.toml | 12 + src/syscall.rs => crates/asm/src/lib.rs | 29 +- crates/lib/Cargo.toml | 28 ++ {benches => crates/lib/benches}/benchmark.rs | 0 {src => crates/lib/src}/colors.rs | 0 {src => crates/lib/src}/desktop.rs | 0 src/main.rs => crates/lib/src/lib.rs | 126 ++++-- {src => crates/lib/src}/release.rs | 0 {src => crates/lib/src}/system.rs | 0 {src => crates/lib/src}/uptime.rs | 0 microfetch/Cargo.toml | 22 + microfetch/src/main.rs | 4 + src/lib.rs | 48 --- 16 files changed, 348 insertions(+), 394 deletions(-) create mode 100644 crates/asm/Cargo.toml rename src/syscall.rs => crates/asm/src/lib.rs (96%) create mode 100644 crates/lib/Cargo.toml rename {benches => crates/lib/benches}/benchmark.rs (100%) rename {src => crates/lib/src}/colors.rs (100%) rename {src => crates/lib/src}/desktop.rs (100%) rename src/main.rs => crates/lib/src/lib.rs (56%) rename {src => crates/lib/src}/release.rs (100%) rename {src => crates/lib/src}/system.rs (100%) rename {src => crates/lib/src}/uptime.rs (100%) create mode 100644 microfetch/Cargo.toml create mode 100644 microfetch/src/main.rs delete mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore index 8799178..3bcdd2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -/target +**/target result* /.direnv diff --git a/Cargo.lock b/Cargo.lock index 48a6768..64584ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -28,9 +28,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -43,15 +43,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -62,7 +62,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -73,14 +73,17 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] name = "arc-swap" -version = "1.7.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" +dependencies = [ + "rustversion", +] [[package]] name = "ascii" @@ -90,21 +93,21 @@ checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -120,9 +123,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -130,9 +133,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chunked_transfer" @@ -169,9 +172,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.35" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -179,9 +182,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.35" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -191,9 +194,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -203,30 +206,30 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys", ] [[package]] name = "criterion" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ "alloca", "anes", @@ -238,8 +241,6 @@ dependencies = [ "num-traits", "oorandom", "page_size", - "plotters", - "rayon", "regex", "serde", "serde_json", @@ -249,9 +250,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools", @@ -266,25 +267,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -293,9 +275,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "dirs-next" @@ -342,21 +324,21 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -365,36 +347,35 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-macro", "futures-sink", "futures-task", "pin-project-lite", - "pin-utils", "slab", ] [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -403,12 +384,13 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -491,7 +473,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -511,15 +493,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -539,19 +521,18 @@ checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ - "bitflags", "libc", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mach2" @@ -561,16 +542,28 @@ checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b" [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "microfetch" version = "0.4.13" +dependencies = [ + "microfetch-lib", +] + +[[package]] +name = "microfetch-asm" +version = "0.4.13" + +[[package]] +name = "microfetch-lib" +version = "0.4.13" dependencies = [ "criterion", "hotpath", + "microfetch-asm", ] [[package]] @@ -584,9 +577,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -612,43 +605,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "prettytable-rs" @@ -665,9 +624,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -689,9 +648,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -705,26 +664,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - [[package]] name = "redox_users" version = "0.4.6" @@ -738,9 +677,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -750,9 +689,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -761,21 +700,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "same-file" @@ -788,18 +721,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -808,14 +751,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -826,9 +770,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "strsim" @@ -838,9 +782,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -902,18 +846,18 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "pin-project-lite", ] [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-width" @@ -945,35 +889,22 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -981,31 +912,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -1029,11 +960,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys", ] [[package]] @@ -1048,15 +979,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -1067,65 +989,27 @@ dependencies = [ ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "zerocopy" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "zerocopy-derive", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "zerocopy-derive" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 5db56fc..2cc5650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,36 +1,19 @@ -[package] -name = "microfetch" -description = "Microscopic fetch tool in Rust, for NixOS systems, with special emphasis on speed" -version = "0.4.13" -edition = "2024" +[workspace] +members = [ "crates/*", "microfetch" ] +resolver = "3" + +[workspace.package] authors = [ "NotAShelf " ] -rust-version = "1.91.0" -readme = "./docs/README.md" -repository = "https://github.com/notashelf/microfetch" +edition = "2024" license = "GPL-3.0" +rust-version = "1.92.0" +version = "0.4.13" -[lib] -name = "microfetch_lib" -path = "src/lib.rs" - -[[bin]] -name = "microfetch" -path = "src/main.rs" - -[dependencies] -hotpath = { optional = true, version = "0.13.0" } - -[dev-dependencies] -criterion = "0.8.1" - -[features] -hotpath = [ "dep:hotpath", "hotpath/hotpath" ] -hotpath-alloc = [ "hotpath/hotpath-alloc" ] -hotpath-off = [ "hotpath/hotpath-off" ] +[workspace.dependencies] +microfetch-asm = { path = "./crates/asm" } +microfetch-lib = { path = "./crates/lib" } -[[bench]] -harness = false -name = "benchmark" +criterion = { default-features = false, features = [ "cargo_bench_support" ], version = "0.8.2" } [profile.dev] opt-level = 1 @@ -48,15 +31,13 @@ inherits = "release" split-debuginfo = "unpacked" strip = "none" -[lints.clippy] +[workspace.lints.clippy] complexity = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 } pedantic = { level = "warn", priority = -1 } perf = { level = "warn", priority = -1 } style = { level = "warn", priority = -1 } -# The lint groups above enable some less-than-desirable rules, we should manually -# enable those to keep our sanity. absolute_paths = "allow" arbitrary_source_item_ordering = "allow" implicit_return = "allow" diff --git a/crates/asm/Cargo.toml b/crates/asm/Cargo.toml new file mode 100644 index 0000000..24a9eac --- /dev/null +++ b/crates/asm/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "microfetch-asm" +description = "Inline assembly syscall helpers for microfetch" +version.workspace = true +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +publish = false +license.workspace = true + +[lints] +workspace = true diff --git a/src/syscall.rs b/crates/asm/src/lib.rs similarity index 96% rename from src/syscall.rs rename to crates/asm/src/lib.rs index cedc68a..1a3cd87 100644 --- a/src/syscall.rs +++ b/crates/asm/src/lib.rs @@ -70,13 +70,14 @@ pub unsafe fn sys_open(path: *const u8, flags: i32) -> i32 { /// Direct syscall to read from a file descriptor /// -/// # Returns n +/// # Returns /// /// Number of bytes read or -1 on error /// /// # Safety /// -/// The caller must ensure: +/// The caller must ensure that: +/// /// - `buf` points to a valid writable buffer of at least `count` bytes /// - `fd` is a valid open file descriptor #[inline] @@ -95,11 +96,13 @@ pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize { lateout("r11") _, options(nostack) ); + #[allow(clippy::cast_possible_truncation)] { ret as isize } } + #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; @@ -112,11 +115,13 @@ pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize { lateout("x0") ret, options(nostack) ); + #[allow(clippy::cast_possible_truncation)] { ret as isize } } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] { compile_error!("Unsupported architecture for inline assembly syscalls"); @@ -152,11 +157,13 @@ pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize { lateout("r11") _, options(nostack) ); + #[allow(clippy::cast_possible_truncation)] { ret as isize } } + #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; @@ -169,11 +176,13 @@ pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize { lateout("x0") ret, options(nostack) ); + #[allow(clippy::cast_possible_truncation)] { ret as isize } } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] { compile_error!("Unsupported architecture for inline assembly syscalls"); @@ -184,7 +193,7 @@ pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize { /// /// # Safety /// -/// The caller must ensure `fd` is a valid open file descriptor +/// The caller must ensure that `fd` is a valid open file descriptor #[inline] #[must_use] pub unsafe fn sys_close(fd: i32) -> i32 { @@ -205,6 +214,7 @@ pub unsafe fn sys_close(fd: i32) -> i32 { ret as i32 } } + #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; @@ -220,6 +230,7 @@ pub unsafe fn sys_close(fd: i32) -> i32 { ret as i32 } } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] { compile_error!("Unsupported architecture for inline assembly syscalls"); @@ -250,7 +261,7 @@ pub struct UtsNameBuf { /// /// # Safety /// -/// The caller must ensure `buf` points to a valid `UtsNameBuf`. +/// The caller must ensure that `buf` points to a valid `UtsNameBuf`. #[inline] #[allow(dead_code)] pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 { @@ -266,11 +277,13 @@ pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 { lateout("r11") _, options(nostack) ); + #[allow(clippy::cast_possible_truncation)] { ret as i32 } } + #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; @@ -286,6 +299,7 @@ pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 { ret as i32 } } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] { compile_error!("Unsupported architecture for inline assembly syscalls"); @@ -342,11 +356,13 @@ pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { lateout("r11") _, options(nostack) ); + #[allow(clippy::cast_possible_truncation)] { ret as i32 } } + #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; @@ -358,11 +374,13 @@ pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { lateout("x0") ret, options(nostack) ); + #[allow(clippy::cast_possible_truncation)] { ret as i32 } } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] { compile_error!("Unsupported architecture for inline assembly syscalls"); @@ -379,7 +397,8 @@ pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> io::Result { const O_RDONLY: i32 = 0; - // Use stack-allocated buffer for null-terminated path (max 256 bytes) + // We use stack-allocated buffer for null-terminated path. The maximum + // is 256 bytes. let path_bytes = path.as_bytes(); if path_bytes.len() >= 256 { return Err(io::Error::new(io::ErrorKind::InvalidInput, "Path too long")); diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml new file mode 100644 index 0000000..25e8e67 --- /dev/null +++ b/crates/lib/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "microfetch-lib" +description = "Microfetch library crate - exports all functionality including main function" +version.workspace = true +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[dependencies] +hotpath = { optional = true, version = "0.13.0" } +microfetch-asm.workspace = true + +[features] +hotpath = [ "dep:hotpath", "hotpath/hotpath" ] +hotpath-alloc = [ "hotpath/hotpath-alloc" ] +hotpath-off = [ "hotpath/hotpath-off" ] + +[dev-dependencies] +criterion.workspace = true + +[[bench]] +harness = false +name = "benchmark" + +[lints] +workspace = true diff --git a/benches/benchmark.rs b/crates/lib/benches/benchmark.rs similarity index 100% rename from benches/benchmark.rs rename to crates/lib/benches/benchmark.rs diff --git a/src/colors.rs b/crates/lib/src/colors.rs similarity index 100% rename from src/colors.rs rename to crates/lib/src/colors.rs diff --git a/src/desktop.rs b/crates/lib/src/desktop.rs similarity index 100% rename from src/desktop.rs rename to crates/lib/src/desktop.rs diff --git a/src/main.rs b/crates/lib/src/lib.rs similarity index 56% rename from src/main.rs rename to crates/lib/src/lib.rs index 351e338..7a2c9c6 100644 --- a/src/main.rs +++ b/crates/lib/src/lib.rs @@ -1,41 +1,64 @@ -use std::io::{self, Cursor, Write}; - -use microfetch_lib::{ - UtsName, - colors::{COLORS, print_dots}, - desktop::get_desktop_info, - release::{get_os_pretty_name, get_system_info}, - syscall::sys_write, - system::{ - get_memory_usage, - get_root_disk_usage, - get_shell, - get_username_and_hostname, - }, - uptime::get_current, +pub mod colors; +pub mod desktop; +pub mod release; +pub mod system; +pub mod uptime; + +use std::{ + ffi::CStr, + io::{self, Cursor, Write}, + mem::MaybeUninit, }; -#[cfg_attr(feature = "hotpath", hotpath::main)] -fn main() -> Result<(), Box> { - if Some("--version") == std::env::args().nth(1).as_deref() { - println!("Microfetch {}", env!("CARGO_PKG_VERSION")); - } else { - let utsname = UtsName::uname()?; - let fields = Fields { - user_info: get_username_and_hostname(&utsname), - os_name: get_os_pretty_name()?, - kernel_version: get_system_info(&utsname), - shell: get_shell(), - desktop: get_desktop_info(), - uptime: get_current()?, - memory_usage: get_memory_usage()?, - storage: get_root_disk_usage()?, - colors: print_dots(), - }; - print_system_info(&fields)?; +pub use microfetch_asm as syscall; +pub use microfetch_asm::{ + StatfsBuf, + UtsNameBuf, + read_file_fast, + sys_close, + sys_open, + sys_read, + sys_statfs, + sys_uname, + sys_write, +}; + +/// Wrapper for `utsname` with safe accessor methods +pub struct UtsName(UtsNameBuf); + +impl UtsName { + /// Calls `uname(2)` syscall and returns a `UtsName` wrapper + /// + /// # Errors + /// + /// Returns an error if the `uname` syscall fails + pub fn uname() -> Result { + let mut uts = MaybeUninit::uninit(); + if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(Self(unsafe { uts.assume_init() })) } - Ok(()) + #[must_use] + pub const fn nodename(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.nodename.as_ptr().cast()) } + } + + #[must_use] + pub const fn sysname(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.sysname.as_ptr().cast()) } + } + + #[must_use] + pub const fn release(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.release.as_ptr().cast()) } + } + + #[must_use] + pub const fn machine(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.machine.as_ptr().cast()) } + } } // Struct to hold all the fields we need in order to print the fetch. This @@ -69,9 +92,9 @@ fn print_system_info( colors, } = fields; - let cyan = COLORS.cyan; - let blue = COLORS.blue; - let reset = COLORS.reset; + let cyan = colors::COLORS.cyan; + let blue = colors::COLORS.blue; + let reset = colors::COLORS.reset; let mut buf = [0u8; 2048]; let mut cursor = Cursor::new(&mut buf[..]); @@ -106,3 +129,32 @@ fn print_system_info( } Ok(()) } + +/// Main entry point for microfetch - can be called by the binary crate +/// or by other consumers of the library +/// +/// # Errors +/// +/// Returns an error if any system call fails +#[cfg_attr(feature = "hotpath", hotpath::main)] +pub fn run() -> Result<(), Box> { + if Some("--version") == std::env::args().nth(1).as_deref() { + println!("Microfetch {}", env!("CARGO_PKG_VERSION")); + } else { + let utsname = UtsName::uname()?; + let fields = Fields { + user_info: system::get_username_and_hostname(&utsname), + os_name: release::get_os_pretty_name()?, + kernel_version: release::get_system_info(&utsname), + shell: system::get_shell(), + desktop: desktop::get_desktop_info(), + uptime: uptime::get_current()?, + memory_usage: system::get_memory_usage()?, + storage: system::get_root_disk_usage()?, + colors: colors::print_dots(), + }; + print_system_info(&fields)?; + } + + Ok(()) +} diff --git a/src/release.rs b/crates/lib/src/release.rs similarity index 100% rename from src/release.rs rename to crates/lib/src/release.rs diff --git a/src/system.rs b/crates/lib/src/system.rs similarity index 100% rename from src/system.rs rename to crates/lib/src/system.rs diff --git a/src/uptime.rs b/crates/lib/src/uptime.rs similarity index 100% rename from src/uptime.rs rename to crates/lib/src/uptime.rs diff --git a/microfetch/Cargo.toml b/microfetch/Cargo.toml new file mode 100644 index 0000000..ce1cbfb --- /dev/null +++ b/microfetch/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "microfetch" +description = "Microscopic fetch tool in Rust, for NixOS systems, with special emphasis on speed" +version.workspace = true +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +readme = "../docs/README.md" +repository = "https://github.com/notashelf/microfetch" +publish = false + +[dependencies] +microfetch-lib.workspace = true + +[features] +hotpath = [ "microfetch-lib/hotpath" ] +hotpath-alloc = [ "microfetch-lib/hotpath-alloc" ] +hotpath-off = [ "microfetch-lib/hotpath-off" ] + +[lints] +workspace = true diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs new file mode 100644 index 0000000..36e19da --- /dev/null +++ b/microfetch/src/main.rs @@ -0,0 +1,4 @@ +#[cfg_attr(feature = "hotpath", hotpath::main)] +fn main() -> Result<(), Box> { + microfetch_lib::run() +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index ab21d03..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,48 +0,0 @@ -pub mod colors; -pub mod desktop; -pub mod release; -pub mod syscall; -pub mod system; -pub mod uptime; - -use std::{ffi::CStr, mem::MaybeUninit}; - -use crate::syscall::{UtsNameBuf, sys_uname}; - -/// Wrapper for `utsname` with safe accessor methods -pub struct UtsName(UtsNameBuf); - -impl UtsName { - /// Calls `uname(2)` syscall and returns a `UtsName` wrapper - /// - /// # Errors - /// - /// Returns an error if the `uname` syscall fails - pub fn uname() -> Result { - let mut uts = MaybeUninit::uninit(); - if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { - return Err(std::io::Error::last_os_error()); - } - Ok(Self(unsafe { uts.assume_init() })) - } - - #[must_use] - pub const fn nodename(&self) -> &CStr { - unsafe { CStr::from_ptr(self.0.nodename.as_ptr().cast()) } - } - - #[must_use] - pub const fn sysname(&self) -> &CStr { - unsafe { CStr::from_ptr(self.0.sysname.as_ptr().cast()) } - } - - #[must_use] - pub const fn release(&self) -> &CStr { - unsafe { CStr::from_ptr(self.0.release.as_ptr().cast()) } - } - - #[must_use] - pub const fn machine(&self) -> &CStr { - unsafe { CStr::from_ptr(self.0.machine.as_ptr().cast()) } - } -} From 1c781aff562ce26dc4b8e0ffcb83f476598d31a4 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 13:49:26 +0300 Subject: [PATCH 13/31] chore: don't benchmark binary crate; centralize assembly helpers Signed-off-by: NotAShelf Change-Id: Ia24cb1647df93034937a8fcd6cad895c6a6a6964 --- Cargo.lock | 9 +- crates/asm/src/lib.rs | 210 +++++++++++++++++++++++++++++++++++---- crates/lib/Cargo.toml | 3 +- crates/lib/src/lib.rs | 2 + crates/lib/src/uptime.rs | 75 +------------- microfetch/Cargo.toml | 6 +- microfetch/src/main.rs | 1 - 7 files changed, 203 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 64584ba..1b9a1e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -417,9 +417,9 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hotpath" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51462b35dc551217e1d1dd3f8c5eebbb5df7103370b1e55d24c19a3411d290f7" +checksum = "2fde50be006a0fe95cc2fd6d25d884aa6932218e4055d7df2fa0d95c386acf8d" dependencies = [ "arc-swap", "cfg-if", @@ -444,9 +444,9 @@ dependencies = [ [[package]] name = "hotpath-macros" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f022365c90cc04455f17a2b9ba5fe0e661cde1ab27434d382f1f8693fe124e7d" +checksum = "dd884cee056e269e41e1127549458e1c4e309f31897ebbc1416982a74d40a5b5" dependencies = [ "proc-macro2", "quote", @@ -550,6 +550,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" name = "microfetch" version = "0.4.13" dependencies = [ + "hotpath", "microfetch-lib", ] diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 1a3cd87..a58fb96 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -5,11 +5,20 @@ //! What do you mean I wasted two whole hours to make the program only 100µs //! faster? //! -//! Supports `x86_64` and `aarch64` architectures. Riscv support will be -//! implemented when and ONLY WHEN I can be bothered to work on it. +//! Supports `x86_64`, `aarch64`, and `riscv64` architectures. use std::io; +// Ensure we're compiling for a supported architecture. +#[cfg(not(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "riscv64" +)))] +compile_error!( + "Unsupported architecture: only x86_64, aarch64, and riscv64 are supported" +); + /// Direct syscall to open a file /// /// # Returns @@ -62,9 +71,24 @@ pub unsafe fn sys_open(path: *const u8, flags: i32) -> i32 { fd as i32 } } - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); + #[cfg(target_arch = "riscv64")] + unsafe { + let fd: i64; + std::arch::asm!( + "ecall", + in("a7") 56i64, // SYS_openat + in("a0") -100i32, // AT_FDCWD + in("a1") path, + in("a2") flags, + in("a3") 0i32, // mode + lateout("a0") fd, + options(nostack) + ); + + #[allow(clippy::cast_possible_truncation)] + { + fd as i32 + } } } @@ -122,9 +146,23 @@ pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize { } } - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); + #[cfg(target_arch = "riscv64")] + unsafe { + let ret: i64; + std::arch::asm!( + "ecall", + in("a7") 63i64, // SYS_read + in("a0") fd, + in("a1") buf, + in("a2") count, + lateout("a0") ret, + options(nostack) + ); + + #[allow(clippy::cast_possible_truncation)] + { + ret as isize + } } } @@ -183,9 +221,23 @@ pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize { } } - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); + #[cfg(target_arch = "riscv64")] + unsafe { + let ret: i64; + std::arch::asm!( + "ecall", + in("a7") 64i64, // SYS_write + in("a0") fd, + in("a1") buf, + in("a2") count, + lateout("a0") ret, + options(nostack) + ); + + #[allow(clippy::cast_possible_truncation)] + { + ret as isize + } } } @@ -231,9 +283,20 @@ pub unsafe fn sys_close(fd: i32) -> i32 { } } - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); + #[cfg(target_arch = "riscv64")] + unsafe { + let ret: i64; + std::arch::asm!( + "ecall", + in("a7") 57i64, // SYS_close + in("a0") fd, + lateout("a0") ret, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } } } @@ -300,9 +363,21 @@ pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 { } } - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); + #[cfg(target_arch = "riscv64")] + unsafe { + let ret: i64; + std::arch::asm!( + "ecall", + in("a7") 160i64, // SYS_uname + in("a0") buf, + lateout("a0") ret, + options(nostack) + ); + + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } } } @@ -381,9 +456,22 @@ pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { } } - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); + #[cfg(target_arch = "riscv64")] + unsafe { + let ret: i64; + std::arch::asm!( + "ecall", + in("a7") 43i64, // SYS_statfs + in("a0") path, + in("a1") buf, + lateout("a0") ret, + options(nostack) + ); + + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } } } @@ -427,3 +515,85 @@ pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> io::Result { } } } + +/// Raw buffer for the `sysinfo(2)` syscall. +/// +/// In the Linux ABI `uptime` is a `long` at offset 0. The remaining fields are +/// not needed, but are declared to give the struct its correct size (112 bytes +/// on 64-bit Linux). +/// +/// The layout matches the kernel's `struct sysinfo` *exactly*: +/// `mem_unit` ends at offset 108, then 4 bytes of implicit padding to 112. +#[repr(C)] +pub struct SysInfo { + pub uptime: i64, + pub loads: [u64; 3], + pub totalram: u64, + pub freeram: u64, + pub sharedram: u64, + pub bufferram: u64, + pub totalswap: u64, + pub freeswap: u64, + pub procs: u16, + _pad: u16, + _pad2: u32, /* alignment padding to reach 8-byte boundary for + * totalhigh */ + pub totalhigh: u64, + pub freehigh: u64, + pub mem_unit: u32, + // 4 bytes implicit trailing padding to reach 112 bytes total; no field + // needed +} + +/// Direct `sysinfo(2)` syscall +/// +/// # Returns +/// +/// 0 on success, negative errno on error +/// +/// # Safety +/// +/// The caller must ensure that `info` points to a valid `SysInfo` buffer. +#[inline] +pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { + #[cfg(target_arch = "x86_64")] + unsafe { + let ret: i64; + std::arch::asm!( + "syscall", + in("rax") 99_i64, // __NR_sysinfo + in("rdi") info, + out("rcx") _, + out("r11") _, + lateout("rax") ret, + options(nostack) + ); + ret + } + + #[cfg(target_arch = "aarch64")] + unsafe { + let ret: i64; + std::arch::asm!( + "svc #0", + in("x8") 179_i64, // __NR_sysinfo + in("x0") info, + lateout("x0") ret, + options(nostack) + ); + ret + } + + #[cfg(target_arch = "riscv64")] + unsafe { + let ret: i64; + std::arch::asm!( + "ecall", + in("a7") 179_i64, // __NR_sysinfo + in("a0") info, + lateout("a0") ret, + options(nostack) + ); + ret + } +} diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 25e8e67..290932b 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -9,13 +9,12 @@ license.workspace = true publish = false [dependencies] -hotpath = { optional = true, version = "0.13.0" } +hotpath = { optional = true, version = "0.14.0" } microfetch-asm.workspace = true [features] hotpath = [ "dep:hotpath", "hotpath/hotpath" ] hotpath-alloc = [ "hotpath/hotpath-alloc" ] -hotpath-off = [ "hotpath/hotpath-off" ] [dev-dependencies] criterion.workspace = true diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 7a2c9c6..28147f7 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -13,12 +13,14 @@ use std::{ pub use microfetch_asm as syscall; pub use microfetch_asm::{ StatfsBuf, + SysInfo, UtsNameBuf, read_file_fast, sys_close, sys_open, sys_read, sys_statfs, + sys_sysinfo, sys_uname, sys_write, }; diff --git a/crates/lib/src/uptime.rs b/crates/lib/src/uptime.rs index c6c4b26..b529f53 100644 --- a/crates/lib/src/uptime.rs +++ b/crates/lib/src/uptime.rs @@ -1,5 +1,7 @@ use std::{io, mem::MaybeUninit}; +use crate::syscall::sys_sysinfo; + /// Faster integer to string conversion without the formatting overhead. #[inline] fn itoa(mut n: u64, buf: &mut [u8]) -> &str { @@ -17,79 +19,6 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { unsafe { std::str::from_utf8_unchecked(&buf[i..]) } } -/// Raw buffer for the `sysinfo(2)` syscall. -/// -/// In the Linux ABI `uptime` is a `long` at offset 0. The remaining fields are -/// not needed, but are declared to give the struct its correct size (112 bytes -/// on 64-bit Linux). -/// -/// The layout matches the kernel's `struct sysinfo` *exactly*: -/// `mem_unit` ends at offset 108, then 4 bytes of implicit padding to 112. -#[repr(C)] -struct SysInfo { - uptime: i64, - loads: [u64; 3], - totalram: u64, - freeram: u64, - sharedram: u64, - bufferram: u64, - totalswap: u64, - freeswap: u64, - procs: u16, - _pad: u16, - _pad2: u32, // alignment padding to reach 8-byte boundary for totalhigh - totalhigh: u64, - freehigh: u64, - mem_unit: u32, - // 4 bytes implicit trailing padding to reach 112 bytes total; no field - // needed -} - -/// Direct `sysinfo(2)` syscall using inline assembly -/// -/// # Safety -/// -/// The caller must ensure the sysinfo pointer is valid. -#[inline] -unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { - #[cfg(target_arch = "x86_64")] - { - let ret: i64; - unsafe { - std::arch::asm!( - "syscall", - in("rax") 99_i64, // __NR_sysinfo - in("rdi") info, - out("rcx") _, - out("r11") _, - lateout("rax") ret, - options(nostack) - ); - } - ret - } - - #[cfg(target_arch = "aarch64")] - { - let ret: i64; - unsafe { - std::arch::asm!( - "svc #0", - in("x8") 179_i64, // __NR_sysinfo - in("x0") info, - lateout("x0") ret, - options(nostack) - ); - } - ret - } - - #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] - { - compile_error!("Unsupported architecture for inline assembly syscalls"); - } -} - /// Gets the current system uptime. /// /// # Errors diff --git a/microfetch/Cargo.toml b/microfetch/Cargo.toml index ce1cbfb..4680aa8 100644 --- a/microfetch/Cargo.toml +++ b/microfetch/Cargo.toml @@ -11,12 +11,12 @@ repository = "https://github.com/notashelf/microfetch" publish = false [dependencies] +hotpath = { optional = true, version = "0.14.0" } microfetch-lib.workspace = true [features] -hotpath = [ "microfetch-lib/hotpath" ] -hotpath-alloc = [ "microfetch-lib/hotpath-alloc" ] -hotpath-off = [ "microfetch-lib/hotpath-off" ] +hotpath = [ "dep:hotpath" ] +hotpath-alloc = [ "hotpath/hotpath-alloc" ] [lints] workspace = true diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index 36e19da..8f22041 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -1,4 +1,3 @@ -#[cfg_attr(feature = "hotpath", hotpath::main)] fn main() -> Result<(), Box> { microfetch_lib::run() } From 0d4377ffcaf667ce248781f99ed2c7abced137fd Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 14:38:15 +0300 Subject: [PATCH 14/31] ci: use cargo-cross for multi-arch testing Signed-off-by: NotAShelf Change-Id: I6f5fac0de48162e114976da3aa5adf216a6a6964 --- .github/workflows/rust.yml | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5f435fb..7367640 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -12,15 +12,14 @@ env: jobs: test: name: Test on ${{ matrix.target }} - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: - include: - - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - - os: ubuntu-latest - target: aarch64-unknown-linux-gnu + target: + - x86_64-unknown-linux-gnu + - aarch64-unknown-linux-gnu + - riscv64gc-unknown-linux-gnu steps: - name: "Checkout" @@ -31,24 +30,12 @@ jobs: with: target: ${{ matrix.target }} - - name: "Install cross-compilation tools" - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - sudo apt-get update - sudo apt-get install -y gcc-aarch64-linux-gnu - - - name: "Configure linker for aarch64" - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - mkdir -p .cargo - cat >> .cargo/config.toml << EOF - [target.aarch64-unknown-linux-gnu] - linker = "aarch64-linux-gnu-gcc" - EOF + - name: "Install cross" + run: cargo install cross --git https://github.com/cross-rs/cross - name: "Build" - run: cargo build --verbose --target ${{ matrix.target }} + run: cross build --verbose --target ${{ matrix.target }} - name: "Run tests" if: matrix.target == 'x86_64-unknown-linux-gnu' - run: cargo test --verbose --target ${{ matrix.target }} + run: cross test --verbose --target ${{ matrix.target }} From 35d0f67812c62be053b5fe2b72c5fae1f11138c1 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 15:31:42 +0300 Subject: [PATCH 15/31] chore: use clang only on `x86_64-unknown-linux-gnu` I don't want to configure cargo-cross tbh Signed-off-by: NotAShelf Change-Id: I749bc02bbdf8922fdf7a5904508b8af66a6a6964 --- .cargo/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index b7efcee..e989948 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,4 @@ # https://github.com/rui314/mold?tab=readme-ov-file#how-to-use -[target.'cfg(target_os = "linux")'] +[target.x86_64-unknown-linux-gnu] linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=mold"] From 26c7a9f528d1f15cc78857b5e3855d897109b66f Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 15:44:28 +0300 Subject: [PATCH 16/31] ci: use hotpath-utils for benchmark workflows Signed-off-by: NotAShelf Change-Id: Iaae187e8320a3642c813ca2095a616056a6a6964 --- .github/workflows/hotpath-comment.yml | 50 +++++----------- .github/workflows/hotpath-profile.yml | 85 +++++++++++++-------------- 2 files changed, 56 insertions(+), 79 deletions(-) diff --git a/.github/workflows/hotpath-comment.yml b/.github/workflows/hotpath-comment.yml index 2fd3ca9..e1ed5df 100644 --- a/.github/workflows/hotpath-comment.yml +++ b/.github/workflows/hotpath-comment.yml @@ -14,49 +14,31 @@ jobs: comment: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} - steps: - - name: Checkout - uses: actions/checkout@v6 + - uses: actions/checkout@v4 + + - uses: actions-rust-lang/setup-rust-toolchain@v1 - - name: Download profiling results - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@v4 with: - name: hotpath-results - path: /tmp/ + name: profile-metrics + path: /tmp/metrics/ github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - - name: Setup Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Install hotpath CLI - run: cargo install hotpath - - - name: Post timing comparison comment - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - HEAD_METRICS=$(cat /tmp/head_timing.json) - BASE_METRICS=$(cat /tmp/base_timing.json) - PR_NUMBER=$(cat /tmp/pr_number.txt) - hotpath profile-pr \ - --head-metrics "$HEAD_METRICS" \ - --base-metrics "$BASE_METRICS" \ - --github-token "$GH_TOKEN" \ - --pr-number "$PR_NUMBER" + - name: Install hotpath-utils CLI + run: cargo install hotpath --bin hotpath-utils --features=utils - - name: Post allocation comparison comment + - name: Post PR comment env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail - HEAD_METRICS=$(cat /tmp/head_alloc.json) - BASE_METRICS=$(cat /tmp/base_alloc.json) - PR_NUMBER=$(cat /tmp/pr_number.txt) - hotpath profile-pr \ - --head-metrics "$HEAD_METRICS" \ - --base-metrics "$BASE_METRICS" \ + export GITHUB_BASE_REF=$(cat /tmp/metrics/base_ref.txt) + export GITHUB_HEAD_REF=$(cat /tmp/metrics/head_ref.txt) + hotpath-utils profile-pr \ + --head-metrics /tmp/metrics/head_timing.json \ + --base-metrics /tmp/metrics/base_timing.json \ --github-token "$GH_TOKEN" \ - --pr-number "$PR_NUMBER" + --pr-number "$(cat /tmp/metrics/pr_number.txt)" \ + --benchmark-id "timing" diff --git a/.github/workflows/hotpath-profile.yml b/.github/workflows/hotpath-profile.yml index 5fa1026..244fb2b 100644 --- a/.github/workflows/hotpath-profile.yml +++ b/.github/workflows/hotpath-profile.yml @@ -4,62 +4,57 @@ on: pull_request: branches: [ "main" ] -env: - CARGO_TERM_COLOR: always +permissions: + contents: read jobs: profile: runs-on: ubuntu-latest - steps: - - name: Checkout PR HEAD - uses: actions/checkout@v6 + - uses: actions/checkout@v4 with: fetch-depth: 0 - - - name: Setup Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Run timing profiling on HEAD + + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Create metrics directory + run: mkdir -p /tmp/metrics + + - name: Head benchmark (timing) env: - HOTPATH_JSON: "true" - run: | - cargo run --features='hotpath' 2>&1 | grep '^{"hotpath_profiling_mode"' > /tmp/head_timing.json - - - name: Run allocation profiling on HEAD + HOTPATH_OUTPUT_FORMAT: json + HOTPATH_OUTPUT_PATH: /tmp/metrics/head_timing.json + run: cargo run -p microfetch --release --features='hotpath' + + - name: Head benchmark (alloc) env: - HOTPATH_JSON: "true" - run: | - cargo run --features='hotpath,hotpath-alloc' 2>&1 | grep '^{"hotpath_profiling_mode"' > /tmp/head_alloc.json - - - name: Checkout base branch - run: | - git checkout ${{ github.event.pull_request.base.sha }} - - - name: Run timing profiling on base + HOTPATH_OUTPUT_FORMAT: json + HOTPATH_OUTPUT_PATH: /tmp/metrics/head_alloc.json + run: cargo run -p microfetch --release --features='hotpath,hotpath-alloc' + + - name: Checkout base + run: git checkout ${{ github.event.pull_request.base.sha }} + + - name: Base benchmark (timing) env: - HOTPATH_JSON: "true" - run: | - cargo run --features='hotpath' 2>&1 | grep '^{"hotpath_profiling_mode"' > /tmp/base_timing.json || echo '{}' > /tmp/base_timing.json - - - name: Run allocation profiling on base + HOTPATH_OUTPUT_FORMAT: json + HOTPATH_OUTPUT_PATH: /tmp/metrics/base_timing.json + run: cargo run -p microfetch --release --features='hotpath' || echo '{}' > /tmp/metrics/base_timing.json + + - name: Base benchmark (alloc) env: - HOTPATH_JSON: "true" - run: | - cargo run --features='hotpath,hotpath-alloc' 2>&1 | grep '^{"hotpath_profiling_mode"' > /tmp/base_alloc.json || echo '{}' > /tmp/base_alloc.json - - - name: Save PR number + HOTPATH_OUTPUT_FORMAT: json + HOTPATH_OUTPUT_PATH: /tmp/metrics/base_alloc.json + run: cargo run -p microfetch --release --features='hotpath,hotpath-alloc' || echo '{}' > /tmp/metrics/base_alloc.json + + - name: Save PR metadata run: | - echo '${{ github.event.pull_request.number }}' > /tmp/pr_number.txt - - - name: Upload profiling results - uses: actions/upload-artifact@v7 + echo '${{ github.event.pull_request.number }}' > /tmp/metrics/pr_number.txt + echo '${{ github.base_ref }}' > /tmp/metrics/base_ref.txt + echo '${{ github.head_ref }}' > /tmp/metrics/head_ref.txt + + - uses: actions/upload-artifact@v4 with: - name: hotpath-results - path: | - /tmp/head_timing.json - /tmp/head_alloc.json - /tmp/base_timing.json - /tmp/base_alloc.json - /tmp/pr_number.txt + name: profile-metrics + path: /tmp/metrics/ retention-days: 1 From d8da2218a4879a2c3591e46d496f2099f7785506 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 16:28:26 +0300 Subject: [PATCH 17/31] ci: use mold globally; drop clang linker Signed-off-by: NotAShelf Change-Id: Iadb9cd4ce33acdd2de585a740ac72da86a6a6964 --- .cargo/config.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index e989948..c7125dc 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,3 @@ # https://github.com/rui314/mold?tab=readme-ov-file#how-to-use -[target.x86_64-unknown-linux-gnu] -linker = "clang" -rustflags = ["-C", "link-arg=-fuse-ld=mold"] +[target.'cfg(target_os = "linux")'] +rustflags = [ "-C", "link-arg=-fuse-ld=mold" ] From 23e994a8fb4eb669d153aded0a49f5f77305eb84 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 16:29:47 +0300 Subject: [PATCH 18/31] ci: make Mold the default linker Signed-off-by: NotAShelf Change-Id: I36756896731a4df6903fcafea68246c76a6a6964 --- .github/workflows/rust.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7367640..deb45e3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,6 +30,9 @@ jobs: with: target: ${{ matrix.target }} + - name: "Make Mold the default linker" + uses: rui314/setup-mold@v1 + - name: "Install cross" run: cargo install cross --git https://github.com/cross-rs/cross From 77d9c725c7f123c165972f5aa8eb551f5af7154e Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 16:57:50 +0300 Subject: [PATCH 19/31] meta: move benchmarks to dedicated crate Signed-off-by: NotAShelf Change-Id: I753245e75da3a995c622a2c73f77bcb26a6a6964 --- Cargo.lock | 89 ++++++++++++++++++- Cargo.toml | 5 +- crates/benchmarks/Cargo.toml | 21 +++++ .../benches/microfetch.rs} | 0 crates/lib/Cargo.toml | 7 -- 5 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 crates/benchmarks/Cargo.toml rename crates/{lib/benches/benchmark.rs => benchmarks/benches/microfetch.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index 1b9a1e3..fa82609 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,8 @@ dependencies = [ "num-traits", "oorandom", "page_size", + "plotters", + "rayon", "regex", "serde", "serde_json", @@ -248,6 +250,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "criterion-cycles-per-byte" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5396de42a52e9e5d8f67ef0702dae30451f310a9ba1c3094dcf228f0be0e54bc" +dependencies = [ + "cfg-if", + "criterion", +] + [[package]] name = "criterion-plot" version = "0.8.2" @@ -267,6 +279,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -559,10 +590,18 @@ name = "microfetch-asm" version = "0.4.13" [[package]] -name = "microfetch-lib" +name = "microfetch-bench" version = "0.4.13" dependencies = [ "criterion", + "criterion-cycles-per-byte", + "microfetch-lib", +] + +[[package]] +name = "microfetch-lib" +version = "0.4.13" +dependencies = [ "hotpath", "microfetch-asm", ] @@ -610,6 +649,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "prettytable-rs" version = "0.10.0" @@ -665,6 +732,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_users" version = "0.4.6" diff --git a/Cargo.toml b/Cargo.toml index 2cc5650..f8bd2a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "crates/*", "microfetch" ] +members = [ "crates/*", "microfetch", "crates/benchmarks" ] resolver = "3" [workspace.package] @@ -13,7 +13,8 @@ version = "0.4.13" microfetch-asm = { path = "./crates/asm" } microfetch-lib = { path = "./crates/lib" } -criterion = { default-features = false, features = [ "cargo_bench_support" ], version = "0.8.2" } +criterion = { default-features = false, features = [ "cargo_bench_support" ], version = "0.8.2" } +criterion-cycles-per-byte = "0.8.0" [profile.dev] opt-level = 1 diff --git a/crates/benchmarks/Cargo.toml b/crates/benchmarks/Cargo.toml new file mode 100644 index 0000000..22fcd8e --- /dev/null +++ b/crates/benchmarks/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "microfetch-bench" +description = "Benchmarks for microfetch" +version.workspace = true +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[dependencies] +criterion.workspace = true +criterion-cycles-per-byte.workspace = true +microfetch-lib.workspace = true + +[[bench]] +harness = false +name = "microfetch" + +[lints] +workspace = true diff --git a/crates/lib/benches/benchmark.rs b/crates/benchmarks/benches/microfetch.rs similarity index 100% rename from crates/lib/benches/benchmark.rs rename to crates/benchmarks/benches/microfetch.rs diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 290932b..176502a 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -16,12 +16,5 @@ microfetch-asm.workspace = true hotpath = [ "dep:hotpath", "hotpath/hotpath" ] hotpath-alloc = [ "hotpath/hotpath-alloc" ] -[dev-dependencies] -criterion.workspace = true - -[[bench]] -harness = false -name = "benchmark" - [lints] workspace = true From 1b0e3070bbd34e96eeda98ff3d8a55d4090ee54f Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 17:23:58 +0300 Subject: [PATCH 20/31] crates: make assembly helpers no_std Signed-off-by: NotAShelf Change-Id: I1af24924f436b3f552b67f54bad2c02a6a6a6964 --- crates/asm/src/lib.rs | 55 ++++++++++++++++++++------------------- crates/lib/src/release.rs | 3 ++- crates/lib/src/system.rs | 3 ++- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index a58fb96..63014d4 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -7,7 +7,7 @@ //! //! Supports `x86_64`, `aarch64`, and `riscv64` architectures. -use std::io; +#![no_std] // Ensure we're compiling for a supported architecture. #[cfg(not(any( @@ -37,7 +37,7 @@ pub unsafe fn sys_open(path: *const u8, flags: i32) -> i32 { #[cfg(target_arch = "x86_64")] unsafe { let fd: i64; - std::arch::asm!( + core::arch::asm!( "syscall", in("rax") 2i64, // SYS_open in("rdi") path, @@ -56,7 +56,7 @@ pub unsafe fn sys_open(path: *const u8, flags: i32) -> i32 { #[cfg(target_arch = "aarch64")] unsafe { let fd: i64; - std::arch::asm!( + core::arch::asm!( "svc #0", in("x8") 56i64, // SYS_openat in("x0") -100i32, // AT_FDCWD @@ -74,7 +74,7 @@ pub unsafe fn sys_open(path: *const u8, flags: i32) -> i32 { #[cfg(target_arch = "riscv64")] unsafe { let fd: i64; - std::arch::asm!( + core::arch::asm!( "ecall", in("a7") 56i64, // SYS_openat in("a0") -100i32, // AT_FDCWD @@ -109,7 +109,7 @@ pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize { #[cfg(target_arch = "x86_64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "syscall", in("rax") 0i64, // SYS_read in("rdi") fd, @@ -130,7 +130,7 @@ pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize { #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "svc #0", in("x8") 63i64, // SYS_read in("x0") fd, @@ -149,7 +149,7 @@ pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize { #[cfg(target_arch = "riscv64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "ecall", in("a7") 63i64, // SYS_read in("a0") fd, @@ -184,7 +184,7 @@ pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize { #[cfg(target_arch = "x86_64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "syscall", in("rax") 1i64, // SYS_write in("rdi") fd, @@ -205,7 +205,7 @@ pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize { #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "svc #0", in("x8") 64i64, // SYS_write in("x0") fd, @@ -224,7 +224,7 @@ pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize { #[cfg(target_arch = "riscv64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "ecall", in("a7") 64i64, // SYS_write in("a0") fd, @@ -252,7 +252,7 @@ pub unsafe fn sys_close(fd: i32) -> i32 { #[cfg(target_arch = "x86_64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "syscall", in("rax") 3i64, // SYS_close in("rdi") fd, @@ -270,7 +270,7 @@ pub unsafe fn sys_close(fd: i32) -> i32 { #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "svc #0", in("x8") 57i64, // SYS_close in("x0") fd, @@ -286,7 +286,7 @@ pub unsafe fn sys_close(fd: i32) -> i32 { #[cfg(target_arch = "riscv64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "ecall", in("a7") 57i64, // SYS_close in("a0") fd, @@ -331,7 +331,7 @@ pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 { #[cfg(target_arch = "x86_64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "syscall", in("rax") 63i64, // SYS_uname in("rdi") buf, @@ -350,7 +350,7 @@ pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 { #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "svc #0", in("x8") 160i64, // SYS_uname in("x0") buf, @@ -366,7 +366,7 @@ pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 { #[cfg(target_arch = "riscv64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "ecall", in("a7") 160i64, // SYS_uname in("a0") buf, @@ -421,7 +421,7 @@ pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { #[cfg(target_arch = "x86_64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "syscall", in("rax") 137i64, // SYS_statfs in("rdi") path, @@ -441,7 +441,7 @@ pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "svc #0", in("x8") 43i64, // SYS_statfs in("x0") path, @@ -459,7 +459,7 @@ pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { #[cfg(target_arch = "riscv64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "ecall", in("a7") 43i64, // SYS_statfs in("a0") path, @@ -480,16 +480,17 @@ pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { /// /// # Errors /// -/// Returns an error if the file cannot be opened or read +/// Returns an error if the file cannot be opened or read. The error value is +/// the negated errno. #[inline] -pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> io::Result { +pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> Result { const O_RDONLY: i32 = 0; // We use stack-allocated buffer for null-terminated path. The maximum // is 256 bytes. let path_bytes = path.as_bytes(); if path_bytes.len() >= 256 { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Path too long")); + return Err(-22); // EINVAL } let mut path_buf = [0u8; 256]; @@ -499,14 +500,14 @@ pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> io::Result { unsafe { let fd = sys_open(path_buf.as_ptr(), O_RDONLY); if fd < 0 { - return Err(io::Error::last_os_error()); + return Err(fd); } let bytes_read = sys_read(fd, buffer.as_mut_ptr(), buffer.len()); let _ = sys_close(fd); if bytes_read < 0 { - return Err(io::Error::last_os_error()); + return Err(bytes_read as i32); } #[allow(clippy::cast_sign_loss)] @@ -559,7 +560,7 @@ pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { #[cfg(target_arch = "x86_64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "syscall", in("rax") 99_i64, // __NR_sysinfo in("rdi") info, @@ -574,7 +575,7 @@ pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { #[cfg(target_arch = "aarch64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "svc #0", in("x8") 179_i64, // __NR_sysinfo in("x0") info, @@ -587,7 +588,7 @@ pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { #[cfg(target_arch = "riscv64")] unsafe { let ret: i64; - std::arch::asm!( + core::arch::asm!( "ecall", in("a7") 179_i64, // __NR_sysinfo in("a0") info, diff --git a/crates/lib/src/release.rs b/crates/lib/src/release.rs index 8d3232e..41a55c3 100644 --- a/crates/lib/src/release.rs +++ b/crates/lib/src/release.rs @@ -30,7 +30,8 @@ pub fn get_os_pretty_name() -> Result { let mut buffer = [0u8; 1024]; // Use fast syscall-based file reading - let bytes_read = read_file_fast("/etc/os-release", &mut buffer)?; + let bytes_read = read_file_fast("/etc/os-release", &mut buffer) + .map_err(|e| io::Error::from_raw_os_error(-e))?; let content = &buffer[..bytes_read]; let mut offset = 0; diff --git a/crates/lib/src/system.rs b/crates/lib/src/system.rs index 9dd1ab3..53ab38a 100644 --- a/crates/lib/src/system.rs +++ b/crates/lib/src/system.rs @@ -117,7 +117,8 @@ pub fn get_memory_usage() -> Result { let mut buffer = [0u8; 1024]; // Use fast syscall-based file reading - let bytes_read = read_file_fast("/proc/meminfo", &mut buffer)?; + let bytes_read = read_file_fast("/proc/meminfo", &mut buffer) + .map_err(|e| io::Error::from_raw_os_error(-e))?; let meminfo = &buffer[..bytes_read]; // Fast scanning for MemTotal and MemAvailable From 472dbfc7e7545873dce9d826ec07283c2b655972 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 17:29:49 +0300 Subject: [PATCH 21/31] treewide: going no_std Signed-off-by: NotAShelf Change-Id: Ia1c001eb099ea8cae9bdf76642b873376a6a6964 --- .cargo/config.toml | 2 +- Cargo.lock | 6 + Cargo.toml | 2 + crates/alloc/Cargo.toml | 12 + crates/alloc/src/lib.rs | 106 +++++++++ crates/asm/src/lib.rs | 39 ++++ crates/lib/src/colors.rs | 68 ++++-- crates/lib/src/desktop.rs | 25 +- crates/lib/src/lib.rs | 477 +++++++++++++++++++++++++++++++++----- crates/lib/src/release.rs | 19 +- crates/lib/src/system.rs | 164 +++++++++---- crates/lib/src/uptime.rs | 12 +- microfetch/Cargo.toml | 6 +- microfetch/src/main.rs | 61 ++++- 14 files changed, 856 insertions(+), 143 deletions(-) create mode 100644 crates/alloc/Cargo.toml create mode 100644 crates/alloc/src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index c7125dc..c406f76 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ # https://github.com/rui314/mold?tab=readme-ov-file#how-to-use [target.'cfg(target_os = "linux")'] -rustflags = [ "-C", "link-arg=-fuse-ld=mold" ] +rustflags = [ "-C", "link-arg=-fuse-ld=mold", "-C", "link-arg=-lc", "-C", "link-arg=-lgcc_s" ] diff --git a/Cargo.lock b/Cargo.lock index fa82609..2f5d4bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -582,9 +582,15 @@ name = "microfetch" version = "0.4.13" dependencies = [ "hotpath", + "microfetch-alloc", + "microfetch-asm", "microfetch-lib", ] +[[package]] +name = "microfetch-alloc" +version = "0.4.13" + [[package]] name = "microfetch-asm" version = "0.4.13" diff --git a/Cargo.toml b/Cargo.toml index f8bd2a2..da548d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ rust-version = "1.92.0" version = "0.4.13" [workspace.dependencies] +microfetch-alloc = { path = "./crates/alloc" } microfetch-asm = { path = "./crates/asm" } microfetch-lib = { path = "./crates/lib" } @@ -18,6 +19,7 @@ criterion-cycles-per-byte = "0.8.0" [profile.dev] opt-level = 1 +panic = "abort" [profile.release] codegen-units = 1 diff --git a/crates/alloc/Cargo.toml b/crates/alloc/Cargo.toml new file mode 100644 index 0000000..47cd1d2 --- /dev/null +++ b/crates/alloc/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "microfetch-alloc" +description = "Simple, std-free bump allocator for Microfetch" +version.workspace = true +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/alloc/src/lib.rs b/crates/alloc/src/lib.rs new file mode 100644 index 0000000..06e3f07 --- /dev/null +++ b/crates/alloc/src/lib.rs @@ -0,0 +1,106 @@ +//! Simple bump allocator for `no_std` environments. Uses a statically allocated +//! 32KB buffer and provides O(1) allocation with no deallocation support +//! (memory is never freed). +#![no_std] +use core::{ + alloc::{GlobalAlloc, Layout}, + cell::UnsafeCell, + ptr::null_mut, +}; + +/// Default heap size is 32KB, should be plenty for Microfetch. Technically it +/// can be invoked with more (or less) depending on our needs but I am quite +/// sure 32KB is more than enough. +pub const DEFAULT_HEAP_SIZE: usize = 32 * 1024; + +/// A simple bump allocator that never frees memory. +/// +/// This allocator maintains a static buffer and a bump pointer. Allocations are +/// fast (just bump the pointer), but memory is never reclaimed. While you might +/// be inclined to point out that this is ugly, it's suitable for a short-lived +/// program with bounded memory usage. +pub struct BumpAllocator { + heap: UnsafeCell<[u8; N]>, + next: UnsafeCell, +} + +// SAFETY: BumpAllocator is thread-safe because it uses UnsafeCell +// and the allocator is only used in single-threaded contexts (i.e., no_std). +unsafe impl Sync for BumpAllocator {} + +impl BumpAllocator { + /// Creates a new bump allocator with the specified heap size. + #[must_use] + pub const fn new() -> Self { + Self { + heap: UnsafeCell::new([0; N]), + next: UnsafeCell::new(0), + } + } + + /// Returns the number of bytes currently allocated. + #[must_use] + pub fn used(&self) -> usize { + // SAFETY: We're just reading the value, and this is only called + // in single-threaded contexts. + unsafe { *self.next.get() } + } + + /// Returns the total heap size. + #[must_use] + pub const fn capacity(&self) -> usize { + N + } + + /// Returns the number of bytes remaining. + #[must_use] + pub fn remaining(&self) -> usize { + N - self.used() + } +} + +impl Default for BumpAllocator { + fn default() -> Self { + Self::new() + } +} + +unsafe impl GlobalAlloc for BumpAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + unsafe { + let next = self.next.get(); + let heap = self.heap.get(); + + // Align the current position + let align = layout.align(); + let start = (*next + align - 1) & !(align - 1); + let end = start + layout.size(); + + if end > N { + // Out of memory + null_mut() + } else { + *next = end; + (*heap).as_mut_ptr().add(start) + } + } + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + // Bump allocator doesn't support deallocation + // Memory is reclaimed when the program exits + } +} + +/// Static bump allocator instance with 32KB heap. +/// +/// # Example +/// +/// Use this with `#[global_allocator]` in your binary: +/// +/// +/// ```rust,ignore +/// #[global_allocator] +/// static ALLOCATOR: BumpAllocator = BumpAllocator::new(); +/// ``` +pub type BumpAlloc = BumpAllocator; diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 63014d4..87c76b2 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -507,6 +507,7 @@ pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> Result { let _ = sys_close(fd); if bytes_read < 0 { + #[allow(clippy::cast_possible_truncation)] return Err(bytes_read as i32); } @@ -598,3 +599,41 @@ pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { ret } } + +/// Direct syscall to exit the process +/// +/// # Safety +/// +/// This syscall never returns. The process will terminate immediately. +#[inline] +pub unsafe fn sys_exit(code: i32) -> ! { + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!( + "syscall", + in("rax") 60i64, // SYS_exit + in("rdi") code, + options(noreturn, nostack) + ); + } + + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!( + "svc #0", + in("x8") 93i64, // SYS_exit + in("x0") code, + options(noreturn, nostack) + ); + } + + #[cfg(target_arch = "riscv64")] + unsafe { + core::arch::asm!( + "ecall", + in("a7") 93i64, // SYS_exit + in("a0") code, + options(noreturn, nostack) + ); + } +} diff --git a/crates/lib/src/colors.rs b/crates/lib/src/colors.rs index 0fca89e..9f012f9 100644 --- a/crates/lib/src/colors.rs +++ b/crates/lib/src/colors.rs @@ -1,5 +1,6 @@ -use std::sync::LazyLock; +use alloc::string::String; +/// Color codes for terminal output pub struct Colors { pub reset: &'static str, pub blue: &'static str, @@ -11,7 +12,8 @@ pub struct Colors { } impl Colors { - const fn new(is_no_color: bool) -> Self { + #[must_use] + pub const fn new(is_no_color: bool) -> Self { if is_no_color { Self { reset: "", @@ -36,46 +38,68 @@ impl Colors { } } -pub static COLORS: LazyLock = LazyLock::new(|| { - // Only presence matters; value is irrelevant per the NO_COLOR spec - let is_no_color = std::env::var_os("NO_COLOR").is_some(); - Colors::new(is_no_color) -}); +use core::sync::atomic::{AtomicBool, Ordering}; + +// Check if NO_COLOR is set (only once, lazily) +// Only presence matters; value is irrelevant per the NO_COLOR spec +static NO_COLOR_CHECKED: AtomicBool = AtomicBool::new(false); +static NO_COLOR_SET: AtomicBool = AtomicBool::new(false); + +/// Checks if `NO_COLOR` environment variable is set. +pub(crate) fn is_no_color() -> bool { + // Fast path: already checked + if NO_COLOR_CHECKED.load(Ordering::Acquire) { + return NO_COLOR_SET.load(Ordering::Relaxed); + } + + // Slow path: check environment + let is_set = crate::env_exists("NO_COLOR"); + NO_COLOR_SET.store(is_set, Ordering::Relaxed); + NO_COLOR_CHECKED.store(true, Ordering::Release); + is_set +} #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn print_dots() -> String { - // Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color const GLYPH: &str = ""; - let capacity = COLORS.blue.len() - + COLORS.cyan.len() - + COLORS.green.len() - + COLORS.yellow.len() - + COLORS.red.len() - + COLORS.magenta.len() - + COLORS.reset.len() + + let colors = if is_no_color() { + Colors::new(true) + } else { + Colors::new(false) + }; + + // Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color + let capacity = colors.blue.len() + + colors.cyan.len() + + colors.green.len() + + colors.yellow.len() + + colors.red.len() + + colors.magenta.len() + + colors.reset.len() + (GLYPH.len() + 2) * 6; let mut result = String::with_capacity(capacity); - result.push_str(COLORS.blue); + result.push_str(colors.blue); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.cyan); + result.push_str(colors.cyan); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.green); + result.push_str(colors.green); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.yellow); + result.push_str(colors.yellow); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.red); + result.push_str(colors.red); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.magenta); + result.push_str(colors.magenta); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.reset); + result.push_str(colors.reset); result } diff --git a/crates/lib/src/desktop.rs b/crates/lib/src/desktop.rs index ea863b4..b9aa362 100644 --- a/crates/lib/src/desktop.rs +++ b/crates/lib/src/desktop.rs @@ -1,18 +1,15 @@ -use std::{ffi::OsStr, fmt::Write}; +use alloc::string::String; + +use crate::getenv_str; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_desktop_info() -> String { - let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP"); - let session_os = std::env::var_os("XDG_SESSION_TYPE"); + let desktop_raw = getenv_str("XDG_CURRENT_DESKTOP").unwrap_or("Unknown"); + let session_raw = getenv_str("XDG_SESSION_TYPE").unwrap_or(""); - let desktop_raw = desktop_os - .as_deref() - .and_then(OsStr::to_str) - .unwrap_or("Unknown"); let desktop_str = desktop_raw.strip_prefix("none+").unwrap_or(desktop_raw); - let session_raw = session_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); let backend_str = if session_raw.is_empty() { "Unknown" } else { @@ -27,9 +24,15 @@ pub fn get_desktop_info() -> String { result.push_str(" ("); // Capitalize first character of backend - if let Some(first_char) = backend_str.chars().next() { - let _ = write!(result, "{}", first_char.to_ascii_uppercase()); - result.push_str(&backend_str[first_char.len_utf8()..]); + if let Some(first_byte) = backend_str.as_bytes().first() { + // Convert first byte to uppercase if it's ASCII lowercase + let upper = if first_byte.is_ascii_lowercase() { + (first_byte - b'a' + b'A') as char + } else { + *first_byte as char + }; + result.push(upper); + result.push_str(&backend_str[1..]); } result.push(')'); diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 28147f7..7da92a0 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -1,13 +1,17 @@ +#![no_std] +extern crate alloc; + pub mod colors; pub mod desktop; pub mod release; pub mod system; pub mod uptime; -use std::{ +use alloc::string::String; +use core::{ ffi::CStr, - io::{self, Cursor, Write}, mem::MaybeUninit, + sync::atomic::{AtomicPtr, Ordering}, }; pub use microfetch_asm as syscall; @@ -25,6 +29,200 @@ pub use microfetch_asm::{ sys_write, }; +/// A simple error type for microfetch operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + /// An OS error occurred, containing the errno value. + OsError(i32), + /// Invalid data or encoding error. + InvalidData, + /// Not found. + NotFound, + /// Write operation failed or partial write. + WriteError, +} + +impl Error { + /// Creates an error from the last OS error (reads errno). + #[inline] + #[must_use] + pub const fn last_os_error() -> Self { + // This is a simplified version - in a real implementation, + // we'd need to get the actual errno from the syscall return + Self::OsError(0) + } + + /// Creates an error from a raw OS error code (negative errno from syscall). + #[inline] + #[must_use] + pub const fn from_raw_os_error(errno: i32) -> Self { + Self::OsError(-errno) + } +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::OsError(errno) => write!(f, "OS error: {errno}"), + Self::InvalidData => write!(f, "Invalid data"), + Self::NotFound => write!(f, "Not found"), + Self::WriteError => write!(f, "Write error"), + } + } +} + +// Simple OnceLock implementation for no_std +pub struct OnceLock { + ptr: AtomicPtr, +} + +impl Default for OnceLock { + fn default() -> Self { + Self::new() + } +} + +impl OnceLock { + #[must_use] + pub const fn new() -> Self { + Self { + ptr: AtomicPtr::new(core::ptr::null_mut()), + } + } + + pub fn get_or_init(&self, f: F) -> &T + where + F: FnOnce() -> T, + { + // Load the current pointer + let mut ptr = self.ptr.load(Ordering::Acquire); + + if ptr.is_null() { + // Need to initialize + let value = f(); + let boxed = alloc::boxed::Box::new(value); + let new_ptr = alloc::boxed::Box::into_raw(boxed); + + // Try to set the pointer + match self.ptr.compare_exchange( + core::ptr::null_mut(), + new_ptr, + Ordering::Release, + Ordering::Acquire, + ) { + Ok(_) => { + // We successfully set it + ptr = new_ptr; + }, + Err(existing) => { + // Someone else set it first, free our allocation + // SAFETY: We just allocated this and no one else has seen it + unsafe { + let _ = alloc::boxed::Box::from_raw(new_ptr); + } + ptr = existing; + }, + } + } + + // SAFETY: We know ptr is non-null and points to a valid T + unsafe { &*ptr } + } +} + +impl Drop for OnceLock { + fn drop(&mut self) { + let ptr = self.ptr.load(Ordering::Acquire); + if !ptr.is_null() { + // SAFETY: We know this was allocated via Box::into_raw + unsafe { + let _ = alloc::boxed::Box::from_raw(ptr); + } + } + } +} + +// Access to the environ pointer (provided by libc startup code) +unsafe extern "C" { + static environ: *const *const u8; +} + +/// Gets an environment variable by name (without using std). +/// +/// # Safety +/// +/// This function reads from the environ global which is initialized +/// by the C runtime before `main()` is called. +#[must_use] +pub fn getenv(name: &str) -> Option<&'static [u8]> { + // SAFETY: environ is set up by the C runtime before main() runs + // and remains valid for the lifetime of the program + let envp = unsafe { environ }; + if envp.is_null() { + return None; + } + + let name_bytes = name.as_bytes(); + + // Walk through environment variables + let mut i = 0; + loop { + // SAFETY: environ is null-terminated array of pointers + let entry = unsafe { *envp.add(i) }; + if entry.is_null() { + break; + } + + // Check if this entry starts with our variable name followed by '=' + let mut matches = true; + for (j, &b) in name_bytes.iter().enumerate() { + // SAFETY: entry is a valid C string + let entry_byte = unsafe { *entry.add(j) }; + if entry_byte != b { + matches = false; + break; + } + } + + if matches { + // Check for '=' after the name + // SAFETY: entry is a valid C string + let eq_byte = unsafe { *entry.add(name_bytes.len()) }; + if eq_byte == b'=' { + // Found it! Calculate the value length + let value_start = unsafe { entry.add(name_bytes.len() + 1) }; + let mut len = 0; + loop { + // SAFETY: entry is a valid C string + let b = unsafe { *value_start.add(len) }; + if b == 0 { + break; + } + len += 1; + } + // SAFETY: We calculated the exact length + return Some(unsafe { core::slice::from_raw_parts(value_start, len) }); + } + } + + i += 1; + } + + None +} + +/// Gets an environment variable as a UTF-8 string. +#[must_use] +pub fn getenv_str(name: &str) -> Option<&'static str> { + getenv(name).and_then(|bytes| core::str::from_utf8(bytes).ok()) +} + +/// Checks if an environment variable exists (regardless of its value). +#[must_use] +pub fn env_exists(name: &str) -> bool { + getenv(name).is_some() +} + /// Wrapper for `utsname` with safe accessor methods pub struct UtsName(UtsNameBuf); @@ -34,10 +232,10 @@ impl UtsName { /// # Errors /// /// Returns an error if the `uname` syscall fails - pub fn uname() -> Result { + pub fn uname() -> Result { let mut uts = MaybeUninit::uninit(); if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { - return Err(std::io::Error::last_os_error()); + return Err(Error::last_os_error()); } Ok(Self(unsafe { uts.assume_init() })) } @@ -79,9 +277,7 @@ struct Fields { } #[cfg_attr(feature = "hotpath", hotpath::measure)] -fn print_system_info( - fields: &Fields, -) -> Result<(), Box> { +fn print_system_info(fields: &Fields) -> Result<(), Error> { let Fields { user_info, os_name, @@ -94,69 +290,242 @@ fn print_system_info( colors, } = fields; - let cyan = colors::COLORS.cyan; - let blue = colors::COLORS.blue; - let reset = colors::COLORS.reset; + let no_color = colors::is_no_color(); + let colors_obj = colors::Colors::new(no_color); + let cyan = colors_obj.cyan; + let blue = colors_obj.blue; + let reset = colors_obj.reset; + // Build output string let mut buf = [0u8; 2048]; - let mut cursor = Cursor::new(&mut buf[..]); - - write!( - cursor, - " - {blue} ▟█▖ {cyan}▝█▙ ▗█▛ {user_info} ~{reset} - {blue} ▗▄▄▟██▄▄▄▄▄{cyan}▝█▙█▛ {blue}▖ {cyan} {blue}System{reset}  {os_name} - {blue} ▀▀▀▀▀▀▀▀▀▀▀▘{cyan}▝██ {blue}▟█▖ {cyan} {blue}Kernel{reset}  {kernel_version} - {cyan} ▟█▛ {cyan}▝█▘{blue}▟█▛ {cyan} {blue}Shell{reset}  {shell} - {cyan}▟█████▛ {blue}▟█████▛ {cyan} {blue}Uptime{reset}  {uptime} - {cyan} ▟█▛{blue}▗█▖ {blue}▟█▛ {cyan} {blue}Desktop{reset}  {desktop} - {cyan} ▝█▛ {blue}██▖{cyan}▗▄▄▄▄▄▄▄▄▄▄▄ {cyan}󰍛 {blue}Memory{reset}  {memory_usage} - {cyan} ▝ {blue}▟█▜█▖{cyan}▀▀▀▀▀██▛▀▀▘ {cyan}󱥎 {blue}Storage (/){reset}  {storage} - {blue} ▟█▘ ▜█▖ {cyan}▝█▛ {cyan} {blue}Colors{reset}  {colors}\n\n" - )?; - - let len = - usize::try_from(cursor.position()).expect("cursor position fits usize"); + let mut pos = 0usize; + + // Helper to write to buffer + let mut write_str = |s: &str| { + let bytes = s.as_bytes(); + let remaining = buf.len() - pos; + let to_write = bytes.len().min(remaining); + buf[pos..pos + to_write].copy_from_slice(&bytes[..to_write]); + pos += to_write; + }; + + write_str("\n "); + write_str(blue); + write_str(" ▟█▖ "); + write_str(cyan); + write_str("▝█▙ ▗█▛ "); + write_str(user_info); + write_str(" ~"); + write_str(reset); + write_str("\n"); + + write_str(" "); + write_str(blue); + write_str(" ▗▄▄▟██▄▄▄▄▄"); + write_str(cyan); + write_str("▝█▙█▛ "); + write_str(blue); + write_str("▖ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("System"); + write_str(reset); + write_str("  "); + write_str(os_name); + write_str("\n"); + + write_str(" "); + write_str(blue); + write_str(" ▀▀▀▀▀▀▀▀▀▀▀▘"); + write_str(cyan); + write_str("▝██ "); + write_str(blue); + write_str("▟█▖ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Kernel"); + write_str(reset); + write_str("  "); + write_str(kernel_version); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▟█▛ "); + write_str(cyan); + write_str("▝█▘"); + write_str(blue); + write_str("▟█▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Shell"); + write_str(reset); + write_str("  "); + write_str(shell); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str("▟█████▛ "); + write_str(blue); + write_str("▟█████▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Uptime"); + write_str(reset); + write_str("  "); + write_str(uptime); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▟█▛"); + write_str(blue); + write_str("▗█▖ "); + write_str(blue); + write_str("▟█▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Desktop"); + write_str(reset); + write_str("  "); + write_str(desktop); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▝█▛ "); + write_str(blue); + write_str("██▖"); + write_str(cyan); + write_str("▗▄▄▄▄▄▄▄▄▄▄▄ "); + write_str(cyan); + write_str("󰍛 "); + write_str(blue); + write_str("Memory"); + write_str(reset); + write_str("  "); + write_str(memory_usage); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▝ "); + write_str(blue); + write_str("▟█▜█▖"); + write_str(cyan); + write_str("▀▀▀▀▀██▛▀▀▘ "); + write_str(cyan); + write_str("󱥎 "); + write_str(blue); + write_str("Storage (/)"); + write_str(reset); + write_str("  "); + write_str(storage); + write_str("\n"); + + write_str(" "); + write_str(blue); + write_str(" ▟█▘ ▜█▖ "); + write_str(cyan); + write_str("▝█▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Colors"); + write_str(reset); + write_str("  "); + write_str(colors); + write_str("\n\n"); + // Direct syscall to avoid stdout buffering allocation - let written = unsafe { sys_write(1, buf.as_ptr(), len) }; + let written = unsafe { sys_write(1, buf.as_ptr(), pos) }; if written < 0 { - return Err(io::Error::last_os_error().into()); + #[allow(clippy::cast_possible_truncation)] + return Err(Error::OsError(written as i32)); } - #[allow(clippy::cast_sign_loss)] // non-negative verified by the guard above - if written as usize != len { - return Err( - io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout") - .into(), - ); + #[allow(clippy::cast_sign_loss)] + if written as usize != pos { + return Err(Error::WriteError); } Ok(()) } +/// Print version information using direct syscall. +fn print_version() { + const VERSION: &str = concat!("Microfetch ", env!("CARGO_PKG_VERSION"), "\n"); + unsafe { + let _ = sys_write(1, VERSION.as_ptr(), VERSION.len()); + } +} + +/// Check if --version was passed via argc/argv. +/// +/// # Safety +/// +/// This function must be called with valid argc and argv from the program entry +/// point. +unsafe fn check_version_flag(argc: i32, argv: *const *const u8) -> bool { + if argc < 2 { + return false; + } + // SAFETY: argv is a valid array of argc pointers + let arg1 = unsafe { *argv.add(1) }; + if arg1.is_null() { + return false; + } + // Check if arg1 is "--version" + let version_flag = b"--version\0"; + for (i, &b) in version_flag.iter().enumerate() { + // SAFETY: arg1 is a valid C string + let arg_byte = unsafe { *arg1.add(i) }; + if arg_byte != b { + return false; + } + } + true +} + /// Main entry point for microfetch - can be called by the binary crate -/// or by other consumers of the library +/// or by other consumers of the library. +/// +/// # Arguments +/// +/// * `argc` - Argument count from main +/// * `argv` - Argument vector from main /// /// # Errors /// /// Returns an error if any system call fails +/// +/// # Safety +/// +/// argv must be a valid null-terminated array of C strings. #[cfg_attr(feature = "hotpath", hotpath::main)] -pub fn run() -> Result<(), Box> { - if Some("--version") == std::env::args().nth(1).as_deref() { - println!("Microfetch {}", env!("CARGO_PKG_VERSION")); - } else { - let utsname = UtsName::uname()?; - let fields = Fields { - user_info: system::get_username_and_hostname(&utsname), - os_name: release::get_os_pretty_name()?, - kernel_version: release::get_system_info(&utsname), - shell: system::get_shell(), - desktop: desktop::get_desktop_info(), - uptime: uptime::get_current()?, - memory_usage: system::get_memory_usage()?, - storage: system::get_root_disk_usage()?, - colors: colors::print_dots(), - }; - print_system_info(&fields)?; +pub unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> { + if unsafe { check_version_flag(argc, argv) } { + print_version(); + return Ok(()); } + let utsname = UtsName::uname()?; + let fields = Fields { + user_info: system::get_username_and_hostname(&utsname), + os_name: release::get_os_pretty_name()?, + kernel_version: release::get_system_info(&utsname), + shell: system::get_shell(), + desktop: desktop::get_desktop_info(), + uptime: uptime::get_current()?, + memory_usage: system::get_memory_usage()?, + storage: system::get_root_disk_usage()?, + colors: colors::print_dots(), + }; + print_system_info(&fields)?; + Ok(()) } diff --git a/crates/lib/src/release.rs b/crates/lib/src/release.rs index 41a55c3..299c1c9 100644 --- a/crates/lib/src/release.rs +++ b/crates/lib/src/release.rs @@ -1,6 +1,6 @@ -use std::{fmt::Write as _, io}; +use alloc::string::String; -use crate::{UtsName, syscall::read_file_fast}; +use crate::{Error, UtsName, syscall::read_file_fast}; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] @@ -13,7 +13,14 @@ pub fn get_system_info(utsname: &UtsName) -> String { let capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1; let mut result = String::with_capacity(capacity); - write!(result, "{sysname} {release} ({machine})").unwrap(); + // Manual string construction instead of write! macro + result.push_str(sysname); + result.push(' '); + result.push_str(release); + result.push_str(" ("); + result.push_str(machine); + result.push(')'); + result } @@ -23,7 +30,7 @@ pub fn get_system_info(utsname: &UtsName) -> String { /// /// Returns an error if `/etc/os-release` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_os_pretty_name() -> Result { +pub fn get_os_pretty_name() -> Result { // Fast byte-level scanning for PRETTY_NAME= const PREFIX: &[u8] = b"PRETTY_NAME="; @@ -31,7 +38,7 @@ pub fn get_os_pretty_name() -> Result { // Use fast syscall-based file reading let bytes_read = read_file_fast("/etc/os-release", &mut buffer) - .map_err(|e| io::Error::from_raw_os_error(-e))?; + .map_err(Error::from_raw_os_error)?; let content = &buffer[..bytes_read]; let mut offset = 0; @@ -66,5 +73,5 @@ pub fn get_os_pretty_name() -> Result { offset += line_end + 1; } - Ok("Unknown".to_owned()) + Ok(String::from("Unknown")) } diff --git a/crates/lib/src/system.rs b/crates/lib/src/system.rs index 53ab38a..b902dac 100644 --- a/crates/lib/src/system.rs +++ b/crates/lib/src/system.rs @@ -1,37 +1,39 @@ -use std::{ffi::OsStr, fmt::Write as _, io, mem::MaybeUninit}; +use alloc::string::String; +use core::mem::MaybeUninit; use crate::{ + Error, UtsName, - colors::COLORS, + colors::Colors, syscall::{StatfsBuf, read_file_fast, sys_statfs}, }; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_username_and_hostname(utsname: &UtsName) -> String { - let username_os = std::env::var_os("USER"); - let username = username_os - .as_deref() - .and_then(OsStr::to_str) - .unwrap_or("unknown_user"); + let username = crate::getenv_str("USER").unwrap_or("unknown_user"); let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); - let capacity = COLORS.yellow.len() + // Get colors (checking NO_COLOR only once) + let no_color = crate::colors::is_no_color(); + let colors = Colors::new(no_color); + + let capacity = colors.yellow.len() + username.len() - + COLORS.red.len() + + colors.red.len() + 1 - + COLORS.green.len() + + colors.green.len() + hostname.len() - + COLORS.reset.len(); + + colors.reset.len(); let mut result = String::with_capacity(capacity); - result.push_str(COLORS.yellow); + result.push_str(colors.yellow); result.push_str(username); - result.push_str(COLORS.red); + result.push_str(colors.red); result.push('@'); - result.push_str(COLORS.green); + result.push_str(colors.green); result.push_str(hostname); - result.push_str(COLORS.reset); + result.push_str(colors.reset); result } @@ -39,13 +41,12 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_shell() -> String { - let shell_os = std::env::var_os("SHELL"); - let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); + let shell = crate::getenv_str("SHELL").unwrap_or(""); let start = shell.rfind('/').map_or(0, |i| i + 1); if shell.is_empty() { - "unknown_shell".into() + String::from("unknown_shell") } else { - shell[start..].into() + String::from(&shell[start..]) } } @@ -56,12 +57,12 @@ pub fn get_shell() -> String { /// Returns an error if the filesystem information cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] #[allow(clippy::cast_precision_loss)] -pub fn get_root_disk_usage() -> Result { +pub fn get_root_disk_usage() -> Result { let mut vfs = MaybeUninit::::uninit(); let path = b"/\0"; if unsafe { sys_statfs(path.as_ptr(), vfs.as_mut_ptr()) } != 0 { - return Err(io::Error::last_os_error()); + return Err(Error::last_os_error()); } let vfs = unsafe { vfs.assume_init() }; @@ -77,18 +78,96 @@ pub fn get_root_disk_usage() -> Result { let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0); let usage = (used_size / total_size) * 100.0; + let no_color = crate::colors::is_no_color(); + let colors = Colors::new(no_color); + let mut result = String::with_capacity(64); - write!( - result, - "{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})", - cyan = COLORS.cyan, - reset = COLORS.reset, - ) - .unwrap(); + + // Manual float formatting + write_float(&mut result, used_size, 2); + result.push_str(" GiB / "); + write_float(&mut result, total_size, 2); + result.push_str(" GiB ("); + result.push_str(colors.cyan); + write_float(&mut result, usage, 0); + result.push('%'); + result.push_str(colors.reset); + result.push(')'); Ok(result) } +/// Write a float to string with specified decimal places +#[allow( + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::cast_precision_loss +)] +fn write_float(s: &mut String, val: f64, decimals: u32) { + // Handle integer part + let int_part = val as u64; + write_u64(s, int_part); + + if decimals > 0 { + s.push('.'); + + // Calculate fractional part + let mut frac = val - int_part as f64; + for _ in 0..decimals { + frac *= 10.0; + let digit = frac as u8; + s.push((b'0' + digit) as char); + frac -= f64::from(digit); + } + } +} + +/// Round an f64 to nearest integer (`f64::round` is not in core) +#[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss +)] +fn round_f64(x: f64) -> f64 { + if x >= 0.0 { + let int_part = x as u64 as f64; + let frac = x - int_part; + if frac >= 0.5 { + int_part + 1.0 + } else { + int_part + } + } else { + let int_part = (-x) as u64 as f64; + let frac = -x - int_part; + if frac >= 0.5 { + -(int_part + 1.0) + } else { + -int_part + } + } +} + +/// Write a u64 to string +fn write_u64(s: &mut String, mut n: u64) { + if n == 0 { + s.push('0'); + return; + } + + let mut buf = [0u8; 20]; + let mut i = 20; + + while n > 0 { + i -= 1; + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + } + + // SAFETY: buf contains only ASCII digits + s.push_str(unsafe { core::str::from_utf8_unchecked(&buf[i..]) }); +} + /// Fast integer parsing without stdlib overhead #[inline] fn parse_u64_fast(s: &[u8]) -> u64 { @@ -109,16 +188,16 @@ fn parse_u64_fast(s: &[u8]) -> u64 { /// /// Returns an error if `/proc/meminfo` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_memory_usage() -> Result { +pub fn get_memory_usage() -> Result { #[cfg_attr(feature = "hotpath", hotpath::measure)] - fn parse_memory_info() -> Result<(f64, f64), io::Error> { + fn parse_memory_info() -> Result<(f64, f64), Error> { let mut total_memory_kb = 0u64; let mut available_memory_kb = 0u64; let mut buffer = [0u8; 1024]; // Use fast syscall-based file reading let bytes_read = read_file_fast("/proc/meminfo", &mut buffer) - .map_err(|e| io::Error::from_raw_os_error(-e))?; + .map_err(Error::from_raw_os_error)?; let meminfo = &buffer[..bytes_read]; // Fast scanning for MemTotal and MemAvailable @@ -168,17 +247,22 @@ pub fn get_memory_usage() -> Result { let (used_memory, total_memory) = parse_memory_info()?; #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let percentage_used = (used_memory / total_memory * 100.0).round() as u64; + let percentage_used = round_f64(used_memory / total_memory * 100.0) as u64; + + let no_color = crate::colors::is_no_color(); + let colors = Colors::new(no_color); let mut result = String::with_capacity(64); - write!( - result, - "{used_memory:.2} GiB / {total_memory:.2} GiB \ - ({cyan}{percentage_used}%{reset})", - cyan = COLORS.cyan, - reset = COLORS.reset, - ) - .unwrap(); + + write_float(&mut result, used_memory, 2); + result.push_str(" GiB / "); + write_float(&mut result, total_memory, 2); + result.push_str(" GiB ("); + result.push_str(colors.cyan); + write_u64(&mut result, percentage_used); + result.push('%'); + result.push_str(colors.reset); + result.push(')'); Ok(result) } diff --git a/crates/lib/src/uptime.rs b/crates/lib/src/uptime.rs index b529f53..4ed2200 100644 --- a/crates/lib/src/uptime.rs +++ b/crates/lib/src/uptime.rs @@ -1,6 +1,7 @@ -use std::{io, mem::MaybeUninit}; +use alloc::string::String; +use core::mem::MaybeUninit; -use crate::syscall::sys_sysinfo; +use crate::{Error, syscall::sys_sysinfo}; /// Faster integer to string conversion without the formatting overhead. #[inline] @@ -16,7 +17,8 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { n /= 10; } - unsafe { std::str::from_utf8_unchecked(&buf[i..]) } + // SAFETY: We only wrote ASCII digits + unsafe { core::str::from_utf8_unchecked(&buf[i..]) } } /// Gets the current system uptime. @@ -25,11 +27,11 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { /// /// Returns an error if the system uptime cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_current() -> Result { +pub fn get_current() -> Result { let uptime_seconds = { let mut info = MaybeUninit::uninit(); if unsafe { sys_sysinfo(info.as_mut_ptr()) } != 0 { - return Err(io::Error::last_os_error()); + return Err(Error::last_os_error()); } #[allow(clippy::cast_sign_loss)] unsafe { diff --git a/microfetch/Cargo.toml b/microfetch/Cargo.toml index 4680aa8..b29b420 100644 --- a/microfetch/Cargo.toml +++ b/microfetch/Cargo.toml @@ -11,12 +11,14 @@ repository = "https://github.com/notashelf/microfetch" publish = false [dependencies] -hotpath = { optional = true, version = "0.14.0" } +hotpath = { optional = true, version = "0.14.0" } +microfetch-alloc.workspace = true microfetch-lib.workspace = true +microfetch-asm.workspace = true [features] hotpath = [ "dep:hotpath" ] hotpath-alloc = [ "hotpath/hotpath-alloc" ] [lints] -workspace = true +workspace = true \ No newline at end of file diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index 8f22041..0bdc022 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -1,3 +1,60 @@ -fn main() -> Result<(), Box> { - microfetch_lib::run() +#![no_std] +#![no_main] + +extern crate alloc; + +use microfetch_alloc::BumpAlloc; +use microfetch_asm::sys_write; +#[cfg(not(test))] +use {core::panic::PanicInfo, microfetch_asm::sys_exit}; + +#[global_allocator] +static ALLOCATOR: BumpAlloc = BumpAlloc::new(); + +/// Receives argc and argv directly. The C runtime will call this after +/// initializing the environment. Cool right? +/// +/// # Safety +/// +/// argv must be a valid pointer to an array of argc C strings. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32 { + // SAFETY: argc and argv are provided by the C runtime and are valid + unsafe { + match microfetch_lib::run(argc, argv) { + Ok(()) => 0, + Err(e) => { + // Print error message to stderr (fd 2) + let msg = alloc::format!("Error: {e}\n"); + let _ = sys_write(2, msg.as_ptr(), msg.len()); + 1 + }, + } + } +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + // Write "panic" to stderr and exit + const PANIC_MSG: &[u8] = b"panic\n"; + unsafe { + let _ = sys_write(2, PANIC_MSG.as_ptr(), PANIC_MSG.len()); + sys_exit(1) + } +} + +// FIXME: Stubs for Rust exception handling symbols needed when using alloc with +// panic=abort These are normally provided by the unwinding runtime, but we're +// using panic=abort. I don't actually think this is the correct approach, but I +// cannot think of anything better. + +#[cfg(not(test))] +#[unsafe(no_mangle)] +const extern "C" fn rust_eh_personality() {} + +#[cfg(not(test))] +#[unsafe(no_mangle)] +extern "C" fn _Unwind_Resume() -> ! { + unsafe { sys_exit(1) } } From 0f5fc124da127c1a55cdd510d681e0a1acdc7117 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 23:06:42 +0300 Subject: [PATCH 22/31] crates/asm: replicate some libc runtime symbols manually, unsafely Signed-off-by: NotAShelf Change-Id: I6df95b1f0a9f117c8e108d8755389a4d6a6a6964 --- crates/asm/src/lib.rs | 242 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 87c76b2..1589a1d 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -19,6 +19,248 @@ compile_error!( "Unsupported architecture: only x86_64, aarch64, and riscv64 are supported" ); +/// Copies `n` bytes from `src` to `dest`. +/// +/// # Safety +/// +/// `dest` and `src` must be valid pointers to non-overlapping regions of +/// memory of at least `n` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memcpy( + dest: *mut u8, + src: *const u8, + n: usize, +) -> *mut u8 { + for i in 0..n { + unsafe { + *dest.add(i) = *src.add(i); + } + } + dest +} + +/// Fills memory region with a byte value. +/// +/// # Safety +/// +/// `s` must be a valid pointer to memory of at least `n` bytes. +/// The value in `c` is treated as unsigned (lower 8 bits used). +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memset(s: *mut u8, c: i32, n: usize) -> *mut u8 { + for i in 0..n { + unsafe { + *s.add(i) = u8::try_from(c).unwrap_or(0); + } + } + s +} + +/// Calculates the length of a null-terminated string. +/// +/// # Safety +/// +/// `s` must be a valid pointer to a null-terminated string. +#[unsafe(no_mangle)] +pub const unsafe extern "C" fn strlen(s: *const u8) -> usize { + let mut len = 0; + while unsafe { *s.add(len) } != 0 { + len += 1; + } + len +} + +/// Function pointer type for the main application entry point. +/// The function receives argc and argv and should return an exit code. +pub type MainFn = unsafe extern "C" fn(i32, *const *const u8) -> i32; + +static mut MAIN_FN: Option = None; + +/// Register the main function to be called from the entry point. +/// This must be called before the program starts (e.g., in a constructor). +pub fn register_main(main_fn: MainFn) { + unsafe { + MAIN_FN = Some(main_fn); + } +} + +/// Rust entry point called from `_start` assembly. +/// +/// The `stack` pointer points to: +/// `[rsp]` = argc +/// `[rsp+8]` = argv[0] +/// etc. +/// +/// # Safety +/// +/// The `stack` pointer must point to valid stack memory set up by the kernel +/// AND the binary must define a `main` function with the following signature: +/// +/// ```rust,ignore +/// unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32` +/// ``` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn entry_rust(stack: *const usize) -> i32 { + // Read argc and argv from stack + let argc = unsafe { *stack }; + let argv = unsafe { stack.add(1).cast::<*const u8>() }; + + // SAFETY: argc is unlikely to exceed i32::MAX on real systems + let argc_i32 = i32::try_from(argc).unwrap_or(i32::MAX); + + // Call the main function (defined by the binary crate) + unsafe { main(argc_i32, argv) } +} + +// External main function that must be defined by the binary using this crate. +// Signature: `unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> +// i32` +unsafe extern "C" { + fn main(argc: i32, argv: *const *const u8) -> i32; +} + +#[cfg(target_arch = "x86_64")] +mod entry { + use core::arch::naked_asm; + + /// Entry point that receives stack pointer directly from kernel. + /// On `x86_64` Linux at program start: + /// + /// - `[rsp]` = argc + /// - `[rsp+8]` = argv[0] + /// - `[rsp+16]` = argv[1] + /// - ... + /// - `[rsp+8n]` = NULL + /// - `[rsp+8n+8]` = envp[0] + /// + /// # Safety + /// + /// This is a naked function with no prologue or epilogue. It directly + /// manipulates the stack pointer (`rsp`) and assumes it was called by the + /// kernel with a valid stack containing argc and argv. The function: + /// + /// - Reads from `[rsp]` without validating the pointer + /// - Modifies `rsp` directly (16-byte alignment) + /// - Does not preserve any registers + /// - Does not return normally (exits via syscall) + /// + /// This function MUST only be used as the program entry point (`_start`). + /// Calling it from any other context is undefined behavior. This has been + /// your safety notice. I WILL put UB in your Rust program. + #[unsafe(no_mangle)] + #[unsafe(naked)] + pub unsafe extern "C" fn _start() { + naked_asm!( + // Move stack pointer to first argument register + "mov rdi, rsp", + // Align stack to 16-byte boundary (System V AMD64 ABI requirement) + "and rsp, -16", + // Call into Rust code + "call {entry_rust}", + // Move return code to syscall argument + "mov rdi, rax", + // Exit syscall + "mov rax, 60", // SYS_exit + "syscall", + entry_rust = sym super::entry_rust, + ); + } +} + +#[cfg(target_arch = "aarch64")] +mod entry { + use core::arch::naked_asm; + + /// Entry point that receives stack pointer directly from kernel. + /// On `aarch64` Linux at program start, the stack layout is identical + /// to x86_64: + /// + /// - `[sp]` = argc + /// - `[sp+8]` = argv[0] + /// - ... + /// + /// # Safety + /// + /// This is a naked function with no prologue or epilogue. It directly + /// manipulates the stack pointer (`sp`) and assumes it was called by the + /// kernel with a valid stack containing argc and argv. The function: + /// + /// - Reads from `[sp]` without validating the pointer + /// - Modifies `sp` directly (16-byte alignment) + /// - Does not preserve any registers + /// - Does not return normally (exits via SVC instruction) + /// + /// This function MUST only be used as the program entry point (`_start`). + /// Calling it from any other context is undefined behavior. + #[unsafe(no_mangle)] + #[unsafe(naked)] + pub unsafe extern "C" fn _start() { + naked_asm!( + // Move stack pointer to first argument register + "mov x0, sp", + // Align stack to 16-byte boundary (AArch64 ABI requirement) + "and sp, sp, -16", + // Call into Rust code + "bl {entry_rust}", + // Move return code to syscall argument + "mov x0, x0", + // Exit syscall + "mov x8, 93", // SYS_exit + "svc #0", + entry_rust = sym super::entry_rust, + ); + } +} + +#[cfg(target_arch = "riscv64")] +mod entry { + use core::arch::naked_asm; + + /// Entry point that receives stack pointer directly from kernel. + /// On `riscv64` Linux at program start, the stack layout is identical + /// to x86_64: + /// + /// - `[sp]` = argc + /// - `[sp+8]` = argv[0] + /// - ... + /// + /// # Safety + /// + /// This is a naked function with no prologue or epilogue. It directly + /// manipulates the stack pointer (`sp`) and assumes it was called by the + /// kernel with a valid stack containing argc and argv. The function: + /// + /// - Reads from `[sp]` without validating the pointer + /// - Modifies `sp` directly (16-byte alignment) + /// - Does not preserve any registers + /// - Does not return normally (exits via ECALL instruction) + /// + /// This function MUST only be used as the program entry point (`_start`). + /// Calling it from any other context is undefined behavior. + #[unsafe(no_mangle)] + #[unsafe(naked)] + pub unsafe extern "C" fn _start() { + naked_asm!( + // Move stack pointer to first argument register + "mv a0, sp", + // Align stack to 16-byte boundary (RISC-V ABI requirement) + "andi sp, sp, -16", + // Call into Rust code + "call {entry_rust}", + // Move return code to syscall argument + "mv a0, a0", + // Exit syscall + "li a7, 93", // SYS_exit + "ecall", + entry_rust = sym super::entry_rust, + ); + } +} + +// Re-export the entry point +#[cfg(target_arch = "x86_64")] pub use entry::_start; +#[cfg(target_arch = "aarch64")] pub use entry::_start; +#[cfg(target_arch = "riscv64")] pub use entry::_start; + /// Direct syscall to open a file /// /// # Returns From d6977bafe59b871bb928d5c17ada5c07b91d8d76 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 23:45:30 +0300 Subject: [PATCH 23/31] various: (ab)use the new syscall wrappers and symbols; drop libc entirely Signed-off-by: NotAShelf Change-Id: I19ecd9801cf6e04adcedd3003d9fc59d6a6a6964 --- .cargo/config.toml | 6 +++-- crates/lib/src/lib.rs | 33 ++++++++++++++++++--------- microfetch/src/main.rs | 51 +++++++++++++++++++++++------------------- 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index c406f76..7a7a98f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,5 @@ -# https://github.com/rui314/mold?tab=readme-ov-file#how-to-use +# Link with Mold, and without libc! We use nostartfiles to avoid the C runtime +# See: +# [target.'cfg(target_os = "linux")'] -rustflags = [ "-C", "link-arg=-fuse-ld=mold", "-C", "link-arg=-lc", "-C", "link-arg=-lgcc_s" ] +rustflags = [ "-C", "link-arg=-fuse-ld=mold", "-C", "link-arg=-nostartfiles" ] diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 7da92a0..84fe5ab 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -142,22 +142,34 @@ impl Drop for OnceLock { } } -// Access to the environ pointer (provided by libc startup code) -unsafe extern "C" { - static environ: *const *const u8; -} +// Store the environment pointer internally,initialized from `main()`. This +// helps avoid the libc dependency *completely*. +static ENVP: AtomicPtr<*const u8> = AtomicPtr::new(core::ptr::null_mut()); -/// Gets an environment variable by name (without using std). +/// Initialize the environment pointer. Must be called before any `getenv()` +/// calls. This is called from `main()` with the calculated `envp`. /// /// # Safety /// -/// This function reads from the environ global which is initialized -/// by the C runtime before `main()` is called. +/// envp must be a valid null-terminated array of C strings, or null if +/// no environment is available. +#[inline] +pub unsafe fn init_env(envp: *const *const u8) { + ENVP.store(envp.cast_mut(), Ordering::Release); +} + +/// Gets the current environment pointer. +#[inline] +#[must_use] +fn get_envp() -> *const *const u8 { + ENVP.load(Ordering::Acquire) +} + +/// Gets an environment variable by name without using std or libc by reading +/// from the environment pointer set by [`init_env`]. #[must_use] pub fn getenv(name: &str) -> Option<&'static [u8]> { - // SAFETY: environ is set up by the C runtime before main() runs - // and remains valid for the lifetime of the program - let envp = unsafe { environ }; + let envp = get_envp(); if envp.is_null() { return None; } @@ -237,6 +249,7 @@ impl UtsName { if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { return Err(Error::last_os_error()); } + Ok(Self(unsafe { uts.assume_init() })) } diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index 0bdc022..25e855c 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -3,40 +3,49 @@ extern crate alloc; -use microfetch_alloc::BumpAlloc; -use microfetch_asm::sys_write; -#[cfg(not(test))] -use {core::panic::PanicInfo, microfetch_asm::sys_exit}; +use core::panic::PanicInfo; + +use microfetch_alloc::BumpAllocator; +// Re-export libc replacement functions from asm crate +pub use microfetch_asm::{memcpy, memset, strlen}; +use microfetch_asm::{sys_exit, sys_write}; +// Global allocator #[global_allocator] -static ALLOCATOR: BumpAlloc = BumpAlloc::new(); +static ALLOCATOR: BumpAllocator = BumpAllocator::new(); -/// Receives argc and argv directly. The C runtime will call this after -/// initializing the environment. Cool right? +/// Main application entry point. Called by the asm crate's entry point +/// after setting up argc, argv, and envp. /// /// # Safety /// /// argv must be a valid pointer to an array of argc C strings. #[unsafe(no_mangle)] pub unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32 { - // SAFETY: argc and argv are provided by the C runtime and are valid + // Calculate envp from argv. On Linux, envp is right after argv on the stack + // but I bet 12 cents that there will be at least one exception. + let argc_usize = usize::try_from(argc).unwrap_or(0); + let envp = unsafe { argv.add(argc_usize + 1) }; + + // Initialize the environment pointer unsafe { - match microfetch_lib::run(argc, argv) { - Ok(()) => 0, - Err(e) => { - // Print error message to stderr (fd 2) - let msg = alloc::format!("Error: {e}\n"); - let _ = sys_write(2, msg.as_ptr(), msg.len()); - 1 - }, - } + microfetch_lib::init_env(envp); + } + + // Run the main application logic + match unsafe { microfetch_lib::run(argc, argv) } { + Ok(()) => 0, + Err(e) => { + let msg = alloc::format!("Error: {e}\n"); + let _ = unsafe { sys_write(2, msg.as_ptr(), msg.len()) }; + 1 + }, } } #[cfg(not(test))] #[panic_handler] fn panic(_info: &PanicInfo) -> ! { - // Write "panic" to stderr and exit const PANIC_MSG: &[u8] = b"panic\n"; unsafe { let _ = sys_write(2, PANIC_MSG.as_ptr(), PANIC_MSG.len()); @@ -44,11 +53,7 @@ fn panic(_info: &PanicInfo) -> ! { } } -// FIXME: Stubs for Rust exception handling symbols needed when using alloc with -// panic=abort These are normally provided by the unwinding runtime, but we're -// using panic=abort. I don't actually think this is the correct approach, but I -// cannot think of anything better. - +// Stubs for Rust exception handling #[cfg(not(test))] #[unsafe(no_mangle)] const extern "C" fn rust_eh_personality() {} From 5ce0f3b1e8b87c6e802f8ead41eb779f82fe6e1e Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 23:48:19 +0300 Subject: [PATCH 24/31] nix: use mold on all Linux targets; fix source filters Signed-off-by: NotAShelf Change-Id: Ib5925097dd366505d01f9448e7e2ee926a6a6964 --- nix/package.nix | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index de8bdf8..3892b41 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,22 +1,13 @@ { lib, - stdenv, - stdenvAdapters, rustPlatform, llvm, - useMold ? stdenv.isLinux, }: let toml = (lib.importTOML ../Cargo.toml).package; pname = toml.name; inherit (toml) version; - - # Select stdenv based on useMold flag - stdenv = - if useMold - then stdenvAdapters.useMoldLinker llvm.stdenv - else llvm.stdenv; in - rustPlatform.buildRustPackage.override {inherit stdenv;} { + rustPlatform.buildRustPackage.override {inherit (llvm) stdenv;} { inherit pname version; src = let fs = lib.fileset; @@ -25,10 +16,11 @@ in fs.toSource { root = s; fileset = fs.unions [ - (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) + (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /crates)) + (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /microfetch)) + (s + /.cargo) (s + /Cargo.lock) (s + /Cargo.toml) - (s + /benches) ]; }; @@ -37,12 +29,6 @@ in buildNoDefaultFeatures = true; doCheck = false; - # Only set RUSTFLAGS for mold if useMold is enabled - env = lib.optionalAttrs useMold { - CARGO_LINKER = "clang"; - RUSTFLAGS = "-C link-arg=-fuse-ld=mold"; - }; - meta = { description = "Microscopic fetch script in Rust, for NixOS systems"; homepage = "https://github.com/NotAShelf/microfetch"; From 781de52ca6edf1271e10a59dc8eb9cec6decfd45 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 23:53:04 +0300 Subject: [PATCH 25/31] build: be more aggressive with linker optimizations; wrap mold Signed-off-by: NotAShelf Change-Id: I0e3132ab1499684eda715c3cee9b27a16a6a6964 --- .cargo/config.toml | 30 ++++++++++++++++++++++++++++-- Cargo.toml | 6 +++--- microfetch/Cargo.toml | 8 ++++---- scripts/ld-wrapper | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 9 deletions(-) create mode 100755 scripts/ld-wrapper diff --git a/.cargo/config.toml b/.cargo/config.toml index 7a7a98f..d9f659e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,31 @@ -# Link with Mold, and without libc! We use nostartfiles to avoid the C runtime +# Use a linker wrapper that invokes mold then strips junk sections with objcopy. +# mold cannot discard .eh_frame/.dynstr/.comment via linker scripts, so we do +# it as a post-link step. # See: # [target.'cfg(target_os = "linux")'] -rustflags = [ "-C", "link-arg=-fuse-ld=mold", "-C", "link-arg=-nostartfiles" ] +linker = "scripts/ld-wrapper" +rustflags = [ + # No C runtime, we provide _start ourselves + "-C", + "link-arg=-nostartfiles", + # Fully static, no dynamic linker, no .interp/.dynsym/.dynamic overhead + "-C", + "link-arg=-static", + # Static PIE is incompatible with -static :( + "-C", + "relocation-model=static", + # Suppress .eh_frame emission from our own codegen (does not cover compiler_builtins; + # those remnants are removed by the linker wrapper via objcopy post-link) + "-C", + "force-unwind-tables=no", + # Linker flags + "-C", + "link-arg=-Wl,--gc-sections", # remove unreferenced input sections + "-C", + "link-arg=-Wl,--strip-all", # strip all symbol table entries + "-C", + "link-arg=-Wl,--build-id=none", # omit the .note.gnu.build-id section + "-C", + "link-arg=-Wl,-z,norelro", # disable RELRO (removes relro_padding) +] diff --git a/Cargo.toml b/Cargo.toml index da548d7..9500909 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,15 +11,15 @@ version = "0.4.13" [workspace.dependencies] microfetch-alloc = { path = "./crates/alloc" } -microfetch-asm = { path = "./crates/asm" } -microfetch-lib = { path = "./crates/lib" } +microfetch-asm = { path = "./crates/asm" } +microfetch-lib = { path = "./crates/lib" } criterion = { default-features = false, features = [ "cargo_bench_support" ], version = "0.8.2" } criterion-cycles-per-byte = "0.8.0" [profile.dev] opt-level = 1 -panic = "abort" +panic = "abort" [profile.release] codegen-units = 1 diff --git a/microfetch/Cargo.toml b/microfetch/Cargo.toml index b29b420..43d4f9c 100644 --- a/microfetch/Cargo.toml +++ b/microfetch/Cargo.toml @@ -11,14 +11,14 @@ repository = "https://github.com/notashelf/microfetch" publish = false [dependencies] -hotpath = { optional = true, version = "0.14.0" } +hotpath = { optional = true, version = "0.14.0" } microfetch-alloc.workspace = true -microfetch-lib.workspace = true -microfetch-asm.workspace = true +microfetch-asm.workspace = true +microfetch-lib.workspace = true [features] hotpath = [ "dep:hotpath" ] hotpath-alloc = [ "hotpath/hotpath-alloc" ] [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/scripts/ld-wrapper b/scripts/ld-wrapper new file mode 100755 index 0000000..cf2cb21 --- /dev/null +++ b/scripts/ld-wrapper @@ -0,0 +1,34 @@ +#!/usr/bin/env sh +# Invoke mold, then strip junk sections from the output binary with objcopy. +# This (more or less) removes sections that mold cannot discard itself, suck as: +# - .eh_frame / .eh_frame_hdr - unwind tables from compiler_builtins +# - dynstr - mold emits this, even for fully static binaries +# - .comment - compiler version string +# +# We forward everything to mold via -fuse-ld, then post-process the output in place. + +set -eu + +# Locate the output file +OUTPUT="" +prev="" +for arg in "$@"; do + if [ "$prev" = "-o" ]; then + OUTPUT="$arg" + break + fi + prev="$arg" +done + +# Invoke mold via the cc driver, forward all original arguments +cc -fuse-ld=mold "$@" + +# Remove sections that mold cannot discard +if [ -n "$OUTPUT" ] && [ -f "$OUTPUT" ]; then + objcopy \ + --remove-section=.eh_frame \ + --remove-section=.eh_frame_hdr \ + --remove-section=.dynstr \ + --remove-section=.comment \ + "$OUTPUT" +fi From 3b823a098214a6e256455847811cc44e992e376e Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 28 Mar 2026 00:05:02 +0300 Subject: [PATCH 26/31] nix: fix Signed-off-by: NotAShelf Change-Id: Ibdd7b1deaff9489cedcbbc05ce7956d86a6a6964 --- nix/package.nix | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index 3892b41..2658d73 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -3,8 +3,8 @@ rustPlatform, llvm, }: let - toml = (lib.importTOML ../Cargo.toml).package; - pname = toml.name; + pname = "microfetch"; + toml = (lib.importTOML ../Cargo.toml).workspace.package; inherit (toml) version; in rustPlatform.buildRustPackage.override {inherit (llvm) stdenv;} { @@ -16,9 +16,10 @@ in fs.toSource { root = s; fileset = fs.unions [ - (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /crates)) - (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /microfetch)) + (s + /crates) + (s + /microfetch) (s + /.cargo) + (s + /scripts/ld-wrapper) (s + /Cargo.lock) (s + /Cargo.toml) ]; From d49e75b6d23dfee2cbfcc937b884912f462580c5 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 17:08:13 -0400 Subject: [PATCH 27/31] ci: stop overriding .cargo/config.toml rustflags in CI workflows --- .github/workflows/hotpath-profile.yml | 2 ++ .github/workflows/rust.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/hotpath-profile.yml b/.github/workflows/hotpath-profile.yml index 244fb2b..9909986 100644 --- a/.github/workflows/hotpath-profile.yml +++ b/.github/workflows/hotpath-profile.yml @@ -16,6 +16,8 @@ jobs: fetch-depth: 0 - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Create metrics directory run: mkdir -p /tmp/metrics diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index deb45e3..945ed5c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,6 +29,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@v1 with: target: ${{ matrix.target }} + rustflags: "" - name: "Make Mold the default linker" uses: rui314/setup-mold@v1 From 308ca53dd8100a6025ce87b92e0ace8830082300 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 17:08:28 -0400 Subject: [PATCH 28/31] ci: add missing mold linker setup to hotpath-profile workflow --- .github/workflows/hotpath-profile.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/hotpath-profile.yml b/.github/workflows/hotpath-profile.yml index 9909986..dfb09c1 100644 --- a/.github/workflows/hotpath-profile.yml +++ b/.github/workflows/hotpath-profile.yml @@ -19,6 +19,9 @@ jobs: with: rustflags: "" + - name: Make Mold the default linker + uses: rui314/setup-mold@v1 + - name: Create metrics directory run: mkdir -p /tmp/metrics From ddca7573e7342c743984941185923caa2680bded Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 18:03:37 -0400 Subject: [PATCH 29/31] crates/asm: add bcmp and memcmp for static linking without libc riscv64 codegen emits calls to bcmp for byte comparisons, which is undefined when linking with -static and no libc. Provide both bcmp and memcmp implementations alongside the existing memcpy/memset/strlen. --- crates/asm/src/lib.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 1589a1d..273cc35 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -55,6 +55,33 @@ pub unsafe extern "C" fn memset(s: *mut u8, c: i32, n: usize) -> *mut u8 { s } +/// Compares two byte sequences. +/// +/// # Safety +/// +/// `s1` and `s2` must be valid pointers to memory of at least `n` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn bcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 { + for i in 0..n { + let a = unsafe { *s1.add(i) }; + let b = unsafe { *s2.add(i) }; + if a != b { + return i32::from(a) - i32::from(b); + } + } + 0 +} + +/// Compares two byte sequences. +/// +/// # Safety +/// +/// `s1` and `s2` must be valid pointers to memory of at least `n` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 { + unsafe { bcmp(s1, s2, n) } +} + /// Calculates the length of a null-terminated string. /// /// # Safety From 9e13a6f67401b7f5d4b6209bd8d356a2bbcfdc9f Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 18:03:59 -0400 Subject: [PATCH 30/31] build: move link flags to build.rs, scope config.toml per-triple cargo config.toml [target.] flags apply to ALL compilations for that triple including proc-macro .so crates and build scripts. Move -nostartfiles, -static, and linker stripping flags to build.rs using cargo:rustc-link-arg-bin so they only affect the final binary. Switch config.toml from cfg(target_os) to per-triple sections and keep only force-unwind-tables=no (safe for all crate types). Make ld-wrapper detect -shared (proc-macro) and fall back to plain cc, and auto-detect mold availability. --- .cargo/config.toml | 40 +++++++++++++++------------------------- microfetch/build.rs | 27 +++++++++++++++++++++++++++ scripts/ld-wrapper | 34 +++++++++++++++++++++++++--------- 3 files changed, 67 insertions(+), 34 deletions(-) create mode 100644 microfetch/build.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index d9f659e..a9b5e34 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,31 +1,21 @@ -# Use a linker wrapper that invokes mold then strips junk sections with objcopy. -# mold cannot discard .eh_frame/.dynstr/.comment via linker scripts, so we do -# it as a post-link step. -# See: -# -[target.'cfg(target_os = "linux")'] -linker = "scripts/ld-wrapper" +# Link-args like -nostartfiles and -static are in microfetch/build.rs using +# cargo:rustc-link-arg-bin so they only affect the final binary and don't +# break proc-macro or build-script linking. + +[target.x86_64-unknown-linux-gnu] rustflags = [ - # No C runtime, we provide _start ourselves - "-C", - "link-arg=-nostartfiles", - # Fully static, no dynamic linker, no .interp/.dynsym/.dynamic overhead - "-C", - "link-arg=-static", - # Static PIE is incompatible with -static :( - "-C", - "relocation-model=static", - # Suppress .eh_frame emission from our own codegen (does not cover compiler_builtins; - # those remnants are removed by the linker wrapper via objcopy post-link) "-C", "force-unwind-tables=no", - # Linker flags - "-C", - "link-arg=-Wl,--gc-sections", # remove unreferenced input sections - "-C", - "link-arg=-Wl,--strip-all", # strip all symbol table entries +] + +[target.aarch64-unknown-linux-gnu] +rustflags = [ "-C", - "link-arg=-Wl,--build-id=none", # omit the .note.gnu.build-id section + "force-unwind-tables=no", +] + +[target.riscv64gc-unknown-linux-gnu] +rustflags = [ "-C", - "link-arg=-Wl,-z,norelro", # disable RELRO (removes relro_padding) + "force-unwind-tables=no", ] diff --git a/microfetch/build.rs b/microfetch/build.rs new file mode 100644 index 0000000..0365145 --- /dev/null +++ b/microfetch/build.rs @@ -0,0 +1,27 @@ +fn main() { + // These flags only apply to the microfetch binary, not to proc-macro crates + // or other host-compiled artifacts. Putting them in .cargo/config.toml would + // break proc-macro crates (e.g. hotpath_macros) that must be loadable shared + // libraries. + println!("cargo:rustc-link-arg-bin=microfetch=-nostartfiles"); + println!("cargo:rustc-link-arg-bin=microfetch=-static"); + println!("cargo:rustc-link-arg-bin=microfetch=-Wl,--gc-sections"); + println!("cargo:rustc-link-arg-bin=microfetch=-Wl,--strip-all"); + println!("cargo:rustc-link-arg-bin=microfetch=-Wl,--build-id=none"); + println!("cargo:rustc-link-arg-bin=microfetch=-Wl,-z,norelro"); + + // Use mold linker if available (faster than default ld) + if which("mold") { + println!("cargo:rustc-link-arg-bin=microfetch=-fuse-ld=mold"); + } +} + +fn which(name: &str) -> bool { + std::process::Command::new("which") + .arg(name) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} diff --git a/scripts/ld-wrapper b/scripts/ld-wrapper index cf2cb21..b643f12 100755 --- a/scripts/ld-wrapper +++ b/scripts/ld-wrapper @@ -1,27 +1,43 @@ #!/usr/bin/env sh -# Invoke mold, then strip junk sections from the output binary with objcopy. -# This (more or less) removes sections that mold cannot discard itself, suck as: +# Linker wrapper for microfetch. +# +# For the final binary: invoke mold (if available) then strip junk sections +# with objcopy. Sections that mold cannot discard itself: # - .eh_frame / .eh_frame_hdr - unwind tables from compiler_builtins -# - dynstr - mold emits this, even for fully static binaries +# - .dynstr - mold emits this, even for fully static binaries # - .comment - compiler version string # -# We forward everything to mold via -fuse-ld, then post-process the output in place. +# For shared libraries (proc-macro crates) and build scripts: just use plain cc. +# These must be loadable .so files and can't use mold's aggressive stripping. set -eu -# Locate the output file +# Detect if we're building a shared library (proc-macro) by checking for -shared +IS_SHARED=0 OUTPUT="" prev="" for arg in "$@"; do + case "$arg" in + -shared) IS_SHARED=1 ;; + esac if [ "$prev" = "-o" ]; then OUTPUT="$arg" - break fi prev="$arg" done -# Invoke mold via the cc driver, forward all original arguments -cc -fuse-ld=mold "$@" +if [ "$IS_SHARED" = 1 ]; then + # Proc-macro or other .so — use plain cc, no mold, no stripping + cc "$@" + exit $? +fi + +# Final binary — use mold if available, then post-process +if command -v mold >/dev/null 2>&1; then + cc -fuse-ld=mold "$@" +else + cc "$@" +fi # Remove sections that mold cannot discard if [ -n "$OUTPUT" ] && [ -f "$OUTPUT" ]; then @@ -30,5 +46,5 @@ if [ -n "$OUTPUT" ] && [ -f "$OUTPUT" ]; then --remove-section=.eh_frame_hdr \ --remove-section=.dynstr \ --remove-section=.comment \ - "$OUTPUT" + "$OUTPUT" 2>/dev/null || true fi From 04e4e448268a2c7f4879986046c7f4e0fe4ec5f1 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 18:04:19 -0400 Subject: [PATCH 31/31] microfetch: move _start entry point from asm lib to binary crate A #[no_mangle] _start in a library .rlib gets linked into every dependent crate's test harness, conflicting with Scrt1.o. Move _start, entry_rust, and the extern main declaration into the binary crate where they belong. Also fixes aarch64 stack alignment: `and sp, sp, #-16` is invalid because logical instructions encode register 31 as xzr, not sp; use a temp register (x9) instead. --- crates/asm/src/lib.rs | 192 ----------------------------------------- microfetch/src/main.rs | 64 ++++++++++++++ 2 files changed, 64 insertions(+), 192 deletions(-) diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 273cc35..840660a 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -96,198 +96,6 @@ pub const unsafe extern "C" fn strlen(s: *const u8) -> usize { len } -/// Function pointer type for the main application entry point. -/// The function receives argc and argv and should return an exit code. -pub type MainFn = unsafe extern "C" fn(i32, *const *const u8) -> i32; - -static mut MAIN_FN: Option = None; - -/// Register the main function to be called from the entry point. -/// This must be called before the program starts (e.g., in a constructor). -pub fn register_main(main_fn: MainFn) { - unsafe { - MAIN_FN = Some(main_fn); - } -} - -/// Rust entry point called from `_start` assembly. -/// -/// The `stack` pointer points to: -/// `[rsp]` = argc -/// `[rsp+8]` = argv[0] -/// etc. -/// -/// # Safety -/// -/// The `stack` pointer must point to valid stack memory set up by the kernel -/// AND the binary must define a `main` function with the following signature: -/// -/// ```rust,ignore -/// unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32` -/// ``` -#[unsafe(no_mangle)] -pub unsafe extern "C" fn entry_rust(stack: *const usize) -> i32 { - // Read argc and argv from stack - let argc = unsafe { *stack }; - let argv = unsafe { stack.add(1).cast::<*const u8>() }; - - // SAFETY: argc is unlikely to exceed i32::MAX on real systems - let argc_i32 = i32::try_from(argc).unwrap_or(i32::MAX); - - // Call the main function (defined by the binary crate) - unsafe { main(argc_i32, argv) } -} - -// External main function that must be defined by the binary using this crate. -// Signature: `unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> -// i32` -unsafe extern "C" { - fn main(argc: i32, argv: *const *const u8) -> i32; -} - -#[cfg(target_arch = "x86_64")] -mod entry { - use core::arch::naked_asm; - - /// Entry point that receives stack pointer directly from kernel. - /// On `x86_64` Linux at program start: - /// - /// - `[rsp]` = argc - /// - `[rsp+8]` = argv[0] - /// - `[rsp+16]` = argv[1] - /// - ... - /// - `[rsp+8n]` = NULL - /// - `[rsp+8n+8]` = envp[0] - /// - /// # Safety - /// - /// This is a naked function with no prologue or epilogue. It directly - /// manipulates the stack pointer (`rsp`) and assumes it was called by the - /// kernel with a valid stack containing argc and argv. The function: - /// - /// - Reads from `[rsp]` without validating the pointer - /// - Modifies `rsp` directly (16-byte alignment) - /// - Does not preserve any registers - /// - Does not return normally (exits via syscall) - /// - /// This function MUST only be used as the program entry point (`_start`). - /// Calling it from any other context is undefined behavior. This has been - /// your safety notice. I WILL put UB in your Rust program. - #[unsafe(no_mangle)] - #[unsafe(naked)] - pub unsafe extern "C" fn _start() { - naked_asm!( - // Move stack pointer to first argument register - "mov rdi, rsp", - // Align stack to 16-byte boundary (System V AMD64 ABI requirement) - "and rsp, -16", - // Call into Rust code - "call {entry_rust}", - // Move return code to syscall argument - "mov rdi, rax", - // Exit syscall - "mov rax, 60", // SYS_exit - "syscall", - entry_rust = sym super::entry_rust, - ); - } -} - -#[cfg(target_arch = "aarch64")] -mod entry { - use core::arch::naked_asm; - - /// Entry point that receives stack pointer directly from kernel. - /// On `aarch64` Linux at program start, the stack layout is identical - /// to x86_64: - /// - /// - `[sp]` = argc - /// - `[sp+8]` = argv[0] - /// - ... - /// - /// # Safety - /// - /// This is a naked function with no prologue or epilogue. It directly - /// manipulates the stack pointer (`sp`) and assumes it was called by the - /// kernel with a valid stack containing argc and argv. The function: - /// - /// - Reads from `[sp]` without validating the pointer - /// - Modifies `sp` directly (16-byte alignment) - /// - Does not preserve any registers - /// - Does not return normally (exits via SVC instruction) - /// - /// This function MUST only be used as the program entry point (`_start`). - /// Calling it from any other context is undefined behavior. - #[unsafe(no_mangle)] - #[unsafe(naked)] - pub unsafe extern "C" fn _start() { - naked_asm!( - // Move stack pointer to first argument register - "mov x0, sp", - // Align stack to 16-byte boundary (AArch64 ABI requirement) - "and sp, sp, -16", - // Call into Rust code - "bl {entry_rust}", - // Move return code to syscall argument - "mov x0, x0", - // Exit syscall - "mov x8, 93", // SYS_exit - "svc #0", - entry_rust = sym super::entry_rust, - ); - } -} - -#[cfg(target_arch = "riscv64")] -mod entry { - use core::arch::naked_asm; - - /// Entry point that receives stack pointer directly from kernel. - /// On `riscv64` Linux at program start, the stack layout is identical - /// to x86_64: - /// - /// - `[sp]` = argc - /// - `[sp+8]` = argv[0] - /// - ... - /// - /// # Safety - /// - /// This is a naked function with no prologue or epilogue. It directly - /// manipulates the stack pointer (`sp`) and assumes it was called by the - /// kernel with a valid stack containing argc and argv. The function: - /// - /// - Reads from `[sp]` without validating the pointer - /// - Modifies `sp` directly (16-byte alignment) - /// - Does not preserve any registers - /// - Does not return normally (exits via ECALL instruction) - /// - /// This function MUST only be used as the program entry point (`_start`). - /// Calling it from any other context is undefined behavior. - #[unsafe(no_mangle)] - #[unsafe(naked)] - pub unsafe extern "C" fn _start() { - naked_asm!( - // Move stack pointer to first argument register - "mv a0, sp", - // Align stack to 16-byte boundary (RISC-V ABI requirement) - "andi sp, sp, -16", - // Call into Rust code - "call {entry_rust}", - // Move return code to syscall argument - "mv a0, a0", - // Exit syscall - "li a7, 93", // SYS_exit - "ecall", - entry_rust = sym super::entry_rust, - ); - } -} - -// Re-export the entry point -#[cfg(target_arch = "x86_64")] pub use entry::_start; -#[cfg(target_arch = "aarch64")] pub use entry::_start; -#[cfg(target_arch = "riscv64")] pub use entry::_start; - /// Direct syscall to open a file /// /// # Returns diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index 25e855c..d466dcc 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -3,6 +3,7 @@ extern crate alloc; +use core::arch::naked_asm; use core::panic::PanicInfo; use microfetch_alloc::BumpAllocator; @@ -10,6 +11,69 @@ use microfetch_alloc::BumpAllocator; pub use microfetch_asm::{memcpy, memset, strlen}; use microfetch_asm::{sys_exit, sys_write}; +// --------------------------------------------------------------------------- +// Program entry point: _start → entry_rust → main +// +// These live in the binary crate (not the asm library) because #[no_mangle] +// _start in an .rlib would conflict with Scrt1.o when any dependent crate +// builds a test harness. +// --------------------------------------------------------------------------- + +/// Rust entry point called from the arch-specific `_start` trampolines below. +#[unsafe(no_mangle)] +unsafe extern "C" fn entry_rust(stack: *const usize) -> i32 { + let argc = unsafe { *stack }; + let argv = unsafe { stack.add(1).cast::<*const u8>() }; + let argc_i32 = i32::try_from(argc).unwrap_or(i32::MAX); + unsafe { main(argc_i32, argv) } +} + +#[cfg(target_arch = "x86_64")] +#[unsafe(no_mangle)] +#[unsafe(naked)] +unsafe extern "C" fn _start() { + naked_asm!( + "mov rdi, rsp", + "and rsp, -16", + "call {entry_rust}", + "mov rdi, rax", + "mov rax, 60", + "syscall", + entry_rust = sym entry_rust, + ); +} + +#[cfg(target_arch = "aarch64")] +#[unsafe(no_mangle)] +#[unsafe(naked)] +unsafe extern "C" fn _start() { + naked_asm!( + "mov x0, sp", + "mov x9, sp", + "and x9, x9, #-16", + "mov sp, x9", + "bl {entry_rust}", + "mov x8, #93", + "svc #0", + entry_rust = sym entry_rust, + ); +} + +#[cfg(target_arch = "riscv64")] +#[unsafe(no_mangle)] +#[unsafe(naked)] +unsafe extern "C" fn _start() { + naked_asm!( + "mv a0, sp", + "andi sp, sp, -16", + "call {entry_rust}", + "mv a0, a0", + "li a7, 93", + "ecall", + entry_rust = sym entry_rust, + ); +} + // Global allocator #[global_allocator] static ALLOCATOR: BumpAllocator = BumpAllocator::new();