Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Cgo dependency on Windows #102

Closed
wants to merge 12 commits into from
32 changes: 31 additions & 1 deletion functions.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package main

import "fmt"
import (
"fmt"
"strings"
)

// A Function definition.
type Function struct {
Expand All @@ -10,6 +13,33 @@ type Function struct {
Return Type
}

// IsImplementedForSyscall reports whether the function is implemented for syscall or not.
func (f Function) IsImplementedForSyscall() bool {
// TODO: Use syscall.Syscall18 when Go 1.12 is the minimum supported version.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like Go 1.12 is the lowest supported version now.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least in glow. The gl package's mod file still says 1.9. Also, in 1.18 all the syscall functions except SyscallN are deprecated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, go-gl/gl#144

if len(f.Parameters) > 15 {
return false
}
return true
}

// Syscall returns a syscall expression for Windows.
func (f Function) Syscall() string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this would be more idiomatic/clear if it returned an error when !IsImplementedForSyscall (rather than having a separate func that needs to be called)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is called at templates to generate Syscall calling, then returning error here would not make sense.

var ps []string
for _, p := range f.Parameters {
ps = append(ps, p.Type.ConvertGoToUintptr(p.GoName()))
}
for len(ps) == 0 || len(ps)%3 != 0 {
ps = append(ps, "0")
}

post := ""
if len(ps) > 3 {
post = fmt.Sprintf("%d", len(ps))
}

return fmt.Sprintf("syscall.Syscall%s(gp%s, %d, %s)", post, f.GoName, len(f.Parameters), strings.Join(ps, ", "))
}

// A Parameter to a Function.
type Parameter struct {
Name string
Expand Down
22 changes: 20 additions & 2 deletions package.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,32 @@ func (pkg *Package) GeneratePackage(dir string) error {
if err := pkg.generateFile("package", dir); err != nil {
return err
}
if err := pkg.generateFile("package_notwindows", dir); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having all these files is super unfortunate, but I'm not sure whether there is anything to be done for it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps something like:

for _, v := range []string{"package", "package_notwindows", "package_windows",
  "conversions", "conversions_notwindows", "conversions_windows",
  "procaddr_notwindows", "procaddr_windows"} {
  if err := pkg.generateFile(v, dir); err != nil {
	  return err
  }
}

return err
}
if err := pkg.generateFile("package_windows", dir); err != nil {
return err
}
if err := pkg.generateFile("conversions", dir); err != nil {
return err
}
if err := pkg.generateFile("procaddr", dir); err != nil {
if err := pkg.generateFile("conversions_notwindows", dir); err != nil {
return err
}
if err := pkg.generateFile("conversions_windows", dir); err != nil {
return err
}
if err := pkg.generateFile("procaddr_notwindows", dir); err != nil {
return err
}
if err := pkg.generateFile("procaddr_windows", dir); err != nil {
return err
}
if pkg.HasDebugCallbackFeature() {
if err := pkg.generateFile("debug", dir); err != nil {
if err := pkg.generateFile("debug_notwindows", dir); err != nil {
return err
}
if err := pkg.generateFile("debug_windows", dir); err != nil {
return err
}
}
Expand Down
46 changes: 1 addition & 45 deletions tmpl/conversions.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//glow:keepspace

// Code generated by glow (https://github.com/go-gl/glow). DO NOT EDIT.

package {{.Name}}
Expand All @@ -10,9 +11,6 @@ import (
"unsafe"
)

// #include <stdlib.h>
import "C"

// Ptr takes a slice or pointer (to a singular scalar value or the first
// element of an array or slice) and returns its GL-compatible address.
//
Expand Down Expand Up @@ -66,45 +64,3 @@ func Str(str string) *uint8 {
header := (*reflect.StringHeader)(unsafe.Pointer(&str))
return (*uint8)(unsafe.Pointer(header.Data))
}

// GoStr takes a null-terminated string returned by OpenGL and constructs a
// corresponding Go string.
func GoStr(cstr *uint8) string {
return C.GoString((*C.char)(unsafe.Pointer(cstr)))
}

// Strs takes a list of Go strings (with or without null-termination) and
// returns their C counterpart.
//
// The returned free function must be called once you are done using the strings
// in order to free the memory.
//
// If no strings are provided as a parameter this function will panic.
func Strs(strs ...string) (cstrs **uint8, free func()) {
if len(strs) == 0 {
panic("Strs: expected at least 1 string")
}

// Allocate a contiguous array large enough to hold all the strings' contents.
n := 0
for i := range strs {
n += len(strs[i])
}
data := C.malloc(C.size_t(n))

// Copy all the strings into data.
dataSlice := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
Data: uintptr(data),
Len: n,
Cap: n,
}))
css := make([]*uint8, len(strs)) // Populated with pointers to each string.
offset := 0
for i := range strs {
copy(dataSlice[offset:offset+len(strs[i])], strs[i][:]) // Copy strs[i] into proper data location.
css[i] = (*uint8)(unsafe.Pointer(&dataSlice[offset])) // Set a pointer to it.
offset += len(strs[i])
}

return (**uint8)(&css[0]), func() { C.free(data) }
}
56 changes: 56 additions & 0 deletions tmpl/conversions_notwindows.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//glow:keepspace
// +build !windows

