General question regarding owned and borrowed variables (the next level)

Hi All,

I'm now in my third project (with large gaps though) using rust and I must say It's getting a little easier, I rarely get into long session where I don't understand what the compiler wants :slight_smile:.

That's not to say that I always have a clear path how to solve problems (even if I do understand why they happen). Let me illustrate a problem and the ways I can think of to handle it:

This is a sample of the real data structures:

pub struct Plugin<'a> {
    pub name: &'a str,
    pub dir: PathBuf,
    pub config: PluginConfig,
}

[...]

pub struct ExecutableContext<'a> {
    cmd_name: &'a str,
    plugin: Plugin<'a>,
    version: &'a str,
    tool_install_root: PathBuf,
}

Some of these structs (and other data structures) are required for different scenarios of app initialization. Several subcommands require the ExecutableContext in order to perform it's work. This is the most involved scenarios and initialization takes about 15 lines of code. However, I'm having problem creating a helper function that returns this struct because if I want to really save lines of code I'll have to define some variables inside this function and I will not be able to return a struct that references these variables as it out lives them ...

I can think of the following solutions:

  • Just copy paste these 15 lines several times. This is not that bad but every time I change the logic I'll have to apply everywhere.
  • Switch all the borrowed variables in the structs to owned. Again, this is not meaningful in terms of resources but I do want to learn how to work correctly with references.
  • Create a function that accepts a closure that accepts ExecutableContext as parameter and perform it's logic (or similar logic with macro).

Is there an obvious solution that I miss? Or a sophisticated way of handling references?

Thanks in advance

Haim

If you want to keep these as references, you need to have a better idea of what they’re borrowing from— Every borrowed value has an owner somewhere, and it’s usually best to be explicit about what that owner is.

In this case, you’re talking about a few relatively small strings. What is responsible for storing them and freeing their memory when they’re no longer needed?

You’ll need to pass a reference to some kind of data storage into this helper function so that the returned values contain references to it instead of to local variables. For example, if you’re parsing this information out of a file, you could pass an &’a str buffer to the parsing function, and have the resulting structures point to portions of the buffer.


Given these structures, I’d probably go with storing Strings instead of borrowed strs— The shared data is too small to worry about the memory duplication. It also doesn’t seem likely that you’ll construct these in a tight loop, so the computational cost of running the allocator isn’t that important either.

If you create new strings inside a function, then it's logically impossible to return &str. That's not Rust's limitation or tricky borrow checker syntax, that's just direct contradiction of what &str is for (it's for preexisting strings that are borrowed, not new strings that were created). In that case you need owned version of it, which is Box<str> or String.

In case you want to use the same struct for both cases where new strings are created, and where pre-existing strings are temporarily borrowed, then use Cow<'a, str> for these strings. It adds a boolean to the type keeping track whether that's a new string or an old one.

If you want to be maximally efficient, then you might use generic struct like Plugin<S: AsRef<str>> that allows storing the string as any string-like type, whether owned or borrowed.

2 Likes

Thanks, this is what I was going for when I started the project. It's just that as the project got a little bigger it takes several steps to create this context for the application. In most cases It doesn't take more then 5 lines of setup code but there are a few cases where the setup is more involved.

This is certainly the simplest choice. I do have an urge to go with the functional approach of a function that accepts lambda but we'll see :slight_smile:. Thanks.

Thanks, these are some interesting ideas. While probably not needed in my little project I'll keep it in mind for larger projects.

Ok, I decided on the functional solution:

pub fn run_with_executable_context<F, T>(env: &RuntimeEnvironment, cmd: &str, func: F) -> Result<T>
where
    F: FnOnce(ExecutableContext) -> Result<T>
{
    // ...
    // steps to create ExecutableContext
    let ec = ExecutableContext::new(&cmd_name, plugin, &version, &env.installs_dir).unwrap();
    func(ec)
}

So now I can call it like this:

pub fn execute_command<'a, I, S>(env: &RuntimeEnvironment, cmd: &str, args: I) -> Result<i32> 
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr>, 
{
    let func = |ec: ExecutableContext| {
        let command = ec.mk_command(args).ok_or_else(|| anyhow!("Command {} does not exist in {} version {}", ec.cmd_name, ec.plugin.name, ec.version))?;
        exec(command)
    };
    run_with_executable_context(env, cmd, func)
}

Thanks for all the ideas.