#pragma once #include #include #include #include #include #include #include #include #include #include namespace qjs { #include "quickjs/quickjs.h" #include "quickjs/quickjs-libc.h" /** Exception type. * Indicates that exception has occured in JS context. */ class exception {}; /** Javascript conversion traits. * Describes how to convert type R to/from JSValue. Second template argument can be used for SFINAE/enable_if type filters. */ template struct js_traits { /** Create an object of C++ type R given JSValue v and JSContext. * This function is intentionally not implemented. User should implement this function for their own type. * @param v This value is passed as JSValueConst so it should be freed by the caller. * @throws exception in case of conversion error */ static R unwrap(JSContext * ctx, JSValueConst v); /** Create JSValue from an object of type R and JSContext. * This function is intentionally not implemented. User should implement this function for their own type. * @return Returns JSValue which should be freed by the caller or JS_EXCEPTION in case of error. */ static JSValue wrap(JSContext * ctx, R value); }; /** Conversion traits for JSValue (identity). */ template <> struct js_traits { static JSValue unwrap(JSContext * ctx, JSValueConst v) noexcept { return JS_DupValue(ctx, v); } static JSValue wrap(JSContext * ctx, JSValue v) noexcept { return v; } }; /** Conversion traits for integers. */ template struct js_traits && sizeof(Int) <= sizeof(int64_t)>> { /// @throws exception static Int unwrap(JSContext * ctx, JSValueConst v) { if constexpr (sizeof(Int) > sizeof(int32_t)) { int64_t r; if(JS_ToInt64(ctx, &r, v)) throw exception{}; return static_cast(r); } else { int32_t r; if(JS_ToInt32(ctx, &r, v)) throw exception{}; return static_cast(r); } } static JSValue wrap(JSContext * ctx, Int i) noexcept { if constexpr (std::is_same_v || sizeof(Int) > sizeof(int32_t)) return JS_NewInt64(ctx, static_cast(i)); else return JS_NewInt32(ctx, static_cast(i)); } }; /** Conversion traits for boolean. */ template <> struct js_traits { static bool unwrap(JSContext * ctx, JSValueConst v) noexcept { return JS_ToBool(ctx, v); } static JSValue wrap(JSContext * ctx, bool i) noexcept { return JS_NewBool(ctx, i); } }; /** Conversion trait for void. */ template <> struct js_traits { /// @throws exception if jsvalue is neither undefined nor null static void unwrap(JSContext * ctx, JSValueConst value) { if(JS_IsException(value)) throw exception{}; } }; /** Conversion traits for float64/double. */ template <> struct js_traits { /// @throws exception static double unwrap(JSContext * ctx, JSValueConst v) { double r; if(JS_ToFloat64(ctx, &r, v)) throw exception{}; return r; } static JSValue wrap(JSContext * ctx, double i) noexcept { return JS_NewFloat64(ctx, i); } }; namespace detail { /** Fake std::string_view which frees the string on destruction. */ class js_string : public std::string_view { using Base = std::string_view; JSContext * ctx = nullptr; friend struct js_traits; js_string(JSContext * ctx, const char * ptr, std::size_t len) : Base(ptr, len), ctx(ctx) {} public: template js_string(Args&& ... args) : Base(std::forward(args)...), ctx(nullptr) {} js_string(const js_string& other) = delete; operator const char * () const { return this->data(); } ~js_string() { if(ctx) JS_FreeCString(ctx, this->data()); } }; } // namespace detail /** Conversion traits from std::string_view and to detail::js_string. */ template <> struct js_traits { static detail::js_string unwrap(JSContext * ctx, JSValueConst v) { size_t plen; const char * ptr = JS_ToCStringLen(ctx, &plen, v); if(!ptr) throw exception{}; return detail::js_string{ctx, ptr, plen}; } static JSValue wrap(JSContext * ctx, std::string_view str) noexcept { return JS_NewStringLen(ctx, str.data(), str.size()); } }; /** Conversion traits for std::string */ template <> // slower struct js_traits { static std::string unwrap(JSContext * ctx, JSValueConst v) { auto str_view = js_traits::unwrap(ctx, v); return std::string{str_view.data(), str_view.size()}; } static JSValue wrap(JSContext * ctx, const std::string& str) noexcept { return JS_NewStringLen(ctx, str.data(), str.size()); } }; /** Conversion from const char * */ template <> struct js_traits { static JSValue wrap(JSContext * ctx, const char * str) noexcept { return JS_NewString(ctx, str); } static detail::js_string unwrap(JSContext * ctx, JSValueConst v) { return js_traits::unwrap(ctx, v); } }; namespace detail { /** Helper function to convert and then free JSValue. */ template T unwrap_free(JSContext * ctx, JSValue val) { if constexpr(std::is_same_v) { JS_FreeValue(ctx, val); return js_traits::unwrap(ctx, val); } else { try { T result = js_traits>::unwrap(ctx, val); JS_FreeValue(ctx, val); return result; } catch(...) { JS_FreeValue(ctx, val); throw; } } } template Tuple unwrap_args_impl(JSContext * ctx, JSValueConst * argv, std::index_sequence) { return Tuple{js_traits>>::unwrap(ctx, argv[I])...}; } /** Helper function to convert an array of JSValues to a tuple. * @tparam Args C++ types of the argv array */ template std::tuple...> unwrap_args(JSContext * ctx, JSValueConst * argv) { return unwrap_args_impl...>>(ctx, argv, std::make_index_sequence()); } /** Helper function to call f with an array of JSValues. * @tparam R return type of f * @tparam Args argument types of f * @tparam Callable type of f (inferred) * @param ctx JSContext * @param f callable object * @param argv array of JSValue's * @return converted return value of f or JS_NULL if f returns void */ template JSValue wrap_call(JSContext * ctx, Callable&& f, JSValueConst * argv) noexcept { try { if constexpr(std::is_same_v) { std::apply(std::forward(f), unwrap_args(ctx, argv)); return JS_NULL; } else { return js_traits>::wrap(ctx, std::apply(std::forward(f), unwrap_args(ctx, argv))); } } catch(exception) { return JS_EXCEPTION; } } /** Same as wrap_call, but pass this_value as first argument. * @tparam FirstArg type of this_value */ template JSValue wrap_this_call(JSContext * ctx, Callable&& f, JSValueConst this_value, JSValueConst * argv) noexcept { try { if constexpr(std::is_same_v) { std::apply(std::forward(f), std::tuple_cat(unwrap_args(ctx, &this_value), unwrap_args(ctx, argv))); return JS_NULL; } else { return js_traits>::wrap(ctx, std::apply(std::forward(f), std::tuple_cat( unwrap_args(ctx, &this_value), unwrap_args(ctx, argv)))); } } catch(exception) { return JS_EXCEPTION; } } template void wrap_args_impl(JSContext * ctx, JSValue * argv, Tuple tuple, std::index_sequence) { ((argv[I] = js_traits>>::wrap(ctx, std::get(tuple))), ...); } /** Converts C++ args to JSValue array. * @tparam Args argument types * @param argv array of size at least sizeof...(Args) */ template void wrap_args(JSContext * ctx, JSValue * argv, Args&& ... args) { wrap_args_impl(ctx, argv, std::make_tuple(std::forward(args)...), std::make_index_sequence()); } } // namespace detail /** A wrapper type for free and class member functions. * Pointer to function F is a template argument. * @tparam F either a pointer to free function or a pointer to class member function * @tparam PassThis if true and F is a pointer to free function, passes Javascript "this" value as first argument: */ template struct fwrapper { /// "name" property of the JS function object (not defined if nullptr) const char * name = nullptr; }; /** Conversion to JSValue for free function in fwrapper. */ template struct js_traits> { static JSValue wrap(JSContext * ctx, fwrapper fw) noexcept { return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) noexcept -> JSValue { if constexpr(PassThis) return detail::wrap_this_call(ctx, F, this_value, argv); else return detail::wrap_call(ctx, F, argv); }, fw.name, sizeof...(Args)); } }; /** Conversion to JSValue for class member function in fwrapper. */ template struct js_traits> { static JSValue wrap(JSContext * ctx, fwrapper fw) noexcept { return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) noexcept -> JSValue { return detail::wrap_this_call, Args...>(ctx, F, this_value, argv); }, fw.name, sizeof...(Args)); } }; /** Conversion to JSValue for const class member function in fwrapper. */ template struct js_traits> { static JSValue wrap(JSContext * ctx, fwrapper fw) noexcept { return JS_NewCFunction(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) noexcept -> JSValue { return detail::wrap_this_call, Args...>(ctx, F, this_value, argv); }, fw.name, sizeof...(Args)); } }; /** A wrapper type for constructor of type T with arguments Args. * Compilation fails if no such constructor is defined. * @tparam Args constructor arguments */ template struct ctor_wrapper { static_assert(std::is_constructible::value, "no such constructor!"); /// "name" property of JS constructor object const char * name = nullptr; }; /** Conversion to JSValue for ctor_wrapper. */ template struct js_traits> { static JSValue wrap(JSContext * ctx, ctor_wrapper cw) noexcept { return JS_NewCFunction2(ctx, [](JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) noexcept -> JSValue { if(js_traits>::QJSClassId == 0) // not registered { #if defined(__cpp_rtti) // automatically register class on first use (no prototype) js_traits>::register_class(ctx, typeid(T).name()); #else JS_ThrowTypeError(ctx, "quickjspp ctor_wrapper::wrap: Class is not registered"); return JS_EXCEPTION; #endif } auto proto = JS_GetPropertyStr(ctx, this_value, "prototype"); if (JS_IsException(proto)) return proto; auto jsobj = JS_NewObjectProtoClass(ctx, proto, js_traits>::QJSClassId); JS_FreeValue(ctx, proto); if (JS_IsException(jsobj)) return jsobj; std::shared_ptr ptr = std::apply(std::make_shared, detail::unwrap_args(ctx, argv)); JS_SetOpaque(jsobj, new std::shared_ptr(std::move(ptr))); return jsobj; // return detail::wrap_call, Args...>(ctx, std::make_shared, argv); }, cw.name, sizeof...(Args), JS_CFUNC_constructor, 0); } }; /** Conversions for std::shared_ptr. * T should be registered to a context before conversions. * @tparam T class type */ template struct js_traits> { /// Registered class id in QuickJS. inline static JSClassID QJSClassId = 0; /** Register class in QuickJS context. * * @param ctx context * @param name class name * @param proto class prototype or JS_NULL * @throws exception */ static void register_class(JSContext * ctx, const char * name, JSValue proto = JS_NULL) { if(QJSClassId == 0) { JS_NewClassID(&QJSClassId); } auto rt = JS_GetRuntime(ctx); if(!JS_IsRegisteredClass(rt, QJSClassId)) { JSClassDef def{ name, // destructor [](JSRuntime * rt, JSValue obj) noexcept { auto pptr = reinterpret_cast *>(JS_GetOpaque(obj, QJSClassId)); delete pptr; } }; int e = JS_NewClass(rt, QJSClassId, &def); if(e < 0) { JS_ThrowInternalError(ctx, "Cant register class %s", name); throw exception{}; } } JS_SetClassProto(ctx, QJSClassId, proto); } /** Create a JSValue from std::shared_ptr. * Creates an object with class if #QJSClassId and sets its opaque pointer to a new copy of #ptr. */ static JSValue wrap(JSContext * ctx, std::shared_ptr ptr) { if(QJSClassId == 0) // not registered { #if defined(__cpp_rtti) // automatically register class on first use (no prototype) register_class(ctx, typeid(T).name()); #else JS_ThrowTypeError(ctx, "quickjspp std::shared_ptr::wrap: Class is not registered"); return JS_EXCEPTION; #endif } auto jsobj = JS_NewObjectClass(ctx, QJSClassId); if(JS_IsException(jsobj)) return jsobj; auto pptr = new std::shared_ptr(std::move(ptr)); JS_SetOpaque(jsobj, pptr); return jsobj; } /// @throws exception if #v doesn't have the correct class id static const std::shared_ptr& unwrap(JSContext * ctx, JSValueConst v) { auto ptr = reinterpret_cast *>(JS_GetOpaque2(ctx, v, QJSClassId)); if(!ptr) throw exception{}; return *ptr; } }; // T * - non-owning pointer template struct js_traits { static JSValue wrap(JSContext * ctx, T * ptr) { if(js_traits>::QJSClassId == 0) // not registered { #if defined(__cpp_rtti) js_traits>::register_class(ctx, typeid(T).name()); #else JS_ThrowTypeError(ctx, "quickjspp js_traits::wrap: Class is not registered"); return JS_EXCEPTION; #endif } auto jsobj = JS_NewObjectClass(ctx, js_traits>::QJSClassId); if(JS_IsException(jsobj)) return jsobj; // shared_ptr with empty deleter since we don't own T* auto pptr = new std::shared_ptr(ptr, [](T *){}); JS_SetOpaque(jsobj, pptr); return jsobj; } static T * unwrap(JSContext * ctx, JSValueConst v) { auto ptr = reinterpret_cast *>(JS_GetOpaque2(ctx, v, js_traits>::QJSClassId)); if(!ptr) throw exception{}; return ptr->get(); } }; namespace detail { /** A faster std::function-like object with type erasure. * Used to convert any callable objects (including lambdas) to JSValue. */ struct function { JSValue (* invoker)(function * self, JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) = nullptr; void (* destroyer)(function * self) = nullptr; alignas(std::max_align_t) char functor[]; template static function * create(JSRuntime * rt, Functor&& f) { auto fptr = reinterpret_cast(js_malloc_rt(rt, sizeof(function) + sizeof(Functor))); if(!fptr) throw std::bad_alloc{}; new(fptr) function; auto functorptr = reinterpret_cast(fptr->functor); new(functorptr) Functor(std::forward(f)); fptr->destroyer = nullptr; if constexpr(!std::is_trivially_destructible_v) { fptr->destroyer = [](function * fptr) { auto functorptr = reinterpret_cast(fptr->functor); functorptr->~Functor(); }; } return fptr; } }; static_assert(std::is_trivially_destructible_v); } template <> struct js_traits { inline static JSClassID QJSClassId = 0; // TODO: replace ctx with rt static void register_class(JSContext * ctx, const char * name) { if(QJSClassId == 0) { JS_NewClassID(&QJSClassId); } auto rt = JS_GetRuntime(ctx); if(JS_IsRegisteredClass(rt, QJSClassId)) return; JSClassDef def{ name, // destructor [](JSRuntime * rt, JSValue obj) noexcept { auto fptr = reinterpret_cast(JS_GetOpaque(obj, QJSClassId)); assert(fptr); if(fptr->destroyer) fptr->destroyer(fptr); js_free_rt(rt, fptr); }, nullptr, // mark // call [](JSContext * ctx, JSValueConst func_obj, JSValueConst this_val, int argc, JSValueConst * argv, int flags) -> JSValue { auto ptr = reinterpret_cast(JS_GetOpaque2(ctx, func_obj, QJSClassId)); if(!ptr) return JS_EXCEPTION; return ptr->invoker(ptr, ctx, this_val, argc, argv); } }; int e = JS_NewClass(rt, QJSClassId, &def); if(e < 0) throw std::runtime_error{"Cannot register C++ function class"}; } }; /** Traits for accessing object properties. * @tparam Key property key type (uint32 and strings are supported) */ template struct js_property_traits { static void set_property(JSContext * ctx, JSValue this_obj, Key key, JSValue value); static JSValue get_property(JSContext * ctx, JSValue this_obj, Key key); }; template <> struct js_property_traits { static void set_property(JSContext * ctx, JSValue this_obj, const char * name, JSValue value) { int err = JS_SetPropertyStr(ctx, this_obj, name, value); if(err < 0) throw exception{}; } static JSValue get_property(JSContext * ctx, JSValue this_obj, const char * name) noexcept { return JS_GetPropertyStr(ctx, this_obj, name); } }; template <> struct js_property_traits { static void set_property(JSContext * ctx, JSValue this_obj, uint32_t idx, JSValue value) { int err = JS_SetPropertyUint32(ctx, this_obj, idx, value); if(err < 0) throw exception{}; } static JSValue get_property(JSContext * ctx, JSValue this_obj, uint32_t idx) noexcept { return JS_GetPropertyUint32(ctx, this_obj, idx); } }; class Value; namespace detail { template struct property_proxy { JSContext * ctx; JSValue this_obj; Key key; /** Conversion helper function */ template T as() const { return unwrap_free(ctx, js_property_traits::get_property(ctx, this_obj, key)); } /** Explicit conversion operator (to any type) */ template explicit operator T() const { return as(); } /** Implicit converion to qjs::Value */ operator Value() const; // defined later due to Value being incomplete type template property_proxy& operator =(Value value) { js_property_traits::set_property(ctx, this_obj, key, js_traits::wrap(ctx, std::move(value))); return *this; } }; // class member variable getter/setter template struct get_set {}; template struct get_set { using is_const = std::is_const; static const R& get(const std::shared_ptr& ptr) { return *ptr.*M; } static R& set(const std::shared_ptr& ptr, R value) { return *ptr.*M = std::move(value); } }; } // namespace detail /** JSValue with RAAI semantics. * A wrapper over (JSValue v, JSContext * ctx). * Calls JS_FreeValue(ctx, v) on destruction. Can be copied and moved. * A JSValue can be released by either JSValue x = std::move(value); or JSValue x = value.release(), then the Value becomes invalid and FreeValue won't be called * Can be converted to C++ type, for example: auto string = value.as(); qjs::exception would be thrown on error * Properties can be accessed (read/write): value["property1"] = 1; value[2] = "2"; */ class Value { public: JSValue v; JSContext * ctx = nullptr; public: /** Use context.newValue(val) instead */ template Value(JSContext * ctx, T&& val) : ctx(ctx) { v = js_traits>::wrap(ctx, std::forward(val)); if(JS_IsException(v)) throw exception{}; } Value(const Value& rhs) { ctx = rhs.ctx; v = JS_DupValue(ctx, rhs.v); } Value(Value&& rhs) { std::swap(ctx, rhs.ctx); v = rhs.v; } Value& operator=(Value rhs) { std::swap(ctx, rhs.ctx); std::swap(v, rhs.v); return *this; } bool operator==(JSValueConst other) const { return JS_VALUE_GET_TAG(v) == JS_VALUE_GET_TAG(other) && JS_VALUE_GET_PTR(v) == JS_VALUE_GET_PTR(other); } bool operator!=(JSValueConst other) const { return !((*this) == other); } /** Returns true if 2 values are the same (equality for arithmetic types or point to the same object) */ bool operator==(const Value& rhs) const { return ctx == rhs.ctx && (*this == rhs.v); } bool operator!=(const Value& rhs) const { return !((*this) == rhs); } ~Value() { if(ctx) JS_FreeValue(ctx, v); } bool isError() const { return JS_IsError(ctx, v); } /** Conversion helper function. Both value.as() and static_cast(value) are supported */ template T as() const { return js_traits>::unwrap(ctx, v); } /** Explicit conversion to any type */ template explicit operator T() const { return as(); } JSValue release() // dont call freevalue { ctx = nullptr; return v; } /** Implicit conversion to JSValue (rvalue only). Example: JSValue v = std::move(value); */ operator JSValue() && { return release(); } /** Access JS properties. Returns proxy type which is implicitly convertible to qjs::Value */ template detail::property_proxy operator [](Key key) { return {ctx, v, std::move(key)}; } // add("f", []() {...}); template Value& add(const char * name, Function&& f) { (*this)[name] = js_traits(f)})>::wrap(ctx, std::forward(f)); return *this; } // add<&f>("f"); // add<&T::f>("f"); template std::enable_if_t, Value&> add(const char * name) { (*this)[name] = fwrapper{name}; return *this; } // add<&T::member>("member"); template std::enable_if_t, Value&> add(const char * name) { auto prop = JS_NewAtom(ctx, name); using fgetter = fwrapper::get, true>; int ret; if constexpr (detail::get_set::is_const::value) { ret = JS_DefinePropertyGetSet(ctx, v, prop, js_traits::wrap(ctx, fgetter{name}), JS_UNDEFINED, JS_PROP_CONFIGURABLE | JS_PROP_ENUMERABLE ); } else { using fsetter = fwrapper::set, true>; ret = JS_DefinePropertyGetSet(ctx, v, prop, js_traits::wrap(ctx, fgetter{name}), js_traits::wrap(ctx, fsetter{name}), JS_PROP_CONFIGURABLE | JS_PROP_WRITABLE | JS_PROP_ENUMERABLE ); } JS_FreeAtom(ctx, prop); if(ret < 0) throw exception{}; return *this; } std::string toJSON(const Value& replacer = Value{nullptr, JS_UNDEFINED}, const Value& space = Value{nullptr, JS_UNDEFINED}) { assert(ctx); assert(!replacer.ctx || ctx == replacer.ctx); assert(!space.ctx || ctx == space.ctx); JSValue json = JS_JSONStringify(ctx, v, replacer.v, space.v); return (std::string)Value{ctx, json}; } }; /** Thin wrapper over JSRuntime * rt * Calls JS_FreeRuntime on destruction. noncopyable. */ class Runtime { public: JSRuntime * rt; Runtime() { rt = JS_NewRuntime(); if(!rt) throw std::runtime_error{"qjs: Cannot create runtime"}; } // noncopyable Runtime(const Runtime&) = delete; ~Runtime() { JS_FreeRuntime(rt); } }; /** Wrapper over JSContext * ctx * Calls JS_SetContextOpaque(ctx, this); on construction and JS_FreeContext on destruction */ class Context { public: JSContext * ctx; /** Module wrapper * Workaround for lack of opaque pointer for module load function by keeping a list of modules in qjs::Context. */ class Module { friend class Context; JSModuleDef * m; JSContext * ctx; const char * name; using nvp = std::pair; std::vector exports; public: Module(JSContext * ctx, const char * name) : ctx(ctx), name(name) { m = JS_NewCModule(ctx, name, [](JSContext * ctx, JSModuleDef * m) noexcept { auto& context = Context::get(ctx); auto it = std::find_if(context.modules.begin(), context.modules.end(), [m](const Module& module) { return module.m == m; }); if(it == context.modules.end()) return -1; for(const auto& e : it->exports) { if(JS_SetModuleExport(ctx, m, e.first, JS_DupValue(ctx, e.second.v)) != 0) return -1; } return 0; }); if(!m) throw exception{}; } Module& add(const char * name, JSValue value) { exports.push_back({name, {ctx, value}}); JS_AddModuleExport(ctx, m, name); return *this; } Module& add(const char * name, Value value) { assert(value.ctx == ctx); exports.push_back({name, std::move(value)}); JS_AddModuleExport(ctx, m, name); return *this; } template Module& add(const char * name, T value) { return add(name, js_traits::wrap(ctx, std::move(value))); } Module(const Module&) = delete; Module(Module&&) = default; //Module& operator=(Module&&) = default; // function wrappers /** Add free function F. * Example: * module.function(&::sin)>("sin"); */ template Module& function(const char * name) { return add(name, qjs::fwrapper{name}); } /** Add function object f. * Slower than template version. * Example: module.function("sin", [](double x) { return ::sin(x); }); */ template Module& function(const char * name, F&& f) { return add(name, js_traits(f)})>::wrap(std::forward(f))); } // class register wrapper private: /** Helper class to register class members and constructors. * See fun, constructor. * Actual registration occurs at object destruction. */ template class class_registrar { const char * name; qjs::Value prototype; qjs::Context::Module& module; qjs::Context& context; public: explicit class_registrar(const char * name, qjs::Context::Module& module, qjs::Context& context) : name(name), prototype(context.newObject()), module(module), context(context) { } class_registrar(const class_registrar&) = delete; /** Add functional object f */ template class_registrar& fun(const char * name, F&& f) { prototype.add(name, std::forward(f)); return *this; } /** Add class member function or class member variable F * Example: * struct T { int var; int func(); } * auto& module = context.addModule("module"); * module.class_("T").fun<&T::var>("var").fun<&T::func>("func"); */ template class_registrar& fun(const char * name) { prototype.add(name); return *this; } /** Add class constructor * @tparam Args contructor arguments * @param name constructor name (if not specified class name will be used) */ template class_registrar& constructor(const char * name = nullptr) { if(!name) name = this->name; Value ctor = context.newValue(qjs::ctor_wrapper{name}); JS_SetConstructor(context.ctx, ctor.v, prototype.v); module.add(name, std::move(ctor)); return *this; } /* TODO: needs casting to base class template class_registrar& base() { assert(js_traits>::QJSClassId && "base class is not registered"); auto base_proto = JS_GetClassProto(context.ctx, js_traits>::QJSClassId); int err = JS_SetPrototype(context.ctx, prototype.v, base_proto); JS_FreeValue(context.ctx, base_proto); if(err < 0) throw exception{}; return *this; } */ ~class_registrar() { context.registerClass(name, std::move(prototype)); } }; public: /** Add class to module. * See \ref class_registrar. */ template class_registrar class_(const char * name) { return class_registrar{name, *this, qjs::Context::get(ctx)}; } }; std::vector modules; private: void init() { JS_SetContextOpaque(ctx, this); js_traits::register_class(ctx, "C++ function"); } public: Context(Runtime& rt) : Context(rt.rt) {} Context(JSRuntime * rt) { ctx = JS_NewContext(rt); if(!ctx) throw std::runtime_error{"qjs: Cannot create context"}; init(); } Context(JSContext * ctx) : ctx{ctx} { init(); } // noncopyable Context(const Context&) = delete; ~Context() { modules.clear(); JS_FreeContext(ctx); } /** Create module and return a reference to it */ Module& addModule(const char * name) { modules.emplace_back(ctx, name); return modules.back(); } /** returns globalThis */ Value global() { return Value{ctx, JS_GetGlobalObject(ctx)}; } /** returns new Object() */ Value newObject() { return Value{ctx, JS_NewObject(ctx)}; } /** returns JS value converted from c++ object val */ template Value newValue(T&& val) { return Value{ctx, std::forward(val)}; } /** returns current exception associated with context, and resets it. Should be called when qjs::exception is caught */ Value getException() { return Value{ctx, JS_GetException(ctx)}; } /** Register class T for conversions to/from std::shared_ptr to work. * Wherever possible module.class_("T")... should be used instead. * @tparam T class type * @param name class name in JS engine * @param proto JS class prototype or JS_UNDEFINED */ template void registerClass(const char * name, JSValue proto = JS_NULL) { js_traits>::register_class(ctx, name, proto); } Value eval(std::string_view buffer, const char * filename = "", unsigned eval_flags = 0) { assert(buffer.data()[buffer.size()] == '\0' && "eval buffer is not null-terminated"); // JS_Eval requirement JSValue v = JS_Eval(ctx, buffer.data(), buffer.size(), filename, eval_flags); return Value{ctx, v}; } Value evalFile(const char * filename, unsigned eval_flags = 0) { size_t buf_len; auto deleter = [this](void * p) { js_free(ctx, p); }; auto buf = std::unique_ptr{js_load_file(ctx, &buf_len, filename), deleter}; if(!buf) throw std::runtime_error{std::string{"evalFile: can't read file: "} + filename}; return eval({reinterpret_cast(buf.get()), buf_len}, filename, eval_flags); } Value fromJSON(std::string_view buffer, const char * filename = "") { assert(buffer.data()[buffer.size()] == '\0' && "fromJSON buffer is not null-terminated"); // JS_ParseJSON requirement JSValue v = JS_ParseJSON(ctx, buffer.data(), buffer.size(), filename); return Value{ctx, v}; } /** Get qjs::Context from JSContext opaque pointer */ static Context& get(JSContext * ctx) { void * ptr = JS_GetContextOpaque(ctx); assert(ptr); return *reinterpret_cast(ptr); } }; /** Conversion traits for Value. */ template <> struct js_traits { static Value unwrap(JSContext * ctx, JSValueConst v) { return Value{ctx, JS_DupValue(ctx, v)}; } static JSValue wrap(JSContext * ctx, Value v) noexcept { assert(ctx == v.ctx); return v.release(); } }; /** Convert to/from std::function * @tparam R return type * @tparam Args argument types */ template struct js_traits> { static std::function unwrap(JSContext * ctx, JSValueConst fun_obj) { return [jsfun_obj = Value{ctx, JS_DupValue(ctx, fun_obj)}](Args&& ... args) -> R { const int argc = sizeof...(Args); JSValue argv[argc]; detail::wrap_args(jsfun_obj.ctx, argv, std::forward(args)...); JSValue result = JS_Call(jsfun_obj.ctx, jsfun_obj.v, JS_UNDEFINED, argc, const_cast(argv)); for(int i = 0; i < argc; i++) JS_FreeValue(jsfun_obj.ctx, argv[i]); return detail::unwrap_free(jsfun_obj.ctx, result); }; } /** Convert from function object functor to JSValue. * Uses detail::function for type-erasure. */ template static JSValue wrap(JSContext * ctx, Functor&& functor) { using detail::function; assert(js_traits::QJSClassId); auto obj = JS_NewObjectClass(ctx, js_traits::QJSClassId); if(JS_IsException(obj)) return JS_EXCEPTION; auto fptr = function::create(JS_GetRuntime(ctx), std::forward(functor)); fptr->invoker = [](function * self, JSContext * ctx, JSValueConst this_value, int argc, JSValueConst * argv) { assert(self); auto f = reinterpret_cast(&self->functor); return detail::wrap_call(ctx, *f, argv); }; JS_SetOpaque(obj, fptr); return obj; } }; /** Convert from std::vector to Array and vice-versa. If Array holds objects that are non-convertible to T throws qjs::exception */ template struct js_traits> { static JSValue wrap(JSContext * ctx, const std::vector& arr) noexcept { auto jsarray = Value{ctx, JS_NewArray(ctx)}; if(JS_IsException(jsarray.v)) return jsarray.v; try { for(uint32_t i = 0; i < (uint32_t) arr.size(); i++) jsarray[i] = arr[i]; } catch(exception) { return JS_EXCEPTION; } return std::move(jsarray); } static std::vector unwrap(JSContext * ctx, JSValueConst jsarr) { int e = JS_IsArray(ctx, jsarr); if(e == 0) JS_ThrowTypeError(ctx, "js_traits>::unwrap expects array"); if(e <= 0) throw exception{}; Value jsarray{ctx, JS_DupValue(ctx, jsarr)}; std::vector arr; auto len = static_cast(jsarray["length"]); arr.reserve((uint32_t) len); for(uint32_t i = 0; i < (uint32_t) len; i++) arr.push_back(static_cast(jsarray[i])); return arr; } }; namespace detail { template property_proxy::operator Value() const { return as(); } } } // namespace qjs