scg: Go all in on Serde serializers

This commit is contained in:
armaniferrante 2020-09-24 13:53:34 -07:00 committed by Armani Ferrante
parent 8478ac0efa
commit d15ff58c09
5 changed files with 77 additions and 76 deletions

View File

@ -8,7 +8,7 @@ edition = "2018"
[features]
program = ["spl-token/program", "solana-sdk/program"]
client = ["spl-token/default", "solana-sdk/default", "solana-client", "anyhow", "rand"]
test = ["solana-client-gen"]
test = []
[dependencies]
spl-token = { version = "=2.0.3", default-features = false }
@ -21,6 +21,3 @@ solana-sdk = { version = "=1.3.9", default-features = false }
anyhow = { version = "1.0.32", optional = true }
rand = { version = "0.7.3", optional = true }
solana-client = { version = "1.3.4", optional = true }
# Test only.
solana-client-gen = { path = "../solana-client-gen", optional = true }

View File

@ -19,3 +19,4 @@ solana-sdk = { version = "=1.3.9", default-features = false }
solana-client = { version = "1.3.4", optional = true }
rand = { version = "0.7.3", optional = true }
codegen = { path = "./codegen", optional = true }
serum-common = { path = "../common" }

View File

@ -22,7 +22,7 @@ pub fn solana_client_gen(
// The one and only argument of the macro should be the Coder struct.
let coder_struct = match args.to_string().as_ref() {
"" => None,
_ => Some(parse_macro_input!(args as syn::Ident)),
_ => Some(parse_macro_input!(args as syn::TypePath)),
};
// Interpet token stream as the instruction `mod`.
@ -57,12 +57,8 @@ pub fn solana_client_gen(
// Second pass:
//
// Parse the instruction enum and generate code from each enum variant.
let (client_methods, instruction_methods, decode_and_dispatch_tree, coder_mod) =
enum_to_methods(
instruction_mod.ident.clone(),
&instruction_enum_item,
coder_struct,
);
let (client_methods, instruction_methods, decode_and_dispatch_tree) =
enum_to_methods(&instruction_enum_item, coder_struct);
// Now recreate the highest level instruction `mod`, but with our new
// instruction_methods inside.
@ -258,22 +254,18 @@ pub fn solana_client_gen(
$($client_ext)*
}
#new_instruction_mod
#coder_mod
}
}
};
// Now put it all together.
//
// Output the transormed AST with the new client, new instruction mod,
// and new coder definition.
// Output the transormed AST with the new client and new instruction mod.
proc_macro::TokenStream::from(quote! {
#[cfg(not(feature = "client-ext"))]
#client_mod
#[cfg(not(feature = "client-ext"))]
#new_instruction_mod
#[cfg(not(feature = "client-ext"))]
#coder_mod
#extendable_client_macro
})
@ -288,18 +280,16 @@ pub fn solana_client_gen(
// * Coder struct for serialization.
//
fn enum_to_methods(
instruction_mod_ident: syn::Ident,
instruction_enum: &syn::ItemEnum,
coder_struct_opt: Option<syn::Ident>,
coder_struct_opt: Option<syn::TypePath>,
) -> (
proc_macro2::TokenStream,
proc_macro2::TokenStream,
proc_macro2::TokenStream,
proc_macro2::TokenStream,
) {
let coder_struct = match &coder_struct_opt {
None => quote! {
solana_client_gen_coder::_DefaultCoder
solana_client_gen::coder::DefaultCoder
},
Some(cs) => quote! {
#cs
@ -673,34 +663,10 @@ fn enum_to_methods(
}
};
// Define the instruction coder to use for serialization.
let coder_mod = match coder_struct_opt {
// It's defined externally so do nothing.
Some(_) => quote! {},
// Coder not provided, so use declare and use the default one.
None => quote! {
pub mod solana_client_gen_coder {
use super::*;
pub struct _DefaultCoder;
impl _DefaultCoder {
pub fn to_bytes(i: #instruction_mod_ident::#instruction_enum_ident) -> Vec<u8> {
serum_common::pack::to_bytes(&i)
.expect("instruction must be serializable")
}
pub fn from_bytes(data: &[u8]) -> Result<#instruction_mod_ident::#instruction_enum_ident, ()> {
serum_common::pack::from_bytes(data)
.map_err(|_| ())
}
}
}
},
};
(
client_methods,
instruction_methods,
decode_and_dispatch_tree,
coder_mod,
)
}

View File

@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
/// InstructionCoder is the trait that must be implemented to user
/// custom serialization with the main macro. If a coder is not
/// provided, the `DefaultCoder` will be used.
pub trait InstructionCoder<'a, T: ?Sized + Serialize + Deserialize<'a>> {
fn to_bytes(i: T) -> Vec<u8>;
fn from_bytes(data: &'a [u8]) -> Result<T, ()>;
}
pub struct DefaultCoder;
impl<'a, T: ?Sized + serde::Serialize + serde::Deserialize<'a>> InstructionCoder<'a, T>
for DefaultCoder
{
fn to_bytes(i: T) -> Vec<u8> {
serum_common::pack::to_bytes(&i).expect("instruction must be serializable")
}
fn from_bytes(data: &'a [u8]) -> Result<T, ()> {
serum_common::pack::from_bytes(data).map_err(|_| ())
}
}

View File

@ -1,6 +1,10 @@
//! solana-client-gen is a convenience macro to generate rpc clients from
//! Solana instruction definitions.
//!
//! Ideally, Solana would have a Rust compiler extension built into their CLI,
//! which emitted a standard runtime IDL that could be used to generate clients.
//! This macro is a stopgap in the mean time.
//!
//! # Creating an interface.
//!
//! To start, one should make an "interface" crate, separate from one's Solana
@ -15,6 +19,7 @@
//!
//! #[cfg_attr(feature = "client", solana_client_gen)]
//! pub mod instruction {
//! #[derive(Serialize, Deserialize)]
//! pub enum MyInstruction {
//! Add: {
//! a: u64,
@ -56,7 +61,7 @@
//! },
//! });
//!
//! Define the accounts the program uses for each instruction.
//! // Define the accounts the program uses for each instruction.
//! let accounts = ...;
//!
//! // Invoke rpcs as desired.
@ -64,11 +69,11 @@
//! client.sub(accounts, 4, 2)
//! ```
//!
//! In addition to generate an rpc client, the macro generates instruction
//! In addition to generating an rpc client, the macro generates instruction
//! methods to create `solana_sdk::instruction::Instruction` types. For example,
//!
//! ```
//! let instruction = program_interface::instruction::add(2, 3);
//! let instruction = my_crate::instruction::add(2, 3);
//! ```
//!
//! This can be used to generate instructions to be invoked with other Solana
@ -79,15 +84,18 @@
//! It's not uncommon to want to atomically create an account and execute an
//! instruction. For example, in the SPL token standard, one must include
//! two instructions in the same transaction: one to create the mint account
//! and another to initialize the mint. To do this, add the #[create_account]
//! and another to initialize the mint. To do this, add the `#[create_account]`
//! attribute to your instruction. For example
//!
//! ```
//! #[cfg_attr(feature = "client", solana_client_gen)]
//! pub mod instruction {
//! #[cfg_attr(feature = "client", create_account)]
//! Initialize {
//! some_data: u64,
//! #[derive(Serialize, Deserialize)]
//! pub enum MyInstruction {
//! #[cfg_attr(feature = "client", create_account)]
//! Initialize {
//! some_data: u64,
//! }
//! }
//! }
//! ```
@ -97,48 +105,52 @@
//! will always be the created account. Note: you don't have to pass the
//! account yourself, since it will be done for you.
//!
//! Furthermore, continuing the above example the simpler `initialize`
//! will also be generated.
//! # Extending the client.
//!
//! If the generated client isn't enough, for example, if you want to batch
//! multiple instructions together into the same transaction as a performance
//! optimization, one can enable the `client-ext` feature in their crate
//! (note: currently this feature is required to be in the crate where the
//! instruction enum is defined).
//!
//! When using the `client-ext`, the proc macro won't generate a client
//! immediately. Instead it will act as a meta-macro, generating yet another
//! macro: `solana_client_gen_extension`, which must be used to build the client.
//!
//! We can extend the above client to have a `hello_world` method.
//!
//! ```
//! solana_client_gen_extension! {
//! impl Client {
//! pub fn hello_world(&self) {
//! println!("hello world from the generated client");
//! }
//! }
//! }
//! ```
//!
//! # Using a custom coder.
//!
//! It's assumed instructions implement serde's `Serialize` and `Deserialize`.
//!
//! By default, a default coder will be used to serialize instructions before
//! sending them to Solana. If you want to use a custom coder, inject it
//! into the macro like this
//!
//! ```
//! #[solana_client_gen(Coder)]
//! #[solana_client_gen(crate::mod::Coder)]
//! mod instruction {
//! ...
//! }
//! ```
//!
//! Where `Coder` is a user defined struct that has two methods. For example,
//!
//! ```
//! struct Coder;
//! impl Coder {
//! pub fn to_bytes(i: instruction::SrmSafeInstruction) -> Vec<u8> {
//! println!("using custom coder");
//! bincode::serialize(&(0u8, i)).expect("instruction must be serializable")
//! }
//! pub fn from_bytes(data: &[u8]) -> Result<instruction::SrmSafeInstruction, ()> {
//! match data.split_first() {
//! None => Err(()),
//! Some((&u08, rest)) => bincode::deserialize(rest).map_err(|_| ()),
//! Some((_, _rest)) => Err(()),
//! }
//! }
//! }
//! ```
//!
//! Note that the names must be `to_bytes` and `from_bytes` and the input/output of
//! both methods must be the name of your instruction's `enum`.
//! Where `Coder` is a user defined struct that implements the
//! `InstructionCoder` interface.
//!
//! # Limitations
//!
//! Currently, only a client is generated for serializing requests to Solana clusters.
//! This is why a conditional attribute macro is used (i.e., cfg_attr).
//! This is why a conditional attribute macro is used via `cfg_attr`.
//!
//! In addition, it would be nice to generate code on the runtime as well to
//! deserialize instructions and dispatch to the correct method. Currently, this
@ -155,6 +167,8 @@ pub mod prelude {
pub use solana_sdk::instruction::{AccountMeta, Instruction};
pub use solana_sdk::pubkey::Pubkey;
pub use crate::coder::InstructionCoder;
#[cfg(feature = "client")]
pub use codegen::solana_client_gen;
#[cfg(feature = "client")]
@ -181,6 +195,8 @@ pub mod prelude {
pub use thiserror::Error;
}
pub mod coder;
// Re-export.
#[cfg(feature = "client")]
pub use solana_client;