diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index fd20fa1..34e8ceb 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -9,7 +9,46 @@ concurrency: cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-latest + outputs: + code: ${{ steps.filter.outputs.code }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + code: + - 'v8pp/**' + - 'test/**' + - 'plugins/**' + - 'examples/**' + - 'CMakeLists.txt' + - 'cmake/**' + - '.github/workflows/**' + + format: + needs: changes + if: needs.changes.outputs.code == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install clang-format-19 + run: | + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-19 main" | sudo tee /etc/apt/sources.list.d/llvm-19.list + sudo apt-get update && sudo apt-get install -y clang-format-19 + - name: Check formatting with clang-format + run: | + find v8pp test plugins \ + -path '*/out/*' -prune -o \ + \( -name '*.hpp' -o -name '*.cpp' -o -name '*.ipp' \) -print | \ + xargs clang-format-19 --dry-run --Werror + build: + needs: changes + if: needs.changes.outputs.code == 'true' timeout-minutes: 30 strategy: fail-fast: false @@ -36,7 +75,7 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install V8 apt + - name: Install V8 apt if: startsWith(matrix.os, 'ubuntu') run: sudo apt update && sudo apt install libv8-dev -y @@ -67,8 +106,22 @@ jobs: - name: Install ninja-build tool uses: seanmiddleditch/gha-setup-ninja@v5 + - name: Set up ccache + if: ${{ !startsWith(matrix.os, 'windows') }} + uses: hendrikmuhs/ccache-action@v1 + with: + key: ${{matrix.os}}-${{matrix.shared_lib}}-${{matrix.header_only}} + - name: Configure CMake - run: cmake -G Ninja -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{matrix.build_type}} -DBUILD_TESTING=TRUE -DBUILD_SHARED_LIBS=${{matrix.shared_lib}} -DV8PP_HEADER_ONLY=${{matrix.header_only}} -DV8_COMPRESS_POINTERS=${{matrix.v8_compress_pointers}} -DV8_ENABLE_SANDBOX=${{matrix.v8_enable_sandbox}} + run: > + cmake -G Ninja -B ${{github.workspace}}/build + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} + -DBUILD_TESTING=TRUE + -DBUILD_SHARED_LIBS=${{matrix.shared_lib}} + -DV8PP_HEADER_ONLY=${{matrix.header_only}} + -DV8_COMPRESS_POINTERS=${{matrix.v8_compress_pointers}} + -DV8_ENABLE_SANDBOX=${{matrix.v8_enable_sandbox}} + ${{ !startsWith(matrix.os, 'windows') && '-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache' || '' }} - name: Build run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}} diff --git a/plugins/file.cpp b/plugins/file.cpp index d0f0e91..e145aed 100644 --- a/plugins/file.cpp +++ b/plugins/file.cpp @@ -94,8 +94,7 @@ v8::Local init(v8::Isolate* isolate) .function("close", &file_base::close) .function("good", &file_base::good) .function("is_open", &file_base::is_open) - .function("eof", &file_base::eof) - ; + .function("eof", &file_base::eof); // .ctor<> template arguments declares types of file_writer constructor // file_writer inherits from file_base_class @@ -105,8 +104,7 @@ v8::Local init(v8::Isolate* isolate) .inherit() .function("open", &file_writer::open) .function("print", &file_writer::print) - .function("println", &file_writer::println) - ; + .function("println", &file_writer::println); // .ctor<> template arguments declares types of file_reader constructor. // file_base inherits from file_base_class @@ -115,16 +113,13 @@ v8::Local init(v8::Isolate* isolate) .ctor() .inherit() .function("open", &file_reader::open) - .function("getln", &file_reader::getline) - ; + .function("getln", &file_reader::getline); // Create a module to add classes and functions to and return a // new instance of the module to be embedded into the v8 context v8pp::module m(isolate); m.function("rename", +[](char const* src, char const* dest) -> bool - { - return std::rename(src, dest) == 0; - }); + { return std::rename(src, dest) == 0; }); m.class_("writer", file_writer_class); m.class_("reader", file_reader_class); diff --git a/test/main.cpp b/test/main.cpp index fac1f77..375bbde 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -36,30 +36,29 @@ void run_tests() void test_adversarial(); void test_thread_safety(); - std::pair tests[] = - { + std::pair tests[] = { { "test_type_info", test_type_info }, - {"test_utility", test_utility}, - {"test_context", test_context}, - {"test_context_store", test_context_store}, - {"test_convert", test_convert}, - {"test_throw_ex", test_throw_ex}, - {"test_function", test_function}, - {"test_ptr_traits", test_ptr_traits}, - {"test_call_v8", test_call_v8}, - {"test_call_from_v8", test_call_from_v8}, - {"test_module", test_module}, - {"test_class", test_class}, - {"test_property", test_property}, - {"test_object", test_object}, - {"test_json", test_json}, - {"test_overload", test_overload}, - {"test_fast_api", test_fast_api}, - {"test_symbol", test_symbol}, - {"test_promise", test_promise}, - {"test_gc_stress", test_gc_stress}, - {"test_adversarial", test_adversarial}, - {"test_thread_safety", test_thread_safety}, + { "test_utility", test_utility }, + { "test_context", test_context }, + { "test_context_store", test_context_store }, + { "test_convert", test_convert }, + { "test_throw_ex", test_throw_ex }, + { "test_function", test_function }, + { "test_ptr_traits", test_ptr_traits }, + { "test_call_v8", test_call_v8 }, + { "test_call_from_v8", test_call_from_v8 }, + { "test_module", test_module }, + { "test_class", test_class }, + { "test_property", test_property }, + { "test_object", test_object }, + { "test_json", test_json }, + { "test_overload", test_overload }, + { "test_fast_api", test_fast_api }, + { "test_symbol", test_symbol }, + { "test_promise", test_promise }, + { "test_gc_stress", test_gc_stress }, + { "test_adversarial", test_adversarial }, + { "test_thread_safety", test_thread_safety }, }; for (auto const& test : tests) @@ -91,22 +90,21 @@ int main(int argc, char const* argv[]) if (arg == "-h" || arg == "--help") { std::cout << "Usage: " << argv[0] << " [arguments] [script]\n" - << "Arguments:\n" - << " --help,-h Print this message and exit\n" - << " --version,-v Print V8 version\n" - << " --lib-path Set for plugins library path\n" - << " --run-tests Run library tests\n" - ; + << "Arguments:\n" + << " --help,-h Print this message and exit\n" + << " --version,-v Print V8 version\n" + << " --lib-path Set for plugins library path\n" + << " --run-tests Run library tests\n"; return EXIT_SUCCESS; } else if (arg == "-v" || arg == "--version") { std::cout << "V8 version " << v8::V8::GetVersion() << std::endl; std::cout << "v8pp version " << v8pp::version() - << " (major=" << v8pp::version_major() - << " minor=" << v8pp::version_minor() - << " patch=" << v8pp::version_patch() - << ")\n"; + << " (major=" << v8pp::version_major() + << " minor=" << v8pp::version_minor() + << " patch=" << v8pp::version_patch() + << ")\n"; std::cout << "v8pp build options " << v8pp::build_options() << std::endl; } else if (arg == "--lib-path") @@ -128,7 +126,7 @@ int main(int argc, char const* argv[]) // for v8pp::class_ tests v8::V8::SetFlagsFromString("--expose_gc"); - //v8::V8::InitializeICU(); + // v8::V8::InitializeICU(); v8::V8::InitializeExternalStartupData(argv[0]); #if V8_MAJOR_VERSION >= 7 std::unique_ptr platform(v8::platform::NewDefaultPlatform()); diff --git a/test/test.hpp b/test/test.hpp index 0f69ac0..6d378fa 100644 --- a/test/test.hpp +++ b/test/test.hpp @@ -151,7 +151,8 @@ std::ostream& operator<<(std::ostream& os, std::pair const& pair) return os << pair.first << ": " << pair.second; } -template requires std::is_enum_v +template +requires std::is_enum_v std::ostream& operator<<(std::ostream& os, Enum value) { return os << static_cast>(value); @@ -167,12 +168,11 @@ template std::ostream& operator<<(std::ostream& os, std::tuple const& tuple) { std::apply([&os](auto&&... elems) mutable - { + { bool first = true; os << '('; ((os << (first ? (first = false, "") : ", ") << elems), ...); - os << ')'; - }, tuple); + os << ')'; }, tuple); return os; } diff --git a/test/test_adversarial.cpp b/test/test_adversarial.cpp index 3085939..7521414 100644 --- a/test/test_adversarial.cpp +++ b/test/test_adversarial.cpp @@ -1,320 +1,322 @@ -#include "v8pp/class.hpp" -#include "v8pp/context.hpp" -#include "v8pp/convert.hpp" - -#include "test.hpp" - -#include -#include - -namespace { - -struct Adv -{ - int value; - - explicit Adv(int v = 0) : value(v) {} - int get() const { return value; } - void set(int v) { value = v; } - int add(int x) const { return value + x; } -}; - -struct Adv2 -{ - std::string name; - explicit Adv2(std::string n = "") : name(std::move(n)) {} - std::string get_name() const { return name; } -}; - -struct ThrowingObj -{ - static std::atomic instance_count; - int value; - - explicit ThrowingObj(int v) - { - if (v < 0) throw std::runtime_error("negative value"); - value = v; - ++instance_count; - } - ~ThrowingObj() { --instance_count; } - - int get() const { return value; } - - int throwing_method() const - { - throw std::runtime_error("method error"); - } - - int throwing_getter() const - { - throw std::runtime_error("getter error"); - } - - void throwing_setter(int) - { - throw std::runtime_error("setter error"); - } -}; - -std::atomic ThrowingObj::instance_count = 0; - -// -// Adversarial JS tests -// - -template -void test_adversarial_js() -{ - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ adv_class(isolate); - adv_class - .template ctor() - .var("value", &Adv::value) - .property("prop", &Adv::get, &Adv::set) - .function("add", &Adv::add) - .function("get", &Adv::get); - - v8pp::class_ adv2_class(isolate); - adv2_class - .template ctor() - .function("get_name", &Adv2::get_name); - - context - .class_("Adv", adv_class) - .class_("Adv2", adv2_class); - - // Proxy forwarding: method call through proxy uses proxy as `this`, - // which has no internal fields, so unwrap_object correctly rejects it. - // The key assertion is no crash. - check_eq("proxy forwarding", - run_script(context, - "var x = new Adv(5);" - "var p = new Proxy(x, {" - " get: function(t, prop) { return t[prop]; }," - " set: function(t, prop, val) { t[prop] = val; return true; }" - "});" - "try { String(p.add(10)); } catch(e) { 'caught'; }"), - "caught"); - - // Proxy with throwing get trap - check_eq("proxy throwing trap", - run_script(context, - "var x = new Adv(5);" - "var p = new Proxy(x, {" - " get: function(t, prop) { throw new Error('trap!'); }" - "});" - "try { p.add(1); 'no error'; } catch(e) { 'caught'; }"), - "caught"); - - // defineProperty on wrapped instance — may throw TypeError (non-configurable) - // or succeed depending on V8 version and property type. Must not crash. - { - auto result = run_script(context, - "var x = new Adv(5);" - "try {" - " Object.defineProperty(x, 'value', { get: function() { return 999; } });" - " 'redefined';" - "} catch(e) { 'caught'; }"); - check("defineProperty on wrapped instance (no crash)", - result == "caught" || result == "redefined"); - } - - // Frozen object — read should still work - check_eq("frozen object read", - run_script(context, - "var x = new Adv(42);" - "Object.freeze(x);" - "x.get()"), - 42); - - // Frozen object — mutation attempt. Native interceptors may bypass freeze. - // Must not crash regardless of outcome. - { - auto result = run_script(context, - "'use strict';" - "var x = new Adv(42);" - "Object.freeze(x);" - "try { x.value = 10; 'no error'; } catch(e) { 'caught'; }"); - check("frozen object mutate (no crash)", - result == "caught" || result == "no error"); - } - - // Null prototype — method call should fail (method no longer on chain) - check_eq("null prototype method call", - run_script(context, - "var x = new Adv(5);" - "Object.setPrototypeOf(x, null);" - "try { x.add(1); 'no error'; } catch(e) { 'caught'; }"), - "caught"); - - // Constructor called as function without `new` — should throw, not crash - check_eq("constructor without new", - run_script(context, - "try { Adv(1); 'no error'; } catch(e) { 'caught'; }"), - "caught"); - - // Circular prototype attempt — V8 prevents this - check_eq("circular prototype", - run_script(context, - "var a = {}; var b = {};" - "Object.setPrototypeOf(a, b);" - "try { Object.setPrototypeOf(b, a); 'no error'; } catch(e) { 'caught'; }"), - "caught"); - - // GetOwnPropertyDescriptor on native property — should not crash. - // Property may be on prototype (not own), so descriptor can be undefined. - { - auto result = run_script(context, - "var x = new Adv(7);" - "var desc = Object.getOwnPropertyDescriptor(x, 'value');" - "desc !== undefined ? 'own' : 'proto'"); - check("getOwnPropertyDescriptor (no crash)", - result == "own" || result == "proto"); - } - - // Spread wrapped object — must not crash - { - auto result = run_script(context, - "var x = new Adv(3);" - "try { var copy = {...x}; 'ok'; } catch(e) { 'caught'; }"); - check("spread wrapped object (no crash)", - result == "ok" || result == "caught"); - } - - // Prototype swap between different wrapped types - check_eq("prototype swap between types", - run_script(context, - "var a = new Adv(1);" - "var b = new Adv2('hello');" - "try {" - " Object.setPrototypeOf(a, Object.getPrototypeOf(b));" - " a.get_name();" - " 'no error';" - "} catch(e) { 'caught'; }"), - "caught"); - - // Method extracted and called on wrong receiver - check_eq("method on wrong receiver", - run_script(context, - "var x = new Adv(5);" - "var f = x.add;" - "try { f.call({}, 1); 'no error'; } catch(e) { 'caught'; }"), - "caught"); - - // Method called on plain object via .call() - check_eq("method via call on plain obj", - run_script(context, - "try { var x = new Adv(5); x.add.call({value: 99}, 1); 'no error'; } catch(e) { 'caught'; }"), - "caught"); - - // Property getter extracted and called on wrong receiver - check_eq("property getter on wrong receiver", - run_script(context, - "var desc = Object.getOwnPropertyDescriptor(new Adv(5), 'prop');" - "try { desc.get.call({}); 'no error'; } catch(e) { 'caught'; }"), - "caught"); - - // Property setter extracted and called on wrong receiver - check_eq("property setter on wrong receiver", - run_script(context, - "var desc = Object.getOwnPropertyDescriptor(new Adv(5), 'prop');" - "try { desc.set.call({}, 42); 'no error'; } catch(e) { 'caught'; }"), - "caught"); - - // Deep prototype chain (beyond 16 limit) - check_eq("deep prototype chain", - run_script(context, - "var obj = {};" - "for (var i = 0; i < 20; i++) { obj = Object.create(obj); }" - "try { var x = new Adv(1); x.add.call(obj, 1); 'no error'; } catch(e) { 'caught'; }"), - "caught"); -} - -// -// Exception safety tests -// - -template -void test_exception_safety() -{ - ThrowingObj::instance_count = 0; - - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ throwing_class(isolate); - throwing_class - .template ctor() - .function("get", &ThrowingObj::get) - .function("throwing_method", &ThrowingObj::throwing_method) - .property("throwing_prop", &ThrowingObj::throwing_getter, &ThrowingObj::throwing_setter); - - context.class_("ThrowingObj", throwing_class); - - // Constructor that throws — should produce JS exception, not crash - check_eq("throwing ctor produces JS exception", - run_script(context, - "try { new ThrowingObj(-1); 'no error'; } catch(e) { e.message; }"), - "negative value"); - - // No instances should have been created - check_eq("throwing ctor no leak", ThrowingObj::instance_count.load(), 0); - - // Successful construction - check_eq("successful ctor", run_script(context, "var t = new ThrowingObj(5); t.get()"), 5); - check_eq("instance created", ThrowingObj::instance_count.load(), 1); - - // Method that throws — should produce JS exception, object still valid - check_eq("throwing method", - run_script(context, - "try { t.throwing_method(); 'no error'; } catch(e) { e.message; }"), - "method error"); - - // Object should still be usable after method exception - check_eq("object valid after method throw", run_script(context, "t.get()"), 5); - - // Property getter that throws — v8pp's property_get catch block only - // re-throws to JS when ShouldThrowOnError() (strict mode). In sloppy mode - // the C++ exception is silently swallowed and the property reads as undefined. - { - auto result = run_script(context, - "try { t.throwing_prop; 'no error'; } catch(e) { e.message; }"); - check("throwing property getter (no crash)", - result == "getter error" || result == "no error"); - } - - // Property setter that throws — same ShouldThrowOnError behavior - { - auto result = run_script(context, - "try { t.throwing_prop = 42; 'no error'; } catch(e) { e.message; }"); - check("throwing property setter (no crash)", - result == "setter error" || result == "no error"); - } - - // Object still valid after property exceptions - check_eq("object valid after prop throw", run_script(context, "t.get()"), 5); - - // Destroy objects, then try to use from JS - v8pp::class_::destroy_objects(isolate); - - check_eq("use after destroy_objects", - run_script(context, - "try { t.get(); 'no error'; } catch(e) { 'caught'; }"), - "caught"); -} - -} // anonymous namespace - -void test_adversarial() -{ - test_adversarial_js(); - test_adversarial_js(); - - test_exception_safety(); - test_exception_safety(); -} +#include "v8pp/class.hpp" +#include "v8pp/context.hpp" +#include "v8pp/convert.hpp" + +#include "test.hpp" + +#include +#include + +namespace { + +struct Adv +{ + int value; + + explicit Adv(int v = 0) + : value(v) {} + int get() const { return value; } + void set(int v) { value = v; } + int add(int x) const { return value + x; } +}; + +struct Adv2 +{ + std::string name; + explicit Adv2(std::string n = "") + : name(std::move(n)) {} + std::string get_name() const { return name; } +}; + +struct ThrowingObj +{ + static std::atomic instance_count; + int value; + + explicit ThrowingObj(int v) + { + if (v < 0) throw std::runtime_error("negative value"); + value = v; + ++instance_count; + } + ~ThrowingObj() { --instance_count; } + + int get() const { return value; } + + int throwing_method() const + { + throw std::runtime_error("method error"); + } + + int throwing_getter() const + { + throw std::runtime_error("getter error"); + } + + void throwing_setter(int) + { + throw std::runtime_error("setter error"); + } +}; + +std::atomic ThrowingObj::instance_count = 0; + +// +// Adversarial JS tests +// + +template +void test_adversarial_js() +{ + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ adv_class(isolate); + adv_class + .template ctor() + .var("value", &Adv::value) + .property("prop", &Adv::get, &Adv::set) + .function("add", &Adv::add) + .function("get", &Adv::get); + + v8pp::class_ adv2_class(isolate); + adv2_class + .template ctor() + .function("get_name", &Adv2::get_name); + + context + .class_("Adv", adv_class) + .class_("Adv2", adv2_class); + + // Proxy forwarding: method call through proxy uses proxy as `this`, + // which has no internal fields, so unwrap_object correctly rejects it. + // The key assertion is no crash. + check_eq("proxy forwarding", + run_script(context, + "var x = new Adv(5);" + "var p = new Proxy(x, {" + " get: function(t, prop) { return t[prop]; }," + " set: function(t, prop, val) { t[prop] = val; return true; }" + "});" + "try { String(p.add(10)); } catch(e) { 'caught'; }"), + "caught"); + + // Proxy with throwing get trap + check_eq("proxy throwing trap", + run_script(context, + "var x = new Adv(5);" + "var p = new Proxy(x, {" + " get: function(t, prop) { throw new Error('trap!'); }" + "});" + "try { p.add(1); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // defineProperty on wrapped instance — may throw TypeError (non-configurable) + // or succeed depending on V8 version and property type. Must not crash. + { + auto result = run_script(context, + "var x = new Adv(5);" + "try {" + " Object.defineProperty(x, 'value', { get: function() { return 999; } });" + " 'redefined';" + "} catch(e) { 'caught'; }"); + check("defineProperty on wrapped instance (no crash)", + result == "caught" || result == "redefined"); + } + + // Frozen object — read should still work + check_eq("frozen object read", + run_script(context, + "var x = new Adv(42);" + "Object.freeze(x);" + "x.get()"), + 42); + + // Frozen object — mutation attempt. Native interceptors may bypass freeze. + // Must not crash regardless of outcome. + { + auto result = run_script(context, + "'use strict';" + "var x = new Adv(42);" + "Object.freeze(x);" + "try { x.value = 10; 'no error'; } catch(e) { 'caught'; }"); + check("frozen object mutate (no crash)", + result == "caught" || result == "no error"); + } + + // Null prototype — method call should fail (method no longer on chain) + check_eq("null prototype method call", + run_script(context, + "var x = new Adv(5);" + "Object.setPrototypeOf(x, null);" + "try { x.add(1); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // Constructor called as function without `new` — should throw, not crash + check_eq("constructor without new", + run_script(context, + "try { Adv(1); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // Circular prototype attempt — V8 prevents this + check_eq("circular prototype", + run_script(context, + "var a = {}; var b = {};" + "Object.setPrototypeOf(a, b);" + "try { Object.setPrototypeOf(b, a); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // GetOwnPropertyDescriptor on native property — should not crash. + // Property may be on prototype (not own), so descriptor can be undefined. + { + auto result = run_script(context, + "var x = new Adv(7);" + "var desc = Object.getOwnPropertyDescriptor(x, 'value');" + "desc !== undefined ? 'own' : 'proto'"); + check("getOwnPropertyDescriptor (no crash)", + result == "own" || result == "proto"); + } + + // Spread wrapped object — must not crash + { + auto result = run_script(context, + "var x = new Adv(3);" + "try { var copy = {...x}; 'ok'; } catch(e) { 'caught'; }"); + check("spread wrapped object (no crash)", + result == "ok" || result == "caught"); + } + + // Prototype swap between different wrapped types + check_eq("prototype swap between types", + run_script(context, + "var a = new Adv(1);" + "var b = new Adv2('hello');" + "try {" + " Object.setPrototypeOf(a, Object.getPrototypeOf(b));" + " a.get_name();" + " 'no error';" + "} catch(e) { 'caught'; }"), + "caught"); + + // Method extracted and called on wrong receiver + check_eq("method on wrong receiver", + run_script(context, + "var x = new Adv(5);" + "var f = x.add;" + "try { f.call({}, 1); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // Method called on plain object via .call() + check_eq("method via call on plain obj", + run_script(context, + "try { var x = new Adv(5); x.add.call({value: 99}, 1); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // Property getter extracted and called on wrong receiver + check_eq("property getter on wrong receiver", + run_script(context, + "var desc = Object.getOwnPropertyDescriptor(new Adv(5), 'prop');" + "try { desc.get.call({}); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // Property setter extracted and called on wrong receiver + check_eq("property setter on wrong receiver", + run_script(context, + "var desc = Object.getOwnPropertyDescriptor(new Adv(5), 'prop');" + "try { desc.set.call({}, 42); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // Deep prototype chain (beyond 16 limit) + check_eq("deep prototype chain", + run_script(context, + "var obj = {};" + "for (var i = 0; i < 20; i++) { obj = Object.create(obj); }" + "try { var x = new Adv(1); x.add.call(obj, 1); 'no error'; } catch(e) { 'caught'; }"), + "caught"); +} + +// +// Exception safety tests +// + +template +void test_exception_safety() +{ + ThrowingObj::instance_count = 0; + + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ throwing_class(isolate); + throwing_class + .template ctor() + .function("get", &ThrowingObj::get) + .function("throwing_method", &ThrowingObj::throwing_method) + .property("throwing_prop", &ThrowingObj::throwing_getter, &ThrowingObj::throwing_setter); + + context.class_("ThrowingObj", throwing_class); + + // Constructor that throws — should produce JS exception, not crash + check_eq("throwing ctor produces JS exception", + run_script(context, + "try { new ThrowingObj(-1); 'no error'; } catch(e) { e.message; }"), + "negative value"); + + // No instances should have been created + check_eq("throwing ctor no leak", ThrowingObj::instance_count.load(), 0); + + // Successful construction + check_eq("successful ctor", run_script(context, "var t = new ThrowingObj(5); t.get()"), 5); + check_eq("instance created", ThrowingObj::instance_count.load(), 1); + + // Method that throws — should produce JS exception, object still valid + check_eq("throwing method", + run_script(context, + "try { t.throwing_method(); 'no error'; } catch(e) { e.message; }"), + "method error"); + + // Object should still be usable after method exception + check_eq("object valid after method throw", run_script(context, "t.get()"), 5); + + // Property getter that throws — v8pp's property_get catch block only + // re-throws to JS when ShouldThrowOnError() (strict mode). In sloppy mode + // the C++ exception is silently swallowed and the property reads as undefined. + { + auto result = run_script(context, + "try { t.throwing_prop; 'no error'; } catch(e) { e.message; }"); + check("throwing property getter (no crash)", + result == "getter error" || result == "no error"); + } + + // Property setter that throws — same ShouldThrowOnError behavior + { + auto result = run_script(context, + "try { t.throwing_prop = 42; 'no error'; } catch(e) { e.message; }"); + check("throwing property setter (no crash)", + result == "setter error" || result == "no error"); + } + + // Object still valid after property exceptions + check_eq("object valid after prop throw", run_script(context, "t.get()"), 5); + + // Destroy objects, then try to use from JS + v8pp::class_::destroy_objects(isolate); + + check_eq("use after destroy_objects", + run_script(context, + "try { t.get(); 'no error'; } catch(e) { 'caught'; }"), + "caught"); +} + +} // anonymous namespace + +void test_adversarial() +{ + test_adversarial_js(); + test_adversarial_js(); + + test_exception_safety(); + test_exception_safety(); +} diff --git a/test/test_call_from_v8.cpp b/test/test_call_from_v8.cpp index cc638b1..a64b174 100644 --- a/test/test_call_from_v8.cpp +++ b/test/test_call_from_v8.cpp @@ -48,7 +48,9 @@ static_assert(std::same_as, v8pp::co static_assert(std::same_as, v8pp::convert>); // cv arg converters -static void s(std::string, std::vector&, std::shared_ptr const&, std::string*, std::string const*) {} +static void s(std::string, std::vector&, std::shared_ptr const&, std::string*, std::string const*) +{ +} static_assert(std::same_as, v8pp::convert>); static_assert(std::same_as, v8pp::convert>); @@ -66,7 +68,9 @@ static_assert(std::same_as, v8pp::co static_assert(std::same_as, v8pp::convert>); // fundamental types cv arg converters -static void t(int, char&, bool const&, float*, char const*) {} +static void t(int, char&, bool const&, float*, char const*) +{ +} static_assert(std::same_as, v8pp::convert>); static_assert(std::same_as, v8pp::convert>); @@ -89,8 +93,8 @@ void test_call_from_v8() v8::Isolate* isolate = context.isolate(); v8::HandleScope scope(isolate); - (void)&s; //context.function("s", s); - (void)&t; //context.function("t", t); + (void)&s; // context.function("s", s); + (void)&t; // context.function("t", t); context.function("x", x); context.function("y", y); context.function("z", z); @@ -104,14 +108,20 @@ void test_call_from_v8() // --- Default parameter tests --- // Free function with 1 default - static auto add_default = [](int a, int b) { return a + b; }; + static auto add_default = [](int a, int b) + { + return a + b; + }; context.function("add_default", add_default, v8pp::defaults(10)); check_eq("defaults: all args provided", run_script(context, "add_default(3, 7)"), 10); check_eq("defaults: 1 default used", run_script(context, "add_default(5)"), 15); // Free function with 2 defaults - static auto three_args = [](int a, int b, int c) { return a + b + c; }; + static auto three_args = [](int a, int b, int c) + { + return a + b + c; + }; context.function("three_args", three_args, v8pp::defaults(20, 30)); check_eq("defaults: 2 defaults, all provided", run_script(context, "three_args(1, 2, 3)"), 6); @@ -120,18 +130,17 @@ void test_call_from_v8() // Too few args should throw check_ex("defaults: too few args", [&context] - { - run_script(context, "three_args()"); - }); + { run_script(context, "three_args()"); }); // Too many args should throw check_ex("defaults: too many args", [&context] - { - run_script(context, "three_args(1, 2, 3, 4)"); - }); + { run_script(context, "three_args(1, 2, 3, 4)"); }); // String default - static auto greet = [](std::string name, std::string greeting) { return greeting + " " + name; }; + static auto greet = [](std::string name, std::string greeting) + { + return greeting + " " + name; + }; context.function("greet", greet, v8pp::defaults(std::string("hello"))); check_eq("defaults: string default used", run_script(context, "greet('world')"), "hello world"); @@ -140,7 +149,8 @@ void test_call_from_v8() // Module function with defaults { v8pp::module m(context.isolate()); - m.function("multiply", [](int a, int b) { return a * b; }, v8pp::defaults(2)); + m.function("multiply", [](int a, int b) + { return a * b; }, v8pp::defaults(2)); context.module("def_mod", m); check_eq("module defaults: provided", run_script(context, "def_mod.multiply(3, 4)"), 12); @@ -152,7 +162,11 @@ void test_call_from_v8() struct Counter { int value = 0; - int add(int n) { value += n; return value; } + int add(int n) + { + value += n; + return value; + } }; v8pp::class_ counter_class(context.isolate()); @@ -171,7 +185,8 @@ void test_call_from_v8() { std::string name; int value; - Named(std::string n, int v) : name(std::move(n)), value(v) {} + Named(std::string n, int v) + : name(std::move(n)), value(v) {} }; v8pp::class_ named_class(context.isolate()); @@ -185,4 +200,16 @@ void test_call_from_v8() check_eq("ctor defaults: default used", run_script(context, "var n2 = new Named('test'); n2.value"), 42); check_eq("ctor defaults: name correct", run_script(context, "n2.name"), "test"); } + + // --- undefined triggers default (matches JS native behavior) --- + // In JS: function foo(a, b = 10) {} foo(1, undefined) → b is 10 + + check_eq("defaults: undefined uses default", run_script(context, "add_default(3, undefined)"), 13); + check_eq("defaults: undefined middle arg", run_script(context, "three_args(1, undefined, 3)"), 24); + check_eq("defaults: undefined last arg", run_script(context, "three_args(1, 2, undefined)"), 33); + check_eq("defaults: both undefined", run_script(context, "three_args(1, undefined, undefined)"), 51); + + // String default with undefined + check_eq("defaults: string undefined uses default", + run_script(context, "greet('world', undefined)"), "hello world"); } diff --git a/test/test_class.cpp b/test/test_class.cpp index e273514..4d300b4 100644 --- a/test/test_class.cpp +++ b/test/test_class.cpp @@ -100,13 +100,20 @@ struct Y : X { static int instance_count; - explicit Y(int x) { var = x; ++instance_count; } + explicit Y(int x) + { + var = x; + ++instance_count; + } ~Y() { --instance_count; } int useX(X& x) { return var + x.var; } template::object_pointer_type> - int useX_ptr(X_ptr x) { return var + x->var; } + int useX_ptr(X_ptr x) + { + return var + x->var; + } }; int Y::instance_count = 0; @@ -163,14 +170,18 @@ void test_class_() .template ctor const&>(X_ctor) .const_("konst", 99) .var("var", &X::var) - //TODO: static property definition works only at the end of class_ declaration! + // TODO: static property definition works only at the end of class_ declaration! //.static_("my_static_var", 1) //.static_("my_static_const_var", 42, true) .property("rprop", &X::get) .property("wprop", &X::get, &X::set) .property("wprop2", static_cast(&X::prop), static_cast(&X::prop)) - .property("prop", [](X const& x) mutable { return x.var; }, [](X& x, int n) { x.var = n; }) - .property("prop2", [](X const& x) { return x.var; }, [](X& x, int n) mutable { x.var = n; }) + .property("prop", [](X const& x) mutable + { return x.var; }, [](X& x, int n) + { x.var = n; }) + .property("prop2", [](X const& x) + { return x.var; }, [](X& x, int n) mutable + { x.var = n; }) .property("rprop_direct", &get_rprop_direct) .property("rprop_external1", &external_get1) .property("rprop_external2", &external_get2) @@ -183,12 +194,12 @@ void test_class_() .function("fun3", &X::fun3) .function("fun4", &X::fun4) .function("static_fun", &X::static_fun) - .function("static_lambda", [](int x) { return x + 3; }) + .function("static_lambda", [](int x) + { return x + 3; }) .function("extern_fun", &extern_fun) .function("toJSON", &X::to_json) .static_("my_static_var", 1) - .static_("my_static_const_var", 42, true) - ; + .static_("my_static_const_var", 42, true); static_assert(std::is_move_constructible_v); static_assert(!std::is_move_assignable_v); @@ -204,28 +215,20 @@ void test_class_() auto Y_class_find = v8pp::class_::extend(isolate); Y_class_find.function("toJSON", [](const v8::FunctionCallbackInfo& args) - { + { bool const with_functions = true; - args.GetReturnValue().Set(v8pp::json_object(args.GetIsolate(), args.This(), with_functions)); - }); + args.GetReturnValue().Set(v8pp::json_object(args.GetIsolate(), args.This(), with_functions)); }); check_ex("already wrapped class X", [isolate]() - { - v8pp::class_ X_class(isolate); - }); + { v8pp::class_ X_class(isolate); }); check_ex("already inherited class X", [&Y_class]() - { - Y_class.template inherit(); - }); + { Y_class.template inherit(); }); check_ex("unwrapped class Z", [isolate]() - { - v8pp::class_::find_object(isolate, nullptr); - }); + { v8pp::class_::find_object(isolate, nullptr); }); context .class_("X", X_class) - .class_("Y", Y_class) - ; + .class_("Y", Y_class); check_eq("C++ exception from X ctor", run_script(context, "ret = ''; try { new X(1, 2); } catch(err) { ret = err.message; } ret"), @@ -260,7 +263,7 @@ void test_class_() check_eq("X::fun3(str)", run_script(context, "x = new X(); x.fun3('str')"), "str1"); check_eq("X::fun4([foo, bar])", run_script>(context, "x = new X(); x.fun4(['foo', 'bar'])"), - std::vector{{ "foo", "bar", "1" }}); + std::vector{ { "foo", "bar", "1" } }); check_eq("X::static_fun(1)", run_script(context, "X.static_fun(1)"), 1); check_eq("X::static_lambda(1)", run_script(context, "X.static_lambda(1)"), 4); check_eq("X::extern_fun(5)", run_script(context, "x = new X(); x.extern_fun(5)"), 6); @@ -271,9 +274,7 @@ void test_class_() check_eq("X::my_static_var after assign", run_script(context, "X.my_static_var = 123; X.my_static_var"), 123); check_ex("call method with invalid instance", [&context]() - { - run_script(context, "x = new X(); f = x.fun1; f(1)"); - }); + { run_script(context, "x = new X(); f = x.fun1; f(1)"); }); // Crash safety: method call on plain object via .call() should throw JS error, not crash check_eq("method call on plain object via .call()", @@ -311,8 +312,7 @@ void test_class_() check_eq("JSON.stringify(X)", run_script(context, "JSON.stringify({'obj': new X(10), 'arr': [new X(11), new X(12)] })"), - R"({"obj":{"key":"obj","var":10},"arr":[{"key":"0","var":11},{"key":"1","var":12}]})" - ); + R"({"obj":{"key":"obj","var":10},"arr":[{"key":"0","var":11},{"key":"1","var":12}]})"); // Use order-independent comparison since V8 property enumeration order // on prototype templates varies across V8 versions @@ -338,8 +338,7 @@ void test_class_() && r.arr.length === 2; })() )"), - true - ); + true); check_eq("Y object", run_script(context, "y = new Y(-100); y.konst + y.var"), -1); @@ -369,9 +368,7 @@ void test_class_() check("unref y1_obj", v8pp::to_v8(isolate, y1).IsEmpty()); y1_obj.Clear(); check_ex("y1 unreferenced", [isolate, &y1]() - { - v8pp::to_v8(isolate, y1); - }); + { v8pp::to_v8(isolate, y1); }); v8pp::class_::destroy_object(isolate, y2); check("unref y2", !v8pp::from_v8(isolate, y2_obj)); @@ -407,7 +404,8 @@ void test_multiple_inheritance() { char a = 'A'; int x; - A() : x(1) {} + A() + : x(1) {} int f() { return x; } void set_f(int v) { x = v; } @@ -418,17 +416,20 @@ void test_multiple_inheritance() { char b = 'B'; int x; - B() : x(2) {} + B() + : x(2) {} int g() { return x; } void set_g(int v) { x = v; } int z() const { return x; } }; - struct C : A, B + struct C : A + , B { int x; - C() : x(3) {} + C() + : x(3) {} int h() { return x; } void set_h(int v) { x = v; } @@ -464,21 +465,17 @@ void test_multiple_inheritance() .property("F", &C::f, &C::set_f) .property("G", &C::g, &C::set_g) - .property("H", &C::h, &C::set_h) - ; + .property("H", &C::h, &C::set_h); context.class_("C", C_class); check_eq("get attributes", run_script(context, "c = new C(); c.xA + c.xB + c.xC"), 1 + 2 + 3); - check_eq("set attributes", run_script(context, - "c = new C(); c.xA = 10; c.xB = 20; c.xC = 30; c.xA + c.xB + c.xC"), 10 + 20 + 30); + check_eq("set attributes", run_script(context, "c = new C(); c.xA = 10; c.xB = 20; c.xC = 30; c.xA + c.xB + c.xC"), 10 + 20 + 30); check_eq("functions", run_script(context, "c = new C(); c.f() + c.g() + c.h()"), 1 + 2 + 3); check_eq("z functions", run_script(context, "c = new C(); c.zA() + c.zB() + c.zC()"), 1 + 2 + 3); - check_eq("rproperties", run_script(context, - "c = new C(); c.rF + c.rG + c.rH"), 1 + 2 + 3); - check_eq("rwproperties", run_script(context, - "c = new C(); c.F = 100; c.G = 200; c.H = 300; c.F + c.G + c.H"), 100 + 200 + 300); + check_eq("rproperties", run_script(context, "c = new C(); c.rF + c.rG + c.rH"), 1 + 2 + 3); + check_eq("rwproperties", run_script(context, "c = new C(); c.F = 100; c.G = 200; c.H = 300; c.F + c.G + c.H"), 100 + 200 + 300); } template @@ -517,7 +514,8 @@ void test_auto_wrap_objects() struct X { int x; - explicit X(int x) : x(x) {} + explicit X(int x) + : x(x) {} int get_x() const { return x; } }; @@ -529,16 +527,195 @@ void test_auto_wrap_objects() X_class .template ctor() .auto_wrap_objects(true) - .property("x", &X::get_x) - ; + .property("x", &X::get_x); - auto f = [](int x) { return X(x); }; + auto f = [](int x) + { + return X(x); + }; context.class_("X", X_class); context.function("f", std::move(f)); check_eq("return X object", run_script(context, "obj = f(123); obj.x"), 123); } +template +void test_ctor_factory_defaults() +{ + struct Widget + { + int width; + int height; + std::string label; + + Widget(int w, int h, std::string l) + : width(w), height(h), label(std::move(l)) {} + }; + + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + // Factory function that returns the right pointer type + auto make_widget = [](int w, int h, std::string label) + -> typename Traits::template object_pointer_type + { + return Traits::template create(w, h, std::move(label)); + }; + + v8pp::class_ widget_class(isolate); + widget_class + .ctor(make_widget, v8pp::defaults(100, 200, std::string("untitled"))) + .var("width", &Widget::width) + .var("height", &Widget::height) + .var("label", &Widget::label); + + context.class_("Widget", widget_class); + + // All defaults + check_eq("ctor factory+defaults: 0 args width", + run_script(context, "w = new Widget(); w.width"), 100); + check_eq("ctor factory+defaults: 0 args height", + run_script(context, "w.height"), 200); + check_eq("ctor factory+defaults: 0 args label", + run_script(context, "w.label"), "untitled"); + + // Partial defaults + check_eq("ctor factory+defaults: 1 arg", + run_script(context, "w = new Widget(50); w.width"), 50); + check_eq("ctor factory+defaults: 1 arg height default", + run_script(context, "w.height"), 200); + + // Two args + check_eq("ctor factory+defaults: 2 args", + run_script(context, "w = new Widget(10, 20); w.width + w.height"), 30); + check_eq("ctor factory+defaults: 2 args label default", + run_script(context, "w.label"), "untitled"); + + // All args + check_eq("ctor factory+defaults: all args", + run_script(context, "w = new Widget(1, 2, 'hello'); w.label"), "hello"); +} + +template +void test_ctor_multi_dispatch() +{ + struct Shape + { + int kind; // 0 = default, 1 = from radius, 2 = from w,h + int a, b; + + Shape() + : kind(0), a(0), b(0) {} + Shape(int radius) + : kind(1), a(radius), b(radius) {} + Shape(int w, int h) + : kind(2), a(w), b(h) {} + }; + + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + auto make_default = []() -> typename Traits::template object_pointer_type + { + return Traits::template create(); + }; + + auto make_circle = [](int radius) -> typename Traits::template object_pointer_type + { + return Traits::template create(radius); + }; + + auto make_rect = [](int w, int h) -> typename Traits::template object_pointer_type + { + return Traits::template create(w, h); + }; + + v8pp::class_ shape_class(isolate); + shape_class + .ctor(make_default, make_circle, make_rect) + .var("kind", &Shape::kind) + .var("a", &Shape::a) + .var("b", &Shape::b); + + context.class_("Shape", shape_class); + + // 0 args → make_default + check_eq("ctor multi-dispatch: 0 args", + run_script(context, "s = new Shape(); s.kind"), 0); + + // 1 arg → make_circle + check_eq("ctor multi-dispatch: 1 arg kind", + run_script(context, "s = new Shape(5); s.kind"), 1); + check_eq("ctor multi-dispatch: 1 arg radius", + run_script(context, "s.a"), 5); + + // 2 args → make_rect + check_eq("ctor multi-dispatch: 2 args kind", + run_script(context, "s = new Shape(10, 20); s.kind"), 2); + check_eq("ctor multi-dispatch: 2 args w+h", + run_script(context, "s.a + s.b"), 30); + + // No match → error + check_eq("ctor multi-dispatch: no match", + run_script(context, + "try { new Shape(1,2,3); 'no error'; } catch(e) { 'caught'; }"), + "caught"); +} + +template +void test_ctor_multi_dispatch_with_defaults() +{ + struct Config + { + int mode; + std::string name; + + Config() + : mode(0), name("default") {} + Config(int m, std::string n) + : mode(m), name(std::move(n)) {} + }; + + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + auto make_default = []() -> typename Traits::template object_pointer_type + { + return Traits::template create(); + }; + + auto make_full = [](int m, std::string n) -> typename Traits::template object_pointer_type + { + return Traits::template create(m, std::move(n)); + }; + + v8pp::class_ config_class(isolate); + config_class + .ctor(make_default, + v8pp::with_defaults(make_full, v8pp::defaults(42, std::string("auto")))) + .var("mode", &Config::mode) + .var("name", &Config::name); + + context.class_("Config", config_class); + + // 0 args → make_default (exact match wins over make_full with all defaults) + check_eq("ctor multi+defaults: 0 args", + run_script(context, "c = new Config(); c.mode"), 0); + + // 1 arg → make_full with 1 default + check_eq("ctor multi+defaults: 1 arg mode", + run_script(context, "c = new Config(7); c.mode"), 7); + check_eq("ctor multi+defaults: 1 arg name default", + run_script(context, "c.name"), "auto"); + + // 2 args → make_full with no defaults + check_eq("ctor multi+defaults: 2 args", + run_script(context, "c = new Config(99, 'custom'); c.name"), "custom"); +} + void test_class() { test_class_(); @@ -552,4 +729,13 @@ void test_class() test_auto_wrap_objects(); test_auto_wrap_objects(); + + test_ctor_factory_defaults(); + test_ctor_factory_defaults(); + + test_ctor_multi_dispatch(); + test_ctor_multi_dispatch(); + + test_ctor_multi_dispatch_with_defaults(); + test_ctor_multi_dispatch_with_defaults(); } diff --git a/test/test_context.cpp b/test/test_context.cpp index 78447da..67a79d1 100644 --- a/test/test_context.cpp +++ b/test/test_context.cpp @@ -100,7 +100,8 @@ void test_context() { v8pp::module m(isolate); m.const_("value", 40); - m.function("func", []() { return 2; }); + m.function("func", []() + { return 2; }); return m.impl(); }; @@ -142,7 +143,8 @@ void test_context() v8::Local require_val; check("get require", ctx.global()->Get(isolate->GetCurrentContext(), - v8pp::to_v8(isolate, "require")).ToLocal(&require_val)); + v8pp::to_v8(isolate, "require")) + .ToLocal(&require_val)); check("require is function", require_val->IsFunction()); require_fn.Reset(isolate, require_val.As()); } diff --git a/test/test_context_store.cpp b/test/test_context_store.cpp index d37ed71..c0df154 100644 --- a/test/test_context_store.cpp +++ b/test/test_context_store.cpp @@ -1,319 +1,324 @@ -#include "v8pp/context_store.hpp" -#include "v8pp/json.hpp" - -#include "test.hpp" - -#include -#include - -static_assert(std::is_move_constructible_v); -static_assert(std::is_move_assignable_v); -static_assert(!std::is_copy_assignable_v); -static_assert(!std::is_copy_constructible_v); - -void test_context_store() -{ - // Test 1: basic set/get with V8 value - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::context_store store(isolate); - check("store isolate", store.isolate() == isolate); - check("store impl", !store.impl().IsEmpty()); - - store.set("answer", v8pp::to_v8(isolate, 42)); - - v8::Local out; - check("get existing", store.get("answer", out)); - check_eq("get value", v8pp::from_v8(isolate, out), 42); - - check("get nonexistent", !store.get("missing", out)); - } - - // Test 2: typed set/get - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::context_store store(isolate); - store.set("num", 42); - store.set("str", "hello"); - store.set("pi", 3.14); - store.set("flag", true); - - int num = 0; - check("get int", store.get("num", num)); - check_eq("int value", num, 42); - - std::string str; - check("get string", store.get("str", str)); - check_eq("string value", str, "hello"); - - double pi = 0; - check("get double", store.get("pi", pi)); - check_eq("double value", pi, 3.14); - - bool flag = false; - check("get bool", store.get("flag", flag)); - check_eq("bool value", flag, true); - } - - // Test 3: has / remove - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::context_store store(isolate); - check("has before set", !store.has("key")); - - store.set("key", 1); - check("has after set", store.has("key")); - - check("remove existing", store.remove("key")); - check("has after remove", !store.has("key")); - check("remove nonexistent", !store.remove("key")); - } - - // Test 4: clear / size / keys - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::context_store store(isolate); - check_eq("empty size", store.size(), size_t(0)); - check_eq("empty keys", store.keys().size(), size_t(0)); - - store.set("a", 1); - store.set("b", 2); - store.set("c", 3); - check_eq("size after set", store.size(), size_t(3)); - - auto k = store.keys(); - check_eq("keys count", k.size(), size_t(3)); - std::sort(k.begin(), k.end()); - check_eq("key 0", k[0], "a"); - check_eq("key 1", k[1], "b"); - check_eq("key 2", k[2], "c"); - - store.clear(); - check_eq("size after clear", store.size(), size_t(0)); - check("has after clear", !store.has("a")); - } - - // Test 5: overwrite - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::context_store store(isolate); - store.set("key", 1); - store.set("key", 2); - - int val = 0; - check("get overwritten", store.get("key", val)); - check_eq("overwritten value", val, 2); - check_eq("size after overwrite", store.size(), size_t(1)); - } - - // Test 6: dot-separated names - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::context_store store(isolate); - store.set("a.b.c", 42); - - check("has a.b.c", store.has("a.b.c")); - check("has a.b", store.has("a.b")); - check("has a", store.has("a")); - - int val = 0; - check("get a.b.c", store.get("a.b.c", val)); - check_eq("nested value", val, 42); - } - - // Test 7: cross-context lifecycle (the critical test) - { - v8::Isolate* isolate = v8pp::context::create_isolate(); - { - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope outer_scope(isolate); - - v8pp::context_store store(isolate); - - // Phase 1: create context, set values, save to store - { - v8pp::context ctx(isolate, nullptr, false, false); - v8::HandleScope scope(isolate); - v8::Context::Scope context_scope(ctx.impl()); - - ctx.run_script("var state = 42; var config = 'hello';"); - store.save_from(ctx.impl(), {"state", "config"}); - } - // ctx destroyed here - - // Phase 2: create new context, restore from store - { - v8pp::context ctx(isolate, nullptr, false, false); - v8::HandleScope scope(isolate); - v8::Context::Scope context_scope(ctx.impl()); - - store.restore_to(ctx.impl(), {"state", "config"}); - - auto state = ctx.run_script("state")->Int32Value( - isolate->GetCurrentContext()).FromJust(); - check_eq("restored state", state, 42); - - auto config = v8pp::from_v8(isolate, ctx.run_script("config")); - check_eq("restored config", config, "hello"); - } - } - isolate->Dispose(); - } - - // Test 8: JS object survives context switch - { - v8::Isolate* isolate = v8pp::context::create_isolate(); - { - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope outer_scope(isolate); - - v8pp::context_store store(isolate); - - // Save an object from ctx1 - { - v8pp::context ctx(isolate, nullptr, false, false); - v8::HandleScope scope(isolate); - v8::Context::Scope context_scope(ctx.impl()); - - v8::Local obj = ctx.run_script("({x: 10, y: 20})"); - store.set("obj", obj); - } - - // Retrieve in ctx2 - { - v8pp::context ctx(isolate, nullptr, false, false); - v8::HandleScope scope(isolate); - v8::Context::Scope context_scope(ctx.impl()); - - v8::Local obj; - check("get obj", store.get("obj", obj)); - check("obj is object", obj->IsObject()); - - // Set it in the new context and verify properties - ctx.impl()->Global()->Set(isolate->GetCurrentContext(), - v8pp::to_v8(isolate, "obj"), obj).FromJust(); - auto x = ctx.run_script("obj.x")->Int32Value( - isolate->GetCurrentContext()).FromJust(); - check_eq("obj.x", x, 10); - - auto y = ctx.run_script("obj.y")->Int32Value( - isolate->GetCurrentContext()).FromJust(); - check_eq("obj.y", y, 20); - } - } - isolate->Dispose(); - } - - // Test 9: bulk save_from / restore_to - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::context_store store(isolate); - store.set("a", 1); - store.set("b", 2); - store.set("c", 3); - - // save_from with nonexistent key - auto saved = store.save_from(context.impl(), {"missing"}); - check_eq("save nonexistent", saved, size_t(0)); - - // restore partial - auto restored = store.restore_to(context.impl(), {"a", "b"}); - check_eq("restore count", restored, size_t(2)); - - auto a = run_script(context, "a"); - check_eq("restored a", a, 1); - auto b = run_script(context, "b"); - check_eq("restored b", b, 2); - } - - // Test 10: JSON deep copy - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - // Create an object and store a JSON deep copy - v8pp::context_store store(isolate); - v8::Local obj = context.run_script("({val: 100})"); - check("set_json", store.set_json("data", obj)); - - // Modify the original - context.run_script("// original is separate, store has its own copy"); - - // Retrieve the deep copy - v8::Local copy; - check("get_json", store.get_json("data", copy)); - check("copy is object", copy->IsObject()); - - // Verify the copy has the original value - context.impl()->Global()->Set(isolate->GetCurrentContext(), - v8pp::to_v8(isolate, "copy"), copy).FromJust(); - check_eq("json copy value", run_script(context, "copy.val"), 100); - } - - // Test 11: move semantics - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::context_store store1(isolate); - store1.set("key", 42); - - v8pp::context_store store2(std::move(store1)); - check("moved-from isolate", store1.isolate() == nullptr); - check("moved-to isolate", store2.isolate() == isolate); - check("moved-to impl", !store2.impl().IsEmpty()); - - int val = 0; - check("get from moved-to", store2.get("key", val)); - check_eq("moved value", val, 42); - } - - // Test 12: store outlives multiple contexts - { - v8::Isolate* isolate = v8pp::context::create_isolate(); - { - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope outer_scope(isolate); - - v8pp::context_store store(isolate); - store.set("persistent", 99); - - // Create and destroy 3 contexts - for (int i = 0; i < 3; ++i) - { - v8pp::context ctx(isolate, nullptr, false, false); - v8::HandleScope scope(isolate); - v8::Context::Scope context_scope(ctx.impl()); - - // Verify store still works - int val = 0; - check("get persistent", store.get("persistent", val)); - check_eq("persistent value", val, 99); - } - } - isolate->Dispose(); - } -} +#include "v8pp/context_store.hpp" +#include "v8pp/json.hpp" + +#include "test.hpp" + +#include +#include + +static_assert(std::is_move_constructible_v); +static_assert(std::is_move_assignable_v); +static_assert(!std::is_copy_assignable_v); +static_assert(!std::is_copy_constructible_v); + +void test_context_store() +{ + // Test 1: basic set/get with V8 value + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::context_store store(isolate); + check("store isolate", store.isolate() == isolate); + check("store impl", !store.impl().IsEmpty()); + + store.set("answer", v8pp::to_v8(isolate, 42)); + + v8::Local out; + check("get existing", store.get("answer", out)); + check_eq("get value", v8pp::from_v8(isolate, out), 42); + + check("get nonexistent", !store.get("missing", out)); + } + + // Test 2: typed set/get + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::context_store store(isolate); + store.set("num", 42); + store.set("str", "hello"); + store.set("pi", 3.14); + store.set("flag", true); + + int num = 0; + check("get int", store.get("num", num)); + check_eq("int value", num, 42); + + std::string str; + check("get string", store.get("str", str)); + check_eq("string value", str, "hello"); + + double pi = 0; + check("get double", store.get("pi", pi)); + check_eq("double value", pi, 3.14); + + bool flag = false; + check("get bool", store.get("flag", flag)); + check_eq("bool value", flag, true); + } + + // Test 3: has / remove + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::context_store store(isolate); + check("has before set", !store.has("key")); + + store.set("key", 1); + check("has after set", store.has("key")); + + check("remove existing", store.remove("key")); + check("has after remove", !store.has("key")); + check("remove nonexistent", !store.remove("key")); + } + + // Test 4: clear / size / keys + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::context_store store(isolate); + check_eq("empty size", store.size(), size_t(0)); + check_eq("empty keys", store.keys().size(), size_t(0)); + + store.set("a", 1); + store.set("b", 2); + store.set("c", 3); + check_eq("size after set", store.size(), size_t(3)); + + auto k = store.keys(); + check_eq("keys count", k.size(), size_t(3)); + std::sort(k.begin(), k.end()); + check_eq("key 0", k[0], "a"); + check_eq("key 1", k[1], "b"); + check_eq("key 2", k[2], "c"); + + store.clear(); + check_eq("size after clear", store.size(), size_t(0)); + check("has after clear", !store.has("a")); + } + + // Test 5: overwrite + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::context_store store(isolate); + store.set("key", 1); + store.set("key", 2); + + int val = 0; + check("get overwritten", store.get("key", val)); + check_eq("overwritten value", val, 2); + check_eq("size after overwrite", store.size(), size_t(1)); + } + + // Test 6: dot-separated names + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::context_store store(isolate); + store.set("a.b.c", 42); + + check("has a.b.c", store.has("a.b.c")); + check("has a.b", store.has("a.b")); + check("has a", store.has("a")); + + int val = 0; + check("get a.b.c", store.get("a.b.c", val)); + check_eq("nested value", val, 42); + } + + // Test 7: cross-context lifecycle (the critical test) + { + v8::Isolate* isolate = v8pp::context::create_isolate(); + { + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope outer_scope(isolate); + + v8pp::context_store store(isolate); + + // Phase 1: create context, set values, save to store + { + v8pp::context ctx(isolate, nullptr, false, false); + v8::HandleScope scope(isolate); + v8::Context::Scope context_scope(ctx.impl()); + + ctx.run_script("var state = 42; var config = 'hello';"); + store.save_from(ctx.impl(), { "state", "config" }); + } + // ctx destroyed here + + // Phase 2: create new context, restore from store + { + v8pp::context ctx(isolate, nullptr, false, false); + v8::HandleScope scope(isolate); + v8::Context::Scope context_scope(ctx.impl()); + + store.restore_to(ctx.impl(), { "state", "config" }); + + auto state = ctx.run_script("state")->Int32Value( + isolate->GetCurrentContext()) + .FromJust(); + check_eq("restored state", state, 42); + + auto config = v8pp::from_v8(isolate, ctx.run_script("config")); + check_eq("restored config", config, "hello"); + } + } + isolate->Dispose(); + } + + // Test 8: JS object survives context switch + { + v8::Isolate* isolate = v8pp::context::create_isolate(); + { + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope outer_scope(isolate); + + v8pp::context_store store(isolate); + + // Save an object from ctx1 + { + v8pp::context ctx(isolate, nullptr, false, false); + v8::HandleScope scope(isolate); + v8::Context::Scope context_scope(ctx.impl()); + + v8::Local obj = ctx.run_script("({x: 10, y: 20})"); + store.set("obj", obj); + } + + // Retrieve in ctx2 + { + v8pp::context ctx(isolate, nullptr, false, false); + v8::HandleScope scope(isolate); + v8::Context::Scope context_scope(ctx.impl()); + + v8::Local obj; + check("get obj", store.get("obj", obj)); + check("obj is object", obj->IsObject()); + + // Set it in the new context and verify properties + ctx.impl()->Global()->Set(isolate->GetCurrentContext(), + v8pp::to_v8(isolate, "obj"), obj) + .FromJust(); + auto x = ctx.run_script("obj.x")->Int32Value( + isolate->GetCurrentContext()) + .FromJust(); + check_eq("obj.x", x, 10); + + auto y = ctx.run_script("obj.y")->Int32Value( + isolate->GetCurrentContext()) + .FromJust(); + check_eq("obj.y", y, 20); + } + } + isolate->Dispose(); + } + + // Test 9: bulk save_from / restore_to + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::context_store store(isolate); + store.set("a", 1); + store.set("b", 2); + store.set("c", 3); + + // save_from with nonexistent key + auto saved = store.save_from(context.impl(), { "missing" }); + check_eq("save nonexistent", saved, size_t(0)); + + // restore partial + auto restored = store.restore_to(context.impl(), { "a", "b" }); + check_eq("restore count", restored, size_t(2)); + + auto a = run_script(context, "a"); + check_eq("restored a", a, 1); + auto b = run_script(context, "b"); + check_eq("restored b", b, 2); + } + + // Test 10: JSON deep copy + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + // Create an object and store a JSON deep copy + v8pp::context_store store(isolate); + v8::Local obj = context.run_script("({val: 100})"); + check("set_json", store.set_json("data", obj)); + + // Modify the original + context.run_script("// original is separate, store has its own copy"); + + // Retrieve the deep copy + v8::Local copy; + check("get_json", store.get_json("data", copy)); + check("copy is object", copy->IsObject()); + + // Verify the copy has the original value + context.impl()->Global()->Set(isolate->GetCurrentContext(), + v8pp::to_v8(isolate, "copy"), copy) + .FromJust(); + check_eq("json copy value", run_script(context, "copy.val"), 100); + } + + // Test 11: move semantics + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::context_store store1(isolate); + store1.set("key", 42); + + v8pp::context_store store2(std::move(store1)); + check("moved-from isolate", store1.isolate() == nullptr); + check("moved-to isolate", store2.isolate() == isolate); + check("moved-to impl", !store2.impl().IsEmpty()); + + int val = 0; + check("get from moved-to", store2.get("key", val)); + check_eq("moved value", val, 42); + } + + // Test 12: store outlives multiple contexts + { + v8::Isolate* isolate = v8pp::context::create_isolate(); + { + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope outer_scope(isolate); + + v8pp::context_store store(isolate); + store.set("persistent", 99); + + // Create and destroy 3 contexts + for (int i = 0; i < 3; ++i) + { + v8pp::context ctx(isolate, nullptr, false, false); + v8::HandleScope scope(isolate); + v8::Context::Scope context_scope(ctx.impl()); + + // Verify store still works + int val = 0; + check("get persistent", store.get("persistent", val)); + check_eq("persistent value", val, 99); + } + } + isolate->Dispose(); + } +} diff --git a/test/test_convert.cpp b/test/test_convert.cpp index bae8c6d..c604d16 100644 --- a/test/test_convert.cpp +++ b/test/test_convert.cpp @@ -75,7 +75,7 @@ struct address std::string house; std::optional flat; - //for test framework + // for test framework bool operator==(address const& other) const = default; friend std::ostream& operator<<(std::ostream& os, address const& a) @@ -90,7 +90,7 @@ struct person int age; std::optional
home; - //for test framework + // for test framework bool operator==(person const& other) const = default; friend std::ostream& operator<<(std::ostream& os, person const& p) @@ -207,7 +207,8 @@ struct convert void test_convert_user_type(v8::Isolate* isolate) { person p; - p.name = "Al"; p.age = 33; + p.name = "Al"; + p.age = 33; test_conv(isolate, p); p.home = { .zip = "90210", .city = "Beverly Hills", .street = "Main St", .house = "123", .flat = "B2" }; test_conv(isolate, p); @@ -215,19 +216,17 @@ void test_convert_user_type(v8::Isolate* isolate) void test_convert_optional(v8::Isolate* isolate) { - test_conv(isolate, std::optional{42}); - test_conv(isolate, std::optional{std::nullopt}); + test_conv(isolate, std::optional{ 42 }); + test_conv(isolate, std::optional{ std::nullopt }); check("null", v8pp::from_v8>(isolate, v8::Null(isolate)) == std::nullopt); - check("undefined", v8pp::from_v8>(isolate, v8::Undefined(isolate)) == std::nullopt); + check("undefined", v8pp::from_v8>(isolate, v8::Undefined(isolate)) == std::nullopt); - check("nullopt", v8pp::to_v8(isolate, std::nullopt)->IsNull()); + check("nullopt", v8pp::to_v8(isolate, std::nullopt)->IsNull()); check("monostate", v8pp::to_v8(isolate, std::monostate{})->IsUndefined()); check_ex("wrong optional type", [isolate]() - { - v8pp::from_v8>(isolate, v8pp::to_v8(isolate, std::optional{"aa"})); - }); + { v8pp::from_v8>(isolate, v8pp::to_v8(isolate, std::optional{ "aa" })); }); } void test_convert_tuple(v8::Isolate* isolate) @@ -245,11 +244,10 @@ void test_convert_tuple(v8::Isolate* isolate) test_conv(isolate, tuple_4); check_ex("Tuple", [isolate, &tuple_1]() - { + { // incorrect number of elements v8::Local tuple_1_ = v8pp::to_v8(isolate, tuple_1); - v8pp::from_v8>(isolate, tuple_1_); - }); + v8pp::from_v8>(isolate, tuple_1_); }); { // bool converts to string via ToString() @@ -266,13 +264,20 @@ struct variant_check v8::Isolate* isolate; - explicit variant_check(v8::Isolate* isolate) : isolate(isolate) {} + explicit variant_check(v8::Isolate* isolate) + : isolate(isolate) {} template - static T const& get(T const& in) { return in; } + static T const& get(T const& in) + { + return in; + } template - static T const& get(variant const& in) { return std::get(in); } + static T const& get(variant const& in) + { + return std::get(in); + } template void check(T const& value) @@ -295,9 +300,7 @@ struct variant_check { v8::Local v8_value = v8pp::convert::to_v8(isolate, value); ::check_ex("variant", [&]() - { - v8pp::convert::from_v8(isolate, v8_value); - }); + { v8pp::convert::from_v8(isolate, v8_value); }); } void operator()(Ts const&... values) @@ -306,7 +309,7 @@ struct variant_check } }; -static int64_t const V8_MAX_INT = (uint64_t{1} << std::numeric_limits::digits) - 1; +static int64_t const V8_MAX_INT = (uint64_t{ 1 } << std::numeric_limits::digits) - 1; static int64_t const V8_MIN_INT = -V8_MAX_INT - 1; template @@ -347,7 +350,7 @@ void check_ranges(v8::Isolate* isolate) struct U { int value = 1; - //for test framework + // for test framework bool operator==(U const& other) const { return value == other.value; } bool operator!=(U const& other) const { return value != other.value; } friend std::ostream& operator<<(std::ostream& os, U const& val) { return os << val.value; } @@ -356,7 +359,7 @@ struct U struct U2 { double value = 2.0; - //for test framework + // for test framework bool operator==(U2 const& other) const { return value == other.value; } bool operator!=(U2 const& other) const { return value != other.value; } friend std::ostream& operator<<(std::ostream& os, U2 const& val) { return os << val.value; } @@ -365,7 +368,7 @@ struct U2 struct V { std::string value; - //for test framework + // for test framework bool operator==(V const& other) const { return value == other.value; } bool operator!=(V const& other) const { return value != other.value; } friend std::ostream& operator<<(std::ostream& os, V const& val) { return os << val.value; } @@ -374,7 +377,7 @@ struct V struct V2 { std::string value; - //for test framework + // for test framework bool operator==(V2 const& other) const { return value == other.value; } bool operator!=(V2 const& other) const { return value != other.value; } friend std::ostream& operator<<(std::ostream& os, V2 const& val) { return os << val.value; } @@ -401,7 +404,7 @@ void test_convert_variant(v8::Isolate* isolate) V2_class.reference_external(isolate, v2); variant_check, int, std::string, U2, std::shared_ptr> check{ isolate }; - check(U{2}, v, -1, "Hello", U2{3.}, v2); + check(U{ 2 }, v, -1, "Hello", U2{ 3. }, v2); variant_check check_arithmetic{ isolate }; check_arithmetic(true, 5.5f, 2); @@ -412,7 +415,7 @@ void test_convert_variant(v8::Isolate* isolate) check_arithmetic_reversed(-2, 2.2f, false); variant_check, float, std::optional> check_vector{ isolate }; - check_vector({1.f, 2.f, 3.f}, 4.f, std::optional("testing")); + check_vector({ 1.f, 2.f, 3.f }, 4.f, std::optional("testing")); check_vector(std::vector{}, 0.f, std::optional{}); // The order here matters — int64_t/uint64_t not included because both map @@ -443,16 +446,16 @@ void test_convert_variant(v8::Isolate* isolate) // test map variant_check> map_check{ isolate }; - map_check(U{3}, std::map{ { 4, U{4} }, { 2, U{2} } }); + map_check(U{ 3 }, std::map{ { 4, U{ 4 } }, { 2, U{ 2 } } }); variant_check> unordered_map_check{ isolate }; - unordered_map_check(U{1}, std::unordered_map{ { 1, U2{1.0} }, { 2, U2{2.0} } }); + unordered_map_check(U{ 1 }, std::unordered_map{ { 1, U2{ 1.0 } }, { 2, U2{ 2.0 } } }); variant_check> multimap_check{ isolate }; - multimap_check(U{2}, std::multimap{ { "x", U{0} }, { "y", U{1} } }); + multimap_check(U{ 2 }, std::multimap{ { "x", U{ 0 } }, { "y", U{ 1 } } }); variant_check> unordered_multimap_check{ isolate }; - unordered_multimap_check(U2{3.0}, std::unordered_multimap{ { 'a', U{1} }, { 'b', U{2} } }); + unordered_multimap_check(U2{ 3.0 }, std::unordered_multimap{ { 'a', U{ 1 } }, { 'b', U{ 2 } } }); variant_check, bool> optional_check{ isolate }; optional_check(true, "test", 1); @@ -463,65 +466,39 @@ void test_convert_crash_safety(v8::Isolate* isolate) { // from_v8 with non-numeric types should throw, not crash check_ex("from_v8 undefined", [isolate]() - { - v8pp::from_v8(isolate, v8::Undefined(isolate)); - }); + { v8pp::from_v8(isolate, v8::Undefined(isolate)); }); check_ex("from_v8 null", [isolate]() - { - v8pp::from_v8(isolate, v8::Null(isolate)); - }); + { v8pp::from_v8(isolate, v8::Null(isolate)); }); check_ex("from_v8 string", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); }); check_ex("from_v8 bool", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, true)); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, true)); }); check_ex("from_v8 object", [isolate]() - { - v8pp::from_v8(isolate, v8::Object::New(isolate)); - }); + { v8pp::from_v8(isolate, v8::Object::New(isolate)); }); check_ex("from_v8 empty handle", [isolate]() - { - v8pp::from_v8(isolate, v8::Local()); - }); + { v8pp::from_v8(isolate, v8::Local()); }); // from_v8 with non-numeric check_ex("from_v8 string", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); }); // from_v8 with non-numeric check_ex("from_v8 string", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); }); check_ex("from_v8 undefined", [isolate]() - { - v8pp::from_v8(isolate, v8::Undefined(isolate)); - }); + { v8pp::from_v8(isolate, v8::Undefined(isolate)); }); // from_v8 with non-boolean check_ex("from_v8 int", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, 42)); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, 42)); }); check_ex("from_v8 string", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); }); check_ex("from_v8 undefined", [isolate]() - { - v8pp::from_v8(isolate, v8::Undefined(isolate)); - }); + { v8pp::from_v8(isolate, v8::Undefined(isolate)); }); // from_v8 with empty handle check_ex("from_v8 empty handle", [isolate]() - { - v8pp::from_v8(isolate, v8::Local()); - }); + { v8pp::from_v8(isolate, v8::Local()); }); // from_v8 with object that has throwing toString (Phase 1a fix) { @@ -529,45 +506,30 @@ void test_convert_crash_safety(v8::Isolate* isolate) v8::Local ctx = isolate->GetCurrentContext(); v8::Local throwing_obj = v8::Object::New(isolate); auto throwing_fn = v8::Function::New(ctx, [](v8::FunctionCallbackInfo const& args) - { - args.GetIsolate()->ThrowException( - v8pp::to_v8(args.GetIsolate(), "toString throws!")); - }).ToLocalChecked(); + { args.GetIsolate()->ThrowException( + v8pp::to_v8(args.GetIsolate(), "toString throws!")); }) + .ToLocalChecked(); throwing_obj->Set(ctx, v8pp::to_v8(isolate, "toString"), throwing_fn).FromJust(); check_ex("from_v8 throwing toString", [isolate, &throwing_obj]() - { - v8pp::from_v8(isolate, throwing_obj); - }); + { v8pp::from_v8(isolate, throwing_obj); }); } // from_v8> with non-array check_ex("from_v8> int", [isolate]() - { - v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); - }); + { v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); }); check_ex("from_v8> undefined", [isolate]() - { - v8pp::from_v8>(isolate, v8::Undefined(isolate)); - }); + { v8pp::from_v8>(isolate, v8::Undefined(isolate)); }); check_ex("from_v8> string", [isolate]() - { - v8pp::from_v8>(isolate, v8pp::to_v8(isolate, "hello")); - }); + { v8pp::from_v8>(isolate, v8pp::to_v8(isolate, "hello")); }); // from_v8 with non-object check_ex("from_v8 int", [isolate]() - { - v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); - }); + { v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); }); check_ex("from_v8 array", [isolate]() - { - v8pp::from_v8>(isolate, v8::Array::New(isolate)); - }); + { v8pp::from_v8>(isolate, v8::Array::New(isolate)); }); check_ex("from_v8 undefined", [isolate]() - { - v8pp::from_v8>(isolate, v8::Undefined(isolate)); - }); + { v8pp::from_v8>(isolate, v8::Undefined(isolate)); }); // from_v8 with default value — should return default on type mismatch, not crash check_eq("from_v8 default on undefined", @@ -582,15 +544,16 @@ void test_convert_crash_safety(v8::Isolate* isolate) v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello"), -1.0), -1.0); // Enum with non-numeric should throw - enum class color { red = 0, green = 1, blue = 2 }; - check_ex("from_v8 string", [isolate]() + enum class color { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, "red")); - }); + red = 0, + green = 1, + blue = 2 + }; + check_ex("from_v8 string", [isolate]() + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, "red")); }); check_ex("from_v8 undefined", [isolate]() - { - v8pp::from_v8(isolate, v8::Undefined(isolate)); - }); + { v8pp::from_v8(isolate, v8::Undefined(isolate)); }); } void test_convert_try_from_v8(v8::Isolate* isolate) @@ -634,7 +597,12 @@ void test_convert_try_from_v8(v8::Isolate* isolate) check("try string from empty", !v8pp::try_from_v8(isolate, v8::Local())); // Enums - enum class color { red = 0, green = 1, blue = 2 }; + enum class color + { + red = 0, + green = 1, + blue = 2 + }; auto enum_result = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, 1)); check("try enum valid", enum_result.has_value()); check_eq("try enum value", *enum_result, color::green); @@ -642,16 +610,16 @@ void test_convert_try_from_v8(v8::Isolate* isolate) // Sequences auto vec_result = v8pp::try_from_v8>(isolate, - v8pp::to_v8(isolate, std::vector{1, 2, 3})); + v8pp::to_v8(isolate, std::vector{ 1, 2, 3 })); check("try vector valid", vec_result.has_value()); - check_eq("try vector value", *vec_result, std::vector({1, 2, 3})); + check_eq("try vector value", *vec_result, std::vector({ 1, 2, 3 })); check("try vector from int", !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, 42))); // Maps check("try map from int", !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, 42))); // Tuples - auto tuple_val = std::tuple{42, true}; + auto tuple_val = std::tuple{ 42, true }; auto tuple_result = v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, tuple_val)); check("try tuple valid", tuple_result.has_value()); check_eq("try tuple value", *tuple_result, tuple_val); @@ -672,7 +640,7 @@ void test_convert_try_from_v8(v8::Isolate* isolate) !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, "hello"))); // Wrapped class: valid unwrap (class_ already registered by test_convert_variant) - U u_obj{42}; + U u_obj{ 42 }; auto u_v8 = v8pp::class_::reference_external(isolate, &u_obj); auto u_ptr_result = v8pp::try_from_v8(isolate, u_v8); @@ -684,7 +652,7 @@ void test_convert_try_from_v8(v8::Isolate* isolate) check("try U* from plain object", !v8pp::try_from_v8(isolate, v8::Object::New(isolate))); // Wrapped class via shared_ptr (class_ already registered by test_convert_variant) - auto v_obj = std::make_shared(V{"test"}); + auto v_obj = std::make_shared(V{ "test" }); v8pp::class_::reference_external(isolate, v_obj); auto v_v8 = v8pp::class_::find_object(isolate, v_obj); @@ -698,58 +666,54 @@ void test_convert_try_from_v8(v8::Isolate* isolate) void test_convert_bigint(v8::Isolate* isolate) { // Basic round-trip for int64_t (values within double precision) - test_conv(isolate, int64_t{0}); - test_conv(isolate, int64_t{42}); - test_conv(isolate, int64_t{-42}); + test_conv(isolate, int64_t{ 0 }); + test_conv(isolate, int64_t{ 42 }); + test_conv(isolate, int64_t{ -42 }); // Basic round-trip for uint64_t (values within double precision) - test_conv(isolate, uint64_t{0}); - test_conv(isolate, uint64_t{42}); + test_conv(isolate, uint64_t{ 0 }); + test_conv(isolate, uint64_t{ 42 }); // to_v8 produces Number (not BigInt) - auto v8_val = v8pp::to_v8(isolate, int64_t{123}); + auto v8_val = v8pp::to_v8(isolate, int64_t{ 123 }); check("int64_t to_v8 is Number", v8_val->IsNumber()); - auto v8_uval = v8pp::to_v8(isolate, uint64_t{456}); + auto v8_uval = v8pp::to_v8(isolate, uint64_t{ 456 }); check("uint64_t to_v8 is Number", v8_uval->IsNumber()); // from_v8 accepts Number auto num_val = v8::Number::New(isolate, 42.0); - check_eq("int64_t from Number", v8pp::from_v8(isolate, num_val), int64_t{42}); - check_eq("uint64_t from Number", v8pp::from_v8(isolate, num_val), uint64_t{42}); + check_eq("int64_t from Number", v8pp::from_v8(isolate, num_val), int64_t{ 42 }); + check_eq("uint64_t from Number", v8pp::from_v8(isolate, num_val), uint64_t{ 42 }); // from_v8 also accepts BigInt for interop auto bigint_val = v8::BigInt::New(isolate, 99); - check_eq("int64_t from BigInt", v8pp::from_v8(isolate, bigint_val), int64_t{99}); + check_eq("int64_t from BigInt", v8pp::from_v8(isolate, bigint_val), int64_t{ 99 }); // from_v8 rejects non-numeric types check_ex("int64_t from string", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); }); check_ex("uint64_t from bool", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, true)); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, true)); }); // try_from_v8 for int64_t - auto try_i64 = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, int64_t{-999})); + auto try_i64 = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, int64_t{ -999 })); check("try int64_t valid", try_i64.has_value()); - check_eq("try int64_t value", *try_i64, int64_t{-999}); + check_eq("try int64_t value", *try_i64, int64_t{ -999 }); check("try int64_t from string", !v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, "abc"))); } void test_convert_set(v8::Isolate* isolate) { // std::set round-trip - std::set int_set{1, 2, 3, 4, 5}; + std::set int_set{ 1, 2, 3, 4, 5 }; auto v8_val = v8pp::to_v8(isolate, int_set); check("set to_v8 is Array", v8_val->IsArray()); auto result = v8pp::from_v8>(isolate, v8_val); check_eq("set round-trip", result, int_set); // std::unordered_set round-trip - std::unordered_set str_set{"hello", "world"}; + std::unordered_set str_set{ "hello", "world" }; auto v8_str = v8pp::to_v8(isolate, str_set); check("unordered_set to_v8 is Array", v8_str->IsArray()); auto str_result = v8pp::from_v8>(isolate, v8_str); @@ -763,21 +727,19 @@ void test_convert_set(v8::Isolate* isolate) // Invalid input check_ex("set from non-array", [isolate]() - { - v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); - }); + { v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); }); // try_from_v8 - auto try_set = v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, std::set{10, 20})); + auto try_set = v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, std::set{ 10, 20 })); check("try set valid", try_set.has_value()); - check_eq("try set size", try_set->size(), size_t{2}); + check_eq("try set size", try_set->size(), size_t{ 2 }); check("try set from int", !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, 42))); } void test_convert_pair(v8::Isolate* isolate) { // Basic round-trip - std::pair p{42, "hello"}; + std::pair p{ 42, "hello" }; auto v8_val = v8pp::to_v8(isolate, p); check("pair to_v8 is Array", v8_val->IsArray()); auto result = v8pp::from_v8>(isolate, v8_val); @@ -785,24 +747,20 @@ void test_convert_pair(v8::Isolate* isolate) check_eq("pair second", result.second, std::string("hello")); // pair - std::pair p2{3.14, true}; + std::pair p2{ 3.14, true }; test_conv(isolate, p2); // Invalid: non-array check_ex("pair from int", [isolate]() - { - v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); - }); + { v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); }); // Invalid: wrong array length check_ex("pair from 3-element array", [isolate]() - { - v8pp::from_v8>(isolate, v8pp::to_v8(isolate, std::vector{1, 2, 3})); - }); + { v8pp::from_v8>(isolate, v8pp::to_v8(isolate, std::vector{ 1, 2, 3 })); }); // try_from_v8 auto try_pair = v8pp::try_from_v8>(isolate, - v8pp::to_v8(isolate, std::pair{7, false})); + v8pp::to_v8(isolate, std::pair{ 7, false })); check("try pair valid", try_pair.has_value()); check_eq("try pair first", try_pair->first, 7); check_eq("try pair second", try_pair->second, false); @@ -833,29 +791,27 @@ void test_convert_chrono(v8::Isolate* isolate) using namespace std::chrono; // duration: milliseconds round-trip - auto ms_val = milliseconds{1500}; + auto ms_val = milliseconds{ 1500 }; auto v8_ms = v8pp::to_v8(isolate, ms_val); check("duration to_v8 is Number", v8_ms->IsNumber()); auto ms_result = v8pp::from_v8(isolate, v8_ms); - check_eq("milliseconds round-trip", ms_result.count(), int64_t{1500}); + check_eq("milliseconds round-trip", ms_result.count(), int64_t{ 1500 }); // duration: seconds to Number (converts to ms internally) - auto sec_val = seconds{3}; + auto sec_val = seconds{ 3 }; auto v8_sec = v8pp::to_v8(isolate, sec_val); double sec_ms = v8_sec->NumberValue(isolate->GetCurrentContext()).FromJust(); check_eq("seconds to_v8 as ms", sec_ms, 3000.0); auto sec_result = v8pp::from_v8(isolate, v8_sec); - check_eq("seconds round-trip", sec_result.count(), int64_t{3}); + check_eq("seconds round-trip", sec_result.count(), int64_t{ 3 }); // duration: microseconds - auto us_val = microseconds{123456}; + auto us_val = microseconds{ 123456 }; test_conv(isolate, us_val); // duration: invalid input check_ex("duration from string", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); }); // time_point: system_clock round-trip auto now = system_clock::now(); @@ -876,21 +832,19 @@ void test_convert_chrono(v8::Isolate* isolate) // time_point: invalid input check_ex("time_point from string", [isolate]() - { - v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); - }); + { v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); }); // try_from_v8 for duration - auto try_dur = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, milliseconds{42})); + auto try_dur = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, milliseconds{ 42 })); check("try duration valid", try_dur.has_value()); - check_eq("try duration value", try_dur->count(), int64_t{42}); + check_eq("try duration value", try_dur->count(), int64_t{ 42 }); check("try duration from string", !v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, "x"))); } void test_convert_arraybuffer(v8::Isolate* isolate) { // Basic round-trip: vector -> ArrayBuffer -> vector - std::vector data{0, 1, 2, 127, 255}; + std::vector data{ 0, 1, 2, 127, 255 }; auto v8_val = v8pp::to_v8(isolate, data); check("vector to_v8 is ArrayBuffer", v8_val->IsArrayBuffer()); auto result = v8pp::from_v8>(isolate, v8_val); @@ -907,7 +861,7 @@ void test_convert_arraybuffer(v8::Isolate* isolate) // from_v8 from ArrayBufferView (Uint8Array) { v8::EscapableHandleScope scope(isolate); - std::vector src{10, 20, 30}; + std::vector src{ 10, 20, 30 }; auto ab = v8pp::to_v8(isolate, src); auto typed = v8::Uint8Array::New(ab.As(), 0, 3); auto view_result = v8pp::from_v8>(isolate, typed); @@ -916,19 +870,15 @@ void test_convert_arraybuffer(v8::Isolate* isolate) // Invalid input check_ex("vector from int", [isolate]() - { - v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); - }); + { v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); }); check_ex("vector from string", [isolate]() - { - v8pp::from_v8>(isolate, v8pp::to_v8(isolate, "hello")); - }); + { v8pp::from_v8>(isolate, v8pp::to_v8(isolate, "hello")); }); // try_from_v8 auto try_buf = v8pp::try_from_v8>(isolate, - v8pp::to_v8(isolate, std::vector{5, 6, 7})); + v8pp::to_v8(isolate, std::vector{ 5, 6, 7 })); check("try arraybuffer valid", try_buf.has_value()); - check_eq("try arraybuffer size", try_buf->size(), size_t{3}); + check_eq("try arraybuffer size", try_buf->size(), size_t{ 3 }); check("try arraybuffer from string", !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, "x"))); } @@ -937,7 +887,7 @@ void test_convert_span(v8::Isolate* isolate) { // span -> Uint8Array { - std::vector data{1, 2, 3, 4, 5}; + std::vector data{ 1, 2, 3, 4, 5 }; std::span sp(data); auto v8_val = v8pp::to_v8(isolate, sp); check("span to_v8 is Uint8Array", v8_val->IsUint8Array()); @@ -948,7 +898,7 @@ void test_convert_span(v8::Isolate* isolate) // span -> Int32Array { - std::vector data{-1, 0, 1, 100}; + std::vector data{ -1, 0, 1, 100 }; std::span sp(data); auto v8_val = v8pp::to_v8(isolate, sp); check("span to_v8 is Int32Array", v8_val->IsInt32Array()); @@ -958,7 +908,7 @@ void test_convert_span(v8::Isolate* isolate) // span -> Float32Array { - std::vector data{1.0f, 2.5f, 3.14f}; + std::vector data{ 1.0f, 2.5f, 3.14f }; std::span sp(data); auto v8_val = v8pp::to_v8(isolate, sp); check("span to_v8 is Float32Array", v8_val->IsFloat32Array()); @@ -968,7 +918,7 @@ void test_convert_span(v8::Isolate* isolate) // span -> Float64Array { - std::vector data{1.0, 2.0}; + std::vector data{ 1.0, 2.0 }; std::span sp(data); auto v8_val = v8pp::to_v8(isolate, sp); check("span to_v8 is Float64Array", v8_val->IsFloat64Array()); @@ -980,12 +930,12 @@ void test_convert_span(v8::Isolate* isolate) auto v8_val = v8pp::to_v8(isolate, empty); check("empty span to_v8 is Uint8Array", v8_val->IsUint8Array()); auto view = v8_val.As(); - check_eq("empty span length", static_cast(view->Length()), size_t{0}); + check_eq("empty span length", static_cast(view->Length()), size_t{ 0 }); } // span data is copied (modifying original doesn't affect JS) { - std::vector data{10, 20, 30}; + std::vector data{ 10, 20, 30 }; std::span sp(data); auto v8_val = v8pp::to_v8(isolate, sp); data[0] = 999; // modify original @@ -1006,10 +956,20 @@ void test_convert() test_conv(isolate, 2.2); test_conv(isolate, true); - enum old_enum { A = 1, B = 5, C = -1 }; + enum old_enum + { + A = 1, + B = 5, + C = -1 + }; test_conv(isolate, B); - enum class new_enum { X = 'a', Y = 'b', Z = 'c' }; + enum class new_enum + { + X = 'a', + Y = 'b', + Z = 'c' + }; test_conv(isolate, new_enum::Z); test_string_conv(isolate, "qaz"); @@ -1028,10 +988,9 @@ void test_convert() const std::array array{ 1, 2, 3 }; test_conv(isolate, array); check_ex("wrong array length", [isolate, &array]() - { + { v8::Local arr = v8pp::to_v8(isolate, array); - v8pp::from_v8>(isolate, arr); - }); + v8pp::from_v8>(isolate, arr); }); test_conv(isolate, std::map{ { 'a', 1 }, { 'b', 2 }, { 'c', 3 } }); test_conv(isolate, std::multimap{ { 1, -1 }, { 2, -2 } }); diff --git a/test/test_fast_api.cpp b/test/test_fast_api.cpp index e6afe80..02e7ad7 100644 --- a/test/test_fast_api.cpp +++ b/test/test_fast_api.cpp @@ -1,105 +1,191 @@ -#include "v8pp/class.hpp" -#include "v8pp/context.hpp" -#include "v8pp/fast_api.hpp" -#include "v8pp/module.hpp" - -#include "test.hpp" - -// Free functions for fast API testing -static int32_t fast_add(int32_t a, int32_t b) { return a + b; } -static double fast_mul(double a, double b) { return a * b; } -static bool fast_negate(bool x) { return !x; } -static uint32_t fast_square(uint32_t x) { return x * x; } - -// Non-compatible functions (should silently fall back to slow-only) -static std::string slow_greet(std::string name) { return "hello " + name; } -static int isolate_func(v8::Isolate*, int x) { return x; } - -// Compile-time compatibility checks -static_assert(v8pp::detail::is_fast_api_compatible::value); -static_assert(v8pp::detail::is_fast_api_compatible::value); -static_assert(v8pp::detail::is_fast_api_compatible::value); -static_assert(v8pp::detail::is_fast_api_compatible::value); -static_assert(!v8pp::detail::is_fast_api_compatible::value); -static_assert(!v8pp::detail::is_fast_api_compatible::value); - -// Return type checks -static_assert(v8pp::detail::is_fast_return_type_v); -static_assert(v8pp::detail::is_fast_return_type_v); -static_assert(v8pp::detail::is_fast_return_type_v); -static_assert(v8pp::detail::is_fast_return_type_v); -static_assert(v8pp::detail::is_fast_return_type_v); -static_assert(!v8pp::detail::is_fast_return_type_v); -static_assert(!v8pp::detail::is_fast_return_type_v); - -// Arg type checks -static_assert(v8pp::detail::is_fast_arg_type_v); -static_assert(v8pp::detail::is_fast_arg_type_v); -static_assert(v8pp::detail::is_fast_arg_type_v); -static_assert(!v8pp::detail::is_fast_arg_type_v); -static_assert(!v8pp::detail::is_fast_arg_type_v); - -void test_fast_api() -{ - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - // --- Free function: int32_t + int32_t --- - context.function("fast_add", v8pp::fast_fn<&fast_add>); - check_eq("fast_api: add ints", run_script(context, "fast_add(10, 20)"), 30); - check_eq("fast_api: add negative", run_script(context, "fast_add(-5, 3)"), -2); - - // --- Free function: double * double --- - context.function("fast_mul", v8pp::fast_fn<&fast_mul>); - check_eq("fast_api: mul doubles", run_script(context, "fast_mul(1.5, 3.0)"), 4.5); - - // --- Bool return --- - context.function("fast_negate", v8pp::fast_fn<&fast_negate>); - check_eq("fast_api: negate true", run_script(context, "fast_negate(true)"), false); - check_eq("fast_api: negate false", run_script(context, "fast_negate(false)"), true); - - // --- uint32_t --- - context.function("fast_square", v8pp::fast_fn<&fast_square>); - check_eq("fast_api: square", run_script(context, "fast_square(7)"), 49); - - // --- Non-compatible function silently falls back to slow --- - context.function("slow_greet", v8pp::fast_fn<&slow_greet>); - check_eq("fast_api: slow fallback", run_script(context, "slow_greet('world')"), "hello world"); - - // --- Module function with fast API --- - { - v8pp::module m(isolate); - m.function("compute", v8pp::fast_fn<&fast_add>); - context.module("fast_mod", m); - - check_eq("fast_api: module func", run_script(context, "fast_mod.compute(3, 4)"), 7); - } - - // --- Class member function with fast API --- - { - struct Vec - { - int32_t x = 0; - int32_t y = 0; - int32_t sum() const { return x + y; } - int32_t dot(int32_t ox, int32_t oy) const { return x * ox + y * oy; } - }; - - v8pp::class_ vec_class(isolate); - vec_class - .ctor<>() - .var("x", &Vec::x) - .var("y", &Vec::y) - .function("sum", v8pp::fast_fn<&Vec::sum>) - .function("dot", v8pp::fast_fn<&Vec::dot>); - context.class_("Vec", vec_class); - - check_eq("fast_api: member sum", run_script(context, "var v = new Vec(); v.x = 3; v.y = 4; v.sum()"), 7); - check_eq("fast_api: member dot", run_script(context, "v.dot(2, 3)"), 18); - } - - // --- Lambda as fast_fn is not supported (lambdas can't be NTTPs) --- - // This is by design: fast_fn requires a function pointer known at compile time. - // Use regular .function() for lambdas. -} +#include "v8pp/class.hpp" +#include "v8pp/context.hpp" +#include "v8pp/fast_api.hpp" +#include "v8pp/module.hpp" + +#include "test.hpp" + +// Free functions for fast API testing +static int32_t fast_add(int32_t a, int32_t b) +{ + return a + b; +} +static double fast_mul(double a, double b) +{ + return a * b; +} +static bool fast_negate(bool x) +{ + return !x; +} +static uint32_t fast_square(uint32_t x) +{ + return x * x; +} + +// Non-compatible functions (should silently fall back to slow-only) +static std::string slow_greet(std::string name) +{ + return "hello " + name; +} +static int isolate_func(v8::Isolate*, int x) +{ + return x; +} + +// Compile-time compatibility checks +static_assert(v8pp::detail::is_fast_api_compatible::value); +static_assert(v8pp::detail::is_fast_api_compatible::value); +static_assert(v8pp::detail::is_fast_api_compatible::value); +static_assert(v8pp::detail::is_fast_api_compatible::value); +static_assert(!v8pp::detail::is_fast_api_compatible::value); +static_assert(!v8pp::detail::is_fast_api_compatible::value); + +// Return type checks +static_assert(v8pp::detail::is_fast_return_type_v); +static_assert(v8pp::detail::is_fast_return_type_v); +static_assert(v8pp::detail::is_fast_return_type_v); +static_assert(v8pp::detail::is_fast_return_type_v); +static_assert(v8pp::detail::is_fast_return_type_v); +static_assert(!v8pp::detail::is_fast_return_type_v); +static_assert(!v8pp::detail::is_fast_return_type_v); + +// Arg type checks +static_assert(v8pp::detail::is_fast_arg_type_v); +static_assert(v8pp::detail::is_fast_arg_type_v); +static_assert(v8pp::detail::is_fast_arg_type_v); +static_assert(!v8pp::detail::is_fast_arg_type_v); +static_assert(!v8pp::detail::is_fast_arg_type_v); + +void test_fast_api() +{ + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + // --- Free function: int32_t + int32_t --- + context.function("fast_add", v8pp::fast_fn<&fast_add>); + check_eq("fast_api: add ints", run_script(context, "fast_add(10, 20)"), 30); + check_eq("fast_api: add negative", run_script(context, "fast_add(-5, 3)"), -2); + + // --- Free function: double * double --- + context.function("fast_mul", v8pp::fast_fn<&fast_mul>); + check_eq("fast_api: mul doubles", run_script(context, "fast_mul(1.5, 3.0)"), 4.5); + + // --- Bool return --- + context.function("fast_negate", v8pp::fast_fn<&fast_negate>); + check_eq("fast_api: negate true", run_script(context, "fast_negate(true)"), false); + check_eq("fast_api: negate false", run_script(context, "fast_negate(false)"), true); + + // --- uint32_t --- + context.function("fast_square", v8pp::fast_fn<&fast_square>); + check_eq("fast_api: square", run_script(context, "fast_square(7)"), 49); + + // --- Non-compatible function silently falls back to slow --- + context.function("slow_greet", v8pp::fast_fn<&slow_greet>); + check_eq("fast_api: slow fallback", run_script(context, "slow_greet('world')"), "hello world"); + + // --- Module function with fast API --- + { + v8pp::module m(isolate); + m.function("compute", v8pp::fast_fn<&fast_add>); + context.module("fast_mod", m); + + check_eq("fast_api: module func", run_script(context, "fast_mod.compute(3, 4)"), 7); + } + + // --- Class member function with fast API --- + { + struct Vec + { + int32_t x = 0; + int32_t y = 0; + int32_t sum() const { return x + y; } + int32_t dot(int32_t ox, int32_t oy) const { return x * ox + y * oy; } + }; + + v8pp::class_ vec_class(isolate); + vec_class + .ctor<>() + .var("x", &Vec::x) + .var("y", &Vec::y) + .function("sum", v8pp::fast_fn<&Vec::sum>) + .function("dot", v8pp::fast_fn<&Vec::dot>); + context.class_("Vec", vec_class); + + check_eq("fast_api: member sum", run_script(context, "var v = new Vec(); v.x = 3; v.y = 4; v.sum()"), 7); + check_eq("fast_api: member dot", run_script(context, "v.dot(2, 3)"), 18); + } + + // --- Lambda as fast_fn is not supported (lambdas can't be NTTPs) --- + // This is by design: fast_fn requires a function pointer known at compile time. + // Use regular .function() for lambdas. + + // --- Class property with fast API getter (read-only) --- + { + struct Point + { + int32_t x = 0; + int32_t y = 0; + int32_t get_x() const { return x; } + int32_t get_y() const { return y; } + void set_x(int32_t v) { x = v; } + void set_y(int32_t v) { y = v; } + }; + + v8pp::class_ point_class(isolate); + point_class + .ctor<>() + .var("x", &Point::x) + .var("y", &Point::y) + .property("fast_x", v8pp::fast_fn<&Point::get_x>) + .property("fast_xy", v8pp::fast_fn<&Point::get_x>, v8pp::fast_fn<&Point::set_x>); + context.class_("Point", point_class); + + // Read-only fast property + check_eq("fast_api: class read-only property", + run_script(context, "var p = new Point(); p.x = 42; p.fast_x"), 42); + + // Read-only should not be writable + check_eq("fast_api: class read-only property is readonly", + run_script(context, "p.fast_x = 999; p.fast_x"), 42); + + // Read-write fast property + check_eq("fast_api: class read-write property get", + run_script(context, "p = new Point(); p.x = 10; p.fast_xy"), 10); + + check_eq("fast_api: class read-write property set", + run_script(context, "p.fast_xy = 77; p.x"), 77); + } + + // --- Module property with fast API --- + { + static int32_t mod_value = 0; + + struct ModState + { + static int32_t get_val() { return mod_value; } + static void set_val(int32_t v) { mod_value = v; } + static int32_t get_const() { return 123; } + }; + + v8pp::module m(isolate); + m.property("fast_val", v8pp::fast_fn<&ModState::get_val>, v8pp::fast_fn<&ModState::set_val>); + m.property("fast_const", v8pp::fast_fn<&ModState::get_const>); + context.module("fmod", m); + + // Read-write module property + mod_value = 0; + check_eq("fast_api: module read-write property set+get", + run_script(context, "fmod.fast_val = 55; fmod.fast_val"), 55); + check_eq("fast_api: module property updated C++ side", mod_value, 55); + + // Read-only module property + check_eq("fast_api: module read-only property", + run_script(context, "fmod.fast_const"), 123); + + // Read-only should not be writable + check_eq("fast_api: module read-only property is readonly", + run_script(context, "fmod.fast_const = 999; fmod.fast_const"), 123); + } +} diff --git a/test/test_function.cpp b/test/test_function.cpp index 5be5bd7..8803201 100644 --- a/test/test_function.cpp +++ b/test/test_function.cpp @@ -3,9 +3,18 @@ #include "test.hpp" -static int f(int const& x) { return x; } -static std::string g(char const* s) { return s ? s : ""; } -static int h(v8::Isolate*, int x, int y) { return x + y; } +static int f(int const& x) +{ + return x; +} +static std::string g(char const* s) +{ + return s ? s : ""; +} +static int h(v8::Isolate*, int x, int y) +{ + return x + y; +} struct X { @@ -38,11 +47,15 @@ void test_function() moveonly& operator=(moveonly&&) = default; }; moveonly z; - context.function("lambda", [x, y, z = std::move(z)](int a) { return a + x + y + z.v; }); + context.function("lambda", [x, y, z = std::move(z)](int a) + { return a + x + y + z.v; }); check_eq("lambda", run_script(context, "lambda(3)"), 9); - auto lambda2 = []() { return 99; }; - //TODO: static_assert(v8pp::detail::external_data::is_bitcast_allowed::value); + auto lambda2 = []() + { + return 99; + }; + // TODO: static_assert(v8pp::detail::external_data::is_bitcast_allowed::value); context.function("lambda2", lambda2); check_eq("lambda2", run_script(context, "lambda2()"), 99); diff --git a/test/test_gc_stress.cpp b/test/test_gc_stress.cpp index 5c165b5..d53c124 100644 --- a/test/test_gc_stress.cpp +++ b/test/test_gc_stress.cpp @@ -1,224 +1,227 @@ -#include "v8pp/class.hpp" -#include "v8pp/context.hpp" - -#include "test.hpp" - -#include -#include - -namespace { - -struct GCObj -{ - static std::atomic instance_count; - int value; - - explicit GCObj(int v = 0) : value(v) { ++instance_count; } - ~GCObj() { --instance_count; } - - int get() const { return value; } -}; - -std::atomic GCObj::instance_count = 0; - -struct GCBase -{ - static std::atomic base_count; - int x; - - explicit GCBase(int v = 0) : x(v) { ++base_count; } - ~GCBase() { --base_count; } - - int get_x() const { return x; } -}; - -std::atomic GCBase::base_count = 0; - -struct GCDerived : GCBase -{ - static std::atomic derived_count; - int y; - - explicit GCDerived(int v = 0) : GCBase(v), y(v * 2) { ++derived_count; } - ~GCDerived() { --derived_count; } - - int get_y() const { return y; } -}; - -std::atomic GCDerived::derived_count = 0; - -void force_gc(v8::Isolate* isolate) -{ - // Force GC twice to ensure weak callbacks are processed - isolate->RequestGarbageCollectionForTesting( - v8::Isolate::GarbageCollectionType::kFullGarbageCollection); - isolate->RequestGarbageCollectionForTesting( - v8::Isolate::GarbageCollectionType::kFullGarbageCollection); -} - -template -void test_gc_stress_bulk() -{ - GCObj::instance_count = 0; - - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ GCObj_class(isolate); - GCObj_class - .template ctor() - .function("get", &GCObj::get); - - context.class_("GCObj", GCObj_class); - - // Create 10k objects in batches via JS, all unreferenced immediately - for (int batch = 0; batch < 100; ++batch) - { - v8::HandleScope batch_scope(isolate); - run_script(context, - "for (var i = 0; i < 100; i++) { new GCObj(i); } 0"); - } - - // Force GC to reclaim all unreferenced objects - force_gc(isolate); - - check_eq("bulk 10k GC cleanup", GCObj::instance_count.load(), 0); -} - -template -void test_gc_stress_mixed_lifespan() -{ - GCObj::instance_count = 0; - - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ GCObj_class(isolate); - GCObj_class - .template ctor() - .function("get", &GCObj::get); - - context.class_("GCObj", GCObj_class); - - // Hold 100 objects alive via reference_external - std::vector> held; - for (int i = 0; i < 100; ++i) - { - auto obj = Traits::template create(i); - v8pp::class_::reference_external(isolate, obj); - held.push_back(obj); - } - - // Create 10k more objects via JS (all unreferenced) - for (int batch = 0; batch < 100; ++batch) - { - v8::HandleScope batch_scope(isolate); - run_script(context, - "for (var i = 0; i < 100; i++) { new GCObj(i); } 0"); - } - - force_gc(isolate); - - // The 100 held objects should survive GC - check_eq("mixed lifespan after GC", GCObj::instance_count.load(), 100); - - // Unreference all held objects - for (auto& obj : held) - { - v8pp::class_::unreference_external(isolate, obj); - } - held.clear(); - - force_gc(isolate); - - bool constexpr use_shared_ptr = std::same_as; - // raw_ptr: unreference doesn't delete, so objects still exist until scope ends - // shared_ptr: unreference removes registry entry, shared_ptr copies in held are cleared - check_eq("mixed lifespan fully cleaned", - GCObj::instance_count.load(), use_shared_ptr ? 0 : 100); -} - -template -void test_gc_stress_rapid_cycles() -{ - GCObj::instance_count = 0; - - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ GCObj_class(isolate); - GCObj_class - .template ctor() - .function("get", &GCObj::get); - - context.class_("GCObj", GCObj_class); - - // Rapid create-destroy cycles: 100 cycles of 100 objects each - for (int cycle = 0; cycle < 100; ++cycle) - { - v8::HandleScope cycle_scope(isolate); - run_script(context, - "for (var i = 0; i < 100; i++) { new GCObj(i); } 0"); - force_gc(isolate); - } - - check_eq("rapid cycles cleanup", GCObj::instance_count.load(), 0); -} - -template -void test_gc_stress_inheritance() -{ - GCBase::base_count = 0; - GCDerived::derived_count = 0; - - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ base_class(isolate); - base_class - .template ctor() - .function("get_x", &GCBase::get_x); - - v8pp::class_ derived_class(isolate); - derived_class - .template ctor() - .template inherit() - .function("get_y", &GCDerived::get_y); - - context.class_("GCBase", base_class); - context.class_("GCDerived", derived_class); - - // Create 5k derived objects (each also constructs a base) - for (int batch = 0; batch < 50; ++batch) - { - v8::HandleScope batch_scope(isolate); - run_script(context, - "for (var i = 0; i < 100; i++) { new GCDerived(i); } 0"); - } - - force_gc(isolate); - - check_eq("inheritance stress derived cleanup", GCDerived::derived_count.load(), 0); - check_eq("inheritance stress base cleanup", GCBase::base_count.load(), 0); -} - -} // anonymous namespace - -void test_gc_stress() -{ - test_gc_stress_bulk(); - test_gc_stress_bulk(); - - test_gc_stress_mixed_lifespan(); - test_gc_stress_mixed_lifespan(); - - test_gc_stress_rapid_cycles(); - test_gc_stress_rapid_cycles(); - - test_gc_stress_inheritance(); - test_gc_stress_inheritance(); -} +#include "v8pp/class.hpp" +#include "v8pp/context.hpp" + +#include "test.hpp" + +#include +#include + +namespace { + +struct GCObj +{ + static std::atomic instance_count; + int value; + + explicit GCObj(int v = 0) + : value(v) { ++instance_count; } + ~GCObj() { --instance_count; } + + int get() const { return value; } +}; + +std::atomic GCObj::instance_count = 0; + +struct GCBase +{ + static std::atomic base_count; + int x; + + explicit GCBase(int v = 0) + : x(v) { ++base_count; } + ~GCBase() { --base_count; } + + int get_x() const { return x; } +}; + +std::atomic GCBase::base_count = 0; + +struct GCDerived : GCBase +{ + static std::atomic derived_count; + int y; + + explicit GCDerived(int v = 0) + : GCBase(v), y(v * 2) { ++derived_count; } + ~GCDerived() { --derived_count; } + + int get_y() const { return y; } +}; + +std::atomic GCDerived::derived_count = 0; + +void force_gc(v8::Isolate* isolate) +{ + // Force GC twice to ensure weak callbacks are processed + isolate->RequestGarbageCollectionForTesting( + v8::Isolate::GarbageCollectionType::kFullGarbageCollection); + isolate->RequestGarbageCollectionForTesting( + v8::Isolate::GarbageCollectionType::kFullGarbageCollection); +} + +template +void test_gc_stress_bulk() +{ + GCObj::instance_count = 0; + + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ GCObj_class(isolate); + GCObj_class + .template ctor() + .function("get", &GCObj::get); + + context.class_("GCObj", GCObj_class); + + // Create 10k objects in batches via JS, all unreferenced immediately + for (int batch = 0; batch < 100; ++batch) + { + v8::HandleScope batch_scope(isolate); + run_script(context, + "for (var i = 0; i < 100; i++) { new GCObj(i); } 0"); + } + + // Force GC to reclaim all unreferenced objects + force_gc(isolate); + + check_eq("bulk 10k GC cleanup", GCObj::instance_count.load(), 0); +} + +template +void test_gc_stress_mixed_lifespan() +{ + GCObj::instance_count = 0; + + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ GCObj_class(isolate); + GCObj_class + .template ctor() + .function("get", &GCObj::get); + + context.class_("GCObj", GCObj_class); + + // Hold 100 objects alive via reference_external + std::vector> held; + for (int i = 0; i < 100; ++i) + { + auto obj = Traits::template create(i); + v8pp::class_::reference_external(isolate, obj); + held.push_back(obj); + } + + // Create 10k more objects via JS (all unreferenced) + for (int batch = 0; batch < 100; ++batch) + { + v8::HandleScope batch_scope(isolate); + run_script(context, + "for (var i = 0; i < 100; i++) { new GCObj(i); } 0"); + } + + force_gc(isolate); + + // The 100 held objects should survive GC + check_eq("mixed lifespan after GC", GCObj::instance_count.load(), 100); + + // Unreference all held objects + for (auto& obj : held) + { + v8pp::class_::unreference_external(isolate, obj); + } + held.clear(); + + force_gc(isolate); + + bool constexpr use_shared_ptr = std::same_as; + // raw_ptr: unreference doesn't delete, so objects still exist until scope ends + // shared_ptr: unreference removes registry entry, shared_ptr copies in held are cleared + check_eq("mixed lifespan fully cleaned", + GCObj::instance_count.load(), use_shared_ptr ? 0 : 100); +} + +template +void test_gc_stress_rapid_cycles() +{ + GCObj::instance_count = 0; + + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ GCObj_class(isolate); + GCObj_class + .template ctor() + .function("get", &GCObj::get); + + context.class_("GCObj", GCObj_class); + + // Rapid create-destroy cycles: 100 cycles of 100 objects each + for (int cycle = 0; cycle < 100; ++cycle) + { + v8::HandleScope cycle_scope(isolate); + run_script(context, + "for (var i = 0; i < 100; i++) { new GCObj(i); } 0"); + force_gc(isolate); + } + + check_eq("rapid cycles cleanup", GCObj::instance_count.load(), 0); +} + +template +void test_gc_stress_inheritance() +{ + GCBase::base_count = 0; + GCDerived::derived_count = 0; + + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ base_class(isolate); + base_class + .template ctor() + .function("get_x", &GCBase::get_x); + + v8pp::class_ derived_class(isolate); + derived_class + .template ctor() + .template inherit() + .function("get_y", &GCDerived::get_y); + + context.class_("GCBase", base_class); + context.class_("GCDerived", derived_class); + + // Create 5k derived objects (each also constructs a base) + for (int batch = 0; batch < 50; ++batch) + { + v8::HandleScope batch_scope(isolate); + run_script(context, + "for (var i = 0; i < 100; i++) { new GCDerived(i); } 0"); + } + + force_gc(isolate); + + check_eq("inheritance stress derived cleanup", GCDerived::derived_count.load(), 0); + check_eq("inheritance stress base cleanup", GCBase::base_count.load(), 0); +} + +} // anonymous namespace + +void test_gc_stress() +{ + test_gc_stress_bulk(); + test_gc_stress_bulk(); + + test_gc_stress_mixed_lifespan(); + test_gc_stress_mixed_lifespan(); + + test_gc_stress_rapid_cycles(); + test_gc_stress_rapid_cycles(); + + test_gc_stress_inheritance(); + test_gc_stress_inheritance(); +} diff --git a/test/test_json.cpp b/test/test_json.cpp index 0cb5742..163f8b3 100644 --- a/test/test_json.cpp +++ b/test/test_json.cpp @@ -1,73 +1,73 @@ -#include "v8pp/json.hpp" -#include "v8pp/context.hpp" -#include "v8pp/object.hpp" - -#include "test.hpp" - -void test_json() -{ - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8::Local v; - std::string str; - - str = v8pp::json_str(isolate, v); - v = v8pp::json_parse(isolate, str); - check("empty string", str.empty()); - check("empty parse", v.IsEmpty()); - - v = v8::Integer::New(isolate, 42); - - str = v8pp::json_str(isolate, v); - v = v8pp::json_parse(isolate, "42"); - check_eq("int string", str, "42"); - check_eq("int parse", v->Int32Value(isolate->GetCurrentContext()).FromJust(), 42); - - v8::Local obj = v8::Object::New(isolate); - v8pp::set_option(isolate, obj, "x", 1); - v8pp::set_option(isolate, obj, "y", 2.2); - v8pp::set_option(isolate, obj, "z", "abc"); - - str = v8pp::json_str(isolate, obj); - v = v8pp::json_parse(isolate, str); - check_eq("object string", str, R"({"x":1,"y":2.2,"z":"abc"})"); - check_eq("object parse", v8pp::json_str(isolate, v), str); - - v = v8pp::json_object(isolate, v.As()); - check_eq("json object", v8pp::json_str(isolate, v), str); - - v8::Local arr = v8::Array::New(isolate, 1); - arr->Set(isolate->GetCurrentContext(), 0, obj).FromJust(); - - str = v8pp::json_str(isolate, arr); - v = v8pp::json_parse(isolate, str); - check_eq("array string", str, R"([{"x":1,"y":2.2,"z":"abc"}])"); - check_eq("array parse", v8pp::json_str(isolate, v), str); - - v = v8pp::json_parse(isolate, "blah-blah"); - check("parse error", v->IsNativeError()); - - // Phase 1a/1b crash safety: Stringify with circular reference should return empty, not crash - { - v8::TryCatch try_catch(isolate); - v8::Local circular = v8::Object::New(isolate); - v8::Local ctx = isolate->GetCurrentContext(); - circular->Set(ctx, v8pp::to_v8(isolate, "self"), circular).FromJust(); - str = v8pp::json_str(isolate, circular); - check("json_str circular returns empty", str.empty()); - } - - // json_str with empty value returns empty string - str = v8pp::json_str(isolate, v8::Local()); - check("json_str empty value", str.empty()); - - // json_object with empty property names (safety of GetPropertyNames path) - { - v8::Local plain = v8::Object::New(isolate); - v8::Local result = v8pp::json_object(isolate, plain); - check("json_object empty obj", !result.IsEmpty()); - check_eq("json_object empty obj str", v8pp::json_str(isolate, result), "{}"); - } -} +#include "v8pp/json.hpp" +#include "v8pp/context.hpp" +#include "v8pp/object.hpp" + +#include "test.hpp" + +void test_json() +{ + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8::Local v; + std::string str; + + str = v8pp::json_str(isolate, v); + v = v8pp::json_parse(isolate, str); + check("empty string", str.empty()); + check("empty parse", v.IsEmpty()); + + v = v8::Integer::New(isolate, 42); + + str = v8pp::json_str(isolate, v); + v = v8pp::json_parse(isolate, "42"); + check_eq("int string", str, "42"); + check_eq("int parse", v->Int32Value(isolate->GetCurrentContext()).FromJust(), 42); + + v8::Local obj = v8::Object::New(isolate); + v8pp::set_option(isolate, obj, "x", 1); + v8pp::set_option(isolate, obj, "y", 2.2); + v8pp::set_option(isolate, obj, "z", "abc"); + + str = v8pp::json_str(isolate, obj); + v = v8pp::json_parse(isolate, str); + check_eq("object string", str, R"({"x":1,"y":2.2,"z":"abc"})"); + check_eq("object parse", v8pp::json_str(isolate, v), str); + + v = v8pp::json_object(isolate, v.As()); + check_eq("json object", v8pp::json_str(isolate, v), str); + + v8::Local arr = v8::Array::New(isolate, 1); + arr->Set(isolate->GetCurrentContext(), 0, obj).FromJust(); + + str = v8pp::json_str(isolate, arr); + v = v8pp::json_parse(isolate, str); + check_eq("array string", str, R"([{"x":1,"y":2.2,"z":"abc"}])"); + check_eq("array parse", v8pp::json_str(isolate, v), str); + + v = v8pp::json_parse(isolate, "blah-blah"); + check("parse error", v->IsNativeError()); + + // Phase 1a/1b crash safety: Stringify with circular reference should return empty, not crash + { + v8::TryCatch try_catch(isolate); + v8::Local circular = v8::Object::New(isolate); + v8::Local ctx = isolate->GetCurrentContext(); + circular->Set(ctx, v8pp::to_v8(isolate, "self"), circular).FromJust(); + str = v8pp::json_str(isolate, circular); + check("json_str circular returns empty", str.empty()); + } + + // json_str with empty value returns empty string + str = v8pp::json_str(isolate, v8::Local()); + check("json_str empty value", str.empty()); + + // json_object with empty property names (safety of GetPropertyNames path) + { + v8::Local plain = v8::Object::New(isolate); + v8::Local result = v8pp::json_object(isolate, plain); + check("json_object empty obj", !result.IsEmpty()); + check_eq("json_object empty obj str", v8pp::json_str(isolate, result), "{}"); + } +} diff --git a/test/test_module.cpp b/test/test_module.cpp index 5556adb..cd87873 100644 --- a/test/test_module.cpp +++ b/test/test_module.cpp @@ -8,11 +8,20 @@ static std::string var; -static int fun(int x) { return x + 1; } +static int fun(int x) +{ + return x + 1; +} static int x = 1; -static int get_x() { return x + 1; } -static void set_x(int v) { x = v - 1; } +static int get_x() +{ + return x + 1; +} +static void set_x(int v) +{ + x = v - 1; +} static_assert(std::is_move_constructible_v); static_assert(std::is_move_assignable_v); @@ -33,8 +42,7 @@ void test_module() .const_("char", 'Z') .const_("int", 100) .const_("str", "str") - .const_("num", 99.9) - ; + .const_("num", 99.9); module .submodule("consts", consts) @@ -42,8 +50,7 @@ void test_module() .function("fun", &fun) .value("empty", v8::Null(context.isolate())) .property("rprop", get_x) - .property("wprop", get_x, set_x) - ; + .property("wprop", get_x, set_x); context.module("module", module); @@ -56,8 +63,7 @@ void test_module() check_eq("module.consts.str", run_script(context, "module.consts.str"), "str"); - check_eq("module.var", run_script(context, - "module.var = 'test'; module.var"), "test"); + check_eq("module.var", run_script(context, "module.var = 'test'; module.var"), "test"); check_eq("var", var, "test"); check_eq("module.fun", diff --git a/test/test_overload.cpp b/test/test_overload.cpp index 24369dd..5bdf076 100644 --- a/test/test_overload.cpp +++ b/test/test_overload.cpp @@ -1,122 +1,142 @@ -#include "v8pp/class.hpp" -#include "v8pp/context.hpp" -#include "v8pp/module.hpp" -#include "v8pp/overload.hpp" - -#include "test.hpp" - -// Free functions for overload testing -static int add_int(int a, int b) { return a + b; } -static double add_double(double a, double b) { return a + b; } -static std::string add_string(std::string a, std::string b) { return a + b; } -static int negate_int(int a) { return -a; } - -void test_overload() -{ - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - // --- Arity-based dispatch --- - // f(int) vs f(int, int) - context.function("arity_test", - static_cast(&negate_int), - static_cast(&add_int)); - - check_eq("overload: arity 1 arg", run_script(context, "arity_test(5)"), -5); - check_eq("overload: arity 2 args", run_script(context, "arity_test(3, 7)"), 10); - - // --- Type-based dispatch --- - // f(int, int) vs f(string, string) - context.function("type_test", - static_cast(&add_int), - static_cast(&add_string)); - - check_eq("overload: type int", run_script(context, "type_test(10, 20)"), 30); - check_eq("overload: type string", run_script(context, "type_test('hello', ' world')"), "hello world"); - - // --- Mixed arity + type --- - // f(int) vs f(double, double) - context.function("mixed_test", - static_cast(&negate_int), - static_cast(&add_double)); - - check_eq("overload: mixed 1 arg", run_script(context, "mixed_test(42)"), -42); - check_eq("overload: mixed 2 args", run_script(context, "mixed_test(1.5, 2.5)"), 4.0); - - // --- Lambda overloads --- - context.function("lambda_test", - [](int x) { return x * 2; }, - [](std::string s) { return s + s; }); - - check_eq("overload: lambda int", run_script(context, "lambda_test(7)"), 14); - check_eq("overload: lambda string", run_script(context, "lambda_test('ab')"), "abab"); - - // --- No match → error --- - check_ex("overload: no match", [&context] - { - run_script(context, "arity_test()"); - }); - - // --- Module function overloads --- - { - v8pp::module m(isolate); - m.function("compute", - [](int x) { return x * x; }, - [](int x, int y) { return x + y; }); - context.module("ovl_mod", m); - - check_eq("overload: module 1 arg", run_script(context, "ovl_mod.compute(5)"), 25); - check_eq("overload: module 2 args", run_script(context, "ovl_mod.compute(3, 4)"), 7); - } - - // --- Overload with defaults --- - { - context.function("defaults_overload", - v8pp::with_defaults([](int a, int b) { return a + b; }, v8pp::defaults(10)), - [](std::string s) { return s; }); - - check_eq("overload: defaults int both", run_script(context, "defaults_overload(3, 7)"), 10); - check_eq("overload: defaults int default", run_script(context, "defaults_overload(5)"), 15); - check_eq("overload: defaults string", run_script(context, "defaults_overload('hi')"), "hi"); - } - - // --- Class member overloads --- - { - struct Calc - { - int value = 0; - int add_one(int n) { value += n; return value; } - int add_two(int a, int b) { value += a + b; return value; } - }; - - v8pp::class_ calc_class(isolate); - calc_class - .ctor() - .function("add", &Calc::add_one, &Calc::add_two); - context.class_("Calc", calc_class); - - check_eq("overload: class 1 arg", run_script(context, "var calc = new Calc(); calc.add(5)"), 5); - check_eq("overload: class 2 args", run_script(context, "calc.add(3, 7)"), 15); - } - - // --- v8pp::overload selector --- - { - struct Multi - { - int compute(int x) { return x * 2; } - int compute(int x, int y) { return x + y; } - }; - - v8pp::class_ multi_class(isolate); - multi_class - .ctor() - .function("compute", - v8pp::overload(&Multi::compute), - v8pp::overload(&Multi::compute)); - context.class_("Multi", multi_class); - - check_eq("overload: selector 1 arg", run_script(context, "var m = new Multi(); m.compute(5)"), 10); - check_eq("overload: selector 2 args", run_script(context, "m.compute(3, 4)"), 7); - } -} +#include "v8pp/class.hpp" +#include "v8pp/context.hpp" +#include "v8pp/module.hpp" +#include "v8pp/overload.hpp" + +#include "test.hpp" + +// Free functions for overload testing +static int add_int(int a, int b) +{ + return a + b; +} +static double add_double(double a, double b) +{ + return a + b; +} +static std::string add_string(std::string a, std::string b) +{ + return a + b; +} +static int negate_int(int a) +{ + return -a; +} + +void test_overload() +{ + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + // --- Arity-based dispatch --- + // f(int) vs f(int, int) + context.function("arity_test", + static_cast(&negate_int), + static_cast(&add_int)); + + check_eq("overload: arity 1 arg", run_script(context, "arity_test(5)"), -5); + check_eq("overload: arity 2 args", run_script(context, "arity_test(3, 7)"), 10); + + // --- Type-based dispatch --- + // f(int, int) vs f(string, string) + context.function("type_test", + static_cast(&add_int), + static_cast(&add_string)); + + check_eq("overload: type int", run_script(context, "type_test(10, 20)"), 30); + check_eq("overload: type string", run_script(context, "type_test('hello', ' world')"), "hello world"); + + // --- Mixed arity + type --- + // f(int) vs f(double, double) + context.function("mixed_test", + static_cast(&negate_int), + static_cast(&add_double)); + + check_eq("overload: mixed 1 arg", run_script(context, "mixed_test(42)"), -42); + check_eq("overload: mixed 2 args", run_script(context, "mixed_test(1.5, 2.5)"), 4.0); + + // --- Lambda overloads --- + context.function("lambda_test", [](int x) + { return x * 2; }, [](std::string s) + { return s + s; }); + + check_eq("overload: lambda int", run_script(context, "lambda_test(7)"), 14); + check_eq("overload: lambda string", run_script(context, "lambda_test('ab')"), "abab"); + + // --- No match → error --- + check_ex("overload: no match", [&context] + { run_script(context, "arity_test()"); }); + + // --- Module function overloads --- + { + v8pp::module m(isolate); + m.function("compute", [](int x) + { return x * x; }, [](int x, int y) + { return x + y; }); + context.module("ovl_mod", m); + + check_eq("overload: module 1 arg", run_script(context, "ovl_mod.compute(5)"), 25); + check_eq("overload: module 2 args", run_script(context, "ovl_mod.compute(3, 4)"), 7); + } + + // --- Overload with defaults --- + { + context.function("defaults_overload", + v8pp::with_defaults([](int a, int b) + { return a + b; }, v8pp::defaults(10)), + [](std::string s) + { return s; }); + + check_eq("overload: defaults int both", run_script(context, "defaults_overload(3, 7)"), 10); + check_eq("overload: defaults int default", run_script(context, "defaults_overload(5)"), 15); + check_eq("overload: defaults string", run_script(context, "defaults_overload('hi')"), "hi"); + } + + // --- Class member overloads --- + { + struct Calc + { + int value = 0; + int add_one(int n) + { + value += n; + return value; + } + int add_two(int a, int b) + { + value += a + b; + return value; + } + }; + + v8pp::class_ calc_class(isolate); + calc_class + .ctor() + .function("add", &Calc::add_one, &Calc::add_two); + context.class_("Calc", calc_class); + + check_eq("overload: class 1 arg", run_script(context, "var calc = new Calc(); calc.add(5)"), 5); + check_eq("overload: class 2 args", run_script(context, "calc.add(3, 7)"), 15); + } + + // --- v8pp::overload selector --- + { + struct Multi + { + int compute(int x) { return x * 2; } + int compute(int x, int y) { return x + y; } + }; + + v8pp::class_ multi_class(isolate); + multi_class + .ctor() + .function("compute", + v8pp::overload(&Multi::compute), + v8pp::overload(&Multi::compute)); + context.class_("Multi", multi_class); + + check_eq("overload: selector 1 arg", run_script(context, "var m = new Multi(); m.compute(5)"), 10); + check_eq("overload: selector 2 args", run_script(context, "m.compute(3, 4)"), 7); + } +} diff --git a/test/test_promise.cpp b/test/test_promise.cpp index 4409ac6..48983dd 100644 --- a/test/test_promise.cpp +++ b/test/test_promise.cpp @@ -1,206 +1,197 @@ -#include "v8pp/promise.hpp" -#include "v8pp/class.hpp" -#include "v8pp/module.hpp" - -#include "test.hpp" - -void test_promise() -{ - // Test 1: immediate resolve with int - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - context.function("makePromise", [](v8::Isolate* isolate) -> v8pp::promise - { +#include "v8pp/promise.hpp" +#include "v8pp/class.hpp" +#include "v8pp/module.hpp" + +#include "test.hpp" + +void test_promise() +{ + // Test 1: immediate resolve with int + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + context.function("makePromise", [](v8::Isolate* isolate) -> v8pp::promise + { v8pp::promise p(isolate); p.resolve(42); - return p; - }); - - // V8 runs microtasks between top-level script evaluations. - // First script sets up the .then(), second script reads the result. - run_script(context, - "var intResult = 0;" - "makePromise().then(function(v) { intResult = v; });" - "''"); - check_eq("resolved int promise", - run_script(context, "intResult"), - 42); - } - - // Test 2: immediate resolve with string - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - context.function("strPromise", [](v8::Isolate* isolate) -> v8pp::promise - { + return p; }); + + // V8 runs microtasks between top-level script evaluations. + // First script sets up the .then(), second script reads the result. + run_script(context, + "var intResult = 0;" + "makePromise().then(function(v) { intResult = v; });" + "''"); + check_eq("resolved int promise", + run_script(context, "intResult"), + 42); + } + + // Test 2: immediate resolve with string + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + context.function("strPromise", [](v8::Isolate* isolate) -> v8pp::promise + { v8pp::promise p(isolate); p.resolve("hello world"); - return p; - }); - - run_script(context, - "var strResult = '';" - "strPromise().then(function(v) { strResult = v; });" - "''"); - check_eq("resolved string promise", - run_script(context, "strResult"), - "hello world"); - } - - // Test 3: immediate reject with error message - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - context.function("rejectPromise", [](v8::Isolate* isolate) -> v8pp::promise - { + return p; }); + + run_script(context, + "var strResult = '';" + "strPromise().then(function(v) { strResult = v; });" + "''"); + check_eq("resolved string promise", + run_script(context, "strResult"), + "hello world"); + } + + // Test 3: immediate reject with error message + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + context.function("rejectPromise", [](v8::Isolate* isolate) -> v8pp::promise + { v8pp::promise p(isolate); p.reject("something went wrong"); - return p; - }); - - run_script(context, - "var errMsg = '';" - "rejectPromise().catch(function(e) { errMsg = e.message; });" - "''"); - check_eq("rejected promise", - run_script(context, "errMsg"), - "something went wrong"); - } - - // Test 4: reject with raw V8 value - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - context.function("rejectRaw", [](v8::Isolate* isolate) -> v8pp::promise - { + return p; }); + + run_script(context, + "var errMsg = '';" + "rejectPromise().catch(function(e) { errMsg = e.message; });" + "''"); + check_eq("rejected promise", + run_script(context, "errMsg"), + "something went wrong"); + } + + // Test 4: reject with raw V8 value + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + context.function("rejectRaw", [](v8::Isolate* isolate) -> v8pp::promise + { v8pp::promise p(isolate); p.reject(v8pp::to_v8(isolate, "raw rejection")); - return p; - }); - - run_script(context, - "var rawErr = '';" - "rejectRaw().catch(function(e) { rawErr = String(e); });" - "''"); - check_eq("raw rejection", - run_script(context, "rawErr"), - "raw rejection"); - } - - // Test 5: promise - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - context.function("voidPromise", [](v8::Isolate* isolate) -> v8pp::promise - { + return p; }); + + run_script(context, + "var rawErr = '';" + "rejectRaw().catch(function(e) { rawErr = String(e); });" + "''"); + check_eq("raw rejection", + run_script(context, "rawErr"), + "raw rejection"); + } + + // Test 5: promise + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + context.function("voidPromise", [](v8::Isolate* isolate) -> v8pp::promise + { v8pp::promise p(isolate); p.resolve(); - return p; - }); - - run_script(context, - "var voidResult = 'not called';" - "voidPromise().then(function() { voidResult = 'called'; });" - "''"); - check_eq("void promise", - run_script(context, "voidResult"), - "called"); - } - - // Test 6: void promise rejection - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - context.function("voidReject", [](v8::Isolate* isolate) -> v8pp::promise - { + return p; }); + + run_script(context, + "var voidResult = 'not called';" + "voidPromise().then(function() { voidResult = 'called'; });" + "''"); + check_eq("void promise", + run_script(context, "voidResult"), + "called"); + } + + // Test 6: void promise rejection + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + context.function("voidReject", [](v8::Isolate* isolate) -> v8pp::promise + { v8pp::promise p(isolate); p.reject("void error"); - return p; - }); - - run_script(context, - "var voidErr = '';" - "voidReject().catch(function(e) { voidErr = e.message; });" - "''"); - check_eq("void promise rejection", - run_script(context, "voidErr"), - "void error"); - } - - // Test 7: promise with double - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - context.function("piPromise", [](v8::Isolate* isolate) -> v8pp::promise - { + return p; }); + + run_script(context, + "var voidErr = '';" + "voidReject().catch(function(e) { voidErr = e.message; });" + "''"); + check_eq("void promise rejection", + run_script(context, "voidErr"), + "void error"); + } + + // Test 7: promise with double + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + context.function("piPromise", [](v8::Isolate* isolate) -> v8pp::promise + { v8pp::promise p(isolate); p.resolve(3.14159); - return p; - }); - - run_script(context, - "var piResult = 0;" - "piPromise().then(function(v) { piResult = v; });" - "''"); - check_eq("double promise", - run_script(context, "piResult"), - 3.14159); - } - - // Test 8: promise chaining - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - context.function("chainPromise", [](v8::Isolate* isolate) -> v8pp::promise - { + return p; }); + + run_script(context, + "var piResult = 0;" + "piPromise().then(function(v) { piResult = v; });" + "''"); + check_eq("double promise", + run_script(context, "piResult"), + 3.14159); + } + + // Test 8: promise chaining + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + context.function("chainPromise", [](v8::Isolate* isolate) -> v8pp::promise + { v8pp::promise p(isolate); p.resolve(10); - return p; - }); - - run_script(context, - "var chainResult = 0;" - "chainPromise()" - " .then(function(v) { return v * 2; })" - " .then(function(v) { chainResult = v; });" - "''"); - check_eq("promise chain", - run_script(context, "chainResult"), - 20); - } - - // Test 9: verify return type is actually a Promise - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - context.function("isPromiseTest", [](v8::Isolate* isolate) -> v8pp::promise - { + return p; }); + + run_script(context, + "var chainResult = 0;" + "chainPromise()" + " .then(function(v) { return v * 2; })" + " .then(function(v) { chainResult = v; });" + "''"); + check_eq("promise chain", + run_script(context, "chainResult"), + 20); + } + + // Test 9: verify return type is actually a Promise + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + context.function("isPromiseTest", [](v8::Isolate* isolate) -> v8pp::promise + { v8pp::promise p(isolate); p.resolve(1); - return p; - }); - - check_eq("is a Promise", - run_script(context, "isPromiseTest() instanceof Promise"), - true); - } -} + return p; }); + + check_eq("is a Promise", + run_script(context, "isPromiseTest() instanceof Promise"), + true); + } +} diff --git a/test/test_property.cpp b/test/test_property.cpp index f28e085..437e638 100644 --- a/test/test_property.cpp +++ b/test/test_property.cpp @@ -5,14 +5,29 @@ namespace { -int get1() { return 0; } -int set1(int) { return 0; } +int get1() +{ + return 0; +} +int set1(int) +{ + return 0; +} -bool get2(v8::Isolate*) { return false; } -void set2(v8::Isolate*, int) {} +bool get2(v8::Isolate*) +{ + return false; +} +void set2(v8::Isolate*, int) +{ +} -void get3(v8::Local, v8::PropertyCallbackInfo const&) {} -void set3(v8::Local, v8::Local, v8::PropertyCallbackInfo const&) {} +void get3(v8::Local, v8::PropertyCallbackInfo const&) +{ +} +void set3(v8::Local, v8::Local, v8::PropertyCallbackInfo const&) +{ +} struct X { @@ -27,18 +42,33 @@ struct X }; // external accessors -int external_get1(const X&) { return 0; } -int external_set1(X&, int) { return 0; } +int external_get1(const X&) +{ + return 0; +} +int external_set1(X&, int) +{ + return 0; +} -bool external_get2(const X&, v8::Isolate*) { return false; } -void external_set2(X&, v8::Isolate*, int) {} +bool external_get2(const X&, v8::Isolate*) +{ + return false; +} +void external_set2(X&, v8::Isolate*, int) +{ +} -void external_get3(const volatile X&, v8::Local, v8::PropertyCallbackInfo const&) {} -void external_set3(volatile X&, v8::Local, v8::Local, v8::PropertyCallbackInfo const&) {} +void external_get3(const volatile X&, v8::Local, v8::PropertyCallbackInfo const&) +{ +} +void external_set3(volatile X&, v8::Local, v8::Local, v8::PropertyCallbackInfo const&) +{ +} using namespace v8pp::detail; -//property metafunctions +// property metafunctions static_assert(is_getter, "getter function"); static_assert(is_getter, "getter member function"); static_assert(is_getter, "getter external function"); diff --git a/test/test_symbol.cpp b/test/test_symbol.cpp index 6da22f8..7bbc21e 100644 --- a/test/test_symbol.cpp +++ b/test/test_symbol.cpp @@ -1,242 +1,245 @@ -#include "v8pp/class.hpp" -#include "v8pp/module.hpp" - -#include "test.hpp" - -namespace { - -struct Widget -{ - int value = 42; -}; - -struct NumericValue -{ - double val; - explicit NumericValue(double v) : val(v) {} - double to_number(std::string_view) const { return val; } -}; - -struct Tag -{ - std::string name; - explicit Tag(std::string n) : name(std::move(n)) {} -}; - -struct NumberList -{ - std::vector numbers; - - auto begin() const { return numbers.begin(); } - auto end() const { return numbers.end(); } -}; - -struct WordList -{ - std::vector words; - - auto begin() const { return words.begin(); } - auto end() const { return words.end(); } -}; - -} // namespace - -void test_symbol() -{ - // Test to_string_tag - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ widget_class(isolate); - widget_class - .ctor<>() - .var("value", &Widget::value) - .to_string_tag("Widget"); - - context.class_("Widget", widget_class); - - check_eq("to_string_tag", - run_script(context, - "let w = new Widget(); Object.prototype.toString.call(w)"), - "[object Widget]"); - } - - // Test to_primitive with member function - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ nv_class(isolate); - nv_class - .ctor() - .to_primitive(&NumericValue::to_number); - - context.class_("NumericValue", nv_class); - - check_eq("to_primitive +", - run_script(context, "let nv = new NumericValue(10); nv + 5"), - 15.0); - - check_eq("to_primitive *", - run_script(context, "nv * 3"), - 30.0); - } - - // Test to_primitive with lambda - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ tag_class(isolate); - tag_class - .ctor() - .var("name", &Tag::name) - .to_primitive([](Tag const& t, std::string_view) -> std::string { - return t.name; - }); - - context.class_("Tag", tag_class); - - check_eq("to_primitive string concat", - run_script(context, - "let t = new Tag('hello'); '' + t"), - "hello"); - } - - // Test iterable with int vector (member begin/end) - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ nl_class(isolate); - nl_class - .ctor<>() - .iterable(&NumberList::begin, &NumberList::end); - - context.class_("NumberList", nl_class); - - auto* nl = new NumberList{{1, 2, 3, 4, 5}}; - auto nl_obj = v8pp::class_::import_external(isolate, nl); - v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "nl", nl_obj); - - check_eq("for...of sum", - run_script(context, - "let sum = 0; for (const n of nl) sum += n; sum"), - 15); - - check_eq("spread to array", - run_script(context, - "JSON.stringify([...nl])"), - "[1,2,3,4,5]"); - - check_eq("Array.from", - run_script(context, - "Array.from(nl).length"), - 5); - } - - // Test iterable with string vector - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ wl_class(isolate); - wl_class - .ctor<>() - .iterable(&WordList::begin, &WordList::end); - - context.class_("WordList", wl_class); - - auto* wl = new WordList{{"hello", "world"}}; - auto wl_obj = v8pp::class_::import_external(isolate, wl); - v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "wl", wl_obj); - - check_eq("string iterable", - run_script(context, - "let parts = []; for (const w of wl) parts.push(w); parts.join(' ')"), - "hello world"); - } - - // Test iterable with empty container - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ nl_class(isolate); - nl_class - .ctor<>() - .iterable(&NumberList::begin, &NumberList::end); - - context.class_("NumberList", nl_class); - - auto* nl = new NumberList{{}}; - auto nl_obj = v8pp::class_::import_external(isolate, nl); - v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "empty_nl", nl_obj); - - check_eq("empty iterable", - run_script(context, - "let count = 0; for (const n of empty_nl) count++; count"), - 0); - } - - // Test iterable with lambda begin/end - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ nl_class(isolate); - nl_class - .ctor<>() - .iterable( - [](NumberList const& nl) { return nl.numbers.begin(); }, - [](NumberList const& nl) { return nl.numbers.end(); }); - - context.class_("NumberList", nl_class); - - auto* nl = new NumberList{{10, 20, 30}}; - auto nl_obj = v8pp::class_::import_external(isolate, nl); - v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "nl2", nl_obj); - - check_eq("lambda iterable", - run_script(context, - "let s = 0; for (const n of nl2) s += n; s"), - 60); - } - - // Test combined: to_string_tag + iterable - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ nl_class(isolate); - nl_class - .ctor<>() - .to_string_tag("NumberList") - .iterable(&NumberList::begin, &NumberList::end); - - context.class_("NumberList", nl_class); - - auto* nl = new NumberList{{1, 2}}; - auto nl_obj = v8pp::class_::import_external(isolate, nl); - v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "nl3", nl_obj); - - check_eq("tag + iterable tag", - run_script(context, - "Object.prototype.toString.call(nl3)"), - "[object NumberList]"); - - check_eq("tag + iterable spread", - run_script(context, - "[...nl3].reduce((a, b) => a + b, 0)"), - 3); - } -} +#include "v8pp/class.hpp" +#include "v8pp/module.hpp" + +#include "test.hpp" + +namespace { + +struct Widget +{ + int value = 42; +}; + +struct NumericValue +{ + double val; + explicit NumericValue(double v) + : val(v) {} + double to_number(std::string_view) const { return val; } +}; + +struct Tag +{ + std::string name; + explicit Tag(std::string n) + : name(std::move(n)) {} +}; + +struct NumberList +{ + std::vector numbers; + + auto begin() const { return numbers.begin(); } + auto end() const { return numbers.end(); } +}; + +struct WordList +{ + std::vector words; + + auto begin() const { return words.begin(); } + auto end() const { return words.end(); } +}; + +} // namespace + +void test_symbol() +{ + // Test to_string_tag + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ widget_class(isolate); + widget_class + .ctor<>() + .var("value", &Widget::value) + .to_string_tag("Widget"); + + context.class_("Widget", widget_class); + + check_eq("to_string_tag", + run_script(context, + "let w = new Widget(); Object.prototype.toString.call(w)"), + "[object Widget]"); + } + + // Test to_primitive with member function + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ nv_class(isolate); + nv_class + .ctor() + .to_primitive(&NumericValue::to_number); + + context.class_("NumericValue", nv_class); + + check_eq("to_primitive +", + run_script(context, "let nv = new NumericValue(10); nv + 5"), + 15.0); + + check_eq("to_primitive *", + run_script(context, "nv * 3"), + 30.0); + } + + // Test to_primitive with lambda + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ tag_class(isolate); + tag_class + .ctor() + .var("name", &Tag::name) + .to_primitive([](Tag const& t, std::string_view) -> std::string + { return t.name; }); + + context.class_("Tag", tag_class); + + check_eq("to_primitive string concat", + run_script(context, + "let t = new Tag('hello'); '' + t"), + "hello"); + } + + // Test iterable with int vector (member begin/end) + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ nl_class(isolate); + nl_class + .ctor<>() + .iterable(&NumberList::begin, &NumberList::end); + + context.class_("NumberList", nl_class); + + auto* nl = new NumberList{ { 1, 2, 3, 4, 5 } }; + auto nl_obj = v8pp::class_::import_external(isolate, nl); + v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "nl", nl_obj); + + check_eq("for...of sum", + run_script(context, + "let sum = 0; for (const n of nl) sum += n; sum"), + 15); + + check_eq("spread to array", + run_script(context, + "JSON.stringify([...nl])"), + "[1,2,3,4,5]"); + + check_eq("Array.from", + run_script(context, + "Array.from(nl).length"), + 5); + } + + // Test iterable with string vector + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ wl_class(isolate); + wl_class + .ctor<>() + .iterable(&WordList::begin, &WordList::end); + + context.class_("WordList", wl_class); + + auto* wl = new WordList{ { "hello", "world" } }; + auto wl_obj = v8pp::class_::import_external(isolate, wl); + v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "wl", wl_obj); + + check_eq("string iterable", + run_script(context, + "let parts = []; for (const w of wl) parts.push(w); parts.join(' ')"), + "hello world"); + } + + // Test iterable with empty container + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ nl_class(isolate); + nl_class + .ctor<>() + .iterable(&NumberList::begin, &NumberList::end); + + context.class_("NumberList", nl_class); + + auto* nl = new NumberList{ {} }; + auto nl_obj = v8pp::class_::import_external(isolate, nl); + v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "empty_nl", nl_obj); + + check_eq("empty iterable", + run_script(context, + "let count = 0; for (const n of empty_nl) count++; count"), + 0); + } + + // Test iterable with lambda begin/end + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ nl_class(isolate); + nl_class + .ctor<>() + .iterable( + [](NumberList const& nl) + { return nl.numbers.begin(); }, + [](NumberList const& nl) + { return nl.numbers.end(); }); + + context.class_("NumberList", nl_class); + + auto* nl = new NumberList{ { 10, 20, 30 } }; + auto nl_obj = v8pp::class_::import_external(isolate, nl); + v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "nl2", nl_obj); + + check_eq("lambda iterable", + run_script(context, + "let s = 0; for (const n of nl2) s += n; s"), + 60); + } + + // Test combined: to_string_tag + iterable + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ nl_class(isolate); + nl_class + .ctor<>() + .to_string_tag("NumberList") + .iterable(&NumberList::begin, &NumberList::end); + + context.class_("NumberList", nl_class); + + auto* nl = new NumberList{ { 1, 2 } }; + auto nl_obj = v8pp::class_::import_external(isolate, nl); + v8pp::set_option(isolate, context.isolate()->GetCurrentContext()->Global(), "nl3", nl_obj); + + check_eq("tag + iterable tag", + run_script(context, + "Object.prototype.toString.call(nl3)"), + "[object NumberList]"); + + check_eq("tag + iterable spread", + run_script(context, + "[...nl3].reduce((a, b) => a + b, 0)"), + 3); + } +} diff --git a/test/test_thread_safety.cpp b/test/test_thread_safety.cpp index 5c70ed0..8e473bb 100644 --- a/test/test_thread_safety.cpp +++ b/test/test_thread_safety.cpp @@ -1,327 +1,336 @@ -#include "v8pp/class.hpp" -#include "v8pp/context.hpp" - -#include "test.hpp" - -#include -#include -#include -#include -#include -#include - -namespace { - -struct ThreadObj -{ - static std::atomic total_created; - static std::atomic total_destroyed; - int value; - - explicit ThreadObj(int v = 0) : value(v) { ++total_created; } - ~ThreadObj() { ++total_destroyed; } - - int get() const { return value; } - int add(int x) const { return value + x; } -}; - -std::atomic ThreadObj::total_created = 0; -std::atomic ThreadObj::total_destroyed = 0; - -struct SharedObj -{ - std::atomic access_count{0}; - int value; - - explicit SharedObj(int v = 0) : value(v) {} - - int get() - { - ++access_count; - return value; - } -}; - -void force_gc(v8::Isolate* isolate) -{ - isolate->RequestGarbageCollectionForTesting( - v8::Isolate::GarbageCollectionType::kFullGarbageCollection); - isolate->RequestGarbageCollectionForTesting( - v8::Isolate::GarbageCollectionType::kFullGarbageCollection); -} - -// -// Test: multiple threads, each with own isolate and context -// - -void test_concurrent_isolates() -{ - ThreadObj::total_created = 0; - ThreadObj::total_destroyed = 0; - - constexpr int num_threads = 4; - constexpr int objects_per_thread = 1000; - std::atomic errors{0}; - - auto worker = [&errors](int thread_id) - { - try - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ obj_class(isolate); - obj_class - .ctor() - .function("get", &ThreadObj::get) - .function("add", &ThreadObj::add); - - context.class_("ThreadObj", obj_class); - - // Create objects in batches - for (int batch = 0; batch < objects_per_thread / 100; ++batch) - { - v8::HandleScope batch_scope(isolate); - std::string script = - "for (var i = 0; i < 100; i++) {" - " var o = new ThreadObj(" + std::to_string(thread_id * 1000 + batch * 100) + " + i);" - " o.add(1);" - "} 0"; - run_script(context, script.c_str()); - } - - force_gc(isolate); - } - catch (std::exception const& ex) - { - ++errors; - std::cerr << "Thread " << thread_id << " error: " << ex.what() << '\n'; - } - }; - - std::vector threads; - threads.reserve(num_threads); - for (int i = 0; i < num_threads; ++i) - { - threads.emplace_back(worker, i); - } - - for (auto& t : threads) - { - t.join(); - } - - check_eq("concurrent isolates no errors", errors.load(), 0); - check_eq("concurrent isolates all cleaned up", - ThreadObj::total_created.load(), ThreadObj::total_destroyed.load()); -} - -// -// Test: cross-isolate shared_ptr (sequential) -// -// Wrap the same shared_ptr object in two different isolates sequentially. -// - -void test_cross_isolate_shared_ptr_sequential() -{ - auto shared = std::make_shared(42); - check_eq("initial use_count", shared.use_count(), 1L); - - // Isolate A - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ obj_class(isolate); - obj_class - .function("get", &SharedObj::get); - - context.class_("SharedObj", obj_class); - - v8::Local js_obj = - v8pp::class_::reference_external(isolate, shared); - check("isolate A wrap", !js_obj.IsEmpty()); - - // Use the object from JS - v8pp::set_option(isolate, context.global(), "obj", js_obj); - check_eq("isolate A get", run_script(context, "obj.get()"), 42); - - v8pp::class_::unreference_external(isolate, shared); - } - - // Object should still be alive (main thread holds shared_ptr) - check_eq("shared survives isolate A", shared->value, 42); - - // Isolate B - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ obj_class(isolate); - obj_class - .function("get", &SharedObj::get); - - context.class_("SharedObj", obj_class); - - v8::Local js_obj = - v8pp::class_::reference_external(isolate, shared); - check("isolate B wrap", !js_obj.IsEmpty()); - - v8pp::set_option(isolate, context.global(), "obj", js_obj); - check_eq("isolate B get", run_script(context, "obj.get()"), 42); - - v8pp::class_::unreference_external(isolate, shared); - } - - // Only main thread's copy should remain - check_eq("shared survives both isolates", shared->value, 42); - check_eq("final use_count", shared.use_count(), 1L); - check_eq("total access count", shared->access_count.load(), 2); -} - -// -// Test: cross-isolate shared_ptr (concurrent) -// -// Multiple threads each wrap the same shared_ptr in their own isolate. -// - -void test_cross_isolate_shared_ptr_concurrent() -{ - auto shared = std::make_shared(99); - - constexpr int num_threads = 4; - std::atomic errors{0}; - - auto worker = [&shared, &errors](int) - { - try - { - // Each thread gets its own copy of the shared_ptr - auto local_copy = shared; - - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ obj_class(isolate); - obj_class - .function("get", &SharedObj::get); - - context.class_("SharedObj", obj_class); - - v8::Local js_obj = - v8pp::class_::reference_external(isolate, local_copy); - - v8pp::set_option(isolate, context.global(), "obj", js_obj); - - int result = run_script(context, "obj.get()"); - if (result != 99) - { - ++errors; - } - - v8pp::class_::unreference_external(isolate, local_copy); - } - catch (std::exception const& ex) - { - ++errors; - std::cerr << "Thread error: " << ex.what() << '\n'; - } - }; - - std::vector threads; - threads.reserve(num_threads); - for (int i = 0; i < num_threads; ++i) - { - threads.emplace_back(worker, i); - } - - for (auto& t : threads) - { - t.join(); - } - - check_eq("concurrent shared_ptr no errors", errors.load(), 0); - check_eq("concurrent shared_ptr use_count", shared.use_count(), 1L); - check("concurrent shared_ptr accessed", shared->access_count.load() >= num_threads); -} - -// -// Test: isolate independence -// -// Two threads register classes with the same name but different types. -// Each should only see its own registration. -// - -struct IsoTypeA -{ - int get() const { return 111; } -}; - -struct IsoTypeB -{ - int get() const { return 222; } -}; - -void test_isolate_independence() -{ - std::atomic errors{0}; - std::atomic result_a{0}; - std::atomic result_b{0}; - - auto worker_a = [&]() - { - try - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ cls(isolate); - cls.ctor<>().function("get", &IsoTypeA::get); - context.class_("MyClass", cls); - - result_a = run_script(context, "var x = new MyClass(); x.get()"); - } - catch (std::exception const&) { ++errors; } - }; - - auto worker_b = [&]() - { - try - { - v8pp::context context; - v8::Isolate* isolate = context.isolate(); - v8::HandleScope scope(isolate); - - v8pp::class_ cls(isolate); - cls.ctor<>().function("get", &IsoTypeB::get); - context.class_("MyClass", cls); - - result_b = run_script(context, "var x = new MyClass(); x.get()"); - } - catch (std::exception const&) { ++errors; } - }; - - std::thread ta(worker_a); - std::thread tb(worker_b); - ta.join(); - tb.join(); - - check_eq("isolate independence no errors", errors.load(), 0); - check_eq("isolate A sees its own type", result_a.load(), 111); - check_eq("isolate B sees its own type", result_b.load(), 222); -} - -} // anonymous namespace - -void test_thread_safety() -{ - test_concurrent_isolates(); - test_cross_isolate_shared_ptr_sequential(); - test_cross_isolate_shared_ptr_concurrent(); - test_isolate_independence(); -} +#include "v8pp/class.hpp" +#include "v8pp/context.hpp" + +#include "test.hpp" + +#include +#include +#include +#include +#include +#include + +namespace { + +struct ThreadObj +{ + static std::atomic total_created; + static std::atomic total_destroyed; + int value; + + explicit ThreadObj(int v = 0) + : value(v) { ++total_created; } + ~ThreadObj() { ++total_destroyed; } + + int get() const { return value; } + int add(int x) const { return value + x; } +}; + +std::atomic ThreadObj::total_created = 0; +std::atomic ThreadObj::total_destroyed = 0; + +struct SharedObj +{ + std::atomic access_count{ 0 }; + int value; + + explicit SharedObj(int v = 0) + : value(v) {} + + int get() + { + ++access_count; + return value; + } +}; + +void force_gc(v8::Isolate* isolate) +{ + isolate->RequestGarbageCollectionForTesting( + v8::Isolate::GarbageCollectionType::kFullGarbageCollection); + isolate->RequestGarbageCollectionForTesting( + v8::Isolate::GarbageCollectionType::kFullGarbageCollection); +} + +// +// Test: multiple threads, each with own isolate and context +// + +void test_concurrent_isolates() +{ + ThreadObj::total_created = 0; + ThreadObj::total_destroyed = 0; + + constexpr int num_threads = 4; + constexpr int objects_per_thread = 1000; + std::atomic errors{ 0 }; + + auto worker = [&errors](int thread_id) + { + try + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ obj_class(isolate); + obj_class + .ctor() + .function("get", &ThreadObj::get) + .function("add", &ThreadObj::add); + + context.class_("ThreadObj", obj_class); + + // Create objects in batches + for (int batch = 0; batch < objects_per_thread / 100; ++batch) + { + v8::HandleScope batch_scope(isolate); + std::string script = + "for (var i = 0; i < 100; i++) {" + " var o = new ThreadObj(" + + std::to_string(thread_id * 1000 + batch * 100) + " + i);" + " o.add(1);" + "} 0"; + run_script(context, script.c_str()); + } + + force_gc(isolate); + } + catch (std::exception const& ex) + { + ++errors; + std::cerr << "Thread " << thread_id << " error: " << ex.what() << '\n'; + } + }; + + std::vector threads; + threads.reserve(num_threads); + for (int i = 0; i < num_threads; ++i) + { + threads.emplace_back(worker, i); + } + + for (auto& t : threads) + { + t.join(); + } + + check_eq("concurrent isolates no errors", errors.load(), 0); + check_eq("concurrent isolates all cleaned up", + ThreadObj::total_created.load(), ThreadObj::total_destroyed.load()); +} + +// +// Test: cross-isolate shared_ptr (sequential) +// +// Wrap the same shared_ptr object in two different isolates sequentially. +// + +void test_cross_isolate_shared_ptr_sequential() +{ + auto shared = std::make_shared(42); + check_eq("initial use_count", shared.use_count(), 1L); + + // Isolate A + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ obj_class(isolate); + obj_class + .function("get", &SharedObj::get); + + context.class_("SharedObj", obj_class); + + v8::Local js_obj = + v8pp::class_::reference_external(isolate, shared); + check("isolate A wrap", !js_obj.IsEmpty()); + + // Use the object from JS + v8pp::set_option(isolate, context.global(), "obj", js_obj); + check_eq("isolate A get", run_script(context, "obj.get()"), 42); + + v8pp::class_::unreference_external(isolate, shared); + } + + // Object should still be alive (main thread holds shared_ptr) + check_eq("shared survives isolate A", shared->value, 42); + + // Isolate B + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ obj_class(isolate); + obj_class + .function("get", &SharedObj::get); + + context.class_("SharedObj", obj_class); + + v8::Local js_obj = + v8pp::class_::reference_external(isolate, shared); + check("isolate B wrap", !js_obj.IsEmpty()); + + v8pp::set_option(isolate, context.global(), "obj", js_obj); + check_eq("isolate B get", run_script(context, "obj.get()"), 42); + + v8pp::class_::unreference_external(isolate, shared); + } + + // Only main thread's copy should remain + check_eq("shared survives both isolates", shared->value, 42); + check_eq("final use_count", shared.use_count(), 1L); + check_eq("total access count", shared->access_count.load(), 2); +} + +// +// Test: cross-isolate shared_ptr (concurrent) +// +// Multiple threads each wrap the same shared_ptr in their own isolate. +// + +void test_cross_isolate_shared_ptr_concurrent() +{ + auto shared = std::make_shared(99); + + constexpr int num_threads = 4; + std::atomic errors{ 0 }; + + auto worker = [&shared, &errors](int) + { + try + { + // Each thread gets its own copy of the shared_ptr + auto local_copy = shared; + + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ obj_class(isolate); + obj_class + .function("get", &SharedObj::get); + + context.class_("SharedObj", obj_class); + + v8::Local js_obj = + v8pp::class_::reference_external(isolate, local_copy); + + v8pp::set_option(isolate, context.global(), "obj", js_obj); + + int result = run_script(context, "obj.get()"); + if (result != 99) + { + ++errors; + } + + v8pp::class_::unreference_external(isolate, local_copy); + } + catch (std::exception const& ex) + { + ++errors; + std::cerr << "Thread error: " << ex.what() << '\n'; + } + }; + + std::vector threads; + threads.reserve(num_threads); + for (int i = 0; i < num_threads; ++i) + { + threads.emplace_back(worker, i); + } + + for (auto& t : threads) + { + t.join(); + } + + check_eq("concurrent shared_ptr no errors", errors.load(), 0); + check_eq("concurrent shared_ptr use_count", shared.use_count(), 1L); + check("concurrent shared_ptr accessed", shared->access_count.load() >= num_threads); +} + +// +// Test: isolate independence +// +// Two threads register classes with the same name but different types. +// Each should only see its own registration. +// + +struct IsoTypeA +{ + int get() const { return 111; } +}; + +struct IsoTypeB +{ + int get() const { return 222; } +}; + +void test_isolate_independence() +{ + std::atomic errors{ 0 }; + std::atomic result_a{ 0 }; + std::atomic result_b{ 0 }; + + auto worker_a = [&]() + { + try + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ cls(isolate); + cls.ctor<>().function("get", &IsoTypeA::get); + context.class_("MyClass", cls); + + result_a = run_script(context, "var x = new MyClass(); x.get()"); + } + catch (std::exception const&) + { + ++errors; + } + }; + + auto worker_b = [&]() + { + try + { + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + v8pp::class_ cls(isolate); + cls.ctor<>().function("get", &IsoTypeB::get); + context.class_("MyClass", cls); + + result_b = run_script(context, "var x = new MyClass(); x.get()"); + } + catch (std::exception const&) + { + ++errors; + } + }; + + std::thread ta(worker_a); + std::thread tb(worker_b); + ta.join(); + tb.join(); + + check_eq("isolate independence no errors", errors.load(), 0); + check_eq("isolate A sees its own type", result_a.load(), 111); + check_eq("isolate B sees its own type", result_b.load(), 222); +} + +} // anonymous namespace + +void test_thread_safety() +{ + test_concurrent_isolates(); + test_cross_isolate_shared_ptr_sequential(); + test_cross_isolate_shared_ptr_concurrent(); + test_isolate_independence(); +} diff --git a/test/test_type_info.cpp b/test/test_type_info.cpp index 07c9c5b..b9ce239 100644 --- a/test/test_type_info.cpp +++ b/test/test_type_info.cpp @@ -1,9 +1,17 @@ #include "v8pp/type_info.hpp" #include "test.hpp" -struct some_struct {}; -namespace test { class some_class {}; } -namespace { using other_class = test::some_class; } +struct some_struct +{ +}; +namespace test { +class some_class +{ +}; +} // namespace test +namespace { +using other_class = test::some_class; +} void test_type_info() { diff --git a/test/test_utility.cpp b/test/test_utility.cpp index b409cce..42bc524 100644 --- a/test/test_utility.cpp +++ b/test/test_utility.cpp @@ -34,8 +34,13 @@ void test_args_derived(F&& f) test_args(std::forward(f)); } -void x() {} -int y(int, float) { return 1; } +void x() +{ +} +int y(int, float) +{ + return 1; +} void test_function_traits() { @@ -45,11 +50,14 @@ void test_function_traits() test_ret(y); test_args>(y); - std::function z; + std::function z; test_ret(z); test_args>(z); - auto lambda = [](int, bool) -> char { return 'z'; }; + auto lambda = [](int, bool) -> char + { + return 'z'; + }; test_ret(lambda); test_args>(lambda); @@ -139,9 +147,18 @@ void test_tuple_tail() static_assert(std::same_as>::type, std::tuple>); } -int f() { return 1; } -int g(int x) { return x; } -int h(int x, bool) { return x; } +int f() +{ + return 1; +} +int g(int x) +{ + return x; +} +int h(int x, bool) +{ + return x; +} struct Y { @@ -161,7 +178,8 @@ void test_is_callable() static_assert(is_callable::value, "g is callable"); static_assert(is_callable::value, "h is callable"); - auto lambda = [](){}; + auto lambda = []() { + }; static_assert(is_callable::value, "lambda is callable"); static_assert(is_callable::value, "Z is callable"); diff --git a/v8pp/call_from_v8.hpp b/v8pp/call_from_v8.hpp index 182682d..bf50fb7 100644 --- a/v8pp/call_from_v8.hpp +++ b/v8pp/call_from_v8.hpp @@ -17,7 +17,8 @@ template struct defaults { std::tuple values; - explicit constexpr defaults(Defs... args) : values(std::move(args)...) {} + explicit constexpr defaults(Defs... args) + : values(std::move(args)...) {} }; template @@ -25,10 +26,14 @@ defaults(Defs...) -> defaults; /// Type trait to detect defaults<...> template -struct is_defaults : std::false_type {}; +struct is_defaults : std::false_type +{ +}; template -struct is_defaults> : std::true_type {}; +struct is_defaults> : std::true_type +{ +}; } // namespace v8pp @@ -66,7 +71,8 @@ struct call_from_v8_traits }; template - using arg_type = typename tuple_element < Index + is_mem_fun, Index<(arg_count + offset)>::type; + using arg_type = typename tuple_element < Index + is_mem_fun, + Index<(arg_count + offset)>::type; template, @@ -104,7 +110,7 @@ decltype(auto) call_from_v8_impl(F&& func, v8::FunctionCallbackInfo c } template - requires (sizeof...(ObjArg) == 0 || (!v8pp::is_defaults>::value && ...)) +requires(sizeof...(ObjArg) == 0 || (!v8pp::is_defaults>::value && ...)) decltype(auto) call_from_v8(F&& func, v8::FunctionCallbackInfo const& args, ObjArg&... obj) { constexpr bool with_isolate = is_first_arg_isolate; @@ -166,7 +172,7 @@ decltype(auto) arg_or_default(v8::FunctionCallbackInfo const& args, using arg_type = typename CallTraits::template arg_type; using value_type = std::remove_cv_t>; - if (static_cast(args.Length()) <= Index) + if (static_cast(args.Length()) <= Index || args[Index]->IsUndefined()) { constexpr size_t def_index = Index - DefaultsStart; return static_cast(std::get(defaults_tuple)); diff --git a/v8pp/call_v8.hpp b/v8pp/call_v8.hpp index df91e22..82c26f4 100644 --- a/v8pp/call_v8.hpp +++ b/v8pp/call_v8.hpp @@ -19,8 +19,7 @@ v8::Local call_v8(v8::Isolate* isolate, v8::Local func, int const arg_count = sizeof...(Args); // +1 to allocate array for arg_count == 0 - v8::Local v8_args[arg_count + 1] = - { + v8::Local v8_args[arg_count + 1] = { to_v8(isolate, std::forward(args))... }; diff --git a/v8pp/class.cpp b/v8pp/class.cpp index 711438c..4e4b0d0 100644 --- a/v8pp/class.cpp +++ b/v8pp/class.cpp @@ -5,32 +5,24 @@ namespace v8pp::detail { -template -class object_registry; +template class object_registry; -template -class object_registry; +template class object_registry; -template -object_registry& classes::add(v8::Isolate* isolate, +template object_registry& classes::add(v8::Isolate* isolate, type_info const& type, object_registry::dtor_function&& dtor); -template -void classes::remove(v8::Isolate* isolate, type_info const& type); +template void classes::remove(v8::Isolate* isolate, type_info const& type); -template -object_registry& classes::find(v8::Isolate* isolate, +template object_registry& classes::find(v8::Isolate* isolate, type_info const& type); -template -object_registry& classes::add(v8::Isolate* isolate, +template object_registry& classes::add(v8::Isolate* isolate, type_info const& type, object_registry::dtor_function&& dtor); -template -void classes::remove(v8::Isolate* isolate, type_info const& type); +template void classes::remove(v8::Isolate* isolate, type_info const& type); -template -object_registry& classes::find(v8::Isolate* isolate, +template object_registry& classes::find(v8::Isolate* isolate, type_info const& type); } // namespace v8pp::detail diff --git a/v8pp/class.hpp b/v8pp/class.hpp index 5285d4d..7b0d162 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -48,8 +48,8 @@ class object_registry final : public class_info using const_pointer_type = typename Traits::const_pointer_type; using object_id = typename Traits::object_id; - using ctor_function = std::function (v8::FunctionCallbackInfo const& args)>; - using dtor_function = std::function; + using ctor_function = std::function(v8::FunctionCallbackInfo const& args)>; + using dtor_function = std::function; using cast_function = pointer_type (*)(pointer_type const&); object_registry(v8::Isolate* isolate, type_info const& type, dtor_function&& dtor); @@ -259,7 +259,7 @@ struct iterator_factory iter_obj->Set(context, v8pp::to_v8_name(isolate, "next"), next_fn).FromJust(); // weak ref to clean up state when iterator object is GC'd - auto* weak_data = new weak_ref_data{iter_state, v8::Global(isolate, iter_obj)}; + auto* weak_data = new weak_ref_data{ iter_state, v8::Global(isolate, iter_obj) }; weak_data->handle.SetWeak(weak_data, weak_callback, v8::WeakCallbackType::kParameter); args.GetReturnValue().Set(iter_obj); @@ -286,20 +286,24 @@ struct iterator_factory if (iter_state->current == iter_state->end) { result->Set(context, - v8pp::to_v8_name(isolate, "value"), - v8::Undefined(isolate)).FromJust(); + v8pp::to_v8_name(isolate, "value"), + v8::Undefined(isolate)) + .FromJust(); result->Set(context, - v8pp::to_v8_name(isolate, "done"), - v8::Boolean::New(isolate, true)).FromJust(); + v8pp::to_v8_name(isolate, "done"), + v8::Boolean::New(isolate, true)) + .FromJust(); } else { result->Set(context, - v8pp::to_v8_name(isolate, "value"), - v8pp::to_v8(isolate, *iter_state->current)).FromJust(); + v8pp::to_v8_name(isolate, "value"), + v8pp::to_v8(isolate, *iter_state->current)) + .FromJust(); result->Set(context, - v8pp::to_v8_name(isolate, "done"), - v8::Boolean::New(isolate, false)).FromJust(); + v8pp::to_v8_name(isolate, "done"), + v8::Boolean::New(isolate, false)) + .FromJust(); ++iter_state->current; } @@ -449,10 +453,9 @@ class class_ class_& ctor(ctor_function create = &Create::call) { class_info_.set_ctor([create = std::move(create)](v8::FunctionCallbackInfo const& args) - { + { auto object = create(args); - return std::make_pair(object, Traits::object_size(object)); - }); + return std::make_pair(object, Traits::object_size(object)); }); return *this; } @@ -461,10 +464,97 @@ class class_ class_& ctor(v8pp::defaults defs) { class_info_.set_ctor([defs = std::move(defs)](v8::FunctionCallbackInfo const& args) - { + { auto object = detail::call_from_v8(Traits::template create, args, defs); - return std::make_pair(object, Traits::object_size(object)); - }); + return std::make_pair(object, Traits::object_size(object)); }); + return *this; + } + + /// Set class constructor from a factory function with default parameter values. + /// The factory must return object_pointer_type (T* for raw_ptr_traits, shared_ptr for shared_ptr_traits). + template + requires(detail::is_callable>::value && !v8pp::is_defaults>::value) + class_& ctor(Function&& func, v8pp::defaults defs) + { + using F = std::decay_t; + static_assert(std::is_convertible_v::return_type, object_pointer_type>, + "Constructor factory must return object_pointer_type"); + + class_info_.set_ctor( + [func = F(std::forward(func)), defs = std::move(defs)](v8::FunctionCallbackInfo const& args) + { + F f = func; // copy from captured const + auto object = detail::call_from_v8(std::move(f), args, defs); + return std::make_pair(pointer_type(object), Traits::object_size(object)); + }); + return *this; + } + + /// Set class constructor with multiple overloaded factory functions (multi-dispatch). + /// Each factory must return object_pointer_type. Dispatched by arity + type matching (first-match-wins). + /// Accepts plain callables and v8pp::with_defaults() entries. + template + requires((detail::is_callable>::value || is_overload_entry>::value) && (detail::is_callable>::value || is_overload_entry>::value) && !std::is_member_function_pointer_v> && !std::is_member_function_pointer_v>) + class_& ctor(F1&& f1, F2&& f2, Fs&&... fs) + { + using Set = detail::overload_set< + decltype(detail::make_overload_entry(std::forward(f1))), + decltype(detail::make_overload_entry(std::forward(f2))), + decltype(detail::make_overload_entry(std::forward(fs)))...>; + + class_info_.set_ctor( + [set = Set{ std::make_tuple( + detail::make_overload_entry(std::forward(f1)), + detail::make_overload_entry(std::forward(f2)), + detail::make_overload_entry(std::forward(fs))...) }](v8::FunctionCallbackInfo const& args) + { + v8::Isolate* isolate = args.GetIsolate(); + size_t const arg_count = args.Length(); + bool matched = false; + object_pointer_type result{}; + std::string errors; + + std::apply([&](auto const&... entries) + { ((matched || [&] + { + using Entry = std::decay_t; + using F = typename detail::entry_func_type::type; + + constexpr size_t min = detail::overload_arg_range::min_args; + constexpr size_t max = detail::overload_arg_range::max_args; + if (arg_count < min || arg_count > max) + return false; + + if (arg_count > 0 && !detail::overload_types_match(isolate, args, arg_count)) + return false; + + try + { + result = object_pointer_type(detail::call_ctor_entry(entries, args)); + matched = true; + return true; + } + catch (std::exception const& ex) + { + if (!errors.empty()) errors += "; "; + errors += ex.what(); + return false; + } + }()), + ...); }, set.entries); + + if (!matched) + { + std::string msg = "No matching constructor overload for " + std::to_string(arg_count) + " argument(s)"; + if (!errors.empty()) + { + msg += ". Tried: " + errors; + } + throw std::runtime_error(msg); + } + + return std::make_pair(pointer_type(result), Traits::object_size(result)); + }); return *this; } @@ -476,10 +566,8 @@ class class_ // TODO: std::is_convertible and check for duplicates in hierarchy? auto& base = detail::classes::find(isolate(), detail::type_id()); class_info_.add_base(base, [](pointer_type const& ptr) - { - return pointer_type{Traits::template static_pointer_cast( - Traits::template static_pointer_cast(ptr))}; - }); + { return pointer_type{ Traits::template static_pointer_cast( + Traits::template static_pointer_cast(ptr)) }; }); class_info_.js_function_template()->Inherit(base.class_function_template()); return *this; } @@ -496,9 +584,7 @@ class class_ template class_& function(std::string_view name, Function&& func, v8::PropertyAttribute attr = v8::None) { - constexpr auto effect = detail::is_const_member_function_v> - ? v8::SideEffectType::kHasNoSideEffect - : v8::SideEffectType::kHasSideEffect; + constexpr auto effect = detail::is_const_member_function_v> ? v8::SideEffectType::kHasNoSideEffect : v8::SideEffectType::kHasSideEffect; return function_impl(name, std::forward(func), effect, attr); } @@ -515,9 +601,7 @@ class class_ class_& function(std::string_view name, Function&& func, v8pp::defaults defs, v8::PropertyAttribute attr = v8::None) { - constexpr auto effect = detail::is_const_member_function_v> - ? v8::SideEffectType::kHasNoSideEffect - : v8::SideEffectType::kHasSideEffect; + constexpr auto effect = detail::is_const_member_function_v> ? v8::SideEffectType::kHasNoSideEffect : v8::SideEffectType::kHasSideEffect; return function_impl(name, std::forward(func), std::move(defs), effect, attr); } @@ -527,9 +611,7 @@ class class_ { using F = typename fast_function::func_type; constexpr bool is_mem_fun = std::is_member_function_pointer_v; - constexpr auto side_effect = detail::is_const_member_function_v - ? v8::SideEffectType::kHasNoSideEffect - : v8::SideEffectType::kHasSideEffect; + constexpr auto side_effect = detail::is_const_member_function_v ? v8::SideEffectType::kHasNoSideEffect : v8::SideEffectType::kHasSideEffect; v8::HandleScope scope(isolate()); @@ -548,9 +630,7 @@ class class_ /// Set multiple overloaded member/static functions template - requires (detail::is_callable>::value - || std::is_member_function_pointer_v> - || is_overload_entry>::value) + requires(detail::is_callable>::value || std::is_member_function_pointer_v> || is_overload_entry>::value) class_& function(std::string_view name, F1&& f1, F2&& f2, Fs&&... fs) { v8::HandleScope scope(isolate()); @@ -584,24 +664,25 @@ class class_ #if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) // SetAccessor removed from ObjectTemplate in V8 12.9+ class_info_.js_function_template() - ->InstanceTemplate() - ->SetNativeDataProperty(v8_name, getter, setter, data, - v8::PropertyAttribute(v8::DontDelete), - v8::SideEffectType::kHasNoSideEffect, - v8::SideEffectType::kHasSideEffectToReceiver); + ->InstanceTemplate() + ->SetNativeDataProperty(v8_name, getter, setter, data, + v8::PropertyAttribute(v8::DontDelete), + v8::SideEffectType::kHasNoSideEffect, + v8::SideEffectType::kHasSideEffectToReceiver); #else class_info_.js_function_template() - ->PrototypeTemplate() - ->SetAccessor(v8_name, getter, setter, data, - v8::DEFAULT, v8::PropertyAttribute(v8::DontDelete), - v8::SideEffectType::kHasNoSideEffect, - v8::SideEffectType::kHasSideEffectToReceiver); + ->PrototypeTemplate() + ->SetAccessor(v8_name, getter, setter, data, + v8::DEFAULT, v8::PropertyAttribute(v8::DontDelete), + v8::SideEffectType::kHasNoSideEffect, + v8::SideEffectType::kHasSideEffectToReceiver); #endif return *this; } /// Set read/write class property with getter and setter template + requires(!is_fast_function>::value) class_& property(std::string_view name, GetFunction&& get, SetFunction&& set = {}) { using Getter = typename std::conditional_t, @@ -612,11 +693,8 @@ class class_ typename detail::function_traits::template pointer_type, typename std::decay_t>; - static_assert(std::is_member_function_pointer_v - || detail::is_callable::value, "GetFunction must be callable"); - static_assert(std::is_member_function_pointer_v - || detail::is_callable::value - || std::same_as, "SetFunction must be callable"); + static_assert(std::is_member_function_pointer_v || detail::is_callable::value, "GetFunction must be callable"); + static_assert(std::is_member_function_pointer_v || detail::is_callable::value || std::same_as, "SetFunction must be callable"); using GetClass = std::conditional_t, T, detail::none>; using SetClass = std::conditional_t, T, detail::none>; @@ -630,9 +708,7 @@ class class_ v8::Local v8_name = v8pp::to_v8_name(isolate(), name); v8::Local data = detail::external_data::set(isolate(), property_type(std::move(get), std::move(set))); - v8::SideEffectType setter_effect = property_type::is_readonly - ? v8::SideEffectType::kHasSideEffect - : v8::SideEffectType::kHasSideEffectToReceiver; + v8::SideEffectType setter_effect = property_type::is_readonly ? v8::SideEffectType::kHasSideEffect : v8::SideEffectType::kHasSideEffectToReceiver; #if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) // SetAccessor removed from ObjectTemplate in V8 12.9+ class_info_.js_function_template()->InstanceTemplate()->SetNativeDataProperty(v8_name, getter, setter, data, @@ -646,6 +722,46 @@ class class_ return *this; } + /// Set read-only class property with V8 Fast API getter + template + class_& property(std::string_view name, fast_function) + { + v8::HandleScope scope(isolate()); + + v8::Local v8_name = v8pp::to_v8_name(isolate(), name); + auto getter_template = wrap_function_template( + isolate(), fast_function{}, + v8::SideEffectType::kHasNoSideEffect); + + class_info_.js_function_template()->InstanceTemplate()->SetAccessorProperty( + v8_name, getter_template, + v8::Local(), + v8::PropertyAttribute(v8::ReadOnly | v8::DontDelete)); + + return *this; + } + + /// Set read/write class property with V8 Fast API getter and setter + template + class_& property(std::string_view name, fast_function, fast_function) + { + v8::HandleScope scope(isolate()); + + v8::Local v8_name = v8pp::to_v8_name(isolate(), name); + auto getter_template = wrap_function_template( + isolate(), fast_function{}, + v8::SideEffectType::kHasNoSideEffect); + auto setter_template = wrap_function_template( + isolate(), fast_function{}, + v8::SideEffectType::kHasSideEffectToReceiver); + + class_info_.js_function_template()->InstanceTemplate()->SetAccessorProperty( + v8_name, getter_template, setter_template, + v8::PropertyAttribute(v8::DontDelete)); + + return *this; + } + /// Set value as a read-only constant, once by the class instance template class_& const_property(std::string_view name, GetFunction&& get) @@ -663,8 +779,7 @@ class class_ class_info_.const_properties.emplace(name, [get = std::move(get)](v8::Isolate* isolate, pointer_type obj) { auto typed_obj = Traits::template static_pointer_cast(obj); - return to_v8(isolate, ((*typed_obj).*get)()); - }); + return to_v8(isolate, ((*typed_obj).*get)()); }); return *this; } @@ -717,7 +832,7 @@ class class_ { using Invoker = detail::to_primitive_invoker>; v8::Local data = detail::external_data::set(iso, - Invoker{std::decay_t(std::forward(func))}); + Invoker{ std::decay_t(std::forward(func)) }); wrapped_fun = v8::FunctionTemplate::New(iso, &Invoker::callback, data, v8::Local(), 0, v8::ConstructorBehavior::kThrow, v8::SideEffectType::kHasNoSideEffect); @@ -741,8 +856,8 @@ class class_ std::decay_t, std::decay_t>; v8::Local data = detail::external_data::set(iso, - Factory{std::decay_t(std::forward(begin_fn)), - std::decay_t(std::forward(end_fn))}); + Factory{ std::decay_t(std::forward(begin_fn)), + std::decay_t(std::forward(end_fn)) }); v8::Local iter_tmpl = v8::FunctionTemplate::New(iso, &Factory::iterator_callback, data); @@ -761,9 +876,9 @@ class class_ v8::HandleScope scope(iso); v8::Local context = iso->GetCurrentContext(); - class_info_.js_function_template()->GetFunction(context).ToLocalChecked() - ->DefineOwnProperty(context, v8pp::to_v8_name(iso, name), to_v8(iso, value), - v8::PropertyAttribute(v8::DontDelete | (readonly ? v8::ReadOnly : 0))).FromJust(); + class_info_.js_function_template()->GetFunction(context).ToLocalChecked()->DefineOwnProperty(context, v8pp::to_v8_name(iso, name), to_v8(iso, value), + v8::PropertyAttribute(v8::DontDelete | (readonly ? v8::ReadOnly : 0))) + .FromJust(); return *this; } @@ -975,7 +1090,7 @@ class class_ { isolate->ThrowException(throw_ex(isolate, ex.what())); } - //TODO: info.GetReturnValue().Set(false); + // TODO: info.GetReturnValue().Set(false); } } }; diff --git a/v8pp/class.ipp b/v8pp/class.ipp index 35ffa95..acd256d 100644 --- a/v8pp/class.ipp +++ b/v8pp/class.ipp @@ -48,8 +48,7 @@ V8PP_IMPL object_registry::object_registry(v8::Isolate* isolate, type_in v8::HandleScope scope(isolate_); v8::Local func = v8::FunctionTemplate::New(isolate_); - v8::Local js_func = v8::FunctionTemplate::New(isolate_, - [](v8::FunctionCallbackInfo const& args) + v8::Local js_func = v8::FunctionTemplate::New(isolate_, [](v8::FunctionCallbackInfo const& args) { v8::Isolate* isolate = args.GetIsolate(); if (!args.IsConstructCall()) @@ -78,8 +77,7 @@ V8PP_IMPL object_registry::object_registry(v8::Isolate* isolate, type_in catch (std::exception const& ex) { args.GetReturnValue().Set(throw_ex(isolate, ex.what())); - } - }, external_data::set(isolate, this)); + } }, external_data::set(isolate, this)); func_.Reset(isolate, func); js_func_.Reset(isolate, js_func); @@ -102,12 +100,12 @@ template V8PP_IMPL void object_registry::add_base(object_registry& info, cast_function cast) { auto it = std::find_if(bases_.begin(), bases_.end(), - [&info](base_class_info const& base) { return &base.info == &info; }); + [&info](base_class_info const& base) + { return &base.info == &info; }); if (it != bases_.end()) { - //assert(false && "duplicated inheritance"); - throw std::runtime_error(class_name() - + " is already inherited from " + info.class_name()); + // assert(false && "duplicated inheritance"); + throw std::runtime_error(class_name() + " is already inherited from " + info.class_name()); } bases_.emplace_back(info, cast); info.derivatives_.emplace_back(this); @@ -208,9 +206,8 @@ V8PP_IMPL v8::Local object_registry::wrap_this(v8::Local object_registry::wrap_this(v8::Localis_valid()) { this_->remove_object(object); - } - }, v8::WeakCallbackType::kInternalFields); + } }, v8::WeakCallbackType::kInternalFields); objects_.emplace(object, wrapped_object{ std::move(pobj), size }); apply_const_properties(isolate_, obj, object); if (size) @@ -249,9 +245,8 @@ V8PP_IMPL v8::Local object_registry::wrap_object(pointer_typ auto it = objects_.find(object); if (it != objects_.end()) { - //assert(false && "duplicate object"); - throw std::runtime_error(class_name() - + " duplicate object " + pointer_str(Traits::pointer_id(object))); + // assert(false && "duplicate object"); + throw std::runtime_error(class_name() + " duplicate object " + pointer_str(Traits::pointer_id(object))); } v8::EscapableHandleScope scope(isolate_); @@ -259,8 +254,7 @@ V8PP_IMPL v8::Local object_registry::wrap_object(pointer_typ v8::Local context = isolate_->GetCurrentContext(); v8::Local func; v8::Local obj; - if (class_function_template()->GetFunction(context).ToLocal(&func) - && func->NewInstance(context).ToLocal(&obj)) + if (class_function_template()->GetFunction(context).ToLocal(&func) && func->NewInstance(context).ToLocal(&obj)) { obj->SetAlignedPointerInInternalField(0, Traits::pointer_id(object)); obj->SetAlignedPointerInInternalField(1, this); @@ -273,8 +267,7 @@ V8PP_IMPL v8::Local object_registry::wrap_object(pointer_typ if (this_ && this_->is_valid()) { this_->remove_object(object); - } - }, v8::WeakCallbackType::kInternalFields); + } }, v8::WeakCallbackType::kInternalFields); objects_.emplace(object, wrapped_object{ std::move(pobj), size }); apply_const_properties(isolate_, obj, object); if (size) @@ -291,11 +284,11 @@ V8PP_IMPL v8::Local object_registry::wrap_object(v8::Functio { if (!ctor_) { - //assert(false && "create not allowed"); + // assert(false && "create not allowed"); throw std::runtime_error(class_name() + " has no constructor"); } auto [object, size] = ctor_(args); - //return wrap_object(object, size); + // return wrap_object(object, size); return wrap_this(args.This(), object, size); } @@ -389,9 +382,8 @@ V8PP_IMPL object_registry& classes::add(v8::Isolate* isolate, type_info auto it = info->classes_.find(type.id()); if (it != info->classes_.end()) { - //assert(false && "class already registred"); - throw std::runtime_error(it->second->class_name() - + " is already exist in isolate " + pointer_str(isolate)); + // assert(false && "class already registred"); + throw std::runtime_error(it->second->class_name() + " is already exist in isolate " + pointer_str(isolate)); } auto registry_ptr = new object_registry(isolate, type, std::move(dtor)); info->classes_[type.id()] = std::unique_ptr(registry_ptr); @@ -410,10 +402,7 @@ V8PP_IMPL void classes::remove(v8::Isolate* isolate, type_info const& type) type_info const& traits = type_id(); if (it->second->traits != traits) { - throw std::runtime_error(it->second->class_name() - + " is already registered in isolate " - + pointer_str(isolate) + " before of " - + class_info(type, traits).class_name()); + throw std::runtime_error(it->second->class_name() + " is already registered in isolate " + pointer_str(isolate) + " before of " + class_info(type, traits).class_name()); } info->classes_.erase(it); if (info->classes_.empty()) @@ -436,17 +425,13 @@ V8PP_IMPL object_registry& classes::find(v8::Isolate* isolate, type_info { if (it->second->traits != traits) { - throw std::runtime_error(it->second->class_name() - + " is already registered in isolate " - + pointer_str(isolate) + " before of " - + class_info(type, traits).class_name()); + throw std::runtime_error(it->second->class_name() + " is already registered in isolate " + pointer_str(isolate) + " before of " + class_info(type, traits).class_name()); } return *static_cast*>(it->second.get()); } } - //assert(false && "class not registered"); - throw std::runtime_error(class_info(type, traits).class_name() - + " is not registered in isolate " + pointer_str(isolate)); + // assert(false && "class not registered"); + throw std::runtime_error(class_info(type, traits).class_name() + " is not registered in isolate " + pointer_str(isolate)); } V8PP_IMPL void classes::remove_all(v8::Isolate* isolate) diff --git a/v8pp/context.cpp b/v8pp/context.cpp index 8bff416..b1719ba 100644 --- a/v8pp/context.cpp +++ b/v8pp/context.cpp @@ -78,8 +78,7 @@ void context::load_module(v8::FunctionCallbackInfo const& args) filename = ctx->lib_path_ + path_sep + name; } std::string const suffix = V8PP_PLUGIN_SUFFIX; - if (filename.size() >= suffix.size() - && filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) != 0) + if (filename.size() >= suffix.size() && filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) != 0) { filename += suffix; } @@ -95,8 +94,7 @@ void context::load_module(v8::FunctionCallbackInfo const& args) if (!module.handle) { - throw std::runtime_error("load_module(" + name - + "): could not load shared library " + filename); + throw std::runtime_error("load_module(" + name + "): could not load shared library " + filename); } #if defined(WIN32) void* sym = ::GetProcAddress((HMODULE)module.handle, @@ -106,10 +104,7 @@ void context::load_module(v8::FunctionCallbackInfo const& args) #endif if (!sym) { - throw std::runtime_error("load_module(" + name - + "): initialization function " - V8PP_STRINGIZE(V8PP_PLUGIN_INIT_PROC_NAME) - " not found in " + filename); + throw std::runtime_error("load_module(" + name + "): initialization function " V8PP_STRINGIZE(V8PP_PLUGIN_INIT_PROC_NAME) " not found in " + filename); } using module_init_proc = v8::Local (*)(v8::Isolate*); @@ -166,8 +161,8 @@ v8::Isolate* context::create_isolate(v8::ArrayBuffer::Allocator* allocator) } context::context(v8::Isolate* isolate, v8::ArrayBuffer::Allocator* allocator, - bool add_default_global_methods, bool enter_context, - v8::Local global) + bool add_default_global_methods, bool enter_context, + v8::Local global) : own_isolate_(isolate == nullptr) , enter_context_(enter_context) , isolate_(isolate ? isolate : create_isolate(allocator)) @@ -335,7 +330,8 @@ v8::Local context::run_script(std::string_view source, std::string_vi #endif v8::Local script; bool const is_valid = v8::Script::Compile(context, - to_v8(isolate_, source), &origin).ToLocal(&script); + to_v8(isolate_, source), &origin) + .ToLocal(&script); (void)is_valid; v8::Local result; diff --git a/v8pp/context.hpp b/v8pp/context.hpp index 0dd9431..148d37f 100644 --- a/v8pp/context.hpp +++ b/v8pp/context.hpp @@ -85,7 +85,7 @@ class context /// Set functions to the context global object template - requires detail::is_callable>::value + requires detail::is_callable>::value context& function(std::string_view name, Function&& func) { return value(name, wrap_function(isolate_, name, std::forward(func))); @@ -104,19 +104,15 @@ class context { using Fun = typename std::decay_t; static_assert(detail::is_callable::value, "Function must be callable"); - return value(name, wrap_function(isolate_, name, - std::forward(func), std::move(defs))); + return value(name, wrap_function(isolate_, name, std::forward(func), std::move(defs))); } /// Set multiple overloaded functions to the context global object template - requires (detail::is_callable>::value - || std::is_member_function_pointer_v> - || is_overload_entry>::value) + requires(detail::is_callable>::value || std::is_member_function_pointer_v> || is_overload_entry>::value) context& function(std::string_view name, F1&& f1, F2&& f2, Fs&&... fs) { - return value(name, wrap_overload(isolate_, name, - std::forward(f1), std::forward(f2), std::forward(fs)...)); + return value(name, wrap_overload(isolate_, name, std::forward(f1), std::forward(f2), std::forward(fs)...)); } /// Set class to the context global object diff --git a/v8pp/context_store.cpp b/v8pp/context_store.cpp index dfbc2b7..fa1ecec 100644 --- a/v8pp/context_store.cpp +++ b/v8pp/context_store.cpp @@ -1,5 +1,5 @@ -#include "v8pp/config.hpp" - -#if !V8PP_HEADER_ONLY -#include "v8pp/context_store.ipp" -#endif +#include "v8pp/config.hpp" + +#if !V8PP_HEADER_ONLY +#include "v8pp/context_store.ipp" +#endif diff --git a/v8pp/context_store.hpp b/v8pp/context_store.hpp index d68c51b..21ebabe 100644 --- a/v8pp/context_store.hpp +++ b/v8pp/context_store.hpp @@ -1,139 +1,139 @@ -#pragma once - -#include -#include -#include - -#include - -#include "v8pp/config.hpp" -#include "v8pp/convert.hpp" -#include "v8pp/object.hpp" - -namespace v8pp { - -/// A key-value store backed by a dedicated V8 context. -/// -/// Designed to hold named V8 values that outlive ephemeral contexts -/// on the same isolate. Values are stored as live V8 object references -/// (no serialization), so wrapped C++ objects, functions, and complex -/// JS structures survive intact. -/// -/// The store creates and owns a lightweight V8 context internally. -/// All stored values live in a dedicated storage object (not the global). -/// -/// Thread safety: same as V8 — single-threaded per isolate. -/// -/// Usage: -/// v8::Isolate* isolate = ...; -/// v8pp::context_store store(isolate); -/// -/// // Save values before destroying a context -/// store.save_from(old_ctx->impl(), {"state", "config"}); -/// -/// // ... destroy and recreate context ... -/// -/// // Restore values into the new context -/// store.restore_to(new_ctx->impl(), {"state", "config"}); -class context_store -{ -public: - /// Create a store on the given isolate. - /// The isolate must outlive this store. - explicit context_store(v8::Isolate* isolate); - - ~context_store(); - - context_store(context_store const&) = delete; - context_store& operator=(context_store const&) = delete; - - context_store(context_store&&) noexcept; - context_store& operator=(context_store&&) noexcept; - - /// The isolate this store belongs to - v8::Isolate* isolate() const { return isolate_; } - - /// The internal V8 context (for advanced use) - v8::Local impl() const { return to_local(isolate_, store_ctx_); } - - /// Store a value under the given name. - /// Dot-separated names create/traverse subobjects. - /// Overwrites existing values. Returns true on success. - bool set(std::string_view name, v8::Local value); - - /// Store a C++ value (converted via convert). - template - bool set(std::string_view name, T const& value) - { - v8::HandleScope scope(isolate_); - v8::Local v8_value = to_v8(isolate_, value); - return set(name, v8_value); - } - - /// Retrieve a value by name. - /// Returns true if the name exists and value is not undefined. - /// The returned handle is valid in the caller's HandleScope. - bool get(std::string_view name, v8::Local& out) const; - - /// Retrieve and convert a value to C++ type T. - template - bool get(std::string_view name, T& out) const - { - v8::HandleScope scope(isolate_); - v8::Local val; - if (!get(name, val)) - { - return false; - } - out = from_v8(isolate_, val); - return true; - } - - /// Check whether a name exists in the store. - bool has(std::string_view name) const; - - /// Remove a value from the store. Returns true if it existed. - bool remove(std::string_view name); - - /// Remove all stored values. - void clear(); - - /// Number of top-level stored values. - size_t size() const; - - /// Names of all top-level stored values. - std::vector keys() const; - - /// Save named values from a source context into this store. - /// Returns the number of values successfully saved. - size_t save_from(v8::Local source, - std::initializer_list names); - - /// Restore named values from this store into a target context. - /// Returns the number of values successfully restored. - size_t restore_to(v8::Local target, - std::initializer_list names) const; - - /// Store a JSON-serialized deep copy of a value. - /// Returns false if serialization fails (e.g., circular references). - bool set_json(std::string_view name, v8::Local value); - - /// Retrieve a deep copy via JSON into the caller's context. - /// Returns false if the name doesn't exist or parsing fails. - bool get_json(std::string_view name, v8::Local& out) const; - -private: - v8::Isolate* isolate_; - v8::Global store_ctx_; - v8::Global store_obj_; - - /// Like traverse_subobjects but creates missing intermediate objects. - static bool ensure_subobjects(v8::Isolate* isolate, v8::Local context, - v8::Local& obj, std::string_view& name); -}; - -} // namespace v8pp - -#if V8PP_HEADER_ONLY -#include "v8pp/context_store.ipp" -#endif +#pragma once + +#include +#include +#include + +#include + +#include "v8pp/config.hpp" +#include "v8pp/convert.hpp" +#include "v8pp/object.hpp" + +namespace v8pp { + +/// A key-value store backed by a dedicated V8 context. +/// +/// Designed to hold named V8 values that outlive ephemeral contexts +/// on the same isolate. Values are stored as live V8 object references +/// (no serialization), so wrapped C++ objects, functions, and complex +/// JS structures survive intact. +/// +/// The store creates and owns a lightweight V8 context internally. +/// All stored values live in a dedicated storage object (not the global). +/// +/// Thread safety: same as V8 — single-threaded per isolate. +/// +/// Usage: +/// v8::Isolate* isolate = ...; +/// v8pp::context_store store(isolate); +/// +/// // Save values before destroying a context +/// store.save_from(old_ctx->impl(), {"state", "config"}); +/// +/// // ... destroy and recreate context ... +/// +/// // Restore values into the new context +/// store.restore_to(new_ctx->impl(), {"state", "config"}); +class context_store +{ +public: + /// Create a store on the given isolate. + /// The isolate must outlive this store. + explicit context_store(v8::Isolate* isolate); + + ~context_store(); + + context_store(context_store const&) = delete; + context_store& operator=(context_store const&) = delete; + + context_store(context_store&&) noexcept; + context_store& operator=(context_store&&) noexcept; + + /// The isolate this store belongs to + v8::Isolate* isolate() const { return isolate_; } + + /// The internal V8 context (for advanced use) + v8::Local impl() const { return to_local(isolate_, store_ctx_); } + + /// Store a value under the given name. + /// Dot-separated names create/traverse subobjects. + /// Overwrites existing values. Returns true on success. + bool set(std::string_view name, v8::Local value); + + /// Store a C++ value (converted via convert). + template + bool set(std::string_view name, T const& value) + { + v8::HandleScope scope(isolate_); + v8::Local v8_value = to_v8(isolate_, value); + return set(name, v8_value); + } + + /// Retrieve a value by name. + /// Returns true if the name exists and value is not undefined. + /// The returned handle is valid in the caller's HandleScope. + bool get(std::string_view name, v8::Local& out) const; + + /// Retrieve and convert a value to C++ type T. + template + bool get(std::string_view name, T& out) const + { + v8::HandleScope scope(isolate_); + v8::Local val; + if (!get(name, val)) + { + return false; + } + out = from_v8(isolate_, val); + return true; + } + + /// Check whether a name exists in the store. + bool has(std::string_view name) const; + + /// Remove a value from the store. Returns true if it existed. + bool remove(std::string_view name); + + /// Remove all stored values. + void clear(); + + /// Number of top-level stored values. + size_t size() const; + + /// Names of all top-level stored values. + std::vector keys() const; + + /// Save named values from a source context into this store. + /// Returns the number of values successfully saved. + size_t save_from(v8::Local source, + std::initializer_list names); + + /// Restore named values from this store into a target context. + /// Returns the number of values successfully restored. + size_t restore_to(v8::Local target, + std::initializer_list names) const; + + /// Store a JSON-serialized deep copy of a value. + /// Returns false if serialization fails (e.g., circular references). + bool set_json(std::string_view name, v8::Local value); + + /// Retrieve a deep copy via JSON into the caller's context. + /// Returns false if the name doesn't exist or parsing fails. + bool get_json(std::string_view name, v8::Local& out) const; + +private: + v8::Isolate* isolate_; + v8::Global store_ctx_; + v8::Global store_obj_; + + /// Like traverse_subobjects but creates missing intermediate objects. + static bool ensure_subobjects(v8::Isolate* isolate, v8::Local context, + v8::Local& obj, std::string_view& name); +}; + +} // namespace v8pp + +#if V8PP_HEADER_ONLY +#include "v8pp/context_store.ipp" +#endif diff --git a/v8pp/context_store.ipp b/v8pp/context_store.ipp index ed82dba..1a5b53f 100644 --- a/v8pp/context_store.ipp +++ b/v8pp/context_store.ipp @@ -1,338 +1,337 @@ -#include "v8pp/context_store.hpp" -#include "v8pp/json.hpp" - -namespace v8pp { - -V8PP_IMPL context_store::context_store(v8::Isolate* isolate) - : isolate_(isolate) -{ - v8::HandleScope scope(isolate_); - v8::Local ctx = v8::Context::New(isolate_); - store_ctx_.Reset(isolate_, ctx); - - v8::Context::Scope context_scope(ctx); - v8::Local obj = v8::Object::New(isolate_); - store_obj_.Reset(isolate_, obj); -} - -V8PP_IMPL context_store::~context_store() -{ - store_obj_.Reset(); - store_ctx_.Reset(); - isolate_ = nullptr; -} - -V8PP_IMPL context_store::context_store(context_store&& src) noexcept - : isolate_(std::exchange(src.isolate_, nullptr)) - , store_ctx_(std::move(src.store_ctx_)) - , store_obj_(std::move(src.store_obj_)) -{ -} - -V8PP_IMPL context_store& context_store::operator=(context_store&& src) noexcept -{ - if (this != &src) - { - store_obj_.Reset(); - store_ctx_.Reset(); - isolate_ = std::exchange(src.isolate_, nullptr); - store_ctx_ = std::move(src.store_ctx_); - store_obj_ = std::move(src.store_obj_); - } - return *this; -} - -V8PP_IMPL bool context_store::ensure_subobjects(v8::Isolate* isolate, v8::Local context, - v8::Local& obj, std::string_view& name) -{ - for (;;) - { - auto const dot = name.find('.'); - if (dot == name.npos) - { - return true; - } - - auto const segment = name.substr(0, dot); - v8::Local key = v8pp::to_v8(isolate, segment); - v8::Local part; - if (obj->Get(context, key).ToLocal(&part) && part->IsObject()) - { - obj = part.As(); - } - else - { - v8::Local new_obj = v8::Object::New(isolate); - if (!obj->Set(context, key, new_obj).FromMaybe(false)) - { - return false; - } - obj = new_obj; - } - name.remove_prefix(dot + 1); - } -} - -V8PP_IMPL bool context_store::set(std::string_view name, v8::Local value) -{ - v8::HandleScope scope(isolate_); - v8::Local ctx = impl(); - v8::Context::Scope context_scope(ctx); - - v8::Local obj = to_local(isolate_, store_obj_); - if (!ensure_subobjects(isolate_, ctx, obj, name)) - { - return false; - } - return obj->Set(ctx, v8pp::to_v8(isolate_, name), value).FromMaybe(false); -} - -V8PP_IMPL bool context_store::get(std::string_view name, v8::Local& out) const -{ - v8::EscapableHandleScope scope(isolate_); - v8::Local ctx = impl(); - v8::Context::Scope context_scope(ctx); - - v8::Local obj = to_local(isolate_, store_obj_); - if (!traverse_subobjects(isolate_, ctx, obj, name)) - { - return false; - } - - v8::Local val; - if (!obj->Get(ctx, v8pp::to_v8(isolate_, name)).ToLocal(&val) || val->IsUndefined()) - { - return false; - } - out = scope.Escape(val); - return true; -} - -V8PP_IMPL bool context_store::has(std::string_view name) const -{ - v8::HandleScope scope(isolate_); - v8::Local ctx = impl(); - v8::Context::Scope context_scope(ctx); - - v8::Local obj = to_local(isolate_, store_obj_); - if (!traverse_subobjects(isolate_, ctx, obj, name)) - { - return false; - } - - v8::Local val; - if (!obj->Get(ctx, v8pp::to_v8(isolate_, name)).ToLocal(&val) || val->IsUndefined()) - { - return false; - } - return true; -} - -V8PP_IMPL bool context_store::remove(std::string_view name) -{ - v8::HandleScope scope(isolate_); - v8::Local ctx = impl(); - v8::Context::Scope context_scope(ctx); - - v8::Local obj = to_local(isolate_, store_obj_); - if (!traverse_subobjects(isolate_, ctx, obj, name)) - { - return false; - } - - v8::Local key = v8pp::to_v8(isolate_, name); - if (!obj->Has(ctx, key).FromMaybe(false)) - { - return false; - } - return obj->Delete(ctx, key).FromMaybe(false); -} - -V8PP_IMPL void context_store::clear() -{ - v8::HandleScope scope(isolate_); - v8::Local ctx = impl(); - v8::Context::Scope context_scope(ctx); - - v8::Local obj = v8::Object::New(isolate_); - store_obj_.Reset(isolate_, obj); -} - -V8PP_IMPL size_t context_store::size() const -{ - v8::HandleScope scope(isolate_); - v8::Local ctx = impl(); - v8::Context::Scope context_scope(ctx); - - v8::Local names; - if (!to_local(isolate_, store_obj_)->GetOwnPropertyNames(ctx).ToLocal(&names)) - { - return 0; - } - return names->Length(); -} - -V8PP_IMPL std::vector context_store::keys() const -{ - v8::HandleScope scope(isolate_); - v8::Local ctx = impl(); - v8::Context::Scope context_scope(ctx); - - std::vector result; - v8::Local names; - if (!to_local(isolate_, store_obj_)->GetOwnPropertyNames(ctx).ToLocal(&names)) - { - return result; - } - - result.reserve(names->Length()); - for (uint32_t i = 0; i < names->Length(); ++i) - { - v8::Local key; - if (names->Get(ctx, i).ToLocal(&key)) - { - result.push_back(from_v8(isolate_, key)); - } - } - return result; -} - -V8PP_IMPL size_t context_store::save_from(v8::Local source, - std::initializer_list names) -{ - v8::HandleScope scope(isolate_); - size_t count = 0; - - for (auto const full_name : names) - { - v8::Local val; - - // Read value from source context - { - v8::Context::Scope source_scope(source); - v8::Local src_global = source->Global(); - std::string_view leaf = full_name; - - if (!traverse_subobjects(isolate_, source, src_global, leaf)) - { - continue; - } - if (!src_global->Get(source, v8pp::to_v8(isolate_, leaf)).ToLocal(&val) - || val->IsUndefined()) - { - continue; - } - } - - // Store under the original full name - if (set(full_name, val)) - { - ++count; - } - } - return count; -} - -V8PP_IMPL size_t context_store::restore_to(v8::Local target, - std::initializer_list names) const -{ - v8::HandleScope scope(isolate_); - size_t count = 0; - - for (auto name : names) - { - v8::Local val; - if (!get(name, val)) - { - continue; - } - - // Write value into target context - { - v8::Context::Scope target_scope(target); - v8::Local tgt_global = target->Global(); - std::string_view leaf = name; - - if (!ensure_subobjects(isolate_, target, tgt_global, leaf)) - { - continue; - } - if (tgt_global->Set(target, v8pp::to_v8(isolate_, leaf), val).FromMaybe(false)) - { - ++count; - } - } - } - return count; -} - -V8PP_IMPL bool context_store::set_json(std::string_view name, v8::Local value) -{ - v8::HandleScope scope(isolate_); - - // Stringify in the caller's current context - std::string json = json_str(isolate_, value); - if (json.empty()) - { - return false; - } - - // Parse and store in the store's context - v8::Local ctx = impl(); - v8::Context::Scope context_scope(ctx); - - v8::Local parsed = json_parse(isolate_, json); - if (parsed.IsEmpty() || parsed->IsUndefined()) - { - return false; - } - - v8::Local obj = to_local(isolate_, store_obj_); - if (!ensure_subobjects(isolate_, ctx, obj, name)) - { - return false; - } - return obj->Set(ctx, v8pp::to_v8(isolate_, name), parsed).FromMaybe(false); -} - -V8PP_IMPL bool context_store::get_json(std::string_view name, v8::Local& out) const -{ - v8::EscapableHandleScope scope(isolate_); - - // Read and stringify in the store's context - std::string json; - { - v8::HandleScope inner_scope(isolate_); - v8::Local ctx = impl(); - v8::Context::Scope context_scope(ctx); - - v8::Local obj = to_local(isolate_, store_obj_); - if (!traverse_subobjects(isolate_, ctx, obj, name)) - { - return false; - } - - v8::Local val; - if (!obj->Get(ctx, v8pp::to_v8(isolate_, name)).ToLocal(&val) || val->IsUndefined()) - { - return false; - } - json = json_str(isolate_, val); - } - - if (json.empty()) - { - return false; - } - - // Parse in the caller's current context - v8::Local parsed = json_parse(isolate_, json); - if (parsed.IsEmpty() || parsed->IsUndefined()) - { - return false; - } - out = scope.Escape(parsed); - return true; -} - -} // namespace v8pp +#include "v8pp/context_store.hpp" +#include "v8pp/json.hpp" + +namespace v8pp { + +V8PP_IMPL context_store::context_store(v8::Isolate* isolate) + : isolate_(isolate) +{ + v8::HandleScope scope(isolate_); + v8::Local ctx = v8::Context::New(isolate_); + store_ctx_.Reset(isolate_, ctx); + + v8::Context::Scope context_scope(ctx); + v8::Local obj = v8::Object::New(isolate_); + store_obj_.Reset(isolate_, obj); +} + +V8PP_IMPL context_store::~context_store() +{ + store_obj_.Reset(); + store_ctx_.Reset(); + isolate_ = nullptr; +} + +V8PP_IMPL context_store::context_store(context_store&& src) noexcept + : isolate_(std::exchange(src.isolate_, nullptr)) + , store_ctx_(std::move(src.store_ctx_)) + , store_obj_(std::move(src.store_obj_)) +{ +} + +V8PP_IMPL context_store& context_store::operator=(context_store&& src) noexcept +{ + if (this != &src) + { + store_obj_.Reset(); + store_ctx_.Reset(); + isolate_ = std::exchange(src.isolate_, nullptr); + store_ctx_ = std::move(src.store_ctx_); + store_obj_ = std::move(src.store_obj_); + } + return *this; +} + +V8PP_IMPL bool context_store::ensure_subobjects(v8::Isolate* isolate, v8::Local context, + v8::Local& obj, std::string_view& name) +{ + for (;;) + { + auto const dot = name.find('.'); + if (dot == name.npos) + { + return true; + } + + auto const segment = name.substr(0, dot); + v8::Local key = v8pp::to_v8(isolate, segment); + v8::Local part; + if (obj->Get(context, key).ToLocal(&part) && part->IsObject()) + { + obj = part.As(); + } + else + { + v8::Local new_obj = v8::Object::New(isolate); + if (!obj->Set(context, key, new_obj).FromMaybe(false)) + { + return false; + } + obj = new_obj; + } + name.remove_prefix(dot + 1); + } +} + +V8PP_IMPL bool context_store::set(std::string_view name, v8::Local value) +{ + v8::HandleScope scope(isolate_); + v8::Local ctx = impl(); + v8::Context::Scope context_scope(ctx); + + v8::Local obj = to_local(isolate_, store_obj_); + if (!ensure_subobjects(isolate_, ctx, obj, name)) + { + return false; + } + return obj->Set(ctx, v8pp::to_v8(isolate_, name), value).FromMaybe(false); +} + +V8PP_IMPL bool context_store::get(std::string_view name, v8::Local& out) const +{ + v8::EscapableHandleScope scope(isolate_); + v8::Local ctx = impl(); + v8::Context::Scope context_scope(ctx); + + v8::Local obj = to_local(isolate_, store_obj_); + if (!traverse_subobjects(isolate_, ctx, obj, name)) + { + return false; + } + + v8::Local val; + if (!obj->Get(ctx, v8pp::to_v8(isolate_, name)).ToLocal(&val) || val->IsUndefined()) + { + return false; + } + out = scope.Escape(val); + return true; +} + +V8PP_IMPL bool context_store::has(std::string_view name) const +{ + v8::HandleScope scope(isolate_); + v8::Local ctx = impl(); + v8::Context::Scope context_scope(ctx); + + v8::Local obj = to_local(isolate_, store_obj_); + if (!traverse_subobjects(isolate_, ctx, obj, name)) + { + return false; + } + + v8::Local val; + if (!obj->Get(ctx, v8pp::to_v8(isolate_, name)).ToLocal(&val) || val->IsUndefined()) + { + return false; + } + return true; +} + +V8PP_IMPL bool context_store::remove(std::string_view name) +{ + v8::HandleScope scope(isolate_); + v8::Local ctx = impl(); + v8::Context::Scope context_scope(ctx); + + v8::Local obj = to_local(isolate_, store_obj_); + if (!traverse_subobjects(isolate_, ctx, obj, name)) + { + return false; + } + + v8::Local key = v8pp::to_v8(isolate_, name); + if (!obj->Has(ctx, key).FromMaybe(false)) + { + return false; + } + return obj->Delete(ctx, key).FromMaybe(false); +} + +V8PP_IMPL void context_store::clear() +{ + v8::HandleScope scope(isolate_); + v8::Local ctx = impl(); + v8::Context::Scope context_scope(ctx); + + v8::Local obj = v8::Object::New(isolate_); + store_obj_.Reset(isolate_, obj); +} + +V8PP_IMPL size_t context_store::size() const +{ + v8::HandleScope scope(isolate_); + v8::Local ctx = impl(); + v8::Context::Scope context_scope(ctx); + + v8::Local names; + if (!to_local(isolate_, store_obj_)->GetOwnPropertyNames(ctx).ToLocal(&names)) + { + return 0; + } + return names->Length(); +} + +V8PP_IMPL std::vector context_store::keys() const +{ + v8::HandleScope scope(isolate_); + v8::Local ctx = impl(); + v8::Context::Scope context_scope(ctx); + + std::vector result; + v8::Local names; + if (!to_local(isolate_, store_obj_)->GetOwnPropertyNames(ctx).ToLocal(&names)) + { + return result; + } + + result.reserve(names->Length()); + for (uint32_t i = 0; i < names->Length(); ++i) + { + v8::Local key; + if (names->Get(ctx, i).ToLocal(&key)) + { + result.push_back(from_v8(isolate_, key)); + } + } + return result; +} + +V8PP_IMPL size_t context_store::save_from(v8::Local source, + std::initializer_list names) +{ + v8::HandleScope scope(isolate_); + size_t count = 0; + + for (auto const full_name : names) + { + v8::Local val; + + // Read value from source context + { + v8::Context::Scope source_scope(source); + v8::Local src_global = source->Global(); + std::string_view leaf = full_name; + + if (!traverse_subobjects(isolate_, source, src_global, leaf)) + { + continue; + } + if (!src_global->Get(source, v8pp::to_v8(isolate_, leaf)).ToLocal(&val) || val->IsUndefined()) + { + continue; + } + } + + // Store under the original full name + if (set(full_name, val)) + { + ++count; + } + } + return count; +} + +V8PP_IMPL size_t context_store::restore_to(v8::Local target, + std::initializer_list names) const +{ + v8::HandleScope scope(isolate_); + size_t count = 0; + + for (auto name : names) + { + v8::Local val; + if (!get(name, val)) + { + continue; + } + + // Write value into target context + { + v8::Context::Scope target_scope(target); + v8::Local tgt_global = target->Global(); + std::string_view leaf = name; + + if (!ensure_subobjects(isolate_, target, tgt_global, leaf)) + { + continue; + } + if (tgt_global->Set(target, v8pp::to_v8(isolate_, leaf), val).FromMaybe(false)) + { + ++count; + } + } + } + return count; +} + +V8PP_IMPL bool context_store::set_json(std::string_view name, v8::Local value) +{ + v8::HandleScope scope(isolate_); + + // Stringify in the caller's current context + std::string json = json_str(isolate_, value); + if (json.empty()) + { + return false; + } + + // Parse and store in the store's context + v8::Local ctx = impl(); + v8::Context::Scope context_scope(ctx); + + v8::Local parsed = json_parse(isolate_, json); + if (parsed.IsEmpty() || parsed->IsUndefined()) + { + return false; + } + + v8::Local obj = to_local(isolate_, store_obj_); + if (!ensure_subobjects(isolate_, ctx, obj, name)) + { + return false; + } + return obj->Set(ctx, v8pp::to_v8(isolate_, name), parsed).FromMaybe(false); +} + +V8PP_IMPL bool context_store::get_json(std::string_view name, v8::Local& out) const +{ + v8::EscapableHandleScope scope(isolate_); + + // Read and stringify in the store's context + std::string json; + { + v8::HandleScope inner_scope(isolate_); + v8::Local ctx = impl(); + v8::Context::Scope context_scope(ctx); + + v8::Local obj = to_local(isolate_, store_obj_); + if (!traverse_subobjects(isolate_, ctx, obj, name)) + { + return false; + } + + v8::Local val; + if (!obj->Get(ctx, v8pp::to_v8(isolate_, name)).ToLocal(&val) || val->IsUndefined()) + { + return false; + } + json = json_str(isolate_, val); + } + + if (json.empty()) + { + return false; + } + + // Parse in the caller's current context + v8::Local parsed = json_parse(isolate_, json); + if (parsed.IsEmpty() || parsed->IsUndefined()) + { + return false; + } + out = scope.Escape(parsed); + return true; +} + +} // namespace v8pp diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index 78c30b5..be7a437 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -8,6 +8,12 @@ #include #include #include +#ifdef WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#undef NOMINMAX +#endif #include #include #include @@ -46,16 +52,56 @@ struct convert // typed_array_trait specializations (requires V8 types) namespace detail { -template<> struct typed_array_trait { using type = v8::Uint8Array; }; -template<> struct typed_array_trait { using type = v8::Int8Array; }; -template<> struct typed_array_trait { using type = v8::Uint16Array; }; -template<> struct typed_array_trait { using type = v8::Int16Array; }; -template<> struct typed_array_trait { using type = v8::Uint32Array; }; -template<> struct typed_array_trait { using type = v8::Int32Array; }; -template<> struct typed_array_trait { using type = v8::Float32Array; }; -template<> struct typed_array_trait { using type = v8::Float64Array; }; -template<> struct typed_array_trait { using type = v8::BigInt64Array; }; -template<> struct typed_array_trait { using type = v8::BigUint64Array; }; +template<> +struct typed_array_trait +{ + using type = v8::Uint8Array; +}; +template<> +struct typed_array_trait +{ + using type = v8::Int8Array; +}; +template<> +struct typed_array_trait +{ + using type = v8::Uint16Array; +}; +template<> +struct typed_array_trait +{ + using type = v8::Int16Array; +}; +template<> +struct typed_array_trait +{ + using type = v8::Uint32Array; +}; +template<> +struct typed_array_trait +{ + using type = v8::Int32Array; +}; +template<> +struct typed_array_trait +{ + using type = v8::Float32Array; +}; +template<> +struct typed_array_trait +{ + using type = v8::Float64Array; +}; +template<> +struct typed_array_trait +{ + using type = v8::BigInt64Array; +}; +template<> +struct typed_array_trait +{ + using type = v8::BigUint64Array; +}; } // namespace detail struct invalid_argument : std::invalid_argument @@ -70,7 +116,7 @@ struct runtime_error : std::runtime_error // converter specializations for string types template - requires detail::is_string::value +requires detail::is_string::value struct convert { using Char = typename String::value_type; @@ -124,13 +170,15 @@ struct convert { return v8::String::NewFromUtf8(isolate, reinterpret_cast(value.data()), - v8::NewStringType::kNormal, static_cast(value.size())).ToLocalChecked(); + v8::NewStringType::kNormal, static_cast(value.size())) + .ToLocalChecked(); } else { return v8::String::NewFromTwoByte(isolate, reinterpret_cast(value.data()), - v8::NewStringType::kNormal, static_cast(value.size())).ToLocalChecked(); + v8::NewStringType::kNormal, static_cast(value.size())) + .ToLocalChecked(); } } @@ -275,7 +323,7 @@ struct convert // convert Number <-> integer types that fit in 32 bits template - requires (sizeof(T) <= sizeof(uint32_t)) +requires(sizeof(T) <= sizeof(uint32_t)) struct convert { using from_type = T; @@ -324,7 +372,7 @@ struct convert // values > 2^53, but this covers all practical use cases (timestamps, counters, IDs). // from_v8 accepts both Number and BigInt for interop. template - requires (sizeof(T) > sizeof(uint32_t)) +requires(sizeof(T) > sizeof(uint32_t)) struct convert { using from_type = T; @@ -371,7 +419,7 @@ struct convert }; template - requires std::is_enum_v +requires std::is_enum_v struct convert { using underlying_type = typename std::underlying_type::type; @@ -392,7 +440,7 @@ struct convert static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) { auto result = convert::try_from_v8(isolate, value); - return result ? std::optional{static_cast(*result)} : std::nullopt; + return result ? std::optional{ static_cast(*result) } : std::nullopt; } static to_type to_v8(v8::Isolate* isolate, T value) @@ -445,7 +493,7 @@ struct convert> static from_type from_v8(v8::Isolate* isolate, v8::Local value) { - if (value.IsEmpty() || value->IsNullOrUndefined()) + if (value.IsEmpty() || value->IsNullOrUndefined()) { return std::nullopt; } @@ -455,7 +503,7 @@ struct convert> } else { - throw invalid_argument(isolate, value, "Optional"); + throw invalid_argument(isolate, value, "Optional"); } } @@ -463,11 +511,11 @@ struct convert> { if (value.IsEmpty() || value->IsNullOrUndefined()) { - return from_type{std::nullopt}; + return from_type{ std::nullopt }; } if (convert::is_valid(isolate, value)) { - return from_type{convert::from_v8(isolate, value)}; + return from_type{ convert::from_v8(isolate, value) }; } return std::nullopt; } @@ -496,8 +544,7 @@ struct convert> static bool is_valid(v8::Isolate*, v8::Local value) { - return !value.IsEmpty() && value->IsArray() - && value.As()->Length() == N; + return !value.IsEmpty() && value->IsArray() && value.As()->Length() == N; } static from_type from_v8(v8::Isolate* isolate, v8::Local value) @@ -625,8 +672,7 @@ struct convert> return std::visit([isolate](auto&& v) -> v8::Local { using T = std::decay_t; - return v8pp::convert::to_v8(isolate, v); - }, value); + return v8pp::convert::to_v8(isolate, v); }, value); } private: @@ -654,8 +700,7 @@ struct convert> static bool is_map_object(v8::Isolate* isolate, v8::Local obj) { v8::Local prop_names; - return obj->GetPropertyNames(isolate->GetCurrentContext()).ToLocal(&prop_names) - && prop_names->Length() > 0; + return obj->GetPropertyNames(isolate->GetCurrentContext()).ToLocal(&prop_names) && prop_names->Length() > 0; } template @@ -743,7 +788,7 @@ struct convert> // For 64-bit integrals from Number, only match if the double // is an exact integer within safe range (±2^53) double d = value->NumberValue(isolate->GetCurrentContext()).FromJust(); - constexpr double safe_max = static_cast(uint64_t{1} << std::numeric_limits::digits); + constexpr double safe_max = static_cast(uint64_t{ 1 } << std::numeric_limits::digits); if (std::isfinite(d) && d == std::trunc(d)) { if constexpr (std::is_signed_v) @@ -791,7 +836,7 @@ struct convert> // convert Array <-> std::array, vector, deque, list template - requires (detail::sequence || detail::is_array::value) +requires(detail::sequence || detail::is_array::value) struct convert { using from_type = Sequence; @@ -825,9 +870,7 @@ struct convert constexpr size_t length = detail::is_array::length; if (array->Length() != length) { - throw std::runtime_error("Invalid array length: expected " - + std::to_string(length) + " actual " - + std::to_string(array->Length())); + throw std::runtime_error("Invalid array length: expected " + std::to_string(length) + " actual " + std::to_string(array->Length())); } } else if constexpr (detail::has_reserve::value) @@ -855,9 +898,7 @@ struct convert constexpr int max_size = std::numeric_limits::max(); if (value.size() > max_size) { - throw std::runtime_error("Invalid array length: actual " - + std::to_string(value.size()) + " exceeds maximal " - + std::to_string(max_size)); + throw std::runtime_error("Invalid array length: actual " + std::to_string(value.size()) + " exceeds maximal " + std::to_string(max_size)); } v8::EscapableHandleScope scope(isolate); @@ -979,9 +1020,7 @@ struct convert constexpr int max_size = std::numeric_limits::max(); if (value.size() > static_cast(max_size)) { - throw std::runtime_error("Invalid array length: actual " - + std::to_string(value.size()) + " exceeds maximal " - + std::to_string(max_size)); + throw std::runtime_error("Invalid array length: actual " + std::to_string(value.size()) + " exceeds maximal " + std::to_string(max_size)); } v8::EscapableHandleScope scope(isolate); @@ -1005,8 +1044,7 @@ struct convert, void> static bool is_valid(v8::Isolate*, v8::Local value) { - return !value.IsEmpty() && value->IsArray() - && value.As()->Length() == 2; + return !value.IsEmpty() && value->IsArray() && value.As()->Length() == 2; } static from_type from_v8(v8::Isolate* isolate, v8::Local value) @@ -1054,7 +1092,14 @@ struct convert static from_type from_v8(v8::Isolate* isolate, v8::Local value) { std::string str = convert::from_v8(isolate, value); +#ifdef WIN32 + int sz = MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast(str.size()), nullptr, 0); + std::wstring w(sz, 0); + MultiByteToWideChar(CP_UTF8, 0, str.data(), static_cast(str.size()), w.data(), sz); + return std::filesystem::path(w); +#else return std::filesystem::path(str); +#endif } static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) @@ -1062,14 +1107,29 @@ struct convert if (auto str = convert::try_from_v8(isolate, value)) { std::string s = *std::move(str); +#ifdef WIN32 + int sz = MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast(s.size()), nullptr, 0); + std::wstring w(sz, 0); + MultiByteToWideChar(CP_UTF8, 0, s.data(), static_cast(s.size()), w.data(), sz); + return std::filesystem::path(w); +#else return std::filesystem::path(s); +#endif } return std::nullopt; } static to_type to_v8(v8::Isolate* isolate, from_type const& value) { +#ifdef WIN32 + std::wstring w = value.generic_wstring(); + int sz = WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast(w.size()), nullptr, 0, nullptr, nullptr); + std::string utf8(sz, 0); + WideCharToMultiByte(CP_UTF8, 0, w.data(), static_cast(w.size()), utf8.data(), sz, nullptr, nullptr); + return convert::to_v8(isolate, utf8); +#else return convert::to_v8(isolate, value.string()); +#endif } }; @@ -1248,19 +1308,7 @@ struct convert> }; template -struct is_wrapped_class : std::conjunction< - std::is_class, - std::negation>, - std::negation>, - std::negation>, - std::negation>, - std::negation>, - std::negation>, - std::negation>, - std::negation>, - std::negation>, - std::negation>, - std::negation>> +struct is_wrapped_class : std::conjunction, std::negation>, std::negation>, std::negation>, std::negation>, std::negation>, std::negation>, std::negation>, std::negation>, std::negation>, std::negation>, std::negation>> { }; @@ -1286,7 +1334,7 @@ struct is_wrapped_class : std::false_type }; template - requires is_wrapped_class::value +requires is_wrapped_class::value struct convert { using from_type = T*; @@ -1311,7 +1359,7 @@ struct convert { // from_v8 returns nullptr without throwing on failure auto ptr = from_v8(isolate, value); - return ptr ? std::optional{ptr} : std::nullopt; + return ptr ? std::optional{ ptr } : std::nullopt; } static to_type to_v8(v8::Isolate* isolate, T const* value) @@ -1321,7 +1369,7 @@ struct convert }; template - requires is_wrapped_class::value +requires is_wrapped_class::value struct convert { using from_type = T&; @@ -1351,7 +1399,7 @@ struct convert { if (value.IsEmpty() || !value->IsObject()) return std::nullopt; T* object = class_::unwrap_object(isolate, value); - return object ? std::optional{*object} : std::nullopt; + return object ? std::optional{ *object } : std::nullopt; } static to_type to_v8(v8::Isolate* isolate, T const& value) @@ -1363,7 +1411,7 @@ struct convert }; template - requires is_wrapped_class::value +requires is_wrapped_class::value struct convert, void> { using from_type = std::shared_ptr; @@ -1388,7 +1436,7 @@ struct convert, void> { // from_v8 returns empty shared_ptr without throwing on failure auto ptr = from_v8(isolate, value); - return ptr ? std::optional{std::move(ptr)} : std::nullopt; + return ptr ? std::optional{ std::move(ptr) } : std::nullopt; } static to_type to_v8(v8::Isolate* isolate, std::shared_ptr const& value) @@ -1418,7 +1466,7 @@ struct convert std::shared_ptr object = class_::unwrap_object(isolate, value); if (object) { - //assert(object.use_count() > 1); + // assert(object.use_count() > 1); return *object; } throw std::runtime_error("failed to unwrap C++ object"); @@ -1428,7 +1476,7 @@ struct convert { if (value.IsEmpty() || !value->IsObject()) return std::nullopt; std::shared_ptr object = class_::unwrap_object(isolate, value); - return object ? std::optional{*object} : std::nullopt; + return object ? std::optional{ *object } : std::nullopt; } static to_type to_v8(v8::Isolate* isolate, T const& value) @@ -1462,7 +1510,8 @@ auto from_v8(v8::Isolate* isolate, v8::Local value, U const& default_ { using return_type = decltype(convert::from_v8(isolate, value)); return convert::is_valid(isolate, value) ? - convert::from_v8(isolate, value) : static_cast(default_value); + convert::from_v8(isolate, value) : + static_cast(default_value); } namespace detail { @@ -1475,7 +1524,7 @@ auto try_from_v8_fallback(v8::Isolate* isolate, v8::Local value) { using result_type = std::decay_t::from_type>; if (!convert::is_valid(isolate, value)) return std::optional{}; - return std::optional{convert::from_v8(isolate, value)}; + return std::optional{ convert::from_v8(isolate, value) }; } } // namespace detail @@ -1553,7 +1602,8 @@ v8::Local to_v8(v8::Isolate* isolate, inline v8::Local to_v8_name(v8::Isolate* isolate, std::string_view name) { return v8::String::NewFromUtf8(isolate, name.data(), - v8::NewStringType::kInternalized, static_cast(name.size())).ToLocalChecked(); + v8::NewStringType::kInternalized, static_cast(name.size())) + .ToLocalChecked(); } template @@ -1609,17 +1659,12 @@ v8::Local to_local(v8::Isolate* isolate, v8::PersistentBase const& handle) } inline invalid_argument::invalid_argument(v8::Isolate* isolate, v8::Local value, char const* expected_type) - : std::invalid_argument(std::string("expected ") - + expected_type - + ", typeof=" - + (value.IsEmpty() ? std::string("") : v8pp::from_v8(isolate, value->TypeOf(isolate)))) + : std::invalid_argument(std::string("expected ") + expected_type + ", typeof=" + (value.IsEmpty() ? std::string("") : v8pp::from_v8(isolate, value->TypeOf(isolate)))) { } inline runtime_error::runtime_error(v8::Isolate* isolate, v8::Local value, char const* message) - : std::runtime_error(std::string("runtime error: ") + message - + ", typeof=" - + (value.IsEmpty() ? std::string("") : v8pp::from_v8(isolate, value->TypeOf(isolate)))) + : std::runtime_error(std::string("runtime error: ") + message + ", typeof=" + (value.IsEmpty() ? std::string("") : v8pp::from_v8(isolate, value->TypeOf(isolate)))) { } diff --git a/v8pp/fast_api.hpp b/v8pp/fast_api.hpp index b9ba09d..3baf920 100644 --- a/v8pp/fast_api.hpp +++ b/v8pp/fast_api.hpp @@ -1,147 +1,163 @@ -#pragma once - -#include - -#include - -// V8 10+ supports Fast API callbacks, but some distributions (e.g. Ubuntu libv8-dev) -// ship V8 10+ without the v8-fast-api-calls.h header. Guard on both version and header. -#if V8_MAJOR_VERSION >= 10 && __has_include() -#include -#define V8PP_HAS_FAST_API_HEADER 1 -#endif - -namespace v8pp { - -namespace detail { - -/// Check if T is a supported Fast API return type -/// V8 10.x supports: void, bool, int32_t, uint32_t, float, double -template -constexpr bool is_fast_return_type_v = - std::is_void_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v; - -/// Check if T is a supported Fast API argument type -/// V8 10.x supports: bool, int32_t, uint32_t, int64_t, uint64_t, float, double -template -constexpr bool is_fast_arg_type_v = - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v; - -/// Check if a function signature is Fast API compatible -template -struct is_fast_api_compatible : std::false_type {}; - -template -struct is_fast_api_compatible - : std::bool_constant && (is_fast_arg_type_v && ...)> {}; - -template -struct is_fast_api_compatible - : std::bool_constant && (is_fast_arg_type_v && ...)> {}; - -template -struct is_fast_api_compatible - : std::bool_constant && (is_fast_arg_type_v && ...)> {}; - -#ifdef V8PP_HAS_FAST_API_HEADER - -/// Fast callback wrapper — generates a static function with the V8 fast API signature. -/// Uses NTTP to bake the function pointer at compile time so V8 can call it directly. -template -struct fast_callback; - -/// Free function: R(*)(Args...) -template -struct fast_callback -{ - static R call(v8::Local /*receiver*/, Args... args, - v8::FastApiCallbackOptions& /*options*/) - { - return FuncPtr(args...); - } -}; - -/// Member function: R(C::*)(Args...) — extracts C++ object from receiver internal field 0 -template -struct fast_callback -{ - static R call(v8::Local receiver, Args... args, - v8::FastApiCallbackOptions& options) - { - void* ptr = receiver->GetAlignedPointerFromInternalField(0); - if (!ptr) - { -#if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) - options.isolate->ThrowError("Invalid receiver: null C++ object"); -#else - options.fallback = true; -#endif - if constexpr (std::is_void_v) return; - else return R{}; - } - return (static_cast(ptr)->*MemPtr)(args...); - } -}; - -/// Const member function: R(C::*)(Args...) const -template -struct fast_callback -{ - static R call(v8::Local receiver, Args... args, - v8::FastApiCallbackOptions& options) - { - void* ptr = receiver->GetAlignedPointerFromInternalField(0); - if (!ptr) - { -#if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) - options.isolate->ThrowError("Invalid receiver: null C++ object"); -#else - options.fallback = true; -#endif - if constexpr (std::is_void_v) return; - else return R{}; - } - return (static_cast(ptr)->*MemPtr)(args...); - } -}; - -#endif // V8PP_HAS_FAST_API_HEADER - -} // namespace detail - -/// Tag to detect fast_function types -template -struct is_fast_function : std::false_type {}; - -/// Compile-time Fast API function wrapper. -/// Wraps a function pointer as a non-type template parameter to enable V8 Fast API callbacks. -/// Compatible signatures get both a fast callback (called directly by V8's JIT) and -/// the standard slow callback. Incompatible signatures silently fall back to slow-only. -/// Usage: module.function("add", v8pp::fast_fn<&add>); -template -struct fast_function -{ - using func_type = decltype(FuncPtr); - static constexpr func_type ptr = FuncPtr; - static constexpr bool compatible = detail::is_fast_api_compatible::value; -}; - -template -struct is_fast_function> : std::true_type {}; - -/// Variable template for convenient use -template -inline constexpr fast_function fast_fn{}; - -} // namespace v8pp +#pragma once + +#include + +#include + +// V8 10+ supports Fast API callbacks, but some distributions (e.g. Ubuntu libv8-dev) +// ship V8 10+ without the v8-fast-api-calls.h header. Guard on both version and header. +#if V8_MAJOR_VERSION >= 10 && __has_include() +#include +#define V8PP_HAS_FAST_API_HEADER 1 +#endif + +namespace v8pp { + +namespace detail { + +/// Check if T is a supported Fast API return type +/// V8 10.x supports: void, bool, int32_t, uint32_t, float, double +template +constexpr bool is_fast_return_type_v = + std::is_void_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v; + +/// Check if T is a supported Fast API argument type +/// V8 10.x supports: bool, int32_t, uint32_t, int64_t, uint64_t, float, double +template +constexpr bool is_fast_arg_type_v = + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v; + +/// Check if a function signature is Fast API compatible +template +struct is_fast_api_compatible : std::false_type +{ +}; + +template +struct is_fast_api_compatible + : std::bool_constant && (is_fast_arg_type_v && ...)> +{ +}; + +template +struct is_fast_api_compatible + : std::bool_constant && (is_fast_arg_type_v && ...)> +{ +}; + +template +struct is_fast_api_compatible + : std::bool_constant && (is_fast_arg_type_v && ...)> +{ +}; + +#ifdef V8PP_HAS_FAST_API_HEADER + +/// Fast callback wrapper — generates a static function with the V8 fast API signature. +/// Uses NTTP to bake the function pointer at compile time so V8 can call it directly. +template +struct fast_callback; + +/// Free function: R(*)(Args...) +template +struct fast_callback +{ + static R call(v8::Local /*receiver*/, Args... args, + v8::FastApiCallbackOptions& /*options*/) + { + return FuncPtr(args...); + } +}; + +/// Member function: R(C::*)(Args...) — extracts C++ object from receiver internal field 0 +template +struct fast_callback +{ + static R call(v8::Local receiver, Args... args, + v8::FastApiCallbackOptions& options) + { + void* ptr = receiver->GetAlignedPointerFromInternalField(0); + if (!ptr) + { +#if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) + options.isolate->ThrowError("Invalid receiver: null C++ object"); +#else + options.fallback = true; +#endif + if constexpr (std::is_void_v) + return; + else + return R{}; + } + return (static_cast(ptr)->*MemPtr)(args...); + } +}; + +/// Const member function: R(C::*)(Args...) const +template +struct fast_callback +{ + static R call(v8::Local receiver, Args... args, + v8::FastApiCallbackOptions& options) + { + void* ptr = receiver->GetAlignedPointerFromInternalField(0); + if (!ptr) + { +#if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) + options.isolate->ThrowError("Invalid receiver: null C++ object"); +#else + options.fallback = true; +#endif + if constexpr (std::is_void_v) + return; + else + return R{}; + } + return (static_cast(ptr)->*MemPtr)(args...); + } +}; + +#endif // V8PP_HAS_FAST_API_HEADER + +} // namespace detail + +/// Tag to detect fast_function types +template +struct is_fast_function : std::false_type +{ +}; + +/// Compile-time Fast API function wrapper. +/// Wraps a function pointer as a non-type template parameter to enable V8 Fast API callbacks. +/// Compatible signatures get both a fast callback (called directly by V8's JIT) and +/// the standard slow callback. Incompatible signatures silently fall back to slow-only. +/// Usage: module.function("add", v8pp::fast_fn<&add>); +template +struct fast_function +{ + using func_type = decltype(FuncPtr); + static constexpr func_type ptr = FuncPtr; + static constexpr bool compatible = detail::is_fast_api_compatible::value; +}; + +template +struct is_fast_function> : std::true_type +{ +}; + +/// Variable template for convenient use +template +inline constexpr fast_function fast_fn{}; + +} // namespace v8pp diff --git a/v8pp/function.hpp b/v8pp/function.hpp index bd167ea..f1db352 100644 --- a/v8pp/function.hpp +++ b/v8pp/function.hpp @@ -19,7 +19,7 @@ class external_data template static constexpr bool is_bitcast_allowed = sizeof(T) <= sizeof(void*) && std::is_default_constructible_v && - std::is_trivially_copyable_v && + std::is_trivially_copyable_v && // Member pointers can be null (offset 0) -> Debug check failed: (value) != nullptr. !std::is_member_pointer_v; template @@ -114,11 +114,8 @@ class external_data new (&storage) T(std::forward(data)); pext.Reset(isolate, v8::External::New(isolate, this)); pext.SetWrapperClassId(external_data::class_id); - pext.SetWeak(this, - [](v8::WeakCallbackInfo const& info) - { - delete info.GetParameter(); - }, v8::WeakCallbackType::kParameter); + pext.SetWeak(this, [](v8::WeakCallbackInfo const& info) + { delete info.GetParameter(); }, v8::WeakCallbackType::kParameter); } ~value_holder() @@ -272,7 +269,7 @@ v8::Local wrap_function_template(v8::Isolate* isolate, F&& using Bundle = detail::function_with_defaults; return v8::FunctionTemplate::New(isolate, &detail::forward_function_with_defaults, - detail::external_data::set(isolate, Bundle{std::forward(func), std::move(defs)}), + detail::external_data::set(isolate, Bundle{ std::forward(func), std::move(defs) }), v8::Local(), 0, v8::ConstructorBehavior::kAllow, side_effect_type); @@ -289,10 +286,11 @@ v8::Local wrap_function(v8::Isolate* isolate, std::string_view nam using Bundle = detail::function_with_defaults; v8::Local fn; if (!v8::Function::New(isolate->GetCurrentContext(), - &detail::forward_function_with_defaults, - detail::external_data::set(isolate, Bundle{std::forward(func), std::move(defs)}), - 0, v8::ConstructorBehavior::kAllow, - side_effect_type).ToLocal(&fn)) + &detail::forward_function_with_defaults, + detail::external_data::set(isolate, Bundle{ std::forward(func), std::move(defs) }), + 0, v8::ConstructorBehavior::kAllow, + side_effect_type) + .ToLocal(&fn)) { return {}; } @@ -313,10 +311,11 @@ v8::Local wrap_function(v8::Isolate* isolate, std::string_view nam using F_type = typename std::decay_t; v8::Local fn; if (!v8::Function::New(isolate->GetCurrentContext(), - &detail::forward_function, - detail::external_data::set(isolate, std::forward(func)), - 0, v8::ConstructorBehavior::kAllow, - side_effect_type).ToLocal(&fn)) + &detail::forward_function, + detail::external_data::set(isolate, std::forward(func)), + 0, v8::ConstructorBehavior::kAllow, + side_effect_type) + .ToLocal(&fn)) { return {}; } diff --git a/v8pp/json.ipp b/v8pp/json.ipp index 63bda28..0373e91 100644 --- a/v8pp/json.ipp +++ b/v8pp/json.ipp @@ -57,8 +57,7 @@ V8PP_IMPL v8::Local json_object(v8::Isolate* isolate, v8::LocalLength(); i < count; ++i) { v8::Local name, value; - if (prop_names->Get(context, i).ToLocal(&name) - && object->Get(context, name).ToLocal(&value)) + if (prop_names->Get(context, i).ToLocal(&name) && object->Get(context, name).ToLocal(&value)) { if (value->IsFunction()) { diff --git a/v8pp/module.hpp b/v8pp/module.hpp index f88e8e6..e8cdeda 100644 --- a/v8pp/module.hpp +++ b/v8pp/module.hpp @@ -67,7 +67,7 @@ class module /// Set a C++ function in the module with specified name template - requires detail::is_callable>::value + requires detail::is_callable>::value module& function(std::string_view name, Function&& func, v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) { @@ -79,8 +79,7 @@ class module module& function(std::string_view name, fast_function, v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) { - return value(name, wrap_function_template(isolate_, - fast_function{}, side_effect_type)); + return value(name, wrap_function_template(isolate_, fast_function{}, side_effect_type)); } /// Set a C++ function with default parameter values in the module @@ -90,20 +89,16 @@ class module { using Fun = typename std::decay_t; static_assert(detail::is_callable::value, "Function must be callable"); - return value(name, wrap_function_template(isolate_, - std::forward(func), std::move(defs), side_effect_type)); + return value(name, wrap_function_template(isolate_, std::forward(func), std::move(defs), side_effect_type)); } /// Set multiple overloaded C++ functions in the module with specified name. /// F2 must be callable or an overload_entry (excludes defaults, SideEffectType, etc.) template - requires (detail::is_callable>::value - || std::is_member_function_pointer_v> - || is_overload_entry>::value) + requires(detail::is_callable>::value || std::is_member_function_pointer_v> || is_overload_entry>::value) module& function(std::string_view name, F1&& f1, F2&& f2, Fs&&... fs) { - return value(name, wrap_overload_template(isolate_, - std::forward(f1), std::forward(f2), std::forward(fs)...)); + return value(name, wrap_overload_template(isolate_, std::forward(f1), std::forward(f2), std::forward(fs)...)); } /// Set a C++ variable in the module with specified name @@ -133,14 +128,14 @@ class module /// Set property in the module with specified name and get/set functions template + requires(!is_fast_function>::value) module& property(char const* name, GetFunction&& get, SetFunction&& set = {}) { using Getter = typename std::decay_t; using Setter = typename std::decay_t; static_assert(detail::is_callable::value, "GetFunction must be callable"); - static_assert(detail::is_callable::value - || std::same_as, "SetFunction must be callable"); + static_assert(detail::is_callable::value || std::same_as, "SetFunction must be callable"); using property_type = v8pp::property; using Traits = detail::none; @@ -152,9 +147,7 @@ class module v8::AccessorNameSetterCallback setter = property_type::is_readonly ? nullptr : property_type::template set; v8::Local data = detail::external_data::set(isolate_, property_type(std::move(get), std::move(set))); - v8::SideEffectType setter_effect = property_type::is_readonly - ? v8::SideEffectType::kHasSideEffect - : v8::SideEffectType::kHasSideEffectToReceiver; + v8::SideEffectType setter_effect = property_type::is_readonly ? v8::SideEffectType::kHasSideEffect : v8::SideEffectType::kHasSideEffectToReceiver; #if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) obj_->SetNativeDataProperty(v8_name, getter, setter, data, v8::PropertyAttribute::DontDelete, @@ -167,6 +160,44 @@ class module return *this; } + /// Set read-only module property with V8 Fast API getter + template + module& property(char const* name, fast_function) + { + v8::HandleScope scope(isolate_); + + v8::Local v8_name = v8pp::to_v8_name(isolate_, name); + auto getter_template = wrap_function_template( + isolate_, fast_function{}, + v8::SideEffectType::kHasNoSideEffect); + + obj_->SetAccessorProperty(v8_name, getter_template, + v8::Local(), + v8::PropertyAttribute(v8::ReadOnly | v8::DontDelete)); + + return *this; + } + + /// Set read/write module property with V8 Fast API getter and setter + template + module& property(char const* name, fast_function, fast_function) + { + v8::HandleScope scope(isolate_); + + v8::Local v8_name = v8pp::to_v8_name(isolate_, name); + auto getter_template = wrap_function_template( + isolate_, fast_function{}, + v8::SideEffectType::kHasNoSideEffect); + auto setter_template = wrap_function_template( + isolate_, fast_function{}, + v8::SideEffectType::kHasSideEffectToReceiver); + + obj_->SetAccessorProperty(v8_name, getter_template, setter_template, + v8::PropertyAttribute(v8::DontDelete)); + + return *this; + } + /// Set another module as a read-only property module& const_(std::string_view name, module& m) { diff --git a/v8pp/object.hpp b/v8pp/object.hpp index 60ec16c..9ed03d6 100644 --- a/v8pp/object.hpp +++ b/v8pp/object.hpp @@ -23,8 +23,7 @@ inline bool traverse_subobjects(v8::Isolate* isolate, v8::Local con } v8::Local part; - if (!options->Get(context, v8pp::to_v8(isolate, name.substr(0, dot_pos))).ToLocal(&part) - || !part->IsObject()) + if (!options->Get(context, v8pp::to_v8(isolate, name.substr(0, dot_pos))).ToLocal(&part) || !part->IsObject()) { return false; } @@ -48,8 +47,7 @@ bool get_option(v8::Isolate* isolate, v8::Local options, } v8::Local val; - if (!options->Get(context, v8pp::to_v8(isolate, name)).ToLocal(&val) - || val->IsUndefined()) + if (!options->Get(context, v8pp::to_v8(isolate, name)).ToLocal(&val) || val->IsUndefined()) { return false; } @@ -102,8 +100,9 @@ bool set_option_data(v8::Isolate* isolate, v8::Local options, } return options->CreateDataProperty(context, - v8pp::to_v8(isolate, name).As(), - to_v8(isolate, value)).FromMaybe(false); + v8pp::to_v8(isolate, name).As(), + to_v8(isolate, value)) + .FromMaybe(false); } /// Alias for set_option_data without subobjects. @@ -121,8 +120,9 @@ void set_const(v8::Isolate* isolate, v8::Local options, std::string_view name, T const& value) { options->DefineOwnProperty(isolate->GetCurrentContext(), - v8pp::to_v8(isolate, name), to_v8(isolate, value), - v8::PropertyAttribute(v8::ReadOnly | v8::DontDelete)).FromJust(); + v8pp::to_v8(isolate, name), to_v8(isolate, value), + v8::PropertyAttribute(v8::ReadOnly | v8::DontDelete)) + .FromJust(); } } // namespace v8pp diff --git a/v8pp/overload.hpp b/v8pp/overload.hpp index bd1adc1..ce7ef07 100644 --- a/v8pp/overload.hpp +++ b/v8pp/overload.hpp @@ -1,243 +1,277 @@ -#pragma once - -#include -#include - -#include "v8pp/call_from_v8.hpp" -#include "v8pp/function.hpp" - -namespace v8pp { - -/// Compile-time overload selector for free functions -template -constexpr auto overload(Sig* f) -> Sig* { return f; } - -/// Compile-time overload selector for member function pointers -template - requires std::is_member_function_pointer_v -constexpr MemFn overload(MemFn f) { return f; } - -/// Tag to detect overload_entry types -template -struct is_overload_entry : std::false_type {}; - -/// An overload entry bundling one function, optionally with defaults -template -struct overload_entry -{ - F func; -}; - -template -struct overload_entry> -{ - F func; - defaults defs; -}; - -template -struct is_overload_entry> : std::true_type {}; - -/// Helper: wrap a function with defaults into an overload_entry -template -auto with_defaults(F&& func, defaults defs) -{ - using F_type = std::decay_t; - return overload_entry>{std::forward(func), std::move(defs)}; -} - -} // namespace v8pp - -namespace v8pp::detail { - -/// Compute min/max JS arg counts for one overload entry -template -struct overload_arg_range; - -template -struct overload_arg_range> -{ - static constexpr size_t offset = is_first_arg_isolate ? 1 : 0; - using traits = call_from_v8_traits; - static constexpr size_t max_args = traits::arg_count; - static constexpr size_t min_args = max_args - traits::optional_arg_count; -}; - -template -struct overload_arg_range>> -{ - static constexpr size_t offset = is_first_arg_isolate ? 1 : 0; - using traits = call_from_v8_traits; - static constexpr size_t max_args = traits::arg_count; - static constexpr size_t num_defaults = sizeof...(Defs); - static constexpr size_t min_args = max_args - num_defaults - traits::optional_arg_count; -}; - -/// Check if a V8 value is valid for a given C++ arg type using convert::is_valid -template -bool arg_type_matches(v8::Isolate* isolate, v8::Local value) -{ - using T = std::remove_cv_t>; - using U = std::remove_pointer_t; - using converter = typename std::conditional_t< - is_wrapped_class>::value, - std::conditional_t, - typename Traits::template convert_ptr, - typename Traits::template convert_ref>, - convert>>; - return converter::is_valid(isolate, value); -} - -/// Check provided JS args against a function's expected types. -/// Uses compile-time index sequence for all possible args, skips unprovided ones at runtime. -template -bool check_arg_types(v8::Isolate* isolate, v8::FunctionCallbackInfo const& args, - size_t provided_count, std::index_sequence) -{ - using traits = call_from_v8_traits; - // Only validate args that were actually provided by JS - return ((Indices >= provided_count || - arg_type_matches, Traits>( - isolate, args[Indices])) && ...); -} - -/// Check if JS args match a function's expected types (arity already validated) -template -bool overload_types_match(v8::Isolate* isolate, v8::FunctionCallbackInfo const& args, - size_t arg_count) -{ - constexpr size_t offset = is_first_arg_isolate ? 1 : 0; - using traits = call_from_v8_traits; - - if (arg_count == 0) - return true; - - return check_arg_types(isolate, args, arg_count, - std::make_index_sequence{}); -} - -/// Try to invoke one overload entry (no defaults). Returns true on success. -template -bool try_invoke_entry(overload_entry const& entry, - v8::FunctionCallbackInfo const& args) -{ - using FTraits = function_traits; - v8::Isolate* isolate = args.GetIsolate(); - // Copy func — entry is const& so entry.func is const, but call_from_v8 - // needs a non-const-qualified type for function_traits to work - F func = entry.func; - - if constexpr (std::is_member_function_pointer_v) - { - using class_type = std::decay_t; - auto obj = class_::unwrap_object(isolate, args.This()); - if (!obj) return false; - - if constexpr (std::same_as) - { - call_from_v8(std::move(func), args, *obj); - } - else +#pragma once + +#include +#include + +#include "v8pp/call_from_v8.hpp" +#include "v8pp/function.hpp" + +namespace v8pp { + +/// Compile-time overload selector for free functions +template +constexpr auto overload(Sig* f) -> Sig* +{ + return f; +} + +/// Compile-time overload selector for member function pointers +template +requires std::is_member_function_pointer_v +constexpr MemFn overload(MemFn f) +{ + return f; +} + +/// Tag to detect overload_entry types +template +struct is_overload_entry : std::false_type +{ +}; + +/// An overload entry bundling one function, optionally with defaults +template +struct overload_entry +{ + F func; +}; + +template +struct overload_entry> +{ + F func; + defaults defs; +}; + +template +struct is_overload_entry> : std::true_type +{ +}; + +/// Helper: wrap a function with defaults into an overload_entry +template +auto with_defaults(F&& func, defaults defs) +{ + using F_type = std::decay_t; + return overload_entry>{ std::forward(func), std::move(defs) }; +} + +} // namespace v8pp + +namespace v8pp::detail { + +/// Compute min/max JS arg counts for one overload entry +template +struct overload_arg_range; + +template +struct overload_arg_range> +{ + static constexpr size_t offset = is_first_arg_isolate ? 1 : 0; + using traits = call_from_v8_traits; + static constexpr size_t max_args = traits::arg_count; + static constexpr size_t min_args = max_args - traits::optional_arg_count; +}; + +template +struct overload_arg_range>> +{ + static constexpr size_t offset = is_first_arg_isolate ? 1 : 0; + using traits = call_from_v8_traits; + static constexpr size_t max_args = traits::arg_count; + static constexpr size_t num_defaults = sizeof...(Defs); + static constexpr size_t min_args = max_args - num_defaults - traits::optional_arg_count; +}; + +/// Check if a V8 value is valid for a given C++ arg type using convert::is_valid +template +bool arg_type_matches(v8::Isolate* isolate, v8::Local value) +{ + using T = std::remove_cv_t>; + using U = std::remove_pointer_t; + using converter = typename std::conditional_t< + is_wrapped_class>::value, + std::conditional_t, + typename Traits::template convert_ptr, + typename Traits::template convert_ref>, + convert>>; + return converter::is_valid(isolate, value); +} + +/// Check provided JS args against a function's expected types. +/// Uses compile-time index sequence for all possible args, skips unprovided ones at runtime. +template +bool check_arg_types(v8::Isolate* isolate, v8::FunctionCallbackInfo const& args, + size_t provided_count, std::index_sequence) +{ + using traits = call_from_v8_traits; + // Only validate args that were actually provided by JS + return ((Indices >= provided_count || + arg_type_matches, Traits>( + isolate, args[Indices])) && + ...); +} + +/// Check if JS args match a function's expected types (arity already validated) +template +bool overload_types_match(v8::Isolate* isolate, v8::FunctionCallbackInfo const& args, + size_t arg_count) +{ + constexpr size_t offset = is_first_arg_isolate ? 1 : 0; + using traits = call_from_v8_traits; + + if (arg_count == 0) + return true; + + return check_arg_types(isolate, args, arg_count, + std::make_index_sequence{}); +} + +/// Try to invoke one overload entry (no defaults). Returns true on success. +template +bool try_invoke_entry(overload_entry const& entry, + v8::FunctionCallbackInfo const& args) +{ + using FTraits = function_traits; + v8::Isolate* isolate = args.GetIsolate(); + // Copy func — entry is const& so entry.func is const, but call_from_v8 + // needs a non-const-qualified type for function_traits to work + F func = entry.func; + + if constexpr (std::is_member_function_pointer_v) + { + using class_type = std::decay_t; + auto obj = class_::unwrap_object(isolate, args.This()); + if (!obj) return false; + + if constexpr (std::same_as) + { + call_from_v8(std::move(func), args, *obj); + } + else + { + using return_type = typename FTraits::return_type; + using converter = typename call_from_v8_traits::template arg_converter; + args.GetReturnValue().Set(converter::to_v8(isolate, + call_from_v8(std::move(func), args, *obj))); + } + } + else + { + if constexpr (std::same_as) + { + call_from_v8(std::move(func), args); + } + else + { + using return_type = typename FTraits::return_type; + using converter = typename call_from_v8_traits::template arg_converter; + args.GetReturnValue().Set(converter::to_v8(isolate, + call_from_v8(std::move(func), args))); + } + } + return true; +} + +/// Try to invoke one overload entry (with defaults). Returns true on success. +template +bool try_invoke_entry(overload_entry> const& entry, + v8::FunctionCallbackInfo const& args) +{ + using FTraits = function_traits; + v8::Isolate* isolate = args.GetIsolate(); + F func = entry.func; + + if constexpr (std::is_member_function_pointer_v) + { + using class_type = std::decay_t; + auto obj = class_::unwrap_object(isolate, args.This()); + if (!obj) return false; + + if constexpr (std::same_as) + { + call_from_v8(std::move(func), args, entry.defs, *obj); + } + else + { + using return_type = typename FTraits::return_type; + using converter = typename call_from_v8_traits::template arg_converter; + args.GetReturnValue().Set(converter::to_v8(isolate, + call_from_v8(std::move(func), args, entry.defs, *obj))); + } + } + else + { + if constexpr (std::same_as) + { + call_from_v8(std::move(func), args, entry.defs); + } + else + { + using return_type = typename FTraits::return_type; + using converter = typename call_from_v8_traits::template arg_converter; + args.GetReturnValue().Set(converter::to_v8(isolate, + call_from_v8(std::move(func), args, entry.defs))); + } + } + return true; +} + +/// Call a constructor factory entry (no defaults), returning the constructed object. +/// Factory must be a free function or callable, not a member function pointer. +template +auto call_ctor_entry(overload_entry const& entry, + v8::FunctionCallbackInfo const& args) +{ + static_assert(!std::is_member_function_pointer_v, + "Constructor factory must be a free function or callable, not a member function pointer"); + F func = entry.func; + return call_from_v8(std::move(func), args); +} + +/// Call a constructor factory entry (with defaults), returning the constructed object. +template +auto call_ctor_entry(overload_entry> const& entry, + v8::FunctionCallbackInfo const& args) +{ + static_assert(!std::is_member_function_pointer_v, + "Constructor factory must be a free function or callable, not a member function pointer"); + F func = entry.func; + return call_from_v8(std::move(func), args, entry.defs); +} + +/// Get the function type from an overload_entry +template +struct entry_func_type; + +template +struct entry_func_type> +{ + using type = F; +}; + +/// Holds a set of overload entries +template +struct overload_set +{ + std::tuple entries; +}; + +/// V8 callback that dispatches to the matching overload (first-match-wins) +template +void forward_overloaded_function(v8::FunctionCallbackInfo const& args) +{ + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + auto& set = external_data::get(args.Data()); + size_t const arg_count = args.Length(); + + bool matched = false; + std::string errors; + + std::apply([&](auto const&... entries) { - using return_type = typename FTraits::return_type; - using converter = typename call_from_v8_traits::template arg_converter; - args.GetReturnValue().Set(converter::to_v8(isolate, - call_from_v8(std::move(func), args, *obj))); - } - } - else - { - if constexpr (std::same_as) - { - call_from_v8(std::move(func), args); - } - else - { - using return_type = typename FTraits::return_type; - using converter = typename call_from_v8_traits::template arg_converter; - args.GetReturnValue().Set(converter::to_v8(isolate, - call_from_v8(std::move(func), args))); - } - } - return true; -} - -/// Try to invoke one overload entry (with defaults). Returns true on success. -template -bool try_invoke_entry(overload_entry> const& entry, - v8::FunctionCallbackInfo const& args) -{ - using FTraits = function_traits; - v8::Isolate* isolate = args.GetIsolate(); - F func = entry.func; - - if constexpr (std::is_member_function_pointer_v) - { - using class_type = std::decay_t; - auto obj = class_::unwrap_object(isolate, args.This()); - if (!obj) return false; - - if constexpr (std::same_as) - { - call_from_v8(std::move(func), args, entry.defs, *obj); - } - else - { - using return_type = typename FTraits::return_type; - using converter = typename call_from_v8_traits::template arg_converter; - args.GetReturnValue().Set(converter::to_v8(isolate, - call_from_v8(std::move(func), args, entry.defs, *obj))); - } - } - else - { - if constexpr (std::same_as) - { - call_from_v8(std::move(func), args, entry.defs); - } - else - { - using return_type = typename FTraits::return_type; - using converter = typename call_from_v8_traits::template arg_converter; - args.GetReturnValue().Set(converter::to_v8(isolate, - call_from_v8(std::move(func), args, entry.defs))); - } - } - return true; -} - -/// Get the function type from an overload_entry -template -struct entry_func_type; - -template -struct entry_func_type> -{ - using type = F; -}; - -/// Holds a set of overload entries -template -struct overload_set -{ - std::tuple entries; -}; - -/// V8 callback that dispatches to the matching overload (first-match-wins) -template -void forward_overloaded_function(v8::FunctionCallbackInfo const& args) -{ - v8::Isolate* isolate = args.GetIsolate(); - v8::HandleScope scope(isolate); - - auto& set = external_data::get(args.Data()); - size_t const arg_count = args.Length(); - - bool matched = false; - std::string errors; - - std::apply([&](auto const&... entries) - { // Fold: short-circuit on first match via (matched || try_one()) ((matched || [&] { @@ -266,72 +300,72 @@ void forward_overloaded_function(v8::FunctionCallbackInfo const& args errors += ex.what(); return false; } - }()), ...); - }, set.entries); - - if (!matched) - { - std::string msg = "No matching overload for " + std::to_string(arg_count) + " argument(s)"; - if (!errors.empty()) - { - msg += ". Tried: " + errors; - } - args.GetReturnValue().Set(throw_ex(isolate, msg)); - } -} - -/// Helper: wrap a plain callable into overload_entry -template -auto make_overload_entry(F&& func) -{ - if constexpr (is_overload_entry>::value) - { - return std::forward(func); - } - else - { - return overload_entry, void>{std::forward(func)}; - } -} - -} // namespace v8pp::detail - -namespace v8pp { - -/// Wrap multiple overloaded functions into a single V8 function template -template -v8::Local wrap_overload_template(v8::Isolate* isolate, Funcs&&... funcs) -{ - using Set = detail::overload_set(funcs)))...>; - Set set{std::make_tuple(detail::make_overload_entry(std::forward(funcs))...)}; - return v8::FunctionTemplate::New(isolate, - &detail::forward_overloaded_function, - detail::external_data::set(isolate, std::move(set)), - v8::Local(), 0, - v8::ConstructorBehavior::kAllow, - v8::SideEffectType::kHasSideEffect); -} - -/// Wrap multiple overloaded functions into a single V8 function -template -v8::Local wrap_overload(v8::Isolate* isolate, std::string_view name, Funcs&&... funcs) -{ - using Set = detail::overload_set(funcs)))...>; - Set set{std::make_tuple(detail::make_overload_entry(std::forward(funcs))...)}; - v8::Local fn; - if (!v8::Function::New(isolate->GetCurrentContext(), - &detail::forward_overloaded_function, - detail::external_data::set(isolate, std::move(set)), - 0, v8::ConstructorBehavior::kAllow, - v8::SideEffectType::kHasSideEffect).ToLocal(&fn)) - { - return {}; - } - if (!name.empty()) - { - fn->SetName(to_v8_name(isolate, name)); - } - return fn; -} - -} // namespace v8pp + }()), ...); }, set.entries); + + if (!matched) + { + std::string msg = "No matching overload for " + std::to_string(arg_count) + " argument(s)"; + if (!errors.empty()) + { + msg += ". Tried: " + errors; + } + args.GetReturnValue().Set(throw_ex(isolate, msg)); + } +} + +/// Helper: wrap a plain callable into overload_entry +template +auto make_overload_entry(F&& func) +{ + if constexpr (is_overload_entry>::value) + { + return std::forward(func); + } + else + { + return overload_entry, void>{ std::forward(func) }; + } +} + +} // namespace v8pp::detail + +namespace v8pp { + +/// Wrap multiple overloaded functions into a single V8 function template +template +v8::Local wrap_overload_template(v8::Isolate* isolate, Funcs&&... funcs) +{ + using Set = detail::overload_set(funcs)))...>; + Set set{ std::make_tuple(detail::make_overload_entry(std::forward(funcs))...) }; + return v8::FunctionTemplate::New(isolate, + &detail::forward_overloaded_function, + detail::external_data::set(isolate, std::move(set)), + v8::Local(), 0, + v8::ConstructorBehavior::kAllow, + v8::SideEffectType::kHasSideEffect); +} + +/// Wrap multiple overloaded functions into a single V8 function +template +v8::Local wrap_overload(v8::Isolate* isolate, std::string_view name, Funcs&&... funcs) +{ + using Set = detail::overload_set(funcs)))...>; + Set set{ std::make_tuple(detail::make_overload_entry(std::forward(funcs))...) }; + v8::Local fn; + if (!v8::Function::New(isolate->GetCurrentContext(), + &detail::forward_overloaded_function, + detail::external_data::set(isolate, std::move(set)), + 0, v8::ConstructorBehavior::kAllow, + v8::SideEffectType::kHasSideEffect) + .ToLocal(&fn)) + { + return {}; + } + if (!name.empty()) + { + fn->SetName(to_v8_name(isolate, name)); + } + return fn; +} + +} // namespace v8pp diff --git a/v8pp/promise.hpp b/v8pp/promise.hpp index cbfc224..9ad8d1a 100644 --- a/v8pp/promise.hpp +++ b/v8pp/promise.hpp @@ -1,176 +1,176 @@ -#pragma once - -#include - -#include - -#include "v8pp/convert.hpp" -#include "v8pp/throw_ex.hpp" - -namespace v8pp { - -/// Synchronous promise wrapper around v8::Promise::Resolver. -/// Resolve/reject must be called on the isolate's thread. -/// -/// Usage: -/// v8pp::promise make_value(v8::Isolate* isolate) { -/// v8pp::promise p(isolate); -/// p.resolve(42); -/// return p; -/// } -/// module.function("makeValue", &make_value); -template -class promise -{ -public: - explicit promise(v8::Isolate* isolate) - : isolate_(isolate) - { - v8::HandleScope scope(isolate); - v8::Local context = isolate->GetCurrentContext(); - v8::Local resolver = - v8::Promise::Resolver::New(context).ToLocalChecked(); - resolver_.Reset(isolate, resolver); - } - - promise(promise const&) = delete; - promise& operator=(promise const&) = delete; - - promise(promise&&) = default; - promise& operator=(promise&&) = default; - - /// Resolve the promise with a C++ value (converted to V8 via convert) - void resolve(T const& value) - { - v8::HandleScope scope(isolate_); - v8::Local context = isolate_->GetCurrentContext(); - v8::Local resolver = to_local(isolate_, resolver_); - resolver->Resolve(context, v8pp::to_v8(isolate_, value)).FromJust(); - } - - /// Reject the promise with an error message (creates a JS Error) - void reject(std::string_view message) - { - v8::HandleScope scope(isolate_); - v8::Local context = isolate_->GetCurrentContext(); - v8::Local resolver = to_local(isolate_, resolver_); - v8::Local error = v8::Exception::Error( - v8pp::to_v8(isolate_, message)); - resolver->Reject(context, error).FromJust(); - } - - /// Reject the promise with a raw V8 value - void reject(v8::Local value) - { - v8::HandleScope scope(isolate_); - v8::Local context = isolate_->GetCurrentContext(); - v8::Local resolver = to_local(isolate_, resolver_); - resolver->Reject(context, value).FromJust(); - } - - /// Get the underlying v8::Promise (the "thenable" JS object) - v8::Local get_promise() const - { - v8::EscapableHandleScope scope(isolate_); - v8::Local resolver = to_local(isolate_, resolver_); - return scope.Escape(resolver->GetPromise()); - } - - v8::Isolate* isolate() const { return isolate_; } - -private: - v8::Isolate* isolate_; - v8::Global resolver_; -}; - -/// Specialization for void promises (signal-only, no value) -template<> -class promise -{ -public: - explicit promise(v8::Isolate* isolate) - : isolate_(isolate) - { - v8::HandleScope scope(isolate); - v8::Local context = isolate->GetCurrentContext(); - v8::Local resolver = - v8::Promise::Resolver::New(context).ToLocalChecked(); - resolver_.Reset(isolate, resolver); - } - - promise(promise const&) = delete; - promise& operator=(promise const&) = delete; - - promise(promise&&) = default; - promise& operator=(promise&&) = default; - - /// Resolve the void promise (with undefined) - void resolve() - { - v8::HandleScope scope(isolate_); - v8::Local context = isolate_->GetCurrentContext(); - v8::Local resolver = to_local(isolate_, resolver_); - resolver->Resolve(context, v8::Undefined(isolate_)).FromJust(); - } - - /// Reject the promise with an error message (creates a JS Error) - void reject(std::string_view message) - { - v8::HandleScope scope(isolate_); - v8::Local context = isolate_->GetCurrentContext(); - v8::Local resolver = to_local(isolate_, resolver_); - v8::Local error = v8::Exception::Error( - v8pp::to_v8(isolate_, message)); - resolver->Reject(context, error).FromJust(); - } - - /// Reject the promise with a raw V8 value - void reject(v8::Local value) - { - v8::HandleScope scope(isolate_); - v8::Local context = isolate_->GetCurrentContext(); - v8::Local resolver = to_local(isolate_, resolver_); - resolver->Reject(context, value).FromJust(); - } - - /// Get the underlying v8::Promise (the "thenable" JS object) - v8::Local get_promise() const - { - v8::EscapableHandleScope scope(isolate_); - v8::Local resolver = to_local(isolate_, resolver_); - return scope.Escape(resolver->GetPromise()); - } - - v8::Isolate* isolate() const { return isolate_; } - -private: - v8::Isolate* isolate_; - v8::Global resolver_; -}; - -// Exclude promise from is_wrapped_class so the convert system doesn't -// try to unwrap it as a bound C++ class -template -struct is_wrapped_class> : std::false_type -{ -}; - -/// convert> — to_v8 returns the JS promise object -template -struct convert> -{ - using from_type = promise; - using to_type = v8::Local; - - static bool is_valid(v8::Isolate*, v8::Local value) - { - return !value.IsEmpty() && value->IsPromise(); - } - - static to_type to_v8(v8::Isolate*, promise const& value) - { - return value.get_promise(); - } -}; - -} // namespace v8pp +#pragma once + +#include + +#include + +#include "v8pp/convert.hpp" +#include "v8pp/throw_ex.hpp" + +namespace v8pp { + +/// Synchronous promise wrapper around v8::Promise::Resolver. +/// Resolve/reject must be called on the isolate's thread. +/// +/// Usage: +/// v8pp::promise make_value(v8::Isolate* isolate) { +/// v8pp::promise p(isolate); +/// p.resolve(42); +/// return p; +/// } +/// module.function("makeValue", &make_value); +template +class promise +{ +public: + explicit promise(v8::Isolate* isolate) + : isolate_(isolate) + { + v8::HandleScope scope(isolate); + v8::Local context = isolate->GetCurrentContext(); + v8::Local resolver = + v8::Promise::Resolver::New(context).ToLocalChecked(); + resolver_.Reset(isolate, resolver); + } + + promise(promise const&) = delete; + promise& operator=(promise const&) = delete; + + promise(promise&&) = default; + promise& operator=(promise&&) = default; + + /// Resolve the promise with a C++ value (converted to V8 via convert) + void resolve(T const& value) + { + v8::HandleScope scope(isolate_); + v8::Local context = isolate_->GetCurrentContext(); + v8::Local resolver = to_local(isolate_, resolver_); + resolver->Resolve(context, v8pp::to_v8(isolate_, value)).FromJust(); + } + + /// Reject the promise with an error message (creates a JS Error) + void reject(std::string_view message) + { + v8::HandleScope scope(isolate_); + v8::Local context = isolate_->GetCurrentContext(); + v8::Local resolver = to_local(isolate_, resolver_); + v8::Local error = v8::Exception::Error( + v8pp::to_v8(isolate_, message)); + resolver->Reject(context, error).FromJust(); + } + + /// Reject the promise with a raw V8 value + void reject(v8::Local value) + { + v8::HandleScope scope(isolate_); + v8::Local context = isolate_->GetCurrentContext(); + v8::Local resolver = to_local(isolate_, resolver_); + resolver->Reject(context, value).FromJust(); + } + + /// Get the underlying v8::Promise (the "thenable" JS object) + v8::Local get_promise() const + { + v8::EscapableHandleScope scope(isolate_); + v8::Local resolver = to_local(isolate_, resolver_); + return scope.Escape(resolver->GetPromise()); + } + + v8::Isolate* isolate() const { return isolate_; } + +private: + v8::Isolate* isolate_; + v8::Global resolver_; +}; + +/// Specialization for void promises (signal-only, no value) +template<> +class promise +{ +public: + explicit promise(v8::Isolate* isolate) + : isolate_(isolate) + { + v8::HandleScope scope(isolate); + v8::Local context = isolate->GetCurrentContext(); + v8::Local resolver = + v8::Promise::Resolver::New(context).ToLocalChecked(); + resolver_.Reset(isolate, resolver); + } + + promise(promise const&) = delete; + promise& operator=(promise const&) = delete; + + promise(promise&&) = default; + promise& operator=(promise&&) = default; + + /// Resolve the void promise (with undefined) + void resolve() + { + v8::HandleScope scope(isolate_); + v8::Local context = isolate_->GetCurrentContext(); + v8::Local resolver = to_local(isolate_, resolver_); + resolver->Resolve(context, v8::Undefined(isolate_)).FromJust(); + } + + /// Reject the promise with an error message (creates a JS Error) + void reject(std::string_view message) + { + v8::HandleScope scope(isolate_); + v8::Local context = isolate_->GetCurrentContext(); + v8::Local resolver = to_local(isolate_, resolver_); + v8::Local error = v8::Exception::Error( + v8pp::to_v8(isolate_, message)); + resolver->Reject(context, error).FromJust(); + } + + /// Reject the promise with a raw V8 value + void reject(v8::Local value) + { + v8::HandleScope scope(isolate_); + v8::Local context = isolate_->GetCurrentContext(); + v8::Local resolver = to_local(isolate_, resolver_); + resolver->Reject(context, value).FromJust(); + } + + /// Get the underlying v8::Promise (the "thenable" JS object) + v8::Local get_promise() const + { + v8::EscapableHandleScope scope(isolate_); + v8::Local resolver = to_local(isolate_, resolver_); + return scope.Escape(resolver->GetPromise()); + } + + v8::Isolate* isolate() const { return isolate_; } + +private: + v8::Isolate* isolate_; + v8::Global resolver_; +}; + +// Exclude promise from is_wrapped_class so the convert system doesn't +// try to unwrap it as a bound C++ class +template +struct is_wrapped_class> : std::false_type +{ +}; + +/// convert> — to_v8 returns the JS promise object +template +struct convert> +{ + using from_type = promise; + using to_type = v8::Local; + + static bool is_valid(v8::Isolate*, v8::Local value) + { + return !value.IsEmpty() && value->IsPromise(); + } + + static to_type to_v8(v8::Isolate*, promise const& value) + { + return value.get_promise(); + } +}; + +} // namespace v8pp diff --git a/v8pp/property.hpp b/v8pp/property.hpp index 6b99387..b91021b 100644 --- a/v8pp/property.hpp +++ b/v8pp/property.hpp @@ -1,235 +1,224 @@ -#pragma once - -#include "v8pp/convert.hpp" -#include "v8pp/function.hpp" - -namespace v8pp::detail { - -template::template arg_type<0>> -inline constexpr bool function_with_object = std::is_member_function_pointer_v || - (std::is_lvalue_reference_v && std::is_base_of_v>>); - -template> -inline constexpr bool is_getter = CallTraits::arg_count == 0 + Offset - && !std::same_as::return_type, void>; - -template> -inline constexpr bool is_direct_getter = CallTraits::arg_count == 2 + Offset - && std::is_convertible_v, v8::Local> - && std::same_as, v8::PropertyCallbackInfo const&> - && std::same_as::return_type, void>; - -template> -inline constexpr bool is_isolate_getter = CallTraits::arg_count == 1 + Offset - && is_first_arg_isolate - && !std::same_as::return_type, void>; - -template> -inline constexpr bool is_setter = CallTraits::arg_count == 1 + Offset; - -template> -inline constexpr bool is_direct_setter = CallTraits::arg_count == 3 + Offset - && std::is_convertible_v, v8::Local> - && std::same_as, v8::Local> - && std::same_as, v8::PropertyCallbackInfo const&> - && std::same_as::return_type, void>; - -template> -inline constexpr bool is_isolate_setter = CallTraits::arg_count == 2 + Offset - && is_first_arg_isolate; - -template -void property_get(Get& getter, v8::Local name, - v8::PropertyCallbackInfo const& info, ObjArg&... obj) -{ - constexpr size_t offset = sizeof...(ObjArg) == 0 || std::is_member_function_pointer_v ? 0 : 1; - - v8::Isolate* isolate = info.GetIsolate(); - - if constexpr (is_direct_getter) - { - (void)isolate; - std::invoke(getter, obj..., name, info); - } - else if constexpr (is_isolate_getter) - { - (void)name; - info.GetReturnValue().Set(to_v8(isolate, std::invoke(getter, obj..., isolate))); - } - else if constexpr (is_getter) - { - (void)name; - info.GetReturnValue().Set(to_v8(isolate, std::invoke(getter, obj...))); - } - else - { - (void)getter; - (void)name; - (void)info; - (void)isolate; - //static_assert(false, "Unsupported getter type"); - } -} - -template -void property_set(Set& setter, v8::Local name, v8::Local value, - v8::PropertyCallbackInfo const& info, ObjArg&... obj) -{ - constexpr size_t offset = sizeof...(ObjArg) == 0 || std::is_member_function_pointer_v ? 0 : 1; - - v8::Isolate* isolate = info.GetIsolate(); - - if constexpr (is_direct_setter) - { - (void)isolate; - std::invoke(setter, obj..., name, value, info); - } - else if constexpr (is_isolate_setter) - { - (void)name; - using value_type = typename call_from_v8_traits::template arg_type<1 + offset>; - std::invoke(setter, obj..., isolate, v8pp::from_v8(isolate, value)); - } - else if constexpr (is_setter) - { - (void)name; - using value_type = typename call_from_v8_traits::template arg_type<0 + offset>; - std::invoke(setter, obj..., v8pp::from_v8(isolate, value)); - } - else - { - (void)setter; - (void)name; - (void)value; - (void)info; - (void)isolate; - //static_assert(false, "Unsupported setter type"); - } -} - -template -void property_get(v8::Local name, v8::PropertyCallbackInfo const& info) -try -{ - auto&& property = detail::external_data::get(info.Data()); - - if constexpr (std::same_as) - { - property_get(property.getter, name, info); - } - else - { - auto obj = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); - if (!obj) - { - info.GetReturnValue().Set(throw_ex(info.GetIsolate(), "accessing property on non-existent C++ object")); - return; - } - property_get(property.getter, name, info, *obj); - } -} -catch (std::exception const& ex) -{ - if (info.ShouldThrowOnError()) - { - info.GetReturnValue().Set(throw_ex(info.GetIsolate(), ex.what())); - } -} - -template -void property_set(v8::Local name, v8::Local value, v8::PropertyCallbackInfo const& info) -try -{ - auto&& property = detail::external_data::get(info.Data()); - - if constexpr (std::same_as) - { - property_set(property.setter, name, value, info); - } - else - { - auto obj = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); - if (!obj) - { - info.GetIsolate()->ThrowException(throw_ex(info.GetIsolate(), "setting property on non-existent C++ object")); - return; - } - property_set(property.setter, name, value, info, *obj); - } -} -catch (std::exception const& ex) -{ - if (info.ShouldThrowOnError()) - { - info.GetIsolate()->ThrowException(throw_ex(info.GetIsolate(), ex.what())); - } - // TODO: info.GetReturnValue().Set(false); -} - -} // namespace v8pp::detail - -namespace v8pp { - -/// Property with get and set functions -template -struct property final -{ - Get getter; - Set setter; - - static constexpr bool is_readonly = false; - - property() = default; - property(Get&& getter, Set&& setter) - : getter(std::move(getter)) - , setter(std::move(setter)) - { - } - - template - static void get(v8::Local name, v8::PropertyCallbackInfo const& info) - { - detail::property_get(name, info); - } - - template - static void set(v8::Local name, v8::Local value, v8::PropertyCallbackInfo const& info) - { - detail::property_set(name, value, info); - } -}; - -/// Read-only property class specialization for get only method -template -struct property final -{ - Get getter; - - static constexpr bool is_readonly = true; - - property() = default; - property(Get&& getter, detail::none) - : getter(std::move(getter)) - { - } - - template - static void get(v8::Local name, v8::PropertyCallbackInfo const& info) - { - detail::property_get(name, info); - } - - template - static void set(v8::Local name, v8::Local, v8::PropertyCallbackInfo const& info) - { - //assert(false && "read-only property"); - if (info.ShouldThrowOnError()) - { - info.GetIsolate()->ThrowException(v8pp::to_v8(info.GetIsolate(), - "read-only property " + from_v8(info.GetIsolate(), name))); - } - // TODO: info.GetReturnValue().Set(false); - } -}; - -} // namespace v8pp +#pragma once + +#include "v8pp/convert.hpp" +#include "v8pp/function.hpp" + +namespace v8pp::detail { + +template::template arg_type<0>> +inline constexpr bool function_with_object = std::is_member_function_pointer_v || + (std::is_lvalue_reference_v && std::is_base_of_v>>); + +template> +inline constexpr bool is_getter = CallTraits::arg_count == 0 + Offset && !std::same_as::return_type, void>; + +template> +inline constexpr bool is_direct_getter = CallTraits::arg_count == 2 + Offset && std::is_convertible_v, v8::Local> && std::same_as, v8::PropertyCallbackInfo const&> && std::same_as::return_type, void>; + +template> +inline constexpr bool is_isolate_getter = CallTraits::arg_count == 1 + Offset && is_first_arg_isolate && !std::same_as::return_type, void>; + +template> +inline constexpr bool is_setter = CallTraits::arg_count == 1 + Offset; + +template> +inline constexpr bool is_direct_setter = CallTraits::arg_count == 3 + Offset && std::is_convertible_v, v8::Local> && std::same_as, v8::Local> && std::same_as, v8::PropertyCallbackInfo const&> && std::same_as::return_type, void>; + +template> +inline constexpr bool is_isolate_setter = CallTraits::arg_count == 2 + Offset && is_first_arg_isolate; + +template +void property_get(Get& getter, v8::Local name, + v8::PropertyCallbackInfo const& info, ObjArg&... obj) +{ + constexpr size_t offset = sizeof...(ObjArg) == 0 || std::is_member_function_pointer_v ? 0 : 1; + + v8::Isolate* isolate = info.GetIsolate(); + + if constexpr (is_direct_getter) + { + (void)isolate; + std::invoke(getter, obj..., name, info); + } + else if constexpr (is_isolate_getter) + { + (void)name; + info.GetReturnValue().Set(to_v8(isolate, std::invoke(getter, obj..., isolate))); + } + else if constexpr (is_getter) + { + (void)name; + info.GetReturnValue().Set(to_v8(isolate, std::invoke(getter, obj...))); + } + else + { + (void)getter; + (void)name; + (void)info; + (void)isolate; + // static_assert(false, "Unsupported getter type"); + } +} + +template +void property_set(Set& setter, v8::Local name, v8::Local value, + v8::PropertyCallbackInfo const& info, ObjArg&... obj) +{ + constexpr size_t offset = sizeof...(ObjArg) == 0 || std::is_member_function_pointer_v ? 0 : 1; + + v8::Isolate* isolate = info.GetIsolate(); + + if constexpr (is_direct_setter) + { + (void)isolate; + std::invoke(setter, obj..., name, value, info); + } + else if constexpr (is_isolate_setter) + { + (void)name; + using value_type = typename call_from_v8_traits::template arg_type<1 + offset>; + std::invoke(setter, obj..., isolate, v8pp::from_v8(isolate, value)); + } + else if constexpr (is_setter) + { + (void)name; + using value_type = typename call_from_v8_traits::template arg_type<0 + offset>; + std::invoke(setter, obj..., v8pp::from_v8(isolate, value)); + } + else + { + (void)setter; + (void)name; + (void)value; + (void)info; + (void)isolate; + // static_assert(false, "Unsupported setter type"); + } +} + +template +void property_get(v8::Local name, v8::PropertyCallbackInfo const& info) +try +{ + auto&& property = detail::external_data::get(info.Data()); + + if constexpr (std::same_as) + { + property_get(property.getter, name, info); + } + else + { + auto obj = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (!obj) + { + info.GetReturnValue().Set(throw_ex(info.GetIsolate(), "accessing property on non-existent C++ object")); + return; + } + property_get(property.getter, name, info, *obj); + } +} +catch (std::exception const& ex) +{ + if (info.ShouldThrowOnError()) + { + info.GetReturnValue().Set(throw_ex(info.GetIsolate(), ex.what())); + } +} + +template +void property_set(v8::Local name, v8::Local value, v8::PropertyCallbackInfo const& info) +try +{ + auto&& property = detail::external_data::get(info.Data()); + + if constexpr (std::same_as) + { + property_set(property.setter, name, value, info); + } + else + { + auto obj = v8pp::class_::unwrap_object(info.GetIsolate(), info.This()); + if (!obj) + { + info.GetIsolate()->ThrowException(throw_ex(info.GetIsolate(), "setting property on non-existent C++ object")); + return; + } + property_set(property.setter, name, value, info, *obj); + } +} +catch (std::exception const& ex) +{ + if (info.ShouldThrowOnError()) + { + info.GetIsolate()->ThrowException(throw_ex(info.GetIsolate(), ex.what())); + } + // TODO: info.GetReturnValue().Set(false); +} + +} // namespace v8pp::detail + +namespace v8pp { + +/// Property with get and set functions +template +struct property final +{ + Get getter; + Set setter; + + static constexpr bool is_readonly = false; + + property() = default; + property(Get&& getter, Set&& setter) + : getter(std::move(getter)) + , setter(std::move(setter)) + { + } + + template + static void get(v8::Local name, v8::PropertyCallbackInfo const& info) + { + detail::property_get(name, info); + } + + template + static void set(v8::Local name, v8::Local value, v8::PropertyCallbackInfo const& info) + { + detail::property_set(name, value, info); + } +}; + +/// Read-only property class specialization for get only method +template +struct property final +{ + Get getter; + + static constexpr bool is_readonly = true; + + property() = default; + property(Get&& getter, detail::none) + : getter(std::move(getter)) + { + } + + template + static void get(v8::Local name, v8::PropertyCallbackInfo const& info) + { + detail::property_get(name, info); + } + + template + static void set(v8::Local name, v8::Local, v8::PropertyCallbackInfo const& info) + { + // assert(false && "read-only property"); + if (info.ShouldThrowOnError()) + { + info.GetIsolate()->ThrowException(v8pp::to_v8(info.GetIsolate(), + "read-only property " + from_v8(info.GetIsolate(), name))); + } + // TODO: info.GetReturnValue().Set(false); + } +}; + +} // namespace v8pp diff --git a/v8pp/ptr_traits.hpp b/v8pp/ptr_traits.hpp index 012ce7d..04d40d7 100644 --- a/v8pp/ptr_traits.hpp +++ b/v8pp/ptr_traits.hpp @@ -23,7 +23,10 @@ struct raw_ptr_traits static pointer_type key(object_id id) { return id; } static pointer_type const_pointer_cast(const_pointer_type ptr) { return const_cast(ptr); } template - static T* static_pointer_cast(U* ptr) { return static_cast(ptr); } + static T* static_pointer_cast(U* ptr) + { + return static_cast(ptr); + } template using convert_ptr = convert; @@ -73,10 +76,16 @@ struct shared_ptr_traits using object_id = void*; static object_id pointer_id(pointer_type const& ptr) { return ptr.get(); } - static pointer_type key(object_id id) { return std::shared_ptr(id, [](void*) {}); } + static pointer_type key(object_id id) + { + return std::shared_ptr(id, [](void*) {}); + } static pointer_type const_pointer_cast(const_pointer_type const& ptr) { return std::const_pointer_cast(ptr); } template - static std::shared_ptr static_pointer_cast(std::shared_ptr const& ptr) { return std::static_pointer_cast(ptr); } + static std::shared_ptr static_pointer_cast(std::shared_ptr const& ptr) + { + return std::static_pointer_cast(ptr); + } template using convert_ptr = convert>; @@ -109,4 +118,4 @@ struct shared_ptr_traits } }; -} //namespace v8pp +} // namespace v8pp diff --git a/v8pp/throw_ex.ipp b/v8pp/throw_ex.ipp index 3eb7386..86f61e5 100644 --- a/v8pp/throw_ex.ipp +++ b/v8pp/throw_ex.ipp @@ -5,7 +5,8 @@ namespace v8pp { V8PP_IMPL v8::Local throw_ex(v8::Isolate* isolate, std::string_view message, exception_ctor ctor, v8::Local exception_options) { v8::Local msg = v8::String::NewFromUtf8(isolate, message.data(), - v8::NewStringType::kNormal, static_cast(message.size())).ToLocalChecked(); + v8::NewStringType::kNormal, static_cast(message.size())) + .ToLocalChecked(); // if constexpr (exception_ctor_with_options) doesn't work win VC++ 2022 #if V8_MAJOR_VERSION > 11 || (V8_MAJOR_VERSION == 11 && V8_MINOR_VERSION >= 9) diff --git a/v8pp/type_info.hpp b/v8pp/type_info.hpp index 416c54e..b329aa9 100644 --- a/v8pp/type_info.hpp +++ b/v8pp/type_info.hpp @@ -13,7 +13,7 @@ class type_info public: constexpr uintptr_t id() const { return id_; } constexpr std::string_view name() const { return name_; } - + constexpr bool operator==(type_info const& other) const { return id_ == other.id_; } constexpr bool operator!=(type_info const& other) const { return id_ != other.id_; } diff --git a/v8pp/utility.hpp b/v8pp/utility.hpp index 4992430..2d6a297 100644 --- a/v8pp/utility.hpp +++ b/v8pp/utility.hpp @@ -132,12 +132,11 @@ struct is_array> : std::true_type // is_set — set-like containers (insert but not emplace_back) // template -concept set_like = !is_string::value && !mapping && !sequence - && requires(T t, typename T::value_type v) { - t.begin(); - t.end(); - t.insert(std::move(v)); - }; +concept set_like = !is_string::value && !mapping && !sequence && requires(T t, typename T::value_type v) { + t.begin(); + t.end(); + t.insert(std::move(v)); +}; template struct is_set : std::bool_constant> @@ -255,7 +254,7 @@ struct function_traits }; template -struct function_traits +struct function_traits { using return_type = R; using arguments = std::tuple; @@ -266,14 +265,14 @@ struct function_traits // function pointer template struct function_traits - : function_traits + : function_traits { }; // member function pointer template struct function_traits - : function_traits + : function_traits { using class_type = C; template @@ -283,7 +282,7 @@ struct function_traits // const member function pointer template struct function_traits - : function_traits + : function_traits { using class_type = C const; template @@ -293,7 +292,7 @@ struct function_traits // volatile member function pointer template struct function_traits - : function_traits + : function_traits { using class_type = C volatile; template @@ -303,7 +302,7 @@ struct function_traits // const volatile member function pointer template struct function_traits - : function_traits + : function_traits { using class_type = C const volatile; template @@ -312,42 +311,42 @@ struct function_traits // member object pointer template -struct function_traits - : function_traits +struct function_traits + : function_traits { using class_type = C; template - using pointer_type = R (D::*); + using pointer_type = R(D::*); }; // const member object pointer template -struct function_traits - : function_traits +struct function_traits + : function_traits { using class_type = C const; template - using pointer_type = const R (D::*); + using pointer_type = const R(D::*); }; // volatile member object pointer template -struct function_traits - : function_traits +struct function_traits + : function_traits { using class_type = C volatile; template - using pointer_type = volatile R (D::*); + using pointer_type = volatile R(D::*); }; // const volatile member object pointer template -struct function_traits - : function_traits +struct function_traits + : function_traits { using class_type = C const volatile; template - using pointer_type = const volatile R (D::*); + using pointer_type = const volatile R(D::*); }; // function object, std::function, lambda @@ -378,8 +377,7 @@ struct function_traits : function_traits }; template -concept callable = std::is_function_v> - || requires { &F::operator(); }; +concept callable = std::is_function_v> || requires { &F::operator(); }; template using is_callable = std::bool_constant>; @@ -388,7 +386,7 @@ template inline constexpr bool is_const_member_function_v = false; template - requires std::is_member_function_pointer_v +requires std::is_member_function_pointer_v inline constexpr bool is_const_member_function_v = std::is_const_v::class_type>; diff --git a/v8pp/version.hpp b/v8pp/version.hpp index 6a7380f..0d769f0 100644 --- a/v8pp/version.hpp +++ b/v8pp/version.hpp @@ -12,7 +12,7 @@ unsigned version_patch(); char const* build_options(); -} +} // namespace v8pp #if V8PP_HEADER_ONLY #include "v8pp/version.ipp" diff --git a/v8pp/version.ipp b/v8pp/version.ipp index 8d76661..49043fc 100644 --- a/v8pp/version.ipp +++ b/v8pp/version.ipp @@ -28,24 +28,24 @@ V8PP_IMPL char const* build_options() #define STR(opt) #opt "=" V8PP_STRINGIZE(opt) " " return "" #ifdef V8PP_ISOLATE_DATA_SLOT - STR(V8PP_ISOLATE_DATA_SLOT) + STR(V8PP_ISOLATE_DATA_SLOT) #endif #ifdef V8PP_HEADER_ONLY - STR(V8PP_HEADER_ONLY) + STR(V8PP_HEADER_ONLY) #endif #ifdef V8PP_PLUGIN_INIT_PROC_NAME - STR(V8PP_PLUGIN_INIT_PROC_NAME) + STR(V8PP_PLUGIN_INIT_PROC_NAME) #endif #ifdef V8PP_PLUGIN_SUFFIX - STR(V8PP_PLUGIN_SUFFIX) + STR(V8PP_PLUGIN_SUFFIX) #endif #ifdef V8_COMPRESS_POINTERS - STR(V8_COMPRESS_POINTERS) + STR(V8_COMPRESS_POINTERS) #endif #ifdef V8_ENABLE_SANDBOX - STR(V8_ENABLE_SANDBOX) + STR(V8_ENABLE_SANDBOX) #endif - ; + ; #undef STR }