rust: Procedural macro for automatic VM declaration

This commit is contained in:
Jake Lang 2019-03-13 20:47:55 +01:00 committed by Alex Beregszaszi
parent d7c6d08947
commit 2e14aa2d5c
13 changed files with 529 additions and 58 deletions

View File

@ -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}

View File

@ -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"
]

View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
/Cargo.lock

View File

@ -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 <jak3lang@gmail.com>"]
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" }

View File

@ -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)
}
}

3
bindings/rust/evmc-declare/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
**/*.rs.bk
/Cargo.lock

View File

@ -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 <jak3lang@gmail.com>"]
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

View File

@ -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<String>,
}
#[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<String> = 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<String> {
&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()
}
}
}

View File

@ -5,7 +5,7 @@
[package]
name = "evmc-vm"
version = "6.3.0-dev"
authors = ["Alex Beregszaszi <alex@rtfs.hu>"]
authors = ["Alex Beregszaszi <alex@rtfs.hu>", "Jake Lang <jak3lang@gmail.com>"]
license = "Apache-2.0"
repository = "https://github.com/ethereum/evmc"
description = "Bindings to EVMC (VM specific)"

View File

@ -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<T: EvmcVm + Sized> {
instance: ::evmc_sys::evmc_instance,
vm: T,
}
impl<T: EvmcVm + Sized> EvmcContainer<T> {
/// 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<Self> {
assert!(!instance.is_null(), "from_ffi_pointer received NULL");
Box::from_raw(instance as *mut EvmcContainer<T>)
}
/// Convert boxed self into an FFI pointer, surrendering ownership of the heap data.
pub unsafe fn into_ffi_pointer(boxed: Box<Self>) -> *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<Target = T> trait.
pub fn execute(&self, code: &[u8], context: &ExecutionContext) -> ExecutionResult {
self.vm.execute(code, context)
}
}

View File

@ -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() {

View File

@ -1,7 +1,7 @@
[package]
name = "example-rust-vm"
version = "0.1.0"
authors = ["Alex Beregszaszi <alex@rtfs.hu>"]
authors = ["Alex Beregszaszi <alex@rtfs.hu>", "Jake Lang <jak3lang@gmail.com>"]
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" }

View File

@ -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 {}
}
fn execute(&self, code: &[u8], context: &ExecutionContext) -> ExecutionResult {
let is_create = context.get_message().kind == evmc_sys::evmc_call_kind::EVMC_CREATE;
if is_create {
ExecutionResult::failure()
} else {
ExecutionResult::success(66, Some(vec![0xc0, 0xff, 0xee]))
}
}
}
extern "C" fn get_capabilities(
instance: *mut ffi::evmc_instance,
) -> ffi::evmc_capabilities_flagset {
ffi::evmc_capabilities::EVMC_CAPABILITY_EVM1 as u32
}
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))
}