Skip to content

proposal:defer at embed target #1419

@luoliwoshang

Description

@luoliwoshang

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:

  1. Do NOT use defer in ISR

    • Interrupts may break defer linked list operations at any time
    • Global variable approach has no interrupt protection
  2. Single-threaded Only

    • Baremetal defer uses global variables
    • Will conflict in FreeRTOS multi-task environment
  3. DeferInLoop Resource Warning

    • defer in loops allocates a node per iteration
    • Large loops may exhaust heap space
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions