Skip to content

Conversation

@luoliwoshang
Copy link
Member

@luoliwoshang luoliwoshang commented Dec 12, 2025

Summary

Add defer support for baremetal/embedded environments (ESP32, ARM7TDMI, etc.).

Key Changes:

  • Add baremetal-specific defer implementation using global variables (single-threaded)
  • Use setjmp/longjmp for baremetal targets instead of sigsetjmp/siglongjmp
  • Add returns_twice attribute to setjmp/sigsetjmp for correct LLVM optimization
  • Refactor target detection: use -target flag directly instead of Baremetal boolean
  • Define platform-specific jmp_buf sizes for all embedded architectures (ESP32, ESP32-C3, ARM7TDMI, etc.)
  • Comprehensive defer testing (DeferAlways, DeferInCond, DeferInLoop)
  • Fix map closure and interface closure handling

Architecture

Target Detection Refactoring

Replaced boolean-based target detection with direct -target flag storage:

// Before (main branch)
type Target struct {
    GOOS   string
    GOARCH string
    GOARM  string
}

// After (this PR)
type Target struct {
    GOOS   string
    GOARCH string
    GOARM  string
    Target string  // "esp32", "arm7tdmi", "wasi", etc.
}

Logic in Sigsetjmp/Siglongjmp:

  • Target != "" or GOARCH == "wasm" → use setjmp/longjmp
  • Otherwise → use sigsetjmp/siglongjmp (platform-specific via build-tags)

Baremetal Defer Implementation

File Structure:

  • defer_tls_baremetal.go: Uses global variable for single-threaded baremetal
  • z_defer_baremetal.go: No-op FreeDeferNode (let GC collect), use longjmp in Rethrow
  • Build tags: //go:build baremetal to separate from pthread-based implementation

JMP_BUF Size Definitions

Added platform-specific jmp_buf sizes for all embedded targets:

Architecture Size (bytes) File
ESP32 (Xtensa) 68 jmpbuf_xtensa_baremetal.go
ESP32-C3 (RISC-V32) 52 jmpbuf_riscv32_baremetal.go
RISC-V64 52 jmpbuf_riscv64_baremetal.go
ARM7TDMI 40 jmpbuf_arm_baremetal.go
Cortex-M 40 jmpbuf_cortexm_baremetal.go
AVR 24 jmpbuf_avr_baremetal.go
macOS/Linux 196/200 jmpbuf_other.go

Returns Twice Attribute

Added LLVM returns_twice attribute to setjmp and sigsetjmp function calls:

  • Prevents LLVM from placing variables in caller-saved registers across setjmp/longjmp
  • Required for correct defer handling (DeferFrame pointer must remain valid)
  • Follows LLVM best practices and Clang implementation

Verification on ESP32

❯ llgo run -target esp32 .
Generating firmware image: defer.elf -> defer.bin (format: esp32)
esptool v5.1.0
Connected to ESP32 on /dev/cu.usbserial-110:
Chip type:          ESP32-D0WD-V3 (revision v3.1)
Features:           Wi-Fi, BT, Dual Core + LP Core, 240MHz

Wrote 49504 bytes (30423 compressed) at 0x00001000 in 3.3 seconds (120.7 kbit/s)
Hash of data verified.

Hard resetting via RTS pin...
Connected to /dev/cu.usbserial-110. Press Ctrl-C to exit.

defer in loop 2
defer in loop 1
defer in loop 0
defer in if
defer 2
defer 1

All three defer modes work correctly:

  • DeferAlways: defer 1, defer 2 (LIFO order ✅)
  • DeferInCond: defer in if (conditional defer ✅)
  • DeferInLoop: defer in loop 2/1/0 (LIFO order per iteration ✅)

Related PRs

Test plan

  • Test basic defer on ESP32 (DeferAlways)
  • Test conditional defer (DeferInCond)
  • Test defer in loops (DeferInLoop)
  • Verify LIFO execution order
  • Add CI tests for embedded target compilation
  • Verify jmp_buf sizes for all architectures
  • Test map closure and interface closure fixes

🤖 Generated with Claude Code

@gemini-code-assist
Copy link

Summary of Changes

