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