Skip to content
File

Blob: src/workerd/api/capnp.c++

31.6 KB
1#include "capnp.h"
2 
3namespace 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.
30struct 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 
509void 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 
517capnp::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 
525CapnpServer::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 
535kj::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 
546CapnpServer::~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 
561kj::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 
609CapnpCapability::CapnpCapability(capnp::DynamicCapability::Client client)
610 : schema(client.getSchema()),
611 client(IoContext::current().addObject(kj::heap(kj::mv(client)))) {}
612 
613CapnpCapability::~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 
648v8::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 
709void 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 
717jsg::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 
734capnp::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