Rust의 struct를 Ruby의 class로 처리

27407 단어 RubyRustffitech

하고 싶은 일


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)
  • Ruby-FFI를 사용한 브리지 커넥터 설치(lib/ruby object with rust.rb)
  • 루비 측면의 User 클래스 설치(lib/ruby object with rust/user.rb)
  • 이 세 파일에 관해서 나는 창고에서 주의해야 할 점에 대한 평론을 위한 원본 코드를 발췌하여 아래에 열거했다.
    다른 언어에서 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 측에 넘긴 경우 메모리 유출이 무섭다는 점에 유의해야 하는데...
    만약 누가 루비 프로그램의 처리를 고속화하고 싶다면 나는 매우 기쁠 것이다.

    참고 자료


    라이브러리 비교
  • 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 관련
  • 루비-FFI를 찾아봤는데(총괄)-고구마산.
  • Examples · ffi/ffi Wiki
  • Exposing FFI from the Rust library · svartalf
  • Ruby에서 Rust 함수 → 빠른 Qita 사용
  • Ruby Can Be Faster with a Bit of Rust - SitePoint
  • Rubby FFI를 이용한 확장된 제작 방법 - Boost Your Programing!
  • 기타 언어 함수 인터페이스
  • Objects - The Rust FFI Omnibus
  • String Return Values - The Rust FFI Omnibus
  • Pointers · ffi/ffi Wiki
  • 좋은 웹페이지 즐겨찾기