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

cmd/cgo: provide some mechanism for treating certain C pointer types as uintptr #22906

Open
worldiety opened this Issue Nov 28, 2017 · 26 comments

Comments

Projects
None yet
9 participants
@ghost

ghost commented Nov 28, 2017

Please answer these questions before submitting your issue. Thanks!

What version of Go are you using (go version)?

go version go1.9.2 darwin/amd64

Does this issue reproduce with the latest release?

yes

What operating system and processor architecture are you using (go env)?

GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/tschinke/go"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/bv/6rt5ck194ls704dz9p4c0hlh0000gn/T/go-build985720357=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"

What did you do?

I'm not using goMobile but using jni + cgo directly to get maximum control. The code works without any problems on ARM but fails on x86 randomly. At first it looks like the usual cgo check, however I'm quite confident that this is a bug. One variant is this:

E/Go: panic: runtime error: cgo argument has Go pointer to Go pointer
E/Go: goroutine 17 [running, locked to thread]:
E/Go: main.GetPrimitiveArrayCritical.func1(0x5d12ef60, 0x72d00005, 0x72c68a80)
E/Go: /Users/tschinke/repos/libs_wdy/go_snappy/go/src/wdy/jni.go:1852 +0x79
E/Go: main.GetPrimitiveArrayCritical(0x5d12ef60, 0x72d00005, 0x62811b64)
E/Go: /Users/tschinke/repos/libs_wdy/go_snappy/go/src/wdy/jni.go:1852 +0x23
E/Go: main.Java_de_worldiety_snappy_go_GoSnappy_uncompress2(0x5d12ef60, 0x7c700001, 0x72d00005, 0x0, 0x8, 0x18)
E/Go: /Users/tschinke/repos/libs_wdy/go_snappy/go/src/wdy/math.go:79 +0x27
E/Go: main._cgoexpwrap_3ee6d16a6a1f_Java_de_worldiety_snappy_go_GoSnappy_uncompress2(0x5d12ef60, 0x7c700001, 0x72d00005, 0x0, 0x8, 0x0)
E/Go: command-line-arguments/_obj/_cgo_gotypes.go:3055 +0x6a

But the cgo statement is wrong, math.go:79 looks like this:

77: //export Java_de_worldiety_snappy_go_GoSnappy_uncompress2
78: func Java_de_worldiety_snappy_go_GoSnappy_uncompress2(env *C.JNIEnv, clazz C.jclass, compressed C.jbyteArray, compressedOffset C.jint, compressedSize C.jint) C.jbyteArray {
79: ptrComp := GetPrimitiveArrayCritical(env, C.jarray(compressed))

To be complete, the GetPrimitiveArrayCritical method is declared like this:
// jni.h:
// void * (JNICALL *GetPrimitiveArrayCritical)(JNIEnv *env, jarray array, jboolean *isCopy);
func GetPrimitiveArrayCritical(env *C.JNIEnv, array C.jarray) unsafe.Pointer {
return C._GoJniGetPrimitiveArrayCritical(env, array)
}

And this:
static void* _GoJniGetPrimitiveArrayCritical(JNIEnv* env, jarray array)
{
return (*env)->GetPrimitiveArrayCritical(env, array, JNI_FALSE);
}

Please correct me, but I can't see any Go-Pointer at all.

What did you expect to see?

a) cgo check should not randomly fail
b) cgo check should be consistent across targets (x86 and arm)
c) a possibility to disable the cgo check at runtime, which works for an android library. Setting the environment variable through Android (Os.setenv or libcore) has no effect, and to set it from Go itself is to late. Give us some public variable like GOGC.

What did you see instead?

Random panics on Dell Venue 8, running Android 4.4 on x86 (32bit), but works on ARMs without problems.

@bradfitz bradfitz changed the title from cgo check has random dropouts on Android + x86 to x/mobile: cgo check has random dropouts on Android + x86 Nov 28, 2017

@gopherbot gopherbot added this to the Unreleased milestone Nov 28, 2017

@gopherbot gopherbot added the mobile label Nov 28, 2017

@bradfitz

This comment has been minimized.

Member

