Skip to main content

miniextendr_macros/
altrep.rs

1//! ALTREP registration code generation.
2//!
3//! This module generates the full ALTREP registration stack for data structs:
4//! `TypedExternal`, `AltrepClass`, `RegisterAltrep`, `IntoR`, linkme entry,
5//! and `Ref`/`Mut` accessor types.
6//!
7//! # Usage
8//!
9//! For types with field-based derives (auto-generates trait impls):
10//! ```ignore
11//! #[derive(AltrepInteger)]
12//! #[altrep(len = "len", elt = "value", class = "MyConstInt")]
13//! struct MyConstInt { value: i32, len: usize }
14//! ```
15//!
16//! For types with manual trait impls (lowlevel + registration, user writes data traits):
17//! ```ignore
18//! #[derive(AltrepInteger)]
19//! #[altrep(manual, class = "MyCustom", serialize)]
20//! struct MyCustomData { ... }
21//!
22//! impl AltrepLen for MyCustomData { ... }
23//! impl AltIntegerData for MyCustomData { ... }
24//! // Family derived from AltrepInteger — generates Altrep, AltVec, AltInteger, InferBase.
25//! ```
26
27/// Generates full ALTREP registration for a data struct.
28///
29/// Generates TypedExternal, AltrepClass, RegisterAltrep, IntoR, linkme entry, and Ref/Mut.
30/// The struct must already implement the low-level ALTREP traits (via `impl_alt*_from_data!`
31/// or `#[derive(AltrepInteger)]`) and `InferBase`.
32pub(crate) fn generate_direct_altrep_registration(
33    ident: &syn::Ident,
34    generics: &syn::Generics,
35    class_name: &str,
36) -> syn::Result<proc_macro2::TokenStream> {
37    let (_impl_generics, ty_generics, where_clause) = generics.split_for_impl();
38
39    // Class name as CStr literal
40    let class_cstr = syn::LitCStr::new(&std::ffi::CString::new(class_name).unwrap(), ident.span());
41
42    // TypedExternal constants — needed for ExternalPtr<T> to work
43    let type_name_str = class_name;
44    let type_name_bytes = format!("{}\0", type_name_str);
45    let type_name_byte_lit = syn::LitByteStr::new(type_name_bytes.as_bytes(), ident.span());
46
47    let ref_ident = quote::format_ident!("{}Ref", ident);
48    let mut_ident = quote::format_ident!("{}Mut", ident);
49
50    let into_r_doc = format!(
51        "Convert [`{}`] to an R ALTREP SEXP.\n\nIn debug builds, asserts that we're on R's main thread.",
52        ident
53    );
54    let ref_doc = format!(
55        "Immutable reference wrapper for [`{}`] ALTREP data. Implements `TryFromSexp` and `Deref<Target = {}>`.",
56        ident, ident
57    );
58    let mut_doc = format!(
59        "Mutable reference wrapper for [`{}`] ALTREP data. Implements `TryFromSexp`, `Deref`, and `DerefMut`.",
60        ident
61    );
62
63    // For non-generic types, emit a registration fn + a distributed_slice
64    // entry pairing the fn pointer with its `#[no_mangle]` symbol name.
65    //
66    // The function is `pub extern "C"` with `#[unsafe(no_mangle)]` so that a separate
67    // compilation unit (the WASM snapshot codegen path) can reference it by name via
68    // an `extern { fn __mx_altrep_reg_<crate>_<Ident>(); }` declaration (crate-prefixed
69    // for webR cross-package symbol uniqueness — #1273). The entry static
70    // carries the symbol string for the host-time snapshot writer (so it doesn't have
71    // to recover the name from a fn pointer).
72    //
73    // The ident is used verbatim (no `.to_lowercase()`) to avoid case-collision footguns:
74    // `MyType` vs `MYType` would both produce the same symbol name if lowercased.
75    let altrep_reg_entry = if generics.params.is_empty() {
76        let reg_fn_name = crate::naming::altrep_reg_fn_ident(ident);
77        let entry_ident = quote::format_ident!("__MX_ALTREP_REG_ENTRY_{}", ident);
78        quote::quote! {
79            #[doc(hidden)]
80            #[unsafe(no_mangle)]
81            pub extern "C" fn #reg_fn_name() {
82                <#ident as ::miniextendr_api::altrep_registration::RegisterAltrep>::get_or_init_class();
83            }
84
85            #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_ALTREP_REGISTRATIONS), linkme(crate = ::miniextendr_api::linkme))]
86            #[doc(hidden)]
87            #[allow(non_upper_case_globals)]
88            static #entry_ident: ::miniextendr_api::registry::AltrepRegistration =
89                ::miniextendr_api::registry::AltrepRegistration {
90                    register: #reg_fn_name,
91                    symbol: stringify!(#reg_fn_name),
92                };
93        }
94    } else {
95        quote::quote! {}
96    };
97
98    let source_loc_doc = crate::source_location_doc(ident.span());
99
100    Ok(quote::quote! {
101        // TypedExternal — enables ExternalPtr<T> storage.
102        // NOTE: We intentionally do NOT implement IntoExternalPtr, because ALTREP types
103        // have their own IntoR impl that creates an ALTREP SEXP (not a plain ExternalPtr).
104        impl ::miniextendr_api::externalptr::TypedExternal for #ident #ty_generics #where_clause {
105            const TYPE_NAME: &'static str = #type_name_str;
106            const TYPE_NAME_CSTR: &'static [u8] = #type_name_byte_lit;
107            const TYPE_ID_CSTR: &'static [u8] =
108                concat!(module_path!(), "::", stringify!(#ident), "\0").as_bytes();
109        }
110
111        // AltrepClass — class name and base type
112        #[doc = concat!("ALTREP class descriptor for [`", stringify!(#ident), "`].")]
113        #[doc = #source_loc_doc]
114        impl ::miniextendr_api::altrep::AltrepClass for #ident #ty_generics #where_clause {
115            const CLASS_NAME: &'static ::core::ffi::CStr = #class_cstr;
116            const BASE: ::miniextendr_api::altrep::RBase =
117                <#ident #ty_generics as ::miniextendr_api::altrep_data::InferBase>::BASE;
118        }
119
120        // RegisterAltrep — OnceLock class registration via InferBase
121        #[doc = concat!("Registration entry point for [`", stringify!(#ident), "`] ALTREP class.")]
122        #[doc = #source_loc_doc]
123        impl ::miniextendr_api::altrep_registration::RegisterAltrep for #ident #ty_generics #where_clause {
124            fn get_or_init_class() -> ::miniextendr_api::sys::altrep::R_altrep_class_t {
125                use ::std::sync::OnceLock;
126                static CLASS: OnceLock<::miniextendr_api::sys::altrep::R_altrep_class_t> = OnceLock::new();
127                *CLASS.get_or_init(move || {
128                    let cls = unsafe {
129                        <#ident as ::miniextendr_api::altrep_data::InferBase>::make_class(
130                            <#ident as ::miniextendr_api::altrep::AltrepClass>::CLASS_NAME.as_ptr(),
131                            ::miniextendr_api::AltrepPkgName::as_ptr(),
132                        )
133                    };
134                    unsafe {
135                        <#ident as ::miniextendr_api::altrep_data::InferBase>::install_methods(cls);
136                    }
137                    cls
138                })
139            }
140        }
141
142        // IntoR — convert to R ALTREP SEXP (wraps self in ExternalPtr)
143        #[doc = #into_r_doc]
144        impl ::miniextendr_api::IntoR for #ident #ty_generics #where_clause {
145            type Error = ::core::convert::Infallible;
146
147            fn try_into_sexp(self) -> ::core::result::Result<::miniextendr_api::SEXP, Self::Error> {
148                Ok(self.into_sexp())
149            }
150
151            unsafe fn try_into_sexp_unchecked(self) -> ::core::result::Result<::miniextendr_api::SEXP, Self::Error> {
152                Ok(unsafe { self.into_sexp_unchecked() })
153            }
154
155            fn into_sexp(self) -> ::miniextendr_api::SEXP {
156                use ::miniextendr_api::altrep_registration::RegisterAltrep;
157                use ::miniextendr_api::externalptr::ExternalPtr;
158                use ::miniextendr_api::SEXP;
159                use ::miniextendr_api::sys::{Rf_protect, Rf_unprotect};
160
161                let ext_ptr = ExternalPtr::new(self);
162                let cls = Self::get_or_init_class();
163                let data1 = ext_ptr.as_sexp();
164                unsafe {
165                    Rf_protect(data1);
166                    let altrep = cls.new_altrep(data1, SEXP::nil());
167                    Rf_unprotect(1);
168                    altrep
169                }
170            }
171
172            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::SEXP {
173                use ::miniextendr_api::altrep_registration::RegisterAltrep;
174                use ::miniextendr_api::externalptr::ExternalPtr;
175                use ::miniextendr_api::sys::{Rf_protect_unchecked, Rf_unprotect_unchecked};
176
177                let ext_ptr = ExternalPtr::new_unchecked(self);
178                let cls = Self::get_or_init_class();
179                let data1 = ext_ptr.as_sexp();
180                unsafe {
181                    Rf_protect_unchecked(data1);
182                    let altrep = cls.new_altrep_unchecked(
183                        data1,
184                        ::miniextendr_api::SEXP::nil(),
185                    );
186                    Rf_unprotect_unchecked(1);
187                    altrep
188                }
189            }
190        }
191
192        // Ref/Mut accessor types for receiving ALTREP back from R
193        #[doc = #ref_doc]
194        pub struct #ref_ident(::miniextendr_api::externalptr::ExternalPtr<#ident #ty_generics>);
195
196        impl ::miniextendr_api::TryFromSexp for #ref_ident {
197            type Error = ::miniextendr_api::SexpTypeError;
198
199            fn try_from_sexp(sexp: ::miniextendr_api::SEXP) -> ::core::result::Result<Self, Self::Error> {
200                use ::miniextendr_api::SEXPTYPE;
201
202                if !::miniextendr_api::SexpExt::is_altrep(&sexp) {
203                    return Err(::miniextendr_api::SexpTypeError {
204                        expected: <#ident #ty_generics as ::miniextendr_api::altrep::AltrepClass>::BASE.sexptype(),
205                        actual: ::miniextendr_api::SexpExt::type_of(&sexp),
206                    });
207                }
208
209                match unsafe { ::miniextendr_api::altrep_data1_as::<#ident #ty_generics>(sexp) } {
210                    Some(ptr) => Ok(#ref_ident(ptr)),
211                    None => Err(::miniextendr_api::SexpTypeError {
212                        expected: SEXPTYPE::EXTPTRSXP,
213                        actual: ::miniextendr_api::SexpExt::type_of(&sexp),
214                    }),
215                }
216            }
217        }
218
219        impl ::core::ops::Deref for #ref_ident {
220            type Target = #ident #ty_generics;
221
222            fn deref(&self) -> &Self::Target {
223                &*self.0
224            }
225        }
226
227        #[doc = #mut_doc]
228        pub struct #mut_ident(::miniextendr_api::externalptr::ExternalPtr<#ident #ty_generics>);
229
230        impl ::miniextendr_api::TryFromSexp for #mut_ident {
231            type Error = ::miniextendr_api::SexpTypeError;
232
233            fn try_from_sexp(sexp: ::miniextendr_api::SEXP) -> ::core::result::Result<Self, Self::Error> {
234                use ::miniextendr_api::SEXPTYPE;
235
236                if !::miniextendr_api::SexpExt::is_altrep(&sexp) {
237                    return Err(::miniextendr_api::SexpTypeError {
238                        expected: <#ident #ty_generics as ::miniextendr_api::altrep::AltrepClass>::BASE.sexptype(),
239                        actual: ::miniextendr_api::SexpExt::type_of(&sexp),
240                    });
241                }
242
243                match unsafe { ::miniextendr_api::altrep_data1_as::<#ident #ty_generics>(sexp) } {
244                    Some(ptr) => Ok(#mut_ident(ptr)),
245                    None => Err(::miniextendr_api::SexpTypeError {
246                        expected: SEXPTYPE::EXTPTRSXP,
247                        actual: ::miniextendr_api::SexpExt::type_of(&sexp),
248                    }),
249                }
250            }
251        }
252
253        impl ::core::ops::Deref for #mut_ident {
254            type Target = #ident #ty_generics;
255
256            fn deref(&self) -> &Self::Target {
257                &*self.0
258            }
259        }
260
261        impl ::core::ops::DerefMut for #mut_ident {
262            fn deref_mut(&mut self) -> &mut Self::Target {
263                &mut *self.0
264            }
265        }
266
267        #altrep_reg_entry
268    })
269}
270
271/// Entry point for `#[derive(Altrep)]`.
272///
273/// Generates ALTREP registration only (TypedExternal, AltrepClass,
274/// RegisterAltrep, IntoR, linkme entry, Ref/Mut accessor types).
275///
276/// The struct must already have low-level ALTREP traits implemented.
277/// For most use cases, prefer a family-specific derive instead:
278/// `#[derive(AltrepInteger)]`, `#[derive(AltrepReal)]`, etc.
279/// Those generate both the low-level traits AND registration.
280/// Use `#[altrep(manual)]` on a family derive to skip data trait generation
281/// when you provide your own `AltrepLen` + `Alt*Data` impls.
282///
283/// # Helper attributes
284///
285/// ```ignore
286/// #[altrep(class = "CustomName")]  // override ALTREP class name (default: struct name)
287/// ```
288pub fn derive_altrep(input: syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
289    use syn::spanned::Spanned;
290
291    let ident = &input.ident;
292
293    if !matches!(input.data, syn::Data::Struct(_)) {
294        return Err(syn::Error::new(
295            input.span(),
296            "#[derive(Altrep)] can only be applied to structs",
297        ));
298    }
299
300    // Parse class name from #[altrep(class = "...")]
301    let mut class_name = None::<String>;
302
303    for attr in &input.attrs {
304        if !attr.path().is_ident("altrep") {
305            continue;
306        }
307        attr.parse_nested_meta(|meta| {
308            if meta.path.is_ident("class") {
309                let value: syn::LitStr = meta.value()?.parse()?;
310                class_name = Some(value.value());
311            } else {
312                return Err(meta.error("unknown #[altrep(...)] attribute; expected `class`"));
313            }
314            Ok(())
315        })?;
316    }
317
318    let class_name = class_name.unwrap_or_else(|| ident.to_string());
319
320    generate_direct_altrep_registration(ident, &input.generics, &class_name)
321}