Notice
Recent Posts
Recent Comments
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

SYDev

[백엔드를 위한 GO 프로그래밍] Chapter 6. 상호 호환성 본문

Google Developer Groups on KHU 1th

[백엔드를 위한 GO 프로그래밍] Chapter 6. 상호 호환성

시데브 2024. 10. 7. 19:52

6.1. 상호 호환성이 중요한 이유

  • Go code로 미리 작성된 library를 이용 -> 새로 code를 짜지 않고, 해당 기능을 간편하게 사용할 수 있음
  • 그렇다면 Go language로 작성되지 않은 코드는 어떻게 사용할까?

- Types of Programming Languages

Interpreter Language

  • program을 실행할 때, source code를 사전에 machine language로 compile하지 않고 한 줄 한 줄 즉각적으로 실행하는 언어
  • ex) python

Just In Time(JIT) Compile Language

  • ex) Java
  • source code를 Java 바이트코드(cpu native 보다는 high level)로 compile
  • 해당 바이트코드를 실행할 때, JVM(Java Virtual Machine)은 python과 유사한 방식으로 해당 코드를 interpret -> interpreted code가 즉시 native CPU code로 compile될 수 있고, 나중에 함수가 호출될 때를 위해 cache될 수 있기 때문에, 일반적 interpreter language보다 성능이 좋음

Ahead Of Time(AOT) Compile Language

  • ex) Go, C, C++, Switft
  • Program이 실행되기 전에, soruce code를 미리 native CPU code로 compile
  • program이 최적의 성능을 발휘하도록 user code를 최적화 가능
  • 실행 가능한 binary 형태로 생성되는 구조 -> 별도의 software 없이 os에서 실행
  • os는 main 함수(Entry Point)를 호출 -> 이 함수를 시작으로 필요한 모든 codes가 실행되고, main 함수 종료 -> program도 종료
Entry Point: 제어가 os에서 computer program으로 이동하는 것을 의미 
- processor는 program이나 code에 진입해서 execution을 시작
참고자료:
https://ko.wikipedia.org/wiki/%EC%97%94%ED%8A%B8%EB%A6%AC_%ED%8F%AC%EC%9D%B8%ED%8A%B8

 

- Shared Library

Shared Library

  • AOT Compile Language에서, 사용자는 이미 compile된 다른 code를 호출 가능
  • ex) malloc이라는 함수의 정의가 code 내에 없어도 호출 가능 -> 해당 함수를 os의 C library에서 제공, compiler가 생성하는 binary에는 사용자 code가 접근해야 하는 library의 link 정보가 포함되어 있기 때문
  • Shared Library: runtime에 로드되면서 compile time에 존재하기로 약속된 library -> 여러 program이 공유 가능
  • Linux에서는 .so(Shared Object), macos에서는 .dylib(Dynamic Library), windows에서는 .dll(Dynamically Linked Library) 사용 -> 모두 같은 목적의 파일, but 처리 속도는 시스템 별로 차이가 발생

Programming Language 간의 code 공유

  • shared library를 이용하면 programming language 간의 code 공유도 가능
  • AOT compile language는 high level code를 모두 machine language로 변환 -> compiled code를 shared library에 연결하면 서로 다른 programming language 간의 code 공유 가능
  • interpreter, JIT compile language의 경우 방식이 다를 수 있음

제한 사항

  • 모든 AOT compile language가 똑같은 기계어 format을 따르는 것은 아님
  • 각 language가 어떤 특수한 목적을 달성하기 위하여 binary에 고유한 트릭을 추가 -> 이렇게 추가된 코드가 "Runtime"
  • 사용자가 작성한 코드 이외의 코드들이 compiler에 추가된 형태 -> Runtime이 많을수록 다른 language에서 code를 호출하는 것이 복잡해짐

 

- Go에서의 코드 공유

  • Go는 C나 Swift, Rust보다 훨씬 더 큰 Runtime을 가짐 -> Go가 여러 개의 Gorutine을 다루는 데 필요한 모든 업무를 처리해야 하기 때문
  • Go program의 entry point -> Go runtime backend 대부분을 포함하는 연결된 공유 라이브러리에 존재 -> 해당 shared library는 runtime을 초기화하고, 일부 내부적인 설정 및 준비를 마치고 나서 main 함수를 호출 -> program의 종료도 해당 library를 통해 진행
  • Go에서 shared library를 호출하면, 다른 언어와 비교할 때 상대적으로 큰 성능 저하가 발생 
  • 그럼에도, 다른 언어의 코드를 호출하는 것은 매우 유용 -> 다른 언어로 구현하는 것이 유리한 작업을 해당 언어로 작업하고, 그 결과를 Go code에서 접근하고 싶은 경우

 

