Merge pull request #54 from NotAShelf/notashelf/push-ypvyyznuosqk
Some checks are pending
Rust / Test on aarch64-unknown-linux-gnu (push) Waiting to run
Rust / Test on x86_64-unknown-linux-gnu (push) Waiting to run

various: replace libc invocations with `std::env` and handwritten syscall wrappers
This commit is contained in:
raf 2026-03-26 22:45:22 +03:00 committed by GitHub
commit bf1b319971
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 296 additions and 70 deletions

1
Cargo.lock generated
View file

@ -571,7 +571,6 @@ version = "0.4.13"
dependencies = [ dependencies = [
"criterion", "criterion",
"hotpath", "hotpath",
"libc",
] ]
[[package]] [[package]]

View file

@ -19,7 +19,6 @@ path = "src/main.rs"
[dependencies] [dependencies]
hotpath = { optional = true, version = "0.13.0" } hotpath = { optional = true, version = "0.13.0" }
libc = "0.2.183"
[dev-dependencies] [dev-dependencies]
criterion = "0.8.1" criterion = "0.8.1"

View file

@ -37,8 +37,8 @@ impl Colors {
} }
pub static COLORS: LazyLock<Colors> = LazyLock::new(|| { pub static COLORS: LazyLock<Colors> = LazyLock::new(|| {
const NO_COLOR: *const libc::c_char = c"NO_COLOR".as_ptr(); // Only presence matters; value is irrelevant per the NO_COLOR spec
let is_no_color = unsafe { !libc::getenv(NO_COLOR).is_null() }; let is_no_color = std::env::var_os("NO_COLOR").is_some();
Colors::new(is_no_color) Colors::new(is_no_color)
}); });

View file

@ -1,27 +1,22 @@
use std::{ffi::CStr, fmt::Write}; use std::{ffi::OsStr, fmt::Write};
#[must_use] #[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_desktop_info() -> String { pub fn get_desktop_info() -> String {
// Retrieve the environment variables and handle Result types let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP");
let desktop_str = unsafe { let session_os = std::env::var_os("XDG_SESSION_TYPE");
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 backend_str = unsafe { let desktop_raw = desktop_os
let ptr = libc::getenv(c"XDG_SESSION_TYPE".as_ptr()); .as_deref()
if ptr.is_null() { .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" "Unknown"
} else { } else {
let s = CStr::from_ptr(ptr).to_str().unwrap_or("Unknown"); session_raw
if s.is_empty() { "Unknown" } else { s }
}
}; };
// Pre-calculate capacity: desktop_len + " (" + backend_len + ")" // Pre-calculate capacity: desktop_len + " (" + backend_len + ")"

View file

@ -5,42 +5,44 @@ pub mod syscall;
pub mod system; pub mod system;
pub mod uptime; pub mod uptime;
use std::mem::MaybeUninit; use std::{ffi::CStr, mem::MaybeUninit};
/// Wrapper for `libc::utsname` with safe accessor methods use crate::syscall::{UtsNameBuf, sys_uname};
pub struct UtsName(libc::utsname);
/// Wrapper for `utsname` with safe accessor methods
pub struct UtsName(UtsNameBuf);
impl UtsName { impl UtsName {
/// Calls `uname` syscall and returns a `UtsName` wrapper /// Calls `uname(2)` syscall and returns a `UtsName` wrapper
/// ///
/// # Errors /// # Errors
/// ///
/// Returns an error if the `uname` syscall fails /// Returns an error if the `uname` syscall fails
pub fn uname() -> Result<Self, std::io::Error> { pub fn uname() -> Result<Self, std::io::Error> {
let mut uts = MaybeUninit::uninit(); 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()); return Err(std::io::Error::last_os_error());
} }
Ok(Self(unsafe { uts.assume_init() })) Ok(Self(unsafe { uts.assume_init() }))
} }
#[must_use] #[must_use]
pub const fn nodename(&self) -> &std::ffi::CStr { pub const fn nodename(&self) -> &CStr {
unsafe { std::ffi::CStr::from_ptr(self.0.nodename.as_ptr()) } unsafe { CStr::from_ptr(self.0.nodename.as_ptr().cast()) }
} }
#[must_use] #[must_use]
pub const fn sysname(&self) -> &std::ffi::CStr { pub const fn sysname(&self) -> &CStr {
unsafe { std::ffi::CStr::from_ptr(self.0.sysname.as_ptr()) } unsafe { CStr::from_ptr(self.0.sysname.as_ptr().cast()) }
} }
#[must_use] #[must_use]
pub const fn release(&self) -> &std::ffi::CStr { pub const fn release(&self) -> &CStr {
unsafe { std::ffi::CStr::from_ptr(self.0.release.as_ptr()) } unsafe { CStr::from_ptr(self.0.release.as_ptr().cast()) }
} }
#[must_use] #[must_use]
pub const fn machine(&self) -> &std::ffi::CStr { pub const fn machine(&self) -> &CStr {
unsafe { std::ffi::CStr::from_ptr(self.0.machine.as_ptr()) } unsafe { CStr::from_ptr(self.0.machine.as_ptr().cast()) }
} }
} }

