Rust의 struct를 Ruby의 class로 처리
하고 싶은 일
Rust의 struct를 Ruby 클래스에 매핑하여 Ruby 측면에서 대상적으로 조작합니다.
Ruby 응용 프로그램에서 부분적으로 Rust의 실현을 통해 실행에 걸리는 시간을 이동하여 처리를 고속화하는 목적을 가진다.
브리지용 FFI
Ruby-FFI는 Ruby에서 로컬 코드를 호출하는 데 사용되는 라이브러리입니다.
이번에는 이 Ruby에서 Rust를 처리하는 코드를 사용합니다.
루비, 러스트 간 FFI의 기본 활용 방법은 아래 기사 등을 참고하면 쉽게 연상할 수 있다.
Ruby에서 Rust 함수 → 빠른 Qita 사용
FFI 이외의 후보
2019년 가을께 조사 당시 노트.FFI 이외에 Rust와 Ruby 사이의 교량에도 몇 개의 창고가 있다.
Fiddle
표준 라이브러리여기도 좋아 보이지만 FFI가 주는 이미지와 기능적으로 똑같아 정보가 많은 FFI를 사용하기로 했다.
Helix
인터페이스가 괜찮아 보였지만 당시 최신 버전의 Rust(1.33.0)에서 작동하지 않았기 때문에 보류할 수밖에 없었다.(최근에 다시 봤는데 디프리캣드가 됐더라고요...)
rutie / ruru
Rust 측에서 Ruby 객체의 라이브러리를 조작할 수 있으며 이번 목적과 일치하지 않습니다.일반적으로 루티는 개발이 중단된 루루의 포크라고 여긴다.
동작 이미지
샘플로 Rust 측면에 정의된 다음 struct 준비
src/user.rs
pub struct User {
pub id: u64,
pub name: String,
}
impl User {
pub fn get_display(&self) -> String {
format!("id: {}, name: {}", self.id, self.name)
}
}
루비 쪽에서는 학급으로 사용할 수 있다.sample.rb
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의 정의)를 제외하고 특별히 작성해야 할 파일은 다음과 같다.다른 언어에서 Rust 구현을 호출하기 위한 인터페이스 정의
src/lib.rs
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를 사용한 브리지 설치lib/ruby_object_with_rust.rb
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 클래스 설치lib/ruby_object_with_rust/user.rb
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 측에 넘긴 경우 메모리 유출이 무섭다는 점에 유의해야 하는데...
만약 누가 루비 프로그램의 처리를 고속화하고 싶다면 나는 매우 기쁠 것이다.
참고 자료
라이브러리 비교
Reference
이 문제에 관하여(Rust의 struct를 Ruby의 class로 처리), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/koyopro/articles/aed36f4bbed85c텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)