Attribute macro expands with no errors, but compiler says there are errors

Hello,

I wrote an attribute macro that essentially modifies a regular Rust function into one that can be called by Java/Kotlin using extern/native methods. The user writes the function like regular Rust, but the argument and return types are Java types or Rust primitives. The macro then converts these Java types to JObject.

Here is an example:

#[jni_fn("Test")]
pub fn test_jni_fn_2<'local>(obj: java.lang.Object) -> i32 {
    3
}

The problem is that I get the following errors:

error: expected one of `:`, `@`, or `|`, found `.`
  --> src/lib.rs:13:44
   |
13 | pub fn test_jni_fn_2<'local>(obj: java.lang.Object) -> i32 {
   |                                            ^ expected one of `:`, `@`, or `|`

error: expected one of `!`, `(`, `)`, `+`, `,`, `::`, or `<`, found `.`
  --> src/lib.rs:13:39
   |
13 | pub fn test_jni_fn_2<'local>(obj: java.lang.Object) -> i32 {
   |                                       ^
   |                                       |
   |                                       expected one of 7 possible tokens
   |                                       help: missing `,`

Initially I thought that the macro was outputting both the input and the intended output,
but then I use cargo expand and I get this.

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use ez_jni::{jni_fn, package};
#[no_mangle]
pub extern "system" fn Java_me_test_Test_test_1jni_1fn_12<'local>(
    mut _env: ::jni::JNIEnv<'local>,
    _class: ::jni::objects::JClass<'local>,
    obj: ::jni::objects::JObject<'local>,
) -> i32 {
    ::ez_jni::__throw::catch_throw(&mut _env, move |env| { 3 })
}

If I use this instead, it will compile with no errors, so the macro is working fine.

Why does this happen? Is it because the compiler parses the function before the macro is expanded?

You have to pass valid Rust syntax to the macro. java.lang.Object is not a valid type expression expected in the place of the type declaration of a function argument. Maybe you meant to write java::lang::Object instead?

Maybe you meant to write java::lang::Object instead?

No, the Java type is meant to be written with Java syntax by design, so that it isn't confused with a Rust type.

You have to pass valid Rust syntax to the macro.

So I have to pass a valid Rust function that can be parsed by syn::ItemFn for example?

Yes, a TokenStream consists of valid Rust syntax.

So by syntax you mean anything that can be put in a TokeStream? Then in that case it doesn't make sense because the input is a valid TokenStream (even though it's not a valid Rust function) and the macro accepts the input.

Huh, sorry, indeed I thought it was an inherent attribute of a TokenStream to be valid, but it seems that it's related to when macros are expanded, not the TokenStream they take as an argument. From the Little Book of Rust Macros:

As previously mentioned, macro processing in Rust happens after the construction of the AST. As such, the syntax used to invoke a macro must be a proper part of the language's syntax.

Macros like Yew's html! wouldn't be possible if that was the case. Wrapping your function declaration in a function-like procedural macro rather than an attribute-like one should allow you to make your code compile I believe.

Can you put a buildable example together somewhere, so that we can reproduce the issue on our systems?

I am wondering if this is a limitation of attribute macros - because they're attached to items, I wouldn't be surprised if the compiler expects the item to be valid Rust before macro expansion, not just after.

If I'm right, switching to a function-like macro will resolve it, because those don't have to be valid Rust before expansion.

I think that makes sense. I should try doing this, but I wanted to do it in a way that didn't stray away too much from rust syntax

1 Like

Can you put a buildable example together somewhere,

I was going to use rust playground for this but i'm not sure how to add my github repo as a dependency there. But you could try pasting the example i gave above into your own machine.

The example you've given doesn't build - it doesn't have a complete set of use statements. With:

use jni_macros::jni_fn;

#[jni_fn("Test")]
pub fn test_jni_fn_2<'local>(obj: java.lang.Object) -> i32 {
    3
}

I get the following errors:

   Compiling ez_jni v0.1.0 (/home/sfarnsworth/Personal/urlo/ez-jni-rs)
error: expected one of `:`, `@`, or `|`, found `.`
 --> src/main.rs:4:44
  |
4 | pub fn test_jni_fn_2<'local>(obj: java.lang.Object) -> i32 {
  |                                            ^ expected one of `:`, `@`, or `|`

error: expected one of `!`, `(`, `)`, `+`, `,`, `::`, or `<`, found `.`
 --> src/main.rs:4:39
  |
4 | pub fn test_jni_fn_2<'local>(obj: java.lang.Object) -> i32 {
  |                                       ^
  |                                       |
  |                                       expected one of 7 possible tokens
  |                                       help: missing `,`

error: Macro jni_macros::package! has not been called.
 --> src/main.rs:3:1
  |
3 | #[jni_fn("Test")]
  | ^^^^^^^^^^^^^^^^^
  |
  = note: this error originates in the attribute macro `jni_fn` (in Nightly builds, run with -Z macro-backtrace for more info)

That said, if I turn jni_fn into a function-like macro, it doesn't complain about the syntax for obj:

error: Macro jni_macros::package! has not been called.
 --> src/main.rs:3:1
  |
3 | / jni_fn!{
4 | |     pub fn test_jni_fn_2<'local>(obj: java.lang.Object) -> i32 {
5 | |         3
6 | |     }
7 | | }
  | |_^
  |
  = note: this error originates in the macro `jni_fn` (in Nightly builds, run with -Z macro-backtrace for more info)
1 Like

Yeah sorry I was using a different package name.

Anyways I guess I'm going to just change it to a function-like macro.

Thanks for your help everyone.

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.