ffi-napi를 사용해 go 라이브러리 불러오기

조나단 (Seagull)·2023년 3월 20일
0

nodejs

목록 보기
1/1

이번 프로젝트에 go로 작성한 순위 계산 코드를 nodejs 백엔드 서버에 붙이는 일을 맡아서 새롭게 공부해보게 되었다.

이전에 cpp를 사용해 napi bindings을 만들어봤을 때와 비교하자면 생각보다 go로 만드는 것이 쉽고 간편해서 최적화가 매우 많이 필요한 것이 아니면 사용해보는 것도 좋을 것 같아 소개해보려한다.

Go

package main

import (
	"C"
    "os"
)

//export OpenFile
func OpenFile(filepathArg *C.char) {
	_ = os.Setenv("GODEBUG", "cgocheck=0")
	filepath := C.GoString(filepathArg)
        
        //your  code
}

func main() {}

라이브러리 빌드를 위한 코드는 위가 전부이다.

cpp로 빌드할 때는 라이브러리 내부 헤더와 외부 헤더 전부 세팅해줘야 했던 것과는 다르게 go 내부 코드가 굉장히 간결하다.

주의 깊게 살펴봐야할 코드는 다음과 같다.

//export OpenFile

위 주석은 뒤에 오는 함수명을 인식해서 자동으로 헤더를 뱉어준다. 직접 인터페이스를 만들어줘야 했던 cpp에 비해서 너무나도 간편한 세팅인 것 같다.

func OpenFile(filepathArg *C.char) {
	filepath :=  C.GoString(filepathArg)
...

nodejs에서 go 파라미터를 인식하는 것은 불가능하기 때문에 우리는 중간에 C를 매개체로 두어야한다.

본 함수는 파일명을 받아서 파일을 여는 기능을 하기 때문에 filepath를 char 포인터로 받고 이걸 GoString으로 변환하는 과정이다.

fatal error: non-Go code disabled sigaltstack

그리고 필자는 다음과 같은 에러가 nodejs에서 라이브러리를 불러올 때 발생했는데 gpt에게 물어본 결과

_ = os.Setenv("GODEBUG", "cgocheck=0")

위 코드를 사용하여 해결을 했다.

Build, C code

빌드는 다음과 같은 명령어로 실행한다.

GOARCH=arm64 CGO_ENABLED=1 go build -o ./lib/ric.dylib -buildmode=c-shared $(CMD_PROJECT)

필자는 m1 mac을 사용해주기 때문에 GOARCH로 arm64를 넣어주었고, dylib으로 빌드시켰다. 본인의 arch와 os에 따라 수정해서 빌드하면 되겠다.

빌드를 하면 두 개의 파일이 생성되는데 하나는 ric.dylib과 nodejs에서 라이브러리 접근을 위한 헤더 파일을 생성해준다.

/* Code generated by cmd/cgo; DO NOT EDIT. */

/* package raceInfoCalculator/cmd/raceInfoCalculator */


#line 1 "cgo-builtin-export-prolog"

#include <stddef.h>

#ifndef GO_CGO_EXPORT_PROLOGUE_H
#define GO_CGO_EXPORT_PROLOGUE_H

#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef struct { const char *p; ptrdiff_t n; } _GoString_;
#endif

#endif

/* Start of preamble from import "C" comments.  */




/* End of preamble from import "C" comments.  */


/* Start of boilerplate cgo prologue.  */
#line 1 "cgo-gcc-export-header-prolog"

#ifndef GO_CGO_PROLOGUE_H
#define GO_CGO_PROLOGUE_H

typedef signed char GoInt8;
typedef unsigned char GoUint8;
typedef short GoInt16;
typedef unsigned short GoUint16;
typedef int GoInt32;
typedef unsigned int GoUint32;
typedef long long GoInt64;
typedef unsigned long long GoUint64;
typedef GoInt64 GoInt;
typedef GoUint64 GoUint;
typedef size_t GoUintptr;
typedef float GoFloat32;
typedef double GoFloat64;
#ifdef _MSC_VER
#include <complex.h>
typedef _Fcomplex GoComplex64;
typedef _Dcomplex GoComplex128;
#else
typedef float _Complex GoComplex64;
typedef double _Complex GoComplex128;
#endif

/*
  static assertion to make sure the file is being used on architecture
  at least with matching size of GoInt.
*/
typedef char _check_for_64_bit_pointer_matching_GoInt[sizeof(void*)==64/8 ? 1:-1];

#ifndef GO_CGO_GOSTRING_TYPEDEF
typedef _GoString_ GoString;
#endif
typedef void *GoMap;
typedef void *GoChan;
typedef struct { void *t; void *v; } GoInterface;
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

#endif

/* End of boilerplate cgo prologue.  */

#ifdef __cplusplus
extern "C" {
#endif

extern void OpenFile(char* filepathArg);

#ifdef __cplusplus
}
#endif

전체적으로 C로 받은 파라미터와 리턴타입을 go의 것들로 넘겨주는 코드인 것 같은데, C의 char 포인터가 param이고 return type이 void라 굳이 저렇게 길 필요가 있나 싶긴하다.

Node.js

import ffi from 'ffi-napi';
import path from 'path';

const __dirname = path.resolve();

const mylib = ffi.Library('./lib/ric.dylib', {
    'OpenFile': ['void', ['string']]
});

mylib.OpenFile(path.join(__dirname, "out.txt"))

ffi-napi 모듈을 통해 라이브러리를 불러와 OpenFile 인터페이스를 구성해주고, 마지막으로 불러와주면 끝이다. 이 부분은 cpp를 사용한 napi 구성과 별 다를 것 없는 것 같다.

nodejs에서 go로 작성된 코드를 불러와볼 생각은 해본 적이 없었는데, 생각보다 방법이 간편하고 쉬워 다음에 계산 모듈이 필요할 때 한번씩 사용해 볼 것 같다. 참고로 해당 프로젝트는 같은 기능을 하는 nodejs와 go의 코드가 20배 정도 성능 차이가 났다. 두 언어를 사용할 수 있는 개발자라면 한번씩 사용해보는 것도 좋을 것 같다.

profile
백엔드 개발자

1개의 댓글

comment-user-thumbnail
2023년 4월 7일

안녕하세요 저도 비슷한 프로젝트를 하고 있는데, ffi-napi 사용이 잘 안되더라구요! 혹시 go 와 node version 알 수 있을까요?

답글 달기