6.2. C code와 상호 호환하기

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}
  1. #include <stdio.h> -> os에서 "stdio.h" 파일을 가져와서, 해당 내용을 이 파일에 붙여넣을 것
    • stdio.h에는 입력과 출력을 처리하는 함수 선언이 많이 포함됨 
    • 그러나, 이런 헤더 파일에는 호출할 수 있는 함수만 알려주는 역할 -> 실제 동작을 위해서 이 코드를 해당 함수가 있는 library에 연결해야 함
    • 일반적인 binary에는 standard library에 default로 연결되어 있기 때문에, stdio를 쓸 때 연결하지 않아도 됨
  2. printf("Hello World\n") -> printf 함수를 호출하고 "Hello, World!\n" 문자열을 포함하는 캐릭터 버퍼에 대한 포인터를 전달

- Go에서 C 호출하기 

  • Program의 entry point(진입점)가 C가 아닌 Go가 되어야 함 -> C code에서 main 함수를 정의할 수 없다는 의미
  • print만 수행하고, 아무것도 반환하지 않음 -> return type은 void
#inclde <stdio.h>

void printHelloWorld() {
    printf("Hello, World!\n");
}

-> main 함수가 없으므로, C compiler로는 compile or execution 불가 

-> 다음 flag를 사용하여 compile하면 shared library format의 machine language로 컴파일된, 위 코드를 포함한 "libhelloworld.so"라는 파일을 만들 수 있음

-shared -fPIC -o -libhelloworld.so

-> 이 공유 라이브러리는 Go에서 호출 불가능

cgo library

C를 Go에서 호출하기 위해서, cgo라고 하는 별도의 Go library 이용

-> C code와 header를 포함하는 Go code를 compile할 수 있고, compile된 C code와 header를 연결할 수 있어 C 원시 코드를 compile하여 실행 가능 형태의 최종 파일을 Go에서 생성 가능

package main

//#include <stdio.h>
//void printHelloWorld() {
//    printf("Hello, World!\n");
//}
import "C"

func main() {
    C.printHelloWorld()
}
  • 주석 처리된 C code와 함께 "C" 패키지를 import함으로써 cgo 사용

 

- Go에서 C로 변수 전달

  • cgo module에는 C.int(32 bit signed integer), C.unit(32 bit unsigned integer), C.long(64 bit signed integer) 등 다양한 type 포함 -> 가장 변환이 어려운 것이 문자열
    • 문자열은 character라는 독립적 데이터 조각의 배열 -> 기술적으로 하나의 데이터 X -> 문자열의 종료를 나타내는 값인 null terminator를 통해서 문자열의 종료 확인(모든 bit가 OFF(0))
    • character 하나로 생각되는 imoji나 일부 unicode character처럼 1byte로 표현되지 않을 수 있음
  • cgo의 CString function -> Go 문자열을 C 문자열로 쉽게 변환 -> 문자열에 필요한 memory 사본이 만들어지므로, 성능에 민감한 경우 바람직 X
  • C 문자열 사용하는 경우 -> Memory Leak(메모리 누수)이 발생하지 않도록 해야함 -> Go의 Garbage Collector는 사용자가 생성한 pointer를 처리할 수 없음 -> ptr가 가리키는 memory buffer를 해제하는 책임은 사용자에게
CString 함수의 시그니처
func C.CString(string) *C.char
package main

//#include <stdio.h>
//#include <stdlib.h>
//
//void printString(char* str) {
//   printf("%s\n", str);
//}

import "C"
import "unsafe"

func main() {
    a:= C.Cstring("This is from Golang")
    C.printString(a)
    C.free(unsafe.Pointer(a))
}
  • C free function -> type 정보를 포함하지 않는 Generic pointer를 가짐 -> unsafe module을 이용, 먼저 C 문자열과 관련된 type 삭제 후 전달
  • free function을 사용하기 위해서 stdlib.h과 stdio.h를 함께 추가해야 함

 

