docs
Loading...
Searching...
No Matches
Reflection.h
Go to the documentation of this file.
1///
2/// Copyright (C) 2026 Ultralight, Inc. All rights reserved.
3/// A license is required for commercial use. https://ultralig.ht
4///
5/// Implementation detail shared by the typed C++ header families (`<Ultralight/js/*.h>`,
6/// `<Ultralight/dom/*.h>`); include those instead. Everything here lives in
7/// ultralight::detail and may change without notice.
8///
9/// Defines ULTRALIGHT_REFLECTION and the name-reflection engine behind the families'
10/// aggregate and enum conversions: enumerator and field names are parsed out of the
11/// compiler's function-signature strings at compile time (no macros, no code generation).
12/// Each family layers its own admission policy and customization points (for example
13/// js::EnumRange and the js aggregate gate) over this engine.
14///
15#pragma once
16
17#include <array>
18#include <bit>
19#include <cstddef>
20#include <limits>
21#include <string_view>
22#include <tuple>
23#include <type_traits>
24#include <utility>
25
26// Define ULTRALIGHT_REFLECTION=0 before including any Ultralight header to disable the
27// engine (and with it every family's reflected-name tier) when a compiler's signature
28// format is unsupported.
29#ifndef ULTRALIGHT_REFLECTION
30#if defined(__clang__) || defined(__GNUC__) || (defined(_MSC_VER) && _MSC_VER >= 1940)
31#define ULTRALIGHT_REFLECTION 1
32#else
33#define ULTRALIGHT_REFLECTION 0
34#endif
35#endif
36
37/// \cond INTERNAL
38
39#if ULTRALIGHT_REFLECTION
40
41namespace ultralight {
42namespace detail {
43
44template <auto V>
45constexpr std::string_view RawValueSig() {
46#if defined(__clang__) || defined(__GNUC__)
47 return __PRETTY_FUNCTION__;
48#else
49 return __FUNCSIG__;
50#endif
51}
52
53template <typename T>
54constexpr std::string_view RawTypeSig() {
55#if defined(__clang__) || defined(__GNUC__)
56 return __PRETTY_FUNCTION__;
57#else
58 return __FUNCSIG__;
59#endif
60}
61
62constexpr bool IsIdentChar(char c) {
63 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
64 || c == '_';
65}
66
67// The region of the signature that renders the template argument. Clang renders
68// "... [V = <value>]"; GCC renders "... [with auto V = <value>; std::string_view =
69// std::basic_string_view<char>]" (the return type's alias expansion follows the
70// argument, so the region is the first "= " inside the bracket, up to the next
71// parameter separator); MSVC renders "...RawValueSig<<value>>(void)...".
72constexpr std::string_view ValueRegion(std::string_view sig) {
73#if defined(__clang__) || defined(__GNUC__)
74 size_t open = sig.find('[');
75 if (open == std::string_view::npos)
76 return {};
77 size_t eq = sig.find("= ", open);
78 if (eq == std::string_view::npos)
79 return {};
80 size_t end = sig.rfind(']');
81 if (end == std::string_view::npos || end <= eq + 2)
82 return {};
83 size_t next = sig.find("; ", eq);
84 if (next != std::string_view::npos && next < end)
85 end = next;
86 return sig.substr(eq + 2, end - (eq + 2));
87#else
88 size_t open = sig.find('<');
89 if (open == std::string_view::npos)
90 return {};
91 size_t end = sig.rfind(">(void)");
92 if (end == std::string_view::npos || end <= open + 1)
93 return {};
94 return sig.substr(open + 1, end - (open + 1));
95#endif
96}
97
98// An enumerator renders as a qualified name ending in the enumerator's identifier
99// ("Size::Small", possibly behind "(anonymous namespace)::"); any other value of the enum
100// type renders as a cast expression ending in a numeric literal ("(Size)70", "0x2"). The
101// trailing identifier run distinguishes them: a name never starts with a digit.
102constexpr std::string_view ParseEnumeratorName(std::string_view sig) {
103 std::string_view region = ValueRegion(sig);
104 if (region.empty())
105 return {};
106 size_t start = region.size();
107 while (start > 0 && IsIdentChar(region[start - 1]))
108 start--;
109 if (start == region.size())
110 return {};
111 std::string_view name = region.substr(start);
112 if (name[0] >= '0' && name[0] <= '9')
113 return {};
114 return name;
115}
116
117// A field access through the fake object renders as "...fake_obj<T>.name" (or "->name" on
118// MSVC; GCC qualifies it as ".ns::T::name"); the field name is the last identifier of
119// the qualified name after the LAST member-access token.
120constexpr std::string_view ParseFieldName(std::string_view sig) {
121 size_t anchor = std::string_view::npos;
122 for (size_t i = 0; i + 1 < sig.size(); i++) {
123 if (sig[i] == '.' && IsIdentChar(sig[i + 1]))
124 anchor = i + 1;
125 if (sig[i] == '-' && sig[i + 1] == '>' && i + 2 < sig.size() && IsIdentChar(sig[i + 2]))
126 anchor = i + 2;
127 }
128 if (anchor == std::string_view::npos)
129 return {};
130 size_t end = anchor;
131 while (end < sig.size() && IsIdentChar(sig[end]))
132 end++;
133 while (end + 2 < sig.size() && sig[end] == ':' && sig[end + 1] == ':' && IsIdentChar(sig[end + 2])) {
134 anchor = end + 2;
135 end = anchor;
136 while (end < sig.size() && IsIdentChar(sig[end]))
137 end++;
138 }
139 return sig.substr(anchor, end - anchor);
140}
141
142// The unqualified name of T ("Settings" for ultralight::Settings; "Vec3" for a Vec3<float>
143// class-template instantiation, whose rendered argument list is skipped nesting-aware).
144constexpr std::string_view ParseTypeName(std::string_view sig) {
145 std::string_view region = ValueRegion(sig);
146 if (region.empty())
147 return {};
148 size_t end = region.size();
149 while (end > 0 && region[end - 1] == ' ')
150 end--;
151 if (end > 0 && region[end - 1] == '>') {
152 int depth = 0;
153 do {
154 end--;
155 if (region[end] == '>')
156 depth++;
157 else if (region[end] == '<')
158 depth--;
159 } while (end > 0 && depth > 0);
160 }
161 size_t start = end;
162 while (start > 0 && IsIdentChar(region[start - 1]))
163 start--;
164 return region.substr(start, end - start);
165}
166
167// Null-terminated compile-time name storage (names handed to C APIs at runtime).
168template <size_t N>
169struct ZString {
170 char data[N + 1] = {};
171 size_t len = 0;
172 constexpr void Append(std::string_view s) {
173 for (char c : s)
174 data[len++] = c;
175 }
176};
177
178// --- Enum reflection ---------------------------------------------------------------------
179
180template <typename U>
181constexpr bool InUnderlyingRange(long long v) {
182 if constexpr (std::is_signed_v<U>) {
183 return v >= static_cast<long long>((std::numeric_limits<U>::min)())
184 && v <= static_cast<long long>((std::numeric_limits<U>::max)());
185 } else {
186 return v >= 0
187 && static_cast<unsigned long long>(v)
188 <= static_cast<unsigned long long>((std::numeric_limits<U>::max)());
189 }
190}
191
192// The scanned value is produced by bit_cast from the underlying type, never by
193// static_cast: for an enum without a fixed underlying type, static_cast of a value beyond
194// the enum's representable range is invalid in constant expressions (a hard error on
195// current compilers, and not one a requires-clause can absorb). The bit_cast value renders
196// as a cast expression for non-enumerators, which the parser already rejects, and values
197// outside the underlying type itself are skipped so they cannot alias real enumerators.
198template <typename E, long long I>
199constexpr std::string_view EnumeratorName() {
200 using U = std::underlying_type_t<E>;
201 if constexpr (!InUnderlyingRange<U>(I)) {
202 return {};
203 } else {
204 return ParseEnumeratorName(RawValueSig<std::bit_cast<E>(static_cast<U>(I))>());
205 }
206}
207
208// Compile-time canaries: if a compiler changes its signature format, fail loudly here
209// rather than silently reflecting wrong names.
210enum class ReflectionProbe { kProbeValue = 1 };
211static_assert(EnumeratorName<ReflectionProbe, 1>() == "kProbeValue",
212 "enum name reflection failed on this compiler; define "
213 "ULTRALIGHT_REFLECTION=0 and specialize the family's TypeTraits explicitly");
214static_assert(EnumeratorName<ReflectionProbe, 2>().empty(),
215 "enum name reflection failed to reject a non-enumerator value");
216enum ReflectionUnscopedProbe { kUnscopedProbeA, kUnscopedProbeB };
217static_assert(EnumeratorName<ReflectionUnscopedProbe, 1>() == "kUnscopedProbeB",
218 "unscoped-enum name reflection failed on this compiler; define "
219 "ULTRALIGHT_REFLECTION=0 and specialize the family's TypeTraits explicitly");
220static_assert(EnumeratorName<ReflectionUnscopedProbe, 63>().empty(),
221 "enum name reflection failed to skip a value beyond an unscoped enum's "
222 "representable range");
223
224// --- Aggregate field reflection ----------------------------------------------------------
225
226struct AnyType {
227 template <typename T>
228 constexpr operator T() const; // Never defined; used in unevaluated contexts only.
229};
230
231template <typename T, size_t... Is>
232constexpr bool BraceConstructible(std::index_sequence<Is...>) {
233 return requires { T { ((void)Is, AnyType {})... }; };
234}
235
236template <typename T, size_t N = 0>
237constexpr size_t CountFields() {
238 if constexpr (N > 24) {
239 static_assert(N <= 24, "aggregate reflection supports at most 24 fields; specialize "
240 "the family's TypeTraits for larger types");
241 return 0;
242 } else if constexpr (BraceConstructible<T>(std::make_index_sequence<N + 1> {})) {
243 return CountFields<T, N + 1>();
244 } else {
245 return N;
246 }
247}
248
249template <size_t I, typename... Ms>
250constexpr auto NthAddress(Ms&... members) noexcept {
251 return &std::get<I>(std::tie(members...));
252}
253
254template <size_t I, typename T>
255constexpr auto MemberPointer(T& t) noexcept {
256 constexpr size_t kCount = CountFields<std::remove_cvref_t<T>>();
257 static_assert(I < kCount);
258 if constexpr (kCount == 1) {
259 auto& [m0] = t;
260 return NthAddress<I>(m0);
261 } else if constexpr (kCount == 2) {
262 auto& [m0, m1] = t;
263 return NthAddress<I>(m0, m1);
264 } else if constexpr (kCount == 3) {
265 auto& [m0, m1, m2] = t;
266 return NthAddress<I>(m0, m1, m2);
267 } else if constexpr (kCount == 4) {
268 auto& [m0, m1, m2, m3] = t;
269 return NthAddress<I>(m0, m1, m2, m3);
270 } else if constexpr (kCount == 5) {
271 auto& [m0, m1, m2, m3, m4] = t;
272 return NthAddress<I>(m0, m1, m2, m3, m4);
273 } else if constexpr (kCount == 6) {
274 auto& [m0, m1, m2, m3, m4, m5] = t;
275 return NthAddress<I>(m0, m1, m2, m3, m4, m5);
276 } else if constexpr (kCount == 7) {
277 auto& [m0, m1, m2, m3, m4, m5, m6] = t;
278 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6);
279 } else if constexpr (kCount == 8) {
280 auto& [m0, m1, m2, m3, m4, m5, m6, m7] = t;
281 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7);
282 } else if constexpr (kCount == 9) {
283 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8] = t;
284 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8);
285 } else if constexpr (kCount == 10) {
286 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9] = t;
287 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9);
288 } else if constexpr (kCount == 11) {
289 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10] = t;
290 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10);
291 } else if constexpr (kCount == 12) {
292 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11] = t;
293 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11);
294 } else if constexpr (kCount == 13) {
295 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12] = t;
296 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12);
297 } else if constexpr (kCount == 14) {
298 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13] = t;
299 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13);
300 } else if constexpr (kCount == 15) {
301 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14] = t;
302 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14);
303 } else if constexpr (kCount == 16) {
304 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15] = t;
305 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14,
306 m15);
307 } else if constexpr (kCount == 17) {
308 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16] = t;
309 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14,
310 m15, m16);
311 } else if constexpr (kCount == 18) {
312 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17]
313 = t;
314 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14,
315 m15, m16, m17);
316 } else if constexpr (kCount == 19) {
317 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17,
318 m18]
319 = t;
320 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14,
321 m15, m16, m17, m18);
322 } else if constexpr (kCount == 20) {
323 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17,
324 m18, m19]
325 = t;
326 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14,
327 m15, m16, m17, m18, m19);
328 } else if constexpr (kCount == 21) {
329 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17,
330 m18, m19, m20]
331 = t;
332 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14,
333 m15, m16, m17, m18, m19, m20);
334 } else if constexpr (kCount == 22) {
335 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17,
336 m18, m19, m20, m21]
337 = t;
338 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14,
339 m15, m16, m17, m18, m19, m20, m21);
340 } else if constexpr (kCount == 23) {
341 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17,
342 m18, m19, m20, m21, m22]
343 = t;
344 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14,
345 m15, m16, m17, m18, m19, m20, m21, m22);
346 } else {
347 auto& [m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15, m16, m17,
348 m18, m19, m20, m21, m22, m23]
349 = t;
350 return NthAddress<I>(m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14,
351 m15, m16, m17, m18, m19, m20, m21, m22, m23);
352 }
353}
354
355// Declared, never defined: a compile-time address anchor so field access renders in the
356// signature string. The fake object is never constructed (non-literal field types are fine).
357#if defined(__clang__)
358#pragma clang diagnostic push
359#pragma clang diagnostic ignored "-Wundefined-var-template"
360#endif
361
362template <typename T>
363extern T fake_obj;
364
365// The field pointer travels boxed in a structural class, never as a bare template
366// argument: MSVC renders a bare pointer-to-subobject argument with the FIRST
367// specialization that produced the same pointee shape, silently collapsing distinct
368// fields (and types) onto one signature string. The boxed form renders each field
369// correctly there, is equally parseable on Clang/GCC, and so is the only route.
370template <typename P>
371struct FieldAnchor {
372 P ptr;
373};
374template <typename P>
375FieldAnchor(P) -> FieldAnchor<P>;
376
377template <typename T, size_t I>
378constexpr std::string_view FieldNameOf() {
379 return ParseFieldName(RawValueSig<FieldAnchor { MemberPointer<I>(fake_obj<T>) }>());
380}
381
382#if defined(__clang__)
383#pragma clang diagnostic pop
384#endif
385
386template <typename T>
387constexpr std::string_view UnqualifiedTypeName() {
388 return ParseTypeName(RawTypeSig<T>());
389}
390
391// Compile-time canaries for the field and type-name parsers, mirroring the enum probes.
392struct ReflectionFieldProbe {
393 int probe_field;
394};
395static_assert(FieldNameOf<ReflectionFieldProbe, 0>() == "probe_field",
396 "field name reflection failed on this compiler; define "
397 "ULTRALIGHT_REFLECTION=0 and specialize the family's TypeTraits explicitly");
398static_assert(UnqualifiedTypeName<ReflectionFieldProbe>() == "ReflectionFieldProbe",
399 "type name reflection failed on this compiler; define "
400 "ULTRALIGHT_REFLECTION=0 and specialize the family's TypeTraits explicitly");
401template <typename T>
402struct ReflectionTemplateProbe {
403 T probe_field;
404};
405static_assert(UnqualifiedTypeName<ReflectionTemplateProbe<int>>() == "ReflectionTemplateProbe",
406 "type name reflection failed on a class-template instantiation; define "
407 "ULTRALIGHT_REFLECTION=0 and specialize the family's TypeTraits explicitly");
408// Deliberately layout-identical to ReflectionFieldProbe and instantiated after it: a
409// compiler that keys subobject-pointer arguments by shape rather than identity reflects
410// the FIRST probe's field name here, so this canary turns that silent collapse loud.
411struct ReflectionFieldCollapseProbe {
412 int collapse_probe_field;
413};
414static_assert(FieldNameOf<ReflectionFieldCollapseProbe, 0>() == "collapse_probe_field",
415 "field name reflection collapsed across types on this compiler; define "
416 "ULTRALIGHT_REFLECTION=0 and specialize the family's TypeTraits explicitly");
417
418template <typename T, size_t I>
419inline constexpr auto kFieldNameZ = [] {
420 constexpr std::string_view name = FieldNameOf<T, I>();
421 ZString<name.size()> s {};
422 s.Append(name);
423 return s;
424}();
425
426template <typename T>
427inline constexpr auto kTypeNameZ = [] {
428 constexpr std::string_view name = UnqualifiedTypeName<T>();
429 ZString<name.size()> s {};
430 s.Append(name);
431 return s;
432}();
433
434template <typename T, size_t I>
435using FieldType
436 = std::remove_pointer_t<decltype(MemberPointer<I>(std::declval<T&>()))>;
437
438template <typename T>
439inline constexpr auto kFieldNames = []<size_t... Is>(std::index_sequence<Is...>) {
440 return std::array<const char*, sizeof...(Is)> { kFieldNameZ<T, Is>.data... };
441}(std::make_index_sequence<CountFields<T>()> {});
442
443} // namespace detail
444} // namespace ultralight
445
446#endif // ULTRALIGHT_REFLECTION
447/// \endcond
Root namespace for every public Ultralight type, function, and enumeration.