Skip to content

feat: add hidden closure environment ABI - #2255

Open
cpunion wants to merge 5 commits into
xgo-dev:mainfrom
cpunion:codex/closure-env-2170-v2
Open

feat: add hidden closure environment ABI#2255
cpunion wants to merge 5 commits into
xgo-dev:mainfrom
cpunion:codex/closure-env-2170-v2

Conversation

@cpunion

@cpunion cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • keep Go/go-types signatures free of closure env and store physical entry metadata on llssa.Function
  • use nest or swiftself on validated native targets; Wasm and portable fallbacks use exact typed env/no-env edges
  • keep native dynamic funcval calls branch-free with an instruction-free identity barrier
  • point plain Go and C funcvals directly at their real entries and remove the __llgo_stub call layer
  • preserve env attributes through aggregate, sret, callback-wrapper, and C ABI lowering
  • call closures through stock libffi with a final-hop CallWithEnv bridge; no libffi rebuild is required
  • preserve {fn, env} with nil meaning no physical env, while eliding provably zero-sized lexical environments
  • restore the Go-required anonymous-to-named closure conversion for synthetic rangefunc yield arguments

Benefits

  • removes one generated call layer, its symbols, metadata, and runtime lookup support
  • keeps ordinary compiler code and Go signatures independent of closure transport
  • keeps native dynamic calls on one hot edge without an env-nil branch
  • preserves exact C ABI for C functions, variadics, callbacks, and exported functions
  • gives closure, method-value, reflection, and FFI paths one backend env model
  • makes the official range4.go named-yield case valid on Go 1.24 through 1.26 and removes its Darwin xfail

Review follow-up

  • parse //llgo:env and // llgo:env through the shared directive parser
  • document that intrinsic opcode 0x48 remains reserved for the coroutine backend
  • document the RISC-V LLVM nest t2/x7 transport and its match with the libffi bridge; ESP32-C3 exercises the riscv32 path
  • retain the whole libffi closure rather than an interior field pointer
  • document the balanced normal-return requirement around the TLS/register bridge

Validation

Fork validation is green in cpunion/llgo#96: 39 successful checks on original implementation commit eca0053a2997.

Coverage includes Linux amd64/arm64, macOS arm64/Intel, Go 1.24/1.26, Wasm explicit transport, nest, swiftself, O0/O2/LTO, plain Go and C funcvals, captured and zero-sized closures, nil and typed-nil method values, direct interface calls, reflection/libffi, variadics, aggregates, and sret. The existing ESP32/ESP32-C3 suites pass; the new full-reflect runtime test is precisely skipped there because the same test does not compile on the baseline embedded runtime.

Additional validation after rebasing onto current main:

  • focused closure-env and rangefunc compiler tests
  • focused closure ABI backend tests
  • existing rangefunc defer runtime tests
  • LLVM verification for direct and synthetic anonymous-to-named closure arguments
  • official test/range4.go with an empty xfail set on Go 1.24.11, 1.25.7, and 1.26.5
  • git diff --check upstream/main...HEAD

Same-machine A/B on Apple M4 Max measured plain dynamic funcval calls improving from about 1.027 ns/op to 0.770 ns/op, captured closures remaining flat, and representative executable sizes decreasing.

Supersedes #2248, whose closed head cannot be reopened after the fork branch was force-pushed.

Closes #2170

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

b407f0e8fa10 | workflow run | long-term charts

Program measurements

Platform Workload File size vs main Build vs main Run vs main
Linux cprintf 18344 B new 351.343 ms new 1.281 ms new
Linux fmtprintf 1831592 B new 3.379 s new 3.091 ms new
Linux println 67720 B new 353.684 ms new 1.593 ms new
macOS cprintf 84672 B new 323.143 ms new 2.377 ms new
macOS fmtprintf 1869328 B new 3.212 s new 13.120 ms new
macOS println 121200 B new 308.004 ms new 3.441 ms new
Core language and compiler benchmarks
Platform Benchmark ns/op vs main
Linux BenchmarkLookupPCRandom 13.480 ns/op new
Linux BenchmarkMergeCompilerFlags 151.800 ns/op new
Linux BenchmarkMergeLinkerFlags 96.920 ns/op new
Linux BenchmarkChannelBuffered 33.350 ns/op new
Linux BenchmarkChannelHandoff 27922 ns/op new
Linux BenchmarkDefer 48.290 ns/op new
Linux BenchmarkDirectCall 1.557 ns/op new
Linux BenchmarkGlobalRead 1.558 ns/op new
Linux BenchmarkGlobalWrite 2.487 ns/op new
Linux BenchmarkGoroutine 43011 ns/op new
Linux BenchmarkInterfaceCall 7.777 ns/op new
Linux BenchmarkRuntimeGetG 2.199 ns/op new
macOS BenchmarkLookupPCRandom 12.550 ns/op new
macOS BenchmarkMergeCompilerFlags 118.300 ns/op new
macOS BenchmarkMergeLinkerFlags 87.710 ns/op new
macOS BenchmarkChannelBuffered 24.860 ns/op new
macOS BenchmarkChannelHandoff 8992 ns/op new
macOS BenchmarkDefer 26.790 ns/op new
macOS BenchmarkDirectCall 1.186 ns/op new
macOS BenchmarkGlobalRead 1.249 ns/op new
macOS BenchmarkGlobalWrite 1.251 ns/op new
macOS BenchmarkGoroutine 27479 ns/op new
macOS BenchmarkInterfaceCall 4.706 ns/op new
macOS BenchmarkRuntimeGetG 2.087 ns/op new

No main baseline exists yet; all metrics are marked new.

Warning

  • The rendered benchmark data could not be pushed.

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cpunion
cpunion marked this pull request as ready for review August 1, 2026 16:01

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review summary

Reviewed the hidden closure-environment ABI change across the compiler (ssa/, cl/), the C libffi bridge, and the reflect runtime. The design is careful and well-commented; the env-vs-no-env distinction, the zero-sized sentinel, and the reentrancy handling in llgo_ffi_call_with_env (save/restore of previous) all check out for the normal synchronous paths. The docs (doc/closure.md) match the implementation.

A few concrete findings are left as inline comments. Cross-cutting notes that don't map to a single line:

Performance (worth a benchmark vs. the base commit):

  • runtime/internal/clite/ffi/_wrap/libffi.c — the ffi_call_go fast path is only weakly linked and is disabled on Apple targets (LLGO_FFI_GO_ABI_MATCHES excludes __APPLE__). Per the code's own comment, common system libffi builds omit that symbol, so on typical Linux and all macOS the slow trampoline is the default path for every env-bearing FFI call. That path spills/reloads the whole argument register file, marshals arguments twice, and adds an extra out-of-line call. Since reflect.Value.call now routes through ffi.CallWithEnv, this is on the reflect hot path — worth measuring the regression.
  • runtime/internal/lib/reflect/makefunc.go makeMethodValue — each method value now allocates a libffi closure (ffi_closure_alloc, mmap-backed executable memory) plus a heap funcval and an extra indirection per call, versus the previous direct funcval. If method values are created in loops this is a real allocation/perf cost; consider caching per (type, method).

Memory-safety (low severity, documentation-only asks):

  • runtime/internal/clite/ffi/_wrap/libffi.c — the aarch64 Apple/Android and arm32 trampolines stash callee-saved registers/return address in the single _Thread_local context and restore them only after the real target returns normally (matching the previous restore in llgo_ffi_call_with_env). A non-local exit that unwinds across the ffi_call frame (longjmp-style) would skip both restores, leaving callee-saved registers clobbered and the TLS record stale. Synchronous nesting and balanced signal-handler nesting are safe. Recommend a comment asserting the "target must return normally / every entry goes through the balanced save/restore" invariant next to the struct definition.
  • runtime/internal/lib/reflect/makefunc.go:74keepAlive stores the interior pointer &closure.Fn to retain the whole *ffi.Closure. This relies on interior-pointer retention in the current GC; storing closure directly would be more robust. No live bug.

View job run

Comment thread cl/compile.go Outdated
Comment thread cl/import.go Outdated
Comment thread ssa/closure_abi.go Outdated
@cpunion
cpunion force-pushed the codex/closure-env-2170-v2 branch from a009e26 to 5b379e0 Compare August 1, 2026 16:41
@cpunion

cpunion commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Review follow-up is pushed on top of current main (5b379e062):

  • accepted both closure-env directive spellings through the shared parser
  • documented the reserved intrinsic slot and RISC-V transport basis
  • retained the complete libffi closure rather than an interior pointer
  • documented the normal-return invariant for balanced register/TLS restoration

I also folded the independently reproduced range4.go failure into this closure-ABI PR instead of opening a conflicting PR. The new counterexample verifies direct and rangefunc-synthetic anonymous-to-named closure arguments; the official case passes on Go 1.24.11, 1.25.7, and 1.26.5.

The review performance concern already has same-machine A/B data in the PR body. Caching reflect method-value closures is a separate allocation/lifetime design change and is intentionally not included here.

}

func makeFunc(typ Type, method bool, fn func(args []Value) (results []Value)) Value {
func makeFunc(typ Type, fn func(args []Value) (results []Value)) Value {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

这里不再需要 makeFunc 函数,可以合并到 MakeFunc 函数。

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.

Proposal: C-compatible closure-context ABI for two-word LLGo funcvals

2 participants