Skip to main content

miniextendr_macros/
externalptr_derive.rs

1//! # `#[derive(ExternalPtr)]` - ExternalPtr Support
2//!
3//! This module implements the `#[derive(ExternalPtr)]` macro which generates
4//! a `TypedExternal` impl for use with `ExternalPtr<T>`.
5//!
6//! Trait ABI wrapper infrastructure is automatically generated when you use
7//! `#[miniextendr]` on `impl Trait for Type` blocks.
8//!
9//! ## Usage
10//!
11//! ### Basic (no traits)
12//!
13//! ```ignore
14//! #[derive(ExternalPtr)]
15//! struct MyData {
16//!     value: i32,
17//! }
18//! // Generates: impl TypedExternal for MyData { ... }
19//! ```
20//!
21//! ### With R Sidecar Slots and Class System
22//!
23//! The `#[r_data]` attribute marks fields for R-side storage. Use `#[externalptr(...)]`
24//! to specify a class system for appropriate R wrapper generation:
25//!
26//! | Class System | Attribute | R Accessors |
27//! |--------------|-----------|-------------|
28//! | Environment | `#[externalptr(env)]` (default) | `Type_get_field()`, `Type_set_field()` |
29//! | R6 | `#[externalptr(r6)]` | Active bindings in R6Class |
30//! | S3 | `#[externalptr(s3)]` | `$.class`, `$<-.class` methods |
31//! | S4 | `#[externalptr(s4)]` | Slot accessors |
32//! | S7 | `#[externalptr(s7)]` | Properties via `new_property()` |
33//!
34//! Three field tiers are supported:
35//!
36//! 1. **Raw SEXP** (`SEXP`) - Direct SEXP access, no conversion
37//! 2. **Zero-overhead scalars** (`i32`, `f64`, `bool`, `u8`) - Direct R memory access
38//! 3. **Conversion types** (anything else) - Uses `IntoR`/`TryFromSexp` traits
39//!
40//! ```ignore
41//! #[derive(ExternalPtr)]
42//! #[externalptr(r6)]  // R6 class - generates active bindings
43//! pub struct MyType {
44//!     pub x: i32,
45//!
46//!     #[r_data]
47//!     r: RSidecar,  // Selector - enables R accessors for this type
48//!
49//!     #[r_data]
50//!     pub raw_slot: SEXP,  // Raw SEXP, no conversion
51//!
52//!     #[r_data]
53//!     pub count: i32,  // Zero-overhead: stored as R INTEGER(1)
54//!
55//!     #[r_data]
56//!     pub score: f64,  // Zero-overhead: stored as R REAL(1)
57//!
58//!     #[r_data]
59//!     pub name: String,  // Conversion: uses IntoR/TryFromSexp
60//! }
61//! // Generates: active bindings `count`, `score`, `name` in R6Class
62//! ```
63//!
64//! ### Trait ABI wiring
65//!
66//! ```ignore
67//! #[derive(ExternalPtr)]
68//! struct MyCounter {
69//!     value: i32,
70//! }
71//!
72//! #[miniextendr]
73//! impl Counter for MyCounter { /* ... */ }
74//! ```
75//!
76//! ## Generated Types (trait impls)
77//!
78//! ### Wrapper Struct
79//!
80//! ```ignore
81//! #[repr(C)]
82//! struct __MxWrapperMyCounter {
83//!     erased: mx_erased,  // Must be first field
84//!     data: MyCounter,
85//! }
86//! ```
87//!
88//! ### Base Vtable
89//!
90//! ```ignore
91//! static __MX_BASE_VTABLE_MYCOUNTER: mx_base_vtable = mx_base_vtable {
92//!     drop: __mx_drop_mycounter,
93//!     concrete_tag: TAG_MYCOUNTER,
94//!     query: __mx_query_mycounter,
95//! };
96//! ```
97//!
98//! ### Query Function
99//!
100//! The query function maps trait tags to vtable pointers:
101//!
102//! ```ignore
103//! unsafe extern "C" fn __mx_query_mycounter(
104//!     ptr: *mut mx_erased,
105//!     trait_tag: mx_tag,
106//! ) -> *const c_void {
107//!     if trait_tag == TAG_COUNTER {
108//!         return std::ptr::from_ref(&__VTABLE_COUNTER_FOR_MYCOUNTER).cast::<c_void>();
109//!     }
110//!     std::ptr::null()
111//! }
112//! ```
113
114use proc_macro2::{Span, TokenStream};
115use syn::{DeriveInput, Field, Ident, Visibility};
116
117use crate::miniextendr_impl::ClassSystem;
118
119/// Parse `#[externalptr(...)]` attributes to extract class system.
120///
121/// Supported forms:
122/// - `#[externalptr(env)]` - Environment style (default)
123/// - `#[externalptr(r6)]` - R6 class
124/// - `#[externalptr(s3)]` - S3 class
125/// - `#[externalptr(s4)]` - S4 class
126/// - `#[externalptr(s7)]` - S7 class
127fn parse_externalptr_attrs(input: &DeriveInput) -> syn::Result<ClassSystem> {
128    let mut class_system = ClassSystem::Env; // Default
129
130    for attr in &input.attrs {
131        if attr.path().is_ident("externalptr") {
132            attr.parse_nested_meta(|meta| {
133                let ident_str = meta
134                    .path
135                    .get_ident()
136                    .map(|i| i.to_string())
137                    .unwrap_or_default();
138
139                match ident_str.as_str() {
140                    "env" => class_system = ClassSystem::Env,
141                    "r6" => class_system = ClassSystem::R6,
142                    "s3" => class_system = ClassSystem::S3,
143                    "s4" => class_system = ClassSystem::S4,
144                    "s7" => class_system = ClassSystem::S7,
145                    "vctrs" => class_system = ClassSystem::Vctrs,
146                    _ => {
147                        return Err(syn::Error::new_spanned(
148                            &meta.path,
149                            format!(
150                                "unknown class system '{}'; expected one of: env, r6, s3, s4, s7, vctrs",
151                                ident_str
152                            ),
153                        ));
154                    }
155                }
156                Ok(())
157            })?;
158        }
159    }
160
161    Ok(class_system)
162}
163
164/// Check if a field has the `#[r_data]` attribute.
165fn has_r_data_attr(field: &Field) -> bool {
166    field.attrs.iter().any(|a| a.path().is_ident("r_data"))
167}
168
169/// Parse `prop_doc = "..."` from an `#[r_data(prop_doc = "...")]` attribute.
170///
171/// Returns `None` if the field has no `#[r_data]` attribute, no `prop_doc` key,
172/// or if the attribute has no parenthesized arguments (bare `#[r_data]`).
173fn parse_r_data_prop_doc(field: &Field) -> syn::Result<Option<String>> {
174    for attr in &field.attrs {
175        if !attr.path().is_ident("r_data") {
176            continue;
177        }
178        // `#[r_data]` with no arguments — no prop_doc
179        if matches!(attr.meta, syn::Meta::Path(_)) {
180            return Ok(None);
181        }
182        let mut prop_doc = None;
183        attr.parse_nested_meta(|meta| {
184            if meta.path.is_ident("prop_doc") {
185                let value = meta.value()?;
186                let lit: syn::LitStr = value.parse()?;
187                prop_doc = Some(lit.value());
188                Ok(())
189            } else {
190                Err(meta.error(format!(
191                    "unknown key `{}`; supported: `prop_doc`",
192                    meta.path
193                        .get_ident()
194                        .map(|i| i.to_string())
195                        .unwrap_or_default()
196                )))
197            }
198        })?;
199        return Ok(prop_doc);
200    }
201    Ok(None)
202}
203
204/// Check if a field type is `RSidecar`.
205///
206/// Returns `true` if the last path segment of the field's type is `RSidecar`,
207/// which acts as the selector marker enabling R sidecar accessor generation.
208fn is_rsidecar_type(field: &Field) -> bool {
209    if let syn::Type::Path(type_path) = &field.ty {
210        type_path
211            .path
212            .segments
213            .last()
214            .map(|seg| seg.ident == "RSidecar")
215            .unwrap_or(false)
216    } else {
217        false
218    }
219}
220
221/// Check if a field is public.
222fn is_pub(field: &Field) -> bool {
223    matches!(field.vis, Visibility::Public(_))
224}
225
226/// The kind of sidecar slot, determining how getter/setter FFI functions are generated.
227///
228/// Each kind maps to a different codegen strategy for reading from and writing to
229/// the Rust struct through R's `.Call` interface:
230/// - Raw SEXP: no conversion, direct pass-through.
231/// - Zero-overhead scalars: use R's `Rf_Scalar*`/`Rf_as*` for single-element coercion.
232/// - Conversion: use the `IntoR`/`TryFromSexp` traits for arbitrary types.
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234enum SlotKind {
235    /// SEXP type -- raw SEXP access (getter returns SEXP, setter takes SEXP).
236    RawSexp,
237    /// Zero-overhead scalar: `i32` (or `i16`/`i8`) stored as `INTEGER(1)`.
238    ScalarInt,
239    /// Zero-overhead scalar: `f64` (or `f32`) stored as `REAL(1)`.
240    ScalarReal,
241    /// Zero-overhead scalar: `bool` (or `Rbool`) stored as `LOGICAL(1)`.
242    ScalarLogical,
243    /// Zero-overhead scalar: `u8` stored as `RAW(1)`.
244    ScalarRaw,
245    /// Conversion type -- uses `IntoR`/`TryFromSexp` traits for arbitrary Rust types.
246    Conversion,
247}
248
249/// Information about a single `#[r_data]`-annotated sidecar slot field.
250///
251/// Collected during struct field parsing and used to generate FFI getter/setter
252/// functions and R wrapper code for each public slot.
253struct SidecarSlot {
254    /// Rust identifier of the field (e.g., `count`, `name`).
255    name: Ident,
256    /// Rust type of the field, used in conversion-based getter/setter codegen.
257    ty: syn::Type,
258    /// Zero-based index of this slot in the protection VECSXP
259    /// (offset from `PROT_BASE_LEN`, which reserves slots for type ID and user data).
260    index: usize,
261    /// Whether the field is `pub`. Only public fields get R accessor functions.
262    is_public: bool,
263    /// Determines the codegen strategy for reading/writing this slot.
264    kind: SlotKind,
265    /// Optional documentation string for the S7 `@prop` tag.
266    /// Sourced from `#[r_data(prop_doc = "...")]`. `None` means no doc was supplied;
267    /// a default fallback string is used at emit time.
268    prop_doc: Option<String>,
269}
270
271/// Aggregated sidecar information extracted from struct field analysis.
272///
273/// Contains everything needed to generate sidecar accessor code: the
274/// selector presence, the list of typed slots, and the target class system.
275struct SidecarInfo {
276    /// Whether the struct contains an `RSidecar`-typed field marked with `#[r_data]`.
277    /// At most one selector is allowed per struct.
278    has_selector: bool,
279    /// The `#[r_data]` slot fields (excluding the RSidecar selector itself),
280    /// each carrying its index, kind, and visibility.
281    slots: Vec<SidecarSlot>,
282    /// The R class system chosen via `#[externalptr(...)]`, controlling the
283    /// style of generated R wrapper code.
284    class_system: ClassSystem,
285}
286
287/// Determine the [`SlotKind`] for a field type by inspecting its last path segment.
288///
289/// Recognizes `SEXP`, scalar numerics (`i32`, `i16`, `i8`, `f64`, `f32`),
290/// booleans (`bool`, `Rbool`), and raw bytes (`u8`). Everything else falls
291/// through to [`SlotKind::Conversion`].
292fn slot_kind_for_type(ty: &syn::Type) -> SlotKind {
293    if let syn::Type::Path(type_path) = ty
294        && let Some(seg) = type_path.path.segments.last()
295    {
296        let ident = &seg.ident;
297        // Check for raw SEXP access
298        if ident == "SEXP" {
299            return SlotKind::RawSexp;
300        }
301        // Check for zero-overhead scalar types
302        if ident == "i32" || ident == "i16" || ident == "i8" {
303            return SlotKind::ScalarInt;
304        }
305        if ident == "f64" || ident == "f32" {
306            return SlotKind::ScalarReal;
307        }
308        if ident == "bool" || ident == "Rbool" {
309            return SlotKind::ScalarLogical;
310        }
311        if ident == "u8" {
312            return SlotKind::ScalarRaw;
313        }
314    }
315    // Everything else uses conversion
316    SlotKind::Conversion
317}
318
319/// Parse struct fields for sidecar information.
320///
321/// Iterates over all fields, identifying `#[r_data]` markers. Fields with
322/// `RSidecar` type are tracked as selector markers (at most one allowed);
323/// all other `#[r_data]` fields become [`SidecarSlot`] entries with their
324/// slot kind inferred from the field type.
325///
326/// Returns `Err` if more than one `RSidecar` field is found.
327fn parse_sidecar_info(input: &DeriveInput, class_system: ClassSystem) -> syn::Result<SidecarInfo> {
328    let fields = match &input.data {
329        syn::Data::Struct(data) => &data.fields,
330        _ => {
331            return Ok(SidecarInfo {
332                has_selector: false,
333                slots: vec![],
334                class_system,
335            });
336        }
337    };
338
339    let mut selector_fields: Vec<&Field> = vec![];
340    let mut slots = vec![];
341    let mut slot_index = 0usize;
342
343    for field in fields.iter() {
344        if !has_r_data_attr(field) {
345            continue;
346        }
347
348        if is_rsidecar_type(field) {
349            // RSidecar is the selector marker, not a slot
350            selector_fields.push(field);
351        } else if let Some(ref ident) = field.ident {
352            // Any other type with #[r_data] becomes a slot
353            let kind = slot_kind_for_type(&field.ty);
354            let prop_doc = parse_r_data_prop_doc(field)?;
355            slots.push(SidecarSlot {
356                name: ident.clone(),
357                ty: field.ty.clone(),
358                index: slot_index,
359                is_public: is_pub(field),
360                kind,
361                prop_doc,
362            });
363            slot_index += 1;
364        }
365    }
366
367    // Check for multiple selectors
368    if selector_fields.len() > 1 {
369        return Err(syn::Error::new_spanned(
370            selector_fields[1],
371            "only one RSidecar field is allowed per struct",
372        ));
373    }
374
375    Ok(SidecarInfo {
376        has_selector: !selector_fields.is_empty(),
377        slots,
378        class_system,
379    })
380}
381
382/// Generate the token stream for a sidecar getter function body.
383///
384/// Reads the field value from the Rust struct (accessed via the external
385/// pointer address) and converts it to an R SEXP. The conversion strategy
386/// depends on the slot kind:
387/// - `RawSexp`: returns the SEXP field directly.
388/// - Scalar kinds: wraps in `Rf_Scalar*` for zero-overhead conversion.
389/// - `Conversion`: clones the value and calls `IntoR::into_sexp`.
390///
391/// Returns `R_NilValue` if the external pointer address is null.
392fn generate_getter_body(
393    struct_name: &syn::Ident,
394    slot: &SidecarSlot,
395    _prot_index_lit: &syn::LitInt,
396) -> TokenStream {
397    let field_name = &slot.name;
398
399    // Helper: generate the pointer extraction code for Box<dyn Any> storage.
400    // R_ExternalPtrAddr returns *mut Box<dyn Any>; we downcast to &T.
401    let extract_ref = quote::quote! {
402        use ::miniextendr_api::ffi::{R_ExternalPtrAddr, SEXP};
403        let any_raw = R_ExternalPtrAddr(x) as *mut Box<dyn ::std::any::Any>;
404        if any_raw.is_null() {
405            return SEXP::nil();
406        }
407        let any_box: &Box<dyn ::std::any::Any> = &*any_raw;
408        let data: &#struct_name = match any_box.downcast_ref::<#struct_name>() {
409            Some(v) => v,
410            None => return SEXP::nil(),
411        };
412    };
413
414    match slot.kind {
415        SlotKind::RawSexp => {
416            // Raw SEXP field - return directly (already an R value)
417            quote::quote! {
418                unsafe {
419                    #extract_ref
420                    data.#field_name
421                }
422            }
423        }
424        SlotKind::ScalarInt => {
425            quote::quote! {
426                use ::miniextendr_api::ffi::SEXP;
427                unsafe {
428                    #extract_ref
429                    SEXP::scalar_integer(data.#field_name)
430                }
431            }
432        }
433        SlotKind::ScalarReal => {
434            quote::quote! {
435                use ::miniextendr_api::ffi::SEXP;
436                unsafe {
437                    #extract_ref
438                    SEXP::scalar_real(data.#field_name)
439                }
440            }
441        }
442        SlotKind::ScalarLogical => {
443            quote::quote! {
444                use ::miniextendr_api::ffi::SEXP;
445                unsafe {
446                    #extract_ref
447                    SEXP::scalar_logical(data.#field_name)
448                }
449            }
450        }
451        SlotKind::ScalarRaw => {
452            quote::quote! {
453                use ::miniextendr_api::ffi::SEXP;
454                unsafe {
455                    #extract_ref
456                    SEXP::scalar_raw(data.#field_name)
457                }
458            }
459        }
460        SlotKind::Conversion => {
461            // Use IntoR trait for conversion (e.g., String -> character)
462            let ty = &slot.ty;
463            quote::quote! {
464                use ::miniextendr_api::into_r::IntoR;
465                unsafe {
466                    #extract_ref
467                    let val: #ty = data.#field_name.clone();
468                    <#ty as IntoR>::into_sexp(val)
469                }
470            }
471        }
472    }
473}
474
475/// Generate the token stream for a sidecar setter function body.
476///
477/// Converts the incoming R SEXP `value` and writes it to the corresponding
478/// Rust struct field. The conversion strategy depends on the slot kind:
479/// - `RawSexp`: stores the SEXP directly.
480/// - Scalar kinds: uses `Rf_as*` or coercion for single-element extraction.
481/// - `Conversion`: uses `TryFromSexp::try_from_sexp`, silently ignoring errors.
482///
483/// Always returns the external pointer `x` (for R's invisible return convention).
484/// No-op if the external pointer address is null.
485fn generate_setter_body(
486    struct_name: &syn::Ident,
487    slot: &SidecarSlot,
488    _prot_index_lit: &syn::LitInt,
489) -> TokenStream {
490    let field_name = &slot.name;
491
492    // Helper: extract mutable reference via Box<dyn Any> downcast.
493    let extract_mut = quote::quote! {
494        use ::miniextendr_api::ffi::R_ExternalPtrAddr;
495        let any_raw = R_ExternalPtrAddr(x) as *mut Box<dyn ::std::any::Any>;
496        if any_raw.is_null() {
497            return x;
498        }
499        let any_box: &mut Box<dyn ::std::any::Any> = &mut *any_raw;
500        let Some(data) = any_box.downcast_mut::<#struct_name>() else {
501            return x;
502        };
503    };
504
505    match slot.kind {
506        SlotKind::RawSexp => {
507            quote::quote! {
508                unsafe {
509                    #extract_mut
510                    data.#field_name = value;
511                    x
512                }
513            }
514        }
515        SlotKind::ScalarInt => {
516            quote::quote! {
517                use ::miniextendr_api::ffi::SexpExt;
518                unsafe {
519                    #extract_mut
520                    data.#field_name = value.as_integer().unwrap_or(::miniextendr_api::altrep_traits::NA_INTEGER);
521                    x
522                }
523            }
524        }
525        SlotKind::ScalarReal => {
526            quote::quote! {
527                use ::miniextendr_api::ffi::SexpExt;
528                unsafe {
529                    #extract_mut
530                    data.#field_name = value.as_real().unwrap_or(::miniextendr_api::altrep_traits::NA_REAL);
531                    x
532                }
533            }
534        }
535        SlotKind::ScalarLogical => {
536            quote::quote! {
537                use ::miniextendr_api::ffi::SexpExt;
538                unsafe {
539                    #extract_mut
540                    data.#field_name = value.as_logical().unwrap_or(false);
541                    x
542                }
543            }
544        }
545        SlotKind::ScalarRaw => {
546            quote::quote! {
547                use ::miniextendr_api::ffi::{SexpExt, SEXPTYPE};
548                unsafe {
549                    #extract_mut
550                    let raw_vec = value.coerce(SEXPTYPE::RAWSXP);
551                    data.#field_name = raw_vec.raw_elt(0);
552                    x
553                }
554            }
555        }
556        SlotKind::Conversion => {
557            let ty = &slot.ty;
558            quote::quote! {
559                use ::miniextendr_api::TryFromSexp;
560                unsafe {
561                    #extract_mut
562                    if let Ok(val) = <#ty as TryFromSexp>::try_from_sexp(value) {
563                        data.#field_name = val;
564                    }
565                    x
566                }
567            }
568        }
569    }
570}
571
572/// Generate R wrapper code (roxygen-annotated R functions) for a single sidecar slot.
573///
574/// Produces getter and setter R functions that call the corresponding C entry
575/// points via `.Call`. The generated R code includes roxygen tags (`@rdname`,
576/// `@param`, `@return`, `@export`) for documentation.
577///
578/// All class systems currently generate the same standalone function pattern
579/// (`Type_get_field` / `Type_set_field`). Class-specific integration (e.g.,
580/// R6 active bindings, S7 properties) is handled separately by
581/// [`generate_class_integration_r_code`].
582fn generate_r_wrapper_for_slot(
583    class_system: ClassSystem,
584    type_name: &str,
585    field_name: &str,
586    getter_c_name: &str,
587    setter_c_name: &str,
588) -> String {
589    match class_system {
590        ClassSystem::Env => {
591            // Standalone functions: Type_get_field(), Type_set_field()
592            let r_getter_name = format!("{}_get_{}", type_name, field_name);
593            let r_setter_name = format!("{}_set_{}", type_name, field_name);
594            format!(
595                r#"
596#' Get `{field}` field from {type}
597#' @rdname {type}
598#' @param x The {type} external pointer
599#' @return The value of the `{field}` field
600#' @export
601{r_getter} <- function(x) .Call({getter_c}, x)
602
603#' Set `{field}` field on {type}
604#' @rdname {type}
605#' @param x The {type} external pointer
606#' @param value The new value to set
607#' @return The {type} pointer (invisibly)
608#' @export
609{r_setter} <- function(x, value) {{
610  .Call({setter_c}, x, value)
611  invisible(x)
612}}
613"#,
614                type = type_name,
615                field = field_name,
616                r_getter = r_getter_name,
617                r_setter = r_setter_name,
618                getter_c = getter_c_name,
619                setter_c = setter_c_name,
620            )
621        }
622        ClassSystem::R6 => {
623            // R6: Generate env-style accessors that can be called from R6 active bindings.
624            // Note: The getter takes x (the ExternalPtr), not private$.ptr.
625            let r_getter_name = format!("{}_get_{}", type_name, field_name);
626            let r_setter_name = format!("{}_set_{}", type_name, field_name);
627            format!(
628                r#"
629#' Get `{field}` field from {type} (for R6)
630#' @rdname {type}
631#' @param x The {type} external pointer
632#' @return The value of the `{field}` field
633#' @export
634{r_getter} <- function(x) .Call({getter_c}, x)
635
636#' Set `{field}` field on {type} (for R6)
637#' @rdname {type}
638#' @param x The {type} external pointer
639#' @param value The new value to set
640#' @return The {type} pointer (invisibly)
641#' @export
642{r_setter} <- function(x, value) {{
643  .Call({setter_c}, x, value)
644  invisible(x)
645}}
646"#,
647                type = type_name,
648                field = field_name,
649                r_getter = r_getter_name,
650                r_setter = r_setter_name,
651                getter_c = getter_c_name,
652                setter_c = setter_c_name,
653            )
654        }
655        ClassSystem::S3 => {
656            // S3: Generate env-style accessors. Users can combine these into
657            // `$.class` and `$<-.class` methods if desired.
658            // Generating separate `$.class` methods per field would overwrite each other.
659            let r_getter_name = format!("{}_get_{}", type_name, field_name);
660            let r_setter_name = format!("{}_set_{}", type_name, field_name);
661            format!(
662                r#"
663#' Get `{field}` field from {type} (for S3)
664#' @rdname {type}
665#' @param x The {type} external pointer
666#' @return The value of the `{field}` field
667#' @export
668{r_getter} <- function(x) .Call({getter_c}, x)
669
670#' Set `{field}` field on {type} (for S3)
671#' @rdname {type}
672#' @param x The {type} external pointer
673#' @param value The new value to set
674#' @return The {type} pointer (invisibly)
675#' @export
676{r_setter} <- function(x, value) {{
677  .Call({setter_c}, x, value)
678  invisible(x)
679}}
680"#,
681                type = type_name,
682                field = field_name,
683                r_getter = r_getter_name,
684                r_setter = r_setter_name,
685                getter_c = getter_c_name,
686                setter_c = setter_c_name,
687            )
688        }
689        ClassSystem::S4 => {
690            // S4: Generate env-style accessors. Users can wrap these in setMethod()
691            // with appropriate generics if desired.
692            let r_getter_name = format!("{}_get_{}", type_name, field_name);
693            let r_setter_name = format!("{}_set_{}", type_name, field_name);
694            format!(
695                r#"
696#' Get `{field}` field from {type} (for S4)
697#' @rdname {type}
698#' @param x The {type} external pointer
699#' @return The value of the `{field}` field
700#' @export
701{r_getter} <- function(x) .Call({getter_c}, x)
702
703#' Set `{field}` field on {type} (for S4)
704#' @rdname {type}
705#' @param x The {type} external pointer
706#' @param value The new value to set
707#' @return The {type} pointer (invisibly)
708#' @export
709{r_setter} <- function(x, value) {{
710  .Call({setter_c}, x, value)
711  invisible(x)
712}}
713"#,
714                type = type_name,
715                field = field_name,
716                r_getter = r_getter_name,
717                r_setter = r_setter_name,
718                getter_c = getter_c_name,
719                setter_c = setter_c_name,
720            )
721        }
722        ClassSystem::S7 => {
723            // S7: Generate env-style accessors that can be used with S7 properties.
724            // These are standalone functions that the user can wrap in S7::new_property().
725            let r_getter_name = format!("{}_get_{}", type_name, field_name);
726            let r_setter_name = format!("{}_set_{}", type_name, field_name);
727            format!(
728                r#"
729#' Get `{field}` field from {type} (for S7)
730#' @rdname {type}
731#' @param x The {type} external pointer
732#' @return The value of the `{field}` field
733#' @export
734{r_getter} <- function(x) .Call({getter_c}, x)
735
736#' Set `{field}` field on {type} (for S7)
737#' @rdname {type}
738#' @param x The {type} external pointer
739#' @param value The new value to set
740#' @return The {type} pointer (invisibly)
741#' @export
742{r_setter} <- function(x, value) {{
743  .Call({setter_c}, x, value)
744  invisible(x)
745}}
746"#,
747                type = type_name,
748                field = field_name,
749                r_getter = r_getter_name,
750                r_setter = r_setter_name,
751                getter_c = getter_c_name,
752                setter_c = setter_c_name,
753            )
754        }
755        ClassSystem::Vctrs => {
756            // Vctrs: Generate env-style accessors. Users can combine these into
757            // `$.class` and `$<-.class` methods if desired.
758            // Generating separate `$.class` methods per field would overwrite each other.
759            let r_getter_name = format!("{}_get_{}", type_name, field_name);
760            let r_setter_name = format!("{}_set_{}", type_name, field_name);
761            format!(
762                r#"
763#' Get `{field}` field from {type} (for vctrs)
764#' @rdname {type}
765#' @param x The {type} external pointer
766#' @return The value of the `{field}` field
767#' @export
768{r_getter} <- function(x) .Call({getter_c}, x)
769
770#' Set `{field}` field on {type} (for vctrs)
771#' @rdname {type}
772#' @param x The {type} external pointer
773#' @param value The new value to set
774#' @return The {type} pointer (invisibly)
775#' @export
776{r_setter} <- function(x, value) {{
777  .Call({setter_c}, x, value)
778  invisible(x)
779}}
780"#,
781                type = type_name,
782                field = field_name,
783                r_getter = r_getter_name,
784                r_setter = r_setter_name,
785                getter_c = getter_c_name,
786                setter_c = setter_c_name,
787            )
788        }
789    }
790}
791
792/// Generate class-integrated R code for sidecar fields.
793///
794/// For R6: generates `Type$set("active", "field", ...)` calls that add active bindings
795/// referencing the sidecar getter/setter .Call entrypoints.
796///
797/// For S7: generates `.rdata_properties_Type <- list(...)` with S7::new_property()
798/// definitions that can be spliced into the S7 class's properties list.
799///
800/// Other class systems return empty strings (their standalone accessors suffice).
801fn generate_class_integration_r_code(
802    class_system: ClassSystem,
803    type_name: &str,
804    pub_slots: &[&SidecarSlot],
805) -> String {
806    if pub_slots.is_empty() {
807        return String::new();
808    }
809
810    match class_system {
811        ClassSystem::R6 => {
812            // Generate $set("active", ...) calls for each sidecar field.
813            // These are appended after the R6Class definition and add active bindings
814            // that delegate to the sidecar .Call accessors.
815            let mut code = String::new();
816            code.push_str(&format!(
817                "\n# Auto-generated active bindings for {type} sidecar fields.\n",
818                type = type_name,
819            ));
820            code.push_str(
821                "# These are applied when `r_data_accessors` is set on the impl block.\n",
822            );
823            code.push_str(&format!(
824                ".rdata_active_bindings_{type} <- function(cls) {{\n\
825                 \x20 # R CMD check: self/private are R6 runtime bindings (set by cls$set)\n\
826                 \x20 self <- private <- NULL\n",
827                type = type_name,
828            ));
829            for slot in pub_slots {
830                let field = slot.name.to_string();
831                let getter_c = format!("C__mx_rdata_get_{}_{}", type_name, field);
832                let setter_c = format!("C__mx_rdata_set_{}_{}", type_name, field);
833                code.push_str(&format!(
834                    "  cls$set(\"active\", \"{field}\", function(value) {{\n\
835                     \x20   if (missing(value)) .Call({getter_c}, private$.ptr)\n\
836                     \x20   else {{ .Call({setter_c}, private$.ptr, value); invisible(self) }}\n\
837                     \x20 }}, overwrite = TRUE)\n",
838                    field = field,
839                    getter_c = getter_c,
840                    setter_c = setter_c,
841                ));
842            }
843            code.push_str("}\n");
844            code
845        }
846        ClassSystem::S7 => {
847            // Generate a helper list of S7::new_property() definitions.
848            // The S7 wrapper generator references this when `r_data_accessors` is set.
849            let mut code = String::new();
850            code.push_str(&format!(
851                "\n# Auto-generated S7 property definitions for {type} sidecar fields.\n",
852                type = type_name,
853            ));
854            code.push_str(&format!(
855                ".rdata_properties_{type} <- list(\n",
856                type = type_name,
857            ));
858            for (i, slot) in pub_slots.iter().enumerate() {
859                let field = slot.name.to_string();
860                let getter_c = format!("C__mx_rdata_get_{}_{}", type_name, field);
861                let setter_c = format!("C__mx_rdata_set_{}_{}", type_name, field);
862                let comma = if i < pub_slots.len() - 1 { "," } else { "" };
863                code.push_str(&format!(
864                    "    {field} = S7::new_property(\n\
865                     \x20       getter = function(self) .Call({getter_c}, self@.ptr),\n\
866                     \x20       setter = function(self, value) {{ .Call({setter_c}, self@.ptr, value); self }}\n\
867                     \x20   ){comma}\n",
868                    field = field,
869                    getter_c = getter_c,
870                    setter_c = setter_c,
871                    comma = comma,
872                ));
873            }
874            code.push_str(")\n");
875            code
876        }
877        // Other class systems don't need class-integrated code
878        _ => String::new(),
879    }
880}
881
882/// Generate sidecar accessor constants and `extern "C-unwind"` functions.
883///
884/// For each public `#[r_data]` field, generates:
885/// - A getter FFI function (`C__mx_rdata_get_Type_field`)
886/// - A setter FFI function (`C__mx_rdata_set_Type_field`)
887/// - `R_CallMethodDef` entries for routine registration
888/// - R wrapper function code (roxygen-documented)
889/// - Class-integration code for R6 / S7 (active bindings / properties)
890///
891/// The generated constants are:
892/// - `RDATA_CALL_DEFS_{TYPE}`: slice of `R_CallMethodDef` for registration
893/// - `R_WRAPPERS_RDATA_{TYPE}`: string literal of R wrapper code
894///
895/// Returns `Err` if the struct has generic type parameters (`.Call` entrypoints
896/// cannot be generic).
897fn generate_sidecar_accessors(input: &DeriveInput, info: &SidecarInfo) -> syn::Result<TokenStream> {
898    // Reject generic structs — .Call entrypoints cannot be generic
899    if !input.generics.params.is_empty() {
900        return Err(syn::Error::new_spanned(
901            &input.generics,
902            "ExternalPtr does not support generic structs; \
903             .Call entrypoints cannot be generic",
904        ));
905    }
906
907    // If no selector or no public slots, nothing to register
908    let pub_slots: Vec<_> = info.slots.iter().filter(|s| s.is_public).collect();
909    if !info.has_selector || pub_slots.is_empty() {
910        return Ok(quote::quote! {});
911    }
912
913    let name = &input.ident;
914    let name_str = name.to_string();
915    let name_upper = name_str.to_uppercase();
916
917    // Base prot slot indices (type ID at 0, user at 1)
918    const PROT_BASE_LEN: usize = 2;
919
920    // Generate getter/setter functions and R wrappers for each pub slot
921    let mut c_functions = vec![];
922    let mut r_wrappers = String::new();
923
924    // Add documentation header that establishes the @name topic for sidecar accessors.
925    // This ensures @rdname references have a valid target even if the main class
926    // definition uses @noRd.
927    if !pub_slots.is_empty() {
928        r_wrappers.push_str(&format!(
929            r#"
930#' @title {type} Sidecar Accessors
931#' @name {type}
932#' @description Getter and setter functions for `#[r_data]` fields on `{type}`.
933#' @source Generated by miniextendr from `#[derive(ExternalPtr)]` on `{type}`
934NULL
935
936"#,
937            type = name_str,
938        ));
939    }
940
941    for slot in &pub_slots {
942        let field_name = &slot.name;
943        let field_name_str = field_name.to_string();
944        let prot_index = PROT_BASE_LEN + slot.index;
945
946        // C function names
947        let getter_c_name = format!("C__mx_rdata_get_{}_{}", name_str, field_name_str);
948        let setter_c_name = format!("C__mx_rdata_set_{}_{}", name_str, field_name_str);
949        let getter_fn_name = Ident::new(&getter_c_name, Span::call_site());
950        let setter_fn_name = Ident::new(&setter_c_name, Span::call_site());
951        let source_location_doc = crate::source_location_doc(field_name.span());
952        let getter_doc = format!(
953            "Generated sidecar getter for `{}` field on Rust type `{}`.",
954            field_name_str, name_str
955        );
956        let setter_doc = format!(
957            "Generated sidecar setter for `{}` field on Rust type `{}`.",
958            field_name_str, name_str
959        );
960        let getter_doc_lit = syn::LitStr::new(&getter_doc, field_name.span());
961        let setter_doc_lit = syn::LitStr::new(&setter_doc, field_name.span());
962
963        let prot_index_lit = syn::LitInt::new(&prot_index.to_string(), Span::call_site());
964
965        // Generate getter/setter bodies based on slot kind
966        let getter_body = generate_getter_body(name, slot, &prot_index_lit);
967        let setter_body = generate_setter_body(name, slot, &prot_index_lit);
968
969        // Generate C getter function
970        c_functions.push(quote::quote! {
971            #[doc = #getter_doc_lit]
972            #[doc = #source_location_doc]
973            #[doc = concat!("Generated from source file `", file!(), "`.")]
974            #[doc(hidden)]
975            #[unsafe(no_mangle)]
976            pub unsafe extern "C-unwind" fn #getter_fn_name(
977                x: ::miniextendr_api::ffi::SEXP
978            ) -> ::miniextendr_api::ffi::SEXP {
979                #getter_body
980            }
981        });
982
983        // Generate C setter function
984        c_functions.push(quote::quote! {
985            #[doc = #setter_doc_lit]
986            #[doc = #source_location_doc]
987            #[doc = concat!("Generated from source file `", file!(), "`.")]
988            #[doc(hidden)]
989            #[unsafe(no_mangle)]
990            pub unsafe extern "C-unwind" fn #setter_fn_name(
991                x: ::miniextendr_api::ffi::SEXP,
992                value: ::miniextendr_api::ffi::SEXP,
993            ) -> ::miniextendr_api::ffi::SEXP {
994                #setter_body
995            }
996        });
997
998        // Generate R_CallMethodDef entries via distributed slice
999        let getter_c_name_cstr = format!("{}\0", getter_c_name);
1000        let setter_c_name_cstr = format!("{}\0", setter_c_name);
1001        let getter_cstr_lit =
1002            syn::LitByteStr::new(getter_c_name_cstr.as_bytes(), Span::call_site());
1003        let setter_cstr_lit =
1004            syn::LitByteStr::new(setter_c_name_cstr.as_bytes(), Span::call_site());
1005        let getter_def_ident = Ident::new(
1006            &format!(
1007                "__MX_CALL_DEF_RDATA_GET_{}_{}",
1008                name_upper,
1009                field_name_str.to_uppercase()
1010            ),
1011            Span::call_site(),
1012        );
1013        let setter_def_ident = Ident::new(
1014            &format!(
1015                "__MX_CALL_DEF_RDATA_SET_{}_{}",
1016                name_upper,
1017                field_name_str.to_uppercase()
1018            ),
1019            Span::call_site(),
1020        );
1021
1022        c_functions.push(quote::quote! {
1023            #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
1024            #[doc(hidden)]
1025            static #getter_def_ident: ::miniextendr_api::ffi::R_CallMethodDef =
1026                ::miniextendr_api::ffi::R_CallMethodDef {
1027                    name: #getter_cstr_lit.as_ptr().cast(),
1028                    fun: Some(unsafe { ::std::mem::transmute(#getter_fn_name as unsafe extern "C-unwind" fn(_) -> _) }),
1029                    numArgs: 1,
1030                };
1031        });
1032        c_functions.push(quote::quote! {
1033            #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
1034            #[doc(hidden)]
1035            static #setter_def_ident: ::miniextendr_api::ffi::R_CallMethodDef =
1036                ::miniextendr_api::ffi::R_CallMethodDef {
1037                    name: #setter_cstr_lit.as_ptr().cast(),
1038                    fun: Some(unsafe { ::std::mem::transmute(#setter_fn_name as unsafe extern "C-unwind" fn(_, _) -> _) }),
1039                    numArgs: 2,
1040                };
1041        });
1042
1043        // Generate R wrapper code based on class system
1044        let field_start = field_name.span().start();
1045        r_wrappers.push_str(&format!(
1046            "# Generated from Rust source line {}:{}\n# Wraps sidecar field `{}` on Rust type `{}` via `{}` and `{}`.\n",
1047            field_start.line,
1048            field_start.column + 1,
1049            field_name_str,
1050            name_str,
1051            getter_c_name,
1052            setter_c_name,
1053        ));
1054        r_wrappers.push_str(&generate_r_wrapper_for_slot(
1055            info.class_system,
1056            &name_str,
1057            &field_name_str,
1058            &getter_c_name,
1059            &setter_c_name,
1060        ));
1061    }
1062
1063    // Generate class-integrated R code for R6 and S7.
1064    // This code is appended after the standalone accessors so that
1065    // `r_data_accessors` in the impl block can auto-integrate sidecar fields.
1066    r_wrappers.push_str(&generate_class_integration_r_code(
1067        info.class_system,
1068        &name_str,
1069        &pub_slots,
1070    ));
1071
1072    let const_name_wrappers = Ident::new(
1073        &format!("R_WRAPPERS_RDATA_{}", name_upper),
1074        Span::call_site(),
1075    );
1076    let source_location_doc = crate::source_location_doc(name.span());
1077
1078    // For S7 class systems, emit MX_S7_SIDECAR_PROPS entries so the S7 codegen
1079    // can substitute @prop lines for sidecar properties at write time.
1080    let sidecar_prop_entries = if info.class_system == ClassSystem::S7 {
1081        let entries: Vec<_> = pub_slots
1082            .iter()
1083            .map(|slot| {
1084                let field_str = slot.name.to_string();
1085                let doc_str = slot
1086                    .prop_doc
1087                    .as_deref()
1088                    .unwrap_or("(undocumented sidecar property)");
1089                let entry_ident = Ident::new(
1090                    &format!(
1091                        "__MX_S7_SIDECAR_PROP_{}_{}",
1092                        name_upper,
1093                        field_str.to_uppercase()
1094                    ),
1095                    Span::call_site(),
1096                );
1097                quote::quote! {
1098                    #[doc(hidden)]
1099                    #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_S7_SIDECAR_PROPS), linkme(crate = ::miniextendr_api::linkme))]
1100                    static #entry_ident: ::miniextendr_api::registry::SidecarPropEntry =
1101                        ::miniextendr_api::registry::SidecarPropEntry {
1102                            rust_type: #name_str,
1103                            field_name: #field_str,
1104                            prop_doc: #doc_str,
1105                        };
1106                }
1107            })
1108            .collect();
1109        quote::quote! { #(#entries)* }
1110    } else {
1111        quote::quote! {}
1112    };
1113
1114    Ok(quote::quote! {
1115        #(#c_functions)*
1116
1117        /// Sidecar accessor R wrapper code via distributed slice.
1118        #[doc = #source_location_doc]
1119        #[doc = concat!("Generated from source file `", file!(), "`.")]
1120        #[doc(hidden)]
1121        #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_R_WRAPPERS), linkme(crate = ::miniextendr_api::linkme))]
1122        static #const_name_wrappers: ::miniextendr_api::registry::RWrapperEntry =
1123            ::miniextendr_api::registry::RWrapperEntry {
1124                priority: ::miniextendr_api::registry::RWrapperPriority::Sidecar,
1125                source_file: file!(),
1126                content: #r_wrappers,
1127            };
1128
1129        #sidecar_prop_entries
1130    })
1131}
1132
1133/// Generate the `TypedExternal` trait implementation for the derive target.
1134///
1135/// Produces three associated constants:
1136/// - `TYPE_NAME`: the struct name as a `&'static str`
1137/// - `TYPE_NAME_CSTR`: null-terminated byte string of the struct name
1138/// - `TYPE_ID_CSTR`: globally unique ID in the format
1139///   `"<crate_name>@<crate_version>::<module_path>::<type_name>\0"`,
1140///   using `CARGO_PKG_NAME`, `CARGO_PKG_VERSION`, and `module_path!()`.
1141///
1142/// Supports generic structs (generics are forwarded to the impl).
1143fn generate_typed_external(input: &DeriveInput) -> TokenStream {
1144    let name = &input.ident;
1145    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1146
1147    let name_str = name.to_string();
1148    let name_lit = syn::LitStr::new(&name_str, name.span());
1149    let name_cstr = syn::LitByteStr::new(format!("{}\0", name_str).as_bytes(), name.span());
1150
1151    // TYPE_ID_CSTR format: "<crate_name>@<crate_version>::<module_path>::<type_name>\0"
1152    //
1153    // Uses env!("CARGO_PKG_NAME") and env!("CARGO_PKG_VERSION") for the crate identifier,
1154    // ensuring two packages with the same type name from the same crate+version are compatible,
1155    // while different crate versions are considered distinct types.
1156    //
1157    // The module_path!() may include "crate::" prefix when compiled within the crate,
1158    // but combined with the explicit crate@version prefix, this is unambiguous.
1159    quote::quote! {
1160        impl #impl_generics ::miniextendr_api::externalptr::TypedExternal for #name #ty_generics #where_clause {
1161            const TYPE_NAME: &'static str = #name_lit;
1162            const TYPE_NAME_CSTR: &'static [u8] = #name_cstr;
1163            const TYPE_ID_CSTR: &'static [u8] =
1164                concat!(
1165                    env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION"),
1166                    "::", module_path!(), "::", #name_lit, "\0"
1167                ).as_bytes();
1168        }
1169    }
1170}
1171
1172/// Generate the `IntoExternalPtr` marker trait impl.
1173///
1174/// This marker trait enables the blanket `impl<T: IntoExternalPtr> IntoR for T`
1175/// in miniextendr-api, allowing the type to be returned directly from functions.
1176fn generate_into_external_ptr(input: &DeriveInput) -> TokenStream {
1177    let name = &input.ident;
1178    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1179
1180    quote::quote! {
1181        impl #impl_generics ::miniextendr_api::externalptr::IntoExternalPtr for #name #ty_generics #where_clause {}
1182    }
1183}
1184
1185/// Main entry point for `#[derive(ExternalPtr)]`.
1186///
1187/// Orchestrates the full derive expansion:
1188/// 1. Parses `#[externalptr(...)]` attributes for class system selection.
1189/// 2. Analyzes struct fields for `#[r_data]` sidecar slots.
1190/// 3. Generates `TypedExternal` impl (type identity for `ExternalPtr<T>`).
1191/// 4. Generates `IntoExternalPtr` marker impl (enables `IntoR` blanket impl).
1192/// 5. Generates sidecar accessor FFI functions, registration constants, and R wrappers.
1193///
1194/// Returns the combined token stream of all generated items.
1195pub fn derive_external_ptr(input: DeriveInput) -> syn::Result<TokenStream> {
1196    // Parse class system from #[externalptr(...)] attribute
1197    let class_system = parse_externalptr_attrs(&input)?;
1198
1199    // Parse sidecar information from struct fields
1200    let sidecar_info = parse_sidecar_info(&input, class_system)?;
1201
1202    let typed_external = generate_typed_external(&input);
1203    let into_external_ptr = generate_into_external_ptr(&input);
1204    let sidecar_accessors = generate_sidecar_accessors(&input, &sidecar_info)?;
1205    let erased_wrapper = generate_erased_wrapper(&input);
1206
1207    Ok(quote::quote! {
1208        #typed_external
1209        #into_external_ptr
1210        #sidecar_accessors
1211        #erased_wrapper
1212    })
1213}
1214
1215/// Generate the type-erased wrapper infrastructure for trait ABI dispatch.
1216///
1217/// For `#[derive(ExternalPtr)] struct MyCounter { ... }` generates:
1218/// - `__MxWrapperMyCounter` - repr(C) wrapper with mx_erased header + data
1219/// - `__MX_TAG_MYCOUNTER` - concrete type tag (FNV-1a hash of module_path + type)
1220/// - `__mx_drop_mycounter` - destructor for R GC
1221/// - `__MX_BASE_VTABLE_MYCOUNTER` - base vtable with universal_query
1222/// - `__mx_wrap_mycounter` - constructor returning `*mut mx_erased`
1223fn generate_erased_wrapper(input: &DeriveInput) -> TokenStream {
1224    let type_ident = &input.ident;
1225
1226    // Only for non-generic types (generics can't participate in trait dispatch)
1227    if !input.generics.params.is_empty() {
1228        return quote::quote! {};
1229    }
1230
1231    let type_upper = type_ident.to_string().to_uppercase();
1232    let type_lower = type_ident.to_string().to_lowercase();
1233
1234    let wrapper_name = quote::format_ident!("__MxWrapper{}", type_ident);
1235    let base_vtable_name = quote::format_ident!("__MX_BASE_VTABLE_{}", type_upper);
1236    let concrete_tag_name = quote::format_ident!("__MX_TAG_{}", type_upper);
1237    let drop_fn_name = quote::format_ident!("__mx_drop_{}", type_lower);
1238    let wrap_fn_name = quote::format_ident!("__mx_wrap_{}", type_lower);
1239    let source_loc_doc = crate::source_location_doc(type_ident.span());
1240    let tag_path = format!("::{}", type_ident);
1241
1242    quote::quote! {
1243        #[doc = concat!(
1244            "Type-erased wrapper for `",
1245            stringify!(#type_ident),
1246            "` with trait dispatch support."
1247        )]
1248        #[doc = "Generated by `#[derive(ExternalPtr)]`."]
1249        #[doc = #source_loc_doc]
1250        #[doc = concat!("Generated from source file `", file!(), "`.")]
1251        #[repr(C)]
1252        #[doc(hidden)]
1253        struct #wrapper_name {
1254            pub erased: ::miniextendr_api::abi::mx_erased,
1255            pub data: #type_ident,
1256        }
1257
1258        #[doc(hidden)]
1259        const #concrete_tag_name: ::miniextendr_api::abi::mx_tag =
1260            ::miniextendr_api::abi::mx_tag_from_path(concat!(module_path!(), #tag_path));
1261
1262        #[doc(hidden)]
1263        unsafe extern "C" fn #drop_fn_name(ptr: *mut ::miniextendr_api::abi::mx_erased) {
1264            if ptr.is_null() {
1265                return;
1266            }
1267            let wrapper = ptr.cast::<#wrapper_name>();
1268            // A panicking Drop impl must not unwind across the C-ABI boundary.
1269            // `drop_catching_panic` catches any panic and aborts instead.
1270            ::miniextendr_api::externalptr::drop_catching_panic(|| {
1271                unsafe { drop(Box::from_raw(wrapper)); }
1272            });
1273        }
1274
1275        #[doc(hidden)]
1276        static #base_vtable_name: ::miniextendr_api::abi::mx_base_vtable =
1277            ::miniextendr_api::abi::mx_base_vtable {
1278                drop: #drop_fn_name,
1279                concrete_tag: #concrete_tag_name,
1280                query: ::miniextendr_api::registry::universal_query,
1281                data_offset: ::std::mem::offset_of!(#wrapper_name, data),
1282            };
1283
1284        #[doc(hidden)]
1285        fn #wrap_fn_name(data: #type_ident) -> *mut ::miniextendr_api::abi::mx_erased {
1286            let wrapper = Box::new(#wrapper_name {
1287                erased: ::miniextendr_api::abi::mx_erased {
1288                    base: &#base_vtable_name,
1289                },
1290                data,
1291            });
1292            Box::into_raw(wrapper).cast::<::miniextendr_api::abi::mx_erased>()
1293        }
1294    }
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299    use super::{ClassSystem, generate_r_wrapper_for_slot};
1300
1301    /// Sidecar accessor C functions take only `x` (getter) or `x, value` (setter) —
1302    /// no `__miniextendr_call` parameter. The R wrappers must NOT pass `.call = match.call()`
1303    /// because that would be counted as an extra positional argument by `.Call()`, causing
1304    /// "Incorrect number of arguments" errors at runtime. Cover every class system variant.
1305    #[test]
1306    fn sidecar_accessors_do_not_pass_match_call() {
1307        let getter_c = "C__mx_rdata_get_T_f";
1308        let setter_c = "C__mx_rdata_set_T_f";
1309
1310        for cs in [
1311            ClassSystem::Env,
1312            ClassSystem::R6,
1313            ClassSystem::S3,
1314            ClassSystem::S4,
1315            ClassSystem::S7,
1316            ClassSystem::Vctrs,
1317        ] {
1318            let out = generate_r_wrapper_for_slot(cs, "T", "f", getter_c, setter_c);
1319            // Correct form: no .call argument — the C function only accepts x (getter) or x, value (setter).
1320            assert!(
1321                out.contains(&format!(".Call({getter_c}, x)")),
1322                "{cs:?} getter should call without .call:\n{out}"
1323            );
1324            assert!(
1325                out.contains(&format!(".Call({setter_c}, x, value)")),
1326                "{cs:?} setter should call without .call:\n{out}"
1327            );
1328            // Must NOT include the erroneous .call = match.call() form.
1329            assert!(
1330                !out.contains(".call = match.call()"),
1331                "{cs:?} sidecar wrapper must not pass .call = match.call():\n{out}"
1332            );
1333        }
1334    }
1335}