Skip to content

Commit 0a92aef

Browse files
committed
Create a metaclass for each supplemental data type
1 parent a570102 commit 0a92aef

13 files changed

+487
-108
lines changed

docs/api_core.rst

+21-3
Original file line numberDiff line numberDiff line change
@@ -2059,9 +2059,20 @@ declarations in generated :ref:`stubs <stubs>`,
20592059

20602060
.. cpp:struct:: template <typename T> supplement
20612061

2062-
Indicate that ``sizeof(T)`` bytes of memory should be set aside to
2063-
store supplemental data in the type object. See :ref:`Supplemental
2064-
type data <supplement>` for more information.
2062+
Indicate that the type being created (not its instances) should contain
2063+
a subobject of type ``T``, which can later be accessed using
2064+
:cpp:func:`nb::type_supplement\<T\>() <type_supplement>`. nanobind will
2065+
create a separate metaclass per type ``T``. See
2066+
:ref:`Supplemental type data <supplement>` for more information.
2067+
2068+
.. cpp::function:: supplement&& inheritable() &&
2069+
2070+
By default, specifying a supplement for a type prevents that type from
2071+
being inherited, since there would be no general way to initialize the
2072+
supplemental data for the derived type. If you instead pass the binding
2073+
annotation ``nb::supplement<T>().inheritable()``, inheritance will be
2074+
allowed. Any derived type gets a new default-constructed copy of the
2075+
supplemental data, not a copy of the data stored in the base type.
20652076

20662077
.. cpp:struct:: type_slots
20672078

@@ -2866,6 +2877,13 @@ Type objects
28662877
accesses may crash the interpreter. Also refer to
28672878
:cpp:class:`nb::supplement\<T\> <supplement>`.
28682879

2880+
.. cpp:function:: template <typename T> bool type_has_supplement(handle h)
2881+
2882+
Assuming that `h` represents a bound type (see :cpp:func:`type_check`),
2883+
check whether it stores supplemental data of type ``T``. If this function
2884+
returns true, then it is valid to call :cpp:func:`nb::type_supplement\<T\>(h)
2885+
<type_supplement>`.
2886+
28692887
.. cpp:function:: str type_name(handle h)
28702888

28712889
Return the full (module-qualified) name of a type object as a Python string.

docs/changelog.rst

+14-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,20 @@ Upcoming version (TBA)
2525
long-standing inconvenience. (PR `#778
2626
<https://github.com/wjakob/nanobind/pull/778>`__).
2727

28-
* ABI version 16.
28+
- nanobind now creates a separate metaclass for each supplemental data type
29+
named in a :cpp:class:`nb::supplement\<T\>() <supplement>` class
30+
binding annotation. (Previously, it created a separate metaclass for each
31+
**size** of supplemental data.) This enables the use of supplemental data
32+
types with non-trivial construction and destruction. You can also now query
33+
whether a nanobind type uses a particular supplement type via the new
34+
function :cpp:func:`nb::type_has_supplement\<T\>() <type_has_supplement>`,
35+
and can implement certain metaclass customizations by defining a method
36+
``static void T::init_metaclass(PyTypeObject *metatype)``. Review the
37+
documentation of :ref:`supplemental type data <supplement>` for more
38+
information about the new capabilities. (PR `#972
39+
<https://github.com/wjakob/nanobind/pull/972>`__).
40+
41+
- ABI version 16.
2942

3043

3144
Version 2.5.0 (Feb 2, 2025)

docs/lowlevel.rst

+78-10
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,10 @@ Here is what this might look like in an implementation:
238238
239239
struct MyTensorMetadata {
240240
bool stored_on_gpu;
241-
// ..
242-
// should be a POD (plain old data) type
241+
// ...
243242
};
244243
245-
// Register a new type MyTensor, and reserve space for sizeof(MyTensorMedadata)
244+
// Register a new type MyTensor, and reserve space for MyTensorMedadata
246245
nb::class_<MyTensor> cls(m, "MyTensor", nb::supplement<MyTensorMedadata>())
247246
248247
/// Mutable reference to 'MyTensorMedadata' portion in Python type object
@@ -251,13 +250,17 @@ Here is what this might look like in an implementation:
251250
252251
The :cpp:class:`nb::supplement\<T\>() <supplement>` annotation implicitly also
253252
passes :cpp:class:`nb::is_final() <is_final>` to ensure that type objects with
254-
supplemental data cannot be subclassed in Python.
255-
256-
nanobind requires that the specified type ``T`` be trivially default
257-
constructible. It zero-initializes the supplement when the type is first
258-
created but does not perform any further custom initialization or destruction.
259-
You can fill the supplement with different contents following the type
260-
creation, e.g., using the placement new operator.
253+
supplemental data cannot be subclassed in Python. If you do wish to allow
254+
subclassing, write ``nb::supplement<T>().inheritable()`` instead. Any subclasses
255+
will get a new default-constructed copy of the supplemental ``T`` data, **not**
256+
a copy of their base class's data.
257+
258+
nanobind requires that the specified type ``T`` be either constructible from
259+
``PyTypeObject*`` (the type that contains this supplement instance) or default
260+
constructible. If ``T`` has a trivial default constructor, it will be
261+
zero-initialized when the type is first created; otherwise, the appropriate
262+
constructor will be run. You can fill the supplement with different contents
263+
following the type creation.
261264

