'How do I persist a struct instance across files?

I want to persist the contents of a struct created in one Rust file into a different Rust file. In this minimal reproduction, command line arguments are used to build a recursive data structure:

// main.rs

struct Foo {
    a: i32,
    b: Option<Box<Foo>>,
}

fn main() {
    let args: Vec<i32> = std::env::args()
        .map(|s| s.parse::<i32>().unwrap())
        .collect();

    let mut my_data = Foo { a: 0, b: None };

    for arg in args {
        my_data = Foo {
            a: arg,
            b: Some(Box::new(my_data)),
        };
    }
    //generate output.rs from my_data
}

I want to generate an output.rs file that allows me to use my_data (as it was built in main.rs) sometime later. If I run rustc main.rs 5 4 then output.rs should look like this:

// output.rs

struct Foo {
    a: i32,
    b: Option<Box<Foo>>,
}

fn main() {
    let my_data = Foo {
        a: 4,
        b: Some(Box::new(Foo {
            a: 5,
            b: Some(Box::new(Foo { a: 0, b: None })),
        })),
    };
    // Do stuff with my_data
}

This way, I have accomplished one part of the computation already (getting the command line arguments) and can save the remainder of the computation for later.

Is there a crate or a macro-related solution to accomplish this?



Solution 1:[1]

What you're looking for is called a build.rs build script. Build scripts are used for more complex compile-time code generation than macros are capable of. For instance, my own makepass password generator uses a build script to handle turning a words dictionary into a rust source file, so that the words can be built directly into the binary.

For your specific use case, you might do something like this. Note that the build script typically lives in the project root, rather than the src directory.

// build.rs

use std::{
    env,
    fmt::{self, Display, Formatter},
    fs::File,
    io::{BufWriter, Write},
    path::Path,
};

fn main() {
    // your original code used `std::env::args`. It's not possible to pass
    // command line arguments to a build script, so instead I'm using an
    // environment variable called LIST, containing a space-separated list
    // of ints
    let list: Vec<i32> = env::var("LIST")
        .expect("no variable called LIST")
        .split_whitespace()
        .map(|item| item.parse().expect("LIST contained an invalid number"))
        .collect();

    // When generating rust code in build.rs, use the OUT_DIR variable as a
    // directory that contains it; cargo uses it to help preserve generated
    // code (so that it's only regenerated when necessary)
    let path = env::var("OUT_DIR").expect("no variable called OUT_DIR");
    let path = Path::new(&path).join("output.rs");
    let output_file = File::create(&path).expect("Failed to create `output.rs`");
    write!(BufWriter::new(output_file), "{}", FooWriter { data: &list })
        .expect("failed to write to output.rs");

    // This directive informs cargo that $LIST is a build-time dependency, so
    // it rebuilds this crate if the variable changes
    println!("cargo:rerun-if-env-changed=LIST");
}


// This struct is used to implement the recursive write that's required
struct FooWriter<'a> {
    data: &'a [i32],
}

impl Display for FooWriter<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self.data.split_first() {
            None => write!(f, "None"),
            Some((item, tail)) => {
                write!(
                    f,
                    "Some(Box::new(Foo {{ a: {}, b: {} }}))",
                    item,
                    FooWriter { data: tail }
                )
            }
        }
    }
}
// src/main.rs
#[derive(Debug)]
struct Foo {
    a: i32,
    b: Option<Box<Foo>>,
}

fn main() {
    // include! performs a textual include of a file at build time.
    //   the contents of the file are loaded directly into this
    //   source file, as though via copy-paste.
    // concat! is a simple compile time string concatenation
    // env! gets the value of an environment variable at compile
    //   time and makes it available as an &'static str
    let my_data = include!(concat!(env!("OUT_DIR"), "/output.rs"));

    println!("{:#?}", my_data)
}

This program will fail to build if the LIST environment variable doesn't exist at build time. Here's how it looks when LIST does exist:

$ env LIST="1 2 3" cargo build
   Compiling rust-crate v0.1.0 (/Users/nathanwest/Documents/Repos
    < ... warnings ... >
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s

$ ./target/debug/rust-crate
Some(
    Foo {
        a: 1,
        b: Some(
            Foo {
                a: 2,
                b: Some(
                    Foo {
                        a: 3,
                        b: None,
                    },
                ),
            },
        ),
    },
)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Lucretiel