Skip to content

File values.h

File List > jac > machine > values.h

Go to the documentation of this file

#pragma once

#include <quickjs.h>
#include <span>
#include <string>
#include <tuple>
#include <vector>

#include "atom.h"
#include "context.h"
#include "internal/declarations.h"
#include "stringView.h"


namespace jac {


enum class PropFlags : int {
    Default = 0,
    Configurable = JS_PROP_CONFIGURABLE,
    Writable = JS_PROP_WRITABLE,
    Enumerable = JS_PROP_ENUMERABLE,
    C_W_E = JS_PROP_C_W_E
};

inline constexpr PropFlags operator|(PropFlags a, PropFlags b) {
    return static_cast<PropFlags>(static_cast<int>(a) | static_cast<int>(b));
}

inline constexpr PropFlags operator&(PropFlags a, PropFlags b) {
    return static_cast<PropFlags>(static_cast<int>(a) & static_cast<int>(b));
}


template<typename T>
T fromValue(ContextRef ctx, ValueWeak val);

template<typename T>
Value toValue(ContextRef ctx, T val);


template<bool managed>
class ValueWrapper {
protected:
    ContextRef _ctx;
    JSValue _val;
public:
    ValueWrapper(ContextRef ctx, JSValue val);
    ValueWrapper(const ValueWrapper &other):
        _ctx(other._ctx),
        _val(managed ? JS_DupValue(_ctx, other._val) : other._val)
    {}
    ValueWrapper(ValueWrapper &&other) : _ctx(other._ctx), _val(other._val) {
        other._ctx = nullptr;
        other._val = JS_UNDEFINED;
    }

    ValueWrapper& operator=(const ValueWrapper &other) {
        if (managed) {
            if (_ctx) {
                JS_FreeValue(_ctx, _val);
            }
            _val = JS_DupValue(_ctx, other._val);
        }
        else {
            _val = other._val;
        }
        _ctx = other._ctx;
        return *this;
    }

    ValueWrapper& operator=(ValueWrapper &&other) {
        if (managed && _ctx) {
            JS_FreeValue(_ctx, _val);
        }
        _val = other._val;
        _ctx = other._ctx;
        other._val = JS_UNDEFINED;
        other._ctx = nullptr;
        return *this;
    }

    operator ValueWeak() {
        return ValueWeak(_ctx, _val);
    }

    ~ValueWrapper() {
        if (managed && _ctx) {
            JS_FreeValue(_ctx, _val);
        }
    }

    std::pair<ContextRef, JSValue> loot() {
        JSValue ret_ = _val;
        ContextRef ctx_ = this->_ctx;
        _ctx = nullptr;
        _val = JS_UNDEFINED;
        return {ctx_, ret_};
    }

    JSValue& getVal() {
        return _val;
    }

    bool isUndefined() {
        return JS_IsUndefined(_val);
    }

    bool isNull() {
        return JS_IsNull(_val);
    }

    bool isObject() {
        return JS_IsObject(_val);
    }

    bool isArray() {
        return JS_IsArray(_ctx, _val);
    }

    bool isFunction() {
        return JS_IsFunction(_ctx, _val);
    }

    StringView toString() {
        return to<StringView>();
    }

    template<typename T>
    T to() {
        return fromValue<T>(_ctx, *this);
    }

    Value stringify(int indent = 0) {
        auto idt = Value::from(_ctx, indent);
        return Value(_ctx, JS_JSONStringify(_ctx, _val, JS_UNDEFINED, idt.getVal()));
    }

    template<typename T>
    static Value from(ContextRef ctx, T val) {
        return toValue(ctx, val);
    }

    static Value fromJSON(ContextRef ctx, std::string json, std::string filename = "<json>", bool extended = false) {
        return Value(ctx, JS_ParseJSON2(ctx, json.c_str(), json.size(), filename.c_str(), extended ? JS_PARSE_JSON_EXT : 0));
    }

    static Value undefined(ContextRef ctx) {
        return ValueWrapper(ctx, JS_UNDEFINED);
    }

    static Value null(ContextRef ctx) {
        return ValueWrapper(ctx, JS_NULL);
    }

