RustでRubyのNative Extensionを書く

はじめに

先日、Ruby Issue Trackerに以下のチケットが作成されました。

Feature #18481 Porting YJIT to Rust (request for feedback)

内容としては、昨年末にリリースされたRuby 3.1にマージされているYJITという新しいJITコンパイラをより開発しやすくするためにRustを使いたいというものです。

これはRuby(ここでのRubyはCで実装されたCRubyことMRIを意味します)で使用しているCの規格がC99であり、C99ではJITコンパイラを作成するための十分なツールがないことに端を発しています。

つまりC99でJITコンパイラを作成するには開発者自身のスキルセットなどが要求され、開発が難しい部分もあるということのようです。

その点、Rustでは強力なエラーサポートや型、メモリ安全性などがあるため開発がしやすいということかもしれません。 またほかの言語での実装の場合(たとえばGoなど)、GCなどの機能があるためそれ単体でビルドはできても別の問題を踏む可能性があり、採用が難しいところがあるようです。

こういった経緯などからYJIT自体は今後Rustで実装されていく可能性があります。

また以前にはHelixなどのようなRubyのNative ExtensionをRustで実装するというものも出ています。

そこで今回はRubyのNative ExtensionをCではなく、Rustで書くことについて解説したいと思います。

Cで書くNative Extension

CでRubyのNative Extensionを書く場合、以下のようにInit_hello()という関数内でメソッドやクラスなどを定義します。

#include <stdio.h>
#include <ruby.h>

void hello_world()
{
  printf("Hello World!");
}

void Init_hello()
{
  rb_define_global_function("hello_world", hello_world, 0);
}

このコードをビルドし、hello_world.soという共有ライブラリを作成することでNative Extensionを使えるようになります。

使う場合は以下のようにrequireを使い、作成された共有ライブラリを読み込めば自動的にInit_hello()が実行され、そこに定義されたメソッドが使えるようになります。

require 'hello_world'

hello_world()
# => "Hello World!"と出力される

Rustで書く場合

Rustで書く場合も基本的にはCで書く場合と同じです。メソッドやクラスを定義するInit_hello()を定義すればOKです。

#![allow(non_snake_case)]

extern crate libc;
use std::ffi::CString;

extern {
    fn rb_define_global_function(name: *const libc::c_char, func: extern fn(), argc: libc::c_int);
}

extern fn hello_world() {
    println!("Hello, world!");
}

#[no_mangle]
pub extern fn Init_hello() {
    let c_func_name = CString::new("hello_world").unwrap();
    let argc = 0;

    unsafe {
        rb_define_global_function(c_func_name.as_ptr(), hello_world, argc);
    }
}

あとは上記のコードをビルドして共有ライブラリを作成し、以下のようにRuby側から呼び出せばOKです。

require './hello'

hello_world()
# => "Hello World!"と出力される

なお、実装例としては以下のリポジトリを参考にしていただければと思います。

rust_hello_world_ruby_extension

そのほかのRustでのRuby Native Extension作成方法

Helix

Helixは以前RubyKaigiでも紹介されたRustで簡単にRuby Native Extensionを書くことができるライブラリです。

現在ではDeprecatedになっており、使用することは控えたほうがいいですね……。

Helixでは以下のようにメソッドなどを書くことができます。 (なお、以下のコードはHelixの公式より引用)

ruby! {
    class Console {
        def log(string: String) {
            println!("LOG: {}", string);
        }
    }
}

インスタンスメソッドや特異メソッドなどは定義できるようでしたが、メタクラスやグローバルなメソッドなどは定義できなさそうですね……。

ruru

ruruもHelixと同じくRustでのRuby Native Extensionを簡単に実装できるライブラリです。

以下のようにクラスにメソッドを実装できます。 (なお、以下のコードはruruの公式より引用)

#[macro_use]
extern crate ruru;

use ruru::{Boolean, Class, Object, RString};

