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등의 서버 어플리케이션을 만들어야 할 때 자주 사용하는 방법인데 함수를 래핑하는 방법이 약간 노가다성이 있기도 하지만.. 생산성 및 효율성 등을 고려하면 사용하는 방법만 알아두면 유용하게 쓰일 수 있을 것 같다.