Skip to content
File

Blob: src/workerd/api/capnp.h

cpp411 lines
1#pragma once
2 
3#include <workerd/io/io-context.h>
4#include <workerd/jsg/jsg.h>
5 
6#include <capnp/dynamic.h>
7#include <kj/map.h>
8 
9namespace workerd::api {
10 
11template <typename TypeWrapper>
12class CapnpTypeWrapper;
13class CapnpCapability;
14class CapnpTypeWrapperBase;
15 
16void fillCapnpFieldFromJs(capnp::DynamicStruct::Builder builder,
17 capnp::StructSchema::Field field,
18 v8::Local<v8::Context> context,
19 v8::Local<v8::Value> jsValue);
20 
21capnp::Orphan<capnp::DynamicValue> capnpValueFromJs(
22 jsg::Lock& js, capnp::Orphanage orphanage, capnp::Type type, v8::Local<v8::Value> jsValue);
23 
24class CapnpServer final: public capnp::DynamicCapability::Server {
25 public:
26 CapnpServer(jsg::Lock& js,
27 capnp::InterfaceSchema schema,
28 jsg::V8Ref<v8::Object> object,
29 CapnpTypeWrapperBase& wrapper);
30 ~CapnpServer() noexcept(false);
31 
32 kj::Promise<void> call(capnp::InterfaceSchema::Method method,
33 capnp::CallContext<capnp::DynamicStruct, capnp::DynamicStruct> context) override;
34 
35 private:
36 kj::Own<IoContext::WeakRef> ioContext;
37 jsg::V8Ref<v8::Object> object;
38 kj::Maybe<jsg::V8Ref<v8::Function>> closeMethod;
39 CapnpTypeWrapperBase& wrapper; // only valid if isolate is locked!
40 
41 kj::Maybe<jsg::V8Ref<v8::Function>> getCloseMethod(jsg::Lock& js);
42 
43 friend class CapnpCapability;
44};
45 
46class CapnpCapability: public jsg::Object {
47 public:
48 CapnpCapability(capnp::DynamicCapability::Client client);
49 ~CapnpCapability() noexcept(false);
50 
51 v8::Local<v8::Value> call(jsg::Lock& js,
52 capnp::InterfaceSchema::Method method,
53 v8::Local<v8::Value> params,
54 CapnpTypeWrapperBase& wrapper);
55 
56 void close();
57 jsg::Promise<kj::Maybe<jsg::V8Ref<v8::Object>>> unwrap(jsg::Lock& js);
58 
59 JSG_RESOURCE_TYPE(CapnpCapability) {
60 JSG_METHOD(close);
61 JSG_METHOD(unwrap);
62 }
63 
64 capnp::DynamicCapability::Client getClient(jsg::Lock& js, CapnpTypeWrapperBase& wrapper);
65 
66 private:
67 // Used for error messages.
68 capnp::InterfaceSchema schema;
69 
70 // null if closed
71 kj::Maybe<IoOwn<capnp::DynamicCapability::Client>> client;
72 
73 template <typename TypeWrapper>
74 friend class CapnpTypeWrapper;
75};
76 
77class CapnpTypeWrapperBase {
78 public:
79 virtual v8::Local<v8::Object> wrapCap(jsg::Lock& js,
80 v8::Local<v8::Context> context,
81 capnp::DynamicCapability::Client value,
82 kj::Maybe<jsg::Ref<CapnpCapability>&> refToInitialize = kj::none) = 0;
83 virtual kj::Maybe<capnp::DynamicCapability::Client> tryUnwrapCap(
84 jsg::Lock& js, v8::Local<v8::Context> context, v8::Local<v8::Value> value) = 0;
85 
86 virtual v8::Local<v8::Promise> wrapPromise(jsg::Lock& js,
87 v8::Local<v8::Context> context,
88 kj::Maybe<v8::Local<v8::Object>> creator,
89 jsg::Promise<jsg::Value> value) = 0;
90 virtual kj::Maybe<jsg::Promise<jsg::Value>> tryUnwrapPromise(
91 jsg::Lock& js, v8::Local<v8::Context> context, v8::Local<v8::Value> value) = 0;
92};
93 
94template <typename TypeWrapper>
95class CapnpTypeWrapper: private CapnpTypeWrapperBase {
96 public:
97 static constexpr const char* getName(capnp::DynamicCapability::Client*) {
98 return "Capability";
99 }
100 
101 v8::Local<v8::Function> wrap(jsg::Lock& js,
102 v8::Local<v8::Context> context,
103 kj::Maybe<v8::Local<v8::Object>> creator,
104 capnp::Schema schema) {
105 auto tmpl = getCapnpTemplate(js, schema);
106 return jsg::check(tmpl->GetFunction(context));
107 }
108 
109 v8::Local<v8::Object> wrap(jsg::Lock& js,
110 v8::Local<v8::Context> context,
111 kj::Maybe<v8::Local<v8::Object>> creator,
112 capnp::DynamicCapability::Client client,
113 kj::Maybe<jsg::Ref<CapnpCapability>&> refToInitialize = kj::none) {
114 auto tmpl = getCapnpTemplate(js, client.getSchema());
115 auto obj = jsg::check(tmpl->InstanceTemplate()->NewInstance(context));
116 auto ref = js.alloc<CapnpCapability>(kj::mv(client));
117 ref.attachWrapper(js.v8Isolate, obj);
118 KJ_IF_SOME(r, refToInitialize) {
119 r = kj::mv(ref);
120 }
121 return obj;
122 }
123 
124 // Wrap a specific compiled-in interface. This lets you use MyType::Client as a return type
125 // in a JSG method.
126 template <typename Client, typename = capnp::FromClient<Client>>
127 v8::Local<v8::Object> wrap(jsg::Lock& js,
128 v8::Local<v8::Context> context,
129 kj::Maybe<v8::Local<v8::Object>> creator,
130 Client client) {
131 return wrap(js, context, creator, capnp::toDynamic(kj::mv(client)));
132 }
133 
134 kj::Maybe<capnp::DynamicCapability::Client> tryUnwrap(jsg::Lock& js,
135 v8::Local<v8::Context> context,
136 v8::Local<v8::Value> handle,
137 capnp::DynamicCapability::Client*,
138 kj::Maybe<v8::Local<v8::Object>> parentObject) {
139 auto& wrapper = static_cast<TypeWrapper&>(*this);
140 KJ_IF_SOME(obj,
141 wrapper.tryUnwrap(js, context, handle, (CapnpCapability*)nullptr, parentObject)) {
142 return obj.getClient(js, *this);
143 } else {
144 // Since we don't know the schema, we cannot accept an arbitrary object.
145 return kj::none;
146 }
147 }
148 
149 // Unwrap a specific compiled-in interface. This lets you use MyType::Client as a parameter
150 // in a JSG method.
151 template <typename Client, typename Interface = capnp::FromClient<Client>>
152 kj::Maybe<Client> tryUnwrap(jsg::Lock& js,
153 v8::Local<v8::Context> context,
154 v8::Local<v8::Value> handle,
155 Client*,
156 kj::Maybe<v8::Local<v8::Object>> parentObject) {
157 auto expectedSchema = capnp::Schema::from<Interface>();
158 
159 auto& wrapper = static_cast<TypeWrapper&>(*this);
160 KJ_IF_SOME(obj,
161 wrapper.tryUnwrap(js, context, handle, (CapnpCapability*)nullptr, parentObject)) {
162 capnp::DynamicCapability::Client dynamic = obj.getClient(js.v8Isolate, *this);
163 if (dynamic.getSchema().extends(expectedSchema)) {
164 return dynamic.as<Interface>();
165 } else {
166 // Incompatible interfaces.
167 return kj::none;
168 }
169 } else if (handle->IsObject()) {
170 // Treat object as a server implementation.
171 auto isolate = js.v8Isolate;
172 CapnpTypeWrapperBase& wrapper = TypeWrapper::from(isolate);
173 capnp::DynamicCapability::Client dynamic = IoContext::current().getLocalCapSet().add(
174 kj::heap<CapnpServer>(expectedSchema, handle.As<v8::Object>(), wrapper, isolate));
175 return dynamic.as<Interface>();
176 } else {
177 return kj::none;
178 }
179 }
180 
181 // Not relevant for us but we must define a method with this name to satisfy TypeWrapperExtension.
182 void newContext() = delete;
183 
184 template <bool isContext = false, typename Client, typename Interface = capnp::FromClient<Client>>
185 v8::Local<v8::FunctionTemplate> getTemplate(jsg::Lock& js, Client*) {
186 static_assert(!isContext);
187 return getCapnpTemplate(js, capnp::Schema::from<Interface>());
188 }
189 
190 v8::Local<v8::FunctionTemplate> getCapnpTemplate(jsg::Lock& js, capnp::Schema schema) {
191 using Ret = decltype(typeConstructors)::Entry;
192 return typeConstructors
193 .findOrCreate(schema, [&]() -> Ret {
194 return js.withinHandleScope([&]() -> Ret {
195 auto handle = makeConstructor(js, schema);
196 return {schema, {js.v8Isolate, handle}};
197 });
198 }).Get(js.v8Isolate);
199 }
200 
201 private:
202 kj::HashMap<capnp::Schema, v8::Global<v8::FunctionTemplate>> typeConstructors;
203 
204 // Each method callback we create needs to pack the method schema into a v8::External. But
205 // v8::External can only store a pointer, and InterfaceSchema::Method is larger than a pointer.
206 // So we need to allocate copies of all the `Method` objects somewhere where they'll live until
207 // the isolate shuts down.
208 kj::HashMap<capnp::InterfaceSchema, kj::Array<capnp::InterfaceSchema::Method>> methodSchemas;
209 
210 v8::Local<v8::FunctionTemplate> makeConstructor(jsg::Lock& js, capnp::Schema schema) {
211 return js.withinHandleScope([&]() -> v8::Local<v8::FunctionTemplate> {
212 // HACK: We happen to know that `Schema` is just a pointer internally, and is
213 // trivially copyable and destructible. So, we can safely stuff it directly into a
214 // v8::External by value, avoiding extra allocations.
215 static_assert(sizeof(schema) == sizeof(void*));
216 static_assert(__is_trivially_copyable(capnp::Schema));
217 static_assert(__is_trivially_destructible(capnp::Schema));
218 void* schemaAsPtr;
219 memcpy(&schemaAsPtr, &schema, sizeof(schema));
220 
221 auto constructor = v8::FunctionTemplate::New(js.v8Isolate, &constructorCallback,
222 v8::External::New(js.v8Isolate, schemaAsPtr, v8::kExternalPointerTypeTagDefault));
223 
224 auto prototype = constructor->PrototypeTemplate();
225 auto signature = v8::Signature::New(js.v8Isolate, constructor);
226 
227 auto instance = constructor->InstanceTemplate();
228 
229 constructor->SetClassName(jsg::v8StrIntern(js.v8Isolate, schema.getShortDisplayName()));
230 
231 auto& wrapper = static_cast<TypeWrapper&>(*this);
232 
233 auto proto = schema.getProto();
234 switch (proto.which()) {
235 case capnp::schema::Node::FILE:
236 case capnp::schema::Node::STRUCT:
237 case capnp::schema::Node::ENUM:
238 case capnp::schema::Node::CONST:
239 case capnp::schema::Node::ANNOTATION:
240 // TODO(someday): Support non-interface types.
241 break;
242 
243 case capnp::schema::Node::INTERFACE: {
244 // As explained in ResourceWrapper, we must have 2 internal fields, where the first one is
245 // the GC visitation callback.
246 instance->SetInternalFieldCount(jsg::Wrappable::INTERNAL_FIELD_COUNT);
247 
248 constructor->Inherit(
249 wrapper.getTemplate(js.v8Isolate, static_cast<CapnpCapability*>(nullptr)));
250 kj::HashSet<uint64_t> seen;
251 addAllMethods(js, prototype, signature, schema.asInterface(), seen);
252 break;
253 }
254 }
255 
256 for (auto nested: proto.getNestedNodes()) {
257 KJ_IF_SOME(child,
258 js.getCapnpSchemaLoader<api::ServiceWorkerGlobalScope>().tryGet(nested.getId())) {
259 switch (child.getProto().which()) {
260 case capnp::schema::Node::FILE:
261 case capnp::schema::Node::STRUCT:
262 case capnp::schema::Node::INTERFACE:
263 constructor->Set(
264 jsg::v8StrIntern(js.v8Isolate, nested.getName()), makeConstructor(js, child));
265 break;
266 
267 case capnp::schema::Node::ENUM:
268 case capnp::schema::Node::CONST:
269 case capnp::schema::Node::ANNOTATION:
270 // These kinds are not implemented and cannot contain further nested scopes, so don't
271 // generate anything at all for now.
272 break;
273 }
274 }
275 }
276 
277 return constructor;
278 });
279 }
280 
281 // Add all methods to the capability prototype. Since JavaScript doesn't support multiple
282 // inheritance, we need to flatten all inherited methods into each interface.
283 //
284 // `seen` is a set of type IDs that we've visited already, so that diamond inheritance doesn't
285 // lead to us double-registering methods.
286 void addAllMethods(jsg::Lock& js,
287 v8::Local<v8::ObjectTemplate> prototype,
288 v8::Local<v8::Signature> signature,
289 capnp::InterfaceSchema schema,
290 kj::HashSet<uint64_t>& seen) {
291 
292 JSG_REQUIRE(seen.size() < 64, TypeError,
293 "Interface inherits too many types: ", schema.getProto().getDisplayName());
294 
295 // Reverse-iterate so that in case of duplicate method names, the method from the first class
296 // in the list takes precedence.
297 auto supers = schema.getSuperclasses();
298 for (auto i = supers.size(); i > 0; i--) {
299 auto super = supers[i - 1];
300 
301 // Check if this superclass is in the `seen` set. As a slight optimization we only check this
302 // before visiting a superclass, so that for a regular interface that doesn't inherit
303 // anything, we never allocate the `seen` set. This assumes that inheritance is not cyclic.
304 // Technically it's possible to declare cyclic inheritance (maliciously, perhaps), but in
305 // that case we'll just redundantly create the methods for one type, which is not a big deal.
306 uint64_t id = super.getProto().getId();
307 bool isNew = false;
308 seen.findOrCreate(id, [&]() {
309 isNew = true;
310 return id;
311 });
312 if (isNew) {
313 addAllMethods(js, prototype, signature, super, seen);
314 }
315 }
316 
317 kj::ArrayPtr<capnp::InterfaceSchema::Method> methods =
318 methodSchemas.findOrCreate(schema, [&]() -> decltype(methodSchemas)::Entry {
319 return {schema, KJ_MAP(m, schema.getMethods()) { return m; }};
320 });
321 
322 for (auto& method: methods) {
323 auto name = jsg::v8StrIntern(js.v8Isolate, method.getProto().getName());
324 prototype->Set(name,
325 v8::FunctionTemplate::New(js.v8Isolate, &methodCallback,
326 v8::External::New(js.v8Isolate, &method, v8::kExternalPointerTypeTagDefault),
327 signature, 0, v8::ConstructorBehavior::kThrow));
328 }
329 }
330 
331 static void constructorCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
332 jsg::liftKj(args, [&]() {
333 auto data = args.Data();
334 KJ_ASSERT(data->IsExternal());
335 void* schemaAsPtr = data.As<v8::External>()->Value(v8::kExternalPointerTypeTagDefault);
336 capnp::Schema schema;
337 memcpy(&schema, &schemaAsPtr, sizeof(schema));
338 
339 JSG_REQUIRE(args.IsConstructCall(), TypeError, "Failed to construct '",
340 schema.getShortDisplayName(),
341 "': Please use the "
342 "'new' operator, this object constructor cannot be called as a function.");
343 
344 auto& js = jsg::Lock::from(args.GetIsolate());
345 auto obj = args.This();
346 KJ_ASSERT(obj->InternalFieldCount() == jsg::Wrappable::INTERNAL_FIELD_COUNT);
347 
348 auto arg = args[0];
349 JSG_REQUIRE(arg->IsObject(), TypeError, "Constructor argument for '",
350 schema.getShortDisplayName(),
351 "' must be an object "
352 "implementing the interface.");
353 
354 CapnpTypeWrapperBase& wrapper = TypeWrapper::from(js.v8Isolate);
355 capnp::DynamicCapability::Client client =
356 IoContext::current().getLocalCapSet().add(kj::heap<CapnpServer>(js, schema.asInterface(),
357 jsg::V8Ref<v8::Object>(js.v8Isolate, arg.As<v8::Object>()), wrapper));
358 auto ptr = js.alloc<CapnpCapability>(kj::mv(client));
359 
360 ptr.attachWrapper(js.v8Isolate, obj);
361 });
362 }
363 
364 static void methodCallback(const v8::FunctionCallbackInfo<v8::Value>& args) {
365 jsg::liftKj(args, [&]() {
366 auto data = args.Data();
367 KJ_ASSERT(data->IsExternal());
368 auto& method = *reinterpret_cast<capnp::InterfaceSchema::Method*>(
369 data.As<v8::External>()->Value(v8::kExternalPointerTypeTagDefault));
370 
371 auto& js = jsg::Lock::from(args.GetIsolate());
372 auto obj = args.This();
373 auto& wrapper = TypeWrapper::from(js.v8Isolate);
374 auto& self = jsg::extractInternalPointer<CapnpCapability, false>(js.v8Context(), obj);
375 
376 return wrapper.wrap(js, js.v8Context(), obj, self.call(js, method, args[0], wrapper));
377 });
378 }
379 
380 v8::Local<v8::Object> wrapCap(jsg::Lock& js,
381 v8::Local<v8::Context> context,
382 capnp::DynamicCapability::Client value,
383 kj::Maybe<jsg::Ref<CapnpCapability>&> refToInitialize) override {
384 return wrap(js, context, kj::none, kj::mv(value), refToInitialize);
385 }
386 kj::Maybe<capnp::DynamicCapability::Client> tryUnwrapCap(
387 jsg::Lock& js, v8::Local<v8::Context> context, v8::Local<v8::Value> value) override {
388 return tryUnwrap(
389 js, context, value, static_cast<capnp::DynamicCapability::Client*>(nullptr), kj::none);
390 }
391 
392 v8::Local<v8::Promise> wrapPromise(jsg::Lock& js,
393 v8::Local<v8::Context> context,
394 kj::Maybe<v8::Local<v8::Object>> creator,
395 jsg::Promise<jsg::Value> value) override {
396 return static_cast<TypeWrapper&>(*this).wrap(js, context, creator, kj::mv(value));
397 }
398 kj::Maybe<jsg::Promise<jsg::Value>> tryUnwrapPromise(
399 jsg::Lock& js, v8::Local<v8::Context> context, v8::Local<v8::Value> value) override {
400 return static_cast<TypeWrapper&>(*this).tryUnwrap(
401 js, context, value, static_cast<jsg::Promise<jsg::Value>*>(nullptr), kj::none);
402 }
403};
404 
405#define EW_CAPNP_TYPES \
406 ::workerd::api::CapnpCapability, \
407 ::workerd::jsg::TypeWrapperExtension<::workerd::api::CapnpTypeWrapper>
408// The list of capnp.h types that are added to worker.c++'s JSG_DECLARE_ISOLATE_TYPE
409 
410} // namespace workerd::api