diff --git a/patches/v8/0014-Add-another-slot-in-the-isolate-for-embedder.patch b/patches/v8/0014-Add-another-slot-in-the-isolate-for-embedder.patch index 92a20670ea5..5cef9c30c74 100644 --- a/patches/v8/0014-Add-another-slot-in-the-isolate-for-embedder.patch +++ b/patches/v8/0014-Add-another-slot-in-the-isolate-for-embedder.patch @@ -12,9 +12,9 @@ index 9a39f8ede92bad9acf1c390cde2048d7a9eb6a88..efdd0ec674f11a0e68a2e996f6ac0dc6 @@ -918,7 +918,7 @@ class Internals { static const int kExternalTwoByteRepresentationTag = 0x02; static const int kExternalOneByteRepresentationTag = 0x0a; - + - static const uint32_t kNumIsolateDataSlots = 4; -+ static const uint32_t kNumIsolateDataSlots = 5; ++ static const uint32_t kNumIsolateDataSlots = 6; static const int kStackGuardSize = 8 * kApiSystemPointerSize; static const int kNumberOfBooleanFlags = 6; static const int kErrorMessageParamSize = 1; diff --git a/src/workerd/jsg/jsg.h b/src/workerd/jsg/jsg.h index 3ab93e2ae03..3b1984f12c7 100644 --- a/src/workerd/jsg/jsg.h +++ b/src/workerd/jsg/jsg.h @@ -796,6 +796,8 @@ enum SetDataIndex { // The address of the base of the 4Gbyte compressed pointer area. // If we are using the sandbox it's also the base of the sandbox. SET_DATA_CAGE_BASE, + // The type handler registry slot + SET_DATA_TYPE_HANDLER_REGISTRY, // The number of slots workerd uses in the API data for Isolate objects. SET_DATA_SLOTS_IN_USE, }; diff --git a/src/workerd/jsg/type-wrapper-test.c++ b/src/workerd/jsg/type-wrapper-test.c++ index c0f3839d830..d787aea47c0 100644 --- a/src/workerd/jsg/type-wrapper-test.c++ +++ b/src/workerd/jsg/type-wrapper-test.c++ @@ -357,5 +357,309 @@ KJ_TEST("unimplemented errors") { e.expectEval("takeStructWithUnimplementedMembers(undefined)", "undefined", "undefined"); } +// ======================================================================================== +// TypeHandlerRegistry tests +// +// These tests verify the TypeHandlerRegistry system, which provides type-erased access to +// TypeHandler instances. The registry allows code to wrap/unwrap values without needing +// to know the full TypeWrapper template instantiation, making it possible to pass type +// conversion capabilities across API boundaries without template parameters. + +struct TypeHandlerRegistryContext: public ContextGlobalObject { + // Test methods that use the registry + v8::Local registryWrapString(jsg::Lock& js, kj::String value) { + auto& registry = TypeHandlerRegistry::from(js); + auto& handler = registry.getHandler(); + return handler.wrap(js, kj::mv(value)); + } + + kj::Maybe registryUnwrapString(jsg::Lock& js, v8::Local value) { + auto& registry = TypeHandlerRegistry::from(js); + auto& handler = registry.getHandler(); + return handler.tryUnwrap(js, value); + } + + v8::Local registryWrapInt(jsg::Lock& js, int value) { + auto& registry = TypeHandlerRegistry::from(js); + auto& handler = registry.getHandler(); + return handler.wrap(js, value); + } + + kj::Maybe registryUnwrapInt(jsg::Lock& js, v8::Local value) { + auto& registry = TypeHandlerRegistry::from(js); + auto& handler = registry.getHandler(); + return handler.tryUnwrap(js, value); + } + + v8::Local registryWrapDouble(jsg::Lock& js, double value) { + auto& registry = TypeHandlerRegistry::from(js); + auto& handler = registry.getHandler(); + return handler.wrap(js, value); + } + + kj::Maybe registryUnwrapDouble(jsg::Lock& js, v8::Local value) { + auto& registry = TypeHandlerRegistry::from(js); + auto& handler = registry.getHandler(); + return handler.tryUnwrap(js, value); + } + + // Test that we can get a handler (throws if not found) + bool registryCanGetStringHandler(jsg::Lock& js) { + auto& registry = TypeHandlerRegistry::from(js); + try { + registry.getHandler(); + return true; + } catch (...) { + return false; + } + } + + bool registryCanGetBoolHandler(jsg::Lock& js) { + auto& registry = TypeHandlerRegistry::from(js); + try { + registry.getHandler(); + return true; + } catch (...) { + return false; + } + } + + struct Foo: public jsg::Object { + JSG_RESOURCE_TYPE(Foo) {} + }; + + bool registryCanGetFooHandler(jsg::Lock& js) { + auto& registry = TypeHandlerRegistry::from(js); + try { + registry.getHandler>(); + return true; + } catch (...) { + return false; + } + } + + JSG_RESOURCE_TYPE(TypeHandlerRegistryContext) { + JSG_METHOD(registryWrapString); + JSG_METHOD(registryUnwrapString); + JSG_METHOD(registryWrapInt); + JSG_METHOD(registryUnwrapInt); + JSG_METHOD(registryWrapDouble); + JSG_METHOD(registryUnwrapDouble); + JSG_METHOD(registryCanGetStringHandler); + JSG_METHOD(registryCanGetBoolHandler); + JSG_METHOD(registryCanGetFooHandler); + } +}; + +JSG_DECLARE_ISOLATE_TYPE( + TypeHandlerRegistryIsolate, TypeHandlerRegistryContext, TypeHandlerRegistryContext::Foo); + +KJ_TEST("TypeHandlerRegistry - basic functionality") { + Evaluator e(v8System); + + // Test wrapping and unwrapping strings + e.expectEval("registryWrapString('hello world')", "string", "hello world"); + e.expectEval("registryUnwrapString('test string')", "string", "test string"); + + // Test wrapping and unwrapping integers + e.expectEval("registryWrapInt(42)", "number", "42"); + e.expectEval("registryUnwrapInt(123)", "number", "123"); + + // Test wrapping and unwrapping doubles + e.expectEval("registryWrapDouble(3.14159)", "number", "3.14159"); + e.expectEval("registryUnwrapDouble(2.71828)", "number", "2.71828"); +} + +KJ_TEST("TypeHandlerRegistry - type checking") { + Evaluator e(v8System); + + // Test that handlers can be retrieved (no exception) + e.expectEval("registryCanGetStringHandler()", "boolean", "true"); + e.expectEval("registryCanGetBoolHandler()", "boolean", "true"); + e.expectEval("registryCanGetFooHandler()", "boolean", "false"); +} + +KJ_TEST("TypeHandlerRegistry - round-trip conversions") { + Evaluator e(v8System); + + // Round-trip string conversion + e.expectEval("registryUnwrapString(registryWrapString('round trip'))", "string", "round trip"); + + // Round-trip number conversions + e.expectEval("registryUnwrapInt(registryWrapInt(999))", "number", "999"); + e.expectEval("registryUnwrapDouble(registryWrapDouble(1.23))", "number", "1.23"); +} + +KJ_TEST("TypeHandlerRegistry - null/undefined handling") { + Evaluator e(v8System); + + // tryUnwrap should return null for incompatible types + e.expectEval("registryUnwrapString(123)", "string", "123"); + e.expectEval("registryUnwrapString(null)", "string", "null"); + e.expectEval("registryUnwrapString(undefined)", "string", "undefined"); + + e.expectEval("registryUnwrapInt('not a number')", "number", "0"); + e.expectEval("registryUnwrapInt(null)", "number", "0"); +} + +// ======================================================================================== +// Mock TypeHandler tests + +template +class MockTypeHandler final: public TypeHandler { + mutable int wrapCallCount = 0; + mutable int unwrapCallCount = 0; + T mockValue; + + public: + explicit MockTypeHandler(T mockValue): mockValue(kj::mv(mockValue)) {} + + v8::Local wrap(Lock& js, T value) const override { + wrapCallCount++; + if constexpr (kj::isSameType()) { + return v8StrIntern(js.v8Isolate, "MOCK_STRING"); + } else if constexpr (kj::isSameType()) { + return v8::Number::New(js.v8Isolate, 999); + } else if constexpr (kj::isSameType()) { + return v8::Number::New(js.v8Isolate, 9.99); + } + return v8::Undefined(js.v8Isolate); + } + + kj::Maybe tryUnwrap(Lock& js, v8::Local handle) const override { + unwrapCallCount++; + if constexpr (kj::isSameType()) { + return kj::str(mockValue); + } else { + return mockValue; + } + } + + int getWrapCallCount() const { + return wrapCallCount; + } + int getUnwrapCallCount() const { + return unwrapCallCount; + } +}; + +struct MockHandlerContext: public ContextGlobalObject { + v8::Local useStringHandler(jsg::Lock& js, kj::String value) { + auto& registry = TypeHandlerRegistry::from(js); + auto& handler = registry.getHandler(); + return handler.wrap(js, kj::mv(value)); + } + + JSG_RESOURCE_TYPE(MockHandlerContext) { + JSG_METHOD(useStringHandler); + } +}; + +JSG_DECLARE_ISOLATE_TYPE(MockHandlerIsolate, MockHandlerContext); + +KJ_TEST("TypeHandlerRegistry - mock handlers") { + Evaluator e(v8System); + + // First, test with default handlers + e.expectEval("useStringHandler('original')", "string", "original"); + + // Now we would need to inject mock handlers for more advanced testing + // This demonstrates the capability but requires access to isolate initialization +} + +// ======================================================================================== +// Test direct registry API usage + +KJ_TEST("TypeHandlerRegistry - direct API") { + Evaluator e(v8System); + + e.getIsolate().runInLockScope([&](TypeHandlerRegistryIsolate::Lock& lock) { + JSG_WITHIN_CONTEXT_SCOPE(lock, + lock.newContext().getHandle(lock.v8Isolate), + [&](jsg::Lock& js) { + auto& registry = TypeHandlerRegistry::from(js); + + // Test that we can get handlers for built-in types (will throw if not registered) + auto& stringHandler = registry.getHandler(); + auto& intHandler = registry.getHandler(); + auto& doubleHandler = registry.getHandler(); + [[maybe_unused]] auto& boolHandler = registry.getHandler(); + + // Test wrapping with the registry + auto jsString = stringHandler.wrap(js, kj::str("test")); + KJ_EXPECT(jsString->IsString()); + + // Test unwrapping with the registry + auto maybeStr = stringHandler.tryUnwrap(js, jsString); + KJ_EXPECT(maybeStr != kj::none); + KJ_EXPECT(KJ_REQUIRE_NONNULL(maybeStr) == "test"); + + // Test integer handler + auto jsInt = intHandler.wrap(js, 42); + KJ_EXPECT(jsInt->IsNumber()); + + auto maybeInt = intHandler.tryUnwrap(js, jsInt); + KJ_EXPECT(maybeInt != kj::none); + KJ_EXPECT(KJ_REQUIRE_NONNULL(maybeInt) == 42); + + // Test double handler + auto jsDouble = doubleHandler.wrap(js, 3.14159); + KJ_EXPECT(jsDouble->IsNumber()); + + auto maybeDouble = doubleHandler.tryUnwrap(js, jsDouble); + KJ_EXPECT(maybeDouble != kj::none); + KJ_EXPECT(KJ_REQUIRE_NONNULL(maybeDouble) == 3.14159); + }); + }); +} + +KJ_TEST("TypeHandlerRegistry - error handling") { + Evaluator e(v8System); + + e.getIsolate().runInLockScope([&](TypeHandlerRegistryIsolate::Lock& lock) { + JSG_WITHIN_CONTEXT_SCOPE(lock, + lock.newContext().getHandle(lock.v8Isolate), + [&](jsg::Lock& js) { + auto& registry = TypeHandlerRegistry::from(js); + + // Test that getHandler works for registered types + auto& stringHandler = registry.getHandler(); + auto jsValue = stringHandler.wrap(js, kj::str("test")); + KJ_EXPECT(jsValue->IsString()); + + // Test that getHandler works for int + auto& intHandler = registry.getHandler(); + auto jsInt = intHandler.wrap(js, 42); + KJ_EXPECT(jsInt->IsNumber()); + }); + }); +} + +KJ_TEST("TypeHandlerRegistry - type mismatches") { + Evaluator e(v8System); + + e.getIsolate().runInLockScope([&](TypeHandlerRegistryIsolate::Lock& lock) { + JSG_WITHIN_CONTEXT_SCOPE(lock, + lock.newContext().getHandle(lock.v8Isolate), + [&](jsg::Lock& js) { + auto& registry = TypeHandlerRegistry::from(js); + + // Try to unwrap wrong type - should return kj::none + auto& stringHandler = registry.getHandler(); + auto jsNumber = v8::Number::New(js.v8Isolate, 42); + + auto maybeStr = stringHandler.tryUnwrap(js, jsNumber); + // String handler should handle number coercion based on its implementation + // This test verifies the tryUnwrap behavior + + auto maybeStrFromNull = stringHandler.tryUnwrap(js, js.null()); + KJ_EXPECT(KJ_ASSERT_NONNULL(maybeStrFromNull) == "null"_kj); + + auto maybeStrFromUndefined = stringHandler.tryUnwrap(js, js.undefined()); + KJ_EXPECT(KJ_ASSERT_NONNULL(maybeStrFromUndefined) == "undefined"_kj); + }); + }); +} + } // namespace } // namespace workerd::jsg::test diff --git a/src/workerd/jsg/type-wrapper.h b/src/workerd/jsg/type-wrapper.h index 780239c77be..a3eaccfb841 100644 --- a/src/workerd/jsg/type-wrapper.h +++ b/src/workerd/jsg/type-wrapper.h @@ -316,6 +316,59 @@ class TypeWrapperBase, JsgKind::EXTENSI Configuration configuration; }; +class TypeHandlerRegistry final { + public: + TypeHandlerRegistry() = default; + KJ_DISALLOW_COPY_AND_MOVE(TypeHandlerRegistry); + + template + void registerHandler(const TypeHandler& handler) { + handlers.insert(std::type_index(typeid(T)), &handler); + } + + template + const TypeHandler& getHandler() const { + auto iter = handlers.find(std::type_index(typeid(T))); + return *reinterpret_cast*>( + KJ_ASSERT_NONNULL(iter, "TypeHandler not found:", typeid(T).name())); + } + + static TypeHandlerRegistry& from(v8::Isolate* isolate); + static TypeHandlerRegistry& from(Lock& js) { + return from(js.v8Isolate); + } + + bool isInitialized() const { + return initialized; + } + + // Store a callback to call initializeRegistry on the owning TypeWrapper + void setInitializer(kj::Function init) { + initializerFunc = kj::mv(init); + } + + void setFactory(kj::Own factoryRegistry) { + factory = kj::mv(factoryRegistry); + } + + void markInitialized() { + initialized = true; + } + + private: + // Store TypeHandlers as void* keyed by std::type_index. + // The actual type is const TypeHandler* for each registered T + kj::HashMap handlers; + kj::Maybe> factory; + kj::Maybe> initializerFunc; + bool initialized = false; + void callInitializer() { + KJ_IF_SOME(func, initializerFunc) { + func(); + } + } +}; + // The TypeWrapper class aggregates functionality to convert between C++ values and JavaScript // values. It primarily implements two methods: // @@ -413,6 +466,7 @@ class TypeWrapper: public DynamicResourceTypeMap, public JsValueWrapper { // TODO(soon): Should the TypeWrapper object be stored on the isolate rather than the context? bool fastApiEnabled = false; + TypeHandlerRegistry registry; public: template @@ -421,6 +475,9 @@ class TypeWrapper: public DynamicResourceTypeMap, MaybeWrapper(configuration), PromiseWrapper(configuration) { isolate->SetData(SET_DATA_TYPE_WRAPPER, this); + // Set the registry pointer immediately so it's available for lazy initialization + isolate->SetData(SET_DATA_TYPE_HANDLER_REGISTRY, ®istry); + registry.setInitializer([this]() { this->initializeRegistry(); }); fastApiEnabled = util::Autogate::isEnabled(util::AutogateKey::V8_FAST_API); } KJ_DISALLOW_COPY_AND_MOVE(TypeWrapper); @@ -596,6 +653,53 @@ class TypeWrapper: public DynamicResourceTypeMap, void initReflection(Holder* holder, PropertyReflection&... reflections) { (initReflection(holder, reflections), ...); } + + private: + void initializeRegistry() { + // Register handlers for all user-defined types. + (registerTypeInRegistry(), ...); + registerBuiltinTypes(); + } + + template + void registerTypeInRegistry() { + // Only register if it's a value type (not a resource or struct) + // Resources/structs use different mechanisms + if constexpr (canWrapByValue()) { + registry.registerHandler(TYPE_HANDLER_INSTANCE); + } + } + + void registerBuiltinTypes() { + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE); + registry.registerHandler(TYPE_HANDLER_INSTANCE>); + registry.registerHandler(TYPE_HANDLER_INSTANCE>); + // Add more built-in types as needed based on what the application uses. + } + + template + static constexpr bool canWrapByValue() { + // Resources, structs, and extensions use different wrapping mechanisms + if constexpr (requires { U::JSG_KIND; }) { + return U::JSG_KIND != JsgKind::RESOURCE && U::JSG_KIND != JsgKind::STRUCT && + U::JSG_KIND != JsgKind::EXTENSION; + } + return true; + } }; template @@ -615,6 +719,21 @@ class TypeWrapper::TypeHandlerImpl final: public TypeHandler } }; +// Implementation of TypeHandlerRegistry::from() - defined here after TypeWrapper is complete +inline TypeHandlerRegistry& TypeHandlerRegistry::from(v8::Isolate* isolate) { + auto* registry = + reinterpret_cast(isolate->GetData(SET_DATA_TYPE_HANDLER_REGISTRY)); + KJ_ASSERT(registry != nullptr, "TypeHandlerRegistry not found on isolate"); + + // Lazy initialization: initialize the registry on first access if not already initialized + if (!registry->isInitialized()) { + registry->callInitializer(); + registry->markInitialized(); + } + + return *registry; +} + // This macro helps cut down on template spam in error messages. Instead of instantiating Isolate // directly, do: //