├── .gitignore ├── Cargo.toml ├── README.md ├── src ├── api.rs ├── events.rs ├── executor.rs ├── lib.rs ├── main.rs ├── registry.rs ├── rejections.rs └── storage.rs └── tests └── api_tests.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-serverless" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | tokio = { version = "1.41.1", features = ["rt", "rt-multi-thread", "macros"] } 8 | serde_json = "1.0.133" 9 | warp = "0.3.7" 10 | serde = { version = "1.0", features = ["derive"] } 11 | sled = "0.34" 12 | wasmtime = "27.0.0" 13 | 14 | [lib] 15 | name = "serverless_rust" 16 | path = "src/lib.rs" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Rust Serverless Platform 3 | 4 | This project demonstrates how to build a serverless platform in Rust powered by WebAssembly. It allows dynamic function registration and invocation using WebAssembly modules. 5 | 6 | ## Features 7 | - **Dynamic Function Registration**: Register WebAssembly modules via a simple HTTP API. 8 | - **Dynamic Function Invocation**: Execute registered functions with runtime-supplied inputs. 9 | 10 | ## Tutorial 11 | A comprehensive tutorial on building this platform is available [here](https://luissoares.dev/building-a-rust-serverless-platform/). 12 | 13 | ## Setup 14 | 1. Clone the repository: 15 | ```bash 16 | git clone https://github.com/luishsr/rust-serverless.git 17 | cd rust-serverless 18 | ``` 19 | 2. Install dependencies and run the server: 20 | ```bash 21 | cargo run 22 | ``` 23 | 24 | ## Example Usage 25 | ### Register a Function 26 | ```bash 27 | curl -X POST http://127.0.0.1:3030/register -H "Content-Type: application/json" -d '{ 28 | "name": "add", 29 | "code": "(module (func (export \"add\") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add))" 30 | }' 31 | ``` 32 | 33 | ### Invoke the Function 34 | ```bash 35 | curl -X POST http://127.0.0.1:3030/invoke -H "Content-Type: application/json" -d '{ 36 | "name": "add", 37 | "input": [3, 7] 38 | }' 39 | ``` 40 | 41 | ## Tests 42 | Run the tests to verify the functionality: 43 | ```bash 44 | cargo test 45 | ``` 46 | 47 | ## License 48 | This project is licensed under the MIT License. 49 | 50 | ## Author 51 | Developed by [Luís Soares](https://github.com/luishsr). 52 | 53 | ## Complete Source Code 54 | The source code for this project is available on GitHub: 55 | [https://github.com/luishsr/rust-serverless](https://github.com/luishsr/rust-serverless) 56 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use warp::Filter; 2 | use std::sync::Arc; 3 | use crate::{executor, storage, rejections}; 4 | use serde_json::Value; 5 | 6 | pub fn server( 7 | storage: Arc, 8 | ) -> impl Filter + Clone { 9 | let register = warp::post() 10 | .and(warp::path("register")) 11 | .and(warp::body::json()) 12 | .and(with_storage(storage.clone())) 13 | .and_then(register_function); 14 | 15 | let invoke = warp::post() 16 | .and(warp::path("invoke")) 17 | .and(warp::body::json()) 18 | .and(with_storage(storage.clone())) 19 | .and_then(invoke_function); 20 | 21 | register.or(invoke) 22 | } 23 | 24 | fn with_storage( 25 | storage: Arc, 26 | ) -> impl Filter,), Error = std::convert::Infallible> + Clone { 27 | warp::any().map(move || storage.clone()) 28 | } 29 | 30 | async fn register_function( 31 | body: Value, 32 | storage: Arc, 33 | ) -> Result { 34 | let function_name = body["name"] 35 | .as_str() 36 | .ok_or_else(|| warp::reject::custom(rejections::InvalidParameter { 37 | message: "Missing or invalid 'name' parameter".to_string(), 38 | }))?; 39 | 40 | let code = body["code"] 41 | .as_str() 42 | .ok_or_else(|| warp::reject::custom(rejections::InvalidParameter { 43 | message: "Missing or invalid 'code' parameter".to_string(), 44 | }))?; 45 | 46 | storage 47 | .save_function(function_name.to_string(), code.to_string()) 48 | .map_err(|_| warp::reject::custom(rejections::InvalidParameter { 49 | message: "Failed to save function".to_string(), 50 | }))?; 51 | 52 | Ok(warp::reply::json(&format!("Function {} registered!", function_name))) 53 | } 54 | 55 | async fn invoke_function( 56 | body: Value, 57 | storage: Arc, 58 | ) -> Result { 59 | let function_name = body["name"] 60 | .as_str() 61 | .ok_or_else(|| warp::reject::custom(rejections::InvalidParameter { 62 | message: "Missing or invalid 'name' parameter".to_string(), 63 | }))?; 64 | 65 | let input = body["input"] 66 | .as_array() 67 | .ok_or_else(|| warp::reject::custom(rejections::InvalidParameter { 68 | message: "Missing or invalid 'input' parameter".to_string(), 69 | }))?; 70 | 71 | let code = storage.load_function(function_name).map_err(|_| { 72 | warp::reject::custom(rejections::NotFound { 73 | message: format!("Function '{}' not found", function_name), 74 | }) 75 | })?; 76 | 77 | let result = executor::execute(&code, function_name, input).map_err(|_| { 78 | warp::reject::custom(rejections::InvalidParameter { 79 | message: "Failed to execute function".to_string(), 80 | }) 81 | })?; 82 | 83 | Ok(warp::reply::json(&result)) 84 | } 85 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::mpsc; 2 | 3 | pub struct EventQueue { 4 | sender: mpsc::Sender, 5 | } 6 | 7 | pub struct Event { 8 | pub function_name: String, 9 | pub payload: String, 10 | } 11 | 12 | impl EventQueue { 13 | pub fn new(buffer_size: usize) -> (Self, mpsc::Receiver) { 14 | let (sender, receiver) = mpsc::channel(buffer_size); 15 | (Self { sender }, receiver) 16 | } 17 | 18 | pub async fn enqueue(&self, event: Event) { 19 | self.sender.send(event).await.unwrap(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/executor.rs: -------------------------------------------------------------------------------- 1 | use wasmtime::*; 2 | use serde_json::Value; 3 | 4 | pub fn execute(code: &str, function_name: &str, inputs: &[Value]) -> Result> { 5 | let engine = Engine::default(); 6 | let module = Module::new(&engine, code)?; 7 | let mut store = Store::new(&engine, ()); 8 | let instance = Instance::new(&mut store, &module, &[])?; 9 | 10 | let func = instance.get_func(&mut store, function_name) 11 | .ok_or_else(|| format!("Function '{}' not found in module", function_name))?; 12 | 13 | let func_ty = func.ty(&store); 14 | let params: Vec<_> = func_ty.params().collect(); 15 | let results: Vec<_> = func_ty.results().collect(); 16 | 17 | println!("Executing function: {}", function_name); 18 | println!("Inputs: {:?}", inputs); 19 | println!("Params: {:?}", params); 20 | println!("Results: {:?}", results); 21 | 22 | if params.len() != inputs.len() { 23 | return Err(format!( 24 | "Function '{}' expected {} arguments, but got {}", 25 | function_name, params.len(), inputs.len() 26 | ).into()); 27 | } 28 | 29 | let mut wasm_inputs = Vec::new(); 30 | for (param, input) in params.iter().zip(inputs.iter()) { 31 | let value = match (param, input) { 32 | (ValType::I32, Value::Number(n)) => Val::I32(n.as_i64().ok_or("Invalid i32")? as i32), 33 | (ValType::F32, Value::Number(n)) => Val::F32((n.as_f64().ok_or("Invalid f32")? as f32).to_bits()), 34 | (ValType::I64, Value::Number(n)) => Val::I64(n.as_i64().ok_or("Invalid i64")?), 35 | (ValType::F64, Value::Number(n)) => Val::F64(n.as_f64().ok_or("Invalid f64")?.to_bits()), 36 | _ => return Err(format!("Unsupported parameter type: {:?}", param).into()), 37 | }; 38 | wasm_inputs.push(value); 39 | } 40 | 41 | let mut wasm_results = vec![Val::I32(0); results.len()]; 42 | func.call(&mut store, &wasm_inputs, &mut wasm_results)?; 43 | 44 | if wasm_results.len() > 1 { 45 | return Err("Multiple return values are not supported yet".into()); 46 | } 47 | 48 | let result = match wasm_results.get(0) { 49 | Some(Val::I32(v)) => Value::Number((*v).into()), 50 | Some(Val::F32(v)) => Value::String(f32::from_bits(*v).to_string()), 51 | Some(Val::I64(v)) => Value::Number((*v).into()), 52 | Some(Val::F64(v)) => Value::String(f64::from_bits(*v).to_string()), 53 | None => Value::Null, 54 | _ => return Err("Unsupported return type".into()), 55 | }; 56 | 57 | Ok(result) 58 | } 59 | 60 | 61 | #[cfg(test)] 62 | mod tests { 63 | use super::*; 64 | use serde_json::Value; 65 | 66 | #[test] 67 | fn test_add_function() { 68 | let wasm_code = r#" 69 | (module 70 | (func (export "add") (param i32 i32) (result i32) 71 | local.get 1 72 | local.get 0 73 | i32.add 74 | ) 75 | ) 76 | "#; 77 | 78 | let result = execute(wasm_code, "add", &[Value::Number(3.into()), Value::Number(7.into())]) 79 | .expect("Execution failed"); 80 | assert_eq!(result, Value::Number(10.into())); 81 | } 82 | 83 | #[test] 84 | fn test_execute_valid_wasm() { 85 | let wasm_code = r#" 86 | (module 87 | (func (export "main") (result i32) 88 | (i32.const 42))) 89 | "#; 90 | 91 | let result = execute(wasm_code, "main", &[]).expect("Execution failed"); 92 | assert_eq!(result, Value::Number(42.into())); 93 | } 94 | 95 | #[test] 96 | fn test_execute_with_input() { 97 | let wasm_code = r#" 98 | (module 99 | (func (export "main") (param i32) (result i32) 100 | local.get 0)) 101 | "#; 102 | 103 | let result = execute(wasm_code, "main", &[Value::Number(21.into())]).expect("Execution failed"); 104 | assert_eq!(result, Value::Number(21.into())); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod storage; 3 | pub mod executor; 4 | pub mod registry; 5 | pub mod events; 6 | mod rejections; 7 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod executor; 3 | mod storage; 4 | mod rejections; 5 | 6 | use tokio; 7 | use std::sync::Arc; 8 | 9 | #[tokio::main] 10 | async fn main() { 11 | let storage = Arc::new(storage::Storage::init().expect("Failed to initialize storage")); 12 | warp::serve(api::server(storage)).run(([127, 0, 0, 1], 3030)).await; 13 | } 14 | -------------------------------------------------------------------------------- /src/registry.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::{Arc, Mutex}; 3 | 4 | #[derive(Default)] 5 | pub struct Registry { 6 | functions: Arc>>, 7 | } 8 | 9 | impl Registry { 10 | pub fn register(&self, name: &str, code: &str) { 11 | self.functions.lock().unwrap().insert(name.to_string(), code.to_string()); 12 | } 13 | 14 | pub fn get(&self, name: &str) -> Option { 15 | self.functions.lock().unwrap().get(name).cloned() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/rejections.rs: -------------------------------------------------------------------------------- 1 | use warp::reject::Reject; 2 | use std::fmt; 3 | 4 | // Define a custom rejection for invalid headers or parameters 5 | #[derive(Debug)] 6 | pub struct InvalidParameter { 7 | pub message: String, 8 | } 9 | 10 | impl Reject for InvalidParameter {} 11 | 12 | impl fmt::Display for InvalidParameter { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | write!(f, "{}", self.message) 15 | } 16 | } 17 | 18 | #[derive(Debug)] 19 | pub struct NotFound { 20 | pub message: String, 21 | } 22 | 23 | impl Reject for NotFound {} 24 | 25 | impl fmt::Display for NotFound { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | write!(f, "{}", self.message) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/storage.rs: -------------------------------------------------------------------------------- 1 | use sled::Db; 2 | 3 | pub struct Storage { 4 | db: Db, 5 | } 6 | 7 | impl Storage { 8 | pub fn init_with_path(path: &str) -> Result> { 9 | let db = sled::open(path)?; 10 | Ok(Self { db }) 11 | } 12 | 13 | pub fn init() -> Result> { 14 | Self::init_with_path("functions_db") 15 | } 16 | 17 | pub fn save_function(&self, name: String, code: String) -> Result<(), sled::Error> { 18 | self.db.insert(name, code.as_bytes())?; 19 | Ok(()) 20 | } 21 | 22 | pub fn load_function(&self, name: &str) -> Result { 23 | if let Some(code) = self.db.get(name)? { 24 | Ok(String::from_utf8(code.to_vec()).unwrap()) 25 | } else { 26 | Err(sled::Error::Io(std::io::Error::new( 27 | std::io::ErrorKind::NotFound, 28 | "Function not found", 29 | ))) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/api_tests.rs: -------------------------------------------------------------------------------- 1 | use warp::test::request; 2 | use serverless_rust::api; 3 | use serverless_rust::storage::Storage; 4 | use std::sync::Arc; 5 | 6 | #[tokio::test] 7 | async fn test_register_function() { 8 | let db_path = "test_db_register_function"; 9 | let storage = Arc::new(Storage::init_with_path(db_path).expect("Failed to initialize storage")); 10 | 11 | let body = r#" 12 | { 13 | "name": "factorial", 14 | "code": "(module (func (export \"main\") (param i32) (result i32) local.get 0))" 15 | } 16 | "#; 17 | 18 | let response = request() 19 | .method("POST") 20 | .path("/register") 21 | .body(body) 22 | .reply(&api::server(storage)) 23 | .await; 24 | 25 | assert_eq!(response.status(), 200, "Response: {:?}", response.body()); 26 | 27 | // Cleanup 28 | std::fs::remove_dir_all(db_path).expect("Failed to clean up test database"); 29 | } 30 | 31 | #[tokio::test] 32 | async fn test_invoke_function() { 33 | let db_path = "test_db_invoke_function"; 34 | let storage = Arc::new(Storage::init_with_path(db_path).expect("Failed to initialize storage")); 35 | 36 | storage 37 | .save_function( 38 | "factorial".to_string(), 39 | "(module (func (export \"main\") (param i32) (result i32) local.get 0))".to_string(), 40 | ) 41 | .expect("Failed to save function"); 42 | 43 | let body = r#" 44 | { 45 | "name": "factorial", 46 | "input": [5] 47 | } 48 | "#; 49 | 50 | let response = request() 51 | .method("POST") 52 | .path("/invoke") 53 | .body(body) 54 | .reply(&api::server(storage)) 55 | .await; 56 | 57 | assert_eq!(response.status(), 200, "Response: {:?}", response.body()); 58 | 59 | let body_str = std::str::from_utf8(response.body()).expect("Invalid UTF-8 in response body"); 60 | assert!(body_str.contains("5")); // Verify the result 61 | 62 | // Cleanup 63 | std::fs::remove_dir_all(db_path).expect("Failed to clean up test database"); 64 | } 65 | --------------------------------------------------------------------------------