File
Blob: src/workerd/jsg/struct.h
| 1 | // Copyright (c) 2017-2022 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 | // INTERNAL IMPLEMENTATION FILE |
| 7 | // |
| 8 | // Translates between C++ struct types and JavaScript objects. This translation is by value: the |
| 9 | // struct is translated to/from a native JS object with the same field names. |
| 10 | |
| 11 | #include <workerd/jsg/util.h> |
| 12 | #include <workerd/jsg/value.h> |
| 13 | #include <workerd/jsg/web-idl.h> |
| 14 | |
| 15 | #include <concepts> |
| 16 | #include <type_traits> |
| 17 | |
| 18 | namespace workerd::jsg { |
| 19 | |
| 20 | template <typename T> |
| 21 | constexpr bool isV8LocalOrData = isV8Local<T>() || std::is_base_of_v<v8::Data, T> || IsJsValue<T>; |
| 22 | |
| 23 | template <typename T> |
| 24 | constexpr bool isV8LocalOrData<kj::Maybe<T>> = isV8LocalOrData<T>; |
| 25 | |
| 26 | template <typename T> |
| 27 | constexpr bool isV8LocalOrData<Optional<T>> = isV8LocalOrData<T>; |
| 28 | |
| 29 | template <typename T> |
| 30 | constexpr bool isV8LocalOrData<LenientOptional<T>> = isV8LocalOrData<T>; |
| 31 | |
| 32 | template <typename T> |
| 33 | constexpr bool isV8LocalOrData<kj::Array<T>> = isV8LocalOrData<T>; |
| 34 | |
| 35 | template <typename T> |
| 36 | constexpr bool isV8LocalOrData<kj::ArrayPtr<T>> = isV8LocalOrData<T>; |
| 37 | |
| 38 | template <typename T> |
| 39 | constexpr bool isV8LocalOrData<Dict<T>> = isV8LocalOrData<T>; |
| 40 | |
| 41 | template <typename T, typename... Rest> |
| 42 | constexpr bool isV8LocalOrData<kj::OneOf<T, Rest...>> = |
| 43 | isV8LocalOrData<T> || (isV8LocalOrData<Rest> || ...); |
| 44 | |
| 45 | // JSG_STRUCT member fields really should not be v8::Locals, v8::Datas, or JsValues because |
| 46 | // there's no guarantee the v8::HandleScope will be valid when the field is accessed. Instead |
| 47 | // they should be wrapped in jsg::V8Ref or jsg::JsRef. However, we only want to enforce this |
| 48 | // for JSG_STRUCTs that we *receive* from JS, not for JSG_STRUCTs that we *send* to JS, so |
| 49 | // we only actually apply this check when unwrapping (JS -> C++). Why? Great question! It's |
| 50 | // because when we are sending a struct to JS, we know we have a valid v8::HandleScope and |
| 51 | // it's fairly expensive to create a jsg::JsRef/jsg::V8Ref, especially when we need to do |
| 52 | // so repeatedly (e.g. for an iterator, for instance). |
| 53 | template <typename T> |
| 54 | concept NotV8Local = !isV8LocalOrData<T>; |
| 55 | |
| 56 | // Just to be sure we got the concept right... |
| 57 | static_assert(NotV8Local<int>); |
| 58 | static_assert(NotV8Local<kj::String>); |
| 59 | static_assert(NotV8Local<kj::Array<int>>); |
| 60 | static_assert(NotV8Local<kj::Maybe<kj::String>>); |
| 61 | static_assert(NotV8Local<kj::OneOf<int, kj::String>>); |
| 62 | static_assert(!NotV8Local<kj::Maybe<v8::Local<v8::Object>>>); |
| 63 | static_assert(!NotV8Local<kj::Maybe<JsValue>>); |
| 64 | static_assert(!NotV8Local<jsg::Optional<v8::Local<v8::Object>>>); |
| 65 | static_assert(!NotV8Local<jsg::Optional<JsObject>>); |
| 66 | static_assert(!NotV8Local<kj::OneOf<int, v8::Local<v8::Object>>>); |
| 67 | static_assert(!NotV8Local<kj::OneOf<int, JsValue>>); |
| 68 | static_assert(!NotV8Local<kj::OneOf<int, kj::String, kj::Maybe<JsValue>>>); |
| 69 | static_assert( |
| 70 | !NotV8Local<kj::OneOf<int, kj::String, kj::Maybe<kj::OneOf<int, kj::Maybe<JsValue>>>>>); |
| 71 | static_assert(!NotV8Local<v8::Local<v8::Object>>); |
| 72 | static_assert(!NotV8Local<JsValue>); |
| 73 | static_assert(!NotV8Local<v8::Local<v8::Value>>); |
| 74 | static_assert(!NotV8Local<v8::Value>); |
| 75 | static_assert(!NotV8Local<kj::Array<JsValue>>); |
| 76 | static_assert(!NotV8Local<kj::Array<v8::Local<v8::Object>>>); |
| 77 | static_assert(!NotV8Local<Dict<JsValue>>); |
| 78 | |
| 79 | template <typename TypeWrapper, |
| 80 | typename Struct, |
| 81 | typename T, |
| 82 | T Struct::*field, |
| 83 | const char* name, |
| 84 | size_t namePrefixStripLength> |
| 85 | class FieldWrapper { |
| 86 | static constexpr inline const char* exportedName = name + namePrefixStripLength; |
| 87 | |
| 88 | public: |
| 89 | using Type = T; |
| 90 | |
| 91 | explicit FieldWrapper(v8::Isolate* isolate) |
| 92 | : nameHandle(isolate, v8StrIntern(isolate, exportedName)) {} |
| 93 | |
| 94 | // The is the original, slow-path wrap implementation that uses Set(). Prefer the other overload |
| 95 | // for better performance. It is, however, a breaking change to remove this overload so we |
| 96 | // need to keep it with a compatibility flag. |
| 97 | void wrap(Lock& js, |
| 98 | TypeWrapper& wrapper, |
| 99 | v8::Isolate* isolate, |
| 100 | v8::Local<v8::Context> context, |
| 101 | kj::Maybe<v8::Local<v8::Object>> creator, |
| 102 | Struct& in, |
| 103 | v8::Local<v8::Object> out) { |
| 104 | if constexpr (kj::isSameType<T, SelfRef>()) { |
| 105 | // Ignore SelfRef when converting to JS. |
| 106 | } else if constexpr (kj::isSameType<T, Unimplemented>() || kj::isSameType<T, WontImplement>()) { |
| 107 | // Fields with these types are required NOT to be present, so don't try to convert them. |
| 108 | } else { |
| 109 | if constexpr (webidl::OptionalType<Type>) { |
| 110 | // Don't even set optional fields that aren't present. |
| 111 | if (in.*field == kj::none) return; |
| 112 | } |
| 113 | auto value = wrapper.wrap(js, context, creator, kj::mv(in.*field)); |
| 114 | check(out->Set(context, nameHandle.Get(isolate), value)); |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | void wrap(Lock& js, |
| 119 | TypeWrapper& wrapper, |
| 120 | v8::Isolate* isolate, |
| 121 | v8::Local<v8::Context> context, |
| 122 | kj::Maybe<v8::Local<v8::Object>> creator, |
| 123 | Struct& in, |
| 124 | v8::MaybeLocal<v8::Value>& out, |
| 125 | size_t& idx) { |
| 126 | if constexpr (kj::isSameType<T, SelfRef>()) { |
| 127 | // Ignore SelfRef when converting to JS. |
| 128 | } else if constexpr (kj::isSameType<T, Unimplemented>() || kj::isSameType<T, WontImplement>()) { |
| 129 | // Fields with these types are required NOT to be present, so don't try to convert them. |
| 130 | } else { |
| 131 | idx++; |
| 132 | out = wrapper.wrap(js, context, creator, kj::mv(in.*field)); |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | Type unwrap(TypeWrapper& wrapper, |
| 137 | v8::Isolate* isolate, |
| 138 | v8::Local<v8::Context> context, |
| 139 | v8::Local<v8::Object> in) { |
| 140 | static_assert(NotV8Local<Type>); |
| 141 | v8::Local<v8::Value> jsValue = check(in->Get(context, nameHandle.Get(isolate))); |
| 142 | auto& js = Lock::from(isolate); |
| 143 | return wrapper.template unwrap<Type>( |
| 144 | js, context, jsValue, TypeErrorContext::structField(typeid(Struct), exportedName), in); |
| 145 | } |
| 146 | |
| 147 | private: |
| 148 | v8::Global<v8::Name> nameHandle; |
| 149 | }; |
| 150 | |
| 151 | template <typename... T> |
| 152 | struct TypeTuple { |
| 153 | using Indexes = kj::_::MakeIndexes<sizeof...(T)>; |
| 154 | }; |
| 155 | |
| 156 | template <typename Self, |
| 157 | typename T, |
| 158 | typename FieldWrapperTuple, |
| 159 | typename Indices = FieldWrapperTuple::Indexes> |
| 160 | class StructWrapper; |
| 161 | |
| 162 | // TypeWrapper mixin for struct types (application-defined C++ structs declared with a |
| 163 | // JSG_STRUCT block). |
| 164 | template <typename Self, typename T, typename... FieldWrappers, size_t... indices> |
| 165 | class StructWrapper<Self, T, TypeTuple<FieldWrappers...>, kj::_::Indexes<indices...>> { |
| 166 | public: |
| 167 | static const JsgKind JSG_KIND = JsgKind::STRUCT; |
| 168 | |
| 169 | static constexpr const std::type_info& getName(T*) { |
| 170 | return typeid(T); |
| 171 | } |
| 172 | |
| 173 | // A count of the JSG_STRUCT fields that are usable for the v8::DictionaryTemplate |
| 174 | // version of wrap (i.e. not SelfRef, Unimplemented, or WontImplement). |
| 175 | static constexpr size_t kCountOfUsableFields = |
| 176 | ((isUsableStructField<typename FieldWrappers::Type> ? 1 : 0) + ...); |
| 177 | |
| 178 | v8::Local<v8::Object> wrap( |
| 179 | Lock& js, v8::Local<v8::Context> context, kj::Maybe<v8::Local<v8::Object>> creator, T&& in) { |
| 180 | auto isolate = js.v8Isolate; |
| 181 | auto& fields = getFields(isolate); |
| 182 | |
| 183 | // Fast path using a cached dictionary template. |
| 184 | if (js.isUsingFastJsgStruct()) { |
| 185 | v8::MaybeLocal<v8::Value> values[kCountOfUsableFields]{}; |
| 186 | |
| 187 | size_t idx = 0; |
| 188 | (kj::get<indices>(fields).wrap( |
| 189 | js, static_cast<Self&>(*this), isolate, context, creator, in, values[idx], idx), |
| 190 | ...); |
| 191 | |
| 192 | // We use a cached dictionary template to improve performance on repeated struct wraps. |
| 193 | |
| 194 | v8::Local<v8::DictionaryTemplate> tmpl; |
| 195 | if (templateHandle.IsEmpty()) { |
| 196 | tmpl = T::template jsgGetTemplate<T>(isolate); |
| 197 | templateHandle.Reset(isolate, tmpl); |
| 198 | } else { |
| 199 | tmpl = templateHandle.Get(isolate); |
| 200 | } |
| 201 | |
| 202 | // Make sure we filled in the expected number of fields. |
| 203 | KJ_ASSERT(idx == kCountOfUsableFields); |
| 204 | |
| 205 | return tmpl->NewInstance(context, values); |
| 206 | } |
| 207 | |
| 208 | // Original slow path. |
| 209 | v8::Local<v8::Object> out = v8::Object::New(isolate); |
| 210 | (kj::get<indices>(fields).wrap( |
| 211 | js, static_cast<Self&>(*this), isolate, context, creator, in, out), |
| 212 | ...); |
| 213 | return out; |
| 214 | } |
| 215 | |
| 216 | kj::Maybe<T> tryUnwrap(Lock& js, |
| 217 | v8::Local<v8::Context> context, |
| 218 | v8::Local<v8::Value> handle, |
| 219 | T*, |
| 220 | kj::Maybe<v8::Local<v8::Object>> parentObject) { |
| 221 | // In the case that an individual field is the wrong type, we don't return null, but throw an |
| 222 | // exception directly. This is because: |
| 223 | // 1) If we returned null, we'd lose useful debugging information about which exact field was |
| 224 | // incorrectly typed. |
| 225 | // 2) Returning null is intended to allow calling code to probe for different types, e.g. to |
| 226 | // allow a parameter which is "either a String or an ArrayBuffer". Such probing really |
| 227 | // intends to check the top-level type. Recursively probing all fields in order to check |
| 228 | // if they match probably isn't a practical use case, since it would be inefficient and |
| 229 | // could lead to ambiguous results, especially when fields are optional. |
| 230 | // |
| 231 | // For similar reasons, if we are initializing this dictionary from null/undefined, and the |
| 232 | // dictionary has required members, we throw. |
| 233 | |
| 234 | if (handle->IsUndefined() || handle->IsNull()) { |
| 235 | if constexpr (((webidl::OptionalType<typename FieldWrappers::Type> || |
| 236 | kj::isSameType<typename FieldWrappers::Type, Unimplemented>()) && |
| 237 | ...)) { |
| 238 | return T{}; |
| 239 | } |
| 240 | jsg::throwTypeError(js.v8Isolate, |
| 241 | kj::str("Cannot initialize ", typeid(T).name(), |
| 242 | " with required members from an " |
| 243 | "undefined or null value.")); |
| 244 | } |
| 245 | |
| 246 | if (!handle->IsObject()) return kj::none; |
| 247 | |
| 248 | auto& fields = getFields(js.v8Isolate); |
| 249 | auto in = handle.As<v8::Object>(); |
| 250 | |
| 251 | // Note: We unwrap struct members in the order in which the compiler evaluates the expressions |
| 252 | // in `T { expressions... }`. This is technically a non-conformity from Web IDL's perspective: |
| 253 | // it prescribes lexicographically-ordered member initialization, with base members ordered |
| 254 | // before derived members. Objects with mutating getters might be broken by this, but it |
| 255 | // doesn't seem worth fixing absent a compelling use case. |
| 256 | auto t = |
| 257 | T{kj::get<indices>(fields).unwrap(static_cast<Self&>(*this), js.v8Isolate, context, in)...}; |
| 258 | |
| 259 | // Note that if a `validate` function is provided, then it will be called after the struct is |
| 260 | // unwrapped from v8. This would be an appropriate time to throw an error. |
| 261 | // Signature: void validate(jsg::Lock& js); |
| 262 | if constexpr (requires { t.validate(js); }) { |
| 263 | t.validate(js); |
| 264 | } |
| 265 | |
| 266 | return t; |
| 267 | } |
| 268 | |
| 269 | void newContext() = delete; |
| 270 | void getTemplate() = delete; |
| 271 | |
| 272 | private: |
| 273 | v8::Global<v8::DictionaryTemplate> templateHandle; |
| 274 | kj::Maybe<kj::Tuple<FieldWrappers...>> lazyFields; |
| 275 | |
| 276 | kj::Tuple<FieldWrappers...>& getFields(v8::Isolate* isolate) { |
| 277 | KJ_IF_SOME(f, lazyFields) { |
| 278 | return f; |
| 279 | } else { |
| 280 | return lazyFields.emplace(kj::tuple(FieldWrappers(isolate)...)); |
| 281 | } |
| 282 | } |
| 283 | }; |
| 284 | |
| 285 | } // namespace workerd::jsg |