LoginSignup
23
15

More than 5 years have passed since last update.

CとGolangの境界

Last updated at Posted at 2016-12-07

C言語 Advent Calendar 2016を書く人が少ないので、、
C言語を触っていたころを思い出して書いてみようと思いました。

ここから

前回、マルチプラットフォーム対応したライブラリGolangを紹介しましたが、
実際のところ、どの程度c-sharedしたライブラリを利用する機会があるのでしょうか。

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-shared -> Go
(2) JavaScript -> Node(native extension) -> c-shared -> Go
(3) ActionScript -> ANE -> c-shared -> Go

c-sharedの実例

githubからc-sharedを利用しているプロジェクトを探してみたら、いい感じのものが見つかりました。

検索キーワード
site:github.com c-shared buildmode -"golang"

gohttplib

GoのhttpserverをC/pythonから呼び出すプロジェクトで、
PYCON2016で発表されたネタのようです。

c-sharedにより生成されたヘッダファイルを見てみます。

libgohttp.h

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のprimitive型、
もしくはCのstructに揃えられています。

先日のlibgoとは大きく違いますね。
libgoでは、GoのptrをそのままCの世界から参照可能になっていたため、
実行時にcgocheckに怒られていました。
※ GODEBUG=cgocheck=0はその処理の無効化

libgo.h
struct request_return {
  GoSlice r0;
  GoInterface r1;
};

extern struct request_return request(GoString p0);

c-sharedの関数が全てC側に寄せている以上、
寄せた側の処理は全てGo(cgo)に記述しています。

gohttplib.go
//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))
  ...

responsewriter.go
//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)の登録と参照、削除を受け持っています。

ptrproxy.go
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するのではないでしょうか。
※ この問題はGoのGCが改善されたGo1.5以降で発生するはず

そういった可能性を排除するため、cgocheckは存在しますし、
リソース確保直後であればGCが発生する可能性が低いため、
libgoはなんとなく動きます。

c-sharedからGoを利用する場合、その問題を回避してコードを書く必要があります。

前提条件としてC側からGo側を呼び出しますが、
場合によってはGo側からC側のcallbackを呼び出すこともありえるでしょう。

また話がややこしいので予め整理しておくと、
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側のリソースをopaque pointerとして扱うようなものですね。

メリット

GoのリソースをCのリソースに変換する処理を書く際に、
ptrproxyを利用すると容易に書けます。

デメリット

mapがいつまでも(id, Go ptr)をhandleしたままではリソースリーク
してしまいますので、適切なタイミングでリソースを開放してあげる必要があります。
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側からリソースとcallbackを教えてもらうようなインターフェースで、
Go側はリソースへの書き込みとcallbackを呼び出すだけの場合、
この方法は容易な連携方法です。

デメリット

Go側からC側のリソースへ値のコピーが発生します。
C側のリソースをGo側に教える必要があるため、
引数が複雑になってしまうかもしれません。
元のC側の実装に依存しますが、C側を変更することが負担になるかもしれません。

回避方法(4)

Go側でリソース確保したポインタをそのままC側に教えるとまずいので、
Go側からCのリソース確保の関数を呼び出し、
そこに値を書き込んだ後、C側へreturn(owner移譲)する方法でも回避可能です。
go側でC.malloc()を呼び出し、そのポインタをC側に伝える感じでしょうか。

メリット

C側を変更する負担が少ないです。

デメリット

Go側からC側のリソースへ値のコピーが発生します。
C側では適切にリソースを開放してあげる必要が出てきますので、
C側から見ると非対称なインターフェースになってしまうかもしれません。

回避方法(5)

c-sharedの引数やリソース云々は全部忘れて、他のRPCで連携する方法があります。
たとえば、c-sharedに公開されているインターフェースはstart/shutdownのみであり、
以降は特定のportに対するREST APIで会話する方法です。

メリット

RPCを書きやすい言語なら有効な方法です。

デメリット

RPC利用に伴うオーバーヘッドが発生します。
また、RPC固有のエラーハンドリングを考慮する必要があるかもしれません。
あれ、これC言語関係なくない?

使い分けは?

C側からGo側を呼び出す前提で私が考えるに
PRCを予めサポートしてる場合(5)
基本的には(2)
複雑なGoのメソッドをC側にそのままexportしたい場合(1)
callbackでbytesを渡したい場合(3)
複雑なCの構造体のcreate()/free()系の補助メソッドが揃っていて
Go側に作らせたい場合(4)

まとめ

C言語は偉大だしいろいろ使い回しが効くので素晴らしいですね!

皆さんもマルチプラットフォーム対応したライブラリGolangを使ってみましょう!

c-sharedの使いみちとして3択っぽく提示してみましたが、
実際に採用しているものが1つあります。どれでしょうか。
こんな実装方法を勧めてくるマネージャーって(ㅍ_ㅍ)

23
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
15