browser wasm
This commit is contained in:
parent
28fab2509b
commit
e8630528af
24 changed files with 2490 additions and 100 deletions
199
implementations/Crafter.Network-ClientHTTP-Browser.cpp
Normal file
199
implementations/Crafter.Network-ClientHTTP-Browser.cpp
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
Crafter®.Network
|
||||
Copyright (C) 2026 Catcrafts®
|
||||
Catcrafts.net
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 3.0 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
// CRAFTER_NETWORK_BROWSER implementation of ClientHTTP. Each SendAsync
|
||||
// hands its request to the JS bridge (additional/network-env.js), which
|
||||
// runs a fetch() and dispatches the result back through the
|
||||
// CrafterNetworkOnFetchComplete / CrafterNetworkOnFetchError wasm exports.
|
||||
|
||||
module;
|
||||
module Crafter.Network:ClientHTTP_impl;
|
||||
import :ClientHTTP;
|
||||
import :HTTP;
|
||||
import std;
|
||||
|
||||
using namespace Crafter;
|
||||
|
||||
namespace Crafter::NetworkBrowserBindings {
|
||||
// External linkage so the import_module/import_name attributes wire up.
|
||||
__attribute__((import_module("env"), import_name("crafterNetworkFetch")))
|
||||
void crafterNetworkFetch(
|
||||
const char* method, std::int32_t methodLen,
|
||||
const char* url, std::int32_t urlLen,
|
||||
const char* headers, std::int32_t headersLen,
|
||||
const char* body, std::int32_t bodyLen,
|
||||
std::int32_t callbackId);
|
||||
}
|
||||
|
||||
namespace {
|
||||
struct FetchCallbacks {
|
||||
std::function<void(HTTPResponse)> onSuccess;
|
||||
std::function<void(std::string)> onError;
|
||||
};
|
||||
|
||||
// JS dispatches back into wasm via a stable id we mint here. The id
|
||||
// counter is monotone — wraparound at 2 billion fetches is not a
|
||||
// realistic concern. The map is touched only from the JS event loop
|
||||
// (single-threaded in the browser), so no synchronisation is needed.
|
||||
std::unordered_map<std::int32_t, FetchCallbacks>& Callbacks() {
|
||||
static std::unordered_map<std::int32_t, FetchCallbacks> m;
|
||||
return m;
|
||||
}
|
||||
std::int32_t NextId() {
|
||||
static std::int32_t counter = 0;
|
||||
return ++counter;
|
||||
}
|
||||
|
||||
// Serialise headers as newline-separated "name: value" pairs. The JS
|
||||
// side splits on '\n' and the first ": " for header construction.
|
||||
std::string SerialiseHeaders(const std::unordered_map<std::string, std::string>& headers) {
|
||||
std::string out;
|
||||
bool first = true;
|
||||
for (const auto& [name, value] : headers) {
|
||||
if (!first) out += '\n';
|
||||
first = false;
|
||||
out += name;
|
||||
out += ": ";
|
||||
out += value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Parse a "name: value\nname2: value2" blob into the HTTPResponse map.
|
||||
// Names are kept verbatim (fetch surfaces them lowercase already on the
|
||||
// browser side via response.headers.forEach).
|
||||
void ParseHeaders(std::string_view raw, HTTPResponse& response) {
|
||||
std::size_t pos = 0;
|
||||
while (pos < raw.size()) {
|
||||
std::size_t end = raw.find('\n', pos);
|
||||
std::string_view line = raw.substr(pos, end == std::string_view::npos ? raw.size() - pos : end - pos);
|
||||
std::size_t sep = line.find(": ");
|
||||
if (sep != std::string_view::npos) {
|
||||
response.headers.emplace(std::string(line.substr(0, sep)),
|
||||
std::string(line.substr(sep + 2)));
|
||||
}
|
||||
if (end == std::string_view::npos) break;
|
||||
pos = end + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientHTTP::Impl {};
|
||||
|
||||
ClientHTTP::ClientHTTP(const char* host, std::uint16_t port, QUICClientCredentials)
|
||||
: host(host), port(port), impl(std::make_unique<Impl>()) {}
|
||||
|
||||
ClientHTTP::ClientHTTP(std::string host, std::uint16_t port, QUICClientCredentials creds)
|
||||
: ClientHTTP(host.c_str(), port, std::move(creds)) {}
|
||||
|
||||
ClientHTTP::ClientHTTP(ClientHTTP&&) noexcept = default;
|
||||
ClientHTTP::~ClientHTTP() = default;
|
||||
|
||||
void ClientHTTP::SendAsync(const HTTPRequest& request,
|
||||
std::function<void(HTTPResponse)> onSuccess,
|
||||
std::function<void(std::string)> onError) {
|
||||
std::int32_t id = NextId();
|
||||
Callbacks().emplace(id, FetchCallbacks{std::move(onSuccess), std::move(onError)});
|
||||
|
||||
std::string method = request.method.empty() ? std::string("GET") : request.method;
|
||||
std::string path = request.path.empty() ? std::string("/") : request.path;
|
||||
// Sentinel: a ClientHTTP constructed with an empty host fetches against
|
||||
// the page's own origin. fetch(url) in JS handles a leading-slash path
|
||||
// by resolving it against window.location, so we just hand the path
|
||||
// straight through. Useful for fetching files served by whatever static
|
||||
// server is hosting the wasm (e.g. ./cert-hash.txt for WebTransport).
|
||||
std::string url;
|
||||
if (host.empty()) {
|
||||
url = path;
|
||||
} else {
|
||||
std::string scheme = request.scheme.empty() ? std::string("https") : request.scheme;
|
||||
std::string authority = request.authority.empty() ? (host + ":" + std::to_string(port)) : request.authority;
|
||||
url = scheme + "://" + authority + path;
|
||||
}
|
||||
std::string headerStr = SerialiseHeaders(request.headers);
|
||||
|
||||
Crafter::NetworkBrowserBindings::crafterNetworkFetch(
|
||||
method.data(), static_cast<std::int32_t>(method.size()),
|
||||
url.data(), static_cast<std::int32_t>(url.size()),
|
||||
headerStr.data(), static_cast<std::int32_t>(headerStr.size()),
|
||||
request.body.data(), static_cast<std::int32_t>(request.body.size()),
|
||||
id);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// JS allocates `headersPtr` and `bodyPtr` via WasmAlloc, copies the
|
||||
// response into them, then transfers ownership across this call. We
|
||||
// free the buffers after copying into the HTTPResponse.
|
||||
__attribute__((export_name("CrafterNetworkOnFetchComplete")))
|
||||
void CrafterNetworkOnFetchComplete(std::int32_t callbackId,
|
||||
std::int32_t status,
|
||||
char* headersPtr, std::int32_t headersLen,
|
||||
char* bodyPtr, std::int32_t bodyLen) {
|
||||
auto& callbacks = Callbacks();
|
||||
auto it = callbacks.find(callbackId);
|
||||
if (it == callbacks.end()) {
|
||||
std::free(headersPtr);
|
||||
std::free(bodyPtr);
|
||||
return;
|
||||
}
|
||||
|
||||
HTTPResponse response;
|
||||
response.status = std::to_string(status);
|
||||
if (headersPtr && headersLen > 0) {
|
||||
ParseHeaders(std::string_view(headersPtr, static_cast<std::size_t>(headersLen)), response);
|
||||
}
|
||||
if (bodyPtr && bodyLen > 0) {
|
||||
response.body.assign(bodyPtr, static_cast<std::size_t>(bodyLen));
|
||||
}
|
||||
std::free(headersPtr);
|
||||
std::free(bodyPtr);
|
||||
|
||||
auto onSuccess = std::move(it->second.onSuccess);
|
||||
callbacks.erase(it);
|
||||
if (onSuccess) onSuccess(std::move(response));
|
||||
}
|
||||
|
||||
__attribute__((export_name("CrafterNetworkOnFetchError")))
|
||||
void CrafterNetworkOnFetchError(std::int32_t callbackId,
|
||||
char* messagePtr, std::int32_t messageLen) {
|
||||
auto& callbacks = Callbacks();
|
||||
auto it = callbacks.find(callbackId);
|
||||
if (it == callbacks.end()) {
|
||||
std::free(messagePtr);
|
||||
return;
|
||||
}
|
||||
std::string msg(messagePtr ? messagePtr : "", static_cast<std::size_t>(messageLen));
|
||||
std::free(messagePtr);
|
||||
|
||||
auto onError = std::move(it->second.onError);
|
||||
callbacks.erase(it);
|
||||
if (onError) onError(std::move(msg));
|
||||
}
|
||||
|
||||
// WasmAlloc / WasmFree are the buffer-marshalling primitives the JS
|
||||
// bridge calls into. Crafter.Graphics's Dom backend defines the same
|
||||
// pair; we mark ours weak so the two libraries can coexist in one
|
||||
// executable without a duplicate-symbol error.
|
||||
__attribute__((export_name("WasmAlloc"), weak))
|
||||
void* WasmAlloc(std::int32_t size) { return std::malloc(static_cast<std::size_t>(size)); }
|
||||
|
||||
__attribute__((export_name("WasmFree"), weak))
|
||||
void WasmFree(void* ptr) { std::free(ptr); }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue