File
Blob: src/workerd/api/capnp.h
| 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 | |
| 9 | namespace workerd::api { |
| 10 | |
| 11 | template <typename TypeWrapper> |
| 12 | class CapnpTypeWrapper; |
| 13 | class CapnpCapability; |
| 14 | class CapnpTypeWrapperBase; |
| 15 | |
| 16 | void fillCapnpFieldFromJs(capnp::DynamicStruct::Builder builder, |
| 17 | capnp::StructSchema::Field field, |
| 18 | v8::Local<v8::Context> context, |
| 19 | v8::Local<v8::Value> jsValue); |
| 20 | |
| 21 | capnp::Orphan<capnp::DynamicValue> capnpValueFromJs( |
| 22 | jsg::Lock& js, capnp::Orphanage orphanage, capnp::Type type, v8::Local<v8::Value> jsValue); |
| 23 | |
| 24 | class 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 | |
| 46 | class 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 | |
| 77 | class 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 | |
| 94 | template <typename TypeWrapper> |
| 95 | class 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 |