    friend std::ostream& operator<<(std::ostream& os, ValueWrapper& val) {
        os << val.toString();
        return os;
    }
};


template<bool managed>
class ExceptionWrapper : public ValueWrapper<managed>, public std::exception {
public:
    enum class Type {
        Any,
        Error,
        SyntaxError,
        TypeError,
        ReferenceError,
        RangeError,
        InternalError
    };
private:
    std::string _message;
    Type _type;

    ExceptionWrapper(Type type, std::string message) : ValueWrapper<managed>(nullptr, JS_UNDEFINED), _message(std::move(message)), _type(type) {}
protected:
    using ValueWrapper<managed>::_val;
    using ValueWrapper<managed>::_ctx;
public:
    ExceptionWrapper(ValueWrapper<managed> value) : ValueWrapper<managed>(std::move(value)), _type(Type::Any) {
        _message = this->toString();
    }
    ExceptionWrapper(ContextRef ctx, JSValue val) : ExceptionWrapper(ValueWrapper<managed>(ctx, val)) {}

    std::string stackTrace() noexcept;

    const char* what() const noexcept override {
        return _message.c_str();
    }

    static Exception create(Type type, std::string message) {
        return ExceptionWrapper(type, message);
    }

    JSValue throwJS(ContextRef ctx);
};


template<bool managed>
class ObjectWrapper : public ValueWrapper<managed> {
protected:
    using ValueWrapper<managed>::_val;
    using ValueWrapper<managed>::_ctx;
public:
    ObjectWrapper(ValueWrapper<managed> value) : ValueWrapper<managed>(std::move(value)) {
        if (!this->isObject()) {
            throw Exception::create(Exception::Type::TypeError, "not an object");
        }
    }
    ObjectWrapper(ContextRef ctx, JSValue val) : ObjectWrapper(ValueWrapper<managed>(ctx, val)) {}

    template<typename T = Value>
    T get(Atom prop) {
        Value val(_ctx, JS_GetProperty(_ctx, _val, prop.get()));
        return val.to<T>();
    }

    template<typename T = Value>
    T get(const std::string& name) {
        return get<T>(Atom::create(_ctx, name.c_str()));
    }

    template<typename T = Value>
    T get(uint32_t idx) {
        return get<T>(Atom::create(_ctx, idx));
    }

    template<typename T>
    void set(Atom prop, T val) {
        if (JS_SetProperty(_ctx, _val, prop.get(), toValue(_ctx, val).loot().second) < 0) {
            throw _ctx.getException();
        }
    }

    template<typename T>
    void set(const std::string& name, T val) {
        set(Atom::create(_ctx, name.c_str()), val);
    }

    template<typename T>
    void set(uint32_t idx, T val) {
        set(Atom::create(_ctx, idx), val);
    }

    template<typename Res, typename... Args>
    Res invoke(Atom key, Args... args);

    template<typename Res, typename... Args>
    Res invoke(const std::string& key, Args... args) {
        return invoke<Res>(Atom::create(_ctx, key.c_str()), args...);
    }

    template<typename Res, typename... Args>
    Res invoke(uint32_t idx, Args... args) {
        return invoke<Res>(Atom::create(_ctx, idx), args...);
    }

    template<typename Id>
    void defineProperty(Id id, Value value, PropFlags flags = PropFlags::Default) {
        Atom atom = Atom::create(_ctx, id);
        if (JS_DefinePropertyValue(_ctx, _val, atom.get(), value.loot().second, static_cast<int>(flags)) < 0) {
            throw _ctx.getException();
        }
    }

    template<typename Id>
    bool hasProperty(Id id) {
        Atom atom = Atom::create(_ctx, id);
        int res = JS_HasProperty(_ctx, _val, atom.get());
        if (res < 0) {
            throw _ctx.getException();
        }
        return res;
    }

    template<typename Id>
    void deleteProperty(Id id) {
        Atom atom = Atom::create(_ctx, id);
        if (JS_DeleteProperty(_ctx, _val, atom.get(), 0) < 0) {
            throw _ctx.getException();
        }
    }

    Object getPrototype() {
        return Object(_ctx, JS_GetPrototype(this->_ctx, this->_val));
    }