bradfitz commented Nov 28, 2017

There already exists GODEBUG=cgocheck=0 as documented first at https://golang.org/doc/go1.6#cgo.

But if the check is wrong, the check should be fixed.

And if the code is wrong, the code should be fixed.

You shouldn't just set GODEBUG=cgocheck=0 and assume the problem is fixed. You'll likely just crash mysteriously elsewhere instead.

/cc @ianlancetaylor @eliasnaur

@ianlancetaylor ianlancetaylor changed the title from x/mobile: cgo check has random dropouts on Android + x86 to runtime: cgo check has random dropouts on Android + x86 Nov 28, 2017

@ianlancetaylor ianlancetaylor modified the milestones: Unreleased, Go1.11 Nov 28, 2017

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Nov 28, 2017

The definition of a Go pointer is a pointer into Go memory. A Go pointer can have a *C.p type, and indeed this is common. While the cgo checks can be over-broad, they are never wrong about whether a value is a Go pointer or not. The closest they can come to being wrong are declaring a value as a pointer when it should really be an integer, or accidentally using uninitialized values from C.

The checks can be over broad as described at #14210.

In order to find the problem, somebody who can recreate it is going to have to look at the actual values in question, to find the Go pointers.

@ghost

This comment has been minimized.

ghost commented Nov 28, 2017

Thanks for the comments, and I've read a lot of tickets before I've opened this issue.

What I don't get is, that I just receive c pointers as parameters and just pass them to another c function, so I expect that Go should not inspect these values at all. (the *C.JNIEnv and C.jarray which are just forwarded back right into the c world)

And the fact, that it works reliable on other devices.

Btw, we are using good old c with JNI on the same device, which does the same, without ever encountering any crash or unwanted behavior.

@ghost

This comment has been minimized.

ghost commented Nov 28, 2017

@bradfitz Setting the GODEBUG environment variable does not work on Android. There is no possibility to define any Environment variable for an Android App - it is just not designed to support that. I tried to workaround this by using setenv from https://developer.android.com/reference/android/system/Os.html (and other hacks) before the library is loaded into the App's process using System.loadLibrary (the way the Java/Art-VM loads shared libraries) but the environment variables are always empty (os.Environ()) at the Go side.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Nov 28, 2017

We need to be clear on terminology. When you say you just receive C pointers as parameters, do you mean that those parameters have C types? Or do you mean that they are allocated by calling C.malloc or returned by C functions. Because in the terminology that matters here, only the latter are C pointers.

That is, when you say that you receive C pointers, where do those pointers come from? What allocates the memory to which they point?

The fact that the code works on other platforms is interesting but it doesn't really prove anything. There is evidently some difference on this platform. You need to understand that difference before you can conclude that the right decision is to disable the check.

@ghost

This comment has been minimized.

ghost commented Nov 29, 2017

Code snippet (math.go):

78: var counter = 0
79: //export Java_de_worldiety_snappy_go_GoSnappy_uncompress2
80: func Java_de_worldiety_snappy_go_GoSnappy_uncompress2(env *C.JNIEnv, clazz C.jclass, compressed C.jbyteArray, compressedOffset C.jint, compressedSize C.jint) C.jbyteArray {
81:		//print our loop number
82:		C._GoSyslog(C.CString("run #"+strconv.Itoa(counter)))
83:		counter++
84:
85:		runPart1 := true
86:		runPart2 := true
87:	
88:		if runPart1{
89:			//this has no Go pointers involved, we just pass pointers from malloc/gralloc/ashmem or whatever through, I don't know the internals from Dalvik/ART/Java-VM
90:			//env is a pointer to the jni environment and compressed
91:			ptrComp := GetPrimitiveArrayCritical(env, C.jarray(compressed))
92:			ReleasePrimitiveArrayCritical(env, C.jarray(compressed), ptrComp, 0)
93:		}
94:
95:	if runPart2{
96:		//create some go garbage, the underlying array is managed by Go
97:		dstSlice := make([]byte,5)
98:		//allocate a java array of the same size, this is managed by the Dalvik/ART/Java-VM
99:		jByteArray := NewByteArray(env, C.jsize(len(dstSlice)))
100:	//tell the Dalvik/ART/Java-VM not to move it's array pointer, so that Go can work with it
101:	pinnedPtr := GetPrimitiveArrayCritical(env, C.jarray(jByteArray))
102:	//copy the bytes from the slice into the pinned java array (dst, src, num) using 'workaround' from https://github.com/golang/go/issues/14210
103:	C.memcpy(pinnedPtr, unsafe.Pointer(&dstSlice[0]), C.size_t(len(dstSlice)))
104:	//release the pinning on the java array
105:	ReleasePrimitiveArrayCritical(env, C.jarray(jByteArray), pinnedPtr,0)
106: }
107:
108: return nil
109:}

Code snippet (jni.go)

790: static void* _GoJniGetPrimitiveArrayCritical(JNIEnv* env, jarray array)
791: {
792: 	return (*env)->GetPrimitiveArrayCritical(env, array, JNI_FALSE);
793: }
...
1849: // jni.h:
1850: //     void * (JNICALL *GetPrimitiveArrayCritical)(JNIEnv *env, jarray array, jboolean *isCopy);
1851: func GetPrimitiveArrayCritical(env *C.JNIEnv, array C.jarray) unsafe.Pointer {
1852: 	return C._GoJniGetPrimitiveArrayCritical(env, array)
1853: }

Oberservation 1: runPart1 == true and runPart2 == true

After a few runs (~ 40 - 800) I get "always"

panic: runtime error: cgo argument has Go pointer to Go pointer
 E/Go: goroutine 17 [running, locked to thread]:
 E/Go: main.GetPrimitiveArrayCritical.func1(0x5d12ef70, 0x73000005, 0x72c84ee0)
 E/Go: 	/Users/tschinke/repos/libs_wdy/go_snappy/go/src/wdy/jni.go:1852 +0x79
 E/Go: main.GetPrimitiveArrayCritical(0x5d12ef70, 0x73000005, 0x933a44c0)
 E/Go: 	/Users/tschinke/repos/libs_wdy/go_snappy/go/src/wdy/jni.go:1852 +0x23
 E/Go: main.Java_de_worldiety_snappy_go_GoSnappy_uncompress2(0x5d12ef70, 0x7ca00001, 0x73000005, 0x0, 0x26, 0x18)
 E/Go: 	/Users/tschinke/repos/libs_wdy/go_snappy/go/src/wdy/math.go:91 +0x9f
 E/Go: main._cgoexpwrap_e45740c9f64c_Java_de_worldiety_snappy_go_GoSnappy_uncompress2(0x5d12ef70, 0x7ca00001, 0x73000005, 0x0, 0x26, 0x0)
 E/Go: 	command-line-arguments/_obj/_cgo_gotypes.go:3055 +0x6a

This indicates that sending a non-Go-allocated pointer is not allowed, but wait...

Oberservation 1: runPart1 == true and runPart2 == false

just running part 1 is fine, cancelled test after more than 30min, "no" crash at all.

Oberservation 2: runPart1 == false and runPart2 == true

After a few runs (~ 40 - 700) I get "always"

panic: runtime error: cgo argument has Go pointer to Go pointer
 E/Go:goroutine 17 [running, locked to thread]:
 E/Go: main.GetPrimitiveArrayCritical.func1(0x5d1468d0, 0x72d00009, 0x72d00009)
 E/Go: 	/Users/tschinke/repos/libs_wdy/go_snappy/go/src/wdy/jni.go:1852 +0x79
 E/Go: main.GetPrimitiveArrayCritical(0x5d1468d0, 0x72d00009, 0x72d00009)
 E/Go: 	/Users/tschinke/repos/libs_wdy/go_snappy/go/src/wdy/jni.go:1852 +0x23
 E/Go: main.Java_de_worldiety_snappy_go_GoSnappy_uncompress2(0x5d1468d0, 0x7f200001, 0x75800005, 0x0, 0x8, 0x18)
 E/Go: 	/Users/tschinke/repos/libs_wdy/go_snappy/go/src/wdy/math.go:101 +0xee
 E/Go: main._cgoexpwrap_69b159f8cea9_Java_de_worldiety_snappy_go_GoSnappy_uncompress2(0x5d1468d0, 0x7f200001, 0x75800005, 0x0, 0x8, 0x0)
 E/Go: 	command-line-arguments/_obj/_cgo_gotypes.go:3055 +0x6a

