-
Notifications
You must be signed in to change notification settings - Fork 46
Description
Defer Implementation for Embedded Platforms
Background
The current llgo defer implementation relies on the following capabilities:
- pthread TLS: Thread-local storage for storing each function's DeferFrame
- sigsetjmp/siglongjmp: Non-local jumps with signal mask support for panic/recover
- Dynamic memory allocation: malloc/free for defer node allocation
Embedded platforms (e.g., ESP32) use newlib/picolibc, a minimal libc without pthread thread library. The defer mechanism needs to be adapted to support baremetal environments.
Target Platforms
Embedded targets in the targets/ directory, including:
- ESP32 Series (Xtensa):
esp32.json,esp8266.json - ESP32-C3 Series (RISC-V):
esp32c3.json - ARM Cortex-M:
cortex-m.json,cortex-m0.json,cortex-m3.json,cortex-m4.json - RP2040/RP2350 (Raspberry Pi Pico):
rp2040.json,rp2350.json,pico.json - STM32:
stm32f4disco.json,bluepill.json,nucleo-*.json - nRF52:
nrf52840.json,nrf52.json - AVR:
avr.json,atmega328p.json,arduino.json - RISC-V:
riscv32.json,riscv64.json,fe310.json
Environment Capability Comparison
| Capability | Standard Linux | newlib (baremetal) |
|---|---|---|
| setjmp/longjmp | ✅ | ✅ |
| sigsetjmp/siglongjmp | ✅ | ❌ |
| pthread TLS | ✅ | ❌ |
| malloc/free | ✅ | ✅ |
Task List
🔴 P0 - Blocking Issues
Must be fixed, otherwise defer won't work at all
TODO 1: Implement DeferFrame Storage for Baremetal Environment
Problem: Currently using pthread TLS to store each thread's DeferFrame pointer. Baremetal has no pthread, and tls_stub.go's Get/Set are no-ops.
Goal: Provide DeferFrame storage mechanism for baremetal environment.
Approach: Use global variable instead of pthread TLS (baremetal is typically single-threaded).
Acceptance Criteria:
-
GetThreadDefer()correctly returns current DeferFrame in baremetal environment -
SetThreadDefer()correctly sets DeferFrame in baremetal environment - Basic defer tests pass
TODO 2: Fix Defer Node Memory Release Issue
Problem: FreeDeferNode in z_defer_nogc.go uses c.Free() to release memory, but baremetal environment uses tinygogc.Alloc() to allocate memory - they are incompatible.
Goal: Ensure defer node memory is properly managed.
Approach: Make FreeDeferNode a no-op in baremetal environment, let tinygogc auto-collect.
Acceptance Criteria:
- Defer nodes don't crash after execution
- Memory is correctly reclaimed by GC
- No memory leaks (long-running tests)
TODO 3: Use setjmp Instead of sigsetjmp
Problem: newlib doesn't support sigsetjmp/siglongjmp (requires signal handling support).
Goal: Use plain setjmp/longjmp in baremetal environment.
Approach: Reference existing WASM handling, add baremetal branch in compiler.
Acceptance Criteria:
- panic triggers normally
- recover correctly catches panic
- panic/recover works in nested defers
🟡 P1 - Important Issues
May cause runtime errors or cross-compilation failures
TODO 4: Fix jmp_buf Size Cross-Compilation Issue
Problem: Currently using cgo to get C.sigjmp_buf size, which is determined when compiling llgo itself (host machine), not the target architecture size.
Impact:
- llgo compiled on x86_64 Mac uses ~200 bytes for jmp_buf
- ESP32 (Xtensa) jmp_buf only needs 68 bytes
- Size mismatch may cause stack overflow or data corruption
Goal: Use correct jmp_buf size based on target architecture.
Approach: Reference newlib's machine/setjmp.h, hardcode jmp_buf size for each target architecture.
Acceptance Criteria:
- ESP32 (Xtensa) uses correct size (68 bytes)
- ESP32-C3 (RISC-V) uses correct size
- panic/recover works correctly after cross-compilation
TODO 5: Verify Panic State Storage
Problem: Panic state (whether panicking, panic value) may be stored in TLS, need to confirm availability in baremetal.
Goal: Ensure panic state can be correctly stored and accessed in baremetal environment.
Acceptance Criteria:
- Confirm panic state storage location
- If stored in TLS, handle together with TODO 1
- recover correctly obtains panic value
TODO 6: Verify Initialization Order
Problem: Global variable initialization timing is uncertain. If initialized after some init() functions, defer in init() will fail.
Goal: Ensure defer mechanism is ready before any user code executes.
Acceptance Criteria:
- defer in
init()functions works normally - defer in package-level variable initialization works normally
🟠 P2 - Environment Limitations
Requires documentation and usage restrictions
TODO 7: Write Baremetal Defer Usage Documentation
Limitations to Document:
-
Do NOT use defer in ISR
- Interrupts may break defer linked list operations at any time
- Global variable approach has no interrupt protection
-
Single-threaded Only
- Baremetal defer uses global variables
- Will conflict in FreeRTOS multi-task environment
-
DeferInLoop Resource Warning
- defer in loops allocates a node per iteration
- Large loops may exhaust heap space
-
Conditional Defer Limit
- Maximum 32/64 conditional defers per function (depends on bits field size)
Acceptance Criteria:
- Documentation clearly explains all limitations
- Provides correct usage examples
- Provides anti-patterns with consequence explanations
Verification Tests
| Test Type | Test Content | Acceptance Criteria |
|---|---|---|
| Basic defer | Single/multiple defer, nested functions | Correct LIFO order |
| DeferInCond | defer in if branches | Conditions trigger correctly |
| DeferInLoop | defer in for loops | drain loop executes correctly |
| panic/recover | Basic panic, nested panic, recover return value | Exceptions caught correctly |
| Edge cases | defer in init(), recursive defer | Works normally |
Implementation Phases
Phase 1: Basic Functionality
├── TODO 1: DeferFrame Storage
├── TODO 2: Memory Release Fix
└── TODO 3: setjmp Replacement
Phase 2: Cross-Compilation
├── TODO 4: jmp_buf Size Fix
├── TODO 5: Panic State Verification
└── TODO 6: Initialization Order Verification
Phase 3: Documentation
└── TODO 7: Usage Documentation
References
- Detailed technical analysis: invest.md
- newlib jmp_buf definitions:
newlib/libc/include/machine/setjmp.h - TinyGo defer implementation:
tinygo/src/runtime/panic.go
TinyGo's recover() Support Status
TinyGo currently does NOT support recover() on the following architectures:
| Architecture | recover() Support | Notes |
|---|---|---|
| wasm32 | ❌ | Requires WebAssembly exception-handling proposal |
| riscv64 | ❌ | TODO: to be implemented |
| xtensa (ESP32) | ❌ | TODO: to be implemented |
| Other architectures | ✅ | Supported |
TinyGo Source Reference (builder/build.go):
func (b *builder) supportsRecover() bool {
switch b.archFamily() {
case "wasm32":
return false // Requires WebAssembly exception-handling proposal
case "riscv64", "xtensa":
return false // TODO: add support for these architectures
default:
return true
}
}Implications for llgo:
- ESP32 (Xtensa) and RISC-V 64 panic/recover implementation may have additional challenges
- TinyGo's choice to not support these suggests setjmp/longjmp implementations may differ
- llgo needs to verify panic/recover works correctly on these architectures
Risk Assessment
| Risk | Severity | Probability | Mitigation |
|---|---|---|---|
| P0 issues unfixed | Fatal | 100% | Fix in order |
| jmp_buf size mismatch | Severe | Medium | Hardcode per-architecture sizes |
| defer used in ISR | Severe | Low | Documentation warning |
| Multi-task conflict | Severe | Medium | Document single-thread limitation |
| DeferInLoop memory exhaustion | Medium | Medium | Documentation warning |
Issue Version: v1.0
Created: 2025-01