Ruby 확장 라이브러리 만들기

Ruby 확장 라이브러리 만들기

Ruby 확장 라이브러리를 C로 작성하는 방법을 설명하는 문서예요. Ruby 자체가 C로 작성되어 있어서, Ruby에서 할 수 있는 일은 원리상 C에서도 할 수 있어요. 이 글은 그 경계에서 Ruby의 데이터와 기능을 C 코드에서 다루는 방법을 하나씩 짚어 주는 가이드예요.

출처: Ruby 4.0 API — Rubyの拡張ライブラリの作り方

1. 기초 지식

C의 변수에는 타입이 있고 데이터에는 타입이 없어요. 그래서 예를 들어 포인터를 int 변수에 넣으면 그 값은 정수로 취급돼요. 반대로 Ruby의 변수에는 타입이 없고 데이터에 타입이 있어요. 이 차이 때문에 C와 Ruby는 서로 변환하지 않으면 상대의 데이터에 접근할 수 없어요.

Ruby의 데이터는 VALUE라는 C 타입으로 표현돼요. VALUE 타입의 데이터는 자기 자신의 데이터 타입을 스스로 알고 있어요. 이 "데이터 타입"은 데이터(객체)의 실제 구조를 뜻하며, Ruby의 클래스와는 별개의 개념이에요.

VALUE에서 C가 의미 있게 쓰는 데이터를 꺼내려면 두 가지가 모두 필요해요.

  • VALUE의 데이터 타입을 안다.
  • VALUE를 C의 데이터로 변환한다.

첫 번째를 깜빡하면 잘못된 데이터 변환이 일어나서, 최악의 경우 프로그램이 core dump를 일으켜요.

데이터 타입

Ruby에는 사용자가 쓸 가능성이 있는 다음 타입들이 있어요.

  • T_NILnil
  • T_OBJECT — 일반 객체
  • T_CLASS — 클래스
  • T_MODULE — 모듈
  • T_FLOAT — 부동소수점
  • T_STRING — 문자열
  • T_REGEXP — 정규식
  • T_ARRAY — 배열
  • T_HASH — 연관 배열(해시)
  • T_STRUCT — (Ruby의) 구조체
  • T_BIGNUM — 다배정밀도 정수
  • T_FIXNUM — Fixnum(31비트 또는 63비트 정수)
  • T_COMPLEX — 복소수
  • T_RATIONAL — 유리수
  • T_FILE — 입출력
  • T_TRUE — 참
  • T_FALSE — 거짓
  • T_DATA — 데이터
  • T_SYMBOL — 심볼

그 외에도 내부에서만 쓰이는 다음 타입들이 있어요.

T_ICLASS
T_MATCH
T_UNDEF
T_NODE
T_ZOMBIE

대부분의 타입은 C 구조체로 구현되어 있어요.

VALUE의 데이터 타입 확인하기

ruby.h에는 TYPE() 매크로가 정의되어 있어서, VALUE의 데이터 타입을 알 수 있어요. TYPE()는 위에서 소개한 T_XXXX 형태의 상수를 반환해요. VALUE의 데이터 타입에 따라 처리를 나눌 때는 TYPE()의 값으로 분기하게 돼요.

switch (TYPE(obj)) {
  case T_FIXNUM:
    /* FIXNUM 처리 */
    break;
  case T_STRING:
    /* 문자열 처리 */
    break;
  case T_ARRAY:
    /* 배열 처리 */
    break;
  default:
    /* 예외 발생 */
    rb_raise(rb_eTypeError, "not valid value");
    break;
}

그리고 데이터 타입을 확인해서 올바르지 않으면 예외를 일으키는 함수가 준비되어 있어요.

void Check_Type(VALUE value, int type)

이 함수는 valuetype이 아니면 예외를 발생시켜요. 인자로 주어진 VALUE의 데이터 타입이 올바른지 확인하려면 이 함수를 쓰면 돼요.

FIXNUMNIL은 더 빠른 판별 매크로가 준비되어 있어요.

FIXNUM_P(obj)
NIL_P(obj)

VALUE를 C의 데이터로 변환하기

데이터 타입이 T_NIL, T_FALSE, T_TRUE이면 데이터는 각각 nil, false, true예요. 이 타입의 객체는 각각 하나씩만 존재해요.

데이터 타입이 T_FIXNUM이면 31비트 또는 63비트 크기의 정수예요. long 크기가 32비트인 플랫폼이면 31비트, long 크기가 64비트인 플랫폼이면 63비트가 돼요. FIXNUM을 C의 정수로 변환하려면 매크로 FIX2INT() 또는 FIX2LONG()을 사용해요. 이 매크로들을 쓰려면 사전에 데이터 타입이 FIXNUM인지 확인해야 하지만, 비교적 빠른 변환이 가능해요. 그리고 FIX2LONG()은 예외를 발생시키지 않지만, FIX2INT()는 변환 결과가 int 크기에 안 들어가면 예외를 발생시켜요.

또한 FIXNUM에 국한하지 않고 Ruby 데이터를 정수로 변환하는 NUM2INT()NUM2LONG()도 있어요. 이 매크로들은 타입 확인 없이 쓸 수 있어요(정수로 변환할 수 없으면 예외 발생). 마찬가지로 확인 없이 쓸 수 있는 변환 매크로로 double을 꺼내는 NUM2DBL()이 있어요.

char*를 꺼낼 때는 StringValue()StringValuePtr()을 사용해요.

  • StringValue(var)varString이면 아무것도 하지 않고, 아니면 varvar.to_str()의 결과로 바꾸는 매크로예요.
  • StringValuePtr(var) — 마찬가지로 varString으로 바꾼 뒤, var의 바이트열 표현에 대한 char*를 반환하는 매크로예요.

var의 내용을 직접 바꾸는 처리가 들어가므로 varlvalue여야 해요. StringValuePtr()과 비슷한 StringValueCStr()도 있어요. StringValueCStr(var)varString으로 바꾼 뒤 var의 문자열 표현에 대한 char*를 반환해요. 반환되는 문자열 끝에는 NUL 문자가 붙어요. 다만 중간에 NUL 문자가 포함되어 있으면 ArgumentError가 발생해요. 반면 StringValuePtr()은 끝에 NUL 문자가 있다는 보장이 없고, 중간에 NUL이 포함될 수도 있어요.

그 밖의 데이터 타입은 대응하는 C 구조체가 있어요. 구조체가 있는 VALUE는 그대로 캐스팅(형 변환)하면 구조체 포인터로 바꿀 수 있어요.

구조체는 ruby.hstruct RXxxxx라는 이름으로 정의되어 있어요. 예를 들어 문자열은 struct RString이에요. 실제로 쓸 일이 있는 건 문자열과 배열 정도일 거예요.

ruby.h에는 구조체로 캐스팅하는 매크로도 RXXXXX()(전부 대문자로 바꾼 이름)로 제공돼요(예: RSTRING()). 하지만 구조체에 직접 접근하는 건 최대한 피하고, 대응하는 rb_xxxx() 같은 함수를 사용하세요. 예를 들어 배열 요소에 접근할 때는 rb_ary_entry(ary, offset), rb_ary_store(ary, offset, obj)를 사용하세요.

구조체에서 데이터를 꺼내는 매크로도 제공돼요. 문자열 str의 길이를 얻으려면 RSTRING_LEN(str), char*로 얻으려면 RSTRING_PTR(str)을 써요.

Ruby 구조체에 직접 접근할 때 주의할 점이 하나 있어요. 배열·문자열 구조체의 내용은 참조만 하고 직접 변경하지 않는 것이에요. 직접 바꾸면 객체 내용의 정합성이 깨져서 예상치 못한 버그의 원인이 돼요.

