PureGo Version
main @ 763bb99
Operating System
Go Version (go version)
go1.26.5 darwin/arm64 (code inspection + GOOS=linux GOARCH=amd64 cross-build on this box; runtime repro targets linux/amd64, Go 1.26)
What steps will reproduce the problem?
getStruct (struct_amd64.go:61-94), which restores ≤16-byte struct returns on amd64 SysV, classifies the two eightbytes with ad-hoc field loops instead of classifyEightbyte (which getCallbackStruct/setStruct already use). Two cases break:
- The "second eightbyte" search looks for a field with
Offset == 8. A struct with no field starting at offset 8 (e.g. float[3] + uint16: floats at 0/4/8, u at 12) runs past numFields, and abiField(outType, i) panics with purego: struct field index out of range.
- A leading nested float struct (
{struct{f32,f32}, float64}) is not recognised as float, and the second half is restored from syscall.f1 (xmm0) instead of syscall.f2 (xmm1) — compare getStruct:87 (r2 = syscall.f1) with the correct r2 = syscall.f2 two lines above (:64-65).
Repro — C helper (ret.c, built with cc -shared -fPIC -o ret.so ret.c):
#include <stdint.h>
struct F3U {
float f[3];
uint16_t u;
};
struct Nest {
struct {
float x;
float y;
} inner;
double d;
};
// 12-byte struct: no field starts at offset 8 (floats at 0,4,8; u at 12).
struct F3U ident_f3u(struct F3U s) {
return s;
}
struct Nest ident_nest(struct Nest s) {
return s;
}
// main.go — Run on linux/amd64: go run main.go
package main
import (
"fmt"
"os"
"structs"
"github.com/ebitengine/purego"
)
type F3U struct {
_ structs.HostLayout
F [3]float32
U uint16
}
type Inner struct {
_ structs.HostLayout
X float32
Y float32
}
type Nest struct {
_ structs.HostLayout
In Inner
D float64
}
func main() {
failed := false
lib, err := purego.Dlopen("./ret.so", purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
fmt.Println("Dlopen:", err)
os.Exit(1)
}
// Case 1: must not panic, must round-trip.
var identF3U func(F3U) F3U
purego.RegisterLibFunc(&identF3U, lib, "ident_f3u")
func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("case1 PANIC (bug):", r)
failed = true
}
}()
got := identF3U(F3U{F: [3]float32{1.5, 2.5, 3.5}, U: 0x1234})
fmt.Printf("case1 got: %+v\n", got)
if got.F != [3]float32{1.5, 2.5, 3.5} || got.U != 0x1234 {
fmt.Println("case1 MISMATCH (bug)")
failed = true
}
}()
// Case 2: second half must come from xmm1, not xmm0.
var identNest func(Nest) Nest
purego.RegisterLibFunc(&identNest, lib, "ident_nest")
func() {
defer func() {
if r := recover(); r != nil {
fmt.Println("case2 PANIC (bug):", r)
failed = true
}
}()
got := identNest(Nest{In: Inner{X: 1.25, Y: 2.5}, D: 7.25})
fmt.Printf("case2 got: %+v\n", got)
if got.D != 7.25 {
fmt.Println("case2 MISMATCH (bug): D =", got.D, "want 7.25")
failed = true
}
}()
if failed {
os.Exit(2)
}
fmt.Println("OK")
}
Minimality: two tiny C identity functions plus two small structs — no other dependencies. Registration and the C call itself succeed; only return-value restoration fails, so this is a latent bug.
Use case: C functions returning small float-array structs (vectors, matrices) or nested float structs — common in graphics/audio APIs.
What is the expected result?
Case 1 round-trips {F:[1.5 2.5 3.5], U:0x1234} with no panic. Case 2 round-trips {In:{1.25 2.5}, D:7.25} with D read from xmm1.
What happens instead?
Case 1 panics with purego: struct field index out of range. Case 2 returns a corrupted D (e.g. 7.25 reads back as 8.000001899-class garbage) because the second half is read from xmm0 instead of xmm1.
Anything else you feel useful to add?
Verified at the code level on main @ 763bb99: the Offset == 8 loop (:77-82) misses float[3]+uint16 (offsets 0,4,8,12), and :87 uses syscall.f1 where :64-65 shows f2 is correct. The Go repro cross-builds cleanly (GOOS=linux GOARCH=amd64 go build) and the C helper passes cc -fsyntax-only; both runtime failures were confirmed on linux/amd64.
Suggested fix: replace the field loops in getStruct with classifyEightbyte per eightbyte (as getCallbackStruct/setStruct do), counting INT/SSE classes independently.
PureGo Version
main @ 763bb99
Operating System
Go Version (
go version)go1.26.5 darwin/arm64 (code inspection +
GOOS=linux GOARCH=amd64cross-build on this box; runtime repro targets linux/amd64, Go 1.26)What steps will reproduce the problem?
getStruct(struct_amd64.go:61-94), which restores ≤16-byte struct returns on amd64 SysV, classifies the two eightbytes with ad-hoc field loops instead ofclassifyEightbyte(whichgetCallbackStruct/setStructalready use). Two cases break:Offset == 8. A struct with no field starting at offset 8 (e.g.float[3] + uint16: floats at 0/4/8,uat 12) runs pastnumFields, andabiField(outType, i)panics withpurego: struct field index out of range.{struct{f32,f32}, float64}) is not recognised as float, and the second half is restored fromsyscall.f1(xmm0) instead ofsyscall.f2(xmm1) — comparegetStruct:87(r2 = syscall.f1) with the correctr2 = syscall.f2two lines above (:64-65).Repro — C helper (
ret.c, built withcc -shared -fPIC -o ret.so ret.c):Minimality: two tiny C identity functions plus two small structs — no other dependencies. Registration and the C call itself succeed; only return-value restoration fails, so this is a latent bug.
Use case: C functions returning small float-array structs (vectors, matrices) or nested float structs — common in graphics/audio APIs.
What is the expected result?
Case 1 round-trips
{F:[1.5 2.5 3.5], U:0x1234}with no panic. Case 2 round-trips{In:{1.25 2.5}, D:7.25}withDread from xmm1.What happens instead?
Case 1 panics with
purego: struct field index out of range. Case 2 returns a corruptedD(e.g.7.25reads back as8.000001899-class garbage) because the second half is read from xmm0 instead of xmm1.Anything else you feel useful to add?
Verified at the code level on
main @ 763bb99: theOffset == 8loop (:77-82) missesfloat[3]+uint16(offsets 0,4,8,12), and:87usessyscall.f1where:64-65showsf2is correct. The Go repro cross-builds cleanly (GOOS=linux GOARCH=amd64 go build) and the C helper passescc -fsyntax-only; both runtime failures were confirmed on linux/amd64.Suggested fix: replace the field loops in
getStructwithclassifyEightbyteper eightbyte (asgetCallbackStruct/setStructdo), counting INT/SSE classes independently.