Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ func main() {

Because calls into C compile to native calls against the C ABI, there is no Go-to-C stack or scheduler transition, so frequent C calls stay cheap.

#### Promoted C variadic methods

Forwarding an incoming C ellipsis through a promoted-method wrapper, including
an interface call, is currently supported only on hosted native targets without
an explicit `-target` profile. The underlying C method must have a pointer
receiver. The wrapper and callee must have matching fixed LLVM prototypes and
calling conventions, with no result conversion. Fixed parameters must be
scalars, with zero or one scalar result: booleans, integers, floating-point
values, and pointers qualify.

Value receivers, aggregate fixed parameters or results, multiple results,
Wasm, and explicit `-target` profiles (including custom native profiles) remain
outside this support. Their existing wrapper lowering can silently drop the
incoming ellipsis; for example, a promoted value-receiver method called through
an interface still loses arguments at O0. Valid LLVM IR for an excluded wrapper
does not establish correct argument forwarding. Support and clear diagnostics
for these forms are tracked in [#2855](https://github.com/xgo-dev/llgo/issues/2855).

#### Windows API

On Windows, bind APIs declared with `WINAPI` or `__stdcall` through the `stdcall.` namespace. The convention is distinct on 386; Windows amd64 and arm64 use their unified native C ABI. An explicitly decorated 386 name such as `_MessageBoxW@16` is also accepted and is normalized to `MessageBoxW` on 64-bit targets.
Expand Down
2 changes: 1 addition & 1 deletion cl/_testrt/vamethod/in.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func main() {
// CHECK: br i1 %[[PTRNIL]], label %{{[^,]+}}, label %{{[^ ]+}}
// CHECK: call void @"{{.*}}/runtime/internal/runtime.AssertNilDeref"(i1 true)
// CHECK: %[[PTRFMT:[0-9]+]] = load ptr, ptr %[[PTRFIELD]], align 8
// CHECK: %[[PTRRET:[0-9]+]] = call i32 (ptr, ...) @printf(ptr %[[PTRFMT]])
// CHECK: %[[PTRRET:[0-9]+]] = {{(musttail )?}}call i32 (ptr, ...) @printf(ptr %[[PTRFMT]]{{(, \.\.\.)?}})
// CHECK: ret i32 %[[PTRRET]]

// CHECK-LABEL: define void @"main.(*CFmt).SetFormat"(
Expand Down
63 changes: 62 additions & 1 deletion cl/instr.go
Original file line number Diff line number Diff line change
Expand Up @@ -2665,7 +2665,13 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm
p.inCFunc = true
args := p.compileValues(b, args, kind)
p.inCFunc = false
ret = p.emitDo(b, act, ds, mayRecover, aFn.Expr, llssa.Builder.Call, args...)
if act == llssa.Call && p.forwardCVariadicWrapper(call) && b.CanForwardVarargs(aFn.Expr) {
// A tail transfer removes the synthetic frame itself, so no
// recover-frame alias may surround it (or run after the call).
ret = b.CallForwardVarargs(aFn.Expr, args...)
} else {
ret = p.emitDo(b, act, ds, mayRecover, aFn.Expr, llssa.Builder.Call, args...)
}
case goFunc:
args := p.compileValues(b, args, kind)
ret = p.emitDo(b, act, ds, mayRecover, aFn.Expr, llssa.Builder.Call, args...)
Expand Down Expand Up @@ -2820,6 +2826,61 @@ func (p *context) callEx(b llssa.Builder, act llssa.DoAction, call *ssa.CallComm
return
}

func (p *context) forwardCVariadicWrapper(call *ssa.CallCommon) bool {
fn := p.goFn
callee := call.Signature()
// Promoted pointer methods adjust the receiver and return the C call's
// result. compileValues omits the incoming ellipsis marker: the wrapper
// must forward the unknown register/stack arguments with a mandatory tail
// call. Wasm does not support variadic tail calls. Explicit -target
// profiles (including custom native profiles) remain excluded because
// their backend and storage ABI may not support this forwarding path.
// Excluded variadic wrappers keep their existing lowering, which can
// silently drop the incoming ellipsis. Correct forwarding or diagnostics
// for these forms is tracked in #2855.
target := p.prog.Target()
if target.GOARCH == "wasm" || target.Target != "" {
return false
}
if fn == nil || fn.Synthetic == "" {
return false
}
// Value receivers do not have LLVM's required matching fixed prototype.
if fn.Signature.Recv() == nil || !isPointerGoType(fn.Signature.Recv().Type()) ||
callee.Recv() == nil || !isPointerGoType(callee.Recv().Type()) {
return false
}
if !llssa.HasNameValist(fn.Signature) || !llssa.HasNameValist(callee) ||
len(call.Args) == 0 || len(fn.Params) == 0 {
return false
}
// Tuple returns need extract/insert instructions after the call, while
// aggregate ABI lowering can replace the call and lose its tail marker.
// Neither path can preserve the immediate return required by musttail.
results := callee.Results()
if results.Len() > 1 || results.Len() == 1 && !isCVariadicForwardScalar(results.At(0).Type()) {
return false
}
params := callee.Params()
for i := 0; i < params.Len()-1; i++ {
if !isCVariadicForwardScalar(params.At(i).Type()) {
return false
}
}
param, ok := call.Args[len(call.Args)-1].(*ssa.Parameter)
return ok && param == fn.Params[len(fn.Params)-1]
}

func isCVariadicForwardScalar(t types.Type) bool {
switch t := t.Underlying().(type) {
case *types.Pointer:
return true
case *types.Basic:
return t.Info()&(types.IsBoolean|types.IsInteger|types.IsFloat) != 0 || t.Kind() == types.UnsafePointer
}
return false
}

func (p *context) compileDynamicCallValues(b llssa.Builder, call *ssa.CallCommon, hasVArg int) []llssa.Expr {
args := p.compileValues(b, call.Args, hasVArg)
params := call.Signature().Params()
Expand Down
172 changes: 172 additions & 0 deletions cl/instr_variadic_method_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package cl

import (
"os"
"strings"
"testing"

"github.com/xgo-dev/llgo/internal/cabi"
"github.com/xgo-dev/llgo/internal/optlevel"
llssa "github.com/xgo-dev/llgo/ssa"
"github.com/xgo-dev/llvm"
)

func TestCVariadicMethodWrapperForwarding(t *testing.T) {
source, err := os.ReadFile("_testrt/vamethod/in.go")
if err != nil {
t.Fatal(err)
}
ssaPkg, _, files := buildGoSSAPkg(t, string(source))
for _, target := range []llssa.Target{
{GOOS: "linux", GOARCH: "amd64"},
{GOOS: "linux", GOARCH: "arm64"},
{GOOS: "linux", GOARCH: "386"},
{GOOS: "linux", GOARCH: "arm"},
{GOOS: "linux", GOARCH: "riscv64"},
{GOOS: "darwin", GOARCH: "amd64"},
{GOOS: "darwin", GOARCH: "arm64"},
{GOOS: "windows", GOARCH: "amd64", LLVMTarget: "x86_64-pc-windows-msvc"},
{GOOS: "windows", GOARCH: "amd64", LLVMTarget: "x86_64-w64-windows-gnu"},
{GOOS: "windows", GOARCH: "arm64", LLVMTarget: "aarch64-pc-windows-msvc"},
{GOOS: "windows", GOARCH: "arm64", LLVMTarget: "aarch64-w64-windows-gnu"},
{GOOS: "windows", GOARCH: "386", LLVMTarget: "i686-pc-windows-msvc"},
{GOOS: "windows", GOARCH: "386", LLVMTarget: "i686-w64-windows-gnu"},
} {
t.Run(target.GOOS+"/"+target.GOARCH+"/"+target.LLVMTarget, func(t *testing.T) {
target.OptLevel = optlevel.O0
prog := newLLSSAProgForTarget(t, &target)
defer prog.Dispose()
pkg, err := NewPackage(prog, ssaPkg, files)
if err != nil {
t.Fatal(err)
}
mod := pkg.Module()
cabi.NewTransformer(prog, target.LLVMTarget, "", false).TransformModule(pkg.Path(), mod)
fn := mustNamedFunction(t, mod, "main.(*CFmt).Printf")
if text := fn.String(); !strings.Contains(text, "musttail call i32 (ptr, ...) @printf(ptr ") ||
!strings.Contains(text, ", ...)") {
t.Fatalf("wrapper does not forward its incoming ellipsis:\n%s", text)
}
if fn.GetStringAttributeAtIndex(-1, "thunk").IsNil() {
t.Fatal("variadic forwarding requires the LLVM thunk attribute")
}
if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil {
t.Fatal(err)
}
object, err := prog.TargetMachine().EmitToMemoryBuffer(mod, llvm.ObjectFile)
if err != nil {
t.Fatal(err)
}
object.Dispose()
})
}
}

func TestCVariadicMethodExternalDeclaration(t *testing.T) {
// An external declaration has no Go body to prove that it cannot recover.
// The forwarding thunk must still tail-call it without a post-call alias
// cleanup, and preserve its extra fixed arguments and void result.
const source = `package foo
type T int8
//llgo:link (*T).Check C.check
func (*T) Check(tag int32, number float64, data *int8, __llgo_va_list ...any)
type Wrapper struct { *T }
func use(w *Wrapper) { w.Check(7, 1.5, new(int8), int32(42)) }
`
ssaPkg, _, files := buildGoSSAPkg(t, source)
prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "linux", GOARCH: "amd64", OptLevel: optlevel.O0})
defer prog.Dispose()
pkg, err := NewPackage(prog, ssaPkg, files)
if err != nil {
t.Fatal(err)
}
fn := mustNamedFunction(t, pkg.Module(), "foo.(*Wrapper).Check")
if text := fn.String(); !strings.Contains(text, "musttail call void (ptr, i32, double, ptr, ...) @check(") ||
strings.Contains(text, "RecoverFrameAlias") {
t.Fatalf("external variadic declaration is not tail-forwarded:\n%s", text)
}
if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil {
t.Fatal(err)
}
}

func TestCVariadicValueReceiverWrapperIR(t *testing.T) {
const source = `package foo
type T int32
//llgo:link T.Check C.check
func (T) Check(__llgo_va_list ...any) int32 { return 0 }
type Wrapper struct { T }
func use(w *Wrapper) int32 { return w.Check(int32(42)) }
`
ssaPkg, _, files := buildGoSSAPkg(t, source)
prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "linux", GOARCH: "amd64"})
defer prog.Dispose()
pkg, err := NewPackage(prog, ssaPkg, files)
if err != nil {
t.Fatal(err)
}
// Loading a scalar receiver changes the fixed ABI from ptr to i32. Such
// wrappers cannot use LLVM's variadic musttail forwarding mechanism.
// This checks only valid IR and exclusion from the forwarding path; the
// ordinary call can still lose incoming arguments through an interface.
// Correct forwarding or diagnostics is tracked in #2855.
fn := mustNamedFunction(t, pkg.Module(), "foo.(*Wrapper).Check")
if text := fn.String(); !strings.Contains(text, "call i32 (i32, ...) @check(") || strings.Contains(text, "musttail") {
t.Fatalf("excluded receiver ABI unexpectedly changed lowering:\n%s", text)
}
if err := llvm.VerifyModule(pkg.Module(), llvm.ReturnStatusAction); err != nil {
t.Fatal(err)
}
}

