Skip to content

Commit

Permalink
Adds JSG_NAMED_INTERCEPT
Browse files Browse the repository at this point in the history
This mechanism uses v8's underlying named property handler mechanism
to allow a JSG_RESOURCE_TYPE to expose dynamic named properties
(much like a Proxy). Currently this only supports read-only properties.
The intent of this is to support the dynamic interface of a JS RPC
proxy.
  • Loading branch information
jasnell committed Oct 26, 2023
1 parent 95493b4 commit a3d7ead
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 3 deletions.
50 changes: 48 additions & 2 deletions src/workerd/jsg/jsg-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -360,10 +360,54 @@ KJ_TEST("Test JSG_CALLABLE") {
e.expectEval("let obj = getCallable(); new obj();", "boolean", "true");
}

} // namespace

// ========================================================================================
struct InterceptContext: public ContextGlobalObject {
struct ProxyImpl: public jsg::Object,
public jsg::NamedIntercept {
static jsg::Ref<ProxyImpl> constructor() { return jsg::alloc<ProxyImpl>(); }

int getBar() { return 123; }

// NamedIntercept implementation
kj::Maybe<jsg::JsValue> getNamed(jsg::Lock& js, kj::StringPtr name) override {
if (name == "foo") {
return kj::Maybe(js.str("bar"_kj));
} else if (name == "abc") {
JSG_FAIL_REQUIRE(TypeError, "boom");
}
return kj::none;
}

kj::Array<kj::String> listNamed(Lock& js) override {
return kj::arr(kj::str("foo"));
}

JSG_RESOURCE_TYPE(ProxyImpl) {
JSG_READONLY_PROTOTYPE_PROPERTY(bar, getBar);
JSG_NAMED_INTERCEPT();
}
};

JSG_RESOURCE_TYPE(InterceptContext) {
JSG_NESTED_TYPE(ProxyImpl);
}
};
JSG_DECLARE_ISOLATE_TYPE(InterceptIsolate, InterceptContext, InterceptContext::ProxyImpl);

KJ_TEST("Named interceptor") {
Evaluator<InterceptContext, InterceptIsolate> e(v8System);
// Calling Object.keys(p) here just to verify that it does not throw.
// Also, the test tries modifying the known intercepted property foo but verifies
// that the value is readonly/unchanged.
e.expectEval("p = new ProxyImpl; Object.keys(p); p.foo = 123; p.foo", "string", "bar");
e.expectEval("p = new ProxyImpl; p.bar", "number", "123");
e.expectEval("p = new ProxyImpl; Reflect.has(p, 'foo')", "boolean", "true");
e.expectEval("p = new ProxyImpl; Reflect.has(p, 'bar')", "boolean", "true");
e.expectEval("p = new ProxyImpl; Reflect.has(p, 'baz')", "boolean", "false");
e.expectEval("p = new ProxyImpl; p.abc", "throws", "TypeError: boom");
}

// ========================================================================================
struct IsolateUuidContext: public ContextGlobalObject {
JSG_RESOURCE_TYPE(IsolateUuidContext) {}
};
Expand All @@ -379,4 +423,6 @@ KJ_TEST("jsg::Lock getUuid") {
KJ_ASSERT(lock.getUuid().size() == 36);
}

} // namespace

} // namespace workerd::jsg::test
7 changes: 7 additions & 0 deletions src/workerd/jsg/jsg.h
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,13 @@ using HasGetTemplateOverload = decltype(
wrapper.initReflection(this, __VA_ARGS__); \
}

// Configures the resource type to implement named property interception.
// @see the definition of jsg::NamedIntercept in resource.h for more information.
#define JSG_NAMED_INTERCEPT() \
do { \
registry.template registerNamedIntercept<Self>(); \
} while (false)

// Use inside a JSG_RESOURCE_TYPE block to declare that this type should be considered a "root" for
// the purposes of automatically generating TypeScript definitions. All "root" types and their
// recursively referenced types (e.g. method parameter/return types, property types, inherits, etc)
Expand Down
21 changes: 21 additions & 0 deletions src/workerd/jsg/jsvalue.h
Original file line number Diff line number Diff line change
Expand Up @@ -554,4 +554,25 @@ class JsMessage final {
v8::Local<v8::Message> inner;
};

inline kj::Maybe<JsValue>
NamedIntercept::getNamed(Lock&, kj::StringPtr) {
return kj::none;
}

inline kj::Maybe<NamedIntercept::Attribute>
NamedIntercept::queryNamed(Lock& js, kj::StringPtr name) {
// By default, we currently only support read only properties.
auto list = listNamed(js);
for (auto& item : list) {
if (item == name) return jsg::NamedIntercept::READ_ONLY_ATTRIBUTE;
}
return kj::none;
}

