From cc2417fbebf7e46aa296e74755f0adf4fc8f1a4f Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:10:17 +0100 Subject: [PATCH 01/38] bump minimum supported V8 is 9.0+ (vcpkg version) --- v8pp/context.cpp | 23 ----------------------- v8pp/convert.hpp | 4 ---- 2 files changed, 27 deletions(-) diff --git a/v8pp/context.cpp b/v8pp/context.cpp index 515b663..0bed65e 100644 --- a/v8pp/context.cpp +++ b/v8pp/context.cpp @@ -129,33 +129,10 @@ void context::run_file(v8::FunctionCallbackInfo const& args) args.GetReturnValue().Set(scope.Escape(result)); } -#if V8_MAJOR_VERSION < 5 || (V8_MAJOR_VERSION == 5 && V8_MINOR_VERSION < 4) -struct array_buffer_allocator : v8::ArrayBuffer::Allocator -{ - void* Allocate(size_t length) - { - return calloc(length, 1); - } - void* AllocateUninitialized(size_t length) - { - return malloc(length); - } - void Free(void* data, size_t length) - { - free(data); - (void)length; - } -}; -#endif - v8::Isolate* context::create_isolate(v8::ArrayBuffer::Allocator* allocator) { v8::Isolate::CreateParams create_params; -#if V8_MAJOR_VERSION < 5 || (V8_MAJOR_VERSION == 5 && V8_MINOR_VERSION < 4) - create_params.array_buffer_allocator = allocator ? allocator : new array_buffer_allocator; -#else create_params.array_buffer_allocator = allocator ? allocator : v8::ArrayBuffer::Allocator::NewDefaultAllocator(); -#endif return v8::Isolate::New(create_params); } diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index cb73d44..d196936 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -224,11 +224,7 @@ struct convert { throw invalid_argument(isolate, value, "Boolean"); } -#if (V8_MAJOR_VERSION > 7) || (V8_MAJOR_VERSION == 7 && V8_MINOR_VERSION >= 1) return value->BooleanValue(isolate); -#else - return value->BooleanValue(isolate->GetCurrentContext()).FromJust(); -#endif } static to_type to_v8(v8::Isolate* isolate, bool value) From db7572df81f3b5140166a8ed1be202ed5bb17911 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:27:44 +0100 Subject: [PATCH 02/38] fix script-reachable ToLocalChecked crashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - convert.hpp: ToString() → ToLocal + throw (proxy/throwing toString) - convert.hpp: GetPropertyNames() → ToLocal + throw (Proxy traps) - function.hpp: Function::New() → ToLocal + return empty - json.ipp: Stringify/GetPropertyNames → ToLocal with fallbacks - json.ipp: Set().FromJust() → FromMaybe(false) - class.hpp: extract isolate/context temps in static_() --- v8pp/class.hpp | 8 ++++++-- v8pp/convert.hpp | 12 ++++++++++-- v8pp/function.hpp | 8 ++++++-- v8pp/json.ipp | 14 +++++++++++--- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/v8pp/class.hpp b/v8pp/class.hpp index 490f204..4e43664 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -428,9 +428,13 @@ class class_ template class_& static_(std::string_view const& name, Value const& value, bool readonly = false) { - v8::HandleScope scope(isolate()); + v8::Isolate* iso = isolate(); + v8::HandleScope scope(iso); + v8::Local context = iso->GetCurrentContext(); - class_info_.js_function_template()->GetFunction(isolate()->GetCurrentContext()).ToLocalChecked()->DefineOwnProperty(isolate()->GetCurrentContext(), v8pp::to_v8(isolate(), name), to_v8(isolate(), value), v8::PropertyAttribute(v8::DontDelete | (readonly ? v8::ReadOnly : 0))).FromJust(); + class_info_.js_function_template()->GetFunction(context).ToLocalChecked() + ->DefineOwnProperty(context, v8pp::to_v8(iso, name), to_v8(iso, value), + v8::PropertyAttribute(v8::DontDelete | (readonly ? v8::ReadOnly : 0))).FromJust(); return *this; } diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index d196936..146982f 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -82,7 +82,11 @@ struct convert::value> } v8::HandleScope scope(isolate); - v8::Local str = value->ToString(isolate->GetCurrentContext()).ToLocalChecked(); + v8::Local str; + if (!value->ToString(isolate->GetCurrentContext()).ToLocal(&str)) + { + throw invalid_argument(isolate, value, "String"); + } #if V8_MAJOR_VERSION > 13 || (V8_MAJOR_VERSION == 13 && V8_MINOR_VERSION >= 3) if constexpr (sizeof(Char) == 1) @@ -727,7 +731,11 @@ struct convert::val v8::HandleScope scope(isolate); v8::Local context = isolate->GetCurrentContext(); v8::Local object = value.As(); - v8::Local prop_names = object->GetPropertyNames(context).ToLocalChecked(); + v8::Local prop_names; + if (!object->GetPropertyNames(context).ToLocal(&prop_names)) + { + throw invalid_argument(isolate, value, "Object"); + } from_type result{}; for (uint32_t i = 0, count = prop_names->Length(); i < count; ++i) diff --git a/v8pp/function.hpp b/v8pp/function.hpp index 04db86f..7564cba 100644 --- a/v8pp/function.hpp +++ b/v8pp/function.hpp @@ -199,9 +199,13 @@ template v8::Local wrap_function(v8::Isolate* isolate, std::string_view name, F&& func) { using F_type = typename std::decay_t; - v8::Local fn = v8::Function::New(isolate->GetCurrentContext(), + v8::Local fn; + if (!v8::Function::New(isolate->GetCurrentContext(), &detail::forward_function, - detail::external_data::set(isolate, std::forward(func))).ToLocalChecked(); + detail::external_data::set(isolate, std::forward(func))).ToLocal(&fn)) + { + return {}; + } if (!name.empty()) { fn->SetName(to_v8(isolate, name)); diff --git a/v8pp/json.ipp b/v8pp/json.ipp index 050d3a4..63bda28 100644 --- a/v8pp/json.ipp +++ b/v8pp/json.ipp @@ -13,7 +13,11 @@ V8PP_IMPL std::string json_str(v8::Isolate* isolate, v8::Local value) v8::HandleScope scope(isolate); v8::Local context = isolate->GetCurrentContext(); - v8::Local result = v8::JSON::Stringify(context, value).ToLocalChecked(); + v8::Local result; + if (!v8::JSON::Stringify(context, value).ToLocal(&result)) + { + return std::string(); + } return v8pp::from_v8(isolate, result); } @@ -45,7 +49,11 @@ V8PP_IMPL v8::Local json_object(v8::Isolate* isolate, v8::Local context = isolate->GetCurrentContext(); v8::Local result = v8::Object::New(isolate); - v8::Local prop_names = object->GetPropertyNames(context).ToLocalChecked(); + v8::Local prop_names; + if (!object->GetPropertyNames(context).ToLocal(&prop_names)) + { + return scope.Escape(result); + } for (uint32_t i = 0, count = prop_names->Length(); i < count; ++i) { v8::Local name, value; @@ -61,7 +69,7 @@ V8PP_IMPL v8::Local json_object(v8::Isolate* isolate, v8::LocalSet(context, name, value).FromJust(); + result->Set(context, name, value).FromMaybe(false); } } return scope.Escape(result); From 941c92d00923104ebf0bf090fb0ff69ebe2b9d04 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:51:12 +0100 Subject: [PATCH 03/38] add try_from_v8 exception-free conversion returning std::optional Add try_from_v8 static method to every convert specialization, combining validation and extraction in a single pass without exceptions. For simple converters (bool, int, float), from_v8 now delegates to try_from_v8 to eliminate code duplication. String converter extracts a shared extract_string helper. Sequence and mapping converters move their full extraction logic into try_from_v8 with from_v8 delegating to it. Free function v8pp::try_from_v8() uses a requires-expression to detect native try_from_v8 support, falling back to is_valid + from_v8 for user-defined converters via a [[deprecated]] helper that emits a compile-time warning encouraging users to add try_from_v8. Add comprehensive tests covering primitives, strings, enums, sequences, maps, tuples, optionals, and wrapped classes via raw and shared pointers. --- test/test_convert.cpp | 109 +++++++++++++++++++++++ v8pp/convert.hpp | 197 +++++++++++++++++++++++++++++++++--------- 2 files changed, 266 insertions(+), 40 deletions(-) diff --git a/test/test_convert.cpp b/test/test_convert.cpp index b2affb6..1454639 100644 --- a/test/test_convert.cpp +++ b/test/test_convert.cpp @@ -450,6 +450,114 @@ void test_convert_variant(v8::Isolate* isolate) optional_check(0, std::optional{}, false); } +void test_convert_try_from_v8(v8::Isolate* isolate) +{ + // Primitives: valid conversions return value + auto int_result = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, 42)); + check("try int valid", int_result.has_value()); + check_eq("try int value", *int_result, 42); + + auto uint_result = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, 100u)); + check("try uint valid", uint_result.has_value()); + check_eq("try uint value", *uint_result, 100u); + + auto double_result = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, 3.14)); + check("try double valid", double_result.has_value()); + check_eq("try double value", *double_result, 3.14); + + auto bool_result = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, true)); + check("try bool valid", bool_result.has_value()); + check_eq("try bool value", *bool_result, true); + + // Primitives: type mismatch returns nullopt + check("try int from string", !v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, "hello"))); + check("try int from bool", !v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, true))); + check("try bool from int", !v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, 42))); + check("try int from undefined", !v8pp::try_from_v8(isolate, v8::Undefined(isolate))); + check("try int from null", !v8pp::try_from_v8(isolate, v8::Null(isolate))); + check("try int from empty", !v8pp::try_from_v8(isolate, v8::Local())); + + // Strings: valid conversion + auto str_result = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, "hello")); + check("try string valid", str_result.has_value()); + check_eq("try string value", *str_result, std::string("hello")); + + // Strings: any non-empty value converts to string (via toString) + auto str_from_int = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, 42)); + check("try string from int", str_from_int.has_value()); + check_eq("try string from int value", *str_from_int, std::string("42")); + + // Strings: empty handle returns nullopt + check("try string from empty", !v8pp::try_from_v8(isolate, v8::Local())); + + // Enums + 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); + check("try enum from string", !v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, "red"))); + + // Sequences + auto vec_result = v8pp::try_from_v8>(isolate, + 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("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_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); + check("try tuple from int", !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, 42))); + + // Optional: undefined/null returns optional containing nullopt + auto opt_undef = v8pp::try_from_v8>(isolate, v8::Undefined(isolate)); + check("try optional undef valid", opt_undef.has_value()); + check("try optional undef is nullopt", !opt_undef->has_value()); + + auto opt_val = v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, 42)); + check("try optional valid", opt_val.has_value()); + check("try optional has value", opt_val->has_value()); + check_eq("try optional value", **opt_val, 42); + + // Optional: wrong type returns outer nullopt + check("try optional from string", + !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, "hello"))); + + // Wrapped class: valid unwrap + v8pp::class_ U_class(isolate); + U_class.template ctor<>().auto_wrap_objects(true); + + 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); + check("try U* valid", u_ptr_result.has_value()); + check_eq("try U* value", (*u_ptr_result)->value, 42); + + // Wrapped class: wrong object type returns nullopt + check("try U* from int", !v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, 42))); + check("try U* from plain object", !v8pp::try_from_v8(isolate, v8::Object::New(isolate))); + + // Wrapped class via shared_ptr + v8pp::class_ V_class(isolate); + V_class.template ctor<>().auto_wrap_objects(true); + + auto v_obj = std::make_shared(V{"test"}); + V_class.reference_external(isolate, v_obj); + auto v_v8 = v8pp::class_::find_object(isolate, v_obj); + + auto v_result = v8pp::try_from_v8>(isolate, v_v8); + check("try shared_ptr valid", v_result.has_value()); + check_eq("try shared_ptr value", (*v_result)->value, std::string("test")); + + check("try shared_ptr from int", !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, 42))); +} + void test_convert() { v8pp::context context; @@ -503,4 +611,5 @@ void test_convert() test_convert_optional(isolate); test_convert_tuple(isolate); test_convert_variant(isolate); + test_convert_try_from_v8(isolate); } diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index 146982f..e96ce4c 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -76,18 +76,46 @@ struct convert::value> static from_type from_v8(v8::Isolate* isolate, v8::Local value) { - if (!is_valid(isolate, value)) + if (auto result = try_from_v8(isolate, value)) { - throw invalid_argument(isolate, value, "String"); + return *std::move(result); } + throw invalid_argument(isolate, value, "String"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (value.IsEmpty()) return std::nullopt; v8::HandleScope scope(isolate); v8::Local str; if (!value->ToString(isolate->GetCurrentContext()).ToLocal(&str)) { - throw invalid_argument(isolate, value, "String"); + return std::nullopt; } + return extract_string(isolate, str); + } + + static to_type to_v8(v8::Isolate* isolate, std::basic_string_view value) + { + if constexpr (sizeof(Char) == 1) + { + return v8::String::NewFromUtf8(isolate, + reinterpret_cast(value.data()), + 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(); + } + } + +private: + static from_type extract_string(v8::Isolate* isolate, v8::Local str) + { #if V8_MAJOR_VERSION > 13 || (V8_MAJOR_VERSION == 13 && V8_MINOR_VERSION >= 3) if constexpr (sizeof(Char) == 1) { @@ -120,22 +148,6 @@ struct convert::value> } #endif } - - static to_type to_v8(v8::Isolate* isolate, std::basic_string_view value) - { - if constexpr (sizeof(Char) == 1) - { - return v8::String::NewFromUtf8(isolate, - reinterpret_cast(value.data()), - 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(); - } - } }; // converter specializations for null-terminated strings @@ -224,10 +236,13 @@ struct convert static from_type from_v8(v8::Isolate* isolate, v8::Local value) { - if (!is_valid(isolate, value)) - { - throw invalid_argument(isolate, value, "Boolean"); - } + if (auto result = try_from_v8(isolate, value)) return *result; + throw invalid_argument(isolate, value, "Boolean"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; return value->BooleanValue(isolate); } @@ -250,10 +265,13 @@ struct convert::value>::type> static from_type from_v8(v8::Isolate* isolate, v8::Local value) { - if (!is_valid(isolate, value)) - { - throw invalid_argument(isolate, value, "Number"); - } + if (auto result = try_from_v8(isolate, value)) return *result; + throw invalid_argument(isolate, value, "Number"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; if constexpr (sizeof(T) <= sizeof(uint32_t)) { @@ -313,6 +331,12 @@ struct convert::value>::type> return static_cast(convert::from_v8(isolate, value)); } + 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; + } + static to_type to_v8(v8::Isolate* isolate, T value) { return convert::to_v8(isolate, @@ -333,11 +357,13 @@ struct convert::value>::typ static from_type from_v8(v8::Isolate* isolate, v8::Local value) { - if (!is_valid(isolate, value)) - { - throw invalid_argument(isolate, value, "Number"); - } + if (auto result = try_from_v8(isolate, value)) return *result; + throw invalid_argument(isolate, value, "Number"); + } + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; return static_cast(value->NumberValue(isolate->GetCurrentContext()).FromJust()); } @@ -375,6 +401,19 @@ struct convert> } } + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (value.IsEmpty() || value->IsNullOrUndefined()) + { + return from_type{std::nullopt}; + } + if (convert::is_valid(isolate, value)) + { + return from_type{convert::from_v8(isolate, value)}; + } + return std::nullopt; + } + static to_type to_v8(v8::Isolate* isolate, std::optional const& value) { if (value) @@ -412,6 +451,12 @@ struct convert> return from_v8_impl(isolate, value, std::make_index_sequence{}); } + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; + return from_v8_impl(isolate, value, std::make_index_sequence{}); + } + static to_type to_v8(v8::Isolate* isolate, from_type const& value) { return to_v8_impl(isolate, value, std::make_index_sequence{}); @@ -642,10 +687,13 @@ struct convert:: static from_type from_v8(v8::Isolate* isolate, v8::Local value) { - if (!is_valid(isolate, value)) - { - throw invalid_argument(isolate, value, "Array"); - } + if (auto result = try_from_v8(isolate, value)) return *std::move(result); + throw invalid_argument(isolate, value, "Array"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; v8::HandleScope scope(isolate); v8::Local context = isolate->GetCurrentContext(); @@ -723,10 +771,13 @@ struct convert::val static from_type from_v8(v8::Isolate* isolate, v8::Local value) { - if (!is_valid(isolate, value)) - { - throw invalid_argument(isolate, value, "Object"); - } + if (auto result = try_from_v8(isolate, value)) return *std::move(result); + throw invalid_argument(isolate, value, "Object"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; v8::HandleScope scope(isolate); v8::Local context = isolate->GetCurrentContext(); @@ -734,7 +785,7 @@ struct convert::val v8::Local prop_names; if (!object->GetPropertyNames(context).ToLocal(&prop_names)) { - throw invalid_argument(isolate, value, "Object"); + return std::nullopt; } from_type result{}; @@ -780,6 +831,12 @@ struct convert> return value.As(); } + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; + return value.As(); + } + static v8::Local to_v8(v8::Isolate*, v8::Local value) { return value; @@ -836,6 +893,13 @@ struct convert::value>::type> return class_::unwrap_object(isolate, value); } + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + // from_v8 returns nullptr without throwing on failure + auto ptr = from_v8(isolate, value); + return ptr ? std::optional{ptr} : std::nullopt; + } + static to_type to_v8(v8::Isolate* isolate, T const* value) { return class_::find_object(isolate, value); @@ -868,6 +932,13 @@ struct convert::value>::type> throw std::runtime_error("failed to unwrap C++ object"); } + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (value.IsEmpty() || !value->IsObject()) return std::nullopt; + T* object = class_::unwrap_object(isolate, value); + return object ? std::optional{*object} : std::nullopt; + } + static to_type to_v8(v8::Isolate* isolate, T const& value) { v8::Local result = class_::find_object(isolate, value); @@ -897,6 +968,13 @@ struct convert, typename std::enable_if:: return class_::unwrap_object(isolate, value); } + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + // 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; + } + static to_type to_v8(v8::Isolate* isolate, std::shared_ptr const& value) { return class_::find_object(isolate, value); @@ -930,6 +1008,13 @@ struct convert throw std::runtime_error("failed to unwrap C++ object"); } + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (value.IsEmpty() || !value->IsObject()) return std::nullopt; + std::shared_ptr object = class_::unwrap_object(isolate, value); + return object ? std::optional{*object} : std::nullopt; + } + static to_type to_v8(v8::Isolate* isolate, T const& value) { v8::Local result = class_::find_object(isolate, value); @@ -964,6 +1049,38 @@ auto from_v8(v8::Isolate* isolate, v8::Local value, U const& default_ convert::from_v8(isolate, value) : static_cast(default_value); } +namespace detail { + +// Fallback for user-defined converters that don't implement try_from_v8. +// Uses is_valid + from_v8 (double validation, potential exception on type mismatch). +template +[[deprecated("convert specialization should implement try_from_v8() for optimal exception-free conversion")]] +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)}; +} + +} // namespace detail + +// Exception-free conversion: returns std::optional with the converted value, +// or std::nullopt if the value cannot be converted to type T. +// Distinct from convert> which handles missing/undefined JS arguments. +// Delegates to convert::try_from_v8 when available, falls back to is_valid + from_v8. +template +auto try_from_v8(v8::Isolate* isolate, v8::Local value) +{ + if constexpr (requires { convert::try_from_v8(isolate, value); }) + { + return convert::try_from_v8(isolate, value); + } + else + { + return detail::try_from_v8_fallback(isolate, value); + } +} + inline v8::Local to_v8(v8::Isolate* isolate, char const* str) { return convert::to_v8(isolate, std::string_view(str)); From 7169dcc46d1f88ddf408078413c7bd96d2f9fe72 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:39:27 +0100 Subject: [PATCH 04/38] fix V8PP_PRETTIFY_TYPENAMES config propagation Use #cmakedefine01 in config.hpp.in to convert CMake BOOL values (ON/OFF) to C preprocessor integers (1/0). Include config.hpp in type_info.hpp so the macro is defined when included standalone. Remove redundant PRIVATE compile definition from v8pp target. --- v8pp/CMakeLists.txt | 3 --- v8pp/config.hpp.in | 6 +++++- v8pp/type_info.hpp | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/v8pp/CMakeLists.txt b/v8pp/CMakeLists.txt index a4c52a8..7152416 100644 --- a/v8pp/CMakeLists.txt +++ b/v8pp/CMakeLists.txt @@ -91,9 +91,6 @@ else() endif() endif() -if(V8PP_PRETTIFY_TYPENAMES) - target_compile_definitions(v8pp PRIVATE V8PP_PRETTIFY_TYPENAMES=1) -endif() #source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${V8PP_HEADERS} ${V8PP_SOURCES}) diff --git a/v8pp/config.hpp.in b/v8pp/config.hpp.in index cb87f3e..0e56ca0 100644 --- a/v8pp/config.hpp.in +++ b/v8pp/config.hpp.in @@ -36,7 +36,7 @@ v8::Local V8PP_PLUGIN_INIT_PROC_NAME(isolate) #ifndef V8PP_HEADER_ONLY -#define V8PP_HEADER_ONLY @V8PP_HEADER_ONLY@ +#cmakedefine01 V8PP_HEADER_ONLY #endif #if V8PP_HEADER_ONLY @@ -45,5 +45,9 @@ v8::Local V8PP_PLUGIN_INIT_PROC_NAME(isolate) #define V8PP_IMPL #endif +#ifndef V8PP_PRETTIFY_TYPENAMES +#cmakedefine01 V8PP_PRETTIFY_TYPENAMES +#endif + #define V8PP_STRINGIZE(s) V8PP_STRINGIZE0(s) #define V8PP_STRINGIZE0(s) #s diff --git a/v8pp/type_info.hpp b/v8pp/type_info.hpp index dd42ddf..416c54e 100644 --- a/v8pp/type_info.hpp +++ b/v8pp/type_info.hpp @@ -1,5 +1,7 @@ #pragma once +#include "v8pp/config.hpp" + #include #include @@ -59,7 +61,7 @@ inline type_info type_id() #else #error "Unknown compiler" #endif -#if V8PP_PRETTIFY_TYPENAMES == 1 +#if V8PP_PRETTIFY_TYPENAMES for (auto&& prefix : all_prefixes) { const auto p = name.find(prefix); From 5521849efa56a066502b9854045ad4f163283062 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 12:40:37 +0100 Subject: [PATCH 05/38] Add crash safety tests: from_v8 with invalid inputs, json circular reference handling, class method/property on non-wrapped objects. Fix non-member functions not accessible on class constructor, update tuple bool->string test expectation, fix class re-registration conflict in try_from_v8 tests, suppress V8 stderr noise in unhandled exception tests. --- test/test_class.cpp | 30 +++++++- test/test_convert.cpp | 155 ++++++++++++++++++++++++++++++++++++++---- test/test_json.cpp | 124 +++++++++++++++++++-------------- v8pp/class.hpp | 8 ++- 4 files changed, 250 insertions(+), 67 deletions(-) diff --git a/test/test_class.cpp b/test/test_class.cpp index 3cc19ac..8673c9c 100644 --- a/test/test_class.cpp +++ b/test/test_class.cpp @@ -230,11 +230,17 @@ void test_class_() check_eq("C++ exception from X ctor", run_script(context, "ret = ''; try { new X(1, 2); } catch(err) { ret = err.message; } ret"), "C++ exception"); - check("Unhandled C++ exception from X ctor", context.run_script("x = new X(1, 2); x").IsEmpty()); + { + v8::TryCatch try_catch(context.isolate()); + check("Unhandled C++ exception from X ctor", context.run_script("x = new X(1, 2); x").IsEmpty()); + } check_eq("V8 exception from X ctor", run_script(context, "ret = ''; try { new X(1, 2, 3); } catch(err) { ret = err.message; } ret"), "JS exception"); - check("Unhandled V8 exception from X ctor", context.run_script("x = new X(1, 2, 3); x").IsEmpty()); + { + v8::TryCatch try_catch(context.isolate()); + check("Unhandled V8 exception from X ctor", context.run_script("x = new X(1, 2, 3); x").IsEmpty()); + } check_eq("X object", run_script(context, "x = new X(); x.var += x.konst"), 100); check_eq("X::rprop", run_script(context, "x = new X(); x.rprop"), 1); @@ -269,6 +275,26 @@ void test_class_() 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()", + run_script(context, + "try { x = new X(); x.fun1.call({}, 1); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // Crash safety: property read on non-wrapped object should throw JS error, not crash + check_eq("property read on non-wrapped object", + run_script(context, + "try { var desc = Object.getOwnPropertyDescriptor(new X(), 'rprop');" + "desc.get.call({}); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // Crash safety: property write on non-wrapped object should throw JS error, not crash + check_eq("property write on non-wrapped object", + run_script(context, + "try { var desc = Object.getOwnPropertyDescriptor(new X(), 'wprop');" + "desc.set.call({}, 42); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + 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}]})" diff --git a/test/test_convert.cpp b/test/test_convert.cpp index 1454639..3f4e7a4 100644 --- a/test/test_convert.cpp +++ b/test/test_convert.cpp @@ -245,12 +245,12 @@ void test_convert_tuple(v8::Isolate* isolate) v8pp::from_v8>(isolate, tuple_1_); }); - check_ex("String", [isolate, &tuple_1]() { - // wrong types + // bool converts to string via ToString() v8::Local tuple_1_ = v8pp::to_v8(isolate, tuple_1); - v8pp::from_v8>(isolate, tuple_1_); - }); + auto result = v8pp::from_v8>(isolate, tuple_1_); + check_eq("tuple bool->string", std::get<1>(result), "true"); + } } template @@ -450,6 +450,140 @@ void test_convert_variant(v8::Isolate* isolate) optional_check(0, std::optional{}, false); } +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)); + }); + check_ex("from_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")); + }); + check_ex("from_v8 bool", [isolate]() + { + v8pp::from_v8(isolate, v8pp::to_v8(isolate, true)); + }); + check_ex("from_v8 object", [isolate]() + { + v8pp::from_v8(isolate, v8::Object::New(isolate)); + }); + check_ex("from_v8 empty handle", [isolate]() + { + 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")); + }); + + // from_v8 with non-numeric + check_ex("from_v8 string", [isolate]() + { + v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); + }); + check_ex("from_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)); + }); + check_ex("from_v8 string", [isolate]() + { + v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello")); + }); + check_ex("from_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()); + }); + + // from_v8 with object that has throwing toString (Phase 1a fix) + { + v8::TryCatch try_catch(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(); + 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); + }); + } + + // from_v8> with non-array + check_ex("from_v8> int", [isolate]() + { + v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); + }); + check_ex("from_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")); + }); + + // from_v8 with non-object + check_ex("from_v8 int", [isolate]() + { + v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); + }); + check_ex("from_v8 array", [isolate]() + { + v8pp::from_v8>(isolate, v8::Array::New(isolate)); + }); + check_ex("from_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", + v8pp::from_v8(isolate, v8::Undefined(isolate), -1), -1); + check_eq("from_v8 default on string", + v8pp::from_v8(isolate, v8pp::to_v8(isolate, "hello"), -1), -1); + check_eq("from_v8 default on null", + v8pp::from_v8(isolate, v8::Null(isolate), -1), -1); + check_eq("from_v8 default on int", + v8pp::from_v8(isolate, v8pp::to_v8(isolate, 42), false), false); + check_eq("from_v8 default on string", + 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]() + { + v8pp::from_v8(isolate, v8pp::to_v8(isolate, "red")); + }); + check_ex("from_v8 undefined", [isolate]() + { + v8pp::from_v8(isolate, v8::Undefined(isolate)); + }); +} + void test_convert_try_from_v8(v8::Isolate* isolate) { // Primitives: valid conversions return value @@ -528,10 +662,7 @@ void test_convert_try_from_v8(v8::Isolate* isolate) check("try optional from string", !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, "hello"))); - // Wrapped class: valid unwrap - v8pp::class_ U_class(isolate); - U_class.template ctor<>().auto_wrap_objects(true); - + // Wrapped class: valid unwrap (class_ already registered by test_convert_variant) U u_obj{42}; auto u_v8 = v8pp::class_::reference_external(isolate, &u_obj); @@ -543,12 +674,9 @@ void test_convert_try_from_v8(v8::Isolate* isolate) check("try U* from int", !v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, 42))); check("try U* from plain object", !v8pp::try_from_v8(isolate, v8::Object::New(isolate))); - // Wrapped class via shared_ptr - v8pp::class_ V_class(isolate); - V_class.template ctor<>().auto_wrap_objects(true); - + // Wrapped class via shared_ptr (class_ already registered by test_convert_variant) auto v_obj = std::make_shared(V{"test"}); - V_class.reference_external(isolate, v_obj); + v8pp::class_::reference_external(isolate, v_obj); auto v_v8 = v8pp::class_::find_object(isolate, v_obj); auto v_result = v8pp::try_from_v8>(isolate, v_v8); @@ -611,5 +739,6 @@ void test_convert() test_convert_optional(isolate); test_convert_tuple(isolate); test_convert_variant(isolate); + test_convert_crash_safety(isolate); test_convert_try_from_v8(isolate); } diff --git a/test/test_json.cpp b/test/test_json.cpp index ce42bdd..0cb5742 100644 --- a/test/test_json.cpp +++ b/test/test_json.cpp @@ -1,51 +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()); -} +#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/v8pp/class.hpp b/v8pp/class.hpp index 4e43664..afc1827 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -316,7 +316,13 @@ class class_ wrapped_fun = wrap_function_template(isolate(), std::forward(func)); } - class_info_.js_function_template()->PrototypeTemplate()->Set(v8_name, wrapped_fun, attr); + v8::Local js_func = class_info_.js_function_template(); + js_func->PrototypeTemplate()->Set(v8_name, wrapped_fun, attr); + if constexpr (!is_mem_fun) + { + // non-member functions are also accessible on the constructor (e.g. X.static_fun()) + js_func->Set(v8_name, wrapped_fun, attr); + } return *this; } From 7d120d265e5fb06b5b4effa3294e3ddcd0709910 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:05:20 +0100 Subject: [PATCH 06/38] Add crash safety design fixes: unwrap_object depth limit, magic number validation, context use-after-free protection - Rewrite unwrap_object with fast path (direct field check) and depth-limited slow path (constexpr kMaxPrototypeDepth = 16) to prevent infinite loops from circular/deep prototype chains - Add magic number sentinel (0xC1A5517F) to class_info, validated in weak callbacks and unwrap_object before static_cast of registry pointer - Protect require()/run() from use-after-free via static alive-context set in context.cpp, checked before dereferencing context pointer in callbacks - Add tests: deep prototype chain, fast path unwrap, inheritance unwrap, require() after context destruction --- test/test_class.cpp | 16 ++++++++++- test/test_context.cpp | 47 ++++++++++++++++++++++++++++++++ v8pp/class.hpp | 10 ++++++- v8pp/class.ipp | 62 ++++++++++++++++++++++++++++++++++--------- v8pp/context.cpp | 23 ++++++++++++++++ 5 files changed, 144 insertions(+), 14 deletions(-) diff --git a/test/test_class.cpp b/test/test_class.cpp index 8673c9c..5bebc27 100644 --- a/test/test_class.cpp +++ b/test/test_class.cpp @@ -295,6 +295,20 @@ void test_class_() "desc.set.call({}, 42); 'no error'; } catch(e) { 'caught'; }"), "caught"); + // Crash safety: deep prototype chain beyond depth limit should not hang + check_eq("deep prototype chain beyond depth limit", + run_script(context, + "var obj = {};" + "for (var i = 0; i < 20; i++) { obj = Object.create(obj); }" + "try { var x = new X(); x.fun1.call(obj, 1); 'no error'; } catch(e) { 'caught'; }"), + "caught"); + + // Crash safety: normal direct unwrap (fast path) still works + check_eq("direct unwrap fast path", run_script(context, "x = new X(3); x.fun1(1)"), 4); + + // Crash safety: inheritance chain unwrap still works + check_eq("inheritance unwrap", run_script(context, "y = new Y(5); y.fun1(10)"), 15); + 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}]})" @@ -325,7 +339,7 @@ void test_class_() check_eq("y3.var", y3->var, -3); run_script(context, "x = new X; for (i = 0; i < 10; ++i) { y = new Y(i); y.useX(x); y.useX_ptr(x); }"); - check_eq("Y count", Y::instance_count, 13 + 4); // 13 + y + y1 + y2 + y3 + check_eq("Y count", Y::instance_count, 14 + 4); // 14 + y + y1 + y2 + y3 (14 includes extra Y from unwrap test) run_script(context, "y = null; 0"); v8pp::class_::unreference_external(isolate, y1); diff --git a/test/test_context.cpp b/test/test_context.cpp index 62ff721..78447da 100644 --- a/test/test_context.cpp +++ b/test/test_context.cpp @@ -121,4 +121,51 @@ void test_context() } isolate->Dispose(); } + + // Crash safety: require() after context destruction should throw, not crash + { + v8::Isolate* isolate = v8pp::context::create_isolate(); + { + v8::Isolate::Scope isolate_scope(isolate); + v8::HandleScope outer_scope(isolate); + + v8::Global require_fn; + v8::Global saved_ctx; + + { + v8pp::context ctx(isolate, nullptr, true, false); + v8::HandleScope scope(isolate); + v8::Context::Scope context_scope(ctx.impl()); + saved_ctx.Reset(isolate, ctx.impl()); + + // Capture the require function + v8::Local require_val; + check("get require", + ctx.global()->Get(isolate->GetCurrentContext(), + v8pp::to_v8(isolate, "require")).ToLocal(&require_val)); + check("require is function", require_val->IsFunction()); + require_fn.Reset(isolate, require_val.As()); + } + // ctx is destroyed here, but isolate is still alive + + // Call require() from the saved context — should get an error, not crash + { + v8::HandleScope scope(isolate); + v8::Local local_ctx = saved_ctx.Get(isolate); + v8::Context::Scope context_scope(local_ctx); + v8::TryCatch try_catch(isolate); + + v8::Local args[] = { v8pp::to_v8(isolate, "nonexistent") }; + v8::Local fn = require_fn.Get(isolate); + auto result = fn->Call(local_ctx, local_ctx->Global(), 1, args); + + check("require after destroy caught exception", + try_catch.HasCaught() || result.IsEmpty()); + } + + saved_ctx.Reset(); + require_fn.Reset(); + } + isolate->Dispose(); + } } diff --git a/v8pp/class.hpp b/v8pp/class.hpp index afc1827..1923131 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -21,12 +21,20 @@ namespace v8pp::detail { struct class_info { + static constexpr uint32_t kMagic = 0xC1A5517F; + + uint32_t const magic = kMagic; type_info const type; type_info const traits; class_info(type_info const& type, type_info const& traits); - virtual ~class_info() = default; // make virtual to delete derived object_registry + virtual ~class_info() + { + const_cast(magic) = 0; + } + + bool is_valid() const { return magic == kMagic; } std::string class_name() const; }; diff --git a/v8pp/class.ipp b/v8pp/class.ipp index e5691a7..4c11977 100644 --- a/v8pp/class.ipp +++ b/v8pp/class.ipp @@ -217,8 +217,11 @@ V8PP_IMPL v8::Local object_registry::wrap_this(v8::Local const& data) { object_id object = data.GetInternalField(0); - object_registry* this_ = static_cast(data.GetInternalField(1)); - this_->remove_object(object); + auto this_ = static_cast(data.GetInternalField(1)); + if (this_ && this_->is_valid()) + { + this_->remove_object(object); + } }, v8::WeakCallbackType::kInternalFields); objects_.emplace(object, wrapped_object{ std::move(pobj), size }); apply_const_properties(isolate_, obj, object); @@ -261,8 +264,11 @@ V8PP_IMPL v8::Local object_registry::wrap_object(pointer_typ pobj.SetWeak(this, [](v8::WeakCallbackInfo const& data) { object_id object = data.GetInternalField(0); - object_registry* this_ = static_cast(data.GetInternalField(1)); - this_->remove_object(object); + auto this_ = static_cast(data.GetInternalField(1)); + if (this_ && this_->is_valid()) + { + this_->remove_object(object); + } }, v8::WeakCallbackType::kInternalFields); objects_.emplace(object, wrapped_object{ std::move(pobj), size }); apply_const_properties(isolate_, obj, object); @@ -294,9 +300,46 @@ object_registry::unwrap_object(v8::Local value) { v8::HandleScope scope(isolate_); - while (value->IsObject()) + if (!value->IsObject()) + { + return nullptr; + } + + v8::Local obj = value.As(); + + // Fast path: check the object itself (most common case) + if (obj->InternalFieldCount() == 2) + { + object_id id = obj->GetAlignedPointerFromInternalField(0); + if (id) + { + auto registry = static_cast( + obj->GetAlignedPointerFromInternalField(1)); + if (registry && registry->is_valid()) + { + pointer_type ptr = registry->find_object(id, type); + if (ptr) + { + return ptr; + } + } + } + } + + // Slow path: walk prototype chain with depth limit (for inheritance) + constexpr int kMaxPrototypeDepth = 16; + for (int depth = 0; depth < kMaxPrototypeDepth; ++depth) { - v8::Local obj = value.As(); +#if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) + value = obj->GetPrototypeV2(); +#else + value = obj->GetPrototype(); +#endif + if (!value->IsObject()) + { + break; + } + obj = value.As(); if (obj->InternalFieldCount() == 2) { object_id id = obj->GetAlignedPointerFromInternalField(0); @@ -304,7 +347,7 @@ object_registry::unwrap_object(v8::Local value) { auto registry = static_cast( obj->GetAlignedPointerFromInternalField(1)); - if (registry) + if (registry && registry->is_valid()) { pointer_type ptr = registry->find_object(id, type); if (ptr) @@ -314,11 +357,6 @@ object_registry::unwrap_object(v8::Local value) } } } -#if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) - value = obj->GetPrototypeV2(); -#else - value = obj->GetPrototype(); -#endif } return nullptr; } diff --git a/v8pp/context.cpp b/v8pp/context.cpp index 0bed65e..d0ad320 100644 --- a/v8pp/context.cpp +++ b/v8pp/context.cpp @@ -7,6 +7,7 @@ #include "v8pp/throw_ex.hpp" #include +#include #include #if defined(WIN32) @@ -21,6 +22,12 @@ static char const path_sep = '/'; namespace v8pp { +static std::unordered_set& alive_contexts() +{ + static std::unordered_set set; + return set; +} + struct context::dynamic_module { void* handle; @@ -42,6 +49,10 @@ void context::load_module(v8::FunctionCallbackInfo const& args) } context* ctx = detail::external_data::get(args.Data()); + if (alive_contexts().find(ctx) == alive_contexts().end()) + { + throw std::runtime_error("require() called on destroyed context"); + } // check if module is already loaded const auto it = ctx->modules_.find(name); @@ -120,6 +131,10 @@ void context::run_file(v8::FunctionCallbackInfo const& args) } context* ctx = detail::external_data::get(args.Data()); + if (alive_contexts().find(ctx) == alive_contexts().end()) + { + throw std::runtime_error("run() called on destroyed context"); + } result = to_v8(isolate, ctx->run_file(filename)); } catch (std::exception const& ex) @@ -176,6 +191,8 @@ context::context(v8::Isolate* isolate, v8::ArrayBuffer::Allocator* allocator, impl->Enter(); } impl_.Reset(isolate_, impl); + + alive_contexts().insert(this); } context::context(context&& src) noexcept @@ -187,6 +204,8 @@ context::context(context&& src) noexcept , modules_(std::move(src.modules_)) , lib_path_(std::move(src.lib_path_)) { + alive_contexts().erase(&src); + alive_contexts().insert(this); } context& context::operator=(context&& src) noexcept @@ -202,6 +221,8 @@ context& context::operator=(context&& src) noexcept impl_ = std::move(src.impl_); modules_ = std::move(src.modules_); lib_path_ = std::move(src.lib_path_); + alive_contexts().erase(&src); + alive_contexts().insert(this); } return *this; } @@ -219,6 +240,8 @@ void context::destroy() return; } + alive_contexts().erase(this); + // remove all class singletons and external data before modules unload cleanup(isolate_); From 1c7af9d179d14ea4eb3ecd1f06f79c8e5224c4fb Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:19:53 +0100 Subject: [PATCH 07/38] deduplicate object.hpp subobject traversal, replace SFINAE with C++20 concepts Extract traverse_subobjects() helper from get_option/set_option/set_option_data to eliminate three identical 12-line loops. Replace void_t detection patterns (is_mapping, is_sequence, has_reserve) and the is_callable_impl SFINAE hack with C++20 concepts. Replace 9 enable_if partial specializations in convert.hpp with requires clauses, using standard concepts (std::integral, std::floating_point) where available. All existing trait aliases preserved for backward compatibility. --- v8pp/convert.hpp | 31 +++++++++++------- v8pp/object.hpp | 85 ++++++++++++++++++------------------------------ v8pp/utility.hpp | 72 +++++++++++----------------------------- 3 files changed, 69 insertions(+), 119 deletions(-) diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index e96ce4c..adf3e02 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -50,7 +51,8 @@ struct runtime_error : std::runtime_error // converter specializations for string types template -struct convert::value>::type> + requires detail::is_string::value +struct convert { using Char = typename String::value_type; using Traits = typename String::traits_type; @@ -252,8 +254,8 @@ struct convert } }; -template -struct convert::value>::type> +template +struct convert { using from_type = T; using to_type = v8::Local; @@ -314,7 +316,8 @@ struct convert::value>::type> }; template -struct convert::value>::type> + requires std::is_enum_v +struct convert { using underlying_type = typename std::underlying_type::type; @@ -344,8 +347,8 @@ struct convert::value>::type> } }; -template -struct convert::value>::type> +template +struct convert { using from_type = T; using to_type = v8::Local; @@ -674,7 +677,8 @@ struct convert> // convert Array <-> std::array, vector, deque, list template -struct convert::value || detail::is_array::value>::type> + requires (detail::sequence || detail::is_array::value) +struct convert { using from_type = Sequence; using to_type = v8::Local; @@ -755,8 +759,8 @@ struct convert:: }; // convert Object <-> std::{unordered_}{multi}map -template -struct convert::value>::type> +template +struct convert { using from_type = Mapping; using to_type = v8::Local; @@ -873,7 +877,8 @@ struct is_wrapped_class> : std::false_type }; template -struct convert::value>::type> + requires is_wrapped_class::value +struct convert { using from_type = T*; using to_type = v8::Local; @@ -907,7 +912,8 @@ struct convert::value>::type> }; template -struct convert::value>::type> + requires is_wrapped_class::value +struct convert { using from_type = T&; using to_type = v8::Local; @@ -948,7 +954,8 @@ struct convert::value>::type> }; template -struct convert, typename std::enable_if::value>::type> + requires is_wrapped_class::value +struct convert, void> { using from_type = std::shared_ptr; using to_type = v8::Local; diff --git a/v8pp/object.hpp b/v8pp/object.hpp index 0e10f62..60ec16c 100644 --- a/v8pp/object.hpp +++ b/v8pp/object.hpp @@ -8,6 +8,31 @@ namespace v8pp { +/// Traverse dot-separated subobject path, updating options and name +/// to point to the final object and leaf property name. +/// Returns false if any intermediate path segment is missing or not an object. +inline bool traverse_subobjects(v8::Isolate* isolate, v8::Local context, + v8::Local& options, std::string_view& name) +{ + for (;;) + { + std::string_view::size_type const dot_pos = name.find('.'); + if (dot_pos == name.npos) + { + return true; + } + + v8::Local part; + if (!options->Get(context, v8pp::to_v8(isolate, name.substr(0, dot_pos))).ToLocal(&part) + || !part->IsObject()) + { + return false; + } + options = part.As(); + name.remove_prefix(dot_pos + 1); + } +} + /// Get optional value from V8 object by name. /// Dot symbols in option name delimits subobjects name. /// return false if the value doesn't exist in the options object @@ -17,25 +42,9 @@ bool get_option(v8::Isolate* isolate, v8::Local options, { v8::Local context = isolate->GetCurrentContext(); - if (support_subobjects) + if (support_subobjects && !traverse_subobjects(isolate, context, options, name)) { - for (;;) - { - std::string_view::size_type const dot_pos = name.find('.'); - if (dot_pos == name.npos) - { - break; - } - - v8::Local part; - if (!options->Get(context, v8pp::to_v8(isolate, name.substr(0, dot_pos))).ToLocal(&part) - || !part->IsObject()) - { - return false; - } - options = part.As(); - name.remove_prefix(dot_pos + 1); - } + return false; } v8::Local val; @@ -64,25 +73,9 @@ bool set_option(v8::Isolate* isolate, v8::Local options, { v8::Local context = isolate->GetCurrentContext(); - if (support_subobjects) + if (support_subobjects && !traverse_subobjects(isolate, context, options, name)) { - for (;;) - { - std::string_view::size_type const dot_pos = name.find('.'); - if (dot_pos == name.npos) - { - break; - } - - v8::Local part; - if (!options->Get(context, v8pp::to_v8(isolate, name.substr(0, dot_pos))).ToLocal(&part) - || !part->IsObject()) - { - return false; - } - options = part.As(); - name.remove_prefix(dot_pos + 1); - } + return false; } return options->Set(context, v8pp::to_v8(isolate, name), to_v8(isolate, value)).FromMaybe(false); @@ -103,25 +96,9 @@ bool set_option_data(v8::Isolate* isolate, v8::Local options, { v8::Local context = isolate->GetCurrentContext(); - if (support_subobjects) + if (support_subobjects && !traverse_subobjects(isolate, context, options, name)) { - for (;;) - { - std::string_view::size_type const dot_pos = name.find('.'); - if (dot_pos == name.npos) - { - break; - } - - v8::Local part; - if (!options->Get(context, v8pp::to_v8(isolate, name.substr(0, dot_pos))).ToLocal(&part) - || !part->IsObject()) - { - return false; - } - options = part.As(); - name.remove_prefix(dot_pos + 1); - } + return false; } return options->CreateDataProperty(context, diff --git a/v8pp/utility.hpp b/v8pp/utility.hpp index b2351a9..755b0a4 100644 --- a/v8pp/utility.hpp +++ b/v8pp/utility.hpp @@ -70,56 +70,47 @@ struct is_string : std::true_type // // is_mapping // -template -struct is_mapping_impl : std::false_type -{ +template +concept mapping = requires(T t) { + typename T::key_type; + typename T::mapped_type; + t.begin(); + t.end(); }; template -struct is_mapping_impl().begin()), decltype(std::declval().end())>> : std::true_type +struct is_mapping : std::bool_constant> { }; -template -using is_mapping = is_mapping_impl; - ///////////////////////////////////////////////////////////////////////////// // // is_sequence // -template -struct is_sequence_impl : std::false_type -{ +template +concept sequence = !is_string::value && requires(T t, typename T::value_type v) { + t.begin(); + t.end(); + t.emplace_back(std::move(v)); }; template -struct is_sequence_impl().begin()), decltype(std::declval().end()), - decltype(std::declval().emplace_back(std::declval()))>> : std::negation> +struct is_sequence : std::bool_constant> { }; -template -using is_sequence = is_sequence_impl; - ///////////////////////////////////////////////////////////////////////////// // // has_reserve // -template -struct has_reserve_impl : std::false_type -{ -}; +template +concept reservable = requires(T t) { t.reserve(size_t{}); }; template -struct has_reserve_impl().reserve(0))>> : std::true_type +struct has_reserve : std::bool_constant> { }; -template -using has_reserve = has_reserve_impl; - ///////////////////////////////////////////////////////////////////////////// // // is_array @@ -316,36 +307,11 @@ struct function_traits : function_traits { }; -template -struct is_callable_impl - : std::is_function::type> -{ -}; - template -struct is_callable_impl -{ -private: - struct fallback { void operator()(); }; - struct derived : F, fallback {}; - - template - struct check; - - template - static std::true_type test(...); - - template - static std::false_type test(check*); - - using type = decltype(test(0)); - -public: - static constexpr bool value = type::value; -}; +concept callable = std::is_function_v> + || requires { &F::operator(); }; template -using is_callable = std::integral_constant::value>::value>; +using is_callable = std::bool_constant>; } // namespace v8pp::detail From 3a04d9db740bbea39e50af04cae8d423d8cb3e87 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 13:46:47 +0100 Subject: [PATCH 08/38] add V8 performance hints: SideEffectType, internalized strings, NewFromUtf8Literal Add SideEffectType annotations to function/var/property bindings in class_ and module. Const member functions auto-detect as kHasNoSideEffect, with explicit override overload. Var and property getters marked kHasNoSideEffect, setters kHasSideEffectToReceiver. Add to_v8_name() helper using kInternalized for property/method name strings. Replace to_v8 char array overload with NewFromUtf8Literal for compile-time constant strings. Add is_const_member_function_v trait with requires-constrained partial specialization. --- v8pp/class.hpp | 106 ++++++++++++++++++++++++++++++---------------- v8pp/context.hpp | 2 +- v8pp/convert.hpp | 14 ++++-- v8pp/function.hpp | 17 +++++--- v8pp/module.hpp | 30 ++++++++----- v8pp/utility.hpp | 8 ++++ 6 files changed, 121 insertions(+), 56 deletions(-) diff --git a/v8pp/class.hpp b/v8pp/class.hpp index 1923131..e998941 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -301,37 +301,22 @@ class class_ } /// Set class member function, or static function, or lambda + /// Const member functions are automatically tagged as side-effect-free template class_& function(std::string_view name, Function&& func, v8::PropertyAttribute attr = v8::None) { - constexpr bool is_mem_fun = std::is_member_function_pointer_v; - - static_assert(is_mem_fun || detail::is_callable::value, - "Function must be pointer to member function or callable object"); - - v8::HandleScope scope(isolate()); - - v8::Local v8_name = v8pp::to_v8(isolate(), name); - v8::Local wrapped_fun; - - if constexpr (is_mem_fun) - { - using mem_func_type = typename detail::function_traits::template pointer_type; - wrapped_fun = wrap_function_template(isolate(), mem_func_type(std::forward(func))); - } - else - { - wrapped_fun = wrap_function_template(isolate(), std::forward(func)); - } + constexpr auto effect = detail::is_const_member_function_v> + ? v8::SideEffectType::kHasNoSideEffect + : v8::SideEffectType::kHasSideEffect; + return function_impl(name, std::forward(func), effect, attr); + } - v8::Local js_func = class_info_.js_function_template(); - js_func->PrototypeTemplate()->Set(v8_name, wrapped_fun, attr); - if constexpr (!is_mem_fun) - { - // non-member functions are also accessible on the constructor (e.g. X.static_fun()) - js_func->Set(v8_name, wrapped_fun, attr); - } - return *this; + /// Set class member function with explicit side-effect type + template + class_& function(std::string_view name, Function&& func, + v8::SideEffectType side_effect, v8::PropertyAttribute attr = v8::None) + { + return function_impl(name, std::forward(func), side_effect, attr); } /// Set class member variable @@ -345,7 +330,7 @@ class class_ using attribute_type = typename detail::function_traits::template pointer_type; attribute_type attr = attribute; - v8::Local v8_name = v8pp::to_v8(isolate(), name); + v8::Local v8_name = v8pp::to_v8_name(isolate(), name); v8::AccessorNameGetterCallback getter = &member_get; v8::AccessorNameSetterCallback setter = &member_set; v8::Local data = detail::external_data::set(isolate(), std::forward(attr)); @@ -353,11 +338,17 @@ class class_ // SetAccessor removed from ObjectTemplate in V8 12.9+ class_info_.js_function_template() ->InstanceTemplate() - ->SetNativeDataProperty(v8_name, getter, setter, data, v8::PropertyAttribute(v8::DontDelete)); + ->SetNativeDataProperty(v8_name, getter, setter, data, + v8::PropertyAttribute(v8::DontDelete), v8::DEFAULT, + 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)); + ->SetAccessor(v8_name, getter, setter, data, + v8::DEFAULT, v8::PropertyAttribute(v8::DontDelete), + v8::SideEffectType::kHasNoSideEffect, + v8::SideEffectType::kHasSideEffectToReceiver); #endif return *this; } @@ -389,14 +380,21 @@ class class_ v8::AccessorNameGetterCallback getter = property_type::template get; v8::AccessorNameSetterCallback setter = property_type::is_readonly ? nullptr : property_type::template set; - v8::Local v8_name = v8pp::to_v8(isolate(), name); + 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; #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)); + class_info_.js_function_template()->InstanceTemplate()->SetNativeDataProperty(v8_name, getter, setter, data, + v8::PropertyAttribute(v8::DontDelete), v8::DEFAULT, + v8::SideEffectType::kHasNoSideEffect, setter_effect); #else - //class_info_.class_function_template()->PrototypeTemplate()->SetAccessor(v8_name, getter, setter, data, v8::PropertyAttribute::DontDelete); - class_info_.js_function_template()->PrototypeTemplate()->SetAccessor(v8_name, getter, setter, data, v8::DEFAULT, v8::PropertyAttribute(v8::DontDelete)); + class_info_.js_function_template()->PrototypeTemplate()->SetAccessor(v8_name, getter, setter, data, + v8::DEFAULT, v8::PropertyAttribute(v8::DontDelete), + v8::SideEffectType::kHasNoSideEffect, setter_effect); #endif return *this; } @@ -433,7 +431,7 @@ class class_ { v8::HandleScope scope(isolate()); - class_info_.js_function_template()->PrototypeTemplate()->Set(v8pp::to_v8(isolate(), name), to_v8(isolate(), value), + class_info_.js_function_template()->PrototypeTemplate()->Set(v8pp::to_v8_name(isolate(), name), to_v8(isolate(), value), v8::PropertyAttribute(v8::ReadOnly | v8::DontDelete)); return *this; } @@ -447,7 +445,7 @@ class class_ v8::Local context = iso->GetCurrentContext(); class_info_.js_function_template()->GetFunction(context).ToLocalChecked() - ->DefineOwnProperty(context, v8pp::to_v8(iso, name), to_v8(iso, value), + ->DefineOwnProperty(context, v8pp::to_v8_name(iso, name), to_v8(iso, value), v8::PropertyAttribute(v8::DontDelete | (readonly ? v8::ReadOnly : 0))).FromJust(); return *this; } @@ -542,6 +540,42 @@ class class_ } private: + template + class_& function_impl(std::string_view name, Function&& func, + v8::SideEffectType side_effect, v8::PropertyAttribute attr) + { + constexpr bool is_mem_fun = std::is_member_function_pointer_v; + + static_assert(is_mem_fun || detail::is_callable::value, + "Function must be pointer to member function or callable object"); + + v8::HandleScope scope(isolate()); + + v8::Local v8_name = v8pp::to_v8_name(isolate(), name); + v8::Local wrapped_fun; + + if constexpr (is_mem_fun) + { + using mem_func_type = typename detail::function_traits::template pointer_type; + wrapped_fun = wrap_function_template(isolate(), + mem_func_type(std::forward(func)), side_effect); + } + else + { + wrapped_fun = wrap_function_template(isolate(), + std::forward(func), side_effect); + } + + v8::Local js_func = class_info_.js_function_template(); + js_func->PrototypeTemplate()->Set(v8_name, wrapped_fun, attr); + if constexpr (!is_mem_fun) + { + // non-member functions are also accessible on the constructor (e.g. X.static_fun()) + js_func->Set(v8_name, wrapped_fun, attr); + } + return *this; + } + template static void member_get(v8::Local, v8::PropertyCallbackInfo const& info) diff --git a/v8pp/context.hpp b/v8pp/context.hpp index 102eaeb..1c640a2 100644 --- a/v8pp/context.hpp +++ b/v8pp/context.hpp @@ -96,7 +96,7 @@ class context context& class_(std::string_view name, v8pp::class_& cl) { v8::HandleScope scope(isolate_); - cl.class_function_template()->SetClassName(v8pp::to_v8(isolate_, name)); + cl.class_function_template()->SetClassName(v8pp::to_v8_name(isolate_, name)); return value(name, cl.js_function_template()->GetFunction(isolate_->GetCurrentContext()).ToLocalChecked()); } diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index adf3e02..3f31aa0 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -1098,11 +1098,10 @@ inline v8::Local to_v8(v8::Isolate* isolate, char const* str, size_t return convert::to_v8(isolate, std::string_view(str, len)); } -template -v8::Local to_v8(v8::Isolate* isolate, - char const (&str)[N], size_t len = N - 1) +template +v8::Local to_v8(v8::Isolate* isolate, char const (&str)[N]) { - return convert::to_v8(isolate, std::string_view(str, len)); + return v8::String::NewFromUtf8Literal(isolate, str); } inline v8::Local to_v8(v8::Isolate* isolate, char16_t const* str) @@ -1141,6 +1140,13 @@ v8::Local to_v8(v8::Isolate* isolate, } #endif +/// Create an internalized V8 string, optimized for property/method names +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(); +} + template v8::Local to_v8(v8::Isolate* isolate, std::optional const& value) { diff --git a/v8pp/function.hpp b/v8pp/function.hpp index 7564cba..7a02a97 100644 --- a/v8pp/function.hpp +++ b/v8pp/function.hpp @@ -184,31 +184,38 @@ namespace v8pp { /// Wrap C++ function into new V8 function template template -v8::Local wrap_function_template(v8::Isolate* isolate, F&& func) +v8::Local wrap_function_template(v8::Isolate* isolate, F&& func, + v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) { using F_type = typename std::decay_t; return v8::FunctionTemplate::New(isolate, &detail::forward_function, - detail::external_data::set(isolate, std::forward(func))); + detail::external_data::set(isolate, std::forward(func)), + v8::Local(), 0, + v8::ConstructorBehavior::kAllow, + side_effect_type); } /// Wrap C++ function into new V8 function /// Set nullptr or empty string for name /// to make the function anonymous template -v8::Local wrap_function(v8::Isolate* isolate, std::string_view name, F&& func) +v8::Local wrap_function(v8::Isolate* isolate, std::string_view name, F&& func, + v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) { 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))).ToLocal(&fn)) + detail::external_data::set(isolate, std::forward(func)), + 0, v8::ConstructorBehavior::kAllow, + side_effect_type).ToLocal(&fn)) { return {}; } if (!name.empty()) { - fn->SetName(to_v8(isolate, name)); + fn->SetName(to_v8_name(isolate, name)); } return fn; } diff --git a/v8pp/module.hpp b/v8pp/module.hpp index 41284bb..0e5890a 100644 --- a/v8pp/module.hpp +++ b/v8pp/module.hpp @@ -44,7 +44,7 @@ class module template module& value(std::string_view name, v8::Local value) { - obj_->Set(v8pp::to_v8(isolate_, name), value); + obj_->Set(v8pp::to_v8_name(isolate_, name), value); return *this; } @@ -60,17 +60,18 @@ class module { v8::HandleScope scope(isolate_); - cl.class_function_template()->SetClassName(v8pp::to_v8(isolate_, name)); + cl.class_function_template()->SetClassName(v8pp::to_v8_name(isolate_, name)); return value(name, cl.js_function_template()); } /// Set a C++ function in the module with specified name template - module& function(std::string_view name, Function&& func) + module& function(std::string_view name, Function&& func, + v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) { 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))); + return value(name, wrap_function_template(isolate_, std::forward(func), side_effect_type)); } /// Set a C++ variable in the module with specified name @@ -80,11 +81,14 @@ class module static_assert(!detail::is_callable::value, "Variable must not be callable"); v8::HandleScope scope(isolate_); - v8::Local v8_name = v8pp::to_v8(isolate_, name); + v8::Local v8_name = v8pp::to_v8_name(isolate_, name); v8::AccessorNameGetterCallback getter = &var_get; v8::AccessorNameSetterCallback setter = &var_set; v8::Local data = detail::external_data::set(isolate_, &var); - obj_->SetNativeDataProperty(v8_name, getter, setter, data, v8::PropertyAttribute::DontDelete); + obj_->SetNativeDataProperty(v8_name, getter, setter, data, + v8::PropertyAttribute::DontDelete, v8::DEFAULT, + v8::SideEffectType::kHasNoSideEffect, + v8::SideEffectType::kHasSideEffectToReceiver); return *this; } @@ -104,11 +108,17 @@ class module v8::HandleScope scope(isolate_); - v8::Local v8_name = v8pp::to_v8(isolate_, name); + v8::Local v8_name = v8pp::to_v8_name(isolate_, name); v8::AccessorNameGetterCallback getter = property_type::template get; 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))); - obj_->SetNativeDataProperty(v8_name, getter, setter, data, v8::PropertyAttribute::DontDelete); + + v8::SideEffectType setter_effect = property_type::is_readonly + ? v8::SideEffectType::kHasSideEffect + : v8::SideEffectType::kHasSideEffectToReceiver; + obj_->SetNativeDataProperty(v8_name, getter, setter, data, + v8::PropertyAttribute::DontDelete, v8::DEFAULT, + v8::SideEffectType::kHasNoSideEffect, setter_effect); return *this; } @@ -117,7 +127,7 @@ class module { v8::HandleScope scope(isolate_); - obj_->Set(v8pp::to_v8(isolate_, name), m.obj_, + obj_->Set(v8pp::to_v8_name(isolate_, name), m.obj_, v8::PropertyAttribute(v8::ReadOnly | v8::DontDelete)); return *this; } @@ -128,7 +138,7 @@ class module { v8::HandleScope scope(isolate_); - obj_->Set(v8pp::to_v8(isolate_, name), to_v8(isolate_, value), + obj_->Set(v8pp::to_v8_name(isolate_, name), to_v8(isolate_, value), v8::PropertyAttribute(v8::ReadOnly | v8::DontDelete)); return *this; } diff --git a/v8pp/utility.hpp b/v8pp/utility.hpp index 755b0a4..25ca896 100644 --- a/v8pp/utility.hpp +++ b/v8pp/utility.hpp @@ -314,4 +314,12 @@ concept callable = std::is_function_v> template using is_callable = std::bool_constant>; +template +inline constexpr bool is_const_member_function_v = false; + +template + requires std::is_member_function_pointer_v +inline constexpr bool is_const_member_function_v = + std::is_const_v::class_type>; + } // namespace v8pp::detail From e8ebabc84f2b94b33a12491019f86dab5a9b6c51 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:25:55 +0100 Subject: [PATCH 09/38] add convert extensions: BigInt, span, ArrayBuffer, set, pair, path, chrono MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New type conversions: - int64_t/uint64_t use BigInt (lossless full-range, accepts Number fallback) - std::span → TypedArray (copy-based, to_v8 only) - std::vector ↔ ArrayBuffer (bidirectional) - std::set/unordered_set ↔ Array - std::pair ↔ [first, second] Array - std::filesystem::path ↔ string - std::chrono::duration ↔ Number (milliseconds) - std::chrono::time_point ↔ Number (epoch milliseconds) Variant converter updated with BigInt dispatch using lossless parameter for correct signed/unsigned 64-bit differentiation. --- test/test_convert.cpp | 355 +++++++++++++++++++++++++++++++--- v8pp/convert.hpp | 433 ++++++++++++++++++++++++++++++++++++++++-- v8pp/utility.hpp | 70 +++++++ 3 files changed, 815 insertions(+), 43 deletions(-) diff --git a/test/test_convert.cpp b/test/test_convert.cpp index 3f4e7a4..bbb7099 100644 --- a/test/test_convert.cpp +++ b/test/test_convert.cpp @@ -3,10 +3,16 @@ #include "test.hpp" +#include #include +#include +#include #include -#include #include +#include +#include +#include +#include template void test_conv(v8::Isolate* isolate, T value, U expected) @@ -309,28 +315,18 @@ void check_range(v8::Isolate* isolate) variant_check check_range{ isolate }; T zero{ 0 }; - T min, max; - if constexpr (std::same_as) - { - min = V8_MIN_INT; - max = V8_MAX_INT; - } - else if constexpr (std::same_as) - { - min = 0; - max = V8_MAX_INT; - } - else - { - min = std::numeric_limits::lowest(); - max = std::numeric_limits::max(); - } + T min = std::numeric_limits::lowest(); + T max = std::numeric_limits::max(); check_range(zero); check_range(min); check_range(max); - check_range.check_ex(std::nextafter(double(min), std::numeric_limits::lowest())); // like min - 1 (out of range) - check_range.check_ex(std::nextafter(double(max), std::numeric_limits::max())); // like max + 1 (out of range) + if constexpr (sizeof(T) <= sizeof(uint32_t)) + { + // For <=32-bit types, test out-of-range doubles + check_range.check_ex(std::nextafter(double(min), std::numeric_limits::lowest())); + check_range.check_ex(std::nextafter(double(max), std::numeric_limits::max())); + } } template @@ -411,12 +407,12 @@ void test_convert_variant(v8::Isolate* isolate) check_vector(std::vector{}, 0.f, std::optional{}); // The order here matters - variant_check order_check{ isolate }; + variant_check order_check{ isolate }; order_check( std::numeric_limits::min(), std::numeric_limits::max(), std::numeric_limits::min(), std::numeric_limits::max(), std::numeric_limits::min(), std::numeric_limits::max(), - //TODO: V8_MIN_INT, V8_MAX_INT, + std::numeric_limits::min(), std::numeric_limits::max(), std::numeric_limits::lowest(), std::numeric_limits::max()); variant_check simple_arithmetic{ isolate }; @@ -429,7 +425,7 @@ void test_convert_variant(v8::Isolate* isolate) objects_only.check_ex(std::string{ "test" }); objects_only.check_ex(1.); - // Note: Not all values of uint64_t/int64_t are possible since v8 stores numeric values as doubles + // BigInt conversion covers full int64_t/uint64_t range check_ranges(isolate); // test map @@ -686,6 +682,314 @@ void test_convert_try_from_v8(v8::Isolate* isolate) check("try shared_ptr from int", !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, 42))); } +void test_convert_bigint(v8::Isolate* isolate) +{ + // Basic round-trip for int64_t + test_conv(isolate, int64_t{0}); + test_conv(isolate, int64_t{42}); + test_conv(isolate, int64_t{-42}); + test_conv(isolate, std::numeric_limits::min()); + test_conv(isolate, std::numeric_limits::max()); + + // Basic round-trip for uint64_t + test_conv(isolate, uint64_t{0}); + test_conv(isolate, uint64_t{42}); + test_conv(isolate, std::numeric_limits::max()); + + // Values beyond double precision round-trip exactly through BigInt + int64_t big_signed = int64_t{1} << 60; + test_conv(isolate, big_signed); + test_conv(isolate, -big_signed); + + uint64_t big_unsigned = uint64_t{1} << 63; + test_conv(isolate, big_unsigned); + + // to_v8 produces BigInt + auto v8_val = v8pp::to_v8(isolate, int64_t{123}); + check("int64_t to_v8 is BigInt", v8_val->IsBigInt()); + + auto v8_uval = v8pp::to_v8(isolate, uint64_t{456}); + check("uint64_t to_v8 is BigInt", v8_uval->IsBigInt()); + + // from_v8 accepts Number for ergonomics + 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}); + + // from_v8 rejects non-numeric types + check_ex("int64_t from string", [isolate]() + { + 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)); + }); + + // try_from_v8 for BigInt + 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("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}; + 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"}; + 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); + check_eq("unordered_set round-trip", str_result, str_set); + + // Empty set + std::set empty_set; + auto v8_empty = v8pp::to_v8(isolate, empty_set); + auto empty_result = v8pp::from_v8>(isolate, v8_empty); + check("empty set", empty_result.empty()); + + // Invalid input + check_ex("set from non-array", [isolate]() + { + 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})); + check("try set valid", try_set.has_value()); + 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"}; + 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); + check_eq("pair first", result.first, 42); + check_eq("pair second", result.second, std::string("hello")); + + // pair + 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)); + }); + + // 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})); + }); + + // try_from_v8 + auto try_pair = v8pp::try_from_v8>(isolate, + 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); + check("try pair from string", !v8pp::try_from_v8>(isolate, v8pp::to_v8(isolate, "x"))); +} + +void test_convert_path(v8::Isolate* isolate) +{ + // Basic round-trip + std::filesystem::path p("some/path/file.txt"); + auto v8_val = v8pp::to_v8(isolate, p); + check("path to_v8 is String", v8_val->IsString()); + auto result = v8pp::from_v8(isolate, v8_val); + check_eq("path round-trip", result, p); + + // Empty path + test_conv(isolate, std::filesystem::path(""), std::filesystem::path("")); + + // try_from_v8 + auto try_path = v8pp::try_from_v8(isolate, v8pp::to_v8(isolate, std::filesystem::path("test"))); + check("try path valid", try_path.has_value()); + check_eq("try path value", *try_path, std::filesystem::path("test")); + check("try path from empty handle", !v8pp::try_from_v8(isolate, v8::Local())); +} + +void test_convert_chrono(v8::Isolate* isolate) +{ + using namespace std::chrono; + + // duration: milliseconds round-trip + 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}); + + // duration: seconds to Number (converts to ms internally) + 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}); + + // duration: microseconds + 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")); + }); + + // time_point: system_clock round-trip + auto now = system_clock::now(); + auto now_ms = time_point_cast(now); + auto v8_now = v8pp::to_v8(isolate, now_ms); + check("time_point to_v8 is Number", v8_now->IsNumber()); + auto now_result = v8pp::from_v8(isolate, v8_now); + auto now_result_ms = time_point_cast(now_result); + check_eq("time_point round-trip ms", + now_result_ms.time_since_epoch().count(), + now_ms.time_since_epoch().count()); + + // time_point: epoch (zero) + system_clock::time_point epoch{}; + auto v8_epoch = v8pp::to_v8(isolate, epoch); + double epoch_ms = v8_epoch->NumberValue(isolate->GetCurrentContext()).FromJust(); + check_eq("epoch to_v8", epoch_ms, 0.0); + + // time_point: invalid input + check_ex("time_point from string", [isolate]() + { + 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})); + check("try duration valid", try_dur.has_value()); + 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}; + 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); + check_eq("arraybuffer round-trip size", result.size(), data.size()); + check("arraybuffer round-trip data", result == data); + + // Empty vector + std::vector empty; + auto v8_empty = v8pp::to_v8(isolate, empty); + check("empty vector to_v8 is ArrayBuffer", v8_empty->IsArrayBuffer()); + auto empty_result = v8pp::from_v8>(isolate, v8_empty); + check("empty arraybuffer", empty_result.empty()); + + // from_v8 from ArrayBufferView (Uint8Array) + { + v8::EscapableHandleScope scope(isolate); + 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); + check("from Uint8Array", view_result == src); + } + + // Invalid input + check_ex("vector from int", [isolate]() + { + v8pp::from_v8>(isolate, v8pp::to_v8(isolate, 42)); + }); + check_ex("vector from string", [isolate]() + { + 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})); + check("try arraybuffer valid", try_buf.has_value()); + 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"))); +} + +void test_convert_span(v8::Isolate* isolate) +{ + // span -> Uint8Array + { + 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()); + // Read back through ArrayBufferView + auto view = v8_val.As(); + check_eq("span length", static_cast(view->Length()), data.size()); + } + + // span -> Int32Array + { + 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()); + auto view = v8_val.As(); + check_eq("span length", static_cast(view->Length()), data.size()); + } + + // span -> Float32Array + { + 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()); + auto view = v8_val.As(); + check_eq("span length", static_cast(view->Length()), data.size()); + } + + // span -> Float64Array + { + 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()); + } + + // Empty span + { + std::span empty; + 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}); + } + + // span data is copied (modifying original doesn't affect JS) + { + std::vector data{10, 20, 30}; + std::span sp(data); + auto v8_val = v8pp::to_v8(isolate, sp); + data[0] = 999; // modify original + auto view = v8_val.As(); + auto buffer = view->Buffer(); + auto* buf_data = static_cast(buffer->GetBackingStore()->Data()); + check_eq("span copy semantics", buf_data[0], 10); // should still be 10 + } +} + void test_convert() { v8pp::context context; @@ -741,4 +1045,11 @@ void test_convert() test_convert_variant(isolate); test_convert_crash_safety(isolate); test_convert_try_from_v8(isolate); + test_convert_bigint(isolate); + test_convert_set(isolate); + test_convert_pair(isolate); + test_convert_path(isolate); + test_convert_chrono(isolate); + test_convert_arraybuffer(isolate); + test_convert_span(isolate); } diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index 3f31aa0..3d06876 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -3,9 +3,13 @@ #include #include +#include #include +#include +#include #include #include +#include #include #include #include @@ -39,6 +43,20 @@ 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; }; +} // namespace detail + struct invalid_argument : std::invalid_argument { invalid_argument(v8::Isolate* isolate, v8::Local value, char const* expected_type); @@ -254,7 +272,9 @@ struct convert } }; +// convert Number <-> integer types that fit in 32 bits template + requires (sizeof(T) <= sizeof(uint32_t)) struct convert { using from_type = T; @@ -275,42 +295,81 @@ struct convert { if (!is_valid(isolate, value)) return std::nullopt; - if constexpr (sizeof(T) <= sizeof(uint32_t)) + if constexpr (std::is_signed_v) + { + return static_cast(value->Int32Value(isolate->GetCurrentContext()).FromJust()); + } + else + { + return static_cast(value->Uint32Value(isolate->GetCurrentContext()).FromJust()); + } + } + + static to_type to_v8(v8::Isolate* isolate, T value) + { + if constexpr (std::is_signed_v) + { + return v8::Integer::New(isolate, static_cast(value)); + } + else + { + return v8::Integer::NewFromUnsigned(isolate, static_cast(value)); + } + } +}; + +// convert BigInt <-> integer types larger than 32 bits (int64_t, uint64_t, etc.) +// to_v8 always produces BigInt. from_v8 accepts both BigInt and Number for ergonomics. +template + requires (sizeof(T) > sizeof(uint32_t)) +struct convert +{ + using from_type = T; + using to_type = v8::Local; + + static bool is_valid(v8::Isolate*, v8::Local value) + { + return !value.IsEmpty() && (value->IsBigInt() || value->IsNumber()); + } + + static from_type from_v8(v8::Isolate* isolate, v8::Local value) + { + if (auto result = try_from_v8(isolate, value)) return *result; + throw invalid_argument(isolate, value, "BigInt"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; + + if (value->IsBigInt()) { + auto bigint = value.As(); if constexpr (std::is_signed_v) { - return static_cast(value->Int32Value(isolate->GetCurrentContext()).FromJust()); + return static_cast(bigint->Int64Value()); } else { - return static_cast(value->Uint32Value(isolate->GetCurrentContext()).FromJust()); + return static_cast(bigint->Uint64Value()); } } else { + // Accept Number for ergonomics (lossy for values > 2^53) return static_cast(value->IntegerValue(isolate->GetCurrentContext()).FromJust()); } } static to_type to_v8(v8::Isolate* isolate, T value) { - if constexpr (sizeof(T) <= sizeof(uint32_t)) + if constexpr (std::is_signed_v) { - if constexpr (std::is_signed_v) - { - return v8::Integer::New(isolate, - static_cast(value)); - } - else - { - return v8::Integer::NewFromUnsigned(isolate, - static_cast(value)); - } + return v8::BigInt::New(isolate, static_cast(value)); } else { - //TODO: check value < (1<::digits)-1 to fit in double? - return v8::Number::New(isolate, static_cast(value)); + return v8::BigInt::NewFromUnsigned(isolate, static_cast(value)); } } }; @@ -524,14 +583,17 @@ struct convert> { return alternate(isolate, value); } + else if (value->IsBigInt()) + { + return alternate(isolate, value); + } else if (value->IsInt32() || value->IsUint32()) { - return alternate(isolate, value); + return alternate(isolate, value); } else if (value->IsNumber()) { - //TODO: 64-bit integers - return alternate(isolate, value); + return alternate(isolate, value); } else if (value->IsString()) { @@ -584,6 +646,12 @@ struct convert> template using is_integral_not_bool = std::bool_constant::value && !is_bool::value>; + template + using is_small_integral = std::bool_constant::value && !is_bool::value && sizeof(T) <= sizeof(uint32_t)>; + + template + using is_large_integral = std::bool_constant::value && !is_bool::value && (sizeof(T) > sizeof(uint32_t))>; + template using is_any = std::true_type; @@ -647,7 +715,37 @@ struct convert> } else if constexpr (is_integral_not_bool::value) { - get_number(isolate, value, result); + if (value->IsBigInt()) + { + auto bigint = value.As(); + bool lossless = false; + if constexpr (std::is_signed_v) + { + int64_t val = bigint->Int64Value(&lossless); + if (lossless) + { + if constexpr (sizeof(T) >= sizeof(int64_t)) + result = static_cast(val); + else if (val >= std::numeric_limits::lowest() && val <= std::numeric_limits::max()) + result = static_cast(val); + } + } + else + { + uint64_t val = bigint->Uint64Value(&lossless); + if (lossless) + { + if constexpr (sizeof(T) >= sizeof(uint64_t)) + result = static_cast(val); + else if (val <= std::numeric_limits::max()) + result = static_cast(val); + } + } + } + else + { + get_number(isolate, value, result); + } } else if constexpr (std::is_floating_point_v) { @@ -819,6 +917,290 @@ struct convert } }; +// convert Array <-> std::set, std::unordered_set +template +struct convert +{ + using from_type = Set; + using to_type = v8::Local; + using item_type = typename Set::value_type; + + static bool is_valid(v8::Isolate*, v8::Local value) + { + return !value.IsEmpty() && value->IsArray(); + } + + static from_type from_v8(v8::Isolate* isolate, v8::Local value) + { + if (auto result = try_from_v8(isolate, value)) return *std::move(result); + throw invalid_argument(isolate, value, "Array"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; + + v8::HandleScope scope(isolate); + v8::Local context = isolate->GetCurrentContext(); + v8::Local array = value.As(); + + from_type result{}; + if constexpr (detail::reservable) + { + result.reserve(array->Length()); + } + + for (uint32_t i = 0, count = array->Length(); i < count; ++i) + { + v8::Local item = array->Get(context, i).ToLocalChecked(); + result.insert(convert::from_v8(isolate, item)); + } + return result; + } + + static to_type to_v8(v8::Isolate* isolate, from_type const& value) + { + 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)); + } + + v8::EscapableHandleScope scope(isolate); + v8::Local context = isolate->GetCurrentContext(); + v8::Local result = v8::Array::New(isolate, static_cast(value.size())); + uint32_t i = 0; + for (item_type const& item : value) + { + result->Set(context, i++, convert::to_v8(isolate, item)).FromJust(); + } + return scope.Escape(result); + } +}; + +// convert [first, second] Array <-> std::pair +template +struct convert, void> +{ + using from_type = std::pair; + using to_type = v8::Local; + + static bool is_valid(v8::Isolate*, v8::Local value) + { + return !value.IsEmpty() && value->IsArray() + && value.As()->Length() == 2; + } + + static from_type from_v8(v8::Isolate* isolate, v8::Local value) + { + if (auto result = try_from_v8(isolate, value)) return *std::move(result); + throw invalid_argument(isolate, value, "Array[2]"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; + + v8::HandleScope scope(isolate); + v8::Local context = isolate->GetCurrentContext(); + v8::Local array = value.As(); + + v8::Local first = array->Get(context, 0).ToLocalChecked(); + v8::Local second = array->Get(context, 1).ToLocalChecked(); + return std::pair{ convert::from_v8(isolate, first), convert::from_v8(isolate, second) }; + } + + static to_type to_v8(v8::Isolate* isolate, from_type const& value) + { + v8::EscapableHandleScope scope(isolate); + v8::Local context = isolate->GetCurrentContext(); + v8::Local result = v8::Array::New(isolate, 2); + result->Set(context, 0, convert::to_v8(isolate, value.first)).FromJust(); + result->Set(context, 1, convert::to_v8(isolate, value.second)).FromJust(); + return scope.Escape(result); + } +}; + +// convert string <-> std::filesystem::path +template<> +struct convert +{ + using from_type = std::filesystem::path; + using to_type = v8::Local; + + static bool is_valid(v8::Isolate* isolate, v8::Local value) + { + return convert::is_valid(isolate, value); + } + + static from_type from_v8(v8::Isolate* isolate, v8::Local value) + { + std::string str = convert::from_v8(isolate, value); + return std::filesystem::path(str); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (auto str = convert::try_from_v8(isolate, value)) + { + std::string s = *std::move(str); + return std::filesystem::path(s); + } + return std::nullopt; + } + + static to_type to_v8(v8::Isolate* isolate, from_type const& value) + { + return convert::to_v8(isolate, value.string()); + } +}; + +// convert Number (milliseconds) <-> std::chrono::duration +template +struct convert, void> +{ + using duration_type = std::chrono::duration; + using from_type = duration_type; + using to_type = v8::Local; + + static bool is_valid(v8::Isolate*, v8::Local value) + { + return !value.IsEmpty() && value->IsNumber(); + } + + static from_type from_v8(v8::Isolate* isolate, v8::Local value) + { + if (auto result = try_from_v8(isolate, value)) return *result; + throw invalid_argument(isolate, value, "Number"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; + double ms = value->NumberValue(isolate->GetCurrentContext()).FromJust(); + return std::chrono::duration_cast( + std::chrono::duration(ms)); + } + + static to_type to_v8(v8::Isolate* isolate, from_type const& value) + { + auto ms = std::chrono::duration_cast>(value); + return v8::Number::New(isolate, ms.count()); + } +}; + +// convert Number (epoch milliseconds) <-> std::chrono::time_point +template +struct convert, void> +{ + using time_point_type = std::chrono::time_point; + using from_type = time_point_type; + using to_type = v8::Local; + + static bool is_valid(v8::Isolate*, v8::Local value) + { + return !value.IsEmpty() && value->IsNumber(); + } + + static from_type from_v8(v8::Isolate* isolate, v8::Local value) + { + if (auto result = try_from_v8(isolate, value)) return *result; + throw invalid_argument(isolate, value, "Number"); + } + + static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) + { + if (!is_valid(isolate, value)) return std::nullopt; + double ms = value->NumberValue(isolate->GetCurrentContext()).FromJust(); + auto epoch_duration = std::chrono::duration_cast( + std::chrono::duration(ms)); + return time_point_type(epoch_duration); + } + + static to_type to_v8(v8::Isolate* isolate, from_type const& value) + { + auto epoch_ms = std::chrono::duration_cast>( + value.time_since_epoch()); + return v8::Number::New(isolate, epoch_ms.count()); + } +}; + +// convert ArrayBuffer <-> std::vector +template<> +struct convert, void> +{ + using from_type = std::vector; + using to_type = v8::Local; + + static bool is_valid(v8::Isolate*, v8::Local value) + { + return !value.IsEmpty() && (value->IsArrayBuffer() || value->IsArrayBufferView()); + } + + static from_type from_v8(v8::Isolate* isolate, v8::Local value) + { + if (auto result = try_from_v8(isolate, value)) return *std::move(result); + throw invalid_argument(isolate, value, "ArrayBuffer"); + } + + static std::optional try_from_v8(v8::Isolate*, v8::Local value) + { + if (value.IsEmpty() || (!value->IsArrayBuffer() && !value->IsArrayBufferView())) + { + return std::nullopt; + } + + v8::Local buffer; + size_t offset = 0; + size_t length = 0; + if (value->IsArrayBufferView()) + { + auto view = value.As(); + buffer = view->Buffer(); + offset = view->ByteOffset(); + length = view->ByteLength(); + } + else + { + buffer = value.As(); + length = buffer->ByteLength(); + } + + auto const* data = static_cast(buffer->GetBackingStore()->Data()) + offset; + return std::vector(data, data + length); + } + + static to_type to_v8(v8::Isolate* isolate, from_type const& value) + { + v8::EscapableHandleScope scope(isolate); + auto backing = v8::ArrayBuffer::NewBackingStore(isolate, value.size()); + std::memcpy(backing->Data(), value.data(), value.size()); + return scope.Escape(v8::ArrayBuffer::New(isolate, std::move(backing))); + } +}; + +// convert TypedArray <- std::span (to_v8 only — span is non-owning) +template +struct convert, void> +{ + using from_type = std::span; + using to_type = v8::Local; + + static to_type to_v8(v8::Isolate* isolate, std::span value) + { + v8::EscapableHandleScope scope(isolate); + size_t byte_length = value.size_bytes(); + auto backing = v8::ArrayBuffer::NewBackingStore(isolate, byte_length); + std::memcpy(backing->Data(), value.data(), byte_length); + auto buffer = v8::ArrayBuffer::New(isolate, std::move(backing)); + using TypedArrayType = typename detail::typed_array_trait::type; + auto typed_array = TypedArrayType::New(buffer, 0, value.size()); + return scope.Escape(typed_array); + } +}; + template struct convert> { @@ -856,7 +1238,11 @@ struct is_wrapped_class : std::conjunction< std::negation>, std::negation>, std::negation>, - std::negation>> + std::negation>, + std::negation>, + std::negation>, + std::negation>, + std::negation>> { }; @@ -876,6 +1262,11 @@ struct is_wrapped_class> : std::false_type { }; +template<> +struct is_wrapped_class : std::false_type +{ +}; + template requires is_wrapped_class::value struct convert diff --git a/v8pp/utility.hpp b/v8pp/utility.hpp index 25ca896..4992430 100644 --- a/v8pp/utility.hpp +++ b/v8pp/utility.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -126,6 +127,75 @@ struct is_array> : std::true_type static constexpr size_t length = N; }; +///////////////////////////////////////////////////////////////////////////// +// +// 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)); + }; + +template +struct is_set : std::bool_constant> +{ +}; + +///////////////////////////////////////////////////////////////////////////// +// +// is_pair +// +template +struct is_pair : std::false_type +{ +}; + +template +struct is_pair> : std::true_type +{ +}; + +///////////////////////////////////////////////////////////////////////////// +// +// is_duration +// +template +struct is_duration : std::false_type +{ +}; + +template +struct is_duration> : std::true_type +{ +}; + +///////////////////////////////////////////////////////////////////////////// +// +// is_time_point +// +template +struct is_time_point : std::false_type +{ +}; + +template +struct is_time_point> : std::true_type +{ +}; + +///////////////////////////////////////////////////////////////////////////// +// +// typed_array_trait — maps C++ types to V8 TypedArray types +// +template +struct typed_array_trait; + +template +concept typed_array_element = requires { typename typed_array_trait::type; }; + ///////////////////////////////////////////////////////////////////////////// // // is_tuple From 929275b0c4fd7b9cc5e941370bef5f7f4016dc1d Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:34:57 +0100 Subject: [PATCH 10/38] add binding features: default parameters, function overloading, V8 Fast API callbacks - add v8pp::defaults<> for trailing default parameter values on bound functions - add call_from_v8 overload with arg_or_default compile-time dispatch - add forward_function_with_defaults V8 callback and wrap_function_template overload - add defaults support to class_::function(), class_::ctor(), module::function(), context::function() - add v8pp::overload() compile-time overload selector - add overload_entry, overload_set, with_defaults() for bundling overloads - add runtime arity+type dispatch in forward_overloaded_function - add variadic .function(name, f1, f2, ...) on class_, module, context - add v8pp::fast_fn<&func> for V8 Fast API JIT fast-path callbacks via NTTP - add is_fast_api_compatible trait, fast_callback wrapper generating CFunction signatures - add fast_function overloads on class_, module, context, wrap_function_template - use ConstructorBehavior::kThrow for fast-path FunctionTemplates (V8 requirement) - replace static_assert with requires clauses on function() to enable SFINAE with fast_function - add test_overload and test_fast_api test suites --- test/CMakeLists.txt | 2 + test/main.cpp | 4 + test/test_call_from_v8.cpp | 87 ++++++++++ test/test_fast_api.cpp | 105 ++++++++++++ test/test_overload.cpp | 122 ++++++++++++++ v8pp/call_from_v8.hpp | 118 +++++++++++++ v8pp/class.hpp | 104 ++++++++++++ v8pp/context.hpp | 30 +++- v8pp/fast_api.hpp | 136 +++++++++++++++ v8pp/function.hpp | 160 ++++++++++++++++++ v8pp/module.hpp | 34 +++- v8pp/overload.hpp | 337 +++++++++++++++++++++++++++++++++++++ 12 files changed, 1237 insertions(+), 2 deletions(-) create mode 100644 test/test_fast_api.cpp create mode 100644 test/test_overload.cpp create mode 100644 v8pp/fast_api.hpp create mode 100644 v8pp/overload.hpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ff24ea4..00b25c3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -8,10 +8,12 @@ add_executable(v8pp_test test_class.cpp test_context.cpp test_convert.cpp + test_fast_api.cpp test_function.cpp test_json.cpp test_module.cpp test_object.cpp + test_overload.cpp test_property.cpp test_ptr_traits.cpp test_throw_ex.cpp diff --git a/test/main.cpp b/test/main.cpp index e5b8162..635adc3 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -27,6 +27,8 @@ void run_tests() void test_property(); void test_object(); void test_json(); + void test_overload(); + void test_fast_api(); std::pair tests[] = { @@ -44,6 +46,8 @@ void run_tests() {"test_property", test_property}, {"test_object", test_object}, {"test_json", test_json}, + {"test_overload", test_overload}, + {"test_fast_api", test_fast_api}, }; for (auto const& test : tests) diff --git a/test/test_call_from_v8.cpp b/test/test_call_from_v8.cpp index 4c5f9c7..cc638b1 100644 --- a/test/test_call_from_v8.cpp +++ b/test/test_call_from_v8.cpp @@ -1,6 +1,8 @@ #include "v8pp/call_from_v8.hpp" +#include "v8pp/class.hpp" #include "v8pp/context.hpp" #include "v8pp/function.hpp" +#include "v8pp/module.hpp" #include "v8pp/ptr_traits.hpp" #include "v8pp/throw_ex.hpp" @@ -98,4 +100,89 @@ void test_call_from_v8() check_eq("y", run_script(context, "y(1)"), 1); check_eq("z", run_script(context, "z(2)"), 2); check_eq("w", run_script(context, "w(2, 'd', true, null)"), 4); + + // --- Default parameter tests --- + + // Free function with 1 default + 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; }; + 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); + check_eq("defaults: 2 defaults, 1 used", run_script(context, "three_args(1, 2)"), 33); + check_eq("defaults: 2 defaults, both used", run_script(context, "three_args(1)"), 51); + + // Too few args should throw + check_ex("defaults: too few args", [&context] + { + 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)"); + }); + + // String default + 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"); + check_eq("defaults: string default overridden", run_script(context, "greet('world', 'hi')"), "hi world"); + + // Module function with defaults + { + v8pp::module m(context.isolate()); + 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); + check_eq("module defaults: default used", run_script(context, "def_mod.multiply(5)"), 10); + } + + // Class member function with defaults + { + struct Counter + { + int value = 0; + int add(int n) { value += n; return value; } + }; + + v8pp::class_ counter_class(context.isolate()); + counter_class + .ctor() + .function("add", &Counter::add, v8pp::defaults(1)); + context.class_("Counter", counter_class); + + check_eq("class defaults: provided", run_script(context, "var c = new Counter(); c.add(5)"), 5); + check_eq("class defaults: default used", run_script(context, "c.add()"), 6); + } + + // Constructor with defaults + { + struct Named + { + std::string name; + int value; + Named(std::string n, int v) : name(std::move(n)), value(v) {} + }; + + v8pp::class_ named_class(context.isolate()); + named_class + .ctor(v8pp::defaults(42)) + .var("name", &Named::name) + .var("value", &Named::value); + context.class_("Named", named_class); + + check_eq("ctor defaults: all provided", run_script(context, "var n1 = new Named('test', 7); n1.value"), 7); + 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"); + } } diff --git a/test/test_fast_api.cpp b/test/test_fast_api.cpp new file mode 100644 index 0000000..e6afe80 --- /dev/null +++ b/test/test_fast_api.cpp @@ -0,0 +1,105 @@ +#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. +} diff --git a/test/test_overload.cpp b/test/test_overload.cpp new file mode 100644 index 0000000..24369dd --- /dev/null +++ b/test/test_overload.cpp @@ -0,0 +1,122 @@ +#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/v8pp/call_from_v8.hpp b/v8pp/call_from_v8.hpp index 939d174..182682d 100644 --- a/v8pp/call_from_v8.hpp +++ b/v8pp/call_from_v8.hpp @@ -8,6 +8,30 @@ #include "v8pp/convert.hpp" #include "v8pp/utility.hpp" +namespace v8pp { + +/// Tag type holding default parameter values for trailing arguments. +/// Defaults fill from the right side of the parameter list, like C++ defaults. +/// Usage: v8pp::defaults(1.0f, "black") for the last 2 parameters. +template +struct defaults +{ + std::tuple values; + explicit constexpr defaults(Defs... args) : values(std::move(args)...) {} +}; + +template +defaults(Defs...) -> defaults; + +/// Type trait to detect defaults<...> +template +struct is_defaults : std::false_type {}; + +template +struct is_defaults> : std::true_type {}; + +} // namespace v8pp + namespace v8pp::detail { template @@ -80,6 +104,7 @@ decltype(auto) call_from_v8_impl(F&& func, v8::FunctionCallbackInfo c } template + 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; @@ -121,4 +146,97 @@ decltype(auto) call_from_v8(F&& func, v8::FunctionCallbackInfo const& } } +/// Extract a single argument: from V8 args if provided, from defaults tuple otherwise. +/// DefaultsStart is the first arg index that has a default value. +template +decltype(auto) arg_or_default(v8::FunctionCallbackInfo const& args, + DefaultsTuple const& defaults_tuple) +{ + if constexpr (Index < DefaultsStart) + { + // No default available — always convert from V8 + return CallTraits::template arg_from_v8(args); + } + else + { + // This parameter has a default value available. + // Both branches must return the same value_type (arg_from_v8 may return a + // proxy type like convertible_string, so explicit cast is needed). + using arg_type = typename CallTraits::template arg_type; + using value_type = std::remove_cv_t>; + + if (static_cast(args.Length()) <= Index) + { + constexpr size_t def_index = Index - DefaultsStart; + return static_cast(std::get(defaults_tuple)); + } + return static_cast( + CallTraits::template arg_from_v8(args)); + } +} + +template +decltype(auto) call_from_v8_defaults_impl(F&& func, v8::FunctionCallbackInfo const& args, + DefaultsTuple const& defaults_tuple, + CallTraits, std::index_sequence, ObjArg&&... obj) +{ + (void)args; + return (std::invoke(func, std::forward(obj)..., + arg_or_default(args, defaults_tuple)...)); +} + +/// call_from_v8 with default parameter values +template +decltype(auto) call_from_v8(F&& func, v8::FunctionCallbackInfo const& args, + v8pp::defaults const& defs, ObjArg&... obj) +{ + constexpr bool with_isolate = is_first_arg_isolate; + + if constexpr (is_direct_args) + { + // direct FunctionCallbackInfo — defaults don't apply + if constexpr (with_isolate) + return (std::invoke(func, obj..., args.GetIsolate(), args)); + else + return (std::invoke(func, obj..., args)); + } + else + { + using call_traits = call_from_v8_traits; + using indices = std::make_index_sequence; + + constexpr size_t num_defaults = sizeof...(Defs); + static_assert(num_defaults <= call_traits::arg_count, + "More defaults than function parameters"); + + constexpr size_t defaults_start = call_traits::arg_count - num_defaults; + constexpr size_t min_args = defaults_start - call_traits::optional_arg_count; + + size_t const arg_count = args.Length(); + if (arg_count > call_traits::arg_count || arg_count < min_args) + { + throw std::runtime_error( + "Argument count does not match function definition. Expected " + + std::to_string(min_args) + ".." + + std::to_string(call_traits::arg_count) + " but got " + + std::to_string(arg_count)); + } + + if constexpr (with_isolate) + { + return (call_from_v8_defaults_impl(std::forward(func), args, + defs.values, call_traits{}, indices{}, obj..., args.GetIsolate())); + } + else + { + return (call_from_v8_defaults_impl(std::forward(func), args, + defs.values, call_traits{}, indices{}, obj...)); + } + } +} + } // namespace v8pp::detail diff --git a/v8pp/class.hpp b/v8pp/class.hpp index e998941..e6234da 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -8,6 +8,7 @@ #include "v8pp/config.hpp" #include "v8pp/function.hpp" +#include "v8pp/overload.hpp" #include "v8pp/property.hpp" #include "v8pp/ptr_traits.hpp" #include "v8pp/type_info.hpp" @@ -277,6 +278,18 @@ class class_ return *this; } + /// Set class constructor signature with default parameter values + template + 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 *this; + } + /// Inhert from C++ class U template class_& inherit() @@ -319,6 +332,62 @@ class class_ return function_impl(name, std::forward(func), side_effect, attr); } + /// Set class member function with default parameter values + template + 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; + return function_impl(name, std::forward(func), std::move(defs), effect, attr); + } + + /// Set class function with Fast API callback + template + class_& function(std::string_view name, fast_function, v8::PropertyAttribute attr = v8::None) + { + 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; + + v8::HandleScope scope(isolate()); + + v8::Local v8_name = v8pp::to_v8_name(isolate(), name); + auto wrapped_fun = wrap_function_template(isolate(), + fast_function{}, side_effect); + + v8::Local js_func = class_info_.js_function_template(); + js_func->PrototypeTemplate()->Set(v8_name, wrapped_fun, attr); + if constexpr (!is_mem_fun) + { + js_func->Set(v8_name, wrapped_fun, attr); + } + return *this; + } + + /// Set multiple overloaded member/static functions + template + 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()); + + v8::Local v8_name = v8pp::to_v8_name(isolate(), name); + v8::Local wrapped_fun = wrap_overload_template(isolate(), + std::forward(f1), std::forward(f2), std::forward(fs)...); + + v8::Local js_func = class_info_.js_function_template(); + js_func->PrototypeTemplate()->Set(v8_name, wrapped_fun); + // Also on constructor for static access + js_func->Set(v8_name, wrapped_fun); + return *this; + } + /// Set class member variable template class_& var(std::string_view name, Attribute attribute) @@ -576,6 +645,41 @@ class class_ return *this; } + template + class_& function_impl(std::string_view name, Function&& func, + v8pp::defaults defs, v8::SideEffectType side_effect, v8::PropertyAttribute attr) + { + constexpr bool is_mem_fun = std::is_member_function_pointer_v; + + static_assert(is_mem_fun || detail::is_callable::value, + "Function must be pointer to member function or callable object"); + + v8::HandleScope scope(isolate()); + + v8::Local v8_name = v8pp::to_v8_name(isolate(), name); + v8::Local wrapped_fun; + + if constexpr (is_mem_fun) + { + using mem_func_type = typename detail::function_traits::template pointer_type; + wrapped_fun = wrap_function_template(isolate(), + mem_func_type(std::forward(func)), std::move(defs), side_effect); + } + else + { + wrapped_fun = wrap_function_template(isolate(), + std::forward(func), std::move(defs), side_effect); + } + + v8::Local js_func = class_info_.js_function_template(); + js_func->PrototypeTemplate()->Set(v8_name, wrapped_fun, attr); + if constexpr (!is_mem_fun) + { + js_func->Set(v8_name, wrapped_fun, attr); + } + return *this; + } + template static void member_get(v8::Local, v8::PropertyCallbackInfo const& info) diff --git a/v8pp/context.hpp b/v8pp/context.hpp index 1c640a2..0dd9431 100644 --- a/v8pp/context.hpp +++ b/v8pp/context.hpp @@ -7,6 +7,7 @@ #include "v8pp/convert.hpp" #include "v8pp/function.hpp" +#include "v8pp/overload.hpp" namespace v8pp { @@ -84,11 +85,38 @@ class context /// Set functions to the context global object template + requires detail::is_callable>::value context& function(std::string_view name, Function&& func) + { + return value(name, wrap_function(isolate_, name, std::forward(func))); + } + + /// Set a Fast API function to the context global object + template + context& function(std::string_view name, fast_function) + { + return value(name, wrap_function(isolate_, name, fast_function{})); + } + + /// Set functions with default parameter values to the context global object + template + context& function(std::string_view name, Function&& func, v8pp::defaults defs) { 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))); + 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) + 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)...)); } /// Set class to the context global object diff --git a/v8pp/fast_api.hpp b/v8pp/fast_api.hpp new file mode 100644 index 0000000..f250e96 --- /dev/null +++ b/v8pp/fast_api.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include + +#include + +#if V8_MAJOR_VERSION >= 10 +#include +#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 && ...)> {}; + +#if V8_MAJOR_VERSION >= 10 + +/// 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) + { + options.fallback = true; + 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) + { + options.fallback = true; + if constexpr (std::is_void_v) return; + else return R{}; + } + return (static_cast(ptr)->*MemPtr)(args...); + } +}; + +#endif // V8_MAJOR_VERSION >= 10 + +} // 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 7a02a97..5788e3e 100644 --- a/v8pp/function.hpp +++ b/v8pp/function.hpp @@ -5,6 +5,7 @@ #include #include "v8pp/call_from_v8.hpp" +#include "v8pp/fast_api.hpp" #include "v8pp/ptr_traits.hpp" #include "v8pp/throw_ex.hpp" #include "v8pp/utility.hpp" @@ -131,6 +132,14 @@ class external_data }; }; +/// Bundles a callable F with its default parameter values +template +struct function_with_defaults +{ + F func; + Defaults defs; +}; + template decltype(auto) invoke(v8::FunctionCallbackInfo const& args) { @@ -178,6 +187,62 @@ void forward_function(v8::FunctionCallbackInfo const& args) } } +/// V8 callback adapter for functions with default parameter values +template +void forward_function_with_defaults(v8::FunctionCallbackInfo const& args) +{ + using FTraits = function_traits; + using Bundle = function_with_defaults; + + static_assert(is_callable::value || std::is_member_function_pointer_v, "required callable F"); + + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + try + { + auto& bundle = external_data::get(args.Data()); + + if constexpr (std::is_member_function_pointer()) + { + using class_type = std::decay_t; + auto obj = class_::unwrap_object(isolate, args.This()); + if (!obj) + { + throw std::runtime_error("method called on null instance"); + } + if constexpr (std::same_as) + { + call_from_v8(std::forward(bundle.func), args, bundle.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::forward(bundle.func), args, bundle.defs, *obj))); + } + } + else + { + if constexpr (std::same_as) + { + call_from_v8(std::forward(bundle.func), args, bundle.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::forward(bundle.func), args, bundle.defs))); + } + } + } + catch (std::exception const& ex) + { + args.GetReturnValue().Set(throw_ex(isolate, ex.what())); + } +} + } // namespace v8pp::detail namespace v8pp { @@ -196,6 +261,48 @@ v8::Local wrap_function_template(v8::Isolate* isolate, F&& side_effect_type); } +/// Wrap C++ function with default parameter values into new V8 function template +template +v8::Local wrap_function_template(v8::Isolate* isolate, F&& func, + v8pp::defaults defs, + v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) +{ + using F_type = typename std::decay_t; + using Defaults = v8pp::defaults; + 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)}), + v8::Local(), 0, + v8::ConstructorBehavior::kAllow, + side_effect_type); +} + +/// Wrap C++ function with default parameter values into new V8 function +template +v8::Local wrap_function(v8::Isolate* isolate, std::string_view name, F&& func, + v8pp::defaults defs, + v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) +{ + using F_type = typename std::decay_t; + using Defaults = v8pp::defaults; + 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)) + { + return {}; + } + if (!name.empty()) + { + fn->SetName(to_v8_name(isolate, name)); + } + return fn; +} + /// Wrap C++ function into new V8 function /// Set nullptr or empty string for name /// to make the function anonymous @@ -220,4 +327,57 @@ v8::Local wrap_function(v8::Isolate* isolate, std::string_view nam return fn; } +/// Wrap a fast_function into a V8 function template with optional Fast API callback. +/// Compatible signatures get a CFunction for the V8 JIT fast path. +/// Incompatible signatures or V8 < 10 silently fall back to slow-only. +template +v8::Local wrap_function_template(v8::Isolate* isolate, + fast_function, + v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) +{ + using F_type = typename fast_function::func_type; +#if V8_MAJOR_VERSION >= 10 + if constexpr (fast_function::compatible) + { + auto c_func = v8::CFunction::Make(&detail::fast_callback::call); + return v8::FunctionTemplate::New(isolate, + &detail::forward_function, + detail::external_data::set(isolate, FuncPtr), + v8::Local(), 0, + v8::ConstructorBehavior::kThrow, + side_effect_type, + &c_func); + } + else +#endif + { + return v8::FunctionTemplate::New(isolate, + &detail::forward_function, + detail::external_data::set(isolate, FuncPtr), + v8::Local(), 0, + v8::ConstructorBehavior::kAllow, + side_effect_type); + } +} + +/// Wrap a fast_function into a V8 function (for context global bindings) +template +v8::Local wrap_function(v8::Isolate* isolate, std::string_view name, + fast_function, + v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) +{ + auto tmpl = wrap_function_template(isolate, + fast_function{}, side_effect_type); + v8::Local fn; + if (!tmpl->GetFunction(isolate->GetCurrentContext()).ToLocal(&fn)) + { + return {}; + } + if (!name.empty()) + { + fn->SetName(to_v8_name(isolate, name)); + } + return fn; +} + } // namespace v8pp diff --git a/v8pp/module.hpp b/v8pp/module.hpp index 0e5890a..45c92b8 100644 --- a/v8pp/module.hpp +++ b/v8pp/module.hpp @@ -3,6 +3,7 @@ #include #include "v8pp/function.hpp" +#include "v8pp/overload.hpp" #include "v8pp/property.hpp" namespace v8pp { @@ -66,12 +67,43 @@ class module /// Set a C++ function in the module with specified name template + requires detail::is_callable>::value module& function(std::string_view name, Function&& func, v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) + { + return value(name, wrap_function_template(isolate_, std::forward(func), side_effect_type)); + } + + /// Set a Fast API C++ function in the module with specified name + template + 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)); + } + + /// Set a C++ function with default parameter values in the module + template + module& function(std::string_view name, Function&& func, v8pp::defaults defs, + v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) { 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), 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) + 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)...)); } /// Set a C++ variable in the module with specified name diff --git a/v8pp/overload.hpp b/v8pp/overload.hpp new file mode 100644 index 0000000..bd1adc1 --- /dev/null +++ b/v8pp/overload.hpp @@ -0,0 +1,337 @@ +#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; +} + +/// 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 || [&] + { + using Entry = std::decay_t; + using F = typename entry_func_type::type; + + // Arity check + constexpr size_t min = overload_arg_range::min_args; + constexpr size_t max = overload_arg_range::max_args; + if (arg_count < min || arg_count > max) + return false; + + // Type check + if (arg_count > 0 && !overload_types_match(isolate, args, arg_count)) + return false; + + // Try invoking + try + { + matched = try_invoke_entry(entries, args); + return matched; + } + catch (std::exception const& ex) + { + if (!errors.empty()) errors += "; "; + 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 From 2211a978904a4d41280d9e79e4bc2f81fe5b064b Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:02:42 +0100 Subject: [PATCH 11/38] add symbol protocol support, iterator protocol, and promise wrapper - to_string_tag() for custom Object.prototype.toString output - to_primitive() for type coercion (member functions and lambdas) - iterable() for Symbol.iterator with member or lambda begin/end - promise and promise with convert specialization --- test/CMakeLists.txt | 2 + test/main.cpp | 4 + test/test_promise.cpp | 206 ++++++++++++++++++++++++++++++++++ test/test_symbol.cpp | 242 ++++++++++++++++++++++++++++++++++++++++ v8pp/CMakeLists.txt | 2 + v8pp/class.hpp | 252 ++++++++++++++++++++++++++++++++++++++++++ v8pp/promise.hpp | 176 +++++++++++++++++++++++++++++ 7 files changed, 884 insertions(+) create mode 100644 test/test_promise.cpp create mode 100644 test/test_symbol.cpp create mode 100644 v8pp/promise.hpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 00b25c3..ef7314a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,7 +14,9 @@ add_executable(v8pp_test test_module.cpp test_object.cpp test_overload.cpp + test_promise.cpp test_property.cpp + test_symbol.cpp test_ptr_traits.cpp test_throw_ex.cpp test_type_info.cpp diff --git a/test/main.cpp b/test/main.cpp index 635adc3..4cb9590 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -29,6 +29,8 @@ void run_tests() void test_json(); void test_overload(); void test_fast_api(); + void test_symbol(); + void test_promise(); std::pair tests[] = { @@ -48,6 +50,8 @@ void run_tests() {"test_json", test_json}, {"test_overload", test_overload}, {"test_fast_api", test_fast_api}, + {"test_symbol", test_symbol}, + {"test_promise", test_promise}, }; for (auto const& test : tests) diff --git a/test/test_promise.cpp b/test/test_promise.cpp new file mode 100644 index 0000000..4409ac6 --- /dev/null +++ b/test/test_promise.cpp @@ -0,0 +1,206 @@ +#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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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 + { + 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 + { + v8pp::promise p(isolate); + p.resolve(1); + return p; + }); + + check_eq("is a Promise", + run_script(context, "isPromiseTest() instanceof Promise"), + true); + } +} diff --git a/test/test_symbol.cpp b/test/test_symbol.cpp new file mode 100644 index 0000000..6da22f8 --- /dev/null +++ b/test/test_symbol.cpp @@ -0,0 +1,242 @@ +#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/v8pp/CMakeLists.txt b/v8pp/CMakeLists.txt index 7152416..e2243c8 100644 --- a/v8pp/CMakeLists.txt +++ b/v8pp/CMakeLists.txt @@ -30,6 +30,8 @@ set(V8PP_HEADERS json.hpp module.hpp object.hpp + overload.hpp + promise.hpp property.hpp ptr_traits.hpp throw_ex.hpp diff --git a/v8pp/class.hpp b/v8pp/class.hpp index e6234da..15b7fa5 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -198,6 +198,184 @@ class classes namespace v8pp { +template +class class_; + +namespace detail { + +/// Holds begin/end function objects and provides V8 callbacks for the iterator protocol. +/// Used by class_::iterable() to implement Symbol.iterator. +template +struct iterator_factory +{ + BeginFn begin_fn; + EndFn end_fn; + + using begin_result = std::invoke_result_t; + using end_result = std::invoke_result_t; + + struct state + { + begin_result current; + end_result end; + v8::Global container_ref; // prevent GC of container during iteration + }; + + static void iterator_callback(v8::FunctionCallbackInfo const& args) + { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + try + { + auto self = class_::unwrap_object(isolate, args.This()); + if (!self) + { + args.GetReturnValue().Set( + throw_ex(isolate, "calling [Symbol.iterator] on null instance")); + return; + } + + decltype(auto) factory = external_data::get(args.Data()); + + auto* iter_state = new state{ + std::invoke(factory.begin_fn, std::as_const(*self)), + std::invoke(factory.end_fn, std::as_const(*self)), + v8::Global(isolate, args.This().As()) + }; + + v8::Local context = isolate->GetCurrentContext(); + v8::Local iter_obj = v8::Object::New(isolate); + + v8::Local state_ext = v8::External::New(isolate, iter_state); + v8::Local next_fn; + if (!v8::Function::New(context, &next_callback, state_ext).ToLocal(&next_fn)) + { + delete iter_state; + args.GetReturnValue().Set(throw_ex(isolate, "failed to create iterator next()")); + return; + } + + 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)}; + weak_data->handle.SetWeak(weak_data, weak_callback, v8::WeakCallbackType::kParameter); + + args.GetReturnValue().Set(iter_obj); + } + catch (std::exception const& ex) + { + args.GetReturnValue().Set(throw_ex(isolate, ex.what())); + } + } + + static void next_callback(v8::FunctionCallbackInfo const& args) + { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + try + { + auto* iter_state = static_cast( + args.Data().As()->Value()); + + v8::Local context = isolate->GetCurrentContext(); + v8::Local result = v8::Object::New(isolate); + + if (iter_state->current == iter_state->end) + { + result->Set(context, + 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(); + } + else + { + result->Set(context, + 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(); + ++iter_state->current; + } + + args.GetReturnValue().Set(result); + } + catch (std::exception const& ex) + { + args.GetReturnValue().Set(throw_ex(isolate, ex.what())); + } + } + + struct weak_ref_data + { + state* iter_state; + v8::Global handle; + }; + + static void weak_callback(v8::WeakCallbackInfo const& info) + { + auto* ref = info.GetParameter(); + delete ref->iter_state; + ref->handle.Reset(); + delete ref; + } +}; + +/// Wraps a non-member callable for Symbol.toPrimitive, unwrapping `this` from args.This(). +/// The callable signature: ReturnType(T const&, std::string_view hint) +template +struct to_primitive_invoker +{ + Function func; + + static void callback(v8::FunctionCallbackInfo const& args) + { + v8::Isolate* isolate = args.GetIsolate(); + v8::HandleScope scope(isolate); + + try + { + auto self = class_::unwrap_object(isolate, args.This()); + if (!self) + { + args.GetReturnValue().Set( + throw_ex(isolate, "calling [Symbol.toPrimitive] on null instance")); + return; + } + + decltype(auto) invoker = external_data::get(args.Data()); + + std::string hint; + if (args.Length() > 0) + { + hint = from_v8(isolate, args[0]); + } + + using return_type = typename function_traits::return_type; + if constexpr (std::same_as) + { + std::invoke(invoker.func, std::as_const(*self), std::string_view(hint)); + } + else + { + args.GetReturnValue().Set( + to_v8(isolate, std::invoke(invoker.func, std::as_const(*self), std::string_view(hint)))); + } + } + catch (std::exception const& ex) + { + args.GetReturnValue().Set(throw_ex(isolate, ex.what())); + } + } +}; + +} // namespace detail + /// Interface to access C++ classes bound to V8 template class class_ @@ -505,6 +683,80 @@ class class_ return *this; } + /// Set Symbol.toStringTag on prototype for custom [object Tag] output + class_& to_string_tag(std::string_view tag) + { + v8::Isolate* iso = isolate(); + v8::HandleScope scope(iso); + + class_info_.js_function_template()->PrototypeTemplate()->Set( + v8::Symbol::GetToStringTag(iso), v8pp::to_v8(iso, tag), + v8::PropertyAttribute(v8::ReadOnly | v8::DontEnum | v8::DontDelete)); + return *this; + } + + /// Set Symbol.toPrimitive on prototype for custom type coercion + /// func signature: ReturnType(std::string_view hint) as member, or + /// ReturnType(T const&, std::string_view hint) as free function/lambda + template + class_& to_primitive(Function&& func) + { + constexpr bool is_mem_fun = std::is_member_function_pointer_v; + + static_assert(is_mem_fun || detail::is_callable::value, + "Function must be pointer to member function or callable object"); + + v8::Isolate* iso = isolate(); + v8::HandleScope scope(iso); + + v8::Local wrapped_fun; + if constexpr (is_mem_fun) + { + using mem_func_type = typename detail::function_traits::template pointer_type; + wrapped_fun = wrap_function_template(iso, + mem_func_type(std::forward(func)), + v8::SideEffectType::kHasNoSideEffect); + } + else + { + using Invoker = detail::to_primitive_invoker>; + v8::Local data = detail::external_data::set(iso, + 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); + } + + class_info_.js_function_template()->PrototypeTemplate()->Set( + v8::Symbol::GetToPrimitive(iso), wrapped_fun, + v8::PropertyAttribute(v8::DontEnum | v8::DontDelete)); + return *this; + } + + /// Make this class iterable in JavaScript (for...of, spread, Array.from, etc.) + /// begin_fn and end_fn: member functions or callables taking T const& and returning iterators + template + class_& iterable(BeginFn&& begin_fn, EndFn&& end_fn) + { + v8::Isolate* iso = isolate(); + v8::HandleScope scope(iso); + + using Factory = detail::iterator_factory, 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))}); + + v8::Local iter_tmpl = v8::FunctionTemplate::New(iso, + &Factory::iterator_callback, data); + + class_info_.js_function_template()->PrototypeTemplate()->Set( + v8::Symbol::GetIterator(iso), iter_tmpl, + v8::PropertyAttribute(v8::DontEnum | v8::DontDelete)); + return *this; + } + /// Set value as a class static property template class_& static_(std::string_view const& name, Value const& value, bool readonly = false) diff --git a/v8pp/promise.hpp b/v8pp/promise.hpp new file mode 100644 index 0000000..cbfc224 --- /dev/null +++ b/v8pp/promise.hpp @@ -0,0 +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 From 61a92d45961a494416fdcb0cae7d25158b169ee4 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:45:24 +0100 Subject: [PATCH 12/38] add context_store for cross-context value persistence - add v8pp::context_store class backed by a dedicated V8 context - store uses isolated v8::Object (not context global) to avoid built-in property pollution - support dot-separated names with auto-creating intermediate objects (ensure_subobjects) - add typed set/get convenience methods via convert - add JSON deep-copy methods (set_json/get_json) for cross-context value cloning - add bulk save_from/restore_to for migrating named values between contexts - add 12-case test suite covering lifecycle, move semantics, and cross-context survival - register context_store in CMake build system (header-only and compiled modes) --- test/CMakeLists.txt | 1 + test/main.cpp | 2 + test/test_context_store.cpp | 319 ++++++++++++++++++++++++++++++++++ v8pp/CMakeLists.txt | 3 + v8pp/context_store.cpp | 5 + v8pp/context_store.hpp | 139 +++++++++++++++ v8pp/context_store.ipp | 338 ++++++++++++++++++++++++++++++++++++ 7 files changed, 807 insertions(+) create mode 100644 test/test_context_store.cpp create mode 100644 v8pp/context_store.cpp create mode 100644 v8pp/context_store.hpp create mode 100644 v8pp/context_store.ipp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ef7314a..71a12ab 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,6 +7,7 @@ add_executable(v8pp_test test_call_v8.cpp test_class.cpp test_context.cpp + test_context_store.cpp test_convert.cpp test_fast_api.cpp test_function.cpp diff --git a/test/main.cpp b/test/main.cpp index 4cb9590..a9798db 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -16,6 +16,7 @@ void run_tests() void test_type_info(); void test_utility(); void test_context(); + void test_context_store(); void test_convert(); void test_throw_ex(); void test_call_v8(); @@ -37,6 +38,7 @@ void run_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}, diff --git a/test/test_context_store.cpp b/test/test_context_store.cpp new file mode 100644 index 0000000..d37ed71 --- /dev/null +++ b/test/test_context_store.cpp @@ -0,0 +1,319 @@ +#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/v8pp/CMakeLists.txt b/v8pp/CMakeLists.txt index e2243c8..93cb36d 100644 --- a/v8pp/CMakeLists.txt +++ b/v8pp/CMakeLists.txt @@ -25,6 +25,7 @@ set(V8PP_HEADERS call_v8.hpp class.hpp context.hpp + context_store.hpp convert.hpp function.hpp json.hpp @@ -43,6 +44,7 @@ set(V8PP_HEADERS if(V8PP_HEADER_ONLY) list(APPEND V8PP_HEADERS class.ipp + context_store.ipp json.ipp throw_ex.ipp version.ipp @@ -52,6 +54,7 @@ else() set(V8PP_SOURCES class.cpp context.cpp + context_store.cpp convert.cpp json.cpp throw_ex.cpp diff --git a/v8pp/context_store.cpp b/v8pp/context_store.cpp new file mode 100644 index 0000000..dfbc2b7 --- /dev/null +++ b/v8pp/context_store.cpp @@ -0,0 +1,5 @@ +#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 new file mode 100644 index 0000000..d68c51b --- /dev/null +++ b/v8pp/context_store.hpp @@ -0,0 +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 diff --git a/v8pp/context_store.ipp b/v8pp/context_store.ipp new file mode 100644 index 0000000..ed82dba --- /dev/null +++ b/v8pp/context_store.ipp @@ -0,0 +1,338 @@ +#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 From 664514db50334d9879a9d0d93aa40536b96b9b07 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:13:05 +0100 Subject: [PATCH 13/38] add test suite expansion and fix constructor-without-new crash - add IsConstructCall() guard in class.ipp constructor callback to throw a JS exception instead of crashing when a bound class is called without new - add test_gc_stress.cpp: bulk 10k object lifecycle, mixed C++/JS lifespans, rapid create-destroy cycles, inheritance cleanup (raw_ptr and shared_ptr) - add test_adversarial.cpp: adversarial JS tests (Proxy, defineProperty, Object.freeze, null prototype, constructor-without-new, prototype swap, wrong-receiver calls, deep prototype chains) and exception safety tests (throwing ctor/method/property, use-after-destroy) - add test_thread_safety.cpp: concurrent isolates, cross-isolate shared_ptr (sequential + concurrent), isolate independence with same-named classes - fix test_type_info.cpp for PRETTIFY_TYPENAMES=OFF with conditional checks and unconditional ID uniqueness/stability/alias/nonzero assertions - update test/CMakeLists.txt and test/main.cpp to register new tests --- test/CMakeLists.txt | 3 + test/main.cpp | 6 + test/test_adversarial.cpp | 320 +++++++++++++++++++++++++++++++++++ test/test_gc_stress.cpp | 224 ++++++++++++++++++++++++ test/test_thread_safety.cpp | 327 ++++++++++++++++++++++++++++++++++++ test/test_type_info.cpp | 47 +++++- v8pp/class.ipp | 5 + 7 files changed, 927 insertions(+), 5 deletions(-) create mode 100644 test/test_adversarial.cpp create mode 100644 test/test_gc_stress.cpp create mode 100644 test/test_thread_safety.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 71a12ab..ec0aa41 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,6 +3,7 @@ add_executable(v8pp_test main.cpp test.hpp + test_adversarial.cpp test_call_from_v8.cpp test_call_v8.cpp test_class.cpp @@ -11,6 +12,7 @@ add_executable(v8pp_test test_convert.cpp test_fast_api.cpp test_function.cpp + test_gc_stress.cpp test_json.cpp test_module.cpp test_object.cpp @@ -18,6 +20,7 @@ add_executable(v8pp_test test_promise.cpp test_property.cpp test_symbol.cpp + test_thread_safety.cpp test_ptr_traits.cpp test_throw_ex.cpp test_type_info.cpp diff --git a/test/main.cpp b/test/main.cpp index a9798db..fac1f77 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -32,6 +32,9 @@ void run_tests() void test_fast_api(); void test_symbol(); void test_promise(); + void test_gc_stress(); + void test_adversarial(); + void test_thread_safety(); std::pair tests[] = { @@ -54,6 +57,9 @@ void run_tests() {"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) diff --git a/test/test_adversarial.cpp b/test/test_adversarial.cpp new file mode 100644 index 0000000..3085939 --- /dev/null +++ b/test/test_adversarial.cpp @@ -0,0 +1,320 @@ +#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_gc_stress.cpp b/test/test_gc_stress.cpp new file mode 100644 index 0000000..4c1543b --- /dev/null +++ b/test/test_gc_stress.cpp @@ -0,0 +1,224 @@ +#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() + .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_thread_safety.cpp b/test/test_thread_safety.cpp new file mode 100644 index 0000000..5c70ed0 --- /dev/null +++ b/test/test_thread_safety.cpp @@ -0,0 +1,327 @@ +#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 6667379..07c9c5b 100644 --- a/test/test_type_info.cpp +++ b/test/test_type_info.cpp @@ -9,9 +9,46 @@ void test_type_info() { using v8pp::detail::type_id; - check_eq("type_id", type_id().name(), "int"); - check_eq("type_id", type_id().name(), "bool"); - check_eq("type_id", type_id().name(), "some_struct"); - check_eq("type_id", type_id().name(), "test::some_class"); - check_eq("type_id", type_id().name(), "test::some_class"); +#if V8PP_PRETTIFY_TYPENAMES + // When prettification is enabled, we get clean type names + check_eq("type_id int", type_id().name(), "int"); + check_eq("type_id bool", type_id().name(), "bool"); + check_eq("type_id some_struct", type_id().name(), "some_struct"); + check_eq("type_id test::some_class", type_id().name(), "test::some_class"); + check_eq("type_id other_class", type_id().name(), "test::some_class"); +#else + // When prettification is disabled, names are raw compiler output. + // Verify names are non-empty and contain the type name substring. + check("type_id non-empty", !type_id().name().empty()); + check("type_id non-empty", !type_id().name().empty()); + check("type_id non-empty", !type_id().name().empty()); + + check("type_id contains 'int'", + type_id().name().find("int") != std::string_view::npos); + check("type_id contains 'bool'", + type_id().name().find("bool") != std::string_view::npos); + check("type_id contains 'some_struct'", + type_id().name().find("some_struct") != std::string_view::npos); + check("type_id contains 'some_class'", + type_id().name().find("some_class") != std::string_view::npos); +#endif + + // These checks must pass regardless of V8PP_PRETTIFY_TYPENAMES + + // IDs are unique per type + check("int != bool", type_id() != type_id()); + check("int != some_struct", type_id() != type_id()); + check("bool != some_struct", type_id() != type_id()); + check("some_struct != some_class", type_id() != type_id()); + + // IDs are stable (same call returns same ID) + check("int == int", type_id() == type_id()); + check("some_struct == some_struct", type_id() == type_id()); + + // Type alias produces same ID as original type + check("other_class == some_class", type_id() == type_id()); + + // ID values are non-zero + check("int id nonzero", type_id().id() != 0); + check("bool id nonzero", type_id().id() != 0); } diff --git a/v8pp/class.ipp b/v8pp/class.ipp index 4c11977..35ffa95 100644 --- a/v8pp/class.ipp +++ b/v8pp/class.ipp @@ -52,6 +52,11 @@ V8PP_IMPL object_registry::object_registry(v8::Isolate* isolate, type_in [](v8::FunctionCallbackInfo const& args) { v8::Isolate* isolate = args.GetIsolate(); + if (!args.IsConstructCall()) + { + args.GetReturnValue().Set(throw_ex(isolate, "must be called with new")); + return; + } object_registry* this_ = external_data::get(args.Data()); try { From a62e3d4278f8ec618e852506392ccd6c1d6bd4b4 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:22:37 +0100 Subject: [PATCH 14/38] modernize CI workflow and improve reliability - update actions/checkout from v2 to v4 (Node 12 EOL) - update gha-setup-ninja from v3 to v5 (Node 20) - add permissions: contents: read for least-privilege security - add concurrency group to cancel stale in-flight runs - set fail-fast: false to surface all platform failures - add timeout-minutes: 30 to prevent stuck jobs - cache NuGet V8 packages on Windows - upload CTest logs as artifacts on failure --- .github/workflows/cmake.yml | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 8e05159..20fda95 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -1,8 +1,18 @@ name: CMake -on: [ push, pull_request ] +on: [push, pull_request] + +permissions: + contents: read + +concurrency: + group: ${{github.workflow}}-${{github.ref}} + cancel-in-progress: true + jobs: build: + timeout-minutes: 30 strategy: + fail-fast: false matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] build_type: [ Release ] @@ -24,7 +34,7 @@ jobs: name: '${{matrix.os}} ${{matrix.build_type}} shared_lib=${{matrix.shared_lib}} header_only=${{matrix.header_only}} v8_compress_pointers=${{matrix.v8_compress_pointers}} v8_enable_sandbox=${{matrix.v8_enable_sandbox}}' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install V8 apt if: startsWith(matrix.os, 'ubuntu') @@ -34,11 +44,18 @@ jobs: if: startsWith(matrix.os, 'macos') run: brew install v8 + - name: Cache NuGet packages + if: startsWith(matrix.os, 'windows') + uses: actions/cache@v4 + with: + path: ${{github.workspace}}/build/v8-v143-x64* + key: nuget-v8-v143-x64-${{runner.os}} + - name: Install V8 nuget if: startsWith(matrix.os, 'windows') run: nuget install v8-v143-x64 -OutputDirectory ${{github.workspace}}/build - - name: Install Visual C++ + - name: Install Visual C++ if: startsWith(matrix.os, 'windows') uses: ilammy/msvc-dev-cmd@v1 with: @@ -46,7 +63,7 @@ jobs: vsversion: 2022 - name: Install ninja-build tool - uses: seanmiddleditch/gha-setup-ninja@v3 + uses: seanmiddleditch/gha-setup-ninja@v5 - 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}} @@ -57,3 +74,11 @@ jobs: - name: Test working-directory: ${{github.workspace}}/build run: ctest -C ${{matrix.build_type}} -V + + - name: Upload test logs on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: test-logs-${{matrix.os}}-shared${{matrix.shared_lib}}-ho${{matrix.header_only}} + path: ${{github.workspace}}/build/Testing/ + retention-days: 7 From 7a8fd52c96014439c5fe97b1566d248b31b287f5 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:24:44 +0100 Subject: [PATCH 15/38] update README to reflect fork state and new features - update CI badge URL to MangelSpec/v8pp fork - remove upstream-specific badges (NPM, Gitter, DeepWiki) - add fork introduction with link back to upstream pmed/v8pp - update requirements to C++20 and V8 9.0+ - add "What this fork adds" section covering performance, crash safety, new binding features, expanded type conversions, V8 API compatibility, and C++20 modernization - add usage examples for function overloading, default parameters, symbol protocols, iterators, and promise support - add full type conversion table (25 types including BigInt, span, ArrayBuffer, set, pair, path, chrono) - add class binding example showing const_property - update build instructions to Ninja-based workflow - present V8 installation and CMake options as tables - add V8PP_PRETTIFY_TYPENAMES and V8_ENABLE_SANDBOX to CMake options table - consolidate external object section (reference_external + import_external) - remove verbose file_reader/file_writer plugin example, keep console example - add License and Upstream sections - add test log upload step on CI failure --- README.md | 475 ++++++++++++++++++++++++------------------------------ 1 file changed, 207 insertions(+), 268 deletions(-) diff --git a/README.md b/README.md index 00e4958..8dc6f3b 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,113 @@ -[![Build status](https://github.com/pmed/v8pp/actions/workflows/cmake.yml/badge.svg)](https://github.com/pmed/v8pp/actions/workflows/cmake.yml) -[![NPM](https://img.shields.io/npm/v/v8pp.svg)](https://npmjs.com/package/v8pp) -[![Join the chat at https://gitter.im/pmed/v8pp](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/pmed/v8pp?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/pmed/v8pp) +[![Build status](https://github.com/MangelSpec/v8pp/actions/workflows/cmake.yml/badge.svg)](https://github.com/MangelSpec/v8pp/actions/workflows/cmake.yml) # v8pp -Header-only library to expose C++ classes and functions into [V8](https://developers.google.com/v8/) to use them in JavaScript code. v8pp requires a compiler with C++17 support. The library has been tested on: - - * Microsoft Visual C++ 2019 (Windows 10) - * GCC 5.4.0 (Ubuntu 16.04) - * Clang 5.0.0 (Ubuntu 16.04) +Header-only C++ library to expose C++ classes and functions into [V8](https://developers.google.com/v8/) JavaScript engine. + +This is a fork of [pmed/v8pp](https://github.com/pmed/v8pp) with performance +optimizations, crash safety hardening, expanded type conversions, and new binding +features. The fork is fully merged with upstream — all upstream changes are +included here. + +**Requirements:** C++20 compiler, V8 9.0+ + +Tested on: + * MSVC 2022 (Windows) + * GCC (Ubuntu) + * Clang (macOS) + +## What this fork adds + +**Performance** +- Integer-based type IDs for O(1) type comparison (replaces string comparison) +- `unordered_map` class registry for O(1) class lookup (replaces linear scan) +- `SideEffectType` hints on function bindings for TurboFan optimization +- Internalized V8 strings for frequently-used property names +- `V8PP_PRETTIFY_TYPENAMES` toggle to skip string processing in production +- Iterative subobject traversal in `object.hpp` (replaces recursion) +- Fast aliases (`get_option_fast`, `set_option_fast`) that skip dot-path parsing + +**Crash safety** +- Null checks on all `unwrap_object` results in property/member accessors +- Prototype chain depth limit (16) in `unwrap_object` prevents infinite loops +- Magic number validation (`0xC1A5517F`) on object registry before `static_cast` +- Use-after-free protection in `context::require()` via weak pointer pattern +- `try_from_v8` — exception-free conversion returning `std::optional` +- Script-reachable `ToLocalChecked` paths replaced with `ToLocal` + proper error handling +- `FromJust` → `FromMaybe` on script-reachable paths +- Member pointer bitcast exclusion (prevents V8 debug assertion crash) + +**New binding features** +- Function overloading with `v8pp::overload()` +- Default parameters with `v8pp::defaults()` +- V8 Fast API callbacks (auto-generated for compatible signatures, V8 10+) +- `const_property` — evaluated once at wrap time, stored as read-only own property +- `v8pp::promise` — synchronous wrapper around `v8::Promise::Resolver` +- Symbol protocol support (`Symbol.toStringTag`, `Symbol.toPrimitive`, `Symbol.iterator`) +- Iterator protocol (`class_::iterable(begin, end)` for `for...of` support) +- `context_store` — cross-context key-value persistence on the same isolate + +**Expanded type conversions** +- `int64_t`/`uint64_t` ↔ `BigInt` +- `std::span` → TypedArrays (`Uint8Array`, `Float32Array`, etc.) +- `std::vector` ↔ `ArrayBuffer` +- `std::set`/`std::unordered_set` ↔ `Array` +- `std::pair` ↔ two-element `Array` +- `std::filesystem::path` ↔ `String` +- `std::chrono::duration`/`time_point` ↔ `Number` (milliseconds) +- `std::monostate` in `std::variant` for `null`/`undefined` + +**V8 API compatibility** +- V8 12.9+: `SetNativeDataProperty` on `InstanceTemplate` (replaces removed `SetAccessor`) +- V8 13.3+: `ExternalMemoryAccounter` for string conversion +- Fixed constructor object identity (`wrap_this` uses `args.This()` instead of creating a second object) +- All bindings registered on `js_function_template` (fixes inheritance and `super` calls) + +**C++20 modernization** +- Concepts replace SFINAE for type dispatch (`mapping`, `sequence`, `set_like`, `callable`, etc.) +- Dead V8 < 9.0 code paths removed + +For a detailed change-by-change analysis, see [docs/FORK_CHANGES.md](docs/FORK_CHANGES.md). ## Building and testing The library has a set of tests that can be configured, built, and run with CMake: ```console -~/v8pp$ mkdir out; cd out -~/v8pp/out$ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON .. -~/v8pp/out$ make -~/v8pp/out$ ctest -V +cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON +cmake --build build --config Release +cd build && ctest -C Release -V ``` -The full list of project options can be listed with cmake command: +### V8 installation -```console -~/v8pp/out$ cmake -LH .. -``` +| Platform | Command | +|----------|---------| +| Windows | `nuget install v8-v143-x64` or vcpkg | +| macOS | `brew install v8` | +| Linux | `sudo apt install libv8-dev` | -Some of them could be: - -> // Build documentation -> BUILD_DOCUMENTATION:BOOL=OFF -> -> // Build shared library -> BUILD_SHARED_LIBS:BOOL=ON -> -> // Build and run tests -> BUILD_TESTING:BOOL=OFF -> -> // Header-only library -> V8PP_HEADER_ONLY:BOOL=0 -> -> // v8::Isolate data slot number, used in v8pp for shared data -> V8PP_ISOLATE_DATA_SLOT:STRING=0 -> -> // v8pp plugin initialization procedure name -> V8PP_PLUGIN_INIT_PROC_NAME:STRING=v8pp_module_init -> -> // v8pp plugin filename suffix -> V8PP_PLUGIN_SUFFIX:STRING=.dylib -> -> // Use new V8 ABI with V8_COMPRESS_POINTERS and V8_31BIT_SMIS_ON_64BIT_ARCH -> V8_COMPRESS_POINTERS:BOOL=ON +### CMake options +| Option | Default | Description | +|--------|---------|-------------| +| `BUILD_SHARED_LIBS` | ON | Build as shared library | +| `BUILD_TESTING` | OFF | Build and run tests | +| `V8PP_HEADER_ONLY` | 0 | Header-only library mode | +| `V8PP_ISOLATE_DATA_SLOT` | 0 | Isolate data slot for v8pp shared data | +| `V8PP_PRETTIFY_TYPENAMES` | 1 | Prettier type names (disable for max performance) | +| `V8_COMPRESS_POINTERS` | ON | V8 compressed pointers ABI | +| `V8_ENABLE_SANDBOX` | OFF | V8 sandboxing | ## Binding example -v8pp supports V8 versions after 6.3 with `v8::Isolate` usage in API. There are 2 targets for binding: +v8pp provides two main binding targets: * `v8pp::module`, a wrapper class around `v8::ObjectTemplate` * `v8pp::class_`, a template class wrapper around `v8::FunctionTemplate` -Both of them require a pointer to `v8::Isolate` instance. They allows to bind from C++ code such items as variables, functions, constants with a function `set(name, item)`: +Both require a pointer to `v8::Isolate`. They allow binding variables, functions, +constants, and properties with a fluent `.set(name, item)` API: ```c++ v8::Isolate* isolate; @@ -82,26 +127,18 @@ struct X // bind free variables and functions v8pp::module mylib(isolate); mylib - // set read-only attribute .const_("PI", 3.1415) - // set variable available in JavaScript with name `var` .var("var", var) - // set function get_var as `fun` .function("fun", &get_var) - // set property `prop` with getter get_var() and setter set_var() .property("prop", get_var, set_var); // bind class v8pp::class_ X_class(isolate); X_class - // specify X constructor signature .ctor() - // bind variable .var("var", &X::var) - // bind function .function("fun", &X::set) - // bind read-only property - .property("prop",&X::get); + .property("prop", &X::get); // set class into the module template mylib.class_("X", X_class); @@ -118,37 +155,122 @@ var x = new mylib.X(1, true); mylib.prop = x.prop + x.fun(); ``` -## Node.js and io.js addons +## Function overloading + +```c++ +v8pp::module m(isolate); +m.function("process", v8pp::overload( + &process_int, + &process_string, + v8pp::with_defaults(&process_opts, v8pp::defaults(42, "default")) +)); +``` + +## Default parameters + +```c++ +v8pp::module m(isolate); +m.function("create", v8pp::with_defaults( + &create_widget, + v8pp::defaults(100, 200, "untitled") // fills from right +)); +``` + +## Symbol protocols and iterators + +```c++ +v8pp::class_ my_class(isolate); +my_class + .ctor() + .to_string_tag("MyClass") // Symbol.toStringTag + .to_primitive(&MyClass::value_of) // Symbol.toPrimitive + .iterable(&MyClass::begin, &MyClass::end); // Symbol.iterator → for...of +``` -The library is suitable to make [Node.js](http://nodejs.org/) and [io.js](https://iojs.org/) addons. See [addons](docs/addons.md) document. +## Promise support ```c++ +v8pp::promise p(isolate); +v8::Local js_promise = p.promise(); // return this to JS + +// later, from C++: +p.resolve(42); +// or: p.reject("something went wrong"); +``` + +## Type conversion table + +| C++ Type | V8 / JS Type | +|----------|-------------| +| `bool`, integral, floating-point | `Boolean`, `Number` | +| `int64_t`, `uint64_t` | `BigInt` | +| `std::string`, `std::string_view`, `char const*` | `String` | +| `enum` / `enum class` | `Number` | +| `std::vector`, `std::list`, `std::deque`, etc. | `Array` | +| `std::set`, `std::unordered_set` | `Array` | +| `std::map`, `std::unordered_map` | `Object` | +| `std::array` | `Array` | +| `std::tuple` | `Array` | +| `std::pair` | `[key, value]` | +| `std::optional` | `T` or `undefined` | +| `std::variant` | First matching type | +| `std::shared_ptr` | Wrapped object | +| `std::vector` | `ArrayBuffer` | +| `std::span` | TypedArray (`Uint8Array`, `Float32Array`, etc.) | +| `std::filesystem::path` | `String` | +| `std::chrono::duration` | `Number` (milliseconds) | +| `std::chrono::time_point` | `Number` (epoch ms) | +| `v8::Local`, `v8::Global` | Pass-through | +| Any `class_`-bound class | Wrapped object | +| `v8pp::promise` | `Promise` | + +## Class binding + +```c++ +v8pp::class_ my_class(isolate); +my_class + .ctor() + .function("method", &MyClass::method) + .property("prop", &MyClass::get_prop, &MyClass::set_prop) + .const_property("id", &MyClass::id) // evaluated once, read-only + .var("field", &MyClass::field) + .const_("MAX", 100); +``` +## Node.js and io.js addons + +The library is suitable to make [Node.js](http://nodejs.org/) addons. See [addons](docs/addons.md) document. + +```c++ void RegisterModule(v8::Local exports) { v8pp::module addon(v8::Isolate::GetCurrent()); - // set bindings... addon .function("fun", &function) .class_("cls", my_class) ; - // set bindings as exports object prototype exports->SetPrototype(addon.new_instance()); } ``` -## v8pp also provides +## Plugins -* `v8pp` - a static library to add several global functions (load/require to the v8 JavaScript context. `require()` is a system for loading plugins from shared libraries. -* `test` - A binary for running JavaScript files in a context which has v8pp module loading functions provided. - -## v8pp module example +v8pp provides a `require()` system for loading plugins from shared libraries: ```c++ -#include +#include + +v8pp::context context; +context.set_lib_path("path/to/plugins/lib"); +v8::HandleScope scope(context.isolate()); +context.run_file("some_file.js"); +``` + +### Plugin example +```c++ #include namespace console { @@ -156,12 +278,11 @@ namespace console { void log(v8::FunctionCallbackInfo const& args) { v8::HandleScope handle_scope(args.GetIsolate()); - for (int i = 0; i < args.Length(); ++i) { if (i > 0) std::cout << ' '; v8::String::Utf8Value str(args[i]); - std::cout << *str; + std::cout << *str; } std::cout << std::endl; } @@ -174,227 +295,45 @@ v8::Local init(v8::Isolate* isolate) } } // namespace console -``` -## Turning a v8pp module into a v8pp plugin - -```c++ V8PP_PLUGIN_INIT(v8::Isolate* isolate) { return console::init(isolate); } ``` -## v8pp class binding example - -```c++ -#include -#include - -#include - -namespace file { - -bool rename(char const* src, char const* dest) -{ - return std::rename(src, dest) == 0; -} - -class file_base -{ -public: - bool is_open() const { return stream_.is_open(); } - bool good() const { return stream_.good(); } - bool eof() const { return stream_.eof(); } - void close() { stream_.close(); } - -protected: - std::fstream stream_; -}; - -class file_writer : public file_base -{ -public: - explicit file_writer(v8::FunctionCallbackInfo const& args) - { - if (args.Length() == 1) - { - v8::String::Utf8Value str(args[0]); - open(*str); - } - } - - bool open(char const* path) - { - stream_.open(path, std::ios_base::out); - return stream_.good(); - } - - void print(v8::FunctionCallbackInfo const& args) - { - v8::HandleScope scope(args.GetIsolate()); - - for (int i = 0; i < args.Length(); ++i) - { - if (i > 0) stream_ << ' '; - v8::String::Utf8Value str(args[i]); - stream_ << *str; - } - } - - void println(v8::FunctionCallbackInfo const& args) - { - print(args); - stream_ << std::endl; - } -}; - -class file_reader : public file_base -{ -public: - explicit file_reader(char const* path) - { - open(path); - } - - bool open(const char* path) - { - stream_.open(path, std::ios_base::in); - return stream_.good(); - } - - v8::Local getline(v8::Isolate* isolate) - { - if ( stream_.good() && ! stream_.eof()) - { - std::string line; - std::getline(stream_, line); - return v8pp::to_v8(isolate, line); - } - else - { - return v8::Undefined(isolate); - } - } -}; - -v8::Local init(v8::Isolate* isolate) -{ - v8::EscapableHandleScope scope(isolate); - - // file_base binding, no .ctor() specified, object creation disallowed in JavaScript - v8pp::class_ file_base_class(isolate); - file_base_class - .function("close", &file_base::close) - .function("good", &file_base::good) - .function("is_open", &file_base::is_open) - .function("eof", &file_base::eof) - ; - - // .ctor<> template arguments declares types of file_writer constructor - // file_writer inherits from file_base_class - v8pp::class_ file_writer_class(isolate); - file_writer_class - .ctor const&>() - .inherit() - .function("open", &file_writer::open) - .function("print", &file_writer::print) - .function("println", &file_writer::println) - ; - - // .ctor<> template arguments declares types of file_reader constructor. - // file_base inherits from file_base_class - v8pp::class_ file_reader_class(isolate); - file_reader_class - .ctor() - .inherit() - .function("open", &file_reader::open) - .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", &rename) - .class_("writer", file_writer_class) - .class_("reader", file_reader_class) - ; - - return scope.Escape(m.new_instance()); -} - -} // namespace file - -V8PP_PLUGIN_INIT(v8::Isolate* isolate) -{ - return file::init(isolate); -} -``` - -## Creating a v8 context capable of using require() function +## External C++ objects ```c++ -#include - -v8pp::context context; -context.set_lib_path("path/to/plugins/lib"); -// script can now use require() function. An application -// that uses v8pp::context must link against v8pp library. -v8::HandleScope scope(context.isolate()); -context.run_file("some_file.js"); -``` - -## Using require() from JavaScript - -```javascript -// Load the file module from the class binding example and the -// console module. -var file = require('file'), - console = require('console') - -var writer = new file.writer("file") -if (writer.is_open()) { - writer.println("some text") - writer.close() - if (! file.rename("file", "newfile")) - console.log("could not rename file") -} -else console.log("could not open `file'") +// Reference external — C++ object outlives JavaScript wrapper +v8::Local val = v8pp::class_::reference_external( + isolate, &my_class::instance()); -console.log("exit") +// Import external — JavaScript takes ownership via pointers / shared_ptr +v8::Local val = v8pp::class_::import_external( + isolate, new my_class); ``` -## Create a handle to an externally referenced C++ class. +## Compile-time configuration -```c++ -// Memory for C++ class will remain when JavaScript object is deleted. -// Useful for classes you only wish to inject. -typedef v8pp::class_ my_class_wrapper; -v8::Local val = my_class_wrapper::reference_external(isolate, &my_class::instance()); -// Assuming my_class::instance() returns reference to class -``` +Defined in `v8pp/config.hpp`: -## Import externally created C++ class into v8pp. + * `V8PP_ISOLATE_DATA_SLOT` — v8::Isolate data slot for v8pp internal data + * `V8PP_PLUGIN_INIT_PROC_NAME` — Plugin initialization procedure name + * `V8PP_PLUGIN_SUFFIX` — Plugin filename suffix for `require()` + * `V8PP_HEADER_ONLY` — Header-only mode (default) + * `V8PP_PRETTIFY_TYPENAMES` — Pretty type names for debugging (disable for performance) -```c++ -// Memory for c++ object will be reclaimed by JavaScript using "delete" when -// JavaScript class is deleted. -typedef v8pp::class_ my_class_wrapper; -v8::Local val = my_class_wrapper::import_external(isolate, new my_class); -``` +## License -## Compile-time configuration +[Boost Software License 1.0](LICENSE.md) -The library uses several preprocessor macros, defined in `v8pp/config.hpp` file: +## Upstream - * `V8PP_ISOLATE_DATA_SLOT` - A v8::Isolate data slot number, used to store v8pp internal data - * `V8PP_PLUGIN_INIT_PROC_NAME` - Plugin initialization procedure name that should be exported from a v8pp plugin. - * `V8PP_PLUGIN_SUFFIX` - Plugin filename suffix that would be added if the plugin name used in `require()` doesn't end with it. - * `V8PP_HEADER_ONLY` - Use header-only implemenation, enabled by default. +[pmed/v8pp](https://github.com/pmed/v8pp) — original project by [pmed](https://github.com/pmed) -## v8pp alternatives +## Alternatives * [nbind](https://github.com/charto/nbind) -* [vu8](https://github.com/tsa/vu8), abandoned -* [v8-juice](http://code.google.com/p/v8-juice/), abandoned -* Script bindng in [cpgf](https://github.com/cpgf/cpgf) +* [vu8](https://github.com/tsa/vu8) +* [v8-juice](http://code.google.com/p/v8-juice/) From b9a48454705188c599d4781021f0b57d813de5a3 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:26:52 +0100 Subject: [PATCH 16/38] improve CMake modernization and cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix typo "sandoxing" → "sandboxing" in V8_ENABLE_SANDBOX option description - remove commented-out CMAKE_CXX_RTTI and source_group dead code in v8pp/CMakeLists.txt - remove obsolete /experimental:external MSVC flag (unnecessary on MSVC 2022+) - add explicit PRIVATE visibility to target_link_libraries() in test and plugin targets - use modern add_test(NAME ... COMMAND ...) syntax with target names instead of hardcoded paths - use idiomatic option() for boolean cache variables instead of set(... CACHE BOOL) - add find_dependency(V8) to Config.cmake.in so find_package(v8pp) consumers auto-resolve V8 --- CMakeLists.txt | 8 ++++---- cmake/Config.cmake.in | 13 ++++++++----- plugins/CMakeLists.txt | 22 +++++++++++----------- test/CMakeLists.txt | 6 +++--- v8pp/CMakeLists.txt | 6 +----- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4109682..1fdf7d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,13 +20,13 @@ option(BUILD_TESTING "Build and run tests" OFF) option(BUILD_DOCUMENTATION "Build documentation" OFF) # Custom project options -set(V8PP_HEADER_ONLY 0 CACHE BOOL "Header-only library") +option(V8PP_HEADER_ONLY "Header-only library" OFF) set(V8PP_ISOLATE_DATA_SLOT 0 CACHE STRING "v8::Isolate data slot number, used in v8pp for shared data") set(V8PP_PLUGIN_INIT_PROC_NAME "v8pp_module_init" CACHE STRING "v8pp plugin initialization procedure name") set(V8PP_PLUGIN_SUFFIX ${CMAKE_SHARED_MODULE_SUFFIX} CACHE STRING "v8pp plugin filename suffix") -set(V8PP_PRETTIFY_TYPENAMES 1 CACHE BOOL "v8pp registered classes will have prettier typeid names") -set(V8_COMPRESS_POINTERS 1 CACHE BOOL "Use new V8 ABI with V8_COMPRESS_POINTERS and V8_31BIT_SMIS_ON_64BIT_ARCH") -set(V8_ENABLE_SANDBOX 0 CACHE BOOL "Enable sandoxing in V8") +option(V8PP_PRETTIFY_TYPENAMES "v8pp registered classes will have prettier typeid names" ON) +option(V8_COMPRESS_POINTERS "Use new V8 ABI with V8_COMPRESS_POINTERS and V8_31BIT_SMIS_ON_64BIT_ARCH" ON) +option(V8_ENABLE_SANDBOX "Enable sandboxing in V8" OFF) if(BUILD_SHARED_LIBS AND WIN32) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS true) diff --git a/cmake/Config.cmake.in b/cmake/Config.cmake.in index b2a5c1b..50fa6a8 100644 --- a/cmake/Config.cmake.in +++ b/cmake/Config.cmake.in @@ -1,6 +1,9 @@ -if(NOT TARGET v8pp::v8pp) - # Provide path for scripts - list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") - - include("${CMAKE_CURRENT_LIST_DIR}/@targets_export_name@.cmake") +if(NOT TARGET v8pp::v8pp) + # Provide path for scripts + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}") + + include(CMakeFindDependencyMacro) + find_dependency(V8) + + include("${CMAKE_CURRENT_LIST_DIR}/@targets_export_name@.cmake") endif() \ No newline at end of file diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 3a79757..c3817bb 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -1,11 +1,11 @@ -# sample plugin targets - -if(BUILD_SHARED_LIBS) - add_library(console MODULE console.cpp) - set_target_properties(console PROPERTIES PREFIX "") - target_link_libraries(console v8pp) - - add_library(file MODULE file.cpp) - set_target_properties(file PROPERTIES PREFIX "") - target_link_libraries(file v8pp) -endif() +# sample plugin targets + +if(BUILD_SHARED_LIBS) + add_library(console MODULE console.cpp) + set_target_properties(console PROPERTIES PREFIX "") + target_link_libraries(console PRIVATE v8pp) + + add_library(file MODULE file.cpp) + set_target_properties(file PROPERTIES PREFIX "") + target_link_libraries(file PRIVATE v8pp) +endif() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ec0aa41..0ea55aa 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,11 +35,11 @@ if(V8PP_HEADER_ONLY) endif() endif() -target_link_libraries(v8pp_test v8pp ${CMAKE_DL_LIBS}) +target_link_libraries(v8pp_test PRIVATE v8pp ${CMAKE_DL_LIBS}) -add_test(v8pp_test ${PROJECT_BINARY_DIR}/v8pp_test --version --run-tests) +add_test(NAME v8pp_test COMMAND v8pp_test --version --run-tests) if(BUILD_SHARED_LIBS) file(GLOB JS_TESTS *.js) - add_test(v8pp_js_test ${PROJECT_BINARY_DIR}/v8pp_test --lib-path ${PROJECT_BINARY_DIR}/plugins ${JS_TESTS}) + add_test(NAME v8pp_js_test COMMAND v8pp_test --lib-path ${PROJECT_BINARY_DIR}/plugins ${JS_TESTS}) endif() diff --git a/v8pp/CMakeLists.txt b/v8pp/CMakeLists.txt index 93cb36d..187f3e5 100644 --- a/v8pp/CMakeLists.txt +++ b/v8pp/CMakeLists.txt @@ -62,13 +62,12 @@ else() ) endif() -#set(CMAKE_CXX_RTTI OFF) if(MSVC) set(V8PP_COMPILE_OPTIONS /GR- /EHsc /permissive- /W4) # disable specific warnings list(APPEND V8PP_COMPILE_OPTIONS /wd4190) # set warning level 3 for system headers - list(APPEND V8PP_COMPILE_OPTIONS /experimental:external /external:anglebrackets /external:W3) + list(APPEND V8PP_COMPILE_OPTIONS /external:anglebrackets /external:W3) else() set(V8PP_COMPILE_OPTIONS -frtti -fexceptions -Wall -Wextra -Wpedantic) endif() @@ -96,9 +95,6 @@ else() endif() endif() - -#source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${V8PP_HEADERS} ${V8PP_SOURCES}) - # Install include(CMakePackageConfigHelpers) From 4dd9ce08c9d3a1abd6c339555f5533c43c8297c7 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:27:07 +0100 Subject: [PATCH 17/38] add CLAUDE.md project guide --- .gitignore | 3 +- CLAUDE.md | 296 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 2765d1b..e7b218e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ out/ /include-what-you-use /build*/ .vscode/ -CMakeSettings.json \ No newline at end of file +CMakeSettings.json +.claude/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..abfe6da --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,296 @@ +# CLAUDE.md — v8pp Project Guide + +## Project Overview + +v8pp (v2.1.1) is a C++20 library that binds C++ functions and classes into the V8 JavaScript engine. +It provides a fluent, type-safe API for exposing C++ types to JavaScript with automatic type conversion. + +- **Upstream:** https://github.com/pmed/v8pp +- **Fork:** https://github.com/MangelSpec/v8pp +- **License:** Boost Software License 1.0 +- **Minimum V8 version:** 9.0+ (with compatibility gates up to V8 13.3+) + +## Repository Structure + +``` +v8pp/ # Main library source + *.hpp # Public headers (class, module, context, convert, property, etc.) + *.ipp # Implementation files included in header-only mode + *.cpp # Compiled sources for non-header-only mode + config.hpp.in # CMake-generated config header +cmake/ # CMake modules (FindV8.cmake, Config.cmake.in) +test/ # Test suite (custom framework, test_*.cpp pattern) +plugins/ # Sample plugin modules (console.cpp, file.cpp) +examples/ # 8 numbered example projects ("01 hello world" through "08 passing wrapped objects") +docs/ # Documentation +.github/workflows/ # CI (GitHub Actions: Ubuntu, macOS, Windows matrix) +``` + +## Build System + +### Requirements +- CMake 3.12+ +- C++20 compiler (MSVC 2022, GCC, Clang) +- V8 JavaScript engine + +### Key CMake Options +| Option | Default | Description | +|--------|---------|-------------| +| `BUILD_SHARED_LIBS` | ON | Build as shared library | +| `BUILD_TESTING` | OFF | Build and run tests | +| `V8PP_HEADER_ONLY` | OFF | Header-only library mode | +| `V8PP_ISOLATE_DATA_SLOT` | 0 | Isolate data slot for v8pp shared data | +| `V8PP_PRETTIFY_TYPENAMES` | ON | Prettier typeid names for registered classes | +| `V8_COMPRESS_POINTERS` | ON | V8 compressed pointers ABI | +| `V8_ENABLE_SANDBOX` | OFF | V8 sandboxing | + +### Building (Windows with Ninja) +```bash +cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON +cmake --build build --config Release +``` + +### Running Tests +```bash +cd build +ctest -C Release -V +``` +The test executable is `v8pp_test`. Run with `--version --run-tests` for the native tests. +When `BUILD_SHARED_LIBS=ON`, JavaScript tests (`test/*.js`) also run via `--lib-path`. + +### V8 Installation by Platform +- **Windows:** NuGet (`nuget install v8-v143-x64`) or vcpkg +- **macOS:** Homebrew (`brew install v8`) +- **Linux:** apt (`sudo apt install libv8-dev`) + +## Architecture & Key Abstractions + +### Core Types (all in namespace `v8pp`) +- **`class_`** — Binds a C++ class to V8: constructors, methods, properties, inheritance, symbol protocols, iterators +- **`module`** — Wraps `v8::ObjectTemplate`, binds functions/variables/classes into a JS module +- **`context`** — Manages `v8::Isolate` + `v8::Context`, provides `run_script()`, `require()` +- **`context_store`** — Cross-context key-value store backed by a dedicated V8 context; persists values across ephemeral contexts on the same isolate +- **`convert`** — Template specializations for V8 ↔ C++ type conversion (30+ types) +- **`property`** — Compile-time getter/setter property binding +- **`promise`** — Synchronous wrapper around `v8::Promise::Resolver` with typed resolve/reject + +### Internal Types (in `v8pp::detail`) +- **`object_registry`** — Tracks wrapped C++ object lifecycles in V8 (magic number validation: `0xC1A5517F`) +- **`external_data`** — Stores C++ data in V8 Externals (bitcast optimization for small types) +- **`type_info`** — Lightweight compile-time RTTI using static variable addresses with integer-based IDs +- **`call_from_v8_traits`** — Parameter extraction with default parameter support +- **`overload_resolution`** — Runtime argument count + type dispatch for function overloading (first-match-wins) +- **`fast_callback`** — V8 10+ Fast API callback generation via NTTP + +### Pointer Traits +- `raw_ptr_traits` — Raw pointer lifecycle (`new`/`delete`) +- `shared_ptr_traits` — `std::shared_ptr` lifecycle + +### Design Patterns +- **Fluent API / Method chaining** — `.ctor<>().function().property().var()` +- **C++20 Concepts** — Compile-time dispatch for type traits (`mapping`, `sequence`, `set_like`, `callable`, etc.) +- **Template specialization** — Type conversion system +- **Header-only option** — `.ipp` files are included by headers when `V8PP_HEADER_ONLY` is defined + +## Code Conventions + +### Naming +- **Classes/types:** `snake_case` with trailing underscore for keyword conflicts (`class_`) +- **Functions:** `snake_case` (`to_v8`, `from_v8`, `throw_ex`, `set_option`) +- **Member variables:** trailing underscore (`isolate_`, `obj_`, `impl_`) +- **Macros/constants:** `V8PP_` prefix, `UPPER_CASE` +- **Type aliases:** `snake_case` (`object_id`, `pointer_type`) +- **Namespaces:** `v8pp`, `v8pp::detail` + +### Formatting (enforced by .clang-format) +- **Indent:** 4 spaces (tabs for continuation) +- **Braces:** Allman-like (open brace on new line for classes, functions, control statements) +- **Namespaces:** compact, no indentation, no brace on new line +- **Column limit:** none (0) +- **Pointer alignment:** left (`int* p`, not `int *p`) +- **Access modifiers:** flush with class keyword (offset -4) +- **Template declarations:** always break before +- **Short forms allowed:** short if-without-else on single line, inline-only short functions + +### Coding Practices +- **Cache repeated lookups:** Extract `isolate()`, `GetCurrentContext()`, and similar + accessor results into local variables when used more than once in a function. + Avoids redundant calls and improves readability. +- **V8 MaybeLocal/Maybe handling:** Use `ToLocalChecked()`/`FromJust()` for internal + operations that should never fail (crash is appropriate). Use `ToLocal()`/`FromMaybe()` + only where failure is genuinely reachable from user script (e.g. `ToString()` via Proxy, + `GetPropertyNames()` via Proxy traps) and there's a meaningful recovery path. +- **`try_from_v8`:** Exception-free conversion returning `std::optional`. Use + instead of `is_valid()` + `from_v8()` two-step pattern for cleaner type dispatch. + +### Headers +- Use `#pragma once` (no include guards) +- Include order: standard library → V8 headers → v8pp headers + +### Modern C++ Features in Use +- C++20 concepts (`mapping`, `sequence`, `set_like`, `callable`, `typed_array_element`, etc.) +- `requires` clauses on convert specializations (replacing SFINAE `enable_if`) +- `std::optional` for optional function parameters and `try_from_v8` results +- `std::string_view` for string parameters +- Fold expressions, structured bindings +- NTTP (non-type template parameters) for Fast API callbacks + +## Type Conversion System + +The `convert` system supports these C++ types: + +| Category | Types | V8 Representation | +|----------|-------|-------------------| +| Numeric | `bool`, `char`, integral, floating-point | `Boolean`, `Number` | +| Large integers | `int64_t`, `uint64_t` (>32-bit) | `BigInt` | +| Strings | `std::string`, `std::string_view`, `char const*`, wide strings | `String` | +| Enums | any `enum` / `enum class` | `Number` (underlying type) | +| Containers | `std::vector`, `std::list`, `std::deque`, etc. | `Array` | +| Sets | `std::set`, `std::unordered_set` | `Array` | +| Mappings | `std::map`, `std::unordered_map` | `Object` | +| Arrays | `std::array` | `Array` (fixed-length) | +| Tuples | `std::tuple` | `Array` | +| Pairs | `std::pair` | `[key, value]` Array | +| Optional | `std::optional` | `T` or `undefined` | +| Variant | `std::variant` | First matching type | +| Smart ptrs | `std::shared_ptr` | Wrapped object | +| Binary | `std::vector` | `ArrayBuffer` | +| TypedArrays | `std::span` (to_v8 only) | `Uint8Array`, `Float32Array`, etc. | +| Filesystem | `std::filesystem::path` | `String` | +| Chrono | `std::chrono::duration`, `time_point` | `Number` (milliseconds / epoch ms) | +| V8 handles | `v8::Local`, `v8::Global` | Pass-through | +| Wrapped classes | Any class bound via `class_` | Wrapped object | +| Promises | `v8pp::promise` | `Promise` | + +## Binding Features + +### Class Binding (`class_`) +```cpp +v8pp::class_ my_class(isolate); +my_class + .ctor() + .function("method", &MyClass::method) + .property("prop", &MyClass::get_prop, &MyClass::set_prop) + .var("field", &MyClass::field) + .to_string_tag("MyClass") // Symbol.toStringTag + .to_primitive(&MyClass::value_of) // Symbol.toPrimitive + .iterable(&MyClass::begin, &MyClass::end); // Symbol.iterator +``` + +### Function Overloading +```cpp +module.function("process", v8pp::overload( + &process_int, + &process_string, + v8pp::with_defaults(&process_opts, v8pp::defaults(42, "default")) +)); +``` + +### Default Parameters +```cpp +module.function("create", v8pp::with_defaults( + &create_widget, + v8pp::defaults(100, 200, "untitled") // fills from right +)); +``` + +### V8 Fast API Callbacks (V8 10+) +Automatically generated for functions with supported signatures (void, bool, int32_t, +uint32_t, float, double params/returns). Registered as dual slow+fast callbacks. + +## V8 API Compatibility + +The codebase tracks V8's evolving API with version guards: + +```cpp +// V8 13.3+: New string conversion API with ExternalMemoryAccounter +#if V8_MAJOR_VERSION > 13 || (V8_MAJOR_VERSION == 13 && V8_MINOR_VERSION >= 3) + +// V8 12.9+: SetAccessor removed from FunctionTemplate, use SetNativeDataProperty +// V8 12.2+: CompileModule API changes +// V8 11.9+: Exception constructor takes options struct +// V8 10.5+: VisitHandlesWithClassIds removed +// V8 10.0+: Fast API callbacks available +``` + +When modifying V8 API calls, always check which version introduced/removed the API and add +appropriate `#if` version guards. Test against the CI matrix (Ubuntu/macOS/Windows with +varying V8 versions). + +## Safety Features + +- **Prototype chain depth limit** (16) in `unwrap_object` prevents infinite loops from circular prototypes +- **Magic number validation** (`0xC1A5517F`) on `object_registry` before `static_cast` catches corruption +- **Use-after-free protection** in `context::require()` callback via weak pointer pattern +- **Null checks** on `unwrap_object` results in property/member accessors +- **`try_from_v8`** for exception-free conversion returning `std::optional` +- **`ToLocal()`/`FromMaybe()`** on script-reachable paths (Proxy traps, ToString, GetPropertyNames) + +## Testing + +### Framework +Custom lightweight framework in `test/test.hpp` (no external dependencies). + +### Key Test Helpers +- `check(msg, condition)` — Assert, throws on failure +- `check_eq(msg, obtained, expected)` — Equality check with pretty-print +- `run_script(context, code)` — Execute JS and convert result to C++ type + +### Test Files (22 files) +Each test file covers one module: `test_class.cpp`, `test_convert.cpp`, `test_property.cpp`, +`test_overload.cpp`, `test_promise.cpp`, `test_context_store.cpp`, `test_fast_api.cpp`, +`test_symbol.cpp`, `test_adversarial.cpp`, `test_gc_stress.cpp`, `test_thread_safety.cpp`, etc. +Tests are registered in `test/main.cpp`. The full suite runs as a single executable. + +## Compiler Flags + +### MSVC +`/GR-` (no RTTI), `/EHsc` (structured exceptions), `/permissive-` (strict conformance), +`/W4` (high warnings), `/wd4190` (suppress C-linkage warning), `/Zc:__cplusplus`, +`/external:anglebrackets /external:W3` (reduced warnings for system headers) + +### GCC/Clang +`-frtti`, `-fexceptions`, `-Wall -Wextra -Wpedantic` + +Note: macOS may need `-fno-rtti` for `context.cpp` specifically. + +## Git Conventions + +- **Commit style:** Short subject line, no trailing period, lowercase start +- **Prefixes used:** `fix`, `add`, `use`, `improve`, or bare description +- **Examples:** + - `fix const reference for _fast setters` + - `add get_option/set_option/set_option_data fast aliases` + - `Fix SetAccessor API for V8 12.9+` + +## Common Tasks + +### Adding a new type conversion +1. Add a `convert` specialization in `v8pp/convert.hpp` +2. Implement `to_v8()` and `from_v8()` static methods +3. Add tests in `test/test_convert.cpp` + +### Binding a new C++ class +Use `v8pp::class_` with the fluent API: +```cpp +v8pp::class_ my_class(isolate); +my_class + .ctor() + .function("method", &MyClass::method) + .property("prop", &MyClass::get_prop, &MyClass::set_prop) + .var("field", &MyClass::field); +``` + +### Fixing V8 API breakage +1. Identify the V8 version that changed the API +2. Add `#if V8_MAJOR_VERSION > X || (V8_MAJOR_VERSION == X && V8_MINOR_VERSION >= Y)` guards +3. Implement both old and new code paths +4. Test across the CI matrix + +## CI Matrix + +GitHub Actions runs on push/PR with this matrix: +- **OS:** Ubuntu, macOS, Windows +- **Build types:** Release +- **Variants:** shared/static × header-only/compiled +- **V8 options:** compressed pointers and sandbox vary by platform From a67f55d6270d51e79bb7952e0f1546ff439d291d Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:26:44 +0100 Subject: [PATCH 18/38] fix V8 12.9+ API compatibility for FastApiCallbackOptions and SetNativeDataProperty - replace options.fallback with options.isolate->ThrowError() in fast_api.hpp (fallback member removed in V8 12.9+) - remove v8::DEFAULT (AccessControl) from SetNativeDataProperty calls in class.hpp V8 12.9+ paths - add V8 12.9+ version guards to SetNativeDataProperty calls in module.hpp --- v8pp/class.hpp | 4 ++-- v8pp/fast_api.hpp | 8 ++++++++ v8pp/module.hpp | 13 +++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/v8pp/class.hpp b/v8pp/class.hpp index 15b7fa5..3ede4da 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -586,7 +586,7 @@ class class_ class_info_.js_function_template() ->InstanceTemplate() ->SetNativeDataProperty(v8_name, getter, setter, data, - v8::PropertyAttribute(v8::DontDelete), v8::DEFAULT, + v8::PropertyAttribute(v8::DontDelete), v8::SideEffectType::kHasNoSideEffect, v8::SideEffectType::kHasSideEffectToReceiver); #else @@ -636,7 +636,7 @@ 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::DEFAULT, + v8::PropertyAttribute(v8::DontDelete), v8::SideEffectType::kHasNoSideEffect, setter_effect); #else class_info_.js_function_template()->PrototypeTemplate()->SetAccessor(v8_name, getter, setter, data, diff --git a/v8pp/fast_api.hpp b/v8pp/fast_api.hpp index f250e96..a6433d1 100644 --- a/v8pp/fast_api.hpp +++ b/v8pp/fast_api.hpp @@ -79,7 +79,11 @@ struct fast_callback 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{}; } @@ -97,7 +101,11 @@ struct fast_callback 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{}; } diff --git a/v8pp/module.hpp b/v8pp/module.hpp index 45c92b8..f88e8e6 100644 --- a/v8pp/module.hpp +++ b/v8pp/module.hpp @@ -117,10 +117,17 @@ class module v8::AccessorNameGetterCallback getter = &var_get; v8::AccessorNameSetterCallback setter = &var_set; v8::Local data = detail::external_data::set(isolate_, &var); +#if V8_MAJOR_VERSION > 12 || (V8_MAJOR_VERSION == 12 && V8_MINOR_VERSION >= 9) + obj_->SetNativeDataProperty(v8_name, getter, setter, data, + v8::PropertyAttribute::DontDelete, + v8::SideEffectType::kHasNoSideEffect, + v8::SideEffectType::kHasSideEffectToReceiver); +#else obj_->SetNativeDataProperty(v8_name, getter, setter, data, v8::PropertyAttribute::DontDelete, v8::DEFAULT, v8::SideEffectType::kHasNoSideEffect, v8::SideEffectType::kHasSideEffectToReceiver); +#endif return *this; } @@ -148,9 +155,15 @@ class module 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, + v8::SideEffectType::kHasNoSideEffect, setter_effect); +#else obj_->SetNativeDataProperty(v8_name, getter, setter, data, v8::PropertyAttribute::DontDelete, v8::DEFAULT, v8::SideEffectType::kHasNoSideEffect, setter_effect); +#endif return *this; } From 6c7993b32d68b3ee3ba4f95dd5606389b492e539 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 18:42:13 +0100 Subject: [PATCH 19/38] convert int64/uint64 to Number instead of BigInt for JS arithmetic compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change convert for 64-bit integers to produce v8::Number instead of v8::BigInt - keep from_v8 accepting both Number and BigInt for interop - add safe-integer check (±2^53) in variant try_as for 64-bit integrals from Number - reorder variant IsNumber() dispatch to try is_large_integral before is_floating_point - add is_large_integral fallback to IsInt32()/IsUint32() variant branch - update tests to expect Number output and use double-safe range values - remove int64_t+uint64_t from mixed variant order test, test them individually - update CLAUDE.md type conversion table --- CLAUDE.md | 2 +- test/test_convert.cpp | 62 ++++++++++++++++++++++++------------------- v8pp/convert.hpp | 48 ++++++++++++++++++++++----------- 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index abfe6da..6b61203 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -142,7 +142,7 @@ The `convert` system supports these C++ types: | Category | Types | V8 Representation | |----------|-------|-------------------| | Numeric | `bool`, `char`, integral, floating-point | `Boolean`, `Number` | -| Large integers | `int64_t`, `uint64_t` (>32-bit) | `BigInt` | +| Large integers | `int64_t`, `uint64_t` (>32-bit) | `Number` (double; accepts `BigInt` on input) | | Strings | `std::string`, `std::string_view`, `char const*`, wide strings | `String` | | Enums | any `enum` / `enum class` | `Number` (underlying type) | | Containers | `std::vector`, `std::list`, `std::deque`, etc. | `Array` | diff --git a/test/test_convert.cpp b/test/test_convert.cpp index bbb7099..bae8c6d 100644 --- a/test/test_convert.cpp +++ b/test/test_convert.cpp @@ -315,14 +315,23 @@ void check_range(v8::Isolate* isolate) variant_check check_range{ isolate }; T zero{ 0 }; - T min = std::numeric_limits::lowest(); - T max = std::numeric_limits::max(); - check_range(zero); - check_range(min); - check_range(max); - if constexpr (sizeof(T) <= sizeof(uint32_t)) + + if constexpr (sizeof(T) > sizeof(uint32_t)) + { + // 64-bit types convert through double, so test values within double precision + check_range(static_cast(V8_MAX_INT)); + if constexpr (std::is_signed_v) + { + check_range(static_cast(V8_MIN_INT)); + } + } + else { + T min = std::numeric_limits::lowest(); + T max = std::numeric_limits::max(); + check_range(min); + check_range(max); // For <=32-bit types, test out-of-range doubles check_range.check_ex(std::nextafter(double(min), std::numeric_limits::lowest())); check_range.check_ex(std::nextafter(double(max), std::numeric_limits::max())); @@ -406,15 +415,19 @@ void test_convert_variant(v8::Isolate* isolate) 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 - variant_check order_check{ isolate }; + // The order here matters — int64_t/uint64_t not included because both map + // to Number, so positive values can't distinguish them in a mixed variant + variant_check order_check{ isolate }; order_check( std::numeric_limits::min(), std::numeric_limits::max(), std::numeric_limits::min(), std::numeric_limits::max(), std::numeric_limits::min(), std::numeric_limits::max(), - std::numeric_limits::min(), std::numeric_limits::max(), std::numeric_limits::lowest(), std::numeric_limits::max()); + // int64_t in variant: negative value resolves unambiguously + variant_check int64_check{ isolate }; + int64_check(static_cast(V8_MIN_INT), std::numeric_limits::max()); + variant_check simple_arithmetic{ isolate }; simple_arithmetic.check_ex(std::numeric_limits::max()); // does not fit into int8_t simple_arithmetic.check_ex(1.5); // is not integral @@ -425,7 +438,7 @@ void test_convert_variant(v8::Isolate* isolate) objects_only.check_ex(std::string{ "test" }); objects_only.check_ex(1.); - // BigInt conversion covers full int64_t/uint64_t range + // Number conversion covers int64_t/uint64_t (with double precision limits) check_ranges(isolate); // test map @@ -684,38 +697,31 @@ void test_convert_try_from_v8(v8::Isolate* isolate) void test_convert_bigint(v8::Isolate* isolate) { - // Basic round-trip for int64_t + // 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, std::numeric_limits::min()); - test_conv(isolate, std::numeric_limits::max()); - // Basic round-trip for uint64_t + // 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, std::numeric_limits::max()); - - // Values beyond double precision round-trip exactly through BigInt - int64_t big_signed = int64_t{1} << 60; - test_conv(isolate, big_signed); - test_conv(isolate, -big_signed); - uint64_t big_unsigned = uint64_t{1} << 63; - test_conv(isolate, big_unsigned); - - // to_v8 produces BigInt + // to_v8 produces Number (not BigInt) auto v8_val = v8pp::to_v8(isolate, int64_t{123}); - check("int64_t to_v8 is BigInt", v8_val->IsBigInt()); + check("int64_t to_v8 is Number", v8_val->IsNumber()); auto v8_uval = v8pp::to_v8(isolate, uint64_t{456}); - check("uint64_t to_v8 is BigInt", v8_uval->IsBigInt()); + check("uint64_t to_v8 is Number", v8_uval->IsNumber()); - // from_v8 accepts Number for ergonomics + // 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}); + // 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}); + // from_v8 rejects non-numeric types check_ex("int64_t from string", [isolate]() { @@ -726,7 +732,7 @@ void test_convert_bigint(v8::Isolate* isolate) v8pp::from_v8(isolate, v8pp::to_v8(isolate, true)); }); - // try_from_v8 for BigInt + // try_from_v8 for int64_t 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}); diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index 3d06876..f48ea2f 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -318,24 +319,26 @@ struct convert } }; -// convert BigInt <-> integer types larger than 32 bits (int64_t, uint64_t, etc.) -// to_v8 always produces BigInt. from_v8 accepts both BigInt and Number for ergonomics. +// convert Number <-> integer types larger than 32 bits (int64_t, uint64_t, etc.) +// to_v8 produces Number (double) for seamless JS arithmetic. Precision loss for +// 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)) struct convert { using from_type = T; - using to_type = v8::Local; + using to_type = v8::Local; static bool is_valid(v8::Isolate*, v8::Local value) { - return !value.IsEmpty() && (value->IsBigInt() || value->IsNumber()); + return !value.IsEmpty() && (value->IsNumber() || value->IsBigInt()); } static from_type from_v8(v8::Isolate* isolate, v8::Local value) { if (auto result = try_from_v8(isolate, value)) return *result; - throw invalid_argument(isolate, value, "BigInt"); + throw invalid_argument(isolate, value, "Number"); } static std::optional try_from_v8(v8::Isolate* isolate, v8::Local value) @@ -344,6 +347,7 @@ struct convert if (value->IsBigInt()) { + // Accept BigInt from JS side for interop auto bigint = value.As(); if constexpr (std::is_signed_v) { @@ -356,21 +360,13 @@ struct convert } else { - // Accept Number for ergonomics (lossy for values > 2^53) return static_cast(value->IntegerValue(isolate->GetCurrentContext()).FromJust()); } } static to_type to_v8(v8::Isolate* isolate, T value) { - if constexpr (std::is_signed_v) - { - return v8::BigInt::New(isolate, static_cast(value)); - } - else - { - return v8::BigInt::NewFromUnsigned(isolate, static_cast(value)); - } + return v8::Number::New(isolate, static_cast(value)); } }; @@ -589,11 +585,11 @@ struct convert> } else if (value->IsInt32() || value->IsUint32()) { - return alternate(isolate, value); + return alternate(isolate, value); } else if (value->IsNumber()) { - return alternate(isolate, value); + return alternate(isolate, value); } else if (value->IsString()) { @@ -742,6 +738,26 @@ struct convert> } } } + else if constexpr (sizeof(T) > sizeof(uint32_t)) + { + // 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); + if (std::isfinite(d) && d == std::trunc(d)) + { + if constexpr (std::is_signed_v) + { + if (d >= -safe_max && d <= safe_max) + result = static_cast(static_cast(d)); + } + else + { + if (d >= 0.0 && d <= safe_max) + result = static_cast(static_cast(d)); + } + } + } else { get_number(isolate, value, result); From 3bbf52841053addb00b8ff312d620bca58eb0589 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:09:29 +0100 Subject: [PATCH 20/38] fix CI build failures across Ubuntu, Windows, and macOS - guard v8-fast-api-calls.h include with __has_include for distros that ship V8 10+ without the header (Ubuntu libv8-dev), define V8PP_HAS_FAST_API_HEADER macro used in fast_api.hpp and function.hpp - replace exact JSON.stringify(Y) string comparison in test_class with order-independent property validation for V8 13.0+ compatibility - add missing template keyword on .inherit() in test_gc_stress.cpp for strict AppleClang 17 - suppress unused parameter warnings for receiver/options in free-function fast_callback - remove unused GetClass and property_type aliases in class_::const_property --- test/test_class.cpp | 26 ++++++++++++++++++++++++-- test/test_gc_stress.cpp | 2 +- v8pp/class.hpp | 4 ---- v8pp/fast_api.hpp | 13 ++++++++----- v8pp/function.hpp | 2 +- 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/test/test_class.cpp b/test/test_class.cpp index 5bebc27..e273514 100644 --- a/test/test_class.cpp +++ b/test/test_class.cpp @@ -314,9 +314,31 @@ void test_class_() 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 check_eq("JSON.stringify(Y)", - run_script(context, "JSON.stringify({'obj': new Y(10), 'arr': [new Y(11), new Y(12)] })"), - R"({"obj":{"useX":"function useX() { [native code] }","useX_ptr":"function useX_ptr() { [native code] }","toJSON":"function toJSON() { [native code] }","wprop_external3":10,"wprop_external2":10,"wprop_external1":10,"rprop_external3":10,"rprop_external2":10,"rprop_external1":10,"rprop_direct":10,"prop2":10,"prop":10,"wprop2":10,"wprop":10,"rprop":10,"var":10,"konst":99,"fun1":"function fun1() { [native code] }","fun2":"function fun2() { [native code] }","fun3":"function fun3() { [native code] }","fun4":"function fun4() { [native code] }","static_fun":"function static_fun() { [native code] }","static_lambda":"function static_lambda() { [native code] }","extern_fun":"function extern_fun() { [native code] }"},"arr":[{"useX":"function useX() { [native code] }","useX_ptr":"function useX_ptr() { [native code] }","toJSON":"function toJSON() { [native code] }","wprop_external3":11,"wprop_external2":11,"wprop_external1":11,"rprop_external3":11,"rprop_external2":11,"rprop_external1":11,"rprop_direct":11,"prop2":11,"prop":11,"wprop2":11,"wprop":11,"rprop":11,"var":11,"konst":99,"fun1":"function fun1() { [native code] }","fun2":"function fun2() { [native code] }","fun3":"function fun3() { [native code] }","fun4":"function fun4() { [native code] }","static_fun":"function static_fun() { [native code] }","static_lambda":"function static_lambda() { [native code] }","extern_fun":"function extern_fun() { [native code] }"},{"useX":"function useX() { [native code] }","useX_ptr":"function useX_ptr() { [native code] }","toJSON":"function toJSON() { [native code] }","wprop_external3":12,"wprop_external2":12,"wprop_external1":12,"rprop_external3":12,"rprop_external2":12,"rprop_external1":12,"rprop_direct":12,"prop2":12,"prop":12,"wprop2":12,"wprop":12,"rprop":12,"var":12,"konst":99,"fun1":"function fun1() { [native code] }","fun2":"function fun2() { [native code] }","fun3":"function fun3() { [native code] }","fun4":"function fun4() { [native code] }","static_fun":"function static_fun() { [native code] }","static_lambda":"function static_lambda() { [native code] }","extern_fun":"function extern_fun() { [native code] }"}]})" + run_script(context, R"( + (function() { + var s = JSON.stringify({'obj': new Y(10), 'arr': [new Y(11), new Y(12)]}); + var r = JSON.parse(s); + var props = ['rprop','wprop','wprop2','prop','prop2','rprop_direct', + 'rprop_external1','rprop_external2','rprop_external3', + 'wprop_external1','wprop_external2','wprop_external3']; + var fns = ['fun1','fun2','fun3','fun4','static_fun','static_lambda', + 'extern_fun','useX','useX_ptr','toJSON']; + function check(o, v) { + if (o['var'] !== v || o.konst !== 99) return false; + for (var i = 0; i < props.length; i++) + if (o[props[i]] !== v) return false; + for (var i = 0; i < fns.length; i++) + if (typeof o[fns[i]] !== 'string' || o[fns[i]].indexOf('native code') < 0) return false; + return Object.keys(o).length === (2 + props.length + fns.length); + } + return check(r.obj, 10) && check(r.arr[0], 11) && check(r.arr[1], 12) + && r.arr.length === 2; + })() + )"), + true ); check_eq("Y object", run_script(context, "y = new Y(-100); y.konst + y.var"), -1); diff --git a/test/test_gc_stress.cpp b/test/test_gc_stress.cpp index 4c1543b..5c165b5 100644 --- a/test/test_gc_stress.cpp +++ b/test/test_gc_stress.cpp @@ -186,7 +186,7 @@ void test_gc_stress_inheritance() v8pp::class_ derived_class(isolate); derived_class .template ctor() - .inherit() + .template inherit() .function("get_y", &GCDerived::get_y); context.class_("GCBase", base_class); diff --git a/v8pp/class.hpp b/v8pp/class.hpp index 3ede4da..5285d4d 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -657,10 +657,6 @@ class class_ static_assert(std::is_member_function_pointer::value || detail::is_callable::value, "GetFunction must be callable"); - using GetClass = std::conditional_t, T, detail::none>; - - using property_type = v8pp::property; - v8::HandleScope scope(isolate()); // Store the native function for the constant property in object_registry diff --git a/v8pp/fast_api.hpp b/v8pp/fast_api.hpp index a6433d1..b9ba09d 100644 --- a/v8pp/fast_api.hpp +++ b/v8pp/fast_api.hpp @@ -4,8 +4,11 @@ #include -#if V8_MAJOR_VERSION >= 10 +// 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 { @@ -51,7 +54,7 @@ template struct is_fast_api_compatible : std::bool_constant && (is_fast_arg_type_v && ...)> {}; -#if V8_MAJOR_VERSION >= 10 +#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. @@ -62,8 +65,8 @@ struct fast_callback; template struct fast_callback { - static R call(v8::Local receiver, Args... args, - v8::FastApiCallbackOptions& options) + static R call(v8::Local /*receiver*/, Args... args, + v8::FastApiCallbackOptions& /*options*/) { return FuncPtr(args...); } @@ -113,7 +116,7 @@ struct fast_callback } }; -#endif // V8_MAJOR_VERSION >= 10 +#endif // V8PP_HAS_FAST_API_HEADER } // namespace detail diff --git a/v8pp/function.hpp b/v8pp/function.hpp index 5788e3e..bd167ea 100644 --- a/v8pp/function.hpp +++ b/v8pp/function.hpp @@ -336,7 +336,7 @@ v8::Local wrap_function_template(v8::Isolate* isolate, v8::SideEffectType side_effect_type = v8::SideEffectType::kHasSideEffect) { using F_type = typename fast_function::func_type; -#if V8_MAJOR_VERSION >= 10 +#ifdef V8PP_HAS_FAST_API_HEADER if constexpr (fast_function::compatible) { auto c_func = v8::CFunction::Make(&detail::fast_callback::call); From d83dcfe61429648d44ff9fd6abf285d804b147dc Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:02:06 +0100 Subject: [PATCH 21/38] fix CI failures: chrono precision, test cleanup, and compiler warnings - fix time_point round-trip off-by-1ms by converting through integer milliseconds instead of double-precision nanosecond scaling - use integer milliseconds in time_point to_v8 for consistency - make JSON.stringify(Y) test order-independent for V8 13.0+ property enumeration changes - guard v8-fast-api-calls.h with __has_include for distros that ship V8 10+ without the header, define V8PP_HAS_FAST_API_HEADER macro - add missing template keyword on .inherit() for AppleClang 17 - skip unnecessary context/isolate creation in test main when no scripts are provided - suppress unused receiver/options parameter warnings in free-function fast_callback - remove unused GetClass and property_type aliases in const_property --- test/main.cpp | 29 ++++++++++++++++------------- v8pp/convert.hpp | 8 +++++--- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/test/main.cpp b/test/main.cpp index fac1f77..4e0d95f 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -144,25 +144,28 @@ int main(int argc, char const* argv[]) } int result = EXIT_SUCCESS; - try + if (!scripts.empty()) { - v8pp::context context; - - if (!lib_path.empty()) + try { - context.set_lib_path(lib_path); + v8pp::context context; + + if (!lib_path.empty()) + { + context.set_lib_path(lib_path); + } + for (std::string const& script : scripts) + { + v8::HandleScope scope(context.isolate()); + context.run_file(script); + } } - for (std::string const& script : scripts) + catch (std::exception const& ex) { - v8::HandleScope scope(context.isolate()); - context.run_file(script); + std::cerr << ex.what() << std::endl; + result = EXIT_FAILURE; } } - catch (std::exception const& ex) - { - std::cerr << ex.what() << std::endl; - result = EXIT_FAILURE; - } v8::V8::Dispose(); #if V8_MAJOR_VERSION > 9 || (V8_MAJOR_VERSION == 9 && V8_MINOR_VERSION >= 8) diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index f48ea2f..78c30b5 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -1130,16 +1130,18 @@ struct convert, void> { if (!is_valid(isolate, value)) return std::nullopt; double ms = value->NumberValue(isolate->GetCurrentContext()).FromJust(); + // Convert via integer milliseconds to avoid floating-point precision loss + // when scaling large epoch timestamps to finer-grained durations (e.g. nanoseconds) auto epoch_duration = std::chrono::duration_cast( - std::chrono::duration(ms)); + std::chrono::milliseconds(std::llround(ms))); return time_point_type(epoch_duration); } static to_type to_v8(v8::Isolate* isolate, from_type const& value) { - auto epoch_ms = std::chrono::duration_cast>( + auto epoch_ms = std::chrono::duration_cast( value.time_since_epoch()); - return v8::Number::New(isolate, epoch_ms.count()); + return v8::Number::New(isolate, static_cast(epoch_ms.count())); } }; From edc63361f115435109d68761b4b110fcdf0949e1 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:03:13 +0100 Subject: [PATCH 22/38] add debug logging to test main and revert context-wrapping regression --- test/main.cpp | 51 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/test/main.cpp b/test/main.cpp index 4e0d95f..a952cc0 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -81,6 +81,8 @@ void run_tests() int main(int argc, char const* argv[]) { + std::cerr << "[debug] main() entered" << std::endl; + std::vector scripts; std::string lib_path; bool do_tests = false; @@ -124,55 +126,72 @@ int main(int argc, char const* argv[]) } } + std::cerr << "[debug] SetFlagsFromString" << std::endl; // allow Isolate::RequestGarbageCollectionForTesting() before Initialize() // for v8pp::class_ tests v8::V8::SetFlagsFromString("--expose_gc"); + std::cerr << "[debug] InitializeExternalStartupData" << std::endl; //v8::V8::InitializeICU(); v8::V8::InitializeExternalStartupData(argv[0]); + + std::cerr << "[debug] NewDefaultPlatform" << std::endl; #if V8_MAJOR_VERSION >= 7 std::unique_ptr platform(v8::platform::NewDefaultPlatform()); #else std::unique_ptr platform(v8::platform::CreateDefaultPlatform()); #endif + + std::cerr << "[debug] InitializePlatform" << std::endl; v8::V8::InitializePlatform(platform.get()); + + std::cerr << "[debug] V8::Initialize" << std::endl; v8::V8::Initialize(); + std::cerr << "[debug] V8 initialized, do_tests=" << do_tests << " scripts=" << scripts.size() << std::endl; + if (do_tests || scripts.empty()) { + std::cerr << "[debug] running tests" << std::endl; run_tests(); + std::cerr << "[debug] tests finished" << std::endl; } int result = EXIT_SUCCESS; - if (!scripts.empty()) + try { - try + std::cerr << "[debug] creating v8pp::context" << std::endl; + v8pp::context context; + std::cerr << "[debug] v8pp::context created" << std::endl; + + if (!lib_path.empty()) { - v8pp::context context; - - if (!lib_path.empty()) - { - context.set_lib_path(lib_path); - } - for (std::string const& script : scripts) - { - v8::HandleScope scope(context.isolate()); - context.run_file(script); - } + context.set_lib_path(lib_path); } - catch (std::exception const& ex) + for (std::string const& script : scripts) { - std::cerr << ex.what() << std::endl; - result = EXIT_FAILURE; + v8::HandleScope scope(context.isolate()); + context.run_file(script); } + std::cerr << "[debug] destroying v8pp::context" << std::endl; } + catch (std::exception const& ex) + { + std::cerr << ex.what() << std::endl; + result = EXIT_FAILURE; + } + std::cerr << "[debug] v8pp::context destroyed" << std::endl; + std::cerr << "[debug] V8::Dispose" << std::endl; v8::V8::Dispose(); + + std::cerr << "[debug] ShutdownPlatform" << std::endl; #if V8_MAJOR_VERSION > 9 || (V8_MAJOR_VERSION == 9 && V8_MINOR_VERSION >= 8) v8::V8::DisposePlatform(); #else v8::V8::ShutdownPlatform(); #endif + std::cerr << "[debug] clean exit" << std::endl; return result; } From 39a34e292b6ed3b1a0753ce3cd45409ab56946a9 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:15:47 +0100 Subject: [PATCH 23/38] fix alive_contexts thread safety and add Windows DLL diagnostics --- .github/workflows/cmake.yml | 14 +++++++++++++ v8pp/context.cpp | 39 +++++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 20fda95..4a97ce4 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -71,6 +71,20 @@ jobs: - name: Build run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}} + - name: Debug DLL layout + if: startsWith(matrix.os, 'windows') + working-directory: ${{github.workspace}}/build + shell: cmd + run: | + echo === EXE location === + dir /b *.exe 2>nul + echo === DLLs in build dir === + dir /b *.dll 2>nul + echo === V8 DLL check === + where /r . v8.dll 2>nul + echo === Test exe dependencies === + dumpbin /dependents v8pp_test.exe 2>nul | findstr /i ".dll" + - name: Test working-directory: ${{github.workspace}}/build run: ctest -C ${{matrix.build_type}} -V diff --git a/v8pp/context.cpp b/v8pp/context.cpp index d0ad320..8bff416 100644 --- a/v8pp/context.cpp +++ b/v8pp/context.cpp @@ -7,6 +7,7 @@ #include "v8pp/throw_ex.hpp" #include +#include #include #include @@ -22,6 +23,12 @@ static char const path_sep = '/'; namespace v8pp { +static std::mutex& alive_contexts_mutex() +{ + static std::mutex mtx; + return mtx; +} + static std::unordered_set& alive_contexts() { static std::unordered_set set; @@ -49,9 +56,12 @@ void context::load_module(v8::FunctionCallbackInfo const& args) } context* ctx = detail::external_data::get(args.Data()); - if (alive_contexts().find(ctx) == alive_contexts().end()) { - throw std::runtime_error("require() called on destroyed context"); + std::lock_guard lock(alive_contexts_mutex()); + if (alive_contexts().find(ctx) == alive_contexts().end()) + { + throw std::runtime_error("require() called on destroyed context"); + } } // check if module is already loaded @@ -131,9 +141,12 @@ void context::run_file(v8::FunctionCallbackInfo const& args) } context* ctx = detail::external_data::get(args.Data()); - if (alive_contexts().find(ctx) == alive_contexts().end()) { - throw std::runtime_error("run() called on destroyed context"); + std::lock_guard lock(alive_contexts_mutex()); + if (alive_contexts().find(ctx) == alive_contexts().end()) + { + throw std::runtime_error("run() called on destroyed context"); + } } result = to_v8(isolate, ctx->run_file(filename)); } @@ -192,7 +205,10 @@ context::context(v8::Isolate* isolate, v8::ArrayBuffer::Allocator* allocator, } impl_.Reset(isolate_, impl); - alive_contexts().insert(this); + { + std::lock_guard lock(alive_contexts_mutex()); + alive_contexts().insert(this); + } } context::context(context&& src) noexcept @@ -204,6 +220,7 @@ context::context(context&& src) noexcept , modules_(std::move(src.modules_)) , lib_path_(std::move(src.lib_path_)) { + std::lock_guard lock(alive_contexts_mutex()); alive_contexts().erase(&src); alive_contexts().insert(this); } @@ -221,8 +238,11 @@ context& context::operator=(context&& src) noexcept impl_ = std::move(src.impl_); modules_ = std::move(src.modules_); lib_path_ = std::move(src.lib_path_); - alive_contexts().erase(&src); - alive_contexts().insert(this); + { + std::lock_guard lock(alive_contexts_mutex()); + alive_contexts().erase(&src); + alive_contexts().insert(this); + } } return *this; } @@ -240,7 +260,10 @@ void context::destroy() return; } - alive_contexts().erase(this); + { + std::lock_guard lock(alive_contexts_mutex()); + alive_contexts().erase(this); + } // remove all class singletons and external data before modules unload cleanup(isolate_); From a739cfacd76d4eaaae48005547b089af42a9c9c3 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 21:23:53 +0100 Subject: [PATCH 24/38] fix NuGet cache missing V8 redist DLLs on Windows --- .github/workflows/cmake.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 4a97ce4..fd20fa1 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -48,8 +48,10 @@ jobs: if: startsWith(matrix.os, 'windows') uses: actions/cache@v4 with: - path: ${{github.workspace}}/build/v8-v143-x64* - key: nuget-v8-v143-x64-${{runner.os}} + path: | + ${{github.workspace}}/build/v8-v143-x64* + ${{github.workspace}}/build/v8.redist-v143-x64* + key: nuget-v8-v143-x64-${{runner.os}}-v2 - name: Install V8 nuget if: startsWith(matrix.os, 'windows') From cbcee3621f3d7d6d705eb4ce332a2dc09b154e14 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:00:06 +0100 Subject: [PATCH 25/38] add BUILD_BENCHMARKS option Add BUILD_BENCHMARKS cmake option (OFF by default) to build the bench/ benchmark suite. --- CMakeLists.txt | 5 ++ bench/CMakeLists.txt | 20 ++++++ bench/bench.hpp | 144 +++++++++++++++++++++++++++++++++++++++ bench/bench_call.cpp | 79 +++++++++++++++++++++ bench/bench_class.cpp | 133 ++++++++++++++++++++++++++++++++++++ bench/bench_convert.cpp | 120 ++++++++++++++++++++++++++++++++ bench/bench_property.cpp | 73 ++++++++++++++++++++ bench/main.cpp | 90 ++++++++++++++++++++++++ 8 files changed, 664 insertions(+) create mode 100644 bench/CMakeLists.txt create mode 100644 bench/bench.hpp create mode 100644 bench/bench_call.cpp create mode 100644 bench/bench_class.cpp create mode 100644 bench/bench_convert.cpp create mode 100644 bench/bench_property.cpp create mode 100644 bench/main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fdf7d5..28f4c7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ endif() option(BUILD_SHARED_LIBS "Build shared library" ON) option(BUILD_TESTING "Build and run tests" OFF) option(BUILD_DOCUMENTATION "Build documentation" OFF) +option(BUILD_BENCHMARKS "Build benchmarks" OFF) # Custom project options option(V8PP_HEADER_ONLY "Header-only library" OFF) @@ -43,6 +44,10 @@ if(BUILD_TESTING) add_subdirectory(test) endif() +if(BUILD_BENCHMARKS) + add_subdirectory(bench) +endif() + if(BUILD_DOCUMENTATION) add_subdirectory(docs) endif() diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt new file mode 100644 index 0000000..a45a275 --- /dev/null +++ b/bench/CMakeLists.txt @@ -0,0 +1,20 @@ +# benchmark target + +add_executable(v8pp_bench + main.cpp + bench.hpp + bench_convert.cpp + bench_call.cpp + bench_class.cpp + bench_property.cpp +) + +if(V8PP_HEADER_ONLY) + target_sources(v8pp_bench PRIVATE ${PROJECT_SOURCE_DIR}/v8pp/context.cpp) + if(APPLE) + # see v8pp/CMakeLists.txt + set_source_files_properties(${PROJECT_SOURCE_DIR}/v8pp/context.cpp PROPERTIES COMPILE_FLAGS -fno-rtti) + endif() +endif() + +target_link_libraries(v8pp_bench PRIVATE v8pp ${CMAKE_DL_LIBS}) diff --git a/bench/bench.hpp b/bench/bench.hpp new file mode 100644 index 0000000..bb9859e --- /dev/null +++ b/bench/bench.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "v8pp/context.hpp" +#include "v8pp/convert.hpp" + +namespace v8pp::bench { + +struct result +{ + std::string name; + size_t iterations; + std::vector samples_ns; // per-iteration time in nanoseconds + + double min_ns() const + { + return *std::min_element(samples_ns.begin(), samples_ns.end()); + } + + double max_ns() const + { + return *std::max_element(samples_ns.begin(), samples_ns.end()); + } + + double mean_ns() const + { + return std::accumulate(samples_ns.begin(), samples_ns.end(), 0.0) + / static_cast(samples_ns.size()); + } + + double median_ns() const + { + std::vector sorted = samples_ns; + std::sort(sorted.begin(), sorted.end()); + size_t n = sorted.size(); + if (n % 2 == 0) + return (sorted[n / 2 - 1] + sorted[n / 2]) / 2.0; + return sorted[n / 2]; + } + + double ops_per_sec() const + { + double med = median_ns(); + return med > 0.0 ? 1'000'000'000.0 / med : 0.0; + } +}; + +/// Run a benchmark: warmup, then collect timing samples. +/// Each sample times `iterations_per_sample` calls of `fn`. +inline result run(std::string_view name, + size_t iterations_per_sample, + size_t sample_count, + std::function const& fn) +{ + // Warmup: 10% of iterations, minimum 10 + size_t warmup = std::max(iterations_per_sample / 10, 10); + for (size_t i = 0; i < warmup; ++i) + { + fn(); + } + + result r; + r.name = name; + r.iterations = iterations_per_sample; + r.samples_ns.reserve(sample_count); + + for (size_t s = 0; s < sample_count; ++s) + { + auto start = std::chrono::steady_clock::now(); + for (size_t i = 0; i < iterations_per_sample; ++i) + { + fn(); + } + auto end = std::chrono::steady_clock::now(); + + double elapsed_ns = std::chrono::duration(end - start).count(); + r.samples_ns.push_back(elapsed_ns / static_cast(iterations_per_sample)); + } + + return r; +} + +/// Convenience: benchmark a JS script executed via context.run_script() +inline result run_script_bench(std::string_view name, + v8pp::context& ctx, + std::string_view script, + size_t iterations = 10000, + size_t samples = 20) +{ + v8::Isolate* isolate = ctx.isolate(); + return run(name, iterations, samples, [&]() + { + v8::HandleScope scope(isolate); + v8::TryCatch try_catch(isolate); + ctx.run_script(script); + }); +} + +inline void print_result(result const& r) +{ + auto fmt_time = [](double ns) -> std::string + { + char buf[64]; + if (ns < 1'000.0) + std::snprintf(buf, sizeof(buf), "%.1f ns", ns); + else if (ns < 1'000'000.0) + std::snprintf(buf, sizeof(buf), "%.2f us", ns / 1'000.0); + else + std::snprintf(buf, sizeof(buf), "%.2f ms", ns / 1'000'000.0); + return buf; + }; + + auto fmt_ops = [](double ops) -> std::string + { + char buf[64]; + if (ops >= 1'000'000.0) + std::snprintf(buf, sizeof(buf), "%.2f M", ops / 1'000'000.0); + else if (ops >= 1'000.0) + std::snprintf(buf, sizeof(buf), "%.2f K", ops / 1'000.0); + else + std::snprintf(buf, sizeof(buf), "%.0f", ops); + return buf; + }; + + std::cout << std::left << std::setw(45) << r.name + << " median=" << std::setw(12) << fmt_time(r.median_ns()) + << " min=" << std::setw(12) << fmt_time(r.min_ns()) + << " max=" << std::setw(12) << fmt_time(r.max_ns()) + << " ops/s=" << fmt_ops(r.ops_per_sec()) + << "\n"; +} + +} // namespace v8pp::bench diff --git a/bench/bench_call.cpp b/bench/bench_call.cpp new file mode 100644 index 0000000..fbfd97f --- /dev/null +++ b/bench/bench_call.cpp @@ -0,0 +1,79 @@ +#include + +#include "v8pp/context.hpp" +#include "v8pp/module.hpp" +#include "v8pp/fast_api.hpp" +#include "bench.hpp" + +namespace { + +void noop() {} +int noop_return() { return 0; } +int32_t add_ints(int32_t a, int32_t b) { return a + b; } +std::string concat(std::string const& a, std::string const& b) { return a + b; } +double compute(double a, double b, double c) { return a * b + c; } + +} // anonymous namespace + +void bench_call() +{ + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + using namespace v8pp::bench; + size_t const N = 10000; + size_t const S = 20; + + // Bind functions into global scope + context.function("noop", &noop); + context.function("noop_return", &noop_return); + context.function("add_ints", &add_ints); + context.function("concat", &concat); + context.function("compute", &compute); + +#if V8_MAJOR_VERSION >= 10 + context.function("fast_add", v8pp::fast_fn<&add_ints>); +#endif + + // --- Baseline: empty function call overhead --- + print_result(run_script_bench("JS->C++ void noop()", + context, "noop()", N, S)); + + print_result(run_script_bench("JS->C++ int noop_return()", + context, "noop_return()", N, S)); + + // --- Primitive argument passing --- + print_result(run_script_bench("JS->C++ add_ints(int, int)", + context, "add_ints(1, 2)", N, S)); + +#if V8_MAJOR_VERSION >= 10 + // --- Fast API vs slow path --- + print_result(run_script_bench("JS->C++ fast_add(int, int)", + context, "fast_add(1, 2)", N, S)); +#endif + + // --- String arguments --- + print_result(run_script_bench("JS->C++ concat(str, str)", + context, "concat('hello', ' world')", N, S)); + + // --- Multiple arguments --- + print_result(run_script_bench("JS->C++ compute(dbl, dbl, dbl)", + context, "compute(1.5, 2.5, 3.5)", N, S)); + + // --- Tight loop from JS (V8 JIT) --- + context.function("add", &add_ints); + print_result(run_script_bench("JS loop: 1000x add_ints", + context, "var s = 0; for (var i = 0; i < 1000; i++) s = add(s, 1); s", + N / 100, S)); + + // --- Module-scoped function call --- + { + v8pp::module m(isolate); + m.function("add", &add_ints); + context.module("mod", m); + + print_result(run_script_bench("JS->C++ mod.add(int, int)", + context, "mod.add(1, 2)", N, S)); + } +} diff --git a/bench/bench_class.cpp b/bench/bench_class.cpp new file mode 100644 index 0000000..035c800 --- /dev/null +++ b/bench/bench_class.cpp @@ -0,0 +1,133 @@ +#include + +#include "v8pp/class.hpp" +#include "v8pp/context.hpp" +#include "v8pp/module.hpp" +#include "bench.hpp" + +namespace { + +struct Point +{ + double x, y; + Point() : x(0), y(0) {} + Point(double x, double y) : x(x), y(y) {} + double length() const { return std::sqrt(x * x + y * y); } + double dot(Point const& other) const { return x * other.x + y * other.y; } + void translate(double dx, double dy) { x += dx; y += dy; } +}; + +struct Base +{ + int value; + explicit Base(int v) : value(v) {} + int get_value() const { return value; } +}; + +struct Derived : Base +{ + int extra; + explicit Derived(int v) : Base(v), extra(v * 2) {} + int get_extra() const { return extra; } +}; + +} // anonymous namespace + +void bench_class() +{ + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + using namespace v8pp::bench; + size_t const N = 10000; + size_t const S = 20; + + // --- Point class: construction, methods, var access --- + { + v8pp::class_ point_class(isolate); + point_class + .ctor() + .var("x", &Point::x) + .var("y", &Point::y) + .function("length", &Point::length) + .function("dot", &Point::dot) + .function("translate", &Point::translate); + context.class_("Point", point_class); + + // Object construction from JS + print_result(run_script_bench("class: new Point(x,y)", + context, "var p = new Point(3.0, 4.0); p.x", N, S)); + + // Set up a persistent object for method/var benchmarks + context.run_script("var pt = new Point(3.0, 4.0)"); + + print_result(run_script_bench("class: pt.length()", + context, "pt.length()", N, S)); + + print_result(run_script_bench("class: pt.dot(pt)", + context, "pt.dot(pt)", N, S)); + + print_result(run_script_bench("class: pt.translate(dx,dy)", + context, "pt.translate(0.1, 0.1); pt.x", N, S)); + + // Direct member variable access via var() + print_result(run_script_bench("class: pt.x (get var)", + context, "pt.x", N, S)); + + print_result(run_script_bench("class: pt.x = val (set var)", + context, "pt.x = 5.0", N, S)); + } + + // --- C++ side wrap/unwrap --- + { + v8pp::class_ base_class(isolate); + base_class + .ctor() + .function("get_value", &Base::get_value); + context.class_("Base", base_class); + + // Measure wrap cost from C++ side + print_result(run("class: wrap_object (C++ side)", N, S, [&]() + { + v8::HandleScope hs(isolate); + Base* obj = new Base(42); + v8pp::class_::reference_external(isolate, obj); + v8pp::class_::unreference_external(isolate, obj); + delete obj; + })); + + // Measure unwrap from a JS value + context.run_script("var b = new Base(42)"); + v8::Local b_val = context.run_script("b"); + print_result(run("class: unwrap_object (C++ side)", N, S, [&]() + { + v8pp::class_::unwrap_object(isolate, b_val); + })); + } + + // --- Inheritance --- + { + v8pp::class_ derived_class(isolate); + derived_class + .ctor() + .inherit() + .function("get_extra", &Derived::get_extra); + context.class_("Derived", derived_class); + + print_result(run_script_bench("class: new Derived (with inherit)", + context, "var d = new Derived(10); d.get_value()", N, S)); + + context.run_script("var dd = new Derived(10)"); + print_result(run_script_bench("class: base method via derived", + context, "dd.get_value()", N, S)); + } + + // --- Bulk object creation (GC pressure) --- + print_result(run("class: 100x new Point from JS", N / 100, S, [&]() + { + v8::HandleScope hs(isolate); + v8::TryCatch try_catch(isolate); + context.run_script("for (var i = 0; i < 100; i++) new Point(i, i)"); + })); +} diff --git a/bench/bench_convert.cpp b/bench/bench_convert.cpp new file mode 100644 index 0000000..d2d2437 --- /dev/null +++ b/bench/bench_convert.cpp @@ -0,0 +1,120 @@ +#include +#include +#include +#include + +#include "v8pp/context.hpp" +#include "v8pp/convert.hpp" +#include "bench.hpp" + +void bench_convert() +{ + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + using namespace v8pp::bench; + size_t const N = 50000; + size_t const S = 20; + + // --- Primitive to_v8 --- + print_result(run("int32 to_v8", N, S, [&]() + { + v8::HandleScope hs(isolate); + v8pp::to_v8(isolate, 42); + })); + + print_result(run("double to_v8", N, S, [&]() + { + v8::HandleScope hs(isolate); + v8pp::to_v8(isolate, 3.14); + })); + + print_result(run("bool to_v8", N, S, [&]() + { + v8::HandleScope hs(isolate); + v8pp::to_v8(isolate, true); + })); + + // --- Primitive from_v8 --- + v8::Local v8_int = v8pp::to_v8(isolate, 42); + print_result(run("int32 from_v8", N, S, [&]() + { + v8pp::from_v8(isolate, v8_int); + })); + + v8::Local v8_double = v8pp::to_v8(isolate, 3.14); + print_result(run("double from_v8", N, S, [&]() + { + v8pp::from_v8(isolate, v8_double); + })); + + v8::Local v8_bool = v8pp::to_v8(isolate, true); + print_result(run("bool from_v8", N, S, [&]() + { + v8pp::from_v8(isolate, v8_bool); + })); + + // --- String conversions --- + print_result(run("short string to_v8 (5 chars)", N, S, [&]() + { + v8::HandleScope hs(isolate); + v8pp::to_v8(isolate, "hello"); + })); + + print_result(run("long string to_v8 (100 chars)", N, S, [&]() + { + v8::HandleScope hs(isolate); + v8pp::to_v8(isolate, std::string(100, 'x')); + })); + + v8::Local v8_str = v8pp::to_v8(isolate, "hello world"); + print_result(run("string from_v8", N, S, [&]() + { + v8pp::from_v8(isolate, v8_str); + })); + + // --- Container conversions --- + std::vector vec100(100, 42); + print_result(run("vector(100) to_v8", N / 10, S, [&]() + { + v8::HandleScope hs(isolate); + v8pp::to_v8(isolate, vec100); + })); + + v8::Local v8_arr = v8pp::to_v8(isolate, vec100); + print_result(run("vector(100) from_v8", N / 10, S, [&]() + { + v8pp::from_v8>(isolate, v8_arr); + })); + + // --- Map conversion --- + std::map map10; + for (int i = 0; i < 10; ++i) map10["key" + std::to_string(i)] = i; + print_result(run("map(10) to_v8", N / 10, S, [&]() + { + v8::HandleScope hs(isolate); + v8pp::to_v8(isolate, map10); + })); + + v8::Local v8_map = v8pp::to_v8(isolate, map10); + print_result(run("map(10) from_v8", N / 10, S, [&]() + { + v8pp::from_v8>(isolate, v8_map); + })); + + // --- std::optional --- + std::optional opt_val = 42; + print_result(run("optional to_v8 (engaged)", N, S, [&]() + { + v8::HandleScope hs(isolate); + v8pp::to_v8(isolate, opt_val); + })); + + std::optional opt_empty; + print_result(run("optional to_v8 (empty)", N, S, [&]() + { + v8::HandleScope hs(isolate); + v8pp::to_v8(isolate, opt_empty); + })); +} diff --git a/bench/bench_property.cpp b/bench/bench_property.cpp new file mode 100644 index 0000000..c5f0093 --- /dev/null +++ b/bench/bench_property.cpp @@ -0,0 +1,73 @@ +#include + +#include "v8pp/class.hpp" +#include "v8pp/context.hpp" +#include "v8pp/property.hpp" +#include "bench.hpp" + +namespace { + +struct Widget +{ + int width_ = 100; + int height_ = 200; + std::string name_ = "widget"; + + int get_width() const { return width_; } + void set_width(int w) { width_ = w; } + + int get_height() const { return height_; } + void set_height(int h) { height_ = h; } + + std::string const& get_name() const { return name_; } + void set_name(std::string const& n) { name_ = n; } +}; + +} // anonymous namespace + +void bench_property() +{ + v8pp::context context; + v8::Isolate* isolate = context.isolate(); + v8::HandleScope scope(isolate); + + using namespace v8pp::bench; + size_t const N = 10000; + size_t const S = 20; + + v8pp::class_ widget_class(isolate); + widget_class + .ctor<>() + .property("width", &Widget::get_width, &Widget::set_width) + .property("height", &Widget::get_height, &Widget::set_height) + .property("name", &Widget::get_name, &Widget::set_name); + context.class_("Widget", widget_class); + + context.run_script("var w = new Widget()"); + + // --- Property getter (int) --- + print_result(run_script_bench("property: get int (w.width)", + context, "w.width", N, S)); + + // --- Property setter (int) --- + print_result(run_script_bench("property: set int (w.width = 42)", + context, "w.width = 42", N, S)); + + // --- Property getter (string) --- + print_result(run_script_bench("property: get string (w.name)", + context, "w.name", N, S)); + + // --- Property setter (string) --- + print_result(run_script_bench("property: set string (w.name = 'x')", + context, "w.name = 'test'", N, S)); + + // --- Property read in tight loop --- + print_result(run_script_bench("property: 100x get in loop", + context, "var s = 0; for (var i = 0; i < 100; i++) s += w.width; s", + N / 10, S)); + + // --- Property write in tight loop --- + print_result(run_script_bench("property: 100x set in loop", + context, "for (var i = 0; i < 100; i++) w.width = i; w.width", + N / 10, S)); +} diff --git a/bench/main.cpp b/bench/main.cpp new file mode 100644 index 0000000..fc75405 --- /dev/null +++ b/bench/main.cpp @@ -0,0 +1,90 @@ +#include +#include +#include +#include + +#include +#include + +#include "v8pp/version.hpp" + +void run_benchmarks() +{ + void bench_convert(); + void bench_call(); + void bench_class(); + void bench_property(); + + std::pair benchmarks[] = + { + { "bench_convert", bench_convert }, + { "bench_call", bench_call }, + { "bench_class", bench_class }, + { "bench_property", bench_property }, + }; + + for (auto const& bench : benchmarks) + { + std::cout << "\n=== " << bench.first << " ===\n"; + try + { + bench.second(); + } + catch (std::exception const& ex) + { + std::cerr << " error: " << ex.what() << '\n'; + } + } +} + +int main(int argc, char const* argv[]) +{ + for (int i = 1; i < argc; ++i) + { + std::string const arg = argv[i]; + if (arg == "-h" || arg == "--help") + { + std::cout << "Usage: " << argv[0] << " [options]\n" + << "Options:\n" + << " --help,-h Print this message and exit\n" + << " --version,-v Print V8 and v8pp version\n" + ; + return 0; + } + else if (arg == "-v" || arg == "--version") + { + std::cout << "V8 version " << v8::V8::GetVersion() << "\n"; + std::cout << "v8pp version " << v8pp::version() << "\n"; + return 0; + } + } + + v8::V8::SetFlagsFromString("--expose_gc"); + v8::V8::InitializeExternalStartupData(argv[0]); + +#if V8_MAJOR_VERSION >= 7 + std::unique_ptr platform(v8::platform::NewDefaultPlatform()); +#else + std::unique_ptr platform(v8::platform::CreateDefaultPlatform()); +#endif + + v8::V8::InitializePlatform(platform.get()); + v8::V8::Initialize(); + + std::cout << "V8 version " << v8::V8::GetVersion() << "\n"; + std::cout << "v8pp version " << v8pp::version() << "\n"; + + run_benchmarks(); + + std::cout << "\ndone.\n"; + + v8::V8::Dispose(); + +#if V8_MAJOR_VERSION > 9 || (V8_MAJOR_VERSION == 9 && V8_MINOR_VERSION >= 8) + v8::V8::DisposePlatform(); +#else + v8::V8::ShutdownPlatform(); +#endif + + return 0; +} From e53d4519a5a49392c6957d0ddb345e5bc0aa97e1 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:00:19 +0100 Subject: [PATCH 26/38] clean up debug printings from before --- test/main.cpp | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/test/main.cpp b/test/main.cpp index a952cc0..fac1f77 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -81,8 +81,6 @@ void run_tests() int main(int argc, char const* argv[]) { - std::cerr << "[debug] main() entered" << std::endl; - std::vector scripts; std::string lib_path; bool do_tests = false; @@ -126,43 +124,29 @@ int main(int argc, char const* argv[]) } } - std::cerr << "[debug] SetFlagsFromString" << std::endl; // allow Isolate::RequestGarbageCollectionForTesting() before Initialize() // for v8pp::class_ tests v8::V8::SetFlagsFromString("--expose_gc"); - std::cerr << "[debug] InitializeExternalStartupData" << std::endl; //v8::V8::InitializeICU(); v8::V8::InitializeExternalStartupData(argv[0]); - - std::cerr << "[debug] NewDefaultPlatform" << std::endl; #if V8_MAJOR_VERSION >= 7 std::unique_ptr platform(v8::platform::NewDefaultPlatform()); #else std::unique_ptr platform(v8::platform::CreateDefaultPlatform()); #endif - - std::cerr << "[debug] InitializePlatform" << std::endl; v8::V8::InitializePlatform(platform.get()); - - std::cerr << "[debug] V8::Initialize" << std::endl; v8::V8::Initialize(); - std::cerr << "[debug] V8 initialized, do_tests=" << do_tests << " scripts=" << scripts.size() << std::endl; - if (do_tests || scripts.empty()) { - std::cerr << "[debug] running tests" << std::endl; run_tests(); - std::cerr << "[debug] tests finished" << std::endl; } int result = EXIT_SUCCESS; try { - std::cerr << "[debug] creating v8pp::context" << std::endl; v8pp::context context; - std::cerr << "[debug] v8pp::context created" << std::endl; if (!lib_path.empty()) { @@ -173,25 +157,19 @@ int main(int argc, char const* argv[]) v8::HandleScope scope(context.isolate()); context.run_file(script); } - std::cerr << "[debug] destroying v8pp::context" << std::endl; } catch (std::exception const& ex) { std::cerr << ex.what() << std::endl; result = EXIT_FAILURE; } - std::cerr << "[debug] v8pp::context destroyed" << std::endl; - std::cerr << "[debug] V8::Dispose" << std::endl; v8::V8::Dispose(); - - std::cerr << "[debug] ShutdownPlatform" << std::endl; #if V8_MAJOR_VERSION > 9 || (V8_MAJOR_VERSION == 9 && V8_MINOR_VERSION >= 8) v8::V8::DisposePlatform(); #else v8::V8::ShutdownPlatform(); #endif - std::cerr << "[debug] clean exit" << std::endl; return result; } From 4afae201b0e6df7930d9f39600429be432af75fe Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:00:51 +0100 Subject: [PATCH 27/38] add CUSTOM_V8_LIB_PATH support for custom V8 builds Support linking against a custom V8 library via CUSTOM_V8_LIB_PATH and CUSTOM_V8_INCLUDE_PATH cmake variables. Handles single-file libs (.a/.so/.lib/.dylib) or a directory of static libs. Links winmm and dbghelp on Windows, sets static CRT (/MT) to match V8 monolith builds. Also accepts a pre-existing V8::V8 target via add_subdirectory. Enables CMP0091 policy for CMAKE_MSVC_RUNTIME_LIBRARY support. --- .gitignore | 3 ++- CMakeLists.txt | 9 +++++++++ v8pp/CMakeLists.txt | 30 +++++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e7b218e..8b31136 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ out/ /build*/ .vscode/ CMakeSettings.json -.claude/ \ No newline at end of file +.claude/ +third_party/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 28f4c7e..dfcf35f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,10 @@ cmake_minimum_required(VERSION 3.12) +# CMP0091: use CMAKE_MSVC_RUNTIME_LIBRARY instead of compiler flags +if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) +endif() + project(v8pp VERSION 2.1.1 DESCRIPTION "Bind C++ functions and classes into V8 JavaScript engine" @@ -13,6 +18,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) if (MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zc:__cplusplus") # enable correct __cplusplus macro + # static CRT when using a custom V8 monolith built with /MT + if(DEFINED CUSTOM_V8_LIB_PATH) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + endif() endif() option(BUILD_SHARED_LIBS "Build shared library" ON) diff --git a/v8pp/CMakeLists.txt b/v8pp/CMakeLists.txt index 187f3e5..f8ea84a 100644 --- a/v8pp/CMakeLists.txt +++ b/v8pp/CMakeLists.txt @@ -2,7 +2,32 @@ include(GNUInstallDirs) -if(DEFINED VCPKG_TARGET_TRIPLET) +if(TARGET V8::V8) + message(STATUS "Using existing V8::V8 target") +elseif(DEFINED CUSTOM_V8_LIB_PATH) + add_library(CustomV8 INTERFACE) + add_library(V8::V8 ALIAS CustomV8) + if(DEFINED CUSTOM_V8_INCLUDE_PATH) + target_include_directories(CustomV8 INTERFACE + $ + ) + endif() + get_filename_component(_v8_ext "${CUSTOM_V8_LIB_PATH}" EXT) + if("${_v8_ext}" STREQUAL ".a" OR "${_v8_ext}" STREQUAL ".so" OR "${_v8_ext}" STREQUAL ".lib" OR "${_v8_ext}" STREQUAL ".dylib") + target_link_libraries(CustomV8 INTERFACE "${CUSTOM_V8_LIB_PATH}") + else() + # directory containing individual V8 static libs + target_link_libraries(CustomV8 INTERFACE + "${CUSTOM_V8_LIB_PATH}/libv8_libbase.a" + "${CUSTOM_V8_LIB_PATH}/libv8_libplatform.a" + "${CUSTOM_V8_LIB_PATH}/libv8_monolith.a" + ) + endif() + if(WIN32) + target_link_libraries(CustomV8 INTERFACE winmm dbghelp) + endif() + message(STATUS "Using custom V8 from ${CUSTOM_V8_LIB_PATH}") +elseif(DEFINED VCPKG_TARGET_TRIPLET) find_package(V8 CONFIG REQUIRED) else() list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) @@ -104,6 +129,9 @@ write_basic_package_version_file("${PROJECT_BINARY_DIR}/ConfigVersion.cmake" COM configure_package_config_file("${PROJECT_SOURCE_DIR}/cmake/Config.cmake.in" "${PROJECT_BINARY_DIR}/Config.cmake" INSTALL_DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/v8pp/cmake) +if(TARGET CustomV8) + install(TARGETS CustomV8 EXPORT ${targets_export_name}) +endif() install(TARGETS v8pp EXPORT ${targets_export_name}) install(EXPORT ${targets_export_name} NAMESPACE v8pp:: From fbee282222726c0bc2bc6691f56e8ca3a9fd73d8 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:05:04 +0100 Subject: [PATCH 28/38] remove reference to none existing change analysis in README --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 8dc6f3b..74423bc 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,6 @@ Tested on: - Concepts replace SFINAE for type dispatch (`mapping`, `sequence`, `set_like`, `callable`, etc.) - Dead V8 < 9.0 code paths removed -For a detailed change-by-change analysis, see [docs/FORK_CHANGES.md](docs/FORK_CHANGES.md). - ## Building and testing The library has a set of tests that can be configured, built, and run with CMake: From 4fb9b92a26b4d6827d748998f696ce8f44f08113 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:22:59 +0100 Subject: [PATCH 29/38] bump version to 3.0.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dfcf35f..53fd8fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if(POLICY CMP0091) endif() project(v8pp - VERSION 2.1.1 + VERSION 3.0.0 DESCRIPTION "Bind C++ functions and classes into V8 JavaScript engine" HOMEPAGE_URL https://github.com/pmed/v8pp LANGUAGES CXX From e0596e09f3ca3483668cab12eac45b98c22094f6 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:36:27 +0100 Subject: [PATCH 30/38] fix: use proper UTF-8 conversion for filesystem::path in v8pp on Windows On Windows, std::filesystem::path(std::string) interprets the string using the ANSI codepage, not UTF-8. V8 strings are UTF-8, so non-ASCII paths were silently mangled in both directions (JS->C++ and C++->JS). Convert through wstring via MultiByteToWideChar/WideCharToMultiByte for correct UTF-8 round-tripping on Windows. --- v8pp/convert.hpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index 78c30b5..243962c 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -8,6 +8,9 @@ #include #include #include +#ifdef WIN32 +#include +#endif #include #include #include @@ -1054,7 +1057,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 +1072,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 } }; From 548f85f2e05069e771d329ac9ef96f873894a379 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:47:40 +0100 Subject: [PATCH 31/38] fix: define _AMD64_ before including stringapiset.h to fix Windows CI build stringapiset.h transitively includes winnt.h which expects _AMD64_ to be defined, but that macro is normally set up by windef.h (via windows.h). Since we include stringapiset.h directly, we need to bridge this ourselves. --- v8pp/convert.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index 243962c..70de485 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -9,6 +9,9 @@ #include #include #ifdef WIN32 +#if defined(_M_AMD64) && !defined(_AMD64_) +#define _AMD64_ +#endif #include #endif #include From 363bbed0bf7ac6078cc98dce4ca89b6d5bceed56 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:49:43 +0100 Subject: [PATCH 32/38] fix: add _AMD64_ and NOMINMAX defines for stringapiset.h on Windows stringapiset.h transitively includes winnt.h which expects _AMD64_, and minwindef.h which defines min/max macros that break std::numeric_limits. --- v8pp/convert.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index 70de485..80e4da3 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -12,7 +12,9 @@ #if defined(_M_AMD64) && !defined(_AMD64_) #define _AMD64_ #endif +#define NOMINMAX #include +#undef NOMINMAX #endif #include #include From f0d8ccda206ea9ba0f2ee168c86db8fee6d81bc2 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Fri, 13 Feb 2026 00:07:15 +0100 Subject: [PATCH 33/38] include windows.h to setup all posibilities... --- v8pp/convert.hpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/v8pp/convert.hpp b/v8pp/convert.hpp index 80e4da3..9f7d5e1 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -9,11 +9,9 @@ #include #include #ifdef WIN32 -#if defined(_M_AMD64) && !defined(_AMD64_) -#define _AMD64_ -#endif +#define WIN32_LEAN_AND_MEAN #define NOMINMAX -#include +#include #undef NOMINMAX #endif #include From 7fd12e3ef6aa49b3fb765b47b3813d58c1e848f1 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:56:43 +0100 Subject: [PATCH 34/38] add ctor extensions and fast property support - add call_ctor_entry helpers to overload.hpp for constructor multi-dispatch - add ctor(factory, defaults) overload to class_ for factory functions with default parameters - add ctor(f1, f2, ...) overload to class_ for multi-dispatch constructors with first-match-wins dispatch - add fast_function property overloads to class_ using SetAccessorProperty (read-only and read-write) - add fast_function property overloads to module using SetAccessorProperty (read-only and read-write) - add requires constraint on existing property() in class_ and module to disambiguate from fast_function overloads - add ctor factory+defaults, multi-dispatch, and multi-dispatch+with_defaults tests to test_class.cpp - add fast class property and fast module property tests to test_fast_api.cpp --- test/test_class.cpp | 184 +++++++++++++++++++++++++++++++++++++++++ test/test_fast_api.cpp | 69 ++++++++++++++++ v8pp/class.hpp | 137 ++++++++++++++++++++++++++++++ v8pp/module.hpp | 39 +++++++++ v8pp/overload.hpp | 23 ++++++ 5 files changed, 452 insertions(+) diff --git a/test/test_class.cpp b/test/test_class.cpp index e273514..30188f3 100644 --- a/test/test_class.cpp +++ b/test/test_class.cpp @@ -539,6 +539,181 @@ void test_auto_wrap_objects() 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 +727,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_fast_api.cpp b/test/test_fast_api.cpp index e6afe80..e54e13f 100644 --- a/test/test_fast_api.cpp +++ b/test/test_fast_api.cpp @@ -102,4 +102,73 @@ void test_fast_api() // --- 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/v8pp/class.hpp b/v8pp/class.hpp index 5285d4d..f8a3edd 100644 --- a/v8pp/class.hpp +++ b/v8pp/class.hpp @@ -468,6 +468,102 @@ class class_ 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; + } + /// Inhert from C++ class U template class_& inherit() @@ -602,6 +698,7 @@ class class_ /// 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, @@ -646,6 +743,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) diff --git a/v8pp/module.hpp b/v8pp/module.hpp index f88e8e6..45fef85 100644 --- a/v8pp/module.hpp +++ b/v8pp/module.hpp @@ -133,6 +133,7 @@ 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; @@ -167,6 +168,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/overload.hpp b/v8pp/overload.hpp index bd1adc1..dd202a5 100644 --- a/v8pp/overload.hpp +++ b/v8pp/overload.hpp @@ -206,6 +206,29 @@ bool try_invoke_entry(overload_entry> const& entry, 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; From 7af9c8016dec73dbd7ceed96cd33184a02c62e1d Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Fri, 13 Feb 2026 04:03:54 +0100 Subject: [PATCH 35/38] Add change detection job with dorny/paths-filter Add clang-format lint job Add ccache for Linux/macOS builds --- .github/workflows/cmake.yml | 52 +++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index fd20fa1..5e8e9af 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -9,7 +9,41 @@ 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: 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 --dry-run --Werror + build: + needs: changes + if: needs.changes.outputs.code == 'true' timeout-minutes: 30 strategy: fail-fast: false @@ -36,7 +70,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 +101,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}} From 690fda3ce796d94858a747bb58de20d352b9acd0 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:53:55 +0100 Subject: [PATCH 36/38] treat explicit undefined as "use the default" to match JS native behavior - modify arg_or_default in call_from_v8.hpp to check args[Index]->IsUndefined() in addition to args.Length() <= Index - add tests for undefined triggering defaults: middle position, last position, multiple undefined args, and string defaults --- test/test_call_from_v8.cpp | 12 ++++++++++++ v8pp/call_from_v8.hpp | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/test/test_call_from_v8.cpp b/test/test_call_from_v8.cpp index cc638b1..a49be2d 100644 --- a/test/test_call_from_v8.cpp +++ b/test/test_call_from_v8.cpp @@ -185,4 +185,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/v8pp/call_from_v8.hpp b/v8pp/call_from_v8.hpp index 182682d..a658e61 100644 --- a/v8pp/call_from_v8.hpp +++ b/v8pp/call_from_v8.hpp @@ -166,7 +166,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)); From 38a0269cfabb164deea57ce6e6b7ab7984773663 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Fri, 13 Feb 2026 11:56:34 +0100 Subject: [PATCH 37/38] apply clang-format to all source files - format all .hpp, .cpp, .ipp files in v8pp/, test/, plugins/ - 46 files reformatted to match .clang-format config --- plugins/file.cpp | 13 +- test/main.cpp | 66 ++-- test/test.hpp | 8 +- test/test_adversarial.cpp | 642 +++++++++++++++++----------------- test/test_call_from_v8.cpp | 47 ++- test/test_class.cpp | 122 +++---- test/test_context.cpp | 6 +- test/test_context_store.cpp | 643 +++++++++++++++++----------------- test/test_convert.cpp | 309 +++++++---------- test/test_fast_api.cpp | 365 +++++++++---------- test/test_function.cpp | 25 +- test/test_gc_stress.cpp | 451 ++++++++++++------------ test/test_json.cpp | 146 ++++---- test/test_module.cpp | 24 +- test/test_overload.cpp | 264 +++++++------- test/test_promise.cpp | 367 ++++++++++---------- test/test_property.cpp | 56 ++- test/test_symbol.cpp | 487 +++++++++++++------------- test/test_thread_safety.cpp | 663 ++++++++++++++++++----------------- test/test_type_info.cpp | 14 +- test/test_utility.cpp | 34 +- v8pp/call_from_v8.hpp | 16 +- v8pp/call_v8.hpp | 3 +- v8pp/class.cpp | 24 +- v8pp/class.hpp | 186 +++++----- v8pp/class.ipp | 57 ++- v8pp/context.cpp | 18 +- v8pp/context.hpp | 12 +- v8pp/context_store.cpp | 10 +- v8pp/context_store.hpp | 278 +++++++-------- v8pp/context_store.ipp | 675 ++++++++++++++++++------------------ v8pp/convert.hpp | 159 +++++---- v8pp/fast_api.hpp | 310 +++++++++-------- v8pp/function.hpp | 29 +- v8pp/json.ipp | 3 +- v8pp/module.hpp | 24 +- v8pp/object.hpp | 16 +- v8pp/overload.hpp | 673 +++++++++++++++++------------------ v8pp/promise.hpp | 352 +++++++++---------- v8pp/property.hpp | 459 ++++++++++++------------ v8pp/ptr_traits.hpp | 17 +- v8pp/throw_ex.ipp | 3 +- v8pp/type_info.hpp | 2 +- v8pp/utility.hpp | 52 ++- v8pp/version.hpp | 2 +- v8pp/version.ipp | 14 +- 46 files changed, 4112 insertions(+), 4034 deletions(-) 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 a49be2d..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()); diff --git a/test/test_class.cpp b/test/test_class.cpp index 30188f3..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,10 +527,12 @@ 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)); @@ -568,8 +568,7 @@ void test_ctor_factory_defaults() .ctor(make_widget, v8pp::defaults(100, 200, std::string("untitled"))) .var("width", &Widget::width) .var("height", &Widget::height) - .var("label", &Widget::label) - ; + .var("label", &Widget::label); context.class_("Widget", widget_class); @@ -606,9 +605,12 @@ void test_ctor_multi_dispatch() 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) {} + 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; @@ -635,8 +637,7 @@ void test_ctor_multi_dispatch() .ctor(make_default, make_circle, make_rect) .var("kind", &Shape::kind) .var("a", &Shape::a) - .var("b", &Shape::b) - ; + .var("b", &Shape::b); context.class_("Shape", shape_class); @@ -671,8 +672,10 @@ void test_ctor_multi_dispatch_with_defaults() int mode; std::string name; - Config() : mode(0), name("default") {} - Config(int m, std::string n) : mode(m), name(std::move(n)) {} + Config() + : mode(0), name("default") {} + Config(int m, std::string n) + : mode(m), name(std::move(n)) {} }; v8pp::context context; @@ -694,8 +697,7 @@ void test_ctor_multi_dispatch_with_defaults() .ctor(make_default, v8pp::with_defaults(make_full, v8pp::defaults(42, std::string("auto")))) .var("mode", &Config::mode) - .var("name", &Config::name) - ; + .var("name", &Config::name); context.class_("Config", config_class); 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 e54e13f..02e7ad7 100644 --- a/test/test_fast_api.cpp +++ b/test/test_fast_api.cpp @@ -1,174 +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. - - // --- 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); - } -} +#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 a658e61..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; 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 f8a3edd..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,18 +464,16 @@ 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) + requires(detail::is_callable>::value && !v8pp::is_defaults>::value) class_& ctor(Function&& func, v8pp::defaults defs) { using F = std::decay_t; @@ -480,8 +481,7 @@ class class_ "Constructor factory must return object_pointer_type"); class_info_.set_ctor( - [func = F(std::forward(func)), defs = std::move(defs)] - (v8::FunctionCallbackInfo const& args) + [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); @@ -494,10 +494,7 @@ class class_ /// 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>) + 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< @@ -506,11 +503,10 @@ class class_ 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) + [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(); @@ -519,39 +515,37 @@ class class_ 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); + { ((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)"; + std::string msg = "No matching constructor overload for " + std::to_string(arg_count) + " argument(s)"; if (!errors.empty()) { msg += ". Tried: " + errors; @@ -572,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; } @@ -592,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); } @@ -611,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); } @@ -623,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()); @@ -644,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()); @@ -680,25 +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) + requires(!is_fast_function>::value) class_& property(std::string_view name, GetFunction&& get, SetFunction&& set = {}) { using Getter = typename std::conditional_t, @@ -709,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>; @@ -727,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, @@ -800,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; } @@ -854,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); @@ -878,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); @@ -898,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; } @@ -1112,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 9f7d5e1..be7a437 100644 --- a/v8pp/convert.hpp +++ b/v8pp/convert.hpp @@ -52,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 @@ -76,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; @@ -130,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(); } } @@ -281,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; @@ -330,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; @@ -377,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; @@ -398,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) @@ -451,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; } @@ -461,7 +503,7 @@ struct convert> } else { - throw invalid_argument(isolate, value, "Optional"); + throw invalid_argument(isolate, value, "Optional"); } } @@ -469,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; } @@ -502,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) @@ -631,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: @@ -660,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 @@ -749,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) @@ -797,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; @@ -831,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) @@ -861,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); @@ -985,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); @@ -1011,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) @@ -1276,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>> { }; @@ -1314,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*; @@ -1339,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) @@ -1349,7 +1369,7 @@ struct convert }; template - requires is_wrapped_class::value +requires is_wrapped_class::value struct convert { using from_type = T&; @@ -1379,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) @@ -1391,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; @@ -1416,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) @@ -1446,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"); @@ -1456,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) @@ -1490,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 { @@ -1503,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 @@ -1581,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 @@ -1637,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 45fef85..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,15 +128,14 @@ class module /// Set property in the module with specified name and get/set functions template - requires (!is_fast_function>::value) + 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; @@ -153,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, 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 dd202a5..ce7ef07 100644 --- a/v8pp/overload.hpp +++ b/v8pp/overload.hpp @@ -1,266 +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 - { - 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) +#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) { - 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) - { // Fold: short-circuit on first match via (matched || try_one()) ((matched || [&] { @@ -289,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 } From 85cb0754a22b7923f1459e0232c70dfa83c2a833 Mon Sep 17 00:00:00 2001 From: MangelSpec <74370284+MangelSpec@users.noreply.github.com> Date: Fri, 13 Feb 2026 12:10:43 +0100 Subject: [PATCH 38/38] use clang-format-19 --- .github/workflows/cmake.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 5e8e9af..34e8ceb 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -34,12 +34,17 @@ jobs: 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 --dry-run --Werror + xargs clang-format-19 --dry-run --Werror build: needs: changes