Skip to content

Commit

Permalink
Enable using context-specific type wrapper instances.
Browse files Browse the repository at this point in the history
Sometimes we'd like to have multiple contexts with different
typewrapper that were instantiated with different configuration objects.
To do that this commit extends the Isolate class, wrapper is now a
vector of wrappers, the 0 indexed wrapper is the default wrapper but
additional wrappers can be created with `instantiateWrapper` which
receives a new Configuration object. To associate a context with its
wrapper we use `context->SetAlignedPointerInEmbedderData(3, wrapper)`.
  • Loading branch information
danlapid committed Dec 7, 2024
1 parent dfc74ca commit 32191ee
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 35 deletions.
8 changes: 8 additions & 0 deletions src/workerd/io/worker.c++
Original file line number Diff line number Diff line change
Expand Up @@ -1279,6 +1279,10 @@ Worker::Script::Script(kj::Own<const Isolate> isolateParam,
// (Undocumented, as usual.)
context =
v8::Context::New(lock.v8Isolate, nullptr, v8::ObjectTemplate::New(lock.v8Isolate));
// We need to set the highest used index in every context we create to be a nullptr
// This is because we might later on call GetAlignedPointerFromEmbedderData which fails with
// a fatal error if the array is smaller than the given index.
context->SetAlignedPointerInEmbedderData(3, nullptr);
}

