Pointer constraint to generics

1,977 views
Skip to first unread message

Jan

unread,
Apr 15, 2023, 6:28:39 AM4/15/23
to golang-nuts
hi,

This is a variation for a previous topic, but since there isn't a clear solution, I thought I would ask if anyone can think of a work around.

I've been interacting a lot with C++ libraries from Go, and one of the commonly returned types is an abls::StatusOr, for which I created a simple C wrapper that casts the error and value to a `char *` and `void *` respectively (dropping the type information in between C++ and Go).

In Go I want to return the type information, so I defined a small generic function:

// PointerOrError converts a StatusOr structure to either a pointer to T with the data
// or the Status converted to an error message and then freed.
func PointerOrError[T any](s C.StatusOr) (*T, error) {
ptr, err := UnsafePointerOrError(s) // returns unsafe.Pointer, error
if err != nil {
return nil, err
}
return (*T)(ptr), nil
}

Now this doesn't work for my forward declared C++ types (most of them are just aliases to C++ objects) -- Go complaints with: `cannot use incomplete (or unallocatable) type as a type argument`, because `T` is incomplete indeed.

But ... I will never instantiate `T`, I only care about `*T`, which is not incomplete.

But there isn't a way to say a generics attribute is a pointer. So if I use the following:

func PointerOrError2[T any](s C.StatusOr) (t T, err error) {
var ptr unsafe.Pointer
ptr, err = UnsafePointerOrError(s) // <-- unsafe.Pointer, error
if err != nil {
return
}
t = (T)(ptr)
return
}

And instantiate it with a `PointerOrError2[*MyType](statusOr)` for instance, I get, as expected:

cannot convert ptr (variable of type unsafe.Pointer) to type T

Any suggestions to make this work ? 

I could probably craft something using the `reflect` package, but I was hoping for a smart (and likely faster?) generics solution.

cheers
Jan






jake...@gmail.com

unread,
Apr 15, 2023, 9:02:14 AM4/15/23
to golang-nuts
What About:

func PointerOrError[T *Q, Q any](s C.StatusOr ) (t T, err error)

Seems to compile: https://go.dev/play/p/n4I-XkONj-O?v=gotip

Jan

unread,
Apr 15, 2023, 2:41:28 PM4/15/23
to golang-nuts
Thanks! I hadn't realized that one could constraint T to be a pointer type by using a second type paramater Q, this in nice.

But alas, it doesn't work. When I copy&pasted your code to mine, and used an undefined C type ... it complained of the same thing:

```
cannot use incomplete (or unallocatable) type as a type argument: gomlx/xla._Ctype_struct_StableHLOHolder
```

I'm using it in the following snipped of code:

```
func (comp *Computation) ToStableHLO() (*StableHLO, error) {
if comp.IsNil() || comp.firstError != nil {
return nil, errors.Errorf("Computation graph is nil!?")
}
statusOr := C.ConvertComputationToStableHLO(comp.cCompPtr)
cPtr, err := PointerOrError[*C.StableHLOHolder](statusOr)
if err != nil {
return nil, errors.Wrapf(err, "failed conversion in Computation.ToStableHLO")
}
return NewStableHLO(cPtr), nil
}
```

I suspect it doesn't allow matching Q to an incomplete type (`C.StableHLOHolder` in this example), same way as my original version :(

I think your example in playground doesn't capture that -- the playground doesn't seem to allow CGO code (i tried this, but it didn't even try to compile).

I mean it's not the end of the world, I can always cast it in the next line ... it's just one of those little things that would be "ergonomically" very nice if it worked :)



Jan

unread,
Apr 15, 2023, 2:47:42 PM4/15/23
to golang-nuts
Re-factoring your example to use CGO, in a small main.go file:

```
$ go run .
./main.go:28:10: cannot use incomplete (or unallocatable) type as a type argument: main._Ctype_struct_MyThing
$ cat main.go
package main

// struct MyThing;
// typedef struct MyThing MyThing;
import "C"
import (
        "fmt"
        "unsafe"
)
import "flag"

func PointerOrError[T *Q, Q any](s int) (t T, err error) {

        var ptr unsafe.Pointer
        ptr, err = UnsafePointerOrError(s) // <-- unsafe.Pointer, error
        if err != nil {
                return
        }
        t = (T)(ptr)
        return
}

func UnsafePointerOrError(v int) (unsafe.Pointer, error) {
        return unsafe.Pointer(&v), nil
}

func main() {
        flag.Parse()
        t, _ := PointerOrError[*C.MyThing](1)
        fmt.Println(t)
}
```

