Rust: initialize a static variable/reference in a lib?

3.3k Views Asked by At

I'm new to Rust. I'm trying to create a static variable DATA of Vec<u8> in a library so that it is initialized after the compilation of the lib. I then include the lib in the main code hoping to use DATA directly without calling init_data() again. Here's what I've tried:

my_lib.rs:

use lazy_static::lazy_static;

pub fn init_data() -> Vec<u8> {
    // some expensive calculations
}

lazy_static! {
    pub static ref DATA: Vec<u8> = init_data();  // supposed to call init_data() only once during compilation
}

main.rs:

use my_lib::DATA;
call1(&DATA);  // use DATA here without calling init_data()
call2(&DATA);

But it turned out that init_data() is still called in the main.rs. What's wrong with this code?


Update: as Ivan C pointed out, lazy_static is not run at compile time. So, what's the right choice for 'pre-loading' the data?

1

There are 1 best solutions below

2
On

There are two problems here: the choice of type, and performing the allocation.

It is not possible to construct a Vec, a Box, or any other type that requires heap allocation at compile time, because the heap allocator and the heap do not yet exist at that point. Instead, you must use a reference type, which can point to data allocated in the binary rather than in the run-time heap, or an array without any reference (if the data is not too large).

Next, we need a way to perform the computation. Theoretically, the cleanest option is constant evaluation — straightforwardly executing parts of your code at compile time.

static DATA: &'static [u8] = {
    // code goes here
};

However, in current stable Rust versions (1.58.1 as I'm writing this), constant evaluation is very limited, because you cannot do anything that looks like dropping a value, or use any function belonging to a trait. It can still do some things, mostly integer arithmetic or constructing other "almost literal" data; for example:

const N: usize = 10;
static FIRST_N_FIBONACCI: &'static [u32; N] = &{
    let mut array = [0; N];
    array[1] = 1;
    let mut i = 2;
    while i < array.len() {
        array[i] = array[i - 1] + array[i - 2];
        i += 1;
    }
    array
};

fn main() {
    dbg!(FIRST_N_FIBONACCI);
}

If your computation cannot be expressed using const evaluation, then you will need to perform it another way:

  • Procedural macros are effectively compiler plugins, and they can perform arbitrary computation, but their output is generated Rust syntax. So, a procedural macro could produce an array literal with the precomputed data.

    The main limitation of procedural macros is that they must be defined in dedicated crates (so if your project is one library crate, it would now be two instead).

  • Build scripts are ordinary Rust code which can compile or generate files used by the main compilation. They don't interact with the compiler, but are run by Cargo before compilation starts.

(Unlike const evaluation, both build scripts and proc macros can't use any of the types or constants defined within the crate being built itself; they can read the source code, but they run too early to use other items in the crate in their own code.)

In your case, because you want to precompute some [u8] data, I think the simplest approach would be to add a build script which writes the data to a file, after which your normal code can embed this data from the file using include_bytes!.