C和Golang之间的界限
因为参与编写C语言Advent Calendar 2016的人很少,所以我想起了当初接触C语言的时候,想要写下来试试看。
从这里开始
上次我们介绍了一个支持多平台的Golang库,实际上,在使用共享C库的机会上有多大的范围呢?
Go支持与名为cgo的C/C++库进行协作,可以轻松地从Go中调用C/C++的API。
然而,从C/C++调用Go的能力相当有限,主要使用情况可能仅限于希望从其他编程语言使用Go实现的功能的情况。
更具体一点来说,因为将Go开发的功能移植到其他语言会很麻烦,所以我们决定将其转化为c-shared库,以便从其他语言中调用。
c-shared是C的接口,因此不仅限于C/C++,可以在其他语言中通过C的接口进行协作,如下所示的示例。
(1) Java -> JNI -> 共享C库 -> Go
(2) JavaScript -> Node(本地扩展)-> 共享C库 -> Go
(3) ActionScript -> ANE -> 共享C库 -> Go
c-shared的示例
当我在 Github 上搜索使用 c-shared 的项目时,我发现了一些不错的选项。
在github.com网站上搜索关键词是“c-shared buildmode”,但排除关键词“golang”。
Gohttplib (高速HTTP库)
在一个从C/Python调用Go的httpserver的项目中,这似乎是PYCON2016上演讲的一个话题。
让我们查看通过c-shared生成的头文件。
typedef struct Request_
{
const char *Method;
const char *Host;
const char *URL;
const char *Body;
const char *Headers;
} Request;
typedef unsigned int ResponseWriterPtr;
typedef void FuncPtr(ResponseWriterPtr w, Request *r);
extern void Call_HandleFunc(ResponseWriterPtr w, Request *r, FuncPtr *fn);
...
extern void ListenAndServe(char* p0);
extern void HandleFunc(char* p0, FuncPtr* p1);
extern int ResponseWriter_Write(unsigned int p0, char* p1, int p2);
extern void ResponseWriter_WriteHeader(unsigned int p0, int p1);
注目的是,c-shared功能全部是由C的基本类型或C的结构体构成的。
最近的libgo与以前相比差别很大。
在以前的libgo中,Go的ptr可以直接从C的世界中引用,
这导致在运行时经常会收到cgocheck的警告。
※ GODEBUG=cgocheck=0可以禁用此处理。
struct request_return {
GoSlice r0;
GoInterface r1;
};
extern struct request_return request(GoString p0);
由于所有c-shared函数都被移到C端,因此所有移动的处理都在Go(cgo)中进行编写。
//export HandleFunc
func HandleFunc(cpattern *C.char, cfn *C.FuncPtr) {
// C-friendly wrapping for our http.HandleFunc call.
pattern := C.GoString(cpattern)
http.HandleFunc(pattern, func(w http.ResponseWriter, req *http.Request) {
...
// Convert the ResponseWriter interface instance to an opaque C integer
// that we can safely pass along.
wPtr := cpointers.Ref(unsafe.Pointer(&w))
// Call our C function pointer using our C shim.
C.Call_HandleFunc(C.ResponseWriterPtr(wPtr), &creq, cfn)
// release the C memory
C.free(unsafe.Pointer(creq.Method))
C.free(unsafe.Pointer(creq.Host))
C.free(unsafe.Pointer(creq.URL))
C.free(unsafe.Pointer(creq.Body))
C.free(unsafe.Pointer(creq.Headers))
...
//export ResponseWriter_WriteHeader
func ResponseWriter_WriteHeader(wPtr C.uint, header C.int) {
w, ok := cpointers.Deref(wPtr)
if !ok {
return
}
(*(*http.ResponseWriter)(w)).WriteHeader(int(header))
}
有趣的是PtrProxy通过一级间接引用来操作。
ref()、deref()、free()等函数负责(resource的id,Go ptr)的注册、引用和删除。
type ptrProxy struct {
sync.Mutex
count uint
lookup map[uint]unsafe.Pointer
}
// Ref registers the given pointer and returns a corresponding id that can be
// used to retrieve it later.
func (p *ptrProxy) Ref(ptr unsafe.Pointer) C.uint
// Deref takes an id and returns the corresponding pointer if it exists.
func (p *ptrProxy) Deref(id C.uint) (unsafe.Pointer, bool)
// Free releases a registered pointer by its id.
func (p *ptrProxy) Free(id C.uint)
cgocheck的绕过方式
如果将在Go侧分配的资源指针直接传递给C侧,
有可能因为Go的垃圾回收器(GC)移动资源或者不知道C会引用该资源,
导致在GC运行中被意外删除,从而在不希望的时刻出现段错误(SEGV)问题。
* 这个问题应该在改进过的Go1.5及以后的版本中发生。
为了排除此类可能性,cgocheck存在,而且如果在资源分配后几乎不太可能触发垃圾回收,
所以libgo有点能够正常工作。
如果要使用c-shared来调用Go,就需要避免这个问题并编写代码。
在某些情况下,可能需要从C端调用Go端,但同时也可能需要从Go端调用C端的回调函数。
由于事情有点复杂,提前整理一下: cgo可以在Go源代码中使用注释编写,也可以编写为独立的C/C++源代码,最终将被构建为Go的库。
c-shared main or wrapper
+---------+----------------+
| Go, cgo | C/C++ |
+---------+----------------+
策略避免(1)
由于直接将在Go侧分配的资源指针传递给C侧是不好的,所以像gohttplib这样的方法是有效的。在gohttplib中,通过在Go侧使用map来管理已分配的资源指针,并通过将map的ID传递给C侧来避免问题。这种方法类似于将Go侧的资源视为不透明指针。
优点
使用ptrproxy可以方便地将Go资源转换为C资源。
缺点 (quē
由于map长时间持有(id,Go ptr)并且没有释放资源,可能会导致资源泄漏,因此需要在适当的时机释放资源。
在gohttplib中,通过显式的free调用来从map中删除并标记为GC对象。
另外,由于引用了map,所以在调用前后会产生相当大的开销。如果想避免Go和C之间的边界开销,建议在其中一层实现时考虑性能。
避免方法(2)
将在Go侧分配的资源指针直接传递给C侧是行不通的,但是通过cgo函数的参数指定的指针将受到GC的保护。
因此,在cgo的参数中将Go的资源传递给C侧是有效的方法。
优点
可以自然地作为cgo编写。
缺点
如果在C端需要多次使用通过参数获得的资源,就需要在C端编写适时的复制处理。
避免方法 (3)
为了避免直接将在Go端分配的资源指针传递给C端而导致问题,可以在C端分配内存资源并通过参数等方式将其传递给Go端,然后在Go端将值复制到C的资源中。
优点
C侧借助接口从Go侧获取资源和回调函数,只需在Go侧写入资源和调用回调函数,这种方法是一种简便的协作方式。
缺点
从Go侧复制值到C侧的资源。
由于需要将C侧的资源告知给Go侧,
参数可能会变得复杂。
这取决于原始的C端实现,可能改变C侧可能会带来负担。
避免的方式(4)
因为将Go侧获取的资源指针直接传给C侧是不合适的,所以可以通过在Go侧调用C资源分配函数,在其中写入值后返回给C侧的方式来避免这个问题。
可以尝试在Go侧调用C.malloc(),然后将指针传递给C侧。
好处
改变C侧的负担较小。
缺點
从Go方向向C方向的资源发生值的复制。
C方需要适当地释放资源,
所以从C方来看可能会变成不对称的接口。
绕开的方式(5)
忘记c-shared的参数和资源之类的事情吧,还有其他的RPC协作方法。
例如,c-shared只公开了start/shutdown接口,
之后可以通过特定的端口使用REST API进行交流。
优点
如果是一种容易编写的语言,使用RPC是有效的方法。
缺点
使用RPC会导致一些额外的开销。
此外,可能还需要考虑处理RPC特定的错误情况。
哎呀,这跟C语言没有关系吧?
换用另一种方式呢?
根据我的考虑,在调用方面,假设从C端调用Go端,如果事先支持PRC(5),基本上(2),如果想要直接将复杂的Go方法导出到C端(1),如果想要在回调中传递字节(3),如果有C端的复杂结构的create()/free()系辅助方法,并且希望在Go端创建(4)。
整理
C语言非常伟大,因为它可以被广泛地重复使用,真是太棒了!
大家也来试试使用支持多平台的Golang库吧!
我试图以三个选项的形式提供使用c-shared的用途,请问实际上有一种被采用了。那是哪一种呢?
这样的经理居然推荐这种实现方法(ㅍ_ㅍ)