Skip to content

Commit

Permalink
Procedural macro for automatic VM declaration
Browse files Browse the repository at this point in the history
  • Loading branch information
jakelang committed May 24, 2019
1 parent 0c49f7a commit 71685ca
Show file tree
Hide file tree
Showing 10 changed files with 536 additions and 164 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
10 changes: 10 additions & 0 deletions bindings/rust/evmc-declare-tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "evmc-declare-tests"
version = "0.1.0"
authors = ["Jake Lang <jak3lang@gmail.com>"]
edition = "2018"

[dependencies]
evmc-declare = { path = "../evmc-declare" }
evmc-sys = { path = "../evmc-sys" }
evmc-vm = { path = "../evmc-vm" }
20 changes: 20 additions & 0 deletions bindings/rust/evmc-declare-tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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")]
pub struct FooVM {
a: i32,
}

impl EvmcVm for FooVM {
fn init() -> Self {
FooVM { a: 105023 }
}

fn execute(&self, code: &[u8], context: &ExecutionContext) -> ExecutionResult {
ExecutionResult::new(evmc_sys::evmc_status_code::EVMC_SUCCESS, 235117, None)
}
}
17 changes: 17 additions & 0 deletions bindings/rust/evmc-declare/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "evmc-declare"
version = "0.1.0"
authors = ["Jake Lang <jak3lang@gmail.com>"]
edition = "2018"

[dependencies]
quote = "0.6.12"
heck = "0.3.1"
proc-macro2 = "0.4.29"

[dependencies.syn]
version = "0.15.33"
features = ["full"]

[lib]
proc-macro = true
310 changes: 310 additions & 0 deletions bindings/rust/evmc-declare/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
#![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,
}

#[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, "Incorrect number of arguments supplied");

let vm_name_meta = &args[0];
let vm_capabilities_meta = &args[1];

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")
}
}
NestedMeta::Meta(_) => 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()
} else {
panic!("Literal argument type mismatch")
}
}
NestedMeta::Meta(_) => panic!("Argument 2 must be a string literal"),
};

// Parse the individual capabilities out of the list and prepare a capabilities flagset.
let capabilities_list = vm_capabilities_string.split(", ");
let capabilities_flags = {
let mut ret: u32 = 0;
for capability in capabilities_list {
match capability {
"ewasm" => ret |= 0x1 << 1,
"evm" => ret |= 0x1,
_ => panic!("Invalid capability specified."),
}
}
ret
};

VMMetaData {
capabilities: capabilities_flags,
name_stylized: vm_name_string,
}
}

fn get_capabilities(&self) -> u32 {
self.capabilities
}

fn get_name_stylized(&self) -> &String {
&self.name_stylized
}
}

#[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);

let vm_name_stylized = vm_data.get_name_stylized();
let vm_capabilities = vm_data.get_capabilities();

// Get all the tokens from the respective helpers.
let static_data_tokens = build_static_data(&names, vm_name_stylized);
let capabilities_tokens = build_capabilities_fn(vm_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, name_stylized: &String) -> 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.
// FIXME: Not sure if the span of name.as_str() is the same as that of name.
let stylized_name_literal = LitStr::new(name_stylized.as_str(), name_stylized.as_str().span());

quote! {
static #static_name_ident: &'static str = #stylized_name_literal;
static #static_version_ident: &'static str = env!("CARGO_PKG_VERSION");
}
}

/// 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_sys::evmc_instance) -> ::evmc_sys::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_sys::evmc_instance {
let new_instance = ::evmc_sys::evmc_instance {
abi_version: ::evmc_sys::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_sys::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_sys::evmc_instance,
context: *mut ::evmc_sys::evmc_context,
rev: ::evmc_sys::evmc_revision,
msg: *const ::evmc_sys::evmc_message,
code: *const u8,
code_size: usize
) -> ::evmc_sys::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()
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_camel_to_lower() {
let a = String::from("FooBarBaz");
let b = a.to_snake_case();
assert_eq!(b, "foo_bar_baz");
let c: String = b.chars().filter(|c| *c != '_').collect();
assert_eq!(c, String::from("foobarbaz"));
}
}
1 change: 1 addition & 0 deletions bindings/rust/evmc-vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ edition = "2018"

[dependencies]
evmc-sys = { path = "../evmc-sys" }
evmc-declare = { path = "../evmc-declare" }
Loading

0 comments on commit 71685ca

Please sign in to comment.