CppInterOp
C++ Language Interoperability Layer
Loading...
Searching...
No Matches
Box.h
Go to the documentation of this file.
1//===--- Box.h - Typed result carrier across the AOT/JIT boundary -*- C++ -*-=//
2//
3// Part of the compiler-research project, under the Apache License v2.0 with
4// LLVM Exceptions.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// Cpp::Box is the typed result vocabulary that crosses the binding-AOT /
10// clang-repl-JIT boundary in both directions: catalog thunks Create<T> a
11// Box to hand to the runtime, Evaluate returns one to hand back. Storage
12// is hybrid -- fundamentals (K_Bool ... K_LongDouble) live inline in the
13// union; K_PtrOrObj keeps an owned pointer plus a type-erased
14// {retain, release} pair so the public header sees only `void*` and
15// function pointers, never the concrete payload type. Copy is shallow +
16// refcounted on K_PtrOrObj (matching clang::Value's model); the bridge
17// composes only when both layers share that semantic.
18//
19// The AOT path `Box::Create<T>(x).visit(v)` is designed to fold to a
20// direct `v(x)` -- the always_inline annotations below preserve this
21// against compiler inliner heuristics (validated separately on clang 17
22// and gcc 12 -O3; this commit does not pin the property in-tree).
23//
24// CPP_BOX_BUILTIN_TYPES is the single source of truth: enum positions,
25// storage union slots, KindOf<T> specialisations, and visit's switch are
26// all generated from this one X-macro -- one edit per new fundamental.
27// Mirrors clang's REPL_BUILTIN_TYPES so the Evaluate bridge in
28// CppInterOp.cpp is a Kind-to-Kind switch with no translation table.
29//
30//===----------------------------------------------------------------------===//
31
32#ifndef CPPINTEROP_BOX_H
33#define CPPINTEROP_BOX_H
34
35// Box.h is meant to be cheap to parse: the JIT pulls it whenever a
36// catalog thunk or evaluated snippet references Cpp::Box, so every
37// pulled-in standard header is paid at interpretation time. We avoid
38// CppInterOpTypes.h (which drags in <vector> / <string> / <set>) and
39// keep <cassert> behind NDEBUG. Under NDEBUG, this file pulls in zero
40// standard or project headers.
41
42#ifndef NDEBUG
43#include <cassert>
44#endif
45
46// Cross-platform always_inline. Duplicated from CppInterOpTypes.h's
47// CPPINTEROP_ALWAYS_INLINE under a guard so consumers don't have to
48// pull that (heavier) header just to use Box.
49#ifndef CPPINTEROP_ALWAYS_INLINE
50#if defined(_MSC_VER)
51#define CPPINTEROP_ALWAYS_INLINE __forceinline
52#elif defined(__GNUC__) || defined(__clang__)
53#define CPPINTEROP_ALWAYS_INLINE __attribute__((always_inline)) inline
54#else
55#define CPPINTEROP_ALWAYS_INLINE inline
56#endif
57#endif
58
59// Cross-platform unreachable hint used in visit's default arms.
60#ifndef CPPINTEROP_UNREACHABLE
61#if defined(_MSC_VER)
62#define CPPINTEROP_UNREACHABLE() __assume(0)
63#elif defined(__GNUC__) || defined(__clang__)
64#define CPPINTEROP_UNREACHABLE() __builtin_unreachable()
65#else
66#define CPPINTEROP_UNREACHABLE() ((void)0)
67#endif
68#endif
69
70namespace Cpp {
71
72// FIXME: clang's REPL_BUILTIN_TYPES lists `unsigned char` twice (Char_U +
73// UChar) to disambiguate the platform-default signedness of `char` from an
74// explicit `signed char` / `unsigned char`. The proper KindOf<char>()
75// resolution uses std::is_signed_v<char> to pick K_Char_S vs K_Char_U;
76// the duplicate Char_U entry is reached only via the runtime bridge
77// (Evaluate). We keep this table single-valued per C++ type for unambiguous
78// per-T specializations; Evaluate adds the K_Char_U case explicitly.
79#define CPP_BOX_BUILTIN_TYPES \
80 X(bool, Bool) \
81 X(char, Char_S) \
82 X(signed char, SChar) \
83 X(unsigned char, UChar) \
84 X(short, Short) \
85 X(unsigned short, UShort) \
86 X(int, Int) \
87 X(unsigned int, UInt) \
88 X(long, Long) \
89 X(unsigned long, ULong) \
90 X(long long, LongLong) \
91 X(unsigned long long, ULongLong) \
92 X(float, Float) \
93 X(double, Double) \
94 X(long double, LongDouble)
95
96class Box {
97public:
98 // `int` (not `unsigned char`) to work around compiler-research/cppyy#223.
99 enum Kind : int {
100#define X(type, name) K_##name,
102#undef X
103 K_Char_U, // alias of UChar storage; reached only by runtime bridge
107 };
108
109 /// Operations vtable for a K_PtrOrObj payload. Defined once per concrete
110 /// payload type in the TU that knows the type (see Evaluate's
111 /// kCompatValueOps in CppInterOp.cpp). The Box stores a pointer to this
112 /// const-static struct; the storage slot is 2 pointers (16 bytes).
113 ///
114 /// retain/release implement intrusive ref-counting: AdoptObject installs
115 /// the payload with refcount 1; copy ctor / copy assign call retain;
116 /// destructor calls release; the producer's release fires the payload's
117 /// destructor when the last ref drops.
118 struct ObjectOps {
119 /// Increment the payload's refcount. Must be noexcept; called from the
120 /// (noexcept) copy ctor / copy assign.
121 void (*retain)(void*) noexcept;
122 /// Decrement the payload's refcount; on the last drop, run the payload's
123 /// destructor and free storage. Must be noexcept; called from ~Box.
124 void (*release)(void*) noexcept;
125 };
126
127private:
128 union Storage {
129#define X(type, name) type m_##name;
131#undef X
132 struct {
133 void* m_Ptr;
134 const ObjectOps* m_Ops;
135 } m_Object;
136 };
137
138 Kind m_kind = K_Unspecified;
139 void* m_Type = nullptr;
140 Storage m_storage = {};
141
142 template <class T> static constexpr Kind KindOf() noexcept;
143 template <class T> static T& slot(Storage& s) noexcept;
144 template <class T> static const T& slot(const Storage& s) noexcept;
145
146public:
147 // -- rule of five: refcount-shared on K_PtrOrObj, bitwise on fundamentals --
148 Box() = default;
149 Box(const Box& o) noexcept
150 : m_kind(o.m_kind), m_Type(o.m_Type), m_storage(o.m_storage) {
151 // Kind check first so fundamentals constant-fold the branch away
152 // (AOT-fold preserved). K_PtrOrObj bumps the payload refcount.
153 if (m_kind == K_PtrOrObj && m_storage.m_Object.m_Ops)
154 m_storage.m_Object.m_Ops->retain(m_storage.m_Object.m_Ptr);
155 }
156 Box& operator=(const Box& o) noexcept {
157 if (this != &o) {
158 this->~Box();
159 m_kind = o.m_kind;
160 m_Type = o.m_Type;
161 m_storage = o.m_storage;
162 if (m_kind == K_PtrOrObj && m_storage.m_Object.m_Ops)
163 m_storage.m_Object.m_Ops->retain(m_storage.m_Object.m_Ptr);
164 }
165 return *this;
166 }
167 Box(Box&& o) noexcept
168 : m_kind(o.m_kind), m_Type(o.m_Type), m_storage(o.m_storage) {
169 // moved-from: dtor sees K_Unspecified → no release of the stolen ref.
170 o.m_kind = K_Unspecified;
171 }
172 Box& operator=(Box&& o) noexcept {
173 if (this != &o) {
174 this->~Box();
175 m_kind = o.m_kind;
176 m_Type = o.m_Type;
177 m_storage = o.m_storage;
178 o.m_kind = K_Unspecified;
179 }
180 return *this;
181 }
182 // always_inline so a static Kind constant-propagates into the dtor
183 // body and the K_PtrOrObj branch dead-strips on the AOT path.
185 if (m_kind == K_PtrOrObj && m_storage.m_Object.m_Ops)
186 m_storage.m_Object.m_Ops->release(m_storage.m_Object.m_Ptr);
187 }
188
189 Kind getKind() const noexcept { return m_kind; }
190 void* getType() const noexcept { return m_Type; }
191
192 /// AOT-typed extraction. \c T must match the runtime \c Kind exactly
193 /// (UB to read an inactive union member otherwise -- e.g. calling
194 /// \c unbox<int>() on a \c K_Long Box). For unknown-kind extraction
195 /// use \c visit() or check \c getKind() against \c KindOf<T>() first.
196 /// The assert is a no-op under \c NDEBUG so the AOT-fold path stays
197 /// zero-overhead.
198 template <class T> T unbox() const noexcept {
199#ifndef NDEBUG
200 assert(m_kind == KindOf<T>() &&
201 "Cpp::Box::unbox<T>(): T does not match the runtime Kind");
202#endif
203 return slot<T>(m_storage);
204 }
205
206 /// AOT-typed construction for fundamentals. Sets Kind = KindOf<T>(),
207 /// stores x. Optional QualType preserves typedef sugar (int8_t vs
208 /// signed char) the Kind enum collapses.
209 // always_inline so `Create<T>(x).visit(toPy)` folds bit-identical to
210 // a direct `toPy(x)` on gcc-12 (default inliner gives up otherwise).
211 template <class T>
213 void* type = nullptr) noexcept {
214 Box v;
215 v.m_kind = KindOf<T>();
216 v.m_Type = type;
217 slot<T>(v.m_storage) = x;
218 return v;
219 }
220
221 /// Object-payload construction. `obj` enters with refcount 1 (the
222 /// producer's invariant); ~Box calls `ops->release(obj)` which on the
223 /// last drop runs the payload's destructor. The ops table is const-static,
224 /// defined in the TU that knows the concrete payload type.
225 static Box AdoptObject(void* obj, const ObjectOps* ops, void* type) noexcept {
226 Box v;
227 v.m_kind = K_PtrOrObj;
228 v.m_Type = type;
229 v.m_storage.m_Object.m_Ptr = obj;
230 v.m_storage.m_Object.m_Ops = ops;
231 return v;
232 }
233
234 /// Object payload accessor; only valid for K_PtrOrObj. Returns the
235 /// raw pointer set at AdoptObject time. Layout is producer-defined --
236 /// the producer that installed the ObjectOps knows how to extract the
237 /// underlying object.
238 void* getObjectPtr() const noexcept {
239 return m_kind == K_PtrOrObj ? m_storage.m_Object.m_Ptr : nullptr;
240 }
241
242 /// Runtime convert across Kinds: dispatches on the actual Kind and
243 /// returns the value reinterpreted (via \c static_cast) as \c T.
244 /// Mirrors clang::Value::convertTo<T>. Only valid for fundamental
245 /// Kinds (K_Bool ... K_LongDouble); K_PtrOrObj / K_Void /
246 /// K_Unspecified are caller-must-check-Kind cases.
247 template <class T> T convertTo() const noexcept {
248 return visit([](auto x) -> T { return static_cast<T>(x); });
249 }
250
251 /// Runtime-typed dispatch via visitor. Switch over Kind, call visitor
252 /// with the typed T extracted from storage. K_PtrOrObj / K_Void / K_*
253 /// non-fundamental kinds are not dispatched -- caller checks Kind first.
254 // always_inline so the switch reduces to the single case the AOT Kind
255 // resolves to; the other arms then dead-strip.
256 template <class V>
257 CPPINTEROP_ALWAYS_INLINE auto visit(V&& vis) const -> decltype(vis(int{})) {
258 switch (m_kind) {
259#define X(type, name) \
260 case K_##name: \
261 return vis(unbox<type>());
263#undef X
264 case K_Char_U:
265 case K_Void:
266 case K_PtrOrObj:
267 case K_Unspecified:
269 }
271 }
272};
273
274// --- Per-T specializations, generated via the same X-macro -------------------
275
276#define X(type, name) \
277 template <> constexpr Box::Kind Box::KindOf<type>() noexcept { \
278 return K_##name; \
279 } \
280 template <> inline type& Box::slot<type>(Storage & s) noexcept { \
281 return s.m_##name; \
282 } \
283 template <> inline const type& Box::slot<type>(const Storage& s) noexcept { \
284 return s.m_##name; \
285 }
287#undef X
288
289} // namespace Cpp
290
291#endif // CPPINTEROP_BOX_H
#define CPPINTEROP_UNREACHABLE()
Definition Box.h:66
#define CPPINTEROP_ALWAYS_INLINE
Definition Box.h:55
#define CPP_BOX_BUILTIN_TYPES
Definition Box.h:79
Definition Box.h:96
void * getObjectPtr() const noexcept
Object payload accessor; only valid for K_PtrOrObj.
Definition Box.h:238
auto visit(V &&vis) const -> decltype(vis(int{}))
Runtime-typed dispatch via visitor.
Definition Box.h:257
Box & operator=(Box &&o) noexcept
Definition Box.h:172
T unbox() const noexcept
AOT-typed extraction.
Definition Box.h:198
T convertTo() const noexcept
Runtime convert across Kinds: dispatches on the actual Kind and returns the value reinterpreted (via ...
Definition Box.h:247
~Box() noexcept
Definition Box.h:184
Kind
Definition Box.h:99
@ K_PtrOrObj
Definition Box.h:105
@ K_Char_U
Definition Box.h:103
@ K_Unspecified
Definition Box.h:106
@ K_Void
Definition Box.h:104
void * getType() const noexcept
Definition Box.h:190
Box(Box &&o) noexcept
Definition Box.h:167
static Box Create(T x, void *type=nullptr) noexcept
AOT-typed construction for fundamentals.
Definition Box.h:212
Kind getKind() const noexcept
Definition Box.h:189
Box & operator=(const Box &o) noexcept
Definition Box.h:156
static Box AdoptObject(void *obj, const ObjectOps *ops, void *type) noexcept
Object-payload construction.
Definition Box.h:225
Definition Box.h:70
Operations vtable for a K_PtrOrObj payload.
Definition Box.h:118
void(* release)(void *) noexcept
Decrement the payload's refcount; on the last drop, run the payload's destructor and free storage.
Definition Box.h:124
void(* retain)(void *) noexcept
Increment the payload's refcount.
Definition Box.h:121