// Code generated by glow (https://github.com/go-gl/glow). DO NOT EDIT.

package {{.Name}}

import (
"reflect"
"unsafe"
)

// #include <stdlib.h>
import "C"

// GoStr takes a null-terminated string returned by OpenGL and constructs a
// corresponding Go string.
func GoStr(cstr *uint8) string {
return C.GoString((*C.char)(unsafe.Pointer(cstr)))
}

// Strs takes a list of Go strings (with or without null-termination) and
// returns their C counterpart.
//
// The returned free function must be called once you are done using the strings
// in order to free the memory.
//
// If no strings are provided as a parameter this function will panic.
func Strs(strs ...string) (cstrs **uint8, free func()) {
if len(strs) == 0 {
panic("Strs: expected at least 1 string")
}

// Allocate a contiguous array large enough to hold all the strings' contents.
n := 0
for i := range strs {
n += len(strs[i])
}
data := C.malloc(C.size_t(n))

// Copy all the strings into data.
dataSlice := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
Data: uintptr(data),
Len: n,
Cap: n,
}))
css := make([]*uint8, len(strs)) // Populated with pointers to each string.
offset := 0
for i := range strs {
copy(dataSlice[offset:offset+len(strs[i])], strs[i][:]) // Copy strs[i] into proper data location.
css[i] = (*uint8)(unsafe.Pointer(&dataSlice[offset])) // Set a pointer to it.
offset += len(strs[i])
}

return (**uint8)(&css[0]), func() { C.free(data) }
}
54 changes: 54 additions & 0 deletions tmpl/conversions_windows.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//glow:keepspace
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me why this conversion code, or the debug code, needs to be forked across Windows/not-Windows.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#102 (comment) is the reason: using this conversions_windows.tmpl version at Cgo caused errors.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way we can reduce the duplication by factoring out the parts that are the same? (Or am I misreading and all the implementations are subtly different?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (created the common conversion.tmpl)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting back to this now (finally, sorry for the delay)...

  1. Are the changes to conversions.tmpl platform agnostic and should be used everywhere?
  2. I still don't quite understand why debug needs to be forked.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick reply: go-gl/gl#80 might be related, but I am not sure. I'll take a look later.

Copy link
Member Author

@hajimehoshi hajimehoshi Apr 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like if we can make conversions.tmpl pure Go first, we should do this before merging this PR. Wdyt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm all for updating conversions to work across the board.

// +build windows

// Code generated by glow (https://github.com/go-gl/glow). DO NOT EDIT.

package {{.Name}}

import (
"runtime"
"strings"
"unsafe"
)

