evmc_declare/
lib.rs

1/* EVMC: Ethereum Client-VM Connector API.
2 * Copyright 2019 The EVMC Authors.
3 * Licensed under the Apache License, Version 2.0.
4 */
5
6//! evmc-declare is an attribute-style procedural macro to be used for automatic generation of FFI
7//! code for the EVMC API with minimal boilerplate.
8//!
9//! evmc-declare can be used by applying its attribute to any struct which implements the `EvmcVm`
10//! trait, from the evmc-vm crate.
11//!
12//! The macro takes three arguments: a valid UTF-8 stylized VM name, a comma-separated list of
13//! capabilities, and a version string.
14//!
15//! # Example
16//! ```
17//! #[evmc_declare::evmc_declare_vm("This is an example VM name", "ewasm, evm", "1.2.3-custom")]
18//! pub struct ExampleVM;
19//!
20//! impl evmc_vm::EvmcVm for ExampleVM {
21//!     fn init() -> Self {
22//!             ExampleVM {}
23//!     }
24//!
25//!     fn execute(&self, revision: evmc_vm::ffi::evmc_revision, code: &[u8], message: &evmc_vm::ExecutionMessage, context: Option<&mut evmc_vm::ExecutionContext>) -> evmc_vm::ExecutionResult {
26//!             evmc_vm::ExecutionResult::success(1337, None)
27//!     }
28//! }
29//! ```
30
31// Set a higher recursion limit because parsing certain token trees might fail with the default of 64.
32#![recursion_limit = "256"]
33
34extern crate proc_macro;
35
36use heck::ShoutySnakeCase;
37use heck::SnakeCase;
38use proc_macro::TokenStream;
39use quote::quote;
40use syn::parse_macro_input;
41use syn::spanned::Spanned;
42use syn::AttributeArgs;
43use syn::Ident;
44use syn::ItemStruct;
45use syn::Lit;
46use syn::LitInt;
47use syn::LitStr;
48use syn::NestedMeta;
49
50struct VMNameSet {
51    type_name: String,
52    name_allcaps: String,
53    name_lowercase: String,
54}
55
56struct VMMetaData {
57    capabilities: u32,
58    // Not included in VMNameSet because it is parsed from the meta-item arguments.
59    name_stylized: String,
60    custom_version: String,
61}
62
63#[allow(dead_code)]
64impl VMNameSet {
65    fn new(ident: String) -> Self {
66        let caps = ident.to_shouty_snake_case();
67        let lowercase = ident
68            .to_snake_case()
69            .chars()
70            .filter(|c| *c != '_')
71            .collect();
72        VMNameSet {
73            type_name: ident,
74            name_allcaps: caps,
75            name_lowercase: lowercase,
76        }
77    }
78
79    /// Return a reference to the struct name, as a string.
80    fn get_type_name(&self) -> &String {
81        &self.type_name
82    }
83
84    /// Return a reference to the name in shouty snake case.
85    fn get_name_caps(&self) -> &String {
86        &self.name_allcaps
87    }
88
89    /// Return a reference to the name in lowercase, with all underscores removed. (Used for
90    /// symbols like evmc_create_vmname)
91    fn get_name_lowercase(&self) -> &String {
92        &self.name_lowercase
93    }
94
95    /// Get the struct's name as an explicit identifier to be interpolated with quote.
96    fn get_type_as_ident(&self) -> Ident {
97        Ident::new(&self.type_name, self.type_name.span())
98    }
99
100    /// Get the lowercase name appended with arbitrary text as an explicit ident.
101    fn get_lowercase_as_ident_append(&self, suffix: &str) -> Ident {
102        let concat = format!("{}{}", &self.name_lowercase, suffix);
103        Ident::new(&concat, self.name_lowercase.span())
104    }
105
106    /// Get the lowercase name prepended with arbitrary text as an explicit ident.
107    fn get_lowercase_as_ident_prepend(&self, prefix: &str) -> Ident {
108        let concat = format!("{}{}", prefix, &self.name_lowercase);
109        Ident::new(&concat, self.name_lowercase.span())
110    }
111
112    /// Get the lowercase name appended with arbitrary text as an explicit ident.
113    fn get_caps_as_ident_append(&self, suffix: &str) -> Ident {
114        let concat = format!("{}{}", &self.name_allcaps, suffix);
115        Ident::new(&concat, self.name_allcaps.span())
116    }
117}
118
119impl VMMetaData {
120    fn new(args: AttributeArgs) -> Self {
121        assert_eq!(args.len(), 3, "Incorrect number of arguments supplied");
122
123        let vm_name_meta = &args[0];
124        let vm_capabilities_meta = &args[1];
125        let vm_version_meta = &args[2];
126
127        let vm_name_string = match vm_name_meta {
128            NestedMeta::Lit(lit) => {
129                if let Lit::Str(s) = lit {
130                    // Add a null terminator here to ensure that it is handled correctly when
131                    // converted to a C String.
132                    let mut ret = s.value().to_string();
133                    ret.push('\0');
134                    ret
135                } else {
136                    panic!("Literal argument type mismatch")
137                }
138            }
139            _ => panic!("Argument 1 must be a string literal"),
140        };
141
142        let vm_capabilities_string = match vm_capabilities_meta {
143            NestedMeta::Lit(lit) => {
144                if let Lit::Str(s) = lit {
145                    s.value().to_string()
146                } else {
147                    panic!("Literal argument type mismatch")
148                }
149            }
150            _ => panic!("Argument 2 must be a string literal"),
151        };
152
153        // Parse the individual capabilities out of the list and prepare a capabilities flagset.
154        // Prune spaces and underscores here to make a clean comma-separated list.
155        let capabilities_list_pruned: String = vm_capabilities_string
156            .chars()
157            .filter(|c| *c != '_' && *c != ' ')
158            .collect();
159        let capabilities_flags = {
160            let mut ret: u32 = 0;
161            for capability in capabilities_list_pruned.split(",") {
162                match capability {
163                    "evm" => ret |= 1,
164                    "ewasm" => ret |= 1 << 1,
165                    "precompiles" => ret |= 1 << 2,
166                    _ => panic!("Invalid capability specified."),
167                }
168            }
169            ret
170        };
171
172        let vm_version_string: String = if let NestedMeta::Lit(lit) = vm_version_meta {
173            match lit {
174                // Add a null terminator here to ensure that it is handled correctly when
175                // converted to a C String.
176                Lit::Str(s) => {
177                    let mut ret = s.value().to_string();
178                    ret.push('\0');
179                    ret
180                }
181                _ => panic!("Literal argument type mismatch"),
182            }
183        } else {
184            panic!("Argument 3 must be a string literal")
185        };
186
187        // Make sure that the only null byte is the terminator we inserted in each string.
188        assert_eq!(vm_name_string.matches('\0').count(), 1);
189        assert_eq!(vm_version_string.matches('\0').count(), 1);
190
191        VMMetaData {
192            capabilities: capabilities_flags,
193            name_stylized: vm_name_string,
194            custom_version: vm_version_string,
195        }
196    }
197
198    fn get_capabilities(&self) -> u32 {
199        self.capabilities
200    }
201
202    fn get_name_stylized_nulterm(&self) -> &String {
203        &self.name_stylized
204    }
205
206    fn get_custom_version_nulterm(&self) -> &String {
207        &self.custom_version
208    }
209}
210
211#[proc_macro_attribute]
212pub fn evmc_declare_vm(args: TokenStream, item: TokenStream) -> TokenStream {
213    // First, try to parse the input token stream into an AST node representing a struct
214    // declaration.
215    let input: ItemStruct = parse_macro_input!(item as ItemStruct);
216
217    // Extract the identifier of the struct from the AST node.
218    let vm_type_name: String = input.ident.to_string();
219
220    // Build the VM name set.
221    let names = VMNameSet::new(vm_type_name);
222
223    // Parse the arguments for the macro.
224    let meta_args = parse_macro_input!(args as AttributeArgs);
225    let vm_data = VMMetaData::new(meta_args);
226
227    // Get all the tokens from the respective helpers.
228    let static_data_tokens = build_static_data(&names, &vm_data);
229    let capabilities_tokens = build_capabilities_fn(vm_data.get_capabilities());
230    let create_tokens = build_create_fn(&names);
231    let destroy_tokens = build_destroy_fn(&names);
232    let execute_tokens = build_execute_fn(&names);
233
234    let quoted = quote! {
235        #input
236        #static_data_tokens
237        #capabilities_tokens
238        #create_tokens
239        #destroy_tokens
240        #execute_tokens
241    };
242
243    quoted.into()
244}
245
246/// Generate tokens for the static data associated with an EVMC VM.
247fn build_static_data(names: &VMNameSet, metadata: &VMMetaData) -> proc_macro2::TokenStream {
248    // Stitch together the VM name and the suffix _NAME
249    let static_name_ident = names.get_caps_as_ident_append("_NAME");
250    let static_version_ident = names.get_caps_as_ident_append("_VERSION");
251
252    // Turn the stylized VM name and version into string literals.
253    let stylized_name_literal = LitStr::new(
254        metadata.get_name_stylized_nulterm().as_str(),
255        metadata.get_name_stylized_nulterm().as_str().span(),
256    );
257
258    // Turn the version into a string literal.
259    let version_string = metadata.get_custom_version_nulterm();
260    let version_literal = LitStr::new(version_string.as_str(), version_string.as_str().span());
261
262    quote! {
263        static #static_name_ident: &'static str = #stylized_name_literal;
264        static #static_version_ident: &'static str = #version_literal;
265    }
266}
267
268/// Takes a capabilities flag and builds the evmc_get_capabilities callback.
269fn build_capabilities_fn(capabilities: u32) -> proc_macro2::TokenStream {
270    let capabilities_string = capabilities.to_string();
271    let capabilities_literal = LitInt::new(&capabilities_string, capabilities.span());
272
273    quote! {
274        extern "C" fn __evmc_get_capabilities(instance: *mut ::evmc_vm::ffi::evmc_vm) -> ::evmc_vm::ffi::evmc_capabilities_flagset {
275            #capabilities_literal
276        }
277    }
278}
279
280/// Takes an identifier and struct definition, builds an evmc_create_* function for FFI.
281fn build_create_fn(names: &VMNameSet) -> proc_macro2::TokenStream {
282    let type_ident = names.get_type_as_ident();
283    let fn_ident = names.get_lowercase_as_ident_prepend("evmc_create_");
284
285    let static_version_ident = names.get_caps_as_ident_append("_VERSION");
286    let static_name_ident = names.get_caps_as_ident_append("_NAME");
287
288    // Note: we can get CStrs unchecked because we did the checks on instantiation of VMMetaData.
289    quote! {
290        #[no_mangle]
291        extern "C" fn #fn_ident() -> *const ::evmc_vm::ffi::evmc_vm {
292            let new_instance = ::evmc_vm::ffi::evmc_vm {
293                abi_version: ::evmc_vm::ffi::EVMC_ABI_VERSION as i32,
294                destroy: Some(__evmc_destroy),
295                execute: Some(__evmc_execute),
296                get_capabilities: Some(__evmc_get_capabilities),
297                set_option: None,
298                name: unsafe { ::std::ffi::CStr::from_bytes_with_nul_unchecked(#static_name_ident.as_bytes()).as_ptr() as *const i8 },
299                version: unsafe { ::std::ffi::CStr::from_bytes_with_nul_unchecked(#static_version_ident.as_bytes()).as_ptr() as *const i8 },
300            };
301
302            let container = ::evmc_vm::EvmcContainer::<#type_ident>::new(new_instance);
303
304            unsafe {
305                // Release ownership to EVMC.
306                ::evmc_vm::EvmcContainer::into_ffi_pointer(container)
307            }
308        }
309    }
310}
311
312/// Builds a callback to dispose of the VM instance.
313fn build_destroy_fn(names: &VMNameSet) -> proc_macro2::TokenStream {
314    let type_ident = names.get_type_as_ident();
315
316    quote! {
317        extern "C" fn __evmc_destroy(instance: *mut ::evmc_vm::ffi::evmc_vm) {
318            if instance.is_null() {
319                // This is an irrecoverable error that violates the EVMC spec.
320                std::process::abort();
321            }
322            unsafe {
323                // Acquire ownership from EVMC. This will deallocate it also at the end of the scope.
324                ::evmc_vm::EvmcContainer::<#type_ident>::from_ffi_pointer(instance);
325            }
326        }
327    }
328}
329
330/// Builds the main execution entry point.
331fn build_execute_fn(names: &VMNameSet) -> proc_macro2::TokenStream {
332    let type_name_ident = names.get_type_as_ident();
333
334    quote! {
335        extern "C" fn __evmc_execute(
336            instance: *mut ::evmc_vm::ffi::evmc_vm,
337            host: *const ::evmc_vm::ffi::evmc_host_interface,
338            context: *mut ::evmc_vm::ffi::evmc_host_context,
339            revision: ::evmc_vm::ffi::evmc_revision,
340            msg: *const ::evmc_vm::ffi::evmc_message,
341            code: *const u8,
342            code_size: usize
343        ) -> ::evmc_vm::ffi::evmc_result
344        {
345            use evmc_vm::EvmcVm;
346
347            // TODO: context is optional in case of the "precompiles" capability
348            if instance.is_null() || msg.is_null() || (code.is_null() && code_size != 0) {
349                // These are irrecoverable errors that violate the EVMC spec.
350                std::process::abort();
351            }
352
353            assert!(!instance.is_null());
354            assert!(!msg.is_null());
355
356            let execution_message: ::evmc_vm::ExecutionMessage = unsafe {
357                msg.as_ref().expect("EVMC message is null").into()
358            };
359
360            let empty_code = [0u8;0];
361            let code_ref: &[u8] = if code.is_null() {
362                assert_eq!(code_size, 0);
363                &empty_code
364            } else {
365                unsafe {
366                    ::std::slice::from_raw_parts(code, code_size)
367                }
368            };
369
370            let container = unsafe {
371                // Acquire ownership from EVMC.
372                ::evmc_vm::EvmcContainer::<#type_name_ident>::from_ffi_pointer(instance)
373            };
374
375            let result = ::std::panic::catch_unwind(|| {
376                if host.is_null() {
377                    container.execute(revision, code_ref, &execution_message, None)
378                } else {
379                    let mut execution_context = unsafe {
380                        ::evmc_vm::ExecutionContext::new(
381                            host.as_ref().expect("EVMC host is null"),
382                            context,
383                        )
384                    };
385                    container.execute(revision, code_ref, &execution_message, Some(&mut execution_context))
386                }
387            });
388
389            let result = if result.is_err() {
390                // Consider a panic an internal error.
391                ::evmc_vm::ExecutionResult::new(::evmc_vm::ffi::evmc_status_code::EVMC_INTERNAL_ERROR, 0, None)
392            } else {
393                result.unwrap()
394            };
395
396            unsafe {
397                // Release ownership to EVMC.
398                ::evmc_vm::EvmcContainer::into_ffi_pointer(container);
399            }
400
401            result.into()
402        }
403    }
404}