Rust idiomatic way to collect and provide program configuration

Hi community,
after the Rust book and Rustlings tutorial I started my very first program and as an old OOP programmer an immediate code design question arises to me.

The task is not harder than providing the path to the OS specific data directory to different places within my code. I have chosen the directories crate for that, which has a neat interface for my requirement:

if let Some(proj_dirs) = ProjectDirs::from("com", "Foo Corp",  "Bar App") {
    proj_dirs.config_dir();
}

So the easiest and most chaotic way seems to create a variable mostly at the start of the code, which holds the returned &Path and pass that variable around:

let data_dir: &Path = ProjectDirs::from("comm", "foo", "bar")
        .expect("No valid home directory path could be retrieved from the operating system.")
        .data_dir();

My object oriented brain however tells me, to store and organize this configuration, as well as all future configurations, into a struct, which I create once and use it all over the code. I started with following first try:

use directories::ProjectDirs;
use std::path::Path;

pub struct ProcopConfig {
    pub data_dir: &Path,
}

impl ProcopConfig {
    pub fn new() -> ProcopConfig {
        let data_dir: &Path = ProjectDirs::from("comm", "foo", "bar")
            .expect("No valid home directory path could be retrieved from the operating system.")
            .data_dir();

        ProcopConfig { data_dir }
    }
}

Of course, this code has an error, the missing lifetime parameter for the data_dir fiel within my struct.

However fixing this error - as first time Rust programmer - I just run into more errors:

  • Adding the lifetime specifier 'static tells me, that the temporary value of ProjectDirs[...].data_dir() is dropped while borrowed. I can't figure out, how to create the &Path value within the new-method/constructor, without the values lifetime beeing ended.
  • I changed the value of the field to PathBuf. My innor monk told me, that Path is more suitable for static configuration than the PathBuf, which is meant to be passed around - and less efficient on the heap.

By struggling around solving my initial use case, I wondered what would be the best design, to create and organize some initial configuration for my program, which I than can use all over my program?

Do you have the Rust idiomatic answer for me? :slight_smile:

Thanks in advance for suggestions.

You can't, lifetimes just help the compiler (and the programmer) reason about lifetimes. You can't extend the lifetime of a borrow by annotating it with a longer lifetime, you'll just get an error saying the annotated lifetime is too long. In this case, you're borrowing from the ProjectDirs struct, so the borrow cannot extend beyond when the ProjectDirs struct is moved/dropped.

It's the reverse, borrows like &Path are suitable for temporarily giving access to the path to a function and such, while the owned PathBuf is what you should reach for when storing the path and moving it around .

Another option you have is to put the whole ProjectDirs struct in your config, but if you just need the one path then turning it into a PathBuf is a fine choice and idiomatic, so it seems like you're already heading in the right direction.

1 Like

If this global config is loaded only once and its contents never change during the lifetime of your program, it's reasonable to use an immutable static variable that's readable from anywhere in your program.

The once_cell crate can help you ensure the variable is initialized only once.

use directories::ProjectDirs;
use once_cell::sync::Lazy;
use std::path::PathBuf;

#[derive(Debug)]
pub struct ProcopConfig {
    pub data_dir: PathBuf,
}

impl ProcopConfig {
    fn new() -> Self {
        ProcopConfig {
            data_dir: ProjectDirs::from("comm", "foo", "bar")
                .expect(
                    "No valid home directory path could be retrieved from the operating system.",
                )
                .data_dir()
                .to_owned(),
        }
    }
}

// this can be used from any function
static CONFIG: Lazy<ProcopConfig> = Lazy::new(|| ProcopConfig::new());

fn main() {
    println!("{:#?}", &CONFIG.data_dir);
}
1 Like

Lifetime

Upon a cool spring day, Brother Sukim came upon a neophyte toiling beneath a grand cherry tree, it's branches festooned with beautiful pink blossoms.

"Ho!", Sukim cried. "If I might ask, what demands your attention so, surrounded as you are by such a sight?" he asked, reaching up and catching a falling petal from the air.

"Why, the sight itself," replied the neophyte, still hunched over his work, back turned. "I wish to discover the secret to immortality!"

Sukim raised an eyebrow. "Quite the endeavour. You wish to ensure you do not miss the blossoming seasons to come?"

"No, no," the neophyte said with a wave of his hand. "Not for me; for the blossoms!

"I was so taken by their beauty three days ago that I took some to decorate my room. But they quickly withered and lost their charm. I wish to find some way to extend the life of these blossoms indefinitely, that I might look on them as I please."

Sukim considered this. "Have you considered planting a cherry tree outside the window of your lodging?"

The neophyte finally turned to look at Sukim. "But that would take years before it bore fruit! Why, there is already a small garden and a paved path outside which I would need to dismantle and relocate first. Such a solution may appear straightforward, but would be supremely inconvenient!

"Not to mention the resources needed to nurture and grow another tree. Very disruptive. And why should I when I desire only the blossom?"

"And yet, without the rest of the tree, it's roots and branches, there would be no blossoms."

The neophyte shrugged. "The wood is of no interest to me. No, the best solution is to somehow extend the life of the blossoms beyond their severing from the rest."

Sukim pondered this for a moment before speaking. "As it happens, I believe I can help you understand what you must do. But first, you must come with me." With that, Sukim led the now enthusiastic neophyte to the workshop of the blacksmith. "Wait here whilst I speak to her," he told the neophyte.

The neophyte patiently waited whilst the two monks had a discussion, beyond his hearing. Eventually, the blacksmith and Sukim returned, carrying between them a wooden block and a large cleaver.

"Now," said Sukim, "you need to lay your neck on this block, and I shall cut your head off."

"You what?!" exclaimed the neophyte. "Why?"

Sukim casually picked at the edge of the blade, inspecting it. "To find the secrets of immortality, I shall need to lead you from here, through the snow drifts still clinging to the mountain pass, through the gardens of abstraction, and up the other slope until we reach the archives where the ancient and terrible Rustonomicon is kept, bound in chains. It is a long and difficult journey, full of peril and great tests of one's fortitude and resolve.

"Luckily, I have no need of your feet, or hands, or heart... I need only your head with your ears and mind to explain the answer to. I understand you might find this approach distasteful, but it truly is more convenient for me."

"But I would be too dead to learn anything!"

Sukim shrugged, testing the heft of the blade. "I wish for this approach to work, and so I shall find a way, somehow."

At that moment, the neophyte was enlightened.

"I should plant a cherry tree outside my window."

"Yes, that is what I would do," Sukim said, handing the blade back to the blacksmith. "All living things need the whole of themselves grounded somewhere, even if you yourself care only for a small part. The inconvenience of which shall, I hope, be outweighed by what you gain."

3 Likes

Somewhat related tangent. Rust is bringing once_cell types into the standard library with OnceCell and OnceLock stabilised and LazyCell and LazyLock being available for use on nightly.

Its always nice to see the efforts of the community being recognised in this manner and it means more users can be exposed to these incredibly useful constructs because of their inclusion in std.

Love to see it!

3 Likes

If it's a static value that's only created once, then why are you even worried about "efficiency"? It just doesn't matter. Period.

There's also no such thing as "less efficient on the heap" (whatever it is you mean by that expression, which doesn't in itself make any sense to me). Borrowed and owned types serve different purposes. If you try to use only one kind of type, you'll run into brick walls, and your code will be less efficient. (E.g. trying to exclusively use borrowed types will incur superfluous cloning on your values in situations when you could have just passed an owned value.)

3 Likes

Thanks for all your answers. You gave me enough confidence, to find a solutions that works for me.
The PathBuf solution should work well for me.
The lazy OnceCell sounds really interesting. I'll look further into it, once it's part of the stable std library.