Hello @luoliwoshang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request lays the groundwork for adding basic defer statement support to baremetal and embedded environments, such as ESP32. It adapts the runtime's defer mechanism to function without traditional thread-local storage by introducing a global variable for defer chain management and integrating with tinygogc for automatic cleanup of defer nodes. This is an initial work-in-progress, currently focusing on core defer functionality without support for panic/recover.

Highlights

  • Baremetal Defer TLS Implementation: Introduced defer_tls_baremetal.go which utilizes a global variable (globalDeferHead) to manage the defer chain in single-threaded baremetal environments, serving as an alternative to pthread TLS.
  • Baremetal Defer Node Cleanup: Added z_defer_baremetal.go containing a no-op FreeDeferNode function, indicating that defer nodes are expected to be garbage collected by tinygogc once they become unreachable in baremetal contexts.
  • Refined Build Tags: Updated build tags across several files to ensure proper separation of baremetal-specific implementations from standard and nogc builds, guaranteeing the correct code is compiled for each target environment.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces basic defer support for baremetal environments by providing alternative implementations for thread-local storage and defer node memory management, controlled by build tags. For single-threaded baremetal targets, it replaces pthread-based TLS with a simple global variable. For memory management, it adds a no-op FreeDeferNode for baremetal builds with tinygogc, letting the GC handle cleanup.

My review identified a critical issue in the build constraints for z_defer_nogc.go. The current change would lead to a build failure for the nogc && baremetal configuration because no implementation of FreeDeferNode would be compiled. I have provided a suggestion to correct the build tag to cover all nogc scenarios as intended. The rest of the changes appear correct and well-structured for the intended purpose.

@@ -1,4 +1,4 @@
//go:build nogc || baremetal
//go:build nogc && !baremetal

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The build constraint nogc && !baremetal leaves the nogc && baremetal build combination without an implementation for FreeDeferNode, which will likely result in a linker error. The nogc build tag should apply to both baremetal and non-baremetal environments, as both require manual memory management when no garbage collector is present. Changing the constraint to just nogc will resolve this by ensuring this c.Free-based implementation is used for all nogc builds.

Suggested change
//go:build nogc && !baremetal
//go:build nogc

@@ -0,0 +1,28 @@
//go:build baremetal && !nogc
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Missing build tag coverage for baremetal && nogc

The current build tag matrix has a gap:

  • !nogc && !baremetalz_defer_gc.go
  • nogc && !baremetalz_defer_nogc.go
  • baremetal && !nogcz_defer_baremetal.go
  • baremetal && nogcNO FILE

When building with both baremetal and nogc tags (valid for embedded systems without GC), FreeDeferNode will be undefined, causing compilation failure.

Recommendation: Change the build tag to //go:build baremetal (remove && !nogc) to cover both baremetal scenarios. The no-op approach is appropriate for all baremetal cases since manual memory management is expected in these environments.

// globalDeferHead stores the current defer chain head.
// In baremetal single-threaded environment, a global variable
// replaces pthread TLS.
var globalDeferHead *Defer
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: Race condition risk in ISR context

The global variable globalDeferHead is not protected against concurrent access. While documented as "single-threaded," baremetal systems commonly use Interrupt Service Routines (ISRs) that can preempt execution.

If an ISR calls Go code using defer, this can cause:

  • Defer chain corruption (linked list broken)
  • Lost defer statements
  • Use-after-free bugs

Evidence from codebase:

  • _demo/embed/export/main.go:10 mentions "hardware interrupt handlers"
  • ISR functions call Go code (c.Printf)

Recommendations:

  1. Disable interrupts during defer chain operations, or
  2. Document that defer MUST NOT be used in ISR context, or
  3. Use separate defer chains for ISR vs normal execution

Reference: CWE-362 (Improper Synchronization)

Comment on lines +23 to +26
// FreeDeferNode is a no-op in baremetal environment.
// Defer nodes become unreachable after being unlinked from the chain,
// and tinygogc will reclaim them in the next GC cycle.
func FreeDeferNode(ptr unsafe.Pointer) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Performance: Memory leak risk in defer-heavy loops

The no-op FreeDeferNode causes memory accumulation until GC runs. In constrained baremetal environments (ESP32 has ~520KB RAM):

Impact analysis:

  • Each defer iteration allocates ~64-96 bytes
  • 1000 iterations = ~96KB held until GC
  • GC uses stop-the-world mark-and-sweep with linear heap scanning
  • Allocation performance degrades as heap fills

