diff --git a/.github/workflows/cxx.yml b/.github/workflows/cxx.yml index c8bc1220e..357262140 100644 --- a/.github/workflows/cxx.yml +++ b/.github/workflows/cxx.yml @@ -8,7 +8,7 @@ jobs: strategy: fail-fast: false matrix: - compiler: [linux-gcc,linux-clang,linux-gcc8,linux-gcc10,linux-gcc-ubsan,linux-gcc-asan,linux-gcc-32,linux-gcc-x32,linux-clang11,linux-clang12,macos-clang] + compiler: [ linux-gcc,linux-clang,linux-gcc8,linux-gcc10,linux-gcc-ubsan,linux-gcc-asan,linux-gcc-32,linux-gcc-x32,linux-clang11,linux-clang12,macos-clang,windows-msvc,windows-msvc-32,windows-clang,windows-clang-32 ] build_type: [Debug] include: - compiler: linux-gcc @@ -21,7 +21,7 @@ jobs: - compiler: linux-gcc8 os: ubuntu-18.04 preconfigure: sudo apt-get update && sudo apt-get install -y gcc-8 g++-8 - cmake_opts: -DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8 -DCMAKE_REQUIRED_LIBRARIES=stdc++fs + cmake_opts: -DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8 - compiler: linux-gcc10 os: ubuntu-20.04 @@ -61,6 +61,20 @@ jobs: - compiler: macos-clang os: macos-latest + - compiler: windows-msvc + os: windows-latest + + - compiler: windows-msvc-32 + os: windows-latest + cmake_opts: -A Win32 + + - compiler: windows-clang + os: windows-latest + cmake_opts: -T ClangCL + + - compiler: windows-clang-32 + os: windows-latest + cmake_opts: -T ClangCL -A Win32 steps: - uses: actions/checkout@v2 with: @@ -70,15 +84,21 @@ jobs: - name: configure run: cmake -S . -B build_dir -Wdev -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_C_FLAGS="${{ matrix.cflags }}" -DCMAKE_CXX_FLAGS="${{ matrix.cflags }}" -DCMAKE_EXE_LINKER_FLAGS="${{ matrix.cflags }}" ${{ matrix.cmake_opts }} - name: build - run: cmake --build build_dir --verbose + run: cmake --build build_dir --verbose --config ${{ matrix.build_type }} - name: test - run: ctest --extra-verbose + run: ctest --extra-verbose --build-config ${{ matrix.build_type }} working-directory: build_dir env: # extra options for address sanitizer ASAN_OPTIONS: strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1 + - name: install + run: cmake --install build_dir --prefix install_prefix --config ${{ matrix.build_type }} + - uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.compiler }} + path: install_prefix - build_windows: + build_mingw: runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -90,9 +110,6 @@ jobs: os: windows-2019 cmake_opts: -G "Ninja" -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -# - compiler: windows-msvs # not supported -# os: windows-latest - defaults: run: shell: msys2 {0} @@ -117,4 +134,10 @@ jobs: working-directory: build_dir env: # extra options for address sanitizer - ASAN_OPTIONS: strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1 \ No newline at end of file + ASAN_OPTIONS: strict_string_checks=1:detect_stack_use_after_return=1:check_initialization_order=1:strict_init_order=1 + - name: install + run: cmake --install build_dir --prefix install_prefix + - uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.compiler }}-${{ matrix.build_type }} + path: install_prefix diff --git a/.github/workflows/valgrind.yml b/.github/workflows/valgrind.yml index 7589b281d..a1450c7f0 100644 --- a/.github/workflows/valgrind.yml +++ b/.github/workflows/valgrind.yml @@ -5,6 +5,10 @@ on: [push, pull_request] jobs: build: runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + build_type: [Debug, Release] steps: - uses: actions/checkout@v2 with: @@ -12,7 +16,7 @@ jobs: - name: install run: sudo apt-get update; sudo apt-get install -y valgrind - name: configure - run: echo 'include(Dart)' >> CMakeLists.txt && cmake -S . -B build_dir -Wdev -DCMAKE_BUILD_TYPE=Debug -DMEMORYCHECK_COMMAND_OPTIONS="--leak-check=full --track-origins=yes --error-exitcode=1" + run: echo 'include(Dart)' >> CMakeLists.txt && cmake -S . -B build_dir -Wdev -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DMEMORYCHECK_COMMAND_OPTIONS="--leak-check=full --track-origins=yes --error-exitcode=1" - name: build run: cmake --build build_dir --verbose - name: test diff --git a/CMakeLists.txt b/CMakeLists.txt index 7842bdc79..1f3df53c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,18 +4,23 @@ project(quickjspp LANGUAGES CXX) #set(CMAKE_CXX_STANDARD 17) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) -if(CMAKE_COMPILER_IS_GNUCC) - add_compile_options(-Wall -Wno-unused-parameter) -endif() - add_subdirectory(quickjs) add_library(quickjspp INTERFACE) -target_link_libraries(quickjspp INTERFACE quickjs ${CMAKE_REQUIRED_LIBRARIES}) -target_compile_features(quickjspp INTERFACE cxx_std_17) +target_link_libraries(quickjspp INTERFACE quickjs) +target_compile_features(quickjspp INTERFACE cxx_std_20) target_include_directories(quickjspp INTERFACE .) set_target_properties(quickjspp PROPERTIES PUBLIC_HEADER quickjspp.hpp) +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + target_compile_options(quickjspp INTERFACE -Wall -Wno-unused-parameter) # enable warnings + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9.0) + target_link_libraries(quickjspp INTERFACE stdc++fs) # required for gcc-8 + endif() +elseif(MSVC) + add_compile_options(/EHsc) # enable exceptions +endif() + add_executable(qjs qjs.cpp) target_link_libraries(qjs quickjspp) diff --git a/quickjs/CMakeLists.txt b/quickjs/CMakeLists.txt index 2664223d5..d72d235da 100644 --- a/quickjs/CMakeLists.txt +++ b/quickjs/CMakeLists.txt @@ -1,27 +1,61 @@ project(quickjs LANGUAGES C) -file(STRINGS VERSION version) +if(MSVC) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(arch 64) + elseif(CMAKE_SIZEOF_VOID_P EQUAL 4) + set(arch 32) + endif() -set(quickjs_src quickjs.c libbf.c libunicode.c libregexp.c cutils.c quickjs-libc.c) -set(quickjs_def CONFIG_VERSION="${version}" _GNU_SOURCE CONFIG_BIGNUM) + message(VERBOSE "Compiling QuickJS with MSVC is not supported") + message(VERBOSE "However we can download QuickJS DLL that was cross-compiled with MinGW-w64 by GH-action") + message(VERBOSE "https://nightly.link/ftk/quickjspp/workflows/quickjsdll/master/") + message(VERBOSE "If you don't want to download DLLs from the internet, see .github/workflows/quickjsdll.yml for instructions on how to compile it yourself") -add_library(quickjs ${quickjs_src}) -target_compile_definitions(quickjs PRIVATE ${quickjs_def} ) -set_target_properties(quickjs PROPERTIES PUBLIC_HEADER "quickjs.h;quickjs-libc.h") + if(NOT EXISTS ${CMAKE_BINARY_DIR}/libquickjs${arch}.dll) + cmake_minimum_required(VERSION 3.18) + file(DOWNLOAD + "https://nightly.link/ftk/quickjspp/workflows/quickjsdll/ci/dlls%20-flto%20-O3%20-DNDEBUG.zip" + libquickjs.zip + TLS_VERIFY ON SHOW_PROGRESS) + file(ARCHIVE_EXTRACT INPUT libquickjs.zip DESTINATION ${CMAKE_BINARY_DIR}) + endif() + add_library(quickjs SHARED IMPORTED GLOBAL) + add_library(quickjs-dumpleaks ALIAS quickjs) + set_target_properties(quickjs PROPERTIES + IMPORTED_LOCATION ${CMAKE_BINARY_DIR}/libquickjs${arch}.dll + IMPORTED_IMPLIB ${CMAKE_BINARY_DIR}/libquickjs${arch}.a + ) + include(GNUInstallDirs) + install(FILES quickjs.h quickjs-libc.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/quickjs) + install(FILES ${CMAKE_BINARY_DIR}/libquickjs32.dll ${CMAKE_BINARY_DIR}/libquickjs64.dll + DESTINATION ${CMAKE_INSTALL_BINDIR}) + install(FILES ${CMAKE_BINARY_DIR}/libquickjs32.a ${CMAKE_BINARY_DIR}/libquickjs64.a + DESTINATION ${CMAKE_INSTALL_LIBDIR}/quickjs) +else(MSVC) + # compile quickjs + file(STRINGS VERSION version) + set(quickjs_src quickjs.c libbf.c libunicode.c libregexp.c cutils.c quickjs-libc.c) + set(quickjs_def CONFIG_VERSION="${version}" _GNU_SOURCE CONFIG_BIGNUM) -add_library(quickjs-dumpleaks ${quickjs_src}) -target_compile_definitions(quickjs-dumpleaks PRIVATE ${quickjs_def} DUMP_LEAKS=1) + add_library(quickjs ${quickjs_src}) + target_compile_definitions(quickjs PRIVATE ${quickjs_def}) + set_target_properties(quickjs PROPERTIES PUBLIC_HEADER "quickjs.h;quickjs-libc.h") -if(UNIX OR MINGW) - find_package(Threads) - target_link_libraries(quickjs ${CMAKE_DL_LIBS} m Threads::Threads) - target_link_libraries(quickjs-dumpleaks ${CMAKE_DL_LIBS} m Threads::Threads) -endif() + add_library(quickjs-dumpleaks ${quickjs_src}) + target_compile_definitions(quickjs-dumpleaks PRIVATE ${quickjs_def} DUMP_LEAKS=1) -# install -include(GNUInstallDirs) -install(TARGETS quickjs - PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/quickjs - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/quickjs - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/quickjs - ) + if(UNIX OR MINGW) + find_package(Threads) + target_link_libraries(quickjs ${CMAKE_DL_LIBS} m Threads::Threads) + target_link_libraries(quickjs-dumpleaks ${CMAKE_DL_LIBS} m Threads::Threads) + endif() + + # install + include(GNUInstallDirs) + install(TARGETS quickjs + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/quickjs + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/quickjs + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/quickjs + ) +endif(MSVC) diff --git a/quickjs/patches/getclassid.patch b/quickjs/patches/getclassid.patch index 991f749e8..bfe6f218f 100644 --- a/quickjs/patches/getclassid.patch +++ b/quickjs/patches/getclassid.patch @@ -7,7 +7,7 @@ index 48aeffc62..8afc4caa1 100644 #endif } + -+JSClassID JS_GetClassID(JSValue v) ++JSClassID JS_GetClassID(JSValueConst v) +{ + JSObject *p; + diff --git a/quickjs/patches/msvc-header-fix.patch b/quickjs/patches/msvc-header-fix.patch new file mode 100644 index 000000000..ee2a5511a --- /dev/null +++ b/quickjs/patches/msvc-header-fix.patch @@ -0,0 +1,25 @@ +diff --git a/quickjs/quickjs.h b/quickjs/quickjs.h +--- a/quickjs/quickjs.h (revision 96fdd7947760bed361be7ea1c749f41c4a3ab62e) ++++ b/quickjs/quickjs.h (revision 47c9957e9b830cfcde70aebf6005a26f45ec0bbe) +@@ -215,12 +215,18 @@ + #define JS_VALUE_GET_FLOAT64(v) ((v).u.float64) + #define JS_VALUE_GET_PTR(v) ((v).u.ptr) + ++#ifdef __cplusplus ++#define JS_MKVAL(tag, val) JSValue{ JSValueUnion{ .int32 = val }, tag } ++#define JS_MKPTR(tag, p) JSValue{ JSValueUnion{ .ptr = p }, tag } ++#define JS_NAN JSValue{ .u.float64 = JS_FLOAT64_NAN, JS_TAG_FLOAT64 } ++#else + #define JS_MKVAL(tag, val) (JSValue){ (JSValueUnion){ .int32 = val }, tag } + #define JS_MKPTR(tag, p) (JSValue){ (JSValueUnion){ .ptr = p }, tag } +- +-#define JS_TAG_IS_FLOAT64(tag) ((unsigned)(tag) == JS_TAG_FLOAT64) +- + #define JS_NAN (JSValue){ .u.float64 = JS_FLOAT64_NAN, JS_TAG_FLOAT64 } ++#endif ++ ++#define JS_TAG_IS_FLOAT64(tag) ((unsigned)(tag) == JS_TAG_FLOAT64) ++ + + static inline JSValue __JS_NewFloat64(JSContext *ctx, double d) + { diff --git a/quickjs/quickjs.c b/quickjs/quickjs.c index 52fc23940..1984561a0 100644 --- a/quickjs/quickjs.c +++ b/quickjs/quickjs.c @@ -54083,7 +54083,7 @@ void JS_AddIntrinsicTypedArrays(JSContext *ctx) #endif } -JSClassID JS_GetClassID(JSValue v) +JSClassID JS_GetClassID(JSValueConst v) { JSObject *p; diff --git a/quickjs/quickjs.h b/quickjs/quickjs.h index 215e7b2fc..2bda219bc 100644 --- a/quickjs/quickjs.h +++ b/quickjs/quickjs.h @@ -215,12 +215,18 @@ typedef struct JSValue { #define JS_VALUE_GET_FLOAT64(v) ((v).u.float64) #define JS_VALUE_GET_PTR(v) ((v).u.ptr) +#ifdef __cplusplus +#define JS_MKVAL(tag, val) JSValue{ JSValueUnion{ .int32 = val }, tag } +#define JS_MKPTR(tag, p) JSValue{ JSValueUnion{ .ptr = p }, tag } +#define JS_NAN JSValue{ .u.float64 = JS_FLOAT64_NAN, JS_TAG_FLOAT64 } +#else #define JS_MKVAL(tag, val) (JSValue){ (JSValueUnion){ .int32 = val }, tag } #define JS_MKPTR(tag, p) (JSValue){ (JSValueUnion){ .ptr = p }, tag } +#define JS_NAN (JSValue){ .u.float64 = JS_FLOAT64_NAN, JS_TAG_FLOAT64 } +#endif #define JS_TAG_IS_FLOAT64(tag) ((unsigned)(tag) == JS_TAG_FLOAT64) -#define JS_NAN (JSValue){ .u.float64 = JS_FLOAT64_NAN, JS_TAG_FLOAT64 } static inline JSValue __JS_NewFloat64(JSContext *ctx, double d) { diff --git a/quickjspp.hpp b/quickjspp.hpp index 6411e4a76..76e2f5eea 100644 --- a/quickjspp.hpp +++ b/quickjspp.hpp @@ -34,6 +34,13 @@ namespace qjs { class Context; class Value; +template class shared_ptr; +template +inline shared_ptr make_shared(JSContext * ctx, Args&&... args); +template +inline shared_ptr make_shared_with_object(JSContext * ctx, JSValue&& jsobj, Args&&... args); + + /** Exception type. * Indicates that exception has occured in JS context. */ @@ -265,7 +272,7 @@ struct js_traits> /* Useful type traits */ template struct is_shared_ptr : std::false_type {}; - template struct is_shared_ptr> : std::true_type {}; + template struct is_shared_ptr> : std::true_type {}; template struct is_string { static constexpr bool value = std::is_same_v || std::is_same_v, std::string> || @@ -654,7 +661,7 @@ void wrap_args(JSContext * ctx, JSValue * argv, Args&& ... args) // Helper trait to obtain `T` in `T::*` expressions template struct class_from_member_pointer { using type = void; }; -template struct class_from_member_pointer { using type = U; }; +template struct class_from_member_pointer { using type = U; using member_type = T; }; template using class_from_member_pointer_t = typename class_from_member_pointer::type; } // namespace detail @@ -696,7 +703,7 @@ struct js_traits> { 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, argc, argv); + return detail::wrap_this_call, Args...>(ctx, F, this_value, argc, argv); }, fw.name, sizeof...(Args)); } @@ -710,7 +717,7 @@ struct js_traits> { 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, argc, argv); + return detail::wrap_this_call, Args...>(ctx, F, this_value, argc, argv); }, fw.name, sizeof...(Args)); } @@ -746,285 +753,41 @@ struct js_traits> { 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 - } - + assert(js_traits>::QJSClassId); auto proto = detail::GetPropertyPrototype(ctx, this_value); if(JS_IsException(proto)) return proto; - auto jsobj = JS_NewObjectProtoClass(ctx, proto, js_traits>::QJSClassId); + auto jsobj = JS_NewObjectProtoClass(ctx, proto, js_traits>::QJSClassId); JS_FreeValue(ctx, proto); if(JS_IsException(jsobj)) return jsobj; - try { - std::shared_ptr ptr = std::apply(std::make_shared, detail::unwrap_args(ctx, argc, argv)); - JS_SetOpaque(jsobj, new std::shared_ptr(std::move(ptr))); - return jsobj; + // TODO: fix/optimize + auto sp = std::apply(make_shared_with_object, std::tuple_cat( + std::tuple{ctx, std::move(jsobj)}, + detail::unwrap_args(ctx, argc, argv))); + return sp.release(); } catch (exception) { - JS_FreeValue(ctx, jsobj); return JS_EXCEPTION; } catch (std::exception const & err) { - JS_FreeValue(ctx, jsobj); JS_ThrowInternalError(ctx, "%s", err.what()); return JS_EXCEPTION; } catch (...) { - JS_FreeValue(ctx, jsobj); JS_ThrowInternalError(ctx, "Unknown error"); return JS_EXCEPTION; } - - // return detail::wrap_call, Args...>(ctx, std::make_shared, argv); }, cw.name, sizeof...(Args), JS_CFUNC_constructor, 0); } }; -/** Conversions for std::shared_ptr. Empty shared_ptr corresponds to JS_NULL. - * 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; - - /// Signature of the function to obtain the std::shared_ptr from the JSValue. - using ptr_cast_fcn_t = std::function(JSContext*, JSValueConst)>; - - /// Used by registerDerivedClass to register new derived classes with this class' base type. - inline static std::function registerWithBase; - - /// Mapping between derived class' JSClassID and function to obtain the std::shared_ptr from the JSValue. - inline static std::unordered_map ptrCastFcnMap; - - /** Register a class as a derived class. - * - * @tparam D type of the derived class - * @param derived_class_id class id of the derived class - * @param ptr_cast_fcn function to obtain a std::shared_ptr from the JSValue - */ - template - static void registerDerivedClass(JSClassID derived_class_id, ptr_cast_fcn_t ptr_cast_fcn) { - static_assert(std::is_base_of::value && !std::is_same::value, "Type is not a derived class"); - using derived_ptr_cast_fcn_t = typename js_traits>::ptr_cast_fcn_t; - - // Register how to obtain the std::shared_ptr from the derived class. - ptrCastFcnMap[derived_class_id] = ptr_cast_fcn; - - // Propagate the registration to our base class (if any). - if (registerWithBase) registerWithBase(derived_class_id, ptr_cast_fcn); - - // Instrument the derived class so that it can propagate new derived classes to us. - auto old_registerWithBase = js_traits>::registerWithBase; - js_traits>::registerWithBase = - [old_registerWithBase = std::move(old_registerWithBase)] - (JSClassID derived_class_id, derived_ptr_cast_fcn_t derived_ptr_cast_fcn){ - if (old_registerWithBase) old_registerWithBase(derived_class_id, derived_ptr_cast_fcn); - registerDerivedClass(derived_class_id, [derived_cast_fcn = std::move(derived_ptr_cast_fcn)](JSContext * ctx, JSValueConst v) { - return std::shared_ptr(derived_cast_fcn(ctx, v)); - }); - }; - } - - template - static - std::enable_if_t || std::is_same_v> - ensureCanCastToBase() { } - - template - static - std::enable_if_t && !std::is_same_v> - ensureCanCastToBase() { - static_assert(std::is_base_of_v, "Type is not a derived class"); - - if(js_traits>::QJSClassId == 0) - JS_NewClassID(&js_traits>::QJSClassId); - - js_traits>::template registerDerivedClass(QJSClassId, unwrap); - } - - template - static void ensureCanCastToBase() { - ensureCanCastToBase>(); - } - - /** Stores offsets to qjs::Value members of T. - * These values should be marked by class_registrar::mark for QuickJS garbage collector - * so that the cycle removal algorithm can find the other objects referenced by this object. - */ - static inline std::vector markOffsets; - - /** 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)) - { - JSClassGCMark * marker = nullptr; - if(!markOffsets.empty()) - { - marker = [](JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) { - auto pptr = static_cast *>(JS_GetOpaque(val, QJSClassId)); - assert(pptr); - const T * ptr = pptr->get(); - assert(ptr); - for(Value T::* member : markOffsets) - { - JS_MarkValue(rt, (*ptr.*member).v, mark_func); - } - }; - } - JSClassDef def{ - name, - // destructor (finalizer) - [](JSRuntime * rt, JSValue obj) noexcept { - auto pptr = static_cast *>(JS_GetOpaque(obj, QJSClassId)); - delete pptr; - }, - // mark - marker, - // call - nullptr, - // exotic - nullptr - }; - int e = JS_NewClass(rt, QJSClassId, &def); - if(e < 0) - { - JS_ThrowInternalError(ctx, "Can't register class %s", name); - throw exception{ctx}; - } - } - 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(!ptr) - return JS_NULL; - 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 std::shared_ptr unwrap(JSContext * ctx, JSValueConst v) - { - std::shared_ptr ptr = nullptr; - if (JS_IsNull(v)) { - return ptr; - } - auto obj_class_id = JS_GetClassID(v); - - if (obj_class_id == QJSClassId) { - // The JS object is of class T - void * opaque = JS_GetOpaque2(ctx, v, obj_class_id); - assert(opaque && "No opaque pointer in object"); - ptr = *static_cast *>(opaque); - } else if (ptrCastFcnMap.count(obj_class_id)) { - // The JS object is of a class derived from T - ptr = ptrCastFcnMap[obj_class_id](ctx, v); - } else { - // The JS object does not derives from T - JS_ThrowTypeError(ctx, "Expected type %s, got object with classid %d", - QJSPP_TYPENAME(T), obj_class_id); - throw exception{ctx}; - } - if(!ptr) { - JS_ThrowInternalError(ctx, "Object's opaque pointer is NULL"); - throw exception{ctx}; - } - return ptr; - } -}; - -/** Conversions for non-owning pointers to class T. nullptr corresponds to JS_NULL. - * @tparam T class type - */ -template -struct js_traits>> -{ - static JSValue wrap(JSContext * ctx, T * ptr) - { - if (ptr == nullptr) { - return JS_NULL; - } - if(js_traits>::QJSClassId == 0) // not registered - { -#if defined(__cpp_rtti) - // If you have an error here with T=JSValueConst - // it probably means you are passing JSValueConst to where JSValue is expected - 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) - { - if (JS_IsNull(v)) { - return nullptr; - } - auto ptr = js_traits>::unwrap(ctx, v); - return ptr.get(); - } -}; - /** Conversions for enums. */ template struct js_traits>> { @@ -1125,9 +888,9 @@ struct js_traits template struct js_property_traits { - static void set_property(JSContext * ctx, JSValue this_obj, Key key, JSValue value); + static void set_property(JSContext * ctx, JSValue this_obj, Key key, JSValue value) = delete; - static JSValue get_property(JSContext * ctx, JSValue this_obj, Key key); + static JSValue get_property(JSContext * ctx, JSValue this_obj, Key key) = delete; }; template <> @@ -1219,12 +982,12 @@ struct get_set { using is_const = std::is_const; - static const R& get(std::shared_ptr ptr) + static const R& get(qjs::shared_ptr ptr) { return *ptr.*M; } - static R& set(std::shared_ptr ptr, R value) + static R& set(qjs::shared_ptr ptr, R value) { return *ptr.*M = std::move(value); } @@ -1297,7 +1060,12 @@ class Value 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); + auto tag = JS_VALUE_GET_TAG(v); + if(tag != JS_VALUE_GET_TAG(other)) + return false; + if(tag >= JS_TAG_INT && tag < JS_TAG_FLOAT64) + return JS_VALUE_GET_INT(v) == JS_VALUE_GET_INT(other); + return JS_VALUE_GET_PTR(v) == JS_VALUE_GET_PTR(other); } bool operator !=(JSValueConst other) const { return !((*this) == other); } @@ -1441,8 +1209,338 @@ class Value return Value{ctx, JS_EvalThis(ctx, v, buffer.data(), buffer.size(), filename, flags)}; } + void swap(Value& r) noexcept { std::swap(this->ctx, r.ctx); std::swap(this->v, r.v); } }; +template +class shared_ptr : public Value +{ + /** Usually it contains the same pointer as JS_GetOpaque. + * In case of T being a base class, it contains converted pointer. + */ + T * pointer = nullptr; +public: + constexpr shared_ptr() noexcept : Value(JS_NULL) {} + constexpr shared_ptr(std::nullptr_t) noexcept : shared_ptr() {} + constexpr shared_ptr(JSContext * ctx, JSValue&& val) noexcept : Value(ctx, std::move(val)), pointer(static_cast(JS_GetOpaque(v, JS_GetClassID(v)))) {} + template + explicit shared_ptr(JSContext * ctx, Y * ptr) : shared_ptr() + { + assert(js_traits::QJSClassId); + assert(ctx); + auto jsobj = JS_NewObjectClass(ctx, js_traits::QJSClassId); + if(JS_IsException(jsobj)) + throw exception{ctx}; + this->pointer = static_cast(ptr); + JS_SetOpaque(jsobj, this->pointer); + this->v = jsobj; + this->ctx = ctx; + } + shared_ptr(shared_ptr&& r) noexcept : Value(std::move(r)), pointer(std::move(r.pointer)) {} + + // convert pointer + template + shared_ptr(shared_ptr&& r) noexcept : Value(std::move(r)) + { + if(r.get()) + pointer = static_cast(r.get()); + } + + shared_ptr(const shared_ptr& r) noexcept : Value(r), pointer(r.pointer) {} + + shared_ptr& operator=(shared_ptr r) { r.swap(*this); return *this; } + + //using Value::operator=; + + void reset() noexcept { shared_ptr().swap(*this); } + template void reset(Y* ptr) { shared_ptr(ctx, ptr).swap(*this); } + //void release() = delete; + + [[nodiscard]] T* get() const noexcept { return pointer; } + T& operator*() const noexcept { return *get(); } + T* operator->() const noexcept { return get(); } + + [[nodiscard]] long use_count() const noexcept + { + if(ctx == nullptr) return 0; + if(JS_VALUE_HAS_REF_COUNT(v)) + return ((JSRefCountHeader *)JS_VALUE_GET_PTR(this->v))->ref_count; + return 1; + } + + void swap(shared_ptr& r) noexcept { std::swap(this->ctx, r.ctx); std::swap(this->v, r.v); std::swap(this->pointer, r.pointer);} + + + explicit operator bool() const noexcept { return get() != nullptr; } +}; + +template +class enable_shared_from_this +{ +public://todo + shared_ptr shared_this; +public: + shared_ptr shared_from_this() + { + if(!shared_this.ctx) throw std::bad_weak_ptr{}; + return shared_this; + } +}; + +template +inline shared_ptr make_shared(JSContext * ctx, Args&& ... args) +{ + // not optimized + T * p = nullptr; + try + { + p = ::new T(std::forward(args)...); + auto sp = shared_ptr{ctx, p}; + if constexpr(std::is_base_of_v, T>) + return p->shared_this = sp; + return sp; + } catch(...) + { + delete p; + throw; + } +} + +template +inline shared_ptr make_shared_with_object(JSContext * ctx, JSValue&& jsobj, Args&& ... args) +{ + // not optimized + T * p = nullptr; + try + { + p = ::new T(std::forward(args)...); + JS_SetOpaque(jsobj, p); + auto sp = shared_ptr{ctx, std::move(jsobj)}; + if constexpr(std::is_base_of_v, T>) + return p->shared_this = sp; + return sp; + } catch(...) + { + JS_SetOpaque(jsobj, nullptr); + JS_FreeValue(ctx, jsobj); + delete p; + throw; + } +} + + +/** Conversions for qjs::shared_ptr. Empty shared_ptr corresponds to JS_NULL. + * 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; + + /// Signature of the function to obtain the qjs::shared_ptr from the JSValue. + using ptr_cast_fcn_t = std::function(JSContext*, JSValueConst)>; + + /// Used by registerDerivedClass to register new derived classes with this class' base type. + inline static std::function registerWithBase; + + /// Mapping between derived class' JSClassID and function to obtain the qjs::shared_ptr from the JSValue. + inline static std::unordered_map ptrCastFcnMap; + + /** Register a class as a derived class. + * + * @tparam D type of the derived class + * @param derived_class_id class id of the derived class + * @param ptr_cast_fcn function to obtain a qjs::shared_ptr from the JSValue + */ + template + static void registerDerivedClass(JSClassID derived_class_id, ptr_cast_fcn_t ptr_cast_fcn) { + static_assert(std::is_base_of::value && !std::is_same::value, "Type is not a derived class"); + using derived_ptr_cast_fcn_t = typename js_traits>::ptr_cast_fcn_t; + + // Register how to obtain the qjs::shared_ptr from the derived class. + ptrCastFcnMap[derived_class_id] = ptr_cast_fcn; + + // Propagate the registration to our base class (if any). + if (registerWithBase) registerWithBase(derived_class_id, ptr_cast_fcn); + + // Instrument the derived class so that it can propagate new derived classes to us. + auto old_registerWithBase = std::move(js_traits>::registerWithBase); + js_traits>::registerWithBase = + [old_registerWithBase = std::move(old_registerWithBase)] + (JSClassID derived_class_id, derived_ptr_cast_fcn_t derived_ptr_cast_fcn) { + if (old_registerWithBase) old_registerWithBase(derived_class_id, derived_ptr_cast_fcn); + registerDerivedClass(derived_class_id, [derived_cast_fcn = std::move(derived_ptr_cast_fcn)] + (JSContext * ctx, JSValueConst v) { + return qjs::shared_ptr{derived_cast_fcn(ctx, v)}; + }); + }; + } + + template + static + std::enable_if_t || std::is_same_v> + ensureCanCastToBase() { } + + template + static + std::enable_if_t && !std::is_same_v> + ensureCanCastToBase() { + static_assert(std::is_base_of_v, "Type is not a derived class"); + + if(QJSClassId == 0) + JS_NewClassID(&QJSClassId); + + js_traits>::template registerDerivedClass(QJSClassId, unwrap); + } + + template + static void ensureCanCastToBase() { + ensureCanCastToBase>(); + } + + /** Stores offsets to qjs::Value members of T. + * These values should be marked by class_registrar::mark for QuickJS garbage collector + * so that the cycle removal algorithm can find the other objects referenced by this object. + */ + static inline std::vector markOffsets; + + /** Register class in QuickJS context. + * + * @param ctx context + * @param name class name + * @param proto class prototype or JS_NULL + * @param call QJS call function. see quickjs doc + * @param exotic pointer to QJS exotic methods(static lifetime) which allow custom property handling. see quickjs doc + * @throws exception + */ + static void register_class(JSContext * ctx, const char * name, JSValue proto = JS_NULL, + JSClassCall * call = nullptr, JSClassExoticMethods * exotic = nullptr) + { + if(QJSClassId == 0) + { + JS_NewClassID(&QJSClassId); + } + auto rt = JS_GetRuntime(ctx); + if(!JS_IsRegisteredClass(rt, QJSClassId)) + { + JSClassGCMark * marker = nullptr; + // automatically mark enable_shared_from_this::shared_this if derived from it + if constexpr(std::is_base_of_v,T>) + { + markOffsets.push_back(reinterpret_cast(&T::shared_this)); + } + if(!markOffsets.empty()) + { + markOffsets.shrink_to_fit(); + marker = [](JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) { + auto ptr = static_cast(JS_GetOpaque(val, QJSClassId)); + assert(ptr); + for(auto&& member : markOffsets) + { + JS_MarkValue(rt, (*ptr.*member).v, mark_func); + } + }; + } + JSClassDef def{ + name, + // destructor (finalizer) + [](JSRuntime * rt, JSValue obj) noexcept { + auto ptr = static_cast(JS_GetOpaque(obj, QJSClassId)); + if constexpr(std::is_base_of_v, T>) + { + if(ptr) ptr->shared_this.release(); + } + delete ptr; + }, + // mark + marker, + // call + call, + // exotic + exotic + }; + int e = JS_NewClass(rt, QJSClassId, &def); + if(e < 0) + { + JS_FreeValue(ctx, proto); + JS_ThrowInternalError(ctx, "Can't register class %s", name); + throw exception{ctx}; + } + } + JS_SetClassProto(ctx, QJSClassId, proto); + } + + static shared_ptr unwrap(JSContext * ctx, JSValueConst v) + { + if(JS_IsNull(v)) return shared_ptr{}; + auto obj_class_id = JS_GetClassID(v); + if(obj_class_id == QJSClassId) + { + // The JS object is of class T + return shared_ptr{ctx, JS_DupValue(ctx, v)}; + } + else if(ptrCastFcnMap.count(obj_class_id)) + { + // The JS object is of a class derived from T + return ptrCastFcnMap[obj_class_id](ctx, v); + } + // The JS object does not derives from T + JS_ThrowTypeError(ctx, "Expected type %s, got object with classid %d", + QJSPP_TYPENAME(T), obj_class_id); + throw exception{ctx}; + } + + static JSValue wrap(JSContext * ctx, shared_ptr v) noexcept + { + if(!v) return JS_NULL; + assert(ctx == v.ctx); + return v.release(); + } +}; + +template +struct js_traits +{ + static JSValue wrap(JSContext * ctx, T * ptr) noexcept + { + static_assert(std::is_base_of_v, T>, "T should be inherited from qjs::enable_shared_from_this to enable T* conversions"); + return js_traits::wrap(ctx, *ptr); + } + static T * unwrap(JSContext * ctx, JSValueConst v) + { + if(JS_IsNull(v)) + return nullptr; + return js_traits>::unwrap(ctx, v).get(); + } +}; + +template +struct js_traits, T>>> +{ + static JSValue wrap(JSContext * ctx, T& obj) noexcept + { + try + { + return js_traits>::wrap(ctx, obj.shared_from_this()); + } catch(const std::exception& e) + { + // probably wasn't made with qjs::make_shared + JS_ThrowInternalError(ctx, "%s", e.what()); + return JS_EXCEPTION; + } + } + + static T& unwrap(JSContext * ctx, JSValueConst v) + { + T * p = js_traits>::unwrap(ctx, v).get(); + assert(p); + return *p; + } +}; + + /** Thin wrapper over JSRuntime * rt * Calls JS_FreeRuntime on destruction. noncopyable. */ @@ -1607,12 +1705,14 @@ class Context qjs::Value prototype; qjs::Context::Module& module; qjs::Context& context; + qjs::Value ctor; // last added constructor public: explicit class_registrar(const char * name, qjs::Context::Module& module, qjs::Context& context) : name(name), prototype(context.newObject()), module(module), - context(context) + context(context), + ctor(JS_NULL) { } @@ -1636,16 +1736,34 @@ class Context template class_registrar& fun(const char * name) { - js_traits>::template ensureCanCastToBase(); + js_traits>::template ensureCanCastToBase(); prototype.add(name); return *this; } + /** Add a static member or function to the last added constructor. + * Example: + * struct T { static int var; static int func(); } + * module.class_("T").contructor<>("T").static_fun<&T::var>("var").static_fun<&T::func>("func"); + */ + template + class_registrar& static_fun(const char * name) + { + assert(!JS_IsNull(ctor.v) && "You should call .constructor before .static_fun"); + js_traits>::template ensureCanCastToBase(); + ctor.add(name); + return *this; + } + + /** Add a property with custom getter and setter. + * Example: + * module.class_("T").property<&T::getX, &T::setX>("x"); + */ template class_registrar& property(const char * name) { - js_traits>::template ensureCanCastToBase(); - js_traits>::template ensureCanCastToBase(); + js_traits>::template ensureCanCastToBase(); + js_traits>::template ensureCanCastToBase(); if constexpr (std::is_same_v) prototype.add_getter(name); else @@ -1662,9 +1780,9 @@ class Context { if(!name) name = this->name; - Value ctor = context.newValue(qjs::ctor_wrapper{name}); + ctor = context.newValue(qjs::ctor_wrapper{name}); JS_SetConstructor(context.ctx, ctor.v, prototype.v); - module.add(name, std::move(ctor)); + module.add(name, qjs::Value{ctor}); return *this; } @@ -1675,9 +1793,9 @@ class Context class_registrar& base() { static_assert(!std::is_same_v, "Type cannot be a base of itself"); - assert(js_traits>::QJSClassId && "base class is not registered"); - js_traits>::template ensureCanCastToBase(); - auto base_proto = JS_GetClassProto(context.ctx, js_traits>::QJSClassId); + assert(js_traits>::QJSClassId && "base class is not registered"); + js_traits>::template ensureCanCastToBase(); + 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) @@ -1688,17 +1806,23 @@ class Context /** All qjs::Value members of T should be marked by mark<> for QuickJS garbage collector * so that the cycle removal algorithm can find the other objects referenced by this object. */ - template + template class_registrar& mark() { - js_traits>::markOffsets.push_back(V); + static_assert(std::is_base_of_v::member_type>, + "marking a member pointer that is not derived from qjs::Value"); + js_traits>::markOffsets.push_back(reinterpret_cast(V)); return *this; } - - ~class_registrar() + ~class_registrar() noexcept(false) { - context.registerClass(name, std::move(prototype)); + // register_class can throw, so check if an exception has already occurred + JSValue v = JS_GetException(context.ctx); + if(JS_IsNull(v)) + context.registerClass(name, std::move(prototype)); + else + JS_Throw(context.ctx, v); } }; @@ -1787,7 +1911,7 @@ class Context /** returns current exception associated with context and clears 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. + /** Register class T for conversions to/from qjs::shared_ptr to work. * Wherever possible module.class_("T")... should be used instead. * @tparam T class type * @param name class name in JS engine @@ -1796,7 +1920,7 @@ class Context template void registerClass(const char * name, JSValue proto = JS_NULL) { - js_traits>::register_class(ctx, name, proto); + js_traits>::register_class(ctx, name, proto); } /// @see JS_Eval @@ -1858,7 +1982,7 @@ struct js_traits, int> { static auto unwrap(JSContext * ctx, JSValueConst fun_obj) { - const int argc = sizeof...(Args); + constexpr int argc = sizeof...(Args); if constexpr(argc == 0) { return [jsfun_obj = Value{ctx, JS_DupValue(ctx, fun_obj)}]() -> R { @@ -1870,7 +1994,7 @@ struct js_traits, int> } else { - return [jsfun_obj = Value{ctx, JS_DupValue(ctx, fun_obj)}](Args&& ... args) -> R { + 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)...); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9e63673d5..fba360ed9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,7 +1,7 @@ macro(make_test name) add_executable(${name} ${name}.cpp) target_link_libraries(${name} quickjspp quickjs-dumpleaks) -add_test(${name} ${name}) +add_test(NAME ${name} COMMAND ${name} WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) # try to compile with -DCONFIG_CHECK_JSVALUE add_library(${name}-checkjsv OBJECT ${name}.cpp) target_compile_definitions(${name}-checkjsv PRIVATE CONFIG_CHECK_JSVALUE=1) @@ -11,6 +11,7 @@ endmacro() foreach(test value class exception example multicontext conversions point variant function_call inheritance jobs unhandled_rejection module_loader enum + exotic ) make_test(${test}) endforeach() diff --git a/test/class.cpp b/test/class.cpp index 6ceee95e0..be00092d9 100644 --- a/test/class.cpp +++ b/test/class.cpp @@ -3,9 +3,9 @@ #include -#define TYPES bool, int32_t, double, std::shared_ptr, const std::shared_ptr&, std::string, const std::string& +#define TYPES bool, int32_t, double, qjs::shared_ptr, const qjs::shared_ptr&, std::string, const std::string& -class base_test +class base_test : public qjs::enable_shared_from_this { public: std::vector> base_field; @@ -15,6 +15,10 @@ class base_test std::swap(x, base_field[0][0]); return x; } + + // TODO: make it work in inherited classes + base_test * self() { return this; } + bool check_self(base_test * self) { return(this == self); } }; class test : public base_test @@ -23,7 +27,7 @@ class test : public base_test bool b; mutable int32_t i; double d = 7.; - std::shared_ptr spt; + qjs::shared_ptr spt; std::string s; test(int32_t i, TYPES) : i(i) @@ -40,7 +44,7 @@ class test : public base_test int32_t fi(TYPES) const { i++; return i; } bool fb(TYPES) { i++; return b; } double fd(TYPES) const { i++; return d; } - const std::shared_ptr& fspt(TYPES) { i++; return spt; } + const qjs::shared_ptr& fspt(TYPES) { i++; return spt; } const std::string& fs(TYPES) { i++; return s; } void f(TYPES) { i++; } @@ -51,7 +55,7 @@ class test : public base_test static void fstatic(TYPES) {} static bool bstatic; }; -bool test::bstatic; +bool test::bstatic = true; void f(TYPES) {} @@ -65,8 +69,10 @@ void qjs_glue(qjs::Context::Module& m) { ; m.class_<::test>("test") - //.base<::base_test>() - .constructor<::int32_t, bool, ::int32_t, double, ::std::shared_ptr, ::std::shared_ptr const &, ::std::string, ::std::string const &>("Test") + .base<::base_test>() + .constructor<::int32_t, bool, ::int32_t, double, ::qjs::shared_ptr, ::qjs::shared_ptr const &, ::std::string, ::std::string const &>("Test") + .static_fun<&::test::fstatic>("fstatic") // (bool, ::int32_t, double, ::std::shared_ptr, ::std::shared_ptrconst &, ::std::string, ::std::string const &) + .static_fun<&::test::bstatic>("bstatic") // bool .constructor<::int32_t>("TestSimple") .fun<&::test::fi>("fi") // (bool, ::int32_t, double, ::std::shared_ptr, ::std::shared_ptr const &, ::std::string, ::std::string const &) .fun<&::test::fb>("fb") // (bool, ::int32_t, double, ::std::shared_ptr, ::std::shared_ptr const &, ::std::string, ::std::string const &) @@ -74,15 +80,16 @@ void qjs_glue(qjs::Context::Module& m) { .fun<&::test::fspt>("fspt") // (bool, ::int32_t, double, ::std::shared_ptr, ::std::shared_ptr const&, ::std::string, ::std::string const &) .fun<&::test::fs>("fs") // (bool, ::int32_t, double, ::std::shared_ptr, ::std::shared_ptr const &, ::std::string, ::std::string const &) .fun<&::test::f>("f") // (bool, ::int32_t, double, ::std::shared_ptr, ::std::shared_ptr const &, ::std::string, ::std::string const &) - .fun<&::test::fstatic>("fstatic") // (bool, ::int32_t, double, ::std::shared_ptr, ::std::shared_ptrconst &, ::std::string, ::std::string const &) - .fun<&::test::bstatic>("bstatic") // bool .fun<&::test::b>("b") // bool .fun<&::test::i>("i") // ::int32_t .fun<&::test::d>("d") // double .fun<&::test::spt>("spt") // ::std::shared_ptr .fun<&::test::s>("s") // ::std::string + .fun<&::test::self>("self") + .fun<&::test::check_self>("check_self") .property<&test::get_d, &test::set_d>("property_rw") .property<&test::get_d>("property_ro") + .mark<&::test::spt>() ; } // qjs_glue @@ -100,13 +107,11 @@ int main() try { - qjs_glue(context.addModule("test")); - js_std_init_handlers(rt); - /* loader for ES6 modules */ - JS_SetModuleLoaderFunc(rt, nullptr, js_module_loader, nullptr); js_std_add_helpers(ctx, 0, nullptr); + qjs_glue(context.addModule("test")); + /* system modules */ js_init_module_std(ctx, "std"); js_init_module_os(ctx, "os"); @@ -117,12 +122,18 @@ int main() "import * as test from 'test';\n" "globalThis.std = std;\n" "globalThis.test = test;\n" - "globalThis.os = os;\n"; + "globalThis.os = os;\n" + "globalThis.assert = function(b, str)\n" + "{\n" + " if (b) {\n" + " std.printf('OK' + str + '\\n'); return;\n" + " } else {\n" + " throw Error(\"assertion failed: \" + str);\n" + " }\n" + "}"; context.eval(str, "", JS_EVAL_TYPE_MODULE); - context.global()["assert"] = [](bool t) { if(!t) std::exit(2); }; - auto xxx = context.eval("\"use strict\";" "var b = new test.base_test();" @@ -137,21 +148,24 @@ int main() "q.d = 456.789;" "q.s = \"STRING\";" "q.spt = t;" - //"q.base_field = 105.5;" + "t.spt = q;" + "q.base_field = [[5],[1,2,3,4],[6]];" "assert(q.b === q.fb(t.vb, t.vi, t.vd, t, t, t.vs, \"test\"));" "assert(q.d === q.fd(t.vb, t.vi, t.vd, t, t, t.vs, \"test\"));" "assert(q.s === q.fs(t.vb, t.vi, t.vd, t, t, \"test\", t.vs));" - //"assert(105.5 === q.base_method(5.1));" - //"assert(5.1 === q.base_field);" - "assert(q.spt !== q.fspt(t.vb, t.vi, t.vd, t, t, t.vs, \"test\"));" // different objects + "assert(5 === q.base_method(7));" + "assert(7 === q.base_field[0][0]);" + "assert(q.spt === q.fspt(t.vb, t.vi, t.vd, t, t, t.vs, \"test\"));" // same objects + "assert(q.check_self(q));" + "assert(!t.check_self(q));" "q.fi(t.vb, t.vi, t.vd, t, t, t.vs, \"test\")"); assert((int)xxx == 18); auto yyy = context.eval("q.fi.bind(t)(t.vb, t.vi, t.vd, t, t, t.vs, \"test\")"); assert((int)yyy == 13); auto f = context.eval("q.fi.bind(q)").as>(); - int zzz = f(false, 1, 0., context.eval("q").as>(), - context.eval("t").as>(), "test string", std::string{"test"}); + int zzz = f(false, 1, 0., context.eval("q").as>(), + context.eval("t").as>(), "test string", std::string{"test"}); assert(zzz == 19); zzz = (int)context.eval("q.property_rw = q.property_ro - q.property_rw + 1;" @@ -159,6 +173,11 @@ int main() "q.i" ); assert(zzz == 23); + + auto qbase = context.eval("q").as>(); + assert(qbase->base_field[0][0] == 7); + + assert((bool)context.eval("test.Test.bstatic === true")); } catch(exception) { diff --git a/test/conversions.cpp b/test/conversions.cpp index 0b2c782f9..fda489897 100644 --- a/test/conversions.cpp +++ b/test/conversions.cpp @@ -37,9 +37,13 @@ int main() qjs::Context context(runtime); try { + test_num(context); + test_num(context); test_num(context); + test_num(context); test_num(context); test_num(context); + test_num(context); // int64 is represented as double... test_conv(context, -(1ll << 52)); diff --git a/test/example.cpp b/test/example.cpp index 1c8ba816b..47f252995 100644 --- a/test/example.cpp +++ b/test/example.cpp @@ -48,6 +48,11 @@ int main() // callback auto cb = (std::function) context.eval("my_callback"); cb("world"); + + // passing c++ objects to JS + auto lambda = context.eval("x=>my.println(x.member_function('lambda'))").as)>>(); + auto v3 = qjs::make_shared(context.ctx, std::vector{1,2,3}); + lambda(v3); } catch(qjs::exception) { diff --git a/test/exotic.cpp b/test/exotic.cpp new file mode 100644 index 000000000..c84c4dd11 --- /dev/null +++ b/test/exotic.cpp @@ -0,0 +1,91 @@ +/* Example of how to register a class without class_registrar and how to use exotic methods */ + +#include +#include +#include "quickjspp.hpp" + +std::string atom_to_string(JSContext * ctx, JSAtom atom) +{ + JSValue v = JS_AtomToString(ctx, atom); + auto str = qjs::js_traits::unwrap(ctx, v); + JS_FreeValue(ctx, v); + return str; +} + +class ExoticClass : public qjs::enable_shared_from_this +{ +public: + std::function has_property; + std::function get_property; + + static void register_class(JSContext * ctx) + { + // we are passing pointer to exotic - requires static lifetime + static JSClassExoticMethods exotic{ + // see JSClassExoticMethods for more methods + .has_property = [](JSContext * ctx, JSValueConst obj, JSAtom atom) -> int { + try + { + auto ptr = qjs::js_traits>::unwrap(ctx, obj); + return ptr->has_property(ctx, atom); + } + catch(qjs::exception) + { + return -1; + } + }, + .get_property = [](JSContext * ctx, JSValueConst obj, JSAtom atom, + JSValueConst receiver) -> JSValue { + try + { + auto ptr = qjs::js_traits>::unwrap(ctx, obj); + return ptr->get_property(ctx, atom, receiver); + } + catch(qjs::exception) + { + return JS_EXCEPTION; + } + } + }; + qjs::js_traits>::register_class(ctx, "ExoticClass", JS_NULL, nullptr, &exotic); + } +}; + + +void println(qjs::rest args) +{ + for(auto const& arg: args) std::cout << arg << " "; + std::cout << "\n"; +} + +int main() +{ + qjs::Runtime runtime; + qjs::Context context(runtime); + try + { + ExoticClass::register_class(context.ctx); + + auto e = qjs::make_shared(context.ctx); + e->has_property = [](JSContext * ctx, JSAtom) { return true; }; + e->get_property = [](JSContext * ctx, JSAtom prop, JSValueConst receiver) { + return qjs::Value{ctx, "got " + atom_to_string(ctx, prop)}; + }; + + context.global()["e"] = e; + context.global()["println"] = qjs::fwrapper<&println>{"println"}; + + // prints: got property1 + context.eval("println(e.property1);"); + + // prints: got property2 + context.eval("println(e.property2);"); + + } catch(qjs::exception) + { + auto exc = context.getException(); + std::cerr << (std::string) exc << std::endl; + if((bool) exc["stack"]) std::cerr << (std::string) exc["stack"] << std::endl; + return 1; + } +} diff --git a/test/inheritance.cpp b/test/inheritance.cpp index d7a916681..ede96d024 100644 --- a/test/inheritance.cpp +++ b/test/inheritance.cpp @@ -82,40 +82,42 @@ int main() qjs::Runtime runtime; qjs::Context context(runtime); - auto& test = context.addModule("test"); - test.class_("A") - .constructor() - .fun<&A::a>("a") - .fun<&A::vfunc_a1>("vfunc_a1") - .fun<&A::vfunc_a2>("vfunc_a2") - .fun<&A::non_vfunc_a1>("non_vfunc_a1") - .fun<&A::non_vfunc_a2>("non_vfunc_a2"); - - test.class_("B") - .constructor() - .fun<&B::b>("b") - .fun<&B::vfunc_b1>("vfunc_b1") - .fun<&B::vfunc_b2>("vfunc_b2") - .fun<&B::non_vfunc_b1>("non_vfunc_b1") - .fun<&B::non_vfunc_b2>("non_vfunc_b2"); - - test.class_("C") - .constructor() - .base() - .fun<&C::c>("c") - .fun<&C::b>("b") - .fun<&C::h>("h") - .fun<&C::vfunc_b1>("vfunc_b1") - .fun<&C::vfunc_b2>("vfunc_b2") - .fun<&C::vfunc_h1>("vfunc_h1") - .fun<&C::vfunc_h2>("vfunc_h2") - .fun<&C::non_vfunc_a1>("non_vfunc_a1") - .fun<&C::non_vfunc_b1>("non_vfunc_b1") - .fun<&C::non_vfunc_b2>("non_vfunc_b2") - .fun<&C::non_vfunc_h1>("non_vfunc_h1") - .fun<&C::non_vfunc_h2>("non_vfunc_h2"); - - try { + try + { + auto& test = context.addModule("test"); + test.class_("A") + .constructor() + .fun<&A::a>("a") + .fun<&A::vfunc_a1>("vfunc_a1") + .fun<&A::vfunc_a2>("vfunc_a2") + .fun<&A::non_vfunc_a1>("non_vfunc_a1") + .fun<&A::non_vfunc_a2>("non_vfunc_a2"); + + test.class_("B") + .constructor() + .fun<&B::b>("b") + .fun<&B::vfunc_b1>("vfunc_b1") + .fun<&B::vfunc_b2>("vfunc_b2") + .fun<&B::non_vfunc_b1>("non_vfunc_b1") + .fun<&B::non_vfunc_b2>("non_vfunc_b2"); + + test.class_("C") + .constructor() + .base() + .fun<&C::c>("c") + .fun<&C::b>("b") + .fun<&C::h>("h") + .fun<&C::vfunc_b1>("vfunc_b1") + .fun<&C::vfunc_b2>("vfunc_b2") + .fun<&C::vfunc_h1>("vfunc_h1") + .fun<&C::vfunc_h2>("vfunc_h2") + .fun<&C::non_vfunc_a1>("non_vfunc_a1") + .fun<&C::non_vfunc_b1>("non_vfunc_b1") + .fun<&C::non_vfunc_b2>("non_vfunc_b2") + .fun<&C::non_vfunc_h1>("non_vfunc_h1") + .fun<&C::non_vfunc_h2>("non_vfunc_h2"); + + context.eval(R"xxx( import { A, B, C } from "test"; diff --git a/test/point.cpp b/test/point.cpp index e42a606e3..4a8176df3 100644 --- a/test/point.cpp +++ b/test/point.cpp @@ -2,15 +2,19 @@ #include #include -class Point +class Point : public qjs::enable_shared_from_this { public: int x, y; Point(int x, int y) : x(x), y(y) {} - double norm() const + double norm() { - return std::sqrt((double)x * x + double(y) * y); + try { + auto js_norm = shared_from_this().evalThis("this.norm.bind(this)").as>(); + return js_norm(); + } catch(qjs::exception e) {} + return std::sqrt((double) x * x + double(y) * y); } }; @@ -49,6 +53,10 @@ class ColorPoint extends Point { get_color() { return this.color; } + norm() { + assert(this.color); + return 1.0; + } }; function main() @@ -66,10 +74,16 @@ function main() assert(pt2.x === 2); assert(pt2.color === 0xffffff); assert(pt2.get_color() === 0xffffff); + assert(pt2.norm() === 1.0); + globalThis.pt2 = pt2; } main(); )xxx", "", JS_EVAL_TYPE_MODULE); + auto pt2 = context.eval("pt2").as>(); + assert(pt2->x == 2); + assert(pt2->y == 3); + assert(pt2->norm() == 1); } catch(qjs::exception) { diff --git a/test/variant.cpp b/test/variant.cpp index d7006e351..9ff9469e9 100644 --- a/test/variant.cpp +++ b/test/variant.cpp @@ -7,17 +7,18 @@ struct A {}; struct B {}; -using var = std::variant, std::shared_ptr, std::shared_ptr>; +using var = std::variant, qjs::shared_ptr, qjs::shared_ptr>; +static JSContext * jsctx; auto f(var v1, const var& /*v2*/) -> var { return std::visit([](auto&& v) -> var { using T = std::decay_t; - if constexpr (std::is_same_v>) - return std::make_shared(); - else if constexpr (std::is_same_v>) - return std::make_shared(); + if constexpr (std::is_same_v>) + return qjs::make_shared(jsctx); + else if constexpr (std::is_same_v>) + return qjs::make_shared(jsctx); else if constexpr (std::is_same_v>) { v.push_back(0); @@ -65,6 +66,7 @@ int main() { qjs::Runtime runtime; qjs::Context context(runtime); + jsctx = context.ctx; try {