OpenRCT2/src/thirdparty/dukglue/dukvalue.h

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;
}
};