- C 함수의 반환 값 Go로 받아오기

CFLAGS param

  • CFLAGS parameter -> 주석에 추가한 C code를 원하는 대로 compile하도록 C compiler에 지시할 수 있음
package main

//#include <stdio.h>
//
//char *getName(int idx) {
//    if(idx == 1)
//        return "Tanmay Bakshi";
//    if(idx == 2)
//        return "Baheer Kamal";
//    return "Invalid index";
//}

import "C"
import (
    "fmt"
)

func main() {
    cstr := C.getName(C.int(2))
    fmt.Println(C.GoString(cstr))
}
  • CString을 사용하지 않았기 때문에, 사본 생성 X -> heap에 할당되지 않고, 스택에만 저장 -> character pointer를 해제할 필요 X
  • 주석에 추가한 코드를 원하는 대로 compile하도록 C compiler에 지시할 수 있음
    • ex) 특정 컴파일 flag를 전달하는 경우 CFLAGS parameter 사용 가능 -> ugly number를 구하는 코드
package main

//#cgo CFLAGS: -O0
// #include <stdio.h>
// int numberIsUgly(int x) {
//     while (x > 1) {
//         int y = x;
//     while (y % 2 == 0)
//         y /= 2;
//     while (y % 3 == 0)
//         y /= 3;
//     while (y % 5 == 0)
//         y /= 5;
//     if (x == y)
//         return 0;
//     x = y;
//     }
//     return 1;
// }
// =
// void getNthUglyNumber(int n) {
//     int i = 0;
//     int j = 0;
//     while (j < n) {
//         i++;
//         if (numberIsUgly(i)) {
//             j++;
//         }
//     }
//     printf("%d\n", i);
// }

import "C"

func main() {
    C.getNthUglyNumber(C.int(1000))
}
  • C compiler에 "-O0" flag 전달하도록 cgo에 지시
  • "-O0" flag: 코드를 최적화하지 말고, assembly 변환을 위한 다이렉트 코드를 가능한 한 많이 사용할 것
    • Intel i9 macbook pro에서 수행 시간 1.7s
  • "-Ofast" flag
    • 같은 조건에서 0.4s

Linker

  • 위와 같은 로직으로 linker에 flag 전달 가능
  • Linker: 실행 파일이 코드를 호출해야 하는 shared library를 os에 알리는 역할

 파일명: factorial.c

int factorial(int x) {
    if(x == 1)
        return x;
    return factorial(x - 1) * x;
}

 

clang factorial.c -shared -fPIC -o libcfactorial.dylib

-> compile 명령

 

package main

//#cgo LDFLAGS: -L. -lcfactorial
//int factorial(int);

import "C"
import "fmt"

func main() {
    fmt.Println(C.factorial(C.int(5)))
}
  1. //#cgo LDFLAGS: -L. -lcfactorial 
    • linker에게 "코드가 컴파일되는 directory에서 shared library를 찾고, 특히 cfactorial이라는 library에 대해 링크할 것"이라 전달
    • 원래 파일명은 libcfactorial.dylib -> 링커는 시작 부분의 "lib"과 끝부분의 ".dylib"을 무시
  2. //int factorial(int) -> program 어딘가에 존재하는 함수 factorial을 호출(32비트 signed int 반환, 32비트 signed int 인자)
  • factorial 함수의 실제 machine language code는 이전 단계에서 구축한 library에 이미 compile되어 있음 -> Go code를 compile할 때는 factorial 함수를 compile하지 않음 -> 미리 컴파일된 factorial 함수 버전을 링크하도록 Go에 지시

 

6.3. Switft와 상호 호환하기

  • Go에서는 tail call optimization과 같은 몇몇 최적화를 지원하지 않음
  • 이런 종류의 구현이 C와 같은 언어로 작성될 수 있지만, C가 제공하는 제어 수준 만큼은 필요하지 않을 때 -> Switft 혹은 이와 유사한 언어 사용
  • Switft에서 테트리스를 자동으로 플레이하고, 게이므이 결과를 커맨드 라인에 시각화하는 application 구현
  • Go에는 존재하지 않는 CMA-ES(Convariance Matrix Adaptation Evolutionary Strategy) method를 통해서, 구현
  • CMA 최적화 method는 python에 구현된 상태, 하지만 python은 interpret 언어
  • Switft 표준 라이브러리에 구축된 Python과의 상호 호환 계층을 이용하여 해당 method 이용