func TestCVariadicIncompatibleSignatureIR(t *testing.T) {
// Ordinary methods have no ellipsis to forward. Aggregate signatures need
// instructions or ABI rewrites that cannot retain a musttail call followed
// immediately by its return. These checks establish valid IR with the
// existing lowering, not correct ellipsis forwarding for excluded variadic
// signatures. Execution coverage and diagnostics are tracked in #2855.
for _, tc := range []struct {
name, declaration, use string
}{
{"non_variadic",
"func (*T) Check(value int32) int32 { return 0 }",
"func use(w *Wrapper) int32 { return w.Check(42) }"},
{"multiple_results",
"func (*T) Check(__llgo_va_list ...any) (int32, int32) { return 0, 0 }",
"func use(w *Wrapper) (int32, int32) { return w.Check(int32(42)) }"},
{"aggregate_result",
"func (*T) Check(__llgo_va_list ...any) R { return R{} }",
"func use(w *Wrapper) R { return w.Check(int32(42)) }"},
{"aggregate_parameter",
"func (*T) Check(tag R, __llgo_va_list ...any) int32 { return 0 }",
"func use(w *Wrapper) int32 { return w.Check(R{}, int32(42)) }"},
} {
t.Run(tc.name, func(t *testing.T) {
source := "package foo\ntype T int8\ntype R struct { a, b int64 }\n" +
"//llgo:link (*T).Check C.check\n" + tc.declaration +
"\ntype Wrapper struct { *T }\n" + tc.use
ssaPkg, _, files := buildGoSSAPkg(t, source)
prog := newLLSSAProgForTarget(t, &llssa.Target{GOOS: "linux", GOARCH: "amd64"})
defer prog.Dispose()
pkg, err := NewPackage(prog, ssaPkg, files)
if err != nil {
t.Fatal(err)
}
mod := pkg.Module()
fn := mustNamedFunction(t, mod, "foo.(*Wrapper).Check")
if text := fn.String(); !strings.Contains(text, "@check(") || strings.Contains(text, "musttail") {
t.Fatalf("excluded wrapper unexpectedly changed lowering:\n%s", text)
}
if !fn.GetStringAttributeAtIndex(-1, "thunk").IsNil() {
t.Fatal("incompatible wrapper must not be marked as a forwarding thunk")
}
if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil {
t.Fatal(err)
}
cabi.NewTransformer(prog, "", "", false).TransformModule(pkg.Path(), mod)
if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil {
t.Fatal(err)
}
})
}
}
18 changes: 18 additions & 0 deletions internal/build/testdata/variadicmethod/_wrap/check.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include <stdarg.h>
#include <stdint.h>

