Why I Chose the libc Crate for My Text Editor

Lately I've been working on a text editor as a learning exercise in how to build terminal apps from scratch. In doing so I had to draw a line somewhere as to what “from scratch” actually meant. After all, I don't intend to invent the universe.

My initial plan was to restrict myself to Rust's standard library, but it quickly became clear that wasn't going to be sufficient. Specifically, this project requires the ability to change the terminal mode and handle signals, both of which require platform specific code to do manually. I think it's fair to say writing separate blocks of inline assembly for every architecture/operating system pair I want to support is totally out of scope for this project.

Ultimately I decided that the libc crate was a reasonable foundation to build off. libc is the standard library for the C programming language, but it's also part of the POSIX specification. This means it's guaranteed to be available on pretty much any OS except for Windows, and is also going to be the lowest level of abstraction possible that doesn't require writing platform specific code.

Another good choice would be the nix crate, which provides the same functionality as the libc crate, but without requiring the user to write any unsafe code. Ultimately I decided that dealing with the unsafe code myself was a reasonable part of the “from scratch” experience. Moreover, the libc crate is part of the rust-lang project, so it doesn't introduce a dependency on a third party.

Unfortunately the POSIX specification has some wiggle room, so portions of libc still differ between operating systems. Up until today I had only tried compiling the editor on Linux, and I was curious if it would build under FreeBSD. Unfortunately I was greeted with errors.

   Compiling takkun v0.1.0 (/usr/home/cameron/workspace/takkun)
error[E0308]: mismatched types
  --> src/terminal.rs:27:11
   |
27 |     c_cc: [0; 32],
   |           ^^^^^^^ expected an array with a fixed size of 20 elements, found one with 32 elements

error[E0560]: struct `termios` has no field named `c_line`
  --> src/terminal.rs:30:5
   |
30 |     c_line: 0,
   |     ^^^^^^ `termios` does not have this field
   |
   = note: available fields are: `c_iflag`, `c_oflag`, `c_cflag`, `c_lflag`, `c_cc` ... and 2 others

Some errors have detailed explanations: E0308, E0560.
For more information about an error, try `rustc --explain E0308`.
error: could not compile `takkun` due to 2 previous errors

Right away it's clear that the termios struct isn't the same of FreeBSD as it is under Linux. Specifically, the c_cc field seems to be a different size and the c_line field only exists in Linux. So I'm going to need a way to conditionally initialize the termios struct depending on what OS I'm on.

Luckily Rust has a way to do this that even works when initializing static items. The cfg attribute can be used to conditionally include code, allowing me to do the following:

#[cfg(any(target_os = "linux"))]
static mut TERMIOS: libc::termios = libc::termios {
    c_iflag: 0,
    c_oflag: 0,
    c_cflag: 0,
    c_lflag: 0,
    c_cc: [0; 32],
    c_ispeed: 0,
    c_ospeed: 0,
    c_line: 0,
};

#[cfg(any(target_os = "freebsd"))]
static mut TERMIOS: libc::termios = libc::termios {
    c_iflag: 0,
    c_oflag: 0,
    c_cflag: 0,
    c_lflag: 0,
    c_cc: [0; 20],
    c_ispeed: 0,
    c_ospeed: 0,
};

And that was all it took. If I had also needed to write some OS specific logic, Rust provides the cfg! macro that can be used in boolean expressions. It's not a perfect out-of-the-box solution, but it's trivial compared to triggering system calls in pure Rust.