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的用途,请问实际上有一种被采用了。那是哪一种呢?
这样的经理居然推荐这种实现方法(ㅍ_ㅍ)

bannerAds