// Copyright (c) 2017-2022 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #pragma once // INTERNAL IMPLEMENTATION FILE // // Translates between C++ struct types and JavaScript objects. This translation is by value: the // struct is translated to/from a native JS object with the same field names. #include #include #include #include #include namespace workerd::jsg { template constexpr bool isV8LocalOrData = isV8Local() || std::is_base_of_v || IsJsValue; template constexpr bool isV8LocalOrData> = isV8LocalOrData; template constexpr bool isV8LocalOrData> = isV8LocalOrData; template constexpr bool isV8LocalOrData> = isV8LocalOrData; template constexpr bool isV8LocalOrData> = isV8LocalOrData; template constexpr bool isV8LocalOrData> = isV8LocalOrData; template constexpr bool isV8LocalOrData> = isV8LocalOrData; template constexpr bool isV8LocalOrData> = isV8LocalOrData || (isV8LocalOrData || ...); // JSG_STRUCT member fields really should not be v8::Locals, v8::Datas, or JsValues because // there's no guarantee the v8::HandleScope will be valid when the field is accessed. Instead // they should be wrapped in jsg::V8Ref or jsg::JsRef. However, we only want to enforce this // for JSG_STRUCTs that we *receive* from JS, not for JSG_STRUCTs that we *send* to JS, so // we only actually apply this check when unwrapping (JS -> C++). Why? Great question! It's // because when we are sending a struct to JS, we know we have a valid v8::HandleScope and // it's fairly expensive to create a jsg::JsRef/jsg::V8Ref, especially when we need to do // so repeatedly (e.g. for an iterator, for instance). template concept NotV8Local = !isV8LocalOrData; // Just to be sure we got the concept right... static_assert(NotV8Local); static_assert(NotV8Local); static_assert(NotV8Local>); static_assert(NotV8Local>); static_assert(NotV8Local>); static_assert(!NotV8Local>>); static_assert(!NotV8Local>); static_assert(!NotV8Local>>); static_assert(!NotV8Local>); static_assert(!NotV8Local>>); static_assert(!NotV8Local>); static_assert(!NotV8Local>>); static_assert( !NotV8Local>>>>); static_assert(!NotV8Local>); static_assert(!NotV8Local); static_assert(!NotV8Local>); static_assert(!NotV8Local); static_assert(!NotV8Local>); static_assert(!NotV8Local>>); static_assert(!NotV8Local>); template class FieldWrapper { static constexpr inline const char* exportedName = name + namePrefixStripLength; public: using Type = T; explicit FieldWrapper(v8::Isolate* isolate) : nameHandle(isolate, v8StrIntern(isolate, exportedName)) {} // The is the original, slow-path wrap implementation that uses Set(). Prefer the other overload // for better performance. It is, however, a breaking change to remove this overload so we // need to keep it with a compatibility flag. void wrap(Lock& js, TypeWrapper& wrapper, v8::Isolate* isolate, v8::Local context, kj::Maybe> creator, Struct& in, v8::Local out) { if constexpr (kj::isSameType()) { // Ignore SelfRef when converting to JS. } else if constexpr (kj::isSameType() || kj::isSameType()) { // Fields with these types are required NOT to be present, so don't try to convert them. } else { if constexpr (webidl::OptionalType) { // Don't even set optional fields that aren't present. if (in.*field == kj::none) return; } auto value = wrapper.wrap(js, context, creator, kj::mv(in.*field)); check(out->Set(context, nameHandle.Get(isolate), value)); } } void wrap(Lock& js, TypeWrapper& wrapper, v8::Isolate* isolate, v8::Local context, kj::Maybe> creator, Struct& in, v8::MaybeLocal& out, size_t& idx) { if constexpr (kj::isSameType()) { // Ignore SelfRef when converting to JS. } else if constexpr (kj::isSameType() || kj::isSameType()) { // Fields with these types are required NOT to be present, so don't try to convert them. } else { idx++; out = wrapper.wrap(js, context, creator, kj::mv(in.*field)); } } Type unwrap(TypeWrapper& wrapper, v8::Isolate* isolate, v8::Local context, v8::Local in) { static_assert(NotV8Local); v8::Local jsValue = check(in->Get(context, nameHandle.Get(isolate))); auto& js = Lock::from(isolate); return wrapper.template unwrap( js, context, jsValue, TypeErrorContext::structField(typeid(Struct), exportedName), in); } private: v8::Global nameHandle; }; template struct TypeTuple { using Indexes = kj::_::MakeIndexes; }; template class StructWrapper; // TypeWrapper mixin for struct types (application-defined C++ structs declared with a // JSG_STRUCT block). template class StructWrapper, kj::_::Indexes> { public: static const JsgKind JSG_KIND = JsgKind::STRUCT; static constexpr const std::type_info& getName(T*) { return typeid(T); } // A count of the JSG_STRUCT fields that are usable for the v8::DictionaryTemplate // version of wrap (i.e. not SelfRef, Unimplemented, or WontImplement). static constexpr size_t kCountOfUsableFields = ((isUsableStructField ? 1 : 0) + ...); v8::Local wrap( Lock& js, v8::Local context, kj::Maybe> creator, T&& in) { auto isolate = js.v8Isolate; auto& fields = getFields(isolate); // Fast path using a cached dictionary template. if (js.isUsingFastJsgStruct()) { v8::MaybeLocal values[kCountOfUsableFields]{}; size_t idx = 0; (kj::get(fields).wrap( js, static_cast(*this), isolate, context, creator, in, values[idx], idx), ...); // We use a cached dictionary template to improve performance on repeated struct wraps. v8::Local tmpl; if (templateHandle.IsEmpty()) { tmpl = T::template jsgGetTemplate(isolate); templateHandle.Reset(isolate, tmpl); } else { tmpl = templateHandle.Get(isolate); } // Make sure we filled in the expected number of fields. KJ_ASSERT(idx == kCountOfUsableFields); return tmpl->NewInstance(context, values); } // Original slow path. v8::Local out = v8::Object::New(isolate); (kj::get(fields).wrap( js, static_cast(*this), isolate, context, creator, in, out), ...); return out; } kj::Maybe tryUnwrap(Lock& js, v8::Local context, v8::Local handle, T*, kj::Maybe> parentObject) { // In the case that an individual field is the wrong type, we don't return null, but throw an // exception directly. This is because: // 1) If we returned null, we'd lose useful debugging information about which exact field was // incorrectly typed. // 2) Returning null is intended to allow calling code to probe for different types, e.g. to // allow a parameter which is "either a String or an ArrayBuffer". Such probing really // intends to check the top-level type. Recursively probing all fields in order to check // if they match probably isn't a practical use case, since it would be inefficient and // could lead to ambiguous results, especially when fields are optional. // // For similar reasons, if we are initializing this dictionary from null/undefined, and the // dictionary has required members, we throw. if (handle->IsUndefined() || handle->IsNull()) { if constexpr (((webidl::OptionalType || kj::isSameType()) && ...)) { return T{}; } jsg::throwTypeError(js.v8Isolate, kj::str("Cannot initialize ", typeid(T).name(), " with required members from an " "undefined or null value.")); } if (!handle->IsObject()) return kj::none; auto& fields = getFields(js.v8Isolate); auto in = handle.As(); // Note: We unwrap struct members in the order in which the compiler evaluates the expressions // in `T { expressions... }`. This is technically a non-conformity from Web IDL's perspective: // it prescribes lexicographically-ordered member initialization, with base members ordered // before derived members. Objects with mutating getters might be broken by this, but it // doesn't seem worth fixing absent a compelling use case. auto t = T{kj::get(fields).unwrap(static_cast(*this), js.v8Isolate, context, in)...}; // Note that if a `validate` function is provided, then it will be called after the struct is // unwrapped from v8. This would be an appropriate time to throw an error. // Signature: void validate(jsg::Lock& js); if constexpr (requires { t.validate(js); }) { t.validate(js); } return t; } void newContext() = delete; void getTemplate() = delete; private: v8::Global templateHandle; kj::Maybe> lazyFields; kj::Tuple& getFields(v8::Isolate* isolate) { KJ_IF_SOME(f, lazyFields) { return f; } else { return lazyFields.emplace(kj::tuple(FieldWrappers(isolate)...)); } } }; } // namespace workerd::jsg