@_cdecl("nextBestMoves")
public func nextBestMoves() -> UnsafeMutablePointer<Int> {
    var nextMoves = game.nextBestMoves()!.0
    nextMoves.insert(nextMoves.count, at: 0)
    return nextMoves.withUnsafeBufferPointer { ptrToMoves -> UnsafeMutablePointer<Int> in
        let newMemory = UnsafeMutablePointer<Int>.allocate(capacity: nextMoves.count)
        memcpy(newMemory, nextMoves, nextMoves.count * MemoryLayout<Int>.size)
        return newMemory
    }
}

@_cdecl("playMove")
public func playMove(move: Int) {
    switch move {
    case -1:
        game.swapHold()
    case 0:
        game.attemptSpin(clockwise: true)
    case 2:
        game.attemptSpin(clockwise: false)
    case 4:
        game.horizontalMove(left: false)
    case 5:
        game.horizontalMove(left: true)
    case 6:
        game.down()
    default:
        fatalError()
    }
}
@_cdecl("renderFrame")
public func renderFrame() -> UnsafeMutablePointer<Int> {
    var x = [24, 10] + game.render().reduce([], +)
    x.insert(x.count, at: 0)
    return x.withUnsafeBufferPointer { ptrToMoves -> UnsafeMutablePointer<Int> in
        let newMemory = UnsafeMutablePointer<Int>.allocate(capacity: x.count)
        memcpy(newMemory, x, x.count * MemoryLayout<Int>.size)
        return newMemory
    }
}

@_cdecl("lockGame")
public func lockGame() -> Bool {
    return game.lock()
}

@_cdecl("resetGame")
public func resetGame() {
    game = Tetris(width: 10, height: 24)
}
  • @_cdecl function decorator: 해당 함수가 binary machine language에서 C 함수처럼 보이고 동작해야 한다는 의미 -> 이를 통해서, Go에서 이것을 C 함수인 것처럼 호출할 수 있게 됨
swiftc TWAI.swift -O -emit-library -o libTWAI.so
  • -O: 컴파일러 최적화를 완전히 사용하여 code를 최대한 빠르게 만듦, but 안전하지 않은 기능은 사용 X
  • -emit-library: 바이너리 실행 파일로 빌드X -> 다른 코드가 동적으로 link할 수 있는 라이브러리로 빌드
  • -o libTWAI.so: 출력되는 파일 이름은 "libTWAI.so"
package main

// #cgo LDFLAGS: -L. -lTWAI
// #include <stdbool.h>
// long *nextBestMoves();
// void playMove(long move);
// long *renderFrame();
// void lockGame();
// bool resetGame();
import "C"
import (
	"fmt"
	"os"
	"reflect"
	"time"
	"unsafe"
	"github.com/gdamore/tcell/v2"
)
  • 주석으로 표현되는 C code를 파악할 수 있도록 cgo import와 다른 import가 구분됨
  • // #cgo LDFLAGS: -L. -lTWAI -> linker에게 TWAI라는 library를 링크하라 전달
  • // long *nextBestMoves();
    // void playMove(long move);
    // long *renderFrame();
    // void lockGame();
    // bool resetGame();
    • Go code에서 호출할 shared library에 있는 함수를 선언
  • 이외 Go 코드에서 Swift 코드를 더 쉽게 호출할 수 있도록 도움을 줄 수 있는 몇 가지 함수를 구현하여 최종적으로 swift의 code를 Go에서 이용

참고자료

 

런타임 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 런타임(영어: runtime→실행시간)은 컴퓨터 과학에서 컴퓨터 프로그램이 실행되고 있는 동안의 동작을 말한다. "런타임"이라는 용어는 컴퓨터 언어 안에 쓰인 프

ko.wikipedia.org

 

Interpreter (computing) - Wikipedia

From Wikipedia, the free encyclopedia Program that executes source code without a separate compilation step W3sDesign Interpreter Design Pattern UML In computer science, an interpreter is a computer program that directly executes instructions written in a

en.wikipedia.org

 

Shared library - Wikipedia

From Wikipedia, the free encyclopedia Code library designed for mutual use by multiple programs A shared library or shared object is a computer file that contains executable code designed to be used by multiple computer programs or other libraries at runti

en.wikipedia.org