Skip to content

add Wit interfaces #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 4, 2023
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
# WASI HTTP

(This is a placeholder so there can be a PR to fill in the contents.)
A proposed [WebAssembly System Interface](https://github.com/WebAssembly/WASI) API.

This proposal currently only contains the proposed Wit interfaces with light
explanation in comments; more work is necessary to fully document the proposal.
The Wit comments annotate where the proposed interface is expected to change in
the short term (for Preview2) once resources and handles are re-added to Wit,
and then after that (for Preview3) once native stream support is added to the
Component Model and Wit.

The `wit` directory currently validates and can generate bindings with:
```
wit-bindgen c wit/ --world proxy
```
or can be manipulated in other ways with:
```
wasm-tools component wit wit/ ...
```

The HTTP proposal depends on the WASI IO and Logging proposals. For simplicity,
the Wit files for these proposals are currently copied into the `wit/deps`
directory and will be updated periodically to match their respective proposals.
As the Wit tooling develops, we should be able to avoid this form of manual
vendoring.

### Current Phase

wasi-http is currently in [Phase 1](https://github.com/WebAssembly/WASI/blob/main/Proposals.md).

### Champions

Piotr Sikora, Jiaxiao Zhou, Dan Chiarlone, David Justice

### TODO

This readme needs to be expanded to cover a number of additional fields suggested in the
[WASI Proposal template](https://github.com/WebAssembly/wasi-proposal-template).
161 changes: 161 additions & 0 deletions wit/deps/io/streams.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/// WASI I/O is an I/O abstraction API which is currently focused on providing
/// stream types.
///
/// In the future, the component model is expected to add built-in stream types;
/// when it does, they are expected to subsume this API.
default interface streams {
use poll.poll.{pollable}

/// An error type returned from a stream operation. Currently this
/// doesn't provide any additional information.
record stream-error {}

/// An input bytestream. In the future, this will be replaced by handle
/// types.
///
/// This conceptually represents a `stream<u8, _>`. It's temporary
/// scaffolding until component-model's async features are ready.
///
/// `input-stream`s are *non-blocking* to the extent practical on underlying
/// platforms. I/O operations always return promptly; if fewer bytes are
/// promptly available than requested, they return the number of bytes promptly
/// available, which could even be zero. To wait for data to be available,
/// use the `subscribe-to-input-stream` function to obtain a `pollable` which
/// can be polled for using `wasi_poll`.
///
/// And at present, it is a `u32` instead of being an actual handle, until
/// the wit-bindgen implementation of handles and resources is ready.
///
/// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources).
type input-stream = u32

/// Read bytes from a stream.
///
/// This function returns a list of bytes containing the data that was
/// read, along with a bool indicating whether the end of the stream
/// was reached. The returned list will contain up to `len` bytes; it
/// may return fewer than requested, but not more.
///
/// Once a stream has reached the end, subsequent calls to read or
/// `skip` will always report end-of-stream rather than producing more
/// data.
///
/// If `len` is 0, it represents a request to read 0 bytes, which should
/// always succeed, assuming the stream hasn't reached its end yet, and
/// return an empty list.
///
/// The len here is a `u64`, but some callees may not be able to allocate
/// a buffer as large as that would imply.
/// FIXME: describe what happens if allocation fails.
read: func(
this: input-stream,
/// The maximum number of bytes to read
len: u64
) -> result<tuple<list<u8>, bool>, stream-error>

/// Skip bytes from a stream.
///
/// This is similar to the `read` function, but avoids copying the
/// bytes into the instance.
///
/// Once a stream has reached the end, subsequent calls to read or
/// `skip` will always report end-of-stream rather than producing more
/// data.
///
/// This function returns the number of bytes skipped, along with a bool
/// indicating whether the end of the stream was reached. The returned
/// value will be at most `len`; it may be less.
skip: func(
this: input-stream,
/// The maximum number of bytes to skip.
len: u64,
) -> result<tuple<u64, bool>, stream-error>

/// Create a `pollable` which will resolve once either the specified stream
/// has bytes available to read or the other end of the stream has been
/// closed.
subscribe-to-input-stream: func(this: input-stream) -> pollable

/// Dispose of the specified `input-stream`, after which it may no longer
/// be used.
drop-input-stream: func(this: input-stream)

/// An output bytestream. In the future, this will be replaced by handle
/// types.
///
/// This conceptually represents a `stream<u8, _>`. It's temporary
/// scaffolding until component-model's async features are ready.
///
/// `output-stream`s are *non-blocking* to the extent practical on
/// underlying platforms. Except where specified otherwise, I/O operations also
/// always return promptly, after the number of bytes that can be written
/// promptly, which could even be zero. To wait for the stream to be ready to
/// accept data, the `subscribe-to-output-stream` function to obtain a
/// `pollable` which can be polled for using `wasi_poll`.
///
/// And at present, it is a `u32` instead of being an actual handle, until
/// the wit-bindgen implementation of handles and resources is ready.
///
/// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources).
type output-stream = u32

/// Write bytes to a stream.
///
/// This function returns a `u64` indicating the number of bytes from
/// `buf` that were written; it may be less than the full list.
write: func(
this: output-stream,
/// Data to write
buf: list<u8>
) -> result<u64, stream-error>

/// Write multiple zero bytes to a stream.
///
/// This function returns a `u64` indicating the number of zero bytes
/// that were written; it may be less than `len`.
write-zeroes: func(
this: output-stream,
/// The number of zero bytes to write
len: u64
) -> result<u64, stream-error>

/// Read from one stream and write to another.
///
/// This function returns the number of bytes transferred; it may be less
/// than `len`.
///
/// Unlike other I/O functions, this function blocks until all the data
/// read from the input stream has been written to the output stream.
splice: func(
this: output-stream,
/// The stream to read from
src: input-stream,
/// The number of bytes to splice
len: u64,
) -> result<tuple<u64, bool>, stream-error>

/// Forward the entire contents of an input stream to an output stream.
///
/// This function repeatedly reads from the input stream and writes
/// the data to the output stream, until the end of the input stream
/// is reached, or an error is encountered.
///
/// Unlike other I/O functions, this function blocks until the end
/// of the input stream is seen and all the data has been written to
/// the output stream.
///
/// This function returns the number of bytes transferred.
forward: func(
this: output-stream,
/// The stream to read from
src: input-stream
) -> result<u64, stream-error>

/// Create a `pollable` which will resolve once either the specified stream
/// is ready to accept bytes or the other end of the stream has been closed.
subscribe-to-output-stream: func(this: output-stream) -> pollable

/// Dispose of the specified `output-stream`, after which it may no longer
/// be used.
drop-output-stream: func(this: output-stream)
}
32 changes: 32 additions & 0 deletions wit/deps/logging/handler.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/// WASI Logging is a logging API intended to let users emit log messages with
/// simple priority levels and context values.
default interface handler {
/// A log level, describing a kind of message.
enum level {
/// Describes messages about the values of variables and the flow of
/// control within a program.
trace,

/// Describes messages likely to be of interest to someone debugging a
/// program.
debug,

/// Describes messages likely to be of interest to someone monitoring a
/// program.
info,

/// Describes messages indicating hazardous situations.
warn,

/// Describes messages indicating serious errors.
error,
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not the right PR, but should we have a higher log level (e.g. critical for messages proceeding panic, etc.)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's right, wasi-logging would be the repo to file that under.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I filed WebAssembly/wasi-logging#9 to track this.


/// Emit a log message.
///
/// A log message has a `level` describing what kind of message is being
/// sent, a context, which is an uninterpreted string meant to help
/// consumers group similar messages, and a string containing the message
/// text.
log: func(level: level, context: string, message: string)
}
39 changes: 39 additions & 0 deletions wit/deps/poll/poll.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/// A poll API intended to let users wait for I/O events on multiple handles
/// at once.
default interface poll {
/// A "pollable" handle.
///
/// This is conceptually represents a `stream<_, _>`, or in other words,
/// a stream that one can wait on, repeatedly, but which does not itself
/// produce any data. It's temporary scaffolding until component-model's
/// async features are ready.
///
/// And at present, it is a `u32` instead of being an actual handle, until
/// the wit-bindgen implementation of handles and resources is ready.
///
/// `pollable` lifetimes are not automatically managed. Users must ensure
/// that they do not outlive the resource they reference.
///
/// This [represents a resource](https://github.com/WebAssembly/WASI/blob/main/docs/WitInWasi.md#Resources).
type pollable = u32

/// Dispose of the specified `pollable`, after which it may no longer
/// be used.
drop-pollable: func(this: pollable)

/// Poll for completion on a set of pollables.
///
/// The "oneoff" in the name refers to the fact that this function must do a
/// linear scan through the entire list of subscriptions, which may be
/// inefficient if the number is large and the same subscriptions are used
/// many times. In the future, this is expected to be obsoleted by the
/// component model async proposal, which will include a scalable waiting
/// facility.
///
/// Note that the return type would ideally be `list<bool>`, but that would
/// be more difficult to polyfill given the current state of `wit-bindgen`.
/// See <https://github.com/bytecodealliance/preview2-prototyping/pull/11#issuecomment-1329873061>
/// for details. For now, we use zero to mean "not ready" and non-zero to
/// mean "ready".
poll-oneoff: func(in: list<pollable>) -> list<u8>
}
42 changes: 42 additions & 0 deletions wit/deps/random/random.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/// WASI Random is a random data API.
///
/// It is intended to be portable at least between Unix-family platforms and
/// Windows.
default interface random {
/// Return `len` cryptographically-secure pseudo-random bytes.
///
/// This function must produce data from an adequately seeded
/// cryptographically-secure pseudo-random number generator (CSPRNG), so it
/// must not block, from the perspective of the calling program, and the
/// returned data is always unpredictable.
///
/// This function must always return fresh pseudo-random data. Deterministic
/// environments must omit this function, rather than implementing it with
/// deterministic data.
get-random-bytes: func(len: u64) -> list<u8>

/// Return a cryptographically-secure pseudo-random `u64` value.
///
/// This function returns the same type of pseudo-random data as
/// `get-random-bytes`, represented as a `u64`.
get-random-u64: func() -> u64

/// Return a 128-bit value that may contain a pseudo-random value.
///
/// The returned value is not required to be computed from a CSPRNG, and may
/// even be entirely deterministic. Host implementations are encouraged to
/// provide pseudo-random values to any program exposed to
/// attacker-controlled content, to enable DoS protection built into many
/// languages' hash-map implementations.
///
/// This function is intended to only be called once, by a source language
/// to initialize Denial Of Service (DoS) protection in its hash-map
/// implementation.
///
/// # Expected future evolution
///
/// This will likely be changed to a value import, to prevent it from being
/// called multiple times and potentially used for purposes other than DoS
/// protection.
insecure-random: func() -> tuple<u64, u64>
}
24 changes: 24 additions & 0 deletions wit/incoming-handler.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// The `wasi:http/incoming-handler` interface is meant to be exported by
// components and called by the host in response to a new incoming HTTP
// response.
//
// NOTE: in Preview3, this interface will be merged with
// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface
// that takes a `request` parameter and returns a `response` result.
//
default interface incoming-handler {
Copy link
Contributor

@brendandburns brendandburns Feb 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to split this out into separate PRs? I feel like it's going to be a heavy lift to do both client and server implementations in a single PR.

Of course, if it's ok to only have a partial implementation, that works too.

use pkg.types.{incoming-request, response-outparam}

// The `handle` function takes an outparam instead of returning its response
// so that the component may stream its response while streaming any other
// request or response bodies. The callee MUST write a response to the
// `response-out` and then finish the response before returning. The `handle`
// function is allowed to continue execution after finishing the response's
// output stream. While this post-response execution is taken off the
// critical path, since there is no return value, there is no way to report
// its success or failure.
handle: func(
request: incoming-request,
response-out: response-outparam
)
}
18 changes: 18 additions & 0 deletions wit/outgoing-handler.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// The `wasi:http/outgoing-handler` interface is meant to be imported by
// components and implemented by the host.
//
// NOTE: in Preview3, this interface will be merged with
// `wasi:http/outgoing-handler` into a single `wasi:http/handler` interface
// that takes a `request` parameter and returns a `response` result.
//
default interface outgoing-handler {
use pkg.types.{outgoing-request, request-options, future-incoming-response}

// The parameter and result types of the `handle` function allow the caller
// to concurrently stream the bodies of the outgoing request and the incoming
// response.
handle: func(
request: outgoing-request,
options: option<request-options>
) -> future-incoming-response
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find those pairings (incoming-request and outgoing-response, and outgoing-request and incoming-response) very non-intuitive and a bit redundant.

Could we consider the same prefix on the same connection, e.g. downstream-request and downstream-response, and upstream-request and upstream-response?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RFC 9110 suggests inbound and outbound as the preferred terms here, and defines upstream and downstream differently than you are using them: https://www.rfc-editor.org/rfc/rfc9110#name-intermediaries

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but the popular implementations (e.g. NGINX and Envoy) disagree with the RFC about upstream and downstream, and I'm not aware of anything that actually uses the RFC terms.

I considered suggesting inbound and outbound, but it's yet another term, and it's actually used in service mesh environment to describe traffic direction in a global sense, and not from the perspective of a local endpoint.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our experience at fastly was that upstream and downstream were so confusing internally, and to our customers, that we stopped using those terms throughout our api and docs.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What did you end up replacing it with? I'm not a fan of downstream / upstream myself (but mostly because the RFCs disagree with implementations), so I'm fine with other terms.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that upstream/downstream is extremely confusing in practice (as evidenced by my own misuse of it below). I saw incoming/outgoing in one of the other proposals and it seemed more clear when you think of it from the component author's perspective. That being said, note that all incoming-/outgoing- prefixes would go away in Preview3 when we get to merge all these resources/interfaces.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most programming languages have standardized on request/response without incoming/outgoing b/c the context is pretty obvious from whether you are implementing a server or making a client call and programmers don't really get confused about that.

I think we should delete incoming/outgoing or downstream/upstream and just stick to request/response with the context providing the direction.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is definitely the goal with Preview 3. However, in a Preview 2 timeframe, where we want to do concurrent streaming of requests/responses and have blocking poll_oneoff, the concrete signatures of the request/response resources have to be different depending on whether you are reading from them or writing to them, so we unfortunately can't wait and infer when the request or response hits a call boundary. I do believe it's possible to implement source-language libraries which are direction-agnostic (e.g., JS Response) in terms of these these directional resource types, though, so that this is abstracted from the developer (e.g., when constructing a Response, the impl would internally call new-outgoing-response...). Avoiding this extra work and complexity is one of the main motivations for adding future/stream (and the stack-switching required to implement these in the underlying implementation).

Loading