diff --git a/3rd/libhv/CMakeLists.txt b/3rd/libhv/CMakeLists.txt index 4047403d..f1182896 100644 --- a/3rd/libhv/CMakeLists.txt +++ b/3rd/libhv/CMakeLists.txt @@ -12,6 +12,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) option(BUILD_SHARED "build shared library" OFF) option(BUILD_STATIC "build static library" ON) option(BUILD_EXAMPLES "build examples" OFF) + option(WITH_OPENSSL "with openssl library" OFF) add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/libhv) diff --git a/3rd/libhv/include/libhv_bindings.hpp b/3rd/libhv/include/libhv_bindings.hpp index d37c260f..f3a05e8c 100644 --- a/3rd/libhv/include/libhv_bindings.hpp +++ b/3rd/libhv/include/libhv_bindings.hpp @@ -1,10 +1,53 @@ #pragma once #include "pocketpy.h" +#include "http/HttpMessage.h" +#include "base/hplatform.h" extern "C" void pk__add_module_libhv(); +void libhv_HttpRequest_create(py_OutRef out, HttpRequestPtr ptr); + +py_Type libhv_register_HttpRequest(py_GlobalRef mod); py_Type libhv_register_HttpClient(py_GlobalRef mod); py_Type libhv_register_HttpServer(py_GlobalRef mod); py_Type libhv_register_WebSocketClient(py_GlobalRef mod); -py_Type libhv_register_WebSocketServer(py_GlobalRef mod); + +#include +#include + +template +class libhv_MQ { +private: + std::atomic lock; + std::deque queue; + +public: + void push(T msg) { + while(lock.exchange(true)) { + hv_delay(1); + } + queue.push_back(msg); + lock.store(false); + } + + bool pop(T* msg) { + while(lock.exchange(true)) { + hv_delay(1); + } + if(queue.empty()) { + lock.store(false); + return false; + } + *msg = queue.front(); + queue.pop_front(); + lock.store(false); + return true; + } +}; + +enum class WsMessageType { + onopen, + onclose, + onmessage, +}; diff --git a/3rd/libhv/src/HttpClient.cpp b/3rd/libhv/src/HttpClient.cpp index ff4a0832..e08367eb 100644 --- a/3rd/libhv/src/HttpClient.cpp +++ b/3rd/libhv/src/HttpClient.cpp @@ -1,21 +1,23 @@ +#include "HttpMessage.h" #include "libhv_bindings.hpp" #include "base/herr.h" #include "http/client/HttpClient.h" struct libhv_HttpResponse { - HttpResponsePtr ptr; + HttpRequestPtr request; + HttpResponsePtr response; bool ok; - bool is_valid() { return ok && ptr != NULL; } + bool is_valid() { return ok && response != NULL; } - libhv_HttpResponse() : ptr(NULL), ok(false) {} + libhv_HttpResponse(HttpRequestPtr request) : request(request), response(NULL), ok(false) {} }; static bool libhv_HttpResponse_status_code(int argc, py_Ref argv) { PY_CHECK_ARGC(1); libhv_HttpResponse* resp = (libhv_HttpResponse*)py_touserdata(argv); if(!resp->is_valid()) return RuntimeError("HttpResponse: no response"); - py_newint(py_retval(), resp->ptr->status_code); + py_newint(py_retval(), resp->response->status_code); return true; }; @@ -28,7 +30,7 @@ static bool libhv_HttpResponse_headers(int argc, py_Ref argv) { py_newdict(headers); py_Ref _0 = py_pushtmp(); py_Ref _1 = py_pushtmp(); - for(auto& kv: resp->ptr->headers) { + for(auto& kv: resp->response->headers) { py_newstr(_0, kv.first.c_str()); py_newstr(_1, kv.second.c_str()); py_dict_setitem(headers, _0, _1); @@ -46,8 +48,8 @@ static bool libhv_HttpResponse_text(int argc, py_Ref argv) { py_Ref text = py_getslot(argv, 1); if(py_isnil(text)) { c11_sv sv; - sv.data = resp->ptr->body.c_str(); - sv.size = resp->ptr->body.size(); + sv.data = resp->response->body.c_str(); + sv.size = resp->response->body.size(); py_newstrv(text, sv); } py_assign(py_retval(), text); @@ -60,9 +62,9 @@ static bool libhv_HttpResponse_content(int argc, py_Ref argv) { if(!resp->is_valid()) return RuntimeError("HttpResponse: no response"); py_Ref content = py_getslot(argv, 2); if(py_isnil(content)) { - int size = resp->ptr->body.size(); + int size = resp->response->body.size(); unsigned char* buf = py_newbytes(content, size); - memcpy(buf, resp->ptr->body.data(), size); + memcpy(buf, resp->response->body.data(), size); } py_assign(py_retval(), content); return true; @@ -72,7 +74,7 @@ static bool libhv_HttpResponse_json(int argc, py_Ref argv) { PY_CHECK_ARGC(1); libhv_HttpResponse* resp = (libhv_HttpResponse*)py_touserdata(argv); if(!resp->is_valid()) return RuntimeError("HttpResponse: no response"); - const char* source = resp->ptr->body.c_str(); // json string is null-terminated + const char* source = resp->response->body.c_str(); // json string is null-terminated return py_json_loads(source); }; @@ -111,11 +113,19 @@ static bool libhv_HttpResponse__repr__(int argc, py_Ref argv) { if(!resp->is_valid()) { py_newstr(py_retval(), ""); } else { - py_newfstr(py_retval(), "", (int)resp->ptr->status_code); + py_newfstr(py_retval(), "", (int)resp->response->status_code); } return true; } +static bool libhv_HttpResponse_cancel(int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_HttpResponse* resp = (libhv_HttpResponse*)py_touserdata(argv); + resp->request->Cancel(); + py_newnone(py_retval()); + return true; +} + static py_Type libhv_register_HttpResponse(py_GlobalRef mod) { py_Type type = py_newtype("HttpResponse", tp_object, mod, [](void* ud) { ((libhv_HttpResponse*)ud)->~libhv_HttpResponse(); @@ -133,6 +143,8 @@ static py_Type libhv_register_HttpResponse(py_GlobalRef mod) { py_bindmagic(type, __repr__, libhv_HttpResponse__repr__); // completed py_bindproperty(type, "completed", libhv_HttpResponse_completed, NULL); + // cancel + py_bindmethod(type, "cancel", libhv_HttpResponse_cancel); return type; } @@ -218,15 +230,15 @@ static bool libhv_HttpClient__send_request(py_Ref arg_self, 3, // headers, text, content sizeof(libhv_HttpResponse)); // placement new - new (retval) libhv_HttpResponse(); + new (retval) libhv_HttpResponse(req); int code = cli->sendAsync(req, [retval](const HttpResponsePtr& resp) { if(resp == NULL) { retval->ok = false; - retval->ptr = NULL; + retval->response = NULL; } else { retval->ok = true; - retval->ptr = resp; + retval->response = resp; } }); if(code != 0) { diff --git a/3rd/libhv/src/HttpRequest.cpp b/3rd/libhv/src/HttpRequest.cpp new file mode 100644 index 00000000..7538f92e --- /dev/null +++ b/3rd/libhv/src/HttpRequest.cpp @@ -0,0 +1,114 @@ +#include "libhv_bindings.hpp" +#include "HttpMessage.h" + +struct libhv_HttpRequest { + HttpRequestPtr ptr; + + libhv_HttpRequest(HttpRequestPtr ptr) : ptr(ptr) {} +}; + +void libhv_HttpRequest_create(py_OutRef out, HttpRequestPtr ptr) { + py_Type type = py_gettype("libhv", py_name("HttpRequest")); + libhv_HttpRequest* self = + (libhv_HttpRequest*)py_newobject(out, type, 2, sizeof(libhv_HttpRequest)); + new (self) libhv_HttpRequest(ptr); +} + +py_Type libhv_register_HttpRequest(py_GlobalRef mod) { + py_Type type = py_newtype("HttpRequest", tp_object, mod, [](void* ud) { + ((libhv_HttpRequest*)ud)->~libhv_HttpRequest(); + }); + + py_bindmagic(type, __new__, [](int argc, py_Ref argv) { + return py_exception(tp_NotImplementedError, ""); + }); + + py_bindproperty( + type, + "method", + [](int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv); + py_newstr(py_retval(), req->ptr->Method()); + return true; + }, + NULL); + + py_bindproperty( + type, + "url", + [](int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv); + py_newstr(py_retval(), req->ptr->Url().c_str()); + return true; + }, + NULL); + + py_bindproperty( + type, + "path", + [](int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv); + py_newstr(py_retval(), req->ptr->Path().c_str()); + return true; + }, + NULL); + + // headers (cache in slots[0]) + py_bindproperty( + type, + "headers", + [](int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv); + py_Ref headers = py_getslot(argv, 0); + if(py_isnil(headers)) { + py_newdict(headers); + py_Ref _0 = py_pushtmp(); + py_Ref _1 = py_pushtmp(); + for(auto& kv: req->ptr->headers) { + py_newstr(_0, kv.first.c_str()); // TODO: tolower + py_newstr(_1, kv.second.c_str()); + py_dict_setitem(headers, _0, _1); + } + py_shrink(2); + } + py_assign(py_retval(), headers); + return true; + }, + NULL); + + // data (cache in slots[1]) + py_bindproperty( + type, + "data", + [](int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_HttpRequest* req = (libhv_HttpRequest*)py_touserdata(argv); + py_Ref data = py_getslot(argv, 1); + + if(py_isnil(data)) { + auto content_type = req->ptr->ContentType(); + bool is_text_data = content_type == TEXT_PLAIN || + content_type == APPLICATION_JSON || + content_type == APPLICATION_XML || content_type == TEXT_HTML || + content_type == CONTENT_TYPE_NONE; + if(is_text_data) { + c11_sv sv; + sv.data = req->ptr->body.data(); + sv.size = req->ptr->body.size(); + py_newstrv(data, sv); + } else { + unsigned char* buf = py_newbytes(data, req->ptr->body.size()); + memcpy(buf, req->ptr->body.data(), req->ptr->body.size()); + } + } + py_assign(py_retval(), data); + return true; + }, + NULL); + + return type; +} \ No newline at end of file diff --git a/3rd/libhv/src/HttpServer.cpp b/3rd/libhv/src/HttpServer.cpp index 30a566bd..0728ebce 100644 --- a/3rd/libhv/src/HttpServer.cpp +++ b/3rd/libhv/src/HttpServer.cpp @@ -1,38 +1,24 @@ +#include "HttpMessage.h" +#include "WebSocketChannel.h" #include "libhv_bindings.hpp" -#include "http/server/HttpServer.h" -#include "base/herr.h" - -#include -#include - -template -struct libhv_MQ { - std::atomic lock_in; - std::atomic lock_out; - std::deque queue_in; - std::deque queue_out; - - void begin_in() { - while(lock_in.exchange(true)) { - hv_delay(1); - } - } - - void end_in() { lock_in.store(false); } - - void begin_out() { - while(lock_out.exchange(true)) { - hv_delay(1); - } - } - - void end_out() { lock_out.store(false); } -}; +#include "http/server/WebSocketServer.h" +#include "pocketpy/pocketpy.h" struct libhv_HttpServer { - hv::HttpService service; - hv::HttpServer server; - libhv_MQ> mq; + hv::HttpService http_service; + hv::WebSocketService ws_service; + hv::WebSocketServer server; + + libhv_MQ>*> mq; + + struct WsMessage { + WsMessageType type; + hv::WebSocketChannel* channel; + HttpRequestPtr request; + std::string body; + }; + + libhv_MQ ws_mq; }; static bool libhv_HttpServer__new__(int argc, py_Ref argv) { @@ -49,31 +35,37 @@ static bool libhv_HttpServer__init__(int argc, py_Ref argv) { PY_CHECK_ARG_TYPE(2, tp_int); const char* host = py_tostr(py_arg(1)); int port = py_toint(py_arg(2)); - - self->service.AllowCORS(); - http_ctx_handler internal_handler = [self](const HttpContextPtr& ctx) { - self->mq.begin_in(); - self->mq.queue_in.push_back(ctx); - self->mq.end_in(); - - while(true) { - self->mq.begin_out(); - if(!self->mq.queue_out.empty()) { - auto& msg = self->mq.queue_out.front(); - if(msg.first == ctx) { - self->mq.queue_out.pop_front(); - self->mq.end_out(); - return msg.second; - } - } - self->mq.end_out(); - hv_delay(1); - } - }; - self->service.Any("*", internal_handler); - self->server.registerHttpService(&self->service); self->server.setHost(host); self->server.setPort(port); + + // http + self->http_service.AllowCORS(); + http_ctx_handler internal_handler = [self](const HttpContextPtr& ctx) { + std::pair> msg(ctx, 0); + self->mq.push(&msg); + int code; + do { + code = msg.second.load(); + } while(code == 0); + return code; + }; + self->http_service.Any("*", internal_handler); + self->server.registerHttpService(&self->http_service); + + // websocket + self->ws_service.onopen = [self](const WebSocketChannelPtr& channel, + const HttpRequestPtr& req) { + self->ws_mq.push({WsMessageType::onopen, channel.get(), req, ""}); + }; + self->ws_service.onmessage = [self](const WebSocketChannelPtr& channel, + const std::string& msg) { + self->ws_mq.push({WsMessageType::onmessage, channel.get(), nullptr, msg}); + }; + self->ws_service.onclose = [self](const WebSocketChannelPtr& channel) { + self->ws_mq.push({WsMessageType::onclose, channel.get(), nullptr, ""}); + }; + self->server.registerWebSocketService(&self->ws_service); + py_newnone(py_retval()); return true; } @@ -84,75 +76,45 @@ static bool libhv_HttpServer_dispatch(int argc, py_Ref argv) { py_Ref callable = py_arg(1); if(!py_callable(callable)) return TypeError("dispatcher must be callable"); - self->mq.begin_in(); - if(self->mq.queue_in.empty()) { - self->mq.end_in(); + std::pair>* mq_msg; + if(!self->mq.pop(&mq_msg)) { py_newbool(py_retval(), false); return true; } else { - HttpContextPtr ctx = self->mq.queue_in.front(); - self->mq.queue_in.pop_front(); - self->mq.end_in(); - - const char* method = ctx->request->Method(); - std::string path = ctx->request->Path(); - const http_headers& headers = ctx->request->headers; - const std::string& data = ctx->request->body; - - py_OutRef msg = py_pushtmp(); - py_newdict(msg); - py_Ref _0 = py_pushtmp(); - py_Ref _1 = py_pushtmp(); - py_Ref _2 = py_pushtmp(); - py_Ref _3 = py_pushtmp(); - - // method - py_newstr(_0, "method"); - py_newstr(_1, method); - py_dict_setitem(msg, _0, _1); - // path - py_newstr(_0, "path"); - py_newstr(_1, path.c_str()); - py_dict_setitem(msg, _0, _1); - // headers - py_newstr(_0, "headers"); - py_newdict(_1); - py_dict_setitem(msg, _0, _1); - for(auto& header: headers) { - py_newstr(_2, header.first.c_str()); - py_newstr(_3, header.second.c_str()); - py_dict_setitem(_1, _2, _3); - } - // data - py_newstr(_0, "data"); - auto content_type = ctx->request->ContentType(); - bool is_text_data = content_type == TEXT_PLAIN || content_type == APPLICATION_JSON || - content_type == APPLICATION_XML || content_type == TEXT_HTML || - content_type == CONTENT_TYPE_NONE; - if(is_text_data) { - py_newstrv(_1, {data.c_str(), (int)data.size()}); - } else { - unsigned char* buf = py_newbytes(_1, data.size()); - memcpy(buf, data.data(), data.size()); - } - py_dict_setitem(msg, _0, _1); - py_assign(py_retval(), msg); - py_shrink(5); - + HttpContextPtr ctx = mq_msg->first; + libhv_HttpRequest_create(py_retval(), ctx->request); // call dispatcher - if(!py_call(callable, 1, py_retval())) { return false; } + if(!py_call(callable, 1, py_retval())) return false; py_Ref object; int status_code = 200; if(py_istuple(py_retval())) { - // "Hello, world!", 200 - if(py_tuple_len(py_retval()) != 2) { - return ValueError("dispatcher should return `object | tuple[object, int]`"); + int length = py_tuple_len(py_retval()); + if(length == 2 || length == 3) { + // "Hello, world!", 200 + object = py_tuple_getitem(py_retval(), 0); + py_ItemRef status_code_object = py_tuple_getitem(py_retval(), 1); + if(!py_checkint(status_code_object)) return false; + status_code = py_toint(status_code_object); + + if(length == 3) { + // "Hello, world!", 200, {"Content-Type": "text/plain"} + py_ItemRef headers_object = py_tuple_getitem(py_retval(), 2); + if(!py_checktype(headers_object, tp_dict)) return false; + bool ok = py_dict_apply( + headers_object, + [](py_Ref key, py_Ref value, void* ctx_) { + if(!py_checkstr(key) || !py_checkstr(value)) return false; + ((hv::HttpContext*)ctx_) + ->response->SetHeader(py_tostr(key), py_tostr(value)); + return true; + }, + ctx.get()); + if(!ok) return false; + } + } else { + return TypeError("dispatcher return tuple must have 2 or 3 elements"); } - object = py_tuple_getitem(py_retval(), 0); - py_ItemRef status_code_object = py_tuple_getitem(py_retval(), 1); - if(!py_checkint(status_code_object)) return false; - status_code = py_toint(status_code_object); } else { // "Hello, world!" object = py_retval(); @@ -182,9 +144,7 @@ static bool libhv_HttpServer_dispatch(int argc, py_Ref argv) { } } - self->mq.begin_out(); - self->mq.queue_out.push_back({ctx, status_code}); - self->mq.end_out(); + mq_msg->second.store(status_code); } py_newbool(py_retval(), true); return true; @@ -194,21 +154,84 @@ static bool libhv_HttpServer_start(int argc, py_Ref argv) { PY_CHECK_ARGC(1); libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0)); int code = self->server.start(); - if(code != 0) { - return RuntimeError("HttpServer start failed: %s (%d)", hv_strerror(code), code); - } - py_newnone(py_retval()); + py_newint(py_retval(), code); return true; } static bool libhv_HttpServer_stop(int argc, py_Ref argv) { PY_CHECK_ARGC(1); libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0)); - self->server.stop(); + int code = self->server.stop(); + py_newint(py_retval(), code); + return true; +} + +static bool libhv_HttpServer_ws_set_ping_interval(int argc, py_Ref argv) { + PY_CHECK_ARGC(2); + libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0)); + PY_CHECK_ARG_TYPE(1, tp_int); + int interval = py_toint(py_arg(1)); + self->ws_service.setPingInterval(interval); py_newnone(py_retval()); return true; } +static bool libhv_HttpServer_ws_send(int argc, py_Ref argv) { + PY_CHECK_ARGC(3); + libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0)); + PY_CHECK_ARG_TYPE(1, tp_int); + PY_CHECK_ARG_TYPE(2, tp_str); + py_i64 channel = py_toint(py_arg(1)); + const char* msg = py_tostr(py_arg(2)); + + hv::WebSocketChannel* p_channel = reinterpret_cast(channel); + int code = p_channel->send(msg); + py_newint(py_retval(), code); + return true; +} + +static bool libhv_HttpServer_ws_recv(int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_HttpServer* self = (libhv_HttpServer*)py_touserdata(py_arg(0)); + libhv_HttpServer::WsMessage msg; + if(!self->ws_mq.pop(&msg)) { + py_newnone(py_retval()); + return true; + } + py_newtuple(py_retval(), 2); + switch(msg.type) { + case WsMessageType::onopen: { + // "onopen", (channel, request) + assert(msg.request != nullptr); + py_newstr(py_tuple_getitem(py_retval(), 0), "onopen"); + py_Ref args = py_tuple_getitem(py_retval(), 1); + py_newtuple(args, 2); + py_newint(py_tuple_getitem(args, 0), (py_i64)msg.channel); + libhv_HttpRequest_create(py_tuple_getitem(args, 1), msg.request); + break; + } + case WsMessageType::onclose: { + // "onclose", channel + py_newstr(py_tuple_getitem(py_retval(), 0), "onclose"); + py_newint(py_tuple_getitem(py_retval(), 1), (py_i64)msg.channel); + break; + } + case WsMessageType::onmessage: { + // "onmessage", (channel, body) + py_newstr(py_tuple_getitem(py_retval(), 0), "onmessage"); + py_Ref args = py_tuple_getitem(py_retval(), 1); + py_newtuple(args, 2); + py_newint(py_tuple_getitem(args, 0), (py_i64)msg.channel); + c11_sv sv; + sv.data = msg.body.data(); + sv.size = msg.body.size(); + py_newstrv(py_tuple_getitem(args, 1), sv); + break; + } + } + return true; +} + py_Type libhv_register_HttpServer(py_GlobalRef mod) { py_Type type = py_newtype("HttpServer", tp_object, mod, [](void* ud) { libhv_HttpServer* self = (libhv_HttpServer*)ud; @@ -217,8 +240,12 @@ py_Type libhv_register_HttpServer(py_GlobalRef mod) { py_bindmagic(type, __new__, libhv_HttpServer__new__); py_bindmagic(type, __init__, libhv_HttpServer__init__); - py_bindmethod(type, "dispatch", libhv_HttpServer_dispatch); py_bindmethod(type, "start", libhv_HttpServer_start); py_bindmethod(type, "stop", libhv_HttpServer_stop); + py_bindmethod(type, "dispatch", libhv_HttpServer_dispatch); + + py_bindmethod(type, "ws_set_ping_interval", libhv_HttpServer_ws_set_ping_interval); + py_bindmethod(type, "ws_send", libhv_HttpServer_ws_send); + py_bindmethod(type, "ws_recv", libhv_HttpServer_ws_recv); return type; -} \ No newline at end of file +} diff --git a/3rd/libhv/src/WebSocketClient.cpp b/3rd/libhv/src/WebSocketClient.cpp index 2d9e27dc..21763d55 100644 --- a/3rd/libhv/src/WebSocketClient.cpp +++ b/3rd/libhv/src/WebSocketClient.cpp @@ -1,7 +1,125 @@ +#include "HttpMessage.h" #include "libhv_bindings.hpp" +#include "pocketpy/pocketpy.h" #include "http/client/WebSocketClient.h" +struct libhv_WebSocketClient { + hv::WebSocketClient ws; + + libhv_MQ> mq; + + libhv_WebSocketClient() { + ws.onopen = [this]() { + mq.push({WsMessageType::onopen, ""}); + }; + ws.onclose = [this]() { + mq.push({WsMessageType::onclose, ""}); + }; + ws.onmessage = [this](const std::string& msg) { + mq.push({WsMessageType::onmessage, msg}); + }; + + // reconnect: 1,2,4,8,10,10,10... + reconn_setting_t reconn; + reconn_setting_init(&reconn); + reconn.min_delay = 1000; + reconn.max_delay = 10000; + reconn.delay_policy = 2; + ws.setReconnect(&reconn); + } +}; + py_Type libhv_register_WebSocketClient(py_GlobalRef mod) { - py_Type type = py_newtype("WebSocketClient", tp_object, mod, NULL); + py_Type type = py_newtype("WebSocketClient", tp_object, mod, [](void* ud) { + libhv_WebSocketClient* self = (libhv_WebSocketClient*)ud; + self->~libhv_WebSocketClient(); + }); + + py_bindmagic(type, __new__, [](int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_WebSocketClient* self = (libhv_WebSocketClient*) + py_newobject(py_retval(), py_totype(argv), 0, sizeof(libhv_WebSocketClient)); + new (self) libhv_WebSocketClient(); + return true; + }); + + py_bindmethod(type, "open", [](int argc, py_Ref argv) { + libhv_WebSocketClient* self = (libhv_WebSocketClient*)py_touserdata(argv); + PY_CHECK_ARG_TYPE(1, tp_str); + const char* url = py_tostr(py_arg(1)); + http_headers headers = DefaultHeaders; + if(argc == 2) { + // open(self, url) + } else if(argc == 3) { + // open(self, url, headers) + if(!py_checktype(py_arg(2), tp_dict)) return false; + bool ok = py_dict_apply( + py_arg(2), + [](py_Ref key, py_Ref value, void* ctx) { + http_headers* p_headers = (http_headers*)ctx; + if(!py_checkstr(key)) return false; + if(!py_checkstr(value)) return false; + p_headers->operator[](py_tostr(key)) = py_tostr(value); + return true; + }, + &headers); + if(!ok) return false; + } else { + return TypeError("open() takes 2 or 3 arguments"); + } + int code = self->ws.open(url, headers); + py_newint(py_retval(), code); + return true; + }); + + py_bindmethod(type, "close", [](int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_WebSocketClient* self = (libhv_WebSocketClient*)py_touserdata(argv); + int code = self->ws.close(); + py_newint(py_retval(), code); + return true; + }); + + py_bindmethod(type, "send", [](int argc, py_Ref argv) { + PY_CHECK_ARGC(2); + libhv_WebSocketClient* self = (libhv_WebSocketClient*)py_touserdata(argv); + PY_CHECK_ARG_TYPE(1, tp_str); + const char* msg = py_tostr(py_arg(1)); + int code = self->ws.send(msg); + py_newint(py_retval(), code); + return true; + }); + + py_bindmethod(type, "recv", [](int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + libhv_WebSocketClient* self = (libhv_WebSocketClient*)py_touserdata(py_arg(0)); + + std::pair mq_msg; + if(!self->mq.pop(&mq_msg)) { + py_newnone(py_retval()); + return true; + } else { + py_newtuple(py_retval(), 2); + switch(mq_msg.first) { + case WsMessageType::onopen: { + py_newstr(py_tuple_getitem(py_retval(), 0), "onopen"); + py_newnone(py_tuple_getitem(py_retval(), 1)); + break; + } + case WsMessageType::onclose: { + py_newstr(py_tuple_getitem(py_retval(), 0), "onclose"); + py_newnone(py_tuple_getitem(py_retval(), 1)); + break; + } + case WsMessageType::onmessage: { + py_newstr(py_tuple_getitem(py_retval(), 0), "onmessage"); + py_newstrv(py_tuple_getitem(py_retval(), 1), + {mq_msg.second.data(), (int)mq_msg.second.size()}); + break; + } + } + return true; + } + }); return type; -} \ No newline at end of file +} diff --git a/3rd/libhv/src/WebSocketServer.cpp b/3rd/libhv/src/WebSocketServer.cpp deleted file mode 100644 index 43dd677d..00000000 --- a/3rd/libhv/src/WebSocketServer.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "libhv_bindings.hpp" -#include "http/server/WebSocketServer.h" - -py_Type libhv_register_WebSocketServer(py_GlobalRef mod) { - py_Type type = py_newtype("WebSocketServer", tp_object, mod, NULL); - return type; -} diff --git a/3rd/libhv/src/libhv_bindings.cpp b/3rd/libhv/src/libhv_bindings.cpp index 6ade67a8..f54cb856 100644 --- a/3rd/libhv/src/libhv_bindings.cpp +++ b/3rd/libhv/src/libhv_bindings.cpp @@ -1,10 +1,20 @@ #include "libhv_bindings.hpp" +#include "base/herr.h" extern "C" void pk__add_module_libhv() { py_GlobalRef mod = py_newmodule("libhv"); + libhv_register_HttpRequest(mod); libhv_register_HttpClient(mod); libhv_register_HttpServer(mod); libhv_register_WebSocketClient(mod); - libhv_register_WebSocketServer(mod); + + py_bindfunc(mod, "strerror", [](int argc, py_Ref argv) { + PY_CHECK_ARGC(1); + PY_CHECK_ARG_TYPE(0, tp_int); + int code = py_toint(py_arg(0)); + const char* msg = hv_strerror(code); + py_newstr(py_retval(), msg); + return true; + }); } diff --git a/docs/modules/libhv.md b/docs/modules/libhv.md index d67b2912..b6eb28e5 100644 --- a/docs/modules/libhv.md +++ b/docs/modules/libhv.md @@ -7,11 +7,17 @@ label: libhv This module is optional. Set option `PK_BUILD_MODULE_LIBHV` to `ON` in your `CMakeLists.txt` to enable it. !!! +`libhv` is a git submodule located at `3rd/libhv/libhv`. If you cannot find it, please run the following command to initialize the submodule: + +```bash +git submodule update --init --recursive +``` + Simple bindings for [libhv](https://github.com/ithewei/libhv), which provides cross platform implementation of the following: + HTTP server + HTTP client -+ WebSocket server (TODO) -+ WebSocket client (TODO) ++ WebSocket server ++ WebSocket client #### Source code diff --git a/include/typings/libhv.pyi b/include/typings/libhv.pyi index f2ff2635..0ab1a2ef 100644 --- a/include/typings/libhv.pyi +++ b/include/typings/libhv.pyi @@ -1,7 +1,15 @@ from typing import Literal, Generator, Callable +WsMessageType = Literal['onopen', 'onclose', 'onmessage'] +WsChannelId = int +HttpStatusCode = int +HttpHeaders = dict[str, str] +ErrorCode = int + class Future[T]: + @property def completed(self) -> bool: ... + def cancel(self) -> None: ... def __iter__(self) -> Generator[T, None, None]: ... class HttpResponse(Future['HttpResponse']): @@ -18,21 +26,75 @@ class HttpResponse(Future['HttpResponse']): class HttpClient: - def get(self, url: str, params=None, headers=None, timeout=10) -> HttpResponse: ... - def post(self, url: str, params=None, headers=None, data=None, json=None, timeout=10) -> HttpResponse: ... - def put(self, url: str, params=None, headers=None, data=None, json=None, timeout=10) -> HttpResponse: ... - def delete(self, url: str, params=None, headers=None, timeout=10) -> HttpResponse: ... + def get(self, url: str, /, params=None, headers=None, timeout=10) -> HttpResponse: ... + def post(self, url: str, /, params=None, headers=None, data=None, json=None, timeout=10) -> HttpResponse: ... + def put(self, url: str, /, params=None, headers=None, data=None, json=None, timeout=10) -> HttpResponse: ... + def delete(self, url: str, /, params=None, headers=None, timeout=10) -> HttpResponse: ... +class HttpRequest: + @property + def method(self) -> Literal['GET', 'POST', 'PUT', 'DELETE']: ... + @property + def path(self) -> str: ... + @property + def url(self) -> str: ... + @property + def headers(self) -> HttpHeaders: ... + @property + def data(self) -> str | bytes: ... + class HttpServer: - def __init__(self, host: str, port: int) -> None: ... - def dispatch(self, fn: Callable[[dict], object | tuple[object, int]]) -> bool: ... - def start(self) -> None: ... - def stop(self) -> None: ... + def __init__(self, host: str, port: int, /) -> None: ... + def start(self) -> ErrorCode: ... + def stop(self) -> ErrorCode: ... + def dispatch[T](self, fn: Callable[ + [HttpRequest], + T | tuple[T, HttpStatusCode] | tuple[T, HttpStatusCode, HttpHeaders] + ], /) -> bool: + """Dispatch one HTTP request through `fn`. `fn` should return one of the following: + + object + + (object, status_code) + + (object, status_code, headers) + + Return `True` if dispatched, otherwise `False`. + """ + + def ws_set_ping_interval(self, milliseconds: int, /) -> None: + """Set WebSocket ping interval in milliseconds.""" + + def ws_send(self, channel: WsChannelId, data: str, /) -> ErrorCode: + """Send WebSocket message through `channel`.""" + + def ws_recv(self) -> tuple[ + WsMessageType, + tuple[WsChannelId, HttpRequest] | WsChannelId | tuple[WsChannelId, str] + ] | None: + """Receive one WebSocket message. + Return one of the following or `None` if nothing to receive. + + + `"onopen"`: (channel, request) + + `"onclose"`: channel + + `"onmessage"`: (channel, body) + """ class WebSocketClient: - pass + def open(self, url: str, headers=None, /) -> ErrorCode: ... + def close(self) -> ErrorCode: ... -class WebSocketServer: - pass \ No newline at end of file + def send(self, data: str, /) -> ErrorCode: + """Send WebSocket message.""" + + def recv(self) -> tuple[WsMessageType, str | None] | None: + """Receive one WebSocket message. + Return one of the following or `None` if nothing to receive. + + + `"onopen"`: `None` + + `"onclose"`: `None` + + `"onmessage"`: body + """ + + +def strerror(errno: ErrorCode, /) -> str: + """Get error message by errno via `hv_strerror`.""" diff --git a/src/public/internal.c b/src/public/internal.c index 538e8f91..3e74eccd 100644 --- a/src/public/internal.c +++ b/src/public/internal.c @@ -152,7 +152,7 @@ bool py_callcfunc(py_CFunction f, int argc, py_Ref argv) { } if(py_checkexc(true)) { const char* name = py_tpname(pk_current_vm->curr_exception.type); - c11__abort("py_CFunction returns `true`, but `%s` is set!", name); + c11__abort("py_CFunction returns `true`, but `%s` was set!", name); } return true; }