├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── examples ├── hello │ ├── .cargo-ok │ ├── .cargo │ │ └── config │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── hello_wasi │ ├── .cargo-ok │ ├── .cargo │ └── config │ ├── .gitignore │ ├── .keys │ ├── account.nk │ └── module.nk │ ├── Cargo.toml │ └── src │ └── lib.rs └── src ├── errors.rs └── lib.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: cargo build --verbose 18 | - name: Run tests 19 | run: cargo test --verbose 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .DS_Store 5 | .idea 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - nightly 5 | matrix: 6 | allow_failures: 7 | - rust: nightly 8 | fast_finish: true -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wapc" 3 | version = "0.10.1" 4 | authors = ["waPC team "] 5 | edition = "2018" 6 | description = "An engine-pluggable WebAssembly Host Runtime implementing the waPC Standard" 7 | license = "Apache-2.0" 8 | homepage = "https://github.com/wapc" 9 | documentation = "https://docs.rs/wapc" 10 | readme = "README.md" 11 | keywords = ["sdk", "wapc", "webassembly", "wasm", "wasi"] 12 | categories = ["wasm", "api-bindings"] 13 | exclude = [".assets"] 14 | 15 | [dependencies] 16 | log = "0.4.11" 17 | env_logger = "0.7" 18 | serde = { version = "1.0.114", features = ["derive"] } 19 | serde_json = "1.0.56" 20 | anyhow = "1.0.31" 21 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ---- 2 | 3 | ---- 4 | 5 | # This repository has moved 6 | 7 | Please update your links to the [wapc/wapc-rs](https://github.com/wapc/wapc-rs) repository. 8 | 9 | ---- 10 | 11 | ---- 12 | 13 | ![Rust](https://github.com/wapc/wapc-rust/workflows/Rust/badge.svg) 14 | ![crates.io](https://img.shields.io/crates/v/wapc.svg) 15 | ![license](https://img.shields.io/crates/l/wapc.svg) 16 | 17 | # waPC 18 | 19 | This is the Rust implementation of the **waPC** standard for WebAssembly host runtimes. It allows any WebAssembly module to be loaded as a guest and receive requests for invocation as well as to make its own function requests of the host. This library allows for both "pure" (completely isolated) wasm modules as well as WASI modules 20 | 21 | This crate defines the protocol for RPC exchange between guest (WebAssembly) modules and the host runtime. That protocol 22 | can be satisfied by any engine that implements the right trait. This allows you to choose the WebAssembly 23 | low-level "driver" that best suits your needs, whether it be JITted or interpreted or bespoke. 24 | 25 | ## Example 26 | 27 | The following is a simple example of synchronous, bi-directional procedure calls between a WebAssembly host runtime and the guest module. 28 | 29 | ```rust 30 | extern crate wapc; 31 | use wasmtime_provider::WasmtimeEngineProvider; 32 | use wapc::prelude::*; 33 | 34 | pub fn main() -> Result<(), Box> { 35 | let module = load_file(); 36 | let engine = WasmtimeEngineProvider::new(&module, None); 37 | let mut host = WapcHost::new( 38 | engine, 39 | |id: u64, bd: &str, ns: &str, op: &str, payload: &str|{ 40 | println!("Guest {} invoked '{}->{}:{}' with payload of {} bytes", id, bd, ns, op, payload.len()); 41 | Ok(vec![]) 42 | }, &module, None)?; 43 | 44 | let res = host.call("wapc:sample!Hello", b"this is a test")?; 45 | assert_eq!(res, b"hello world!"); 46 | Ok(()) 47 | } 48 | ``` 49 | 50 | For running examples, take a look at the examples available in the individual engine provider 51 | repositories: 52 | 53 | * [wasmtime-provider](https://github.com/wapc/wasmtime-provider) - Utilizes the [Bytecode Alliance](https://bytecodealliance.org/) runtime [wasmtime](https://github.com/bytecodealliance/wasmtime) for WebAssembly JIT compilation and execution. 54 | * [wasm3-provider](https://github.com/wapc/wasm3-provider) - Uses the [wasm3](https://github.com/wasm3) C interpreter runtime (with a [Rust wrapper](https://github.com/Veykril/wasm3-rs)) 55 | -------------------------------------------------------------------------------- /examples/hello/.cargo-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapc/wapc-rust/2178a9dab8f3d54c1d3a71c7052aa244e4a27529/examples/hello/.cargo-ok -------------------------------------------------------------------------------- /examples/hello/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" -------------------------------------------------------------------------------- /examples/hello/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk -------------------------------------------------------------------------------- /examples/hello/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello" 3 | version = "0.1.0" 4 | authors = ["Kevin Hoffman "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wapc-guest = "0.3.1" 12 | 13 | [profile.release] 14 | # Optimize for small code size 15 | opt-level = "s" 16 | -------------------------------------------------------------------------------- /examples/hello/src/lib.rs: -------------------------------------------------------------------------------- 1 | 2 | 3 | extern crate wapc_guest as guest; 4 | 5 | use guest::prelude::*; 6 | 7 | wapc_handler!(handle_wapc); 8 | 9 | pub fn handle_wapc(operation: &str, msg: &[u8]) -> CallResult { 10 | match operation { 11 | "wapc:sample!Hello" => hello_world(msg), 12 | _ => hello_world(b"Unknown operation") 13 | } 14 | } 15 | 16 | fn hello_world(msg: &[u8]) -> CallResult { 17 | guest::console_log(&format!( 18 | "Received message: {}", 19 | std::str::from_utf8(msg).unwrap() 20 | )); 21 | let _res = host_call("myBinding", "wapc:sample", "Ping", msg)?; 22 | Ok(b"hello world!".to_vec()) 23 | } 24 | -------------------------------------------------------------------------------- /examples/hello_wasi/.cargo-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wapc/wapc-rust/2178a9dab8f3d54c1d3a71c7052aa244e4a27529/examples/hello_wasi/.cargo-ok -------------------------------------------------------------------------------- /examples/hello_wasi/.cargo/config: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-wasi" -------------------------------------------------------------------------------- /examples/hello_wasi/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # These are backup files generated by rustfmt 6 | **/*.rs.bk -------------------------------------------------------------------------------- /examples/hello_wasi/.keys/account.nk: -------------------------------------------------------------------------------- 1 | SAAJKDNCSW6RBR6RJXJ5EC36YDCJTPAIESPKFTKTCINV672RWDHYAMNCTQ 2 | 3 | -------------------------------------------------------------------------------- /examples/hello_wasi/.keys/module.nk: -------------------------------------------------------------------------------- 1 | SMAJQNR3JWFNNSE7CYSCR2BRFB2VJTURR4HV7JTMVYGUH4KCSPL5235SEE 2 | 3 | -------------------------------------------------------------------------------- /examples/hello_wasi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello" 3 | version = "0.1.0" 4 | authors = ["Kevin Hoffman "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib"] 9 | 10 | [dependencies] 11 | wapc-guest = "0.2.0" 12 | 13 | [profile.release] 14 | # Optimize for small code size 15 | opt-level = "s" 16 | -------------------------------------------------------------------------------- /examples/hello_wasi/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2019 Capital One Services, LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | extern crate wapc_guest as guest; 16 | 17 | use guest::prelude::*; 18 | 19 | wapc_handler!(handle_wapc); 20 | 21 | pub fn handle_wapc(operation: &str, msg: &[u8]) -> CallResult { 22 | match operation { 23 | "wapc:sample!Hello" => hello_world(msg), 24 | _ => Err("bad dispatch".into()), 25 | } 26 | } 27 | 28 | // This function emits directly to the console, which requires a WASI host runtime 29 | fn hello_world(msg: &[u8]) -> CallResult { 30 | let _res = host_call("myBinding", "wapc:sample", "Ping", msg)?; 31 | println!("Hello from inside the WASI module!"); 32 | Ok(b"hello world!".to_vec()) 33 | } 34 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2019 Capital One Services, LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //! Library-specific error types and utility functions 16 | 17 | use std::error::Error as StdError; 18 | use std::fmt; 19 | 20 | #[derive(Debug)] 21 | pub struct Error(Box); 22 | 23 | pub fn new(kind: ErrorKind) -> Error { 24 | Error(Box::new(kind)) 25 | } 26 | 27 | #[derive(Debug)] 28 | pub enum ErrorKind { 29 | NoSuchFunction(String), 30 | IO(std::io::Error), 31 | WasmMisc(String), 32 | HostCallFailure(Box), 33 | GuestCallFailure(String), 34 | } 35 | 36 | impl Error { 37 | pub fn kind(&self) -> &ErrorKind { 38 | &self.0 39 | } 40 | 41 | pub fn into_kind(self) -> ErrorKind { 42 | *self.0 43 | } 44 | } 45 | 46 | impl StdError for Error { 47 | fn description(&self) -> &str { 48 | match *self.0 { 49 | ErrorKind::NoSuchFunction(_) => "No such function in Wasm module", 50 | ErrorKind::IO(_) => "I/O error", 51 | ErrorKind::WasmMisc(_) => "WebAssembly failure", 52 | ErrorKind::HostCallFailure(_) => "Error occurred during host call", 53 | ErrorKind::GuestCallFailure(_) => "Guest call failure", 54 | } 55 | } 56 | 57 | fn cause(&self) -> Option<&dyn StdError> { 58 | match *self.0 { 59 | ErrorKind::NoSuchFunction(_) => None, 60 | ErrorKind::IO(ref err) => Some(err), 61 | ErrorKind::WasmMisc(_) => None, 62 | ErrorKind::HostCallFailure(_) => None, 63 | ErrorKind::GuestCallFailure(_) => None, 64 | } 65 | } 66 | } 67 | 68 | impl fmt::Display for Error { 69 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 70 | match *self.0 { 71 | ErrorKind::NoSuchFunction(ref fname) => { 72 | write!(f, "No such function in Wasm module: {}", fname) 73 | } 74 | ErrorKind::IO(ref err) => write!(f, "I/O error: {}", err), 75 | ErrorKind::WasmMisc(ref err) => write!(f, "WebAssembly error: {}", err), 76 | ErrorKind::HostCallFailure(ref err) => { 77 | write!(f, "Error occurred during host call: {}", err) 78 | } 79 | ErrorKind::GuestCallFailure(ref reason) => write!(f, "Guest call failure: {}", reason), 80 | } 81 | } 82 | } 83 | 84 | impl From for Error { 85 | fn from(source: std::io::Error) -> Error { 86 | Error(Box::new(ErrorKind::IO(source))) 87 | } 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | #[allow(dead_code)] 93 | fn assert_sync_send() {} 94 | const _: fn() = || assert_sync_send::(); 95 | } 96 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/54989751?s=200&v=4")] 2 | 3 | //! # wapc 4 | //! 5 | //! The `wapc` crate provides a high-level WebAssembly host runtime that conforms to an RPC mechanism 6 | //! called **waPC**. waPC is designed to be a fixed, lightweight standard allowing both sides of the 7 | //! guest/host boundary to make method calls containing arbitrary binary payloads. Neither side 8 | //! of the contract is ever required to perform explicit allocation, ensuring maximum portability 9 | //! for wasm targets that might behave differently in the presence of garbage collectors and memory 10 | //! relocation, compaction, etc. 11 | //! 12 | //! The interface may at first appear more "chatty" than other protocols, but the cleanliness, ease of use, 13 | //! simplified developer experience, and purpose-fit aim toward stateless WebAssembly modules 14 | //! is worth the few extra nanoseconds of latency. 15 | //! 16 | //! To use `wapc`, first you'll need a waPC-compliant WebAssembly module (referred to as the _guest_) to load 17 | //! and execute. You can find a number of these samples available in the GitHub repository, 18 | //! and anything compiled with the [wascc](https://github.com/wascc) actor SDK can also be invoked 19 | //! via waPC as it is 100% waPC compliant. 20 | //! 21 | //! Next, you will need to chose a _runtime engine_. waPC describes the function call flow required 22 | //! for wasm-RPC, but it does not dictate how the low-level WebAssembly function calls are made. This 23 | //! allows you to select whatever engine best suits your needs, whether it's a JIT-based engine or an 24 | //! interpreter-based one. Simply instantiate anything that implements the 25 | //! [WebAssemblyEngineProvider](trait.WebAssemblyEngineProvider.html) trait and pass it to the WapcHost 26 | //! constructor and the [WapcHost](struct.WapcHost.html) will facilitate all RPCs. 27 | //! 28 | //! To make function calls, ensure that you provided a suitable host callback function (or closure) 29 | //! when you created your WapcHost. Then invoke the `call` function to initiate the RPC flow. 30 | //! 31 | //! # Example 32 | //! 33 | //! ```ignore 34 | //! extern crate wapc; 35 | //! use wapc::prelude::*; 36 | //! use wasmtime_provider::WasmtimeEnginerProvider; // Choose your own engine provider 37 | //! 38 | //! # fn load_file() -> Vec { 39 | //! # include_bytes!("../.assets/hello.wasm").to_vec() 40 | //! # } 41 | //! # fn load_wasi_file() -> Vec { 42 | //! # include_bytes!("../.assets/hello_wasi.wasm").to_vec() 43 | //! # } 44 | //! pub fn main() -> Result<(), Box> { 45 | //! let module_bytes = load_file(); 46 | //! let engine = WasmtimeEngineProvider::new(&bytes, None); 47 | //! let host = WapcHost::new( 48 | //! Box::new(engine), 49 | //! |id: u64, bd: &str, ns: &str, op: &str, payload: &[u8]| { 50 | //! println!("Guest {} invoked '{}->{}:{}' with payload of {} bytes", 51 | //! id, bd, ns, op, payload.len()); 52 | //! Ok(vec![]) 53 | //! })?; 54 | //! 55 | //! let res = host.call("wapc:sample!Hello", b"this is a test")?; 56 | //! assert_eq!(res, b"hello world!"); 57 | //! 58 | //! Ok(()) 59 | //! } 60 | //! ``` 61 | //! 62 | //! # Notes 63 | //! 64 | //! waPC is _reactive_. Guest modules cannot initiate host calls without first handling a call 65 | //! initiated by the host. It is up to the runtime engine provider (e.g. `wasmtime` or `wasm3`) 66 | //! to invoke the required start functions (if present) during initialization. Guest modules can 67 | //! synchronously make as many host calls as they like, but keep in mind that if a host call takes too long or fails, it'll cause the initiating 68 | //! guest call to also fail. 69 | //! 70 | //! In summary, keep host callbacks fast and and free of panic-friendly `unwrap()`s, and do not spawn new threads 71 | //! within a host callback unless you must (and can synchronously return a value) because waPC 72 | //! assumes a single-threaded execution environment. Also note that for safety the host callback function 73 | //! intentionally has no references to the WebAssembly module bytes or the running instance. If you need 74 | //! an external reference in the callback, you can capture it in a closure. 75 | //! 76 | //! ## RPC Exchange Flow 77 | //! 78 | //! The following is a detailed outline of which functions are invoked and in which order to support 79 | //! a waPC exchange flow, which is always triggered by a consumer invoking the `call` function. Invoking 80 | //! and handling these low-level functions is the responsibility of the _engine provider_, while 81 | //! orchestrating the high-level control flow is the job of the `WapcHost`. 82 | //! 83 | //! 1. Host invokes `__guest_call` on the WebAssembly module (via the engine provider) 84 | //! 1. Guest calls the `__guest_request` function to instruct the host to write the request parameters to linear memory 85 | //! 1. Guest uses the `op_len` and `msg_len` parameters long with the pointer values it generated in step 2 to retrieve the operation (UTF-8 string) and payload (opaque byte array) 86 | //! 1. Guest performs work 87 | //! 1. _(Optional)_ Guest invokes `__host_call` on host with pointers and lengths indicating the `binding`, `namespace`, `operation`, and payload. 88 | //! 1. _(Optional)_ Guest can use `__host_response` and `host_response_len` functions to obtain and interpret results 89 | //! 1. _(Optional)_ Guest can use `__host_error_len` and `__host_error` to obtain the host error if indicated (`__host_call` returns 0) 90 | //! 1. Steps 5-7 can repeat with as many different host calls as the guest needs 91 | //! 1. Guest will call `guest_error` to indicate if an error occurred during processing 92 | //! 1. Guest will call `guest_response` to store the opaque response payload 93 | //! 1. Guest will return 0 (error) or 1 (success) at the end of `__guest_call` 94 | //! 95 | //! ## Required Host Exports 96 | //! List of functions that must be exported by the host (imported by the guest) 97 | //! 98 | //! | Module | Function | Parameters | Description | 99 | //! |----------------|----------------|-----------------|-----------------------------------------| 100 | //! | wapc | __host_call | br_ptr: i32
bd_len: i32
ns_ptr: i32
ns_len: i32
op_ptr: i32
op_len: i32
ptr: i32
len: i32
-> i32 | Invoked to initiate a host call | 101 | //! | wapc | __console_log | ptr: i32, len: i32 | Allows guest to log to `stdout` | 102 | //! | wapc | __guest_request | op_ptr: i32
ptr: i32 | Writes the guest request payload and operation name to linear memory at the designated locations | 103 | //! | wapc | __host_response | ptr: i32 | Instructs host to write the host response payload to the given location in linear memory | 104 | //! | wapc | __host_response_len | -> i32 | Obtains the length of the current host response | 105 | //! | wapc | __guest_response | ptr: i32
len: i32 | Tells the host the size and location of the current guest response payload | 106 | //! | wapc | __guest_error | ptr: i32
len: i32 | Tells the host the size and location of the current guest error payload | 107 | //! | wapc | __host_error | ptr: i32 | Instructs the host to write the host error payload to the given location | 108 | //! | wapc | __host_error_len | -> i32 | Queries the host for the length of the current host error (0 if none) | 109 | //! 110 | //! 111 | //! ## Required Guest Exports 112 | //! List of functions that must be exported by the guest (invoked by the host) 113 | //! 114 | //! | Function | Parameters | Description | 115 | //! |----------|------------|-------------| 116 | //! | __guest_call | op_len: i32
msg_len: i32 | Invoked by the host to start an RPC exchange with the guest module | 117 | 118 | #[macro_use] 119 | extern crate log; 120 | 121 | pub mod errors; 122 | 123 | 124 | /// A result type for errors that occur within the wapc library 125 | pub type Result = std::result::Result; 126 | 127 | use std::sync::atomic::{AtomicU64, Ordering}; 128 | 129 | use std::error::Error; 130 | use std::cell::RefCell; 131 | use std::sync::{Arc, RwLock}; 132 | 133 | static GLOBAL_MODULE_COUNT: AtomicU64 = AtomicU64::new(1); 134 | 135 | /// The host module name / namespace that guest modules must use for imports 136 | pub const HOST_NAMESPACE: &str = "wapc"; 137 | 138 | /// A list of the function names that are part of each waPC conversation 139 | pub struct WapcFunctions; 140 | 141 | impl WapcFunctions { 142 | // -- Functions called by guest, exported by host 143 | pub const HOST_CONSOLE_LOG: &'static str = "__console_log"; 144 | pub const HOST_CALL: &'static str = "__host_call"; 145 | pub const GUEST_REQUEST_FN: &'static str = "__guest_request"; 146 | pub const HOST_RESPONSE_FN: &'static str = "__host_response"; 147 | pub const HOST_RESPONSE_LEN_FN: &'static str = "__host_response_len"; 148 | pub const GUEST_RESPONSE_FN: &'static str = "__guest_response"; 149 | pub const GUEST_ERROR_FN: &'static str = "__guest_error"; 150 | pub const HOST_ERROR_FN: &'static str = "__host_error"; 151 | pub const HOST_ERROR_LEN_FN: &'static str = "__host_error_len"; 152 | 153 | // -- Functions called by host, exported by guest 154 | pub const GUEST_CALL: &'static str = "__guest_call"; 155 | pub const WAPC_INIT: &'static str = "wapc_init"; 156 | pub const TINYGO_START: &'static str = "_start"; 157 | 158 | /// Start functions to attempt to call - order is important 159 | pub const REQUIRED_STARTS: [&'static str;2] = [Self::TINYGO_START, Self::WAPC_INIT]; 160 | } 161 | 162 | /// Parameters defining the options for enabling WASI on a module (if applicable) 163 | #[derive(Debug, Default)] 164 | pub struct WasiParams { 165 | pub argv: Vec, 166 | pub map_dirs: Vec<(String, String)>, 167 | pub env_vars: Vec<(String, String)>, 168 | pub preopened_dirs: Vec, 169 | } 170 | 171 | impl WasiParams { 172 | pub fn new( 173 | argv: Vec, 174 | map_dirs: Vec<(String, String)>, 175 | env_vars: Vec<(String, String)>, 176 | preopened_dirs: Vec, 177 | ) -> Self { 178 | WasiParams { 179 | argv, 180 | map_dirs, 181 | preopened_dirs, 182 | env_vars, 183 | } 184 | } 185 | } 186 | 187 | #[derive(Default)] 188 | /// Module state is essentially a 'handle' that is passed to a runtime engine to allow it 189 | /// to read and write relevant data as different low-level functions are executed during 190 | /// a waPC conversation 191 | pub struct ModuleState { 192 | guest_request: RwLock>, 193 | guest_response: RwLock>>, 194 | host_response: RwLock>>, 195 | guest_error: RwLock>, 196 | host_error: RwLock>, 197 | host_callback: Option>, 198 | id: u64, 199 | } 200 | 201 | impl ModuleState { 202 | pub(crate) fn new(host_callback: Box, id: u64) -> ModuleState { 203 | ModuleState { 204 | host_callback: Some(Box::new(host_callback)), 205 | id, 206 | guest_request: RwLock::new(None), 207 | guest_response: RwLock::new(None), 208 | host_response: RwLock::new(None), 209 | guest_error: RwLock::new(None), 210 | host_error: RwLock::new(None), 211 | } 212 | } 213 | } 214 | 215 | impl ModuleState { 216 | /// Retrieves the value, if any, of the current guest request 217 | pub fn get_guest_request(&self) -> Option { 218 | self.guest_request.read().unwrap().clone() 219 | } 220 | 221 | /// Retrieves the value of the current host response 222 | pub fn get_host_response(&self) -> Option> { 223 | self.host_response.read().unwrap().clone() 224 | } 225 | 226 | /// Sets a value indicating that an error occurred inside the execution of a guest call 227 | pub fn set_guest_error(&self, error: String) { 228 | *self.guest_error.write().unwrap() = Some(error); 229 | } 230 | 231 | /// Sets the value indicating the response data from a guest call 232 | pub fn set_guest_response(&self, response: Vec) { 233 | *self.guest_response.write().unwrap() = Some(response); 234 | } 235 | 236 | /// Queries the value of the current guest response 237 | pub fn get_guest_response(&self) -> Option> { 238 | self.guest_response.read().unwrap().clone() 239 | } 240 | 241 | /// Queries the value of the current host error 242 | pub fn get_host_error(&self) -> Option { 243 | self.host_error.read().unwrap().clone() 244 | } 245 | 246 | /// Invoked when the guest module wishes to make a call on the host 247 | pub fn do_host_call( 248 | &self, 249 | binding: &str, 250 | namespace: &str, 251 | operation: &str, 252 | payload: &[u8], 253 | ) -> std::result::Result> { 254 | let id = { 255 | *self.host_response.write().unwrap() = None; 256 | *self.host_error.write().unwrap() = None; 257 | self.id 258 | }; 259 | let result = { 260 | match self.host_callback { 261 | Some(ref f) => f(id, binding, namespace, operation, &payload), 262 | None => Err("Missing host callback function!".into()), 263 | } 264 | }; 265 | Ok(match result { 266 | Ok(v) => { 267 | *self.host_response.write().unwrap() = Some(v); 268 | 1 269 | } 270 | Err(e) => { 271 | *self.host_error.write().unwrap() = Some(format!("{}", e)); 272 | 0 273 | } 274 | }) 275 | } 276 | 277 | /// Invoked when the guest module wants to write a message to the host's `stdout` 278 | pub fn do_console_log(&self, msg: &str) { 279 | info!("Guest module {}: {}", self.id, msg); 280 | } 281 | } 282 | 283 | /// An engine provider is any code that encapsulates low-level WebAssembly interactions such 284 | /// as reading from and writing to linear memory, executing functions, and mapping imports 285 | /// in a way that conforms to the waPC conversation protocol. 286 | pub trait WebAssemblyEngineProvider { 287 | /// Tell the engine provider that it can do whatever processing it needs to do for 288 | /// initialization and give it access to the module state 289 | fn init( 290 | &mut self, 291 | host: Arc, 292 | ) -> std::result::Result<(), Box>; 293 | /// Trigger the waPC function call. Engine provider is responsible for execution and using the appropriate methods 294 | /// on the module host. When this function is complete, the guest response and optionally the guest 295 | /// error must be set to represent the high-level call result 296 | fn call( 297 | &mut self, 298 | op_length: i32, 299 | msg_length: i32, 300 | ) -> std::result::Result>; 301 | /// Called by the host to replace the WebAssembly module bytes of the previously initialized module. Engine must return an 302 | /// error if it does not support bytes replacement. 303 | fn replace(&mut self, bytes: &[u8]) -> std::result::Result<(), Box>; 304 | } 305 | 306 | /// The module host (waPC) must provide an implementation of this trait to the engine provider 307 | /// to enable waPC function calls. 308 | pub trait ModuleHost { 309 | /// Called by the engine provider to obtain the Invocation bound for the guest module 310 | fn get_guest_request(&self) -> Option; 311 | /// Called by the engine provider to query the results of a host function call 312 | fn get_host_response(&self) -> Option>; 313 | /// Called by the engine provider to set the error message indicating a failure that occurred inside the guest module execution 314 | fn set_guest_error(&self, error: String); 315 | /// Called by the engine provider to set the response data for a guest call 316 | fn set_guest_response(&self, response: Vec); 317 | /// Called by the engine provider to query the host error if one is indicated by the return code for a host call 318 | fn get_host_error(&self) -> Option; 319 | /// Called by the engine provider to allow a guest module to perform a host call. The numeric return value 320 | /// will be > 0 for success (engine must obtain the host response) or 0 for error (engine must obtain the error) 321 | fn do_host_call( 322 | &self, 323 | binding: &str, 324 | namespace: &str, 325 | operation: &str, 326 | payload: &[u8], 327 | ) -> std::result::Result>; 328 | /// Attempts to perform a console log. There are no guarantees this will happen, and no error will be returned 329 | /// to the guest module if the host rejects the attempt 330 | fn do_console_log(&self, msg: &str); 331 | } 332 | 333 | type HostCallback = dyn Fn( 334 | u64, 335 | &str, 336 | &str, 337 | &str, 338 | &[u8], 339 | ) -> std::result::Result, Box> 340 | + Sync 341 | + Send 342 | + 'static; 343 | 344 | #[derive(Debug, Clone)] 345 | /// Represents a waPC invocation, which is a combination of an operation string and the 346 | /// corresponding binary payload 347 | pub struct Invocation { 348 | pub operation: String, 349 | pub msg: Vec, 350 | } 351 | 352 | impl Invocation { 353 | /// Creates a new invocation 354 | fn new(op: &str, msg: Vec) -> Invocation { 355 | Invocation { 356 | operation: op.to_string(), 357 | msg, 358 | } 359 | } 360 | } 361 | 362 | /// A WebAssembly host runtime for waPC-compliant modules 363 | /// 364 | /// Use an instance of this struct to provide a means of invoking procedure calls by 365 | /// specifying an operation name and a set of bytes representing the opaque operation payload. 366 | /// `WapcHost` makes no assumptions about the contents or format of either the payload or the 367 | /// operation name, other than that the operation name is a UTF-8 encoded string. 368 | pub struct WapcHost { 369 | engine: RefCell>, 370 | state: Arc, 371 | } 372 | 373 | impl WapcHost { 374 | /// Creates a new instance of a waPC-compliant host runtime paired with a given 375 | /// low-level engine provider 376 | pub fn new( 377 | engine: Box, 378 | host_callback: impl Fn( 379 | u64, 380 | &str, 381 | &str, 382 | &str, 383 | &[u8], 384 | ) 385 | -> std::result::Result, Box> 386 | + 'static 387 | + Sync 388 | + Send, 389 | ) -> Result { 390 | let id = GLOBAL_MODULE_COUNT.fetch_add(1, Ordering::SeqCst); 391 | //let state = Rc::new(RefCell::new(ModuleState::new(id, Box::new(host_callback)))); 392 | let state = Arc::new(ModuleState::new(Box::new(host_callback), id)); 393 | 394 | let mh = WapcHost { 395 | engine: RefCell::new(engine), 396 | state: state.clone(), 397 | }; 398 | 399 | mh.initialize(state)?; 400 | 401 | Ok(mh) 402 | } 403 | 404 | fn initialize(&self, state: Arc) -> Result<()> { 405 | match self.engine.borrow_mut().init(state) { 406 | Ok(_) => Ok(()), 407 | Err(e) => Err(crate::errors::new( 408 | crate::errors::ErrorKind::GuestCallFailure(format!( 409 | "Failed to initialize guest module: {}", 410 | e 411 | )), 412 | )), 413 | } 414 | } 415 | 416 | /// Returns a reference to the unique identifier of this module. If a parent process 417 | /// has instantiated multiple `WapcHost`s, then the single static host callback function 418 | /// will contain this value to allow disambiguation of modules 419 | pub fn id(&self) -> u64 { 420 | self.state.id 421 | } 422 | 423 | /// Invokes the `__guest_call` function within the guest module as per the waPC specification. 424 | /// Provide an operation name and an opaque payload of bytes and the function returns a `Result` 425 | /// containing either an error or an opaque reply of bytes. 426 | /// 427 | /// It is worth noting that the _first_ time `call` is invoked, the WebAssembly module 428 | /// might incur a "cold start" penalty, depending on which underlying engine you're using. This 429 | /// might be due to lazy initialization or JIT-compilation. 430 | pub fn call(&self, op: &str, payload: &[u8]) -> Result> { 431 | let inv = Invocation::new(op, payload.to_vec()); 432 | 433 | { 434 | *self.state.guest_response.write().unwrap() = None; 435 | *self.state.guest_request.write().unwrap() = Some((inv).clone()); 436 | *self.state.guest_error.write().unwrap() = None; 437 | *self.state.host_response.write().unwrap() = None; 438 | *self.state.host_error.write().unwrap() = None; 439 | } 440 | 441 | let callresult = match self 442 | .engine 443 | .borrow_mut() 444 | .call(inv.operation.len() as i32, inv.msg.len() as i32) 445 | { 446 | Ok(c) => c, 447 | Err(e) => { 448 | return Err(errors::new(errors::ErrorKind::GuestCallFailure(format!( 449 | "{}", 450 | e 451 | )))); 452 | } 453 | }; 454 | 455 | if callresult == 0 { 456 | // invocation failed 457 | let lock = self.state.guest_error.read().unwrap(); 458 | match *lock { 459 | Some(ref s) => Err(errors::new(errors::ErrorKind::GuestCallFailure(s.clone()))), 460 | None => Err(errors::new(errors::ErrorKind::GuestCallFailure( 461 | "No error message set for call failure".to_string(), 462 | ))), 463 | } 464 | } else { 465 | // invocation succeeded 466 | match *self.state.guest_response.read().unwrap() { 467 | Some(ref e) => Ok(e.clone()), 468 | None => { 469 | let lock = self.state.guest_error.read().unwrap(); 470 | match *lock { 471 | Some(ref s) => Err(errors::new(errors::ErrorKind::GuestCallFailure(s.clone()))), 472 | None => Err(errors::new(errors::ErrorKind::GuestCallFailure( 473 | "No error message OR response set for call success".to_string(), 474 | ))), 475 | } 476 | } 477 | } 478 | } 479 | } 480 | 481 | /// Performs a live "hot swap" of the WebAssembly module. Since all internal waPC execution is assumed to be 482 | /// single-threaded and non-reentrant, this call is synchronous and so 483 | /// you should never attempt to invoke `call` from another thread while performing this hot swap. 484 | /// 485 | /// **Note**: if the underlying engine you've chosen is a JITting engine, then performing a swap 486 | /// will re-introduce a "cold start" delay upon the next function call. 487 | /// 488 | /// If you perform a hot swap of a WASI module, you cannot alter the parameters used to create the WASI module 489 | /// like the environment variables, mapped directories, pre-opened files, etc. Not abiding by this could lead 490 | /// to privilege escalation attacks or non-deterministic behavior after the swap. 491 | pub fn replace_module(&self, module: &[u8]) -> Result<()> { 492 | match self.engine.borrow_mut().replace(module) { 493 | Ok(_) => Ok(()), 494 | Err(e) => Err(errors::new(errors::ErrorKind::GuestCallFailure( 495 | format!("Failed to swap module bytes: {}", e) 496 | ))) 497 | } 498 | } 499 | } 500 | --------------------------------------------------------------------------------