1 // Copyright 2023 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! Proc macros for `pourover`. These macros are reexported by the `pourover` crate, so this crate
16 //! is an implementation detail.
17 
18 use proc_macro::TokenStream;
19 
20 mod call_method;
21 mod jni_method;
22 mod type_parser;
23 
24 /// Export a function as a JNI native method. This will attach a `#[export_name = "..."]` attribute that
25 /// is formatted with the given parameters. The provided `package`, `class`, and `method_name` will
26 /// be combined and formatted in according to the [JNI method name resolution rules][JNI naming].
27 ///
28 /// [JNI naming]: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#resolving_native_method_names
29 ///
30 /// # Parameters
31 /// - `package` (LitStr): the Java package for the class being implemented
32 /// - `class` (LitStr): the Java class being implemented. Use `Foo.Inner` syntax for inner
33 /// classes.
34 /// - `method_name` (*optional* LitStr): the method's name in Java. The Rust function name will be
35 /// used if this parameter is not set.
36 /// - `panic_returns` (*optional* Expr): the value to return when a panic is encountered. This can
37 /// not access local variables. This may only be used with `panic=unwind` and will produce a
38 /// compile error otherwise.
39 ///
40 /// When using `panic_returns` function arguments must be [`std::panic::UnwindSafe`]. See
41 /// [`std::panic::catch_unwind`] for details. In practice this will not cause issues as JNI
42 /// arguments and return values are passed by pointer or value and not by Rust reference.
43 ///
44 /// # Example
45 /// ```
46 /// # use pourover_macro::jni_method;
47 /// # use jni::{sys::jint, objects::{JObject, JString}, JNIEnv};
48 ///
49 /// #[jni_method(package = "my.package", class = "Foo", panic_returns = -1)]
50 /// extern "system" fn getFoo<'local>(
51 ///     mut env: JNIEnv<'local>,
52 ///     this: JObject<'local>,
53 /// ) -> jint {
54 ///     // ...
55 ///     0
56 /// }
57 /// ```
58 ///
59 /// This function will be exported with `#[export_name = "Java_my_package_Foo_getFoo"]`.
60 #[proc_macro_attribute]
jni_method(meta: TokenStream, item: TokenStream) -> TokenStream61 pub fn jni_method(meta: TokenStream, item: TokenStream) -> TokenStream {
62     use quote::ToTokens;
63     match jni_method::jni_method(meta.into(), item.into()) {
64         Ok(item_fn) => item_fn.into_token_stream(),
65         Err(err) => err.into_compile_error(),
66     }
67     .into()
68 }
69 
70 /// Call a Java method.
71 ///
72 /// # Parameters
73 /// `call_method!($env, $cls, $name, $sig, $this, $($args),*)`
74 /// - `env` (Expr: `&mut jni::JNIEnv`): The JNI environment.
75 /// - `cls` (Expr: `&'static ClassDesc`): The class containing the method.
76 /// - `name` (Expr: `&'static str`): The name of the method.
77 /// - `sig` (LitStr): The JNI type signature of the method. This needs to be a literal so that it
78 /// can be parsed by the macro to type-check args and return a correctly-typed value.
79 /// - `this` (Expr: `&JObject`): The Java object receiving the method call.
80 /// - `args` (Expr ...): A variable number of arguments to be passed to the method.
81 ///
82 /// # Caching
83 /// Each macro callsite will generate a `static` `MethodDesc` to cache the
84 /// method id. Due to this, **this macro call should be wrapped in function** instead of being called
85 /// multiple times.
86 ///
87 /// # Type-Safety
88 /// The given type signature will be parsed and arguments will be type checked against it. The
89 /// expected types are from the `jni` crate:
90 /// - Primitives: `jni::sys::{jboolean, jbyte, jchar, jshort, jint, jlong, jfloat, jdouble}`
91 /// - Arrays: `jni::objects::{JPrimitiveArray, JObjectArray}`
92 /// - Objects: `jni::objects::{JObject, JString, JMap, JList}`
93 ///
94 /// Similarly, the return type will be one of the types above.
95 ///
96 /// # Returns
97 /// The macro will evaluate to `jni::errors::Result<R>` where `R` is the return type parsed from
98 /// the type signature.
99 ///
100 /// # Example
101 /// Let's call `sayHello` from the following class.
102 /// ```java
103 /// package com.example;
104 /// class Foo {
105 ///     int sayHello(String name) { /* ... */ }
106 /// }
107 /// ```
108 /// We can use `call_method!` to implement the function call.
109 /// ```rust
110 /// # use jni::{sys::jint, objects::{JObject, JString}, JNIEnv};
111 /// # use pourover_macro::call_method;
112 /// # use pourover::desc::*;
113 /// static MY_CLASS: ClassDesc = ClassDesc::new("com/example/Foo");
114 /// fn say_hello<'l>(
115 ///     env: &mut JNIEnv<'l>,
116 ///     my_obj: &JObject<'_>,
117 ///     name: &JString<'_>
118 /// ) -> jni::errors::Result<jint> {
119 ///     call_method!(env, &MY_CLASS, "sayHello", "(Ljava/lang/String;)I", my_obj, name)
120 /// }
121 /// ```
122 #[proc_macro]
call_method(args: TokenStream) -> TokenStream123 pub fn call_method(args: TokenStream) -> TokenStream {
124     call_method::call_method(args.into())
125         .unwrap_or_else(syn::Error::into_compile_error)
126         .into()
127 }
128 
129 /// Call a static Java method.
130 ///
131 /// # Parameters
132 /// `call_static_method!($env, $cls, $name, $sig, $($args),*)`
133 /// - `env` (Expr: `&mut jni::JNIEnv`): The JNI environment.
134 /// - `cls` (Expr: `&'static ClassDesc`): The class containing the method.
135 /// - `name` (Expr: `&'static str`): The name of the method.
136 /// - `sig` (LitStr): The JNI type signature of the method. This needs to be a literal so that it
137 /// can be parsed by the macro to type-check args and return a correctly-typed value.
138 /// - `args` (Expr ...): A variable number of arguments to be passed to the method.
139 ///
140 /// # Caching
141 /// Each macro callsite will generate a `static` `StaticMethodDesc` to cache the
142 /// method id. Due to this, **this macro call should be wrapped in function** instead of being called
143 /// multiple times.
144 ///
145 /// # Type-Safety
146 /// The given type signature will be parsed and arguments will be type checked against it. The
147 /// expected types are from the `jni` crate:
148 /// - Primitives: `jni::sys::{jboolean, jbyte, jchar, jshort, jint, jlong, jfloat, jdouble}`
149 /// - Arrays: `jni::objects::{JPrimitiveArray, JObjectArray}`
150 /// - Objects: `jni::objects::{JObject, JString, JMap, JList}`
151 ///
152 /// Similarly, the return type will be one of the types above.
153 ///
154 /// # Returns
155 /// The macro will evaluate to `jni::errors::Result<R>` where `R` is the return type parsed from
156 /// the type signature.
157 ///
158 /// # Example
159 /// Let's call `sayHello` from the following class.
160 /// ```java
161 /// package com.example;
162 /// class Foo {
163 ///     static int sayHello(String name) { /* ... */ }
164 /// }
165 /// ```
166 /// We can use `call_static_method!` to implement the function call.
167 /// ```rust
168 /// # use jni::{sys::jint, objects::{JObject, JString}, JNIEnv};
169 /// # use pourover_macro::call_static_method;
170 /// # use pourover::desc::*;
171 /// static MY_CLASS: ClassDesc = ClassDesc::new("com/example/Foo");
172 /// fn say_hello<'l>(
173 ///     env: &mut JNIEnv<'l>,
174 ///     name: &JString<'_>
175 /// ) -> jni::errors::Result<jint> {
176 ///     call_static_method!(env, &MY_CLASS, "sayHello", "(Ljava/lang/String;)I", name)
177 /// }
178 /// ```
179 #[proc_macro]
call_static_method(args: TokenStream) -> TokenStream180 pub fn call_static_method(args: TokenStream) -> TokenStream {
181     call_method::call_static_method(args.into())
182         .unwrap_or_else(syn::Error::into_compile_error)
183         .into()
184 }
185 
186 /// Call a Java constructor.
187 ///
188 /// # Parameters
189 /// `call_constructor!($env, $cls, $sig, $($args),*)`
190 /// - `env` (Expr: `&mut jni::JNIEnv`): The JNI environment.
191 /// - `cls` (Expr: `&'static ClassDesc`): The class to be constructed.
192 /// - `sig` (LitStr): The JNI type signature of the constructor. This needs to be a literal so that it
193 /// can be parsed by the macro to type-check args and return a correctly-typed value.
194 /// - `args` (Expr ...): A variable number of arguments to be passed to the constructor.
195 ///
196 /// # Caching
197 /// Each macro callsite will generate a `static` `MethodDesc` to cache the
198 /// method id. Due to this, **this macro call should be wrapped in function** instead of being called
199 /// multiple times.
200 ///
201 /// # Type-Safety
202 /// The given type signature will be parsed and arguments will be type checked against it. The
203 /// expected types are from the `jni` crate:
204 /// - Primitives: `jni::sys::{jboolean, jbyte, jchar, jshort, jint, jlong, jfloat, jdouble}`
205 /// - Arrays: `jni::objects::{JPrimitiveArray, JObjectArray}`
206 /// - Objects: `jni::objects::{JObject, JString, JMap, JList}`
207 ///
208 /// # Returns
209 /// The macro will evaluate to `jni::errors::Result<jni::objects::JObject>`.
210 ///
211 /// # Example
212 /// Let's call the constructor from the following class.
213 /// ```java
214 /// package com.example;
215 /// class Foo {
216 ///     Foo(String name) { /* ... */ }
217 /// }
218 /// ```
219 /// We can use `call_constructor!` to implement the function call.
220 /// ```rust
221 /// # use jni::{objects::{JObject, JString}, JNIEnv};
222 /// # use pourover_macro::call_constructor;
223 /// # use pourover::desc::*;
224 /// static MY_CLASS: ClassDesc = ClassDesc::new("com/example/Foo");
225 /// fn construct_foo<'l>(
226 ///     env: &mut JNIEnv<'l>,
227 ///     name: &JString<'_>
228 /// ) -> jni::errors::Result<JObject<'l>> {
229 ///     call_constructor!(env, &MY_CLASS, "(Ljava/lang/String;)V", name)
230 /// }
231 /// ```
232 #[proc_macro]
call_constructor(args: TokenStream) -> TokenStream233 pub fn call_constructor(args: TokenStream) -> TokenStream {
234     call_method::call_constructor(args.into())
235         .unwrap_or_else(syn::Error::into_compile_error)
236         .into()
237 }
238 
239 #[cfg(test)]
240 pub(crate) mod test_util {
241     use proc_macro2::{TokenStream, TokenTree};
242 
243     /// Iterator that traverses TokenTree:Group structures in preorder.
244     struct FlatStream {
245         streams: Vec<<TokenStream as IntoIterator>::IntoIter>,
246     }
247 
248     impl FlatStream {
249         fn new(stream: TokenStream) -> Self {
250             Self {
251                 streams: vec![stream.into_iter()],
252             }
253         }
254     }
255 
256     impl Iterator for FlatStream {
257         type Item = TokenTree;
258 
259         fn next(&mut self) -> Option<TokenTree> {
260             let next = loop {
261                 let stream = self.streams.last_mut()?;
262                 if let Some(next) = stream.next() {
263                     break next;
264                 }
265                 let _ = self.streams.pop();
266             };
267 
268             if let TokenTree::Group(group) = &next {
269                 self.streams.push(group.stream().into_iter());
270             }
271 
272             Some(next)
273         }
274     }
275 
276     pub fn contains_ident(stream: TokenStream, ident: &str) -> bool {
277         FlatStream::new(stream)
278             .filter_map(|tree| {
279                 let TokenTree::Ident(ident) = tree else {
280                     return None;
281                 };
282                 Some(ident.to_string())
283             })
284             .any(|ident_str| ident_str == ident)
285     }
286 }
287