1use proc_macro2::{Span, TokenStream};
115use syn::{DeriveInput, Field, Ident, Visibility};
116
117use crate::miniextendr_impl::ClassSystem;
118
119fn parse_externalptr_attrs(input: &DeriveInput) -> syn::Result<ClassSystem> {
128 let mut class_system = ClassSystem::Env; for attr in &input.attrs {
131 if attr.path().is_ident("externalptr") {
132 attr.parse_nested_meta(|meta| {
133 let ident_str = meta
134 .path
135 .get_ident()
136 .map(|i| i.to_string())
137 .unwrap_or_default();
138
139 match ident_str.as_str() {
140 "env" => class_system = ClassSystem::Env,
141 "r6" => class_system = ClassSystem::R6,
142 "s3" => class_system = ClassSystem::S3,
143 "s4" => class_system = ClassSystem::S4,
144 "s7" => class_system = ClassSystem::S7,
145 "vctrs" => class_system = ClassSystem::Vctrs,
146 _ => {
147 return Err(syn::Error::new_spanned(
148 &meta.path,
149 format!(
150 "unknown class system '{}'; expected one of: env, r6, s3, s4, s7, vctrs",
151 ident_str
152 ),
153 ));
154 }
155 }
156 Ok(())
157 })?;
158 }
159 }
160
161 Ok(class_system)
162}
163
164fn has_r_data_attr(field: &Field) -> bool {
166 field.attrs.iter().any(|a| a.path().is_ident("r_data"))
167}
168
169fn parse_r_data_prop_doc(field: &Field) -> syn::Result<Option<String>> {
174 for attr in &field.attrs {
175 if !attr.path().is_ident("r_data") {
176 continue;
177 }
178 if matches!(attr.meta, syn::Meta::Path(_)) {
180 return Ok(None);
181 }
182 let mut prop_doc = None;
183 attr.parse_nested_meta(|meta| {
184 if meta.path.is_ident("prop_doc") {
185 let value = meta.value()?;
186 let lit: syn::LitStr = value.parse()?;
187 prop_doc = Some(lit.value());
188 Ok(())
189 } else {
190 Err(meta.error(format!(
191 "unknown key `{}`; supported: `prop_doc`",
192 meta.path
193 .get_ident()
194 .map(|i| i.to_string())
195 .unwrap_or_default()
196 )))
197 }
198 })?;
199 return Ok(prop_doc);
200 }
201 Ok(None)
202}
203
204fn is_rsidecar_type(field: &Field) -> bool {
209 if let syn::Type::Path(type_path) = &field.ty {
210 type_path
211 .path
212 .segments
213 .last()
214 .map(|seg| seg.ident == "RSidecar")
215 .unwrap_or(false)
216 } else {
217 false
218 }
219}
220
221fn is_pub(field: &Field) -> bool {
223 matches!(field.vis, Visibility::Public(_))
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
234enum SlotKind {
235 RawSexp,
237 ScalarInt,
239 ScalarReal,
241 ScalarLogical,
243 ScalarRaw,
245 Conversion,
247}
248
249struct SidecarSlot {
254 name: Ident,
256 ty: syn::Type,
258 index: usize,
261 is_public: bool,
263 kind: SlotKind,
265 prop_doc: Option<String>,
269}
270
271struct SidecarInfo {
276 has_selector: bool,
279 slots: Vec<SidecarSlot>,
282 class_system: ClassSystem,
285}
286
287fn slot_kind_for_type(ty: &syn::Type) -> SlotKind {
293 if let syn::Type::Path(type_path) = ty
294 && let Some(seg) = type_path.path.segments.last()
295 {
296 let ident = &seg.ident;
297 if ident == "SEXP" {
299 return SlotKind::RawSexp;
300 }
301 if ident == "i32" || ident == "i16" || ident == "i8" {
303 return SlotKind::ScalarInt;
304 }
305 if ident == "f64" || ident == "f32" {
306 return SlotKind::ScalarReal;
307 }
308 if ident == "bool" || ident == "Rbool" {
309 return SlotKind::ScalarLogical;
310 }
311 if ident == "u8" {
312 return SlotKind::ScalarRaw;
313 }
314 }
315 SlotKind::Conversion
317}
318
319fn parse_sidecar_info(input: &DeriveInput, class_system: ClassSystem) -> syn::Result<SidecarInfo> {
328 let fields = match &input.data {
329 syn::Data::Struct(data) => &data.fields,
330 _ => {
331 return Ok(SidecarInfo {
332 has_selector: false,
333 slots: vec![],
334 class_system,
335 });
336 }
337 };
338
339 let mut selector_fields: Vec<&Field> = vec![];
340 let mut slots = vec![];
341 let mut slot_index = 0usize;
342
343 for field in fields.iter() {
344 if !has_r_data_attr(field) {
345 continue;
346 }
347
348 if is_rsidecar_type(field) {
349 selector_fields.push(field);
351 } else if let Some(ref ident) = field.ident {
352 let kind = slot_kind_for_type(&field.ty);
354 let prop_doc = parse_r_data_prop_doc(field)?;
355 slots.push(SidecarSlot {
356 name: ident.clone(),
357 ty: field.ty.clone(),
358 index: slot_index,
359 is_public: is_pub(field),
360 kind,
361 prop_doc,
362 });
363 slot_index += 1;
364 }
365 }
366
367 if selector_fields.len() > 1 {
369 return Err(syn::Error::new_spanned(
370 selector_fields[1],
371 "only one RSidecar field is allowed per struct",
372 ));
373 }
374
375 Ok(SidecarInfo {
376 has_selector: !selector_fields.is_empty(),
377 slots,
378 class_system,
379 })
380}
381
382fn generate_getter_body(
393 struct_name: &syn::Ident,
394 slot: &SidecarSlot,
395 _prot_index_lit: &syn::LitInt,
396) -> TokenStream {
397 let field_name = &slot.name;
398
399 let extract_ref = quote::quote! {
402 use ::miniextendr_api::ffi::{R_ExternalPtrAddr, SEXP};
403 let any_raw = R_ExternalPtrAddr(x) as *mut Box<dyn ::std::any::Any>;
404 if any_raw.is_null() {
405 return SEXP::nil();
406 }
407 let any_box: &Box<dyn ::std::any::Any> = &*any_raw;
408 let data: &#struct_name = match any_box.downcast_ref::<#struct_name>() {
409 Some(v) => v,
410 None => return SEXP::nil(),
411 };
412 };
413
414 match slot.kind {
415 SlotKind::RawSexp => {
416 quote::quote! {
418 unsafe {
419 #extract_ref
420 data.#field_name
421 }
422 }
423 }
424 SlotKind::ScalarInt => {
425 quote::quote! {
426 use ::miniextendr_api::ffi::SEXP;
427 unsafe {
428 #extract_ref
429 SEXP::scalar_integer(data.#field_name)
430 }
431 }
432 }
433 SlotKind::ScalarReal => {
434 quote::quote! {
435 use ::miniextendr_api::ffi::SEXP;
436 unsafe {
437 #extract_ref
438 SEXP::scalar_real(data.#field_name)
439 }
440 }
441 }
442 SlotKind::ScalarLogical => {
443 quote::quote! {
444 use ::miniextendr_api::ffi::SEXP;
445 unsafe {
446 #extract_ref
447 SEXP::scalar_logical(data.#field_name)
448 }
449 }
450 }
451 SlotKind::ScalarRaw => {
452 quote::quote! {
453 use ::miniextendr_api::ffi::SEXP;
454 unsafe {
455 #extract_ref
456 SEXP::scalar_raw(data.#field_name)
457 }
458 }
459 }
460 SlotKind::Conversion => {
461 let ty = &slot.ty;
463 quote::quote! {
464 use ::miniextendr_api::into_r::IntoR;
465 unsafe {
466 #extract_ref
467 let val: #ty = data.#field_name.clone();
468 <#ty as IntoR>::into_sexp(val)
469 }
470 }
471 }
472 }
473}
474
475fn generate_setter_body(
486 struct_name: &syn::Ident,
487 slot: &SidecarSlot,
488 _prot_index_lit: &syn::LitInt,
489) -> TokenStream {
490 let field_name = &slot.name;
491
492 let extract_mut = quote::quote! {
494 use ::miniextendr_api::ffi::R_ExternalPtrAddr;
495 let any_raw = R_ExternalPtrAddr(x) as *mut Box<dyn ::std::any::Any>;
496 if any_raw.is_null() {
497 return x;
498 }
499 let any_box: &mut Box<dyn ::std::any::Any> = &mut *any_raw;
500 let Some(data) = any_box.downcast_mut::<#struct_name>() else {
501 return x;
502 };
503 };
504
505 match slot.kind {
506 SlotKind::RawSexp => {
507 quote::quote! {
508 unsafe {
509 #extract_mut
510 data.#field_name = value;
511 x
512 }
513 }
514 }
515 SlotKind::ScalarInt => {
516 quote::quote! {
517 use ::miniextendr_api::ffi::SexpExt;
518 unsafe {
519 #extract_mut
520 data.#field_name = value.as_integer().unwrap_or(::miniextendr_api::altrep_traits::NA_INTEGER);
521 x
522 }
523 }
524 }
525 SlotKind::ScalarReal => {
526 quote::quote! {
527 use ::miniextendr_api::ffi::SexpExt;
528 unsafe {
529 #extract_mut
530 data.#field_name = value.as_real().unwrap_or(::miniextendr_api::altrep_traits::NA_REAL);
531 x
532 }
533 }
534 }
535 SlotKind::ScalarLogical => {
536 quote::quote! {
537 use ::miniextendr_api::ffi::SexpExt;
538 unsafe {
539 #extract_mut
540 data.#field_name = value.as_logical().unwrap_or(false);
541 x
542 }
543 }
544 }
545 SlotKind::ScalarRaw => {
546 quote::quote! {
547 use ::miniextendr_api::ffi::{SexpExt, SEXPTYPE};
548 unsafe {
549 #extract_mut
550 let raw_vec = value.coerce(SEXPTYPE::RAWSXP);
551 data.#field_name = raw_vec.raw_elt(0);
552 x
553 }
554 }
555 }
556 SlotKind::Conversion => {
557 let ty = &slot.ty;
558 quote::quote! {
559 use ::miniextendr_api::TryFromSexp;
560 unsafe {
561 #extract_mut
562 if let Ok(val) = <#ty as TryFromSexp>::try_from_sexp(value) {
563 data.#field_name = val;
564 }
565 x
566 }
567 }
568 }
569 }
570}
571
572fn generate_r_wrapper_for_slot(
583 class_system: ClassSystem,
584 type_name: &str,
585 field_name: &str,
586 getter_c_name: &str,
587 setter_c_name: &str,
588) -> String {
589 match class_system {
590 ClassSystem::Env => {
591 let r_getter_name = format!("{}_get_{}", type_name, field_name);
593 let r_setter_name = format!("{}_set_{}", type_name, field_name);
594 format!(
595 r#"
596#' Get `{field}` field from {type}
597#' @rdname {type}
598#' @param x The {type} external pointer
599#' @return The value of the `{field}` field
600#' @export
601{r_getter} <- function(x) .Call({getter_c}, x)
602
603#' Set `{field}` field on {type}
604#' @rdname {type}
605#' @param x The {type} external pointer
606#' @param value The new value to set
607#' @return The {type} pointer (invisibly)
608#' @export
609{r_setter} <- function(x, value) {{
610 .Call({setter_c}, x, value)
611 invisible(x)
612}}
613"#,
614 type = type_name,
615 field = field_name,
616 r_getter = r_getter_name,
617 r_setter = r_setter_name,
618 getter_c = getter_c_name,
619 setter_c = setter_c_name,
620 )
621 }
622 ClassSystem::R6 => {
623 let r_getter_name = format!("{}_get_{}", type_name, field_name);
626 let r_setter_name = format!("{}_set_{}", type_name, field_name);
627 format!(
628 r#"
629#' Get `{field}` field from {type} (for R6)
630#' @rdname {type}
631#' @param x The {type} external pointer
632#' @return The value of the `{field}` field
633#' @export
634{r_getter} <- function(x) .Call({getter_c}, x)
635
636#' Set `{field}` field on {type} (for R6)
637#' @rdname {type}
638#' @param x The {type} external pointer
639#' @param value The new value to set
640#' @return The {type} pointer (invisibly)
641#' @export
642{r_setter} <- function(x, value) {{
643 .Call({setter_c}, x, value)
644 invisible(x)
645}}
646"#,
647 type = type_name,
648 field = field_name,
649 r_getter = r_getter_name,
650 r_setter = r_setter_name,
651 getter_c = getter_c_name,
652 setter_c = setter_c_name,
653 )
654 }
655 ClassSystem::S3 => {
656 let r_getter_name = format!("{}_get_{}", type_name, field_name);
660 let r_setter_name = format!("{}_set_{}", type_name, field_name);
661 format!(
662 r#"
663#' Get `{field}` field from {type} (for S3)
664#' @rdname {type}
665#' @param x The {type} external pointer
666#' @return The value of the `{field}` field
667#' @export
668{r_getter} <- function(x) .Call({getter_c}, x)
669
670#' Set `{field}` field on {type} (for S3)
671#' @rdname {type}
672#' @param x The {type} external pointer
673#' @param value The new value to set
674#' @return The {type} pointer (invisibly)
675#' @export
676{r_setter} <- function(x, value) {{
677 .Call({setter_c}, x, value)
678 invisible(x)
679}}
680"#,
681 type = type_name,
682 field = field_name,
683 r_getter = r_getter_name,
684 r_setter = r_setter_name,
685 getter_c = getter_c_name,
686 setter_c = setter_c_name,
687 )
688 }
689 ClassSystem::S4 => {
690 let r_getter_name = format!("{}_get_{}", type_name, field_name);
693 let r_setter_name = format!("{}_set_{}", type_name, field_name);
694 format!(
695 r#"
696#' Get `{field}` field from {type} (for S4)
697#' @rdname {type}
698#' @param x The {type} external pointer
699#' @return The value of the `{field}` field
700#' @export
701{r_getter} <- function(x) .Call({getter_c}, x)
702
703#' Set `{field}` field on {type} (for S4)
704#' @rdname {type}
705#' @param x The {type} external pointer
706#' @param value The new value to set
707#' @return The {type} pointer (invisibly)
708#' @export
709{r_setter} <- function(x, value) {{
710 .Call({setter_c}, x, value)
711 invisible(x)
712}}
713"#,
714 type = type_name,
715 field = field_name,
716 r_getter = r_getter_name,
717 r_setter = r_setter_name,
718 getter_c = getter_c_name,
719 setter_c = setter_c_name,
720 )
721 }
722 ClassSystem::S7 => {
723 let r_getter_name = format!("{}_get_{}", type_name, field_name);
726 let r_setter_name = format!("{}_set_{}", type_name, field_name);
727 format!(
728 r#"
729#' Get `{field}` field from {type} (for S7)
730#' @rdname {type}
731#' @param x The {type} external pointer
732#' @return The value of the `{field}` field
733#' @export
734{r_getter} <- function(x) .Call({getter_c}, x)
735
736#' Set `{field}` field on {type} (for S7)
737#' @rdname {type}
738#' @param x The {type} external pointer
739#' @param value The new value to set
740#' @return The {type} pointer (invisibly)
741#' @export
742{r_setter} <- function(x, value) {{
743 .Call({setter_c}, x, value)
744 invisible(x)
745}}
746"#,
747 type = type_name,
748 field = field_name,
749 r_getter = r_getter_name,
750 r_setter = r_setter_name,
751 getter_c = getter_c_name,
752 setter_c = setter_c_name,
753 )
754 }
755 ClassSystem::Vctrs => {
756 let r_getter_name = format!("{}_get_{}", type_name, field_name);
760 let r_setter_name = format!("{}_set_{}", type_name, field_name);
761 format!(
762 r#"
763#' Get `{field}` field from {type} (for vctrs)
764#' @rdname {type}
765#' @param x The {type} external pointer
766#' @return The value of the `{field}` field
767#' @export
768{r_getter} <- function(x) .Call({getter_c}, x)
769
770#' Set `{field}` field on {type} (for vctrs)
771#' @rdname {type}
772#' @param x The {type} external pointer
773#' @param value The new value to set
774#' @return The {type} pointer (invisibly)
775#' @export
776{r_setter} <- function(x, value) {{
777 .Call({setter_c}, x, value)
778 invisible(x)
779}}
780"#,
781 type = type_name,
782 field = field_name,
783 r_getter = r_getter_name,
784 r_setter = r_setter_name,
785 getter_c = getter_c_name,
786 setter_c = setter_c_name,
787 )
788 }
789 }
790}
791
792fn generate_class_integration_r_code(
802 class_system: ClassSystem,
803 type_name: &str,
804 pub_slots: &[&SidecarSlot],
805) -> String {
806 if pub_slots.is_empty() {
807 return String::new();
808 }
809
810 match class_system {
811 ClassSystem::R6 => {
812 let mut code = String::new();
816 code.push_str(&format!(
817 "\n# Auto-generated active bindings for {type} sidecar fields.\n",
818 type = type_name,
819 ));
820 code.push_str(
821 "# These are applied when `r_data_accessors` is set on the impl block.\n",
822 );
823 code.push_str(&format!(
824 ".rdata_active_bindings_{type} <- function(cls) {{\n\
825 \x20 # R CMD check: self/private are R6 runtime bindings (set by cls$set)\n\
826 \x20 self <- private <- NULL\n",
827 type = type_name,
828 ));
829 for slot in pub_slots {
830 let field = slot.name.to_string();
831 let getter_c = format!("C__mx_rdata_get_{}_{}", type_name, field);
832 let setter_c = format!("C__mx_rdata_set_{}_{}", type_name, field);
833 code.push_str(&format!(
834 " cls$set(\"active\", \"{field}\", function(value) {{\n\
835 \x20 if (missing(value)) .Call({getter_c}, private$.ptr)\n\
836 \x20 else {{ .Call({setter_c}, private$.ptr, value); invisible(self) }}\n\
837 \x20 }}, overwrite = TRUE)\n",
838 field = field,
839 getter_c = getter_c,
840 setter_c = setter_c,
841 ));
842 }
843 code.push_str("}\n");
844 code
845 }
846 ClassSystem::S7 => {
847 let mut code = String::new();
850 code.push_str(&format!(
851 "\n# Auto-generated S7 property definitions for {type} sidecar fields.\n",
852 type = type_name,
853 ));
854 code.push_str(&format!(
855 ".rdata_properties_{type} <- list(\n",
856 type = type_name,
857 ));
858 for (i, slot) in pub_slots.iter().enumerate() {
859 let field = slot.name.to_string();
860 let getter_c = format!("C__mx_rdata_get_{}_{}", type_name, field);
861 let setter_c = format!("C__mx_rdata_set_{}_{}", type_name, field);
862 let comma = if i < pub_slots.len() - 1 { "," } else { "" };
863 code.push_str(&format!(
864 " {field} = S7::new_property(\n\
865 \x20 getter = function(self) .Call({getter_c}, self@.ptr),\n\
866 \x20 setter = function(self, value) {{ .Call({setter_c}, self@.ptr, value); self }}\n\
867 \x20 ){comma}\n",
868 field = field,
869 getter_c = getter_c,
870 setter_c = setter_c,
871 comma = comma,
872 ));
873 }
874 code.push_str(")\n");
875 code
876 }
877 _ => String::new(),
879 }
880}
881
882fn generate_sidecar_accessors(input: &DeriveInput, info: &SidecarInfo) -> syn::Result<TokenStream> {
898 if !input.generics.params.is_empty() {
900 return Err(syn::Error::new_spanned(
901 &input.generics,
902 "ExternalPtr does not support generic structs; \
903 .Call entrypoints cannot be generic",
904 ));
905 }
906
907 let pub_slots: Vec<_> = info.slots.iter().filter(|s| s.is_public).collect();
909 if !info.has_selector || pub_slots.is_empty() {
910 return Ok(quote::quote! {});
911 }
912
913 let name = &input.ident;
914 let name_str = name.to_string();
915 let name_upper = name_str.to_uppercase();
916
917 const PROT_BASE_LEN: usize = 2;
919
920 let mut c_functions = vec![];
922 let mut r_wrappers = String::new();
923
924 if !pub_slots.is_empty() {
928 r_wrappers.push_str(&format!(
929 r#"
930#' @title {type} Sidecar Accessors
931#' @name {type}
932#' @description Getter and setter functions for `#[r_data]` fields on `{type}`.
933#' @source Generated by miniextendr from `#[derive(ExternalPtr)]` on `{type}`
934NULL
935
936"#,
937 type = name_str,
938 ));
939 }
940
941 for slot in &pub_slots {
942 let field_name = &slot.name;
943 let field_name_str = field_name.to_string();
944 let prot_index = PROT_BASE_LEN + slot.index;
945
946 let getter_c_name = format!("C__mx_rdata_get_{}_{}", name_str, field_name_str);
948 let setter_c_name = format!("C__mx_rdata_set_{}_{}", name_str, field_name_str);
949 let getter_fn_name = Ident::new(&getter_c_name, Span::call_site());
950 let setter_fn_name = Ident::new(&setter_c_name, Span::call_site());
951 let source_location_doc = crate::source_location_doc(field_name.span());
952 let getter_doc = format!(
953 "Generated sidecar getter for `{}` field on Rust type `{}`.",
954 field_name_str, name_str
955 );
956 let setter_doc = format!(
957 "Generated sidecar setter for `{}` field on Rust type `{}`.",
958 field_name_str, name_str
959 );
960 let getter_doc_lit = syn::LitStr::new(&getter_doc, field_name.span());
961 let setter_doc_lit = syn::LitStr::new(&setter_doc, field_name.span());
962
963 let prot_index_lit = syn::LitInt::new(&prot_index.to_string(), Span::call_site());
964
965 let getter_body = generate_getter_body(name, slot, &prot_index_lit);
967 let setter_body = generate_setter_body(name, slot, &prot_index_lit);
968
969 c_functions.push(quote::quote! {
971 #[doc = #getter_doc_lit]
972 #[doc = #source_location_doc]
973 #[doc = concat!("Generated from source file `", file!(), "`.")]
974 #[doc(hidden)]
975 #[unsafe(no_mangle)]
976 pub unsafe extern "C-unwind" fn #getter_fn_name(
977 x: ::miniextendr_api::ffi::SEXP
978 ) -> ::miniextendr_api::ffi::SEXP {
979 #getter_body
980 }
981 });
982
983 c_functions.push(quote::quote! {
985 #[doc = #setter_doc_lit]
986 #[doc = #source_location_doc]
987 #[doc = concat!("Generated from source file `", file!(), "`.")]
988 #[doc(hidden)]
989 #[unsafe(no_mangle)]
990 pub unsafe extern "C-unwind" fn #setter_fn_name(
991 x: ::miniextendr_api::ffi::SEXP,
992 value: ::miniextendr_api::ffi::SEXP,
993 ) -> ::miniextendr_api::ffi::SEXP {
994 #setter_body
995 }
996 });
997
998 let getter_c_name_cstr = format!("{}\0", getter_c_name);
1000 let setter_c_name_cstr = format!("{}\0", setter_c_name);
1001 let getter_cstr_lit =
1002 syn::LitByteStr::new(getter_c_name_cstr.as_bytes(), Span::call_site());
1003 let setter_cstr_lit =
1004 syn::LitByteStr::new(setter_c_name_cstr.as_bytes(), Span::call_site());
1005 let getter_def_ident = Ident::new(
1006 &format!(
1007 "__MX_CALL_DEF_RDATA_GET_{}_{}",
1008 name_upper,
1009 field_name_str.to_uppercase()
1010 ),
1011 Span::call_site(),
1012 );
1013 let setter_def_ident = Ident::new(
1014 &format!(
1015 "__MX_CALL_DEF_RDATA_SET_{}_{}",
1016 name_upper,
1017 field_name_str.to_uppercase()
1018 ),
1019 Span::call_site(),
1020 );
1021
1022 c_functions.push(quote::quote! {
1023 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
1024 #[doc(hidden)]
1025 static #getter_def_ident: ::miniextendr_api::ffi::R_CallMethodDef =
1026 ::miniextendr_api::ffi::R_CallMethodDef {
1027 name: #getter_cstr_lit.as_ptr().cast(),
1028 fun: Some(unsafe { ::std::mem::transmute(#getter_fn_name as unsafe extern "C-unwind" fn(_) -> _) }),
1029 numArgs: 1,
1030 };
1031 });
1032 c_functions.push(quote::quote! {
1033 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_CALL_DEFS), linkme(crate = ::miniextendr_api::linkme))]
1034 #[doc(hidden)]
1035 static #setter_def_ident: ::miniextendr_api::ffi::R_CallMethodDef =
1036 ::miniextendr_api::ffi::R_CallMethodDef {
1037 name: #setter_cstr_lit.as_ptr().cast(),
1038 fun: Some(unsafe { ::std::mem::transmute(#setter_fn_name as unsafe extern "C-unwind" fn(_, _) -> _) }),
1039 numArgs: 2,
1040 };
1041 });
1042
1043 let field_start = field_name.span().start();
1045 r_wrappers.push_str(&format!(
1046 "# Generated from Rust source line {}:{}\n# Wraps sidecar field `{}` on Rust type `{}` via `{}` and `{}`.\n",
1047 field_start.line,
1048 field_start.column + 1,
1049 field_name_str,
1050 name_str,
1051 getter_c_name,
1052 setter_c_name,
1053 ));
1054 r_wrappers.push_str(&generate_r_wrapper_for_slot(
1055 info.class_system,
1056 &name_str,
1057 &field_name_str,
1058 &getter_c_name,
1059 &setter_c_name,
1060 ));
1061 }
1062
1063 r_wrappers.push_str(&generate_class_integration_r_code(
1067 info.class_system,
1068 &name_str,
1069 &pub_slots,
1070 ));
1071
1072 let const_name_wrappers = Ident::new(
1073 &format!("R_WRAPPERS_RDATA_{}", name_upper),
1074 Span::call_site(),
1075 );
1076 let source_location_doc = crate::source_location_doc(name.span());
1077
1078 let sidecar_prop_entries = if info.class_system == ClassSystem::S7 {
1081 let entries: Vec<_> = pub_slots
1082 .iter()
1083 .map(|slot| {
1084 let field_str = slot.name.to_string();
1085 let doc_str = slot
1086 .prop_doc
1087 .as_deref()
1088 .unwrap_or("(undocumented sidecar property)");
1089 let entry_ident = Ident::new(
1090 &format!(
1091 "__MX_S7_SIDECAR_PROP_{}_{}",
1092 name_upper,
1093 field_str.to_uppercase()
1094 ),
1095 Span::call_site(),
1096 );
1097 quote::quote! {
1098 #[doc(hidden)]
1099 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_S7_SIDECAR_PROPS), linkme(crate = ::miniextendr_api::linkme))]
1100 static #entry_ident: ::miniextendr_api::registry::SidecarPropEntry =
1101 ::miniextendr_api::registry::SidecarPropEntry {
1102 rust_type: #name_str,
1103 field_name: #field_str,
1104 prop_doc: #doc_str,
1105 };
1106 }
1107 })
1108 .collect();
1109 quote::quote! { #(#entries)* }
1110 } else {
1111 quote::quote! {}
1112 };
1113
1114 Ok(quote::quote! {
1115 #(#c_functions)*
1116
1117 #[doc = #source_location_doc]
1119 #[doc = concat!("Generated from source file `", file!(), "`.")]
1120 #[doc(hidden)]
1121 #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_R_WRAPPERS), linkme(crate = ::miniextendr_api::linkme))]
1122 static #const_name_wrappers: ::miniextendr_api::registry::RWrapperEntry =
1123 ::miniextendr_api::registry::RWrapperEntry {
1124 priority: ::miniextendr_api::registry::RWrapperPriority::Sidecar,
1125 source_file: file!(),
1126 content: #r_wrappers,
1127 };
1128
1129 #sidecar_prop_entries
1130 })
1131}
1132
1133fn generate_typed_external(input: &DeriveInput) -> TokenStream {
1144 let name = &input.ident;
1145 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1146
1147 let name_str = name.to_string();
1148 let name_lit = syn::LitStr::new(&name_str, name.span());
1149 let name_cstr = syn::LitByteStr::new(format!("{}\0", name_str).as_bytes(), name.span());
1150
1151 quote::quote! {
1160 impl #impl_generics ::miniextendr_api::externalptr::TypedExternal for #name #ty_generics #where_clause {
1161 const TYPE_NAME: &'static str = #name_lit;
1162 const TYPE_NAME_CSTR: &'static [u8] = #name_cstr;
1163 const TYPE_ID_CSTR: &'static [u8] =
1164 concat!(
1165 env!("CARGO_PKG_NAME"), "@", env!("CARGO_PKG_VERSION"),
1166 "::", module_path!(), "::", #name_lit, "\0"
1167 ).as_bytes();
1168 }
1169 }
1170}
1171
1172fn generate_into_external_ptr(input: &DeriveInput) -> TokenStream {
1177 let name = &input.ident;
1178 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
1179
1180 quote::quote! {
1181 impl #impl_generics ::miniextendr_api::externalptr::IntoExternalPtr for #name #ty_generics #where_clause {}
1182 }
1183}
1184
1185pub fn derive_external_ptr(input: DeriveInput) -> syn::Result<TokenStream> {
1196 let class_system = parse_externalptr_attrs(&input)?;
1198
1199 let sidecar_info = parse_sidecar_info(&input, class_system)?;
1201
1202 let typed_external = generate_typed_external(&input);
1203 let into_external_ptr = generate_into_external_ptr(&input);
1204 let sidecar_accessors = generate_sidecar_accessors(&input, &sidecar_info)?;
1205 let erased_wrapper = generate_erased_wrapper(&input);
1206
1207 Ok(quote::quote! {
1208 #typed_external
1209 #into_external_ptr
1210 #sidecar_accessors
1211 #erased_wrapper
1212 })
1213}
1214
1215fn generate_erased_wrapper(input: &DeriveInput) -> TokenStream {
1224 let type_ident = &input.ident;
1225
1226 if !input.generics.params.is_empty() {
1228 return quote::quote! {};
1229 }
1230
1231 let type_upper = type_ident.to_string().to_uppercase();
1232 let type_lower = type_ident.to_string().to_lowercase();
1233
1234 let wrapper_name = quote::format_ident!("__MxWrapper{}", type_ident);
1235 let base_vtable_name = quote::format_ident!("__MX_BASE_VTABLE_{}", type_upper);
1236 let concrete_tag_name = quote::format_ident!("__MX_TAG_{}", type_upper);
1237 let drop_fn_name = quote::format_ident!("__mx_drop_{}", type_lower);
1238 let wrap_fn_name = quote::format_ident!("__mx_wrap_{}", type_lower);
1239 let source_loc_doc = crate::source_location_doc(type_ident.span());
1240 let tag_path = format!("::{}", type_ident);
1241
1242 quote::quote! {
1243 #[doc = concat!(
1244 "Type-erased wrapper for `",
1245 stringify!(#type_ident),
1246 "` with trait dispatch support."
1247 )]
1248 #[doc = "Generated by `#[derive(ExternalPtr)]`."]
1249 #[doc = #source_loc_doc]
1250 #[doc = concat!("Generated from source file `", file!(), "`.")]
1251 #[repr(C)]
1252 #[doc(hidden)]
1253 struct #wrapper_name {
1254 pub erased: ::miniextendr_api::abi::mx_erased,
1255 pub data: #type_ident,
1256 }
1257
1258 #[doc(hidden)]
1259 const #concrete_tag_name: ::miniextendr_api::abi::mx_tag =
1260 ::miniextendr_api::abi::mx_tag_from_path(concat!(module_path!(), #tag_path));
1261
1262 #[doc(hidden)]
1263 unsafe extern "C" fn #drop_fn_name(ptr: *mut ::miniextendr_api::abi::mx_erased) {
1264 if ptr.is_null() {
1265 return;
1266 }
1267 let wrapper = ptr.cast::<#wrapper_name>();
1268 ::miniextendr_api::externalptr::drop_catching_panic(|| {
1271 unsafe { drop(Box::from_raw(wrapper)); }
1272 });
1273 }
1274
1275 #[doc(hidden)]
1276 static #base_vtable_name: ::miniextendr_api::abi::mx_base_vtable =
1277 ::miniextendr_api::abi::mx_base_vtable {
1278 drop: #drop_fn_name,
1279 concrete_tag: #concrete_tag_name,
1280 query: ::miniextendr_api::registry::universal_query,
1281 data_offset: ::std::mem::offset_of!(#wrapper_name, data),
1282 };
1283
1284 #[doc(hidden)]
1285 fn #wrap_fn_name(data: #type_ident) -> *mut ::miniextendr_api::abi::mx_erased {
1286 let wrapper = Box::new(#wrapper_name {
1287 erased: ::miniextendr_api::abi::mx_erased {
1288 base: &#base_vtable_name,
1289 },
1290 data,
1291 });
1292 Box::into_raw(wrapper).cast::<::miniextendr_api::abi::mx_erased>()
1293 }
1294 }
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299 use super::{ClassSystem, generate_r_wrapper_for_slot};
1300
1301 #[test]
1306 fn sidecar_accessors_do_not_pass_match_call() {
1307 let getter_c = "C__mx_rdata_get_T_f";
1308 let setter_c = "C__mx_rdata_set_T_f";
1309
1310 for cs in [
1311 ClassSystem::Env,
1312 ClassSystem::R6,
1313 ClassSystem::S3,
1314 ClassSystem::S4,
1315 ClassSystem::S7,
1316 ClassSystem::Vctrs,
1317 ] {
1318 let out = generate_r_wrapper_for_slot(cs, "T", "f", getter_c, setter_c);
1319 assert!(
1321 out.contains(&format!(".Call({getter_c}, x)")),
1322 "{cs:?} getter should call without .call:\n{out}"
1323 );
1324 assert!(
1325 out.contains(&format!(".Call({setter_c}, x, value)")),
1326 "{cs:?} setter should call without .call:\n{out}"
1327 );
1328 assert!(
1330 !out.contains(".call = match.call()"),
1331 "{cs:?} sidecar wrapper must not pass .call = match.call():\n{out}"
1332 );
1333 }
1334 }
1335}