Skip to content

feat(c): add libc bindings with native ABI coverage - #45

Open
MeteorsLiu wants to merge 9 commits into
goplus:mainfrom
MeteorsLiu:codex/c-native-shared-types
Open

MeteorsLiu wants to merge 9 commits into
goplus:mainfrom
MeteorsLiu:codex/c-native-shared-types

Conversation

@MeteorsLiu

@MeteorsLiu MeteorsLiu commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Add generated libc bindings and native ABI checks for macOS arm64, Linux amd64 and Linux arm64 while preserving existing handwritten APIs.

The implementation includes:

  • Retain handwritten entry points, including c.Char and c.Printf, omit duplicate generated Native* declarations, and defer the 36 macOS functions that require the generated C wrapper mechanism.
  • Exclude private implementation entry points: 27 on macOS and 35 on each Linux target. Keep public aliases such as _Exit, _toupper, _tolower, XPG basename and strerror_r, together with native types needed for ABI mappings.
  • Use explicit C. targets for the macOS SysNerr, SysErrlist and Suboptarg native variables, represent the external sys_errlist array as [0]*Char, and retain Linux Cfree through its native free alias.
  • Declare LLGoPackage directly under platform build tags. Linux amd64 and arm64 use "link" so generated Go union accessors are compiled; other targets retain their previous package mode. All 47 Go target pairs select exactly one declaration.
  • Check every retained new C function's link, parameter/result count, variadic status, size, alignment and ABI class against the original C headers. Check new native type layouts, constants and variables, exercise the Go union accessors and representative conversions, allocation, callbacks, memory/string and binary stdio calls, and fail the inventory check when a generated public symbol lacks a test.

Validation after rebasing onto goplus/lib main (d83f71e1): the CI-pinned LLGo dbce82ab passes llgo test -count=1 -v ./c, normally and with -tags=nogc, with 841 tests/subtests on Linux amd64 and 840 on Linux arm64. The nine-package libc integration build passes in all four Linux configurations. macOS arm64 CI passes 676 root-package tests/subtests, including native global storage checks. All four GitHub Actions jobs pass on this head. Every native binding has an explicit C./stdcall. target, compiler intrinsics retain llgo.*, and imported native functions remain bodyless declarations. Native C prototype and availability checks cover all 204 retained new C entries on macOS and all 214 on each Linux target.

Generated with unmodified llcppg main da227ff4, macOS SDK 14.0 and Bootlin stable glibc 2.24 headers. Previously approved va_list and verified Linux long-double generation/ABI exclusions remain. Runtime results cover the current native environments; the SDK/sysroot versions provide the header baseline.

The root package adds tested bindings without replacing existing handwritten signatures or publishing the deferred C wrappers and private implementation functions.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review — C-native shared types

Adds generated libc type bindings: Iovec (c), SchedParam/MachPortT/SemaphoreT (c/os), SigsetT (c/signal), plus the matching llcppg.pub entries.

ABI/memory-layout: verified correct on both targets. I independently checked each struct's size/alignment and cross-referenced the real C headers and the existing c/syscall/ztypes_* references:

Type linux/amd64 darwin/arm64 C ABI
Iovec ptr + size = 16B ptr + size = 16B struct iovec 16B ✓
SchedParam int = 4B int + char[4] = 8B glibc 4B / macOS 8B ✓
SigsetT [16]Ulong = 128B uint32 = 4B glibc 1024-bit / __darwin_sigset_t ✓
MachPortT/SemaphoreT — uint = 4B mach_port_t ✓

Build tags correctly gate each definition to the architecture whose ABI it models, and both targets cross-compile cleanly. No memory-safety or correctness issues — the undersized-sigset_t-OOB risk does not materialize.

The remaining notes are generated-code consistency / documentation nits (left inline). The highest-value one is the two iovec files using a non-standard // Code generated ... header; the rest are cosmetic. Since these are generated artifacts, the durable fix is in the llcppg generator / its post-processing rather than hand edits, so they don't regress on the next regeneration.

Comment thread c/iovec_darwin_arm64.go Outdated
@@ -0,0 +1,11 @@
//go:build darwin && arm64 && !ios

// Code generated from the libc binding; DO NOT EDIT.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-standard generated-code header. This file (and c/iovec_linux_amd64.go:3) use // Code generated from the libc binding; DO NOT EDIT., while every other file in this PR and across the repo uses the canonical // Code generated by llcppg; DO NOT EDIT. — these are the only two files with the different wording.