C의 데이터를 VALUE로 변환하기

VALUE의 실제 구조는 다음과 같아요.

  • FIXNUM의 경우: 1비트 왼쪽 시프트 후 LSB(최하위 비트)를 세운다.
  • 그 외 포인터의 경우: 그대로 VALUE로 캐스팅한다.

즉 LSB를 확인하면 VALUEFIXNUM인지 알 수 있어요(포인터의 LSB가 세워져 있지 않다는 걸 가정). 그래서 FIXNUM이 아닌 Ruby 객체의 구조체는 단순히 VALUE로 캐스팅하면 변환할 수 있어요. 다만 임의의 구조체가 캐스팅될 수 있는 건 아니고, Ruby가 아는 구조체(ruby.h에 정의된 struct RXxxx 계열)만 캐스팅할 수 있어요.

FIXNUM은 변환 매크로를 경유해야 해요. C의 정수에서 VALUE로 변환하는 매크로는 다음과 같아요.

  • INT2FIX() — 원래의 정수가 31비트/63비트 안에 들어간다는 확신이 있을 때
  • INT2NUM() — 임의의 정수에서 VALUE

INT2NUM()은 정수가 FIXNUM 범위에 안 들어가면 Bignum으로 변환해 줘요(단, 조금 느려요).

Ruby 데이터를 조작하기

앞서 말했듯 Ruby의 구조체에 접근할 때 내용을 갱신하는 건 권장하지 않아요. Ruby 데이터를 조작할 때는 Ruby가 준비한 함수를 사용하세요.

여기서는 가장 자주 쓰일 문자열과 배열의 생성/조작 함수를 소개할게요(전부는 아니에요).

문자열 함수

  • rb_str_new(const char *ptr, long len) — 새 Ruby 문자열 생성
  • rb_str_new2(const char *ptr) / rb_str_new_cstr(const char *ptr) — C 문자열에서 Ruby 문자열 생성. 기능은 rb_str_new(ptr, strlen(ptr))과 동일
  • rb_str_new_literal(const char *ptr) — C 리터럴 문자열에서 Ruby 문자열 생성
  • rb_str_append(VALUE str1, VALUE str2) — Ruby 문자열 str1str2 추가
  • rb_sprintf(const char *format, …) / rb_vsprintf(const char *format, va_list ap) — C 문자열 format과 이어지는 인자를 printf(3) 포맷으로 정형화해 Ruby 문자열 생성
  • rb_str_cat(VALUE str, const char *ptr, long len) — Ruby 문자열 strlen 바이트의 ptr 추가
  • rb_str_cat2(VALUE str, const char* ptr) / rb_str_cat_cstr(VALUE str, const char* ptr) — Ruby 문자열 str에 C 문자열 ptr 추가. 기능은 rb_str_cat(str, ptr, strlen(ptr))과 동일
  • rb_str_catf(VALUE str, const char* format, …) / rb_str_vcatf(VALUE str, const char* format, va_list ap) — 포맷에 따라 정형화해 str에 추가
  • rb_enc_str_new(const char *ptr, long len, rb_encoding *enc) / rb_enc_str_new_cstr(...) — 지정한 인코딩으로 Ruby 문자열 생성
  • rb_enc_str_new_literal(const char *ptr, rb_encoding *enc) — C 리터럴 문자열에서 지정 인코딩으로 Ruby 문자열 생성
  • rb_usascii_str_new(...) / rb_usascii_str_new_cstr(...) / rb_usascii_str_new_literal(...) — US-ASCII 인코딩 문자열 생성
  • rb_utf8_str_new(...) / rb_utf8_str_new_cstr(...) / rb_utf8_str_new_literal(...) — UTF-8 인코딩 문자열 생성
  • rb_str_resize(VALUE str, long len) — Ruby 문자열 크기를 len 바이트로 변경. 짧아지면 넘친 부분은 버려지고, 길어지면 넘친 부분은 쓰레기가 될 수 있어요. 호출로 RSTRING_PTR(str)이 바뀔 수 있으니 주의
  • rb_str_set_len(VALUE str, long len) — 크기를 len으로 설정. str이 변경 가능하지 않으면 예외. len이 용량을 넘으면 안 됨
  • rb_str_modify(VALUE str) — 문자열 변경 준비. RSTRING_PTR로 내용을 바꾸거나 rb_str_set_len을 부르기 전에 반드시 호출

배열 함수

  • rb_ary_new() — 요소 0개 배열 생성
  • rb_ary_new2(long len) / rb_ary_new_capa(long len) — 요소 0개 배열 생성. len개분 영역을 미리 할당
  • rb_ary_new3(long n, …) / rb_ary_new_from_args(long n, …) — 인자로 지정한 n개 요소를 포함하는 배열 생성
  • rb_ary_new4(long n, VALUE *elts) / rb_ary_new_from_values(long n, VALUE *elts) — 배열로 주어진 n개 요소의 배열 생성
  • rb_ary_to_ary(VALUE obj) — 객체를 배열로 변환. Object#to_ary와 동일

배열을 조작하는 함수는 많아요. 이 함수들은 인자 ary에 배열을 넘겨야 해요. 그러지 않으면 코어를 덤프할 수 있어요.

  • rb_ary_aref(int argc, const VALUE *argv, VALUE ary)Array#[]와 동일
  • rb_ary_entry(VALUE ary, long offset)ary[offset]
  • rb_ary_store(VALUE ary, long offset, VALUE obj)ary[offset] = obj
  • rb_ary_subseq(VALUE ary, long beg, long len)ary[beg, len]
  • rb_ary_push(VALUE ary, VALUE val) / rb_ary_pop / rb_ary_shift / rb_ary_unshift — 배열 push/pop/shift/unshift
  • rb_ary_cat(VALUE ary, const VALUE *ptr, long len)ptr에서 len개 객체를 ary에 추가

2. Ruby의 기능 사용하기

원리적으로 Ruby로 쓸 수 있는 것은 C로도 쓸 수 있어요. Ruby 자체가 C로 작성되어 있으니 당연한 얘기죠. 여기서는 Ruby 확장에 자주 쓰일 것으로 예상되는 기능을 중심으로 소개할게요.

Ruby에 기능 추가하기

Ruby가 제공하는 함수를 쓰면 Ruby 인터프리터에 새 기능을 추가할 수 있어요. Ruby에서 추가할 수 있는 기능은 클래스·모듈, 메서드·특이 메서드 등, 상수예요.

클래스/모듈 정의

VALUE rb_define_class(const char *name, VALUE super)
VALUE rb_define_module(const char *name)

이 함수들은 새로 정의한 클래스·모듈을 반환해요. 메서드·상수 정의에 이 값들이 필요하므로, 대부분 반환값을 변수에 저장해 둬야 해요.

클래스·모듈을 다른 클래스 안에 네스트해서 정의할 때는 다음 함수를 써요.

VALUE rb_define_class_under(VALUE outer, const char *name, VALUE super)
VALUE rb_define_module_under(VALUE outer, const char *name)

메서드/특이 메서드 정의

void rb_define_method(VALUE klass, const char *name,
                      VALUE (*func)(ANYARGS), int argc)

void rb_define_singleton_method(VALUE object, const char *name,
                                VALUE (*func)(ANYARGS), int argc)

참고로 **특이 메서드(singleton method)**란 특정 객체에 대해서만 유효한 메서드예요. Ruby에서는 Smalltalk의 클래스 메서드에 해당하는 것으로서, 클래스에 대한 특이 메서드가 자주 쓰여요.

