Rust and ioctl
When I explore new programming language, I like to poke audio. Rust is a new language for me and I
need a pet project to learn. As I’m also using FreeBSD, that means audio device configuration is
done via ioctl
. Here is the summary of how to handle ioctl
in Rust. Create the project, add
nix
crate enabling ioctl
feature.
$ cargo new oss
$ cd oss
$ cargo add --features ioctl nix
For now I will just exclude AudioInfo
from the code and show you the rest. The following is an
example of how to handle two ioctl
calls in Rust. One writes integer and one reads structure.
Ignore for now that logicaly, setting channels before getting information about hardware is wrong.
I wanted to show i32
version first, struct later.
use nix::libc;
use std::fs;
use std::os::fd::AsRawFd;
const SNDCTL_DSP_MAGIC: u8 = b'P';
const SNDCTL_DSP_CHANNELS: u8 = 6;
nix::ioctl_readwrite!(oss_channels, SNDCTL_DSP_MAGIC, SNDCTL_DSP_CHANNELS, i32);
const SNDCTL_INFO_MAGIC: u8 = b'X';
const SNDCTL_ENGINEINFO: u8 = 12;
nix::ioctl_readwrite!(
oss_audio_info,
SNDCTL_INFO_MAGIC,
SNDCTL_ENGINEINFO,
AudioInfo
);
fn main() {
let devpath = String::from("/dev/dsp");
let dsp = fs::File::open(devpath).unwrap();
let fd = dsp.as_raw_fd();
let mut channels: i32 = 2;
let mut audio_info = AudioInfo::new();
unsafe {
oss_channels(fd, &mut channels).expect("Failed to set number of channels");
oss_audio_info(fd, &mut audio_info).expect("Failed to get info on device");
}
println!("channels = {}", audio_info.max_channels);
println!("rate = {}", audio_info.max_rate);
}
Let’s just concentrate on the three lines after use
block. How did I know what values to use? For
start you have to look in /usr/src/sys/sys/soundcard.h
(assuming that’s where your FreeBSD source
tree is). Let’s take a look.
#define SOUND_PCM_WRITE_CHANNELS _IOWR('P', 6, int)
#define SNDCTL_DSP_CHANNELS SOUND_PCM_WRITE_CHANNELS
As the call is _IOWR
I know I have to use ioctl_readwrite
macro, and it’s obvious where P
and
6
come from. As the third argument to _IOWR
is int
I know I have to use i32
as fourth
argument to ioctl_readwrite
. Also, the first argument to that macro is the function name that is
generated and used later in the code.
For getting information about underlaying hardware, OSS uses ioctl
with a struct. Let’s see how
it’s defined in the FreeBSD source
#define SNDCTL_ENGINEINFO _IOWR('X',12, oss_audioinfo)
From this I know I have to use ioctl_readwrite
, X
for MAGIC number and 12
for the argument.
The last one is to figure out how to work with the struct oss_audioinfo
. Here’s that structure.
typedef char oss_longname_t[64];
typedef char oss_label_t[16];
typedef char oss_devnode_t[32];
typedef struct oss_audioinfo
{
int dev;
char name[64];
int busy;
int pid;
int caps;
int iformats;
int oformats;
int magic;
char cmd[64];
int card_number;
int port_number;
int mixer_dev;
int legacy_device;
int enabled;
int flags;
int min_rate;
int max_rate;
int min_channels;
int max_channels;
int binding;
int rate_source;
char handle[32];
unsigned int nrates;
unsigned int rates[20];
oss_longname_t song_name;
oss_label_t label;
int latency;
oss_devnode_t devnode;
int next_play_engine;
int next_rec_engine;
int filler[184];
} oss_audioinfo;
The trick in Rust is to use #[repr(C)]
, libc::c_int
and other libc::c_*
types. If you put
this right below use
block, you have the whole code.
#[repr(C)]
struct AudioInfo {
pub dev: libc::c_int,
pub name: [libc::c_char; 64],
pub busy: libc::c_int,
pub pid: libc::c_int,
pub caps: libc::c_int,
pub iformats: libc::c_int,
pub oformats: libc::c_int,
pub magic: libc::c_int,
pub cmd: [libc::c_char; 64],
pub card_number: libc::c_int,
pub port_number: libc::c_int,
pub mixer_dev: libc::c_int,
pub legacy_device: libc::c_int,
pub enabled: libc::c_int,
pub flags: libc::c_int,
pub min_rate: libc::c_int,
pub max_rate: libc::c_int,
pub min_channels: libc::c_int,
pub max_channels: libc::c_int,
pub binding: libc::c_int,
pub rate_source: libc::c_int,
pub handle: [libc::c_char; 32],
pub nrates: libc::c_uint,
pub rates: [libc::c_uint; 20],
pub song_name: [libc::c_char; 64],
pub label: [libc::c_char; 16],
pub latency: libc::c_int,
pub devnode: [libc::c_char; 32],
pub next_play_engine: libc::c_int,
pub next_rec_engine: libc::c_int,
pub filler: [libc::c_int; 184],
}
Although nobody can give you formula on how to write ioctl code in Rust based on C but there are some guidelines. Based on C code, you can tell which macro it uses so here’s a rough table.
C | Rust |
---|---|
_IO | ioctl_none |
_IOR | ioctl_read |
_IOW | ioctl_write_* |
_IOWR | ioctl_readwrite |
For a list of ioctl_write_*
macros and full documentation refer to
https://docs.rs/nix/latest/nix/sys/ioctl. I have to say it is easy to dive into something like this
when you have one of the Rust advocates in FreeBSD
as a work colleague, as he was really quick to spot my errors and generally guide me to a working
code as a newbie Rustacean.