Anatomy of a Rust Program, Part II: The main() Function in /src/main.rs

This blog post is the second in a series that describes the structure of the wink command line program that I have written in rust and that I use to invoke other Windows and Linux programs from bash and Windows shells under Windows Subsystem for Linux. This post describes the main() function in the /src/main.rs file. The main function typically passes any configuration from the environment, the command line, and elsewhere to a function that implements the program.

Comments that begin with //! will appear in documentation generated by cargo. Code comments begin with // or we can use /*comment*/.

The main() function does not accept any parameters or return any value. It can use the args() function under the std::env path (part of rust standard libraries) to retrieve command line arguments passed to the wink command. It can use the std::process::exit() function to return a value to the operating system and exit the program.

This example uses a wink::wsl::inv::invocablecategorylist::InvocableCategoryList named category_list defined by the InvocableCategoryList struct in the /src/wsl/invocablecategorylist.rs file to retrieve some configuration data hard-coded elsewhere in the program and from a JSON file, if that file exists. This custom InvocableCategoryList struct contains a list of categories, each of which has a name and contains a list of Invocables that each contain metadata about a command that the system can invoke.

Sometimes I prefer to be explicit, especially for demonstrative purposes, but to avoid repeating long paths in code, this code could begin with a statement such as the following line.

use wink::wsl::inv::invocablecategorylist::InvocableCategoryList;

The main() function then uses the crate::winkconfig::WinkConfig::new() function to instantiate a crate::winkconfig::WinkConfig struct defined in /src/winkconfig.rs from the command line arguments.

fn main() {
    // get a list of categories containing invocable commands
    // defined in /wsl/inv/invocablecategorylist.rs
    let category_list = wink::wsl::inv::invocablecategorylist::InvocableCategoryList::get();
    let config = wink::winkconfig::WinkConfig::new(std::env::args().collect()); 

The std::process::exit() function returns an exit code value to the operating system and ends the program. That value is the result of a match operation against the WinkConfig structure. In fact, calling WinkConfig::new(), where new is just a naming convention for a function associated with the WinkConfig struct, did not return a WinkConfig struct. It returned a Result, which is an enumeration, where enumerations can contain values. The compiler just hides a bit of these details for us. In the case of Result, there are two members of the enumeration: Ok, which the run() function returns in the case of success, and Err(), which it returns if an error occurs.

    std::process::exit(match config {
        Ok(config) => wink::run(config, category_list), // what if this raises an error
        Err((config, e)) => wink::help(&e.to_string(), config, category_list.categories),
    });
}

The Ok() and Err() enumerations each contain values determined by the WinkConfig::new() function. The Ok() enumeration contains the WinkConfig that you might have expected the new() function to return, but which the compiler places in Ok(). The Err() enumeration contains a tuple (I think) that contains the same WinkConfig, but with an std::error::Error, which holds data about an error. I generally avoid tuples in favor of explicit types, but they can be useful for binding things together, instrumentation, and error management at a high level, and can avoid unnecessary simple type definitions at any level. Incidentally, I also avoid functions that return multiple values and think that wrapping errors and even multiple values in an enumeration may be an optimal solution.

It is worth noting that because this program does not expect the wink::run() function in /src/lib.rs to panic (crash the program), it does not trap its potential errors, for which the operating system should provide error handling superior to any that this program could perform. Under memory constraints and other system problem conditions, the program should terminate rather than launching more programs or otherwise consuming resources.

The if the Result returned by WinkConfig::new() is the Ok() enumeration, then it contains the WinkConfig. The => operator passes that WinkConfig to wink::run() as the config argument, along with the category_list retrieved previously. The comma ends that condition of the match expression.

Along with the WinkConfig and the list of categories from category_list, the Err() enumeration passes the string value of the std::error::Error that it contains, which should be an instance wink::helperror::HelpError struct defined in /src/helperror.rs, to the wink::help function defined in /lib.rs.

Even though /src/main.rs is the root of a binary (executable) crate and /src/lib.rs is the root of a library crate in the same package, at least for command line tools and background processes rather than libraries, I think that it is appropriate to think them together as defining a single type, where main() has minimal responsibilities, primarily for retrieving data from the environment including the command line.

One thought on “Anatomy of a Rust Program, Part II: The main() Function in /src/main.rs

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: