Skip to content

compiler: implement go:cgo_import_dynamic and lower Darwin cgo import trampolines (#5604) - #5612

Open
neomantra wants to merge 3 commits into
tinygo-org:devfrom
neomantra:nm-cgo-import-dynamic
Open

compiler: implement go:cgo_import_dynamic and lower Darwin cgo import trampolines (#5604)#5612
neomantra wants to merge 3 commits into
tinygo-org:devfrom
neomantra:nm-cgo-import-dynamic

Conversation

@neomantra

@neomantra neomantra commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

As noted in #5604, this was worked through with LLM. It adds parsing of the go:cgo_import_dynamic directive and implementation for Darwin.

The other Tinygo-supported platform that could use this is OpenBSD, but is more important for Darwin. It targets loadASTComments generally because these directives are not necessarily attached to a function/module/etc; the comment there explains it.

The following description of the PR is LLM-generated from our journey; it is verbose but comprehensive. I have read it and I have reviewed all the submitted code, both humanually and with multiple LLMs. Thanks for the review.


Summary

Extend TinyGo's existing Darwin stdlib trampoline lowering to the variable-based pattern used by golang.org/x/sys/unix and internal/syscall/unix, and route all four variadic libc imports (open, openat, fcntl, ioctl) through fixed-signature wrappers on both trampoline paths.

Fixes #5604.
Fixes #5365.
Builds on #5401.
The companion _ioctl/___sincos_stret stubs (macos-minimal-sdk#5) are already in dev via the macos-minimal-sdk v0.1.0 submodule update.

Problem

Darwin syscall wrappers generated by x/sys declare globals such as libc_ioctl_trampoline_addr and associate them with dylib symbols using file-level //go:cgo_import_dynamic pragmas. TinyGo does not compile the assembly that initializes those globals, so the existing libc syscall engine receives address zero and crashes before main in Bubble Tea programs.

TinyGo already avoids the equivalent assembly trampoline for the standard library: createDarwinFuncPCABI0Call recognizes abi.FuncPCABI0(libc_*_trampoline) and substitutes the address of an external libc declaration. This change applies that same lowering to the x/sys global-load pattern.

That existing stdlib lowering also carries a silent bug of its own: among its variadic imports it routes only open through a fixed-signature wrapper, so stdlib fcntl, ioctl, and openat calls corrupt their variadic arguments on darwin/arm64. This PR fixes that too, since the new lowering needs the same wrapper set anyway.

Scope

This is a targeted implementation of the //go:cgo_import_dynamic pattern used by Darwin's generated syscall wrappers, not general cross-platform support for the directive. Although the file-level metadata is parsed during compilation, the libc_*_trampoline_addr symbol-address substitution is restricted to GOOS=darwin; non-Darwin targets retain their existing behavior.

Upstream Go also uses //go:cgo_import_dynamic on platforms such as OpenBSD, AIX, Solaris, and illumos. Supporting those platforms, the directive's library operand, other use patterns, and platform-specific linking or calling conventions remains out of scope.

Implementation

  • Parse the local and remote symbol metadata from file-level //go:cgo_import_dynamic pragmas into a package-local map for Darwin trampoline lowering, accepting the same one-, two-, and three-operand forms as the gc compiler (local [remote ["library"]]); the remote symbol defaults to the local one and the library operand is ignored (the linker already resolves against libSystem).
  • On Darwin, replace loads of matching uintptr-typed libc_*_trampoline_addr globals with ptrtoint of an external declaration for the pragma's remote symbol. Globals of any other type keep their normal load.
  • Preserve remote names verbatim, including amd64 $INODE64 variants.
  • Route the variadic imports through fixed-signature C wrappers via a shared darwinVariadicImports table. Of the symbols Darwin's generated syscall wrappers import (zsyscall_darwin_*.go in x/sys and the standard library), exactly open, openat, fcntl, and ioctl are variadic. The syscall engine calls imported addresses through fixed-signature function pointers (tinygo_syscallX and friends) that pass every argument in a register, while a variadic callee reads its variadic arguments from the stack on darwin/arm64, so direct calls silently receive garbage in the variadic slot (observed: EFAULT from ioctl; unix.Open creating files with mode 0 instead of the requested mode).
  • In a separate commit, point the existing stdlib FuncPCABI0 trampoline lowering (createDarwinFuncPCABI0Call) at the same table, replacing its open-only special case. This fixes the pre-existing silent bug on the stdlib path: syscall.SetNonblock (fcntl F_SETFL with the new flags in the variadic slot) observably wrote garbage file flags on darwin/arm64, and the stdlib ioctl and openat trampolines were equally unsound.
  • Extend the focused compiler IR test: pragma parsing including the short directive forms, exact remote-symbol preservation, load replacement, wrapper routing for all four variadic imports (with no direct declarations for them), and the non-uintptr-global guard.

The companion _ioctl and ___sincos_stret stub additions landed upstream in macos-minimal-sdk#5 and reached dev with the macos-minimal-sdk v0.1.0 submodule update, so this branch (rebased onto that dev) builds and links darwin x/sys programs out of the box with no SDK changes of its own. No new stubs are needed for open, openat, or fcntl — those symbols are already in the stock stub list, and their prototypes come from the SDK's sys/fcntl.h.

Verification

  • go test -tags llvm22 ./compiler -count=1
  • 11-line golang.org/x/sys/unix.IoctlGetTermios repro under a PTY: returned a populated termios value and <nil>.
  • github.com/charmbracelet/x/term.IsTerminal under a PTY: true.
  • term.MakeRaw and term.Restore under a PTY: both <nil>.
  • Bubble Tea examples/spinner: initialized, rendered animated frames, and exited on q.
  • Bubble Tea examples/list-default: initialized, rendered the list UI, and exited on q.
  • Variadic-slot checks (x/sys path), asserting values that travel through the variadic parameter: unix.Open with mode 0o640 created the file with mode 640 exactly, unix.FcntlInt(F_DUPFD, 50) returned fd 50, and unix.Openat with mode 0o600 created mode 600 exactly. The pre-fix compiler produced mode 0, fd 49, and mode 41 on the same program — the silent-corruption failure mode.
  • Variadic-slot check (stdlib FuncPCABI0 path): syscall.SetNonblock(fd, true) followed by an F_GETFL readback yields flags exactly 0o4 (O_NONBLOCK), and a read from the empty pipe returns EAGAIN immediately. Without the FuncPCABI0 change, the same program showed garbage flags (0o20000110), O_NONBLOCK unset, and the read blocked forever; the blocking read was also reproduced on stock dev at f71b630 with a stdlib-only variant.

No startup panic or os/signal/SIGWINCH failure surfaced in this ladder.

Context

This is the actual current Darwin-native root cause behind #5365. It is the "class 2" failure described by @mparrett in #4794 and is independent of the public syscall.Syscall* API discussed there.

An earlier attempt in #5403 made public syscall.Syscall* calls work by lowering syscall numbers directly to raw Darwin kernel instructions. That was a separate route which bypassed Darwin's libc-based dispatch and was never used by the x/sys/Bubble Tea call chain.

After #5401 made x/sys's private linknames resolve, the failing program reached TinyGo's existing syscall.syscalln/syscall.rawsyscalln libc dispatcher with the correct ioctl arguments but a zero function pointer. The dispatcher was not broken; the assembly-generated bridge that supplied its libc function pointer was missing. This change supplies that address and lets x/sys use TinyGo's existing libc path as intended.

This does not implement public syscall.Syscall*-by-number on Darwin, nor does it guarantee that every libc symbol is already present in the macOS minimal SDK.

Signed-off-by: Evan Wies <evan@neomantra.net>
Route the remaining variadic imports (open, openat, fcntl) through
fixed-signature C wrappers the way ioctl already is: the syscall engine
calls imported addresses through fixed-signature function pointers, and
a variadic callee reads its variadic arguments from the stack on
darwin/arm64, so direct calls silently receive garbage arguments (an
x/sys unix.Open created files with mode 0 instead of the requested
mode). Of the symbols darwin's generated syscall wrappers import,
exactly open, openat, fcntl, and ioctl are variadic.

Also accept the one- and two-operand forms of //go:cgo_import_dynamic
like the gc compiler does, ignore the unused library operand, and only
replace loads of uintptr-typed trampoline globals.

Signed-off-by: Evan Wies <evan@neomantra.net>
The standard library's function-based trampoline pattern has the same
variadic problem as the address-global pattern: of the libc functions
darwin's zsyscall wrappers import, open, openat, fcntl, and ioctl are
variadic, but createDarwinFuncPCABI0Call only routed open through a
fixed-signature wrapper. As a result syscall.SetNonblock (fcntl F_SETFL
with the new flags in the variadic slot) observably wrote garbage file
flags on darwin/arm64, and the stdlib ioctl and openat paths were
equally unsound.

Use the shared darwinVariadicImports table for this path too, replacing
the open-only special case.

Signed-off-by: Evan Wies <evan@neomantra.net>
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.

darwin: x/sys //go:cgo_import_dynamic trampoline addresses are nil → SIGSEGV Tinygo doesn't build bubbletea/huh CLI applications on macOS.

1 participant