File
Blob: src/workerd/api/capnp.c++
| 1 | #include "capnp.h" |
| 2 | |
| 3 | namespace workerd::api { |
| 4 | |
| 5 | // ======================================================================================= |
| 6 | // Some code here is derived from node-capnp. |
| 7 | // Copyright (c) 2014-2021 Kenton Varda, Sandstorm Development Group, Inc., and contributors |
| 8 | // Licensed under the MIT License |
| 9 | |
| 10 | #define STACK_STR(js, name, handle, sizeHint) \ |
| 11 | /* Read a JavaScript string, allocating it on the stack if it's small enough. */ \ |
| 12 | char name##_buf[sizeHint]{}; \ |
| 13 | kj::Array<char> name##_heap; \ |
| 14 | kj::StringPtr name; \ |
| 15 | { \ |
| 16 | v8::Local<v8::String> v8str = jsg::check(handle->ToString(js.v8Context())); \ |
| 17 | char* ptr; \ |
| 18 | size_t len = v8str->Utf8LengthV2(js.v8Isolate); \ |
| 19 | if (len < sizeHint) { \ |
| 20 | ptr = name##_buf; \ |
| 21 | } else { \ |
| 22 | name##_heap = kj::heapArray<char>(len + 1); \ |
| 23 | ptr = name##_heap.begin(); \ |
| 24 | } \ |
| 25 | v8str->WriteUtf8V2(js.v8Isolate, ptr, len); \ |
| 26 | name = kj::StringPtr(ptr, len); \ |
| 27 | } |
| 28 | |
| 29 | // Convert JS values to/from capnp. |
| 30 | struct JsCapnpConverter { |
| 31 | kj::Maybe<CapnpTypeWrapperBase&> wrapper; |
| 32 | |
| 33 | capnp::Orphan<capnp::DynamicValue> orphanFromJs(jsg::Lock& js, |
| 34 | kj::Maybe<capnp::StructSchema::Field> field, |
| 35 | capnp::Orphanage orphanage, |
| 36 | capnp::Type type, |
| 37 | v8::Local<v8::Value> jsValue) { |
| 38 | return js.withinHandleScope([&]() -> capnp::Orphan<capnp::DynamicValue> { |
| 39 | switch (type.which()) { |
| 40 | case capnp::schema::Type::VOID: |
| 41 | if (jsValue->IsNull()) { |
| 42 | return capnp::VOID; |
| 43 | } |
| 44 | break; |
| 45 | case capnp::schema::Type::BOOL: |
| 46 | return jsValue->BooleanValue(js.v8Isolate); |
| 47 | case capnp::schema::Type::INT8: |
| 48 | return jsg::check(jsValue->Int32Value(js.v8Context())); |
| 49 | case capnp::schema::Type::INT16: |
| 50 | return jsg::check(jsValue->Int32Value(js.v8Context())); |
| 51 | case capnp::schema::Type::INT32: |
| 52 | return jsg::check(jsValue->Int32Value(js.v8Context())); |
| 53 | case capnp::schema::Type::UINT8: |
| 54 | return jsg::check(jsValue->Uint32Value(js.v8Context())); |
| 55 | case capnp::schema::Type::UINT16: |
| 56 | return jsg::check(jsValue->Uint32Value(js.v8Context())); |
| 57 | case capnp::schema::Type::UINT32: |
| 58 | return jsg::check(jsValue->Uint32Value(js.v8Context())); |
| 59 | case capnp::schema::Type::FLOAT32: |
| 60 | return jsg::check(jsValue->NumberValue(js.v8Context())); |
| 61 | case capnp::schema::Type::FLOAT64: |
| 62 | return jsg::check(jsValue->NumberValue(js.v8Context())); |
| 63 | case capnp::schema::Type::UINT64: { |
| 64 | if (jsValue->IsNumber()) { |
| 65 | // js->ToBigInt() doesn't work with Numbers. V8 bug? |
| 66 | double value = jsg::check(jsValue->NumberValue(js.v8Context())); |
| 67 | |
| 68 | // Casting a double to an integer when the double is out-of-range is UB. `0x1p64` is a |
| 69 | // C++17 hex double literal with value 2^64. We cannot use UINT64_MAX here because it is |
| 70 | // not exactly representable as a double, so casting it to double will actually change |
| 71 | // the value (rounding it up to 2^64). The compiler will rightly produce a warning about |
| 72 | // this. |
| 73 | if (value >= 0 && value < 0x1p64 && value == static_cast<uint64_t>(value)) { |
| 74 | return static_cast<uint64_t>(value); |
| 75 | } |
| 76 | } else { |
| 77 | // Let V8 decide what types can be implicitly cast to BigInt. |
| 78 | auto bi = jsg::check(jsValue->ToBigInt(js.v8Context())); |
| 79 | bool lossless; |
| 80 | uint64_t value = bi->Uint64Value(&lossless); |
| 81 | if (lossless) { |
| 82 | return value; |
| 83 | } |
| 84 | } |
| 85 | break; |
| 86 | } |
| 87 | case capnp::schema::Type::INT64: { |
| 88 | // (See comments above for UInt64 case.) |
| 89 | if (jsValue->IsNumber()) { |
| 90 | double value = jsg::check(jsValue->NumberValue(js.v8Context())); |
| 91 | if (value >= -0x1p63 && value < 0x1p63 && value == static_cast<uint64_t>(value)) { |
| 92 | return static_cast<uint64_t>(value); |
| 93 | } |
| 94 | } else { |
| 95 | auto bi = jsg::check(jsValue->ToBigInt(js.v8Context())); |
| 96 | bool lossless; |
| 97 | int64_t value = bi->Int64Value(&lossless); |
| 98 | if (lossless) { |
| 99 | return value; |
| 100 | } |
| 101 | } |
| 102 | break; |
| 103 | } |
| 104 | case capnp::schema::Type::TEXT: { |
| 105 | auto str = jsg::check(jsValue->ToString(js.v8Context())); |
| 106 | capnp::Orphan<capnp::Text> orphan = |
| 107 | orphanage.newOrphan<capnp::Text>(str->Utf8LengthV2(js.v8Isolate)); |
| 108 | str->WriteUtf8V2(js.v8Isolate, orphan.get().begin(), orphan.get().size()); |
| 109 | return kj::mv(orphan); |
| 110 | } |
| 111 | case capnp::schema::Type::DATA: |
| 112 | if (jsValue->IsArrayBuffer()) { |
| 113 | auto backing = jsValue.As<v8::ArrayBuffer>()->GetBackingStore(); |
| 114 | return orphanage.newOrphanCopy(capnp::Data::Reader(kj::arrayPtr( |
| 115 | reinterpret_cast<const kj::byte*>(backing->Data()), backing->ByteLength()))); |
| 116 | } else if (jsValue->IsArrayBufferView()) { |
| 117 | auto arrayBufferView = jsValue.As<v8::ArrayBufferView>(); |
| 118 | auto backing = arrayBufferView->Buffer()->GetBackingStore(); |
| 119 | kj::ArrayPtr buffer(static_cast<kj::byte*>(backing->Data()), backing->ByteLength()); |
| 120 | auto sliceStart = arrayBufferView->ByteOffset(); |
| 121 | auto sliceEnd = sliceStart + arrayBufferView->ByteLength(); |
| 122 | KJ_ASSERT(buffer.size() >= sliceEnd); |
| 123 | return orphanage.newOrphanCopy(capnp::Data::Reader(buffer.slice(sliceStart, sliceEnd))); |
| 124 | } |
| 125 | break; |
| 126 | case capnp::schema::Type::LIST: { |
| 127 | if (jsValue->IsArray()) { |
| 128 | auto jsArray = jsValue.As<v8::Array>(); |
| 129 | auto schema = type.asList(); |
| 130 | auto elementType = schema.getElementType(); |
| 131 | auto orphan = orphanage.newOrphan(schema, jsArray->Length()); |
| 132 | auto builder = orphan.get(); |
| 133 | if (elementType.isStruct()) { |
| 134 | // Struct lists can't adopt. |
| 135 | bool error = false; |
| 136 | for (uint i: kj::indices(builder)) { |
| 137 | auto element = jsg::check(jsArray->Get(js.v8Context(), i)); |
| 138 | if (element->IsObject()) { |
| 139 | structFromJs(js, builder[i].as<capnp::DynamicStruct>(), element.As<v8::Object>()); |
| 140 | } else { |
| 141 | error = true; |
| 142 | break; |
| 143 | } |
| 144 | } |
| 145 | if (error) break; |
| 146 | } else { |
| 147 | bool isPointerList = |
| 148 | builder.as<capnp::AnyList>().getElementSize() == capnp::ElementSize::POINTER; |
| 149 | for (uint i: kj::indices(builder)) { |
| 150 | auto jsElement = jsg::check(jsArray->Get(js.v8Context(), i)); |
| 151 | if (isPointerList && (jsElement->IsNull() || jsElement->IsUndefined())) { |
| 152 | // Skip null element. |
| 153 | } else { |
| 154 | builder.adopt(i, orphanFromJs(js, field, orphanage, elementType, jsElement)); |
| 155 | } |
| 156 | } |
| 157 | } |
| 158 | return kj::mv(orphan); |
| 159 | } |
| 160 | break; |
| 161 | } |
| 162 | case capnp::schema::Type::ENUM: { |
| 163 | auto schema = type.asEnum(); |
| 164 | if (jsValue->IsUint32()) { |
| 165 | return capnp::DynamicEnum(schema, jsg::check(jsValue->Uint32Value(js.v8Context()))); |
| 166 | } |
| 167 | |
| 168 | STACK_STR(js, name, jsValue, 32); |
| 169 | KJ_IF_SOME(enumerant, schema.findEnumerantByName(name)) { |
| 170 | return capnp::DynamicEnum(enumerant); |
| 171 | } |
| 172 | break; |
| 173 | } |
| 174 | case capnp::schema::Type::STRUCT: { |
| 175 | if (jsValue->IsObject()) { |
| 176 | auto schema = type.asStruct(); |
| 177 | auto orphan = orphanage.newOrphan(schema); |
| 178 | structFromJs(js, orphan.get(), jsValue.As<v8::Object>()); |
| 179 | return kj::mv(orphan); |
| 180 | } |
| 181 | break; |
| 182 | } |
| 183 | case capnp::schema::Type::INTERFACE: { |
| 184 | KJ_IF_SOME(wrapper, this->wrapper) { |
| 185 | auto schema = type.asInterface(); |
| 186 | if (jsValue->IsNull()) { |
| 187 | auto cap = |
| 188 | capnp::Capability::Client(nullptr).castAs<capnp::DynamicCapability>(schema); |
| 189 | return orphanage.newOrphanCopy(cap); |
| 190 | } else KJ_IF_SOME(cap, wrapper.tryUnwrapCap(js, js.v8Context(), jsValue)) { |
| 191 | // We were given a capability type obtained from elsewhere. |
| 192 | if (cap.getSchema().extends(schema)) { |
| 193 | return orphanage.newOrphanCopy(cap); |
| 194 | } |
| 195 | } else if (jsValue->IsObject()) { |
| 196 | // We were given a raw object, which we will treat as a server implementation. |
| 197 | auto cap = IoContext::current().getLocalCapSet().add( |
| 198 | kj::heap<CapnpServer>(js, schema, js.v8Ref(jsValue.As<v8::Object>()), wrapper)); |
| 199 | return orphanage.newOrphanCopy(kj::mv(cap)); |
| 200 | } |
| 201 | } |
| 202 | break; |
| 203 | } |
| 204 | case capnp::schema::Type::ANY_POINTER: |
| 205 | // TODO(someday): Support this somehow? |
| 206 | break; |
| 207 | } |
| 208 | |
| 209 | KJ_IF_SOME(ff, field) { |
| 210 | JSG_FAIL_REQUIRE( |
| 211 | TypeError, "Incorrect type for Cap'n Proto field: ", ff.getProto().getName()); |
| 212 | } else { |
| 213 | JSG_FAIL_REQUIRE(TypeError, "Incorrect type for Cap'n Proto value."); |
| 214 | } |
| 215 | }); |
| 216 | } |
| 217 | |
| 218 | void fieldFromJs(jsg::Lock& js, |
| 219 | capnp::DynamicStruct::Builder builder, |
| 220 | capnp::StructSchema::Field field, |
| 221 | v8::Local<v8::Value> jsValue) { |
| 222 | if (jsValue->IsUndefined()) { |
| 223 | // Ignore. |
| 224 | return; |
| 225 | } |
| 226 | auto proto = field.getProto(); |
| 227 | switch (proto.which()) { |
| 228 | case capnp::schema::Field::SLOT: { |
| 229 | builder.adopt(field, |
| 230 | orphanFromJs(js, field, capnp::Orphanage::getForMessageContaining(builder), |
| 231 | field.getType(), jsValue)); |
| 232 | return; |
| 233 | } |
| 234 | |
| 235 | case capnp::schema::Field::GROUP: |
| 236 | if (jsValue->IsObject()) { |
| 237 | structFromJs( |
| 238 | js, builder.init(field).as<capnp::DynamicStruct>(), jsValue.As<v8::Object>()); |
| 239 | } else { |
| 240 | JSG_FAIL_REQUIRE(TypeError, "Incorrect type for Cap'n Proto field: ", proto.getName()); |
| 241 | } |
| 242 | return; |
| 243 | } |
| 244 | |
| 245 | KJ_FAIL_ASSERT("Unimplemented field type (not slot or group)."); |
| 246 | } |
| 247 | |
| 248 | void structFromJs( |
| 249 | jsg::Lock& js, capnp::DynamicStruct::Builder builder, v8::Local<v8::Object> jsValue) { |
| 250 | js.withinHandleScope([&] { |
| 251 | auto schema = builder.getSchema(); |
| 252 | v8::Local<v8::Array> fieldNames = jsg::check(jsValue->GetOwnPropertyNames(js.v8Context())); |
| 253 | for (uint i: kj::zeroTo(fieldNames->Length())) { |
| 254 | auto jsName = jsg::check(fieldNames->Get(js.v8Context(), i)); |
| 255 | STACK_STR(js, fieldName, jsName, 32); |
| 256 | KJ_IF_SOME(field, schema.findFieldByName(fieldName)) { |
| 257 | fieldFromJs(js, builder, field, jsg::check(jsValue->Get(js.v8Context(), jsName))); |
| 258 | } else { |
| 259 | JSG_FAIL_REQUIRE(TypeError, "No such field in Cap'n Proto struct: ", fieldName); |
| 260 | } |
| 261 | } |
| 262 | }); |
| 263 | } |
| 264 | |
| 265 | void rpcResultsFromJs(jsg::Lock& js, |
| 266 | capnp::CallContext<capnp::DynamicStruct, capnp::DynamicStruct>& rpcContext, |
| 267 | v8::Local<v8::Value> jsValue) { |
| 268 | if (jsValue->IsObject()) { |
| 269 | structFromJs(js, rpcContext.getResults(), jsValue.As<v8::Object>()); |
| 270 | } else if (jsValue->IsUndefined()) { |
| 271 | // assume default return |
| 272 | } else { |
| 273 | JSG_FAIL_REQUIRE(TypeError, "RPC method server implementation returned a non-object."); |
| 274 | } |
| 275 | } |
| 276 | |
| 277 | // --------------------------------------------------------------------------- |
| 278 | // handle pipelines (as in promise pipelining) |
| 279 | // |
| 280 | // In C++, a capnp::RemotePromise<T> represents a combination of a Promise<T::Reader> and a |
| 281 | // T::Pipeline. The latter is a special object that allows immediately initiating pipeline calls |
| 282 | // on any capabilities that the response is expected to contain. |
| 283 | // |
| 284 | // In JavaScript, we will accomplish something similar by returning a Promise that has been |
| 285 | // extended with properties representing the pipelined capabilities. |
| 286 | |
| 287 | struct PipelinedCap; |
| 288 | using PipelinedCapMap = kj::HashMap<capnp::StructSchema::Field, PipelinedCap>; |
| 289 | |
| 290 | // We return a set of pipelined capabilities on the Promise returned by an RPC call. Later on, |
| 291 | // that Promise resolves to a response object likely containing the same capabilities again. |
| 292 | // We don't want the application to have to call `.close()` on both the pipelined version and |
| 293 | // the final version in order to actually close a capability. So, we need to make sure the final |
| 294 | // response uses the same CapnpCapability objects that were returned as part of the pipeline. |
| 295 | // To facilitate this, when we extend the Promise with pipeline properties, we also return a |
| 296 | // PipelineCapMap which contains all the objects that need to be injected into the final |
| 297 | // response. |
| 298 | struct PipelinedCap { |
| 299 | kj::OneOf<jsg::Ref<CapnpCapability>, PipelinedCapMap> content; |
| 300 | }; |
| 301 | |
| 302 | v8::Local<v8::Object> pipelineStructFieldToJs(jsg::Lock& js, |
| 303 | capnp::DynamicStruct::Pipeline& pipeline, |
| 304 | capnp::StructSchema::Field field, |
| 305 | PipelinedCapMap& capMap) { |
| 306 | v8::Local<v8::Object> fieldValue = v8::Object::New(js.v8Isolate); |
| 307 | auto subMap = |
| 308 | pipelineToJs(js, pipeline.get(field).releaseAs<capnp::DynamicStruct>(), fieldValue); |
| 309 | if (subMap.size() > 0) { |
| 310 | // Some capabilities were found in this sub-message, so add it to the map. |
| 311 | capMap.insert(field, PipelinedCap{kj::mv(subMap)}); |
| 312 | } |
| 313 | return fieldValue; |
| 314 | } |
| 315 | |
| 316 | // This function is only useful in the context of RPC, where this->wrapper will always be |
| 317 | // available. |
| 318 | PipelinedCapMap pipelineToJs( |
| 319 | jsg::Lock& js, capnp::DynamicStruct::Pipeline&& pipeline, v8::Local<v8::Object> jsValue) { |
| 320 | CapnpTypeWrapperBase& wrapper = KJ_REQUIRE_NONNULL(this->wrapper); |
| 321 | |
| 322 | return js.withinHandleScope([&]() -> PipelinedCapMap { |
| 323 | capnp::StructSchema schema = pipeline.getSchema(); |
| 324 | |
| 325 | PipelinedCapMap capMap; |
| 326 | |
| 327 | for (capnp::StructSchema::Field field: schema.getNonUnionFields()) { |
| 328 | auto proto = field.getProto(); |
| 329 | v8::Local<v8::Value> fieldValue; |
| 330 | |
| 331 | switch (proto.which()) { |
| 332 | case capnp::schema::Field::SLOT: { |
| 333 | auto type = field.getType(); |
| 334 | switch (type.which()) { |
| 335 | case capnp::schema::Type::STRUCT: |
| 336 | fieldValue = pipelineStructFieldToJs(js, pipeline, field, capMap); |
| 337 | break; |
| 338 | case capnp::schema::Type::ANY_POINTER: |
| 339 | if (type.whichAnyPointerKind() != |
| 340 | capnp::schema::Type::AnyPointer::Unconstrained::CAPABILITY) { |
| 341 | continue; |
| 342 | } |
| 343 | [[fallthrough]]; |
| 344 | case capnp::schema::Type::INTERFACE: { |
| 345 | jsg::Ref<CapnpCapability> ref = nullptr; |
| 346 | fieldValue = wrapper.wrapCap(js, js.v8Context(), |
| 347 | pipeline.get(field).releaseAs<capnp::DynamicCapability>(), &ref); |
| 348 | capMap.insert(field, PipelinedCap{kj::mv(ref)}); |
| 349 | break; |
| 350 | } |
| 351 | default: |
| 352 | continue; |
| 353 | } |
| 354 | break; |
| 355 | } |
| 356 | |
| 357 | case capnp::schema::Field::GROUP: |
| 358 | fieldValue = pipelineStructFieldToJs(js, pipeline, field, capMap); |
| 359 | break; |
| 360 | |
| 361 | default: |
| 362 | continue; |
| 363 | } |
| 364 | |
| 365 | KJ_ASSERT(!fieldValue.IsEmpty()); |
| 366 | jsg::check(jsValue->Set( |
| 367 | js.v8Context(), jsg::v8StrIntern(js.v8Isolate, proto.getName()), fieldValue)); |
| 368 | } |
| 369 | |
| 370 | return capMap; |
| 371 | }); |
| 372 | } |
| 373 | |
| 374 | // --------------------------------------------------------------------------- |
| 375 | // convert capnp values to JS |
| 376 | |
| 377 | v8::Local<v8::Value> valueToJs(jsg::Lock& js, |
| 378 | capnp::DynamicValue::Reader value, |
| 379 | capnp::Type type, |
| 380 | kj::Maybe<PipelinedCap&> pipelinedCap) { |
| 381 | // TODO(later): support deserialization outside of RPC, i.e., not requiring a wrapper. |
| 382 | CapnpTypeWrapperBase& wrapper = KJ_REQUIRE_NONNULL(this->wrapper); |
| 383 | |
| 384 | return js.withinHandleScope([&]() -> v8::Local<v8::Value> { |
| 385 | switch (value.getType()) { |
| 386 | case capnp::DynamicValue::UNKNOWN: |
| 387 | return js.undefined(); |
| 388 | case capnp::DynamicValue::VOID: |
| 389 | return js.null(); |
| 390 | case capnp::DynamicValue::BOOL: |
| 391 | return js.boolean(value.as<bool>()); |
| 392 | case capnp::DynamicValue::INT: { |
| 393 | if (type.which() == capnp::schema::Type::INT64 || |
| 394 | type.which() == capnp::schema::Type::UINT64) { |
| 395 | return v8::BigInt::New(js.v8Isolate, value.as<int64_t>()); |
| 396 | } else { |
| 397 | return v8::Integer::New(js.v8Isolate, value.as<int32_t>()); |
| 398 | } |
| 399 | } |
| 400 | case capnp::DynamicValue::UINT: { |
| 401 | if (type.which() == capnp::schema::Type::INT64 || |
| 402 | type.which() == capnp::schema::Type::UINT64) { |
| 403 | return v8::BigInt::NewFromUnsigned(js.v8Isolate, value.as<uint64_t>()); |
| 404 | } else { |
| 405 | return v8::Integer::NewFromUnsigned(js.v8Isolate, value.as<uint32_t>()); |
| 406 | } |
| 407 | } |
| 408 | case capnp::DynamicValue::FLOAT: |
| 409 | return v8::Number::New(js.v8Isolate, value.as<double>()); |
| 410 | case capnp::DynamicValue::TEXT: |
| 411 | return jsg::v8Str(js.v8Isolate, value.as<capnp::Text>()); |
| 412 | case capnp::DynamicValue::DATA: { |
| 413 | capnp::Data::Reader data = value.as<capnp::Data>(); |
| 414 | |
| 415 | // In theory we could avoid a copy if we kept the response message in memory, but we |
| 416 | // probably don't want to do that. |
| 417 | auto result = jsg::check(v8::ArrayBuffer::MaybeNew(js.v8Isolate, data.size())); |
| 418 | memcpy(result->GetBackingStore()->Data(), data.begin(), data.size()); |
| 419 | |
| 420 | return result; |
| 421 | } |
| 422 | case capnp::DynamicValue::LIST: { |
| 423 | capnp::DynamicList::Reader list = value.as<capnp::DynamicList>(); |
| 424 | auto elementType = list.getSchema().getElementType(); |
| 425 | auto indices = kj::indices(list); |
| 426 | KJ_STACK_ARRAY(v8::Local<v8::Value>, items, indices.size(), 100, 100); |
| 427 | for (uint i: indices) { |
| 428 | items[i] = valueToJs(js, list[i], elementType, kj::none); |
| 429 | } |
| 430 | return v8::Array::New(js.v8Isolate, items.begin(), items.size()); |
| 431 | } |
| 432 | case capnp::DynamicValue::ENUM: { |
| 433 | auto enumValue = value.as<capnp::DynamicEnum>(); |
| 434 | KJ_IF_SOME(enumerant, enumValue.getEnumerant()) { |
| 435 | return jsg::v8StrIntern(js.v8Isolate, enumerant.getProto().getName()); |
| 436 | } else { |
| 437 | return v8::Integer::NewFromUnsigned(js.v8Isolate, enumValue.getRaw()); |
| 438 | } |
| 439 | } |
| 440 | case capnp::DynamicValue::STRUCT: { |
| 441 | auto capMap = pipelinedCap.map([](PipelinedCap& pc) -> PipelinedCapMap& { |
| 442 | // If we had a PipelinedCap for a struct field, it must be a PipelinedCapMap. |
| 443 | return pc.content.get<PipelinedCapMap>(); |
| 444 | }); |
| 445 | |
| 446 | capnp::DynamicStruct::Reader reader = value.as<capnp::DynamicStruct>(); |
| 447 | auto object = v8::Object::New(js.v8Isolate); |
| 448 | KJ_IF_SOME(field, reader.which()) { |
| 449 | fieldToJs(js, object, reader, field, capMap); |
| 450 | } |
| 451 | |
| 452 | for (auto field: reader.getSchema().getNonUnionFields()) { |
| 453 | if (reader.has(field)) { |
| 454 | fieldToJs(js, object, reader, field, capMap); |
| 455 | } |
| 456 | } |
| 457 | return object; |
| 458 | } |
| 459 | case capnp::DynamicValue::CAPABILITY: |
| 460 | KJ_IF_SOME(p, pipelinedCap) { |
| 461 | // Use the same CapnpCapability object that we returned earlier for promise pipelining. |
| 462 | // Note: We know the JS wrapper exists because CapnpCapability objects are always created |
| 463 | // by CapnpTypeWrapper::wrap() and immediately have a wrapper added. |
| 464 | return KJ_ASSERT_NONNULL(p.content.get<jsg::Ref<CapnpCapability>>().tryGetHandle(js)); |
| 465 | } else { |
| 466 | return wrapper.wrapCap(js, js.v8Context(), value.as<capnp::DynamicCapability>()); |
| 467 | } |
| 468 | case capnp::DynamicValue::ANY_POINTER: |
| 469 | return js.null(); |
| 470 | } |
| 471 | |
| 472 | KJ_FAIL_ASSERT("Unimplemented DynamicValue type."); |
| 473 | }); |
| 474 | } |
| 475 | |
| 476 | void fieldToJs(jsg::Lock& js, |
| 477 | v8::Local<v8::Object> object, |
| 478 | capnp::DynamicStruct::Reader reader, |
| 479 | capnp::StructSchema::Field field, |
| 480 | kj::Maybe<PipelinedCapMap&> capMap) { |
| 481 | js.withinHandleScope([&] { |
| 482 | kj::Maybe<PipelinedCap&> pipelinedCap; |
| 483 | KJ_IF_SOME(m, capMap) { |
| 484 | pipelinedCap = m.find(field); |
| 485 | } |
| 486 | |
| 487 | auto proto = field.getProto(); |
| 488 | v8::Local<v8::Value> fieldValue; |
| 489 | switch (proto.which()) { |
| 490 | case capnp::schema::Field::SLOT: |
| 491 | fieldValue = valueToJs(js, reader.get(field), field.getType(), pipelinedCap); |
| 492 | break; |
| 493 | case capnp::schema::Field::GROUP: |
| 494 | fieldValue = valueToJs(js, reader.get(field), field.getType(), pipelinedCap); |
| 495 | break; |
| 496 | } |
| 497 | |
| 498 | JSG_REQUIRE( |
| 499 | !fieldValue.IsEmpty(), TypeError, "Unimplemented field type (not slot or group)."); |
| 500 | |
| 501 | jsg::check( |
| 502 | object->Set(js.v8Context(), jsg::v8StrIntern(js.v8Isolate, proto.getName()), fieldValue)); |
| 503 | }); |
| 504 | } |
| 505 | }; |
| 506 | |
| 507 | // ======================================================================================= |
| 508 | |
| 509 | void fillCapnpFieldFromJs(jsg::Lock& js, |
| 510 | capnp::DynamicStruct::Builder builder, |
| 511 | capnp::StructSchema::Field field, |
| 512 | v8::Local<v8::Value> jsValue) { |
| 513 | JsCapnpConverter converter; |
| 514 | converter.fieldFromJs(js, builder, field, jsValue); |
| 515 | } |
| 516 | |
| 517 | capnp::Orphan<capnp::DynamicValue> capnpValueFromJs( |
| 518 | jsg::Lock& js, capnp::Orphanage orphanage, capnp::Type type, v8::Local<v8::Value> jsValue) { |
| 519 | JsCapnpConverter converter; |
| 520 | return converter.orphanFromJs(js, kj::none, orphanage, type, jsValue); |
| 521 | } |
| 522 | |
| 523 | // ======================================================================================= |
| 524 | |
| 525 | CapnpServer::CapnpServer(jsg::Lock& js, |
| 526 | capnp::InterfaceSchema schema, |
| 527 | jsg::V8Ref<v8::Object> objectParam, |
| 528 | CapnpTypeWrapperBase& wrapper) |
| 529 | : capnp::DynamicCapability::Server(schema), |
| 530 | ioContext(IoContext::current().getWeakRef()), |
| 531 | object(kj::mv(objectParam)), |
| 532 | closeMethod(getCloseMethod(js)), |
| 533 | wrapper(wrapper) {} |
| 534 | |
| 535 | kj::Maybe<jsg::V8Ref<v8::Function>> CapnpServer::getCloseMethod(jsg::Lock& js) { |
| 536 | auto handle = object.getHandle(js); |
| 537 | auto methodHandle = |
| 538 | jsg::check(handle->Get(js.v8Context(), jsg::v8StrIntern(js.v8Isolate, "close"))); |
| 539 | if (methodHandle->IsFunction()) { |
| 540 | return js.v8Ref(methodHandle.As<v8::Function>()); |
| 541 | } else { |
| 542 | return kj::none; |
| 543 | } |
| 544 | } |
| 545 | |
| 546 | CapnpServer::~CapnpServer() noexcept(false) { |
| 547 | KJ_IF_SOME(c, closeMethod) { |
| 548 | ioContext->runIfAlive([&](IoContext& rc) { |
| 549 | rc.addTask( |
| 550 | rc.run([object = kj::mv(object), closeMethod = kj::mv(c)](Worker::Lock& lock) mutable { |
| 551 | auto handle = object.getHandle(lock); |
| 552 | auto methodHandle = closeMethod.getHandle(lock); |
| 553 | if (methodHandle->IsFunction()) { |
| 554 | jsg::check(methodHandle.As<v8::Function>()->Call(lock.getContext(), handle, 0, nullptr)); |
| 555 | } |
| 556 | })); |
| 557 | }); |
| 558 | } |
| 559 | } |
| 560 | |
| 561 | kj::Promise<void> CapnpServer::call(capnp::InterfaceSchema::Method method, |
| 562 | capnp::CallContext<capnp::DynamicStruct, capnp::DynamicStruct> rpcContext) { |
| 563 | kj::Promise<void> result = nullptr; |
| 564 | |
| 565 | bool live = ioContext->runIfAlive([&](IoContext& rc) { |
| 566 | result = |
| 567 | rc.run([this, method, rpcContext, &rc](Worker::Lock& lock) mutable -> kj::Promise<void> { |
| 568 | jsg::Lock& js = lock; |
| 569 | auto handle = object.getHandle(js); |
| 570 | auto methodName = method.getProto().getName(); |
| 571 | auto methodHandle = |
| 572 | jsg::check(handle->Get(lock.getContext(), jsg::v8StrIntern(js.v8Isolate, methodName))); |
| 573 | |
| 574 | if (!methodHandle->IsFunction()) { |
| 575 | KJ_UNIMPLEMENTED(kj::str("jsg.Error: RPC method not implemented: ", methodName)); |
| 576 | } |
| 577 | |
| 578 | JsCapnpConverter converter{wrapper}; |
| 579 | auto params = rpcContext.getParams(); |
| 580 | auto jsParams = converter.valueToJs(js, params, params.getSchema(), kj::none); |
| 581 | rpcContext.releaseParams(); |
| 582 | |
| 583 | auto result = jsg::check( |
| 584 | methodHandle.As<v8::Function>()->Call(lock.getContext(), handle, 1, &jsParams)); |
| 585 | KJ_IF_SOME(promise, wrapper.tryUnwrapPromise(lock, lock.getContext(), result)) { |
| 586 | return rc.awaitJs(js, |
| 587 | promise.then( |
| 588 | js, rc.addFunctor([this, rpcContext](jsg::Lock& js, jsg::Value result) mutable { |
| 589 | JsCapnpConverter converter{wrapper}; |
| 590 | converter.rpcResultsFromJs(js, rpcContext, result.getHandle(js)); |
| 591 | }))); |
| 592 | |
| 593 | } else { |
| 594 | converter.rpcResultsFromJs(js, rpcContext, result); |
| 595 | return kj::READY_NOW; |
| 596 | } |
| 597 | }); |
| 598 | }); |
| 599 | |
| 600 | if (live) { |
| 601 | return result; |
| 602 | } else { |
| 603 | return KJ_EXCEPTION(DISCONNECTED, "jsg.Error: Called to event context that is no longer live."); |
| 604 | } |
| 605 | } |
| 606 | |
| 607 | // ======================================================================================= |
| 608 | |
| 609 | CapnpCapability::CapnpCapability(capnp::DynamicCapability::Client client) |
| 610 | : schema(client.getSchema()), |
| 611 | client(IoContext::current().addObject(kj::heap(kj::mv(client)))) {} |
| 612 | |
| 613 | CapnpCapability::~CapnpCapability() noexcept(false) { |
| 614 | KJ_IF_SOME(c, client) { |
| 615 | // The client was not explicitly close()ed and instead waited for GC. There are two problems |
| 616 | // with this: |
| 617 | // 1. It's rude to force the remote peer to wait until the lazy garbage collector gets around |
| 618 | // to collecting the object before we let the peer know that it can clean up its end. Our |
| 619 | // GC is sociopathic, it decides when to collect based purely on its own memory pressure |
| 620 | // and has no idea what memory pressure the peer might be feeling, so likely won't make |
| 621 | // empathetic choices about when to collect. |
| 622 | // 2. We generally do not want to allow an application to observe its own garbage collection |
| 623 | // behavior, as this may reveal side channels. The capability could be a loopback into |
| 624 | // this very isolate, in which case closing it now would immediately call back into the |
| 625 | // server's close() method, notifying the application of its own GC. We need to prevent that. |
| 626 | |
| 627 | // To solve #2, we defer destruction of the object until the end of the IoContext. |
| 628 | kj::mv(c).deferGcToContext(); |
| 629 | |
| 630 | // In preview, let's try to warn the developer about the problem. |
| 631 | // |
| 632 | // TODO(cleanup): Instead of logging this warning at GC time, it would be better if we logged |
| 633 | // it at the time that the client is destroyed, i.e. when the IoContext is torn down, |
| 634 | // which is usually sooner (and more deterministic). But logging a warning during |
| 635 | // IoContext tear-down is problematic since logWarningOnce() is a method on |
| 636 | // IoContext... |
| 637 | KJ_IF_SOME(ioContext, IoContext::tryCurrent()) { |
| 638 | ioContext.logWarningOnce( |
| 639 | kj::str("A Cap'n Proto capability of type ", schema.getShortDisplayName(), |
| 640 | " was not closed properly. You must call close() on all capabilities in order to " |
| 641 | "let the other side know that you are no longer using them. You cannot rely on " |
| 642 | "the garbage collector for this because it may take arbitrarily long before actually " |
| 643 | "collecting unreachable objects.")); |
| 644 | } |
| 645 | } |
| 646 | } |
| 647 | |
| 648 | v8::Local<v8::Value> CapnpCapability::call(jsg::Lock& js, |
| 649 | capnp::InterfaceSchema::Method method, |
| 650 | v8::Local<v8::Value> params, |
| 651 | CapnpTypeWrapperBase& wrapper) { |
| 652 | auto& ioContext = IoContext::current(); |
| 653 | auto req = getClient(js, wrapper).newRequest(method); |
| 654 | JsCapnpConverter converter{wrapper}; |
| 655 | if (params->IsObject()) { |
| 656 | converter.structFromJs(js, req, params.As<v8::Object>()); |
| 657 | } else if (params->IsUndefined()) { |
| 658 | // leave params all-default |
| 659 | } else { |
| 660 | JSG_FAIL_REQUIRE(TypeError, "Argument to a capnp RPC call must be an object."); |
| 661 | } |
| 662 | if (method.isStreaming()) { |
| 663 | // Note: We know the JS wrapper exists for JSG_THIS because CapnpCapability objects are always |
| 664 | // created by CapnpTypeWrapper::wrap() and immediately have a wrapper added. |
| 665 | return wrapper.wrapPromise(js, js.v8Context(), KJ_ASSERT_NONNULL(JSG_THIS.tryGetHandle(js)), |
| 666 | ioContext.awaitIo( |
| 667 | js, req.sendStreaming(), [](jsg::Lock& js) { return js.v8Ref(js.v8Undefined()); })); |
| 668 | } else { |
| 669 | // The RPC promise is actually both a promise and a pipeline. |
| 670 | auto rpcPromise = req.send(); |
| 671 | |
| 672 | auto pipelinedCapHolder = kj::heap<JsCapnpConverter::PipelinedCap>(); |
| 673 | auto& pipelinedCapRef = *pipelinedCapHolder; |
| 674 | |
| 675 | // We'll consume the promise itself to handle converting the response. |
| 676 | // Note: We know the JS wrapper exists for JSG_THIS because CapnpCapability objects are always |
| 677 | // created by CapnpTypeWrapper::wrap() and immediately have a wrapper added. |
| 678 | auto responsePromise = |
| 679 | kj::Promise<capnp::Response<capnp::DynamicStruct>>(kj::mv(rpcPromise)) |
| 680 | .catch_([](kj::Exception&& ex) -> kj::Promise<capnp::Response<capnp::DynamicStruct>> { |
| 681 | auto errorType = jsg::tunneledErrorType(ex.getDescription()); |
| 682 | if (!errorType.isJsgError) { |
| 683 | // Wrap any non-JS exceptions as JS errors |
| 684 | auto newDescription = |
| 685 | kj::str("remote." JSG_EXCEPTION(Error) ": capnp RPC exception: "_kj, errorType.message); |
| 686 | ex.setDescription(kj::mv(newDescription)); |
| 687 | } |
| 688 | return kj::mv(ex); |
| 689 | }); |
| 690 | auto result = |
| 691 | wrapper.wrapPromise(js, js.v8Context(), KJ_ASSERT_NONNULL(JSG_THIS.tryGetHandle(js)), |
| 692 | ioContext.awaitIo(js, kj::mv(responsePromise), |
| 693 | [&wrapper, pipelinedCapHolder = kj::mv(pipelinedCapHolder)]( |
| 694 | jsg::Lock& js, capnp::Response<capnp::DynamicStruct> resp) mutable { |
| 695 | JsCapnpConverter converter{wrapper}; |
| 696 | return js.v8Ref(converter.valueToJs(js, resp, resp.getSchema(), *pipelinedCapHolder)); |
| 697 | })); |
| 698 | |
| 699 | // Now we take the pipeline part of `rpcPromise` and merge it into the V8 promise object, by |
| 700 | // adding fields representing the pipelined struct. |
| 701 | KJ_ASSERT(result->IsPromise()); |
| 702 | pipelinedCapRef.content = |
| 703 | converter.pipelineToJs(js, kj::mv(rpcPromise), result.As<v8::Promise>()); |
| 704 | |
| 705 | return result; |
| 706 | } |
| 707 | } |
| 708 | |
| 709 | void CapnpCapability::close() { |
| 710 | KJ_IF_SOME(c, client) { |
| 711 | // Verify we're in the correct IoContext. This will throw otherwise. |
| 712 | *c; |
| 713 | } |
| 714 | client = kj::none; |
| 715 | } |
| 716 | |
| 717 | jsg::Promise<kj::Maybe<jsg::V8Ref<v8::Object>>> CapnpCapability::unwrap(jsg::Lock& js) { |
| 718 | // We need to allocate a heap copy of the `Client` so that if this capability is closed while |
| 719 | // the promise is still outstanding, the client isn't destroyed, which would otherwise cause |
| 720 | // UAF in the getLocalServer() implementation. |
| 721 | auto capHolder = kj::heap(*JSG_REQUIRE_NONNULL(client, Error, "Capability has been closed.")); |
| 722 | auto& ioContext = IoContext::current(); |
| 723 | auto promise = ioContext.getLocalCapSet().getLocalServer(*capHolder); |
| 724 | |
| 725 | return ioContext.awaitIo(js, kj::mv(promise), |
| 726 | [capHolder = kj::mv(capHolder)]( |
| 727 | jsg::Lock& js, kj::Maybe<capnp::DynamicCapability::Server&> server) { |
| 728 | return server.map([&](capnp::DynamicCapability::Server& s) { |
| 729 | return kj::downcast<CapnpServer>(s).object.addRef(js); |
| 730 | }); |
| 731 | }); |
| 732 | } |
| 733 | |
| 734 | capnp::DynamicCapability::Client CapnpCapability::getClient( |
| 735 | jsg::Lock&, CapnpTypeWrapperBase& wrapper) { |
| 736 | return *JSG_REQUIRE_NONNULL(client, Error, "Capability has been closed."); |
| 737 | } |
| 738 | |
| 739 | } // namespace workerd::api |