이 함수들의 argc 인자는 C 함수로 넘겨지는 인자의 수(와 형식)를 결정해요.

  • argc0 이상이면 함수에 전달되는 인자의 수를 의미해요. 16개 이상의 인자는 쓸 수 없어요. 실제 C 함수는 첫 인자로 self가 주어지므로, 지정한 수보다 하나 많은 인자를 갖게 돼요.
  • argc음수면 인자의 수가 아니라 형식을 지정한 것이에요.
    • argc-1이면 인자를 배열에 넣어 전달해요.
    • argc-2이면 인자를 Ruby 배열로 전달해요.

메서드를 정의하는 함수가 몇 가지 더 있어요. 하나는 메서드 이름으로 ID를 받아요(ID에 대해서는 2.2.2 참고).

void rb_define_method_id(VALUE klass, ID name,
                         VALUE (*func)(ANYARGS), int argc)

private/protected 메서드를 정의하는 함수도 있어요.

void rb_define_private_method(VALUE klass, const char *name,
                              VALUE (*func)(ANYARGS), int argc)
void rb_define_protected_method(VALUE klass, const char *name,
                                VALUE (*func)(ANYARGS), int argc)

private 메서드는 함수 형태로만 호출할 수 있는 메서드예요.

마지막으로 rb_define_module_function모듈 함수를 정의해요. 모듈 함수는 모듈의 특이 메서드이면서 동시에 private 메서드인 것이에요. 예를 들어 Math.sqrt()가 그렇죠. 이 메서드는 Math.sqrt(4) 형태로도, include Math; sqrt(4) 형태로도 쓸 수 있어요.

void rb_define_module_function(VALUE module, const char *name,
                               VALUE (*func)(ANYARGS), int argc)

함수적 메서드(Kernel 모듈의 private method)를 정의하는 함수는 다음과 같아요.

void rb_define_global_function(const char *name, VALUE (*func)(ANYARGS), int argc)

메서드 별칭을 정의하는 함수는 다음과 같아요.

void rb_define_alias(VALUE module, const char* new, const char* old);

속성(attribute)의 get/set 메서드를 정의하려면 다음과 같이 해요.

void rb_define_attr(VALUE klass, const char *name, int read, int write)

클래스 메서드 allocate를 정의하거나 제거하는 함수는 다음과 같아요.

void rb_define_alloc_func(VALUE klass, VALUE (*func)(VALUE klass));
void rb_undef_alloc_func(VALUE klass);

func는 클래스를 인자로 받아서, 새로 할당된 인스턴스를 반환해야 해요. 이 인스턴스는 외부 리소스 등을 포함하지 않는, 가능한 "빈" 상태로 두는 게 좋아요.

상속한 클래스의 기존 메서드를 오버라이드했다면, 오버라이드된 메서드를 호출할 때는 다음 함수를 써요.

VALUE rb_call_super(int argc, const VALUE *argv)

현재 스코프의 리시버는(다른 방법이 없으면) 다음 함수로 얻을 수 있어요.

VALUE rb_current_receiver(void)

상수 정의

확장 라이브러리가 필요로 하는 상수는 미리 정의해 두는 게 좋아요. 상수를 정의하는 함수는 두 가지예요.

void rb_define_const(VALUE klass, const char *name, VALUE val)
void rb_define_global_const(const char *name, VALUE val)

前者는 특정 클래스·모듈에 속하는 상수를, 후자는 전역 상수를 정의해요.

C에서 Ruby 기능 호출하기

앞의 "Ruby 데이터 조작하기"에서 일부 소개한 함수를 쓰면, Ruby 기능을 구현하는 함수를 직접 호출할 수 있어요. 이런 함수들의 목록표는 아직 없으니 소스를 봐야 해요.

그 외에도 Ruby 기능을 호출하는 방법이 몇 가지 있어요.

Ruby 프로그램 eval하기

C에서 Ruby 기능을 호출하는 가장 간단한 방법으로, 문자열로 주어진 Ruby 프로그램을 평가하는 다음 함수가 있어요.

VALUE rb_eval_string(const char *str)

이 평가는 현재 환경에서 이뤄져요. 즉 현재의 로컬 변수 등을 이어받아요.

평가는 예외를 발생시킬 수 있음을 주의하세요. 더 안전한 함수도 있어요.

VALUE rb_eval_string_protect(const char *str, int *state)

이 함수는 에러가 발생하면 nil을 반환해요. 성공 시 *state는 0, 실패 시 비제로가 돼요.

ID 또는 심볼

C에서 문자열을 경유하지 않고 Ruby 메서드를 호출할 수도 있어요. 그 전에 Ruby 인터프리터 안에서 메서드·변수 이름을 지정할 때 쓰는 ID에 대해 설명할게요.

ID는 변수명·메서드명을 나타내는 정수예요. Ruby에서는 :식별자 또는 :"임의의 문자열"로 접근할 수 있어요. C에서 이 정수를 얻으려면 다음 함수를 써요.

rb_intern(const char *name)
rb_intern_str(VALUE name)

Ruby에서 인자로 주어진 심볼(또는 문자열)을 ID로 변환할 때는 다음 함수를 써요.

rb_to_id(VALUE symbol)
rb_check_id(volatile VALUE *name)
rb_check_id_cstr(const char *name, long len, rb_encoding *enc)

인자가 심볼도 문자열도 아니면 to_str 메서드로 문자열로 변환하려고 해요. 두 번째 함수는 변환 결과를 *name에 저장하고, 그 이름이 알려진 심볼이 아니면 0을 반환해요. 0이 아닌 값을 반환하면 *name은 항상 심볼이나 문자열이고, 0을 반환하면 항상 문자열이에요. 세 번째 함수는 Ruby 문자열이 아니라 NUL 종료된 C 문자열을 써요.

Ruby에서 인자로 주어진 심볼(또는 문자열)을 심볼로 변환할 때는 다음 함수를 써요.

rb_to_symbol(VALUE name)
rb_check_symbol(volatile VALUE *namep)
rb_check_symbol_cstr(const char *ptr, long len, rb_encoding *enc)

이 함수들은 ID 대신 심볼을 반환한다는 점만 빼면 위 함수들과 같아요.

C에서 Ruby 메서드 호출하기

C에서 문자열을 경유하지 않고 Ruby 메서드를 호출하려면 다음 함수를 써요.

VALUE rb_funcall(VALUE recv, ID mid, int argc, ...)

이 함수는 객체 recvmid로 지정한 메서드를 호출해요. 인자 지정 방식이 다른 다음 함수도 있어요.

VALUE rb_funcall2(VALUE recv, ID mid, int argc, VALUE *argv)
VALUE rb_funcallv(VALUE recv, ID mid, int argc, VALUE *argv)
VALUE rb_apply(VALUE recv, ID mid, VALUE args)

rb_apply에는 인자로 Ruby 배열을 줘요.

변수/상수 참조·갱신

C에서 함수를 통해 참조·갱신할 수 있는 것은 상수인스턴스 변수예요. 대역 변수 중 일부는 C의 대역 변수로 접근할 수 있어요. 로컬 변수를 참조하는 방법은 공개되어 있지 않아요.

객체의 인스턴스 변수를 참조·갱신하는 함수는 다음과 같아요.

VALUE rb_ivar_get(VALUE obj, ID id)
VALUE rb_ivar_set(VALUE obj, ID id, VALUE val)

idrb_intern()으로 얻은 값을 사용하세요.

상수를 참조하려면 다음 함수를 써요.

VALUE rb_const_get(VALUE obj, ID id)

상수를 새로 정의하려면 앞의 "상수 정의"에서 소개한 함수를 쓰면 돼요.

3. Ruby와 C의 정보 공유