methods!(
   RString,
   itself,

   fn string_is_blank() -> Boolean {
       Boolean::new(itself.to_string().chars().all(|c| c.is_whitespace()))
   }
);

#[no_mangle]
pub extern fn initialize_string() {
    Class::from_existing("String").define(|itself| {
        itself.def("blank?", string_is_blank);
    });
}

ただ、こちらは長い間メンテナンスされていないようなのであまり推奨できないですね……。

rutie

rutieもHelixなどと同じくRustでRuby Native Extensionを簡単に実装できるライブラリです。

以下のようにマクロを使い、クラスやメソッドを定義することができます。 (なお、以下のコードはrutieの公式より引用)

#[macro_use]
extern crate rutie;

use rutie::{Class, Object, RString, VM};

class!(RutieExample);

methods!(
    RutieExample,
    _rtself,

    fn pub_reverse(input: RString) -> RString {
        let ruby_string = input.
          map_err(|e| VM::raise_ex(e) ).
          unwrap();

        RString::new_utf8(
          &ruby_string.
          to_string().
          chars().
          rev().
          collect::<String>()
        )
    }
);

#[allow(non_snake_case)]
#[no_mangle]
pub extern "C" fn Init_rutie_ruby_example() {
    Class::new("RutieExample", None).define(|klass| {
        klass.def_self("reverse", pub_reverse);
    });
}

rutieは比較的最近もメンテナンスされているようなので、RustでRuby Native Extensionを作成する際には候補に挙げてもいいかもしれません。

FFI

Rubyの標準ライブラリにあるffiを使うことで比較的簡単にRustで一部の実装を作成することもできます。

たとえば以下のようなRustのコードは

#[no_mangle]
pub extern fn hello_world() {
  println!("Hello world!");
}

// ffi_libメソッドで共有ライブラリを読み込む際にInit関数が必要なため
#[no_mangle]
pub extern fn Init_librust_ffi() {
}

以下のようにffiを使い、呼び出すことができます。

require "ffi"

module Hello
    extend FFI::Library

    ffi_lib "./hello.so"
    attach_function :hello_world, [], :void
end

Hello.hello_world()
# => "Hello World!"と出力される

少しだけ面倒くさいのは、以下の二点です。 - module内でextendを使う必要がある - 専用のInit関数を用意する必要がある

この部分が無ければより簡単にRustでのRuby Native Extensionが実装できそうなんですが……。

実際の実装例は以下のリポジトリを参考にしていただければ幸いです。

rust_ffi_ruby_extension

Fiddle

FiddleRubyで簡単に共有ライブラリをimportし、それをもとにクラスやメソッドなどを定義することができるものです。

例えば以下のようなRustのコードを

#[no_mangle]
extern fn hello_world() {
    println!("Hello World");
}

以下のようなRubyのコードで呼び出し、実行することができます。

require 'fiddle'

libm = Fiddle.dlopen('./hello.so')

hello_world = Fiddle::Function.new(
    libm["hello_world"],
    [],
    :void
)

hello_world.call()
# => "Hello World"と出力される

Init_hello関数が不要なので具体的な処理をRustで書いて、それをRubyのclassやmodule内でメソッドとして使う形でいくのがよさそうです。

Fiddleでの実際の実装は、こちらのリポジトリを参考にしていただければと思います。

rust_fiddle_ruby_extension

おわりに

現状、RustでRuby Native Extensionを書く場合は、rutieかFiddleを使うのが楽でよさそうです。 Rust単体で書く場合は、各コードが多くなり辛くなりそうです。とはいえ、Rust単体で書く方があとあと幅広くカスタマイズが出来そうではありますね。

なので、Rust単体で書くか、ruiteまたはFiddleを使うのが今後のスタンダードになりそうです。

参考記事など

RustだけでRuby native extensionを書く Rubyのネイティブ拡張をRustで作成してgemとして公開した RustのstructをRubyのclassとして扱う Ruby/Rust 連携 (2) 手段 Ruby FFIを使ったエクステンションの作り方 Rust でつくるかんたん Ruby Gem