We have a discussion here over using (or not) unsafe.Pointer
for passing pointers to byte arrays from Go to C.
What is the biggest reason (not) to use unsafe.Pointer()
? I would take consistency as a reason, as you'd call an 'external' function, even in a different language, and you want to guarantee that its a Pointer type.
However, due to the Go language style looking a bit like C, using a straight cast using (*C.uchar)(&buffer[0]))
is valid and works. The fact that it works doesn't make me confident that its safer than using unsafe.Pointer()
.
Maybe I'm a bit confused/conflicted by the Go casts looking like function calls, and Pointer
being defined as type Pointer *ArbitraryType
actually says (*ArbitraryType)&buffer[0]
and does not actually invoke any command to do the 'conversion' but really just helps explaining what is going on, on a functional level, kind of like a macro could do.
The Go documention is littered with warnings against using package unsafe
to needlessly defeat Go's type system. unsafe.Pointer
is clearly identified as unsafe for a reason.
Package unsafe contains operations that step around the type safety of Go programs.
Packages that import unsafe may be non-portable and are not protected by the Go 1 compatibility guidelines.
Pointer therefore allows a program to defeat the type system and read and write arbitrary memory. It should be used with extreme care.
Go is a garbage collected language, and the garbage collector needs to know the location of every pointer to Go memory. Because of this, there are restrictions on passing pointers between Go and C.
[These] rules are checked dynamically at runtime.
It is possible to defeat this enforcement by using the unsafe package, and of course there is nothing stopping the C code from doing anything it likes. However, programs that break these rules are likely to fail in unexpected and unpredictable ways.
Let's review a realistic example.
Here's a C function that receives a bytes buffer.
void printbuf(size_t len, unsigned char *buf)
In Go, using cgo and preserving type safety with matching types, we could write,
var buf []byte
C.printbuf(C.size_t(len(buf)), (*C.uchar)(&buf[0]))
However, that's still unsafe, buf[0]
will be out of range if len(buf) == 0
. When buf
is initialized to it's zero-value the array pointer will also be nil
. We can neatly encapsulate the integrity checks in a Go function, which the Go gc optimizing compiler will inline.
func cbuf(buf []byte) (size C.size_t, ptr *C.uchar) {
var bufptr *byte
if cap(buf) > 0 {
bufptr = &(buf[:1][0])
}
return C.size_t(len(buf)), (*C.uchar)(bufptr)
}
and
bufsize, bufptr := cbuf(buf)
C.printbuf(bufsize, bufptr)
Defeating the type system by using unsafe.Pointer
is unsafe. For example,
C.printbuf(C.size_t(len(buf)), (*C.uchar)(unsafe.Pointer(&buf[0])))
The buf
type could be any indexed type: array, pointer to array, slice, string or map. Even worse, the size, if not one byte, will be wrong. Now it gets get really ugly,
C.printbuf(C.size_t(len(buf)*int(unsafe.Sizeof(buf[0]))), (*C.uchar)(unsafe.Pointer(&buf[0])))
And we haven't taken into account nil pointers and out of range values.
Next the code review: code should be correct, maintainable, robust, reasonably efficient, and, most importantly, readable. Don't expect the code review of unsafe.Pointer
usage to go well.
Let's hear your rationale for the use of unsafe.Pointer for this purpose.
Sample code:
printbuf.go
:
package main
/*
#include <stdio.h>
void printbuf(size_t len, unsigned char *buf) {
printf("%lu [", len);
if (!buf) {
len = 0;
}
size_t maxwidth = 16;
size_t width = len <= maxwidth ? len : maxwidth;
for (size_t i = 0; i < width; i++) {
if (i > 0) {
printf(" ");
}
printf("%02X", buf[i]);
}
if (width < len) {
printf(" ...");
}
printf("]
");
}
*/
import "C"
import (
"unsafe"
)
// NOTE: -gcflags='-m' : can inline cbuf : inlining call to cbuf
func cbuf(buf []byte) (size C.size_t, ptr *C.uchar) {
var bufptr *byte
if cap(buf) > 0 {
bufptr = &(buf[:1][0])
}
return C.size_t(len(buf)), (*C.uchar)(bufptr)
}
func main() {
var buf []byte // zero-value = nil, len = 0, cap = 0
bufsize, bufptr := cbuf(buf)
C.printbuf(bufsize, bufptr)
buf = make([]byte, 0) // len = 0, cap = 0
bufsize, bufptr = cbuf(buf)
C.printbuf(bufsize, bufptr)
buf = make([]byte, 0, 32) // len = 0
bufsize, bufptr = cbuf(buf)
C.printbuf(bufsize, bufptr)
buf = make([]byte, 32) // len > 0
for i := range buf {
buf[i] = byte(i)
}
bufsize, bufptr = cbuf(buf)
C.printbuf(bufsize, bufptr)
if len(buf) > 0 {
C.printbuf(C.size_t(len(buf)), (*C.uchar)(&buf[0]))
C.printbuf(C.size_t(len(buf)), (*C.uchar)(unsafe.Pointer(&buf[0])))
C.printbuf(C.size_t(len(buf)*int(unsafe.Sizeof(buf[0]))), (*C.uchar)(unsafe.Pointer(&buf[0])))
}
}
Output:
0 []
0 []
0 []
32 [00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ...]
32 [00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ...]
32 [00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ...]
32 [00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ...]