├── .github └── workflows │ ├── main.yml │ ├── publish.yml │ └── update.yml ├── LICENSE.md ├── README.md ├── imports.md ├── test └── README.md └── wit ├── error.wit ├── poll.wit ├── streams.wit └── world.wit /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | abi-up-to-date: 10 | name: Check ABI files are up-to-date 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: WebAssembly/wit-abi-up-to-date@v23 15 | with: 16 | wit-bindgen: '0.37.0' 17 | all-features: 'true' -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish a Wasm Component package to GitHub Artifacts 2 | 3 | # Run this action whenever a new release is tagged 4 | on: 5 | push: 6 | tags: 7 | - v* 8 | workflow_dispatch: 9 | 10 | env: 11 | IMAGE_NAME: ${{ github.repository }} 12 | 13 | jobs: 14 | publish: 15 | runs-on: ubuntu-latest 16 | permissions: 17 | id-token: write 18 | packages: write 19 | contents: write 20 | 21 | steps: 22 | # Checkout the repo and install dependencies 23 | - name: Checkout repository 24 | uses: actions/checkout@v2 25 | - name: Install cargo-binstall 26 | uses: cargo-bins/cargo-binstall@v1.10.15 27 | - name: Install wkg 28 | shell: bash 29 | run: cargo binstall wkg 30 | - name: Install cosign 31 | uses: sigstore/cosign-installer@v3.7.0 32 | 33 | # To version our image we want to obtain the version from the tag 34 | - name: Get version 35 | id: meta 36 | uses: docker/metadata-action@v5 37 | with: 38 | images: ghcr.io/webassembly/wasi/io 39 | tags: | 40 | type=semver,pattern={{version}} 41 | 42 | # To upload our image to the GitHub registry, we first have to login 43 | - name: Login to the GitHub registry 44 | uses: docker/login-action@v3 45 | with: 46 | registry: ghcr.io 47 | username: ${{ github.actor }} 48 | password: ${{ secrets.ORG_PAT }} 49 | 50 | # Build our `.wit` files into a Wasm binary 51 | - name: Build 52 | shell: bash 53 | run: wkg wit build -o wasi-io.wasm 54 | 55 | # Upload the Wasm binary to the GitHub registry 56 | - name: Publish to GitHub Container Registry 57 | id: publish 58 | uses: bytecodealliance/wkg-github-action@v5 59 | with: 60 | oci-reference-without-tag: 'ghcr.io/webassembly/wasi/io' 61 | file: 'wasi-io.wasm' 62 | description: 'A WASI API providing I/O stream abstractions.' 63 | source: 'https://github.com/webassembly/wasi' 64 | homepage: 'https://wasi.dev' 65 | version: ${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} 66 | licenses: 'Apache-2.0 WITH LLVM-exception' 67 | 68 | # Sign the output component 69 | - name: Sign the wasm component 70 | run: cosign sign --yes ghcr.io/webassembly/wasi/io@${{ steps.publish.outputs.digest }} 71 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: Create a PR to upgrade WIT to a new version 2 | 3 | # Manually dispatch this action from the Actions page 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | prev_version: 8 | description: 'Upgrading from version (without the v)' 9 | required: true 10 | type: string 11 | next_version: 12 | description: 'Upgrading to version (without the v)' 13 | required: true 14 | type: string 15 | 16 | permissions: 17 | pull-requests: write 18 | contents: write 19 | 20 | jobs: 21 | update-versions: 22 | runs-on: ubuntu-latest 23 | steps: 24 | # Checkout the repo and install dependencies 25 | - name: Checkout repository 26 | uses: actions/checkout@v4 27 | - name: Install cargo-binstall 28 | uses: cargo-bins/cargo-binstall@v1.10.15 29 | - name: Install wit-bindgen 30 | shell: bash 31 | run: cargo binstall wit-bindgen-cli 32 | - name: Install wit-deps 33 | shell: bash 34 | run: cargo binstall wit-deps-cli 35 | 36 | # echo input 37 | - name: Print the versions 38 | run: echo Upgrading from ${{ inputs.prev_version }} to ${{ inputs.next_version }} 39 | 40 | # upgrade 41 | - name: Upgrade tag 42 | run: find . -type f -name "*.wit" -exec sed -i "s/${{ inputs.prev_version }}/${{ inputs.next_version }}/g" {} + 43 | # - name: Upgrade wit deps 44 | # run: wit-deps update 45 | - name: Generate markdown 46 | run: wit-bindgen markdown wit --html-in-md --all-features 47 | 48 | # file PR 49 | - name: Create feature branch 50 | env: 51 | FEATURE_BRANCH: release-v${{ inputs.next_version }} 52 | run: | 53 | git config user.name "github-actions[bot]" 54 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 55 | git add . 56 | git checkout -b $FEATURE_BRANCH 57 | git push -u origin $FEATURE_BRANCH 58 | git commit -m "Update to v${{ inputs.next_version }}" 59 | git push 60 | 61 | - name: Create pull request 62 | run: gh pr create -B main -H release-v${{ inputs.next_version }} --title 'Release v${{ inputs.next_version }}' --body 'Updates the package version to v${{ inputs.next_version }}. Thanks!' 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2019-2023 the Contributors to the WASI Specification, published 2 | by the [WebAssembly Community Group][cg] under the 3 | [W3C Community Contributor License Agreement (CLA)][cla]. A human-readable 4 | [summary][summary] is available. 5 | 6 | [cg]: https://www.w3.org/community/webassembly/ 7 | [cla]: https://www.w3.org/community/about/agreements/cla/ 8 | [summary]: https://www.w3.org/community/about/agreements/cla-deed/ 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WASI I/O 2 | 3 | A proposed [WebAssembly System Interface](https://github.com/WebAssembly/WASI) API. 4 | 5 | ### Current Phase 6 | 7 | WASI I/O is currently in [Phase 3]. 8 | 9 | [Phase 3]: https://github.com/WebAssembly/WASI/blob/main/Proposals.md#phase-3---implementation-phase-cg--wg 10 | 11 | ### Champions 12 | 13 | - Dan Gohman 14 | 15 | ### Portability Criteria 16 | 17 | WASI I/O must have host implementations which can pass the testsuite on at least Windows, macOS, and Linux. 18 | 19 | WASI I/O must have at least two complete independent implementations. 20 | 21 | ## Table of Contents 22 | 23 | - [Introduction](#introduction) 24 | - [Goals [or Motivating Use Cases, or Scenarios]](#goals-or-motivating-use-cases-or-scenarios) 25 | - [Non-goals](#non-goals) 26 | - [API walk-through](#api-walk-through) 27 | - [Use case: copying from input to output using `read`/`write`](#use-case-copying-from-input-to-output-using-readwrite) 28 | - [Use case: copying from input to output using `splice`](#use-case-copying-from-input-to-output-using-splice) 29 | - [Use case: copying from input to output using `forward`](#use-case-copying-from-input-to-output-using-forward) 30 | - [Detailed design discussion](#detailed-design-discussion) 31 | - [Should we have support for non-blocking read/write?](#should-we-have-support-for-non-blocking-read-write) 32 | - [Why do read/write use u64 sizes?[Tricky design choice 2]](#why-do-read-write-use-u64-sizes) 33 | - [Why have a `forward` function when you can just `splice` in a loop?](#why-have-a-forward-function-when-you-can-just-splice-in-a-loop) 34 | - [Stakeholder Interest & Feedback](#stakeholder-interest--feedback) 35 | - [References & acknowledgements](#references--acknowledgements) 36 | 37 | ### Introduction 38 | 39 | Wasi I/O is an API providing I/O stream abstractions. There are two 40 | types, `input-stream`, and `output-stream`, which support `read` and 41 | `write`, respectively, as well as a number of utility functions. 42 | 43 | ### Goals 44 | 45 | - Be usable by wasi-libc to implement POSIX-like file and socket APIs. 46 | - Support many different kinds of host streams, including files, sockets, 47 | pipes, character devices, and more. 48 | 49 | ### Non-goals 50 | 51 | - Support for async. That will be addressed in the component-model async 52 | design, where we can have the benefit of tighter integration with language 53 | bindings. 54 | - Bidirectional streams. 55 | 56 | ### API walk-through 57 | 58 | #### Use Case: copying from input to output using `read`/`write` 59 | 60 | ```rust 61 | fn copy_data(input: InputStream, output: OutputStream) -> Result<(), StreamError> { 62 | const BUFFER_LEN: usize = 4096; 63 | 64 | let wait_input = [subscribe_to_input_stream(input)]; 65 | let wait_output = [subscribe_to_output_stream(output)]; 66 | 67 | loop { 68 | let (mut data, mut eos) = input.read(BUFFER_LEN)?; 69 | 70 | // If we didn't get any data promptly, wait for it. 71 | if data.len() == 0 { 72 | let _ = poll_list(&wait_input[..]); 73 | (data, eos) = input.read(BUFFER_LEN)?; 74 | } 75 | 76 | let mut remaining = &data[..]; 77 | while !remaining.is_empty() { 78 | let mut num_written = output.write(remaining)?; 79 | 80 | // If we didn't put any data promptly, wait for it. 81 | if num_written == 0 { 82 | let _ = poll_list(&wait_output[..]); 83 | num_written = output.write(remaining)?; 84 | } 85 | 86 | remaining = &remaining[num_written..]; 87 | } 88 | if eos { 89 | break; 90 | } 91 | } 92 | Ok(()) 93 | } 94 | ``` 95 | 96 | #### Use case: copying from input to output using `splice` 97 | 98 | ```rust 99 | fn copy_data(input: InputStream, output: OutputStream) -> Result<(), StreamError> { 100 | let wait_input = [subscribe_to_input_stream(input)]; 101 | 102 | loop { 103 | let (num_copied, eos) = output.splice(input, u64::MAX)?; 104 | if eos { 105 | break; 106 | } 107 | 108 | // If we didn't get any data promptly, wait for it. 109 | if num_copied == 0 { 110 | let _ = poll_list(&wait_input[..]); 111 | } 112 | } 113 | Ok(()) 114 | } 115 | ``` 116 | 117 | #### Use case: copying from input to output using `forward` 118 | 119 | ```rust 120 | fn copy_data(input: InputStream, output: OutputStream) -> Result<(), StreamError> { 121 | output.forward(input)?; 122 | Ok(()) 123 | } 124 | ``` 125 | 126 | ### Detailed design discussion 127 | 128 | #### Should we have support for non-blocking read/write? 129 | 130 | This may be something we'll need to revisit, but currently, the way 131 | non-blocking streams work is that they perform reads or writes that 132 | read or write fewer bytes than requested. 133 | 134 | #### Why do read/write use u64 sizes? 135 | 136 | This is to make the API independent of the address space size of 137 | the caller. Callees are still advised to avoid using sizes that 138 | are larger than their instances will be able to allocate. 139 | 140 | #### Why have a `forward` function when you can just `splice` in a loop? 141 | 142 | This seems like it'll be a common use case, and `forward` 143 | addresses it in a very simple way. 144 | 145 | ### Stakeholder Interest & Feedback 146 | 147 | Wasi-io is a dependency of wasi-filesystem, wasi-sockets, and wasi-http, and 148 | is a foundational piece of WASI Preview 2. 149 | 150 | ### References & acknowledgements 151 | 152 | Many thanks for valuable feedback and advice from: 153 | 154 | - Thanks to Luke Wagner for many design functions and the design of 155 | the component-model async streams which inform the design here. 156 | - Thanks to Calvin Prewitt for the idea to include a `forward` function 157 | in this API. 158 | -------------------------------------------------------------------------------- /imports.md: -------------------------------------------------------------------------------- 1 |
wasi:io/error@0.2.5
wasi:io/poll@0.2.5
wasi:io/streams@0.2.5
resource error
A resource which represents some error information.
16 |The only method provided by this resource is to-debug-string
,
17 | which provides some human-readable information about the error.
In the wasi:io
package, this resource is returned through the
19 | wasi:io/streams/stream-error
type.
To provide more specific error information, other interfaces may
21 | offer functions to "downcast" this error into more specific types. For example,
22 | errors returned from streams derived from filesystem types can be described using
23 | the filesystem's own error-code type. This is done using the function
24 | wasi:filesystem/types/filesystem-error-code
, which takes a borrow<error>
25 | parameter and returns an option<wasi:filesystem/types/error-code>
.
error
into a more
27 | concrete type is open.[method]error.to-debug-string: func
Returns a string that is suitable to assist humans in debugging 31 | this error.
32 |WARNING: The returned string should not be consumed mechanically! 33 | It may change across platforms, hosts, or other implementation 34 | details. Parsing this string is a major platform-compatibility 35 | hazard.
36 |self
: borrow<error
>A poll API intended to let users wait for I/O events on multiple handles 46 | at once.
47 |resource pollable
pollable
represents a single I/O event which may be ready, or not.[method]pollable.ready: func
Return the readiness of a pollable. This function never blocks.
54 |Returns true
when the pollable is ready, and false
otherwise.
self
: borrow<pollable
>[method]pollable.block: func
block
returns immediately if the pollable is ready, and otherwise
65 | blocks until ready.
This function is equivalent to calling poll.poll
on a list
67 | containing only this pollable.
self
: borrow<pollable
>poll: func
Poll for completion on a set of pollables.
74 |This function takes a list of pollables, which identify I/O sources of 75 | interest, and waits until one or more of the events is ready for I/O.
76 |The result list<u32>
contains one or more indices of handles in the
77 | argument list that is ready for I/O.
This function traps if either:
79 |u32
value.A timeout can be implemented by adding a pollable from the 84 | wasi-clocks API to the list.
85 |This function does not return a result
; polling in itself does not
86 | do any I/O so it doesn't fail. If any of the I/O sources identified by
87 | the pollables has an error, it is indicated by marking the source as
88 | being ready for I/O.
in
: list<borrow<pollable
>>WASI I/O is an I/O abstraction API which is currently focused on providing 99 | stream types.
100 |In the future, the component model is expected to add built-in stream types; 101 | when it does, they are expected to subsume this API.
102 |type error
107 | #### `type pollable` 108 | [`pollable`](#pollable) 109 |
110 | #### `variant stream-error` 111 |
An error for input-stream and output-stream operations.
112 |last-operation-failed
: own<error
>
The last operation (a write or flush) failed before completion. 117 |
More information is available in the error
payload.
After this, the stream will be closed. All future operations return
119 | stream-error::closed
.
The stream is closed: no more input will be accepted by the 124 | stream. A closed output-stream will return this error on all 125 | future operations. 126 |
resource input-stream
An input bytestream.
130 |input-stream
s are non-blocking to the extent practical on underlying
131 | platforms. I/O operations always return promptly; if fewer bytes are
132 | promptly available than requested, they return the number of bytes promptly
133 | available, which could even be zero. To wait for data to be available,
134 | use the subscribe
function to obtain a pollable
which can be polled
135 | for using wasi:io/poll
.
resource output-stream
An output bytestream.
138 |output-stream
s are non-blocking to the extent practical on
139 | underlying platforms. Except where specified otherwise, I/O operations also
140 | always return promptly, after the number of bytes that can be written
141 | promptly, which could even be zero. To wait for the stream to be ready to
142 | accept data, the subscribe
function to obtain a pollable
which can be
143 | polled for using wasi:io/poll
.
output-stream
while there's still an active write in
145 | progress may result in the data being lost. Before dropping the stream,
146 | be sure to fully flush your writes.[method]input-stream.read: func
Perform a non-blocking read from the stream.
150 |When the source of a read
is binary data, the bytes from the source
151 | are returned verbatim. When the source of a read
is known to the
152 | implementation to be text, bytes containing the UTF-8 encoding of the
153 | text are returned.
This function returns a list of bytes containing the read data,
155 | when successful. The returned list will contain up to len
bytes;
156 | it may return fewer than requested, but not more. The list is
157 | empty when no bytes are available for reading at this time. The
158 | pollable given by subscribe
will be ready when more bytes are
159 | available.
This function fails with a stream-error
when the operation
161 | encounters an error, giving last-operation-failed
, or when the
162 | stream is closed, giving closed
.
When the caller gives a len
of 0, it represents a request to
164 | read 0 bytes. If the stream is still open, this call should
165 | succeed and return an empty list, or otherwise fail with closed
.
The len
parameter is a u64
, which could represent a list of u8 which
167 | is not possible to allocate in wasm32, or not desirable to allocate as
168 | as a return value by the callee. The callee may return a list of bytes
169 | less than len
in size while more bytes are available for reading.
self
: borrow<input-stream
>len
: u64
u8
>, stream-error
>[method]input-stream.blocking-read: func
Read bytes from a stream, after blocking until at least one byte can
181 | be read. Except for blocking, behavior is identical to read
.
self
: borrow<input-stream
>len
: u64
u8
>, stream-error
>[method]input-stream.skip: func
Skip bytes from a stream. Returns number of bytes skipped.
193 |Behaves identical to read
, except instead of returning a list
194 | of bytes, returns the number of bytes consumed from the stream.
self
: borrow<input-stream
>len
: u64
u64
, stream-error
>[method]input-stream.blocking-skip: func
Skip bytes from a stream, after blocking until at least one byte
206 | can be skipped. Except for blocking behavior, identical to skip
.
self
: borrow<input-stream
>len
: u64
u64
, stream-error
>[method]input-stream.subscribe: func
Create a pollable
which will resolve once either the specified stream
218 | has bytes available to read or the other end of the stream has been
219 | closed.
220 | The created pollable
is a child resource of the input-stream
.
221 | Implementations may trap if the input-stream
is dropped before
222 | all derived pollable
s created with this function are dropped.
self
: borrow<input-stream
>pollable
>[method]output-stream.check-write: func
Check readiness for writing. This function never blocks.
233 |Returns the number of bytes permitted for the next call to write
,
234 | or an error. Calling write
with more bytes than this function has
235 | permitted will trap.
When this function returns 0 bytes, the subscribe
pollable will
237 | become ready when this function will report at least 1 byte, or an
238 | error.
self
: borrow<output-stream
>u64
, stream-error
>[method]output-stream.write: func
Perform a write. This function never blocks.
249 |When the destination of a write
is binary data, the bytes from
250 | contents
are written verbatim. When the destination of a write
is
251 | known to the implementation to be text, the bytes of contents
are
252 | transcoded from UTF-8 into the encoding of the destination and then
253 | written.
Precondition: check-write gave permit of Ok(n) and contents has a 255 | length of less than or equal to n. Otherwise, this function will trap.
256 |returns Err(closed) without writing if the stream has closed since 257 | the last call to check-write provided a permit.
258 |self
: borrow<output-stream
>contents
: list<u8
>stream-error
>[method]output-stream.blocking-write-and-flush: func
Perform a write of up to 4096 bytes, and then flush the stream. Block 269 | until all of these operations are complete, or an error occurs.
270 |This is a convenience wrapper around the use of check-write
,
271 | subscribe
, write
, and flush
, and is implemented with the
272 | following pseudo-code:
let pollable = this.subscribe();
274 | while !contents.is_empty() {
275 | // Wait for the stream to become writable
276 | pollable.block();
277 | let Ok(n) = this.check-write(); // eliding error handling
278 | let len = min(n, contents.len());
279 | let (chunk, rest) = contents.split_at(len);
280 | this.write(chunk ); // eliding error handling
281 | contents = rest;
282 | }
283 | this.flush();
284 | // Wait for completion of `flush`
285 | pollable.block();
286 | // Check for any errors that arose during `flush`
287 | let _ = this.check-write(); // eliding error handling
288 |
289 | self
: borrow<output-stream
>contents
: list<u8
>stream-error
>[method]output-stream.flush: func
Request to flush buffered output. This function never blocks.
300 |This tells the output-stream that the caller intends any buffered
301 | output to be flushed. the output which is expected to be flushed
302 | is all that has been passed to write
prior to this call.
Upon calling this function, the output-stream
will not accept any
304 | writes (check-write
will return ok(0)
) until the flush has
305 | completed. The subscribe
pollable will become ready when the
306 | flush has completed and the stream can accept more writes.
self
: borrow<output-stream
>stream-error
>[method]output-stream.blocking-flush: func
Request to flush buffered output, and block until flush completes 317 | and stream is ready for writing again.
318 |self
: borrow<output-stream
>stream-error
>[method]output-stream.subscribe: func
Create a pollable
which will resolve once the output-stream
328 | is ready for more writing, or an error has occurred. When this
329 | pollable is ready, check-write
will return ok(n)
with n>0, or an
330 | error.
If the stream is closed, this pollable is always ready immediately.
332 |The created pollable
is a child resource of the output-stream
.
333 | Implementations may trap if the output-stream
is dropped before
334 | all derived pollable
s created with this function are dropped.
self
: borrow<output-stream
>pollable
>[method]output-stream.write-zeroes: func
Write zeroes to a stream.
345 |This should be used precisely like write
with the exact same
346 | preconditions (must use check-write first), but instead of
347 | passing a list of bytes, you simply pass the number of zero-bytes
348 | that should be written.
self
: borrow<output-stream
>len
: u64
stream-error
>[method]output-stream.blocking-write-zeroes-and-flush: func
Perform a write of up to 4096 zeroes, and then flush the stream. 360 | Block until all of these operations are complete, or an error 361 | occurs.
362 |This is a convenience wrapper around the use of check-write
,
363 | subscribe
, write-zeroes
, and flush
, and is implemented with
364 | the following pseudo-code:
let pollable = this.subscribe();
366 | while num_zeroes != 0 {
367 | // Wait for the stream to become writable
368 | pollable.block();
369 | let Ok(n) = this.check-write(); // eliding error handling
370 | let len = min(n, num_zeroes);
371 | this.write-zeroes(len); // eliding error handling
372 | num_zeroes -= len;
373 | }
374 | this.flush();
375 | // Wait for completion of `flush`
376 | pollable.block();
377 | // Check for any errors that arose during `flush`
378 | let _ = this.check-write(); // eliding error handling
379 |
380 | self
: borrow<output-stream
>len
: u64
stream-error
>[method]output-stream.splice: func
Read from one stream and write to another.
391 |The behavior of splice is equivalent to:
392 |check-write
on the output-stream
read
on the input-stream
with the smaller of the
395 | check-write
permitted length and the len
provided to splice
write
on the output-stream
with that read data.Any error reported by the call to check-write
, read
, or
399 | write
ends the splice and reports that error.
This function returns the number of bytes transferred; it may be less
401 | than len
.
self
: borrow<output-stream
>src
: borrow<input-stream
>len
: u64
u64
, stream-error
>[method]output-stream.blocking-splice: func
Read from one stream and write to another, with blocking.
414 |This is similar to splice
, except that it blocks until the
415 | output-stream
is ready for writing, and the input-stream
416 | is ready for reading, before performing the splice
.
self
: borrow<output-stream
>src
: borrow<input-stream
>len
: u64
u64
, stream-error
>