Introduction

heim logo

heim is a cross-platform Rust crate for retrieving information about system processes and various system details (such as CPU, memory, disks, networks and sensors).

heim has few key goals which define its development and public interface:

  • Async-first.
  • Cross-platform.
    Any code from heim should just work on all supported platforms. OS-specific things do exist, but API design forces users to pay attention to them.
  • Modular design.
  • Idiomatic and easy to use.

License

Licensed under either of Apache License 2.0 or MIT license at your option.

Donations

heim is an open-source project, developed in my spare time.
If you appreciate my work and want to support me or speed up the project development, you can do it here or support this project at Open Collective.

Platforms support

Tier 1

heim is still in a MVP phase and due to the lack of resources right now it targets to support Rust Tier 1 platforms only:

OSArchitecture
Linuxi686
Linuxx86_64
macOSi686
macOSx86_64
Windowsi686
Windowsx86_64

In addition, most recent OS versions are targeted right now; for example, there might be a chance that it will not work on Linux 2.6.18. This should be considered as a bug and if you have compatibility issues, feel free to create an issue about it.

Tier 2

Following targets are explicitly tested in the CI.
Same to Rust Tier 2 platforms, these can be thought of as "guaranteed to build", but no further guarantees about correctness are provided.

Target
aarch64-unknown-linux-gnu
aarch64-unknown-linux-musl
armv7-unknown-linux-gnueabihf
armv7-unknown-linux-musleabihf
arm-unknown-linux-gnueabihf
arm-unknown-linux-musleabihf

Other platforms

Other platforms are not implemented yet and there is no ETA on them. Contributions are welcomed!

Async-first approach

As it was mentioned before, heim is created with the "async-first" idea in mind.

If you are not familiar with async programming in Rust, refer to "Asynchronous Programming in Rust" book to learn about wider view on the topic.

Motivation

It might be seen unusual for such a library to be so persistent on the "async-first" approach. The reason for that is because even the modern operating systems might spend a lot of time gathering requested system information.

Most common argument against it is the "how much time will one syscall take, am I right?", but in a worse case scenarios, it might take some unknown and unpredictable amount of time to make one simple operation, which is not what you usually expect from your operating system.

For example, it might take seconds to read one file from the Linux procfs virtual filesystem; or if you accidentally called access(2) on a NFS mount, performance slaps you in the face, because now OS needs to make a network request to do that.

Acknowledging this fact, you might consider async keyword in the heim public API as a mere marker of uncertainty on how much time this operation will take; it really depends on what information you are requesting and what operating system is used.

Thanks to the modern Rust async runtimes, it is very easy and safe to do something else while waiting on this one operation, so why should not we embrace it already?

Runtime support

Internally heim uses smol crate to execute async operations and blocking crate to execute blocking operations on a separate thread pool.

Bundled support for tokio and async-std runtimes existed for some time in pre-release v0.1.0 versions, but was removed as it was slowing down the development process and introduced too much maintenance burden.\

Public API

Platform-specific information

By default heim exposes only that information, which is guaranteed to be available on all supported platforms.

Let's check heim::cpu::CpuStats struct as an example. It has two public methods: CpuStats::ctx_switches and CpuStats::interrupts, which are returning amount of the context switches and interrupts correspondingly:

use heim::{cpu, Result};

#[tokio::main]
async fn main() -> Result<()> {
    let stats = cpu::stats().await?;
    println!("Context switches: {}", stats.ctx_switches());
    println!("Interrupts: {}", stats.interrupts());

    Ok(())
}

But for Linux there is also an amount of software interrupts available, and for Windows we might also want to get the syscalls amount too.
heim follows Rust solution for providing OS-specific information and exposes extension traits for some of the publicly available structs.

In order to show software interrupts and syscalls amount, we need to bring these traits into the scope and call the corresponding methods:

use heim::{cpu, Result};

#[cfg(target_os = "linux")]
use heim::cpu::os::linux::CpuStatsExt;

#[cfg(target_os = "windows")]
use heim::cpu::os::windows::CpuStatsExt;

#[tokio::main]
async fn main() -> Result<()> {
    let stats = cpu::stats().await?;
    println!("Context switches: {}", stats.ctx_switches());
    println!("Interrupts: {}", stats.interrupts());

    #[cfg(target_os = "linux")]
    println!("Software interrupts: {}", stats.soft_interrupts());

    #[cfg(target_os = "windows")]
    println!("Syscalls: {}", stats.syscalls());

    Ok(())
}

