rogue_macros/lib.rs
1//! # runtime‑macros
2//!
3//! Procedural macros used by the **runtime** project to generate RPC stubs, strongly‑typed
4//! message‑handlers, and boilerplate for object references at **compile‑time**.
5//! They contain **no runtime logic** – their sole purpose is to expand user‑written async
6//! functions and `impl` blocks into the code required for inter‑runtime communication.
7//!
8//! ## What it generates
9//! * A [`MessageHandler`] implementation that the runtime can dispatch.
10//! * `Input` and `Output` structs that implement [`TypeInfo`] for binary serialization.
11//! * A tiny, ergonomic forwarding stub that performs the actual RPC call.
12//!
13//! The generated pieces are completely hidden from the end‑user, giving them a clean API while
14//! retaining full type‑safety over the wire.
15//!
16//! ## Usage
17//! Put async functions or methods inside an `rpc! { ... }` block:
18//! ```rust,ignore
19//! rpc! {
20//! async fn add(a: i32, b: i32) -> i32 {
21//! a + b
22//! }
23//! }
24//! ```.
25use heck::ToPascalCase;
26use proc_macro::TokenStream;
27use proc_macro_error::proc_macro_error;
28use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
29use quote::{ToTokens, quote};
30use syn::{
31 Attribute, Item, ItemStruct, Result,
32 fold::Fold,
33 parse::{Parse, ParseStream},
34 parse_macro_input,
35};
36
37/// Returns `true` if the attribute list contains `#[local]`.
38fn has_local_attr(attrs: &[Attribute]) -> bool {
39 attrs.iter().any(|a| a.path().is_ident("local"))
40}
41
42/// Removes all `#[local]` attributes from the list in‑place.
43fn remove_local_attr(attrs: &mut Vec<Attribute>) {
44 attrs.retain(|a| !a.path().is_ident("local"));
45}
46
47/// Generate the body of an RPC‑forwarding stub.
48///
49/// The returned `TokenStream2` is the method body that:
50/// 1. Builds the `args` input struct.
51/// 2. Calls `ctx.runtime.call::<Handler>(target_id, args).await`.
52/// 3. Emits tracing spans and converts errors into a `pending` future.
53fn build_rpc_call_body(
54 input_ident: &Ident,
55 handler_ident: &Ident,
56 fld_inits: &[TokenStream2],
57 target_expr: TokenStream2,
58 rpc_name: TokenStream2,
59 ret_ty: TokenStream2,
60) -> TokenStream2 {
61 quote! {
62 let ctx = rogue_runtime::CONTEXT.get();
63 let args = #input_ident { #(#fld_inits),* };
64 let target_id = #target_expr;
65 rogue_runtime::tracing::trace!(rpc = #rpc_name, target = %target_id, "calling");
66 let ret = match ctx.runtime.call::<#handler_ident>(target_id.clone(), args).await {
67 Ok(output) => output.0,
68 Err(e) => {
69 rogue_runtime::tracing::error!(rpc = #rpc_name, target = %target_id, error = %e.to_string(), "failed");
70 let _ = ctx.error_tx.send(e).await;
71 return ::std::future::pending::<#ret_ty>().await;
72 }
73 };
74 rogue_runtime::tracing::trace!(rpc = #rpc_name, target = %target_id, "done");
75 ret
76 }
77}
78
79/// Helper that produces the boiler‑plate `TypeInfo` implementation for a
80/// given identifier, eliminating the repetition of the `type_name` and
81/// `type_id` methods throughout the macro code.
82fn gen_type_info_impl(ident: &Ident) -> TokenStream2 {
83 quote! {
84 impl rogue_runtime::TypeInfo for #ident {
85 fn type_name() -> &'static str { stringify!(#ident) }
86 fn type_id() -> rogue_runtime::TypeId {
87 <rogue_runtime::sha2::Sha256 as rogue_runtime::sha2::Digest>::digest(Self::type_name()).into()
88 }
89 }
90 }
91}
92
93/// Recursively search a type for any occurrence of `Self`.
94fn type_contains_self(ty: &syn::Type) -> bool {
95 use syn::visit::Visit;
96 struct Finder {
97 found: bool,
98 }
99 impl<'ast> Visit<'ast> for Finder {
100 fn visit_type_path(&mut self, tp: &'ast syn::TypePath) {
101 if tp.qself.is_none()
102 && tp.path.segments.len() == 1
103 && tp.path.segments[0].ident == "Self"
104 {
105 self.found = true;
106 }
107 syn::visit::visit_type_path(self, tp);
108 }
109 }
110 let mut finder = Finder { found: false };
111 Finder::visit_type(&mut finder, ty);
112 finder.found
113}
114
115/* -------- mini-AST: just “a list of items inside the block” -------- */
116
117/// Minimal wrapper around a sequence of Rust items that appear inside an
118/// `rpc! { ... }` block. The parser simply collects everything until the end
119/// of the block so the macro can iterate over the items later on.
120struct RpcBlock {
121 items: Vec<Item>,
122}
123
124impl Parse for RpcBlock {
125 fn parse(input: ParseStream) -> Result<Self> {
126 let mut items = Vec::new();
127 while !input.is_empty() {
128 items.push(input.parse()?);
129 }
130 Ok(Self { items })
131 }
132}
133
134/* -------- the procedural macro -------- */
135
136/// Generates the glue code that turns a user‑written async function or method
137/// into a fully‑fledged RPC endpoint understood by the runtime.
138///
139/// The returned [`TokenStream2`] contains:
140/// * A [`MessageHandler`] implementation that owns the actual async `handle`
141/// logic and is registered through `inventory`.
142/// * Typed `Input` and `Output` structs that carry the arguments and return
143/// value across the wire and implement [`TypeInfo`].
144/// * (When required) an ergonomic forwarding stub that clients call instead
145/// of the original function, which performs the RPC under the hood.
146///
147/// # Parameters
148/// * `fn_ident` – Identifier of the original async function/method.
149/// * `impl_ident_opt` – `Some` with the containing struct’s ident for
150/// methods, or `None` for free functions.
151/// * `inputs` – Punctuated list of the function’s parameters.
152/// * `output` – The declared return type.
153fn generate_message_handler(
154 fn_ident: &Ident,
155 impl_ident_opt: Option<&Ident>,
156 inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
157 output: &syn::ReturnType,
158) -> TokenStream2 {
159 // strip `_impl` suffix and convert to PascalCase
160 let pascal = fn_ident
161 .to_string()
162 .trim_end_matches("_impl")
163 .to_pascal_case();
164
165 // If this comes from an `impl` block, include the (trimmed) struct name as prefix
166 let struct_prefix = if let Some(impl_ident) = impl_ident_opt {
167 let mut s = impl_ident.to_string();
168 // our macro renames structs to `<Orig>Impl`; strip the suffix for readability
169 if s.ends_with("Impl") {
170 s.truncate(s.len() - 4);
171 }
172 s
173 } else {
174 String::new()
175 };
176
177 // original (alias) struct name – e.g. "Calculator" – used for `Self` returns
178 let alias_ident = if !struct_prefix.is_empty() {
179 Ident::new(&struct_prefix, fn_ident.span())
180 } else {
181 // dummy ident; won’t be used for free functions
182 Ident::new("_Unused", fn_ident.span())
183 };
184
185 let handler_ident = Ident::new(
186 &format!("{}{}Handler", struct_prefix, pascal),
187 fn_ident.span(),
188 );
189 let input_ident = Ident::new(
190 &format!("{}{}Input", struct_prefix, pascal),
191 fn_ident.span(),
192 );
193 let output_ident = Ident::new(
194 &format!("{}{}Output", struct_prefix, pascal),
195 fn_ident.span(),
196 );
197
198 // Pre‑compute `TypeInfo` implementations via the helper to avoid repeating
199 // the same quoted code three times.
200 let handler_ti = gen_type_info_impl(&handler_ident);
201 let input_ti = gen_type_info_impl(&input_ident);
202 let output_ti = gen_type_info_impl(&output_ident);
203
204 // ----- build input‑struct fields & argument list -------------------------
205 let mut arg_fields: Vec<TokenStream2> = Vec::new();
206 let mut arg_names: Vec<Ident> = Vec::new();
207 let mut has_receiver = false;
208 let mut consumes_self = false;
209 let mut is_mut_receiver = false;
210
211 for arg in inputs {
212 match arg {
213 syn::FnArg::Receiver(recv) => {
214 has_receiver = true;
215 if recv.reference.is_none() {
216 consumes_self = true; // by-value `self`
217 } else if recv.mutability.is_some() {
218 is_mut_receiver = true; // `&mut self`
219 }
220 }
221 syn::FnArg::Typed(pat_ty) => {
222 if let syn::Pat::Ident(pat_ident) = &*pat_ty.pat {
223 let nm = &pat_ident.ident;
224 let ty = &pat_ty.ty;
225 arg_fields.push(quote! { #nm: #ty });
226 arg_names.push(nm.clone());
227 }
228 }
229 }
230 }
231
232 // Choose RwLock access mode for &self / &mut self
233 let lock_method = if is_mut_receiver {
234 quote! { write }
235 } else {
236 quote! { read }
237 };
238
239 // prepend instance id for &self methods
240 if has_receiver {
241 arg_fields.insert(0, quote! { __instance_id: rogue_runtime::InstanceId });
242 }
243
244 // When there are no arguments the repetition expands to nothing, so we no
245 // longer need a special‑case branch.
246 let input_struct_fields = quote! { #(pub #arg_fields,)* };
247
248 // Helper that replaces every `Self` in a type with the generated alias
249 struct ReplaceSelf<'a> {
250 alias: &'a Ident,
251 }
252
253 impl<'a> Fold for ReplaceSelf<'a> {
254 fn fold_type_path(&mut self, mut tp: syn::TypePath) -> syn::TypePath {
255 if tp.qself.is_none()
256 && tp.path.segments.len() == 1
257 && tp.path.segments[0].ident == "Self"
258 {
259 tp.path.segments[0].ident = self.alias.clone();
260 }
261 // continue walking so nested generics are handled, too
262 syn::fold::fold_type_path(self, tp)
263 }
264 }
265
266 let (ret_ty, needs_auto_register) = match output {
267 syn::ReturnType::Type(_, ty) => {
268 // Replace every occurrence of `Self` inside the type with the alias
269 let mut replacer = ReplaceSelf {
270 alias: &alias_ident,
271 };
272 let new_ty = replacer.fold_type((**ty).clone());
273
274 let needs_auto_register = type_contains_self(&**ty);
275
276 (quote! { #new_ty }, needs_auto_register)
277 }
278 syn::ReturnType::Default => (quote! { () }, false),
279 };
280
281 // Build the expressions we will actually pass to the wrapped function —
282 // they must reference the fields on `data`, e.g. `data.n`
283 let arg_values: Vec<TokenStream2> = arg_names.iter().map(|nm| quote! { data.#nm }).collect();
284 // body of MessageHandler::handle -----------------------------------------
285 let call_site = if has_receiver {
286 let impl_ident = impl_ident_opt.expect("impl ident required for method with receiver");
287 if consumes_self {
288 if needs_auto_register {
289 quote! {
290 let ctx = rogue_runtime::CONTEXT.get();
291 match ctx.runtime.take_instance::<#impl_ident>(data.__instance_id).await {
292 Some(instance) => {
293 let res = instance.#fn_ident(#(#arg_values),*).await;
294 let res = res.register_instance(&ctx.runtime).await;
295 Ok(#output_ident(res))
296 }
297 None => Err("instance not found".into()),
298 }
299 }
300 } else {
301 quote! {
302 let ctx = rogue_runtime::CONTEXT.get();
303 match ctx.runtime.take_instance::<#impl_ident>(data.__instance_id).await {
304 Some(instance) => {
305 let res = instance.#fn_ident(#(#arg_values),*).await;
306 Ok(#output_ident(res))
307 }
308 None => Err("instance not found".into()),
309 }
310 }
311 }
312 } else if needs_auto_register {
313 quote! {
314 let ctx = rogue_runtime::CONTEXT.get();
315 match ctx.runtime.get_instance::<#impl_ident>(data.__instance_id).await {
316 Ok(instance) => {
317 let res = instance.#lock_method().await.#fn_ident(#(#arg_values),*).await;
318 let res = res.register_instance(&ctx.runtime).await;
319 Ok(#output_ident(res))
320 }
321 Err(e) => Err(e),
322 }
323 }
324 } else {
325 quote! {
326 let ctx = rogue_runtime::CONTEXT.get();
327 match ctx.runtime.get_instance::<#impl_ident>(data.__instance_id).await {
328 Ok(instance) => {
329 let res = instance.#lock_method().await.#fn_ident(#(#arg_values),*).await;
330 Ok(#output_ident(res))
331 }
332 Err(e) => Err(e),
333 }
334 }
335 }
336 } else if let Some(impl_ident) = impl_ident_opt {
337 if needs_auto_register {
338 quote! {
339 let ctx = rogue_runtime::CONTEXT.get();
340 let res = #impl_ident::#fn_ident(#(#arg_values),*).await;
341 let res = res.register_instance(&ctx.runtime).await;
342 Ok(#output_ident(res))
343 }
344 } else {
345 quote! {
346 let res = #impl_ident::#fn_ident(#(#arg_values),*).await;
347 Ok(#output_ident(res))
348 }
349 }
350 } else {
351 // free async function
352 quote! {
353 let res = #fn_ident(#(#arg_values),*).await;
354 Ok(#output_ident(res))
355 }
356 };
357
358 // final token stream -----------------------------------------------------
359 quote! {
360 struct #handler_ident;
361
362 #handler_ti
363
364 #[derive(rogue_runtime::serde::Deserialize, rogue_runtime::serde::Serialize)]
365 struct #input_ident { #input_struct_fields }
366
367 #input_ti
368
369 #[allow(non_camel_case_types)]
370 #[derive(rogue_runtime::serde::Deserialize, rogue_runtime::serde::Serialize)]
371 struct #output_ident(#ret_ty);
372
373 #output_ti
374
375 impl rogue_runtime::InventoryItemProvider for #handler_ident {
376 fn type_info() -> rogue_runtime::InventoryItem {
377 rogue_runtime::InventoryItem::Function(rogue_runtime::InventoryFunctionItem::new(stringify!(#handler_ident).to_string()))
378 }
379 }
380
381 #[rogue_runtime::async_trait]
382 impl rogue_runtime::MessageHandler for #handler_ident {
383 type Input = #input_ident;
384 type Output = #output_ident;
385
386 async fn handle(data: Self::Input) -> std::result::Result<Self::Output, String> {
387 #call_site
388 }
389 }
390
391 rogue_runtime::inventory::submit! {
392 rogue_runtime::HandlerRegistration {
393 register: |rt: rogue_runtime::Runtime| {
394 Box::pin(async move {
395 rt.register_handler::<#handler_ident>().await
396 })
397 },
398 }
399 }
400 }
401}
402
403/// Procedural macro `rpc!` – transforms async functions and methods into
404/// remote‑callable RPC endpoints.
405///
406/// The macro walks every item inside the supplied block:
407/// * Async functions/methods become RPC handlers plus forwarding stubs.
408/// * Structs are renamed and wrapped in `ObjectRef` type aliases.
409/// * Impl blocks are duplicated into trait definitions so both concrete
410/// instances and their `ObjectRef` stubs implement the same API.
411///
412/// Items annotated with `#[local]` are passed through unchanged, allowing
413/// authors to mix local utilities and remote calls within the same block.
414///
415/// See the crate‑level docs for a full usage example.
416#[proc_macro_error]
417#[proc_macro]
418pub fn rpc(input: TokenStream) -> TokenStream {
419 let RpcBlock { items } = parse_macro_input!(input as RpcBlock);
420
421 /* ---------- first pass: gather struct renames ---------- */
422 use std::collections::HashMap;
423
424 // map OriginalName -> GeneratedNameImpl
425 let mut struct_map: HashMap<String, Ident> = HashMap::new();
426 for item in &items {
427 if let Item::Struct(ItemStruct { ident, .. }) = item {
428 let impl_ident = Ident::new(&format!("{}Impl", ident), ident.span());
429 struct_map.insert(ident.to_string(), impl_ident);
430 }
431 }
432
433 /* containers for emitted tokens */
434 let mut emitted: Vec<TokenStream2> = Vec::new();
435 let mut extra: Vec<TokenStream2> = Vec::new(); // generated extras (type alias, traits …)
436
437 /* ---------- second pass: transform items ---------- */
438 for mut item in items {
439 match &mut item {
440 /* ---- free functions --------------------------------------------------------- */
441 Item::Fn(f) => {
442 let local = has_local_attr(&f.attrs);
443 if local {
444 let mut func = f.clone();
445 remove_local_attr(&mut func.attrs);
446 emitted.push(func.into_token_stream());
447 continue;
448 }
449 if f.sig.asyncness.is_some() {
450 // rename original to foo_impl
451 let orig_ident = f.sig.ident.clone();
452 let orig_vis = f.vis.clone();
453 let impl_ident = Ident::new(&format!("{}_impl", orig_ident), orig_ident.span());
454
455 // change the original function’s ident & visibility
456 f.sig.ident = impl_ident.clone();
457 f.vis = syn::Visibility::Inherited;
458
459 // ---- generate the MessageHandler BEFORE we move `item` ----------
460 let handler_ts =
461 generate_message_handler(&f.sig.ident, None, &f.sig.inputs, &f.sig.output);
462
463 // --- build a forwarding stub that performs an RPC call ------------------
464 let mut stub_sig = f.sig.clone();
465 stub_sig.ident = orig_ident.clone();
466
467 /* -------- collect argument idents & build Input struct init ------------ */
468 let mut fld_inits: Vec<TokenStream2> = Vec::new();
469 for arg in &stub_sig.inputs {
470 if let syn::FnArg::Typed(pt) = arg {
471 if let syn::Pat::Ident(pat_ident) = &*pt.pat {
472 let nm = &pat_ident.ident;
473 fld_inits.push(quote! { #nm: #nm });
474 }
475 }
476 }
477
478 /* -------- Handler/Input idents ---------------------------------------- */
479 let pascal = orig_ident.to_string().to_pascal_case();
480 let handler_ident =
481 Ident::new(&format!("{}Handler", pascal), Span::call_site());
482 let input_ident = Ident::new(&format!("{}Input", pascal), Span::call_site());
483
484 /* -------- return type tokens ----------------------------------------- */
485 let ret_ty_tokens: TokenStream2 = match &stub_sig.output {
486 syn::ReturnType::Type(_, ty) => quote! { #ty },
487 syn::ReturnType::Default => quote! { () },
488 };
489
490 /* -------- build stub body -------------------------------------------- */
491 let rpc_name = quote! { stringify!(#orig_ident) };
492 let body_stmts = build_rpc_call_body(
493 &input_ident,
494 &handler_ident,
495 &fld_inits,
496 quote! { ctx.target_id.clone() },
497 rpc_name,
498 quote! { #ret_ty_tokens },
499 );
500
501 let stub_fn = quote! {
502 #orig_vis #stub_sig {
503 #body_stmts
504 }
505 };
506
507 emitted.push(item.into_token_stream());
508 emitted.push(stub_fn);
509 extra.push(handler_ts);
510 } else {
511 emitted.push(item.into_token_stream());
512 }
513 }
514
515 /* ---- structs --------------------------------------------------------------- */
516 Item::Struct(strct) => {
517 let orig_ident = strct.ident.clone();
518 let orig_vis = strct.vis.clone();
519 let impl_ident = struct_map.get(&orig_ident.to_string()).unwrap().clone();
520 strct.ident = impl_ident.clone(); // rename struct inside original item
521 emitted.push(item.into_token_stream());
522
523 // type alias
524 let alias = quote! {
525 #orig_vis type #orig_ident = rogue_runtime::ObjectRef<#impl_ident>;
526 };
527 extra.push(alias);
528 }
529
530 /* ---- impl blocks ----------------------------------------------------------- */
531 Item::Impl(imp) => {
532 // does this impl target one of our renamed structs?
533 let self_ty_ident_opt = match &*imp.self_ty {
534 syn::Type::Path(p) => p.path.get_ident().cloned(),
535 _ => None,
536 };
537 // If the whole impl is marked #[local], just rename the target type (if needed)
538 // and pass it through without generating RPC wrappers.
539 let impl_is_local = has_local_attr(&imp.attrs);
540 if impl_is_local {
541 remove_local_attr(&mut imp.attrs);
542 if let Some(self_ident) = self_ty_ident_opt.clone() {
543 if let Some(impl_ident) = struct_map.get(&self_ident.to_string()) {
544 let new_ty: syn::Type = syn::parse_quote!(#impl_ident);
545 imp.self_ty = Box::new(new_ty);
546 }
547 }
548 emitted.push(item.into_token_stream());
549 continue;
550 }
551 if let Some(self_ident) = self_ty_ident_opt {
552 if let Some(impl_ident) = struct_map.get(&self_ident.to_string()) {
553 /* rename target type in the impl */
554 let new_ty: syn::Type = syn::parse_quote!(#impl_ident);
555 imp.self_ty = Box::new(new_ty);
556
557 /* buckets for methods */
558 let mut rpc_methods: Vec<syn::ImplItem> = Vec::new();
559 let mut keep_methods: Vec<syn::ImplItem> = Vec::new();
560
561 // Is this `impl SomeTrait for Type` ?
562 let is_trait_impl = imp.trait_.is_some();
563
564 for mut itm in std::mem::take(&mut imp.items) {
565 if let syn::ImplItem::Fn(ref mut m) = itm {
566 let async_ = m.sig.asyncness.is_some();
567 let local = has_local_attr(&m.attrs);
568 if async_ && !local {
569 remove_local_attr(&mut m.attrs);
570
571 // clone for RPC handler generation
572 rpc_methods.push(itm.clone());
573
574 // keep original method bodies *only* for trait impls so
575 // server‑side logic is still present
576 if is_trait_impl {
577 keep_methods.push(itm);
578 }
579 // for inherent impls we strip the method here – it will
580 // re‑appear inside the generated `impl <Trait> for Type`
581 } else {
582 remove_local_attr(&mut m.attrs);
583 keep_methods.push(itm);
584 }
585 } else {
586 keep_methods.push(itm);
587 }
588 }
589 imp.items = keep_methods;
590
591 /* ensure async‑trait on impl <Trait> for <Type> */
592 if is_trait_impl {
593 let has_async_trait_attr = imp.attrs.iter().any(|a| {
594 a.path()
595 .segments
596 .last()
597 .map(|s| s.ident == "async_trait")
598 .unwrap_or(false)
599 });
600 if !has_async_trait_attr {
601 imp.attrs
602 .insert(0, syn::parse_quote!(#[rogue_runtime::async_trait]));
603 }
604 }
605
606 /* generate trait or stub‑only impls when we collected async RPC methods */
607 if !rpc_methods.is_empty() {
608 let mut trait_items: Vec<TokenStream2> = Vec::new();
609 let mut impl_items: Vec<TokenStream2> = Vec::new();
610 let mut stub_items: Vec<TokenStream2> = Vec::new();
611
612 for itm in &rpc_methods {
613 if let syn::ImplItem::Fn(m) = itm {
614 /* ------------- signature for generated trait (inherent impls) --------- */
615 let mut sig_trait = m.sig.clone();
616
617 let mut needs_auto_register = false;
618 if let syn::ReturnType::Type(_, ty) = &sig_trait.output {
619 if type_contains_self(&**ty) {
620 needs_auto_register = true;
621 }
622 }
623 if needs_auto_register {
624 sig_trait.generics.make_where_clause();
625 sig_trait
626 .generics
627 .where_clause
628 .as_mut()
629 .unwrap()
630 .predicates
631 .push(syn::parse_quote!(Self: rogue_runtime::AutoRegister + Sized));
632 }
633
634 trait_items.push(quote! { #sig_trait ; });
635
636 /* ------------- keep original method (for Impl type) ------------------- */
637 let mut m_impl = m.clone();
638 m_impl.vis = syn::Visibility::Inherited;
639 impl_items.push(m_impl.into_token_stream());
640
641 /* ------------- build ObjectRef stub method ---------------------------- */
642 let sig_stub = m.sig.clone();
643
644 let mut fld_inits = Vec::<TokenStream2>::new();
645 let mut has_receiver = false;
646 for arg in &m.sig.inputs {
647 match arg {
648 syn::FnArg::Receiver(_) => {
649 has_receiver = true;
650 }
651 syn::FnArg::Typed(pt) => {
652 if let syn::Pat::Ident(pat_ident) = &*pt.pat {
653 let nm = &pat_ident.ident;
654 fld_inits.push(quote! { #nm: #nm });
655 }
656 }
657 }
658 }
659 if has_receiver {
660 fld_inits.insert(0, quote! { __instance_id: *self.id() });
661 }
662
663 let pascal = m.sig.ident.to_string().to_pascal_case();
664 let struct_prefix = self_ident.to_string();
665 let handler_ident = Ident::new(
666 &format!("{}{}Handler", struct_prefix, pascal),
667 Span::call_site(),
668 );
669 let input_ident = Ident::new(
670 &format!("{}{}Input", struct_prefix, pascal),
671 Span::call_site(),
672 );
673
674 let ret_ty_tokens: TokenStream2 = match &m.sig.output {
675 syn::ReturnType::Type(_, ty) => quote! { #ty },
676 syn::ReturnType::Default => quote! { () },
677 };
678
679 let target_expr = if has_receiver {
680 quote! { self.runtime_id() }
681 } else {
682 quote! { ctx.target_id.clone() }
683 };
684
685 let rpc_name = quote! { concat!(stringify!(#struct_prefix), "::", stringify!(#pascal)) };
686 let body_stmts = build_rpc_call_body(
687 &input_ident,
688 &handler_ident,
689 &fld_inits,
690 quote! { #target_expr },
691 rpc_name,
692 quote! { #ret_ty_tokens },
693 );
694
695 stub_items.push(quote! {
696 #sig_stub {
697 #body_stmts
698 }
699 });
700
701 /* ------------- server‑side handler generation ------------------------- */
702 let handler_ts = generate_message_handler(
703 &m.sig.ident,
704 Some(&impl_ident),
705 &m.sig.inputs,
706 &m.sig.output,
707 );
708 extra.push(handler_ts);
709 }
710 }
711
712 let stub_ty: syn::Type =
713 syn::parse_quote!(rogue_runtime::ObjectRef<#impl_ident>);
714
715 if let Some((_, trait_path, _)) = &imp.trait_ {
716 /* ------ user wrote `impl SomeTrait for Type` --------------------------- */
717 let trait_block = quote! {
718 #[rogue_runtime::async_trait]
719 impl #trait_path for #stub_ty {
720 #(#stub_items)*
721 }
722 };
723 extra.push(trait_block);
724 } else {
725 /* ------ inherent impl: synthesise a new trait -------------------------- */
726 let trait_ident =
727 Ident::new(&format!("{}Trait", self_ident), Span::call_site());
728
729 let trait_block = quote! {
730 #[rogue_runtime::async_trait]
731 pub trait #trait_ident {
732 #(#trait_items)*
733 }
734
735 #[rogue_runtime::async_trait]
736 impl #trait_ident for #impl_ident {
737 #(#impl_items)*
738 }
739
740 #[rogue_runtime::async_trait]
741 impl #trait_ident for #stub_ty {
742 #(#stub_items)*
743 }
744 };
745 extra.push(trait_block);
746 }
747 }
748
749 emitted.push(item.into_token_stream());
750 } else {
751 // impl for unrelated type, pass through
752 emitted.push(item.into_token_stream());
753 }
754 } else {
755 emitted.push(item.into_token_stream());
756 }
757 }
758
759 /* ---- traits --------------------------------------------------------------- */
760 Item::Trait(trt) => {
761 // prepend #[rogue_runtime::async_trait] unless already present
762 let has_async_trait = trt.attrs.iter().any(|a| {
763 a.path()
764 .segments
765 .last()
766 .map(|s| s.ident == "async_trait")
767 .unwrap_or(false)
768 });
769 if !has_async_trait {
770 trt.attrs
771 .insert(0, syn::parse_quote!(#[rogue_runtime::async_trait]));
772 }
773 emitted.push(item.into_token_stream());
774 }
775 /* ---- everything else: pass through ---------------------------------------- */
776 _ => emitted.push(item.into_token_stream()),
777 }
778 }
779
780 /* ---------- final output ---------- */
781 emitted.extend(extra);
782
783 quote! { #(#emitted)* }.into()
784}