Skip to content

Commit 74e9457

Browse files
committed
py/modweakref: Implement weakref module with ref and finalize classes.
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 micropython#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>
1 parent 803a4d7 commit 74e9457

9 files changed

Lines changed: 433 additions & 20 deletions

File tree

py/gc.c

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,21 @@
104104
#if MICROPY_ENABLE_FINALISER
105105
// FTB = finaliser table byte
106106
// if set, then the corresponding block may have a finaliser
107-
108107
#define BLOCKS_PER_FTB (8)
109-
110108
#define FTB_GET(area, block) ((area->gc_finaliser_table_start[(block) / BLOCKS_PER_FTB] >> ((block) & 7)) & 1)
111109
#define FTB_SET(area, block) do { area->gc_finaliser_table_start[(block) / BLOCKS_PER_FTB] |= (1 << ((block) & 7)); } while (0)
112110
#define FTB_CLEAR(area, block) do { area->gc_finaliser_table_start[(block) / BLOCKS_PER_FTB] &= (~(1 << ((block) & 7))); } while (0)
113111
#endif
114112

113+
#if MICROPY_PY_WEAKREF
114+
// WTB = weakref table byte
115+
// if set, then the corresponding block may have a weakref in MP_STATE_VM(mp_weakref_map).
116+
#define BLOCKS_PER_WTB (8)
117+
#define WTB_GET(area, block) ((area->gc_weakref_table_start[(block) / BLOCKS_PER_WTB] >> ((block) & 7)) & 1)
118+
#define WTB_SET(area, block) do { area->gc_weakref_table_start[(block) / BLOCKS_PER_WTB] |= (1 << ((block) & 7)); } while (0)
119+
#define WTB_CLEAR(area, block) do { area->gc_weakref_table_start[(block) / BLOCKS_PER_WTB] &= (~(1 << ((block) & 7))); } while (0)
120+
#endif
121+
115122
#if MICROPY_PY_THREAD && !MICROPY_PY_THREAD_GIL
116123
#define GC_MUTEX_INIT() mp_thread_recursive_mutex_init(&MP_STATE_MEM(gc_mutex))
117124
#define GC_ENTER() mp_thread_recursive_mutex_lock(&MP_STATE_MEM(gc_mutex), 1)
@@ -138,17 +145,23 @@ static void gc_sweep_free_blocks(void);
138145
// TODO waste less memory; currently requires that all entries in alloc_table have a corresponding block in pool
139146
static void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) {
140147
// calculate parameters for GC (T=total, A=alloc table, F=finaliser table, P=pool; all in bytes):
141-
// T = A + F + P
148+
// T = A + F + W + P
142149
// F = A * BLOCKS_PER_ATB / BLOCKS_PER_FTB
150+
// W = A * BLOCKS_PER_ATB / BLOCKS_PER_WTB
143151
// P = A * BLOCKS_PER_ATB * BYTES_PER_BLOCK
144-
// => T = A * (1 + BLOCKS_PER_ATB / BLOCKS_PER_FTB + BLOCKS_PER_ATB * BYTES_PER_BLOCK)
152+
// => T = A * (1 + BLOCKS_PER_ATB / BLOCKS_PER_FTB + BLOCKS_PER_ATB / BLOCKS_PER_WTB + BLOCKS_PER_ATB * BYTES_PER_BLOCK)
145153
size_t total_byte_len = (byte *)end - (byte *)start;
146-
#if MICROPY_ENABLE_FINALISER
154+
#if MICROPY_ENABLE_FINALISER || MICROPY_PY_WEAKREF
147155
area->gc_alloc_table_byte_len = (total_byte_len - ALLOC_TABLE_GAP_BYTE)
148156
* MP_BITS_PER_BYTE
149157
/ (
150158
MP_BITS_PER_BYTE
159+
#if MICROPY_ENABLE_FINALISER
151160
+ MP_BITS_PER_BYTE * BLOCKS_PER_ATB / BLOCKS_PER_FTB
161+
#endif
162+
#if MICROPY_PY_WEAKREF
163+
+ MP_BITS_PER_BYTE * BLOCKS_PER_ATB / BLOCKS_PER_WTB
164+
#endif
152165
+ MP_BITS_PER_BYTE * BLOCKS_PER_ATB * BYTES_PER_BLOCK
153166
);
154167
#else
@@ -157,26 +170,36 @@ static void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) {
157170

158171
area->gc_alloc_table_start = (byte *)start;
159172

173+
// Allocate FTB and WTB blocks if they are enabled.
174+
byte *next_table = area->gc_alloc_table_start + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE;
175+
(void)next_table;
160176
#if MICROPY_ENABLE_FINALISER
161177
size_t gc_finaliser_table_byte_len = (area->gc_alloc_table_byte_len * BLOCKS_PER_ATB + BLOCKS_PER_FTB - 1) / BLOCKS_PER_FTB;
162-
area->gc_finaliser_table_start = area->gc_alloc_table_start + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE;
178+
area->gc_finaliser_table_start = next_table;
179+
next_table += gc_finaliser_table_byte_len;
180+
#endif
181+
#if MICROPY_PY_WEAKREF
182+
size_t gc_weakref_table_byte_len = (area->gc_alloc_table_byte_len * BLOCKS_PER_ATB + BLOCKS_PER_WTB - 1) / BLOCKS_PER_WTB;
183+
area->gc_weakref_table_start = next_table;
184+
next_table += gc_weakref_table_byte_len;
163185
#endif
164186

187+
// Allocate the GC pool of heap blocks.
165188
size_t gc_pool_block_len = area->gc_alloc_table_byte_len * BLOCKS_PER_ATB;
166189
area->gc_pool_start = (byte *)end - gc_pool_block_len * BYTES_PER_BLOCK;
167190
area->gc_pool_end = end;
191+
assert(area->gc_pool_start >= next_table);
168192

169-
#if MICROPY_ENABLE_FINALISER
170-
assert(area->gc_pool_start >= area->gc_finaliser_table_start + gc_finaliser_table_byte_len);
171-
#endif
172-
173-
#if MICROPY_ENABLE_FINALISER
174-
// clear ATB's and FTB's
175-
memset(area->gc_alloc_table_start, 0, gc_finaliser_table_byte_len + area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE);
176-
#else
177-
// clear ATB's
178-
memset(area->gc_alloc_table_start, 0, area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE);
179-
#endif
193+
// Clear ATB's, and FTB's and WTB's if they are enabled.
194+
memset(area->gc_alloc_table_start, 0,
195+
area->gc_alloc_table_byte_len + ALLOC_TABLE_GAP_BYTE
196+
#if MICROPY_ENABLE_FINALISER
197+
+ gc_finaliser_table_byte_len
198+
#endif
199+
#if MICROPY_PY_WEAKREF
200+
+ gc_weakref_table_byte_len
201+
#endif
202+
);
180203

181204
area->gc_last_free_atb_index = 0;
182205
area->gc_last_used_block = 0;
@@ -196,6 +219,12 @@ static void gc_setup_area(mp_state_mem_area_t *area, void *start, void *end) {
196219
gc_finaliser_table_byte_len,
197220
gc_finaliser_table_byte_len * BLOCKS_PER_FTB);
198221
#endif
222+
#if MICROPY_PY_WEAKREF
223+
DEBUG_printf(" weakref table at %p, length " UINT_FMT " bytes, "
224+
UINT_FMT " blocks\n", area->gc_weakref_table_start,
225+
gc_weakref_table_byte_len,
226+
gc_weakref_table_byte_len * BLOCKS_PER_WTB);
227+
#endif
199228
DEBUG_printf(" pool at %p, length " UINT_FMT " bytes, "
200229
UINT_FMT " blocks\n", area->gc_pool_start,
201230
gc_pool_block_len * BYTES_PER_BLOCK, gc_pool_block_len);
@@ -310,6 +339,9 @@ static bool gc_try_add_heap(size_t failed_alloc) {
310339
#if MICROPY_ENABLE_FINALISER
311340
+ total_blocks / BLOCKS_PER_FTB
312341
#endif
342+
#if MICROPY_PY_WEAKREF
343+
+ total_blocks / BLOCKS_PER_WTB
344+
#endif
313345
+ total_blocks * BYTES_PER_BLOCK
314346
+ ALLOC_TABLE_GAP_BYTE
315347
+ sizeof(mp_state_mem_area_t);
@@ -556,6 +588,9 @@ void gc_collect_end(void) {
556588
}
557589
MP_STATE_THREAD(gc_lock_depth) &= ~GC_COLLECT_FLAG;
558590
GC_EXIT();
591+
#if MICROPY_PY_WEAKREF
592+
gc_weakref_sweep();
593+
#endif
559594
}
560595

561596
static void gc_deal_with_stack_overflow(void) {
@@ -581,12 +616,16 @@ static void gc_deal_with_stack_overflow(void) {
581616

582617
// Run finalisers for all to-be-freed blocks
583618
static void gc_sweep_run_finalisers(void) {
584-
#if MICROPY_ENABLE_FINALISER
619+
#if MICROPY_ENABLE_FINALISER || MICROPY_PY_WEAKREF
620+
#if MICROPY_ENABLE_FINALISER && MICROPY_PY_WEAKREF
621+
MP_STATIC_ASSERT(BLOCKS_PER_FTB == BLOCKS_PER_WTB);
622+
#endif
585623
for (const mp_state_mem_area_t *area = &MP_STATE_MEM(area); area != NULL; area = NEXT_AREA(area)) {
586624
assert(area->gc_last_used_block <= area->gc_alloc_table_byte_len * BLOCKS_PER_ATB);
587625
// Small speed optimisation: skip over empty FTB blocks
588626
size_t ftb_end = area->gc_last_used_block / BLOCKS_PER_FTB; // index is inclusive
589627
for (size_t ftb_idx = 0; ftb_idx <= ftb_end; ftb_idx++) {
628+
#if MICROPY_ENABLE_FINALISER
590629
byte ftb = area->gc_finaliser_table_start[ftb_idx];
591630
size_t block = ftb_idx * BLOCKS_PER_FTB;
592631
while (ftb) {
@@ -616,9 +655,26 @@ static void gc_sweep_run_finalisers(void) {
616655
ftb >>= 1;
617656
block++;
618657
}
658+
#endif
659+
#if MICROPY_PY_WEAKREF
660+
byte wtb = area->gc_weakref_table_start[ftb_idx];
661+
block = ftb_idx * BLOCKS_PER_WTB;
662+
while (wtb) {
663+
MICROPY_GC_HOOK_LOOP(block);
664+
if (wtb & 1) { // WTB_GET(area, block) shortcut
665+
if (ATB_GET_KIND(area, block) == AT_HEAD) {
666+
mp_obj_base_t *obj = (mp_obj_base_t *)PTR_FROM_BLOCK(area, block);
667+
gc_weakref_about_to_be_freed(obj);
668+
WTB_CLEAR(area, block);
669+
}
670+
}
671+
wtb >>= 1;
672+
block++;
673+
}
674+
#endif
619675
}
620676
}
621-
#endif // MICROPY_ENABLE_FINALISER
677+
#endif // MICROPY_ENABLE_FINALISER || MICROPY_PY_WEAKREF
622678
}
623679

624680
// Free unmarked heads and their tails
@@ -769,6 +825,25 @@ void gc_info(gc_info_t *info) {
769825
GC_EXIT();
770826
}
771827

828+
#if MICROPY_PY_WEAKREF
829+
// Mark the GC heap pointer as having a weakref.
830+
void gc_weakref_mark(void *ptr) {
831+
mp_state_mem_area_t *area;
832+
#if MICROPY_GC_SPLIT_HEAP
833+
area = gc_get_ptr_area(ptr);
834+
assert(area);
835+
#else
836+
assert(VERIFY_PTR(ptr));
837+
area = &MP_STATE_MEM(area);
838+
#endif
839+
840+
size_t block = BLOCK_FROM_PTR(area, ptr);
841+
assert(ATB_GET_KIND(area, block) == AT_HEAD);
842+
843+
WTB_SET(area, block);
844+
}
845+
#endif
846+
772847
void *gc_alloc(size_t n_bytes, unsigned int alloc_flags) {
773848
bool has_finaliser = alloc_flags & GC_ALLOC_FLAG_HAS_FINALISER;
774849
size_t n_blocks = ((n_bytes + BYTES_PER_BLOCK - 1) & (~(BYTES_PER_BLOCK - 1))) / BYTES_PER_BLOCK;
@@ -967,6 +1042,11 @@ void gc_free(void *ptr) {
9671042
FTB_CLEAR(area, block);
9681043
#endif
9691044

1045+
#if MICROPY_PY_WEAKREF
1046+
// Objects that have a weak reference should not be explicitly freed.
1047+
assert(!WTB_GET(area, block));
1048+
#endif
1049+
9701050
#if MICROPY_GC_SPLIT_HEAP
9711051
if (MP_STATE_MEM(gc_last_free_area) != area) {
9721052
// We freed something but it isn't the current area. Reset the

py/gc.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ void gc_collect_end(void);
5858
// Use this function to sweep the whole heap and run all finalisers
5959
void gc_sweep_all(void);
6060

61+
// These functions are used to manage weakrefs.
62+
void gc_weakref_mark(void *ptr);
63+
void gc_weakref_about_to_be_freed(void *ptr);
64+
void gc_weakref_sweep(void);
65+
6166
enum {
6267
GC_ALLOC_FLAG_HAS_FINALISER = 1,
6368
};

0 commit comments

Comments
 (0)