diff --git a/README.md b/README.md index 0b091b5..dd13619 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,62 @@ A new CIF must be prepared for each unique combination of variadic argument type portion of the CIF can be reused by re-calling `PrepareVariadicCallInterface` with different variadic arg type slices. +### Example: Passing and Returning Structs + +goffi handles C struct pass-by-value across all ABI size classes. Describe the struct layout +with a `TypeDescriptor`, then pass struct values directly via `unsafe.Pointer(&s)`. + +```go +// C struct: typedef struct { int64_t x; int64_t y; } Point; +pointType := &types.TypeDescriptor{ + Kind: types.StructType, + Size: 16, // must match C sizeof(Point) + Alignment: 8, // must match C alignof(Point) + Members: []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, // x + types.SInt64TypeDescriptor, // y + }, +} + +var cif types.CallInterface +ffi.PrepareCallInterface(&cif, types.DefaultCall, + pointType, // return: Point + []*types.TypeDescriptor{types.SInt64TypeDescriptor, types.SInt64TypeDescriptor}, +) + +x, y := int64(3), int64(4) +var result Point +_, _ = ffi.CallFunction(&cif, makePointFn, + unsafe.Pointer(&result), // buffer for struct return value + []unsafe.Pointer{unsafe.Pointer(&x), unsafe.Pointer(&y)}, +) +// result.X == 3, result.Y == 4 + +// Pass struct as argument: +var distCif types.CallInterface +ffi.PrepareCallInterface(&distCif, types.DefaultCall, + types.SInt64TypeDescriptor, + []*types.TypeDescriptor{pointType, pointType}, // two Point args +) + +a := Point{X: 0, Y: 0} +b := Point{X: 3, Y: 4} +var dist int64 +_, _ = ffi.CallFunction(&distCif, distFn, + unsafe.Pointer(&dist), + []unsafe.Pointer{unsafe.Pointer(&a), unsafe.Pointer(&b)}, // pointer to struct data +) +// dist == 25 +``` + +Structs >16 bytes are returned via hidden pointer (sret) — goffi handles this transparently. + +> **Note:** On Windows AMD64, struct arguments containing float fields are not supported +> due to `syscall.SyscallN` limitations. Use integer-only structs for cross-platform code, +> or pass float fields as individual arguments. + +See [`examples/struct/`](examples/struct/) for a complete working example with compile-and-run. + --- ## Performance diff --git a/ROADMAP.md b/ROADMAP.md index b31ce54..33f9b85 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -3,7 +3,7 @@ > **Strategic Approach**: Build production-ready Zero-CGO FFI with benchmarked performance > **Philosophy**: Performance first, usability second, platform coverage third -**Last Updated**: 2026-07-21 | **Current Version**: v0.6.1 | **Strategy**: Benchmarks → Callbacks → ARM64 → Runtime → ABI → v1.0 LTS | **Milestone**: v0.6.1 (Android preview + fakecgo cleanup) → v0.7.0 RegisterFunc/Builder → v1.0.0 LTS +**Last Updated**: 2026-08-01 | **Current Version**: v0.6.3 | **Strategy**: Benchmarks → Callbacks → ARM64 → Runtime → ABI → v1.0 LTS | **Milestone**: v0.6.3 (HFA checkptr fix) → v0.7.0 RegisterFunc/Builder → v1.0.0 LTS --- @@ -167,6 +167,14 @@ v1.0.0 LTS → Long-term support release (2027 Q1) - LICENSE + NOTICE updated to GoGPU ecosystem pattern - @besmpl added as CODEOWNER for Android paths +**v0.6.2** = Windows float returns fix ✅ RELEASED (2026-07-22) +- Windows XMM0 float return capture (TASK-019 resolved) — PR #65 by @besmpl + +**v0.6.3** = ARM64 HFA checkptr fix ✅ RELEASED (2026-08-01) +- ARM64 `handleHFAReturn` checkptr crash fix (#67, reported by @jbunds) +- ARM64 9-16B struct return proactive fix (copy pattern) +- Struct pass/return examples and README section (#58) + **v0.7.0** = RegisterFunc + Builder API (2026 Q3-Q4) - RegisterFunc convenience API (ADR-008) - Library struct + OpenLibraryBytes (ADR-009) @@ -180,9 +188,9 @@ v1.0.0 LTS → Long-term support release (2027 Q1) --- -## 📊 Current Status (v0.6.0) +## 📊 Current Status (v0.6.3) -**Phase**: errno always-capture, stack-move fix, 8 platforms. Planning v0.7.0 (RegisterFunc) +**Phase**: HFA checkptr fix, struct examples. 9 platforms. Planning v0.7.0 (RegisterFunc) **What Works**: - ✅ Dynamic library loading (`LoadLibrary`, `GetSymbol`, `FreeLibrary`) diff --git a/examples/struct/go.mod b/examples/struct/go.mod new file mode 100644 index 0000000..09011dd --- /dev/null +++ b/examples/struct/go.mod @@ -0,0 +1,7 @@ +module struct-example + +go 1.25 + +require github.com/go-webgpu/goffi v0.0.0 + +replace github.com/go-webgpu/goffi => ../.. diff --git a/examples/struct/main.go b/examples/struct/main.go new file mode 100644 index 0000000..7a8e875 --- /dev/null +++ b/examples/struct/main.go @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 Andrey Kolkov and GoGPU Contributors + +// Package main demonstrates struct pass-by-value and struct return via goffi. +// +// It compiles structlib.c at runtime using gcc, loads the resulting shared +// library, and exercises four C functions that cover the three struct ABI +// size classes: ≤16 B (register pairs), >16 B (sret hidden pointer). +// +// Run: +// +// go run . (requires gcc in PATH) +// +// The example gracefully skips when gcc is unavailable. +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "unsafe" + + "github.com/go-webgpu/goffi/ffi" + "github.com/go-webgpu/goffi/types" +) + +// Point mirrors C: typedef struct { int64_t x; int64_t y; } Point; +type Point struct { + X int64 + Y int64 +} + +// Vec3 mirrors C: typedef struct { int64_t x; int64_t y; int64_t z; } Vec3; +type Vec3 struct { + X int64 + Y int64 + Z int64 +} + +func main() { + libPath, ok := buildLib() + if !ok { + fmt.Println("gcc not found — skipping struct example (install gcc to run)") + return + } + + handle, err := ffi.LoadLibrary(libPath) + if err != nil { + fmt.Println("LoadLibrary error:", err) + return + } + defer ffi.FreeLibrary(handle) + + // TypeDescriptor for Point {int64, int64} — 16 bytes, alignment 8. + // Size and Alignment must match C sizeof/alignof exactly. + pointType := &types.TypeDescriptor{ + Kind: types.StructType, + Size: 16, + Alignment: 8, + Members: []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, // x + types.SInt64TypeDescriptor, // y + }, + } + + // TypeDescriptor for Vec3 {int64, int64, int64} — 24 bytes (>16 B → sret). + vec3Type := &types.TypeDescriptor{ + Kind: types.StructType, + Size: 24, + Alignment: 8, + Members: []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, // x + types.SInt64TypeDescriptor, // y + types.SInt64TypeDescriptor, // z + }, + } + + demoMakePoint(handle, pointType) + demoDistanceSquared(handle, pointType) + demoMakeVec3(handle, vec3Type) + demoVec3Dot(handle, vec3Type) +} + +// demoMakePoint calls: Point make_point(int64_t x, int64_t y) +// Return: ≤16 B struct — goffi reads it from GP register pair (Unix) or sret (Windows). +func demoMakePoint(handle unsafe.Pointer, pointType *types.TypeDescriptor) { + sym, err := ffi.GetSymbol(handle, "make_point") + if err != nil { + fmt.Println("GetSymbol make_point error:", err) + return + } + + var cif types.CallInterface + if err = ffi.PrepareCallInterface(&cif, types.DefaultCall, + pointType, + []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, + types.SInt64TypeDescriptor, + }, + ); err != nil { + fmt.Println("PrepareCallInterface error:", err) + return + } + + x, y := int64(3), int64(4) + var result Point + if _, err = ffi.CallFunction(&cif, sym, + unsafe.Pointer(&result), // buffer for struct return value + []unsafe.Pointer{ + unsafe.Pointer(&x), + unsafe.Pointer(&y), + }, + ); err != nil { + fmt.Println("CallFunction make_point error:", err) + return + } + + fmt.Printf("make_point(%d, %d) = {X:%d Y:%d}\n", x, y, result.X, result.Y) +} + +// demoDistanceSquared calls: int64_t distance_squared(Point a, Point b) +// Arguments: two Point structs passed by value. +func demoDistanceSquared(handle unsafe.Pointer, pointType *types.TypeDescriptor) { + sym, err := ffi.GetSymbol(handle, "distance_squared") + if err != nil { + fmt.Println("GetSymbol distance_squared error:", err) + return + } + + var cif types.CallInterface + if err = ffi.PrepareCallInterface(&cif, types.DefaultCall, + types.SInt64TypeDescriptor, + []*types.TypeDescriptor{pointType, pointType}, // two Point args + ); err != nil { + fmt.Println("PrepareCallInterface error:", err) + return + } + + a := Point{X: 0, Y: 0} + b := Point{X: 3, Y: 4} + var dist int64 + if _, err = ffi.CallFunction(&cif, sym, + unsafe.Pointer(&dist), + []unsafe.Pointer{ + unsafe.Pointer(&a), // pointer to struct data + unsafe.Pointer(&b), + }, + ); err != nil { + fmt.Println("CallFunction distance_squared error:", err) + return + } + + // distance_squared({0,0}, {3,4}) = 3²+4² = 25 + fmt.Printf("distance_squared({%d,%d}, {%d,%d}) = %d\n", + a.X, a.Y, b.X, b.Y, dist) +} + +// demoMakeVec3 calls: Vec3 make_vec3(int64_t x, int64_t y, int64_t z) +// Return: >16 B struct — always via sret hidden pointer; goffi handles this transparently. +func demoMakeVec3(handle unsafe.Pointer, vec3Type *types.TypeDescriptor) { + sym, err := ffi.GetSymbol(handle, "make_vec3") + if err != nil { + fmt.Println("GetSymbol make_vec3 error:", err) + return + } + + var cif types.CallInterface + if err = ffi.PrepareCallInterface(&cif, types.DefaultCall, + vec3Type, + []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, + types.SInt64TypeDescriptor, + types.SInt64TypeDescriptor, + }, + ); err != nil { + fmt.Println("PrepareCallInterface error:", err) + return + } + + x, y, z := int64(1), int64(2), int64(3) + var result Vec3 + if _, err = ffi.CallFunction(&cif, sym, + unsafe.Pointer(&result), // goffi passes &result as the hidden sret pointer + []unsafe.Pointer{ + unsafe.Pointer(&x), + unsafe.Pointer(&y), + unsafe.Pointer(&z), + }, + ); err != nil { + fmt.Println("CallFunction make_vec3 error:", err) + return + } + + fmt.Printf("make_vec3(%d, %d, %d) = {X:%d Y:%d Z:%d}\n", + x, y, z, result.X, result.Y, result.Z) +} + +// demoVec3Dot calls: int64_t vec3_dot(Vec3 a, Vec3 b) +// Arguments: two Vec3 structs (>16 B each) passed by value on the stack. +func demoVec3Dot(handle unsafe.Pointer, vec3Type *types.TypeDescriptor) { + sym, err := ffi.GetSymbol(handle, "vec3_dot") + if err != nil { + fmt.Println("GetSymbol vec3_dot error:", err) + return + } + + var cif types.CallInterface + if err = ffi.PrepareCallInterface(&cif, types.DefaultCall, + types.SInt64TypeDescriptor, + []*types.TypeDescriptor{vec3Type, vec3Type}, + ); err != nil { + fmt.Println("PrepareCallInterface error:", err) + return + } + + a := Vec3{X: 1, Y: 2, Z: 3} + b := Vec3{X: 4, Y: 5, Z: 6} + var dot int64 + if _, err = ffi.CallFunction(&cif, sym, + unsafe.Pointer(&dot), + []unsafe.Pointer{ + unsafe.Pointer(&a), + unsafe.Pointer(&b), + }, + ); err != nil { + fmt.Println("CallFunction vec3_dot error:", err) + return + } + + // dot = 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32 + fmt.Printf("vec3_dot({%d,%d,%d}, {%d,%d,%d}) = %d\n", + a.X, a.Y, a.Z, b.X, b.Y, b.Z, dot) +} + +// buildLib compiles structlib.c into a shared library and returns its path. +// Returns ("", false) if gcc is not available. +func buildLib() (string, bool) { + cc := os.Getenv("CC") + if cc == "" { + cc = "gcc" + } + if _, err := exec.LookPath(cc); err != nil { + return "", false + } + + dir, err := os.MkdirTemp("", "goffi-struct-example-*") + if err != nil { + fmt.Println("TempDir error:", err) + return "", false + } + + src := filepath.Join(filepath.Dir(os.Args[0]), "structlib.c") + if _, statErr := os.Stat(src); statErr != nil { + // When invoked via "go run .", the source directory is the working directory. + src = "structlib.c" + } + + var soPath string + var args []string + switch runtime.GOOS { + case "darwin": + soPath = filepath.Join(dir, "libstructlib.dylib") + args = []string{"-shared", "-fPIC", "-O2", "-o", soPath, src} + case "windows": + soPath = filepath.Join(dir, "structlib.dll") + args = []string{"-shared", "-O2", "-o", soPath, src} + default: + soPath = filepath.Join(dir, "libstructlib.so") + args = []string{"-shared", "-fPIC", "-O2", "-o", soPath, src} + } + + cmd := exec.Command(cc, args...) + cmd.Stderr = os.Stderr + if err = cmd.Run(); err != nil { + fmt.Println("gcc compile error:", err) + return "", false + } + + return soPath, true +} diff --git a/examples/struct/structlib.c b/examples/struct/structlib.c new file mode 100644 index 0000000..3084945 --- /dev/null +++ b/examples/struct/structlib.c @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 Andrey Kolkov and GoGPU Contributors + +// structlib.c — minimal C library for the struct pass/return goffi example. +// Uses integer-only fields so the same binary works on Windows AMD64 +// (syscall.SyscallN cannot propagate XMM register values for float struct fields). +#include + +typedef struct { int64_t x; int64_t y; } Point; +typedef struct { int64_t x; int64_t y; int64_t z; } Vec3; + +// Return struct by value (16 B — fits two integer GP registers on Unix, +// goes through sret on Windows AMD64). +Point make_point(int64_t x, int64_t y) { + Point p; + p.x = x; + p.y = y; + return p; +} + +// Accept struct by value, return scalar. +int64_t distance_squared(Point a, Point b) { + int64_t dx = a.x - b.x; + int64_t dy = a.y - b.y; + return dx * dx + dy * dy; +} + +// Return large struct (24 B — always sret on all platforms). +Vec3 make_vec3(int64_t x, int64_t y, int64_t z) { + Vec3 v; + v.x = x; + v.y = y; + v.z = z; + return v; +} + +// Accept struct, return scalar. +int64_t vec3_dot(Vec3 a, Vec3 b) { + return a.x * b.x + a.y * b.y + a.z * b.z; +}