diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8e2d0ce70..61464a239 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -31,6 +31,12 @@ search = version = \"{current_version}\" [bumpversion:file:bindings/rust/evmc-vm/Cargo.toml] search = version = \"{current_version}\" +[bumpversion:file:bindings/rust/evmc-declare/Cargo.toml] +search = version = \"{current_version}\" + +[bumpversion:file:bindings/rust/evmc-declare-tests/Cargo.toml] +search = version = \"{current_version}\" + [bumpversion:file:docs/EVMC.md] serialize = {major} search = ABI version {current_version} diff --git a/Cargo.toml b/Cargo.toml index e2f53611b..d265bcdbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,5 +2,7 @@ members = [ "bindings/rust/evmc-sys", "bindings/rust/evmc-vm", + "bindings/rust/evmc-declare", + "bindings/rust/evmc-declare-tests", "examples/example-rust-vm" ] diff --git a/bindings/rust/evmc-declare-tests/.gitignore b/bindings/rust/evmc-declare-tests/.gitignore new file mode 100644 index 000000000..84c47ed70 --- /dev/null +++ b/bindings/rust/evmc-declare-tests/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +/Cargo.lock diff --git a/bindings/rust/evmc-declare-tests/Cargo.toml b/bindings/rust/evmc-declare-tests/Cargo.toml new file mode 100644 index 000000000..9087a3df0 --- /dev/null +++ b/bindings/rust/evmc-declare-tests/Cargo.toml @@ -0,0 +1,18 @@ +# EVMC: Ethereum Client-VM Connector API. +# Copyright 2019 The EVMC Authors. +# Licensed under the Apache License, Version 2.0. + +[package] +name = "evmc-declare-tests" +version = "6.3.0-dev" +authors = ["Jake Lang "] +license = "Apache-2.0" +repository = "https://github.com/ethereum/evmc" +description = "Bindings to EVMC (VM declare macro) -- Test crate" +edition = "2018" +publish = false + +[dependencies] +evmc-declare = { path = "../evmc-declare" } +evmc-sys = { path = "../evmc-sys" } +evmc-vm = { path = "../evmc-vm" } diff --git a/bindings/rust/evmc-declare-tests/src/lib.rs b/bindings/rust/evmc-declare-tests/src/lib.rs new file mode 100644 index 000000000..59ce79a3b --- /dev/null +++ b/bindings/rust/evmc-declare-tests/src/lib.rs @@ -0,0 +1,25 @@ +/* EVMC: Ethereum Client-VM Connector API. + * Copyright 2019 The EVMC Authors. + * Licensed under the Apache License, Version 2.0. + */ + +use evmc_vm::EvmcVm; +use evmc_vm::ExecutionContext; +use evmc_vm::ExecutionResult; +#[macro_use] +use evmc_declare::evmc_declare_vm; + +#[evmc_declare_vm("Foo VM", "ewasm, evm", "1.42-alpha.gamma.starship")] +pub struct FooVM { + a: i32, +} + +impl EvmcVm for FooVM { + fn init() -> Self { + FooVM { a: 105023 } + } + + fn execute(&self, code: &[u8], context: &ExecutionContext) -> ExecutionResult { + ExecutionResult::success(1337, None) + } +} diff --git a/bindings/rust/evmc-declare/.gitignore b/bindings/rust/evmc-declare/.gitignore new file mode 100644 index 000000000..84c47ed70 --- /dev/null +++ b/bindings/rust/evmc-declare/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +/Cargo.lock diff --git a/bindings/rust/evmc-declare/Cargo.toml b/bindings/rust/evmc-declare/Cargo.toml new file mode 100644 index 000000000..859c2df44 --- /dev/null +++ b/bindings/rust/evmc-declare/Cargo.toml @@ -0,0 +1,27 @@ +# EVMC: Ethereum Client-VM Connector API. +# Copyright 2019 The EVMC Authors. +# Licensed under the Apache License, Version 2.0. + +[package] +name = "evmc-declare" +version = "6.3.0-dev" +authors = ["Jake Lang "] +license = "Apache-2.0" +repository = "https://github.com/ethereum/evmc" +description = "Bindings to EVMC (VM declare macro)" +edition = "2018" + +[dependencies] +quote = "0.6.12" +heck = "0.3.1" +proc-macro2 = "0.4.29" + +# For documentation examples +evmc-vm = { path = "../evmc-vm" } + +[dependencies.syn] +version = "0.15.33" +features = ["full"] + +[lib] +proc-macro = true diff --git a/bindings/rust/evmc-declare/src/lib.rs b/bindings/rust/evmc-declare/src/lib.rs new file mode 100644 index 000000000..1b29526bb --- /dev/null +++ b/bindings/rust/evmc-declare/src/lib.rs @@ -0,0 +1,374 @@ +/* EVMC: Ethereum Client-VM Connector API. + * Copyright 2019 The EVMC Authors. + * Licensed under the Apache License, Version 2.0. + */ + +//! evmc-declare is an attribute-style procedural macro to be used for automatic generation of FFI +//! code for the EVMC API with minimal boilerplate. +//! +//! evmc-declare can be used by applying its attribute to any struct which implements the `EvmcVm` +//! trait, from the evmc-vm crate. +//! +//! The macro takes two or three arguments: a valid UTF-8 stylized VM name, a comma-separated list of +//! capabilities, and an optional custom version string. If only the name and capabilities are +//! passed, the version string will default to the crate version. +//! +//! # Example +//! ``` +//! #[evmc_declare::evmc_declare_vm("This is an example VM name", "ewasm, evm", "1.2.3-custom")] +//! pub struct ExampleVM; +//! +//! impl evmc_vm::EvmcVm for ExampleVM { +//! fn init() -> Self { +//! ExampleVM {} +//! } +//! +//! fn execute(&self, code: &[u8], context: &evmc_vm::ExecutionContext) -> evmc_vm::ExecutionResult { +//! evmc_vm::ExecutionResult::success(1337, None) +//! } +//! } +//! ``` + +// Set a higher recursion limit because parsing certain token trees might fail with the default of 64. +#![recursion_limit = "128"] + +extern crate proc_macro; + +use heck::ShoutySnakeCase; +use heck::SnakeCase; +use proc_macro::TokenStream; +use quote::quote; +use syn::parse_macro_input; +use syn::spanned::Spanned; +use syn::AttributeArgs; +use syn::Ident; +use syn::IntSuffix; +use syn::ItemStruct; +use syn::Lit; +use syn::LitInt; +use syn::LitStr; +use syn::NestedMeta; + +struct VMNameSet { + type_name: String, + name_allcaps: String, + name_lowercase: String, +} + +struct VMMetaData { + capabilities: u32, + // Not included in VMNameSet because it is parsed from the meta-item arguments. + name_stylized: String, + custom_version: Option, +} + +#[allow(dead_code)] +impl VMNameSet { + fn new(ident: String) -> Self { + let caps = ident.to_shouty_snake_case(); + let lowercase = ident + .to_snake_case() + .chars() + .filter(|c| *c != '_') + .collect(); + VMNameSet { + type_name: ident, + name_allcaps: caps, + name_lowercase: lowercase, + } + } + + /// Return a reference to the struct name, as a string. + fn get_type_name(&self) -> &String { + &self.type_name + } + + /// Return a reference to the name in shouty snake case. + fn get_name_caps(&self) -> &String { + &self.name_allcaps + } + + /// Return a reference to the name in lowercase, with all underscores removed. (Used for + /// symbols like evmc_create_vmname) + fn get_name_lowercase(&self) -> &String { + &self.name_lowercase + } + + /// Get the struct's name as an explicit identifier to be interpolated with quote. + fn get_type_as_ident(&self) -> Ident { + Ident::new(&self.type_name, self.type_name.span()) + } + + /// Get the lowercase name appended with arbitrary text as an explicit ident. + fn get_lowercase_as_ident_append(&self, suffix: &str) -> Ident { + let concat = format!("{}{}", &self.name_lowercase, suffix); + Ident::new(&concat, self.name_lowercase.span()) + } + + /// Get the lowercase name prepended with arbitrary text as an explicit ident. + fn get_lowercase_as_ident_prepend(&self, prefix: &str) -> Ident { + let concat = format!("{}{}", prefix, &self.name_lowercase); + Ident::new(&concat, self.name_lowercase.span()) + } + + /// Get the lowercase name appended with arbitrary text as an explicit ident. + fn get_caps_as_ident_append(&self, suffix: &str) -> Ident { + let concat = format!("{}{}", &self.name_allcaps, suffix); + Ident::new(&concat, self.name_allcaps.span()) + } +} + +impl VMMetaData { + fn new(args: AttributeArgs) -> Self { + assert!( + args.len() == 2 || args.len() == 3, + "Incorrect number of arguments supplied" + ); + + let vm_name_meta = &args[0]; + let vm_capabilities_meta = &args[1]; + + let vm_version_meta = if args.len() == 3 { + Some(&args[2]) + } else { + None + }; + + let vm_name_string = match vm_name_meta { + NestedMeta::Literal(lit) => { + if let Lit::Str(s) = lit { + s.value() + } else { + panic!("Literal argument type mismatch") + } + } + _ => panic!("Argument 1 must be a string literal"), + }; + + let vm_capabilities_string = match vm_capabilities_meta { + NestedMeta::Literal(lit) => { + if let Lit::Str(s) = lit { + s.value().to_string() + } else { + panic!("Literal argument type mismatch") + } + } + _ => panic!("Argument 2 must be a string literal"), + }; + + // Parse the individual capabilities out of the list and prepare a capabilities flagset. + // Prune spaces and underscores here to make a clean comma-separated list. + let capabilities_list_pruned: String = vm_capabilities_string + .chars() + .filter(|c| *c != '_' && *c != ' ') + .collect(); + let capabilities_flags = { + let mut ret: u32 = 0; + for capability in capabilities_list_pruned.split(",") { + match capability { + "ewasm" => ret |= 0x1 << 1, + "evm" => ret |= 0x1, + _ => panic!("Invalid capability specified."), + } + } + ret + }; + + // If a custom version string was supplied, then include it in the metadata. + let vm_version_string_optional: Option = match vm_version_meta { + Some(meta) => { + if let NestedMeta::Literal(lit) = meta { + match lit { + Lit::Str(s) => Some(s.value().to_string()), + _ => panic!("Literal argument type mismatch"), + } + } else { + panic!("Argument 3 must be a string literal") + } + } + None => None, + }; + + VMMetaData { + capabilities: capabilities_flags, + name_stylized: vm_name_string, + custom_version: vm_version_string_optional, + } + } + + fn get_capabilities(&self) -> u32 { + self.capabilities + } + + fn get_name_stylized(&self) -> &String { + &self.name_stylized + } + + fn get_custom_version(&self) -> &Option { + &self.custom_version + } +} + +#[proc_macro_attribute] +pub fn evmc_declare_vm(args: TokenStream, item: TokenStream) -> TokenStream { + // First, try to parse the input token stream into an AST node representing a struct + // declaration. + let input: ItemStruct = parse_macro_input!(item as ItemStruct); + + // Extract the identifier of the struct from the AST node. + let vm_type_name: String = input.ident.to_string(); + + // Build the VM name set. + let names = VMNameSet::new(vm_type_name); + + // Parse the arguments for the macro. + let meta_args = parse_macro_input!(args as AttributeArgs); + let vm_data = VMMetaData::new(meta_args); + + // Get all the tokens from the respective helpers. + let static_data_tokens = build_static_data(&names, &vm_data); + let capabilities_tokens = build_capabilities_fn(vm_data.get_capabilities()); + let create_tokens = build_create_fn(&names); + let destroy_tokens = build_destroy_fn(&names); + let execute_tokens = build_execute_fn(&names); + + let quoted = quote! { + #input + #static_data_tokens + #capabilities_tokens + #create_tokens + #destroy_tokens + #execute_tokens + }; + + quoted.into() +} + +/// Generate tokens for the static data associated with an EVMC VM. +fn build_static_data(names: &VMNameSet, metadata: &VMMetaData) -> proc_macro2::TokenStream { + // Stitch together the VM name and the suffix _NAME + let static_name_ident = names.get_caps_as_ident_append("_NAME"); + let static_version_ident = names.get_caps_as_ident_append("_VERSION"); + + // Turn the stylized VM name and version into string literals. + let stylized_name_literal = LitStr::new( + metadata.get_name_stylized().as_str(), + metadata.get_name_stylized().as_str().span(), + ); + + // If the user supplied a custom version, use it here. Otherwise, default to crate version. + let version_tokens = match metadata.get_custom_version() { + Some(s) => { + let lit = LitStr::new(s.as_str(), s.as_str().span()); + quote! { + #lit + } + } + None => quote! { + env!("CARGO_PKG_VERSION") + }, + }; + + quote! { + static #static_name_ident: &'static str = #stylized_name_literal; + static #static_version_ident: &'static str = #version_tokens; + } +} + +/// Takes a capabilities flag and builds the evmc_get_capabilities callback. +fn build_capabilities_fn(capabilities: u32) -> proc_macro2::TokenStream { + let capabilities_literal = + LitInt::new(capabilities as u64, IntSuffix::U32, capabilities.span()); + + quote! { + extern "C" fn __evmc_get_capabilities(instance: *mut ::evmc_vm::ffi::evmc_instance) -> ::evmc_vm::ffi::evmc_capabilities_flagset { + #capabilities_literal + } + } +} + +/// Takes an identifier and struct definition, builds an evmc_create_* function for FFI. +fn build_create_fn(names: &VMNameSet) -> proc_macro2::TokenStream { + let type_ident = names.get_type_as_ident(); + let fn_ident = names.get_lowercase_as_ident_prepend("evmc_create_"); + + let static_version_ident = names.get_caps_as_ident_append("_VERSION"); + let static_name_ident = names.get_caps_as_ident_append("_NAME"); + + quote! { + #[no_mangle] + extern "C" fn #fn_ident() -> *const ::evmc_vm::ffi::evmc_instance { + let new_instance = ::evmc_vm::ffi::evmc_instance { + abi_version: ::evmc_vm::ffi::EVMC_ABI_VERSION as i32, + destroy: Some(__evmc_destroy), + execute: Some(__evmc_execute), + get_capabilities: Some(__evmc_get_capabilities), + set_option: None, + set_tracer: None, + name: ::std::ffi::CString::new(#static_name_ident).expect("Failed to build VM name string").into_raw() as *const i8, + version: ::std::ffi::CString::new(#static_version_ident).expect("Failed to build VM version string").into_raw() as *const i8, + }; + + unsafe { + ::evmc_vm::EvmcContainer::into_ffi_pointer(Box::new(::evmc_vm::EvmcContainer::<#type_ident>::new(new_instance))) + } + } + } +} + +/// Builds a callback to dispose of the VM instance +fn build_destroy_fn(names: &VMNameSet) -> proc_macro2::TokenStream { + let type_ident = names.get_type_as_ident(); + + quote! { + extern "C" fn __evmc_destroy(instance: *mut ::evmc_vm::ffi::evmc_instance) { + unsafe { + ::evmc_vm::EvmcContainer::<#type_ident>::from_ffi_pointer(instance); + } + } + } +} + +fn build_execute_fn(names: &VMNameSet) -> proc_macro2::TokenStream { + let type_name_ident = names.get_type_as_ident(); + + quote! { + extern "C" fn __evmc_execute( + instance: *mut ::evmc_vm::ffi::evmc_instance, + context: *mut ::evmc_vm::ffi::evmc_context, + rev: ::evmc_vm::ffi::evmc_revision, + msg: *const ::evmc_vm::ffi::evmc_message, + code: *const u8, + code_size: usize + ) -> ::evmc_vm::ffi::evmc_result + { + assert!(!msg.is_null()); + assert!(!context.is_null()); + assert!(!instance.is_null()); + assert!(!code.is_null()); + + let execution_context = unsafe { + ::evmc_vm::ExecutionContext::new( + msg.as_ref().expect("EVMC message is null"), + context.as_mut().expect("EVMC context is null") + ) + }; + + let code_ref: &[u8] = unsafe { + ::std::slice::from_raw_parts(code, code_size) + }; + + let container = unsafe { + ::evmc_vm::EvmcContainer::<#type_name_ident>::from_ffi_pointer(instance) + }; + + let result = container.execute(code_ref, &execution_context); + + unsafe { + ::evmc_vm::EvmcContainer::into_ffi_pointer(container); + } + + result.into() + } + } +} diff --git a/bindings/rust/evmc-vm/Cargo.toml b/bindings/rust/evmc-vm/Cargo.toml index ea8dc3452..5f8ac3f69 100644 --- a/bindings/rust/evmc-vm/Cargo.toml +++ b/bindings/rust/evmc-vm/Cargo.toml @@ -5,7 +5,7 @@ [package] name = "evmc-vm" version = "6.3.0-dev" -authors = ["Alex Beregszaszi "] +authors = ["Alex Beregszaszi ", "Jake Lang "] license = "Apache-2.0" repository = "https://github.com/ethereum/evmc" description = "Bindings to EVMC (VM specific)" diff --git a/bindings/rust/evmc-vm/src/container.rs b/bindings/rust/evmc-vm/src/container.rs new file mode 100644 index 000000000..833b31bba --- /dev/null +++ b/bindings/rust/evmc-vm/src/container.rs @@ -0,0 +1,40 @@ +/* EVMC: Ethereum Client-VM Connector API. + * Copyright 2019 The EVMC Authors. + * Licensed under the Apache License, Version 2.0. + */ + +use crate::EvmcVm; +use crate::ExecutionContext; +use crate::ExecutionResult; + +/// Container struct for EVMC instances and user-defined data. +pub struct EvmcContainer { + instance: ::evmc_sys::evmc_instance, + vm: T, +} + +impl EvmcContainer { + /// Basic constructor. + pub fn new(_instance: ::evmc_sys::evmc_instance) -> Self { + Self { + instance: _instance, + vm: T::init(), + } + } + + /// Take ownership of the given pointer and return a box. + pub unsafe fn from_ffi_pointer(instance: *mut ::evmc_sys::evmc_instance) -> Box { + assert!(!instance.is_null(), "from_ffi_pointer received NULL"); + Box::from_raw(instance as *mut EvmcContainer) + } + + /// Convert boxed self into an FFI pointer, surrendering ownership of the heap data. + pub unsafe fn into_ffi_pointer(boxed: Box) -> *const ::evmc_sys::evmc_instance { + Box::into_raw(boxed) as *const ::evmc_sys::evmc_instance + } + + // TODO: Maybe this can just be done with the Deref trait. + pub fn execute(&self, code: &[u8], context: &ExecutionContext) -> ExecutionResult { + self.vm.execute(code, context) + } +} diff --git a/bindings/rust/evmc-vm/src/lib.rs b/bindings/rust/evmc-vm/src/lib.rs index 706efd7f7..8da3b026c 100644 --- a/bindings/rust/evmc-vm/src/lib.rs +++ b/bindings/rust/evmc-vm/src/lib.rs @@ -3,11 +3,15 @@ * Licensed under the Apache License, Version 2.0. */ -pub extern crate evmc_sys; +mod container; + +pub use container::EvmcContainer; pub use evmc_sys as ffi; -// TODO: Add convenient helpers for evmc_execute -// TODO: Add a derive macro here for creating evmc_create +pub trait EvmcVm { + fn init() -> Self; + fn execute(&self, code: &[u8], context: &ExecutionContext) -> ExecutionResult; +} /// EVMC result structure. pub struct ExecutionResult { @@ -338,7 +342,6 @@ extern "C" fn release_stack_result(result: *const ffi::evmc_result) { #[cfg(test)] mod tests { use super::*; - use evmc_sys as ffi; #[test] fn new_result() { diff --git a/examples/example-rust-vm/Cargo.toml b/examples/example-rust-vm/Cargo.toml index 2c2c6adae..0e35029ef 100644 --- a/examples/example-rust-vm/Cargo.toml +++ b/examples/example-rust-vm/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "example-rust-vm" version = "0.1.0" -authors = ["Alex Beregszaszi "] +authors = ["Alex Beregszaszi ", "Jake Lang "] edition = "2018" [lib] @@ -9,4 +9,6 @@ name = "examplerustvm" crate-type = ["staticlib", "dylib"] [dependencies] +evmc-sys = { path = "../../bindings/rust/evmc-sys" } evmc-vm = { path = "../../bindings/rust/evmc-vm" } +evmc-declare = { path = "../../bindings/rust/evmc-declare" } diff --git a/examples/example-rust-vm/src/lib.rs b/examples/example-rust-vm/src/lib.rs index 9a8419c31..b62622837 100644 --- a/examples/example-rust-vm/src/lib.rs +++ b/examples/example-rust-vm/src/lib.rs @@ -1,58 +1,26 @@ -extern crate evmc_vm; +/* EVMC: Ethereum Client-VM Connector API. + * Copyright 2019 The EVMC Authors. + * Licensed under the Apache License, Version 2.0. + */ +use evmc_declare::evmc_declare_vm; use evmc_vm::*; -extern "C" fn execute( - instance: *mut ffi::evmc_instance, - context: *mut ffi::evmc_context, - rev: ffi::evmc_revision, - msg: *const ffi::evmc_message, - code: *const u8, - code_size: usize, -) -> ffi::evmc_result { - let execution_ctx = unsafe { - ExecutionContext::new( - msg.as_ref().expect("tester passed nullptr as message"), - context.as_mut().expect("tester passed nullptr as context"), - ) - }; - let is_create = execution_ctx.get_message().kind == ffi::evmc_call_kind::EVMC_CREATE; +#[evmc_declare_vm("ExampleRustVM", "evm")] +pub struct ExampleRustVM; - if is_create { - ExecutionResult::failure().into() - } else { - ExecutionResult::success(66, Some(vec![0xc0, 0xff, 0xee])).into() +impl EvmcVm for ExampleRustVM { + fn init() -> Self { + ExampleRustVM {} } -} -extern "C" fn get_capabilities( - instance: *mut ffi::evmc_instance, -) -> ffi::evmc_capabilities_flagset { - ffi::evmc_capabilities::EVMC_CAPABILITY_EVM1 as u32 -} + fn execute(&self, code: &[u8], context: &ExecutionContext) -> ExecutionResult { + let is_create = context.get_message().kind == evmc_sys::evmc_call_kind::EVMC_CREATE; -extern "C" fn destroy(instance: *mut ffi::evmc_instance) { - drop(unsafe { Box::from_raw(instance) }) -} - -#[no_mangle] -pub extern "C" fn evmc_create_examplerustvm() -> *const ffi::evmc_instance { - let ret = ffi::evmc_instance { - abi_version: ffi::EVMC_ABI_VERSION as i32, - destroy: Some(destroy), - execute: Some(execute), - get_capabilities: Some(get_capabilities), - set_option: None, - set_tracer: None, - name: { - let c_str = - std::ffi::CString::new("ExampleRustVM").expect("Failed to build EVMC name string"); - c_str.into_raw() as *const i8 - }, - version: { - let c_str = std::ffi::CString::new("1.0").expect("Failed to build EVMC version string"); - c_str.into_raw() as *const i8 - }, - }; - Box::into_raw(Box::new(ret)) + if is_create { + ExecutionResult::failure() + } else { + ExecutionResult::success(66, Some(vec![0xc0, 0xff, 0xee])) + } + } }