C 언어와 Ruby 사이에서 정보를 공유하는 방법을 설명할게요.

C에서 참조할 수 있는 Ruby 상수

다음 Ruby 상수는 C 레벨에서 참조할 수 있어요.

  • Qtrue / Qfalse — 진릿값. C에서 보는 "true"와 "false"
  • Qnil — C에서 보는 "nil"

RTEST(obj) 매크로는 objQfalse 또는 Qnil이면 0을 반환해요.

C와 Ruby가 공유하는 대역 변수

C와 Ruby는 대역 변수로 정보를 공유할 수 있어요. 가장 자주 쓰이는 건 rb_define_variable()이에요.

void rb_define_variable(const char *name, VALUE *var)

이 함수는 Ruby와 C가 공유하는 대역 변수를 정의해요. 변수명이 $로 시작하지 않으면 자동으로 추가돼요. 이 변수의 값을 바꾸면 자동으로 Ruby의 대응 변수 값도 바뀌어요.

Ruby 쪽에서 갱신할 수 없는 변수도 있어요. 이 read only 변수는 다음 함수로 정의해요.

void rb_define_readonly_variable(const char *name, VALUE *var)

이 변수들 외에 hook을 붙인 대역 변수를 정의할 수도 있어요. hook이 붙은 대역 변수는 다음 함수로 정의해요. hook 붙은 변수의 값 참조·설정은 hook에서 해야 해요.

void rb_define_hooked_variable(const char *name, VALUE *var,
                               VALUE (*getter)(), void (*setter)())

이 함수는 C 함수로 hook이 붙은 대역 변수를 정의해요. 변수가 참조되면 getter가, 값이 설정되면 setter가 호출돼요. hook을 지정하지 않으려면 getter·setter에 0을 주면 돼요.

gettersetter의 시그니처는 다음과 같아요.

VALUE (*getter)(ID id, VALUE *var);
void (*setter)(VALUE val, ID id, VALUE *var);

그리고 대응하는 C 변수가 없는 Ruby 대역 변수도 정의할 수 있어요. 그 변수의 값은 훅 함수로만 얻고 설정돼요.

void rb_define_virtual_variable(const char *name,
                                VALUE (*getter)(), void (*setter)())

이 함수로 정의된 Ruby 대역 변수가 참조되면 getter가, 값이 설정되면 setter가 호출돼요.

(*getter)(ID id);
(*setter)(VALUE val, ID id);

C 데이터를 Ruby 객체로 만들기

C 세계에서 정의된 데이터(구조체)를 Ruby 객체로 취급하고 싶을 수 있어요. 그럴 때는 TypedData_XXX 매크로 군으로 구조체 포인터와 Ruby 객체를 서로 변환할 수 있어요.

구조체에서 객체로

구조체 포인터 sval을 Ruby 객체로 변환하려면 다음 매크로를 써요.

TypedData_Wrap_Struct(klass, data_type, sval)

반환값은 생성된 객체를 나타내는 VALUE예요.

  • klass — 객체의 클래스. Object 클래스에서 파생되고, 반드시 rb_define_alloc_func 또는 rb_undef_alloc_func을 호출해 allocator를 설정해야 해요.
  • data_type — 구조체를 Ruby가 관리하기 위한 정보를 기술한 const rb_data_type_t 타입 포인터예요.

rb_data_type_t는 다음과 같이 정의돼요.

typedef struct rb_data_type_struct rb_data_type_t;

struct rb_data_type_struct {
    const char *wrap_struct_name;
    struct {
        void (*dmark)(void*);
        void (*dfree)(void*);
        size_t (*dsize)(const void *);
        void *reserved[2];
    } function;
    const rb_data_type_t *parent;
    void *data;
    VALUE flags;
};
  • wrap_struct_name — 구조체를 식별하는 이름. 주로 통계 정보 수집·출력에 쓰여요. 프로세스 안에서 유일하면 C나 Ruby 식별자로 유효할 필요는 없어요.
  • dmark·dfree 함수는 GC 실행 중 호출돼요. GC 실행 중에는 Ruby 객체 할당이 금지되므로, dmark·dfree에서 Ruby 객체를 할당하면 안 돼요.
  • dmark — GC가 객체로의 참조를 마크할 때 쓰는 함수. 구조체가 Ruby 객체로의 참조를 가지면, dmark에서 rb_gc_mark 등을 사용해 구조체 안의 모든 참조를 마크해야 해요. 그런 참조를 포함하지 않으면 0을 지정해요.
  • dfree — 구조체가 더 이상 필요 없을 때 GC가 호출하는 함수. RUBY_DEFAULT_FREE를 지정하면 단순히 구조체가 해제돼요.
  • dsize — 구조체가 소비하는 메모리 바이트 수를 반환하는 함수. 구현이 어려우면 0을 넘겨도 괜찮지만, 가능하면 지정하는 게 좋아요.
  • reserved·parent — 0으로 채워야 해요.
  • data — 사용자 정의의 임의 값을 지정할 수 있어요. Ruby는 이 값에 관여하지 않으니 자유롭게 써도 돼요.
  • flags — 해당되는 아래 플래그들의 비트 합을 지정해요. 어느 쪽이든 Ruby GC에 대한 깊은 이해가 필요하므로, 잘 모르겠으면 0을 지정하는 게 좋아요.
    • RUBY_TYPED_FREE_IMMEDIATELY — GC가 구조체가 불필요해지면 GC 중에 즉시 dfree를 호출하도록 해요. dfree가 Ruby 내부의 락(GVL)을 해제할 가능성이 없으면 이 플래그를 지정할 수 있어요. 지정하지 않으면 dfree 호출은 지연되어 파이널라이저와 같은 타이밍에 실행돼요.
    • RUBY_TYPED_WB_PROTECTED — 객체 구현이 라이트 배리어를 지원한다는 뜻이에요. 이 플래그를 지정하면 Ruby가 GC를 더 효율적으로 실행할 수 있어요. 다만 지정하는 경우 사용자는 그 객체의 모든 메서드 구현에 올바르게 라이트 배리어를 넣을 책임이 있어요. 그러지 않으면 Ruby가 런타임에 크래시할 수 있어요.

이 매크로는 예외를 발생시킬 가능성이 있어요. 감싸는 sval이 해제해야 할 리소스(할당된 메모리, 외부 라이브러리의 핸들 등)를 가지고 있다면 rb_protect를 사용해야 해요.

C 구조체의 할당과 객체 생성을 동시에 하는 매크로도 있어요.

TypedData_Make_Struct(klass, type, data_type, sval)

반환값은 생성된 객체의 VALUE예요. 이 매크로는 다음처럼 동작해요.

(sval = ZALLOC(type), TypedData_Wrap_Struct(klass, data_type, sval))

klass, data_typeTypedData_Wrap_Struct와 같은 역할을 해요. type은 할당할 C 구조체의 타입이고, 할당된 구조체는 변수 sval에 대입돼요. 이 변수의 타입은 (type*)이어야 해요.

객체에서 구조체로

TypedData_Wrap_StructTypedData_Make_Struct로 생성한 객체에서 구조체 포인터를 복원하려면 다음 매크로를 써요.

TypedData_Get_Struct(obj, type, &data_type, sval)

C 구조체 포인터는 변수 sval에 대입돼요.

이 매크로들의 사용법은 조금 헷갈리니, 아래에서 설명할 예제를 참고하세요.

4. 예제: dbm 확장 라이브러리 만들기

dbm 라이브러리를 확장 라이브러리로 만드는 전체 과정이에요. 실제 코드의 흐름을 따라가면서 구조체 캡슐화와 메서드 정의가 어떻게 이뤄지는지 확인할 수 있어요.

