From 34cf1ef78905ff6491c9659a69b7331a99955bf3 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Wed, 29 May 2024 13:32:27 +0200 Subject: [PATCH 01/22] Use SpiderMonkey's encoding_c instead of our own crate SpiderMonkey uses the `encoding_c` Rust crate and comes bundled with it. This lead to either duplication or linking errors when using our own crate re-exporting `encoding_c`. Instead, with this PR we just bundle a C header file for interacting with the crate's functionality, and rely on SpiderMonkey's version. --- CMakeLists.txt | 2 +- cmake/build-crates.cmake | 3 -- crates/rust-encoding/.gitignore | 1 - crates/rust-encoding/Cargo.lock | 34 ------------ crates/rust-encoding/Cargo.toml | 17 ------ crates/rust-encoding/cbindgen.toml | 54 ------------------- crates/rust-encoding/src/lib.rs | 1 - .../include}/rust-encoding.h | 3 ++ 8 files changed, 4 insertions(+), 111 deletions(-) delete mode 100644 crates/rust-encoding/.gitignore delete mode 100644 crates/rust-encoding/Cargo.lock delete mode 100644 crates/rust-encoding/Cargo.toml delete mode 100644 crates/rust-encoding/cbindgen.toml delete mode 100644 crates/rust-encoding/src/lib.rs rename {crates/rust-encoding => deps/include}/rust-encoding.h (99%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9218480..0691f3d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,7 +37,7 @@ include("host_api") include("build-crates") add_library(extension_api INTERFACE include/extension-api.h runtime/encode.h) -target_link_libraries(extension_api INTERFACE rust-url rust-encoding spidermonkey) +target_link_libraries(extension_api INTERFACE rust-url spidermonkey) target_include_directories(extension_api INTERFACE include deps/include runtime) include("builtins") diff --git a/cmake/build-crates.cmake b/cmake/build-crates.cmake index 39a8645..6c8cc5d 100644 --- a/cmake/build-crates.cmake +++ b/cmake/build-crates.cmake @@ -1,5 +1,2 @@ corrosion_import_crate(MANIFEST_PATH ${CMAKE_CURRENT_SOURCE_DIR}/crates/rust-url/Cargo.toml NO_LINKER_OVERRIDE) set_property(TARGET rust-url PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/crates/rust-url/) - -corrosion_import_crate(MANIFEST_PATH ${CMAKE_CURRENT_SOURCE_DIR}/crates/rust-encoding/Cargo.toml NO_LINKER_OVERRIDE) -set_property(TARGET rust-encoding PROPERTY INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/crates/rust-encoding/) diff --git a/crates/rust-encoding/.gitignore b/crates/rust-encoding/.gitignore deleted file mode 100644 index eb5a316..0000000 --- a/crates/rust-encoding/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target diff --git a/crates/rust-encoding/Cargo.lock b/crates/rust-encoding/Cargo.lock deleted file mode 100644 index 8e0ce86..0000000 --- a/crates/rust-encoding/Cargo.lock +++ /dev/null @@ -1,34 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "encoding_c" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9af727805f3b0d79956bde5b35732669fb5c5d45a94893798e7b7e70cfbf9cc1" -dependencies = [ - "encoding_rs", -] - -[[package]] -name = "encoding_rs" -version = "0.8.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "rust-encoding" -version = "0.1.0" -dependencies = [ - "encoding_c", -] diff --git a/crates/rust-encoding/Cargo.toml b/crates/rust-encoding/Cargo.toml deleted file mode 100644 index 9419498..0000000 --- a/crates/rust-encoding/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "rust-encoding" -version = "0.1.0" -edition = "2018" - -[lib] -crate-type = ["staticlib"] - -[dependencies] -encoding_c = { version = "0.9.8", features = [] } - -[profile.release] -lto = true -panic = 'abort' - -[profile.dev] -panic = 'abort' diff --git a/crates/rust-encoding/cbindgen.toml b/crates/rust-encoding/cbindgen.toml deleted file mode 100644 index db25391..0000000 --- a/crates/rust-encoding/cbindgen.toml +++ /dev/null @@ -1,54 +0,0 @@ -language = "C++" - -header = """ -// The constructor created by cbindgen's `derive_constructor` causes this warning. -// Gecko's various uses of cbindgen silence it, so we do, too. -#ifdef __clang__ -# pragma GCC diagnostic ignored "-Wreturn-type-c-linkage" -#endif -""" - -pragma_once = true -include_guard = "rust_encoding_bindings_h" - -autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" - -include_version = false -namespace = "jsencoding" -using_namespaces = [] -sys_includes = [] -includes = [] -no_includes = false -after_includes = "" - -braces = "SameLine" -line_length = 100 -tab_width = 2 -documentation = true -documentation_style = "auto" -line_endings = "LF" - -usize_is_size_t = true - -[struct] -derive_constructor = true - - -[parse] -parse_deps = true -# include = [] -exclude = [] -clean = false -extra_bindings = ["encoding_c"] - - -[export] -# A list of additional items to always include in the generated bindings if they're -# found but otherwise don't appear to be used by the public API. -# -# default: [] -include = [ - "Decoder", - "Encoder", - "BIG5_ENCODING", -] \ No newline at end of file diff --git a/crates/rust-encoding/src/lib.rs b/crates/rust-encoding/src/lib.rs deleted file mode 100644 index 76bd65e..0000000 --- a/crates/rust-encoding/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub use encoding_c; diff --git a/crates/rust-encoding/rust-encoding.h b/deps/include/rust-encoding.h similarity index 99% rename from crates/rust-encoding/rust-encoding.h rename to deps/include/rust-encoding.h index 6f67cc9..70eeea6 100644 --- a/crates/rust-encoding/rust-encoding.h +++ b/deps/include/rust-encoding.h @@ -1,3 +1,6 @@ +// NOTE: This file was generated by cbindgen for the encoding_c crate. +// Since that crate is included in SpiderMonkey, we just include the header file here. +// TODO: look into whether we can bundle the header file with SpiderMonkey's includes. // The constructor created by cbindgen's `derive_constructor` causes this warning. // Gecko's various uses of cbindgen silence it, so we do, too. #ifdef __clang__ From 0d022837ad4fde232127f7b5253f6f00fb6f84b5 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Thu, 18 Apr 2024 13:38:42 +0200 Subject: [PATCH 02/22] Change WPT support to use a CMake option instead of an env var --- CMakeLists.txt | 3 ++- README.md | 4 ++-- tests/wpt-harness/wpt.cmake | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0691f3d..1aa0c15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,7 +42,8 @@ target_include_directories(extension_api INTERFACE include deps/include runtime) include("builtins") -if (DEFINED ENV{WPT}) +option(ENABLE_WPT "Enable WPT harness support" OFF) +if (ENABLE_WPT) include("tests/wpt-harness/wpt.cmake") endif() diff --git a/README.md b/README.md index 4613373..c0bde81 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,12 @@ StarlingMonkey includes a test runner for the [Web Platform Tests](https://web-p ### Requirements -The WPT runner requires `Node.js` to be installed, and during build configuration the environment variable `WPT` must be defined. +The WPT runner requires `Node.js` to be installed, and during build configuration the option `ENABLE_WPT:BOOL=ON` must be set. When running the test, `WPT_ROOT` must be set to the path of a checkout of the WPT suite at revision `1014eae5e66f8f334610d5a1521756f7a2fb769f`: ```bash -WPT=1 WPT_ROOT=[path to your WPT checkout] cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug +WPT_ROOT=[path to your WPT checkout] cmake -S . -B cmake-build-debug -DENABLE_WPT:BOOL=ON -DCMAKE_BUILD_TYPE=Debug cmake --build cmake-build-debug --parallel 8 --target wpt-runtime cd cmake-build-debug ctest --verbose # Note: some of the tests run fairly slowly in debug builds, so be patient diff --git a/tests/wpt-harness/wpt.cmake b/tests/wpt-harness/wpt.cmake index f0d2933..bfb541d 100644 --- a/tests/wpt-harness/wpt.cmake +++ b/tests/wpt-harness/wpt.cmake @@ -1,7 +1,8 @@ enable_testing() -add_builtin(wpt_builtins SRC "${CMAKE_CURRENT_LIST_DIR}/wpt_builtins.cpp") -target_include_directories(wpt_builtins PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/builtins/web/") +add_builtin(wpt_support + SRC "${CMAKE_CURRENT_LIST_DIR}/wpt_builtins.cpp" + INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/builtins/web/") if(NOT DEFINED ENV{WPT_ROOT}) message(FATAL_ERROR "WPT_ROOT environment variable is not set") From bfe3158c4f2487534cbbf7bc4c2e28cf6ed87f45 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Wed, 13 Mar 2024 01:02:58 +0100 Subject: [PATCH 03/22] WIP: rework headers --- builtins/web/fetch/fetch_event.cpp | 2 +- builtins/web/fetch/headers.cpp | 112 +----------------------- builtins/web/fetch/headers.h | 18 ++-- builtins/web/fetch/request-response.cpp | 2 +- builtins/web/fetch/request-response.h | 2 +- host-apis/wasi-0.2.0/host_api.cpp | 48 +++++----- include/host_api.h | 47 +++++++--- 7 files changed, 74 insertions(+), 157 deletions(-) diff --git a/builtins/web/fetch/fetch_event.cpp b/builtins/web/fetch/fetch_event.cpp index 8772464..8279b2e 100644 --- a/builtins/web/fetch/fetch_event.cpp +++ b/builtins/web/fetch/fetch_event.cpp @@ -168,7 +168,7 @@ bool start_response(JSContext *cx, JS::HandleObject response_obj, bool streaming auto incoming_response = static_cast(generic_response); auto status = incoming_response->status(); MOZ_RELEASE_ASSERT(!status.is_err(), "Incoming response must have a status code"); - auto headers = new host_api::HttpHeaders(*incoming_response->headers().unwrap()); + auto headers = incoming_response->headers().unwrap()->clone(); response = host_api::HttpOutgoingResponse::make(status.unwrap(), headers); auto *source_body = incoming_response->body().unwrap(); auto *dest_body = response->body().unwrap(); diff --git a/builtins/web/fetch/headers.cpp b/builtins/web/fetch/headers.cpp index 32f3d8f..87e6f2e 100644 --- a/builtins/web/fetch/headers.cpp +++ b/builtins/web/fetch/headers.cpp @@ -56,17 +56,6 @@ const char VALID_NAME_CHARS[128] = { return false; \ } -JSObject *get_backing_map(JSObject *self) { - MOZ_ASSERT(Headers::is_instance(self)); - return &JS::GetReservedSlot(self, static_cast(Headers::Slots::BackingMap)).toObject(); -} - -bool lazy_values(JSObject *self) { - MOZ_ASSERT(Headers::is_instance(self)); - return JS::GetReservedSlot(self, static_cast(Headers::Slots::HasLazyValues)) - .toBoolean(); -} - Handle *get_handle(JSObject *self) { MOZ_ASSERT(Headers::is_instance(self)); auto handle = @@ -291,46 +280,10 @@ bool retrieve_value_for_header_from_handle(JSContext *cx, JS::HandleObject self, return true; } -/** - * Ensures that a value for the given header is available to client code. - * - * The calling code must ensure that a header with the given name exists, but - * might not yet have been retrieved from the host, i.e., it might be a "lazy" - * value. - * - * The value is returned via the `values` outparam, but *only* if the Headers - * object has lazy values at all. This is to avoid the map lookup in those cases - * where none is necessary in this function, and the consumer wouldn't use the - * value anyway. - */ -bool ensure_value_for_header(JSContext *cx, JS::HandleObject self, JS::HandleValue normalized_name, - JS::MutableHandleValue values) { - if (!lazy_values(self)) - return true; - - JS::RootedObject map(cx, get_backing_map(self)); - if (!JS::MapGet(cx, map, normalized_name, values)) - return false; - - // Value isn't lazy, just return it. - if (!values.isNull()) - return true; - - return retrieve_value_for_header_from_handle(cx, self, normalized_name, values); -} - bool get_header_value_for_name(JSContext *cx, JS::HandleObject self, JS::HandleValue name, JS::MutableHandleValue rval, const char *fun_name) { NORMALIZE_NAME(name, fun_name) - if (!ensure_value_for_header(cx, self, normalized_name, rval)) { - return false; - } - - if (rval.isString()) { - return true; - } - JS::RootedObject map(cx, get_backing_map(self)); if (!JS::MapGet(cx, map, normalized_name, rval)) { return false; @@ -390,39 +343,6 @@ std::vector splitCookiesString(std::string_view cookiesString) return cookiesStrings; } -bool ensure_all_header_values_from_handle(JSContext *cx, JS::HandleObject self, - JS::HandleObject backing_map) { - if (!lazy_values(self)) - return true; - - JS::RootedValue iterable(cx); - if (!JS::MapKeys(cx, backing_map, &iterable)) - return false; - - JS::ForOfIterator it(cx); - if (!it.init(iterable)) - return false; - - JS::RootedValue name(cx); - JS::RootedValue v(cx); - while (true) { - bool done; - if (!it.next(&name, &done)) - return false; - - if (done) - break; - - if (!ensure_value_for_header(cx, self, name, &v)) - return false; - } - - JS_SetReservedSlot(self, static_cast(Headers::Slots::HasLazyValues), - JS::BooleanValue(false)); - - return true; -} - } // namespace bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::HandleValue name, @@ -430,12 +350,6 @@ bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::Hand NORMALIZE_NAME(name, fun_name) NORMALIZE_VALUE(value, fun_name) - // Ensure that any host-side values have been applied JS-side. - JS::RootedValue v(cx); - if (!ensure_value_for_header(cx, self, normalized_name, &v)) { - return false; - } - auto handle = get_handle(self); if (handle) { std::string_view name = name_chars; @@ -461,11 +375,6 @@ bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::Hand return append_header_value_to_map(cx, self, normalized_name, &normalized_value); } -bool Headers::delazify(JSContext *cx, JS::HandleObject headers) { - JS::RootedObject backing_map(cx, get_backing_map(headers)); - return ensure_all_header_values_from_handle(cx, headers, backing_map); -} - JSObject *Headers::create(JSContext *cx, JS::HandleObject self, host_api::HttpHeaders *handle, JS::HandleObject init_headers) { JS::RootedObject headers(cx, create(cx, self, handle)); @@ -767,26 +676,9 @@ bool Headers::init_class(JSContext *cx, JS::HandleObject global) { return JS_DefinePropertyById(cx, proto_obj, iteratorId, entries, 0); } -JSObject *Headers::create(JSContext *cx, JS::HandleObject self, host_api::HttpHeaders *handle) { +JSObject *Headers::create(JSContext *cx, JS::HandleObject self, + host_api::HttpHeadersReadOnly *handle) { JS_SetReservedSlot(self, static_cast(Slots::Handle), JS::PrivateValue(handle)); - - JS::RootedObject backing_map(cx, JS::NewMapObject(cx)); - if (!backing_map) { - return nullptr; - } - JS::SetReservedSlot(self, static_cast(Slots::BackingMap), - JS::ObjectValue(*backing_map)); - - bool lazy = false; - if (handle) { - lazy = true; - if (!get_header_names_from_handle(cx, handle, backing_map)) { - return nullptr; - } - } - - JS_SetReservedSlot(self, static_cast(Slots::HasLazyValues), JS::BooleanValue(lazy)); - return self; } diff --git a/builtins/web/fetch/headers.h b/builtins/web/fetch/headers.h index 8bfdf69..2b61a86 100644 --- a/builtins/web/fetch/headers.h +++ b/builtins/web/fetch/headers.h @@ -23,14 +23,10 @@ class Headers final : public BuiltinImpl { static constexpr const char *class_name = "Headers"; enum class Slots { - BackingMap, Handle, - HasLazyValues, Count, }; - static bool delazify(JSContext *cx, JS::HandleObject headers); - /** * Adds the given header name/value to `self`'s list of headers iff `self` * doesn't already contain a header with that name. @@ -55,14 +51,14 @@ class Headers final : public BuiltinImpl { static const unsigned ctor_length = 1; - static bool init_class(JSContext *cx, JS::HandleObject global); - static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); + static bool init_class(JSContext *cx, HandleObject global); + static bool constructor(JSContext *cx, unsigned argc, Value *vp); - static JSObject *create(JSContext *cx, JS::HandleObject headers, host_api::HttpHeaders *handle, - JS::HandleObject init_headers); - static JSObject *create(JSContext *cx, JS::HandleObject headers, host_api::HttpHeaders *handle, - JS::HandleValue initv); - static JSObject *create(JSContext *cx, JS::HandleObject self, host_api::HttpHeaders *handle); + static JSObject *create(JSContext *cx, HandleObject self, host_api::HttpHeaders *handle, + HandleObject init_headers); + static JSObject *create(JSContext *cx, HandleObject self, host_api::HttpHeaders *handle, + HandleValue init_headers); + static JSObject *create(JSContext *cx, HandleObject self, host_api::HttpHeadersReadOnly *handle); }; } // namespace fetch diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index 36c66df..48b0e34 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -227,7 +227,7 @@ bool RequestOrResponse::is_instance(JSObject *obj) { bool RequestOrResponse::is_incoming(JSObject *obj) { return handle(obj)->is_incoming(); } -host_api::HttpHeaders *RequestOrResponse::headers_handle(JSObject *obj) { +host_api::HttpHeadersReadOnly *RequestOrResponse::headers_handle(JSObject *obj) { MOZ_ASSERT(is_instance(obj)); auto res = handle(obj)->headers(); MOZ_ASSERT(!res.is_err(), "TODO: proper error handling"); diff --git a/builtins/web/fetch/request-response.h b/builtins/web/fetch/request-response.h index e1b2f1d..be2f4fb 100644 --- a/builtins/web/fetch/request-response.h +++ b/builtins/web/fetch/request-response.h @@ -31,7 +31,7 @@ class RequestOrResponse final { static bool is_instance(JSObject *obj); static bool is_incoming(JSObject *obj); static host_api::HttpRequestResponseBase *handle(JSObject *obj); - static host_api::HttpHeaders *headers_handle(JSObject *obj); + static host_api::HttpHeadersReadOnly *headers_handle(JSObject *obj); static bool has_body(JSObject *obj); static host_api::HttpIncomingBody *incoming_body_handle(JSObject *obj); static host_api::HttpOutgoingBody *outgoing_body_handle(JSObject *obj); diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index 39b4f61..1d3427b 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -185,13 +185,13 @@ void MonotonicClock::unsubscribe(const int32_t handle_id) { wasi_io_0_2_0_poll_pollable_drop_own(own_pollable_t{handle_id}); } -HttpHeaders::HttpHeaders() { +HttpHeaders::HttpHeaders(Handle handle) : HttpHeadersReadOnly(handle) {} + +HttpHeaders::HttpHeaders() : HttpHeadersReadOnly() { this->handle_state_ = new HandleState(wasi_http_0_2_0_types_constructor_fields().__handle); } -HttpHeaders::HttpHeaders(Handle handle) { handle_state_ = new HandleState(handle); } -// TODO: make this a factory function -HttpHeaders::HttpHeaders(const vector>> &entries) { +Result HttpHeaders::FromEntries(const vector>> &entries) { std::vector pairs; for (const auto &[name, values] : entries) { @@ -207,16 +207,16 @@ HttpHeaders::HttpHeaders(const vector>> & wasi_http_0_2_0_types_static_fields_from_list(&tuples, &ret, &err); // TODO: handle `err` - this->handle_state_ = new HandleState(ret.__handle); + return Result::ok(new HttpHeaders(ret.__handle)); } -HttpHeaders::HttpHeaders(const HttpHeaders &headers) { +HttpHeaders::HttpHeaders(const HttpHeadersReadOnly &headers) : HttpHeadersReadOnly() { Borrow borrow(headers.handle_state_); auto handle = wasi_http_0_2_0_types_method_fields_clone(borrow); this->handle_state_ = new HandleState(handle.__handle); } -Result>> HttpHeaders::entries() const { +Result>> HttpHeadersReadOnly::entries() const { Result>> res; MOZ_ASSERT(valid()); @@ -237,7 +237,7 @@ Result>> HttpHeaders::entries() const { return res; } -Result> HttpHeaders::names() const { +Result> HttpHeadersReadOnly::names() const { Result> res; MOZ_ASSERT(valid()); @@ -256,7 +256,7 @@ Result> HttpHeaders::names() const { return res; } -Result>> HttpHeaders::get(string_view name) const { +Result>> HttpHeadersReadOnly::get(string_view name) const { Result>> res; MOZ_ASSERT(valid()); @@ -720,10 +720,10 @@ Result HttpOutgoingRequest::method() { return Result::ok(method_); } -Result HttpOutgoingRequest::headers() { +Result HttpOutgoingRequest::headers() { MOZ_ASSERT(valid()); MOZ_ASSERT(headers_); - return Result::ok(headers_); + return Result::ok(headers_); } Result HttpOutgoingRequest::body() { @@ -838,6 +838,10 @@ void FutureHttpIncomingResponse::unsubscribe() { // TODO: implement } +HttpHeadersReadOnly::HttpHeadersReadOnly(Handle handle) { + handle_state_ = new HandleState(handle); +} + Result HttpIncomingResponse::status() { if (status_ == UNSET_STATUS) { if (!valid()) { @@ -853,17 +857,17 @@ HttpIncomingResponse::HttpIncomingResponse(Handle handle) { handle_state_ = new HandleState(handle); } -Result HttpIncomingResponse::headers() { +Result HttpIncomingResponse::headers() { if (!headers_) { if (!valid()) { - return Result::err(154); + return Result::err(154); } auto res = wasi_http_0_2_0_types_method_incoming_response_headers( wasi_http_0_2_0_types_borrow_incoming_response({handle_state_->handle})); - headers_ = new HttpHeaders(res.__handle); + headers_ = new HttpHeadersReadOnly(res.__handle); } - return Result::ok(headers_); + return Result::ok(headers_); } Result HttpIncomingResponse::body() { @@ -907,11 +911,11 @@ HttpOutgoingResponse *HttpOutgoingResponse::make(const uint16_t status, HttpHead return resp; } -Result HttpOutgoingResponse::headers() { +Result HttpOutgoingResponse::headers() { if (!valid()) { - return Result::err(154); + return Result::err(154); } - return Result::ok(headers_); + return Result::ok(headers_); } Result HttpOutgoingResponse::body() { @@ -963,17 +967,17 @@ Result HttpIncomingRequest::method() { return Result::ok(method_); } -Result HttpIncomingRequest::headers() { +Result HttpIncomingRequest::headers() { if (!headers_) { if (!valid()) { - return Result::err(154); + return Result::err(154); } borrow_incoming_request_t borrow(handle_state_->handle); auto res = wasi_http_0_2_0_types_method_incoming_request_headers(borrow); - headers_ = new HttpHeaders(res.__handle); + headers_ = new HttpHeadersReadOnly(res.__handle); } - return Result::ok(headers_); + return Result::ok(headers_); } Result HttpIncomingRequest::body() { diff --git a/include/host_api.h b/include/host_api.h index 4e8abba..f063264 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -310,6 +310,8 @@ class HttpBodyPipe { }; class HttpIncomingResponse; +class HttpHeaders; + class FutureHttpIncomingResponse final : public Pollable { public: FutureHttpIncomingResponse() = delete; @@ -322,21 +324,44 @@ class FutureHttpIncomingResponse final : public Pollable { void unsubscribe() override; }; -class HttpHeaders final : public Resource { +class HttpHeadersReadOnly : public Resource { friend HttpIncomingResponse; friend HttpIncomingRequest; friend HttpOutgoingResponse; friend HttpOutgoingRequest; + friend HttpHeaders; + +protected: + explicit HttpHeadersReadOnly(Handle handle); + HttpHeadersReadOnly() = default; public: - HttpHeaders(); - explicit HttpHeaders(Handle handle); - explicit HttpHeaders(const vector>> &entries); - HttpHeaders(const HttpHeaders &headers); + HttpHeadersReadOnly(const HttpHeadersReadOnly &headers) = delete; + + HttpHeaders* clone(); + + virtual bool is_writable() { return false; }; Result>> entries() const; Result> names() const; Result>> get(string_view name) const; +}; + +class HttpHeaders final : public HttpHeadersReadOnly { + friend HttpIncomingResponse; + friend HttpIncomingRequest; + friend HttpOutgoingResponse; + friend HttpOutgoingRequest; + + explicit HttpHeaders(Handle handle); + +public: + HttpHeaders(); + explicit HttpHeaders(const HttpHeadersReadOnly &headers); + + static Result FromEntries(const vector>> &entries); + + bool is_writable() override { return true; }; Result set(string_view name, string_view value); Result append(string_view name, string_view value); @@ -345,13 +370,13 @@ class HttpHeaders final : public Resource { class HttpRequestResponseBase : public Resource { protected: - HttpHeaders *headers_ = nullptr; + HttpHeadersReadOnly *headers_ = nullptr; std::string *_url = nullptr; public: ~HttpRequestResponseBase() override = default; - virtual Result headers() = 0; + virtual Result headers() = 0; virtual string_view url(); virtual bool is_incoming() = 0; @@ -400,7 +425,7 @@ class HttpIncomingRequest final : public HttpRequest, public HttpIncomingBodyOwn bool is_request() override { return true; } [[nodiscard]] Result method() override; - Result headers() override; + Result headers() override; Result body() override; }; @@ -417,7 +442,7 @@ class HttpOutgoingRequest final : public HttpRequest, public HttpOutgoingBodyOwn bool is_request() override { return true; } [[nodiscard]] Result method() override; - Result headers() override; + Result headers() override; Result body() override; Result send(); @@ -440,7 +465,7 @@ class HttpIncomingResponse final : public HttpResponse, public HttpIncomingBodyO bool is_incoming() override { return true; } bool is_request() override { return false; } - Result headers() override; + Result headers() override; Result body() override; [[nodiscard]] Result status() override; }; @@ -458,7 +483,7 @@ class HttpOutgoingResponse final : public HttpResponse, public HttpOutgoingBodyO bool is_incoming() override { return false; } bool is_request() override { return false; } - Result headers() override; + Result headers() override; Result body() override; [[nodiscard]] Result status() override; From 86db17e9fadce9db613b74aada5916d002cac0da Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Fri, 22 Mar 2024 16:34:39 +0100 Subject: [PATCH 04/22] WIP: rework headers --- CMakeLists.txt | 3 +- builtins/web/fetch/headers.cpp | 483 ++++++++++++++---------------- builtins/web/fetch/headers.h | 38 ++- builtins/web/url.cpp | 2 +- crates/rust-url/rust-url.h | 11 + host-apis/wasi-0.2.0/host_api.cpp | 12 + include/host_api.h | 1 + runtime/decode.cpp | 10 + runtime/decode.h | 12 + 9 files changed, 301 insertions(+), 271 deletions(-) create mode 100644 runtime/decode.cpp create mode 100644 runtime/decode.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1aa0c15..b143611 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,7 +36,7 @@ include("openssl") include("host_api") include("build-crates") -add_library(extension_api INTERFACE include/extension-api.h runtime/encode.h) +add_library(extension_api INTERFACE include/extension-api.h runtime/encode.h runtime/decode.h) target_link_libraries(extension_api INTERFACE rust-url spidermonkey) target_include_directories(extension_api INTERFACE include deps/include runtime) @@ -51,6 +51,7 @@ add_executable(starling.wasm runtime/js.cpp runtime/allocator.cpp runtime/encode.cpp + runtime/decode.cpp runtime/engine.cpp runtime/event_loop.cpp runtime/builtin.cpp diff --git a/builtins/web/fetch/headers.cpp b/builtins/web/fetch/headers.cpp index 87e6f2e..83ab631 100644 --- a/builtins/web/fetch/headers.cpp +++ b/builtins/web/fetch/headers.cpp @@ -1,22 +1,20 @@ #include "headers.h" // #include "request-response.h" #include "encode.h" +#include "decode.h" #include "sequence.hpp" #include "js/Conversions.h" -namespace builtins { -namespace web { -namespace fetch { - +namespace builtins::web::fetch { namespace { using Handle = host_api::HttpHeaders; #define HEADERS_ITERATION_METHOD(argc) \ METHOD_HEADER(argc) \ - JS::RootedObject backing_map(cx, get_backing_map(self)); \ - if (!ensure_all_header_values_from_handle(cx, self, backing_map)) { \ + JS::RootedObject entries(cx, get_entries(cx, self)); \ + if (!entries) { \ return false; \ } @@ -43,16 +41,16 @@ const char VALID_NAME_CHARS[128] = { }; #define NORMALIZE_NAME(name, fun_name) \ - JS::RootedValue normalized_name(cx, name); \ - auto name_chars = normalize_header_name(cx, &normalized_name, fun_name); \ + bool name_changed; \ + auto name_chars = normalize_header_name(cx, name, &name_changed, fun_name); \ if (!name_chars) { \ return false; \ } #define NORMALIZE_VALUE(value, fun_name) \ - JS::RootedValue normalized_value(cx, value); \ - auto value_chars = normalize_header_value(cx, &normalized_value, fun_name); \ - if (!value_chars) { \ + bool value_changed; \ + auto value_chars = normalize_header_value(cx, value, &value_changed, fun_name); \ + if (!value_chars.ptr) { \ return false; \ } @@ -71,14 +69,10 @@ Handle *get_handle(JSObject *self) { * See * https://searchfox.org/mozilla-central/rev/9f76a47f4aa935b49754c5608a1c8e72ee358c46/netwerk/protocol/http/nsHttp.cpp#172-215 * For details on validation. - * - * Mutates `name_val` in place, and returns the name as UniqueChars. - * This is done because most uses of header names require handling of both the - * JSString and the char* version, so they'd otherwise have to recreate one of - * the two. */ -host_api::HostString normalize_header_name(JSContext *cx, JS::MutableHandleValue name_val, +host_api::HostString normalize_header_name(JSContext *cx, HandleValue name_val, bool* named_changed, const char *fun_name) { + *named_changed = false; JS::RootedString name_str(cx, JS::ToString(cx, name_val)); if (!name_str) { return nullptr; @@ -94,42 +88,42 @@ host_api::HostString normalize_header_name(JSContext *cx, JS::MutableHandleValue return nullptr; } - bool changed = false; - char *name_chars = name.begin(); for (size_t i = 0; i < name.len; i++) { - unsigned char ch = name_chars[i]; + const unsigned char ch = name_chars[i]; if (ch > 127 || !VALID_NAME_CHARS[ch]) { JS_ReportErrorUTF8(cx, "%s: Invalid header name '%s'", fun_name, name_chars); return nullptr; } if (ch >= 'A' && ch <= 'Z') { + *named_changed = true; name_chars[i] = ch - 'A' + 'a'; - changed = true; - } - } - - if (changed) { - name_str = JS_NewStringCopyN(cx, name_chars, name.len); - if (!name_str) { - return nullptr; } } - name_val.setString(name_str); return name; } -host_api::HostString normalize_header_value(JSContext *cx, JS::MutableHandleValue value_val, - const char *fun_name) { +/** + * Validates and normalizes the given header value, by + * - stripping leading and trailing whitespace + * - checking for interior line breaks and `\0` + * + * See + * https://searchfox.org/mozilla-central/rev/9f76a47f4aa935b49754c5608a1c8e72ee358c46/netwerk/protocol/http/nsHttp.cpp#247-260 + * For details on validation. + */ +host_api::HostString normalize_header_value(JSContext *cx, HandleValue value_val, + bool* value_changed, const char *fun_name) { + *value_changed = false; JS::RootedString value_str(cx, JS::ToString(cx, value_val)); if (!value_str) { return nullptr; } auto value = core::encode(cx, value_str); - if (!value) { + if (!value.ptr) { return nullptr; } @@ -137,11 +131,6 @@ host_api::HostString normalize_header_value(JSContext *cx, JS::MutableHandleValu size_t start = 0; size_t end = value.len; - // We follow Gecko's interpretation of what's a valid header value. After - // stripping leading and trailing whitespace, all interior line breaks and - // `\0` are considered invalid. See - // https://searchfox.org/mozilla-central/rev/9f76a47f4aa935b49754c5608a1c8e72ee358c46/netwerk/protocol/http/nsHttp.cpp#247-260 - // for details. while (start < end) { unsigned char ch = value_chars[start]; if (ch == '\t' || ch == ' ' || ch == '\r' || ch == '\n') { @@ -160,6 +149,10 @@ host_api::HostString normalize_header_value(JSContext *cx, JS::MutableHandleValu } } + if (start != 0 || end != value.len) { + *value_changed = true; + } + for (size_t i = start; i < end; i++) { unsigned char ch = value_chars[i]; if (ch == '\r' || ch == '\n' || ch == '\0') { @@ -168,91 +161,16 @@ host_api::HostString normalize_header_value(JSContext *cx, JS::MutableHandleValu } } - if (start != 0 || end != value.len) { - value_str = JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(value_chars + start, end - start)); - if (!value_str) { - return nullptr; - } - } - - value_val.setString(value_str); - return value; } JS::PersistentRooted comma; -// Append an already normalized value for an already normalized header name -// to the JS side map, but not the host. -// -// Returns the resulting combined value in `normalized_value`. -bool append_header_value_to_map(JSContext *cx, JS::HandleObject self, - JS::HandleValue normalized_name, - JS::MutableHandleValue normalized_value) { - JS::RootedValue existing(cx); - JS::RootedObject map(cx, get_backing_map(self)); - if (!JS::MapGet(cx, map, normalized_name, &existing)) - return false; - - // Existing value must only be null if we're in the process if applying - // header values from a handle. - if (!existing.isNullOrUndefined()) { - if (!comma.get()) { - comma.init(cx, JS_NewStringCopyN(cx, ", ", 2)); - if (!comma) { - return false; - } - } - - JS::RootedString str(cx, existing.toString()); - str = JS_ConcatStrings(cx, str, comma); - if (!str) { - return false; - } - - JS::RootedString val_str(cx, normalized_value.toString()); - str = JS_ConcatStrings(cx, str, val_str); - if (!str) { - return false; - } - - normalized_value.setString(str); - } - - return JS::MapSet(cx, map, normalized_name, normalized_value); -} - -bool get_header_names_from_handle(JSContext *cx, Handle *handle, JS::HandleObject backing_map) { - - auto names = handle->names(); - if (auto *err = names.to_err()) { - HANDLE_ERROR(cx, *err); - return false; - } - - JS::RootedString name(cx); - JS::RootedValue name_val(cx); - for (auto &str : names.unwrap()) { - // TODO: can `name` take ownership of the buffer here instead? - name = JS_NewStringCopyN(cx, str.ptr.get(), str.len); - if (!name) { - return false; - } - - name_val.setString(name); - JS::MapSet(cx, backing_map, name_val, JS::NullHandleValue); - } - - return true; -} - bool retrieve_value_for_header_from_handle(JSContext *cx, JS::HandleObject self, - JS::HandleValue name, JS::MutableHandleValue value) { + const host_api::HostString &name, + MutableHandleValue value) { auto handle = get_handle(self); - - JS::RootedString name_str(cx, name.toString()); - auto name_chars = core::encode(cx, name_str); - auto ret = handle->get(name_chars); + auto ret = handle->get(name); if (auto *err = ret.to_err()) { HANDLE_ERROR(cx, *err); @@ -261,39 +179,33 @@ bool retrieve_value_for_header_from_handle(JSContext *cx, JS::HandleObject self, auto &values = ret.unwrap(); if (!values.has_value()) { + value.setNull(); return true; } - JS::RootedString val_str(cx); + RootedString res_str(cx); + RootedString val_str(cx); for (auto &str : values.value()) { val_str = JS_NewStringCopyUTF8N(cx, JS::UTF8Chars(str.ptr.get(), str.len)); if (!val_str) { return false; } - value.setString(val_str); - if (!append_header_value_to_map(cx, self, name, value)) { - return false; + if (!res_str) { + res_str = val_str; + } else { + res_str = JS_ConcatStrings(cx, res_str, comma); + if (!res_str) { + return false; + } + res_str = JS_ConcatStrings(cx, res_str, val_str); + if (!res_str) { + return false; + } } } - return true; -} - -bool get_header_value_for_name(JSContext *cx, JS::HandleObject self, JS::HandleValue name, - JS::MutableHandleValue rval, const char *fun_name) { - NORMALIZE_NAME(name, fun_name) - - JS::RootedObject map(cx, get_backing_map(self)); - if (!JS::MapGet(cx, map, normalized_name, rval)) { - return false; - } - - // Return `null` for non-existent headers. - if (rval.isUndefined()) { - rval.setNull(); - } - + value.setString(res_str); return true; } @@ -311,7 +223,7 @@ std::vector splitCookiesString(std::string_view cookiesString) start = currentPosition; // Iterate until we find a comma that might be used as a separator. - while ((currentPosition = cookiesString.find_first_of(",", currentPosition)) != + while ((currentPosition = cookiesString.find_first_of(',', currentPosition)) != std::string_view::npos) { // ',' is a cookie separator only if we later have '=', before having ';' or ',' lastComma = currentPosition; @@ -345,6 +257,11 @@ std::vector splitCookiesString(std::string_view cookiesString) } // namespace +void switch_to_content_only_mode(JSObject* self) { + SetReservedSlot(self, static_cast(Headers::Slots::Mode), + JS::Int32Value(static_cast(Headers::Mode::ContentOnly))); +} + bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::HandleValue name, JS::HandleValue value, const char *fun_name) { NORMALIZE_NAME(name, fun_name) @@ -354,17 +271,15 @@ bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::Hand if (handle) { std::string_view name = name_chars; if (name == "set-cookie") { - std::string_view value = value_chars; - for (auto value : splitCookiesString(value)) { - auto res = handle->append(name, value); + for (auto value : splitCookiesString(value_chars)) { + auto res = handle->append(name_chars, value); if (auto *err = res.to_err()) { HANDLE_ERROR(cx, *err); return false; } } } else { - std::string_view value = value_chars; - auto res = handle->append(name, value); + auto res = handle->append(name_chars, value_chars); if (auto *err = res.to_err()) { HANDLE_ERROR(cx, *err); return false; @@ -372,65 +287,12 @@ bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::Hand } } - return append_header_value_to_map(cx, self, normalized_name, &normalized_value); -} - -JSObject *Headers::create(JSContext *cx, JS::HandleObject self, host_api::HttpHeaders *handle, - JS::HandleObject init_headers) { - JS::RootedObject headers(cx, create(cx, self, handle)); - if (!headers) { - return nullptr; - } - - if (!init_headers) { - return headers; - } - - if (!Headers::delazify(cx, init_headers)) { - return nullptr; - } - - JS::RootedObject headers_map(cx, get_backing_map(headers)); - JS::RootedObject init_map(cx, get_backing_map(init_headers)); - - JS::RootedValue iterable(cx); - if (!JS::MapEntries(cx, init_map, &iterable)) { - return nullptr; - } - - JS::ForOfIterator it(cx); - if (!it.init(iterable)) { - return nullptr; - } - - JS::RootedObject entry(cx); - JS::RootedValue entry_val(cx); - JS::RootedValue name_val(cx); - JS::RootedValue value_val(cx); - while (true) { - bool done; - if (!it.next(&entry_val, &done)) { - return nullptr; - } - - if (done) { - break; - } - - entry = &entry_val.toObject(); - JS_GetElement(cx, entry, 0, &name_val); - JS_GetElement(cx, entry, 1, &value_val); - - if (!Headers::append_header_value(cx, headers, name_val, value_val, "Headers constructor")) { - return nullptr; - } - } - - return headers; + return true; } -JSObject *Headers::create(JSContext *cx, JS::HandleObject self, host_api::HttpHeaders *handle, +JSObject *Headers::create(JSContext *cx, JS::HandleObject self, Handle *handle, JS::HandleValue initv) { + // TODO: check if initv is a Headers instance and clone its handle if so. JS::RootedObject headers(cx, create(cx, self, handle)); if (!headers) return nullptr; @@ -449,12 +311,42 @@ JSObject *Headers::create(JSContext *cx, JS::HandleObject self, host_api::HttpHe return headers; } +bool redecode_str_if_changed(JSContext* cx, HandleValue str_val, host_api::HostString& chars, + bool changed, MutableHandleValue rval) { + if (!changed) { + rval.set(str_val); + return true; + } + + RootedString str(cx, core::decode(cx, chars)); + if (!str) { + return false; + } + + rval.setString(str); + return true; +} + bool Headers::get(JSContext *cx, unsigned argc, JS::Value *vp) { METHOD_HEADER(1) NORMALIZE_NAME(args[0], "Headers.get") - return get_header_value_for_name(cx, self, normalized_name, args.rval(), "Headers.get"); + Mode mode = Headers::mode(self); + if (mode == Mode::HostOnly) { + return retrieve_value_for_header_from_handle(cx, self, name_chars, args.rval()); + } + + RootedObject entries(cx, get_entries(cx, self)); + if (!entries) { + return false; + } + + RootedValue name_val(cx); + if (!redecode_str_if_changed(cx, args[0], name_chars, name_changed, &name_val)) { + return false; + } + return MapGet(cx, entries, name_val, args.rval()); } bool Headers::set(JSContext *cx, unsigned argc, JS::Value *vp) { @@ -463,20 +355,38 @@ bool Headers::set(JSContext *cx, unsigned argc, JS::Value *vp) { NORMALIZE_NAME(args[0], "Headers.set") NORMALIZE_VALUE(args[1], "Headers.set") - auto handle = get_handle(self); - if (handle) { - std::string_view name = name_chars; - std::string_view val = value_chars; - auto res = handle->set(name, val); + Mode mode = Headers::mode(self); + if (mode == Mode::HostOnly) { + auto handle = get_handle(self); + MOZ_ASSERT(handle); + auto res = handle->set(name_chars, value_chars); if (auto *err = res.to_err()) { HANDLE_ERROR(cx, *err); return false; } + } else if (mode == Mode::CachedInContent) { + switch_to_content_only_mode(self); } - JS::RootedObject map(cx, get_backing_map(self)); - if (!JS::MapSet(cx, map, normalized_name, normalized_value)) { - return false; + if (mode == Mode::ContentOnly) { + RootedObject entries(cx, get_entries(cx, self)); + if (!entries) { + return false; + } + + RootedValue name_val(cx); + if (!redecode_str_if_changed(cx, args[0], name_chars, name_changed, &name_val)) { + return false; + } + + RootedValue value_val(cx); + if (!redecode_str_if_changed(cx, args[0], value_chars, value_changed, &value_val)) { + return false; + } + + if (!MapSet(cx, entries, name_val, value_val)) { + return false; + } } args.rval().setUndefined(); @@ -487,20 +397,33 @@ bool Headers::has(JSContext *cx, unsigned argc, JS::Value *vp) { METHOD_HEADER(1) NORMALIZE_NAME(args[0], "Headers.has") - bool has; - JS::RootedObject map(cx, get_backing_map(self)); - if (!JS::MapHas(cx, map, normalized_name, &has)) { - return false; + + Mode mode = Headers::mode(self); + if (mode == Mode::HostOnly) { + auto handle = get_handle(self); + MOZ_ASSERT(handle); + auto res = handle->has(name_chars); + MOZ_ASSERT(!res.is_err()); + args.rval().setBoolean(res.unwrap()); + } else { + RootedObject entries(cx, get_entries(cx, self)); + if (!entries) { + return false; + } + bool has; + if (!MapHas(cx, entries, args[0], &has)) { + return false; + } + args.rval().setBoolean(has); } - args.rval().setBoolean(has); return true; } bool Headers::append(JSContext *cx, unsigned argc, JS::Value *vp) { METHOD_HEADER(2) - if (!Headers::append_header_value(cx, self, args[0], args[1], "Headers.append")) { + if (!append_header_value(cx, self, args[0], args[1], "Headers.append")) { return false; } @@ -509,58 +432,51 @@ bool Headers::append(JSContext *cx, unsigned argc, JS::Value *vp) { } bool Headers::maybe_add(JSContext *cx, JS::HandleObject self, const char *name, const char *value) { - MOZ_ASSERT(Headers::is_instance(self)); - JS::RootedString name_str(cx, JS_NewStringCopyN(cx, name, strlen(name))); - if (!name_str) { - return false; - } - JS::RootedValue name_val(cx, JS::StringValue(name_str)); - - JS::RootedObject map(cx, get_backing_map(self)); - bool has; - if (!JS::MapHas(cx, map, name_val, &has)) { - return false; - } - if (has) { + MOZ_ASSERT(mode(self) == Mode::HostOnly); + auto handle = get_handle(self); + auto has = handle->has(name); + MOZ_ASSERT(!has.is_err()); + if (has.unwrap()) { return true; } - JS::RootedString value_str(cx, JS_NewStringCopyN(cx, value, strlen(value))); - if (!value_str) { + auto res = handle->append(name, value); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); return false; } - JS::RootedValue value_val(cx, JS::StringValue(value_str)); - return Headers::append_header_value(cx, self, name_val, value_val, "internal_maybe_add"); + return true; } bool Headers::delete_(JSContext *cx, unsigned argc, JS::Value *vp) { METHOD_HEADER_WITH_NAME(1, "delete") - NORMALIZE_NAME(args[0], "Headers.delete") - - bool has; - JS::RootedObject map(cx, get_backing_map(self)); - if (!JS::MapDelete(cx, map, normalized_name, &has)) { - return false; - } - - // If no header with the given name exists, `delete` is a no-op. - if (!has) { - args.rval().setUndefined(); - return true; - } - - auto handle = get_handle(self); - if (handle) { + Mode mode = Headers::mode(self); + if (mode == Mode::HostOnly) { + auto handle = get_handle(self); + MOZ_ASSERT(handle); std::string_view name = name_chars; auto res = handle->remove(name); if (auto *err = res.to_err()) { HANDLE_ERROR(cx, *err); return false; } + } else if (mode == Mode::CachedInContent) { + switch_to_content_only_mode(self); + } + + if (mode == Mode::ContentOnly) { + RootedObject entries(cx, get_entries(cx, self)); + if (!entries) { + return false; + } + + bool had; + return MapDelete(cx, entries, args[0], &had); } + args.rval().setUndefined(); return true; } @@ -580,7 +496,7 @@ bool Headers::forEach(JSContext *cx, unsigned argc, JS::Value *vp) { JS::RootedValue rval(cx); JS::RootedValue iterable(cx); - if (!JS::MapEntries(cx, backing_map, &iterable)) + if (!JS::MapEntries(cx, entries, &iterable)) return false; JS::ForOfIterator it(cx); @@ -609,19 +525,19 @@ bool Headers::forEach(JSContext *cx, unsigned argc, JS::Value *vp) { return true; } -bool Headers::entries(JSContext *cx, unsigned argc, JS::Value *vp) { +bool Headers::entries(JSContext *cx, unsigned argc, Value *vp) { HEADERS_ITERATION_METHOD(0) - return JS::MapEntries(cx, backing_map, args.rval()); + return MapEntries(cx, entries, args.rval()); } -bool Headers::keys(JSContext *cx, unsigned argc, JS::Value *vp) { +bool Headers::keys(JSContext *cx, unsigned argc, Value *vp) { HEADERS_ITERATION_METHOD(0) - return JS::MapKeys(cx, backing_map, args.rval()); + return MapKeys(cx, entries, args.rval()); } -bool Headers::values(JSContext *cx, unsigned argc, JS::Value *vp) { +bool Headers::values(JSContext *cx, unsigned argc, Value *vp) { HEADERS_ITERATION_METHOD(0) - return JS::MapValues(cx, backing_map, args.rval()); + return MapValues(cx, entries, args.rval()); } const JSFunctionSpec Headers::static_methods[] = { @@ -667,6 +583,12 @@ bool Headers::init_class(JSContext *cx, JS::HandleObject global) { if (!ok) return false; + auto comma_str = JS_NewStringCopyN(cx, ", ", 2); + if (!comma_str) { + return false; + } + comma.init(cx, comma_str); + JS::RootedValue entries(cx); if (!JS_GetProperty(cx, proto_obj, "entries", &entries)) return false; @@ -676,12 +598,55 @@ bool Headers::init_class(JSContext *cx, JS::HandleObject global) { return JS_DefinePropertyById(cx, proto_obj, iteratorId, entries, 0); } -JSObject *Headers::create(JSContext *cx, JS::HandleObject self, - host_api::HttpHeadersReadOnly *handle) { - JS_SetReservedSlot(self, static_cast(Slots::Handle), JS::PrivateValue(handle)); +JSObject *Headers::create(JSContext *cx, HandleObject self, host_api::HttpHeadersReadOnly *handle) { + SetReservedSlot(self, static_cast(Slots::Handle), PrivateValue(handle)); + SetReservedSlot(self, static_cast(Slots::Mode), + JS::Int32Value(static_cast(Mode::HostOnly))); return self; } +JSObject *Headers::get_entries(JSContext *cx, HandleObject self) { + MOZ_ASSERT(is_instance(self)); + if (mode(self) != Mode::HostOnly) { + return &GetReservedSlot(self, static_cast(Slots::Entries)).toObject(); + } + + host_api::HttpHeadersReadOnly *handle = get_handle(self); + MOZ_ASSERT(handle); + auto res = handle->entries(); + if (res.is_err()) { + HANDLE_ERROR(cx, *res.to_err()); + return nullptr; + } + + RootedObject map(cx, JS::NewMapObject(cx)); + if (!map) { + return nullptr; + } + + RootedString key(cx); + RootedValue key_val(cx); + RootedString value(cx); + RootedValue value_val(cx); + for (auto& entry : std::move(res.unwrap())) { + key = core::decode(cx, std::get<0>(entry)); + if (!key) { + return nullptr; + } + value = core::decode(cx, std::get<1>(entry)); + if (!value) { + return nullptr; + } + key_val.setString(key); + value_val.setString(value); + if (!MapSet(cx, map, key_val, value_val)) { + return nullptr; + } + } + + SetReservedSlot(self, static_cast(Slots::Entries), ObjectValue(*map)); + SetReservedSlot(self, static_cast(Slots::Mode), + JS::Int32Value(static_cast(Mode::CachedInContent))); + return map; +} -} // namespace fetch -} // namespace web -} // namespace builtins +} // namespace builtins::web::fetch diff --git a/builtins/web/fetch/headers.h b/builtins/web/fetch/headers.h index 2b61a86..36b75de 100644 --- a/builtins/web/fetch/headers.h +++ b/builtins/web/fetch/headers.h @@ -22,43 +22,61 @@ class Headers final : public BuiltinImpl { public: static constexpr const char *class_name = "Headers"; + /// Usually, headers are stored on the host only, including when they're created in-content. + /// However, iterating over headers (as keys, values, or entries) would be extremely slow if + /// we retrieved all of them from the host for each iteration step. + /// So, when we start iterating, we retrieve them once and store them in a Map as a cache. + /// If, while iterating, a header is added, deleted, or replaced, we start treating the Map as + /// the canonical store for headers, and discard the underlying resource handle entirely. + enum class Mode { + HostOnly, // Headers are stored in the host. + CachedInContent, // Host holds canonical headers, content a cached copy. + ContentOnly, // Headers are stored in a Map held by the `Entries` slot. + }; + enum class Slots { Handle, + Entries, // Map holding headers if they are available in-content. + Mode, Count, }; /** * Adds the given header name/value to `self`'s list of headers iff `self` * doesn't already contain a header with that name. - * - * Assumes that both the name and value are valid and normalized. - * TODO(performance): fully skip normalization. - * https://github.com/fastly/js-compute-runtime/issues/221 */ static bool maybe_add(JSContext *cx, JS::HandleObject self, const char *name, const char *value); - // Appends a non-normalized value for a non-normalized header name to both - // the JS side Map and, in non-standalone mode, the host. + /// Appends a value for a header name. // - // Verifies and normalizes the name and value. + /// Validates and normalizes the name and value. static bool append_header_value(JSContext *cx, JS::HandleObject self, JS::HandleValue name, JS::HandleValue value, const char *fun_name); + static Mode mode(JSObject* self) const { + MOZ_ASSERT(Headers::is_instance(self)); + return static_cast(JS::GetReservedSlot(self, static_cast(Slots::Mode)).toInt32()); + } + static const JSFunctionSpec static_methods[]; static const JSPropertySpec static_properties[]; static const JSFunctionSpec methods[]; static const JSPropertySpec properties[]; - static const unsigned ctor_length = 1; + static constexpr unsigned ctor_length = 1; static bool init_class(JSContext *cx, HandleObject global); static bool constructor(JSContext *cx, unsigned argc, Value *vp); - static JSObject *create(JSContext *cx, HandleObject self, host_api::HttpHeaders *handle, - HandleObject init_headers); static JSObject *create(JSContext *cx, HandleObject self, host_api::HttpHeaders *handle, HandleValue init_headers); static JSObject *create(JSContext *cx, HandleObject self, host_api::HttpHeadersReadOnly *handle); + + /// Returns a Map object containing the headers. + /// + /// Depending on the `Mode` the instance is in, this can be a cache or the canonical store for + /// the headers. + static JSObject* get_entries(JSContext *cx, HandleObject self); }; } // namespace fetch diff --git a/builtins/web/url.cpp b/builtins/web/url.cpp index e05fa15..90de01e 100644 --- a/builtins/web/url.cpp +++ b/builtins/web/url.cpp @@ -24,7 +24,7 @@ bool URLSearchParamsIterator::next(JSContext *cx, unsigned argc, JS::Value *vp) if (!result) return false; - jsurl::JSSearchParam param{jsurl::SpecSlice(nullptr, 0), jsurl::SpecSlice(nullptr, 0), false}; + jsurl::JSSearchParam param{}; jsurl::params_at(params, index, ¶m); if (param.done) { diff --git a/crates/rust-url/rust-url.h b/crates/rust-url/rust-url.h index 9a7cdb2..5706b3f 100644 --- a/crates/rust-url/rust-url.h +++ b/crates/rust-url/rust-url.h @@ -47,6 +47,11 @@ struct SpecSlice { const uint8_t *data; size_t len; + SpecSlice() + : data(nullptr), + len(0) + {} + SpecSlice(const uint8_t *const& data, size_t const& len) : data(data), @@ -60,6 +65,12 @@ struct JSSearchParam { SpecSlice value; bool done; + JSSearchParam() + : name(SpecSlice()), + value(SpecSlice()), + done(false) + {} + JSSearchParam(SpecSlice const& name, SpecSlice const& value, bool const& done) diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index 1d3427b..aaa4b3c 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -216,6 +216,10 @@ HttpHeaders::HttpHeaders(const HttpHeadersReadOnly &headers) : HttpHeadersReadOn this->handle_state_ = new HandleState(handle.__handle); } +HttpHeaders *HttpHeadersReadOnly::clone() { + return new HttpHeaders(*this); +} + Result>> HttpHeadersReadOnly::entries() const { Result>> res; MOZ_ASSERT(valid()); @@ -280,6 +284,14 @@ Result>> HttpHeadersReadOnly::get(string_view name) return res; } +Result HttpHeadersReadOnly::has(string_view name) const { + MOZ_ASSERT(valid()); + + auto hdr = string_view_to_world_string(name); + Borrow borrow(this->handle_state_); + return Result::ok(wasi_http_0_2_0_types_method_fields_has(borrow, &hdr)); +} + Result HttpHeaders::set(string_view name, string_view value) { MOZ_ASSERT(valid()); auto hdr = from_string_view(name); diff --git a/include/host_api.h b/include/host_api.h index f063264..1d27436 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -345,6 +345,7 @@ class HttpHeadersReadOnly : public Resource { Result>> entries() const; Result> names() const; Result>> get(string_view name) const; + Result has(string_view name) const; }; class HttpHeaders final : public HttpHeadersReadOnly { diff --git a/runtime/decode.cpp b/runtime/decode.cpp new file mode 100644 index 0000000..4f92564 --- /dev/null +++ b/runtime/decode.cpp @@ -0,0 +1,10 @@ +#include "encode.h" + +namespace core { + +JSString* decode(JSContext* cx, host_api::HostString& str) { + JS::UTF8Chars ret_chars(str.ptr.get(), str.len); + return JS_NewStringCopyUTF8N(cx, ret_chars); +} + +} // namespace core diff --git a/runtime/decode.h b/runtime/decode.h new file mode 100644 index 0000000..93ecb80 --- /dev/null +++ b/runtime/decode.h @@ -0,0 +1,12 @@ +#ifndef JS_COMPUTE_RUNTIME_DECODE_H +#define JS_COMPUTE_RUNTIME_DECODE_H + +#include "host_api.h" + +namespace core { + +JSString* decode(JSContext *cx, host_api::HostString& str); + +} // namespace core + +#endif From d4e43db26a8cd152b81b4d378a25dff50393e972 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Sat, 6 Apr 2024 13:49:08 +0200 Subject: [PATCH 05/22] Immediately lock reified body stream when the underlying body has already been consumed --- builtins/web/fetch/request-response.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index 48b0e34..5ce5901 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -456,13 +456,12 @@ bool RequestOrResponse::append_body(JSContext *cx, JS::HandleObject self, JS::Ha return false; } - bool success = mark_body_used(cx, source); + mozilla::DebugOnly success = mark_body_used(cx, source); MOZ_ASSERT(success); if (body_stream(source) != body_stream(self)) { success = mark_body_used(cx, self); MOZ_ASSERT(success); } - (void)success; return true; } @@ -1135,7 +1134,11 @@ JSObject *RequestOrResponse::create_body_stream(JSContext *cx, JS::HandleObject return nullptr; } - // TODO: immediately lock the stream if the owner's body is already used. + // If the body has already been used without being reified as a ReadableStream, + // lock the stream immediately. + if (body_used(owner)) { + MOZ_RELEASE_ASSERT(streams::NativeStreamSource::lock_stream(cx, body_stream)); + } JS_SetReservedSlot(owner, static_cast(Slots::BodyStream), JS::ObjectValue(*body_stream)); @@ -1544,6 +1547,7 @@ JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance, JS::H host_api::HostString method; if (init_val.isObject()) { + // TODO: investigate special-casing native Request objects here to not reify headers and bodies. JS::RootedObject init(cx, init_val.toObjectOrNull()); if (!JS_GetProperty(cx, init, "method", &method_val) || !JS_GetProperty(cx, init, "headers", &headers_val) || From dae69535926c1f724d3f6d82de487d724416e6ca Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Wed, 10 Apr 2024 12:23:16 +0200 Subject: [PATCH 06/22] More headers rework! --- builtins/web/fetch/headers.cpp | 40 ++++++++++++++----------------- builtins/web/fetch/headers.h | 2 +- host-apis/wasi-0.2.0/host_api.cpp | 14 ++++++++--- include/host_api.h | 6 +++++ spin.toml | 2 +- 5 files changed, 37 insertions(+), 27 deletions(-) diff --git a/builtins/web/fetch/headers.cpp b/builtins/web/fetch/headers.cpp index 83ab631..199b9ef 100644 --- a/builtins/web/fetch/headers.cpp +++ b/builtins/web/fetch/headers.cpp @@ -9,8 +9,6 @@ namespace builtins::web::fetch { namespace { -using Handle = host_api::HttpHeaders; - #define HEADERS_ITERATION_METHOD(argc) \ METHOD_HEADER(argc) \ JS::RootedObject entries(cx, get_entries(cx, self)); \ @@ -54,11 +52,11 @@ const char VALID_NAME_CHARS[128] = { return false; \ } -Handle *get_handle(JSObject *self) { +host_api::HttpHeadersReadOnly *get_handle(JSObject *self) { MOZ_ASSERT(Headers::is_instance(self)); auto handle = JS::GetReservedSlot(self, static_cast(Headers::Slots::Handle)).toPrivate(); - return static_cast(handle); + return static_cast(handle); } /** @@ -267,30 +265,28 @@ bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::Hand NORMALIZE_NAME(name, fun_name) NORMALIZE_VALUE(value, fun_name) - auto handle = get_handle(self); - if (handle) { - std::string_view name = name_chars; - if (name == "set-cookie") { - for (auto value : splitCookiesString(value_chars)) { - auto res = handle->append(name_chars, value); - if (auto *err = res.to_err()) { - HANDLE_ERROR(cx, *err); - return false; - } - } - } else { - auto res = handle->append(name_chars, value_chars); + auto handle = get_handle(self)->as_writable(); + std::string_view name_str = name_chars; + if (name_str == "set-cookie") { + for (auto value : splitCookiesString(value_chars)) { + auto res = handle->append(name_chars, value); if (auto *err = res.to_err()) { HANDLE_ERROR(cx, *err); return false; } } + } else { + auto res = handle->append(name_chars, value_chars); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } } return true; } -JSObject *Headers::create(JSContext *cx, JS::HandleObject self, Handle *handle, +JSObject *Headers::create(JSContext *cx, JS::HandleObject self, host_api::HttpHeaders *handle, JS::HandleValue initv) { // TODO: check if initv is a Headers instance and clone its handle if so. JS::RootedObject headers(cx, create(cx, self, handle)); @@ -357,7 +353,7 @@ bool Headers::set(JSContext *cx, unsigned argc, JS::Value *vp) { Mode mode = Headers::mode(self); if (mode == Mode::HostOnly) { - auto handle = get_handle(self); + auto handle = get_handle(self)->as_writable(); MOZ_ASSERT(handle); auto res = handle->set(name_chars, value_chars); if (auto *err = res.to_err()) { @@ -433,7 +429,7 @@ bool Headers::append(JSContext *cx, unsigned argc, JS::Value *vp) { bool Headers::maybe_add(JSContext *cx, JS::HandleObject self, const char *name, const char *value) { MOZ_ASSERT(mode(self) == Mode::HostOnly); - auto handle = get_handle(self); + auto handle = get_handle(self)->as_writable(); auto has = handle->has(name); MOZ_ASSERT(!has.is_err()); if (has.unwrap()) { @@ -454,7 +450,7 @@ bool Headers::delete_(JSContext *cx, unsigned argc, JS::Value *vp) { NORMALIZE_NAME(args[0], "Headers.delete") Mode mode = Headers::mode(self); if (mode == Mode::HostOnly) { - auto handle = get_handle(self); + auto handle = get_handle(self)->as_writable(); MOZ_ASSERT(handle); std::string_view name = name_chars; auto res = handle->remove(name); @@ -610,7 +606,7 @@ JSObject *Headers::get_entries(JSContext *cx, HandleObject self) { return &GetReservedSlot(self, static_cast(Slots::Entries)).toObject(); } - host_api::HttpHeadersReadOnly *handle = get_handle(self); + auto handle = get_handle(self); MOZ_ASSERT(handle); auto res = handle->entries(); if (res.is_err()) { diff --git a/builtins/web/fetch/headers.h b/builtins/web/fetch/headers.h index 36b75de..d9a4404 100644 --- a/builtins/web/fetch/headers.h +++ b/builtins/web/fetch/headers.h @@ -53,7 +53,7 @@ class Headers final : public BuiltinImpl { static bool append_header_value(JSContext *cx, JS::HandleObject self, JS::HandleValue name, JS::HandleValue value, const char *fun_name); - static Mode mode(JSObject* self) const { + static Mode mode(JSObject* self) { MOZ_ASSERT(Headers::is_instance(self)); return static_cast(JS::GetReservedSlot(self, static_cast(Slots::Mode)).toInt32()); } diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index aaa4b3c..ecf3a9f 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -313,10 +313,18 @@ Result HttpHeaders::append(string_view name, string_view value) { auto val = from_string_view(value); Borrow borrow(this->handle_state_); + // TODO: properly handle `err` wasi_http_0_2_0_types_header_error_t err; - wasi_http_0_2_0_types_method_fields_append(borrow, &hdr, &val, &err); - - // TODO: handle `err` + if (!wasi_http_0_2_0_types_method_fields_append(borrow, &hdr, &val, &err)) { + switch (err.tag) { + case WASI_HTTP_0_2_0_TYPES_HEADER_ERROR_INVALID_SYNTAX: + case WASI_HTTP_0_2_0_TYPES_HEADER_ERROR_FORBIDDEN: + return Result::err(154); + case WASI_HTTP_0_2_0_TYPES_HEADER_ERROR_IMMUTABLE: + fprintf(stderr, "Headers %d should not be immutable", this->handle_state_->handle); + MOZ_ASSERT_UNREACHABLE(); + } + } return {}; } diff --git a/include/host_api.h b/include/host_api.h index 1d27436..3333cff 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -341,6 +341,9 @@ class HttpHeadersReadOnly : public Resource { HttpHeaders* clone(); virtual bool is_writable() { return false; }; + virtual HttpHeaders* as_writable() { + MOZ_ASSERT_UNREACHABLE(); + }; Result>> entries() const; Result> names() const; @@ -363,6 +366,9 @@ class HttpHeaders final : public HttpHeadersReadOnly { static Result FromEntries(const vector>> &entries); bool is_writable() override { return true; }; + HttpHeaders* as_writable() override { + return this; + }; Result set(string_view name, string_view value); Result append(string_view name, string_view value); diff --git a/spin.toml b/spin.toml index c63db16..a76fa6c 100644 --- a/spin.toml +++ b/spin.toml @@ -12,7 +12,7 @@ component = "js-component" [component.js-component] source = "smoke-test.wasm" -allowed_outbound_hosts = ["https://fermyon.com/", "https://example.com:443"] +allowed_outbound_hosts = ["https://fermyon.com/", "https://example.com:443", "http://localhost:3000"] [component.js-component.build] command = "" watch = ["smoke-test.wasm"] From aa5a32c112e5a9a1a4d3766f6c3b145f496a235b Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 30 Apr 2024 00:02:47 +0200 Subject: [PATCH 07/22] Use `string_view` instead of `HostString in core::decode` `HostString` converts to `string_view`, so this makes `decode` easy to use from more places. --- runtime/decode.cpp | 4 ++-- runtime/decode.h | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/runtime/decode.cpp b/runtime/decode.cpp index 4f92564..2bd7daf 100644 --- a/runtime/decode.cpp +++ b/runtime/decode.cpp @@ -2,8 +2,8 @@ namespace core { -JSString* decode(JSContext* cx, host_api::HostString& str) { - JS::UTF8Chars ret_chars(str.ptr.get(), str.len); +JSString* decode(JSContext* cx, string_view str) { + JS::UTF8Chars ret_chars(str.data(), str.length()); return JS_NewStringCopyUTF8N(cx, ret_chars); } diff --git a/runtime/decode.h b/runtime/decode.h index 93ecb80..e473fcd 100644 --- a/runtime/decode.h +++ b/runtime/decode.h @@ -1,11 +1,9 @@ #ifndef JS_COMPUTE_RUNTIME_DECODE_H #define JS_COMPUTE_RUNTIME_DECODE_H -#include "host_api.h" - namespace core { -JSString* decode(JSContext *cx, host_api::HostString& str); +JSString* decode(JSContext *cx, string_view str); } // namespace core From d1984e9487a90939dee82f47ec34374184e8701f Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 30 Apr 2024 00:03:16 +0200 Subject: [PATCH 08/22] Adjust namespace of wpt support functionality --- tests/wpt-harness/wpt_builtins.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wpt-harness/wpt_builtins.cpp b/tests/wpt-harness/wpt_builtins.cpp index ab83cfc..d62566b 100644 --- a/tests/wpt-harness/wpt_builtins.cpp +++ b/tests/wpt-harness/wpt_builtins.cpp @@ -32,7 +32,7 @@ const JSPropertySpec properties[] = { JS_PSGS("baseURL", baseURL_get, baseURL_set, JSPROP_ENUMERATE), JS_PS_END}; -namespace wpt_builtins { +namespace wpt_support { bool install(api::Engine* engine) { engine->enable_module_mode(false); From ca335a770b4868fdb18fedd0962bdc9cbab93319 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 30 Apr 2024 00:05:34 +0200 Subject: [PATCH 09/22] =?UTF-8?q?And=20yet=20more=20headers=20rework?= =?UTF-8?q?=E2=80=94along=20with=20an=20overhaul=20of=20lots=20of=20the=20?= =?UTF-8?q?host=20API=20abstraction=20and=20various=20other=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- builtins/web/fetch/fetch-api.cpp | 2 +- builtins/web/fetch/fetch_event.cpp | 163 ++++----- builtins/web/fetch/headers.cpp | 434 ++++++++++++++++++------ builtins/web/fetch/headers.h | 61 +++- builtins/web/fetch/request-response.cpp | 192 ++++------- builtins/web/fetch/request-response.h | 20 +- builtins/web/url.cpp | 7 +- host-apis/wasi-0.2.0/host_api.cpp | 322 ++++++++++++------ include/host_api.h | 63 ++-- 9 files changed, 785 insertions(+), 479 deletions(-) diff --git a/builtins/web/fetch/fetch-api.cpp b/builtins/web/fetch/fetch-api.cpp index 9f45119..25c2cdf 100644 --- a/builtins/web/fetch/fetch-api.cpp +++ b/builtins/web/fetch/fetch-api.cpp @@ -42,7 +42,7 @@ class ResponseFutureTask final : public api::AsyncTask { return false; } - response_obj = Response::create(cx, response_obj, response); + response_obj = Response::create_incoming(cx, response_obj, response); if (!response_obj) { return false; } diff --git a/builtins/web/fetch/fetch_event.cpp b/builtins/web/fetch/fetch_event.cpp index 8279b2e..ca6a5e0 100644 --- a/builtins/web/fetch/fetch_event.cpp +++ b/builtins/web/fetch/fetch_event.cpp @@ -20,8 +20,6 @@ api::Engine *ENGINE; PersistentRooted INSTANCE; JS::PersistentRootedObjectVector *FETCH_HANDLERS; - -host_api::HttpOutgoingResponse::ResponseOutparam RESPONSE_OUT; host_api::HttpOutgoingBody *STREAMING_BODY; void inc_pending_promise_count(JSObject *self) { @@ -70,7 +68,7 @@ JSObject *FetchEvent::prepare_downstream_request(JSContext *cx) { cx, JS_NewObjectWithGivenProto(cx, &Request::class_, Request::proto_obj)); if (!requestInstance) return nullptr; - return Request::create(cx, requestInstance, nullptr); + return Request::create(cx, requestInstance); } bool FetchEvent::init_incoming_request(JSContext *cx, JS::HandleObject self, @@ -149,7 +147,7 @@ bool send_response(host_api::HttpOutgoingResponse *response, JS::HandleObject se FetchEvent::State new_state) { MOZ_ASSERT(FetchEvent::state(self) == FetchEvent::State::unhandled || FetchEvent::state(self) == FetchEvent::State::waitToRespond); - auto result = response->send(RESPONSE_OUT); + auto result = response->send(); FetchEvent::set_state(self, new_state); if (auto *err = result.to_err()) { @@ -161,26 +159,27 @@ bool send_response(host_api::HttpOutgoingResponse *response, JS::HandleObject se } bool start_response(JSContext *cx, JS::HandleObject response_obj, bool streaming) { - auto generic_response = Response::response_handle(response_obj); - host_api::HttpOutgoingResponse *response; - - if (generic_response->is_incoming()) { - auto incoming_response = static_cast(generic_response); - auto status = incoming_response->status(); - MOZ_RELEASE_ASSERT(!status.is_err(), "Incoming response must have a status code"); - auto headers = incoming_response->headers().unwrap()->clone(); - response = host_api::HttpOutgoingResponse::make(status.unwrap(), headers); - auto *source_body = incoming_response->body().unwrap(); - auto *dest_body = response->body().unwrap(); - - auto res = dest_body->append(ENGINE, source_body); - if (auto *err = res.to_err()) { - HANDLE_ERROR(cx, *err); - return false; + auto status = Response::status(response_obj); + auto headers = RequestOrResponse::headers_clone(cx, response_obj); + if (!headers) { + return false; + } + + auto incoming_response = Response::response_handle(response_obj); + host_api::HttpOutgoingResponse* response = + host_api::HttpOutgoingResponse::make(status, headers.get()); + if (incoming_response) { + if (streaming) { + auto *source_body = incoming_response->body().unwrap(); + auto *dest_body = response->body().unwrap(); + + auto res = dest_body->append(ENGINE, source_body); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + MOZ_RELEASE_ASSERT(RequestOrResponse::mark_body_used(cx, response_obj)); } - MOZ_RELEASE_ASSERT(RequestOrResponse::mark_body_used(cx, response_obj)); - } else { - response = static_cast(generic_response); } if (streaming && response->has_body()) { @@ -509,6 +508,49 @@ static void dispatch_fetch_event(HandleObject event, double *total_compute) { // LOG("Request handler took %fms\n", diff / 1000); } +bool handle_incoming_request(host_api::HttpIncomingRequest * request) { + HandleObject fetch_event = FetchEvent::instance(); + MOZ_ASSERT(FetchEvent::is_instance(fetch_event)); + if (!FetchEvent::init_incoming_request(ENGINE->cx(), fetch_event, request)) { + ENGINE->dump_pending_exception("initialization of FetchEvent"); + return false; + } + + double total_compute = 0; + + dispatch_fetch_event(fetch_event, &total_compute); + + // track fetch event interest, which when decremented ends the event loop + ENGINE->incr_event_loop_interest(); + + bool success = ENGINE->run_event_loop(); + + if (JS_IsExceptionPending(ENGINE->cx())) { + ENGINE->dump_pending_exception("evaluating incoming request"); + } + + if (!success) { + fprintf(stderr, "Internal error."); + } + + if (ENGINE->debug_logging_enabled() && ENGINE->has_pending_async_tasks()) { + fprintf(stderr, "Event loop terminated with async tasks pending. " + "Use FetchEvent#waitUntil to extend the component's " + "lifetime if needed.\n"); + } + + if (!FetchEvent::response_started(fetch_event)) { + FetchEvent::respondWithError(ENGINE->cx(), fetch_event); + return true; + } + + if (STREAMING_BODY && STREAMING_BODY->valid()) { + STREAMING_BODY->close(); + } + + return true; +} + bool install(api::Engine *engine) { ENGINE = engine; FETCH_HANDLERS = new JS::PersistentRootedObjectVector(engine->cx()); @@ -544,81 +586,8 @@ bool install(api::Engine *engine) { // } // } + host_api::HttpIncomingRequest::set_handler(handle_incoming_request); return true; } } // namespace builtins::web::fetch::fetch_event - -// #define S_TO_NS(s) ((s) * 1000000000) -// static int64_t now_ns() { -// timespec now{}; -// clock_gettime(CLOCK_MONOTONIC, &now); -// return S_TO_NS(now.tv_sec) + now.tv_nsec; -// } -using namespace builtins::web::fetch::fetch_event; -// TODO: change this to fully work in terms of host_api. -void exports_wasi_http_incoming_handler(exports_wasi_http_incoming_request request_handle, - exports_wasi_http_response_outparam response_out) { - - // auto begin = now_ns(); - // auto id1 = host_api::MonotonicClock::subscribe(begin + 1, true); - // auto id2 = host_api::MonotonicClock::subscribe(begin + 1000000*1000, true); - // bindings_borrow_pollable_t handles[2] = {bindings_borrow_pollable_t{id2}, - // bindings_borrow_pollable_t{id1}}; auto list = bindings_list_borrow_pollable_t{handles, 2}; - // bindings_list_u32_t res = {.ptr = nullptr,.len = 0}; - // wasi_io_0_2_0_rc_2023_10_18_poll_poll_list(&list, &res); - // fprintf(stderr, "first ready after first poll: %d. diff: %lld\n", handles[res.ptr[0]].__handle, - // (now_ns() - begin) / 1000); - // - // wasi_io_0_2_0_rc_2023_10_18_poll_pollable_drop_own(bindings_own_pollable_t{id1}); - // - // bindings_borrow_pollable_t handles2[1] = {bindings_borrow_pollable_t{id2}}; - // list = bindings_list_borrow_pollable_t{handles2, 1}; - // wasi_io_0_2_0_rc_2023_10_18_poll_poll_list(&list, &res); - // fprintf(stderr, "first ready after second poll: %d. diff: %lld\n", - // handles2[res.ptr[0]].__handle, (now_ns() - begin) / 1000); - // - // return; - - RESPONSE_OUT = response_out.__handle; - - auto *request = new host_api::HttpIncomingRequest(request_handle.__handle); - HandleObject fetch_event = FetchEvent::instance(); - MOZ_ASSERT(FetchEvent::is_instance(fetch_event)); - if (!FetchEvent::init_incoming_request(ENGINE->cx(), fetch_event, request)) { - ENGINE->dump_pending_exception("initialization of FetchEvent"); - return; - } - - double total_compute = 0; - - dispatch_fetch_event(fetch_event, &total_compute); - - // track fetch event interest, which when decremented ends the event loop - ENGINE->incr_event_loop_interest(); - - bool success = ENGINE->run_event_loop(); - - if (JS_IsExceptionPending(ENGINE->cx())) { - ENGINE->dump_pending_exception("evaluating incoming request"); - } - - if (!success) { - fprintf(stderr, "Internal error."); - } - - if (ENGINE->debug_logging_enabled() && ENGINE->has_pending_async_tasks()) { - fprintf(stderr, "Event loop terminated with async tasks pending. " - "Use FetchEvent#waitUntil to extend the component's " - "lifetime if needed.\n"); - } - - if (!FetchEvent::response_started(fetch_event)) { - FetchEvent::respondWithError(ENGINE->cx(), fetch_event); - return; - } - - if (STREAMING_BODY && STREAMING_BODY->valid()) { - STREAMING_BODY->close(); - } -} diff --git a/builtins/web/fetch/headers.cpp b/builtins/web/fetch/headers.cpp index 199b9ef..6139b0c 100644 --- a/builtins/web/fetch/headers.cpp +++ b/builtins/web/fetch/headers.cpp @@ -255,9 +255,213 @@ std::vector splitCookiesString(std::string_view cookiesString) } // namespace -void switch_to_content_only_mode(JSObject* self) { +bool redecode_str_if_changed(JSContext* cx, HandleValue str_val, string_view chars, + bool changed, MutableHandleValue rval) { + if (!changed) { + rval.set(str_val); + return true; + } + + RootedString str(cx, core::decode(cx, chars)); + if (!str) { + return false; + } + + rval.setString(str); + return true; +} + +static bool switch_mode(JSContext* cx, HandleObject self, const Headers::Mode mode) { + auto current_mode = Headers::mode(self); + if (mode == current_mode) { + return true; + } + + if (current_mode == Headers::Mode::Uninitialized) { + if (mode == Headers::Mode::ContentOnly) { + RootedObject map(cx, JS::NewMapObject(cx)); + if (!map) { + return false; + } + SetReservedSlot(self, static_cast(Headers::Slots::Entries), ObjectValue(*map)); + } else { + MOZ_ASSERT(mode == Headers::Mode::HostOnly); + auto handle = new host_api::HttpHeaders(); + SetReservedSlot(self, static_cast(Headers::Slots::Handle), PrivateValue(handle)); + } + + SetReservedSlot(self, static_cast(Headers::Slots::Mode), JS::Int32Value(static_cast(mode))); + return true; + } + + if (current_mode == Headers::Mode::ContentOnly) { + MOZ_ASSERT(mode == Headers::Mode::CachedInContent, + "Switching from ContentOnly to HostOnly is wasteful and not implemented"); + RootedObject entries(cx, Headers::get_entries(cx, self)); + MOZ_ASSERT(entries); + RootedValue iterable(cx); + if (!MapEntries(cx, entries, &iterable)) { + return false; + } + + JS::ForOfIterator it(cx); + if (!it.init(iterable)) { + return false; + } + + vector> string_entries; + + RootedValue entry_val(cx); + RootedObject entry(cx); + RootedValue name_val(cx); + RootedString name_str(cx); + RootedValue value_val(cx); + RootedString value_str(cx); + while (true) { + bool done; + if (!it.next(&entry_val, &done)) { + return false; + } + + if (done) { + break; + } + + entry = &entry_val.toObject(); + JS_GetElement(cx, entry, 1, &name_val); + JS_GetElement(cx, entry, 0, &value_val); + name_str = name_val.toString(); + value_str = value_val.toString(); + + auto name = core::encode(cx, name_str); + if (!name.ptr) { + return false; + } + + auto value = core::encode(cx, value_str); + if (!value.ptr) { + return false; + } + + string_entries.emplace_back(name, value); + } + + auto handle = host_api::HttpHeaders::FromEntries(string_entries); + if (handle.is_err()) { + JS_ReportErrorASCII(cx, "Failed to clone headers"); + return false; + } + SetReservedSlot(self, static_cast(Headers::Slots::Handle), + PrivateValue(handle.unwrap())); + } + + // Regardless of whether we're switching to CachedInContent or ContentOnly, + // get all entries into content. + if (current_mode == Headers::Mode::HostOnly) { + auto handle = get_handle(self); + MOZ_ASSERT(handle); + auto res = handle->entries(); + if (res.is_err()) { + HANDLE_ERROR(cx, *res.to_err()); + return false; + } + + RootedObject map(cx, JS::NewMapObject(cx)); + if (!map) { + return false; + } + + RootedString key(cx); + RootedValue key_val(cx); + RootedString value(cx); + RootedValue value_val(cx); + for (auto &entry : std::move(res.unwrap())) { + key = core::decode(cx, std::get<0>(entry)); + if (!key) { + return false; + } + value = core::decode(cx, std::get<1>(entry)); + if (!value) { + return false; + } + key_val.setString(key); + value_val.setString(value); + if (!MapSet(cx, map, key_val, value_val)) { + return false; + } + } + + SetReservedSlot(self, static_cast(Headers::Slots::Entries), ObjectValue(*map)); + } + + if (mode == Headers::Mode::ContentOnly) { + auto handle = get_handle(self); + delete handle; + SetReservedSlot(self, static_cast(Headers::Slots::Handle), PrivateValue(nullptr)); + SetReservedSlot(self, static_cast(Headers::Slots::Mode), + JS::Int32Value(static_cast(Headers::Mode::CachedInContent))); + } + SetReservedSlot(self, static_cast(Headers::Slots::Mode), - JS::Int32Value(static_cast(Headers::Mode::ContentOnly))); + JS::Int32Value(static_cast(mode))); + return true; +} + +bool prepare_for_entries_modification(JSContext* cx, JS::HandleObject self) { + auto mode = Headers::mode(self); + if (mode == Headers::Mode::HostOnly) { + auto handle = get_handle(self); + if (!handle->is_writable()) { + auto new_handle = handle->clone(); + if (!new_handle) { + JS_ReportErrorASCII(cx, "Failed to clone headers"); + return false; + } + delete handle; + SetReservedSlot(self, static_cast(Headers::Slots::Handle), PrivateValue(new_handle)); + } + } else if (mode == Headers::Mode::CachedInContent || mode == Headers::Mode::Uninitialized) { + return switch_mode(cx, self, Headers::Mode::ContentOnly); + } + return true; +} + +bool append_single_normalized_header_value(JSContext *cx, HandleObject self, + HandleValue name, string_view name_chars, bool name_changed, + HandleValue value, string_view value_chars, bool value_changed, + const char *fun_name) { + Headers::Mode mode = Headers::mode(self); + if (mode == Headers::Mode::HostOnly) { + auto handle = get_handle(self)->as_writable(); + MOZ_ASSERT(handle); + auto res = handle->append(name_chars, value_chars); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + } else { + MOZ_ASSERT(mode == Headers::Mode::ContentOnly); + RootedObject entries(cx, Headers::get_entries(cx, self)); + if (!entries) { + return false; + } + + RootedValue name_val(cx); + if (!redecode_str_if_changed(cx, name, name_chars, name_changed, &name_val)) { + return false; + } + + RootedValue value_val(cx); + if (!redecode_str_if_changed(cx, value, value_chars, value_changed, &value_val)) { + return false; + } + + if (!MapSet(cx, entries, name_val, value_val)) { + return false; + } + } + + return true; } bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::HandleValue name, @@ -265,20 +469,21 @@ bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::Hand NORMALIZE_NAME(name, fun_name) NORMALIZE_VALUE(value, fun_name) - auto handle = get_handle(self)->as_writable(); + if (!prepare_for_entries_modification(cx, self)) { + return false; + } + std::string_view name_str = name_chars; if (name_str == "set-cookie") { for (auto value : splitCookiesString(value_chars)) { - auto res = handle->append(name_chars, value); - if (auto *err = res.to_err()) { - HANDLE_ERROR(cx, *err); + if (!append_single_normalized_header_value(cx, self, name, name_chars, name_changed, UndefinedHandleValue, + value, true, fun_name)) { return false; } } } else { - auto res = handle->append(name_chars, value_chars); - if (auto *err = res.to_err()) { - HANDLE_ERROR(cx, *err); + if (!append_single_normalized_header_value(cx, self, name, name_chars, name_changed, value, + value_chars, value_changed, fun_name)) { return false; } } @@ -286,41 +491,45 @@ bool Headers::append_header_value(JSContext *cx, JS::HandleObject self, JS::Hand return true; } -JSObject *Headers::create(JSContext *cx, JS::HandleObject self, host_api::HttpHeaders *handle, - JS::HandleValue initv) { - // TODO: check if initv is a Headers instance and clone its handle if so. - JS::RootedObject headers(cx, create(cx, self, handle)); - if (!headers) - return nullptr; +void init_from_handle(JSObject* self, host_api::HttpHeadersReadOnly* handle) { + MOZ_ASSERT(Headers::is_instance(self)); + MOZ_ASSERT(Headers::mode(self) == Headers::Mode::Uninitialized); + SetReservedSlot(self, static_cast(Headers::Slots::Mode), + JS::Int32Value(static_cast(Headers::Mode::HostOnly))); + SetReservedSlot(self, static_cast(Headers::Slots::Handle), PrivateValue(handle)); +} - bool consumed = false; - if (!core::maybe_consume_sequence_or_record(cx, initv, headers, - &consumed, "Headers")) { +JSObject *Headers::create(JSContext *cx, host_api::HttpHeadersReadOnly *handle) { + RootedObject self(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + if (!self) { return nullptr; } + init_from_handle(self, handle); + return self; +} - if (!consumed) { - core::report_sequence_or_record_arg_error(cx, "Headers", ""); +JSObject *Headers::create(JSContext *cx, HandleValue init_headers) { + RootedObject self(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + if (!self) { return nullptr; } - - return headers; + return create(cx, self, init_headers); } -bool redecode_str_if_changed(JSContext* cx, HandleValue str_val, host_api::HostString& chars, - bool changed, MutableHandleValue rval) { - if (!changed) { - rval.set(str_val); - return true; +JSObject *Headers::create(JSContext *cx, HandleObject self, HandleValue initv) { + // TODO: check if initv is a Headers instance and clone its handle if so. + bool consumed = false; + if (!core::maybe_consume_sequence_or_record(cx, initv, self, + &consumed, "Headers")) { + return nullptr; } - RootedString str(cx, core::decode(cx, chars)); - if (!str) { - return false; + if (!consumed) { + core::report_sequence_or_record_arg_error(cx, "Headers", ""); + return nullptr; } - rval.setString(str); - return true; + return self; } bool Headers::get(JSContext *cx, unsigned argc, JS::Value *vp) { @@ -329,6 +538,11 @@ bool Headers::get(JSContext *cx, unsigned argc, JS::Value *vp) { NORMALIZE_NAME(args[0], "Headers.get") Mode mode = Headers::mode(self); + if (mode == Headers::Mode::Uninitialized) { + args.rval().setNull(); + return true; + } + if (mode == Mode::HostOnly) { return retrieve_value_for_header_from_handle(cx, self, name_chars, args.rval()); } @@ -342,7 +556,15 @@ bool Headers::get(JSContext *cx, unsigned argc, JS::Value *vp) { if (!redecode_str_if_changed(cx, args[0], name_chars, name_changed, &name_val)) { return false; } - return MapGet(cx, entries, name_val, args.rval()); + if (!MapGet(cx, entries, name_val, args.rval())) { + return false; + } + + if (args.rval().isUndefined()) { + args.rval().setNull(); + } + + return true; } bool Headers::set(JSContext *cx, unsigned argc, JS::Value *vp) { @@ -351,6 +573,10 @@ bool Headers::set(JSContext *cx, unsigned argc, JS::Value *vp) { NORMALIZE_NAME(args[0], "Headers.set") NORMALIZE_VALUE(args[1], "Headers.set") + if (!prepare_for_entries_modification(cx, self)) { + return false; + } + Mode mode = Headers::mode(self); if (mode == Mode::HostOnly) { auto handle = get_handle(self)->as_writable(); @@ -360,11 +586,8 @@ bool Headers::set(JSContext *cx, unsigned argc, JS::Value *vp) { HANDLE_ERROR(cx, *err); return false; } - } else if (mode == Mode::CachedInContent) { - switch_to_content_only_mode(self); - } - - if (mode == Mode::ContentOnly) { + } else { + MOZ_ASSERT(mode == Mode::ContentOnly); RootedObject entries(cx, get_entries(cx, self)); if (!entries) { return false; @@ -376,7 +599,7 @@ bool Headers::set(JSContext *cx, unsigned argc, JS::Value *vp) { } RootedValue value_val(cx); - if (!redecode_str_if_changed(cx, args[0], value_chars, value_changed, &value_val)) { + if (!redecode_str_if_changed(cx, args[1], value_chars, value_changed, &value_val)) { return false; } @@ -395,6 +618,11 @@ bool Headers::has(JSContext *cx, unsigned argc, JS::Value *vp) { NORMALIZE_NAME(args[0], "Headers.has") Mode mode = Headers::mode(self); + if (mode == Headers::Mode::Uninitialized) { + args.rval().setBoolean(false); + return true; + } + if (mode == Mode::HostOnly) { auto handle = get_handle(self); MOZ_ASSERT(handle); @@ -427,26 +655,59 @@ bool Headers::append(JSContext *cx, unsigned argc, JS::Value *vp) { return true; } -bool Headers::maybe_add(JSContext *cx, JS::HandleObject self, const char *name, const char *value) { - MOZ_ASSERT(mode(self) == Mode::HostOnly); - auto handle = get_handle(self)->as_writable(); - auto has = handle->has(name); - MOZ_ASSERT(!has.is_err()); - if (has.unwrap()) { +bool Headers::set_if_undefined(JSContext *cx, JS::HandleObject self, const char *name, const char *value) { + if (!prepare_for_entries_modification(cx, self)) { + return false; + } + + if (mode(self) == Mode::HostOnly) { + auto handle = get_handle(self)->as_writable(); + auto has = handle->has(name); + MOZ_ASSERT(!has.is_err()); + if (has.unwrap()) { + return true; + } + + auto res = handle->append(name, value); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } return true; } - auto res = handle->append(name, value); - if (auto *err = res.to_err()) { - HANDLE_ERROR(cx, *err); + MOZ_ASSERT(mode(self) == Mode::ContentOnly); + RootedObject entries(cx, get_entries(cx, self)); + RootedString name_str(cx, JS_NewStringCopyZ(cx, name)); + if (!name_str) { return false; } + RootedValue name_val(cx, StringValue(name_str)); - return true; + bool has; + if (!MapHas(cx, entries, name_val, &has)) { + return false; + } + if (has) { + return true; + } + + RootedString value_str(cx, JS_NewStringCopyZ(cx, value)); + if (!value_str) { + return false; + } + RootedValue value_val(cx, StringValue(value_str)); + + return JS::MapSet(cx, entries, name_val, value_val); } bool Headers::delete_(JSContext *cx, unsigned argc, JS::Value *vp) { METHOD_HEADER_WITH_NAME(1, "delete") + + if (!prepare_for_entries_modification(cx, self)) { + return false; + } + NORMALIZE_NAME(args[0], "Headers.delete") Mode mode = Headers::mode(self); if (mode == Mode::HostOnly) { @@ -458,18 +719,19 @@ bool Headers::delete_(JSContext *cx, unsigned argc, JS::Value *vp) { HANDLE_ERROR(cx, *err); return false; } - } else if (mode == Mode::CachedInContent) { - switch_to_content_only_mode(self); - } - - if (mode == Mode::ContentOnly) { + } else { + MOZ_ASSERT(mode == Mode::ContentOnly); RootedObject entries(cx, get_entries(cx, self)); if (!entries) { return false; } + RootedValue name_val(cx); + if (!redecode_str_if_changed(cx, args[0], name_chars, name_changed, &name_val)) { + return false; + } bool had; - return MapDelete(cx, entries, args[0], &had); + return MapDelete(cx, entries, name_val, &had); } @@ -564,8 +826,12 @@ const JSPropertySpec Headers::properties[] = { bool Headers::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { CTOR_HEADER("Headers", 0); - JS::RootedObject headersInstance(cx, JS_NewObjectForConstructor(cx, &class_, args)); - JS::RootedObject headers(cx, create(cx, headersInstance, nullptr, args.get(0))); + HandleValue headersInit = args.get(0); + RootedObject headersInstance(cx, JS_NewObjectForConstructor(cx, &class_, args)); + if (!headersInstance) { + return false; + } + JS::RootedObject headers(cx, create(cx, headersInstance, headersInit)); if (!headers) { return false; } @@ -594,55 +860,29 @@ bool Headers::init_class(JSContext *cx, JS::HandleObject global) { return JS_DefinePropertyById(cx, proto_obj, iteratorId, entries, 0); } -JSObject *Headers::create(JSContext *cx, HandleObject self, host_api::HttpHeadersReadOnly *handle) { - SetReservedSlot(self, static_cast(Slots::Handle), PrivateValue(handle)); - SetReservedSlot(self, static_cast(Slots::Mode), - JS::Int32Value(static_cast(Mode::HostOnly))); - return self; -} JSObject *Headers::get_entries(JSContext *cx, HandleObject self) { MOZ_ASSERT(is_instance(self)); - if (mode(self) != Mode::HostOnly) { - return &GetReservedSlot(self, static_cast(Slots::Entries)).toObject(); - } - - auto handle = get_handle(self); - MOZ_ASSERT(handle); - auto res = handle->entries(); - if (res.is_err()) { - HANDLE_ERROR(cx, *res.to_err()); + if (mode(self) == Mode::HostOnly && !switch_mode(cx, self, Mode::CachedInContent)) { return nullptr; } - RootedObject map(cx, JS::NewMapObject(cx)); - if (!map) { + return &GetReservedSlot(self, static_cast(Slots::Entries)).toObject(); +} + +unique_ptr Headers::handle_clone(JSContext* cx, HandleObject self) { + auto mode = Headers::mode(self); + if (mode == Mode::ContentOnly && !switch_mode(cx, self, Mode::CachedInContent)) { + // Switch to Mode::CachedInContent to ensure that the latest data is available on the handle, + // but without discarding the existing entries, in case content reads them later. return nullptr; } - RootedString key(cx); - RootedValue key_val(cx); - RootedString value(cx); - RootedValue value_val(cx); - for (auto& entry : std::move(res.unwrap())) { - key = core::decode(cx, std::get<0>(entry)); - if (!key) { - return nullptr; - } - value = core::decode(cx, std::get<1>(entry)); - if (!value) { - return nullptr; - } - key_val.setString(key); - value_val.setString(value); - if (!MapSet(cx, map, key_val, value_val)) { - return nullptr; - } + auto handle = unique_ptr(get_handle(self)->clone()); + if (!handle) { + JS_ReportErrorASCII(cx, "Failed to clone headers"); + return nullptr; } - - SetReservedSlot(self, static_cast(Slots::Entries), ObjectValue(*map)); - SetReservedSlot(self, static_cast(Slots::Mode), - JS::Int32Value(static_cast(Mode::CachedInContent))); - return map; + return handle; } } // namespace builtins::web::fetch diff --git a/builtins/web/fetch/headers.h b/builtins/web/fetch/headers.h index d9a4404..e9a946f 100644 --- a/builtins/web/fetch/headers.h +++ b/builtins/web/fetch/headers.h @@ -22,16 +22,41 @@ class Headers final : public BuiltinImpl { public: static constexpr const char *class_name = "Headers"; - /// Usually, headers are stored on the host only, including when they're created in-content. - /// However, iterating over headers (as keys, values, or entries) would be extremely slow if - /// we retrieved all of them from the host for each iteration step. - /// So, when we start iterating, we retrieve them once and store them in a Map as a cache. - /// If, while iterating, a header is added, deleted, or replaced, we start treating the Map as - /// the canonical store for headers, and discard the underlying resource handle entirely. + /// Headers instances can be in one of three modes: + /// - `HostOnly`: Headers are stored in the host only. + /// - `CachedInContent`: Host holds canonical headers, content a cached copy. + /// - `ContentOnly`: Headers are stored in a Map held by the `Entries` slot. + /// + /// For Headers instances created in-content, the mode is determined by the `HeadersInit` + /// argument: + /// - If `HeadersInit` is a `Headers` instance, the mode is inherited from that instance, + /// as is the underlying data. + /// - If `HeadersInit` is empty or a sequence of header name/value pairs, the mode is + /// `ContentOnly`. + /// + /// The mode of Headers instances created via the `headers` accessor on `Request` and `Response` + /// instances is determined by how those instances themselves were created: + /// - If a `Request` or `Response` instance represents an incoming request or response, the mode + /// will initially be `HostOnly`. + /// - If a `Request` or `Response` instance represents an outgoing request or response, the mode + /// of the `Headers` instance depends on the `HeadersInit` argument passed to the `Request` or + /// `Response` constructor (see above). + /// + /// A `Headers` instance can transition from `HostOnly` to `CachedInContent` or `ContentOnly` + /// mode: + /// Iterating over headers (as keys, values, or entries) would be extremely slow if we retrieved + /// all of them from the host for each iteration step. + /// Instead, when iterating over the headers of a `HostOnly` mode `Headers` instance, the instance + /// is transitioned to `CachedInContent` mode, and the entries are stored in a Map in the + /// `Entries` slot. + /// + /// If a header is added, deleted, or replaced on an instance in `CachedInContent` mode, the + /// instance transitions to `ContentOnly` mode, and the underlying resource handle is discarded. enum class Mode { HostOnly, // Headers are stored in the host. CachedInContent, // Host holds canonical headers, content a cached copy. ContentOnly, // Headers are stored in a Map held by the `Entries` slot. + Uninitialized, // Headers have not been initialized. }; enum class Slots { @@ -45,7 +70,7 @@ class Headers final : public BuiltinImpl { * Adds the given header name/value to `self`'s list of headers iff `self` * doesn't already contain a header with that name. */ - static bool maybe_add(JSContext *cx, JS::HandleObject self, const char *name, const char *value); + static bool set_if_undefined(JSContext *cx, JS::HandleObject self, const char *name, const char *value); /// Appends a value for a header name. // @@ -55,7 +80,11 @@ class Headers final : public BuiltinImpl { static Mode mode(JSObject* self) { MOZ_ASSERT(Headers::is_instance(self)); - return static_cast(JS::GetReservedSlot(self, static_cast(Slots::Mode)).toInt32()); + Value modeVal = JS::GetReservedSlot(self, static_cast(Slots::Mode)); + if (modeVal.isUndefined()) { + return Mode::Uninitialized; + } + return static_cast(modeVal.toInt32()); } static const JSFunctionSpec static_methods[]; @@ -68,15 +97,25 @@ class Headers final : public BuiltinImpl { static bool init_class(JSContext *cx, HandleObject global); static bool constructor(JSContext *cx, unsigned argc, Value *vp); - static JSObject *create(JSContext *cx, HandleObject self, host_api::HttpHeaders *handle, - HandleValue init_headers); - static JSObject *create(JSContext *cx, HandleObject self, host_api::HttpHeadersReadOnly *handle); + static JSObject *create(JSContext *cx, HandleValue init_headers); + static JSObject *create(JSContext *cx, HandleObject self, HandleValue init_headers); + static JSObject *create(JSContext *cx, host_api::HttpHeadersReadOnly *handle); /// Returns a Map object containing the headers. /// /// Depending on the `Mode` the instance is in, this can be a cache or the canonical store for /// the headers. static JSObject* get_entries(JSContext *cx, HandleObject self); + + /** + * Returns a cloned handle representing the contents of this Headers object. + * + * The main purposes for this function are use in sending outgoing requests/responses and + * in the constructor of request/response objects when a HeadersInit object is passed. + * + * The handle is guaranteed to be uniquely owned by the caller. + */ + static unique_ptr handle_clone(JSContext*, HandleObject self); }; } // namespace fetch diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index 5ce5901..c6f70b2 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -127,66 +127,6 @@ class BodyFutureTask final : public api::AsyncTask { void trace(JSTracer *trc) override { TraceEdge(trc, &body_source_, "body source for future"); } }; -class ResponseFutureTask final : public api::AsyncTask { - Heap request_; - host_api::FutureHttpIncomingResponse *future_; - -public: - explicit ResponseFutureTask(const HandleObject request, - host_api::FutureHttpIncomingResponse *future) - : request_(request), future_(future) { - auto res = future->subscribe(); - MOZ_ASSERT(!res.is_err(), "Subscribing to a future should never fail"); - handle_ = res.unwrap(); - } - - ResponseFutureTask(const RootedObject &rooted); - - [[nodiscard]] bool run(api::Engine *engine) override { - // MOZ_ASSERT(ready()); - JSContext *cx = engine->cx(); - - const RootedObject request(cx, request_); - RootedObject response_promise(cx, Request::response_promise(request)); - - auto res = future_->maybe_response(); - if (res.is_err()) { - JS_ReportErrorUTF8(cx, "NetworkError when attempting to fetch resource."); - return RejectPromiseWithPendingError(cx, response_promise); - } - - auto maybe_response = res.unwrap(); - MOZ_ASSERT(maybe_response.has_value()); - auto response = maybe_response.value(); - RootedObject response_obj( - cx, JS_NewObjectWithGivenProto(cx, &Response::class_, Response::proto_obj)); - if (!response_obj) { - return false; - } - - response_obj = Response::create(cx, response_obj, response); - if (!response_obj) { - return false; - } - - RequestOrResponse::set_url(response_obj, RequestOrResponse::url(request)); - RootedValue response_val(cx, ObjectValue(*response_obj)); - if (!ResolvePromise(cx, response_promise, response_val)) { - return false; - } - - return cancel(engine); - } - - [[nodiscard]] bool cancel(api::Engine *engine) override { - // TODO(TS): implement - handle_ = -1; - return true; - } - - void trace(JSTracer *trc) override { TraceEdge(trc, &request_, "Request for response future"); } -}; - namespace { // https://fetch.spec.whatwg.org/#concept-method-normalize // Returns `true` if the method name was normalized, `false` otherwise. @@ -225,7 +165,7 @@ bool RequestOrResponse::is_instance(JSObject *obj) { return Request::is_instance(obj) || Response::is_instance(obj); } -bool RequestOrResponse::is_incoming(JSObject *obj) { return handle(obj)->is_incoming(); } +bool RequestOrResponse::is_incoming(JSObject *obj) { return !!handle(obj); } host_api::HttpHeadersReadOnly *RequestOrResponse::headers_handle(JSObject *obj) { MOZ_ASSERT(is_instance(obj)); @@ -413,8 +353,9 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self, content_type = "text/plain;charset=UTF-8"; } - auto body = RequestOrResponse::outgoing_body_handle(self); - auto write_res = body->write_all(reinterpret_cast(buf), length); + // TODO: restore + // auto body = RequestOrResponse::outgoing_body_handle(self); + // auto write_res = body->write_all(reinterpret_cast(buf), length); // Ensure that the NoGC is reset, so throwing an error in HANDLE_ERROR // succeeds. @@ -422,16 +363,16 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self, maybeNoGC.reset(); } - if (auto *err = write_res.to_err()) { - HANDLE_ERROR(cx, *err); - return false; - } + // if (auto *err = write_res.to_err()) { + // HANDLE_ERROR(cx, *err); + // return false; + // } } // Step 36.3 of Request constructor / 8.4 of Response constructor. if (content_type) { JS::RootedObject headers(cx, RequestOrResponse::headers(cx, self)); - if (!Headers::maybe_add(cx, headers, "content-type", content_type)) { + if (!Headers::set_if_undefined(cx, headers, "content-type", content_type)) { return false; } } @@ -445,6 +386,23 @@ JSObject *RequestOrResponse::maybe_headers(JSObject *obj) { return JS::GetReservedSlot(obj, static_cast(Slots::Headers)).toObjectOrNull(); } +unique_ptr RequestOrResponse::headers_clone(JSContext* cx, + HandleObject self) { + MOZ_ASSERT(is_instance(self)); + + RootedObject headers(cx, maybe_headers(self)); + if (headers) { + return Headers::handle_clone(cx, headers); + } + + auto res = handle(self)->headers(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return nullptr; + } + return unique_ptr(res.unwrap()->clone()); +} + bool RequestOrResponse::append_body(JSContext *cx, JS::HandleObject self, JS::HandleObject source) { MOZ_ASSERT(!body_used(source)); MOZ_ASSERT(!body_used(self)); @@ -469,18 +427,9 @@ bool RequestOrResponse::append_body(JSContext *cx, JS::HandleObject self, JS::Ha JSObject *RequestOrResponse::headers(JSContext *cx, JS::HandleObject obj) { JSObject *headers = maybe_headers(obj); if (!headers) { - JS::RootedObject headersInstance( - cx, JS_NewObjectWithGivenProto(cx, &Headers::class_, Headers::proto_obj)); - - if (!headersInstance) { - return nullptr; - } - - auto *headers_handle = RequestOrResponse::headers_handle(obj); - if (!headers_handle) { - headers_handle = new host_api::HttpHeaders(); + if (auto *headers_handle = RequestOrResponse::headers_handle(obj)) { + headers = Headers::create(cx, headers_handle); } - headers = Headers::create(cx, headersInstance, headers_handle); if (!headers) { return nullptr; } @@ -1032,6 +981,8 @@ bool RequestOrResponse::body_reader_catch_handler(JSContext *cx, JS::HandleObjec bool RequestOrResponse::maybe_stream_body(JSContext *cx, JS::HandleObject body_owner, bool *requires_streaming) { + *requires_streaming = false; + if (is_incoming(body_owner) && has_body(body_owner)) { *requires_streaming = true; return true; @@ -1058,6 +1009,8 @@ bool RequestOrResponse::maybe_stream_body(JSContext *cx, JS::HandleObject body_o // If the body stream is backed by an HTTP body handle, we can directly pipe // that handle into the body we're about to send. + // TODO: check if this case can still be hit. Given that we're not creating outgoing responses + // anymore until sending them, it probably can't. if (streams::NativeStreamSource::stream_is_body(cx, stream)) { MOZ_ASSERT(!is_incoming(body_owner)); // First, directly append the source's body to the target's and lock the stream. @@ -1400,10 +1353,8 @@ bool Request::init_class(JSContext *cx, JS::HandleObject global) { return !!GET_atom; } -JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance, - host_api::HttpRequest *request_handle) { - JS::SetReservedSlot(requestInstance, static_cast(Slots::Request), - JS::PrivateValue(request_handle)); +JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance) { + JS::SetReservedSlot(requestInstance, static_cast(Slots::Request), JS::PrivateValue(nullptr)); JS::SetReservedSlot(requestInstance, static_cast(Slots::Headers), JS::NullValue()); JS::SetReservedSlot(requestInstance, static_cast(Slots::BodyStream), JS::NullValue()); JS::SetReservedSlot(requestInstance, static_cast(Slots::HasBody), JS::FalseValue()); @@ -1697,7 +1648,6 @@ JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance, JS::H // `init["headers"]` exists, create the request's `headers` from that, // otherwise create it from the `init` object's `headers`, or create a new, // empty one. - auto *headers_handle = new host_api::HttpHeaders(); JS::RootedObject headers(cx); if (headers_val.isUndefined() && input_headers) { @@ -1709,7 +1659,7 @@ JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance, JS::H if (!headersInstance) return nullptr; - headers = Headers::create(cx, headersInstance, headers_handle, headers_val); + headers = Headers::create(cx, headersInstance, headers_val); if (!headers) { return nullptr; } @@ -1744,8 +1694,8 @@ JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance, JS::H // Actually create the instance, now that we have all the parts required for // it. We have to delay this step to here because the wasi-http API requires // that all the request's properties are provided to the constructor. - auto request_handle = host_api::HttpOutgoingRequest::make(method, std::move(url), headers_handle); - RootedObject request(cx, create(cx, requestInstance, request_handle)); + // auto request_handle = host_api::HttpOutgoingRequest::make(method, std::move(url), headers); + RootedObject request(cx, create(cx, requestInstance)); if (!request) { return nullptr; } @@ -1837,8 +1787,7 @@ JSObject *Request::create_instance(JSContext *cx) { } bool Request::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { - REQUEST_HANDLER_ONLY("The Request builtin"); - CTOR_HEADER("Request", 1); + CTOR_HEADER("Reques", 1); JS::RootedObject requestInstance(cx, JS_NewObjectForConstructor(cx, &class_, args)); JS::RootedObject request(cx, create(cx, requestInstance, args[0], args.get(1))); if (!request) @@ -1855,9 +1804,9 @@ static_assert((int)Response::Slots::BodyUsed == (int)Request::Slots::BodyUsed); static_assert((int)Response::Slots::Headers == (int)Request::Slots::Headers); static_assert((int)Response::Slots::Response == (int)Request::Slots::Request); -host_api::HttpResponse *Response::response_handle(JSObject *obj) { +host_api::HttpIncomingResponse *Response::response_handle(JSObject *obj) { MOZ_ASSERT(is_instance(obj)); - return static_cast( + return static_cast( JS::GetReservedSlot(obj, static_cast(Slots::Response)).toPrivate()); } @@ -2452,8 +2401,6 @@ const JSPropertySpec Response::properties[] = { * The `Response` constructor https://fetch.spec.whatwg.org/#dom-response */ bool Response::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { - REQUEST_HANDLER_ONLY("The Response builtin"); - CTOR_HEADER("Response", 0); JS::RootedValue body_val(cx, args.get(0)); @@ -2520,19 +2467,16 @@ bool Response::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { if (!headersInstance) return false; - auto *headers_handle = new host_api::HttpHeaders(); - headers = Headers::create(cx, headersInstance, headers_handle, headers_val); + headers = Headers::create(cx, headersInstance, headers_val); if (!headers) { return false; } - auto *response_handle = host_api::HttpOutgoingResponse::make(status, headers_handle); - JS::RootedObject responseInstance(cx, JS_NewObjectForConstructor(cx, &class_, args)); if (!responseInstance) { return false; } - JS::RootedObject response(cx, create(cx, responseInstance, response_handle)); + JS::RootedObject response(cx, create(cx, responseInstance)); if (!response) { return false; } @@ -2585,13 +2529,15 @@ bool Response::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { if (!RequestOrResponse::extract_body(cx, response, body_val)) { return false; } - if (RequestOrResponse::has_body(response)) { - if (response_handle->body().is_err()) { - auto err = response_handle->body().to_err(); - HANDLE_ERROR(cx, *err); - return false; - } - } + + // TODO: presumably remove this code, but check what it's for first + // if (RequestOrResponse::has_body(response)) { + // if (response_handle->body().is_err()) { + // auto err = response_handle->body().to_err(); + // HANDLE_ERROR(cx, *err); + // return false; + // } + // } } args.rval().setObject(*response); @@ -2609,33 +2555,39 @@ bool Response::init_class(JSContext *cx, JS::HandleObject global) { (type_error_atom = JS_AtomizeAndPinString(cx, "error")); } -JSObject *Response::create(JSContext *cx, JS::HandleObject response, - host_api::HttpResponse *response_handle) { +JSObject *Response::create(JSContext *cx, JS::HandleObject response) { MOZ_ASSERT(cx); MOZ_ASSERT(is_instance(response)); - MOZ_ASSERT(response_handle); - JS::SetReservedSlot(response, static_cast(Slots::Response), - JS::PrivateValue(response_handle)); + JS::SetReservedSlot(response, static_cast(Slots::Response), JS::PrivateValue(nullptr)); JS::SetReservedSlot(response, static_cast(Slots::Headers), JS::NullValue()); JS::SetReservedSlot(response, static_cast(Slots::BodyStream), JS::NullValue()); JS::SetReservedSlot(response, static_cast(Slots::HasBody), JS::FalseValue()); JS::SetReservedSlot(response, static_cast(Slots::BodyUsed), JS::FalseValue()); JS::SetReservedSlot(response, static_cast(Slots::Redirected), JS::FalseValue()); - if (response_handle->is_incoming()) { - auto res = reinterpret_cast(response_handle)->status(); - MOZ_ASSERT(!res.is_err(), "TODO: proper error handling"); - auto status = res.unwrap(); - JS::SetReservedSlot(response, static_cast(Slots::Status), JS::Int32Value(status)); - set_status_message_from_code(cx, response, status); + return response; +} - if (!(status == 204 || status == 205 || status == 304)) { - JS::SetReservedSlot(response, static_cast(Slots::HasBody), JS::TrueValue()); - } +JSObject * Response::create_incoming(JSContext *cx, HandleObject obj, host_api::HttpIncomingResponse *response) { + RootedObject self(cx, create(cx, obj)); + if (!self) { + return nullptr; } - return response; + JS::SetReservedSlot(self, static_cast(Slots::Response), PrivateValue(response)); + + auto res = response->status(); + MOZ_ASSERT(!res.is_err(), "TODO: proper error handling"); + auto status = res.unwrap(); + JS::SetReservedSlot(self, static_cast(Slots::Status), JS::Int32Value(status)); + set_status_message_from_code(cx, self, status); + + if (!(status == 204 || status == 205 || status == 304)) { + JS::SetReservedSlot(self, static_cast(Slots::HasBody), JS::TrueValue()); + } + + return self; } namespace request_response { diff --git a/builtins/web/fetch/request-response.h b/builtins/web/fetch/request-response.h index be2f4fb..5f435b5 100644 --- a/builtins/web/fetch/request-response.h +++ b/builtins/web/fetch/request-response.h @@ -50,6 +50,16 @@ class RequestOrResponse final { */ static JSObject *maybe_headers(JSObject *obj); + /** + * Returns a handle to a clone of the RequestOrResponse's Headers. + * + * The main purposes for this function are use in sending outgoing requests/responses and + * in the constructor of request/response objects when a HeadersInit object is passed. + * + * The handle is guaranteed to be uniquely owned by the caller. + */ + static unique_ptr headers_clone(JSContext *, HandleObject self); + /** * Returns the RequestOrResponse's Headers, reifying it if necessary. */ @@ -148,8 +158,7 @@ class Request final : public BuiltinImpl { static bool init_class(JSContext *cx, JS::HandleObject global); static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); - static JSObject *create(JSContext *cx, JS::HandleObject requestInstance, - host_api::HttpRequest *request_handle); + static JSObject *create(JSContext *cx, JS::HandleObject requestInstance); static JSObject *create(JSContext *cx, JS::HandleObject requestInstance, JS::HandleValue input, JS::HandleValue init_val); @@ -198,10 +207,11 @@ class Response final : public BuiltinImpl { static bool init_class(JSContext *cx, JS::HandleObject global); static bool constructor(JSContext *cx, unsigned argc, JS::Value *vp); - static JSObject *create(JSContext *cx, JS::HandleObject response, - host_api::HttpResponse *response_handle); + static JSObject *create(JSContext *cx, JS::HandleObject response); + static JSObject* create_incoming(JSContext * cx, HandleObject self, + host_api::HttpIncomingResponse* response); - static host_api::HttpResponse *response_handle(JSObject *obj); + static host_api::HttpIncomingResponse *response_handle(JSObject *obj); static uint16_t status(JSObject *obj); static JSString *status_message(JSObject *obj); static void set_status_message_from_code(JSContext *cx, JSObject *obj, uint16_t code); diff --git a/builtins/web/url.cpp b/builtins/web/url.cpp index 90de01e..b326a53 100644 --- a/builtins/web/url.cpp +++ b/builtins/web/url.cpp @@ -635,9 +635,10 @@ JSObject *URL::create(JSContext *cx, JS::HandleObject self, JS::HandleValue url_ JSObject *URL::create(JSContext *cx, JS::HandleObject self, JS::HandleValue url_val, JS::HandleObject base_obj) { - MOZ_RELEASE_ASSERT(is_instance(base_obj)); - const auto *base = - static_cast(JS::GetReservedSlot(base_obj, Slots::Url).toPrivate()); + jsurl::JSUrl* base = nullptr; + if (is_instance(base_obj)) { + base = static_cast(JS::GetReservedSlot(base_obj, Slots::Url).toPrivate()); + } return create(cx, self, url_val, base); } diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index ecf3a9f..e036336 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -2,6 +2,8 @@ #include "bindings/bindings.h" #include +#include +#include using std::optional; using std::string_view; @@ -17,6 +19,7 @@ typedef wasi_http_0_2_0_types_own_incoming_request_t incoming_request_t; typedef wasi_http_0_2_0_types_borrow_incoming_request_t borrow_incoming_request_t; typedef wasi_http_0_2_0_types_own_incoming_response_t incoming_response_t; typedef wasi_http_0_2_0_types_borrow_outgoing_request_t borrow_outgoing_request_t; +typedef wasi_http_0_2_0_types_borrow_outgoing_response_t borrow_outgoing_response_t; typedef wasi_http_0_2_0_types_own_future_incoming_response_t future_incoming_response_t; typedef wasi_http_0_2_0_types_borrow_future_incoming_response_t borrow_future_incoming_response_t; @@ -39,8 +42,72 @@ typedef wasi_io_0_2_0_streams_borrow_input_stream_t borrow_input_stream_t; typedef wasi_io_0_2_0_streams_own_output_stream_t own_output_stream_t; +/// The type of handles used by the host interface. +typedef int32_t Handle; +constexpr Handle UNINITIALIZED_HANDLE = -1; + +/// An abstract base class to be used in classes representing host resources. +/// +/// Some host resources have different requirements for their client-side representation +/// depending on the host API. To accommodate this, we introduce a base class to use for +/// all of them, which the API-specific implementation can subclass as needed. +class host_api::HandleState { + Handle handle_ = UNINITIALIZED_HANDLE; + +#ifdef DEBUG + #define ASSERT_VALID()\ + MOZ_ASSERT(handle_state_.get() != nullptr, "Handle state missing"); \ + MOZ_ASSERT(handle_state_.get()->initialized(), "Handle not initialized"); \ + MOZ_ASSERT(!handle_state_.get()->poisoned(), "Handle poisoned"); + bool poisoned_ = false; +#else +#define ASSERT_VALID +#endif + +public: + HandleState() {} + explicit HandleState(Handle handle) : handle_{handle} { + DBG("Adding handle %d\n", handle); + // MOZ_ASSERT(handle > UNINITIALIZED_HANDLE); + // MOZ_ASSERT(!used_handles.has(handle)); + // MOZ_ASSERT(used_handles.put(handle)); + } + virtual ~HandleState() { + // TODO: support dropping handles for all handle types. Probably using a template class. + DBG("Removing handle %d\n", handle_); +#ifdef DEBUG + // MOZ_ASSERT(used_handles.has(handle)); + // used_handles.remove(handle); +#endif + } + + Handle get() const { + DBG("Getting handle %d\n", handle_); + MOZ_ASSERT(handle_ != UNINITIALIZED_HANDLE, "Handle is uninitialized"); +#ifdef DEBUG + if (poisoned_) { + fprintf(stderr, "Handle %d is poisened", handle_); fflush(stderr); + MOZ_ASSERT(false); + } +#endif + return handle_; + } + + Handle take() { + auto handle = get(); + DBG("Consuming handle %d\n", handle); + poisoned_ = true; + return handle; + } + + bool valid() const { return handle_ != UNINITIALIZED_HANDLE && !poisoned_; } + bool initialized() const { return handle_ != UNINITIALIZED_HANDLE;} + bool poisoned() const { return poisoned_; } +}; + namespace { +// TODO: merge HandleOps into a WASIHandleState subclass of HandleState, and use that everywhere. /// This is the type contract for using the Own and Borrow templates. template struct HandleOps {}; @@ -57,10 +124,12 @@ template class Borrow final { Borrow(HandleOps::own handle) : handle{HandleOps::borrow_owned(handle)} {} // Construct a borrow from a raw `Handle` value. - Borrow(host_api::Handle handle) : Borrow{typename HandleOps::own{handle}} {} + Borrow(Handle handle) : Borrow{typename HandleOps::own{handle}} { + MOZ_ASSERT(valid()); + } // Convenience wrapper for constructing a borrow of a HandleState. - Borrow(host_api::HandleState *state) : Borrow{typename HandleOps::own{state->handle}} {} + Borrow(host_api::HandleState *state) : Borrow{typename HandleOps::own{state->get()}} {} bool valid() const { return this->handle.__handle != Borrow::invalid.__handle; } @@ -185,19 +254,17 @@ void MonotonicClock::unsubscribe(const int32_t handle_id) { wasi_io_0_2_0_poll_pollable_drop_own(own_pollable_t{handle_id}); } -HttpHeaders::HttpHeaders(Handle handle) : HttpHeadersReadOnly(handle) {} +HttpHeaders::HttpHeaders(std::unique_ptr state) : HttpHeadersReadOnly(std::move(state)) {} -HttpHeaders::HttpHeaders() : HttpHeadersReadOnly() { - this->handle_state_ = new HandleState(wasi_http_0_2_0_types_constructor_fields().__handle); +HttpHeaders::HttpHeaders() : HttpHeadersReadOnly(std::make_unique(wasi_http_0_2_0_types_constructor_fields().__handle)) { } -Result HttpHeaders::FromEntries(const vector>> &entries) { +Result HttpHeaders::FromEntries(const vector> &entries) { std::vector pairs; + pairs.reserve(entries.size()); - for (const auto &[name, values] : entries) { - for (const auto &value : values) { - pairs.emplace_back(from_string_view(name), from_string_view(value)); - } + for (const auto &[name, value] : entries) { + pairs.emplace_back(from_string_view(name), from_string_view(value)); } wasi_http_0_2_0_types_list_tuple2_field_key_field_value_t tuples{pairs.data(), entries.size()}; @@ -207,13 +274,13 @@ Result HttpHeaders::FromEntries(const vector::ok(new HttpHeaders(ret.__handle)); + return Result::ok(new HttpHeaders(std::make_unique(ret.__handle))); } -HttpHeaders::HttpHeaders(const HttpHeadersReadOnly &headers) : HttpHeadersReadOnly() { - Borrow borrow(headers.handle_state_); +HttpHeaders::HttpHeaders(const HttpHeadersReadOnly &headers) : HttpHeadersReadOnly(nullptr) { + Borrow borrow(headers.handle_state_.get()); auto handle = wasi_http_0_2_0_types_method_fields_clone(borrow); - this->handle_state_ = new HandleState(handle.__handle); + this->handle_state_ = std::unique_ptr(new HandleState(handle.__handle)); } HttpHeaders *HttpHeadersReadOnly::clone() { @@ -222,10 +289,10 @@ HttpHeaders *HttpHeadersReadOnly::clone() { Result>> HttpHeadersReadOnly::entries() const { Result>> res; - MOZ_ASSERT(valid()); + ASSERT_VALID(); wasi_http_0_2_0_types_list_tuple2_field_key_field_value_t entries; - Borrow borrow(this->handle_state_); + Borrow borrow(this->handle_state_.get()); wasi_http_0_2_0_types_method_fields_entries(borrow, &entries); vector> entries_vec; @@ -243,10 +310,10 @@ Result>> HttpHeadersReadOnly::entries() con Result> HttpHeadersReadOnly::names() const { Result> res; - MOZ_ASSERT(valid()); + ASSERT_VALID(); wasi_http_0_2_0_types_list_tuple2_field_key_field_value_t entries; - Borrow borrow(this->handle_state_); + Borrow borrow(this->handle_state_.get()); wasi_http_0_2_0_types_method_fields_entries(borrow, &entries); vector names; @@ -262,11 +329,11 @@ Result> HttpHeadersReadOnly::names() const { Result>> HttpHeadersReadOnly::get(string_view name) const { Result>> res; - MOZ_ASSERT(valid()); + ASSERT_VALID(); wasi_http_0_2_0_types_list_field_value_t values; auto hdr = string_view_to_world_string(name); - Borrow borrow(this->handle_state_); + Borrow borrow(this->handle_state_.get()); wasi_http_0_2_0_types_method_fields_get(borrow, &hdr, &values); if (values.len > 0) { @@ -285,19 +352,19 @@ Result>> HttpHeadersReadOnly::get(string_view name) } Result HttpHeadersReadOnly::has(string_view name) const { - MOZ_ASSERT(valid()); + ASSERT_VALID(); auto hdr = string_view_to_world_string(name); - Borrow borrow(this->handle_state_); + Borrow borrow(this->handle_state_.get()); return Result::ok(wasi_http_0_2_0_types_method_fields_has(borrow, &hdr)); } Result HttpHeaders::set(string_view name, string_view value) { - MOZ_ASSERT(valid()); + ASSERT_VALID(); auto hdr = from_string_view(name); auto val = from_string_view(value); wasi_http_0_2_0_types_list_field_value_t host_values{&val, 1}; - Borrow borrow(this->handle_state_); + Borrow borrow(this->handle_state_.get()); wasi_http_0_2_0_types_header_error_t err; wasi_http_0_2_0_types_method_fields_set(borrow, &hdr, &host_values, &err); @@ -308,10 +375,10 @@ Result HttpHeaders::set(string_view name, string_view value) { } Result HttpHeaders::append(string_view name, string_view value) { - MOZ_ASSERT(valid()); + ASSERT_VALID(); auto hdr = from_string_view(name); auto val = from_string_view(value); - Borrow borrow(this->handle_state_); + Borrow borrow(this->handle_state_.get()); // TODO: properly handle `err` wasi_http_0_2_0_types_header_error_t err; @@ -321,7 +388,7 @@ Result HttpHeaders::append(string_view name, string_view value) { case WASI_HTTP_0_2_0_TYPES_HEADER_ERROR_FORBIDDEN: return Result::err(154); case WASI_HTTP_0_2_0_TYPES_HEADER_ERROR_IMMUTABLE: - fprintf(stderr, "Headers %d should not be immutable", this->handle_state_->handle); + fprintf(stderr, "Headers %d should not be immutable", this->handle_state_->get()); MOZ_ASSERT_UNREACHABLE(); } } @@ -330,9 +397,9 @@ Result HttpHeaders::append(string_view name, string_view value) { } Result HttpHeaders::remove(string_view name) { - MOZ_ASSERT(valid()); + ASSERT_VALID(); auto hdr = string_view_to_world_string(name); - Borrow borrow(this->handle_state_); + Borrow borrow(this->handle_state_.get()); wasi_http_0_2_0_types_header_error_t err; wasi_http_0_2_0_types_method_fields_delete(borrow, &hdr, &err); @@ -348,7 +415,7 @@ string_view HttpRequestResponseBase::url() { return string_view(*_url); } - auto borrow = borrow_incoming_request_t{handle_state_->handle}; + auto borrow = borrow_incoming_request_t{handle_state_->get()}; wasi_http_0_2_0_types_scheme_t scheme; bool success; @@ -380,7 +447,7 @@ bool write_to_outgoing_body(Borrow borrow, const uint8_t *ptr, con return wasi_io_0_2_0_streams_method_output_stream_write(borrow, &list, &err); } -class OutgoingBodyHandleState final : HandleState { +class OutgoingBodyHandleState final : public HandleState { Handle stream_handle_; PollableHandle pollable_handle_; @@ -398,8 +465,8 @@ class OutgoingBodyHandleState final : HandleState { } }; -HttpOutgoingBody::HttpOutgoingBody(Handle handle) : Pollable() { - handle_state_ = new OutgoingBodyHandleState(handle); +HttpOutgoingBody::HttpOutgoingBody(std::unique_ptr state) : Pollable() { + handle_state_ = std::move(state); } Result HttpOutgoingBody::capacity() { if (!valid()) { @@ -407,7 +474,7 @@ Result HttpOutgoingBody::capacity() { return Result::err(154); } - auto *state = static_cast(this->handle_state_); + auto *state = static_cast(this->handle_state_.get()); Borrow borrow(state->stream_handle_); uint64_t capacity = 0; wasi_io_0_2_0_streams_stream_error_t err; @@ -426,7 +493,7 @@ Result HttpOutgoingBody::write(const uint8_t *bytes, size_t len) { auto capacity = res.unwrap(); auto bytes_to_write = std::min(len, static_cast(capacity)); - auto *state = static_cast(this->handle_state_); + auto *state = static_cast(this->handle_state_.get()); Borrow borrow(state->stream_handle_); if (!write_to_outgoing_body(borrow, bytes, bytes_to_write)) { return Result::err(154); @@ -441,7 +508,7 @@ Result HttpOutgoingBody::write_all(const uint8_t *bytes, size_t len) { return Result::err({}); } - auto *state = static_cast(handle_state_); + auto *state = static_cast(handle_state_.get()); Borrow borrow(state->stream_handle_); while (len > 0) { @@ -594,15 +661,15 @@ class BodyAppendTask final : public api::AsyncTask { }; Result HttpOutgoingBody::append(api::Engine *engine, HttpIncomingBody *other) { - MOZ_ASSERT(valid()); + ASSERT_VALID(); engine->queue_async_task(new BodyAppendTask(other, this)); return {}; } Result HttpOutgoingBody::close() { - MOZ_ASSERT(valid()); + ASSERT_VALID(); - auto state = static_cast(handle_state_); + auto state = static_cast(handle_state_.get()); // A blocking flush is required here to ensure that all buffered contents are // actually written before finishing the body. Borrow borrow{state->stream_handle_}; @@ -621,17 +688,14 @@ Result HttpOutgoingBody::close() { { wasi_http_0_2_0_types_error_code_t err; - wasi_http_0_2_0_types_static_outgoing_body_finish({state->handle}, nullptr, &err); + wasi_http_0_2_0_types_static_outgoing_body_finish({state->take()}, nullptr, &err); // TODO: handle `err` } - delete handle_state_; - handle_state_ = nullptr; - return {}; } Result HttpOutgoingBody::subscribe() { - auto state = static_cast(handle_state_); + auto state = static_cast(handle_state_.get()); if (state->pollable_handle_ == INVALID_POLLABLE_HANDLE) { Borrow borrow(state->stream_handle_); state->pollable_handle_ = wasi_io_0_2_0_streams_method_output_stream_subscribe(borrow).__handle; @@ -640,7 +704,7 @@ Result HttpOutgoingBody::subscribe() { } void HttpOutgoingBody::unsubscribe() { - auto state = static_cast(handle_state_); + auto state = static_cast(handle_state_.get()); if (state->pollable_handle_ == INVALID_POLLABLE_HANDLE) { return; } @@ -670,10 +734,10 @@ wasi_http_0_2_0_types_method_t http_method_to_host(string_view method_str) { return wasi_http_0_2_0_types_method_t{WASI_HTTP_0_2_0_TYPES_METHOD_OTHER, {val}}; } -HttpOutgoingRequest::HttpOutgoingRequest(HandleState *state) { this->handle_state_ = state; } +HttpOutgoingRequest::HttpOutgoingRequest(std::unique_ptr state) { this->handle_state_ = std::move(state); } HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional url_str, - HttpHeaders *headers) { + HttpHeadersReadOnly *headers) { bindings_string_t path_with_query; wasi_http_0_2_0_types_scheme_t scheme; bindings_string_t authority; @@ -706,8 +770,10 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< maybe_path_with_query = &path_with_query; } + Handle headers_handle = headers->handle_state_->take(); + DBG("HH: %d\n", headers_handle); auto handle = - wasi_http_0_2_0_types_constructor_outgoing_request({headers->handle_state_->handle}); + wasi_http_0_2_0_types_constructor_outgoing_request({headers_handle}); { auto borrow = wasi_http_0_2_0_types_borrow_outgoing_request(handle); @@ -727,7 +793,7 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< } auto *state = new HandleState(handle.__handle); - auto *resp = new HttpOutgoingRequest(state); + auto *resp = new HttpOutgoingRequest(std::unique_ptr(state)); resp->headers_ = headers; @@ -735,41 +801,47 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< } Result HttpOutgoingRequest::method() { - MOZ_ASSERT(valid()); - MOZ_ASSERT(headers_); + ASSERT_VALID(); return Result::ok(method_); } Result HttpOutgoingRequest::headers() { - MOZ_ASSERT(valid()); - MOZ_ASSERT(headers_); + if (!headers_) { + if (!valid()) { + return Result::err(154); + } + borrow_outgoing_request_t borrow(handle_state_->get()); + auto res = wasi_http_0_2_0_types_method_outgoing_request_headers(borrow); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(new HandleState(res.__handle))); + } + return Result::ok(headers_); } Result HttpOutgoingRequest::body() { typedef Result Res; - MOZ_ASSERT(valid()); + ASSERT_VALID(); if (!this->body_) { outgoing_body_t body; if (!wasi_http_0_2_0_types_method_outgoing_request_body( - wasi_http_0_2_0_types_borrow_outgoing_request({handle_state_->handle}), &body)) { + wasi_http_0_2_0_types_borrow_outgoing_request({handle_state_->take()}), &body)) { return Res::err(154); } - this->body_ = new HttpOutgoingBody(body.__handle); + this->body_ = new HttpOutgoingBody(std::unique_ptr(new HandleState(body.__handle))); } return Res::ok(body_); } Result HttpOutgoingRequest::send() { - MOZ_ASSERT(valid()); + ASSERT_VALID(); future_incoming_response_t ret; wasi_http_0_2_0_outgoing_handler_error_code_t err; - wasi_http_0_2_0_outgoing_handler_handle({handle_state_->handle}, nullptr, &ret, &err); - auto res = new FutureHttpIncomingResponse(ret.__handle); + wasi_http_0_2_0_outgoing_handler_handle({handle_state_->take()}, nullptr, &ret, &err); + auto res = new FutureHttpIncomingResponse(std::unique_ptr(new HandleState(ret.__handle))); return Result::ok(res); } -class IncomingBodyHandleState final : HandleState { +class IncomingBodyHandleState final : public HandleState { Handle stream_handle_; PollableHandle pollable_handle_; @@ -787,8 +859,16 @@ class IncomingBodyHandleState final : HandleState { } }; -HttpIncomingBody::HttpIncomingBody(const Handle handle) : Pollable() { - handle_state_ = new IncomingBodyHandleState(handle); +HttpIncomingBody::HttpIncomingBody(std::unique_ptr state) : Pollable() { handle_state_ = std::move(state); } + +Resource::~Resource() { + if (handle_state_ != nullptr) { + delete handle_state_.release(); + } +} + +bool Resource::valid() const { + return this->handle_state_ != nullptr && this->handle_state_->valid(); } Result HttpIncomingBody::read(uint32_t chunk_size) { @@ -797,7 +877,7 @@ Result HttpIncomingBody::read(uint32_t chunk_size) wasi_io_0_2_0_streams_list_u8_t ret{}; wasi_io_0_2_0_streams_stream_error_t err{}; auto borrow = borrow_input_stream_t( - {static_cast(handle_state_)->stream_handle_}); + {static_cast(handle_state_.get())->stream_handle_}); bool success = wasi_io_0_2_0_streams_method_input_stream_read(borrow, chunk_size, &ret, &err); if (!success) { if (err.tag == WASI_IO_0_2_0_STREAMS_STREAM_ERROR_CLOSED) { @@ -813,12 +893,12 @@ Result HttpIncomingBody::close() { return {}; } Result HttpIncomingBody::subscribe() { auto borrow = borrow_input_stream_t( - {static_cast(handle_state_)->stream_handle_}); + {static_cast(handle_state_.get())->stream_handle_}); auto pollable = wasi_io_0_2_0_streams_method_input_stream_subscribe(borrow); return Result::ok(pollable.__handle); } void HttpIncomingBody::unsubscribe() { - auto state = static_cast(handle_state_); + auto state = static_cast(handle_state_.get()); if (state->pollable_handle_ == INVALID_POLLABLE_HANDLE) { return; } @@ -826,14 +906,14 @@ void HttpIncomingBody::unsubscribe() { state->pollable_handle_ = INVALID_POLLABLE_HANDLE; } -FutureHttpIncomingResponse::FutureHttpIncomingResponse(Handle handle) { - handle_state_ = new HandleState(handle); +FutureHttpIncomingResponse::FutureHttpIncomingResponse(std::unique_ptr state) { + handle_state_ = std::move(state); } Result> FutureHttpIncomingResponse::maybe_response() { typedef Result> Res; wasi_http_0_2_0_types_result_result_own_incoming_response_error_code_void_t res; - auto borrow = wasi_http_0_2_0_types_borrow_future_incoming_response({handle_state_->handle}); + auto borrow = wasi_http_0_2_0_types_borrow_future_incoming_response({handle_state_->get()}); if (!wasi_http_0_2_0_types_method_future_incoming_response_get(borrow, &res)) { return Res::ok(std::nullopt); } @@ -846,11 +926,11 @@ Result> FutureHttpIncomingResponse::maybe_respo return Res::err(154); } - return Res::ok(new HttpIncomingResponse(val.ok.__handle)); + return Res::ok(new HttpIncomingResponse(std::unique_ptr(new HandleState(val.ok.__handle)))); } Result FutureHttpIncomingResponse::subscribe() { - auto borrow = wasi_http_0_2_0_types_borrow_future_incoming_response({handle_state_->handle}); + auto borrow = wasi_http_0_2_0_types_borrow_future_incoming_response({handle_state_->get()}); auto pollable = wasi_http_0_2_0_types_method_future_incoming_response_subscribe(borrow); return Result::ok(pollable.__handle); } @@ -858,8 +938,8 @@ void FutureHttpIncomingResponse::unsubscribe() { // TODO: implement } -HttpHeadersReadOnly::HttpHeadersReadOnly(Handle handle) { - handle_state_ = new HandleState(handle); +HttpHeadersReadOnly::HttpHeadersReadOnly(std::unique_ptr state) { + handle_state_ = std::move(state); } Result HttpIncomingResponse::status() { @@ -867,14 +947,14 @@ Result HttpIncomingResponse::status() { if (!valid()) { return Result::err(154); } - auto borrow = wasi_http_0_2_0_types_borrow_incoming_response_t({handle_state_->handle}); + auto borrow = wasi_http_0_2_0_types_borrow_incoming_response_t({handle_state_->get()}); status_ = wasi_http_0_2_0_types_method_incoming_response_status(borrow); } return Result::ok(status_); } -HttpIncomingResponse::HttpIncomingResponse(Handle handle) { - handle_state_ = new HandleState(handle); +HttpIncomingResponse::HttpIncomingResponse(std::unique_ptr state) { + handle_state_ = std::move(state); } Result HttpIncomingResponse::headers() { @@ -883,8 +963,8 @@ Result HttpIncomingResponse::headers() { return Result::err(154); } auto res = wasi_http_0_2_0_types_method_incoming_response_headers( - wasi_http_0_2_0_types_borrow_incoming_response({handle_state_->handle})); - headers_ = new HttpHeadersReadOnly(res.__handle); + wasi_http_0_2_0_types_borrow_incoming_response({handle_state_->get()})); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(new HandleState(res.__handle))); } return Result::ok(headers_); @@ -897,23 +977,24 @@ Result HttpIncomingResponse::body() { } incoming_body_t body; if (!wasi_http_0_2_0_types_method_incoming_response_consume( - wasi_http_0_2_0_types_borrow_incoming_response({handle_state_->handle}), &body)) { + wasi_http_0_2_0_types_borrow_incoming_response({handle_state_->get()}), &body)) { return Result::err(154); } - body_ = new HttpIncomingBody(body.__handle); + body_ = new HttpIncomingBody(std::unique_ptr(new HandleState(body.__handle))); } return Result::ok(body_); } -HttpOutgoingResponse::HttpOutgoingResponse(HandleState *state) { this->handle_state_ = state; } +HttpOutgoingResponse::HttpOutgoingResponse(std::unique_ptr state) { this->handle_state_ = std::move(state); } +// TODO: change this to take a unique_ptr to prevent reuse after consumption HttpOutgoingResponse *HttpOutgoingResponse::make(const uint16_t status, HttpHeaders *headers) { - wasi_http_0_2_0_types_own_headers_t owned{headers->handle_state_->handle}; + wasi_http_0_2_0_types_own_headers_t owned{headers->handle_state_->take()}; auto handle = wasi_http_0_2_0_types_constructor_outgoing_response(owned); auto borrow = wasi_http_0_2_0_types_borrow_outgoing_response(handle); auto *state = new HandleState(handle.__handle); - auto *resp = new HttpOutgoingResponse(state); + auto *resp = new HttpOutgoingResponse(std::unique_ptr(state)); // Set the status if (status != 200) { @@ -921,9 +1002,7 @@ HttpOutgoingResponse *HttpOutgoingResponse::make(const uint16_t status, HttpHead wasi_http_0_2_0_types_method_outgoing_response_set_status_code(borrow, status); } - // Freshen the headers handle to point to an immutable version of the outgoing headers. - headers->handle_state_->handle = - wasi_http_0_2_0_types_method_outgoing_response_headers(borrow).__handle; + // TODO: ensure that the passed-in headers aren't used anywhere anymore. resp->status_ = status; resp->headers_ = headers; @@ -932,43 +1011,37 @@ HttpOutgoingResponse *HttpOutgoingResponse::make(const uint16_t status, HttpHead } Result HttpOutgoingResponse::headers() { - if (!valid()) { - return Result::err(154); + if (!headers_) { + if (!valid()) { + return Result::err(154); + } + borrow_outgoing_response_t borrow(handle_state_->get()); + auto res = wasi_http_0_2_0_types_method_outgoing_response_headers(borrow); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(new HandleState(res.__handle))); } + return Result::ok(headers_); } Result HttpOutgoingResponse::body() { typedef Result Res; - MOZ_ASSERT(valid()); + ASSERT_VALID(); if (!this->body_) { outgoing_body_t body; if (!wasi_http_0_2_0_types_method_outgoing_response_body( - wasi_http_0_2_0_types_borrow_outgoing_response({handle_state_->handle}), &body)) { + wasi_http_0_2_0_types_borrow_outgoing_response({handle_state_->get()}), &body)) { return Res::err(154); } - this->body_ = new HttpOutgoingBody(body.__handle); + this->body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandleState(body.__handle))); } return Res::ok(this->body_); } Result HttpOutgoingResponse::status() { return Result::ok(status_); } -Result HttpOutgoingResponse::send(ResponseOutparam out_param) { - // Drop the headers that we eagerly grab in the factory function - wasi_http_0_2_0_types_fields_drop_own({this->headers_->handle_state_->handle}); - - wasi_http_0_2_0_types_result_own_outgoing_response_error_code_t result; - - result.is_err = false; - result.val.ok = {this->handle_state_->handle}; - - wasi_http_0_2_0_types_static_response_outparam_set({out_param}, &result); - - return {}; +HttpIncomingRequest::HttpIncomingRequest(std::unique_ptr state) { + handle_state_ = std::move(state); } -HttpIncomingRequest::HttpIncomingRequest(Handle handle) { handle_state_ = new HandleState(handle); } - Result HttpIncomingRequest::method() { if (method_.empty()) { if (!valid()) { @@ -977,7 +1050,7 @@ Result HttpIncomingRequest::method() { } wasi_http_0_2_0_types_method_t method; wasi_http_0_2_0_types_method_incoming_request_method( - borrow_incoming_request_t(handle_state_->handle), &method); + borrow_incoming_request_t(handle_state_->get()), &method); if (method.tag != WASI_HTTP_0_2_0_TYPES_METHOD_OTHER) { method_ = std::string(http_method_names[method.tag], strlen(http_method_names[method.tag])); } else { @@ -992,9 +1065,9 @@ Result HttpIncomingRequest::headers() { if (!valid()) { return Result::err(154); } - borrow_incoming_request_t borrow(handle_state_->handle); + borrow_incoming_request_t borrow(handle_state_->get()); auto res = wasi_http_0_2_0_types_method_incoming_request_headers(borrow); - headers_ = new HttpHeadersReadOnly(res.__handle); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(new HandleState(res.__handle))); } return Result::ok(headers_); @@ -1007,12 +1080,43 @@ Result HttpIncomingRequest::body() { } incoming_body_t body; if (!wasi_http_0_2_0_types_method_incoming_request_consume( - borrow_incoming_request_t(handle_state_->handle), &body)) { + borrow_incoming_request_t(handle_state_->get()), &body)) { return Result::err(154); } - body_ = new HttpIncomingBody(body.__handle); + body_ = new HttpIncomingBody(std::unique_ptr(new IncomingBodyHandleState(body.__handle))); } return Result::ok(body_); } } // namespace host_api + +static host_api::HttpIncomingRequest::RequestHandler REQUEST_HANDLER = nullptr; +static host_api::HandleState *RESPONSE_OUT = nullptr; + +void host_api::HttpIncomingRequest::set_handler(RequestHandler handler) { + MOZ_ASSERT(!REQUEST_HANDLER); + REQUEST_HANDLER = handler; +} + +host_api::Result host_api::HttpOutgoingResponse::send() { + // Drop the headers that we eagerly grab in the factory function + wasi_http_0_2_0_types_fields_drop_own({this->headers_->handle_state_->take()}); + + wasi_http_0_2_0_types_result_own_outgoing_response_error_code_t result; + + result.is_err = false; + result.val.ok = {this->handle_state_->take()}; + + wasi_http_0_2_0_types_static_response_outparam_set({RESPONSE_OUT->take()}, &result); + + return {}; +} + +void exports_wasi_http_incoming_handler(exports_wasi_http_incoming_request request_handle, +exports_wasi_http_response_outparam response_out) { + RESPONSE_OUT = new host_api::HandleState(response_out.__handle); + auto *request = new host_api::HttpIncomingRequest(std::unique_ptr(new host_api::HandleState(request_handle.__handle))); + auto res = REQUEST_HANDLER(request); + MOZ_RELEASE_ASSERT(res); + MOZ_RELEASE_ASSERT(!RESPONSE_OUT->valid()); +} diff --git a/include/host_api.h b/include/host_api.h index 3333cff..83f4d1f 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -198,33 +198,24 @@ struct HostBytes final { operator std::span() const { return std::span(this->ptr.get(), this->len); } }; -/// The type of handles used by the host interface. -typedef int32_t Handle; - -/// An abstract base class to be used in classes representing host resources. +/// An opaque class to be used in classes representing host resources. /// /// Some host resources have different requirements for their client-side representation -/// depending on the host API. To accommodate this, we introduce a base class to use for -/// all of them, which the API-specific implementation can subclass as needed. -class HandleState { -public: - Handle handle; - HandleState() = delete; - explicit HandleState(Handle handle) : handle{handle} {} - virtual ~HandleState() = default; - - bool valid() const { return handle != -1; } -}; +/// depending on the host API. To accommodate this, we introduce an opaque class to use for +/// all of them, which the API-specific implementation can define as needed. +class HandleState; class Resource { protected: - HandleState *handle_state_; + std::unique_ptr handle_state_ = nullptr; + + // explicit Resource(HandleState *handle_state); public: - virtual ~Resource() = default; + virtual ~Resource(); - /// Returns true when this resource handle is valid. - virtual bool valid() const { return this->handle_state_ != nullptr; } + /// Returns true if this resource handle has been initialized and is still valid. + bool valid() const; }; class Pollable : public Resource { @@ -241,7 +232,7 @@ class Pollable : public Resource { class HttpIncomingBody final : public Pollable { public: HttpIncomingBody() = delete; - explicit HttpIncomingBody(Handle handle); + explicit HttpIncomingBody(std::unique_ptr handle); class ReadResult final { public: @@ -267,7 +258,7 @@ class HttpIncomingBody final : public Pollable { class HttpOutgoingBody final : public Pollable { public: HttpOutgoingBody() = delete; - explicit HttpOutgoingBody(Handle handle); + explicit HttpOutgoingBody(std::unique_ptr handle); /// Get the body's stream's current capacity. Result capacity(); @@ -315,7 +306,7 @@ class HttpHeaders; class FutureHttpIncomingResponse final : public Pollable { public: FutureHttpIncomingResponse() = delete; - explicit FutureHttpIncomingResponse(Handle handle); + explicit FutureHttpIncomingResponse(std::unique_ptr handle); /// Returns the response if it is ready, or `nullopt` if it is not. Result> maybe_response(); @@ -331,11 +322,9 @@ class HttpHeadersReadOnly : public Resource { friend HttpOutgoingRequest; friend HttpHeaders; -protected: - explicit HttpHeadersReadOnly(Handle handle); - HttpHeadersReadOnly() = default; - public: + HttpHeadersReadOnly() = delete; + explicit HttpHeadersReadOnly(std::unique_ptr handle); HttpHeadersReadOnly(const HttpHeadersReadOnly &headers) = delete; HttpHeaders* clone(); @@ -357,13 +346,13 @@ class HttpHeaders final : public HttpHeadersReadOnly { friend HttpOutgoingResponse; friend HttpOutgoingRequest; - explicit HttpHeaders(Handle handle); + explicit HttpHeaders(std::unique_ptr handle); public: HttpHeaders(); explicit HttpHeaders(const HttpHeadersReadOnly &headers); - static Result FromEntries(const vector>> &entries); + static Result FromEntries(const vector> &entries); bool is_writable() override { return true; }; HttpHeaders* as_writable() override { @@ -425,8 +414,10 @@ class HttpRequest : public HttpRequestResponseBase { class HttpIncomingRequest final : public HttpRequest, public HttpIncomingBodyOwner { public: + using RequestHandler = bool (*)(HttpIncomingRequest* request); + HttpIncomingRequest() = delete; - explicit HttpIncomingRequest(Handle handle); + explicit HttpIncomingRequest(std::unique_ptr handle); bool is_incoming() override { return true; } bool is_request() override { return true; } @@ -434,16 +425,18 @@ class HttpIncomingRequest final : public HttpRequest, public HttpIncomingBodyOwn [[nodiscard]] Result method() override; Result headers() override; Result body() override; + + static void set_handler(RequestHandler handler); }; class HttpOutgoingRequest final : public HttpRequest, public HttpOutgoingBodyOwner { - HttpOutgoingRequest(HandleState *state); + HttpOutgoingRequest(std::unique_ptr handle); public: HttpOutgoingRequest() = delete; static HttpOutgoingRequest *make(string_view method, optional url, - HttpHeaders *headers); + HttpHeadersReadOnly *headers); bool is_incoming() override { return false; } bool is_request() override { return true; } @@ -467,7 +460,7 @@ class HttpResponse : public HttpRequestResponseBase { class HttpIncomingResponse final : public HttpResponse, public HttpIncomingBodyOwner { public: HttpIncomingResponse() = delete; - explicit HttpIncomingResponse(Handle handle); + explicit HttpIncomingResponse(std::unique_ptr handle); bool is_incoming() override { return true; } bool is_request() override { return false; } @@ -478,11 +471,9 @@ class HttpIncomingResponse final : public HttpResponse, public HttpIncomingBodyO }; class HttpOutgoingResponse final : public HttpResponse, public HttpOutgoingBodyOwner { - HttpOutgoingResponse(HandleState *state); + HttpOutgoingResponse(std::unique_ptr handle); public: - using ResponseOutparam = Handle; - HttpOutgoingResponse() = delete; static HttpOutgoingResponse *make(uint16_t status, HttpHeaders *headers); @@ -494,7 +485,7 @@ class HttpOutgoingResponse final : public HttpResponse, public HttpOutgoingBodyO Result body() override; [[nodiscard]] Result status() override; - Result send(ResponseOutparam out_param); + Result send(); }; class Random final { From 3bb6b5d9719c798b5e4e7d86e333c93a9523b040 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 28 May 2024 14:17:35 +0200 Subject: [PATCH 10/22] Support creating a component without evaluating a top-level script during wizening The idea is that with this, builtins handling runtime events can dynamically load and execute a top-level script as needed. This functionality isn't actually used in this commit, but will be in another commit changing how incoming HTTP requests are handled. --- CMakeLists.txt | 1 + componentize.sh | 65 ++++++++++++++++++++++--- include/extension-api.h | 6 +++ runtime/engine.cpp | 65 ++++++++++++++++++++++++- runtime/js.cpp | 34 +------------ runtime/script_loader.cpp | 100 +++++++++++++++++++++----------------- runtime/script_loader.h | 3 +- 7 files changed, 187 insertions(+), 87 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b143611..e2c35fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,5 +104,6 @@ function(componentize OUTPUT) endfunction() componentize(smoke-test SOURCES tests/cases/smoke/smoke.js) +componentize(runtime-eval) include("tests/tests.cmake") diff --git a/componentize.sh b/componentize.sh index 22aff03..0d8419e 100755 --- a/componentize.sh +++ b/componentize.sh @@ -1,18 +1,69 @@ #!/usr/bin/env bash -set -euo pipefail +#set -euo pipefail wizer="${WIZER:-wizer}" wasm_tools="${WASM_TOOLS:-wasm-tools}" -# Use $2 as output file if provided, otherwise use the input base name with a .wasm extension -if [ $# -gt 1 ] +usage() { + echo "Usage: $(basename "$0") [input.js] [-o output.wasm]" + echo " Providing an input file but no output uses the input base name with a .wasm extension" + echo " Providing an output file but no input creates a component without running any top-level script" + exit 1 +} + +if [ $# -lt 1 ] then - OUT_FILE="$2" -else - BASENAME="$(basename "$1")" + usage +fi + +IN_FILE="" +OUT_FILE="" + +while [ $# -gt 0 ] +do + case "$1" in + -o|--output) + OUT_FILE="$2" + shift 2 + ;; + *) + if [ -n "$IN_FILE" ] && [ -z "$OUT_FILE" ] && [ $# -eq 1 ] + then + OUT_FILE="$1" + else + IN_FILE="$1" + fi + shift + ;; + esac +done + +# Exit if neither input file nor output file is provided. +if [ -z "$IN_FILE" ] && [ -z "$OUT_FILE" ] +then + usage +fi + +# Use the -o param as output file if provided, otherwise use the input base name with a .wasm +# extension. +if [ -z "$OUT_FILE" ] +then + BASENAME="$(basename "$IN_FILE")" OUT_FILE="${BASENAME%.*}.wasm" fi -echo "$1" | WASMTIME_BACKTRACE_DETAILS=1 $wizer --allow-wasi --wasm-bulk-memory true --inherit-stdio true --dir "$(dirname "$1")" -o "$OUT_FILE" -- "$(dirname "$0")/starling.wasm" +PREOPEN_DIR="" +if [ -n "$IN_FILE" ] +then + PREOPEN_DIR="--dir "$(dirname "$IN_FILE")"" + echo "Componentizing $IN_FILE into $OUT_FILE" +else + echo "Creating runtime-script component $OUT_FILE" +fi + + +echo "$IN_FILE" | WASMTIME_BACKTRACE_DETAILS=1 $wizer --allow-wasi --wasm-bulk-memory true \ + --inherit-stdio true --inherit-env true $PREOPEN_DIR -o "$OUT_FILE" \ + -- "$(dirname "$0")/starling.wasm" $wasm_tools component new -v --adapt "wasi_snapshot_preview1=$(dirname "$0")/preview1-adapter.wasm" --output "$OUT_FILE" "$OUT_FILE" diff --git a/include/extension-api.h b/include/extension-api.h index f6558b4..8619276 100644 --- a/include/extension-api.h +++ b/include/extension-api.h @@ -38,6 +38,9 @@ class Engine { JSContext *cx(); HandleObject global(); + /// Initialize the engine with the given filename + bool initialize(const char * filename); + /** * Define a new builtin module * @@ -62,6 +65,9 @@ class Engine { */ void enable_module_mode(bool enable); bool eval_toplevel(const char *path, MutableHandleValue result); + bool eval_toplevel(JS::SourceText &source, const char *path, + MutableHandleValue result); + bool toplevel_evaluated(); /** * Run the async event loop as long as there's interest registered in keeping it running. diff --git a/runtime/engine.cpp b/runtime/engine.cpp index 8286cdf..4d8529d 100644 --- a/runtime/engine.cpp +++ b/runtime/engine.cpp @@ -327,17 +327,60 @@ static void abort(JSContext *cx, const char *description) { exit(1); } +static api::Engine *ENGINE; + api::Engine::Engine() { // total_compute = 0; bool result = init_js(); MOZ_RELEASE_ASSERT(result); JS::EnterRealm(cx(), global()); core::EventLoop::init(cx()); + ENGINE = this; } JSContext *api::Engine::cx() { return CONTEXT; } HandleObject api::Engine::global() { return GLOBAL; } + +extern bool install_builtins(api::Engine *engine); + +#ifdef DEBUG +static bool trap(JSContext *cx, unsigned argc, JS::Value *vp) { + JS::CallArgs args = CallArgsFromVp(argc, vp); + ENGINE->dump_value(args.get(0)); + MOZ_ASSERT(false, "trap function called"); + return false; +} +#endif + +bool api::Engine::initialize(const char *filename) { + if (!install_builtins(this)) { + return false; + } + +#ifdef DEBUG + if (!JS_DefineFunction(cx(), global(), "trap", trap, 1, 0)) { + return false; + } +#endif + + if (!filename || strlen(filename) == 0) { + return true; + } + + RootedValue result(cx()); + + if (!eval_toplevel(filename, &result)) { + if (JS_IsExceptionPending(cx())) { + dump_pending_exception("pre-initializing"); + } + return false; + } + + js::ResetMathRandomSeed(cx()); + + return true; +} void api::Engine::enable_module_mode(bool enable) { scriptLoader->enable_module_mode(enable); } @@ -353,11 +396,14 @@ bool api::Engine::define_builtin_module(const char* id, HandleValue builtin) { return scriptLoader->define_builtin_module(id, builtin); } -bool api::Engine::eval_toplevel(const char *path, MutableHandleValue result) { +static bool TOPLEVEL_EVALUATED = false; + +bool api::Engine::eval_toplevel(JS::SourceText &source, const char *path, + MutableHandleValue result) { JSContext *cx = CONTEXT; RootedValue ns(cx); RootedValue tla_promise(cx); - if (!scriptLoader->load_top_level_script(path, &ns, &tla_promise)) { + if (!scriptLoader->eval_top_level_script(path, source, &ns, &tla_promise)) { return false; } @@ -410,9 +456,24 @@ bool api::Engine::eval_toplevel(const char *path, MutableHandleValue result) { // dedicated log target for telemetry messages like this. JS_SetGCCallback(cx, gc_callback, nullptr); + TOPLEVEL_EVALUATED = true; + return true; } +bool api::Engine::eval_toplevel(const char *path, MutableHandleValue result) { + JS::SourceText source; + if (!scriptLoader->load_script(CONTEXT, path, source)) { + return false; + } + + return eval_toplevel(source, path, result); +} + +bool api::Engine::toplevel_evaluated() { + return TOPLEVEL_EVALUATED; +} + bool api::Engine::run_event_loop() { return core::EventLoop::run_event_loop(this, 0); } diff --git a/runtime/js.cpp b/runtime/js.cpp index ba050fb..49a0d4f 100644 --- a/runtime/js.cpp +++ b/runtime/js.cpp @@ -16,42 +16,10 @@ bool WIZENED = false; extern "C" void __wasm_call_ctors(); api::Engine *engine; -extern bool install_builtins(api::Engine *engine); - -#ifdef DEBUG -static bool trap(JSContext *cx, unsigned argc, JS::Value *vp) { - JS::CallArgs args = CallArgsFromVp(argc, vp); - engine->dump_value(args.get(0)); - MOZ_ASSERT(false, "trap function called"); - return false; -} -#endif bool initialize(const char *filename) { auto engine = api::Engine(); - - if (!install_builtins(&engine)) { - return false; - } - -#ifdef DEBUG - if (!JS_DefineFunction(engine.cx(), engine.global(), "trap", trap, 1, 0)) { - return false; - } -#endif - - RootedValue result(engine.cx()); - - if (!engine.eval_toplevel(filename, &result)) { - if (JS_IsExceptionPending(engine.cx())) { - engine.dump_pending_exception("pre-initializing"); - } - return false; - } - - js::ResetMathRandomSeed(engine.cx()); - - return true; + return engine.initialize(filename); } extern "C" bool exports_wasi_cli_run_run() { diff --git a/runtime/script_loader.cpp b/runtime/script_loader.cpp index ad1f0a5..06ca71a 100644 --- a/runtime/script_loader.cpp +++ b/runtime/script_loader.cpp @@ -4,9 +4,9 @@ #include #include #include -#include #include #include +#include #include static JSContext* CONTEXT; @@ -135,50 +135,61 @@ static const char* resolve_path(const char* path, const char* base, size_t base_ static bool load_script(JSContext *cx, const char *script_path, const char* resolved_path, JS::SourceText &script); -static JSObject* get_module(JSContext* cx, const char* specifier, const char* resolved_path, - const JS::CompileOptions &opts) { - RootedString resolved_path_str(cx, JS_NewStringCopyZ(cx, resolved_path)); - if (!resolved_path_str) { +static JSObject* get_module(JSContext* cx, JS::SourceText &source, + const char* resolved_path, const JS::CompileOptions &opts) { + RootedObject module(cx, JS::CompileModule(cx, opts, source)); + if (!module) { return nullptr; } + RootedValue module_val(cx, ObjectValue(*module)); - RootedValue module_val(cx); - RootedValue resolved_path_val(cx, StringValue(resolved_path_str)); - if (!JS::MapGet(cx, moduleRegistry, resolved_path_val, &module_val)) { + RootedObject info(cx, JS_NewPlainObject(cx)); + if (!info) { return nullptr; } - if (!module_val.isUndefined()) { - return &module_val.toObject(); + RootedString resolved_path_str(cx, JS_NewStringCopyZ(cx, resolved_path)); + if (!resolved_path_str) { + return nullptr; } + RootedValue resolved_path_val(cx, StringValue(resolved_path_str)); - JS::SourceText source; - if (!load_script(cx, specifier, resolved_path, source)) { + if (!JS_DefineProperty(cx, info, "id", resolved_path_val, JSPROP_ENUMERATE)) { return nullptr; } - RootedObject module(cx, JS::CompileModule(cx, opts, source)); - if (!module) { + SetModulePrivate(module, ObjectValue(*info)); + + if (!MapSet(cx, moduleRegistry, resolved_path_val, module_val)) { return nullptr; } - module_val.setObject(*module); - RootedObject info(cx, JS_NewPlainObject(cx)); - if (!info) { + return module; +} + +static JSObject* get_module(JSContext* cx, const char* specifier, const char* resolved_path, + const JS::CompileOptions &opts) { + RootedString resolved_path_str(cx, JS_NewStringCopyZ(cx, resolved_path)); + if (!resolved_path_str) { return nullptr; } + RootedValue resolved_path_val(cx, StringValue(resolved_path_str)); - if (!JS_DefineProperty(cx, info, "id", resolved_path_val, JSPROP_ENUMERATE)) { + RootedValue module_val(cx); + if (!JS::MapGet(cx, moduleRegistry, resolved_path_val, &module_val)) { return nullptr; } - SetModulePrivate(module, ObjectValue(*info)); + if (!module_val.isUndefined()) { + return &module_val.toObject(); + } - if (!MapSet(cx, moduleRegistry, resolved_path_val, module_val)) { + JS::SourceText source; + if (!load_script(cx, specifier, resolved_path, source)) { return nullptr; } - return module; + return get_module(cx, source, resolved_path, opts); } static JSObject* get_builtin_module(JSContext* cx, HandleValue id, HandleObject builtin) { @@ -377,8 +388,8 @@ static bool load_script(JSContext *cx, const char *specifier, const char* resolv JS::SourceText &script) { FILE *file = fopen(resolved_path, "r"); if (!file) { - std::cerr << "Error opening file " << specifier << " (resolved to " << resolved_path << ")" - << std::endl; + std::cerr << "Error opening file " << specifier << " (resolved to " << resolved_path << "): " + << std::strerror(errno) << std::endl; return false; } @@ -409,27 +420,32 @@ static bool load_script(JSContext *cx, const char *specifier, const char* resolv bool ScriptLoader::load_script(JSContext *cx, const char *script_path, JS::SourceText &script) { - auto resolved_path = resolve_path(script_path, BASE_PATH, strlen(BASE_PATH)); + const char *resolved_path; + if (!BASE_PATH) { + auto last_slash = strrchr(script_path, '/'); + size_t base_len; + if (last_slash) { + last_slash++; + base_len = last_slash - script_path; + BASE_PATH = new char[base_len + 1]; + MOZ_ASSERT(BASE_PATH); + strncpy(BASE_PATH, script_path, base_len); + BASE_PATH[base_len] = '\0'; + } else { + BASE_PATH = strdup("./"); + } + resolved_path = script_path; + } else { + resolved_path = resolve_path(script_path, BASE_PATH, strlen(BASE_PATH)); + } + return ::load_script(cx, script_path, resolved_path, script); } -bool ScriptLoader::load_top_level_script(const char *path, MutableHandleValue result, MutableHandleValue tla_promise) { +bool ScriptLoader::eval_top_level_script(const char *path, JS::SourceText &source, + MutableHandleValue result, MutableHandleValue tla_promise) { JSContext *cx = CONTEXT; - MOZ_ASSERT(!BASE_PATH); - auto last_slash = strrchr(path, '/'); - size_t base_len; - if (last_slash) { - last_slash++; - base_len = last_slash - path; - BASE_PATH = new char[base_len + 1]; - MOZ_ASSERT(BASE_PATH); - strncpy(BASE_PATH, path, base_len); - BASE_PATH[base_len] = '\0'; - } else { - BASE_PATH = strdup("./"); - } - JS::CompileOptions opts(cx, *COMPILE_OPTS); opts.setFileAndLine(strip_base(path, BASE_PATH), 1); JS::RootedScript script(cx); @@ -440,7 +456,7 @@ bool ScriptLoader::load_top_level_script(const char *path, MutableHandleValue re // (Whereas disabling it during execution below meaningfully increases it, // which is why this is scoped to just compilation.) JS::AutoDisableGenerationalGC noGGC(cx); - module = get_module(cx, path, path, opts); + module = get_module(cx, source, path, opts); if (!module) { return false; } @@ -448,10 +464,6 @@ bool ScriptLoader::load_top_level_script(const char *path, MutableHandleValue re return false; } } else { - JS::SourceText source; - if (!::load_script(cx, path, path, source)) { - return false; - } // See comment above about disabling GGC during compilation. JS::AutoDisableGenerationalGC noGGC(cx); script = JS::Compile(cx, opts, source); diff --git a/runtime/script_loader.h b/runtime/script_loader.h index 4a645a4..460b58d 100644 --- a/runtime/script_loader.h +++ b/runtime/script_loader.h @@ -19,7 +19,8 @@ class ScriptLoader { bool define_builtin_module(const char* id, HandleValue builtin); void enable_module_mode(bool enable); - bool load_top_level_script(const char *path, MutableHandleValue result, MutableHandleValue tla_promise); + bool eval_top_level_script(const char *path, JS::SourceText &source, + MutableHandleValue result, MutableHandleValue tla_promise); bool load_script(JSContext* cx, const char *script_path, JS::SourceText &script); }; From c8cf151a3ce1b7e3e60b52aceeb005f074b239e0 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 28 May 2024 14:19:53 +0200 Subject: [PATCH 11/22] Slight improvements to the WPT harness --- tests/wpt-harness/run-wpt.mjs | 15 ++++++++++----- tests/wpt-harness/tests.json | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/wpt-harness/run-wpt.mjs b/tests/wpt-harness/run-wpt.mjs index 0200109..bad8b45 100644 --- a/tests/wpt-harness/run-wpt.mjs +++ b/tests/wpt-harness/run-wpt.mjs @@ -14,6 +14,7 @@ function relativePath(path) { return new URL(path, import.meta.url).pathname; } +const SKIP_PREFIX = "SKIP "; const SLOW_PREFIX = "SLOW "; const config = { @@ -280,9 +281,9 @@ async function startWptServer(root, logLevel) { async function ensureWasmtime(config, logLevel) { if (config.external) { - let wasmtime = { ...config }; + let wasmtime = { ...config, host: `http://${config.addr}/` }; if (logLevel > LogLevel.Quiet) { - console.info(`Using external Wasmtime host ${config.host}`); + console.info(`Using external Wasmtime host ${wasmtime.host}`); } return wasmtime; } else { @@ -379,6 +380,7 @@ function getTests(pattern) { console.log(`Loading tests list from ${config.tests.list}`); let testPaths = JSON.parse(readFileSync(config.tests.list, { encoding: "utf-8" })); + testPaths = testPaths.filter(path => !path.startsWith(SKIP_PREFIX)); let totalCount = testPaths.length; if (config.skipSlowTests) { testPaths = testPaths.filter(path => !path.startsWith(SLOW_PREFIX)); @@ -412,10 +414,11 @@ async function runTests(testPaths, wasmtime, resultCallback, errorCallback) { let t1 = Date.now(); let response, body; try { - response = await fetch(`${wasmtime.host}${path}`); - body = await response.text(); + if (config.logLevel >= LogLevel.VeryVerbose) { + console.log(`Sending request to ${wasmtime.host}${path}`); + } } catch (e) { - shutdown(`Wasmtime bailed while running test ${path}`); + shutdown(`Error while running test ${path}: ${e}`); } let stats = { count: 0, @@ -428,6 +431,8 @@ async function runTests(testPaths, wasmtime, resultCallback, errorCallback) { totalStats.duration += stats.duration; let results; try { + response = await fetch(`${wasmtime.host}${path}`); + body = await response.text(); results = JSON.parse(body); if (response.status == 500) { throw {message: results.error.message, stack: results.error.stack}; diff --git a/tests/wpt-harness/tests.json b/tests/wpt-harness/tests.json index 2545d8d..686903c 100644 --- a/tests/wpt-harness/tests.json +++ b/tests/wpt-harness/tests.json @@ -72,7 +72,7 @@ "fetch/api/headers/headers-record.any.js", "fetch/api/headers/headers-structure.any.js", "fetch/api/request/forbidden-method.any.js", - "fetch/api/request/request-bad-port.any.js", + "SKIP [tests restrictions we're not imposing] fetch/api/request/request-bad-port.any.js", "fetch/api/request/request-cache-default-conditional.any.js", "fetch/api/request/request-cache-default.any.js", "fetch/api/request/request-cache-force-cache.any.js", From c44b816cf64610dab66d39754e3ba2f2f7af2c48 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Tue, 28 May 2024 14:34:25 +0200 Subject: [PATCH 12/22] Substantially more reworking of the fetch related host API surface Gets back to passing all current tests, and most of the WPT suite that Fastly's JS Compute runtime passes. --- builtins/web/fetch/fetch-api.cpp | 64 ++++++-- builtins/web/fetch/fetch_event.cpp | 79 +++++++--- builtins/web/fetch/headers.cpp | 23 ++- builtins/web/fetch/request-response.cpp | 161 +++++++++++--------- builtins/web/fetch/request-response.h | 2 +- host-apis/wasi-0.2.0/host_api.cpp | 194 ++++++++++++++---------- include/host_api.h | 15 +- 7 files changed, 342 insertions(+), 196 deletions(-) diff --git a/builtins/web/fetch/fetch-api.cpp b/builtins/web/fetch/fetch-api.cpp index 25c2cdf..520de25 100644 --- a/builtins/web/fetch/fetch-api.cpp +++ b/builtins/web/fetch/fetch-api.cpp @@ -3,6 +3,10 @@ #include "headers.h" #include "request-response.h" +#include + +#include + namespace builtins::web::fetch { static api::Engine *ENGINE; @@ -80,30 +84,66 @@ bool fetch(JSContext *cx, unsigned argc, Value *vp) { return ReturnPromiseRejectedWithPendingError(cx, args); } - RootedObject requestInstance( + RootedObject request_obj( cx, JS_NewObjectWithGivenProto(cx, &Request::class_, Request::proto_obj)); - if (!requestInstance) - return false; + if (!request_obj) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + if (!Request::create(cx, request_obj, args[0], args.get(1))) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + RootedString method_str(cx, Request::method(cx, request_obj)); + if (!method_str) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } - RootedObject request(cx, Request::create(cx, requestInstance, args[0], args.get(1))); - if (!request) { + host_api::HostString method = core::encode(cx, method_str); + if (!method.ptr) { return ReturnPromiseRejectedWithPendingError(cx, args); } + RootedValue url_val(cx, RequestOrResponse::url(request_obj)); + host_api::HostString url = core::encode(cx, url_val); + if (!url.ptr) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + unique_ptr headers; + RootedObject headers_obj(cx, RequestOrResponse::maybe_headers(request_obj)); + if (headers_obj) { + headers = Headers::handle_clone(cx, headers_obj); + } else { + headers = std::make_unique(); + } + + if (!headers) { + return ReturnPromiseRejectedWithPendingError(cx, args); + } + + auto request = host_api::HttpOutgoingRequest::make(method, std::move(url), + std::move(headers)); + MOZ_RELEASE_ASSERT(request); + JS_SetReservedSlot(request_obj, static_cast(Request::Slots::Request), + PrivateValue(request)); + RootedObject response_promise(cx, JS::NewPromiseObject(cx, nullptr)); if (!response_promise) return ReturnPromiseRejectedWithPendingError(cx, args); bool streaming = false; - if (!RequestOrResponse::maybe_stream_body(cx, request, &streaming)) { + if (!RequestOrResponse::maybe_stream_body(cx, request_obj, &streaming)) { return false; } + if (streaming) { + // Ensure that the body handle is stored before making the request handle invalid by sending it. + request->body(); + } host_api::FutureHttpIncomingResponse *pending_handle; { - auto request_handle = Request::outgoing_handle(request); - auto res = request_handle->send(); - + auto res = request->send(); if (auto *err = res.to_err()) { HANDLE_ERROR(cx, *err); return ReturnPromiseRejectedWithPendingError(cx, args); @@ -115,11 +155,11 @@ bool fetch(JSContext *cx, unsigned argc, Value *vp) { // If the request body is streamed, we need to wait for streaming to complete // before marking the request as pending. if (!streaming) { - ENGINE->queue_async_task(new ResponseFutureTask(request, pending_handle)); + ENGINE->queue_async_task(new ResponseFutureTask(request_obj, pending_handle)); } - JS::SetReservedSlot(request, static_cast(Request::Slots::ResponsePromise), - JS::ObjectValue(*response_promise)); + JS::SetReservedSlot(request_obj, static_cast(Request::Slots::ResponsePromise), + ObjectValue(*response_promise)); args.rval().setObject(*response_promise); return true; diff --git a/builtins/web/fetch/fetch_event.cpp b/builtins/web/fetch/fetch_event.cpp index ca6a5e0..d1fbb10 100644 --- a/builtins/web/fetch/fetch_event.cpp +++ b/builtins/web/fetch/fetch_event.cpp @@ -3,10 +3,13 @@ #include "../url.h" #include "../worker-location.h" #include "encode.h" -#include "exports.h" #include "request-response.h" #include "bindings.h" + +#include +#include + #include #include @@ -165,12 +168,22 @@ bool start_response(JSContext *cx, JS::HandleObject response_obj, bool streaming return false; } - auto incoming_response = Response::response_handle(response_obj); host_api::HttpOutgoingResponse* response = - host_api::HttpOutgoingResponse::make(status, headers.get()); - if (incoming_response) { + host_api::HttpOutgoingResponse::make(status, std::move(headers)); + if (streaming) { + // Get the body here, so it will be stored on the response object. + // Otherwise, it'd not be available anymore, because the response handle itself + // is consumed by sending it off. + auto body = response->body().unwrap(); + MOZ_RELEASE_ASSERT(body); + } + MOZ_RELEASE_ASSERT(response); + + auto existing_handle = Response::response_handle(response_obj); + if (existing_handle) { + MOZ_ASSERT(existing_handle->is_incoming()); if (streaming) { - auto *source_body = incoming_response->body().unwrap(); + auto *source_body = static_cast(existing_handle)->body().unwrap(); auto *dest_body = response->body().unwrap(); auto res = dest_body->append(ENGINE, source_body); @@ -180,6 +193,9 @@ bool start_response(JSContext *cx, JS::HandleObject response_obj, bool streaming } MOZ_RELEASE_ASSERT(RequestOrResponse::mark_body_used(cx, response_obj)); } + } else { + SetReservedSlot(response_obj, static_cast(Response::Slots::Response), + PrivateValue(response)); } if (streaming && response->has_body()) { @@ -216,17 +232,6 @@ bool response_promise_then_handler(JSContext *cx, JS::HandleObject event, JS::Ha // very different.) JS::RootedObject response_obj(cx, &args[0].toObject()); - // Ensure that all headers are stored client-side, so we retain access to them - // after sending the response off. - // TODO(TS): restore proper headers handling - // if (Response::is_upstream(response_obj)) { - // JS::RootedObject headers(cx); - // headers = - // RequestOrResponse::headers(cx, response_obj); - // if (!Headers::delazify(cx, headers)) - // return false; - // } - bool streaming = false; if (!RequestOrResponse::maybe_stream_body(cx, response_obj, &streaming)) { return false; @@ -304,7 +309,7 @@ bool FetchEvent::respondWithError(JSContext *cx, JS::HandleObject self) { MOZ_RELEASE_ASSERT(state(self) == State::unhandled || state(self) == State::waitToRespond); auto headers = std::make_unique(); - auto *response = host_api::HttpOutgoingResponse::make(500, headers.get()); + auto *response = host_api::HttpOutgoingResponse::make(500, std::move(headers)); auto body_res = response->body(); if (auto *err = body_res.to_err()) { @@ -509,6 +514,46 @@ static void dispatch_fetch_event(HandleObject event, double *total_compute) { } bool handle_incoming_request(host_api::HttpIncomingRequest * request) { + if (!ENGINE->toplevel_evaluated()) { + JS::SourceText source; + auto body = request->body().unwrap(); + auto pollable = body->subscribe().unwrap(); + size_t len = 0; + vector chunks; + + while (true) { + host_api::block_on_pollable_handle(pollable); + auto result = body->read(4096); + if (result.unwrap().done) { + break; + } + + auto chunk = std::move(result.unwrap().bytes); + len += chunk.size(); + chunks.push_back(std::move(chunk)); + } + + // Merge all chunks into one buffer + auto buffer = new char[len]; + size_t offset = 0; + for (auto &chunk : chunks) { + memcpy(buffer + offset, chunk.ptr.get(), chunk.size()); + offset += chunk.size(); + } + + if (!source.init(CONTEXT, buffer, len, JS::SourceOwnership::TakeOwnership)) { + return false; + } + + RootedValue rval(ENGINE->cx()); + if (!ENGINE->eval_toplevel(source, "", &rval)) { + if (JS_IsExceptionPending(ENGINE->cx())) { + ENGINE->dump_pending_exception("Runtime script evaluation"); + } + return false; + } + } + HandleObject fetch_event = FetchEvent::instance(); MOZ_ASSERT(FetchEvent::is_instance(fetch_event)); if (!FetchEvent::init_incoming_request(ENGINE->cx(), fetch_event, request)) { diff --git a/builtins/web/fetch/headers.cpp b/builtins/web/fetch/headers.cpp index 6139b0c..2281190 100644 --- a/builtins/web/fetch/headers.cpp +++ b/builtins/web/fetch/headers.cpp @@ -1,5 +1,4 @@ #include "headers.h" -// #include "request-response.h" #include "encode.h" #include "decode.h" #include "sequence.hpp" @@ -70,7 +69,7 @@ host_api::HttpHeadersReadOnly *get_handle(JSObject *self) { */ host_api::HostString normalize_header_name(JSContext *cx, HandleValue name_val, bool* named_changed, const char *fun_name) { - *named_changed = false; + *named_changed = !name_val.isString(); JS::RootedString name_str(cx, JS::ToString(cx, name_val)); if (!name_str) { return nullptr; @@ -114,7 +113,7 @@ host_api::HostString normalize_header_name(JSContext *cx, HandleValue name_val, */ host_api::HostString normalize_header_value(JSContext *cx, HandleValue value_val, bool* value_changed, const char *fun_name) { - *value_changed = false; + *value_changed = !value_val.isString(); JS::RootedString value_str(cx, JS::ToString(cx, value_val)); if (!value_str) { return nullptr; @@ -309,7 +308,8 @@ static bool switch_mode(JSContext* cx, HandleObject self, const Headers::Mode mo return false; } - vector> string_entries; + using host_api::HostString; + vector> string_entries; RootedValue entry_val(cx); RootedObject entry(cx); @@ -328,8 +328,8 @@ static bool switch_mode(JSContext* cx, HandleObject self, const Headers::Mode mo } entry = &entry_val.toObject(); - JS_GetElement(cx, entry, 1, &name_val); - JS_GetElement(cx, entry, 0, &value_val); + JS_GetElement(cx, entry, 0, &name_val); + JS_GetElement(cx, entry, 1, &value_val); name_str = name_val.toString(); value_str = value_val.toString(); @@ -343,7 +343,7 @@ static bool switch_mode(JSContext* cx, HandleObject self, const Headers::Mode mo return false; } - string_entries.emplace_back(name, value); + string_entries.emplace_back(std::move(name), std::move(value)); } auto handle = host_api::HttpHeaders::FromEntries(string_entries); @@ -862,6 +862,9 @@ bool Headers::init_class(JSContext *cx, JS::HandleObject global) { JSObject *Headers::get_entries(JSContext *cx, HandleObject self) { MOZ_ASSERT(is_instance(self)); + if (mode(self) == Mode::Uninitialized && !switch_mode(cx, self, Mode::ContentOnly)) { + return nullptr; + } if (mode(self) == Mode::HostOnly && !switch_mode(cx, self, Mode::CachedInContent)) { return nullptr; } @@ -871,6 +874,12 @@ JSObject *Headers::get_entries(JSContext *cx, HandleObject self) { unique_ptr Headers::handle_clone(JSContext* cx, HandleObject self) { auto mode = Headers::mode(self); + + // If this instance uninitialized, return an empty handle without initializing this instance. + if (mode == Mode::Uninitialized) { + return std::make_unique(); + } + if (mode == Mode::ContentOnly && !switch_mode(cx, self, Mode::CachedInContent)) { // Switch to Mode::CachedInContent to ensure that the latest data is available on the handle, // but without discarding the existing entries, in case content reads them later. diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index c6f70b2..d065411 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -157,6 +157,7 @@ struct ReadResult { } // namespace host_api::HttpRequestResponseBase *RequestOrResponse::handle(JSObject *obj) { + MOZ_ASSERT(is_instance(obj)); auto slot = JS::GetReservedSlot(obj, static_cast(Slots::RequestOrResponse)); return static_cast(slot.toPrivate()); } @@ -165,7 +166,10 @@ bool RequestOrResponse::is_instance(JSObject *obj) { return Request::is_instance(obj) || Response::is_instance(obj); } -bool RequestOrResponse::is_incoming(JSObject *obj) { return !!handle(obj); } +bool RequestOrResponse::is_incoming(JSObject *obj) { + auto handle = RequestOrResponse::handle(obj); + return handle && handle->is_incoming(); +} host_api::HttpHeadersReadOnly *RequestOrResponse::headers_handle(JSObject *obj) { MOZ_ASSERT(is_instance(obj)); @@ -190,15 +194,11 @@ host_api::HttpIncomingBody *RequestOrResponse::incoming_body_handle(JSObject *ob host_api::HttpOutgoingBody *RequestOrResponse::outgoing_body_handle(JSObject *obj) { MOZ_ASSERT(!is_incoming(obj)); - host_api::Result res; - if (Request::is_instance(obj)) { - auto owner = reinterpret_cast(handle(obj)); - res = owner->body(); + if (handle(obj)->is_request()) { + return reinterpret_cast(handle(obj))->body().unwrap(); } else { - auto owner = reinterpret_cast(handle(obj)); - res = owner->body(); + return reinterpret_cast(handle(obj))->body().unwrap(); } - return res.unwrap(); } JSObject *RequestOrResponse::body_stream(JSObject *obj) { @@ -318,60 +318,97 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self, } } } else { - mozilla::Maybe maybeNoGC; - JS::UniqueChars text; - char *buf; - size_t length; + RootedValue chunk(cx); + RootedObject buffer(cx); + char *buf = nullptr; + size_t length = 0; + + // Must be declared here to keep the buffer alive. + host_api::HostString text; if (body_obj && JS_IsArrayBufferViewObject(body_obj)) { - // Short typed arrays have inline data which can move on GC, so assert - // that no GC happens. (Which it doesn't, because we're not allocating - // before `buf` goes out of scope.) - maybeNoGC.emplace(cx); - JS::AutoCheckCannotGC &noGC = maybeNoGC.ref(); - bool is_shared; length = JS_GetArrayBufferViewByteLength(body_obj); - buf = (char *)JS_GetArrayBufferViewData(body_obj, &is_shared, noGC); - } else if (body_obj && JS::IsArrayBufferObject(body_obj)) { + buf = static_cast(js_malloc(length)); + if (!buf) { + return false; + } + bool is_shared; - JS::GetArrayBufferLengthAndData(body_obj, &length, &is_shared, (uint8_t **)&buf); + JS::AutoCheckCannotGC noGC(cx); + auto temp_buf = JS_GetArrayBufferViewData(body_obj, &is_shared, noGC); + memcpy(buf, temp_buf, length); + } else if (body_obj && IsArrayBufferObject(body_obj)) { + buffer = CopyArrayBuffer(cx, body_obj); + if (!buffer) { + return false; + } } else if (body_obj && url::URLSearchParams::is_instance(body_obj)) { auto slice = url::URLSearchParams::serialize(cx, body_obj); buf = (char *)slice.data; length = slice.len; content_type = "application/x-www-form-urlencoded;charset=UTF-8"; } else { - { - auto str = core::encode(cx, body_val); - text = std::move(str.ptr); - length = str.len; + text = core::encode(cx, body_val); + if (!text.ptr) { + return false; } + buf = text.ptr.get(); + length = text.len; + content_type = "text/plain;charset=UTF-8"; + } - if (!text) + if (buf) { + MOZ_ASSERT(!buffer); + buffer = NewArrayBufferWithContents(cx, length, buf, + JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory); + if (text.ptr) { + static_cast(text.ptr.release()); + } + if (!buffer) { + js_free(buf); return false; - buf = text.get(); - content_type = "text/plain;charset=UTF-8"; + } + chunk.setObject(*buffer); } - // TODO: restore - // auto body = RequestOrResponse::outgoing_body_handle(self); - // auto write_res = body->write_all(reinterpret_cast(buf), length); + if (buffer) { + RootedObject array(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 0, length)); + if (!array) { + return false; + } + chunk.setObject(*array); + } - // Ensure that the NoGC is reset, so throwing an error in HANDLE_ERROR - // succeeds. - if (maybeNoGC.isSome()) { - maybeNoGC.reset(); + RootedObject source(cx, JS_NewObjectWithGivenProto(cx, nullptr, nullptr)); + if (!source) { + return false; + } + RootedObject body_stream(cx, JS::NewReadableDefaultStreamObject(cx, source, + nullptr, 0.0)); + if (!body_stream) { + return false; } - // if (auto *err = write_res.to_err()) { - // HANDLE_ERROR(cx, *err); - // return false; - // } + mozilla::DebugOnly disturbed; + MOZ_ASSERT(ReadableStreamIsDisturbed(cx, body_stream, &disturbed)); + MOZ_ASSERT(!disturbed); + + + if (!ReadableStreamEnqueue(cx, body_stream, chunk) || + !ReadableStreamClose(cx, body_stream)) { + return false; + } + + JS_SetReservedSlot(self, static_cast(Slots::BodyStream), + ObjectValue(*body_stream)); } // Step 36.3 of Request constructor / 8.4 of Response constructor. if (content_type) { JS::RootedObject headers(cx, RequestOrResponse::headers(cx, self)); + if (!headers) { + return false; + } if (!Headers::set_if_undefined(cx, headers, "content-type", content_type)) { return false; } @@ -427,8 +464,11 @@ bool RequestOrResponse::append_body(JSContext *cx, JS::HandleObject self, JS::Ha JSObject *RequestOrResponse::headers(JSContext *cx, JS::HandleObject obj) { JSObject *headers = maybe_headers(obj); if (!headers) { - if (auto *headers_handle = RequestOrResponse::headers_handle(obj)) { - headers = Headers::create(cx, headers_handle); + host_api::HttpHeadersReadOnly *handle; + if (is_incoming(obj) && (handle = headers_handle(obj))) { + headers = Headers::create(cx, handle); + } else { + headers = Headers::create(cx, NullHandleValue); } if (!headers) { return nullptr; @@ -788,10 +828,6 @@ bool RequestOrResponse::bodyAll(JSContext *cx, JS::CallArgs args, JS::HandleObje return true; } - if (!mark_body_used(cx, self)) { - return ReturnPromiseRejectedWithPendingError(cx, args); - } - JS::RootedValue body_parser(cx, JS::PrivateValue((void *)parse_body)); // TODO(performance): don't reify a ReadableStream for body handles—use an AsyncTask instead @@ -896,10 +932,6 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject return false; } - if (Request::is_instance(body_owner)) { - ENGINE->queue_async_task(new BodyFutureTask(body_owner)); - } - return true; } @@ -939,6 +971,7 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject bool is_shared; uint8_t *bytes = JS_GetUint8ArrayData(array, &is_shared, nogc); size_t length = JS_GetTypedArrayByteLength(array); + // TODO: change this to write in chunks, respecting backpressure. res = body->write_all(bytes, length); } @@ -1029,17 +1062,6 @@ bool RequestOrResponse::maybe_stream_body(JSContext *cx, JS::HandleObject body_o if (!reader) return false; - bool is_closed; - if (!JS::ReadableStreamReaderIsClosed(cx, reader, &is_closed)) - return false; - - // It's ok for the stream to be closed, as its contents might - // already have fully been written to the body handle. - // In that case, we can do a blocking send instead. - if (is_closed) { - return true; - } - // Create handlers for both `then` and `catch`. // These are functions with two reserved slots, in which we store all // information required to perform the reactions. We store the actually @@ -1124,20 +1146,24 @@ host_api::HttpRequest *Request::request_handle(JSObject *obj) { host_api::HttpOutgoingRequest *Request::outgoing_handle(JSObject *obj) { auto base = RequestOrResponse::handle(obj); + MOZ_ASSERT(base->is_outgoing()); return reinterpret_cast(base); } host_api::HttpIncomingRequest *Request::incoming_handle(JSObject *obj) { auto base = RequestOrResponse::handle(obj); + MOZ_ASSERT(base->is_incoming()); return reinterpret_cast(base); } JSObject *Request::response_promise(JSObject *obj) { + MOZ_ASSERT(is_instance(obj)); return &JS::GetReservedSlot(obj, static_cast(Request::Slots::ResponsePromise)) .toObject(); } JSString *Request::method(JSContext *cx, JS::HandleObject obj) { + MOZ_ASSERT(is_instance(obj)); return JS::GetReservedSlot(obj, static_cast(Slots::Method)).toString(); } @@ -1374,6 +1400,7 @@ JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance) { */ JSObject *Request::create(JSContext *cx, JS::HandleObject requestInstance, JS::HandleValue input, JS::HandleValue init_val) { + create(cx, requestInstance); JS::RootedString url_str(cx); JS::RootedString method_str(cx); bool method_needs_normalization = false; @@ -1804,10 +1831,9 @@ static_assert((int)Response::Slots::BodyUsed == (int)Request::Slots::BodyUsed); static_assert((int)Response::Slots::Headers == (int)Request::Slots::Headers); static_assert((int)Response::Slots::Response == (int)Request::Slots::Request); -host_api::HttpIncomingResponse *Response::response_handle(JSObject *obj) { +host_api::HttpResponse *Response::response_handle(JSObject *obj) { MOZ_ASSERT(is_instance(obj)); - return static_cast( - JS::GetReservedSlot(obj, static_cast(Slots::Response)).toPrivate()); + return static_cast(RequestOrResponse::handle(obj)); } uint16_t Response::status(JSObject *obj) { @@ -2529,15 +2555,6 @@ bool Response::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { if (!RequestOrResponse::extract_body(cx, response, body_val)) { return false; } - - // TODO: presumably remove this code, but check what it's for first - // if (RequestOrResponse::has_body(response)) { - // if (response_handle->body().is_err()) { - // auto err = response_handle->body().to_err(); - // HANDLE_ERROR(cx, *err); - // return false; - // } - // } } args.rval().setObject(*response); diff --git a/builtins/web/fetch/request-response.h b/builtins/web/fetch/request-response.h index 5f435b5..b7af49e 100644 --- a/builtins/web/fetch/request-response.h +++ b/builtins/web/fetch/request-response.h @@ -211,7 +211,7 @@ class Response final : public BuiltinImpl { static JSObject* create_incoming(JSContext * cx, HandleObject self, host_api::HttpIncomingResponse* response); - static host_api::HttpIncomingResponse *response_handle(JSObject *obj); + static host_api::HttpResponse *response_handle(JSObject *obj); static uint16_t status(JSObject *obj); static JSString *status_message(JSObject *obj); static void set_status_message_from_code(JSContext *cx, JSObject *obj, uint16_t code); diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index e036336..1322fed 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -2,9 +2,12 @@ #include "bindings/bindings.h" #include -#include #include +#ifdef DEBUG +#include +#endif +using host_api::HostString; using std::optional; using std::string_view; using std::tuple; @@ -46,56 +49,67 @@ typedef wasi_io_0_2_0_streams_own_output_stream_t own_output_stream_t; typedef int32_t Handle; constexpr Handle UNINITIALIZED_HANDLE = -1; -/// An abstract base class to be used in classes representing host resources. -/// -/// Some host resources have different requirements for their client-side representation -/// depending on the host API. To accommodate this, we introduce a base class to use for -/// all of them, which the API-specific implementation can subclass as needed. class host_api::HandleState { Handle handle_ = UNINITIALIZED_HANDLE; #ifdef DEBUG - #define ASSERT_VALID()\ - MOZ_ASSERT(handle_state_.get() != nullptr, "Handle state missing"); \ - MOZ_ASSERT(handle_state_.get()->initialized(), "Handle not initialized"); \ - MOZ_ASSERT(!handle_state_.get()->poisoned(), "Handle poisoned"); + uint8_t handle_ns_; bool poisoned_ = false; -#else -#define ASSERT_VALID + + static std::set used_handles; #endif + static int32_t to_namespaced(Handle handle, uint8_t handle_ns) { + return (handle_ns << 24) | handle; + } + public: - HandleState() {} - explicit HandleState(Handle handle) : handle_{handle} { - DBG("Adding handle %d\n", handle); - // MOZ_ASSERT(handle > UNINITIALIZED_HANDLE); - // MOZ_ASSERT(!used_handles.has(handle)); - // MOZ_ASSERT(used_handles.put(handle)); + HandleState() = delete; + HandleState(Handle handle, uint8_t handle_ns) : handle_{handle} { +#ifdef DEBUG + handle_ns_ = handle_ns; + // DBG("Adding handle %d,%d\n", handle_ns_, handle); + // TODO: remove this, and replace all this with better things + MOZ_ASSERT(handle < 500000); + auto ns_handle = to_namespaced(handle, handle_ns); + MOZ_ASSERT(handle > UNINITIALIZED_HANDLE); + MOZ_ASSERT(!used_handles.contains(ns_handle)); + used_handles.insert(ns_handle); +#endif } virtual ~HandleState() { // TODO: support dropping handles for all handle types. Probably using a template class. - DBG("Removing handle %d\n", handle_); + // DBG("Removing handle %d,%d\n", handle_ns_, handle_); #ifdef DEBUG - // MOZ_ASSERT(used_handles.has(handle)); - // used_handles.remove(handle); + if (!poisoned_) { + auto ns_handle = to_namespaced(handle_, handle_ns_); + MOZ_ASSERT(used_handles.contains(ns_handle)); + used_handles.erase(ns_handle); + } #endif } - Handle get() const { - DBG("Getting handle %d\n", handle_); - MOZ_ASSERT(handle_ != UNINITIALIZED_HANDLE, "Handle is uninitialized"); + void assert_valid() const { #ifdef DEBUG - if (poisoned_) { - fprintf(stderr, "Handle %d is poisened", handle_); fflush(stderr); + MOZ_ASSERT(initialized(), "Handle not initialized"); + if (poisoned()) { + fprintf(stderr, "Handle %d,%d is poisened", handle_ns_, handle_); fflush(stderr); MOZ_ASSERT(false); } #endif + } + + Handle get() const { + // DBG("Getting handle %d,%d\n", handle_ns_, handle_); + assert_valid(); return handle_; } Handle take() { auto handle = get(); - DBG("Consuming handle %d\n", handle); + // DBG("Consuming handle %d,%d\n", handle_ns_, handle); + auto ns_handle = to_namespaced(handle, handle_ns_); + used_handles.erase(ns_handle); poisoned_ = true; return handle; } @@ -105,6 +119,8 @@ class host_api::HandleState { bool poisoned() const { return poisoned_; } }; +std::set host_api::HandleState::used_handles = std::set(); + namespace { // TODO: merge HandleOps into a WASIHandleState subclass of HandleState, and use that everywhere. @@ -256,10 +272,11 @@ void MonotonicClock::unsubscribe(const int32_t handle_id) { HttpHeaders::HttpHeaders(std::unique_ptr state) : HttpHeadersReadOnly(std::move(state)) {} -HttpHeaders::HttpHeaders() : HttpHeadersReadOnly(std::make_unique(wasi_http_0_2_0_types_constructor_fields().__handle)) { +static Resource::HandleNS HEADERS_HANDLE_NS = Resource::next_handle_ns("HttpHeaders"); +HttpHeaders::HttpHeaders() : HttpHeadersReadOnly(std::make_unique(wasi_http_0_2_0_types_constructor_fields().__handle, HEADERS_HANDLE_NS)) { } -Result HttpHeaders::FromEntries(const vector> &entries) { +Result HttpHeaders::FromEntries(vector>& entries) { std::vector pairs; pairs.reserve(entries.size()); @@ -271,16 +288,19 @@ Result HttpHeaders::FromEntries(const vector::err(154); + } - return Result::ok(new HttpHeaders(std::make_unique(ret.__handle))); + return Result::ok(new HttpHeaders(std::make_unique(ret.__handle, + HEADERS_HANDLE_NS))); } HttpHeaders::HttpHeaders(const HttpHeadersReadOnly &headers) : HttpHeadersReadOnly(nullptr) { Borrow borrow(headers.handle_state_.get()); auto handle = wasi_http_0_2_0_types_method_fields_clone(borrow); - this->handle_state_ = std::unique_ptr(new HandleState(handle.__handle)); + this->handle_state_ = std::unique_ptr(new HandleState(handle.__handle, HEADERS_HANDLE_NS)); } HttpHeaders *HttpHeadersReadOnly::clone() { @@ -289,7 +309,7 @@ HttpHeaders *HttpHeadersReadOnly::clone() { Result>> HttpHeadersReadOnly::entries() const { Result>> res; - ASSERT_VALID(); + handle_state_->assert_valid(); wasi_http_0_2_0_types_list_tuple2_field_key_field_value_t entries; Borrow borrow(this->handle_state_.get()); @@ -310,7 +330,7 @@ Result>> HttpHeadersReadOnly::entries() con Result> HttpHeadersReadOnly::names() const { Result> res; - ASSERT_VALID(); + handle_state_->assert_valid(); wasi_http_0_2_0_types_list_tuple2_field_key_field_value_t entries; Borrow borrow(this->handle_state_.get()); @@ -329,7 +349,7 @@ Result> HttpHeadersReadOnly::names() const { Result>> HttpHeadersReadOnly::get(string_view name) const { Result>> res; - ASSERT_VALID(); + handle_state_->assert_valid(); wasi_http_0_2_0_types_list_field_value_t values; auto hdr = string_view_to_world_string(name); @@ -352,7 +372,7 @@ Result>> HttpHeadersReadOnly::get(string_view name) } Result HttpHeadersReadOnly::has(string_view name) const { - ASSERT_VALID(); + handle_state_->assert_valid(); auto hdr = string_view_to_world_string(name); Borrow borrow(this->handle_state_.get()); @@ -360,7 +380,7 @@ Result HttpHeadersReadOnly::has(string_view name) const { } Result HttpHeaders::set(string_view name, string_view value) { - ASSERT_VALID(); + handle_state_->assert_valid(); auto hdr = from_string_view(name); auto val = from_string_view(value); wasi_http_0_2_0_types_list_field_value_t host_values{&val, 1}; @@ -375,7 +395,7 @@ Result HttpHeaders::set(string_view name, string_view value) { } Result HttpHeaders::append(string_view name, string_view value) { - ASSERT_VALID(); + handle_state_->assert_valid(); auto hdr = from_string_view(name); auto val = from_string_view(value); Borrow borrow(this->handle_state_.get()); @@ -397,7 +417,7 @@ Result HttpHeaders::append(string_view name, string_view value) { } Result HttpHeaders::remove(string_view name) { - ASSERT_VALID(); + handle_state_->assert_valid(); auto hdr = string_view_to_world_string(name); Borrow borrow(this->handle_state_.get()); @@ -447,6 +467,7 @@ bool write_to_outgoing_body(Borrow borrow, const uint8_t *ptr, con return wasi_io_0_2_0_streams_method_output_stream_write(borrow, &list, &err); } +static Resource::HandleNS OUT_BODY_HANDLE_NS = Resource::next_handle_ns("OutgoingBody"); class OutgoingBodyHandleState final : public HandleState { Handle stream_handle_; PollableHandle pollable_handle_; @@ -455,7 +476,7 @@ class OutgoingBodyHandleState final : public HandleState { public: explicit OutgoingBodyHandleState(const Handle handle) - : HandleState(handle), pollable_handle_(INVALID_POLLABLE_HANDLE) { + : HandleState(handle, OUT_BODY_HANDLE_NS), pollable_handle_(INVALID_POLLABLE_HANDLE) { const borrow_outgoing_body_t borrow = {handle}; own_output_stream_t stream{}; if (!wasi_http_0_2_0_types_method_outgoing_body_write(borrow, &stream)) { @@ -661,13 +682,13 @@ class BodyAppendTask final : public api::AsyncTask { }; Result HttpOutgoingBody::append(api::Engine *engine, HttpIncomingBody *other) { - ASSERT_VALID(); + handle_state_->assert_valid(); engine->queue_async_task(new BodyAppendTask(other, this)); return {}; } Result HttpOutgoingBody::close() { - ASSERT_VALID(); + handle_state_->assert_valid(); auto state = static_cast(handle_state_.get()); // A blocking flush is required here to ensure that all buffered contents are @@ -736,8 +757,9 @@ wasi_http_0_2_0_types_method_t http_method_to_host(string_view method_str) { HttpOutgoingRequest::HttpOutgoingRequest(std::unique_ptr state) { this->handle_state_ = std::move(state); } +static Resource::HandleNS OUT_REQUEST_HANDLE_NS = Resource::next_handle_ns("HttpOutgoingRequest"); HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional url_str, - HttpHeadersReadOnly *headers) { + std::unique_ptr headers) { bindings_string_t path_with_query; wasi_http_0_2_0_types_scheme_t scheme; bindings_string_t authority; @@ -771,7 +793,6 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< } Handle headers_handle = headers->handle_state_->take(); - DBG("HH: %d\n", headers_handle); auto handle = wasi_http_0_2_0_types_constructor_outgoing_request({headers_handle}); { @@ -792,16 +813,14 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< maybe_path_with_query); } - auto *state = new HandleState(handle.__handle); + auto *state = new HandleState(handle.__handle, OUT_REQUEST_HANDLE_NS); auto *resp = new HttpOutgoingRequest(std::unique_ptr(state)); - resp->headers_ = headers; - return resp; } Result HttpOutgoingRequest::method() { - ASSERT_VALID(); + handle_state_->assert_valid(); return Result::ok(method_); } @@ -812,7 +831,7 @@ Result HttpOutgoingRequest::headers() { } borrow_outgoing_request_t borrow(handle_state_->get()); auto res = wasi_http_0_2_0_types_method_outgoing_request_headers(borrow); - headers_ = new HttpHeadersReadOnly(std::unique_ptr(new HandleState(res.__handle))); + headers_ = new HttpHeadersReadOnly(std::make_unique(res.__handle, HEADERS_HANDLE_NS)); } return Result::ok(headers_); @@ -820,27 +839,32 @@ Result HttpOutgoingRequest::headers() { Result HttpOutgoingRequest::body() { typedef Result Res; - ASSERT_VALID(); if (!this->body_) { + handle_state_->assert_valid(); outgoing_body_t body; if (!wasi_http_0_2_0_types_method_outgoing_request_body( - wasi_http_0_2_0_types_borrow_outgoing_request({handle_state_->take()}), &body)) { + wasi_http_0_2_0_types_borrow_outgoing_request({handle_state_->get()}), &body)) { return Res::err(154); } - this->body_ = new HttpOutgoingBody(std::unique_ptr(new HandleState(body.__handle))); + body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandleState(body.__handle))); } return Res::ok(body_); } +static Resource::HandleNS FUTURE_INCOMING_RESPONSE_HANDLE_NS = Resource::next_handle_ns("FutureHttpIncomingResponse"); Result HttpOutgoingRequest::send() { - ASSERT_VALID(); + typedef Result Res; + handle_state_->assert_valid(); future_incoming_response_t ret; wasi_http_0_2_0_outgoing_handler_error_code_t err; - wasi_http_0_2_0_outgoing_handler_handle({handle_state_->take()}, nullptr, &ret, &err); - auto res = new FutureHttpIncomingResponse(std::unique_ptr(new HandleState(ret.__handle))); + if (!wasi_http_0_2_0_outgoing_handler_handle({handle_state_->take()}, nullptr, &ret, &err)) { + return Res::err(154); + } + auto res = new FutureHttpIncomingResponse(std::make_unique(ret.__handle, FUTURE_INCOMING_RESPONSE_HANDLE_NS)); return Result::ok(res); } +static Resource::HandleNS IN_BODY_HANDLE_NS = Resource::next_handle_ns("IncomingBody"); class IncomingBodyHandleState final : public HandleState { Handle stream_handle_; PollableHandle pollable_handle_; @@ -849,7 +873,7 @@ class IncomingBodyHandleState final : public HandleState { public: explicit IncomingBodyHandleState(const Handle handle) - : HandleState(handle), pollable_handle_(INVALID_POLLABLE_HANDLE) { + : HandleState(handle, IN_BODY_HANDLE_NS), pollable_handle_(INVALID_POLLABLE_HANDLE) { const borrow_incoming_body_t borrow = {handle}; own_input_stream_t stream{}; if (!wasi_http_0_2_0_types_method_incoming_body_stream(borrow, &stream)) { @@ -859,6 +883,10 @@ class IncomingBodyHandleState final : public HandleState { } }; +void block_on_pollable_handle(PollableHandle handle) { + wasi_io_0_2_0_poll_method_pollable_block({handle}); +} + HttpIncomingBody::HttpIncomingBody(std::unique_ptr state) : Pollable() { handle_state_ = std::move(state); } Resource::~Resource() { @@ -867,6 +895,12 @@ Resource::~Resource() { } } +static Resource::HandleNS HANDLE_NS = 0; +Resource::HandleNS Resource::next_handle_ns(const char* ns_name) { + // DBG("Creating handle namespace %d with name %s\n", HANDLE_NS, ns_name); + return HANDLE_NS++; +} + bool Resource::valid() const { return this->handle_state_ != nullptr && this->handle_state_->valid(); } @@ -892,8 +926,8 @@ Result HttpIncomingBody::read(uint32_t chunk_size) Result HttpIncomingBody::close() { return {}; } Result HttpIncomingBody::subscribe() { - auto borrow = borrow_input_stream_t( - {static_cast(handle_state_.get())->stream_handle_}); + auto state = static_cast(handle_state_.get()); + auto borrow = borrow_input_stream_t({state->stream_handle_}); auto pollable = wasi_io_0_2_0_streams_method_input_stream_subscribe(borrow); return Result::ok(pollable.__handle); } @@ -910,6 +944,8 @@ FutureHttpIncomingResponse::FutureHttpIncomingResponse(std::unique_ptr> FutureHttpIncomingResponse::maybe_response() { typedef Result> Res; wasi_http_0_2_0_types_result_result_own_incoming_response_error_code_void_t res; @@ -926,7 +962,7 @@ Result> FutureHttpIncomingResponse::maybe_respo return Res::err(154); } - return Res::ok(new HttpIncomingResponse(std::unique_ptr(new HandleState(val.ok.__handle)))); + return Res::ok(new HttpIncomingResponse(std::make_unique(val.ok.__handle, IN_RESPONSE_HANDLE_NS))); } Result FutureHttpIncomingResponse::subscribe() { @@ -964,7 +1000,7 @@ Result HttpIncomingResponse::headers() { } auto res = wasi_http_0_2_0_types_method_incoming_response_headers( wasi_http_0_2_0_types_borrow_incoming_response({handle_state_->get()})); - headers_ = new HttpHeadersReadOnly(std::unique_ptr(new HandleState(res.__handle))); + headers_ = new HttpHeadersReadOnly(std::make_unique(res.__handle, HEADERS_HANDLE_NS)); } return Result::ok(headers_); @@ -980,33 +1016,29 @@ Result HttpIncomingResponse::body() { wasi_http_0_2_0_types_borrow_incoming_response({handle_state_->get()}), &body)) { return Result::err(154); } - body_ = new HttpIncomingBody(std::unique_ptr(new HandleState(body.__handle))); + body_ = new HttpIncomingBody(std::unique_ptr(new IncomingBodyHandleState(body.__handle))); } return Result::ok(body_); } HttpOutgoingResponse::HttpOutgoingResponse(std::unique_ptr state) { this->handle_state_ = std::move(state); } -// TODO: change this to take a unique_ptr to prevent reuse after consumption -HttpOutgoingResponse *HttpOutgoingResponse::make(const uint16_t status, HttpHeaders *headers) { +static Resource::HandleNS OUT_RESPONSE_HANDLE_NS = Resource::next_handle_ns("HttpOutgoingResponse"); +HttpOutgoingResponse *HttpOutgoingResponse::make(const uint16_t status, unique_ptr headers) { wasi_http_0_2_0_types_own_headers_t owned{headers->handle_state_->take()}; auto handle = wasi_http_0_2_0_types_constructor_outgoing_response(owned); auto borrow = wasi_http_0_2_0_types_borrow_outgoing_response(handle); - auto *state = new HandleState(handle.__handle); + auto *state = new HandleState(handle.__handle, OUT_RESPONSE_HANDLE_NS); auto *resp = new HttpOutgoingResponse(std::unique_ptr(state)); // Set the status if (status != 200) { - // TODO: handle success result - wasi_http_0_2_0_types_method_outgoing_response_set_status_code(borrow, status); + // The DOM implementation is expected to have validated the status code already. + MOZ_RELEASE_ASSERT(wasi_http_0_2_0_types_method_outgoing_response_set_status_code(borrow, status)); } - // TODO: ensure that the passed-in headers aren't used anywhere anymore. - resp->status_ = status; - resp->headers_ = headers; - return resp; } @@ -1017,7 +1049,7 @@ Result HttpOutgoingResponse::headers() { } borrow_outgoing_response_t borrow(handle_state_->get()); auto res = wasi_http_0_2_0_types_method_outgoing_response_headers(borrow); - headers_ = new HttpHeadersReadOnly(std::unique_ptr(new HandleState(res.__handle))); + headers_ = new HttpHeadersReadOnly(std::make_unique(res.__handle, HEADERS_HANDLE_NS)); } return Result::ok(headers_); @@ -1025,14 +1057,14 @@ Result HttpOutgoingResponse::headers() { Result HttpOutgoingResponse::body() { typedef Result Res; - ASSERT_VALID(); if (!this->body_) { + handle_state_->assert_valid(); outgoing_body_t body; if (!wasi_http_0_2_0_types_method_outgoing_response_body( wasi_http_0_2_0_types_borrow_outgoing_response({handle_state_->get()}), &body)) { return Res::err(154); } - this->body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandleState(body.__handle))); + body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandleState(body.__handle))); } return Res::ok(this->body_); } @@ -1067,7 +1099,7 @@ Result HttpIncomingRequest::headers() { } borrow_incoming_request_t borrow(handle_state_->get()); auto res = wasi_http_0_2_0_types_method_incoming_request_headers(borrow); - headers_ = new HttpHeadersReadOnly(std::unique_ptr(new HandleState(res.__handle))); + headers_ = new HttpHeadersReadOnly(std::make_unique(res.__handle, HEADERS_HANDLE_NS)); } return Result::ok(headers_); @@ -1099,9 +1131,6 @@ void host_api::HttpIncomingRequest::set_handler(RequestHandler handler) { } host_api::Result host_api::HttpOutgoingResponse::send() { - // Drop the headers that we eagerly grab in the factory function - wasi_http_0_2_0_types_fields_drop_own({this->headers_->handle_state_->take()}); - wasi_http_0_2_0_types_result_own_outgoing_response_error_code_t result; result.is_err = false; @@ -1112,10 +1141,13 @@ host_api::Result host_api::HttpOutgoingResponse::send() { return {}; } +static host_api::Resource::HandleNS RESPONSE_OUTPARAM_HANDLE_NS = + host_api::Resource::next_handle_ns("ResponseOutParam"); +static host_api::Resource::HandleNS IN_REQUEST_HANDLE_NS = host_api::Resource::next_handle_ns("HttpIncomingRequest"); void exports_wasi_http_incoming_handler(exports_wasi_http_incoming_request request_handle, -exports_wasi_http_response_outparam response_out) { - RESPONSE_OUT = new host_api::HandleState(response_out.__handle); - auto *request = new host_api::HttpIncomingRequest(std::unique_ptr(new host_api::HandleState(request_handle.__handle))); + exports_wasi_http_response_outparam response_out) { + RESPONSE_OUT = new host_api::HandleState(response_out.__handle, RESPONSE_OUTPARAM_HANDLE_NS); + auto *request = new host_api::HttpIncomingRequest(std::make_unique(request_handle.__handle, IN_REQUEST_HANDLE_NS)); auto res = REQUEST_HANDLER(request); MOZ_RELEASE_ASSERT(res); MOZ_RELEASE_ASSERT(!RESPONSE_OUT->valid()); diff --git a/include/host_api.h b/include/host_api.h index 83f4d1f..5375756 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -209,11 +209,12 @@ class Resource { protected: std::unique_ptr handle_state_ = nullptr; - // explicit Resource(HandleState *handle_state); - public: virtual ~Resource(); + typedef uint8_t HandleNS; + static HandleNS next_handle_ns(const char* ns_name); + /// Returns true if this resource handle has been initialized and is still valid. bool valid() const; }; @@ -229,6 +230,8 @@ class Pollable : public Resource { virtual void unsubscribe() = 0; }; +void block_on_pollable_handle(PollableHandle handle); + class HttpIncomingBody final : public Pollable { public: HttpIncomingBody() = delete; @@ -352,7 +355,7 @@ class HttpHeaders final : public HttpHeadersReadOnly { HttpHeaders(); explicit HttpHeaders(const HttpHeadersReadOnly &headers); - static Result FromEntries(const vector> &entries); + static Result FromEntries(vector>& entries); bool is_writable() override { return true; }; HttpHeaders* as_writable() override { @@ -435,8 +438,8 @@ class HttpOutgoingRequest final : public HttpRequest, public HttpOutgoingBodyOwn public: HttpOutgoingRequest() = delete; - static HttpOutgoingRequest *make(string_view method, optional url, - HttpHeadersReadOnly *headers); + static HttpOutgoingRequest *make(string_view method_str, optional url_str, + std::unique_ptr headers); bool is_incoming() override { return false; } bool is_request() override { return true; } @@ -476,7 +479,7 @@ class HttpOutgoingResponse final : public HttpResponse, public HttpOutgoingBodyO public: HttpOutgoingResponse() = delete; - static HttpOutgoingResponse *make(uint16_t status, HttpHeaders *headers); + static HttpOutgoingResponse *make(const uint16_t status, unique_ptr headers); bool is_incoming() override { return false; } bool is_request() override { return false; } From 2d68405b11c902f3065f563f703dffe50196fbfd Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Wed, 29 May 2024 03:36:20 -0700 Subject: [PATCH 13/22] fix: release build on fetch rework (#54) fix: release build --- host-apis/wasi-0.2.0/host_api.cpp | 14 +++++++++++++- include/host_api.h | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index 1322fed..3d81ff7 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -1,6 +1,7 @@ #include "host_api.h" #include "bindings/bindings.h" +#include #include #include #ifdef DEBUG @@ -108,18 +109,29 @@ class host_api::HandleState { Handle take() { auto handle = get(); // DBG("Consuming handle %d,%d\n", handle_ns_, handle); +#ifdef DEBUG auto ns_handle = to_namespaced(handle, handle_ns_); used_handles.erase(ns_handle); poisoned_ = true; +#endif return handle; } - bool valid() const { return handle_ != UNINITIALIZED_HANDLE && !poisoned_; } + bool valid() const { +#ifdef DEBUG + if (posoned_) return false; +#endif + return handle_ != UNINITIALIZED_HANDLE; + } bool initialized() const { return handle_ != UNINITIALIZED_HANDLE;} +#ifdef DEBUG bool poisoned() const { return poisoned_; } +#endif }; +#ifdef DEBUG std::set host_api::HandleState::used_handles = std::set(); +#endif namespace { diff --git a/include/host_api.h b/include/host_api.h index 5375756..85e8876 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -335,6 +335,7 @@ class HttpHeadersReadOnly : public Resource { virtual bool is_writable() { return false; }; virtual HttpHeaders* as_writable() { MOZ_ASSERT_UNREACHABLE(); + return nullptr; }; Result>> entries() const; From 2bad18a88af4ab17d2474e50d7fcaa73d528af33 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Sat, 1 Jun 2024 21:57:48 +0200 Subject: [PATCH 14/22] Use a templated WASIHandle class to represent handles for WASI 0.2.0 This changes the implementation of the handle abstraction to make it more robust, and cleans it up at least somewhat. --- host-apis/wasi-0.2.0/host_api.cpp | 489 +++++++++++++++--------------- include/host_api.h | 6 +- 2 files changed, 249 insertions(+), 246 deletions(-) diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index 3d81ff7..bf24858 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -1,8 +1,6 @@ #include "host_api.h" #include "bindings/bindings.h" -#include -#include #include #ifdef DEBUG #include @@ -19,12 +17,6 @@ using std::vector; // pointer. static_assert(sizeof(uint32_t) == sizeof(void *)); -typedef wasi_http_0_2_0_types_own_incoming_request_t incoming_request_t; -typedef wasi_http_0_2_0_types_borrow_incoming_request_t borrow_incoming_request_t; -typedef wasi_http_0_2_0_types_own_incoming_response_t incoming_response_t; -typedef wasi_http_0_2_0_types_borrow_outgoing_request_t borrow_outgoing_request_t; -typedef wasi_http_0_2_0_types_borrow_outgoing_response_t borrow_outgoing_response_t; - typedef wasi_http_0_2_0_types_own_future_incoming_response_t future_incoming_response_t; typedef wasi_http_0_2_0_types_borrow_future_incoming_response_t borrow_future_incoming_response_t; @@ -34,173 +26,233 @@ typedef wasi_http_0_2_0_types_own_outgoing_body_t outgoing_body_t; using field_key = wasi_http_0_2_0_types_field_key_t; using field_value = wasi_http_0_2_0_types_field_value_t; -typedef wasi_http_0_2_0_types_borrow_incoming_body_t borrow_incoming_body_t; -typedef wasi_http_0_2_0_types_borrow_outgoing_body_t borrow_outgoing_body_t; - typedef wasi_io_0_2_0_poll_own_pollable_t own_pollable_t; typedef wasi_io_0_2_0_poll_borrow_pollable_t borrow_pollable_t; typedef wasi_io_0_2_0_poll_list_borrow_pollable_t list_borrow_pollable_t; -typedef wasi_io_0_2_0_streams_own_input_stream_t own_input_stream_t; -typedef wasi_io_0_2_0_streams_borrow_input_stream_t borrow_input_stream_t; - -typedef wasi_io_0_2_0_streams_own_output_stream_t own_output_stream_t; +#ifdef LOG_HANDLE_OPS +#define LOG_HANDLE_OP(...) fprintf(stderr, "%s", __PRETTY_FUNCTION__); fprintf(stderr, __VA_ARGS__) +#else +#define LOG_HANDLE_OP(...) +#endif /// The type of handles used by the host interface. typedef int32_t Handle; -constexpr Handle UNINITIALIZED_HANDLE = -1; +constexpr Handle POISONED_HANDLE = -1; class host_api::HandleState { - Handle handle_ = UNINITIALIZED_HANDLE; +protected: + HandleState() = default; -#ifdef DEBUG - uint8_t handle_ns_; - bool poisoned_ = false; +public: + virtual ~HandleState() = default; + virtual bool valid() const = 0; +}; + +template struct HandleOps {}; - static std::set used_handles; +template +class WASIHandle : public host_api::HandleState { +#ifdef DEBUG + static inline auto used_handles = std::set(); #endif - static int32_t to_namespaced(Handle handle, uint8_t handle_ns) { - return (handle_ns << 24) | handle; - } +protected: + Handle handle_; +#ifdef DEBUG + bool owned_; +#endif public: - HandleState() = delete; - HandleState(Handle handle, uint8_t handle_ns) : handle_{handle} { + using Borrowed = typename HandleOps::borrowed; + + explicit WASIHandle(typename HandleOps::owned handle) : handle_{handle.__handle} { + LOG_HANDLE_OP("Creating owned handle %d\n", handle.__handle); #ifdef DEBUG - handle_ns_ = handle_ns; - // DBG("Adding handle %d,%d\n", handle_ns_, handle); - // TODO: remove this, and replace all this with better things - MOZ_ASSERT(handle < 500000); - auto ns_handle = to_namespaced(handle, handle_ns); - MOZ_ASSERT(handle > UNINITIALIZED_HANDLE); - MOZ_ASSERT(!used_handles.contains(ns_handle)); - used_handles.insert(ns_handle); + owned_ = true; + MOZ_ASSERT(!used_handles.contains(handle.__handle)); + used_handles.insert(handle.__handle); #endif } - virtual ~HandleState() { - // TODO: support dropping handles for all handle types. Probably using a template class. - // DBG("Removing handle %d,%d\n", handle_ns_, handle_); + + explicit WASIHandle(typename HandleOps::borrowed handle) : handle_{handle.__handle} { + LOG_HANDLE_OP("Creating borrowed handle %d\n", handle.__handle); #ifdef DEBUG - if (!poisoned_) { - auto ns_handle = to_namespaced(handle_, handle_ns_); - MOZ_ASSERT(used_handles.contains(ns_handle)); - used_handles.erase(ns_handle); - } + owned_ = false; + MOZ_ASSERT(!used_handles.contains(handle.__handle)); + used_handles.insert(handle.__handle); #endif } - void assert_valid() const { + ~WASIHandle() override { #ifdef DEBUG - MOZ_ASSERT(initialized(), "Handle not initialized"); - if (poisoned()) { - fprintf(stderr, "Handle %d,%d is poisened", handle_ns_, handle_); fflush(stderr); - MOZ_ASSERT(false); + if (handle_ != POISONED_HANDLE) { + LOG_HANDLE_OP("Deleting (owned? %d) handle %d\n", owned_, handle_); + MOZ_ASSERT(used_handles.contains(handle_)); + used_handles.erase(handle_); } #endif } - Handle get() const { - // DBG("Getting handle %d,%d\n", handle_ns_, handle_); - assert_valid(); - return handle_; + static WASIHandle* cast(HandleState* handle) { + return reinterpret_cast*>(handle); } - Handle take() { - auto handle = get(); - // DBG("Consuming handle %d,%d\n", handle_ns_, handle); -#ifdef DEBUG - auto ns_handle = to_namespaced(handle, handle_ns_); - used_handles.erase(ns_handle); - poisoned_ = true; -#endif - return handle; + typename HandleOps::borrowed borrow(HandleState *handle) { + return cast(handle)->borrow(); } - bool valid() const { -#ifdef DEBUG - if (posoned_) return false; -#endif - return handle_ != UNINITIALIZED_HANDLE; + bool valid() const override { + bool valid = handle_ != POISONED_HANDLE; + MOZ_ASSERT_IF(valid, used_handles.contains(handle_)); + return valid; + } + + typename HandleOps::borrowed borrow() const { + MOZ_ASSERT(valid()); + LOG_HANDLE_OP("borrowing handle %d\n", handle_); + return {handle_}; } - bool initialized() const { return handle_ != UNINITIALIZED_HANDLE;} + + typename HandleOps::owned take() { + MOZ_ASSERT(valid()); + MOZ_ASSERT(owned_); + LOG_HANDLE_OP("taking handle %d\n", handle_); + typename HandleOps::owned handle = { handle_ }; #ifdef DEBUG - bool poisoned() const { return poisoned_; } + used_handles.erase(handle_); #endif + handle_ = POISONED_HANDLE; + return handle; + } }; -#ifdef DEBUG -std::set host_api::HandleState::used_handles = std::set(); -#endif +template +struct Borrow { + static constexpr typename HandleOps::borrowed invalid{std::numeric_limits::max()}; + typename HandleOps::borrowed handle_{invalid}; -namespace { + explicit Borrow(host_api::HandleState *handle) { + handle_ = WASIHandle::cast(handle)->borrow(); + } + + explicit Borrow(typename HandleOps::borrowed handle) { + handle_ = handle; + } -// TODO: merge HandleOps into a WASIHandleState subclass of HandleState, and use that everywhere. -/// This is the type contract for using the Own and Borrow templates. -template struct HandleOps {}; + explicit Borrow(typename HandleOps::owned handle) { + handle_ = {handle.__handle}; + } -/// A convenience wrapper for constructing a borrow. As we only create borrows of things we already -/// own, this wrapper will never explicitly drop borrows. -template class Borrow final { - static constexpr const typename HandleOps::borrow invalid{std::numeric_limits::max()}; - HandleOps::borrow handle{Borrow::invalid}; + operator typename HandleOps::borrowed() const { return handle_; } +}; -public: - Borrow() = default; +template <> struct HandleOps { + using owned = wasi_io_0_2_0_poll_own_pollable_t; + using borrowed = wasi_io_0_2_0_poll_borrow_pollable_t; +}; - // Construct a borrow from an owned handle. - Borrow(HandleOps::own handle) : handle{HandleOps::borrow_owned(handle)} {} +template <> struct HandleOps { + using owned = wasi_http_0_2_0_types_own_headers_t; + using borrowed = wasi_http_0_2_0_types_borrow_fields_t; +}; - // Construct a borrow from a raw `Handle` value. - Borrow(Handle handle) : Borrow{typename HandleOps::own{handle}} { - MOZ_ASSERT(valid()); - } +template <> struct HandleOps { + using owned = wasi_http_0_2_0_types_own_incoming_request_t; + using borrowed = wasi_http_0_2_0_types_borrow_incoming_request_t; +}; - // Convenience wrapper for constructing a borrow of a HandleState. - Borrow(host_api::HandleState *state) : Borrow{typename HandleOps::own{state->get()}} {} +template <> struct HandleOps { + using owned = wasi_http_0_2_0_types_own_outgoing_request_t; + using borrowed = wasi_http_0_2_0_types_borrow_outgoing_request_t; +}; - bool valid() const { return this->handle.__handle != Borrow::invalid.__handle; } +template <> struct HandleOps { + using owned = wasi_http_0_2_0_types_own_future_incoming_response_t; + using borrowed = wasi_http_0_2_0_types_borrow_future_incoming_response_t; +}; - operator bool() const { return this->valid(); } +template <> struct HandleOps { + using owned = wasi_http_0_2_0_types_own_incoming_response_t; + using borrowed = wasi_http_0_2_0_types_borrow_incoming_response_t; +}; - operator typename HandleOps::borrow() const { return this->handle; } +template <> struct HandleOps { + using owned = wasi_http_0_2_0_types_own_outgoing_response_t; + using borrowed = wasi_http_0_2_0_types_borrow_outgoing_response_t; }; -template <> struct HandleOps { - using own = wasi_http_0_2_0_types_own_fields_t; - using borrow = wasi_http_0_2_0_types_borrow_fields_t; +template <> struct HandleOps { + using owned = wasi_http_0_2_0_types_own_incoming_body_t; + using borrowed = wasi_http_0_2_0_types_borrow_incoming_body_t; +}; - static constexpr const auto borrow_owned = wasi_http_0_2_0_types_borrow_fields; +template <> struct HandleOps { + using owned = wasi_http_0_2_0_types_own_outgoing_body_t; + using borrowed = wasi_http_0_2_0_types_borrow_outgoing_body_t; }; struct OutputStream {}; - template <> struct HandleOps { - using own = wasi_io_0_2_0_streams_own_output_stream_t; - using borrow = wasi_io_0_2_0_streams_borrow_output_stream_t; + using owned = wasi_io_0_2_0_streams_own_output_stream_t; + using borrowed = wasi_io_0_2_0_streams_borrow_output_stream_t; +}; - static constexpr const auto borrow_owned = wasi_io_0_2_0_streams_borrow_output_stream; +struct InputStream {}; +template <> struct HandleOps { + using owned = wasi_io_0_2_0_streams_own_input_stream_t; + using borrowed = wasi_io_0_2_0_streams_borrow_input_stream_t; }; -struct Pollable {}; +class IncomingBodyHandle final : public WASIHandle { + HandleOps::owned stream_handle_; + PollableHandle pollable_handle_; + + friend host_api::HttpIncomingBody; -template <> struct HandleOps { - using own = wasi_io_0_2_0_poll_own_pollable_t; - using borrow = wasi_io_0_2_0_poll_borrow_pollable_t; +public: + explicit IncomingBodyHandle(HandleOps::owned handle) + : WASIHandle(handle), pollable_handle_(INVALID_POLLABLE_HANDLE) { + HandleOps::owned stream{}; + if (!wasi_http_0_2_0_types_method_incoming_body_stream(borrow(), &stream)) { + MOZ_ASSERT_UNREACHABLE("Getting a body's stream should never fail"); + } + stream_handle_ = stream; + } - static constexpr const auto borrow_owned = wasi_io_0_2_0_poll_borrow_pollable; + static IncomingBodyHandle* cast(HandleState* handle) { + return reinterpret_cast(handle); + } }; -} // namespace +class OutgoingBodyHandle final : public WASIHandle { + HandleOps::owned stream_handle_; + PollableHandle pollable_handle_; + + friend host_api::HttpOutgoingBody; + +public: + explicit OutgoingBodyHandle(HandleOps::owned handle) + : WASIHandle(handle), pollable_handle_(INVALID_POLLABLE_HANDLE) { + HandleOps::owned stream{}; + if (!wasi_http_0_2_0_types_method_outgoing_body_write(borrow(), &stream)) { + MOZ_ASSERT_UNREACHABLE("Getting a body's stream should never fail"); + } + stream_handle_ = stream; + } + + static OutgoingBodyHandle* cast(HandleState* handle) { + return reinterpret_cast(handle); + } +}; -size_t api::AsyncTask::select(std::vector *tasks) { +size_t api::AsyncTask::select(std::vector *tasks) { auto count = tasks->size(); - vector> handles; + vector::Borrowed> handles; for (const auto task : *tasks) { handles.emplace_back(task->id()); } - auto list = list_borrow_pollable_t{ - reinterpret_cast::borrow *>(handles.data()), count}; + auto list = list_borrow_pollable_t{ handles.data(), count}; wasi_io_0_2_0_poll_list_u32_t result{nullptr, 0}; wasi_io_0_2_0_poll_poll(&list, &result); MOZ_ASSERT(result.len > 0); @@ -236,7 +288,7 @@ template T from_string_view(std::string_view str) { auto string_view_to_world_string = from_string_view; -HostString scheme_to_string(const wasi_http_0_2_0_types_scheme_t scheme) { +HostString scheme_to_string(const wasi_http_0_2_0_types_scheme_t &scheme) { if (scheme.tag == WASI_HTTP_0_2_0_TYPES_SCHEME_HTTP) { return {"http:"}; } @@ -284,8 +336,8 @@ void MonotonicClock::unsubscribe(const int32_t handle_id) { HttpHeaders::HttpHeaders(std::unique_ptr state) : HttpHeadersReadOnly(std::move(state)) {} -static Resource::HandleNS HEADERS_HANDLE_NS = Resource::next_handle_ns("HttpHeaders"); -HttpHeaders::HttpHeaders() : HttpHeadersReadOnly(std::make_unique(wasi_http_0_2_0_types_constructor_fields().__handle, HEADERS_HANDLE_NS)) { +HttpHeaders::HttpHeaders() { + handle_state_ = std::make_unique>(wasi_http_0_2_0_types_constructor_fields()); } Result HttpHeaders::FromEntries(vector>& entries) { @@ -305,14 +357,13 @@ Result HttpHeaders::FromEntries(vector::err(154); } - return Result::ok(new HttpHeaders(std::make_unique(ret.__handle, - HEADERS_HANDLE_NS))); + return Result::ok(new HttpHeaders(std::unique_ptr(new WASIHandle(ret)))); } HttpHeaders::HttpHeaders(const HttpHeadersReadOnly &headers) : HttpHeadersReadOnly(nullptr) { Borrow borrow(headers.handle_state_.get()); auto handle = wasi_http_0_2_0_types_method_fields_clone(borrow); - this->handle_state_ = std::unique_ptr(new HandleState(handle.__handle, HEADERS_HANDLE_NS)); + this->handle_state_ = std::unique_ptr(new WASIHandle(handle)); } HttpHeaders *HttpHeadersReadOnly::clone() { @@ -321,7 +372,6 @@ HttpHeaders *HttpHeadersReadOnly::clone() { Result>> HttpHeadersReadOnly::entries() const { Result>> res; - handle_state_->assert_valid(); wasi_http_0_2_0_types_list_tuple2_field_key_field_value_t entries; Borrow borrow(this->handle_state_.get()); @@ -342,13 +392,13 @@ Result>> HttpHeadersReadOnly::entries() con Result> HttpHeadersReadOnly::names() const { Result> res; - handle_state_->assert_valid(); wasi_http_0_2_0_types_list_tuple2_field_key_field_value_t entries; Borrow borrow(this->handle_state_.get()); wasi_http_0_2_0_types_method_fields_entries(borrow, &entries); vector names; + names.reserve(entries.len); for (int i = 0; i < entries.len; i++) { names.emplace_back(bindings_string_to_host_string(entries.ptr[i].f0)); } @@ -361,7 +411,6 @@ Result> HttpHeadersReadOnly::names() const { Result>> HttpHeadersReadOnly::get(string_view name) const { Result>> res; - handle_state_->assert_valid(); wasi_http_0_2_0_types_list_field_value_t values; auto hdr = string_view_to_world_string(name); @@ -370,6 +419,7 @@ Result>> HttpHeadersReadOnly::get(string_view name) if (values.len > 0) { std::vector names; + names.reserve(values.len); for (int i = 0; i < values.len; i++) { names.emplace_back(to_host_string(values.ptr[i])); } @@ -384,15 +434,12 @@ Result>> HttpHeadersReadOnly::get(string_view name) } Result HttpHeadersReadOnly::has(string_view name) const { - handle_state_->assert_valid(); - auto hdr = string_view_to_world_string(name); Borrow borrow(this->handle_state_.get()); return Result::ok(wasi_http_0_2_0_types_method_fields_has(borrow, &hdr)); } Result HttpHeaders::set(string_view name, string_view value) { - handle_state_->assert_valid(); auto hdr = from_string_view(name); auto val = from_string_view(value); wasi_http_0_2_0_types_list_field_value_t host_values{&val, 1}; @@ -407,7 +454,6 @@ Result HttpHeaders::set(string_view name, string_view value) { } Result HttpHeaders::append(string_view name, string_view value) { - handle_state_->assert_valid(); auto hdr = from_string_view(name); auto val = from_string_view(value); Borrow borrow(this->handle_state_.get()); @@ -420,8 +466,9 @@ Result HttpHeaders::append(string_view name, string_view value) { case WASI_HTTP_0_2_0_TYPES_HEADER_ERROR_FORBIDDEN: return Result::err(154); case WASI_HTTP_0_2_0_TYPES_HEADER_ERROR_IMMUTABLE: - fprintf(stderr, "Headers %d should not be immutable", this->handle_state_->get()); - MOZ_ASSERT_UNREACHABLE(); + MOZ_ASSERT_UNREACHABLE("Headers should not be immutable"); + default: + MOZ_ASSERT_UNREACHABLE("Unknown header error type"); } } @@ -429,14 +476,14 @@ Result HttpHeaders::append(string_view name, string_view value) { } Result HttpHeaders::remove(string_view name) { - handle_state_->assert_valid(); auto hdr = string_view_to_world_string(name); Borrow borrow(this->handle_state_.get()); wasi_http_0_2_0_types_header_error_t err; - wasi_http_0_2_0_types_method_fields_delete(borrow, &hdr, &err); - - // TODO: handle `err` + if (!wasi_http_0_2_0_types_method_fields_delete(borrow, &hdr, &err)) { + // TODO: handle `err` + return Result::err(154); + } return {}; } @@ -447,7 +494,7 @@ string_view HttpRequestResponseBase::url() { return string_view(*_url); } - auto borrow = borrow_incoming_request_t{handle_state_->get()}; + Borrow borrow(handle_state_.get()); wasi_http_0_2_0_types_scheme_t scheme; bool success; @@ -479,25 +526,6 @@ bool write_to_outgoing_body(Borrow borrow, const uint8_t *ptr, con return wasi_io_0_2_0_streams_method_output_stream_write(borrow, &list, &err); } -static Resource::HandleNS OUT_BODY_HANDLE_NS = Resource::next_handle_ns("OutgoingBody"); -class OutgoingBodyHandleState final : public HandleState { - Handle stream_handle_; - PollableHandle pollable_handle_; - - friend HttpOutgoingBody; - -public: - explicit OutgoingBodyHandleState(const Handle handle) - : HandleState(handle, OUT_BODY_HANDLE_NS), pollable_handle_(INVALID_POLLABLE_HANDLE) { - const borrow_outgoing_body_t borrow = {handle}; - own_output_stream_t stream{}; - if (!wasi_http_0_2_0_types_method_outgoing_body_write(borrow, &stream)) { - MOZ_ASSERT_UNREACHABLE("Getting a body's stream should never fail"); - } - stream_handle_ = stream.__handle; - } -}; - HttpOutgoingBody::HttpOutgoingBody(std::unique_ptr state) : Pollable() { handle_state_ = std::move(state); } @@ -507,7 +535,7 @@ Result HttpOutgoingBody::capacity() { return Result::err(154); } - auto *state = static_cast(this->handle_state_.get()); + auto *state = static_cast(this->handle_state_.get()); Borrow borrow(state->stream_handle_); uint64_t capacity = 0; wasi_io_0_2_0_streams_stream_error_t err; @@ -526,7 +554,7 @@ Result HttpOutgoingBody::write(const uint8_t *bytes, size_t len) { auto capacity = res.unwrap(); auto bytes_to_write = std::min(len, static_cast(capacity)); - auto *state = static_cast(this->handle_state_.get()); + auto *state = static_cast(this->handle_state_.get()); Borrow borrow(state->stream_handle_); if (!write_to_outgoing_body(borrow, bytes, bytes_to_write)) { return Result::err(154); @@ -538,10 +566,10 @@ Result HttpOutgoingBody::write(const uint8_t *bytes, size_t len) { Result HttpOutgoingBody::write_all(const uint8_t *bytes, size_t len) { if (!valid()) { // TODO: proper error handling for all 154 error codes. - return Result::err({}); + return Result::err(154); } - auto *state = static_cast(handle_state_.get()); + auto *state = static_cast(handle_state_.get()); Borrow borrow(state->stream_handle_); while (len > 0) { @@ -642,7 +670,7 @@ class BodyAppendTask final : public api::AsyncTask { return true; } - auto offset = 0; + unsigned offset = 0; while (bytes.len - offset > 0) { // TODO: remove double checking of write-readiness // TODO: make this async by storing the remaining chunk in the task and marking it as @@ -694,15 +722,12 @@ class BodyAppendTask final : public api::AsyncTask { }; Result HttpOutgoingBody::append(api::Engine *engine, HttpIncomingBody *other) { - handle_state_->assert_valid(); engine->queue_async_task(new BodyAppendTask(other, this)); return {}; } Result HttpOutgoingBody::close() { - handle_state_->assert_valid(); - - auto state = static_cast(handle_state_.get()); + auto state = static_cast(handle_state_.get()); // A blocking flush is required here to ensure that all buffered contents are // actually written before finishing the body. Borrow borrow{state->stream_handle_}; @@ -728,7 +753,7 @@ Result HttpOutgoingBody::close() { return {}; } Result HttpOutgoingBody::subscribe() { - auto state = static_cast(handle_state_.get()); + auto state = static_cast(handle_state_.get()); if (state->pollable_handle_ == INVALID_POLLABLE_HANDLE) { Borrow borrow(state->stream_handle_); state->pollable_handle_ = wasi_io_0_2_0_streams_method_output_stream_subscribe(borrow).__handle; @@ -737,7 +762,7 @@ Result HttpOutgoingBody::subscribe() { } void HttpOutgoingBody::unsubscribe() { - auto state = static_cast(handle_state_.get()); + auto state = static_cast(handle_state_.get()); if (state->pollable_handle_ == INVALID_POLLABLE_HANDLE) { return; } @@ -769,7 +794,6 @@ wasi_http_0_2_0_types_method_t http_method_to_host(string_view method_str) { HttpOutgoingRequest::HttpOutgoingRequest(std::unique_ptr state) { this->handle_state_ = std::move(state); } -static Resource::HandleNS OUT_REQUEST_HANDLE_NS = Resource::next_handle_ns("HttpOutgoingRequest"); HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional url_str, std::unique_ptr headers) { bindings_string_t path_with_query; @@ -804,9 +828,9 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< maybe_path_with_query = &path_with_query; } - Handle headers_handle = headers->handle_state_->take(); + auto headers_handle = WASIHandle::cast(headers->handle_state_.get())->take(); auto handle = - wasi_http_0_2_0_types_constructor_outgoing_request({headers_handle}); + wasi_http_0_2_0_types_constructor_outgoing_request(headers_handle); { auto borrow = wasi_http_0_2_0_types_borrow_outgoing_request(handle); @@ -825,14 +849,13 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< maybe_path_with_query); } - auto *state = new HandleState(handle.__handle, OUT_REQUEST_HANDLE_NS); + auto *state = new WASIHandle(handle); auto *resp = new HttpOutgoingRequest(std::unique_ptr(state)); return resp; } Result HttpOutgoingRequest::method() { - handle_state_->assert_valid(); return Result::ok(method_); } @@ -841,9 +864,9 @@ Result HttpOutgoingRequest::headers() { if (!valid()) { return Result::err(154); } - borrow_outgoing_request_t borrow(handle_state_->get()); + Borrow borrow(handle_state_.get()); auto res = wasi_http_0_2_0_types_method_outgoing_request_headers(borrow); - headers_ = new HttpHeadersReadOnly(std::make_unique(res.__handle, HEADERS_HANDLE_NS)); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(new WASIHandle(res))); } return Result::ok(headers_); @@ -852,49 +875,28 @@ Result HttpOutgoingRequest::headers() { Result HttpOutgoingRequest::body() { typedef Result Res; if (!this->body_) { - handle_state_->assert_valid(); outgoing_body_t body; - if (!wasi_http_0_2_0_types_method_outgoing_request_body( - wasi_http_0_2_0_types_borrow_outgoing_request({handle_state_->get()}), &body)) { + Borrow borrow(handle_state_.get()); + if (!wasi_http_0_2_0_types_method_outgoing_request_body(borrow, &body)) { return Res::err(154); } - body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandleState(body.__handle))); + body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandle(body))); } return Res::ok(body_); } -static Resource::HandleNS FUTURE_INCOMING_RESPONSE_HANDLE_NS = Resource::next_handle_ns("FutureHttpIncomingResponse"); Result HttpOutgoingRequest::send() { typedef Result Res; - handle_state_->assert_valid(); future_incoming_response_t ret; wasi_http_0_2_0_outgoing_handler_error_code_t err; - if (!wasi_http_0_2_0_outgoing_handler_handle({handle_state_->take()}, nullptr, &ret, &err)) { + auto request_handle = WASIHandle::cast(handle_state_.get())->take(); + if (!wasi_http_0_2_0_outgoing_handler_handle(request_handle, nullptr, &ret, &err)) { return Res::err(154); } - auto res = new FutureHttpIncomingResponse(std::make_unique(ret.__handle, FUTURE_INCOMING_RESPONSE_HANDLE_NS)); + auto res = new FutureHttpIncomingResponse(std::unique_ptr(new WASIHandle(ret))); return Result::ok(res); } -static Resource::HandleNS IN_BODY_HANDLE_NS = Resource::next_handle_ns("IncomingBody"); -class IncomingBodyHandleState final : public HandleState { - Handle stream_handle_; - PollableHandle pollable_handle_; - - friend HttpIncomingBody; - -public: - explicit IncomingBodyHandleState(const Handle handle) - : HandleState(handle, IN_BODY_HANDLE_NS), pollable_handle_(INVALID_POLLABLE_HANDLE) { - const borrow_incoming_body_t borrow = {handle}; - own_input_stream_t stream{}; - if (!wasi_http_0_2_0_types_method_incoming_body_stream(borrow, &stream)) { - MOZ_ASSERT_UNREACHABLE("Getting a body's stream should never fail"); - } - stream_handle_ = stream.__handle; - } -}; - void block_on_pollable_handle(PollableHandle handle) { wasi_io_0_2_0_poll_method_pollable_block({handle}); } @@ -903,16 +905,10 @@ HttpIncomingBody::HttpIncomingBody(std::unique_ptr state) : Pollabl Resource::~Resource() { if (handle_state_ != nullptr) { - delete handle_state_.release(); + handle_state_ = nullptr; } } -static Resource::HandleNS HANDLE_NS = 0; -Resource::HandleNS Resource::next_handle_ns(const char* ns_name) { - // DBG("Creating handle namespace %d with name %s\n", HANDLE_NS, ns_name); - return HANDLE_NS++; -} - bool Resource::valid() const { return this->handle_state_ != nullptr && this->handle_state_->valid(); } @@ -922,8 +918,8 @@ Result HttpIncomingBody::read(uint32_t chunk_size) wasi_io_0_2_0_streams_list_u8_t ret{}; wasi_io_0_2_0_streams_stream_error_t err{}; - auto borrow = borrow_input_stream_t( - {static_cast(handle_state_.get())->stream_handle_}); + auto body_handle = IncomingBodyHandle::cast(handle_state_.get()); + auto borrow = Borrow(body_handle->stream_handle_); bool success = wasi_io_0_2_0_streams_method_input_stream_read(borrow, chunk_size, &ret, &err); if (!success) { if (err.tag == WASI_IO_0_2_0_STREAMS_STREAM_ERROR_CLOSED) { @@ -938,13 +934,13 @@ Result HttpIncomingBody::read(uint32_t chunk_size) Result HttpIncomingBody::close() { return {}; } Result HttpIncomingBody::subscribe() { - auto state = static_cast(handle_state_.get()); - auto borrow = borrow_input_stream_t({state->stream_handle_}); + auto body_handle = IncomingBodyHandle::cast(handle_state_.get()); + auto borrow = Borrow(body_handle->stream_handle_); auto pollable = wasi_io_0_2_0_streams_method_input_stream_subscribe(borrow); return Result::ok(pollable.__handle); } void HttpIncomingBody::unsubscribe() { - auto state = static_cast(handle_state_.get()); + auto state = static_cast(handle_state_.get()); if (state->pollable_handle_ == INVALID_POLLABLE_HANDLE) { return; } @@ -956,12 +952,11 @@ FutureHttpIncomingResponse::FutureHttpIncomingResponse(std::unique_ptr> FutureHttpIncomingResponse::maybe_response() { typedef Result> Res; wasi_http_0_2_0_types_result_result_own_incoming_response_error_code_void_t res; - auto borrow = wasi_http_0_2_0_types_borrow_future_incoming_response({handle_state_->get()}); + Borrow borrow(handle_state_.get()); if (!wasi_http_0_2_0_types_method_future_incoming_response_get(borrow, &res)) { return Res::ok(std::nullopt); } @@ -974,11 +969,12 @@ Result> FutureHttpIncomingResponse::maybe_respo return Res::err(154); } - return Res::ok(new HttpIncomingResponse(std::make_unique(val.ok.__handle, IN_RESPONSE_HANDLE_NS))); + auto state = new WASIHandle(val.ok); + return Res::ok(new HttpIncomingResponse(std::unique_ptr(state))); } Result FutureHttpIncomingResponse::subscribe() { - auto borrow = wasi_http_0_2_0_types_borrow_future_incoming_response({handle_state_->get()}); + Borrow borrow(handle_state_.get()); auto pollable = wasi_http_0_2_0_types_method_future_incoming_response_subscribe(borrow); return Result::ok(pollable.__handle); } @@ -986,6 +982,10 @@ void FutureHttpIncomingResponse::unsubscribe() { // TODO: implement } +HttpHeadersReadOnly::HttpHeadersReadOnly() { + handle_state_ = nullptr; +} + HttpHeadersReadOnly::HttpHeadersReadOnly(std::unique_ptr state) { handle_state_ = std::move(state); } @@ -995,7 +995,7 @@ Result HttpIncomingResponse::status() { if (!valid()) { return Result::err(154); } - auto borrow = wasi_http_0_2_0_types_borrow_incoming_response_t({handle_state_->get()}); + auto borrow = Borrow(handle_state_.get()); status_ = wasi_http_0_2_0_types_method_incoming_response_status(borrow); } return Result::ok(status_); @@ -1010,9 +1010,10 @@ Result HttpIncomingResponse::headers() { if (!valid()) { return Result::err(154); } - auto res = wasi_http_0_2_0_types_method_incoming_response_headers( - wasi_http_0_2_0_types_borrow_incoming_response({handle_state_->get()})); - headers_ = new HttpHeadersReadOnly(std::make_unique(res.__handle, HEADERS_HANDLE_NS)); + auto borrow = Borrow(handle_state_.get()); + auto res = wasi_http_0_2_0_types_method_incoming_response_headers(borrow); + auto state = new WASIHandle(res); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(state)); } return Result::ok(headers_); @@ -1023,31 +1024,29 @@ Result HttpIncomingResponse::body() { if (!valid()) { return Result::err(154); } + auto borrow = Borrow(handle_state_.get()); incoming_body_t body; - if (!wasi_http_0_2_0_types_method_incoming_response_consume( - wasi_http_0_2_0_types_borrow_incoming_response({handle_state_->get()}), &body)) { + if (!wasi_http_0_2_0_types_method_incoming_response_consume(borrow, &body)) { return Result::err(154); } - body_ = new HttpIncomingBody(std::unique_ptr(new IncomingBodyHandleState(body.__handle))); + body_ = new HttpIncomingBody(std::unique_ptr(new IncomingBodyHandle(body))); } return Result::ok(body_); } HttpOutgoingResponse::HttpOutgoingResponse(std::unique_ptr state) { this->handle_state_ = std::move(state); } -static Resource::HandleNS OUT_RESPONSE_HANDLE_NS = Resource::next_handle_ns("HttpOutgoingResponse"); HttpOutgoingResponse *HttpOutgoingResponse::make(const uint16_t status, unique_ptr headers) { - wasi_http_0_2_0_types_own_headers_t owned{headers->handle_state_->take()}; - auto handle = wasi_http_0_2_0_types_constructor_outgoing_response(owned); - auto borrow = wasi_http_0_2_0_types_borrow_outgoing_response(handle); + auto owned_headers = WASIHandle::cast(headers->handle_state_.get())->take(); + auto handle = wasi_http_0_2_0_types_constructor_outgoing_response(owned_headers); - auto *state = new HandleState(handle.__handle, OUT_RESPONSE_HANDLE_NS); + auto *state = new WASIHandle(handle); auto *resp = new HttpOutgoingResponse(std::unique_ptr(state)); // Set the status if (status != 200) { // The DOM implementation is expected to have validated the status code already. - MOZ_RELEASE_ASSERT(wasi_http_0_2_0_types_method_outgoing_response_set_status_code(borrow, status)); + MOZ_RELEASE_ASSERT(wasi_http_0_2_0_types_method_outgoing_response_set_status_code(state->borrow(), status)); } resp->status_ = status; @@ -1059,9 +1058,10 @@ Result HttpOutgoingResponse::headers() { if (!valid()) { return Result::err(154); } - borrow_outgoing_response_t borrow(handle_state_->get()); + auto borrow = Borrow(handle_state_.get()); auto res = wasi_http_0_2_0_types_method_outgoing_response_headers(borrow); - headers_ = new HttpHeadersReadOnly(std::make_unique(res.__handle, HEADERS_HANDLE_NS)); + auto state = new WASIHandle(res); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(state)); } return Result::ok(headers_); @@ -1070,13 +1070,12 @@ Result HttpOutgoingResponse::headers() { Result HttpOutgoingResponse::body() { typedef Result Res; if (!this->body_) { - handle_state_->assert_valid(); + auto borrow = Borrow(handle_state_.get()); outgoing_body_t body; - if (!wasi_http_0_2_0_types_method_outgoing_response_body( - wasi_http_0_2_0_types_borrow_outgoing_response({handle_state_->get()}), &body)) { + if (!wasi_http_0_2_0_types_method_outgoing_response_body(borrow, &body)) { return Res::err(154); } - body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandleState(body.__handle))); + body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandle(body))); } return Res::ok(this->body_); } @@ -1092,9 +1091,9 @@ Result HttpIncomingRequest::method() { return Result::err(154); } } + auto borrow = Borrow(handle_state_.get()); wasi_http_0_2_0_types_method_t method; - wasi_http_0_2_0_types_method_incoming_request_method( - borrow_incoming_request_t(handle_state_->get()), &method); + wasi_http_0_2_0_types_method_incoming_request_method(borrow, &method); if (method.tag != WASI_HTTP_0_2_0_TYPES_METHOD_OTHER) { method_ = std::string(http_method_names[method.tag], strlen(http_method_names[method.tag])); } else { @@ -1109,9 +1108,10 @@ Result HttpIncomingRequest::headers() { if (!valid()) { return Result::err(154); } - borrow_incoming_request_t borrow(handle_state_->get()); + auto borrow = Borrow(handle_state_.get()); auto res = wasi_http_0_2_0_types_method_incoming_request_headers(borrow); - headers_ = new HttpHeadersReadOnly(std::make_unique(res.__handle, HEADERS_HANDLE_NS)); + auto state = new WASIHandle(res); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(state)); } return Result::ok(headers_); @@ -1122,12 +1122,12 @@ Result HttpIncomingRequest::body() { if (!valid()) { return Result::err(154); } + auto borrow = Borrow(handle_state_.get()); incoming_body_t body; - if (!wasi_http_0_2_0_types_method_incoming_request_consume( - borrow_incoming_request_t(handle_state_->get()), &body)) { + if (!wasi_http_0_2_0_types_method_incoming_request_consume(borrow, &body)) { return Result::err(154); } - body_ = new HttpIncomingBody(std::unique_ptr(new IncomingBodyHandleState(body.__handle))); + body_ = new HttpIncomingBody(std::unique_ptr(new IncomingBodyHandle(body))); } return Result::ok(body_); } @@ -1135,7 +1135,7 @@ Result HttpIncomingRequest::body() { } // namespace host_api static host_api::HttpIncomingRequest::RequestHandler REQUEST_HANDLER = nullptr; -static host_api::HandleState *RESPONSE_OUT = nullptr; +static exports_wasi_http_response_outparam RESPONSE_OUT; void host_api::HttpIncomingRequest::set_handler(RequestHandler handler) { MOZ_ASSERT(!REQUEST_HANDLER); @@ -1145,22 +1145,21 @@ void host_api::HttpIncomingRequest::set_handler(RequestHandler handler) { host_api::Result host_api::HttpOutgoingResponse::send() { wasi_http_0_2_0_types_result_own_outgoing_response_error_code_t result; + auto own = WASIHandle::cast(this->handle_state_.get())->take(); + result.is_err = false; - result.val.ok = {this->handle_state_->take()}; + result.val.ok = own; - wasi_http_0_2_0_types_static_response_outparam_set({RESPONSE_OUT->take()}, &result); + wasi_http_0_2_0_types_static_response_outparam_set(RESPONSE_OUT, &result); return {}; } -static host_api::Resource::HandleNS RESPONSE_OUTPARAM_HANDLE_NS = - host_api::Resource::next_handle_ns("ResponseOutParam"); -static host_api::Resource::HandleNS IN_REQUEST_HANDLE_NS = host_api::Resource::next_handle_ns("HttpIncomingRequest"); void exports_wasi_http_incoming_handler(exports_wasi_http_incoming_request request_handle, exports_wasi_http_response_outparam response_out) { - RESPONSE_OUT = new host_api::HandleState(response_out.__handle, RESPONSE_OUTPARAM_HANDLE_NS); - auto *request = new host_api::HttpIncomingRequest(std::make_unique(request_handle.__handle, IN_REQUEST_HANDLE_NS)); + RESPONSE_OUT = response_out; + auto state = new WASIHandle(request_handle); + auto *request = new host_api::HttpIncomingRequest(std::unique_ptr(state)); auto res = REQUEST_HANDLER(request); MOZ_RELEASE_ASSERT(res); - MOZ_RELEASE_ASSERT(!RESPONSE_OUT->valid()); } diff --git a/include/host_api.h b/include/host_api.h index 85e8876..ee0e560 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -325,8 +325,12 @@ class HttpHeadersReadOnly : public Resource { friend HttpOutgoingRequest; friend HttpHeaders; +protected: + // It's never valid to create an HttpHeadersReadOnly without a handle, + // but a subclass can create a handle and then assign it. + explicit HttpHeadersReadOnly(); + public: - HttpHeadersReadOnly() = delete; explicit HttpHeadersReadOnly(std::unique_ptr handle); HttpHeadersReadOnly(const HttpHeadersReadOnly &headers) = delete; From ec28998990b579cc9beef9fe1b571c46ec3a6459 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Sun, 2 Jun 2024 16:19:22 +0200 Subject: [PATCH 15/22] Fix bugs in runtime-evaluation --- CMakeLists.txt | 2 +- include/extension-api.h | 2 ++ runtime/engine.cpp | 16 ++++++++++------ runtime/script_loader.cpp | 26 +++++++++++++++----------- runtime/script_loader.h | 2 +- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e2c35fd..98c3e9d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,7 +96,7 @@ function(componentize OUTPUT) add_custom_command( OUTPUT ${OUTPUT}.wasm WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} - COMMAND ${CMAKE_COMMAND} -E env "PATH=${WASM_TOOLS_DIR};${WIZER_DIR};$ENV{PATH}" ${RUNTIME_DIR}/componentize.sh ${SOURCES} ${OUTPUT}.wasm + COMMAND ${CMAKE_COMMAND} -E env "PATH=${WASM_TOOLS_DIR};${WIZER_DIR};$ENV{PATH}" ${RUNTIME_DIR}/componentize.sh ${SOURCES} -o ${OUTPUT}.wasm DEPENDS ${ARG_SOURCES} ${RUNTIME_DIR}/componentize.sh starling.wasm VERBATIM ) diff --git a/include/extension-api.h b/include/extension-api.h index 8619276..790a2f4 100644 --- a/include/extension-api.h +++ b/include/extension-api.h @@ -67,6 +67,8 @@ class Engine { bool eval_toplevel(const char *path, MutableHandleValue result); bool eval_toplevel(JS::SourceText &source, const char *path, MutableHandleValue result); + + bool is_preinitializing(); bool toplevel_evaluated(); /** diff --git a/runtime/engine.cpp b/runtime/engine.cpp index 4d8529d..a61c719 100644 --- a/runtime/engine.cpp +++ b/runtime/engine.cpp @@ -201,6 +201,8 @@ bool fix_math_random(JSContext *cx, HandleObject global) { return JS_DefineFunctions(cx, math, funs); } +static api::Engine *ENGINE; + bool init_js() { JS_Init(); @@ -253,8 +255,9 @@ bool init_js() { // generating bytecode for functions. // https://searchfox.org/mozilla-central/rev/5b2d2863bd315f232a3f769f76e0eb16cdca7cb0/js/public/CompileOptions.h#571-574 opts->setForceFullParse(); - scriptLoader = new ScriptLoader(cx, opts); + scriptLoader = new ScriptLoader(ENGINE, opts); + // TODO: restore in a way that doesn't cause a dependency on the Performance builtin in the core runtime. // builtins::Performance::timeOrigin.emplace( // std::chrono::high_resolution_clock::now()); @@ -327,15 +330,13 @@ static void abort(JSContext *cx, const char *description) { exit(1); } -static api::Engine *ENGINE; - api::Engine::Engine() { // total_compute = 0; + ENGINE = this; bool result = init_js(); MOZ_RELEASE_ASSERT(result); JS::EnterRealm(cx(), global()); core::EventLoop::init(cx()); - ENGINE = this; } JSContext *api::Engine::cx() { return CONTEXT; } @@ -447,8 +448,10 @@ bool api::Engine::eval_toplevel(JS::SourceText &source, const // the shrinking GC causes them to be intermingled with other objects. I.e., // writes become more fragmented due to the shrinking GC. // https://github.com/fastly/js-compute-runtime/issues/224 - JS::PrepareForFullGC(cx); - JS::NonIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::API); + if (isWizening()) { + JS::PrepareForFullGC(cx); + JS::NonIncrementalGC(cx, JS::GCOptions::Normal, JS::GCReason::API); + } // Ignore the first GC, but then print all others, because ideally GCs // should be rare, and developers should know about them. @@ -460,6 +463,7 @@ bool api::Engine::eval_toplevel(JS::SourceText &source, const return true; } +bool api::Engine::is_preinitializing() { return isWizening(); } bool api::Engine::eval_toplevel(const char *path, MutableHandleValue result) { JS::SourceText source; diff --git a/runtime/script_loader.cpp b/runtime/script_loader.cpp index 06ca71a..fa4a472 100644 --- a/runtime/script_loader.cpp +++ b/runtime/script_loader.cpp @@ -9,7 +9,7 @@ #include #include -static JSContext* CONTEXT; +static api::Engine* ENGINE; static ScriptLoader* SCRIPT_LOADER; JS::PersistentRootedObject moduleRegistry; JS::PersistentRootedObject builtinModules; @@ -345,11 +345,12 @@ bool module_metadata_hook(JSContext* cx, HandleValue referencingPrivate, HandleO return true; } -ScriptLoader::ScriptLoader(JSContext *cx, JS::CompileOptions *opts) { +ScriptLoader::ScriptLoader(api::Engine* engine, JS::CompileOptions *opts) { MOZ_ASSERT(!SCRIPT_LOADER); + ENGINE = engine; SCRIPT_LOADER = this; - CONTEXT = cx; COMPILE_OPTS = opts; + JSContext* cx = engine->cx(); moduleRegistry.init(cx, JS::NewMapObject(cx)); builtinModules.init(cx, JS::NewMapObject(cx)); MOZ_RELEASE_ASSERT(moduleRegistry); @@ -360,21 +361,22 @@ ScriptLoader::ScriptLoader(JSContext *cx, JS::CompileOptions *opts) { } bool ScriptLoader::define_builtin_module(const char* id, HandleValue builtin) { - RootedString id_str(CONTEXT, JS_NewStringCopyZ(CONTEXT, id)); + JSContext* cx = ENGINE->cx(); + RootedString id_str(cx, JS_NewStringCopyZ(cx, id)); if (!id_str) { return false; } - RootedValue module_val(CONTEXT); - RootedValue id_val(CONTEXT, StringValue(id_str)); + RootedValue module_val(cx); + RootedValue id_val(cx, StringValue(id_str)); bool already_exists; - if (!MapHas(CONTEXT, builtinModules, id_val, &already_exists)) { + if (!MapHas(cx, builtinModules, id_val, &already_exists)) { return false; } if (already_exists) { fprintf(stderr, "Unable to define builtin %s, as it already exists", id); return false; } - if (!MapSet(CONTEXT, builtinModules, id_val, builtin)) { + if (!MapSet(cx, builtinModules, id_val, builtin)) { return false; } return true; @@ -444,7 +446,7 @@ bool ScriptLoader::load_script(JSContext *cx, const char *script_path, bool ScriptLoader::eval_top_level_script(const char *path, JS::SourceText &source, MutableHandleValue result, MutableHandleValue tla_promise) { - JSContext *cx = CONTEXT; + JSContext *cx = ENGINE->cx(); JS::CompileOptions opts(cx, *COMPILE_OPTS); opts.setFileAndLine(strip_base(path, BASE_PATH), 1); @@ -482,8 +484,10 @@ bool ScriptLoader::eval_top_level_script(const char *path, JS::SourceTextis_preinitializing()) { + JS::PrepareForFullGC(cx); + JS::NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::API); + } // Execute the top-level module script. if (!MODULE_MODE) { diff --git a/runtime/script_loader.h b/runtime/script_loader.h index 460b58d..e7022ef 100644 --- a/runtime/script_loader.h +++ b/runtime/script_loader.h @@ -14,7 +14,7 @@ class ScriptLoader { public: - ScriptLoader(JSContext* cx, JS::CompileOptions* opts); + ScriptLoader(api::Engine* engine, JS::CompileOptions* opts); ~ScriptLoader(); bool define_builtin_module(const char* id, HandleValue builtin); From 429daecc349e7464297b1ecd783e814d34fe954b Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Sun, 2 Jun 2024 16:59:46 +0200 Subject: [PATCH 16/22] Fix bugs in headers handling --- builtins/web/fetch/headers.cpp | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/builtins/web/fetch/headers.cpp b/builtins/web/fetch/headers.cpp index 2281190..4b1373a 100644 --- a/builtins/web/fetch/headers.cpp +++ b/builtins/web/fetch/headers.cpp @@ -158,6 +158,11 @@ host_api::HostString normalize_header_value(JSContext *cx, HandleValue value_val } } + if (*value_changed) { + memmove(value_chars, value_chars + start, end - start); + value.len = end - start; + } + return value; } @@ -456,6 +461,25 @@ bool append_single_normalized_header_value(JSContext *cx, HandleObject self, return false; } + RootedValue entry(cx); + if (!MapGet(cx, entries, name_val, &entry)) { + return false; + } + + if (!entry.isUndefined()) { + RootedString entry_str(cx, JS::ToString(cx, entry)); + entry_str = JS_ConcatStrings(cx, entry_str, comma); + if (!entry_str) { + return false; + } + RootedString val_str(cx, value_val.toString()); + entry_str = JS_ConcatStrings(cx, entry_str, val_str); + if (!entry_str) { + return false; + } + value_val.setString(entry_str); + } + if (!MapSet(cx, entries, name_val, value_val)) { return false; } @@ -618,7 +642,7 @@ bool Headers::has(JSContext *cx, unsigned argc, JS::Value *vp) { NORMALIZE_NAME(args[0], "Headers.has") Mode mode = Headers::mode(self); - if (mode == Headers::Mode::Uninitialized) { + if (mode == Mode::Uninitialized) { args.rval().setBoolean(false); return true; } @@ -634,8 +658,13 @@ bool Headers::has(JSContext *cx, unsigned argc, JS::Value *vp) { if (!entries) { return false; } + + RootedValue name_val(cx); + if (!redecode_str_if_changed(cx, args[0], name_chars, name_changed, &name_val)) { + return false; + } bool has; - if (!MapHas(cx, entries, args[0], &has)) { + if (!MapHas(cx, entries, name_val, &has)) { return false; } args.rval().setBoolean(has); From a0781f0ca347b5cf0cb3af3060e332ce64e91e2e Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Sun, 2 Jun 2024 21:20:12 +0200 Subject: [PATCH 17/22] Make the `extract body` spec operation work for outgoing requests and responses --- builtins/web/fetch/headers.cpp | 14 ++++++++++++-- builtins/web/fetch/headers.h | 1 + builtins/web/fetch/request-response.cpp | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/builtins/web/fetch/headers.cpp b/builtins/web/fetch/headers.cpp index 4b1373a..edac89d 100644 --- a/builtins/web/fetch/headers.cpp +++ b/builtins/web/fetch/headers.cpp @@ -523,8 +523,18 @@ void init_from_handle(JSObject* self, host_api::HttpHeadersReadOnly* handle) { SetReservedSlot(self, static_cast(Headers::Slots::Handle), PrivateValue(handle)); } +JSObject *Headers::create(JSContext *cx) { + JSObject* self = JS_NewObjectWithGivenProto(cx, &class_, proto_obj); + if (!self) { + return nullptr; + } + SetReservedSlot(self, static_cast(Slots::Mode), + JS::Int32Value(static_cast(Mode::Uninitialized))); + return self; +} + JSObject *Headers::create(JSContext *cx, host_api::HttpHeadersReadOnly *handle) { - RootedObject self(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + RootedObject self(cx, create(cx)); if (!self) { return nullptr; } @@ -533,7 +543,7 @@ JSObject *Headers::create(JSContext *cx, host_api::HttpHeadersReadOnly *handle) } JSObject *Headers::create(JSContext *cx, HandleValue init_headers) { - RootedObject self(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj)); + RootedObject self(cx, create(cx)); if (!self) { return nullptr; } diff --git a/builtins/web/fetch/headers.h b/builtins/web/fetch/headers.h index e9a946f..9d7247b 100644 --- a/builtins/web/fetch/headers.h +++ b/builtins/web/fetch/headers.h @@ -97,6 +97,7 @@ class Headers final : public BuiltinImpl { static bool init_class(JSContext *cx, HandleObject global); static bool constructor(JSContext *cx, unsigned argc, Value *vp); + static JSObject *create(JSContext *cx); static JSObject *create(JSContext *cx, HandleValue init_headers); static JSObject *create(JSContext *cx, HandleObject self, HandleValue init_headers); static JSObject *create(JSContext *cx, host_api::HttpHeadersReadOnly *handle); diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index d065411..33c7698 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -379,6 +379,7 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self, chunk.setObject(*array); } + // Set a __proto__-less source so modifying Object.prototype doesn't change the behavior. RootedObject source(cx, JS_NewObjectWithGivenProto(cx, nullptr, nullptr)); if (!source) { return false; @@ -468,7 +469,7 @@ JSObject *RequestOrResponse::headers(JSContext *cx, JS::HandleObject obj) { if (is_incoming(obj) && (handle = headers_handle(obj))) { headers = Headers::create(cx, handle); } else { - headers = Headers::create(cx, NullHandleValue); + headers = Headers::create(cx); } if (!headers) { return nullptr; From 7f4ba5a047a5e156ff99d1934bc33db807e68974 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Sun, 2 Jun 2024 21:38:49 +0200 Subject: [PATCH 18/22] Lock request/response body when consuming it for `.text()`, `.arrayBuffer()`, or `.json()` --- builtins/web/fetch/request-response.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index 33c7698..eb10e1b 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -806,7 +806,8 @@ template bool RequestOrResponse::bodyAll(JSContext *cx, JS::CallArgs args, JS::HandleObject self) { // TODO: mark body as consumed when operating on stream, too. if (body_used(self)) { - JS_ReportErrorASCII(cx, "Body has already been consumed"); + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_RESPONSE_BODY_DISTURBED_OR_LOCKED); return ReturnPromiseRejectedWithPendingError(cx, args); } @@ -843,6 +844,7 @@ bool RequestOrResponse::bodyAll(JSContext *cx, JS::CallArgs args, JS::HandleObje return false; } + SetReservedSlot(self, static_cast(Slots::BodyUsed), JS::BooleanValue(true)); JS::RootedValue extra(cx, JS::ObjectValue(*stream)); if (!enqueue_internal_method(cx, self, extra)) { return ReturnPromiseRejectedWithPendingError(cx, args); From e2f3b377702d346d6940ec9b8352d2da4fd73227 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Sun, 2 Jun 2024 22:18:35 +0200 Subject: [PATCH 19/22] Fix handling of various value types when extracting request/response bodies --- builtins/web/fetch/request-response.cpp | 26 +++++++++---------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index eb10e1b..c49f109 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -323,9 +323,6 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self, char *buf = nullptr; size_t length = 0; - // Must be declared here to keep the buffer alive. - host_api::HostString text; - if (body_obj && JS_IsArrayBufferViewObject(body_obj)) { length = JS_GetArrayBufferViewByteLength(body_obj); buf = static_cast(js_malloc(length)); @@ -342,42 +339,37 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self, if (!buffer) { return false; } + length = GetArrayBufferByteLength(buffer); } else if (body_obj && url::URLSearchParams::is_instance(body_obj)) { auto slice = url::URLSearchParams::serialize(cx, body_obj); buf = (char *)slice.data; length = slice.len; content_type = "application/x-www-form-urlencoded;charset=UTF-8"; } else { - text = core::encode(cx, body_val); + auto text = core::encode(cx, body_val); if (!text.ptr) { return false; } - buf = text.ptr.get(); + buf = text.ptr.release(); length = text.len; content_type = "text/plain;charset=UTF-8"; } - if (buf) { - MOZ_ASSERT(!buffer); + if (!buffer) { + MOZ_ASSERT_IF(length, buf); buffer = NewArrayBufferWithContents(cx, length, buf, JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory); - if (text.ptr) { - static_cast(text.ptr.release()); - } if (!buffer) { js_free(buf); return false; } - chunk.setObject(*buffer); } - if (buffer) { - RootedObject array(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 0, length)); - if (!array) { - return false; - } - chunk.setObject(*array); + RootedObject array(cx, JS_NewUint8ArrayWithBuffer(cx, buffer, 0, length)); + if (!array) { + return false; } + chunk.setObject(*array); // Set a __proto__-less source so modifying Object.prototype doesn't change the behavior. RootedObject source(cx, JS_NewObjectWithGivenProto(cx, nullptr, nullptr)); From 4820fdff5a4cdd98461f0a24331dc934fcf580a8 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 3 Jun 2024 16:39:41 +0200 Subject: [PATCH 20/22] Remove some outdated comments and TODOs --- builtins/web/fetch/request-response.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index c49f109..b088093 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -270,11 +270,6 @@ bool RequestOrResponse::body_unusable(JSContext *cx, JS::HandleObject body) { * Implementation of the `extract a body` algorithm at * https://fetch.spec.whatwg.org/#concept-bodyinit-extract * - * Note: our implementation is somewhat different from what the spec describes - * in that we immediately write all non-streaming body types to the host instead - * of creating a stream for them. We don't have threads, so there's nothing "in - * parallel" to be had anyway. - * * Note: also includes the steps applying the `Content-Type` header from the * Request and Response constructors in step 36 and 8 of those, respectively. */ @@ -2468,16 +2463,6 @@ bool Response::constructor(JSContext *cx, unsigned argc, JS::Value *vp) { // be consumed by the content creating it, so we're lenient about its format. // 3. Set `this`’s `response` to a new `response`. - // TODO(performance): consider not creating a host-side representation for responses - // eagerly. Some applications create Response objects purely for internal use, - // e.g. to represent cache entries. While that's perhaps not ideal to begin - // with, it exists, so we should handle it in a good way, and not be - // superfluously slow. - // https://github.com/fastly/js-compute-runtime/issues/219 - // TODO(performance): enable creating Response objects during the init phase, and only - // creating the host-side representation when processing requests. - // https://github.com/fastly/js-compute-runtime/issues/220 - // 5. (Reordered) Set `this`’s `response`’s `status` to `init`["status"]. // 7. (Reordered) If `init`["headers"] `exists`, then `fill` `this`’s `headers` with From f992afe735a86fdee0906712de09c97f0c71a3b5 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 3 Jun 2024 17:24:04 +0200 Subject: [PATCH 21/22] Ensure that the `ResponseFutureTask` is enqueued for streaming requests --- builtins/web/fetch/fetch-api.cpp | 64 ++----------------------- builtins/web/fetch/request-response.cpp | 17 +++++-- builtins/web/fetch/request-response.h | 59 +++++++++++++++++++++++ 3 files changed, 76 insertions(+), 64 deletions(-) diff --git a/builtins/web/fetch/fetch-api.cpp b/builtins/web/fetch/fetch-api.cpp index 520de25..45dc10b 100644 --- a/builtins/web/fetch/fetch-api.cpp +++ b/builtins/web/fetch/fetch-api.cpp @@ -11,64 +11,6 @@ namespace builtins::web::fetch { static api::Engine *ENGINE; -class ResponseFutureTask final : public api::AsyncTask { - Heap request_; - host_api::FutureHttpIncomingResponse *future_; - -public: - explicit ResponseFutureTask(const HandleObject request, - host_api::FutureHttpIncomingResponse *future) - : request_(request), future_(future) { - auto res = future->subscribe(); - MOZ_ASSERT(!res.is_err(), "Subscribing to a future should never fail"); - handle_ = res.unwrap(); - } - - [[nodiscard]] bool run(api::Engine *engine) override { - // MOZ_ASSERT(ready()); - JSContext *cx = engine->cx(); - - const RootedObject request(cx, request_); - RootedObject response_promise(cx, Request::response_promise(request)); - - auto res = future_->maybe_response(); - if (res.is_err()) { - JS_ReportErrorUTF8(cx, "NetworkError when attempting to fetch resource."); - return RejectPromiseWithPendingError(cx, response_promise); - } - - auto maybe_response = res.unwrap(); - MOZ_ASSERT(maybe_response.has_value()); - auto response = maybe_response.value(); - RootedObject response_obj( - cx, JS_NewObjectWithGivenProto(cx, &Response::class_, Response::proto_obj)); - if (!response_obj) { - return false; - } - - response_obj = Response::create_incoming(cx, response_obj, response); - if (!response_obj) { - return false; - } - - RequestOrResponse::set_url(response_obj, RequestOrResponse::url(request)); - RootedValue response_val(cx, ObjectValue(*response_obj)); - if (!ResolvePromise(cx, response_promise, response_val)) { - return false; - } - - return cancel(engine); - } - - [[nodiscard]] bool cancel(api::Engine *engine) override { - // TODO(TS): implement - handle_ = -1; - return true; - } - - void trace(JSTracer *trc) override { TraceEdge(trc, &request_, "Request for response future"); } -}; - // TODO: throw in all Request methods/getters that rely on host calls once a // request has been sent. The host won't let us act on them anymore anyway. /** @@ -158,8 +100,10 @@ bool fetch(JSContext *cx, unsigned argc, Value *vp) { ENGINE->queue_async_task(new ResponseFutureTask(request_obj, pending_handle)); } - JS::SetReservedSlot(request_obj, static_cast(Request::Slots::ResponsePromise), - ObjectValue(*response_promise)); + SetReservedSlot(request_obj, static_cast(Request::Slots::ResponsePromise), + ObjectValue(*response_promise)); + SetReservedSlot(request_obj, static_cast(Request::Slots::PendingResponseHandle), + PrivateValue(pending_handle)); args.rval().setObject(*response_promise); return true; diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index b088093..349b4f5 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -906,6 +906,12 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject return false; if (done_val.toBoolean()) { + auto res = body->close(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + // The only response we ever send is the one passed to // `FetchEvent#respondWith` to send to the client. As such, we can be // certain that if we have a response here, we can advance the FetchState to @@ -916,10 +922,13 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject fetch_event::FetchEvent::State::responseDone); } - auto res = body->close(); - if (auto *err = res.to_err()) { - HANDLE_ERROR(cx, *err); - return false; + if (Request::is_instance(body_owner)) { + auto pending_handle = + static_cast( + GetReservedSlot(body_owner, + static_cast(Request::Slots::PendingResponseHandle)) + .toPrivate()); + ENGINE->queue_async_task(new ResponseFutureTask(body_owner, pending_handle)); } return true; diff --git a/builtins/web/fetch/request-response.h b/builtins/web/fetch/request-response.h index b7af49e..13ae335 100644 --- a/builtins/web/fetch/request-response.h +++ b/builtins/web/fetch/request-response.h @@ -139,6 +139,7 @@ class Request final : public BuiltinImpl { URL = static_cast(RequestOrResponse::Slots::URL), Method = static_cast(RequestOrResponse::Slots::Count), ResponsePromise, + PendingResponseHandle, Count, }; @@ -217,6 +218,64 @@ class Response final : public BuiltinImpl { static void set_status_message_from_code(JSContext *cx, JSObject *obj, uint16_t code); }; +class ResponseFutureTask final : public api::AsyncTask { + Heap request_; + host_api::FutureHttpIncomingResponse *future_; + +public: + explicit ResponseFutureTask(const HandleObject request, + host_api::FutureHttpIncomingResponse *future) + : request_(request), future_(future) { + auto res = future->subscribe(); + MOZ_ASSERT(!res.is_err(), "Subscribing to a future should never fail"); + handle_ = res.unwrap(); + } + + [[nodiscard]] bool run(api::Engine *engine) override { + // MOZ_ASSERT(ready()); + JSContext *cx = engine->cx(); + + const RootedObject request(cx, request_); + RootedObject response_promise(cx, Request::response_promise(request)); + + auto res = future_->maybe_response(); + if (res.is_err()) { + JS_ReportErrorUTF8(cx, "NetworkError when attempting to fetch resource."); + return RejectPromiseWithPendingError(cx, response_promise); + } + + auto maybe_response = res.unwrap(); + MOZ_ASSERT(maybe_response.has_value()); + auto response = maybe_response.value(); + RootedObject response_obj( + cx, JS_NewObjectWithGivenProto(cx, &Response::class_, Response::proto_obj)); + if (!response_obj) { + return false; + } + + response_obj = Response::create_incoming(cx, response_obj, response); + if (!response_obj) { + return false; + } + + RequestOrResponse::set_url(response_obj, RequestOrResponse::url(request)); + RootedValue response_val(cx, ObjectValue(*response_obj)); + if (!ResolvePromise(cx, response_promise, response_val)) { + return false; + } + + return cancel(engine); + } + + [[nodiscard]] bool cancel(api::Engine *engine) override { + // TODO(TS): implement + handle_ = -1; + return true; + } + + void trace(JSTracer *trc) override { TraceEdge(trc, &request_, "Request for response future"); } +}; + } // namespace fetch } // namespace web } // namespace builtins From f40bafb1f6432153a4f88f71768d76f1e4119349 Mon Sep 17 00:00:00 2001 From: Till Schneidereit Date: Mon, 3 Jun 2024 19:11:48 -0400 Subject: [PATCH 22/22] Fix streaming of outgoing bodies and setting their content-length header --- builtins/web/fetch/fetch_event.cpp | 4 +- builtins/web/fetch/headers.cpp | 6 +- builtins/web/fetch/headers.h | 3 +- builtins/web/fetch/request-response.cpp | 102 +++++++++++++----------- builtins/web/fetch/request-response.h | 5 -- host-apis/wasi-0.2.0/host_api.cpp | 43 +++++++--- include/extension-api.h | 3 + include/host_api.h | 3 +- 8 files changed, 98 insertions(+), 71 deletions(-) diff --git a/builtins/web/fetch/fetch_event.cpp b/builtins/web/fetch/fetch_event.cpp index d1fbb10..c9f6e75 100644 --- a/builtins/web/fetch/fetch_event.cpp +++ b/builtins/web/fetch/fetch_event.cpp @@ -186,7 +186,9 @@ bool start_response(JSContext *cx, JS::HandleObject response_obj, bool streaming auto *source_body = static_cast(existing_handle)->body().unwrap(); auto *dest_body = response->body().unwrap(); - auto res = dest_body->append(ENGINE, source_body); + // TODO: check if we should add a callback here and do something in response to body + // streaming being finished. + auto res = dest_body->append(ENGINE, source_body, nullptr, nullptr); if (auto *err = res.to_err()) { HANDLE_ERROR(cx, *err); return false; diff --git a/builtins/web/fetch/headers.cpp b/builtins/web/fetch/headers.cpp index edac89d..18533c6 100644 --- a/builtins/web/fetch/headers.cpp +++ b/builtins/web/fetch/headers.cpp @@ -694,7 +694,7 @@ bool Headers::append(JSContext *cx, unsigned argc, JS::Value *vp) { return true; } -bool Headers::set_if_undefined(JSContext *cx, JS::HandleObject self, const char *name, const char *value) { +bool Headers::set_if_undefined(JSContext *cx, HandleObject self, string_view name, string_view value) { if (!prepare_for_entries_modification(cx, self)) { return false; } @@ -717,7 +717,7 @@ bool Headers::set_if_undefined(JSContext *cx, JS::HandleObject self, const char MOZ_ASSERT(mode(self) == Mode::ContentOnly); RootedObject entries(cx, get_entries(cx, self)); - RootedString name_str(cx, JS_NewStringCopyZ(cx, name)); + RootedString name_str(cx, core::decode(cx, name)); if (!name_str) { return false; } @@ -731,7 +731,7 @@ bool Headers::set_if_undefined(JSContext *cx, JS::HandleObject self, const char return true; } - RootedString value_str(cx, JS_NewStringCopyZ(cx, value)); + RootedString value_str(cx, core::decode(cx, value)); if (!value_str) { return false; } diff --git a/builtins/web/fetch/headers.h b/builtins/web/fetch/headers.h index 9d7247b..2a29876 100644 --- a/builtins/web/fetch/headers.h +++ b/builtins/web/fetch/headers.h @@ -70,7 +70,8 @@ class Headers final : public BuiltinImpl { * Adds the given header name/value to `self`'s list of headers iff `self` * doesn't already contain a header with that name. */ - static bool set_if_undefined(JSContext *cx, JS::HandleObject self, const char *name, const char *value); + static bool set_if_undefined(JSContext *cx, JS::HandleObject self, string_view name, + string_view value); /// Appends a value for a header name. // diff --git a/builtins/web/fetch/request-response.cpp b/builtins/web/fetch/request-response.cpp index 349b4f5..2d6bb11 100644 --- a/builtins/web/fetch/request-response.cpp +++ b/builtins/web/fetch/request-response.cpp @@ -280,6 +280,7 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self, MOZ_ASSERT(!body_val.isNullOrUndefined()); const char *content_type = nullptr; + mozilla::Maybe content_length; // We currently support five types of body inputs: // - byte sequence @@ -381,7 +382,6 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self, MOZ_ASSERT(ReadableStreamIsDisturbed(cx, body_stream, &disturbed)); MOZ_ASSERT(!disturbed); - if (!ReadableStreamEnqueue(cx, body_stream, chunk) || !ReadableStreamClose(cx, body_stream)) { return false; @@ -389,15 +389,24 @@ bool RequestOrResponse::extract_body(JSContext *cx, JS::HandleObject self, JS_SetReservedSlot(self, static_cast(Slots::BodyStream), ObjectValue(*body_stream)); + content_length.emplace(length); } - // Step 36.3 of Request constructor / 8.4 of Response constructor. - if (content_type) { + if (content_type || content_length.isSome()) { JS::RootedObject headers(cx, RequestOrResponse::headers(cx, self)); if (!headers) { return false; } - if (!Headers::set_if_undefined(cx, headers, "content-type", content_type)) { + + if (content_length.isSome()) { + auto length_str = std::to_string(content_length.value()); + if (!Headers::set_if_undefined(cx, headers, "content-length", length_str)) { + return false; + } + } + + // Step 36.3 of Request constructor / 8.4 of Response constructor. + if (content_type && !Headers::set_if_undefined(cx, headers, "content-type", content_type)) { return false; } } @@ -428,12 +437,42 @@ unique_ptr RequestOrResponse::headers_clone(JSContext* cx return unique_ptr(res.unwrap()->clone()); } +bool finish_outgoing_body_streaming(JSContext* cx, HandleObject body_owner) { + // The only response we ever send is the one passed to + // `FetchEvent#respondWith` to send to the client. As such, we can be + // certain that if we have a response here, we can advance the FetchState to + // `responseDone`. + // TODO(TS): factor this out to remove dependency on fetch-event.h + auto body = RequestOrResponse::outgoing_body_handle(body_owner); + auto res = body->close(); + if (auto *err = res.to_err()) { + HANDLE_ERROR(cx, *err); + return false; + } + + if (Response::is_instance(body_owner)) { + fetch_event::FetchEvent::set_state(fetch_event::FetchEvent::instance(), + fetch_event::FetchEvent::State::responseDone); + } + + if (Request::is_instance(body_owner)) { + auto pending_handle = static_cast( + GetReservedSlot(body_owner, static_cast(Request::Slots::PendingResponseHandle)) + .toPrivate()); + SetReservedSlot(body_owner, static_cast(Request::Slots::PendingResponseHandle), + PrivateValue(nullptr)); + ENGINE->queue_async_task(new ResponseFutureTask(body_owner, pending_handle)); + } + + return true; +} + bool RequestOrResponse::append_body(JSContext *cx, JS::HandleObject self, JS::HandleObject source) { MOZ_ASSERT(!body_used(source)); MOZ_ASSERT(!body_used(self)); host_api::HttpIncomingBody *source_body = incoming_body_handle(source); host_api::HttpOutgoingBody *dest_body = outgoing_body_handle(self); - auto res = dest_body->append(ENGINE, source_body); + auto res = dest_body->append(ENGINE, source_body, finish_outgoing_body_streaming, self); if (auto *err = res.to_err()) { HANDLE_ERROR(cx, *err); return false; @@ -888,14 +927,14 @@ bool RequestOrResponse::body_source_cancel_algorithm(JSContext *cx, JS::CallArgs return true; } -bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject body_owner, - JS::HandleValue extra, JS::CallArgs args) { +bool reader_for_outgoing_body_then_handler(JSContext *cx, JS::HandleObject body_owner, + JS::HandleValue extra, JS::CallArgs args) { + ENGINE->dump_value(RequestOrResponse::url(body_owner)); JS::RootedObject then_handler(cx, &args.callee()); // The reader is stored in the catch handler, which we need here as well. // So we get that first, then the reader. JS::RootedObject catch_handler(cx, &extra.toObject()); JS::RootedObject reader(cx, &js::GetFunctionNativeReserved(catch_handler, 1).toObject()); - auto body = outgoing_body_handle(body_owner); // We're guaranteed to work with a native ReadableStreamDefaultReader here, // which in turn is guaranteed to vend {done: bool, value: any} objects to @@ -906,32 +945,7 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject return false; if (done_val.toBoolean()) { - auto res = body->close(); - if (auto *err = res.to_err()) { - HANDLE_ERROR(cx, *err); - return false; - } - - // The only response we ever send is the one passed to - // `FetchEvent#respondWith` to send to the client. As such, we can be - // certain that if we have a response here, we can advance the FetchState to - // `responseDone`. - // TODO(TS): factor this out to remove dependency on fetch-event.h - if (Response::is_instance(body_owner)) { - fetch_event::FetchEvent::set_state(fetch_event::FetchEvent::instance(), - fetch_event::FetchEvent::State::responseDone); - } - - if (Request::is_instance(body_owner)) { - auto pending_handle = - static_cast( - GetReservedSlot(body_owner, - static_cast(Request::Slots::PendingResponseHandle)) - .toPrivate()); - ENGINE->queue_async_task(new ResponseFutureTask(body_owner, pending_handle)); - } - - return true; + return finish_outgoing_body_streaming(cx, body_owner); } JS::RootedValue val(cx); @@ -943,16 +957,10 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject // reject the request promise if (Request::is_instance(body_owner)) { JS::RootedObject response_promise(cx, Request::response_promise(body_owner)); - JS::RootedValue exn(cx); // TODO: this should be a TypeError, but I'm not sure how to make that work JS_ReportErrorUTF8(cx, "TypeError"); - if (!JS_GetPendingException(cx, &exn)) { - return false; - } - JS_ClearPendingException(cx); - - return JS::RejectPromise(cx, response_promise, exn); + return RejectPromiseWithPendingError(cx, response_promise); } // TODO: should we also create a rejected promise if a response reads something that's not a @@ -971,6 +979,7 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject uint8_t *bytes = JS_GetUint8ArrayData(array, &is_shared, nogc); size_t length = JS_GetTypedArrayByteLength(array); // TODO: change this to write in chunks, respecting backpressure. + auto body = RequestOrResponse::outgoing_body_handle(body_owner); res = body->write_all(bytes, length); } @@ -980,6 +989,7 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject return false; } + // Read the next chunk. JS::RootedObject promise(cx, JS::ReadableStreamDefaultReaderRead(cx, reader)); if (!promise) { @@ -989,8 +999,8 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject return JS::AddPromiseReactions(cx, promise, then_handler, catch_handler); } -bool RequestOrResponse::body_reader_catch_handler(JSContext *cx, JS::HandleObject body_owner, - JS::HandleValue extra, JS::CallArgs args) { +bool reader_for_outgoing_body_catch_handler(JSContext *cx, JS::HandleObject body_owner, + JS::HandleValue extra, JS::CallArgs args) { // TODO: check if this should create a rejected promise instead, so an // in-content handler for unhandled rejections could deal with it. The body // stream errored during the streaming send. Not much we can do, but at least @@ -1041,8 +1051,6 @@ bool RequestOrResponse::maybe_stream_body(JSContext *cx, JS::HandleObject body_o // If the body stream is backed by an HTTP body handle, we can directly pipe // that handle into the body we're about to send. - // TODO: check if this case can still be hit. Given that we're not creating outgoing responses - // anymore until sending them, it probably can't. if (streams::NativeStreamSource::stream_is_body(cx, stream)) { MOZ_ASSERT(!is_incoming(body_owner)); // First, directly append the source's body to the target's and lock the stream. @@ -1070,13 +1078,13 @@ bool RequestOrResponse::maybe_stream_body(JSContext *cx, JS::HandleObject body_o // perform another operation in this way. JS::RootedObject catch_handler(cx); JS::RootedValue extra(cx, JS::ObjectValue(*reader)); - catch_handler = create_internal_method(cx, body_owner, extra); + catch_handler = create_internal_method(cx, body_owner, extra); if (!catch_handler) return false; JS::RootedObject then_handler(cx); extra.setObject(*catch_handler); - then_handler = create_internal_method(cx, body_owner, extra); + then_handler = create_internal_method(cx, body_owner, extra); if (!then_handler) return false; diff --git a/builtins/web/fetch/request-response.h b/builtins/web/fetch/request-response.h index 13ae335..f7220c3 100644 --- a/builtins/web/fetch/request-response.h +++ b/builtins/web/fetch/request-response.h @@ -91,11 +91,6 @@ class RequestOrResponse final { JS::HandleValue reason); static bool body_source_pull_algorithm(JSContext *cx, JS::CallArgs args, JS::HandleObject source, JS::HandleObject body_owner, JS::HandleObject controller); - static bool body_reader_then_handler(JSContext *cx, JS::HandleObject body_owner, - JS::HandleValue extra, JS::CallArgs args); - - static bool body_reader_catch_handler(JSContext *cx, JS::HandleObject body_owner, - JS::HandleValue extra, JS::CallArgs args); /** * Ensures that the given |body_owner|'s body is properly streamed, if it diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index bf24858..7cd326e 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -604,16 +604,30 @@ class BodyAppendTask final : public api::AsyncTask { HttpOutgoingBody *outgoing_body_; PollableHandle incoming_pollable_; PollableHandle outgoing_pollable_; + + api::TaskCompletionCallback cb_; + Heap cb_receiver_; State state_; - void set_state(const State state) { + void set_state(JSContext* cx, const State state) { MOZ_ASSERT(state_ != State::Done); state_ = state; + if (state == State::Done && cb_) { + RootedObject receiver(cx, cb_receiver_); + cb_(cx, receiver); + cb_ = nullptr; + cb_receiver_ = nullptr; + } } public: - explicit BodyAppendTask(HttpIncomingBody *incoming_body, HttpOutgoingBody *outgoing_body) - : incoming_body_(incoming_body), outgoing_body_(outgoing_body) { + explicit BodyAppendTask(api::Engine *engine, + HttpIncomingBody *incoming_body, + HttpOutgoingBody *outgoing_body, + api::TaskCompletionCallback completion_callback, + HandleObject callback_receiver) + : incoming_body_(incoming_body), outgoing_body_(outgoing_body), + cb_(completion_callback) { auto res = incoming_body_->subscribe(); MOZ_ASSERT(!res.is_err()); incoming_pollable_ = res.unwrap(); @@ -622,7 +636,9 @@ class BodyAppendTask final : public api::AsyncTask { MOZ_ASSERT(!res.is_err()); outgoing_pollable_ = res.unwrap(); - state_ = State::BlockedOnBoth; + cb_receiver_ = callback_receiver; + + set_state(engine->cx(), State::BlockedOnBoth); } [[nodiscard]] bool run(api::Engine *engine) override { @@ -633,10 +649,10 @@ class BodyAppendTask final : public api::AsyncTask { MOZ_ASSERT(!res.is_err()); auto [done, _] = std::move(res.unwrap()); if (done) { - set_state(State::Done); + set_state(engine->cx(), State::Done); return true; } - set_state(State::BlockedOnOutgoing); + set_state(engine->cx(), State::BlockedOnOutgoing); } uint64_t capacity = 0; @@ -647,7 +663,7 @@ class BodyAppendTask final : public api::AsyncTask { } capacity = res.unwrap(); if (capacity > 0) { - set_state(State::Ready); + set_state(engine->cx(), State::Ready); } else { engine->queue_async_task(this); return true; @@ -665,7 +681,7 @@ class BodyAppendTask final : public api::AsyncTask { } auto [done, bytes] = std::move(res.unwrap()); if (bytes.len == 0 && !done) { - set_state(State::BlockedOnIncoming); + set_state(engine->cx(), State::BlockedOnIncoming); engine->queue_async_task(this); return true; } @@ -684,7 +700,7 @@ class BodyAppendTask final : public api::AsyncTask { } if (done) { - set_state(State::Done); + set_state(engine->cx(), State::Done); return true; } @@ -696,7 +712,7 @@ class BodyAppendTask final : public api::AsyncTask { capacity = capacity_res.unwrap(); } while (capacity > 0); - set_state(State::BlockedOnOutgoing); + set_state(engine->cx(), State::BlockedOnOutgoing); engine->queue_async_task(this); return true; } @@ -717,12 +733,13 @@ class BodyAppendTask final : public api::AsyncTask { } void trace(JSTracer *trc) override { - // Nothing to trace. + JS::TraceEdge(trc, &cb_receiver_, "BodyAppendTask completion callback receiver"); } }; -Result HttpOutgoingBody::append(api::Engine *engine, HttpIncomingBody *other) { - engine->queue_async_task(new BodyAppendTask(other, this)); +Result HttpOutgoingBody::append(api::Engine *engine, HttpIncomingBody *other, + api::TaskCompletionCallback callback, HandleObject callback_receiver) { + engine->queue_async_task(new BodyAppendTask(engine, other, this, callback, callback_receiver)); return {}; } diff --git a/include/extension-api.h b/include/extension-api.h index 790a2f4..ca58f06 100644 --- a/include/extension-api.h +++ b/include/extension-api.h @@ -120,6 +120,9 @@ class Engine { void dump_promise_rejection(HandleValue reason, HandleObject promise, FILE *fp); }; + +typedef bool (*TaskCompletionCallback)(JSContext* cx, HandleObject receiver); + class AsyncTask { protected: PollableHandle handle_ = -1; diff --git a/include/host_api.h b/include/host_api.h index ee0e560..9364969 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -281,7 +281,8 @@ class HttpOutgoingBody final : public Pollable { Result write_all(const uint8_t *bytes, size_t len); /// Append an HttpIncomingBody to this one. - Result append(api::Engine *engine, HttpIncomingBody *incoming); + Result append(api::Engine *engine, HttpIncomingBody *other, + api::TaskCompletionCallback callback, HandleObject callback_receiver); /// Close this handle, and reset internal state to invalid. Result close();