0

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?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
springworks00
  • 104
  • 15
  • Are you sure you want an additional compilation step? Your title and first paragraph makes it seem like a static variable is the solution, so I just want to be sure. Code generation can be useful but using it just for collecting command line arguments doesn't make sense to me unless you're doing *a lot* of pre-processing from them. (or you just want to avoid passing them in the final program) – kmdreko Aug 24 '20 at 22:44
  • The command line arguments are just to minimize the question complexity. I'm actually parsing large files with complicated syntax and would like to save the content in custom data structures for later. So yes, I would prefer code generation for this situation, even if it involves extra compilation. – springworks00 Aug 24 '20 at 23:01
  • Your question may be answered by the answers of [How to create a static string at compile time](https://stackoverflow.com/a/32956193/155423). If not, please **[edit]** your question to explain the differences. Otherwise, we can mark this question as already answered. – Shepmaster Aug 25 '20 at 01:46
  • 1
    This question is **not** a dup of "How to create a static string at compile time" because here the OP is looking for a serde-like serialization facility that outputs Rust source, not just creating any old static string. The question is quite clear about that. – user4815162342 Aug 25 '20 at 11:57
  • I would definitely suggest using [serde](https://serde.rs/) to save your data as json or [whatever](https://serde.rs/#data-formats) and simply load it later. You can use [include_str!](https://doc.rust-lang.org/std/macro.include_str.html) to embed it in your final program if you still want to do that. Perhaps some day we'll get data-to-code generation like you describe. – kmdreko Aug 26 '20 at 17:38

1 Answers1

0

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,
                    },
                ),
            },
        ),
    },
)
Lucretiel
  • 3,145
  • 1
  • 24
  • 52