-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy patharena.c
More file actions
230 lines (204 loc) · 8.02 KB
/
arena.c
File metadata and controls
230 lines (204 loc) · 8.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdio.h>
/* Arena allocator: boundary-tag free-list allocator inside a Go-allocated pool.
*
* TinyGo's malloc IS the Go GC allocator (picolibc's malloc/sbrk are not
* compiled). The WiFi blob stores allocated pointers in ROM BSS which the
* GC cannot scan, so individual malloc'd objects get collected → crash.
*
* Solution: Go allocates one large []byte (kept alive by a package-level
* variable) and passes its backing array here. All blob allocations are
* sub-allocated from this pool using a first-fit free-list with boundary
* tags for O(1) coalescing on free.
*
* Block layout:
* [header: size_t] payload ... [footer: size_t]
* The size field = total block size (header+payload+footer).
* Low bit of header = 1 means allocated, 0 means free.
* Free blocks additionally store a next pointer in the first sizeof(void*)
* bytes of the payload area.
*
* Minimum block size = HDR + max(sizeof(void*), ALIGN) + FTR.
*/
#define ALIGN 8
#define HDR sizeof(size_t)
#define FTR sizeof(size_t)
#define OVERHEAD (HDR + FTR)
#define MIN_PAYLOAD (sizeof(void *) > ALIGN ? sizeof(void *) : ALIGN)
#define MIN_BLOCK (OVERHEAD + MIN_PAYLOAD)
#define ALLOC_BIT ((size_t)1)
/* Access helpers */
#define BLK_HDR(b) (*(size_t *)(b))
#define BLK_SIZE(b) (BLK_HDR(b) & ~ALLOC_BIT)
#define BLK_ALLOC(b) (BLK_HDR(b) & ALLOC_BIT)
#define BLK_FTR(b) (*(size_t *)((uint8_t *)(b) + BLK_SIZE(b) - FTR))
#define BLK_PAYLOAD(b) ((void *)((uint8_t *)(b) + HDR))
#define PAYLOAD_BLK(p) ((uint8_t *)(p) - HDR)
#define BLK_NEXT(b) ((uint8_t *)((uint8_t *)(b) + BLK_SIZE(b)))
/* Free-list next pointer stored at start of payload */
#define FREE_NEXT(b) (*(uint8_t **)((uint8_t *)(b) + HDR))
static uint8_t *arena_base;
static size_t arena_cap;
static uint8_t *free_list; /* singly-linked list of free blocks */
#if 0
#define ARENA_DBG(...) printf(__VA_ARGS__)
#else
#define ARENA_DBG(...) ((void)0)
#endif
void espradio_arena_init(uint8_t *base, size_t cap) {
arena_base = base;
arena_cap = cap;
/* Align base forward */
uintptr_t misalign = ((uintptr_t)base) & (ALIGN - 1);
if (misalign) {
size_t pad = ALIGN - misalign;
base += pad;
cap -= pad;
}
/* Round cap down to alignment */
cap &= ~(size_t)(ALIGN - 1);
arena_base = base;
arena_cap = cap;
/* Initialise entire pool as one free block */
BLK_HDR(base) = cap; /* size, ALLOC_BIT=0 → free */
BLK_FTR(base) = cap;
FREE_NEXT(base) = NULL;
free_list = base;
}
void *espradio_arena_alloc(size_t size) {
if (size == 0) size = 1;
/* Round payload up to alignment */
size = (size + ALIGN - 1) & ~(size_t)(ALIGN - 1);
size_t need = size + OVERHEAD;
if (need < MIN_BLOCK) need = MIN_BLOCK;
/* First-fit search */
uint8_t **prev = &free_list;
uint8_t *blk = free_list;
while (blk) {
size_t bsz = BLK_SIZE(blk);
if (bsz >= need) {
/* Try to split */
size_t rem = bsz - need;
if (rem >= MIN_BLOCK) {
/* Shrink this free block, allocate from the end */
BLK_HDR(blk) = rem; /* free block is now smaller */
BLK_FTR(blk) = rem;
uint8_t *alloc_blk = blk + rem;
BLK_HDR(alloc_blk) = need | ALLOC_BIT;
BLK_FTR(alloc_blk) = need | ALLOC_BIT;
ARENA_DBG("arena: alloc %zu @ %p (split, free_rem=%zu)\n",
size, BLK_PAYLOAD(alloc_blk), rem);
return BLK_PAYLOAD(alloc_blk);
} else {
/* Use entire block */
*prev = FREE_NEXT(blk);
BLK_HDR(blk) = bsz | ALLOC_BIT;
BLK_FTR(blk) = bsz | ALLOC_BIT;
ARENA_DBG("arena: alloc %zu @ %p (exact %zu)\n",
size, BLK_PAYLOAD(blk), bsz);
return BLK_PAYLOAD(blk);
}
}
prev = (uint8_t **)((uint8_t *)blk + HDR); /* &FREE_NEXT(blk) */
blk = FREE_NEXT(blk);
}
ARENA_DBG("arena: alloc %zu FAILED (OOM)\n", size);
return NULL;
}
void *espradio_arena_calloc(size_t n, size_t size) {
size_t total = n * size;
void *p = espradio_arena_alloc(total);
if (p) memset(p, 0, total);
return p;
}
void espradio_arena_free(void *p) {
if (!p) return;
uint8_t *blk = PAYLOAD_BLK(p);
/* Reject pointers outside the arena pool. With --wrap=free on
* Xtensa, ALL free() calls arrive here — including frees of
* non-arena memory (picolibc internals, ROM buffers, etc.).
* Without this check the coalescing code below would interpret
* random memory as block headers and corrupt the Go heap. */
if (blk < arena_base || blk >= arena_base + arena_cap) {
ARENA_DBG("arena: free %p OUTSIDE pool [%p, %p) — ignored\n",
p, arena_base, arena_base + arena_cap);
return;
}
size_t bsz = BLK_SIZE(blk);
/* Sanity-check the block header. */
if (bsz == 0 || bsz > arena_cap || blk + bsz > arena_base + arena_cap) {
ARENA_DBG("arena: free %p corrupt header (bsz=%zu) — ignored\n", p, bsz);
return;
}
ARENA_DBG("arena: free %p (blk %p, size %zu)\n", p, blk, bsz);
/* Coalesce with next block */
uint8_t *next = blk + bsz;
if (next < arena_base + arena_cap && !BLK_ALLOC(next)) {
/* Remove next from free list */
uint8_t **pp = &free_list;
while (*pp && *pp != next) pp = (uint8_t **)(*pp + HDR);
if (*pp == next) *pp = FREE_NEXT(next);
bsz += BLK_SIZE(next);
}
/* Coalesce with previous block */
if (blk > arena_base) {
size_t prev_sz = *(size_t *)(blk - FTR) & ~ALLOC_BIT;
uint8_t *prev_blk = blk - prev_sz;
if (prev_blk >= arena_base && !BLK_ALLOC(prev_blk)) {
/* Remove prev from free list */
uint8_t **pp = &free_list;
while (*pp && *pp != prev_blk) pp = (uint8_t **)(*pp + HDR);
if (*pp == prev_blk) *pp = FREE_NEXT(prev_blk);
blk = prev_blk;
bsz += BLK_SIZE(prev_blk);
}
}
/* Mark free and add to front of free list */
BLK_HDR(blk) = bsz; /* ALLOC_BIT=0 */
BLK_FTR(blk) = bsz;
FREE_NEXT(blk) = free_list;
free_list = blk;
}
void *espradio_arena_realloc(void *ptr, size_t new_size) {
if (!ptr) return espradio_arena_alloc(new_size);
if (new_size == 0) { espradio_arena_free(ptr); return NULL; }
uint8_t *blk = PAYLOAD_BLK(ptr);
/* Reject non-arena pointers (see espradio_arena_free comment). */
if (blk < arena_base || blk >= arena_base + arena_cap) {
return espradio_arena_alloc(new_size);
}
size_t old_total = BLK_SIZE(blk);
if (old_total == 0 || old_total > arena_cap || blk + old_total > arena_base + arena_cap) {
return espradio_arena_alloc(new_size);
}
size_t old_payload = old_total - OVERHEAD;
void *np = espradio_arena_alloc(new_size);
if (np) {
memcpy(np, ptr, old_payload < new_size ? old_payload : new_size);
espradio_arena_free(ptr);
}
return np;
}
void espradio_arena_stats(uint32_t *used, uint32_t *capacity) {
/* Walk all blocks to compute used bytes */
uint32_t u = 0;
uint8_t *blk = arena_base;
uint8_t *end = arena_base + arena_cap;
while (blk < end) {
size_t bsz = BLK_SIZE(blk);
if (bsz == 0 || bsz > (size_t)(end - blk)) break;
if (BLK_ALLOC(blk)) u += (uint32_t)bsz;
blk += bsz;
}
if (used) *used = u;
if (capacity) *capacity = (uint32_t)arena_cap;
}
#ifdef __XTENSA__
/* --wrap redirects (ESP32-S3): catch direct malloc calls from blob .a */
void *__wrap_malloc(size_t size) { return espradio_arena_alloc(size); }
void *__wrap_calloc(size_t n, size_t size) { return espradio_arena_calloc(n, size); }
void __wrap_free(void *p) { espradio_arena_free(p); }
void *__wrap_realloc(void *p, size_t size) { return espradio_arena_realloc(p, size); }
#endif