├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── codegen │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── functions │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── metadata │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── runtime │ ├── Cargo.toml │ ├── src │ ├── host.rs │ ├── lib.rs │ ├── log.rs │ └── server.rs │ └── witx │ └── functions.witx ├── examples └── hello │ ├── Cargo.toml │ ├── README.md │ └── src │ └── lib.rs └── host ├── Cargo.toml └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "host", 4 | ] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wasmtime Functions 2 | 3 | A demonstration prototype serverless functions runtime built on [Wasmtime](https://github.com/bytecodealliance/wasmtime). 4 | 5 | ## Getting started 6 | 7 | Follow the directions in the [hello example](examples/hello/README.md) to get started. 8 | 9 | ## What is this? 10 | 11 | This is just the runtime for executing *serverless HTTP functions* implemented in [WebAssembly](https://webassembly.org/). 12 | 13 | The runtime is capable of instantiating a WebAssembly module and routing HTTP requests to the functions it exposes. 14 | 15 | ## What is this not? 16 | 17 | This **isn't** the *orchestration magic* one might expect from a functions as a service (FaaS) provider, such as on-demand provisioning, horizontal scaling, and load balancing of a serverless application. 18 | 19 | In truth, there's really not much "serverless" about what you'll find in this repository. 20 | 21 | ## Is this even useful? 22 | 23 | The runtime itself isn't terribly useful as the functions can only accept HTTP requests, do some computation, and then return a HTTP response. 24 | 25 | There is no integration with the various cloud services (e.g. Amazon S3, Azure CosmosDB, etc.) one would expect from a serverless application on popular FaaS offerings, such as Amazon Lambda and Azure Functions. 26 | 27 | In fact, the functions have no mechanism yet for doing network requests themselves. However, a simple HTTP client interface could be added to the runtime in the future. The HTTP client would then be the basis for the implementation of cloud service SDKs usable from the serverless application. 28 | 29 | That said, the runtime *is* a simple demonstration of the potential of using WebAssembly in the serverless space. 30 | -------------------------------------------------------------------------------- /crates/codegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmtime-functions-codegen" 3 | version = "0.1.0" 4 | authors = ["Peter Huene "] 5 | edition = "2018" 6 | 7 | [lib] 8 | proc-macro = true 9 | test = false 10 | 11 | [dependencies] 12 | syn = { version = "1.0.76", features = ["full"] } 13 | quote = "1.0.9" 14 | serde = { version = "1.0.130", features = ["derive"] } 15 | serde_json = "1.0.68" 16 | proc-macro2 = "1.0.29" 17 | heck = "0.3.3" 18 | -------------------------------------------------------------------------------- /crates/codegen/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Wasmtime Functions codegen crate. 2 | //! 3 | //! This crate is responsible for implementing the procedural macros used in Wasmtime Functions applications. 4 | //! 5 | //! There are two types of macros: 6 | //! 7 | //! * The `http` and verb (e.g. `get`, `post`, `delete`, etc.) macros that define a user's HTTP-triggered function. 8 | //! * The `env` macro that declares a required environment variable. 9 | //! 10 | //! Each macro expands to include a "descriptor" comprising a static array of bytes that is appended to a custom section 11 | //! in the resulting WebAssembly module. 12 | //! 13 | //! Depending on which macros are used, the following custom sections may be present in the WebAssembly module: 14 | //! 15 | //! * The `__functions` section that defines the metadata about user functions and how they can be triggered. 16 | //! * The `__vars` section that defines the metadata about the required environment variables for the application. 17 | //! 18 | //! The `__functions` section is required to run a Wasmtime Functions application, as without it there is nothing for the runtime to do. 19 | //! 20 | //! The `__vars` sections is optional. It is primarily used by the host to source the required 21 | //! environment variable values when running an application. 22 | 23 | #![deny(missing_docs)] 24 | 25 | extern crate proc_macro; 26 | 27 | use proc_macro::{Span, TokenStream}; 28 | use quote::quote; 29 | use serde::Serialize; 30 | use std::sync::atomic::{AtomicUsize, Ordering}; 31 | use syn::{ 32 | parse::{Parse, ParseStream}, 33 | parse_macro_input, 34 | spanned::Spanned, 35 | Error, FnArg, Ident, ItemFn, LitByteStr, LitStr, Result, Token, Type, 36 | }; 37 | 38 | #[derive(Clone, Copy, Eq, PartialEq, Serialize)] 39 | #[serde(rename_all = "UPPERCASE")] 40 | enum Method { 41 | Get, 42 | Head, 43 | Post, 44 | Put, 45 | Delete, 46 | Connect, 47 | Options, 48 | Trace, 49 | Patch, 50 | } 51 | 52 | #[derive(Serialize)] 53 | #[serde(rename_all = "camelCase", tag = "type")] 54 | enum FunctionTrigger { 55 | Http { path: String, methods: Vec }, 56 | } 57 | 58 | #[derive(Serialize)] 59 | #[serde(rename_all = "camelCase", tag = "type")] 60 | enum FunctionInput {} 61 | 62 | #[derive(Serialize)] 63 | #[serde(rename_all = "camelCase", tag = "type")] 64 | enum FunctionOutput { 65 | Http, 66 | } 67 | 68 | #[derive(Serialize)] 69 | #[serde(rename_all = "camelCase")] 70 | struct Function { 71 | name: String, 72 | trigger: FunctionTrigger, 73 | inputs: Vec, 74 | outputs: Vec, 75 | } 76 | 77 | fn parse_methods(s: &LitStr) -> Result> { 78 | let mut methods = Vec::new(); 79 | for m in s.value().split(',') { 80 | methods.push(match m.trim().to_lowercase().as_ref() { 81 | "get" => Method::Get, 82 | "head" => Method::Head, 83 | "post" => Method::Post, 84 | "put" => Method::Put, 85 | "delete" => Method::Delete, 86 | "connect" => Method::Connect, 87 | "options" => Method::Options, 88 | "trace" => Method::Trace, 89 | "path" => Method::Patch, 90 | _ => { 91 | return Err(Error::new( 92 | s.span(), 93 | format!("unsupported HTTP method '{}'", m), 94 | )) 95 | } 96 | }); 97 | } 98 | 99 | if methods.is_empty() { 100 | return Err(Error::new(s.span(), "at least one HTTP method is required")); 101 | } 102 | 103 | Ok(methods) 104 | } 105 | 106 | fn check_function_validity(func: &ItemFn) -> Result<()> { 107 | if let Some(constness) = func.sig.constness { 108 | return Err(Error::new(constness.span, "function cannot be const")); 109 | } 110 | 111 | if let Some(asyncness) = func.sig.asyncness { 112 | return Err(Error::new(asyncness.span, "function cannot be async")); 113 | } 114 | 115 | if let Some(abi) = &func.sig.abi { 116 | return Err(Error::new( 117 | abi.extern_token.span, 118 | "function cannot be extern", 119 | )); 120 | } 121 | 122 | if let Some(lt) = func.sig.generics.lt_token { 123 | return Err(Error::new(lt.spans[0], "function cannot be generic")); 124 | } 125 | 126 | if let Some(variadic) = &func.sig.variadic { 127 | return Err(Error::new( 128 | variadic.dots.spans[0], 129 | "function cannot be variadic", 130 | )); 131 | } 132 | 133 | Ok(()) 134 | } 135 | 136 | fn check_http_validity(func: &ItemFn) -> Result<()> { 137 | let inputs = &func.sig.inputs; 138 | if inputs.is_empty() { 139 | return Err(Error::new( 140 | func.sig.ident.span(), 141 | "function must have a single parameter of type 'Request'", 142 | )); 143 | } 144 | 145 | if inputs.len() > 1 { 146 | return Err(Error::new( 147 | inputs[1].span(), 148 | "function cannot have more than one parameter", 149 | )); 150 | } 151 | 152 | if let FnArg::Typed(arg) = &inputs[0] { 153 | if let Type::Path(ty) = &*arg.ty { 154 | if ty.qself.is_none() { 155 | if let Some(segment) = ty.path.segments.last() { 156 | if segment.ident == "Request" { 157 | return Ok(()); 158 | } 159 | } 160 | } 161 | } 162 | } 163 | 164 | Err(Error::new( 165 | inputs[0].span(), 166 | "parameter must be type 'Request'", 167 | )) 168 | } 169 | 170 | fn emit_descriptor(section: &str, name: &Ident, descriptor: &[u8]) -> proc_macro2::TokenStream { 171 | // As each descriptor is concatenated in the final Wasm section, prepend with the length 172 | // so that we can easily iterate each descriptor 173 | let descriptor_length = descriptor.len() + 4; 174 | let mut bytes = vec![ 175 | descriptor.len() as u8, 176 | (descriptor.len() >> 8) as u8, 177 | (descriptor.len() >> 16) as u8, 178 | (descriptor.len() >> 24) as u8, 179 | ]; 180 | 181 | bytes.extend_from_slice(descriptor); 182 | let descriptor_bytes = LitByteStr::new(&bytes, Span::call_site().into()); 183 | 184 | quote!( 185 | #[allow(dead_code)] 186 | #[link_section = #section] 187 | #[cfg(target_arch = "wasm32")] 188 | pub static #name: [u8; #descriptor_length] = *#descriptor_bytes; 189 | ) 190 | } 191 | 192 | fn emit_http_function(mut func: ItemFn, path: LitStr, methods: Vec) -> Result { 193 | check_function_validity(&func)?; 194 | check_http_validity(&func)?; 195 | 196 | let function = Function { 197 | name: func.sig.ident.to_string(), 198 | trigger: FunctionTrigger::Http { 199 | path: path.value(), 200 | methods, 201 | }, 202 | inputs: Vec::new(), 203 | outputs: vec![FunctionOutput::Http], 204 | }; 205 | 206 | let ident = func.sig.ident; 207 | let inner = Ident::new(&format!("__{}", ident), ident.span()); 208 | let name = Ident::new( 209 | &format!("__FUNCTION_{}", function.name.to_uppercase()), 210 | ident.span(), 211 | ); 212 | 213 | func.sig.ident = inner.clone(); 214 | 215 | let descriptor = emit_descriptor( 216 | "__functions", 217 | &name, 218 | serde_json::to_string(&[function]).unwrap().as_bytes(), 219 | ); 220 | 221 | Ok(quote!( 222 | #[no_mangle] 223 | pub extern "C" fn #ident(req: u32) -> u32 { 224 | #func 225 | 226 | unsafe { 227 | wasmtime_functions::Response::from( 228 | #inner(wasmtime_functions::Request::from_raw(req)) 229 | ) 230 | .into_raw() 231 | } 232 | } 233 | 234 | #descriptor 235 | ) 236 | .into()) 237 | } 238 | 239 | /// A macro for declaring an HTTP-triggered function using the `GET` verb. 240 | #[proc_macro_attribute] 241 | pub fn get(attr: TokenStream, item: TokenStream) -> TokenStream { 242 | match emit_http_function( 243 | parse_macro_input!(item as ItemFn), 244 | parse_macro_input!(attr as LitStr), 245 | vec![Method::Get], 246 | ) { 247 | Ok(s) => s, 248 | Err(e) => e.to_compile_error().into(), 249 | } 250 | } 251 | 252 | /// A macro for declaring an HTTP-triggered function using the `HEAD` verb. 253 | #[proc_macro_attribute] 254 | pub fn head(attr: TokenStream, item: TokenStream) -> TokenStream { 255 | match emit_http_function( 256 | parse_macro_input!(item as ItemFn), 257 | parse_macro_input!(attr as LitStr), 258 | vec![Method::Head], 259 | ) { 260 | Ok(s) => s, 261 | Err(e) => e.to_compile_error().into(), 262 | } 263 | } 264 | 265 | /// A macro for declaring an HTTP-triggered function using the `POST` verb. 266 | #[proc_macro_attribute] 267 | pub fn post(attr: TokenStream, item: TokenStream) -> TokenStream { 268 | match emit_http_function( 269 | parse_macro_input!(item as ItemFn), 270 | parse_macro_input!(attr as LitStr), 271 | vec![Method::Post], 272 | ) { 273 | Ok(s) => s, 274 | Err(e) => e.to_compile_error().into(), 275 | } 276 | } 277 | 278 | /// A macro for declaring an HTTP-triggered function using the `PUT` verb. 279 | #[proc_macro_attribute] 280 | pub fn put(attr: TokenStream, item: TokenStream) -> TokenStream { 281 | match emit_http_function( 282 | parse_macro_input!(item as ItemFn), 283 | parse_macro_input!(attr as LitStr), 284 | vec![Method::Put], 285 | ) { 286 | Ok(s) => s, 287 | Err(e) => e.to_compile_error().into(), 288 | } 289 | } 290 | 291 | /// A macro for declaring an HTTP-triggered function using the `DELETE` verb. 292 | #[proc_macro_attribute] 293 | pub fn delete(attr: TokenStream, item: TokenStream) -> TokenStream { 294 | match emit_http_function( 295 | parse_macro_input!(item as ItemFn), 296 | parse_macro_input!(attr as LitStr), 297 | vec![Method::Delete], 298 | ) { 299 | Ok(s) => s, 300 | Err(e) => e.to_compile_error().into(), 301 | } 302 | } 303 | 304 | /// A macro for declaring an HTTP-triggered function using the `CONNECT` verb. 305 | #[proc_macro_attribute] 306 | pub fn connect(attr: TokenStream, item: TokenStream) -> TokenStream { 307 | match emit_http_function( 308 | parse_macro_input!(item as ItemFn), 309 | parse_macro_input!(attr as LitStr), 310 | vec![Method::Connect], 311 | ) { 312 | Ok(s) => s, 313 | Err(e) => e.to_compile_error().into(), 314 | } 315 | } 316 | 317 | /// A macro for declaring an HTTP-triggered function using the `OPTIONS` verb. 318 | #[proc_macro_attribute] 319 | pub fn options(attr: TokenStream, item: TokenStream) -> TokenStream { 320 | match emit_http_function( 321 | parse_macro_input!(item as ItemFn), 322 | parse_macro_input!(attr as LitStr), 323 | vec![Method::Options], 324 | ) { 325 | Ok(s) => s, 326 | Err(e) => e.to_compile_error().into(), 327 | } 328 | } 329 | 330 | /// A macro for declaring an HTTP-triggered function using the `TRACE` verb. 331 | #[proc_macro_attribute] 332 | pub fn trace(attr: TokenStream, item: TokenStream) -> TokenStream { 333 | match emit_http_function( 334 | parse_macro_input!(item as ItemFn), 335 | parse_macro_input!(attr as LitStr), 336 | vec![Method::Trace], 337 | ) { 338 | Ok(s) => s, 339 | Err(e) => e.to_compile_error().into(), 340 | } 341 | } 342 | 343 | /// A macro for declaring an HTTP-triggered function using the `PATCH` verb. 344 | #[proc_macro_attribute] 345 | pub fn patch(attr: TokenStream, item: TokenStream) -> TokenStream { 346 | match emit_http_function( 347 | parse_macro_input!(item as ItemFn), 348 | parse_macro_input!(attr as LitStr), 349 | vec![Method::Patch], 350 | ) { 351 | Ok(s) => s, 352 | Err(e) => e.to_compile_error().into(), 353 | } 354 | } 355 | 356 | /// A macro for declaring an HTTP-triggered function. 357 | #[proc_macro_attribute] 358 | pub fn http(attr: TokenStream, item: TokenStream) -> TokenStream { 359 | struct Args { 360 | methods: LitStr, 361 | path: LitStr, 362 | } 363 | 364 | impl Parse for Args { 365 | fn parse(input: ParseStream) -> Result { 366 | let methods = input.parse()?; 367 | input.parse::()?; 368 | let path = input.parse()?; 369 | 370 | Ok(Self { methods, path }) 371 | } 372 | } 373 | 374 | let args = parse_macro_input!(attr as Args); 375 | 376 | let methods = match parse_methods(&args.methods) { 377 | Ok(methods) => methods, 378 | Err(e) => return e.to_compile_error().into(), 379 | }; 380 | 381 | match emit_http_function(parse_macro_input!(item as ItemFn), args.path, methods) { 382 | Ok(s) => s, 383 | Err(e) => e.to_compile_error().into(), 384 | } 385 | } 386 | 387 | /// A macro for declaring a required environment variable in a Wasmtime Functions application. 388 | #[proc_macro] 389 | pub fn var(item: TokenStream) -> TokenStream { 390 | struct Vars { 391 | vec: Vec, 392 | } 393 | 394 | impl Parse for Vars { 395 | fn parse(input: ParseStream) -> Result { 396 | Ok(Self { 397 | vec: input 398 | .parse_terminated::<_, Token![,]>(Ident::parse)? 399 | .into_iter() 400 | .map(|i| i.to_string()) 401 | .collect(), 402 | }) 403 | } 404 | } 405 | 406 | let vars = parse_macro_input!(item as Vars); 407 | 408 | static COUNTER: AtomicUsize = AtomicUsize::new(0); 409 | 410 | let name = Ident::new( 411 | &format!("__VAR_{}", COUNTER.fetch_add(1, Ordering::SeqCst)), 412 | Span::call_site().into(), 413 | ); 414 | 415 | emit_descriptor( 416 | "__vars", 417 | &name, 418 | serde_json::to_string(&vars.vec).unwrap().as_bytes(), 419 | ) 420 | .into() 421 | } 422 | -------------------------------------------------------------------------------- /crates/functions/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmtime-functions" 3 | version = "0.1.0" 4 | authors = ["Peter Huene "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | witx-bindgen-rust = { git = "https://github.com/bytecodealliance/witx-bindgen", rev = "aa00fa06ec7c90073e098a9a652ea8daa51ab1dc" } 9 | wasmtime-functions-codegen = { path = "../codegen" } 10 | http = "0.2.5" 11 | time = "0.3.2" 12 | -------------------------------------------------------------------------------- /crates/functions/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Wasmtime Functions crate. 2 | //! 3 | //! This crate defines the API used in Wasmtime Functions applications. 4 | 5 | #![deny(missing_docs)] 6 | 7 | witx_bindgen_rust::import!("../../crates/runtime/witx/functions.witx"); 8 | 9 | use http::Uri; 10 | use std::fmt; 11 | use time::Duration; 12 | 13 | /// Represents a HTTP status code. 14 | pub type StatusCode = http::StatusCode; 15 | 16 | /// Represents a HTTP request. 17 | #[derive(Debug)] 18 | pub struct Request(functions::Request); 19 | 20 | impl Request { 21 | #[doc(hidden)] 22 | pub unsafe fn from_raw(handle: u32) -> Self { 23 | Self(functions::Request::from_raw(handle as i32)) 24 | } 25 | 26 | /// Gets the URI of the HTTP request. 27 | pub fn uri(&self) -> Uri { 28 | self.0.uri().parse().expect("URI is invalid") 29 | } 30 | 31 | /// Gets the method of the HTTP request. 32 | pub fn method(&self) -> String { 33 | self.0.method() 34 | } 35 | 36 | /// Gets a header of the HTTP request. 37 | pub fn header>(&self, name: T) -> Option { 38 | self.0.header(name.as_ref()) 39 | } 40 | 41 | /// Gets a cookie of the HTTP request. 42 | pub fn cookie>(&self, name: T) -> Option { 43 | self.0.cookie(name.as_ref()) 44 | } 45 | 46 | /// Gets a parameter of the HTTP request. 47 | pub fn param>(&self, name: T) -> Option { 48 | self.0.param(name.as_ref()) 49 | } 50 | 51 | /// Gets the body of the HTTP request. 52 | pub fn body(&self) -> Result, String> { 53 | self.0.body() 54 | } 55 | } 56 | 57 | /// Used for building HTTP responses. 58 | pub struct ResponseBuilder(functions::Response); 59 | 60 | impl ResponseBuilder { 61 | /// Creates a new HTTP response builder. 62 | pub fn new(status: StatusCode) -> Self { 63 | Self(functions::Response::new(status.as_u16()).expect("status code is invalid")) 64 | } 65 | 66 | /// Sets a header of the HTTP response. 67 | pub fn header, U: AsRef>(self, name: T, value: U) -> Self { 68 | self.0.set_header(name.as_ref(), value.as_ref()); 69 | self 70 | } 71 | 72 | /// Adds a cookie into the HTTP response. 73 | pub fn add_cookie(self, cookie: &Cookie) -> Self { 74 | self.0.add_cookie(&cookie.0); 75 | self 76 | } 77 | 78 | /// Removes a cookie in the HTTP response. 79 | pub fn remove_cookie(self, cookie: &Cookie) -> Self { 80 | self.0.remove_cookie(&cookie.0); 81 | self 82 | } 83 | 84 | /// Sets the body of the HTTP response. 85 | /// 86 | /// This completes the builder and returns the response. 87 | pub fn body>(self, body: T) -> Response { 88 | self.0.set_body(body.as_ref()); 89 | Response(self.0) 90 | } 91 | } 92 | 93 | /// Represents a HTTP response. 94 | #[derive(Debug)] 95 | pub struct Response(functions::Response); 96 | 97 | impl Response { 98 | /// Creates a new HTTP response builder. 99 | pub fn build(status: StatusCode) -> ResponseBuilder { 100 | ResponseBuilder::new(status) 101 | } 102 | 103 | /// Gets the status code of the HTTP response. 104 | pub fn status(&self) -> StatusCode { 105 | StatusCode::from_u16(self.0.status()).unwrap() 106 | } 107 | 108 | /// Gets a header of the HTTP response. 109 | pub fn header>(&self, name: T) -> Option { 110 | self.0.header(name.as_ref()) 111 | } 112 | 113 | /// Gets the body of the HTTP response. 114 | pub fn body(&self) -> Vec { 115 | self.0.body() 116 | } 117 | 118 | #[doc(hidden)] 119 | pub unsafe fn into_raw(self) -> u32 { 120 | self.0.into_raw() as u32 121 | } 122 | } 123 | 124 | impl From<()> for Response { 125 | fn from(_: ()) -> Self { 126 | Self::build(StatusCode::NO_CONTENT).body("") 127 | } 128 | } 129 | 130 | impl From> for Response { 131 | fn from(res: std::result::Result) -> Self { 132 | res.unwrap_or_else(|e| { 133 | Self::build(StatusCode::INTERNAL_SERVER_ERROR) 134 | .header("Content-Type", "text/plain; charset=utf-8") 135 | .body(e.to_string()) 136 | }) 137 | } 138 | } 139 | 140 | impl From for Response { 141 | fn from(s: String) -> Self { 142 | Self::build(StatusCode::OK) 143 | .header("Content-Type", "text/plain; charset=utf-8") 144 | .body(s) 145 | } 146 | } 147 | 148 | /// The `SameSite` cookie attribute. 149 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 150 | pub enum SameSite { 151 | /// Cookies will only be sent in a first-party context and not be sent along with requests initiated by third party websites. 152 | Strict, 153 | /// Cookies are allowed to be sent with top-level navigations and will be sent along with GET request initiated by third party website. 154 | Lax, 155 | /// Cookies will be sent in all contexts, i.e sending cross-origin is allowed. 156 | None, 157 | } 158 | 159 | /// Used for building HTTP response cookies. 160 | pub struct CookieBuilder(functions::Cookie); 161 | 162 | impl CookieBuilder { 163 | /// Creates a new HTTP response cookie builder. 164 | pub fn new, U: AsRef>(name: T, value: U) -> Self { 165 | Self(functions::Cookie::new(name.as_ref(), value.as_ref())) 166 | } 167 | 168 | /// Sets the HttpOnly attribute on the cookie. 169 | pub fn http_only(self) -> Self { 170 | self.0.set_http_only(true); 171 | self 172 | } 173 | 174 | /// Sets the Secure attribute on the cookie. 175 | pub fn secure(self) -> Self { 176 | self.0.set_secure(true); 177 | self 178 | } 179 | 180 | /// Sets the MaxAge attribute on the cookie. 181 | pub fn max_age(self, value: Duration) -> Self { 182 | self.0.set_max_age(value.whole_seconds()); 183 | self 184 | } 185 | 186 | /// Sets the SameSite attribute on the cookie. 187 | pub fn same_site(self, value: SameSite) -> Self { 188 | self.0.set_same_site(match value { 189 | SameSite::Strict => functions::SameSitePolicy::Strict, 190 | SameSite::Lax => functions::SameSitePolicy::Lax, 191 | SameSite::None => functions::SameSitePolicy::None, 192 | }); 193 | self 194 | } 195 | 196 | /// Sets the Domain attribute on the cookie. 197 | pub fn domain>(self, value: T) -> Self { 198 | self.0.set_domain(value.as_ref()); 199 | self 200 | } 201 | 202 | /// Sets the Path attribute on the cookie. 203 | pub fn path>(self, value: T) -> Self { 204 | self.0.set_path(value.as_ref()); 205 | self 206 | } 207 | 208 | /// Finishes building the cookie. 209 | pub fn finish(self) -> Cookie { 210 | Cookie(self.0) 211 | } 212 | } 213 | 214 | /// Represents a HTTP response cookie. 215 | pub struct Cookie(functions::Cookie); 216 | 217 | impl Cookie { 218 | /// Builds a new HTTP response cookie with the given name and value. 219 | pub fn build, U: AsRef>(name: T, value: U) -> CookieBuilder { 220 | CookieBuilder::new(name.as_ref(), value.as_ref()) 221 | } 222 | } 223 | 224 | pub use wasmtime_functions_codegen::{ 225 | connect, delete, get, head, http, options, patch, post, put, trace, var, 226 | }; 227 | -------------------------------------------------------------------------------- /crates/metadata/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmtime-functions-metadata" 3 | version = "0.1.0" 4 | authors = ["Peter Huene "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | anyhow = "1.0.44" 9 | serde = { version = "1.0.130", features = ["derive"] } 10 | serde_json = "1.0.68" 11 | wasmparser = "0.80.1" 12 | -------------------------------------------------------------------------------- /crates/metadata/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Wasmtime Functions metadata crate. 2 | //! 3 | //! This crate is responsible for reading the metadata present in a WebAssembly module created by the 4 | //! Wasmtime Functions procedural macros. 5 | //! 6 | //! The data structures defined here should correspond to those in the `wasmtime-functions-codegen` crate. 7 | //! 8 | //! See the documentation of the `wasmtime-functions-codegen` crate for more information. 9 | 10 | #![deny(missing_docs)] 11 | 12 | use anyhow::{anyhow, bail, Result}; 13 | use serde::Deserialize; 14 | use std::collections::HashSet; 15 | use wasmparser::{Chunk, Parser, Payload}; 16 | 17 | /// Represents a HTTP method. 18 | #[derive(Clone, Copy, Eq, PartialEq, Deserialize)] 19 | #[serde(rename_all = "UPPERCASE")] 20 | pub enum Method { 21 | /// The `GET` HTTP method. 22 | Get, 23 | /// The `HEAD` HTTP method. 24 | Head, 25 | /// The `POST` HTTP method. 26 | Post, 27 | /// The `PUT` HTTP method. 28 | Put, 29 | /// The `DELETE` HTTP method. 30 | Delete, 31 | /// The `CONNECT` HTTP method. 32 | Connect, 33 | /// The `OPTIONS` HTTP method. 34 | Options, 35 | /// The `TRACE` HTTP method. 36 | Trace, 37 | /// The `PATCH` HTTP method. 38 | Patch, 39 | } 40 | 41 | impl std::fmt::Display for Method { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | write!(f, "{}", self.as_ref()) 44 | } 45 | } 46 | 47 | impl AsRef for Method { 48 | fn as_ref(&self) -> &str { 49 | match self { 50 | Self::Get => "GET", 51 | Self::Head => "HEAD", 52 | Self::Post => "POST", 53 | Self::Put => "PUT", 54 | Self::Delete => "DELETE", 55 | Self::Connect => "CONNECT", 56 | Self::Options => "OPTIONS", 57 | Self::Trace => "TRACE", 58 | Self::Patch => "PATCH", 59 | } 60 | } 61 | } 62 | 63 | impl std::borrow::Borrow for Method { 64 | fn borrow(&self) -> &str { 65 | self.as_ref() 66 | } 67 | } 68 | 69 | /// Represents the ways a Wasmtime Function can be triggered. 70 | #[derive(Deserialize)] 71 | #[serde(rename_all = "camelCase", tag = "type")] 72 | pub enum FunctionTrigger { 73 | /// The function is triggered by a HTTP request. 74 | Http { 75 | /// The request path that triggers the function. 76 | path: String, 77 | /// The request methods that trigger the function. 78 | methods: Vec, 79 | }, 80 | } 81 | 82 | /// Represents an input to a Wasmtime Function. 83 | #[derive(Deserialize)] 84 | #[serde(rename_all = "camelCase", tag = "type")] 85 | pub enum FunctionInput {} 86 | 87 | /// Represents an output of a Wasmtime Function. 88 | #[derive(Deserialize)] 89 | #[serde(rename_all = "camelCase", tag = "type")] 90 | pub enum FunctionOutput { 91 | /// The Wasmtime Function returns a HTTP response. 92 | Http, 93 | } 94 | 95 | /// Represents the metadata of a Wasmtime Function. 96 | #[derive(Deserialize)] 97 | #[serde(rename_all = "camelCase")] 98 | pub struct Function { 99 | /// The name of the function. 100 | pub name: String, 101 | /// The trigger of the function. 102 | pub trigger: FunctionTrigger, 103 | /// The inputs of the function. 104 | pub inputs: Vec, 105 | /// The outputs of the function. 106 | pub outputs: Vec, 107 | } 108 | 109 | /// Represents the Wasmtime Functions metadata for a WebAssembly module. 110 | pub struct Metadata { 111 | /// The set of functions exposed in the WebAssembly module. 112 | pub functions: Vec, 113 | /// The set of required environment variables exposed in the WebAssembly module. 114 | pub vars: Vec, 115 | } 116 | 117 | impl Metadata { 118 | /// Creates a `Metadata` from the bytes of a WebAssembly module. 119 | pub fn from_module_bytes>(bytes: &T) -> Result { 120 | let mut parser = Parser::new(0); 121 | let mut offset = 0; 122 | let bytes = bytes.as_ref(); 123 | 124 | let mut functions: Vec = Vec::new(); 125 | let mut vars: Vec = Vec::new(); 126 | 127 | loop { 128 | if offset >= bytes.len() { 129 | break; 130 | } 131 | 132 | match parser.parse(&bytes[offset..], true)? { 133 | Chunk::NeedMoreData(_) => bail!("the module is not a valid WebAssembly module"), 134 | Chunk::Parsed { consumed, payload } => { 135 | offset += consumed; 136 | 137 | if let Payload::CustomSection { name, data, .. } = payload { 138 | if name == "__functions" { 139 | Self::read_section_data(data, &mut functions).map_err(|e| { 140 | anyhow!( 141 | "WebAssembly module has an invalid '__functions' section: {}", 142 | e 143 | ) 144 | })?; 145 | } else if name == "__vars" { 146 | Self::read_section_data(data, &mut vars).map_err(|e| { 147 | anyhow!("WebAssembly module has an invalid '__vars' section: {}", e) 148 | })?; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | let mut set = HashSet::new(); 156 | for f in functions.iter() { 157 | if !set.insert(&f.name) { 158 | bail!( 159 | "WebAssembly module has a duplicate function named '{}'.", 160 | f.name 161 | ); 162 | } 163 | } 164 | 165 | set.clear(); 166 | for v in vars.iter() { 167 | if !set.insert(v) { 168 | bail!("WebAssembly module has a duplicate variable named '{}'.", v); 169 | } 170 | } 171 | 172 | Ok(Self { functions, vars }) 173 | } 174 | 175 | fn read_section_data<'de, T: Deserialize<'de>>( 176 | data: &'de [u8], 177 | items: &mut Vec, 178 | ) -> Result<()> { 179 | let mut offset = 0; 180 | 181 | loop { 182 | if offset >= data.len() { 183 | break; 184 | } 185 | 186 | match Self::read_data_len(&data[offset..]) { 187 | Some(len) => { 188 | let begin = offset + 4; 189 | let end = begin + len; 190 | if end > data.len() { 191 | bail!("not enough data in the section"); 192 | } 193 | 194 | for item in serde_json::from_slice::>(&data[begin..end])? { 195 | items.push(item); 196 | } 197 | 198 | offset = end; 199 | } 200 | None => bail!("not enough data in the section"), 201 | } 202 | } 203 | 204 | Ok(()) 205 | } 206 | 207 | fn read_data_len(data: &[u8]) -> Option { 208 | if data.len() < 4 { 209 | return None; 210 | } 211 | 212 | Some( 213 | (data[0] as usize) 214 | | ((data[1] as usize) << 8) 215 | | ((data[2] as usize) << 16) 216 | | ((data[3] as usize) << 24), 217 | ) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /crates/runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmtime-functions-runtime" 3 | version = "0.1.0" 4 | authors = ["Peter Huene "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | witx-bindgen-wasmtime = { git = "https://github.com/bytecodealliance/witx-bindgen", rev = "aa00fa06ec7c90073e098a9a652ea8daa51ab1dc", features = ["async"] } 9 | wasmtime-functions-metadata = { path = "../metadata" } 10 | tide = { version = "0.16.0", default_features = false, features = ["h1-server", "cookies", "sessions"] } 11 | http-types = "2.12.0" 12 | time = "0.2.27" 13 | anyhow = "1.0.44" 14 | bytes = "1.1.0" 15 | async-std = "1.10.0" 16 | async-h1 = "2.3.2" 17 | async-trait = "0.1.51" 18 | log = "0.4.14" 19 | wasmtime = "0.30.0" 20 | wasmtime-wasi = "0.30.0" 21 | futures-timer = "3.0.2" 22 | futures = "0.3.17" 23 | -------------------------------------------------------------------------------- /crates/runtime/src/host.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use http_types::cookies::SameSite; 3 | use std::cell::RefCell; 4 | use std::convert::TryFrom; 5 | use wasmtime::Linker; 6 | use wasmtime_wasi::WasiCtx; 7 | 8 | witx_bindgen_wasmtime::import!({ 9 | paths: ["crates/runtime/witx/functions.witx"], 10 | async: ["request::body"] 11 | }); 12 | 13 | type Tables = functions::FunctionsTables; 14 | 15 | pub struct Context { 16 | host: Host, 17 | request_handle: u32, 18 | tables: Tables, 19 | wasi: WasiCtx, 20 | } 21 | 22 | impl Context { 23 | pub fn new(req: crate::server::Request, wasi: WasiCtx) -> Self { 24 | let mut tables = Tables::default(); 25 | 26 | // Insert a placeholder request resource 27 | let request_handle = tables.request_table.insert(Request); 28 | 29 | Self { 30 | host: Host(req), 31 | request_handle, 32 | tables, 33 | wasi, 34 | } 35 | } 36 | 37 | pub fn request_handle(&self) -> u32 { 38 | self.request_handle 39 | } 40 | 41 | pub fn take_response(&self, handle: u32) -> Option { 42 | self.tables.response_table.get(handle).map(|r| { 43 | let mut res = r.inner.take().unwrap(); 44 | res.set_body(r.body.take()); 45 | res 46 | }) 47 | } 48 | 49 | pub fn add_to_linker(linker: &mut Linker) -> Result<()> { 50 | wasmtime_wasi::add_to_linker(linker, |s| &mut s.wasi)?; 51 | functions::add_functions_to_linker(linker, |s| (&mut s.host, &mut s.tables))?; 52 | 53 | Ok(()) 54 | } 55 | } 56 | 57 | #[derive(Debug)] 58 | pub struct Request; 59 | 60 | #[derive(Debug)] 61 | pub struct Response { 62 | inner: RefCell>, 63 | body: RefCell>, 64 | } 65 | 66 | // This is temporarily needed as a reference to the resource is captured 67 | // in the future across await points, but is not *used* by multiple threads concurrently. 68 | // TODO: remove this in the future 69 | unsafe impl Sync for Response {} 70 | 71 | #[derive(Debug)] 72 | pub struct Cookie { 73 | inner: RefCell>, 74 | } 75 | 76 | // This is temporarily needed as a reference to the resource is captured 77 | // in the future across await points, but is not *used* by multiple threads concurrently. 78 | // TODO: remove this in the future 79 | unsafe impl Sync for Cookie {} 80 | 81 | struct Host(crate::server::Request); 82 | 83 | #[witx_bindgen_wasmtime::async_trait] 84 | impl functions::Functions for Host { 85 | type Cookie = Cookie; 86 | type Request = Request; 87 | type Response = Response; 88 | 89 | fn request_method(&mut self, _: &Self::Request) -> String { 90 | self.0.method().to_string() 91 | } 92 | 93 | fn request_uri(&mut self, _: &Self::Request) -> String { 94 | self.0.url().as_str().to_string() 95 | } 96 | 97 | fn request_header(&mut self, _: &Self::Request, name: &str) -> Option { 98 | self.0.header(name).map(|v| v.as_str().to_string()) 99 | } 100 | 101 | fn request_cookie(&mut self, _: &Self::Request, name: &str) -> Option { 102 | self.0.cookie(name).map(|c| c.value().to_string()) 103 | } 104 | 105 | fn request_param(&mut self, _: &Self::Request, name: &str) -> Option { 106 | self.0.param(name).map(ToString::to_string).ok() 107 | } 108 | 109 | async fn request_body(&mut self, _: &Self::Request) -> Result, String> { 110 | self.0.body_bytes().await.map_err(|e| e.to_string()) 111 | } 112 | 113 | fn response_new(&mut self, status: functions::HttpStatus) -> Result { 114 | Ok(Response { 115 | inner: RefCell::new(Some(tide::Response::new( 116 | tide::StatusCode::try_from(status).map_err(|e| e.to_string())?, 117 | ))), 118 | body: RefCell::new(Vec::new()), 119 | }) 120 | } 121 | 122 | fn response_status(&mut self, response: &Self::Response) -> functions::HttpStatus { 123 | functions::HttpStatus::from(response.inner.borrow().as_ref().unwrap().status()) 124 | } 125 | 126 | fn response_header(&mut self, response: &Self::Response, name: &str) -> Option { 127 | response 128 | .inner 129 | .borrow() 130 | .as_ref() 131 | .unwrap() 132 | .header(name) 133 | .map(|v| v.as_str().to_string()) 134 | } 135 | 136 | fn response_set_header(&mut self, response: &Self::Response, name: &str, value: &str) { 137 | response 138 | .inner 139 | .borrow_mut() 140 | .as_mut() 141 | .unwrap() 142 | .insert_header(name, value); 143 | } 144 | 145 | fn response_add_cookie(&mut self, response: &Self::Response, cookie: &Self::Cookie) { 146 | response 147 | .inner 148 | .borrow_mut() 149 | .as_mut() 150 | .unwrap() 151 | .insert_cookie(cookie.inner.borrow().clone()); 152 | } 153 | 154 | fn response_remove_cookie(&mut self, response: &Self::Response, cookie: &Self::Cookie) { 155 | response 156 | .inner 157 | .borrow_mut() 158 | .as_mut() 159 | .unwrap() 160 | .remove_cookie(cookie.inner.borrow().clone()); 161 | } 162 | 163 | fn response_body(&mut self, response: &Self::Response) -> Vec { 164 | response.body.borrow().clone() 165 | } 166 | 167 | fn response_set_body(&mut self, response: &Self::Response, body: &[u8]) { 168 | let mut b = response.body.borrow_mut(); 169 | b.resize(body.len(), 0); 170 | b.copy_from_slice(body); 171 | } 172 | 173 | fn cookie_new(&mut self, name: &str, value: &str) -> Self::Cookie { 174 | Cookie { 175 | inner: RefCell::new(http_types::Cookie::new(name.to_string(), value.to_string())), 176 | } 177 | } 178 | 179 | fn cookie_set_http_only(&mut self, cookie: &Self::Cookie, enabled: bool) { 180 | cookie.inner.borrow_mut().set_http_only(Some(enabled)) 181 | } 182 | 183 | fn cookie_set_secure(&mut self, cookie: &Self::Cookie, enabled: bool) { 184 | cookie.inner.borrow_mut().set_secure(Some(enabled)) 185 | } 186 | 187 | fn cookie_set_max_age(&mut self, cookie: &Self::Cookie, age: i64) { 188 | cookie 189 | .inner 190 | .borrow_mut() 191 | .set_max_age(Some(time::Duration::seconds(age))) 192 | } 193 | 194 | fn cookie_set_same_site(&mut self, cookie: &Self::Cookie, policy: functions::SameSitePolicy) { 195 | cookie.inner.borrow_mut().set_same_site(match policy { 196 | functions::SameSitePolicy::Strict => SameSite::Strict, 197 | functions::SameSitePolicy::Lax => SameSite::Lax, 198 | functions::SameSitePolicy::None => SameSite::None, 199 | }); 200 | } 201 | 202 | fn cookie_set_domain(&mut self, cookie: &Self::Cookie, domain: &str) { 203 | cookie.inner.borrow_mut().set_domain(domain.to_string()); 204 | } 205 | 206 | fn cookie_set_path(&mut self, cookie: &Self::Cookie, path: &str) { 207 | cookie.inner.borrow_mut().set_path(path.to_string()); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /crates/runtime/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Wasmtime Functions runtime crate. 2 | //! 3 | //! This crate is responsible for implementing the runtime that hosts Wasmtime Functions applications. 4 | 5 | #![deny(missing_docs)] 6 | 7 | mod host; 8 | mod log; 9 | mod server; 10 | 11 | pub use server::{EnvironmentProvider, Server}; 12 | -------------------------------------------------------------------------------- /crates/runtime/src/log.rs: -------------------------------------------------------------------------------- 1 | use tide::{Middleware, Next, Request}; 2 | 3 | #[derive(Debug, Default, Clone)] 4 | pub struct LogMiddleware; 5 | 6 | // A logging middleware similar to the one that comes out-of-the box with 7 | // tide-rs. Unlike tide's, this one doesn't use the structured logging 8 | // experimental feature and thus env-logger works well with it. 9 | struct LogMiddlewareRan; 10 | 11 | impl LogMiddleware { 12 | /// Log a request and a response. 13 | async fn log<'a, State: Clone + Send + Sync + 'static>( 14 | &'a self, 15 | mut req: Request, 16 | next: Next<'a, State>, 17 | ) -> tide::Result { 18 | if req.ext::().is_some() { 19 | return Ok(next.run(req).await); 20 | } 21 | 22 | req.set_ext(LogMiddlewareRan); 23 | 24 | let path = req.url().path().to_owned(); 25 | let method = req.method().to_string(); 26 | 27 | log::info!("Request received: {} {}", method, path); 28 | 29 | let start = std::time::Instant::now(); 30 | let response = next.run(req).await; 31 | let elapsed = start.elapsed(); 32 | 33 | let status = response.status(); 34 | 35 | if status.is_server_error() { 36 | if let Some(error) = response.error() { 37 | log::error!( 38 | "Internal error: {} {} {} {:?}: {:?}", 39 | method, 40 | path, 41 | status, 42 | elapsed, 43 | error 44 | ); 45 | } else { 46 | log::error!( 47 | "Internal error: {} {} {} {:?}", 48 | method, 49 | path, 50 | status, 51 | elapsed 52 | ); 53 | } 54 | } else if status.is_client_error() { 55 | if let Some(error) = response.error() { 56 | log::warn!( 57 | "Client error: {} {} {} {:?}: {:?}", 58 | method, 59 | path, 60 | status, 61 | elapsed, 62 | error 63 | ); 64 | } else { 65 | log::warn!("Client error: {} {} {} {:?}", method, path, status, elapsed); 66 | } 67 | } else { 68 | log::info!( 69 | "Response sent: {} {} {} {:?}", 70 | method, 71 | path, 72 | status, 73 | elapsed 74 | ); 75 | } 76 | Ok(response) 77 | } 78 | } 79 | 80 | #[async_trait::async_trait] 81 | impl Middleware for LogMiddleware { 82 | async fn handle(&self, req: Request, next: Next<'_, State>) -> tide::Result { 83 | self.log(req, next).await 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /crates/runtime/src/server.rs: -------------------------------------------------------------------------------- 1 | use crate::host::Context; 2 | use anyhow::{anyhow, bail, Context as _, Result}; 3 | use async_trait::async_trait; 4 | use std::convert::TryFrom; 5 | use std::fmt; 6 | use std::net::SocketAddr; 7 | use std::sync::Arc; 8 | use wasmtime::{Config, Engine, Instance, Linker, Module, Store}; 9 | use wasmtime_functions_metadata::{FunctionTrigger, Metadata}; 10 | use wasmtime_wasi::sync::WasiCtxBuilder; 11 | 12 | const FUNCTION_TIMEOUT_SECS: u64 = 60; 13 | 14 | /// Provides environment variables to the runtime server. 15 | pub trait EnvironmentProvider { 16 | /// Gets the environment variable of the given name. 17 | fn var(&self, name: &str) -> Result; 18 | } 19 | 20 | pub type Request = tide::Request; 21 | 22 | #[derive(Clone)] 23 | pub struct State { 24 | inner: Arc, 25 | } 26 | 27 | struct StateInner { 28 | module: Module, 29 | linker: Linker, 30 | env: Vec<(String, String)>, 31 | inherit_stdout: bool, 32 | } 33 | 34 | impl StateInner { 35 | pub async fn instantiate(&self, request: Request) -> Result<(Store, Instance)> { 36 | let mut wasi_ctx = WasiCtxBuilder::new(); 37 | 38 | if self.inherit_stdout { 39 | wasi_ctx = wasi_ctx.inherit_stdout().inherit_stderr(); 40 | } 41 | 42 | wasi_ctx = wasi_ctx.envs(&self.env)?; 43 | 44 | let mut store = Store::new( 45 | self.module.engine(), 46 | Context::new(request, wasi_ctx.build()), 47 | ); 48 | store.out_of_fuel_async_yield(u64::MAX, 10000); 49 | 50 | let instance = self 51 | .linker 52 | .instantiate_async(&mut store, &self.module) 53 | .await?; 54 | 55 | Ok((store, instance)) 56 | } 57 | } 58 | 59 | #[derive(Clone)] 60 | struct Endpoint { 61 | function: Arc, 62 | } 63 | 64 | impl Endpoint { 65 | async fn invoke_function(&self, req: tide::Request) -> tide::Result { 66 | let state = req.state().inner.clone(); 67 | let (mut store, instance) = state.instantiate(req).await?; 68 | 69 | let entry = instance.get_typed_func::(&mut store, &self.function)?; 70 | 71 | let req = store.data().request_handle(); 72 | 73 | log::info!("Invoking function '{}'.", self.function); 74 | 75 | let res = entry 76 | .call_async(&mut store, req) 77 | .await 78 | .with_context(|| format!("call to function '{}' trapped", self.function))?; 79 | 80 | store 81 | .data() 82 | .take_response(res) 83 | .ok_or_else(|| tide::Error::from(anyhow!("function did not return a HTTP response"))) 84 | } 85 | } 86 | 87 | #[async_trait] 88 | impl tide::Endpoint for Endpoint { 89 | async fn call(&self, req: tide::Request) -> tide::Result { 90 | use async_std::prelude::FutureExt; 91 | 92 | self.invoke_function(req) 93 | .timeout(std::time::Duration::from_secs(FUNCTION_TIMEOUT_SECS)) 94 | .await? 95 | } 96 | } 97 | 98 | /// The Wasmtime Functions HTTP server. 99 | /// 100 | /// This server is used to host the given WebAssembly module and route requests to Wasmtime functions. 101 | pub struct Server(Box>); 102 | 103 | impl Server { 104 | /// Creates a runtime server. 105 | pub async fn new>( 106 | addr: A, 107 | module: &[u8], 108 | environment: &dyn EnvironmentProvider, 109 | debug_info: bool, 110 | inherit_stdout: bool, 111 | ) -> Result { 112 | let metadata = Metadata::from_module_bytes(&module)?; 113 | 114 | if metadata.functions.is_empty() { 115 | bail!("module contains no Wasmtime functions"); 116 | } 117 | 118 | let mut env = Vec::new(); 119 | for name in metadata.vars { 120 | let value = environment.var(&name)?; 121 | env.push((name, value)); 122 | } 123 | 124 | let mut config = Config::default(); 125 | 126 | config.allocation_strategy(wasmtime::InstanceAllocationStrategy::pooling()); 127 | config.debug_info(debug_info); 128 | config.consume_fuel(true); 129 | config.async_support(true); 130 | 131 | let engine = Engine::new(&config)?; 132 | let module = Module::new(&engine, module)?; 133 | 134 | let mut linker = Linker::new(&engine); 135 | Context::add_to_linker(&mut linker)?; 136 | 137 | let mut app = tide::with_state(State { 138 | inner: Arc::new(StateInner { 139 | module, 140 | linker, 141 | env, 142 | inherit_stdout, 143 | }), 144 | }); 145 | 146 | app.with(crate::log::LogMiddleware); 147 | 148 | for function in metadata.functions { 149 | match &function.trigger { 150 | FunctionTrigger::Http { path, methods } => { 151 | let mut route = app.at(path); 152 | 153 | let endpoint = Endpoint { 154 | function: Arc::new(function.name.clone()), 155 | }; 156 | 157 | if methods.is_empty() { 158 | log::info!( 159 | "Adding route for function '{}' at '{}'.", 160 | function.name, 161 | path, 162 | ); 163 | route.all(endpoint); 164 | } else { 165 | for method in methods { 166 | log::info!( 167 | "Adding route for function '{}' at '{}' ({}).", 168 | function.name, 169 | path, 170 | method 171 | ); 172 | http_types::Method::try_from(method.as_ref()) 173 | .map(|m| route.method(m, endpoint.clone())) 174 | .ok(); 175 | } 176 | } 177 | } 178 | } 179 | } 180 | 181 | Ok(Self(Box::new(app.bind(addr.into()).await?))) 182 | } 183 | 184 | /// Accepts and processes incoming connections. 185 | pub async fn accept(&mut self) -> Result<()> { 186 | self.0.accept().await?; 187 | Ok(()) 188 | } 189 | } 190 | 191 | impl fmt::Display for Server { 192 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 193 | write!( 194 | f, 195 | "{}", 196 | self.0.info().first().map(|i| i.connection()).unwrap_or("") 197 | ) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /crates/runtime/witx/functions.witx: -------------------------------------------------------------------------------- 1 | enum same_site_policy { 2 | strict, 3 | lax, 4 | none 5 | } 6 | 7 | type http_status = u16 8 | 9 | resource request { 10 | method: function() -> string 11 | uri: function() -> string 12 | header: function(name: string) -> option 13 | cookie: function(name: string) -> option 14 | param: function(name: string) -> option 15 | body: function() -> expected, string> 16 | } 17 | 18 | resource response { 19 | static new: function(status: http_status) -> expected 20 | status: function() -> http_status 21 | header: function(name: string) -> option 22 | set_header: function(name: string, value: string) 23 | add_cookie: function(cookie: cookie) 24 | remove_cookie: function(cookie: cookie) 25 | body: function() -> list 26 | set_body: function(body: list) 27 | } 28 | 29 | resource cookie { 30 | static new: function(name: string, value: string) -> cookie 31 | set_http_only: function(enabled: bool) 32 | set_secure: function(enabled: bool) 33 | set_max_age: function(age: s64) 34 | set_same_site: function(policy: same_site_policy) 35 | set_domain: function(domain: string) 36 | set_path: function(path: string) 37 | } 38 | -------------------------------------------------------------------------------- /examples/hello/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-example" 3 | version = "0.1.0" 4 | authors = ["Peter Huene "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wasmtime-functions = { path = "../../crates/functions" } 12 | 13 | [workspace] 14 | -------------------------------------------------------------------------------- /examples/hello/README.md: -------------------------------------------------------------------------------- 1 | # Hello example 2 | 3 | This is a [simple serverless application](src/lib.rs) that exposes a single HTTP function that responds with a greeting message. 4 | 5 | ## Install `cargo wasi` 6 | 7 | [`cargo wasi`](https://github.com/bytecodealliance/cargo-wasi) is a fantastic tool for easily building Rust crates as WebAssembly modules. 8 | 9 | Use `cargo` to install: 10 | 11 | ```text 12 | $ cargo install cargo-wasi 13 | ``` 14 | 15 | ## Running the example 16 | 17 | Start with building the example application with `cargo wasi`: 18 | 19 | ```text 20 | $ cargo wasi build --release 21 | ``` 22 | 23 | This will create a `hello_example.wasm` file in `target/wasm32-wasi/release`. 24 | 25 | Next, start the Wasmtime Functions host: 26 | 27 | ```text 28 | $ cargo run --manifest-path ../../Cargo.toml --release -- target/wasm32-wasi/release/hello_example.wasm --addr 127.0.0.1:3000 29 | [2021-07-15T00:25:40Z INFO ] Adding route for function 'hello' at '/hello/:name' (GET). 30 | [2021-07-15T00:25:40Z INFO ] Application listening at http://127.0.0.1:3000 31 | ``` 32 | 33 | The host will be listening for connections on port 3000. 34 | 35 | Lastly, execute the `hello` function: 36 | 37 | ```text 38 | $ curl localhost:3000/hello/world && echo 39 | Hello, world! 40 | ``` 41 | -------------------------------------------------------------------------------- /examples/hello/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasmtime_functions::{get, Request}; 2 | 3 | #[get("/hello/:name")] 4 | fn hello(req: Request) -> String { 5 | format!("Hello, {}!", req.param("name").unwrap()) 6 | } 7 | -------------------------------------------------------------------------------- /host/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wasmtime-functions-host" 3 | version = "0.1.0" 4 | authors = ["Peter Huene "] 5 | edition = "2018" 6 | default-run = "wasmtime-functions-host" 7 | 8 | [dependencies] 9 | wasmtime-functions-runtime = { path = "../crates/runtime" } 10 | structopt = { version = "0.3.23", features = ["color", "suggestions"] } 11 | anyhow = "1.0.44" 12 | futures = "0.3.17" 13 | async-std = { version = "1.10.0", features = ["attributes"] } 14 | async-ctrlc = "1.2.0" 15 | log = "0.4.14" 16 | env_logger = "0.9.0" 17 | rpassword = "5.0.1" 18 | -------------------------------------------------------------------------------- /host/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Result}; 2 | use async_ctrlc::CtrlC; 3 | use async_std::prelude::FutureExt; 4 | use env_logger::builder; 5 | use rpassword::read_password_from_tty; 6 | use std::net::SocketAddr; 7 | use std::path::PathBuf; 8 | use structopt::StructOpt; 9 | use wasmtime_functions_runtime::Server; 10 | 11 | fn parse_env_var(s: &str) -> Result<(String, String)> { 12 | let parts: Vec<_> = s.splitn(2, '=').collect(); 13 | if parts.len() != 2 { 14 | bail!("must be of the form `key=value`"); 15 | } 16 | Ok((parts[0].to_owned(), parts[1].to_owned())) 17 | } 18 | 19 | struct EnvironmentProvider(Vec<(String, String)>); 20 | 21 | impl wasmtime_functions_runtime::EnvironmentProvider for EnvironmentProvider { 22 | fn var(&self, name: &str) -> Result { 23 | Ok( 24 | if let Some((_, v)) = self.0.iter().find(|(n, _)| n == name) { 25 | v.clone() 26 | } else if let Ok(value) = std::env::var(&name) { 27 | value 28 | } else { 29 | read_password_from_tty(Some(&format!( 30 | "enter the value for environment variable '{}': ", 31 | name 32 | )))? 33 | }, 34 | ) 35 | } 36 | } 37 | 38 | #[derive(StructOpt)] 39 | pub struct Options { 40 | /// The path to the WebAssembly module to run. 41 | pub module: String, 42 | 43 | /// The listen address for the application. 44 | #[structopt(long, default_value = "127.0.0.1:0")] 45 | pub addr: SocketAddr, 46 | 47 | /// Enable debug information for the application. 48 | #[structopt(short = "g", long)] 49 | pub debug_info: bool, 50 | 51 | /// Override an application environment variable value. 52 | #[structopt(long = "env", short, number_of_values = 1, value_name = "NAME=VAL", parse(try_from_str = parse_env_var))] 53 | pub environment: Vec<(String, String)>, 54 | } 55 | 56 | async fn run(options: Options) -> Result<()> { 57 | let addr = options.addr; 58 | let module_path = PathBuf::from(options.module); 59 | 60 | if !module_path.is_file() { 61 | bail!("module '{}' does not exist.", module_path.display()); 62 | } 63 | 64 | let module = std::fs::read(&module_path)?; 65 | 66 | let environment = EnvironmentProvider(options.environment); 67 | 68 | let mut server = Server::new(addr, &module, &environment, options.debug_info, true).await?; 69 | 70 | log::info!("Application listening at {}", server); 71 | 72 | let ctrlc = CtrlC::new()?; 73 | 74 | ctrlc 75 | .race(async move { 76 | server.accept().await.unwrap(); 77 | }) 78 | .await; 79 | 80 | log::info!("Shutting down..."); 81 | 82 | Ok(()) 83 | } 84 | 85 | #[async_std::main] 86 | async fn main() { 87 | builder() 88 | .format_module_path(false) 89 | .filter_module("wasmtime_functions_runtime", log::LevelFilter::Info) 90 | .filter_module("wasmtime_functions_host", log::LevelFilter::Info) 91 | .init(); 92 | 93 | if let Err(e) = run(Options::from_args()).await { 94 | log::error!("{:?}", e); 95 | std::process::exit(1); 96 | } 97 | } 98 | --------------------------------------------------------------------------------