    void setPrototype(Object proto) {
        if (JS_SetPrototype(this->_ctx, this->_val, proto.getVal()) < 0) {
            throw _ctx.getException();
        }
    }

    static Object create(ContextRef ctx) {
        return Object(ctx, JS_NewObject(ctx));
    }
};


template<bool managed>
class FunctionWrapper : public ObjectWrapper<managed> {
protected:
    using ObjectWrapper<managed>::_val;
    using ObjectWrapper<managed>::_ctx;
public:
    FunctionWrapper(ObjectWrapper<managed> value) : ObjectWrapper<managed>(std::move(value)) {
        if (!this->isFunction()) {
            throw Exception::create(Exception::Type::TypeError, "not a function");
        }
    }
    FunctionWrapper(ContextRef ctx, JSValue val) : FunctionWrapper(ObjectWrapper<managed>(ctx, val)) {}

    template<typename Res, typename... Args>
    Res callThis(Value thisVal, Args... args) {
        std::vector<JSValue> vals;
        vals.reserve(sizeof...(Args));
        try {
            (vals.push_back(toValue(_ctx, args).loot().second), ...);
            Value ret(_ctx, JS_Call(_ctx, _val, thisVal.getVal(), vals.size(), vals.data()));

            for (auto &v : vals) {
                JS_FreeValue(_ctx, v);
            }
            vals.clear();

            return ret.to<Res>();
        } catch (Exception &e) {
            for (auto &v : vals) {
                JS_FreeValue(_ctx, v);
            }
            throw e;
        }
    }

    template<typename Res, typename... Args>
    Res call(Args... args) {
        return callThis<Res>(Value::undefined(_ctx), args...);
    }

    template<typename... Args>
    Value callConstructor(Args... args) {
        std::vector<JSValue> vals;
        vals.reserve(sizeof...(Args));
        try {
            (vals.push_back(toValue(_ctx, args).loot().second), ...);
            Value ret(_ctx, JS_CallConstructor(_ctx, _val, vals.size(), vals.data()));

            for (auto &v : vals) {
                JS_FreeValue(_ctx, v);
            }
            vals.clear();

            return ret;
        } catch (Exception &e) {
            for (auto &v : vals) {
                JS_FreeValue(_ctx, v);
            }
            throw e;
        }
    }
};


template<bool managed>
class ArrayWrapper : public ObjectWrapper<managed> {
protected:
    using ObjectWrapper<managed>::_val;
    using ObjectWrapper<managed>::_ctx;
public:
    ArrayWrapper(ObjectWrapper<managed> value) : ObjectWrapper<managed>(std::move(value)) {
        if (!this->isArray()) {
            throw Exception::create(Exception::Type::TypeError, "not an array");
        }
    }
    ArrayWrapper(ContextRef ctx, JSValue val) : ArrayWrapper(ObjectWrapper<managed>(ctx, val)) {}

    int length() {
        return this->template get<int>("length");
    }

    static Array create(ContextRef ctx) {
        return Array(ctx, JS_NewArray(ctx));
    }
};


template<bool managed>
class PromiseWrapper : public ObjectWrapper<managed> {
protected:
    using ObjectWrapper<managed>::_val;
    using ObjectWrapper<managed>::_ctx;
public:
    PromiseWrapper(ObjectWrapper<managed> value) : ObjectWrapper<managed>(std::move(value)) {
        // TODO: check if value is Promise
        // not implemented, because a convenient check is not a part of QuickJS API
        // different type being converted to promise may cause hard to find errors
    }
    PromiseWrapper(ContextRef ctx, JSValue val) : PromiseWrapper(ObjectWrapper<managed>(ctx, val)) {}

    static std::tuple<Promise, Function, Function> create(ContextRef ctx) {
        JSValue functions[2];
        JSValue promise = JS_NewPromiseCapability(ctx, functions);
        return std::make_tuple(Promise(ctx, promise), Function(ctx, functions[0]), Function(ctx, functions[1]));
    }
};


template<bool managed>
class ArrayBufferWrapper : public ObjectWrapper<managed> {
protected:
    using ObjectWrapper<managed>::_val;
    using ObjectWrapper<managed>::_ctx;