Both variants satisfy Go's ^// Code generated .* DO NOT EDIT\.$ detection, so this is not a functional bug. But any repo tooling/CI/regeneration script that keys on the exact by llcppg string would miss these two files, and the differing provenance string is confusing. Best fixed in the generator so it doesn't recur on regeneration.

Comment thread c/iovec_darwin_arm64.go Outdated
// Iovec describes one buffer for scatter/gather I/O.
type Iovec struct {
IovBase Pointer
IovLen Ulong

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IovLen type differs across platforms (minor, no ABI impact). Here it is Ulong; the linux file (c/iovec_linux_amd64.go:10) uses SizeT. The C field iov_len is size_t on both platforms, so SizeT would be the faithful and cross-platform-consistent mapping.

This is cosmetic only: on darwin/arm64 and linux/amd64 both Ulong and SizeT are 64-bit, so the struct size/layout is identical (16B) and there is no runtime bug. Worth reconciling to SizeT on both so portable code against c.Iovec.IovLen sees one type regardless of target. Confirm it reflects actual llcppg output rather than a hand edit.

Comment thread c/signal/types_darwin_arm64.go Outdated

type SigsetT = X__DarwinSigsetT

// [???] signal set

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Generator placeholder leaked into a doc comment. // [???] signal set contains an unresolved [???] marker that will show up in godoc. The linux counterpart (c/signal/types_linux_amd64.go:13) has a clean comment: // A set of signals to be blocked, unblocked, or waited for.

Suggest emitting a real description (or dropping the placeholder). Note it also currently attaches to X__DarwinSigsetT, leaving the exported SigsetT (line 11) undocumented — unlike the linux file which documents SigsetT directly.

Comment thread c/os/mach_darwin_arm64.go Outdated

import "github.com/goplus/lib/c"

// Canonical Mach typedefs from the macOS 14.0 SDK.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor doc nits. The declarations are correct (MachPortT = c.Uint matches mach_port_t→natural_t→unsigned int; SemaphoreT = MachPortT matches semaphore_t→mach_port_t). Two small things:

  • The comment is phrased for multiple "typedefs" (plural) but, per Go doc-comment association, it attaches only to MachPortT; SemaphoreT ends up undocumented.
  • Pinning "macOS 14.0 SDK" is an arbitrary/unverifiable version claim for typedefs that are stable across SDKs — consider dropping the version.

Comment thread c/os/sched_param_darwin_arm64.go Outdated

import "github.com/goplus/lib/c"

type SchedParam struct {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing doc comment (consistency). The exported SchedParam has no doc comment here, while the linux counterpart (c/os/sched_param_linux_amd64.go:9) carries // Data structure to describe a process' schedulability. The X__Opaque [4]c.Char padding itself is correct and required (matches macOS struct sched_param, 8B) — this is purely a docs-coverage inconsistency across the two platform files.

@MeteorsLiu
MeteorsLiu force-pushed the codex/c-native-shared-types branch from 82b5401 to e3a4dec Compare October 19, 2026 06:00
@MeteorsLiu MeteorsLiu changed the title feat(c): add shared native platform types feat(c): add shared native iovec types Oct 19, 2026
@MeteorsLiu
MeteorsLiu marked this pull request as draft October 19, 2026 06:05
@MeteorsLiu

MeteorsLiu commented Oct 19, 2026 •

Copy link
Copy Markdown
Member Author

Merge order: #45 → #51 → #48 → #49 → #44 → #47 → #46 → #50 → #52.

Each draft changes one folder only. This refresh regenerates native APIs without omitting functions merely to preserve handwritten entry points. Existing compatibility APIs remain available; colliding native names use the Native prefix.

The combined prerequisite snapshot passes llgo build and executed llgo test runs on macOS arm64, Linux amd64 and Linux arm64, both with GC and -tags=nogc (54 package test runs and 6 complete builds). Generation uses unmodified llcppg main da227ff4, macOS SDK 14.0 and Bootlin glibc 2.24 headers. The earlier Timeval receiver and Signal layout failures are resolved.

These PRs remain draft for review. Documented generator/ABI failures and previously approved va_list exclusions remain; there are zero handwritten-retention exclusions.

@MeteorsLiu MeteorsLiu changed the title feat(c): add shared native iovec types feat(c): share native iovec across supported targets Oct 19, 2026
@MeteorsLiu MeteorsLiu changed the title feat(c): share native iovec across supported targets feat(c): add shared native iovec layouts Oct 19, 2026
@MeteorsLiu MeteorsLiu changed the title feat(c): add shared native iovec layouts feat(c): add common libc bindings from unified generation Oct 19, 2026
@MeteorsLiu MeteorsLiu changed the title feat(c): add common libc bindings from unified generation feat(c): restore native libc APIs from unified generation Oct 20, 2026
@MeteorsLiu MeteorsLiu changed the title feat(c): restore native libc APIs from unified generation feat(c): add libc APIs while preserving handwritten entry points Oct 20, 2026
@MeteorsLiu MeteorsLiu changed the title feat(c): add libc APIs while preserving handwritten entry points feat(c): add libc bindings with native ABI coverage Oct 20, 2026
@MeteorsLiu
MeteorsLiu marked this pull request as ready for review October 20, 2026 11:19

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review: native (link-mode) libc bindings

Reviewed the diff with code-quality, performance, security, and documentation-accuracy passes. This PR adds llcppg-generated native libc bindings for darwin/arm64, linux/amd64 and linux/arm64, moves LLGoPackage into platform-specific files, and adds a thorough ABI/symbol test harness.

Overall this looks solid. A few things stand out:

  • LLGoPackage coverage is correct. After the move, LLGoPackage is defined exactly once for every target — darwin (stdio_darwin.go), windows (stdio_windows.go), linux/{amd64,arm64}/!android (the generated libc_linux_*.go), and the new catch-all package_default_other.go (!darwin && !windows && !(linux && (amd64 || arm64) && !android)). No double definition, no uncovered platform; removing the const from stdio_default.go was the right complement.
  • The test harness is well designed. The symbol inventory subtest cross-checks the generated source AST against the hand-maintained tables, so a newly generated symbol without a corresponding test fails loudly — good low-maintenance coverage.

The inline comments below cover one portability inconsistency worth addressing plus a few minor cleanups.

Minor (no inline anchor)

  • Missing license header. package_default_other.go and the four new hand-written test files (generated_native_llgo_test.go, libc_native_{linux_amd64,linux_arm64,darwin_arm64}_llgo_test.go) lack the // Copyright (c) 2026 The XGo Authors. Licensed under the Apache License 2.0. header that sibling files carry. They aren't marked as generated (DO NOT EDIT), so the generated-file exemption doesn't apply — worth adding for consistency.
  • nativeChar signedness divergence (FYI). libc_native_linux_arm64_llgo_test.go aliases nativeChar = uint8 while the package-wide c.Char = int8 (c.go:26, untagged) also applies on arm64. On ARM char is unsigned, so uint8 is physically correct, but the harness papers over a representation mismatch between c.Char and the arm64 bindings. ABI size/class are unaffected; flagging only so the divergence is a conscious choice.

Comment thread c/c_test.go
@@ -1,3 +1,5 @@
//go:build !(linux && arm64 && !android)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The exclusion only special-cases linux/arm64, but c.LongDouble/c.LongDoubleSize are defined solely by ldouble_float64.go and ldouble_x87.go, whose tags cover linux only on amd64, arm, mips, mipsle. So on other linux arches that otherwise compile (e.g. 386, riscv64, s390x — all have Long defined via ctypes_*.go), c.LongDouble is still undefined and a plain go test ./c fails to compile c_test.go exactly as it did before, since those arches aren't excluded here.

This fixes the one arch the PR adds bindings for while leaving the broader gap. Consider tying the constraint to the real LongDouble coverage (mirror the ldouble_*.go union) or moving TestLongDouble into its own file tagged identically to ldouble_*.go, and adding a one-line comment explaining the cross-file coupling.

}},
}