Comparison: Other build configurations (z_defer_gc.go, z_defer_nogc.go) explicitly free defer nodes immediately.

Recommendation: Consider implementing a simple defer node pool/freelist to:

  • Eliminate GC pressure from defer nodes
  • Provide O(1) allocation/deallocation
  • Match performance of other builds

Reference: CWE-400 (Uncontrolled Resource Consumption)

Comment on lines +21 to +23
// globalDeferHead stores the current defer chain head.
// In baremetal single-threaded environment, a global variable
// replaces pthread TLS.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation: Add thread-safety warning

Consider making the concurrency constraints more explicit:

Suggested change
// globalDeferHead stores the current defer chain head.
// In baremetal single-threaded environment, a global variable
// replaces pthread TLS.
// globalDeferHead stores the current defer chain head.
// In baremetal single-threaded environment, a global variable
// replaces pthread TLS.
//
// WARNING: This implementation is NOT thread-safe and NOT ISR-safe.
// Only use in single-threaded baremetal environments. Do NOT call
// any Go code using defer from interrupt service routines (ISRs).

This makes the safety constraints clearer for future maintainers.

@xgopilot
Copy link

xgopilot bot commented Dec 12, 2025

Code Review Summary

This PR adds clean baremetal defer support with good separation of concerns. However, there are critical issues that must be addressed:

Critical:

  • Missing baremetal && nogc build tag coverage - will cause compilation failure
  • Race condition risk with ISRs accessing global globalDeferHead

High Priority:

  • Memory leak potential in defer-heavy loops (no-op FreeDeferNode accumulates until GC)
  • Missing thread-safety documentation

Positive aspects:

  • Clean API consistency across implementations
  • Appropriate use of global variable for single-threaded performance (5-10x faster than TLS)
  • Good build tag separation

Please address the critical build tag gap and ISR safety issues before merging.

@codecov
Copy link

codecov bot commented Dec 12, 2025

Codecov Report

❌ Patch coverage is 0% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.93%. Comparing base (3e5807d) to head (1658295).

Files with missing lines Patch % Lines
ssa/eh.go 0.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1464   +/-   ##
=======================================
  Coverage   90.93%   90.93%           
=======================================
  Files          44       44           
  Lines       11528    11528           
=======================================
  Hits        10483    10483           
  Misses        883      883           
  Partials      162      162           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@luoliwoshang
Copy link
Member Author

target defer build fail cause by libc's log
logs_52070496149.zip

@luoliwoshang luoliwoshang force-pushed the baremetal/defer-support branch 8 times, most recently from ffbf719 to 26fbf15 Compare December 18, 2025 02:51
@luoliwoshang luoliwoshang force-pushed the baremetal/defer-support branch 2 times, most recently from f4cfb7f to b33ef23 Compare December 22, 2025 09:13
@luoliwoshang luoliwoshang changed the title [WIP] runtime: add baremetal defer support feat(embed): add baremetal defer support Dec 22, 2025
@luoliwoshang luoliwoshang force-pushed the baremetal/defer-support branch from 6acf2c3 to 2eafdb6 Compare December 23, 2025 09:54
luoliwoshang and others added 7 commits December 28, 2025 18:38
- Add defer_tls_baremetal.go: use global variable instead of pthread TLS
  for single-threaded baremetal environments
- Add z_defer_baremetal.go: no-op FreeDeferNode that lets tinygogc
  collect unreachable defer nodes
- Update defer_tls.go: exclude from baremetal builds
- Update z_defer_nogc.go: exclude baremetal (uses different strategy)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Baremetal environments (ESP32, etc.) don't have signal support,
so sigsetjmp/siglongjmp are not available. Use setjmp/longjmp
instead, which are provided by newlib.

- Add Baremetal field to ssa.Target
- Set Baremetal based on "baremetal" build tag from target config
- In Sigsetjmp/Siglongjmp, fallback to setjmp/longjmp for baremetal

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add support for multiple test scenarios in the embedded target build script:
- Accept test directory as required first argument (e.g., empty, defer)
- Create separate test directories for different compilation tests
- Add empty/main.go for basic compilation test
- Add defer/main.go for defer statement compilation test

This allows testing different Go features across all embedded targets.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Split the targets build step into two:
- Build targets (empty): basic compilation test
- Build targets (defer): defer statement compilation test

