|
@@ -17,12 +17,39 @@ use syn::parse_macro_input;
|
|
|
/// As a result, any calls to `AccountDeserialize`'s `try_deserialize` will
|
|
|
/// check this discriminator. If it doesn't match, an invalid account was given,
|
|
|
/// and the account deserialization will exit with an error.
|
|
|
+///
|
|
|
+/// # Zero Copy Deserialization
|
|
|
+///
|
|
|
+/// To enable zero-copy-deserialization, one can pass in the `zero_copy`
|
|
|
+/// argument to the macro as follows:
|
|
|
+///
|
|
|
+/// ```ignore
|
|
|
+/// #[account(zero_copy)]
|
|
|
+/// ```
|
|
|
+///
|
|
|
+/// This can be used to conveniently implement
|
|
|
+/// [`ZeroCopy`](./trait.ZeroCopy.html) so that the account can be used
|
|
|
+/// with [`AccountLoader`](./struct.AccountLoader.html).
|
|
|
+///
|
|
|
+/// Other than being more efficient, the most salient benefit this provides is
|
|
|
+/// the ability to define account types larger than the max stack or heap size.
|
|
|
+/// This is used in special cases, for example, the Serum DEX event queue. When
|
|
|
+/// using borsh, one is limited, since the account has to be copied and
|
|
|
+/// deserialized into a new data structure. With zero copy deserialization,
|
|
|
+/// everything is, effectively, lazy loaded on field access.
|
|
|
#[proc_macro_attribute]
|
|
|
pub fn account(
|
|
|
args: proc_macro::TokenStream,
|
|
|
input: proc_macro::TokenStream,
|
|
|
) -> proc_macro::TokenStream {
|
|
|
let namespace = args.to_string().replace("\"", "");
|
|
|
+ let is_zero_copy = match args.into_iter().next() {
|
|
|
+ None => false,
|
|
|
+ Some(tt) => match tt {
|
|
|
+ proc_macro::TokenTree::Literal(_) => false,
|
|
|
+ _ => namespace == "zero_copy",
|
|
|
+ },
|
|
|
+ };
|
|
|
|
|
|
let account_strct = parse_macro_input!(input as syn::ItemStruct);
|
|
|
let account_name = &account_strct.ident;
|
|
@@ -30,7 +57,8 @@ pub fn account(
|
|
|
let discriminator: proc_macro2::TokenStream = {
|
|
|
// Namespace the discriminator to prevent collisions.
|
|
|
let discriminator_preimage = {
|
|
|
- if namespace.is_empty() {
|
|
|
+ // For now, zero copy accounts can't be namespaced.
|
|
|
+ if is_zero_copy || namespace.is_empty() {
|
|
|
format!("account:{}", account_name.to_string())
|
|
|
} else {
|
|
|
format!("{}:{}", namespace, account_name.to_string())
|
|
@@ -44,61 +72,111 @@ pub fn account(
|
|
|
format!("{:?}", discriminator).parse().unwrap()
|
|
|
};
|
|
|
|
|
|
- let coder = quote! {
|
|
|
- impl anchor_lang::AccountSerialize for #account_name {
|
|
|
- fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> std::result::Result<(), ProgramError> {
|
|
|
- writer.write_all(&#discriminator).map_err(|_| ProgramError::InvalidAccountData)?;
|
|
|
- AnchorSerialize::serialize(
|
|
|
- self,
|
|
|
- writer
|
|
|
- )
|
|
|
- .map_err(|_| ProgramError::InvalidAccountData)?;
|
|
|
- Ok(())
|
|
|
- }
|
|
|
- }
|
|
|
+ proc_macro::TokenStream::from({
|
|
|
+ if is_zero_copy {
|
|
|
+ quote! {
|
|
|
+ #[zero_copy]
|
|
|
+ #account_strct
|
|
|
+
|
|
|
+ unsafe impl anchor_lang::__private::bytemuck::Pod for #account_name {}
|
|
|
+ unsafe impl anchor_lang::__private::bytemuck::Zeroable for #account_name {}
|
|
|
+
|
|
|
+ impl anchor_lang::ZeroCopy for #account_name {}
|
|
|
|
|
|
- impl anchor_lang::AccountDeserialize for #account_name {
|
|
|
- fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
|
|
|
- if buf.len() < #discriminator.len() {
|
|
|
- return Err(ProgramError::AccountDataTooSmall);
|
|
|
+ impl anchor_lang::Discriminator for #account_name {
|
|
|
+ fn discriminator() -> [u8; 8] {
|
|
|
+ #discriminator
|
|
|
+ }
|
|
|
}
|
|
|
- let given_disc = &buf[..8];
|
|
|
- if &#discriminator != given_disc {
|
|
|
- return Err(ProgramError::InvalidInstructionData);
|
|
|
+
|
|
|
+ // This trait is useful for clients deserializing accounts.
|
|
|
+ // It's expected on-chain programs deserialize via zero-copy.
|
|
|
+ impl anchor_lang::AccountDeserialize for #account_name {
|
|
|
+ fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
|
|
|
+ if buf.len() < #discriminator.len() {
|
|
|
+ return Err(ProgramError::AccountDataTooSmall);
|
|
|
+ }
|
|
|
+ let given_disc = &buf[..8];
|
|
|
+ if &#discriminator != given_disc {
|
|
|
+ return Err(ProgramError::InvalidInstructionData);
|
|
|
+ }
|
|
|
+ Self::try_deserialize_unchecked(buf)
|
|
|
+ }
|
|
|
+
|
|
|
+ fn try_deserialize_unchecked(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
|
|
|
+ let data: &[u8] = &buf[8..];
|
|
|
+ // Re-interpret raw bytes into the POD data structure.
|
|
|
+ let account = anchor_lang::__private::bytemuck::from_bytes(data);
|
|
|
+ // Copy out the bytes into a new, owned data structure.
|
|
|
+ Ok(*account)
|
|
|
+ }
|
|
|
}
|
|
|
- Self::try_deserialize_unchecked(buf)
|
|
|
}
|
|
|
+ } else {
|
|
|
+ quote! {
|
|
|
+ #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
|
|
|
+ #account_strct
|
|
|
|
|
|
- fn try_deserialize_unchecked(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
|
|
|
- let mut data: &[u8] = &buf[8..];
|
|
|
- AnchorDeserialize::deserialize(&mut data)
|
|
|
- .map_err(|_| ProgramError::InvalidAccountData)
|
|
|
- }
|
|
|
- }
|
|
|
+ impl anchor_lang::AccountSerialize for #account_name {
|
|
|
+ fn try_serialize<W: std::io::Write>(&self, writer: &mut W) -> std::result::Result<(), ProgramError> {
|
|
|
+ writer.write_all(&#discriminator).map_err(|_| ProgramError::InvalidAccountData)?;
|
|
|
+ AnchorSerialize::serialize(
|
|
|
+ self,
|
|
|
+ writer
|
|
|
+ )
|
|
|
+ .map_err(|_| ProgramError::InvalidAccountData)?;
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- impl anchor_lang::Discriminator for #account_name {
|
|
|
- fn discriminator() -> [u8; 8] {
|
|
|
- #discriminator
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
+ impl anchor_lang::AccountDeserialize for #account_name {
|
|
|
+ fn try_deserialize(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
|
|
|
+ if buf.len() < #discriminator.len() {
|
|
|
+ return Err(ProgramError::AccountDataTooSmall);
|
|
|
+ }
|
|
|
+ let given_disc = &buf[..8];
|
|
|
+ if &#discriminator != given_disc {
|
|
|
+ return Err(ProgramError::InvalidInstructionData);
|
|
|
+ }
|
|
|
+ Self::try_deserialize_unchecked(buf)
|
|
|
+ }
|
|
|
|
|
|
- proc_macro::TokenStream::from(quote! {
|
|
|
- #[derive(AnchorSerialize, AnchorDeserialize, Clone)]
|
|
|
- #account_strct
|
|
|
+ fn try_deserialize_unchecked(buf: &mut &[u8]) -> std::result::Result<Self, ProgramError> {
|
|
|
+ let mut data: &[u8] = &buf[8..];
|
|
|
+ AnchorDeserialize::deserialize(&mut data)
|
|
|
+ .map_err(|_| ProgramError::InvalidAccountData)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- #coder
|
|
|
+ impl anchor_lang::Discriminator for #account_name {
|
|
|
+ fn discriminator() -> [u8; 8] {
|
|
|
+ #discriminator
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-/// Extends the `#[account]` attribute to allow one to create associated token
|
|
|
+/// Extends the `#[account]` attribute to allow one to create associated
|
|
|
/// accounts. This includes a `Default` implementation, which means all fields
|
|
|
/// in an `#[associated]` struct must implement `Default` and an
|
|
|
/// `anchor_lang::Bump` trait implementation, which allows the account to be
|
|
|
/// used as a program derived address.
|
|
|
+///
|
|
|
+/// # Zero Copy Deserialization
|
|
|
+///
|
|
|
+/// Similar to the `#[account]` attribute one can enable zero copy
|
|
|
+/// deserialization by using the `zero_copy` argument:
|
|
|
+///
|
|
|
+/// ```ignore
|
|
|
+/// #[associated(zero_copy)]
|
|
|
+/// ```
|
|
|
+///
|
|
|
+/// For more, see the [`account`](./attr.account.html) attribute.
|
|
|
#[proc_macro_attribute]
|
|
|
pub fn associated(
|
|
|
- _args: proc_macro::TokenStream,
|
|
|
+ args: proc_macro::TokenStream,
|
|
|
input: proc_macro::TokenStream,
|
|
|
) -> proc_macro::TokenStream {
|
|
|
let mut account_strct = parse_macro_input!(input as syn::ItemStruct);
|
|
@@ -132,8 +210,9 @@ pub fn associated(
|
|
|
_ => panic!("Fields must be named"),
|
|
|
}
|
|
|
|
|
|
+ let args: proc_macro2::TokenStream = args.into();
|
|
|
proc_macro::TokenStream::from(quote! {
|
|
|
- #[anchor_lang::account]
|
|
|
+ #[anchor_lang::account(#args)]
|
|
|
#[derive(Default)]
|
|
|
#account_strct
|
|
|
|
|
@@ -144,3 +223,87 @@ pub fn associated(
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
+
|
|
|
+#[proc_macro_derive(ZeroCopyAccessor, attributes(accessor))]
|
|
|
+pub fn derive_zero_copy_accessor(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
|
|
+ let account_strct = parse_macro_input!(item as syn::ItemStruct);
|
|
|
+ let account_name = &account_strct.ident;
|
|
|
+
|
|
|
+ let fields = match &account_strct.fields {
|
|
|
+ syn::Fields::Named(n) => n,
|
|
|
+ _ => panic!("Fields must be named"),
|
|
|
+ };
|
|
|
+ let methods: Vec<proc_macro2::TokenStream> = fields
|
|
|
+ .named
|
|
|
+ .iter()
|
|
|
+ .filter_map(|field: &syn::Field| {
|
|
|
+ field
|
|
|
+ .attrs
|
|
|
+ .iter()
|
|
|
+ .filter(|attr| {
|
|
|
+ let name = anchor_syn::parser::tts_to_string(&attr.path);
|
|
|
+ if name != "accessor" {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ })
|
|
|
+ .next()
|
|
|
+ .map(|attr| {
|
|
|
+ let mut tts = attr.tokens.clone().into_iter();
|
|
|
+ let g_stream = match tts.next().expect("Must have a token group") {
|
|
|
+ proc_macro2::TokenTree::Group(g) => g.stream(),
|
|
|
+ _ => panic!("Invalid syntax"),
|
|
|
+ };
|
|
|
+ let accessor_ty = match g_stream.into_iter().next() {
|
|
|
+ Some(token) => token,
|
|
|
+ _ => panic!("Missing accessor type"),
|
|
|
+ };
|
|
|
+
|
|
|
+ let field_name = field.ident.as_ref().unwrap();
|
|
|
+
|
|
|
+ let get_field: proc_macro2::TokenStream =
|
|
|
+ format!("get_{}", field_name.to_string()).parse().unwrap();
|
|
|
+ let set_field: proc_macro2::TokenStream =
|
|
|
+ format!("set_{}", field_name.to_string()).parse().unwrap();
|
|
|
+
|
|
|
+ quote! {
|
|
|
+ pub fn #get_field(&self) -> #accessor_ty {
|
|
|
+ anchor_lang::__private::ZeroCopyAccessor::get(&self.#field_name)
|
|
|
+ }
|
|
|
+ pub fn #set_field(&mut self, input: &#accessor_ty) {
|
|
|
+ self.#field_name = anchor_lang::__private::ZeroCopyAccessor::set(input);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+ .collect();
|
|
|
+ proc_macro::TokenStream::from(quote! {
|
|
|
+ impl #account_name {
|
|
|
+ #(#methods)*
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+/// A data structure that can be used as an internal field for a zero copy
|
|
|
+/// deserialized account, i.e., a struct marked with `#[account(zero_copy)]`.
|
|
|
+///
|
|
|
+/// This is just a convenient alias for
|
|
|
+///
|
|
|
+/// ```ignore
|
|
|
+/// #[derive(Copy, Clone)]
|
|
|
+/// #[repr(packed)]
|
|
|
+/// struct MyStruct {...}
|
|
|
+/// ```
|
|
|
+#[proc_macro_attribute]
|
|
|
+pub fn zero_copy(
|
|
|
+ _args: proc_macro::TokenStream,
|
|
|
+ item: proc_macro::TokenStream,
|
|
|
+) -> proc_macro::TokenStream {
|
|
|
+ let account_strct = parse_macro_input!(item as syn::ItemStruct);
|
|
|
+
|
|
|
+ proc_macro::TokenStream::from(quote! {
|
|
|
+ #[derive(anchor_lang::__private::ZeroCopyAccessor, Copy, Clone)]
|
|
|
+ #[repr(packed)]
|
|
|
+ #account_strct
|
|
|
+ })
|
|
|
+}
|