Imran's Blog
Stuff I feel like blogging about.


My first time using rust

Posted on

Replacing one of my shell scripts with a compiled binary using rust.

I like to have the systems uptime (or more specifically load average) displayed in status line in tmux. The reason for this is to just check if I have a rogue process or script that is eating too much CPU and uptime is a quick and easy way to determine that.

I had a little script called uptime.bash that just pulled out the information I needed from the uptime

#!/bin/bash

uptime | awk {'y=$(NF-2) $(NF-1) $(NF); gsub(/,/, " ", y); print y'}

I placed this shell script in my home directory and appended the following to my tmux configuration:

set -g status-right '#[fg=colour0,bg=colour112] #(~/uptime.bash) #[fg=colour0,bg=colour72] %H:%M #[fg=colour0,bg=colour48] %Y-%m-%d '

and this ends up getting me a nice little status bar like so:

tmux status line

Now I've been wanting to use rust for quite some time, but there wasn't anything I wanted to build. I thought why not replace this. In its current iteration it ends up spawning a new shell which then starts up the uptime process then pipes the result into awk which then thus further processing all to just show me my load averages. That to me seemed quite wasteful for something that is replaceable with a single syscall (and let's be real this is just an excuse to build something with rust).

# The program

All the information I need for the program could found with man sysinfo

SYSINFO(2)                                                                                                                    Linux Programmer's Manual                                                                                                                   SYSINFO(2)

NAME
       sysinfo - return system information

SYNOPSIS
       #include <sys/sysinfo.h>

       int sysinfo(struct sysinfo *info);

DESCRIPTION
       sysinfo() returns certain statistics on memory and swap usage, as well as the load average.

       Until Linux 2.3.16, sysinfo() returned information in the following structure:

           struct sysinfo {
               long uptime;             /* Seconds since boot */
               unsigned long loads[3];  /* 1, 5, and 15 minute load averages */
               unsigned long totalram;  /* Total usable main memory size */
               unsigned long freeram;   /* Available memory size */
               unsigned long sharedram; /* Amount of shared memory */
               unsigned long bufferram; /* Memory used by buffers */
               unsigned long totalswap; /* Total swap space size */
               unsigned long freeswap;  /* Swap space still available */
               unsigned short procs;    /* Number of current processes */
               char _f[22];             /* Pads structure to 64 bytes */
           };

       In the above structure, the sizes of the memory and swap fields are given in bytes.

       Since Linux 2.3.23 (i386) and Linux 2.3.48 (all architectures) the structure is:

           struct sysinfo {
               long uptime;             /* Seconds since boot */
               unsigned long loads[3];  /* 1, 5, and 15 minute load averages */
               unsigned long totalram;  /* Total usable main memory size */
               unsigned long freeram;   /* Available memory size */
               unsigned long sharedram; /* Amount of shared memory */
               unsigned long bufferram; /* Memory used by buffers */
               unsigned long totalswap; /* Total swap space size */
               unsigned long freeswap;  /* Swap space still available */
               unsigned short procs;    /* Number of current processes */
               unsigned long totalhigh; /* Total high memory size */
               unsigned long freehigh;  /* Available high memory size */
               unsigned int mem_unit;   /* Memory unit size in bytes */
               char _f[20-2*sizeof(long)-sizeof(int)];
                                        /* Padding to 64 bytes */
           };

       In the above structure, sizes of the memory and swap fields are given as multiples of mem_unit bytes.

RETURN VALUE
       On success, sysinfo() returns zero.  On error, -1 is returned, and errno is set to indicate the cause of the error.

If I was using C, this would be as easy as including that header file, creating and empty sysinfo struct and passing its ref into the sysinfo function which would populate the loads array with information I needed.

Rust should be able to call out to C code (and by proxy the kernel sysinfo call) as far as I am aware. Turns out the rust team keeps a crate around for this purpose called libc

There also is a crate called sysinfo that does what I need, but where's the fun in that?

First I needed to uninstall the version of rust that I procured through my package manager as that includes rustc and not any of the other tooling, i.e. cargo, rustmt, etc.

Just had to follow the guide here: https://doc.rust-lang.org/cargo/getting-started/installation.html and get started.

cargo new uptime_r --bin

.
├── Cargo.toml
└── src
    └── main.rs

I needed to add libc to my Cargo.toml file

[package]
name = "uptime_r"
version = "0.1.0"

[dependencies]
libc = "0.2.79"

Next I needed to actually use the crate in my actual code

extern crate libc;

use libc::sysinfo;

fn main() {
    let mut sysinfo_data = sysinfo { 0 };

    unsafe {
        sysinfo(&mut sysinfo_data);
    };

    println!(
        "{:.2} {:.2} {:.2}", sysinfo_data.loads[0] sysinfo_data.loads[1] sysinfo_data.loads[2]
    );
}

This resulted in the following from cargo run:

   Compiling uptime_r v0.1.0 (/home/imran/code/dotfiles/tmux/uptime_r)
error: expected identifier, found `0`
 --> src/main.rs:8:38
  |
8 |     let mut sysinfo_data = sysinfo { 0 };
  |                            -------   ^ expected identifier
  |                            |
  |                            while parsing this struct

error[E0063]: missing fields `_f`, `bufferram`, `freehigh` and 11 other fields in initializer of `libc::sysinfo`
 --> src/main.rs:8:28
  |
8 |     let mut sysinfo_data = sysinfo { 0 };
  |                            ^^^^^^^ missing `_f`, `bufferram`, `freehigh` and 11 other fields

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0063`.
error: could not compile `uptime_r`.

To learn more, run the command again with --verbose.

What a breath of fresh air! I've been spending most of my time working in python (and now ruby) professionally and having a compiler yell at me was something that was sorely missing from my life.

Turns out in rust you need to declare the value of every single field in a struct. (AFAIK) the accepted practice is to add a Default trait to your struct that initializes it for you. In this case since I am using sysinfo from an external crate I can not add a trait to it. I could make a wrapper struct around it but that seems a bit much for what I want to do. With the help of the compiler I filed out the remaining fields:

    let mut sysinfo_data = sysinfo {
        uptime: 0,
        loads: [0; 3],
        totalram: 0,
        freeram: 0,
        sharedram: 0,
        bufferram: 0,
        totalswap: 0,
        freeswap: 0,
        procs: 0,
        pad: 0,
        totalhigh: 0,
        freehigh: 0,
        mem_unit: 0,
        _f: [0; 0],
    };

And we have lift off!

cargo run

   Compiling uptime_r v0.1.0 (/home/imran/code/dotfiles/tmux/uptime_r)
    Finished dev [unoptimized + debuginfo] target(s) in 0.21s
     Running `target/debug/uptime_r`
80608 67808 65888

Hang on. These numbers don't make any sense 80608 67808 65888. These are not as nice as what I had in my earlier screen shot.

Turns out the kernel does some shifting to get these floating values into a long int representation for us. After much googling (and thanks to this) turns out there is a special number which you need to use to multiply loads by and this is the SI_LOAD_SHIFT value which libc declares for us.

To use the value I had to do:

const LINUX_SYSINFO_LOADS_SCALE: f64 = (1 << libc::SI_LOAD_SHIFT) as f64;

...

    println!(
        "{:.2} {:.2} {:.2}",
        sysinfo_data.loads[0] as f64 / LINUX_SYSINFO_LOADS_SCALE,
        sysinfo_data.loads[1] as f64 / LINUX_SYSINFO_LOADS_SCALE,
        sysinfo_data.loads[2] as f64 / LINUX_SYSINFO_LOADS_SCALE,
    );

And now I have my nice load average values:

cargo run
   Compiling uptime_r v0.1.0 (/home/imran/code/dotfiles/tmux/uptime_r)
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/uptime_r`
1.26 1.07 1.00