Observation 3: runPart == false and runPart2 == true and comment memcpy line 103(!)

just running part 1 is fine, cancelled test after more than 30min, "no" crash at all.
Update: crashed after more than 2h

Feelings

  • the code lines which cgo complains about, seems not to relate to the real problem
  • for me, as someone who writes java-jni-c code bindings regularly, this cgo check behavior does not make sense. Actually they hinder me to get the job done. It feels like "magic", which I don't want at this point.
  • Go seems not to "scale" well on FFI

Cause analysis

I used your comment about the pointer terminology as an occasion to find out what "jbytearray" is. In the end, the typedef resolves over jobject to a void*.
E.g. in [1] one can see the method "addLocalReference" which uses "IndirectRefTable.add()"[2] of a thread.
And here we see the crux of the matter: jobject is not a pointer at all in the Dalvik VM, but an artificial generated number, containing bits for table lookups, reference type etc.

[1] https://android.googlesource.com/platform/dalvik/+/tools_r21/vm/Jni.cpp
[2] https://android.googlesource.com/platform/dalvik/+/eclair-release/vm/IndirectRefTable.h

My conclusion / my wish

The cgo checks should never be enabled by default, because Go cannot decide if a pointer is actually a pointer which is allowed to be checked.
Even the developer, who glues libraries together, cannot be made responsible of checking if a pointer is actually a pointer or just a random number.
The implementation difference between Dalvik and ART, which is not expressly specified, but deployed to a billion devices, illustrates such scenario. I don't know how a library will change in the future (e.g. due to a device update), so my cgo code will be fragile in general and is prone to fail because my code is vulnerable to the internals of external bits.

What else can I do, besides disabling or circumvent "any" cgochecks, to write a reliable cgo program?

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Nov 29, 2017

OK, if you a generated number that is not a pointer, then in Go it must not have a pointer type. In Go it must have a type such as uintptr.

I understand your desire, but it can't be done. Go is not C. Go is a garbage collected language, and the garbage collector runs concurrently while the rest of the program is running. The cgo rules were not created to make people's lives difficult; they were created because they are required by the garbage collector. The garbage collector treats every variable with a pointer type as a potential root. If the variable does not hold an actual pointer, if it holds a value that happens to be the same as a pointer to various internal data structures, then the garbage collector will crash. It will crash at a random time during your program's execution.

Disabling the cgo checks will hide the problem for a while, until the garbage collector crashes.

You need to change your code to not use a pointer type for a value that is not a valid pointer.

@randall77

This comment has been minimized.

Contributor

randall77 commented Nov 29, 2017

It might be possible to extend the mechanism in https://go-review.googlesource.com/c/go/+/66332 to handle your case. That fix is for Darwin's CFTypeRef types which are declared as pointers, but sometimes contain integers.

I'd need more info about the C types, their declarations, and their constructors to be sure.

@ghost

This comment has been minimized.

ghost commented Nov 30, 2017

@ianlancetaylor Thanks for giving more insight into this. Perhaps it would be a good idea to have a chapter for cgo and unsafe in the https://golang.org/ref/spec.

However, Keith recognized the problem, which is indeed the same as in https://go-review.googlesource.com/c/go/+/66332.

So I have to insist that the cgo decision - to treat every pointer crossing the FFI to be an unsafe.Pointer - is a fundamental design flaw. Currently there is no way to tell cgo that a c pointer should be an uintptr instead. The approach in gcc.go to hardcode pointer categorization in "badPointerTypedef" can never be a generic solution. Bluntly said, yesterday it was CF*Ref on Darwin, today it is jobject on Dalvik and tomorrow it will be an endless story. We definitely need a change here, so that developers have at least the possibility to have a customizable "badPointerTypedef" somehow.

