Theodore Tsirpanis
Theodore Tsirpanis

Reputation: 787

Does Go implicitly keep alive the parent structures of pointers passed to C functions?

Consider the following snippet in Go that uses cgo to wrap a C API:

/*
  #include <myheader.h>

  void MyCall(my_type* a, my_type* b);
*/
import "C"

// Go wrapper for native pointer to my_type.
// The struct can be assumed to have a finalizer set.
type MyType struct {
    handle *C.my_type
}

func (a *MyType) MyCall(b *MyType) {
    C.MyCall(a.handle, b.handle)
}

My question is, in function MyCall is runtime.KeepAlive required for variables a and b?

According to documentation, Go pointers passed as function arguments to C functions have the memory they point to implicitly pinned for the duration of the call. Does this extend to the parent structures of Go pointers passed as function arguments to C functions?

Upvotes: 0

Views: 79

Answers (2)

kostix
kostix

Reputation: 55553

My reasoning is as follows: the citation you're considering

Go pointers passed as function arguments to C functions have the memory they point to implicitly pinned for the duration of the call.

is a red herring: it describes the memory, pointers passed to a C call refer to, not the memory these pointers occupy by themselves. So, the citation says it's guaranteed that if the blocks of memory pointed at by a.handle and b.handle were allocated by the Go runtime, these blocks are pinned and won't be GC'ed. If they were allocated by other means — such as calls to C.malloc, they will not be pinned¹.

Now that we've moved that out of the way, let's consider your issue:

Does this extend to the parent structures of Go pointers passed as function arguments to C functions?

Note that when a call C.MyCall(a.handle, b.handle) is made, the function C.MyCall receives two uintptr-sized values taken from a.handle and b.handle, which have no connection to the variables a and b except in that they contain the same bit patterns as the handle fields in these variables, respectively (that is because in Go, everything is passed by value, so values of these pointers — that is, the addresses they contain, — are copied to the call frame of the target function (or passed to it in CPU registers; these are all things peculiar to the platform's ABI, and are not really important)).

I hereby propose that the GC is free to collect a and b as "it sees" neither of these values is referenced neither by the call to C.MyCall itself nor by the code past it (as there is no such code). Let me again stress that even though a and b syntactically appear at the call site of C.MyCall, these variables are not referenced by the call as the "dot plus field name" expressions will be evaluated before the call is made, and will result in two pointer values.

Basically, you can reason like the GC:

  1. Suppose the GC scans the call stack which led to the currently active call to C.MyCall.
  2. Suppose the GC has proven there's no live references to a and b except the call frame which is the parent of C.MyCall.
  3. The GC knows that if it collects a and b, it won't do anything bad to the memory referenced by the handle fields of those values: if a.handle and b.handle were the last references to these blocks, the blocks themselves will be eligible for collecting, otherwise they will survive.

All-in-all, this is just my reasoning (that is, an opinion, however educated it may be). I still suggest posting to the mailing list as suggested by my comment).

¹ …which is not really important as the GC won't touch memory not allocated by the runtime's memory-manager.

Upvotes: 0

eik
eik

Reputation: 4610

The simple answer is “No”, because a and b are not reachable from C code.

The function call f(a.handle, b.handle), be it C or Go, passes copies of the values in a.handle and b.handle (which are pointers to C.my_type structures), but other than that the garbage collector is free to release a and b when they are not references elsewhere.

Upvotes: 0

Related Questions