With this approach it is very easy to write cross-platform code and platform-specific calls are more noticeable.

Measurement units

heim heavily relies on the uom crate in order to expose proper measurement units where it is applicable.

In a short, that means that, for example, heim::cpu::CpuFrequency struct methods are returning Frequency type, instead of the primitive types such as u64, which are prone to the logical bugs.

heim re-exports all used measurement quantities and units at heim::units module.
Refer to uom documentation on how to work with these types.

Example

use heim::{cpu, units, Result};

#[tokio::main]
async fn main() -> Result<()> {
    let freq = cpu::frequency().await?;

    println!("Current CPU frequency: {} GHz", freq.current().get::<units::frequency::gigahertz>());

    Ok(())
}

Components

heim provides information about various system components, and for the sake of clarity, they are split into the corresponding modules.
For example, there is heim::process module for system processes routines, heim::cpu for CPU related stuff, heim::memory for memory and swap info, you got an idea.
See crate documentation for all available modules.

Note that all these modules are not included into heim by default, and you need to explicitly enable them with Cargo features, ex.

heim = { version = "*", features = ["process", "cpu", "memory"] }

Alternatively, you can use full feature, which enables all components at once:

heim = { version = "*", features = ["full"] }

It is strongly discouraged to use full feature, unless you are really planning to use everything in heim; prefer to enable separate features instead.

Contributing

First of all, see CONTRIBUTING.md document for a contributing guidelines.

If you want to create a PR, see "development" section for a quickstart on the project structure.

Development

Project structure

heim is split into multiple sub-crates, which are gathered together in the heim facade crate.

Most of the crates has very obvious names, ex. heim-cpu, heim-memory or heim-process — they are responsible for each separate heim component.

heim-common contains commonly used types, such as heim::Result, heim::Error, prelude module for heim development and also has a bunch of platform-specific FFI bindings, which are utilized by other heim-* crates.

heim-runtime is an abtraction for all supported async runtimes. Note that instead of the calling, for example, tokio::fs::read_link() function, you need to use heim_runtime::fs::read_link() instead.
Same goes for pin!/pin_mut!, join! and try_join! macros, all of them are exported by heim-runtime.

Crate structure

Each crate responsible for the corresponding heim module has the same files structure.
For example, let's write heim-cpu from scratch and provide information about CPU statistics.

$ ls /src

os/
sys/
lib.rs
stats.rs

Where:

  • os/ contains OS-specific traits and implementations, same as Rust does
  • sys/ contains platform-specific implementations
  • lib.rs should expose all public types and functions
  • stats.rs contains the CpuStats struct and async function which returns heim::Result<crate::CpuStats>

Public interface


#![allow(unused_variables)]
fn main() {
use heim_common::prelude::wrap;

use crate::sys;

/// System CPU stats.
pub struct CpuStats(sys::CpuStats);

wrap!(CpuStats, sys::CpuStats);
}

wrap! macro generates AsRef, AsMut and From implementations, which are allowing working with the "inner" sys::CpuStats struct.

It is strictly important that struct should only has these methods, which are available on all platforms supported, as done in the following example.


#![allow(unused_variables)]
fn main() {
impl CpuStats {
    pub fn ctx_switches(&self) -> u64 {
        self.as_ref().ctx_switches()
    }

    pub fn interrupts(&self) -> u64 {
        self.as_ref().interrupts()
    }
}
}

Linux additionally provides the amount of "soft interrupts", but we can't expose it here, because it would not be portable.
Instead, we should create the CpuStatsExt trait at os/linux/stats.rs:


#![allow(unused_variables)]
fn main() {
pub trait CpuStatsExt {
    fn soft_interrupts(&self) -> u64;
}

#[cfg(target_os = "linux")]
impl CpuStatsExt for crate::CpuStats {
    fn soft_interrupts(&self) -> u64 {
        self.as_ref().soft_interrupts()   
    }
}
}

Trait itself should be publicly accessible, but impl block for our crate::CpuStats should be gated and implemented only for target_os = "linux"

Platform implementations

Now we need to create platform-specific implementation and we will start with the sys/linux/mod.rs module.

sys/linux/mod.rs should be compile-gated too with #[cfg(target_os = "linux")], because it can only be used for Linux systems. Same thing applies to all other platform-specific implementations.

Implementation for our src/stats.rs goes into the sys/linux/stats.rs module.

In the case of Linux it will contain few fields, which will be populated later:


#![allow(unused_variables)]
fn main() {
pub struct CpuStats {
    ctx_switches: u64,
    interrupts: u64,
    soft_interrupts: u64,
}

impl FromStr for CpuStats {
    type Err = Error;

    fn from_str(s: &str) -> Result<CpuStats, Self::Err> {
        // ..
    }
}
}

Now, we need to provide the async interface. In case of Linux we need to parse the /proc/stat file and create the sys::CpuStats struct with data from it.

Our sys/linux/stats.rs should declare one function:


#![allow(unused_variables)]
fn main() {
use heim_common::Result;
use heim_runtime as rt;

pub async fn cpu_times() -> Result<CpuStats> {
    rt::fs::read_into("/proc/stat")
}
}

What will happen here: /proc/stat will be read asynchronously with the help of heim_runtime::fs and then parsed with help of the FromStr implementation.

Now let's go back to our public CpuStats struct.

It should declare a similar function too, but its implementation will be much simpler:


#![allow(unused_variables)]
fn main() {
use heim_common::Result;

use crate::sys;

pub async fn stats() -> Result<CpuStats> {
    sys::stats().map(Into::into).await
}
}

Since that wrap! macro from the start generates From<sys::CpuStats> for CpuStats implementation, all we need now is to call the platform-specific function and wrap the result into public struct.

Same thing applies to all structs and functions returning Futures and Streams -- platform-specific implementations should be received from the Future or Stream and wrapped into a public struct via Into::into.

Additional

Please, stick to the Rust API guidelines where possible.

Showcases

These are some projects, which are using heim to fetch and show system information:

If you know or maintain project, that is not mentioned in here, feel free to create an issue or send a Pull Request and it will be included to this page.

Nushell

Nushell ("A new type of shell") uses heim to power two bundled plugins: ps and sys.
They are providing information about system processes and system components correspondingly as a structured data:

heim::process routines:

❯ ps | where name == node
━━━━━━━┯━━━━━━┯━━━━━━━━━┯━━━━━━━━┯━━━━━━━━━┯━━━━━━━━━
 pid   │ name │ status  │ cpu    │ mem     │ virtual 
───────┼──────┼─────────┼────────┼─────────┼─────────
 15447 │ node │ Running │ 0.0000 │ 18.5 MB │  4.7 GB 
━━━━━━━┷━━━━━━┷━━━━━━━━━┷━━━━━━━━┷━━━━━━━━━┷━━━━━━━━━

heim::net::io_counters:

> sys | get net | where sent > 0
───┬────────┬─────────┬──────────
 # │ name   │ sent    │ recv 
───┼────────┼─────────┼──────────
 0 │ tun0   │ 30.3 MB │ 653.4 MB 
 1 │ wlp3s0 │ 97.7 MB │   1.2 GB 
 2 │ lo     │ 97.1 MB │  97.1 MB 
───┴────────┴─────────┴──────────

heim::host::platform, heim::host::uptime, and heim::host::users:

> sys | get host
───┬───────┬────────────────┬──────────────────────────────────────────┬──────────┬────────┬────────────────────────────┬────────────────
 # │ name  │ release        │ version                                  │ hostname │ arch   │ uptime                     │ users 
───┼───────┼────────────────┼──────────────────────────────────────────┼──────────┼────────┼────────────────────────────┼────────────────
 0 │ Linux │ 5.4.15-arch1-1 │ #1 SMP PREEMPT Sun, 26 Jan 2020 09:48:50 │ tardis   │ x86_64 │ [row days hours mins secs] │ [table 1 rows] 
   │       │                │ +0000                                    │          │        │                            │  
───┴───────┴────────────────┴──────────────────────────────────────────┴──────────┴────────┴────────────────────────────┴────────────────

bottom

bottom (Yet another cross-platform graphical process/system monitor) uses heim to show system information in a really cool text user interface:

Zenith

zenith (sort of like top or htop but with histograms) uses heim to show system information in a really cool text user interface with histograms:

Screenshot