// GoStr takes a null-terminated string returned by OpenGL and constructs a
// corresponding Go string.
func GoStr(cstr *uint8) string {
str := ""
for {
if *cstr == 0 {
break
}
str += string(*cstr)
cstr = (*uint8)(unsafe.Pointer(uintptr(unsafe.Pointer(cstr)) + 1))
}
return str
}

// Strs takes a list of Go strings (with or without null-termination) and
// returns their C counterpart.
//
// The returned free function must be called once you are done using the strings
// in order to free the memory.
//
// If no strings are provided as a parameter this function will panic.
func Strs(strs ...string) (cstrs **uint8, free func()) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@errcw My concern was that **uint8 would be a Go pointer to a Go pointer, which is not allowed to pass to C world. (This is fine to pass this to Syscall on Windows as long as we care the lifetime of the object). That's why I separated the implementation for Windows and non-Windows. Do you have any suggestions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm so sorry for the slow responses here. The amount of free time I have is vanishingly small these days. The cstrs **unit8 return value is actually a set of pointers into malloc'ed C memory, which I believe is safe?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for responding! Our plan is to make this function in pure Go (no cgo in any environment), right? If so, we would not be able to use C allocator.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, right, of course. So I'm clear, on Windows, we're using Syscall which is inherently safe to accept values that are Go pointers to Go pointers? Whereas on other platforms those same values are unsafe because they cross the Cgo boundary? If so, sounds good.

Copy link
Member Author

@hajimehoshi hajimehoshi Apr 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on Windows, we're using Syscall which is inherently safe to accept values that are Go pointers to Go pointers?

Yes, there is no warning, and we can assure the lifetime of the objects (by runtime.KeepAlive).

Whereas on other platforms those same values are unsafe because they cross the Cgo boundary?

This is unsafe, and unfortunately Go runtime suspends the execution with warning messages. That's the problem.

Then, I think it would be inevitable to split the implementation for Windows and non-Windows. My suggestion is to split the allocation part to minimize the split code. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If so, sounds good.

So, you are fine with splitting the code? :-)

if len(strs) == 0 {
panic("Strs: expected at least 1 string")
}

var pinned []string
var ptrs []*uint8
for _, str := range strs {
if !strings.HasSuffix(str, "\x00") {
str += "\x00"
}
pinned = append(pinned, str)
ptrs = append(ptrs, Str(str))
}

return &ptrs[0], func() {
runtime.KeepAlive(pinned)
pinned = nil
}
}
2 changes: 2 additions & 0 deletions tmpl/debug.tmpl → tmpl/debug_notwindows.tmpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
//glow:keepspace
// +build !windows

// Code generated by glow (https://github.com/go-gl/glow). DO NOT EDIT.

package {{.Name}}
Expand Down
16 changes: 16 additions & 0 deletions tmpl/debug_windows.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//glow:keepspace
// Code generated by glow (https://github.com/go-gl/glow). DO NOT EDIT.

package {{.Name}}
//glow:rmspace

import "unsafe"

type DebugProc func(
source uint32,
gltype uint32,
id uint32,
severity uint32,
length int32,
message string,
userParam unsafe.Pointer)
106 changes: 3 additions & 103 deletions tmpl/package.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -18,97 +18,12 @@
package {{.Name}}
//glow:rmspace

{{define "paramsCDecl"}}{{range $i, $p := .}}{{if ne $i 0}}, {{end}}{{$p.Type.CType}} {{$p.CName}}{{end}}{{end}}
{{define "paramsCCall"}}{{range $i, $p := .}}{{if ne $i 0}}, {{end}}{{if $p.Type.IsDebugProc}}glowCDebugCallback{{else}}{{$p.CName}}{{end}}{{end}}{{end}}

{{define "paramsGoDecl"}}{{range $i, $p := .}}{{if ne $i 0}}, {{end}}{{$p.GoName}} {{$p.Type.GoType}}{{end}}{{end}}
{{define "paramsGoCall"}}{{range $i, $p := .}}{{if ne $i 0}}, {{end}}{{$p.Type.ConvertGoToC $p.GoName}}{{end}}{{end}}

