/src/wasm3/source/m3_core.c
Line | Count | Source |
1 | | // |
2 | | // m3_core.c |
3 | | // |
4 | | // Created by Steven Massey on 4/15/19. |
5 | | // Copyright © 2019 Steven Massey. All rights reserved. |
6 | | // |
7 | | |
8 | | #define M3_IMPLEMENT_ERROR_STRINGS |
9 | | #include "m3_config.h" |
10 | | #include "wasm3.h" |
11 | | |
12 | | #include "m3_core.h" |
13 | | #include "m3_env.h" |
14 | | |
15 | 0 | void m3_Abort(const char* message) { |
16 | | #ifdef DEBUG |
17 | | fprintf(stderr, "Error: %s\n", message); |
18 | | #endif |
19 | 0 | abort(); |
20 | 0 | } |
21 | | |
22 | | M3_WEAK |
23 | | M3Result m3_Yield () |
24 | 0 | { |
25 | 0 | return m3Err_none; |
26 | 0 | } |
27 | | |
28 | | #if d_m3LogTimestamps |
29 | | |
30 | | #include <time.h> |
31 | | |
32 | | #define SEC_TO_US(sec) ((sec)*1000000) |
33 | | #define NS_TO_US(ns) ((ns)/1000) |
34 | | |
35 | | static uint64_t initial_ts = -1; |
36 | | |
37 | | uint64_t m3_GetTimestamp() |
38 | | { |
39 | | if (initial_ts == -1) { |
40 | | initial_ts = 0; |
41 | | initial_ts = m3_GetTimestamp(); |
42 | | } |
43 | | struct timespec ts; |
44 | | timespec_get(&ts, TIME_UTC); |
45 | | uint64_t us = SEC_TO_US((uint64_t)ts.tv_sec) + NS_TO_US((uint64_t)ts.tv_nsec); |
46 | | return us - initial_ts; |
47 | | } |
48 | | |
49 | | #endif |
50 | | |
51 | | #if d_m3FixedHeap |
52 | | |
53 | | static u8 fixedHeap[d_m3FixedHeap]; |
54 | | static u8* fixedHeapPtr = fixedHeap; |
55 | | static u8* const fixedHeapEnd = fixedHeap + d_m3FixedHeap; |
56 | | static u8* fixedHeapLast = NULL; |
57 | | |
58 | | #if d_m3FixedHeapAlign > 1 |
59 | | # define HEAP_ALIGN_PTR(P) P = (u8*)(((size_t)(P)+(d_m3FixedHeapAlign-1)) & ~ (d_m3FixedHeapAlign-1)); |
60 | | #else |
61 | | # define HEAP_ALIGN_PTR(P) |
62 | | #endif |
63 | | |
64 | | void * m3_Malloc_Impl (size_t i_size) |
65 | | { |
66 | | u8 * ptr = fixedHeapPtr; |
67 | | |
68 | | fixedHeapPtr += i_size; |
69 | | HEAP_ALIGN_PTR(fixedHeapPtr); |
70 | | |
71 | | if (fixedHeapPtr >= fixedHeapEnd) |
72 | | { |
73 | | return NULL; |
74 | | } |
75 | | |
76 | | memset (ptr, 0x0, i_size); |
77 | | fixedHeapLast = ptr; |
78 | | |
79 | | return ptr; |
80 | | } |
81 | | |
82 | | void m3_Free_Impl (void * i_ptr) |
83 | | { |
84 | | // Handle the last chunk |
85 | | if (i_ptr && i_ptr == fixedHeapLast) { |
86 | | fixedHeapPtr = fixedHeapLast; |
87 | | fixedHeapLast = NULL; |
88 | | } else { |
89 | | //printf("== free %p [failed]\n", io_ptr); |
90 | | } |
91 | | } |
92 | | |
93 | | void * m3_Realloc_Impl (void * i_ptr, size_t i_newSize, size_t i_oldSize) |
94 | | { |
95 | | if (M3_UNLIKELY(i_newSize == i_oldSize)) return i_ptr; |
96 | | |
97 | | void * newPtr; |
98 | | |
99 | | // Handle the last chunk |
100 | | if (i_ptr && i_ptr == fixedHeapLast) { |
101 | | fixedHeapPtr = fixedHeapLast + i_newSize; |
102 | | HEAP_ALIGN_PTR(fixedHeapPtr); |
103 | | if (fixedHeapPtr >= fixedHeapEnd) |
104 | | { |
105 | | return NULL; |
106 | | } |
107 | | newPtr = i_ptr; |
108 | | } else { |
109 | | newPtr = m3_Malloc_Impl(i_newSize); |
110 | | if (!newPtr) { |
111 | | return NULL; |
112 | | } |
113 | | if (i_ptr) { |
114 | | memcpy(newPtr, i_ptr, i_oldSize); |
115 | | } |
116 | | } |
117 | | |
118 | | if (i_newSize > i_oldSize) { |
119 | | memset ((u8 *) newPtr + i_oldSize, 0x0, i_newSize - i_oldSize); |
120 | | } |
121 | | |
122 | | return newPtr; |
123 | | } |
124 | | |
125 | | #else |
126 | | |
127 | | void * m3_Malloc_Impl (size_t i_size) |
128 | 15.1k | { |
129 | 15.1k | return calloc (i_size, 1); |
130 | 15.1k | } |
131 | | |
132 | | void m3_Free_Impl (void * io_ptr) |
133 | 29.3k | { |
134 | 29.3k | free (io_ptr); |
135 | 29.3k | } |
136 | | |
137 | | void * m3_Realloc_Impl (void * i_ptr, size_t i_newSize, size_t i_oldSize) |
138 | 1.18k | { |
139 | 1.18k | if (M3_UNLIKELY(i_newSize == i_oldSize)) return i_ptr; |
140 | | |
141 | 1.18k | void * newPtr = realloc (i_ptr, i_newSize); |
142 | | |
143 | 1.18k | if (M3_LIKELY(newPtr)) |
144 | 1.18k | { |
145 | 1.18k | if (i_newSize > i_oldSize) { |
146 | 1.18k | memset ((u8 *) newPtr + i_oldSize, 0x0, i_newSize - i_oldSize); |
147 | 1.18k | } |
148 | 1.18k | return newPtr; |
149 | 1.18k | } |
150 | 0 | return NULL; |
151 | 1.18k | } |
152 | | |
153 | | #endif |
154 | | |
155 | | void * m3_CopyMem (const void * i_from, size_t i_size) |
156 | 1 | { |
157 | 1 | void * ptr = m3_Malloc("CopyMem", i_size); |
158 | 1 | if (ptr) { |
159 | 1 | memcpy (ptr, i_from, i_size); |
160 | 1 | } |
161 | 1 | return ptr; |
162 | 1 | } |
163 | | |
164 | | //-------------------------------------------------------------------------------------------- |
165 | | |
166 | | #if d_m3LogNativeStack |
167 | | |
168 | | static size_t stack_start; |
169 | | static size_t stack_end; |
170 | | |
171 | | void m3StackCheckInit () |
172 | | { |
173 | | char stack; |
174 | | stack_end = stack_start = (size_t)&stack; |
175 | | } |
176 | | |
177 | | void m3StackCheck () |
178 | | { |
179 | | char stack; |
180 | | size_t addr = (size_t)&stack; |
181 | | |
182 | | size_t stackEnd = stack_end; |
183 | | stack_end = M3_MIN (stack_end, addr); |
184 | | |
185 | | // if (stackEnd != stack_end) |
186 | | // printf ("maxStack: %ld\n", m3StackGetMax ()); |
187 | | } |
188 | | |
189 | | int m3StackGetMax () |
190 | | { |
191 | | return stack_start - stack_end; |
192 | | } |
193 | | |
194 | | #endif |
195 | | |
196 | | //-------------------------------------------------------------------------------------------- |
197 | | |
198 | | M3Result NormalizeType (u8 * o_type, i8 i_convolutedWasmType) |
199 | 5.26k | { |
200 | 5.26k | M3Result result = m3Err_none; |
201 | | |
202 | 5.26k | u8 type = -i_convolutedWasmType; |
203 | | |
204 | 5.26k | if (type == 0x40) |
205 | 1.86k | type = c_m3Type_none; |
206 | 3.40k | else if (type < c_m3Type_i32 or type > c_m3Type_f64) |
207 | 6 | result = m3Err_invalidTypeId; |
208 | | |
209 | 5.26k | * o_type = type; |
210 | | |
211 | 5.26k | return result; |
212 | 5.26k | } |
213 | | |
214 | | |
215 | | bool IsFpType (u8 i_m3Type) |
216 | 2.87k | { |
217 | 2.87k | return (i_m3Type == c_m3Type_f32 or i_m3Type == c_m3Type_f64); |
218 | 2.87k | } |
219 | | |
220 | | |
221 | | bool IsIntType (u8 i_m3Type) |
222 | 166 | { |
223 | 166 | return (i_m3Type == c_m3Type_i32 or i_m3Type == c_m3Type_i64); |
224 | 166 | } |
225 | | |
226 | | |
227 | | bool Is64BitType (u8 i_m3Type) |
228 | 545k | { |
229 | 545k | if (i_m3Type == c_m3Type_i64 or i_m3Type == c_m3Type_f64) |
230 | 157k | return true; |
231 | 388k | else if (i_m3Type == c_m3Type_i32 or i_m3Type == c_m3Type_f32 or i_m3Type == c_m3Type_none) |
232 | 388k | return false; |
233 | 149 | else |
234 | 149 | return (sizeof (voidptr_t) == 8); // all other cases are pointers |
235 | 545k | } |
236 | | |
237 | | u32 SizeOfType (u8 i_m3Type) |
238 | 202 | { |
239 | 202 | if (i_m3Type == c_m3Type_i32 or i_m3Type == c_m3Type_f32) |
240 | 12 | return sizeof (i32); |
241 | | |
242 | 190 | return sizeof (i64); |
243 | 202 | } |
244 | | |
245 | | |
246 | | //-- Binary Wasm parsing utils ------------------------------------------------------------------------------------------ |
247 | | |
248 | | |
249 | | M3Result Read_u64 (u64 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
250 | 0 | { |
251 | 0 | const u8 * ptr = * io_bytes; |
252 | 0 | ptr += sizeof (u64); |
253 | |
|
254 | 0 | if (ptr <= i_end) |
255 | 0 | { |
256 | 0 | memcpy(o_value, * io_bytes, sizeof(u64)); |
257 | 0 | M3_BSWAP_u64(*o_value); |
258 | 0 | * io_bytes = ptr; |
259 | 0 | return m3Err_none; |
260 | 0 | } |
261 | 0 | else return m3Err_wasmUnderrun; |
262 | 0 | } |
263 | | |
264 | | |
265 | | M3Result Read_u32 (u32 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
266 | 338 | { |
267 | 338 | const u8 * ptr = * io_bytes; |
268 | 338 | ptr += sizeof (u32); |
269 | | |
270 | 338 | if (ptr <= i_end) |
271 | 338 | { |
272 | 338 | memcpy(o_value, * io_bytes, sizeof(u32)); |
273 | 338 | M3_BSWAP_u32(*o_value); |
274 | 338 | * io_bytes = ptr; |
275 | 338 | return m3Err_none; |
276 | 338 | } |
277 | 0 | else return m3Err_wasmUnderrun; |
278 | 338 | } |
279 | | |
280 | | #if d_m3ImplementFloat |
281 | | |
282 | | M3Result Read_f64 (f64 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
283 | 304 | { |
284 | 304 | const u8 * ptr = * io_bytes; |
285 | 304 | ptr += sizeof (f64); |
286 | | |
287 | 304 | if (ptr <= i_end) |
288 | 304 | { |
289 | 304 | memcpy(o_value, * io_bytes, sizeof(f64)); |
290 | 304 | M3_BSWAP_f64(*o_value); |
291 | 304 | * io_bytes = ptr; |
292 | 304 | return m3Err_none; |
293 | 304 | } |
294 | 0 | else return m3Err_wasmUnderrun; |
295 | 304 | } |
296 | | |
297 | | |
298 | | M3Result Read_f32 (f32 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
299 | 454 | { |
300 | 454 | const u8 * ptr = * io_bytes; |
301 | 454 | ptr += sizeof (f32); |
302 | | |
303 | 454 | if (ptr <= i_end) |
304 | 454 | { |
305 | 454 | memcpy(o_value, * io_bytes, sizeof(f32)); |
306 | 454 | M3_BSWAP_f32(*o_value); |
307 | 454 | * io_bytes = ptr; |
308 | 454 | return m3Err_none; |
309 | 454 | } |
310 | 0 | else return m3Err_wasmUnderrun; |
311 | 454 | } |
312 | | |
313 | | #endif |
314 | | |
315 | | M3Result Read_u8 (u8 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
316 | 5.34k | { |
317 | 5.34k | const u8 * ptr = * io_bytes; |
318 | | |
319 | 5.34k | if (ptr < i_end) |
320 | 5.34k | { |
321 | 5.34k | * o_value = * ptr; |
322 | 5.34k | * io_bytes = ptr + 1; |
323 | | |
324 | 5.34k | return m3Err_none; |
325 | 5.34k | } |
326 | 1 | else return m3Err_wasmUnderrun; |
327 | 5.34k | } |
328 | | |
329 | | M3Result Read_opcode (m3opcode_t * o_value, bytes_t * io_bytes, cbytes_t i_end) |
330 | 24.7k | { |
331 | 24.7k | const u8 * ptr = * io_bytes; |
332 | | |
333 | 24.7k | if (ptr < i_end) |
334 | 24.7k | { |
335 | 24.7k | m3opcode_t opcode = * ptr++; |
336 | | |
337 | | #if d_m3CascadedOpcodes == 0 |
338 | | if (M3_UNLIKELY(opcode == c_waOp_extended)) |
339 | | { |
340 | | if (ptr < i_end) |
341 | | { |
342 | | opcode = (opcode << 8) | (* ptr++); |
343 | | } |
344 | | else return m3Err_wasmUnderrun; |
345 | | } |
346 | | #endif |
347 | 24.7k | * o_value = opcode; |
348 | 24.7k | * io_bytes = ptr; |
349 | | |
350 | 24.7k | return m3Err_none; |
351 | 24.7k | } |
352 | 0 | else return m3Err_wasmUnderrun; |
353 | 24.7k | } |
354 | | |
355 | | |
356 | | M3Result ReadLebUnsigned (u64 * o_value, u32 i_maxNumBits, bytes_t * io_bytes, cbytes_t i_end) |
357 | 32.9k | { |
358 | 32.9k | M3Result result = m3Err_wasmUnderrun; |
359 | | |
360 | 32.9k | u64 value = 0; |
361 | | |
362 | 32.9k | u32 shift = 0; |
363 | 32.9k | const u8 * ptr = * io_bytes; |
364 | | |
365 | 36.3k | while (ptr < i_end) |
366 | 36.3k | { |
367 | 36.3k | u64 byte = * (ptr++); |
368 | | |
369 | 36.3k | value |= ((byte & 0x7f) << shift); |
370 | 36.3k | shift += 7; |
371 | | |
372 | 36.3k | if ((byte & 0x80) == 0) |
373 | 32.8k | { |
374 | 32.8k | result = m3Err_none; |
375 | 32.8k | break; |
376 | 32.8k | } |
377 | | |
378 | 3.44k | if (shift >= i_maxNumBits) |
379 | 1 | { |
380 | 1 | result = m3Err_lebOverflow; |
381 | 1 | break; |
382 | 1 | } |
383 | 3.44k | } |
384 | | |
385 | 32.9k | * o_value = value; |
386 | 32.9k | * io_bytes = ptr; |
387 | | |
388 | 32.9k | return result; |
389 | 32.9k | } |
390 | | |
391 | | |
392 | | M3Result ReadLebSigned (i64 * o_value, u32 i_maxNumBits, bytes_t * io_bytes, cbytes_t i_end) |
393 | 26.3k | { |
394 | 26.3k | M3Result result = m3Err_wasmUnderrun; |
395 | | |
396 | 26.3k | i64 value = 0; |
397 | | |
398 | 26.3k | u32 shift = 0; |
399 | 26.3k | const u8 * ptr = * io_bytes; |
400 | | |
401 | 71.9k | while (ptr < i_end) |
402 | 71.9k | { |
403 | 71.9k | u64 byte = * (ptr++); |
404 | | |
405 | 71.9k | value |= ((byte & 0x7f) << shift); |
406 | 71.9k | shift += 7; |
407 | | |
408 | 71.9k | if ((byte & 0x80) == 0) |
409 | 26.3k | { |
410 | 26.3k | result = m3Err_none; |
411 | | |
412 | 26.3k | if ((byte & 0x40) and (shift < 64)) // do sign extension |
413 | 21.5k | { |
414 | 21.5k | u64 extend = 0; |
415 | 21.5k | value |= (~extend << shift); |
416 | 21.5k | } |
417 | | |
418 | 26.3k | break; |
419 | 26.3k | } |
420 | | |
421 | 45.5k | if (shift >= i_maxNumBits) |
422 | 5 | { |
423 | 5 | result = m3Err_lebOverflow; |
424 | 5 | break; |
425 | 5 | } |
426 | 45.5k | } |
427 | | |
428 | 26.3k | * o_value = value; |
429 | 26.3k | * io_bytes = ptr; |
430 | | |
431 | 26.3k | return result; |
432 | 26.3k | } |
433 | | |
434 | | |
435 | | M3Result ReadLEB_u32 (u32 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
436 | 27.7k | { |
437 | 27.7k | u64 value; |
438 | 27.7k | M3Result result = ReadLebUnsigned (& value, 32, io_bytes, i_end); |
439 | 27.7k | * o_value = (u32) value; |
440 | | |
441 | 27.7k | return result; |
442 | 27.7k | } |
443 | | |
444 | | |
445 | | M3Result ReadLEB_u7 (u8 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
446 | 5.10k | { |
447 | 5.10k | u64 value; |
448 | 5.10k | M3Result result = ReadLebUnsigned (& value, 7, io_bytes, i_end); |
449 | 5.10k | * o_value = (u8) value; |
450 | | |
451 | 5.10k | return result; |
452 | 5.10k | } |
453 | | |
454 | | |
455 | | M3Result ReadLEB_i7 (i8 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
456 | 5.35k | { |
457 | 5.35k | i64 value; |
458 | 5.35k | M3Result result = ReadLebSigned (& value, 7, io_bytes, i_end); |
459 | 5.35k | * o_value = (i8) value; |
460 | | |
461 | 5.35k | return result; |
462 | 5.35k | } |
463 | | |
464 | | |
465 | | M3Result ReadLEB_i32 (i32 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
466 | 9.37k | { |
467 | 9.37k | i64 value; |
468 | 9.37k | M3Result result = ReadLebSigned (& value, 32, io_bytes, i_end); |
469 | 9.37k | * o_value = (i32) value; |
470 | | |
471 | 9.37k | return result; |
472 | 9.37k | } |
473 | | |
474 | | |
475 | | M3Result ReadLEB_i64 (i64 * o_value, bytes_t * io_bytes, cbytes_t i_end) |
476 | 11.3k | { |
477 | 11.3k | i64 value; |
478 | 11.3k | M3Result result = ReadLebSigned (& value, 64, io_bytes, i_end); |
479 | 11.3k | * o_value = value; |
480 | | |
481 | 11.3k | return result; |
482 | 11.3k | } |
483 | | |
484 | | |
485 | | M3Result Read_utf8 (cstr_t * o_utf8, bytes_t * io_bytes, cbytes_t i_end) |
486 | 13.3k | { |
487 | 13.3k | *o_utf8 = NULL; |
488 | | |
489 | 13.3k | u32 utf8Length; |
490 | 13.3k | M3Result result = ReadLEB_u32 (& utf8Length, io_bytes, i_end); |
491 | | |
492 | 13.3k | if (not result) |
493 | 13.3k | { |
494 | 13.3k | if (utf8Length <= d_m3MaxSaneUtf8Length) |
495 | 13.3k | { |
496 | 13.3k | const u8 * ptr = * io_bytes; |
497 | 13.3k | const u8 * end = ptr + utf8Length; |
498 | | |
499 | 13.3k | if (end <= i_end) |
500 | 13.3k | { |
501 | 13.3k | char * utf8 = (char *)m3_Malloc ("UTF8", utf8Length + 1); |
502 | | |
503 | 13.3k | if (utf8) |
504 | 13.3k | { |
505 | 13.3k | memcpy (utf8, ptr, utf8Length); |
506 | 13.3k | utf8 [utf8Length] = 0; |
507 | 13.3k | * o_utf8 = utf8; |
508 | 13.3k | } |
509 | | |
510 | 13.3k | * io_bytes = end; |
511 | 13.3k | } |
512 | 10 | else result = m3Err_wasmUnderrun; |
513 | 13.3k | } |
514 | 2 | else result = m3Err_missingUTF8; |
515 | 13.3k | } |
516 | | |
517 | 13.3k | return result; |
518 | 13.3k | } |
519 | | |
520 | | #if d_m3RecordBacktraces |
521 | | u32 FindModuleOffset (IM3Runtime i_runtime, pc_t i_pc) |
522 | | { |
523 | | // walk the code pages |
524 | | IM3CodePage curr = i_runtime->pagesOpen; |
525 | | bool pageFound = false; |
526 | | |
527 | | while (curr) |
528 | | { |
529 | | if (ContainsPC (curr, i_pc)) |
530 | | { |
531 | | pageFound = true; |
532 | | break; |
533 | | } |
534 | | curr = curr->info.next; |
535 | | } |
536 | | |
537 | | if (!pageFound) |
538 | | { |
539 | | curr = i_runtime->pagesFull; |
540 | | while (curr) |
541 | | { |
542 | | if (ContainsPC (curr, i_pc)) |
543 | | { |
544 | | pageFound = true; |
545 | | break; |
546 | | } |
547 | | curr = curr->info.next; |
548 | | } |
549 | | } |
550 | | |
551 | | if (pageFound) |
552 | | { |
553 | | u32 result = 0; |
554 | | |
555 | | bool pcFound = MapPCToOffset (curr, i_pc, & result); |
556 | | d_m3Assert (pcFound); |
557 | | |
558 | | return result; |
559 | | } |
560 | | else return 0; |
561 | | } |
562 | | |
563 | | |
564 | | void PushBacktraceFrame (IM3Runtime io_runtime, pc_t i_pc) |
565 | | { |
566 | | // don't try to push any more frames if we've already had an alloc failure |
567 | | if (M3_UNLIKELY (io_runtime->backtrace.lastFrame == M3_BACKTRACE_TRUNCATED)) |
568 | | return; |
569 | | |
570 | | M3BacktraceFrame * newFrame = m3_AllocStruct(M3BacktraceFrame); |
571 | | |
572 | | if (!newFrame) |
573 | | { |
574 | | io_runtime->backtrace.lastFrame = M3_BACKTRACE_TRUNCATED; |
575 | | return; |
576 | | } |
577 | | |
578 | | newFrame->moduleOffset = FindModuleOffset (io_runtime, i_pc); |
579 | | |
580 | | if (!io_runtime->backtrace.frames || !io_runtime->backtrace.lastFrame) |
581 | | io_runtime->backtrace.frames = newFrame; |
582 | | else |
583 | | io_runtime->backtrace.lastFrame->next = newFrame; |
584 | | io_runtime->backtrace.lastFrame = newFrame; |
585 | | } |
586 | | |
587 | | |
588 | | void FillBacktraceFunctionInfo (IM3Runtime io_runtime, IM3Function i_function) |
589 | | { |
590 | | // If we've had an alloc failure then the last frame doesn't refer to the |
591 | | // frame we want to fill in the function info for. |
592 | | if (M3_UNLIKELY (io_runtime->backtrace.lastFrame == M3_BACKTRACE_TRUNCATED)) |
593 | | return; |
594 | | |
595 | | if (!io_runtime->backtrace.lastFrame) |
596 | | return; |
597 | | |
598 | | io_runtime->backtrace.lastFrame->function = i_function; |
599 | | } |
600 | | |
601 | | |
602 | | void ClearBacktrace (IM3Runtime io_runtime) |
603 | | { |
604 | | M3BacktraceFrame * currentFrame = io_runtime->backtrace.frames; |
605 | | while (currentFrame) |
606 | | { |
607 | | M3BacktraceFrame * nextFrame = currentFrame->next; |
608 | | m3_Free (currentFrame); |
609 | | currentFrame = nextFrame; |
610 | | } |
611 | | |
612 | | io_runtime->backtrace.frames = NULL; |
613 | | io_runtime->backtrace.lastFrame = NULL; |
614 | | } |
615 | | #endif // d_m3RecordBacktraces |