mirror of
https://github.com/micropython/micropython.git
synced 2026-04-24 18:00:24 +02:00
74e945752b
This adds support for the standard `weakref` module, to make weak references to Python objects and have callbacks for when an object is reclaimed by the GC. This feature was requested by PyScript, to allow control over the lifetime of external proxy objects (distinct from JS<->Python proxies). Addresses issue #646 (that's nearly a 12 year old issue!). Functionality added here: - `weakref.ref(object [, callback])` create a simple weak reference with optional callback to be called when the object is reclaimed by the GC - `weakref.finalize(object, callback, /, *args, **kwargs)` create a finalize object that holds a weak reference to an object and allows more convenient callback usage and state change The new module is enabled at the "everything" level. The implementation aims to be as efficient as possible, by adding another bit-per-block to the garbage collector, the WTB (weak table). Similar to the finalizer bit (FTB), if a GC block has its corresponding WTB bit set then a weak reference to that block is held. The details of that weak reference are stored in a global map, `mp_weakref_map`, which maps weak reference to ref/finalize objects, allowing the callbacks to be efficiently found when the object is reclaimed. With this feature enabled the overhead is: - 1/128th of the available memory is used for the new WTB table (eg a 128k heap now needs an extra 1k for the WTB). - Code size is increased. - At garbage collection time, there is a small overhead to check if the collected objects had weak references. This check is the same as the existing FTB finaliser scan, so shouldn't add much overhead. If there are weak reference objects alive (ref/finalize objects) then additional time is taken to call the callbacks and do some accounting to clean up the used weak reference. Signed-off-by: Damien George <damien@micropython.org>
315 lines
12 KiB
C
315 lines
12 KiB
C
/*
|
|
* This file is part of the MicroPython project, http://micropython.org/
|
|
*
|
|
* The MIT License (MIT)
|
|
*
|
|
* Copyright (c) 2026 Damien P. George
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
* THE SOFTWARE.
|
|
*/
|
|
|
|
#include "py/gc.h"
|
|
#include "py/runtime.h"
|
|
|
|
#if MICROPY_PY_WEAKREF
|
|
|
|
// Macros to obfuscate a heap pointer as a small integer object.
|
|
#define PTR_TO_INT_OBJ(ptr) (MP_OBJ_NEW_SMALL_INT(((uintptr_t)ptr) >> 1))
|
|
#define PTR_FROM_INT_OBJ(obj) ((void *)(MP_OBJ_SMALL_INT_VALUE((obj)) << 1))
|
|
|
|
// Macros to convert between a weak reference and a heap pointer.
|
|
#define WEAK_REFERENCE_FROM_HEAP_PTR(ptr) PTR_TO_INT_OBJ(ptr)
|
|
#define WEAK_REFERENCE_TO_HEAP_PTR(weak_ref) PTR_FROM_INT_OBJ(weak_ref)
|
|
|
|
// Macros to manage ref-finalizer linked-list pointers.
|
|
// - mp_obj_ref_t is obfuscated as a small integer object so it's not traced by the GC.
|
|
// - mp_obj_finalize_t is stored as-is so it is traced by the GC.
|
|
#define REF_FIN_LIST_OBJ_IS_FIN(r) (!mp_obj_is_small_int((r)))
|
|
#define REF_FIN_LIST_OBJ_TO_PTR(r) ((mp_obj_ref_t *)(mp_obj_is_small_int((r)) ? PTR_FROM_INT_OBJ((r)) : MP_OBJ_TO_PTR((r))))
|
|
#define REF_FIN_LIST_OBJ_FROM_REF(r) (PTR_TO_INT_OBJ((r)))
|
|
#define REF_FIN_LIST_OBJ_FROM_FIN(r) (MP_OBJ_FROM_PTR((r)))
|
|
#define REF_FIN_LIST_OBJ_TAIL (PTR_TO_INT_OBJ(NULL))
|
|
|
|
// weakref.ref() instance.
|
|
typedef struct _mp_obj_ref_t {
|
|
mp_obj_base_t base;
|
|
mp_obj_t ref_fin_next;
|
|
mp_obj_t obj_weak_ref;
|
|
mp_obj_t callback;
|
|
} mp_obj_ref_t;
|
|
|
|
// weakref.finalize() instance.
|
|
// This is an extension of weakref.ref() and shares a lot of code with it.
|
|
typedef struct _mp_obj_finalize_t {
|
|
mp_obj_ref_t base;
|
|
size_t n_args;
|
|
size_t n_kw;
|
|
mp_obj_t *args;
|
|
} mp_obj_finalize_t;
|
|
|
|
static const mp_obj_type_t mp_type_ref;
|
|
static const mp_obj_type_t mp_type_finalize;
|
|
|
|
static mp_obj_t ref___del__(mp_obj_t self_in);
|
|
|
|
void gc_weakref_about_to_be_freed(void *ptr) {
|
|
mp_obj_t idx = WEAK_REFERENCE_FROM_HEAP_PTR(ptr);
|
|
mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), idx, MP_MAP_LOOKUP);
|
|
if (elem != NULL) {
|
|
// Mark element as being freed.
|
|
elem->key = mp_const_none;
|
|
}
|
|
}
|
|
|
|
void gc_weakref_sweep(void) {
|
|
mp_map_t *map = &MP_STATE_VM(mp_weakref_map);
|
|
for (size_t i = 0; i < map->alloc; i++) {
|
|
if (map->table[i].key == mp_const_none) {
|
|
// Element was just freed, so call all the registered callbacks.
|
|
--map->used;
|
|
map->table[i].key = MP_OBJ_SENTINEL;
|
|
mp_obj_ref_t *ref = REF_FIN_LIST_OBJ_TO_PTR(map->table[i].value);
|
|
map->table[i].value = MP_OBJ_NULL;
|
|
while (ref != NULL) {
|
|
// Invalidate the weak reference.
|
|
assert(ref->obj_weak_ref != mp_const_none);
|
|
ref->obj_weak_ref = mp_const_none;
|
|
|
|
// Call any registered callbacks.
|
|
if (ref->callback != mp_const_none) {
|
|
nlr_buf_t nlr;
|
|
if (nlr_push(&nlr) == 0) {
|
|
if (ref->base.type == &mp_type_ref) {
|
|
// weakref.ref() type.
|
|
mp_call_function_1(ref->callback, MP_OBJ_FROM_PTR(ref));
|
|
} else {
|
|
// weakref.finalize() type.
|
|
mp_obj_finalize_t *fin = (mp_obj_finalize_t *)ref;
|
|
mp_call_function_n_kw(fin->base.callback, fin->n_args, fin->n_kw, fin->args);
|
|
}
|
|
nlr_pop();
|
|
} else {
|
|
mp_printf(MICROPY_ERROR_PRINTER, "Unhandled exception in weakref callback:\n");
|
|
mp_obj_print_exception(MICROPY_ERROR_PRINTER, MP_OBJ_FROM_PTR(nlr.ret_val));
|
|
}
|
|
}
|
|
|
|
// Unlink the node.
|
|
mp_obj_ref_t *ref_fin_next = REF_FIN_LIST_OBJ_TO_PTR(ref->ref_fin_next);
|
|
ref->ref_fin_next = REF_FIN_LIST_OBJ_TAIL;
|
|
ref = ref_fin_next;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static mp_obj_t mp_obj_ref_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
|
|
if (type == &mp_type_ref) {
|
|
// weakref.ref() type.
|
|
mp_arg_check_num(n_args, n_kw, 1, 2, false);
|
|
} else {
|
|
// weakref.finalize() type.
|
|
mp_arg_check_num(n_args, n_kw, 2, MP_OBJ_FUN_ARGS_MAX, true);
|
|
}
|
|
|
|
// Validate the input object can have a weakref.
|
|
void *ptr = NULL;
|
|
if (mp_obj_is_obj(args[0])) {
|
|
ptr = MP_OBJ_TO_PTR(args[0]);
|
|
if (gc_nbytes(ptr) == 0) {
|
|
ptr = NULL;
|
|
}
|
|
}
|
|
if (ptr == NULL) {
|
|
mp_raise_TypeError(MP_ERROR_TEXT("not a heap object"));
|
|
}
|
|
|
|
// Create or get the entry in mp_weakref_map corresponding to this object.
|
|
mp_obj_t obj_weak_reference = WEAK_REFERENCE_FROM_HEAP_PTR(ptr);
|
|
mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), obj_weak_reference, MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
|
|
if (elem->value == MP_OBJ_NULL) {
|
|
// This heap object does not have any existing weakref's, so initialise it.
|
|
elem->value = REF_FIN_LIST_OBJ_TAIL;
|
|
gc_weakref_mark(ptr);
|
|
}
|
|
|
|
mp_obj_ref_t *self;
|
|
if (type == &mp_type_ref) {
|
|
// Create a new weakref.ref() object.
|
|
self = mp_obj_malloc_with_finaliser(mp_obj_ref_t, type);
|
|
// Link this new ref into the list of all refs/finalizers pointing to this object.
|
|
// To ensure it will *NOT* be traced by the GC (the user must manually hold onto it),
|
|
// store an integer version of the object after any weakref.finalize() objects (so
|
|
// the weakref.finalize() objects continue to be traced by the GC).
|
|
mp_obj_t *link = &elem->value;
|
|
while (REF_FIN_LIST_OBJ_IS_FIN(*link)) {
|
|
link = &REF_FIN_LIST_OBJ_TO_PTR(*link)->ref_fin_next;
|
|
}
|
|
self->ref_fin_next = *link;
|
|
*link = REF_FIN_LIST_OBJ_FROM_REF(self);
|
|
} else {
|
|
// Create a new weakref.finalize() object.
|
|
mp_obj_finalize_t *self_fin = mp_obj_malloc(mp_obj_finalize_t, type);
|
|
self_fin->n_args = n_args - 2;
|
|
self_fin->n_kw = n_kw;
|
|
size_t n_args_kw = self_fin->n_args + self_fin->n_kw * 2;
|
|
if (n_args_kw == 0) {
|
|
self_fin->args = NULL;
|
|
} else {
|
|
self_fin->args = m_new(mp_obj_t, n_args_kw);
|
|
memcpy(self_fin->args, args + 2, n_args_kw * sizeof(mp_obj_t));
|
|
}
|
|
self = &self_fin->base;
|
|
// Link this new finalizer into the list of all refs/finalizers pointing to this object.
|
|
// To ensure it will be traced by the GC, store its pointer at the start of the list.
|
|
self->ref_fin_next = elem->value;
|
|
elem->value = REF_FIN_LIST_OBJ_FROM_FIN(self_fin);
|
|
}
|
|
|
|
// Populate the object weak reference, and the callback.
|
|
self->obj_weak_ref = obj_weak_reference;
|
|
if (n_args > 1) {
|
|
self->callback = args[1];
|
|
} else {
|
|
self->callback = mp_const_none;
|
|
}
|
|
|
|
return MP_OBJ_FROM_PTR(self);
|
|
}
|
|
|
|
static mp_obj_t mp_obj_ref_call(mp_obj_t self_in, size_t n_args, size_t n_kw, const mp_obj_t *args) {
|
|
mp_obj_ref_t *self = MP_OBJ_TO_PTR(self_in);
|
|
if (self->obj_weak_ref == mp_const_none) {
|
|
return mp_const_none;
|
|
}
|
|
if (self->base.type == &mp_type_ref) {
|
|
// weakref.ref() type.
|
|
return MP_OBJ_FROM_PTR(WEAK_REFERENCE_TO_HEAP_PTR(self->obj_weak_ref));
|
|
} else {
|
|
// weakref.finalize() type.
|
|
mp_obj_finalize_t *self_fin = MP_OBJ_TO_PTR(self_in);
|
|
ref___del__(self_in);
|
|
return mp_call_function_n_kw(self_fin->base.callback, self_fin->n_args, self_fin->n_kw, self_fin->args);
|
|
}
|
|
}
|
|
|
|
static mp_obj_t ref___del__(mp_obj_t self_in) {
|
|
mp_obj_ref_t *self = MP_OBJ_TO_PTR(self_in);
|
|
mp_map_elem_t *elem = mp_map_lookup(&MP_STATE_VM(mp_weakref_map), self->obj_weak_ref, MP_MAP_LOOKUP);
|
|
if (elem != NULL) {
|
|
for (mp_obj_t *link = &elem->value; REF_FIN_LIST_OBJ_TO_PTR(*link) != NULL; link = &REF_FIN_LIST_OBJ_TO_PTR(*link)->ref_fin_next) {
|
|
if (self == REF_FIN_LIST_OBJ_TO_PTR(*link)) {
|
|
// Unlink and clear this node.
|
|
*link = self->ref_fin_next;
|
|
self->ref_fin_next = REF_FIN_LIST_OBJ_TAIL;
|
|
self->obj_weak_ref = mp_const_none;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return mp_const_none;
|
|
}
|
|
static MP_DEFINE_CONST_FUN_OBJ_1(ref___del___obj, ref___del__);
|
|
|
|
static mp_obj_t finalize_peek_detach_helper(mp_obj_t self_in, bool detach) {
|
|
mp_obj_finalize_t *self = MP_OBJ_TO_PTR(self_in);
|
|
if (self->base.obj_weak_ref == mp_const_none) {
|
|
return mp_const_none;
|
|
}
|
|
mp_obj_t tuple[4] = {
|
|
MP_OBJ_FROM_PTR(WEAK_REFERENCE_TO_HEAP_PTR(self->base.obj_weak_ref)),
|
|
self->base.callback,
|
|
mp_obj_new_tuple(self->n_args, self->args),
|
|
mp_obj_dict_make_new(&mp_type_dict, 0, self->n_kw, self->args + self->n_args),
|
|
};
|
|
if (detach) {
|
|
ref___del__(self_in);
|
|
}
|
|
return mp_obj_new_tuple(MP_ARRAY_SIZE(tuple), tuple);
|
|
}
|
|
|
|
static mp_obj_t finalize_peek(mp_obj_t self_in) {
|
|
return finalize_peek_detach_helper(self_in, false);
|
|
}
|
|
static MP_DEFINE_CONST_FUN_OBJ_1(finalize_peek_obj, finalize_peek);
|
|
|
|
static mp_obj_t finalize_detach(mp_obj_t self_in) {
|
|
return finalize_peek_detach_helper(self_in, true);
|
|
}
|
|
static MP_DEFINE_CONST_FUN_OBJ_1(finalize_detach_obj, finalize_detach);
|
|
|
|
static void mp_obj_finalize_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
|
|
if (dest[0] != MP_OBJ_NULL) {
|
|
// Store/delete attribute, unsupported.
|
|
return;
|
|
}
|
|
|
|
if (attr == MP_QSTR_alive) {
|
|
mp_obj_finalize_t *self = MP_OBJ_TO_PTR(self_in);
|
|
dest[0] = mp_obj_new_bool(self->base.obj_weak_ref != mp_const_none);
|
|
return;
|
|
} else if (attr == MP_QSTR_peek) {
|
|
dest[0] = MP_OBJ_FROM_PTR(&finalize_peek_obj);
|
|
dest[1] = self_in;
|
|
} else if (attr == MP_QSTR_detach) {
|
|
dest[0] = MP_OBJ_FROM_PTR(&finalize_detach_obj);
|
|
dest[1] = self_in;
|
|
}
|
|
}
|
|
|
|
static const mp_rom_map_elem_t ref_locals_dict_table[] = {
|
|
{ MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&ref___del___obj) },
|
|
};
|
|
static MP_DEFINE_CONST_DICT(ref_locals_dict, ref_locals_dict_table);
|
|
|
|
static MP_DEFINE_CONST_OBJ_TYPE(
|
|
mp_type_ref,
|
|
MP_QSTR_ref,
|
|
MP_TYPE_FLAG_NONE,
|
|
make_new, mp_obj_ref_make_new,
|
|
call, mp_obj_ref_call,
|
|
locals_dict, &ref_locals_dict
|
|
);
|
|
|
|
static MP_DEFINE_CONST_OBJ_TYPE(
|
|
mp_type_finalize,
|
|
MP_QSTR_finalize,
|
|
MP_TYPE_FLAG_NONE,
|
|
make_new, mp_obj_ref_make_new,
|
|
call, mp_obj_ref_call,
|
|
attr, mp_obj_finalize_attr
|
|
);
|
|
|
|
static const mp_rom_map_elem_t mp_module_weakref_globals_table[] = {
|
|
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_weakref) },
|
|
{ MP_ROM_QSTR(MP_QSTR_ref), MP_ROM_PTR(&mp_type_ref) },
|
|
{ MP_ROM_QSTR(MP_QSTR_finalize), MP_ROM_PTR(&mp_type_finalize) },
|
|
};
|
|
static MP_DEFINE_CONST_DICT(mp_module_weakref_globals, mp_module_weakref_globals_table);
|
|
|
|
const mp_obj_module_t mp_module_weakref = {
|
|
.base = { &mp_type_module },
|
|
.globals = (mp_obj_dict_t *)&mp_module_weakref_globals,
|
|
};
|
|
|
|
MP_REGISTER_ROOT_POINTER(mp_map_t mp_weakref_map);
|
|
MP_REGISTER_MODULE(MP_QSTR_weakref, mp_module_weakref);
|
|
|
|
#endif // MICROPY_PY_WEAKREF
|