From f0de6533a032102b441b9af6ccf3f67e01dff3e6 Mon Sep 17 00:00:00 2001 From: Ted John Date: Sat, 17 Mar 2018 23:26:38 +0000 Subject: [PATCH] Add dukglue --- .../thirdparty/dukglue/detail_class_proto.h | 201 ++++++ .../thirdparty/dukglue/detail_constructor.h | 73 +++ .../thirdparty/dukglue/detail_function.h | 100 +++ .../thirdparty/dukglue/detail_method.h | 183 ++++++ .../dukglue/detail_primitive_types.h | 252 ++++++++ src/openrct2/thirdparty/dukglue/detail_refs.h | 199 ++++++ .../thirdparty/dukglue/detail_stack.h | 51 ++ .../thirdparty/dukglue/detail_traits.h | 122 ++++ .../thirdparty/dukglue/detail_typeinfo.h | 63 ++ .../thirdparty/dukglue/detail_types.h | 160 +++++ .../thirdparty/dukglue/dukexception.h | 40 ++ src/openrct2/thirdparty/dukglue/dukglue.h | 7 + src/openrct2/thirdparty/dukglue/dukvalue.h | 590 ++++++++++++++++++ src/openrct2/thirdparty/dukglue/public_util.h | 315 ++++++++++ .../thirdparty/dukglue/register_class.h | 202 ++++++ .../thirdparty/dukglue/register_function.h | 35 ++ .../thirdparty/dukglue/register_property.h | 123 ++++ 17 files changed, 2716 insertions(+) create mode 100644 src/openrct2/thirdparty/dukglue/detail_class_proto.h create mode 100644 src/openrct2/thirdparty/dukglue/detail_constructor.h create mode 100644 src/openrct2/thirdparty/dukglue/detail_function.h create mode 100644 src/openrct2/thirdparty/dukglue/detail_method.h create mode 100644 src/openrct2/thirdparty/dukglue/detail_primitive_types.h create mode 100644 src/openrct2/thirdparty/dukglue/detail_refs.h create mode 100644 src/openrct2/thirdparty/dukglue/detail_stack.h create mode 100644 src/openrct2/thirdparty/dukglue/detail_traits.h create mode 100644 src/openrct2/thirdparty/dukglue/detail_typeinfo.h create mode 100644 src/openrct2/thirdparty/dukglue/detail_types.h create mode 100644 src/openrct2/thirdparty/dukglue/dukexception.h create mode 100644 src/openrct2/thirdparty/dukglue/dukglue.h create mode 100644 src/openrct2/thirdparty/dukglue/dukvalue.h create mode 100644 src/openrct2/thirdparty/dukglue/public_util.h create mode 100644 src/openrct2/thirdparty/dukglue/register_class.h create mode 100644 src/openrct2/thirdparty/dukglue/register_function.h create mode 100644 src/openrct2/thirdparty/dukglue/register_property.h diff --git a/src/openrct2/thirdparty/dukglue/detail_class_proto.h b/src/openrct2/thirdparty/dukglue/detail_class_proto.h new file mode 100644 index 0000000000..7b008fccd1 --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_class_proto.h @@ -0,0 +1,201 @@ +#pragma once + +#include "detail_typeinfo.h" +#include + +namespace dukglue { + namespace detail { + + struct ProtoManager + { + public: + template + static void push_prototype(duk_context* ctx) + { + push_prototype(ctx, TypeInfo(typeid(Cls))); + } + + static void push_prototype(duk_context* ctx, const TypeInfo& check_info) + { + if (!find_and_push_prototype(ctx, check_info)) { + // nope, need to create our prototype object + duk_push_object(ctx); + + // add reference to this class' info object so we can do type checking + // when trying to pass this object into method calls + typedef dukglue::detail::TypeInfo TypeInfo; + TypeInfo* info = new TypeInfo(check_info); + + duk_push_pointer(ctx, info); + duk_put_prop_string(ctx, -2, "\xFF" "type_info"); + + // Clean up the TypeInfo object when this prototype is destroyed. + // We can't put a finalizer directly on this prototype, because it + // will be run whenever the wrapper for an object of this class is + // destroyed; instead, we make a dummy object and put the finalizer + // on that. + // If you're memory paranoid: this duplicates the type_info pointer + // once per registered class. If you don't care about freeing memory + // during shutdown, you can probably comment out this part. + duk_push_object(ctx); + duk_push_pointer(ctx, info); + duk_put_prop_string(ctx, -2, "\xFF" "type_info"); + duk_push_c_function(ctx, type_info_finalizer, 1); + duk_set_finalizer(ctx, -2); + duk_put_prop_string(ctx, -2, "\xFF" "type_info_finalizer"); + + // register it in the stash + register_prototype(ctx, info); + } + } + + template + static void make_script_object(duk_context* ctx, Cls* obj) + { + assert(obj != NULL); + + duk_push_object(ctx); + duk_push_pointer(ctx, obj); + duk_put_prop_string(ctx, -2, "\xFF" "obj_ptr"); + + // push the appropriate prototype +#ifdef DUKGLUE_INFER_BASE_CLASS + // In the "infer base class" case, we push the prototype + // corresponding to the compile-time class if no prototype + // for the run-time type has been defined. This allows us to + // skip calling dukglue_set_base_class() for every derived class, + // so long as we: + // (1) Always use the derived class as a pointer typed as the base class + // (2) Do not create a prototype for the derived class + // (i.e. do not register any functions on the derived class). + + // For big projects with hundreds of derived classes, this is preferrable + // to registering each type's base class individually. However, + // registering a native method on a derived class will cause the + // base class's methods to disappear until dukglue_set_base_class() is + // also called (because registering the native method causes a prototype + // to be created for the run-time type). This behavior may be unexpected, + // and for "small" projects it is reasonable to require + // dukglue_set_base_class() to be called, so it is opt-in via an ifdef. + + // does a prototype exist for the run-time type? if so, push it + if (!find_and_push_prototype(ctx, TypeInfo(typeid(*obj)))) { + // nope, find or create the prototype for the compile-time type + // and push that + push_prototype(ctx); + } +#else + // always use the prototype for the run-time type + push_prototype(ctx, TypeInfo(typeid(*obj))); +#endif + + duk_set_prototype(ctx, -2); + } + + private: + static duk_ret_t type_info_finalizer(duk_context* ctx) + { + duk_get_prop_string(ctx, 0, "\xFF" "type_info"); + dukglue::detail::TypeInfo* info = static_cast(duk_require_pointer(ctx, -1)); + delete info; + + // set pointer to NULL in case this finalizer runs again + duk_push_pointer(ctx, NULL); + duk_put_prop_string(ctx, 0, "\xFF" "type_info"); + + return 0; + } + + // puts heap_stash["dukglue_prototypes"] on the stack, + // or creates it if it doesn't exist + static void push_prototypes_array(duk_context* ctx) + { + static const char* DUKGLUE_PROTOTYPES = "dukglue_prototypes"; + + duk_push_heap_stash(ctx); + + // does the prototype array already exist? + if (!duk_has_prop_string(ctx, -1, DUKGLUE_PROTOTYPES)) { + // nope, we need to create it + duk_push_array(ctx); + duk_put_prop_string(ctx, -2, DUKGLUE_PROTOTYPES); + } + + duk_get_prop_string(ctx, -1, DUKGLUE_PROTOTYPES); + + // remove the heap stash from the stack + duk_remove(ctx, -2); + } + + // Stack: ... [proto] -> ... [proto] + static void register_prototype(duk_context* ctx, const TypeInfo* info) { + // 1. We assume info is not in the prototype array already + // 2. Duktape has no efficient "shift array indices" operation (at least publicly) + // 3. This method doesn't need to be fast, it's only called during registration + + // Work from high to low in the prototypes array, shifting as we go, + // until we find the spot for info. + + push_prototypes_array(ctx); + duk_size_t i = duk_get_length(ctx, -1); + while (i > 0) { + duk_get_prop_index(ctx, -1, i - 1); + + duk_get_prop_string(ctx, -1, "\xFF" "type_info"); + const TypeInfo* chk_info = static_cast(duk_require_pointer(ctx, -1)); + duk_pop(ctx); // pop type_info + + if (*chk_info > *info) { + duk_put_prop_index(ctx, -2, i); + i--; + } else { + duk_pop(ctx); // pop prototypes_array[i] + break; + } + } + + //std::cout << "Registering prototype for " << typeid(Cls).name() << " at " << i << std::endl; + + duk_dup(ctx, -2); // copy proto to top + duk_put_prop_index(ctx, -2, i); + duk_pop(ctx); // pop prototypes_array + } + + static bool find_and_push_prototype(duk_context* ctx, const TypeInfo& search_info) { + push_prototypes_array(ctx); + + // these are ints and not duk_size_t to deal with negative indices + int min = 0; + int max = duk_get_length(ctx, -1) - 1; + + while (min <= max) { + int mid = (max - min) / 2 + min; + + duk_get_prop_index(ctx, -1, mid); + + duk_get_prop_string(ctx, -1, "\xFF" "type_info"); + TypeInfo* mid_info = static_cast(duk_require_pointer(ctx, -1)); + duk_pop(ctx); // pop type_info pointer + + if (*mid_info == search_info) { + // found it + duk_remove(ctx, -2); // pop prototypes_array + return true; + } + else if (*mid_info < search_info) { + min = mid + 1; + } + else { + max = mid - 1; + } + + duk_pop(ctx); // pop prototypes_array[mid] + } + + duk_pop(ctx); // pop prototypes_array + return false; + } + + }; + } +} diff --git a/src/openrct2/thirdparty/dukglue/detail_constructor.h b/src/openrct2/thirdparty/dukglue/detail_constructor.h new file mode 100644 index 0000000000..de68e85d5e --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_constructor.h @@ -0,0 +1,73 @@ +#pragma once + +#include "detail_stack.h" +#include "detail_traits.h" + +namespace dukglue { + namespace detail { + + template + static duk_ret_t call_native_constructor(duk_context* ctx) + { + if (!duk_is_constructor_call(ctx)) { + duk_error(ctx, DUK_RET_TYPE_ERROR, "Constructor must be called with new T()."); + return DUK_RET_TYPE_ERROR; + } + + // construct the new instance + auto constructor_args = dukglue::detail::get_stack_values(ctx); + Cls* obj = dukglue::detail::apply_constructor(std::move(constructor_args)); + + duk_push_this(ctx); + + // make the new script object keep the pointer to the new object instance + duk_push_pointer(ctx, obj); + duk_put_prop_string(ctx, -2, "\xFF" "obj_ptr"); + + // register it + if (!managed) + dukglue::detail::RefManager::register_native_object(ctx, obj); + + duk_pop(ctx); // pop this + + return 0; + } + + template + static duk_ret_t managed_finalizer(duk_context* ctx) + { + duk_get_prop_string(ctx, 0, "\xFF" "obj_ptr"); + Cls* obj = (Cls*) duk_require_pointer(ctx, -1); + duk_pop(ctx); // pop obj_ptr + + if (obj != NULL) { + delete obj; + + // for safety, set the pointer to undefined + duk_push_undefined(ctx); + duk_put_prop_string(ctx, 0, "\xFF" "obj_ptr"); + } + + return 0; + } + + template + static duk_ret_t call_native_deleter(duk_context* ctx) + { + duk_push_this(ctx); + duk_get_prop_string(ctx, -1, "\xFF" "obj_ptr"); + + if (!duk_is_pointer(ctx, -1)) { + duk_error(ctx, DUK_RET_REFERENCE_ERROR, "Object has already been invalidated; cannot delete."); + return DUK_RET_REFERENCE_ERROR; + } + + Cls* obj = static_cast(duk_require_pointer(ctx, -1)); + dukglue_invalidate_object(ctx, obj); + delete obj; + + duk_pop_2(ctx); + return 0; + } + } +} diff --git a/src/openrct2/thirdparty/dukglue/detail_function.h b/src/openrct2/thirdparty/dukglue/detail_function.h new file mode 100644 index 0000000000..3d715884d3 --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_function.h @@ -0,0 +1,100 @@ +#pragma once + +#include "detail_stack.h" + +namespace dukglue +{ + namespace detail + { + // This struct can be used to generate a Duktape C function that + // pulls the argument values off the stack (with type checking), + // calls the appropriate function with them, and puts the function's + // return value (if any) onto the stack. + template + struct FuncInfoHolder + { + typedef RetType(*FuncType)(Ts...); + + template + struct FuncCompiletime + { + // The function to call is embedded into call_native_function at + // compile-time through template magic. + // Performance is so similar to run-time function calls that + // this is not recommended due to the ugly syntax it requires. + static duk_ret_t call_native_function(duk_context* ctx) + { + auto bakedArgs = dukglue::detail::get_stack_values(ctx); + actually_call(ctx, bakedArgs); + return std::is_void::value ? 0 : 1; + } + + private: + // this mess is to support functions with void return values + + template + static typename std::enable_if::value>::type actually_call(duk_context* ctx, const std::tuple& args) + { + // ArgStorage has some static_asserts in it that validate value types, + // so we typedef it to force ArgStorage to compile and run the asserts + typedef typename dukglue::types::ArgStorage::type ValidateReturnType; + + RetType return_val = dukglue::detail::apply_fp(funcToCall, args); + + using namespace dukglue::types; + DukType::type>::template push(ctx, std::move(return_val)); + } + + template + static typename std::enable_if::value>::type actually_call(duk_context* ctx, const std::tuple& args) + { + dukglue::detail::apply_fp(funcToCall, args); + } + }; + + struct FuncRuntime + { + // Pull the address of the function to call from the + // Duktape function object at run time. + static duk_ret_t call_native_function(duk_context* ctx) + { + duk_push_current_function(ctx); + duk_get_prop_string(ctx, -1, "\xFF" "func_ptr"); + void* fp_void = duk_require_pointer(ctx, -1); + if (fp_void == NULL) { + duk_error(ctx, DUK_RET_TYPE_ERROR, "what even"); + return DUK_RET_TYPE_ERROR; + } + + duk_pop_2(ctx); + + static_assert(sizeof(RetType(*)(Ts...)) == sizeof(void*), "Function pointer and data pointer are different sizes"); + RetType(*funcToCall)(Ts...) = reinterpret_cast(fp_void); + + actually_call(ctx, funcToCall, dukglue::detail::get_stack_values(ctx)); + return std::is_void::value ? 0 : 1; + } + + // this mess is to support functions with void return values + template + static typename std::enable_if::value>::type actually_call(duk_context* ctx, RetType(*funcToCall)(Ts...), const std::tuple& args) + { + // ArgStorage has some static_asserts in it that validate value types, + // so we typedef it to force ArgStorage to compile and run the asserts + typedef typename dukglue::types::ArgStorage::type ValidateReturnType; + + RetType return_val = dukglue::detail::apply_fp(funcToCall, args); + + using namespace dukglue::types; + DukType::type>::template push(ctx, std::move(return_val)); + } + + template + static typename std::enable_if::value>::type actually_call(duk_context* ctx, RetType(*funcToCall)(Ts...), const std::tuple& args) + { + dukglue::detail::apply_fp(funcToCall, args); + } + }; + }; + } +} diff --git a/src/openrct2/thirdparty/dukglue/detail_method.h b/src/openrct2/thirdparty/dukglue/detail_method.h new file mode 100644 index 0000000000..d3e929034d --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_method.h @@ -0,0 +1,183 @@ +#pragma once + +#include "detail_stack.h" + +namespace dukglue +{ + namespace detail + { + template + struct MethodInfo + { + typedef typename std::conditional::type MethodType; + + // The size of a method pointer is not guaranteed to be the same size as a function pointer. + // This means we can't just use duk_push_pointer(ctx, &MyClass::method) to store the method at run time. + // To get around this, we wrap the method pointer in a MethodHolder (on the heap), and push a pointer to + // that. The MethodHolder is cleaned up by the finalizer. + struct MethodHolder + { + MethodType method; + }; + + template + struct MethodCompiletime + { + static duk_ret_t call_native_method(duk_context* ctx) + { + // get this.obj_ptr + duk_push_this(ctx); + duk_get_prop_string(ctx, -1, "\xFF" "obj_ptr"); + void* obj_void = duk_require_pointer(ctx, -1); + if (obj_void == nullptr) { + duk_error(ctx, DUK_RET_REFERENCE_ERROR, "Native object missing."); + return DUK_RET_REFERENCE_ERROR; + } + + duk_pop_2(ctx); + + // (should always be valid unless someone is intentionally messing with this.obj_ptr...) + Cls* obj = static_cast(obj_void); + + // read arguments and call function + auto bakedArgs = dukglue::detail::get_stack_values(ctx); + actually_call(ctx, obj, bakedArgs); + return std::is_void::value ? 0 : 1; + } + + // this mess is to support functions with void return values + template + static typename std::enable_if::value>::type actually_call(duk_context* ctx, Cls* obj, const std::tuple& args) + { + // ArgStorage has some static_asserts in it that validate value types, + // so we typedef it to force ArgStorage to compile and run the asserts + typedef typename dukglue::types::ArgStorage::type ValidateReturnType; + + RetType return_val = dukglue::detail::apply_method(methodToCall, obj, args); + + using namespace dukglue::types; + DukType::type>::template push(ctx, std::move(return_val)); + } + + template + static typename std::enable_if::value>::type actually_call(duk_context* ctx, Cls* obj, const std::tuple& args) + { + dukglue::detail::apply_method(methodToCall, obj, args); + } + }; + + + struct MethodRuntime + { + static duk_ret_t finalize_method(duk_context* ctx) + { + // clean up the MethodHolder reference + duk_get_prop_string(ctx, 0, "\xFF" "method_holder"); + + void* method_holder_void = duk_require_pointer(ctx, -1); + MethodHolder* method_holder = static_cast(method_holder_void); + delete method_holder; + + return 0; + } + + static duk_ret_t call_native_method(duk_context* ctx) + { + // get this.obj_ptr + duk_push_this(ctx); + duk_get_prop_string(ctx, -1, "\xFF" "obj_ptr"); + void* obj_void = duk_get_pointer(ctx, -1); + if (obj_void == nullptr) { + duk_error(ctx, DUK_RET_REFERENCE_ERROR, "Invalid native object for 'this'"); + return DUK_RET_REFERENCE_ERROR; + } + + duk_pop_2(ctx); // pop this.obj_ptr and this + + // get current_function.method_info + duk_push_current_function(ctx); + duk_get_prop_string(ctx, -1, "\xFF" "method_holder"); + void* method_holder_void = duk_require_pointer(ctx, -1); + if (method_holder_void == nullptr) { + duk_error(ctx, DUK_RET_TYPE_ERROR, "Method pointer missing?!"); + return DUK_RET_TYPE_ERROR; + } + + duk_pop_2(ctx); + + // (should always be valid unless someone is intentionally messing with this.obj_ptr...) + Cls* obj = static_cast(obj_void); + MethodHolder* method_holder = static_cast(method_holder_void); + + // read arguments and call method + auto bakedArgs = dukglue::detail::get_stack_values(ctx); + actually_call(ctx, method_holder->method, obj, bakedArgs); + return std::is_void::value ? 0 : 1; + } + + // this mess is to support functions with void return values + template + static typename std::enable_if::value>::type actually_call(duk_context* ctx, MethodType method, Cls* obj, const std::tuple& args) + { + // ArgStorage has some static_asserts in it that validate value types, + // so we typedef it to force ArgStorage to compile and run the asserts + typedef typename dukglue::types::ArgStorage::type ValidateReturnType; + + RetType return_val = dukglue::detail::apply_method(method, obj, args); + + using namespace dukglue::types; + DukType::type>::template push(ctx, std::move(return_val)); + } + + template + static typename std::enable_if::value>::type actually_call(duk_context* ctx, MethodType method, Cls* obj, const std::tuple& args) + { + dukglue::detail::apply_method(method, obj, args); + } + }; + }; + + template + struct MethodVariadicRuntime + { + typedef MethodInfo MethodInfoVariadic; + typedef typename MethodInfoVariadic::MethodHolder MethodHolderVariadic; + + static duk_ret_t finalize_method(duk_context* ctx) + { + return MethodInfoVariadic::MethodRuntime::finalize_method(ctx); + } + + static duk_ret_t call_native_method(duk_context* ctx) + { + // get this.obj_ptr + duk_push_this(ctx); + duk_get_prop_string(ctx, -1, "\xFF" "obj_ptr"); + void* obj_void = duk_get_pointer(ctx, -1); + if (obj_void == nullptr) { + duk_error(ctx, DUK_RET_REFERENCE_ERROR, "Invalid native object for 'this'"); + return DUK_RET_REFERENCE_ERROR; + } + + duk_pop_2(ctx); // pop this.obj_ptr and this + + // get current_function.method_info + duk_push_current_function(ctx); + duk_get_prop_string(ctx, -1, "\xFF" "method_holder"); + void* method_holder_void = duk_require_pointer(ctx, -1); + if (method_holder_void == nullptr) { + duk_error(ctx, DUK_RET_TYPE_ERROR, "Method pointer missing?!"); + return DUK_RET_TYPE_ERROR; + } + + duk_pop_2(ctx); + + // (should always be valid unless someone is intentionally messing with this.obj_ptr...) + Cls* obj = static_cast(obj_void); + MethodHolderVariadic* method_holder = static_cast(method_holder_void); + + return (*obj.*method_holder->method)(ctx); + } + }; + } +} diff --git a/src/openrct2/thirdparty/dukglue/detail_primitive_types.h b/src/openrct2/thirdparty/dukglue/detail_primitive_types.h new file mode 100644 index 0000000000..ebba1ad80a --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_primitive_types.h @@ -0,0 +1,252 @@ +#pragma once + +#include "detail_types.h" +#include "detail_typeinfo.h" +#include "dukvalue.h" + +#include +#include +#include // for std::shared_ptr + +namespace dukglue { + namespace types { + +#define DUKGLUE_SIMPLE_VALUE_TYPE(TYPE, DUK_IS_FUNC, DUK_GET_FUNC, DUK_PUSH_FUNC, PUSH_VALUE) \ + template<> \ + struct DukType { \ + typedef std::true_type IsValueType; \ + \ + template \ + static TYPE read(duk_context* ctx, duk_idx_t arg_idx) { \ + if (DUK_IS_FUNC(ctx, arg_idx)) { \ + return static_cast(DUK_GET_FUNC(ctx, arg_idx)); \ + } else { \ + duk_int_t type_idx = duk_get_type(ctx, arg_idx); \ + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected " #TYPE ", got %s", arg_idx, detail::get_type_name(type_idx)); \ + } \ + } \ + \ + template \ + static void push(duk_context* ctx, TYPE value) { \ + DUK_PUSH_FUNC(ctx, PUSH_VALUE); \ + } \ + }; + + DUKGLUE_SIMPLE_VALUE_TYPE(bool, duk_is_boolean, 0 != duk_get_boolean, duk_push_boolean, value) + + DUKGLUE_SIMPLE_VALUE_TYPE(uint8_t, duk_is_number, duk_get_uint, duk_push_uint, value) + DUKGLUE_SIMPLE_VALUE_TYPE(uint16_t, duk_is_number, duk_get_uint, duk_push_uint, value) + DUKGLUE_SIMPLE_VALUE_TYPE(uint32_t, duk_is_number, duk_get_uint, duk_push_uint, value) + DUKGLUE_SIMPLE_VALUE_TYPE(uint64_t, duk_is_number, duk_get_number, duk_push_number, value) // have to cast to double + + DUKGLUE_SIMPLE_VALUE_TYPE(int8_t, duk_is_number, duk_get_int, duk_push_int, value) + DUKGLUE_SIMPLE_VALUE_TYPE(int16_t, duk_is_number, duk_get_int, duk_push_int, value) + DUKGLUE_SIMPLE_VALUE_TYPE(int32_t, duk_is_number, duk_get_int, duk_push_int, value) + DUKGLUE_SIMPLE_VALUE_TYPE(int64_t, duk_is_number, duk_get_number, duk_push_number, value) // have to cast to double + + // signed char and unsigned char are surprisingly *both* different from char, at least in MSVC + DUKGLUE_SIMPLE_VALUE_TYPE(char, duk_is_number, duk_get_int, duk_push_int, value) + + DUKGLUE_SIMPLE_VALUE_TYPE(float, duk_is_number, duk_get_number, duk_push_number, value) + DUKGLUE_SIMPLE_VALUE_TYPE(double, duk_is_number, duk_get_number, duk_push_number, value) + + DUKGLUE_SIMPLE_VALUE_TYPE(std::string, duk_is_string, duk_get_string, duk_push_string, value.c_str()) + + // We have to do some magic for const char* to work correctly. + // We override the "bare type" and "storage type" to both be const char*. + // char* is a bit tricky because its "bare type" should still be const char*, to differentiate it from just char + template<> + struct Bare { + typedef const char* type; + }; + template<> + struct Bare { + typedef const char* type; + }; + + // the storage type should also be const char* - if we don't do this, it will end up as just "char" + template<> + struct ArgStorage { + typedef const char* type; + }; + + template<> + struct DukType { + typedef std::true_type IsValueType; + + template + static const char* read(duk_context* ctx, duk_idx_t arg_idx) { + if (duk_is_string(ctx, arg_idx)) { + return duk_get_string(ctx, arg_idx); + } else { + duk_int_t type_idx = duk_get_type(ctx, arg_idx); + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected string, got %s", arg_idx, detail::get_type_name(type_idx)); + } + } + + template + static void push(duk_context* ctx, const char* value) { + duk_push_string(ctx, value); + } + }; + + // DukValue + template<> + struct DukType { + typedef std::true_type IsValueType; + + template + static DukValue read(duk_context* ctx, duk_idx_t arg_idx) { + try { + return DukValue::copy_from_stack(ctx, arg_idx); + } catch (DukException& e) { + // only DukException can be thrown by DukValue::copy_from_stack + duk_error(ctx, DUK_ERR_ERROR, e.what()); + } + } + + template + static void push(duk_context* ctx, const DukValue& value) { + if (value.context() == NULL) { + duk_error(ctx, DUK_ERR_ERROR, "DukValue is uninitialized"); + return; + } + + if (value.context() != ctx) { + duk_error(ctx, DUK_ERR_ERROR, "DukValue comes from a different context"); + return; + } + + try { + value.push(); + } catch (DukException& e) { + // only DukException can be thrown by DukValue::copy_from_stack + duk_error(ctx, DUK_ERR_ERROR, e.what()); + } + } + }; + + // std::vector (as value) + template + struct DukType< std::vector > { + typedef std::true_type IsValueType; + + template + static std::vector read(duk_context* ctx, duk_idx_t arg_idx) { + if (!duk_is_array(ctx, arg_idx)) { + duk_int_t type_idx = duk_get_type(ctx, arg_idx); + duk_error(ctx, DUK_ERR_TYPE_ERROR, "Argument %d: expected array, got %s", arg_idx, detail::get_type_name(type_idx)); + } + + duk_size_t len = duk_get_length(ctx, arg_idx); + const duk_idx_t elem_idx = duk_get_top(ctx); + + std::vector vec; + vec.reserve(len); + for (duk_size_t i = 0; i < len; i++) { + duk_get_prop_index(ctx, arg_idx, i); + vec.push_back(DukType< typename Bare::type >::template read(ctx, elem_idx)); + duk_pop(ctx); + } + return vec; + } + + template + static void push(duk_context* ctx, const std::vector& value) { + duk_idx_t obj_idx = duk_push_array(ctx); + + for (size_t i = 0; i < value.size(); i++) { + DukType< typename Bare::type >::template push(ctx, value[i]); + duk_put_prop_index(ctx, obj_idx, i); + } + } + }; + + // std::shared_ptr (as value) + template + struct DukType< std::shared_ptr > { + typedef std::true_type IsValueType; + + static_assert(std::is_same::IsValueType, std::false_type>::value, "Dukglue can only use std::shared_ptr to non-value types!"); + + template + static std::shared_ptr read(duk_context* ctx, duk_idx_t arg_idx) { + if (duk_is_null(ctx, arg_idx)) + return nullptr; + + if (!duk_is_object(ctx, arg_idx)) { + duk_int_t type_idx = duk_get_type(ctx, arg_idx); + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected shared_ptr object, got ", arg_idx, detail::get_type_name(type_idx)); + } + + duk_get_prop_string(ctx, arg_idx, "\xFF" "type_info"); + if (!duk_is_pointer(ctx, -1)) // missing type_info, must not be a native object + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected shared_ptr object (missing type_info)", arg_idx); + + // make sure this object can be safely returned as a T* + dukglue::detail::TypeInfo* info = static_cast(duk_get_pointer(ctx, -1)); + if (!info->can_cast()) + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: wrong type of shared_ptr object", arg_idx); + duk_pop(ctx); // pop type_info + + duk_get_prop_string(ctx, arg_idx, "\xFF" "shared_ptr"); + if (!duk_is_pointer(ctx, -1)) + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: not a shared_ptr object (missing shared_ptr)", arg_idx); + void* ptr = duk_get_pointer(ctx, -1); + duk_pop(ctx); // pop pointer to shared_ptr + + return *((std::shared_ptr*) ptr); + } + + static duk_ret_t shared_ptr_finalizer(duk_context* ctx) + { + duk_get_prop_string(ctx, 0, "\xFF" "shared_ptr"); + std::shared_ptr* ptr = (std::shared_ptr*) duk_require_pointer(ctx, -1); + duk_pop(ctx); // pop shared_ptr ptr + + if (ptr != NULL) { + delete ptr; + + // for safety, set the pointer to undefined + // (finalizers can run multiple times) + duk_push_undefined(ctx); + duk_put_prop_string(ctx, 0, "\xFF" "shared_ptr"); + } + + return 0; + } + + template + static void push(duk_context* ctx, const std::shared_ptr& value) { + dukglue::detail::ProtoManager::make_script_object(ctx, value.get()); + + // create + set shared_ptr + duk_push_pointer(ctx, new std::shared_ptr(value)); + duk_put_prop_string(ctx, -2, "\xFF" "shared_ptr"); + + // set shared_ptr finalizer + duk_push_c_function(ctx, &shared_ptr_finalizer, 1); + duk_set_finalizer(ctx, -2); + } + }; + + // std::function + /*template + struct DukType< std::function > { + typedef std::true_type IsValueType; + + template + static std::function read(duk_context* ctx, duk_idx_t arg_idx) { + DukValue callable = DukValue::copy_from_stack(ctx, -1, DUK_TYPE_MASK_OBJECT); + return [ctx, callable] (ArgTs... args) -> RetT { + dukglue_call(ctx, callable, args...); + }; + } + + template + static void push(duk_context* ctx, std::function value) { + static_assert(false, "Pushing an std::function has not been implemented yet. Sorry!"); + } + };*/ + } +} diff --git a/src/openrct2/thirdparty/dukglue/detail_refs.h b/src/openrct2/thirdparty/dukglue/detail_refs.h new file mode 100644 index 0000000000..67ad671639 --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_refs.h @@ -0,0 +1,199 @@ +#pragma once + +#include + +#include + +namespace dukglue +{ + namespace detail + { + // This class handles keeping a map of void* -> script object. + // It also prevents script objects from being GC'd until someone + // explicitly frees the underlying native object. + + // Implemented by keeping an array of script objects in the heap stash. + // An std::unordered_map maps pointer -> array index. + // Thanks to std::unordered_map, lookup time is O(1) on average. + + // Using std::unordered_map has some memory overhead (~32 bytes per object), + // which could be removed by using a different data structure: + + // 1. Use no data structure. Blindly scan through the reference registry, + // checking \xFFobj_ptr on every object until you find yours. + // Performance when returning native objects from functions when a lot + // of native objects are registered will suffer. + + // 2. Implement a self-balancing binary tree on top of a Duktape array + // for the registry. Still fast - O(log(N)) - and no memory overhead. + + // 3. A sorted list would work too, though insertion speed might be worse + // than a binary tree. + + struct RefManager + { + public: + + // Find the script object corresponding to obj_ptr and push it. + // Returns true if successful, false if obj_ptr has not been registered. + // Stack: ... -> ... (if object has been registered before) + // ... -> ... [object] (if object has not been registered) + static bool find_and_push_native_object(duk_context* ctx, void* obj_ptr) + { + RefMap* ref_map = get_ref_map(ctx); + + const auto it = ref_map->find(obj_ptr); + + if (it == ref_map->end()) { + return false; + } else { + push_ref_array(ctx); + duk_get_prop_index(ctx, -1, it->second); + duk_remove(ctx, -2); + return true; + } + } + + // Takes a script object and adds it to the registry, associating + // it with obj_ptr. unregistered_object is not modified. + // If obj_ptr has already been registered with another object, + // the old registry entry will be overidden. + // Does nothing if obj_ptr is NULL. + // Stack: ... [object] -> ... [object] + static void register_native_object(duk_context* ctx, void* obj_ptr) + { + if (obj_ptr == NULL) + return; + + RefMap* ref_map = get_ref_map(ctx); + + push_ref_array(ctx); + + // find next free index + // free indices are kept in a linked list, starting at ref_array[0] + duk_get_prop_index(ctx, -1, 0); + duk_uarridx_t next_free_idx = duk_get_uint(ctx, -1); + duk_pop(ctx); + + if (next_free_idx == 0) { + // no free spots in the array, make a new one at arr.length + next_free_idx = duk_get_length(ctx, -1); + } else { + // free spot found, need to remove it from the free list + // ref_array[0] = ref_array[next_free_idx] + duk_get_prop_index(ctx, -1, next_free_idx); + duk_put_prop_index(ctx, -2, 0); + } + + // std::cout << "putting reference at ref_array[" << next_free_idx << "]" << std::endl; + (*ref_map)[obj_ptr] = next_free_idx; + + duk_dup(ctx, -2); // put object on top + + // ... [object] [ref_array] [object] + duk_put_prop_index(ctx, -2, next_free_idx); + + duk_pop(ctx); // pop ref_array + } + + // Remove the object associated with obj_ptr from the registry + // and invalidate the object's internal native pointer (by setting it to undefined). + // Does nothing if obj_ptr if object was never registered or obj_ptr is NULL. + // Does not affect the stack. + static void find_and_invalidate_native_object(duk_context* ctx, void* obj_ptr) + { + if (obj_ptr == NULL) + return; + + RefMap* ref_map = get_ref_map(ctx); + auto it = ref_map->find(obj_ptr); + if (it == ref_map->end()) // was never registered + return; + + push_ref_array(ctx); + duk_get_prop_index(ctx, -1, it->second); + + // invalidate internal pointer + duk_push_undefined(ctx); + duk_put_prop_string(ctx, -2, "\xFF" "obj_ptr"); + duk_pop(ctx); // pop object + + // remove from references array and add the space it was in to free list + // (refs[0] -> tail) -> (refs[0] -> old_obj_idx -> tail) + + // refs[old_obj_idx] = refs[0] + duk_get_prop_index(ctx, -1, 0); + duk_put_prop_index(ctx, -2, it->second); + + // refs[0] = old_obj_idx + duk_push_uint(ctx, it->second); + duk_put_prop_index(ctx, -2, 0); + + duk_pop(ctx); // pop ref_array + + // also remove from map + // std::cout << "Freeing ref_array[" << it->second << "]" << std::endl; + ref_map->erase(it); + } + + private: + typedef std::unordered_map RefMap; + + static RefMap* get_ref_map(duk_context* ctx) + { + static const char* DUKGLUE_REF_MAP = "dukglue_ref_map"; + static const char* PTR = "ptr"; + + duk_push_heap_stash(ctx); + + if (!duk_has_prop_string(ctx, -1, DUKGLUE_REF_MAP)) { + // doesn't exist yet, need to create it + duk_push_object(ctx); + + duk_push_pointer(ctx, new RefMap()); + duk_put_prop_string(ctx, -2, PTR); + + duk_push_c_function(ctx, ref_map_finalizer, 1); + duk_set_finalizer(ctx, -2); + + duk_put_prop_string(ctx, -2, DUKGLUE_REF_MAP); + } + + duk_get_prop_string(ctx, -1, DUKGLUE_REF_MAP); + duk_get_prop_string(ctx, -1, PTR); + RefMap* map = static_cast(duk_require_pointer(ctx, -1)); + duk_pop_3(ctx); + + return map; + } + + static duk_ret_t ref_map_finalizer(duk_context* ctx) + { + duk_get_prop_string(ctx, 0, "ptr"); + RefMap* map = static_cast(duk_require_pointer(ctx, -1)); + delete map; + + return 0; + } + + static void push_ref_array(duk_context* ctx) + { + static const char* DUKGLUE_REF_ARRAY = "dukglue_ref_array"; + duk_push_heap_stash(ctx); + + if (!duk_has_prop_string(ctx, -1, DUKGLUE_REF_ARRAY)) { + duk_push_array(ctx); + + // ref_array[0] = 0 (initialize free list as empty) + duk_push_int(ctx, 0); + duk_put_prop_index(ctx, -2, 0); + + duk_put_prop_string(ctx, -2, DUKGLUE_REF_ARRAY); + } + + duk_get_prop_string(ctx, -1, DUKGLUE_REF_ARRAY); + duk_remove(ctx, -2); // pop heap stash + } + }; + } +} diff --git a/src/openrct2/thirdparty/dukglue/detail_stack.h b/src/openrct2/thirdparty/dukglue/detail_stack.h new file mode 100644 index 0000000000..f5382ab898 --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_stack.h @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include "detail_traits.h" +#include "detail_types.h" + +#include + +namespace dukglue +{ + namespace detail + { + // Helper to get the argument tuple type, with correct storage types. + template + struct ArgsTuple { + typedef std::tuple::type...> type; + }; + + // Helper to get argument indices. + // Call read for every Ts[i], for matching argument index Index[i]. + // The traits::index_tuple is used for type inference. + // A concrete example: + // get_values(duktape_context) + // get_values_helper<{int, bool}, {0, 1}>(ctx, ignored) + // std::make_tuple(read(ctx, 0), read(ctx, 1)) + template + typename ArgsTuple::type get_stack_values_helper(duk_context* ctx, dukglue::detail::index_tuple) + { + using namespace dukglue::types; + return std::forward_as_tuple(DukType::type>::template read::type>(ctx, Indexes)...); + } + + // Returns an std::tuple of the values asked for in the template parameters. + // Values will remain on the stack. + // Values are indexed from the bottom of the stack up (0, 1, ...). + // If a value does not exist or does not have the expected type, an error is thrown + // through Duktape (with duk_error(...)), and the function does not return + template + typename ArgsTuple::type get_stack_values(duk_context* ctx) + { + // We need the argument indices for read_value, and we need to be able + // to unpack them as a template argument to match Ts. + // So, we use traits::make_indexes, which returns a traits::index_tuple<0, 1, 2, ...> object. + // We pass that into a helper function so we can put a name to that <0, 1, ...> template argument. + // Here, the type of Args isn't important, the length of it is. + auto indices = typename dukglue::detail::make_indexes::type(); + return get_stack_values_helper(ctx, indices); + } + } +} diff --git a/src/openrct2/thirdparty/dukglue/detail_traits.h b/src/openrct2/thirdparty/dukglue/detail_traits.h new file mode 100644 index 0000000000..247e2e52bf --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_traits.h @@ -0,0 +1,122 @@ +#pragma once + +#include + +namespace dukglue +{ + namespace detail + { + ////////////////////////////////////////////////////////////////////////////////////////////// + + // Credit to LuaState for this code: + // https://github.com/AdUki/LuaState/blob/master/include/Traits.h + + template struct index_tuple {}; + + template + struct make_indexes_impl; + + template + struct make_indexes_impl, T, Types...> + { + typedef typename make_indexes_impl, Types...>::type type; + }; + + template + struct make_indexes_impl > + { + typedef index_tuple type; + }; + + template + struct make_indexes : make_indexes_impl<0, index_tuple<>, Types...> + {}; + + ////////////////////////////////////////////////////////////////////////////////////////////// + + template + struct indexes {}; + + template + struct indexes_builder : indexes_builder {}; + + template + struct indexes_builder<0, Is...> { + typedef indexes index; + }; + + ////////////////////////////////////////////////////////////////////////////////////////////// + + // This mess is used to use function arugments stored in an std::tuple to an + // std::function, function pointer, or method. + + // std::function + template + Ret apply_helper(std::function pf, index_tuple< Indexes... >, std::tuple&& tup) + { + return pf(std::forward(std::get(tup))...); + } + + template + Ret apply(std::function pf, const std::tuple& tup) + { + return apply_helper(pf, typename make_indexes::type(), std::tuple(tup)); + } + + // function pointer + template + Ret apply_fp_helper(Ret(*pf)(Args...), index_tuple< Indexes... >, std::tuple&& tup) + { + return pf(std::forward(std::get(tup))...); + } + + template + Ret apply_fp(Ret(*pf)(Args...), const std::tuple& tup) + { + return apply_fp_helper(pf, typename make_indexes::type(), std::tuple(tup)); + } + + // method pointer + template + Ret apply_method_helper(Ret(Cls::*pf)(Args...), index_tuple< Indexes... >, Cls* obj, std::tuple&& tup) + { + return (*obj.*pf)(std::forward(std::get(tup))...); + } + + template + Ret apply_method(Ret(Cls::*pf)(Args...), Cls* obj, const std::tuple& tup) + { + return apply_method_helper(pf, typename make_indexes::type(), obj, std::tuple(tup)); + } + + // const method pointer + template + Ret apply_method_helper(Ret(Cls::*pf)(Args...) const, index_tuple< Indexes... >, Cls* obj, std::tuple&& tup) + { + return (*obj.*pf)(std::forward(std::get(tup))...); + } + + template + Ret apply_method(Ret(Cls::*pf)(Args...) const, Cls* obj, const std::tuple& tup) + { + return apply_method_helper(pf, typename make_indexes::type(), obj, std::tuple(tup)); + } + + // constructor + template + Cls* apply_constructor_helper(index_tuple< Indexes... >, std::tuple&& tup) + { + return new Cls(std::forward(std::get(tup))...); + } + + template + Cls* apply_constructor(const std::tuple& tup) + { + return apply_constructor_helper(typename make_indexes::type(), std::tuple(tup)); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + + + } +} \ No newline at end of file diff --git a/src/openrct2/thirdparty/dukglue/detail_typeinfo.h b/src/openrct2/thirdparty/dukglue/detail_typeinfo.h new file mode 100644 index 0000000000..5626c64c5a --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_typeinfo.h @@ -0,0 +1,63 @@ +#pragma once + +#include + +namespace dukglue +{ + namespace detail + { + // same as duk_get_type_name, which is private for some reason *shakes fist* + static const char* get_type_name(duk_int_t type_idx) { + static const char* names[] = { + "none", + "undefined", + "null", + "boolean", + "number", + "string", + "object", + "buffer", + "pointer", + "lightfunc" + }; + + if (type_idx >= 0 && type_idx < sizeof(names) / sizeof(names[0])) + return names[type_idx]; + else + return "unknown"; + } + + class TypeInfo + { + public: + TypeInfo(std::type_index&& idx) : index_(idx), base_(nullptr) {} + TypeInfo(const TypeInfo& rhs) : index_(rhs.index_), base_(rhs.base_) {} + + inline void set_base(TypeInfo* base) { + base_ = base; + } + + template + bool can_cast() const { + if (index_ == typeid(T)) + return true; + + if (base_) + return base_->can_cast(); + + return false; + } + + inline bool operator<(const TypeInfo& rhs) const { return index_ < rhs.index_; } + inline bool operator<=(const TypeInfo& rhs) const { return index_ <= rhs.index_; } + inline bool operator>(const TypeInfo& rhs) const { return index_ > rhs.index_; } + inline bool operator>=(const TypeInfo& rhs) const { return index_ >= rhs.index_; } + inline bool operator==(const TypeInfo& rhs) const { return index_ == rhs.index_; } + inline bool operator!=(const TypeInfo& rhs) const { return index_ != rhs.index_; } + + private: + std::type_index index_; + TypeInfo* base_; + }; + } +} diff --git a/src/openrct2/thirdparty/dukglue/detail_types.h b/src/openrct2/thirdparty/dukglue/detail_types.h new file mode 100644 index 0000000000..de86afe414 --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/detail_types.h @@ -0,0 +1,160 @@ +#pragma once + +#include + +#include "detail_refs.h" +#include "detail_typeinfo.h" +#include "detail_class_proto.h" + +// TODO try adding a using namespace std in here if I can scope it to just this file + +namespace dukglue { + namespace types { + + // Bare::type is T stripped of reference, pointer, and const off a type, like so: + // Bare::type = Dog + // Bare::type = Dog + // Bare::type = Dog + // Bare::type = Dog + // Bare::type = Dog + // Bare::type = Dog + template + struct Bare { + typedef typename std::remove_const::type>::type>::type type; + }; + + // DukType provides functions for reading and writing T from the Duktape stack. + // T is always a "bare type," i.e. "Dog" rather than "Dog*". + + // There are two kinds of DukTypes: + // 1. "Native" DukTypes. This is the default. + // These types use an underlying native object allocated on the heap. + // A pointer to the object (of type T*) is expected at script_object.\xFFobj_ptr. + // "Native" DukTypes can return a value (returns a copy-constructed T from the native object), + // a pointer (just returns script_object.\xFFobj_ptr), or a reference (dereferences script_object.\xFFobj_ptr if it is not null). + + // 2. "Value" DukTypes. These are implemented through template specialization. + // This is how primitive types are implemented (int, float, const char*). + // These types can only be returned by value (T) or by const reference (const T&). + // Attempting to read a pointer (T*) or non-const reference (T&) will give a compile-time error. + // You can also use this to implement your own lightweight types, such as a 3D vector. + // (Strictly speaking, non-const references (T&) *could* be returned, but any changes to the reference would + // be discarded. So, I wrote a static assert to disable the option. If you understand the implications, + // you should be able to safely comment out the static_assert in ArgStorage.) + template + struct DukType { + static_assert(std::is_same::type >::value, "Invalid base type, expected bare type"); + + typedef std::false_type IsValueType; + + // read pointer + template::value>::type > + static T* read(duk_context* ctx, duk_idx_t arg_idx) { + using namespace dukglue::detail; + + if (duk_is_null(ctx, arg_idx)) + return nullptr; + + if (!duk_is_object(ctx, arg_idx)) { + duk_int_t type_idx = duk_get_type(ctx, arg_idx); + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected native object, got %s", arg_idx, get_type_name(type_idx)); + } + + duk_get_prop_string(ctx, arg_idx, "\xFF" "type_info"); + if (!duk_is_pointer(ctx, -1)) // missing type_info, must not be a native object + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected native object (missing type_info)", arg_idx); + + // make sure this object can be safely returned as a T* + TypeInfo* info = static_cast(duk_get_pointer(ctx, -1)); + if (!info->can_cast()) + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: wrong type of native object", arg_idx); + + duk_pop(ctx); // pop type_info + + duk_get_prop_string(ctx, arg_idx, "\xFF" "obj_ptr"); + if (!duk_is_pointer(ctx, -1)) + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: invalid native object.", arg_idx); + + T* obj = static_cast(duk_get_pointer(ctx, -1)); + + duk_pop(ctx); // pop obj_ptr + + return obj; + } + + // read reference + template::value>::type > + static T& read(duk_context* ctx, duk_idx_t arg_idx) { + T* obj = read(ctx, arg_idx); + if (obj == nullptr) + duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: cannot be null (native function expects reference)", arg_idx); + + return *obj; + } + + // read value + // commented out because it breaks for abstract classes + /*template::type >::value>::type > + static T read(duk_context* ctx, duk_idx_t arg_idx) { + static_assert(std::is_copy_constructible::value, "Reading a value requires a copy-constructable type"); + const T& obj = read(ctx, arg_idx); + return T(obj); + }*/ + + // ----------------------------------------------------- + // Writing + + // Reference + template::value>::type > + static void push(duk_context* ctx, T& value) { + using namespace dukglue::detail; + + if (!RefManager::find_and_push_native_object(ctx, &value)) { + // need to create new script object + ProtoManager::make_script_object(ctx, &value); + RefManager::register_native_object(ctx, &value); + } + } + + // Pointer + template::value>::type > + static void push(duk_context* ctx, T* value) { + if (value == nullptr) + duk_push_null(ctx); + else + push(ctx, *value); + } + + // Value (create new instance on the heap) + // commented out because this is an easy way to accidentally cause a memory leak + /*template::type >::value>::type > + static void push(duk_context* ctx, T value) { + static_assert(std::is_copy_constructible::value, "Cannot push value for non-copy-constructable type."); + return push(ctx, new T(value)); + }*/ + }; + + // Figure out what the type for an argument should be inside the tuple. + // If a function expects a reference to a value type, we need temporary storage for the value. + // For example, a reference to a value type (const int&) will need to be temporarily + // stored in the tuple, so ArgStorage::type == int. + // Native objects are already allocated on the heap, so there's no problem storing, say, const Dog& in the tuple. + template + struct ArgStorage { + private: + typedef typename Bare::type BareType; + //typedef DukType ThisDukType; + typedef typename DukType::IsValueType IsValueType; + + static_assert(!IsValueType::value || !std::is_pointer::value, "Cannot return pointer to value type."); + static_assert(!IsValueType::value || + (!std::is_reference::value || std::is_const::type>::value), + "Value types can only be returned as const references."); + + public: + typedef typename std::conditional::type type; + }; + } +} + +#include "detail_primitive_types.h" diff --git a/src/openrct2/thirdparty/dukglue/dukexception.h b/src/openrct2/thirdparty/dukglue/dukexception.h new file mode 100644 index 0000000000..7d42768ebe --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/dukexception.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +class DukException : public std::exception +{ +public: + virtual const char* what() const noexcept override + { + return mMsg.c_str(); + } + + template + DukException& operator<<(T rhs) + { + std::stringstream ss; + ss << mMsg << rhs; + mMsg = ss.str(); + return *this; + } + +protected: + std::string mMsg; +}; + +class DukErrorException : public DukException +{ +public: + DukErrorException(duk_context* ctx, int return_code, bool pop_error = true) { + if (return_code != 0) { + duk_get_prop_string(ctx, -1, "stack"); + mMsg = duk_safe_to_string(ctx, -1); + duk_pop(ctx); + + if (pop_error) + duk_pop(ctx); + } + } +}; \ No newline at end of file diff --git a/src/openrct2/thirdparty/dukglue/dukglue.h b/src/openrct2/thirdparty/dukglue/dukglue.h new file mode 100644 index 0000000000..1b62f7b9d4 --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/dukglue.h @@ -0,0 +1,7 @@ +#pragma once + +#include "register_function.h" +#include "register_class.h" +#include "register_property.h" +#include "public_util.h" +#include "dukvalue.h" \ No newline at end of file diff --git a/src/openrct2/thirdparty/dukglue/dukvalue.h b/src/openrct2/thirdparty/dukglue/dukvalue.h new file mode 100644 index 0000000000..67d17da9b5 --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/dukvalue.h @@ -0,0 +1,590 @@ +#pragma once + +#include +#include +#include +#include + +#include "dukexception.h" + +// A variant class for Duktape values. +// This class is not really dependant on the rest of dukglue, but the rest of dukglue is integrated to support it. +// Script objects are persisted by copying a reference to the object into an array in the heap stash. +// When we need to push a reference to the object, we just look up that reference in the stash. + +// DukValues can be copied freely. We use reference counting behind the scenes to keep track of when we need +// to remove our reference from the heap stash. Memory for reference counting is only allocated once a DukValue +// is copied (either by copy constructor or operator=). std::move can be used if you are trying to avoid ref counting +// for some reason. + +// One script object can have multiple, completely separate DukValues "pointing" to it - in this case, there will be +// multiple entries in the "ref array" that point to the same object. This will happen if the same script object is +// put on the stack and turned into a DukValue multiple times independently (copy-constructing/operator=-ing +// DukValues will not do this!). This is okay, as we are only keeping track of these objects to prevent garbage +// collection (and access them later). This could be changed to use a map structure to look up one canonical entry per +// script object in the "ref array" (I guess it would be more like a ref map in this case), but this would require a map +// lookup every time we construct a DukValue. The performance difference probably isn't *that* noticeable (a good map +// would probably be amortized constant-time lookup), but I am guessing constructing many separate DukValues that point +// to the same script object isn't a very common thing. +class DukValue { +public: + enum Type : uint8_t { + //NONE = DUK_TYPE_NONE, + UNDEFINED = DUK_TYPE_UNDEFINED, + NULLREF = DUK_TYPE_NULL, + BOOLEAN = DUK_TYPE_BOOLEAN, + NUMBER = DUK_TYPE_NUMBER, + STRING = DUK_TYPE_STRING, + OBJECT = DUK_TYPE_OBJECT, + BUFFER = DUK_TYPE_BUFFER, // not implemented + POINTER = DUK_TYPE_POINTER, + LIGHTFUNC = DUK_TYPE_LIGHTFUNC // not implemented + }; + + // default constructor just makes an undefined-type DukValue + inline DukValue() : mContext(NULL), mType(UNDEFINED), mRefCount(NULL) {} + + virtual ~DukValue() { + // release any references we have + release_ref_count(); + } + + // move constructor + inline DukValue(DukValue&& move) { + mContext = move.mContext; + mType = move.mType; + mPOD = move.mPOD; + mRefCount = move.mRefCount; + + if (mType == STRING) + mString = std::move(move.mString); + + move.mType = UNDEFINED; + move.mRefCount = NULL; + } + + inline DukValue& operator=(const DukValue& rhs) { + // free whatever we had + release_ref_count(); + + // copy things + mContext = rhs.mContext; + mType = rhs.mType; + mPOD = rhs.mPOD; + + if (mType == STRING) + mString = rhs.mString; + + if (mType == OBJECT) + { + // ref counting increment + if (rhs.mRefCount == NULL) { + // not ref counted before, need to allocate memory + const_cast(rhs).mRefCount = new int(2); + mRefCount = rhs.mRefCount; + } else { + // already refcounting, just increment + mRefCount = rhs.mRefCount; + *mRefCount = *mRefCount + 1; + } + } + + return *this; + } + + // copy constructor + inline DukValue(const DukValue& copy) : DukValue() { + *this = copy; + } + + // equality operator + inline bool operator==(const DukValue& rhs) const + { + if (mType != rhs.mType || mContext != rhs.mContext) + return false; + + switch (mType) { + case UNDEFINED: + case NULLREF: + return true; + case BOOLEAN: + return mPOD.boolean == rhs.mPOD.boolean; + case NUMBER: + return mPOD.number == rhs.mPOD.number; + case STRING: + return mString == rhs.mString; + + case OBJECT: + { + // this could be optimized to only push ref_array once... + this->push(); + rhs.push(); + bool equal = duk_equals(mContext, -1, -2) ? true : false; + duk_pop_2(mContext); + return equal; + } + + case POINTER: + return mPOD.pointer == rhs.mPOD.pointer; + + case BUFFER: + case LIGHTFUNC: + default: + throw DukException() << "operator== not implemented (" << type_name() << ")"; + } + } + + inline bool operator!=(const DukValue& rhs) const { + return !(*this == rhs); + } + + // copies the object at idx on the stack into a new DukValue and returns it + static DukValue copy_from_stack(duk_context* ctx, duk_idx_t idx = -1) { + DukValue value; + value.mContext = ctx; + value.mType = (Type) duk_get_type(ctx, idx); + switch (value.mType) { + case UNDEFINED: + break; + + case NULLREF: + value.mPOD.pointer = NULL; + break; + + case BOOLEAN: + value.mPOD.boolean = duk_require_boolean(ctx, idx) ? true : false; + break; + + case NUMBER: + value.mPOD.number = duk_require_number(ctx, idx); + break; + + case STRING: + { + duk_size_t len; + const char* data = duk_get_lstring(ctx, idx, &len); + value.mString.assign(data, len); + break; + } + + case OBJECT: + value.mPOD.ref_array_idx = stash_ref(ctx, idx); + break; + + case POINTER: + value.mPOD.pointer = duk_require_pointer(ctx, idx); + break; + + case BUFFER: + case LIGHTFUNC: + default: + throw DukException() << "Cannot turn type into DukValue (" << value.type_name() << ")"; + } + + return value; + } + +protected: + static duk_ret_t json_decode_safe(duk_context* ctx, void* user_data) + { + duk_json_decode(ctx, -1); + return 1; + } + +public: + static_assert(sizeof(char) == 1, "Serialization probably broke"); + static DukValue deserialize(duk_context* ctx, const char* data, size_t data_len) { + DukValue v; + v.mContext = ctx; + v.mType = *((Type*)data); + + const char* data_ptr = data + sizeof(Type); + data_len -= sizeof(Type); + + switch (v.mType) { + case UNDEFINED: + case NULLREF: + break; + + case BOOLEAN: + { + if (data_len < 1) + throw DukException() << "Malformed boolean data"; + + v.mPOD.boolean = data[1] == 1 ? true : false; + break; + } + + case NUMBER: + { + if (data_len < sizeof(double)) + throw DukException() << "Malformed number data"; + + v.mPOD.number = *((double*)data_ptr); + break; + } + + case STRING: + { + if (data_len < sizeof(uint32_t)) + throw DukException() << "Malformed string data (no length)"; + uint32_t str_len = *((uint32_t*)data_ptr); + + if (data_len < sizeof(uint32_t) + str_len) + throw DukException() << "Malformed string data (appears truncated)"; + + const char* str_data = (data_ptr + sizeof(uint32_t)); + v.mString.assign(str_data, str_len); + break; + } + + case OBJECT: + { + if (data_len < sizeof(uint32_t)) + throw DukException() << "Malformed object JSON data (no length)"; + uint32_t json_len = *((uint32_t*)data_ptr); + + if (data_len < sizeof(uint32_t) + json_len) + throw DukException() << "Malformed object JSON data (appears truncated)"; + + const char* json_data = (data_ptr + sizeof(uint32_t)); + duk_push_lstring(ctx, json_data, json_len); + int rc = duk_safe_call(ctx, &json_decode_safe, NULL, 1, 1); + if (rc) { + throw DukErrorException(ctx, rc) << "Could not decode JSON"; + } else { + v.mPOD.ref_array_idx = stash_ref(ctx, -1); + duk_pop(ctx); + } + break; + } + + default: + throw DukException() << "not implemented"; + } + + return v; + } + + // same as above (copy_from_stack), but also removes the value we copied from the stack + static DukValue take_from_stack(duk_context* ctx, duk_idx_t idx = -1) { + DukValue val = copy_from_stack(ctx, idx); + duk_remove(ctx, idx); + return val; + } + + // push the value we hold onto the stack + inline void push() const { + duk_context* ctx = mContext; + + switch (mType) { + case UNDEFINED: + duk_push_undefined(ctx); + break; + case NULLREF: + duk_push_null(ctx); + break; + + case BOOLEAN: + duk_push_boolean(ctx, mPOD.boolean); + break; + + case NUMBER: + duk_push_number(ctx, mPOD.number); + break; + + case STRING: + duk_push_lstring(ctx, mString.data(), mString.size()); + break; + + case OBJECT: + push_ref_array(ctx); + duk_get_prop_index(ctx, -1, mPOD.ref_array_idx); + duk_remove(ctx, -2); + break; + + case POINTER: + duk_push_pointer(ctx, mPOD.pointer); + break; + + case BUFFER: + case LIGHTFUNC: + default: + throw DukException() << "DukValue.push() not implemented for type (" << type_name() << ")"; + } + } + + // various (type-safe) getters + inline bool as_bool() const { + if (mType != BOOLEAN) + throw DukException() << "Expected boolean, got " << type_name(); + return mPOD.boolean; + } + + inline double as_double() const { + if (mType != NUMBER) + throw DukException() << "Expected number, got " << type_name(); + return mPOD.number; + } + + inline float as_float() const { + if (mType != NUMBER) + throw DukException() << "Expected number, got " << type_name(); + return static_cast(mPOD.number); + } + + inline duk_int_t as_int() const { + if (mType != NUMBER) + throw DukException() << "Expected number, got " << type_name(); + return static_cast(mPOD.number); + } + + inline duk_uint_t as_uint() const { + if (mType != NUMBER) + throw DukException() << "Expected number, got " << type_name(); + return static_cast(mPOD.number); + } + + inline void* as_pointer() const { + if (mType != POINTER && mType != NULLREF) + throw DukException() << "Expected pointer or null, got " << type_name(); + return mPOD.pointer; + } + + inline const std::string& as_string() const { + if (mType != STRING) + throw DukException() << "Expected string, got " << type_name(); + return mString; + } + + inline const char* as_c_string() const { + if (mType != STRING) + throw DukException() << "Expected string, got " << type_name(); + return mString.data(); + } + + inline Type type() const { + return mType; + } + + // same as duk_get_type_name(), but that's internal to Duktape, so we shouldn't use it + inline const char* type_name() const { + switch (mType) { + case UNDEFINED: return "undefined"; + case NULLREF: return "null"; + case BOOLEAN: return "boolean"; + case NUMBER: return "number"; + case STRING: return "string"; + case OBJECT: return "object"; + case BUFFER: return "buffer"; + case POINTER: return "pointer"; + case LIGHTFUNC: return "lightfunc"; + } + return "?"; + } + + inline duk_context* context() const { + return mContext; + } + + // Important limitations: + // - The returned value is binary and will not behave well if you treat it like a string (it will almost certainly contain '\0'). + // If you need to transport it like a string, maybe try encoding it as base64. + // - Strings longer than 2^32 (UINT32_MAX) characters will throw an exception. You can raise this to be a uint64_t if you need + // really long strings for some reason (be sure to change DukValue::deserialize() as well). + // - Objects are encoded to JSON and then sent like a string. If your object can't be encoded as JSON (i.e. it's a function), + // this will not work. This can be done, but I chose not to because it poses a security issue if you deserializing untrusted data. + // If you require this functionality, you'll have to add it yourself with using duk_dump_function(...). + static_assert(sizeof(char) == 1, "Serialization probably broke"); + std::vector serialize() const { + std::vector buff; + buff.resize(sizeof(Type)); + *((Type*)buff.data()) = mType; + + switch (mType) { + case UNDEFINED: + case NULLREF: + break; + + case BOOLEAN: + { + buff.push_back(mPOD.boolean ? 1 : 0); + break; + } + + case NUMBER: + { + buff.resize(buff.size() + sizeof(double)); + *((double*)(buff.data() + sizeof(Type))) = mPOD.number; + break; + } + + case STRING: + { + if (mString.length() > static_cast(UINT32_MAX)) + throw DukException() << "String length larger than uint32_t max"; + + uint32_t len = mString.length(); + buff.resize(buff.size() + sizeof(uint32_t) + len); + + uint32_t* len_ptr = (uint32_t*)(buff.data() + sizeof(Type)); + *len_ptr = len; + + char* out_ptr = (char*)(buff.data() + sizeof(Type) + sizeof(uint32_t)); + strncpy(out_ptr, mString.data(), len); // note: this will NOT be null-terminated + break; + } + + case OBJECT: + { + push(); + if (duk_is_function(mContext, -1)) { + duk_pop(mContext); + throw DukException() << "Functions cannot be serialized"; + // well, technically they can...see the comments at the start of this method + } + + std::string json = duk_json_encode(mContext, -1); + duk_pop(mContext); + + if (json.length() > static_cast(UINT32_MAX)) + throw DukException() << "JSON length larger than uint32_t max"; + + uint32_t len = json.length(); + buff.resize(buff.size() + sizeof(uint32_t) + len); + + uint32_t* len_ptr = (uint32_t*)(buff.data() + sizeof(Type)); + *len_ptr = len; + + char* out_ptr = (char*)(buff.data() + sizeof(Type) + sizeof(uint32_t)); + strncpy(out_ptr, json.data(), len); // note: this will NOT be null-terminated + break; + } + + default: + throw DukException() << "Type not implemented for serialization."; + } + + return buff; + } + +private: + // THIS IS COMPLETELY UNRELATED TO DETAIL_REFS.H. + // detail_refs.h stores a mapping of native object -> script object. + // This just stores arbitrary script objects (which likely have no native object backing them). + // If I was smarter I might merge the two implementations, but this one is simpler + // (since we don't need the std::map here). + static void push_ref_array(duk_context* ctx) + { + static const char* DUKVALUE_REF_ARRAY = "dukglue_dukvalue_refs"; + duk_push_heap_stash(ctx); + + if (!duk_has_prop_string(ctx, -1, DUKVALUE_REF_ARRAY)) { + duk_push_array(ctx); + + // ref_array[0] = 0 (initialize free list as empty) + duk_push_int(ctx, 0); + duk_put_prop_index(ctx, -2, 0); + + duk_put_prop_string(ctx, -2, DUKVALUE_REF_ARRAY); + } + + duk_get_prop_string(ctx, -1, DUKVALUE_REF_ARRAY); + duk_remove(ctx, -2); // pop heap stash + } + + // put a new reference into the ref array and return its index in the array + static duk_uint_t stash_ref(duk_context* ctx, duk_idx_t idx) + { + push_ref_array(ctx); + + // if idx is relative, we need to adjust it to deal with the array we just pushed + if (idx < 0) + idx--; + + // find next free index + // free indices are kept in a linked list, starting at ref_array[0] + duk_get_prop_index(ctx, -1, 0); + duk_uarridx_t next_free_idx = duk_get_uint(ctx, -1); + duk_pop(ctx); + + if (next_free_idx == 0) { + // no free spots in the array, make a new one at arr.length + next_free_idx = duk_get_length(ctx, -1); + } else { + // free spot found, need to remove it from the free list + // ref_array[0] = ref_array[next_free_idx] + duk_get_prop_index(ctx, -1, next_free_idx); + duk_put_prop_index(ctx, -2, 0); + } + + duk_dup(ctx, idx); // copy value we are storing (since store consumes it) + duk_put_prop_index(ctx, -2, next_free_idx); // store it (consumes duplicated value) + duk_pop(ctx); // pop ref array + + return next_free_idx; + } + + // remove ref_array_idx from the ref array and add its spot to the free list (at refs[0]) + static void free_ref(duk_context* ctx, duk_uarridx_t ref_array_idx) + { + push_ref_array(ctx); + + // add this spot to the free list + // refs[old_obj_idx] = refs[0] (implicitly gives up our reference) + duk_get_prop_index(ctx, -1, 0); + duk_put_prop_index(ctx, -2, ref_array_idx); + + // refs[0] = old_obj_idx + duk_push_uint(ctx, ref_array_idx); + duk_put_prop_index(ctx, -2, 0); + + duk_pop(ctx); // pop ref array + } + + // this is for reference counting - used to release our reference based on the state + // of mRefCount. If mRefCount is NULL, we never got copy constructed, so we have ownership + // of our reference and can free it. If it's not null and above 1, we decrement the counter + // (someone else owns the reference). If it's not null and equal to 1, we are the last owner + // of a previously shared reference, so we can free it. + void release_ref_count() + { + if (mType == OBJECT) + { + if (mRefCount != NULL) + { + // sharing with another DukValue, are we the only one left? + if (*mRefCount > 1) { // still someone else referencing this + *mRefCount = *mRefCount - 1; + } else { + // not sharing anymore, we can free it + free_ref(mContext, mPOD.ref_array_idx); + delete mRefCount; + } + + mRefCount = NULL; + } else { + // not sharing with any other DukValue, free it + free_ref(mContext, mPOD.ref_array_idx); + } + + mType = UNDEFINED; + } + } + + duk_context* mContext; + Type mType; // our type - one of the standard Duktape DUK_TYPE_* values + + // This holds the plain-old-data types. Since this is a variant, + // we hold only one value at a time, so this is a union to save + // a bit of space. + union ValueTypes { + bool boolean; + double number; + void* pointer; // if mType == NULLREF, this is 0 (otherwise holds pointer value when mType == POINTER) + duk_uarridx_t ref_array_idx; + } mPOD; + + std::string mString; // if it's a string, we store it with std::string + int* mRefCount; // if mType == OBJECT and we're sharing, this will point to our ref counter +}; \ No newline at end of file diff --git a/src/openrct2/thirdparty/dukglue/public_util.h b/src/openrct2/thirdparty/dukglue/public_util.h new file mode 100644 index 0000000000..d6ff344d8f --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/public_util.h @@ -0,0 +1,315 @@ +#pragma once + +#include "dukexception.h" +#include "detail_traits.h" // for index_tuple/make_indexes + +// This file has some useful utility functions for users. +// Hopefully this saves you from wading through the implementation. + +/** + * @brief Push a value onto the duktape stack. + * + * WARNING: THIS IS NOT "PROTECTED." If an error occurs when pushing (unlikely, but possible), + * the Duktape fatal error handler will be invoked (and the program will probably terminate). + * + * @param ctx duktape context + * @param[in] val value to push + */ +template +void dukglue_push(duk_context* ctx, const FullT& val) { + // ArgStorage has some static_asserts in it that validate value types, + // so we typedef it to force ArgStorage to compile and run the asserts + typedef typename dukglue::types::ArgStorage::type ValidateReturnType; + + using namespace dukglue::types; + DukType::type>::template push(ctx, std::move(val)); +} + +template +void dukglue_push(duk_context* ctx, const T& arg, ArgTs... args) +{ + dukglue_push(ctx, arg); + dukglue_push(ctx, args...); +} + +inline void dukglue_push(duk_context* ctx) +{ + // no-op +} + + +/** + * WARNING: THIS IS NOT "PROTECTED." If an error occurs while reading (which is possible if you didn't + * explicitly check the type), the fatal Duktape error handler will be invoked, and the program + * will probably abort. + */ +template +void dukglue_read(duk_context* ctx, duk_idx_t arg_idx, RetT* out) +{ + // ArgStorage has some static_asserts in it that validate value types, + // so we typedef it to force ArgStorage to compile and run the asserts + typedef typename dukglue::types::ArgStorage::type ValidateReturnType; + + using namespace dukglue::types; + *out = DukType::type>::template read(ctx, arg_idx); +} + + +// methods + +// leaves return value on stack +template +void dukglue_call_method(duk_context* ctx, const ObjT& obj, const char* method_name, ArgTs... args) +{ + dukglue_push(ctx, obj); + duk_get_prop_string(ctx, -1, method_name); + + if (duk_check_type(ctx, -1, DUK_TYPE_UNDEFINED)) { + duk_error(ctx, DUK_ERR_REFERENCE_ERROR, "Method does not exist", method_name); + return; + } + + if (!duk_is_callable(ctx, -1)) { + duk_error(ctx, DUK_ERR_TYPE_ERROR, "Property is not callable"); + return; + } + + duk_swap_top(ctx, -2); + dukglue_push(ctx, args...); + duk_call_method(ctx, sizeof...(args)); +} + +namespace dukglue { +namespace detail { + +template +struct SafeMethodCallData { + const ObjT* obj; + const char* method_name; + std::tuple args; + RetT* out; +}; + +template +void call_method_safe_helper(duk_context* ctx, const ObjT& obj, const char* method_name, std::tuple& tup, index_tuple indexes) +{ + dukglue_call_method(ctx, obj, method_name, std::forward(std::get(tup))...); +} + +template +typename std::enable_if::value, duk_idx_t>::type call_method_safe(duk_context* ctx, void* udata) +{ + typedef SafeMethodCallData DataT; + DataT* data = (DataT*) udata; + call_method_safe_helper(ctx, *(data->obj), data->method_name, data->args, typename make_indexes::type()); + return 1; +} + +template +typename std::enable_if::value, duk_idx_t>::type call_method_safe(duk_context* ctx, void* udata) +{ + typedef SafeMethodCallData DataT; + DataT* data = (DataT*)udata; + + call_method_safe_helper(ctx, *(data->obj), data->method_name, data->args, typename make_indexes::type()); + dukglue_read(ctx, -1, data->out); + return 1; +} + +} +} + +template +typename std::enable_if::value, RetT>::type dukglue_pcall_method(duk_context* ctx, const ObjT& obj, const char* method_name, ArgTs... args) +{ + dukglue::detail::SafeMethodCallData data { + &obj, method_name, std::tuple(args...), nullptr + }; + + duk_idx_t rc = duk_safe_call(ctx, &dukglue::detail::call_method_safe, (void*) &data, 0, 1); + if (rc != 0) + throw DukErrorException(ctx, rc); + + duk_pop(ctx); // remove result from stack +} + +template +typename std::enable_if::value, RetT>::type dukglue_pcall_method(duk_context* ctx, const ObjT& obj, const char* method_name, ArgTs... args) +{ + RetT out; + dukglue::detail::SafeMethodCallData data { + &obj, method_name, std::tuple(args...), &out + }; + + duk_idx_t rc = duk_safe_call(ctx, &dukglue::detail::call_method_safe, (void*) &data, 0, 1); + if (rc != 0) + throw DukErrorException(ctx, rc); + + duk_pop(ctx); // remove result from stack + return std::move(out); +} + + +// calls + +// leaves return value on the stack +template +void dukglue_call(duk_context* ctx, const ObjT& func, ArgTs... args) +{ + dukglue_push(ctx, func); + if (!duk_is_callable(ctx, -1)) { + duk_pop(ctx); + duk_error(ctx, DUK_ERR_TYPE_ERROR, "Object is not callable"); + return; + } + + dukglue_push(ctx, args...); + duk_call(ctx, sizeof...(args)); +} + + +// safe call +namespace dukglue { +namespace detail { + +template +struct SafeCallData { + const ObjT* obj; + std::tuple args; + RetT* out; +}; + +template +void call_safe_helper(duk_context* ctx, const ObjT& obj, std::tuple& tup, index_tuple indexes) +{ + dukglue_call(ctx, obj, std::forward(std::get(tup))...); +} + +// leaves result on stack +template +typename std::enable_if::value, duk_ret_t>::type call_safe(duk_context* ctx, void* udata) +{ + typedef SafeCallData DataT; + DataT* data = (DataT*)udata; + + call_safe_helper(ctx, *(data->obj), data->args, typename make_indexes::type()); + return 1; +} + +// leaves result on stack +// The result is read into RetT here because it can potentially trigger an error (with duk_error). +// If we did it "above" this function, that error would trigger a panic instead of error handling. +template +typename std::enable_if::value, duk_ret_t>::type call_safe(duk_context* ctx, void* udata) +{ + typedef SafeCallData DataT; + DataT* data = (DataT*)udata; + + call_safe_helper(ctx, *(data->obj), data->args, typename make_indexes::type()); + dukglue_read(ctx, -1, data->out); + return 1; +} + +} +} + +// Unlike duktape, this will remove the return value from the stack! +template +typename std::enable_if::value, RetT>::type dukglue_pcall(duk_context* ctx, const ObjT& obj, ArgTs... args) +{ + dukglue::detail::SafeCallData data{ + &obj, std::tuple(args...), nullptr + }; + + duk_int_t rc = duk_safe_call(ctx, &dukglue::detail::call_safe, (void*) &data, 0, 1); + if (rc != 0) + throw DukErrorException(ctx, rc); + duk_pop(ctx); // remove result from stack +} + +template +typename std::enable_if::value, RetT>::type dukglue_pcall(duk_context* ctx, const ObjT& obj, ArgTs... args) +{ + RetT result; + dukglue::detail::SafeCallData data{ + &obj, std::tuple(args...), &result + }; + + duk_int_t rc = duk_safe_call(ctx, &dukglue::detail::call_safe, (void*) &data, 0, 1); + if (rc != 0) + throw DukErrorException(ctx, rc); + + duk_pop(ctx); // remove result from stack + return std::move(result); +} + +// same as dukglue_pcall, but leaves the result or error on the stack and returns the Duktape return code +template +duk_int_t dukglue_pcall_raw(duk_context* ctx, const ObjT& obj, ArgTs... args) +{ + dukglue::detail::SafeCallData data{ + &obj, std::tuple(args...), nullptr + }; + + return duk_safe_call(ctx, &dukglue::detail::call_safe, (void*)&data, 0, 1); +} + + +// peval +namespace dukglue { +namespace detail { + +template +struct SafeEvalData { + const char* str; + RetT* out; +}; + +template +duk_ret_t eval_safe(duk_context* ctx, void* udata) +{ + SafeEvalData* data = (SafeEvalData*) udata; + + duk_eval_string(ctx, data->str); + dukglue_read(ctx, -1, data->out); + return 1; +} + +} +} + +template +typename std::enable_if::value, RetT>::type dukglue_peval(duk_context* ctx, const char* str) +{ + int prev_top = duk_get_top(ctx); + int rc = duk_peval_string(ctx, str); + if (rc != 0) + throw DukErrorException(ctx, rc); + + duk_pop_n(ctx, duk_get_top(ctx) - prev_top); // pop any results +} + +template +typename std::enable_if::value, RetT>::type dukglue_peval(duk_context* ctx, const char* str) +{ + int prev_top = duk_get_top(ctx); + + RetT ret; + dukglue::detail::SafeEvalData data{ + str, &ret + }; + + int rc = duk_safe_call(ctx, &dukglue::detail::eval_safe, (void*) &data, 0, 1); + if (rc != 0) + throw DukErrorException(ctx, rc); + duk_pop_n(ctx, duk_get_top(ctx) - prev_top); // pop any results + return ret; +} + +// register a global object (very simple helper, but very common for "Hello World"-ish applications) +template +inline void dukglue_register_global(duk_context* ctx, const T& obj, const char* name) +{ + dukglue_push(ctx, obj); + duk_put_global_string(ctx, name); +} \ No newline at end of file diff --git a/src/openrct2/thirdparty/dukglue/register_class.h b/src/openrct2/thirdparty/dukglue/register_class.h new file mode 100644 index 0000000000..bb7669029f --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/register_class.h @@ -0,0 +1,202 @@ +#pragma once + +#include "detail_class_proto.h" +#include "detail_constructor.h" +#include "detail_method.h" + +// Set the constructor for the given type. +template +void dukglue_register_constructor(duk_context* ctx, const char* name) +{ + duk_c_function constructor_func = dukglue::detail::call_native_constructor; + + duk_push_c_function(ctx, constructor_func, sizeof...(Ts)); + + // set constructor_func.prototype + dukglue::detail::ProtoManager::push_prototype(ctx); + duk_put_prop_string(ctx, -2, "prototype"); + + // set name = constructor_func + duk_put_global_string(ctx, name); +} + +template +void dukglue_register_constructor_managed(duk_context* ctx, const char* name) +{ + duk_c_function constructor_func = dukglue::detail::call_native_constructor; + duk_c_function finalizer_func = dukglue::detail::managed_finalizer; + + duk_push_c_function(ctx, constructor_func, sizeof...(Ts)); + + // create new prototype with finalizer + duk_push_object(ctx); + + // set the finalizer + duk_push_c_function(ctx, finalizer_func, 1); + duk_set_finalizer(ctx, -2); + + // hook prototype with finalizer up to real class prototype + // must use duk_set_prototype, not set the .prototype property + dukglue::detail::ProtoManager::push_prototype(ctx); + duk_set_prototype(ctx, -2); + + // set constructor_func.prototype to the prototype with the finalizer + duk_put_prop_string(ctx, -2, "prototype"); + + // set name = constructor_func + duk_put_global_string(ctx, name); +} + +template +void dukglue_set_base_class(duk_context* ctx) +{ + static_assert(!std::is_pointer::value && !std::is_pointer::value + && !std::is_const::value && !std::is_const::value, "Use bare class names."); + static_assert(std::is_base_of::value, "Invalid class hierarchy!"); + + using namespace dukglue::detail; + + // Derived.type_info->set_base(Base.type_info) + ProtoManager::push_prototype(ctx); + duk_get_prop_string(ctx, -1, "\xFF" "type_info"); + TypeInfo* derived_type_info = static_cast(duk_require_pointer(ctx, -1)); + duk_pop_2(ctx); + + ProtoManager::push_prototype(ctx); + duk_get_prop_string(ctx, -1, "\xFF" "type_info"); + TypeInfo* base_type_info = static_cast(duk_require_pointer(ctx, -1)); + duk_pop_2(ctx); + + derived_type_info->set_base(base_type_info); + + // also set up the prototype chain + ProtoManager::push_prototype(ctx); + ProtoManager::push_prototype(ctx); + duk_set_prototype(ctx, -2); + duk_pop(ctx); +} + +// methods +template +void dukglue_register_method_compiletime(duk_context* ctx, RetType(Cls::*method)(Ts...), const char* name) +{ + static_assert(std::is_same::value, "Mismatching method types."); + dukglue_register_method_compiletime(ctx, name); +} + +template +void dukglue_register_method_compiletime(duk_context* ctx, RetType(Cls::*method)(Ts...) const, const char* name) +{ + static_assert(std::is_same::value, "Mismatching method types."); + dukglue_register_method_compiletime(ctx, name); +} + +template +void dukglue_register_method_compiletime(duk_context* ctx, const char* name) +{ + using namespace dukglue::detail; + typedef MethodInfo MethodInfo; + + duk_c_function method_func = MethodInfo::template MethodCompiletime::call_native_method; + + ProtoManager::push_prototype(ctx); + + duk_push_c_function(ctx, method_func, sizeof...(Ts)); + duk_put_prop_string(ctx, -2, name); // consumes func above + + duk_pop(ctx); // pop prototype +} + +template +void dukglue_register_method(duk_context* ctx, RetType(Cls::*method)(Ts...), const char* name) +{ + dukglue_register_method(ctx, method, name); +} + +template +void dukglue_register_method(duk_context* ctx, RetType(Cls::*method)(Ts...) const, const char* name) +{ + dukglue_register_method(ctx, method, name); +} + +// I'm sorry this signature is so long, but I figured it was better than duplicating the method, +// once for const methods and once for non-const methods. +template +void dukglue_register_method(duk_context* ctx, typename std::conditional::type method, const char* name) +{ + using namespace dukglue::detail; + typedef MethodInfo MethodInfo; + + duk_c_function method_func = MethodInfo::MethodRuntime::call_native_method; + + ProtoManager::push_prototype(ctx); + + duk_push_c_function(ctx, method_func, sizeof...(Ts)); + + duk_push_pointer(ctx, new typename MethodInfo::MethodHolder{ method }); + duk_put_prop_string(ctx, -2, "\xFF" "method_holder"); // consumes raw method pointer + + // make sure we free the method_holder when this function is removed + duk_push_c_function(ctx, MethodInfo::MethodRuntime::finalize_method, 1); + duk_set_finalizer(ctx, -2); + + duk_put_prop_string(ctx, -2, name); // consumes method function + + duk_pop(ctx); // pop prototype +} + +// methods with a variable number of (script) arguments +template +inline void dukglue_register_method_varargs(duk_context* ctx, duk_ret_t(Cls::*method)(duk_context*), const char* name) +{ + dukglue_register_method_varargs(ctx, method, name); +} + +template +inline void dukglue_register_method_varargs(duk_context* ctx, duk_ret_t(Cls::*method)(duk_context*) const, const char* name) +{ + dukglue_register_method_varargs(ctx, method, name); +} + +template +void dukglue_register_method_varargs(duk_context* ctx, + typename std::conditional::type method, + const char* name) +{ + using namespace dukglue::detail; + typedef MethodVariadicRuntime MethodVariadicInfo; + + duk_c_function method_func = MethodVariadicInfo::call_native_method; + + ProtoManager::push_prototype(ctx); + + duk_push_c_function(ctx, method_func, DUK_VARARGS); + + duk_push_pointer(ctx, new typename MethodVariadicInfo::MethodHolderVariadic{ method }); + duk_put_prop_string(ctx, -2, "\xFF" "method_holder"); // consumes raw method pointer + + // make sure we free the method_holder when this function is removed + duk_push_c_function(ctx, MethodVariadicInfo::finalize_method, 1); + duk_set_finalizer(ctx, -2); + + duk_put_prop_string(ctx, -2, name); // consumes method function + + duk_pop(ctx); // pop prototype +} + +inline void dukglue_invalidate_object(duk_context* ctx, void* obj_ptr) +{ + dukglue::detail::RefManager::find_and_invalidate_native_object(ctx, obj_ptr); +} + +// register a deleter +template +void dukglue_register_delete(duk_context* ctx) +{ + duk_c_function delete_func = dukglue::detail::call_native_deleter; + + dukglue::detail::ProtoManager::push_prototype(ctx); + duk_push_c_function(ctx, delete_func, 0); + duk_put_prop_string(ctx, -2, "delete"); + duk_pop(ctx); // pop prototype +} \ No newline at end of file diff --git a/src/openrct2/thirdparty/dukglue/register_function.h b/src/openrct2/thirdparty/dukglue/register_function.h new file mode 100644 index 0000000000..77e83aad95 --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/register_function.h @@ -0,0 +1,35 @@ +#pragma once + +#include "detail_function.h" + +// Register a function, embedding the function address at compile time. +// According to benchmarks, there's really not much reason to do this +// (inconsistent 2-3% performance improvement for a 10,000 function call stress test averaged over 100 runs), +// since it has much uglier syntax and may bloat executable size if you have many functions with identical signatures. +template +void dukglue_register_function_compiletime(duk_context* ctx, RetType(*)(Ts...), const char* name) +{ + static_assert(std::is_same::value, + "Mismatching function pointer template parameter and function pointer argument types. " + "Try: dukglue_register_function(ctx, \"funcName\", func)"); + + duk_c_function evalFunc = dukglue::detail::FuncInfoHolder::template FuncActual::call_native_function; + + duk_push_c_function(ctx, evalFunc, sizeof...(Ts)); + duk_put_global_string(ctx, name); +} + +// Register a function. +template +void dukglue_register_function(duk_context* ctx, RetType(*funcToCall)(Ts...), const char* name) +{ + duk_c_function evalFunc = dukglue::detail::FuncInfoHolder::FuncRuntime::call_native_function; + + duk_push_c_function(ctx, evalFunc, sizeof...(Ts)); + + static_assert(sizeof(RetType(*)(Ts...)) == sizeof(void*), "Function pointer and data pointer are different sizes"); + duk_push_pointer(ctx, reinterpret_cast(funcToCall)); + duk_put_prop_string(ctx, -2, "\xFF" "func_ptr"); + + duk_put_global_string(ctx, name); +} diff --git a/src/openrct2/thirdparty/dukglue/register_property.h b/src/openrct2/thirdparty/dukglue/register_property.h new file mode 100644 index 0000000000..c4fd8b36b7 --- /dev/null +++ b/src/openrct2/thirdparty/dukglue/register_property.h @@ -0,0 +1,123 @@ +#pragma once + +#include "detail_method.h" + +// const getter, setter +template +void dukglue_register_property(duk_context* ctx, + RetT(Cls::*getter)() const, + void(Cls::*setter)(ArgT), + const char* name) +{ + dukglue_register_property(ctx, getter, setter, name); +} + +// const getter, no setter +template +void dukglue_register_property(duk_context* ctx, + RetT(Cls::*getter)() const, + std::nullptr_t setter, + const char* name) +{ + dukglue_register_property(ctx, getter, setter, name); +} + +// non-const getter, setter +template +void dukglue_register_property(duk_context* ctx, + RetT(Cls::*getter)(), + void(Cls::*setter)(ArgT), + const char* name) +{ + dukglue_register_property(ctx, getter, setter, name); +} + +// non-const getter, no setter +template +void dukglue_register_property(duk_context* ctx, + RetT(Cls::*getter)(), + std::nullptr_t setter, + const char* name) +{ + dukglue_register_property(ctx, getter, setter, name); +} + +// no getter, setter +template +void dukglue_register_property(duk_context* ctx, + std::nullptr_t getter, + void(Cls::*setter)(ArgT), + const char* name) +{ + dukglue_register_property(ctx, getter, setter, name); +} + +// no getter, no setter +template +void dukglue_register_property(duk_context* ctx, std::nullptr_t getter, std::nullptr_t setter, const char* name) +{ + // strictly speaking I think duktape can probably handle neither + // (according to the wonderful API docs), but I don't know why you + // would want to do this in the first place + static_assert(std::is_void::value, "Must have getter or setter"); +} + +inline duk_ret_t dukglue_throw_error(duk_context* ctx) +{ + duk_error(ctx, DUK_ERR_TYPE_ERROR, "Property does not have getter or setter."); +} + +template +void dukglue_register_property(duk_context* ctx, + typename std::conditional::type getter, + void(Cls::*setter)(ArgT), + const char* name) +{ + using namespace dukglue::detail; + typedef MethodInfo GetterMethodInfo; + typedef MethodInfo SetterMethodInfo; + + ProtoManager::push_prototype(ctx); + + // push key + duk_push_string(ctx, name); + + // push getter + if (getter != nullptr) { + duk_c_function method_func = GetterMethodInfo::MethodRuntime::call_native_method; + + duk_push_c_function(ctx, method_func, 0); + + duk_push_pointer(ctx, new typename GetterMethodInfo::MethodHolder{ getter }); + duk_put_prop_string(ctx, -2, "\xFF" "method_holder"); // consumes raw method pointer + + // make sure we free the method_holder when this function is removed + duk_push_c_function(ctx, GetterMethodInfo::MethodRuntime::finalize_method, 1); + duk_set_finalizer(ctx, -2); + } else { + duk_push_c_function(ctx, dukglue_throw_error, 1); + } + + if (setter != nullptr) { + duk_c_function method_func = SetterMethodInfo::MethodRuntime::call_native_method; + + duk_push_c_function(ctx, method_func, 1); + + duk_push_pointer(ctx, new typename SetterMethodInfo::MethodHolder{ setter }); + duk_put_prop_string(ctx, -2, "\xFF" "method_holder"); // consumes raw method pointer + + // make sure we free the method_holder when this function is removed + duk_push_c_function(ctx, SetterMethodInfo::MethodRuntime::finalize_method, 1); + duk_set_finalizer(ctx, -2); + } else { + duk_push_c_function(ctx, dukglue_throw_error, 1); + } + + duk_uint_t flags = DUK_DEFPROP_HAVE_GETTER + | DUK_DEFPROP_HAVE_SETTER + | DUK_DEFPROP_HAVE_CONFIGURABLE /* set not configurable (from JS) */ + | DUK_DEFPROP_FORCE /* allow overriding built-ins and previously defined properties */; + + duk_def_prop(ctx, -4, flags); + duk_pop(ctx); // pop prototype +}