Skip to content

Commit ecad02e

Browse files
committed
py/objtype: Add support for PEP487 __set_name__.
This PR adds support for the `__set_name__` data model method specified by PEP487 - Simpler customisation of class creation. This includes support for methods that mutate the owner class, and avoids the naive modify-while-iterating hazard possible in a naive implementation like #15503. Note that based on the benchmarks in #16825, this is also as fast or faster than the naive implementation, thanks to clever data layout in setname_list_t, and the way this allows the capture step to run during an existing loop through the class dict. Other rejected approaches for dealing with the hazard include: - python/cpython#72983 During the implementation of this feature for MicroPython, it was discovered that some versions of CPython also have this naive hazard. CPython resolved this bug in BPO-28797 and now makes a complete flat copy of the class's dict to iterate. This design decision doesn't make much sense for a microcontroller though, even if it's perfectly reasonable in the desktop world where memcpy might actually be cheaper than a hard-to-branch-predict conditional; and it's also motivated in their case by error-tracing considerations. - #16816 This is an equivalent implementation to CPython's approach that places this copy directly on the stack; however it is both slower and has larger code size than the approach taken here. - #15503 The simplest implementation is to just not worry about it and let the user face the consequences if they mutate the owner class. That's not a very friendly behavior, though, and it's not actually much more performant than this implementation on either time or code size. - #17693 Another alternative is to do the same as #15503 but leverage MicroPython's existing `is_fixed` field in its dict type to convert attempted mutations of the owner dict into `AttributeError`s. This is safer than just leaving the open hazard, but there's still important use-cases for owner-mutating descriptors, and the performance ain is small enough that it isn't worth missing support for those cases. - combined #17693 with this Another version of this feature used a new feature define, `MICROPY_PY_METACLASSES_LITE`, to control whether this algorithm or the naive version is used. This was rejected in favor of simplicity, based on the very limited performance margin the naive version has (which in some cases even goes _against_ it). Signed-off-by: Anson Mansfield <amansfield@mantaro.com>
1 parent 8d1f61a commit ecad02e

File tree

1 file changed

+80
-13
lines changed

1 file changed

+80
-13
lines changed

