RustのstructをRubyのclassとして扱う
やりたいこと
RustのstructをRubyのクラスにマッピングして、Ruby側からオブジェクト的に操作したい。
Rubyのアプリケーションにおいて、実行に時間がかかる処理を部分的にRustの実装で動かして処理を高速化する目的。
FFIをブリッジに使う
Ruby-FFIはRubyからネイティブコードを呼ぶためのライブラリ。
今回はこれを使ってRubyからRustのコードを扱う。
Ruby・Rust間でのFFIの基本的な利用方法は、以下の記事などを参考にするとイメージがしやすい。
RubyからRustの関数をつかう → はやい - Qiita
FFI以外の候補
2019年秋頃に調査した際のメモ。FFIの他にもRustとRubyの橋渡しに使えるライブラリはいくつか存在するよう。
-
Fiddle
Ruby標準のライブラリ。こちらでも良さそうだったが機能的にはFFIと同じような印象だったので、情報の多そうなFFIを利用することにした。 -
Helix
インターフェースは良さそうだったが、当時最新バージョンのRust(1.33.0)で動作しなかったので見送りに。(最近改めて見てみるとDeprecatedになっていた…。) -
rutie / ruru
Rust側からRubyオブジェクトの操作ができるライブラリで、今回の目的とは合わなかった。rutieは開発の止まってしまったruruのforkかと思われる。
動作イメージ
サンプルとして、Rust側で定義した以下のようなstructを用意して、
pub struct User {
pub id: u64,
pub name: String,
}
impl User {
pub fn get_display(&self) -> String {
format!("id: {}, name: {}", self.id, self.name)
}
}
Ruby側でクラスとして利用できるようにする。
user = User.new(4) # 初期化, idの指定
user.name = 'piyo' # nameの指定
user.display # => 'id: 4, name: piyo'
ソースコード
今回動作させたコードはこちらのリポジトリに置いた。
https://github.com/koyopro/ruby_object_with_rust
READMEにビルドやテストの実行コマンドも記載しているので、動作を見ながら確認できると思う。
連携周りのソースコード
上の項で記載したsrc/user.rs
(struct Userの定義)以外で特筆すべきファイルは以下の3つ。
- 他言語からRustの実装を呼ぶ為のインターフェース定義(src/lib.rs)
- Ruby-FFIを利用したブリッジの実装(lib/ruby_object_with_rust.rb)
- Ruby側のUserクラスの実装(lib/ruby_object_with_rust/user.rb)
この3ファイルについて、注意すべきポイントについてコメントをつけたソースコードをリポジトリから抜粋して以下に載せた。
他の言語からRustの実装を呼び出す用のインターフェース定義
mod user;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use user::User;
// ユーザーを初期化する関数。
// FFI経由でポインタをRuby側に渡すことになるので`*mut User`型の返り値が必要になる。
// `Box::new`でスマートポインタを得た上で、`Box::into_raw`で生ポインタを作っている。
#[no_mangle]
pub extern "C" fn create_user(id: u64) -> *mut User {
return Box::into_raw(Box::new(User {
id,
name: String::new(),
}));
}
// Userのidを取得する関数。
// create_userでRuby側に渡したUserのポインタをここで受け取って処理することになる。
// 生ポインタを操作する処理なので、`unsafe`を使う必要がある。
#[no_mangle]
pub unsafe extern "C" fn get_id(user: *mut User) -> u64 {
let u: &mut User = &mut *user;
return u.id;
}
// Userのnameを更新する関数。
// Ruby側から文字列を受け取るには`*const c_char`型を使う。
// ポインタからStringを生成してnameにセットしている。
// 今回は変換に失敗することが無いとして`unwrap()`を用いているが、状況に応じて要エラーハンドリング。
#[no_mangle]
pub unsafe extern "C" fn set_name(user: *mut User, name: *const c_char) {
let u: &mut User = &mut *user;
u.name = CStr::from_ptr(name).to_str().unwrap().to_string();
}
// Userのget_displayメソッドを呼んで得られた文字列を返す関数。
// set_name関数の引数で文字列を受け取ったのと同じく、文字列を返すときも`*const c_char`型を使う。
// CStringを生成してそのポインタを返している。
#[no_mangle]
pub unsafe extern "C" fn get_display(user: *mut User) -> *const c_char {
let u: &mut User = &mut *user;
let display = u.get_display();
return CString::new(display).unwrap().into_raw();
}
// ポインタからメモリを開放する関数。
// ここまでの関数で、Userや文字列のポインタを外部に渡しているため
// 最終的にそれらを解放するために利用する。
#[no_mangle]
pub unsafe extern "C" fn release(c_ptr: *mut libc::c_void) {
libc::free(c_ptr);
}
Ruby-FFIを利用したブリッジの実装
require 'ffi'
require 'ruby_object_with_rust/user'
module RubyObjectWithRust
extend FFI::Library
# FFIを利用するために、`extend FFI::Library`したモジュールを用意する。
# MacとLinuxではRustをビルドした時に生成されるバイナリの拡張子が異なるので、環境で分岐している。
ext = `uname` =~ /Darwin/ ? 'dylib' : 'so'
ffi_lib File.join(__dir__, '..', "target/release/libruby_object_with_rust.#{ext}")
class AutoPointer < FFI::AutoPointer
def self.release(ptr)
RubyObjectWithRust.release(ptr)
end
end
class Error < StandardError; end
# FFIの機能でRubyのメソッドとRust側で用意したインターフェースを紐付けている。
# 引数は<Ruby側メソッド名, Rust側関数名, 引数の型の配列, 返り値の型>の4つ
# (メソッド名と関数名が一致する場合は一つだけ書けば良い)
# Userを指定している部分は`:pointer`でも動作する。
attach_function :create_user, :create_user, [:int], :pointer
attach_function :get_id, [User], :int
attach_function :set_name, [User, :string], :void
attach_function :get_display, [User], :strptr
attach_function :release, [:pointer], :void
end
Ruby側のUserクラスの実装
module RubyObjectWithRust
#
# Userクラス
#
# FFI::ManagedStructを継承する
class User < FFI::ManagedStruct
layout :user, :pointer
# このクラスの各メソッドではRubyObjectWithRustモジュールで定義したメソッドを使う
# 以下で定義したメソッドを利用してstruct Userのポインタを取得
# attach_function :create_user, :create_user, [:int], :pointer
# super (= FFI::ManagedStruct.new ) には得られたポインタを渡す
def self.new(id)
super(RubyObjectWithRust.create_user(id))
end
# 以下で定義したメソッドを利用してidを取得
# attach_function :get_id, [User], :int
# 引数にUserを指定しているので、selfを渡している
def id
RubyObjectWithRust.get_id(self)
end
# 以下で定義したメソッドを利用してnameを設定
# attach_function :set_name, [User, :string], :void
# new_valueはRubyの文字列で、:stringタイプとしてそのまま渡せる
def name=(new_value)
RubyObjectWithRust.set_name(self, new_value)
end
# 以下で定義したメソッドを利用して文字列を取得
# attach_function :get_display, [User], :strptr
# Rust側の返り値が文字列のポインタなので、:strptrタイプを使う
# resultには文字列、ptrにはポインタが渡ってくる
# AutoPointerを使って、文字列の利用が終わったらGCで処理されるようにしている
def display
result, ptr = RubyObjectWithRust.get_display(self)
RubyObjectWithRust::AutoPointer.new(ptr)
result
end
# UserクラスのインスタンスがGCで開放される際に実行される
# 以下で定義したメソッドを利用してメモリを開放する
# attach_function :release, [:pointer], :void
def self.release(ptr)
RubyObjectWithRust.release(ptr)
end
end
end
所感
今回やりたかったことを実現するためには、FFIについていくつかのページの情報を集めて組み合わせる必要があったので一度整理したいと思っていた(以下に役立ったリンクを貼ったのでFFIを利用する場合は参考にしてみてください)。Rust側でメモリを確保してRuby側に渡す場合にはメモリリークに気をつける必要があるのがちょっと怖い…。
Rubyアプリケーション中の処理を高速化したい誰かの役に立つと嬉しい。
参考
ライブラリ比較
- tildeio/helix: Native Ruby extensions without fear
- danielpclark/rutie: “The Tie Between Ruby and Rust.”
- d-unseductable/ruru: Native Ruby extensions written in Rust
FFI関連
- Ruby-FFIについて調べてみた。(まとめ) - いものやま。
- Examples · ffi/ffi Wiki
- Exposing FFI from the Rust library · svartalf
- RubyからRustの関数をつかう → はやい - Qiita
- Ruby Can Be Faster with a Bit of Rust - SitePoint
- Ruby FFIを使ったエクステンションの作り方 - Boost Your Programming!
- 他言語関数インターフェイス
- Objects - The Rust FFI Omnibus
- String Return Values - The Rust FFI Omnibus
- Pointers · ffi/ffi Wiki
Discussion