diff --git a/CMakeLists.txt b/CMakeLists.txt index 82f366570..a860128e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -532,6 +532,13 @@ if(QJS_BUILD_EXAMPLES) set_target_properties(test_fib PROPERTIES ENABLE_EXPORTS TRUE) endif() +# Test c++ +option(TEST_CXX "Build tests" ON) + +if(TEST_CXX) + add_subdirectory(tests-cpp) +endif() + # Install target # @@ -548,6 +555,7 @@ if (QJS_ENABLE_INSTALL) SOVERSION ${QJS_VERSION_MAJOR} ) install(FILES quickjs.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + install(FILES quickjs.hpp DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) if(QJS_BUILD_LIBC) install(FILES quickjs-libc.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) endif() diff --git a/quickjs.hpp b/quickjs.hpp new file mode 100644 index 000000000..b473956d7 --- /dev/null +++ b/quickjs.hpp @@ -0,0 +1,400 @@ +#pragma once + +#include +#include +#include +#include + +namespace qjs { + +class Value { +public: + static std::vector makeVector(JSContext *ctx, int argc, + JSValueConst *argv) { + std::vector out; + out.reserve(argc); + + for (int i = 0; i < argc; i++) { + out.emplace_back(ctx, JS_DupValue(ctx, argv[i])); + } + + return out; + } + + void assign(JSContext *ctx, JSValue value, bool takeOwnership = true) { + free(); + ctx_ = ctx; + if (takeOwnership){ + value_ = value; + } else { + value_ = JS_DupValue(ctx, value); + } + } + + void assign(JSContext *ctx, double value){ + assign(ctx, JS_NewFloat64(ctx, value)); + } + + void assign(JSContext *ctx, const std::string& value){ + assign(ctx, JS_NewString(ctx, value.c_str())); + } + + void assignArray(JSContext *ctx){ + assign(ctx, JS_NewArray(ctx)); + } + + void assignObject(JSContext *ctx){ + assign(ctx, JS_NewObject(ctx)); + } + + #define ASSIGN_MOVE(val) { \ + assign(val.ctx_, val.value_, false); \ + val.value_ = JS_UNDEFINED; \ + val.ctx_ = nullptr; \ + } + + template Value(JSContext *ctx, T t){ + assign(ctx, t); + } + + void assign(const Value &val) { + assign(val.ctx_, val.value_, false); + } + + Value(){ + } + + Value(JSContext *ctx, JSValue value, bool takeOwnership = true) { + assign(ctx, value, takeOwnership); + } + + Value &operator=(Value &&val) noexcept { + ASSIGN_MOVE(val); + return *this; + } + + Value &operator=(const Value &val) { + assign(val); + return *this; + } + + Value(const Value &val) { assign(val); }; + + Value(Value &&val) noexcept { ASSIGN_MOVE(val); } + + JSContext *context() const { return ctx_; } + + JSValue raw() const { return value_; } + + JSValue rawdup() const { return JS_DupValue(ctx_, value_); } + + bool set(size_t index, qjs::Value value){ + if (isArray()){ + auto ctx = context(); + auto val = raw(); + JS_SetPropertyUint32(ctx, val, index, value.rawdup()); + return true; + } else { + return false; + } + } + + bool set(std::string index, qjs::Value value){ + if (isObject()){ + JS_SetPropertyStr(context(), raw(), index.c_str(), value.rawdup()); + return true; + } else { + return false; + } + } + + template bool set(size_t index, T value){ + return set(index, qjs::Value(ctx_, value)); + } + + template bool set(std::string index, T value){ + return set(index, qjs::Value(ctx_, value)); + } + + qjs::Value get(size_t index){ + if (isArray()){ + auto ctx = context(); + auto new_val = JS_GetPropertyUint32(ctx, raw(), index); + return Value(ctx, new_val); + } else { + return Value(nullptr, JS_UNDEFINED); + } + } + + qjs::Value get(const std::string & name){ + if (isObject()){ + auto ctx = context(); + auto new_val = JS_GetPropertyStr(ctx, raw(), name.c_str()); + return Value(ctx, new_val); + } else { + return Value(nullptr, JS_UNDEFINED); + } + } + + template bool is() const; + + template T as() const; + + bool isException() const { return JS_IsException(raw()); } + + bool isUndefined() const { return JS_IsUndefined(raw()); } + + bool isNull() const { return JS_IsNull(raw()); } + + bool isBool() const { return JS_IsBool(raw()); } + + bool isNumber() const { return JS_IsNumber(raw()); } + + bool isString() const { return JS_IsString(raw()); } + + bool isObject() const { return JS_IsObject(raw()); } + + bool isFunction() const { return JS_IsFunction(context(), raw()); } + + bool isArray() const { return JS_IsArray(raw()); } + + bool isSymbol() const { return JS_VALUE_GET_TAG(raw()) == JS_TAG_SYMBOL; } + + bool isBigInt() const { return JS_VALUE_GET_TAG(raw()) == JS_TAG_BIG_INT; } + + ~Value() { free(); } + +private: + void free() { + if (ctx_ != nullptr) { + JS_FreeValue(ctx_, value_); + ctx_ = nullptr; + } + } + JSContext *ctx_ = nullptr; + JSValue value_ = JS_UNDEFINED; +}; + +class Runtime { +public: + Runtime() { + rt_ = JS_NewRuntime(); + if (!rt_) + throw std::runtime_error("JS_NewRuntime failed"); + } + + ~Runtime() { + if (rt_) + JS_FreeRuntime(rt_); + } + + Runtime(const Runtime &) = delete; + Runtime &operator=(const Runtime &) = delete; + + Runtime(Runtime &&other) noexcept : rt_(other.rt_) { other.rt_ = nullptr; } + + Runtime &operator=(Runtime &&other) noexcept { + if (this != &other) { + if (rt_) + JS_FreeRuntime(rt_); + rt_ = other.rt_; + other.rt_ = nullptr; + } + return *this; + } + + JSRuntime *get() const { return rt_; } + +private: + JSRuntime *rt_{}; +}; + +class Context { +public: + explicit Context(Runtime &runtime) { + ctx_ = JS_NewContext(runtime.get()); + if (!ctx_) + throw std::runtime_error("JS_NewContext failed"); + } + + ~Context() { + if (ctx_) + JS_FreeContext(ctx_); + } + + Context(const Context &) = delete; + Context &operator=(const Context &) = delete; + + JSContext *get() const { return ctx_; } + + template qjs::Value newValue(T t){ + return qjs::Value(ctx_, t); + } + + qjs::Value newObject() { + qjs::Value val; + val.assignObject(ctx_); + return val; + } + + qjs::Value newArray() { + qjs::Value val; + val.assignArray(ctx_); + return val; + } + + qjs::Value eval(const std::string &code, + const std::string &filename = "") { + return Value(ctx_, JS_Eval(ctx_, code.c_str(), code.size(), + filename.c_str(), JS_EVAL_TYPE_GLOBAL)); + } + +private: + JSContext *ctx_{}; +}; + +template <> inline bool Value::is>() const { + return isArray(); +} + +template <> inline bool Value::is>() const { + return isObject(); +} + +template <> inline bool Value::is() const { return isNull(); } + +template <> inline bool Value::is() const { return isBool(); } + +template <> inline bool Value::is() const { + return isNumber(); +} + +template <> inline bool Value::is() const { + return isNumber(); +} + +template <> inline bool Value::is() const { return isNumber(); } + +template <> inline bool Value::is() const { return isString(); } + +class Exception : public std::runtime_error { +public: + explicit Exception(JSContext *ctx, const std::string &error = "") : std::runtime_error(extract(ctx, error)) {} + +private: + static std::string extract(JSContext *ctx, const std::string& error) { + JSValue exc = JS_GetException(ctx); + + JSAtom atom = JS_NewAtom(ctx, "stack"); + std::string result; + + JSValue stack = JS_GetProperty(ctx, exc, atom); + + JS_FreeAtom(ctx, atom); + + if (!JS_IsUndefined(stack)) { + const char *s = JS_ToCString(ctx, stack); + + if (s) { + result = s; + JS_FreeCString(ctx, s); + } + } + + { + const char *s = JS_ToCString(ctx, exc); + + if (s) { + result += s; + JS_FreeCString(ctx, s); + } + } + + JS_FreeValue(ctx, stack); + JS_FreeValue(ctx, exc); + if (error.empty()){ + return result; + } else { + return error + " " + result; + } + } +}; + +template <> inline std::string Value::as() const { + auto ctx_ = context(); + const char *str = JS_ToCString(context(), raw()); + + if (!str) + throw Exception(ctx_); + + std::string result(str); + + JS_FreeCString(ctx_, str); + + return result; +} +/* +template <> inline std::nullptr_t Value::as() const { + if (!JS_IsNull(raw())) + throw std::bad_cast(); + + return nullptr; +}*/ + +template <> inline bool Value::as() const { + auto value_ = raw(); + auto ctx_ = context(); + int r = JS_ToBool(ctx_, value_); + + if (r < 0) + throw Exception(ctx_); + + return r; +} + +template <> inline int32_t Value::as() const { + int32_t v; + auto value_ = raw(); + auto ctx_ = context(); + + if (JS_ToInt32(ctx_, &v, value_)) + throw Exception(ctx_); + + return v; +} + +template <> inline int64_t Value::as() const { + int64_t v; + auto value_ = raw(); + auto ctx_ = context(); + + if (JS_ToInt64(ctx_, &v, value_)) + throw Exception(ctx_); + + return v; +} + +template <> inline double Value::as() const { + double v; + auto value_ = raw(); + auto ctx_ = context(); + + if (JS_ToFloat64(ctx_, &v, value_)) + throw Exception(ctx_); + + return v; +} + +inline void throwIfException(JSContext *ctx, JSValueConst value) { + if (!JS_IsException(value)){ + return; + } + + throw qjs::Exception(ctx); +} + +inline void throwIfException(JSContext *ctx, qjs::Value &value) { + throwIfException(ctx, value.raw()); +} + +} // namespace qjs diff --git a/tests-cpp/CMakeLists.txt b/tests-cpp/CMakeLists.txt new file mode 100644 index 000000000..3c62e8c76 --- /dev/null +++ b/tests-cpp/CMakeLists.txt @@ -0,0 +1,18 @@ +project(tests_cpp + VERSION 0.1.0 + LANGUAGES CXX) + +add_executable(test_eval + test_eval.cpp +) + +target_link_libraries(test_eval + PRIVATE + qjs +) + +add_custom_command( + TARGET test_eval + POST_BUILD + COMMAND test_eval +) diff --git a/tests-cpp/test_eval.cpp b/tests-cpp/test_eval.cpp new file mode 100644 index 000000000..065161d41 --- /dev/null +++ b/tests-cpp/test_eval.cpp @@ -0,0 +1,57 @@ +#include +#include +#include + +int main() { + qjs::Runtime rt; + qjs::Context ctx(rt); + + // get 40 + 2 + qjs::Value result = ctx.eval("40 + 2"); + // check if result is assignable + auto result2 = result; + + // check if throw does not throw nothing + qjs::throwIfException(ctx.get(), result); + + // + assert(result.as() == 42); + + // + qjs::Value newval = ctx.newValue(54); + assert(newval.as() == 54); + + // + newval = ctx.newValue("Fineload"); + assert(newval.as() == "Fineload"); + + // + result = ctx.eval("40asd43"); + try{ + qjs::throwIfException(ctx.get(), result); + // + assert(false); + } catch (qjs::Exception e){ + assert (true); + } + + // + assert(ctx.newArray().isArray()); + + // + assert(ctx.newObject().isObject()); + + // + auto obj = ctx.newObject(); + obj.set("name", 1); + + // + assert(obj.get("name").is()); + + // + assert(obj.get("name").as() == 1); + + std::cout << "Tests succeeded" << std::endl; + + return 0; +}