commit 36b95584667e3e03f11641bba83a753f02ee18a7 Author: vms Date: Mon Oct 7 14:01:20 2019 +0300 add sdk 0.1.9 version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0474e94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +Cargo.lock + +# IDE metadate +.idea +.vscode +*.iml + +# MacOS folder metadata +.DS_Store diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5018354 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "fluence" +version = "0.1.9" # remember to update html_root_url +description = "Fluence backend SDK for developing backend applications for the Fluence network" +documentation = "https://docs.rs/fluence/" +repository = "https://github.com/fluencelabs/rust-sdk" +authors = ["Fluence Labs"] +readme = "Readme.md" +keywords = ["fluence", "sdk", "webassembly"] +categories = ["api-bindings", "wasm"] +license = "Apache-2.0" +maintenance = { status = "actively-developed" } + +[package.metadata.docs.rs] # https://docs.rs/about +all-features = true + +[lib] +path = "src/lib.rs" + +[dependencies] +fluence-sdk-macro = { path = "crates/macro", version = "=0.1.9" } +fluence-sdk-main = { path = "crates/main", version = "=0.1.9" } + +[features] +default = ["export_allocator"] +# Turn on a compilation for the module that contains Wasm logger realization. +wasm_logger = ["fluence-sdk-main/wasm_logger"] + +# Turn on a compilation for the module that contains the standard implementation of allocate/deallocate functions. +export_allocator = ["fluence-sdk-main/export_allocator"] + +[workspace] +members = [ + "crates/main", + "crates/macro", + ] diff --git a/README.md b/README.md new file mode 100644 index 0000000..66b6810 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## Rust backend SDK + +This SDK intended to run backend application on the Fluence network. More information about usage and some internals could found in [docs](https://fluence.dev/docs/rust-sdk). diff --git a/crates/macro/Cargo.toml b/crates/macro/Cargo.toml new file mode 100644 index 0000000..79b6f2a --- /dev/null +++ b/crates/macro/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "fluence-sdk-macro" +version = "0.1.9" # remember to update html_root_url +edition = "2018" +description = "Definition of `#[invoke_handler]` attribute" +documentation = "https://docs.rs/fluence/fluence-sdk-macro" +repository = "https://github.com/fluencelabs/rust-sdk/crates/macro" +authors = ["Fluence Labs"] +keywords = ["fluence", "sdk", "webassembly", "procedural_macros"] +categories = ["api-bindings", "wasm"] +license = "Apache-2.0" +maintenance = { status = "actively-developed" } + +[package.metadata.docs.rs] # https://docs.rs/about +all-features = true + +[lib] +proc-macro = true + +[dependencies] +syn = { version = '0.15.44', features = ['full'] } +quote = "0.6.13" +proc-macro2 = "0.4" +fluence-sdk-main = { path = "../main", version = "=0.1.9" } diff --git a/crates/macro/src/lib.rs b/crates/macro/src/lib.rs new file mode 100644 index 0000000..ff09c9f --- /dev/null +++ b/crates/macro/src/lib.rs @@ -0,0 +1,242 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +//! This module defines an `invocation_handler` attribute procedural macro. It can be used to +//! simplify the signature of the main module invocation handler: +//! +//! ``` +//! use fluence::sdk::*; +//! +//! #[invocation_handler] +//! fn greeting(name: String) -> String { +//! format!("Hello from Fluence to {}", name) +//! } +//! ``` +//! +//! To use this macro with a function `f` certain conditions must be met: +//! 1. `f` shouldn't have more than one input argument. +//! 2. `f` shouldn't be `unsafe`, `const`, generic, have custom ABI linkage or variadic param. +//! 3. The type of `f` input (if it presents) and output parameters should be one from +//! {String, Vec} set. +//! 4. `f` shouldn't have the name `invoke`. +//! +//! For troubleshooting and macros debugging [cargo expand](https://github.com/dtolnay/cargo-expand) +//! can be used. +//! +//! Internally this macro creates a new function `invoke` that converts a raw argument to the +//! appropriate format, calls `f` and then writes `f` result via `memory::write_response_to_mem` to +//! module memory. So to use this crate apart from `fluence` `fluence_sdk_main` has to be imported. +//! +//! The macro also has the `init_fn` and `side_modules` attributes. The first one that can be used +//! for specifying initialization function name. This function is called only once at the first +//! call of the invoke function. It can be used like this: +//! +//! ``` +//! use fluence::sdk::*; +//! use log::info; +//! +//! fn init() { +//! logger::WasmLogger::init_with_level(log::Level::Info).is_ok() +//! } +//! +//! #[invocation_handler(init_fn = init)] +//! fn greeting(name: String) -> String { +//! info!("{} has been successfully greeted", name); +//! format!("Hello from Fluence to {}", name) +//! } +//! ``` +//! +//! The second macro could be used for generate API to connect with side modules like SQlite and +//! Redis. It can be used like this: +//! ``` +//! use fluence::sdk::*; +//! +//! #[invocation_handler(side_modules = (sqlite, redis))] +//! fn greeting(name: String) -> String { +//! sqlite::call("SELECT * from users"); +//! sqlite::call("GET user"); +//! format!("Hello from Fluence to {}", name) +//! } +//! ``` +//! +//! # Examples +//! +//! Please find more examples [here](https://github.com/fluencelabs/tutorials). + +#![doc(html_root_url = "https://docs.rs/fluence-sdk-macro/0.1.9")] +#![deny( + dead_code, + nonstandard_style, + unused_imports, + unused_mut, + unused_variables, + unused_unsafe, + unreachable_patterns +)] +#![recursion_limit = "128"] + +extern crate proc_macro; +mod macro_attr_parser; +mod macro_input_parser; + +use crate::macro_attr_parser::{generate_side_modules_glue_code, HandlerAttrs}; +use crate::macro_input_parser::{InputTypeGenerator, ParsedType, ReturnTypeGenerator}; +use proc_macro::TokenStream; +use quote::quote; +use syn::spanned::Spanned; +use syn::{parse::Error, parse_macro_input, ItemFn}; + +fn invoke_handler_impl( + attr: proc_macro2::TokenStream, + fn_item: syn::ItemFn, +) -> syn::Result { + let ItemFn { + constness, + unsafety, + abi, + ident, + decl, + .. + } = &fn_item; + + if let Err(e) = (|| { + if let Some(constness) = constness { + return Err(Error::new( + constness.span, + "The invocation handler shouldn't be constant", + )); + } + if let Some(unsafety) = unsafety { + return Err(Error::new( + unsafety.span, + "The invocation handler shouldn't be unsage", + )); + } + if let Some(abi) = abi { + return Err(Error::new( + abi.extern_token.span, + "The invocation handler shouldn't have any custom linkage", + )); + } + if !decl.generics.params.is_empty() || decl.generics.where_clause.is_some() { + return Err(Error::new( + decl.fn_token.span, + "The invocation handler shouldn't use template parameters", + )); + } + if let Some(variadic) = decl.variadic { + return Err(Error::new( + variadic.spans[0], + "The invocation handler shouldn't use variadic interface", + )); + } + Ok(()) + })() { + return Err(e); + } + + let input_type = match decl.inputs.len() { + 0 => ParsedType::Empty, + 1 => ParsedType::from_fn_arg(decl.inputs.first().unwrap().into_value())?, + _ => { + return Err(Error::new( + decl.inputs.span(), + "The invocation handler shouldn't have more than one argument", + )) + }, + }; + let output_type = ParsedType::from_return_type(&decl.output)?; + if output_type == ParsedType::Empty { + return Err(Error::new( + decl.output.span(), + "The invocation handler should have the return value", + )); + } + + let prolog = input_type.generate_fn_prolog(); + let prolog = match input_type { + ParsedType::Empty => quote! { + #prolog + + let result = #ident(); + }, + _ => quote! { + #prolog + + let result = #ident(arg); + }, + }; + let epilog = output_type.generate_fn_epilog(); + + let attrs = syn::parse2::(attr)?; + let raw_init_fn_name = attrs.init_fn_name(); + let raw_side_modules_list = attrs.side_modules(); + + let resulted_invoke = match raw_init_fn_name { + Some(init_fn_name) => { + let init_fn_name = syn::parse_str::(init_fn_name)?; + quote! { + #fn_item + + static mut __FLUENCE_SDK_IS_INITED_d28374a960b570e5db00dfe7a0c7b93: bool = false; + + #[no_mangle] + pub unsafe fn invoke(ptr: *mut u8, len: usize) -> std::ptr::NonNull { + if !__FLUENCE_SDK_IS_INITED_d28374a960b570e5db00dfe7a0c7b93 { + #init_fn_name(); + unsafe { __FLUENCE_SDK_IS_INITED_d28374a960b570e5db00dfe7a0c7b93 = true; } + } + + #prolog + + #epilog + } + } + }, + None => quote! { + #fn_item + + #[no_mangle] + pub unsafe fn invoke(ptr: *mut u8, len: usize) -> std::ptr::NonNull { + #prolog + + #epilog + } + }, + }; + + match raw_side_modules_list { + Some(side_modules) => { + let side_modules_glue_code = generate_side_modules_glue_code(side_modules)?; + Ok(quote! { + #side_modules_glue_code + #resulted_invoke + }) + }, + _ => Ok(resulted_invoke), + } +} + +#[proc_macro_attribute] +pub fn invocation_handler(attr: TokenStream, input: TokenStream) -> TokenStream { + let fn_item = parse_macro_input!(input as ItemFn); + match invoke_handler_impl(attr.into(), fn_item) { + Ok(v) => v, + // converts syn:error to proc_macro2::TokenStream + Err(e) => e.to_compile_error(), + } + // converts proc_macro2::TokenStream to proc_macro::TokenStream + .into() +} diff --git a/crates/macro/src/macro_attr_parser.rs b/crates/macro/src/macro_attr_parser.rs new file mode 100644 index 0000000..a4f10e7 --- /dev/null +++ b/crates/macro/src/macro_attr_parser.rs @@ -0,0 +1,217 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use quote::quote; +use syn::export::TokenStream2; +use syn::parse::{Parse, ParseStream}; + +pub struct HandlerAttrs { + handler_attrs: Vec, +} + +pub enum HandlerAttr { + InitFnName(String), + SideModules(Vec), +} + +impl HandlerAttrs { + pub fn init_fn_name(&self) -> Option<(&str)> { + self.handler_attrs + .iter() + .filter_map(|attr| match attr { + HandlerAttr::InitFnName(name) => Some(&name[..]), + _ => None, + }) + .next() + } + + pub fn side_modules(&self) -> Option<(&Vec)> { + self.handler_attrs + .iter() + .filter_map(|attr| match attr { + HandlerAttr::SideModules(modules) => Some(modules), + _ => None, + }) + .next() + } +} + +impl Default for HandlerAttrs { + fn default() -> Self { + HandlerAttrs { + handler_attrs: Vec::new(), + } + } +} + +impl Parse for HandlerAttrs { + fn parse(input: ParseStream) -> syn::Result { + let mut attrs = HandlerAttrs::default(); + if input.is_empty() { + return Ok(attrs); + } + + let attr_opts = + syn::punctuated::Punctuated::::parse_terminated(input)?; + attrs.handler_attrs = attr_opts.into_iter().collect(); + + Ok(attrs) + } +} + +impl Parse for HandlerAttr { + fn parse(input: ParseStream) -> syn::Result { + // trying to parse the `init_fn`/`side_modules`/... tokens + let attr_name = input.step(|cursor| match cursor.ident() { + Some((ident, rem)) => Ok((ident, rem)), + None => Err(cursor.error("Expected a valid ident")), + })?; + + match attr_name.to_string().as_str() { + "init_fn" => { + // trying to parse `=` + input.parse::<::syn::token::Eq>()?; + + // trying to parse a init function name + match input.parse::() { + Ok(init_fn_name) => Ok(HandlerAttr::InitFnName(init_fn_name.to_string())), + Err(_) => Err(syn::Error::new( + attr_name.span(), + "Expected a function name", + )), + } + }, + + "side_modules" => { + // trying to parse `=` + input.parse::<::syn::token::Eq>()?; + + // check for parens + let raw_side_modules_list = match syn::group::parse_parens(&input) { + Ok(parens) => parens.content, + _ => { + match input.parse::() { + Ok(module_name) => return Ok(HandlerAttr::SideModules(vec![module_name.to_string()])), + Err(_) => return Err(syn::Error::new( + attr_name.span(), + "Expected a module name name", + )), + } + } + }; + + let raw_side_modules_opts = + syn::punctuated::Punctuated::::parse_terminated( + &raw_side_modules_list, + )?; + + let side_modules = raw_side_modules_opts + .iter() + .map(|c| c.to_string()) + .collect(); + + Ok(HandlerAttr::SideModules(side_modules)) + }, + + _ => Err(syn::Error::new( + attr_name.span(), + "Expected a `side_modules` or `init_fn` tokens in invocation_handler macros attributes", + )), + } + } +} + +pub fn generate_side_modules_glue_code(side_modules_list: &[String]) -> syn::Result { + let mut modules_glue_code = quote!(); + for module_name in side_modules_list { + let allocate_fn_name = format!("{}_allocate", module_name); + let deallocate_fn_name = format!("{}_deallocate", module_name); + let invoke_fn_name = format!("{}_invoke", module_name); + let load_fn_name = format!("{}_load", module_name); + let store_fn_name = format!("{}_store", module_name); + let module_name_ident = syn::parse_str::(&module_name)?; + + modules_glue_code = quote! { + pub mod #module_name_ident { + #[link(wasm_import_module = #module_name)] + extern "C" { + // Allocate chunk of module memory, and return a pointer to that region + #[link_name = #allocate_fn_name] + pub fn allocate(size: usize) -> i32; + + // Deallocate chunk of module memory after it's not used anymore + #[link_name = #deallocate_fn_name] + pub fn deallocate(ptr: i32, size: usize); + + // Call module's invocation handler with data specified by pointer and size + #[link_name = #invoke_fn_name] + pub fn invoke(ptr: i32, size: usize) -> i32; + + // Read 1 byte from ptr location of module memory + #[link_name = #load_fn_name] + pub fn load(ptr: i32) -> u8; + + // Put 1 byte at ptr location in module memory + #[link_name = #store_fn_name] + pub fn store(ptr: *mut i32, byte: u8); + } + + // Execute query on module + pub fn call(request: &[u8]) -> Vec { + unsafe { + // Allocate memory for the query in module + let query_ptr = allocate(request.len()); + + // Store query in module's memory + for (i, byte) in request.iter().enumerate() { + let ptr = query_ptr + i as i32; + store(ptr as *mut i32, *byte); + } + + // Execute the query, and get pointer to the result + let response_ptr = invoke(query_ptr, request.len()); + + // First 4 bytes at result_ptr location encode result size, read that first + let mut response_size: usize = 0; + for byte_id in 0..3 { + let ptr = response_ptr + byte_id as i32; + let b = load(ptr) as usize; + response_size = response_size + (b << (8 * byte_id)); + } + // Now we know exact size of the query execution result + + // Read query execution result byte-by-byte + let mut response_bytes = vec![0; response_size as usize]; + for byte_id in 0..response_size { + let ptr = response_ptr + (byte_id + 4) as i32; + let b = load(ptr); + response_bytes[byte_id as usize] = b; + } + + // Deallocate response + deallocate(response_ptr, response_size + 4); + + response_bytes + } + } + } + + #modules_glue_code + } + } + + Ok(modules_glue_code) +} diff --git a/crates/macro/src/macro_input_parser.rs b/crates/macro/src/macro_input_parser.rs new file mode 100644 index 0000000..acb6b83 --- /dev/null +++ b/crates/macro/src/macro_input_parser.rs @@ -0,0 +1,186 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use quote::quote; +use syn::{parse::Error, spanned::Spanned}; + +#[derive(PartialEq)] +pub enum ParsedType { + Utf8String, + ByteVector, + Empty, +} + +impl ParsedType { + pub fn from_type(input_type: &syn::Type) -> syn::Result { + // parses generic param T in Vec to string representation + fn parse_vec_bracket(args: &syn::PathArguments) -> syn::Result { + // checks that T is angle bracketed + let generic_arg = match args { + syn::PathArguments::AngleBracketed(args) => Ok(args), + _ => Err(Error::new( + args.span(), + "It has to be a bracketed value after Vec", + )), + }?; + + let arg = generic_arg.args.first().ok_or_else(|| { + Error::new( + generic_arg.span(), + "Unsuitable type in Vec brackets - only Vec is supported", + ) + })?; + let arg_val = arg.value(); + + // converts T to syn::Type + let arg_type = match arg_val { + syn::GenericArgument::Type(ty) => Ok(ty), + _ => Err(Error::new( + arg_val.span(), + "Unsuitable type in Vec brackets - only Vec is supported", + )), + }?; + + // converts T to syn::path + let arg_path = match arg_type { + syn::Type::Path(path) => Ok(&path.path), + _ => Err(Error::new( + arg_type.span(), + "Unsuitable type in Vec brackets - only Vec is supported", + )), + }?; + + // There could be cases like Vec + // that why this segments count check is needed + if arg_path.segments.len() != 1 { + return Err(Error::new( + arg_path.span(), + "Unsuitable type in Vec brackets - only Vec is supported", + )); + } + + // converts T to String + let arg_segment = arg_path.segments.first().ok_or_else(|| { + Error::new( + arg_path.span(), + "Unsuitable type in Vec brackets - only Vec is supported", + ) + })?; + let arg_segment = arg_segment.value(); + + Ok(arg_segment.ident.to_string()) + } + + let path = match input_type { + syn::Type::Path(path) => Ok(&path.path), + _ => Err(Error::new( + input_type.span(), + "Incorrect argument type - only Vec and String are supported", + )), + }?; + + let type_segment = path + .segments + // argument can be given in full path form: ::std::string::String + // that why the last one used + .last() + .ok_or_else(|| { + Error::new( + path.span(), + "The invocation handler should have a non-empty input argument type", + ) + })?; + let type_segment = type_segment.value(); + + match type_segment.ident.to_string().as_str() { + "String" => Ok(ParsedType::Utf8String), + "Vec" => match parse_vec_bracket(&type_segment.arguments) { + Ok(value) => match value.as_str() { + "u8" => Ok(ParsedType::ByteVector), + _ => Err(Error::new( + value.span(), + "Unsuitable type in Vec brackets - only Vec is supported", + )), + }, + Err(e) => Err(e), + }, + _ => Err(Error::new( + type_segment.span(), + "Only String and Vec input types are supported (also, it is possible not to specify the input argument)", + )), + } + } + + pub fn from_fn_arg(fn_arg: &syn::FnArg) -> syn::Result { + match fn_arg { + syn::FnArg::Captured(arg) => ParsedType::from_type(&arg.ty), + _ => Err(Error::new(fn_arg.span(), "Unknown argument")), + } + } + + pub fn from_return_type(ret_type: &syn::ReturnType) -> syn::Result { + match ret_type { + syn::ReturnType::Type(_, t) => ParsedType::from_type(t.as_ref()), + syn::ReturnType::Default => Ok(ParsedType::Empty), + } + } +} + +pub trait InputTypeGenerator { + fn generate_fn_prolog(&self) -> proc_macro2::TokenStream; +} + +pub trait ReturnTypeGenerator { + fn generate_fn_epilog(&self) -> proc_macro2::TokenStream; +} + +impl InputTypeGenerator for ParsedType { + fn generate_fn_prolog(&self) -> proc_macro2::TokenStream { + match self { + ParsedType::Utf8String => quote! { + let arg = memory::read_request_from_mem(ptr, len); + // TODO: it should be changed to more accurate check + let arg = String::from_utf8(arg).unwrap(); + }, + ParsedType::ByteVector => quote! { + let arg = memory::read_request_from_mem(ptr, len); + }, + ParsedType::Empty => quote! { + // it is needed to delete memory occupied by the input argument + // this way does it without any additional imports of the export allocator module + let arg = memory::read_request_from_mem(ptr, len); + }, + } + } +} + +impl ReturnTypeGenerator for ParsedType { + fn generate_fn_epilog(&self) -> proc_macro2::TokenStream { + match self { + ParsedType::Utf8String => quote! { + memory::write_response_to_mem( + result.as_bytes() + ) + .expect("Putting result string to memory has failed") + }, + ParsedType::ByteVector => quote! { + memory::write_response_to_mem(&result[..]) + .expect("Putting result vector to memory has failed") + }, + ParsedType::Empty => quote! {}, + } + } +} diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml new file mode 100644 index 0000000..8a8a1b7 --- /dev/null +++ b/crates/main/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "fluence-sdk-main" +version = "0.1.9" # remember to update html_root_url +edition = "2018" +description = "Rust SDK for writing applications for Fluence" +documentation = "https://docs.rs/fluence/fluence-sdk-macro" +repository = "https://github.com/fluencelabs/rust-sdk/crates/main" +authors = ["Fluence Labs"] +keywords = ["fluence", "sdk", "webassembly"] +categories = ["api-bindings", "wasm"] +license = "Apache-2.0" +maintenance = { status = "actively-developed" } + +[package.metadata.docs.rs] # https://docs.rs/about +all-features = true + +[lib] +path = "src/lib.rs" +crate-type = ["rlib"] + +[dependencies] +log = { version = "0.4", features = ["std"] } +syn = { version = '0.15.0', features = ['full'] } + +[dev-dependencies] +simple_logger = "1.0" # used in doc test +lazy_static = "1.3.0" # used in doc test + +[features] +# Turn on the Wasm logger. +wasm_logger = [] + +# Make this module as side module. +side_module = [] + +# Notify the VM that this module expects Ethereum blocks. +expect_eth = [] + +# Turn on the module contained implementation of allocate/deallocate functions. +export_allocator = [] diff --git a/crates/main/src/eth/mod.rs b/crates/main/src/eth/mod.rs new file mode 100644 index 0000000..3b9e16f --- /dev/null +++ b/crates/main/src/eth/mod.rs @@ -0,0 +1,24 @@ +/* + * Copyright 2019 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Notifies the VM that this module expects Ethereum blocks. +//! +//! This module contains functions for loading from and storing to memory. + +/// A temporary solution to let users configure their ethereum expectations via WASM bytecode: +/// to enable block uploading via invoke method, just export expects_eth method from the module. +#[no_mangle] +pub unsafe fn expects_eth() {} diff --git a/crates/main/src/export_allocator/mod.rs b/crates/main/src/export_allocator/mod.rs new file mode 100644 index 0000000..fd44721 --- /dev/null +++ b/crates/main/src/export_allocator/mod.rs @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! This module provides default implementations of [`allocate`] and [`deallocate`] functions that +//! can be used for array passing and returning. +//! +//! [`allocate`]: fn.allocate.html +//! [`deallocate`]: fn.deallocate.html + +use crate::memory::{alloc, dealloc}; +use std::num::NonZeroUsize; +use std::ptr::NonNull; + +/// Allocates memory area of specified size and returns its address. +/// Used from the host environment for memory allocation while parameters passing. +#[no_mangle] +pub unsafe fn allocate(size: usize) -> NonNull { + let non_zero_size = NonZeroUsize::new(size) + .unwrap_or_else(|| panic!("[Error] Allocation of zero bytes is not allowed.")); + alloc(non_zero_size).unwrap_or_else(|_| panic!("[Error] Allocation of {} bytes failed.", size)) +} + +/// Deallocates memory area for provided memory pointer and size. +/// Used from the host environment for memory deallocation after reading results of function from +/// Wasm memory. +#[no_mangle] +pub unsafe fn deallocate(ptr: NonNull, size: usize) { + let non_zero_size = NonZeroUsize::new(size) + .unwrap_or_else(|| panic!("[Error] Deallocation of zero bytes is not allowed.")); + dealloc(ptr, non_zero_size) + .unwrap_or_else(|_| panic!("[Error] Deallocate failed for ptr={:?} size={}.", ptr, size)); +} diff --git a/crates/main/src/lib.rs b/crates/main/src/lib.rs new file mode 100644 index 0000000..ce70243 --- /dev/null +++ b/crates/main/src/lib.rs @@ -0,0 +1,47 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! The main part of Fluence backend SDK. Contains `export_allocator` (is turned on by the +//! `export_allocator` feature), `logger` (is turned on by the `wasm_logger` feature), and `memory` +//! modules. + +#![doc(html_root_url = "https://docs.rs/fluence-sdk-main/0.1.9")] +#![feature(allocator_api)] +#![deny( + dead_code, + nonstandard_style, + unused_imports, + unused_mut, + unused_variables, + unused_unsafe, + unreachable_patterns +)] + +extern crate core; + +pub mod memory; + +#[cfg(feature = "wasm_logger")] +pub mod logger; + +#[cfg(feature = "side_module")] +pub mod side_module; + +#[cfg(feature = "expect_eth")] +pub mod eth; + +#[cfg(feature = "export_allocator")] +pub mod export_allocator; diff --git a/crates/main/src/logger/mod.rs b/crates/main/src/logger/mod.rs new file mode 100644 index 0000000..9653437 --- /dev/null +++ b/crates/main/src/logger/mod.rs @@ -0,0 +1,170 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! This module enables log messages from the Wasm side. It is implemented as a logging facade for +//! crate [`log`]. To enable this module in your project please specify `wasm_logger` feature of +//! `fluence_sdk`. +//! +//! Note that this module works only for the Wasm environments and Fluence `WasmVm` - with this +//! feature set it is possible to compile applications only for Wasm targets such as +//! `wasm32-unknown-unknown`, `wasm32-wasi`. (please refer to the first example to find out a way +//! to avoid it). +//! +//! This feature should be used only for debugging purposes, you can find more info in the +//! [`backend app debugging`] section of the Fluence guide. +//! +//! # Examples +//! +//! This example initializes [`WasmLogger`] if target arch is Wasm and [`simple_logger`] otherwise. +//! Macros from crate [`log`] are used as a logging facade. +//! +//! ``` +//! use fluence::sdk::*; +//! use log::{error, trace}; +//! use simple_logger; +//! +//! fn main() { +//! if cfg!(target_arch = "wasm32") { +//! logger::WasmLogger::init_with_level(log::Level::Info).unwrap(); +//! } else { +//! simple_logger::init_with_level(log::Level::Info).unwrap(); +//! } +//! +//! error!("This message will be logged."); +//! trace!("This message will not be logged."); +//! } +//! +//! ``` +//! +//! This example provides methods for [`WasmLogger`] initialization only for Wasm target without +//! specifying logger level: +//! +//! ``` +//! use fluence::sdk::*; +//! use log::info; +//! +//! /// This method initializes WasmLogger and should be called at the start of the application. +//! #[no_mangle] +//! #[cfg(target_arch = "wasm32")] +//! fn init_logger() { +//! logger::WasmLogger::init().unwrap(); +//! info!("If you can see this message that logger was successfully initialized."); +//! } +//! +//! ``` +//! +//! [`WasmLogger`]: struct.WasmLogger.html +//! [`log`]: https://docs.rs/log +//! [`simple_logger`]: https://docs.rs/simple_logger +//! [`static_lazy`]: https://docs.rs/lazy_static +//! [`lazy_static::initialize()`]: https://docs.rs/lazy_static/1.3.0/lazy_static/fn.initialize.html +//! [`backend app debugging`]: https://fluence.dev/docs/debugging + +extern crate log; + +/// The Wasm Logger. +/// +/// This struct implements the [`Log`] trait from the [`log`] crate, which allows it to act as a +/// logger. +/// +/// For initialization of WasmLogger as a default logger please see [`init()`] +/// and [`init_with_level()`] +/// +/// [log-crate-url]: https://docs.rs/log/ +/// [`Log`]: https://docs.rs/log/0.4.6/log/trait.Log.html +/// [`init_with_level()`]: struct.WasmLogger.html#method.init_with_level +/// [`init()`]: struct.WasmLogger.html#method.init +pub struct WasmLogger { + level: log::Level, +} + +impl WasmLogger { + /// Initializes the global logger with a [`WasmLogger`] instance with + /// `max_log_level` set to a specific log level. + /// + /// ``` + /// # use fluence::sdk::*; + /// # use log::info; + /// # + /// # fn main() { + /// if cfg!(target_arch = "wasm32") { + /// logger::WasmLogger::init_with_level(log::Level::Error).unwrap(); + /// } + /// error!("This message will be logged."); + /// info!("This message will not be logged."); + /// # } + /// ``` + pub fn init_with_level(level: log::Level) -> Result<(), log::SetLoggerError> { + let logger = WasmLogger { level }; + log::set_boxed_logger(Box::new(logger))?; + log::set_max_level(level.to_level_filter()); + Ok(()) + } + + /// Initializes the global logger with a [`WasmLogger`] instance with `max_log_level` set to + /// `Level::Info`. + /// + /// ``` + /// # use fluence::sdk::*; + /// # use log::info; + /// # + /// # fn main() { + /// if cfg!(target_arch = "wasm32") { + /// fluence::logger::WasmLogger::init().unwrap(); + /// } + /// + /// error!("This message will be logged."); + /// trace!("This message will not be logged."); + /// # } + /// ``` + pub fn init() -> Result<(), log::SetLoggerError> { + WasmLogger::init_with_level(log::Level::Info) + } +} + +impl log::Log for WasmLogger { + #[inline] + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= self.level + } + + #[inline] + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let log_msg = format!( + "{:<5} [{}] {}\n", + record.level().to_string(), + record.module_path().unwrap_or_default(), + record.args() + ); + + unsafe { log_utf8_string(log_msg.as_ptr() as i32, log_msg.len() as i32) }; + } + + // in our case flushing is performed by the VM itself + #[inline] + fn flush(&self) {} +} + +/// logger is a module provided by a VM that can process log messages. +#[link(wasm_import_module = "logger")] +extern "C" { + // Writes a byte string of size bytes that starts from ptr to a logger + fn log_utf8_string(ptr: i32, size: i32); +} diff --git a/crates/main/src/memory/errors.rs b/crates/main/src/memory/errors.rs new file mode 100644 index 0000000..6fa88b2 --- /dev/null +++ b/crates/main/src/memory/errors.rs @@ -0,0 +1,77 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Contains definitions for memory errors. + +use core::alloc::LayoutErr; +use std::alloc::AllocErr; +use std::error::Error; +use std::fmt::{self, Display}; +use std::io; + +#[derive(Debug, Clone)] +pub struct MemError(String); + +impl Display for MemError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "MemError({:?})", self) + } +} + +impl Error for MemError {} + +impl MemError { + pub fn new(message: &str) -> Self { + MemError(message.to_string()) + } + + pub fn from_err(err: &E) -> Self { + MemError::new(&err.to_string()) + } +} + +/// Creates `From` instance for MemError for each specified error types. +/// +/// Example: +/// +/// ``` +/// // usage of macro: +/// +/// mem_error_from![io::Error] +/// +/// // code will be generated: +/// +/// impl From for MemError { +/// fn from(err: io::Error) -> Self { +/// MemError::from_err(err) +/// } +/// } +/// +/// ``` +// TODO: move this macro to utils or use 'quick-error' or 'error-chain' crates +macro_rules! mem_error_from { + ( $( $x:ty );* ) => { + $( + impl From<$x> for MemError { + fn from(err: $x) -> Self { + MemError::from_err(&err) + } + } + )* + } +} + +mem_error_from! [LayoutErr; AllocErr; io::Error]; diff --git a/crates/main/src/memory/mod.rs b/crates/main/src/memory/mod.rs new file mode 100644 index 0000000..5e2ea06 --- /dev/null +++ b/crates/main/src/memory/mod.rs @@ -0,0 +1,187 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Raw API for dealing with Wasm memory. +//! +//! This module contains functions for memory initializing and manipulating. + +pub mod errors; + +use self::errors::MemError; +use std::alloc::{Alloc, Global, Layout}; +use std::mem; +use std::num::NonZeroUsize; +use std::ptr::{self, NonNull}; + +/// Result type for this module. +pub type MemResult = ::std::result::Result; + +/// Bytes count occupied by a length of resulted array. +pub const RESPONSE_SIZE_BYTES: usize = 4; + +/// Allocates memory area of specified size and returns its address. Actually is just a wrapper for +/// [`GlobalAlloc::alloc`]. +/// +/// # Safety +/// +/// See [`GlobalAlloc::alloc`]. +/// +/// [`GlobalAlloc::alloc`]: https://doc.rust-lang.org/core/alloc/trait.GlobalAlloc.html#tymethod.alloc +/// +pub unsafe fn alloc(size: NonZeroUsize) -> MemResult> { + let layout: Layout = Layout::from_size_align(size.get(), mem::align_of::())?; + Global.alloc(layout).map_err(Into::into) +} + +/// Deallocates memory area for current memory pointer and size. Actually is just a wrapper for +/// [`GlobalAlloc::dealloc`]. +/// +/// # Safety +/// +/// See [`GlobalAlloc::dealloc`]. +/// +/// [`GlobalAlloc::dealloc`]: https://doc.rust-lang.org/core/alloc/trait.GlobalAlloc.html#tymethod.dealloc +/// +pub unsafe fn dealloc(ptr: NonNull, size: NonZeroUsize) -> MemResult<()> { + let layout = Layout::from_size_align(size.get(), mem::align_of::())?; + Global.dealloc(ptr, layout); + Ok(()) +} + +/// Allocates 'RESPONSE_SIZE_BYTES + result.len()' bytes, writes length of the result as little +/// endian RESPONSE_SIZE_BYTES bytes, and finally writes content of 'result'. So the final layout of +/// the result in memory is following: +/// ` +/// | array_length: RESPONSE_SIZE_BYTES bytes (little-endian) | array: $array_length bytes | +/// ` +/// This function should normally be used to return result of `invoke` function. Vm wrapper +/// expects result in this format. +/// +pub unsafe fn write_response_to_mem(result: &[u8]) -> MemResult> { + let result_len = result.len(); + let total_len = result_len + .checked_add(RESPONSE_SIZE_BYTES) + .ok_or_else(|| MemError::new("usize overflow occurred"))?; + + // converts array size to bytes in little-endian + let len_as_bytes: [u8; RESPONSE_SIZE_BYTES] = mem::transmute((result_len as u32).to_le()); + + // allocates a new memory region for the result + let result_ptr = alloc(NonZeroUsize::new_unchecked(total_len))?; + + // copies length of array to memory + ptr::copy_nonoverlapping( + len_as_bytes.as_ptr(), + result_ptr.as_ptr(), + RESPONSE_SIZE_BYTES, + ); + + // copies array to memory + ptr::copy_nonoverlapping( + result.as_ptr(), + result_ptr.as_ptr().add(RESPONSE_SIZE_BYTES), + result_len, + ); + + Ok(result_ptr) +} + +/// Reads array of bytes from a given `ptr` that must to have `len` bytes size. +/// +/// # Safety +/// +/// The ownership of `ptr` is effectively (without additional allocation) transferred to the +/// resulted `Vec` which can be then safely deallocated, reallocated or so on. +/// **There have to be only one instance of `Vec` constructed from one of such pointer since +/// there isn't any memory copied.** +/// +pub unsafe fn read_request_from_mem(ptr: *mut u8, len: usize) -> Vec { + Vec::from_raw_parts(ptr, len, len) +} + +/// Reads `u32` (assuming that it is given in little endianness order) from a specified pointer. +pub unsafe fn read_len(ptr: *mut u8) -> u32 { + let mut len_as_bytes: [u8; RESPONSE_SIZE_BYTES] = [0; RESPONSE_SIZE_BYTES]; + ptr::copy_nonoverlapping(ptr, len_as_bytes.as_mut_ptr(), RESPONSE_SIZE_BYTES); + mem::transmute(len_as_bytes) +} + +#[cfg(test)] +pub mod test { + use super::*; + use std::num::NonZeroUsize; + + pub unsafe fn read_result_from_mem(ptr: NonNull) -> MemResult> { + // read string length from current pointer + let input_len = super::read_len(ptr.as_ptr()) as usize; + + let total_len = RESPONSE_SIZE_BYTES + .checked_add(input_len) + .ok_or_else(|| MemError::new("usize overflow occurred"))?; + + // creates object from raw bytes + let mut input = Vec::from_raw_parts(ptr.as_ptr(), total_len, total_len); + + // drains RESPONSE_SIZE_BYTES from the beginning of created vector, it allows to effectively + // skips (without additional allocations) length of the result. + { + input.drain(0..RESPONSE_SIZE_BYTES); + } + + Ok(input) + } + + #[test] + fn alloc_dealloc_test() { + unsafe { + let size = NonZeroUsize::new_unchecked(123); + let ptr = alloc(size).unwrap(); + assert_eq!(dealloc(ptr, size).unwrap(), ()); + } + } + + #[test] + fn write_and_read_str_test() { + unsafe { + let src_str = "some string Ω"; + + let ptr = write_response_to_mem(src_str.as_bytes()).unwrap(); + let result_array = read_result_from_mem(ptr).unwrap(); + let result_str = String::from_utf8(result_array).unwrap(); + assert_eq!(src_str, result_str); + } + } + + /// Creates a big string with pattern "Q..Q" of the specified length. + fn create_big_str(len: usize) -> String { + unsafe { String::from_utf8_unchecked(vec!['Q' as u8; len]) } + } + + #[test] + fn lot_of_write_and_read_str_test() { + unsafe { + let mb_str = create_big_str(1024 * 1024); + + // writes and read 1mb string (takes several seconds) + for _ in 1..10_000 { + let ptr = write_response_to_mem(mb_str.as_bytes()).unwrap(); + let result_array = read_result_from_mem(ptr).unwrap(); + let result_str = String::from_utf8(result_array).unwrap(); + assert_eq!(mb_str, result_str); + } + } + } +} diff --git a/crates/main/src/side_module/mod.rs b/crates/main/src/side_module/mod.rs new file mode 100644 index 0000000..85c1a1e --- /dev/null +++ b/crates/main/src/side_module/mod.rs @@ -0,0 +1,33 @@ +/* + * Copyright 2019 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Allows a module be side module. +//! +//! This module contains functions for loading from and storing to memory. + +/// Stores one byte into module memory by the given address. +/// Could be used from other modules for parameter passing. +#[no_mangle] +pub unsafe fn store(address: *mut u8, byte: u8) { + *address = byte; +} + +/// Loads one byte from module memory by the given address. +/// Could be used from other modules for obtaining results. +#[no_mangle] +pub unsafe fn load(address: *mut u8) -> u8 { + return *address; +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..63156a8 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +match_block_trailing_comma = true +binop_separator = "Back" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..494b302 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,56 @@ +/* + * Copyright 2018 Fluence Labs Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Rust backend SDK for writing applications for Fluence. This crate is just a wrapper for two +//! other crates: `main` and `macro`. The `main` crate is used for all memory relative operations +//! and logging, while the `macro` crate contains the invocation macro to simplify entry point +//! functions. +//! +//! By default this crate turns on export-allocator feature of the `main` crate, to disable it +//! please import this crate with `default-features = false`. +//! +#![doc(html_root_url = "https://docs.rs/fluence/0.1.9")] +#![feature(allocator_api)] +#![deny( + dead_code, + nonstandard_style, + unused_imports, + unused_mut, + unused_variables, + unused_unsafe, + unreachable_patterns +)] + +extern crate fluence_sdk_macro; +extern crate fluence_sdk_main; + +/// A module which should be typically globally imported: +/// +/// ``` +/// use fluence::sdk::*; +/// ``` +pub mod sdk { + // TODO: need to introduce macros to avoid code duplication with crates/main/lib.rs + pub use fluence_sdk_main::memory; + + #[cfg(feature = "wasm_logger")] + pub use fluence_sdk_main::logger; + + #[cfg(feature = "export_allocator")] + pub use fluence_sdk_main::export_allocator; + + pub use fluence_sdk_macro::invocation_handler; +}