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::ffi::altrep::R_altrep_class_t {
124                use ::std::sync::OnceLock;
125                static CLASS: OnceLock<::miniextendr_api::ffi::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::ffi::SEXP, Self::Error> {
147                Ok(self.into_sexp())
148            }
149
150            unsafe fn try_into_sexp_unchecked(self) -> ::core::result::Result<::miniextendr_api::ffi::SEXP, Self::Error> {
151                Ok(unsafe { self.into_sexp_unchecked() })
152            }
153
154            fn into_sexp(self) -> ::miniextendr_api::ffi::SEXP {
155                use ::miniextendr_api::altrep_registration::RegisterAltrep;
156                use ::miniextendr_api::externalptr::ExternalPtr;
157                use ::miniextendr_api::ffi::{SEXP, Rf_protect, Rf_unprotect};
158
159                let ext_ptr = ExternalPtr::new(self);
160                let cls = Self::get_or_init_class();
161                let data1 = ext_ptr.as_sexp();
162                unsafe {
163                    Rf_protect(data1);
164                    let altrep = cls.new_altrep(data1, SEXP::nil());
165                    Rf_unprotect(1);
166                    altrep
167                }
168            }
169
170            unsafe fn into_sexp_unchecked(self) -> ::miniextendr_api::ffi::SEXP {
171                use ::miniextendr_api::altrep_registration::RegisterAltrep;
172                use ::miniextendr_api::externalptr::ExternalPtr;
173                use ::miniextendr_api::ffi::{Rf_protect_unchecked, Rf_unprotect_unchecked};
174
175                let ext_ptr = ExternalPtr::new_unchecked(self);
176                let cls = Self::get_or_init_class();
177                let data1 = ext_ptr.as_sexp();
178                unsafe {
179                    Rf_protect_unchecked(data1);
180                    let altrep = cls.new_altrep_unchecked(
181                        data1,
182                        ::miniextendr_api::ffi::SEXP::nil(),
183                    );
184                    Rf_unprotect_unchecked(1);
185                    altrep
186                }
187            }
188        }
189
190        // Ref/Mut accessor types for receiving ALTREP back from R
191        #[doc = #ref_doc]
192        pub struct #ref_ident(::miniextendr_api::externalptr::ExternalPtr<#ident #ty_generics>);
193
194        impl ::miniextendr_api::TryFromSexp for #ref_ident {
195            type Error = ::miniextendr_api::SexpTypeError;
196
197            fn try_from_sexp(sexp: ::miniextendr_api::ffi::SEXP) -> ::core::result::Result<Self, Self::Error> {
198                use ::miniextendr_api::ffi::SEXPTYPE;
199
200                if !::miniextendr_api::ffi::SexpExt::is_altrep(&sexp) {
201                    return Err(::miniextendr_api::SexpTypeError {
202                        expected: SEXPTYPE::INTSXP,
203                        actual: ::miniextendr_api::ffi::SexpExt::type_of(&sexp),
204                    });
205                }
206
207                match unsafe { ::miniextendr_api::altrep_data1_as::<#ident #ty_generics>(sexp) } {
208                    Some(ptr) => Ok(#ref_ident(ptr)),
209                    None => Err(::miniextendr_api::SexpTypeError {
210                        expected: SEXPTYPE::EXTPTRSXP,
211                        actual: ::miniextendr_api::ffi::SexpExt::type_of(&sexp),
212                    }),
213                }
214            }
215        }
216
217        impl ::core::ops::Deref for #ref_ident {
218            type Target = #ident #ty_generics;
219
220            fn deref(&self) -> &Self::Target {
221                &*self.0
222            }
223        }
224
225        #[doc = #mut_doc]
226        pub struct #mut_ident(::miniextendr_api::externalptr::ExternalPtr<#ident #ty_generics>);
227
228        impl ::miniextendr_api::TryFromSexp for #mut_ident {
229            type Error = ::miniextendr_api::SexpTypeError;
230
231            fn try_from_sexp(sexp: ::miniextendr_api::ffi::SEXP) -> ::core::result::Result<Self, Self::Error> {
232                use ::miniextendr_api::ffi::SEXPTYPE;
233
234                if !::miniextendr_api::ffi::SexpExt::is_altrep(&sexp) {
235                    return Err(::miniextendr_api::SexpTypeError {
236                        expected: SEXPTYPE::INTSXP,
237                        actual: ::miniextendr_api::ffi::SexpExt::type_of(&sexp),
238                    });
239                }
240
241                match unsafe { ::miniextendr_api::altrep_data1_as::<#ident #ty_generics>(sexp) } {
242                    Some(ptr) => Ok(#mut_ident(ptr)),
243                    None => Err(::miniextendr_api::SexpTypeError {
244                        expected: SEXPTYPE::EXTPTRSXP,
245                        actual: ::miniextendr_api::ffi::SexpExt::type_of(&sexp),
246                    }),
247                }
248            }
249        }
250
251        impl ::core::ops::Deref for #mut_ident {
252            type Target = #ident #ty_generics;
253
254            fn deref(&self) -> &Self::Target {
255                &*self.0
256            }
257        }
258
259        impl ::core::ops::DerefMut for #mut_ident {
260            fn deref_mut(&mut self) -> &mut Self::Target {
261                &mut *self.0
262            }
263        }
264
265        #altrep_reg_entry
266    })
267}
268
269/// Entry point for `#[derive(Altrep)]`.
270///
271/// Generates ALTREP registration only (TypedExternal, AltrepClass,
272/// RegisterAltrep, IntoR, linkme entry, Ref/Mut accessor types).
273///
274/// The struct must already have low-level ALTREP traits implemented.
275/// For most use cases, prefer a family-specific derive instead:
276/// `#[derive(AltrepInteger)]`, `#[derive(AltrepReal)]`, etc.
277/// Those generate both the low-level traits AND registration.
278/// Use `#[altrep(manual)]` on a family derive to skip data trait generation
279/// when you provide your own `AltrepLen` + `Alt*Data` impls.
280///
281/// # Helper attributes
282///
283/// ```ignore
284/// #[altrep(class = "CustomName")]  // override ALTREP class name (default: struct name)
285/// ```
286pub fn derive_altrep(input: syn::DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
287    use syn::spanned::Spanned;
288
289    let ident = &input.ident;
290
291    if !matches!(input.data, syn::Data::Struct(_)) {
292        return Err(syn::Error::new(
293            input.span(),
294            "#[derive(Altrep)] can only be applied to structs",
295        ));
296    }
297
298    // Parse class name from #[altrep(class = "...")]
299    let mut class_name = None::<String>;
300
301    for attr in &input.attrs {
302        if !attr.path().is_ident("altrep") {
303            continue;
304        }
305        attr.parse_nested_meta(|meta| {
306            if meta.path.is_ident("class") {
307                let value: syn::LitStr = meta.value()?.parse()?;
308                class_name = Some(value.value());
309            } else {
310                return Err(meta.error("unknown #[altrep(...)] attribute; expected `class`"));
311            }
312            Ok(())
313        })?;
314    }
315
316    let class_name = class_name.unwrap_or_else(|| ident.to_string());
317
318    generate_direct_altrep_registration(ident, &input.generics, &class_name)
319}