디렉터리 만들기

% mkdir ext/dbm

Ruby 1.1부터는 임의의 디렉터리에서 다이내믹 라이브러리를 만들 수 있어요. Ruby에 정적으로 링크하려면 Ruby를 풀어 놓은 디렉터리 아래 ext 디렉터리 안에 확장 라이브러리용 디렉터리를 만들어야 해요. 이름은 적당히 골라도 돼요.

설계하기

당연한 얘기지만, 어떤 기능을 구현할지 먼저 설계해야 해요. 어떤 클래스를 만들지, 그 클래스에 어떤 메서드가 있는지, 클래스가 제공하는 상수는 무엇인지 등을 설계해요.

C 코드 작성하기

확장 라이브러리 본체인 C 소스를 작성해요. C 소스가 하나일 때는 "라이브러리명.c"를 고르는 게 좋아요. C 소스가 여러 개면 거꾸로 "라이브러리명.c"라는 파일명은 피해야 해요. 오브젝트 파일과 모듈 생성 시 중간 생성되는 "라이브러리명.o" 파일과 충돌하기 때문이에요. 또, 후술할 mkmf 라이브러리의 일부 함수가 컴파일을 요하는 테스트에 "conftest.c"를 사용한다는 점에 주의하세요. 소스 파일명으로 "conftest.c"를 사용하면 안 돼요.

Ruby는 확장 라이브러리를 로드할 때 Init_라이브러리명 함수를 자동 실행해요. dbm 라이브러리라면 Init_dbm이에요. 이 함수 안에서 클래스, 모듈, 메서드, 상수 등을 정의해요. dbm.c에서 일부를 인용할게요.

void
Init_dbm(void)
{
    /* DBM 클래스 정의 */
    VALUE cDBM = rb_define_class("DBM", rb_cObject);
    /* DBM이 Enumerable 모듈을 include */
    rb_include_module(cDBM, rb_mEnumerable);

    /* DBM 클래스의 클래스 메서드 open(): 인자는 C 배열로 받음 */
    rb_define_singleton_method(cDBM, "open", fdbm_s_open, -1);

    /* DBM 클래스의 메서드 close(): 인자 없음 */
    rb_define_method(cDBM, "close", fdbm_close, 0);
    /* DBM 클래스의 메서드 []: 인자 1개 */
    rb_define_method(cDBM, "[]", fdbm_fetch, 1);

    /* ... */

    /* DBM 데이터를 저장하는 인스턴스 변수명을 위한 ID */
    id_dbm = rb_intern("dbm");
}

DBM 라이브러리는 dbm 데이터에 대응하는 객체가 되어야 하므로, C 세계의 dbm을 Ruby 세계로 들여와야 해요. dbm.cTypedData_Make_Struct를 다음과 같이 사용해요.

struct dbmdata {
    int  di_size;
    DBM *di_dbm;
};

static const rb_data_type_t dbm_type = {
    "dbm",
    {0, free_dbm, memsize_dbm,},
    0, 0,
    RUBY_TYPED_FREE_IMMEDIATELY,
};

obj = TypedData_Make_Struct(klass, struct dbmdata, &dbm_type, dbmp);

여기서 dbmdata 구조체 포인터를 Ruby 객체로 캡슐화하고 있어요. DBM*을 직접 캡슐화하지 않는 건 close()할 때의 처리를 생각한 것이에요.

Ruby 객체에서 dbmdata 구조체 포인터를 꺼내기 위해 다음 매크로를 써요.

#define GetDBM(obj, dbmp) do {\
    TypedData_Get_Struct((obj), struct dbmdata, &dbm_type, (dbmp));\
    if ((dbmp) == 0) closed_dbm();\
    if ((dbmp)->di_dbm == 0) closed_dbm();\
} while (0)

조금 복잡한 매크로지만, 요컨대 dbmdata 구조체 포인터를 꺼내고 close됐는지 확인하는 처리를 모아 둔 것이에요.

DBM 클래스에는 메서드가 많지만, 인자를 받는 방식으로 분류하면 세 종류가 있어요. 첫째는 인자 수가 고정된 것으로, 예를 들면 delete 메서드가 있어요. fdbm_delete()는 이렇게 생겼어요.

static VALUE
fdbm_delete(VALUE obj, VALUE keystr)
{
    /* ... */
}

인자 수가 고정된 타입은 첫 인자가 self, 두 번째 인자 이후가 메서드 인자예요.

둘째는 인자 수가 가변이라서, C 배열로 받는 것과 Ruby 배열로 받는 것이 있어요. dbm 라이브러리에서 C 배열로 받는 것은 DBM의 클래스 메서드 open()이에요. fdbm_s_open()은 이렇게 생겼어요.

static VALUE
fdbm_s_open(int argc, VALUE *argv, VALUE klass)
{
    /* ... */

    if (rb_scan_args(argc, argv, "11", &file, &vmode) == 1) {
        mode = 0666;          /* default value */
    }

    /* ... */
}

이 타입의 함수는 첫 인자가 주어진 인자의 수, 두 번째 인자가 주어진 인자를 담은 배열이에요. self는 세 번째 인자로 주어져요. 이 배열로 주어진 인자를 해석하는 함수가 open()에서도 쓰인 rb_scan_args()예요. 세 번째 인자에 지정한 포맷에 따라, 네 번째 인자 이후에 지정한 VALUE 참조에 값을 대입해 줘요.

인자 수만 확인한다면 rb_check_arity()를 쓸 수 있어요. 인자를 리스트로 취급하고 싶을 때 편리해요.

셋째는 인자를 Ruby 배열로 받는 메서드로, 예를 들면 Thread#initialize가 있어요. 구현은 이렇게 생겼어요.

static VALUE
thread_initialize(VALUE thread, VALUE args)
{
    /* ... */
}

첫 인자는 self, 두 번째 인자는 Ruby 배열이에요.

주의사항: Ruby와 공유하지는 않지만 Ruby 객체를 저장할 가능성이 있는 C 대역 변수는, 다음 함수로 Ruby 인터프리터에 변수의 존재를 알려주세요. 그러지 않으면 GC에서 문제가 생겨요.

void rb_global_variable(VALUE *var)

extconf.rb 준비하기

Makefile을 만들 때의 빵틀이 되는 extconf.rb 파일을 만들어요. extconf.rb는 라이브러리 컴파일에 필요한 조건을 체크하는 것이 목적이에요. 먼저 다음을 extconf.rb의 맨 앞에 놓아요.

require 'mkmf'

extconf.rb 안에서는 다음 Ruby 함수를 쓸 수 있어요.

have_library(lib, func): 라이브러리의 존재 체크
have_func(func, header): 함수의 존재 체크
have_header(header): 헤더 파일의 존재 체크
create_makefile(target[, target_prefix]): Makefile 생성

다음 변수도 쓸 수 있어요.

$CFLAGS: 컴파일 시 추가로 지정하는 플래그(-O 등)
$CPPFLAGS: 전처리기에 추가로 지정하는 플래그(-I나 -D 등)
$LDFLAGS: 링크 시 추가로 지정하는 플래그(-L 등)
$objs: 링크되는 오브젝트 파일명 목록

오브젝트 파일 목록은 보통 소스 파일을 검색해 자동 생성되지만, make 도중에 소스를 생성하는 경우에는 명시적으로 지정해야 해요.

라이브러리를 컴파일할 조건이 갖춰지지 않아 컴파일하지 않을 때는 create_makefile을 호출하지 않으면 Makefile이 생성되지 않고 컴파일도 이뤄지지 않아요.

depend 준비하기