A tedious workaround may be to create wrappers to hide the bad pointers from cgo (if you ever know them before suffering under random crashes) but it costs a lot of effort. I understand that the intention is to protect the runtime's invariance but it is not a requirement for it to work properly - at least for c-owned pointers and in particular these bad pointers. Other ecosystems with GC do not care either, and they are fine.

@randall77 Thanks for looking into this. jobject is declared in jni.h, which is more or less the same for all NDKs and for Java SE, e.g. https://github.com/aosp-mirror/platform_prebuilt/blob/master/ndk/android-ndk-r8/platforms/android-5/arch-arm/usr/include/jni.h. jni.h defines also various methods to create jobjects, e.g. NewByteArray

@eliasnaur

This comment has been minimized.

Contributor

eliasnaur commented Nov 30, 2017

@randall77 the mechanism for CFTypeRef seems like the way forward, which would make gomobile correct as well. What more information do you need? Valid jobject (and its aliases j*array) values other than nil/0 can only be generated from JNI functions so the only downside is the nil => 0 conversion.

@randall77

This comment has been minimized.

Contributor

randall77 commented Nov 30, 2017

@worldiety : Thanks for the reference. Where is the code that actually makes one of these jobjects? I'm afraid I don't know my way around the android source tree.

I understand that the intention is to protect the runtime's invariance but it is not a requirement for it to work properly - at least for c-owned pointers and in particular these bad pointers.

Actually, it is a requirement. If you type an integer as a pointer, bad things can happen. For instance, Go copies stacks. If one of the integers-typed-as-pointers happens to look like a pointer into the Go stack, when the stack is copied the pointer will be adjusted. If the pointer was really an integer then the Go runtime just randomly adjusted your integer for you.

@ianlancetaylor ianlancetaylor changed the title from runtime: cgo check has random dropouts on Android + x86 to cmd/cgo: provide some mechanism for treating certain C pointer types as uintptr Nov 30, 2017

@ianlancetaylor ianlancetaylor removed the mobile label Nov 30, 2017

@ghost

This comment has been minimized.

ghost commented Nov 30, 2017

@randall77 My wording was probably a bit misleading. What I wanted to say is actually the same (as you pointed already out): Go needs his managed (unsafe) pointers to be fine, however it must not use (unsafe) bad pointers from c. Lumping the ownerships of both worlds blindly together (which is what cgo does) is the design failure. Other ecosystems - like the Java-Jni - never try to detect an ownership automatically in the FFI logic like Go does. And this automatism is "not a requirement" for Go to work.

I'm not a real Android expert either, but let's try to find a way through it:

Please be aware that JNI is also supported by all Java(TM) VMs as well (on perhaps all cgo targets), so the workaround is not limited to Android. Also the way through the Android ART runtime looks totally different.

Proposal A - method level

  • Keep everything as it is, so the default behavior is not changed
  • introduce a comment annotation to optionally instruct cgo when to use uintptr
  • e.g. //bytearray as uintptr

Proposal B - unit level

  • Keep everything as it is, so the default behavior is not changed
  • introduce a gobal comment to mark all jobject's as uintptr
@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Nov 30, 2017

As we've explained, correct identification of pointers is simply not optional. It would be nice if it were. But Go's garbage collector is designed for optimal handling of Go code, not for Go calling C.

Clearly a JNI-style like approach would be a solution, but I think most people find cgo significantly easier to use than JNI.

I've changed the title of this issue to come up with some mechanism for telling cgo that certain C pointer types must not be treated as Go pointers. If anybody has any suggestions for how to do that, I would like to hear them. Thanks.

@randall77

This comment has been minimized.

Contributor

randall77 commented Nov 30, 2017

Random thoughts, in no particular order:

IndirectKey is the base type of all the trouble here. It's the one that is defined as void* but is made from packed integer fields. It is then cast to jobject I think.

I can't find this IndirectKey in other JVMs, only Dalvik and Art. For instance, I don't see it in OpenJDK anywhere. How sure are we that other JVMs use similar techniques?

Using directives isn't really satisfactory. You'd have to include them in every cgo section of every file (or just package? not sure.) that mentions the type. That's a lot of repetition and prone to error.
To avoid repetition, you could can add the directives to the .h files themselves. Good luck getting all the JVM vendors to include a Go-specific comment.

I don't think we want to map all C pointer types to uintptr. We could do that if we had a JNI-style interface requiring a callback for each dereference. But not the way cgo is currently structured. That is, if we have

typedef Foo struct {
    int x,y,z;
}
typedef Foo *FooPtr;

Then we want to have C.FooPtr as a Go type and be able to do

   var p C.FooPtr = ...
   ... = p.x

An interesting possibility is to map void* to uintptr instead of unsafe.Pointer. Although that doesn't have the aforementioned problem, it would nevertheless still break a lot of cgo. It would also break code that stored real Go pointers in a void* on the cgo side, then passed that void* back into Go (does anyone know of such code?). It also doesn't solve the problem that, at least in the CF*Ref cases, that only the top of the class hierarchy is void*; the subclasses are real typed pointers that we also have to map to uintptr. I think the same problem happens for the JNI case - IndirectKey is a *void but jobject is a *struct _jobject.

We already "hide" pointers in some JNI types. For instance, jvalue is a union with both pointer and scalar members. On the Go side it is just a [N]byte. If you're using C.jvalue, it contains a pointer, and that pointer is to a Go object, then you're in trouble. Maybe we assume jvalues can't point to Go objects?

@ghost

This comment has been minimized.

ghost commented Dec 1, 2017

I also looked through the Avian VM [1] and Graal VM [2]. At first glance, I was not able to spot bad pointers for jobject, but that does not mean anything. There are also many other closed source blackbox JVMs, as listed in [3]. We should not assume, that they have no bad pointers.

[1] https://github.com/ReadyTalk/avian (btw Avian is also able to run on iOS)
[2] https://github.com/smarr/graal (probably the successor of the current OpenJDK JVM)
[3] https://en.wikipedia.org/wiki/List_of_Java_virtual_machines

@gopherbot

This comment has been minimized.

gopherbot commented Dec 5, 2017

Change https://golang.org/cl/81876 mentions this issue: cmd/cgo: make JNI's jobject type map to uintptr in Go

@randall77

This comment has been minimized.

Contributor

randall77 commented Dec 5, 2017

CL 81876 contains a possible fix for jobject and friends, similar to what was done for CFTypeRef and friends on darwin.
Opinions welcome on whether we should continue down this one-off route for now.

@bcmills

This comment has been minimized.

Member

bcmills commented Dec 5, 2017

If anybody has any suggestions for how to do that, I would like to hear them.

I posted an idea to a golang-dev thread that might be worth revisiting in light of this issue, #22218, #19928, #21878, #20275, and perhaps #13467.

The idea is to define a distinguished Go type, which I'm calling unsafe.Void, that specifically refers to an object that cannot reside in Go memory.

The differences between *unsafe.Void, unsafe.Pointer, and uintptr would be:

  • uintptr is an integer type: it takes the value 0 instead of nil, and arithmetic can be performed on it.
  • unsafe.Pointer is either nil or a pointer to a valid (Go or non-Go) memory address.
  • unsafe.Void is a zero-sized type that is not instantiable in Go.

The general rules for non-instantiable types would be:

  • A struct with a field of a non-instantiable type is itself non-instantiable, as are maps, slices, channels, and arrays with uninstantiable elements.
  • Any expression that produces a value of a non-instantiable type — such as declaring a variable, calling make, or dereferencing a pointer except in an address operatotion — is a compile-time error.
  • A pointer-to-non-instantiable type is itself instantiable, and has nil as its zero-value, but the compiler must assume that it never points to Go memory (and thus skip it during garbage collection).
  • A pointer to a non-instantiable type can be explicitly converted to and from *unsafe.Void and/or uintptr.
  • A pointer to a non-instantiable type can be converted to or from unsafe.Pointer only if it does not point into the Go heap. (This rule may be enforced with a run-time check.)

To stitch that together with cgo, we would map any incomplete C struct type (and, if necessary, complete types in a whitelist) to an uninstantiable Go type.

