Store traits with same lifetime reference bound


The title is not that great so I’ll explain more in details here.

I am working on the v1 of Tera, a template engine.

One of the feature is Function: in short a Rust function we can call from a template. It can be something simple like getting the current date to something operating on some context like:

In v1, Function is becoming the following trait:

pub trait Function: Sync + Send {
    fn call(&self, args: &HashMap<String, Value>) -> Result<Value>;

The main perf bottleneck of Zola are those get_page/get_section that need access to the full context and which needed to be serialized. Ok when you have 200 pages, less when you have 100k.

I was hoping to use the fact that these are now traits to use references instead of cloning. I made an example of how it would ideally look like:
This gets a lifetime error on which is solved by making the reference 'site which in turns errors with:

error[E0597]: `site` does not live long enough
  --> examples/borrowed_trait_context/
66 |     site.register_tera_fns();
   |     ^^^^ borrowed value does not live long enough
69 | }
   | -
   | |
   | `site` dropped here while still borrowed
   | borrow might be used here, when `site` is dropped and runs the destructor for type `Site<'_>`

error[E0499]: cannot borrow `site` as mutable more than once at a time
  --> examples/borrowed_trait_context/
66 |     site.register_tera_fns();
   |     ---- first mutable borrow occurs here
67 |     rebuild(&mut site);
   |             ^^^^^^^^^
   |             |
   |             second mutable borrow occurs here
   |             first borrow later used here

All of those make sense but now I’m stuck. I do not want to borrow self.pages mutably but I have to to register the function on the Tera instance so I do need the mut.

It might be a case of XY problem and me trying to pigeonhole my original idea onto it.

Does anyone have solutions/alternative ideas to make it work while keeping references? Writing this made me think of maybe looking into crates for self-references from the user side?

In the case of Zola in particular, references will always be valid as Tera functions are re-registered on change and I am guessing for most other usecases other than Zola, cloning will be just fine perf wise since it will happen once.



How about storing pages outside of Site? That is:

struct Site<'site> {
    pages: &'site HashMap<String, String>,
    tera: Tera<'site>,

rebuild() should also be changed to:

fn rebuild<'a, 'site>(site: &'a mut Site<'site>) {

That’s the explicit version, but you probably want to use elision:

fn rebuild(site: &mut Site<'_>) {

Then main() looks like:

fn main() {
    let mut pages = HashMap::new();
    pages.insert("page1".to_string(), "Some content".to_string());
    pages.insert("page2".to_string(), "Some other content".to_string());

    let mut site = Site::new(&pages);

    rebuild(&mut site);

Maybe the above doesn’t make actual logical sense though? In general though, I think you’ll want to have Site borrow pages, and not own them, if you also want to own the Tera instance in there.



That’s the issue, the actual struct is and I’m not sure how I could split them since they are used in conjunction all the time.

Edit: to be clear, the struct that holds tons of data in this file is the Library and is what pages in the example code would be.



I understand you have a self-referential struct situation, but can’t say I understand enough about Zola or Tera to suggest a concrete different design.

Have you considered using Rc to store the pages? That would give you cheap clones to pass around, and obviate some of the lifetimes.



Thanks a lot, I was too focused on lifetimes but using Arc solved my actual issue!