mkxp-z/binding/http-binding.cpp
Wayward Heart 99ad4fa636 Throw exceptions for Resets and Exits instead of directly raising.
While this does close small memory leaks, this is mostly for threading reasons. We're not supposed to call rb_raise with the gvl released, and calling rb_raise prevents GFX_UNLOCK from being called, which would cause problems for any games that want to call graphical operations in multiple threads should the user reset.

We're also now calling Graphics.__reset__ and Audio.__reset__ via eval instead of directly calling the functions, in case a game wants to hook them.
2024-08-02 09:26:51 -05:00

321 lines
8.8 KiB
C++

//
// http-binding.cpp
// mkxp-z
//
// Created by ゾロアーク on 12/29/20.
//
#include <stdio.h>
#include "util/json5pp.hpp"
#include "binding-util.h"
#include "net/net.h"
VALUE stringMap2hash(mkxp_net::StringMap &map) {
VALUE ret = rb_hash_new();
for (auto const &item : map) {
VALUE key = rb_utf8_str_new_cstr(item.first.c_str());
VALUE val = rb_utf8_str_new_cstr(item.second.c_str());
rb_hash_aset(ret, key, val);
}
return ret;
}
mkxp_net::StringMap hash2StringMap(VALUE hash) {
mkxp_net::StringMap ret;
Check_Type(hash, T_HASH);
VALUE keys = rb_funcall(hash, rb_intern("keys"), 0);
for (int i = 0; i < RARRAY_LEN(keys); i++) {
VALUE key = rb_ary_entry(keys, i);
VALUE val = rb_hash_aref(hash, key);
SafeStringValue(key);
SafeStringValue(val);
ret.emplace(RSTRING_PTR(key), RSTRING_PTR(val));
}
return ret;
}
bool strContainsStr(std::string &first, std::string second) {
return first.find(second) != std::string::npos;
}
VALUE getResponseBody(mkxp_net::HTTPResponse &res) {
#if RAPI_FULL >= 190
auto it = res.headers().find("Content-Type");
if (it == res.headers().end())
return rb_str_new(res.body().c_str(), res.body().length());
std::string &ctype = it->second;
if (strContainsStr(ctype, "text/plain") || strContainsStr(ctype, "application/json") ||
strContainsStr(ctype, "application/xml") || strContainsStr(ctype, "text/html") ||
strContainsStr(ctype, "text/css") || strContainsStr(ctype, "text/javascript") ||
strContainsStr(ctype, "application/x-sh") || strContainsStr(ctype, "image/svg+xml") ||
strContainsStr(ctype, "application/x-httpd-php"))
return rb_utf8_str_new(res.body().c_str(), res.body().length());
#endif
return rb_str_new(res.body().c_str(), res.body().length());
}
VALUE formResponse(mkxp_net::HTTPResponse &res) {
VALUE ret = rb_hash_new();
rb_hash_aset(ret, ID2SYM(rb_intern("status")), INT2NUM(res.status()));
rb_hash_aset(ret, ID2SYM(rb_intern("body")), getResponseBody(res));
rb_hash_aset(ret, ID2SYM(rb_intern("headers")), stringMap2hash(res.headers()));
return ret;
}
#if RAPI_MAJOR >= 2
void* httpGetInternal(void *req) {
VALUE ret;
mkxp_net::HTTPResponse res = ((mkxp_net::HTTPRequest*)req)->get();
ret = formResponse(res);
return (void*)ret;
}
#endif
RB_METHOD_GUARD(httpGet) {
RB_UNUSED_PARAM;
VALUE path, rheaders, redirect;
rb_scan_args(argc, argv, "12", &path, &rheaders, &redirect);
SafeStringValue(path);
bool rd;
rb_bool_arg(redirect, &rd);
mkxp_net::HTTPRequest req(RSTRING_PTR(path), rd);
if (rheaders != Qnil) {
auto headers = hash2StringMap(rheaders);
req.headers().insert(headers.begin(), headers.end());
}
#if RAPI_MAJOR >= 2
return (VALUE)drop_gvl_guard(httpGetInternal, &req, 0, 0);
#else
return (VALUE)httpGetInternal(&req);
#endif
}
RB_METHOD_GUARD_END
#if RAPI_MAJOR >= 2
typedef struct {
mkxp_net::HTTPRequest *req;
mkxp_net::StringMap *postData;
} httpPostInternalArgs;
void* httpPostInternal(void *args) {
VALUE ret;
mkxp_net::HTTPRequest *req = ((httpPostInternalArgs*)args)->req;
mkxp_net::StringMap *postData = ((httpPostInternalArgs*)args)->postData;
mkxp_net::HTTPResponse res = req->post(*postData);
ret = formResponse(res);
return (void*)ret;
}
#endif
RB_METHOD_GUARD(httpPost) {
RB_UNUSED_PARAM;
VALUE path, postDataHash, rheaders, redirect;
rb_scan_args(argc, argv, "22", &path, &postDataHash, &rheaders, &redirect);
SafeStringValue(path);
bool rd;
rb_bool_arg(redirect, &rd);
mkxp_net::HTTPRequest req(RSTRING_PTR(path), rd);
if (rheaders != Qnil) {
auto headers = hash2StringMap(rheaders);
req.headers().insert(headers.begin(), headers.end());
}
mkxp_net::StringMap postData = hash2StringMap(postDataHash);
httpPostInternalArgs args {&req, &postData};
#if RAPI_MAJOR >= 2
return (VALUE)drop_gvl_guard(httpPostInternal, &args, 0, 0);
#else
return httpPostInternal(&args);
#endif
}
RB_METHOD_GUARD_END
#if RAPI_MAJOR >= 2
typedef struct {
mkxp_net::HTTPRequest *req;
const char *body;
const char *ctype;
} httpPostBodyInternalArgs;
void* httpPostBodyInternal(void *args) {
VALUE ret;
mkxp_net::HTTPRequest *req = ((httpPostBodyInternalArgs*)args)->req;
const char *reqbody = ((httpPostBodyInternalArgs*)args)->body;
const char *reqctype = ((httpPostBodyInternalArgs*)args)->ctype;
mkxp_net::HTTPResponse res = req->post(reqbody, reqctype);
ret = formResponse(res);
return (void*)ret;
}
#endif
RB_METHOD_GUARD(httpPostBody) {
RB_UNUSED_PARAM;
VALUE path, body, ctype, rheaders;
rb_scan_args(argc, argv, "31", &path, &body, &ctype, &rheaders);
SafeStringValue(path);
SafeStringValue(body);
SafeStringValue(ctype);
mkxp_net::HTTPRequest req(RSTRING_PTR(path));
if (rheaders != Qnil) {
auto headers = hash2StringMap(rheaders);
req.headers().insert(headers.begin(), headers.end());
}
httpPostBodyInternalArgs args {&req, RSTRING_PTR(body), RSTRING_PTR(ctype)};
#if RAPI_MAJOR >= 2
return (VALUE)drop_gvl_guard(httpPostBodyInternal, &args, 0, 0);
#else
return httpPostBodyInternal(&args);
#endif
}
RB_METHOD_GUARD_END
VALUE json2rb(json5pp::value const &v) {
if (v.is_null())
return Qnil;
if (v.is_number())
return rb_float_new(v.as_number());
if (v.is_string())
return rb_utf8_str_new_cstr(v.as_string().c_str());
if (v.is_boolean())
return rb_bool_new(v.as_boolean());
if (v.is_integer())
return LL2NUM(v.as_integer());
if (v.is_array()) {
auto &a = v.as_array();
VALUE ret = rb_ary_new();
for (auto item : a) {
rb_ary_push(ret, json2rb(item));
}
return ret;
}
if (v.is_object()) {
auto &o = v.as_object();
VALUE ret = rb_hash_new();
for (auto const &pair : o) {
rb_hash_aset(ret, rb_utf8_str_new_cstr(pair.first.c_str()), json2rb(pair.second));
}
return ret;
}
// This should be unreachable
return Qnil;
}
json5pp::value rb2json(VALUE v) {
if (v == Qnil)
return json5pp::value(nullptr);
if (RB_TYPE_P(v, RUBY_T_FLOAT))
return json5pp::value(RFLOAT_VALUE(v));
if (RB_TYPE_P(v, RUBY_T_STRING))
return json5pp::value(RSTRING_PTR(v));
if (v == Qtrue || v == Qfalse)
return json5pp::value(RTEST(v));
if (RB_TYPE_P(v, RUBY_T_FIXNUM))
return json5pp::value(NUM2DBL(v));
if (RB_TYPE_P(v, RUBY_T_ARRAY)) {
json5pp::value ret_value = json5pp::array({});
auto &ret = ret_value.as_array();
for (int i = 0; i < RARRAY_LEN(v); i++) {
ret.push_back(rb2json(rb_ary_entry(v, i)));
}
return ret_value;
}
if (RTEST(rb_funcall(v, rb_intern("is_a?"), 1, rb_cHash))) {
json5pp::value ret_value = json5pp::object({});
auto &ret = ret_value.as_object();
VALUE keys = rb_funcall(v, rb_intern("keys"), 0);
for (int i = 0; i < RARRAY_LEN(keys); i++) {
VALUE key = rb_ary_entry(keys, i); SafeStringValue(key);
VALUE val = rb_hash_aref(v, key);
ret.emplace(RSTRING_PTR(key), rb2json(val));
}
return ret_value;
}
throw Exception(Exception::MKXPError, "Invalid value for JSON: %s", RSTRING_PTR(rb_inspect(v)));
// This should be unreachable
return json5pp::value(0);
}
RB_METHOD_GUARD(httpJsonParse) {
RB_UNUSED_PARAM;
VALUE jsonv;
rb_scan_args(argc, argv, "1", &jsonv);
SafeStringValue(jsonv);
json5pp::value v;
try {
v = json5pp::parse5(RSTRING_PTR(jsonv));
}
catch (const std::exception &e) {
throw Exception(Exception::MKXPError, "Failed to parse JSON: %s", e.what());
}
return json2rb(v);
}
RB_METHOD_GUARD_END
RB_METHOD_GUARD(httpJsonStringify) {
RB_UNUSED_PARAM;
VALUE obj;
rb_scan_args(argc, argv, "1", &obj);
json5pp::value v = rb2json(obj);
return rb_utf8_str_new_cstr(v.stringify5(json5pp::rule::space_indent<>()).c_str());
}
RB_METHOD_GUARD_END
void httpBindingInit() {
VALUE mNet = rb_define_module("HTTPLite");
_rb_define_module_function(mNet, "get", httpGet);
_rb_define_module_function(mNet, "post", httpPost);
_rb_define_module_function(mNet, "post_body", httpPostBody);
VALUE mNetJSON = rb_define_module_under(mNet, "JSON");
_rb_define_module_function(mNetJSON, "stringify", httpJsonStringify);
_rb_define_module_function(mNetJSON, "parse", httpJsonParse);
}