diff --git a/host/src/default_outgoing_http.rs b/host/src/default_outgoing_http.rs new file mode 100644 index 00000000..3a3d7bcb --- /dev/null +++ b/host/src/default_outgoing_http.rs @@ -0,0 +1,16 @@ +use crate::{ + wasi, + wasi::types::{FutureIncomingResponse as Response, OutgoingRequest as Request, RequestOptions}, + WasiCtx, +}; + +#[async_trait::async_trait] +impl wasi::default_outgoing_http::Host for WasiCtx { + async fn handle( + &mut self, + _req: Request, + _options: Option, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } +} diff --git a/host/src/http.rs b/host/src/http.rs new file mode 100644 index 00000000..08e91519 --- /dev/null +++ b/host/src/http.rs @@ -0,0 +1,12 @@ +use crate::{ + wasi, + wasi::types::{IncomingRequest as Request, ResponseOutparam as Response}, + WasiCtx, +}; + +#[async_trait::async_trait] +impl wasi::http::Host for WasiCtx { + async fn handle(&mut self, _req: Request, _resp: Response) -> anyhow::Result<()> { + anyhow::bail!("not implemented") + } +} diff --git a/host/src/http_types.rs b/host/src/http_types.rs new file mode 100644 index 00000000..681bebb0 --- /dev/null +++ b/host/src/http_types.rs @@ -0,0 +1,203 @@ +use crate::{ + wasi, + wasi::poll::Pollable, + wasi::types::{ + Error, Fields, FutureIncomingResponse, Headers, IncomingRequest, IncomingResponse, + IncomingStream, Method, OutgoingRequest, OutgoingResponse, OutgoingStream, + ResponseOutparam, Scheme, StatusCode, Trailers, + }, + WasiCtx, +}; + +#[async_trait::async_trait] +impl wasi::types::Host for WasiCtx { + async fn drop_fields(&mut self, _fields: Fields) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn new_fields(&mut self, _entries: Vec<(String, String)>) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn fields_get( + &mut self, + _fields: Fields, + _name: String, + ) -> wasmtime::Result> { + anyhow::bail!("not implemented") + } + async fn fields_set( + &mut self, + _fields: Fields, + _name: String, + _value: Vec, + ) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn fields_delete(&mut self, _fields: Fields, _name: String) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn fields_append( + &mut self, + _fields: Fields, + _name: String, + _value: String, + ) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn fields_entries(&mut self, _fields: Fields) -> wasmtime::Result> { + anyhow::bail!("not implemented") + } + async fn fields_clone(&mut self, _fields: Fields) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn finish_incoming_stream( + &mut self, + _s: IncomingStream, + ) -> wasmtime::Result> { + anyhow::bail!("not implemented") + } + async fn finish_outgoing_stream( + &mut self, + _s: OutgoingStream, + _trailers: Option, + ) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn drop_incoming_request(&mut self, _request: IncomingRequest) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn drop_outgoing_request(&mut self, _request: OutgoingRequest) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn incoming_request_method( + &mut self, + _request: IncomingRequest, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn incoming_request_path( + &mut self, + _request: IncomingRequest, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn incoming_request_scheme( + &mut self, + _request: IncomingRequest, + ) -> wasmtime::Result> { + anyhow::bail!("not implemented") + } + async fn incoming_request_authority( + &mut self, + _request: IncomingRequest, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn incoming_request_headers( + &mut self, + _request: IncomingRequest, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn incoming_request_consume( + &mut self, + _request: IncomingRequest, + ) -> wasmtime::Result> { + anyhow::bail!("not implemented") + } + async fn incoming_request_query( + &mut self, + _request: IncomingRequest, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn new_outgoing_request( + &mut self, + _method: Method, + _path: String, + _query: String, + _scheme: Option, + _authority: String, + _headers: Headers, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn outgoing_request_write( + &mut self, + _request: OutgoingRequest, + ) -> wasmtime::Result> { + anyhow::bail!("not implemented") + } + async fn drop_response_outparam( + &mut self, + _response: ResponseOutparam, + ) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn set_response_outparam( + &mut self, + _response: Result, + ) -> wasmtime::Result> { + anyhow::bail!("not implemented") + } + async fn drop_incoming_response( + &mut self, + _response: IncomingResponse, + ) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn drop_outgoing_response( + &mut self, + _response: OutgoingResponse, + ) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn incoming_response_status( + &mut self, + _response: IncomingResponse, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn incoming_response_headers( + &mut self, + _response: IncomingResponse, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn incoming_response_consume( + &mut self, + _response: IncomingResponse, + ) -> wasmtime::Result> { + anyhow::bail!("not implemented") + } + async fn new_outgoing_response( + &mut self, + _status_code: StatusCode, + _headers: Headers, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } + async fn outgoing_response_write( + &mut self, + _response: OutgoingResponse, + ) -> wasmtime::Result> { + anyhow::bail!("not implemented") + } + async fn drop_future_incoming_response( + &mut self, + _f: FutureIncomingResponse, + ) -> wasmtime::Result<()> { + anyhow::bail!("not implemented") + } + async fn future_incoming_response_get( + &mut self, + _f: FutureIncomingResponse, + ) -> wasmtime::Result>> { + anyhow::bail!("not implemented") + } + async fn listen_to_future_incoming_response( + &mut self, + _f: FutureIncomingResponse, + ) -> wasmtime::Result { + anyhow::bail!("not implemented") + } +} diff --git a/host/src/lib.rs b/host/src/lib.rs index caa7b9c4..e95523d5 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -1,7 +1,10 @@ mod clocks; +mod default_outgoing_http; mod env; mod exit; mod filesystem; +mod http; +mod http_types; mod io; mod ip_name_lookup; mod network; @@ -45,5 +48,8 @@ pub fn add_to_linker( wasi::exit::add_to_linker(l, f)?; wasi::environment::add_to_linker(l, f)?; wasi::environment_preopens::add_to_linker(l, f)?; + wasi::types::add_to_linker(l, f)?; + wasi::default_outgoing_http::add_to_linker(l, f)?; + wasi::http::add_to_linker(l, f)?; Ok(()) } diff --git a/verify/src/main.rs b/verify/src/main.rs index 7241cfec..73bbc644 100644 --- a/verify/src/main.rs +++ b/verify/src/main.rs @@ -45,6 +45,9 @@ fn main() -> Result<()> { && i.module != "environment-preopens" && i.module != "exit" && i.module != "stderr" + && i.module != "types" + && i.module != "incoming-handler" + && i.module != "outgoing-handler" && i.module != "canonical_abi" && i.module != "__main_module__" { diff --git a/wasi/Cargo.toml b/wasi/Cargo.toml index 55d28162..644a6750 100644 --- a/wasi/Cargo.toml +++ b/wasi/Cargo.toml @@ -6,7 +6,7 @@ edition.workspace = true publish = false [dependencies] -wit-bindgen = { workspace = true, default-features = true } +wit-bindgen = { workspace = true, features = ["macros", "realloc"] } [lib] crate-type = ["lib"] diff --git a/wit/command.wit b/wit/command.wit index 2039f5cb..cbdc318e 100644 --- a/wit/command.wit +++ b/wit/command.wit @@ -15,6 +15,8 @@ default world command { import random: random.random import poll: poll.poll import streams: io.streams + import default-outgoing-HTTP: http.outgoing-handler + import HTTP: http.incoming-handler import environment: pkg.environment import environment-preopens: pkg.environment-preopens import exit: pkg.exit diff --git a/wit/deps/http/incoming-handler.wit b/wit/deps/http/incoming-handler.wit new file mode 100644 index 00000000..1ecff0aa --- /dev/null +++ b/wit/deps/http/incoming-handler.wit @@ -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 { + 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 + ) +} diff --git a/wit/deps/http/outgoing-handler.wit b/wit/deps/http/outgoing-handler.wit new file mode 100644 index 00000000..abe812ff --- /dev/null +++ b/wit/deps/http/outgoing-handler.wit @@ -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 + ) -> future-incoming-response +} diff --git a/wit/deps/http/types.wit b/wit/deps/http/types.wit new file mode 100644 index 00000000..bdcf7973 --- /dev/null +++ b/wit/deps/http/types.wit @@ -0,0 +1,157 @@ +// The `wasi:http/types` interface is meant to be imported by components to +// define the HTTP resource types and operations used by the component's +// imported and exported interfaces. +default interface types { + use io.streams.{input-stream, output-stream} + use poll.poll.{pollable} + + // This type corresponds to HTTP standard Methods. + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string) + } + + // This type corresponds to HTTP standard Related Schemes. + variant scheme { + HTTP, + HTTPS, + other(string) + } + + // TODO: perhaps better align with HTTP semantics? + // This type enumerates the different kinds of errors that may occur when + // initially returning a response. + variant error { + invalid-url(string), + timeout-error(string), + protocol-error(string), + unexpected-error(string) + } + + // This following block defines the `fields` resource which corresponds to + // HTTP standard Fields. Soon, when resource types are added, the `type + // fields = u32` type alias can be replaced by a proper `resource fields` + // definition containing all the functions using the method syntactic sugar. + type fields = u32 + drop-fields: func(fields: fields) + new-fields: func(entries: list>) -> fields + fields-get: func(fields: fields, name: string) -> list + fields-set: func(fields: fields, name: string, value: list) + fields-delete: func(fields: fields, name: string) + fields-append: func(fields: fields, name: string, value: string) + fields-entries: func(fields: fields) -> list> + fields-clone: func(fields: fields) -> fields + + type headers = fields + type trailers = fields + + // The following block defines stream types which corresponds to the HTTP + // standard Contents and Trailers. With Preview3, all of these fields can be + // replaced by a stream>. In the interim, we need to + // build on separate resource types defined by `wasi:io/streams`. The + // `finish-` functions emulate the stream's result value and MUST be called + // exactly once after the final read/write from/to the stream before dropping + // the stream. + type incoming-stream = input-stream + type outgoing-stream = output-stream + finish-incoming-stream: func(s: incoming-stream) -> option + finish-outgoing-stream: func(s: outgoing-stream, trailers: option) + + // The following block defines the `incoming-request` and `outgoing-request` + // resource types that correspond to HTTP standard Requests. Soon, when + // resource types are added, the `u32` type aliases can be replaced by + // proper `resource` type definitions containing all the functions as + // methods. Later, Preview2 will allow both types to be merged together into + // a single `request` type (that uses the single `stream` type mentioned + // above). The `consume` and `write` methods may only be called once (and + // return failure thereafter). + type incoming-request = u32 + type outgoing-request = u32 + drop-incoming-request: func(request: incoming-request) + drop-outgoing-request: func(request: outgoing-request) + incoming-request-method: func(request: incoming-request) -> method + incoming-request-path: func(request: incoming-request) -> string + incoming-request-query: func(request: incoming-request) -> string + incoming-request-scheme: func(request: incoming-request) -> option + incoming-request-authority: func(request: incoming-request) -> string + incoming-request-headers: func(request: incoming-request) -> headers + incoming-request-consume: func(request: incoming-request) -> result + new-outgoing-request: func( + method: method, + path: string, + query: string, + scheme: option, + authority: string, + headers: headers + ) -> outgoing-request + outgoing-request-write: func(request: outgoing-request) -> result + + // Additional optional parameters that can be set when making a request. + record request-options { + // The following timeouts are specific to the HTTP protocol and work + // independently of the overall timeouts passed to `io.poll.poll-oneoff`. + + // The timeout for the initial connect. + connect-timeout-ms: option, + + // The timeout for receiving the first byte of the response body. + first-byte-timeout-ms: option, + + // The timeout for receiving the next chunk of bytes in the response body + // stream. + between-bytes-timeout-ms: option + } + + // The following block defines a special resource type used by the + // `wasi:http/incoming-handler` interface. When resource types are added, this + // block can be replaced by a proper `resource response-outparam { ... }` + // definition. Later, with Preview3, the need for an outparam goes away entirely + // (the `wasi:http/handler` interface used for both incoming and outgoing can + // simply return a `stream`). + type response-outparam = u32 + drop-response-outparam: func(response: response-outparam) + set-response-outparam: func(response: result) -> result + + // This type corresponds to the HTTP standard Status Code. + type status-code = u16 + + // The following block defines the `incoming-response` and `outgoing-response` + // resource types that correspond to HTTP standard Responses. Soon, when + // resource types are added, the `u32` type aliases can be replaced by proper + // `resource` type definitions containing all the functions as methods. Later, + // Preview2 will allow both types to be merged together into a single `response` + // type (that uses the single `stream` type mentioned above). The `consume` and + // `write` methods may only be called once (and return failure thereafter). + type incoming-response = u32 + type outgoing-response = u32 + drop-incoming-response: func(response: incoming-response) + drop-outgoing-response: func(response: outgoing-response) + incoming-response-status: func(response: incoming-response) -> status-code + incoming-response-headers: func(response: incoming-response) -> headers + incoming-response-consume: func(response: incoming-response) -> result + new-outgoing-response: func( + status-code: status-code, + headers: headers + ) -> outgoing-response + outgoing-response-write: func(response: outgoing-response) -> result + + // The following block defines a special resource type used by the + // `wasi:http/outgoing-handler` interface to emulate + // `future>` in advance of Preview3. Given a + // `future-incoming-response`, the client can call the non-blocking `get` + // method to get the result if it is available. If the result is not available, + // the client can call `listen` to get a `pollable` that can be passed to + // `io.poll.poll-oneoff`. + type future-incoming-response = u32 + drop-future-incoming-response: func(f: future-incoming-response) + future-incoming-response-get: func(f: future-incoming-response) -> option> + listen-to-future-incoming-response: func(f: future-incoming-response) -> pollable +} diff --git a/wit/reactor.wit b/wit/reactor.wit index 065cc4c7..5ce3cfc5 100644 --- a/wit/reactor.wit +++ b/wit/reactor.wit @@ -15,6 +15,8 @@ default world reactor { import random: random.random import poll: poll.poll import streams: io.streams + import default-outgoing-HTTP: http.outgoing-handler + import HTTP: http.incoming-handler import environment: pkg.environment import environment-preopens: pkg.environment-preopens import exit: pkg.exit