Skip to content

Struct support for Windows #237

Closed

Description

@JupiterRider

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • Android
  • iOS

What feature would you like to be added?

Support structs as return values and parameters of functions on Windows.

Why is this needed?

Technically purego already supports structs, but you have to make some weird stunts:

If the struct's size is less than or equal 8 bytes, you just can convert them into a uintptr. Same goes for return values. Example:

C:

RLAPI void ClearBackground(Color color); 

Golang:

var clearBackground func(col uintptr)

purego.RegisterLibFunc(&clearBackground, raylibDll, "ClearBackground")

func ClearBackground(col color.RGBA) {
	clearBackground(*(*uintptr)(unsafe.Pointer(&col)))
}

If the struct's size is lager then 8 bytes, you have to use a pointer as parameter. For return values you have to pass it as first argument. Here is another example from raylib-go:

C:

RLAPI Texture2D LoadTextureCubemap(Image image, int layout);   

Golang:

var loadTextureCubemap func(texture *Texture2D, image *Image, layout int32)

purego.RegisterLibFunc(&loadTextureCubemap, raylibDll, "LoadTextureCubemap")

func LoadTextureCubemap(image Image, layout int32) Texture2D {
	var texture Texture2D
	loadTextureCubemap(&texture, &image, layout)
	return texture
}

Activity

  1. TotallyGamerJet commented on Apr 21, 2024

    @TotallyGamerJet
    Collaborator

    My comment applies here too

  2. JupiterRider commented on Apr 26, 2024

    @JupiterRider
    ContributorAuthor

    Here is a first example of how it could look like:

    func RegisterNew(fptr any, handle uintptr, name string) {
    	if fptr == nil {
    		panic("argument is nil")
    	}
    	typ := reflect.TypeOf(fptr)
    	kind := typ.Kind()
    	if kind != reflect.Ptr {
    		panic("wrong kind " + kind.String())
    	}
    	typ = typ.Elem()
    	kind = typ.Kind()
    	if kind != reflect.Func {
    		panic("wrong kind " + kind.String())
    	}
    	numOut := typ.NumOut()
    	if numOut > 1 {
    		panic("at most one return value is supported")
    	}
    	numIn := typ.NumIn()
    	var outToIn bool
    	var outToUintptr bool
    	ptrPositions := make([]bool, numIn)
    	uintptrPositions := make([]bool, numIn)
    	outs := []reflect.Type{}
    	ins := []reflect.Type{}
    
    	uintptrTyp := reflect.TypeFor[uintptr]()
    
    	for i := 0; i < numOut; i++ {
    		out := typ.Out(i)
    		if out.Kind() == reflect.Struct {
    			if out.Size() > 8 {
    				out = reflect.PointerTo(out)
    				outToIn = true
    				ins = append(ins, out)
    				continue
    			}
    			outs = append(outs, uintptrTyp)
    			outToUintptr = true
    			continue
    		}
    		outs = append(outs, out)
    	}
    
    	for i := 0; i < numIn; i++ {
    		in := typ.In(i)
    		if in.Kind() == reflect.Struct {
    			if in.Size() > 8 {
    				in = reflect.PointerTo(in)
    				ptrPositions[i] = true
    			} else {
    				in = uintptrTyp
    				uintptrPositions[i] = true
    			}
    		}
    		ins = append(ins, in)
    	}
    
    	fnTyp := reflect.FuncOf(ins, outs, false)
    	fn := reflect.New(fnTyp)
    
    	purego.RegisterLibFunc(fn.Interface(), handle, name)
    	impl := reflect.MakeFunc(typ, func(args []reflect.Value) (results []reflect.Value) {
    		newArgs := make([]reflect.Value, 0, len(args))
    		var newResult reflect.Value
    		if outToIn {
    			newResult = reflect.New(ins[0].Elem())
    			newArgs = append(newArgs, newResult)
    		}
    		for i := 0; i < len(args); i++ {
    			if ptrPositions[i] {
    				value := reflect.New(args[i].Type())
    				value.Elem().Set(args[i])
    				newArgs = append(newArgs, value)
    				continue
    			} else if uintptrPositions[i] {
    				value := reflect.New(args[i].Type())
    				value.Elem().Set(args[i])
    				newArgs = append(newArgs, reflect.ValueOf(*(*uintptr)(value.UnsafePointer())))
    				continue
    			}
    			newArgs = append(newArgs, args[i])
    		}
    
    		res := fn.Elem().Call(newArgs)
    		if outToIn {
    			return []reflect.Value{newResult.Elem()}
    		} else if outToUintptr {
    			value := reflect.New(res[0].Type())
    			value.Elem().Set(res[0])
    			newValue := reflect.NewAt(typ.Out(0), value.UnsafePointer())
    			return []reflect.Value{newValue.Elem()}
    		}
    		return res
    	})
    	reflect.ValueOf(fptr).Elem().Set(impl)
    }
  3. judah-caruso commented on Aug 18, 2025

    @judah-caruso

    I'd like to take a crack at this. I have a windows machine (amd64) and a macbook (arm64 with Rosetta), so I can properly test and maintain this.

    I'm assuming this involves moving struct_amd64.go/struct_arm64.go to struct_darwin_amd64.go/struct_darwin_arm64.go (with appropriate build tags), then creating a new struct_windows_amd64.go. If that's the case, I'd just need to write addStruct + others that comply with Microsoft's x64 calling conventions?

    I'm most definitely missing something, so let me know!

  4. TotallyGamerJet commented on Aug 18, 2025

    @TotallyGamerJet
    Collaborator

    That should pretty much be it. Idk if the amd64 windows ABI needs any special registers like arm64 did so that might complicate. Also I think that arm64 windows should have the same ABI as macOS so those can be shared

  5. judah-caruso commented on Aug 18, 2025

    @judah-caruso

    Sounds good! I don’t think I’ll be able to test arm64 windows (unless there’s an easy emulator I can utilize). Would you rather me leave it out or include it in the build tags (+ add it to the CICD pipeline)?

  6. unxed commented on May 28, 2026

    @unxed

    Check this. Should solve your problem due to specifics of underlyting ffi engine.
    https://github.com/unxed/pureffi

  7. hajimehoshi commented on Jun 16, 2026

    @hajimehoshi
    Member

    Apparently, it's not difficult to fix purego to pass structs, but it's quite difficult to treat structs in callbacks. This requries to implement the callback system by ourselves instead of syscall's NewCallback.

  8. unxed commented on Jun 16, 2026

    @unxed

    Apparently, it's not difficult to fix purego to pass structs, but it's quite difficult to treat structs in callbacks. This requries to implement the callback system by ourselves instead of syscall's NewCallback.

    pureffi supports this, it even has a test case for this scenario, TestFullCircleFFI

  9. added this to the v0.11.0 milestone on Jun 16, 2026
  10. hajimehoshi commented on Jun 16, 2026

    @hajimehoshi
    Member

    pureffi supports this, it even has a test case for this scenario, TestFullCircleFFI

    It's because goffi is used as the backend, right?

  11. added 7 commits that reference this issue on Jun 16, 2026
    a81e3a8
    01d358a
    b365dec
    7c7a011
    9027911
    a717c8d
    fd6436b
  12. added a commit that references this issue on Jun 20, 2026
    7e97482
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions