다른 언어에서 Go 함수 호출하기 [번역]

티스토리 메뉴 펼치기 댓글수0

Go

다른 언어에서 Go 함수 호출하기 [번역]

[前草] 이승현 (wowlsh93@gmail.com)
댓글수0


현재 파이썬으로 짜여져 있는 IoT 데이터중계/분석 프로그램에서 일부분을 C 로 짜려고 하다가 C,C++ 보다는 Go 가 모든 면에서 좋다고 판단하여 관련 자료를 찾다가 이하 글을 번역하였습니다.

마이크로서비스를 넘어 서버리스 아키텍처가 유행하는 요즘,  보다 작은 모듈단위로 강력한 힘을 가질 수 있는 golang 은 구글의 막강한 지원을 등에 업고 큰 힘을 발휘하고 있는거 같습니다. 실제 언어 순위를 매기는 각종 지표에서도 Go 는 파죽지세로 위로 솟구쳐 올라가고 있습니다. 개인적으로는 쓸데없이 복잡하다고 느끼는 소위  객체지향 언어들에 대한 염증(사실 OOP디자인을 사용하는것도 때와 시기가 있는데..무조건 적용하려고 하면.. 부작용이 생기겠지요) 과 함께 스크립트 언어들이 떴는데, 그 스크립트 언어에 없는 강력함을 갖춘 언어가 Go 라서 그런거 같습니다. 즉 심플함 + 강력함 + 구글의지원 (미래보장) 





다른언어로 부터의 Go 함수 호출 

글쓴이는 Packt Publishing의 Learning Go Programming 의 저자이다.)

버전 1.5부터 Go 컴파일러는 -buildmode 플래그를 통해 여러 빌드 모드에 대한 지원을 도입 했습니다. Go 실행 모드라고 알려진 이러한 빌드 모드는 go tool를 확장하여 Go 패키지를 Go 아카이브, Go 공유 라이브러리, C 아카이브, C 공유 라이브러리 및 (introduced in 1.8) Go 동적 플러그인을 비롯한 여러 형식으로 컴파일 합니다.

이 게시물은 Go 패키지를 C 공유 라이브러리로 컴파일하는 것에 관한 내용입니다. 이 빌드 모드에서 컴파일러는 표준 공유 객체 바이너리 파일 (.so 역주: 윈도우즈에서 dll)을 출력하여 Go 함수를 C 스타일 API로 노출하게 됩니다. 여기에서는 C, Python, Node 및 Java에서 호출 할 수 있는 Go 라이브러리를 작성하는 방법에 대해 설명 할 예정입니다.

모든 소스는 올려두었습니다:  GitHub.

Go 코드

다른 언어에서 사용 할 수 있는 멋진 Go 라이브러리를 작성했다고 가정 보겠습니다. 
 코드를 공유 라이브러리로 컴파일하기 전에 수행해야 할 4 가지 요구 사항이 있는데요.

  • 패키지는 반드시 main 패키지 여야 합니다. 컴파일러는 패키지와 모든 종속성을 단일 공유 객체 바이너리로 빌드합니다.
  • 소스는 pseudo-package  "C"를 import 해야 합니다.
  • // export 주석을 사용하여 다른 언어에서 액세스 할 수 있도록 하려는 함수에 주석을 답니다.
  • 빈 main 함수를 선언해야합니다.

다음 Go 소스는 Add, Cosine, Sort 및 Log 함수를 export 할 것인데요  나름 괜찮다고 보지만 여러분을 만족시킬 만큼 멋진 패키지는 아닐 수도 있겠지만 다양한 함수 시그니처는 타입 매핑 관련 사항을 탐색하는 데 도움이 될 것입니다


package main
import "C"
import (
 "fmt"
 "math"
 "sort"
 "sync"
)
var count int
var mtx sync.Mutex

//export Add
func Add(a, b int) int { return a + b }

//export Cosine
func Cosine(x float64) float64 { return math.Cos(x) }

//export Sort
func Sort(vals []int) { sort.Ints(vals) }

//export Log
func Log(msg string) int {
  mtx.Lock()
  defer mtx.Unlock()
  fmt.Println(msg)
  count++
  return count
}

func main() {}

패키지는 -buildmode = c-shared 빌드 플래그를 사용해 컴파일되어 공유 객체 바이너리로 만들어 집니다.

go build -o awesome.so -buildmode=c-shared awesome.go

완료되면 컴파일러는 awesome.h 이라는 C 헤더 파일 및 awesome.so라는 공유 객체 파일을 출력합니다.

-rw-rw-r —    1362 Feb 11 07:59 awesome.h
-rw-rw-r — 1997880 Feb 11 07:59 awesome.so

.so 파일은 약 2MB 크기로 작은 라이브러리에 비해 상대적으로 큽니다. 이것은 Go 실행 엔진의 장점과 종속 패키지가 하나의 공유 객체 바이너리로 묶여 있기 때문이에요. (하나의 정적 실행 파일을 컴파일하는 것과 비슷 함).

헤더파일


헤더 파일은 Go 호환 타입에 매핑 된 C 타입을 정의합니다 (여기서는 설명하지 않음).

/* Created by “go tool cgo” — DO NOT EDIT. */
...
typedef long long GoInt64;
typedef GoInt64 GoInt;
...
typedef struct { const char *p; GoInt n; } GoString;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
...
extern GoInt Add(GoInt p0, GoInt p1);
extern GoFloat64 Cosine(GoFloat64 p0);
...

공유 오브젝트 파일 


컴파일러에 의해 생성된 다른 파일은 64 비트 ELF 공유 오브젝트 바이너리 파일입니다. file command를 사용하여 정보를 확인할 수 있습니다.

$> file awesome.so
awesome.so: ELF 64-bit LSB shared object, x86–64, version 1 (SYSV), dynamically linked, BuildID[sha1]=1fcf29a2779a335371f17219fffbdc47b2ed378a, not stripped

nm 및 grep 명령을 사용하여 Go 함수가 exported 되었는지 확인할 수 있습니다.

$> nm awesome.so | grep -e "T Add" -e "T Cosine" -e "T Sort" -e "T Log"
00000000000d0db0 T Add
00000000000d0e30 T Cosine
00000000000d0f30 T Log
00000000000d0eb0 T Sort

다음으로는 다른 언어에서 Go 로 만들어서 exported 한 함수를 호출하는 방법에 대해 보여주는 몇 가지 예제를 살펴 보겠습니다.

C 에서 호출하기 


공유 객체 라이브러리를 사용하여 C에서 Go 함수를 호출하는 두 가지 방법이 있습니다. 먼저 컴파일 중에 코드를 정적으로 공유 라이브러리에 바인딩 할 수 있지만 런타임에 동적으로 링크합니다. 또는 Go 함수 심볼을 런타임에 동적으로 로드하고 바인딩 할 수 있습니다.

동적 링크

이 접근법에서는 헤더 파일을 사용하여 공유 객체 파일에서 내 보낸 타입 및 함수를 정적으로 참조합니다. 코드는 아래와 같이 간단하고 깔끔 합니다 (일부 출력문은 생략).


#include <stdio.h>
#include "awesome.h"
int main() {
    GoInt a = 12;
    GoInt b = 99;
    printf("awesome.Add(12,99) = %d\n", Add(a, b));
    printf("awesome.Cosine(1) = %f\n", (float)(Cosine(1.0)));
    GoInt data[6] = {77, 12, 5, 99, 28, 23};
    GoSlice nums = {data, 6, 6};
    Sort(nums);
    for (int i = 0; i < 6; i++){
        printf("%d,", ((GoInt *)nums.data)[i]);
    }
    GoString msg = {"Hello from C!", 13};
    Log(msg);
}

다음 코드는 공유 객체 라이브러리를 지정하여 컴파일됩니다.

$> gcc -o client client1.c ./awesome.so

실행되면, 바이너리는 awesome.so 라이브러리에 링크되어 아래 출력을 생성합니다.

$> ./client
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(77,12,5,99,28,23): 5,12,23,28,77,99,
Hello from C!

동적 로드

이 방법에서 C 코드는 동적 링크 로더 라이브러리 (libdl.so)를 사용하여 내 보낸 심볼을 동적으로 로드하고 바인딩합니다. dlopen과 같이 dhfcn.h에 정의 된 함수를 사용하여 라이브러리 파일을 열고 dlsym을 사용하여 심볼을 찾거나 dlerror를 사용하여 오류를 검색하거나 dlclose를 사용하여 공유 라이브러리 파일을 닫습니다.

아래에서 강조한 바와 같이 C 버전은 더 길지만 이전과 똑같습니다

#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
typedef long long go_int;
typedef double go_float64;
typedef struct{void *arr; go_int len; go_int cap} go_slice;
typedef struct{const char *p; go_int len;} go_str;
int main(int argc, char **argv) {
  void *handle;
  char *error;
  handle = dlopen ("./awesome.so", RTLD_LAZY);
  if (!handle) {
    fputs (dlerror(), stderr);
    exit(1);
  }
  go_int (*add)(go_int, go_int) = dlsym(handle, "Add");
  if ((error = dlerror()) != NULL)  {
      fputs(error, stderr);
      exit(1);
  }
  go_int sum = (*add)(12, 99);
  printf("awesome.Add(12, 99) = %d\n", sum);

 go_float64 (*cosine)(go_float64) = dlsym(handle, "Cosine");
 go_float64 cos = (*cosine)(1.0);

 void (*sort)(go_slice) = dlsym(handle, "Sort");
 go_int data[5] = {44,23,7,66,2};
 go_slice nums = {data, 5, 5};
 sort(nums);

 go_int (*log)(go_str) = dlsym(handle, "Log");
 go_str msg = {"Hello from C!", 13};
 log(msg);

 dlclose(handle);
}

이 버전에서 코드는 자체 정의 된 C 타입인 go_int, go_float, go_slice 및 go_str을 사용합니다 (설명을 위해 awesome.h가 사용될 수 있음). 함수 dlsym은 함수 심볼을 로드하고 각각의 함수 포인터에 할당합니다. 다음으로, 코드를 dl 라이브러리 (awesome.so가 아닌)와 링크하여 컴파일 할 수 있습니다.

$> gcc -o client client2.c -ldl

코드가 실행되면 C 바이너리가 로드되어 공유 라이브러리 awesome.so에 연결되어 다음과 같은 결과가 출력됩니다.

$> ./client
awesome.Add(12, 99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(44,23,7,66,2): 2,7,23,44,66,
Hello from C!

Python 에서 호출하기 

파이썬에서는 좀 더 쉬워집니다. ctypes 외부 함수 라이브러리가 export 된 Go 함수를  호출하는 데 사용됩니다.  (일부 출력문은 생략 됨).

from ctypes import *
lib = cdll.LoadLibrary("./awesome.so")
lib.Add.argtypes = [c_longlong, c_longlong]
print "awesome.Add(12,99) = %d" % lib.Add(12,99)
lib.Cosine.argtypes = [c_double]
lib.Cosine.restype = c_double 
cos = lib.Cosine(1)
print "awesome.Cosine(1) = %f" % cos
class GoSlice(Structure):
    _fields_ = [("data", POINTER(c_void_p)), 
                ("len", c_longlong), ("cap", c_longlong)]
nums = GoSlice((c_void_p * 5)(74, 4, 122, 9, 12), 5, 5)
lib.Sort.argtypes = [GoSlice]
lib.Sort.restype = None
lib.Sort(nums)
class GoString(Structure):
    _fields_ = [("p", c_char_p), ("n", c_longlong)]
lib.Log.argtypes = [GoString]
msg = GoString(b"Hello Python!", 13)
lib.Log(msg)

lib 변수는 공유 객체 파일에서 로드된 심볼을 나타냅니다. Python 클래스인 GoString과 GoSlice는 각각의 C struct 타입에 매핑됩니다. 파이썬 코드가 실행되면 공유 객체에서 Go 함수를 호출하여 다음 출력을 생성합니다.

$> python client.py
awesome.Add(12,99) = 111
awesome.Cosine(1) = 0.540302
awesome.Sort(74,4,122,9,12) = [ 4 9 12 74 122 ]
Hello Python!

Node 에서 호출하기 

Node는 node-ffi (및 기타 종속 라이브러리)라는 외부 함수 라이브러리를 사용하여 다음 코드 조각에 강조 표시된대로 export 된 Go 함수를 동적으로 로드하고 호출합니다.

var ref = require("ref");
var ffi = require("ffi");
var Struct = require("ref-struct")
var ArrayType = require("ref-array")
var LongArray = ArrayType(ref.types.longlong);
var GoSlice = Struct({
  data: LongArray,
  len:  "longlong",
  cap: "longlong"
});
var GoString = Struct({
  p: "string",
  n: "longlong"
});
var awesome = ffi.Library("./awesome.so", {
  Add: ["longlong", ["longlong", "longlong"]],
  Cosine: ["double", ["double"]],
  Sort: ["void", [GoSlice]],
  Log: ["longlong", [GoString]]
});
console.log("awesome.Add(12, 99) = ", awesome.Add(12, 99));
console.log("awesome.Cosine(1) = ", awesome.Cosine(1));
nums = LongArray([12,54,0,423,9]);
var slice = new GoSlice();
slice["data"] = nums;
slice["len"] = 5;
slice["cap"] = 5;
awesome.Sort(slice);
str = new GoString();
str["p"] = "Hello Node!";
str["n"] = 11;
awesome.Log(str);

ffi 객체는 공유 라이브러리에서 로드 된 심볼을 관리합니다. Node Sturct 개체는 GoSlice 및 GoString을 만들어 각각의 C 구조체에 매핑하는 데 사용됩니다. 코드를 실행하면 아래와 같이 export 된 Go 함수가 호출 됩니다.

awesome.Add(12, 99) =  111
awesome.Cosine(1) = 0.5403023058681398
awesome.Sort([12,54,9,423,9] = [ 0, 9, 12, 54, 423 ]
Hello Node!

Java 에서 호출하기

Java에서 export 된 Go 함수를 호출하려면 Java Native Access 프로젝트 또는 JNA를 사용하여 다음 코드에 표시된 것처럼 프로그래밍 하십시오 (일부 명령문은 생략되거나 축약 됨).


import com.sun.jna.*;
public class Client {
  public interface Awesome extends Library {
    public class GoSlice extends Structure {
      ...
      public Pointer data;
      public long len;
      public long cap;
    }
  
    public class GoString extends Structure {
      ...
      public String p;
      public long n;
    }
    public long Add(long a, long b);
    public double Cosine(double val);
    public void Sort(GoSlice.ByValue vals);
    public long Log(GoString.ByValue str);
  }
  static public void main(String argv[]) {
    Awesome awesome = (Awesome) Native.loadLibrary(
      "./awesome.so", Awesome.class);
    System.out.printf(... awesome.Add(12, 99));
    System.out.printf(... awesome.Cosine(1.0));
    long[] nums = new long[]{53,11,5,2,88};
    Memory arr = new Memory(... Native.getNativeSize(Long.TYPE));
    Awesome.GoSlice.ByValue slice = new Awesome.GoSlice.ByValue();
    slice.data = arr;
    slice.len = nums.length;
    slice.cap = nums.length;
    awesome.Sort(slice);
    Awesome.GoString.ByValue str = new Awesome.GoString.ByValue();
    str.p = "Hello Java!";
    str.n = str.p.length();
    System.out.printf(... awesome.Log(str));
  }
}

Java 인터페이스 Awesome은 awesome.so 공유 라이브러리 파일에서 로드된 심볼을 나타냅니다. GoSlice 및 GoString 클래스는 각각의 C struct 표현에 매핑됩니다. 코드가 컴파일되고 실행되면 아래와 같이 export 된 Go 함수가 호출됩니다.

$> javac -cp jna.jar Client.java
$> java -cp .:jna.jar Client
awesome.Add(12, 99) = 111
awesome.Cosine(1.0) = 0.5403023058681398
awesome.Sort(53,11,5,2,88) = [2 5 11 53 88 ]
Hello Java!

결론

이 게시물은 다른 언어에서 사용할 수 있는 Go 라이브러리를 작성하는 방법을 보여줍니다. Go 패키지를 C 스타일 공유 라이브러리로 컴파일함으로써 Go 프로그래머는 공유 객체 바이너리의 In-Process Integration을 사용하여 C, Python, Ruby, Node, Java 등과 함께 프로젝트를 쉽게 작업 할 수 있습니다. 따라서 혹시 다음에 Go 를 이용하여 괜찮은 API를 만들게 되면  Go가 아닌 다른 개발자와 공유하는 것을 잊지 마십시오. :-) 



원문링크: https://medium.com/learning-the-go-programming-language/calling-go-functions-from-other-languages-4c7d8bcc69bf


맨위로