View file

@ -99,14 +99,19 @@ fn print_system_info(
{blue} {cyan} {cyan} {blue}Colors{reset} {colors}\n\n" {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 // 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 { if written < 0 {
return Err(io::Error::last_os_error().into()); 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 { 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(()) Ok(())
} }

View file

@ -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 /// Direct syscall to close a file descriptor
/// ///
/// # Safety /// # 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 /// Read entire file using direct syscalls. This avoids libc overhead and can be
/// significantly faster for small files. /// significantly faster for small files.
/// ///

View file

@ -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] #[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_username_and_hostname(utsname: &UtsName) -> String { pub fn get_username_and_hostname(utsname: &UtsName) -> String {
let username = unsafe { let username_os = std::env::var_os("USER");
let ptr = libc::getenv(c"USER".as_ptr()); let username = username_os
if ptr.is_null() { .as_deref()
"unknown_user" .and_then(OsStr::to_str)
} else { .unwrap_or("unknown_user");
CStr::from_ptr(ptr).to_str().unwrap_or("unknown_user")
}
};
let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); let hostname = utsname.nodename().to_str().unwrap_or("unknown_host");
let capacity = COLORS.yellow.len() let capacity = COLORS.yellow.len()
@ -38,16 +39,13 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String {
#[must_use] #[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_shell() -> String { pub fn get_shell() -> String {
unsafe { let shell_os = std::env::var_os("SHELL");
let ptr = libc::getenv(c"SHELL".as_ptr()); let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or("");
if ptr.is_null() { let start = shell.rfind('/').map_or(0, |i| i + 1);
return "unknown_shell".into(); if shell.is_empty() {
} "unknown_shell".into()
} else {
let bytes = CStr::from_ptr(ptr).to_bytes(); shell[start..].into()
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()
} }
} }
@ -59,15 +57,16 @@ pub fn get_shell() -> String {
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
pub fn get_root_disk_usage() -> Result<String, io::Error> { pub fn get_root_disk_usage() -> Result<String, io::Error> {
let mut vfs = MaybeUninit::uninit(); let mut vfs = MaybeUninit::<StatfsBuf>::uninit();
let path = b"/\0"; 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()); return Err(io::Error::last_os_error());
} }
let vfs = unsafe { vfs.assume_init() }; 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 total_blocks = vfs.f_blocks;
let available_blocks = vfs.f_bavail; let available_blocks = vfs.f_bavail;
@ -158,12 +157,12 @@ pub fn get_memory_usage() -> Result<String, io::Error> {
} }
#[allow(clippy::cast_precision_loss)] #[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)] #[allow(clippy::cast_precision_loss)]
let available_memory_gb = available_memory_kb as f64 / 1024.0 / 1024.0; let available_gb = available_memory_kb as f64 / 1024.0 / 1024.0;
let used_memory_gb = total_memory_gb - available_memory_gb; 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()?; let (used_memory, total_memory) = parse_memory_info()?;

View file

@ -17,14 +17,41 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str {
unsafe { std::str::from_utf8_unchecked(&buf[i..]) } 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 /// # Safety
/// ///
/// This function uses inline assembly to make a direct syscall.
/// The caller must ensure the sysinfo pointer is valid. /// The caller must ensure the sysinfo pointer is valid.
#[inline] #[inline]
unsafe fn sys_sysinfo(info: *mut libc::sysinfo) -> i64 { unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 {
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
{ {
let ret: i64; 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")))] #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
{ {
unsafe { libc::sysinfo(info) as i64 } compile_error!("Unsupported architecture for inline assembly syscalls");
} }
} }