Skip to content
File

Blob: src/workerd/jsg/struct.h

cpp286 lines
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 
18namespace workerd::jsg {
19 
20template <typename T>
21constexpr bool isV8LocalOrData = isV8Local<T>() || std::is_base_of_v<v8::Data, T> || IsJsValue<T>;
22 
23template <typename T>
24constexpr bool isV8LocalOrData<kj::Maybe<T>> = isV8LocalOrData<T>;
25 
26template <typename T>
27constexpr bool isV8LocalOrData<Optional<T>> = isV8LocalOrData<T>;
28 
29template <typename T>
30constexpr bool isV8LocalOrData<LenientOptional<T>> = isV8LocalOrData<T>;
31 
32template <typename T>
33constexpr bool isV8LocalOrData<kj::Array<T>> = isV8LocalOrData<T>;
34 
35template <typename T>
36constexpr bool isV8LocalOrData<kj::ArrayPtr<T>> = isV8LocalOrData<T>;
37 
38template <typename T>
39constexpr bool isV8LocalOrData<Dict<T>> = isV8LocalOrData<T>;
40 
41template <typename T, typename... Rest>
42constexpr 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).
53template <typename T>
54concept NotV8Local = !isV8LocalOrData<T>;
55 
56// Just to be sure we got the concept right...
57static_assert(NotV8Local<int>);
58static_assert(NotV8Local<kj::String>);
59static_assert(NotV8Local<kj::Array<int>>);
60static_assert(NotV8Local<kj::Maybe<kj::String>>);
61static_assert(NotV8Local<kj::OneOf<int, kj::String>>);
62static_assert(!NotV8Local<kj::Maybe<v8::Local<v8::Object>>>);
63static_assert(!NotV8Local<kj::Maybe<JsValue>>);
64static_assert(!NotV8Local<jsg::Optional<v8::Local<v8::Object>>>);
65static_assert(!NotV8Local<jsg::Optional<JsObject>>);
66static_assert(!NotV8Local<kj::OneOf<int, v8::Local<v8::Object>>>);
67static_assert(!NotV8Local<kj::OneOf<int, JsValue>>);
68static_assert(!NotV8Local<kj::OneOf<int, kj::String, kj::Maybe<JsValue>>>);
69static_assert(
70 !NotV8Local<kj::OneOf<int, kj::String, kj::Maybe<kj::OneOf<int, kj::Maybe<JsValue>>>>>);
71static_assert(!NotV8Local<v8::Local<v8::Object>>);
72static_assert(!NotV8Local<JsValue>);
73static_assert(!NotV8Local<v8::Local<v8::Value>>);
74static_assert(!NotV8Local<v8::Value>);
75static_assert(!NotV8Local<kj::Array<JsValue>>);
76static_assert(!NotV8Local<kj::Array<v8::Local<v8::Object>>>);
77static_assert(!NotV8Local<Dict<JsValue>>);
78 
79template <typename TypeWrapper,
80 typename Struct,
81 typename T,
82 T Struct::*field,
83 const char* name,
84 size_t namePrefixStripLength>
85class 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 
151template <typename... T>
152struct TypeTuple {
153 using Indexes = kj::_::MakeIndexes<sizeof...(T)>;
154};
155 
156template <typename Self,
157 typename T,
158 typename FieldWrapperTuple,
159 typename Indices = FieldWrapperTuple::Indexes>
160class StructWrapper;
161 
162// TypeWrapper mixin for struct types (application-defined C++ structs declared with a
163// JSG_STRUCT block).
164template <typename Self, typename T, typename... FieldWrappers, size_t... indices>
165class 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