List move closure captures in proc macro

Given:

let a = ...

my_macro!(move |a: ...| {
    let b = ...;
    move_and_use_a(a);
});

Is it possible to list identifiers moved into the closure (like a and not b) in a proc_macro? My first thought was to walk the AST to find identifiers used not defined in the body, but it's pretty brittle.

What is it you're actually trying to do? This sounds like an XY problem.

No, because you can't tell the difference between identifiers and constants/globals/functions.

For example, that a might actually be fn a(_: i32) because move_and_use_a is a higher-order function taking impl Fn(i32).

And at macro time you don't have the information needed to do name resolution to figure stuff out.

(And thus jhpratt's correct in that you should explain what you're really trying to accomplish.)

It's a bit all over the place (sorry for wall of text). I'm making a networked simulation game (w/ Godot for rendering and utils + bevy_ecs for game state) that needs to handle non-deterministic events like a player clicking a button that increments a counter. These commands need to be transmitted reliably to other nodes before they're processed.

I was thinking of writing the command body inline, like:

let some_state = ...
command!(&mut CommandHandler, move |q: /*an ECS argument (like a component query)*/| {
      // operate on query...
      // perhaps the captured variable is the amount of health to subtract from each player
      do_something_that_moves(some_state);
});

Because each client has the same source, each command can be assigned a unique ID. When a player issues a command, before queueing it for execution, they transmit the command ID and the serialized closure arguments to others. Then, the closure is locally allocated with captures as usual:

#[derive(Serialize, Deserialize)]
struct Payload {
    some_state: ...
}
let payload = // serialize some_state
// transmit to other clients w/ ID
command_handler.queue.push(Box::new(... closure here)); // queue locally

When receiving a command, you just look up the ID, invoke a generated function which deserializes the message specific to that command, and queue the closure for execution locally:

let payload = deserialize(bytes); // deserialize payload
let some_state = payload.some_state;
command_handler.queue.push(Box::new(... closure here)); // queue locally

Obviously you could just have a Message mega-enum that stores each command body for serialization/deserialization, but closures already implicitly include the body.

You could try a "best effort" approximation (and document it), so that it

  • only captures lower-case stuff
  • doesn't capture closures or function pointers when they appear in normal call expressions like f(x) only. Document this and require users to use (f)(x)
  • tries to capture everything else that's an ident, even if it's an fn item. E. g. let f = g; could have g being a function item that's not a local variable. Document this and require users to use let f = path::to::g instead. (Usually let f = self::g should work.)

Note that im only addressing the "find a list of captures for a closure" question, I haven't tried to understand if the rest of your idea/approach would work with such a macro.

1 Like

For anything going over a network, I think you should probably just make the enum. Then "send the command id and the serialized arguments" is just "serialize the enum value". And dispatching based on the id is just match.

Maybe make a quick macro_rules macro to help generate all the structs and the enum, but being able to just send things in json or whatever in debug mode will probably be a big assistance for debugging network issues anyway.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.