262265
The contents of the supplemental data are not directly visible to Python's
263266
cyclic garbage collector, which creates challenges if you want to reference
@@ -268,6 +271,71 @@ name that begins with the symbol ``@``, then nanobind will prevent Python
268271
code from rebinding or deleting the attribute after it has been set, making
269272
the borrowed reference reasonably safe.
270273

274+
Metaclass customizations
275+
^^^^^^^^^^^^^^^^^^^^^^^^
276+
277+
nanobind internally creates a separate metaclass for each distinct
278+
supplement type ``T`` used in your program. It is possible to customize this
279+
metaclass in some limited ways, by providing a static member function of the
280+
supplement type: ``static void T::init_metaclass(PyTypeObject *mcls)``.
281+
If provided, this function will be invoked once after the metaclass has been
282+
created but before any types that use it have been created. It can customize
283+
the metaclass's behavior by assigning attributes, including ``__dunder__``
284+
methods.
285+
286+
It is not possible to provide custom :ref:`type slots <typeslots>` for the
287+
metaclass, nor can the metaclass itself contain user-provided data (try
288+
defining properties backed by global variables instead). It is not possible
289+
to customize the metaclass's base or its type, because nanobind requires all
290+
nanobind instances to have the same meta-metaclass for quick identification.
291+
292+
The following example shows how you could customize
293+
``__instancecheck__`` to obtain results similar to :py:class:`abc.ABC`\'s
294+
support for virtual base classes.
295+
296+
.. code-block:: cpp
297+
298+
struct HasVirtualSubclasses {
299+
static void init_metaclass(PyTypeObject *mcls) {
300+
nb::cpp_function_def(
301+
[](nb::handle cls, nb::handle instance) {
302+
if (!nb::type_check(cls) ||
303+
!nb::type_has_supplement<HasVirtualSubclasses>(cls))
304+
return false;
305+
306+
auto& supp = nb::type_supplement<HasVirtualSubclasses>(cls);
307+
return PyType_IsSubtype((PyTypeObject *) instance.type(),
308+
(PyTypeObject *) cls.ptr()) ||
309+
nb::borrow<nb::set>(supp.subclasses).contains(
310+
instance.type());
311+
},
312+
nb::is_method(), nb::scope(metatype),
313+
nb::name("__instancecheck__"));
314+
}
315+
316+
explicit HasVirtualSubclasses(PyTypeObject *tp) {
317+
nb::set subclasses_set;
318+
nb::handle(tp).attr("@subclasses") = subclasses_set;
319+
subclasses = subclasses_set;
320+
}
321+
322+
void register(nb::handle tp) {
323+
nb::borrow<nb::set>(subclasses).add(tp);
324+
}
325+
326+
nb::handle subclasses;
327+
};
328+
329+
struct Collection {};
330+
struct Set {};
331+
332+
auto coll = nb::class_<Collection>(m, "Collection",
333+
nb::supplement<HasVirtualSubclasses>());
334+
auto set = nb::class_<Set>(m, "Set").def(nb::init<>());
335+
nb::type_supplement<HasVirtualSubclasses>(coll).register(set);
336+
337+
// assert isinstance(m.Set(), m.Collection)
338+
271339
.. _typeslots:
272340

273341
Customizing type creation

docs/porting.rst

+6-1
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,12 @@ Removed features include:
338338
annotation was removed. However, the same behavior can be achieved by
339339
creating unnamed arguments; see the discussion in the section on
340340
:ref:`keyword-only arguments <kw_only>`.
341-
- ○ **Metaclasses**: creating types with custom metaclasses is unsupported.
341+
- ○ **Metaclasses**: Creating types with arbitrary custom metaclasses is
342+
unsupported. However, many of the things you might want a metaclass for can
343+
be accomplished using :ref:`supplemental type data <supplement>`. Each
344+
supplement type is associated with its own metaclass, so you can add some
345+
customizations after the metaclass has been created, but nanobind does not
346+
allow customizing the metaclass's type, bases, or slots.
342347
- ○ **Module-local bindings**: support was removed (both for types and exceptions).
343348
- ○ **Custom allocation**: C++ classes with an overloaded or deleted ``operator
344349
new`` / ``operator delete`` are not supported.