Axel Wagner

unread,
Apr 15, 2023, 4:25:04 PM4/15/23
to Jan, golang-nuts
You should be able to instantiate the function using a Pointer. That is, you can write

func PointerOrError[T any](s C.StatusOr) (t T, err error) {

        var ptr unsafe.Pointer
        ptr, err = UnsafePointerOrError(s) // <-- unsafe.Pointer, error
        if err != nil {
                return
        }
        return *(*T)(unsafe.Pointer(&ptr))
}

func main() {
    var s C.StatusOr
    p := PointerOrError[*C.mystruct](s)
    _ = p
}

It's unfortunate, of course, that this would allow you to instantiate the function using a non-pointer as well, but it seems that's impossible to prevent statically? You can catch it dynamically by doing something akin to

if k := reflect.TypeOf(*new(T)).Kind(); k != reflect.Pointer && k != reflect.UnsafePointer {
    panic("PointerOrError must be instantiated with pointer type")
}

Obviously, none of this is ideal, but maybe it's the best you can do - apart from generating code.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/639d57d5-37da-424b-a137-41a7bca7e821n%40googlegroups.com.

Axel Wagner

unread,
Apr 15, 2023, 4:54:47 PM4/15/23
to Jan, golang-nuts
I guess thinking about some more, I don't really understand the point of what you're doing. It seems you *can* actually get the static guarantee you want - you just have to spell the call `(*C.mystruct)(UnsafePointerOrError(s))` instead of `PointerOrError[C.mystruct](s)`. If you *could* restrict the type parameter to be "any pointer", the two would actually be entirely equivalent, giving you exactly the same guarantees.

I think in reality you *don't* want to allow "any pointer type". I think in reality you want the type to be dependent on the C++ function you are calling - which returns the Status. I don't think you can use templated C++ types, otherwise you would probably want to do something like
func Call[T any](f func(…) C.StatusOr<T>) (T, error)

In the absence of that, you might try listing the types which are valid:
type CPointer interface{
    *C.MyStruct | *C.MyOtherStruct | *C.YourStruct
}
func PointerOrError[T CPointer](s C.StatusOr) (T, error) { … }


Jan

unread,
Apr 16, 2023, 1:59:10 AM4/16/23
to golang-nuts
Thanks for the suggestions Alex. Interesting that enumerating the pointers in a constraint would work ... but it makes sense.

I think I disagree with the statement about not wanting to allow any pointer type. From my project perspective there will be indeed a limited number of those (~10).  But from the `PointerOrError` library (my StatusOr library) perspective, I think it shouldn't know which pointers it's being used for (or need to include every ".h" file I have). Ideally it would support arbitrary pointer type that a client of the library might want to use. 

Again ... not a big issue I can simply use the UnsafePointerOrError version, and cast to the desired pointer at the next line. It's just would feel clearner with one fewer line with a floating unsafe pointer around :)

cheers 

Axel Wagner

unread,
Apr 16, 2023, 3:44:52 AM4/16/23
to Jan, golang-nuts
The thing is, if it works with an arbitrary pointer type, it also works with `*[1<<30]byte`, giving unsafe memory access to essentially the entire memory space. Without any additional safeguards. You say it would "feel cleaner without the additional lines of unsafe.Pointer floating around", but that feeling is treacherous. Returning an unsafe.Pointer is honest and clear, because it really *is* an unsafe operation. And adding generics to the mix just gives off the false impression that you have a type-checked operation, when you don't.

I really think the right solution here is to provide wrappers that just provide type-safe access to the underlying C calls. But you do you, of course.

Jan

unread,
Apr 16, 2023, 4:31:50 AM4/16/23
to golang-nuts
I think I see your point now, thanks for explaining. You are right, I should avoid having this sense of "type security". The `Pointer[]` library is only doing syntatic sugar around the fundamentally unsafe casting of an `unsafe.Pointer`.

Having said that, unfortunately limiting it to a list of types in `CPointer` also doesn't solve the problem, because one could still choose the wrong one.
Reply all
Reply to author
Forward
0 new messages