    static void freeArrayBuffer(JSRuntime*, void*, void *ptr) {
        delete[] static_cast<uint8_t*>(ptr);
    }
public:
    ArrayBufferWrapper(ObjectWrapper<managed> value) : ObjectWrapper<managed>(std::move(value)) {
        // TODO: check if value is ArrayBuffer
        // not implemented, because a convenient check is not a part of QuickJS API
        // different type being converted to promise may cause hard to find errors
        // methods of ArrayBufferWrapper will throw exceptions if the type is not ArrayBuffer
    }
    ArrayBufferWrapper(ContextRef ctx, JSValue val) : ArrayBufferWrapper(ObjectWrapper<managed>(ctx, val)) {}

    uint8_t* data() {
        return static_cast<uint8_t*>(JS_GetArrayBuffer(_ctx, nullptr, _val));
    }

    size_t size() {
        size_t size;
        JS_GetArrayBuffer(_ctx, &size, _val);
        return size;
    }

    template<typename T>
    std::span<T> typedView() {
        if (size() % sizeof(T) != 0) {
            throw Exception::create(Exception::Type::TypeError, "size is not a multiple of the element size");
        }
        size_t size;
        T* ptr = static_cast<T*>(JS_GetArrayBuffer(_ctx, &size, _val));
        return std::span<T>(ptr, size / sizeof(T));
    }

    static ArrayBuffer create(ContextRef ctx, size_t size) {
        return ArrayBuffer(ctx, JS_NewArrayBuffer(ctx, new uint8_t[size]{}, size, freeArrayBuffer, nullptr, false));
    }

    static ArrayBuffer create(ContextRef ctx, std::span<const uint8_t> data) {
        return ArrayBuffer(ctx, JS_NewArrayBufferCopy(ctx, data.data(), data.size()));
    }
};

template<bool managed>
ValueWrapper<managed>::ValueWrapper(ContextRef ctx, JSValue val) : _ctx(ctx), _val(val) {
    if (JS_IsException(_val)) {
        throw ctx.getException();
    }
}

template<bool managed>
template<typename Res, typename... Args>
Res ObjectWrapper<managed>::invoke(Atom key, Args... args) {
    return get<Function>(key).template callThis<Res>(*this, args...);
};


template<bool managed>
std::string ExceptionWrapper<managed>::stackTrace() noexcept {
    try {
        ObjectWeak obj(*this);
        return obj.get("stack").toString();
    } catch (std::exception &e) {
        return "failed to get stack trace: " + std::string(e.what());
    }
}


template<bool managed>
JSValue ExceptionWrapper<managed>::throwJS(ContextRef ctx) {
    if (_type == Type::Any) {
        auto [_, val] = ValueWrapper<managed>::loot();
        return JS_Throw(ctx, val);
    }


    if (_type == Type::Error) {
        ObjectWeak errObj(ctx, JS_NewError(ctx));
        errObj.set("message", _message);

        return JS_Throw(ctx, errObj.getVal());
    }

    switch (_type) {
        case Type::SyntaxError:
            return JS_ThrowSyntaxError(ctx, "%s", _message.c_str());
        case Type::TypeError:
            return JS_ThrowTypeError(ctx, "%s", _message.c_str());
        case Type::ReferenceError:
            return JS_ThrowReferenceError(ctx, "%s", _message.c_str());
        case Type::RangeError:
            return JS_ThrowRangeError(ctx, "%s", _message.c_str());
        case Type::InternalError:
            return JS_ThrowInternalError(ctx, "%s", _message.c_str());
        default:
            return JS_Throw(ctx, JS_NewError(ctx));
    }
}


} // namespace jac


#include "traits.h"


namespace jac {


template<typename T>
T fromValue([[maybe_unused]] ContextRef ctx, [[maybe_unused]] ValueWeak val) {
    if constexpr (std::is_same_v<T, void>) {
        return;
    }
    else {
        return ConvTraits<T>::from(ctx, val);
    }
}

template<typename T>
Value toValue([[maybe_unused]] ContextRef ctx, [[maybe_unused]] T value) {
    if constexpr (std::is_same_v<T, void>) {
        return Value::undefined(ctx);
    }

    auto val = ConvTraits<T>::to(ctx, value);

    return val;
}


} // namespace jac