// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: MIT OR Apache-2.0 // cSpell: ignore asyncness constness containee defaultness impls qself supertraits vref /*! Implementation detail for the vtable crate */ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; use syn::parse::Parser; use syn::spanned::Spanned; use syn::*; /// Returns true if the type `ty` is "Container" fn match_generic_type(ty: &Type, container: &str, containee: &Ident) -> bool { if let Type::Path(pat) = ty { if let Some(seg) = pat.path.segments.last() { if seg.ident != container { return false; } if let PathArguments::AngleBracketed(args) = &seg.arguments { if let Some(GenericArgument::Type(Type::Path(arg))) = args.args.last() { return Some(containee) == arg.path.get_ident(); } } } } false } /// Returns Some(type) if the type is `Pin` fn is_pin(ty: &Type) -> Option<&Type> { if let Type::Path(pat) = ty { if let Some(seg) = pat.path.segments.last() { if seg.ident != "Pin" { return None; } if let PathArguments::AngleBracketed(args) = &seg.arguments { if let Some(GenericArgument::Type(t)) = args.args.last() { return Some(t); } } } } None } /** This macro needs to be applied to a VTable structure The design choice is that it is applied to a VTable and not to a trait so that cbindgen can see the actual vtable struct. This macro needs to be applied to a struct whose name ends with "VTable", and which contains members which are function pointers. For example, if it is applied to `struct MyTraitVTable`, it will create: - The `MyTrait` trait with all the functions. - The `MyTraitConsts` trait for the associated constants, if any - `MyTraitVTable_static!` macro. It will also implement the `VTableMeta` and `VTableMetaDrop` traits so that VRef and so on can work, allowing to access methods from the trait directly from VRef. This macro does the following transformation: For function type fields: - `unsafe` is added to the signature, since it is unsafe to call these functions directly from the vtable without having a valid pointer to the actual object. But if the original function was marked unsafe, the unsafety is forwarded to the trait. - If a field is called `drop`, then it is understood that this is the destructor for a VBox. It must have the type `fn(VRefMut)` - If two fields called `drop_in_place` and `dealloc` are present, then they are understood to be in-place destructors and deallocation functions. `drop_in_place` must have the signature `fn(VRefMut -> Layout`, and `dealloc` must have the signature `fn(&MyVTable, ptr: *mut u8, layout: Layout)`. `drop_in_place` is responsible for destructing the object and returning the memory layout that was used for the initial allocation. It will be passed to `dealloc`, which is responsible for releasing the memory. These two functions are used to enable the use of `VRc` and `VWeak`. - If the first argument of the function is `VRef` or `VRefMut`, then it is understood as a `&self` or `&mut self` argument in the trait. - Similarly, if it is a `Pin>` or `Pin>`, self is mapped to `Pin<&Self>` or `Pin<&mut Self>` For the other fields: - They are considered associated constants of the MyTraitConsts trait. - If they are annotated with the `#[field_offset(FieldType)]` attribute, the type of the field must be `usize`, and the associated const in the trait will be of type `FieldOffset`, and an accessor to the field reference and reference mut will be added to the Target of VRef and VRefMut. The VRef/VRefMut/VBox structure will dereference to a type which has the following associated items: - The functions from the vtable that have a VRef or VRefMut first parameter for self. - For each `#[field_offset]` attributes, a corresponding getter returns a reference to that field, and mutable accessor that ends with `_mut` returns a mutable reference. - `as_ptr` returns a `*mut u8` - `get_vtable` Return a reference to the VTable so one can access the associated consts. The VTable struct gets a `new` associated function that creates a vtable for any type that implements the generated traits. ## Example ``` use vtable::*; // we are going to declare a VTable structure for an Animal trait #[vtable] #[repr(C)] struct AnimalVTable { /// Pointer to a function that make noise. /// `unsafe` will automatically be added make_noise: fn(VRef, i32) -> i32, /// if there is a 'drop' member, it is considered as the destructor drop: fn(VRefMut), /// Associated constant. LEG_NUMBER: i8, /// There exist a `bool` field in the structure and this is an offset #[field_offset(bool)] IS_HUNGRY: usize, } #[repr(C)] struct Dog{ strength: i32, is_hungry: bool }; // The #[vtable] macro created the Animal Trait impl Animal for Dog { fn make_noise(&self, intensity: i32) -> i32 { println!("Wof!"); return self.strength * intensity; } } // The #[vtable] macro created the AnimalConsts Trait impl AnimalConsts for Dog { const LEG_NUMBER: i8 = 4; const IS_HUNGRY: vtable::FieldOffset = unsafe { vtable::FieldOffset::new_from_offset(4) }; } // The #[vtable] macro also exposed a macro to create a vtable AnimalVTable_static!(static DOG_VT for Dog); // with that, it is possible to instantiate a vtable::VRefMut let mut dog = Dog { strength: 100, is_hungry: false }; { let mut animal_vref = VRefMut::::new(&mut dog); // access to the vtable through the get_vtable() function assert_eq!(animal_vref.get_vtable().LEG_NUMBER, 4); // functions are also added for the #[field_offset] member assert_eq!(*animal_vref.IS_HUNGRY(), false); *animal_vref.IS_HUNGRY_mut() = true; } assert_eq!(dog.is_hungry, true); ``` */ #[proc_macro_attribute] pub fn vtable(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut input = parse_macro_input!(item as ItemStruct); let fields = if let Fields::Named(fields) = &mut input.fields { fields } else { return Error::new( proc_macro2::Span::call_site(), "Only supported for structure with named fields", ) .to_compile_error() .into(); }; let vtable_name = input.ident.to_string(); if !vtable_name.ends_with("VTable") { return Error::new(input.ident.span(), "The structure does not ends in 'VTable'") .to_compile_error() .into(); } let trait_name = Ident::new(&vtable_name[..vtable_name.len() - 6], input.ident.span()); let to_name = quote::format_ident!("{}TO", trait_name); let module_name = quote::format_ident!("{}_vtable_mod", trait_name); let static_vtable_macro_name = quote::format_ident!("{}_static", vtable_name); let vtable_name = input.ident.clone(); let mut drop_impls = vec![]; let mut generated_trait = ItemTrait { attrs: input .attrs .iter() .filter(|a| a.path().get_ident().as_ref().map(|i| *i == "doc").unwrap_or(false)) .cloned() .collect(), vis: Visibility::Public(Default::default()), unsafety: None, auto_token: None, trait_token: Default::default(), ident: trait_name.clone(), generics: Generics::default(), colon_token: None, supertraits: Default::default(), brace_token: Default::default(), items: Default::default(), restriction: Default::default(), }; let additional_doc = format!("\nNote: Was generated from the [`#[vtable]`](vtable) macro on [`{vtable_name}`]"); generated_trait .attrs .append(&mut Attribute::parse_outer.parse2(quote!(#[doc = #additional_doc])).unwrap()); let mut generated_trait_assoc_const = None; let mut generated_to_fn_trait = vec![]; let mut generated_type_assoc_fn = vec![]; let mut vtable_ctor = vec![]; for field in &mut fields.named { // The vtable can only be accessed in unsafe code, so it is ok if all its fields are Public field.vis = Visibility::Public(Default::default()); let ident = field.ident.as_ref().unwrap(); let mut some = None; let func_ty = if let Type::BareFn(f) = &mut field.ty { Some(f) } else if let Type::Path(pat) = &mut field.ty { pat.path.segments.last_mut().and_then(|seg| { if seg.ident == "Option" { some = Some(quote!(Some)); if let PathArguments::AngleBracketed(args) = &mut seg.arguments { if let Some(GenericArgument::Type(Type::BareFn(f))) = args.args.first_mut() { Some(f) } else { None } } else { None } } else { None } }) } else { None }; if let Some(f) = func_ty { let mut sig = Signature { constness: None, asyncness: None, unsafety: f.unsafety, abi: None, fn_token: f.fn_token, ident: ident.clone(), generics: Default::default(), paren_token: f.paren_token, inputs: Default::default(), variadic: None, output: f.output.clone(), }; let mut sig_extern = sig.clone(); sig_extern.generics = parse_str(&format!("")).unwrap(); // check parameters let mut call_code = None; let mut self_call = None; let mut forward_code = None; let mut has_self = false; for param in &f.inputs { let arg_name = quote::format_ident!("_{}", sig_extern.inputs.len()); let typed_arg = FnArg::Typed(PatType { attrs: param.attrs.clone(), pat: Box::new(Pat::Path(syn::PatPath { attrs: Default::default(), qself: None, path: arg_name.clone().into(), })), colon_token: Default::default(), ty: Box::new(param.ty.clone()), }); sig_extern.inputs.push(typed_arg.clone()); // check for the vtable if let Type::Ptr(TypePtr { mutability, elem, .. }) | Type::Reference(TypeReference { mutability, elem, .. }) = ¶m.ty { if let Type::Path(p) = &**elem { if let Some(pointer_to) = p.path.get_ident() { if pointer_to == &vtable_name { if mutability.is_some() { return Error::new(p.span(), "VTable cannot be mutable") .to_compile_error() .into(); } if call_code.is_some() || !sig.inputs.is_empty() { return Error::new( p.span(), "VTable pointer need to be the first", ) .to_compile_error() .into(); } call_code = Some(quote!(vtable as _,)); continue; } } } } let (is_pin, self_ty) = match is_pin(¶m.ty) { Some(t) => (true, t), None => (false, ¶m.ty), }; // check for self if let (true, mutability) = if match_generic_type(self_ty, "VRef", &vtable_name) { (true, None) } else if match_generic_type(self_ty, "VRefMut", &vtable_name) { (true, Some(Default::default())) } else { (false, None) } { if !sig.inputs.is_empty() { return Error::new(param.span(), "Self pointer need to be the first") .to_compile_error() .into(); } let const_or_mut = mutability.map_or_else(|| quote!(const), |x| quote!(#x)); has_self = true; if !is_pin { sig.inputs.push(FnArg::Receiver(Receiver { attrs: param.attrs.clone(), reference: Some(Default::default()), mutability, self_token: Default::default(), colon_token: None, ty: Box::new(parse_quote!(& #mutability Self)), })); call_code = Some(quote!(#call_code <#self_ty>::from_raw(self.vtable, self.ptr),)); self_call = Some(quote!(&#mutability (*(#arg_name.as_ptr() as *#const_or_mut T)),)); } else { // Pinned sig.inputs.push(FnArg::Typed(PatType { attrs: param.attrs.clone(), pat: Box::new(Pat::parse_single.parse2(quote!(self)).unwrap()), colon_token: Default::default(), ty: parse_quote!(core::pin::Pin<& #mutability Self>), })); call_code = Some( quote!(#call_code ::core::pin::Pin::new_unchecked(<#self_ty>::from_raw(self.vtable, self.ptr)),), ); self_call = Some( quote!(::core::pin::Pin::new_unchecked(&#mutability (*(#arg_name.as_ptr() as *#const_or_mut T))),), ); } continue; } sig.inputs.push(typed_arg); call_code = Some(quote!(#call_code #arg_name,)); forward_code = Some(quote!(#forward_code #arg_name,)); } // Add unsafe: The function are not safe to call unless the self parameter is of the correct type f.unsafety = Some(Default::default()); sig_extern.abi.clone_from(&f.abi); let mut wrap_trait_call = None; if !has_self { sig.generics = Generics { where_clause: Some(parse_str("where Self : Sized").unwrap()), ..Default::default() }; // Check if this is a constructor functions if let ReturnType::Type(_, ret) = &f.output { if match_generic_type(ret, "VBox", &vtable_name) { // Change VBox to Self sig.output = parse_str("-> Self").unwrap(); wrap_trait_call = Some(quote! { let wrap_trait_call = |x| unsafe { // Put the object on the heap and get a pointer to it let ptr = ::core::ptr::NonNull::from(Box::leak(Box::new(x))); VBox::<#vtable_name>::from_raw(vtable, ptr.cast()) }; wrap_trait_call }); } } } if ident == "drop" { vtable_ctor.push(quote!(#ident: { #sig_extern { unsafe { ::core::mem::drop(Box::from_raw((#self_call).0 as *mut _)); } } #ident:: },)); drop_impls.push(quote! { unsafe impl VTableMetaDrop for #vtable_name { unsafe fn drop(ptr: *mut #to_name) { // Safety: The vtable is valid and inner is a type corresponding to the vtable, // which was allocated such that drop is expected. unsafe { let (vtable, ptr) = ((*ptr).vtable, (*ptr).ptr); (vtable.as_ref().#ident)(VRefMut::from_raw(vtable, ptr)) } } fn new_box>(value: X) -> VBox<#vtable_name> { // Put the object on the heap and get a pointer to it let ptr = ::core::ptr::NonNull::from(Box::leak(Box::new(value))); unsafe { VBox::from_raw(core::ptr::NonNull::from(X::static_vtable()), ptr.cast()) } } } }); continue; } if ident == "drop_in_place" { vtable_ctor.push(quote!(#ident: { #[allow(unsafe_code)] #sig_extern { #[allow(unused_unsafe)] unsafe { ::core::ptr::drop_in_place((#self_call).0 as *mut T) }; ::core::alloc::Layout::new::().into() } #ident:: },)); drop_impls.push(quote! { #[allow(unsafe_code)] unsafe impl VTableMetaDropInPlace for #vtable_name { unsafe fn #ident(vtable: &Self::VTable, ptr: *mut u8) -> vtable::Layout { // Safety: The vtable is valid and ptr is a type corresponding to the vtable, (vtable.#ident)(VRefMut::from_raw(core::ptr::NonNull::from(vtable), ::core::ptr::NonNull::new_unchecked(ptr).cast())) } unsafe fn dealloc(vtable: &Self::VTable, ptr: *mut u8, layout: vtable::Layout) { (vtable.dealloc)(vtable, ptr, layout) } } }); continue; } if ident == "dealloc" { let abi = &sig_extern.abi; vtable_ctor.push(quote!(#ident: { #[allow(unsafe_code)] unsafe #abi fn #ident(_: &#vtable_name, ptr: *mut u8, layout: vtable::Layout) { use ::core::convert::TryInto; vtable::internal::dealloc(ptr, layout.try_into().unwrap()) } #ident },)); continue; } generated_trait.items.push(TraitItem::Fn(TraitItemFn { attrs: field.attrs.clone(), sig: sig.clone(), default: None, semi_token: Some(Default::default()), })); generated_to_fn_trait.push(ImplItemFn { attrs: field.attrs.clone(), vis: Visibility::Public(Default::default()), defaultness: None, sig: sig.clone(), block: if has_self { parse_quote!({ // Safety: this rely on the vtable being valid, and the ptr being a valid instance for this vtable #[allow(unsafe_code)] unsafe { let vtable = self.vtable.as_ref(); if let #some(func) = vtable.#ident { func (#call_code) } else { panic!("Called a not-implemented method") } } }) } else { // This should never happen: nobody should be able to access the Trait Object directly. parse_quote!({ panic!("Calling Sized method on a Trait Object") }) }, }); if !has_self { sig.inputs.insert( 0, FnArg::Receiver(Receiver { attrs: Default::default(), reference: Some(Default::default()), mutability: None, self_token: Default::default(), colon_token: None, ty: Box::new(parse_quote!(&Self)), }), ); sig.output = sig_extern.output.clone(); generated_type_assoc_fn.push(ImplItemFn { attrs: field.attrs.clone(), vis: generated_trait.vis.clone(), defaultness: None, sig, block: parse_quote!({ let vtable = self; // Safety: this rely on the vtable being valid, and the ptr being a valid instance for this vtable #[allow(unsafe_code)] unsafe { (self.#ident)(#call_code) } }), }); vtable_ctor.push(quote!(#ident: { #sig_extern { // This is safe since the self must be a instance of our type #[allow(unused)] #[allow(unsafe_code)] let vtable = unsafe { ::core::ptr::NonNull::from(&*_0) }; #wrap_trait_call(T::#ident(#self_call #forward_code)) } #some(#ident::) },)); } else { let erase_return_type_lifetime = match &sig_extern.output { ReturnType::Default => quote!(), // If the return type contains a implicit lifetime, it is safe to erase it while returning it // because a sound implementation of the trait wouldn't allow unsound things here ReturnType::Type(_, r) => { quote!(#[allow(clippy::useless_transmute)] ::core::mem::transmute::<#r, #r>) } }; vtable_ctor.push(quote!(#ident: { #sig_extern { // This is safe since the self must be a instance of our type #[allow(unsafe_code)] unsafe { #erase_return_type_lifetime(T::#ident(#self_call #forward_code)) } } #ident:: },)); } } else { // associated constant let generated_trait_assoc_const = generated_trait_assoc_const.get_or_insert_with(|| ItemTrait { attrs: Attribute::parse_outer.parse_str(&format!( "/** Trait containing the associated constant relative to the trait {trait_name}.\n{additional_doc} */", )).unwrap(), ident: quote::format_ident!("{}Consts", trait_name), items: vec![], ..generated_trait.clone() }); let const_type = if let Some(o) = field .attrs .iter() .position(|a| a.path().get_ident().map(|a| a == "field_offset").unwrap_or(false)) { let a = field.attrs.remove(o); let member_type = match a.parse_args::() { Err(e) => return e.to_compile_error().into(), Ok(ty) => ty, }; match &field.ty { Type::Path(p) if p.path.get_ident().map(|i| i == "usize").unwrap_or(false) => {} ty => { return Error::new( ty.span(), "The type of an #[field_offset] member in the vtable must be 'usize'", ) .to_compile_error() .into() } } // add `: Sized` to the trait in case it does not have it if generated_trait_assoc_const.supertraits.is_empty() { generated_trait_assoc_const.colon_token = Some(Default::default()); generated_trait_assoc_const.supertraits.push(parse_quote!(Sized)); } let offset_type: Type = parse_quote!(vtable::FieldOffset); vtable_ctor.push(quote!(#ident: T::#ident.get_byte_offset(),)); let attrs = &field.attrs; let vis = &field.vis; generated_to_fn_trait.push( parse_quote! { #(#attrs)* #vis fn #ident(&self) -> &#member_type { unsafe { &*(self.ptr.as_ptr().add(self.vtable.as_ref().#ident) as *const #member_type) } } }, ); let ident_mut = quote::format_ident!("{}_mut", ident); generated_to_fn_trait.push( parse_quote! { #(#attrs)* #vis fn #ident_mut(&mut self) -> &mut #member_type { unsafe { &mut *(self.ptr.as_ptr().add(self.vtable.as_ref().#ident) as *mut #member_type) } } }, ); offset_type } else { vtable_ctor.push(quote!(#ident: T::#ident,)); field.ty.clone() }; generated_trait_assoc_const.items.push(TraitItem::Const(TraitItemConst { attrs: field.attrs.clone(), const_token: Default::default(), ident: ident.clone(), colon_token: Default::default(), ty: const_type, default: None, semi_token: Default::default(), generics: Default::default(), })); }; } let vis = input.vis; input.vis = Visibility::Public(Default::default()); let new_trait_extra = generated_trait_assoc_const.as_ref().map(|x| { let i = &x.ident; quote!(+ #i) }); let static_vtable_macro_doc = format!( r"Instantiate a static {vtable} for a given type and implements `vtable::HasStaticVTable<{vtable}>` for it. ```ignore // The preview above is misleading because of rust-lang/rust#45939, so it is reproduced below macro_rules! {macro} {{ ($(#[$meta:meta])* $vis:vis static $ident:ident for $ty:ty) => {{ ... }} }} ``` Given a type `MyType` that implements the trait `{trait} {trait_extra}`, create a static variable of type {vtable}, and implements HasStaticVTable for it. ```ignore struct Foo {{ ... }} impl {trait} for Foo {{ ... }} {macro}!(static FOO_VTABLE for Foo); // now VBox::new can be called let vbox = VBox::new(Foo{{ ... }}); ``` {extra}", vtable = vtable_name, trait = trait_name, trait_extra = new_trait_extra.as_ref().map(|x| x.to_string()).unwrap_or_default(), macro = static_vtable_macro_name, extra = additional_doc, ); let result = quote!( #[allow(non_snake_case)] #[macro_use] /// This private module is generated by the `vtable` macro mod #module_name { #![allow(unused_parens)] #[allow(unused)] use super::*; use ::vtable::*; use ::vtable::internal::*; #input impl #vtable_name { // unfortunately cannot be const in stable rust because of the bounds (depends on rfc 2632) /// Create a vtable suitable for a given type implementing the trait. pub /*const*/ fn new() -> Self { Self { #(#vtable_ctor)* } } #(#generated_type_assoc_fn)* } #generated_trait #generated_trait_assoc_const /// Invariant, same as vtable::Inner: vtable and ptr has to be valid and ptr an instance matching the vtable #[doc(hidden)] #[repr(C)] pub struct #to_name { vtable: ::core::ptr::NonNull<#vtable_name>, ptr: ::core::ptr::NonNull, } impl #to_name { #(#generated_to_fn_trait)* /// Returns a reference to the VTable pub fn get_vtable(&self) -> &#vtable_name { unsafe { self.vtable.as_ref() } } /// Return a raw pointer to the object pub fn as_ptr(&self) -> *const u8 { self.ptr.as_ptr() } } unsafe impl VTableMeta for #vtable_name { type VTable = #vtable_name; type Target = #to_name; } #(#drop_impls)* #[macro_export] #[doc = #static_vtable_macro_doc] macro_rules! #static_vtable_macro_name { ($(#[$meta:meta])* $vis:vis static $ident:ident for $ty:ty) => { $(#[$meta])* $vis static $ident : #vtable_name = { use vtable::*; type T = $ty; #vtable_name { #(#vtable_ctor)* } }; #[allow(unsafe_code)] unsafe impl vtable::HasStaticVTable<#vtable_name> for $ty { fn static_vtable() -> &'static #vtable_name { &$ident } } } } } #[doc(inline)] #[macro_use] #vis use #module_name::*; ); //println!("{}", result); result.into() }