Cgo

Cgo는 Go에서 C언어를 사용할 수 있게해주는 Go의 feature중 하나이다. 이 기능을 이용해 C언어와 Go언어사이의 콜백함수를 만들어 볼 것이다. 주의할 점은 Cgo에서 읽을 수 있는것은 C스타일의 심볼을 갖는 함수 뿐이다. 순수 C언어로만 작성된 라이브러리라면 문제가 없겠지만 C++ 베이스의 함수를 이용하고 싶기 때문에 해당 함수를 한번 wrapping해주는 형태로 만들어 볼 것이다

기본 사용법

함수를 wrapping 해보기 전, 기본적인 사용법을 다시 보자

package main
/*
#include <stdio.h>
void CFoo() {
    printf("Hello from C\n");
}
*/
import "C"
import "fmt"
func main() {
    fmt.Println("Hello from Go")
    C.CFoo()
}

출력결과는 다음과 같을 것이다

Hello from Go
Hello from C

C++ 함수 래핑

C++함수를 래핑하여 Go에서 호출하는 방법이다

// foo.h
#include <string>
void Foo(std::string str);

// ------------------------------------------------

// foo.cpp
#include "foo.h"
#include <iostream>
void Foo(std::string str) {
    std::cout << str << std::endl;
}

위의 함수를 가지는libfoo.so 라이브러리가 있다고 했을 때, Go에서 위 함수를 사용하기 위해선 Cgo에서 wrapper함수를 만들어 호출하는 방식을 사용하면 된다. Cgo에서는 C스타일의 심볼만 읽을 수 있기 때문에 래퍼함수의 헤더파일은 반드시 C 스타일로 작성하여야 한다.

// fooWrapper.h
#ifdef __cplusplus
#define extern "C" {
#endif

void FooCgo(char* str);

#ifdef __cplusplus
}
#endif
// fooWrapper.cpp
#include "fooWrapper.h"

#include "foo.h"
void FooCgo(char* str) {
    Foo(str);
}
// fooWrapper.go
package fooWrapper
/*
#cgo CFLAGS: -I/path/to/cpp_lib
#cgo CXXFLAGS: -I/path/to/cpp_lib
#cgo LDFLAGS: -L/path/to/cpp_lib -lfoo
#include "fooWrapper.h"
*/
import "C"
import "fmt"
func Foo() {
    fmt.Println("Hello from Go")
    C.FooCgo(C.CString("Hello from Cgo"))
}

출력결과

Hello from Go
Hello from Cgo

소스 파일에서 foo.h를 인클루드하고, libfoo.so의 함수Foo()를 호출해주면 된다. 중요한 점은 Cgo에서 컴파일 될 수 있도록 extern "C"키워드를 사용하는 것이다.

Go콜백함수 부르기

그렇다면, Go함수에서 콜백함수를 정의하고 그 함수를 C언어에서 콜백함수를 등록하는 방법이 있을까? Cgo에서는 export를 사용하여 Go로 작성된 함수를 C언어 심볼로 export할 수 있다.

// foo.h
#include <functional>
using callbackFn = std::function<void(int a)>;
void DoSomething(callbackFn fn)

// ------------------------------------------------

// foo.cpp
#include "foo.h"
#include <iostream>
void DoSomething(callbackFn fn) {
    std::cout << "foo::DoSomething()" << std::endl;
    fn(7);
}

위 형태의 콜백함수를 인자로 갖는 호출하는 함수가 있다고 해보자, 우리는 위에서 CPP함수를 래핑하는 방법을 통해서 CPP함수를 호출할 수 있었다. 그리고 Go에서 작성된 콜백함수를 DoSomething()함수의 함수포인터로 보내고 싶다고 하자

// fooWrapper.h
#ifdef __cplusplus
#define extern "C" {
#endif
typedef void(*callbackFnCGO)(int a);

void DoSomethingCGO(callbackFnCGO fn)

#ifdef __cplusplus
}
#endif

아까 했던 방법과 같은 방식으로 DoSomething()래퍼 함수를 만들고, C스타일 자료형 지정자 typedef 키워드로 콜백함수 형식을 만든다.

// fooWrapper.go
package wrapper

/*
#cgo CFLAGS: -I/path/to/cpp_lib
#cgo CXXFLAGS: -I/path/to/cpp_lib
#cgo LDFLAGS: -L/path/to/cpp_lib -lfoo
#include "fooWrapper.h"
void myCallback(int a);
*/
import "C"

func DoSomething() {
	C.DoSomethingCGO(C.callbackFnCGO(C.myCallback))
}

//export myCallback
func myCallback(a C.int) {
	fmt.Printf("callback from go context (%v)\n", int(a))
}

(예제 코드 보기)

Go에서 콜백함수를 작성하고 주석으로 //export [함수이름]의 형태로 작성한다. Cgo에서 이 부분을 보고 C함수심볼로 만들어주고 그것을 코드내에서 C.[export한 함수이름]로 사용할 수 있다. 그 함수를 래핑함수의 파라미터로 넘겨주면 된다. 주의할 점은 //export사이에 공백이 있으면 안된다. 이 점을 유의하여 삽질하지 않도록 주의하자..

출력결과

foo::DoSomething()
callback from go context (7)

c/c++ 베이스로 작성된 라이브러리가 있고, 해당 라이브러리를 사용하는 REST API, GraphQL등의 서버 어플리케이션을 만들어야 할 때 자주 사용하는 방법인데 함수를 래핑하는 방법이 약간 노가다성이 있기도 하지만.. 생산성 및 효율성 등을 고려하면 사용하는 방법만 알아두면 유용하게 쓰일 수 있을 것 같다.