// #cgo darwin LDFLAGS: -framework OpenGL
// #cgo windows LDFLAGS: -lopengl32
//
// #cgo !egl,linux !egl,freebsd pkg-config: gl
// #cgo egl,linux egl,freebsd pkg-config: egl
//
// #if defined(_WIN32) && !defined(APIENTRY) && !defined(__CYGWIN__) && !defined(__SCITECH_SNAP__)
// #ifndef WIN32_LEAN_AND_MEAN
// #define WIN32_LEAN_AND_MEAN 1
// #endif
// #include <windows.h>
// #endif
//
// #ifndef APIENTRY
// #define APIENTRY
// #endif
// #ifndef APIENTRYP
// #define APIENTRYP APIENTRY *
// #endif
// #ifndef GLAPI
// #define GLAPI extern
// #endif
//
// {{range .Typedefs}}
// {{replace .CTypedef "\n" "\n// " -1}}
// {{end}}
//
// {{if .HasDebugCallbackFeature}}
// extern void glowDebugCallback_{{.UniqueName}}(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam);
// static void APIENTRY glowCDebugCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) {
// glowDebugCallback_{{.UniqueName}}(source, type, id, severity, length, message, userParam);
// }
// {{end}}
//
// {{range .Functions}}
// typedef {{.Return.CType}} (APIENTRYP GP{{toUpper .GoName}})({{template "paramsCDecl" .Parameters}});
// {{end}}
//
// {{range .Functions}}
// static {{.Return.CType}} glow{{.GoName}}(GP{{toUpper .GoName}} fnptr{{if ge (len .Parameters) 1}}, {{end}}{{template "paramsCDecl" .Parameters}}) {
// {{if not .Return.IsVoid}}return {{end}}(*fnptr)({{template "paramsCCall" .Parameters}});
// }
// {{end}}
//
import "C"
import (
{{if .HasRequiredFunctions}}
"errors"
{{end}}
"unsafe"
)

const (
{{range .Enums}}
{{.GoName}} = {{.Value}}
{{end}}
{{range .Enums}}
{{.GoName}} = {{.Value}}
{{end}}
)

var (
{{range .Functions}}
gp{{.GoName}} C.GP{{toUpper .GoName}}
{{end}}
)

// Helper functions
func boolToInt(b bool) int {
if b { return 1 }
return 0
}

{{define "bridgeCall"}}C.glow{{.GoName}}(gp{{.GoName}}{{if ge (len .Parameters) 1}}, {{end}}{{template "paramsGoCall" .Parameters}}){{end}}
{{range .Functions}}
{{.Comment}}
func {{.GoName}}({{template "paramsGoDecl" .Parameters}}){{if not .Return.IsVoid}} {{.Return.GoType}}{{end}} {
{{range .Parameters}}
{{if .Type.IsDebugProc}}userDebugCallback = {{.GoName}}{{end}}
{{end}}
{{if .Return.IsVoid}}{{template "bridgeCall" .}}
{{else}}
ret := {{template "bridgeCall" .}}
return {{.Return.ConvertCToGo "ret"}}
{{end}}
}
{{end}}

//glow:keepspace
// Init initializes the OpenGL bindings by loading the function pointers (for
// each OpenGL function) from the active OpenGL context.
Expand All @@ -133,18 +48,3 @@ func {{.GoName}}({{template "paramsGoDecl" .Parameters}}){{if not .Return.IsVoid
func Init() error {
return InitWithProcAddrFunc(getProcAddress)
}

// InitWithProcAddrFunc intializes the package using the specified OpenGL
// function pointer loading function. For more cases Init should be used
// instead.
func InitWithProcAddrFunc(getProcAddr func(name string) unsafe.Pointer) error {
{{range .Functions}}
gp{{.GoName}} = (C.GP{{toUpper .GoName}})(getProcAddr("{{.Name}}"))
{{if .Required}}
if gp{{.GoName}} == nil {
return errors.New("{{.Name}}")
}
{{end}}
{{end}}
return nil
}
Loading