mirror of https://github.com/OpenRCT2/OpenRCT2.git
636 lines
17 KiB
C++
636 lines
17 KiB
C++
#pragma once
|
|
|
|
#include <duktape.h>
|
|
#include <stdint.h>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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<DukValue&>(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<float>(mPOD.number);
|
|
}
|
|
|
|
inline duk_int_t as_int() const {
|
|
if (mType != NUMBER)
|
|
throw DukException() << "Expected number, got " << type_name();
|
|
return static_cast<uint32_t>(mPOD.number);
|
|
}
|
|
|
|
inline duk_uint_t as_uint() const {
|
|
if (mType != NUMBER)
|
|
throw DukException() << "Expected number, got " << type_name();
|
|
return static_cast<uint32_t>(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<char> serialize() const {
|
|
std::vector<char> 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<size_t>(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<size_t>(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
|
|
|
|
public:
|
|
DukValue operator [](const std::string &key) const
|
|
{
|
|
push();
|
|
duk_get_prop_string(mContext, -1, key.c_str());
|
|
return DukValue::take_from_stack(mContext);
|
|
}
|
|
|
|
bool is_array() const
|
|
{
|
|
push();
|
|
bool result = duk_is_array(mContext, -1);
|
|
duk_pop(mContext);
|
|
return result;
|
|
}
|
|
|
|
std::vector<DukValue> as_array() const
|
|
{
|
|
push();
|
|
if (!duk_is_array(mContext, -1))
|
|
{
|
|
duk_pop(mContext);
|
|
throw DukException() << "Expected array, got " << type_name();
|
|
}
|
|
|
|
auto arrayLength = duk_get_length(mContext, -1);
|
|
std::vector<DukValue> result;
|
|
result.reserve(arrayLength);
|
|
for (size_t i = 0; i < arrayLength; i++)
|
|
{
|
|
duk_get_prop_index(mContext, -1, i);
|
|
result.push_back(take_from_stack(mContext));
|
|
}
|
|
|
|
duk_pop(mContext);
|
|
return result;
|
|
}
|
|
|
|
bool is_function() const
|
|
{
|
|
push();
|
|
bool result = duk_is_function(mContext, -1);
|
|
duk_pop(mContext);
|
|
return result;
|
|
}
|
|
}; |