Embed list of values in Rust application

const include! concat! env! OUT_DIR build

In the previous example we saw how to embed a simple string in our code that is stored in an external file.

What if the data we would like to maintain is more complex? What if it is a list of strings? For example a list of words to be used in a Wordle game?

What if our data looks like this:

examples/crate-build/data/animals.txt

cow
pig
cat
dog

We still would like to maintain this data in a separate, code-less file to make it easy for non-programmers to edit without any fear of breaking the code or even seeing code.

Embedding

We could embed it as a string and then when the program starts we could split it up into a vector, but that would mean we store the data both in the code and then in the heap. A waste of memory.

I found a much better solution in the names crated by Fletcher Nichol.

It uses a build script to convert the text file into a rust file that will be converted to an rs-file during the build process. Then it is loaded in a const It uses the OUT_DIR from the environment variables available during build.

The way this work is that when cargo builds the executable of the crate, the first step is to compile and run the content of the build.rs file located in the root of the project.

The build script

examples/crate-build/build.rs

use std::env;
use std::fs::File;
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let out_dir = env::var("OUT_DIR")?;
    let out_dir = Path::new(&out_dir);
    let src_dir = Path::new("data");

    generate(src_dir.join("animals.txt"), out_dir.join("animals.rs"))?;
    Ok(())
}

fn generate(src_path: impl AsRef<Path>, dst_path: impl AsRef<Path>) -> io::Result<()> {
    let src = BufReader::new(File::open(src_path.as_ref())?);
    let mut dst = BufWriter::new(File::create(dst_path.as_ref())?);

    writeln!(dst, "[")?;
    for word in src.lines() {
        writeln!(dst, "\"{}\",", &word.unwrap())?;
    }
    writeln!(dst, "]")
}

This takes the animals.txt file from the data folder of the project and converts it into a file called animals.rs in the OUT_DIR.

When I executed cargo run it was ./target/debug/build/crate-build-29933f5e206e748f/out/animals.rs.

When I executed cargo build --releases it was in ./target/release/build/crate-build-185456e02760ac01/out/animals.rs.

(crate-build is the name of the crate I am using for this examples)

The animals.rs file looked like this:

[
"cow",
"pig",
"cat",
"dog",
]

The embedding

examples/crate-build/src/main.rs

pub const ANIMALS: &[&str] = &include!(concat!(env!("OUT_DIR"), "/animals.rs"));

fn main() {
    println!("{:?}", ANIMALS);
    for animal in ANIMALS {
        println!("{}", animal);
    }
}

We used the following macros:

Running this code as cargo run we get the following output:

["cow", "pig", "cat", "dog"]
cow
pig
cat
dog

We can also build the executable with cargo build --release then we can move the generated executable (target/release/crate-build in our case) to any other place and run it. We'll get the same results.

Related Pages

Embedding simple CSV file in Rust application
Embedding simple CSV file in Rust application

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo