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_<Ident>(); }` declaration. The entry static
69    // carries the symbol string for the host-time snapshot writer (so it doesn't have
70    // to recover the name from a fn pointer).
71    //
72    // The ident is used verbatim (no `.to_lowercase()`) to avoid case-collision footguns:
73    // `MyType` vs `MYType` would both produce the same symbol name if lowercased.
74    let altrep_reg_entry = if generics.params.is_empty() {
75        let reg_fn_name = quote::format_ident!("__mx_altrep_reg_{}", ident);
76        let entry_ident = quote::format_ident!("__MX_ALTREP_REG_ENTRY_{}", ident);
77        quote::quote! {
78            #[doc(hidden)]
79            #[unsafe(no_mangle)]
80            pub extern "C" fn #reg_fn_name() {
81                <#ident as ::miniextendr_api::altrep_registration::RegisterAltrep>::get_or_init_class();
82            }
83
84            #[cfg_attr(not(target_arch = "wasm32"), ::miniextendr_api::linkme::distributed_slice(::miniextendr_api::registry::MX_ALTREP_REGISTRATIONS), linkme(crate = ::miniextendr_api::linkme))]
85            #[doc(hidden)]
86            #[allow(non_upper_case_globals)]
87            static #entry_ident: ::miniextendr_api::registry::AltrepRegistration =
88                ::miniextendr_api::registry::AltrepRegistration {
89                    register: #reg_fn_name,
90                    symbol: stringify!(#reg_fn_name),
91                };
92        }
93    } else {
94        quote::quote! {}
95    };
96
97    let source_loc_doc = crate::source_location_doc(ident.span());
98
99    Ok(quote::quote! {
100        // TypedExternal — enables ExternalPtr<T> storage.
101        // NOTE: We intentionally do NOT implement IntoExternalPtr, because ALTREP types
102        // have their own IntoR impl that creates an ALTREP SEXP (not a plain ExternalPtr).
103        impl ::miniextendr_api::externalptr::TypedExternal for #ident #ty_generics #where_clause {
104            const TYPE_NAME: &'static str = #type_name_str;
105            const TYPE_NAME_CSTR: &'static [u8] = #type_name_byte_lit;
106            const TYPE_ID_CSTR: &'static [u8] =
107                concat!(module_path!(), "::", stringify!(#ident), "\0").as_bytes();
108        }
109
110        // AltrepClass — class name and base type
111        #[doc = concat!("ALTREP class descriptor for [`", stringify!(#ident), "`].")]
112        #[doc = #source_loc_doc]
113        impl ::miniextendr_api::altrep::AltrepClass for #ident #ty_generics #where_clause {
114            const CLASS_NAME: &'static ::core::ffi::CStr = #class_cstr;
115            const BASE: ::miniextendr_api::altrep::RBase =
116                <#ident #ty_generics as ::miniextendr_api::altrep_data::InferBase>::BASE;
117        }
118
119        // RegisterAltrep — OnceLock class registration via InferBase
120        #[doc = concat!("Registration entry point for [`", stringify!(#ident), "`] ALTREP class.")]
121        #[doc = #source_loc_doc]
122        impl ::miniextendr_api::altrep_registration::RegisterAltrep for #ident #ty_generics #where_clause {
123            fn get_or_init_class() -> ::miniextendr_api::sys::altrep::R_altrep_class_t {
124                use ::std::sync::OnceLock;
125                static CLASS: OnceLock<::miniextendr_api::sys::altrep::R_altrep_class_t> = OnceLock::new();
126                *CLASS.get_or_init(move || {
127                    let cls = unsafe {
128                        <#ident as ::miniextendr_api::altrep_data::InferBase>::make_class(
129                            <#ident as ::miniextendr_api::altrep::AltrepClass>::CLASS_NAME.as_ptr(),
130                            ::miniextendr_api::AltrepPkgName::as_ptr(),
131                        )
132                    };
133                    unsafe {
134                        <#ident as ::miniextendr_api::altrep_data::InferBase>::install_methods(cls);
135                    }
136                    cls
137                })
138            }
139        }
140
141        // IntoR — convert to R ALTREP SEXP (wraps self in ExternalPtr)
142        #[doc = #into_r_doc]
143        impl ::miniextendr_api::IntoR for #ident #ty_generics #where_clause {
144            type Error = ::core::convert::Infallible;
145
146            fn try_into_sexp(self) -> ::core::result::Result<::miniextendr_api::SEXP, Self::Error> {
147                Ok(self.into_sexp())
148            }
149
150            unsafe fn try_into_sexp_unchecked(self) -> ::core::result::Result<::miniextendr_api::SEXP, Self::Error> {
151                Ok(unsafe { self.into_sexp_unchecked() })
152            }
153
154            fn into_sexp(self) -> ::miniextendr_api::SEXP {
155                use ::miniextendr_api::altrep_registration::RegisterAltrep;
156                use ::miniextendr_api::externalptr::ExternalPtr;
157                use ::miniextendr_api::SEXP;
158                use ::miniextendr_api::sys::{Rf_protect, Rf_unprotect};
159
160                let ext_ptr = ExternalPtr::new(self);
161                let cls = Self::get_or_init_class();
162                let data1 = ext_ptr.as_sexp();
163                unsafe {
164                    Rf_protect(data1);
165                    let altrep = cls.new_altrep(data1, SEXP::nil());
166                    Rf_unprotect(1);
167                    altrep
168                }
169            }
170
171            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::SEXP {
172                use ::miniextendr_api::altrep_registration::RegisterAltrep;
173                use ::miniextendr_api::externalptr::ExternalPtr;
174                use ::miniextendr_api::sys::{Rf_protect_unchecked, Rf_unprotect_unchecked};
175
176                let ext_ptr = ExternalPtr::new_unchecked(self);
177                let cls = Self::get_or_init_class();
178                let data1 = ext_ptr.as_sexp();
179                unsafe {
180                    Rf_protect_unchecked(data1);
181                    let altrep = cls.new_altrep_unchecked(
182                        data1,
183                        ::miniextendr_api::SEXP::nil(),
184                    );
185                    Rf_unprotect_unchecked(1);
186                    altrep
187                }
188            }
189        }
190
191        // Ref/Mut accessor types for receiving ALTREP back from R
192        #[doc = #ref_doc]
193        pub struct #ref_ident(::miniextendr_api::externalptr::ExternalPtr<#ident #ty_generics>);
194
195        impl ::miniextendr_api::TryFromSexp for #ref_ident {
196            type Error = ::miniextendr_api::SexpTypeError;
197
198            fn try_from_sexp(sexp: ::miniextendr_api::SEXP) -> ::core::result::Result<Self, Self::Error> {
199                use ::miniextendr_api::SEXPTYPE;
200
201                if !::miniextendr_api::SexpExt::is_altrep(&sexp) {
202                    return Err(::miniextendr_api::SexpTypeError {
203                        expected: <#ident #ty_generics as ::miniextendr_api::altrep::AltrepClass>::BASE.sexptype(),
204                        actual: ::miniextendr_api::SexpExt::type_of(&sexp),
205                    });
206                }
207
208                match unsafe { ::miniextendr_api::altrep_data1_as::<#ident #ty_generics>(sexp) } {
209                    Some(ptr) => Ok(#ref_ident(ptr)),
210                    None => Err(::miniextendr_api::SexpTypeError {
211                        expected: SEXPTYPE::EXTPTRSXP,
212                        actual: ::miniextendr_api::SexpExt::type_of(&sexp),
213                    }),
214                }
215            }
216        }
217
218        impl ::core::ops::Deref for #ref_ident {
219            type Target = #ident #ty_generics;
220
221            fn deref(&self) -> &Self::Target {
222                &*self.0
223            }
224        }
225
226        #[doc = #mut_doc]
227        pub struct #mut_ident(::miniextendr_api::externalptr::ExternalPtr<#ident #ty_generics>);
228
229        impl ::miniextendr_api::TryFromSexp for #mut_ident {
230            type Error = ::miniextendr_api::SexpTypeError;
231
232            fn try_from_sexp(sexp: ::miniextendr_api::SEXP) -> ::core::result::Result<Self, Self::Error> {
233                use ::miniextendr_api::SEXPTYPE;
234
235                if !::miniextendr_api::SexpExt::is_altrep(&sexp) {
236                    return Err(::miniextendr_api::SexpTypeError {
237                        expected: <#ident #ty_generics as ::miniextendr_api::altrep::AltrepClass>::BASE.sexptype(),
238                        actual: ::miniextendr_api::SexpExt::type_of(&sexp),
239                    });
240                }
241
242                match unsafe { ::miniextendr_api::altrep_data1_as::<#ident #ty_generics>(sexp) } {
243                    Some(ptr) => Ok(#mut_ident(ptr)),
244                    None => Err(::miniextendr_api::SexpTypeError {
245                        expected: SEXPTYPE::EXTPTRSXP,
246                        actual: ::miniextendr_api::SexpExt::type_of(&sexp),
247                    }),
248                }
249            }
250        }
251
252        impl ::core::ops::Deref for #mut_ident {
253            type Target = #ident #ty_generics;
254
255            fn deref(&self) -> &Self::Target {
256                &*self.0
257            }
258        }
259
260        impl ::core::ops::DerefMut for #mut_ident {
261            fn deref_mut(&mut self) -> &mut Self::Target {
262                &mut *self.0
263            }
264        }
265
266        #altrep_reg_entry
267    })
268}
269
270/// Entry point for `#[derive(Altrep)]`.
271///
272/// Generates ALTREP registration only (TypedExternal, AltrepClass,
273/// RegisterAltrep, IntoR, linkme entry, Ref/Mut accessor types).
274///
275/// The struct must already have low-level ALTREP traits implemented.
276/// For most use cases, prefer a family-specific derive instead:
277/// `#[derive(AltrepInteger)]`, `#[derive(AltrepReal)]`, etc.
278/// Those generate both the low-level traits AND registration.
279/// Use `#[altrep(manual)]` on a family derive to skip data trait generation
280/// when you provide your own `AltrepLen` + `Alt*Data` impls.
281///
282/// # Helper attributes
283///
284/// ```ignore
285/// #[altrep(class = "CustomName")]  // override ALTREP class name (default: struct name)
286/// ```
287pub fn derive_altrep(input: syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
288    use syn::spanned::Spanned;
289
290    let ident = &input.ident;
291
292    if !matches!(input.data, syn::Data::Struct(_)) {
293        return Err(syn::Error::new(
294            input.span(),
295            "#[derive(Altrep)] can only be applied to structs",
296        ));
297    }
298
299    // Parse class name from #[altrep(class = "...")]
300    let mut class_name = None::<String>;
301
302    for attr in &input.attrs {
303        if !attr.path().is_ident("altrep") {
304            continue;
305        }
306        attr.parse_nested_meta(|meta| {
307            if meta.path.is_ident("class") {
308                let value: syn::LitStr = meta.value()?.parse()?;
309                class_name = Some(value.value());
310            } else {
311                return Err(meta.error("unknown #[altrep(...)] attribute; expected `class`"));
312            }
313            Ok(())
314        })?;
315    }
316
317    let class_name = class_name.unwrap_or_else(|| ident.to_string());
318
319    generate_direct_altrep_registration(ident, &input.generics, &class_name)
320}