inline kj::Array<kj::String>
NamedIntercept::listNamed(Lock&) {
return kj::Array<kj::String>();
}


} // namespace workerd::jsg
3 changes: 3 additions & 0 deletions src/workerd/jsg/resource.c++
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ namespace workerd::jsg {

// TODO(cleanup): Factor out toObject(), getInterned() into some sort of v8 tools module?

const NamedIntercept::Attribute NamedIntercept::READ_ONLY_ATTRIBUTE =
NamedIntercept::Attribute::READ_ONLY | NamedIntercept::Attribute::DONT_DELETE;

void exposeGlobalScopeType(v8::Isolate* isolate, v8::Local<v8::Context> context) {
auto global = context->Global();

Expand Down
163 changes: 163 additions & 0 deletions src/workerd/jsg/resource.h
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,163 @@ class DynamicResourceTypeMap {
friend class ObjectWrapper;
};

// ======================================================================================
// NamedIntercept implementation

class JsValue;

// A utility class that allows dynamic evaluation of properties on the JavaScript wrapper.
// TODO(soon): Currently, this mechanism only supports read-only properties. The setter
// interceptor is not implemented. That is, for a known dynamic name like `foo`, it is not
// yet possible to set `obj.foo = whatever` in JS. However, it remains possible to
// set unknown properties on the object (properties for which `queryNamed()` and `getNamed()`
// would return kj::none).
//
// struct ProxyImpl: public jsg::Object,
// public jsg::NamedIntercept {
// static jsg::Ref<ProxyImpl> constructor() { return jsg::alloc<ProxyImpl>(); }
//
// int getBar() { return 123; }
//
// // Return the value, if any, of a given named property
// kj::Maybe<jsg::JsValue> getNamed(jsg::Lock& js, kj::StringPtr name) override {
// if (name == "foo") {
// return kj::Maybe(js.str("bar"_kj));
// }
// return kj::none;
// }
//
// // Return the attributes, if any, of a given named property
// kj::Maybe<jsg::NamedIntercept::Attribute> queryNamed(jsg::Lock& js,
// kj::StringPtr name) override {
// auto list = listNamed(js);
// for (auto& item : list) {
// if (item == name) return jsg::NamedIntercept::READ_ONLY_ATTRIBUTE;
// }
// return kj::none;
// }
//
// // Return a list of the named properties that can be handled dynamically.
// kj::Array<kj::String> listNamed(Lock& js) override {
// return kj::arr(kj::str("foo"));
// }
//
// JSG_RESOURCE_TYPE(ProxyImpl) {
// JSG_READONLY_PROTOTYPE_PROPERTY(bar, getBar);
// JSG_NAMED_INTERCEPT();
// }
// };
class NamedIntercept {
public:
enum class Attribute {
NONE = v8::PropertyAttribute::None,
READ_ONLY = v8::PropertyAttribute::ReadOnly,
DONT_ENUM = v8::PropertyAttribute::DontEnum,
DONT_DELETE = v8::PropertyAttribute::DontDelete,
};

// Returns the value associated with the given name, or kj::none if the name is not known.
virtual kj::Maybe<JsValue> getNamed(Lock& js, kj::StringPtr name);

// Returns the Attribute(s) for the given name, or kj::none if the name is not known.
virtual kj::Maybe<Attribute> queryNamed(Lock& js, kj::StringPtr name);

// List all names that are known (all names for which getNamed and queryNamed should never
// return kj::none)
virtual kj::Array<kj::String> listNamed(Lock& js);

static const Attribute READ_ONLY_ATTRIBUTE;
};

inline constexpr NamedIntercept::Attribute operator|(NamedIntercept::Attribute a,
NamedIntercept::Attribute b) {
return static_cast<NamedIntercept::Attribute>(static_cast<int>(a) | static_cast<int>(b));
}
inline constexpr NamedIntercept::Attribute operator&(NamedIntercept::Attribute a,
NamedIntercept::Attribute b) {
return static_cast<NamedIntercept::Attribute>(static_cast<int>(a) & static_cast<int>(b));
}
inline constexpr NamedIntercept::Attribute operator~(NamedIntercept::Attribute a) {
return static_cast<NamedIntercept::Attribute>(~static_cast<uint>(a));
}

template <typename TypeWrapper, typename T,
typename = kj::EnableIf<std::is_assignable_v<NamedIntercept, T>>>
struct NamedInterceptorCallbacks: public v8::NamedPropertyHandlerConfiguration {
NamedInterceptorCallbacks() : v8::NamedPropertyHandlerConfiguration(
getter,
nullptr,
query,
nullptr,
enumerator,
nullptr,
nullptr,
v8::Local<v8::Value>(),
static_cast<v8::PropertyHandlerFlags>(
static_cast<int>(v8::PropertyHandlerFlags::kNonMasking) |
static_cast<int>(v8::PropertyHandlerFlags::kHasNoSideEffect) |
static_cast<int>(v8::PropertyHandlerFlags::kOnlyInterceptStrings))) {}

template <typename U>
static kj::Maybe<T&> unwrapThis(const v8::PropertyCallbackInfo<U>& info) {
auto context = info.GetIsolate()->GetCurrentContext();
if (info.This()->InternalFieldCount() != 3) return kj::none;
return extractInternalPointer<T, false>(context, info.This());
}

static void getter(v8::Local<v8::Name> name,
const v8::PropertyCallbackInfo<v8::Value>& info) {
auto isolate = info.GetIsolate();
auto& lock = Lock::from(isolate);
KJ_IF_SOME(self, unwrapThis(info)) {
lock.tryCatch([&] {
KJ_IF_SOME(value, self.getNamed(lock, kj::str(name.As<v8::String>()))) {
info.GetReturnValue().Set(v8::Local<v8::Value>(value));
}
}, [&](Value exception) {
// Catch any jsg::JsExceptionThrown or kj::Exceptions that are thrown
// and just reschedule the exception on the isolate.
isolate->ThrowException(exception.getHandle(lock));
});
}
}

static void query(v8::Local<v8::Name> name,
const v8::PropertyCallbackInfo<v8::Integer>& info) {
KJ_IF_SOME(self, unwrapThis(info)) {
auto isolate = info.GetIsolate();
auto& lock = Lock::from(isolate);
lock.tryCatch([&] {
KJ_IF_SOME(attr, self.queryNamed(lock, kj::str(name.As<v8::String>()))) {
info.GetReturnValue().Set(v8::Integer::New(isolate, static_cast<int32_t>(attr)));
}
}, [&](Value exception) {
// Catch any jsg::JsExceptionThrown or kj::Exceptions that are thrown
// and just reschedule the exception on the isolate.
isolate->ThrowException(exception.getHandle(lock));
});
}
}

static void enumerator(const v8::PropertyCallbackInfo<v8::Array>& info) {
KJ_IF_SOME(self, unwrapThis(info)) {
auto isolate = info.GetIsolate();
auto& lock = Lock::from(isolate);
auto& wrapper = TypeWrapper::from(isolate);
lock.tryCatch([&] {
auto value = wrapper.wrap(isolate->GetCurrentContext(), kj::none, self.listNamed(lock));
info.GetReturnValue().Set(value.template As<v8::Array>());
}, [&](Value exception) {
// Catch any jsg::JsExceptionThrown or kj::Exceptions that are thrown
// and just reschedule the exception on the isolate.
isolate->ThrowException(exception.getHandle(lock));
});
}
}
};

// ======================================================================================

// Used by the JSG_METHOD macro to register a method on a resource type.
template<typename TypeWrapper, typename Self, bool isContext>
struct ResourceTypeBuilder {
Expand All @@ -623,6 +780,12 @@ struct ResourceTypeBuilder {
v8::PropertyAttribute::ReadOnly | v8::PropertyAttribute::DontEnum));
}

template <typename Type,
typename = kj::EnableIf<std::is_assignable_v<NamedIntercept, Type>>>
inline void registerNamedIntercept() {
prototype->SetHandler(NamedInterceptorCallbacks<TypeWrapper, Type> {});
}

template<typename Type>
inline void registerInherit() {
constructor->Inherit(typeWrapper.template getTemplate<isContext>(isolate, (Type*)nullptr));
Expand Down
12 changes: 11 additions & 1 deletion src/workerd/jsg/rtti.h
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,10 @@ struct BuildRtti<Configuration, const T&> {

// count all members in the structure
struct MemberCounter {
template <typename Type,
typename = kj::EnableIf<std::is_assignable_v<NamedIntercept, Type>>>
inline void registerNamedIntercept() { /* not a member */}

template<const char* name, typename Method, Method method>
inline void registerMethod() { ++members; }

Expand Down Expand Up @@ -812,7 +816,13 @@ struct MembersBuilder {
m.setSpecifier(module.getName());
m.setTsDeclarations(module.getTsDeclaration());
}
}
}

template <typename Type,
typename = kj::EnableIf<std::is_assignable_v<NamedIntercept, Type>>>
inline void registerNamedIntercept() {
// Nothing to do in this case.
}
};

// true when the T has registerMembers() function generated by JSG_RESOURCE/JSG_STRUCT
Expand Down

0 comments on commit a3d7ead

Please sign in to comment.