-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
dd14b2c
commit 879601e
Showing
28 changed files
with
1,259 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,302 @@ | ||
use proc_macro2::{Literal, TokenStream}; | ||
use quote::{format_ident, quote, ToTokens}; | ||
|
||
pub fn gen_lazy(strct: &syn::ItemStruct) -> syn::Result<TokenStream> { | ||
let ident = &strct.ident; | ||
let lazy_ident = format_ident!("Lazy{}", ident); | ||
let load_common_ident = to_private_ident("load_common"); | ||
let initialize_fields = to_private_ident("initialize_fields"); | ||
let lazy_acc_ty = quote! { anchor_lang::accounts::lazy_account::LazyAccount }; | ||
let disc_len = quote! { <#ident as anchor_lang::Discriminator>::DISCRIMINATOR.len() }; | ||
|
||
let load_common_docs = quote! { | ||
/// The deserialized value is cached for future uses i.e. all subsequent calls to this | ||
/// method do not deserialize the data again, instead, they return the cached value. | ||
/// | ||
/// To reload the data from the underlying account info (e.g. after a CPI call), run | ||
/// [`LazyAccount::unload`] before running this method. | ||
/// | ||
/// See [`LazyAccount`]'s documentation for more information. | ||
}; | ||
let load_panic_docs = quote! { | ||
/// # Panics | ||
/// | ||
/// If there is an existing mutable reference crated by any of the `load_mut` methods. | ||
}; | ||
let load_mut_panic_docs = quote! { | ||
/// # Panics | ||
/// | ||
/// If there is an existing reference (mutable or not) created by any of the `load` methods. | ||
}; | ||
|
||
let (loader_signatures, loader_impls) = strct | ||
.fields | ||
.iter() | ||
.enumerate() | ||
.map(|(i, field)| { | ||
let field_ident = to_field_ident(field, i); | ||
let load_ident = format_ident!("load_{field_ident}"); | ||
let load_mut_ident = format_ident!("load_mut_{field_ident}"); | ||
let load_common_ident = to_private_ident(format!("load_common_{field_ident}")); | ||
let offset_of_ident = to_private_ident(format!("offset_of_{field_ident}")); | ||
let size_of_ident = to_private_ident(format!("size_of_{field_ident}")); | ||
|
||
let offset = i.eq(&0).then(|| quote!(#disc_len)).unwrap_or_else(|| { | ||
// Current offset is the previous field's offset + size | ||
strct | ||
.fields | ||
.iter() | ||
.nth(i - 1) | ||
.map(|field| { | ||
let field_ident = to_field_ident(field, i - 1); | ||
let offset_of_ident = to_private_ident(format!("offset_of_{field_ident}")); | ||
let size_of_ident = to_private_ident(format!("size_of_{field_ident}")); | ||
quote! { self.#offset_of_ident() + self.#size_of_ident() } | ||
}) | ||
.expect("Previous field should always exist when i > 0") | ||
}); | ||
|
||
let ty = &field.ty; | ||
let size = quote! { | ||
<#ty as anchor_lang::__private::Lazy>::size_of( | ||
&self.__info.data.borrow()[self.#offset_of_ident()..] | ||
) | ||
}; | ||
|
||
let signatures = quote! { | ||
/// Load a reference to the field. | ||
/// | ||
#load_common_docs | ||
/// | ||
#load_panic_docs | ||
fn #load_ident(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ty>>; | ||
|
||
/// Load a mutable reference to the field. | ||
/// | ||
#load_common_docs | ||
/// | ||
#load_mut_panic_docs | ||
fn #load_mut_ident(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ty>>; | ||
|
||
#[doc(hidden)] | ||
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R>; | ||
|
||
#[doc(hidden)] | ||
fn #offset_of_ident(&self) -> usize; | ||
|
||
#[doc(hidden)] | ||
fn #size_of_ident(&self) -> usize; | ||
}; | ||
|
||
let impls = quote! { | ||
fn #load_ident(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ty>> { | ||
self.#load_common_ident(|| { | ||
// SAFETY: The common load method makes sure the field is initialized. | ||
::core::cell::Ref::map(self.__account.borrow(), |acc| unsafe { | ||
&*::core::ptr::addr_of!((*acc.as_ptr()).#field_ident) | ||
}) | ||
}) | ||
} | ||
|
||
fn #load_mut_ident(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ty>> { | ||
self.#load_common_ident(|| { | ||
// SAFETY: The common load method makes sure the field is initialized. | ||
::core::cell::RefMut::map(self.__account.borrow_mut(), |acc| unsafe { | ||
&mut *::core::ptr::addr_of_mut!((*acc.as_mut_ptr()).#field_ident) | ||
}) | ||
}) | ||
} | ||
|
||
#[inline(never)] | ||
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R> { | ||
self.#initialize_fields(); | ||
|
||
// Return early if initialized | ||
if self.__fields.borrow().as_ref().unwrap()[#i] { | ||
return Ok(f()); | ||
} | ||
|
||
// Deserialize and write | ||
let offset = self.#offset_of_ident(); | ||
let size = self.#size_of_ident(); | ||
let data = self.__info.data.borrow(); | ||
let val = anchor_lang::AnchorDeserialize::try_from_slice( | ||
&data[offset..offset + size] | ||
)?; | ||
unsafe { | ||
::core::ptr::addr_of_mut!( | ||
(*self.__account.borrow_mut().as_mut_ptr()).#field_ident | ||
).write(val) | ||
}; | ||
|
||
// Set initialized | ||
self.__fields.borrow_mut().as_mut().unwrap()[#i] = true; | ||
|
||
Ok(f()) | ||
} | ||
|
||
// If this method gets inlined when there are >= 12 fields, compilation breaks with | ||
// `LLVM ERROR: Branch target out of insn range` | ||
#[inline(never)] | ||
fn #offset_of_ident(&self) -> usize { | ||
#offset | ||
} | ||
|
||
#[inline(always)] | ||
fn #size_of_ident(&self) -> usize { | ||
#size | ||
} | ||
}; | ||
|
||
Ok((signatures, impls)) | ||
}) | ||
.collect::<syn::Result<Vec<_>>>()? | ||
.into_iter() | ||
.unzip::<_, _, Vec<_>, Vec<_>>(); | ||
|
||
let load_idents = strct | ||
.fields | ||
.iter() | ||
.enumerate() | ||
.map(|(i, field)| to_field_ident(field, i)) | ||
.map(|field| format_ident!("load_{field}")); | ||
let total_fields = strct.fields.len(); | ||
|
||
Ok(quote! { | ||
pub trait #lazy_ident { | ||
/// Load a reference to the entire account. | ||
/// | ||
#load_common_docs | ||
/// | ||
#load_panic_docs | ||
fn load(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ident>>; | ||
|
||
/// Load a mutable reference to the entire account. | ||
/// | ||
#load_common_docs | ||
/// | ||
#load_mut_panic_docs | ||
fn load_mut(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ident>>; | ||
|
||
#[doc(hidden)] | ||
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R>; | ||
|
||
#(#loader_signatures)* | ||
|
||
#[doc(hidden)] | ||
fn #initialize_fields(&self); | ||
|
||
/// Run the exit routine of the account, similar to [`AccountsExit`] but implemented | ||
/// as a regular method because we can't implement external traits for external structs. | ||
fn exit(&self, program_id: &anchor_lang::prelude::Pubkey) -> anchor_lang::Result<()>; | ||
} | ||
|
||
impl<'info> #lazy_ident for #lazy_acc_ty<'info, #ident> { | ||
fn load(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ident>> { | ||
self.#load_common_ident(|| { | ||
// SAFETY: The common load method makes sure all fields are initialized. | ||
::core::cell::Ref::map(self.__account.borrow(), |acc| unsafe { | ||
acc.assume_init_ref() | ||
}) | ||
}) | ||
} | ||
|
||
fn load_mut(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ident>> { | ||
self.#load_common_ident(|| { | ||
// SAFETY: The common load method makes sure all fields are initialized. | ||
::core::cell::RefMut::map(self.__account.borrow_mut(), |acc| unsafe { | ||
acc.assume_init_mut() | ||
}) | ||
}) | ||
} | ||
|
||
#[inline(never)] | ||
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R> { | ||
self.#initialize_fields(); | ||
|
||
// Create a scope to drop the `__fields` borrow | ||
let all_uninit = { | ||
// Return early if all fields are initialized | ||
let fields = self.__fields.borrow(); | ||
let fields = fields.as_ref().unwrap(); | ||
if !fields.contains(&false) { | ||
return Ok(f()); | ||
} | ||
|
||
!fields.contains(&true) | ||
}; | ||
|
||
if all_uninit { | ||
// Nothing is initialized, initialize all | ||
let offset = #disc_len; | ||
let mut data = self.__info.data.borrow(); | ||
let val = anchor_lang::AnchorDeserialize::deserialize(&mut &data[offset..])?; | ||
unsafe { self.__account.borrow_mut().as_mut_ptr().write(val) }; | ||
|
||
// Set fields to initialized | ||
let mut fields = self.__fields.borrow_mut(); | ||
let fields = fields.as_mut().unwrap(); | ||
for field in fields { | ||
*field = true; | ||
} | ||
} else { | ||
// Only initialize uninitialized fields (`load` methods already do this). | ||
// | ||
// This is not exactly efficient because `load` methods have a bit of | ||
// runtime ownership overhead. This could be optimized further, but it | ||
// requires some refactoring and also makes the code harder to reason about. | ||
// | ||
// We can return back to this if benchmarks show this is a bottleneck. | ||
#(self.#load_idents()?;)* | ||
} | ||
|
||
Ok(f()) | ||
} | ||
|
||
#(#loader_impls)* | ||
|
||
#[inline(always)] | ||
fn #initialize_fields(&self) { | ||
if self.__fields.borrow().is_none() { | ||
*self.__fields.borrow_mut() = Some(vec![false; #total_fields]); | ||
} | ||
} | ||
|
||
// TODO: This method can be optimized to *only* serialize the fields that we have | ||
// initialized rather than deserializing the whole account, and then serializing it | ||
// back, which consumes a lot more CUs than it should for most accounts. | ||
fn exit(&self, program_id: &anchor_lang::prelude::Pubkey) -> anchor_lang::Result<()> { | ||
// Only persist if the owner is the current program and the account is not closed | ||
if &<#ident as anchor_lang::Owner>::owner() == program_id | ||
&& !anchor_lang::__private::is_closed(self.__info) | ||
{ | ||
// Make sure all fields are initialized | ||
let acc = self.load()?; | ||
let mut data = self.__info.try_borrow_mut_data()?; | ||
let dst: &mut [u8] = &mut data; | ||
let mut writer = anchor_lang::__private::BpfWriter::new(dst); | ||
acc.try_serialize(&mut writer)?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
} | ||
}) | ||
} | ||
|
||
/// Get the field's ident and if the ident doesn't exist (e.g. for tuple structs), default to the | ||
/// given index. | ||
fn to_field_ident(field: &syn::Field, i: usize) -> TokenStream { | ||
field | ||
.ident | ||
.as_ref() | ||
.map(ToTokens::to_token_stream) | ||
.unwrap_or_else(|| Literal::usize_unsuffixed(i).to_token_stream()) | ||
} | ||
|
||
/// Convert to private ident. | ||
/// | ||
/// This is used to indicate to the users that they shouldn't use this identifier. | ||
fn to_private_ident<S: AsRef<str>>(ident: S) -> syn::Ident { | ||
format_ident!("__{}", ident.as_ref()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.