// Expected values were compiled from the original macOS 14.0 / glibc 2.24 C headers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The macOS 14.0 / half looks like a copy-paste artifact from the darwin harness. This file is linux && amd64 only and its values are glibc-only (e.g. FNM_PATHNAME=1, LC_ALL=6, BUFSIZ=8192). Suggest referencing only the glibc 2.24 headers here.

}},
}

// Expected values were compiled from the original macOS 14.0 / glibc 2.24 C headers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same stale dual-attribution as the amd64 harness: this linux && arm64 file only contains glibc values, so the macOS 14.0 / reference is misleading — drop it.

check func(*testing.T)
}{}

// Expected values were compiled from the original macOS 14.0 / glibc 2.24 C headers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Inverse of the linux files: this darwin && arm64 file's values are macOS-only (e.g. FNM_PATHNAME=2, LC_ALL=0, BUFSIZ=1024), so the / glibc 2.24 attribution is inaccurate here. (The dual mention is fine in the shared libc_native_llgo_test.go, which compiles for all three targets.)

if pointer, ok := receiver.(*ast.StarExpr); ok {
receiver, prefix = pointer.X, "*"
}
id := receiver.(*ast.Ident).Name

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor/defensive: receiver.(*ast.Ident).Name is an unchecked type assertion. If the generator ever emits a non-*ast.Ident receiver (e.g. a generic/indexed receiver), this panics instead of failing cleanly. A comma-ok form with t.Fatalf on an unexpected shape would give a clearer diagnostic. Not a bug today — current bindings only emit simple identifier receivers.

