Storing struct methods as fn pointers in a `static`

I'm following along with a book which uses Pratt Parsing when writing a parser in C and have run into exactly the problem you'd expect when writing similar code in Rust. There are of course plenty of other ways to set up a Pratt Parser in Rust, it just feels like it almost works which makes me worry I'm missing a relatively straightforward fix here

The problem: since the Parser (Actor in the example code) type the method is defined on has a lifetime, there's not a straightforward way to define the static table of fn pointers that is populated with Actor's methods.

Playground with the full example code

The "obvious" code just ends up making the fn pointer use an Actor<'static> argument which certainly can't be called with a non-'static lifetime.

    struct Actor<'a> {
        context: &'a str,
    }

    impl<'a> Actor<'a> {
        fn act(&mut self) {
            println!("{}", self.context);
        }

        fn lookup_action(&mut self) {
            ACTIONS[0](self) // Fails: `ACTIONS` contains a 'static lifetime which is hidden
        }
    }

    type Action<'a> = fn(&mut Actor<'a>);

    static ACTIONS: &[Action] = &[Actor::act];

You can use a transmute but that sort of defeats the point of using the lifetimes in the first place, even if it's not super likely to be a problem in practice. (The building of the table could probably be made safer too, for example with a const fn populating the table from inside the Actor impl)

    pub struct Actor<'a> {
        pub context: &'a str,
    }

    impl<'a> Actor<'a> {
        fn act(&mut self) {
            println!("{}", self.context);
        }

        pub fn lookup_action(&mut self) {
            ACTIONS[0].act(self)
        }
    }

    struct Lookup(Action<'static>);

    impl Lookup {
        /// Tie the lifetime of the action to the actor as a safeguard since we're transmuting.
        fn act<'a>(&self, c: &mut Actor<'a>) {
            let f: Action<'a> = unsafe { std::mem::transmute(self.0) };
            f(c)
        }
    }

    type Action<'a> = fn(&mut Actor<'a>);

    static ACTIONS: &[Lookup] = &[Lookup(Actor::act)];

You can build the table inside the Actor impl which dodges the problem by allowing you to actually refer to the proper lifetime. But now the table is no longer static, and you have to hope the compiler gets really clever about optimizations (admittedly this example is the silliest way to do this)

    pub struct Actor<'a> {
        pub context: &'a str,
    }

    impl<'a> Actor<'a> {
        fn act(&mut self) {
            println!("{}", self.context);
        }

        pub fn lookup_action(&mut self) {
            Self::rules()[0](self)
        }

        fn rules() -> [Action<'a>; 1] {
            [Actor::act]
        }
    }

    type Action<'a> = fn(&mut Actor<'a>);

Is there a better option to solve this kind of problem? (besides "stop trying to do that" which I will freely admit is probably the right answer to this problem in most cases)

The issue is that you declare Action as being a function that act on an Actor with a specific 'a lifetime, but then you want it to use with Actors that have any lifetime. The proper syntax to express this is type Action = for<'a> fn(&mut Actor<'a>) (or type Action = for<'a, 'b> fn(&'a mut Actor<'b>) if you want to be fully explicit).

With this done you'll notice a new error:

error[E0308]: mismatched types
  --> src/main.rs:22:35
   |
22 |     static ACTIONS: &[Action] = &[Actor::act];
   |                                   ^^^^^^^^^^ one type is more general than the other
   |
   = note: expected fn pointer `for<'a, 'b> fn(&'b mut lifetime_type::Actor<'a>)`
                 found fn item `for<'a> fn(&'a mut lifetime_type::Actor<'_>) {lifetime_type::Actor::<'_>::act}`

The issue here is that Actor::act here is implicit for Actor::<'some_lifetime>::act, which is bad because we need this to be valid for any lifetime, while this forces some lifetime (it doesn't matter which, what matters is that the lifetime gets fixed). The solution to this is by either writing a separate function fn act(actor: &mut Actor<'_>) { actor.act() } and using act instead of Actor::act in the static, or you can just use a closure |actor| actor.act()

Here's the fixed version of your playground Rust Playground

7 Likes

Interesting, the HRTB was what I tried first (and is also what you get if you leave off the lifetime in the type alias) but I wasn't sure how I could possibly resolve that error. It makes sense that the closure/free function resolves the issue since they change how the lifetime on Actor is constrained, and the method calls don't have any additional constraints on the lifetimes involved that would make a free function not work. I guess I was just coming at the problem from the wrong direction. Thank you!

Perhaps unconventional, but could also "unfix" the lifetime on Self in the Actor implementation (not sure if there are consequences of this):
Rust Playground

-    fn act(&mut self) {
+    fn act(this: &mut Actor<'_>) {
-    type Action<'a> = fn(&mut Actor<'a>);
+    type Action = fn(&mut Actor<'_>);

If I'm not mistaken, this translates to HRTB.