feat(libSceHttp2): complete asynchronous requests through the kqueue option

sceHttp2SendRequestAsync no longer fails synchronously: it queues a
completion carrying the network error and triggers the user event named
by the kqueue option. sceHttp2WaitAsync returns queued completions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kjhq 2026-10-07 10:39:14 +00:00
parent 0d3c1eec89
commit 1cc4d25c04
5 changed files with 93 additions and 11 deletions

View File

@ -1243,6 +1243,14 @@ struct Http2AsyncResult {
void* reserved;
};
struct Http2AsyncOption {
KernelEqueue equeue;
int user_event_id;
std::uint8_t padding[4];
void* user_data;
void* reserved;
};
struct NpTitleId { char data[13]; char pad[3]; };
struct NpTitleSecret { std::uint8_t data[128]; };
struct NpContentRestriction { std::uint8_t opaque[128]; };

View File

@ -1,5 +1,6 @@
add_library(libSceHttp2 SHARED EXCLUDE_FROM_ALL Export.cpp)
target_include_directories(libSceHttp2 PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(libSceHttp2 PRIVATE libc libkernel)
configure_windows_unwind(libSceHttp2)
if(MINGW)
@ -11,7 +12,7 @@ endif()
if(BUILD_TESTING)
add_test_executable(guest_http2_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp2.cpp)
target_include_directories(guest_http2_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_http2_tests PRIVATE libSceHttp2 libc)
target_link_libraries(guest_http2_tests PRIVATE libSceHttp2 libkernel libc)
configure_windows_unwind(guest_http2_tests)
add_test(NAME guest_http2 COMMAND guest_http2_tests)
if(WIN32)

View File

@ -2,12 +2,35 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/Equeue/Equeue.hpp"
#include <atomic>
#include <deque>
#include <mutex>
#include <unordered_map>
// No network is emulated: contexts, templates and requests can be created, but any request
// that would touch the network fails with the library's network error.
static constexpr int ERROR_NETWORK = static_cast<int>(0x80436063);
static std::atomic<int> g_nextHandle{1};
static std::mutex g_completionsMutex;
static std::unordered_map<int, std::deque<Http2AsyncResult>> g_completions;
static void CompleteAsync(const char* function, int req_id, const Http2AsyncOption* kqueue_option, const void* option) {
if (kqueue_option == nullptr) NotImplemented_nid_no_patch(function);
if (option != nullptr) NotImplemented_nid_no_patch(function);
{
std::lock_guard lock(g_completionsMutex);
Http2AsyncResult completion{};
completion.req_id = req_id;
completion.result = ERROR_NETWORK;
g_completions[req_id].push_back(completion);
}
if (EqueueTriggerEvent_nid_postfix(kqueue_option->equeue, static_cast<uintptr_t>(kqueue_option->user_event_id), EVFILT_USER, kqueue_option->user_data) != 0) {
std::lock_guard lock(g_completionsMutex);
g_completions[req_id].pop_back();
NotImplemented_nid_no_patch(function);
}
}
extern "C" {
@ -36,7 +59,8 @@ int APS5_VABI sceHttp2CreateTemplate(int lib_http2_ctx_id, const char* user_agen
}
int APS5_VABI sceHttp2DeleteRequest(int req_id) {
(void)req_id;
std::lock_guard lock(g_completionsMutex);
g_completions.erase(req_id);
return 0;
}
@ -96,13 +120,11 @@ int APS5_VABI sceHttp2SendRequest(int req_id, const void* post_data, size_t size
return ERROR_NETWORK;
}
int APS5_VABI sceHttp2SendRequestAsync(int req_id, const void* post_data, size_t size, void* kqueue_option, void* option) {
(void)req_id;
int APS5_VABI sceHttp2SendRequestAsync(int req_id, const void* post_data, size_t size, Http2AsyncOption* kqueue_option, void* option) {
(void)post_data;
(void)size;
(void)kqueue_option;
(void)option;
return ERROR_NETWORK;
CompleteAsync(__func__, req_id, kqueue_option, option);
return 0;
}
int APS5_VABI sceHttp2SetAuthEnabled(int id, int is_enable) {
@ -197,11 +219,14 @@ int APS5_VABI sceHttp2Term(int lib_http2_ctx_id) {
}
int APS5_VABI sceHttp2WaitAsync(int req_id, Http2AsyncResult* result, uint32_t* timeout, void* option) {
(void)req_id;
(void)result;
(void)timeout;
(void)option;
return ERROR_NETWORK;
if (result == nullptr || option != nullptr) NotImplemented_nid_no_patch(__func__);
std::lock_guard lock(g_completionsMutex);
const auto pending = g_completions.find(req_id);
if (pending == g_completions.end() || pending->second.empty()) NotImplemented_nid_no_patch("sceHttp2WaitAsync: waiting for an operation that has not completed");
*result = pending->second.front();
pending->second.pop_front();
return 0;
}
int APS5_VABI sceHttp2AbortRequest(int req_id) {

View File

@ -1,6 +1,7 @@
#include "SceTypes.hpp"
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
extern "C" {
int APS5_VABI sceHttp2Init(int, int, std::size_t, int);
@ -11,11 +12,31 @@ int APS5_VABI sceHttp2SetCookieBox(int, int);
int APS5_VABI sceHttp2CookieFlush(int);
int APS5_VABI sceHttp2SetRequestNoContentLength(int);
int APS5_VABI sceHttp2SendRequest(int, const void*, std::size_t);
int APS5_VABI sceHttp2SendRequestAsync(int, const void*, std::size_t, Http2AsyncOption*, void*);
int APS5_VABI sceHttp2WaitAsync(int, Http2AsyncResult*, std::uint32_t*, void*);
int APS5_VABI sceHttp2DeleteRequest(int);
int APS5_VABI sceHttp2Term(int);
int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name);
int APS5_VABI sceKernelDeleteEqueue(KernelEqueue eq);
int APS5_VABI sceKernelWaitEqueue(KernelEqueue eq, KernelEvent* ev, int num, int* out, const KernelUseconds* timo);
int APS5_VABI sceKernelAddUserEventEdge(KernelEqueue eq, int id);
int APS5_VABI sceKernelDeleteUserEvent(KernelEqueue eq, int id);
uintptr_t APS5_VABI sceKernelGetEventId(const KernelEvent* ev);
void* APS5_VABI sceKernelGetEventUserData(const KernelEvent* ev);
}
static void Require(bool value) { if (!value) std::abort(); }
template <typename TCall>
static bool Throws(TCall call) {
try {
call();
} catch (const std::exception&) {
return true;
}
return false;
}
int main() {
const int context = sceHttp2Init(1, 1, 0x10000, 4);
Require(context > 0);
@ -32,5 +53,31 @@ int main() {
Require(sceHttp2SetRequestNoContentLength(request) == 0);
Require(sceHttp2CookieFlush(context) == 0);
Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast<int>(0x80436063));
KernelEqueue eq = 0;
Require(sceKernelCreateEqueue(&eq, "http2") == 0);
Require(sceKernelAddUserEventEdge(eq, request) == 0);
int tag = 0;
Http2AsyncOption option{};
option.equeue = eq;
option.user_event_id = request;
option.user_data = &tag;
Require(sceHttp2SendRequestAsync(request, nullptr, 0, &option, nullptr) == 0);
KernelEvent event{};
int count = 0;
const KernelUseconds timeout = 1000000;
Require(sceKernelWaitEqueue(eq, &event, 1, &count, &timeout) == 0 && count == 1);
Require(sceKernelGetEventId(&event) == static_cast<uintptr_t>(request));
Require(sceKernelGetEventUserData(&event) == &tag);
Http2AsyncResult result{};
Require(sceHttp2WaitAsync(request, &result, nullptr, nullptr) == 0);
Require(result.req_id == request && result.result == static_cast<int>(0x80436063));
Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); }));
Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, nullptr, nullptr); }));
Require(sceKernelDeleteUserEvent(eq, request) == 0);
Require(Throws([&] { sceHttp2SendRequestAsync(request, nullptr, 0, &option, nullptr); }));
Require(Throws([&] { sceHttp2WaitAsync(request, &result, nullptr, nullptr); }));
Require(sceHttp2DeleteRequest(request) == 0);
Require(sceKernelDeleteEqueue(eq) == 0);
Require(sceHttp2Term(context) == 0);
}

View File

@ -182,6 +182,7 @@ Throughout the project, every function at every stage either **does exactly what
### Functional
- [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw
- [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported
- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out
- [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler but nothing delivers to it: a host fault still ends the process, and `sceKernelRaiseException` is not implemented. PPSA12544's PS5Util module (Unity) installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection, so delivery must run the handler on the target thread with a FreeBSD `ucontext`