JSG_WITHIN_CONTEXT_SCOPE(lock, context, [&](jsg::Lock& js) {
Expand Down Expand Up @@ -2594,6 +2598,10 @@ class Worker::Isolate::InspectorChannelImpl final: public v8_inspector::V8Inspec
// We don't know which contexts exist in this isolate, so I guess we have to
// create one. Ugh.
auto dummyContext = v8::Context::New(lock->v8Isolate);
// We need to set the highest used index in every context we create to be a nullptr
// This is because we might later on call GetAlignedPointerFromEmbedderData which fails with
// a fatal error if the array is smaller than the given index.
dummyContext->SetAlignedPointerInEmbedderData(3, nullptr);
auto& inspector = *KJ_ASSERT_NONNULL(isolate.impl->inspector);
inspector.contextCreated(v8_inspector::V8ContextInfo(dummyContext, 1,
v8_inspector::StringView(reinterpret_cast<const uint8_t*>("Worker"), 6)));
Expand Down
4 changes: 4 additions & 0 deletions src/workerd/jsg/resource.h
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,10 @@ class ResourceWrapper {

// Store a pointer to this object in slot 1, to be extracted in callbacks.
context->SetAlignedPointerInEmbedderData(1, ptr.get());
// We need to set the highest used index in every context we create to be a nullptr
// This is because we might later on call GetAlignedPointerFromEmbedderData which fails with
// a fatal error if the array is smaller than the given index.
context->SetAlignedPointerInEmbedderData(3, nullptr);

// (Note: V8 docs say: "Note that index 0 currently has a special meaning for Chrome's
// debugger." We aren't Chrome, but it does appear that some versions of V8 will mess with
Expand Down
4 changes: 2 additions & 2 deletions src/workerd/jsg/setup.c++
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ v8::Local<v8::FunctionTemplate> IsolateBase::getOpaqueTemplate(v8::Isolate* isol
->opaqueTemplate.Get(isolate);
}

void IsolateBase::dropWrappers(kj::Own<void> typeWrapperInstance) {
void IsolateBase::dropWrappers(kj::FunctionParam<void()> drop) {
// Delete all wrappers.
jsg::runInV8Stack([&](jsg::V8StackScope& stackScope) {
v8::Locker lock(ptr);
Expand All @@ -425,7 +425,7 @@ void IsolateBase::dropWrappers(kj::Own<void> typeWrapperInstance) {

// Make sure the TypeWrapper is destroyed under lock by declaring a new copy of the variable
// that is destroyed before the lock is released.
kj::Own<void> typeWrapperInstanceInner = kj::mv(typeWrapperInstance);
drop();

// Destroy all wrappers.
heapTracer.clearWrappers();
Expand Down
134 changes: 101 additions & 33 deletions src/workerd/jsg/setup.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

#include <kj/map.h>
#include <kj/mutex.h>
#include <kj/vector.h>

namespace workerd::jsg {

Expand Down Expand Up @@ -295,7 +296,7 @@ class IsolateBase {
~IsolateBase() noexcept(false);
KJ_DISALLOW_COPY_AND_MOVE(IsolateBase);

void dropWrappers(kj::Own<void> typeWrapperInstance);
void dropWrappers(kj::FunctionParam<void()> drop);

bool getCaptureThrowsAsRejections() const {
return captureThrowsAsRejections;
Expand Down Expand Up @@ -406,14 +407,20 @@ class Isolate: public IsolateBase {
// If different wrappers use different types, then `MetaConfiguration` should be some value that
// inherits or defines conversion operators to each required type -- or the individual
// configuration types must declare constructors from `MetaConfiguration`.
// If `instantiateTypeWrapper` is false, then the default wrapper will not be instantiated
// and should be instantiated with `instantiateTypeWrapper` before `newContext` is called on
// a jsg::Lock of this Isolate.
template <typename MetaConfiguration>
explicit Isolate(const V8System& system,
MetaConfiguration&& configuration,
kj::Own<IsolateObserver> observer,
v8::Isolate::CreateParams createParams = {})
: IsolateBase(system, kj::mv(createParams), kj::mv(observer)),
wrapper(wrapperSpace.construct(ptr, kj::fwd<MetaConfiguration>(configuration))) {
wrapper->initTypeWrapper();
v8::Isolate::CreateParams createParams = {},
bool instantiateTypeWrapper = true)
: IsolateBase(system, kj::mv(createParams), kj::mv(observer)) {
wrappers.resize(1);
if (instantiateTypeWrapper) {
instantiateDefaultWrapper(kj::fwd<MetaConfiguration>(configuration));
}
}

// Use this constructor when no wrappers have any required configuration.
Expand All @@ -422,23 +429,33 @@ class Isolate: public IsolateBase {
v8::Isolate::CreateParams createParams = {})
: Isolate(system, nullptr, kj::mv(observer), kj::mv(createParams)) {}

template <typename MetaConfiguration>
void instantiateDefaultWrapper(MetaConfiguration&& configuration) {
KJ_DASSERT(wrappers[0].get() == nullptr);
auto wrapper = wrapperSpace.construct(ptr, kj::fwd<MetaConfiguration>(configuration));
wrapper->initTypeWrapper();
wrappers[0] = kj::mv(wrapper);
}

~Isolate() noexcept(false) {
dropWrappers(kj::mv(wrapper));
dropWrappers([this]() { wrappers.clear(); });
}

public:
kj::Exception unwrapException(
v8::Local<v8::Context> context, v8::Local<v8::Value> exception) override {
return wrapper->template unwrap<kj::Exception>(
return getWrapperByContext(context)->template unwrap<kj::Exception>(
context, exception, jsg::TypeErrorContext::other());
}

v8::Local<v8::Value> wrapException(
v8::Local<v8::Context> context, kj::Exception&& exception) override {
return wrapper->wrap(context, kj::none, kj::fwd<kj::Exception>(exception));
return getWrapperByContext(context)->wrap(context, kj::none, kj::fwd<kj::Exception>(exception));
}

bool serialize(
Lock& js, std::type_index type, jsg::Object& instance, Serializer& serializer) override {
auto* wrapper = getWrapperByContext(js);
KJ_IF_SOME(func, wrapper->serializerMap.find(type)) {
func(*wrapper, js, instance, serializer);
return true;
Expand All @@ -448,6 +465,7 @@ class Isolate: public IsolateBase {
}
kj::Maybe<v8::Local<v8::Object>> deserialize(
Lock& js, uint tag, Deserializer& deserializer) override {
auto* wrapper = getWrapperByContext(js);
KJ_IF_SOME(func, wrapper->deserializerMap.find(tag)) {
return func(*wrapper, js, tag, deserializer);
} else {
Expand Down Expand Up @@ -483,29 +501,29 @@ class Isolate: public IsolateBase {
// Wrap a C++ value, returning a v8::Local (possibly of a specific type).
template <typename T>
auto wrap(v8::Local<v8::Context> context, T&& value) {
return jsgIsolate.wrapper->wrap(context, kj::none, kj::fwd<T>(value));
return jsgIsolate.getWrapperByContext(context)->wrap(context, kj::none, kj::fwd<T>(value));
}

// Wrap a context-independent value. Only a few built-in types, like numbers and strings,
// can be wrapped without a context.
template <typename T>
auto wrapNoContext(T&& value) {
return jsgIsolate.wrapper->wrap(v8Isolate, kj::none, kj::fwd<T>(value));
return jsgIsolate.getWrapperByContext(*this)->wrap(v8Isolate, kj::none, kj::fwd<T>(value));
}

// Convert a JavaScript value to a C++ value, or throw a JS exception if the type doesn't
// match.
template <typename T>
auto unwrap(v8::Local<v8::Context> context, v8::Local<v8::Value> handle) {
return jsgIsolate.wrapper->template unwrap<T>(
return jsgIsolate.getWrapperByContext(context)->template unwrap<T>(
context, handle, jsg::TypeErrorContext::other());
}

Ref<DOMException> domException(
kj::String name, kj::String message, kj::Maybe<kj::String> maybeStack) override {
return withinHandleScope([&] {
v8::Local<v8::FunctionTemplate> tmpl =
jsgIsolate.wrapper->getTemplate(v8Isolate, static_cast<DOMException*>(nullptr));
v8::Local<v8::FunctionTemplate> tmpl = jsgIsolate.getWrapperByContext(*this)->getTemplate(
v8Isolate, static_cast<DOMException*>(nullptr));
KJ_DASSERT(!tmpl.IsEmpty());
v8::Local<v8::Object> obj = check(tmpl->InstanceTemplate()->NewInstance(v8Context()));
v8::Local<v8::String> stackName = str("stack"_kjc);
Expand Down Expand Up @@ -541,59 +559,75 @@ class Isolate: public IsolateBase {
template <typename T>
jsg::JsObject getConstructor(v8::Local<v8::Context> context) {
v8::EscapableHandleScope scope(v8Isolate);
v8::Local<v8::FunctionTemplate> tpl = jsgIsolate.wrapper->getTemplate(v8Isolate, (T*)nullptr);
v8::Local<v8::FunctionTemplate> tpl =
jsgIsolate.getWrapperByContext(context)->getTemplate(v8Isolate, (T*)nullptr);
v8::Local<v8::Object> prototype = check(tpl->GetFunction(context));
return jsg::JsObject(scope.Escape(prototype));
}

v8::Local<v8::ArrayBuffer> wrapBytes(kj::Array<byte> data) override {
return jsgIsolate.wrapper->wrap(v8Isolate, kj::none, kj::mv(data));
return jsgIsolate.getWrapperByContext(*this)->wrap(v8Isolate, kj::none, kj::mv(data));
}
v8::Local<v8::Function> wrapSimpleFunction(v8::Local<v8::Context> context,
jsg::Function<void(const v8::FunctionCallbackInfo<v8::Value>& info)> simpleFunction)
override {
return jsgIsolate.wrapper->wrap(context, kj::none, kj::mv(simpleFunction));
return jsgIsolate.getWrapperByContext(context)->wrap(
context, kj::none, kj::mv(simpleFunction));
}
v8::Local<v8::Function> wrapReturningFunction(v8::Local<v8::Context> context,
jsg::Function<v8::Local<v8::Value>(const v8::FunctionCallbackInfo<v8::Value>& info)>
returningFunction) override {
return jsgIsolate.wrapper->wrap(context, kj::none, kj::mv(returningFunction));
return jsgIsolate.getWrapperByContext(context)->wrap(
context, kj::none, kj::mv(returningFunction));
}
v8::Local<v8::Function> wrapPromiseReturningFunction(v8::Local<v8::Context> context,
jsg::Function<jsg::Promise<jsg::Value>(const v8::FunctionCallbackInfo<v8::Value>& info)>
returningFunction) override {
return jsgIsolate.wrapper->wrap(context, kj::none, kj::mv(returningFunction));
return jsgIsolate.getWrapperByContext(context)->wrap(
context, kj::none, kj::mv(returningFunction));
}
kj::String toString(v8::Local<v8::Value> value) override {
return jsgIsolate.wrapper->template unwrap<kj::String>(
return jsgIsolate.getWrapperByContext(*this)->template unwrap<kj::String>(
v8Isolate->GetCurrentContext(), value, jsg::TypeErrorContext::other());
}
jsg::Dict<v8::Local<v8::Value>> toDict(v8::Local<v8::Value> value) override {
return jsgIsolate.wrapper->template unwrap<jsg::Dict<v8::Local<v8::Value>>>(
v8Isolate->GetCurrentContext(), value, jsg::TypeErrorContext::other());
return jsgIsolate.getWrapperByContext(*this)
->template unwrap<jsg::Dict<v8::Local<v8::Value>>>(
v8Isolate->GetCurrentContext(), value, jsg::TypeErrorContext::other());
}
jsg::Dict<jsg::JsValue> toDict(const jsg::JsValue& value) override {
return jsgIsolate.wrapper->template unwrap<jsg::Dict<jsg::JsValue>>(
return jsgIsolate.getWrapperByContext(*this)->template unwrap<jsg::Dict<jsg::JsValue>>(
v8Isolate->GetCurrentContext(), value, jsg::TypeErrorContext::other());
}
v8::Local<v8::Promise> wrapSimplePromise(jsg::Promise<jsg::Value> promise) override {
return jsgIsolate.wrapper->wrap(v8Context(), kj::none, kj::mv(promise));
return jsgIsolate.getWrapperByContext(*this)->wrap(v8Context(), kj::none, kj::mv(promise));
}
jsg::Promise<jsg::Value> toPromise(v8::Local<v8::Value> promise) override {
return jsgIsolate.wrapper->template unwrap<jsg::Promise<jsg::Value>>(
return jsgIsolate.getWrapperByContext(*this)->template unwrap<jsg::Promise<jsg::Value>>(
v8Isolate->GetCurrentContext(), promise, jsg::TypeErrorContext::other());
}

// Creates a new JavaScript "context", i.e. the global object. This is the first step to
// executing JavaScript code. T should be one of your API types which you want to use as the
// global object. `args...` are passed to the type's constructor.
template <typename T, typename... Args>
JsContext<T> newContext(NewContextOptions options, Args&&... args) {
JsContext<T> newContextWithWrapper(
TypeWrapper* wrapper, NewContextOptions options, Args&&... args) {
// TODO(soon): Requiring move semantics for the global object is awkward. This should instead
// allocate the object (forwarding arguments to the constructor) and return something like
// a Ref.
return jsgIsolate.wrapper->newContext(*this, options, jsgIsolate.getObserver(),
auto context = wrapper->newContext(*this, options, jsgIsolate.getObserver(),
static_cast<T*>(nullptr), kj::fwd<Args>(args)...);
context.getHandle(v8Isolate)->SetAlignedPointerInEmbedderData(3, wrapper);
return context;
}

// Creates a new JavaScript "context", i.e. the global object. This is the first step to
// executing JavaScript code. T should be one of your API types which you want to use as the
// global object. `args...` are passed to the type's constructor.
template <typename T, typename... Args>
JsContext<T> newContext(NewContextOptions options, Args&&... args) {
KJ_DASSERT(!jsgIsolate.wrappers.empty());
KJ_DASSERT(jsgIsolate.wrappers[0].get() != nullptr);
return newContextWithWrapper<T>(
jsgIsolate.wrappers[0].get(), options, kj::fwd<Args>(args)...);
}

// Creates a new JavaScript "context", i.e. the global object. This is the first step to
Expand All @@ -604,9 +638,18 @@ class Isolate: public IsolateBase {
return newContext<T>(NewContextOptions{}, kj::fwd<Args>(args)...);
}

template <typename T, typename MetaConfiguration, typename... Args>
JsContext<T> newContextWithConfiguration(
MetaConfiguration&& configuration, NewContextOptions options, Args&&... args) {
jsgIsolate.hasExtraWrappers = true;
auto& wrapper = jsgIsolate.wrappers.add(
kj::heap<TypeWrapper>(jsgIsolate.ptr, kj::fwd<MetaConfiguration>(configuration)));
return newContextWithWrapper<T>(wrapper.get(), options, kj::fwd<Args>(args)...);
}

void reportError(const JsValue& value) override {
KJ_IF_SOME(domException,
jsgIsolate.wrapper->tryUnwrap(
jsgIsolate.getWrapperByContext(*this)->tryUnwrap(
v8Context(), value, static_cast<DOMException*>(nullptr), kj::none)) {
auto desc =
kj::str("DOMException(", domException.getName(), "): ", domException.getMessage());
Expand All @@ -623,7 +666,7 @@ class Isolate: public IsolateBase {
virtual kj::Maybe<Object&> getInstance(
v8::Local<v8::Object> obj, const std::type_info& type) override {
auto instance = v8::Local<v8::Object>(obj)->FindInstanceInPrototypeChain(
jsgIsolate.wrapper->getDynamicTypeInfo(v8Isolate, type).tmpl);
jsgIsolate.getWrapperByContext(*this)->getDynamicTypeInfo(v8Isolate, type).tmpl);
if (instance.IsEmpty()) {
return kj::none;
} else {
Expand All @@ -634,7 +677,7 @@ class Isolate: public IsolateBase {

virtual v8::Local<v8::Object> getPrototypeFor(const std::type_info& type) override {
v8::EscapableHandleScope scope(v8Isolate);
auto tmpl = jsgIsolate.wrapper->getDynamicTypeInfo(v8Isolate, type).tmpl;
auto tmpl = jsgIsolate.getWrapperByContext(*this)->getDynamicTypeInfo(v8Isolate, type).tmpl;
auto constructor = JsObject(check(tmpl->GetFunction(v8Context())));

// Note that `constructor.getPrototype()` returns the prototype of the constructor itself,
Expand All @@ -656,9 +699,34 @@ class Isolate: public IsolateBase {
});
}

protected:
inline TypeWrapper* getWrapperByContext(jsg::Lock& js) {
if (KJ_LIKELY(!hasExtraWrappers)) {
return wrappers[0].get();
} else {
return getWrapperByContext(js.v8Context());
}
}
inline TypeWrapper* getWrapperByContext(v8::Local<v8::Context> context) {
if (KJ_LIKELY(!hasExtraWrappers)) {
return wrappers[0].get();
} else {
auto ptr = context->GetAlignedPointerFromEmbedderData(3);
if (KJ_LIKELY(ptr != nullptr)) {
return static_cast<TypeWrapper*>(ptr);
} else {
// This can happen when we create dummy contexts such as in worker.c++.
return wrappers[0].get();
}
}
}

private:
kj::SpaceFor<TypeWrapper> wrapperSpace;
kj::Own<TypeWrapper> wrapper; // Needs to be destroyed under lock...
kj::Vector<kj::Own<TypeWrapper>> wrappers; // Needs to be destroyed under lock...
// This is just an optimization boolean, when we only have one wrapper we can skip calling
// GetAlignedPointerFromEmbedderData and just return wrappers[0].
bool hasExtraWrappers = false;
};

// This macro helps cut down on template spam in error messages. Instead of instantiating Isolate
Expand Down

0 comments on commit 32191ee

Please sign in to comment.