Terminal Window Size With Rust FFI
I was writing some code in Rust and wanted to get the size of my terminal. This is currently not implemented in Rust though. I decided to read up on The Foreign Function Interface Guide to figure out how to do it myself. The Foreign Function Interface (FFI) is how Rust code interfaces with native C code. I also found a great Stack Overflow post that showed me how to write native C to get the terminal size. Based on my research, I needed to do three things in order to get my terminal size:
- Create a
winsize
struct in Rust. - Use or externalize the
ioctl
C function. - Use or externalize the
STDOUT_FILENO
andTIOCGWINSZ
constants.
Winsize Struct
Creating the winsize
struct in Rust is pretty straight forward as Rust has structs too. I first needed to find the definition of winsize
in C, so I did some googling and found the sys/ioctl.h source. When defining the struct, we must tell Rust to represent the struct as a C struct using #[repr(C)]
. If you read the FFI Guide, then you may be wondering about #[repr(C, packed)]
. I talk about packing in more detail at the end of the post. The struct members within winsize
are all unsigned short
. The C unsigned short
is represented in Rust as c_ushort
in the libc
Rust module. We now have:
use libc::c_ushort;
#[repr(C)]
struct winsize {
ws_row: c_ushort, /* rows, in characters */
ws_col: c_ushort, /* columns, in characters */
ws_xpixel: c_ushort, /* horizontal size, pixels */
ws_ypixel: c_ushort /* vertical size, pixels */
}
ioctl
Now I need to figure out what to do about the ioctl
function. Checking out the Rust docs leads me to the ioctl function signature but I notice that this signature does not look like a variadic function (no varargs). I guess I have to externalize it in my code as a variadic function. I decided to check the Rust source to see if I could find an example of a variadic function and I stumbled in the definition of ioctl. This definition is variadic, so I guess rustdoc does not show this. Strange.
I have read that ws_xpixel
and ws_ypixel
are not used. I also have no use for them. I still opted to include them in my struct definition as I have no idea what ioctl
is doing to that struct.
I have used this word externalized a few times already, so maybe I should now define it. To externalize something is to make that somethings C representation accessible to Rust code. You normally do this with function signatures, constants and global variables. Note that we did not externalize winsize
, but instead copied the definition from C to Rust. We cannot externalize winsize
as Rust needs to directly manage the definition and memory related to that struct.
The Constants
Finally, I need to deal with my constants. I was pretty sure STDOUT_FILENO
would already be in Rust. Sure enough, libc::STDOUT_FILENO
exists. I was not so lucky with TIOCGWINSZ
. The TIOCGWINSZ
constant acts as a command to ioctl
. If you read the source of sys/ioctl.h
, you will notice the value of the commands is based on some rules that encode information to ioctl
. There is a fair amount of bit twiddling going on to generate these values. Even if we do the bitwise math by hand, we should still check our work. To do that, I wrote a simple C program that would tell us the proper hex value of TIOCGWINSZ
:
#include <sys/ioctl.h>
#include <stdio.h>
#include <unistd.h>
int main (int argc, char **argv)
{
printf("0x%x", TIOCGWINSZ);
return 0;
}
Using this value I can create the same constant in Rust:
const TIOCGWINSZ: c_ulong = 0x40087468;
Putting It All Together
My function for get_winsize
now looks like this:
fn get_winsize() -> IoResult<(isize, isize)> {
let w = winsize { ws_row: 0, ws_col: 0, ws_xpixel: 0, ws_ypixel: 0 };
let r = unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) };
match r {
0 => Ok((w.ws_col as isize, w.ws_row as isize)),
_ => {
return Err(standard_error(ResourceUnavailable))
}
}
}
I initialize my variable containing a winsize
struct with values of zero, just like I would memset(w, 0, sizeof(winsize))
in C. In order to use the externalized ioctl
function, we have to wrap the code in unsafe {}
blocks. This informs Rust this code is not to be checked by the compiler for safety. The ioctl
function follows the C convention of returning a 0
for success and a -1
for an error. If an error occurs, I decided to throw an existing IoResult
error already in Rust. I need to spend a little more time to externalize the errno
global variable in C so I can get the exact error. If the function is successful, I return the width and height as a tuple.
Here is a gist of the complete program, including a simple test. This puts all the peices discussed above together and will properly calculate the terminal window size when executed.
To Pack Or Not
If you see a struct defined with __attribute__((__packed__))
then you need to use #[repr(C, packed)]
. Example:
```c` struct attribute((packed)) foo { char first; int second; };
A packed C struct, usually only found in kernel development, is not _padded_. If you are not familiar with _padding_ in C, then you may not understand what `#[repr(C, packed)]` does. When defining a struct in C, the struct members are aligned to _word boundaries_. A _word_ is the natural address boundary for a given architecture. For example, on a 32-bit machine a word is 4 bytes. If a struct member does not align to a word boundary, the compiler will insert padding after the variable. A struct like
```c
struct foo {
char first; // 1 byte
int second; // 4 bytes
};
is not 5 bytes in size, but 8 bytes due to padding. Here is how the same struct looks after the compiler has added padding:
struct foo {
char first; // 1 byte
char padding[3]; // 3 bytes
int second; // 4 bytes
};
I found a Stack Overflow post the explains it in even greater detail. Also, check out the Data Structure Alignment article on Wikipedia.