include/nanobind/nb_attr.h

+8-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,14 @@ struct kw_only {};
125125
struct lock_self {};
126126

127127
template <size_t /* Nurse */, size_t /* Patient */> struct keep_alive {};
128-
template <typename T> struct supplement {};
128+
template <typename T> struct supplement {
129+
bool is_inheritable = false;
130+
131+
supplement&& inheritable() && {
132+
is_inheritable = true;
133+
return std::move(*this);
134+
}
135+
};
129136
template <typename T> struct intrusive_ptr {
130137
intrusive_ptr(void (*set_self_py)(T *, PyObject *) noexcept)
131138
: set_self_py(set_self_py) { }

include/nanobind/nb_class.h

+59-6
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,65 @@ struct type_data {
131131
#endif
132132
};
133133

134+
/// Information about a type T used as nb::supplement<T>()
135+
struct supplement_data {
136+
// These aliases are required to work around a MSVC bug
137+
using constructor_t = void (*)(void *supp, PyTypeObject *tp);
138+
using destructor_t = void (*)(void *supp);
139+
using init_metaclass_t = void (*)(PyTypeObject *metatype);
140+
141+
const std::type_info *type;
142+
size_t size;
143+
constructor_t construct; // nullptr if trivial
144+
destructor_t destruct; // nullptr if trivial
145+
init_metaclass_t init_metaclass; // nullptr if not defined
146+
147+
// Cache the metaclass object that nanobind has created to serve as
148+
// the type of all nanobind types that use this supplement type.
149+
// Due to this cache, the cost of looking up a given supplement type is
150+
// paid once per module rather than once per nanobind type that uses it.
151+
mutable PyTypeObject *metaclass_cache = nullptr;
152+
153+
// Link together all the supplement_data structures for the same type
154+
// in different extension modules, so that all of their metaclass_cache
155+
// members can be cleared when the metaclass is destroyed.
156+
mutable const supplement_data *next_in_chain = nullptr;
157+
};
158+
159+
template <typename T, typename = void>
160+
inline supplement_data::init_metaclass_t init_metaclass_for = nullptr;
161+
162+
template <typename T>
163+
inline supplement_data::init_metaclass_t init_metaclass_for<
164+
T, std::void_t<decltype(&T::init_metaclass)>> = &T::init_metaclass;
165+
166+
template <typename T, typename = int>
167+
inline supplement_data::constructor_t construct_supplement_for =
168+
std::is_trivially_default_constructible_v<T> ? nullptr :
169+
+[](void *p, PyTypeObject *) { new (p) T{}; };
170+
171+
template <typename T>
172+
inline supplement_data::constructor_t construct_supplement_for<
173+
T, enable_if_t<std::is_constructible_v<T, PyTypeObject *>>> =
174+
+[](void *p, PyTypeObject *tp) { new (p) T{tp}; };
175+
176+
template <typename T>
177+
inline const supplement_data supplement_data_for = {
178+
&typeid(T), sizeof(T),
179+
construct_supplement_for<T>,
180+
std::is_trivially_destructible_v<T> ? nullptr :
181+
+[](void *p) { ((T *) p)->~T(); },
182+
init_metaclass_for<T>,
183+
};
184+
134185
/// Information about a type that is only relevant when it is being created
135186
struct type_init_data : type_data {
136187
PyObject *scope;
137188
const std::type_info *base;
138189
PyTypeObject *base_py;
139190
const char *doc;
140191
const PyType_Slot *type_slots;
141-
size_t supplement;
192+
const supplement_data *supplement;
142193
};
143194

144195
NB_INLINE void type_extra_apply(type_init_data &t, const handle &h) {
@@ -184,13 +235,13 @@ NB_INLINE void type_extra_apply(type_init_data & t, const sig &s) {
184235
}
185236

186237
template <typename T>
187-
NB_INLINE void type_extra_apply(type_init_data &t, supplement<T>) {
188-
static_assert(std::is_trivially_default_constructible_v<T>,
189-
"The supplement must be a POD (plain old data) type");
238+
NB_INLINE void type_extra_apply(type_init_data &t, supplement<T> supp) {
190239
static_assert(alignof(T) <= alignof(void *),
191240
"The alignment requirement of the supplement is too high.");
192-
t.flags |= (uint32_t) type_init_flags::has_supplement | (uint32_t) type_flags::is_final;
193-
t.supplement = sizeof(T);
241+
t.flags |= (uint32_t) type_init_flags::has_supplement;
242+
if (!supp.is_inheritable)
243+
t.flags |= (uint32_t) type_flags::is_final;
244+
t.supplement = &supplement_data_for<T>;
194245
}
195246

196247
enum class enum_flags : uint32_t {
@@ -287,6 +338,8 @@ inline size_t type_align(handle h) { return detail::nb_type_align(h.ptr()); }
287338
inline const std::type_info& type_info(handle h) { return *detail::nb_type_info(h.ptr()); }
288339
template <typename T>
289340
inline T &type_supplement(handle h) { return *(T *) detail::nb_type_supplement(h.ptr()); }
341+
template <typename T>
342+
inline bool type_has_supplement(handle h) { return detail::nb_type_has_supplement(h.ptr(), &typeid(T)); }
290343
inline str type_name(handle h) { return steal<str>(detail::nb_type_name(h.ptr())); }
291344

292345
// Low level access to nanobind instance objects

include/nanobind/nb_lib.h

+4
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,10 @@ NB_CORE void nb_type_restore_ownership(PyObject *o, bool cpp_delete) noexcept;
316316
/// Get a pointer to a user-defined 'extra' value associated with the nb_type t.
317317
NB_CORE void *nb_type_supplement(PyObject *t) noexcept;
318318

319+
/// Determine whether the nb_type t has a supplement whose type matches supp_type.
320+
NB_CORE bool nb_type_has_supplement(PyObject *t,
321+
const std::type_info *supp_type) noexcept;
322+
319323
/// Check if the given python object represents a nanobind type
320324
NB_CORE bool nb_type_check(PyObject *t) noexcept;
321325

src/nb_internals.cpp

+12-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ extern int nb_bound_method_traverse(PyObject *, visitproc, void *);
3131
extern int nb_bound_method_clear(PyObject *);
3232
extern void nb_bound_method_dealloc(PyObject *);
3333
extern PyObject *nb_method_descr_get(PyObject *, PyObject *, PyObject *);
34+
extern PyTypeObject *nb_meta_new(nb_internals *internals,
35+
const supplement_data *supplement) noexcept;
36+
extern void nb_meta_dealloc(PyObject *);
3437

3538
#if PY_VERSION_HEX >= 0x03090000
3639
# define NB_HAVE_VECTORCALL_PY39_OR_NEWER NB_HAVE_VECTORCALL
@@ -40,6 +43,7 @@ extern PyObject *nb_method_descr_get(PyObject *, PyObject *, PyObject *);
4043

4144
static PyType_Slot nb_meta_slots[] = {
4245
{ Py_tp_base, nullptr },
46+
{ Py_tp_dealloc, (void *) nb_meta_dealloc },
4347
{ 0, nullptr }
4448
};
4549

@@ -363,8 +367,12 @@ NB_NOINLINE void init(const char *name) {
363367
p->nb_module = PyModule_NewObject(nb_name.ptr());
364368

365369
nb_meta_slots[0].pfunc = (PyObject *) &PyType_Type;
370+
#if PY_VERSION_HEX >= 0x030C0000
371+
nb_meta_spec.basicsize = -(int) sizeof(const supplement_data*);
372+
#else
373+
nb_meta_spec.basicsize = (int) (PyType_Type.tp_basicsize + sizeof(const supplement_data*));
374+
#endif
366375
nb_meta_cache = p->nb_meta = (PyTypeObject *) PyType_FromSpec(&nb_meta_spec);
367-
p->nb_type_dict = PyDict_New();
368376
p->nb_func = (PyTypeObject *) PyType_FromSpec(&nb_func_spec);
369377
p->nb_method = (PyTypeObject *) PyType_FromSpec(&nb_method_spec);
370378
p->nb_bound_method = (PyTypeObject *) PyType_FromSpec(&nb_bound_method_spec);
@@ -379,7 +387,7 @@ NB_NOINLINE void init(const char *name) {
379387
p->shards[i].inst_c2p.min_load_factor(.1f);
380388
}
381389

382-
check(p->nb_module && p->nb_meta && p->nb_type_dict && p->nb_func &&
390+
check(p->nb_module && p->nb_meta && p->nb_func &&
383391
p->nb_method && p->nb_bound_method,
384392
"nanobind::detail::init(): initialization failed!");
385393

@@ -431,6 +439,8 @@ NB_NOINLINE void init(const char *name) {
431439
is_alive_ptr = &is_alive_value;
432440
p->is_alive_ptr = is_alive_ptr;
433441

442+
p->nb_type_0 = nb_meta_new(p, nullptr);
443+
434444
#if PY_VERSION_HEX < 0x030C0000 && !defined(PYPY_VERSION)
435445
/* The implementation of typing.py on CPython <3.12 tends to introduce
436446
spurious reference leaks that upset nanobind's leak checker. The

0 commit comments

Comments
 (0)