py/objtype.c

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -661,8 +661,8 @@ static void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *des
661661
// try __getattr__
662662
if (attr != MP_QSTR___getattr__) {
663663
#if MICROPY_PY_DESCRIPTORS
664-
// With descriptors enabled, don't delegate lookups of __get__/__set__/__delete__.
665-
if (attr == MP_QSTR___get__ || attr == MP_QSTR___set__ || attr == MP_QSTR___delete__) {
664+
// With descriptors enabled, don't delegate lookups of __get__/__set__/__delete__/__set_name__.
665+
if (attr == MP_QSTR___get__ || attr == MP_QSTR___set__ || attr == MP_QSTR___delete__ || attr == MP_QSTR___set_name__) {
666666
return;
667667
}
668668
#endif
@@ -960,7 +960,7 @@ static bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) {
960960
#endif
961961
#if MICROPY_PY_DESCRIPTORS
962962
static const uint8_t to_check[] = {
963-
MP_QSTR___get__, MP_QSTR___set__, MP_QSTR___delete__,
963+
MP_QSTR___get__, MP_QSTR___set__, MP_QSTR___delete__, // not needed for MP_QSTR___set_name__ tho
964964
};
965965
for (size_t i = 0; i < MP_ARRAY_SIZE(to_check); ++i) {
966966
mp_obj_t dest_temp[2];
@@ -974,6 +974,51 @@ static bool check_for_special_accessors(mp_obj_t key, mp_obj_t value) {
974974
}
975975
#endif
976976

977+
#if MICROPY_PY_DESCRIPTORS
978+
// Shared data layout for the __set_name__ call and a linked list of calls to be made.
979+
typedef union _setname_list_t setname_list_t;
980+
union _setname_list_t {
981+
mp_obj_t call[4];
982+
struct {
983+
mp_obj_t _meth;
984+
mp_obj_t _self;
985+
setname_list_t *next; // can use the "owner" argument position temporarily for the linked list
986+
mp_obj_t _name;
987+
};
988+
};
989+
990+
// Append any `__set_name__` method on `value` to the setname list, with its per-attr args
991+
static setname_list_t *setname_maybe_bind_append(setname_list_t *tail, mp_obj_t name, mp_obj_t value) {
992+
// make certain our type-punning is safe:
993+
MP_STATIC_ASSERT_NONCONSTEXPR(offsetof(setname_list_t, next) == offsetof(setname_list_t, call[2]));
994+
995+
// tail is a blank list entry
996+
mp_load_method_maybe(value, MP_QSTR___set_name__, tail->call);
997+
if (tail->call[1] != MP_OBJ_NULL) {
998+
// Each time a __set_name__ is found, leave it in-place in the former tail and allocate a new tail
999+
tail->next = m_new_obj(setname_list_t);
1000+
tail->next->next = NULL;
1001+
tail->call[3] = name;
1002+
return tail->next;
1003+
} else {
1004+
return tail;
1005+
}
1006+
}
1007+
1008+
// Execute the captured `__set_name__` calls, destroying the setname list in the process.
1009+
static void setname_consume_call_all(setname_list_t head, mp_obj_t owner) {
1010+
setname_list_t *iter = &head, *next;
1011+
while ((next = iter->next) != NULL) {
1012+
iter->call[2] = owner;
1013+
mp_call_method_n_kw(2, 0, iter->call);
1014+
if (iter != &head) { // head is stack, just mark blank
1015+
m_del_obj(setname_list_t, iter); // optimistic free, help gc not take too many passes to unwind
1016+
}
1017+
iter = next;
1018+
}
1019+
}
1020+
#endif
1021+
9771022
static void type_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
9781023
(void)kind;
9791024
mp_obj_type_t *self = MP_OBJ_TO_PTR(self_in);
@@ -1210,20 +1255,38 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
12101255
}
12111256
}
12121257

1258+
#if MICROPY_PY_DESCRIPTORS
1259+
// To avoid any dynamic allocations when no __set_name__ exists,
1260+
// the head of this list is kept on the stack (marked blank with `next = NULL`).
1261+
setname_list_t setname_list = { .next = NULL };
1262+
setname_list_t *setname_tail = &setname_list;
1263+
#endif
1264+
12131265
#if ENABLE_SPECIAL_ACCESSORS
1214-
// Check if the class has any special accessor methods
1215-
if (!(o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS)) {
1216-
for (size_t i = 0; i < locals_ptr->map.alloc; i++) {
1217-
if (mp_map_slot_is_filled(&locals_ptr->map, i)) {
1218-
const mp_map_elem_t *elem = &locals_ptr->map.table[i];
1219-
if (check_for_special_accessors(elem->key, elem->value)) {
1220-
o->flags |= MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS;
1221-
break;
1222-
}
1266+
// Check if the class has any special accessor methods,
1267+
// and accumulate bound __set_name__ methods that need to be called
1268+
for (size_t i = 0; i < locals_ptr->map.alloc; i++) {
1269+
#if !MICROPY_PY_DESCRIPTORS
1270+
// __set_name__ needs to scan the entire locals map, can't early-terminate
1271+
if (o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS) {
1272+
break;
1273+
}
1274+
#endif
1275+
1276+
if (mp_map_slot_is_filled(&locals_ptr->map, i)) {
1277+
const mp_map_elem_t *elem = &locals_ptr->map.table[i];
1278+
1279+
if (!(o->flags & MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS) // elidable when the early-termination check is enabled
1280+
&& check_for_special_accessors(elem->key, elem->value)) {
1281+
o->flags |= MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS;
12231282
}
1283+
1284+
#if MICROPY_PY_DESCRIPTORS
1285+
setname_tail = setname_maybe_bind_append(setname_tail, elem->key, elem->value);
1286+
#endif
12241287
}
12251288
}
1226-
#endif
1289+
#endif // ENABLE_SPECIAL_ACCESSORS
12271290

12281291
const mp_obj_type_t *native_base;
12291292
size_t num_native_bases = instance_count_native_bases(o, &native_base);
@@ -1241,6 +1304,10 @@ mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict)
12411304
}
12421305
}
12431306

1307+
#if MICROPY_PY_DESCRIPTORS
1308+
setname_consume_call_all(setname_list, MP_OBJ_FROM_PTR(o));
1309+
#endif
1310+
12441311
return MP_OBJ_FROM_PTR(o);
12451312
}
12461313

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy