Repository navigation
py/runtime: Return obj->attr for obj.attr if attr has offset type. - #4969
laurensvalk wants to merge 1 commit into
Conversation
d705b89 to
8b2a4b2
Compare
This provides a simple and lightweight mechanism for getting instance attributes that are defined in C.
|
Thanks for the patch, it's quite interesting. My main reservation would be that it's adding a feature that won't be used much. Are there any places in the existing code base that could be converted to use this feature? Alternatives to this are 1) use the type->attr function to handle attribute access explicitly, which probably leads to smaller code (because there's only one function to handle all attributes); 2) use properties (this is pretty heavy though). OTOH, this feature might be able to be used to implement modifiable sys.stdin/stdout/stderr, by also extending it to allow setting of the attribute. |
|
Thanks for sharing your thoughts on this, I appreciate it.
Good point, I'll have to look at that.
I tried it via a custom attribute handler first, but it seems this led to more code and less generic code. But this may have been my use case. Some issued I encountered with that approach are:
|
One example where this could be used in the existing code base as-is (even without adding a write equivalent) is in py/objslice, where the whole #if MICROPY_PY_BUILTINS_SLICE_ATTRS
STATI const mp_rom_map_elem_t slice_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_start), MP_ROM_ATTRIBUTE_OFFSET(mp_obj_slice_t, start)},
{ MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_ATTRIBUTE_OFFSET(mp_obj_slice_t, stop) },
{ MP_ROM_QSTR(MP_QSTR_step), MP_ROM_ATTRIBUTE_OFFSET(mp_obj_slice_t, step) },
};
STATIC MP_DEFINE_CONST_DICT(slice_locals_dict, slice_locals_dict_table);
#endif
const mp_obj_type_t mp_type_slice = {
{ &mp_type_type },
.name = MP_QSTR_slice,
.print = slice_print,
#if MICROPY_PY_BUILTINS_SLICE_ATTRS
.locals_dict = (mp_obj_dict_t*)&slice_locals_dict,
#endif
};But the real benefit is for types that have both regular methods and class attributes with the default attribute handler, alongside the instance attributes introduced here. There's not really a gain in this particular example. And it isn't a size improvement here because of the |
|
Archiving some similar questions/requests: |
|
This is used heavily in the upcoming Pybricks MicroPython ports, so I'm going to try to make the case why this could be helpful for other ports as well. Our main use case is to allow a certain Python API design style, but implement it entirely in C due to device constraints. It is useful for other applications too however, see below. I won't go into the pros and cons of the API style here; this just sets out the thinking behind the implementation. Our Python stubs API is here. Rendered docs are here. I'm going to take the various LEGO hubs and sensors as an example, but this could also apply to boards like Pyboard variants. The API is designed around the "fixed modularity" of such boards. They consist of distinct "modules/components" (LEDs, screens, buttons, batteries, ...) but they are fixed instances on the boards. For example, a LEGO hub usually has a built-in status LED. (Likewise, a Pyboard has several LEDs soldered onto the board.) Consider for example these two completely different programmable hubs in pseudo code: from ._common import _Speaker, _Battery, _Light, _KeyPad
class EV3Brick:
"""LEGO® MINDSTORMS® EV3 Brick."""
def __init__(self):
self.speaker = _Speaker()
self.battery = _Battery()
self.light = _Light()
self.buttons = _KeyPad()
class MoveHub:
"""LEGO® Powered Up Move Hub."""
def __init__(self):
self.battery = _Battery()
self.light = _Light()The end-user instantiates the device and is then able to interact with the pre-configured built-in devices like the light or battery: hub = MoveHub()
hub.light.off()To make this work, we use the solution in this PR and then do: typedef struct _hubs_MoveHub_obj_t {
mp_obj_base_t base;
mp_obj_t light;
} hubs_MoveHub_obj_t;Then inside // Create an instance of the Light class, representing the hub light
self->light = common_Light_obj_make_new(internal_id, internal_capability_flags);And then { MP_ROM_QSTR(MP_QSTR_light), MP_ROM_ATTRIBUTE_OFFSET(hubs_MoveHub_obj_t, light) },We use this implementation across various classes, each with different common devices as in the example above. This avoids having to make a custom attribute handler for each one. In our case, the read-only aspect is by design, as it is meant to expose access but not override builtin physical devices. Applications are not limited to this style alone, though. It applies to exposing any desired runtime "attribute" of the C-implemented MicroPython class. It could be runtime constants (like screen size), or runtime variables (like counters, status objects, etc). So this is not about properties or handlers that execute code, but strictly about exposing an attribute like it would be accessible in Python. And arguably, setters might be useful if the goal is to generalize it, but that is perhaps better suited for fully fledged property setters because running some code is usually required. |
Thanks for the detailed explanation. I agree this new feature is useful to expose C-level attributes to Python. But it does seem quite limiting in that it can only do read-only loads of fixed/known attributes. Eg it can't handle store, delete, or dynamic selection of whether the attribute exist or not. I would have thought the latter was needed for some cases, eg a Hub with pluggable components. |
True, this could certainly be generalized to be useful for other cases. Currently it does safely handle non-existing attributes by failing to return something if
We don't auto-populate classes when things are plugged in. It's just that when a user instantiates, e.g.
I responded to this earlier but the new usyncio seems to have a close match too. If storing is added, it looks like the following STATIC void task_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
mp_obj_task_t *self = MP_OBJ_TO_PTR(self_in);
if (dest[0] == MP_OBJ_NULL) {
// Load
if (attr == MP_QSTR_coro) {
dest[0] = self->coro;
} else if (attr == MP_QSTR_data) {
dest[0] = self->data;
} else if (attr == MP_QSTR_waiting) {
if (self->waiting != mp_const_none) {
dest[0] = self->waiting;
}
} else if (attr == MP_QSTR_cancel) {
dest[0] = MP_OBJ_FROM_PTR(&task_cancel_obj);
dest[1] = self_in;
} else if (attr == MP_QSTR_ph_key) {
dest[0] = self->ph_key;
}
} else if (dest[1] != MP_OBJ_NULL) {
// Store
if (attr == MP_QSTR_coro) {
self->coro = dest[1];
dest[0] = MP_OBJ_NULL;
} else if (attr == MP_QSTR_data) {
self->data = dest[1];
dest[0] = MP_OBJ_NULL;
} else if (attr == MP_QSTR_waiting) {
self->waiting = dest[1];
dest[0] = MP_OBJ_NULL;
}
}
} |
|
I think I'll have a look to see how store could work. Since there is the |
Yes, that's exactly what I was thinking (and perhaps delete could work by storing |
Below is one way to store (and adding write protection will be easy to add), but I'm not sure I'm familiar enough with the MicroPython codebase to decide what's the best place something like this should go, if anywhere. This version feels like a bit too much duplication with the complementary load step via It also seemed like Quoting from #6253 :
So feel free to defer taking action on this one if you want to give those other areas some more thought first. We can keep our patch on our development fork for now. If you'd like to see this happen in a particular way (or just expand on this attempt) I'm happy to give it a try. diff --git a/py/runtime.c b/py/runtime.c
index 57188c930..527e3029a 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -1157,6 +1157,22 @@ void mp_store_attr(mp_obj_t base, qstr attr, mp_obj_t value) {
return;
}
}
+
+ #if MICROPY_PY_INSTANCE_ATTRS
+ if (type->locals_dict != NULL) {
+ // generic lookup in the object (ie not class or type)
+ assert(type->locals_dict->base.type == &mp_type_dict); // MicroPython restriction, for now
+ mp_map_t *locals_map = &type->locals_dict->map;
+ mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
+ if (elem != NULL && MP_OBJ_IS_TYPE(elem->value, &mp_type_offset)) {
+ // Set instance attribute self->member to x using self.member = x
+ size_t offset = ((offset_obj_t*)MP_OBJ_TO_PTR(elem->value))->offset;
+ *(mp_obj_t*)((char*)MP_OBJ_TO_PTR(base) + offset) = value;
+ return;
+ }
+ }
+ #endif // MICROPY_PY_INSTANCE_ATTRS
+
#if MICROPY_ERROR_REPORTING == MICROPY_ERROR_REPORTING_TERSE
mp_raise_msg(&mp_type_AttributeError, MP_ERROR_TEXT("no such attribute"));
#else |
|
Some further thoughts: It may be possible to dispense with this one entirely and still achieve similar goals after cleaning up #6253. With #6253, it should be possible to make a single generic I'll have a look and report back later this week. |
This includes any boards that have _bleio implemented via HCI. Fixes micropython#4969
|
Implemented in a different way in e2ca8ab |
This provides a simple and lightweight mechanism for getting class instance attributes that are defined in C.
The idea is to simplify the process of making
self->somethinga readable attribute. By adding it to thelocals_dictinstead of writing custom attribute handlers.Below is a minimal example C-defined class that uses this feature. Everything in the example is standard practice. The only new thing at this higher level is the
somethingentry in thelocals_dicttable.This is a simplified example to illustrate how it works; the main practical use case arises when
mp_obj_t somethingis an instance of another class that is also written in C.The offset type in the
locals_dictkeeps the offset to the requested field that corresponds to that attribute. In turn, this is used bymp_convert_member_lookupto get the instance attribute as follows:When called as a class attribute or when called if
self->somethingis not (yet) defined, this returns that the attribute is not found, as expected.It seems that
mp_obj_class_lookupdoes something related, but I wasn't sure if that was suited for this problem. It is also entirely possible such a mechanism already exists and that I just failed to find it. In that case, please feel free to ignore this PR (although a hint to the right approach would be highly appreciated, thanks!)