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