This verifies that defer works correctly on baremetal targets.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Refactor ignore_list to be defined per test directory using case statement.
Each target on a separate line for cleaner diffs when modifying ignore lists.
Unknown test directories now error out instead of using a default list.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Update defer/main.go to test all three defer modes:
- DeferAlways: regular defer statements
- DeferInCond: defer inside if/else blocks
- DeferInLoop: defer inside for loops

Use c.Printf instead of println for baremetal compatibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Add targets to defer ignore list:
- digispark: inline asm branch target out of range
- nintendoswitch: missing bits/wordsize.h header
- simavr: incompatible target ISA
- Various targets lacking libc symbols (arduino-*, cortex-m*, esp8266, etc.)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
luoliwoshang and others added 12 commits December 28, 2025 18:38
Add platform-specific jmpbuf size files for TinyGo-compatible targets:
- jmpbuf_arm.go: generic ARM (Linux ARM, ARM7TDMI)
- jmpbuf_avr.go: AVR 8-bit (Arduino)
- jmpbuf_cortexm.go: ARM Cortex-M
- jmpbuf_riscv.go: generic RISC-V
- jmpbuf_riscv32.go: RISC-V 32-bit
- jmpbuf_riscv64.go: RISC-V 64-bit
- jmpbuf_xtensa.go: Xtensa (ESP32/ESP8266)

Update jmpbuf_other.go to exclude baremetal and wasm targets.

All sizes are set to 200 bytes as placeholder, to be confirmed
from picolibc/newlib machine/setjmp.h for each architecture.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Use maximum size (Windowed ABI) to cover both ESP32 and ESP8266.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Use setjmp.Longjmp to execute deferred functions on panic in
baremetal environments instead of immediately exiting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Rename jmpbuf files for baremetal targets to use _baremetal suffix:
- jmpbuf_arm.go -> jmpbuf_arm_baremetal.go
- jmpbuf_avr.go -> jmpbuf_avr_baremetal.go
- jmpbuf_cortexm.go -> jmpbuf_cortexm_baremetal.go
- jmpbuf_riscv.go -> jmpbuf_riscv_baremetal.go
- jmpbuf_riscv32.go -> jmpbuf_riscv32_baremetal.go
- jmpbuf_riscv64.go -> jmpbuf_riscv64_baremetal.go
- jmpbuf_xtensa.go -> jmpbuf_xtensa_baremetal.go

This fixes Go's go/build package incorrectly parsing filenames like
jmpbuf_riscv64.go as requiring GOARCH=riscv64, when the actual GOARCH
for baremetal targets is "arm".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Remove the non-baremetal arm condition from jmpbuf_arm_baremetal.go.
Non-baremetal ARM targets should fallback to jmpbuf_other.go.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Update jmp_buf sizes based on picolibc/newlib definitions:
- ARM7TDMI (GBA): 160 bytes
- AVR: 48 bytes
- Cortex-M: 160 bytes
- RISC-V 32-bit: 304 bytes
- RISC-V 64-bit: 208 bytes
- Xtensa: 68 bytes (Windowed ABI, _JBLEN=17)

Also update comments with correct source references.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
The jmpbuf_riscv_baremetal.go file is no longer needed since
jmpbuf_riscv32_baremetal.go and jmpbuf_riscv64_baremetal.go
now handle both 32-bit and 64-bit RISC-V architectures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
When defer in loop captures variables, panic exit causes issues with
preceding defer normal execution. Without variable capture, it works correctly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@luoliwoshang luoliwoshang force-pushed the baremetal/defer-support branch from 2eafdb6 to 9fa3aed Compare December 28, 2025 10:38
Replace Target.Baremetal with Target.Target to directly store the
-target flag value (e.g., "esp32", "arm7tdmi", "wasi"). This allows
platform-specific setjmp/sigsetjmp selection to be handled via
build-tags in runtime package instead of hardcoded logic.

Changes:
- Remove Target.Baremetal field
- Add Target.Target string field for -target parameter
- Simplify Sigsetjmp/Siglongjmp logic: use setjmp/longjmp when
  GOARCH is "wasm" or Target is non-empty
- Platform-specific sigsetjmp implementations can now be selected
  via build-tags (e.g., sigsetjmp_darwin.go)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant