Konrad Koschel
Konrad Koschel

Reputation: 205

How do I pass arguments from a generated function to another function in a procedural macro?

I'm trying to create a macro that generates two functions from a function signature. As the signature of the functions is dependent from what is passed to the macro, the number of function arguments is varying.

The functions I want to create are (i) an internal function, that actually does something (which I was able to generate as I wanted) and (ii) a public function that should wrap around the internal function. The function signatures are the same regarding the parameters. Therefore, it should be easy to pass all the arguments that were passed to the public function down into the internal function.

The following snippet shows the simplified macro generation for the public function. How can I achieve that the arguments are passed down correctly? (#params_from_public_function)

let public_function_ident = ...;
let internal_function_ident = ...;

// ast_function_stub-Type: ForeignItemFn
let params_from_public_function: Punctuated<FnArg, Comma> = ast_function_stub.sig.inputs.clone();


quote! {
  pub fn #public_name(#params_from_public_function) {
    #internal_function_ident(#params_from_public_function);
  }
}

Here are two examples, how the resulting macro should work:

generate_both_functions!(fn foo(someParam: &str) -> u8;);

// What the macro should generate:

fn _foo(someParam: &str) -> u8 { // Internal function
  // Some internal stuff happens here
}

pub fn foo(someParam: &str) { // Public function
  _foo(someParam);
}

// Second example (having two instead of one argument and different types)

generate_both_functions!(fn bar(param1: i32, param2: String) -> String;);

// What the macro should generate
fn _bar(param1: i32, param2: String) -> String {
  // Internal stuff again
}

fn bar(param1: i32, param2: String) {
  _bar(param1, param2);
}

Just to clarify: I was able to correctly parse the macro's input (generate_both_functions!) and extract the function argument metadata from the AST. However, I am not able to pass the actual inputs dynamically to the internal function.

Upvotes: 3

Views: 1707

Answers (2)

Konrad Koschel
Konrad Koschel

Reputation: 205

First of all: Thank you @AlphaModder!

As I don't want to support the destructuring case, i went with your first alternative. I want to explain how I finally achieved what I want, so that people with similar issues are able to fix it faster than me.

First of all, I constructed the function itself using the ItemFn-Struct instead of using the parse_quote!-Macro:

// The idents from both functions
let internal_function_ident = ...;
let public_function_ident = ...;

// The arguments with the following pattern: $ident: $type, $ident: $type, ...
let params_from_public_function: Punctuated<FnArg, Comma> = ast_function_stub.sig.inputs.clone();

// Transform the arguments to the pattern: $ident, $ident, ...
let transformed_params = transform_params(params_from_public_function.clone());

// Assemble the function
ItemFn {
  ...
  sig: syn::Signature {
    ...
    ident: public_function_ident,
    inputs: params_from_public_function
  },
  block: parse_quote!({
    #internal_function_ident#transformed_params;
  })
}

The transform_params-function then looks like this:

fn transform_params(params: Punctuated<syn::FnArg, syn::token::Comma>) -> Expr {
    // 1. Filter the params, so that only typed arguments remain
    // 2. Extract the ident (in case the pattern type is ident)
    let idents = params.iter().filter_map(|param|{
        if let syn::FnArg::Typed(pat_type) = param {
            if let syn::Pat::Ident(pat_ident) = *pat_type.pat.clone() {
                return Some(pat_ident.ident)
            }
        }
        None
    });

    // Add all idents to a Punctuated => param1, param2, ...
    let mut punctuated: Punctuated<syn::Ident, Comma> = Punctuated::new();
    idents.for_each(|ident| punctuated.push(ident));

    // Generate expression from Punctuated (and wrap with parentheses)
    let transformed_params = parse_quote!((#punctuated));
    transformed_params
}

Upvotes: 3

AlphaModder
AlphaModder

Reputation: 3386

This is not possible in full generality. Consider the following function signature, which makes use of destructuring in argument position to extract the fields of its first parameter:

fn bar(Foo { a, b, .. }: Foo)

Since the Foo has been destructured, there is no way for the macro to generate code which passes it to another function with the exact same signature. You might try to reconstruct the Foo and call _bar like _bar(Foo { a, b }), but due to the .. some of the fields have been permanently lost and it cannot be reconstructed.

The only remaining option here is to extract the set of bindings from the pattern (which is not trivial), and generate a function that takes each of those bindings as separate parameters. But this is also impossible. We'd have to generate a signature like fn _bar(a: A, b: B), where A and B are the types of a and b. But the macro cannot see the declaration of the type Foo, and so it has no way to determine what these types are.

Alternatives

  • It is possible to support the common case where each argument has the form $ident: $type. All you need to do in this case is to check that the pat field on each syn::FnArg is a Pat::Ident and grab the name of the argument. Then you can generate a matching signature for _bar and pass each argument in.

  • If no external code needs to call _bar, you can make it a by-move closure:

    fn bar(/* any signature you like */) {
        let _bar = move || { /* body goes here, using any arguments from bar */ };
        _bar();
    }
    

    This should inherit all the bindings of the outer function seamlessly, but the outer function will no longer be able to use them after, which may be a dealbreaker.

Upvotes: 2

Related Questions