디렉터리에 depend 파일이 있으면 Makefile이 의존 관계를 체크해 줘요.

% gcc -MM *.c > depend

이런 식으로 만들 수 있어요. 있어서 나쁠 건 없어요.

Makefile 생성하기

Makefile을 실제로 생성하려면 다음을 실행해요.

ruby extconf.rb

extconf.rbrequire 'mkmf' 줄이 없으면 에러가 나니, 인자를 추가해 아래처럼 해 주세요.

ruby -r mkmf extconf.rb

site_ruby 디렉터리가 아니라 vendor_ruby 디렉터리에 설치하려면 --vendor 옵션을 추가해요.

ruby extconf.rb --vendor

디렉터리를 ext 아래에 준비했다면 Ruby 전체의 make 시에 Makefile이 자동 생성되므로 이 단계는 불필요해요.

make하기

다이내믹 링크 라이브러리를 생성할 때는 그 자리에서 make 해요. 필요하면 make install로 설치해요.

ext 아래에 디렉터리를 준비했다면, Ruby 디렉터리에서 make를 실행하면 Makefile 생성부터 make, 필요에 따라 그 모듈의 Ruby로의 링크까지 자동으로 실행돼요. extconf.rb를 고쳐서 Makefile을 다시 생성해야 할 때는 다시 Ruby 디렉터리에서 make 해요.

확장 라이브러리는 make install로 Ruby 라이브러리 디렉터리 아래에 복사돼요. 확장 라이브러리와 협조해서 쓰는 Ruby 프로그램을 Ruby 라이브러리에 두고 싶다면, 확장 라이브러리용 디렉터리 아래에 lib 디렉터리를 만들고 그 안에 확장자 .rb 파일을 두면 동시에 설치돼요.

디버그

디버그하지 않으면 안 돌아가죠. ext/Setup에 디렉터리명을 쓰면 정적으로 링크되므로 디버거를 쓸 수 있어요. 그만큼 컴파일은 느려지지만요.

완성

이후에는 몰래 쓰든, 널리 공개하든, 팔든 마음대로 하세요. Ruby의 저자는 확장 라이브러리에 대해 일절 권리를 주장하지 않아요.

부록 A. Ruby 소스 코드의 분류

Ruby 소스는 몇 가지로 분류할 수 있어요. 이 중 클래스 라이브러리 부분은 기본적으로 확장 라이브러리와 만드는 방법이 같아요. 이 소스들은 지금까지의 설명으로 거의 이해할 수 있을 거예요.

Ruby의 헤더 파일

$repo_root/include/ruby 아래는 모두 make install로 설치돼요. 확장 라이브러리에서는 #include <ruby.h>로 인클루드해야 해요. rbimpl_, RBIMPL_ 프리픽스가 붙은 구현 상세용 심볼을 제외하면 모든 심볼이 공개 API예요.