int32_t variadic_method_check(const int32_t *marker, ...) {
if (*marker == 0) return 42;
if (*marker != 7) return -1;
va_list args;
va_start(args, marker);
int ok = 1;
for (int i = 0; i < 10; i++) {
if (va_arg(args, int) != 100 + i) ok = 0;
}
for (int i = 0; i < 10; i++) {
if (va_arg(args, double) != i + 0.25) ok = 0;
}
va_end(args);
return ok ? 42 : -1;
}
69 changes: 69 additions & 0 deletions internal/build/testdata/variadicmethod/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import _ "unsafe"

const LLGoFiles = "_wrap/check.c"

type Receiver int32

//llgo:link (*Receiver).Check C.variadic_method_check
func (*Receiver) Check(__llgo_va_list ...any) int32 { return 0 }

type Embedded struct {
padding [3]int64
*Receiver
}

type Nested struct {
padding int64
*Embedded
}

type Checker interface {
Check(__llgo_va_list ...any) int32
}

//go:noinline
func check(value Checker) {
// More arguments than the native GP/FP register sets can hold: both
// register and stack ellipsis arguments must survive receiver adjustment.
if value.Check(
int32(100), int32(101), int32(102), int32(103), int32(104),
int32(105), int32(106), int32(107), int32(108), int32(109),
0.25, 1.25, 2.25, 3.25, 4.25, 5.25, 6.25, 7.25, 8.25, 9.25,
) != 42 {
panic("variadic method arguments were corrupted")
}
}

//go:noinline
func empty(value Checker) {
if value.Check() != 42 {
panic("empty variadic method call failed")
}
}

func checkNil(value Checker) {
defer func() {
if recover() == nil {
panic("nil promoted receiver did not panic")
}
}()
check(value)
}

func main() {
marker := Receiver(7)
embedded := Embedded{Receiver: &marker}
nested := Nested{Embedded: &embedded}
check(&embedded)
check(embedded)
check(&nested)
check(nested)
marker = 0
empty(&embedded)
empty(&nested)
checkNil((*Embedded)(nil))
checkNil(&Nested{})
println("variadic-method-ok")
}
Loading
Loading