Skip to content
Draft
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
144 changes: 144 additions & 0 deletions src/cmd/cgo/internal/testout/out_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package out_test

import (
"bufio"
"bytes"
"fmt"
"internal/testenv"
"internal/goarch"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"testing"
)

type methodAlign struct {
Method string
Align int
}

var wantAligns = map[string]int{
"ReturnEmpty": 1,
"ReturnOnlyUint8": 1,
"ReturnOnlyUint16": 2,
"ReturnOnlyUint32": 4,
"ReturnOnlyUint64": goarch.PtrSize,
"ReturnOnlyInt": goarch.PtrSize,
"ReturnOnlyPtr": goarch.PtrSize,
"ReturnByteSlice": goarch.PtrSize,
"ReturnString": goarch.PtrSize,
"InputAndReturnUint8": 1,
"MixedTypes": goarch.PtrSize,
}

// TestAligned tests that the generated _cgo_export.c file has the wanted
// align attributes for struct types used as arguments or results of
// //exported functions.
func TestAligned(t *testing.T) {
testenv.MustHaveGoRun(t)
testenv.MustHaveCGO(t)

testdata, err := filepath.Abs("testdata")
if err != nil {
t.Fatal(err)
}

objDir := t.TempDir()

cmd := testenv.Command(t, testenv.GoToolPath(t), "tool", "cgo",
"-objdir", objDir,
filepath.Join(testdata, "aligned.go"))
cmd.Stderr = new(bytes.Buffer)

err = cmd.Run()
if err != nil {
t.Fatalf("%#q: %v\n%s", cmd, err, cmd.Stderr)
}

haveAligns, err := parseAlign(filepath.Join(objDir, "_cgo_export.c"))
if err != nil {
t.Fatal(err)
}

// Check that we have all the wanted methods
if len(haveAligns) != len(wantAligns) {
t.Fatalf("have %d methods with aligned, want %d", len(haveAligns), len(wantAligns))
}

for i := range haveAligns {
method := haveAligns[i].Method
haveAlign := haveAligns[i].Align

wantAlign, ok := wantAligns[method]
if !ok {
t.Errorf("method %s: have aligned %d, want missing entry", method, haveAlign)
} else if haveAlign != wantAlign {
t.Errorf("method %s: have aligned %d, want %d", method, haveAlign, wantAlign)
}
}
}

func parseAlign(filename string) ([]methodAlign, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()

var results []methodAlign
scanner := bufio.NewScanner(file)

// Regex to match function declarations like "struct MethodName_return MethodName("
funcRegex := regexp.MustCompile(`^struct\s+(\w+)_return\s+(\w+)\(`)
// Regex to match simple function declarations like "GoSlice MethodName("
simpleFuncRegex := regexp.MustCompile(`^Go\w+\s+(\w+)\(`)
// Regex to match void-returning exported functions like "void ReturnEmpty("
voidFuncRegex := regexp.MustCompile(`^void\s+(\w+)\(`)
// Regex to match align attributes like "__attribute__((aligned(8)))"
alignRegex := regexp.MustCompile(`__attribute__\(\(aligned\((\d+)\)\)\)`)

var currentMethod string

for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())

// Check if this line declares a function with struct return type
if matches := funcRegex.FindStringSubmatch(line); matches != nil {
currentMethod = matches[2] // Extract the method name
} else if matches := simpleFuncRegex.FindStringSubmatch(line); matches != nil {
// Check if this line declares a function with simple return type (like GoSlice)
currentMethod = matches[1] // Extract the method name
} else if matches := voidFuncRegex.FindStringSubmatch(line); matches != nil {
// Check if this line declares a void-returning function
currentMethod = matches[1] // Extract the method name
}

// Check if this line contains align information
if alignMatches := alignRegex.FindStringSubmatch(line); alignMatches != nil && currentMethod != "" {
alignStr := alignMatches[1]
align, err := strconv.Atoi(alignStr)
if err != nil {
// Skip this entry if we can't parse the align as integer
currentMethod = ""
continue
}
results = append(results, methodAlign{
Method: currentMethod,
Align: align,
})
currentMethod = "" // Reset for next method
}
}

if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error reading file: %w", err)
}

return results, nil
}
63 changes: 63 additions & 0 deletions src/cmd/cgo/internal/testout/testdata/aligned.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import "C"

//export ReturnEmpty
func ReturnEmpty() {
return
}

//export ReturnOnlyUint8
func ReturnOnlyUint8() (uint8, uint8, uint8) {
return 1, 2, 3
}

//export ReturnOnlyUint16
func ReturnOnlyUint16() (uint16, uint16, uint16) {
return 1, 2, 3
}

//export ReturnOnlyUint32
func ReturnOnlyUint32() (uint32, uint32, uint32) {
return 1, 2, 3
}

//export ReturnOnlyUint64
func ReturnOnlyUint64() (uint64, uint64, uint64) {
return 1, 2, 3
}

//export ReturnOnlyInt
func ReturnOnlyInt() (int, int, int) {
return 1, 2, 3
}

//export ReturnOnlyPtr
func ReturnOnlyPtr() (*int, *int, *int) {
a, b, c := 1, 2, 3
return &a, &b, &c
}

//export ReturnString
func ReturnString() string {
return "hello"
}

//export ReturnByteSlice
func ReturnByteSlice() []byte {
return []byte{1, 2, 3}
}

//export InputAndReturnUint8
func InputAndReturnUint8(a, b, c uint8) (uint8, uint8, uint8) {
return a, b, c
}

//export MixedTypes
func MixedTypes(a uint8, b uint16, c uint32, d uint64, e int, f *int) (uint8, uint16, uint32, uint64, int, *int) {
return a, b, c, d, e, f
}
13 changes: 12 additions & 1 deletion src/cmd/cgo/out.go
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,8 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) {
fmt.Fprintf(gotype, "struct {\n")
off := int64(0)
npad := 0
// the align is at least 1 (for char)
maxAlign := int64(1)
argField := func(typ ast.Expr, namePat string, args ...interface{}) {
name := fmt.Sprintf(namePat, args...)
t := p.cgoType(typ)
Expand All @@ -963,6 +965,11 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) {
noSourceConf.Fprint(gotype, fset, typ)
fmt.Fprintf(gotype, "\n")
off += t.Size
// keep track of the maximum alignment among all fields
// so that we can align the struct correctly
if t.Align > maxAlign {
maxAlign = t.Align
}
}
if fn.Recv != nil {
argField(fn.Recv.List[0].Type, "recv")
Expand Down Expand Up @@ -1051,7 +1058,11 @@ func (p *Package) writeExports(fgo2, fm, fgcc, fgcch io.Writer) {
// string.h for memset, and is also robust to C++
// types with constructors. Both GCC and LLVM optimize
// this into just zeroing _cgo_a.
fmt.Fprintf(fgcc, "\ttypedef %s %v _cgo_argtype;\n", ctype.String(), p.packedAttribute())
//
// The struct should be aligned to the maximum alignment
// of any of its fields. This to avoid alignment
// issues.
fmt.Fprintf(fgcc, "\ttypedef %s %v __attribute__((aligned(%d))) _cgo_argtype;\n", ctype.String(), p.packedAttribute(), maxAlign)
fmt.Fprintf(fgcc, "\tstatic _cgo_argtype _cgo_zero;\n")
fmt.Fprintf(fgcc, "\t_cgo_argtype _cgo_a = _cgo_zero;\n")
if gccResult != "void" && (len(fntype.Results.List) > 1 || len(fntype.Results.List[0].Names) > 1) {
Expand Down