@MeteorsLiu
MeteorsLiu marked this pull request as draft October 20, 2026 12:28
@MeteorsLiu
MeteorsLiu marked this pull request as ready for review October 20, 2026 12:31

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review summary

Nicely structured change. It moves the Linux amd64/arm64 libc bindings to generated native (LLGoPackage = "link") files, relocates the "decl" fallback into the new package_default_other.go, and adds a thorough native ABI / symbol-inventory / runtime test suite.

Strong points verified during review:

  • The LLGoPackage build-constraint partition across all files is clean and exhaustive — exactly one definition per platform, no gaps or overlaps (including linux/386 and android/arm64 falling through to the new package_default_other.go).
  • The per-arch nativeChar / nativeWide / nativeFilePosition aliases correctly model real ABI differences (unsigned char and wchar_t width on linux/arm64; glibc fpos_t 16 vs darwin 8).
  • Header-value comments (SEEK_SET==0, EOF==-1, glibc 2.24 / macOS 14 struct sizes) are accurate, and the shared runtime tests only use symbols present on all three native targets.
  • Dedicated security and performance passes found nothing of concern (bindings are thin //go:linkname stubs; test buffers are correctly bounded).

A few observations below — none blocking.

Additional minor notes (no reliable inline anchor):

  • c/libc_native_llgo_test.go:73-74 reads libc_<GOOS>_<GOARCH>.go via a relative path, relying on go test running with the package dir as CWD. A one-line comment would make a future CWD-related t.Fatal(err) less puzzling.
  • c/package_default_other.go has no comment explaining why the fallback is declaration-only, unlike the explanatory note on stdio_windows.go. Pure nit.

Comment thread c/c_test.go
@@ -1,3 +1,5 @@
//go:build !(linux && arm64 && !android)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This exclusion is needed because c.LongDouble / c.LongDoubleSize are undefined on linux/arm64: ldouble_x87.go is amd64-only and ldouble_float64.go's constraint covers arm (32-bit) but not linux && arm64. So the build tag works around a real platform gap rather than fixing it — on linux/arm64 c.LongDouble simply does not exist, even though the other two native targets (linux/amd64, darwin/arm64) have it, so any user code referencing it there won't compile. Note AArch64 long double is IEEE binary128 (16 bytes), so adding linux/arm64 to ldouble_float64.go (8 bytes) would be wrong — it needs its own 16-byte definition. Consider a brief comment here explaining the exclusion, and tracking proper linux/arm64 LongDouble support as follow-up.

continue
}
if decl.Tok == token.CONST && name.Name == "LLGoPackage" {
if c.LLGoPackage != "link" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This LLGoPackage == "link" assertion only runs when the parsed generated file actually declares an LLGoPackage const. The Linux generated files do (libc_linux_{amd64,arm64}.go set it to "link"), but libc_darwin_arm64.go does not — so on darwin this branch is a silent no-op, and darwin instead resolves LLGoPackage to "decl" from the unchanged stdio_darwin.go. Yet the darwin native tests still exercise real linked libc calls (Strtold, TestNativeLibcStdio, etc.), identical in spirit to the Linux ones that require "link". So the three native targets disagree on LLGoPackage while the test meant to guard it doesn't fire on darwin. Could you confirm darwin is intended to stay "decl"? If it should be "link" too, this is a latent linking inconsistency; if "decl" is correct for darwin, the check should assert a per-platform expected value instead of unconditionally "link".

if pointer, ok := receiver.(*ast.StarExpr); ok {
receiver, prefix = pointer.X, "*"
}
id := receiver.(*ast.Ident).Name

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

receiver.(*ast.Ident).Name (and the *ast.StarExpr assertion above) assume every exported method receiver is Ident or *Ident. Any other receiver shape would panic the test rather than fail it cleanly. Fine for today's generated libc output, but a , ok check with a t.Fatalf would degrade gracefully if the generator ever emits a different receiver form.

@MeteorsLiu
MeteorsLiu force-pushed the codex/c-native-shared-types branch from d33ea8b to 99ddac9 Compare October 21, 2026 13:11
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