Skip to content
Dev Tools Article

GCC Finally Has a Plan to Kill the Trampoline

New built-ins on GCC's development branch make capturing nested functions work without executable stacks.

Lenn Voss
Lenn Voss
Cloud & Infrastructure Writer · Aug 15, 2026 · 5 min read
GCC Finally Has a Plan to Kill the Trampoline

For almost forty years, GCC's nested functions have carried an asterisk big enough to kill the feature: take a pointer to one and the compiler emits a trampoline — a few bytes of machine code generated at runtime, on the stack. That means an executable stack, which means giving up one of the oldest and cheapest exploit mitigations we have. It's why linters flag the feature, why hardened distros reject binaries that use it, and why the Linux kernel scrubbed its last nested functions out a decade ago when Clang refused to compile them.

That asterisk is now being removed, quietly and in pieces. GCC 16 guarantees that a nested function which doesn't touch its enclosing scope never needs a trampoline, even at -O0, and warns when you return one that does. And a patch by Martin Uecker — a WG14 member who's been writing up the technique on his blog — has been merged into GCC's development branch, adding two built-ins that let you skip trampolines even when the function does capture locals. The trick is an old one wearing new clothes: wide pointers.

Why the trampoline existed at all

The trampoline was never stupidity; it was ABI discipline. A pointer to a nested function has to be a plain T (*)(args) so you can hand it to qsort, pthread_create, or any callback API written since 1978. But a capturing nested function needs two words — where the code lives and where the captured variables live — and a plain function pointer only holds one. The 1980s answer: synthesize a tiny stub at runtime that loads the environment pointer into a reserved register (the static chain) and jumps to the real code. The stub's address fits in a normal pointer. Everyone's happy, except the stub is data being executed, and post-NX-bit that's a hole.

GCC 14 added -ftrampoline-impl=heap to move the stubs off the stack — necessary on Apple silicon, where an executable stack simply isn't available — but that trades a security problem for a lifetime problem. Heap trampolines cost an allocation per closure and can leak across longjmp. It's a patch on a patch.

Two words, honestly

Uecker's approach stops pretending the closure fits in one word. A wide pointer is just a struct:

#define wide(T) struct wide_##T { typeof(T) *code; void *chain; }

The new built-ins, __builtin_call_code_address() and __builtin_call_static_chain(), extract the real code address and the environment pointer from a nested function — no stub generated. On the call side, __builtin_call_with_static_chain() (which Richard Henderson added back in 2014, and which Clang also implements) invokes the code pointer with the chain register set. Wrap both ends in CLOSURE() and CALL() macros and you have capturing callbacks in plain C, with no runtime code generation and no allocation.

The part that should make you sit up is what the optimizer does with it. Because there's no opaque runtime stub in the middle, GCC can see through the whole closure. Uecker's example — a nested function capturing k, passed through a wide pointer to another function — compiles down to a single lea eax, [rdi+rdi*2]. The entire closure apparatus evaporates. Trampolines could never do that; runtime-generated code is an optimization barrier by construction.

You've been writing wide pointers all along

Here's the synthesis worth internalizing: every serious C callback API already uses wide pointers — assembled by hand. qsort_r, pthread_create, every register_callback(fn, void *userdata) pair in every C library ever shipped is a wide pointer with the two words passed separately and the capture struct packed manually. The convention works, but the compiler can't check it and can't optimize across it, and every project reinvents the packing boilerplate.

This is also the road not taken twenty years ago. Apple's blocks extension in Clang solved the same problem with new syntax, a runtime library, and reference-counted heap captures — closer to a language fork than an extension, which is a big reason it never spread beyond Apple's ecosystem. The Pascal and Ada lineage did it with fat procedure pointers at the ABI level. C is arriving last, but with the most conservative possible design: no new syntax, no runtime, just two built-ins and a struct you define yourself.

That conservatism matters because this is really a standards play. WG14 currently has competing visions for closures in C — N3780 surveys blocks, nested functions, and lambdas as candidate designs, and Uecker has his own papers on nested-function semantics. The GCC built-ins are a proof that the minimal design is sufficient: the demonstrated machinery is exactly what a standardized wide function pointer would need, and it's now sitting in a shipping compiler's development branch rather than in a PDF. That's how features actually win committee arguments.

What to do with this today

Be honest about the maturity gradient. If you're on GCC 16, the no-capture guarantee is immediately useful: a nested helper that only reads its own parameters and statics is now a legitimate, zero-cost way to keep a callback next to its only call site, and -Wtrampolines in CI will catch anyone who accidentally captures. That should be standard practice in embedded and security-sensitive trees where executable stacks are a non-starter — the same trees that banned the feature outright.

The capturing built-ins are a different story. They're in the development branch, presumably GCC 17, and they're GCC-only — Clang has the call-side built-in but not the extraction pair, and while Uecker says a portable formulation for both compilers is coming, it isn't written up yet. Building a product API on them now would be premature. Building an internal macro layer to experiment, in a codebase that's already GCC-only? Reasonable, and the fallback is trivial: the same macros can degrade to explicit void * context arguments, which is what your code does today anyway.

The honest verdict: this is a genuine fix to a forty-year-old design wart, not hype — but its significance is less "new tool for your current project" and more "the ground truth for C's closure debate just shifted." The trampoline survived four decades because nobody wanted to break the single-word function pointer. Wide pointers break it openly, cheaply, and in a way qsort will never accept — and that's fine. The lesson from every language that solved this is that closures were always two words. C is finally saying so out loud.

Sources & further reading

  1. Using GCC's Nested Functions with Wide Pointers and No Trampolines II — uecker.codeberg.page
  2. Using GCC's Nested Functions with Wide Pointers and No Trampolines — uecker.codeberg.page
  3. GCC 16 Release Series Changes, New Features, and Fixes — gcc.gnu.org
  4. c: Warn when returning nested functions that require a non-local context — github.com
  5. Allow the static chain to be set from C — gcc.gnu.org
  6. Implement the __builtin_call_with_static_chain GNU extension — reviews.llvm.org
  7. N3780: Functions with Data - Closures in C — open-std.org
Lenn Voss
Written by
Lenn Voss · Cloud & Infrastructure Writer

Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading