Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Fiddle: Ruby에서 C 라이브러리를 자유롭게 활용하는 방법 – 가장 잘 알려지지 않은 표준 라이브러리

Fiddle은 Ruby 1.9.x 버전에 표준 라이브러리로 추가된, 아직 많이 알려지지 않은 모듈입니다. 이 모듈을 사용하면 Ruby 코드에서 C 라이브러리와 직접 상호작용할 수 있으며, 심지어 실행 중인 Ruby 인터프리터 자체를 들여다보고 수정하는 것도 가능합니다.

Fiddle은 널리 쓰이는 C 라이브러리인 libffi를 감싸는 방식으로 동작합니다. libffi는 한 언어로 작성된 코드가 다른 언어로 작성된 함수를 호출할 수 있게 해주는 라이브러리로, "ffi"는 "foreign function interface(외부 함수 인터페이스)"의 약자입니다. 흥미로운 점은 C에만 국한되지 않는다는 것입니다. Fiddle 사용법만 익히면 Rust 등 FFI를 지원하는 다른 언어로 작성된 라이브러리도 활용할 수 있습니다.

이번 글에서는 Fiddle을 하나씩 살펴보겠습니다. 간단한 예제로 시작해서, 마지막에는 시리얼 포트를 통해 Arduino에 접근하는 것까지 다뤄볼 겁니다. C를 깊이 알아야 할 것 같지만 걱정하지 않으셔도 됩니다. 그렇게 많이 필요하지 않으니까요. :)

간단한 예제

일반적인 Ruby에서는 메서드를 정의하기 전에는 호출할 수 없습니다. Fiddle도 마찬가지입니다. C 함수를 바로 호출할 수는 없고, 먼저 해당 C 함수의 래퍼(wrapper)를 만든 뒤 그 래퍼를 호출해야 합니다.

아래 예제에서는 C의 로그(logarithm) 함수를 감싸고 있습니다. 사실상 Ruby의 Math.log를 복제하는 셈이죠.

require 'fiddle'

# We're going to "open" a library, so we have to tell Fiddle where
# it's located on disk. This example works on OSX Yosemite.
libm = Fiddle.dlopen('/usr/lib/libSystem.dylib')


# Create a wrapper for the c function "log". 
log = Fiddle::Function.new(
  libm['log'],            # Get the function from the math library we opened
  [Fiddle::TYPE_DOUBLE],  # It has one argument, a double, which is similar to ruby's Float
  Fiddle::TYPE_DOUBLE     # It returns a double
)

# call the c function via our wrapper
puts log.call(3.14159)

더 깔끔하게 만들기

앞선 예제도 동작은 하지만 다소 장황합니다. 100개나 되는 함수를 일일이 감싸야 한다면 코드가 얼마나 지저분해질지 상상이 되시죠? 그래서 Fiddle은 편리한 DSL을 제공합니다. Fiddle::Importer 믹스인(mixin)을 통해 노출되는 DSL입니다.

이 믹스인을 사용하면 외부 함수들로 가득한 모듈을 손쉽게 만들 수 있습니다. 아래 예제에서는 여러 로그 관련 메서드를 담은 모듈을 만들어 보겠습니다.

require 'fiddle'
require 'fiddle/import'

module Logs
  extend Fiddle::Importer
  dlload '/usr/lib/libSystem.dylib'
  extern 'double log(double)'
  extern 'double log10(double)'
  extern 'double log2(double)'
end

# We can call the external functions as if they were ruby methods!
puts Logs.log(10)   # 2.302585092994046
puts Logs.log10(10) # 1.0
puts Logs.log2(10)  # 3.321928094887362

시리얼 포트 제어하기

여러 해 동안 사고 싶었던 Arduino를 드디어 구입했다고 가정해 봅시다. 보드는 시리얼 포트를 통해 매초 컴퓨터로 "hello world"라는 텍스트를 전송하고 있습니다.

이 데이터를 Ruby로 읽어올 수 있다면 참 좋겠죠? 실제로 Ruby의 표준 IO 메서드만으로도 시리얼 포트에서 읽는 것이 가능합니다. 하지만 이런 IO 메서드들은 진지한 하드웨어 해커에게 필요한 세밀한 수준(granularity)으로 시리얼 입력을 설정할 수 없습니다.

다행히 C 표준 라이브러리는 시리얼 포트를 다루는 데 필요한 모든 함수를 제공합니다. 그리고 Fiddle을 사용하면 이 C 함수들을 Ruby에서 그대로 접근할 수 있습니다.

물론 이미 이런 작업을 대신 처리해 주는 gem들이 존재합니다. 필자도 이 코드를 작성할 때 rubyserial gem에서 영감을 얻었습니다. 하지만 우리의 목표는 이런 작업을 스스로 하는 법을 배우는 것입니다.

termios.h 살펴보기

C에서 파일명이 .h로 끝나는 파일은 헤더 파일입니다. 헤더 파일에는 해당 라이브러리가 서드파티 코드(즉, 우리 코드)에 공개하는 모든 함수와 상수 목록이 담겨 있습니다. 어떤 C 라이브러리를 감싸든 첫 번째 단계는 이 헤더 파일을 찾아 열고 내용을 살펴보는 것입니다.

우리가 사용할 라이브러리는 termios입니다. macOS Yosemite 기준으로 헤더 파일은 /usr/include/sys/termios.h에 위치하며, 리눅스에서는 다른 경로에 있습니다.

다음은 termios.h의 모습입니다. 설명을 위해 상당 부분 축약했습니다.

typedef unsigned long   tcflag_t;
typedef unsigned char   cc_t;
typedef unsigned long   speed_t;

struct termios {
    tcflag_t    c_iflag;    /* input flags */
    tcflag_t    c_oflag;    /* output flags */
    tcflag_t    c_cflag;    /* control flags */
    tcflag_t    c_lflag;    /* local flags */
    cc_t        c_cc[NCCS]; /* control chars */
    speed_t     c_ispeed;   /* input speed */
    speed_t     c_ospeed;   /* output speed */
};


int tcgetattr(int, struct termios *);
int tcsetattr(int, int, const struct termios *);
int tcflush(int, int);

이 코드에서 주목해야 할 중요한 포인트는 세 가지입니다. 첫째, 몇 가지 typedef가 있다는 점. 둘째, 연결 설정 정보를 담는 데 사용되는 데이터 구조체가 있다는 점. 셋째, 우리가 사용할 메서드들이 있다는 점입니다.

Fiddle의 importer를 사용하면 이 섹션들을 거의 그대로 Ruby 코드로 옮길 수 있습니다. 하나씩 차근차근 살펴보겠습니다.

타입 별칭과 typedef

C에서는 데이터 타입에 별칭(alias)을 붙이는 일이 흔합니다. 예를 들어 termios.h는 단순히 long 정수인 speed_t라는 새 타입을 만듭니다. Fiddle importer는 typealias 기능을 통해 이런 typedef를 처리할 수 있습니다.

# equivalent to `typedef unsigned long tcflag_t;`
typealias "tcflag_t", "unsigned long"

구조체(Structs)

C에는 클래스나 모듈이 없습니다. 여러 변수를 하나로 묶고 싶다면 struct(구조체)를 사용해야 합니다. Fiddle은 Ruby에서 구조체를 다룰 수 있는 좋은 메커니즘을 제공합니다.

첫 번째 단계는 Fiddle importer 안에 구조체를 정의하는 것입니다. 보시다시피 헤더 파일에서 거의 복사해서 붙여넣은 수준입니다.

Termios = struct [
  'tcflag_t c_iflag',
  'tcflag_t c_oflag',
  'tcflag_t c_cflag',
  'tcflag_t c_lflag',
  'cc_t         c_cc[20]',
  'speed_t  c_ispeed',
  'speed_t  c_ospeed',
]

