Rustでgemを作ってみた

こんにちは。新卒エンジニアのhansprocsです!

この度2024年8月に行われた大阪Ruby会議04に参加してきました。 とても面白いテーマが多かったですね...業務では触れることのできなかったものも多くRubyのポテンシャルはまだまだあると感じました。

大阪Ruby会議に参加した話については以下記事をご覧ください!

blog.ingage.jp

さて、カンファレンスに参加しただけでは知識が身につかないので、早速手を動かしてみました。 大阪Ruby会議で紹介されたtree-sitter-rbsのようにRustとRubyを繋げたgemを作っています。 (RustとRubyって並べてみると文字の感じも似ていていいですね)

どんなもの?

audirubyを実行すると以下のようなUI画面が開きます。

Start Captureでオーディオの入力を検知し始める形となっています。 入出力の制御は開発中ですが、ひとまずRust側でオーディオの検知ができていることをログとして出しています。 画面にも出ている通り、周波数やコードを検知してチューナーに近いものを見せる予定です。

$ ruby lib/audiruby.rb

Rust library loaded successfully
Ruby: Calling start_audio_capture
Ruby: Calling Rust method _rust_start_audio_capture
Rust: start_audio_capture called
Ruby: start_audio_capture result: "Audio capture started"

なぜRust?

というところで、RustでGemを作る話を聞いて自分で触らずにはいられなくなり、実際にgemの雛形を作ってみました。

私はギターのエフェクターが好きで、前から自分でRubyやJavaScriptを使ってギターの音の入出力を制御してみたいと思っていました。 しかし、少し触ってみた感じだとRubyやJavaScriptの処理速度ではどうしてもレイテンシが生まれてしまい、半分諦めの状態でした。

そこで今回の大阪Ruby会議でRustを使ってRubyのgemが開発できるということを聞き、速く処理して欲しいものはRustに任せ、UIなどはRubyに任せることで今までやってみたかったことが実現できるのではないか、と思いました。

C言語の方が歴史が深く情報が多いかもしれませんが、言語自体のハードルとしてはRustの方が私の目を惹きましたね。

実装

まずは以下が今回作ったgemの雛形になります。 RubyでAudioを制御したい、というところでaudirubyと名づけました。

github.com

まだRust側でオーディオのリアルタイムキャプチャーができていることをRubyが検知しているだけのものになります。

ライブラリー

RubyでRustを呼び出すためにrutieを、GUIを見せるためtkを採用しています。

コード

Rustのコードを呼び出すに、以下のようなことをしています。 AudioProcessor._rust_start_audio_captureのようにオブジェクトに持たせて呼び出している形ですね。

def process(input)
    puts "Ruby: Calling process"
    begin
        result = _rust_process(input)
        puts "Ruby: process result: #{result.inspect}"
        result
    rescue => e
        puts "Ruby: Error in process: #{e.message}"
        nil
    end
end

def _rust_start_audio_capture
    puts "Ruby: Calling Rust method _rust_start_audio_capture"
    # Rustのコードからできたオブジェクトを呼び出す
    AudioProcessor._rust_start_audio_capture
rescue => e
    puts "Error calling Rust method: #{e.message}"
    puts e.backtrace
    raise
end

Rustでは以上の実装のためにlib.rsでこのような実装をしています。

大阪Ruby会議のスピーチでもあった話ですが、RStringのようにRustでRubyの型を制御することに非常に苦労しました。 メモリ管理に厳しいRustの特徴の関係で、Rubyと繋がりを持たせることが難しかったですね。

fn ruby_process(input: RString) -> AnyObject {
    println!("Rust: process called with input: {:?}", input);
    match input.map_err(|e| VM::raise_ex(e)) {
        Ok(s) => audio_processor::AudioProcessor::process(s),
        Err(_) => RString::new_utf8("Error processing input").into()
    }
}

fn ruby_start_audio_capture() -> AnyObject {
    println!("Rust: start_audio_capture called");
    audio_processor::AudioProcessor::start_audio_capture()
}

audirubyのこれから

まずはgemとして公開できる最低限の要件として、ギターのチューナーを作るところから始めてみたいと思います。 rust-ttfライブラリーを使ってプーリエ変換した値を元に、現在弾いている音やコードを検知し、Ruby側に渡す。 Rubyでは渡ってきたものをUIで出力してみたいと思っています。

さらに、エフェクターまで実装できたら夢のようなアプリケーションの誕生ですね!