확장 라이브러리가 직접 인클루드할 수 있는 것은 $repo_root/include/ruby/*.h 중, 대응하는 HAVE_RUBY_*_H 매크로가 $repo_root/include/ruby.h에 정의된 것이에요.

Ruby 언어의 코어

  • class.c — 클래스와 모듈
  • error.c — 예외 클래스와 예외 기구
  • gc.c — 기억 영역 관리
  • load.c — 라이브러리 로드
  • object.c — 객체
  • variable.c — 변수와 상수

Ruby의 구문 해석기

  • parse.y — 어휘 분석기와 구문 정의
  • parse.c — 자동 생성
  • defs/keywords — 예약어
  • lex.c — 자동 생성

Ruby의 평가기 (통칭 YARV)

compile.c
eval.c
eval_error.c
eval_jump.c
eval_safe.c
insns.def           : 가상 머신어 정의
iseq.c              : VM::ISeq 구현
thread.c            : 스레드 관리와 컨텍스트 전환
thread_win32.c      : 스레드 구현
thread_pthread.c    : 동일
vm.c
vm_dump.c
vm_eval.c
vm_exec.c
vm_insnhelper.c
vm_method.c

defs/opt_insns_unif.def  : 명령 융합
defs/opt_operand.def     : 최적화를 위한 정의

  -> insn*.inc           : 자동 생성
  -> opt*.inc            : 자동 생성
  -> vm.inc              : 자동 생성

정규식 엔진 (鬼雲)

regcomp.c
regenc.c
regerror.c
regexec.c
regparse.c
regsyntax.c

유틸리티 함수

  • debug.c — C 디버거용 디버그 심볼
  • dln.c — 다이내믹 로딩
  • st.c — 범용 해시 테이블
  • strftime.c — 시간 정형화
  • util.c — 그 외 유틸리티

Ruby 커맨드의 구현

dmyext.c
dmydln.c
dmyencoding.c
id.c
inits.c
main.c
ruby.c
version.c

gem_prelude.rb
prelude.rb

클래스 라이브러리

  • array.cArray
  • bignum.c — Bignum
  • compar.cComparable
  • complex.cComplex
  • cont.cFiber, Continuation
  • dir.cDir
  • enum.cEnumerable
  • enumerator.cEnumerator
  • file.cFile
  • hash.cHash
  • io.cIO
  • marshal.cMarshal
  • math.cMath
  • numeric.cNumeric, Integer, Fixnum, Float
  • pack.cArray#pack, String#unpack
  • proc.cBinding, Proc
  • process.c — Process
  • random.c — 난수
  • range.cRange
  • rational.cRational
  • re.cRegexp, MatchData
  • signal.cSignal
  • sprintf.cString#sprintf
  • string.cString
  • struct.cStruct
  • time.cTime
  • defs/known_errors.def — 예외 클래스 Errno::*known_errors.inc(자동 생성)

다국어화

  • encoding.cEncoding
  • transcode.cEncoding::Converter
  • enc/*.c — 인코딩 클래스 군
  • enc/trans/* — 코드 포인트 대응표

goruby 커맨드의 구현

goruby.c
golf_prelude.rb      : goruby 고유의 라이브러리
  -> golf_prelude.c  : 자동 생성

부록 B. 확장용 함수 레퍼런스

C 언어에서 Ruby 기능을 사용하는 API는 다음과 같아요.

타입

  • VALUE — Ruby 객체를 표현하는 타입. 필요에 따라 캐스팅해 사용. 내장 타입을 표현하는 C 타입은 ruby.h에 있는 R로 시작하는 구조체. VALUE를 이것들로 캐스팅하기 위한 R 구조체명을 전부 대문자로 한 매크로가 준비되어 있음

변수·상수

  • Qnil — 상수: nil 객체
  • Qtrue — 상수: true 객체(참의 기본값)
  • Qfalse — 상수: false 객체

C 데이터의 캡슐화

  • Data_Wrap_Struct(VALUE klass, void (*mark)(), void (*free)(), void *sval) — C의 임의 포인터를 캡슐화한 Ruby 객체를 반환. 이 포인터가 Ruby에서 접근되지 않게 되면 free가 호출됨. 포인터가 가리키는 데이터가 다른 Ruby 객체를 가리키면 mark로 마크해야 함
  • Data_Make_Struct(klass, type, mark, free, sval)type형 메모리를 malloc하고 sval에 대입한 뒤, 그것을 캡슐화한 데이터를 반환하는 매크로
  • Data_Get_Struct(data, type, sval)data에서 type형 포인터를 꺼내 sval에 대입하는 매크로

타입 체크

RB_TYPE_P(value, type)
TYPE(value)
FIXNUM_P(value)
NIL_P(value)
RB_INTEGER_TYPE_P(value)
RB_FLOAT_TYPE_P(value)
void Check_Type(VALUE value, int type)

타입 변환

FIX2INT(value), INT2FIX(i)
FIX2LONG(value), LONG2FIX(l)
NUM2INT(value), INT2NUM(i)
NUM2UINT(value), UINT2NUM(ui)
NUM2LONG(value), LONG2NUM(l)
NUM2ULONG(value), ULONG2NUM(ul)
NUM2LL(value), LL2NUM(ll)
NUM2ULL(value), ULL2NUM(ull)
NUM2OFFT(value), OFFT2NUM(off)
NUM2SIZET(value), SIZET2NUM(size)
NUM2SSIZET(value), SSIZET2NUM(ssize)
rb_integer_pack(value, words, numwords, wordsize, nails, flags), rb_integer_unpack(words, numwords, wordsize, nails, flags)
NUM2DBL(value)
rb_float_new(f)
RSTRING_LEN(str)
RSTRING_PTR(str)
StringValue(value)
StringValuePtr(value)
StringValueCStr(value)
rb_str_new2(s)

클래스/모듈 정의

  • VALUE rb_define_class(const char *name, VALUE super)super의 서브클래스로 새 Ruby 클래스 정의
  • VALUE rb_define_class_under(VALUE module, const char *name, VALUE super)super의 서브클래스로 새 클래스를 정의하고 module의 상수로 정의
  • VALUE rb_define_module(const char *name) — 새 Ruby 모듈 정의
  • VALUE rb_define_module_under(VALUE module, const char *name) — 새 모듈을 정의하고 module의 상수로 정의
  • void rb_include_module(VALUE klass, VALUE module) — 모듈을 include. 이미 include했으면 아무것도 하지 않음(중복 include 금지)
  • void rb_extend_object(VALUE object, VALUE module) — 객체를 모듈의 메서드로 확장

대역 변수 정의

  • void rb_define_variable(const char *name, VALUE *var) — Ruby와 C가 공유하는 전역 변수 정의. 이름이 $로 시작하지 않으면 자동 추가. Ruby 식별자로 허용되지 않는 문자가 있으면 Ruby 프로그램에서는 보이지 않음
  • void rb_define_readonly_variable(const char *name, VALUE *var) — Ruby와 C가 공유하는 read only 전역 변수 정의. read only라는 점만 빼면 rb_define_variable()과 같음
  • void rb_define_virtual_variable(const char *name, VALUE (*getter)(), void (*setter)()) — 함수로 구현되는 Ruby 변수 정의. 참조되면 getter, 설정되면 setter 호출
  • void rb_define_hooked_variable(const char *name, VALUE *var, VALUE (*getter)(), void (*setter)()) — 함수로 hook이 붙은 전역 변수 정의. getter·setter에 0을 지정하면 hook을 지정하지 않은 것과 같음
  • void rb_global_variable(VALUE *var) — 마크해야 할 Ruby 객체를 포함하는 전역 변수를 GC가 해제하지 않도록 보호
  • void rb_gc_register_mark_object(VALUE object) — 마크해야 할 Ruby 객체를 GC가 해제하지 않도록 등록

상수

  • void rb_define_const(VALUE klass, const char *name, VALUE val) — 상수 정의
  • void rb_define_global_const(const char *name, VALUE val) — 전역 상수 정의. rb_define_const(rb_cObject, name, val)과 같은 의미

메서드 정의

  • rb_define_method(VALUE klass, const char *name, VALUE (*func)(ANYARGS), int argc) — 메서드 정의. argcself를 제외한 인자 수. argc-1이면 함수에 인자 수(self 미포함)를 첫 인자, 인자 배열을 두 번째 인자로 주는 형식(세 번째는 self). argc-2이면 첫 인자가 self, 두 번째가 args(인자를 포함하는 Ruby 배열) 형식
  • rb_define_private_method(...) — private 메서드 정의. 인자는 rb_define_method()과 같음
  • rb_define_singleton_method(...) — 특이 메서드 정의. 인자는 rb_define_method()과 같음
  • rb_check_arity(int argc, int min, int max)argcmin..max 범위인지 체크. maxUNLIMITED_ARGUMENTS면 상한을 체크하지 않음. 범위 밖이면 ArgumentError
  • rb_scan_args(int argc, VALUE *argv, const char *fmt, …)argc/argv 형식으로 주어진 것을 지정 형식에 따라 분해해 이어지는 VALUE 참조에 설정. 형식 문자열 "12"는 인자가 최소 1개, 최대 3개(1+2) 허용이라는 뜻. 포맷 문자 뒤에 그만큼의 VALUE 참조를 놓아야 함. 참조 대신 NULL을 지정하면 그 값은 버려짐. 선택 인자가 생략되면 nil이 됨. 반환값은 주어진 인자 수(옵션 해시·블록은 세지 않음)
  • int rb_get_kwargs(VALUE keyword_hash, const ID *table, int required, int optional, VALUE *values) — 키워드로 지정된 값을 table에 따라 꺼냄. 필수 키워드가 없으면 "missing keyword" ArgumentError, 미사용 요소가 있으면 optional이 음수가 아니면 "unknown keyword" ArgumentError
  • VALUE rb_extract_keywords(VALUE *original_hash)original_hash가 가리키는 Hash에서 Symbol인 키와 값을 새 Hash로 꺼냄

Ruby 메서드 호출

  • VALUE rb_funcall(VALUE recv, ID mid, int narg, …) — 메서드 호출. 문자열에서 mid를 얻으려면 rb_intern() 사용. private/protected 메서드도 호출 가능
  • VALUE rb_funcall2(VALUE recv, ID mid, int argc, VALUE *argv) / rb_funcallv(...) — 메서드 호출. 인자를 argc/argv로 전달. private/protected도 호출 가능
  • VALUE rb_funcallv_public(VALUE recv, ID mid, int argc, VALUE *argv) — 메서드 호출. public 메서드만 호출 가능
  • VALUE rb_eval_string(const char *str) — 문자열을 Ruby 스크립트로 컴파일·실행
  • ID rb_intern(const char *name) — 문자열에 대응하는 ID 반환
  • char *rb_id2name(ID id) — ID에 대응하는 문자열 반환(디버그용)
  • char *rb_class2name(VALUE klass) — 클래스 이름 반환(디버그용). 이름 없는 클래스는 조상을 거슬러 이름 있는 클래스의 이름을 반환
  • int rb_respond_to(VALUE obj, ID id)objid 메서드를 가지는지 반환

인스턴스 변수

  • VALUE rb_iv_get(VALUE obj, const char *name)obj의 인스턴스 변수 값 얻기. @로 시작하지 않는 인스턴스 변수는 Ruby 프로그램에서 접근할 수 없는 "숨은" 인스턴스 변수. 상수는 대문자 이름을 가진 클래스(모듈)의 인스턴스 변수로 구현됨
  • VALUE rb_iv_set(VALUE obj, const char *name, VALUE val)obj의 인스턴스 변수를 val로 설정

제어 구조

  • VALUE rb_block_call(VALUE obj, ID mid, int argc, VALUE * argv, VALUE (*func) (ANYARGS), VALUE data2)func를 블록으로 설정하고 obj를 리시버, argc·argv를 인자로 mid 메서드 호출. func는 첫 인자로 yield된 값, 두 번째로 data2를 받음
  • VALUE rb_yield(VALUE val)val을 값으로 이터레이터 블록 호출
  • VALUE rb_rescue(VALUE (*func1)(ANYARGS), VALUE arg1, VALUE (*func2)(ANYARGS), VALUE arg2)func1arg1 인자로 호출. 도중 예외가 발생하면 func2를 불러 처리. 반환값은 예외 없으면 func1의, 있으면 func2의 반환값
  • VALUE rb_ensure(VALUE (*func1)(ANYARGS), VALUE arg1, VALUE (*func2)(ANYARGS), VALUE arg2)func1 실행 후(예외가 나도) 반드시 func2 실행
  • VALUE rb_protect(VALUE (*func) (VALUE), VALUE arg, int *state)func를 실행하고 예외가 없으면 반환값, 있으면 *state에 비제로를 세우고 Qnil 반환. 잡은 예외를 무시하려면 rb_set_errinfo(Qnil)로 에러 정보를 지워야 함
  • void rb_jump_tag(int state)rb_protect() 등이 잡은 예외를 재전송. 직접 호출자에게는 돌아가지 않음
  • void rb_iter_break() / void rb_iter_break_value(VALUE value) — 현재 가장 안쪽 블록을 종료. 직접 호출자에게는 돌아가지 않음

예외·에러

  • void rb_warning(const char *fmt, …)rb_verbose 시 표준 에러 출력에 경고 표시. 인자는 printf()와 같음
  • void rb_raise(VALUE exception, const char *fmt, …)exception으로 지정한 예외 발생. 인자는 printf()와 같음
  • void rb_fatal(const char *fmt, …) — 치명적 예외 발생. 통상의 예외 처리는 이루어지지 않고 인터프리터가 종료(단, ensure로 지정된 코드는 종료 전에 실행)
  • void rb_bug(const char *fmt, …) — 인터프리터 등의 버그로만 발생해야 할 상황에서 호출. 코어 덤프 후 즉시 종료. 예외 처리는 전혀 이루어지지 않음

참고: %PRIsVALUE는 Object#to_s(+ 플래그 지정 시 Object#inspect)를 사용하는 VALUE 출력에 쓸 수 있어요. 이것은 %i와 충돌하므로 정수에는 %d를 사용하세요.

Ruby의 초기화·실행

Ruby를 애플리케이션에 임베드할 때는 다음 인터페이스를 써요. 통상의 확장 라이브러리에는 필요 없어요.

  • void ruby_init() — Ruby 인터프리터 초기화
  • void *ruby_options(int argc, char **argv) — 명령줄 인자를 처리하고 Ruby 소스를 컴파일. 컴파일된 소스 포인터 또는 특수값 반환
  • int ruby_run_node(void *n) — 컴파일된 코드 실행. 성공 시 EXIT_SUCCESS, 에러 시 그 외
  • void ruby_script(char *name) — Ruby 스크립트명($0) 설정

인터프리터 이벤트 훅

  • void rb_add_event_hook(rb_event_hook_func_t func, rb_event_flag_t events, VALUE data) — 지정 인터프리터 이벤트에 훅 함수 추가. eventsRUBY_EVENT_LINE, RUBY_EVENT_CLASS, RUBY_EVENT_END, RUBY_EVENT_CALL, RUBY_EVENT_RETURN, RUBY_EVENT_C_CALL, RUBY_EVENT_C_RETURN, RUBY_EVENT_RAISE, RUBY_EVENT_ALL 중 하나의 OR
  • int rb_remove_event_hook(rb_event_hook_func_t func) — 지정 훅 함수 제거

메모리 사용량

  • void rb_gc_adjust_memory_usage(ssize_t diff) — 등록된 외부 메모리 사용량을 조정. GC에 외부 라이브러리가 메모리를 얼마나 쓰는지 알림. 양수 diff는 증가(새 블록 할당 등), 음수는 감소(해제 등). 이 함수는 GC를 일으킬 수 있음

호환성 매크로

  • NORETURN_STYLE_NEW, HAVE_RB_DEFINE_ALLOC_FUNC, HAVE_RB_REG_NEW_STR, HAVE_RB_IO_T, USE_SYMBOL_AS_METHOD_NAME, HAVE_RUBY_*_H, RB_EVENT_HOOKS_HAVE_CALLBACK_DATA

부록 C. extconf.rb에서 쓸 수 있는 함수들

  • have_macro(macro, headers) — 헤더를 include해 매크로 정의 여부 체크
  • have_library(lib, func)func를 정의하는 라이브러리 lib 존재 체크. 성공 시 -llib$libs에 추가하고 true
  • find_library(lib, func, path…)-Lpath를 추가하면서 라이브러리 존재 체크
  • have_func(func, header) — 헤더를 include해 함수 존재 체크. 성공 시 전처리기 매크로 HAVE_{FUNC} 정의
  • have_var(var, header) — 변수 존재 체크. 성공 시 HAVE_{VAR} 정의
  • have_header(header) — 헤더 존재 체크. 성공 시 HAVE_{HEADER_H} 정의(슬래시·점은 언더스코어로 치환)
  • find_header(header, path…)-Ipath를 추가하면서 헤더 존재 체크
  • have_struct_member(type, member[, header[, opt]]) — 타입·멤버 존재 체크. 성공 시 HAVE_{TYPE}_{MEMBER} 정의
  • have_type(type, header, opt) — 타입 존재 체크. 성공 시 HAVE_TYPE_{TYPE} 정의
  • check_sizeof(type, header) — 타입의 char 단위 크기 확인. 성공 시 SIZEOF_{TYPE} 정의 후 크기 반환, 없으면 nil
  • append_cppflags(array-of-flags[, opt]) / append_cflags(...) / append_ldflags(...) — 각 플래그가 사용 가능하면 $CPPFLAGS/$CFLAGS/$LDFLAGS에 추가. 컴파일러 플래그는 이식성이 없으므로 변수에 직접 추가하지 말고 이 함수들을 쓰는 게 좋음
  • create_makefile(target[, target_prefix]) — 확장 라이브러리용 Makefile 생성. 이 함수를 호출하지 않으면 라이브러리는 컴파일되지 않음
  • find_executable(command, path)commandFile::PATH_SEPARATOR로 구분된 경로 목록에서 탐색. 찾으면 경로 포함 파일명, 없으면 nil
  • with_config(withval[, default=nil]) — 명령줄 --with-<withval> 옵션값 얻기
  • enable_config(config, *defaults) / disable_config(config, *defaults)--enable-<config>/--disable-<config>의 진릿값 얻기
  • dir_config(target[, default_dir])--with-<target>-dir 등으로 지정된 디렉터리를 $CFLAGS/$LDFLAGS에 추가. 추가된 include·lib 디렉터리 배열 반환
  • pkg_config(pkg, option=nil) — pkg-config에서 패키지 정보를 [cflags, ldflags, libs] 배열로 얻어 각각 $CFLAGS, $LDFLAGS, $libs에 추가

부록 D. 세대별 GC

Ruby 2.1부터 세대별 GC(RGenGC)를 지원해요. RGenGC는 기존 확장 라이브러리에 (거의) 호환성을 유지하도록 개발되어, 확장 라이브러리 쪽 대응은 거의 불필요해요. 다만 대응하면 성능을 향상시킬 수 있는 가능성이 있어요. 성능이 필요한 라이브러리라면 대응을 검토해 보세요.

특히 RARRAY_PTR()/RHASH_TBL() 같은 매크로로 포인터에 직접 접근하는 코드는 쓰지 말고, rb_ary_aref(), rb_ary_store() 같은 적절한 API 함수를 사용하세요.

부록 E. Ractor 지원

Ruby 3.0부터 Ruby 프로그램을 병렬 실행하는 Ractor가 도입되었어요. 적절히 병렬 실행하려면 Ractor 지원이 필요해요. 지원하지 않는 라이브러리는 메인 Ractor 이외에서 실행하면 에러(Ractor::UnsafeError)가 발생해요.

Ractor를 지원하는 자세한 내용은 "부록 F. Ractor support"를 참고하세요.