'Generic lifetime parameter and local scope

piet is a drawing library with generic backends (cairo, for example).

I want to have a trait Renderable that can render to any piet backend (=context).

pub trait Renderable {
    fn render<C: piet::RenderContext>(&self, ctx: &mut C);

    // Since `Renderable` can render to any `RenderContext`, it can render to a file too.
    fn save_to_file(&self, path: &Path) {
        let ctx = build_cairo_context_for_image_file(path);
        self.render::<piet_common::CairoRenderContext>(&mut ctx);
    }
}

However rustc complained about object unsafety of render<C>, so I made the trait itself generic:

pub trait Renderable<C: piet::RenderContext> {
    fn render(&self, ctx: &mut C);
    fn save_to_file(&self, path: &Path) -> Result<(), piet_common::Error> {
        let width = 512;
        let height = 512;
        let pix_scale = 1.0;
        let mut device = piet_common::Device::new()?;
        let mut bitmap: piet_common::BitmapTarget =
            device.bitmap_target(width, height, pix_scale)?;
        let mut rc = bitmap.render_context();
        // Renderable::<CairoRenderContext>::render(self, &mut rc);
        self.render(&mut rc);
        rc.finish()?;
        bitmap.save_to_file(path);

        Ok(())
    }
}

Now the problem is, when save_to_file calls self.render(&mut rc), it cannot find the method because save_to_file is implemented for C and not CairoRenderContext (although CairoRenderContext implements RenderContext).

    |
 14 | pub trait Renderable<C: piet_common::RenderContext> {
    |                      - this type parameter
 ...
 25 |         self.render(&mut rc);
    |                     ^^^^^^^ expected type parameter `C`, found struct `CairoRenderContext`
    |
    = note: expected mutable reference `&mut C`
               found mutable reference `&mut CairoRenderContext<'_>`

As an awkward workaround, I added trait Drawable that is implemented for any type of Renderable<CairoRenderContext>. The problem is CairoRenderContext has a lifetime parameter, struct CairoRenderContext<'a>.

pub trait Renderable<C: piet_common::RenderContext> {
    fn render(&self, ctx: &mut C);
}

pub trait Drawable {
    fn save_to_file<'a>(&self, path: &Path) -> Result<(), piet_common::Error>
    where
        Self: Renderable<CairoRenderContext<'a>>,
    {
        let width = 512;
        let height = 512;
        let pix_scale = 1.0;
        let mut device = piet_common::Device::new()?;
        let mut bitmap: piet_common::BitmapTarget =
            device.bitmap_target(width, height, pix_scale)?;
        let mut rc = bitmap.render_context();
        Renderable::<CairoRenderContext>::render(self, &mut rc);
        // self.render(&mut rc);
        rc.finish()?;
        drop(rc);
        bitmap.save_to_file(path);

        // at this point, we don't need device, bitmap or rc.
        // rustc thinks they should be alive.
        Ok(())
    }
}

The lifetime 'a is supposed to represent the lifetime of a local variable device but rustc errors that device and bitmap variables should live long enough for 'a.


 1  error[E0597]: `device` does not live long enough                                                     
    |                                                                                                    
    |     fn save_to_file<'a>(&self, path: &Path) -> Result<(), piet_common::Error>                      
    |                     -- lifetime `'a` defined here                                                  
 ...                                                                                                     
    |             device.bitmap_target(width, height, pix_scale)?;                                       
    |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ borrowed value does not live long enough
    |         let mut rc = bitmap.render_context();                                                      
    |                      ----------------------- assignment requires that `device` is borrowed for `'a`
 ...                                                                                                     
    |     }                                                                                              
    |     - `device` dropped here while still borrowed                                                   
                                                                                                
 2  error[E0597]: `bitmap` does not live long enough                                                     
    |                                                                                                    
    |     fn save_to_file<'a>(&self, path: &Path) -> Result<(), piet_common::Error>                      
    |                     -- lifetime `'a` defined here                                                  
 ...                                                                                                     
    |         let mut rc = bitmap.render_context();                                                      
    |                      ^^^^^^^^^^^^^^^^^^^^^^^                                                       
    |                      |                                                                             
    |                      borrowed value does not live long enough                                      
    |                      assignment requires that `bitmap` is borrowed for `'a`                        
 ...                                                                                                     
    |     }                                                                                              
    |     - `bitmap` dropped here while still borrowed                                                   
                                                                                                
 3  error[E0505]: cannot move out of `bitmap` because it is borrowed                                     
    |                                                                                                    
    |     fn save_to_file<'a>(&self, path: &Path) -> Result<(), piet_common::Error>                      
    |                     -- lifetime `'a` defined here                                                  
 ...                                                                                                     
    |         let mut rc = bitmap.render_context();                                                      
    |                      -----------------------                                                       
    |                      |                                                                             
    |                      borrow of `bitmap` occurs here                                                
    |                      assignment requires that `bitmap` is borrowed for `'a`                        
 ...                                                                                                     
    |         bitmap.save_to_file(path);                                                                 
    |         ^^^^^^ move out of `bitmap` occurs here                     
  • Q1. When rustc errors about object-safety, was it a correct solution to uplift a type parameter from render<C> to Renderable<C>?
  • Q2. Should I have two traits (Renderable, Drawable) so save_to_file can call render or is there a better way?
  • Q3. How can I correctly tell rustc about Self: Renderable<CairoRenderContext<_>> in the last example?


Solution 1:[1]

Your second error, regarding the &mut C and &mut CairoRenderContext<'_> mismatch, is solved by omitting the body of save_to_file() from the trait definition, and instead defining it in the impl block. Here's a highly simplified version of what I mean:

pub trait Renderable<C: RenderContext> {
    fn render(&self, ctx: &mut C);
    fn save_to_file(&self);
}

struct Cairo;

impl Renderable<CairoRenderContext<'_>> for Cairo {
    fn render(&self, ctx: &mut CairoRenderContext<'_>) {
        todo!()
    }

    fn save_to_file(&self) {
        let mut rc = CairoRenderContext::new();
        self.render(&mut rc);
    }
}

The original approach seems conceptually wrong in that the Renderable trait is supposed to be backend-agnostic, yet the default impl of save_to_file() was using types specific to Cairo, thus causing the error.


Q1

Moving the generic from the method to the trait is indeed one strategy to make a trait object-safe. Other strategies are

  • Making the method use dynamic dispatch for its arguments
  • Marking the method with where Self: Sized (though you won't be able to call that method using dynamic dispatch)

Q2

You don't need two traits if you used the approach I described.

Q3

The answer is a higher-ranked trait bound:

pub trait Drawable {
    fn save_to_file(&self, path: &Path) -> ...
    where
        Self: for<'a> Renderable<CairoRenderContext<'a>>,
    {...}
}

Note you can make the bound a supertrait instead:

pub trait Drawable: for<'a> Renderable<CairoRenderContext<'a>> {
    fn save_to_file(&self, path: &Path) -> ... {...}
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Brian Bowman