C 확장(C Extension)을 작성하면 C 언어에서 Ruby 객체를 다루고 상호작용할 수 있습니다.
성능이 중요한 특정 메서드를 C의 빠른 속도로 최적화하고 싶거나, 기존에 작성된 C 라이브러리를 Ruby에서 사용할 수 있도록 인터페이스를 만들고 싶을 때 확장 작성이 유용합니다.
참고로 이런 작업에는 FFI 모듈이라는 대안도 있습니다.
그럼 지금부터 C 확장을 작성하는 방법을 하나씩 살펴보겠습니다!
첫 번째 C 확장 만들기
먼저 extconf.rb라는 이름의 파일을 만들고 아래 내용을 입력합니다:
require 'mkmf' create_header create_makefile 'foobar'
그다음 ruby extconf.rb 명령을 실행하면 Makefile과 extconf.h 파일이 자동으로 생성됩니다. 이 파일들은 수정하지 않는 것이 좋습니다.
이제 foobar.c 파일을 생성합니다. 이 파일명은 extconf.rb의 create_makefile 'foobar' 구문에서 비롯된 것입니다. 원한다면 변경할 수 있지만, 반드시 실제 .c 파일명과 일치해야 합니다.
foobar.c 안에 다음 코드를 추가합니다:
#include "ruby.h"
#include "extconf.h"
void Init_foobar()
{
// 여기에 C 코드를 작성합니다
}
이것이 C 확장의 기본 골격입니다. 주목할 점은 초기화 함수 이름이 extconf.rb에서 선언한 이름 앞에 Init_을 붙여야 한다는 것입니다. 이 예제에서는 Init_foobar가 됩니다.
make 명령으로 컴파일하면 .so 공유 라이브러리 파일이 생성되며, 이를 Ruby 애플리케이션에서 require로 불러올 수 있습니다.
C로 Ruby 메서드 작성하기
Ruby 코드에서 호출할 수 있는 C 함수를 만들 수 있습니다. Ruby의 내장 클래스와 메서드들도 바로 이 방식으로 동작합니다.
Ruby 메서드가 되는 모든 C 함수는 반드시 VALUE를 반환해야 합니다. VALUE란 Ruby 객체를 나타내는 타입입니다.
예제:
VALUE rb_return_nil() {
return Qnil;
}
이제 이 함수를 Ruby 클래스나 모듈에 연결해야 합니다. 새로운 클래스를 만들거나 기존 것을 사용할 수 있습니다.
여기서는 모듈을 하나 만들어 보겠습니다:
void Init_foobar() {
VALUE mod = rb_define_module("RubyGuides");
rb_define_method(mod, "return_nil", rb_return_nil, 0);
}
rb_define_method 함수를 사용하면 C 함수를 해당 모듈에 연결할 수 있습니다.
인수는 다음과 같습니다:
| 인수 | 설명 |
|---|---|
| mod | 모듈 객체 |
| "return_nil" | Ruby 메서드의 이름 |
| rb_return_nil | C 함수의 이름 |
| 0 | 메서드가 받는 인수 개수, 가변 인수일 경우 -1 사용 |
다시 make로 컴파일한 후, 아래처럼 실행해 보세요:
require 'foobar' include RubyGuides return_nil
성공입니다! 방금 Ruby에서 C 함수를 호출했습니다.
C에서 Ruby 해시(Hash) 만들기
C 코드에서 Ruby 객체를 생성하고 조작해야 하는 경우가 자주 있습니다.
이럴 때는 C API를 활용하면 됩니다.
예를 들어 해시를 만들고 요소를 추가하는 코드는 다음과 같습니다:
VALUE create_hash() {
hash = rb_hash_new();
rb_hash_aset(hash, rb_str_new2("test"), INT2FIX(1));
return hash;
}
여기서 중요한 부분 두 가지를 짚어보겠습니다. 첫째, rb_str_new2 함수는 Ruby 문자열을 생성합니다.
둘째, INT2FIX 매크로는 일반 정수를 Ruby 정수로 변환합니다.
이 함수를 rb_define_method로 Ruby에 노출하면, test라는 키와 값 1을 가진 일반적인 Ruby 해시를 얻게 됩니다.
코드를 수정할 때마다 확장을 다시 컴파일하는 것을 잊지 마세요 🙂
문자열 다루기
C 함수는 매개변수도 받을 수 있습니다.
예를 들어 다음과 같습니다:
VALUE rb_print_length(VALUE self, VALUE str) {
if (RB_TYPE_P(str, T_STRING) == 1) {
return rb_sprintf("String length: %ld", RSTRING_LEN(str));
}
return Qnil;
}
void Init_foobar()
{
rb_define_global_function("print_length", rb_print_length, 1);
}
여기서는 VALUE(Ruby 객체)를 인수로 받은 후, RB_TYPE_P 매크로로 해당 객체가 문자열인지 확인합니다. 그다음 RSTRING_LEN 매크로로 문자열 길이를 출력합니다.
문자열 데이터에 대한 포인터가 필요하다면 RSTRING_PTR 매크로를 사용하세요.
예제:
VALUE rb_print_data(VALUE self, VALUE str) {
if (RB_TYPE_P(str, T_STRING) == 1) {
return rb_sprintf("String data: %s", RSTRING_PTR(str));
}
return Qnil;
}
또한 위 함수에는 self라는 또 다른 매개변수가 있는데, 이것은 메서드 호출을 받는 객체 자신입니다.
이 메서드를 String 클래스 자체에 정의하고 싶다면 다음과 같이 하면 됩니다:
void Init_foobar()
{
rb_define_method(rb_cString, "print_length", rb_print_length, 0);
}
마무리
이번 글에서는 Ruby용 C 확장을 작성하는 방법을 배웠습니다. 이를 통해 Ruby 프로그램에서 C의 강력한 성능을 그대로 활용할 수 있습니다. 또한 C 함수를 Ruby에 노출하는 방법과, 문자열·해시 같은 Ruby 객체를 C에서 다루는 방법도 익혔습니다.
C에서 Ruby와 상호작용할 수 있는 더 많은 함수가 필요하다면, 관련 문서가 잘 정리되어 있지 않기 때문에 Ruby 소스 코드나 기존 C 확장 코드를 직접 찾아보며 학습해야 할 수 있습니다.
이 글이 도움이 되었기를 바랍니다! 유익했다면 친구들에게 공유해서 함께 즐겨보세요.