はじめに
先日、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が実装できそうなんですが……。
実際の実装例は以下のリポジトリを参考にしていただければ幸いです。
Fiddle
FiddleはRubyで簡単に共有ライブラリを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で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