diff --git a/CMakeLists.txt b/CMakeLists.txt index 0691f3d6..98c3e9d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,13 +36,14 @@ 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) include("builtins") -if (DEFINED ENV{WPT}) +option(ENABLE_WPT "Enable WPT harness support" OFF) +if (ENABLE_WPT) include("tests/wpt-harness/wpt.cmake") endif() @@ -50,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 @@ -94,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 ) @@ -102,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/README.md b/README.md index 46133736..c0bde81f 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/builtins/web/fetch/fetch-api.cpp b/builtins/web/fetch/fetch-api.cpp index 9f45119d..45dc10b5 100644 --- a/builtins/web/fetch/fetch-api.cpp +++ b/builtins/web/fetch/fetch-api.cpp @@ -3,67 +3,13 @@ #include "headers.h" #include "request-response.h" -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(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; - } +#include - return cancel(engine); - } +#include - [[nodiscard]] bool cancel(api::Engine *engine) override { - // TODO(TS): implement - handle_ = -1; - return true; - } +namespace builtins::web::fetch { - void trace(JSTracer *trc) override { TraceEdge(trc, &request_, "Request for response future"); } -}; +static api::Engine *ENGINE; // 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. @@ -80,30 +26,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); + } - RootedObject request(cx, Request::create(cx, requestInstance, args[0], args.get(1))); - if (!request) { + RootedString method_str(cx, Request::method(cx, request_obj)); + if (!method_str) { return ReturnPromiseRejectedWithPendingError(cx, args); } + 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 +97,13 @@ 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)); + 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/fetch_event.cpp b/builtins/web/fetch/fetch_event.cpp index 87724642..865119e4 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 @@ -20,8 +23,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) { @@ -29,6 +30,9 @@ void inc_pending_promise_count(JSObject *self) { auto count = JS::GetReservedSlot(self, static_cast(FetchEvent::Slots::PendingPromiseCount)) .toInt32(); + if (count == 0) { + ENGINE->incr_event_loop_interest(); + } count++; MOZ_ASSERT(count > 0); JS::SetReservedSlot(self, static_cast(FetchEvent::Slots::PendingPromiseCount), @@ -42,8 +46,9 @@ void dec_pending_promise_count(JSObject *self) { .toInt32(); MOZ_ASSERT(count > 0); count--; - if (count == 0) + if (count == 0) { ENGINE->decr_event_loop_interest(); + } JS::SetReservedSlot(self, static_cast(FetchEvent::Slots::PendingPromiseCount), JS::Int32Value(count)); } @@ -70,7 +75,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 +154,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,32 +166,52 @@ 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 = new host_api::HttpHeaders(*incoming_response->headers().unwrap()); - 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; + } + + host_api::HttpOutgoingResponse* 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 = static_cast(existing_handle)->body().unwrap(); + auto *dest_body = response->body().unwrap(); + + // 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; + } + 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); + SetReservedSlot(response_obj, static_cast(Response::Slots::Response), + PrivateValue(response)); } if (streaming && response->has_body()) { STREAMING_BODY = response->body().unwrap(); } + if (streaming) { + ENGINE->incr_event_loop_interest(); + } + return send_response(response, FetchEvent::instance(), streaming ? FetchEvent::State::responseStreaming : FetchEvent::State::responseDone); @@ -217,17 +242,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; @@ -305,7 +319,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()) { @@ -424,11 +438,13 @@ bool FetchEvent::is_dispatching(JSObject *self) { void FetchEvent::start_dispatching(JSObject *self) { MOZ_ASSERT(!is_dispatching(self)); + ENGINE->incr_event_loop_interest(); JS::SetReservedSlot(self, static_cast(Slots::Dispatch), JS::TrueValue()); } void FetchEvent::stop_dispatching(JSObject *self) { MOZ_ASSERT(is_dispatching(self)); + ENGINE->decr_event_loop_interest(); JS::SetReservedSlot(self, static_cast(Slots::Dispatch), JS::FalseValue()); } @@ -509,6 +525,89 @@ 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) { + 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)) { + 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,6 +643,7 @@ bool install(api::Engine *engine) { // } // } + host_api::HttpIncomingRequest::set_handler(handle_incoming_request); return true; } @@ -594,9 +694,6 @@ void exports_wasi_http_incoming_handler(exports_wasi_http_incoming_request reque 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())) { diff --git a/builtins/web/fetch/headers.cpp b/builtins/web/fetch/headers.cpp index 32f3d8fc..18533c69 100644 --- a/builtins/web/fetch/headers.cpp +++ b/builtins/web/fetch/headers.cpp @@ -1,22 +1,17 @@ #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,35 +38,24 @@ 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; \ } -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) { +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); } /** @@ -82,14 +66,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 = !name_val.isString(); JS::RootedString name_str(cx, JS::ToString(cx, name_val)); if (!name_str) { return nullptr; @@ -105,42 +85,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 = !value_val.isString(); 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; } @@ -148,11 +128,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') { @@ -171,6 +146,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') { @@ -179,91 +158,21 @@ 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; - } + if (*value_changed) { + memmove(value_chars, value_chars + start, end - start); + value.len = end - start; } - 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); @@ -272,75 +181,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; -} - -/** - * 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; - } - - // Return `null` for non-existent headers. - if (rval.isUndefined()) { - rval.setNull(); - } - + value.setString(res_str); return true; } @@ -358,7 +225,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; @@ -390,145 +257,304 @@ 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)) +} // namespace + +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; + } - JS::RootedValue iterable(cx); - if (!JS::MapKeys(cx, backing_map, &iterable)) + RootedString str(cx, core::decode(cx, chars)); + if (!str) { return false; + } - JS::ForOfIterator it(cx); - if (!it.init(iterable)) - return false; + rval.setString(str); + return true; +} - JS::RootedValue name(cx); - JS::RootedValue v(cx); - while (true) { - bool done; - if (!it.next(&name, &done)) +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; + } - if (done) - break; + JS::ForOfIterator it(cx); + if (!it.init(iterable)) { + return false; + } - if (!ensure_value_for_header(cx, self, name, &v)) + using host_api::HostString; + 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, 0, &name_val); + JS_GetElement(cx, entry, 1, &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(std::move(name), std::move(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)); } - JS_SetReservedSlot(self, static_cast(Headers::Slots::HasLazyValues), - JS::BooleanValue(false)); + 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(mode))); return true; } -} // namespace +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; + } + + 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; + } + } + + return true; +} 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) 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)) { + if (!prepare_for_entries_modification(cx, self)) { return false; } - auto handle = get_handle(self); - 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); - 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); - if (auto *err = res.to_err()) { - HANDLE_ERROR(cx, *err); + std::string_view name_str = name_chars; + if (name_str == "set-cookie") { + for (auto value : splitCookiesString(value_chars)) { + if (!append_single_normalized_header_value(cx, self, name, name_chars, name_changed, UndefinedHandleValue, + value, true, fun_name)) { return false; } } + } else { + if (!append_single_normalized_header_value(cx, self, name, name_chars, name_changed, value, + value_chars, value_changed, fun_name)) { + return false; + } } - return append_header_value_to_map(cx, self, normalized_name, &normalized_value); + return true; } -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); +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)); } -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)) { +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; +} - 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)) { +JSObject *Headers::create(JSContext *cx, host_api::HttpHeadersReadOnly *handle) { + RootedObject self(cx, create(cx)); + if (!self) { return nullptr; } + init_from_handle(self, handle); + return self; +} - JS::ForOfIterator it(cx); - if (!it.init(iterable)) { +JSObject *Headers::create(JSContext *cx, HandleValue init_headers) { + RootedObject self(cx, create(cx)); + if (!self) { 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 create(cx, self, init_headers); } -JSObject *Headers::create(JSContext *cx, JS::HandleObject self, host_api::HttpHeaders *handle, - JS::HandleValue initv) { - JS::RootedObject headers(cx, create(cx, self, handle)); - if (!headers) - return nullptr; - +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, headers, - &consumed, "Headers")) { + if (!core::maybe_consume_sequence_or_record(cx, initv, self, + &consumed, "Headers")) { return nullptr; } @@ -537,7 +563,7 @@ JSObject *Headers::create(JSContext *cx, JS::HandleObject self, host_api::HttpHe return nullptr; } - return headers; + return self; } bool Headers::get(JSContext *cx, unsigned argc, JS::Value *vp) { @@ -545,7 +571,34 @@ bool Headers::get(JSContext *cx, unsigned argc, JS::Value *vp) { 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 == 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()); + } + + 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; + } + 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) { @@ -554,20 +607,39 @@ 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); + 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(); + MOZ_ASSERT(handle); + auto res = handle->set(name_chars, value_chars); if (auto *err = res.to_err()) { HANDLE_ERROR(cx, *err); return false; } - } + } else { + MOZ_ASSERT(mode == Mode::ContentOnly); + RootedObject entries(cx, get_entries(cx, self)); + if (!entries) { + return false; + } - JS::RootedObject map(cx, get_backing_map(self)); - if (!JS::MapSet(cx, map, normalized_name, normalized_value)) { - 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[1], value_chars, value_changed, &value_val)) { + return false; + } + + if (!MapSet(cx, entries, name_val, value_val)) { + return false; + } } args.rval().setUndefined(); @@ -578,20 +650,43 @@ 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::Uninitialized) { + args.rval().setBoolean(false); + return true; + } + + 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; + } + + 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, name_val, &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; } @@ -599,59 +694,86 @@ 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(Headers::is_instance(self)); - JS::RootedString name_str(cx, JS_NewStringCopyN(cx, name, strlen(name))); +bool Headers::set_if_undefined(JSContext *cx, HandleObject self, string_view name, string_view 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; + } + + MOZ_ASSERT(mode(self) == Mode::ContentOnly); + RootedObject entries(cx, get_entries(cx, self)); + RootedString name_str(cx, core::decode(cx, name)); if (!name_str) { return false; } - JS::RootedValue name_val(cx, JS::StringValue(name_str)); + RootedValue name_val(cx, StringValue(name_str)); - JS::RootedObject map(cx, get_backing_map(self)); bool has; - if (!JS::MapHas(cx, map, name_val, &has)) { + if (!MapHas(cx, entries, name_val, &has)) { return false; } if (has) { return true; } - JS::RootedString value_str(cx, JS_NewStringCopyN(cx, value, strlen(value))); + RootedString value_str(cx, core::decode(cx, value)); if (!value_str) { return false; } - JS::RootedValue value_val(cx, JS::StringValue(value_str)); + RootedValue value_val(cx, StringValue(value_str)); - return Headers::append_header_value(cx, self, name_val, value_val, "internal_maybe_add"); + 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") - NORMALIZE_NAME(args[0], "Headers.delete") - - bool has; - JS::RootedObject map(cx, get_backing_map(self)); - if (!JS::MapDelete(cx, map, normalized_name, &has)) { + if (!prepare_for_entries_modification(cx, self)) { 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) { + NORMALIZE_NAME(args[0], "Headers.delete") + Mode mode = Headers::mode(self); + if (mode == Mode::HostOnly) { + auto handle = get_handle(self)->as_writable(); + 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 { + 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, name_val, &had); } + args.rval().setUndefined(); return true; } @@ -671,7 +793,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); @@ -700,19 +822,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[] = { @@ -743,8 +865,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; } @@ -758,6 +884,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; @@ -767,29 +899,38 @@ 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) { - JS_SetReservedSlot(self, static_cast(Slots::Handle), JS::PrivateValue(handle)); - - JS::RootedObject backing_map(cx, JS::NewMapObject(cx)); - if (!backing_map) { +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; } - 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; - } + return &GetReservedSlot(self, static_cast(Slots::Entries)).toObject(); +} + +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(); } - JS_SetReservedSlot(self, static_cast(Slots::HasLazyValues), JS::BooleanValue(lazy)); + 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; + } - return self; + auto handle = unique_ptr(get_handle(self)->clone()); + if (!handle) { + JS_ReportErrorASCII(cx, "Failed to clone headers"); + return nullptr; + } + return handle; } -} // 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 8bfdf69f..2a29876d 100644 --- a/builtins/web/fetch/headers.h +++ b/builtins/web/fetch/headers.h @@ -22,47 +22,102 @@ class Headers final : public BuiltinImpl { public: static constexpr const char *class_name = "Headers"; + /// 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 { - BackingMap, Handle, - HasLazyValues, + Entries, // Map holding headers if they are available in-content. + Mode, 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. - * - * 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); + static bool set_if_undefined(JSContext *cx, JS::HandleObject self, string_view name, + string_view 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) { + MOZ_ASSERT(Headers::is_instance(self)); + 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[]; 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 bool init_class(JSContext *cx, JS::HandleObject global); - static bool constructor(JSContext *cx, unsigned argc, JS::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); - 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); + /// 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 36c66dfa..bb25e273 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. @@ -217,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()); } @@ -225,9 +166,12 @@ 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) { + auto handle = RequestOrResponse::handle(obj); + return handle && handle->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"); @@ -250,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) { @@ -330,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. */ @@ -345,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 @@ -378,60 +314,99 @@ 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; 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; + } + 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 { - { - auto str = core::encode(cx, body_val); - text = std::move(str.ptr); - length = str.len; + auto text = core::encode(cx, body_val); + if (!text.ptr) { + return false; } + buf = text.ptr.release(); + length = text.len; + content_type = "text/plain;charset=UTF-8"; + } - if (!text) + if (!buffer) { + MOZ_ASSERT_IF(length, buf); + buffer = NewArrayBufferWithContents(cx, length, buf, + JS::NewArrayBufferOutOfMemory::CallerMustFreeMemory); + if (!buffer) { + js_free(buf); return false; - buf = text.get(); - content_type = "text/plain;charset=UTF-8"; + } } - auto body = RequestOrResponse::outgoing_body_handle(self); - auto write_res = body->write_all(reinterpret_cast(buf), length); + 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(); + // 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; + } + 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); + 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)); + 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::maybe_add(cx, headers, "content-type", content_type)) { + if (!headers) { + return false; + } + + 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; } } @@ -445,24 +420,70 @@ 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 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; } - 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; } @@ -470,18 +491,12 @@ 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(); + host_api::HttpHeadersReadOnly *handle; + if (is_incoming(obj) && (handle = headers_handle(obj))) { + headers = Headers::create(cx, handle); + } else { + headers = Headers::create(cx); } - headers = Headers::create(cx, headersInstance, headers_handle); if (!headers) { return nullptr; } @@ -817,7 +832,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); } @@ -840,10 +856,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 @@ -858,6 +870,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); @@ -914,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 @@ -938,6 +951,7 @@ bool RequestOrResponse::body_reader_then_handler(JSContext *cx, JS::HandleObject // `responseDone`. // TODO(TS): factor this out to remove dependency on fetch-event.h if (Response::is_instance(body_owner)) { + ENGINE->decr_event_loop_interest(); fetch_event::FetchEvent::set_state(fetch_event::FetchEvent::instance(), fetch_event::FetchEvent::State::responseDone); } @@ -964,16 +978,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 @@ -991,6 +999,8 @@ 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. + auto body = RequestOrResponse::outgoing_body_handle(body_owner); res = body->write_all(bytes, length); } @@ -1000,6 +1010,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) { @@ -1009,8 +1020,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 @@ -1025,14 +1036,17 @@ bool RequestOrResponse::body_reader_catch_handler(JSContext *cx, JS::HandleObjec // `responseDone` is the right state: `respondedWithError` is for when sending // a response at all failed.) // TODO(TS): investigate why this is disabled. - // if (Response::is_instance(body_owner)) { - // FetchEvent::set_state(FetchEvent::instance(), FetchEvent::State::responseDone); - // } + if (Response::is_instance(body_owner)) { + ENGINE->decr_event_loop_interest(); + // FetchEvent::set_state(FetchEvent::instance(), FetchEvent::State::responseDone); + } return true; } 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; @@ -1077,17 +1091,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 @@ -1097,13 +1100,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; @@ -1135,7 +1138,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)); @@ -1168,20 +1175,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(); } @@ -1397,10 +1408,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()); @@ -1420,6 +1429,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; @@ -1544,6 +1554,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) || @@ -1693,7 +1704,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) { @@ -1705,7 +1715,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; } @@ -1740,8 +1750,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; } @@ -1833,8 +1843,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) @@ -1853,8 +1862,7 @@ static_assert((int)Response::Slots::Response == (int)Request::Slots::Request); 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) { @@ -2448,8 +2456,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)); @@ -2496,16 +2502,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 @@ -2516,19 +2512,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; } @@ -2581,13 +2574,6 @@ 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; - } - } } args.rval().setObject(*response); @@ -2605,33 +2591,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 e1b2f1d5..f7220c3a 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); @@ -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. */ @@ -81,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 @@ -129,6 +134,7 @@ class Request final : public BuiltinImpl { URL = static_cast(RequestOrResponse::Slots::URL), Method = static_cast(RequestOrResponse::Slots::Count), ResponsePromise, + PendingResponseHandle, Count, }; @@ -148,8 +154,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,8 +203,9 @@ 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 uint16_t status(JSObject *obj); @@ -207,6 +213,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 diff --git a/builtins/web/url.cpp b/builtins/web/url.cpp index e05fa15a..b326a536 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) { @@ -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/componentize.sh b/componentize.sh index 22aff032..0d8419e7 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/crates/rust-url/rust-url.h b/crates/rust-url/rust-url.h index 9a7cdb2e..5706b3f4 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-rc-2023-10-18/host_api.cpp b/host-apis/wasi-0.2.0-rc-2023-10-18/host_api.cpp index 732525fb..86e2d12a 100644 --- a/host-apis/wasi-0.2.0-rc-2023-10-18/host_api.cpp +++ b/host-apis/wasi-0.2.0-rc-2023-10-18/host_api.cpp @@ -112,6 +112,23 @@ size_t api::AsyncTask::select(std::vector *tasks) { return ready_index; } +std::optional api::AsyncTask::ready(std::vector *tasks) { + auto count = tasks->size(); + vector> handles; + for (const auto task : *tasks) { + handles.emplace_back(task->id()); + } + auto list = list_borrow_pollable_t{ + reinterpret_cast::borrow *>(handles.data()), count}; + bindings_list_u32_t result{nullptr, 0}; + wasi_io_0_2_0_rc_2023_10_18_poll_poll_list(&list, &result); + MOZ_ASSERT(result.len > 0); + const auto ready_index = result.ptr[0]; + free(result.ptr); + + return ready_index; +} + namespace host_api { HostString::HostString(const char *c_str) { diff --git a/host-apis/wasi-0.2.0-rc-2023-12-05/host_api.cpp b/host-apis/wasi-0.2.0-rc-2023-12-05/host_api.cpp index 263794ed..f125b29b 100644 --- a/host-apis/wasi-0.2.0-rc-2023-12-05/host_api.cpp +++ b/host-apis/wasi-0.2.0-rc-2023-12-05/host_api.cpp @@ -116,6 +116,20 @@ size_t api::AsyncTask::select(std::vector *tasks) { return ready_index; } +std::optional api::AsyncTask::ready(std::vector *tasks) { + auto count = tasks->size(); + vector> handles; + auto list = list_borrow_pollable_t{ + reinterpret_cast::borrow *>(handles.data()), count}; + bindings_list_u32_t result{nullptr, 0}; + wasi_io_0_2_0_rc_2023_10_18_poll_poll_list(&list, &result); + MOZ_ASSERT(result.len > 0); + const auto ready_index = result.ptr[0]; + free(result.ptr); + + return ready_index; +} + namespace host_api { HostString::HostString(const char *c_str) { diff --git a/host-apis/wasi-0.2.0/host_api.cpp b/host-apis/wasi-0.2.0/host_api.cpp index 39b4f61a..6e6913f6 100644 --- a/host-apis/wasi-0.2.0/host_api.cpp +++ b/host-apis/wasi-0.2.0/host_api.cpp @@ -1,8 +1,12 @@ #include "host_api.h" #include "bindings/bindings.h" -#include +#include +#ifdef DEBUG +#include +#endif +using host_api::HostString; using std::optional; using std::string_view; using std::tuple; @@ -13,11 +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_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; @@ -27,83 +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; +#ifdef LOG_HANDLE_OPS +#define LOG_HANDLE_OP(...) fprintf(stderr, "%s", __PRETTY_FUNCTION__); fprintf(stderr, __VA_ARGS__) +#else +#define LOG_HANDLE_OP(...) +#endif -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 POISONED_HANDLE = -1; -namespace { +class host_api::HandleState { +protected: + HandleState() = default; + +public: + virtual ~HandleState() = default; + virtual bool valid() const = 0; +}; + +template struct HandleOps {}; -/// This is the type contract for using the Own and Borrow templates. -template struct HandleOps {}; +template +class WASIHandle : public host_api::HandleState { +#ifdef DEBUG + static inline auto used_handles = std::set(); +#endif -/// 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}; +protected: + Handle handle_; +#ifdef DEBUG + bool owned_; +#endif public: - Borrow() = default; + 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 + owned_ = true; + MOZ_ASSERT(!used_handles.contains(handle.__handle)); + used_handles.insert(handle.__handle); +#endif + } + + explicit WASIHandle(typename HandleOps::borrowed handle) : handle_{handle.__handle} { + LOG_HANDLE_OP("Creating borrowed handle %d\n", handle.__handle); +#ifdef DEBUG + owned_ = false; + MOZ_ASSERT(!used_handles.contains(handle.__handle)); + used_handles.insert(handle.__handle); +#endif + } + + ~WASIHandle() override { +#ifdef DEBUG + 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 + } + + static WASIHandle* cast(HandleState* handle) { + return reinterpret_cast*>(handle); + } + + typename HandleOps::borrowed borrow(HandleState *handle) { + return cast(handle)->borrow(); + } + + bool valid() const override { + bool valid = handle_ != POISONED_HANDLE; + MOZ_ASSERT_IF(valid, used_handles.contains(handle_)); + return valid; + } - // Construct a borrow from an owned handle. - Borrow(HandleOps::own handle) : handle{HandleOps::borrow_owned(handle)} {} + typename HandleOps::borrowed borrow() const { + MOZ_ASSERT(valid()); + LOG_HANDLE_OP("borrowing handle %d\n", handle_); + return {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 + used_handles.erase(handle_); +#endif + handle_ = POISONED_HANDLE; + return handle; + } +}; + +template +struct Borrow { + static constexpr typename HandleOps::borrowed invalid{std::numeric_limits::max()}; + typename HandleOps::borrowed handle_{invalid}; - // Construct a borrow from a raw `Handle` value. - Borrow(host_api::Handle handle) : Borrow{typename HandleOps::own{handle}} {} + explicit Borrow(host_api::HandleState *handle) { + handle_ = WASIHandle::cast(handle)->borrow(); + } - // Convenience wrapper for constructing a borrow of a HandleState. - Borrow(host_api::HandleState *state) : Borrow{typename HandleOps::own{state->handle}} {} + explicit Borrow(typename HandleOps::borrowed handle) { + handle_ = handle; + } - bool valid() const { return this->handle.__handle != Borrow::invalid.__handle; } + explicit Borrow(typename HandleOps::owned handle) { + handle_ = {handle.__handle}; + } - operator bool() const { return this->valid(); } + operator typename HandleOps::borrowed() const { return handle_; } +}; - operator typename HandleOps::borrow() const { return this->handle; } +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; }; 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; + using owned = wasi_http_0_2_0_types_own_headers_t; + using borrowed = wasi_http_0_2_0_types_borrow_fields_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_incoming_request_t; + using borrowed = wasi_http_0_2_0_types_borrow_incoming_request_t; }; -struct OutputStream {}; +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; +}; + +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; +}; + +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; +}; + +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 owned = wasi_http_0_2_0_types_own_incoming_body_t; + using borrowed = wasi_http_0_2_0_types_borrow_incoming_body_t; +}; + +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_; -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; + friend host_api::HttpIncomingBody; - static constexpr const auto borrow_owned = wasi_io_0_2_0_poll_borrow_pollable; +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 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; + } -size_t api::AsyncTask::select(std::vector *tasks) { + static OutgoingBodyHandle* cast(HandleState* handle) { + return reinterpret_cast(handle); + } +}; + +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); @@ -113,6 +262,19 @@ size_t api::AsyncTask::select(std::vector *tasks) { return ready_index; } +std::optional api::AsyncTask::ready(std::vector *tasks) { + auto count = tasks->size(); + vector> handles; + for (size_t idx = 0; idx < count; ++idx) { + auto task = tasks->at(idx); + Borrow poll = (task->id()); + if (wasi_io_0_2_0_poll_method_pollable_ready(poll)) { + return idx; + } + } + return std::nullopt; +} + namespace host_api { HostString::HostString(const char *c_str) { @@ -139,7 +301,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:"}; } @@ -185,43 +347,47 @@ void MonotonicClock::unsubscribe(const int32_t handle_id) { wasi_io_0_2_0_poll_pollable_drop_own(own_pollable_t{handle_id}); } +HttpHeaders::HttpHeaders(std::unique_ptr state) : HttpHeadersReadOnly(std::move(state)) {} + HttpHeaders::HttpHeaders() { - this->handle_state_ = new HandleState(wasi_http_0_2_0_types_constructor_fields().__handle); + handle_state_ = std::make_unique>(wasi_http_0_2_0_types_constructor_fields()); } -HttpHeaders::HttpHeaders(Handle handle) { handle_state_ = new HandleState(handle); } -// TODO: make this a factory function -HttpHeaders::HttpHeaders(const vector>> &entries) { +Result HttpHeaders::FromEntries(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()}; wasi_http_0_2_0_types_own_fields_t ret; wasi_http_0_2_0_types_header_error_t err; - wasi_http_0_2_0_types_static_fields_from_list(&tuples, &ret, &err); - // TODO: handle `err` + if (!wasi_http_0_2_0_types_static_fields_from_list(&tuples, &ret, &err)) { + // TODO: handle `err` + return Result::err(154); + } - this->handle_state_ = new HandleState(ret.__handle); + return Result::ok(new HttpHeaders(std::unique_ptr(new WASIHandle(ret)))); } -HttpHeaders::HttpHeaders(const HttpHeaders &headers) { - 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 WASIHandle(handle)); +} + +HttpHeaders *HttpHeadersReadOnly::clone() { + return new HttpHeaders(*this); } -Result>> HttpHeaders::entries() const { +Result>> HttpHeadersReadOnly::entries() const { Result>> res; - MOZ_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; @@ -237,15 +403,15 @@ Result>> HttpHeaders::entries() const { return res; } -Result> HttpHeaders::names() const { +Result> HttpHeadersReadOnly::names() const { Result> res; - MOZ_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; + names.reserve(entries.len); for (int i = 0; i < entries.len; i++) { names.emplace_back(bindings_string_to_host_string(entries.ptr[i].f0)); } @@ -256,17 +422,17 @@ 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()); 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) { std::vector names; + names.reserve(values.len); for (int i = 0; i < values.len; i++) { names.emplace_back(to_host_string(values.ptr[i])); } @@ -280,12 +446,17 @@ Result>> HttpHeaders::get(string_view name) const { return res; } +Result HttpHeadersReadOnly::has(string_view name) const { + 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) { - MOZ_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); @@ -296,28 +467,36 @@ Result HttpHeaders::set(string_view name, string_view value) { } Result HttpHeaders::append(string_view name, string_view value) { - MOZ_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; - 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: + MOZ_ASSERT_UNREACHABLE("Headers should not be immutable"); + default: + MOZ_ASSERT_UNREACHABLE("Unknown header error type"); + } + } return {}; } Result HttpHeaders::remove(string_view name) { - MOZ_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); - - // TODO: handle `err` + if (!wasi_http_0_2_0_types_method_fields_delete(borrow, &hdr, &err)) { + // TODO: handle `err` + return Result::err(154); + } return {}; } @@ -328,7 +507,7 @@ string_view HttpRequestResponseBase::url() { return string_view(*_url); } - auto borrow = borrow_incoming_request_t{handle_state_->handle}; + Borrow borrow(handle_state_.get()); wasi_http_0_2_0_types_scheme_t scheme; bool success; @@ -360,26 +539,8 @@ 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 { - Handle stream_handle_; - PollableHandle pollable_handle_; - - friend HttpOutgoingBody; - -public: - explicit OutgoingBodyHandleState(const Handle handle) - : HandleState(handle), 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(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()) { @@ -387,7 +548,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; @@ -406,7 +567,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); @@ -418,10 +579,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_); + auto *state = static_cast(handle_state_.get()); Borrow borrow(state->stream_handle_); while (len > 0) { @@ -456,16 +617,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(); @@ -474,7 +649,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 { @@ -485,10 +662,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; @@ -499,7 +676,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; @@ -517,12 +694,12 @@ 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; } - 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 @@ -536,7 +713,7 @@ class BodyAppendTask final : public api::AsyncTask { } if (done) { - set_state(State::Done); + set_state(engine->cx(), State::Done); return true; } @@ -548,7 +725,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; } @@ -569,20 +746,18 @@ 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) { - MOZ_ASSERT(valid()); - 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 {}; } Result HttpOutgoingBody::close() { - MOZ_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_}; @@ -601,17 +776,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; @@ -620,7 +792,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; } @@ -650,10 +822,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) { + std::unique_ptr headers) { bindings_string_t path_with_query; wasi_http_0_2_0_types_scheme_t scheme; bindings_string_t authority; @@ -686,8 +858,9 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< maybe_path_with_query = &path_with_query; } + auto headers_handle = WASIHandle::cast(headers->handle_state_.get())->take(); 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); @@ -706,69 +879,68 @@ HttpOutgoingRequest *HttpOutgoingRequest::make(string_view method_str, optional< maybe_path_with_query); } - auto *state = new HandleState(handle.__handle); - auto *resp = new HttpOutgoingRequest(state); - - resp->headers_ = headers; + auto *state = new WASIHandle(handle); + auto *resp = new HttpOutgoingRequest(std::unique_ptr(state)); return resp; } Result HttpOutgoingRequest::method() { - MOZ_ASSERT(valid()); - MOZ_ASSERT(headers_); return Result::ok(method_); } -Result HttpOutgoingRequest::headers() { - MOZ_ASSERT(valid()); - MOZ_ASSERT(headers_); - return Result::ok(headers_); +Result HttpOutgoingRequest::headers() { + if (!headers_) { + if (!valid()) { + return Result::err(154); + } + Borrow borrow(handle_state_.get()); + auto res = wasi_http_0_2_0_types_method_outgoing_request_headers(borrow); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(new WASIHandle(res))); + } + + return Result::ok(headers_); } Result HttpOutgoingRequest::body() { typedef Result Res; - MOZ_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)) { + Borrow borrow(handle_state_.get()); + if (!wasi_http_0_2_0_types_method_outgoing_request_body(borrow, &body)) { return Res::err(154); } - this->body_ = new HttpOutgoingBody(body.__handle); + body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandle(body))); } return Res::ok(body_); } Result HttpOutgoingRequest::send() { - MOZ_ASSERT(valid()); + typedef Result Res; 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); + 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::unique_ptr(new WASIHandle(ret))); return Result::ok(res); } -class IncomingBodyHandleState final : HandleState { - Handle stream_handle_; - PollableHandle pollable_handle_; +void block_on_pollable_handle(PollableHandle handle) { + wasi_io_0_2_0_poll_method_pollable_block({handle}); +} - friend HttpIncomingBody; +HttpIncomingBody::HttpIncomingBody(std::unique_ptr state) : Pollable() { handle_state_ = std::move(state); } -public: - explicit IncomingBodyHandleState(const Handle handle) - : HandleState(handle), 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; +Resource::~Resource() { + if (handle_state_ != nullptr) { + handle_state_ = nullptr; } -}; +} -HttpIncomingBody::HttpIncomingBody(const Handle handle) : Pollable() { - handle_state_ = new IncomingBodyHandleState(handle); +bool Resource::valid() const { + return this->handle_state_ != nullptr && this->handle_state_->valid(); } Result HttpIncomingBody::read(uint32_t chunk_size) { @@ -776,8 +948,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_)->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) { @@ -792,13 +964,13 @@ Result HttpIncomingBody::read(uint32_t chunk_size) Result HttpIncomingBody::close() { return {}; } Result HttpIncomingBody::subscribe() { - auto borrow = borrow_input_stream_t( - {static_cast(handle_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_); + auto state = static_cast(handle_state_.get()); if (state->pollable_handle_ == INVALID_POLLABLE_HANDLE) { return; } @@ -806,14 +978,15 @@ 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}); + Borrow borrow(handle_state_.get()); if (!wasi_http_0_2_0_types_method_future_incoming_response_get(borrow, &res)) { return Res::ok(std::nullopt); } @@ -826,11 +999,12 @@ Result> FutureHttpIncomingResponse::maybe_respo return Res::err(154); } - return Res::ok(new HttpIncomingResponse(val.ok.__handle)); + 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_->handle}); + Borrow borrow(handle_state_.get()); auto pollable = wasi_http_0_2_0_types_method_future_incoming_response_subscribe(borrow); return Result::ok(pollable.__handle); } @@ -838,32 +1012,41 @@ void FutureHttpIncomingResponse::unsubscribe() { // TODO: implement } +HttpHeadersReadOnly::HttpHeadersReadOnly() { + handle_state_ = nullptr; +} + +HttpHeadersReadOnly::HttpHeadersReadOnly(std::unique_ptr state) { + handle_state_ = std::move(state); +} + Result HttpIncomingResponse::status() { if (status_ == UNSET_STATUS) { if (!valid()) { return Result::err(154); } - auto borrow = wasi_http_0_2_0_types_borrow_incoming_response_t({handle_state_->handle}); + auto borrow = Borrow(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() { +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); + 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_); + return Result::ok(headers_); } Result HttpIncomingResponse::body() { @@ -871,89 +1054,76 @@ 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_->handle}), &body)) { + if (!wasi_http_0_2_0_types_method_incoming_response_consume(borrow, &body)) { return Result::err(154); } - body_ = new HttpIncomingBody(body.__handle); + body_ = new HttpIncomingBody(std::unique_ptr(new IncomingBodyHandle(body))); } return Result::ok(body_); } -HttpOutgoingResponse::HttpOutgoingResponse(HandleState *state) { this->handle_state_ = state; } +HttpOutgoingResponse::HttpOutgoingResponse(std::unique_ptr state) { this->handle_state_ = std::move(state); } -HttpOutgoingResponse *HttpOutgoingResponse::make(const uint16_t status, HttpHeaders *headers) { - wasi_http_0_2_0_types_own_headers_t owned{headers->handle_state_->handle}; - auto handle = wasi_http_0_2_0_types_constructor_outgoing_response(owned); - auto borrow = wasi_http_0_2_0_types_borrow_outgoing_response(handle); +HttpOutgoingResponse *HttpOutgoingResponse::make(const uint16_t status, unique_ptr headers) { + 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); - auto *resp = new HttpOutgoingResponse(state); + auto *state = new WASIHandle(handle); + 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(state->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; - resp->status_ = status; - resp->headers_ = headers; - return resp; } -Result HttpOutgoingResponse::headers() { - if (!valid()) { - return Result::err(154); +Result HttpOutgoingResponse::headers() { + if (!headers_) { + if (!valid()) { + return Result::err(154); + } + auto borrow = Borrow(handle_state_.get()); + auto res = wasi_http_0_2_0_types_method_outgoing_response_headers(borrow); + auto state = new WASIHandle(res); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(state)); } - return Result::ok(headers_); + + return Result::ok(headers_); } Result HttpOutgoingResponse::body() { typedef Result Res; - MOZ_ASSERT(valid()); if (!this->body_) { + 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_->handle}), &body)) { + if (!wasi_http_0_2_0_types_method_outgoing_response_body(borrow, &body)) { return Res::err(154); } - this->body_ = new HttpOutgoingBody(body.__handle); + body_ = new HttpOutgoingBody(std::unique_ptr(new OutgoingBodyHandle(body))); } 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()) { 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_->handle), &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 { @@ -963,17 +1133,18 @@ 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 borrow = Borrow(handle_state_.get()); auto res = wasi_http_0_2_0_types_method_incoming_request_headers(borrow); - headers_ = new HttpHeaders(res.__handle); + auto state = new WASIHandle(res); + headers_ = new HttpHeadersReadOnly(std::unique_ptr(state)); } - return Result::ok(headers_); + return Result::ok(headers_); } Result HttpIncomingRequest::body() { @@ -981,14 +1152,44 @@ 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_->handle), &body)) { + if (!wasi_http_0_2_0_types_method_incoming_request_consume(borrow, &body)) { return Result::err(154); } - body_ = new HttpIncomingBody(body.__handle); + body_ = new HttpIncomingBody(std::unique_ptr(new IncomingBodyHandle(body))); } return Result::ok(body_); } } // namespace host_api + +static host_api::HttpIncomingRequest::RequestHandler REQUEST_HANDLER = nullptr; +static exports_wasi_http_response_outparam RESPONSE_OUT; + +void host_api::HttpIncomingRequest::set_handler(RequestHandler handler) { + MOZ_ASSERT(!REQUEST_HANDLER); + REQUEST_HANDLER = 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 = own; + + wasi_http_0_2_0_types_static_response_outparam_set(RESPONSE_OUT, &result); + + return {}; +} + +void exports_wasi_http_incoming_handler(exports_wasi_http_incoming_request request_handle, + exports_wasi_http_response_outparam response_out) { + 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); +} diff --git a/include/extension-api.h b/include/extension-api.h index f6558b4b..5592fce3 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,11 @@ 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 is_preinitializing(); + bool toplevel_evaluated(); /** * Run the async event loop as long as there's interest registered in keeping it running. @@ -112,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; @@ -133,6 +144,11 @@ class AsyncTask { * Select for the next available ready task, providing the oldest ready first. */ static size_t select(std::vector *handles); + + /** + * Non-blocking check for a ready task, providing the oldest ready first, if any. + */ + static std::optional ready(std::vector *handles); }; } // namespace api diff --git a/include/host_api.h b/include/host_api.h index 4e8abba7..9364969e 100644 --- a/include/host_api.h +++ b/include/host_api.h @@ -198,33 +198,25 @@ 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; public: - virtual ~Resource() = default; + virtual ~Resource(); - /// Returns true when this resource handle is valid. - virtual bool valid() const { return this->handle_state_ != nullptr; } + 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; }; class Pollable : public Resource { @@ -238,10 +230,12 @@ class Pollable : public Resource { virtual void unsubscribe() = 0; }; +void block_on_pollable_handle(PollableHandle handle); + class HttpIncomingBody final : public Pollable { public: HttpIncomingBody() = delete; - explicit HttpIncomingBody(Handle handle); + explicit HttpIncomingBody(std::unique_ptr handle); class ReadResult final { public: @@ -267,7 +261,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(); @@ -287,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(); @@ -310,10 +305,12 @@ class HttpBodyPipe { }; class HttpIncomingResponse; +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(); @@ -322,21 +319,54 @@ 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: + // 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: - HttpHeaders(); - explicit HttpHeaders(Handle handle); - explicit HttpHeaders(const vector>> &entries); - HttpHeaders(const HttpHeaders &headers); + explicit HttpHeadersReadOnly(std::unique_ptr handle); + HttpHeadersReadOnly(const HttpHeadersReadOnly &headers) = delete; + + HttpHeaders* clone(); + + virtual bool is_writable() { return false; }; + virtual HttpHeaders* as_writable() { + MOZ_ASSERT_UNREACHABLE(); + return nullptr; + }; Result>> entries() const; Result> names() const; Result>> get(string_view name) const; + Result has(string_view name) const; +}; + +class HttpHeaders final : public HttpHeadersReadOnly { + friend HttpIncomingResponse; + friend HttpIncomingRequest; + friend HttpOutgoingResponse; + friend HttpOutgoingRequest; + + explicit HttpHeaders(std::unique_ptr handle); + +public: + HttpHeaders(); + explicit HttpHeaders(const HttpHeadersReadOnly &headers); + + static Result FromEntries(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); @@ -345,13 +375,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; @@ -393,31 +423,35 @@ 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; } [[nodiscard]] Result method() override; - Result headers() 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); + 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; } [[nodiscard]] Result method() override; - Result headers() override; + Result headers() override; Result body() override; Result send(); @@ -435,34 +469,32 @@ 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; } - Result headers() override; + Result headers() override; Result body() override; [[nodiscard]] Result status() override; }; 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); + static HttpOutgoingResponse *make(const uint16_t status, unique_ptr headers); 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; - Result send(ResponseOutparam out_param); + Result send(); }; class Random final { diff --git a/runtime/decode.cpp b/runtime/decode.cpp new file mode 100644 index 00000000..2bd7daf6 --- /dev/null +++ b/runtime/decode.cpp @@ -0,0 +1,10 @@ +#include "encode.h" + +namespace core { + +JSString* decode(JSContext* cx, string_view str) { + JS::UTF8Chars ret_chars(str.data(), str.length()); + return JS_NewStringCopyUTF8N(cx, ret_chars); +} + +} // namespace core diff --git a/runtime/decode.h b/runtime/decode.h new file mode 100644 index 00000000..e473fcd7 --- /dev/null +++ b/runtime/decode.h @@ -0,0 +1,10 @@ +#ifndef JS_COMPUTE_RUNTIME_DECODE_H +#define JS_COMPUTE_RUNTIME_DECODE_H + +namespace core { + +JSString* decode(JSContext *cx, string_view str); + +} // namespace core + +#endif diff --git a/runtime/engine.cpp b/runtime/engine.cpp index 8286cdf9..a61c7192 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()); @@ -329,6 +332,7 @@ static void abort(JSContext *cx, const char *description) { api::Engine::Engine() { // total_compute = 0; + ENGINE = this; bool result = init_js(); MOZ_RELEASE_ASSERT(result); JS::EnterRealm(cx(), global()); @@ -338,6 +342,46 @@ api::Engine::Engine() { 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 +397,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; } @@ -401,8 +448,10 @@ bool api::Engine::eval_toplevel(const char *path, MutableHandleValue result) { // 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. @@ -410,8 +459,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::is_preinitializing() { return isWizening(); } + +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/event_loop.cpp b/runtime/event_loop.cpp index ee8637e8..4ea4e204 100644 --- a/runtime/event_loop.cpp +++ b/runtime/event_loop.cpp @@ -59,7 +59,7 @@ bool EventLoop::run_event_loop(api::Engine *engine, double total_compute) { queue.get().event_loop_running = true; JSContext *cx = engine->cx(); - while (true) { + do { // Run a microtask checkpoint js::RunJobs(cx); @@ -67,24 +67,34 @@ bool EventLoop::run_event_loop(api::Engine *engine, double total_compute) { exit_event_loop(); return false; } - // if there is no interest in the event loop at all, just run one tick - if (interest_complete()) { - exit_event_loop(); - return true; - } const auto tasks = &queue.get().tasks; size_t tasks_size = tasks->size(); + if (tasks_size == 0) { + if (interest_complete()) { + break; + } exit_event_loop(); - MOZ_ASSERT(!interest_complete()); fprintf(stderr, "event loop error - both task and job queues are empty, but expected " "operations did not resolve"); return false; } + size_t task_idx; + // Select the next task to run according to event-loop semantics of oldest-first. - size_t task_idx = api::AsyncTask::select(tasks); + if (interest_complete()) { + // Perform a non-blocking select in the case of there being no event loop interest + // (we are thus only performing a "single tick", but must still progress work that is ready) + std::optional maybe_task_idx = api::AsyncTask::ready(tasks); + if (!maybe_task_idx.has_value()) { + break; + } + task_idx = maybe_task_idx.value(); + } else { + task_idx = api::AsyncTask::select(tasks); + } auto task = tasks->at(task_idx); bool success = task->run(engine); @@ -93,7 +103,10 @@ bool EventLoop::run_event_loop(api::Engine *engine, double total_compute) { exit_event_loop(); return false; } - } + } while (!interest_complete()); + + exit_event_loop(); + return true; } void EventLoop::init(JSContext *cx) { queue.init(cx); } diff --git a/runtime/js.cpp b/runtime/js.cpp index ba050fb4..49a0d4f0 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 ad1f0a5a..fa4a472e 100644 --- a/runtime/script_loader.cpp +++ b/runtime/script_loader.cpp @@ -4,12 +4,12 @@ #include #include #include -#include #include #include +#include #include -static JSContext* CONTEXT; +static api::Engine* ENGINE; static ScriptLoader* SCRIPT_LOADER; JS::PersistentRootedObject moduleRegistry; JS::PersistentRootedObject builtinModules; @@ -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) { @@ -334,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); @@ -349,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; @@ -377,8 +390,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,26 +422,31 @@ 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) { - 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("./"); - } +bool ScriptLoader::eval_top_level_script(const char *path, JS::SourceText &source, + MutableHandleValue result, MutableHandleValue tla_promise) { + JSContext *cx = ENGINE->cx(); JS::CompileOptions opts(cx, *COMPILE_OPTS); opts.setFileAndLine(strip_base(path, BASE_PATH), 1); @@ -440,7 +458,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 +466,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); @@ -470,8 +484,10 @@ bool ScriptLoader::load_top_level_script(const char *path, MutableHandleValue re // optimizing them for compactness makes sense and doesn't fragment writes // later on. // https://github.com/fastly/js-compute-runtime/issues/222 - JS::PrepareForFullGC(cx); - JS::NonIncrementalGC(cx, JS::GCOptions::Shrink, JS::GCReason::API); + if (ENGINE->is_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 4a645a4c..e7022ef6 100644 --- a/runtime/script_loader.h +++ b/runtime/script_loader.h @@ -14,12 +14,13 @@ 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); 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); }; diff --git a/spin.toml b/spin.toml index c63db163..a76fa6cd 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"] diff --git a/tests/wpt-harness/run-wpt.mjs b/tests/wpt-harness/run-wpt.mjs index 0200109e..bad8b455 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 2545d8de..686903c3 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", diff --git a/tests/wpt-harness/wpt.cmake b/tests/wpt-harness/wpt.cmake index f0d2933f..bfb541d2 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") diff --git a/tests/wpt-harness/wpt_builtins.cpp b/tests/wpt-harness/wpt_builtins.cpp index ab83cfc5..d62566b9 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);