Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 2 additions & 0 deletions src/workerd/jsg/jsg.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
304 changes: 304 additions & 0 deletions src/workerd/jsg/type-wrapper-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -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<v8::Value> registryWrapString(jsg::Lock& js, kj::String value) {
auto& registry = TypeHandlerRegistry::from(js);
auto& handler = registry.getHandler<kj::String>();
return handler.wrap(js, kj::mv(value));
}

kj::Maybe<kj::String> registryUnwrapString(jsg::Lock& js, v8::Local<v8::Value> value) {
auto& registry = TypeHandlerRegistry::from(js);
auto& handler = registry.getHandler<kj::String>();
return handler.tryUnwrap(js, value);
}

v8::Local<v8::Value> registryWrapInt(jsg::Lock& js, int value) {
auto& registry = TypeHandlerRegistry::from(js);
auto& handler = registry.getHandler<int>();
return handler.wrap(js, value);
}

kj::Maybe<int> registryUnwrapInt(jsg::Lock& js, v8::Local<v8::Value> value) {
auto& registry = TypeHandlerRegistry::from(js);
auto& handler = registry.getHandler<int>();
return handler.tryUnwrap(js, value);
}

v8::Local<v8::Value> registryWrapDouble(jsg::Lock& js, double value) {
auto& registry = TypeHandlerRegistry::from(js);
auto& handler = registry.getHandler<double>();
return handler.wrap(js, value);
}

kj::Maybe<double> registryUnwrapDouble(jsg::Lock& js, v8::Local<v8::Value> value) {
auto& registry = TypeHandlerRegistry::from(js);
auto& handler = registry.getHandler<double>();
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<kj::String>();
return true;
} catch (...) {
return false;
}
}

bool registryCanGetBoolHandler(jsg::Lock& js) {
auto& registry = TypeHandlerRegistry::from(js);
try {
registry.getHandler<bool>();
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<jsg::Ref<Foo>>();
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<TypeHandlerRegistryContext, TypeHandlerRegistryIsolate> 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<TypeHandlerRegistryContext, TypeHandlerRegistryIsolate> 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<TypeHandlerRegistryContext, TypeHandlerRegistryIsolate> 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<TypeHandlerRegistryContext, TypeHandlerRegistryIsolate> 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 <typename T>
class MockTypeHandler final: public TypeHandler<T> {
mutable int wrapCallCount = 0;
mutable int unwrapCallCount = 0;
T mockValue;

public:
explicit MockTypeHandler(T mockValue): mockValue(kj::mv(mockValue)) {}

v8::Local<v8::Value> wrap(Lock& js, T value) const override {
wrapCallCount++;
if constexpr (kj::isSameType<T, kj::String>()) {
return v8StrIntern(js.v8Isolate, "MOCK_STRING");
} else if constexpr (kj::isSameType<T, int>()) {
return v8::Number::New(js.v8Isolate, 999);
} else if constexpr (kj::isSameType<T, double>()) {
return v8::Number::New(js.v8Isolate, 9.99);
}
return v8::Undefined(js.v8Isolate);
}

kj::Maybe<T> tryUnwrap(Lock& js, v8::Local<v8::Value> handle) const override {
unwrapCallCount++;
if constexpr (kj::isSameType<T, kj::String>()) {
return kj::str(mockValue);
} else {
return mockValue;
}
}

int getWrapCallCount() const {
return wrapCallCount;
}
int getUnwrapCallCount() const {
return unwrapCallCount;
}
};

struct MockHandlerContext: public ContextGlobalObject {
v8::Local<v8::Value> useStringHandler(jsg::Lock& js, kj::String value) {
auto& registry = TypeHandlerRegistry::from(js);
auto& handler = registry.getHandler<kj::String>();
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<MockHandlerContext, MockHandlerIsolate> 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<TypeHandlerRegistryContext, TypeHandlerRegistryIsolate> e(v8System);

e.getIsolate().runInLockScope([&](TypeHandlerRegistryIsolate::Lock& lock) {
JSG_WITHIN_CONTEXT_SCOPE(lock,
lock.newContext<TypeHandlerRegistryContext>().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<kj::String>();
auto& intHandler = registry.getHandler<int>();
auto& doubleHandler = registry.getHandler<double>();
[[maybe_unused]] auto& boolHandler = registry.getHandler<bool>();

// 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<TypeHandlerRegistryContext, TypeHandlerRegistryIsolate> e(v8System);

e.getIsolate().runInLockScope([&](TypeHandlerRegistryIsolate::Lock& lock) {
JSG_WITHIN_CONTEXT_SCOPE(lock,
lock.newContext<TypeHandlerRegistryContext>().getHandle(lock.v8Isolate),
[&](jsg::Lock& js) {
auto& registry = TypeHandlerRegistry::from(js);

// Test that getHandler works for registered types
auto& stringHandler = registry.getHandler<kj::String>();
auto jsValue = stringHandler.wrap(js, kj::str("test"));
KJ_EXPECT(jsValue->IsString());

// Test that getHandler works for int
auto& intHandler = registry.getHandler<int>();
auto jsInt = intHandler.wrap(js, 42);
KJ_EXPECT(jsInt->IsNumber());
});
});
}

KJ_TEST("TypeHandlerRegistry - type mismatches") {
Evaluator<TypeHandlerRegistryContext, TypeHandlerRegistryIsolate> e(v8System);

e.getIsolate().runInLockScope([&](TypeHandlerRegistryIsolate::Lock& lock) {
JSG_WITHIN_CONTEXT_SCOPE(lock,
lock.newContext<TypeHandlerRegistryContext>().getHandle(lock.v8Isolate),
[&](jsg::Lock& js) {
auto& registry = TypeHandlerRegistry::from(js);

// Try to unwrap wrong type - should return kj::none
auto& stringHandler = registry.getHandler<kj::String>();
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
Loading
Loading