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_MYPKG_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 // Must match the sidecar accessor's actual C symbol exactly (#1273
805 // crate-prefixing) — routed through the shared naming.rs helpers.
806 let getter_c = crate::naming::sidecar_getter_c_name(type_name, &field);
807 let setter_c = crate::naming::sidecar_setter_c_name(type_name, &field);
808 code.push_str(&format!(
809 " cls$set(\"active\", \"{field}\", function(value) {{\n\
810 \x20 if (missing(value)) {{\n\
811 \x20 .val <- .Call({getter_c}, private$.ptr)\n\
812 {getter_guard}\n\
813 \x20 .val\n\
814 \x20 }} else {{\n\
815 \x20 .val <- .Call({setter_c}, private$.ptr, value)\n\
816 {setter_guard}\n\
817 \x20 invisible(self)\n\
818 \x20 }}\n\
819 \x20 }}, overwrite = TRUE)\n",
820 field = field,
821 getter_c = getter_c,
822 setter_c = setter_c,
823 getter_guard = guard(" "),
824 setter_guard = guard(" "),
825 ));
826 }
827 code.push_str("}\n");
828 code
829 }
830 ClassSystem::S7 => {
831 // Generate a helper list of S7::new_property() definitions.
832 // The S7 wrapper generator references this when `r_data_accessors` is set.
833 let mut code = String::new();
834 code.push_str(&format!(
835 "\n# Auto-generated S7 property definitions for {type} sidecar fields.\n",
836 type = type_name,
837 ));
838 code.push_str(&format!(
839 ".rdata_properties_{type} <- list(\n",
840 type = type_name,
841 ));
842 for (i, slot) in pub_slots.iter().enumerate() {
843 let field = slot.name.to_string();
844 // Must match the sidecar accessor's actual C symbol exactly (#1273
845 // crate-prefixing) — routed through the shared naming.rs helpers.
846 let getter_c = crate::naming::sidecar_getter_c_name(type_name, &field);
847 let setter_c = crate::naming::sidecar_setter_c_name(type_name, &field);
848 let comma = if i < pub_slots.len() - 1 { "," } else { "" };
849 code.push_str(&format!(
850 " {field} = S7::new_property(\n\
851 \x20 getter = function(self) {{\n\
852 \x20 .val <- .Call({getter_c}, self@.ptr)\n\
853 {getter_guard}\n\
854 \x20 .val\n\
855 \x20 }},\n\
856 \x20 setter = function(self, value) {{\n\
857 \x20 .val <- .Call({setter_c}, self@.ptr, value)\n\
858 {setter_guard}\n\
859 \x20 self\n\
860 \x20 }}\n\
861 \x20 ){comma}\n",
862 field = field,
863 getter_c = getter_c,
864 setter_c = setter_c,
865 getter_guard = guard(" "),
866 setter_guard = guard(" "),
867 comma = comma,
868 ));
869 }
870 code.push_str(")\n");
871 code
872 }
873 // Other class systems don't need class-integrated code
874 _ => String::new(),
875 }
876}
877
878/// Generate sidecar accessor constants and `extern "C-unwind"` functions.
879///
880/// For each public `#[r_data]` field, generates:
881/// - A getter FFI function (`C_<crate>__mx_rdata_get_Type_field`)
882/// - A setter FFI function (`C_<crate>__mx_rdata_set_Type_field`)
883/// - `R_CallMethodDef` entries for routine registration
884/// - R wrapper function code (roxygen-documented)
885/// - Class-integration code for R6 / S7 (active bindings / properties)
886///
887/// The generated constants are:
888/// - `RDATA_CALL_DEFS_{TYPE}`: slice of `R_CallMethodDef` for registration
889/// - `R_WRAPPERS_RDATA_{TYPE}`: string literal of R wrapper code
890///
891/// Returns `Err` if the struct has generic type parameters (`.Call` entrypoints
892/// cannot be generic).
893fn generate_sidecar_accessors(input: &DeriveInput, info: &SidecarInfo) -> syn::Result<TokenStream> {
894 // Reject generic structs — .Call entrypoints cannot be generic
895 if !input.generics.params.is_empty() {
896 return Err(syn::Error::new_spanned(
897 &input.generics,
898 "ExternalPtr does not support generic structs; \
899 .Call entrypoints cannot be generic",
900 ));
901 }
902
903 // If no selector or no public slots, nothing to register
904 let pub_slots: Vec<_> = info.slots.iter().filter(|s| s.is_public).collect();
905 if !info.has_selector || pub_slots.is_empty() {
906 return Ok(quote::quote! {});
907 }
908
909 let name = &input.ident;
910 let name_str = name.to_string();
911 let name_upper = name_str.to_uppercase();
912
913 // Base prot slot indices (type ID at 0, user at 1)
914 const PROT_BASE_LEN: usize = 2;
915
916 // Generate getter/setter functions and R wrappers for each pub slot
917 let mut c_functions = vec![];
918 let mut r_wrappers = String::new();
919
920 // Add documentation header that establishes the @name topic for sidecar accessors.
921 // This ensures @rdname references have a valid target even if the main class
922 // definition uses @noRd.
923 if !pub_slots.is_empty() {
924 r_wrappers.push_str(&format!(
925 r#"
926#' @title {type} Sidecar Accessors
927#' @name {type}
928#' @description Getter and setter functions for `#[r_data]` fields on `{type}`.
929#' @source Generated by miniextendr from `#[derive(ExternalPtr)]` on `{type}`
930NULL
931
932"#,
933 type = name_str,
934 ));
935 }
936
937 for slot in &pub_slots {
938 let field_name = &slot.name;
939 let field_name_str = field_name.to_string();
940 let prot_index = PROT_BASE_LEN + slot.index;
941
942 // C function names (crate-prefixed for webR cross-package symbol
943 // uniqueness — #1273, routed through the shared naming.rs helpers)
944 let getter_c_name = crate::naming::sidecar_getter_c_name(&name_str, &field_name_str);
945 let setter_c_name = crate::naming::sidecar_setter_c_name(&name_str, &field_name_str);
946 let getter_fn_name = Ident::new(&getter_c_name, Span::call_site());
947 let setter_fn_name = Ident::new(&setter_c_name, Span::call_site());
948 let source_location_doc = crate::source_location_doc(field_name.span());
949 let getter_doc = format!(
950 "Generated sidecar getter for `{}` field on Rust type `{}`.",
951 field_name_str, name_str
952 );
953 let setter_doc = format!(
954 "Generated sidecar setter for `{}` field on Rust type `{}`.",
955 field_name_str, name_str
956 );
957 let getter_doc_lit = syn::LitStr::new(&getter_doc, field_name.span());
958 let setter_doc_lit = syn::LitStr::new(&setter_doc, field_name.span());
959
960 let prot_index_lit = syn::LitInt::new(&prot_index.to_string(), Span::call_site());
961
962 // Generate getter/setter bodies based on slot kind
963 let getter_body = generate_getter_body(name, slot, &prot_index_lit);
964 let setter_body = generate_setter_body(name, slot, &prot_index_lit);
965
966 // Generate C getter function.
967 //
968 // The body runs under `with_r_unwind_protect` so Rust panics (and R
969 // longjmps during allocation) become a tagged condition SEXP that the
970 // R wrapper re-raises, matching the main call-slot path. Sidecar
971 // accessors have no `__miniextendr_call` slot (#344/#348), so the
972 // transport uses null call attribution; the R wrapper's guard passes
973 // `sys.call()` to `.miniextendr_raise_condition` as the fallback.
974 c_functions.push(quote::quote! {
975 #[doc = #getter_doc_lit]
976 #[doc = #source_location_doc]
977 #[doc = concat!("Generated from source file `", file!(), "`.")]
978 #[doc(hidden)]
979 #[unsafe(no_mangle)]
980 pub unsafe extern "C-unwind" fn #getter_fn_name(
981 x: ::miniextendr_api::SEXP
982 ) -> ::miniextendr_api::SEXP {
983 ::miniextendr_api::unwind_protect::with_r_unwind_protect(
984 || { #getter_body },
985 ::core::option::Option::None,
986 )
987 }
988 });
989
990 // Generate C setter function (same unwind/condition transport as the
991 // getter above).
992 c_functions.push(quote::quote! {
993 #[doc = #setter_doc_lit]
994 #[doc = #source_location_doc]
995 #[doc = concat!("Generated from source file `", file!(), "`.")]
996 #[doc(hidden)]
997 #[unsafe(no_mangle)]
998 pub unsafe extern "C-unwind" fn #setter_fn_name(
999 x: ::miniextendr_api::SEXP,
1000 value: ::miniextendr_api::SEXP,
1001 ) -> ::miniextendr_api::SEXP {
1002 ::miniextendr_api::unwind_protect::with_r_unwind_protect(
1003 || { #setter_body },
1004 ::core::option::Option::None,
1005 )
1006 }
1007 });
1008
1009 // Generate R_CallMethodDef entries via distributed slice
1010 let getter_c_name_cstr = format!("{}\0", getter_c_name);
1011 let setter_c_name_cstr = format!("{}\0", setter_c_name);
1012 let getter_cstr_lit =
1013 syn::LitByteStr::new(getter_c_name_cstr.as_bytes(), Span::call_site());
1014 let setter_cstr_lit =
1015 syn::LitByteStr::new(setter_c_name_cstr.as_bytes(), Span::call_site());
1016 let getter_def_ident = Ident::new(
1017 &format!(
1018 "__MX_CALL_DEF_RDATA_GET_{}_{}",
1019 name_upper,
1020 field_name_str.to_uppercase()
1021 ),
1022 Span::call_site(),
1023 );
1024 let setter_def_ident = Ident::new(
1025 &format!(
1026 "__MX_CALL_DEF_RDATA_SET_{}_{}",
1027 name_upper,
1028 field_name_str.to_uppercase()
1029 ),
1030 Span::call_site(),
1031 );
1032
1033 c_functions.push(quote::quote! {
1034 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
1035 #[doc(hidden)]
1036 static #getter_def_ident: ::miniextendr_api::sys::R_CallMethodDef =
1037 ::miniextendr_api::sys::R_CallMethodDef {
1038 name: #getter_cstr_lit.as_ptr().cast(),
1039 fun: Some(unsafe { ::std::mem::transmute(#getter_fn_name as unsafe extern "C-unwind" fn(_) -> _) }),
1040 numArgs: 1,
1041 };
1042 });
1043 c_functions.push(quote::quote! {
1044 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
1045 #[doc(hidden)]
1046 static #setter_def_ident: ::miniextendr_api::sys::R_CallMethodDef =
1047 ::miniextendr_api::sys::R_CallMethodDef {
1048 name: #setter_cstr_lit.as_ptr().cast(),
1049 fun: Some(unsafe { ::std::mem::transmute(#setter_fn_name as unsafe extern "C-unwind" fn(_, _) -> _) }),
1050 numArgs: 2,
1051 };
1052 });
1053
1054 // Generate R wrapper code based on class system
1055 let field_start = field_name.span().start();
1056 r_wrappers.push_str(&format!(
1057 "# Generated from Rust source line {}:{}\n# Wraps sidecar field `{}` on Rust type `{}` via `{}` and `{}`.\n",
1058 field_start.line,
1059 field_start.column + 1,
1060 field_name_str,
1061 name_str,
1062 getter_c_name,
1063 setter_c_name,
1064 ));
1065 r_wrappers.push_str(&generate_r_wrapper_for_slot(
1066 info.class_system,
1067 &name_str,
1068 &field_name_str,
1069 &getter_c_name,
1070 &setter_c_name,
1071 ));
1072 }
1073
1074 // Generate class-integrated R code for R6 and S7.
1075 // This code is appended after the standalone accessors so that
1076 // `r_data_accessors` in the impl block can auto-integrate sidecar fields.
1077 r_wrappers.push_str(&generate_class_integration_r_code(
1078 info.class_system,
1079 &name_str,
1080 &pub_slots,
1081 ));
1082
1083 let const_name_wrappers = Ident::new(
1084 &format!("R_WRAPPERS_RDATA_{}", name_upper),
1085 Span::call_site(),
1086 );
1087 let source_location_doc = crate::source_location_doc(name.span());
1088
1089 // For S7 class systems, emit MX_S7_SIDECAR_PROPS entries so the S7 codegen
1090 // can substitute @prop lines for sidecar properties at write time.
1091 let sidecar_prop_entries = if info.class_system == ClassSystem::S7 {
1092 let entries: Vec<_> = pub_slots
1093 .iter()
1094 .map(|slot| {
1095 let field_str = slot.name.to_string();
1096 let doc_str = slot
1097 .prop_doc
1098 .as_deref()
1099 .unwrap_or("(undocumented sidecar property)");
1100 let entry_ident = Ident::new(
1101 &format!(
1102 "__MX_S7_SIDECAR_PROP_{}_{}",
1103 name_upper,
1104 field_str.to_uppercase()
1105 ),
1106 Span::call_site(),
1107 );
1108 quote::quote! {
1109 #[doc(hidden)]
1110 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_S7_SIDECAR_PROPS), linkme(crate = ::miniextendr_api::linkme))]
1111 static #entry_ident: ::miniextendr_api::registry::SidecarPropEntry =
1112 ::miniextendr_api::registry::SidecarPropEntry {
1113 rust_type: #name_str,
1114 field_name: #field_str,
1115 prop_doc: #doc_str,
1116 };
1117 }
1118 })
1119 .collect();
1120 quote::quote! { #(#entries)* }
1121 } else {
1122 quote::quote! {}
1123 };
1124
1125 Ok(quote::quote! {
1126 #(#c_functions)*
1127
1128 /// Sidecar accessor R wrapper code via distributed slice.
1129 #[doc = #source_location_doc]
1130 #[doc = concat!("Generated from source file `", file!(), "`.")]
1131 #[doc(hidden)]
1132 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_R_WRAPPERS), linkme(crate = ::miniextendr_api::linkme))]
1133 static #const_name_wrappers: ::miniextendr_api::registry::RWrapperEntry =
1134 ::miniextendr_api::registry::RWrapperEntry {
1135 priority: ::miniextendr_api::registry::RWrapperPriority::Sidecar,
1136 source_file: file!(),
1137 content: #r_wrappers,
1138 };
1139
1140 #sidecar_prop_entries
1141 })
1142}
1143
1144/// Generate the `TypedExternal` trait implementation for the derive target.
1145///
1146/// Produces three associated constants:
1147/// - `TYPE_NAME`: the struct name as a `&'static str`
1148/// - `TYPE_NAME_CSTR`: null-terminated byte string of the struct name
1149/// - `TYPE_ID_CSTR`: globally unique ID in the format
1150/// `"<crate_name>@<crate_version>::<module_path>::<type_name>\0"`,
1151/// using `CARGO_PKG_NAME`, `CARGO_PKG_VERSION`, and `module_path!()`.
1152///
1153/// Supports generic structs (generics are forwarded to the impl).
1154fn generate_typed_external(input: &DeriveInput) -> TokenStream {
1155 let name = &input.ident;
1156 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1157
1158 let name_str = name.to_string();
1159 let name_lit = syn::LitStr::new(&name_str, name.span());
1160 let name_cstr = syn::LitByteStr::new(format!("{}\0", name_str).as_bytes(), name.span());
1161
1162 // TYPE_ID_CSTR format: "<crate_name>@<crate_version>::<module_path>::<type_name>\0"
1163 //
1164 // Uses env!("CARGO_PKG_NAME") and env!("CARGO_PKG_VERSION") for the crate identifier,
1165 // ensuring two packages with the same type name from the same crate+version are compatible,
1166 // while different crate versions are considered distinct types.
1167 //
1168 // The module_path!() may include "crate::" prefix when compiled within the crate,
1169 // but combined with the explicit crate@version prefix, this is unambiguous.
1170 quote::quote! {
1171 impl #impl_generics ::miniextendr_api::externalptr::TypedExternal for #name #ty_generics #where_clause {
1172 const TYPE_NAME: &'static str = #name_lit;
1173 const TYPE_NAME_CSTR: &'static [u8] = #name_cstr;
1174 const TYPE_ID_CSTR: &'static [u8] =
1175 concat!(
1176 env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION"),
1177 "::", module_path!(), "::", #name_lit, "\0"
1178 ).as_bytes();
1179 }
1180 }
1181}
1182
1183/// Generate the `IntoExternalPtr` marker trait impl.
1184///
1185/// This marker trait enables the blanket `impl<T: IntoExternalPtr> IntoR for T`
1186/// in miniextendr-api, allowing the type to be returned directly from functions.
1187fn generate_into_external_ptr(input: &DeriveInput) -> TokenStream {
1188 let name = &input.ident;
1189 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1190
1191 quote::quote! {
1192 impl #impl_generics ::miniextendr_api::externalptr::IntoExternalPtr for #name #ty_generics #where_clause {}
1193 }
1194}
1195
1196/// Generate the concrete `IntoRVecElement` impl (issue #1284).
1197///
1198/// Lights up the single `impl<T: IntoRVecElement> IntoR for Vec<T>` blanket in
1199/// miniextendr-api, so `-> Vec<MyType>` returns compile and produce a `VECSXP`
1200/// of external pointers. The impl must be concrete (not a blanket over
1201/// `IntoExternalPtr`) because the `MatchArg` bridge already occupies a blanket
1202/// on `IntoRVecElement` and two blankets would collide (E0119) — the same
1203/// funnel design `#[derive(IntoR)]` newtypes use (see
1204/// `miniextendr_api::newtype` module docs).
1205///
1206/// GC discipline: `ExternalPtr::collect_into_r_list` allocates each
1207/// `EXTPTRSXP` directly into the already-protected destination list, so no
1208/// element is ever live-but-unrooted across a later allocation.
1209fn generate_into_r_vec_element(input: &DeriveInput) -> TokenStream {
1210 let name = &input.ident;
1211 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1212
1213 quote::quote! {
1214 impl #impl_generics ::miniextendr_api::IntoRVecElement for #name #ty_generics #where_clause {
1215 #[inline]
1216 fn elements_into_sexp(values: ::std::vec::Vec<Self>) -> ::miniextendr_api::SEXP {
1217 ::miniextendr_api::externalptr::ExternalPtr::<Self>::collect_into_r_list(values)
1218 }
1219 }
1220 }
1221}
1222
1223/// Main entry point for `#[derive(ExternalPtr)]`.
1224///
1225/// Orchestrates the full derive expansion:
1226/// 1. Parses `#[externalptr(...)]` attributes for class system selection.
1227/// 2. Analyzes struct fields for `#[r_data]` sidecar slots.
1228/// 3. Generates `TypedExternal` impl (type identity for `ExternalPtr<T>`).
1229/// 4. Generates `IntoExternalPtr` marker impl (enables `IntoR` blanket impl)
1230/// and a concrete `IntoRVecElement` impl (enables the `IntoR for Vec<T>`
1231/// blanket, so `-> Vec<MyType>` returns a `VECSXP` of external pointers —
1232/// #1284). Both are suppressed when `emit_into_r_marker` is `false`, which
1233/// the struct-level `#[miniextendr(prefer = "native")]` path uses so its
1234/// concrete `AsRNative` `IntoR` does not collide with the blanket
1235/// `IntoExternalPtr` `IntoR` (E0119, #1283).
1236/// 5. Generates sidecar accessor FFI functions, registration constants, and R wrappers.
1237///
1238/// Returns the combined token stream of all generated items.
1239pub fn derive_external_ptr(
1240 input: DeriveInput,
1241 emit_into_r_marker: bool,
1242) -> syn::Result<TokenStream> {
1243 // Parse class system from #[externalptr(...)] attribute
1244 let class_system = parse_externalptr_attrs(&input)?;
1245
1246 // Parse sidecar information from struct fields
1247 let sidecar_info = parse_sidecar_info(&input, class_system)?;
1248
1249 let typed_external = generate_typed_external(&input);
1250 let (into_external_ptr, into_r_vec_element) = if emit_into_r_marker {
1251 (
1252 generate_into_external_ptr(&input),
1253 generate_into_r_vec_element(&input),
1254 )
1255 } else {
1256 (
1257 proc_macro2::TokenStream::new(),
1258 proc_macro2::TokenStream::new(),
1259 )
1260 };
1261 let sidecar_accessors = generate_sidecar_accessors(&input, &sidecar_info)?;
1262 let erased_wrapper = generate_erased_wrapper(&input);
1263
1264 Ok(quote::quote! {
1265 #typed_external
1266 #into_external_ptr
1267 #into_r_vec_element
1268 #sidecar_accessors
1269 #erased_wrapper
1270 })
1271}
1272
1273/// Generate the type-erased wrapper infrastructure for trait ABI dispatch.
1274///
1275/// For `#[derive(ExternalPtr)] struct MyCounter { ... }` generates:
1276/// - `__MxWrapperMyCounter` - repr(C) wrapper with mx_erased header + data
1277/// - `__MX_TAG_MYCOUNTER` - concrete type tag (FNV-1a hash of module_path + type)
1278/// - `__mx_drop_mycounter` - destructor for R GC
1279/// - `__MX_BASE_VTABLE_MYCOUNTER` - base vtable with universal_query
1280/// - `__mx_wrap_mycounter` - constructor returning `*mut mx_erased`
1281fn generate_erased_wrapper(input: &DeriveInput) -> TokenStream {
1282 let type_ident = &input.ident;
1283
1284 // Only for non-generic types (generics can't participate in trait dispatch)
1285 if !input.generics.params.is_empty() {
1286 return quote::quote! {};
1287 }
1288
1289 let type_upper = type_ident.to_string().to_uppercase();
1290 let type_lower = type_ident.to_string().to_lowercase();
1291
1292 let wrapper_name = quote::format_ident!("__MxWrapper{}", type_ident);
1293 let base_vtable_name = quote::format_ident!("__MX_BASE_VTABLE_{}", type_upper);
1294 let concrete_tag_name = quote::format_ident!("__MX_TAG_{}", type_upper);
1295 let drop_fn_name = quote::format_ident!("__mx_drop_{}", type_lower);
1296 let wrap_fn_name = quote::format_ident!("__mx_wrap_{}", type_lower);
1297 let source_loc_doc = crate::source_location_doc(type_ident.span());
1298 let tag_path = format!("::{}", type_ident);
1299
1300 quote::quote! {
1301 #[doc = concat!(
1302 "Type-erased wrapper for `",
1303 stringify!(#type_ident),
1304 "` with trait dispatch support."
1305 )]
1306 #[doc = "Generated by `#[derive(ExternalPtr)]`."]
1307 #[doc = #source_loc_doc]
1308 #[doc = concat!("Generated from source file `", file!(), "`.")]
1309 #[repr(C)]
1310 #[doc(hidden)]
1311 struct #wrapper_name {
1312 pub erased: ::miniextendr_api::abi::mx_erased,
1313 pub data: #type_ident,
1314 }
1315
1316 #[doc(hidden)]
1317 const #concrete_tag_name: ::miniextendr_api::abi::mx_tag =
1318 ::miniextendr_api::abi::mx_tag_from_path(concat!(module_path!(), #tag_path));
1319
1320 #[doc(hidden)]
1321 unsafe extern "C" fn #drop_fn_name(ptr: *mut ::miniextendr_api::abi::mx_erased) {
1322 if ptr.is_null() {
1323 return;
1324 }
1325 let wrapper = ptr.cast::<#wrapper_name>();
1326 // A panicking Drop impl must not unwind across the C-ABI boundary.
1327 // `drop_catching_panic` catches any panic and aborts instead.
1328 ::miniextendr_api::externalptr::drop_catching_panic(|| {
1329 unsafe { drop(Box::from_raw(wrapper)); }
1330 });
1331 }
1332
1333 #[doc(hidden)]
1334 static #base_vtable_name: ::miniextendr_api::abi::mx_base_vtable =
1335 ::miniextendr_api::abi::mx_base_vtable {
1336 drop: #drop_fn_name,
1337 concrete_tag: #concrete_tag_name,
1338 query: ::miniextendr_api::registry::universal_query,
1339 data_offset: ::std::mem::offset_of!(#wrapper_name, data),
1340 };
1341
1342 #[doc(hidden)]
1343 fn #wrap_fn_name(data: #type_ident) -> *mut ::miniextendr_api::abi::mx_erased {
1344 let wrapper = Box::new(#wrapper_name {
1345 erased: ::miniextendr_api::abi::mx_erased {
1346 base: &#base_vtable_name,
1347 },
1348 data,
1349 });
1350 Box::into_raw(wrapper).cast::<::miniextendr_api::abi::mx_erased>()
1351 }
1352 }
1353}
1354
1355#[cfg(test)]
1356mod tests {
1357 use super::{ClassSystem, generate_class_integration_r_code, generate_r_wrapper_for_slot};
1358
1359 const ALL_CLASS_SYSTEMS: [ClassSystem; 6] = [
1360 ClassSystem::Env,
1361 ClassSystem::R6,
1362 ClassSystem::S3,
1363 ClassSystem::S4,
1364 ClassSystem::S7,
1365 ClassSystem::Vctrs,
1366 ];
1367
1368 /// Sidecar accessor C functions take only `x` (getter) or `x, value` (setter) —
1369 /// no `__miniextendr_call` parameter. The R wrappers must NOT pass `.call = match.call()`
1370 /// because that would be counted as an extra positional argument by `.Call()`, causing
1371 /// "Incorrect number of arguments" errors at runtime. Cover every class system variant.
1372 #[test]
1373 fn sidecar_accessors_do_not_pass_match_call() {
1374 let getter_c = "C__mx_rdata_get_T_f";
1375 let setter_c = "C__mx_rdata_set_T_f";
1376
1377 for cs in ALL_CLASS_SYSTEMS {
1378 let out = generate_r_wrapper_for_slot(cs, "T", "f", getter_c, setter_c);
1379 // Correct form: no .call argument — the C function only accepts x (getter) or x, value (setter).
1380 assert!(
1381 out.contains(&format!(".Call({getter_c}, x)")),
1382 "{cs:?} getter should call without .call:\n{out}"
1383 );
1384 assert!(
1385 out.contains(&format!(".Call({setter_c}, x, value)")),
1386 "{cs:?} setter should call without .call:\n{out}"
1387 );
1388 // Must NOT include the erroneous .call = match.call() form.
1389 assert!(
1390 !out.contains(".call = match.call()"),
1391 "{cs:?} sidecar wrapper must not pass .call = match.call():\n{out}"
1392 );
1393 }
1394 }
1395
1396 /// Every sidecar R wrapper (getter and setter, all class systems) must
1397 /// carry the tagged-condition guard so Rust panics / conversion failures
1398 /// transported by `with_r_unwind_protect(…, None)` are re-raised as
1399 /// structured R conditions instead of leaking the tagged SEXP to the user.
1400 #[test]
1401 fn sidecar_accessors_reraise_tagged_conditions() {
1402 let getter_c = "C__mx_rdata_get_T_f";
1403 let setter_c = "C__mx_rdata_set_T_f";
1404
1405 for cs in ALL_CLASS_SYSTEMS {
1406 let out = generate_r_wrapper_for_slot(cs, "T", "f", getter_c, setter_c);
1407 assert_eq!(
1408 out.matches(".miniextendr_raise_condition(.val, sys.call())")
1409 .count(),
1410 2,
1411 "{cs:?} getter+setter should each carry the condition guard:\n{out}"
1412 );
1413 }
1414 }
1415
1416 /// The R6 active-binding and S7 property integration code also call the
1417 /// sidecar C entrypoints directly; they need the same guard (with
1418 /// `sys.call()` fallback attribution) and must not pass `.call =`.
1419 #[test]
1420 fn sidecar_class_integration_reraises_tagged_conditions() {
1421 let slot = super::SidecarSlot {
1422 name: syn::Ident::new("f", proc_macro2::Span::call_site()),
1423 ty: syn::parse_quote!(i32),
1424 index: 0,
1425 is_public: true,
1426 kind: super::SlotKind::ScalarInt,
1427 prop_doc: None,
1428 };
1429 let slots = [&slot];
1430
1431 for cs in [ClassSystem::R6, ClassSystem::S7] {
1432 let out = generate_class_integration_r_code(cs, "T", &slots);
1433 assert_eq!(
1434 out.matches(".miniextendr_raise_condition(.val, sys.call())")
1435 .count(),
1436 2,
1437 "{cs:?} integration getter+setter should each carry the condition guard:\n{out}"
1438 );
1439 assert!(
1440 !out.contains(".call = match.call()"),
1441 "{cs:?} integration code must not pass .call = match.call():\n{out}"
1442 );
1443 }
1444 }
1445}