File
Blob: src/workerd/io/frankenvalue.h
| 1 | // Copyright (c) 2024 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #pragma once |
| 6 | |
| 7 | #include <workerd/io/frankenvalue.capnp.h> |
| 8 | #include <workerd/jsg/jsg.h> |
| 9 | |
| 10 | namespace workerd { |
| 11 | |
| 12 | // C++ class mirroring `Frankenvalue` as defined in `frankenvalue.capnp`. |
| 13 | // |
| 14 | // Represents a JavaScript value that has been stitched together from multiple sources outside of |
| 15 | // a JavaScript evaluation context. The Frankevalue can be evaluated down to a JS value as soon |
| 16 | // as it has a JS execution environment in which to be evaluated. |
| 17 | // |
| 18 | // This is used in particular to represent `ctx.props`. |
| 19 | class Frankenvalue { |
| 20 | public: |
| 21 | Frankenvalue(): value(EmptyObject()) {} |
| 22 | |
| 23 | bool empty() const { |
| 24 | return value.is<EmptyObject>() && properties.empty(); |
| 25 | } |
| 26 | |
| 27 | Frankenvalue clone(); |
| 28 | |
| 29 | // This method only works if the `CapTableEntry`s in this `Frankenvalue` all implement |
| 30 | // `threadSafeClone()`. |
| 31 | Frankenvalue threadSafeClone() const; |
| 32 | |
| 33 | class CapTableEntry; |
| 34 | |
| 35 | // Convert to/from capnp format. |
| 36 | // |
| 37 | // The CapTable, if any, is expected to be handled separately, as different use cases call for |
| 38 | // very different handling of the cap table. |
| 39 | void toCapnp(rpc::Frankenvalue::Builder builder); |
| 40 | static Frankenvalue fromCapnp( |
| 41 | rpc::Frankenvalue::Reader reader, kj::Vector<kj::Own<CapTableEntry>> capTable = {}); |
| 42 | |
| 43 | // Convert to/from JavaScript values. Note that round trips here don't produce the exact same |
| 44 | // Frankenvalue representation: toJs() puts all the contents together into a single value, and |
| 45 | // fromJs() always returns a Frankenvalue containing a single V8-serialized value. |
| 46 | jsg::JsValue toJs(jsg::Lock& js); |
| 47 | static Frankenvalue fromJs(jsg::Lock& js, jsg::JsValue value); |
| 48 | |
| 49 | // Like toJs() but add the properties to an existing object. Throws if the `Frankenvalue` does |
| 50 | // not represent an object. This is used to populate `env` in particular. |
| 51 | void populateJsObject(jsg::Lock& js, jsg::JsObject target); |
| 52 | |
| 53 | // Construct a Frankenvalue from JSON. |
| 54 | // |
| 55 | // (It's not possible to convert a Frankenvalue back to JSON, except by evaluating it in JS and |
| 56 | // then JSON-stringifying from there.) |
| 57 | static Frankenvalue fromJson(kj::String json); |
| 58 | |
| 59 | // Add a property to the value, represented as another Frankenvalue. This is how you "stitch |
| 60 | // together" values! |
| 61 | // |
| 62 | // This is called `set` because the new property will override any existing property with the |
| 63 | // same name, but note that this strictly appends content. The replacement happens only when the |
| 64 | // Frankenvalue is finally converted to JS. |
| 65 | void setProperty(kj::String name, Frankenvalue value); |
| 66 | |
| 67 | // --------------------------------------------------------------------------- |
| 68 | // Capability handling |
| 69 | // |
| 70 | // A Frankenvalue can contain capabilities (typically ServiceStubs). When serializing from |
| 71 | // JavaScript, these will be encoded as integer indexes into a separate table -- the CapTable. |
| 72 | |
| 73 | // The Frankenvalue itself doesn't know how these "capabilities" are implemneted, so leaves this |
| 74 | // up to a higher layer. It simply maintains a table of `CapTableEntry` objects. `CapTableEntry` |
| 75 | // serves as a generic base class for multiple representations which serializers and |
| 76 | // deserializers for specific types will need to support through downcasting. |
| 77 | // |
| 78 | // In particular: |
| 79 | // - Typically, the type is `IoChannelFactory::SubrequestChannel`. |
| 80 | // - When a Frankenvalue is being used to initialize the `env` of a dynamically-loaded isolate, |
| 81 | // each CapTableEntry may simply contain an I/O channel number. |
| 82 | // - In some environments, a CapTableEntry might be some sort of description of how to load a |
| 83 | // Worker that implements the capability. |
| 84 | class CapTableEntry { |
| 85 | public: |
| 86 | // Clone the entry, used when `Frankenvalue::clone()` is called. Many implementations may |
| 87 | // implement this using addRef(). |
| 88 | virtual kj::Own<CapTableEntry> clone() = 0; |
| 89 | |
| 90 | // Like `clone()` but works on const values. Used only when `Frankenvalue::threadSafeClone()` |
| 91 | // is called. The default implementation throws an exception. |
| 92 | virtual kj::Own<CapTableEntry> threadSafeClone() const; |
| 93 | }; |
| 94 | |
| 95 | kj::ArrayPtr<kj::Own<CapTableEntry>> getCapTable() { |
| 96 | return capTable; |
| 97 | } |
| 98 | |
| 99 | // Rewrite all the caps in the table by calling the `rewrite()` callback on each one. |
| 100 | template <typename Func> |
| 101 | void rewriteCaps(Func&& rewrite) { |
| 102 | for (auto& slot: capTable) { |
| 103 | slot = rewrite(kj::mv(slot)); |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | // When deserializing a JS value, the jsg::Deserializer's ExternalHandler will have this type. |
| 108 | class CapTableReader final: public jsg::Deserializer::ExternalHandler { |
| 109 | public: |
| 110 | kj::Maybe<CapTableEntry&> get(uint index) { |
| 111 | if (index < table.size()) { |
| 112 | return *table[index]; |
| 113 | } else { |
| 114 | return kj::none; |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | private: |
| 119 | kj::ArrayPtr<kj::Own<CapTableEntry>> table; |
| 120 | CapTableReader(kj::ArrayPtr<kj::Own<CapTableEntry>> table): table(table) {} |
| 121 | friend class Frankenvalue; |
| 122 | }; |
| 123 | |
| 124 | // When serializing a JS value, the jsg::Serializer's ExternalHandler will have this type. |
| 125 | class CapTableBuilder final: public jsg::Serializer::ExternalHandler { |
| 126 | public: |
| 127 | uint add(kj::Own<CapTableEntry> entry) { |
| 128 | uint result = target.capTable.size(); |
| 129 | target.capTable.add(kj::mv(entry)); |
| 130 | return result; |
| 131 | } |
| 132 | |
| 133 | private: |
| 134 | Frankenvalue& target; |
| 135 | CapTableBuilder(Frankenvalue& target): target(target) {} |
| 136 | friend class Frankenvalue; |
| 137 | }; |
| 138 | |
| 139 | private: |
| 140 | struct EmptyObject {}; |
| 141 | struct Json { |
| 142 | kj::String json; |
| 143 | }; |
| 144 | struct V8Serialized { |
| 145 | kj::Array<byte> data; |
| 146 | }; |
| 147 | kj::OneOf<EmptyObject, Json, V8Serialized> value; |
| 148 | |
| 149 | struct Property; |
| 150 | kj::Vector<Property> properties; |
| 151 | |
| 152 | kj::Vector<kj::Own<CapTableEntry>> capTable; |
| 153 | |
| 154 | Frankenvalue cloneImpl() const; |
| 155 | void fromCapnpImpl(rpc::Frankenvalue::Reader reader, uint& capTablePos); |
| 156 | void toCapnpImpl(rpc::Frankenvalue::Builder builder, uint capTableSize); |
| 157 | jsg::JsValue toJsImpl(jsg::Lock& js, kj::ArrayPtr<kj::Own<CapTableEntry>> capTable); |
| 158 | }; |
| 159 | |
| 160 | // Can't be defined inline since `Frankenvalue` is still incomplete there. |
| 161 | struct Frankenvalue::Property { |
| 162 | kj::String name; |
| 163 | Frankenvalue value; |
| 164 | |
| 165 | // `value.capTable` is always empty. Instead, these two values specify the slice of the parent's |
| 166 | // capTable which this Frankenvalue refers into. |
| 167 | uint capTableOffset = 0; |
| 168 | uint capTableSize = 0; |
| 169 | }; |
| 170 | |
| 171 | } // namespace workerd |