├── .cargo └── config.toml ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── workflows │ └── ci.yaml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-Apache-2.0_WITH_LLVM-exception ├── LICENSE-MIT ├── README.md ├── examples ├── complex_http_client.rs ├── http_client.rs ├── http_server.rs └── tcp_echo_server.rs ├── macro ├── Cargo.toml └── src │ └── lib.rs ├── rust-toolchain.toml ├── src ├── future │ ├── delay.rs │ ├── future_ext.rs │ ├── mod.rs │ └── timeout.rs ├── http │ ├── body.rs │ ├── client.rs │ ├── error.rs │ ├── fields.rs │ ├── method.rs │ ├── mod.rs │ ├── request.rs │ ├── response.rs │ ├── scheme.rs │ └── server.rs ├── io │ ├── copy.rs │ ├── cursor.rs │ ├── empty.rs │ ├── mod.rs │ ├── read.rs │ ├── seek.rs │ ├── stdio.rs │ ├── streams.rs │ └── write.rs ├── iter │ └── mod.rs ├── lib.rs ├── net │ ├── mod.rs │ ├── tcp_listener.rs │ └── tcp_stream.rs ├── rand │ └── mod.rs ├── runtime │ ├── block_on.rs │ ├── mod.rs │ └── reactor.rs ├── task.rs └── time │ ├── duration.rs │ ├── instant.rs │ ├── mod.rs │ └── utils.rs ├── test-programs ├── Cargo.toml ├── artifacts │ ├── Cargo.toml │ ├── build.rs │ ├── src │ │ └── lib.rs │ └── tests │ │ ├── http_server.rs │ │ └── tcp_echo_server.rs └── src │ └── bin │ ├── http_server.rs │ └── tcp_echo_server.rs └── tests ├── http_first_byte_timeout.rs ├── http_get.rs ├── http_get_json.rs ├── http_post.rs ├── http_post_json.rs ├── http_timeout.rs └── sleep.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.wasm32-wasip2] 2 | runner = "wasmtime -Shttp" 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, 10 | education, socio-economic status, nationality, personal appearance, race, 11 | religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | - Using welcoming and inclusive language 19 | - Being respectful of differing viewpoints and experiences 20 | - Gracefully accepting constructive criticism 21 | - Focusing on what is best for the community 22 | - Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | - The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | - Trolling, insulting/derogatory comments, and personal or political attacks 29 | - Public or private harassment 30 | - Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | - Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of acceptable 39 | behavior and are expected to take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or 43 | reject comments, commits, code, wiki edits, issues, and other contributions 44 | that are not aligned to this Code of Conduct, or to ban temporarily or 45 | permanently any contributor for other behaviors that they deem inappropriate, 46 | threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies both within project spaces and in public spaces 51 | when an individual is representing the project or its community. Examples of 52 | representing a project or community include using an official project e-mail 53 | address, posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. Representation of a project may be 55 | further defined and clarified by project maintainers. 56 | 57 | ## Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 60 | reported by contacting the project team at conduct@yosh.is, or through 61 | IRC. All complaints will be reviewed and investigated and will result in a 62 | response that is deemed necessary and appropriate to the circumstances. The 63 | project team is obligated to maintain confidentiality with regard to the 64 | reporter of an incident. 65 | Further details of specific enforcement policies may be posted separately. 66 | 67 | Project maintainers who do not follow or enforce the Code of Conduct in good 68 | faith may face temporary or permanent repercussions as determined by other 69 | members of the project's leadership. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 74 | available at 75 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Contributions include code, documentation, answering user questions, running the 3 | project's infrastructure, and advocating for all types of users. 4 | 5 | The project welcomes all contributions from anyone willing to work in good faith 6 | with other contributors and the community. No contribution is too small and all 7 | contributions are valued. 8 | 9 | This guide explains the process for contributing to the project's GitHub 10 | Repository. 11 | 12 | - [Code of Conduct](#code-of-conduct) 13 | - [Bad Actors](#bad-actors) 14 | 15 | ## Code of Conduct 16 | The project has a [Code of Conduct](./CODE_OF_CONDUCT.md) that *all* 17 | contributors are expected to follow. This code describes the *minimum* behavior 18 | expectations for all contributors. 19 | 20 | As a contributor, how you choose to act and interact towards your 21 | fellow contributors, as well as to the community, will reflect back not only 22 | on yourself but on the project as a whole. The Code of Conduct is designed and 23 | intended, above all else, to help establish a culture within the project that 24 | allows anyone and everyone who wants to contribute to feel safe doing so. 25 | 26 | Should any individual act in any way that is considered in violation of the 27 | [Code of Conduct](./CODE_OF_CONDUCT.md), corrective actions will be taken. It is 28 | possible, however, for any individual to *act* in such a manner that is not in 29 | violation of the strict letter of the Code of Conduct guidelines while still 30 | going completely against the spirit of what that Code is intended to accomplish. 31 | 32 | Open, diverse, and inclusive communities live and die on the basis of trust. 33 | Contributors can disagree with one another so long as they trust that those 34 | disagreements are in good faith and everyone is working towards a common 35 | goal. 36 | 37 | ## Bad Actors 38 | All contributors to tacitly agree to abide by both the letter and 39 | spirit of the [Code of Conduct](./CODE_OF_CONDUCT.md). Failure, or 40 | unwillingness, to do so will result in contributions being respectfully 41 | declined. 42 | 43 | A *bad actor* is someone who repeatedly violates the *spirit* of the Code of 44 | Conduct through consistent failure to self-regulate the way in which they 45 | interact with other contributors in the project. In doing so, bad actors 46 | alienate other contributors, discourage collaboration, and generally reflect 47 | poorly on the project as a whole. 48 | 49 | Being a bad actor may be intentional or unintentional. Typically, unintentional 50 | bad behavior can be easily corrected by being quick to apologize and correct 51 | course *even if you are not entirely convinced you need to*. Giving other 52 | contributors the benefit of the doubt and having a sincere willingness to admit 53 | that you *might* be wrong is critical for any successful open collaboration. 54 | 55 | Don't be a bad actor. 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - staging 8 | - trying 9 | 10 | env: 11 | RUSTFLAGS: -Dwarnings 12 | 13 | jobs: 14 | build_and_test: 15 | name: Build and test 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macOS-latest] 20 | rust: [stable] 21 | 22 | steps: 23 | - uses: actions/checkout@master 24 | 25 | - name: Install ${{ matrix.rust }} 26 | uses: actions-rs/toolchain@v1 27 | with: 28 | toolchain: ${{ matrix.rust }} 29 | target: wasm32-wasip2 30 | override: true 31 | 32 | - name: Install wasmtime 33 | uses: bytecodealliance/actions/wasmtime/setup@v1 34 | 35 | - name: check 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: check 39 | args: --all --bins --examples 40 | 41 | - name: wstd tests 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | args: -p wstd --target wasm32-wasip2 46 | 47 | - name: example tests 48 | uses: actions-rs/cargo@v1 49 | with: 50 | command: test 51 | args: -p test-programs-artifacts 52 | 53 | 54 | check_fmt_and_docs: 55 | name: Checking fmt and docs 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@master 59 | - uses: actions-rs/toolchain@v1 60 | with: 61 | toolchain: nightly 62 | components: rustfmt, clippy 63 | override: true 64 | 65 | - name: fmt 66 | run: cargo fmt --all -- --check 67 | 68 | - name: Docs 69 | run: cargo doc 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | tmp/ 3 | Cargo.lock 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.check.overrideCommand": [ 3 | "cargo", 4 | "component", 5 | "check", 6 | "--workspace", 7 | "--all-targets", 8 | "--message-format=json" 9 | ], 10 | } 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wstd" 3 | version.workspace = true 4 | license.workspace = true 5 | documentation = "https://docs.rs/wstd" 6 | description = "An async standard library for Wasm Components and WASI 0.2" 7 | readme = "README.md" 8 | edition.workspace = true 9 | authors.workspace = true 10 | keywords.workspace = true 11 | categories.workspace = true 12 | repository.workspace = true 13 | 14 | [features] 15 | default = ["json"] 16 | json = ["dep:serde", "dep:serde_json"] 17 | 18 | [dependencies] 19 | futures-core.workspace = true 20 | http.workspace = true 21 | itoa.workspace = true 22 | pin-project-lite.workspace = true 23 | slab.workspace = true 24 | wasi.workspace = true 25 | wstd-macro.workspace = true 26 | 27 | # optional 28 | serde = { workspace = true, optional = true } 29 | serde_json = { workspace = true, optional = true } 30 | 31 | [dev-dependencies] 32 | anyhow.workspace = true 33 | clap.workspace = true 34 | futures-lite.workspace = true 35 | humantime.workspace = true 36 | serde = { workspace = true, features = ["derive"] } 37 | serde_json.workspace = true 38 | 39 | [workspace] 40 | members = [ 41 | "macro", 42 | "test-programs", 43 | "test-programs/artifacts", 44 | ] 45 | resolver = "2" 46 | 47 | [workspace.package] 48 | version = "0.5.3" 49 | edition = "2021" 50 | license = "MIT OR Apache-2.0 OR Apache-2.0 WITH LLVM-exception" 51 | repository = "https://github.com/yoshuawuyts/wstd" 52 | keywords = ["WebAssembly", "async", "stdlib", "Components"] 53 | categories = ["wasm", "asynchronous"] 54 | authors = [ 55 | "Yoshua Wuyts ", 56 | "Pat Hickey ", 57 | "Dan Gohman ", 58 | ] 59 | 60 | [workspace.dependencies] 61 | anyhow = "1" 62 | cargo_metadata = "0.18.1" 63 | clap = { version = "4.5.26", features = ["derive"] } 64 | futures-core = "0.3.19" 65 | futures-lite = "1.12.0" 66 | humantime = "2.1.0" 67 | heck = "0.5" 68 | http = "1.1" 69 | itoa = "1" 70 | pin-project-lite = "0.2.8" 71 | quote = "1.0" 72 | serde= "1" 73 | serde_json = "1" 74 | slab = "0.4.9" 75 | syn = "2.0" 76 | test-log = { version = "0.2", features = ["trace"] } 77 | test-programs = { path = "test-programs" } 78 | test-programs-artifacts = { path = "test-programs/artifacts" } 79 | ureq = { version = "2.12.1", default-features = false } 80 | wasi = "0.14.0" 81 | wasmtime = "26" 82 | wasmtime-wasi = "26" 83 | wasmtime-wasi-http = "26" 84 | wstd = { path = "." } 85 | wstd-macro = { path = "macro", version = "=0.5.3" } 86 | 87 | [package.metadata.docs.rs] 88 | all-features = true 89 | targets = [ 90 | "wasm32-wasip2" 91 | ] 92 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 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 | Copyright 2020 Yoshua Wuyts 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /LICENSE-Apache-2.0_WITH_LLVM-exception: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | 205 | --- LLVM Exceptions to the Apache 2.0 License ---- 206 | 207 | As an exception, if, as a result of your compiling your source code, portions 208 | of this Software are embedded into an Object form of such source code, you 209 | may redistribute such embedded portions in such Object form without complying 210 | with the conditions of Sections 4(a), 4(b) and 4(d) of the License. 211 | 212 | In addition, if you combine or link compiled forms of this Software with 213 | software that is licensed under the GPLv2 ("Combined Software") and if a 214 | court of competent jurisdiction determines that the patent provision (Section 215 | 3), the indemnity provision (Section 9) or other Section of the License 216 | conflicts with the conditions of the GPLv2, you may retroactively and 217 | prospectively choose to deem waived or otherwise exclude such Section(s) of 218 | the License, but only in their entirety and only with respect to the Combined 219 | Software. 220 | 221 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Yoshua Wuyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

wstd

2 |
3 | 4 | An async standard library for Wasm Components and WASI 0.2 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | 13 | Crates.io version 15 | 16 | 17 | 18 | Download 20 | 21 | 22 | 23 | docs.rs docs 25 | 26 |
27 | 28 |
29 |

30 | 31 | API Docs 32 | 33 | | 34 | 35 | Releases 36 | 37 | | 38 | 39 | Contributing 40 | 41 |

42 |
43 | 44 | 45 | This is a minimal async standard library written exclusively to support Wasm 46 | Components. It exists primarily to enable people to write async-based 47 | applications in Rust before async-std, smol, or tokio land support for Wasm 48 | Components and WASI 0.2. Once those runtimes land support, it is recommended 49 | users switch to use those instead. 50 | 51 | ## Examples 52 | 53 | **TCP echo server** 54 | 55 | ```rust 56 | use wstd::io; 57 | use wstd::iter::AsyncIterator; 58 | use wstd::net::TcpListener; 59 | use wstd::runtime::block_on; 60 | 61 | fn main() -> io::Result<()> { 62 | block_on(async move { 63 | let listener = TcpListener::bind("127.0.0.1:8080").await?; 64 | println!("Listening on {}", listener.local_addr()?); 65 | println!("type `nc localhost 8080` to create a TCP client"); 66 | 67 | let mut incoming = listener.incoming(); 68 | while let Some(stream) = incoming.next().await { 69 | let stream = stream?; 70 | println!("Accepted from: {}", stream.peer_addr()?); 71 | io::copy(&stream, &stream).await?; 72 | } 73 | Ok(()) 74 | }) 75 | } 76 | ``` 77 | 78 | ## Installation 79 | ```sh 80 | $ cargo add wstd 81 | ``` 82 | 83 | ## Safety 84 | This crate uses ``#![forbid(unsafe_code)]`` to ensure everything is implemented in 85 | 100% Safe Rust. 86 | 87 | ## Contributing 88 | Want to join us? Check out our ["Contributing" guide][contributing] and take a 89 | look at some of these issues: 90 | 91 | - [Issues labeled "good first issue"][good-first-issue] 92 | - [Issues labeled "help wanted"][help-wanted] 93 | 94 | [contributing]: https://github.com/yoshuawuyts/wstd/blob/master.github/CONTRIBUTING.md 95 | [good-first-issue]: https://github.com/yoshuawuyts/wstd/labels/good%20first%20issue 96 | [help-wanted]: https://github.com/yoshuawuyts/wstd/labels/help%20wanted 97 | 98 | ## License 99 | 100 | 101 | Licensed under either of Apache License, Version 102 | 2.0 or MIT license at your option. 103 | 104 | 105 |
106 | 107 | 108 | Unless you explicitly state otherwise, any contribution intentionally submitted 109 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 110 | be dual licensed as above, without any additional terms or conditions. 111 | 112 | -------------------------------------------------------------------------------- /examples/complex_http_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use clap::{ArgAction, Parser}; 3 | use std::str::FromStr; 4 | use wstd::http::{ 5 | body::BodyForthcoming, Client, HeaderMap, HeaderName, HeaderValue, Method, Request, Uri, 6 | }; 7 | 8 | /// Complex HTTP client 9 | /// 10 | /// A somewhat more complex command-line HTTP client, implemented using 11 | /// `wstd`, using WASI. 12 | #[derive(Parser, Debug)] 13 | #[command(version, about)] 14 | struct Args { 15 | /// The URL to request 16 | url: Uri, 17 | 18 | /// Forward stdin to the request body 19 | #[arg(long)] 20 | body: bool, 21 | 22 | /// Add a header to the request 23 | #[arg(long = "header", action = ArgAction::Append, value_name = "HEADER")] 24 | headers: Vec, 25 | 26 | /// Add a trailer to the request 27 | #[arg(long = "trailer", action = ArgAction::Append, value_name = "TRAILER")] 28 | trailers: Vec, 29 | 30 | /// Method of the request 31 | #[arg(long, default_value = "GET")] 32 | method: Method, 33 | 34 | /// Set the connect timeout 35 | #[arg(long, value_name = "DURATION")] 36 | connect_timeout: Option, 37 | 38 | /// Set the first-byte timeout 39 | #[arg(long, value_name = "DURATION")] 40 | first_byte_timeout: Option, 41 | 42 | /// Set the between-bytes timeout 43 | #[arg(long, value_name = "DURATION")] 44 | between_bytes_timeout: Option, 45 | } 46 | 47 | #[wstd::main] 48 | async fn main() -> Result<()> { 49 | let args = Args::parse(); 50 | 51 | // Create and configure the `Client` 52 | 53 | let mut client = Client::new(); 54 | 55 | if let Some(connect_timeout) = args.connect_timeout { 56 | client.set_connect_timeout(*connect_timeout); 57 | } 58 | if let Some(first_byte_timeout) = args.first_byte_timeout { 59 | client.set_first_byte_timeout(*first_byte_timeout); 60 | } 61 | if let Some(between_bytes_timeout) = args.between_bytes_timeout { 62 | client.set_between_bytes_timeout(*between_bytes_timeout); 63 | } 64 | 65 | // Create and configure the request. 66 | 67 | let mut request = Request::builder(); 68 | 69 | request = request.uri(args.url).method(args.method); 70 | 71 | for header in args.headers { 72 | let mut parts = header.splitn(2, ": "); 73 | let key = parts.next().unwrap(); 74 | let value = parts 75 | .next() 76 | .ok_or_else(|| anyhow!("headers must be formatted like \"key: value\""))?; 77 | request = request.header(key, value); 78 | } 79 | let mut trailers = HeaderMap::new(); 80 | for trailer in args.trailers { 81 | let mut parts = trailer.splitn(2, ": "); 82 | let key = parts.next().unwrap(); 83 | let value = parts 84 | .next() 85 | .ok_or_else(|| anyhow!("trailers must be formatted like \"key: value\""))?; 86 | trailers.insert(HeaderName::from_str(key)?, HeaderValue::from_str(value)?); 87 | } 88 | 89 | // Send the request. 90 | 91 | let request = request.body(BodyForthcoming)?; 92 | 93 | eprintln!("> {} / {:?}", request.method(), request.version()); 94 | for (key, value) in request.headers().iter() { 95 | let value = String::from_utf8_lossy(value.as_bytes()); 96 | eprintln!("> {key}: {value}"); 97 | } 98 | 99 | let (mut outgoing_body, response) = client.start_request(request).await?; 100 | 101 | if args.body { 102 | wstd::io::copy(wstd::io::stdin(), &mut outgoing_body).await?; 103 | } else { 104 | wstd::io::copy(wstd::io::empty(), &mut outgoing_body).await?; 105 | } 106 | 107 | if !trailers.is_empty() { 108 | eprintln!("..."); 109 | } 110 | for (key, value) in trailers.iter() { 111 | let value = String::from_utf8_lossy(value.as_bytes()); 112 | eprintln!("> {key}: {value}"); 113 | } 114 | 115 | Client::finish(outgoing_body, Some(trailers))?; 116 | 117 | let response = response.await?; 118 | 119 | // Print the response. 120 | 121 | eprintln!("< {:?} {}", response.version(), response.status()); 122 | for (key, value) in response.headers().iter() { 123 | let value = String::from_utf8_lossy(value.as_bytes()); 124 | eprintln!("< {key}: {value}"); 125 | } 126 | 127 | let mut body = response.into_body(); 128 | wstd::io::copy(&mut body, wstd::io::stdout()).await?; 129 | 130 | let trailers = body.finish().await?; 131 | if let Some(trailers) = trailers { 132 | for (key, value) in trailers.iter() { 133 | let value = String::from_utf8_lossy(value.as_bytes()); 134 | eprintln!("< {key}: {value}"); 135 | } 136 | } 137 | 138 | Ok(()) 139 | } 140 | -------------------------------------------------------------------------------- /examples/http_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use clap::{ArgAction, Parser}; 3 | use wstd::http::{ 4 | body::{IncomingBody, StreamedBody}, 5 | request::Builder, 6 | Body, Client, Method, Request, Response, Uri, 7 | }; 8 | 9 | /// Simple HTTP client 10 | /// 11 | /// A simple command-line HTTP client, implemented using `wstd`, using WASI. 12 | #[derive(Parser, Debug)] 13 | #[command(version, about)] 14 | struct Args { 15 | /// The URL to request 16 | url: Uri, 17 | 18 | /// Forward stdin to the request body 19 | #[arg(long)] 20 | body: bool, 21 | 22 | /// Add a header to the request 23 | #[arg(long = "header", action = ArgAction::Append, value_name = "HEADER")] 24 | headers: Vec, 25 | 26 | /// Method of the request 27 | #[arg(long, default_value = "GET")] 28 | method: Method, 29 | 30 | /// Set the connect timeout 31 | #[arg(long, value_name = "DURATION")] 32 | connect_timeout: Option, 33 | 34 | /// Set the first-byte timeout 35 | #[arg(long, value_name = "DURATION")] 36 | first_byte_timeout: Option, 37 | 38 | /// Set the between-bytes timeout 39 | #[arg(long, value_name = "DURATION")] 40 | between_bytes_timeout: Option, 41 | } 42 | 43 | #[wstd::main] 44 | async fn main() -> Result<()> { 45 | let args = Args::parse(); 46 | 47 | // Create and configure the `Client` 48 | 49 | let mut client = Client::new(); 50 | 51 | if let Some(connect_timeout) = args.connect_timeout { 52 | client.set_connect_timeout(*connect_timeout); 53 | } 54 | if let Some(first_byte_timeout) = args.first_byte_timeout { 55 | client.set_first_byte_timeout(*first_byte_timeout); 56 | } 57 | if let Some(between_bytes_timeout) = args.between_bytes_timeout { 58 | client.set_between_bytes_timeout(*between_bytes_timeout); 59 | } 60 | 61 | // Create and configure the request. 62 | 63 | let mut request = Request::builder(); 64 | 65 | request = request.uri(args.url).method(args.method); 66 | 67 | for header in args.headers { 68 | let mut parts = header.splitn(2, ": "); 69 | let key = parts.next().unwrap(); 70 | let value = parts 71 | .next() 72 | .ok_or_else(|| anyhow!("headers must be formatted like \"key: value\""))?; 73 | request = request.header(key, value); 74 | } 75 | 76 | // Send the request. 77 | 78 | async fn send_request( 79 | client: &Client, 80 | request: Builder, 81 | body: B, 82 | ) -> Result> { 83 | let request = request.body(body)?; 84 | 85 | eprintln!("> {} / {:?}", request.method(), request.version()); 86 | for (key, value) in request.headers().iter() { 87 | let value = String::from_utf8_lossy(value.as_bytes()); 88 | eprintln!("> {key}: {value}"); 89 | } 90 | 91 | Ok(client.send(request).await?) 92 | } 93 | let response = if args.body { 94 | send_request(&client, request, StreamedBody::new(wstd::io::stdin())).await 95 | } else { 96 | send_request(&client, request, wstd::io::empty()).await 97 | }?; 98 | 99 | // Print the response. 100 | 101 | eprintln!("< {:?} {}", response.version(), response.status()); 102 | for (key, value) in response.headers().iter() { 103 | let value = String::from_utf8_lossy(value.as_bytes()); 104 | eprintln!("< {key}: {value}"); 105 | } 106 | 107 | let mut body = response.into_body(); 108 | wstd::io::copy(&mut body, wstd::io::stdout()).await?; 109 | 110 | let trailers = body.finish().await?; 111 | if let Some(trailers) = trailers { 112 | for (key, value) in trailers.iter() { 113 | let value = String::from_utf8_lossy(value.as_bytes()); 114 | eprintln!("< {key}: {value}"); 115 | } 116 | } 117 | 118 | Ok(()) 119 | } 120 | -------------------------------------------------------------------------------- /examples/http_server.rs: -------------------------------------------------------------------------------- 1 | use wstd::http::body::{BodyForthcoming, IncomingBody, OutgoingBody}; 2 | use wstd::http::server::{Finished, Responder}; 3 | use wstd::http::{IntoBody, Request, Response, StatusCode}; 4 | use wstd::io::{copy, empty, AsyncWrite}; 5 | use wstd::time::{Duration, Instant}; 6 | 7 | #[wstd::http_server] 8 | async fn main(request: Request, responder: Responder) -> Finished { 9 | match request.uri().path_and_query().unwrap().as_str() { 10 | "/wait" => http_wait(request, responder).await, 11 | "/echo" => http_echo(request, responder).await, 12 | "/echo-headers" => http_echo_headers(request, responder).await, 13 | "/echo-trailers" => http_echo_trailers(request, responder).await, 14 | "/fail" => http_fail(request, responder).await, 15 | "/bigfail" => http_bigfail(request, responder).await, 16 | "/" => http_home(request, responder).await, 17 | _ => http_not_found(request, responder).await, 18 | } 19 | } 20 | 21 | async fn http_home(_request: Request, responder: Responder) -> Finished { 22 | // To send a single string as the response body, use `Responder::respond`. 23 | responder 24 | .respond(Response::new("Hello, wasi:http/proxy world!\n".into_body())) 25 | .await 26 | } 27 | 28 | async fn http_wait(_request: Request, responder: Responder) -> Finished { 29 | // Get the time now 30 | let now = Instant::now(); 31 | 32 | // Sleep for one second. 33 | wstd::task::sleep(Duration::from_secs(1)).await; 34 | 35 | // Compute how long we slept for. 36 | let elapsed = Instant::now().duration_since(now).as_millis(); 37 | 38 | // To stream data to the response body, use `Responder::start_response`. 39 | let mut body = responder.start_response(Response::new(BodyForthcoming)); 40 | let result = body 41 | .write_all(format!("slept for {elapsed} millis\n").as_bytes()) 42 | .await; 43 | Finished::finish(body, result, None) 44 | } 45 | 46 | async fn http_echo(mut request: Request, responder: Responder) -> Finished { 47 | // Stream data from the request body to the response body. 48 | let mut body = responder.start_response(Response::new(BodyForthcoming)); 49 | let result = copy(request.body_mut(), &mut body).await; 50 | Finished::finish(body, result, None) 51 | } 52 | 53 | async fn http_fail(_request: Request, responder: Responder) -> Finished { 54 | let body = responder.start_response(Response::new(BodyForthcoming)); 55 | Finished::fail(body) 56 | } 57 | 58 | async fn http_bigfail(_request: Request, responder: Responder) -> Finished { 59 | async fn write_body(body: &mut OutgoingBody) -> wstd::io::Result<()> { 60 | for _ in 0..0x10 { 61 | body.write_all("big big big big\n".as_bytes()).await?; 62 | } 63 | body.flush().await?; 64 | Ok(()) 65 | } 66 | 67 | let mut body = responder.start_response(Response::new(BodyForthcoming)); 68 | let _ = write_body(&mut body).await; 69 | Finished::fail(body) 70 | } 71 | 72 | async fn http_echo_headers(request: Request, responder: Responder) -> Finished { 73 | let mut response = Response::builder(); 74 | *response.headers_mut().unwrap() = request.into_parts().0.headers; 75 | let response = response.body(empty()).unwrap(); 76 | responder.respond(response).await 77 | } 78 | 79 | async fn http_echo_trailers(request: Request, responder: Responder) -> Finished { 80 | let body = responder.start_response(Response::new(BodyForthcoming)); 81 | let (trailers, result) = match request.into_body().finish().await { 82 | Ok(trailers) => (trailers, Ok(())), 83 | Err(err) => (Default::default(), Err(std::io::Error::other(err))), 84 | }; 85 | Finished::finish(body, result, trailers) 86 | } 87 | 88 | async fn http_not_found(_request: Request, responder: Responder) -> Finished { 89 | let response = Response::builder() 90 | .status(StatusCode::NOT_FOUND) 91 | .body(empty()) 92 | .unwrap(); 93 | responder.respond(response).await 94 | } 95 | -------------------------------------------------------------------------------- /examples/tcp_echo_server.rs: -------------------------------------------------------------------------------- 1 | use wstd::io; 2 | use wstd::iter::AsyncIterator; 3 | use wstd::net::TcpListener; 4 | 5 | #[wstd::main] 6 | async fn main() -> io::Result<()> { 7 | let listener = TcpListener::bind("127.0.0.1:8080").await?; 8 | println!("Listening on {}", listener.local_addr()?); 9 | println!("type `nc localhost 8080` to create a TCP client"); 10 | 11 | let mut incoming = listener.incoming(); 12 | while let Some(stream) = incoming.next().await { 13 | let stream = stream?; 14 | println!("Accepted from: {}", stream.peer_addr()?); 15 | io::copy(&stream, &stream).await?; 16 | } 17 | Ok(()) 18 | } 19 | -------------------------------------------------------------------------------- /macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wstd-macro" 3 | version.workspace = true 4 | edition.workspace = true 5 | license.workspace = true 6 | authors.workspace = true 7 | keywords.workspace = true 8 | categories.workspace = true 9 | description = "proc-macros for the wstd crate" 10 | documentation = "https://docs.rs/wstd-macro" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | syn = { workspace = true, features = ["full"] } 17 | quote.workspace = true 18 | -------------------------------------------------------------------------------- /macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{quote, quote_spanned}; 3 | use syn::{parse_macro_input, spanned::Spanned, ItemFn}; 4 | 5 | #[proc_macro_attribute] 6 | pub fn attr_macro_main(_attr: TokenStream, item: TokenStream) -> TokenStream { 7 | let input = parse_macro_input!(item as ItemFn); 8 | 9 | if input.sig.asyncness.is_none() { 10 | return quote_spanned! { input.sig.fn_token.span()=> 11 | compile_error!("fn must be `async fn`"); 12 | } 13 | .into(); 14 | } 15 | 16 | if input.sig.ident != "main" { 17 | return quote_spanned! { input.sig.ident.span()=> 18 | compile_error!("only `async fn main` can be used for #[wstd::main]"); 19 | } 20 | .into(); 21 | } 22 | 23 | if !input.sig.inputs.is_empty() { 24 | return quote_spanned! { input.sig.inputs.span()=> 25 | compile_error!("arguments to main are not supported"); 26 | } 27 | .into(); 28 | } 29 | let attrs = input.attrs; 30 | let output = input.sig.output; 31 | let block = input.block; 32 | quote! { 33 | pub fn main() #output { 34 | 35 | #(#attrs)* 36 | async fn __run() #output { 37 | #block 38 | } 39 | 40 | ::wstd::runtime::block_on(async { 41 | __run().await 42 | }) 43 | } 44 | } 45 | .into() 46 | } 47 | 48 | #[proc_macro_attribute] 49 | pub fn attr_macro_test(_attr: TokenStream, item: TokenStream) -> TokenStream { 50 | let input = parse_macro_input!(item as ItemFn); 51 | 52 | if input.sig.asyncness.is_none() { 53 | return quote_spanned! { input.sig.fn_token.span()=> 54 | compile_error!("fn must be `async fn`"); 55 | } 56 | .into(); 57 | } 58 | 59 | let name = input.sig.ident; 60 | 61 | if !input.sig.inputs.is_empty() { 62 | return quote_spanned! { input.sig.inputs.span()=> 63 | compile_error!("arguments to main are not supported"); 64 | } 65 | .into(); 66 | } 67 | let attrs = input.attrs; 68 | let output = input.sig.output; 69 | let block = input.block; 70 | quote! { 71 | #[test] 72 | pub fn #name() #output { 73 | 74 | #(#attrs)* 75 | async fn __run() #output { 76 | #block 77 | } 78 | 79 | ::wstd::runtime::block_on(async { 80 | __run().await 81 | }) 82 | } 83 | } 84 | .into() 85 | } 86 | 87 | /// Enables a HTTP server main function, for creating [HTTP servers]. 88 | /// 89 | /// [HTTP servers]: https://docs.rs/wstd/latest/wstd/http/server/index.html 90 | /// 91 | /// # Examples 92 | /// 93 | /// ```ignore 94 | /// #[wstd::http_server] 95 | /// async fn main(request: Request, responder: Responder) -> Finished { 96 | /// responder 97 | /// .respond(Response::new("Hello!\n".into_body())) 98 | /// .await 99 | /// } 100 | /// ``` 101 | #[proc_macro_attribute] 102 | pub fn attr_macro_http_server(_attr: TokenStream, item: TokenStream) -> TokenStream { 103 | let input = parse_macro_input!(item as ItemFn); 104 | 105 | if input.sig.asyncness.is_none() { 106 | return quote_spanned! { input.sig.fn_token.span()=> 107 | compile_error!("fn must be `async fn`"); 108 | } 109 | .into(); 110 | } 111 | 112 | let output = &input.sig.output; 113 | let inputs = &input.sig.inputs; 114 | let name = &input.sig.ident; 115 | let body = &input.block; 116 | let attrs = &input.attrs; 117 | let vis = &input.vis; 118 | 119 | if name != "main" { 120 | return quote_spanned! { input.sig.ident.span()=> 121 | compile_error!("only `async fn main` can be used for #[wstd::http_server]"); 122 | } 123 | .into(); 124 | } 125 | 126 | quote! { 127 | struct TheServer; 128 | 129 | impl ::wstd::wasi::exports::http::incoming_handler::Guest for TheServer { 130 | fn handle( 131 | request: ::wstd::wasi::http::types::IncomingRequest, 132 | response_out: ::wstd::wasi::http::types::ResponseOutparam 133 | ) { 134 | #(#attrs)* 135 | #vis async fn __run(#inputs) #output { 136 | #body 137 | } 138 | 139 | let responder = ::wstd::http::server::Responder::new(response_out); 140 | let _finished: ::wstd::http::server::Finished = 141 | match ::wstd::http::request::try_from_incoming(request) 142 | { 143 | Ok(request) => ::wstd::runtime::block_on(async { __run(request, responder).await }), 144 | Err(err) => responder.fail(err), 145 | }; 146 | } 147 | } 148 | 149 | ::wstd::wasi::http::proxy::export!(TheServer with_types_in ::wstd::wasi); 150 | 151 | // Provide an actual function named `main`. 152 | // 153 | // WASI HTTP server components don't use a traditional `main` function. 154 | // They export a function named `handle` which takes a `Request` 155 | // argument, and which may be called multiple times on the same 156 | // instance. To let users write a familiar `fn main` in a file 157 | // named src/main.rs, we provide this `wstd::http_server` macro, which 158 | // transforms the user's `fn main` into the appropriate `handle` 159 | // function. 160 | // 161 | // However, when the top-level file is named src/main.rs, rustc 162 | // requires there to be a function named `main` somewhere in it. This 163 | // requirement can be disabled using `#![no_main]`, however we can't 164 | // use that automatically because macros can't contain inner 165 | // attributes, and we don't want to require users to add `#![no_main]` 166 | // in their own code. 167 | // 168 | // So, we include a definition of a function named `main` here, which 169 | // isn't intended to ever be called, and exists just to satify the 170 | // requirement for a `main` function. 171 | // 172 | // Users could use `#![no_main]` if they want to. Or, they could name 173 | // their top-level file src/lib.rs and add 174 | // ```toml 175 | // [lib] 176 | // crate-type = ["cdylib"] 177 | // ``` 178 | // to their Cargo.toml. With either of these, this "main" function will 179 | // be ignored as dead code. 180 | fn main() { 181 | unreachable!("HTTP server components should be run with `handle` rather than `run`") 182 | } 183 | } 184 | .into() 185 | } 186 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.82.0" 3 | targets = ["wasm32-wasip2"] 4 | -------------------------------------------------------------------------------- /src/future/delay.rs: -------------------------------------------------------------------------------- 1 | use futures_core::ready; 2 | use std::future::Future; 3 | use std::pin::Pin; 4 | use std::task::{Context, Poll}; 5 | 6 | use pin_project_lite::pin_project; 7 | 8 | pin_project! { 9 | /// Suspends a future until the specified deadline. 10 | /// 11 | /// This `struct` is created by the [`delay`] method on [`FutureExt`]. See its 12 | /// documentation for more. 13 | /// 14 | /// [`delay`]: crate::future::FutureExt::delay 15 | /// [`FutureExt`]: crate::future::futureExt 16 | #[must_use = "futures do nothing unless polled or .awaited"] 17 | pub struct Delay { 18 | #[pin] 19 | future: F, 20 | #[pin] 21 | deadline: D, 22 | state: State, 23 | } 24 | } 25 | 26 | /// The internal state 27 | #[derive(Debug)] 28 | enum State { 29 | Started, 30 | PollFuture, 31 | Completed, 32 | } 33 | 34 | impl Delay { 35 | pub(super) fn new(future: F, deadline: D) -> Self { 36 | Self { 37 | future, 38 | deadline, 39 | state: State::Started, 40 | } 41 | } 42 | } 43 | 44 | impl Future for Delay { 45 | type Output = F::Output; 46 | 47 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 48 | let mut this = self.project(); 49 | loop { 50 | match this.state { 51 | State::Started => { 52 | ready!(this.deadline.as_mut().poll(cx)); 53 | *this.state = State::PollFuture; 54 | } 55 | State::PollFuture => { 56 | let value = ready!(this.future.as_mut().poll(cx)); 57 | *this.state = State::Completed; 58 | return Poll::Ready(value); 59 | } 60 | State::Completed => panic!("future polled after completing"), 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/future/future_ext.rs: -------------------------------------------------------------------------------- 1 | use super::{Delay, Timeout}; 2 | use std::future::{Future, IntoFuture}; 3 | 4 | /// Extend `Future` with time-based operations. 5 | pub trait FutureExt: Future { 6 | /// Return an error if a future does not complete within a given time span. 7 | /// 8 | /// Typically timeouts are, as the name implies, based on _time_. However 9 | /// this method can time out based on any future. This can be useful in 10 | /// combination with channels, as it allows (long-lived) futures to be 11 | /// cancelled based on some external event. 12 | /// 13 | /// When a timeout is returned, the future will be dropped and destructors 14 | /// will be run. 15 | /// 16 | /// # Example 17 | /// 18 | /// ```no_run 19 | /// use wstd::prelude::*; 20 | /// use wstd::time::{Instant, Duration}; 21 | /// use std::io; 22 | /// 23 | /// #[wstd::main] 24 | /// async fn main() { 25 | /// let res = async { "meow" } 26 | /// .delay(Duration::from_millis(100)) // longer delay 27 | /// .timeout(Duration::from_millis(50)) // shorter timeout 28 | /// .await; 29 | /// assert_eq!(res.unwrap_err().kind(), io::ErrorKind::TimedOut); // error 30 | /// 31 | /// let res = async { "meow" } 32 | /// .delay(Duration::from_millis(50)) // shorter delay 33 | /// .timeout(Duration::from_millis(100)) // longer timeout 34 | /// .await; 35 | /// assert_eq!(res.unwrap(), "meow"); // success 36 | /// } 37 | /// ``` 38 | fn timeout(self, deadline: D) -> Timeout 39 | where 40 | Self: Sized, 41 | D: IntoFuture, 42 | { 43 | Timeout::new(self, deadline.into_future()) 44 | } 45 | 46 | /// Delay resolving the future until the given deadline. 47 | /// 48 | /// The underlying future will not be polled until the deadline has expired. In addition 49 | /// to using a time source as a deadline, any future can be used as a 50 | /// deadline too. When used in combination with a multi-consumer channel, 51 | /// this method can be used to synchronize the start of multiple futures and streams. 52 | /// 53 | /// # Example 54 | /// 55 | /// ```no_run 56 | /// use wstd::prelude::*; 57 | /// use wstd::time::{Instant, Duration}; 58 | /// 59 | /// #[wstd::main] 60 | /// async fn main() { 61 | /// let now = Instant::now(); 62 | /// let delay = Duration::from_millis(100); 63 | /// let _ = async { "meow" }.delay(delay).await; 64 | /// assert!(now.elapsed() >= delay); 65 | /// } 66 | /// ``` 67 | fn delay(self, deadline: D) -> Delay 68 | where 69 | Self: Sized, 70 | D: IntoFuture, 71 | { 72 | Delay::new(self, deadline.into_future()) 73 | } 74 | } 75 | 76 | impl FutureExt for T where T: Future {} 77 | -------------------------------------------------------------------------------- /src/future/mod.rs: -------------------------------------------------------------------------------- 1 | //! Asynchronous values. 2 | //! 3 | //! # Cancellation 4 | //! 5 | //! Futures can be cancelled by dropping them before they finish executing. This 6 | //! is useful when we're no longer interested in the result of an operation, as 7 | //! it allows us to stop doing needless work. This also means that a future may cancel at any `.await` point, and so just 8 | //! like with `?` we have to be careful to roll back local state if our future 9 | //! halts there. 10 | //! 11 | //! 12 | //! ```no_run 13 | //! use futures_lite::prelude::*; 14 | //! use wstd::prelude::*; 15 | //! use wstd::time::Duration; 16 | //! 17 | //! #[wstd::main] 18 | //! async fn main() { 19 | //! let mut counter = 0; 20 | //! let value = async { "meow" } 21 | //! .delay(Duration::from_millis(100)) 22 | //! .timeout(Duration::from_millis(200)) 23 | //! .await; 24 | //! 25 | //! assert_eq!(value.unwrap(), "meow"); 26 | //! } 27 | //! ``` 28 | 29 | mod delay; 30 | mod future_ext; 31 | mod timeout; 32 | 33 | pub use delay::Delay; 34 | pub use future_ext::FutureExt; 35 | pub use timeout::Timeout; 36 | -------------------------------------------------------------------------------- /src/future/timeout.rs: -------------------------------------------------------------------------------- 1 | use crate::time::utils::timeout_err; 2 | 3 | use std::future::Future; 4 | use std::io; 5 | use std::pin::Pin; 6 | use std::task::{Context, Poll}; 7 | 8 | use pin_project_lite::pin_project; 9 | 10 | pin_project! { 11 | /// A future that times out after a duration of time. 12 | /// 13 | /// This `struct` is created by the [`timeout`] method on [`FutureExt`]. See its 14 | /// documentation for more. 15 | /// 16 | /// [`timeout`]: crate::future::FutureExt::timeout 17 | /// [`FutureExt`]: crate::future::futureExt 18 | #[must_use = "futures do nothing unless polled or .awaited"] 19 | pub struct Timeout { 20 | #[pin] 21 | future: F, 22 | #[pin] 23 | deadline: D, 24 | completed: bool, 25 | } 26 | } 27 | 28 | impl Timeout { 29 | pub(super) fn new(future: F, deadline: D) -> Self { 30 | Self { 31 | future, 32 | deadline, 33 | completed: false, 34 | } 35 | } 36 | } 37 | 38 | impl Future for Timeout { 39 | type Output = io::Result; 40 | 41 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 42 | let this = self.project(); 43 | 44 | assert!(!*this.completed, "future polled after completing"); 45 | 46 | match this.future.poll(cx) { 47 | Poll::Ready(v) => { 48 | *this.completed = true; 49 | Poll::Ready(Ok(v)) 50 | } 51 | Poll::Pending => match this.deadline.poll(cx) { 52 | Poll::Ready(_) => { 53 | *this.completed = true; 54 | Poll::Ready(Err(timeout_err("future timed out"))) 55 | } 56 | Poll::Pending => Poll::Pending, 57 | }, 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/http/body.rs: -------------------------------------------------------------------------------- 1 | //! HTTP body types 2 | 3 | use crate::http::fields::header_map_from_wasi; 4 | use crate::io::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Cursor, Empty}; 5 | use crate::runtime::AsyncPollable; 6 | use core::fmt; 7 | use http::header::CONTENT_LENGTH; 8 | use wasi::http::types::IncomingBody as WasiIncomingBody; 9 | 10 | #[cfg(feature = "json")] 11 | use serde::de::DeserializeOwned; 12 | #[cfg(feature = "json")] 13 | use serde_json; 14 | 15 | pub use super::{ 16 | error::{Error, ErrorVariant}, 17 | HeaderMap, 18 | }; 19 | 20 | #[derive(Debug)] 21 | pub(crate) enum BodyKind { 22 | Fixed(u64), 23 | Chunked, 24 | } 25 | 26 | impl BodyKind { 27 | pub(crate) fn from_headers(headers: &HeaderMap) -> Result { 28 | if let Some(value) = headers.get(CONTENT_LENGTH) { 29 | let content_length = std::str::from_utf8(value.as_ref()) 30 | .unwrap() 31 | .parse::() 32 | .map_err(|_| InvalidContentLength)?; 33 | Ok(BodyKind::Fixed(content_length)) 34 | } else { 35 | Ok(BodyKind::Chunked) 36 | } 37 | } 38 | } 39 | 40 | /// A trait representing an HTTP body. 41 | #[doc(hidden)] 42 | pub trait Body: AsyncRead { 43 | /// Returns the exact remaining length of the iterator, if known. 44 | fn len(&self) -> Option; 45 | 46 | /// Returns `true` if the body is known to be empty. 47 | fn is_empty(&self) -> bool { 48 | matches!(self.len(), Some(0)) 49 | } 50 | } 51 | 52 | /// Conversion into a `Body`. 53 | #[doc(hidden)] 54 | pub trait IntoBody { 55 | /// What type of `Body` are we turning this into? 56 | type IntoBody: Body; 57 | /// Convert into `Body`. 58 | fn into_body(self) -> Self::IntoBody; 59 | } 60 | impl IntoBody for T 61 | where 62 | T: Body, 63 | { 64 | type IntoBody = T; 65 | fn into_body(self) -> Self::IntoBody { 66 | self 67 | } 68 | } 69 | 70 | impl IntoBody for String { 71 | type IntoBody = BoundedBody>; 72 | fn into_body(self) -> Self::IntoBody { 73 | BoundedBody(Cursor::new(self.into_bytes())) 74 | } 75 | } 76 | 77 | impl IntoBody for &str { 78 | type IntoBody = BoundedBody>; 79 | fn into_body(self) -> Self::IntoBody { 80 | BoundedBody(Cursor::new(self.to_owned().into_bytes())) 81 | } 82 | } 83 | 84 | impl IntoBody for Vec { 85 | type IntoBody = BoundedBody>; 86 | fn into_body(self) -> Self::IntoBody { 87 | BoundedBody(Cursor::new(self)) 88 | } 89 | } 90 | 91 | impl IntoBody for &[u8] { 92 | type IntoBody = BoundedBody>; 93 | fn into_body(self) -> Self::IntoBody { 94 | BoundedBody(Cursor::new(self.to_owned())) 95 | } 96 | } 97 | 98 | /// An HTTP body with a known length 99 | #[derive(Debug)] 100 | pub struct BoundedBody(Cursor); 101 | 102 | impl> AsyncRead for BoundedBody { 103 | async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { 104 | self.0.read(buf).await 105 | } 106 | } 107 | impl> Body for BoundedBody { 108 | fn len(&self) -> Option { 109 | Some(self.0.get_ref().as_ref().len()) 110 | } 111 | } 112 | 113 | /// An HTTP body with an unknown length 114 | #[derive(Debug)] 115 | pub struct StreamedBody(S); 116 | 117 | impl StreamedBody { 118 | /// Wrap an `AsyncRead` impl in a type that provides a [`Body`] implementation. 119 | pub fn new(s: S) -> Self { 120 | Self(s) 121 | } 122 | } 123 | impl AsyncRead for StreamedBody { 124 | async fn read(&mut self, buf: &mut [u8]) -> crate::io::Result { 125 | self.0.read(buf).await 126 | } 127 | } 128 | impl Body for StreamedBody { 129 | fn len(&self) -> Option { 130 | None 131 | } 132 | } 133 | 134 | impl Body for Empty { 135 | fn len(&self) -> Option { 136 | Some(0) 137 | } 138 | } 139 | 140 | /// An incoming HTTP body 141 | #[derive(Debug)] 142 | pub struct IncomingBody { 143 | kind: BodyKind, 144 | // IMPORTANT: the order of these fields here matters. `body_stream` must 145 | // be dropped before `incoming_body`. 146 | body_stream: AsyncInputStream, 147 | incoming_body: WasiIncomingBody, 148 | } 149 | 150 | impl IncomingBody { 151 | pub(crate) fn new( 152 | kind: BodyKind, 153 | body_stream: AsyncInputStream, 154 | incoming_body: WasiIncomingBody, 155 | ) -> Self { 156 | Self { 157 | kind, 158 | body_stream, 159 | incoming_body, 160 | } 161 | } 162 | 163 | /// Consume this `IncomingBody` and return the trailers, if present. 164 | pub async fn finish(self) -> Result, Error> { 165 | // The stream is a child resource of the `IncomingBody`, so ensure that 166 | // it's dropped first. 167 | drop(self.body_stream); 168 | 169 | let trailers = WasiIncomingBody::finish(self.incoming_body); 170 | 171 | AsyncPollable::new(trailers.subscribe()).wait_for().await; 172 | 173 | let trailers = trailers.get().unwrap().unwrap()?; 174 | 175 | let trailers = match trailers { 176 | None => None, 177 | Some(trailers) => Some(header_map_from_wasi(trailers)?), 178 | }; 179 | 180 | Ok(trailers) 181 | } 182 | 183 | /// Try to deserialize the incoming body as JSON. The optional 184 | /// `json` feature is required. 185 | /// 186 | /// Fails whenever the response body is not in JSON format, 187 | /// or it cannot be properly deserialized to target type `T`. For more 188 | /// details please see [`serde_json::from_reader`]. 189 | /// 190 | /// [`serde_json::from_reader`]: https://docs.serde.rs/serde_json/fn.from_reader.html 191 | #[cfg(feature = "json")] 192 | pub async fn json(&mut self) -> Result { 193 | let buf = self.bytes().await?; 194 | serde_json::from_slice(&buf).map_err(|e| ErrorVariant::Other(e.to_string()).into()) 195 | } 196 | 197 | /// Get the full response body as `Vec`. 198 | pub async fn bytes(&mut self) -> Result, Error> { 199 | let mut buf = match self.kind { 200 | BodyKind::Fixed(l) => { 201 | if l > (usize::MAX as u64) { 202 | return Err(ErrorVariant::Other( 203 | "incoming body is too large to allocate and buffer in memory".to_string(), 204 | ) 205 | .into()); 206 | } else { 207 | Vec::with_capacity(l as usize) 208 | } 209 | } 210 | BodyKind::Chunked => Vec::with_capacity(4096), 211 | }; 212 | self.read_to_end(&mut buf).await?; 213 | Ok(buf) 214 | } 215 | } 216 | 217 | impl AsyncRead for IncomingBody { 218 | async fn read(&mut self, out_buf: &mut [u8]) -> crate::io::Result { 219 | self.body_stream.read(out_buf).await 220 | } 221 | 222 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 223 | Some(&self.body_stream) 224 | } 225 | } 226 | 227 | impl Body for IncomingBody { 228 | fn len(&self) -> Option { 229 | match self.kind { 230 | BodyKind::Fixed(l) => { 231 | if l > (usize::MAX as u64) { 232 | None 233 | } else { 234 | Some(l as usize) 235 | } 236 | } 237 | BodyKind::Chunked => None, 238 | } 239 | } 240 | } 241 | 242 | #[derive(Debug)] 243 | pub struct InvalidContentLength; 244 | 245 | impl fmt::Display for InvalidContentLength { 246 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 247 | "incoming content-length should be a u64; violates HTTP/1.1".fmt(f) 248 | } 249 | } 250 | 251 | impl std::error::Error for InvalidContentLength {} 252 | 253 | impl From for Error { 254 | fn from(e: InvalidContentLength) -> Self { 255 | // TODO: What's the right error code here? 256 | ErrorVariant::Other(e.to_string()).into() 257 | } 258 | } 259 | 260 | /// The output stream for the body, implementing [`AsyncWrite`]. Call 261 | /// [`Responder::start_response`] or [`Client::start_request`] to obtain 262 | /// one. Once the body is complete, it must be declared finished, using 263 | /// [`Finished::finish`], [`Finished::fail`], [`Client::finish`], or 264 | /// [`Client::fail`]. 265 | /// 266 | /// [`Responder::start_response`]: crate::http::server::Responder::start_response 267 | /// [`Client::start_request`]: crate::http::client::Client::start_request 268 | /// [`Finished::finish`]: crate::http::server::Finished::finish 269 | /// [`Finished::fail`]: crate::http::server::Finished::fail 270 | /// [`Client::finish`]: crate::http::client::Client::finish 271 | /// [`Client::fail`]: crate::http::client::Client::fail 272 | #[must_use] 273 | pub struct OutgoingBody { 274 | // IMPORTANT: the order of these fields here matters. `stream` must 275 | // be dropped before `body`. 276 | stream: AsyncOutputStream, 277 | body: wasi::http::types::OutgoingBody, 278 | dontdrop: DontDropOutgoingBody, 279 | } 280 | 281 | impl OutgoingBody { 282 | pub(crate) fn new(stream: AsyncOutputStream, body: wasi::http::types::OutgoingBody) -> Self { 283 | Self { 284 | stream, 285 | body, 286 | dontdrop: DontDropOutgoingBody, 287 | } 288 | } 289 | 290 | pub(crate) fn consume(self) -> (AsyncOutputStream, wasi::http::types::OutgoingBody) { 291 | let Self { 292 | stream, 293 | body, 294 | dontdrop, 295 | } = self; 296 | 297 | std::mem::forget(dontdrop); 298 | 299 | (stream, body) 300 | } 301 | 302 | /// Return a reference to the underlying `AsyncOutputStream`. 303 | /// 304 | /// This usually isn't needed, as `OutgoingBody` implements `AsyncWrite` 305 | /// too, however it is useful for code that expects to work with 306 | /// `AsyncOutputStream` specifically. 307 | pub fn stream(&mut self) -> &mut AsyncOutputStream { 308 | &mut self.stream 309 | } 310 | } 311 | 312 | impl AsyncWrite for OutgoingBody { 313 | async fn write(&mut self, buf: &[u8]) -> crate::io::Result { 314 | self.stream.write(buf).await 315 | } 316 | 317 | async fn flush(&mut self) -> crate::io::Result<()> { 318 | self.stream.flush().await 319 | } 320 | 321 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 322 | Some(&self.stream) 323 | } 324 | } 325 | 326 | /// A utility to ensure that `OutgoingBody` is either finished or failed, and 327 | /// not implicitly dropped. 328 | struct DontDropOutgoingBody; 329 | 330 | impl Drop for DontDropOutgoingBody { 331 | fn drop(&mut self) { 332 | unreachable!("`OutgoingBody::drop` called; `OutgoingBody`s should be consumed with `finish` or `fail`."); 333 | } 334 | } 335 | 336 | /// A placeholder for use as the type parameter to [`Request`] and [`Response`] 337 | /// to indicate that the body has not yet started. This is used with 338 | /// [`Client::start_request`] and [`Responder::start_response`], which have 339 | /// `Requeset` and `Response` arguments, 340 | /// respectively. 341 | /// 342 | /// To instead start the response and obtain the output stream for the body, 343 | /// use [`Responder::respond`]. 344 | /// To instead send a request or response with an input stream for the body, 345 | /// use [`Client::send`] or [`Responder::respond`]. 346 | /// 347 | /// [`Request`]: crate::http::Request 348 | /// [`Response`]: crate::http::Response 349 | /// [`Client::start_request`]: crate::http::Client::start_request 350 | /// [`Responder::start_response`]: crate::http::server::Responder::start_response 351 | /// [`Client::send`]: crate::http::Client::send 352 | /// [`Responder::respond`]: crate::http::server::Responder::respond 353 | pub struct BodyForthcoming; 354 | -------------------------------------------------------------------------------- /src/http/client.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | body::{BodyForthcoming, IncomingBody, OutgoingBody}, 3 | fields::header_map_to_wasi, 4 | Body, Error, HeaderMap, Request, Response, Result, 5 | }; 6 | use crate::http::request::try_into_outgoing; 7 | use crate::http::response::try_from_incoming; 8 | use crate::io::{self, AsyncOutputStream, AsyncPollable}; 9 | use crate::runtime::WaitFor; 10 | use crate::time::Duration; 11 | use pin_project_lite::pin_project; 12 | use std::future::Future; 13 | use std::pin::Pin; 14 | use std::task::{Context, Poll}; 15 | use wasi::http::types::{ 16 | FutureIncomingResponse as WasiFutureIncomingResponse, OutgoingBody as WasiOutgoingBody, 17 | RequestOptions as WasiRequestOptions, 18 | }; 19 | 20 | /// An HTTP client. 21 | // Empty for now, but permits adding support for RequestOptions soon: 22 | #[derive(Debug)] 23 | pub struct Client { 24 | options: Option, 25 | } 26 | 27 | impl Default for Client { 28 | fn default() -> Self { 29 | Self::new() 30 | } 31 | } 32 | 33 | impl Client { 34 | /// Create a new instance of `Client` 35 | pub fn new() -> Self { 36 | Self { options: None } 37 | } 38 | 39 | /// Send an HTTP request. 40 | /// 41 | /// TODO: Should this automatically add a "Content-Length" header if the 42 | /// body size is known? 43 | /// 44 | /// To respond with trailers, use [`Client::start_request`] instead. 45 | pub async fn send(&self, req: Request) -> Result> { 46 | // We don't use `body::OutputBody` here because we can report I/O 47 | // errors from the `copy` directly. 48 | let (wasi_req, body) = try_into_outgoing(req)?; 49 | let wasi_body = wasi_req.body().unwrap(); 50 | let wasi_stream = wasi_body.write().unwrap(); 51 | 52 | // 1. Start sending the request head 53 | let res = wasi::http::outgoing_handler::handle(wasi_req, self.wasi_options()?).unwrap(); 54 | 55 | // 2. Start sending the request body 56 | io::copy(body, AsyncOutputStream::new(wasi_stream)).await?; 57 | 58 | // 3. Finish sending the request body 59 | let trailers = None; 60 | WasiOutgoingBody::finish(wasi_body, trailers).unwrap(); 61 | 62 | // 4. Receive the response 63 | AsyncPollable::new(res.subscribe()).wait_for().await; 64 | 65 | // NOTE: the first `unwrap` is to ensure readiness, the second `unwrap` 66 | // is to trap if we try and get the response more than once. The final 67 | // `?` is to raise the actual error if there is one. 68 | let res = res.get().unwrap().unwrap()?; 69 | try_from_incoming(res) 70 | } 71 | 72 | /// Start sending an HTTP request, and return an `OutgoingBody` stream to 73 | /// write the body to. 74 | /// 75 | /// The returned `OutgoingBody` must be consumed by [`Client::finish`] or 76 | /// [`Client::fail`]. 77 | pub async fn start_request( 78 | &self, 79 | req: Request, 80 | ) -> Result<( 81 | OutgoingBody, 82 | impl Future>>, 83 | )> { 84 | let (wasi_req, _body_forthcoming) = try_into_outgoing(req)?; 85 | let wasi_body = wasi_req.body().unwrap(); 86 | let wasi_stream = wasi_body.write().unwrap(); 87 | 88 | // Start sending the request head. 89 | let res = wasi::http::outgoing_handler::handle(wasi_req, self.wasi_options()?).unwrap(); 90 | 91 | let outgoing_body = OutgoingBody::new(AsyncOutputStream::new(wasi_stream), wasi_body); 92 | 93 | pin_project! { 94 | #[must_use = "futures do nothing unless polled or .awaited"] 95 | struct IncomingResponseFuture { 96 | #[pin] 97 | subscription: WaitFor, 98 | wasi: WasiFutureIncomingResponse, 99 | } 100 | } 101 | impl Future for IncomingResponseFuture { 102 | type Output = Result>; 103 | 104 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 105 | let this = self.project(); 106 | match this.subscription.poll(cx) { 107 | Poll::Pending => Poll::Pending, 108 | Poll::Ready(()) => Poll::Ready( 109 | this.wasi 110 | .get() 111 | .unwrap() 112 | .unwrap() 113 | .map_err(Error::from) 114 | .and_then(try_from_incoming), 115 | ), 116 | } 117 | } 118 | } 119 | 120 | let subscription = AsyncPollable::new(res.subscribe()).wait_for(); 121 | let future = IncomingResponseFuture { 122 | subscription, 123 | wasi: res, 124 | }; 125 | 126 | Ok((outgoing_body, future)) 127 | } 128 | 129 | /// Finish the body, optionally with trailers. 130 | /// 131 | /// This is used with [`Client::start_request`]. 132 | pub fn finish(body: OutgoingBody, trailers: Option) -> Result<()> { 133 | let (stream, body) = body.consume(); 134 | 135 | // The stream is a child resource of the `OutgoingBody`, so ensure that 136 | // it's dropped first. 137 | drop(stream); 138 | 139 | let wasi_trailers = match trailers { 140 | Some(trailers) => Some(header_map_to_wasi(&trailers)?), 141 | None => None, 142 | }; 143 | 144 | wasi::http::types::OutgoingBody::finish(body, wasi_trailers) 145 | .expect("body length did not match Content-Length header value"); 146 | Ok(()) 147 | } 148 | 149 | /// Consume the `OutgoingBody` and indicate that the body was not 150 | /// completed. 151 | /// 152 | /// This is used with [`Client::start_request`]. 153 | pub fn fail(body: OutgoingBody) { 154 | let (_stream, _body) = body.consume(); 155 | } 156 | 157 | /// Set timeout on connecting to HTTP server 158 | pub fn set_connect_timeout(&mut self, d: impl Into) { 159 | self.options_mut().connect_timeout = Some(d.into()); 160 | } 161 | 162 | /// Set timeout on recieving first byte of the Response body 163 | pub fn set_first_byte_timeout(&mut self, d: impl Into) { 164 | self.options_mut().first_byte_timeout = Some(d.into()); 165 | } 166 | 167 | /// Set timeout on recieving subsequent chunks of bytes in the Response body stream 168 | pub fn set_between_bytes_timeout(&mut self, d: impl Into) { 169 | self.options_mut().between_bytes_timeout = Some(d.into()); 170 | } 171 | 172 | fn options_mut(&mut self) -> &mut RequestOptions { 173 | match &mut self.options { 174 | Some(o) => o, 175 | uninit => { 176 | *uninit = Some(Default::default()); 177 | uninit.as_mut().unwrap() 178 | } 179 | } 180 | } 181 | 182 | fn wasi_options(&self) -> Result> { 183 | self.options 184 | .as_ref() 185 | .map(RequestOptions::to_wasi) 186 | .transpose() 187 | } 188 | } 189 | 190 | #[derive(Default, Debug)] 191 | struct RequestOptions { 192 | connect_timeout: Option, 193 | first_byte_timeout: Option, 194 | between_bytes_timeout: Option, 195 | } 196 | 197 | impl RequestOptions { 198 | fn to_wasi(&self) -> Result { 199 | let wasi = WasiRequestOptions::new(); 200 | if let Some(timeout) = self.connect_timeout { 201 | wasi.set_connect_timeout(Some(timeout.0)).map_err(|()| { 202 | Error::other("wasi-http implementation does not support connect timeout option") 203 | })?; 204 | } 205 | if let Some(timeout) = self.first_byte_timeout { 206 | wasi.set_first_byte_timeout(Some(timeout.0)).map_err(|()| { 207 | Error::other("wasi-http implementation does not support first byte timeout option") 208 | })?; 209 | } 210 | if let Some(timeout) = self.between_bytes_timeout { 211 | wasi.set_between_bytes_timeout(Some(timeout.0)) 212 | .map_err(|()| { 213 | Error::other( 214 | "wasi-http implementation does not support between byte timeout option", 215 | ) 216 | })?; 217 | } 218 | Ok(wasi) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/http/error.rs: -------------------------------------------------------------------------------- 1 | use crate::http::fields::ToWasiHeaderError; 2 | use std::fmt; 3 | 4 | /// The `http` result type. 5 | pub type Result = std::result::Result; 6 | 7 | /// The `http` error type. 8 | pub struct Error { 9 | variant: ErrorVariant, 10 | context: Vec, 11 | } 12 | 13 | pub use http::header::{InvalidHeaderName, InvalidHeaderValue}; 14 | pub use http::method::InvalidMethod; 15 | pub use wasi::http::types::{ErrorCode as WasiHttpErrorCode, HeaderError as WasiHttpHeaderError}; 16 | 17 | impl fmt::Debug for Error { 18 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 19 | for c in self.context.iter() { 20 | writeln!(f, "in {c}:")?; 21 | } 22 | match &self.variant { 23 | ErrorVariant::WasiHttp(e) => write!(f, "wasi http error: {e:?}"), 24 | ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e:?}"), 25 | ErrorVariant::HeaderName(e) => write!(f, "header name error: {e:?}"), 26 | ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e:?}"), 27 | ErrorVariant::Method(e) => write!(f, "method error: {e:?}"), 28 | ErrorVariant::BodyIo(e) => write!(f, "body error: {e:?}"), 29 | ErrorVariant::Other(e) => write!(f, "{e}"), 30 | } 31 | } 32 | } 33 | 34 | impl fmt::Display for Error { 35 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 36 | match &self.variant { 37 | ErrorVariant::WasiHttp(e) => write!(f, "wasi http error: {e}"), 38 | ErrorVariant::WasiHeader(e) => write!(f, "wasi header error: {e}"), 39 | ErrorVariant::HeaderName(e) => write!(f, "header name error: {e}"), 40 | ErrorVariant::HeaderValue(e) => write!(f, "header value error: {e}"), 41 | ErrorVariant::Method(e) => write!(f, "method error: {e}"), 42 | ErrorVariant::BodyIo(e) => write!(f, "body error: {e}"), 43 | ErrorVariant::Other(e) => write!(f, "{e}"), 44 | } 45 | } 46 | } 47 | 48 | impl std::error::Error for Error {} 49 | 50 | impl Error { 51 | pub fn variant(&self) -> &ErrorVariant { 52 | &self.variant 53 | } 54 | pub(crate) fn other(s: impl Into) -> Self { 55 | ErrorVariant::Other(s.into()).into() 56 | } 57 | pub(crate) fn context(self, s: impl Into) -> Self { 58 | let mut context = self.context; 59 | context.push(s.into()); 60 | Self { 61 | variant: self.variant, 62 | context, 63 | } 64 | } 65 | } 66 | 67 | impl From for Error { 68 | fn from(variant: ErrorVariant) -> Error { 69 | Error { 70 | variant, 71 | context: Vec::new(), 72 | } 73 | } 74 | } 75 | 76 | impl From for Error { 77 | fn from(e: WasiHttpErrorCode) -> Error { 78 | ErrorVariant::WasiHttp(e).into() 79 | } 80 | } 81 | 82 | impl From for Error { 83 | fn from(error: ToWasiHeaderError) -> Error { 84 | Error { 85 | variant: ErrorVariant::WasiHeader(error.error), 86 | context: vec![error.context], 87 | } 88 | } 89 | } 90 | 91 | impl From for Error { 92 | fn from(e: InvalidHeaderValue) -> Error { 93 | ErrorVariant::HeaderValue(e).into() 94 | } 95 | } 96 | 97 | impl From for Error { 98 | fn from(e: InvalidHeaderName) -> Error { 99 | ErrorVariant::HeaderName(e).into() 100 | } 101 | } 102 | 103 | impl From for Error { 104 | fn from(e: InvalidMethod) -> Error { 105 | ErrorVariant::Method(e).into() 106 | } 107 | } 108 | 109 | impl From for Error { 110 | fn from(e: std::io::Error) -> Error { 111 | ErrorVariant::BodyIo(e).into() 112 | } 113 | } 114 | 115 | #[derive(Debug)] 116 | pub enum ErrorVariant { 117 | WasiHttp(WasiHttpErrorCode), 118 | WasiHeader(WasiHttpHeaderError), 119 | HeaderName(InvalidHeaderName), 120 | HeaderValue(InvalidHeaderValue), 121 | Method(InvalidMethod), 122 | BodyIo(std::io::Error), 123 | Other(String), 124 | } 125 | -------------------------------------------------------------------------------- /src/http/fields.rs: -------------------------------------------------------------------------------- 1 | pub use http::header::{HeaderMap, HeaderName, HeaderValue}; 2 | 3 | use super::Error; 4 | use wasi::http::types::{Fields, HeaderError as WasiHttpHeaderError}; 5 | 6 | pub(crate) fn header_map_from_wasi(wasi_fields: Fields) -> Result { 7 | let mut output = HeaderMap::new(); 8 | for (key, value) in wasi_fields.entries() { 9 | let key = HeaderName::from_bytes(key.as_bytes()) 10 | .map_err(|e| Error::from(e).context("header name {key}"))?; 11 | let value = HeaderValue::from_bytes(&value) 12 | .map_err(|e| Error::from(e).context("header value for {key}"))?; 13 | output.append(key, value); 14 | } 15 | Ok(output) 16 | } 17 | 18 | pub(crate) fn header_map_to_wasi(header_map: &HeaderMap) -> Result { 19 | let wasi_fields = Fields::new(); 20 | for (key, value) in header_map { 21 | // Unwrap because `HeaderMap` has already validated the headers. 22 | wasi_fields 23 | .append(key.as_str(), value.as_bytes()) 24 | .map_err(|error| ToWasiHeaderError { 25 | error, 26 | context: format!("header {key}: {value:?}"), 27 | })?; 28 | } 29 | Ok(wasi_fields) 30 | } 31 | 32 | #[derive(Debug)] 33 | pub(crate) struct ToWasiHeaderError { 34 | pub(crate) error: WasiHttpHeaderError, 35 | pub(crate) context: String, 36 | } 37 | -------------------------------------------------------------------------------- /src/http/method.rs: -------------------------------------------------------------------------------- 1 | use wasi::http::types::Method as WasiMethod; 2 | 3 | use http::method::InvalidMethod; 4 | pub use http::Method; 5 | 6 | pub(crate) fn to_wasi_method(value: Method) -> WasiMethod { 7 | match value { 8 | Method::GET => WasiMethod::Get, 9 | Method::HEAD => WasiMethod::Head, 10 | Method::POST => WasiMethod::Post, 11 | Method::PUT => WasiMethod::Put, 12 | Method::DELETE => WasiMethod::Delete, 13 | Method::CONNECT => WasiMethod::Connect, 14 | Method::OPTIONS => WasiMethod::Options, 15 | Method::TRACE => WasiMethod::Trace, 16 | Method::PATCH => WasiMethod::Patch, 17 | other => WasiMethod::Other(other.as_str().to_owned()), 18 | } 19 | } 20 | 21 | pub(crate) fn from_wasi_method(value: WasiMethod) -> Result { 22 | Ok(match value { 23 | WasiMethod::Get => Method::GET, 24 | WasiMethod::Head => Method::HEAD, 25 | WasiMethod::Post => Method::POST, 26 | WasiMethod::Put => Method::PUT, 27 | WasiMethod::Delete => Method::DELETE, 28 | WasiMethod::Connect => Method::CONNECT, 29 | WasiMethod::Options => Method::OPTIONS, 30 | WasiMethod::Trace => Method::TRACE, 31 | WasiMethod::Patch => Method::PATCH, 32 | WasiMethod::Other(s) => Method::from_bytes(s.as_bytes())?, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/http/mod.rs: -------------------------------------------------------------------------------- 1 | //! HTTP networking support 2 | //! 3 | pub use http::status::StatusCode; 4 | pub use http::uri::{Authority, PathAndQuery, Uri}; 5 | 6 | #[doc(inline)] 7 | pub use body::{Body, IntoBody}; 8 | pub use client::Client; 9 | pub use error::{Error, Result}; 10 | pub use fields::{HeaderMap, HeaderName, HeaderValue}; 11 | pub use method::Method; 12 | pub use request::Request; 13 | pub use response::Response; 14 | pub use scheme::{InvalidUri, Scheme}; 15 | 16 | pub mod body; 17 | 18 | mod client; 19 | pub mod error; 20 | mod fields; 21 | mod method; 22 | pub mod request; 23 | pub mod response; 24 | mod scheme; 25 | pub mod server; 26 | -------------------------------------------------------------------------------- /src/http/request.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | body::{BodyKind, IncomingBody}, 3 | error::WasiHttpErrorCode, 4 | fields::{header_map_from_wasi, header_map_to_wasi}, 5 | method::{from_wasi_method, to_wasi_method}, 6 | scheme::{from_wasi_scheme, to_wasi_scheme}, 7 | Authority, Error, HeaderMap, PathAndQuery, Uri, 8 | }; 9 | use crate::io::AsyncInputStream; 10 | use wasi::http::outgoing_handler::OutgoingRequest; 11 | use wasi::http::types::IncomingRequest; 12 | 13 | pub use http::request::{Builder, Request}; 14 | 15 | #[cfg(feature = "json")] 16 | use super::{ 17 | body::{BoundedBody, IntoBody}, 18 | error::ErrorVariant, 19 | }; 20 | #[cfg(feature = "json")] 21 | use http::header::{HeaderValue, CONTENT_TYPE}; 22 | #[cfg(feature = "json")] 23 | use serde::Serialize; 24 | #[cfg(feature = "json")] 25 | use serde_json; 26 | 27 | #[cfg(feature = "json")] 28 | pub trait JsonRequest { 29 | fn json(self, json: &T) -> Result>>, Error>; 30 | } 31 | 32 | #[cfg(feature = "json")] 33 | impl JsonRequest for Builder { 34 | /// Send a JSON body. Requires optional `json` feature. 35 | /// 36 | /// Serialization can fail if `T`'s implementation of `Serialize` decides to 37 | /// fail. 38 | #[cfg(feature = "json")] 39 | fn json(self, json: &T) -> Result>>, Error> { 40 | let encoded = serde_json::to_vec(json).map_err(|e| ErrorVariant::Other(e.to_string()))?; 41 | let builder = if !self 42 | .headers_ref() 43 | .is_some_and(|headers| headers.contains_key(CONTENT_TYPE)) 44 | { 45 | self.header( 46 | CONTENT_TYPE, 47 | HeaderValue::from_static("application/json; charset=utf-8"), 48 | ) 49 | } else { 50 | self 51 | }; 52 | builder 53 | .body(encoded.into_body()) 54 | .map_err(|e| ErrorVariant::Other(e.to_string()).into()) 55 | } 56 | } 57 | 58 | pub(crate) fn try_into_outgoing(request: Request) -> Result<(OutgoingRequest, T), Error> { 59 | let wasi_req = OutgoingRequest::new(header_map_to_wasi(request.headers())?); 60 | 61 | let (parts, body) = request.into_parts(); 62 | 63 | // Set the HTTP method 64 | let method = to_wasi_method(parts.method); 65 | wasi_req 66 | .set_method(&method) 67 | .map_err(|()| Error::other(format!("method rejected by wasi-http: {method:?}",)))?; 68 | 69 | // Set the url scheme 70 | let scheme = parts 71 | .uri 72 | .scheme() 73 | .map(to_wasi_scheme) 74 | .unwrap_or(wasi::http::types::Scheme::Https); 75 | wasi_req 76 | .set_scheme(Some(&scheme)) 77 | .map_err(|()| Error::other(format!("scheme rejected by wasi-http: {scheme:?}")))?; 78 | 79 | // Set authority 80 | let authority = parts.uri.authority().map(Authority::as_str); 81 | wasi_req 82 | .set_authority(authority) 83 | .map_err(|()| Error::other(format!("authority rejected by wasi-http {authority:?}")))?; 84 | 85 | // Set the url path + query string 86 | if let Some(p_and_q) = parts.uri.path_and_query() { 87 | wasi_req 88 | .set_path_with_query(Some(p_and_q.as_str())) 89 | .map_err(|()| { 90 | Error::other(format!("path and query rejected by wasi-http {p_and_q:?}")) 91 | })?; 92 | } 93 | 94 | // All done; request is ready for send-off 95 | Ok((wasi_req, body)) 96 | } 97 | 98 | /// This is used by the `http_server` macro. 99 | #[doc(hidden)] 100 | pub fn try_from_incoming( 101 | incoming: IncomingRequest, 102 | ) -> Result, WasiHttpErrorCode> { 103 | // TODO: What's the right error code to use for invalid headers? 104 | let headers: HeaderMap = header_map_from_wasi(incoming.headers()) 105 | .map_err(|e| WasiHttpErrorCode::InternalError(Some(e.to_string())))?; 106 | 107 | let method = from_wasi_method(incoming.method()) 108 | .map_err(|_| WasiHttpErrorCode::HttpRequestMethodInvalid)?; 109 | let scheme = incoming.scheme().map(|scheme| { 110 | from_wasi_scheme(scheme).expect("TODO: what shall we do with an invalid uri here?") 111 | }); 112 | let authority = incoming.authority().map(|authority| { 113 | Authority::from_maybe_shared(authority) 114 | .expect("TODO: what shall we do with an invalid uri authority here?") 115 | }); 116 | let path_and_query = incoming.path_with_query().map(|path_and_query| { 117 | PathAndQuery::from_maybe_shared(path_and_query) 118 | .expect("TODO: what shall we do with an invalid uri path-and-query here?") 119 | }); 120 | 121 | // TODO: What's the right error code to use for invalid headers? 122 | let kind = BodyKind::from_headers(&headers) 123 | .map_err(|e| WasiHttpErrorCode::InternalError(Some(e.to_string())))?; 124 | // `body_stream` is a child of `incoming_body` which means we cannot 125 | // drop the parent before we drop the child 126 | let incoming_body = incoming 127 | .consume() 128 | .expect("cannot call `consume` twice on incoming request"); 129 | let body_stream = incoming_body 130 | .stream() 131 | .expect("cannot call `stream` twice on an incoming body"); 132 | let body_stream = AsyncInputStream::new(body_stream); 133 | 134 | let body = IncomingBody::new(kind, body_stream, incoming_body); 135 | 136 | let mut uri = Uri::builder(); 137 | if let Some(scheme) = scheme { 138 | uri = uri.scheme(scheme); 139 | } 140 | if let Some(authority) = authority { 141 | uri = uri.authority(authority); 142 | } 143 | if let Some(path_and_query) = path_and_query { 144 | uri = uri.path_and_query(path_and_query); 145 | } 146 | // TODO: What's the right error code to use for an invalid uri? 147 | let uri = uri 148 | .build() 149 | .map_err(|e| WasiHttpErrorCode::InternalError(Some(e.to_string())))?; 150 | 151 | let mut request = Request::builder().method(method).uri(uri); 152 | if let Some(headers_mut) = request.headers_mut() { 153 | *headers_mut = headers; 154 | } 155 | // TODO: What's the right error code to use for an invalid request? 156 | request 157 | .body(body) 158 | .map_err(|e| WasiHttpErrorCode::InternalError(Some(e.to_string()))) 159 | } 160 | -------------------------------------------------------------------------------- /src/http/response.rs: -------------------------------------------------------------------------------- 1 | use wasi::http::types::IncomingResponse; 2 | 3 | use super::{ 4 | body::{BodyKind, IncomingBody}, 5 | fields::header_map_from_wasi, 6 | Error, HeaderMap, 7 | }; 8 | use crate::io::AsyncInputStream; 9 | use http::StatusCode; 10 | 11 | pub use http::response::{Builder, Response}; 12 | 13 | pub(crate) fn try_from_incoming( 14 | incoming: IncomingResponse, 15 | ) -> Result, Error> { 16 | let headers: HeaderMap = header_map_from_wasi(incoming.headers())?; 17 | // TODO: Does WASI guarantee that the incoming status is valid? 18 | let status = 19 | StatusCode::from_u16(incoming.status()).map_err(|err| Error::other(err.to_string()))?; 20 | 21 | let kind = BodyKind::from_headers(&headers)?; 22 | // `body_stream` is a child of `incoming_body` which means we cannot 23 | // drop the parent before we drop the child 24 | let incoming_body = incoming 25 | .consume() 26 | .expect("cannot call `consume` twice on incoming response"); 27 | let body_stream = incoming_body 28 | .stream() 29 | .expect("cannot call `stream` twice on an incoming body"); 30 | 31 | let body = IncomingBody::new(kind, AsyncInputStream::new(body_stream), incoming_body); 32 | 33 | let mut builder = Response::builder().status(status); 34 | 35 | if let Some(headers_mut) = builder.headers_mut() { 36 | *headers_mut = headers; 37 | } 38 | 39 | builder 40 | .body(body) 41 | .map_err(|err| Error::other(err.to_string())) 42 | } 43 | -------------------------------------------------------------------------------- /src/http/scheme.rs: -------------------------------------------------------------------------------- 1 | use wasi::http::types::Scheme as WasiScheme; 2 | 3 | pub use http::uri::{InvalidUri, Scheme}; 4 | use std::str::FromStr; 5 | 6 | pub(crate) fn to_wasi_scheme(value: &Scheme) -> WasiScheme { 7 | match value.as_str() { 8 | "http" => WasiScheme::Http, 9 | "https" => WasiScheme::Https, 10 | other => WasiScheme::Other(other.to_owned()), 11 | } 12 | } 13 | 14 | pub(crate) fn from_wasi_scheme(value: WasiScheme) -> Result { 15 | Ok(match value { 16 | WasiScheme::Http => Scheme::HTTP, 17 | WasiScheme::Https => Scheme::HTTPS, 18 | WasiScheme::Other(other) => Scheme::from_str(&other)?, 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/http/server.rs: -------------------------------------------------------------------------------- 1 | //! HTTP servers 2 | //! 3 | //! The WASI HTTP server API uses the [typed main] idiom, with a `main` function 4 | //! that takes a [`Request`] and a [`Responder`], and responds with a [`Response`], 5 | //! using the [`http_server`] macro: 6 | //! 7 | //! ```no_run 8 | //! #[wstd::http_server] 9 | //! async fn main(request: Request, responder: Responder) -> Finished { 10 | //! responder 11 | //! .respond(Response::new("Hello!\n".into_body())) 12 | //! .await 13 | //! } 14 | //! ``` 15 | //! 16 | //! [typed main]: https://sunfishcode.github.io/typed-main-wasi-presentation/chapter_1.html 17 | //! [`Request`]: crate::http::Request 18 | //! [`Responder`]: crate::http::server::Responder 19 | //! [`Response`]: crate::http::Response 20 | //! [`http_server`]: crate::http_server 21 | 22 | use super::{ 23 | body::{BodyForthcoming, OutgoingBody}, 24 | error::WasiHttpErrorCode, 25 | fields::header_map_to_wasi, 26 | Body, HeaderMap, Response, 27 | }; 28 | use crate::io::{copy, AsyncOutputStream}; 29 | use http::header::CONTENT_LENGTH; 30 | use wasi::exports::http::incoming_handler::ResponseOutparam; 31 | use wasi::http::types::OutgoingResponse; 32 | 33 | /// This is passed into the [`http_server`] `main` function and holds the state 34 | /// needed for a handler to produce a response, or fail. There are two ways to 35 | /// respond, with [`Responder::start_response`] to stream the body in, or 36 | /// [`Responder::respond`] to give the body as a string, byte array, or input 37 | /// stream. See those functions for examples. 38 | /// 39 | /// [`http_server`]: crate::http_server 40 | #[must_use] 41 | pub struct Responder { 42 | outparam: ResponseOutparam, 43 | } 44 | 45 | impl Responder { 46 | /// Start responding with the given `Response` and return an `OutgoingBody` 47 | /// stream to write the body to. 48 | /// 49 | /// # Example 50 | /// 51 | /// ``` 52 | /// # use wstd::http::{body::IncomingBody, BodyForthcoming, Response, Request}; 53 | /// # use wstd::http::server::{Finished, Responder}; 54 | /// # use crate::wstd::io::AsyncWrite; 55 | /// # async fn example(responder: Responder) -> Finished { 56 | /// let mut body = responder.start_response(Response::new(BodyForthcoming)); 57 | /// let result = body 58 | /// .write_all("Hello!\n".as_bytes()) 59 | /// .await; 60 | /// Finished::finish(body, result, None) 61 | /// # } 62 | /// ``` 63 | pub fn start_response(self, response: Response) -> OutgoingBody { 64 | let wasi_headers = header_map_to_wasi(response.headers()).expect("header error"); 65 | let wasi_response = OutgoingResponse::new(wasi_headers); 66 | let wasi_status = response.status().as_u16(); 67 | 68 | // Unwrap because `StatusCode` has already validated the status. 69 | wasi_response.set_status_code(wasi_status).unwrap(); 70 | 71 | // Unwrap because we can be sure we only call these once. 72 | let wasi_body = wasi_response.body().unwrap(); 73 | let wasi_stream = wasi_body.write().unwrap(); 74 | 75 | // Tell WASI to start the show. 76 | ResponseOutparam::set(self.outparam, Ok(wasi_response)); 77 | 78 | OutgoingBody::new(AsyncOutputStream::new(wasi_stream), wasi_body) 79 | } 80 | 81 | /// Respond with the given `Response` which contains the body. 82 | /// 83 | /// If the body has a known length, a Content-Length header is automatically added. 84 | /// 85 | /// To respond with trailers, use [`Responder::start_response`] instead. 86 | /// 87 | /// # Example 88 | /// 89 | /// ``` 90 | /// # use wstd::http::{body::IncomingBody, BodyForthcoming, IntoBody, Response, Request}; 91 | /// # use wstd::http::server::{Finished, Responder}; 92 | /// # 93 | /// # async fn example(responder: Responder) -> Finished { 94 | /// responder 95 | /// .respond(Response::new("Hello!\n".into_body())) 96 | /// .await 97 | /// # } 98 | /// ``` 99 | pub async fn respond(self, response: Response) -> Finished { 100 | let headers = response.headers(); 101 | let status = response.status().as_u16(); 102 | 103 | let wasi_headers = header_map_to_wasi(headers).expect("header error"); 104 | 105 | // Consume the `response` and prepare to write the body. 106 | let mut body = response.into_body(); 107 | 108 | // Automatically add a Content-Length header. 109 | if let Some(len) = body.len() { 110 | let mut buffer = itoa::Buffer::new(); 111 | wasi_headers 112 | .append(CONTENT_LENGTH.as_str(), buffer.format(len).as_bytes()) 113 | .unwrap(); 114 | } 115 | 116 | let wasi_response = OutgoingResponse::new(wasi_headers); 117 | 118 | // Unwrap because `StatusCode` has already validated the status. 119 | wasi_response.set_status_code(status).unwrap(); 120 | 121 | // Unwrap because we can be sure we only call these once. 122 | let wasi_body = wasi_response.body().unwrap(); 123 | let wasi_stream = wasi_body.write().unwrap(); 124 | 125 | // Tell WASI to start the show. 126 | ResponseOutparam::set(self.outparam, Ok(wasi_response)); 127 | 128 | let mut outgoing_body = OutgoingBody::new(AsyncOutputStream::new(wasi_stream), wasi_body); 129 | 130 | let result = copy(&mut body, &mut outgoing_body).await; 131 | let trailers = None; 132 | Finished::finish(outgoing_body, result, trailers) 133 | } 134 | 135 | /// This is used by the `http_server` macro. 136 | #[doc(hidden)] 137 | pub fn new(outparam: ResponseOutparam) -> Self { 138 | Self { outparam } 139 | } 140 | 141 | /// This is used by the `http_server` macro. 142 | #[doc(hidden)] 143 | pub fn fail(self, err: WasiHttpErrorCode) -> Finished { 144 | ResponseOutparam::set(self.outparam, Err(err)); 145 | Finished(()) 146 | } 147 | } 148 | 149 | /// An opaque value returned from a handler indicating that the body is 150 | /// finished, either by [`Finished::finish`] or [`Finished::fail`]. 151 | pub struct Finished(pub(crate) ()); 152 | 153 | impl Finished { 154 | /// Finish the body, optionally with trailers, and return a `Finished` 155 | /// token to be returned from the [`http_server`] `main` function to indicate 156 | /// that the response is finished. 157 | /// 158 | /// `result` is a `std::io::Result` for reporting any I/O errors that 159 | /// occur while writing to the body stream. 160 | /// 161 | /// [`http_server`]: crate::http_server 162 | pub fn finish( 163 | body: OutgoingBody, 164 | result: std::io::Result<()>, 165 | trailers: Option, 166 | ) -> Self { 167 | let (stream, body) = body.consume(); 168 | 169 | // The stream is a child resource of the `OutgoingBody`, so ensure that 170 | // it's dropped first. 171 | drop(stream); 172 | 173 | // If there was an I/O error, panic and don't call `OutgoingBody::finish`. 174 | result.expect("I/O error while writing the body"); 175 | 176 | let wasi_trailers = 177 | trailers.map(|trailers| header_map_to_wasi(&trailers).expect("header error")); 178 | 179 | wasi::http::types::OutgoingBody::finish(body, wasi_trailers) 180 | .expect("body length did not match Content-Length header value"); 181 | 182 | Self(()) 183 | } 184 | 185 | /// Return a `Finished` token that can be returned from a handler to 186 | /// indicate that the body is not finished and should be considered 187 | /// corrupted. 188 | pub fn fail(body: OutgoingBody) -> Self { 189 | let (stream, _body) = body.consume(); 190 | 191 | // The stream is a child resource of the `OutgoingBody`, so ensure that 192 | // it's dropped first. 193 | drop(stream); 194 | 195 | // No need to do anything else; omitting the call to `finish` achieves 196 | // the desired effect. 197 | Self(()) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/io/copy.rs: -------------------------------------------------------------------------------- 1 | use crate::io::{AsyncRead, AsyncWrite, Error}; 2 | use wasi::io::streams::StreamError; 3 | 4 | /// Copy bytes from a reader to a writer. 5 | pub async fn copy(mut reader: R, mut writer: W) -> crate::io::Result<()> 6 | where 7 | R: AsyncRead, 8 | W: AsyncWrite, 9 | { 10 | // Optimized path when we have an `AsyncInputStream` and an 11 | // `AsyncOutputStream`. 12 | if let Some(reader) = reader.as_async_input_stream() { 13 | if let Some(writer) = writer.as_async_output_stream() { 14 | loop { 15 | match super::splice(reader, writer, u64::MAX).await { 16 | Ok(_n) => (), 17 | Err(StreamError::Closed) => return Ok(()), 18 | Err(StreamError::LastOperationFailed(err)) => { 19 | return Err(Error::other(err.to_debug_string())); 20 | } 21 | } 22 | } 23 | } 24 | } 25 | 26 | // Unoptimized case: read the input and then write it. 27 | let mut buf = [0; 1024]; 28 | 'read: loop { 29 | let bytes_read = reader.read(&mut buf).await?; 30 | if bytes_read == 0 { 31 | break 'read Ok(()); 32 | } 33 | writer.write_all(&buf[0..bytes_read]).await?; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/io/cursor.rs: -------------------------------------------------------------------------------- 1 | use crate::io::{self, AsyncRead, AsyncSeek, AsyncWrite}; 2 | 3 | use super::SeekFrom; 4 | 5 | /// A `Cursor` wraps an in-memory buffer and provides it with a 6 | /// [`AsyncSeek`] implementation. 7 | #[derive(Clone, Debug, Default)] 8 | pub struct Cursor { 9 | inner: std::io::Cursor, 10 | } 11 | 12 | impl Cursor { 13 | /// Creates a new cursor wrapping the provided underlying in-memory buffer. 14 | pub fn new(inner: T) -> Cursor { 15 | Cursor { 16 | inner: std::io::Cursor::new(inner), 17 | } 18 | } 19 | 20 | /// Consumes this cursor, returning the underlying value. 21 | pub fn into_inner(self) -> T { 22 | self.inner.into_inner() 23 | } 24 | 25 | /// Gets a reference to the underlying value in this cursor. 26 | pub fn get_ref(&self) -> &T { 27 | self.inner.get_ref() 28 | } 29 | 30 | /// Gets a mutable reference to the underlying value in this cursor. 31 | pub fn get_mut(&mut self) -> &mut T { 32 | self.inner.get_mut() 33 | } 34 | 35 | /// Returns the current position of this cursor. 36 | pub fn position(&self) -> u64 { 37 | self.inner.position() 38 | } 39 | 40 | /// Sets the position of this cursor. 41 | pub fn set_position(&mut self, pos: u64) { 42 | self.inner.set_position(pos) 43 | } 44 | } 45 | 46 | impl AsyncSeek for Cursor 47 | where 48 | T: AsRef<[u8]>, 49 | { 50 | async fn seek(&mut self, pos: SeekFrom) -> io::Result { 51 | let pos = match pos { 52 | SeekFrom::Start(pos) => std::io::SeekFrom::Start(pos), 53 | SeekFrom::End(pos) => std::io::SeekFrom::End(pos), 54 | SeekFrom::Current(pos) => std::io::SeekFrom::Current(pos), 55 | }; 56 | std::io::Seek::seek(&mut self.inner, pos) 57 | } 58 | } 59 | 60 | impl AsyncRead for Cursor 61 | where 62 | T: AsRef<[u8]>, 63 | { 64 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 65 | std::io::Read::read(&mut self.inner, buf) 66 | } 67 | } 68 | 69 | impl AsyncWrite for Cursor<&mut [u8]> { 70 | async fn write(&mut self, buf: &[u8]) -> io::Result { 71 | std::io::Write::write(&mut self.inner, buf) 72 | } 73 | async fn flush(&mut self) -> io::Result<()> { 74 | std::io::Write::flush(&mut self.inner) 75 | } 76 | } 77 | 78 | impl AsyncWrite for Cursor<&mut Vec> { 79 | async fn write(&mut self, buf: &[u8]) -> io::Result { 80 | std::io::Write::write(&mut self.inner, buf) 81 | } 82 | async fn flush(&mut self) -> io::Result<()> { 83 | std::io::Write::flush(&mut self.inner) 84 | } 85 | } 86 | 87 | impl AsyncWrite for Cursor> { 88 | async fn write(&mut self, buf: &[u8]) -> io::Result { 89 | std::io::Write::write(&mut self.inner, buf) 90 | } 91 | async fn flush(&mut self) -> io::Result<()> { 92 | std::io::Write::flush(&mut self.inner) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/io/empty.rs: -------------------------------------------------------------------------------- 1 | use super::{AsyncRead, AsyncWrite}; 2 | 3 | #[non_exhaustive] 4 | pub struct Empty; 5 | impl AsyncRead for Empty { 6 | async fn read(&mut self, _buf: &mut [u8]) -> super::Result { 7 | Ok(0) 8 | } 9 | } 10 | 11 | impl AsyncWrite for Empty { 12 | async fn write(&mut self, buf: &[u8]) -> super::Result { 13 | Ok(buf.len()) 14 | } 15 | 16 | async fn flush(&mut self) -> super::Result<()> { 17 | Ok(()) 18 | } 19 | } 20 | 21 | /// Creates a value that is always at EOF for reads, and ignores all data written. 22 | pub fn empty() -> Empty { 23 | Empty {} 24 | } 25 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | //! Async IO abstractions. 2 | 3 | mod copy; 4 | mod cursor; 5 | mod empty; 6 | mod read; 7 | mod seek; 8 | mod stdio; 9 | mod streams; 10 | mod write; 11 | 12 | pub use crate::runtime::AsyncPollable; 13 | pub use copy::*; 14 | pub use cursor::*; 15 | pub use empty::*; 16 | pub use read::*; 17 | pub use seek::*; 18 | pub use stdio::*; 19 | pub use streams::*; 20 | pub use write::*; 21 | 22 | /// The error type for I/O operations. 23 | /// 24 | pub use std::io::Error; 25 | 26 | /// A specialized Result type for I/O operations. 27 | /// 28 | pub use std::io::Result; 29 | -------------------------------------------------------------------------------- /src/io/read.rs: -------------------------------------------------------------------------------- 1 | use crate::io; 2 | 3 | const CHUNK_SIZE: usize = 2048; 4 | 5 | /// Read bytes from a source. 6 | pub trait AsyncRead { 7 | async fn read(&mut self, buf: &mut [u8]) -> io::Result; 8 | async fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { 9 | // total bytes written to buf 10 | let mut n = 0; 11 | 12 | loop { 13 | // grow buf if empty 14 | if buf.len() == n { 15 | buf.resize(n + CHUNK_SIZE, 0u8); 16 | } 17 | 18 | let len = self.read(&mut buf[n..]).await?; 19 | if len == 0 { 20 | buf.truncate(n); 21 | return Ok(n); 22 | } 23 | 24 | n += len; 25 | } 26 | } 27 | 28 | // If the `AsyncRead` implementation is an unbuffered wrapper around an 29 | // `AsyncInputStream`, some I/O operations can be more efficient. 30 | #[inline] 31 | fn as_async_input_stream(&self) -> Option<&io::AsyncInputStream> { 32 | None 33 | } 34 | } 35 | 36 | impl AsyncRead for &mut R { 37 | #[inline] 38 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 39 | (**self).read(buf).await 40 | } 41 | 42 | #[inline] 43 | async fn read_to_end(&mut self, buf: &mut Vec) -> io::Result { 44 | (**self).read_to_end(buf).await 45 | } 46 | 47 | #[inline] 48 | fn as_async_input_stream(&self) -> Option<&io::AsyncInputStream> { 49 | (**self).as_async_input_stream() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/io/seek.rs: -------------------------------------------------------------------------------- 1 | /// The `Seek` trait provides a cursor which can be moved within a stream of 2 | /// bytes. 3 | pub trait AsyncSeek { 4 | /// Seek to an offset, in bytes, in a stream. 5 | async fn seek(&mut self, pos: SeekFrom) -> super::Result; 6 | 7 | /// Rewind to the beginning of a stream. 8 | async fn rewind(&mut self) -> super::Result<()> { 9 | self.seek(SeekFrom::Start(0)).await?; 10 | Ok(()) 11 | } 12 | 13 | /// Returns the length of this stream (in bytes). 14 | async fn stream_len(&mut self) -> super::Result { 15 | let old_pos = self.stream_position().await?; 16 | let len = self.seek(SeekFrom::End(0)).await?; 17 | 18 | // Avoid seeking a third time when we were already at the end of the 19 | // stream. The branch is usually way cheaper than a seek operation. 20 | if old_pos != len { 21 | self.seek(SeekFrom::Start(old_pos)).await?; 22 | } 23 | 24 | Ok(len) 25 | } 26 | 27 | /// Returns the current seek position from the start of the stream. 28 | async fn stream_position(&mut self) -> super::Result { 29 | self.seek(SeekFrom::Current(0)).await 30 | } 31 | 32 | /// Seeks relative to the current position. 33 | async fn seek_relative(&mut self, offset: i64) -> super::Result<()> { 34 | self.seek(SeekFrom::Current(offset)).await?; 35 | Ok(()) 36 | } 37 | } 38 | 39 | /// Enumeration of possible methods to seek within an I/O object. 40 | /// 41 | /// It is used by the [`AsyncSeek`] trait. 42 | #[derive(Copy, PartialEq, Eq, Clone, Debug)] 43 | pub enum SeekFrom { 44 | /// Sets the offset to the provided number of bytes. 45 | Start(u64), 46 | 47 | /// Sets the offset to the size of this object plus the specified number of 48 | /// bytes. 49 | End(i64), 50 | 51 | /// Sets the offset to the current position plus the specified number of 52 | /// bytes. 53 | Current(i64), 54 | } 55 | -------------------------------------------------------------------------------- /src/io/stdio.rs: -------------------------------------------------------------------------------- 1 | use super::{AsyncInputStream, AsyncOutputStream, AsyncRead, AsyncWrite, Result}; 2 | use std::cell::LazyCell; 3 | use wasi::cli::terminal_input::TerminalInput; 4 | use wasi::cli::terminal_output::TerminalOutput; 5 | 6 | /// Use the program's stdin as an `AsyncInputStream`. 7 | #[derive(Debug)] 8 | pub struct Stdin { 9 | stream: AsyncInputStream, 10 | terminput: LazyCell>, 11 | } 12 | 13 | /// Get the program's stdin for use as an `AsyncInputStream`. 14 | pub fn stdin() -> Stdin { 15 | let stream = AsyncInputStream::new(wasi::cli::stdin::get_stdin()); 16 | Stdin { 17 | stream, 18 | terminput: LazyCell::new(wasi::cli::terminal_stdin::get_terminal_stdin), 19 | } 20 | } 21 | 22 | impl Stdin { 23 | /// Check if stdin is a terminal. 24 | pub fn is_terminal(&self) -> bool { 25 | LazyCell::force(&self.terminput).is_some() 26 | } 27 | } 28 | 29 | impl AsyncRead for Stdin { 30 | #[inline] 31 | async fn read(&mut self, buf: &mut [u8]) -> Result { 32 | self.stream.read(buf).await 33 | } 34 | 35 | #[inline] 36 | async fn read_to_end(&mut self, buf: &mut Vec) -> Result { 37 | self.stream.read_to_end(buf).await 38 | } 39 | 40 | #[inline] 41 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 42 | Some(&self.stream) 43 | } 44 | } 45 | 46 | /// Use the program's stdout as an `AsyncOutputStream`. 47 | #[derive(Debug)] 48 | pub struct Stdout { 49 | stream: AsyncOutputStream, 50 | termoutput: LazyCell>, 51 | } 52 | 53 | /// Get the program's stdout for use as an `AsyncOutputStream`. 54 | pub fn stdout() -> Stdout { 55 | let stream = AsyncOutputStream::new(wasi::cli::stdout::get_stdout()); 56 | Stdout { 57 | stream, 58 | termoutput: LazyCell::new(wasi::cli::terminal_stdout::get_terminal_stdout), 59 | } 60 | } 61 | 62 | impl Stdout { 63 | /// Check if stdout is a terminal. 64 | pub fn is_terminal(&self) -> bool { 65 | LazyCell::force(&self.termoutput).is_some() 66 | } 67 | } 68 | 69 | impl AsyncWrite for Stdout { 70 | #[inline] 71 | async fn write(&mut self, buf: &[u8]) -> Result { 72 | self.stream.write(buf).await 73 | } 74 | 75 | #[inline] 76 | async fn flush(&mut self) -> Result<()> { 77 | self.stream.flush().await 78 | } 79 | 80 | #[inline] 81 | async fn write_all(&mut self, buf: &[u8]) -> Result<()> { 82 | self.stream.write_all(buf).await 83 | } 84 | 85 | #[inline] 86 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 87 | self.stream.as_async_output_stream() 88 | } 89 | } 90 | 91 | /// Use the program's stdout as an `AsyncOutputStream`. 92 | #[derive(Debug)] 93 | pub struct Stderr { 94 | stream: AsyncOutputStream, 95 | termoutput: LazyCell>, 96 | } 97 | 98 | /// Get the program's stdout for use as an `AsyncOutputStream`. 99 | pub fn stderr() -> Stderr { 100 | let stream = AsyncOutputStream::new(wasi::cli::stderr::get_stderr()); 101 | Stderr { 102 | stream, 103 | termoutput: LazyCell::new(wasi::cli::terminal_stderr::get_terminal_stderr), 104 | } 105 | } 106 | 107 | impl Stderr { 108 | /// Check if stderr is a terminal. 109 | pub fn is_terminal(&self) -> bool { 110 | LazyCell::force(&self.termoutput).is_some() 111 | } 112 | } 113 | 114 | impl AsyncWrite for Stderr { 115 | #[inline] 116 | async fn write(&mut self, buf: &[u8]) -> Result { 117 | self.stream.write(buf).await 118 | } 119 | 120 | #[inline] 121 | async fn flush(&mut self) -> Result<()> { 122 | self.stream.flush().await 123 | } 124 | 125 | #[inline] 126 | async fn write_all(&mut self, buf: &[u8]) -> Result<()> { 127 | self.stream.write_all(buf).await 128 | } 129 | 130 | #[inline] 131 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 132 | self.stream.as_async_output_stream() 133 | } 134 | } 135 | 136 | #[cfg(test)] 137 | mod test { 138 | use crate::io::AsyncWrite; 139 | use crate::runtime::block_on; 140 | #[test] 141 | // No internal predicate. Run test with --nocapture and inspect output manually. 142 | fn stdout_println_hello_world() { 143 | block_on(async { 144 | let mut stdout = super::stdout(); 145 | let term = if stdout.is_terminal() { "is" } else { "is not" }; 146 | stdout 147 | .write_all(format!("hello, world! stdout {term} a terminal\n",).as_bytes()) 148 | .await 149 | .unwrap(); 150 | }) 151 | } 152 | #[test] 153 | // No internal predicate. Run test with --nocapture and inspect output manually. 154 | fn stderr_println_hello_world() { 155 | block_on(async { 156 | let mut stdout = super::stdout(); 157 | let term = if stdout.is_terminal() { "is" } else { "is not" }; 158 | stdout 159 | .write_all(format!("hello, world! stderr {term} a terminal\n",).as_bytes()) 160 | .await 161 | .unwrap(); 162 | }) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/io/streams.rs: -------------------------------------------------------------------------------- 1 | use super::{AsyncPollable, AsyncRead, AsyncWrite}; 2 | use std::cell::OnceCell; 3 | use std::io::Result; 4 | use wasi::io::streams::{InputStream, OutputStream, StreamError}; 5 | 6 | /// A wrapper for WASI's `InputStream` resource that provides implementations of `AsyncRead` and 7 | /// `AsyncPollable`. 8 | #[derive(Debug)] 9 | pub struct AsyncInputStream { 10 | // Lazily initialized pollable, used for lifetime of stream to check readiness. 11 | // Field ordering matters: this child must be dropped before stream 12 | subscription: OnceCell, 13 | stream: InputStream, 14 | } 15 | 16 | impl AsyncInputStream { 17 | /// Construct an `AsyncInputStream` from a WASI `InputStream` resource. 18 | pub fn new(stream: InputStream) -> Self { 19 | Self { 20 | subscription: OnceCell::new(), 21 | stream, 22 | } 23 | } 24 | /// Await for read readiness. 25 | async fn ready(&self) { 26 | // Lazily initialize the AsyncPollable 27 | let subscription = self 28 | .subscription 29 | .get_or_init(|| AsyncPollable::new(self.stream.subscribe())); 30 | // Wait on readiness 31 | subscription.wait_for().await; 32 | } 33 | /// Asynchronously read from the input stream. 34 | /// This method is the same as [`AsyncRead::read`], but doesn't require a `&mut self`. 35 | pub async fn read(&self, buf: &mut [u8]) -> Result { 36 | let read = loop { 37 | self.ready().await; 38 | // Ideally, the ABI would be able to read directly into buf. 39 | // However, with the default generated bindings, it returns a 40 | // newly allocated vec, which we need to copy into buf. 41 | match self.stream.read(buf.len() as u64) { 42 | // A read of 0 bytes from WASI's `read` doesn't mean 43 | // end-of-stream as it does in Rust. However, `self.ready()` 44 | // cannot guarantee that at least one byte is ready for 45 | // reading, so in this case we try again. 46 | Ok(r) if r.is_empty() => continue, 47 | Ok(r) => break r, 48 | // 0 bytes from Rust's `read` means end-of-stream. 49 | Err(StreamError::Closed) => return Ok(0), 50 | Err(StreamError::LastOperationFailed(err)) => { 51 | return Err(std::io::Error::other(err.to_debug_string())) 52 | } 53 | } 54 | }; 55 | let len = read.len(); 56 | buf[0..len].copy_from_slice(&read); 57 | Ok(len) 58 | } 59 | } 60 | 61 | impl AsyncRead for AsyncInputStream { 62 | async fn read(&mut self, buf: &mut [u8]) -> Result { 63 | Self::read(self, buf).await 64 | } 65 | 66 | #[inline] 67 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 68 | Some(self) 69 | } 70 | } 71 | 72 | /// A wrapper for WASI's `output-stream` resource that provides implementations of `AsyncWrite` and 73 | /// `AsyncPollable`. 74 | #[derive(Debug)] 75 | pub struct AsyncOutputStream { 76 | // Lazily initialized pollable, used for lifetime of stream to check readiness. 77 | // Field ordering matters: this child must be dropped before stream 78 | subscription: OnceCell, 79 | stream: OutputStream, 80 | } 81 | 82 | impl AsyncOutputStream { 83 | /// Construct an `AsyncOutputStream` from a WASI `OutputStream` resource. 84 | pub fn new(stream: OutputStream) -> Self { 85 | Self { 86 | subscription: OnceCell::new(), 87 | stream, 88 | } 89 | } 90 | /// Await write readiness. 91 | async fn ready(&self) { 92 | // Lazily initialize the AsyncPollable 93 | let subscription = self 94 | .subscription 95 | .get_or_init(|| AsyncPollable::new(self.stream.subscribe())); 96 | // Wait on readiness 97 | subscription.wait_for().await; 98 | } 99 | /// Asynchronously write to the output stream. This method is the same as 100 | /// [`AsyncWrite::write`], but doesn't require a `&mut self`. 101 | /// 102 | /// Awaits for write readiness, and then performs at most one write to the 103 | /// output stream. Returns how much of the argument `buf` was written, or 104 | /// a `std::io::Error` indicating either an error returned by the stream write 105 | /// using the debug string provided by the WASI error, or else that the, 106 | /// indicated by `std::io::ErrorKind::ConnectionReset`. 107 | pub async fn write(&self, buf: &[u8]) -> Result { 108 | // Loops at most twice. 109 | loop { 110 | match self.stream.check_write() { 111 | Ok(0) => { 112 | self.ready().await; 113 | // Next loop guaranteed to have nonzero check_write, or error. 114 | continue; 115 | } 116 | Ok(some) => { 117 | let writable = some.try_into().unwrap_or(usize::MAX).min(buf.len()); 118 | match self.stream.write(&buf[0..writable]) { 119 | Ok(()) => return Ok(writable), 120 | Err(StreamError::Closed) => { 121 | return Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset)) 122 | } 123 | Err(StreamError::LastOperationFailed(err)) => { 124 | return Err(std::io::Error::other(err.to_debug_string())) 125 | } 126 | } 127 | } 128 | Err(StreamError::Closed) => { 129 | return Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset)) 130 | } 131 | Err(StreamError::LastOperationFailed(err)) => { 132 | return Err(std::io::Error::other(err.to_debug_string())) 133 | } 134 | } 135 | } 136 | } 137 | /// Asyncronously flush the output stream. Initiates a flush, and then 138 | /// awaits until the flush is complete and the output stream is ready for 139 | /// writing again. 140 | /// 141 | /// This method is the same as [`AsyncWrite::flush`], but doesn't require 142 | /// a `&mut self`. 143 | /// 144 | /// Fails with a `std::io::Error` indicating either an error returned by 145 | /// the stream flush, using the debug string provided by the WASI error, 146 | /// or else that the stream is closed, indicated by 147 | /// `std::io::ErrorKind::ConnectionReset`. 148 | pub async fn flush(&self) -> Result<()> { 149 | match self.stream.flush() { 150 | Ok(()) => { 151 | self.ready().await; 152 | Ok(()) 153 | } 154 | Err(StreamError::Closed) => { 155 | Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset)) 156 | } 157 | Err(StreamError::LastOperationFailed(err)) => { 158 | Err(std::io::Error::other(err.to_debug_string())) 159 | } 160 | } 161 | } 162 | } 163 | impl AsyncWrite for AsyncOutputStream { 164 | // Required methods 165 | async fn write(&mut self, buf: &[u8]) -> Result { 166 | Self::write(self, buf).await 167 | } 168 | async fn flush(&mut self) -> Result<()> { 169 | Self::flush(self).await 170 | } 171 | 172 | #[inline] 173 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 174 | Some(self) 175 | } 176 | } 177 | 178 | /// Wait for both streams to be ready and then do a WASI splice. 179 | pub(crate) async fn splice( 180 | reader: &AsyncInputStream, 181 | writer: &AsyncOutputStream, 182 | len: u64, 183 | ) -> core::result::Result { 184 | // Wait for both streams to be ready. 185 | let r = reader.ready(); 186 | writer.ready().await; 187 | r.await; 188 | 189 | writer.stream.splice(&reader.stream, len) 190 | } 191 | -------------------------------------------------------------------------------- /src/io/write.rs: -------------------------------------------------------------------------------- 1 | use crate::io; 2 | 3 | /// Write bytes to a sink. 4 | pub trait AsyncWrite { 5 | // Required methods 6 | async fn write(&mut self, buf: &[u8]) -> io::Result; 7 | async fn flush(&mut self) -> io::Result<()>; 8 | 9 | async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { 10 | let mut to_write = &buf[0..]; 11 | loop { 12 | let bytes_written = self.write(to_write).await?; 13 | to_write = &to_write[bytes_written..]; 14 | if to_write.is_empty() { 15 | return Ok(()); 16 | } 17 | } 18 | } 19 | 20 | // If the `AsyncWrite` implementation is an unbuffered wrapper around an 21 | // `AsyncOutputStream`, some I/O operations can be more efficient. 22 | #[inline] 23 | fn as_async_output_stream(&self) -> Option<&io::AsyncOutputStream> { 24 | None 25 | } 26 | } 27 | 28 | impl AsyncWrite for &mut W { 29 | #[inline] 30 | async fn write(&mut self, buf: &[u8]) -> io::Result { 31 | (**self).write(buf).await 32 | } 33 | 34 | #[inline] 35 | async fn flush(&mut self) -> io::Result<()> { 36 | (**self).flush().await 37 | } 38 | 39 | #[inline] 40 | async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { 41 | (**self).write_all(buf).await 42 | } 43 | 44 | #[inline] 45 | fn as_async_output_stream(&self) -> Option<&io::AsyncOutputStream> { 46 | (**self).as_async_output_stream() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/iter/mod.rs: -------------------------------------------------------------------------------- 1 | //! Composable async iteration. 2 | 3 | /// A trait for dealing with async iterators. 4 | pub trait AsyncIterator { 5 | /// The type of the elements being iterated over. 6 | type Item; 7 | 8 | /// Advances the iterator and returns the next value. 9 | async fn next(&mut self) -> Option; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(async_fn_in_trait)] 2 | #![warn(future_incompatible, unreachable_pub)] 3 | #![forbid(unsafe_code)] 4 | //#![deny(missing_debug_implementations)] 5 | //#![warn(missing_docs)] 6 | //#![forbid(rustdoc::missing_doc_code_examples)] 7 | 8 | //! An async standard library for Wasm Components and WASI 0.2 9 | //! 10 | //! This is a minimal async standard library written exclusively to support Wasm 11 | //! Components. It exists primarily to enable people to write async-based 12 | //! applications in Rust before async-std, smol, or tokio land support for Wasm 13 | //! Components and WASI 0.2. Once those runtimes land support, it is recommended 14 | //! users switch to use those instead. 15 | //! 16 | //! # Examples 17 | //! 18 | //! **TCP echo server** 19 | //! 20 | //! ```rust,no_run 21 | #![doc = include_str!("../examples/tcp_echo_server.rs")] 22 | //! ``` 23 | //! 24 | //! **HTTP Client** 25 | //! 26 | //! ```rust,no_run 27 | #![doc = include_str!("../tests/http_get.rs")] 28 | //! ``` 29 | //! 30 | //! **HTTP Server** 31 | //! 32 | //! ```rust,no_run 33 | #![doc = include_str!("../examples/http_server.rs")] 34 | //! ``` 35 | //! 36 | //! # Design Decisions 37 | //! 38 | //! This library is entirely self-contained. This means that it does not share 39 | //! any traits or types with any other async runtimes. This means we're trading 40 | //! in some compatibility for ease of maintenance. Because this library is not 41 | //! intended to be maintained in the long term, this seems like the right 42 | //! tradeoff to make. 43 | //! 44 | //! WASI 0.2 does not yet support multi-threading. For that reason this library 45 | //! does not provide any multi-threaded primitives, and is free to make liberal 46 | //! use of Async Functions in Traits since no `Send` bounds are required. This 47 | //! makes for a simpler end-user experience, again at the cost of some 48 | //! compatibility. Though ultimately we do believe that using Async Functions is 49 | //! the right foundation for the standard library abstractions - meaning we may 50 | //! be trading in backward-compatibility for forward-compatibility. 51 | //! 52 | //! This library also supports slightly more interfaces than the stdlib does. 53 | //! For example `wstd::rand` is a new module that provides access to random 54 | //! bytes. And `wstd::runtime` provides access to async runtime primitives. 55 | //! These are unique capabilities provided by WASI 0.2, and because this library 56 | //! is specific to that are exposed from here. 57 | 58 | pub mod future; 59 | #[macro_use] 60 | pub mod http; 61 | pub mod io; 62 | pub mod iter; 63 | pub mod net; 64 | pub mod rand; 65 | pub mod runtime; 66 | pub mod task; 67 | pub mod time; 68 | 69 | pub use wstd_macro::attr_macro_http_server as http_server; 70 | pub use wstd_macro::attr_macro_main as main; 71 | pub use wstd_macro::attr_macro_test as test; 72 | 73 | // Re-export the wasi crate for use by the `http_server` macro. 74 | #[doc(hidden)] 75 | pub use wasi; 76 | 77 | pub mod prelude { 78 | pub use crate::future::FutureExt as _; 79 | pub use crate::http::Body as _; 80 | pub use crate::io::AsyncRead as _; 81 | pub use crate::io::AsyncWrite as _; 82 | } 83 | -------------------------------------------------------------------------------- /src/net/mod.rs: -------------------------------------------------------------------------------- 1 | //! Async network abstractions. 2 | 3 | mod tcp_listener; 4 | mod tcp_stream; 5 | 6 | pub use tcp_listener::*; 7 | pub use tcp_stream::*; 8 | -------------------------------------------------------------------------------- /src/net/tcp_listener.rs: -------------------------------------------------------------------------------- 1 | use wasi::sockets::network::Ipv4SocketAddress; 2 | use wasi::sockets::tcp::{ErrorCode, IpAddressFamily, IpSocketAddress, TcpSocket}; 3 | 4 | use crate::io; 5 | use crate::iter::AsyncIterator; 6 | use std::io::ErrorKind; 7 | use std::net::SocketAddr; 8 | 9 | use super::TcpStream; 10 | use crate::runtime::AsyncPollable; 11 | 12 | /// A TCP socket server, listening for connections. 13 | #[derive(Debug)] 14 | pub struct TcpListener { 15 | // Field order matters: must drop this child before parent below 16 | pollable: AsyncPollable, 17 | socket: TcpSocket, 18 | } 19 | 20 | impl TcpListener { 21 | /// Creates a new TcpListener which will be bound to the specified address. 22 | /// 23 | /// The returned listener is ready for accepting connections. 24 | pub async fn bind(addr: &str) -> io::Result { 25 | let addr: SocketAddr = addr 26 | .parse() 27 | .map_err(|_| io::Error::other("failed to parse string to socket addr"))?; 28 | let family = match addr { 29 | SocketAddr::V4(_) => IpAddressFamily::Ipv4, 30 | SocketAddr::V6(_) => IpAddressFamily::Ipv6, 31 | }; 32 | let socket = 33 | wasi::sockets::tcp_create_socket::create_tcp_socket(family).map_err(to_io_err)?; 34 | let network = wasi::sockets::instance_network::instance_network(); 35 | 36 | let local_address = match addr { 37 | SocketAddr::V4(addr) => { 38 | let ip = addr.ip().octets(); 39 | let address = (ip[0], ip[1], ip[2], ip[3]); 40 | let port = addr.port(); 41 | IpSocketAddress::Ipv4(Ipv4SocketAddress { port, address }) 42 | } 43 | SocketAddr::V6(_) => todo!("IPv6 not yet supported in `wstd::net::TcpListener`"), 44 | }; 45 | socket 46 | .start_bind(&network, local_address) 47 | .map_err(to_io_err)?; 48 | let pollable = AsyncPollable::new(socket.subscribe()); 49 | pollable.wait_for().await; 50 | socket.finish_bind().map_err(to_io_err)?; 51 | 52 | socket.start_listen().map_err(to_io_err)?; 53 | pollable.wait_for().await; 54 | socket.finish_listen().map_err(to_io_err)?; 55 | Ok(Self { pollable, socket }) 56 | } 57 | 58 | /// Returns the local socket address of this listener. 59 | // TODO: make this return an actual socket addr 60 | pub fn local_addr(&self) -> io::Result { 61 | let addr = self.socket.local_address().map_err(to_io_err)?; 62 | Ok(format!("{addr:?}")) 63 | } 64 | 65 | /// Returns an iterator over the connections being received on this listener. 66 | pub fn incoming(&self) -> Incoming<'_> { 67 | Incoming { listener: self } 68 | } 69 | } 70 | 71 | /// An iterator that infinitely accepts connections on a TcpListener. 72 | #[derive(Debug)] 73 | pub struct Incoming<'a> { 74 | listener: &'a TcpListener, 75 | } 76 | 77 | impl<'a> AsyncIterator for Incoming<'a> { 78 | type Item = io::Result; 79 | 80 | async fn next(&mut self) -> Option { 81 | self.listener.pollable.wait_for().await; 82 | let (socket, input, output) = match self.listener.socket.accept().map_err(to_io_err) { 83 | Ok(accepted) => accepted, 84 | Err(err) => return Some(Err(err)), 85 | }; 86 | Some(Ok(TcpStream::new(input, output, socket))) 87 | } 88 | } 89 | 90 | pub(super) fn to_io_err(err: ErrorCode) -> io::Error { 91 | match err { 92 | wasi::sockets::network::ErrorCode::Unknown => ErrorKind::Other.into(), 93 | wasi::sockets::network::ErrorCode::AccessDenied => ErrorKind::PermissionDenied.into(), 94 | wasi::sockets::network::ErrorCode::NotSupported => ErrorKind::Unsupported.into(), 95 | wasi::sockets::network::ErrorCode::InvalidArgument => ErrorKind::InvalidInput.into(), 96 | wasi::sockets::network::ErrorCode::OutOfMemory => ErrorKind::OutOfMemory.into(), 97 | wasi::sockets::network::ErrorCode::Timeout => ErrorKind::TimedOut.into(), 98 | wasi::sockets::network::ErrorCode::WouldBlock => ErrorKind::WouldBlock.into(), 99 | wasi::sockets::network::ErrorCode::InvalidState => ErrorKind::InvalidData.into(), 100 | wasi::sockets::network::ErrorCode::AddressInUse => ErrorKind::AddrInUse.into(), 101 | wasi::sockets::network::ErrorCode::ConnectionRefused => ErrorKind::ConnectionRefused.into(), 102 | wasi::sockets::network::ErrorCode::ConnectionReset => ErrorKind::ConnectionReset.into(), 103 | wasi::sockets::network::ErrorCode::ConnectionAborted => ErrorKind::ConnectionAborted.into(), 104 | wasi::sockets::network::ErrorCode::ConcurrencyConflict => ErrorKind::AlreadyExists.into(), 105 | _ => ErrorKind::Other.into(), 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/net/tcp_stream.rs: -------------------------------------------------------------------------------- 1 | use wasi::{ 2 | io::streams::{InputStream, OutputStream}, 3 | sockets::tcp::TcpSocket, 4 | }; 5 | 6 | use crate::io::{self, AsyncInputStream, AsyncOutputStream}; 7 | 8 | /// A TCP stream between a local and a remote socket. 9 | pub struct TcpStream { 10 | input: AsyncInputStream, 11 | output: AsyncOutputStream, 12 | socket: TcpSocket, 13 | } 14 | 15 | impl TcpStream { 16 | pub(crate) fn new(input: InputStream, output: OutputStream, socket: TcpSocket) -> Self { 17 | TcpStream { 18 | input: AsyncInputStream::new(input), 19 | output: AsyncOutputStream::new(output), 20 | socket, 21 | } 22 | } 23 | /// Returns the socket address of the remote peer of this TCP connection. 24 | pub fn peer_addr(&self) -> io::Result { 25 | let addr = self 26 | .socket 27 | .remote_address() 28 | .map_err(super::tcp_listener::to_io_err)?; 29 | Ok(format!("{addr:?}")) 30 | } 31 | 32 | pub fn split(&self) -> (ReadHalf<'_>, WriteHalf<'_>) { 33 | (ReadHalf(self), WriteHalf(self)) 34 | } 35 | } 36 | 37 | impl Drop for TcpStream { 38 | fn drop(&mut self) { 39 | let _ = self.socket.shutdown(wasi::sockets::tcp::ShutdownType::Both); 40 | } 41 | } 42 | 43 | impl io::AsyncRead for TcpStream { 44 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 45 | self.input.read(buf).await 46 | } 47 | 48 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 49 | Some(&self.input) 50 | } 51 | } 52 | 53 | impl io::AsyncRead for &TcpStream { 54 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 55 | self.input.read(buf).await 56 | } 57 | 58 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 59 | (**self).as_async_input_stream() 60 | } 61 | } 62 | 63 | impl io::AsyncWrite for TcpStream { 64 | async fn write(&mut self, buf: &[u8]) -> io::Result { 65 | self.output.write(buf).await 66 | } 67 | 68 | async fn flush(&mut self) -> io::Result<()> { 69 | self.output.flush().await 70 | } 71 | 72 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 73 | Some(&self.output) 74 | } 75 | } 76 | 77 | impl io::AsyncWrite for &TcpStream { 78 | async fn write(&mut self, buf: &[u8]) -> io::Result { 79 | self.output.write(buf).await 80 | } 81 | 82 | async fn flush(&mut self) -> io::Result<()> { 83 | self.output.flush().await 84 | } 85 | 86 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 87 | (**self).as_async_output_stream() 88 | } 89 | } 90 | 91 | pub struct ReadHalf<'a>(&'a TcpStream); 92 | impl<'a> io::AsyncRead for ReadHalf<'a> { 93 | async fn read(&mut self, buf: &mut [u8]) -> io::Result { 94 | self.0.read(buf).await 95 | } 96 | 97 | fn as_async_input_stream(&self) -> Option<&AsyncInputStream> { 98 | self.0.as_async_input_stream() 99 | } 100 | } 101 | 102 | impl<'a> Drop for ReadHalf<'a> { 103 | fn drop(&mut self) { 104 | let _ = self 105 | .0 106 | .socket 107 | .shutdown(wasi::sockets::tcp::ShutdownType::Receive); 108 | } 109 | } 110 | 111 | pub struct WriteHalf<'a>(&'a TcpStream); 112 | impl<'a> io::AsyncWrite for WriteHalf<'a> { 113 | async fn write(&mut self, buf: &[u8]) -> io::Result { 114 | self.0.write(buf).await 115 | } 116 | 117 | async fn flush(&mut self) -> io::Result<()> { 118 | self.0.flush().await 119 | } 120 | 121 | fn as_async_output_stream(&self) -> Option<&AsyncOutputStream> { 122 | self.0.as_async_output_stream() 123 | } 124 | } 125 | 126 | impl<'a> Drop for WriteHalf<'a> { 127 | fn drop(&mut self) { 128 | let _ = self 129 | .0 130 | .socket 131 | .shutdown(wasi::sockets::tcp::ShutdownType::Send); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/rand/mod.rs: -------------------------------------------------------------------------------- 1 | //! Random number generation. 2 | 3 | use wasi::random; 4 | 5 | /// Fill the slice with cryptographically secure random bytes. 6 | pub fn get_random_bytes(buf: &mut [u8]) { 7 | match buf.len() { 8 | 0 => {} 9 | _ => { 10 | let output = random::random::get_random_bytes(buf.len() as u64); 11 | buf.copy_from_slice(&output[..]); 12 | } 13 | } 14 | } 15 | 16 | /// Fill the slice with insecure random bytes. 17 | pub fn get_insecure_random_bytes(buf: &mut [u8]) { 18 | match buf.len() { 19 | 0 => {} 20 | _ => { 21 | let output = random::insecure::get_insecure_random_bytes(buf.len() as u64); 22 | buf.copy_from_slice(&output[..]); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/runtime/block_on.rs: -------------------------------------------------------------------------------- 1 | use super::{Reactor, REACTOR}; 2 | 3 | use core::future::Future; 4 | use core::pin::pin; 5 | use core::task::Waker; 6 | use core::task::{Context, Poll}; 7 | use std::sync::Arc; 8 | use std::task::Wake; 9 | 10 | /// Start the event loop 11 | pub fn block_on(fut: Fut) -> Fut::Output 12 | where 13 | Fut: Future, 14 | { 15 | // Construct the reactor 16 | let reactor = Reactor::new(); 17 | // Store a copy as a singleton to be used elsewhere: 18 | let prev = REACTOR.replace(Some(reactor.clone())); 19 | if prev.is_some() { 20 | panic!("cannot wstd::runtime::block_on inside an existing block_on!") 21 | } 22 | 23 | // Pin the future so it can be polled 24 | let mut fut = pin!(fut); 25 | 26 | // Create a new context to be passed to the future. 27 | let waker = noop_waker(); 28 | let mut cx = Context::from_waker(&waker); 29 | 30 | // Either the future completes and we return, or some IO is happening 31 | // and we wait. 32 | let res = loop { 33 | match fut.as_mut().poll(&mut cx) { 34 | Poll::Ready(res) => break res, 35 | Poll::Pending => reactor.block_until(), 36 | } 37 | }; 38 | // Clear the singleton 39 | REACTOR.replace(None); 40 | res 41 | } 42 | 43 | /// Construct a new no-op waker 44 | // NOTE: we can remove this once lands 45 | fn noop_waker() -> Waker { 46 | struct NoopWaker; 47 | 48 | impl Wake for NoopWaker { 49 | fn wake(self: Arc) {} 50 | } 51 | 52 | Waker::from(Arc::new(NoopWaker)) 53 | } 54 | -------------------------------------------------------------------------------- /src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | //! Async event loop support. 2 | //! 3 | //! The way to use this is to call [`block_on()`]. Inside the future, [`Reactor::current`] 4 | //! will give an instance of the [`Reactor`] running the event loop, which can be 5 | //! to [`AsyncPollable::wait_for`] instances of 6 | //! [`wasi::Pollable`](https://docs.rs/wasi/latest/wasi/io/poll/struct.Pollable.html). 7 | //! This will automatically wait for the futures to resolve, and call the 8 | //! necessary wakers to work. 9 | 10 | #![deny(missing_debug_implementations, nonstandard_style)] 11 | #![warn(missing_docs, unreachable_pub)] 12 | 13 | mod block_on; 14 | mod reactor; 15 | 16 | pub use block_on::block_on; 17 | pub use reactor::{AsyncPollable, Reactor, WaitFor}; 18 | use std::cell::RefCell; 19 | 20 | // There are no threads in WASI 0.2, so this is just a safe way to thread a single reactor to all 21 | // use sites in the background. 22 | std::thread_local! { 23 | pub(crate) static REACTOR: RefCell> = const { RefCell::new(None) }; 24 | } 25 | -------------------------------------------------------------------------------- /src/runtime/reactor.rs: -------------------------------------------------------------------------------- 1 | use super::REACTOR; 2 | 3 | use core::cell::RefCell; 4 | use core::future; 5 | use core::pin::Pin; 6 | use core::task::{Context, Poll, Waker}; 7 | use slab::Slab; 8 | use std::collections::HashMap; 9 | use std::rc::Rc; 10 | use wasi::io::poll::Pollable; 11 | 12 | /// A key for a `Pollable`, which is an index into the `Slab` in `Reactor`. 13 | #[repr(transparent)] 14 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] 15 | pub(crate) struct EventKey(pub(crate) usize); 16 | 17 | /// A Registration is a reference to the Reactor's owned Pollable. When the registration is 18 | /// dropped, the reactor will drop the Pollable resource. 19 | #[derive(Debug, PartialEq, Eq, Hash)] 20 | struct Registration { 21 | key: EventKey, 22 | } 23 | 24 | impl Drop for Registration { 25 | fn drop(&mut self) { 26 | Reactor::current().deregister_event(self.key) 27 | } 28 | } 29 | 30 | /// An AsyncPollable is a reference counted Registration. It can be cloned, and used to create 31 | /// as many WaitFor futures on a Pollable that the user needs. 32 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 33 | pub struct AsyncPollable(Rc); 34 | 35 | impl AsyncPollable { 36 | /// Create an `AsyncPollable` from a Wasi `Pollable`. Schedules the `Pollable` with the current 37 | /// `Reactor`. 38 | pub fn new(pollable: Pollable) -> Self { 39 | Reactor::current().schedule(pollable) 40 | } 41 | /// Create a Future that waits for the Pollable's readiness. 42 | pub fn wait_for(&self) -> WaitFor { 43 | use std::sync::atomic::{AtomicUsize, Ordering}; 44 | static COUNTER: AtomicUsize = AtomicUsize::new(0); 45 | let unique = COUNTER.fetch_add(1, Ordering::Relaxed); 46 | WaitFor { 47 | waitee: Waitee { 48 | pollable: self.clone(), 49 | unique, 50 | }, 51 | needs_deregistration: false, 52 | } 53 | } 54 | } 55 | 56 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 57 | struct Waitee { 58 | /// This needs to be a reference counted registration, because it may outlive the AsyncPollable 59 | /// &self that it was created from. 60 | pollable: AsyncPollable, 61 | unique: usize, 62 | } 63 | 64 | /// A Future that waits for the Pollable's readiness. 65 | #[must_use = "futures do nothing unless polled or .awaited"] 66 | #[derive(Debug)] 67 | pub struct WaitFor { 68 | waitee: Waitee, 69 | needs_deregistration: bool, 70 | } 71 | impl future::Future for WaitFor { 72 | type Output = (); 73 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 74 | let reactor = Reactor::current(); 75 | if reactor.ready(&self.as_ref().waitee, cx.waker()) { 76 | Poll::Ready(()) 77 | } else { 78 | self.as_mut().needs_deregistration = true; 79 | Poll::Pending 80 | } 81 | } 82 | } 83 | impl Drop for WaitFor { 84 | fn drop(&mut self) { 85 | if self.needs_deregistration { 86 | Reactor::current().deregister_waitee(&self.waitee) 87 | } 88 | } 89 | } 90 | 91 | /// Manage async system resources for WASI 0.2 92 | #[derive(Debug, Clone)] 93 | pub struct Reactor { 94 | inner: Rc>, 95 | } 96 | 97 | /// The private, internal `Reactor` implementation - factored out so we can take 98 | /// a lock of the whole. 99 | #[derive(Debug)] 100 | struct InnerReactor { 101 | pollables: Slab, 102 | wakers: HashMap, 103 | } 104 | 105 | impl Reactor { 106 | /// Return a `Reactor` for the currently running `wstd::runtime::block_on`. 107 | /// 108 | /// # Panic 109 | /// This will panic if called outside of `wstd::runtime::block_on`. 110 | pub fn current() -> Self { 111 | REACTOR.with(|r| { 112 | r.borrow() 113 | .as_ref() 114 | .expect("Reactor::current must be called within a wstd runtime") 115 | .clone() 116 | }) 117 | } 118 | 119 | /// Create a new instance of `Reactor` 120 | pub(crate) fn new() -> Self { 121 | Self { 122 | inner: Rc::new(RefCell::new(InnerReactor { 123 | pollables: Slab::new(), 124 | wakers: HashMap::new(), 125 | })), 126 | } 127 | } 128 | 129 | /// Block until new events are ready. Calls the respective wakers once done. 130 | /// 131 | /// # On Wakers and single-threaded runtimes 132 | /// 133 | /// At first glance it might seem silly that this goes through the motions 134 | /// of calling the wakers. The main waker we create here is a `noop` waker: 135 | /// it does nothing. However, it is common and encouraged to use wakers to 136 | /// distinguish between events. Concurrency primitives may construct their 137 | /// own wakers to keep track of identity and wake more precisely. We do not 138 | /// control the wakers construted by other libraries, and it is for this 139 | /// reason that we have to call all the wakers - even if by default they 140 | /// will do nothing. 141 | pub(crate) fn block_until(&self) { 142 | let reactor = self.inner.borrow(); 143 | 144 | // We're about to wait for a number of pollables. When they wake we get 145 | // the *indexes* back for the pollables whose events were available - so 146 | // we need to be able to associate the index with the right waker. 147 | 148 | // We start by iterating over the pollables, and keeping note of which 149 | // pollable belongs to which waker 150 | let mut indexed_wakers = Vec::with_capacity(reactor.wakers.len()); 151 | let mut targets = Vec::with_capacity(reactor.wakers.len()); 152 | for (waitee, waker) in reactor.wakers.iter() { 153 | let pollable_index = waitee.pollable.0.key; 154 | indexed_wakers.push(waker); 155 | targets.push(&reactor.pollables[pollable_index.0]); 156 | } 157 | 158 | debug_assert_ne!( 159 | targets.len(), 160 | 0, 161 | "Attempting to block on an empty list of pollables - without any pending work, no progress can be made and wasi::io::poll::poll will trap" 162 | ); 163 | 164 | // Now that we have that association, we're ready to poll our targets. 165 | // This will block until an event has completed. 166 | let ready_indexes = wasi::io::poll::poll(&targets); 167 | 168 | // Once we have the indexes for which pollables are available, we need 169 | // to convert it back to the right keys for the wakers. Earlier we 170 | // established a positional index -> waker key relationship, so we can 171 | // go right ahead and perform a lookup there. 172 | let ready_wakers = ready_indexes 173 | .into_iter() 174 | .map(|index| indexed_wakers[index as usize]); 175 | 176 | for waker in ready_wakers { 177 | waker.wake_by_ref() 178 | } 179 | } 180 | 181 | /// Turn a Wasi [`Pollable`] into an [`AsyncPollable`] 182 | pub fn schedule(&self, pollable: Pollable) -> AsyncPollable { 183 | let mut reactor = self.inner.borrow_mut(); 184 | let key = EventKey(reactor.pollables.insert(pollable)); 185 | AsyncPollable(Rc::new(Registration { key })) 186 | } 187 | 188 | fn deregister_event(&self, key: EventKey) { 189 | let mut reactor = self.inner.borrow_mut(); 190 | reactor.pollables.remove(key.0); 191 | } 192 | 193 | fn deregister_waitee(&self, waitee: &Waitee) { 194 | let mut reactor = self.inner.borrow_mut(); 195 | reactor.wakers.remove(waitee); 196 | } 197 | 198 | fn ready(&self, waitee: &Waitee, waker: &Waker) -> bool { 199 | let mut reactor = self.inner.borrow_mut(); 200 | let ready = reactor 201 | .pollables 202 | .get(waitee.pollable.0.key.0) 203 | .expect("only live EventKey can be checked for readiness") 204 | .ready(); 205 | if !ready { 206 | reactor.wakers.insert(waitee.clone(), waker.clone()); 207 | } 208 | ready 209 | } 210 | } 211 | 212 | #[cfg(test)] 213 | mod test { 214 | use super::*; 215 | // Using WASMTIME_LOG, observe that this test doesn't even call poll() - the pollable is ready 216 | // immediately. 217 | #[test] 218 | fn subscribe_no_duration() { 219 | crate::runtime::block_on(async { 220 | let reactor = Reactor::current(); 221 | let pollable = wasi::clocks::monotonic_clock::subscribe_duration(0); 222 | let sched = reactor.schedule(pollable); 223 | sched.wait_for().await; 224 | }) 225 | } 226 | // Using WASMTIME_LOG, observe that this test calls poll() until the timer is ready. 227 | #[test] 228 | fn subscribe_some_duration() { 229 | crate::runtime::block_on(async { 230 | let reactor = Reactor::current(); 231 | let pollable = wasi::clocks::monotonic_clock::subscribe_duration(10_000_000); 232 | let sched = reactor.schedule(pollable); 233 | sched.wait_for().await; 234 | }) 235 | } 236 | 237 | // Using WASMTIME_LOG, observe that this test results in a single poll() on the second 238 | // subscription, rather than spinning in poll() with first subscription, which is instantly 239 | // ready, but not what the waker requests. 240 | #[test] 241 | fn subscribe_multiple_durations() { 242 | crate::runtime::block_on(async { 243 | let reactor = Reactor::current(); 244 | let now = wasi::clocks::monotonic_clock::subscribe_duration(0); 245 | let soon = wasi::clocks::monotonic_clock::subscribe_duration(10_000_000); 246 | let now = reactor.schedule(now); 247 | let soon = reactor.schedule(soon); 248 | soon.wait_for().await; 249 | drop(now) 250 | }) 251 | } 252 | 253 | // Using WASMTIME_LOG, observe that this test results in two calls to poll(), one with both 254 | // pollables because both are awaiting, and one with just the later pollable. 255 | #[test] 256 | fn subscribe_multiple_durations_zipped() { 257 | crate::runtime::block_on(async { 258 | let reactor = Reactor::current(); 259 | let start = wasi::clocks::monotonic_clock::now(); 260 | let soon = wasi::clocks::monotonic_clock::subscribe_duration(10_000_000); 261 | let later = wasi::clocks::monotonic_clock::subscribe_duration(40_000_000); 262 | let soon = reactor.schedule(soon); 263 | let later = reactor.schedule(later); 264 | 265 | futures_lite::future::zip( 266 | async move { 267 | soon.wait_for().await; 268 | println!( 269 | "*** subscribe_duration(soon) ready ({})", 270 | wasi::clocks::monotonic_clock::now() - start 271 | ); 272 | }, 273 | async move { 274 | later.wait_for().await; 275 | println!( 276 | "*** subscribe_duration(later) ready ({})", 277 | wasi::clocks::monotonic_clock::now() - start 278 | ); 279 | }, 280 | ) 281 | .await; 282 | }) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/task.rs: -------------------------------------------------------------------------------- 1 | //! Types and Traits for working with asynchronous tasks. 2 | 3 | use crate::time::{Duration, Instant, Timer, Wait}; 4 | 5 | /// Sleeps for the specified amount of time. 6 | pub fn sleep(dur: Duration) -> Wait { 7 | Timer::after(dur).wait() 8 | } 9 | 10 | /// Sleeps until the specified instant. 11 | pub fn sleep_until(deadline: Instant) -> Wait { 12 | Timer::at(deadline).wait() 13 | } 14 | -------------------------------------------------------------------------------- /src/time/duration.rs: -------------------------------------------------------------------------------- 1 | use super::{Instant, Wait}; 2 | use std::future::IntoFuture; 3 | use std::ops::{Add, AddAssign, Sub, SubAssign}; 4 | use wasi::clocks::monotonic_clock; 5 | 6 | /// A Duration type to represent a span of time, typically used for system 7 | /// timeouts. 8 | /// 9 | /// This type wraps `std::time::Duration` so we can implement traits on it 10 | /// without coherence issues, just like if we were implementing this in the 11 | /// stdlib. 12 | #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)] 13 | pub struct Duration(pub(crate) monotonic_clock::Duration); 14 | impl Duration { 15 | /// Creates a new `Duration` from the specified number of whole seconds and 16 | /// additional nanoseconds. 17 | #[must_use] 18 | #[inline] 19 | pub fn new(secs: u64, nanos: u32) -> Duration { 20 | std::time::Duration::new(secs, nanos).into() 21 | } 22 | 23 | /// Creates a new `Duration` from the specified number of whole seconds. 24 | #[must_use] 25 | #[inline] 26 | pub fn from_secs(secs: u64) -> Duration { 27 | std::time::Duration::from_secs(secs).into() 28 | } 29 | 30 | /// Creates a new `Duration` from the specified number of milliseconds. 31 | #[must_use] 32 | #[inline] 33 | pub fn from_millis(millis: u64) -> Self { 34 | std::time::Duration::from_millis(millis).into() 35 | } 36 | 37 | /// Creates a new `Duration` from the specified number of microseconds. 38 | #[must_use] 39 | #[inline] 40 | pub fn from_micros(micros: u64) -> Self { 41 | std::time::Duration::from_micros(micros).into() 42 | } 43 | 44 | /// Creates a new `Duration` from the specified number of nanoseconds. 45 | #[must_use] 46 | #[inline] 47 | pub fn from_nanos(nanos: u64) -> Self { 48 | std::time::Duration::from_nanos(nanos).into() 49 | } 50 | 51 | /// Creates a new `Duration` from the specified number of seconds represented 52 | /// as `f64`. 53 | /// 54 | /// # Panics 55 | /// This constructor will panic if `secs` is not finite, negative or overflows `Duration`. 56 | /// 57 | /// # Examples 58 | /// ```no_run 59 | /// use wstd::time::Duration; 60 | /// 61 | /// let dur = Duration::from_secs_f64(2.7); 62 | /// assert_eq!(dur, Duration::new(2, 700_000_000)); 63 | /// ``` 64 | #[must_use] 65 | #[inline] 66 | pub fn from_secs_f64(secs: f64) -> Duration { 67 | std::time::Duration::from_secs_f64(secs).into() 68 | } 69 | 70 | /// Creates a new `Duration` from the specified number of seconds represented 71 | /// as `f32`. 72 | /// 73 | /// # Panics 74 | /// This constructor will panic if `secs` is not finite, negative or overflows `Duration`. 75 | #[must_use] 76 | #[inline] 77 | pub fn from_secs_f32(secs: f32) -> Duration { 78 | std::time::Duration::from_secs_f32(secs).into() 79 | } 80 | 81 | /// Returns the number of whole seconds contained by this `Duration`. 82 | #[must_use] 83 | #[inline] 84 | pub const fn as_secs(&self) -> u64 { 85 | self.0 / 1_000_000_000 86 | } 87 | 88 | /// Returns the number of whole milliseconds contained by this `Duration`. 89 | #[must_use] 90 | #[inline] 91 | pub const fn as_millis(&self) -> u128 { 92 | (self.0 / 1_000_000) as u128 93 | } 94 | 95 | /// Returns the number of whole microseconds contained by this `Duration`. 96 | #[must_use] 97 | #[inline] 98 | pub const fn as_micros(&self) -> u128 { 99 | (self.0 / 1_000) as u128 100 | } 101 | 102 | /// Returns the total number of nanoseconds contained by this `Duration`. 103 | #[must_use] 104 | #[inline] 105 | pub const fn as_nanos(&self) -> u128 { 106 | self.0 as u128 107 | } 108 | } 109 | 110 | impl From for Duration { 111 | fn from(inner: std::time::Duration) -> Self { 112 | Self( 113 | inner 114 | .as_nanos() 115 | .try_into() 116 | .expect("only dealing with durations that can fit in u64"), 117 | ) 118 | } 119 | } 120 | 121 | impl From for std::time::Duration { 122 | fn from(duration: Duration) -> Self { 123 | Self::from_nanos(duration.0) 124 | } 125 | } 126 | 127 | impl Add for Duration { 128 | type Output = Self; 129 | 130 | fn add(self, rhs: Duration) -> Self::Output { 131 | Self(self.0 + rhs.0) 132 | } 133 | } 134 | 135 | impl AddAssign for Duration { 136 | fn add_assign(&mut self, rhs: Duration) { 137 | *self = Self(self.0 + rhs.0) 138 | } 139 | } 140 | 141 | impl Sub for Duration { 142 | type Output = Self; 143 | 144 | fn sub(self, rhs: Duration) -> Self::Output { 145 | Self(self.0 - rhs.0) 146 | } 147 | } 148 | 149 | impl SubAssign for Duration { 150 | fn sub_assign(&mut self, rhs: Duration) { 151 | *self = Self(self.0 - rhs.0) 152 | } 153 | } 154 | 155 | impl IntoFuture for Duration { 156 | type Output = Instant; 157 | 158 | type IntoFuture = Wait; 159 | 160 | fn into_future(self) -> Self::IntoFuture { 161 | crate::task::sleep(self) 162 | } 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | use super::*; 168 | 169 | #[test] 170 | fn test_new_from_as() { 171 | assert_eq!(Duration::new(456, 864209753).as_secs(), 456); 172 | assert_eq!(Duration::new(456, 864209753).as_millis(), 456864); 173 | assert_eq!(Duration::new(456, 864209753).as_micros(), 456864209); 174 | assert_eq!(Duration::new(456, 864209753).as_nanos(), 456864209753); 175 | 176 | assert_eq!(Duration::from_secs(9876543210).as_secs(), 9876543210); 177 | assert_eq!(Duration::from_secs(9876543210).as_millis(), 9876543210_000); 178 | assert_eq!( 179 | Duration::from_secs(9876543210).as_micros(), 180 | 9876543210_000000 181 | ); 182 | assert_eq!( 183 | Duration::from_secs(9876543210).as_nanos(), 184 | 9876543210_000000000 185 | ); 186 | 187 | assert_eq!(Duration::from_millis(9876543210).as_secs(), 9876543); 188 | assert_eq!(Duration::from_millis(9876543210).as_millis(), 9876543210); 189 | assert_eq!( 190 | Duration::from_millis(9876543210).as_micros(), 191 | 9876543210_000 192 | ); 193 | assert_eq!( 194 | Duration::from_millis(9876543210).as_nanos(), 195 | 9876543210_000000 196 | ); 197 | 198 | assert_eq!(Duration::from_micros(9876543210).as_secs(), 9876); 199 | assert_eq!(Duration::from_micros(9876543210).as_millis(), 9876543); 200 | assert_eq!(Duration::from_micros(9876543210).as_micros(), 9876543210); 201 | assert_eq!(Duration::from_micros(9876543210).as_nanos(), 9876543210_000); 202 | 203 | assert_eq!(Duration::from_nanos(9876543210).as_secs(), 9); 204 | assert_eq!(Duration::from_nanos(9876543210).as_millis(), 9876); 205 | assert_eq!(Duration::from_nanos(9876543210).as_micros(), 9876543); 206 | assert_eq!(Duration::from_nanos(9876543210).as_nanos(), 9876543210); 207 | } 208 | 209 | #[test] 210 | fn test_from_secs_float() { 211 | assert_eq!(Duration::from_secs_f64(158.9).as_secs(), 158); 212 | assert_eq!(Duration::from_secs_f32(158.9).as_secs(), 158); 213 | assert_eq!(Duration::from_secs_f64(159.1).as_secs(), 159); 214 | assert_eq!(Duration::from_secs_f32(159.1).as_secs(), 159); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/time/instant.rs: -------------------------------------------------------------------------------- 1 | use super::{Duration, Wait}; 2 | use std::future::IntoFuture; 3 | use std::ops::{Add, AddAssign, Sub, SubAssign}; 4 | use wasi::clocks::monotonic_clock; 5 | 6 | /// A measurement of a monotonically nondecreasing clock. Opaque and useful only 7 | /// with Duration. 8 | /// 9 | /// This type wraps `std::time::Duration` so we can implement traits on it 10 | /// without coherence issues, just like if we were implementing this in the 11 | /// stdlib. 12 | #[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Clone, Copy)] 13 | pub struct Instant(pub(crate) monotonic_clock::Instant); 14 | 15 | impl Instant { 16 | /// Returns an instant corresponding to "now". 17 | /// 18 | /// # Examples 19 | /// 20 | /// ```no_run 21 | /// use wstd::time::Instant; 22 | /// 23 | /// let now = Instant::now(); 24 | /// ``` 25 | #[must_use] 26 | pub fn now() -> Self { 27 | Instant(wasi::clocks::monotonic_clock::now()) 28 | } 29 | 30 | /// Returns the amount of time elapsed from another instant to this one, or zero duration if 31 | /// that instant is later than this one. 32 | pub fn duration_since(&self, earlier: Instant) -> Duration { 33 | Duration::from_nanos(self.0.checked_sub(earlier.0).unwrap_or_default()) 34 | } 35 | 36 | /// Returns the amount of time elapsed since this instant. 37 | pub fn elapsed(&self) -> Duration { 38 | Instant::now().duration_since(*self) 39 | } 40 | } 41 | 42 | impl Add for Instant { 43 | type Output = Self; 44 | 45 | fn add(self, rhs: Duration) -> Self::Output { 46 | Self(self.0 + rhs.0) 47 | } 48 | } 49 | 50 | impl AddAssign for Instant { 51 | fn add_assign(&mut self, rhs: Duration) { 52 | *self = Self(self.0 + rhs.0) 53 | } 54 | } 55 | 56 | impl Sub for Instant { 57 | type Output = Self; 58 | 59 | fn sub(self, rhs: Duration) -> Self::Output { 60 | Self(self.0 - rhs.0) 61 | } 62 | } 63 | 64 | impl SubAssign for Instant { 65 | fn sub_assign(&mut self, rhs: Duration) { 66 | *self = Self(self.0 - rhs.0) 67 | } 68 | } 69 | 70 | impl IntoFuture for Instant { 71 | type Output = Instant; 72 | 73 | type IntoFuture = Wait; 74 | 75 | fn into_future(self) -> Self::IntoFuture { 76 | crate::task::sleep_until(self) 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | 84 | #[test] 85 | fn test_duration_since() { 86 | let x = Instant::now(); 87 | let d = Duration::new(456, 789); 88 | let y = x + d; 89 | assert_eq!(y.duration_since(x), d); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/time/mod.rs: -------------------------------------------------------------------------------- 1 | //! Async time interfaces. 2 | 3 | pub(crate) mod utils; 4 | 5 | mod duration; 6 | mod instant; 7 | pub use duration::Duration; 8 | pub use instant::Instant; 9 | 10 | use pin_project_lite::pin_project; 11 | use std::future::Future; 12 | use std::pin::Pin; 13 | use std::task::{Context, Poll}; 14 | use wasi::clocks::{ 15 | monotonic_clock::{subscribe_duration, subscribe_instant}, 16 | wall_clock, 17 | }; 18 | 19 | use crate::{ 20 | iter::AsyncIterator, 21 | runtime::{AsyncPollable, Reactor}, 22 | }; 23 | 24 | /// A measurement of the system clock, useful for talking to external entities 25 | /// like the file system or other processes. 26 | #[derive(Debug, Clone, Copy)] 27 | #[allow(dead_code)] 28 | pub struct SystemTime(wall_clock::Datetime); 29 | 30 | impl SystemTime { 31 | pub fn now() -> Self { 32 | Self(wall_clock::now()) 33 | } 34 | } 35 | 36 | /// An async iterator representing notifications at fixed interval. 37 | pub fn interval(duration: Duration) -> Interval { 38 | Interval { duration } 39 | } 40 | 41 | /// An async iterator representing notifications at fixed interval. 42 | /// 43 | /// See the [`interval`] function for more. 44 | #[derive(Debug)] 45 | pub struct Interval { 46 | duration: Duration, 47 | } 48 | impl AsyncIterator for Interval { 49 | type Item = Instant; 50 | 51 | async fn next(&mut self) -> Option { 52 | Some(Timer::after(self.duration).wait().await) 53 | } 54 | } 55 | 56 | #[derive(Debug)] 57 | pub struct Timer(Option); 58 | 59 | impl Timer { 60 | pub fn never() -> Timer { 61 | Timer(None) 62 | } 63 | pub fn at(deadline: Instant) -> Timer { 64 | let pollable = Reactor::current().schedule(subscribe_instant(deadline.0)); 65 | Timer(Some(pollable)) 66 | } 67 | pub fn after(duration: Duration) -> Timer { 68 | let pollable = Reactor::current().schedule(subscribe_duration(duration.0)); 69 | Timer(Some(pollable)) 70 | } 71 | pub fn set_after(&mut self, duration: Duration) { 72 | *self = Self::after(duration); 73 | } 74 | pub fn wait(&self) -> Wait { 75 | let wait_for = self.0.as_ref().map(AsyncPollable::wait_for); 76 | Wait { wait_for } 77 | } 78 | } 79 | 80 | pin_project! { 81 | /// Future created by [`Timer::wait`] 82 | #[must_use = "futures do nothing unless polled or .awaited"] 83 | pub struct Wait { 84 | #[pin] 85 | wait_for: Option 86 | } 87 | } 88 | 89 | impl Future for Wait { 90 | type Output = Instant; 91 | 92 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 93 | let this = self.project(); 94 | match this.wait_for.as_pin_mut() { 95 | None => Poll::Pending, 96 | Some(f) => match f.poll(cx) { 97 | Poll::Pending => Poll::Pending, 98 | Poll::Ready(()) => Poll::Ready(Instant::now()), 99 | }, 100 | } 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod test { 106 | use super::*; 107 | 108 | async fn debug_duration(what: &str, f: impl Future) { 109 | let start = Instant::now(); 110 | let now = f.await; 111 | let d = now.duration_since(start); 112 | let d: std::time::Duration = d.into(); 113 | println!("{what} awaited for {} s", d.as_secs_f32()); 114 | } 115 | 116 | #[test] 117 | fn timer_now() { 118 | crate::runtime::block_on(debug_duration("timer_now", async { 119 | Timer::at(Instant::now()).wait().await 120 | })); 121 | } 122 | 123 | #[test] 124 | fn timer_after_100_milliseconds() { 125 | crate::runtime::block_on(debug_duration("timer_after_100_milliseconds", async { 126 | Timer::after(Duration::from_millis(100)).wait().await 127 | })); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/time/utils.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | pub(crate) fn timeout_err(msg: &'static str) -> io::Error { 4 | io::Error::new(io::ErrorKind::TimedOut, msg) 5 | } 6 | -------------------------------------------------------------------------------- /test-programs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-programs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | futures-lite.workspace = true 9 | serde_json.workspace = true 10 | wstd.workspace = true 11 | -------------------------------------------------------------------------------- /test-programs/artifacts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-programs-artifacts" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | license.workspace = true 6 | publish = false 7 | 8 | [dependencies] 9 | 10 | [dev-dependencies] 11 | anyhow.workspace = true 12 | test-log.workspace = true 13 | test-programs-artifacts.workspace = true 14 | ureq.workspace = true 15 | wasmtime.workspace = true 16 | wasmtime-wasi.workspace = true 17 | wasmtime-wasi-http.workspace = true 18 | 19 | [build-dependencies] 20 | cargo_metadata.workspace = true 21 | heck.workspace = true 22 | -------------------------------------------------------------------------------- /test-programs/artifacts/build.rs: -------------------------------------------------------------------------------- 1 | use heck::ToShoutySnakeCase; 2 | use std::env::var_os; 3 | use std::path::PathBuf; 4 | use std::process::Command; 5 | 6 | fn main() { 7 | let out_dir = PathBuf::from(var_os("OUT_DIR").expect("OUT_DIR env var exists")); 8 | 9 | let meta = cargo_metadata::MetadataCommand::new() 10 | .exec() 11 | .expect("cargo metadata"); 12 | let test_programs_meta = meta 13 | .packages 14 | .iter() 15 | .find(|p| p.name == "test-programs") 16 | .expect("test-programs is in cargo metadata"); 17 | let test_programs_root = test_programs_meta.manifest_path.parent().unwrap(); 18 | println!( 19 | "cargo:rerun-if-changed={}", 20 | test_programs_root.as_os_str().to_str().unwrap() 21 | ); 22 | 23 | let status = Command::new("cargo") 24 | .arg("build") 25 | .arg("--target=wasm32-wasip2") 26 | .arg("--package=test-programs") 27 | .env("CARGO_TARGET_DIR", &out_dir) 28 | .env("CARGO_PROFILE_DEV_DEBUG", "2") 29 | .env("RUSTFLAGS", rustflags()) 30 | .env_remove("CARGO_ENCODED_RUSTFLAGS") 31 | .status() 32 | .expect("cargo build test programs"); 33 | assert!(status.success()); 34 | 35 | let mut generated_code = "// THIS FILE IS GENERATED CODE\n".to_string(); 36 | 37 | for binary in test_programs_meta 38 | .targets 39 | .iter() 40 | .filter(|t| t.kind == ["bin"]) 41 | { 42 | let component_path = out_dir 43 | .join("wasm32-wasip2") 44 | .join("debug") 45 | .join(format!("{}.wasm", binary.name)); 46 | 47 | let const_name = binary.name.to_shouty_snake_case(); 48 | generated_code += &format!( 49 | "pub const {const_name}: &str = {:?};\n", 50 | component_path.as_os_str().to_str().expect("path is str") 51 | ); 52 | } 53 | 54 | std::fs::write(out_dir.join("gen.rs"), generated_code).unwrap(); 55 | } 56 | 57 | fn rustflags() -> &'static str { 58 | match option_env!("RUSTFLAGS") { 59 | Some(s) if s.contains("-D warnings") => "-D warnings", 60 | _ => "", 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test-programs/artifacts/src/lib.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/gen.rs")); 2 | -------------------------------------------------------------------------------- /test-programs/artifacts/tests/http_server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use std::process::Command; 3 | 4 | #[test_log::test] 5 | fn http_server() -> Result<()> { 6 | use std::net::TcpStream; 7 | use std::thread::sleep; 8 | use std::time::Duration; 9 | 10 | // Run wasmtime serve. 11 | // Enable -Scli because we currently don't have a way to build with the 12 | // proxy adapter, so we build with the default adapter. 13 | let mut wasmtime_process = Command::new("wasmtime") 14 | .arg("serve") 15 | .arg("-Scli") 16 | .arg("--addr=127.0.0.1:8081") 17 | .arg(test_programs_artifacts::HTTP_SERVER) 18 | .spawn()?; 19 | 20 | // Clumsily wait for the server to accept connections. 21 | 'wait: loop { 22 | sleep(Duration::from_millis(100)); 23 | if TcpStream::connect("127.0.0.1:8081").is_ok() { 24 | break 'wait; 25 | } 26 | } 27 | 28 | // Do some tests! 29 | 30 | let body: String = ureq::get("http://127.0.0.1:8081").call()?.into_string()?; 31 | assert_eq!(body, "Hello, wasi:http/proxy world!\n"); 32 | 33 | match ureq::get("http://127.0.0.1:8081/fail").call() { 34 | Ok(body) => { 35 | unreachable!("unexpected success from /fail: {:?}", body); 36 | } 37 | Err(ureq::Error::Transport(_transport)) => {} 38 | Err(other) => { 39 | unreachable!("unexpected error: {:?}", other); 40 | } 41 | } 42 | 43 | const MESSAGE: &[u8] = b"hello, echoserver!\n"; 44 | 45 | let body: String = ureq::get("http://127.0.0.1:8081/echo") 46 | .send(MESSAGE)? 47 | .into_string()?; 48 | assert_eq!(body.as_bytes(), MESSAGE); 49 | 50 | let test_headers = [ 51 | ("Red", "Rhubarb"), 52 | ("Orange", "Carrots"), 53 | ("Yellow", "Bananas"), 54 | ("Green", "Broccoli"), 55 | ("Blue", "Blueberries"), 56 | ("Purple", "Beets"), 57 | ]; 58 | 59 | let mut response = ureq::get("http://127.0.0.1:8081/echo-headers"); 60 | for (name, value) in test_headers { 61 | response = response.set(name, value); 62 | } 63 | let response = response.call()?; 64 | 65 | assert!(response.headers_names().len() >= test_headers.len()); 66 | for (name, value) in test_headers { 67 | assert_eq!(response.header(name), Some(value)); 68 | } 69 | 70 | wasmtime_process.kill()?; 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /test-programs/artifacts/tests/tcp_echo_server.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use wasmtime::{ 3 | component::{Component, Linker, ResourceTable}, 4 | Config, Engine, Store, 5 | }; 6 | use wasmtime_wasi::{pipe::MemoryOutputPipe, WasiCtx, WasiView}; 7 | use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; 8 | 9 | struct Ctx { 10 | table: ResourceTable, 11 | wasi: WasiCtx, 12 | http: WasiHttpCtx, 13 | } 14 | 15 | impl WasiView for Ctx { 16 | fn table(&mut self) -> &mut ResourceTable { 17 | &mut self.table 18 | } 19 | fn ctx(&mut self) -> &mut WasiCtx { 20 | &mut self.wasi 21 | } 22 | } 23 | 24 | impl WasiHttpView for Ctx { 25 | fn table(&mut self) -> &mut ResourceTable { 26 | &mut self.table 27 | } 28 | fn ctx(&mut self) -> &mut WasiHttpCtx { 29 | &mut self.http 30 | } 31 | } 32 | 33 | fn run_in_wasmtime(wasm: &[u8], stdout: Option) -> Result<()> { 34 | let config = Config::default(); 35 | let engine = Engine::new(&config).context("creating engine")?; 36 | let component = Component::new(&engine, wasm).context("loading component")?; 37 | 38 | let mut linker: Linker = Linker::new(&engine); 39 | wasmtime_wasi::add_to_linker_sync(&mut linker).context("add wasi to linker")?; 40 | wasmtime_wasi_http::add_only_http_to_linker_sync(&mut linker) 41 | .context("add wasi-http to linker")?; 42 | 43 | let mut builder = WasiCtx::builder(); 44 | builder.inherit_stderr().inherit_network(); 45 | let wasi = match stdout { 46 | Some(stdout) => builder.stdout(stdout).build(), 47 | None => builder.inherit_stdout().build(), 48 | }; 49 | let mut store = Store::new( 50 | &engine, 51 | Ctx { 52 | table: ResourceTable::new(), 53 | wasi, 54 | http: WasiHttpCtx::new(), 55 | }, 56 | ); 57 | 58 | let instance = linker.instantiate(&mut store, &component)?; 59 | let run_interface = instance 60 | .get_export(&mut store, None, "wasi:cli/run@0.2.0") 61 | .ok_or_else(|| anyhow!("wasi:cli/run missing?"))?; 62 | let run_func_export = instance 63 | .get_export(&mut store, Some(&run_interface), "run") 64 | .ok_or_else(|| anyhow!("run export missing?"))?; 65 | let run_func = instance 66 | .get_typed_func::<(), (Result<(), ()>,)>(&mut store, &run_func_export) 67 | .context("run as typed func")?; 68 | 69 | println!("entering wasm..."); 70 | let (runtime_result,) = run_func.call(&mut store, ())?; 71 | runtime_result.map_err(|()| anyhow!("run returned an error"))?; 72 | println!("done"); 73 | 74 | Ok(()) 75 | } 76 | 77 | #[test_log::test] 78 | fn tcp_echo_server() -> Result<()> { 79 | use std::io::{Read, Write}; 80 | use std::net::{Shutdown, TcpStream}; 81 | use std::thread::sleep; 82 | use std::time::Duration; 83 | 84 | println!("testing {}", test_programs_artifacts::TCP_ECHO_SERVER); 85 | let wasm = std::fs::read(test_programs_artifacts::TCP_ECHO_SERVER).context("read wasm")?; 86 | 87 | let pipe = wasmtime_wasi::pipe::MemoryOutputPipe::new(1024 * 1024); 88 | let write_end = pipe.clone(); 89 | let wasmtime_thread = std::thread::spawn(move || run_in_wasmtime(&wasm, Some(write_end))); 90 | 91 | 'wait: loop { 92 | sleep(Duration::from_millis(100)); 93 | for line in pipe.contents().split(|c| *c == b'\n') { 94 | if line.starts_with(b"Listening on") { 95 | break 'wait; 96 | } 97 | } 98 | } 99 | 100 | let mut tcpstream = 101 | TcpStream::connect("127.0.0.1:8080").context("connect to wasm echo server")?; 102 | println!("connected to wasm echo server"); 103 | 104 | const MESSAGE: &[u8] = b"hello, echoserver!\n"; 105 | 106 | tcpstream.write_all(MESSAGE).context("write to socket")?; 107 | println!("wrote to echo server"); 108 | 109 | tcpstream.shutdown(Shutdown::Write)?; 110 | 111 | let mut readback = Vec::new(); 112 | tcpstream 113 | .read_to_end(&mut readback) 114 | .context("read from socket")?; 115 | 116 | println!("read from wasm server"); 117 | assert_eq!(MESSAGE, readback); 118 | 119 | if wasmtime_thread.is_finished() { 120 | wasmtime_thread.join().expect("wasmtime panicked")?; 121 | } 122 | Ok(()) 123 | } 124 | -------------------------------------------------------------------------------- /test-programs/src/bin/http_server.rs: -------------------------------------------------------------------------------- 1 | include!("../../../examples/http_server.rs"); 2 | -------------------------------------------------------------------------------- /test-programs/src/bin/tcp_echo_server.rs: -------------------------------------------------------------------------------- 1 | include!("../../../examples/tcp_echo_server.rs"); 2 | -------------------------------------------------------------------------------- /tests/http_first_byte_timeout.rs: -------------------------------------------------------------------------------- 1 | use wstd::http::{ 2 | error::{ErrorVariant, WasiHttpErrorCode}, 3 | Client, Request, 4 | }; 5 | use wstd::io::empty; 6 | 7 | #[wstd::main] 8 | async fn main() -> Result<(), Box> { 9 | // Set first byte timeout to 1/2 second. 10 | let mut client = Client::new(); 11 | client.set_first_byte_timeout(std::time::Duration::from_millis(500)); 12 | // This get request will connect to the server, which will then wait 1 second before 13 | // returning a response. 14 | let request = Request::get("https://postman-echo.com/delay/1").body(empty())?; 15 | let result = client.send(request).await; 16 | 17 | assert!(result.is_err(), "response should be an error"); 18 | let error = result.unwrap_err(); 19 | assert!( 20 | matches!( 21 | error.variant(), 22 | ErrorVariant::WasiHttp(WasiHttpErrorCode::ConnectionReadTimeout) 23 | ), 24 | "expected ConnectionReadTimeout error, got: {error:?>}" 25 | ); 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /tests/http_get.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use wstd::http::{Body, Client, HeaderValue, Request}; 3 | use wstd::io::{empty, AsyncRead}; 4 | 5 | #[wstd::test] 6 | async fn main() -> Result<(), Box> { 7 | let request = Request::get("https://postman-echo.com/get") 8 | .header("my-header", HeaderValue::from_str("my-value")?) 9 | .body(empty())?; 10 | 11 | let mut response = Client::new().send(request).await?; 12 | 13 | let content_type = response 14 | .headers() 15 | .get("Content-Type") 16 | .ok_or_else(|| "response expected to have Content-Type header")?; 17 | assert_eq!(content_type, "application/json; charset=utf-8"); 18 | 19 | let body = response.body_mut(); 20 | let body_len = body 21 | .len() 22 | .ok_or_else(|| "GET postman-echo.com/get is supposed to provide a content-length")?; 23 | 24 | let mut body_buf = Vec::new(); 25 | body.read_to_end(&mut body_buf).await?; 26 | 27 | assert_eq!( 28 | body_buf.len(), 29 | body_len, 30 | "read_to_end length should match content-length" 31 | ); 32 | 33 | let val: serde_json::Value = serde_json::from_slice(&body_buf)?; 34 | let body_url = val 35 | .get("url") 36 | .ok_or_else(|| "body json has url")? 37 | .as_str() 38 | .ok_or_else(|| "body json url is str")?; 39 | assert!( 40 | body_url.contains("postman-echo.com/get"), 41 | "expected body url to contain the authority and path, got: {body_url}" 42 | ); 43 | 44 | assert_eq!( 45 | val.get("headers") 46 | .ok_or_else(|| "body json has headers")? 47 | .get("my-header") 48 | .ok_or_else(|| "headers contains my-header")? 49 | .as_str() 50 | .ok_or_else(|| "my-header is a str")?, 51 | "my-value" 52 | ); 53 | 54 | Ok(()) 55 | } 56 | -------------------------------------------------------------------------------- /tests/http_get_json.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::error::Error; 3 | use wstd::http::{Client, Request}; 4 | use wstd::io::empty; 5 | 6 | #[derive(Deserialize)] 7 | struct Echo { 8 | url: String, 9 | } 10 | 11 | #[wstd::test] 12 | async fn main() -> Result<(), Box> { 13 | let request = Request::get("https://postman-echo.com/get").body(empty())?; 14 | 15 | let mut response = Client::new().send(request).await?; 16 | 17 | let content_type = response 18 | .headers() 19 | .get("Content-Type") 20 | .ok_or_else(|| "response expected to have Content-Type header")?; 21 | assert_eq!(content_type, "application/json; charset=utf-8"); 22 | 23 | let Echo { url } = response.body_mut().json::().await?; 24 | assert!( 25 | url.contains("postman-echo.com/get"), 26 | "expected body url to contain the authority and path, got: {url}" 27 | ); 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /tests/http_post.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use wstd::http::{Client, HeaderValue, IntoBody, Request}; 3 | use wstd::io::AsyncRead; 4 | 5 | #[wstd::test] 6 | async fn main() -> Result<(), Box> { 7 | let request = Request::post("https://postman-echo.com/post") 8 | .header( 9 | "content-type", 10 | HeaderValue::from_str("application/json; charset=utf-8")?, 11 | ) 12 | .body("{\"test\": \"data\"}".into_body())?; 13 | 14 | let mut response = Client::new().send(request).await?; 15 | 16 | let content_type = response 17 | .headers() 18 | .get("Content-Type") 19 | .ok_or_else(|| "response expected to have Content-Type header")?; 20 | assert_eq!(content_type, "application/json; charset=utf-8"); 21 | 22 | let mut body_buf = Vec::new(); 23 | response.body_mut().read_to_end(&mut body_buf).await?; 24 | 25 | let val: serde_json::Value = serde_json::from_slice(&body_buf)?; 26 | let body_url = val 27 | .get("url") 28 | .ok_or_else(|| "body json has url")? 29 | .as_str() 30 | .ok_or_else(|| "body json url is str")?; 31 | assert!( 32 | body_url.contains("postman-echo.com/post"), 33 | "expected body url to contain the authority and path, got: {body_url}" 34 | ); 35 | 36 | let posted_json = val 37 | .get("json") 38 | .ok_or_else(|| "body json has 'json' key")? 39 | .as_object() 40 | .ok_or_else(|| format!("body json 'json' is object. got {val:?}"))?; 41 | 42 | assert_eq!(posted_json.len(), 1); 43 | assert_eq!( 44 | posted_json 45 | .get("test") 46 | .ok_or_else(|| "returned json has 'test' key")? 47 | .as_str() 48 | .ok_or_else(|| "returned json 'test' key should be str value")?, 49 | "data" 50 | ); 51 | 52 | Ok(()) 53 | } 54 | -------------------------------------------------------------------------------- /tests/http_post_json.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::error::Error; 3 | use wstd::http::{request::JsonRequest, Client, Request}; 4 | 5 | #[derive(Serialize)] 6 | struct TestData { 7 | test: String, 8 | } 9 | 10 | #[derive(Deserialize)] 11 | struct Echo { 12 | url: String, 13 | } 14 | 15 | #[wstd::test] 16 | async fn main() -> Result<(), Box> { 17 | let test_data = TestData { 18 | test: "data".to_string(), 19 | }; 20 | let request = Request::post("https://postman-echo.com/post").json(&test_data)?; 21 | 22 | let content_type = request 23 | .headers() 24 | .get("Content-Type") 25 | .ok_or_else(|| "request expected to have Content-Type header")?; 26 | assert_eq!(content_type, "application/json; charset=utf-8"); 27 | 28 | let mut response = Client::new().send(request).await?; 29 | 30 | let content_type = response 31 | .headers() 32 | .get("Content-Type") 33 | .ok_or_else(|| "response expected to have Content-Type header")?; 34 | assert_eq!(content_type, "application/json; charset=utf-8"); 35 | 36 | let Echo { url } = response.body_mut().json::().await?; 37 | assert!( 38 | url.contains("postman-echo.com/post"), 39 | "expected body url to contain the authority and path, got: {url}" 40 | ); 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /tests/http_timeout.rs: -------------------------------------------------------------------------------- 1 | use wstd::future::FutureExt; 2 | use wstd::http::{Client, Request}; 3 | use wstd::io::empty; 4 | use wstd::time::Duration; 5 | 6 | #[wstd::test] 7 | async fn http_timeout() -> Result<(), Box> { 8 | // This get request will connect to the server, which will then wait 1 second before 9 | // returning a response. 10 | let request = Request::get("https://postman-echo.com/delay/1").body(empty())?; 11 | let result = Client::new() 12 | .send(request) 13 | .timeout(Duration::from_millis(500)) 14 | .await; 15 | 16 | assert!(result.is_err(), "response should be an error"); 17 | let error = result.unwrap_err(); 18 | assert!( 19 | matches!(error.kind(), std::io::ErrorKind::TimedOut), 20 | "expected TimedOut error, got: {error:?>}" 21 | ); 22 | 23 | Ok(()) 24 | } 25 | -------------------------------------------------------------------------------- /tests/sleep.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use wstd::task::sleep; 3 | use wstd::time::Duration; 4 | 5 | #[wstd::test] 6 | async fn just_sleep() -> Result<(), Box> { 7 | sleep(Duration::from_secs(1)).await; 8 | Ok(()) 9 | } 10 | --------------------------------------------------------------------------------