이제 malloc 메서드를 사용해 구조체의 "인스턴스"를 생성할 수 있습니다. 값의 설정과 조회는 일반적인 Ruby 클래스 인스턴스를 다루는 방식과 거의 같습니다.

s = Termios.malloc
s.c_iflag = 12345

모두 합치기

방금 배운 typedef와 구조체 지식을 앞서 살펴본 함수 래핑 기법과 결합하면, 실제로 동작하는 termios 라이브러리 래퍼를 완성할 수 있습니다.

래퍼에는 시리얼 포트 설정에 필요한 메서드만 담습니다. 나머지는 평범한 Ruby 코드로 처리합니다.

require 'fiddle'
require 'fiddle/import'

# Everything in this module was pretty much copied directly from 
# termios.h. In Yosemite it's at /usr/include/sys/termios.h
module Serial
  extend Fiddle::Importer
  dlload '/usr/lib/libSystem.dylib'

  # Type definitions
  typealias "tcflag_t", "unsigned long"
  typealias "speed_t", "unsigned long"
  typealias "cc_t", "char"

  # A structure which will hold configuratin data. Instantiate like
  # so: `Serial::Termios.malloc()`
  Termios = struct [
    'tcflag_t   c_iflag',
    'tcflag_t   c_oflag',
    'tcflag_t   c_cflag',
    'tcflag_t   c_lflag',
    'cc_t         c_cc[20]',
    'speed_t    c_ispeed',
    'speed_t    c_ospeed',
  ]

  # Functions for working with a serial device
  extern 'int   tcgetattr(int, struct termios*)'       # get the config for a serial device
  extern 'int   tcsetattr(int, int, struct termios*)'  # set the config for a serial device
  extern 'int   tcflush(int, int)'                     # flush all buffers in the device

end

라이브러리 사용하기

아래 예제에서는 Ruby로 시리얼 디바이스를 열고 파일 디스크립터(file descriptor)를 얻습니다. 그런 다음 termios 함수들로 읽기용으로 디바이스를 설정하고, 데이터를 읽습니다.

여기서는 몇 가지 '매직 넘버(magic number)'를 사용했습니다. 이 값들은 Arduino와 통신하기 위해 필요한 여러 비트 단위 설정 옵션들을 조합한 결과입니다. 매직 넘버의 유래가 궁금하다면 관련 블로그 글을 참고하세요.

file = open("/dev/cu.wchusbserial1450", 'r')
fd = file.to_i

# Create a new instance of our config structure
config = Serial::Termios.malloc

# Load the default config options into our struct
Serial.tcgetattr(fd, config);

# Set config options important to the arduino. 
# I'm sorry for the magic numbers. 
config.c_ispeed = 9600;
config.c_ospeed = 9600;
config.c_cflag = 51968;
config.c_iflag = 0;
config.c_oflag = 0;

# wait for 12 characters to come in before read returns.
config.c_cc[17] = 12;
# no minimum time to wait before read returns 
config.c_cc[16] = 0;

# Save our configuration
Serial.tcsetattr(fd, 0, config);

# Wait 1 second for the arduino to reboot
sleep(1)

# Remove any existing serial input waiting to be read
Serial.tcflush(fd, 1)

buffer = file.read(12)
puts "#{ buffer.size } bytes: #{ buffer }"

file.close

실제 동작하는 시리얼 리더

이제 Arduino에 "hello world"를 계속 출력하는 프로그램을 올리고, Ruby 스크립트로 그 출력을 읽어보겠습니다.

Fiddle: Ruby에서 C 라이브러리를 자유롭게 활용하는 방법 – 가장 잘 알려지지 않은 표준 라이브러리 이 Arduino 프로그램은 시리얼로 "hello world"를 계속해서 출력합니다.

그리고 시리얼 모니터를 실행했을 때의 모습입니다.

Fiddle: Ruby에서 C 라이브러리를 자유롭게 활용하는 방법 – 가장 잘 알려지지 않은 표준 라이브러리