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
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.