Unfortunately, this proposal introduces a backward-incompatibility: the C type void* properly maps to *unsafe.Void rather than unsafe.Pointer, and those two types should not be equivalent or even mutually assignable. I'm not sure how to resolve that.

@bcmills

This comment has been minimized.

Member

bcmills commented Dec 5, 2017

A simpler approach, I suppose, would be to add another structured comment to indicate the type mapping:

// #cgo TYPE_MAP: jobject=uintptr

That would give a migration path for unsafe.Void, but could be used even without it.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Dec 5, 2017

The only problem with the structured comment is that it has to appear in every single file that uses cgo with the troublesome type. It's not a fatal problem, but it's less than ideal.

@bcmills

This comment has been minimized.

Member

bcmills commented Dec 5, 2017

Given that we can't control the C header files, I don't see an alternative in general. The only other option I can think of is to add a (non-portable) annotation to the declaration in the C header to say “this is a sometimes-pointer”, but if we could make changes to the C header in general we could just change the type from a pointer to the more portable uintptr_t to begin with.

On the other hand, if we could address #13467, we could at least recommend that only one Go source file should import a given C header. That would ensure consistency, but isn't feasible today.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Dec 5, 2017

Well, just for example, we could adopt your #cgo TYPE_MAP suggestion, and then also say that the go tool will walk up the tree from the directory holding the cgo-using file to GOROOT looking for config.cgo files, and direct cgo to add all such files to the cgo comment.

gopherbot pushed a commit that referenced this issue Dec 8, 2017

cmd/cgo: make JNI's jobject type map to uintptr in Go
The jobject type is declared as a pointer, but some JVMs
(Dalvik, ART) store non-pointer values in them. In Go, we must
use uintptr instead of a real pointer for these types.

This is similar to the CoreFoundation types on Darwin which
were "fixed" in CL 66332.

Update #22906
Update #21897

RELNOTE=yes

Change-Id: I0d4c664501d89a696c2fb037c995503caabf8911
Reviewed-on: https://go-review.googlesource.com/81876
Run-TryBot: Keith Randall <khr@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
@gopherbot

This comment has been minimized.

gopherbot commented Dec 8, 2017

Change https://golang.org/cl/82917 mentions this issue: doc: add doc about C types that we map to uintptr instead of ptr

gopherbot pushed a commit that referenced this issue Dec 8, 2017

doc: add doc about C types that we map to uintptr instead of ptr
Update #22906
Update #21897

Change-Id: I73709b2fdac6981d4bc2f7dab0767f2dd7be3be5
Reviewed-on: https://go-review.googlesource.com/82917
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>

@ianlancetaylor ianlancetaylor modified the milestones: Go1.11, Unplanned Jun 13, 2018

@jtprobst

This comment has been minimized.

jtprobst commented Aug 23, 2018

We are seeing the same issue as described by @worldiety. We are using a library with the same design - it stores integers in pointers. In our case, the library uses incomplete typedef struct instancePrivate* instance types where instancePrivate is never defined. They could have typed those things uintptr_t but they didn't and C won't ever force them to.
The API is pretty big, so wrapping every function is impractical, albeit not impossible (e.g. by writing a code generator which would be our last resort). Our library is not so widespread that it would be a good idea to special-case the types as done for the JNI types. A customizable solution like the proposal by @bcmills would work for us.

@dominikh

This comment has been minimized.

Member

dominikh commented Oct 1, 2018

Aside from many instances of APIs using void* for opaque types that store non-pointers (I can add OpenGL to the existing list), there is also the case of void *data style pointers used in callback functions, used to associate callbacks with custom data. In Go, this is particularly required since we have to use generic callback functions that dispatch to concrete Go callbacks. And since we can't pass arbitrary Go pointers into C, void* tends to end up storing arbitrary integers that correspond to map keys. When the void* argument isn't part of the callback signature itself but instead stored in a struct, then invalid unsafe.Pointers end up in Go space.

One such example is wl_resource in libwayland.

@bcmills

This comment has been minimized.

Member

bcmills commented Oct 1, 2018

@dominikh, you can always use C.malloc to obtain ranges of valid void* addresses to use as map keys.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment