I have taken the code for my p910nd daemon and am rewriting it in Rust. One thing I discovered is trying to edit the C code to Rust is the wrong way to go. The languages differ so much in feel and ways of achieving results. For example a huge chunk of command line option handling in C can be simplified in Rust using the clap crate. Not only that but it autogenerates help responses for invocations.
However I also found it frustrating because I need to discover the equivalent for many C and Linux features. So not all the features have been ported. But it's also more difficult as p910nd is a system utility, and has to do things like run as a service, interact with system services like the logger. If it were only a data manipulation program, it would have been much easier.
Delving into the code
To make concrete the observations above, I present parts of my Rust code. First the main program, which is usually named src/main.rs in the project tree.
First I show the output of the help text if --help is given to the program. This shows what the options are:
$ cargo run -- --help Non-spooling printer daemon Usage: p910nd-rust [OPTIONS] [PRINTER_NUMBER] Arguments: [PRINTER_NUMBER] Printer number [default: 0] Options: -b, --bidir Bidirectional communication -d, --debug Log to stderr -f, --device Device to spool to [default: /dev/usb/lp0] -i, --bindaddr IP address to bind to [default: 0.0.0.0] -h, --help Print help -V, --version Print version
Now the main program, which is mostly an example of the use of the clap (command line argument parsing) crate. This is a popular module; other languages also have modules that encapsulate argument parsing.
extern crate clap; use clap::{Arg,Command}; extern crate log; use log::{info,error}; use std::process; extern crate p910nd; use p910nd::logger; fn main() { let matches = Command::new("P910nd") .version(env!("CARGO_PKG_VERSION")) .author("https://github.com/kenyapcomau/p910nd-rust") .about("Non-spooling printer daemon") .arg( Arg::new("bidir") .short('b') .long("bidir") .action(clap::ArgAction::SetTrue) .help("Bidirectional communication"), ) .arg( Arg::new("debug") .short('d') .long("debug") .action(clap::ArgAction::SetTrue) .help("Log to stderr"), ) .arg( Arg::new("device") .short('f') .long("device") .value_parser(clap::builder::NonEmptyStringValueParser::new()) .action(clap::ArgAction::Set) .default_value("/dev/usb/lp0") .value_name("DEVICE") .help("Device to spool to"), ) .arg( Arg::new("bindaddr") .short('i') .long("bindaddr") .value_parser(clap::builder::NonEmptyStringValueParser::new()) .action(clap::ArgAction::Set) .default_value("0.0.0.0") .value_name("BINDADDR") .help("IP address to bind to"), ) .arg( Arg::new("printer") .value_parser(clap::value_parser!(u32).range(0..9)) .default_value("0") .value_name("PRINTER_NUMBER") .help("Printer number"), ) .get_matches(); let bidir = matches.get_flag("bidir"); let debug = matches.get_flag("debug"); let device = matches.get_one("device").unwrap(); let bindaddr = matches.get_one("bindaddr").unwrap(); let pnumber: u32 = *matches.get_one("printer").expect("required"); logger::log_init(debug); info!("Run as server"); if let Err(e) = p910nd::server(pnumber, &device, bidir, bindaddr) { error!("{}", e); process::exit(1); } }
Instead of the #include mechanism of C, Rust uses safer indications to import modules. The crate p910nd in fact contains the body of the daemon code, which we will examine later.
Most of the work is to understand the options of clap to use it effectively. Also note that method chaining is heavily used. This is a technique also used in C++ and Java and relies on references in the language, a safer alternative to pointers. Returning a reference to the self object allows the result to be used to call the next method.
Finally note the use of the if let idiom to handle the case where the server function returns an Err.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.