miniextendr_macros/miniextendr_trait.rs
1//! # Trait Support for `#[miniextendr]`
2//!
3//! This module handles `#[miniextendr]` applied to trait definitions,
4//! generating the ABI infrastructure for cross-package trait dispatch.
5//!
6//! ## Overview
7//!
8//! When `#[miniextendr]` is applied to a trait, it generates:
9//!
10//! 1. **Type tag constant** (`TAG_<TraitName>`) - 128-bit identifier for runtime type checking
11//! 2. **Vtable struct** (`<TraitName>VTable`) - Function pointer table for method dispatch
12//! 3. **View struct** (`<TraitName>View`) - Runtime wrapper combining data pointer and vtable
13//! 4. **Method shims** - `extern "C"` functions that convert SEXP arguments and call methods
14//! 5. **Vtable builder** - `__<trait>_build_vtable::<T>()` for impl blocks
15//!
16//! ## Usage
17//!
18//! ```ignore
19//! #[miniextendr]
20//! pub trait Counter {
21//! fn value(&self) -> i32;
22//! fn increment(&mut self);
23//! fn add(&mut self, n: i32);
24//! }
25//! ```
26//!
27//! Generates (conceptually):
28//!
29//! ```text
30//! // Original trait (passed through)
31//! pub trait Counter {
32//! fn value(&self) -> i32;
33//! fn increment(&mut self);
34//! fn add(&mut self, n: i32);
35//! }
36//!
37//! // Type tag for runtime identification
38//! pub const TAG_COUNTER: mx_tag = mx_tag::new(0x..., 0x...);
39//!
40//! // Vtable with one entry per method
41//! #[repr(C)]
42//! pub struct CounterVTable {
43//! pub value: mx_meth,
44//! pub increment: mx_meth,
45//! pub add: mx_meth,
46//! }
47//!
48//! // View combining data pointer and vtable
49//! #[repr(C)]
50//! pub struct CounterView {
51//! pub data: *mut std::ffi::c_void,
52//! pub vtable: *const CounterVTable,
53//! }
54//!
55//! // Shim for each method
56//! unsafe extern "C" fn __counter_value_shim<T: Counter>(
57//! data: *mut c_void, argc: i32, argv: *const SEXP
58//! ) -> SEXP {
59//! // 1. Check arity
60//! // 2. Cast data to &T
61//! // 3. Call method
62//! // 4. Convert result to SEXP
63//! // 5. Catch panics
64//! }
65//!
66//! // Builder to create vtable for a concrete type
67//! pub const fn __counter_build_vtable<T: Counter>() -> CounterVTable {
68//! CounterVTable {
69//! value: __counter_value_shim::<T>,
70//! increment: __counter_increment_shim::<T>,
71//! add: __counter_add_shim::<T>,
72//! }
73//! }
74//! ```
75//!
76//! ## Supported Method Signatures
77//!
78//! Methods must follow these constraints:
79//!
80//! - **Receiver**: `&self` or `&mut self` for instance methods, or none for static methods
81//! - **Arguments**: Types that implement `TryFromSexp`
82//! - **Return**: Types that implement `IntoR`, or `()`
83//! - **No generics**: Methods cannot have generic type parameters
84//! - **No async**: Async methods are not supported
85//! - **Static methods**: Methods without a receiver are allowed and resolved at compile time
86//! (they don't go through the vtable)
87//!
88//! ## Default Methods
89//!
90//! Default method implementations are supported. The vtable builder will
91//! use the default implementation if the concrete type doesn't override it.
92//!
93//! ```ignore
94//! #[miniextendr]
95//! pub trait Counter {
96//! fn value(&self) -> i32;
97//!
98//! // Default implementation - included in vtable
99//! fn is_zero(&self) -> bool {
100//! self.value() == 0
101//! }
102//! }
103//! ```
104//!
105//! ## Error Handling
106//!
107//! Method shims handle errors as follows:
108//!
109//! - **Arity mismatch**: Raises R error ("expected N arguments, got M")
110//! - **Type conversion failure**: Raises R error with the error message
111//! - **Panic**: Caught via `with_r_unwind_protect`, converted to R error
112//!
113//! ## Thread Safety
114//!
115//! All generated shims are **main-thread only**. They do not route through
116//! `with_r_thread` because R invokes `.Call` on the main thread.
117
118use proc_macro2::TokenStream;
119use syn::ItemTrait;
120
121/// Expand `#[miniextendr]` applied to a trait definition.
122///
123/// # Arguments
124///
125/// * `attr` - Attribute arguments (currently unused, reserved for future options)
126/// * `item` - The trait definition token stream
127///
128/// # Returns
129///
130/// Expanded token stream containing:
131/// - Original trait definition
132/// - Type tag constant
133/// - Vtable struct
134/// - View struct
135/// - Method shims
136/// - Vtable builder function
137///
138/// # Errors
139///
140/// Returns a compile error if:
141/// - Methods have unsupported signatures
142/// - Methods are async
143pub fn expand_trait(
144 _attr: proc_macro::TokenStream,
145 item: proc_macro::TokenStream,
146) -> proc_macro::TokenStream {
147 let trait_item = syn::parse_macro_input!(item as ItemTrait);
148
149 // Validate trait constraints
150 if let Err(e) = validate_trait(&trait_item) {
151 return e.into_compile_error().into();
152 }
153
154 // Generate the expanded code
155 let expanded = generate_trait_abi(&trait_item);
156
157 expanded.into()
158}
159
160/// Validate that the trait meets requirements for ABI generation.
161///
162/// # Constraints
163///
164/// - All methods must have `&self` or `&mut self` receiver
165/// - Methods cannot be async
166/// - Methods cannot have generic parameters
167/// - Generic type parameters on the trait itself are allowed
168fn validate_trait(trait_item: &ItemTrait) -> syn::Result<()> {
169 let trait_name = &trait_item.ident;
170
171 // Validate each method
172 for item in &trait_item.items {
173 if let syn::TraitItem::Fn(method) = item {
174 validate_method(method, trait_name)?;
175 }
176 }
177
178 Ok(())
179}
180
181/// Validate a single trait method for ABI compatibility.
182///
183/// Rejects async methods, methods with generic type parameters, and methods
184/// that take `self` by value (only `&self` and `&mut self` are allowed).
185/// Static methods (no receiver) are permitted.
186fn validate_method(method: &syn::TraitItemFn, trait_name: &syn::Ident) -> syn::Result<()> {
187 let method_name = &method.sig.ident;
188
189 // Check for async
190 if method.sig.asyncness.is_some() {
191 return Err(syn::Error::new_spanned(
192 method.sig.asyncness,
193 format!(
194 "#[miniextendr] trait `{}::{}` cannot be async",
195 trait_name, method_name
196 ),
197 ));
198 }
199
200 // Check for type/const generics on method.
201 // Lifetime params are allowed (erased at codegen); type/const params would require
202 // monomorphization and are incompatible with the vtable-shim codegen path.
203 {
204 let has_type_or_const = method
205 .sig
206 .generics
207 .params
208 .iter()
209 .any(|p| matches!(p, syn::GenericParam::Type(_) | syn::GenericParam::Const(_)));
210 if has_type_or_const {
211 return Err(syn::Error::new_spanned(
212 &method.sig.generics,
213 format!(
214 "#[miniextendr] trait method `{}::{}` cannot have generic type or const parameters",
215 trait_name, method_name
216 ),
217 ));
218 }
219 }
220
221 // Check receiver - must be &self, &mut self, self: &Self, self: &mut Self, or no receiver
222 // Static methods are allowed but won't be included in the vtable
223 // (they're resolved at compile time via <Type as Trait>::method())
224 let receiver = method.sig.inputs.first();
225 if let Some(syn::FnArg::Receiver(r)) = receiver {
226 // Accept either:
227 // - `&self` / `&mut self` (r.reference is Some)
228 // - `self: &Self` / `self: &mut Self` (r.colon_token is Some with reference type)
229 let is_ref = if r.reference.is_some() {
230 true
231 } else if r.colon_token.is_some() {
232 // Check if the type is a reference type (&Self or &mut Self)
233 matches!(r.ty.as_ref(), syn::Type::Reference(_))
234 } else {
235 false
236 };
237
238 if !is_ref {
239 return Err(syn::Error::new_spanned(
240 r,
241 format!(
242 "#[miniextendr] trait method `{}::{}` receiver must be `&self` or `&mut self`, not `self` by value",
243 trait_name, method_name
244 ),
245 ));
246 }
247 }
248 // If receiver is None or FnArg::Typed (no self), it's a static method - allowed
249
250 Ok(())
251}
252
253/// Generate the ABI infrastructure for a trait.
254///
255/// This is the main code generation function that produces:
256/// - Type tag constant
257/// - Vtable struct
258/// - View struct (skipped for generic traits)
259/// - Method shims (with trait type params threaded through)
260/// - Vtable builder (with trait type params threaded through)
261fn generate_trait_abi(trait_item: &ItemTrait) -> TokenStream {
262 let trait_name = &trait_item.ident;
263 let vis = &trait_item.vis;
264
265 // Generate names for generated items
266 let tag_name = quote::format_ident!("TAG_{}", trait_name.to_string().to_uppercase());
267 let vtable_name = quote::format_ident!("{}VTable", trait_name);
268 let view_name = quote::format_ident!("{}View", trait_name);
269 let build_vtable_fn =
270 quote::format_ident!("__{}_build_vtable", trait_name.to_string().to_lowercase());
271
272 // Collect trait-level generic type parameters
273 let trait_type_params: Vec<&syn::GenericParam> = trait_item.generics.params.iter().collect();
274 let trait_param_idents: Vec<&syn::Ident> = trait_type_params
275 .iter()
276 .filter_map(|p| {
277 if let syn::GenericParam::Type(tp) = p {
278 Some(&tp.ident)
279 } else {
280 None
281 }
282 })
283 .collect();
284 let has_generics = !trait_param_idents.is_empty();
285
286 // Collect associated types
287 let assoc_types: Vec<&syn::Ident> = trait_item
288 .items
289 .iter()
290 .filter_map(|item| {
291 if let syn::TraitItem::Type(t) = item {
292 Some(&t.ident)
293 } else {
294 None
295 }
296 })
297 .collect();
298
299 // Collect trait where clause
300 let trait_where_clause = &trait_item.generics.where_clause;
301
302 // Collect method information
303 // Filter to only include instance methods (with &self or &mut self) that aren't skipped
304 let methods: Vec<_> = {
305 let mut collected = Vec::new();
306 for item in &trait_item.items {
307 if let syn::TraitItem::Fn(method) = item {
308 let info = match extract_method_info(method) {
309 Ok(info) => info,
310 Err(e) => return e.into_compile_error(),
311 };
312 if info.has_self && !info.skip {
313 collected.push(info);
314 }
315 }
316 }
317 collected
318 }
319 .into_iter()
320 .collect();
321
322 // Generate tag path string for hashing
323 // IMPORTANT: For cross-package trait dispatch, the tag must NOT include module_path!()
324 // Different packages defining the same trait signature should get the same tag.
325 // We use just the trait name - in practice, trait names + methods should be unique enough.
326 let tag_path = trait_name.to_string();
327
328 // Generate vtable fields
329 let vtable_fields: Vec<_> = methods
330 .iter()
331 .map(|m| {
332 let name = &m.name;
333 quote::quote! {
334 pub #name: ::miniextendr_api::abi::mx_meth
335 }
336 })
337 .collect();
338
339 // Compute extra bounds needed for shims
340 let extra_bounds =
341 compute_extra_bounds(&methods, trait_name, &assoc_types, &trait_param_idents);
342
343 // Build trait bound for __ImplT
344 let impl_t = quote::format_ident!("__ImplT");
345 let trait_bound = if trait_param_idents.is_empty() {
346 quote::quote! { #trait_name }
347 } else {
348 quote::quote! { #trait_name<#(#trait_param_idents),*> }
349 };
350
351 // Build combined where clause
352 let all_where_predicates = build_where_predicates(trait_where_clause, &extra_bounds);
353 let where_clause = if all_where_predicates.is_empty() {
354 quote::quote! {}
355 } else {
356 quote::quote! { where #(#all_where_predicates),* }
357 };
358
359 // Generate shim functions
360 let shim_fns: Vec<_> = methods
361 .iter()
362 .map(|m| {
363 generate_method_shim(
364 trait_name,
365 m,
366 &extra_bounds,
367 &trait_param_idents,
368 &trait_type_params,
369 trait_where_clause,
370 )
371 })
372 .collect();
373
374 // Generate vtable field initializers (turbofish includes trait type params)
375 let vtable_inits: Vec<_> = methods
376 .iter()
377 .map(|m| {
378 let name = &m.name;
379 let shim_name =
380 quote::format_ident!("__{}_{}_shim", trait_name.to_string().to_lowercase(), name);
381 quote::quote! {
382 #name: #shim_name::<#(#trait_param_idents,)* #impl_t>
383 }
384 })
385 .collect();
386
387 // Generate method wrappers for the View struct
388 // Skip entirely for generic traits (type-erased view can't know type params)
389 let view_methods: Vec<_> = if has_generics {
390 vec![]
391 } else {
392 methods.iter().filter_map(generate_view_method).collect()
393 };
394
395 // Strip #[miniextendr(...)] attrs from trait items before emitting
396 let mut clean_trait = trait_item.clone();
397 for item in &mut clean_trait.items {
398 if let syn::TraitItem::Fn(method) = item {
399 method
400 .attrs
401 .retain(|attr| !attr.path().is_ident("miniextendr"));
402 }
403 }
404
405 let trait_name_str = trait_name.to_string();
406 let source_loc_doc = crate::source_location_doc(trait_name.span());
407
408 let impl_bounds = &extra_bounds.impl_bounds;
409
410 // View struct and its impls (skipped for generic traits)
411 let view_tokens = if has_generics {
412 quote::quote! {}
413 } else {
414 quote::quote! {
415 #[doc = concat!(
416 "Runtime view for objects implementing `",
417 stringify!(#trait_name),
418 "`."
419 )]
420 #[doc = #source_loc_doc]
421 #[doc = concat!("Generated from source file `", file!(), "`.")]
422 ///
423 /// Combines a data pointer with a vtable pointer for method dispatch.
424 /// Use `try_from_sexp` to create a view from an R external pointer.
425 #[repr(C)]
426 #vis struct #view_name {
427 /// Pointer to the concrete object data.
428 pub data: *mut ::std::os::raw::c_void,
429 /// Pointer to the vtable for this trait.
430 pub vtable: *const #vtable_name,
431 }
432
433 // TraitView implementation
434 impl ::miniextendr_api::TraitView for #view_name {
435 const TAG: ::miniextendr_api::abi::mx_tag = #tag_name;
436
437 #[inline]
438 unsafe fn from_raw_parts(
439 data: *mut ::std::os::raw::c_void,
440 vtable: *const ::std::os::raw::c_void,
441 ) -> Self {
442 Self {
443 data,
444 vtable: vtable.cast::<#vtable_name>(),
445 }
446 }
447 }
448
449 // Method wrappers on View
450 impl #view_name {
451 /// Try to create a view from an R SEXP.
452 ///
453 /// Returns `Some(Self)` if the object implements this trait,
454 /// `None` otherwise.
455 ///
456 /// # Safety
457 ///
458 /// - `sexp` must be a valid R external pointer (EXTPTRSXP)
459 /// - Must be called on R's main thread
460 #[inline]
461 pub unsafe fn try_from_sexp(sexp: ::miniextendr_api::SEXP) -> Option<Self> {
462 <Self as ::miniextendr_api::TraitView>::try_from_sexp(sexp)
463 }
464
465 /// Try to create a view, panicking with error message on failure.
466 ///
467 /// # Safety
468 ///
469 /// Same as `try_from_sexp`.
470 #[inline]
471 pub unsafe fn from_sexp(sexp: ::miniextendr_api::SEXP) -> Self {
472 Self::try_from_sexp(sexp)
473 .expect(concat!("Object does not implement ", #trait_name_str, " trait"))
474 }
475
476 #(#view_methods)*
477 }
478 }
479 };
480
481 // For generic traits (with type params like <T>), skip shim and builder generation.
482 // These are generated at the impl site with concrete types to avoid recursive trait
483 // resolution overflow (e.g., `Vec<T>: TryFromSexp` triggers infinite recursion through
484 // `impl<T> TryFromSexp for Vec<Vec<T>>`).
485 let shim_and_builder = if has_generics {
486 quote::quote! {}
487 } else {
488 quote::quote! {
489 // Method shims
490 #(#shim_fns)*
491
492 #[doc = concat!(
493 "Build a vtable for a concrete type implementing `",
494 stringify!(#trait_name),
495 "`."
496 )]
497 #[doc = #source_loc_doc]
498 #[doc = concat!("Generated from source file `", file!(), "`.")]
499 #vis const fn #build_vtable_fn<#(#trait_type_params,)* #impl_t: #trait_bound #(+ #impl_bounds)*>() -> #vtable_name
500 #where_clause
501 {
502 #vtable_name {
503 #(#vtable_inits),*
504 }
505 }
506 }
507 };
508
509 // TPIE: Generate macro_rules! for non-generic traits without associated types.
510 // This enables `#[miniextendr] impl Trait for Type {}` (empty body) to auto-expand wrappers.
511 let tpie_macro = if !has_generics && assoc_types.is_empty() {
512 // Collect ALL non-skipped methods (including static) for TPIE metadata
513 let tpie_method_metadata: Vec<TokenStream> = {
514 let mut collected = Vec::new();
515 for item in &trait_item.items {
516 if let syn::TraitItem::Fn(method) = item {
517 let info = match extract_method_info(method) {
518 Ok(info) => info,
519 Err(e) => return e.into_compile_error(),
520 };
521 if !info.skip {
522 let r_name_ident = if let Some(ref rn) = info.r_name {
523 quote::format_ident!("{}", rn)
524 } else {
525 method.sig.ident.clone()
526 };
527 let sig = &method.sig;
528 collected.push(quote::quote! {
529 method { r_name = #r_name_ident; #sig; }
530 });
531 }
532 }
533 }
534 collected
535 };
536
537 let tpie_macro_name = quote::format_ident!("__mx_impl_{}", trait_name);
538 quote::quote! {
539 #[macro_export]
540 #[doc(hidden)]
541 macro_rules! #tpie_macro_name {
542 ($concrete_type:ty, $trait_path:path, $class_system:ident, $no_rd:tt, $internal:tt, $noexport:tt) => {
543 $crate::__mx_trait_impl_expand! {
544 concrete_type = $concrete_type;
545 trait_path = $trait_path;
546 class_system = $class_system;
547 no_rd = $no_rd;
548 internal = $internal;
549 noexport = $noexport;
550 #(#tpie_method_metadata)*
551 }
552 };
553 }
554 }
555 } else {
556 quote::quote! {}
557 };
558
559 quote::quote! {
560 // Pass through the original trait (with #[miniextendr] attrs stripped from items)
561 #clean_trait
562
563 #[doc = concat!(
564 "Type tag for runtime identification of the `",
565 stringify!(#trait_name),
566 "` trait."
567 )]
568 #[doc = #source_loc_doc]
569 #[doc = concat!("Generated from source file `", file!(), "`.")]
570 #vis const #tag_name: ::miniextendr_api::abi::mx_tag =
571 ::miniextendr_api::abi::mx_tag_from_path(#tag_path);
572
573 #[doc = concat!("Vtable for the `", stringify!(#trait_name), "` trait.")]
574 #[doc = #source_loc_doc]
575 #[doc = concat!("Generated from source file `", file!(), "`.")]
576 ///
577 /// Contains one `mx_meth` function pointer per trait method.
578 #[repr(C)]
579 #[doc(hidden)]
580 #vis struct #vtable_name {
581 #(#vtable_fields),*
582 }
583
584 #view_tokens
585
586 #shim_and_builder
587
588 #tpie_macro
589 }
590}
591
592/// Generate a method wrapper for the View struct.
593///
594/// This creates a method on the View that calls through the vtable.
595/// Returns None for methods with `Self` in return types or `&Self` in parameters,
596/// since these can't be meaningfully expressed on the type-erased View.
597fn generate_view_method(method: &MethodInfo) -> Option<TokenStream> {
598 // Skip methods where Self appears in return type or parameters.
599 // In the View context, Self refers to the View struct, not the concrete type,
600 // so these methods can't work through the type-erased vtable dispatch.
601 if method.return_type.as_ref().is_some_and(type_contains_self) {
602 return None;
603 }
604 if method.param_types.iter().any(type_contains_self) {
605 return None;
606 }
607
608 let method_name = &method.name;
609 let param_names = &method.param_names;
610 let param_types = &method.param_types;
611
612 // Generate function parameters
613 let params: Vec<_> = param_names
614 .iter()
615 .zip(param_types.iter())
616 .map(|(name, ty)| {
617 quote::quote! { #name: #ty }
618 })
619 .collect();
620
621 // Generate self receiver
622 let self_param = if method.is_mut {
623 quote::quote! { &mut self }
624 } else {
625 quote::quote! { &self }
626 };
627
628 // Generate argument array for vtable call
629 let argc = param_types.len() as i32;
630 let arg_conversions: Vec<_> = param_names
631 .iter()
632 .map(|name| {
633 quote::quote! {
634 ::miniextendr_api::trait_abi::to_sexp(#name)
635 }
636 })
637 .collect();
638
639 // Generate vtable call
640 let vtable_call = if argc > 0 {
641 quote::quote! {
642 let args: [::miniextendr_api::SEXP; #argc as usize] = [#(#arg_conversions),*];
643 ((*self.vtable).#method_name)(self.data, #argc, args.as_ptr())
644 }
645 } else {
646 quote::quote! {
647 ((*self.vtable).#method_name)(self.data, 0, ::std::ptr::null())
648 }
649 };
650
651 // Generate return type handling
652 let return_type = &method.return_type;
653 let (return_sig, result_conversion) = if let Some(ret_ty) = return_type {
654 (
655 quote::quote! { -> #ret_ty },
656 quote::quote! {
657 ::miniextendr_api::trait_abi::from_sexp::<#ret_ty>(result)
658 },
659 )
660 } else {
661 (
662 quote::quote! {},
663 quote::quote! {
664 let _ = result;
665 },
666 )
667 };
668
669 Some(quote::quote! {
670 #[doc = concat!("Call `", stringify!(#method_name), "` through the vtable.")]
671 #[inline]
672 pub fn #method_name(#self_param #(, #params)*) #return_sig {
673 unsafe {
674 let result = { #vtable_call };
675 // Approach 1 (issue #345): if the shim returned a tagged error SEXP,
676 // re-panic with the reconstructed RCondition so the consumer's outer
677 // `with_r_unwind_protect` guard can apply rust_* class layering.
678 ::miniextendr_api::trait_abi::repanic_if_rust_error(result);
679 #result_conversion
680 }
681 }
682 })
683}
684
685/// Generate a method shim function for a trait method.
686///
687/// The shim is an `extern "C"` function that:
688/// 1. Checks argument arity
689/// 2. Wraps everything in `with_r_unwind_protect` to prevent unwinding across FFI
690/// 3. Converts SEXP arguments to Rust types
691/// 4. Calls the actual method on the concrete type
692/// 5. Converts the result back to SEXP
693/// 6. On panic, converts to R error via `with_r_unwind_protect`
694///
695/// For generic traits, the shim carries the trait's type parameters plus `__ImplT`.
696fn generate_method_shim(
697 trait_name: &syn::Ident,
698 method: &MethodInfo,
699 extra_bounds: &ExtraBounds,
700 trait_param_idents: &[&syn::Ident],
701 trait_type_params: &[&syn::GenericParam],
702 trait_where_clause: &Option<syn::WhereClause>,
703) -> TokenStream {
704 let method_name = &method.name;
705 let shim_name = quote::format_ident!(
706 "__{}_{}_shim",
707 trait_name.to_string().to_lowercase(),
708 method_name
709 );
710 let impl_t = quote::format_ident!("__ImplT");
711
712 let param_count = method.param_types.len();
713 let expected_argc = param_count as i32;
714
715 // Generate argument extraction
716 // For &Self params, extract ExternalPtr<__ImplT> and borrow from it
717 let arg_extractions: Vec<_> = method
718 .param_names
719 .iter()
720 .zip(method.param_types.iter())
721 .enumerate()
722 .map(|(i, (name, ty))| {
723 let name_str = name.to_string();
724 let (is_self_ref, is_mut) = param_is_self_ref(ty);
725 if is_self_ref {
726 let extptr_name = quote::format_ident!("__extptr_{}", name);
727 if is_mut {
728 quote::quote! {
729 let mut #extptr_name: ::miniextendr_api::ExternalPtr<#impl_t> = unsafe {
730 ::miniextendr_api::trait_abi::extract_arg(argc, argv, #i, #name_str)
731 };
732 let #name: &mut #impl_t = &mut *#extptr_name;
733 }
734 } else {
735 quote::quote! {
736 let #extptr_name: ::miniextendr_api::ExternalPtr<#impl_t> = unsafe {
737 ::miniextendr_api::trait_abi::extract_arg(argc, argv, #i, #name_str)
738 };
739 let #name: &#impl_t = &*#extptr_name;
740 }
741 }
742 } else {
743 quote::quote! {
744 let #name: #ty = unsafe {
745 ::miniextendr_api::trait_abi::extract_arg(argc, argv, #i, #name_str)
746 };
747 }
748 }
749 })
750 .collect();
751
752 // Generate method call (uses __ImplT)
753 let param_names = &method.param_names;
754 let method_call = if method.is_mut {
755 quote::quote! {
756 let self_ref = unsafe { &mut *data.cast::<#impl_t>() };
757 self_ref.#method_name(#(#param_names),*)
758 }
759 } else {
760 quote::quote! {
761 let self_ref = unsafe { &*data.cast::<#impl_t>().cast_const() };
762 self_ref.#method_name(#(#param_names),*)
763 }
764 };
765
766 // Generate result conversion
767 let result_conversion = if method.return_type.is_some() {
768 quote::quote! {
769 unsafe { ::miniextendr_api::trait_abi::to_sexp(result) }
770 }
771 } else {
772 quote::quote! {
773 let _ = result;
774 unsafe { ::miniextendr_api::trait_abi::nil() }
775 }
776 };
777
778 // Build trait bound for __ImplT
779 let trait_bound = if trait_param_idents.is_empty() {
780 quote::quote! { #trait_name }
781 } else {
782 quote::quote! { #trait_name<#(#trait_param_idents),*> }
783 };
784
785 let impl_bounds = &extra_bounds.impl_bounds;
786 let all_where_predicates = build_where_predicates(trait_where_clause, extra_bounds);
787 let where_clause = if all_where_predicates.is_empty() {
788 quote::quote! {}
789 } else {
790 quote::quote! { where #(#all_where_predicates),* }
791 };
792
793 let method_name_str = format!("{}::{}", trait_name, method_name);
794
795 quote::quote! {
796 #[doc = concat!(
797 "Method shim for `",
798 stringify!(#trait_name),
799 "::",
800 stringify!(#method_name),
801 "`."
802 )]
803 ///
804 /// Converts SEXP arguments, calls the method, and returns SEXP result.
805 /// Both Rust panics and R longjmps are caught via `with_r_unwind_protect`.
806 #[doc(hidden)]
807 unsafe extern "C" fn #shim_name<#(#trait_type_params,)* #impl_t: #trait_bound #(+ #impl_bounds)*>(
808 data: *mut ::std::os::raw::c_void,
809 argc: i32,
810 argv: *const ::miniextendr_api::SEXP,
811 ) -> ::miniextendr_api::SEXP
812 #where_clause
813 {
814 // Check arity (before unwind protect - uses r_stop which doesn't return)
815 unsafe {
816 ::miniextendr_api::trait_abi::check_arity(argc, #expected_argc, #method_name_str);
817 }
818
819 // Wrap in with_r_unwind_protect_shim: catches Rust panics and returns
820 // a tagged error SEXP instead of calling Rf_errorcall directly. The
821 // tagged SEXP is returned to the View method wrapper which re-panics
822 // via repanic_if_rust_error, allowing the consumer's outer
823 // `with_r_unwind_protect` guard to produce rust_* class layering
824 // (issue #345). R-origin longjmps still propagate via R_ContinueUnwind.
825 ::miniextendr_api::unwind_protect::with_r_unwind_protect_shim(|| {
826 // Extract arguments
827 #(#arg_extractions)*
828
829 // Call method
830 let result = { #method_call };
831
832 // Convert result
833 #result_conversion
834 })
835 }
836 }
837}
838
839/// Information extracted from a trait method for code generation.
840///
841/// Collects everything needed to generate vtable shims, view methods,
842/// and extra trait bounds for a single method in a `#[miniextendr]` trait.
843#[derive(Debug)]
844struct MethodInfo {
845 /// Method name (Rust identifier).
846 name: syn::Ident,
847 /// Whether the method has a self receiver (instance method).
848 /// False for static/associated methods.
849 has_self: bool,
850 /// Whether receiver is `&mut self` (vs `&self`). Only meaningful if `has_self` is true.
851 is_mut: bool,
852 /// Parameter types (excluding the self receiver).
853 param_types: Vec<syn::Type>,
854 /// Parameter names (excluding the self receiver). Uses `arg{i}` for unnamed patterns.
855 param_names: Vec<syn::Ident>,
856 /// Return type. `None` when the method returns `()` (unit type or no return annotation).
857 return_type: Option<syn::Type>,
858 /// Whether method is marked `#[miniextendr(skip)]`, excluding it from codegen.
859 skip: bool,
860 /// Override the R-facing method name (from `#[miniextendr(r_name = "...")]`).
861 /// When set, R wrappers and TPIE metadata use this name instead of the Rust ident.
862 r_name: Option<String>,
863}
864
865// region: Self-type detection helpers
866
867/// Check if a type syntactically contains `Self`.
868///
869/// Used to detect when a method returns `Self` (or `Option<Self>`, `Vec<Self>`, etc.)
870/// so the generated shim can add `IntoR` bounds.
871fn type_contains_self(ty: &syn::Type) -> bool {
872 match ty {
873 syn::Type::Path(tp) => {
874 for seg in &tp.path.segments {
875 if seg.ident == "Self" {
876 return true;
877 }
878 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
879 for arg in &args.args {
880 if let syn::GenericArgument::Type(inner) = arg
881 && type_contains_self(inner)
882 {
883 return true;
884 }
885 }
886 }
887 }
888 false
889 }
890 syn::Type::Reference(r) => type_contains_self(&r.elem),
891 syn::Type::Tuple(t) => t.elems.iter().any(type_contains_self),
892 syn::Type::Slice(s) => type_contains_self(&s.elem),
893 syn::Type::Array(a) => type_contains_self(&a.elem),
894 syn::Type::Paren(p) => type_contains_self(&p.elem),
895 _ => false,
896 }
897}
898
899/// Check if a parameter type is `&Self` or `&mut Self`.
900///
901/// Returns `(is_self_ref, is_mut)`. When true, the generated shim extracts
902/// an `ExternalPtr<T>` from the SEXP and borrows from it instead of trying
903/// to extract `&T` directly (which doesn't implement `TryFromSexp`).
904fn param_is_self_ref(ty: &syn::Type) -> (bool, bool) {
905 if let syn::Type::Reference(r) = ty
906 && let syn::Type::Path(tp) = r.elem.as_ref()
907 && tp.path.is_ident("Self")
908 {
909 return (true, r.mutability.is_some());
910 }
911 (false, false)
912}
913
914/// Check if a type syntactically contains `Self::AssocType` for a given associated type name.
915///
916/// Recursively walks the type tree looking for a 2-segment path where the first
917/// segment is `Self` and the second matches `assoc_name` (e.g., `Self::Item`).
918/// Used to determine whether extra `where` bounds are needed for associated types.
919fn type_contains_self_assoc(ty: &syn::Type, assoc_name: &syn::Ident) -> bool {
920 match ty {
921 syn::Type::Path(tp) => {
922 if tp.path.segments.len() == 2
923 && tp.path.segments[0].ident == "Self"
924 && tp.path.segments[1].ident == *assoc_name
925 {
926 return true;
927 }
928 for seg in &tp.path.segments {
929 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
930 for arg in &args.args {
931 if let syn::GenericArgument::Type(inner) = arg
932 && type_contains_self_assoc(inner, assoc_name)
933 {
934 return true;
935 }
936 }
937 }
938 }
939 false
940 }
941 syn::Type::Reference(r) => type_contains_self_assoc(&r.elem, assoc_name),
942 syn::Type::Tuple(t) => t
943 .elems
944 .iter()
945 .any(|e| type_contains_self_assoc(e, assoc_name)),
946 syn::Type::Slice(s) => type_contains_self_assoc(&s.elem, assoc_name),
947 syn::Type::Array(a) => type_contains_self_assoc(&a.elem, assoc_name),
948 syn::Type::Paren(p) => type_contains_self_assoc(&p.elem, assoc_name),
949 _ => false,
950 }
951}
952
953/// Rewrite `Self` and `Self::AssocType` in a type tree to use `__ImplT`.
954///
955/// Transforms:
956/// - `Self` → `__ImplT`
957/// - `Self::Item` → `<__ImplT as TraitName>::Item`
958/// - Recursively processes generic arguments (e.g., `Option<Self::Item>` →
959/// `Option<<__ImplT as TraitName>::Item>`)
960fn rewrite_self_in_type(
961 ty: &syn::Type,
962 trait_name: &syn::Ident,
963 assoc_types: &[&syn::Ident],
964) -> syn::Type {
965 match ty {
966 syn::Type::Path(tp) => {
967 // Check for Self::AssocType (2-segment path: Self::Item)
968 if tp.path.segments.len() == 2
969 && tp.path.segments[0].ident == "Self"
970 && assoc_types.iter().any(|a| *a == &tp.path.segments[1].ident)
971 {
972 let assoc = &tp.path.segments[1].ident;
973 let impl_t = quote::format_ident!("__ImplT");
974 return syn::parse_quote!(<#impl_t as #trait_name>::#assoc);
975 }
976 // Check for bare Self
977 if tp.path.is_ident("Self") {
978 let impl_t = quote::format_ident!("__ImplT");
979 return syn::parse_quote!(#impl_t);
980 }
981 // Recursively process generic args
982 let mut new_tp = tp.clone();
983 for seg in &mut new_tp.path.segments {
984 if let syn::PathArguments::AngleBracketed(args) = &mut seg.arguments {
985 for arg in &mut args.args {
986 if let syn::GenericArgument::Type(inner) = arg {
987 *inner = rewrite_self_in_type(inner, trait_name, assoc_types);
988 }
989 }
990 }
991 }
992 syn::Type::Path(new_tp)
993 }
994 syn::Type::Reference(r) => {
995 let mut new_r = r.clone();
996 new_r.elem = Box::new(rewrite_self_in_type(&r.elem, trait_name, assoc_types));
997 syn::Type::Reference(new_r)
998 }
999 syn::Type::Tuple(t) => {
1000 let mut new_t = t.clone();
1001 for elem in &mut new_t.elems {
1002 *elem = rewrite_self_in_type(elem, trait_name, assoc_types);
1003 }
1004 syn::Type::Tuple(new_t)
1005 }
1006 syn::Type::Slice(s) => {
1007 let mut new_s = s.clone();
1008 new_s.elem = Box::new(rewrite_self_in_type(&s.elem, trait_name, assoc_types));
1009 syn::Type::Slice(new_s)
1010 }
1011 syn::Type::Array(a) => {
1012 let mut new_a = a.clone();
1013 new_a.elem = Box::new(rewrite_self_in_type(&a.elem, trait_name, assoc_types));
1014 syn::Type::Array(new_a)
1015 }
1016 syn::Type::Paren(p) => {
1017 let mut new_p = p.clone();
1018 new_p.elem = Box::new(rewrite_self_in_type(&p.elem, trait_name, assoc_types));
1019 syn::Type::Paren(new_p)
1020 }
1021 _ => ty.clone(),
1022 }
1023}
1024
1025/// Check if a type syntactically contains a specific identifier.
1026///
1027/// Used to detect trait type parameters (like `T`) in method signatures so that
1028/// appropriate `TryFromSexp` or `IntoR` bounds can be added. Recursively walks
1029/// through path segments, generic arguments, references, tuples, slices, and arrays.
1030fn type_contains_ident(ty: &syn::Type, ident: &syn::Ident) -> bool {
1031 match ty {
1032 syn::Type::Path(tp) => {
1033 if tp.path.segments.len() == 1 && tp.path.segments[0].ident == *ident {
1034 return true;
1035 }
1036 for seg in &tp.path.segments {
1037 if let syn::PathArguments::AngleBracketed(args) = &seg.arguments {
1038 for arg in &args.args {
1039 if let syn::GenericArgument::Type(inner) = arg
1040 && type_contains_ident(inner, ident)
1041 {
1042 return true;
1043 }
1044 }
1045 }
1046 }
1047 false
1048 }
1049 syn::Type::Reference(r) => type_contains_ident(&r.elem, ident),
1050 syn::Type::Tuple(t) => t.elems.iter().any(|e| type_contains_ident(e, ident)),
1051 syn::Type::Slice(s) => type_contains_ident(&s.elem, ident),
1052 syn::Type::Array(a) => type_contains_ident(&a.elem, ident),
1053 syn::Type::Paren(p) => type_contains_ident(&p.elem, ident),
1054 _ => false,
1055 }
1056}
1057
1058/// Extra trait bounds inferred from method signatures.
1059///
1060/// For generic traits and methods that reference `Self` or associated types,
1061/// the generated shim and vtable builder functions need additional bounds
1062/// beyond `__ImplT: TraitName`. This struct collects those bounds.
1063struct ExtraBounds {
1064 /// Bounds added directly to `__ImplT` (e.g., `IntoR` when methods return `Self`,
1065 /// or `TypedExternal + Send + 'static` when methods take `&Self` parameters).
1066 impl_bounds: Vec<TokenStream>,
1067 /// Where clause predicates for complex types (e.g.,
1068 /// `<__ImplT as Trait>::Item: IntoR` or `Vec<T>: TryFromSexp`).
1069 where_predicates: Vec<TokenStream>,
1070}
1071
1072/// Compute extra bounds needed for the shim and build_vtable functions.
1073///
1074/// - Methods returning `Self` → `__ImplT: IntoR`
1075/// - Methods with `&Self` params → `__ImplT: TypedExternal + Send + 'static`
1076/// - Methods returning types with `Self::AssocType` or trait type params →
1077/// full rewritten return type `: IntoR` (e.g., `Option<<__ImplT as RIterator>::Item>: IntoR`)
1078/// - Methods with trait type params in params →
1079/// full param type `: TryFromSexp` (e.g., `Vec<T>: TryFromSexp`)
1080fn compute_extra_bounds(
1081 methods: &[MethodInfo],
1082 trait_name: &syn::Ident,
1083 assoc_types: &[&syn::Ident],
1084 trait_param_idents: &[&syn::Ident],
1085) -> ExtraBounds {
1086 let mut impl_bounds = Vec::new();
1087 let mut where_predicates = Vec::new();
1088
1089 let mut needs_into_r = false;
1090 let mut needs_typed_external = false;
1091
1092 // Track full types needing bounds (deduplicated by token string)
1093 let mut return_type_bound_keys: std::collections::BTreeMap<String, syn::Type> =
1094 Default::default();
1095 let mut param_type_bound_keys: std::collections::BTreeMap<String, syn::Type> =
1096 Default::default();
1097
1098 for method in methods {
1099 // Bare Self in returns → __ImplT: IntoR (as impl bound)
1100 if method.return_type.as_ref().is_some_and(type_contains_self) {
1101 needs_into_r = true;
1102 }
1103 // &Self in params → __ImplT: TypedExternal + 'static
1104 if method.param_types.iter().any(|ty| param_is_self_ref(ty).0) {
1105 needs_typed_external = true;
1106 }
1107
1108 // Full return type bounds for Self::AssocType and/or trait type params.
1109 // Instead of bare `<__ImplT as Trait>::Item: IntoR`, we add the FULL rewritten
1110 // return type: `Option<<__ImplT as Trait>::Item>: IntoR`, `Vec<T>: IntoR`, etc.
1111 // This is required because IntoR impls are concrete (no blanket `Option<T: IntoR>: IntoR`).
1112 if let Some(ref ret_ty) = method.return_type {
1113 let has_assoc = assoc_types
1114 .iter()
1115 .any(|a| type_contains_self_assoc(ret_ty, a));
1116 let has_param = trait_param_idents
1117 .iter()
1118 .any(|p| type_contains_ident(ret_ty, p));
1119 if has_assoc || has_param {
1120 let rewritten = rewrite_self_in_type(ret_ty, trait_name, assoc_types);
1121 let key = quote::quote!(#rewritten).to_string();
1122 return_type_bound_keys.entry(key).or_insert(rewritten);
1123 }
1124 }
1125
1126 // Full param type bounds for trait type params.
1127 // Instead of bare `T: TryFromSexp`, we add `Vec<T>: TryFromSexp` etc.
1128 for param_ty in &method.param_types {
1129 if !param_is_self_ref(param_ty).0 {
1130 let has_param = trait_param_idents
1131 .iter()
1132 .any(|p| type_contains_ident(param_ty, p));
1133 if has_param {
1134 let key = quote::quote!(#param_ty).to_string();
1135 param_type_bound_keys.entry(key).or_insert(param_ty.clone());
1136 }
1137 }
1138 }
1139 }
1140
1141 if needs_into_r {
1142 impl_bounds.push(quote::quote! { ::miniextendr_api::IntoR });
1143 }
1144 if needs_typed_external {
1145 impl_bounds.push(quote::quote! { ::miniextendr_api::TypedExternal + Send + 'static });
1146 }
1147
1148 // Add full return type bounds: RewrittenType: IntoR
1149 for ty in return_type_bound_keys.values() {
1150 where_predicates.push(quote::quote! {
1151 #ty: ::miniextendr_api::IntoR
1152 });
1153 }
1154
1155 // Add full param type bounds: ParamType: TryFromSexp, Error: Display
1156 for ty in param_type_bound_keys.values() {
1157 where_predicates.push(quote::quote! {
1158 #ty: ::miniextendr_api::TryFromSexp
1159 });
1160 where_predicates.push(quote::quote! {
1161 <#ty as ::miniextendr_api::TryFromSexp>::Error: ::std::fmt::Display
1162 });
1163 }
1164
1165 ExtraBounds {
1166 impl_bounds,
1167 where_predicates,
1168 }
1169}
1170
1171/// Build combined where predicates from the trait's own where clause and computed extra bounds.
1172///
1173/// Merges the original trait-level where clause predicates with the extra
1174/// bounds computed from method signatures (e.g., `IntoR` for return types,
1175/// `TryFromSexp` for parameters containing trait type params).
1176///
1177/// Returns a flat list of predicates suitable for use in a `where` clause.
1178fn build_where_predicates(
1179 trait_where_clause: &Option<syn::WhereClause>,
1180 extra_bounds: &ExtraBounds,
1181) -> Vec<TokenStream> {
1182 let mut all = Vec::new();
1183 if let Some(wc) = trait_where_clause {
1184 for pred in &wc.predicates {
1185 all.push(quote::quote! { #pred });
1186 }
1187 }
1188 all.extend(extra_bounds.where_predicates.iter().cloned());
1189 all
1190}
1191
1192/// Extract method information from a trait method definition.
1193///
1194/// Parses the method signature to determine receiver type, parameter names/types,
1195/// return type, and any `#[miniextendr(...)]` attributes like `skip` and `r_name`.
1196/// Parameters with non-ident patterns are assigned synthetic names (`arg0`, `arg1`, etc.).
1197fn extract_method_info(method: &syn::TraitItemFn) -> syn::Result<MethodInfo> {
1198 let name = method.sig.ident.clone();
1199
1200 // Check for #[miniextendr(skip)] and #[miniextendr(r_name = "...")]
1201 let mut skip = false;
1202 let mut r_name: Option<String> = None;
1203 for attr in &method.attrs {
1204 if !attr.path().is_ident("miniextendr") {
1205 continue;
1206 }
1207 attr.parse_nested_meta(|meta| {
1208 if meta.path.is_ident("skip") {
1209 skip = true;
1210 } else if meta.path.is_ident("r_name") {
1211 let value: syn::LitStr = meta.value()?.parse()?;
1212 r_name = Some(value.value());
1213 } else {
1214 return Err(meta.error(
1215 "unknown #[miniextendr] option on trait method; expected `skip` or `r_name`",
1216 ));
1217 }
1218 Ok(())
1219 })?;
1220 }
1221
1222 // Check for receiver
1223 let (has_self, is_mut) = method.sig.inputs.first().map_or((false, false), |arg| {
1224 if let syn::FnArg::Receiver(r) = arg {
1225 (true, r.mutability.is_some())
1226 } else {
1227 (false, false)
1228 }
1229 });
1230
1231 // Extract parameters (skip self if present)
1232 let skip_count = if has_self { 1 } else { 0 };
1233 let mut param_types = Vec::new();
1234 let mut param_names = Vec::new();
1235 for (i, arg) in method.sig.inputs.iter().skip(skip_count).enumerate() {
1236 if let syn::FnArg::Typed(pat_type) = arg {
1237 param_types.push((*pat_type.ty).clone());
1238 if let syn::Pat::Ident(pat_ident) = pat_type.pat.as_ref() {
1239 param_names.push(pat_ident.ident.clone());
1240 } else {
1241 param_names.push(quote::format_ident!("arg{}", i));
1242 }
1243 }
1244 }
1245
1246 // Extract return type
1247 let return_type = match &method.sig.output {
1248 syn::ReturnType::Default => None,
1249 syn::ReturnType::Type(_, ty) => {
1250 // Check if it's unit type ()
1251 if matches!(ty.as_ref(), syn::Type::Tuple(t) if t.elems.is_empty()) {
1252 None
1253 } else {
1254 Some((**ty).clone())
1255 }
1256 }
1257 };
1258
1259 Ok(MethodInfo {
1260 name,
1261 has_self,
1262 is_mut,
1263 param_types,
1264 param_names,
1265 return_type,
1266 skip,
1267 r_name,
1268 })
1269}
1270
1271#[cfg(test)]
1272mod tests;
1273// endregion