Go语言和C语言共享某一段内存
的场景。我们知道C语言的内存在分配之后就是稳定
的,但是Go语言因为函数栈的动态伸缩可能导致栈中内存地址的移动
(这是Go和C内存模型的最大差异)。如果C语言持有的是移动之前的Go指针,那么以旧指针访问Go对象时会导致程序崩溃。无法在Go语言中创建大于2GB内存的切片
(具体请参考makeslice实现代码)。不过借助cgo技术,我们可以在C语言环境创建大于2GB的内存,然后转为Go语言的切片使用:package main
/*
#include <stdlib.h>
void* makeslice(size_t memsize) {
return malloc(memsize);
}
*/
import "C"
import (
"unsafe"
)
func makeByteSlize(n int) []byte {
p := C.makeslice(C.size_t(n))
return ((*[1<<31]byte)(p))[0:n:n]
}
func freeByteSlice(p []byte) {
C.free(unsafe.Pointer(&p[0]))
}
func main() {
s := makeByteSlize(1 << 31)
s[len(s)-1] = 255
print(s[len(s)-1])
freeByteSlice(s)
}
goroutinue
的栈上的Go语言内存传入了C语言函数后,在此C语言函数执行期间,此goroutinue
的栈因为空间不足的原因发生了扩展,也就是导致了原来的Go语言内存被移动到了新的位置。但是此时此刻C语言函数并不知道该Go语言内存已经移动了位置,仍然用之前的地址来操作该内存——这将将导致内存越界。以上是一个推论(真实情况有些差异),也就是说C访问传入的Go内存可能是不安全的!package main
/*
#include <stdlib.h>
#include <stdio.h>
void printString(const char* s) {
printf("%s", s);
}
*/
import "C"
import "unsafe"
func printString(s string) {
cs := C.CString(s) // 在c中申请内存
defer C.free(unsafe.Pointer(cs))
C.printString(cs) // 释放
}
func main() {
s := "hello"
printString(s)
}
C.CString
将Go语言字符串对应的内存数据复制到新创建的C语言内存空间上。上面例子的处理思路虽然是安全的,但是效率极其低下(因为要多次分配内存并逐个复制元素),同时也极其繁琐。在CGO调用的C语言函数返回前,cgo保证传入的Go语言内存在此期间不会发生移动
,C语言函数可以大胆地使用Go语言的内存!package main
/*
#include<stdio.h>
void printString(const char* s, int n) {
int i;
for(i = 0; i < n; i++) {
putchar(s[i]);
}
putchar(‘\n‘);
}
*/
import "C"
import (
"reflect"
"unsafe"
)
func printString(s string) {
p := (*reflect.StringHeader)(unsafe.Pointer(&s))
C.printString((*C.char)(unsafe.Pointer(p.Data)), C.int(len(s)))
}
func main() {
s := "hello"
printString(s)
}
goroutine不能动态伸缩栈内存
,也就是可能导致这个goroutine被阻塞
。因此,在需要长时间运行的C语言函数(特别是在纯CPU运算之外,还可能因为需要等待其它的资源而需要不确定时间才能完成的函数),需要谨慎处理传入的Go语言内存
。// 错误的代码
tmp := uintptr(unsafe.Pointer(&x))
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
不能在C语言函数中直接使用Go语言对象的内存
。package main
import "sync"
type ObjectId int32
var refs struct {
sync.Mutex
objs map[ObjectId]interface{}
next ObjectId
}
func init() {
refs.Lock()
defer refs.Unlock()
refs.objs = make(map[ObjectId]interface{})
refs.next = 1000
}
func NewObjectId(obj interface{}) ObjectId {
refs.Lock()
defer refs.Unlock()
id := refs.next
refs.next++
refs.objs[id] = obj
return id
}
func (id ObjectId) IsNil() bool {
return id == 0
}
func (id ObjectId) Get() interface{} {
refs.Lock()
defer refs.Unlock()
return refs.objs[id]
}
func (id *ObjectId) Free() interface{} {
refs.Lock()
defer refs.Unlock()
obj := refs.objs[*id]
delete(refs.objs, *id)
*id = 0
return obj
}
package main
/*
extern char* NewGoString(char* );
extern void FreeGoString(char* );
extern void PrintGoString(char* );
static void printString(char* s) {
char* gs = NewGoString(s);
PrintGoString(gs);
FreeGoString(gs);
}
*/
import "C"
//export NewGoString
func NewGoString(s *C.char) *C.char {
gs := C.GoString(s)
id := NewObjectId(gs)
return (*C.char)(unsafe.Pointer(uintptr(id)))
}
//export FreeGoString
func FreeGoString(p *C.char) {
id := ObjectId(uintptr(unsafe.Pointer(p)))
id.Free()
}
//export PrintGoString
func PrintGoString(p *C.char) {
id := ObjectId(uintptr(unsafe.Pointer(p)))
gs := id.Get().(string)
print(gs)
}
func main() {
C.printString(C.CString("hello\n"))}
NewGoString
创建一个对应的Go字符串对象,返回的其实是一个id,不能直接使用。我们借助PrintGoString
函数将id解析为Go语言字符串后打印。该字符串在C语言函数中完全跨越了Go语言的内存管理,在PrintGoString
调用前即使发生了栈伸缩导致的Go字符串地址发生变化也依然可以正常工作,因为该字符串对应的id是稳定的,在Go语言空间通过id解码得到的字符串也就是有效的。/*
extern int* getGoPtr();
static void Main() {
int* p = getGoPtr();
*p = 42;
}
*/
import "C"
func main() {
C.Main()
}
//export getGoPtr
func getGoPtr() *C.int {
return new(C.int)
}
$ go run main.go
panic: runtime error: cgo result has Go pointer
goroutine 1 [running]:
main._cgoexpwrap_cfb3840e3af2_getGoPtr.func1(0xc420051dc0)
command-line-arguments/_obj/_cgo_gotypes.go:60 +0x3a
main._cgoexpwrap_cfb3840e3af2_getGoPtr(0xc420016078)
command-line-arguments/_obj/_cgo_gotypes.go:62 +0x67
main._Cfunc_Main()
command-line-arguments/_obj/_cgo_gotypes.go:43 +0x41
main.main()
/Users/chai/go/src/github.com/chai2010 /advanced-go-programming-book/examples/ch2-xx /return-go-ptr/main.go:17 +0x20
exit status 2
_cgo_export.c
文件定义):int* getGoPtr()
{
__SIZE_TYPE__ _cgo_ctxt = _cgo_wait_runtime_init_done();
struct {
int* r0;
} __attribute__((__packed__)) a;
_cgo_tsan_release();
crosscall2(_cgoexp_95d42b8e6230_getGoPtr, &a, 8, _cgo_ctxt);
_cgo_tsan_acquire();
_cgo_release_context(_cgo_ctxt);
return a.r0;
}
_cgo_tsan_acquire
是从LLVM项目移植过来的内存指针扫描函数,它会检查cgo函数返回的结果是否包含Go指针。GODEBUG=cgocheck=0
来关闭指针检查行为。$ GODEBUG=cgocheck=0 go run main.go
原文:https://www.cnblogs.com/binHome/p/12991022.html